Created shared component (#4580)

## New one on the left
<img width="1184" height="351"
alt="{A4B797C1-E52E-4F90-8EAA-C53CDD0BBB95}"
src="https://github.com/user-attachments/assets/d6cfbc9f-350d-48b9-8ae3-def723b72ad7"
/>
<img width="1144" height="1268"
alt="{4EE3680E-EFF2-4C7E-A12F-1050CA96D687}"
src="https://github.com/user-attachments/assets/a7f4c0bc-67c8-4400-bcad-be68108809e1"
/>
<img width="1114" height="784"
alt="{2811741D-9CEB-47A4-8E7D-CB8CE50B8088}"
src="https://github.com/user-attachments/assets/982dca0f-8505-4f04-b699-7332b1ee81da"
/>

---------

Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
This commit is contained in:
ConnorYoh 2025-10-02 13:10:13 +01:00 committed by GitHub
parent 06b4c147bd
commit 247f82b5a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 158 additions and 116 deletions

View File

@ -0,0 +1,34 @@
import React from "react";
interface ToolIconProps {
icon: React.ReactNode;
opacity?: number;
color?: string;
marginRight?: string;
}
/**
* Shared icon component for consistent tool icon styling across the application.
* Uses the same visual pattern as ToolButton: scaled to 0.8, centered transform, consistent spacing.
*/
export const ToolIcon: React.FC<ToolIconProps> = ({
icon,
opacity = 1,
color = "var(--tools-text-and-icon-color)",
marginRight = "0.5rem"
}) => {
return (
<div
className="tool-button-icon"
style={{
color,
marginRight,
transform: "scale(0.8)",
transformOrigin: "center",
opacity
}}
>
{icon}
</div>
);
};

View File

@ -1,11 +1,12 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Group, Text, ActionIcon, Menu, Box } from '@mantine/core';
import { Group, Text, ActionIcon, Menu, Button, Box } from '@mantine/core';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import { Tooltip } from '../../shared/Tooltip';
import { ToolIcon } from '../../shared/ToolIcon';
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
interface AutomationEntryProps {
@ -50,8 +51,7 @@ export default function AutomationEntry({
const [isHovered, setIsHovered] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);
// Keep item in hovered state if menu is open
const shouldShowHovered = isHovered || isMenuOpen;
const shouldShowMenu = isHovered || isMenuOpen;
// Helper function to resolve tool display names
const getToolDisplayName = (operation: string): string => {
@ -108,134 +108,135 @@ export default function AutomationEntry({
);
};
const renderContent = () => {
if (title) {
// Custom automation with title
return (
<Group gap="md" align="center" justify="flex-start" style={{ width: '100%' }}>
{BadgeIcon && (
<BadgeIcon
style={{
color: keepIconColor ? 'var(--mantine-primary-color-filled)' : 'var(--mantine-color-text)'
}}
/>
)}
<Text size="xs" style={{ flex: 1, textAlign: 'left', color: 'var(--mantine-color-text)' }}>
const buttonContent = (
<>
{BadgeIcon && (
<ToolIcon
icon={<BadgeIcon />}
{...(keepIconColor && { color: 'var(--mantine-primary-color-filled)' })}
/>
)}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', flex: 1, overflow: 'visible' }}>
{title ? (
// Custom automation with title
<Text size="sm" style={{ textAlign: 'left' }}>
{title}
</Text>
</Group>
);
} else {
// Suggested automation showing tool chain
return (
<Group gap="md" align="center" justify="flex-start" style={{ width: '100%' }}>
{BadgeIcon && (
<BadgeIcon
style={{
color: keepIconColor ? 'var(--mantine-primary-color-filled)' : 'var(--mantine-color-text)'
}}
/>
)}
) : (
// Suggested automation showing tool chain
<Group gap="xs" justify="flex-start" style={{ flex: 1 }}>
{operations.map((op, index) => (
<React.Fragment key={`${op}-${index}`}>
<Text size="xs" style={{ color: 'var(--mantine-color-text)' }}>
<Text size="sm">
{getToolDisplayName(op)}
</Text>
{index < operations.length - 1 && (
<Text size="xs" c="dimmed" style={{ color: 'var(--mantine-color-text)' }}>
<Text size="sm" c="dimmed">
</Text>
)}
</React.Fragment>
))}
</Group>
</Group>
);
}
};
)}
</div>
</>
);
const boxContent = (
const wrapperContent = (
<Box
style={{
backgroundColor: shouldShowHovered ? 'var(--mantine-color-gray-1)' : 'transparent',
borderRadius: 'var(--mantine-radius-md)',
transition: 'background-color 0.15s ease',
padding: '0.75rem 1rem',
cursor: 'pointer'
}}
onClick={onClick}
style={{ position: 'relative', width: '100%' }}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<Group gap="md" align="center" justify="space-between" style={{ width: '100%' }}>
<div style={{ flex: 1, display: 'flex', justifyContent: 'flex-start' }}>
{renderContent()}
</div>
<Button
variant="subtle"
onClick={onClick}
size="sm"
radius="md"
fullWidth
justify="flex-start"
className="tool-button"
styles={{
root: {
borderRadius: 0,
color: "var(--tools-text-and-icon-color)",
overflow: 'visible',
backgroundColor: shouldShowMenu ? 'var(--automation-entry-hover-bg)' : undefined,
'&:hover': {
backgroundColor: 'var(--automation-entry-hover-bg)'
}
},
label: { overflow: 'visible' }
}}
>
{buttonContent}
</Button>
{showMenu && (
<Menu
position="bottom-end"
withinPortal
onOpen={() => setIsMenuOpen(true)}
onClose={() => setIsMenuOpen(false)}
>
<Menu.Target>
<ActionIcon
variant="subtle"
c="dimmed"
size="md"
onClick={(e) => e.stopPropagation()}
style={{
position: 'absolute',
right: '0.5rem',
top: '50%',
transform: 'translateY(-50%)',
zIndex: 1,
opacity: shouldShowMenu ? 1 : 0,
transition: 'opacity 0.2s ease',
pointerEvents: shouldShowMenu ? 'auto' : 'none'
}}
>
<MoreVertIcon style={{ fontSize: 20 }} />
</ActionIcon>
</Menu.Target>
{showMenu && (
<Menu
position="bottom-end"
withinPortal
onOpen={() => setIsMenuOpen(true)}
onClose={() => setIsMenuOpen(false)}
>
<Menu.Target>
<ActionIcon
variant="subtle"
c="dimmed"
size="md"
onClick={(e) => e.stopPropagation()}
style={{
opacity: shouldShowHovered ? 1 : 0,
transform: shouldShowHovered ? 'scale(1)' : 'scale(0.8)',
transition: 'opacity 0.3s ease, transform 0.3s ease',
pointerEvents: shouldShowHovered ? 'auto' : 'none'
<Menu.Dropdown>
{onCopy && (
<Menu.Item
leftSection={<ContentCopyIcon style={{ fontSize: 16 }} />}
onClick={(e) => {
e.stopPropagation();
onCopy();
}}
>
<MoreVertIcon style={{ fontSize: 20 }} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
{onCopy && (
<Menu.Item
leftSection={<ContentCopyIcon style={{ fontSize: 16 }} />}
onClick={(e) => {
e.stopPropagation();
onCopy();
}}
>
{t('automate.copyToSaved', 'Copy to Saved')}
</Menu.Item>
)}
{onEdit && (
<Menu.Item
leftSection={<EditIcon style={{ fontSize: 16 }} />}
onClick={(e) => {
e.stopPropagation();
onEdit();
}}
>
{t('edit', 'Edit')}
</Menu.Item>
)}
{onDelete && (
<Menu.Item
leftSection={<DeleteIcon style={{ fontSize: 16 }} />}
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
>
{t('delete', 'Delete')}
</Menu.Item>
)}
</Menu.Dropdown>
</Menu>
)}
</Group>
{t('automate.copyToSaved', 'Copy to Saved')}
</Menu.Item>
)}
{onEdit && (
<Menu.Item
leftSection={<EditIcon style={{ fontSize: 16 }} />}
onClick={(e) => {
e.stopPropagation();
onEdit();
}}
>
{t('edit', 'Edit')}
</Menu.Item>
)}
{onDelete && (
<Menu.Item
leftSection={<DeleteIcon style={{ fontSize: 16 }} />}
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
>
{t('delete', 'Delete')}
</Menu.Item>
)}
</Menu.Dropdown>
</Menu>
)}
</Box>
);
@ -249,9 +250,9 @@ export default function AutomationEntry({
arrow={true}
delay={500}
>
{boxContent}
{wrapperContent}
</Tooltip>
) : (
boxContent
wrapperContent
);
}

View File

@ -2,6 +2,7 @@ import React from 'react';
import { Stack, Text, Divider, Card, Group, Anchor } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { useSuggestedTools } from '../../../hooks/useSuggestedTools';
import { ToolIcon } from '../../shared/ToolIcon';
export function SuggestedToolsSection(): React.ReactElement {
const { t } = useTranslation();
@ -31,7 +32,7 @@ export function SuggestedToolsSection(): React.ReactElement {
style={{ cursor: 'pointer' }}
>
<Group gap="xs">
<IconComponent fontSize="small" />
<ToolIcon icon={<IconComponent />} />
<Text size="sm" fw={500}>
{tool.title}
</Text>

View File

@ -2,6 +2,7 @@ import React from "react";
import { Button } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { Tooltip } from "../../shared/Tooltip";
import { ToolIcon } from "../../shared/ToolIcon";
import { ToolRegistryEntry } from "../../../data/toolsTaxonomy";
import { useToolNavigation } from "../../../hooks/useToolNavigation";
import { handleUnlessSpecialClick } from "../../../utils/clickHandlers";
@ -57,7 +58,10 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
const buttonContent = (
<>
<div className="tool-button-icon" style={{ color: "var(--tools-text-and-icon-color)", marginRight: "0.5rem", transform: "scale(0.8)", transformOrigin: "center", opacity: isUnavailable ? 0.25 : 1 }}>{tool.icon}</div>
<ToolIcon
icon={tool.icon}
opacity={isUnavailable ? 0.25 : 1}
/>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', flex: 1, overflow: 'visible' }}>
<FitText
text={tool.name}
@ -67,9 +71,9 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
style={{ display: 'inline-block', maxWidth: '100%', opacity: isUnavailable ? 0.25 : 1 }}
/>
{matchedSynonym && (
<span style={{
fontSize: '0.75rem',
color: 'var(--mantine-color-dimmed)',
<span style={{
fontSize: '0.75rem',
color: 'var(--mantine-color-dimmed)',
opacity: isUnavailable ? 0.25 : 1,
marginTop: '1px',
overflow: 'visible',

View File

@ -120,6 +120,7 @@
--border-strong: #9ca3af;
--hover-bg: #f9fafb;
--active-bg: #f3f4f6;
--automation-entry-hover-bg: var(--color-gray-100);
/* Icon colors for light mode */
--icon-user-bg: #9CA3AF;
@ -317,6 +318,7 @@
--border-strong: #4b5563;
--hover-bg: #374151;
--active-bg: #4b5563;
--automation-entry-hover-bg: var(--color-gray-200);
/* Icon colors for dark mode */
--icon-user-bg: #2A2F36;