@webjsdev/mcp 0.1.18 → 0.1.19

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.
@@ -1,231 +0,0 @@
1
- # TypeScript
2
-
3
- ## What This Covers
4
-
5
- - TypeScript at runtime with **no build step**: `.ts` / `.mts` is stripped in place, not compiled.
6
- - **Erasable syntax only** (`erasableSyntaxOnly: true`) and the exact list of banned constructs, with their allowed rewrites.
7
- - The **pluggable stripper** (Node 24+ built-in vs `amaro` on Bun) and how the browser gets stripped source.
8
- - **Full-stack type safety**: the rule (derive the type at every boundary, never `unknown` / `any`), server-action types flowing to the call site, and the `import type` carrier rule across the `.server` boundary.
9
- - **Typing pages, layouts, and route handlers** with `PageProps` / `LayoutProps` / `RouteHandlerContext` and the generated route union (`npx webjsdev types`).
10
-
11
- Read this when you are writing `.ts` in a WebJs app, hit a strip-time 500, or want typed params and hrefs. For action signatures and the serializer wire see `data-and-actions.md`. For typing reactive props see `components.md`.
12
-
13
- TypeScript is optional. JS + JSDoc gets the same call-site safety (the language server reads `@typedef` / `@param` / `@returns` identically). Add `"checkJs": true` to enforce it.
14
-
15
- ## No build step: how `.ts` runs
16
-
17
- `.ts` works everywhere `.js` does, same routing conventions, same server-action behaviour. There is no user-visible `tsc` run and no build output.
18
-
19
- - **Server-side** `.ts` imports are stripped by the runtime automatically (Node exposes `process.features.typescript === 'strip'`, Bun runs `.ts` natively).
20
- - **Browser-bound** `.ts` requests go through the pluggable stripper on the dev server, which does whitespace replacement (every source position maps to the same output position, so stack traces stay byte-exact with no sourcemap shipped). Cached by mtime.
21
-
22
- The stripper backs onto **Node 24+'s built-in `module.stripTypeScriptTypes`** (itself SWC's WASM transform in strip-only mode) or, on **Bun**, `amaro` loaded directly (byte-identical output). Force one with `WEBJS_TS_STRIPPER=builtin|amaro`.
23
-
24
- ## Erasable syntax only
25
-
26
- The stripper supports **erasable TypeScript only**: type annotations, `interface`, `type`, `declare`, generics, `import type`, `as` casts, and `satisfies`. Non-erasable syntax is rejected at strip time (a 500 naming the file), so set `erasableSyntaxOnly: true` in `tsconfig.json` to catch it as an editor squiggle first. `webjs check`'s `erasable-typescript-only` rule verifies the flag is set.
27
-
28
- Banned constructs and their erasable rewrites:
29
-
30
- ```ts
31
- // BANNED (rejected at compile + runtime)
32
- enum Color { Red, Green, Blue }
33
- class Foo { constructor(public x: number) {} } // parameter property
34
- namespace Util { export const helper = 1; } // value namespace
35
- import fs = require('fs'); // import = require
36
- @legacyDecorator class C {} // legacy decorator + emitDecoratorMetadata
37
-
38
- // ALLOWED (canonical erasable forms)
39
- const Color = { Red: 'Red', Green: 'Green', Blue: 'Blue' } as const;
40
- type Color = typeof Color[keyof typeof Color];
41
-
42
- class Foo {
43
- x: number;
44
- constructor(x: number) { this.x = x; }
45
- }
46
-
47
- const Util = { helper: 1 };
48
-
49
- import * as fs from 'fs';
50
- ```
51
-
52
- A third-party `.ts` dependency shipping non-erasable syntax fails the same way (rare, most npm packages publish compiled `.js`). WebJs is buildless end-to-end with no bundler fallback, so keep `erasableSyntaxOnly` on and your own code never hits it.
53
-
54
- Prefer explicit `.ts` extensions in imports. A `.js` specifier pointing at a `.ts` sibling also resolves in the dev server, but explicit `.ts` is clearer.
55
-
56
- ## Minimum `tsconfig.json`
57
-
58
- ```json
59
- {
60
- "compilerOptions": {
61
- "target": "ES2022",
62
- "module": "NodeNext",
63
- "moduleResolution": "NodeNext",
64
- "lib": ["ES2022", "DOM", "DOM.Iterable"],
65
- "types": ["node"],
66
- "strict": true,
67
- "noEmit": true,
68
- "allowImportingTsExtensions": true,
69
- "skipLibCheck": true,
70
- "erasableSyntaxOnly": true
71
- },
72
- "include": [
73
- "app/**/*",
74
- "components/**/*",
75
- "modules/**/*",
76
- "lib/**/*",
77
- "test/**/*",
78
- "middleware.js",
79
- "middleware.ts",
80
- ".webjs/routes.d.ts"
81
- ],
82
- "exclude": ["node_modules", ".webjs/vendor", "db/migrations"]
83
- }
84
- ```
85
-
86
- `erasableSyntaxOnly: true` is the non-negotiable line. It aligns the compiler's accepted syntax with the stripper's, so violations surface as diagnostics instead of a runtime 500.
87
-
88
- `test/**/*` is in the `include` on purpose (#1299), the way Next / Remix / Astro's generated configs cover the whole tree. Leave it there. A test file outside the `include` is a file `webjs typecheck` never opens, so an implicitly-`any` parameter or a wrong argument shape in a test survives until somebody reads the line, which is exactly how one reached review here. Do not add a second `tsconfig.test.json` either: a config nobody remembers to run reproduces the same gap in a new place.
89
-
90
- Note what is absent: `checkJs`, the flag mentioned at the top of this file for a JSDoc-typed codebase (it implies `allowJs`, so it is the only one you add). Turning it on makes `tsc` read your `.js` files, which is the point, but it also pulls in browser tests written as `.js`. Those run in a real browser through web-test-runner, so their test globals are not in scope for `tsc` and each one reports a `Cannot find name 'test'`. Turn it on deliberately, and give the browser tests a `types` entry or their own exclude when you do.
91
-
92
- ## Full-stack type safety
93
-
94
- ### The rule: derive the type, never `unknown` or `any`
95
-
96
- Every value crossing an app boundary in WebJs already has a type you can reach, so reach for it. Writing `unknown` or `any` at a boundary throws away the framework's central guarantee, and it does so silently: the app still runs, the checker just stops helping. Nothing catches this for you. Both are valid TypeScript, so `webjs check` will not flag them (it is a correctness tool, and this is a convention), and `tsc --noEmit` passes happily.
97
-
98
- `unknown` is the more dangerous of the two, because it reads as the safe choice. It is safe only in the sense that it forces a narrow at the read site. As a boundary type it is exactly as uninformative as `any`, and it pushes a cast into every consumer.
99
-
100
- The ladder, in the order to climb it:
101
-
102
- | Boundary | Write this | Not this |
103
- |---|---|---|
104
- | A database row | `export type Todo = typeof todos.$inferSelect` in `db/schema.server.ts` (`$inferInsert` for a write) | a hand-written interface that drifts from the schema |
105
- | That row inside a shipping component | `import type { Todo } from '#db/schema.server.ts'` | `any[]`, or re-declaring the shape by hand |
106
- | A server action's input | a named `interface CreatePostInput` | `input: unknown` / `input: any` |
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. 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
- | A page | `PageProps<'/blog/[slug]'>` | `{ params: Record<string, any> }` |
110
- | A layout | `LayoutProps` (whose `children` is a `TemplateResult`) | `{ children: unknown }` |
111
- | A route handler's 2nd argument | `RouteHandlerContext<'/api/users/[id]'>` | `{ params: any }` |
112
- | A client-router href | the generated `Route` union (`npx webjsdev types`; `npm run dev` also emits it) | a bare `string` |
113
- | A reactive property | `prop<Student>(Object)`, `prop<Tag[]>(Array)` | `prop(Object)` plus a cast at every read |
114
- | An optimistic temp row | the pending shape on the row type (`pending?: boolean`) | `as any` on the temp id |
115
-
116
- One flow, end to end, with no escape hatch anywhere in it:
117
-
118
- ```ts
119
- // db/schema.server.ts
120
- export const posts = table('posts', { id: uuidPk(), title: text().notNull(), body: text().notNull() });
121
- export type Post = typeof posts.$inferSelect; // derived, never hand-written
122
-
123
- // modules/posts/actions/create-post.server.ts
124
- 'use server';
125
- import type { Post } from '#db/schema.server.ts'; // type-only: erased before the browser sees it
126
- export interface CreatePostInput { title: string; body: string }
127
- export const validate = (input: unknown) => { /* narrows, returns data satisfying CreatePostInput */ };
128
- export async function createPost(input: CreatePostInput): Promise<ActionResult<Post>> { /* ... */ }
129
-
130
- // modules/posts/components/new-post.ts
131
- import { createPost } from '#modules/posts/actions/create-post.server.ts';
132
- const r = await createPost({ title, body });
133
- if (r.success && r.data) r.data.title; // Post.title: string, checked at the call site
134
- ```
135
-
136
- The payoff is not stylistic. A typo in `r.data.titel`, a renamed column, a changed action signature, and a page reading `params.slugg` are all compile errors in that version and all silent runtime `undefined` in the `unknown` version.
137
-
138
- ### Where `unknown` is still the right type
139
-
140
- There are two cases, and only the first is about narrowing.
141
-
142
- **Case one: a value that genuinely has no type yet, at the moment before it is narrowed.**
143
-
144
- ```ts
145
- // app/api/webhook/route.ts
146
- export async function POST(req: Request) {
147
- const body: unknown = await req.json(); // correct: nothing has vouched for this yet
148
- const parsed = parseWebhook(body); // narrowed on the very next line
149
- return Response.json({ ok: parsed.kind });
150
- }
151
- ```
152
-
153
- The test is what the next line does. Correct `unknown` here is narrowed immediately by a parse, a validator, or a type guard, and the narrowed type is what the rest of the function sees. Anything standing at that boundary qualifies: a `route.ts` handler's `await req.json()` (above), a `catch (e)` binding (already `unknown` under `strict`), an action's `export const validate`, and any validator function those delegate to. In a validator, returning `data` that `satisfies` the action's input type is what carries a real type into the action body. See `data-and-actions.md`.
154
-
155
- **Case two: a parameter of YOUR OWN helper that forwards its argument into an `html` template hole, which is NOT narrowed.** A hole renders a string, a number, a `TemplateResult`, an array of those, a directive result, or nothing, so a helper like `lede(content: unknown)` is correctly typed and `TemplateResult` alone would be too narrow. Narrow it only when the helper genuinely accepts one shape (`backLink(href: string, ...)`).
156
-
157
- This case is about a value YOU accept, never one the framework hands you. A layout's `children` also ends up in a hole, but the framework already types it (`LayoutProps.children` is a `TemplateResult`), so `{ children: unknown }` is a discarded type, not this carve-out.
158
-
159
- Everywhere else `unknown` is a missing type, not a safe one: surviving into a return type, a component prop, a layout's `children`, or an action signature is the shape to fix.
160
-
161
- `any` gets no carve-out at all in app code. It does not defer checking, it disables it, so a validator typed `(input: any)` un-types everything downstream of the call.
162
-
163
- ### Server actions type-check automatically
164
-
165
- Calling a server action from a client component resolves at type-check time to the action's real source file. The runtime stub swap is invisible to the checker, and the RPC serializer makes runtime match the types (`Date` stays `Date`, `Map` stays `Map`, `BigInt` stays `BigInt`; see `data-and-actions.md` for the full supported set).
166
-
167
- ```ts
168
- // modules/posts/actions/create-post.server.ts
169
- export async function createPost(
170
- input: { title: string; body: string },
171
- ): Promise<ActionResult<PostFormatted>> { /* ... */ }
172
-
173
- // modules/posts/components/new-post.ts
174
- import { createPost } from '#modules/posts/actions/create-post.server.ts';
175
- const r = await createPost({ title, body });
176
- // ^ Promise<ActionResult<PostFormatted>>
177
- if (r.success) r.data.title; // PostFormatted.title: string
178
- ```
179
-
180
- Class instances arrive as plain objects (prototypes and methods lost, matching React Server Actions). The opt-in `SerializableActionFn` annotation turns that silent loss into a compile error (`Serializable<T>` / `SerializableArgs` / `SerializableResult` are also exported, all types-only).
181
-
182
- ### The carrier rule: `import type` across the `.server` boundary
183
-
184
- A `.server.ts` file WITHOUT `'use server'` is a server-only utility whose browser stub throws at load. But a **type-only** `import type { Row } from '#db/schema.server.ts'` is safe: the stripper erases it before it can reach the browser, so sharing a derived row type from a `.server.ts` into a shipping component is fine and is not flagged. A **value** import of that same file into a shipping module is the crash the `no-server-import-in-browser-module` check catches. So carry TYPES over the boundary with `import type`, and carry DATA over it through a `'use server'` action (whose RPC stub loads safely client-side).
185
-
186
- ### Typed page / layout / route-handler props
187
-
188
- Type each routing entry with the exported helpers so a param typo is a compile error.
189
-
190
- ```ts
191
- import type { PageProps, LayoutProps, RouteHandlerContext } from '@webjsdev/core';
192
-
193
- // Static route: params is Record<string, string>.
194
- export default function About({ searchParams }: PageProps) { /* ... */ }
195
-
196
- // Dynamic route: pass the route literal to narrow params.
197
- export default function Post({ params }: PageProps<'/blog/[slug]'>) {
198
- const slug = params.slug; // typed string
199
- }
200
-
201
- // Layout adds children.
202
- export default function RootLayout({ children }: LayoutProps) { /* ... */ }
203
-
204
- // Route handler's 2nd arg.
205
- export async function GET(req: Request, ctx: RouteHandlerContext) {
206
- return Response.json({ id: ctx.params.id });
207
- }
208
- ```
209
-
210
- With no route literal (or before you generate route types), `params` is `Record<string, string>`, the runtime default. With `R` set to a generated dynamic route, `params` narrows to its exact shape (`{ slug: string }`, `{ rest: string[] }`, `{ slug?: string[] }`). These are pure types, erased at runtime.
211
-
212
- Type page metadata with the exported `Metadata` type (and `MetadataContext` for the `generateMetadata` argument), the same ergonomics as Next.js's `import type { Metadata } from 'next'`.
213
-
214
- ### The generated route union (`npx webjsdev types`)
215
-
216
- Run `npx webjsdev types` to write `.webjs/routes.d.ts`, an opt-in overlay augmenting `@webjsdev/core` with one key per route in `app/`. It narrows two things at tsserver time:
217
-
218
- - The `Route` href type: `navigate('/blog/anything')` passes, `navigate('/nonexistent')` is an error. Until you generate the types, `Route` is `string` (unconstrained, non-breaking for JSDoc and un-generated apps).
219
- - Per-route `params`: `PageProps<'/blog/[slug]'>['params']` becomes `{ slug: string }`.
220
-
221
- ```sh
222
- npx webjsdev types # writes .webjs/routes.d.ts (route count printed)
223
- ```
224
-
225
- `npm run dev` emits it at startup and re-emits after each route rebuild, so the editor always has fresh types. The file is gitignored (regenerated per machine, like Next's `.next/types`); the scaffold `tsconfig.json` already lists it in `include`. To opt in for an existing app, run `npx webjsdev types` once and add `.webjs/routes.d.ts` to `include`. This is the WebJs no-build equivalent of Next 15's `typedRoutes`, achieved via interface declaration-merging rather than a bundler.
226
-
227
- ### The `webjs` config block and auth user
228
-
229
- The `webjs` object in `package.json` has two typed references so a typo'd key is diagnosed instead of dropped: a JSON Schema (VS Code flags an unknown key while you edit) and the `WebjsConfig` type from `@webjsdev/core`. Type `auth()`'s session user by augmenting the `AuthUser` interface (types every `auth()` call) or by parameterising `createAuth<AppUser>(...)` (types one instance), both types-only. Un-augmented, `user` resolves to `Record<string, unknown>`.
230
-
231
- Both `@webjsdev/core` and `@webjsdev/server` ship hand-authored `.d.ts` overlays with a `types` export condition, so a `strict` + `nodenext` app resolves real types for either import with no TS7016 error. The runtime stays plain `.js` + JSDoc; the overlays cost nothing at runtime.
@@ -1,161 +0,0 @@
1
- # The `@webjsdev/ui` component kit
2
-
3
- Load this when the app uses `@webjsdev/ui` (a `components.json` is present), OR
4
- when you are about to add a UI primitive (button, card, input, badge) to a fresh
5
- app that has not initialised the kit yet: running `npx webjsdev ui init` then
6
- `npx webjsdev ui add <name>` is HOW the kit comes to exist, and it is the default for a
7
- repeated primitive over hand-writing one from scratch. `@webjsdev/ui` is the shadcn-style
8
- kit for WebJs. The source is copied into your repo (`components/ui/`), so you own
9
- and edit it exactly as freely as code you wrote yourself, and can add, remove,
10
- restructure, or theme it however your app needs. Two tiers:
11
-
12
- - **Tier 1, class helpers (23 components).** Pure functions returning Tailwind
13
- class strings (`buttonClass({ variant })`, `cardClass()`), composed with
14
- whatever native element you write. Reach for these instead of expanding
15
- Tailwind by hand: the call site is a fraction of the tokens and the class list
16
- cannot drift.
17
- - **Tier 2, stateful custom elements (9 components).** `<ui-dialog>`, `<ui-tabs>`,
18
- `<ui-dropdown-menu>`, and friends own their ARIA (focus trap, roving tabindex,
19
- `aria-controls` / `inert`, live regions). Write the tag and the accessible
20
- behaviour comes with it. Do NOT hand-roll these; the wiring is easy to get
21
- subtly wrong.
22
-
23
- ## The workflow: query for the structure, do not guess it
24
-
25
- `add` copies a Tier-1 component's class helpers plus a lean header (what each
26
- helper is, the accessibility obligations) and a one-line pointer. It does NOT
27
- copy the worked structural example, because that example is guidance you consume
28
- once while composing, not code that should sit in your repo. Get the full
29
- paste-ready structure on demand:
30
-
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, 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.
36
- - **CLI**: `npx webjsdev ui list` (inventory), `npx webjsdev ui view <name>` (the projected
37
- view plus the full source). Same data as the MCP tool (one shared projector).
38
-
39
- So the loop is: `add` the component, then query `ui <name>` (MCP) or
40
- `npx webjsdev ui view <name>` for the accessible structure, paste it, and fill it in.
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
-
75
- ## Setup and resolution
76
-
77
- - `npx webjsdev ui init` writes `components.json`, `lib/utils/cn.ts`, and the CSS design
78
- tokens the helpers render against (`--background`, `--foreground`,
79
- `--destructive`, ...). It HARD-FAILS if the tokens cannot be written, so a
80
- clean exit means the kit is styled. Re-running it is safe on an existing
81
- project: it keeps the aliases, stylesheet path, and base color already in
82
- `components.json` and leaves an edited `cn.ts` / `dom.ts` alone, so it only
83
- fills in what is missing (`--overwrite` resets them instead). `add` self-heals the tokens if they go
84
- missing.
85
- - Resolution is LOCAL-FIRST: `init` / `add` / `list` / `view` read the registry
86
- that ships inside the installed `@webjsdev/ui`, with no network. This pins you
87
- to the installed version; run `npx webjsdev ui diff` to see where your local copies
88
- drift from the upstream (that command alone compares against the live registry).
89
- - `npx webjsdev ui lint` is an OPT-IN design-system linter over the app's own
90
- source. It reads the Tailwind classes in `html` templates, `cn()` calls and
91
- `class=${...}` holes and reports, at the line, a raw palette colour where the
92
- theme declares a role token (`no-raw-colors`, with a message naming only the
93
- `--color-*` tokens the configured `tailwind.css` actually declares), an
94
- arbitrary value such as `p-[13px]` (`no-arbitrary-values`; an arbitrary
95
- VARIANT like `[&_svg]:size-4` never fires), and a class composed over a kit
96
- helper (`no-restyle`, naming the helper's real variants and sizes read from
97
- the app's copied `components/ui/*.ts`). It is off until `components.json`
98
- carries a `lint` block, and with no block it reports nothing and exits 0.
99
- `components/ui/**` is skipped by default (a copied primitive owns structural
100
- values no variant expresses), and `allow` uses shadcn's category taxonomy
101
- (`layout`, `color`, `typography`, `spacing`, `shape`, `effects`, `motion`) or
102
- a class-group id such as `rounded`. `--json` emits `{ violations, summary }`
103
- for an agent loop; `--max-warnings <n>` pins a count.
104
-
105
- ```json
106
- "lint": {
107
- "rules": {
108
- "no-raw-colors": "warn",
109
- "no-arbitrary-values": { "severity": "warn", "allow": ["layout"] },
110
- "no-restyle": { "severity": "error", "allow": ["layout", "rounded"] }
111
- }
112
- }
113
- ```
114
-
115
- ## Inventory (run `npx webjsdev ui list` or the MCP `ui` tool for the authoritative, current set)
116
-
117
- **Tier 1 (class helpers):** accordion, alert, aspect-ratio, avatar, badge,
118
- breadcrumb, button, card, checkbox, collapsible, empty, field, input, kbd,
119
- label, native-select, pagination, popover, progress, radio-group, separator,
120
- skeleton, spinner, switch, table, textarea.
121
-
122
- **Tier 2 (custom elements, own their ARIA):** alert-dialog, dialog,
123
- dropdown-menu, hover-card, sonner, tabs, tooltip, plus toggle and toggle-group
124
- (these two register an element AND export a `*Class` helper).
125
-
126
- ## Idioms
127
-
128
- - A helper is a function, so compose it: `class=${buttonClass({ variant: 'outline' })}`.
129
- The unquoted `${...}` is a normal `html` attribute hole.
130
- - The three states a screen owes its user each have a primitive, so do not hand-roll
131
- them. A list or table with no rows renders `empty` (title as a real heading, a
132
- description saying what to do next, and the create action). A form field is
133
- `field` (`fieldClass()` holding a `<label for>`, the control, `fieldDescriptionClass()`
134
- text, and on a validation error `data-invalid="true"`, `aria-invalid` on the control,
135
- and a `fieldErrorClass()` message with `role="alert"` whose id the control's
136
- `aria-describedby` lists, with the typed value kept). A pending save puts
137
- `spinner({ decorative: true })` in a `disabled` + `aria-busy="true"` submit button
138
- that keeps its text. The `fieldClass` / `fieldLabelClass` in `components/ui/field.ts`
139
- are not the same-named rhythm helpers in `lib/utils/cn.ts`; import each from its own
140
- module and alias one if a file needs both.
141
- - Tier-1 helpers assume the design tokens exist; if a component paints unstyled,
142
- the tokens are missing (re-run `npx webjsdev ui init` or let `add` self-heal them).
143
- - Custom elements are display-only-safe at SSR and hydrate in the browser, the
144
- standard WebJs component model (`references/components.md`).
145
- - A registry module should do no work at module scope, because the elision
146
- analyser reads a module-scope call or a `document` reference as client work
147
- and then the page that imports it ships whole (#1320). `cn` itself is clean,
148
- so importing it never pins a page. Six modules still trip the analyser and DO
149
- pin an importing page: `checkbox`, `radio-group`, `pagination`, `progress`,
150
- `sonner`, `tabs`. The first two inject a stylesheet for real; the other four
151
- are an analyser precision gap (an arrow with an expression body puts its call
152
- at brace depth 0). Either way the page ships, so treat the list as fact rather
153
- than as a technicality. Keep your own copies clean when you edit them, and run
154
- `npx webjsdev elision`, which names the blocker whenever a page ships.
155
- - `native-select`'s `<option>` colours ride the design tokens, not the module.
156
- An app with no theme block gets the browser default `<option>` colours along
157
- with everything else unstyled, fixed the same way (re-run `init`, or let `add`
158
- plant the block). An app whose block predates the rule keeps the default until
159
- the rule is added by hand, because `init` never rewrites an existing block.
160
-
161
- Full per-package reference lives in the installed `@webjsdev/ui/AGENTS.md`.