hd-garden 0.3.8 → 0.3.10

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.
Files changed (95) hide show
  1. package/.agents/skills/garden-post-review/SKILL.md +2 -0
  2. package/.agents/skills/garden-ui/SKILL.md +111 -0
  3. package/.env.example +8 -0
  4. package/app/admin/page.tsx +14 -3
  5. package/app/admin/preview/[...path]/page.tsx +88 -0
  6. package/app/admin/ui/blocks/page.tsx +16 -0
  7. package/app/admin/ui/catalog/page.tsx +16 -0
  8. package/app/admin/ui/evals/page.tsx +19 -0
  9. package/app/admin/ui/page.tsx +22 -0
  10. package/app/admin-ui.css +227 -0
  11. package/app/api/admin/distribution/route.ts +4 -2
  12. package/app/api/admin/posts/source/route.ts +21 -0
  13. package/app/api/admin/preview/asset/route.ts +32 -0
  14. package/app/api/admin/thoughts/route.ts +1 -1
  15. package/app/api/admin/ui/blocks/route.ts +19 -0
  16. package/app/api/admin/ui/draft/route.ts +30 -0
  17. package/app/api/auth/github/route.ts +40 -1
  18. package/app/blog/page.tsx +6 -5
  19. package/app/columns/page.tsx +1 -1
  20. package/app/components/AdminDashboard.tsx +53 -1
  21. package/app/components/AdminUiPanels.tsx +258 -0
  22. package/app/components/AdminUiShell.tsx +56 -0
  23. package/app/components/ContentEmptyState.tsx +6 -6
  24. package/app/components/MarkdownArticle.tsx +15 -5
  25. package/app/components/UiBlock.tsx +22 -0
  26. package/app/components/UiStudio.tsx +422 -0
  27. package/app/components/ui-studio-keys.ts +3 -0
  28. package/app/globals.css +2 -0
  29. package/app/lib/admin-auth.ts +13 -1
  30. package/app/lib/content.ts +2 -1
  31. package/app/lib/fallback-site-config.ts +18 -18
  32. package/app/lib/github-content.ts +47 -4
  33. package/app/lib/repository-path.mjs +55 -0
  34. package/app/lib/ui-content.ts +79 -0
  35. package/app/lib/ui-model.ts +145 -0
  36. package/app/raw.d.ts +4 -0
  37. package/app/rss.xml/route.ts +2 -1
  38. package/app/ui-parity/page.tsx +120 -0
  39. package/bin/garden.mjs +71 -0
  40. package/eslint.config.mjs +3 -0
  41. package/package.json +15 -1
  42. package/scripts/build-content.mjs +35 -4
  43. package/scripts/build-ui-preview.mjs +29 -0
  44. package/scripts/check-post.mjs +26 -4
  45. package/scripts/ui-cli.mjs +303 -0
  46. package/site.config.yaml +19 -19
  47. package/templates/site/site.config.yaml +19 -19
  48. package/tsconfig.json +1 -1
  49. package/ui/core/apply.mjs +81 -0
  50. package/ui/core/catalog.mjs +536 -0
  51. package/ui/core/compile.mjs +63 -0
  52. package/ui/core/demos.mjs +223 -0
  53. package/ui/core/expr.mjs +310 -0
  54. package/ui/core/fence.mjs +201 -0
  55. package/ui/core/fix-prompt.mjs +35 -0
  56. package/ui/core/format.mjs +96 -0
  57. package/ui/core/index.mjs +37 -0
  58. package/ui/core/parse.mjs +229 -0
  59. package/ui/core/scan.mjs +53 -0
  60. package/ui/core/sha256.mjs +66 -0
  61. package/ui/core/starters.mjs +18 -0
  62. package/ui/core/state.mjs +259 -0
  63. package/ui/core/static.mjs +524 -0
  64. package/ui/core/transform.mjs +146 -0
  65. package/ui/core/validate.mjs +665 -0
  66. package/ui/evals/cases.yaml +155 -0
  67. package/ui/preview/dist/garden-ui.css +1182 -0
  68. package/ui/preview/dist/garden-ui.esm.js +367 -0
  69. package/ui/preview/dist/garden-ui.iife.js +367 -0
  70. package/ui/preview/index.tsx +191 -0
  71. package/ui/preview/mermaid-shim.ts +19 -0
  72. package/ui/prompt/build.mjs +24 -0
  73. package/ui/prompt/recipes/code-walkthrough.md +68 -0
  74. package/ui/prompt/recipes/compare.md +48 -0
  75. package/ui/prompt/recipes/converter.md +55 -0
  76. package/ui/prompt/recipes/data-chart.md +57 -0
  77. package/ui/prompt/recipes/inspect-grid.md +45 -0
  78. package/ui/prompt/recipes/param-bench.md +55 -0
  79. package/ui/prompt/recipes/plan.md +45 -0
  80. package/ui/prompt/recipes/stepper.md +52 -0
  81. package/ui/prompt/recipes/trace.md +62 -0
  82. package/ui/prompt/rules.md +104 -0
  83. package/ui/prompt/studio.md +20 -0
  84. package/ui/react/CatalogPage.tsx +166 -0
  85. package/ui/react/components/chart.tsx +470 -0
  86. package/ui/react/components/display.tsx +296 -0
  87. package/ui/react/components/inputs.tsx +268 -0
  88. package/ui/react/components/layout.tsx +236 -0
  89. package/ui/react/components/media.tsx +184 -0
  90. package/ui/react/index.tsx +59 -0
  91. package/ui/react/recipes.ts +25 -0
  92. package/ui/react/runtime.tsx +239 -0
  93. package/ui/react/types.ts +29 -0
  94. package/ui/react/ui.css +1182 -0
  95. package/vite.config.ts +4 -3
@@ -0,0 +1,239 @@
1
+ "use client";
2
+
3
+ import { createContext, createElement, Fragment, useCallback, useContext, useMemo, useState, type ReactNode } from "react";
4
+ import { createBudget, evaluate } from "../core/expr.mjs";
5
+ import { formatValue } from "../core/format.mjs";
6
+ import { collectDecls, computeScope, defaultStateValues, lookupFrom } from "../core/state.mjs";
7
+ import type { Env, Expr, Lookup, Prop, UiNode, UiPayload } from "./types";
8
+
9
+ type Decls = ReturnType<typeof collectDecls>;
10
+
11
+ export type Runtime = {
12
+ blockId: string;
13
+ root: UiNode;
14
+ decls: Decls;
15
+ data: Record<string, unknown>;
16
+ defaults: Record<string, unknown>;
17
+ state: Record<string, unknown>;
18
+ scope: Record<string, unknown>;
19
+ sliders: Map<string, UiNode>;
20
+ setState: (patch: Record<string, unknown>) => void;
21
+ reset: () => void;
22
+ scopeFor: (state: Record<string, unknown>) => Record<string, unknown>;
23
+ };
24
+
25
+ const RuntimeContext = createContext<Runtime | null>(null);
26
+
27
+ export function useRuntime() {
28
+ const runtime = useContext(RuntimeContext);
29
+ if (!runtime) throw new Error("Garden UI component outside UiRuntime");
30
+ return runtime;
31
+ }
32
+
33
+ const isPlainObject = (value: unknown): value is Record<string, unknown> =>
34
+ Boolean(value) && typeof value === "object" && !Array.isArray(value);
35
+
36
+ export function lookupFor(runtime: Runtime, env: Env, scope = runtime.scope): Lookup {
37
+ return lookupFrom(...env.locals, scope) as Lookup;
38
+ }
39
+
40
+ export function evalExpr(expression: Expr, lookup: Lookup): unknown {
41
+ try {
42
+ return evaluate(expression, lookup, createBudget(200000));
43
+ } catch {
44
+ return undefined;
45
+ }
46
+ }
47
+
48
+ export function propValue(runtime: Runtime, node: UiNode, name: string, env: Env, scope?: Record<string, unknown>): unknown {
49
+ const prop = node.p?.[name];
50
+ if (!prop) return undefined;
51
+ if (prop.k === "lit") return prop.v;
52
+ return evalExpr(prop.e, lookupFor(runtime, env, scope));
53
+ }
54
+
55
+ export function rowValue(runtime: Runtime, prop: Prop | undefined, row: unknown, env: Env): unknown {
56
+ if (!prop) return undefined;
57
+ if (prop.k === "lit") return prop.v;
58
+ return evalExpr(prop.e, lookupFrom(isPlainObject(row) ? row : {}, ...env.locals, runtime.scope) as Lookup);
59
+ }
60
+
61
+ export const asString = (value: unknown, fallback = ""): string =>
62
+ value === undefined || value === null ? fallback : typeof value === "string" ? value : formatValue(value);
63
+
64
+ export const asNumber = (value: unknown) => (typeof value === "number" && Number.isFinite(value) ? value : undefined);
65
+
66
+ /** If / Each expansion shared by components that count or iterate children. */
67
+ export function expandChildren(runtime: Runtime, children: UiNode[] | undefined, env: Env): Array<{ node: UiNode; env: Env }> {
68
+ const output: Array<{ node: UiNode; env: Env }> = [];
69
+ for (const child of children ?? []) {
70
+ if (child.t !== "el") continue;
71
+ if (child.n === "If") {
72
+ if (propValue(runtime, child, "test", env)) output.push(...expandChildren(runtime, child.c, env));
73
+ } else if (child.n === "Each") {
74
+ const items = propValue(runtime, child, "of", env);
75
+ if (!Array.isArray(items)) continue;
76
+ const asName = child.p?.as?.k === "lit" ? String(child.p.as.v) : "item";
77
+ const indexName = child.p?.index?.k === "lit" ? String(child.p.index.v) : null;
78
+ items.slice(0, 200).forEach((item, index) => {
79
+ const local: Record<string, unknown> = { [asName]: item };
80
+ if (indexName) local[indexName] = index;
81
+ output.push(...expandChildren(runtime, child.c, { locals: [local, ...env.locals] }));
82
+ });
83
+ } else {
84
+ output.push({ node: child, env });
85
+ }
86
+ }
87
+ return output;
88
+ }
89
+
90
+ type ComponentRenderer = (props: { node: UiNode; env: Env }) => ReactNode;
91
+ const registry = new Map<string, ComponentRenderer>();
92
+
93
+ export function registerComponents(entries: Record<string, ComponentRenderer>) {
94
+ for (const [name, component] of Object.entries(entries)) registry.set(name, component);
95
+ }
96
+
97
+ function Interpolation({ node, env }: { node: UiNode; env: Env }) {
98
+ const runtime = useRuntime();
99
+ const value = node.e ? evalExpr(node.e, lookupFor(runtime, env)) : undefined;
100
+ return <span className="garden-ui-value">{formatValue(value)}</span>;
101
+ }
102
+
103
+ const hasInterpolation = (nodes: UiNode[] | undefined): boolean =>
104
+ (nodes ?? []).some((node) => node.t === "x" || hasInterpolation(node.c));
105
+
106
+ const safeHref = (href: string) => (/^(?:https:\/\/|\.{0,2}\/|#)/u.test(href) || /^[\w\u3400-\u9fff./-]+$/u.test(href) ? href : "#");
107
+
108
+ export function RenderNode({ node, env }: { node: UiNode; env: Env }): ReactNode {
109
+ const runtime = useRuntime();
110
+ switch (node.t) {
111
+ case "text":
112
+ return node.v ?? "";
113
+ case "x":
114
+ return <Interpolation node={node} env={env} />;
115
+ case "p":
116
+ return (
117
+ <p aria-live={hasInterpolation(node.c) ? "polite" : undefined}>
118
+ <RenderNodes nodes={node.c} env={env} />
119
+ </p>
120
+ );
121
+ case "strong":
122
+ return <strong><RenderNodes nodes={node.c} env={env} /></strong>;
123
+ case "em":
124
+ return <em><RenderNodes nodes={node.c} env={env} /></em>;
125
+ case "code":
126
+ return <code>{node.v}</code>;
127
+ case "br":
128
+ return <br />;
129
+ case "hr":
130
+ return <hr />;
131
+ case "link": {
132
+ const href = safeHref(node.u ?? "");
133
+ const external = href.startsWith("https://");
134
+ return (
135
+ <a href={href} {...(external ? { target: "_blank", rel: "noopener noreferrer" } : {})}>
136
+ <RenderNodes nodes={node.c} env={env} />
137
+ </a>
138
+ );
139
+ }
140
+ case "list": {
141
+ const Tag = node.o ? "ol" : "ul";
142
+ return (
143
+ <Tag start={node.o && node.s && node.s !== 1 ? node.s : undefined}>
144
+ {(node.c ?? []).map((item, index) => (
145
+ <li key={index}><RenderNodes nodes={item.c} env={env} /></li>
146
+ ))}
147
+ </Tag>
148
+ );
149
+ }
150
+ case "quote":
151
+ return <blockquote><RenderNodes nodes={node.c} env={env} /></blockquote>;
152
+ case "el": {
153
+ if (node.n === "State" || node.n === "Let" || node.n === "Data") return null;
154
+ if (node.n === "If" || node.n === "Each") {
155
+ return (
156
+ <>
157
+ {expandChildren(runtime, [node], env).map((item, index) => (
158
+ <RenderNode key={index} node={item.node} env={item.env} />
159
+ ))}
160
+ </>
161
+ );
162
+ }
163
+ const component = node.n ? registry.get(node.n) : undefined;
164
+ if (!component) return <span className="garden-ui-unknown">预览需要更新 Garden 插件(不认识 &lt;{node.n}&gt;)</span>;
165
+ // Registry entries are module-level functions, so their identity is stable across renders.
166
+ return createElement(component, { node, env });
167
+ }
168
+ default:
169
+ return null;
170
+ }
171
+ }
172
+
173
+ export function RenderNodes({ nodes, env }: { nodes: UiNode[] | undefined; env: Env }) {
174
+ return (
175
+ <>
176
+ {(nodes ?? []).map((node, index) => (
177
+ <Fragment key={index}>
178
+ <RenderNode node={node} env={env} />
179
+ </Fragment>
180
+ ))}
181
+ </>
182
+ );
183
+ }
184
+
185
+ /** Finds the node that binds a state, so inputs and buttons can clamp values. */
186
+ function collectSliders(root: UiNode) {
187
+ const sliders = new Map<string, UiNode>();
188
+ const visit = (node: UiNode) => {
189
+ if (node.t === "el" && node.n === "Slider" && node.p?.bind?.k === "lit") sliders.set(String(node.p.bind.v), node);
190
+ node.c?.forEach(visit);
191
+ };
192
+ visit(root);
193
+ return sliders;
194
+ }
195
+
196
+ const EMPTY_DATA: Record<string, unknown> = {};
197
+
198
+ /** Keeps values the reader already chose when the block is edited, if they still fit. */
199
+ function carryOver(defaults: Record<string, unknown>, previous: Record<string, unknown>) {
200
+ const next = { ...defaults };
201
+ for (const [name, value] of Object.entries(previous)) {
202
+ if (Object.hasOwn(defaults, name) && typeof defaults[name] === typeof value) next[name] = value;
203
+ }
204
+ return next;
205
+ }
206
+
207
+ export function UiRuntime({ payload }: { payload: UiPayload }) {
208
+ const decls = useMemo(() => collectDecls(payload.root), [payload.root]);
209
+ const defaults = useMemo(() => defaultStateValues(decls) as Record<string, unknown>, [decls]);
210
+ const [state, setStateValue] = useState<Record<string, unknown>>(defaults);
211
+ const [stateRoot, setStateRoot] = useState(payload.root);
212
+ if (stateRoot !== payload.root) {
213
+ setStateRoot(payload.root);
214
+ setStateValue(carryOver(defaults, state));
215
+ }
216
+ const data = payload.data ?? EMPTY_DATA;
217
+ const scope = useMemo(
218
+ () => computeScope(decls, { stateValues: state, dataValues: data, onError: () => {} }) as Record<string, unknown>,
219
+ [decls, state, data],
220
+ );
221
+ const sliders = useMemo(() => collectSliders(payload.root), [payload.root]);
222
+ const setState = useCallback((patch: Record<string, unknown>) => setStateValue((current) => ({ ...current, ...patch })), []);
223
+ const reset = useCallback(() => setStateValue(defaults), [defaults]);
224
+ const scopeFor = useCallback(
225
+ (next: Record<string, unknown>) => computeScope(decls, { stateValues: next, dataValues: data, onError: () => {} }) as Record<string, unknown>,
226
+ [decls, data],
227
+ );
228
+ const runtime = useMemo<Runtime>(
229
+ () => ({ blockId: payload.id, root: payload.root, decls, data, defaults, state, scope, sliders, setState, reset, scopeFor }),
230
+ [payload.id, payload.root, decls, data, defaults, state, scope, sliders, setState, reset, scopeFor],
231
+ );
232
+ return (
233
+ <RuntimeContext.Provider value={runtime}>
234
+ <div className="garden-ui-body">
235
+ <RenderNodes nodes={payload.root.c} env={{ locals: [] }} />
236
+ </div>
237
+ </RuntimeContext.Provider>
238
+ );
239
+ }
@@ -0,0 +1,29 @@
1
+ export type Expr = { type: string; [key: string]: unknown };
2
+
3
+ export type Prop = { k: "lit"; v: unknown } | { k: "expr"; e: Expr };
4
+
5
+ export type UiNode = {
6
+ t: string;
7
+ n?: string;
8
+ p?: Record<string, Prop>;
9
+ c?: UiNode[];
10
+ v?: string;
11
+ e?: Expr;
12
+ u?: string;
13
+ o?: boolean;
14
+ s?: number;
15
+ };
16
+
17
+ export type UiPayload = {
18
+ v: 1;
19
+ id: string;
20
+ caption: string;
21
+ root: UiNode;
22
+ data: Record<string, unknown>;
23
+ };
24
+
25
+ export type Lookup = (name: string) => { found: boolean; value: unknown };
26
+
27
+ export type Env = { locals: Record<string, unknown>[] };
28
+
29
+ export type Diagnostic = { code: string; line: number; message: string; severity: "error" | "warning" };