From beee07c5b867e191e09a4a7bfcfede77e3ace206 Mon Sep 17 00:00:00 2001 From: Joseph Dale Banares Date: Sat, 3 Oct 2026 10:57:31 +0800 Subject: [PATCH 1/4] feat(viewer): focus a selected card's neighbours, peek at a type from the legend, and let H hide the toolbar anywhere --- packages/viewer/e2e/focus.spec.ts | 12 +++-- .../viewer/src/canvas/DiagramCanvas.test.tsx | 5 +- .../viewer/src/chrome/IdentityCard.test.tsx | 21 -------- packages/viewer/src/chrome/IdentityCard.tsx | 28 ---------- packages/viewer/src/chrome/Inspector.tsx | 14 +++-- packages/viewer/src/chrome/Toolbar.tsx | 23 ++++---- .../viewer/src/chrome/ViewerShell.test.tsx | 30 ++++++++--- packages/viewer/src/chrome/ViewerShell.tsx | 52 ++++++------------- packages/viewer/src/embed.test.tsx | 6 --- packages/viewer/src/explore/emphasis.test.ts | 15 ++++-- packages/viewer/src/explore/emphasis.ts | 19 ++++++- packages/viewer/src/explore/state.test.ts | 7 +++ packages/viewer/src/explore/state.ts | 6 +++ 13 files changed, 115 insertions(+), 123 deletions(-) delete mode 100644 packages/viewer/src/chrome/IdentityCard.test.tsx delete mode 100644 packages/viewer/src/chrome/IdentityCard.tsx diff --git a/packages/viewer/e2e/focus.spec.ts b/packages/viewer/e2e/focus.spec.ts index 1a24b3f..13a132e 100644 --- a/packages/viewer/e2e/focus.spec.ts +++ b/packages/viewer/e2e/focus.spec.ts @@ -37,12 +37,19 @@ test('the toolbar button enters focus, and the URL never keeps it', async ({ pag await expect(header(page)).toBeVisible(); }); -test('in focus, H hides the toolbar and shows the way back; leaving focus restores it', async ({ page }) => { +test('H hides the toolbar outside focus too', async ({ page }) => { await page.goto('/?page=release-delivery'); await expect(page.locator('.sm-card').first()).toBeVisible(); - // Outside focus H does nothing. + await page.keyboard.press('h'); + await expect(tool(page, 'Focus on the canvas (Z)')).toHaveCount(0); + await expect(header(page)).toBeVisible(); await page.keyboard.press('h'); await expect(tool(page, 'Focus on the canvas (Z)')).toBeVisible(); +}); + +test('in focus, H hides the toolbar and shows the way back; leaving focus restores it', async ({ page }) => { + await page.goto('/?page=release-delivery'); + await expect(page.locator('.sm-card').first()).toBeVisible(); await page.keyboard.press('z'); await page.keyboard.press('h'); await expect(tool(page, 'Leave focus (Z)')).toHaveCount(0); @@ -56,7 +63,6 @@ test('in focus, H hides the toolbar and shows the way back; leaving focus restor await page.keyboard.press('z'); await expect(header(page)).toBeVisible(); await expect(tool(page, /^Play the flow/)).toBeVisible(); - await expect(tool(page, 'Hide the toolbar (H)')).toHaveCount(0); }); test('Z and H are ignored while typing in search', async ({ page }) => { diff --git a/packages/viewer/src/canvas/DiagramCanvas.test.tsx b/packages/viewer/src/canvas/DiagramCanvas.test.tsx index 034afa8..b8275c9 100644 --- a/packages/viewer/src/canvas/DiagramCanvas.test.tsx +++ b/packages/viewer/src/canvas/DiagramCanvas.test.tsx @@ -42,7 +42,8 @@ describe('DiagramCanvas', () => { ); nodeCardRenders.mockClear(); fireEvent.click(container.querySelector('.sm-card[data-card-id="orders"]')!); - // orders gains focus + the tab stop; edge (the default tab stop) loses it. The other four stay put. - expect([...new Set(nodeCardRenders.mock.calls.map(([id]) => id))].sort()).toEqual(['edge', 'orders']); + // orders gains focus + the tab stop; edge (the default tab stop) loses it; sessions, not a neighbour, dims. + // The other three stay put. + expect([...new Set(nodeCardRenders.mock.calls.map(([id]) => id))].sort()).toEqual(['edge', 'orders', 'sessions']); }); }); diff --git a/packages/viewer/src/chrome/IdentityCard.test.tsx b/packages/viewer/src/chrome/IdentityCard.test.tsx deleted file mode 100644 index b9b756a..0000000 --- a/packages/viewer/src/chrome/IdentityCard.test.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import { describe, expect, it } from 'vitest'; -import type { DiagramDraft } from '@stackmap/core'; -import { IdentityCard } from './IdentityCard'; - -const draft: DiagramDraft = { - kind: 'architecture', - title: 'A very long diagram title that will not fit in the identity card', - subtitle: 'A very long diagram subtitle that will not fit in the identity card either', - direction: 'RIGHT', - nodes: [], - edges: [], -}; - -describe('IdentityCard', () => { - it('carries title on the diagram title and subtitle', () => { - render(); - expect(screen.getByText(draft.title)).toHaveAttribute('title', draft.title); - expect(screen.getByText(draft.subtitle!)).toHaveAttribute('title', draft.subtitle); - }); -}); diff --git a/packages/viewer/src/chrome/IdentityCard.tsx b/packages/viewer/src/chrome/IdentityCard.tsx deleted file mode 100644 index 26ab797..0000000 --- a/packages/viewer/src/chrome/IdentityCard.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Ellipsis, Workflow } from 'lucide-react'; -import type { DiagramDraft } from '@stackmap/core'; -import { IconButton, PANEL_CLASS, PANEL_STYLE } from './ui'; - -export function IdentityCard({ draft, onDetails }: { draft: DiagramDraft; onDetails?: () => void }) { - return ( -
-
-
-
-
- {draft.title} -
- {draft.subtitle && ( -
- {draft.subtitle} -
- )} -
- {onDetails && ( - - - - )} -
- ); -} diff --git a/packages/viewer/src/chrome/Inspector.tsx b/packages/viewer/src/chrome/Inspector.tsx index b07b6bc..9bf71eb 100644 --- a/packages/viewer/src/chrome/Inspector.tsx +++ b/packages/viewer/src/chrome/Inspector.tsx @@ -38,12 +38,20 @@ const LINE_STYLES: { key: string; label: string; test: (e: DiagramEdge) => boole ]; function Legend({ draft }: { draft: DiagramDraft }) { + const { dispatch } = useExplore(); const lines = LINE_STYLES.filter((l) => draft.edges.some(l.test)); + // A row that unmounts under the pointer never gets its mouseleave. + useEffect(() => () => dispatch({ type: 'peek', nodeType: null }), [dispatch]); return ( <> -