Files
pdf/frontend/src/components/Toolbar.tsx
T
2026-06-10 15:16:20 +05:30

137 lines
6.3 KiB
TypeScript

import { CustomButton } from './custom/CustomButton';
import React from 'react';
import type { ToolId, ToolSettings } from '../lib/tools';
import { STAMP_PRESETS } from '../lib/tools';
import { ColorSwatches, Slider } from './ui';
import {
SelectIcon, PanIcon, HighlightIcon, DrawIcon, CommentIcon, TextBoxIcon,
SignatureIcon, StampIcon, RedactIcon,
} from './icons';
interface ToolbarProps {
activeTool: ToolId;
settings: ToolSettings;
onSettingsChange: (patch: Partial<ToolSettings>) => void;
onOpenSignature: () => void;
hasSignature: boolean;
activeStamp: string | null;
onSelectStamp: (label: string, color: string) => void;
}
const TOOL_META: Record<ToolId, { label: string; icon: React.ReactNode }> = {
select: { label: 'Select', icon: <SelectIcon size={17} /> },
pan: { label: 'Pan', icon: <PanIcon size={17} /> },
highlight: { label: 'Highlight', icon: <HighlightIcon size={17} /> },
draw: { label: 'Draw', icon: <DrawIcon size={17} /> },
comment: { label: 'Comment', icon: <CommentIcon size={17} /> },
textbox: { label: 'Text box', icon: <TextBoxIcon size={17} /> },
signature: { label: 'Signature', icon: <SignatureIcon size={17} /> },
stamp: { label: 'Stamp', icon: <StampIcon size={17} /> },
redact: { label: 'Redact', icon: <RedactIcon size={17} /> },
};
const Hint: React.FC<{ children: React.ReactNode; tone?: 'normal' | 'warn' }> = ({ children, tone = 'normal' }) => (
<span className={`text-[12px] ${tone === 'warn' ? 'font-semibold text-[#dc2626]' : 'text-[#98a1ad]'}`}>{children}</span>
);
const Label: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<span className="text-[11px] font-semibold uppercase tracking-wide text-[#98a1ad]">{children}</span>
);
const Divider = () => <div className="mx-1 h-5 w-px shrink-0 bg-[#ebedf0]" />;
export const Toolbar: React.FC<ToolbarProps> = ({
activeTool, settings, onSettingsChange, onOpenSignature, hasSignature, activeStamp, onSelectStamp,
}) => {
const meta = TOOL_META[activeTool];
const isRedact = activeTool === 'redact';
return (
<div
className="flex h-[48px] shrink-0 items-center gap-3 border-b border-[#ebedf0] bg-[#ffffff]"
style={{ paddingLeft: '16px', paddingRight: '16px' }}
>
{/* Active tool identity */}
<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 ? '#fdecec' : '#eef4ff',
color: isRedact ? '#dc2626' : '#2563eb',
}}
>
{meta.icon}
</span>
<span className="text-[13px] font-bold text-[#18212e]">{meta.label}</span>
</div>
<Divider />
{/* Per-tool controls (no overflow clip — would cut off the active-swatch ring) */}
<div className="flex min-w-0 flex-1 items-center gap-3">
{activeTool === 'select' && <Hint>Drag to select text release to copy, or press <Kbd>Ctrl/ + C</Kbd></Hint>}
{activeTool === 'pan' && <Hint>Drag anywhere to move the page.</Hint>}
{activeTool === 'highlight' && (
<>
<Label>Color</Label>
<ColorSwatches value={settings.highlightColor} onChange={(c) => onSettingsChange({ highlightColor: c })} />
<Divider />
<Slider label="Opacity" min={10} max={100} step={5} value={Math.round(settings.highlightOpacity * 100)} onChange={(v) => onSettingsChange({ highlightOpacity: v / 100 })} suffix="%" />
</>
)}
{activeTool === 'draw' && (
<>
<Label>Color</Label>
<ColorSwatches value={settings.inkColor} onChange={(c) => onSettingsChange({ inkColor: c })}
palette={['#2563eb', '#dc2626', '#16a34a', '#d97706', '#7c3aed', '#18212e', '#ec4899', '#0891b2']} />
<Divider />
<Slider label="Thickness" min={1} max={12} step={1} value={settings.inkThickness} onChange={(v) => onSettingsChange({ inkThickness: v })} suffix="px" />
</>
)}
{activeTool === 'comment' && <Hint>Click anywhere on the page to drop a sticky note.</Hint>}
{activeTool === 'textbox' && (
<>
<Label>Color</Label>
<ColorSwatches value={settings.textColor} onChange={(c) => onSettingsChange({ textColor: c })}
palette={['#18212e', '#dc2626', '#2563eb', '#16a34a', '#d97706', '#7c3aed', '#ffffff']} />
<Divider />
<Slider label="Size" min={8} max={48} step={1} value={settings.fontSize} onChange={(v) => onSettingsChange({ fontSize: v })} suffix="pt" />
<Hint>Click to place a text box.</Hint>
</>
)}
{activeTool === 'signature' && (
<>
<CustomButton variant={hasSignature ? 'outline' : 'primary'} size="sm" onClick={onOpenSignature}>
<SignatureIcon size={16} /> {hasSignature ? 'Change signature' : 'Create signature'}
</CustomButton>
{hasSignature ? <Hint>Click on the page to place it.</Hint> : <Hint>Draw, type, or upload a signature.</Hint>}
<span className="ml-auto shrink-0 rounded-full bg-[#f6f7f9] px-2 py-0.5 text-[10px] font-semibold text-[#98a1ad]">Visual signature not certified</span>
</>
)}
{activeTool === 'stamp' && (
<>
<div className="flex items-center gap-2">
{STAMP_PRESETS.map((s) => (
<CustomButton variant="unstyled" key={s.label} onClick={() => onSelectStamp(s.label, s.color)}
className={`shrink-0 rounded-[6px] border px-2.5 py-1 text-[10.5px] font-bold tracking-wide transition-transform hover:scale-[1.04] ${activeStamp === s.label ? 'ring-2 ring-offset-1 ring-[#2563eb]' : ''}`}
style={{ color: s.color, borderColor: s.color, background: `color-mix(in srgb, ${s.color} 8%, white)` }}>
{s.label}
</CustomButton>
))}
</div>
<Hint>Pick a stamp, then click to place it.</Hint>
</>
)}
{activeTool === 'redact' && <Hint tone="warn"> Drag a box to permanently remove content underneath.</Hint>}
</div>
</div>
);
};
const Kbd: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<kbd className="rounded border border-[#ebedf0] bg-[#f6f7f9] px-1.5 py-0.5 text-[10px] font-semibold text-[#18212e]">{children}</kbd>
);