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 <[email protected]>
This commit is contained in:
ConnorYoh
2025-10-02 13:10:13 +01:00
committed by GitHub
co-authored by Connor Yoh
parent 06b4c147bd
commit 247f82b5a7
5 changed files with 158 additions and 116 deletions
@@ -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>
);
};
@@ -1,11 +1,12 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; 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 MoreVertIcon from '@mui/icons-material/MoreVert';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import { Tooltip } from '../../shared/Tooltip'; import { Tooltip } from '../../shared/Tooltip';
import { ToolIcon } from '../../shared/ToolIcon';
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy'; import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
interface AutomationEntryProps { interface AutomationEntryProps {
@@ -50,8 +51,7 @@ export default function AutomationEntry({
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
// Keep item in hovered state if menu is open const shouldShowMenu = isHovered || isMenuOpen;
const shouldShowHovered = isHovered || isMenuOpen;
// Helper function to resolve tool display names // Helper function to resolve tool display names
const getToolDisplayName = (operation: string): string => { const getToolDisplayName = (operation: string): string => {
@@ -108,134 +108,135 @@ export default function AutomationEntry({
); );
}; };
const renderContent = () => { const buttonContent = (
if (title) { <>
// Custom automation with title {BadgeIcon && (
return ( <ToolIcon
<Group gap="md" align="center" justify="flex-start" style={{ width: '100%' }}> icon={<BadgeIcon />}
{BadgeIcon && ( {...(keepIconColor && { color: 'var(--mantine-primary-color-filled)' })}
<BadgeIcon />
style={{ )}
color: keepIconColor ? 'var(--mantine-primary-color-filled)' : 'var(--mantine-color-text)' <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', flex: 1, overflow: 'visible' }}>
}} {title ? (
/> // Custom automation with title
)} <Text size="sm" style={{ textAlign: 'left' }}>
<Text size="xs" style={{ flex: 1, textAlign: 'left', color: 'var(--mantine-color-text)' }}>
{title} {title}
</Text> </Text>
</Group> ) : (
); // Suggested automation showing tool chain
} 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)'
}}
/>
)}
<Group gap="xs" justify="flex-start" style={{ flex: 1 }}> <Group gap="xs" justify="flex-start" style={{ flex: 1 }}>
{operations.map((op, index) => ( {operations.map((op, index) => (
<React.Fragment key={`${op}-${index}`}> <React.Fragment key={`${op}-${index}`}>
<Text size="xs" style={{ color: 'var(--mantine-color-text)' }}> <Text size="sm">
{getToolDisplayName(op)} {getToolDisplayName(op)}
</Text> </Text>
{index < operations.length - 1 && ( {index < operations.length - 1 && (
<Text size="xs" c="dimmed" style={{ color: 'var(--mantine-color-text)' }}> <Text size="sm" c="dimmed">
</Text> </Text>
)} )}
</React.Fragment> </React.Fragment>
))} ))}
</Group> </Group>
</Group> )}
); </div>
} </>
}; );
const boxContent = ( const wrapperContent = (
<Box <Box
style={{ style={{ position: 'relative', width: '100%' }}
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}
onMouseEnter={() => setIsHovered(true)} onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(false)}
> >
<Group gap="md" align="center" justify="space-between" style={{ width: '100%' }}> <Button
<div style={{ flex: 1, display: 'flex', justifyContent: 'flex-start' }}> variant="subtle"
{renderContent()} onClick={onClick}
</div> 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.Dropdown>
<Menu {onCopy && (
position="bottom-end" <Menu.Item
withinPortal leftSection={<ContentCopyIcon style={{ fontSize: 16 }} />}
onOpen={() => setIsMenuOpen(true)} onClick={(e) => {
onClose={() => setIsMenuOpen(false)} e.stopPropagation();
> onCopy();
<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'
}} }}
> >
<MoreVertIcon style={{ fontSize: 20 }} /> {t('automate.copyToSaved', 'Copy to Saved')}
</ActionIcon> </Menu.Item>
</Menu.Target> )}
{onEdit && (
<Menu.Dropdown> <Menu.Item
{onCopy && ( leftSection={<EditIcon style={{ fontSize: 16 }} />}
<Menu.Item onClick={(e) => {
leftSection={<ContentCopyIcon style={{ fontSize: 16 }} />} e.stopPropagation();
onClick={(e) => { onEdit();
e.stopPropagation(); }}
onCopy(); >
}} {t('edit', 'Edit')}
> </Menu.Item>
{t('automate.copyToSaved', 'Copy to Saved')} )}
</Menu.Item> {onDelete && (
)} <Menu.Item
{onEdit && ( leftSection={<DeleteIcon style={{ fontSize: 16 }} />}
<Menu.Item onClick={(e) => {
leftSection={<EditIcon style={{ fontSize: 16 }} />} e.stopPropagation();
onClick={(e) => { onDelete();
e.stopPropagation(); }}
onEdit(); >
}} {t('delete', 'Delete')}
> </Menu.Item>
{t('edit', 'Edit')} )}
</Menu.Item> </Menu.Dropdown>
)} </Menu>
{onDelete && ( )}
<Menu.Item
leftSection={<DeleteIcon style={{ fontSize: 16 }} />}
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
>
{t('delete', 'Delete')}
</Menu.Item>
)}
</Menu.Dropdown>
</Menu>
)}
</Group>
</Box> </Box>
); );
@@ -249,9 +250,9 @@ export default function AutomationEntry({
arrow={true} arrow={true}
delay={500} delay={500}
> >
{boxContent} {wrapperContent}
</Tooltip> </Tooltip>
) : ( ) : (
boxContent wrapperContent
); );
} }
@@ -2,6 +2,7 @@ import React from 'react';
import { Stack, Text, Divider, Card, Group, Anchor } from '@mantine/core'; import { Stack, Text, Divider, Card, Group, Anchor } from '@mantine/core';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSuggestedTools } from '../../../hooks/useSuggestedTools'; import { useSuggestedTools } from '../../../hooks/useSuggestedTools';
import { ToolIcon } from '../../shared/ToolIcon';
export function SuggestedToolsSection(): React.ReactElement { export function SuggestedToolsSection(): React.ReactElement {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -31,7 +32,7 @@ export function SuggestedToolsSection(): React.ReactElement {
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
> >
<Group gap="xs"> <Group gap="xs">
<IconComponent fontSize="small" /> <ToolIcon icon={<IconComponent />} />
<Text size="sm" fw={500}> <Text size="sm" fw={500}>
{tool.title} {tool.title}
</Text> </Text>
@@ -2,6 +2,7 @@ import React from "react";
import { Button } from "@mantine/core"; import { Button } from "@mantine/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Tooltip } from "../../shared/Tooltip"; import { Tooltip } from "../../shared/Tooltip";
import { ToolIcon } from "../../shared/ToolIcon";
import { ToolRegistryEntry } from "../../../data/toolsTaxonomy"; import { ToolRegistryEntry } from "../../../data/toolsTaxonomy";
import { useToolNavigation } from "../../../hooks/useToolNavigation"; import { useToolNavigation } from "../../../hooks/useToolNavigation";
import { handleUnlessSpecialClick } from "../../../utils/clickHandlers"; import { handleUnlessSpecialClick } from "../../../utils/clickHandlers";
@@ -57,7 +58,10 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
const buttonContent = ( 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' }}> <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', flex: 1, overflow: 'visible' }}>
<FitText <FitText
text={tool.name} text={tool.name}
+2
View File
@@ -120,6 +120,7 @@
--border-strong: #9ca3af; --border-strong: #9ca3af;
--hover-bg: #f9fafb; --hover-bg: #f9fafb;
--active-bg: #f3f4f6; --active-bg: #f3f4f6;
--automation-entry-hover-bg: var(--color-gray-100);
/* Icon colors for light mode */ /* Icon colors for light mode */
--icon-user-bg: #9CA3AF; --icon-user-bg: #9CA3AF;
@@ -317,6 +318,7 @@
--border-strong: #4b5563; --border-strong: #4b5563;
--hover-bg: #374151; --hover-bg: #374151;
--active-bg: #4b5563; --active-bg: #4b5563;
--automation-entry-hover-bg: var(--color-gray-200);
/* Icon colors for dark mode */ /* Icon colors for dark mode */
--icon-user-bg: #2A2F36; --icon-user-bg: #2A2F36;