@webjsdev/mcp 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -35,6 +35,7 @@ Three seams pick a runtime-specific implementation, all inside the framework, no
35
35
  | Hot reload | restart on change (the `webjs dev` supervisor, #1521) | in place in one long-lived process (`bun --hot` resets the module registry, #1575); only an `instrumentation.*` / `env.*` edit restarts it |
36
36
  | WebSocket | the `ws` library | native `Bun.serve` + a bridge adapter |
37
37
  | 103 Early Hints | yes | no (`Bun.serve` has no informational-response API) |
38
+ | Response compression (prod) | brotli / gzip / deflate over `node:zlib`: a buffered body (an SSR page, JSON, a module) is compressed synchronously and sent with its `Content-Length`, a streamed body through a streaming compressor, a buffered body under 1 KB is sent as is, and a buffered body under 32 KB is gzip-encoded even when brotli is accepted (gzip 6 is 2 to 3x cheaper than brotli 4 at that size for a few percent more bytes, #1646) | same (both shells share the seam, the 1 KB floor and the 32 KB brotli floor) |
38
39
  | Dev edit to a page / layout | full reload (the dev restart replaces the process) | refreshes IN PLACE, no reload (#1398) |
39
40
  | Reverse-proxy headers | `X-Forwarded-Proto` / `X-Forwarded-Host` honored | same |
40
41
 
@@ -42,7 +43,7 @@ Three seams pick a runtime-specific implementation, all inside the framework, no
42
43
 
43
44
  **The in-place dev refresh (#1398) needs the server process to SURVIVE the edit,** which is the whole of the Node-versus-Bun difference in that row. A page or layout never hydrates, so a freshly rendered page is the complete truth for it and the client router can swap it in without a reload, keeping scroll and (for a page edit) the hydrated state of components outside the changed region. The server classifies the changed file and puts the verdict on the live-reload event, so this needs a process that is still alive to do the classifying.
44
45
 
45
- Bun keeps ONE dev server process for the whole session (#1575), so it gets the refresh. `bun --hot` re-runs the CLI on a change; the first run's server owns the process (its listener, live-reload stream, watchers and analysis caches) and a re-run only tells it the module registry was reset, so nothing is started twice and memory stays flat over hundreds of edits (it used to grow about 25 MB an edit until `bun --hot` stopped reloading). The app's modules load through a `Bun.plugin` that reads them fresh, and its `#` imports resolve through the app's own `imports` map, because Bun keeps the old source of a file that was replaced (an atomic save) and a stale directory listing for a new file next to a `*.server.*` module. So `bun --hot` no longer sees app edits itself, and the dev server asks it for a registry reset after an edit to a module some other module imports, a new module, or a `*.server.*` module, all without a process restart. A page, layout or route handler nothing imports needs no reset: the dev re-import is keyed by the file's content, so an unchanged file reuses its loaded module (no new module instance per request) and an edited one is a new import. `instrumentation.*` and `env.*` run once per process, so an edit to one restarts the server on both runtimes. On Node the `webjs dev` supervisor (it replaced `node --watch` in #1521) RESTARTS the server process on a change under `app`, `components`, `modules`, `lib`, or `actions`, or to a root `middleware.{ts,js,mts,mjs}`, and a fresh process holds no record of what changed, so those edits are always a full reload. Two Node cases still refresh in place: an edit OUTSIDE that watched set (`db/schema.server.ts`, a `webjs.dev.watch` content dir), and running `npm run dev -- --no-hot`, which keeps the server in one process on either runtime. An in-place refresh loads the rebuilt stylesheets (a `webjs.dev.regenerate` compile runs on that request) BEFORE it swaps the new markup in, and drops the old sheets only after, so an element that gained a utility class never paints without its rule (#1535; a full reload never had the gap, since a head stylesheet is render-blocking). A component edit is a full reload everywhere by design, because `customElements.define` is once-per-tag and swapping fresh markup onto the old class would be worse than the reload.
46
+ Bun keeps ONE dev server process for the whole session (#1575), so it gets the refresh. `bun --hot` re-runs the CLI on a change; the first run's server owns the process (its listener, live-reload stream, watchers and analysis caches) and a re-run only tells it the module registry was reset, so nothing is started twice and memory stays flat over hundreds of edits (it used to grow about 25 MB an edit until `bun --hot` stopped reloading). The app's modules load through a `Bun.plugin` that reads them fresh, and its `#` imports resolve through the app's own `imports` map, because Bun keeps the old source of a file that was replaced (an atomic save) and a stale directory listing for a new file next to a `*.server.*` module. So `bun --hot` no longer sees app edits itself, and the dev server asks it for a registry reset after an edit to a module some other module imports, a new module, or a `*.server.*` module, all without a process restart. A page, layout or route handler nothing imports needs no reset: the dev re-import is keyed by the file's content, so an unchanged file reuses its loaded module (no new module instance per request) and an edited one is a new import. `instrumentation.*` and `env.*` run once per process, so an edit to one restarts the server on both runtimes. On Node the `webjs dev` supervisor (it replaced `node --watch` in #1521) RESTARTS the server process on a change under `app`, `components`, `modules`, `lib`, `actions`, or `db` (a schema edit has to reach a fresh process, #1664; only JS and TS source under `db/` counts, so a database file and its sidecars never restart it), or to a root `middleware.{ts,js,mts,mjs}`, and a fresh process holds no record of what changed, so those edits are always a full reload. Two Node cases still refresh in place: an edit OUTSIDE that watched set (a `webjs.dev.watch` content dir), and running `npm run dev -- --no-hot`, which keeps the server in one process on either runtime. An in-place refresh loads the rebuilt stylesheets (a `webjs.dev.regenerate` compile runs on that request) BEFORE it swaps the new markup in, and drops the old sheets only after, so an element that gained a utility class never paints without its rule (#1535; a full reload never had the gap, since a head stylesheet is render-blocking). A component edit is a full reload everywhere by design, because `customElements.define` is once-per-tag and swapping fresh markup onto the old class would be worse than the reload.
46
47
 
47
48
  After a reset, app code that imports `@webjsdev/server` gets a fresh copy of the package while the first run's server keeps handling requests. Everything the two copies must agree on (the request `auth()`, `cookies()` and `headers()` read, the action signal, the seed collector and action identity, the default cache store, sessions, broadcast clients) lives in process-wide state keyed by `Symbol.for`, so a signed-in page and the `'use server'` queries it calls still see the request after any number of edits (#1590). Before that fix, `auth()` without an explicit request returned `null` in app code after the first edit.
48
49
 
@@ -97,6 +98,10 @@ Both `node:sqlite` and `bun:sqlite` default `busy_timeout` to 0, so a contended
97
98
 
98
99
  A `--db postgres` app opens one `pg` Pool in `db/connection.server.ts`. It holds no session state between queries (no `SET`, `LISTEN`, or advisory lock kept across them), so the same code works behind a transaction pooler. Idle connections close after 10 seconds, a connection attempt gives up after 5 seconds (a database that is down fails the request instead of hanging it), `DATABASE_POOL_MAX` caps the pool (default 10), and an idle connection the server drops (a restart or failover) is logged rather than crashing the process. Keep it that way: do not add `SET` statements or session-scoped features to app code.
99
100
 
101
+ ## Knowing when an edit has settled
102
+
103
+ After an edit, the dev server rebuilds and re-analyses the app, and on Node it may restart first. The reload frame a tab receives goes out BEFORE anything is re-imported, so a broken module only shows on the next request. Do not `sleep` and grep a page for "Server error". Run `npx webjs status [path...]` (#1664): it waits until the change has been rebuilt and the analysis has run again (retrying through a Node restart), requests each path (the app root when none is given) plus every page with a recorded render error, then prints every error, render, TypeScript strip, rebuild, route handler, server action, middleware, with its file, line and code frame. A render error stays listed across an unrelated rebuild until that url answers below 500 again (fixed, or deleted and now a 404), and the other kinds clear on each rebuild. A page re-requested only because of an old error fails the run on a server error, not on a 404. It exits 0 when settled and clean, 1 on any problem, 2 when no dev server answered; `--json` gives one document an agent can branch on. The same data is the dev-only `GET /__webjs/dev/status?wait=<seconds>` (`{ status, settled, ready, boot, seq, rebuilds, errors }`), which answers 404 under `npm run start`.
104
+
100
105
  ## Verifying a runtime-sensitive change
101
106
 
102
107
  Most app code needs no runtime-specific testing, because it does not touch a runtime seam. If you DO change something runtime-sensitive (the serializer, a stream, `node:crypto`, low-level request handling, anything that behaves differently under `Bun.serve` versus `node:http`), prove it on both runtimes. The Node suite is the source of truth, and an additive Bun matrix re-runs the runtime-sensitive tests under Bun. See `testing.md` for the cross-runtime matrix and the `test/bun/**` assertions.
@@ -150,9 +150,9 @@ Native `<button>` / `<a>` / `<input>` already have correct focus + keyboard beha
150
150
 
151
151
  ## Design tokens and theming
152
152
 
153
- The default stack is a static compiled Tailwind stylesheet (`css:build` compiles `public/input.css` to the linked `public/tailwind.css`, so it works with JS off) plus `@theme` design tokens declared once in the root layout. You consume them as utility classes (`bg-background`, `text-foreground`, `bg-card`, `border-border`, `font-serif`).
153
+ The default stack is a static compiled Tailwind stylesheet (the `webjs.dev.regenerate` rule compiles `public/input.css` to the linked `public/tailwind.css`, so it works with JS off; `webjs regenerate` in `dev.before` / `start.before` builds it only when stale and records the build, and `npm run css:build` is the same compile by hand, unrecorded) plus `@theme` design tokens declared once in the root layout. You consume them as utility classes (`bg-background`, `text-foreground`, `bg-card`, `border-border`, `font-serif`).
154
154
 
155
- **Two halves.** (1) `public/input.css` MAPS token names into Tailwind with `@theme inline` (`--color-background: var(--background)`), so `bg-background` resolves to `var(--background)`. That is infrastructure; leave it. (2) The root layout (`app/layout.ts`) DEFINES the values as plain CSS custom properties in a `<style>` block. That is your palette; make it your own. A freshly cleared app (after `npm run gallery:clear`) ships only the OS system-colour base (`Canvas` / `CanvasText`) with NO tokens, so building this palette is your first styling step.
155
+ **Two halves.** (1) The `@theme inline` maps (`--color-background: var(--background)`) turn token names into utilities, so `bg-background` resolves to `var(--background)`. That is infrastructure; leave it. (2) The VALUES live in `styles/globals.css`, inside the theme block `webjs ui init` owns (between `/* @webjsdev/ui theme */` and `/* @webjsdev/ui theme end */`): the light ones on `:root`, the dark ones on `.dark`, the fonts in the block's `@theme inline`. That is your palette; make it your own, by editing the block or with `npx webjsdev ui init --preset <code>` (a code from webjs.dev/ui/create), which replaces it. The scaffold writes its gallery palette there, and a cleared app keeps it. **Never redefine a theme token in the root layout**: a `<style>` there comes after the stylesheet, so it wins, and every later preset or block edit silently stops showing. Tokens your app adds itself go AFTER the block's end marker, where a preset leaves them alone.
156
156
 
157
157
  **`@theme` and `@theme inline` differ in whether the token reaches `:root`, and the difference is silent.** Measured on `tailwindcss@4.3.0`, a token mapped in a theme block is emitted as a real `:root` custom property when:
158
158
 
@@ -167,7 +167,7 @@ The one cell that bites is `inline` plus utility-only usage. Nothing on the page
167
167
 
168
168
  Whichever form you use, a token nothing references is dropped in both, so an unused mapping is dead configuration rather than a safety net.
169
169
 
170
- **Light and dark, defined once (DRY).** Write each colour token ONE time with the native CSS `light-dark(LIGHT, DARK)` function and let `color-scheme` pick the side. The default `color-scheme: light dark` follows the OS; a `[data-theme]` attribute forces one. No duplicated light/dark blocks:
170
+ **Light and dark, defined once (DRY), for tokens you write by hand.** The kit's theme block uses `:root` plus `.dark`, the shape a preset writes. For a palette you author yourself (in place of the block's values, or for your own extra tokens after it), you can instead write each colour token ONE time with the native CSS `light-dark(LIGHT, DARK)` function and let `color-scheme` pick the side. The default `color-scheme: light dark` follows the OS; a `[data-theme]` attribute forces one. No duplicated light/dark blocks:
171
171
 
172
172
  ```html
173
173
  <style>
@@ -195,7 +195,7 @@ Whichever form you use, a token nothing references is dropped in both, so an unu
195
195
 
196
196
  `light-dark()` is a native CSS function (CSS Color 5, Baseline 2024), not a library, so nothing to import. A single-theme app drops the `[data-theme]` rules and gives each token one colour.
197
197
 
198
- **A manual theme toggle** writes `data-theme` on `<html>` (`light` / `dark`, or removes it for "follow the OS"). If you use `@webjsdev/ui` components, ALSO keep the `.dark` class in sync (the ui kit keys its own tokens off `.dark`), and apply the saved choice in a tiny inline `<script>` in the layout head so there is no first-paint flash. Verify dark mode in a real browser. Light mode passing proves nothing about dark.
198
+ **A manual theme toggle** writes `data-theme` on `<html>` (`light` / `dark`, or removes it for "follow the OS"). If you use `@webjsdev/ui` components, ALSO keep the `.dark` class in sync (the ui kit keys its own tokens off `.dark`), and apply the saved choice in a tiny inline `<script>` in the layout head so there is no first-paint flash. When the palette itself lives on `.dark` (the kit's theme block, the scaffold's shape), switch `color-scheme` on the same class (`:root { color-scheme: light } :root.dark { color-scheme: dark }`, with `<meta name="color-scheme" content="light">`): only the script sets `.dark`, so with JavaScript off the page is light throughout, where `color-scheme: light dark` would draw OS-dark native controls and scrollbars over light tokens. Verify dark mode in a real browser. Light mode passing proves nothing about dark.
199
199
 
200
200
  **Edge cases.** `light-dark()` is COLOUR-only. A colour needed in just one theme sets the unused side to a no-op (`light-dark(#fff, transparent)`). A derived token that references a `light-dark()` one (like `--primary-tint` above) tracks both themes automatically. A NON-colour token that must differ per theme (a shadow's geometry, a gradient, a size, an image) cannot use `light-dark()`; give it a `:root[data-theme='dark']` override plus an `@media (prefers-color-scheme: dark) { :root:not([data-theme]) { ... } }` rule for the OS default.
201
201
 
@@ -203,6 +203,22 @@ Whichever form you use, a token nothing references is dropped in both, so an unu
203
203
 
204
204
  **Focus rings.** The design system applies ONE themed, keyboard-only focus ring globally in the theme CSS: `@layer base { * { @apply border-border outline-ring/50 } }` themes the outline COLOUR to `--ring/50`, and a `:focus-visible { outline: 2px solid color-mix(in oklab, var(--color-ring) 50%, transparent); outline-offset: 2px }` forces a SOLID outline. That second rule matters: `outline-ring/50` alone leaves `outline-style: auto`, so the browser draws its OWN focus ring (which can look thick and white and ignore the colour). So every focusable element (button, link, input) shares one `--ring`-coloured ring with no per-element styling (a native `<button>` renders it a touch wider than a link, a Chromium form-control quirk, but the colour is the same). Do NOT re-add a focus style on a light-DOM element (`buttonClass` deliberately carries none), and NEVER remove it (`outline: none` with no replacement fails WCAG 2.4.7). `:focus-visible` already limits the ring to keyboard / programmatic focus, not a mouse click. A SHADOW-DOM component is the ONE exception: a document rule cannot cross the shadow boundary, so it styles its own focus in `static styles`, matching the global ring EXACTLY (`--ring` at 50%, the same as `outline-ring/50`): `button:focus-visible { outline: 2px solid color-mix(in oklab, var(--color-ring) 50%, transparent); outline-offset: 2px }`. Without it, its controls fall back to the raw browser outline (thick, light on a dark theme, and shown on window-refocus).
205
205
 
206
+ ## A stylesheet for one section of the app
207
+
208
+ When one part of the app has its own stylesheet (an admin area, a docs section, a component gallery), link it from THAT section's layout, as the first thing the layout renders, and leave the root layout alone:
209
+
210
+ ```ts
211
+ // app/admin/layout.ts
212
+ export default function AdminLayout({ children }: LayoutProps) {
213
+ return html`
214
+ <link rel="stylesheet" href=${asset('/public/admin.css')}>
215
+ <div class="admin-shell">${children}</div>
216
+ `;
217
+ }
218
+ ```
219
+
220
+ The server moves a `<link rel="stylesheet">` at the start of any layout's or page's output into `<head>`, after the root layout's own links, so it blocks first paint like a root stylesheet does (#1669). Comments and other `<link>` tags before it are fine; any other element ends the run, so a link further down stays where it is. A second link to a file the head already links is dropped, whether its href is written relative or absolute (a link with a different `media` is a different sheet, so a print copy and a screen copy are both kept). A page with a `loading.{js,ts}` boundary is the one exception: it streams in after the head is sent, so its own leading link is not hoisted; link that page's sheet from its layout instead. The hoisted link is marked with the layout that owns it, so when a soft navigation leaves the section the client router switches the sheet off, and switches it back on when the reader returns (see `references/client-router-and-streaming.md`). Do NOT also link the section's sheet from the root layout by pathname: the root layout is not re-rendered on a soft navigation, so that copy is never switched off and outlives the section.
221
+
206
222
  ## Light-DOM host display, and shadow hosts
207
223
 
208
224
  A custom element is `display: inline` in plain CSS, which collapses a component used as a block container to its content size. WebJs marks every LIGHT-DOM host `data-wj-host` and defaults it to `display: block` via 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: any author style INCLUDING a Tailwind utility (`class="flex"`, `grid`, `hidden`) wins over it, and `[hidden]` still hides the host so `?hidden=${cond}` works. Opt into an inline light component with a tag-prefixed rule (`my-badge { display: inline }`).
@@ -249,21 +265,22 @@ Verify a layout by USING it, not by glancing at the first paint. A layout bug on
249
265
 
250
266
  A `position: sticky` header (the common `sticky top-0` pattern) flickers its background for one frame on iOS WebKit (every iOS browser uses WebKit) during a client-router forward navigation. The router's scroll-to-top after the content swap drives a sticky recompute that WebKit mis-repaints. It is iOS-only (fine on desktop and Android, invisible in DevTools emulation), and neither compositor promotion (`translateZ(0)` / `will-change`) nor changing the swap paint timing fixes it. Preserving the header across nav is correct and standard, only the `sticky` positioning is the problem.
251
267
 
252
- The fix is `position: fixed`. A fixed header is always pinned and never does the scroll-relative recompute, so the repaint bug never fires. Because fixed leaves normal flow, reserve the header height on the content below with a single `--header-h` custom property (kept exact with a `ResizeObserver`, degrading fine with no JS):
268
+ The fix is `position: fixed`. A fixed header is always pinned and never does the scroll-relative recompute, so the repaint bug never fires. Because fixed leaves normal flow, reserve the header height on the content below with a single `--header-h` custom property, and size the header BY that property:
253
269
 
254
270
  ```css
255
- :root { --header-h: 56px; } /* sane SSR first-paint default */
256
- header { position: fixed; inset-inline: 0; top: 0; }
271
+ :root { --header-h: 3.5rem; }
272
+ header { position: fixed; inset-inline: 0; top: 0; height: var(--header-h); }
257
273
  body { padding-top: var(--header-h); }
274
+ html { scroll-padding-top: var(--header-h); } /* #anchor jumps stop below the header */
258
275
  ```
259
- ```js
260
- const hdr = document.querySelector('header');
261
- const apply = () => document.documentElement.style.setProperty('--header-h', hdr.offsetHeight + 'px');
262
- apply();
263
- new ResizeObserver(apply).observe(hdr);
264
- ```
265
276
 
266
- For a dashboard, an alternative is an app-shell scroll container (a non-scrolling `100dvh` flex column with `<main>` as the internal scroller), which needs no offset but changes the scroll model.
277
+ The last line matters as soon as a page has in-page links: without it an `#anchor` jump, native or the client router's `scrollIntoView`, lands the target under the header. Next's app router reads the same `scroll-padding-top` when it scrolls a segment into view, and shadcn's site sets it from its header-height variable.
278
+
279
+ With Tailwind, that is `class="fixed inset-x-0 top-0 h-[var(--header-h)]"` on the header. The scaffold's layout already carries the token and a `body:has(> header.fixed)` rule, so pinning its navbar is adding `fixed inset-x-0 top-0 z-20` and nothing else. Pinning from a breakpoint up works the same way: `md:fixed md:inset-x-0 md:top-0` pins it on wider screens only, because `public/input.css` carries a matching rule for each default breakpoint written with Tailwind's `@variant`, so it follows the app's own `--breakpoint-*` values. A breakpoint added under a new name needs its own line beside them.
280
+
281
+ **Do not measure the header with JavaScript.** The tempting version, a `<head>` script that writes `offsetHeight` into `--header-h` from a `ResizeObserver`, breaks on a client-router swap that replaces the header (a layout refresh, a form action that re-renders the layouts): the observer fires for the detached old node, reads a height of 0, and the content slides under the navbar with no way back short of a reload. The head script never re-runs, so nothing re-measures. A token the header is sized by cannot drift, because there is nothing to keep in sync.
282
+
283
+ A header whose height changes by breakpoint sets the token per breakpoint (`@media (min-width: 768px) { :root { --header-h: 4rem } }`). A header that genuinely wraps to an unknown height belongs in an app-shell scroll container instead: a non-scrolling `100dvh` flex column with `<main>` as the internal scroller, which needs no offset at all but changes the scroll model.
267
284
 
268
285
  ### A fixed header and a modal that locks scroll
269
286
 
@@ -105,7 +105,7 @@ The ladder, in the order to climb it:
105
105
  | That row inside a shipping component | `import type { Todo } from '#db/schema.server.ts'` | `any[]`, or re-declaring the shape by hand |
106
106
  | A server action's input | a named `interface CreatePostInput` | `input: unknown` / `input: any` |
107
107
  | A server action's result | `Promise<ActionResult<Post>>` | `Promise<any>` |
108
- | `export const validate` | `(input: unknown)`, narrowed in the body, returning `data` that `satisfies` the action's input type | `(input: any)`, which un-types the returned `data` too |
108
+ | `export const validate` | `(input: unknown)`, narrowed in the body, returning `data` that `satisfies` the action's input type. On every boundary the input reads by property (`raw.title`), on a form submission as strings and `File`s | `(input: any)`, which un-types the returned `data` too |
109
109
  | A page | `PageProps<'/blog/[slug]'>` | `{ params: Record<string, any> }` |
110
110
  | A layout | `LayoutProps` (whose `children` is a `TemplateResult`) | `{ children: unknown }` |
111
111
  | A route handler's 2nd argument | `RouteHandlerContext<'/api/users/[id]'>` | `{ params: any }` |
@@ -29,15 +29,49 @@ once while composing, not code that should sit in your repo. Get the full
29
29
  paste-ready structure on demand:
30
30
 
31
31
  - **MCP `ui` tool** (preferred when available): call `ui` with no args for the
32
- kit inventory (each component's tier, helper signatures, npm deps); pass
33
- `{ name: "accordion" }` for one component's helper signatures, the paste-ready
34
- structural example, the accessibility header, and deps.
32
+ kit inventory (each component's tier, helper signatures, npm deps, plus the
33
+ block and chart names); pass `{ name: "accordion" }` for one component's
34
+ helper signatures, the paste-ready structural example, the accessibility
35
+ header, and deps.
35
36
  - **CLI**: `npx webjsdev ui list` (inventory), `npx webjsdev ui view <name>` (the projected
36
37
  view plus the full source). Same data as the MCP tool (one shared projector).
37
38
 
38
39
  So the loop is: `add` the component, then query `ui <name>` (MCP) or
39
40
  `npx webjsdev ui view <name>` for the accessible structure, paste it, and fill it in.
40
41
 
42
+ ## Blocks and charts: install a view, do not write one
43
+
44
+ **Rule: before hand-writing a login or signup page, an app sidebar shell, a
45
+ dashboard, a data table or a chart, list the blocks and install the closest
46
+ one.** A block is a ready-made view you install and then edit. Its files land
47
+ in your app as code you own, so you spend your effort on the app instead of
48
+ regenerating a page shell.
49
+
50
+ - **Find one.** The MCP `ui` tool with no args lists the block and chart NAMES
51
+ grouped by category; `{ type: "block" }` or `{ category: "login" }` adds a
52
+ one-line description each. On the CLI, `npx webjsdev ui list --type block`
53
+ (or `--category login`, and `--json` for a compact array).
54
+ - **See what it installs.** `ui { name: "login-03" }` (MCP) or
55
+ `npx webjsdev ui view login-03 --files-only` returns the description, the
56
+ components it depends on, the install command, and every file with the path
57
+ it lands at (`app/login/page.ts`, `components/login-form.ts`). Ask for the
58
+ sources only if you need them before installing (`source: true`, or `view`
59
+ without `--files-only`); installing and reading the files is cheaper.
60
+ - **Install it.** `npx webjsdev ui add login-03`. It writes each file to its
61
+ target, installs the components it uses, rewrites every import to your
62
+ aliases, and adds any design tokens it needs that your stylesheet lacks. A
63
+ file that already exists is kept unless you pass `--overwrite`, and the
64
+ command lists what it wrote.
65
+ - **Then edit it.** Rename the route, change the copy, and wire the form: a
66
+ block's form has no `action`, so bind your `'use server'` action
67
+ (`<form action=${signIn}>`) as `references/data-and-actions.md` describes.
68
+
69
+ Shipped today: shadcn's ten authentication blocks, `login-01` to `login-05`
70
+ and `signup-01` to `signup-05` (categories `authentication` plus `login` or
71
+ `signup`). A chart is a block whose categories include `chart`
72
+ (`type: "chart"` lists them). The list grows, so query it rather than trusting
73
+ this paragraph.
74
+
41
75
  ## Setup and resolution
42
76
 
43
77
  - `npx webjsdev ui init` writes `components.json`, `lib/utils/cn.ts`, and the CSS design
package/src/mcp-docs.js CHANGED
@@ -400,8 +400,9 @@ export async function initText(deps) {
400
400
  'pull the specific docs you need via the `docs` tool or the `webjs-docs://*`',
401
401
  'resources. webjs is web-components-first and looks like Lit + Rails, NOT React/Next:',
402
402
  'there is NO RSC, no server/client component split, no `use client`. Components',
403
- 'hydrate (islands); pages and layouts do NOT hydrate. Signals are the default',
404
- 'state primitive. Server-only code lives behind the `.server.{js,ts}` boundary.',
403
+ 'hydrate (islands); pages and layouts do NOT hydrate. State works as in Lit',
404
+ '(`state: true` props) alongside signals. Server-only code lives behind the',
405
+ '`.server.{js,ts}` boundary.',
405
406
  'When writing a component, read `webjs-docs://lit-muscle-memory-gotchas` first:',
406
407
  'the Lit habits that break webjs SSR/reactivity each have a webjs-shaped fix there.',
407
408
  '',
@@ -564,7 +565,8 @@ const PROMPT_BODIES = {
564
565
  ].join('\n'),
565
566
  add_component: [
566
567
  'Add an interactive WebComponent. One custom element per file; register at module',
567
- 'top level. Signals are the default state; read with `.get()` inside render().',
568
+ 'top level. Hold state in a `state: true` prop (reassign it) or a signal (read with',
569
+ '`.get()`, write with `.set()`).',
568
570
  '',
569
571
  '```ts',
570
572
  "import { WebComponent, html, signal } from '@webjsdev/core';",
package/src/mcp.js CHANGED
@@ -105,13 +105,26 @@ const DOCS_SCHEMA = {
105
105
  required: [],
106
106
  };
107
107
 
108
- /** `ui` lists the @webjsdev/ui kit, or details one component by `name`. */
108
+ /** `ui` lists the @webjsdev/ui kit, or details one component or block by `name`. */
109
109
  const UI_SCHEMA = {
110
110
  type: 'object',
111
111
  properties: {
112
112
  name: {
113
113
  type: 'string',
114
- description: 'A component name (e.g. button, accordion, dialog). Omit for the full kit inventory.',
114
+ description: 'A component (button, dialog) or block (login-01) name. Omit for the inventory.',
115
+ },
116
+ type: {
117
+ type: 'string',
118
+ enum: ['ui', 'block', 'chart', 'all'],
119
+ description: 'List one kind with one-line descriptions: ui (components), block, chart, or all.',
120
+ },
121
+ category: {
122
+ type: 'string',
123
+ description: 'List the blocks and charts in one category (login, signup, sidebar, dashboard, area, ...) with descriptions.',
124
+ },
125
+ source: {
126
+ type: 'boolean',
127
+ description: 'With a block `name`: include the file sources. Off by default, since installing the block and reading the files is cheaper.',
115
128
  },
116
129
  },
117
130
  required: [],
@@ -170,7 +183,7 @@ const TOOL_DEFS = [
170
183
  {
171
184
  name: 'ui',
172
185
  description:
173
- 'The @webjsdev/ui component kit (shadcn-style, copied into your repo). No args returns the inventory (each component: tier, helper signatures, npm deps); pass `name` for one component: helper signatures, the paste-ready structural example, the a11y/description header, and deps. Use before hand-writing UI so you reach for a helper (e.g. buttonClass()) and the accessible structure instead of expanding Tailwind blind. Reads the kit, not your app. Read-only.',
186
+ 'The @webjsdev/ui kit (shadcn-style, copied into your repo): components, plus blocks and charts (a block is a ready-made view you install and then edit: a login or signup page, a sidebar shell, a dashboard, a chart). No args returns the component inventory (tier, helper signatures, npm deps) and the block and chart NAMES grouped by category; `type` / `category` list them with one-line descriptions. Pass `name` for one component (helper signatures, the paste-ready example, the a11y header, deps) or one block (description, dependencies, the install command, and the files it writes with their targets; `source: true` adds the sources). Before hand-writing a page, a dashboard, a sidebar shell, an auth form or a chart, look for a block and install it. Reads the kit, not your app. Read-only.',
174
187
  inputSchema: UI_SCHEMA,
175
188
  },
176
189
  {
@@ -420,16 +433,6 @@ export function makeToolRunners(deps) {
420
433
  };
421
434
  }
422
435
 
423
- /**
424
- * Run the `ui` tool: the @webjsdev/ui kit inventory (no args) or one component's
425
- * projection (helper signatures + paste-ready example + a11y/description header
426
- * + deps). Projects the SHARED `@webjsdev/ui/registry/extract` leaf, the same
427
- * one `webjsui view` renders, so the CLI and MCP cannot drift (#983, the #979
428
- * shared-projector pattern). `uiDeps` is injected so tests need no real package.
429
- *
430
- * @param {{ uiInventory: Function, uiComponent: Function }} uiDeps
431
- * @param {{ name?: string }} args
432
- */
433
436
  /**
434
437
  * Resolve the `ui` tool's deps from the shared `@webjsdev/ui/registry/extract`
435
438
  * leaf, GUARDED (#983): if `@webjsdev/ui` is absent or too old to carry the
@@ -439,29 +442,70 @@ export function makeToolRunners(deps) {
439
442
  * top-level import would instead reject during bootstrap and sink the whole
440
443
  * server. `importer` is injected so the failure path is unit-testable.
441
444
  *
442
- * @param {() => Promise<{ uiInventory: Function, uiComponent: Function }>} importer
443
- * @returns {Promise<{ uiInventory: Function, uiComponent: Function }>}
445
+ * A `@webjsdev/ui` that predates blocks (no `uiBlocks` export) still serves
446
+ * the components: the block deps then answer with nothing rather than throw.
447
+ *
448
+ * @param {() => Promise<any>} importer
449
+ * @returns {Promise<UiDeps>}
444
450
  */
445
451
  export async function loadUiDeps(importer) {
446
452
  try {
447
453
  const ui = await importer();
448
- return { uiInventory: ui.uiInventory, uiComponent: ui.uiComponent };
454
+ return {
455
+ uiInventory: ui.uiInventory,
456
+ uiComponent: ui.uiComponent,
457
+ uiBlocks: ui.uiBlocks ?? (() => []),
458
+ uiBlockIndex: ui.uiBlockIndex ?? (() => ({ blocks: {}, charts: {} })),
459
+ uiBlock: ui.uiBlock ?? (() => null),
460
+ };
449
461
  } catch {
450
462
  const unavailable = () => {
451
463
  throw new Error('@webjsdev/ui is not available (install/upgrade @webjsdev/ui to use the ui tool)');
452
464
  };
453
- return { uiInventory: unavailable, uiComponent: unavailable };
465
+ return { uiInventory: unavailable, uiComponent: unavailable, uiBlocks: unavailable, uiBlockIndex: unavailable, uiBlock: unavailable };
454
466
  }
455
467
  }
456
468
 
469
+ /**
470
+ * @typedef {{
471
+ * uiInventory: Function, uiComponent: Function,
472
+ * uiBlocks: Function, uiBlockIndex: Function, uiBlock: Function,
473
+ * }} UiDeps
474
+ */
475
+
476
+ /**
477
+ * Run the `ui` tool. No args: the component inventory plus the block and chart
478
+ * names grouped by category. `type` / `category`: one compact entry per match
479
+ * with its description. A component `name`: its projection (helper signatures,
480
+ * paste-ready example, a11y header, deps). A block `name`: its description,
481
+ * dependencies, install command and files with targets (sources with
482
+ * `source: true`). Projects the SHARED `@webjsdev/ui/registry/extract` leaf,
483
+ * the same one `webjsui view` renders, so the CLI and MCP cannot drift (#983,
484
+ * the #979 shared-projector pattern). `uiDeps` is injected so tests need no
485
+ * real package.
486
+ *
487
+ * @param {UiDeps} uiDeps
488
+ * @param {{ name?: string, type?: string, category?: string, source?: boolean }} args
489
+ */
457
490
  export function runUiTool(uiDeps, args) {
458
491
  const name = typeof args.name === 'string' && args.name ? args.name : null;
459
- if (!name) return { inventory: uiDeps.uiInventory() };
460
- const component = uiDeps.uiComponent(name);
461
- if (!component) {
462
- throw new Error(`Unknown component "${String(name)}". Call ui with no args for the inventory.`);
492
+ const type = typeof args.type === 'string' && args.type ? args.type : null;
493
+ if (type && !['ui', 'block', 'chart', 'all'].includes(type)) {
494
+ throw new Error(`Unknown type "${type}". Use ui, block, chart or all.`);
463
495
  }
464
- return component;
496
+ const category = typeof args.category === 'string' && args.category ? args.category : null;
497
+ if (!name) {
498
+ // A filter lists one kind with its one-line descriptions. The bare call
499
+ // stays cheap: the component inventory plus the block and chart NAMES by
500
+ // category (D18), and a test caps its size.
501
+ if (type || category) return { items: uiDeps.uiBlocks({ type: type ?? undefined, category: category ?? undefined }) };
502
+ return { inventory: uiDeps.uiInventory(), ...uiDeps.uiBlockIndex() };
503
+ }
504
+ const component = uiDeps.uiComponent(name);
505
+ if (component) return component;
506
+ const block = uiDeps.uiBlock(name, { source: args.source === true });
507
+ if (block) return block;
508
+ throw new Error(`Unknown component or block "${String(name)}". Call ui with no args for the inventory.`);
465
509
  }
466
510
 
467
511
  /**