feat: implement core application shell with toolbar, sidebar, and tool management components

This commit is contained in:
azeeee05
2026-08-24 15:22:38 +05:30
parent 4256ac8128
commit 2fe27a22d2
4 changed files with 196 additions and 16 deletions
+7 -3
View File
@@ -1,6 +1,6 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { TopBar } from './components/TopBar';
import { ToolRail } from './components/ToolRail';
import { Sidebar } from './components/Sidebar';
import { Toolbar } from './components/Toolbar';
import { InspectorPanel } from './components/InspectorPanel';
import type { InspectorTab } from './components/InspectorPanel';
@@ -65,6 +65,7 @@ function App() {
const [zoom, setZoom] = useState(1.0);
const [activeTool, setActiveTool] = useState<ToolId>('select');
const [activeGroup, setActiveGroup] = useState<import('./lib/tools').ToolGroup>('primary');
const [toolSettings, setToolSettings] = useState<ToolSettings>(DEFAULT_TOOL_SETTINGS);
const [currentPage, setCurrentPage] = useState(0);
const [isInspectorOpen, setIsInspectorOpen] = useState(true);
@@ -1209,21 +1210,24 @@ function App() {
/>
<div className="flex min-h-0 flex-1">
<ToolRail
<Sidebar
activeTool={activeTool}
activeGroup={activeGroup}
onToolChange={handleToolChange}
onGroupChange={setActiveGroup}
hasSignature={!!pendingSignature}
onOpenSignature={() => setSignatureModalOpen(true)}
onOpenAbout={() => setAboutModalOpen(true)}
disabledTools={disabledTools}
onRunOCR={handleRunOCR}
isOCRLoading={isOCRLoading}
/>
<div className="flex min-w-0 flex-1 flex-col">
{activeTool !== 'create_pdf' && !createPdfModalOpen && (
<Toolbar
activeGroup={activeGroup}
activeTool={activeTool}
onToolChange={handleToolChange}
settings={toolSettings}
onSettingsChange={(patch) => setToolSettings((s) => ({ ...s, ...patch }))}
onOpenSignature={() => setSignatureModalOpen(true)}
+135
View File
@@ -0,0 +1,135 @@
import { CustomButton } from './custom/CustomButton';
import React from 'react';
import type { ToolId, ToolGroup } from '../lib/tools';
import { Popover } from './ui';
import {
SelectIcon, PanIcon, DrawIcon, TextBoxIcon,
SignatureIcon, PagesIcon, InfoIcon, HelpIcon
} from './icons';
interface SidebarProps {
activeTool: ToolId;
activeGroup: ToolGroup;
onToolChange: (t: ToolId) => void;
onGroupChange: (g: ToolGroup) => void;
hasSignature: boolean;
onOpenSignature: () => void;
onOpenAbout: () => void;
disabledTools?: Set<ToolId>;
}
interface GroupDef {
id: ToolGroup;
label: string;
icon: React.ReactNode;
}
const GROUPS: GroupDef[] = [
{ id: 'annotations', label: 'Annotations', icon: <DrawIcon /> },
{ id: 'edit', label: 'Edit', icon: <TextBoxIcon /> },
{ id: 'security', label: 'Security', icon: <SignatureIcon /> },
{ id: 'organize', label: 'Organize', icon: <PagesIcon /> },
];
const SidebarButton: React.FC<{
label: string;
icon: React.ReactNode;
active: boolean;
disabled?: boolean;
onClick: () => void;
}> = ({ label, icon, active, disabled, onClick }) => (
<CustomButton variant="unstyled"
title={disabled ? `${label} (Disabled)` : label}
aria-label={label}
aria-pressed={active}
aria-disabled={disabled}
onClick={onClick}
className={`relative flex h-[52px] w-[72px] shrink-0 flex-col items-center justify-center gap-[3px] rounded-[10px] transition-colors ${disabled
? 'cursor-not-allowed text-text-tertiary opacity-40 dark:text-zinc-600 dark:opacity-60'
: active
? 'bg-brand-secondary text-brand-primary'
: 'text-text-secondary hover:bg-bg-tertiary hover:text-text-primary'
}`}
>
{active && !disabled && <span className="absolute left-[0px] h-5 w-[3px] rounded-r-full" style={{ background: 'var(--brand-primary)' }} />}
{React.isValidElement(icon) ? React.cloneElement(icon as React.ReactElement<{ size?: number }>, { size: 20 }) : icon}
<span className="text-[10px] font-medium leading-none tracking-tight">{label}</span>
</CustomButton>
);
export const Sidebar: React.FC<SidebarProps> = ({
activeTool, activeGroup, onToolChange, onGroupChange, hasSignature, onOpenSignature, onOpenAbout, disabledTools
}) => {
return (
<nav className="flex h-full shrink-0 flex-col items-center gap-2 overflow-y-auto border-r border-border-primary bg-bg-primary scroll-micro" style={{ width: '92px', paddingTop: '16px', paddingBottom: '16px' }}>
{/* Primary Tools */}
<SidebarButton
label="Select"
icon={<SelectIcon />}
active={activeTool === 'select'}
disabled={disabledTools?.has('select')}
onClick={() => {
onGroupChange('primary');
onToolChange('select');
}}
/>
<SidebarButton
label="Pan"
icon={<PanIcon />}
active={activeTool === 'pan'}
disabled={disabledTools?.has('pan')}
onClick={() => {
onGroupChange('primary');
onToolChange('pan');
}}
/>
<div className="my-1 h-px w-10 shrink-0 bg-border-primary" />
{/* Tool Groups */}
{GROUPS.map(g => (
<SidebarButton
key={g.id}
label={g.label}
icon={g.icon}
active={activeGroup === g.id}
onClick={() => {
onGroupChange(g.id);
// Default tools for groups when clicked
if (g.id === 'annotations') onToolChange('highlight');
if (g.id === 'edit') onToolChange('edit_text');
if (g.id === 'security') onToolChange('signature');
if (g.id === 'organize') onToolChange('create_pdf');
}}
/>
))}
<div className="flex-1 shrink-0 min-h-[16px]" />
<CustomButton variant="unstyled" title="About Maskan PDF Editor" onClick={onOpenAbout} className="flex h-9 w-9 shrink-0 items-center justify-center rounded-[10px] text-text-tertiary transition-colors hover:bg-bg-tertiary hover:text-text-primary">
<InfoIcon size={19} />
</CustomButton>
<Popover
align="left"
width={210}
trigger={(open) => (
<CustomButton variant="unstyled" title="Keyboard shortcuts" className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-[10px] transition-colors ${open ? 'bg-bg-tertiary text-text-primary' : 'text-text-tertiary hover:bg-bg-tertiary hover:text-text-primary'}`}>
<HelpIcon size={19} />
</CustomButton>
)}
>
<div className="text-[12px]">
<p className="mb-1.5 px-1 font-bold text-text-primary">Shortcuts</p>
<div className="my-1 h-px bg-border-primary" />
<div className="flex items-center justify-between px-1 py-0.5">
<span className="text-text-secondary">Undo / Redo</span>
<kbd className="rounded border border-border-primary bg-bg-secondary px-1.5 text-[10px] font-semibold">Ctrl+Z / Y</kbd>
</div>
</div>
</Popover>
</nav>
);
};
+31 -13
View File
@@ -10,7 +10,9 @@ import {
} from './icons';
interface ToolbarProps {
activeGroup: import('../lib/tools').ToolGroup;
activeTool: ToolId;
onToolChange: (t: ToolId) => void;
settings: ToolSettings;
onSettingsChange: (patch: Partial<ToolSettings>) => void;
onOpenSignature: () => void;
@@ -92,6 +94,14 @@ const TOOL_META: Record<ToolId, { label: string; icon: React.ReactNode }> = {
},
};
const GROUP_TOOLS: Record<import('../lib/tools').ToolGroup, ToolId[]> = {
primary: ['select', 'pan'],
annotations: ['highlight', 'underline', 'strikeout', 'squiggly', 'draw', 'comment', 'textbox'],
edit: ['edit_text', 'ocr', 'stream_edit'],
security: ['signature', 'stamp', 'watermark', 'redact'],
organize: ['create_pdf', 'merge_pdf']
};
const Hint: React.FC<{ children: React.ReactNode; tone?: 'normal' | 'warn' }> = ({ children, tone = 'normal' }) => (
<span className={`text-[12px] ${tone === 'warn' ? 'font-semibold text-brand-primary' : 'text-text-secondary'}`}>{children}</span>
);
@@ -101,13 +111,14 @@ const Label: React.FC<{ children: React.ReactNode }> = ({ children }) => (
const Divider = () => <div className="mx-1 h-5 w-px shrink-0 bg-border-primary" />;
export const Toolbar: React.FC<ToolbarProps> = ({
activeTool, settings, onSettingsChange, onOpenSignature, hasSignature, activeStamp, onSelectStamp,
activeGroup, activeTool, onToolChange, settings, onSettingsChange, onOpenSignature, hasSignature, activeStamp, onSelectStamp,
redactionMode = 'area', onRedactionModeChange,
pendingRedactionCount = 0, onApplyRedactions, onClearRedactions, onRedactPages, onOpenWatermark,
selectedAnnotation, onUpdateAnnotation, onDeleteAnnotation, onDeselectAnnotation
}) => {
const meta = TOOL_META[activeTool];
const isRedact = activeTool === 'redact';
const toolsInGroup = GROUP_TOOLS[activeGroup] || [];
if (selectedAnnotation) {
const selectedMeta = TOOL_META[selectedAnnotation.type as ToolId];
@@ -156,20 +167,27 @@ export const Toolbar: React.FC<ToolbarProps> = ({
return (
<div
className="flex h-[48px] shrink-0 items-center gap-3 border-b border-border-primary bg-bg-secondary"
className="flex h-[48px] shrink-0 items-center gap-3 border-b border-border-primary bg-bg-secondary overflow-x-auto"
style={{ paddingLeft: '16px', paddingRight: '16px' }}
>
<div className="flex shrink-0 items-center gap-2">
<span
className="flex h-7 w-7 items-center justify-center rounded-[8px]"
style={{
background: isRedact ? 'var(--brand-tertiary)' : 'var(--brand-secondary)',
color: isRedact ? 'var(--brand-primary)' : 'var(--brand-primary)',
}}
>
{meta?.icon}
</span>
<span className="text-[13px] font-bold text-text-primary">{meta?.label}</span>
<div className="flex shrink-0 items-center gap-1">
{toolsInGroup.map((toolId) => {
const t = TOOL_META[toolId];
const isActive = activeTool === toolId;
return (
<button
key={toolId}
onClick={() => onToolChange(toolId)}
className={`flex items-center gap-1.5 rounded-[8px] px-2.5 py-1.5 transition-colors ${
isActive ? 'bg-brand-secondary text-brand-primary' : 'text-text-secondary hover:bg-bg-tertiary hover:text-text-primary'
}`}
title={t.label}
>
{t.icon}
<span className="text-[12px] font-semibold">{t.label}</span>
</button>
);
})}
</div>
<Divider />
+23
View File
@@ -18,6 +18,29 @@ export type ToolId =
| 'watermark'
| 'merge_pdf';
export type ToolGroup = 'primary' | 'annotations' | 'edit' | 'security' | 'organize';
export const TOOL_GROUP_MAPPING: Record<ToolId, ToolGroup> = {
select: 'primary',
pan: 'primary',
highlight: 'annotations',
underline: 'annotations',
strikeout: 'annotations',
squiggly: 'annotations',
draw: 'annotations',
comment: 'annotations',
textbox: 'annotations',
edit_text: 'edit',
ocr: 'edit',
stream_edit: 'edit',
signature: 'security',
stamp: 'security',
watermark: 'security',
redact: 'security',
create_pdf: 'organize',
merge_pdf: 'organize',
};
export interface ToolSettings {
highlightColor: string;
highlightOpacity: number;