@webjsdev/mcp 0.1.17 → 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.
- package/README.md +12 -4
- package/package.json +3 -3
- package/resources/AGENTS.md +37 -34
- package/resources/SKILL.md +9 -8
- package/resources/corpus.json +3 -3
- package/resources/references/built-ins.md +13 -3
- package/resources/references/client-router-and-streaming.md +4 -0
- package/resources/references/components.md +50 -14
- package/resources/references/data-and-actions.md +10 -4
- package/resources/references/muscle-memory-gotchas.md +37 -16
- package/resources/references/optimistic-ui.md +5 -5
- package/resources/references/routing-and-pages.md +4 -4
- package/resources/references/runtime.md +6 -1
- package/resources/references/styling.md +31 -14
- package/resources/references/typescript.md +1 -1
- package/resources/references/ui-kit.md +37 -3
- package/src/mcp-docs.js +5 -3
- package/src/mcp.js +66 -22
|
@@ -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 (`
|
|
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)
|
|
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).**
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
33
|
-
`{ name: "accordion" }` for one component's
|
|
34
|
-
|
|
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.
|
|
404
|
-
'state
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
*
|
|
443
|
-
*
|
|
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 {
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
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
|
/**
|