Read from the shipped bundle of the reference hub on 2026-09-10 — the compiled
stylesheet and the route chunk, not a screenshot — so every number here is the
value the browser actually applies. The live page could not be read from the
DOM: it was still on its loading state after ninety seconds (see *The one thing
not to copy*), which is itself the most useful finding.
Their labels for the registries are ClawHub, skills.sh, LobeHub,
browse.sh, NVIDIA, built-in, optional. No vendor prefix appears
anywhere in their UI, because the prefix is *ours* — our federation stamps
axehub:<registry> into metadata.source on ingest to record which hop a row
arrived through. That is provenance worth keeping in the database and worth
dropping on screen, which is what source_label() now does.
A card is not a link. It is a click-to-expand panel: cursor:pointer on
the card, an expanded class that changes only border and shadow, and a detail
block that animates in underneath. Every control inside the card
(stopPropagation() on each) does something narrower:
| Control | Effect |
|---|---|
| The card itself | expands in place; no navigation, nothing to come back from |
| Category button | sets the category filter to that category |
| Tag button | sets the search box to that tag |
| Copy button | copies the install command, shows a tick for 1.5 s |
| "View full documentation →" / "View source ↗" | the only outbound links |
Ours navigates to a listing page per card. Theirs never loses your place in a
list of 90,000 — which is the entire reason a catalogue this size is usable at
all. That is the thing to mirror first.
Collapsed:
opacity:.5 rising to 1 on hover,coloured per registry — built-in #4ade80, optional #fbbf24,
ClawHub #f472b6, skills.sh #34d399, LobeHub #60a5fa,
browse.sh #22d3ee, NVIDIA #76b900, OpenAI #10b981,
Anthropic #d4845a, gstack #fb923c, GitHub #94a3b8.
display:flex, align-items:baseline, gap:.75rem: an icon,the title (h3, .92rem, weight 600, word-break:break-word), and a
source badge carrying that registry's colour as text, background and border.
.82rem, line-height:1.55, clamped to exactly2 lines with -webkit-line-clamp — so a long description costs no
vertical space, and the grid stays even.
macOS,🐧 Linux).
Expanded adds, in order: Overview · Prerequisites (env vars and commands as
<code> in JetBrains Mono) · tags as buttons · Author · Version · License ·
the install command with its copy button · an optional "+ Add to this Agent"
· the documentation or source link. The detail block is separated by
border-top:1px solid #ffffff0a and padding-top:.7rem, not by a heading.
card background #0c0c16 · border 1px #ffffff0d · radius 10px
transition border-color .2s, box-shadow .2s, transform .2s
card:hover border #ffd70026 · shadow 0 4px 24px #0000004d, 0 0 0 1px #ffd7000d
transform translateY(-1px)
expanded border #ffd70033 · shadow 0 8px 32px #0006, 0 0 0 1px #ffd70014
inner pad 1rem 1rem .85rem 1.15rem (the extra left pad clears the accent bar)
grid repeat(auto-fill, minmax(320px, 1fr)) · gap 1rem · margin 1.5rem 0
tag pill background #ffffff05 · border 1px #ffffff0f · radius 3px · .68rem
install background #00000040 · border 1px #ffd7000f · radius 5px
code in mono at .72rem, nowrap, scrollbar hidden
The hover state moves the card 1px and tints the border toward the accent
colour. Nothing scales, nothing changes size: the grid never reflows on hover.
overview, category label, author and tags joined into _search. Filtering is
a plain includes against that — no fuzzy matching, no per-keystroke work
over 90k objects.
<mark> in both title and description./ focuses the search box; a clear button sits in the field; "Clear all"resets source and category together.
hardcoded list, so a new registry appears without an edit. Category counts
are recomputed under the active source filter and sorted by count.
built-in oroptional — so it cannot drift out of step with the registry list.
filters."
subtitle instead of replacing the page, so the shell and the filters survive
the failure.
twice daily".
?embed=picker turns the hub into a picker that postMessages the chosenskill to the parent frame. That is how the catalogue gets embedded in an
agent UI rather than reimplemented inside it.
Their page fetches the whole catalogue into the browser: skills.json is
50,075,808 bytes. The page showed "Loading the catalog… Fetching 88k+
skills across every registry" for the entire time it was observed, and the
request had not completed, so none of the design above was reachable from the
DOM at all. Everything good about their interaction model — instant filtering,
no navigation, live counts — is paid for by a first paint that depends on a
50 MB download.
Ours already queries SQLite server-side and pages at 60 rows, so we can have
the interaction model without the download: expand-in-place needs the detail
fields in the card payload, not the whole catalogue in memory. The rule to keep
is *derive the filters from the data and never navigate away from a result
list*; the rule to drop is *ship the data to the client*.
The page finished loading later the same evening, so everything above was
checked against the rendered result. Three things the stylesheet could not
show:
An expanded card stretches its whole grid row. Expanding one card leaves
its two row-mates as tall, mostly-empty panels beside it — the grid's default
align-items:stretch applied to a card that just grew. Ours should set
align-items:start so only the expanded card grows. This is the one part of
their layout to fix rather than copy.
Every registry gets a chip, not the first six. Twelve wrap onto a second
row: All 90700 active in yellow, then Built-in 58, Optional 141,
Anthropic 17, OpenAI 44, HuggingFace 25, NVIDIA 299, skills.sh 19967,
ClawHub 69150, browse.sh 440, LobeHub 505, gstack 53 — the count in a
dimmer weight after the name. Ours truncates at six, which hides gstack and
every small registry, the ones someone is most likely to be hunting for.
The sidebar carries an emoji per category with a right-aligned count, "All
Skills" pinned first and the rest by count descending. The tier badge
(✓ Built-in) sits at the top right of the card head, and platform pills carry
their own glyphs ( macOS, 🐧 Linux, windows).
"View full documentation →" leads to a real page per skill, not a raw body:
breadcrumbs (Features / Skills / Bundled / apple / Apple Notes), the title, the
one-line description, then a Skill metadata table — Source, Path, Version,
Author, License, Platforms, Tags, Related skills — and then the SKILL.md itself
under a right-hand table of contents: *Reference: full SKILL.md ·
Prerequisites · When to Use · When NOT to Use · Quick Reference (one entry per
command) · Limitations · Rules*.
That structure is the argument for the taxonomy we already store: Source,
Version, Author, License, Platforms and Tags are on every row today, and
/v1/skills/<name> already returns the body. Only "Related skills" and the
sectioned render are missing, so this page is a renderer over data we hold
rather than data we have to go and collect.
Shipped on feat/skills-card-mirror, verified against the real 111k-row
catalogue rather than fixtures:
.sk.ex is a role="button" panel witharia-expanded, Enter and Space bound alongside the click, and the detail
block already in the markup — expanding costs no second request, because the
listing query already returned every field it shows. A search for "tail"
renders 100 expandable cards, each with its own detail block.
align-items:start on the grid — the one thing not copied. Their gridstretches, so an expanded card drags its whole row taller and leaves the
row-mates as tall empty panels.
olive as the fallback so a registry nobody has seen yet gets a card rather
than a KeyError. Five distinct accents appear on a single page of results.
description costs no extra height and the rows stay even.
<mark> highlighting done server-side, because the rows were alreadyfiltered in SQL and there is no client-side copy of the catalogue to search
again. Both haystack and needle are escaped before matching, so a search for
<script> highlights text rather than injecting markup.
/docs/skills/search, and every control insidea card calls stopPropagation() so it does its own narrower job instead of
also toggling the card.
carries a path only; the browser prefixes location.origin, so the same page
is correct behind the tunnel, on localhost, and on whatever hostname it is
served from next.
One thing worth knowing before reading a card's command: it addresses
/v1/skills/<stored name>, and the stored name is the full federated slug. The
leaf alone cannot be used because 7,291 upstream names collide, so the command
shows the slug. Labels, titles and badges are clean; the URL is an address, not
a label, and a copy button that pastes a 404 would be the worse trade.
Still not built: the per-skill documentation page (the metadata table plus the
sectioned SKILL.md render). Every column it needs is already stored and
/v1/skills/<name> already serves the body, so it is a renderer over data we
hold.