@gunkin/uiify 0.0.26-alpha → 0.0.27-alpha
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 +59 -0
- package/agents/components.md +2 -0
- package/agents/index.md +1 -0
- package/dist/ai/UiDocument.d.ts +16 -0
- package/dist/ai/UiDocument.d.ts.map +1 -0
- package/dist/ai/UiDocument.js +33 -0
- package/dist/ai/UiDocument.js.map +1 -0
- package/dist/ai/catalog.generated.d.ts +2766 -0
- package/dist/ai/catalog.generated.d.ts.map +1 -0
- package/dist/ai/catalog.generated.js +214 -0
- package/dist/ai/catalog.generated.js.map +1 -0
- package/dist/ai/index.d.ts +9 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +13 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/issues.d.ts +10 -0
- package/dist/ai/issues.d.ts.map +1 -0
- package/dist/ai/issues.js +178 -0
- package/dist/ai/issues.js.map +1 -0
- package/dist/ai/layout.d.ts +26 -0
- package/dist/ai/layout.d.ts.map +1 -0
- package/dist/ai/layout.js +46 -0
- package/dist/ai/layout.js.map +1 -0
- package/dist/ai/limits.d.ts +16 -0
- package/dist/ai/limits.d.ts.map +1 -0
- package/dist/ai/limits.js +99 -0
- package/dist/ai/limits.js.map +1 -0
- package/dist/ai/load.d.ts +16 -0
- package/dist/ai/load.d.ts.map +1 -0
- package/dist/ai/load.js +69 -0
- package/dist/ai/load.js.map +1 -0
- package/dist/ai/loaders.generated.d.ts +5 -0
- package/dist/ai/loaders.generated.d.ts.map +1 -0
- package/dist/ai/loaders.generated.js +122 -0
- package/dist/ai/loaders.generated.js.map +1 -0
- package/dist/ai/rules.d.ts +11 -0
- package/dist/ai/rules.d.ts.map +1 -0
- package/dist/ai/rules.js +126 -0
- package/dist/ai/rules.js.map +1 -0
- package/dist/ai/schema.generated.d.ts +8 -0
- package/dist/ai/schema.generated.d.ts.map +1 -0
- package/dist/ai/schema.generated.js +3316 -0
- package/dist/ai/schema.generated.js.map +1 -0
- package/dist/ai/types.d.ts +33 -0
- package/dist/ai/types.d.ts.map +1 -0
- package/dist/ai/types.js +14 -0
- package/dist/ai/types.js.map +1 -0
- package/dist/ai/validate.d.ts +15 -0
- package/dist/ai/validate.d.ts.map +1 -0
- package/dist/ai/validate.js +32 -0
- package/dist/ai/validate.js.map +1 -0
- package/dist/ai/validator.generated.d.ts +13 -0
- package/dist/ai/validator.generated.d.ts.map +1 -0
- package/dist/ai/validator.generated.js +49133 -0
- package/dist/ai/validator.generated.js.map +1 -0
- package/dist/components/virtual-scroll/VirtualScroll.d.ts.map +1 -1
- package/dist/components/virtual-scroll/VirtualScroll.js +1 -1
- package/dist/components/virtual-scroll/VirtualScroll.js.map +1 -1
- package/dist/styles/index.css +1 -1
- package/package.json +10 -1
- package/skills/uiify/SKILL.md +14 -10
- package/skills/uiify/references/guides/verification.md +13 -2
package/README.md
CHANGED
|
@@ -32,6 +32,65 @@ export function SaveButton() {
|
|
|
32
32
|
Use focused `@gunkin/uiify/components/*`, `@gunkin/uiify/elements/*`, `@gunkin/uiify/core/*`, or `@gunkin/uiify/hooks` imports in
|
|
33
33
|
Server Components so client boundaries remain explicit.
|
|
34
34
|
|
|
35
|
+
## UI documents (`@gunkin/uiify/ai`)
|
|
36
|
+
|
|
37
|
+
A UI document is a JSON tree of UIify parts: `{ type, props, children }`, with no styling and no
|
|
38
|
+
code. An agent writes it in your repository, or an LLM in your product stores it in a database;
|
|
39
|
+
`UiDocument` renders it in Server Components, with SSR and hydration, and without JavaScript in
|
|
40
|
+
the browser.
|
|
41
|
+
|
|
42
|
+
Ask your agent, for example `/uiify Add a status filter above the orders table at /orders`.
|
|
43
|
+
The result is an ordinary component:
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
import { Suspense } from "react";
|
|
47
|
+
import { UiDocument, type UiNodeInput } from "@gunkin/uiify/ai";
|
|
48
|
+
|
|
49
|
+
const filter = {
|
|
50
|
+
type: "form",
|
|
51
|
+
props: { method: "get", role: "search" },
|
|
52
|
+
children: [
|
|
53
|
+
{ type: "label", props: { htmlFor: "status" }, children: ["Status"] },
|
|
54
|
+
{
|
|
55
|
+
type: "Select.Root",
|
|
56
|
+
props: { id: "status", name: "status" },
|
|
57
|
+
children: [
|
|
58
|
+
{ type: "Select.Item", props: { value: "open" }, children: ["Open"] },
|
|
59
|
+
{ type: "Select.Item", props: { value: "closed" }, children: ["Closed"] },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{ type: "Button", props: { type: "submit" }, children: ["Apply"] },
|
|
63
|
+
],
|
|
64
|
+
} satisfies UiNodeInput;
|
|
65
|
+
|
|
66
|
+
export function OrdersFilter() {
|
|
67
|
+
return (
|
|
68
|
+
<Suspense>
|
|
69
|
+
<UiDocument node={filter} />
|
|
70
|
+
</Suspense>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`satisfies UiNodeInput` makes TypeScript reject an unknown part, prop or value at the line that is
|
|
76
|
+
wrong. For a document from a database, validate it before saving; `UiDocument` validates again
|
|
77
|
+
before rendering and never renders an invalid document:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { validateDocument } from "@gunkin/uiify/ai";
|
|
81
|
+
|
|
82
|
+
const result = validateDocument(JSON.parse(body));
|
|
83
|
+
if (!result.ok) return Response.json(result.issues, { status: 422 });
|
|
84
|
+
// result.issues[0] for `variant: "primary"`:
|
|
85
|
+
// { breadcrumb: "form › Stack › Button.props.variant",
|
|
86
|
+
// message: "\"primary\" is not allowed; use one of \"secondary\", \"outline\", …" }
|
|
87
|
+
await db.pages.save(result.document);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Interactivity is native: links, `GET`/`POST` forms and invoker commands (`commandfor`). Parts
|
|
91
|
+
documents cannot use yet are listed in `uiUnavailableParts`; `uiDocumentSchema` is the JSON Schema
|
|
92
|
+
to give your own LLM.
|
|
93
|
+
|
|
35
94
|
## AI coding agents
|
|
36
95
|
|
|
37
96
|
`@gunkin/uiify` ships rules, a component index and per-component references that match the
|
package/agents/components.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
Rules and anti-patterns: [index.md](./index.md). Each entry links to a generated reference with
|
|
5
5
|
its exports, props and shapes; exact types are in the `.d.ts` file listed in the Types column.
|
|
6
6
|
|
|
7
|
+
UI documents (`@gunkin/uiify/ai`) use many of these components as parts; the part types are in `dist/ai/catalog.generated.d.ts`.
|
|
8
|
+
|
|
7
9
|
## Tiers
|
|
8
10
|
|
|
9
11
|
| Tier | Meaning | Client JavaScript | Without JavaScript |
|
package/agents/index.md
CHANGED
|
@@ -186,6 +186,7 @@ With that order in place, rules in the `app` layer win over the library:
|
|
|
186
186
|
|
|
187
187
|
- `@gunkin/uiify/components/<name>`: components (names in `components.md`)
|
|
188
188
|
- `@gunkin/uiify/elements/<name>`: native-element primitives for building your own anatomy
|
|
189
|
+
- `@gunkin/uiify/ai`: render JSON UI documents with `UiDocument` and check them with `validateDocument`; the version-matched catalog of part types and TSDoc is `dist/ai/catalog.generated.d.ts`
|
|
189
190
|
- `@gunkin/uiify/hooks`: hooks
|
|
190
191
|
- `@gunkin/uiify/core/<name>`: React-agnostic stores and render helpers
|
|
191
192
|
- `@gunkin/uiify/components/behavior.css`: required behavior CSS, no visual styles
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import type { UiDocumentProps } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Renders an untrusted UI document. It validates first and throws `UiDocumentError` instead of
|
|
5
|
+
* rendering a partial guess, then renders each part with its UIify component. It uses no state
|
|
6
|
+
* or context, so it runs in a Server Component and hydrates on the client; wrap it in `Suspense`.
|
|
7
|
+
*
|
|
8
|
+
* Documents are immutable: the first render of a `node` object validates and renders a frozen
|
|
9
|
+
* copy of it (see `loadDocument`), and later changes to that object are ignored. Pass a new
|
|
10
|
+
* object to render a changed document.
|
|
11
|
+
*/
|
|
12
|
+
export declare function UiDocument({ node }: UiDocumentProps): ReactElement;
|
|
13
|
+
export declare namespace UiDocument {
|
|
14
|
+
var displayName: string;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=UiDocument.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UiDocument.d.ts","sourceRoot":"","sources":["../../src/ai/UiDocument.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAiB,YAAY,EAAa,MAAM,OAAO,CAAC;AAIpE,OAAO,KAAK,EAAkB,eAAe,EAAE,MAAM,YAAY,CAAC;AAkClE;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,EAAE,IAAI,EAAE,EAAE,eAAe,GAAG,YAAY,CAGlE;yBAHe,UAAU"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createElement, use } from "react";
|
|
2
|
+
import { uiParts } from "./catalog.generated.js";
|
|
3
|
+
import { assertDocument, childrenOf, loadDocument } from "./load.js";
|
|
4
|
+
const loads = /* @__PURE__ */ new WeakMap();
|
|
5
|
+
function loadOnce(node) {
|
|
6
|
+
if (typeof node !== "object" || node === null) {
|
|
7
|
+
assertDocument(node);
|
|
8
|
+
return loadDocument(node);
|
|
9
|
+
}
|
|
10
|
+
let load = loads.get(node);
|
|
11
|
+
if (!load) {
|
|
12
|
+
load = loadDocument(node);
|
|
13
|
+
loads.set(node, load);
|
|
14
|
+
}
|
|
15
|
+
return load;
|
|
16
|
+
}
|
|
17
|
+
function renderNode(node, components, key) {
|
|
18
|
+
const type = uiParts[node.type].kind === "native" ? node.type : components.get(node.type);
|
|
19
|
+
if (type === void 0) throw new Error(`UI part type ${node.type} was not loaded`);
|
|
20
|
+
const children = childrenOf(node).map(
|
|
21
|
+
(child, index) => typeof child === "string" ? child : renderNode(child, components, index)
|
|
22
|
+
);
|
|
23
|
+
return createElement(type, key === void 0 ? node.props : { ...node.props, key }, ...children);
|
|
24
|
+
}
|
|
25
|
+
function UiDocument({ node }) {
|
|
26
|
+
const { root, components } = use(loadOnce(node));
|
|
27
|
+
return renderNode(root, components);
|
|
28
|
+
}
|
|
29
|
+
UiDocument.displayName = "UiDocument";
|
|
30
|
+
export {
|
|
31
|
+
UiDocument
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=UiDocument.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/ai/UiDocument.tsx"],"sourcesContent":["import { createElement, use } from \"react\";\nimport type { ComponentType, ReactElement, ReactNode } from \"react\";\nimport type { UiChild, UiNode, UiType } from \"./catalog.generated.js\";\nimport { uiParts } from \"./catalog.generated.js\";\nimport { assertDocument, childrenOf, loadDocument } from \"./load.js\";\nimport type { LoadedDocument, UiDocumentProps } from \"./types.js\";\n\n// One load per document object, so every render of the same `node` hands `use()` the same promise.\nconst loads = new WeakMap<object, Promise<LoadedDocument>>();\n\nfunction loadOnce(node: unknown): Promise<LoadedDocument> {\n if (typeof node !== \"object\" || node === null) {\n // A primitive cannot key the WeakMap and is never a valid document: throw its issues now.\n assertDocument(node);\n return loadDocument(node);\n }\n let load = loads.get(node);\n if (!load) {\n load = loadDocument(node);\n loads.set(node, load);\n }\n return load;\n}\n\nfunction renderNode(\n node: UiNode,\n // oxlint-disable-next-line typescript/no-explicit-any -- each part takes its own props\n components: ReadonlyMap<UiType, ComponentType<any>>,\n key?: number,\n): ReactElement {\n const type = uiParts[node.type].kind === \"native\" ? node.type : components.get(node.type);\n if (type === undefined) throw new Error(`UI part type ${node.type} was not loaded`);\n const children = childrenOf(node).map(\n (child: UiChild, index: number): ReactNode =>\n typeof child === \"string\" ? child : renderNode(child, components, index),\n );\n return createElement(type, key === undefined ? node.props : { ...node.props, key }, ...children);\n}\n\n/**\n * Renders an untrusted UI document. It validates first and throws `UiDocumentError` instead of\n * rendering a partial guess, then renders each part with its UIify component. It uses no state\n * or context, so it runs in a Server Component and hydrates on the client; wrap it in `Suspense`.\n *\n * Documents are immutable: the first render of a `node` object validates and renders a frozen\n * copy of it (see `loadDocument`), and later changes to that object are ignored. Pass a new\n * object to render a changed document.\n */\nexport function UiDocument({ node }: UiDocumentProps): ReactElement {\n const { root, components } = use(loadOnce(node));\n return renderNode(root, components);\n}\nUiDocument.displayName = \"UiDocument\";\n"],"mappings":"AAAA,SAAS,eAAe,WAAW;AAGnC,SAAS,eAAe;AACxB,SAAS,gBAAgB,YAAY,oBAAoB;AAIzD,MAAM,QAAQ,oBAAI,QAAyC;AAE3D,SAAS,SAAS,MAAwC;AACxD,MAAI,OAAO,SAAS,YAAY,SAAS,MAAM;AAE7C,mBAAe,IAAI;AACnB,WAAO,aAAa,IAAI;AAAA,EAC1B;AACA,MAAI,OAAO,MAAM,IAAI,IAAI;AACzB,MAAI,CAAC,MAAM;AACT,WAAO,aAAa,IAAI;AACxB,UAAM,IAAI,MAAM,IAAI;AAAA,EACtB;AACA,SAAO;AACT;AAEA,SAAS,WACP,MAEA,YACA,KACc;AACd,QAAM,OAAO,QAAQ,KAAK,IAAI,EAAE,SAAS,WAAW,KAAK,OAAO,WAAW,IAAI,KAAK,IAAI;AACxF,MAAI,SAAS,OAAW,OAAM,IAAI,MAAM,gBAAgB,KAAK,IAAI,iBAAiB;AAClF,QAAM,WAAW,WAAW,IAAI,EAAE;AAAA,IAChC,CAAC,OAAgB,UACf,OAAO,UAAU,WAAW,QAAQ,WAAW,OAAO,YAAY,KAAK;AAAA,EAC3E;AACA,SAAO,cAAc,MAAM,QAAQ,SAAY,KAAK,QAAQ,EAAE,GAAG,KAAK,OAAO,IAAI,GAAG,GAAG,QAAQ;AACjG;AAWO,SAAS,WAAW,EAAE,KAAK,GAAkC;AAClE,QAAM,EAAE,MAAM,WAAW,IAAI,IAAI,SAAS,IAAI,CAAC;AAC/C,SAAO,WAAW,MAAM,UAAU;AACpC;AACA,WAAW,cAAc;","names":[]}
|