WHAT
Purpose and context
- Purpose: build a standalone HTML file so people (designers, devs, agents) can look up and reuse the project's REAL design system like a Figma UI kit: view every token and component, switch themes in place, copy tokens or markup directly to take away.
- Trigger: "extract it into a UI kit", "figma ui kit in html", "design system reference page", "component gallery for reuse". Usually encountered after the project already has stable tokens and components but no page gathers them together.
- Non-goals: inventing a new system or adjusting aesthetics (→
hallmark); cloning the whole page to run it again (→extract-site/web-clone); exporting a real Figma file, Storybook or npm package; modifying the project's component code.
Mental model
nguồn token (TS/CSS/Tailwind/Compose…) ──chạy thật──▶ JSON token ──▶ CSS vars --<ns>-<role> + --k
component trong code (kích thước, variant, tên kind) ──đọc──▶ class .<ns>-* chỉ dựa vào vars
URL gốc ──discover.mjs (link BFS · mở phần ẩn · chữ ký · bão hoà)──▶ inventory.json + crops ──▶ danh mục phải dựng
kit.html + inventory.json ──--coverage──▶ phủ 100% (data-sig / ui-kit-skip)
└──▶ ui-kit.html (7 phần bắt buộc) ──Playwright──▶ bằng chứng
The kit is a verified copy of the system in code: every color and measurement can be traced back to a constant or function in the repo. Where something must be drawn to spec because the code does not state it clearly, it must be declared (RULE-04).
Input and output contract
| Field | Required? | Meaning | |
|---|---|---|---|
| In | token source | yes | file/function generating colors, type, shape, sizes (e.g. lib/tokens.ts → paletteOf) |
| In | component inventory | yes | type name (kind/component name) + groups as the app currently divides them (palette, sidebar, docs) |
| In | target platform | yes | mobile / desktop / both, with real frame sizes |
| Out | docs/ui-kit.html (or the path the user chooses) |
yes | 1 file, openable via file://, containing all 7 sections below |
| Out | provenance report | yes | which parts come from code, which parts are drawn to spec |
| Out | Playwright evidence | yes | 0 JS errors, scrollWidth - innerWidth = 0 at 390px, light + dark screenshots + sample screen |
| Out | version on the kit repo | yes (except UI_KIT_SYNC=0) |
<id>/vN/index.html + manifest.json + line registry.json on the PRIVATE repo Rheinmir/ui-kits (W09) |
Rules and capabilities
- RULE-01 (MUST): colors and measurements are obtained by running the project's token code (test runner/temporary script, deleted after dumping), not by typing hex by hand. If the code can only be read but not run (CSS vars, JSON), parse the file.
- RULE-02 (MUST): every component class references only CSS var (
--<ns>-<role>,--k); no loose hex/rgb in component CSS. Thanks to that, the block:root+ CSS.<ns>-*can be moved to another project and just works. - RULE-03 (MUST): each component tag records exactly the name in the code (kind/component name + main fields) and measurements (dp/px) as used in the code.
- RULE-04 (MUST): declare in the report (and note on the tag if needed) every part drawn per spec or simplified (e.g. morph animation is rotation only); do not present it as a 1:1 copy.
- RULE-05 (MUST): all 7 structural sections below are required; for any missing section, state the reason in the report (e.g. the app has no desktop).
- RULE-06 (MUST): self-contained file: fonts only from Google Fonts, JS/CSS inline, no build, no fetching external data.
- RULE-08 (MUST): Do not stop at the skeleton. The component inventory must come from the W01b discovery loop (follow links, click to open hidden parts, stop when saturated), not just from a few known pages/files. Every pattern in
inventory.jsonmust have a tag in the kit carryingdata-sig="<sig>", or be omitted with a reason via<meta name="ui-kit-skip" content="<sig>:<lý do>;…">. Gate:node references/discover.mjs --coverage inventory.json kit.htmlrc 0. - RULE-10 (MUST): Components must not have only an idle state. Every state that W01b measured (hover, focus-visible, active, selected) must appear in the kit: the tag shows a variant in that state (class
is-hover/is-focus/is-active/is-oncopies the exact rule:hover… of the site) and carriesdata-sig-state="<sig>:<state>". Or it is omitted with a reason viaui-kit-skip"<sig>:<state>:lý do". If focus is only the browseroutline:auto(ua: true), the kit states clearly "focus is the browser default, the site does not style it separately". Gate--coveragealso counts states. - RULE-11 (MUST): Respect robots.txt and the site's terms.
discover.mjsread/robots.txtbefore scanning.User-agent: *blocks the starting path, or there is a note prohibiting automated collection (e.g. facebook.com) → stop rc 3, do NOT add--i-have-permissionyourself. Only use that flag when the user confirms they have a license. Instead, propose B05: the user saves the page themselves. - RULE-09 (MUST): if the source is someone else's site, do not embed their images, logos, SVG wordmarks or copyrighted content in the kit; replace with a placeholder block stating clearly "image …". The kit syncing itself to a PRIVATE repo (W09) is fine; to share it OUTSIDE that repo (public link, hosting), ask first (B03).
- RULE-12 (MUST): Only sync a kit that has PASSED (W05b + W06 green), and only to a PRIVATE —
sync-kit.pyself-checkvisibilityand refuse a PUBLIC repo (GH#187). Versions only increase: no overwriting, no deleting oldvN. - RULE-07 (SHOULD): place the sample file
references/ui-kit-reference.htmlalongside while working; reuse its frame, page shell CSS and control script, only swapping tokens, components, sample screens. - Capabilities: read the codebase; run the project's test runner/script; write 1 HTML file; run Playwright (see
/playwright-verify).
Required structure of each section
In the exact order in references/ui-kit-reference.html:
| # | Section | Required to have | In the sample file |
|---|---|---|---|
| 1 | Control bar (sticky) | select palette (if the app has multiple) · light/dark · shape scale (if the app has one) · "Copy CSS token" button for the block :root of the current selection; remember the selection with localStorage (wrap in try/catch); on first load follow prefers-color-scheme |
.bar · #swatches #mode #shape #copy-tokens |
| 2 | Table of contents (sidebar, hidden < 900px) | group Foundations / Components / Patterns; auto-highlight the section being viewed (IntersectionObserver) | .side |
| 3 | Intro | eyebrow pointing to the source (<file token> → <file kit>) · h1 · 1 paragraph saying where the kit comes from and how to reuse it · a "facts" strip of the important layout constants |
.intro .facts |
| 4 | Foundations | (a) Color: every role, cells with the text color on* in the correct pair, click to copy hex · (b) Type: type scale with name + size/line-height/weight · (c) Shape: corner radius scale multiplied by --k · (d) Layout & sizing: size scale table of the main component + table of frame/margin/spacing constants |
#color #type #shape #layout |
| 5 | Components | group them the way the app groups them (e.g. Actions · Navigation · Content containers · Input · Feedback); each card .spec = header (human-readable name + chip name in code + measurements) → .stage show every important variant/size/state → footer (notes + Copy HTML button auto-attached); the card carries data-sig of the patterns it covers (RULE-08). If the inventory has many patterns for a group, split the cards by pattern, do not merge them into one generic card |
#actions #navigation #containment #inputs #feedback |
| 6 | Sample screens | ≥ 1 mobile screen and ≥ 1 desktop screen (when the app supports it) at the app's exact frame size, scaled down with zoom; assembled from the actual component classes in section 5 following the app's layout rules (e.g. bottom nav on mobile → navigation rail + list-detail on desktop); a caption below the frame lists the components used |
#screens .phone .desk |
| 7 | Script | token table embedded as ROLES + PALETTES[key].light/dark generated from the dump · apply() write CSS vars to :root · render color cells / type scale / shape scale from the data · attach copy buttons · toast to report copy (if clipboard is blocked, report manual copy) |
<script> at the end of the file |
Common to the whole page: the page shell wears the kit's own tokens (the page changes theme together with the components); prefers-reduced-motion turn off animation; :focus-visible clear; icon buttons have aria-label; mobile 390px does not overflow horizontally (tables/sample screens are inside the container overflow-x:auto).
Failure boundaries
- Could not find a token source (colors hardcoded scattered in components) → partial: collect the colors actually used with grep, state clearly "no token source, colors inferred from components", propose extracting tokens (separate task).
- Site scrolls inside its own container (body is exactly 1 screen tall) →
fullPageonly captures 1 screen;discover.mjsopen the container yourself before capturing, a self-written script must do the same. - States exist only when JS runs (menu opened by click, delayed tooltip, entrance animation) →
:hoverpure CSS is measurable, while JS-based states may slip through; W01c looks at the crop*-hover.pngthen adds them manually. Press-and-hold (active) is measured withmouse.down→ drag the mouse outside →mouse.up, so it does not activate the link. If it cannot be measured, state it clearly in the report. - Page with JS disabled (B05): states attached by JS (e.g. FDS Facebook's hover/pressed overlay class) cannot be measured. Reconstruct from the corresponding token (
--hover-overlay,--press-overlay), state clearly "reconstructed from token, not measured". Very heavy pages (feed with 3000+ elements) need--page-timeouthigh. - Exploration hits the page ceiling (
stoppedBy: page-budget) while the last page still yields new patterns → tell the user, propose increasing--pagesor narrowing--include, do not consider the kit sufficient. - Tokens only run in a heavy runtime (needs DB/app server) → dump with Playwright from the running app (
getComputedStyleon:root), state clearly how to obtain them. - Project has only one platform → section 6 has only that screen, state the reason.
- Playwright reports a JS error or horizontal overflow after 3 rounds of fixes → stop, report the remaining error.
- W09 sync fails due to network / no push permission / repo not PRIVATE → script prints "not synced, reason …" rc 0; the local kit is still a valid result, report the reason in W08.
HOW
Main workflow
| Step | Type | Inputs | Action | Outputs/exit | Failure/next |
|---|---|---|---|---|---|
| W01 | judgment | codebase | find token source, component catalog + groups, mobile/desktop frame sizes, schema documentation (if any) | anchor list file:line |
no tokens → partial (see Failure) |
| W01b | deterministic | root URL (dev app or site) | exploration loop: NODE_PATH=$(npm root -g) node references/discover.mjs <url> --out <scratch>/disc --pages 32 --saturate 4 (needs global Playwright; ~40 s/page, runs in background) (BFS same-origin links, scroll all inner containers, click to open accordions/tabs/details, signature = type + style + child shape, dedupe; for clickable patterns also force HOVER / ACTIVE / FOCUS-VISIBLE via CDP CSS.forcePseudoState (like DevTools' "Force state": deterministic, no drag-and-drop, no click, works with JS disabled), and match the SELECTED variant via aria-pressed/selected/current, data-state, class is-active) |
inventory.json + crops/*.png + image of each page |
stoppedBy: page-budget still produces new ones → increase budget (Failure) |
| W01c | judgment | crops + page images | VIEW every new crop and full-page image; group patterns into kit groups, record patterns the classifier missed (e.g. trapezoid tabs, flow diagrams) in the list to be built | pattern table → group → kit card | — |
| W02 | deterministic | token anchor | write a temporary test/script that calls the real token function, dump JSON (every palette × light/dark, size scale) → delete the temporary file | tokens.json in scratchpad |
run fails → static parse |
| W03 | effect | JSON | reduce to ROLES + PALETTES and embed into a copy of the sample file |
data block in <script> |
— |
| W04 | judgment | component code | rewrite sections 4–5: one class per component .<ns>-* use measurements from code; card .spec with full header/stage/footer |
sections 4–5 | missing measurements in code → draw per spec + declare (RULE-04) |
| W05 | judgment | app layout rules | assemble section 6: mobile + desktop screens from the classes in W04 | section 6 | single-platform app → record the reason |
| W05b | deterministic | kit + inventory | node references/discover.mjs --coverage <scratch>/disc/inventory.json <kit.html> |
phủ N/N (100%) + trạng thái M/M rc 0 |
MISSING pattern → build card; MISSING sig:state → add state variants to the card (RULE-10) or ui-kit-skip with reason; rerun |
| W06 | deterministic | file | Playwright: load the page, count pageerror, capture top + middle + sample screen, enable dark + another palette and capture again, viewport 390 measure horizontal overflow |
images + metrics | errors → fix, up to 3 rounds |
| W07 | judgment | images | look at the images in one pass, fix visible layout errors (text wrapping, misaligned items), capture once more | kit done | — |
| W08 | judgment | results | report to user: file path, contents of the 7 sections, which sections come from code / per spec, W06 evidence, W09 sync results | report | — |
| W09 | effect | kit that PASSED W05b + W06 | python3 references/sync-kit.py <kit.html> --source <thư mục repo | URL gốc> (add --id <id> when the source cannot infer the id, e.g. the app after login). Id: repo → <owner>-<repo> from origin (no remote → <thư mục>-local); URL → host without www., dots → dashes. Hash sha256 excluding meta ui-kit-id; same as latest version → "unchanged from vN", no commit. Different → vN+1 + latest/, push, if rejected then pull and recompute up to 3 times. UI_KIT_SYNC=0 skip; UI_KIT_SYNC_REPO change repo |
✓ <id>: vN → <link> or = không đổi |
error → "not synced, reason" rc 0 (Failure) |
Sample token dump (vitest, TS): create lib/zz-kit-dump.test.ts call the token function then writeFileSync(<scratchpad>/tokens.json, …), run npx vitest run lib/zz-kit-dump.test.ts, then rm file. Do not let temp files leak into the project diff.
Sample Playwright check: like /playwright-verify: chromium.launch() → goto(file://…/ui-kit.html) → page.on("pageerror") → screenshot → click('#mode button[data-v="dark"]') → page 390×844 measure document.documentElement.scrollWidth - innerWidth.
Branches
| ID | Kind | Guard | Behavior | Skip / failure | Rejoin |
|---|---|---|---|---|---|
| B01 | conditional_required | token is CSS vars / Tailwind config / JSON (not a function) | static parse instead of running (W02) | — | W03 |
| B02 | conditional_required | app has schema documentation for the agent (e.g. agent.md) |
chip "name in code" records the exact field name of the schema, sample screen follows the rules in that documentation | none → use component name | W04 |
| B04 | conditional_required | source is the URL of a running site (no code) | W02 replaced by Playwright: read CSS vars at :root/.dark + getComputedStyle of the real element; color lab()/oklch() convert to hex; apply RULE-09 |
has code → skip | W03 |
| B05 | conditional_required | site forbids crawlers (RULE-11) or the interface is behind login | user saves the page themselves ("Webpage, Complete") into a folder → python3 -m http.server 8765 --bind 127.0.0.1 → discover.mjs http://127.0.0.1:8765/<file>.html --offline --no-js --page-timeout 1500 (block all outbound requests, disable JS so no script calls back to the site). The saved page contains personal data: the kit only takes style/structure, all names/images/content replaced with samples, check by grepping real names before delivery |
site allows crawling → W01b as usual | W01c |
| B03 | user_optional | user wants to share the link outside the private kit repo | upload the file to the Artifact/hosting the user chooses; the file stays in the repo | default: local file + private kit repo (W09) | W08 |
Validation and stopping
Machine check: deterministic test of W09 python3 -m pytest harness/tests/test_ui_kit_sync.py (id, hash, version count, v1 immutable); W05b coverage gate rc 0 (every pattern has data-sig or ui-kit-skip); W06 must give pageerror = 0 and horizontal overflow = 0 at 390px. Visual check: all 7 sections present per the structure table; compare light/dark images; try the Copy CSS token button and 1 Copy HTML button. Maximum 3 revision rounds at W06, W07 just re-screenshot once.
Examples
- Positive: m3e-canvas (Next.js, Material 3 Expressive editor). Dump
paletteOf()via vitest into 7 palettes × light/dark; the kit has 25 color roles, 15 type styles, 3 shape levels, about 30 components split into 5 groups like the app's Parts table, mobile screens 412×892 (list + detail) and desktop 1280×800 (rail + list-detail); Playwright 0 errors, 390px no overflow. The main result isreferences/ui-kit-reference.html. - Positive (URL): drafted.ai. The first time only 3 known pages were scanned so the kit was just a skeleton. Ran W01b from
/learn/ai-for-homebuyerswith--pages 32 --saturate 4:discover.mjswent through 32 pages (learn, use-cases, house plan listing pages, FAQ, news, careers, onboarding), collected 171 patterns, stopped because 4 consecutive pages had nothing new (~40 s/page). W05b first reportedphủ 62/171 (36%). After building additional groups House plan listings, Articles & sections, a real type table generated from the inventory, and recording 22ui-kit-skip(images, third-party embeds, page frames), the gate reportedphủ 171/171 (100%). W01c also caught one more pattern the classifier missed: trapezoid tabs drawn with SVG. Round 3 added state measurement: 175 patterns, 104 states (44 hover, 56 focus, 1 active, 3 selected pairs). First timetrạng thái 0/104. After adding 23 state strips and a "Focus · 2 styles" card, the gate reported175/175+104/104. The measurements also fixed 6 previously mis-guessed hover rules (e.g. the selection card hovers to stone-500, not stone-900), and showed that 44/56 focuses are just the browser default outline. - Positive (B05, site bans crawlers): facebook.com. robots.txt bans automated collection so
discover.mjsstopped with rc 3, the agent asked the user. The user saved the News Feed (6.4 MB + 252 resources), ran offline + with JS off (blocked 49 outbound requests). Got 579 FDS variables for both.__fb-light-modeand.__fb-dark-modefrom the saved CSS, 78 patterns, 5 measurable states (FAB hover changes shadow, story activescale(.98), link hover underline). Button hover/pressed built from--hover-overlay/--press-overlay. Kit 27 cards, 66 color roles; gate 78/78 + 5/5; grep 0 real names; 0 outbound requests. - Boundary/failure: React repo has only hex colors scattered across 40 files
.module.css, no tokens → partial: grep found 18 colors actually used, the kit notes "colors inferred from components", recommends extracting tokens before full kit-ification.