mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-17 13:52:14 +01:00
legacy UI
This commit is contained in:
parent
c9e1b8eec5
commit
667a9b4867
@ -1946,6 +1946,25 @@ viewer.zoomIn=Zoom in
|
||||
# Tool Picker
|
||||
toolPicker.searchPlaceholder=Search tools...
|
||||
toolPicker.noToolsFound=No tools found
|
||||
toolPanel.toggle.legacy=Switch to legacy mode
|
||||
toolPanel.toggle.sidebar=Switch to sidebar mode
|
||||
toolPanel.placeholder=Choose a tool to get started
|
||||
toolPanel.legacy.heading=All tools (legacy view)
|
||||
toolPanel.legacy.tagline=Browse and launch tools while keeping the classic full-width gallery.
|
||||
toolPanel.legacy.descriptionsOn=Showing descriptions
|
||||
toolPanel.legacy.descriptionsOff=Descriptions hidden
|
||||
toolPanel.legacy.noResults=Try adjusting your search or toggle descriptions to find what you need.
|
||||
toolPanel.legacy.matchedSynonym=Matches "{{text}}"
|
||||
toolPanel.modePrompt.title=Choose how you browse tools
|
||||
toolPanel.modePrompt.description=Preview both layouts and decide how you want to explore Stirling PDF tools.
|
||||
toolPanel.modePrompt.sidebarTitle=Advanced sidebar
|
||||
toolPanel.modePrompt.sidebarDescription=Keep tools alongside your workspace for quick switching.
|
||||
toolPanel.modePrompt.recommended=Recommended
|
||||
toolPanel.modePrompt.chooseSidebar=Use advanced sidebar
|
||||
toolPanel.modePrompt.legacyTitle=Legacy fullscreen
|
||||
toolPanel.modePrompt.legacyDescription=Browse every tool in a catalogue that covers the workspace until you pick one.
|
||||
toolPanel.modePrompt.chooseLegacy=Use legacy fullscreen
|
||||
toolPanel.modePrompt.dismiss=Maybe later
|
||||
pageEditor.reset=Reset Changes
|
||||
pageEditor.zoomIn=Zoom In
|
||||
pageEditor.zoomOut=Zoom Out
|
||||
|
||||
@ -176,7 +176,7 @@ export default function RightRail() {
|
||||
}, [currentView]);
|
||||
|
||||
return (
|
||||
<div className="right-rail">
|
||||
<div className="right-rail" data-sidebar="right-rail">
|
||||
<div className="right-rail-inner">
|
||||
{topButtons.length > 0 && (
|
||||
<>
|
||||
@ -481,4 +481,3 @@ export default function RightRail() {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
214
frontend/src/components/tools/LegacyToolList.tsx
Normal file
214
frontend/src/components/tools/LegacyToolList.tsx
Normal file
@ -0,0 +1,214 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Badge, Text } from '@mantine/core';
|
||||
import { Tooltip } from '../shared/Tooltip';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ToolRegistryEntry } from '../../data/toolsTaxonomy';
|
||||
import { ToolId } from '../../types/toolId';
|
||||
import { useToolSections } from '../../hooks/useToolSections';
|
||||
import { getSubcategoryLabel } from '../../data/toolsTaxonomy';
|
||||
import NoToolsFound from './shared/NoToolsFound';
|
||||
import './ToolPanel.css';
|
||||
|
||||
interface LegacyToolListProps {
|
||||
filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>;
|
||||
searchQuery: string;
|
||||
showDescriptions: boolean;
|
||||
selectedToolKey: string | null;
|
||||
matchedTextMap: Map<string, string>;
|
||||
onSelect: (id: ToolId) => void;
|
||||
}
|
||||
|
||||
const LegacyToolList = ({
|
||||
filteredTools,
|
||||
searchQuery,
|
||||
showDescriptions,
|
||||
selectedToolKey,
|
||||
matchedTextMap,
|
||||
onSelect,
|
||||
}: LegacyToolListProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { sections, searchGroups } = useToolSections(filteredTools, searchQuery);
|
||||
|
||||
const tooltipPortalTarget = typeof document !== 'undefined' ? document.body : undefined;
|
||||
|
||||
const subcategoryGroups = useMemo(() => {
|
||||
if (searchQuery.trim().length > 0) {
|
||||
return searchGroups;
|
||||
}
|
||||
const allSection = sections.find(section => section.key === 'all');
|
||||
return allSection ? allSection.subcategories : [];
|
||||
}, [searchGroups, sections, searchQuery]);
|
||||
|
||||
if (subcategoryGroups.length === 0) {
|
||||
return (
|
||||
<div className="tool-panel__legacy-empty">
|
||||
<NoToolsFound />
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('toolPanel.legacy.noResults', 'Try adjusting your search or toggle descriptions to find what you need.')}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const containerClass = showDescriptions
|
||||
? 'tool-panel__legacy-groups tool-panel__legacy-groups--detailed'
|
||||
: 'tool-panel__legacy-groups tool-panel__legacy-groups--compact';
|
||||
|
||||
return (
|
||||
<div className={containerClass}>
|
||||
{subcategoryGroups.map(({ subcategoryId, tools }) => (
|
||||
<section
|
||||
key={subcategoryId}
|
||||
className={`tool-panel__legacy-group ${showDescriptions ? 'tool-panel__legacy-group--detailed' : 'tool-panel__legacy-group--compact'}`}
|
||||
>
|
||||
<header className="tool-panel__legacy-section-header">
|
||||
<Text size="sm" fw={600} tt="uppercase" lts={0.5} c="dimmed">
|
||||
{getSubcategoryLabel(t, subcategoryId)}
|
||||
</Text>
|
||||
<Badge size="sm" variant="light" color="gray">
|
||||
{tools.length}
|
||||
</Badge>
|
||||
</header>
|
||||
|
||||
{showDescriptions ? (
|
||||
<div className="tool-panel__legacy-grid tool-panel__legacy-grid--detailed">
|
||||
{tools.map(({ id, tool }) => {
|
||||
const matchedText = matchedTextMap.get(id);
|
||||
const isSelected = selectedToolKey === id;
|
||||
const isDisabled = !tool.component && !tool.link && id !== 'read' && id !== 'multiTool';
|
||||
|
||||
let iconNode: React.ReactNode = null;
|
||||
if (React.isValidElement<{ style?: React.CSSProperties }>(tool.icon)) {
|
||||
const element = tool.icon as React.ReactElement<{ style?: React.CSSProperties }>;
|
||||
iconNode = React.cloneElement(element, {
|
||||
style: {
|
||||
...(element.props.style || {}),
|
||||
fontSize: '1.75rem',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
iconNode = tool.icon;
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
if (isDisabled) return;
|
||||
if (tool.link) {
|
||||
window.open(tool.link, '_blank', 'noopener,noreferrer');
|
||||
return;
|
||||
}
|
||||
onSelect(id as ToolId);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
className={`tool-panel__legacy-item tool-panel__legacy-item--detailed ${isSelected ? 'tool-panel__legacy-item--selected' : ''}`}
|
||||
onClick={handleClick}
|
||||
aria-disabled={isDisabled}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{tool.icon ? (
|
||||
<span className="tool-panel__legacy-icon" aria-hidden>
|
||||
{iconNode}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="tool-panel__legacy-body">
|
||||
<Text fw={600} size="sm" className="tool-panel__legacy-name">
|
||||
{tool.name}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" className="tool-panel__legacy-description">
|
||||
{tool.description}
|
||||
</Text>
|
||||
{matchedText && (
|
||||
<Text size="xs" c="dimmed" className="tool-panel__legacy-match">
|
||||
{t('toolPanel.legacy.matchedSynonym', 'Matches "{{text}}"', { text: matchedText })}
|
||||
</Text>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="tool-panel__legacy-list">
|
||||
{tools.map(({ id, tool }) => {
|
||||
const matchedText = matchedTextMap.get(id);
|
||||
const isSelected = selectedToolKey === id;
|
||||
const isDisabled = !tool.component && !tool.link && id !== 'read' && id !== 'multiTool';
|
||||
|
||||
let iconNode: React.ReactNode = null;
|
||||
if (React.isValidElement<{ style?: React.CSSProperties }>(tool.icon)) {
|
||||
const element = tool.icon as React.ReactElement<{ style?: React.CSSProperties }>;
|
||||
iconNode = React.cloneElement(element, {
|
||||
style: {
|
||||
...(element.props.style || {}),
|
||||
fontSize: '1.5rem',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
iconNode = tool.icon;
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
if (isDisabled) return;
|
||||
if (tool.link) {
|
||||
window.open(tool.link, '_blank', 'noopener,noreferrer');
|
||||
return;
|
||||
}
|
||||
onSelect(id as ToolId);
|
||||
};
|
||||
|
||||
const baseButton = (
|
||||
<button
|
||||
type="button"
|
||||
className={`tool-panel__legacy-list-item ${isSelected ? 'tool-panel__legacy-list-item--selected' : ''}`}
|
||||
onClick={handleClick}
|
||||
aria-disabled={isDisabled}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{tool.icon ? (
|
||||
<span className="tool-panel__legacy-list-icon" aria-hidden>
|
||||
{iconNode}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="tool-panel__legacy-list-body">
|
||||
<Text fw={600} size="sm" className="tool-panel__legacy-name">
|
||||
{tool.name}
|
||||
</Text>
|
||||
{matchedText && (
|
||||
<Text size="xs" c="dimmed" className="tool-panel__legacy-match">
|
||||
{t('toolPanel.legacy.matchedSynonym', 'Matches "{{text}}"', { text: matchedText })}
|
||||
</Text>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
if (showDescriptions || !tool.description) {
|
||||
return React.cloneElement(baseButton, { key: id });
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
key={id}
|
||||
content={tool.description}
|
||||
position="top"
|
||||
portalTarget={tooltipPortalTarget}
|
||||
arrow
|
||||
delay={80}
|
||||
>
|
||||
{baseButton}
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LegacyToolList;
|
||||
131
frontend/src/components/tools/LegacyToolSurface.tsx
Normal file
131
frontend/src/components/tools/LegacyToolSurface.tsx
Normal file
@ -0,0 +1,131 @@
|
||||
import { useEffect } from 'react';
|
||||
import { ActionIcon, Group, ScrollArea, Switch, Text, Tooltip } from '@mantine/core';
|
||||
import ViewSidebarRoundedIcon from '@mui/icons-material/ViewSidebarRounded';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ToolSearch from './toolPicker/ToolSearch';
|
||||
import LegacyToolList from './LegacyToolList';
|
||||
import { ToolRegistryEntry } from '../../data/toolsTaxonomy';
|
||||
import { ToolId } from '../../types/toolId';
|
||||
import './ToolPanel.css';
|
||||
|
||||
interface LegacyToolSurfaceProps {
|
||||
searchQuery: string;
|
||||
toolRegistry: Record<string, ToolRegistryEntry>;
|
||||
filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>;
|
||||
selectedToolKey: string | null;
|
||||
showDescriptions: boolean;
|
||||
matchedTextMap: Map<string, string>;
|
||||
onSearchChange: (value: string) => void;
|
||||
onSelect: (id: ToolId) => void;
|
||||
onToggleDescriptions: () => void;
|
||||
onExitLegacyMode: () => void;
|
||||
toggleLabel: string;
|
||||
geometry: {
|
||||
left: number;
|
||||
top: number;
|
||||
width: number;
|
||||
height: number;
|
||||
} | null;
|
||||
}
|
||||
|
||||
const LegacyToolSurface = ({
|
||||
searchQuery,
|
||||
toolRegistry,
|
||||
filteredTools,
|
||||
selectedToolKey,
|
||||
showDescriptions,
|
||||
matchedTextMap,
|
||||
onSearchChange,
|
||||
onSelect,
|
||||
onToggleDescriptions,
|
||||
onExitLegacyMode,
|
||||
toggleLabel,
|
||||
geometry,
|
||||
}: LegacyToolSurfaceProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
onExitLegacyMode();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [onExitLegacyMode]);
|
||||
|
||||
const style = geometry
|
||||
? {
|
||||
left: `${geometry.left}px`,
|
||||
top: `${geometry.top}px`,
|
||||
width: `${geometry.width}px`,
|
||||
height: `${geometry.height}px`,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="tool-panel__legacy-surface"
|
||||
style={style}
|
||||
role="region"
|
||||
aria-label={t('toolPanel.legacy.heading', 'All tools (legacy view)')}
|
||||
>
|
||||
<div className="tool-panel__legacy-surface-inner">
|
||||
<header className="tool-panel__legacy-header">
|
||||
<div className="tool-panel__legacy-heading">
|
||||
<Text fw={700} size="lg">
|
||||
{t('toolPanel.legacy.heading', 'All tools (legacy view)')}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('toolPanel.legacy.tagline', 'Browse and launch tools while keeping the classic full-width gallery.')}
|
||||
</Text>
|
||||
</div>
|
||||
<Tooltip label={toggleLabel} position="bottom" withArrow>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
radius="xl"
|
||||
size="lg"
|
||||
onClick={onExitLegacyMode}
|
||||
aria-label={toggleLabel}
|
||||
>
|
||||
<ViewSidebarRoundedIcon fontSize="small" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</header>
|
||||
|
||||
<div className="tool-panel__legacy-controls">
|
||||
<ToolSearch
|
||||
value={searchQuery}
|
||||
onChange={onSearchChange}
|
||||
toolRegistry={toolRegistry}
|
||||
mode="filter"
|
||||
autoFocus
|
||||
/>
|
||||
<Switch
|
||||
checked={showDescriptions}
|
||||
onChange={() => onToggleDescriptions()}
|
||||
size="md"
|
||||
labelPosition="left"
|
||||
label={showDescriptions ? t('toolPanel.legacy.descriptionsOn', 'Showing descriptions') : t('toolPanel.legacy.descriptionsOff', 'Descriptions hidden')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="tool-panel__legacy-body">
|
||||
<ScrollArea className="tool-panel__legacy-scroll" offsetScrollbars>
|
||||
<LegacyToolList
|
||||
filteredTools={filteredTools}
|
||||
searchQuery={searchQuery}
|
||||
showDescriptions={showDescriptions}
|
||||
selectedToolKey={selectedToolKey}
|
||||
matchedTextMap={matchedTextMap}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LegacyToolSurface;
|
||||
351
frontend/src/components/tools/ToolPanel.css
Normal file
351
frontend/src/components/tools/ToolPanel.css
Normal file
@ -0,0 +1,351 @@
|
||||
.tool-panel {
|
||||
position: relative;
|
||||
transition: width 0.3s ease, max-width 0.3s ease;
|
||||
}
|
||||
|
||||
.tool-panel--legacy-active {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.tool-panel__search-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.tool-panel__search-row .search-input-container {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.tool-panel__mode-toggle {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.tool-panel__mode-toggle:hover {
|
||||
transform: scale(1.04);
|
||||
}
|
||||
|
||||
.tool-panel--legacy {
|
||||
background: var(--bg-toolbar);
|
||||
}
|
||||
|
||||
.tool-panel__placeholder {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tool-panel__legacy-surface {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
pointer-events: none;
|
||||
z-index: 1200;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.tool-panel__legacy-surface-inner {
|
||||
pointer-events: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 0 1.25rem 1.25rem 0;
|
||||
background:
|
||||
linear-gradient(
|
||||
140deg,
|
||||
color-mix(in srgb, var(--bg-toolbar) 96%, transparent),
|
||||
color-mix(in srgb, var(--bg-background) 90%, transparent)
|
||||
)
|
||||
padding-box;
|
||||
border: 1px solid color-mix(in srgb, var(--border-subtle) 75%, transparent);
|
||||
box-shadow:
|
||||
0 24px 64px color-mix(in srgb, var(--shadow-color, rgba(15, 23, 42, 0.55)) 25%, transparent),
|
||||
0 6px 18px color-mix(in srgb, var(--shadow-color, rgba(15, 23, 42, 0.35)) 30%, transparent);
|
||||
backdrop-filter: blur(18px);
|
||||
overflow: hidden;
|
||||
animation: tool-panel-legacy-slide 0.28s ease forwards;
|
||||
}
|
||||
|
||||
.tool-panel__legacy-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
padding: 1.25rem 1.75rem;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--border-subtle) 70%, transparent);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
color-mix(in srgb, var(--bg-toolbar) 86%, transparent),
|
||||
transparent 85%
|
||||
);
|
||||
}
|
||||
|
||||
.tool-panel__legacy-heading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.tool-panel__legacy-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.75rem;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--border-subtle) 70%, transparent);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
color-mix(in srgb, var(--bg-toolbar) 84%, transparent),
|
||||
color-mix(in srgb, var(--bg-background) 72%, transparent)
|
||||
);
|
||||
}
|
||||
|
||||
.tool-panel__legacy-controls .search-input-container {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.tool-panel__legacy-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
color-mix(in srgb, var(--bg-background) 86%, transparent),
|
||||
color-mix(in srgb, var(--bg-toolbar) 78%, transparent)
|
||||
);
|
||||
}
|
||||
|
||||
.tool-panel__legacy-scroll {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* legacy group layout */
|
||||
.tool-panel__legacy-groups {
|
||||
padding: 1.5rem 1.75rem;
|
||||
column-width: 18rem;
|
||||
column-gap: 1.5rem;
|
||||
}
|
||||
|
||||
.tool-panel__legacy-groups--compact {
|
||||
column-width: 17rem;
|
||||
}
|
||||
|
||||
.tool-panel__legacy-groups--detailed {
|
||||
column-width: auto;
|
||||
}
|
||||
|
||||
.tool-panel__legacy-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
margin: 0 0 1.5rem;
|
||||
padding: 0.65rem 0.75rem 1rem;
|
||||
border-radius: 1rem;
|
||||
background: color-mix(in srgb, var(--bg-toolbar) 82%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--border-subtle) 65%, transparent);
|
||||
box-shadow: 0 14px 32px color-mix(in srgb, var(--shadow-color, rgba(15, 23, 42, 0.45)) 18%, transparent);
|
||||
break-inside: avoid;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.tool-panel__legacy-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.1rem 0.15rem 0.35rem;
|
||||
}
|
||||
|
||||
.tool-panel__legacy-grid {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.tool-panel__legacy-grid--detailed {
|
||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||
}
|
||||
|
||||
.tool-panel__legacy-item {
|
||||
all: unset;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.75rem;
|
||||
align-items: flex-start;
|
||||
padding: 0.85rem 0.95rem;
|
||||
border: 1px solid color-mix(in srgb, var(--border-subtle) 70%, transparent);
|
||||
border-radius: 0.95rem;
|
||||
background: color-mix(in srgb, var(--bg-toolbar) 88%, transparent);
|
||||
backdrop-filter: blur(6px);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tool-panel__legacy-item:focus-visible {
|
||||
outline: 2px solid var(--accent-primary, var(--mantine-color-pink-6));
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.tool-panel__legacy-item[aria-disabled="true"],
|
||||
.tool-panel__legacy-item:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.tool-panel__legacy-item:hover:not([aria-disabled="true"]):not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
border-color: color-mix(in srgb, var(--accent-primary, var(--mantine-color-pink-6)) 38%, var(--border-subtle));
|
||||
box-shadow: var(--shadow-xl, 0 18px 34px rgba(15, 23, 42, 0.14));
|
||||
}
|
||||
|
||||
.tool-panel__legacy-item--selected {
|
||||
border-color: color-mix(in srgb, var(--accent-primary, var(--mantine-color-pink-6)) 52%, var(--border-subtle));
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-primary, var(--mantine-color-pink-6)) 28%, transparent);
|
||||
}
|
||||
|
||||
.tool-panel__legacy-item--detailed {
|
||||
min-height: 7.5rem;
|
||||
}
|
||||
|
||||
.tool-panel__legacy-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
border-radius: 0.75rem;
|
||||
background: color-mix(in srgb, var(--bg-muted) 75%, transparent);
|
||||
color: color-mix(in srgb, var(--text-primary) 90%, var(--text-muted));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tool-panel__legacy-icon svg {
|
||||
font-size: 1.65rem;
|
||||
}
|
||||
|
||||
.tool-panel__legacy-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tool-panel__legacy-name {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.tool-panel__legacy-description {
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.tool-panel__legacy-match {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.tool-panel__legacy-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.tool-panel__legacy-list-item {
|
||||
all: unset;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.55rem 0.5rem 0.55rem 0.65rem;
|
||||
border-radius: 0.65rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, transform 0.2s ease;
|
||||
background: color-mix(in srgb, var(--bg-toolbar) 86%, transparent);
|
||||
border: 1px solid transparent;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tool-panel__legacy-list-item[aria-disabled="true"],
|
||||
.tool-panel__legacy-list-item:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.tool-panel__legacy-list-item:hover:not([aria-disabled="true"]):not(:disabled),
|
||||
.tool-panel__legacy-list-item--selected {
|
||||
background: color-mix(in srgb, var(--accent-primary, var(--mantine-color-pink-6)) 22%, var(--bg-toolbar));
|
||||
border-color: color-mix(in srgb, var(--accent-primary, var(--mantine-color-pink-6)) 30%, var(--border-subtle));
|
||||
}
|
||||
|
||||
.tool-panel__legacy-list-item--selected {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.tool-panel__legacy-list-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.1rem;
|
||||
height: 2.1rem;
|
||||
border-radius: 0.6rem;
|
||||
background: color-mix(in srgb, var(--bg-muted) 70%, transparent);
|
||||
color: color-mix(in srgb, var(--text-primary) 88%, var(--text-muted));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tool-panel__legacy-list-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.2rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tool-panel__legacy-empty {
|
||||
padding: 2rem 1.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@keyframes tool-panel-legacy-slide {
|
||||
from {
|
||||
transform: translateX(-6%) scaleX(0.85);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0) scaleX(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1440px) {
|
||||
.tool-panel__legacy-content {
|
||||
padding-inline: 1.5rem;
|
||||
}
|
||||
|
||||
.tool-panel__legacy-grid--compact {
|
||||
column-width: 15rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.tool-panel__legacy-controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.tool-panel__legacy-controls .mantine-Switch-root {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.tool-panel__legacy-grid--compact {
|
||||
column-width: 14rem;
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import { useEffect, useMemo, useState, useLayoutEffect } from 'react';
|
||||
import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';
|
||||
import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
|
||||
import ToolPicker from './ToolPicker';
|
||||
@ -6,20 +7,24 @@ import ToolRenderer from './ToolRenderer';
|
||||
import ToolSearch from './toolPicker/ToolSearch';
|
||||
import { useSidebarContext } from "../../contexts/SidebarContext";
|
||||
import rainbowStyles from '../../styles/rainbow.module.css';
|
||||
import { ScrollArea } from '@mantine/core';
|
||||
import { ActionIcon, ScrollArea, Tooltip } from '@mantine/core';
|
||||
import { ToolId } from '../../types/toolId';
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
import ViewSidebarRoundedIcon from '@mui/icons-material/ViewSidebarRounded';
|
||||
import DashboardCustomizeRoundedIcon from '@mui/icons-material/DashboardCustomizeRounded';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import LegacyToolSurface from './LegacyToolSurface';
|
||||
import './ToolPanel.css';
|
||||
|
||||
// No props needed - component uses context
|
||||
|
||||
export default function ToolPanel() {
|
||||
const { t } = useTranslation();
|
||||
const { isRainbowMode } = useRainbowThemeContext();
|
||||
const { sidebarRefs } = useSidebarContext();
|
||||
const { toolPanelRef } = sidebarRefs;
|
||||
const { toolPanelRef, quickAccessRef } = sidebarRefs;
|
||||
const isMobile = useMediaQuery('(max-width: 1024px)');
|
||||
|
||||
|
||||
// Use context-based hooks to eliminate prop drilling
|
||||
const {
|
||||
leftPanelView,
|
||||
isPanelVisible,
|
||||
@ -27,84 +32,235 @@ export default function ToolPanel() {
|
||||
filteredTools,
|
||||
toolRegistry,
|
||||
setSearchQuery,
|
||||
selectedToolKey,
|
||||
handleToolSelect,
|
||||
setPreviewFile,
|
||||
toolPanelMode,
|
||||
setToolPanelMode,
|
||||
setLeftPanelView,
|
||||
} = useToolWorkflow();
|
||||
|
||||
const { selectedToolKey, handleToolSelect } = useToolWorkflow();
|
||||
const { setPreviewFile } = useToolWorkflow();
|
||||
const isLegacyMode = toolPanelMode === 'legacy';
|
||||
const legacyExpanded = isLegacyMode && leftPanelView === 'toolPicker' && !isMobile;
|
||||
const [legacyGeometry, setLegacyGeometry] = useState<{ left: number; top: number; width: number; height: number } | null>(null);
|
||||
|
||||
const LEGACY_DESCRIPTION_STORAGE_KEY = 'legacyToolDescriptions';
|
||||
const [showLegacyDescriptions, setShowLegacyDescriptions] = useState(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const stored = window.localStorage.getItem(LEGACY_DESCRIPTION_STORAGE_KEY);
|
||||
if (stored === null) {
|
||||
return true;
|
||||
}
|
||||
return stored === 'true';
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
window.localStorage.setItem(LEGACY_DESCRIPTION_STORAGE_KEY, String(showLegacyDescriptions));
|
||||
}, [showLegacyDescriptions]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!legacyExpanded) {
|
||||
setLegacyGeometry(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const panelEl = toolPanelRef.current;
|
||||
if (!panelEl) {
|
||||
setLegacyGeometry(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const rightRailEl = () => document.querySelector('[data-sidebar="right-rail"]') as HTMLElement | null;
|
||||
|
||||
const updateGeometry = () => {
|
||||
const rect = panelEl.getBoundingClientRect();
|
||||
const rail = rightRailEl();
|
||||
const rightOffset = rail ? Math.max(0, window.innerWidth - rail.getBoundingClientRect().left) : 0;
|
||||
const width = Math.max(360, window.innerWidth - rect.left - rightOffset);
|
||||
const height = Math.max(rect.height, window.innerHeight - rect.top);
|
||||
setLegacyGeometry({
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
};
|
||||
|
||||
updateGeometry();
|
||||
|
||||
const handleResize = () => updateGeometry();
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
resizeObserver = new ResizeObserver(() => updateGeometry());
|
||||
resizeObserver.observe(panelEl);
|
||||
if (quickAccessRef.current) {
|
||||
resizeObserver.observe(quickAccessRef.current);
|
||||
}
|
||||
const rail = rightRailEl();
|
||||
if (rail) {
|
||||
resizeObserver.observe(rail);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
resizeObserver?.disconnect();
|
||||
};
|
||||
}, [legacyExpanded, quickAccessRef, toolPanelRef]);
|
||||
|
||||
const toggleLabel = isLegacyMode
|
||||
? t('toolPanel.toggle.sidebar', 'Switch to sidebar mode')
|
||||
: t('toolPanel.toggle.legacy', 'Switch to legacy mode');
|
||||
|
||||
const handleModeToggle = () => {
|
||||
const nextMode = isLegacyMode ? 'sidebar' : 'legacy';
|
||||
setToolPanelMode(nextMode);
|
||||
|
||||
if (nextMode === 'legacy' && leftPanelView !== 'toolPicker') {
|
||||
setLeftPanelView('toolPicker');
|
||||
}
|
||||
};
|
||||
|
||||
const computedWidth = () => {
|
||||
if (isMobile) {
|
||||
return '100%';
|
||||
}
|
||||
|
||||
if (!isPanelVisible) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
return '18.5rem';
|
||||
};
|
||||
|
||||
const matchedTextMap = useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
filteredTools.forEach(({ item: [id], matchedText }) => {
|
||||
if (matchedText) {
|
||||
map.set(id, matchedText);
|
||||
}
|
||||
});
|
||||
return map;
|
||||
}, [filteredTools]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={toolPanelRef}
|
||||
data-sidebar="tool-panel"
|
||||
className={`flex flex-col overflow-hidden bg-[var(--bg-toolbar)] border-r border-[var(--border-subtle)] transition-all duration-300 ease-out ${
|
||||
className={`tool-panel flex flex-col ${legacyExpanded ? 'tool-panel--legacy-active' : 'overflow-hidden'} bg-[var(--bg-toolbar)] border-r border-[var(--border-subtle)] transition-all duration-300 ease-out ${
|
||||
isRainbowMode ? rainbowStyles.rainbowPaper : ''
|
||||
} ${isMobile ? 'h-full border-r-0' : 'h-screen'}`}
|
||||
} ${isMobile ? 'h-full border-r-0' : 'h-screen'} ${legacyExpanded ? 'tool-panel--legacy' : ''}`}
|
||||
style={{
|
||||
width: isMobile ? '100%' : isPanelVisible ? '18.5rem' : '0',
|
||||
width: computedWidth(),
|
||||
padding: '0'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
opacity: isMobile || isPanelVisible ? 1 : 0,
|
||||
transition: 'opacity 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
{/* Search Bar - Always visible at the top */}
|
||||
{!legacyExpanded && (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'var(--tool-panel-search-bg)',
|
||||
borderBottom: '1px solid var(--tool-panel-search-border-bottom)',
|
||||
padding: '0.75rem 1rem',
|
||||
opacity: isMobile || isPanelVisible ? 1 : 0,
|
||||
transition: 'opacity 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
<ToolSearch
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
toolRegistry={toolRegistry}
|
||||
mode="filter"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="tool-panel__search-row"
|
||||
style={{
|
||||
backgroundColor: 'var(--tool-panel-search-bg)',
|
||||
borderBottom: '1px solid var(--tool-panel-search-border-bottom)'
|
||||
}}
|
||||
>
|
||||
<ToolSearch
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
toolRegistry={toolRegistry}
|
||||
mode="filter"
|
||||
/>
|
||||
{!isMobile && (
|
||||
<Tooltip label={toggleLabel} position="bottom" withArrow>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
radius="xl"
|
||||
color="gray"
|
||||
onClick={handleModeToggle}
|
||||
aria-label={toggleLabel}
|
||||
className="tool-panel__mode-toggle"
|
||||
>
|
||||
{isLegacyMode ? (
|
||||
<ViewSidebarRoundedIcon fontSize="small" />
|
||||
) : (
|
||||
<DashboardCustomizeRoundedIcon fontSize="small" />
|
||||
)}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{searchQuery.trim().length > 0 ? (
|
||||
// Searching view (replaces both picker and content)
|
||||
<div className="flex-1 flex flex-col overflow-y-auto">
|
||||
{searchQuery.trim().length > 0 ? (
|
||||
<div className="flex-1 flex flex-col overflow-y-auto">
|
||||
<SearchResults
|
||||
filteredTools={filteredTools}
|
||||
onSelect={(id) => handleToolSelect(id as ToolId)}
|
||||
searchQuery={searchQuery}
|
||||
/>
|
||||
</div>
|
||||
) : leftPanelView === 'toolPicker' ? (
|
||||
// Tool Picker View
|
||||
<div className="flex-1 flex flex-col overflow-auto">
|
||||
<ToolPicker
|
||||
selectedToolKey={selectedToolKey}
|
||||
onSelect={(id) => handleToolSelect(id as ToolId)}
|
||||
filteredTools={filteredTools}
|
||||
isSearching={Boolean(searchQuery && searchQuery.trim().length > 0)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
// Selected Tool Content View
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Tool content */}
|
||||
<div className="flex-1 min-h-0 overflow-hidden">
|
||||
<ScrollArea h="100%">
|
||||
{selectedToolKey && (
|
||||
<ToolRenderer
|
||||
selectedToolKey={selectedToolKey}
|
||||
onPreviewFile={setPreviewFile}
|
||||
/>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : leftPanelView === 'toolPicker' ? (
|
||||
<div className="flex-1 flex flex-col overflow-auto">
|
||||
<ToolPicker
|
||||
selectedToolKey={selectedToolKey}
|
||||
onSelect={(id) => handleToolSelect(id as ToolId)}
|
||||
filteredTools={filteredTools}
|
||||
isSearching={Boolean(searchQuery && searchQuery.trim().length > 0)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<div className="flex-1 min-h-0 overflow-hidden">
|
||||
<ScrollArea h="100%">
|
||||
{selectedToolKey ? (
|
||||
<ToolRenderer
|
||||
selectedToolKey={selectedToolKey}
|
||||
onPreviewFile={setPreviewFile}
|
||||
/>
|
||||
) : (
|
||||
<div className="tool-panel__placeholder">
|
||||
{t('toolPanel.placeholder', 'Choose a tool to get started')}
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{legacyExpanded && (
|
||||
<LegacyToolSurface
|
||||
searchQuery={searchQuery}
|
||||
toolRegistry={toolRegistry}
|
||||
filteredTools={filteredTools}
|
||||
selectedToolKey={selectedToolKey}
|
||||
showDescriptions={showLegacyDescriptions}
|
||||
matchedTextMap={matchedTextMap}
|
||||
onSearchChange={setSearchQuery}
|
||||
onSelect={(id) => handleToolSelect(id as ToolId)}
|
||||
onToggleDescriptions={() => setShowLegacyDescriptions((prev) => !prev)}
|
||||
onExitLegacyMode={() => setToolPanelMode('sidebar')}
|
||||
toggleLabel={toggleLabel}
|
||||
geometry={legacyGeometry}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
167
frontend/src/components/tools/ToolPanelModePrompt.css
Normal file
167
frontend/src/components/tools/ToolPanelModePrompt.css
Normal file
@ -0,0 +1,167 @@
|
||||
.tool-panel-mode-prompt__modal {
|
||||
background: color-mix(in srgb, var(--bg-toolbar) 94%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--border-subtle) 70%, transparent);
|
||||
box-shadow: 0 32px 64px color-mix(in srgb, var(--shadow-color, rgba(15, 23, 42, 0.55)) 20%, transparent);
|
||||
max-width: min(46rem, 100%);
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__options {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
background: linear-gradient(145deg,
|
||||
color-mix(in srgb, var(--bg-surface) 96%, transparent),
|
||||
color-mix(in srgb, var(--bg-muted) 70%, transparent)
|
||||
);
|
||||
width: 100%;
|
||||
max-width: 19rem;
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__card--sidebar {
|
||||
border: 1px solid color-mix(in srgb, var(--accent-primary, var(--mantine-color-blue-6, #228be6)) 18%, var(--border-subtle));
|
||||
background: linear-gradient(165deg,
|
||||
color-mix(in srgb, var(--bg-surface) 96%, transparent),
|
||||
color-mix(in srgb, var(--accent-primary, var(--mantine-color-blue-6, #228be6)) 8%, transparent)
|
||||
);
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__preview {
|
||||
border-radius: 0.9rem;
|
||||
border: 1px solid color-mix(in srgb, var(--border-subtle) 70%, transparent);
|
||||
background: linear-gradient(135deg, color-mix(in srgb, var(--bg-muted) 82%, transparent), transparent 75%);
|
||||
padding: 0.75rem;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 0.65rem;
|
||||
min-height: 6.5rem;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__preview--sidebar {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__sidebar-panel {
|
||||
width: 3rem;
|
||||
border-radius: 0.65rem;
|
||||
padding: 0.45rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.45rem;
|
||||
background: linear-gradient(180deg,
|
||||
color-mix(in srgb, var(--bg-muted) 88%, transparent),
|
||||
color-mix(in srgb, var(--bg-muted) 72%, transparent)
|
||||
);
|
||||
border: 1px solid color-mix(in srgb, var(--border-subtle) 65%, transparent);
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__sidebar-search {
|
||||
height: 0.5rem;
|
||||
border-radius: 0.4rem;
|
||||
background: color-mix(in srgb, var(--bg-background) 90%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--border-subtle) 60%, transparent);
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__sidebar-item {
|
||||
height: 0.55rem;
|
||||
border-radius: 0.35rem;
|
||||
background: color-mix(in srgb, var(--accent-primary, var(--mantine-color-blue-6, #228be6)) 32%, var(--bg-muted));
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__sidebar-item--muted {
|
||||
background: color-mix(in srgb, var(--bg-background) 88%, transparent);
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__workspace {
|
||||
flex: 1;
|
||||
border-radius: 0.65rem;
|
||||
border: 1px solid color-mix(in srgb, var(--border-subtle) 65%, transparent);
|
||||
padding: 0.5rem;
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
grid-template-rows: 1.4fr 0.6fr;
|
||||
background: linear-gradient(160deg,
|
||||
color-mix(in srgb, var(--bg-background) 94%, transparent),
|
||||
color-mix(in srgb, var(--bg-muted) 68%, transparent)
|
||||
);
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__workspace-page {
|
||||
border-radius: 0.45rem;
|
||||
background: color-mix(in srgb, var(--bg-surface) 96%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--border-subtle) 55%, transparent);
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--bg-background) 60%, transparent);
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__workspace-page--secondary {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__preview--legacy {
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 0.65rem 0.6rem;
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__legacy-columns {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 0.45rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__legacy-column {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__legacy-card {
|
||||
border-radius: 0.45rem;
|
||||
border: 1px solid color-mix(in srgb, var(--border-subtle) 55%, transparent);
|
||||
background: linear-gradient(150deg,
|
||||
color-mix(in srgb, var(--bg-muted) 88%, transparent),
|
||||
color-mix(in srgb, var(--bg-background) 76%, transparent)
|
||||
);
|
||||
height: 1.2rem;
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__legacy-card--muted {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__action {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__action:hover {
|
||||
box-shadow: 0 10px 18px color-mix(in srgb, var(--accent-primary, var(--mantine-color-blue-6, #228be6)) 25%, transparent);
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__maybe-later {
|
||||
color: color-mix(in srgb, var(--text-secondary) 90%, var(--text-muted));
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__maybe-later:hover {
|
||||
background: color-mix(in srgb, var(--bg-muted) 78%, transparent);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.tool-panel-mode-prompt__options {
|
||||
grid-template-columns: 1fr;
|
||||
justify-items: stretch;
|
||||
}
|
||||
}
|
||||
161
frontend/src/components/tools/ToolPanelModePrompt.tsx
Normal file
161
frontend/src/components/tools/ToolPanelModePrompt.tsx
Normal file
@ -0,0 +1,161 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Badge, Button, Card, Group, Modal, Stack, Text } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useToolWorkflow, TOOL_PANEL_MODE_STORAGE_KEY } from '../../contexts/ToolWorkflowContext';
|
||||
import './ToolPanelModePrompt.css';
|
||||
|
||||
type ToolPanelModeOption = 'sidebar' | 'legacy';
|
||||
|
||||
const PROMPT_SEEN_KEY = 'toolPanelModePromptSeen';
|
||||
|
||||
const ToolPanelModePrompt = () => {
|
||||
const { t } = useTranslation();
|
||||
const { toolPanelMode, setToolPanelMode } = useToolWorkflow();
|
||||
const [opened, setOpened] = useState(false);
|
||||
const [hydrated, setHydrated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasSeenPrompt = window.localStorage.getItem(PROMPT_SEEN_KEY);
|
||||
const storedPreference = window.localStorage.getItem(TOOL_PANEL_MODE_STORAGE_KEY);
|
||||
|
||||
if (!hasSeenPrompt && !storedPreference) {
|
||||
setOpened(true);
|
||||
}
|
||||
|
||||
setHydrated(true);
|
||||
}, []);
|
||||
|
||||
const persistSeen = () => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
window.localStorage.setItem(PROMPT_SEEN_KEY, 'true');
|
||||
};
|
||||
|
||||
const handleSelect = (mode: ToolPanelModeOption) => {
|
||||
setToolPanelMode(mode);
|
||||
persistSeen();
|
||||
setOpened(false);
|
||||
};
|
||||
|
||||
const handleDismiss = () => {
|
||||
persistSeen();
|
||||
setOpened(false);
|
||||
};
|
||||
|
||||
if (!hydrated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={handleDismiss}
|
||||
centered
|
||||
size="xl"
|
||||
radius="lg"
|
||||
overlayProps={{ blur: 6, opacity: 0.35 }}
|
||||
classNames={{ content: 'tool-panel-mode-prompt__modal' }}
|
||||
title={t('toolPanel.modePrompt.title', 'Choose how you browse tools')}
|
||||
>
|
||||
<Stack gap="lg">
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('toolPanel.modePrompt.description', 'Preview both layouts and decide how you want to explore Stirling PDF tools.')}
|
||||
</Text>
|
||||
<div className="tool-panel-mode-prompt__options">
|
||||
<Card withBorder radius="lg" shadow="sm" padding="lg" className="tool-panel-mode-prompt__card tool-panel-mode-prompt__card--sidebar">
|
||||
<Stack gap="md" className="tool-panel-mode-prompt__card-content">
|
||||
<Group justify="space-between">
|
||||
<Stack gap={2}>
|
||||
<Text fw={600}>{t('toolPanel.modePrompt.sidebarTitle', 'Advanced sidebar')}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('toolPanel.modePrompt.sidebarDescription', 'Keep tools alongside your workspace for quick switching.')}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Badge color="blue" variant="filled">
|
||||
{t('toolPanel.modePrompt.recommended', 'Recommended')}
|
||||
</Badge>
|
||||
</Group>
|
||||
<div className="tool-panel-mode-prompt__preview tool-panel-mode-prompt__preview--sidebar" aria-hidden>
|
||||
<div className="tool-panel-mode-prompt__sidebar-panel">
|
||||
<span className="tool-panel-mode-prompt__sidebar-search" />
|
||||
<span className="tool-panel-mode-prompt__sidebar-item" />
|
||||
<span className="tool-panel-mode-prompt__sidebar-item" />
|
||||
<span className="tool-panel-mode-prompt__sidebar-item" />
|
||||
<span className="tool-panel-mode-prompt__sidebar-item tool-panel-mode-prompt__sidebar-item--muted" />
|
||||
</div>
|
||||
<div className="tool-panel-mode-prompt__workspace" aria-hidden>
|
||||
<div className="tool-panel-mode-prompt__workspace-page" />
|
||||
<div className="tool-panel-mode-prompt__workspace-page tool-panel-mode-prompt__workspace-page--secondary" />
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant={toolPanelMode === 'sidebar' ? 'filled' : 'light'}
|
||||
color="blue"
|
||||
radius="md"
|
||||
className="tool-panel-mode-prompt__action"
|
||||
onClick={() => handleSelect('sidebar')}
|
||||
>
|
||||
{t('toolPanel.modePrompt.chooseSidebar', 'Use advanced sidebar')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Card>
|
||||
<Card withBorder radius="lg" shadow="xs" padding="lg" className="tool-panel-mode-prompt__card">
|
||||
<Stack gap="md" className="tool-panel-mode-prompt__card-content">
|
||||
<Stack gap={2}>
|
||||
<Text fw={600}>{t('toolPanel.modePrompt.legacyTitle', 'Legacy fullscreen')}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('toolPanel.modePrompt.legacyDescription', 'Browse every tool in a catalogue that covers the workspace until you pick one.')}
|
||||
</Text>
|
||||
</Stack>
|
||||
<div className="tool-panel-mode-prompt__preview tool-panel-mode-prompt__preview--legacy" aria-hidden>
|
||||
<div className="tool-panel-mode-prompt__legacy-columns">
|
||||
<div className="tool-panel-mode-prompt__legacy-column">
|
||||
<span className="tool-panel-mode-prompt__legacy-card" />
|
||||
<span className="tool-panel-mode-prompt__legacy-card" />
|
||||
<span className="tool-panel-mode-prompt__legacy-card tool-panel-mode-prompt__legacy-card--muted" />
|
||||
</div>
|
||||
<div className="tool-panel-mode-prompt__legacy-column">
|
||||
<span className="tool-panel-mode-prompt__legacy-card" />
|
||||
<span className="tool-panel-mode-prompt__legacy-card" />
|
||||
<span className="tool-panel-mode-prompt__legacy-card tool-panel-mode-prompt__legacy-card--muted" />
|
||||
</div>
|
||||
<div className="tool-panel-mode-prompt__legacy-column">
|
||||
<span className="tool-panel-mode-prompt__legacy-card" />
|
||||
<span className="tool-panel-mode-prompt__legacy-card" />
|
||||
<span className="tool-panel-mode-prompt__legacy-card tool-panel-mode-prompt__legacy-card--muted" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant={toolPanelMode === 'legacy' ? 'filled' : 'outline'}
|
||||
color="blue"
|
||||
radius="md"
|
||||
className="tool-panel-mode-prompt__action"
|
||||
onClick={() => handleSelect('legacy')}
|
||||
>
|
||||
{t('toolPanel.modePrompt.chooseLegacy', 'Use legacy fullscreen')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Card>
|
||||
</div>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
radius="md"
|
||||
className="tool-panel-mode-prompt__maybe-later"
|
||||
onClick={handleDismiss}
|
||||
>
|
||||
{t('toolPanel.modePrompt.dismiss', 'Maybe later')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolPanelModePrompt;
|
||||
@ -14,11 +14,14 @@ import { getDefaultWorkbench } from '../types/workbench';
|
||||
import { filterToolRegistryByQuery } from '../utils/toolSearch';
|
||||
|
||||
// State interface
|
||||
type ToolPanelMode = 'sidebar' | 'legacy';
|
||||
|
||||
interface ToolWorkflowState {
|
||||
// UI State
|
||||
sidebarsVisible: boolean;
|
||||
leftPanelView: 'toolPicker' | 'toolContent' | 'hidden';
|
||||
readerMode: boolean;
|
||||
toolPanelMode: ToolPanelMode;
|
||||
|
||||
// File/Preview State
|
||||
previewFile: File | null;
|
||||
@ -33,13 +36,29 @@ type ToolWorkflowAction =
|
||||
| { type: 'SET_SIDEBARS_VISIBLE'; payload: boolean }
|
||||
| { type: 'SET_LEFT_PANEL_VIEW'; payload: 'toolPicker' | 'toolContent' | 'hidden' }
|
||||
| { type: 'SET_READER_MODE'; payload: boolean }
|
||||
| { type: 'SET_TOOL_PANEL_MODE'; payload: ToolPanelMode }
|
||||
| { type: 'SET_PREVIEW_FILE'; payload: File | null }
|
||||
| { type: 'SET_PAGE_EDITOR_FUNCTIONS'; payload: PageEditorFunctions | null }
|
||||
| { type: 'SET_SEARCH_QUERY'; payload: string }
|
||||
| { type: 'RESET_UI_STATE' };
|
||||
|
||||
// Initial state
|
||||
const initialState: ToolWorkflowState = {
|
||||
export const TOOL_PANEL_MODE_STORAGE_KEY = 'toolPanelModePreference';
|
||||
|
||||
const getStoredToolPanelMode = (): ToolPanelMode => {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'sidebar';
|
||||
}
|
||||
|
||||
const stored = window.localStorage.getItem(TOOL_PANEL_MODE_STORAGE_KEY);
|
||||
if (stored === 'legacy' || stored === 'fullscreen') {
|
||||
return 'legacy';
|
||||
}
|
||||
|
||||
return 'sidebar';
|
||||
};
|
||||
|
||||
const baseState: Omit<ToolWorkflowState, 'toolPanelMode'> = {
|
||||
sidebarsVisible: true,
|
||||
leftPanelView: 'toolPicker',
|
||||
readerMode: false,
|
||||
@ -48,6 +67,11 @@ const initialState: ToolWorkflowState = {
|
||||
searchQuery: '',
|
||||
};
|
||||
|
||||
const createInitialState = (): ToolWorkflowState => ({
|
||||
...baseState,
|
||||
toolPanelMode: getStoredToolPanelMode(),
|
||||
});
|
||||
|
||||
// Reducer
|
||||
function toolWorkflowReducer(state: ToolWorkflowState, action: ToolWorkflowAction): ToolWorkflowState {
|
||||
switch (action.type) {
|
||||
@ -57,6 +81,8 @@ function toolWorkflowReducer(state: ToolWorkflowState, action: ToolWorkflowActio
|
||||
return { ...state, leftPanelView: action.payload };
|
||||
case 'SET_READER_MODE':
|
||||
return { ...state, readerMode: action.payload };
|
||||
case 'SET_TOOL_PANEL_MODE':
|
||||
return { ...state, toolPanelMode: action.payload };
|
||||
case 'SET_PREVIEW_FILE':
|
||||
return { ...state, previewFile: action.payload };
|
||||
case 'SET_PAGE_EDITOR_FUNCTIONS':
|
||||
@ -64,7 +90,11 @@ function toolWorkflowReducer(state: ToolWorkflowState, action: ToolWorkflowActio
|
||||
case 'SET_SEARCH_QUERY':
|
||||
return { ...state, searchQuery: action.payload };
|
||||
case 'RESET_UI_STATE':
|
||||
return { ...initialState, searchQuery: state.searchQuery }; // Preserve search
|
||||
return {
|
||||
...baseState,
|
||||
toolPanelMode: state.toolPanelMode,
|
||||
searchQuery: state.searchQuery,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@ -82,6 +112,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
|
||||
setSidebarsVisible: (visible: boolean) => void;
|
||||
setLeftPanelView: (view: 'toolPicker' | 'toolContent' | 'hidden') => void;
|
||||
setReaderMode: (mode: boolean) => void;
|
||||
setToolPanelMode: (mode: ToolPanelMode) => void;
|
||||
setPreviewFile: (file: File | null) => void;
|
||||
setPageEditorFunctions: (functions: PageEditorFunctions | null) => void;
|
||||
setSearchQuery: (query: string) => void;
|
||||
@ -113,7 +144,7 @@ interface ToolWorkflowProviderProps {
|
||||
}
|
||||
|
||||
export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
||||
const [state, dispatch] = useReducer(toolWorkflowReducer, initialState);
|
||||
const [state, dispatch] = useReducer(toolWorkflowReducer, undefined, createInitialState);
|
||||
|
||||
// Store reset functions for tools
|
||||
const [toolResetFunctions, setToolResetFunctions] = React.useState<Record<string, () => void>>({});
|
||||
@ -148,6 +179,10 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
||||
dispatch({ type: 'SET_READER_MODE', payload: mode });
|
||||
}, [actions]);
|
||||
|
||||
const setToolPanelMode = useCallback((mode: ToolPanelMode) => {
|
||||
dispatch({ type: 'SET_TOOL_PANEL_MODE', payload: mode });
|
||||
}, []);
|
||||
|
||||
const setPreviewFile = useCallback((file: File | null) => {
|
||||
dispatch({ type: 'SET_PREVIEW_FILE', payload: file });
|
||||
if (file) {
|
||||
@ -163,6 +198,14 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
||||
dispatch({ type: 'SET_SEARCH_QUERY', payload: query });
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
window.localStorage.setItem(TOOL_PANEL_MODE_STORAGE_KEY, state.toolPanelMode);
|
||||
}, [state.toolPanelMode]);
|
||||
|
||||
// Tool reset methods
|
||||
const registerToolReset = useCallback((toolId: string, resetFunction: () => void) => {
|
||||
setToolResetFunctions(prev => ({ ...prev, [toolId]: resetFunction }));
|
||||
@ -261,6 +304,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
||||
setSidebarsVisible,
|
||||
setLeftPanelView,
|
||||
setReaderMode,
|
||||
setToolPanelMode,
|
||||
setPreviewFile,
|
||||
setPageEditorFunctions,
|
||||
setSearchQuery,
|
||||
@ -289,6 +333,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
||||
setSidebarsVisible,
|
||||
setLeftPanelView,
|
||||
setReaderMode,
|
||||
setToolPanelMode,
|
||||
setPreviewFile,
|
||||
setPageEditorFunctions,
|
||||
setSearchQuery,
|
||||
|
||||
@ -16,6 +16,7 @@ import FileManager from "../components/FileManager";
|
||||
import LocalIcon from "../components/shared/LocalIcon";
|
||||
import { useFilesModalContext } from "../contexts/FilesModalContext";
|
||||
import AppConfigModal from "../components/shared/AppConfigModal";
|
||||
import ToolPanelModePrompt from "../components/tools/ToolPanelModePrompt";
|
||||
|
||||
import "./HomePage.css";
|
||||
|
||||
@ -30,7 +31,12 @@ export default function HomePage() {
|
||||
|
||||
const { quickAccessRef } = sidebarRefs;
|
||||
|
||||
const { selectedTool, selectedToolKey, handleToolSelect, handleBackToTools } = useToolWorkflow();
|
||||
const {
|
||||
selectedTool,
|
||||
selectedToolKey,
|
||||
handleToolSelect,
|
||||
handleBackToTools,
|
||||
} = useToolWorkflow();
|
||||
|
||||
const { openFilesModal } = useFilesModalContext();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
@ -126,6 +132,7 @@ export default function HomePage() {
|
||||
|
||||
return (
|
||||
<div className="h-screen overflow-hidden">
|
||||
<ToolPanelModePrompt />
|
||||
{isMobile ? (
|
||||
<div className="mobile-layout">
|
||||
<div className="mobile-toggle">
|
||||
@ -231,8 +238,7 @@ export default function HomePage() {
|
||||
h="100%"
|
||||
className="flex-nowrap flex"
|
||||
>
|
||||
<QuickAccessBar
|
||||
ref={quickAccessRef} />
|
||||
<QuickAccessBar ref={quickAccessRef} />
|
||||
<ToolPanel />
|
||||
<Workbench />
|
||||
<RightRail />
|
||||
|
||||
@ -445,4 +445,11 @@
|
||||
/* Smooth transitions for theme switching */
|
||||
* {
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
}
|
||||
:root {
|
||||
--shadow-color: rgba(15, 23, 42, 0.55);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--shadow-color: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user