From 89c496f5d289598823f6d2ed67f27c0195785282 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 19:37:47 +0000 Subject: [PATCH 01/10] Review follow-up 4: standards map first, filters behind one button, stacked map on phones The head ends on a quiet Filter disclosure and the live count, the map follows directly at its full height, the legend sits below it. Below 40rem a stacked arc-diagram drawing replaces the wide one. The matrix's disclosure styles move to a shared .disclosure in global.css. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_015K8n6c3uztMvwMXWSzxJW1 --- specs/016-visual-redesign/tasks.md | 1 + src/components/MatrixPage.astro | 28 +-- src/components/StandardsMap.astro | 380 +++++++++++++++++------------ src/components/StandardsMap.svelte | 15 +- src/lib/graph.ts | 70 ++++++ src/pages/standards/index.astro | 147 +++++------ src/styles/global.css | 31 +++ tests/e2e/standards.spec.ts | 63 ++++- tests/unit/graph.test.ts | 41 ++++ 9 files changed, 497 insertions(+), 279 deletions(-) diff --git a/specs/016-visual-redesign/tasks.md b/specs/016-visual-redesign/tasks.md index 38489ec..bc588a2 100644 --- a/specs/016-visual-redesign/tasks.md +++ b/specs/016-visual-redesign/tasks.md @@ -165,3 +165,4 @@ Items from the redesign review, one pull request each, in order. - [x] R-02 Small fixes: `.notice`, `.notice.caution` and `.card.notice` tint in oklab, not oklch (the toggle notice in `ChainPage.astro` keeps its local tint in the kind's ink); the link after "in the" on `/explorer/` and after "See" on `/standards/` no longer loses its space to HTML compression; the entry-format `pre` on `/contribute/` is focusable (axe scrollable-region-focusable) - [x] R-02a Imagery on the new system (asked on the R-02 pull request): the social card (`public/og.png`) redrawn as the home hero on flat tokens with the real matrix, replacing the pre-redesign card (glow, shadow, dot grid, slogan); the Apple touch icon on `--ground`; the README hero and how-it-works pictures on the new tokens without glow, shadow, gradient or dot grid, with MatrixThumb's marks and the home headline. The favicon and `public/brand/` already match the supplied logo and are unchanged - [x] R-03 Nav wording: the primary and footer navigation read Servers, Standards, Explorer, Breaches, Contribute (Scanner stays in the footer); every `` trail, the page-foot cross link on `/standards/` and the 404 buttons use the same words; `/mcp/` and `/standards/` take Servers and Standards as `` and eyebrow. "Atlas" stays the product word in ledes and body copy +- [x] R-04 Standards map first: on `/standards/` the head (h1 "The standards map", counts folded into the lede) ends on one quiet Filter button with the live count, the filters open in a disclosure below it (the matrix's caret styles moved to a shared `.disclosure` in `global.css`), and the map follows directly with the legend below it; "Recently revised" moves to the top of the list. The wide drawing fills the column at its full height (no inner vertical scroll; it scrolls sideways only below 60rem); below 40rem a stacked drawing (`layoutStacked`: one column of nodes per lane, relations as nested arcs on the right) replaces it at full width. The island filters and highlights both drawings. Baselines through the `refresh-screenshots` label diff --git a/src/components/MatrixPage.astro b/src/components/MatrixPage.astro index 9d69a9d..b99f947 100644 --- a/src/components/MatrixPage.astro +++ b/src/components/MatrixPage.astro @@ -97,7 +97,7 @@ const classWords = (counts: { value: string; count: number }[], label: Record<st </header> <div class="matrix-bar"> - <details class="filters" id="matrix-filter-panel"> + <details class="disclosure filters" id="matrix-filter-panel"> <summary class="button small quiet">Filter and group</summary> <form id="matrix-filters" class="toolbar explainer" method="get" action={hrefFor(revisionId)}> <div class="field search"> @@ -288,32 +288,6 @@ const classWords = (counts: { value: string; count: number }[], label: Record<st gap: var(--space-3) var(--space-5); margin: var(--space-6) 0 var(--space-4); } - .filters > summary { - list-style: none; - } - .filters > summary::-webkit-details-marker { - display: none; - } - .filters > summary::after { - content: ""; - width: 0.4em; - height: 0.4em; - margin: -0.2em 0 0 0.15em; - border: solid currentColor; - border-width: 0 1.5px 1.5px 0; - rotate: 45deg; - } - .filters[open] { - order: 2; - flex-basis: 100%; - } - .filters[open] > summary::after { - margin-top: 0.2em; - rotate: -135deg; - } - .filters[open] > .toolbar { - margin: var(--space-3) 0 0; - } .matrix-bar__count { margin: 0 0 0 auto; } diff --git a/src/components/StandardsMap.astro b/src/components/StandardsMap.astro index 341004e..9437450 100644 --- a/src/components/StandardsMap.astro +++ b/src/components/StandardsMap.astro @@ -13,11 +13,19 @@ import type { RelationType } from "@/schema"; import MaturityTag from "./MaturityTag.astro"; interface Props { + /** the wide drawing: lanes in rows, dependency depth in columns */ layout: Layout; + /** the phone drawing: one column with relations as arcs (layoutStacked) */ + stacked: Layout; } -const { layout } = Astro.props; -const { nodes, edges, lanes, width, height } = layout; +const { layout, stacked } = Astro.props; +const { nodes, edges } = layout; +// both drawings are in the HTML; CSS shows one by width, so each keeps its own marker ids +const drawings = [ + { id: "map", prefix: "", layout, stacked: false }, + { id: "map-stacked", prefix: "s-", layout: stacked, stacked: true }, +]; const byId = new Map(nodes.map((n) => [n.spec.id, n.spec])); const specs = nodes.map((n) => n.spec); const relations = edges.map((e) => e.relation); @@ -28,9 +36,6 @@ const usedMaturities = MATURITY_ORDER.filter((m) => specs.some((s) => s.maturity // M4: the longest builds-on chain draws in from the oldest document to the newest const spine = buildsOnSpine(specs, relations); const spineStep = new Map(spine.map((r, i) => [`${r.from}|${r.type}|${r.to}`, i])); -const spineEdges = edges.filter((e) => - spineStep.has(`${e.relation.from}|${e.relation.type}|${e.relation.to}`), -); const spineFirst = spine[0] ? byId.get(spine[0].to) : undefined; const spineLast = spine.at(-1) ? byId.get(spine.at(-1)?.from ?? "") : undefined; @@ -53,159 +58,171 @@ const key = (e: (typeof edges)[number]) => `${e.relation.from}|${e.relation.type id="map-frame" tabindex="0" role="region" - aria-label="Standards map, scrolls horizontally" + aria-label="Standards map" > - <svg - id="map" - viewBox={`0 0 ${width} ${height}`} - width={width} - height={height} - xmlns="http://www.w3.org/2000/svg" - aria-labelledby="map-title" - data-nodes={nodes.length} - data-edges={edges.length} - > - <title id="map-title"> - Specifications for agent authorization, one node each, grouped by publishing body in - rows and by what they build on in columns, with their relations drawn as lines. - - - { - directed.flatMap((t) => [`arrow-${t}`, `arrow-${t}-full`]).map((id) => ( - - - - )) - } - - - + { + drawings.map((drawing) => ( + + + {drawing.stacked + ? "Specifications for agent authorization, one node each, stacked by publishing body, with their relations drawn as arcs on the right." + : "Specifications for agent authorization, one node each, grouped by publishing body in rows and by what they build on in columns, with their relations drawn as lines."} + + + {directed + .flatMap((t) => [`arrow-${t}`, `arrow-${t}-full`]) + .map((id) => ( + + + + ))} + - + - - { - nodes.map((n) => { - const lines = wrapLabel(n.spec.name, 24, 2); - const label = `${n.spec.name}, ${BODY_LABEL[n.spec.body]}, ${MATURITY_LABEL[n.spec.maturity]}`; - const tag = MATURITY_TAG[n.spec.maturity]; - return ( - - - - ); - }) - } - - + d={e.d} + pathLength={onSpine ? "1" : undefined} + style={onSpine ? `--i: ${step}; --n: ${spine.length}` : undefined} + data-relation={key(e)} + data-type={e.relation.type} + data-from={e.relation.from} + data-to={e.relation.to} + marker-end={ + e.symmetric + ? undefined + : `url(#${drawing.prefix}arrow-${e.relation.type}${onSpine ? "-full" : ""})` + } + > + + {edgeTitle(e.relation.type, e.relation.from, e.relation.to, e.relation.note)} + + + ); + })} + + + + {drawing.layout.nodes.map((n) => { + const lines = wrapLabel(n.spec.name, drawing.stacked ? 30 : 24, 2); + const label = `${n.spec.name}, ${BODY_LABEL[n.spec.body]}, ${MATURITY_LABEL[n.spec.maturity]}`; + const tag = MATURITY_TAG[n.spec.maturity]; + return ( + + + + + {tag} + + + {lines.map((line, i) => ( + + {line} + + ))} + + + ); + })} + + + )) + }
- Rows are publishing bodies. A specification sits one column right of what it builds on, - profiles or replaces.{" "} + + Rows are publishing bodies. A specification sits one column right of what it builds on, + profiles or replaces. + + + Specifications stack by publishing body, and the arcs on the right are their relations. + {" "} { spineFirst && spineLast && ( <> @@ -261,19 +278,48 @@ const key = (e: (typeof edges)[number]) => `${e.relation.from}|${e.relation.type .map { margin: 0 0 var(--space-6); } + /* + * Wide screens: the whole drawing at the column's width, so the page scrolls and the frame + * does not; below 60rem of drawing it keeps that width and scrolls sideways. Phones get the + * stacked drawing at full width instead (follow-up 4). + */ .map-frame { padding: 0; - overflow: auto; - max-height: 80vh; + overflow-x: auto; + } + .map-svg { + display: block; + width: 100%; + height: auto; + font-family: var(--font-body); + } + .map-svg--wide { + min-width: 60rem; + } + .map-svg--stacked { + max-width: 30rem; + margin-inline: auto; + } + .map-svg--stacked, + .map-only-stacked { + display: none; + } + @media (max-width: 40rem) { + .map-svg--wide, + .map-only-wide { + display: none; + } + .map-svg--stacked { + display: block; + } + .map-only-stacked { + display: inline; + } } .map-frame:focus-visible { outline: 2px solid var(--signal); outline-offset: 3px; } - svg { - display: block; - font-family: var(--font-body); - } figcaption { margin-top: var(--space-3); color: var(--ink-2); @@ -458,29 +504,41 @@ const key = (e: (typeof edges)[number]) => `${e.relation.from}|${e.relation.type * Filtering and the neighbourhood highlight, driven by the island. Hidden items keep their box * (visibility, not display) so the spine's one-time draw-in does not restart on a filter. */ - :global(#map .is-hidden) { + :global(.map-svg .is-hidden) { visibility: hidden; } - :global(#map .is-far) { + :global(.map-svg .is-far) { opacity: 0.18; } - :global(#map .edge.is-near) { + :global(.map-svg .edge.is-near) { --edge: var(--c); stroke-width: 2.5; } :global(#map .edge-builds-on.is-near) { marker-end: url(#arrow-builds-on-full); } + :global(#map-stacked .edge-builds-on.is-near) { + marker-end: url(#s-arrow-builds-on-full); + } :global(#map .edge-profiles.is-near) { marker-end: url(#arrow-profiles-full); } + :global(#map-stacked .edge-profiles.is-near) { + marker-end: url(#s-arrow-profiles-full); + } :global(#map .edge-replaces.is-near) { marker-end: url(#arrow-replaces-full); } + :global(#map-stacked .edge-replaces.is-near) { + marker-end: url(#s-arrow-replaces-full); + } :global(#map .edge-references.is-near) { marker-end: url(#arrow-references-full); } - :global(#map .node.is-near .node-box) { + :global(#map-stacked .edge-references.is-near) { + marker-end: url(#s-arrow-references-full); + } + :global(.map-svg .node.is-near .node-box) { stroke: var(--ink); stroke-width: 1.5; } diff --git a/src/components/StandardsMap.svelte b/src/components/StandardsMap.svelte index c854461..3799db1 100644 --- a/src/components/StandardsMap.svelte +++ b/src/components/StandardsMap.svelte @@ -1,7 +1,7 @@