Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ package of the redesign that builds it. Anything not in this table does not anim
| M1 | Every page | Cross-document view transition, root crossfade 220ms | continuity | none (as today) | WP2 |
| M2 | Home → `/mcp/`, `/standards/` → spec, `/explorer/` → scenario | Shared `view-transition-name` on the data view (matrix plate from `MatrixThumb.astro` as `matrix`, map; on the explorer, the scenario title into the page h1) so it morphs instead of crossfading | "this is the same object, larger" | none | WP3, WP5, WP6 |
| M3 | Home facts, server page facts, spec page facts (`Facts.astro`) | Count-up from 0 to the value, 600ms, once, when 50% in view, tabular figures | "these are live numbers" | value rendered at once | WP3 |
| M4 | Standards map, decorative plate | Highlighted path draws in (`stroke-dashoffset`), 1200ms, halo fades after, once on load | "follow this path" | path shown complete | WP5, WP8 |
| M4 | Decorative plate | Highlighted path draws in (`stroke-dashoffset`), 1200ms, halo fades after, once on load. The standards map dropped its draw-in in review follow-up R-12: on a merged trunk only one or two arrows moved, so it read as a glitch; the spine is drawn bold and still | "follow this path" | path shown complete | WP5, WP8 |
| M5 | Explorer chain | Per step: the sender and receiver light up in the actor head, the current arrow draws in (300ms, ease-out), the body fades up 8px (220ms); earlier steps keep a quieter arrow and later ones a hairline, in colour, not opacity, so text keeps its contrast | "you are here" | arrow and body shown, highlight and dimming applied without transition | WP6, R-08 |
| M6 | Explorer chain | One scene clock (a Web Animations API timeline) holds the playhead; Play, Pause, Previous, Next, the scrub bar, a click on a step or a log line and the arrow keys all move it. Steps advance every 2.5 s. Auto-advance (amended by review follow-up 8): armed once per page, it starts when the chain has sat in view for 1 s with no input, and stops for good on hover, touch, focus, scroll or any key; it never restarts by itself and never starts with a step in the URL. Play, once pressed, pauses on any click, key or scroll | guided reading | Play button hidden, no auto-advance; the scrub bar and the step buttons still seek | WP6, R-08 |
| M7 | Explorer token view | On step change, claims that differ from the previous hop get a 2px `--signal` rail that fades to `--rule` over 480ms, starting when the message has drawn (300ms) and 40ms apart | "this is what changed in the token" | rail shown without fade | WP6, R-08 |
Expand All @@ -265,6 +265,7 @@ package of the redesign that builds it. Anything not in this table does not anim
| M16 | Theme toggle | Icon swap only; no rotation | n/a | n/a | WP2 |
| M17 | Explorer toggles | A toggle link swaps the chain in place (crumbs, toggles, notice, steps) through a same-document view transition, 220ms; steps the two chains share morph, the rest fade, and M8 runs on the new rails. The toggle stays its own static page for a reload, a new tab or no JS | "the same chain, with one thing changed" | ordinary navigation | R-08 |
| M18 | Explorer chain | A quiet decision log under the chain gains one line per step reached (number, sender → receiver, message), the newest fading in over 220ms; a line seeks back to its step | "what has happened so far" | lines appear | R-08 |
| M19 | Standards map | Neighbourhood highlight on hover once the pointer rests on a node for 90ms, at once on keyboard focus: the other nodes and lines fade to 18% over 220ms and the node's own lines take full colour; leaving clears after 160ms unless the pointer reaches another node, so a sweep across the map moves the highlight once instead of flashing the drawing | "these are related" | the same delays, the fade instant | R-12 |

Removed from the site by the redesign: the pulsing dot in the work-in-progress banner (it loops),
the hover lifts on cards, buttons and the hero thumbnail, the rotating theme toggle, the ambient
Expand Down
3 changes: 2 additions & 1 deletion specs/016-visual-redesign/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ package that builds it. Anything not in this table does not animate.
| M1 | Every page | Cross-document view transition, root crossfade 220ms | continuity | none (as today) | WP2 |
| M2 | Home → `/mcp/`, `/standards/` → spec, `/explorer/` → scenario | Shared `view-transition-name` on the data view (matrix plate, map, chain head) so it morphs instead of crossfading | "this is the same object, larger" | none | WP3, WP5, WP6 |
| M3 | Home facts, server page facts | Count-up from 0 to the value, 600ms, once, when 50% in view, tabular figures | "these are live numbers" | value rendered at once | WP3 |
| M4 | Standards map, decorative plate | Highlighted path draws in (`stroke-dashoffset`), 1200ms, halo fades after, once on load | "follow this path" | path shown complete | WP5, WP8 |
| M4 | Decorative plate | Highlighted path draws in (`stroke-dashoffset`), 1200ms, halo fades after, once on load. The standards map dropped its draw-in in review follow-up R-12: on a merged trunk only one or two arrows moved, so it read as a glitch; the spine is drawn bold and still | "follow this path" | path shown complete | WP5, WP8 |
| M5 | Explorer chain | Per step: the sender and receiver light up in the actor head, the current arrow draws in (300ms, ease-out), the body fades up 8px (220ms); earlier steps keep a quieter arrow and later ones a hairline, in colour, not opacity, so text keeps its contrast | "you are here" | arrow and body shown, highlight and dimming applied without transition | WP6, R-08 |
| M6 | Explorer chain | One scene clock (a Web Animations API timeline) holds the playhead; Play, Pause, Previous, Next, the scrub bar, a click on a step or a log line and the arrow keys all move it. Steps advance every 2.5 s. Auto-advance (amended by review follow-up 8): armed once per page, it starts when the chain has sat in view for 1 s with no input, and stops for good on hover, touch, focus, scroll or any key; it never restarts by itself and never starts with a step in the URL. Play, once pressed, pauses on any click, key or scroll | guided reading | Play button hidden, no auto-advance; the scrub bar and the step buttons still seek | WP6, R-08 |
| M7 | Explorer token view | On step change, claims that differ from the previous hop get a 2px `--signal` rail that fades to `--rule` over 480ms, starting when the message has drawn (300ms) and 40ms apart | "this is what changed in the token" | rail shown without fade | WP6, R-08 |
Expand All @@ -217,6 +217,7 @@ package that builds it. Anything not in this table does not animate.
| M16 | Theme toggle | Icon swap only; no rotation | — | — | WP2 |
| M17 | Explorer toggles | A toggle link swaps the chain in place (crumbs, toggles, notice, steps) through a same-document view transition, 220ms; steps the two chains share morph, the rest fade, and M8 runs on the new rails. The toggle stays its own static page for a reload, a new tab or no JS | "the same chain, with one thing changed" | ordinary navigation | R-08 |
| M18 | Explorer chain | A quiet decision log under the chain gains one line per step reached (number, sender → receiver, message), the newest fading in over 220ms; a line seeks back to its step | "what has happened so far" | lines appear | R-08 |
| M19 | Standards map | Neighbourhood highlight on hover once the pointer rests on a node for 90ms, at once on keyboard focus: the other nodes and lines fade to 18% over 220ms and the node's own lines take full colour; leaving clears after 160ms unless the pointer reaches another node, so a sweep across the map moves the highlight once instead of flashing the drawing | "these are related" | the same delays, the fade instant | R-12 |

Explicitly removed from today's site: the pulsing dot in the work-in-progress banner (it loops),
the hover lifts on cards, buttons and the hero thumbnail, the rotating theme toggle, the ambient
Expand Down
1 change: 1 addition & 0 deletions specs/016-visual-redesign/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,4 @@ Items from the redesign review, one pull request each, in order.
- [x] R-09 Content pass (WP9 above): section-root headlines under 24 characters, the title suffix and footer statement, an accessibility statement that lists every motion with its reduced-motion form, `check:prose` as a gate, and the about, contribute, made-in-Europe, legal and 404 pages in plain English
- [x] R-10 Decorative plate (WP8 above): a deterministic miniature delegation chain in the head of the explorer and breach indexes and every spec page, beside the title on wide screens, drawn in once (M4) and complete under reduced motion, with social cards for the two section roots from the same generator
- [x] R-11 Spec page composition: under the head, relations and features take the wide column and a panel beside them holds the figures (revisions, relations, features, counting up as M3) and the revision timeline (its rail drawing in as M13, the entries still so their contrast holds), sticky as on a breach page; below 60rem the panel follows the relations. Relation groups and feature topics pack into columns (`columns: 19rem`) instead of one long list; a group heading is the verb phrase alone ("Builds on", "Is built on by") and the source link follows the note. Features group by topic under a bar of how many carry each level, named in words beside it. A spec with no recorded features shows a sentence in the panel, not a zero and not an empty section. The withdrawn or expired notice moves into the main column. Baselines through the `refresh-screenshots` label
- [x] R-12 Standards map polish (asked by Jan): the drawing is about a third shorter and fills its plate. `layoutGraph` lays out each family of documents joined by dependencies on its own and packs them onto one column grid: the largest (the OAuth family) goes top left, then the narrow ones first, each into the run of columns whose floor is highest up, so the standalone documents stack by body beside the long first column and the WIMSE, SD-JWT and EU families fill the room under the MCP column instead of a row under everything (2151 to 1385 units tall at today's data). The hover highlight waits for the pointer to rest on a node (90ms) and clears 160ms after it leaves unless it reaches another node, so a sweep across the map no longer flashes the whole drawing; keyboard focus acts at once; nodes and lines fade together over `--dur` (M19, new). The spine draw-in and its halo are gone from the map (on the merged trunk only one or two arrows moved): the spine stays bold and still; M4 now covers the decorative plate only. Baselines through the `refresh-screenshots` label
82 changes: 13 additions & 69 deletions src/components/StandardsMap.astro
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ const usedMaturities = MATURITY_ORDER.filter((m) => specs.some((s) => s.maturity
// references are the most and the least telling lines, so the map opens without them
const shownByDefault = (t: RelationType) => t !== "references";

// M4: the longest builds-on chain draws in from the oldest document to the newest
// the longest builds-on chain is drawn bold, 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 onSpine = new Set(spine.map((r) => `${r.from}|${r.type}|${r.to}`));
const spineFirst = spine[0] ? byId.get(spine[0].to) : undefined;
const spineLast = spine.at(-1) ? byId.get(spine.at(-1)?.from ?? "") : undefined;

Expand All @@ -61,10 +61,10 @@ const unchecked = (name: string, value: string) =>
const filterCss = [
...usedTypes.map(
(t) =>
`${unchecked("type", t)} :is(#map .edge-${t}, #map .spine-halo[data-type="${t}"]) { visibility: hidden }\n` +
`${unchecked("type", t)} #map .edge-${t} { visibility: hidden }\n` +
`${unchecked("type", t)} #spec-list .xl[data-type="${t}"] { display: none }`,
),
`${scope}:has(input[name="view"][value="recent"]:checked) :is(#map .node:not([data-recent]), #map .band, #map .edge, #map .spine-halo) { opacity: 0.2 }`,
`${scope}:has(input[name="view"][value="recent"]:checked) :is(#map .node:not([data-recent]), #map .band, #map .edge) { opacity: 0.2 }`,
`${scope}:has(input[name="view"][value="recent"]:checked) #spec-list li:not([data-recent]) > .row { opacity: 0.35 }`,
...bodies.map(
(b) =>
Expand Down Expand Up @@ -191,35 +191,17 @@ const filterCss = [
</g>

<g class="edges" aria-hidden="true">
{
edges
.filter((e) => spineStep.has(key(e)))
.map((e) => (
<path
class="spine-halo"
d={e.d}
pathLength="1"
data-type={e.relation.type}
data-from-body={e.from.spec.body}
data-to-body={e.to.spec.body}
style={`--i: ${spineStep.get(key(e))}; --n: ${spine.length}`}
/>
))
}
{
edges.map((e) => {
const step = spineStep.get(key(e));
const onSpine = step !== undefined;
const spined = onSpine.has(key(e));
return (
<path
class:list={[
"edge",
`edge-${e.relation.type}`,
{ symmetric: e.symmetric, spine: onSpine },
{ symmetric: e.symmetric, spine: spined },
]}
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}
Expand All @@ -229,7 +211,7 @@ const filterCss = [
marker-end={
e.symmetric
? undefined
: `url(#arrow-${e.relation.type}${onSpine ? "-full" : ""})`
: `url(#arrow-${e.relation.type}${spined ? "-full" : ""})`
}
>
<title>
Expand Down Expand Up @@ -290,7 +272,9 @@ const filterCss = [
</div>
<figcaption id="map-caption">
A specification sits one column right of what it builds on, profiles or replaces, and the
tinted bands group each column by publishing body.{" "}
tinted bands group each column by publishing body. Families that build on none of the
OAuth documents, and documents that stand alone, fill the free room beside the larger
family.{" "}
{
spineFirst && spineLast && (
<>
Expand Down Expand Up @@ -561,7 +545,7 @@ const filterCss = [
stroke: var(--edge);
stroke-width: 1.5;
transition:
opacity var(--dur-fast) var(--ease),
opacity var(--dur) var(--ease),
stroke var(--dur-fast) var(--ease);
}
.edge-builds-on,
Expand Down Expand Up @@ -597,56 +581,16 @@ const filterCss = [
fill: var(--c);
}

/* the builds-on spine: full colour, drawn in once on load from the oldest end (M4) */
/* the builds-on spine: full colour and a heavier line, drawn as it is */
.edge.spine {
--edge: var(--c);
stroke-width: 2;
stroke-dasharray: 1;
}
.spine-halo {
fill: none;
stroke: var(--signal-soft);
stroke-width: 9;
stroke-linecap: round;
stroke-dasharray: 1;
opacity: 0;
}
@media (prefers-reduced-motion: no-preference) {
.edge.spine {
animation: map-draw calc(var(--dur-draw) / var(--n)) linear
calc(var(--dur-draw) / var(--n) * var(--i)) both;
}
.spine-halo {
animation:
map-draw calc(var(--dur-draw) / var(--n)) linear
calc(var(--dur-draw) / var(--n) * var(--i)) both,
map-halo calc(var(--dur-draw) + var(--dur-slow)) linear both;
}
}
/* a negative offset reveals the path from its end, where the older document sits */
@keyframes map-draw {
from {
stroke-dashoffset: -1;
}
to {
stroke-dashoffset: 0;
}
}
/* the halo shows while the spine draws (1200 of 1680ms), then fades out */
@keyframes map-halo {
0%,
100% {
opacity: 0;
}
2%,
71% {
opacity: 1;
}
}

.node {
cursor: pointer;
text-decoration: none;
transition: opacity var(--dur) var(--ease);
}
.node-box {
fill: var(--ground);
Expand Down
35 changes: 31 additions & 4 deletions src/components/StandardsMap.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,10 @@ onMount(() => {
(neighbours.get(a) ?? neighbours.set(a, new Set()).get(a))?.add(b);
(neighbours.get(b) ?? neighbours.set(b, new Set()).get(b))?.add(a);
}
let current: string | null = null;
function highlight(id: string | null) {
if (id === current) return;
current = id;
show(id);
if (!id) {
for (const el of [...nodes, ...edges]) el.classList.remove("is-near", "is-far");
Expand All @@ -130,12 +133,36 @@ onMount(() => {
edge.classList.toggle("is-far", !isNear);
}
}
/*
* Hover intent: a pointer crossing the map passes over many nodes, and switching the whole
* drawing on each one reads as flicker. A node takes the highlight once the pointer rests on
* it, and leaving waits a moment, so a sweep from one node to the next moves the highlight
* once instead of clearing and refilling the map. Keyboard focus is deliberate and acts at once.
* These are delays of intent, not motion, so they hold under reduced motion too.
*/
const REST_MS = 90;
const LEAVE_MS = 160;
let pending: ReturnType<typeof setTimeout> | undefined;
const later = (ms: number, id: string | null) => {
clearTimeout(pending);
pending = setTimeout(() => highlight(id), ms);
};
const focused = () => nodes.find((n) => n === document.activeElement)?.dataset.spec ?? null;
const now = (id: string | null) => {
clearTimeout(pending);
highlight(id);
};
for (const node of nodes) {
const id = node.dataset.spec ?? null;
node.addEventListener("mouseenter", () => highlight(id));
node.addEventListener("mouseleave", () => highlight(null));
node.addEventListener("focus", () => highlight(id));
node.addEventListener("blur", () => highlight(null));
node.addEventListener("pointerenter", (e) => {
if (e.pointerType !== "touch") later(id === current ? 0 : REST_MS, id);
});
// leaving goes back to the node in keyboard focus, if there is one
node.addEventListener("pointerleave", (e) => {
if (e.pointerType !== "touch") later(LEAVE_MS, focused());
});
node.addEventListener("focus", () => now(id));
node.addEventListener("blur", () => now(null));
}

// M2: name the followed link for the view transition. A map node is SVG, which a view
Expand Down
Loading