Skip to content

feat: refactor the scaffold gallery onto a @webjsdev/ui class-helper design system#1060

Draft
vivek7405 wants to merge 26 commits into
mainfrom
feat/gallery-ui-design-system
Draft

feat: refactor the scaffold gallery onto a @webjsdev/ui class-helper design system#1060
vivek7405 wants to merge 26 commits into
mainfrom
feat/gallery-ui-design-system

Conversation

@vivek7405

@vivek7405 vivek7405 commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Closes #1059

What this started as

Refactor the scaffold feature gallery onto @webjsdev/ui's tier-1 class helpers (buttonClass / cardClass / inputClass) for its high-repetition primitives, instead of hand-rolled inline Tailwind on every demo. Decision + rationale in #1058. The gallery keeps looking the same (same tokens, neutral palette): a consistency + variants refactor, not a redesign. The helpers are CLASS strings spread onto native elements (class=${buttonClass({...})}), so demo markup stays readable and the #1057 cursor-pointer bug class becomes impossible.

What it became

The refactor surfaced a bigger decision: the gallery's design system is an EXAMPLE to learn from, not a base to inherit. So the branch now makes the split explicit and durable.

1. gallery:clear is a truly barebones blank slate. It removes the gallery, the example design system (components/ui/), the example theme-toggle (and strips its import from the layout + its widget from the reset home), the example test suite (test/hello/), and every empty leftover dir (app/api, test/unit, test/e2e, test/). It keeps only the buildable base: the skill, the layout (OS-preference dark mode + tokens, minus the toggle button), db wiring, and cn.ts. Verified: cleared app boots, no dangling refs, dark mode still honoured, webjs check passes.

2. The skill is enforced as the durable teacher. Since the gallery is disposable, .agents/skills/webjs/ is the only teaching surface that survives a clear. require-scaffold-with-src.sh now counts a skill change as a satisfying teaching surface, and webjs-scaffold-sync elevates the skill to surface 1 with a "needs BOTH skill + demo" change-type row, a skill-vs-gallery coverage sweep, and the clear-gallery removal list as a synced surface.

3. Skill-vs-gallery coverage audit + fills. Audited every gallery demo against the skill references (5 parallel clusters) and filled every concept the demos teach but the skill omitted, so nothing is lost at clear time:

  • components.md: effect / batch, Task / TaskStatus, the context API, the lit-html directive table
  • client-router-and-streaming.md: enableClientRouter, the connectWS handle + onOpen/onClose, the renderStream-clobber gotcha, view-transition via metadata.other
  • auth-and-sessions.md: the Session method API (fixing the wrong s.userId= style), storage factories, scrypt password hashing (fixing the phantom verifyPassword), handlers mounting, the no-JS login/logout flow, per-segment middleware protection
  • data-and-actions.md: actionSignal() cancellation, the middleware ctx shape
  • routing-and-pages.md: the route-handler toolkit (json/readBody/clientIp), richFetch, image metadata routes
  • built-ins.md: env.ts boot validation, signedUrl's base option
  • optimistic-ui.md: the degrade-first <form> + page-action pattern
  • styling.md: accessible native controls
  • SKILL.md: routes every new export to its reference

Tests

  • test/scaffolds/scaffold-gallery.test.js: a new full-stack gallery:clear blank-slate test (none existed), pre-clear existence asserts as the counterfactual.
  • test/hooks/require-scaffold-with-src.test.mjs: a new case proving the skill satisfies the scaffold gate.
  • Verified a freshly generated app carries the updated skill content end to end.

Draft until self-review converges.

…fold

Add components/ui/{button,card,input,textarea,badge}.ts (the ui tier-1 class
helpers buttonClass/cardClass/inputClass/textareaClass/badgeClass) to the
gallery template, with the cn import rewritten to the scaffold's #lib/utils/cn.ts
alias. First step of the gallery design-system refactor (#1059).
@vivek7405 vivek7405 self-assigned this Jul 23, 2026
@vivek7405

Copy link
Copy Markdown
Collaborator Author

Foundation de-risked: the ui helpers resolve and render with zero extra deps

Before touching the demos I proved the helper layer works in a real scaffolded app. Three things confirmed:

  • The scaffold ships "#*": "./*" in its imports map and writes cn to lib/utils/cn.ts, so import { cn } from '#lib/utils/cn.ts' resolves in a generated app.
  • That cn is the scaffold's DEPENDENCY-FREE inline clsx + tailwind-merge replacement, so buttonClass needs no clsx / tailwind-merge install. The helpers work out of the box.
  • Wired buttonClass() into the streaming demo in a real scaffolded app, restarted it, and /features/streaming renders 200 (a broken import would 500).

So the components/ui/{button,card,input,textarea,badge}.ts files (cn import rewritten to the # alias) are a sound base. The rest is the mechanical swap of inline class strings for the helper calls across the demos, keeping the same look.

Rewrite components/ui/button.ts into the gallery's themed design token (the
own-and-theme @webjsdev/ui model): a variant scale (default/secondary/ghost/
link) + size scale (default/sm/xs/none) whose class values reproduce the
gallery's existing look (rounded-xl primary, card-surfaced secondary, muted
link), with dense teaching comments on WHY a class helper (no indirection, one
source of truth, the #1057 cursor gap becomes impossible) and HOW to theme it.

Convert every light-DOM gallery button (~25 sites across 15 files, plus the
stream-demo shared const) from hand-rolled inline Tailwind to
buttonClass({ variant, size }). Bespoke toggles (todo checkbox/delete, the
like pill) and shadow-DOM demos are left as-is. All feature pages render and
webjs check passes.
…helpers

- components/ui/card.ts: themed cardClass() to the gallery panel surface
  (rounded-2xl border bg-card), surface-only so per-panel padding/layout stays
  on the call site; convert every light-DOM demo panel to cardClass().
- components/ui/input.ts: themed inputClass() to the gallery field look; convert
  the shared inputCls consts (forms + auth) to it (one style for input AND
  textarea, matching the app's intent).
- Drop the unused textarea.ts + badge.ts rather than ship dead files.
- Dense teaching comments on each: WHY a class helper, WHY surface-vs-layout,
  and the own-and-theme workflow, so an agent reading the scaffold learns to
  build and theme a design system for its own app.

All pages render, webjs check passes.
…erence

Add a 'design system for repeated primitives' subsection: use a class helper
(buttonClass/cardClass/inputClass built on @webjsdev/ui) for a themed primitive
with variants, vs a lib/utils/ui.ts html-fragment helper for a repeated markup
chunk. Teaches the own-and-theme workflow the gallery now demonstrates, so an
agent gathering context from the scaffold learns to build a themed design
system for its own app.
…plicit)

The design system in components/ui/ (buttonClass/cardClass/inputClass + cn) is
infrastructure a real app keeps importing after the reset, not demo cruft. It
already survives (never in the removal list), but state that intent explicitly
in the header + closing message so it is not accidentally added later. This is
the durable home for the design-system pattern (skill + surviving helpers),
while the demos are the disposable runnable illustration.
copyGallery only copied {app,modules,test}, so a fresh webjs create shipped
demos importing #components/ui/button.ts with no components/ui present, a broken
scaffold. Add components to the copied subdirs so the design-system helpers land
in every new app (and gallery:clear keeps them).
Reverse course: gallery:clear now REMOVES components/ui/ (the gallery's example
design system) so the reset app is a barebones blank slate. Keeping it would
nudge the implementing agent to lean on the gallery's buttons instead of
building the app's own. The durable knowledge is the PATTERN, which lives in the
skill (.agents/skills/webjs/references/styling.md, re-read every session); cn.ts
is kept as the webjs ui add prerequisite, so after clear the agent runs
webjs ui add and themes its own components/ui/. Verified: clear removes
components/ui, keeps cn + skill, cleared app boots.
Per direction, gallery:clear now strips the app to a minimal buildable base
rather than 'base scaffold minus gallery'. Beyond the gallery + components/ui, it
also removes the example theme-toggle component (and strips its registration
import from the layout + its widget from the reset home), the example test suite
(test/hello/), and every empty leftover dir (app/api after its lone auth handler,
test/unit, test/e2e, test/). Kept: the skill, the layout (with OS-preference dark
mode + tokens, minus the toggle button), db wiring, cn.ts, and empty
components/ + modules/ as build targets. Verified: cleared app boots, no dangling
theme-toggle refs, dark mode still honoured, webjs check passes.
Now that gallery:clear strips the gallery to a blank slate, the agent skill
.agents/skills/webjs/ is the ONLY teaching surface that survives, so a feature
the skill does not teach is lost the moment an agent clears the gallery. Elevate
the skill to a co-primary teaching surface alongside the gallery:

- require-scaffold-with-src.sh now counts .agents/skills/webjs/ as a satisfying
  teaching surface (a feature-source commit is satisfied by the gallery OR the
  skill), and names the skill in its block message.
- webjs-scaffold-sync SKILL.md adds the skill as surface 1 (the durable teacher),
  a change-type row requiring BOTH skill + demo, a skill-vs-gallery coverage
  sweep in audit mode, and the clear-gallery removal list as a synced surface.

The per-surface judgment (did you update both where needed) stays the skill's
manual walk; the hook is the commit-time floor.
Assert gallery:clear strips the gallery + example design system + example
theme-toggle (and its layout/home wiring) + example tests + empty leftover dirs,
while keeping the skill, layout, db wiring, and cn.ts. Pre-clear existence asserts
are the counterfactual. No full-stack clear test existed before.
@vivek7405

Copy link
Copy Markdown
Collaborator Author

Progress: the gallery is now disposable, the skill is the durable teacher

Beyond the original @webjsdev/ui class-helper design-system refactor, this branch has taken on the follow-on decision that fell out of it: since the gallery's design system is an example to learn from (not a base to inherit), gallery:clear should shed it and leave the agent to build its own, with the durable knowledge living in the skill.

Landed so far:

  • gallery:clear is now a truly barebones blank slate. It removes the gallery, the example design system (components/ui/), the example theme-toggle (and strips its import from the layout + its widget from the reset home), the example test suite (test/hello/), and every empty leftover dir (app/api, test/unit, test/e2e, test/). It keeps only the buildable base: the skill, the layout (with OS-preference dark mode + tokens, minus the toggle button), db wiring, and cn.ts (the webjs ui add prerequisite). Verified: cleared app boots, no dangling refs, dark mode still honoured, webjs check passes.
  • webjs-scaffold-sync now enforces the skill, not just the gallery. The skill .agents/skills/webjs/ is elevated to surface 1 (the durable teacher, the only surface that survives the clear). require-scaffold-with-src.sh counts a skill change as a satisfying teaching surface, and the skill doc adds a change-type row requiring BOTH skill + demo, a skill-vs-gallery coverage sweep, and the clear-gallery removal list as a synced surface.
  • Test coverage for the truly-minimal clear (no full-stack clear test existed before), with pre-clear existence asserts as the counterfactual.

In progress:

  • Skill-vs-gallery coverage audit. Auditing whether .agents/skills/webjs/ teaches every concept the ~26 gallery demos show, so nothing is lost at clear time. Filling any gaps in the matching references.

vivek7405 added 16 commits July 23, 2026 12:47
The gallery demos (reactive-meter, task-loader, theme-context, directive-demo)
teach these, but the skill omitted them, so they vanished at gallery:clear. Add
effect/batch to the signals section, and new sections for Task/TaskStatus,
the context API, and a lit-html directive table.
Fill gaps the gallery demos (router-controls, ws-echo, broadcast-feed, stream-demo,
view-transitions) teach but the skill omitted: enableClientRouter() as the toggle
counterpart, the connectWS return handle (.send/.close) + onOpen/onClose lifecycle,
the renderStream-clobber gotcha (plain field not signal), and the metadata.other
form for the per-page view-transition opt-in.
…hape

The server-actions demo teaches actionSignal() (request AbortSignal, the 499
cancelled-envelope pattern) and the per-action middleware ctx { request, args,
signal, context } with ctx.context writes read back via actionContext(); the
skill named them but never showed them. Add both.
…utes

The route-handler + metadata-image demos teach json()/readBody()/clientIp() (and
the no-arg accessor convention), richFetch() for hand-calling your own route.ts,
and image metadata routes that return a Response with an inline SVG referenced
from metadata; the skill omitted all three. Add them.
The auth/sessions demos teach what the skill omitted or got wrong: the Session
method API (.get/.set/.flash/.destroy, replacing the incorrect s.userId=
property style), the cookieSession/storeSession storage factories, scrypt
password hash/compare (the skill referenced a nonexistent verifyPassword),
mounting handlers at app/api/auth/[...path] with the fixed-path constraint, the
no-JS login/logout form flow (redirectTo + pages.error + ?error), and per-segment
middleware protection via auth(req).
The env demo directs readers to an app-root env.ts (schema or validator, fails
fast) and the file-storage demo uses signedUrl's base option to target a custom
serve route; the skill covered neither. Add both.
The todo example authors each optimistic mutation as a real <form method=post>
that degrades to the page action (with a hidden intent for multi-mutation pages),
and hand-authors accessible controls (label-for, aria-pressed, aria-label); the
skill taught neither. Add the degrade-first form pattern to optimistic-ui.md and
an accessible-controls note to styling.md.
Add effect/batch to the signals line, the missing directives (asyncAppend/
asyncReplace/templateContent) + Task/TaskStatus + context to the directives line,
and the route-handler toolkit (json/readBody/clientIp) + auth/session/file-storage
helpers to the server-side export map, each pointing at the reference that now
teaches it. Closes the skill-vs-gallery coverage gaps.
Cover the new behavior: a feature-src commit is satisfied by a .agents/skills/webjs/
change (the durable teacher), not only a gallery/generator change. The existing
'blocks when no surface staged' case is the counterfactual.
…ystem

Three demos still hand-reimplemented the button base + variants inline instead of
using buttonClass, the exact drift the design system exists to prevent (they even
missed the focus-visible ring every other button has). The like toggle IS the
secondary/default variants, and the two 'Go to page two' links ARE the default
variant on an <a>. Compose buttonClass() and override only the genuinely bespoke
bits with cn() (the like button's pill radius + heart SVG). The todo
toggle-checkbox, delete icon-button, and the native file: input button stay
bespoke (the variants do not cover them). Verified: all render, webjs check passes.
Extend the comment to say why the liked state re-adds a border: secondary carries
a 1px border and default carries none, so under box-sizing border-box the toggle
would jitter the button width by ~1px without it. The border matches the primary
fill (invisible), doing only the no-jitter job.
…lpers

Balanced design-system extension (per decision). New primitives the audit found
missing: badgeClass (components/ui/badge.ts, themed to the scaffold's soft-primary
pill + muted outline tag), a 'destructive' button variant (subtle muted-to-red,
so the todo delete icon converts cleanly), and lib/utils/ui.ts markup-chunk
helpers (pageHeading/lede/backLink, the styling.md html-fragment pattern). Wire
copyGallery to ship the gallery lib/. Also fix the stale copyGallery comment that
claimed gallery:clear keeps components/ui (it removes it). Conversions follow.
…ystem

Convert the create.js-emitted base surfaces to the helpers: theme-toggle uses
buttonClass({ variant: 'secondary', size: 'none' }) + cn() overrides for the
circular icon shape (dropping its bespoke focus ring for the shared one); the
home feature/example cards use cardClass(); the 'Example app' pill uses
badgeClass({ variant: 'outline' }). Teach the icon-button pattern (size:'none' +
cn overrides, since cn does not merge p-0 vs px-4 py-2) in styling.md. Verified:
generates, home SSRs 200, webjs check passes.
Sweep the gallery to eliminate inline-Tailwind that a helper should own (the
like-button drift class, everywhere). Drift fixes: hand-rolled inputs ->
inputClass (broadcast, ws-echo, directives, view-transitions x2), panels ->
cardClass (theme-context, file-storage), the global-not-found button -> buttonClass
(clearing bg-neutral/text-white hardcoded colors), reactive-meter #8883 ->
var(--color-border), task-loader text-red-500 -> text-destructive. Primitives:
the 'Signed in' pill -> badgeClass, the todo delete icon -> the destructive
variant, the two Gallery back-links -> backLink(). Sweep: 29 pages + 4 boundary
files use pageHeading()/lede(). Verified: generate + webjs check + typecheck +
boot, all 27 routes serve (boundaries 403/401 by design).
gallery:clear now also removes lib/utils/ui.ts (its users, the feature pages +
features/examples layouts, are all cleared), keeping cn.ts/dom.ts. Update the
scaffold-gallery tests: assert ui.ts is present pre-clear and removed post-clear,
and match the back-to-gallery link via the backLink('/') helper instead of a
literal href. Verified: full suite green, boundary routes render (403/401/404 +
error boundary).
Update stale assertions: the gallery now ships components/ui/ (buttonClass/
cardClass/inputClass/badgeClass) + lib/utils/ui.ts, and the home builds its cards
on cardClass/badgeClass. The pre-init config (components.json, cn.ts,
styles/globals.css) still ships for webjs ui add. Full scaffold suite green (46).
@vivek7405

Copy link
Copy Markdown
Collaborator Author

Design-system consistency sweep (every component, not just the like-button)

Audited every gallery component + page + the base surfaces (three parallel read-only agents) for inline Tailwind that a design-system helper should own, then fixed it all. Decision: extend the system with the genuinely-missing primitives, keep prose-level repeats inline for readability.

New primitives:

  • components/ui/badge.ts (badgeClass, themed: soft-primary pill + muted outline tag)
  • a destructive button variant (subtle muted-to-red, so the todo delete icon converts cleanly)
  • lib/utils/ui.ts fragment helpers (pageHeading / lede / backLink)

Conversions (no hand-rolled primitive left where the system covers it):

  • Base surfaces: theme-toggle to buttonClass({size:'none'}) + cn overrides; home cards to cardClass(); the "Example app" pill to badgeClass.
  • Drift: hand-rolled inputs to inputClass (broadcast, ws-echo, directives, view-transitions x2); panels to cardClass (theme-context, file-storage); the global-not-found button to buttonClass.
  • Hardcoded colors to tokens: #8883 to var(--color-border), text-red-500 to text-destructive, and bg-neutral-900/text-white/text-orange-500/text-neutral-500 gone.
  • Primitives: the "Signed in" pill to badgeClass; the todo delete to the destructive variant; the two byte-identical back-links to backLink().
  • Sweep: 29 pages + 4 boundary files use pageHeading() / lede().

Left intentionally bespoke: shadow-DOM reactive-meter (static styles), the todo round-check toggle (a checkbox, not a button), the native file: input pseudo, SVG icons, and prose-level repeats (text links, inline code, ul lists). A helper there would hurt readability more than it helps.

Verified: generate + webjs check + webjs typecheck + boot; all 27 routes serve (boundaries 403/401/404 + the error boundary); gallery:clear now also removes lib/utils/ui.ts; full scaffold suite green (46), including updated scaffold-ui-integration + scaffold-gallery assertions. Taught the icon-button pattern (size:'none', since cn cannot merge p-0 against px-4 py-2) + badgeClass in styling.md.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor the scaffold gallery onto a @webjsdev/ui class-helper design system

1 participant