Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
ded50cf
feat: add a json<T>() column helper to the scaffold db seam
vivek7405 Jul 12, 2026
c8b2b7a
fix: guide past the db generate TTY dead-end on a table rename
vivek7405 Jul 12, 2026
8367777
feat: cut the scaffold placeholder-teardown burden
vivek7405 Jul 12, 2026
9eefd65
fix: clear whole multi-line markers and stop misdiagnosing db generat…
vivek7405 Jul 12, 2026
b747c1d
fix: bound placeholder clear to the marker, keep adjacent doc comments
vivek7405 Jul 12, 2026
0a00f62
fix: key the db generate hint on stderr, not the exit code
vivek7405 Jul 12, 2026
4cc15fe
fix: read captured db generate stderr on close, not exit
vivek7405 Jul 12, 2026
30b00f4
feat: add no-missing-local-import check rule
vivek7405 Jul 12, 2026
98c8314
fix: eliminate three no-missing-local-import false positives
vivek7405 Jul 12, 2026
b9c1228
fix: collect generator exports and keep generic annotations checkable
vivek7405 Jul 12, 2026
a417caa
fix: two more no-missing-local-import false positives (typed no-init …
vivek7405 Jul 12, 2026
d43b26c
feat: keep the design bar from being lost when the scaffold shell is …
vivek7405 Jul 12, 2026
b38e881
fix: do not print the design reminder on the api template
vivek7405 Jul 12, 2026
f3037ed
fix: tighten the attribution tell and cover the api-quiet paths
vivek7405 Jul 12, 2026
aead0cf
docs: sharpen the design bar (own the token COLORS, exercise every st…
vivek7405 Jul 12, 2026
2b2b171
feat: default component hosts to display:block (light and shadow)
vivek7405 Jul 12, 2026
9558cc0
feat: doctor design advisory also flags a kept scaffold palette
vivek7405 Jul 12, 2026
5a37a7b
docs: add a real-browser layout-stability test recipe (even/stable ce…
vivek7405 Jul 12, 2026
fc51d0b
feat: scaffold a render-and-look design-review skill + hooks for UI work
vivek7405 Jul 12, 2026
bdc869e
docs: document the display:block host default across doc surfaces
vivek7405 Jul 12, 2026
2184225
test: update top-level SSR/e2e host snapshots for the data-wj-host ma…
vivek7405 Jul 12, 2026
673e816
test: tolerate the data-wj-host marker in the import-only elision sna…
vivek7405 Jul 12, 2026
40af257
test: tolerate the data-wj-host marker in the nested-DSD e2e assertion
vivek7405 Jul 12, 2026
d899b4e
feat: guard the scaffold palette with a placeholder marker so check f…
vivek7405 Jul 12, 2026
5d0cb1a
feat: mark the scaffold brand + nav as placeholders so check forces o…
vivek7405 Jul 12, 2026
147eb13
feat: ship app/layout.ts as a minimal shell, move the rich layout to …
vivek7405 Jul 12, 2026
80fe626
docs: sync the per-agent scaffold rules to the minimal-shell layout m…
vivek7405 Jul 12, 2026
62c6d42
fix: layer the host display:block rule so Tailwind flex/grid utilitie…
vivek7405 Jul 12, 2026
2c8f83f
docs: guide sizing the host + treating LAYOUT-REFERENCE as an example…
vivek7405 Jul 12, 2026
549e549
fix: scope host display:block to light DOM + add a [hidden] carve-out
vivek7405 Jul 12, 2026
1a7ec1a
fix: doctor design advisory no longer nags on kept theme apparatus (d…
vivek7405 Jul 12, 2026
461514f
docs: fix stale layout comments + guard against a multi-line block-co…
vivek7405 Jul 12, 2026
202c8ed
fix: exclude hidden=until-found from the host carve-out; clear residu…
vivek7405 Jul 12, 2026
19735a1
fix: mark the light host on the SSR error path too (uniform display:b…
vivek7405 Jul 12, 2026
2d34979
docs: clarify the error-path host-marker comment (idempotency covers …
vivek7405 Jul 12, 2026
e344786
docs: add the size-the-host guidance to components.md (completes #935…
vivek7405 Jul 12, 2026
c089b24
docs: document the light-DOM host display:block default in lit-gotcha…
vivek7405 Jul 12, 2026
4b9c560
fix: scaffold design-review skill referenced a non-existent test helper
vivek7405 Jul 12, 2026
ab816e8
fix: make the render-and-look design review harness-agnostic
vivek7405 Jul 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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\`<h3>${u.name}</h3>\`; }`. 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 `<slot>`, `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 `<webjs-suspense .fallback=${html\`…\`}>` 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; `<slot>` 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 `<style>` or `<script>` body** (`html\`<style>${css}</style>\``): 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 `<style>`. Install the `webjs` VSCode extension (`packages/editors/vscode`, VS Marketplace + Open VSX; also covers Cursor / Antigravity / Windsurf) or `webjs.nvim` (`packages/editors/nvim`, via lazy.nvim) for template highlighting + editor intelligence with no Lit plugin, or add the standalone `@webjsdev/intellisense` to `tsconfig.json` `plugins` manually (JetBrains). Full deep-dive in `agent-docs/components.md` + `agent-docs/lit-muscle-memory-gotchas.md`.
**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; `<slot>` works in either. A light-DOM component authoring custom CSS MUST prefix every class selector with its tag name (invariant 7); prefer Tailwind. **Light-DOM component hosts default to `display: block`**: a custom element is `display: inline` in plain CSS, so the framework marks every LIGHT-DOM host `data-wj-host` and injects one head rule in a low-priority cascade layer, `@layer webjs-host { :where([data-wj-host]) { display: block } }`, so a container component does not collapse; the layer keeps it overridable by any author style INCLUDING Tailwind utilities (`class="flex"`/`grid`/`hidden` win, because their layer is ordered after `webjs-host`), a `[hidden]` carve-out keeps `?hidden` working, and an inline light component opts out with `my-tag { display: inline }`. **Shadow-DOM hosts are NOT marked** (a document rule would override the shadow tree's `:host`), so a shadow component sets its host display via `:host { display: block }` in `static styles` (respected because the host is unmarked; set it for a shadow block container). See the even-grid / no-reflow layout recipes in `agent-docs/styling.md`. **Never interpolate into a component's `<style>` or `<script>` body** (`html\`<style>${css}</style>\``): 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 `<style>`. Install the `webjs` VSCode extension (`packages/editors/vscode`, VS Marketplace + Open VSX; also covers Cursor / Antigravity / Windsurf) or `webjs.nvim` (`packages/editors/nvim`, via lazy.nvim) for template highlighting + editor intelligence with no Lit plugin, or add the standalone `@webjsdev/intellisense` to `tsconfig.json` `plugins` manually (JetBrains). Full deep-dive in `agent-docs/components.md` + `agent-docs/lit-muscle-memory-gotchas.md`.

---

Expand Down Expand Up @@ -448,7 +448,7 @@ const result = await optimistic(liked, true, () => likePost(postId));

Three scaffolds exist (do not invent template names): `webjs create <name>` (full-stack: layout, page, components, modules, Drizzle+SQLite), `webjs create <name> --template api` (backend-only routes + modules + Drizzle, no SSR), `webjs create <name> --template saas` (auth + login/signup + protected dashboard + User model). The `--db sqlite|postgres` flag (default sqlite) picks the dialect; the schema/queries/actions are identical across dialects (see #563). The `--runtime node|bun` flag (default node, #541) is ORTHOGONAL to the template and re-flavors any of the three for Bun (`bun --bun` dev/start scripts so the SERVER runs on Bun, `bun.lock`, a pure `oven/bun:1` Dockerfile (#595; safe since cli@0.10.20's npx-free `webjs db migrate` (#570) needs no Node in the image) + bun-install CI, bun-command agent docs; the test/db/check tooling stays on Node); `bun create webjs <name>` auto-detects it. Pick from the request: default for any product with UI (todo, blog, dashboard, marketplace, social, e-commerce), `api` for an HTTP/JSON API with no UI, `saas` for accounts/login/signup; default to full-stack when ambiguous.

Rules: **always scaffold via `webjs create`** (never hand-roll). **Default to a real database (Drizzle + SQLite); NEVER use JSON files, in-memory arrays, or localStorage for persistence.** Update `db/schema.server.ts` to real models FIRST, then `webjs db generate` + `webjs db migrate`, then build pages/actions/queries. **Treat the scaffold as REFERENCE, not the final product:** replace the example page / `User` model / components and adapt `app/layout.ts` (brand, nav, content width; the default `<main class="max-w-[760px]">` reading column needs widening for a full-bleed app). ENFORCED: examples carry a `webjs-scaffold-placeholder` comment and `no-scaffold-placeholder` fails until the content is replaced and the marker deleted. **The scaffold IS the primary teaching surface:** every UI scaffold (full-stack AND saas) ships a densely commented feature gallery (one WebJs concept per demo under `app/features/`, logic in `modules/`, plus a whole example app under `app/examples/`), and the `api` template ships a backend-features showcase under `app/api/features/` (the `route()` adapter + validation, rate limiting, streaming, file storage, WebSockets + broadcast) listed in the root `app/route.ts` index; read the demos to learn the idioms, then prune per-feature. Docs at https://docs.webjs.dev.
Rules: **always scaffold via `webjs create`** (never hand-roll). **Default to a real database (Drizzle + SQLite); NEVER use JSON files, in-memory arrays, or localStorage for persistence.** Update `db/schema.server.ts` to real models FIRST, then `webjs db generate` + `webjs db migrate`, then build pages/actions/queries. **Treat the scaffold as REFERENCE, not the final product:** replace the example page / `User` model / components and DESIGN `app/layout.ts` (it ships as a MINIMAL shell, theme + tokens + Tailwind infra then a bare `{children}` container with no header/nav/footer/reading column; build the app's own chrome from what the app IS, using `LAYOUT-REFERENCE.md` as the worked example). ENFORCED: examples carry a `webjs-scaffold-placeholder` comment and `no-scaffold-placeholder` fails until the content is replaced and the marker deleted. **The scaffold IS the primary teaching surface:** every UI scaffold (full-stack AND saas) ships a densely commented feature gallery (one WebJs concept per demo under `app/features/`, logic in `modules/`, plus a whole example app under `app/examples/`), and the `api` template ships a backend-features showcase under `app/api/features/` (the `route()` adapter + validation, rate limiting, streaming, file storage, WebSockets + broadcast) listed in the root `app/route.ts` index; read the demos to learn the idioms, then prune per-feature. Docs at https://docs.webjs.dev.

---

Expand All @@ -458,9 +458,9 @@ Rules: **always scaffold via `webjs create`** (never hand-roll). **Default to a
webjs dev [--port N] [--no-hot] # dev server with live reload (node --watch on Node, bun --hot on Bun). --no-hot runs in-process. Runs webjs.dev.before + webjs.dev.parallel (#550)
webjs start [--port N] # prod server; source IS the runtime, plain HTTP/1.1 (reverse-proxy for TLS + HTTP/2). Runs webjs.start.before first (#550)
webjs test [--server] [--browser] [--watch]
webjs check [--rules] [--json] # correctness validator (report-only, no autofix); --json for an agent loop
webjs check [--rules] [--json] [--clear-placeholders] # correctness validator (report-only, no autofix); --json for an agent loop; --clear-placeholders strips scaffold markers in one shot
webjs mcp # read-only MCP: routes, actions (RPC hashes), components, check
webjs doctor # project-health checklist (incl. a page/layout elision advisory, #646); non-zero exit on a hard fail
webjs doctor # project-health checklist (incl. a page/layout elision advisory + a scaffold-shell design advisory that warns when a UI app still rides the scaffold chrome); non-zero exit on a hard fail
webjs types # generate .webjs/routes.d.ts (typed Route union + per-route params, #258)
webjs typecheck [tsc args...] # the project's own tsc --noEmit
webjs create <name> [--template api|saas]
Expand Down
39 changes: 39 additions & 0 deletions agent-docs/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,45 @@ Both modes are fully SSR'd (shadow DOM via Declarative Shadow DOM, light
DOM as direct HTML with a `<!--webjs-hydrate-->` marker) and hydrate
without flash on the client.

### Light-DOM hosts are `display: block` by default

A custom element is `display: inline` in plain CSS, so a light-DOM component
used as a block container (a board, a card, a panel) would otherwise collapse
to its content size. WebJs marks every SSR'd and client-upgraded LIGHT-DOM host
with a `data-wj-host` attribute and injects ONE rule into the document head,
wrapped in a dedicated low-priority cascade layer, `@layer webjs-host {
:where([data-wj-host]) { display: block } }`, so a container component fills
its parent as expected. The layer (not the zero-specificity `:where()`) is
what keeps it overridable: any author style wins, INCLUDING a Tailwind
utility (`class="flex"`, `grid`, `hidden`) whose layer is ordered after
`webjs-host`. A bare unlayered rule was NOT enough (unlayered beats layered
regardless of specificity, so it silently overrode `class="flex"`). A same-layer
`[hidden]` carve-out keeps `[hidden]` / `?hidden=${cond}` working. If you WANT an
inline light-DOM component (a badge in flowing text), opt out with
`my-badge { display: inline }` (tag-prefixed, per the class-prefix rule below).
The marker is uniform across every light host, so it does not perturb the
display-only elision verdict (the SSR output stays byte-identical with JS on or
off).

**Shadow-DOM hosts are NOT marked.** A document-level rule targeting the host
would override the shadow tree's own `:host` display (the encapsulation-context
criterion outranks layer and specificity for normal declarations), so WebJs
leaves shadow hosts alone. A shadow component sets its host display the idiomatic
way, `:host { display: block }` (or `flex` / `grid` / `inline`) in `static
styles`, and because the framework does not mark it, that `:host` is fully
respected. Set `:host { display: block }` for a shadow component used as a block
container (an unstyled shadow host stays `display: inline`).

**Size the HOST, not just an inner wrapper.** The host custom element is the box
its parent lays out. `display: block` stops the inline-collapse, 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. So put `w-full max-w-[...]` on the host (the render root), not only on an
inner `<div>` (an inner `w-full` resolves against a collapsed host and the whole
component renders tiny). Symptom: a board or card renders small even though its
inner grid says `w-full max-w-[400px]`. See `agent-docs/styling.md` for the
even-grid / no-reflow layout recipes.

### Class-prefix rule for light-DOM components

If a light-DOM component authors its own custom CSS (a `<style>` block
Expand Down
28 changes: 28 additions & 0 deletions agent-docs/lit-muscle-memory-gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,34 @@ the styling invariant. When a utility bundle repeats across light-DOM
components, extract it into a `lib/utils/ui.ts` helper returning an
`` html`...` `` fragment, never a shared CSS class.

### 7b. Reaching for `:host { display: block }` on a light-DOM component

A custom element is `display: inline` by default, so a component used as
a block container (a board, a card, a panel) collapses to its content
size. In lit the reflex is to fix this with `:host { display: block }` in
`static styles`. That works in lit because lit is **shadow-DOM-first**, so
every component has a shadow root and `:host` always exists. A WebJs
component is **light-DOM by default**, and a light-DOM component has NO
shadow root, so **there is no `:host` to write** (a `static styles`
`:host` block without `static shadow = true` does nothing, per #7).

**The webjs-shaped fix is: nothing to do.** The framework already defaults
every LIGHT-DOM host to `display: block`, by stamping the host with a
`data-wj-host` attribute and injecting one low-priority head rule
(`@layer webjs-host { :where([data-wj-host]) { display: block } }`). It is
overridable by any author style, INCLUDING a Tailwind utility (`class="flex"`,
`grid`, `hidden` win, because their layer is ordered after `webjs-host`), and
`[hidden]` still hides. So do not reach for `:host` on a light component; just
use Tailwind on the host or let the block default stand.

For a **shadow-DOM** WebJs component (`static shadow = true`), it works
exactly like lit: the framework does NOT mark shadow hosts (a document rule
would override the shadow tree's `:host`), so you set `:host { display: block }`
in `static styles` yourself and it is fully respected. One reflex to keep for
a shadow block container: set that `:host` display (an unstyled shadow host
stays `display: inline`). See `agent-docs/components.md` and
`agent-docs/styling.md`.

### 8. `<slot>` timing differs across DOM modes

Both modes accept `<slot>` syntax in templates and provide
Expand Down
8 changes: 7 additions & 1 deletion agent-docs/orm.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ return { success: true, data: { ...formatPost(row), authorName: me.name } };
## Column helpers live in `db/columns.server.ts` (the one dialect seam)

The schema is written against helpers (`table`, `pk`, `uuidPk`, `uuid`, `bool`,
`timestamp`, `createdAt`, `updatedAt`, `index`) rather than raw drizzle
`json`, `timestamp`, `createdAt`, `updatedAt`, `index`) rather than raw drizzle
builders, so the same `db/schema.server.ts` runs on SQLite and Postgres. Only
`db/columns.server.ts` differs per dialect. Two of those helpers exist because
rc.3 has not yet exposed a stable no-arg overload.
Expand All @@ -172,6 +172,12 @@ rc.3 has not yet exposed a stable no-arg overload.
argument the runtime auto-fills. The helper synthesizes drizzle-kit's own
collision-free name, so schema authors call `index(t.createdAt)` with no name.
Replace it with a bare `index()` once 1.0 stable ships the no-arg overload.
- **`json<T>()`** persists a structured value (an array or object) as JSON,
typed by `T` so reads and writes are narrowed instead of `unknown`. It is
`text({ mode: 'json' }).$type<T>()` on SQLite and `jsonb().$type<T>()` on
Postgres, so `board: json<Cell[]>()` or `settings: json<{ theme?: string }>()`
works unchanged on both dialects. Reach for it whenever a column holds
structured data rather than a scalar (a board, a tag list, a settings blob).

When you add a table, use the helpers (`pk()` for an integer autoincrement id,
`uuidPk()` for an app-generated uuid string id, `createdAt()` for a
Expand Down
Loading
Loading