Add pane show/hide toggle strip with persisted visibility (M6, §01A)

A persistent left-rail toolbar (APG toolbar + roving tabindex) toggles the
library / editor / preview panes; a hidden pane frees its space and the rest
redistribute proportionally to their remembered widths. Visibility persists to
astrolabe:ux-prefs (separate key from widths, so a hidden pane keeps its width).
Adds the positional pane-* glyph sub-family to Icon and a Datasets shortcut
button in the strip.
This commit is contained in:
2026-06-07 17:15:58 +03:00
parent f92118712c
commit 8c0a6b9239
11 changed files with 601 additions and 45 deletions
+48 -24
View File
@@ -2,6 +2,7 @@ import { useEffect, useRef } from 'react';
import { ConfirmDialog } from './components/ConfirmDialog';
import { LivePreview } from './components/LivePreview';
import { ModalShell } from './components/ModalShell';
import { PaneToggleStrip } from './components/PaneToggleStrip';
import { ResizeHandle } from './components/ResizeHandle';
import { SnippetLibrary } from './components/SnippetLibrary';
import { SpecEditor } from './components/SpecEditor';
@@ -19,13 +20,23 @@ import styles from './App.module.css';
* (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). Pane show/hide
* toggling, modals, routing, and shortcuts arrive in later milestones (see
* docs/IMPLEMENTATION-PLAN.md).
* are resized via the drag handles between them (spec §01A). Each pane can be
* shown/hidden from the always-present 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.
*/
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);
// 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<HTMLInputElement>(null);
@@ -112,27 +123,40 @@ export function App() {
{/* tabIndex -1 makes the landmark a focus target for the skip link. */}
<main id="main" className={styles.panes} tabIndex={-1}>
<section
id="pane-library"
className={styles.pane}
style={{ width: libraryWidth }}
aria-label="Snippet library"
>
<SnippetLibrary />
</section>
<ResizeHandle side="library" label="Resize snippet library" />
<section className={styles.paneEditor} aria-label="Spec editor">
<SpecEditor />
</section>
<ResizeHandle side="preview" label="Resize live preview" />
<section
id="pane-preview"
className={styles.pane}
style={{ width: previewWidth }}
aria-label="Live preview"
>
<LivePreview />
</section>
{/* Always-present rail: shows/hides panes and shortcuts to Datasets (§01A). */}
<PaneToggleStrip />
{libraryVisible && (
<section
id="pane-library"
className={styles.pane}
style={sideStyle(libraryWidth)}
aria-label="Snippet library"
>
<SnippetLibrary />
</section>
)}
{/* A resize handle only sits between two visible panes that flank the editor. */}
{libraryVisible && editorVisible && (
<ResizeHandle side="library" label="Resize snippet library" />
)}
{editorVisible && (
<section id="pane-editor" className={styles.paneEditor} aria-label="Spec editor">
<SpecEditor />
</section>
)}
{editorVisible && previewVisible && (
<ResizeHandle side="preview" label="Resize live preview" />
)}
{previewVisible && (
<section
id="pane-preview"
className={styles.pane}
style={sideStyle(previewWidth)}
aria-label="Live preview"
>
<LivePreview />
</section>
)}
</main>
{/* The one feature modal (Datasets / Extract / …), rendered from the