feat: refactor the scaffold gallery onto a @webjsdev/ui class-helper design system#1060
feat: refactor the scaffold gallery onto a @webjsdev/ui class-helper design system#1060vivek7405 wants to merge 26 commits into
Conversation
…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).
|
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:
So the |
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.
Progress: the gallery is now disposable, the skill is the durable teacherBeyond the original Landed so far:
In progress:
|
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).
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:
Conversions (no hand-rolled primitive left where the system covers it):
Left intentionally bespoke: shadow-DOM Verified: generate + |
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:clearis a truly barebones blank slate. It removes the gallery, the example design system (components/ui/), the exampletheme-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, andcn.ts. Verified: cleared app boots, no dangling refs, dark mode still honoured,webjs checkpasses.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.shnow counts a skill change as a satisfying teaching surface, andwebjs-scaffold-syncelevates 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 tableclient-router-and-streaming.md:enableClientRouter, theconnectWShandle +onOpen/onClose, the renderStream-clobber gotcha, view-transition viametadata.otherauth-and-sessions.md: theSessionmethod API (fixing the wrongs.userId=style), storage factories, scrypt password hashing (fixing the phantomverifyPassword),handlersmounting, the no-JS login/logout flow, per-segment middleware protectiondata-and-actions.md:actionSignal()cancellation, the middlewarectxshaperouting-and-pages.md: the route-handler toolkit (json/readBody/clientIp),richFetch, image metadata routesbuilt-ins.md:env.tsboot validation,signedUrl'sbaseoptionoptimistic-ui.md: the degrade-first<form>+ page-action patternstyling.md: accessible native controlsSKILL.md: routes every new export to its referenceTests
test/scaffolds/scaffold-gallery.test.js: a new full-stackgallery:clearblank-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.Draft until self-review converges.