AXe Skills HubSearch /

Skill card: how the reference hub does it

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.

The interaction model, which is the real difference

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:

ControlEffect
The card itselfexpands in place; no navigation, nothing to come back from
Category buttonsets the category filter to that category
Tag buttonsets the search box to that tag
Copy buttoncopies 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.

Card anatomy

Collapsed:

coloured per registrybuilt-in #4ade80, optional #fbbf24,

ClawHub #f472b6, skills.sh #34d399, LobeHub #60a5fa,

browse.sh #22d3ee, NVIDIA #76b900, OpenAI #10b981,

Anthropic #d4845a, gstack #fb923c, GitHub #94a3b8.

the title (h3, .92rem, weight 600, word-break:break-word), and a

source badge carrying that registry's colour as text, background and border.

2 lines with -webkit-line-clamp — so a long description costs no

vertical space, and the grid stays even.

🐧 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.

Exact tokens

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.

Search and list behaviour

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.

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.

optional — 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".

skill to the parent frame. That is how the catalogue gets embedded in an

agent UI rather than reimplemented inside it.

The one thing not to copy

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*.

Confirmed against the live page

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).

The per-skill page

"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.

What we built from this

Shipped on feat/skills-card-mirror, verified against the real 111k-row

catalogue rather than fixtures:

aria-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.

stretches, 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.

filtered 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.

a 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.