@ulearning/ai-generative-frame 0.0.0-stage → 1.0.0-next.0

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 (323) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -2
  3. package/dist/assistant-ui/entry.d.ts +34 -0
  4. package/dist/assistant-ui/entry.d.ts.map +1 -0
  5. package/dist/assistant-ui/entry.js +50 -0
  6. package/dist/assistant-ui/entry.js.map +1 -0
  7. package/dist/assistant-ui/extension.d.ts +18 -0
  8. package/dist/assistant-ui/extension.d.ts.map +1 -0
  9. package/dist/assistant-ui/extension.js +0 -0
  10. package/dist/assistant-ui/history.d.ts +15 -0
  11. package/dist/assistant-ui/history.d.ts.map +1 -0
  12. package/dist/assistant-ui/history.js +39 -0
  13. package/dist/assistant-ui/history.js.map +1 -0
  14. package/dist/assistant-ui/modelContext.d.ts +45 -0
  15. package/dist/assistant-ui/modelContext.d.ts.map +1 -0
  16. package/dist/assistant-ui/modelContext.js +88 -0
  17. package/dist/assistant-ui/modelContext.js.map +1 -0
  18. package/dist/assistant-ui/snapshotSaver.d.ts +11 -0
  19. package/dist/assistant-ui/snapshotSaver.d.ts.map +1 -0
  20. package/dist/assistant-ui/snapshotSaver.js +83 -0
  21. package/dist/assistant-ui/snapshotSaver.js.map +1 -0
  22. package/dist/assistant-ui/tool-calls.d.ts +21 -0
  23. package/dist/assistant-ui/tool-calls.d.ts.map +1 -0
  24. package/dist/assistant-ui/tool-calls.js +65 -0
  25. package/dist/assistant-ui/tool-calls.js.map +1 -0
  26. package/dist/assistant-ui/toolkit.d.ts +96 -0
  27. package/dist/assistant-ui/toolkit.d.ts.map +1 -0
  28. package/dist/assistant-ui/toolkit.js +456 -0
  29. package/dist/assistant-ui/toolkit.js.map +1 -0
  30. package/dist/assistant-ui/useSharedContext.d.ts +14 -0
  31. package/dist/assistant-ui/useSharedContext.d.ts.map +1 -0
  32. package/dist/assistant-ui/useSharedContext.js +27 -0
  33. package/dist/assistant-ui/useSharedContext.js.map +1 -0
  34. package/dist/assistant-ui.d.ts +3 -0
  35. package/dist/assistant-ui.d.ts.map +1 -0
  36. package/dist/assistant-ui.js +1 -0
  37. package/dist/bootstrap.d.ts +23 -0
  38. package/dist/bootstrap.d.ts.map +1 -0
  39. package/dist/bootstrap.js +78 -0
  40. package/dist/bootstrap.js.map +1 -0
  41. package/dist/csp.d.ts +25 -0
  42. package/dist/csp.d.ts.map +1 -0
  43. package/dist/csp.js +78 -0
  44. package/dist/csp.js.map +1 -0
  45. package/dist/index.d.ts +6 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +5 -0
  48. package/dist/json-schema.d.ts +70 -0
  49. package/dist/json-schema.d.ts.map +1 -0
  50. package/dist/json-schema.js +163 -0
  51. package/dist/json-schema.js.map +1 -0
  52. package/dist/preview.d.ts +30 -0
  53. package/dist/preview.d.ts.map +1 -0
  54. package/dist/preview.js +60 -0
  55. package/dist/preview.js.map +1 -0
  56. package/dist/prompts/guidance.d.ts +41 -0
  57. package/dist/prompts/guidance.d.ts.map +1 -0
  58. package/dist/prompts/guidance.js +268 -0
  59. package/dist/prompts/guidance.js.map +1 -0
  60. package/dist/prompts.d.ts +2 -0
  61. package/dist/prompts.d.ts.map +1 -0
  62. package/dist/prompts.js +1 -0
  63. package/dist/protocol.d.ts +114 -0
  64. package/dist/protocol.d.ts.map +1 -0
  65. package/dist/protocol.js +49 -0
  66. package/dist/protocol.js.map +1 -0
  67. package/dist/repair/repair.d.ts +72 -0
  68. package/dist/repair/repair.d.ts.map +1 -0
  69. package/dist/repair/repair.js +103 -0
  70. package/dist/repair/repair.js.map +1 -0
  71. package/dist/repair.d.ts +2 -0
  72. package/dist/repair.d.ts.map +1 -0
  73. package/dist/repair.js +1 -0
  74. package/dist/rpc.d.ts +60 -0
  75. package/dist/rpc.d.ts.map +1 -0
  76. package/dist/rpc.js +127 -0
  77. package/dist/rpc.js.map +1 -0
  78. package/dist/runtime/diagnostics.d.ts +17 -0
  79. package/dist/runtime/diagnostics.d.ts.map +1 -0
  80. package/dist/runtime/diagnostics.js +128 -0
  81. package/dist/runtime/diagnostics.js.map +1 -0
  82. package/dist/runtime/generated.d.ts +2 -0
  83. package/dist/runtime/generated.d.ts.map +1 -0
  84. package/dist/runtime/generated.js +5 -0
  85. package/dist/runtime/generated.js.map +1 -0
  86. package/dist/runtime/index.d.ts +7 -0
  87. package/dist/runtime/index.d.ts.map +1 -0
  88. package/dist/runtime/index.js +264 -0
  89. package/dist/runtime/index.js.map +1 -0
  90. package/dist/runtime/morph.d.ts +51 -0
  91. package/dist/runtime/morph.d.ts.map +1 -0
  92. package/dist/runtime/morph.js +251 -0
  93. package/dist/runtime/morph.js.map +1 -0
  94. package/dist/runtime/screenshot.d.ts +23 -0
  95. package/dist/runtime/screenshot.d.ts.map +1 -0
  96. package/dist/runtime/screenshot.js +188 -0
  97. package/dist/runtime/screenshot.js.map +1 -0
  98. package/dist/runtime/size.d.ts +10 -0
  99. package/dist/runtime/size.d.ts.map +1 -0
  100. package/dist/runtime/size.js +21 -0
  101. package/dist/runtime/size.js.map +1 -0
  102. package/dist/runtime/storage.d.ts +4 -0
  103. package/dist/runtime/storage.d.ts.map +1 -0
  104. package/dist/runtime/storage.js +38 -0
  105. package/dist/runtime/storage.js.map +1 -0
  106. package/dist/spec/actions.d.ts +40 -0
  107. package/dist/spec/actions.d.ts.map +1 -0
  108. package/dist/spec/actions.js +53 -0
  109. package/dist/spec/actions.js.map +1 -0
  110. package/dist/spec/assistant-ui/thread.d.ts +66 -0
  111. package/dist/spec/assistant-ui/thread.d.ts.map +1 -0
  112. package/dist/spec/assistant-ui/thread.js +252 -0
  113. package/dist/spec/assistant-ui/thread.js.map +1 -0
  114. package/dist/spec/assistant-ui/toolkit.d.ts +33 -0
  115. package/dist/spec/assistant-ui/toolkit.d.ts.map +1 -0
  116. package/dist/spec/assistant-ui/toolkit.js +142 -0
  117. package/dist/spec/assistant-ui/toolkit.js.map +1 -0
  118. package/dist/spec/assistant-ui/web.d.ts +6 -0
  119. package/dist/spec/assistant-ui/web.d.ts.map +1 -0
  120. package/dist/spec/assistant-ui/web.js +48 -0
  121. package/dist/spec/assistant-ui/web.js.map +1 -0
  122. package/dist/spec/assistant-ui.d.ts +3 -0
  123. package/dist/spec/assistant-ui.d.ts.map +1 -0
  124. package/dist/spec/assistant-ui.js +1 -0
  125. package/dist/spec/calc.d.ts +8 -0
  126. package/dist/spec/calc.d.ts.map +1 -0
  127. package/dist/spec/calc.js +498 -0
  128. package/dist/spec/calc.js.map +1 -0
  129. package/dist/spec/catalog.d.ts +52 -0
  130. package/dist/spec/catalog.d.ts.map +1 -0
  131. package/dist/spec/catalog.js +104 -0
  132. package/dist/spec/catalog.js.map +1 -0
  133. package/dist/spec/expressions.d.ts +45 -0
  134. package/dist/spec/expressions.d.ts.map +1 -0
  135. package/dist/spec/expressions.js +199 -0
  136. package/dist/spec/expressions.js.map +1 -0
  137. package/dist/spec/patch.d.ts +20 -0
  138. package/dist/spec/patch.d.ts.map +1 -0
  139. package/dist/spec/patch.js +77 -0
  140. package/dist/spec/patch.js.map +1 -0
  141. package/dist/spec/pending.d.ts +15 -0
  142. package/dist/spec/pending.d.ts.map +1 -0
  143. package/dist/spec/pending.js +33 -0
  144. package/dist/spec/pending.js.map +1 -0
  145. package/dist/spec/pointer.d.ts +17 -0
  146. package/dist/spec/pointer.d.ts.map +1 -0
  147. package/dist/spec/pointer.js +88 -0
  148. package/dist/spec/pointer.js.map +1 -0
  149. package/dist/spec/prompt.d.ts +29 -0
  150. package/dist/spec/prompt.d.ts.map +1 -0
  151. package/dist/spec/prompt.js +145 -0
  152. package/dist/spec/prompt.js.map +1 -0
  153. package/dist/spec/render-spec.d.ts +50 -0
  154. package/dist/spec/render-spec.d.ts.map +1 -0
  155. package/dist/spec/render-spec.js +104 -0
  156. package/dist/spec/render-spec.js.map +1 -0
  157. package/dist/spec/resolve.d.ts +39 -0
  158. package/dist/spec/resolve.d.ts.map +1 -0
  159. package/dist/spec/resolve.js +51 -0
  160. package/dist/spec/resolve.js.map +1 -0
  161. package/dist/spec/resolved.d.ts +3 -0
  162. package/dist/spec/resolved.d.ts.map +1 -0
  163. package/dist/spec/resolved.js +36 -0
  164. package/dist/spec/resolved.js.map +1 -0
  165. package/dist/spec/state.d.ts +34 -0
  166. package/dist/spec/state.d.ts.map +1 -0
  167. package/dist/spec/state.js +111 -0
  168. package/dist/spec/state.js.map +1 -0
  169. package/dist/spec/stream.d.ts +50 -0
  170. package/dist/spec/stream.d.ts.map +1 -0
  171. package/dist/spec/stream.js +168 -0
  172. package/dist/spec/stream.js.map +1 -0
  173. package/dist/spec/tools.d.ts +3 -0
  174. package/dist/spec/tools.d.ts.map +1 -0
  175. package/dist/spec/tools.js +3 -0
  176. package/dist/spec/tree.d.ts +21 -0
  177. package/dist/spec/tree.d.ts.map +1 -0
  178. package/dist/spec/tree.js +27 -0
  179. package/dist/spec/tree.js.map +1 -0
  180. package/dist/spec/types.d.ts +66 -0
  181. package/dist/spec/types.d.ts.map +1 -0
  182. package/dist/spec/types.js +9 -0
  183. package/dist/spec/types.js.map +1 -0
  184. package/dist/spec/validate.d.ts +32 -0
  185. package/dist/spec/validate.d.ts.map +1 -0
  186. package/dist/spec/validate.js +264 -0
  187. package/dist/spec/validate.js.map +1 -0
  188. package/dist/spec/vue/SpecRenderer.d.ts +43 -0
  189. package/dist/spec/vue/SpecRenderer.d.ts.map +1 -0
  190. package/dist/spec/vue/SpecRenderer.js +357 -0
  191. package/dist/spec/vue/SpecRenderer.js.map +1 -0
  192. package/dist/spec/vue/useSpecStream.d.ts +19 -0
  193. package/dist/spec/vue/useSpecStream.d.ts.map +1 -0
  194. package/dist/spec/vue/useSpecStream.js +115 -0
  195. package/dist/spec/vue/useSpecStream.js.map +1 -0
  196. package/dist/spec/vue.d.ts +3 -0
  197. package/dist/spec/vue.d.ts.map +1 -0
  198. package/dist/spec/vue.js +3 -0
  199. package/dist/spec.d.ts +12 -0
  200. package/dist/spec.d.ts.map +1 -0
  201. package/dist/spec.js +8 -0
  202. package/dist/theme.d.ts +38 -0
  203. package/dist/theme.d.ts.map +1 -0
  204. package/dist/theme.js +370 -0
  205. package/dist/theme.js.map +1 -0
  206. package/dist/tools/define.d.ts +43 -0
  207. package/dist/tools/define.d.ts.map +1 -0
  208. package/dist/tools/define.js +31 -0
  209. package/dist/tools/define.js.map +1 -0
  210. package/dist/tools/edits.d.ts +19 -0
  211. package/dist/tools/edits.d.ts.map +1 -0
  212. package/dist/tools/edits.js +55 -0
  213. package/dist/tools/edits.js.map +1 -0
  214. package/dist/tools/registry.d.ts +19 -0
  215. package/dist/tools/registry.d.ts.map +1 -0
  216. package/dist/tools/registry.js +33 -0
  217. package/dist/tools/registry.js.map +1 -0
  218. package/dist/tools/tools.d.ts +88 -0
  219. package/dist/tools/tools.d.ts.map +1 -0
  220. package/dist/tools/tools.js +235 -0
  221. package/dist/tools/tools.js.map +1 -0
  222. package/dist/tools.d.ts +5 -0
  223. package/dist/tools.d.ts.map +1 -0
  224. package/dist/tools.js +5 -0
  225. package/dist/version.d.ts +2 -0
  226. package/dist/version.d.ts.map +1 -0
  227. package/dist/version.js +5 -0
  228. package/dist/version.js.map +1 -0
  229. package/dist/vue/Widget.d.ts +16 -0
  230. package/dist/vue/Widget.d.ts.map +1 -0
  231. package/dist/vue/Widget.js +58 -0
  232. package/dist/vue/Widget.js.map +1 -0
  233. package/dist/vue/sync.d.ts +19 -0
  234. package/dist/vue/sync.d.ts.map +1 -0
  235. package/dist/vue/sync.js +31 -0
  236. package/dist/vue/sync.js.map +1 -0
  237. package/dist/vue/themeStore.d.ts +23 -0
  238. package/dist/vue/themeStore.d.ts.map +1 -0
  239. package/dist/vue/themeStore.js +115 -0
  240. package/dist/vue/themeStore.js.map +1 -0
  241. package/dist/vue/useThemeTokens.d.ts +6 -0
  242. package/dist/vue/useThemeTokens.d.ts.map +1 -0
  243. package/dist/vue/useThemeTokens.js +49 -0
  244. package/dist/vue/useThemeTokens.js.map +1 -0
  245. package/dist/vue/useWidget.d.ts +10 -0
  246. package/dist/vue/useWidget.d.ts.map +1 -0
  247. package/dist/vue/useWidget.js +81 -0
  248. package/dist/vue/useWidget.js.map +1 -0
  249. package/dist/vue.d.ts +5 -0
  250. package/dist/vue.d.ts.map +1 -0
  251. package/dist/vue.js +4 -0
  252. package/dist/widget.d.ts +129 -0
  253. package/dist/widget.d.ts.map +1 -0
  254. package/dist/widget.js +655 -0
  255. package/dist/widget.js.map +1 -0
  256. package/package.json +125 -3
  257. package/src/assistant-ui/entry.ts +124 -0
  258. package/src/assistant-ui/extension.ts +19 -0
  259. package/src/assistant-ui/history.ts +63 -0
  260. package/src/assistant-ui/modelContext.ts +162 -0
  261. package/src/assistant-ui/snapshotSaver.ts +116 -0
  262. package/src/assistant-ui/tool-calls.ts +115 -0
  263. package/src/assistant-ui/toolkit.tsx +747 -0
  264. package/src/assistant-ui/useSharedContext.ts +40 -0
  265. package/src/assistant-ui.ts +14 -0
  266. package/src/bootstrap.ts +112 -0
  267. package/src/csp.ts +96 -0
  268. package/src/index.ts +44 -0
  269. package/src/json-schema.ts +353 -0
  270. package/src/preview.ts +120 -0
  271. package/src/prompts/guidance.ts +369 -0
  272. package/src/prompts.ts +8 -0
  273. package/src/protocol.ts +141 -0
  274. package/src/repair/repair.ts +202 -0
  275. package/src/repair.ts +11 -0
  276. package/src/rpc.ts +228 -0
  277. package/src/runtime/diagnostics.ts +165 -0
  278. package/src/runtime/generated.ts +2 -0
  279. package/src/runtime/index.ts +343 -0
  280. package/src/runtime/morph.ts +352 -0
  281. package/src/runtime/screenshot.ts +288 -0
  282. package/src/runtime/size.ts +17 -0
  283. package/src/runtime/storage.ts +49 -0
  284. package/src/spec/actions.ts +111 -0
  285. package/src/spec/assistant-ui/thread.ts +381 -0
  286. package/src/spec/assistant-ui/toolkit.tsx +295 -0
  287. package/src/spec/assistant-ui/web.tsx +63 -0
  288. package/src/spec/assistant-ui.ts +2 -0
  289. package/src/spec/calc.ts +598 -0
  290. package/src/spec/catalog.ts +196 -0
  291. package/src/spec/expressions.ts +301 -0
  292. package/src/spec/patch.ts +112 -0
  293. package/src/spec/pending.ts +36 -0
  294. package/src/spec/pointer.ts +170 -0
  295. package/src/spec/prompt.ts +243 -0
  296. package/src/spec/render-spec.ts +144 -0
  297. package/src/spec/resolve.ts +123 -0
  298. package/src/spec/resolved.ts +53 -0
  299. package/src/spec/state.ts +193 -0
  300. package/src/spec/stream.ts +230 -0
  301. package/src/spec/tools.ts +17 -0
  302. package/src/spec/tree.ts +37 -0
  303. package/src/spec/types.ts +51 -0
  304. package/src/spec/validate.ts +429 -0
  305. package/src/spec/vue/SpecRenderer.tsx +544 -0
  306. package/src/spec/vue/__tests__/harness.ts +50 -0
  307. package/src/spec/vue/useSpecStream.ts +162 -0
  308. package/src/spec/vue.ts +13 -0
  309. package/src/spec.ts +55 -0
  310. package/src/theme.ts +423 -0
  311. package/src/tools/define.ts +65 -0
  312. package/src/tools/edits.ts +73 -0
  313. package/src/tools/registry.ts +44 -0
  314. package/src/tools/tools.ts +361 -0
  315. package/src/tools.ts +32 -0
  316. package/src/version.ts +4 -0
  317. package/src/vue/Widget.tsx +74 -0
  318. package/src/vue/sync.ts +36 -0
  319. package/src/vue/themeStore.ts +157 -0
  320. package/src/vue/useThemeTokens.ts +71 -0
  321. package/src/vue/useWidget.ts +117 -0
  322. package/src/vue.ts +8 -0
  323. package/src/widget.ts +1080 -0
@@ -0,0 +1,196 @@
1
+ import {
2
+ describeSchema,
3
+ toJsonSchema,
4
+ validateSchema,
5
+ type JsonSchema,
6
+ type SchemaIssue,
7
+ type StandardSchemaLike,
8
+ } from "../json-schema";
9
+ import { isExpression } from "./expressions";
10
+ import { buildSpecPrompt, type SpecPromptOptions } from "./prompt";
11
+
12
+ export type PropsSchema = JsonSchema | StandardSchemaLike;
13
+
14
+ /** A component the model may use. Implementations are supplied per framework. */
15
+ export type ComponentDefinition = {
16
+ description: string;
17
+ /** JSON Schema for the props, or a Standard Schema (e.g. Zod 4) that can export one. */
18
+ props?: PropsSchema;
19
+ /**
20
+ * Child slots. `"default"` maps to `children`; other names map to
21
+ * `slots.<name>`. Components without slots take no children.
22
+ */
23
+ slots?: readonly string[];
24
+ /** Events the component emits, which elements bind with `on`. */
25
+ events?: readonly string[];
26
+ };
27
+
28
+ export type ActionDefinition = {
29
+ description: string;
30
+ params?: PropsSchema;
31
+ };
32
+
33
+ export type CatalogDefinition<
34
+ TComponents extends Record<string, ComponentDefinition> = Record<
35
+ string,
36
+ ComponentDefinition
37
+ >,
38
+ TActions extends Record<string, ActionDefinition> = Record<
39
+ string,
40
+ ActionDefinition
41
+ >,
42
+ > = {
43
+ components: TComponents;
44
+ actions?: TActions;
45
+ };
46
+
47
+ export type Catalog<
48
+ TComponents extends Record<string, ComponentDefinition> = Record<
49
+ string,
50
+ ComponentDefinition
51
+ >,
52
+ TActions extends Record<string, ActionDefinition> = Record<
53
+ string,
54
+ ActionDefinition
55
+ >,
56
+ > = {
57
+ readonly components: TComponents;
58
+ readonly actions: TActions;
59
+ /** Component definition by name, or undefined. */
60
+ component(type: string): ComponentDefinition | undefined;
61
+ /** Action definition by name, including the built-in `setState`. */
62
+ action(name: string): ActionDefinition | undefined;
63
+ /** JSON Schema of a component's props, if it has one. */
64
+ propsSchema(type: string): JsonSchema | undefined;
65
+ /** Validates props, treating expression values as valid placeholders. */
66
+ /** JSON Schema of an action's params, if it has one. */
67
+ paramsSchema(action: string): JsonSchema | undefined;
68
+ validateProps(type: string, props: unknown): SchemaIssue[];
69
+ validateParams(action: string, params: unknown): SchemaIssue[];
70
+ /** Model guidance for this catalog: components, actions, protocol, expressions, rules. */
71
+ prompt(options?: SpecPromptOptions): string;
72
+ };
73
+
74
+ /** The built-in action every catalog accepts. */
75
+ export const SET_STATE_ACTION: ActionDefinition = {
76
+ description: "Writes `value` at the JSON Pointer `path` in state.",
77
+ params: {
78
+ type: "object",
79
+ properties: {
80
+ path: { type: "string", description: "JSON Pointer into state" },
81
+ value: { description: "The value to write" },
82
+ },
83
+ required: ["path"],
84
+ },
85
+ };
86
+
87
+ const SEND_MESSAGE_ACTION: ActionDefinition = {
88
+ description: "Sends `text` as the user's next message.",
89
+ params: {
90
+ type: "object",
91
+ properties: { text: { type: "string", description: "The message" } },
92
+ required: ["text"],
93
+ },
94
+ };
95
+
96
+ const UPDATE_MODEL_CONTEXT_ACTION: ActionDefinition = {
97
+ description:
98
+ "Replaces what this UI tells the model about the user's choices with `text` and `data`.",
99
+ params: {
100
+ type: "object",
101
+ properties: {
102
+ text: { type: "string", description: "The choices in words" },
103
+ data: { description: "The choices as data" },
104
+ },
105
+ },
106
+ };
107
+
108
+ const COPY_TEXT_ACTION: ActionDefinition = {
109
+ description: "Copies `text` to the user's clipboard.",
110
+ params: {
111
+ type: "object",
112
+ properties: { text: { type: "string", description: "The text to copy" } },
113
+ required: ["text"],
114
+ },
115
+ };
116
+
117
+ /** Built-in actions every catalog accepts; a host handles `sendMessage`, `updateModelContext`, and `copyText` the way it handles its own actions. */
118
+ const BUILT_IN_ACTIONS: Readonly<Record<string, ActionDefinition>> = {
119
+ setState: SET_STATE_ACTION,
120
+ sendMessage: SEND_MESSAGE_ACTION,
121
+ updateModelContext: UPDATE_MODEL_CONTEXT_ACTION,
122
+ copyText: COPY_TEXT_ACTION,
123
+ };
124
+
125
+ const skipExpressions = { skip: isExpression };
126
+
127
+ /**
128
+ * Declares the components and actions a model may use in a spec. Props are
129
+ * JSON Schema (or a Standard Schema exposing JSON Schema), so the catalog
130
+ * stays dependency-free and works as model guidance and runtime validation.
131
+ */
132
+ export function defineCatalog<
133
+ const TComponents extends Record<string, ComponentDefinition>,
134
+ const TActions extends Record<string, ActionDefinition> = Record<
135
+ never,
136
+ ActionDefinition
137
+ >,
138
+ >(
139
+ definition: CatalogDefinition<TComponents, TActions>,
140
+ ): Catalog<TComponents, TActions> {
141
+ const components = definition.components;
142
+ const actions = (definition.actions ?? {}) as TActions;
143
+ const schemaCache = new Map<string, JsonSchema | undefined>();
144
+
145
+ const component = (type: string) =>
146
+ Object.hasOwn(components, type) ? components[type] : undefined;
147
+ for (const name of Object.keys(BUILT_IN_ACTIONS)) {
148
+ if (Object.hasOwn(actions, name)) {
149
+ throw new Error(
150
+ `The action name "${name}" is reserved for the built-in action.`,
151
+ );
152
+ }
153
+ }
154
+ const action = (name: string) =>
155
+ Object.hasOwn(BUILT_IN_ACTIONS, name)
156
+ ? BUILT_IN_ACTIONS[name]
157
+ : Object.hasOwn(actions, name)
158
+ ? actions[name]
159
+ : undefined;
160
+
161
+ const catalog: Catalog<TComponents, TActions> = {
162
+ components,
163
+ actions,
164
+ component,
165
+ action,
166
+ propsSchema(type) {
167
+ if (!schemaCache.has(type)) {
168
+ schemaCache.set(type, toJsonSchema(component(type)?.props));
169
+ }
170
+ return schemaCache.get(type);
171
+ },
172
+ paramsSchema(name) {
173
+ const key = `action:${name}`;
174
+ if (!schemaCache.has(key)) {
175
+ schemaCache.set(key, toJsonSchema(action(name)?.params));
176
+ }
177
+ return schemaCache.get(key);
178
+ },
179
+ validateProps(type, props) {
180
+ const schema = component(type)?.props;
181
+ if (!schema) return [];
182
+ return validateSchema(schema, props ?? {}, skipExpressions);
183
+ },
184
+ validateParams(name, params) {
185
+ const schema = action(name)?.params;
186
+ if (!schema) return [];
187
+ return validateSchema(schema, params ?? {}, skipExpressions);
188
+ },
189
+ prompt: (options) => buildSpecPrompt(catalog, options),
190
+ };
191
+ return catalog;
192
+ }
193
+
194
+ /** One line describing a component's props for prompts and errors. */
195
+ export const describeProps = (catalog: Catalog, type: string) =>
196
+ describeSchema(catalog.propsSchema(type));
@@ -0,0 +1,301 @@
1
+ import { deepEqual } from "../json-schema";
2
+ import { evaluateFormula } from "./calc";
3
+ import { getAtPointer, joinPointer, parsePointer } from "./pointer";
4
+ import type { Condition } from "./types";
5
+
6
+ /** What expressions can read while an element renders or an action runs. */
7
+ export type ExpressionContext = {
8
+ state: unknown;
9
+ /** The current item and index inside a `repeat`. */
10
+ item?: unknown;
11
+ index?: number;
12
+ /** The state path of the current item, so `$bindItem` can write back. */
13
+ itemPath?: string;
14
+ /** The payload an event was emitted with, for action params. */
15
+ event?: unknown;
16
+ };
17
+
18
+ const SOURCE_KEYS = [
19
+ "$state",
20
+ "$bindState",
21
+ "$item",
22
+ "$bindItem",
23
+ "$index",
24
+ "$event",
25
+ ] as const;
26
+ const EXPRESSION_KEYS = new Set<string>([
27
+ ...SOURCE_KEYS,
28
+ "$cond",
29
+ "$calc",
30
+ "$template",
31
+ ]);
32
+ const COMPARATORS = ["eq", "neq", "gt", "gte", "lt", "lte"] as const;
33
+
34
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
35
+ typeof value === "object" && value !== null && !Array.isArray(value);
36
+
37
+ /** True for `{ $state }`, `{ $bindState }`, `{ $item }`, `{ $bindItem }`, `{ $index }`, `{ $event }`, `{ $cond }`, `{ $calc }`, and `{ $template }`. */
38
+ export const isExpression = (
39
+ value: unknown,
40
+ ): value is Record<string, unknown> =>
41
+ isRecord(value) && Object.keys(value).some((key) => EXPRESSION_KEYS.has(key));
42
+
43
+ /** True for a string that is only an expression key, such as `"$calc"`. */
44
+ export const isExpressionKey = (value: string) =>
45
+ EXPRESSION_KEYS.has(value.trim());
46
+
47
+ const readPath = (base: unknown, path: unknown) =>
48
+ typeof path === "string" && path !== ""
49
+ ? getAtPointer(base, safeTokens(path))
50
+ : base;
51
+
52
+ const safeTokens = (path: string) => {
53
+ try {
54
+ return parsePointer(path.startsWith("/") ? path : `/${path}`);
55
+ } catch {
56
+ return [];
57
+ }
58
+ };
59
+
60
+ const readSource = (
61
+ expression: Record<string, unknown>,
62
+ ctx: ExpressionContext,
63
+ ) => {
64
+ if ("$state" in expression) return readPath(ctx.state, expression["$state"]);
65
+ if ("$bindState" in expression)
66
+ return readPath(ctx.state, expression["$bindState"]);
67
+ if ("$item" in expression) return readPath(ctx.item, expression["$item"]);
68
+ if ("$bindItem" in expression)
69
+ return readPath(ctx.item, expression["$bindItem"]);
70
+ if ("$index" in expression) return ctx.index;
71
+ if ("$event" in expression) return readPath(ctx.event, expression["$event"]);
72
+ return undefined;
73
+ };
74
+
75
+ const formatTemplateValue = (value: unknown) =>
76
+ value === undefined || value === null
77
+ ? ""
78
+ : typeof value === "object"
79
+ ? JSON.stringify(value)
80
+ : String(value);
81
+
82
+ /**
83
+ * Fills `${/path}` (state), `${$item/path}`, `${$item}`, and `${$index}`
84
+ * placeholders in a `$template` string.
85
+ */
86
+ export function renderTemplate(
87
+ template: string,
88
+ ctx: ExpressionContext,
89
+ ): string {
90
+ const fill = (ref: string) => {
91
+ if (ref === "$index") return formatTemplateValue(ctx.index);
92
+ if (ref === "$item") return formatTemplateValue(ctx.item);
93
+ if (ref.startsWith("$item/")) {
94
+ return formatTemplateValue(readPath(ctx.item, ref.slice(5)));
95
+ }
96
+ return formatTemplateValue(readPath(ctx.state, ref));
97
+ };
98
+ let out = "";
99
+ let from = 0;
100
+ for (const { start, end, ref } of placeholders(template)) {
101
+ out += template.slice(from, start) + fill(ref);
102
+ from = end + 1;
103
+ }
104
+ return out + template.slice(from);
105
+ }
106
+
107
+ function* placeholders(template: string) {
108
+ let from = 0;
109
+ for (;;) {
110
+ const start = template.indexOf("${", from);
111
+ if (start === -1) return;
112
+ const end = template.indexOf("}", start + 2);
113
+ if (end === -1) return;
114
+ yield { start, end, ref: template.slice(start + 2, end).trim() };
115
+ from = end + 1;
116
+ }
117
+ }
118
+
119
+ /** The state paths a `$template` string's placeholders read. */
120
+ export const templateStatePaths = (template: string): string[] =>
121
+ [...placeholders(template)]
122
+ .map(({ ref }) => ref)
123
+ .filter((ref) => !ref.startsWith("$"));
124
+
125
+ /** Resolves expressions anywhere inside a value; plain values pass through. */
126
+ export function resolveValue(value: unknown, ctx: ExpressionContext): unknown {
127
+ if (Array.isArray(value)) return value.map((item) => resolveValue(item, ctx));
128
+ if (!isRecord(value)) return value;
129
+ if ("$cond" in value) {
130
+ return evaluateCondition(value["$cond"] as Condition, ctx)
131
+ ? resolveValue(value["$then"], ctx)
132
+ : resolveValue(value["$else"], ctx);
133
+ }
134
+ if ("$calc" in value) {
135
+ return typeof value["$calc"] === "string"
136
+ ? evaluateFormula(value["$calc"], ctx)
137
+ : null;
138
+ }
139
+ if ("$template" in value) {
140
+ return typeof value["$template"] === "string"
141
+ ? renderTemplate(value["$template"], ctx)
142
+ : "";
143
+ }
144
+ if (SOURCE_KEYS.some((key) => key in value)) return readSource(value, ctx);
145
+ const result: Record<string, unknown> = {};
146
+ for (const [key, child] of Object.entries(value)) {
147
+ result[key] = resolveValue(child, ctx);
148
+ }
149
+ return result;
150
+ }
151
+
152
+ const compare = (
153
+ left: unknown,
154
+ operator: (typeof COMPARATORS)[number],
155
+ right: unknown,
156
+ ) => {
157
+ switch (operator) {
158
+ case "eq":
159
+ return deepEqual(left, right);
160
+ case "neq":
161
+ return !deepEqual(left, right);
162
+ default: {
163
+ if (
164
+ !(typeof left === "number" && typeof right === "number") &&
165
+ !(typeof left === "string" && typeof right === "string")
166
+ ) {
167
+ return false;
168
+ }
169
+ if (operator === "gt") return left > right;
170
+ if (operator === "gte") return left >= right;
171
+ if (operator === "lt") return left < right;
172
+ return left <= right;
173
+ }
174
+ }
175
+ };
176
+
177
+ /** The state paths a condition only holds true while they have a value: a `$state` it tests for truthiness or with any comparator but `neq` alone, in every part of `$and` or a list. */
178
+ export function conditionGuards(condition: unknown): string[] {
179
+ if (Array.isArray(condition)) return condition.flatMap(conditionGuards);
180
+ if (!isRecord(condition)) return [];
181
+ if (Array.isArray(condition["$and"]))
182
+ return conditionGuards(condition["$and"]);
183
+ if (typeof condition["$state"] !== "string" || condition["not"] === true)
184
+ return [];
185
+ const comparators = COMPARATORS.filter((operator) => operator in condition);
186
+ return comparators.length === 0 ||
187
+ comparators.some((operator) => operator !== "neq")
188
+ ? [condition["$state"]]
189
+ : [];
190
+ }
191
+
192
+ /**
193
+ * Evaluates a visibility condition. Unknown shapes are false, so a
194
+ * half-streamed condition hides its element rather than throwing.
195
+ */
196
+ export function evaluateCondition(
197
+ condition: Condition | undefined,
198
+ ctx: ExpressionContext,
199
+ ): boolean {
200
+ return evaluateConditionValue(condition, ctx) ?? false;
201
+ }
202
+
203
+ function evaluateConditionValue(
204
+ condition: Condition | undefined,
205
+ ctx: ExpressionContext,
206
+ ): boolean | undefined {
207
+ if (condition === undefined) return true;
208
+ if (typeof condition === "boolean") return condition;
209
+ if (Array.isArray(condition)) {
210
+ let result: boolean | undefined = true;
211
+ for (const part of condition) {
212
+ const value = evaluateConditionValue(part as Condition, ctx);
213
+ if (value === false) return false;
214
+ if (value === undefined) result = undefined;
215
+ }
216
+ return result;
217
+ }
218
+ if (!isRecord(condition)) return Boolean(condition);
219
+ if (Array.isArray(condition["$and"])) {
220
+ return evaluateConditionValue(condition["$and"] as Condition[], ctx);
221
+ }
222
+ if (Array.isArray(condition["$or"])) {
223
+ let result: boolean | undefined = false;
224
+ for (const part of condition["$or"] as Condition[]) {
225
+ const value = evaluateConditionValue(part, ctx);
226
+ if (value === true) return true;
227
+ if (value === undefined) result = undefined;
228
+ }
229
+ return result;
230
+ }
231
+ const hasSource = SOURCE_KEYS.some((key) => key in condition);
232
+ if (!hasSource && "not" in condition) {
233
+ const value = evaluateConditionValue(condition["not"] as Condition, ctx);
234
+ return value === undefined ? undefined : !value;
235
+ }
236
+ if (!hasSource) {
237
+ if (!isExpression(condition)) return undefined;
238
+ let expression: unknown = condition;
239
+ while (isRecord(expression) && "$cond" in expression) {
240
+ const matches = evaluateConditionValue(
241
+ expression["$cond"] as Condition,
242
+ ctx,
243
+ );
244
+ if (matches === undefined) return undefined;
245
+ expression = expression[matches ? "$then" : "$else"];
246
+ }
247
+ return Boolean(resolveValue(expression, ctx));
248
+ }
249
+
250
+ const value = readSource(condition, ctx);
251
+ let result = true;
252
+ let compared = false;
253
+ for (const operator of COMPARATORS) {
254
+ if (!(operator in condition)) continue;
255
+ compared = true;
256
+ result &&= compare(value, operator, resolveValue(condition[operator], ctx));
257
+ }
258
+ if (!compared) result = Boolean(value);
259
+ return condition["not"] === true ? !result : result;
260
+ }
261
+
262
+ /** Resolved props plus the state path behind each two-way bound prop. */
263
+ export type ResolvedProps = {
264
+ props: Record<string, unknown>;
265
+ bindings: Record<string, string>;
266
+ };
267
+
268
+ const normalizePath = (path: string) =>
269
+ path === "" || path.startsWith("/") ? path : `/${path}`;
270
+
271
+ /**
272
+ * Resolves an element's props. Top-level `$bindState` and `$bindItem` props
273
+ * are also reported in `bindings`, so the component can write the value back.
274
+ */
275
+ export function resolveProps(
276
+ props: Record<string, unknown> | undefined,
277
+ ctx: ExpressionContext,
278
+ ): ResolvedProps {
279
+ const resolved: Record<string, unknown> = {};
280
+ const bindings: Record<string, string> = {};
281
+ for (const [key, value] of Object.entries(props ?? {})) {
282
+ if (isRecord(value) && typeof value["$bindState"] === "string") {
283
+ bindings[key] = normalizePath(value["$bindState"]);
284
+ } else if (
285
+ isRecord(value) &&
286
+ typeof value["$bindItem"] === "string" &&
287
+ ctx.itemPath !== undefined
288
+ ) {
289
+ const field = value["$bindItem"];
290
+ bindings[key] = field
291
+ ? `${ctx.itemPath}${normalizePath(field)}`
292
+ : ctx.itemPath;
293
+ }
294
+ resolved[key] = resolveValue(value, ctx);
295
+ }
296
+ return { props: resolved, bindings };
297
+ }
298
+
299
+ /** The state path of item `index` in the array at `path`. */
300
+ export const itemPointer = (path: string, index: number) =>
301
+ `${path === "" ? "" : normalizePath(path)}${joinPointer([index])}`;
@@ -0,0 +1,112 @@
1
+ import { deepEqual } from "../json-schema";
2
+ import {
3
+ getAtPointer,
4
+ parsePointer,
5
+ writeAtPointer,
6
+ type WriteOptions,
7
+ } from "./pointer";
8
+ import type { PatchOperation } from "./types";
9
+
10
+ const OPS = new Set(["add", "replace", "remove", "move", "copy", "test"]);
11
+
12
+ /** Narrows an unknown value to a patch operation, or explains why it is not one. */
13
+ export function checkPatchOperation(
14
+ value: unknown,
15
+ ): { ok: true; op: PatchOperation } | { ok: false; error: string } {
16
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
17
+ return { ok: false, error: "a patch operation must be a JSON object" };
18
+ }
19
+ const op = value as Record<string, unknown>;
20
+ if (typeof op["op"] !== "string" || !OPS.has(op["op"])) {
21
+ return {
22
+ ok: false,
23
+ error: `"op" must be one of ${[...OPS].join(", ")}`,
24
+ };
25
+ }
26
+ if (typeof op["path"] !== "string") {
27
+ return { ok: false, error: '"path" must be a string' };
28
+ }
29
+ if (
30
+ (op["op"] === "move" || op["op"] === "copy") &&
31
+ typeof op["from"] !== "string"
32
+ ) {
33
+ return { ok: false, error: `"${op["op"]}" needs a string "from"` };
34
+ }
35
+ if (
36
+ (op["op"] === "add" || op["op"] === "replace" || op["op"] === "test") &&
37
+ !("value" in op)
38
+ ) {
39
+ return { ok: false, error: `"${op["op"]}" needs a "value"` };
40
+ }
41
+ return { ok: true, op: op as PatchOperation };
42
+ }
43
+
44
+ const clone = <T>(value: T): T =>
45
+ value === undefined ? value : (JSON.parse(JSON.stringify(value)) as T);
46
+
47
+ /**
48
+ * Applies one RFC 6902 operation and returns the new document; the input is
49
+ * not mutated and unchanged branches are shared. Throws when the operation
50
+ * cannot apply. With `createMissing`, `add` and `replace` create missing
51
+ * parent objects, which streaming relies on for paths like `/state/a/b`.
52
+ */
53
+ export function applyPatchOperation<T>(
54
+ doc: T,
55
+ operation: PatchOperation,
56
+ options: WriteOptions = {},
57
+ ): T {
58
+ const path = parsePointer(operation.path);
59
+ switch (operation.op) {
60
+ case "add":
61
+ return writeAtPointer(doc, path, "add", operation.value, options) as T;
62
+ case "replace":
63
+ return writeAtPointer(
64
+ doc,
65
+ path,
66
+ "replace",
67
+ operation.value,
68
+ options,
69
+ ) as T;
70
+ case "remove":
71
+ return writeAtPointer(doc, path, "remove") as T;
72
+ case "move": {
73
+ const from = parsePointer(operation.from);
74
+ if (
75
+ from.length < path.length &&
76
+ from.every((token, index) => path[index] === token)
77
+ ) {
78
+ throw new Error("Cannot move a value into one of its own children");
79
+ }
80
+ const value = getAtPointer(doc, from);
81
+ if (value === undefined) {
82
+ throw new Error(`Nothing to move at "${operation.from}"`);
83
+ }
84
+ const removed = writeAtPointer(doc, from, "remove");
85
+ return writeAtPointer(removed, path, "add", value, options) as T;
86
+ }
87
+ case "copy": {
88
+ const value = getAtPointer(doc, operation.from);
89
+ if (value === undefined) {
90
+ throw new Error(`Nothing to copy at "${operation.from}"`);
91
+ }
92
+ return writeAtPointer(doc, path, "add", clone(value), options) as T;
93
+ }
94
+ case "test":
95
+ if (!deepEqual(getAtPointer(doc, path), operation.value)) {
96
+ throw new Error(`Test failed at "${operation.path}"`);
97
+ }
98
+ return doc;
99
+ }
100
+ }
101
+
102
+ /** Applies operations in order; throws on the first one that fails. */
103
+ export function applyPatch<T>(
104
+ doc: T,
105
+ operations: readonly PatchOperation[],
106
+ options: WriteOptions = {},
107
+ ): T {
108
+ return operations.reduce(
109
+ (current, operation) => applyPatchOperation(current, operation, options),
110
+ doc,
111
+ );
112
+ }
@@ -0,0 +1,36 @@
1
+ const DOT_FRAMES = [{ opacity: 0.2 }, { opacity: 0.8 }, { opacity: 0.2 }];
2
+
3
+ export const PENDING_STYLE = {
4
+ display: "inline-flex",
5
+ gap: "4px",
6
+ padding: "6px 2px",
7
+ } as const;
8
+
9
+ export const PENDING_DOT_STYLE = {
10
+ width: "5px",
11
+ height: "5px",
12
+ borderRadius: "50%",
13
+ background: "currentColor",
14
+ opacity: 0.2,
15
+ } as const;
16
+
17
+ /** Pulses the pending indicator's dots in turn, and leaves them still under reduced motion; returns the cleanup. */
18
+ export function animatePendingDots(host: Element): () => void {
19
+ if (globalThis.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches) {
20
+ return () => {};
21
+ }
22
+ const animations = Array.from(host.children).flatMap((dot, index) =>
23
+ typeof dot.animate === "function"
24
+ ? [
25
+ dot.animate(DOT_FRAMES, {
26
+ duration: 1200,
27
+ delay: index * 150,
28
+ iterations: Infinity,
29
+ }),
30
+ ]
31
+ : [],
32
+ );
33
+ return () => {
34
+ for (const animation of animations) animation.cancel();
35
+ };
36
+ }