Files
pdf-tools/src/components/WorkspacePanel.tsx

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;