mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a234cf1d8 | ||
|
|
41cea1d50a | ||
|
|
28b146f0f4 | ||
|
|
c200e2a189 | ||
|
|
130168e171 | ||
|
|
0740ad946c | ||
|
|
7a13d42116 |
@@ -1958,3 +1958,28 @@ viewer.nextPage=Next Page
|
||||
viewer.pageNavigation=Page Navigation
|
||||
viewer.currentPage=Current Page
|
||||
viewer.totalPages=Total Pages
|
||||
|
||||
# Tool panel view modes
|
||||
toolPanel.modeToggle.sidebar=Switch to advanced sidebar
|
||||
toolPanel.modeToggle.fullscreen=Switch to legacy fullscreen
|
||||
toolPanel.overlay.title=All tools
|
||||
toolPanel.overlay.subtitle=Browse every tool in the legacy fullscreen catalog.
|
||||
toolPanel.overlay.close=Close
|
||||
toolPanel.overlay.totalLabel_one={{count}} tool available
|
||||
toolPanel.overlay.totalLabel_other={{count}} tools available
|
||||
toolPanel.overlay.layoutLabel=Layout
|
||||
toolPanel.overlay.layoutCompact=Compact grid
|
||||
toolPanel.overlay.layoutDetailed=Detailed cards
|
||||
toolPanel.overlay.matchedSynonym=Matches "{{text}}"
|
||||
toolPanel.overlayHint=Select a tool to open it in the workspace.
|
||||
toolPanel.modePrompt.title=Choose your tools view
|
||||
toolPanel.modePrompt.description=Preview both layouts and choose how you want to explore Stirling PDF tools.
|
||||
toolPanel.modePrompt.legacyTitle=Legacy fullscreen
|
||||
toolPanel.modePrompt.legacyBadge=Not recommended
|
||||
toolPanel.modePrompt.legacyDescription=Open a fullscreen catalog of tools that hides the workspace until a tool is chosen.
|
||||
toolPanel.modePrompt.chooseLegacy=Use legacy fullscreen mode
|
||||
toolPanel.modePrompt.advancedTitle=Advanced sidebar
|
||||
toolPanel.modePrompt.advancedBadge=Recommended
|
||||
toolPanel.modePrompt.advancedDescription=Stay in the enhanced sidebar with quick access to tools alongside your workspace.
|
||||
toolPanel.modePrompt.chooseAdvanced=Use advanced sidebar mode
|
||||
toolPanel.modePrompt.dismiss=Maybe later
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
.tool-panel {
|
||||
transition: width 0.35s ease, flex 0.35s ease;
|
||||
}
|
||||
|
||||
.tool-panel--catalog {
|
||||
background: var(--bg-background);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.tool-panel__mode-toggle {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.tool-panel__mode-toggle:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.tool-panel__overlay-hint {
|
||||
padding: 0.5rem 1rem;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
background: var(--bg-background);
|
||||
}
|
||||
@@ -4,22 +4,26 @@ import ToolPicker from './ToolPicker';
|
||||
import SearchResults from './SearchResults';
|
||||
import ToolRenderer from './ToolRenderer';
|
||||
import ToolSearch from './toolPicker/ToolSearch';
|
||||
import ToolPanelOverlay from './ToolPanelOverlay';
|
||||
import { useSidebarContext } from "../../contexts/SidebarContext";
|
||||
import rainbowStyles from '../../styles/rainbow.module.css';
|
||||
import { ScrollArea } from '@mantine/core';
|
||||
import { ActionIcon, Group, ScrollArea, Text, 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 './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 isMobile = useMediaQuery('(max-width: 1024px)');
|
||||
|
||||
|
||||
// Use context-based hooks to eliminate prop drilling
|
||||
const {
|
||||
leftPanelView,
|
||||
isPanelVisible,
|
||||
@@ -27,26 +31,62 @@ export default function ToolPanel() {
|
||||
filteredTools,
|
||||
toolRegistry,
|
||||
setSearchQuery,
|
||||
toolPanelMode,
|
||||
setToolPanelMode,
|
||||
selectedToolKey,
|
||||
handleToolSelect,
|
||||
setPreviewFile,
|
||||
} = useToolWorkflow();
|
||||
|
||||
const { selectedToolKey, handleToolSelect } = useToolWorkflow();
|
||||
const { setPreviewFile } = useToolWorkflow();
|
||||
const isFullscreenMode = toolPanelMode === 'fullscreen';
|
||||
const isCatalogActive = isFullscreenMode && leftPanelView === 'toolPicker' && !isMobile;
|
||||
|
||||
const toggleLabel = isFullscreenMode
|
||||
? t('toolPanel.modeToggle.sidebar', 'Switch to advanced sidebar')
|
||||
: t('toolPanel.modeToggle.fullscreen', 'Switch to legacy fullscreen');
|
||||
|
||||
const handleToggleMode = () => {
|
||||
setToolPanelMode(isFullscreenMode ? 'sidebar' : 'fullscreen');
|
||||
};
|
||||
|
||||
const reservedDesktopWidth = '8.5rem';
|
||||
|
||||
const computedWidth = () => {
|
||||
if (isMobile) {
|
||||
return '100%';
|
||||
}
|
||||
|
||||
if (isFullscreenMode) {
|
||||
if (isCatalogActive) {
|
||||
return `calc(100vw - ${reservedDesktopWidth})`;
|
||||
}
|
||||
|
||||
if (leftPanelView === 'toolContent' && isPanelVisible) {
|
||||
return '20rem';
|
||||
}
|
||||
|
||||
return isPanelVisible ? '20rem' : '0';
|
||||
}
|
||||
|
||||
return isPanelVisible ? '18.5rem' : '0';
|
||||
};
|
||||
|
||||
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 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'} ${isCatalogActive ? 'tool-panel--catalog' : ''}`}
|
||||
style={{
|
||||
width: isMobile ? '100%' : isPanelVisible ? '18.5rem' : '0',
|
||||
padding: '0'
|
||||
width: computedWidth(),
|
||||
maxWidth: isCatalogActive ? `calc(100vw - ${reservedDesktopWidth})` : undefined,
|
||||
padding: '0',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
opacity: isMobile || isPanelVisible ? 1 : 0,
|
||||
opacity: isMobile || isPanelVisible || isCatalogActive ? 1 : 0,
|
||||
transition: 'opacity 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
@@ -54,22 +94,42 @@ export default function ToolPanel() {
|
||||
}}
|
||||
>
|
||||
{/* Search Bar - Always visible at the top */}
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'var(--tool-panel-search-bg)',
|
||||
borderBottom: '1px solid var(--tool-panel-search-border-bottom)',
|
||||
padding: '0.75rem 1rem',
|
||||
}}
|
||||
>
|
||||
<ToolSearch
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
toolRegistry={toolRegistry}
|
||||
mode="filter"
|
||||
/>
|
||||
</div>
|
||||
{!isCatalogActive && (
|
||||
<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="left" withArrow>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
radius="xl"
|
||||
color="gray"
|
||||
onClick={handleToggleMode}
|
||||
aria-label={toggleLabel}
|
||||
className="tool-panel__mode-toggle"
|
||||
>
|
||||
{isFullscreenMode ? (
|
||||
<ViewSidebarRoundedIcon fontSize="small" />
|
||||
) : (
|
||||
<DashboardCustomizeRoundedIcon fontSize="small" />
|
||||
)}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{searchQuery.trim().length > 0 ? (
|
||||
{searchQuery.trim().length > 0 && !isCatalogActive ? (
|
||||
// Searching view (replaces both picker and content)
|
||||
<div className="flex-1 flex flex-col overflow-y-auto">
|
||||
<SearchResults
|
||||
@@ -81,12 +141,27 @@ export default function ToolPanel() {
|
||||
) : 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)}
|
||||
/>
|
||||
{isCatalogActive ? (
|
||||
<ToolPanelOverlay />
|
||||
) : (
|
||||
<>
|
||||
{isFullscreenMode && !isMobile ? (
|
||||
<div className="tool-panel__overlay-hint">
|
||||
<Group gap="xs" justify="center">
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('toolPanel.overlayHint', 'Select a tool to open it in the workspace.')}
|
||||
</Text>
|
||||
</Group>
|
||||
</div>
|
||||
) : null}
|
||||
<ToolPicker
|
||||
selectedToolKey={selectedToolKey}
|
||||
onSelect={(id) => handleToolSelect(id as ToolId)}
|
||||
filteredTools={filteredTools}
|
||||
isSearching={Boolean(searchQuery && searchQuery.trim().length > 0)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// Selected Tool Content View
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
.tool-panel-mode-prompt__previews {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__card {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__preview {
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid var(--border-subtle);
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.9), rgba(240, 240, 255, 0.6));
|
||||
padding: 0.75rem;
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
min-height: 7.5rem;
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__preview--sidebar {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__preview-sidebar {
|
||||
width: 3.25rem;
|
||||
border-radius: 0.5rem;
|
||||
background: linear-gradient(180deg, var(--accent-ghost), rgba(100, 116, 139, 0.35));
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__preview-canvas {
|
||||
flex: 1;
|
||||
border-radius: 0.5rem;
|
||||
background: repeating-linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 255, 255, 0.9),
|
||||
rgba(255, 255, 255, 0.9) 12px,
|
||||
rgba(226, 232, 240, 0.7) 12px,
|
||||
rgba(226, 232, 240, 0.7) 24px
|
||||
);
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__preview--fullscreen {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__preview-grid {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__preview-grid span {
|
||||
display: block;
|
||||
aspect-ratio: 4 / 3;
|
||||
border-radius: 0.5rem;
|
||||
background: linear-gradient(180deg, rgba(79, 70, 229, 0.2), rgba(14, 165, 233, 0.35));
|
||||
}
|
||||
|
||||
.tool-panel-mode-prompt__card--recommended {
|
||||
border: 1px solid rgba(236, 72, 153, 0.4);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
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_KEY } from '../../contexts/ToolWorkflowContext';
|
||||
import './ToolPanelModePrompt.css';
|
||||
|
||||
type ToolPanelModeOption = 'sidebar' | 'fullscreen';
|
||||
|
||||
const PROMPT_STORAGE_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_STORAGE_KEY);
|
||||
const hasStoredPreference = window.localStorage.getItem(TOOL_PANEL_MODE_KEY);
|
||||
if (!hasSeenPrompt && !hasStoredPreference) {
|
||||
setOpened(true);
|
||||
}
|
||||
setHydrated(true);
|
||||
}, []);
|
||||
|
||||
const persistPromptState = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(PROMPT_STORAGE_KEY, 'true');
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
persistPromptState();
|
||||
setOpened(false);
|
||||
};
|
||||
|
||||
const handleSelect = (mode: ToolPanelModeOption) => {
|
||||
setToolPanelMode(mode);
|
||||
persistPromptState();
|
||||
setOpened(false);
|
||||
};
|
||||
|
||||
if (!hydrated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={handleClose}
|
||||
size="lg"
|
||||
centered
|
||||
title={t('toolPanel.modePrompt.title', 'Choose your tools view')}
|
||||
overlayProps={{ blur: 4, opacity: 0.45 }}
|
||||
>
|
||||
<Stack gap="lg">
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('toolPanel.modePrompt.description', 'Preview both layouts and choose how you want to explore Stirling PDF tools.')}
|
||||
</Text>
|
||||
<div className="tool-panel-mode-prompt__previews">
|
||||
<Card shadow="lg" padding="md" radius="lg" withBorder className="tool-panel-mode-prompt__card tool-panel-mode-prompt__card--recommended">
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between" align="center">
|
||||
<Text fw={600}>{t('toolPanel.modePrompt.advancedTitle', 'Advanced sidebar')}</Text>
|
||||
<Badge color="pink" variant="filled">
|
||||
{t('toolPanel.modePrompt.advancedBadge', 'Recommended')}
|
||||
</Badge>
|
||||
</Group>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('toolPanel.modePrompt.advancedDescription', 'Stay in the enhanced sidebar with quick access to tools alongside your workspace.')}
|
||||
</Text>
|
||||
<div className="tool-panel-mode-prompt__preview tool-panel-mode-prompt__preview--sidebar" aria-hidden>
|
||||
<div className="tool-panel-mode-prompt__preview-sidebar" />
|
||||
<div className="tool-panel-mode-prompt__preview-canvas" />
|
||||
</div>
|
||||
<Button
|
||||
variant={toolPanelMode === 'sidebar' ? 'filled' : 'outline'}
|
||||
color="pink"
|
||||
onClick={() => handleSelect('sidebar')}
|
||||
aria-label={t('toolPanel.modePrompt.chooseAdvanced', 'Use advanced sidebar mode')}
|
||||
>
|
||||
{t('toolPanel.modePrompt.chooseAdvanced', 'Use advanced sidebar mode')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Card>
|
||||
<Card shadow="sm" padding="md" radius="lg" withBorder className="tool-panel-mode-prompt__card">
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between" align="center">
|
||||
<Text fw={600}>{t('toolPanel.modePrompt.legacyTitle', 'Legacy fullscreen')}</Text>
|
||||
<Badge color="gray" variant="light">
|
||||
{t('toolPanel.modePrompt.legacyBadge', 'Not recommended')}
|
||||
</Badge>
|
||||
</Group>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('toolPanel.modePrompt.legacyDescription', 'Open a fullscreen catalog of tools that hides the workspace until a tool is chosen.')}
|
||||
</Text>
|
||||
<div className="tool-panel-mode-prompt__preview tool-panel-mode-prompt__preview--fullscreen" aria-hidden>
|
||||
<div className="tool-panel-mode-prompt__preview-grid">
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant={toolPanelMode === 'fullscreen' ? 'filled' : 'outline'}
|
||||
onClick={() => handleSelect('fullscreen')}
|
||||
aria-label={t('toolPanel.modePrompt.chooseLegacy', 'Use legacy fullscreen mode')}
|
||||
>
|
||||
{t('toolPanel.modePrompt.chooseLegacy', 'Use legacy fullscreen mode')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Card>
|
||||
</div>
|
||||
<Button variant="subtle" color="gray" onClick={handleClose} aria-label={t('toolPanel.modePrompt.dismiss', 'Maybe later')}>
|
||||
{t('toolPanel.modePrompt.dismiss', 'Maybe later')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolPanelModePrompt;
|
||||
@@ -0,0 +1,211 @@
|
||||
.tool-panel-overlay {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: var(--bg-background);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.tool-panel-overlay__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
background: var(--bg-toolbar);
|
||||
}
|
||||
|
||||
.tool-panel-overlay__heading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.tool-panel-overlay__search {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
background: var(--bg-toolbar);
|
||||
}
|
||||
|
||||
.tool-panel-overlay__search-input {
|
||||
flex: 1 1 20rem;
|
||||
min-width: 16rem;
|
||||
}
|
||||
|
||||
.tool-panel-overlay__search-input .search-input-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tool-panel-overlay__search-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tool-panel-overlay__layout-toggle .mantine-SegmentedControl-control {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tool-panel-overlay__body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
background: var(--bg-background);
|
||||
}
|
||||
|
||||
.tool-panel-overlay__scroll {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tool-panel-overlay__content {
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.tool-panel-overlay__empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.tool-panel-overlay__section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.tool-panel-overlay__section-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
color: var(--tool-subcategory-text-color);
|
||||
}
|
||||
|
||||
.tool-panel-overlay__grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.tool-panel-overlay__grid--compact {
|
||||
grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
|
||||
}
|
||||
|
||||
.tool-panel-overlay__grid--detailed {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
}
|
||||
|
||||
.tool-panel-overlay__tile-link,
|
||||
.tool-panel-overlay__tile-button {
|
||||
all: unset;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tool-panel-overlay__tile-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tool-panel-overlay__tile-button[aria-disabled="true"],
|
||||
.tool-panel-overlay__tile-link[aria-disabled="true"] {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tool-panel-overlay__tile {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--tool-card-background, var(--bg-elevated));
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 1rem;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
color: var(--text-primary);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.tool-panel-overlay__tile[data-variant="compact"] {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
min-height: 10rem;
|
||||
}
|
||||
|
||||
.tool-panel-overlay__tile[data-variant="detailed"] {
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
min-height: 9rem;
|
||||
}
|
||||
|
||||
.tool-panel-overlay__tile[data-selected="true"] {
|
||||
border-color: var(--accent-primary, var(--primary-border, var(--mantine-color-pink-6)));
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-primary, var(--mantine-color-pink-6)) 20%, transparent);
|
||||
}
|
||||
|
||||
.tool-panel-overlay__tile[data-disabled="true"] {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.tool-panel-overlay__tile:hover:not([data-disabled="true"]) {
|
||||
transform: translateY(-2px);
|
||||
border-color: color-mix(in srgb, var(--accent-primary, var(--mantine-color-pink-6)) 30%, var(--border-subtle));
|
||||
box-shadow: 0 12px 24px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
|
||||
.tool-panel-overlay__tile-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 0.75rem;
|
||||
background: color-mix(in srgb, var(--accent-primary, var(--mantine-color-pink-6)) 10%, transparent);
|
||||
color: var(--tools-text-and-icon-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tool-panel-overlay__tile-icon svg,
|
||||
.tool-panel-overlay__tile-icon span {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.tool-panel-overlay__tile-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.tool-panel-overlay__tile-name {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.tool-panel-overlay__tile-description {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.tool-panel-overlay__tile-match {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.tool-panel-overlay__tile-hotkey {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.tool-panel-overlay__content {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.tool-panel-overlay__search {
|
||||
padding-inline: 1.25rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { ActionIcon, Badge, Group, ScrollArea, SegmentedControl, Text, Tooltip } from '@mantine/core';
|
||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||
import ViewSidebarRoundedIcon from '@mui/icons-material/ViewSidebarRounded';
|
||||
import DashboardCustomizeRoundedIcon from '@mui/icons-material/DashboardCustomizeRounded';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
|
||||
import ToolSearch from './toolPicker/ToolSearch';
|
||||
import { ToolId } from '../../types/toolId';
|
||||
import { useToolSections } from '../../hooks/useToolSections';
|
||||
import NoToolsFound from './shared/NoToolsFound';
|
||||
import ToolPanelOverlayTile from './ToolPanelOverlayTile';
|
||||
import { getSubcategoryLabel } from '../../data/toolsTaxonomy';
|
||||
import './ToolPanelOverlay.css';
|
||||
|
||||
type LayoutVariant = 'compact' | 'detailed';
|
||||
|
||||
const LAYOUT_STORAGE_KEY = 'toolPanelOverlayLayout';
|
||||
|
||||
const getInitialLayout = (): LayoutVariant => {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'detailed';
|
||||
}
|
||||
const stored = window.localStorage.getItem(LAYOUT_STORAGE_KEY);
|
||||
return stored === 'compact' ? 'compact' : 'detailed';
|
||||
};
|
||||
|
||||
export default function ToolPanelOverlay() {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
filteredTools,
|
||||
selectedToolKey,
|
||||
handleToolSelect,
|
||||
toolRegistry,
|
||||
setToolPanelMode,
|
||||
toolPanelMode,
|
||||
setLeftPanelView,
|
||||
} = useToolWorkflow();
|
||||
|
||||
const [layout, setLayout] = useState<LayoutVariant>(getInitialLayout);
|
||||
|
||||
const { sections, searchGroups } = useToolSections(filteredTools, searchQuery);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
window.localStorage.setItem(LAYOUT_STORAGE_KEY, layout);
|
||||
}, [layout]);
|
||||
|
||||
const showSearchResults = useMemo(() => searchQuery.trim().length > 0, [searchQuery]);
|
||||
const totalToolCount = showSearchResults ? filteredTools.length : Object.keys(toolRegistry).length;
|
||||
|
||||
const matchedTextMap = useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
filteredTools.forEach(({ item: [id], matchedText }) => {
|
||||
if (matchedText) {
|
||||
map.set(id, matchedText);
|
||||
}
|
||||
});
|
||||
return map;
|
||||
}, [filteredTools]);
|
||||
|
||||
const subcategoryGroups = useMemo(() => {
|
||||
if (showSearchResults) {
|
||||
return searchGroups;
|
||||
}
|
||||
const allSection = sections.find(section => section.key === 'all');
|
||||
return allSection ? allSection.subcategories : [];
|
||||
}, [searchGroups, sections, showSearchResults]);
|
||||
|
||||
const handleClose = () => {
|
||||
setSearchQuery('');
|
||||
setLeftPanelView(selectedToolKey ? 'toolContent' : 'toolPicker');
|
||||
};
|
||||
|
||||
const toggleLabel = toolPanelMode === 'fullscreen'
|
||||
? t('toolPanel.modeToggle.sidebar', 'Switch to advanced sidebar')
|
||||
: t('toolPanel.modeToggle.fullscreen', 'Switch to legacy fullscreen');
|
||||
|
||||
const layoutLabel = t('toolPanel.overlay.layoutLabel', 'Layout');
|
||||
|
||||
return (
|
||||
<div
|
||||
className="tool-panel-overlay"
|
||||
role="region"
|
||||
aria-label={t('toolPanel.overlay.title', 'All tools')}
|
||||
>
|
||||
<header className="tool-panel-overlay__header">
|
||||
<div className="tool-panel-overlay__heading">
|
||||
<Text fw={600} size="lg">
|
||||
{t('toolPanel.overlay.title', 'All tools')}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('toolPanel.overlay.subtitle', 'Browse every tool in the legacy fullscreen catalog.')}
|
||||
</Text>
|
||||
</div>
|
||||
<Group gap="xs">
|
||||
<Tooltip label={toggleLabel} position="bottom" withArrow>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
radius="xl"
|
||||
size="lg"
|
||||
onClick={() => setToolPanelMode(toolPanelMode === 'fullscreen' ? 'sidebar' : 'fullscreen')}
|
||||
aria-label={toggleLabel}
|
||||
>
|
||||
{toolPanelMode === 'fullscreen' ? (
|
||||
<ViewSidebarRoundedIcon fontSize="small" />
|
||||
) : (
|
||||
<DashboardCustomizeRoundedIcon fontSize="small" />
|
||||
)}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label={t('toolPanel.overlay.close', 'Close')} position="bottom" withArrow>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
radius="xl"
|
||||
size="lg"
|
||||
onClick={handleClose}
|
||||
aria-label={t('toolPanel.overlay.close', 'Close')}
|
||||
>
|
||||
<CloseRoundedIcon fontSize="small" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</header>
|
||||
|
||||
<div className="tool-panel-overlay__search">
|
||||
<div className="tool-panel-overlay__search-input">
|
||||
<ToolSearch
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
toolRegistry={toolRegistry}
|
||||
mode="filter"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="tool-panel-overlay__search-controls">
|
||||
<div className="tool-panel-overlay__layout-toggle">
|
||||
<SegmentedControl
|
||||
value={layout}
|
||||
onChange={value => setLayout(value as LayoutVariant)}
|
||||
size="sm"
|
||||
aria-label={layoutLabel}
|
||||
data={[
|
||||
{ label: t('toolPanel.overlay.layoutCompact', 'Compact grid'), value: 'compact' },
|
||||
{ label: t('toolPanel.overlay.layoutDetailed', 'Detailed cards'), value: 'detailed' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<Badge variant="light" size="lg" radius="sm">
|
||||
{t('toolPanel.overlay.totalLabel', {
|
||||
count: totalToolCount,
|
||||
defaultValue: '{{count}} tools available',
|
||||
})}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="tool-panel-overlay__body">
|
||||
<ScrollArea className="tool-panel-overlay__scroll" offsetScrollbars>
|
||||
<div className="tool-panel-overlay__content">
|
||||
{subcategoryGroups.length === 0 ? (
|
||||
<div className="tool-panel-overlay__empty">
|
||||
<NoToolsFound />
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('toolPanel.overlayHint', 'Select a tool to open it in the workspace.')}
|
||||
</Text>
|
||||
</div>
|
||||
) : (
|
||||
subcategoryGroups.map(({ subcategoryId, tools }) => (
|
||||
<section key={subcategoryId} className="tool-panel-overlay__section">
|
||||
<header className="tool-panel-overlay__section-header">
|
||||
<Text size="sm" fw={600} tt="uppercase" c="dimmed" lts={0.5}>
|
||||
{getSubcategoryLabel(t, subcategoryId)}
|
||||
</Text>
|
||||
<Badge variant="outline" size="xs" radius="sm" color="gray">
|
||||
{tools.length}
|
||||
</Badge>
|
||||
</header>
|
||||
<div
|
||||
className={`tool-panel-overlay__grid tool-panel-overlay__grid--${layout}`}
|
||||
role="list"
|
||||
>
|
||||
{tools.map(({ id: toolId, tool: toolConfig }) => {
|
||||
const matchedText = matchedTextMap.get(toolId);
|
||||
return (
|
||||
<ToolPanelOverlayTile
|
||||
key={toolId}
|
||||
id={toolId}
|
||||
tool={toolConfig}
|
||||
layout={layout}
|
||||
onSelect={(nextId) => handleToolSelect(nextId as ToolId)}
|
||||
isSelected={selectedToolKey === toolId}
|
||||
matchedSynonym={matchedText}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Text } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ToolRegistryEntry } from '../../data/toolsTaxonomy';
|
||||
import { Tooltip } from '../shared/Tooltip';
|
||||
import { useToolNavigation } from '../../hooks/useToolNavigation';
|
||||
import { handleUnlessSpecialClick } from '../../utils/clickHandlers';
|
||||
import { useHotkeys } from '../../contexts/HotkeyContext';
|
||||
import HotkeyDisplay from '../hotkeys/HotkeyDisplay';
|
||||
|
||||
interface ToolPanelOverlayTileProps {
|
||||
id: string;
|
||||
tool: ToolRegistryEntry;
|
||||
layout: 'compact' | 'detailed';
|
||||
onSelect: (id: string) => void;
|
||||
isSelected: boolean;
|
||||
matchedSynonym?: string;
|
||||
}
|
||||
|
||||
const ToolPanelOverlayTile: React.FC<ToolPanelOverlayTileProps> = ({
|
||||
id,
|
||||
tool,
|
||||
layout,
|
||||
onSelect,
|
||||
isSelected,
|
||||
matchedSynonym,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { getToolNavigation } = useToolNavigation();
|
||||
const { hotkeys } = useHotkeys();
|
||||
|
||||
const isUnavailable = !tool.component && !tool.link && id !== 'read' && id !== 'multiTool';
|
||||
const binding = hotkeys[id];
|
||||
|
||||
const navProps = !isUnavailable && !tool.link ? getToolNavigation(id, tool) : null;
|
||||
|
||||
const tooltipContent = useMemo(() => {
|
||||
if (layout !== 'compact') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
|
||||
<span>{tool.description}</span>
|
||||
{binding && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.75rem' }}>
|
||||
<span style={{ color: 'var(--mantine-color-dimmed)', fontWeight: 500 }}>
|
||||
{t('settings.hotkeys.shortcut', 'Shortcut')}
|
||||
</span>
|
||||
<HotkeyDisplay binding={binding} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}, [binding, layout, t, tool.description]);
|
||||
|
||||
const iconNode = useMemo(() => {
|
||||
if (!tool.icon) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (React.isValidElement<{ style?: React.CSSProperties }>(tool.icon)) {
|
||||
const existingStyle = tool.icon.props.style || {};
|
||||
return React.cloneElement(tool.icon, {
|
||||
style: {
|
||||
...existingStyle,
|
||||
fontSize: layout === 'compact' ? '1.75rem' : '2rem',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return tool.icon;
|
||||
}, [layout, tool.icon]);
|
||||
|
||||
const handleSelect = () => {
|
||||
if (isUnavailable) return;
|
||||
if (tool.link) {
|
||||
window.open(tool.link, '_blank', 'noopener,noreferrer');
|
||||
return;
|
||||
}
|
||||
onSelect(id);
|
||||
};
|
||||
|
||||
const handleButtonClick = (event: React.MouseEvent) => {
|
||||
handleUnlessSpecialClick(event, handleSelect);
|
||||
};
|
||||
|
||||
const matchedLine = matchedSynonym
|
||||
? t('toolPanel.overlay.matchedSynonym', 'Matches "{{text}}"', { text: matchedSynonym })
|
||||
: null;
|
||||
|
||||
const content = (
|
||||
<div
|
||||
className="tool-panel-overlay__tile"
|
||||
data-variant={layout}
|
||||
data-selected={isSelected}
|
||||
data-disabled={isUnavailable || undefined}
|
||||
>
|
||||
<div className="tool-panel-overlay__tile-icon" aria-hidden>
|
||||
{iconNode}
|
||||
</div>
|
||||
<div className="tool-panel-overlay__tile-body">
|
||||
<Text fw={600} size="sm" className="tool-panel-overlay__tile-name">
|
||||
{tool.name}
|
||||
</Text>
|
||||
{layout === 'detailed' && (
|
||||
<Text size="sm" c="dimmed" className="tool-panel-overlay__tile-description">
|
||||
{tool.description}
|
||||
</Text>
|
||||
)}
|
||||
{matchedLine && (
|
||||
<Text size="xs" c="dimmed" className="tool-panel-overlay__tile-match">
|
||||
{matchedLine}
|
||||
</Text>
|
||||
)}
|
||||
{layout === 'detailed' && binding && (
|
||||
<div className="tool-panel-overlay__tile-hotkey">
|
||||
<span>{t('settings.hotkeys.shortcut', 'Shortcut')}</span>
|
||||
<HotkeyDisplay binding={binding} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const wrappedContent = layout === 'compact' && tooltipContent ? (
|
||||
<Tooltip content={tooltipContent} position="top" arrow>
|
||||
{content}
|
||||
</Tooltip>
|
||||
) : (
|
||||
content
|
||||
);
|
||||
|
||||
if (navProps) {
|
||||
return (
|
||||
<a
|
||||
href={navProps.href}
|
||||
onClick={navProps.onClick}
|
||||
className="tool-panel-overlay__tile-link"
|
||||
aria-disabled={isUnavailable}
|
||||
data-variant={layout}
|
||||
>
|
||||
{wrappedContent}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
if (tool.link && !isUnavailable) {
|
||||
return (
|
||||
<a
|
||||
href={tool.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={handleButtonClick}
|
||||
className="tool-panel-overlay__tile-link"
|
||||
data-variant={layout}
|
||||
>
|
||||
{wrappedContent}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleButtonClick}
|
||||
className="tool-panel-overlay__tile-button"
|
||||
data-variant={layout}
|
||||
aria-disabled={isUnavailable}
|
||||
>
|
||||
{wrappedContent}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolPanelOverlayTile;
|
||||
@@ -14,11 +14,14 @@ import { getDefaultWorkbench } from '../types/workbench';
|
||||
import { filterToolRegistryByQuery } from '../utils/toolSearch';
|
||||
|
||||
// State interface
|
||||
type ToolPanelMode = 'sidebar' | 'fullscreen';
|
||||
|
||||
interface ToolWorkflowState {
|
||||
// UI State
|
||||
sidebarsVisible: boolean;
|
||||
leftPanelView: 'toolPicker' | 'toolContent' | 'hidden';
|
||||
readerMode: boolean;
|
||||
toolPanelMode: ToolPanelMode;
|
||||
|
||||
// File/Preview State
|
||||
previewFile: File | null;
|
||||
@@ -33,13 +36,25 @@ 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_KEY = 'toolPanelModePreference';
|
||||
|
||||
const getInitialToolPanelMode = (): ToolPanelMode => {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'sidebar';
|
||||
}
|
||||
|
||||
const storedValue = window.localStorage.getItem(TOOL_PANEL_MODE_KEY);
|
||||
return storedValue === 'fullscreen' ? 'fullscreen' : 'sidebar';
|
||||
};
|
||||
|
||||
const baseInitialState: Omit<ToolWorkflowState, 'toolPanelMode'> = {
|
||||
sidebarsVisible: true,
|
||||
leftPanelView: 'toolPicker',
|
||||
readerMode: false,
|
||||
@@ -48,6 +63,11 @@ const initialState: ToolWorkflowState = {
|
||||
searchQuery: '',
|
||||
};
|
||||
|
||||
const createInitialState = (): ToolWorkflowState => ({
|
||||
...baseInitialState,
|
||||
toolPanelMode: getInitialToolPanelMode(),
|
||||
});
|
||||
|
||||
// Reducer
|
||||
function toolWorkflowReducer(state: ToolWorkflowState, action: ToolWorkflowAction): ToolWorkflowState {
|
||||
switch (action.type) {
|
||||
@@ -57,6 +77,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 +86,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 {
|
||||
...baseInitialState,
|
||||
searchQuery: state.searchQuery,
|
||||
toolPanelMode: state.toolPanelMode,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@@ -85,6 +111,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
|
||||
setPreviewFile: (file: File | null) => void;
|
||||
setPageEditorFunctions: (functions: PageEditorFunctions | null) => void;
|
||||
setSearchQuery: (query: string) => void;
|
||||
setToolPanelMode: (mode: ToolPanelMode) => void;
|
||||
|
||||
// Tool Actions
|
||||
selectTool: (toolId: ToolId | null) => void;
|
||||
@@ -113,7 +140,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>>({});
|
||||
@@ -163,6 +190,15 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
||||
dispatch({ type: 'SET_SEARCH_QUERY', payload: query });
|
||||
}, []);
|
||||
|
||||
const setToolPanelMode = useCallback((mode: ToolPanelMode) => {
|
||||
dispatch({ type: 'SET_TOOL_PANEL_MODE', payload: mode });
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
window.localStorage.setItem(TOOL_PANEL_MODE_KEY, state.toolPanelMode);
|
||||
}, [state.toolPanelMode]);
|
||||
|
||||
// Tool reset methods
|
||||
const registerToolReset = useCallback((toolId: string, resetFunction: () => void) => {
|
||||
setToolResetFunctions(prev => ({ ...prev, [toolId]: resetFunction }));
|
||||
@@ -264,6 +300,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
||||
setPreviewFile,
|
||||
setPageEditorFunctions,
|
||||
setSearchQuery,
|
||||
setToolPanelMode,
|
||||
selectTool: actions.setSelectedTool,
|
||||
clearToolSelection: () => actions.setSelectedTool(null),
|
||||
|
||||
@@ -292,6 +329,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
||||
setPreviewFile,
|
||||
setPageEditorFunctions,
|
||||
setSearchQuery,
|
||||
setToolPanelMode,
|
||||
actions.setSelectedTool,
|
||||
registerToolReset,
|
||||
resetTool,
|
||||
|
||||
@@ -166,3 +166,36 @@
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.home-desktop-layout {
|
||||
width: 100%;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.home-desktop-layout__quick,
|
||||
.home-desktop-layout__tool-panel,
|
||||
.home-desktop-layout__workbench,
|
||||
.home-desktop-layout__right-rail {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
transition: max-width 0.35s ease, transform 0.35s ease, opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.home-desktop-layout__quick {
|
||||
max-width: 5rem;
|
||||
}
|
||||
|
||||
.home-desktop-layout__tool-panel {
|
||||
flex: 0 0 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.home-desktop-layout__workbench {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.home-desktop-layout__right-rail {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useMediaQuery } from "@mantine/hooks";
|
||||
import AppsIcon from '@mui/icons-material/AppsRounded';
|
||||
|
||||
import ToolPanel from "../components/tools/ToolPanel";
|
||||
import ToolPanelModePrompt from "../components/tools/ToolPanelModePrompt";
|
||||
import Workbench from "../components/layout/Workbench";
|
||||
import QuickAccessBar from "../components/shared/QuickAccessBar";
|
||||
import RightRail from "../components/shared/RightRail";
|
||||
@@ -30,7 +31,14 @@ export default function HomePage() {
|
||||
|
||||
const { quickAccessRef } = sidebarRefs;
|
||||
|
||||
const { selectedTool, selectedToolKey, handleToolSelect, handleBackToTools } = useToolWorkflow();
|
||||
const {
|
||||
selectedTool,
|
||||
selectedToolKey,
|
||||
handleToolSelect,
|
||||
handleBackToTools,
|
||||
leftPanelView,
|
||||
toolPanelMode,
|
||||
} = useToolWorkflow();
|
||||
|
||||
const { openFilesModal } = useFilesModalContext();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
@@ -126,6 +134,7 @@ export default function HomePage() {
|
||||
|
||||
return (
|
||||
<div className="h-screen overflow-hidden">
|
||||
<ToolPanelModePrompt />
|
||||
{isMobile ? (
|
||||
<div className="mobile-layout">
|
||||
<div className="mobile-toggle">
|
||||
@@ -229,13 +238,22 @@ export default function HomePage() {
|
||||
align="flex-start"
|
||||
gap={0}
|
||||
h="100%"
|
||||
className="flex-nowrap flex"
|
||||
className="flex-nowrap flex home-desktop-layout"
|
||||
>
|
||||
<QuickAccessBar
|
||||
ref={quickAccessRef} />
|
||||
<ToolPanel />
|
||||
<Workbench />
|
||||
<RightRail />
|
||||
<div className="home-desktop-layout__quick">
|
||||
<QuickAccessBar
|
||||
ref={quickAccessRef}
|
||||
/>
|
||||
</div>
|
||||
<div className="home-desktop-layout__tool-panel">
|
||||
<ToolPanel />
|
||||
</div>
|
||||
<div className="home-desktop-layout__workbench">
|
||||
<Workbench />
|
||||
</div>
|
||||
<div className="home-desktop-layout__right-rail">
|
||||
<RightRail />
|
||||
</div>
|
||||
<FileManager selectedTool={selectedTool as any /* FIX ME */} />
|
||||
</Group>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user