diff --git a/tests/web/read-only-editor.spec.ts b/tests/web/read-only-editor.spec.ts index a06f6ec..3787896 100644 --- a/tests/web/read-only-editor.spec.ts +++ b/tests/web/read-only-editor.spec.ts @@ -154,6 +154,25 @@ test('viewer boots locked: chrome-less, selection inspect-only, hotkey edits ine await expect(writer.locator('[data-testid="chrome-toggle"]')).toBeVisible(); await expect(writer.getByTestId('view-only-pill')).toHaveCount(0); + // Viewer-panels boot defaults: both panels come up OPEN as COLLAPSED + // headers (inspector anchored top-LEFT, layers top-right) — then close + // them so the click probes below can't land on a header. The panels test + // re-opens them through the overlay-menu toggles. First close the still- + // open overlay menu (z-50) — it covers the layers header's close button. + await viewer.getByTestId('overlay-menu-fab').click(); + await expect(viewer.getByTestId('overlay-menu-panel')).toHaveCount(0); + await expect(viewer.getByTestId('layers-panel')).toBeVisible(); + await expect(viewer.getByTestId('inspector-panel')).toBeVisible(); + await expect(viewer.getByTestId('layers-panel-list')).toHaveCount(0); + await expect(viewer.getByTestId('inspector-panel-list')).toHaveCount(0); + const inspectorBox = (await viewer.getByTestId('inspector-panel').boundingBox())!; + expect(inspectorBox.x, 'inspector defaults to the top-left').toBeLessThan(100); + expect(inspectorBox.y, 'inspector defaults to the top-left').toBeLessThan(60); + await viewer.getByTestId('layers-panel-close').click(); + await viewer.getByTestId('inspector-panel-close').click(); + await expect(viewer.getByTestId('layers-panel')).toHaveCount(0); + await expect(viewer.getByTestId('inspector-panel')).toHaveCount(0); + // No presence/comments surfaces for a viewer. await expect(viewer.locator('[data-testid="presence-roster"]')).toHaveCount(0); expect( @@ -348,6 +367,9 @@ test('viewer panels: layer selector + selection inspector (viewer-panels)', asyn // and would swallow the row clicks below. await viewer.keyboard.press('Escape'); await expect(viewer.getByTestId('overlay-menu-panel')).toHaveCount(0); + // Reopened panels keep the read-only default: collapsed — expand. + await viewer.getByTestId('layers-panel-collapse').click(); + await expect(viewer.getByTestId('layers-panel-list')).toBeVisible(); await expect(viewer.locator('[data-testid="layer-row"]').first()).toBeVisible(); const st0 = await layersState(); @@ -433,6 +455,10 @@ test('viewer panels: layer selector + selection inspector (viewer-panels)', asyn // show. The FAB toggle never touches the canvas. await viewer.getByTestId('overlay-menu-fab').click(); await expect(viewer.getByTestId('overlay-menu-panel')).toHaveCount(0); + // Still collapsed by default — expand (this is a header-button click, it + // never touches the canvas or the selection). + await viewer.getByTestId('inspector-panel-collapse').click(); + await expect(viewer.getByTestId('inspector-panel-list')).toBeVisible(); // The already-made selection renders with real property rows (every item // type yields at least one of these labels). await expect diff --git a/web/standalone/src/components/LayerPanel.tsx b/web/standalone/src/components/LayerPanel.tsx index c6e41bf..de4a44d 100644 --- a/web/standalone/src/components/LayerPanel.tsx +++ b/web/standalone/src/components/LayerPanel.tsx @@ -78,7 +78,17 @@ export function parseLayersState(json: string): LayersState | null { } } -export function LayerPanel({ mod, onClose }: { mod: LayersModule; onClose: () => void }) { +export function LayerPanel({ + mod, + defaultCollapsed, + onClose, +}: { + mod: LayersModule; + /** Boot state when no per-browser choice is stored (read-only sessions + * start as a collapsed header — viewer-panels). */ + defaultCollapsed?: boolean; + onClose: () => void; +}) { const rootRef = React.useRef(null); const drag = useDraggablePanel({ storageKey: PANEL_POS_KEY, @@ -87,10 +97,12 @@ export function LayerPanel({ mod, onClose }: { mod: LayersModule; onClose: () => }); const [collapsed, setCollapsedState] = React.useState(() => { try { - return localStorage.getItem(PANEL_COLLAPSED_KEY) === "1"; + const stored = localStorage.getItem(PANEL_COLLAPSED_KEY); + if (stored !== null) return stored === "1"; } catch { - return false; + /* private mode */ } + return defaultCollapsed === true; }); const setCollapsed = (v: boolean) => { setCollapsedState(v); @@ -201,22 +213,12 @@ export function LayerPanel({ mod, onClose }: { mod: LayersModule; onClose: () => state.active === l.id ? "bg-sky-500/10 dark:bg-sky-400/10" : "" }`} > - {/* Row body sets the ACTIVE layer (the wx Appearance pane's - click semantics); the eye toggles visibility. */} - + {/* KiCad Appearance-pane row order: color swatch, eye, name. + The name sets the ACTIVE layer; the eye toggles visibility. */} + + ))} diff --git a/web/standalone/src/components/SelectionInspector.tsx b/web/standalone/src/components/SelectionInspector.tsx index 64d552e..5aee11d 100644 --- a/web/standalone/src/components/SelectionInspector.tsx +++ b/web/standalone/src/components/SelectionInspector.tsx @@ -37,12 +37,16 @@ const MAX_ITEMS = 20; export function SelectionInspector({ doc, + defaultCollapsed, onClose, }: { /** The bound collab doc (pcbnew: the board room; eeschema: the ACTIVE * sheet's room). Null when no doc room is bound (?collab=0) — the panel * then shows selection counts only. */ doc: Y.Doc | null; + /** Boot state when no per-browser choice is stored (read-only sessions + * start as a collapsed header — viewer-panels). */ + defaultCollapsed?: boolean; onClose: () => void; }) { const rootRef = React.useRef(null); @@ -53,10 +57,12 @@ export function SelectionInspector({ }); const [collapsed, setCollapsedState] = React.useState(() => { try { - return localStorage.getItem(PANEL_COLLAPSED_KEY) === "1"; + const stored = localStorage.getItem(PANEL_COLLAPSED_KEY); + if (stored !== null) return stored === "1"; } catch { - return false; + /* private mode */ } + return defaultCollapsed === true; }); const setCollapsed = (v: boolean) => { setCollapsedState(v); @@ -103,11 +109,12 @@ export function SelectionInspector({ const count = selection.uuids.length; - // Default anchor: below the overlay-menu FAB, clear of the layer panel's - // default (right 12 / top 56, w-64) — stack under it. + // Default anchor: TOP-LEFT (the layer panel owns the top-right stack under + // the overlay-menu FAB). Clear of the transient status chip (left-3 top-3, + // pointer-events-none anyway). const style: React.CSSProperties = drag.pos ? { left: drag.pos.x, top: drag.pos.y } - : { right: 12, top: 96 }; + : { left: 12, top: 12 }; return (
(null); + // Read-only sessions boot with BOTH panels open as collapsed headers + // (viewer-panels): discoverable without a trip through the menu; a stored + // per-browser choice wins over the default. const [layersOpen, setLayersOpenState] = React.useState(() => { try { - return localStorage.getItem(LAYERS_OPEN_KEY) === "1"; + const stored = localStorage.getItem(LAYERS_OPEN_KEY); + if (stored !== null) return stored === "1"; } catch { - return false; + /* private mode */ } + return readOnly === true; }); const setLayersOpen = React.useCallback((v: boolean) => { setLayersOpenState(v); @@ -1199,10 +1204,12 @@ export function WasmTool({ }, []); const [inspectorOpen, setInspectorOpenState] = React.useState(() => { try { - return localStorage.getItem(INSPECTOR_OPEN_KEY) === "1"; + const stored = localStorage.getItem(INSPECTOR_OPEN_KEY); + if (stored !== null) return stored === "1"; } catch { - return false; + /* private mode */ } + return readOnly === true; }); const setInspectorOpen = React.useCallback((v: boolean) => { setInspectorOpenState(v); @@ -2767,13 +2774,21 @@ export function WasmTool({ inspector for canvas-only sessions — the React stand-ins for the wx Appearance/Properties panes that kicadSetChrome(false) hides. */} {ready && effectiveChromeHidden && layersOpen && layersMod && ( - setLayersOpen(false)} /> + setLayersOpen(false)} + /> )} {ready && effectiveChromeHidden && inspectorOpen && (tool === "pcbnew" || tool === "eeschema") && ( - setInspectorOpen(false)} /> + setInspectorOpen(false)} + /> )} {/* DEV: presence style tuner (VITE_PRESENCE_TUNER=1). */}