feat: make the UI kit and AI design guidance first class - #1414
Conversation
The design gate needs an instrument before it can measure anything, and the instrument has to be on main so both sides of the comparison are scored by byte-identical code. This adds it alone, ahead of the guidance it exists to evaluate. scripts/eval-design.mjs walks a generated app and emits nine numbers, one per rubric line, with no model in the loop, so two runs over the same input agree. It reports numbers and never a judgment, which is what lets a reviewer re-run it against either side and compare. The three prompts are committed verbatim and carry no design vocabulary. A brief that asks for a polished screen measures the brief rather than the guidance, so the ban is asserted by a test rather than left as a convention. The dirty fixture trips all nine lines exactly once so each can be asserted individually. Without that, a silently dead line would hide behind a failing sibling and the harness would report a clean instrument that measures nothing.
The kit had one semantic colour, --destructive, and no elevation scale. A component that needed to say success reached past the token layer for text-emerald-500, which is exactly what sonner.ts did, so the missing roles were the root cause of the raw-palette drift rather than a separate problem. Four tokens per role, a solid fill plus the text on it and a tinted surface plus the text on it, for success, warning and info, with destructive folded into the same shape. Not an 11-step ramp: no further step has a role a component asks for, and Tailwind's opacity modifier already covers borders. Elevation is Tailwind's own geometry renamed by role, e1 raised through e4 top. The shadow colour has to be a var INSIDE the value, because Tailwind inlines a --shadow-* token into the utility rather than emitting a var reference, so a .dark redeclaration of the shadow itself would do nothing. Fixes a live contrast bug on the way. --destructive-foreground measured 1.65:1 against --destructive in dark mode, so the destructive button shipped with a label very nearly invisible against its own fill. Light was 4.35:1, also under the floor. Both now clear 4.5:1, which also lets button.ts use the token instead of the hardcoded text-white it was working around. The cn() classifier had to learn e1 through e4 first. A bare shadow name the size scale does not list falls to the colour catch-all, so an elevation utility and a real shadow colour would have silently evicted each other.
…n-list, timeline Six primitives the kit was missing, each replacing a shape an app otherwise hand-rolls badly. All Tier-1 class helpers: none holds state, so none registers a custom element. empty-state is the highest-leverage one, because the defect it prevents is invisible in development. A list rendered against seeded data looks finished and renders as blank space for every user who has not created anything yet. description-list is the direct replacement for printing a field name and its value at the same weight on one line, which is the single most common generated-screen defect and is what makes a detail panel unscannable. stat inverts the label and the value deliberately, since a reader scans a dashboard for numbers. Its delta colours go through the semantic roles added in the previous commit rather than a raw green or red. field-group adds the grouping and addon shapes only. lib/utils.ts already owns the single-field helpers and this must not shadow them, which a test asserts, because fieldErrorClass (the reserved space) and errorClass (the text that goes in it) read alike and are different things. The helpers use block bodies rather than concise arrow bodies. The module-scope purity scanner reads a top-level arrow-body cn() call as module-scope work, and the alternative was six more entries on a known-false-positive allowlist that should only ever shrink.
af1d92c to
1f24005
Compare
A reference file only helps an agent that loads it. The component header is read where the decision actually gets made, by whoever is placing the component, and webjsui add copies it into the user's own repo, so the intent travels with the code rather than staying in this repo. Every component gains a Design block beside its existing A11y block, saying what the component is for, what it is not for, and where its variants sit in the hierarchy. The button block states the action pyramid at the one call site where it is most often broken. The alert block says why two alerts on a screen mean neither reads as urgent. The skeleton block says a skeleton whose shape does not match its content is worse than a spinner. The structural test asserts both blocks exist and that the Design one is long enough to be intent rather than a placeholder. It cannot judge the prose, but it can stop a later component shipping with none.
The skill taught agents how to apply styles mechanically and taught them nothing about what a screen should look like before the class list is written. styling.md answers how do I make this work in WebJs, and nothing answered what should this look like. Two files, because one loses either way. design.md is the core, under the same budget as styling.md so it can load alongside it on every UI task: hierarchy, the action pyramid, spacing and grouping, type, colour, elevation, empty states, then the WebJs-shaped part. design-depth.md is on demand: palette construction from an accent, the per-family variant checklist, the finishing pass, and the three archetypes. The WebJs-shaped section is the half that is not in any design book. An empty state must be in the first paint because pages do not hydrate. A suspense fallback is a design surface rather than a spinner dump, since it flushes on the first byte and is often on screen longest. Polish on a display-only component is free because elision means the browser never downloads it. Both routing tables get a row, and they are not the same table: SKILL.md routes topic then reference, root AGENTS.md routes reference then topic. The test asserts the file exists AND is routed from both, because an unrouted reference is a file no agent opens, and the gate would then measure the routing rather than the content.
Code teaches harder than prose. Every demo in the gallery taught one framework feature and none taught what a screen should look like, so an agent reading the gallery learned mechanics and nothing else. These two are the archetypes where flat output is worst, and each is commented with the reason behind the decision rather than the rule. The dashboard carries one primary action, the label-under-value inversion, an empty branch written beside its list, and state through the semantic roles. It draws its own bar chart in seven divs rather than shipping a chart library for something a page can render server-side. The settings form is grouped into fieldsets with legends, uses a tighter gap between a label and its field than between fields, and reserves the error space so a validation message cannot shift the control the reader was about to click. It binds a server action, so it submits with JS off. Both score zero on the design rubric, which is the test that they teach what they claim to. The data has awkward shapes in it on purpose, a depot name long enough to wrap and a zero in the series, because a layout built around tidy data breaks the first time real data arrives. Two fixes fell out of building them. The timeline connector used a magic offset that its own rubric line flags, and now centres on the scale instead. The empty-state rubric line counted a map over a literal array, which cannot be empty at runtime, so it flagged a fixed set of six checkboxes. Its own definition was always a list over a query result; the first implementation could not tell the difference. The settings exemplar also tripped invariant 9 on its first boot, a backtick inside an html template body, in a comment. The comment now says so.
…n reference The docs site's styling page covered the mechanics and stopped there, which is the same gap the skill had. It now documents the semantic roles, why writing a palette family instead makes an app un-rethemable, the role-named elevation scale, and the reason the shadow colour has to ride a var inside the value rather than being redeclared per theme. packages/ui/AGENTS.md gains the token layer as its own section and the six primitives in the inventory, with the field-group caveat that fieldErrorClass is the reserved space and errorClass is the text, since the two read alike. Root AGENTS.md points at both reference files from the styling section and states the rule that state goes through a role rather than a palette family. gallery:clear learned the two new modules. Without that the command left a scaffolded app with modules it claims to have stripped, which the scaffold test caught. The MCP ui tool and the marketing site's /ui pages needed no change: both derive from the registry, so the six primitives and their headers arrive there on their own. Verified rather than assumed, by rendering all six pages and reading the tool's inventory back.
|
Gate: the guidance half needs your before-and-after run before this merges The artifacts are done and stand on their own. The guidance is the half nobody has measured, and this is the stop for measuring it. I cannot run it myself: the party checking the result must not be the party that wrote the thing being checked. What to run. Three runs per prompt per side is the floor, and post every run rather than the best one. Both sides carry the harness, the tokens, the primitives and the headers, so the only difference between them is What each outcome means. The after side wins on the rubric lines, regresses none, and the screenshots agree: the guidance ships and this merges whole. The after side does not beat the before side: the guidance does not ship. Revert The after runs provably never loaded the file: that measures the routing, not the content. Fix the routing and rerun the after side once. It is the only rerun the gate allows. One thing the harness cannot see. The nine rubric lines are static scans, so they catch a raw palette colour, an arbitrary spacing value, a missing empty branch and two primary buttons on a page. They cannot tell you whether the screen reads well. That is what the screenshot pairs are for, and your own read of them is the half of the result no number covers. |
|
Where the implementation departs from the plan, and why Four places. Each was a decision the plan settled and the code could not follow as written. Elevation reuses sm / md / lg / xl, not xs / md / lg / xl. The plan mapped The theme file is not on The six helpers use block bodies rather than concise arrow bodies. The module-scope purity scanner reads a top-level Rule 7 of the rubric now excludes a map over a literal array. Its written definition was always a list "over a query result", and the first implementation could not tell a query result from a hardcoded array, so it flagged the settings exemplar's fixed set of six notification checkboxes. A list that cannot be empty at runtime needs no empty branch, and counting it would train an author to add an empty state that can never render. One thing I did not do. Scoring the whole gallery turns up 72 hits in pre-existing feature-demo payload. The plan asked for an audit of the home page, the root layout and the todo example, and the finding is that those three are clean on hierarchy and the action pyramid, with only off-scale type left. I recorded that rather than rewriting demos whose job is to teach framework features, but it is a real number and someone should decide about it separately. |
…ertion Seven review findings, all real. The two exemplars did not compile. They called buttonClass with an outline variant, which the registry has and the gallery's own customised button does not, and the settings action imported ActionResult from @webjsdev/core, which documents the envelope but exports no such type. The type is declared locally now, since an exemplar that does not compile teaches worse than one that repeats a shape. The settings form rendered four empty fields. It bound .value on a native input, and a .prop hole is dropped at SSR on anything without a hyphen in its tag, so with a page that never hydrates the value was never set on either side. It also meant a 422 lost everything typed, which is precisely what the file claims to demonstrate. It is a plain value= attribute now, with a comment saying why, because this is the trap the exemplar exists to not teach. The website's Tailwind theme carried none of the new tokens, and it @sources the mirrored registry, so every shadow-e1 through shadow-e4, text-success, text-info and text-warning the kit now emits compiled to nothing on the deployed /ui gallery. Verified by building the stylesheet: zero rules before, one each after. The same file also still carried the old --destructive-foreground, so the 1.65:1 dark-mode contrast bug this branch fixes in the registry was live on webjs.dev, and swapping button.ts from text-white to the token would have shipped it there. The header test's block terminator could not match A11y ( , because the character class it used rejects digits. Wherever the Design block sat above the A11y one the terminator found nothing, the block swallowed the rest of the header, and the length assertion passed on A11y prose rather than on any design content. The terminator is a literal set now, with a guard that fails if the extraction ever absorbs the A11y section again. Also corrects a comment in the gallery layout that the line beneath it falsified, and the exemplar headers that still described the old variant.
vivek7405
left a comment
There was a problem hiding this comment.
Went through the whole diff. The design work holds up and the token layer is the right shape, but the two exemplars did not compile and the website half of the change was missing entirely, so I would not have merged this as it stood.
The exemplars are the part that worries me most, because they are the thing an agent copies. One called a button variant the gallery's own button does not have, one imported a type core does not export, and the settings form bound .value on a native input, which SSR drops. That last one meant it rendered four empty fields and lost everything typed on a validation failure, which is the exact behaviour the file's own header claims to demonstrate.
The website findings are worse than they look. website/public/input.css @sources the mirrored registry but never learned any of the new tokens, so every new utility the kit emits compiled to nothing on the deployed gallery. It also still carried the old --destructive-foreground, which means the 1.65:1 contrast bug this branch advertises as fixed was live on webjs.dev, and swapping button.ts to the token would have shipped it there rather than fixing it.
The header test needed a look too: its block terminator could not match A11y (, so for a good chunk of the kit the assertion was passing on A11y prose instead of on design content.
Note the inline anchors point at the fixed lines rather than the broken ones, since the repairs are already on the branch.
…d fill The delta round found that the contrast change regressed the very component it claimed to fix, and that the claim behind it was wrong. --destructive-foreground is built for a SOLID --destructive fill, where the dark value measures 5.58:1. button.ts does not paint a solid fill in dark: dark:bg-destructive/60 composites red-400 at 60% over the page, and against that the token measures 2.49:1 while the text-white it replaced measures 6.48:1. So the token was exactly wrong on the one component it looked most correct on, and badge.ts kept text-white on the identical shape, so the two disagreed. button.ts goes back to text-white with the measurement written beside it, and a test pins both components against the token. The claim was wrong too. No registry component read --destructive-foreground before this branch, so the 1.65:1 pair was latent rather than shipped. It was this branch that would have put it on screen. The token correction is still right, since it makes the pair safe for app code writing bg-destructive with text-destructive-foreground, but it fixes a latent bug rather than a live one, and both theme files now say so. ActionResult is exported, from @webjsdev/server rather than core, and eight sibling actions in the same app already import it that way. The local re-declaration is gone. It was checked against core alone and the comment asserting the type did not exist was simply false. Also: the new page-header component's example still showed the outline variant the gallery's button does not have, so a paste of it would not compile. aria-describedby was emitted empty on every field with no hint and no error, which is an IDREF list resolving to nothing rather than an absent attribute, so the template branches now. The gallery layout comment I rewrote replaced an accurate note with an inaccurate one, since nothing in that app renders text on a destructive fill. And --color-success was declared twice in one @theme block.
vivek7405
left a comment
There was a problem hiding this comment.
Second round, scoped to the fix commit and its blast radius. It caught something I got badly wrong the first time, so this one is worth reading even though the diff is small.
The contrast change regressed the exact component it advertised as fixed. --destructive-foreground is built for a solid --destructive fill, but button.ts paints dark:bg-destructive/60 in dark, a composite rather than a solid. Against what that button actually renders, the token measures 2.49:1 and the text-white I removed measures 6.48:1. So pointing the button at the token pushed it from comfortably passing to well under the floor, on the one preview where anyone would look. badge.ts kept text-white on the identical shape, so the two disagreed as well.
The claim behind it was wrong too, and this is the part I want on the record: no registry component read --destructive-foreground before this branch, so the 1.65:1 pair was latent, never rendered. The PR presented it as a live shipped bug. Correcting the token is still right, because it makes the pair safe for app code that writes bg-destructive text-destructive-foreground, but it fixes a latent defect rather than a live one and the PR body said otherwise.
ActionResult is exported after all, from @webjsdev/server rather than core, and eight sibling actions in the same app already import it that way. I checked core alone and wrote a comment asserting the type did not exist.
The rest are smaller: the new page-header's example still showed a variant the gallery's button does not have, aria-describedby was emitted empty on fields with no hint and no error, the layout comment I rewrote traded an accurate note for an inaccurate one, and --color-success ended up declared twice in one theme block.
…e near it The previous commit retracted a false claim about half a token pair and left the other half asserting the opposite. The dark comment said the bad value was latent and never rendered; the light comment, describing the same never-read token seventy lines above, still said the kit shipped it. Neither value was ever rendered, since button.ts and badge.ts carry text-white in both modes, and both comments now say so. The website theme had the same split and stated it explicitly, scoping the latency to the dark pair, which asserted by contrast that the light one was live. The test added with that commit was worse than the bug it guarded. Its primary finder matched no line in either file, so every assertion ran through a looser fallback that scanned raw source including comments, and it passed only because no single comment line happened to carry both a bg-destructive and a text- utility. A comment mentioning the two together, which the theme file three directories away already contains, would have failed a correct variant; one worded the other way would have passed a real regression. It now reads the comment-blanked source like every other test in the file and anchors on the VARIANTS key, so it can only ever match the real variant. Both directions are counterfactualled: the regression fails it, and adding the trap comment does not. The text-white exception was recorded only in a source comment while three doc surfaces this branch adds teach the pairing rule it breaks. All three now carry it, phrased as the general rule rather than the special case: a -foreground token is measured against its own solid fill, so diluting that fill with an opacity modifier means re-measuring against the composite. The raw-palette scan cannot see text-white, since it matches only palette families with a numeric step, which is why the pin is a test rather than a convention. design.md also taught the registry's variant set as if it were universal. The kit is copied into a user's repo and edited there, so it now says to check the local button rather than assume the names.
vivek7405
left a comment
There was a problem hiding this comment.
Third round, scoped to the previous fix commit. It found that my correction was half-applied and that the test I added to lock it in was unsound.
The retraction only landed on the dark half. The dark comment said the bad value was latent and never rendered; the light comment, describing the same never-read token seventy lines above, still said the kit shipped it. The website theme had the same split and made it explicit by scoping the latency to the dark pair, which asserts by contrast that the light one was live. Neither was: button and badge carry text-white in both modes, so nothing ever rendered either value.
The test was the worse half. Its primary finder matched no line in either file, so every assertion fell through to a looser one that scanned raw source including comments. It passed only because no single comment line happened to carry both a bg-destructive and a text- utility, and the theme file three directories away already contains that exact sentence. A comment worded one way would have failed a correct variant, and worded the other way would have passed a real regression. So the guard against the regression was itself decided by prose near the code rather than by the code.
Last one is a doc gap rather than a defect: the text-white exception lived only in a source comment, while three surfaces this branch adds teach the pairing rule it breaks.
The light-mode comment on --destructive-foreground was self-contradictory on its own terms. It called button.ts and badge.ts "the only two consumers" of a token whose whole point in this branch is that neither reads it. Read as consumers of --destructive instead it was still wrong, since fourteen components read that one. It now states the fact the way the dark comment and the website theme already do: no registry component read the token, so neither value was ever rendered. The claim that text-white is "the one place in the kit a non-token colour is correct" was also wrong. dialog.ts and alert-dialog.ts both carry backdrop:bg-black/50, because a scrim is black in both themes and has no token to be, and it is invisible to the raw-palette scan for exactly the reason the same paragraph cites two sentences later. The skill reference framed it the same way, directly under a rule that says never a hex, which is where an app author meets the scrim question first. Both surfaces now say the actual rule: a colour is a token whenever the theme should be able to move it, which is almost always, rather than a literal never being right.
vivek7405
left a comment
There was a problem hiding this comment.
Fourth round. Two factual claims I added in the previous commit were wrong, both in prose rather than code.
The light-mode comment was self-contradictory on its own terms: it called button and badge "the only two consumers" of a token whose whole point in this branch is that neither reads it. Read as consumers of --destructive instead it is still wrong, since fourteen components read that. The dark comment and the website theme both already stated the fact correctly, so the one I "fixed" was the only one of the three still saying something untrue.
And text-white is not the one place a literal colour is correct here. dialog.ts and alert-dialog.ts carry backdrop:bg-black/50, because a scrim is black in both themes and has no token to be. That sentence also undercut its own justification, since the scrim is invisible to the raw-palette scan for exactly the reason the same paragraph gives two sentences later for why the pin has to be a test. The skill reference framed it the same way, sitting directly under a rule that says never a hex, which is where an app author meets the scrim question first.
Both now state the actual rule: a colour is a token whenever the theme should be able to move it, which is almost always, rather than a literal never being right.
vivek7405
left a comment
There was a problem hiding this comment.
Inline anchors for the round above, which posted without them.
vivek7405
left a comment
There was a problem hiding this comment.
Correction to the review above it: that one says it carries inline anchors and it does not. GitHub accepted both attempts and attached no comments, so the three findings are path-anchored here instead.
packages/ui/packages/registry/themes/index.css, the light --destructive-foreground comment. It called button and badge "the only two consumers" of a token neither of them reads, which is the fact the surrounding change exists to establish. Read as a claim about --destructive consumers it is wrong too, since fourteen components read that one.
packages/ui/AGENTS.md, the token-layer section. It called text-white the one place a non-token colour is correct in the kit. dialog.ts and alert-dialog.ts both carry backdrop:bg-black/50, and that literal is invisible to the raw-palette scan for exactly the reason the same paragraph gives two sentences later for why the text-white pin has to be a test.
.agents/skills/webjs/references/design.md, the colour section. Same overstatement, and it sat directly under "Every colour is a token. Never a hex", which is where an app author meets the scrim question first.
All three are fixed in the commit this review points at. Both surfaces now state the rule as: a colour is a token whenever the theme should be able to move it, which is almost always, rather than a literal never being right.
vivek7405
left a comment
There was a problem hiding this comment.
Fifth round, scoped to the previous fix commit and its blast radius. Nothing to report: the claims that commit now makes check out against the code, and no other surface still states the old version of either. First round on this branch that produced no fixes.
That buys the final pass over the whole diff, which is running now.
…itives The final whole-diff review found that the dark-mode elevation story this branch documents in four places was not implemented. --surface-1/2/3 were declared in all four theme files, mapped, and read by nothing at all, while design.md, packages/ui/AGENTS.md, the docs site and the theme comment each asserted that the tokens handle the dark inversion for you. What actually shipped was the opposite: a dark shadow five to six times stronger than the light one, which design.md itself calls reading as a hole. The lift was already there. --card and --popover sit at 0.205 against a 0.145 --background, so a raised surface is lighter than the page through tokens the theme has always had. The surface tokens added nothing and are gone rather than left as dead configuration, the dark shadow is moderated to 0.3 and 0.4, and all four surfaces now describe the mechanism that exists. The six new primitives were missing from V1_COMPONENTS, so none of them was covered by the guards for a complete, parser-safe, extractable example, while uiInventory, webjsui view and the MCP ui tool all served them. One would have failed the moment it was covered: empty-state's example carried an elided comment, which is exactly what the guard bans. They were also absent from the website's example map, so all six rendered the no-live-preview fallback on the public gallery. The earlier claim that all six pages render was true and did not mean what it read as. The settings exemplar lost its six notification toggles on every submit, in a file whose own header says losing typed input is what makes people abandon a form. An unchecked box submits nothing, so the action now reads the present keys back and the page restores them. They ride as one comma-joined field because the envelope types values as a string map, which is what a form actually submits. The runbook could not be run as written. It cut both sides from main, which after this branch moved the gate pre-merge is the ref that carries no guidance at all, and its worktree command would fail against a ref already checked out. It now takes the ref as a variable, and its revert step names all three routing surfaces rather than two, since the cheat-sheet rows are paired against the gallery demos by a repo-health test.
vivek7405
left a comment
There was a problem hiding this comment.
Final pass over the whole diff. Six findings, and the first one is the kind I would want caught before merge rather than after.
The dark-mode elevation story was documented in four places and not implemented. --surface-1/2/3 were declared in all four theme files, mapped into @theme, and read by nothing whatsoever, while design.md, packages/ui/AGENTS.md, the docs styling page and the theme comment each stated that the tokens handle the dark inversion for you. What shipped was the opposite of the claim: a dark shadow five to six times stronger than the light one, which design.md itself describes as reading like a hole. The lift was already there through --card and --popover sitting above --background, so the tokens added nothing. They are gone, the dark shadow is moderated, and the four surfaces describe what exists.
Seven form controls moved up an elevation level unverified. shadow-xs to shadow-e1 roughly doubles the light shadow and adds a second layer, and the role scale has no level below raised, so the rename could not preserve the input-versus-card distinction. Moderating the dark values takes the worst of it out; the consolidation itself is deliberate and is now stated rather than implied.
The six new primitives were outside every manifest guard. Absent from V1_COMPONENTS, so nothing checked their examples were complete and extractable, while uiInventory, webjsui view and the MCP ui tool all served them. empty-state would have failed the moment it was covered, since its example carried an elided comment, which is the specific thing that guard bans.
And all six rendered the no-live-preview fallback on the public gallery, because none had an entry in the website example map. My test plan said all six /ui/<name> pages render, which was true and did not mean what it read as.
The settings exemplar lost its six toggles on every submit, in a file whose header says losing typed input is what makes people abandon a form. An unchecked box submits nothing, so nothing read them back.
The runbook could not be run. It cut both sides from main, which is the ref carrying no guidance now that the gate is pre-merge, and its worktree command fails against an already-checked-out ref.
All six fixed in da38b248. Verified: 541 tests green, both stylesheets rebuilt with the surface tokens gone and shadow-e1 still emitting, all six /ui pages now serving a real preview, and the settings form rendering its default toggles checked.
The fix-check found that the six previews added last commit rendered with EMPTY class attributes. evalHole resolves each hole against a HELPERS map and fails soft to an empty string, none of the 27 new helpers was in that map, and the only assertion covering them was that the page returns 200. So they shipped as unstyled markup inside a styled shell, which is the defect the commit claimed to fix. The helpers are imported and registered, and a new test asserts a preview pane carries real classes rather than that its route answers. Also from the fix-check: the packages/ui prose lost the sentence naming the cn() classifier, leaving two pronouns with no antecedent; the dark elevation values diverged across the three theme copies while a comment asserted they are identical; the gallery layout comment still explained the surface tokens the previous commit deleted; root AGENTS.md was the last surface still stating the retracted lighter-surface claim; and NOTIFICATION_KEYS was a non-function export from a 'use server' file, which is invariant 2, duplicated by hand in the page it existed to serve. The list moved to a browser-safe types module both sides import, so renaming a field can no longer break the restore silently. The deferred gallery audit is done rather than recorded. It went from 72 hits to zero, and roughly a third of that was the scorer being wrong rather than the payload. Five false-positive classes are fixed: rule 9 scanned whole files rather than html templates, so a robots.txt line and a cache key counted as screen output; its attribute check treated the > in an arrow function as a closing tag; global-error and global-not-found were charged for literal colours when they render their own document with no stylesheet to token against; the heading rule flagged zero h1 on pages whose heading comes from a helper; and an arbitrary text colour counted as an arbitrary font size. The rest was real: seventeen off-scale widths, thirty-six off-scale type sizes, two lists with no empty branch (the frames filter can match nothing, and gallery:clear empties the home), and five label-colon-value captions, which the design reference itself says are usually unnecessary. Invariant 9 caught me a second time on the way through, a backtick inside an html template body in a CSS comment this time.
…n the inventory Four findings from a review pass, all real. design-depth.md told agents to run `webjsui init --accent <name>`. There is no such option: init takes --base-color, which picks the NEUTRAL ramp, and the accent is set by editing the theme block the kit copies into your repo. The recommended workflow failed on the first command. It now describes what exists, and says why a shipped accent default would be wrong rather than treating the absence as a gap. references/ui-kit.md claims to be the inventory and listed none of the six new primitives, so an agent loading that reference would not discover them and would hand-roll an empty state instead. All six are listed, with a line each on the job they do, since the inventory is where the discovery happens. The scorer counted an issue reference as a literal colour. Blanking comments was not enough, because the same reference appears in a metadata description string, so `(#492)` in the blog scored as a hex. A hex now only counts in a colour POSITION, after a colon or a comma, which is where a CSS declaration and a gradient stop put one. The blog goes from thirteen literal-colour hits to two, and both survivors are real. The scaffold gallery test covered only the todo example, so the dashboard and settings exemplars were unverified in both directions: that a generated app carries them, and that gallery:clear strips them. That second half is exactly the regression this PR already shipped once. Removing settings from the clear list now fails the test.
Reverts 09f8134 in full. The tree is byte-identical to the commit before it, produced by git revert rather than by hand so nothing is left behind. The token layer was the part that looked most defensible and it is the part that settles this. @webjsdev/ui exists for shadcn parity, and measured against the shadcn clone every token change in that PR moved away from it. shadcn ships no --success, --warning or --info, so those twelve tokens were an invention. It uses Tailwind's stock shadow scale at the call site, so renaming seventeen usages to a shadow-e1 through shadow-e4 role scale was churn against parity, and it forced a cn() classifier change in three copies to support a name shadcn does not have. The destructive contrast fix is the sharpest case. shadcn's own values are byte-identical to what the kit already had, light oklch(0.97 0.01 17) and dark oklch(0.58 0.22 27), and shadcn's button never reads the token: it carries text-white, exactly as this kit did. So the pair that PR called a live bug is upstream parity, latent by design on both sides, and the change would have put a real regression on the button until review caught it. The rest goes for reasons that were visible earlier and that I did not act on. The six primitives each composed from helpers the kit already had. The two example screens were single static screens listed beside a real composed app. The design reference was explicitly the unproven half and its owner-run gate never ran, so it shipped measuring nothing, and the evaluation harness exists only to run that gate. The header blocks carry the same unproven premise into every user's repo. The gallery payload rescaling edited working demos to satisfy a rubric that PR introduced. One genuine parity gap surfaced on the way and is NOT addressed here, since adding it inside a revert would be the same mistake again: shadcn ships --surface, --surface-foreground, --code, --code-foreground and --selection, and this registry theme has none of them.
Reverts 09f8134 in full. The tree is byte-identical to the commit before it, produced by git revert rather than by hand so nothing is left behind. The token layer was the part that looked most defensible and it is the part that settles this. @webjsdev/ui exists for shadcn parity, and measured against the shadcn clone every token change in that PR moved away from it. shadcn ships no --success, --warning or --info, so those twelve tokens were an invention. It uses Tailwind's stock shadow scale at the call site, so renaming seventeen usages to a shadow-e1 through shadow-e4 role scale was churn against parity, and it forced a cn() classifier change in three copies to support a name shadcn does not have. The destructive contrast fix is the sharpest case. shadcn's own values are byte-identical to what the kit already had, light oklch(0.97 0.01 17) and dark oklch(0.58 0.22 27), and shadcn's button never reads the token: it carries text-white, exactly as this kit did. So the pair that PR called a live bug is upstream parity, latent by design on both sides, and the change would have put a real regression on the button until review caught it. The rest goes for reasons that were visible earlier and that I did not act on. The six primitives each composed from helpers the kit already had. The two example screens were single static screens listed beside a real composed app. The design reference was explicitly the unproven half and its owner-run gate never ran, so it shipped measuring nothing, and the evaluation harness exists only to run that gate. The header blocks carry the same unproven premise into every user's repo. The gallery payload rescaling edited working demos to satisfy a rubric that PR introduced. One genuine parity gap surfaced on the way and is NOT addressed here, since adding it inside a revert would be the same mistake again: shadcn ships --surface, --surface-foreground, --code, --code-foreground and --selection, and this registry theme has none of them.
|
Reverted in full by #1417 This is reverted on The token layer is what settled it. The destructive contrast fix is the sharpest case and the one worth remembering. shadcn's own values are byte-identical to what this kit already had, light The rest went for reasons that were visible while it was being built. The six primitives each composed from helpers already in What is worth keeping from it, for whoever picks this up later. Nothing in the diff, but two findings. The first is a real parity gap in the opposite direction: shadcn ships The second is the process lesson. The plan in #1116 was treated as authority rather than as a proposal, so each phase was built because the plan listed it rather than because it earned its place. Parity should have been the first question asked of the token layer, not the last. |
Clears every package carrying unreleased user-facing work since 0.7.50. core adds refreshPage, which re-renders the current url on the server and applies it in place with no reload, no history entry and no scroll, and prefetches a link that drives a frame in that frame's own dimension so the click costs no round trip. The fix records a same-document history entry before the swap rather than after, which is what made an iOS swipe-back preview the page the user was already on. server stops a dev edit to a page or layout reloading the browser, so hydrated state and scroll survive a save, and marks a sliced frame response so the client can tell a real subtree from a whole document. The fixes render an error boundary inside its own layout chain, which makes navigating into a failing page a soft navigation with the chrome intact rather than a full page load, coalesce a burst of dev reload signals so rapid edits stop leaving the page unstyled, and root the vendor specifier scan in the module graph, where 21 of the 24 specifiers the old whole-directory scan found in website were phantom. ui takes the a11y fix alone, measured against Chrome's computed accessibility tree: two nested dialog nodes collapse to one, and an ordinary toast stops resolving under two live roots. Three packages are deliberately absent. cli, mcp and intellisense carry no user-facing change in the range. cli's only such commit was #1414, which #1417 reverted in full, so what remains is the doctor barrel split, an internal move with an unchanged export surface. mcp picked up three doc-string edits from the same refactor, naming the new sibling-directory paths its `source` tool can read, and the paths it already documents still resolve because they are barrels. intellisense picked up nothing at all. Raises packages/server's declared @webjsdev/core range from ^0.7.50 to ^0.7.51. The dev refresh reads refreshPage through a runtime feature check rather than a static import, so nothing breaks without it, but the release PR is the only place that bump is legal and core carries the earliest date in the batch so it publishes first. The generated notes were curated before committing. The generator reads conventional-commit prefixes and cannot see a revert, so it wrote #1414 into ui's entry for work no longer on main; that entry is removed. Each remaining excerpt was the first lines of a squashed commit message, which for the multi-commit squashes opened on a test or docs sub-commit rather than on the change itself.
Clears every package carrying unreleased user-facing work since 0.7.50. core adds refreshPage, which re-renders the current url on the server and applies it in place with no reload, no history entry and no scroll, and prefetches a link that drives a frame in that frame's own dimension so the click costs no round trip. The fix records a same-document history entry before the swap rather than after, which is what made an iOS swipe-back preview the page the user was already on. server stops a dev edit to a page or layout reloading the browser, so hydrated state and scroll survive a save, and marks a sliced frame response so the client can tell a real subtree from a whole document. The fixes render an error boundary inside its own layout chain, coalesce a burst of dev reload signals so rapid edits stop leaving the page unstyled, and root the vendor specifier scan in the module graph. ui takes the a11y fix alone. Three packages are deliberately absent. cli, mcp and intellisense carry no user-facing change in the range. cli's only such commit was #1414, which #1417 reverted in full, so what remains is the doctor barrel split, an internal move with an unchanged export surface. mcp picked up three doc-string edits from the same refactor, naming the new sibling-directory paths its `source` tool can read, and the paths it already documents still resolve because they are barrels. intellisense picked up nothing at all. Raises packages/server's declared @webjsdev/core range from ^0.7.50 to ^0.7.51. The dev refresh reads refreshPage through a runtime feature check rather than a static import, so nothing breaks without it, but the release PR is the only place that bump is legal and core carries the earliest date in the batch so it publishes first. core and server carry the generated notes verbatim. ui is edited twice, both because the generator reads conventional-commit prefixes and cannot see anything else. It wrote #1414 into the entry for work #1417 has since reverted, which is removed along with the Features section it emptied; and its excerpt is the first lines of a squashed message, which for #1412 opened on the CDP measurement instrument rather than on the fix, so the fix sub-commit takes its place.
Clears every package carrying unreleased user-facing work since 0.7.50. core adds refreshPage, which re-renders the current url on the server and applies it in place with no reload, no history entry and no scroll, and prefetches a link that drives a frame in that frame's own dimension so the click costs no round trip. The fix records a same-document history entry before the swap rather than after, which is what made an iOS swipe-back preview the page the user was already on. server stops a dev edit to a page or layout reloading the browser, so hydrated state and scroll survive a save, and marks a sliced frame response so the client can tell a real subtree from a whole document. The fixes render an error boundary inside its own layout chain, coalesce a burst of dev reload signals so rapid edits stop leaving the page unstyled, and root the vendor specifier scan in the module graph. ui takes the a11y fix alone. Three packages are deliberately absent. cli, mcp and intellisense carry no user-facing change in the range. cli's only such commit was #1414, which #1417 reverted in full, so what remains is the doctor barrel split, an internal move with an unchanged export surface. mcp picked up three doc-string edits from the same refactor, naming the new sibling-directory paths its `source` tool can read, and the paths it already documents still resolve because they are barrels. intellisense picked up nothing at all. Raises packages/server's declared @webjsdev/core range from ^0.7.50 to ^0.7.51. The dev refresh reads refreshPage through a runtime feature check rather than a static import, so nothing breaks without it, but the release PR is the only place that bump is legal and core carries the earliest date in the batch so it publishes first. core and server carry the generated notes verbatim. ui is edited twice, both because the generator reads conventional-commit prefixes and cannot see anything else. It wrote #1414 into the entry for work #1417 has since reverted, which is removed along with the Features section it emptied; and its excerpt is the first lines of a squashed message, which for #1412 opened on the CDP measurement instrument rather than on the fix, so the fix sub-commit takes its place.
Ships the scaffold audit fix from #1419, so that `npm create webjs` stops emitting apps that report 5 high-severity advisories on their first install. The fix only reaches users through a published `@webjsdev/cli`, since `create-webjs` resolves it from the registry. The changelog is hand-written rather than generated. The generator matches conventional subjects in the package tree, and the unreleased log for `@webjsdev/cli` also carries the `feat:` subject from #1414, whose work was fully reverted by #1417. That revert landed with a `revert:` prefix the generator does not match, so an auto-generated entry would have advertised a feature that is not in the code.
Ships the scaffold audit fix from #1419, so that `npm create webjs` stops emitting apps that report 5 high-severity advisories on their first install. The fix only reaches users through a published `@webjsdev/cli`, since `create-webjs` resolves it from the registry. The changelog is hand-written rather than generated. The generator matches conventional subjects in the package tree, and the unreleased log for `@webjsdev/cli` also carries the `feat:` subject from #1414, whose work was fully reverted by #1417. That revert landed with a `revert:` prefix the generator does not match, so an auto-generated entry would have advertised a feature that is not in the code.
Closes #1116
Makes the WebJs UI kit and the AI design guidance first class, in one PR rather than the eight the plan of record splits it into, at the owner's request.
Two halves, and the order matters. The artifacts (a real token layer, six missing primitives, design intent on every helper header, two worked exemplars) are justified on their own and land first. The guidance (a design reference in the skill) is the unproven half: frontier models already carry hierarchy and spacing principles, so whether a loaded reference changes generated output is a question, not an assumption. It lands behind the artifacts, and an owner-run gate measures its marginal effect on top of them.
The gate is a pre-merge stop
Phase 6 of the plan is a hard stop by design. In a single PR it becomes a stop before merge: everything is implemented, and the before-and-after comparison runs against this branch before it merges. The protocol, what each outcome means, and what the harness cannot see are in a comment on this PR.
scripts/eval-design/RUNBOOK.mdis executable from a few commands with nothing reconstructed from the issue.If the after side does not beat the before side, the guidance does not ship: revert the two reference files, their routing rows in
SKILL.mdandAGENTS.md, and the two cheat-sheet rows, then merge the rest. That is a documented outcome, not a failure of the PR.A latent contrast bug, corrected (and an earlier version of this claim was wrong)
--destructive-foregroundmeasured 1.65:1 against--destructivein dark mode, well under the 4.5:1 floor, and 4.35:1 in light. Both now clear it.The first version of this PR called that a live shipped bug and pointed
button.tsat the corrected token. The review round proved both halves wrong. No registry component read--destructive-foregroundat all, so the bad pair was latent, never rendered. And the button paintsdark:bg-destructive/60, a composite rather than a solid fill, against which the corrected token measures 2.49:1 where thetext-whiteit would have replaced measures 6.48:1. So the change would have introduced the regression it advertised as a fix.The token correction stays, because it makes the pair safe for app code that writes
bg-destructive text-destructive-foregroundover a solid fill.button.tskeepstext-white, with the measurement recorded beside it, and a test pins button and badge against the token so the two cannot drift apart.Every colour value in this PR is backed by a measured contrast ratio rather than a guess, computed from Tailwind v4's real oklch ramps.
What shipped, by phase
scripts/eval-design.mjsemits nine numbers per app, no model in the loop, so two runs agree. Three prompts committed verbatim with a test-asserted ban on design vocabulary, because a brief that asks for a polished screen measures the brief.success/warning/info,destructivefolded into the same shape), four tokens each. Role-named elevationshadow-e1throughshadow-e4. In dark the lift comes from the surface being lighter than the page, which--cardand--popoveralready express; the shadow only separates the edge.empty-state,stat,page-header,field-group,description-list,timeline.webjsui addcopies into the user's own repo.design.mdplusdesign-depth.md, routed from both tables.packages/ui/AGENTS.md, rootAGENTS.md,gallery:clear.Two landmines worth knowing about
cn()would have silently broken elevation. A bare shadow name the size alternation does not list falls through to theshadow-colorgroup, soshadow-e1and a real shadow colour would have evicted each other with no error. The alternation learnede1throughe4first, in all three copies of the classifier.Tailwind inlines
--shadow-*values. It substitutes the value into the utility rather than emitting avar()reference, so redeclaring--shadow-e1under.darkdoes nothing at all. The shadow colour rides a var inside the value, which is what makes the dark inversion work.Test plan
test/scripts/eval-design.test.mjs, 32 assertions, with a per-line counterfactual so a silently dead rubric line cannot hide behind a failing sibling. Verified by neutering one line and watching only its own assertion fail.packages/ui/test/*plus repo-health, scripts and scaffolds: 541 passing, including the raw-palette and elevation regression tests, thecn()classifier test (counterfactual verified ate7ac7d62), and the structural header test across all 38 components.test/repo-health/design-reference-routed.test.mjs, asserting the reference exists AND is routed from both tables, since an unrouted reference is a file no agent opens.npm test. Two failures, both proven pre-existing:test/bun/listener.test.mjsfails identically with origin/main's copy of the file (a known linked-worktree failure), and thegallery:clearfailure was mine and is fixed.webjs checkclean ongalleryandwebsite./,/docs/styling,/ui,/ui/buttonwith no broken preloads; gallery boots 200 on/,/examples/dashboard,/examples/settings,/examples/todo; all six new/ui/<name>pages render.uitool reports 38 components and serves all six new headers. Verified by reading the inventory back rather than assuming, since it derives from the registry.node:crypto, or the TS stripper.The audit, fixed rather than reported
The gallery scored 72 rubric hits when this started. It now scores zero on all nine lines.
About a third of those were the scorer being wrong, not the payload, and fixing the instrument mattered more than fixing the code: had I churned the demos to satisfy a broken rule, I would have made them worse. Six false-positive classes, all fixed in
scripts/eval-design.mjs:Sitemap: ${url},`hint:${key}`htmltemplatestitle="Theme: ${x}">in an@click=${() => …}read as a closing tagglobal-error.tshex values<h1>pageHeading(), invisible to a static scantext-[color:var(--c)](#492)in a metadata descriptionThe rest were real and are fixed: 17 off-scale widths, 36 off-scale type sizes, two lists with no empty branch (the frames filter genuinely matches nothing;
gallery:cleargenuinely empties the home), and five label-colon-value captions, whichdesign.mditself says are usually unnecessary.Not in this PR, and not filed
websitescores 179 andexamples/blog90 on the same rubric. Neither was in the audit's scope, and the website's hits are mostly literal colours in a deliberately hand-tuned marketing palette. Applying the kit's rubric there is a redesign, not a cleanup, so it is recorded here for a separate decision rather than folded in or filed as an issue.Review
Six rounds plus a final whole-diff pass and a fix-check. Nineteen defects found and fixed, including two in fixes for earlier defects. The pattern worth knowing: the code was mostly sound and the claims about the code kept being wrong, which is why several fixes are corrections to comments and docs rather than to behaviour. Two findings would have shipped visibly broken output (six unstyled component previews; a destructive button pushed to 2.49:1 in dark by a change advertised as a contrast fix).