viewer-panels: read-only boots panels open-collapsed; kicad-style layer rows
- Read-only sessions boot BOTH panels open as collapsed headers (a stored per-browser choice wins); inspector's default anchor moves to the top-left, layers keep the top-right stack under the FAB. - Layer rows follow KiCad's Appearance-pane order: color swatch, eye toggle, layer name (name click = set active). - read-only spec: boot-default assertions (open + collapsed + top-left anchor) in the lock test, chevron expansion in the panels test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011gJ3M1RpeZLeNUUj8jKC4h
This commit is contained in:
parent
acc76f65ec
commit
3df3468d36
4 changed files with 90 additions and 30 deletions
|
|
@ -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.locator('[data-testid="chrome-toggle"]')).toBeVisible();
|
||||||
await expect(writer.getByTestId('view-only-pill')).toHaveCount(0);
|
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.
|
// No presence/comments surfaces for a viewer.
|
||||||
await expect(viewer.locator('[data-testid="presence-roster"]')).toHaveCount(0);
|
await expect(viewer.locator('[data-testid="presence-roster"]')).toHaveCount(0);
|
||||||
expect(
|
expect(
|
||||||
|
|
@ -348,6 +367,9 @@ test('viewer panels: layer selector + selection inspector (viewer-panels)', asyn
|
||||||
// and would swallow the row clicks below.
|
// and would swallow the row clicks below.
|
||||||
await viewer.keyboard.press('Escape');
|
await viewer.keyboard.press('Escape');
|
||||||
await expect(viewer.getByTestId('overlay-menu-panel')).toHaveCount(0);
|
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();
|
await expect(viewer.locator('[data-testid="layer-row"]').first()).toBeVisible();
|
||||||
|
|
||||||
const st0 = await layersState();
|
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.
|
// show. The FAB toggle never touches the canvas.
|
||||||
await viewer.getByTestId('overlay-menu-fab').click();
|
await viewer.getByTestId('overlay-menu-fab').click();
|
||||||
await expect(viewer.getByTestId('overlay-menu-panel')).toHaveCount(0);
|
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
|
// The already-made selection renders with real property rows (every item
|
||||||
// type yields at least one of these labels).
|
// type yields at least one of these labels).
|
||||||
await expect
|
await expect
|
||||||
|
|
|
||||||
|
|
@ -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<HTMLDivElement | null>(null);
|
const rootRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
const drag = useDraggablePanel({
|
const drag = useDraggablePanel({
|
||||||
storageKey: PANEL_POS_KEY,
|
storageKey: PANEL_POS_KEY,
|
||||||
|
|
@ -87,10 +97,12 @@ export function LayerPanel({ mod, onClose }: { mod: LayersModule; onClose: () =>
|
||||||
});
|
});
|
||||||
const [collapsed, setCollapsedState] = React.useState<boolean>(() => {
|
const [collapsed, setCollapsedState] = React.useState<boolean>(() => {
|
||||||
try {
|
try {
|
||||||
return localStorage.getItem(PANEL_COLLAPSED_KEY) === "1";
|
const stored = localStorage.getItem(PANEL_COLLAPSED_KEY);
|
||||||
|
if (stored !== null) return stored === "1";
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
/* private mode */
|
||||||
}
|
}
|
||||||
|
return defaultCollapsed === true;
|
||||||
});
|
});
|
||||||
const setCollapsed = (v: boolean) => {
|
const setCollapsed = (v: boolean) => {
|
||||||
setCollapsedState(v);
|
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" : ""
|
state.active === l.id ? "bg-sky-500/10 dark:bg-sky-400/10" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* Row body sets the ACTIVE layer (the wx Appearance pane's
|
{/* KiCad Appearance-pane row order: color swatch, eye, name.
|
||||||
click semantics); the eye toggles visibility. */}
|
The name sets the ACTIVE layer; the eye toggles visibility. */}
|
||||||
<button
|
<span
|
||||||
data-testid="layer-activate"
|
className="h-3 w-3 shrink-0 rounded-sm ring-1 ring-inset ring-black/20 dark:ring-white/25"
|
||||||
className="flex min-w-0 flex-1 items-center gap-2 text-left hover:text-sky-600 dark:hover:text-sky-300"
|
style={l.color ? { backgroundColor: l.color } : undefined}
|
||||||
title={`Make ${l.name} the active layer`}
|
/>
|
||||||
onClick={() => setActive(l.id)}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="h-3 w-3 shrink-0 rounded-sm ring-1 ring-inset ring-black/20 dark:ring-white/25"
|
|
||||||
style={l.color ? { backgroundColor: l.color } : undefined}
|
|
||||||
/>
|
|
||||||
<span className={`truncate ${state.active === l.id ? "font-semibold" : ""}`}>
|
|
||||||
{l.name}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
data-testid="layer-visibility"
|
data-testid="layer-visibility"
|
||||||
aria-pressed={l.visible}
|
aria-pressed={l.visible}
|
||||||
|
|
@ -230,6 +232,16 @@ export function LayerPanel({ mod, onClose }: { mod: LayersModule; onClose: () =>
|
||||||
>
|
>
|
||||||
{l.visible ? <Eye size={13} /> : <EyeOff size={13} />}
|
{l.visible ? <Eye size={13} /> : <EyeOff size={13} />}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
data-testid="layer-activate"
|
||||||
|
className="min-w-0 flex-1 text-left hover:text-sky-600 dark:hover:text-sky-300"
|
||||||
|
title={`Make ${l.name} the active layer`}
|
||||||
|
onClick={() => setActive(l.id)}
|
||||||
|
>
|
||||||
|
<span className={`block truncate ${state.active === l.id ? "font-semibold" : ""}`}>
|
||||||
|
{l.name}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -37,12 +37,16 @@ const MAX_ITEMS = 20;
|
||||||
|
|
||||||
export function SelectionInspector({
|
export function SelectionInspector({
|
||||||
doc,
|
doc,
|
||||||
|
defaultCollapsed,
|
||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
/** The bound collab doc (pcbnew: the board room; eeschema: the ACTIVE
|
/** 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
|
* sheet's room). Null when no doc room is bound (?collab=0) — the panel
|
||||||
* then shows selection counts only. */
|
* then shows selection counts only. */
|
||||||
doc: Y.Doc | null;
|
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;
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
const rootRef = React.useRef<HTMLDivElement | null>(null);
|
const rootRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
|
|
@ -53,10 +57,12 @@ export function SelectionInspector({
|
||||||
});
|
});
|
||||||
const [collapsed, setCollapsedState] = React.useState<boolean>(() => {
|
const [collapsed, setCollapsedState] = React.useState<boolean>(() => {
|
||||||
try {
|
try {
|
||||||
return localStorage.getItem(PANEL_COLLAPSED_KEY) === "1";
|
const stored = localStorage.getItem(PANEL_COLLAPSED_KEY);
|
||||||
|
if (stored !== null) return stored === "1";
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
/* private mode */
|
||||||
}
|
}
|
||||||
|
return defaultCollapsed === true;
|
||||||
});
|
});
|
||||||
const setCollapsed = (v: boolean) => {
|
const setCollapsed = (v: boolean) => {
|
||||||
setCollapsedState(v);
|
setCollapsedState(v);
|
||||||
|
|
@ -103,11 +109,12 @@ export function SelectionInspector({
|
||||||
|
|
||||||
const count = selection.uuids.length;
|
const count = selection.uuids.length;
|
||||||
|
|
||||||
// Default anchor: below the overlay-menu FAB, clear of the layer panel's
|
// Default anchor: TOP-LEFT (the layer panel owns the top-right stack under
|
||||||
// default (right 12 / top 56, w-64) — stack under it.
|
// the overlay-menu FAB). Clear of the transient status chip (left-3 top-3,
|
||||||
|
// pointer-events-none anyway).
|
||||||
const style: React.CSSProperties = drag.pos
|
const style: React.CSSProperties = drag.pos
|
||||||
? { left: drag.pos.x, top: drag.pos.y }
|
? { left: drag.pos.x, top: drag.pos.y }
|
||||||
: { right: 12, top: 96 };
|
: { left: 12, top: 12 };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -1182,12 +1182,17 @@ export function WasmTool({
|
||||||
// Read-only sessions never bind presence, so the inspector's selection
|
// Read-only sessions never bind presence, so the inspector's selection
|
||||||
// store is fed by this minimal local handler (+ the C++ input hooks).
|
// store is fed by this minimal local handler (+ the C++ input hooks).
|
||||||
const localSelectionRef = React.useRef<{ destroy(): void } | null>(null);
|
const localSelectionRef = React.useRef<{ destroy(): void } | null>(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<boolean>(() => {
|
const [layersOpen, setLayersOpenState] = React.useState<boolean>(() => {
|
||||||
try {
|
try {
|
||||||
return localStorage.getItem(LAYERS_OPEN_KEY) === "1";
|
const stored = localStorage.getItem(LAYERS_OPEN_KEY);
|
||||||
|
if (stored !== null) return stored === "1";
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
/* private mode */
|
||||||
}
|
}
|
||||||
|
return readOnly === true;
|
||||||
});
|
});
|
||||||
const setLayersOpen = React.useCallback((v: boolean) => {
|
const setLayersOpen = React.useCallback((v: boolean) => {
|
||||||
setLayersOpenState(v);
|
setLayersOpenState(v);
|
||||||
|
|
@ -1199,10 +1204,12 @@ export function WasmTool({
|
||||||
}, []);
|
}, []);
|
||||||
const [inspectorOpen, setInspectorOpenState] = React.useState<boolean>(() => {
|
const [inspectorOpen, setInspectorOpenState] = React.useState<boolean>(() => {
|
||||||
try {
|
try {
|
||||||
return localStorage.getItem(INSPECTOR_OPEN_KEY) === "1";
|
const stored = localStorage.getItem(INSPECTOR_OPEN_KEY);
|
||||||
|
if (stored !== null) return stored === "1";
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
/* private mode */
|
||||||
}
|
}
|
||||||
|
return readOnly === true;
|
||||||
});
|
});
|
||||||
const setInspectorOpen = React.useCallback((v: boolean) => {
|
const setInspectorOpen = React.useCallback((v: boolean) => {
|
||||||
setInspectorOpenState(v);
|
setInspectorOpenState(v);
|
||||||
|
|
@ -2767,13 +2774,21 @@ export function WasmTool({
|
||||||
inspector for canvas-only sessions — the React stand-ins for the wx
|
inspector for canvas-only sessions — the React stand-ins for the wx
|
||||||
Appearance/Properties panes that kicadSetChrome(false) hides. */}
|
Appearance/Properties panes that kicadSetChrome(false) hides. */}
|
||||||
{ready && effectiveChromeHidden && layersOpen && layersMod && (
|
{ready && effectiveChromeHidden && layersOpen && layersMod && (
|
||||||
<LayerPanel mod={layersMod} onClose={() => setLayersOpen(false)} />
|
<LayerPanel
|
||||||
|
mod={layersMod}
|
||||||
|
defaultCollapsed={readOnly}
|
||||||
|
onClose={() => setLayersOpen(false)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{ready &&
|
{ready &&
|
||||||
effectiveChromeHidden &&
|
effectiveChromeHidden &&
|
||||||
inspectorOpen &&
|
inspectorOpen &&
|
||||||
(tool === "pcbnew" || tool === "eeschema") && (
|
(tool === "pcbnew" || tool === "eeschema") && (
|
||||||
<SelectionInspector doc={panelDoc} onClose={() => setInspectorOpen(false)} />
|
<SelectionInspector
|
||||||
|
doc={panelDoc}
|
||||||
|
defaultCollapsed={readOnly}
|
||||||
|
onClose={() => setInspectorOpen(false)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* DEV: presence style tuner (VITE_PRESENCE_TUNER=1). */}
|
{/* DEV: presence style tuner (VITE_PRESENCE_TUNER=1). */}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue