328 lines
9.3 KiB
TypeScript
328 lines
9.3 KiB
TypeScript
import React from 'react';
|
|
import type { WorkspaceSummary } from '../workspace/workspaceTypes';
|
|
import type { WorkspaceCommandRecord } from '../workspace/workspaceCommands';
|
|
|
|
interface WorkspacePanelProps {
|
|
hasPdf: boolean;
|
|
isBusy: boolean;
|
|
|
|
activeWorkspaceId: string | null;
|
|
workspaceName: string;
|
|
workspaceDirty: boolean;
|
|
workspaceMessage: string | null;
|
|
|
|
workspaces: WorkspaceSummary[];
|
|
history: WorkspaceCommandRecord[];
|
|
redoHistory: WorkspaceCommandRecord[];
|
|
|
|
onWorkspaceNameChange: (value: string) => void;
|
|
onSaveWorkspace: () => void;
|
|
onLoadWorkspace: (workspaceId: string) => void;
|
|
onDeleteWorkspace: (workspaceId: string) => void;
|
|
onRefreshWorkspaces: () => void;
|
|
onResetWorkspace: () => void;
|
|
onUndo: () => void;
|
|
onRedo: () => void;
|
|
}
|
|
|
|
const WorkspacePanel: React.FC<WorkspacePanelProps> = ({
|
|
hasPdf,
|
|
isBusy,
|
|
activeWorkspaceId,
|
|
workspaceName,
|
|
workspaceDirty,
|
|
workspaceMessage,
|
|
workspaces,
|
|
history,
|
|
redoHistory,
|
|
onWorkspaceNameChange,
|
|
onSaveWorkspace,
|
|
onLoadWorkspace,
|
|
onDeleteWorkspace,
|
|
onRefreshWorkspaces,
|
|
onResetWorkspace,
|
|
onUndo,
|
|
onRedo,
|
|
}) => {
|
|
const canUndo = history.length > 0;
|
|
const canRedo = redoHistory.length > 0;
|
|
|
|
const latestUndo = history[history.length - 1];
|
|
const latestRedo = redoHistory[redoHistory.length - 1];
|
|
|
|
return (
|
|
<div className="card">
|
|
<h2>Workspace</h2>
|
|
|
|
<p style={{ fontSize: '0.85rem', color: '#6b7280' }}>
|
|
Save named workspaces in this browser. PDF binaries are stored in
|
|
IndexedDB; nothing is uploaded.
|
|
</p>
|
|
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
gap: '0.5rem',
|
|
flexWrap: 'wrap',
|
|
alignItems: 'center',
|
|
}}
|
|
>
|
|
<input
|
|
type="text"
|
|
value={workspaceName}
|
|
onChange={(e) => onWorkspaceNameChange(e.target.value)}
|
|
placeholder="Workspace name"
|
|
disabled={!hasPdf || isBusy}
|
|
style={{
|
|
flex: '1 1 220px',
|
|
minWidth: 0,
|
|
padding: '0.45rem 0.55rem',
|
|
borderRadius: '0.5rem',
|
|
border: '1px solid #d1d5db',
|
|
fontSize: '0.9rem',
|
|
}}
|
|
/>
|
|
|
|
<button
|
|
type="button"
|
|
className="secondary"
|
|
onClick={onUndo}
|
|
disabled={!hasPdf || isBusy || !canUndo}
|
|
title={latestUndo ? `Undo: ${latestUndo.label}` : 'Nothing to undo'}
|
|
>
|
|
↶ Undo
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
className="secondary"
|
|
onClick={onRedo}
|
|
disabled={!hasPdf || isBusy || !canRedo}
|
|
title={latestRedo ? `Redo: ${latestRedo.label}` : 'Nothing to redo'}
|
|
>
|
|
↷ Redo
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
className="secondary"
|
|
onClick={onSaveWorkspace}
|
|
disabled={!hasPdf || isBusy}
|
|
title={!hasPdf ? 'Open a PDF first' : 'Save workspace'}
|
|
>
|
|
💾 {activeWorkspaceId ? 'Save' : 'Save as'}
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
className="secondary"
|
|
onClick={onResetWorkspace}
|
|
disabled={!hasPdf || isBusy}
|
|
title={
|
|
!hasPdf ? 'No active workspace' : 'Close the current workspace'
|
|
}
|
|
>
|
|
Reset workspace
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
className="secondary"
|
|
onClick={onRefreshWorkspaces}
|
|
disabled={isBusy}
|
|
>
|
|
↻ Refresh
|
|
</button>
|
|
</div>
|
|
|
|
{workspaceDirty && hasPdf && (
|
|
<div
|
|
style={{
|
|
marginTop: '0.5rem',
|
|
fontSize: '0.8rem',
|
|
color: '#92400e',
|
|
}}
|
|
>
|
|
Unsaved workspace changes.
|
|
</div>
|
|
)}
|
|
|
|
{workspaceMessage && (
|
|
<div
|
|
style={{
|
|
marginTop: '0.5rem',
|
|
fontSize: '0.85rem',
|
|
color: '#166534',
|
|
}}
|
|
>
|
|
{workspaceMessage}
|
|
</div>
|
|
)}
|
|
|
|
{workspaces.length > 0 && (
|
|
<div style={{ marginTop: '0.75rem' }}>
|
|
<strong style={{ fontSize: '0.9rem' }}>Saved workspaces</strong>
|
|
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '0.4rem',
|
|
marginTop: '0.4rem',
|
|
}}
|
|
>
|
|
{workspaces.map((workspace) => {
|
|
const active = workspace.id === activeWorkspaceId;
|
|
|
|
return (
|
|
<div
|
|
key={workspace.id}
|
|
style={{
|
|
border: '1px solid #e5e7eb',
|
|
borderRadius: '0.5rem',
|
|
padding: '0.5rem',
|
|
background: active ? '#eff6ff' : '#f9fafb',
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
gap: '0.75rem',
|
|
alignItems: 'center',
|
|
flexWrap: 'wrap',
|
|
}}
|
|
>
|
|
<div style={{ minWidth: 0 }}>
|
|
<div style={{ fontSize: '0.9rem' }}>
|
|
<strong>{workspace.name}</strong>
|
|
{active && (
|
|
<span style={{ color: '#2563eb' }}> · active</span>
|
|
)}
|
|
</div>
|
|
|
|
<div style={{ fontSize: '0.75rem', color: '#6b7280' }}>
|
|
{workspace.pdfName} · source pages:{' '}
|
|
{workspace.sourcePageCount} · workspace pages:{' '}
|
|
{workspace.workspacePageCount} · undo:{' '}
|
|
{workspace.historyCount} · redo: {workspace.redoCount} ·
|
|
updated {new Date(workspace.updatedAt).toLocaleString()}
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
gap: '0.35rem',
|
|
flexWrap: 'wrap',
|
|
}}
|
|
>
|
|
<button
|
|
type="button"
|
|
className="secondary"
|
|
disabled={isBusy}
|
|
onClick={() => onLoadWorkspace(workspace.id)}
|
|
>
|
|
Load
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
className="secondary"
|
|
disabled={isBusy}
|
|
onClick={() => onDeleteWorkspace(workspace.id)}
|
|
style={{
|
|
background: '#fee2e2',
|
|
color: '#991b1b',
|
|
}}
|
|
>
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{(history.length > 0 || redoHistory.length > 0) && (
|
|
<details style={{ marginTop: '0.75rem' }} open>
|
|
<summary style={{ cursor: 'pointer', fontSize: '0.9rem' }}>
|
|
Command history ({history.length} undo / {redoHistory.length} redo)
|
|
</summary>
|
|
|
|
<div
|
|
style={{
|
|
marginTop: '0.5rem',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '0.25rem',
|
|
}}
|
|
>
|
|
{history.map((entry, index) => (
|
|
<div
|
|
key={entry.id}
|
|
style={{
|
|
fontSize: '0.8rem',
|
|
color: '#374151',
|
|
borderLeft: '3px solid #2563eb',
|
|
paddingLeft: '0.45rem',
|
|
paddingTop: '0.2rem',
|
|
paddingBottom: '0.2rem',
|
|
}}
|
|
>
|
|
<strong>
|
|
Undo {history.length - index}. {entry.label}
|
|
</strong>
|
|
<br />
|
|
<span style={{ color: '#6b7280' }}>
|
|
{new Date(entry.timestamp).toLocaleString()}
|
|
</span>
|
|
</div>
|
|
))}
|
|
|
|
<div
|
|
style={{
|
|
margin: '0.25rem 0',
|
|
borderRadius: '999px',
|
|
background: '#ecfdf5',
|
|
color: '#166534',
|
|
fontSize: '0.8rem',
|
|
fontWeight: 600,
|
|
alignSelf: 'flex-start',
|
|
border: '2px solid #166534',
|
|
width: '100%',
|
|
}}
|
|
></div>
|
|
|
|
{redoHistory
|
|
.slice()
|
|
.reverse()
|
|
.map((entry, index) => (
|
|
<div
|
|
key={entry.id}
|
|
style={{
|
|
fontSize: '0.8rem',
|
|
color: '#9ca3af',
|
|
borderLeft: '3px solid #d1d5db',
|
|
paddingLeft: '0.45rem',
|
|
paddingTop: '0.2rem',
|
|
paddingBottom: '0.2rem',
|
|
opacity: 0.75,
|
|
}}
|
|
>
|
|
<strong>
|
|
Redo {index + 1}. {entry.label}
|
|
</strong>
|
|
<br />
|
|
<span style={{ color: '#9ca3af' }}>
|
|
{new Date(entry.timestamp).toLocaleString()}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</details>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default WorkspacePanel;
|