a2uink 0.1.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 (134) hide show
  1. package/.eslintignore +4 -0
  2. package/.eslintrc.cjs +21 -0
  3. package/.gitattributes +5 -0
  4. package/.github/copilot-instructions.md +21 -0
  5. package/.github/workflows/ci.yml +31 -0
  6. package/.husky/pre-commit +6 -0
  7. package/.prettierignore +6 -0
  8. package/.prettierrc +7 -0
  9. package/README.md +44 -0
  10. package/dist/binding.d.ts +3 -0
  11. package/dist/binding.js +73 -0
  12. package/dist/catalog.d.ts +6 -0
  13. package/dist/catalog.js +165 -0
  14. package/dist/examples/demo.d.ts +1 -0
  15. package/dist/examples/demo.js +309 -0
  16. package/dist/focus.d.ts +15 -0
  17. package/dist/focus.js +68 -0
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.js +1 -0
  20. package/dist/renderer.d.ts +6 -0
  21. package/dist/renderer.js +144 -0
  22. package/dist/src/binding.d.ts +8 -0
  23. package/dist/src/binding.js +141 -0
  24. package/dist/src/catalog.d.ts +2 -0
  25. package/dist/src/catalog.js +1 -0
  26. package/dist/src/components/Box.d.ts +6 -0
  27. package/dist/src/components/Box.js +23 -0
  28. package/dist/src/components/Button.d.ts +7 -0
  29. package/dist/src/components/Button.js +71 -0
  30. package/dist/src/components/Chart.d.ts +5 -0
  31. package/dist/src/components/Chart.js +65 -0
  32. package/dist/src/components/Checkbox.d.ts +7 -0
  33. package/dist/src/components/Checkbox.js +51 -0
  34. package/dist/src/components/DateTimeInput.d.ts +1 -0
  35. package/dist/src/components/DateTimeInput.js +1 -0
  36. package/dist/src/components/Divider.d.ts +5 -0
  37. package/dist/src/components/Divider.js +7 -0
  38. package/dist/src/components/Image.d.ts +5 -0
  39. package/dist/src/components/Image.js +8 -0
  40. package/dist/src/components/Input.d.ts +7 -0
  41. package/dist/src/components/Input.js +124 -0
  42. package/dist/src/components/List.d.ts +5 -0
  43. package/dist/src/components/List.js +9 -0
  44. package/dist/src/components/Modal.d.ts +6 -0
  45. package/dist/src/components/Modal.js +13 -0
  46. package/dist/src/components/RadioGroup.d.ts +7 -0
  47. package/dist/src/components/RadioGroup.js +56 -0
  48. package/dist/src/components/Select.d.ts +7 -0
  49. package/dist/src/components/Select.js +66 -0
  50. package/dist/src/components/Slider.d.ts +7 -0
  51. package/dist/src/components/Slider.js +74 -0
  52. package/dist/src/components/Spacer.d.ts +1 -0
  53. package/dist/src/components/Spacer.js +1 -0
  54. package/dist/src/components/Table.d.ts +5 -0
  55. package/dist/src/components/Table.js +14 -0
  56. package/dist/src/components/Tabs.d.ts +7 -0
  57. package/dist/src/components/Tabs.js +56 -0
  58. package/dist/src/components/Text.d.ts +5 -0
  59. package/dist/src/components/Text.js +15 -0
  60. package/dist/src/components/helpers.d.ts +4 -0
  61. package/dist/src/components/helpers.js +39 -0
  62. package/dist/src/components/index.d.ts +16 -0
  63. package/dist/src/components/index.js +15 -0
  64. package/dist/src/components/renderNode.d.ts +4 -0
  65. package/dist/src/components/renderNode.js +61 -0
  66. package/dist/src/components/types.d.ts +7 -0
  67. package/dist/src/components/types.js +1 -0
  68. package/dist/src/components.d.ts +1 -0
  69. package/dist/src/components.js +1 -0
  70. package/dist/src/focus.d.ts +15 -0
  71. package/dist/src/focus.js +68 -0
  72. package/dist/src/index.d.ts +2 -0
  73. package/dist/src/index.js +1 -0
  74. package/dist/src/renderer.d.ts +6 -0
  75. package/dist/src/renderer.js +673 -0
  76. package/dist/src/tree.d.ts +2 -0
  77. package/dist/src/tree.js +47 -0
  78. package/dist/src/types.d.ts +92 -0
  79. package/dist/src/types.js +1 -0
  80. package/dist/tree.d.ts +2 -0
  81. package/dist/tree.js +45 -0
  82. package/dist/types.d.ts +73 -0
  83. package/dist/types.js +1 -0
  84. package/docs/demo/README.md +90 -0
  85. package/docs/demo/app.js +268 -0
  86. package/docs/demo/index.html +14 -0
  87. package/docs/demo/package-lock.json +4512 -0
  88. package/docs/demo/package.json +32 -0
  89. package/docs/demo/src/App.tsx +1403 -0
  90. package/docs/demo/src/main.tsx +9 -0
  91. package/docs/demo/src/setEnv.ts +29 -0
  92. package/docs/demo/src/shims/fs.js +16 -0
  93. package/docs/demo/src/shims/process.js +10 -0
  94. package/docs/demo/src/styles.css +720 -0
  95. package/docs/demo/styles.css +1 -0
  96. package/docs/demo/tsconfig.json +14 -0
  97. package/docs/demo/vite-plugin-node-polyfills/shims/buffer +2 -0
  98. package/docs/demo/vite-plugin-node-polyfills/shims/global +2 -0
  99. package/docs/demo/vite-plugin-node-polyfills/shims/process +10 -0
  100. package/docs/demo/vite.config.js +200 -0
  101. package/docs/overview.md +277 -0
  102. package/examples/demo.d.ts +1 -0
  103. package/examples/demo.js +66 -0
  104. package/examples/demo.ts +315 -0
  105. package/package.json +48 -0
  106. package/src/binding.ts +191 -0
  107. package/src/catalog.ts +2 -0
  108. package/src/components/Box.ts +39 -0
  109. package/src/components/Button.ts +84 -0
  110. package/src/components/Checkbox.ts +66 -0
  111. package/src/components/DateTimeInput.ts +1 -0
  112. package/src/components/Divider.ts +8 -0
  113. package/src/components/Image.ts +15 -0
  114. package/src/components/Input.ts +148 -0
  115. package/src/components/List.ts +15 -0
  116. package/src/components/Modal.ts +21 -0
  117. package/src/components/RadioGroup.ts +77 -0
  118. package/src/components/Select.ts +94 -0
  119. package/src/components/Slider.ts +98 -0
  120. package/src/components/Spacer.ts +1 -0
  121. package/src/components/Table.ts +22 -0
  122. package/src/components/Tabs.ts +82 -0
  123. package/src/components/Text.ts +21 -0
  124. package/src/components/helpers.ts +42 -0
  125. package/src/components/index.ts +16 -0
  126. package/src/components/renderNode.ts +73 -0
  127. package/src/components/types.ts +8 -0
  128. package/src/components.ts +1 -0
  129. package/src/focus.ts +94 -0
  130. package/src/index.ts +12 -0
  131. package/src/renderer.ts +779 -0
  132. package/src/tree.ts +63 -0
  133. package/src/types.ts +110 -0
  134. package/tsconfig.json +16 -0
@@ -0,0 +1,309 @@
1
+ import { createA2uiInkRenderer } from "../src/index.js";
2
+ import { inspect } from "node:util";
3
+ process.on("uncaughtException", (error) => {
4
+ process.stderr.write(`\nUncaught exception: ${error instanceof Error ? error.stack : JSON.stringify(error)}\n`);
5
+ });
6
+ process.on("unhandledRejection", (reason) => {
7
+ process.stderr.write(`\nUnhandled rejection: ${reason instanceof Error ? reason.stack : JSON.stringify(reason)}\n`);
8
+ });
9
+ const renderer = createA2uiInkRenderer({
10
+ onUserAction: (action) => {
11
+ process.stderr.write(`\nUser action: ${JSON.stringify(action)}\n`);
12
+ }
13
+ });
14
+ const surfaceId = "main";
15
+ const safeHandleMessage = (message) => {
16
+ var _a, _b;
17
+ try {
18
+ renderer.handleMessage(message);
19
+ }
20
+ catch (error) {
21
+ process.stderr.write(`\nRenderer error: ${inspect(error, { depth: 5 })}\n`);
22
+ process.stderr.write(`stdout.isTTY=${(_a = process.stdout) === null || _a === void 0 ? void 0 : _a.isTTY} stderr.isTTY=${(_b = process.stderr) === null || _b === void 0 ? void 0 : _b.isTTY}\n`);
23
+ throw error;
24
+ }
25
+ };
26
+ safeHandleMessage({
27
+ type: "surfaceUpdate",
28
+ surfaceId,
29
+ rootComponentId: "root",
30
+ components: [
31
+ {
32
+ id: "root",
33
+ type: "Box",
34
+ props: { direction: "column", padding: 1, borderStyle: "round" },
35
+ children: {
36
+ explicitList: [
37
+ "title",
38
+ "rootSpacer1",
39
+ "inputSection",
40
+ "rootSpacer2",
41
+ "choiceSection",
42
+ "rootSpacer3",
43
+ "contentSection",
44
+ "rootSpacer4",
45
+ "extraSection",
46
+ "rootSpacer5",
47
+ "submit"
48
+ ]
49
+ }
50
+ },
51
+ {
52
+ id: "title",
53
+ type: "Text",
54
+ props: { text: { path: "title" }, bold: true }
55
+ },
56
+ {
57
+ id: "rootSpacer1",
58
+ type: "Spacer"
59
+ },
60
+ {
61
+ id: "inputSection",
62
+ type: "Box",
63
+ props: { direction: "column", borderStyle: "single", paddingX: 1, paddingY: 0 },
64
+ children: { explicitList: ["inputLabel", "nameInput"] }
65
+ },
66
+ {
67
+ id: "inputLabel",
68
+ type: "Text",
69
+ props: { text: "Input" }
70
+ },
71
+ {
72
+ id: "nameInput",
73
+ type: "Input",
74
+ props: {
75
+ value: { path: "form.name", literalString: "" },
76
+ onChange: { actionId: "nameChange" },
77
+ onSubmit: { actionId: "nameSubmit" }
78
+ }
79
+ },
80
+ {
81
+ id: "rootSpacer2",
82
+ type: "Spacer"
83
+ },
84
+ {
85
+ id: "choiceSection",
86
+ type: "Box",
87
+ props: { direction: "column", borderStyle: "single", paddingX: 1, paddingY: 0 },
88
+ children: { explicitList: ["checkbox", "radioLabel", "radioGroup", "selectLabel", "options"] }
89
+ },
90
+ {
91
+ id: "checkbox",
92
+ type: "Checkbox",
93
+ props: {
94
+ label: "Enable feature",
95
+ checked: { path: "flags.enabled", literalBoolean: false },
96
+ onChange: { actionId: "toggleFeature" }
97
+ }
98
+ },
99
+ {
100
+ id: "radioLabel",
101
+ type: "Text",
102
+ props: { text: "RadioGroup" }
103
+ },
104
+ {
105
+ id: "radioGroup",
106
+ type: "RadioGroup",
107
+ props: {
108
+ options: { path: "radioOptions" },
109
+ selectedIndex: { path: "selectedRadio", literalNumber: 0 },
110
+ onChange: { actionId: "radioChange" }
111
+ }
112
+ },
113
+ {
114
+ id: "selectLabel",
115
+ type: "Text",
116
+ props: { text: "Select" }
117
+ },
118
+ {
119
+ id: "options",
120
+ type: "Select",
121
+ props: {
122
+ items: { path: "options" },
123
+ selectedIndex: { path: "selectedIndex", literalNumber: 0 },
124
+ onSelect: { actionId: "optionSelect" }
125
+ }
126
+ },
127
+ {
128
+ id: "rootSpacer3",
129
+ type: "Spacer"
130
+ },
131
+ {
132
+ id: "contentSection",
133
+ type: "Box",
134
+ props: { direction: "column", borderStyle: "single", paddingX: 1, paddingY: 0 },
135
+ children: { explicitList: ["listLabel", "simpleList", "tabsLabel", "tabs", "tableLabel", "table"] }
136
+ },
137
+ {
138
+ id: "listLabel",
139
+ type: "Text",
140
+ props: { text: "List" }
141
+ },
142
+ {
143
+ id: "simpleList",
144
+ type: "List",
145
+ props: {
146
+ items: { path: "listItems" }
147
+ }
148
+ },
149
+ {
150
+ id: "tabsLabel",
151
+ type: "Text",
152
+ props: { text: "Tabs" }
153
+ },
154
+ {
155
+ id: "tabs",
156
+ type: "Tabs",
157
+ props: {
158
+ tabs: { path: "tabs" },
159
+ selectedIndex: { path: "selectedTab", literalNumber: 0 },
160
+ onChange: { actionId: "tabChange" }
161
+ },
162
+ children: {
163
+ explicitList: ["tabPanelA", "tabPanelB", "tabPanelC"]
164
+ }
165
+ },
166
+ {
167
+ id: "tabPanelA",
168
+ type: "Text",
169
+ props: { text: "Panel A content" }
170
+ },
171
+ {
172
+ id: "tabPanelB",
173
+ type: "Text",
174
+ props: { text: "Panel B content" }
175
+ },
176
+ {
177
+ id: "tabPanelC",
178
+ type: "Text",
179
+ props: { text: "Panel C content" }
180
+ },
181
+ {
182
+ id: "tableLabel",
183
+ type: "Text",
184
+ props: { text: "Table" }
185
+ },
186
+ {
187
+ id: "table",
188
+ type: "Table",
189
+ props: {
190
+ columns: { path: "table.columns" },
191
+ rows: { path: "table.rows" }
192
+ }
193
+ },
194
+ {
195
+ id: "rootSpacer4",
196
+ type: "Spacer"
197
+ },
198
+ {
199
+ id: "extraSection",
200
+ type: "Box",
201
+ props: { direction: "column", borderStyle: "single", paddingX: 1, paddingY: 0 },
202
+ children: {
203
+ explicitList: [
204
+ "textFieldLabel",
205
+ "textField",
206
+ "choiceLabel",
207
+ "multipleChoice",
208
+ "sliderLabel",
209
+ "slider",
210
+ "mediaLabel",
211
+ "image"
212
+ ]
213
+ }
214
+ },
215
+ {
216
+ id: "textFieldLabel",
217
+ type: "Text",
218
+ props: { text: "TextField" }
219
+ },
220
+ {
221
+ id: "textField",
222
+ type: "TextField",
223
+ props: {
224
+ label: "Email",
225
+ value: { path: "form.email" },
226
+ onChange: { actionId: "emailChange" }
227
+ }
228
+ },
229
+ {
230
+ id: "choiceLabel",
231
+ type: "Text",
232
+ props: { text: "MultipleChoice" }
233
+ },
234
+ {
235
+ id: "multipleChoice",
236
+ type: "MultipleChoice",
237
+ props: {
238
+ label: "Meal",
239
+ items: [
240
+ { label: "Pizza", value: "pizza" },
241
+ { label: "Sushi", value: "sushi" },
242
+ { label: "Salad", value: "salad" }
243
+ ],
244
+ onSelect: { actionId: "mealSelect" }
245
+ }
246
+ },
247
+ {
248
+ id: "sliderLabel",
249
+ type: "Text",
250
+ props: { text: "Slider" }
251
+ },
252
+ {
253
+ id: "slider",
254
+ type: "Slider",
255
+ props: { min: 1, max: 5, step: 1, value: { path: "form.priority" }, onChange: { actionId: "priorityChange" } }
256
+ },
257
+ {
258
+ id: "mediaLabel",
259
+ type: "Text",
260
+ props: { text: "Media" }
261
+ },
262
+ {
263
+ id: "image",
264
+ type: "Image",
265
+ props: { label: "Menu", url: "https://example.com/menu.png" }
266
+ },
267
+ {
268
+ id: "rootSpacer5",
269
+ type: "Spacer"
270
+ },
271
+ {
272
+ id: "submit",
273
+ type: "Button",
274
+ props: {
275
+ text: "Submit",
276
+ onPress: { actionId: "submit" }
277
+ }
278
+ }
279
+ ]
280
+ });
281
+ safeHandleMessage({
282
+ type: "dataModelUpdate",
283
+ surfaceId,
284
+ dataModel: {
285
+ title: "A2UI Ink Demo",
286
+ form: { name: "", email: "", priority: 3 },
287
+ flags: { enabled: false },
288
+ radioOptions: ["Low", "Medium", "High"],
289
+ selectedRadio: 1,
290
+ listItems: ["Item A", "Item B", "Item C"],
291
+ options: ["Alpha", "Beta", "Gamma"],
292
+ selectedIndex: 0,
293
+ tabs: ["Tab A", "Tab B", "Tab C"],
294
+ selectedTab: 0,
295
+ table: {
296
+ columns: ["Name", "Status"],
297
+ rows: [
298
+ ["Service A", "OK"],
299
+ ["Service B", "Warn"],
300
+ ["Service C", "Down"]
301
+ ]
302
+ }
303
+ }
304
+ });
305
+ safeHandleMessage({
306
+ type: "beginRendering",
307
+ surfaceId,
308
+ catalogId: "standard"
309
+ });
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+ import type { Key } from "ink";
3
+ export type FocusKeyHandler = (input: string, key: Key) => void;
4
+ export interface FocusRegistry {
5
+ register(id: string, handler: FocusKeyHandler): void;
6
+ unregister(id: string): void;
7
+ isFocused(id: string): boolean;
8
+ focusNext(): void;
9
+ focusPrev(): void;
10
+ handleKey(input: string, key: Key): void;
11
+ }
12
+ export declare const FocusProvider: React.FC<{
13
+ children: React.ReactNode;
14
+ }>;
15
+ export declare function useFocusRegistry(): FocusRegistry;
package/dist/focus.js ADDED
@@ -0,0 +1,68 @@
1
+ import React, { createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
2
+ const FocusContext = createContext(null);
3
+ export const FocusProvider = ({ children }) => {
4
+ const [order, setOrder] = useState([]);
5
+ const [activeId, setActiveId] = useState(null);
6
+ const handlers = useRef(new Map());
7
+ const register = useCallback((id, handler) => {
8
+ handlers.current.set(id, handler);
9
+ setOrder((current) => (current.includes(id) ? current : [...current, id]));
10
+ setActiveId((current) => current !== null && current !== void 0 ? current : id);
11
+ }, []);
12
+ const unregister = useCallback((id) => {
13
+ handlers.current.delete(id);
14
+ setOrder((current) => current.filter((value) => value !== id));
15
+ setActiveId((current) => (current === id ? null : current));
16
+ }, []);
17
+ const focusNext = useCallback(() => {
18
+ setActiveId((current) => {
19
+ var _a;
20
+ if (!order.length) {
21
+ return null;
22
+ }
23
+ if (!current) {
24
+ return order[0];
25
+ }
26
+ const index = order.indexOf(current);
27
+ return (_a = order[(index + 1) % order.length]) !== null && _a !== void 0 ? _a : order[0];
28
+ });
29
+ }, [order]);
30
+ const focusPrev = useCallback(() => {
31
+ setActiveId((current) => {
32
+ var _a;
33
+ if (!order.length) {
34
+ return null;
35
+ }
36
+ if (!current) {
37
+ return order[order.length - 1];
38
+ }
39
+ const index = order.indexOf(current);
40
+ return (_a = order[(index - 1 + order.length) % order.length]) !== null && _a !== void 0 ? _a : order[0];
41
+ });
42
+ }, [order]);
43
+ const handleKey = useCallback((input, key) => {
44
+ if (!activeId) {
45
+ return;
46
+ }
47
+ const handler = handlers.current.get(activeId);
48
+ if (handler) {
49
+ handler(input, key);
50
+ }
51
+ }, [activeId]);
52
+ const registry = useMemo(() => ({
53
+ register,
54
+ unregister,
55
+ isFocused: (id) => activeId === id,
56
+ focusNext,
57
+ focusPrev,
58
+ handleKey
59
+ }), [register, unregister, activeId, focusNext, focusPrev, handleKey]);
60
+ return React.createElement(FocusContext.Provider, { value: registry }, children);
61
+ };
62
+ export function useFocusRegistry() {
63
+ const context = useContext(FocusContext);
64
+ if (!context) {
65
+ throw new Error("FocusRegistry is not available");
66
+ }
67
+ return context;
68
+ }
@@ -0,0 +1,2 @@
1
+ export { createA2uiInkRenderer } from "./renderer.js";
2
+ export type { A2uiServerMessage, A2uiUserAction, BeginRenderingMessage, SurfaceUpdateMessage, DataModelUpdateMessage, DeleteSurfaceMessage, RendererOptions, ComponentDef, BoundValue } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { createA2uiInkRenderer } from "./renderer.js";
@@ -0,0 +1,6 @@
1
+ import type { A2uiServerMessage, RendererOptions } from "./types.js";
2
+ export interface A2uiInkRenderer {
3
+ handleMessage(message: A2uiServerMessage): void;
4
+ dispose(): void;
5
+ }
6
+ export declare function createA2uiInkRenderer(options?: RendererOptions): A2uiInkRenderer;
@@ -0,0 +1,144 @@
1
+ import React from "react";
2
+ import { render, Text, useInput } from "ink";
3
+ import { FocusProvider, useFocusRegistry } from "./focus.js";
4
+ import { buildResolvedTree } from "./tree.js";
5
+ import { renderNode } from "./catalog.js";
6
+ export function createA2uiInkRenderer(options = {}) {
7
+ const surfaces = new Map();
8
+ let inkInstance = null;
9
+ const ensureSurface = (surfaceId) => {
10
+ const existing = surfaces.get(surfaceId);
11
+ if (existing) {
12
+ return existing;
13
+ }
14
+ const created = {
15
+ surfaceId,
16
+ components: {},
17
+ dataModel: {},
18
+ renderReady: false
19
+ };
20
+ surfaces.set(surfaceId, created);
21
+ return created;
22
+ };
23
+ const renderSurfaces = () => {
24
+ const surface = Array.from(surfaces.values()).find((entry) => entry.renderReady && entry.rootComponentId);
25
+ const element = React.createElement(A2uiRoot, {
26
+ surface: surface !== null && surface !== void 0 ? surface : null,
27
+ onUserAction: options.onUserAction
28
+ });
29
+ if (!inkInstance) {
30
+ inkInstance = render(element, {
31
+ stdin: options.stdin,
32
+ stdout: options.stdout,
33
+ stderr: options.stderr,
34
+ exitOnCtrlC: false
35
+ });
36
+ }
37
+ else {
38
+ inkInstance.rerender(element);
39
+ }
40
+ };
41
+ const handleMessage = (message) => {
42
+ switch (message.type) {
43
+ case "beginRendering": {
44
+ const surface = ensureSurface(message.surfaceId);
45
+ surface.catalogId = message.catalogId;
46
+ surface.renderReady = true;
47
+ renderSurfaces();
48
+ break;
49
+ }
50
+ case "surfaceUpdate": {
51
+ const surface = ensureSurface(message.surfaceId);
52
+ surface.rootComponentId = message.rootComponentId;
53
+ surface.components = message.components.reduce((acc, component) => {
54
+ acc[component.id] = component;
55
+ return acc;
56
+ }, {});
57
+ renderSurfaces();
58
+ break;
59
+ }
60
+ case "dataModelUpdate": {
61
+ const surface = ensureSurface(message.surfaceId);
62
+ surface.dataModel = mergeDataModel(surface.dataModel, message.dataModel);
63
+ renderSurfaces();
64
+ break;
65
+ }
66
+ case "deleteSurface": {
67
+ surfaces.delete(message.surfaceId);
68
+ renderSurfaces();
69
+ break;
70
+ }
71
+ default:
72
+ break;
73
+ }
74
+ };
75
+ const dispose = () => {
76
+ inkInstance === null || inkInstance === void 0 ? void 0 : inkInstance.unmount();
77
+ inkInstance = null;
78
+ };
79
+ return { handleMessage, dispose };
80
+ }
81
+ const A2uiRoot = ({ surface, onUserAction }) => {
82
+ if (!surface || !surface.rootComponentId) {
83
+ return React.createElement(Text, null, "No surface");
84
+ }
85
+ const tree = buildResolvedTree(surface.components, surface.rootComponentId, surface.dataModel);
86
+ if (!tree) {
87
+ return React.createElement(Text, null, "Invalid surface");
88
+ }
89
+ return React.createElement(FocusProvider, null, React.createElement(FocusInputHandler), React.createElement(RenderTree, {
90
+ surfaceId: surface.surfaceId,
91
+ tree,
92
+ onUserAction
93
+ }));
94
+ };
95
+ const FocusInputHandler = () => {
96
+ const focus = useFocusRegistry();
97
+ useInput((input, key) => {
98
+ if (key.tab) {
99
+ if (key.shift) {
100
+ focus.focusPrev();
101
+ }
102
+ else {
103
+ focus.focusNext();
104
+ }
105
+ return;
106
+ }
107
+ focus.handleKey(input, key);
108
+ });
109
+ return null;
110
+ };
111
+ const RenderTree = ({ surfaceId, tree, onUserAction }) => {
112
+ const dispatchAction = (action, node, value) => {
113
+ var _a, _b, _c;
114
+ const context = {
115
+ ...((_a = action.context) !== null && _a !== void 0 ? _a : {}),
116
+ ...(((_b = node.bindingContext) === null || _b === void 0 ? void 0 : _b.index) !== undefined ? { index: node.bindingContext.index } : {}),
117
+ ...(((_c = node.bindingContext) === null || _c === void 0 ? void 0 : _c.item) !== undefined ? { item: node.bindingContext.item } : {})
118
+ };
119
+ onUserAction === null || onUserAction === void 0 ? void 0 : onUserAction({
120
+ type: "userAction",
121
+ surfaceId,
122
+ componentId: node.id,
123
+ actionId: action.actionId,
124
+ context,
125
+ value
126
+ });
127
+ };
128
+ return renderNode(tree, { dispatchAction });
129
+ };
130
+ function mergeDataModel(base, update) {
131
+ const result = { ...base };
132
+ for (const [key, value] of Object.entries(update)) {
133
+ if (isPlainObject(value) && isPlainObject(result[key])) {
134
+ result[key] = mergeDataModel(result[key], value);
135
+ }
136
+ else {
137
+ result[key] = value;
138
+ }
139
+ }
140
+ return result;
141
+ }
142
+ function isPlainObject(value) {
143
+ return !!value && typeof value === "object" && !Array.isArray(value);
144
+ }
@@ -0,0 +1,8 @@
1
+ import type { BindingContext, BoundValue } from "./types.js";
2
+ export declare function resolveBoundValue(boundValue: BoundValue | undefined, dataModel: Record<string, unknown>, context?: BindingContext): unknown;
3
+ export declare function resolveProps(props: Record<string, unknown>, dataModel: Record<string, unknown>, context?: BindingContext): Record<string, unknown>;
4
+ export declare function resolvePropsWithBindings(props: Record<string, unknown>, dataModel: Record<string, unknown>, context?: BindingContext): {
5
+ props: Record<string, unknown>;
6
+ boundProps: Record<string, BoundValue>;
7
+ };
8
+ export declare function setBoundValue(path: string, dataModel: Record<string, unknown>, context: BindingContext | undefined, value: unknown): Record<string, unknown>;