mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-17 13:52:14 +01:00
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:
parent
06b4c147bd
commit
247f82b5a7
34
frontend/src/components/shared/ToolIcon.tsx
Normal file
34
frontend/src/components/shared/ToolIcon.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user