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,779 @@
1
+ import React from "react";
2
+ import { Box, render, Text, useInput, useStdout } from "ink";
3
+ import type { Key } from "ink";
4
+ import type { ActionDef, A2uiServerMessage, A2uiUserAction, BoundValue, ComponentDef, DataModelEntry, RendererOptions, ResolvedNode } from "./types.js";
5
+ import { resolveBoundValue, setBoundValue } from "./binding.js";
6
+ import { FocusProvider, useFocusRegistry } from "./focus.js";
7
+ import { buildResolvedTree } from "./tree.js";
8
+ import { renderNode } from "./catalog.js";
9
+
10
+ interface SurfaceState {
11
+ surfaceId: string;
12
+ catalogId?: string;
13
+ rootComponentId?: string;
14
+ components: Record<string, ComponentDef>;
15
+ dataModel: Record<string, unknown>;
16
+ renderReady: boolean;
17
+ }
18
+
19
+ export interface A2uiInkRenderer {
20
+ handleMessage(message: A2uiServerMessage): void;
21
+ dispose(): void;
22
+ }
23
+
24
+ export function createA2uiInkRenderer(options: RendererOptions = {}): A2uiInkRenderer {
25
+ const surfaces = new Map<string, SurfaceState>();
26
+ let inkInstance: ReturnType<typeof render> | null = null;
27
+
28
+ const updateLocalDataModel = (surfaceId: string, path: string, value: unknown, node: ResolvedNode) => {
29
+ const surface = surfaces.get(surfaceId);
30
+ if (!surface) {
31
+ return;
32
+ }
33
+ surface.dataModel = setBoundValue(path, surface.dataModel, node.bindingContext, value);
34
+ renderSurfaces();
35
+ };
36
+
37
+ const ensureSurface = (surfaceId: string): SurfaceState => {
38
+ const existing = surfaces.get(surfaceId);
39
+ if (existing) {
40
+ return existing;
41
+ }
42
+ const created: SurfaceState = {
43
+ surfaceId,
44
+ components: {},
45
+ dataModel: {},
46
+ renderReady: false
47
+ };
48
+ surfaces.set(surfaceId, created);
49
+ return created;
50
+ };
51
+
52
+ const renderSurfaces = () => {
53
+ const surface = Array.from(surfaces.values()).find((entry) => entry.renderReady && entry.rootComponentId);
54
+
55
+ const element = React.createElement(A2uiRoot, {
56
+ surface: surface ?? null,
57
+ onUserAction: options.onUserAction,
58
+ onLocalDataModelUpdate: updateLocalDataModel
59
+ });
60
+
61
+ if (!inkInstance) {
62
+ const canPatchConsole = typeof console !== "undefined" && typeof console.Console === "function";
63
+ const renderOptions: Parameters<typeof render>[1] = {
64
+ exitOnCtrlC: options.exitOnCtrlC ?? true,
65
+ patchConsole: options.patchConsole ?? canPatchConsole
66
+ };
67
+
68
+ if (options.stdin) {
69
+ renderOptions.stdin = options.stdin;
70
+ }
71
+ if (options.stdout) {
72
+ renderOptions.stdout = options.stdout;
73
+ }
74
+ if (options.stderr) {
75
+ renderOptions.stderr = options.stderr;
76
+ }
77
+
78
+ inkInstance = render(element, renderOptions);
79
+ } else {
80
+ inkInstance.clear();
81
+ inkInstance.rerender(element);
82
+ }
83
+ };
84
+
85
+ const handleMessage = (message: A2uiServerMessage) => {
86
+ switch (message.type) {
87
+ case "beginRendering": {
88
+ const surface = ensureSurface(message.surfaceId);
89
+ surface.catalogId = message.catalogId;
90
+ surface.renderReady = true;
91
+ renderSurfaces();
92
+ break;
93
+ }
94
+ case "surfaceUpdate": {
95
+ const surface = ensureSurface(message.surfaceId);
96
+ surface.rootComponentId = message.rootComponentId;
97
+ surface.components = message.components.reduce<Record<string, ComponentDef>>(
98
+ (acc: Record<string, ComponentDef>, component) => {
99
+ const normalized = normalizeComponentDef(component);
100
+ if (normalized) {
101
+ acc[normalized.id] = normalized;
102
+ }
103
+ return acc;
104
+ },
105
+ {}
106
+ );
107
+ renderSurfaces();
108
+ break;
109
+ }
110
+ case "dataModelUpdate": {
111
+ const surface = ensureSurface(message.surfaceId);
112
+ if (message.dataModel) {
113
+ surface.dataModel = mergeDataModel(surface.dataModel, message.dataModel);
114
+ } else if (message.contents) {
115
+ surface.dataModel = applyDataModelUpdate(surface.dataModel, message.path, message.contents);
116
+ }
117
+ renderSurfaces();
118
+ break;
119
+ }
120
+ case "deleteSurface": {
121
+ surfaces.delete(message.surfaceId);
122
+ renderSurfaces();
123
+ break;
124
+ }
125
+ default:
126
+ break;
127
+ }
128
+ };
129
+
130
+ const dispose = () => {
131
+ inkInstance?.unmount();
132
+ inkInstance = null;
133
+ };
134
+
135
+ return { handleMessage, dispose };
136
+ }
137
+
138
+ const A2uiRoot: React.FC<{
139
+ surface: SurfaceState | null;
140
+ onUserAction?: (action: A2uiUserAction) => void;
141
+ onLocalDataModelUpdate?: (surfaceId: string, path: string, value: unknown, node: ResolvedNode) => void;
142
+ }> = ({ surface, onUserAction, onLocalDataModelUpdate }) => {
143
+ if (!surface || !surface.rootComponentId) {
144
+ return React.createElement(Text, null, "No surface");
145
+ }
146
+
147
+ const tree = buildResolvedTree(surface.components, surface.rootComponentId, surface.dataModel);
148
+ if (!tree) {
149
+ return React.createElement(Text, null, "Invalid surface");
150
+ }
151
+
152
+ return React.createElement(
153
+ FocusProvider,
154
+ null,
155
+ React.createElement(FocusInputHandler),
156
+ React.createElement(
157
+ RootContainer,
158
+ null,
159
+ React.createElement(RenderTree, {
160
+ surfaceId: surface.surfaceId,
161
+ tree,
162
+ dataModel: surface.dataModel,
163
+ onUserAction,
164
+ onLocalDataModelUpdate
165
+ })
166
+ )
167
+ );
168
+ };
169
+
170
+ const FocusInputHandler: React.FC = () => {
171
+ const focus = useFocusRegistry();
172
+
173
+ useInput((input: string, key: Key) => {
174
+ if (key.tab) {
175
+ if (key.shift) {
176
+ focus.focusPrev();
177
+ } else {
178
+ focus.focusNext();
179
+ }
180
+ return;
181
+ }
182
+
183
+ focus.handleKey(input, key);
184
+ });
185
+
186
+ return null;
187
+ };
188
+
189
+ const RootContainer: React.FC<{ children: React.ReactNode }> = ({ children }) => {
190
+ const { stdout } = useStdout();
191
+ const width = stdout?.columns ?? undefined;
192
+ const height = stdout?.rows ?? undefined;
193
+
194
+ return React.createElement(Box, { width, height, flexDirection: "column" }, children);
195
+ };
196
+
197
+ const RenderTree: React.FC<{
198
+ surfaceId: string;
199
+ tree: ResolvedNode;
200
+ dataModel: Record<string, unknown>;
201
+ onUserAction?: (action: A2uiUserAction) => void;
202
+ onLocalDataModelUpdate?: (surfaceId: string, path: string, value: unknown, node: ResolvedNode) => void;
203
+ }> = ({ surfaceId, tree, dataModel, onUserAction, onLocalDataModelUpdate }) => {
204
+ const dispatchAction = (action: ActionDef, node: ResolvedNode, value?: unknown) => {
205
+ const context = {
206
+ ...resolveActionContext(action.context, dataModel, node.bindingContext),
207
+ ...(node.bindingContext?.index !== undefined ? { index: node.bindingContext.index } : {}),
208
+ ...(node.bindingContext?.item !== undefined ? { item: node.bindingContext.item } : {})
209
+ } as Record<string, unknown>;
210
+
211
+ onUserAction?.({
212
+ type: "userAction",
213
+ surfaceId,
214
+ componentId: node.id,
215
+ actionId: action.actionId,
216
+ context,
217
+ value
218
+ });
219
+ };
220
+
221
+ const updateLocalDataModel = (path: string, value: unknown, node: ResolvedNode) => {
222
+ onLocalDataModelUpdate?.(surfaceId, path, value, node);
223
+ };
224
+
225
+ return renderNode(tree, { dispatchAction, updateLocalDataModel });
226
+ };
227
+
228
+ function resolveActionContext(
229
+ context: Record<string, unknown> | undefined,
230
+ dataModel: Record<string, unknown>,
231
+ bindingContext: ResolvedNode["bindingContext"]
232
+ ): Record<string, unknown> {
233
+ if (!context) {
234
+ return {};
235
+ }
236
+ const resolved: Record<string, unknown> = {};
237
+ for (const [key, value] of Object.entries(context)) {
238
+ if (isBoundValueLike(value)) {
239
+ resolved[key] = resolveBoundValue(value as BoundValue, dataModel, bindingContext);
240
+ } else {
241
+ resolved[key] = value;
242
+ }
243
+ }
244
+ return resolved;
245
+ }
246
+
247
+ function isBoundValueLike(value: unknown): boolean {
248
+ if (!value || typeof value !== "object") {
249
+ return false;
250
+ }
251
+ return (
252
+ "path" in (value as Record<string, unknown>) ||
253
+ "literalString" in (value as Record<string, unknown>) ||
254
+ "literalNumber" in (value as Record<string, unknown>) ||
255
+ "literalBoolean" in (value as Record<string, unknown>) ||
256
+ "literalObject" in (value as Record<string, unknown>) ||
257
+ "literalArray" in (value as Record<string, unknown>)
258
+ );
259
+ }
260
+
261
+ function mergeDataModel(
262
+ base: Record<string, unknown>,
263
+ update: Record<string, unknown>
264
+ ): Record<string, unknown> {
265
+ const result: Record<string, unknown> = { ...base };
266
+ for (const [key, value] of Object.entries(update)) {
267
+ if (isPlainObject(value) && isPlainObject(result[key])) {
268
+ result[key] = mergeDataModel(result[key] as Record<string, unknown>, value as Record<string, unknown>);
269
+ } else {
270
+ result[key] = value;
271
+ }
272
+ }
273
+ return result;
274
+ }
275
+
276
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
277
+ return !!value && typeof value === "object" && !Array.isArray(value);
278
+ }
279
+
280
+ function normalizeComponentDef(component: unknown): ComponentDef | null {
281
+ if (!component || typeof component !== "object") {
282
+ return null;
283
+ }
284
+
285
+ const record = component as Record<string, unknown>;
286
+ if (typeof record.id !== "string") {
287
+ return null;
288
+ }
289
+
290
+ if (typeof record.type === "string") {
291
+ const normalizedProps = normalizeBoundValuesDeep(record.props as Record<string, unknown> | undefined) as
292
+ | Record<string, unknown>
293
+ | undefined;
294
+ if (normalizedProps) {
295
+ if (normalizedProps.action && !normalizedProps.onPress && record.type === "Button") {
296
+ normalizedProps.onPress = normalizeAction(normalizedProps.action);
297
+ delete normalizedProps.action;
298
+ }
299
+ if (normalizedProps.action && !normalizedProps.onChange && (record.type === "Checkbox" || record.type === "CheckBox")) {
300
+ normalizedProps.onChange = normalizeAction(normalizedProps.action);
301
+ delete normalizedProps.action;
302
+ }
303
+ }
304
+
305
+ return {
306
+ id: record.id,
307
+ type: record.type,
308
+ props: normalizedProps,
309
+ children: normalizeChildren(record.children as ComponentDef["children"] | undefined)
310
+ };
311
+ }
312
+
313
+ if (record.component && typeof record.component === "object") {
314
+ const [type, props] = extractComponentType(record.component as Record<string, unknown>);
315
+ if (!type) {
316
+ return null;
317
+ }
318
+ const { normalizedType, normalizedProps, children } = normalizeComponentShape(type, props);
319
+ if (!normalizedType) {
320
+ return null;
321
+ }
322
+ return {
323
+ id: record.id,
324
+ type: normalizedType,
325
+ props: normalizedProps,
326
+ children
327
+ };
328
+ }
329
+
330
+ return null;
331
+ }
332
+
333
+ function extractComponentType(componentObject: Record<string, unknown>): [string | null, Record<string, unknown>] {
334
+ if (typeof componentObject.type === "string") {
335
+ return [componentObject.type, (componentObject.props as Record<string, unknown>) ?? {}];
336
+ }
337
+
338
+ const entries = Object.entries(componentObject);
339
+ if (entries.length === 1) {
340
+ const [type, props] = entries[0];
341
+ return [type, (props as Record<string, unknown>) ?? {}];
342
+ }
343
+
344
+ const candidate = entries.find(([key, value]) => typeof key === "string" && typeof value === "object");
345
+ if (candidate) {
346
+ return [candidate[0], (candidate[1] as Record<string, unknown>) ?? {}];
347
+ }
348
+
349
+ return [null, {}];
350
+ }
351
+
352
+ function normalizeComponentShape(type: string, props: Record<string, unknown>) {
353
+ const nextProps = normalizeBoundValuesDeep({ ...(props ?? {}) }) as Record<string, unknown>;
354
+ let children = undefined as ComponentDef["children"] | undefined;
355
+
356
+ if (nextProps.children) {
357
+ children = normalizeChildren(nextProps.children as ComponentDef["children"]);
358
+ delete nextProps.children;
359
+ }
360
+
361
+ if (nextProps.child) {
362
+ children = normalizeChildren({ explicitList: [nextProps.child as string] });
363
+ delete nextProps.child;
364
+ }
365
+
366
+ if (nextProps.action && !nextProps.onPress && type === "Button") {
367
+ nextProps.onPress = normalizeAction(nextProps.action);
368
+ delete nextProps.action;
369
+ }
370
+
371
+ if (nextProps.action && !nextProps.onChange && (type === "Checkbox" || type === "CheckBox")) {
372
+ nextProps.onChange = normalizeAction(nextProps.action);
373
+ delete nextProps.action;
374
+ }
375
+
376
+ if (type === "Text" && nextProps.usageHint && nextProps.bold === undefined) {
377
+ const hint = String(nextProps.usageHint).toLowerCase();
378
+ if (hint === "h1" || hint === "h2" || hint === "h3") {
379
+ nextProps.bold = true;
380
+ }
381
+ }
382
+
383
+ if (type === "Row" || type === "Column") {
384
+ const direction = type === "Row" ? "row" : "column";
385
+ const alignItems = mapAlignment(nextProps.alignment as string | undefined);
386
+ const justifyContent = mapDistribution(nextProps.distribution as string | undefined);
387
+ return {
388
+ normalizedType: "Box",
389
+ normalizedProps: {
390
+ ...nextProps,
391
+ direction,
392
+ alignItems,
393
+ justifyContent
394
+ },
395
+ children
396
+ };
397
+ }
398
+
399
+ if (type === "Card") {
400
+ return {
401
+ normalizedType: "Box",
402
+ normalizedProps: {
403
+ direction: "column",
404
+ borderStyle: "round",
405
+ padding: 1,
406
+ ...nextProps
407
+ },
408
+ children
409
+ };
410
+ }
411
+
412
+ if (type === "Divider") {
413
+ return {
414
+ normalizedType: "Divider",
415
+ normalizedProps: nextProps,
416
+ children: undefined
417
+ };
418
+ }
419
+
420
+ if (type === "Icon") {
421
+ return {
422
+ normalizedType: "Text",
423
+ normalizedProps: {
424
+ text: resolveIconText(nextProps.name)
425
+ },
426
+ children: undefined
427
+ };
428
+ }
429
+
430
+ if (type === "Image") {
431
+ return {
432
+ normalizedType: "Image",
433
+ normalizedProps: nextProps,
434
+ children: undefined
435
+ };
436
+ }
437
+
438
+ if (type === "TextField") {
439
+ const textValue = nextProps.value ?? nextProps.text;
440
+ return {
441
+ normalizedType: "TextField",
442
+ normalizedProps: {
443
+ label: nextProps.label,
444
+ value: textValue,
445
+ placeholder: nextProps.placeholder ?? resolveLiteralString(nextProps.label),
446
+ onChange: nextProps.onChange,
447
+ onSubmit: nextProps.onSubmit
448
+ },
449
+ children: undefined
450
+ };
451
+ }
452
+
453
+ if (type === "CheckBox") {
454
+ return {
455
+ normalizedType: "Checkbox",
456
+ normalizedProps: {
457
+ label: nextProps.label,
458
+ checked: nextProps.checked ?? nextProps.value,
459
+ onChange: nextProps.onChange ?? normalizeAction(nextProps.action)
460
+ },
461
+ children: undefined
462
+ };
463
+ }
464
+
465
+ if (type === "MultipleChoice") {
466
+ return {
467
+ normalizedType: "MultipleChoice",
468
+ normalizedProps: {
469
+ label: nextProps.label,
470
+ items: normalizeMultipleChoiceOptions(nextProps.options ?? []),
471
+ onSelect: normalizeAction(nextProps.action)
472
+ },
473
+ children: undefined
474
+ };
475
+ }
476
+
477
+ if (type === "Slider") {
478
+ return {
479
+ normalizedType: "Slider",
480
+ normalizedProps: {
481
+ label: nextProps.label,
482
+ min: nextProps.min ?? nextProps.minValue,
483
+ max: nextProps.max ?? nextProps.maxValue,
484
+ step: nextProps.step,
485
+ value: nextProps.value,
486
+ onChange: nextProps.onChange ?? normalizeAction(nextProps.action)
487
+ },
488
+ children: undefined
489
+ };
490
+ }
491
+
492
+ if (type === "Tabs") {
493
+ const tabItems = Array.isArray(nextProps.tabItems) ? nextProps.tabItems : [];
494
+ const tabLabels = tabItems.map((item) => resolveLiteralString((item as Record<string, unknown>)?.title) ?? "Tab");
495
+ if (!children && tabItems.length) {
496
+ const childIds = tabItems
497
+ .map((item) => (item as Record<string, unknown>)?.child)
498
+ .filter((entry): entry is string => typeof entry === "string");
499
+ if (childIds.length) {
500
+ children = { explicitList: childIds };
501
+ }
502
+ }
503
+ return {
504
+ normalizedType: "Tabs",
505
+ normalizedProps: {
506
+ tabs: tabLabels,
507
+ selectedIndex: nextProps.selectedIndex ?? 0,
508
+ onChange: nextProps.onChange ?? normalizeAction(nextProps.action)
509
+ },
510
+ children
511
+ };
512
+ }
513
+
514
+ if (type === "Modal") {
515
+ return {
516
+ normalizedType: "Modal",
517
+ normalizedProps: nextProps,
518
+ children
519
+ };
520
+ }
521
+
522
+
523
+ return {
524
+ normalizedType: type,
525
+ normalizedProps: nextProps,
526
+ children
527
+ };
528
+ }
529
+
530
+ function normalizeChildren(children: ComponentDef["children"] | undefined): ComponentDef["children"] | undefined {
531
+ if (!children || typeof children !== "object") return children;
532
+ const record = children as Record<string, unknown>;
533
+ if (Array.isArray(record.explicitList)) {
534
+ return { explicitList: record.explicitList as string[] };
535
+ }
536
+ if (record.template && typeof record.template === "object") {
537
+ const template = record.template as Record<string, unknown>;
538
+ const dataBinding = template.dataBinding;
539
+ return {
540
+ template: {
541
+ componentId: template.componentId as string,
542
+ dataBinding: typeof dataBinding === "string" ? dataBinding : (dataBinding as Record<string, unknown>)
543
+ }
544
+ };
545
+ }
546
+ return children;
547
+ }
548
+
549
+ function normalizeAction(action: unknown): ActionDef | undefined {
550
+ if (!action || typeof action !== "object") return undefined;
551
+ const record = action as Record<string, unknown>;
552
+ if (record.actionId && typeof record.actionId === "string") {
553
+ return { actionId: record.actionId, context: record.context as Record<string, unknown> };
554
+ }
555
+ if (record.name && typeof record.name === "string") {
556
+ return {
557
+ actionId: record.name,
558
+ context: normalizeActionContext(record.context)
559
+ };
560
+ }
561
+ return undefined;
562
+ }
563
+
564
+ function normalizeActionContext(context: unknown): Record<string, unknown> | undefined {
565
+ if (!context) return undefined;
566
+ if (Array.isArray(context)) {
567
+ const result: Record<string, unknown> = {};
568
+ context.forEach((entry) => {
569
+ if (!entry || typeof entry !== "object") return;
570
+ const record = entry as Record<string, unknown>;
571
+ if (!record.key) return;
572
+ result[String(record.key)] = record.value ?? record.literal ?? record;
573
+ });
574
+ return result;
575
+ }
576
+ if (typeof context === "object") {
577
+ return context as Record<string, unknown>;
578
+ }
579
+ return undefined;
580
+ }
581
+
582
+ function normalizeBoundValuesDeep(value: unknown): Record<string, unknown> | undefined {
583
+ if (!value || typeof value !== "object") {
584
+ return value as Record<string, unknown> | undefined;
585
+ }
586
+ if (Array.isArray(value)) {
587
+ return value.map((item) => normalizeBoundValuesDeep(item)) as unknown as Record<string, unknown>;
588
+ }
589
+ if (isBoundValueLike(value)) {
590
+ return value as Record<string, unknown>;
591
+ }
592
+ const result: Record<string, unknown> = {};
593
+ Object.entries(value as Record<string, unknown>).forEach(([key, entry]) => {
594
+ result[key] = normalizeBoundValuesDeep(entry);
595
+ });
596
+ return result;
597
+ }
598
+
599
+ function resolveLiteralString(value: unknown): string | undefined {
600
+ if (!value || typeof value !== "object") return undefined;
601
+ const record = value as Record<string, unknown>;
602
+ if (typeof record.literalString === "string") {
603
+ return record.literalString;
604
+ }
605
+ return undefined;
606
+ }
607
+
608
+ function normalizeMultipleChoiceOptions(options: unknown): unknown {
609
+ if (!Array.isArray(options)) {
610
+ return options;
611
+ }
612
+ return options.map((option) => {
613
+ if (!option || typeof option !== "object") {
614
+ return option;
615
+ }
616
+ const record = option as Record<string, unknown>;
617
+ const label = resolveLiteralString(record.label) ?? record.label;
618
+ return {
619
+ ...record,
620
+ label
621
+ };
622
+ });
623
+ }
624
+
625
+ function resolveIconText(nameBinding: unknown): { literalString?: string; path?: string } {
626
+ const map: Record<string, string> = {
627
+ flight: "✈",
628
+ airplane: "✈",
629
+ plane: "✈",
630
+ calendar: "📅",
631
+ time: "⏰",
632
+ favorite: "★",
633
+ star: "★"
634
+ };
635
+
636
+ if (!nameBinding || typeof nameBinding !== "object") {
637
+ return { literalString: "•" };
638
+ }
639
+
640
+ const record = nameBinding as Record<string, unknown>;
641
+ if (typeof record.literalString === "string") {
642
+ const key = record.literalString.toLowerCase();
643
+ return { literalString: map[key] ?? record.literalString };
644
+ }
645
+
646
+ if (typeof record.path === "string") {
647
+ return { path: record.path };
648
+ }
649
+
650
+ return { literalString: "•" };
651
+ }
652
+
653
+ function mapAlignment(alignment?: string): "flex-start" | "flex-end" | "center" | "stretch" | undefined {
654
+ if (!alignment) return undefined;
655
+ const normalized = alignment.toLowerCase();
656
+ switch (normalized) {
657
+ case "start":
658
+ case "flexstart":
659
+ case "flex-start":
660
+ return "flex-start";
661
+ case "end":
662
+ case "flexend":
663
+ case "flex-end":
664
+ return "flex-end";
665
+ case "center":
666
+ return "center";
667
+ case "stretch":
668
+ return "stretch";
669
+ default:
670
+ return undefined;
671
+ }
672
+ }
673
+
674
+ function mapDistribution(distribution?: string):
675
+ | "flex-start"
676
+ | "flex-end"
677
+ | "center"
678
+ | "space-between"
679
+ | "space-around"
680
+ | undefined {
681
+ if (!distribution) return undefined;
682
+ const normalized = distribution.toLowerCase();
683
+ switch (normalized) {
684
+ case "spacebetween":
685
+ case "space-between":
686
+ return "space-between";
687
+ case "spacearound":
688
+ case "space-around":
689
+ return "space-around";
690
+ case "center":
691
+ return "center";
692
+ case "start":
693
+ case "flexstart":
694
+ case "flex-start":
695
+ return "flex-start";
696
+ case "end":
697
+ case "flexend":
698
+ case "flex-end":
699
+ return "flex-end";
700
+ default:
701
+ return undefined;
702
+ }
703
+ }
704
+
705
+ function applyDataModelUpdate(
706
+ existing: Record<string, unknown>,
707
+ path: string | undefined,
708
+ contents: DataModelEntry[]
709
+ ): Record<string, unknown> {
710
+ const next = { ...(existing ?? {}) };
711
+ const updatedValue = buildValueFromContents(contents);
712
+ const parts = normalizePath(path);
713
+
714
+ if (!parts.length) {
715
+ if (isPlainObject(updatedValue)) {
716
+ return { ...next, ...updatedValue };
717
+ }
718
+ return (updatedValue as Record<string, unknown>) ?? next;
719
+ }
720
+
721
+ let cursor: Record<string, unknown> = next;
722
+ for (let i = 0; i < parts.length - 1; i += 1) {
723
+ const key = parts[i];
724
+ if (!cursor[key] || typeof cursor[key] !== "object") {
725
+ cursor[key] = {};
726
+ }
727
+ cursor = cursor[key] as Record<string, unknown>;
728
+ }
729
+ cursor[parts[parts.length - 1]] = updatedValue;
730
+ return next;
731
+ }
732
+
733
+ function normalizePath(path?: string): string[] {
734
+ if (!path || path === "/") {
735
+ return [];
736
+ }
737
+ const trimmed = String(path).replace(/^\/+/, "");
738
+ if (!trimmed) {
739
+ return [];
740
+ }
741
+ return trimmed.split("/").filter(Boolean);
742
+ }
743
+
744
+ function buildValueFromContents(contents: DataModelEntry[] | unknown): unknown {
745
+ if (!Array.isArray(contents)) {
746
+ return contents;
747
+ }
748
+ const result: Record<string, unknown> = {};
749
+ contents.forEach((entry) => {
750
+ if (!entry || typeof entry !== "object") {
751
+ return;
752
+ }
753
+ result[entry.key] = extractTypedValue(entry);
754
+ });
755
+ return result;
756
+ }
757
+
758
+ function extractTypedValue(entry: DataModelEntry): unknown {
759
+ if ("valueString" in entry) return entry.valueString;
760
+ if ("valueNumber" in entry) return entry.valueNumber;
761
+ if ("valueBoolean" in entry) return entry.valueBoolean;
762
+ if ("valueNull" in entry) return null;
763
+ if ("valueMap" in entry) return buildValueFromContents(entry.valueMap ?? []);
764
+ if ("valueArray" in entry) return normalizeValueArray(entry.valueArray ?? []);
765
+ if ("value" in entry) return entry.value;
766
+ return undefined;
767
+ }
768
+
769
+ function normalizeValueArray(valueArray: Array<DataModelEntry | unknown>): unknown[] {
770
+ return valueArray.map((item) => {
771
+ if (item && typeof item === "object" && "key" in (item as Record<string, unknown>)) {
772
+ return extractTypedValue(item as DataModelEntry);
773
+ }
774
+ if (item && typeof item === "object" && ("valueString" in (item as Record<string, unknown>) || "valueNumber" in (item as Record<string, unknown>) || "valueBoolean" in (item as Record<string, unknown>) || "valueMap" in (item as Record<string, unknown>) || "valueArray" in (item as Record<string, unknown>) || "valueNull" in (item as Record<string, unknown>))) {
775
+ return extractTypedValue(item as DataModelEntry);
776
+ }
777
+ return item as unknown;
778
+ });
779
+ }