feat(website): serve the UI gallery at webjs.dev/ui#1126
Conversation
The sidebar, mobile drawer, and .prose-docs typography move out of the docs sub-layout into a shared shell so the component library at /ui (#1099) can render the exact same chrome instead of a drifting copy. No DOM change for /docs; the source-reading chrome guards now point at the shell file.
scripts/copy-registry.mjs copies packages/ui/packages/registry components into the gitignored components/ui/ and lib/ui/, rewriting the kit-local lib imports, so the upcoming /ui gallery pages (#1099) can import the components for SSR + live previews. Wired into webjs.dev.before / webjs.start.before, with the registry sources added to the Tailwind regenerate inputs.
The gallery moves off ui.webjs.dev into the marketing app, rendering in the same shell as /docs: shared header, footer, theme toggle, fonts, and tokens, with the component list in the sidebar under plain Tier 1 and Tier 2 headers. There is no landing page; /ui opens on the introduction the way /docs opens on Getting Started. Preview panes carry the kit's neutral shadcn palette, scoped to .ui-preview so a demo looks like the reader's own app while the page around it stays webjs.dev. Those tokens sit in a @theme inline block because a plain @theme resolves its var() at :root, where the kit palette does not exist, which renders every filled component transparent.
The page claimed the CLI detects Next, Astro, Vite, Lit, or plain projects and adapts. Detection covers four types and neither Lit nor SvelteKit is among them, and all it picks is the stylesheet path and the alias set written into components.json. Dropping the cross-framework pitch rather than describing it more precisely: the kit is for WebJs apps.
Component pages titled '<Name> | WebJs UI' rather than '<name>: WebJs UI', so a tab or a search result reads the same coming from /docs or /ui. The registry identifier stays lowercase where it is the thing you type.
The sidebar lists every component on every page, so the 'All components' control above each title was a second way to do what the nav already does. The heading now reads as a name (Alert Dialog) while the registry identifier stays lowercase wherever it is a value you type.
The three endpoints shipped @webjsdev/ui versions fetch from (/registry, /registry/index.json, /registry/<name>.json) now answer under /ui as well, ahead of turning ui.webjs.dev into a redirect host. Shared headers live in one module so the three cannot drift on caching or CORS. Verified by running the real published webjsui 0.3.8 binary against http://localhost:5001/ui/registry: init wrote components.json and the theme, add fetched and wrote a genuine components/ui/button.ts.
The gallery and the registry API now live at webjs.dev/ui, so this app drops its pages, layout, chrome, assets, and registry composer and keeps only a middleware that permanently redirects. The mapping is path-aware rather than a blind prefix: /registry/** keeps its shape under /ui, /docs/components/<name> flattens to /ui/<name>, and / and /docs both land on /ui. A prefix redirect would have sent every existing component link to /ui/docs/components/<name>, which 404s. /registry is a live API for already-published packages, so the redirect also carries CORS, and test/ui/ui-host-redirect.test.mjs asserts destinations rather than the presence of a redirect.
|
Registry compatibility: how I settled the gating question, and what it rules out The issue called the registry coupling the whole risk and said to settle it before anything else, so this is what I did before writing a line of the migration. The question is whether a 301 from Then I ran the real thing end to end rather than trusting the unit-level result. With the gallery serving on one port and the redirect host on another, I pointed the published 0.3.8 binary at the OLD URL shape: The file it wrote is the genuine card source, not an empty shell. Same for Worth noting how small the blast radius actually is: since 0.3.9 the kit resolves local-first (#983), so What this rules out. The issue's fallback plan was that if redirects were not followed, One premise in the issue turned out to be wrong. It lists The mapping is the part worth reviewing. A blind path-preserving redirect (what the docs host does) would have been wrong here, because the old URL shapes are not the new ones: |
…page The sitemap enumerates one URL per component from the live registry index, so a component added to the registry is crawlable the moment it exists. Themes and lib items are excluded: they have no page. /docs/ui was a second hand-written description of the same kit and had drifted badly, claiming roughly 55 components against an actual 32 and showing a <ui-button> API the kit does not have. It is deleted and its URL permanently redirects to /ui, so one page owns the keyword.
The composer, the Dockerfile bake guard, the preload probe, and the SEO tag checks all pointed at files the migration moved or deleted. The guards themselves still matter, so they follow the code rather than being dropped: the preload probe now covers /ui and /ui/button, and the SEO checks cover only apps that still serve HTML. Adds a bake-ordering assertion (copy-registry before Tailwind) that nothing covered: the mirrored sources are a scanned @source, so a stylesheet built first is missing every kit utility class and the previews render unstyled in production only. Also pins HOSTED_REGISTRY_URL, which selects local-first resolution.
The touch e2e booted the ui-website and read /docs/components/<name>, which the migration emptied, so it would have failed against a host that now only redirects. It boots the marketing app and reads /ui/<name> instead; all 5 checks pass, including the soft-nav round trip that exercises the client router across gallery links. The Bun and Node boot checks drop the two redirect-only hosts, whose routes are empty 301s that pass a status check vacuously, and cover /ui and /ui/button on the app that actually serves them.
Root README, framework-dev, AGENTS.md, packages/ui/AGENTS.md, the blog example's docs, and the start-work skill all described a ui-website that serves pages. It serves redirects now, so they point at the marketing site instead, and the mirror footgun section is rewritten: the scope narrowed to the ui/ subdirectories, so a hand-written site component no longer needs the app/_components workaround. Leaves packages/ui/README.md's project-detection claim alone on purpose. It is wrong (it advertises Lit detection that does not exist), but the fix belongs with removing the behaviour, tracked as #1129.
The JSDoc described a Tier-2 component with a literal class-declaration snippet, and the dev server's orphan-component scan reads source text without skipping comments, so every boot warned that a file declaring no components had an unregistered one. Rewritten as prose.
Review found seven real problems. A `git add -A` swept in a self-referential node_modules symlink and three screenshots, all pre-existing working-tree junk that would materialize as an ELOOP path in any fresh clone. The lockfile still listed the two dependencies the redirect host no longer needs, so `npm ci` would have failed in CI. The redirect sent asset URLs under /ui, where they 404. The old root layout published /public/og.png as its og:image and the favicons beside it, so every social card already scraped from that host pointed there; assets now keep their path, which resolves to the equivalent file on the new host, with a test per URL. dev-all never passed SITE_URL to the ui host, so `npm run dev:all` made localhost:5003 redirect to production instead of the local site. The Tailwind regenerate step scanned a stale mirror: the generated sources are a scanned @source, so it now refreshes the mirror first or the rebuild silently omits every kit utility class. website/AGENTS.md documented none of app/ui, lib/docs-shell, modules/ui, or the generated mirror, and still told deployers to set UI_URL, which no longer exists.
The node_modules rule had a trailing slash, so it matched directories only and a symlink of that name sailed past it into a commit. A self-referential one resolves to an ELOOP path that npm and Node's resolver both stat on every fresh clone. Root-level images are ignored the same way, since screenshots taken while working are exactly what a broad `git add -A` picks up. Scoped to the root so the real assets under */public/ stay tracked.
vivek7405
left a comment
There was a problem hiding this comment.
Went through this fresh. The migration itself holds up: the registry URL shapes survive, the palette scoping is genuinely leak-free, and dropping /docs/ui does not break the doc-page enumeration. What it did not hold up on is hygiene and the edges of the redirect.
Two things would have broken CI or a clone outright. A git add -A swept in a self-referential node_modules symlink plus three loose screenshots, and the lockfile was never resynced after the redirect host shed two dependencies, so npm ci would have failed on the desync.
The one I care most about is the asset redirect. The old root layout published /public/og.png as its og:image, so every social card already scraped from that host points there, and the fall-through rule was sending it to /ui/public/og.png, which does not exist. Same for the favicons. Assets have to keep their path, not gain the /ui prefix the pages do.
Then two that only bite locally but bite quietly: dev:all never handed SITE_URL to the redirect host, so hitting localhost:5003 sent you to production, and the Tailwind regenerate step listed the registry sources as an input while its command could not refresh the mirror those sources feed.
Docs: website/AGENTS.md is the file the rewritten packages/ui/AGENTS.md now points a gallery editor at, and it documented none of the new layout while still telling deployers to set a UI_URL that no longer exists.
|
Also fixed from that round, without an inline anchor Two findings sat on lines the diff does not carry, so they get recorded here. The lockfile was resynced with
|
CI was red and my earlier local runs were not evidence: the mirror in website/components/ui is gitignored and only webjs.dev.before, webjs.start.before, and the Dockerfile bake create it, so /ui/button 500'd in every job that boots the app. It passed locally only because a dev server had already produced it. A pretest hook in both the root and the website package now generates it, and both suites pass from a tree with the mirror deleted. The registry endpoints had no test at their new location: the redirect test asserted Location strings, so a correct redirect into a broken endpoint would have shipped green. Now the manifest, the flat index, an item's inlined source, the reserved index/registry slugs, CORS, and the static-over-dynamic precedence are all asserted. Also: the mirror writes only changed files, so a stylesheet regeneration no longer restarts the dev server through its own watch path; the registry lib dir joins the regenerate inputs (it owns cn and the layout helpers every example imports); and the dark variant is pinned, since it is inert outside previews and nothing else would catch its removal.
vivek7405
left a comment
There was a problem hiding this comment.
Zoomed in on runtime correctness this round rather than hygiene, and the important finding is that the previous round's green test numbers were not real.
CI is red on the pushed head, and the reason is structural: website/components/ui/ is gitignored and only webjs.dev.before, webjs.start.before, and the Dockerfile bake ever create it. webjs test runs none of those, so /ui/button 500s in every job that boots the app. It passed locally only because a dev server had already produced the mirror, which means the test plan was measured against a warm tree and reported as if it were a clean one. That is the failure mode worth naming: the numbers were not wrong so much as not evidence.
Second, the registry API has no test where it now lives. The redirect test asserts Location strings, and the composer test covers the function, so a correct redirect into a broken route would have shipped green on the one surface this PR calls a released contract.
Beyond those, two dev-loop papercuts and a doc line, all small. The mirror rewrote all 32 files unconditionally while the dev supervisor watches that directory, so every stylesheet regeneration also restarted the server. The regenerate inputs list the registry's components but not its lib, which owns cn and the layout helpers every example imports. And README.md still documented UI_URL as an overridable cross-link.
I checked the things most likely to hide a real bug and they are clean: startCase in generateMetadata is a hoisted function declaration, not a TDZ trap; unsafeHTML only ever receives module-authored strings, with every registry-derived value going through an escaped hole; the .json strip is safe against a doubled suffix and against traversal, because lookup only matches manifest names; and the extracted shell is byte-equivalent for /docs.
|
Also from that round: two smaller fixes and one finding I am not taking The regenerate inputs now include the registry's
The dark variant got a test. The review confirmed it compiles correctly and resolves right in all four OS-preference and Not taking: the one-function-per-file convention for the registry composer. It exports three reads (item, index, manifest) from They are three views of ONE parse and they share the module-level caches. Splitting them would either duplicate the cache per view, which loses the same-reference guarantee |
Two findings from review, both measured rather than reasoned about. The shared shell's inline onclick attributes made lib/docs-shell.ts client-effecting, which shipped BOTH layout modules to the browser against the execution model's own self-check. The handlers move into the root layout's existing delegated listener, which already survives navigation, so both layouts now elide. That also fixes a stale aria-expanded: dismissing the drawer via the backdrop needs no navigation and used to leave the toggle advertising an expanded drawer, and Escape now closes it and restores focus. SSR action seeding was serializing every registry read into the page for a consumer that cannot exist: no component here does an async render or calls an action, and a page function never re-runs in the browser. It was a quarter of some responses. /ui/dropdown-menu drops 141KB to 107KB and the pre-existing /changelog 1.10MB to 800KB. Also unpinned the snippet map and API metadata from the browser bundle (a module-scope IIFE and one comment mentioning a DOM global were enough to mark both client-effecting) and trimmed the page to only the Tier-2 side-effect imports, since a Tier-1 helper registers no element.
The kit's own docs list scope on header cells and a caption as the reader's Tier-1 obligation, and the gallery shipped neither: 2 bare th on /ui and 6 on every component page. Worse, the paste-ready table snippet in the Code tab taught the omission, contradicting the component's JSDoc that spells out scope=col. preview-tabs declared role=tablist with two role=tab buttons and no aria-controls, no tabpanel, and no keyboard handling, so it announced as tabs with no associated panel on 33 indexed pages. It now implements what the roles promise: cross-linked panels, a roving tabindex so the group is one tab stop, and Arrow / Home / End. Covered by browser tests, since all of it is focus behaviour that only exists after upgrade; neutering aria-controls and the roving tabindex reds two of them.
… in llms.txt All 33 gallery URLs shared one meta description inherited from the layout, which is the duplicate-content shape this migration exists to remove; introducing 33 of them in a search-consolidation change would have been self-defeating. Each page now derives its own from the tier and the naming convention, so it says something true and distinct and stays correct if a component changes tier. The registry carries no per-item description, so a hand-written map would just be 33 more strings to drift. llms.txt enumerated the 45 doc pages but not a single component, while the sitemap advertised all 33. An agent entering through llms.txt could not discover the kit it is meant to use. Also repaired three stale pointers the move left behind: the favicon generator wrote into a deleted directory and would have thrown, and the registry README plus two source comments still credited the emptied app as the composer.
vivek7405
left a comment
There was a problem hiding this comment.
Zoomed out to the surfaces nobody had looked at, and this round was the most productive of the three.
The one I would not have found by reading the diff: both layout modules and the component page were shipping WHOLE to the browser, against the execution model's own self-check. webjs doctor says so plainly and nobody had run it. The causes were almost comically small, which is what makes them worth writing down: a module-scope IIFE building the sonner demos, and one comment containing the word document, were each enough to mark a pure data module client-effecting and drag its importer along. The shell's inline onclick attributes did the same to both layouts.
That led to the second finding, measured rather than reasoned about: every page was carrying an SSR action-seed payload no component on this site can consume. It was a quarter of some responses. This is mostly PRE-EXISTING and much bigger than the gallery: /changelog was shipping 304KB of dead seed in a 1.1MB page, and /blog 19KB. Turning seeding off for the app fixes all of it.
On accessibility, the gallery was violating the rules it documents. preview-tabs declared role="tablist" with two role="tab" buttons and no aria-controls, no tabpanel, and no keyboard handling, so it announced as tabs with no associated panel on 33 indexed pages. And every table shipped bare <th> while the page beside it told the reader that scope is their responsibility, with the paste-ready snippet in the Code tab teaching the omission.
On search, the change was quietly working against its own purpose: 33 new URLs all sharing one meta description, and an llms.txt that enumerated 45 doc pages but not one component, while the sitemap advertised all 33.
Also: the favicon generator still wrote into a directory this PR deletes, so it would have thrown after writing two of its three outputs.
|
The seed payload: a pre-existing problem much bigger than this PR This came out of the round-3 review and is worth its own note, because most of it predates the gallery and the numbers are large. Every Measured before and after turning it off:
On the elision findings, one thing worth internalising The causes were tiny and invisible: a module-scope IIFE, and a single comment containing the word for a DOM global. Each was enough to mark a pure data module client-effecting and drag its importing page into the browser bundle. That is the second time in this PR a comment has tripped a source-text scan (the first was the component scanner reporting an unregistered component in a file that declares none). The analyser weakness is pre-existing and still fires for the docs pages that legitimately contain code samples. I have not filed it: the false positives are warnings rather than errors, and a scanner that parsed instead of grepped is a real piece of work in Browser CI The Browser job failed on this branch twice, on a Firefox navigation interrupt in |
…derline The derived helper name in each Tier-1 description was guessed from the component name, and the convention does not hold for all of them: breadcrumb exports breadcrumbListClass, popover exports popoverContentClass, and switch exports switchInputClass, none of which have a bare xClass. Three search snippets named a function that does not exist. It now reads the exported name out of the component's own source, so all 32 are true and stay true when the kit renames one. The accordion trigger no longer underlines on hover. An underline reads as a link affordance and this is a disclosure control on a summary element; the pointer cursor and the rotating chevron already carry it. Presentation only, so the shadcn API parity that matters is untouched.
Clicking the drawer toggle returned early before the header mobile menu's outside-click close, so the header dropdown stayed open. The header is a stacking context, so its menu was then trapped beneath the drawer's backdrop: visible, dimmed, and unclickable. The menu housekeeping now runs first, unconditionally. Escape with both open closed both and left focus on the header summary rather than the control that opened the drawer, contradicting the comment right above it. The drawer branch returns now. Verified in a real browser across all ten states, then pinned: six browser tests for the interactions, plus a source assertion that the transcribed listener still matches the real inline script, including the ordering that actually broke.
… menu The comment claimed the header menu was trapped under the drawer's backdrop because the header is a stacking context. Measured on the live page, that is backwards: the backdrop sits inside the root layout's z-1 context, so it composites below the z-20 header, and a hit test at a dropdown link with the drawer open returns the link. The menu was never unclickable, and docs-shell.ts already documents the real z-order, so the repo carried two comments asserting opposite stacking. The behaviour is right and unchanged; only the justification was wrong, which is worse than no comment because the next reader reasons from it.
|
Review loop: seven rounds, last one clean Recording where this landed, since the loop ran long and found more than I expected. Rounds 1 and 2 were hygiene and process: a self-referential Round 3 was the substantive one and found four things I would not have caught by reading the diff. Both layouts and the component page were shipping whole to the browser against the execution model's own self-check, and Rounds 4 through 6 checked the fixes rather than the original diff, which is where the last real bugs were. Two were in the delegated listener I had just written: clicking the drawer toggle returned early before the header menu's outside-click close, and Escape with both open closed both and left focus on the wrong control. Round 6 then caught that my comment explaining the first fix asserted a false z-index mechanism, contradicting the correct explanation already sitting in Round 7 came back clean. Every finding is accounted for. All fixed on the branch except one, which is filed: scoping Two things I deliberately did not do, both argued rather than skipped. The composer keeps its three exports instead of splitting one-per-file, because they are three views of one parse sharing a cache and splitting them would lose the same-reference guarantee its tests pin; the reasoning is in the file header. And the |
Replaces the affordance removed with the underline. A slight drop in text weight reads as interactive without borrowing a link's vocabulary, and it pairs with the chevron that rotates on open. The label now carries text-foreground explicitly rather than inheriting it, because the hover target needs a known starting colour: dimming an author's inherited colour to 80% of the THEME foreground would shift the hue rather than just the weight. Slash opacity is already idiomatic in the kit (text-foreground/60, ring-ring/50), and Tailwind emits a solid fallback ahead of the color-mix form. Verified hovered in a real browser in both themes.
#1101 widened the documentation from the 1200 container and 800 content column it used on its own domain to 1240/820, and this branch inherited that and then applied it to the gallery as well. Back to 1200/800 for both, from the one shared shell. A reference page is read line by line and 800px is the comfortable ceiling for that measure; the extra 20px bought nothing. Note the shell is now 40px narrower than the header's own container, so the sidebar sits 20px inside the wordmark rather than flush with it. That is the deliberate trade, recorded in the shell, and the header can follow if the offset ever reads as an accident.
… 800 Splits the two decisions #1101 had moved together. The outer container goes back to the header's 1240 so the sidebar sits flush under the wordmark, while the content column stays capped at 800, the comfortable ceiling for a reference page read line by line. The slack lands as space between the sidebar and the text rather than as a longer line. Verified at 1600px: sidebar and wordmark both start at 204, content column 800, no horizontal scroll.
Reverts the two width commits from this session, putting the container back to 1240 and the content column to 820, byte-identical to what main serves today. Those changes were chasing a widening that happened in #1101, not here, and the second of them (820 to 800) was a 20px difference nobody could see. The shell is back to inheriting whatever the documentation uses, so /docs is untouched by this PR and /ui matches it, which was the original request.
The height cap and overflow sat on a wrapper div while the border, background, and rounded corners come from the pre inside it, so the scrollbar rendered outside the visible rectangle, detached from the card it was scrolling. A scrollbar renders inside its own element's border box, so both now live on the pre.
Closes #1099
The component gallery moves from
ui.webjs.devonto the marketing app atwebjs.dev/ui, rendering in the same shell as/docs: the header, footer, theme toggle, fonts, and design tokens are literally the same code every other page uses, and the sidebar comes from a shell module the two sections now share. There is no landing page./uiopens straight onto the gallery with the introduction in the content column, the way/docsopens on Getting Started, and the sidebar groups components under plainTier 1/Tier 2headers.Two reasons, the same as the docs move. In search, a subdomain accrues authority to itself rather than to
webjs.dev. In design, the gallery carried a parallel layout, nav, footer, announce strip, and a duplicated@themeblock, so a reader crossing over left one design system and entered another.ui.webjs.devbecomes a redirect-only host, and it has to keep resolving forever for a harder reason than the docs host did:/registry/*is a live API that already-published packages fetch from.The registry gate, settled before anything moved
The issue called this the whole risk, so it was verified against real published tarballs rather than the source in this repo.
@webjsdev/ui@0.3.1and0.3.8were installed from npm and pointed at a host that 301s cross-origin; both followed the redirect and parsed the result. Then the full chain was run end to end: the published 0.3.8 binary, pointed at the OLD URL shape, followed the 301 into the new endpoint and wrote a genuinecomponents/ui/card.ts.Since 0.3.9 the kit resolves local-first (#983), so
add/list/viewnever reach the network on the default registry at all; the exposed population is 0.3.8 and below, pluswebjsui diffat any version. Full write-up in a comment below, including what it rules out (the fallback plan of maintaining a second composer on the old host indefinitely).What changed
website/app/ui/**: the introduction at/ui, one page per component at/ui/<name>, and the registry API at/ui/registry/**. The composer moved towebsite/modules/ui/queries/registry.server.tsand still reads its sources frompackages/ui/packages/registry/, which is unchanged.website/lib/docs-shell.tscarries the sidebar, the mobile drawer, and the.prose-docstypography;/docsand/uiboth render through it, so they cannot drift. No DOM change for/docs..ui-preview, so a demo looks like the reader's own app while the page around it stays webjs.dev. The old site could declare those values on:rootbecause nothing else lived on that domain; here they must not leak.ui.webjs.devredirects, path-aware rather than path-preserving:/registry/**keeps its shape under/ui,/docs/components/<name>flattens to/ui/<name>, and/and/docsboth land on/ui. A blind prefix redirect would have sent every existing component link to/ui/docs/components/<name>, which 404s.ui.webjs.devserved no sitemap at all, which is most of why this migration was cheap.HOSTED_REGISTRY_URL, the package homepages, and the README catalogue links.Bugs and drift found on the way
@themevs@theme inlinesilently broke every preview. A plain@themeemits--color-primary: var(--primary)on:rootand compiles the utility tovar(--color-primary). Custom properties substitute at the element where they are DECLARED, so that innervar(--primary)resolves against:root, where the kit palette does not exist. Every filled component rendered transparent while outlined ones looked fine, because only the latter got their color from a border utility. Caught by looking at the rendered page, not by any test that existed. The scoping is now asserted./docs/uiwas a second, badly drifted description of the same kit. It advertised roughly 55 components against an actual 32, listed two dozen that had been removed in the v1 scope cut, and showed a<ui-button>/<ui-card>API the kit does not have (those are Tier-1 class helpers, not custom elements). It is deleted and permanently redirects to/ui, so one page owns the keyword rather than two cannibalising it.The cross-framework claim was overstated. The page said the CLI detects Next, Astro, Vite, Lit, or plain projects and adapts. Detection covers four types, neither Lit nor SvelteKit is among them (both silently fall through to
plain), and all it decides is the stylesheet path and the alias set written intocomponents.json. The copy is now scoped to WebJs apps; removing the behaviour from the package is filed as #1129.The dev server's orphan-component scan reads comments. A JSDoc line describing a Tier-2 component as
class X extends WebComponentwas reported on every boot as an unregistered component in a file that declares none. Reworded as prose here; the analyser weakness is pre-existing and still fires for the docs pages that legitimately contain code samples.Deliberately not done
The
$schemaURLs are left byte-identical. The issue listsui.webjs.dev/schema.jsonas something that must keep resolving, but it does not resolve today and never has: both it and/schema/registry-item.jsonreturn 404 on the live host. There is no contract to preserve, and repointing a dead link at awebjs.dev/ui/...path that would also 404 would rewrite the$schemaline in every user'scomponents.jsonwithout making anything resolve. Making them actually serve a schema is real work (zod@3.25here has notoJSONSchema) and belongs in its own issue.Test plan
Every number below was measured from a COLD tree (
rm -rf website/components/ui website/lib/uifirst). An earlier version of this plan was measured on a working tree where a dev server had already generated that gitignored mirror, which made CI red while local runs looked green. Thepretesthooks exist so that gap cannot reopen.npm test3338 pass, 0 fail.ui-gallery: chrome compared byte for byte against a marketing page through the real request pipeline, one-header/footer/toggle counts, shared-shell markers, bare tier headers, every registry component reachable, non-components 404, the palette scoped and mapped through@theme inline, the dark variant trackingdata-theme, the registry endpoints (manifest, flat index, inlined item source, reserved slugs, CORS, static-over-dynamic precedence), sitemap coverage, and the/docs/uiredirect.test/ui/ui-host-redirect.test.mjs11 pass, asserting destinations rather than the presence of a redirect, including the asset passthrough and CORS on the registry 301.WEBJS_E2E=178 pass, 0 fail./ui, including the soft-nav round trip that exercises the client router across gallery links.node scripts/run-bun-tests.js253 pass, 0 genuine failures.bun test test/ui/ui-host-redirect.test.mjsgreen.bun test/bun/app-boot.mjsgreen on/,/docs/*,/ui,/ui/button, 42 preloads, none broken.@theme inlineto a plain@themereds the scoping test; dropping the component type guard reds the non-component 404 test; neutering the redirect's component rule reds 3 of the mapping tests.@webjsdev/ui@0.3.8init+addagainst both the new endpoint and the old redirecting one, writing genuine component source both times./,/docs/*,/ui,/ui/buttonin prod mode with no broken modulepreload, on Node and Bun; blog e2e green; both redirect hosts covered by their mapping tests rather than a vacuous 301 status check.webjs checkclean onwebsite/and on the redirect host;tsc --noEmitclean on both.data-themerather than only the OS preference, and that the gallery reads as the same site as/docs.Doc surfaces
Updated:
AGENTS.md,README.md,framework-dev.md,packages/ui/AGENTS.md(module map plus the mirror footgun, whose scope narrowed),packages/ui/README.mdcatalogue links,packages/cli/README.md,packages/ui/packages/registry/README.md, the redirect host's ownAGENTS.mdandREADME.md(rewritten),examples/blog/AGENTS.mdandCONVENTIONS.md,Dockerfile,compose.yaml,.github/workflows/ci.yml, and the start-work skill's dogfood gate.N/A: the MCP server (no introspection projection changed; its
uitool reads the kit inventory from the package, not the website). The editor plugins (no intellisense, grammar, snippet, or command surface changed). The scaffold generators (webjs createwrites acomponents.jsonwhose$schemais deliberately unchanged, and nothing it emits references the gallery).Follow-up, not in this PR
ui.webjs.devneeds its Railway service redeployed from this branch after merge. It defaults to redirecting athttps://webjs.devwith no env var set, so no Railway variable change is required; theUI_URLvariable on the website service becomes dead and can be removed at leisure. Scoping the ui package to WebJs projects is #1129.