diff --git a/AGENTS.md b/AGENTS.md index ff6afca89..e3baedb0c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -255,7 +255,7 @@ MyThing.register('my-thing'); **Async render (`async render()`), bare-await data fetch (#469).** A component may write `async render() { const u = await getUser(this.id); return html\`

${u.name}

\`; }`. Writing `await` makes the function async by JS rule, and every render path awaits a promise-returning `render()` automatically (no flag). This co-locates the fetch in the leaf component (no prop-drilling). The model is decoupled into three separate concerns. (1) **SSR always blocks**, so the resolved DATA is in the first paint with no fallback markup (PE-safe, JS-off reads it). (2) **The client re-fetch default is stale-while-revalidate**: when a prop / dependency change re-runs `async render()`, the current content stays until the new render resolves (no blank, no flash). (3) **`renderFallback()` is the OPTIONAL re-fetch loading UI**, a prop-aware method shown ONLY during a client re-fetch, NEVER on the first paint, and it does NOT trigger SSR streaming. **Errors are isolated per component by default** (no user code): a thrown `await getData()` renders a component-scoped error state while siblings render, and `renderError()` optionally customizes it (dev surfaces the message, prod stays silent). `getData()` is already isomorphic (a `'use server'` action is the real function during SSR and an RPC stub on the client), so the same line works both sides. Use `async render()` for request-time-known SERVER data that should be in the first paint; keep `Task` / signals for genuinely client-only data (a `Task` shows its pending state at SSR, losing first-paint data). A **bare** async-render component (an `async render()` with no other client signal, light DOM) is **elided** like any display-only component (#474): its SSR'd HTML is the complete output, so the framework drops the module AND the redundant on-hydration re-fetch. It SHIPS only when it also carries an independent signal (an `@event`, a non-`state` reactive prop, a signal / reactive import, a lifecycle hook including `renderFallback()`, a ``, `static shadow = true`, `static refresh = true`, cross-module observation, or a transitively-reachable interactive child). Two carve-outs always ship: `static shadow = true` (Declarative Shadow DOM attaches only during HTML parsing, so a streamed or soft-navigated shadow component needs its module to re-run `attachShadow`) and `static refresh = true` (the explicit opt-in keeping the stale-while-revalidate on-load re-fetch that eliding drops, moot for request-stable data). **For SLOW data where blocking the first byte hurts, wrap the region in `` to STREAM it** (the fallback flushes on the first byte, the data streams in; multiple boundaries fetch concurrently). This is the only way to show a first-paint fallback, a deliberate choice for slow regions, and it streams progressively on soft navigation too. A throwing component inside a boundary is isolated (renders its error state, siblings stream). **The on-hydration re-fetch is itself eliminated by SSR action seeding (#472):** each `'use server'` action result invoked during a (non-streamed) SSR render is serialized into the page, and the generated RPC stub reads that seed on its first client call, so a shipping async component does NOT re-issue the RPC on hydration (a later refetch / arg-change still goes to the network). Keyed by action-hash + fn + serialized args, consume-once, fail-open (a miss degrades to a normal RPC, never wrong data). Captured via a transparent server-side `'use server'` facade (no source transform, no build step; the browser source tab and on-disk files are unchanged), default on, opt out with `"webjs": { "seed": false }` or `WEBJS_SEED=0`. -**Light DOM (default) vs Shadow DOM.** Light DOM applies global CSS and Tailwind directly (default; for Tailwind/global CSS + simple composition). Shadow DOM (`static shadow = true`) is for `static styles` scoped CSS and third-party isolation; `` works in either. A light-DOM component authoring custom CSS MUST prefix every class selector with its tag name (invariant 7); prefer Tailwind. **Never interpolate into a component's `\``): the server emits it but the client drops the raw-text hole, so it paints at SSR then wipes to empty on hydrate. Use `static styles` or Tailwind instead (flagged by `no-interpolation-in-raw-text-element`). A page/layout, which never hydrates, may interpolate a `css` result into `\``): the server emits it but the client drops the raw-text hole, so it paints at SSR then wipes to empty on hydrate. Use `static styles` or Tailwind instead (flagged by `no-interpolation-in-raw-text-element`). A page/layout, which never hydrates, may interpolate a `css` result into ` -
- - ${displayName} - - -
- - -
-
- \${children} -
- - -
+ +
+ \${children} +
\`; } `); @@ -1406,9 +1400,9 @@ export default function Home() { \${rubric('welcome')} \${displayH1(html\`Hello from ${displayName}.\`)}

- This scaffold ships a gallery below: single-feature demos and one whole - example app, all small, idiomatic, and heavily commented. Browse them for - context, then replace this page with your own. See + This WebJs scaffold ships a gallery below: single-feature demos and one + whole example app, all small, idiomatic, and heavily commented. Browse + them for context, then replace this page with your own. See \${accentLink('https://docs.webjs.dev', 'the docs')} for the full reference.

diff --git a/packages/cli/lib/db-hints.js b/packages/cli/lib/db-hints.js new file mode 100644 index 000000000..ed28ecc67 --- /dev/null +++ b/packages/cli/lib/db-hints.js @@ -0,0 +1,34 @@ +// Actionable hints for `webjs db` failure modes that drizzle-kit surfaces +// opaquely. Kept as pure functions so the dispatch path in bin/webjs.js stays a +// thin spawn and the messages are unit-testable without a child process. + +// drizzle-kit prints this on stderr when a rename prompt has no TTY to answer. +// It is the reliable signal: this drizzle-kit version EXITS 0 on that failure, +// so the exit code cannot be used, and keying on stderr also means an unrelated +// generate failure (a schema type error, a missing config) does NOT misfire. +const TTY_PROMPT = /require a tty|interactive prompt/i; + +/** + * `webjs db generate` off a non-interactive stdin dead-ends when a table is + * renamed or swapped: drizzle-kit asks "is a rename of ?" + * and, with no TTY to answer, prints "Interactive prompts require a TTY" to + * stderr (and exits 0). Returns the escape-hatch hint only when the captured + * stderr carries that signature, so it fires on exactly that case and stays + * silent on success, an interactive run, another subcommand, or an unrelated + * generate error. + * + * @param {string} sub the `webjs db` subcommand (generate|migrate|push|studio) + * @param {boolean|undefined} isTTY process.stdin.isTTY + * @param {string|undefined} stderr drizzle-kit's captured stderr + * @returns {string|null} + */ +export function dbGenerateTtyHint(sub, isTTY, stderr) { + if (sub !== 'generate' || isTTY) return null; + if (!TTY_PROMPT.test(stderr || '')) return null; + return ( + '\nwebjs db generate needs an interactive terminal to resolve a table rename.\n' + + 'Run it in a real terminal to answer the prompt, or, if the dev database has no\n' + + 'data yet, delete the db/migrations/ folder and re-run to author a clean\n' + + 'create-table migration.' + ); +} diff --git a/packages/cli/lib/design-bar.js b/packages/cli/lib/design-bar.js new file mode 100644 index 000000000..c0e5cdfdf --- /dev/null +++ b/packages/cli/lib/design-bar.js @@ -0,0 +1,67 @@ +// The design bar the scaffold sets (AGENTS.md / CONVENTIONS.md item 6): a +// delivered UI app must have its OWN design, not the scaffold's. The scaffold is +// a teaching artifact for how to USE the framework, never a starting design. +// This lives in one place so the `--clear-placeholders` reminder and the +// `webjs doctor` advisory speak with one voice (the clear command strips the +// layout marker that carried this reminder just-in-time, so it is re-surfaced +// there, and doctor catches an app that kept the shell anyway). + +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; + +/** + * True when the app has a root layout, i.e. it is a UI app the design bar + * applies to. The `api` template ships no `app/layout`, so the reminder / + * advisory stay quiet there. + * @param {string} appDir + * @returns {boolean} + */ +export function hasUiLayout(appDir) { + return ['ts', 'js', 'mts', 'mjs'].some((e) => existsSync(join(appDir, 'app', `layout.${e}`))); +} + +export const DESIGN_REMINDER = + '\nDesign: the scaffold is a TEACHING artifact, not a starting design.\n' + + 'A delivered UI app must have its OWN design chosen from what the app IS:\n' + + 'layout, palette, typography, icons, spacing, and chrome.\n' + + '- Layout: app/layout.ts ships as a MINIMAL shell (no header / nav / footer /\n' + + ' reading column). Design your own from what the app IS. LAYOUT-REFERENCE.md\n' + + ' shows the mechanics; do not reproduce its example header verbatim.\n' + + '- Palette: the design-token NAMES (--background, --primary, --card, ...) are\n' + + ' infrastructure to keep, but their COLOR VALUES are yours. Set a distinctive\n' + + ' palette that fits the app; keeping the starter orange is not a redesign.\n' + + '- Verify by USING it: render the app and play through every state, and confirm\n' + + ' nothing resizes or shifts as it fills (even, stable cells). A glance at the\n' + + ' empty first paint is not enough; the layout bugs show up mid-interaction.\n' + + 'See AGENTS.md / CONVENTIONS.md item 6.'; + +// Distinctive strings that indicate an app kept scaffold-specific chrome or the +// unmodified starter palette, rather than designing its own. Counting them is an +// objective proxy for "did not own the design" without judging taste. NOTE: the +// theme apparatus (`--header-h`, the `theme-toggle` import) is KEEP-infrastructure +// the minimal shell ships in every app, so it is NOT a tell (it fired on every +// finished app and made the advisory nag forever). The tells that remain are +// genuine "reproduced the scaffold" signals: the exact 760px reading column, the +// scaffold's own attribution footer, and the two exact default palette VALUES (a +// verbatim match means the palette was never changed; a recolor does not match). +const SHELL_TELLS = [ + { key: 'reading-column (max-w-[760px])', re: /max-w-\[760px\]/ }, + // Specific to the scaffold's own attribution (its footer links webjs.dev and + // says "Built with webjs"). A bare "Built with ..." is a common bespoke footer, + // so it is NOT a tell on its own. + { key: 'attribution footer', re: /webjs\.dev|Built with webjs/ }, + { key: 'default scaffold primary color', re: /--primary:\s*oklch\(0\.7\s+0\.16\s+52\)/ }, + { key: 'default scaffold card color', re: /--card:\s*oklch\(0\.18\s+0\.01\s+55\)/ }, +]; + +/** + * The scaffold-shell tells present in a root-layout source string. Two or more + * is a strong signal the app kept the scaffold chrome instead of designing its + * own. Returns the human-readable keys that matched. + * @param {string} layoutSrc + * @returns {string[]} + */ +export function scaffoldShellTells(layoutSrc) { + if (typeof layoutSrc !== 'string' || !layoutSrc) return []; + return SHELL_TELLS.filter((t) => t.re.test(layoutSrc)).map((t) => t.key); +} diff --git a/packages/cli/lib/doctor.js b/packages/cli/lib/doctor.js index 3600cfad1..cd42cb09b 100644 --- a/packages/cli/lib/doctor.js +++ b/packages/cli/lib/doctor.js @@ -842,6 +842,42 @@ async function checkElisionCarriers(appDir) { }; } +/** + * ADVISORY: the delivered app still rides the scaffold shell. AGENTS.md / + * CONVENTIONS.md item 6 asks a UI app to own its design (layout, palette, + * typography, chrome); the scaffold is a teaching artifact, not a starting + * design. WARN-level and never a hard fail: a reading column or a theme toggle + * CAN be a legitimate choice, so this nudges, it does not gate. The signal is + * objective (distinctive scaffold-authored chrome strings still present in the + * root layout), not a judgment of taste. Two or more tells is the threshold. + * @param {string} appDir + * @returns {Promise} + */ +async function checkScaffoldDesign(appDir) { + const name = 'App design (own design, not the scaffold shell)'; + let layoutSrc = ''; + for (const ext of ['ts', 'js', 'mts', 'mjs']) { + const p = join(appDir, 'app', `layout.${ext}`); + if (existsSync(p)) { layoutSrc = await readFile(p, 'utf8').catch(() => ''); break; } + } + if (!layoutSrc) { + return { name, status: 'pass', message: 'no app/layout to analyse' }; + } + const { scaffoldShellTells } = await import('./design-bar.js'); + const tells = scaffoldShellTells(layoutSrc); + if (tells.length < 2) { + return { name, status: 'pass', message: 'app/layout does not look like the unmodified scaffold shell' }; + } + return { + name, + status: 'warn', + message: + `app/layout still carries ${tells.length} scaffold design signal(s): ${tells.join(', ')}. ` + + 'A delivered UI app should own its design (layout AND palette), not adapt the scaffold.', + fix: 'Design the app\'s own layout, palette, typography, and chrome from what the app IS (a centered board, a full-bleed dashboard, ...), not the scaffold\'s exact 760px reading column, its "Built with webjs" attribution footer, or the unmodified starter palette values (the theme-toggle and --header-h are keep-infrastructure). Recoloring the scaffold is not a redesign. Render the app and look at it. See AGENTS.md / CONVENTIONS.md item 6.', + }; +} + export async function runDoctorChecks(appDir, opts = {}) { const cliDir = opts.cliDir || new URL('.', import.meta.url).pathname; const results = await Promise.all([ @@ -854,6 +890,7 @@ export async function runDoctorChecks(appDir, opts = {}) { checkImportmapCoherence(appDir, opts), Promise.resolve(checkGitHook(appDir)), checkElisionCarriers(appDir), + checkScaffoldDesign(appDir), ]); return results; } diff --git a/packages/cli/templates/.agents/rules/workflow.md b/packages/cli/templates/.agents/rules/workflow.md index 0d1c36279..a5c77bfbb 100644 --- a/packages/cli/templates/.agents/rules/workflow.md +++ b/packages/cli/templates/.agents/rules/workflow.md @@ -48,21 +48,39 @@ cover what you need, the full hosted docs are at **https://docs.webjs.dev**. logic in `modules/`. - **Use a unique design, and redesign means more than recolor (UI apps).** Give the app a design of its own (palette, typography, LAYOUT, and chrome) chosen - from what the app IS. Recoloring the scaffold and swapping the logo while - keeping its skeleton (a fixed top header with a Home link and a theme toggle, - the centered ~760px reading column, the "Built with webjs" footer) is NOT a - unique design. Decide from scratch whether this app even needs a header or - footer, what nav (if any), and what layout fits (a centered board, a full-bleed - dashboard, a split, a single card). The scaffold ships a - `webjs-scaffold-placeholder` marker on its footer, so `webjs check` fails until - you remove or replace the "Built with webjs" branding. Self-audit before - finishing: nothing should read as the scaffold example (no "Built with webjs" - footer, no leftover example nav, no default reading column unless it truly - fits). Keep only the design TOKENS and theme wiring in `app/layout.ts` - (infrastructure the ui kit reads) and restyle on top. Style with Tailwind - utilities wherever they reach, and use custom CSS only for what utilities - cannot express (@theme tokens, @keyframes, scrollbar, complex color-mix or - gradients). The `api` template has no UI, so this does not apply there. + from what the app IS. `app/layout.ts` ships as a MINIMAL shell (theme, design + tokens, and Tailwind infra, then `${children}` in a bare padded container) with + NO header, nav, footer, or reading column: design the app's own chrome from + scratch. Decide whether it needs a header at all, a nav (or none), a footer, a + sidebar, a centered reading column, or a full-bleed canvas, from what fits the + app. `LAYOUT-REFERENCE.md` at the project root is a complete worked layout to + learn the patterns from, then build your own. Two `webjs-scaffold-placeholder` + markers gate `webjs check`: the minimal shell ("design your layout from + scratch") and the palette block ("own the colors"), so check fails until each + is addressed. Keep the design TOKENS and theme wiring in `app/layout.ts` + (infrastructure the ui kit reads) and set the token VALUES to your own palette; + run `webjs check --clear-placeholders` to keep the starter palette + deliberately. Style with Tailwind utilities wherever they reach, and use custom + CSS only for what utilities cannot express (@theme tokens, @keyframes, + scrollbar, complex color-mix or gradients). The `api` template has no UI, so + this does not apply there. +- **Render the app and LOOK before you call UI work done (every agent, not just one harness).** + You write CSS blind, so a layout or design defect ships silently: `webjs check` + and `webjs typecheck` pass even when a component collapses, grid cells are + uneven, the layout resizes as it fills, or the app just kept the scaffold's + colors. Static tools give no failure signal for this. The only thing that + catches it is rendering the app and looking at the pixels. So for ANY page, + layout, or component work: run it (`webjs dev`), open every route you changed in + a real browser (drive it with your harness's browser tool or MCP if it has one, + otherwise open it yourself and screenshot), and PLAY THROUGH every state (empty, + filled, win, draw, reload, narrow and wide, light and dark). Confirm nothing + collapses or reflows, that cells stay equal, that the design is the app's OWN, + and that both themes read. Ship a real-browser test (`webjs test --browser`) for + the mechanical floor (measure `getBoundingClientRect()` and assert cells stay + equal across a move). Fix and re-render until it holds, then state in your final + message what you rendered and confirmed. Claude Code additionally ENFORCES this + via the `webjs-design-review` skill plus a Stop hook, but the discipline is + harness-agnostic and this rule is the source of truth for every agent. - **Only three templates exist:** `webjs create ` (default full-stack), `--template api`, `--template saas`. The CLI rejects any other `--template` value. Pick: diff --git a/packages/cli/templates/.claude/hooks/design-review-before-stop.sh b/packages/cli/templates/.claude/hooks/design-review-before-stop.sh new file mode 100755 index 000000000..9c6e283f2 --- /dev/null +++ b/packages/cli/templates/.claude/hooks/design-review-before-stop.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# +# Claude Code Stop hook: render-and-look before finishing UI work. +# +# An AI agent writes CSS blind (it never renders), and layout / design defects +# have NO failure signal: `webjs check` and `typecheck` pass, the app runs. So a +# collapsed board, uneven cells, a layout that resizes as it fills, or an app +# that just kept the scaffold's design all ship silently. The one thing that +# catches them is looking at the rendered pixels. This backstop fires at the END +# of a turn that touched UI files and reminds you to render the app and inspect +# every state (see the webjs-design-review skill + CONVENTIONS item 6) before you +# stop. Loop-safe (fires at most once per stop) and skipped when no UI changed. +# +# Disable with WEBJS_NO_DESIGN_STOP=1. + +set -uo pipefail +payload=$(cat 2>/dev/null || true) + +if [ "${WEBJS_NO_DESIGN_STOP:-}" = "1" ]; then exit 0; fi +active=$(printf '%s' "$payload" | jq -r '.stop_hook_active // false' 2>/dev/null || echo false) +if [ "$active" = "true" ]; then exit 0; fi +if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then exit 0; fi + +# Did this turn touch UI surface? A component, a page/layout, or app styling. +ui_changed=$(git status --porcelain --untracked-files=all 2>/dev/null \ + | grep -vE '(^|/)(node_modules|\.webjs)(/|$)' \ + | grep -cE '(app/.*(page|layout)\.(t|j)sx?$)|(components/.*\.(t|j)sx?$)|(modules/.*components/.*\.(t|j)sx?$)|(\.css$)' || true) + +if [ -z "$ui_changed" ] || [ "$ui_changed" -lt 1 ]; then exit 0; fi + +reason="You changed UI in this turn but a design/layout defect has no failing test: check and typecheck pass even when a component collapses, cells are uneven, the layout shifts as it fills, or the app just resembles the scaffold. Before you stop, RENDER the app and LOOK at it: start it (webjs dev / start), open the routes you changed in a browser, and PLAY THROUGH every state (fill the board, win, draw, reload). Confirm (1) nothing collapses or resizes, cells stay equal; (2) the design is the app's OWN (layout, palette, typography, chrome), not the scaffold shell or its default colors; (3) it looks correct in light AND dark. See the webjs-design-review skill and CONVENTIONS item 6. If you already rendered and verified it this turn, say so in your final message. Disable this backstop with WEBJS_NO_DESIGN_STOP=1." + +jq -n --arg r "$reason" '{decision: "block", reason: $r}' 2>/dev/null \ + || printf '{"decision":"block","reason":%s}\n' "$(printf '%s' "$reason" | jq -Rs . 2>/dev/null || echo '""')" + +exit 0 diff --git a/packages/cli/templates/.claude/hooks/route-skills.sh b/packages/cli/templates/.claude/hooks/route-skills.sh new file mode 100644 index 000000000..880b18df8 --- /dev/null +++ b/packages/cli/templates/.claude/hooks/route-skills.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# +# UserPromptSubmit hook: route a UI-building prompt to the design-review +# skill, so it is never silently skipped. +# +# Why this exists: a Skill is model-invoked, so it fires only when the model +# judges the prompt to match, and that judgement is exactly what fails for +# design work ("build a tic-tac-toe app" reads as backend/logic work and the +# render-and-look step gets skipped, shipping a collapsed or scaffold-looking +# UI). A hook is deterministic: it runs on every prompt, decides from the +# prompt TEXT, and injects a directive the model reads before acting. It +# cannot invoke the Skill itself (the harness forbids that); the strongest +# lever is UserPromptSubmit additionalContext. +# +# Output contract: print one JSON object with +# hookSpecificOutput.additionalContext and exit 0. Never block (exit 2 would +# erase the prompt); routing informs, it does not gate. + +set -euo pipefail +payload=$(cat) +prompt=$(printf '%s' "$payload" | jq -r '.prompt // empty' 2>/dev/null || true) +[ -z "$prompt" ] && exit 0 +lc=$(printf '%s' "$prompt" | tr '[:upper:]' '[:lower:]') +has() { printf '%s' "$lc" | grep -Eq "$1"; } + +# UI / app-building intent: any request to build/create/change something the +# user will SEE. Broad on purpose (a false positive just reminds you to look). +if has '(build|create|make|add|design|redesign|style|implement|scaffold).{0,40}(app|page|layout|component|screen|view|board|form|dashboard|ui|site|game|list|table|card|nav|header|footer|modal|button|theme)' \ + || has '(make|help me|let'\''s).{0,20}(look|prettier|beautiful|nicer|design)' \ + || has '(tic.?tac.?toe|todo|blog|dashboard|landing|storefront|kanban|chat)'; then + ctx="ROUTING: this prompt involves UI work. Invoke the webjs-design-review skill (Skill tool) as part of this task: after building/changing any page, layout, or component and BEFORE reporting the work done, render the app in a real browser and LOOK at every state, confirming the app owns its design (layout + palette + type, not the scaffold), nothing collapses or resizes, cells stay even, and light + dark both read. A design/layout defect has NO failing test, so the render-and-look is the only check that catches it." + jq -n --arg c "$ctx" '{hookSpecificOutput: {hookEventName: "UserPromptSubmit", additionalContext: $c}}' +fi + +exit 0 diff --git a/packages/cli/templates/.claude/settings.json b/packages/cli/templates/.claude/settings.json index 6f71bc42e..b3c1890f8 100644 --- a/packages/cli/templates/.claude/settings.json +++ b/packages/cli/templates/.claude/settings.json @@ -1,5 +1,15 @@ { "hooks": { + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": ".claude/hooks/route-skills.sh" + } + ] + } + ], "PreToolUse": [ { "matcher": "Write|Edit|MultiEdit", @@ -73,6 +83,10 @@ { "type": "command", "command": ".claude/hooks/commit-before-stop.sh" + }, + { + "type": "command", + "command": ".claude/hooks/design-review-before-stop.sh" } ] } diff --git a/packages/cli/templates/.claude/skills/webjs-design-review/SKILL.md b/packages/cli/templates/.claude/skills/webjs-design-review/SKILL.md new file mode 100644 index 000000000..4688e8853 --- /dev/null +++ b/packages/cli/templates/.claude/skills/webjs-design-review/SKILL.md @@ -0,0 +1,77 @@ +--- +name: webjs-design-review +description: >- + Render-and-look review for ANY UI work in a WebJs app. Invoke after building + or changing a page, layout, or component, and before you report the work done. + Triggers: "build", "create", "add a page", "component", "layout", "style", + "design", "UI", "screen", "board", "form", "dashboard", "make it look". +--- + +# Render the app and LOOK before you call UI work done + +You write CSS blind. You never see the pixels, so a whole class of defects ships +silently: `webjs check` passes, `webjs typecheck` passes, the server boots, and +the app still looks broken. A collapsed component, cells of unequal size, a +layout that resizes as it fills with content, text that overflows its box, an +app that just kept the scaffold's colors and chrome, none of these fail a test. +The only thing that catches them is rendering the app and looking at it. + +So for ANY work that touches a page, layout, or component, this is the loop: + +## 1. Run the app and open what you changed + +```sh +webjs dev # or: webjs start, for the production render +``` + +Open every route you touched in a real browser. Use the browser MCP +(`mcp__playwright__*` or `mcp__chrome-devtools__*`) if available so you can drive +and screenshot it; otherwise open it yourself and take screenshots. + +## 2. Drive EVERY state, not just the first paint + +The first paint is the easy case. Bugs hide in the states you reach by +interacting. Play the app the way a user will: + +- A game board: play a full game. Fill it. Win. Draw. Reset. Watch whether the + board or its cells change size as marks appear (they must NOT). +- A list: empty, one item, many items, an item long enough to wrap. +- A form: empty, invalid, submitted, error returned, success. +- Anything async: loading, loaded, error, refetch. + +Reload each state. Resize the window narrow (mobile) and wide. + +## 3. Confirm the three things a test can't + +Look at each state and confirm, with your eyes: + +1. **Nothing collapses, overflows, or resizes.** A container is the size it + should be (not 0-height, not collapsed to its content when it should fill). + Grid/flex children that should be equal ARE equal, and STAY equal as content + changes. Text stays inside its box. +2. **The design is this app's OWN.** Not the scaffold shell, not its default + color tokens. The palette (real `oklch`/hex values, not just shadcn token + NAMES), the typography, the layout, and the chrome are chosen for THIS app. + "It still looks like the starter" is a defect to fix, not ship. +3. **Light AND dark both look right.** Toggle the theme. Check contrast, that + nothing disappears against its background, that borders and shadows read. + +## 4. Iterate until it holds, then say what you saw + +If any of the above is wrong, fix it and re-render. Do not stop on the first +render. When it holds, state in your final message WHAT you rendered and WHAT +you confirmed (which states, light + dark), so the review is on the record. + +--- + +**Why this is a skill and not just a test:** a real-browser test +(`webjs test --browser`) catches the mechanical failures (collapse, uneven +cells, reflow) and you SHOULD ship one. There is no framework helper for this; a +layout-stability check is a few lines you write against your own component (in a +`*/test/**/browser/*.test.js`): measure `getBoundingClientRect()` on the grid +children and assert they stay equal-sized before AND after a move, so a collapse +or reflow FAILS the test. But "looks like the scaffold", "the +palette is bland", "the spacing is off", "it's ugly in dark mode" are judgment +calls no assertion makes for you. That is what this human-in-the-loop look is +for. Do both: the test for the mechanical floor, the look for everything above +it. See CONVENTIONS item 6 and `agent-docs/styling.md`. diff --git a/packages/cli/templates/.cursorrules b/packages/cli/templates/.cursorrules index 330f4e135..754538eb0 100644 --- a/packages/cli/templates/.cursorrules +++ b/packages/cli/templates/.cursorrules @@ -48,21 +48,39 @@ cover what you need, the full hosted docs are at **https://docs.webjs.dev**. `lib/utils/`, feature logic in `modules/`. - **Use a unique design, and redesign means more than recolor (UI apps).** Give the app a design of its own (palette, typography, LAYOUT, and chrome) chosen - from what the app IS. Recoloring the scaffold and swapping the logo while - keeping its skeleton (a fixed top header with a Home link and a theme toggle, - the centered ~760px reading column, the "Built with webjs" footer) is NOT a - unique design. Decide from scratch whether this app even needs a header or - footer, what nav (if any), and what layout fits (a centered board, a - full-bleed dashboard, a split, a single card). The scaffold ships a - `webjs-scaffold-placeholder` marker on its footer, so `webjs check` fails until - you remove or replace the "Built with webjs" branding. Self-audit before - finishing: nothing should read as the scaffold example (no "Built with webjs" - footer, no leftover example nav, no default reading column unless it truly - fits). Keep only the design TOKENS and theme wiring in `app/layout.ts` - (infrastructure the ui kit reads) and restyle on top. Style with Tailwind - utilities wherever they reach, and use custom CSS only for what utilities - cannot express (@theme tokens, @keyframes, scrollbar, complex color-mix or - gradients). The `api` template has no UI, so this does not apply there. + from what the app IS. `app/layout.ts` ships as a MINIMAL shell (theme, design + tokens, and Tailwind infra, then `${children}` in a bare padded container) with + NO header, nav, footer, or reading column: design the app's own chrome from + scratch. Decide whether it needs a header at all, a nav (or none), a footer, a + sidebar, a centered reading column, or a full-bleed canvas, from what fits the + app. `LAYOUT-REFERENCE.md` at the project root is a complete worked layout to + learn the patterns from, then build your own. Two `webjs-scaffold-placeholder` + markers gate `webjs check`: the minimal shell ("design your layout from + scratch") and the palette block ("own the colors"), so check fails until each + is addressed. Keep the design TOKENS and theme wiring in `app/layout.ts` + (infrastructure the ui kit reads) and set the token VALUES to your own palette; + run `webjs check --clear-placeholders` to keep the starter palette + deliberately. Style with Tailwind utilities wherever they reach, and use custom + CSS only for what utilities cannot express (@theme tokens, @keyframes, + scrollbar, complex color-mix or gradients). The `api` template has no UI, so + this does not apply there. +- **Render the app and LOOK before you call UI work done (every agent, not just one harness).** + You write CSS blind, so a layout or design defect ships silently: `webjs check` + and `webjs typecheck` pass even when a component collapses, grid cells are + uneven, the layout resizes as it fills, or the app just kept the scaffold's + colors. Static tools give no failure signal for this. The only thing that + catches it is rendering the app and looking at the pixels. So for ANY page, + layout, or component work: run it (`webjs dev`), open every route you changed in + a real browser (drive it with your harness's browser tool or MCP if it has one, + otherwise open it yourself and screenshot), and PLAY THROUGH every state (empty, + filled, win, draw, reload, narrow and wide, light and dark). Confirm nothing + collapses or reflows, that cells stay equal, that the design is the app's OWN, + and that both themes read. Ship a real-browser test (`webjs test --browser`) for + the mechanical floor (measure `getBoundingClientRect()` and assert cells stay + equal across a move). Fix and re-render until it holds, then state in your final + message what you rendered and confirmed. Claude Code additionally ENFORCES this + via the `webjs-design-review` skill plus a Stop hook, but the discipline is + harness-agnostic and this rule is the source of truth for every agent. - **Only three templates exist:** `webjs create ` (default full-stack), `--template api`, `--template saas`. The CLI rejects any other `--template` value. Pick: diff --git a/packages/cli/templates/.github/copilot-instructions.md b/packages/cli/templates/.github/copilot-instructions.md index 805567ff1..30dd18a87 100644 --- a/packages/cli/templates/.github/copilot-instructions.md +++ b/packages/cli/templates/.github/copilot-instructions.md @@ -48,21 +48,39 @@ the full hosted docs are at **https://docs.webjs.dev**. `lib/utils/`, feature logic in `modules/`. - **Use a unique design, and redesign means more than recolor (UI apps).** Give the app a design of its own (palette, typography, LAYOUT, and chrome) chosen - from what the app IS. Recoloring the scaffold and swapping the logo while - keeping its skeleton (a fixed top header with a Home link and a theme toggle, - the centered ~760px reading column, the "Built with webjs" footer) is NOT a - unique design. Decide from scratch whether this app even needs a header or - footer, what nav (if any), and what layout fits (a centered board, a - full-bleed dashboard, a split, a single card). The scaffold ships a - `webjs-scaffold-placeholder` marker on its footer, so `webjs check` fails until - you remove or replace the "Built with webjs" branding. Self-audit before - finishing: nothing should read as the scaffold example (no "Built with webjs" - footer, no leftover example nav, no default reading column unless it truly - fits). Keep only the design TOKENS and theme wiring in `app/layout.ts` - (infrastructure the ui kit reads) and restyle on top. Style with Tailwind - utilities wherever they reach, and use custom CSS only for what utilities - cannot express (@theme tokens, @keyframes, scrollbar, complex color-mix or - gradients). The `api` template has no UI, so this does not apply there. + from what the app IS. `app/layout.ts` ships as a MINIMAL shell (theme, design + tokens, and Tailwind infra, then `${children}` in a bare padded container) with + NO header, nav, footer, or reading column: design the app's own chrome from + scratch. Decide whether it needs a header at all, a nav (or none), a footer, a + sidebar, a centered reading column, or a full-bleed canvas, from what fits the + app. `LAYOUT-REFERENCE.md` at the project root is a complete worked layout to + learn the patterns from, then build your own. Two `webjs-scaffold-placeholder` + markers gate `webjs check`: the minimal shell ("design your layout from + scratch") and the palette block ("own the colors"), so check fails until each + is addressed. Keep the design TOKENS and theme wiring in `app/layout.ts` + (infrastructure the ui kit reads) and set the token VALUES to your own palette; + run `webjs check --clear-placeholders` to keep the starter palette + deliberately. Style with Tailwind utilities wherever they reach, and use custom + CSS only for what utilities cannot express (@theme tokens, @keyframes, + scrollbar, complex color-mix or gradients). The `api` template has no UI, so + this does not apply there. +- **Render the app and LOOK before you call UI work done (every agent, not just one harness).** + You write CSS blind, so a layout or design defect ships silently: `webjs check` + and `webjs typecheck` pass even when a component collapses, grid cells are + uneven, the layout resizes as it fills, or the app just kept the scaffold's + colors. Static tools give no failure signal for this. The only thing that + catches it is rendering the app and looking at the pixels. So for ANY page, + layout, or component work: run it (`webjs dev`), open every route you changed in + a real browser (drive it with your harness's browser tool or MCP if it has one, + otherwise open it yourself and screenshot), and PLAY THROUGH every state (empty, + filled, win, draw, reload, narrow and wide, light and dark). Confirm nothing + collapses or reflows, that cells stay equal, that the design is the app's OWN, + and that both themes read. Ship a real-browser test (`webjs test --browser`) for + the mechanical floor (measure `getBoundingClientRect()` and assert cells stay + equal across a move). Fix and re-render until it holds, then state in your final + message what you rendered and confirmed. Claude Code additionally ENFORCES this + via the `webjs-design-review` skill plus a Stop hook, but the discipline is + harness-agnostic and this rule is the source of truth for every agent. - **Only three templates exist:** `webjs create ` (default full-stack), `--template api`, `--template saas`. The CLI rejects any other `--template` value. Pick: diff --git a/packages/cli/templates/AGENTS.md b/packages/cli/templates/AGENTS.md index ac5070cdb..fc91eecde 100644 --- a/packages/cli/templates/AGENTS.md +++ b/packages/cli/templates/AGENTS.md @@ -14,34 +14,51 @@ now (`app/page.ts` printing "Hello from {{APP_NAME}}", the example `User` model in `db/schema.server.ts`, the `theme-toggle` component, the example users module in api/saas templates) are **starting-point references, not the final product**. Your job is to replace them with -the app the user actually asked for. That includes adapting -`app/layout.ts`, not just the page. Set the real brand, replace the -example `Home` nav, and pick a content-width container that fits. The -default `
` is a reading column for prose and -forms, so for a full-bleed app, dashboard, or board, widen the cap or -remove it (keep the theme tokens). A wide layout left in the 760px -reading column overflows into a horizontal scrollbar. **Give the app a -unique design, and redesign means more than recolor.** When it has a UI, -choose its palette, typography, LAYOUT, and chrome from what the app IS. -Recoloring the scaffold and swapping the logo while keeping its skeleton (a -fixed top header with a Home link and a theme toggle, the centered ~760px -reading column, the "Built with webjs" footer) is NOT a unique design. -Decide from scratch whether this app even needs a header or footer, what nav -(if any), and what layout fits (a centered board, a full-bleed dashboard, a -split, a single card). Before finishing, self-audit that nothing still reads -as the scaffold example (no "Built with webjs" footer, no leftover example -nav, no default reading column unless it truly fits). The `api` template has -no UI, so this does not apply there. The design tokens and theme wiring are -infrastructure to keep and restyle on top of. Style with Tailwind utilities -wherever they reach, and use custom CSS only for what utilities cannot -express (@theme tokens, @keyframes, scrollbar, complex color-mix or -gradients). This is ENFORCED: +the app the user actually asked for. That includes designing +`app/layout.ts`, not just the page. It ships as a MINIMAL shell (theme, +design tokens, and Tailwind infra, then `${children}` in a bare padded +container) with NO header, nav, footer, or reading column, so design the +app's own chrome from scratch. `LAYOUT-REFERENCE.md` at the project root is +a complete worked layout (fixed header, brand, nav, theme toggle, reading +column, footer) to learn the patterns from, then build your own. **Give the +app a unique design, and redesign means more than recolor.** When it has a +UI, choose its palette, typography, LAYOUT, and chrome from what the app IS. +Decide whether it needs a header at all, a nav (or none), a footer, a +sidebar, a centered reading column, or a full-bleed canvas (a centered +board, a full-bleed dashboard, a split, a single card). The `api` template +has no UI, so this does not apply there. The design tokens and theme wiring +are infrastructure to keep and set your own palette VALUES on. Style with +Tailwind utilities wherever they reach, and use custom CSS only for what +utilities cannot express (@theme tokens, @keyframes, scrollbar, complex +color-mix or gradients). This is ENFORCED: the example `app/page.ts` and `app/layout.ts` carry a `webjs-scaffold-placeholder` marker comment, and `webjs check` fails while any marker remains, so this freshly scaffolded app fails the check until you replace the example content (or deliberately keep it) and -delete the marker line. The delivered app must contain only what the -user asked for, never leftover scaffold code. +delete the marker line. To keep the gallery and clear every marker at +once, run `webjs check --clear-placeholders` (it strips the marker lines +and keeps the demo code), then delete any demo you do not want. The +delivered app must contain only what the user asked for, never leftover +scaffold code. + +**Render the app and LOOK before you call UI work done (every agent, not +just one harness).** You write CSS blind, so a layout or design defect +ships silently: `webjs check` and `webjs typecheck` pass even when a +component collapses, grid cells are uneven, the layout resizes as it fills, +or the app just kept the scaffold's colors. Static tools give no failure +signal for this. The only thing that catches it is rendering the app and +looking at the pixels. So for ANY page, layout, or component work: run it +(`webjs dev`), open every route you changed in a real browser (drive it +with your harness's browser tool or MCP if it has one, otherwise open it +yourself and screenshot), and PLAY THROUGH every state (empty, filled, win, +draw, reload, narrow and wide, light and dark). Confirm nothing collapses +or reflows, that cells stay equal, that the design is the app's OWN, and +that both themes read. Ship a real-browser test (`webjs test --browser`) +for the mechanical floor (measure `getBoundingClientRect()` and assert +cells stay equal across a move). Fix and re-render until it holds, then +state what you rendered and confirmed. Claude Code additionally ENFORCES +this via the `webjs-design-review` skill plus a Stop hook, but the +discipline is harness-agnostic and applies to every agent. **Non-negotiables for every webjs app:** diff --git a/packages/cli/templates/CONVENTIONS.md b/packages/cli/templates/CONVENTIONS.md index 3001b5c19..16c76deb4 100644 --- a/packages/cli/templates/CONVENTIONS.md +++ b/packages/cli/templates/CONVENTIONS.md @@ -365,32 +365,54 @@ When the user asks the agent to build their actual app: `app/features/` demos and the `app/examples/` app the real app uses, delete the rest (route + module + any table), and remove their links from `app/page.ts`. -5. **Adapt `app/layout.ts` to the app, not just the page.** Set the real - brand, replace the example `Home` nav with the app's navigation, and - pick a content-width container that fits. The default - `
` is a reading column for prose, forms, - and marketing. Widen it or drop the cap for a full-bleed app, - dashboard, or board, or a wide layout overflows into an unnecessary - horizontal scrollbar. Keep the design tokens and theme setup, those - are infrastructure. +5. **Design the layout in `app/layout.ts` (it ships MINIMAL on purpose).** + The root layout wires the theme, design tokens, and Tailwind (keep all of + that), then drops `${children}` into a bare padded `
` with NO chrome. + There is no header, nav, footer, or reading column to inherit: design the + app's own from what the app IS. Decide from scratch whether it needs a + header at all, a nav (or none), a footer, a sidebar, a centered reading + column, or a full-bleed canvas. `LAYOUT-REFERENCE.md` at the project root is + a complete worked layout (fixed header, brand, nav, theme toggle, reading + column, footer) to learn the patterns from, then build your own. Keep the + design tokens and theme apparatus, those are infrastructure. 6. **Use a unique design, and redesign means more than recolor (UI apps).** Give the app a design of its own (palette, typography, LAYOUT, spacing, - and chrome) chosen from what the app IS. Recoloring the scaffold and - swapping the logo while keeping its skeleton (a fixed top header with a - Home link and a theme toggle, the centered ~760px reading column, the - "Built with webjs" footer) is NOT a unique design. Decide from scratch - whether this app even needs a header or footer, what nav (if any), and - what layout fits (a centered board, a full-bleed dashboard, a split, a - single card). The scaffold ships a `webjs-scaffold-placeholder` marker on - its footer, so `webjs check` fails until you remove or replace the - "Built with webjs" branding. Self-audit before finishing: nothing should - read as the scaffold example (no "Built with webjs" footer, no leftover - example nav, no default reading column unless it truly fits). The design - tokens and theme wiring in `app/layout.ts` are infrastructure to keep and - restyle on top of, not the example look to preserve. Style with Tailwind - utilities wherever they reach, and use custom CSS only for what utilities - cannot express (@theme tokens, @keyframes, scrollbar, complex color-mix - or gradients). The `api` template has no UI, so this does not apply there. + and chrome) chosen from what the app IS. The layout ships MINIMAL (see item + 5), so there is no scaffold skeleton to inherit: build the chrome the app + actually needs. Two things are gated by a `webjs-scaffold-placeholder` + marker, so `webjs check` fails until each is addressed: the minimal shell + carries a "design your layout from scratch" marker (delete it once you have + built a real layout), and the palette block carries its own marker (the + starter orange looks finished on purpose). The design token NAMES and theme + wiring (`--background`, `--primary`, `--card`, ... in `app/layout.ts`) are + infrastructure to keep, but their COLOR VALUES are yours: set a distinctive + palette that fits the app, in both the light and dark blocks. Keeping the + scaffold's token colors (or a light warm recolor of them) is NOT owning the + palette. To keep the starter palette deliberately, run + `webjs check --clear-placeholders`. Style with Tailwind utilities wherever + they reach, and use custom CSS only for what utilities cannot express (@theme + tokens, @keyframes, scrollbar, complex color-mix or gradients). The `api` + template has no UI, so this does not apply there. + **Size the component HOST, not just an inner wrapper.** A component's host + custom element is the box its parent lays out. Hosts default to + `display: block`, but a host that is a flex/grid item in a centering parent + (`flex justify-center`, `grid place-items-center`) is still sized to its + content unless it carries width itself. Put `w-full max-w-[...]` on the host, + not only on an inner `
` (an inner `w-full` resolves against a collapsed + host and the whole component renders tiny). If a board or card renders small + despite `w-full max-w-[400px]` on its inner grid, move that sizing to the host. + **Definition of done (design gate):** a UI app is NOT finished until you + have (a) given it a design of its own (layout AND palette) and removed the + scaffold shell, and (b) run it and PLAYED THROUGH every state in a browser + (fill the board, win, draw, reload), confirming nothing resizes or shifts as + it fills (even, stable squares) and it does not resemble the scaffold. A + glance at the empty first paint is not enough; the layout bugs show up + mid-interaction. + `webjs doctor` emits an advisory when `app/layout` still reproduces scaffold + design (the exact 760px reading column, the "Built with webjs" attribution, or + the unmodified starter palette values); the kept theme apparatus (theme-toggle, + `--header-h`) is infrastructure and does NOT trip it. Treat the advisory as a + to-do, not noise. 7. **Keep:** the Drizzle setup, the test config, the agent config files (`AGENTS.md`, `CONVENTIONS.md`, `CLAUDE.md`, `.cursorrules`, etc.), `db/connection.server.ts` + `db/columns.server.ts`, the directory @@ -406,7 +428,9 @@ freshly scaffolded app fails `webjs check` until you address each placeholder. The marker is acknowledge-and-remove: replace the example content, or deliberately keep it, and in either case delete the marker line. So the delivered app contains only what the user asked for, never -leftover scaffold code. +leftover scaffold code. To keep the gallery and clear every marker in one +step (instead of one edit per file), run `webjs check --clear-placeholders`, +then delete whichever demo routes/modules you do not want. The scaffold exists so the agent doesn't reinvent the directory layout, the Drizzle wiring, the test runner config, or the convention files. It diff --git a/packages/cli/templates/LAYOUT-REFERENCE.md b/packages/cli/templates/LAYOUT-REFERENCE.md new file mode 100644 index 000000000..eece77e71 --- /dev/null +++ b/packages/cli/templates/LAYOUT-REFERENCE.md @@ -0,0 +1,94 @@ +# Layout reference + +`app/layout.ts` ships as a **minimal shell**: it wires the theme, design tokens, +and the Tailwind runtime, then renders `${children}` in a bare full-height +container with no chrome. That is on purpose. A delivered app should design its +own layout from what the app IS, not inherit a generic header and footer. + +This file is the **reference** for how to build a real layout: read it to learn +the patterns (a fixed header, a brand mark, a nav, a theme toggle, a reading +column, a footer), then write the layout your app actually needs in +`app/layout.ts`. Decide from scratch: does a tic-tac-toe game want a header at +all? Does a dashboard want a sidebar instead? Does a landing page want a +full-bleed hero? Keep only what fits. + +> **This is ONE example, not a template to reproduce.** Reproducing this exact +> header (a slim bar with a mark on the left and a theme toggle on the right) +> just recreates the old scaffold look under a new name. It is here to show the +> mechanics (how a header, nav, theme toggle, or footer are wired), not the +> design. Design a layout that fits what THIS app is: a game might be a +> full-bleed centered stage with no header; a tool might have a compact command +> bar; a reader might have a wide sidebar. Take the mechanics, invent the form. + +You do not import from this file. Copy the mechanics you want into +`app/layout.ts`'s returned template, inside the `
` (or replacing it), and +restyle them into your own design. + +## A complete worked layout + +This is the chrome the scaffold used to ship inline. It goes in the body of +`RootLayout`, after the `