Uh oh!
There was an error while loading. Please reload this page.
Simplify the skills explorer and add a 2D relationship canvas - #24
Merged
Conversation
Records the two problems this work exists to solve — a catalog that costs 34 paragraphs to read, and a reference graph drawn as a fixed ring nobody can explore — with measurable success criteria and IDed acceptance criteria. The design doc carries the contract: the repository stays the only source of truth, placement is deterministic rather than simulated, the list is a peer of the canvas rather than a fallback, and interaction state stays ours while the renderer owns only the viewport. It also records what was rejected and why. A 3D force graph costs a WebGL context and a physics loop on a page whose job is reading, occludes its own labels, degrades badly on touch, and has no honest accessible equivalent. It is prior art here, not a later phase.
The circular layout gave every skill identical weight, carried no grouping, and could not be rearranged, so it answered "are these connected" and nothing else. Skills now sit in fixed category lanes, alphabetical within a lane, and the same graph produces the same map on every load — a picture you can refer back to next week is worth more than one that looks alive. React Flow owns the viewport: pan, zoom, node drag, minimap and controls. Selection, pinning, URL sync and persistence stay in the view, so the renderer is replaceable and the inspector renders directly rather than behind an animation that could leave it blank. Dragged positions live in sessionStorage under a versioned key; a payload of the wrong version or shape is discarded whole rather than half-restored, and Reset layout returns to canonical. The list is a first-class mode, not a fallback: it carries every skill and both directions of every relationship, it is the default under 768px, and it is what loads when the canvas is never opened — the renderer is imported only when the canvas view is.
Choosing one skill out of 34 meant reading 34 summary paragraphs, while the things that actually decide the choice — the handle you would type and the phrase that triggers it — were buried inside prose written for another purpose. A card now carries the handle, the title, the category and one trigger phrase. Summaries, file counts and reference counts move to detail, where someone is comparing rather than scanning. The header is a word and a sentence. Sort and capability flags fold behind one Filters control, with any active filter shown as a chip you can remove. The default view groups by category, because at 34 skills across nine groups that is the first useful cut and a flat alphabetical wall makes the reader do the grouping themselves. Search, ranking, filters and every deep link behave exactly as before.
The two roadmap items PR #15 delivered were still sitting in "now" after it merged. They become one shipped entry referencing the merge commit, and this work takes their place as current. Adds the changelog entry for the quieter catalog and the relationship map, including that detail, workspace and browser-local drafts are unchanged — this release changed how you find a skill, not what you can do with it.
The changelog entry and the roadmap item were written before the PR existed, so both carried a guessed number. They name PR #24 now.
Self-review against the plan found three things I had written down as true and not built, plus one that would have shipped broken. Hover tracing did not exist. The requirements doc carried an acceptance criterion for it and the design doc described it, which is worse than a missing feature — the docs were lying. Hover now traces a skill while the pin keeps its own marker and the inspector stays on the pinned skill. The pin's halo never rendered. It was written as a Tailwind arbitrary shadow containing a color-mix() with commas, which the class parser drops silently, so a pinned node and a hovered node were pixel-identical. It is an inline style now, which is exactly the distinction this component exists to keep. Nodes carried no declared size, so React Flow held every one at visibility: hidden until it had measured them. Measurement rides on ResizeObserver delivery, and where that is throttled the entire map renders blank — and hidden nodes are unfocusable, which is why keyboard access to the canvas did not work either. The layout knows every size, so the nodes say so. The mobile inspector was a bare sheet. It is a dialog now, with a label, focus moved in, Tab trapped and Escape to close, matching the workspace file drawer. The catalog also drops to two columns at 1280 to match the agreed matrix; a third column has nowhere to live, since the inspector takes over at 1440.
The map was technically correct and unpleasant to use. It opened fitted to the viewport, so eight lanes were squeezed into a panel and every node rendered at roughly a centimetre of unreadable colour; the shell, the page header, three stat cards and a permanently docked inspector took the rest. It read as a dashboard with a small graph in it. Focus mode. F gives the canvas the whole application viewport: rail, header, heading and stats collapse out of the way, the inspector becomes an overlay, and a floating toolbar carries the controls. It is app level rather than the browser Fullscreen API, and it works by setting one attribute on <html> and collapsing the shell around a canvas that never moves in the React tree, so React Flow is neither unmounted nor duplicated and the viewport survives the switch. Esc peels one layer at a time, R resets the layout, and all three are ignored while a text field has focus. The mode is carried in the URL. Readable by default. The canvas no longer fits the graph on load: with ?skill= it centres that node at zoom 1.1, otherwise it anchors the map's top left corner at zoom 1, and Fit all becomes a deliberate control. Nodes are larger, their type is larger, lanes are three columns wide so nothing starts off screen, and below zoom 0.62 a card drops its metadata row instead of drawing it at sub 8px. Selecting a skill moves the viewport only when the node is off frame or too small to read. Layout version is bumped, since the geometry a stored position describes no longer exists. Less chrome. The heading is two lines and one row of counts instead of three stat cards, the hint moved into a help popover that opens itself once per session, and the docked inspector takes layout only while something is selected, so the empty state is all canvas. Accessibility, from an axe-core 4.13 run over the catalog, both canvas layouts, the list view, the mobile inspector, the detail page and the workspace. That run found four real defects, now fixed: informational text at --text-disabled measuring 2.54:1 to 4.27:1 on the light palette; an aria-label on a bare div; a tablist owning links and buttons instead of only tabs; and, once the close control moved inside its tab, a focusable button nested in a widget. The close affordance is now pointer only and out of the accessibility tree, with Delete or Backspace closing the focused tab. GooeyTextReveal stops asking GSAP for aria, which was putting a prohibited label on a paragraph and hiding every line under it. All seven surfaces now report zero violations at WCAG 2.0 and 2.1 A and AA, and the lowest measured contrast is 5.03:1 dark and 5.35:1 light.
The map was correct and mute. It said references, referenced by, entry points, out and in — accurate about the data structure, silent about the question anyone opens the page with: which skill do I reach for, and what goes with it. A node gave a handle, a category and two numbers, so a reader had to know how connected something was before they could learn what it did. Vocabulary, defined once and used everywhere. References became uses, referenced by became used by, out and in became Uses n and Used by n, graph became map, entry points became starting points, Fit all became Show whole map, and the page is called How skills connect. The map, the browse list, the detail page and the catalog now use the same words, so a reader who works out the direction on one surface does not work it out again on the next. Two things had to exist first. SKILL_PURPOSE is a curated one-line purpose per skill, in the site layer beside CATEGORY_MAP, because frontmatter carries a name, a version and a long trigger-shaped description and none of them answer "what is this?" in a card-width line; extracting a clause reads unevenly across 34 skills, and this way changing one is a copy edit in one file rather than a change to the skills themselves. oftenUsedWith is not an edge: two skills keep company when some third skill pulls in both, and counting those shared parents answers what else a reader will want open, which neither direction of the real edge does. A way in before anything is selected. The rail used to vanish until you clicked something, so the one moment a reader most needs help was the moment the page offered dots. It now holds a start panel: group chips that frame a lane, and the skills nothing else pulls in, with what each leads to. Under 1280 and in focus mode the same groups are a strip over the map, and the browse list carries them as its first two sections. A starting point is a skill nothing pulls in that pulls in something itself — the naive in-degree-zero list includes one skill with no connections at all, and sending someone there is worse than silence. A node card is now a handle, a title and what the skill is for. The counts moved to the panel, where they sit beside the list each one describes. Framing is clamped to the map, and centred against what is visible rather than the element, so a deep link to a skill in the first row no longer opens with a third of the canvas showing nothing, and the focus drawer no longer covers the node the map just centred. Also fixed, found while re-running the audit: the rendered-document and eval scrollers had no focusable child whenever a skill's body happened to carry no links, which left a keyboard user unable to scroll them. 37 end-to-end checks pass against a production build, including axe runs on all seven surfaces at WCAG 2.0 and 2.1 A and AA.
The canvas panel was translucent, so the animated wash behind the shell came through it and lifted the surface to something lighter than the page itself. The dot grid, drawn at the same value as a panel border, then had nothing to sit against and disappeared: the map read as a pale rectangle with cards floating on it rather than as a space you look into. The map now has its own surface token, opaque and below the page instead of above it — near black in dark, white in light — with a dot colour picked to read against it, and it is a fraction larger. Focus mode drops the wash entirely; there is nothing decorative left to see past. The minimap takes the canvas colour too, since an elevated panel in that corner was the brightest thing on a near-black screen, which is backwards for an overview. 37 end-to-end checks still pass, axe still reports nothing on all seven surfaces.
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Product intent
/skillshad two costs. Choosing one skill out of 34 meant reading 34 summaryparagraphs, while the things that decide the choice — the handle you would type
and the phrase that triggers it — sat inside prose written for another purpose.
And the reference graph, which is the real structure of the kit, was drawn as a
fixed circular ring: every skill the same weight, no grouping, nothing you could
move. It answered "are these connected" and nothing else.
This makes the catalog something you scan and the graph something you explore.
2D over 3D
A 3D force graph was considered and rejected, and that is recorded in the design
doc as prior art rather than a promise. It costs a WebGL context and a physics
loop on a page whose job is reading, it occludes its own labels, it degrades
badly on touch, and it has no honest accessible equivalent — the list would
quietly become the real interface for a large share of readers while the 3D view
took the budget. A force simulation was rejected for a smaller reason: placement
that differs on every load is a map you cannot refer back to.
What changed
Catalog. Header is a word and a sentence. A card carries the handle, title,
category and one trigger phrase; summaries, file counts and reference counts
move to detail where someone is comparing rather than scanning. Sort and
capability flags fold behind one
Filterscontrol, with active filters shown asremovable chips. The default view groups by category. Search, ranking, filters
and every deep link behave exactly as before.
Relationship canvas. Skills sit in fixed category lanes, alphabetical within
a lane, so the same graph produces the same map every load. React Flow owns pan,
zoom, drag, minimap and controls; selection, pinning, URL sync and persistence
stay in the view. Dragged positions persist per browser tab under a versioned
key, and
Reset layoutreturns to canonical. The list is a first-class modecarrying every skill and both directions of every relationship, and it is the
default under 768px.
Preserved. Detail, workspace, Preview, Source, Edit and browser-local drafts
are untouched. This changed how you find a skill, not what you can do with it.
URL contracts
/skills?query=&category=&sort=&has=&skill=/skills/visualize-interactions?skill=<id>cmk:prefix accepted/skills/visualize-interactions?view=canvas|list/skills/[skillId]/workspace?file=&mode=Pan and zoom are local UI state and deliberately stay out of the URL.
Responsive
Verified
translate(1234px, 987px)from a seeded payload) and awrong-version payload is discarded back to the canonical position.
?skill=cmk:delivery-reviewselects and normalizes to?skill=delivery-review.?view=listrenders the list and never mounts the canvas.route's first load is 113 kB, down from 161 kB.
type-check,lint, cleanbuild(85 static pages) andgit diff --checkpass.Known accepted gaps
relationship they hint at is at full contrast in the traced state, the
inspector and the list.
layer either side of it is verified directly, but a physical drag was not
scripted.
to its browser profile in this environment.
Scope
PR #22 and PR #21 were not opened, inspected, reviewed, modified, rebased or
merged. Nothing outside the skills dashboard, docs, roadmap and changelog is
touched.
Self-review corrections (
d9a8968)Checking this branch against the plan rather than trusting the first pass found
four defects, three of which the documentation had already claimed as working.
SKEX-2.2and the design doc both describedit. Hover now traces while the pin keeps its marker and the inspector stays on
the pinned skill.
a
color-mix()with commas, which the class parser drops silently, so pinnedand hovered nodes were pixel-identical. Inline style now.
visibility: hiddenpending measurement. Where ResizeObserver delivery isthrottled the map renders blank, and hidden nodes cannot take focus, which is
why canvas keyboard access did not work. Sizes are declared now: 42/42 visible
and node focus confirmed working.
aria-modal, focus moved in, Tab trapped, Escape closes.Also corrected: the catalog renders 2 columns at 1280, matching the agreed
matrix. The table above said 3, which was wrong. A third column has nowhere to
live, because the inspector takes over at 1440 — say the word if you would
rather move the inspector breakpoint up and keep a 3-column band.
Re-verified after the fixes
document.activeElementis the node, name readscmk:cicd, Setup & Infra. 7 references out…role=dialog,aria-modal, labelled, focus in, Escape closes and clears?skill=/skillsStill not verified
No automated axe or Lighthouse run — the devtools integration cannot attach to
its browser profile here. A physical pointer drag of a node was not scripted;
the persistence layer either side of it is verified directly. And I cannot
attach screenshots to this PR from my environment, so the evidence above is
measurements rather than images.
Gate closure
Node count, 34 vs 42. The canvas renders 42 React Flow nodes: 34 skill
nodes, matching the catalog exactly, plus 8 lane heading nodes with ids
lane:delivery,lane:docs,lane:setup,lane:agent,lane:testing,lane:sui,lane:sync,lane:session. Headings are nodes so the categorystructure pans and zooms with the map instead of floating over it. No dangling
reference is rendered as a skill.
Real pointer drag, full lifecycle. Not just the persistence layer:
adrwith a real pointer fromtranslate(0px, 372px).translate(295.619px, 554.857px).sessionStorageholds{"version":1,"positions":{"adr":{"x":295.62,"y":554.86}}}.translate(295.619px, 554.857px).Reset layout— back totranslate(0px, 372px), storage cleared.Live resize, not just reload.
?query=adr&skill=adrskill, filter intactNo 40px panel, no stale selection, no URL overwrite.
Technical gate from clean state.
git statusshows only untracked.claude/, which is not in the diff. 15 files changed againstorigin/main,none outside skills UI, docs, roadmap and changelog.
git diff --checkclean.No commit in this branch references PR #22 or PR #21.
type-check,lintand aclean
build(85 static pages) all pass.Canvas first revision (
44957ea)Review feedback on the previous head was that the feature was correct and
unpleasant: the map opened fitted to the viewport, so eight lanes were squeezed
into a panel and nodes rendered at roughly a centimetre of unreadable colour,
while the shell, the page header, three stat cards and a permanently docked
inspector took the rest of the screen. It read as a dashboard containing a
small graph. This revision makes the canvas the page.
Focus mode.
Fgives the canvas the whole application viewport: rail,header, heading and stats collapse away, the inspector becomes an overlay, and
a floating toolbar carries Exit focus, Canvas/List, zoom, Fit all, Reset and
help. It is application level rather than the browser Fullscreen API, and it
works by setting
data-skills-focuson<html>and collapsing the shell arounda canvas that never changes position in the React tree. Measured: the same
.react-flowelement survives both directions of the toggle, exactly oneinstance exists, and the viewport transform is unchanged across the switch.
Escpeels one layer at a time,Rresets the layout, and all three areignored while a text field has focus. The mode round trips through
?focus=canvas.Readable by default. No blanket
fitViewon load. With?skill=the canvascentres that node at zoom 1.1; otherwise it anchors the map's top left corner at
zoom 1, so the first thing on screen is a whole card.
Fit allis now anexplicit control. Nodes and their type are larger, lanes are three columns wide
so no column starts off screen, and below zoom 0.62 a card drops its metadata
row rather than drawing it at sub 8px. Selecting a skill moves the viewport only
when the node is off frame or the zoom is below legible.
LAYOUT_VERSIONis 2,because stored positions describe geometry that no longer exists.
Less chrome. Two lines and one row of counts instead of three stat cards.
The hint moved into a help popover that opens itself once per session. The
docked inspector takes layout only while a skill is selected, so the empty state
is all canvas: 976px of a 1280 viewport, 100% of the panel.
Automated accessibility, now closed
The DevTools integration could not attach (
The browser is already running for /Users/.../chrome-devtools-mcp/chrome-profile), so the audit was run adifferent way: axe-core 4.13.0 served same origin and executed in the page, with
transitions disabled first, because this environment throttles rAF and a
mid-transition
getComputedStylereports the previous theme's colours./skillsScope: WCAG 2.0 and 2.1, levels A and AA. The run found four real defects, all
fixed in this commit:
--text-disabledmeasuring 2.54:1 to 4.27:1 on thelight palette, across 22 nodes in the catalog and the lane counts on the map.
aria-labelon a barediv, which names nothing.role="tablist"owning links and buttons rather than only tabs, reportedcritical.
nested inside a widget role. The close affordance is now pointer only and out
of the accessibility tree, with
DeleteorBackspaceclosing the focusedtab; both paths verified.
GooeyTextRevealalso stopped asking GSAP foraria: "auto", which was puttinga prohibited
aria-labelon a<p>while hiding every line beneath it, so aparagraph could read as empty to assistive technology.
axe returns
color-contrastas incomplete over the canvas, where backgroundsare
color-mix()values it will not resolve. Those were measured directlyinstead, compositing each background through a canvas so
color(srgb ...)andcolor-mix()resolve to real pixels: the lowest ratio on graph and inspectortext is 5.03:1 dark and 5.35:1 light, against a 4.5:1 requirement.
End to end, re-run after the UX change
F; canvas 820px to 1440px andback; probe on the DOM node survives both toggles; one
.react-flowinstancethroughout; URL gains and drops
focus=canvas.?skill=; overlaydrawer is
role="dialog"+aria-modal, labelled, focus moves in, Tab istrapped,
Esccloses it and focus returns to the node.adrfromtranslate(0px, 392px)to
translate(108px, 312px);sessionStoragerecorded{"version":2,"positions":{"adr":{"x":108,"y":312}}}; reload restored itexactly;
Rreturned it to canonical and cleared storage.Fit allreached zoom 0.458 with all eight lanes visible andmetadata correctly dropped; zoom in/out; pan.
never loaded and no horizontal overflow; 768 canvas with focus available and
no minimap; 1280 canvas 976px empty, 660px with the inspector docked; 1440
focus mode at the full 1440.
preview; workspace draft written, survived navigation away and back, and the
close confirmation (
role="alertdialog") still guards it.Technical gate
rm -rf .nextthentype-check,lint,build: clean, 85 static pages.git diff --checkclean. Nothing outside the skills UI, the shared componentsthe audit implicated, docs, roadmap and changelog.
.claude/stays untracked.No commit references PR #21 or #22.
React Flow is in neither
/skills, the workspace, nor even the initial bundlefor
/skills/visualize-interactions; it is a lazy chunk, confirmed againstapp-build-manifest.json.Files added to the diff by the audit
components/skills/file-content-pane.tsxandcomponents/motion/gooey-text-reveal.tsxwere not part of this PR's originalscope. They are here because the accessibility run this PR was asked to perform
found a critical and a serious violation in them, and
app/skills/layout.tsxgains three class hooks so focus mode has something to collapse.
Clarity pass (
6c25bda)Review feedback on the previous head: the technical gates proved the canvas was
not blank, that drag and zoom worked, and that the page was accessible and
responsive — none of which proves a reader understands what the map is saying.
The page spoke graph theory (references, referenced by, entry points, out, in)
and a node card answered "how connected is this?" before it answered "what is
this?".
One vocabulary, defined once. references → uses, referenced by →
used by,
out · in→ Uses n · Used by n, graph → map, entry points→ starting points, Fit all → Show whole map, Reset layout → Reset
map, List → Browse list, and the page is How skills connect. The map,
the browse list, the skill detail page and the catalog now all use it, so the
direction is learned once.
Two things had to exist before the words could work.
SKILL_PURPOSEinlib/skill-types.tsis a curated one-line purpose perskill, living in the site layer beside
CATEGORY_MAPfor the same reason thatmap does: frontmatter has a name, a version and a long trigger-shaped
description, and none of them answer "what is this?" in a card-width line.
Extracting a clause reads unevenly across 34 skills; this way changing a line
is a copy edit in one file, not a change to the skills themselves, and
skillPurpose()falls back to the first trigger phrase for anything new.oftenUsedWithinlib/skill-graph.tsis not an edge — two skills keepcompany when some third skill pulls in both, and ranking by shared parents
answers what else a reader will want open. For
cmk:delivery-reviewit yieldscicd,delivery-pipeline,delivery-workflow.A way in before anything is selected. The rail used to appear only on
selection, so the one moment a reader most needs help was the moment the page
showed them dots. It now holds a start panel — group chips that frame a lane,
and the skills nothing else pulls in with what each leads to. Below 1280 and in
focus mode the same groups are a strip over the map; the browse list carries
them as its first two sections. A group jump requested from the list is handed
to the canvas as its opening frame, because at that moment the canvas does not
exist to be moved.
Starting points are honest. In-degree zero gives five skills, one of which
(
cmk:codebase-docs) has no connections in either direction. Offering it as aplace to begin is worse than saying nothing, so a starting point is a skill
nothing pulls in that pulls in something itself — four of them.
The node card is a handle, a title and what the skill is for. The counts
moved to the panel, where each sits beside the list it describes.
Framing is clamped to the map and centred against the visible width, so a
deep link to a first-row skill no longer opens with a third of the canvas
empty, and the focus drawer no longer covers the node the map just centred.
Verification, re-run end to end
37 automated checks against a production build, in a real browser (the
in-app preview stops compositing when its pane is hidden, which stalls every
animated viewport move and produced false failures — every result below comes
from headless Chrome over CDP):
Fenters, full 1440 viewport,Escpeels hint → selection → focusUses n · Used by nversion: 3, restores on reload,Rresets and clearsAccessibility: axe-core 4.13 at WCAG 2.0/2.1 A and AA on all seven
surfaces — catalog 27 passes, map + inspector 24, focus mode 24, browse list
24, mobile list + drawer 25, detail 25, workspace 29. Zero violations. The
run found one more real defect, now fixed: the rendered-document and eval
scrollers have no focusable child when a skill's body carries no links, which
left a keyboard user unable to scroll them.
Build:
rm -rf .nextthen type-check, lint and build clean, 85 staticpages. React Flow remains absent from every bundle including the map route's
initial load.
Gaps that remain open
desktop and mobile, light and dark, rendered from the production build to
~/Desktop/pr24-skills-canvas/and ready to drop into this description.Nothing is committed as test media.
PR and grounded in each skill's own description and heading. They deserve a
read as copy, not as code.
cold. That is the remaining merge gate, and the one the automated suite
cannot stand in for.