@webjsdev/mcp 0.1.17 → 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.
- package/README.md +30 -103
- package/package.json +5 -10
- package/src/brief.js +52 -0
- package/src/catalog.js +82 -0
- package/src/mcp-init.js +139 -0
- package/src/mcp-source.js +94 -8
- package/src/mcp.js +96 -164
- package/resources/AGENTS.md +0 -701
- package/resources/SKILL.md +0 -286
- package/resources/corpus.json +0 -6
- package/resources/references/auth-and-sessions.md +0 -300
- package/resources/references/built-ins.md +0 -336
- package/resources/references/client-router-and-streaming.md +0 -313
- package/resources/references/components.md +0 -469
- package/resources/references/data-and-actions.md +0 -319
- package/resources/references/module-structure.md +0 -229
- package/resources/references/muscle-memory-gotchas.md +0 -341
- package/resources/references/optimistic-ui.md +0 -179
- package/resources/references/routing-and-pages.md +0 -323
- package/resources/references/runtime.md +0 -108
- package/resources/references/service-worker.md +0 -80
- package/resources/references/styling.md +0 -295
- package/resources/references/testing.md +0 -223
- package/resources/references/typescript.md +0 -231
- package/resources/references/ui-kit.md +0 -127
- package/src/mcp-docs.js +0 -641
|
@@ -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 | `(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,127 +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); pass
|
|
33
|
-
`{ name: "accordion" }` for one component's helper signatures, the paste-ready
|
|
34
|
-
structural example, the accessibility header, and deps.
|
|
35
|
-
- **CLI**: `npx webjsdev ui list` (inventory), `npx webjsdev ui view <name>` (the projected
|
|
36
|
-
view plus the full source). Same data as the MCP tool (one shared projector).
|
|
37
|
-
|
|
38
|
-
So the loop is: `add` the component, then query `ui <name>` (MCP) or
|
|
39
|
-
`npx webjsdev ui view <name>` for the accessible structure, paste it, and fill it in.
|
|
40
|
-
|
|
41
|
-
## Setup and resolution
|
|
42
|
-
|
|
43
|
-
- `npx webjsdev ui init` writes `components.json`, `lib/utils/cn.ts`, and the CSS design
|
|
44
|
-
tokens the helpers render against (`--background`, `--foreground`,
|
|
45
|
-
`--destructive`, ...). It HARD-FAILS if the tokens cannot be written, so a
|
|
46
|
-
clean exit means the kit is styled. Re-running it is safe on an existing
|
|
47
|
-
project: it keeps the aliases, stylesheet path, and base color already in
|
|
48
|
-
`components.json` and leaves an edited `cn.ts` / `dom.ts` alone, so it only
|
|
49
|
-
fills in what is missing (`--overwrite` resets them instead). `add` self-heals the tokens if they go
|
|
50
|
-
missing.
|
|
51
|
-
- Resolution is LOCAL-FIRST: `init` / `add` / `list` / `view` read the registry
|
|
52
|
-
that ships inside the installed `@webjsdev/ui`, with no network. This pins you
|
|
53
|
-
to the installed version; run `npx webjsdev ui diff` to see where your local copies
|
|
54
|
-
drift from the upstream (that command alone compares against the live registry).
|
|
55
|
-
- `npx webjsdev ui lint` is an OPT-IN design-system linter over the app's own
|
|
56
|
-
source. It reads the Tailwind classes in `html` templates, `cn()` calls and
|
|
57
|
-
`class=${...}` holes and reports, at the line, a raw palette colour where the
|
|
58
|
-
theme declares a role token (`no-raw-colors`, with a message naming only the
|
|
59
|
-
`--color-*` tokens the configured `tailwind.css` actually declares), an
|
|
60
|
-
arbitrary value such as `p-[13px]` (`no-arbitrary-values`; an arbitrary
|
|
61
|
-
VARIANT like `[&_svg]:size-4` never fires), and a class composed over a kit
|
|
62
|
-
helper (`no-restyle`, naming the helper's real variants and sizes read from
|
|
63
|
-
the app's copied `components/ui/*.ts`). It is off until `components.json`
|
|
64
|
-
carries a `lint` block, and with no block it reports nothing and exits 0.
|
|
65
|
-
`components/ui/**` is skipped by default (a copied primitive owns structural
|
|
66
|
-
values no variant expresses), and `allow` uses shadcn's category taxonomy
|
|
67
|
-
(`layout`, `color`, `typography`, `spacing`, `shape`, `effects`, `motion`) or
|
|
68
|
-
a class-group id such as `rounded`. `--json` emits `{ violations, summary }`
|
|
69
|
-
for an agent loop; `--max-warnings <n>` pins a count.
|
|
70
|
-
|
|
71
|
-
```json
|
|
72
|
-
"lint": {
|
|
73
|
-
"rules": {
|
|
74
|
-
"no-raw-colors": "warn",
|
|
75
|
-
"no-arbitrary-values": { "severity": "warn", "allow": ["layout"] },
|
|
76
|
-
"no-restyle": { "severity": "error", "allow": ["layout", "rounded"] }
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
## Inventory (run `npx webjsdev ui list` or the MCP `ui` tool for the authoritative, current set)
|
|
82
|
-
|
|
83
|
-
**Tier 1 (class helpers):** accordion, alert, aspect-ratio, avatar, badge,
|
|
84
|
-
breadcrumb, button, card, checkbox, collapsible, empty, field, input, kbd,
|
|
85
|
-
label, native-select, pagination, popover, progress, radio-group, separator,
|
|
86
|
-
skeleton, spinner, switch, table, textarea.
|
|
87
|
-
|
|
88
|
-
**Tier 2 (custom elements, own their ARIA):** alert-dialog, dialog,
|
|
89
|
-
dropdown-menu, hover-card, sonner, tabs, tooltip, plus toggle and toggle-group
|
|
90
|
-
(these two register an element AND export a `*Class` helper).
|
|
91
|
-
|
|
92
|
-
## Idioms
|
|
93
|
-
|
|
94
|
-
- A helper is a function, so compose it: `class=${buttonClass({ variant: 'outline' })}`.
|
|
95
|
-
The unquoted `${...}` is a normal `html` attribute hole.
|
|
96
|
-
- The three states a screen owes its user each have a primitive, so do not hand-roll
|
|
97
|
-
them. A list or table with no rows renders `empty` (title as a real heading, a
|
|
98
|
-
description saying what to do next, and the create action). A form field is
|
|
99
|
-
`field` (`fieldClass()` holding a `<label for>`, the control, `fieldDescriptionClass()`
|
|
100
|
-
text, and on a validation error `data-invalid="true"`, `aria-invalid` on the control,
|
|
101
|
-
and a `fieldErrorClass()` message with `role="alert"` whose id the control's
|
|
102
|
-
`aria-describedby` lists, with the typed value kept). A pending save puts
|
|
103
|
-
`spinner({ decorative: true })` in a `disabled` + `aria-busy="true"` submit button
|
|
104
|
-
that keeps its text. The `fieldClass` / `fieldLabelClass` in `components/ui/field.ts`
|
|
105
|
-
are not the same-named rhythm helpers in `lib/utils/cn.ts`; import each from its own
|
|
106
|
-
module and alias one if a file needs both.
|
|
107
|
-
- Tier-1 helpers assume the design tokens exist; if a component paints unstyled,
|
|
108
|
-
the tokens are missing (re-run `npx webjsdev ui init` or let `add` self-heal them).
|
|
109
|
-
- Custom elements are display-only-safe at SSR and hydrate in the browser, the
|
|
110
|
-
standard WebJs component model (`references/components.md`).
|
|
111
|
-
- A registry module should do no work at module scope, because the elision
|
|
112
|
-
analyser reads a module-scope call or a `document` reference as client work
|
|
113
|
-
and then the page that imports it ships whole (#1320). `cn` itself is clean,
|
|
114
|
-
so importing it never pins a page. Six modules still trip the analyser and DO
|
|
115
|
-
pin an importing page: `checkbox`, `radio-group`, `pagination`, `progress`,
|
|
116
|
-
`sonner`, `tabs`. The first two inject a stylesheet for real; the other four
|
|
117
|
-
are an analyser precision gap (an arrow with an expression body puts its call
|
|
118
|
-
at brace depth 0). Either way the page ships, so treat the list as fact rather
|
|
119
|
-
than as a technicality. Keep your own copies clean when you edit them, and run
|
|
120
|
-
`npx webjsdev elision`, which names the blocker whenever a page ships.
|
|
121
|
-
- `native-select`'s `<option>` colours ride the design tokens, not the module.
|
|
122
|
-
An app with no theme block gets the browser default `<option>` colours along
|
|
123
|
-
with everything else unstyled, fixed the same way (re-run `init`, or let `add`
|
|
124
|
-
plant the block). An app whose block predates the rule keeps the default until
|
|
125
|
-
the rule is added by hand, because `init` never rewrites an existing block.
|
|
126
|
-
|
|
127
|
-
Full per-package reference lives in the installed `@webjsdev/ui/AGENTS.md`.
|