import { useEffect, useRef } from 'react'; import { Button } from './components/Button'; import { ConfirmDialog } from './components/ConfirmDialog'; import { LivePreview } from './components/LivePreview'; import { ModalShell } from './components/ModalShell'; import { Onboarding } from './components/Onboarding'; import { PaneSplitHandle } from './components/PaneSplitHandle'; import { Icon } from './components/Icon'; import { IconButton } from './components/IconButton'; import { PaneToggleStrip } from './components/PaneToggleStrip'; import { ResizeHandle } from './components/ResizeHandle'; import { SnippetLibrary } from './components/SnippetLibrary'; import { SpecEditor } from './components/SpecEditor'; import { ThemeToggle } from './components/ThemeToggle'; import { Toaster } from './components/Toaster'; import { openModal, setConfirm } from './modals/ModalCoordinator'; import { confirm } from './stores/ConfirmStore'; import { usePanesStore } from './stores/PanesStore'; import { useSnippetStore } from './stores/SnippetStore'; import { exportWorkspace, importWorkspace } from './services/transfer'; import styles from './App.module.css'; /** * Application shell — the three-pane workspace from spec §01A * (library · editor · preview) under a fixed header. * * The center editor flexes; the library and preview carry remembered widths and * are resized via the drag handles between them (spec §01A). Each pane can be * shown/hidden from the toggle strip (the leftmost rail); a hidden pane frees its * space and the rest redistribute — when the editor is hidden the two side panes * flex proportionally to their remembered widths. * * When the library is empty, this whole pane chrome (strip + panes) is replaced * by the full-width onboarding canvas (spec §02 → First-Run & Empty Workspace). */ export function App() { const libraryWidth = usePanesStore((s) => s.libraryWidth); const previewWidth = usePanesStore((s) => s.previewWidth); const libraryVisible = usePanesStore((s) => s.libraryVisible); const editorVisible = usePanesStore((s) => s.editorVisible); const previewVisible = usePanesStore((s) => s.previewVisible); // An empty library is the onboarding surface: the whole pane chrome (toggle // strip, library list, editor, preview) is replaced by the full-width Onboarding // canvas (spec §02 → First-Run & Empty Workspace). const hasSnippets = useSnippetStore((s) => s.snippets.length > 0); // Side-pane sizing: fixed remembered width while the editor (the flex filler) is // present; when it's hidden, the side panes grow proportionally to those widths // so they fill the freed space (spec §01A → "redistributing proportionally"). const sideStyle = (width: number): React.CSSProperties => editorVisible ? { width } : { flex: `${width} 1 0` }; // Hidden file input driving Import — the header button proxies its click so the // browser file picker is the only chrome (spec §08 → no intermediate dialog). const fileInputRef = useRef(null); // Route the modal coordinator's discard prompt through the in-app confirm // dialog (docs/architecture/03 → "The coordinator seam"). useEffect(() => { setConfirm((message) => confirm({ title: 'Discard changes?', message, danger: true })); }, []); const handleImportFile = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; // Reset the input so picking the same file again still fires onChange. e.target.value = ''; if (file) void importWorkspace(file); }; return (
{/* Skip link (WCAG 2.4.1 / GOV.UK): the first focusable element, hidden until focused, lets keyboard users bypass the header into the work area. */} Skip to content

Astrolabe

v{__APP_VERSION__} {/* Header actions establish a hierarchy (Carbon button/usage → one high-emphasis action per region; Carbon UI-shell header → global actions are a right-aligned row of icon-only buttons). The utilities are IconButtons — tooltips carry the full action, accessible names scope it ("Export workspace" vs the preview pane's per-chart "Export") — then a divider sets off Support (feedback + the one donation solicitation), given a soft-accent wash — and the theme toggle. */} openModal('datasets')} aria-keyshortcuts="Meta+K Control+K" title="Datasets (⌘/Ctrl+K)" > fileInputRef.current?.click()} title="Import a workspace JSON file" > exportWorkspace()} title="Export your workspace to a JSON file" > openModal('about')} title="About, keyboard shortcuts, and privacy information" >
{/* tabIndex -1 makes the landmark a focus target for the skip link. */}
{hasSnippets ? ( <> {/* Always-present rail: shows/hides panes and shortcuts to Datasets (§01A). */} {libraryVisible && (
)} {/* A resize handle sits between any two adjacent visible panes. With the editor present it flanks the editor (it absorbs the drag); with the editor hidden, the library and preview become adjacent and share a single split handle between them (spec §01A). */} {libraryVisible && editorVisible && ( )} {libraryVisible && previewVisible && !editorVisible && ( )} {editorVisible && (
)} {editorVisible && previewVisible && ( )} {previewVisible && (
)} ) : ( // Empty library → the onboarding canvas takes the full workspace. The // pane chrome (toggle strip, library list, editor, preview) is hidden: // with no snippets, Create/Search/Sort/Storage and the pane toggles have // nothing to act on, so the welcome gets the whole width (spec §02).
)}
{/* The one feature modal (Datasets / Extract / …), rendered from the registry by the shared shell. At most one open at a time (spec §01C). */} {/* Global confirmation layer — sits above the feature-modal shell so a discard-changes prompt can appear over an open modal. */} {/* Non-blocking notifications (failed saves, etc.) — top-right toasts, layered above the confirm backdrop so a failure stays visible. */}
); }