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
+38 -1
View File
@@ -1,5 +1,10 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { loadPanelLayout, savePanelLayout } from './ux-prefs';
import {
loadPanelLayout,
loadPaneVisibility,
savePanelLayout,
savePaneVisibility,
} from './ux-prefs';
const KEY = 'astrolabe:ux-prefs';
@@ -61,3 +66,35 @@ describe('ux-prefs · panelLayout', () => {
expect(stored.panelLayout.libraryWidth).toBe(260);
});
});
describe('ux-prefs · paneVisibility', () => {
beforeEach(() => vi.stubGlobal('localStorage', makeStorageStub()));
it('round-trips persisted visibility; absent flags stay undefined', () => {
savePaneVisibility({ library: false, editor: true });
expect(loadPaneVisibility()).toEqual({ library: false, editor: true, preview: undefined });
});
it('surfaces only booleans — junk in storage falls back to undefined', () => {
localStorage.setItem(
KEY,
JSON.stringify({ paneVisibility: { library: 'nope', preview: false } }),
);
expect(loadPaneVisibility()).toEqual({
library: undefined,
editor: undefined,
preview: false,
});
});
it('preserves panelLayout when writing visibility (separate keys)', () => {
savePanelLayout({ libraryWidth: 300 });
savePaneVisibility({ editor: false });
const stored = JSON.parse(localStorage.getItem(KEY)!) as {
panelLayout: { libraryWidth: number };
paneVisibility: { editor: boolean };
};
expect(stored.panelLayout.libraryWidth).toBe(300);
expect(stored.paneVisibility.editor).toBe(false);
});
});
+33 -3
View File
@@ -6,9 +6,10 @@
* record. Its own key, `astrolabe:ux-prefs`, holds the snippet sort preference
* (lands with M5/M6) and the panel layout (per-pane widths + visibility).
*
* This slice persists the panel **widths** only; visibility joins it when the
* toggle strip lands. Read-with-fallback + write-through merge, the same
* contract as the settings adapter, so adding fields later upgrades cleanly.
* This slice persists the panel **widths** and per-pane **visibility** (spec §01A).
* Read-with-fallback + write-through merge, the same contract as the settings
* adapter, so adding fields later upgrades cleanly. Widths and visibility are kept
* as separate keys: a hidden pane retains its remembered width.
*
* Per the architecture rule this is one of the only modules that may touch
* `localStorage`; everything else goes through these typed functions.
@@ -22,8 +23,16 @@ export interface PanelLayout {
previewWidth?: number;
}
/** Per-pane visibility. Optional — a missing field falls back to "shown". */
export interface PaneVisibilityPref {
library?: boolean;
editor?: boolean;
preview?: boolean;
}
interface StoredPrefs {
panelLayout?: PanelLayout;
paneVisibility?: PaneVisibilityPref;
[k: string]: unknown;
}
@@ -76,3 +85,24 @@ export function savePanelLayout(layout: PanelLayout): void {
const current = readRaw();
writeRaw({ ...current, panelLayout: { ...current.panelLayout, ...layout } });
}
/** A boolean, or undefined — guards against junk in storage. */
function bool(v: unknown): boolean | undefined {
return typeof v === 'boolean' ? v : undefined;
}
/** The persisted per-pane visibility, with only valid booleans surfaced. */
export function loadPaneVisibility(): PaneVisibilityPref {
const stored = readRaw().paneVisibility ?? {};
return {
library: bool(stored.library),
editor: bool(stored.editor),
preview: bool(stored.preview),
};
}
/** Persist per-pane visibility, preserving every other key already in the record. */
export function savePaneVisibility(visibility: PaneVisibilityPref): void {
const current = readRaw();
writeRaw({ ...current, paneVisibility: { ...current.paneVisibility, ...visibility } });
}