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,1403 @@
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
+ import { Terminal } from "xterm";
3
+ import { FitAddon } from "xterm-addon-fit";
4
+ import { PassThrough } from "stream";
5
+ import { createA2uiInkRenderer } from "../../../src/index.ts";
6
+ import type { A2uiUserAction } from "../../../src/index.ts";
7
+ import process from "process";
8
+ import "xterm/css/xterm.css";
9
+
10
+ const navSections = [
11
+ {
12
+ title: "Getting Started",
13
+ items: [
14
+ { id: "overview", label: "Overview" },
15
+ { id: "installation", label: "Installation" },
16
+ { id: "quick-start", label: "Quick Start" }
17
+ ]
18
+ },
19
+ {
20
+ title: "Components",
21
+ items: [
22
+ { id: "text", label: "Text" },
23
+ { id: "box", label: "Box" },
24
+ { id: "spacer", label: "Spacer" },
25
+ { id: "button", label: "Button" },
26
+ { id: "input", label: "Input" },
27
+ { id: "textfield", label: "TextField" },
28
+ { id: "select", label: "Select" },
29
+ { id: "multiple-choice", label: "MultipleChoice" },
30
+ { id: "checkbox", label: "Checkbox" },
31
+ { id: "radiogroup", label: "RadioGroup" },
32
+ { id: "slider", label: "Slider" },
33
+ { id: "list", label: "List" },
34
+ { id: "tabs", label: "Tabs" },
35
+ { id: "table", label: "Table" },
36
+ { id: "modal", label: "Modal" },
37
+ { id: "image", label: "Image" }
38
+ ]
39
+ },
40
+ {
41
+ title: "Playground",
42
+ items: [{ id: "playground", label: "JSON Playground" }]
43
+ }
44
+ ] as const;
45
+
46
+ type DemoSurface = {
47
+ surfaceId?: string;
48
+ rootComponentId?: string;
49
+ catalogId?: string;
50
+ components?: Array<Record<string, unknown>>;
51
+ dataModel?: Record<string, unknown>;
52
+ };
53
+
54
+ type ActionEntry = A2uiUserAction & Record<string, unknown>;
55
+
56
+ type ComponentChildDef = {
57
+ explicitList?: string[];
58
+ };
59
+
60
+ type ComponentDef = {
61
+ id: string;
62
+ type: string;
63
+ props?: Record<string, unknown>;
64
+ children?: ComponentChildDef;
65
+ };
66
+
67
+ const defaultJson: DemoSurface = {
68
+ surfaceId: "demo",
69
+ rootComponentId: "root",
70
+ components: [
71
+ {
72
+ id: "root",
73
+ type: "Box",
74
+ props: {
75
+ direction: "column",
76
+ padding: 1,
77
+ borderStyle: "round"
78
+ },
79
+ children: {
80
+ explicitList: ["title", "greeting"]
81
+ }
82
+ },
83
+ {
84
+ id: "title",
85
+ type: "Text",
86
+ props: {
87
+ text: { literalString: "Welcome to A2UI!" },
88
+ bold: true
89
+ }
90
+ },
91
+ {
92
+ id: "greeting",
93
+ type: "Text",
94
+ props: {
95
+ text: { path: "message" }
96
+ }
97
+ }
98
+ ],
99
+ dataModel: {
100
+ message: "Edit this JSON and click Render!"
101
+ }
102
+ };
103
+
104
+ const exampleJson: DemoSurface = {
105
+ surfaceId: "demo",
106
+ rootComponentId: "root",
107
+ components: [
108
+ {
109
+ id: "root",
110
+ type: "Box",
111
+ props: {
112
+ direction: "column",
113
+ padding: 1,
114
+ borderStyle: "round"
115
+ },
116
+ children: {
117
+ explicitList: [
118
+ "title",
119
+ "spacer1",
120
+ "inputSection",
121
+ "spacer2",
122
+ "statusSection",
123
+ "spacer3",
124
+ "sliderLabel",
125
+ "slider",
126
+ "imagePreview",
127
+ "submit"
128
+ ]
129
+ }
130
+ },
131
+ {
132
+ id: "title",
133
+ type: "Text",
134
+ props: {
135
+ text: { path: "title" },
136
+ bold: true
137
+ }
138
+ },
139
+ { id: "spacer1", type: "Spacer" },
140
+ {
141
+ id: "inputSection",
142
+ type: "Box",
143
+ props: { direction: "column", borderStyle: "single", paddingX: 1 },
144
+ children: { explicitList: ["nameLabel", "nameInput", "emailField", "menuChoice", "checkbox"] }
145
+ },
146
+ {
147
+ id: "nameLabel",
148
+ type: "Text",
149
+ props: { text: { literalString: "Enter your name:" } }
150
+ },
151
+ {
152
+ id: "nameInput",
153
+ type: "Input",
154
+ props: {
155
+ value: { path: "form.name" },
156
+ placeholder: "Your name here",
157
+ onChange: { actionId: "nameChange" },
158
+ onSubmit: { actionId: "nameSubmit" }
159
+ }
160
+ },
161
+ {
162
+ id: "emailField",
163
+ type: "TextField",
164
+ props: {
165
+ label: "Email",
166
+ value: { path: "form.email" },
167
+ onChange: { actionId: "emailChange" }
168
+ }
169
+ },
170
+ {
171
+ id: "menuChoice",
172
+ type: "MultipleChoice",
173
+ props: {
174
+ label: "Meal",
175
+ items: [
176
+ { label: "Pizza", value: "pizza" },
177
+ { label: "Sushi", value: "sushi" },
178
+ { label: "Salad", value: "salad" }
179
+ ],
180
+ onSelect: { actionId: "mealSelect" }
181
+ }
182
+ },
183
+ {
184
+ id: "checkbox",
185
+ type: "Checkbox",
186
+ props: {
187
+ label: "Subscribe to newsletter",
188
+ checked: { path: "form.subscribe" },
189
+ onChange: { actionId: "toggleSubscribe" }
190
+ }
191
+ },
192
+ { id: "spacer2", type: "Spacer" },
193
+ {
194
+ id: "statusSection",
195
+ type: "Box",
196
+ props: { direction: "column", borderStyle: "single", paddingX: 1 },
197
+ children: { explicitList: ["statusLabel", "statusTable"] }
198
+ },
199
+ {
200
+ id: "statusLabel",
201
+ type: "Text",
202
+ props: { text: { literalString: "System Status:" } }
203
+ },
204
+ {
205
+ id: "statusTable",
206
+ type: "Table",
207
+ props: {
208
+ columns: { path: "status.columns" },
209
+ rows: { path: "status.rows" }
210
+ }
211
+ },
212
+ { id: "spacer3", type: "Spacer" },
213
+ {
214
+ id: "sliderLabel",
215
+ type: "Text",
216
+ props: { text: { literalString: "Order priority" } }
217
+ },
218
+ {
219
+ id: "slider",
220
+ type: "Slider",
221
+ props: { min: 1, max: 5, step: 1, value: { path: "form.priority" }, onChange: { actionId: "priorityChange" } }
222
+ },
223
+ {
224
+ id: "imagePreview",
225
+ type: "Image",
226
+ props: { label: "Menu image", url: "https://example.com/menu.png" }
227
+ },
228
+ {
229
+ id: "submit",
230
+ type: "Button",
231
+ props: { label: { literalString: "Submit" }, onPress: { actionId: "submit" } }
232
+ }
233
+ ],
234
+ dataModel: {
235
+ title: "A2UI Demo Form",
236
+ form: {
237
+ name: "John Doe",
238
+ email: "john@example.com",
239
+ subscribe: true,
240
+ priority: 3
241
+ },
242
+ status: {
243
+ columns: [
244
+ { key: "service", header: "Service" },
245
+ { key: "status", header: "Status" }
246
+ ],
247
+ rows: [
248
+ { service: "API", status: "Online" },
249
+ { service: "Database", status: "Online" },
250
+ { service: "Cache", status: "Warning" }
251
+ ]
252
+ }
253
+ }
254
+ };
255
+
256
+ const SIMPLE_SURFACE_ID = "demo";
257
+ const SIMPLE_ROOT_ID = "root";
258
+
259
+ export default function App() {
260
+ const [activeSection, setActiveSection] = useState<string>("overview");
261
+ const [jsonText, setJsonText] = useState<string>(() => JSON.stringify(defaultJson, null, 2));
262
+ const [inputMode, setInputMode] = useState<"full" | "simple">("full");
263
+ const [componentsText, setComponentsText] = useState<string>(
264
+ () => JSON.stringify(defaultJson.components ?? [], null, 2)
265
+ );
266
+ const [dataModelText, setDataModelText] = useState<string>(
267
+ () => JSON.stringify(defaultJson.dataModel ?? {}, null, 2)
268
+ );
269
+ const [actions, setActions] = useState<ActionEntry[]>([]);
270
+ const terminalRef = useRef<HTMLDivElement | null>(null);
271
+ const termInstanceRef = useRef<Terminal | null>(null);
272
+ const fitAddonRef = useRef<FitAddon | null>(null);
273
+ const rendererRef = useRef<ReturnType<typeof createA2uiInkRenderer> | null>(null);
274
+ const currentSurfaceIdRef = useRef<string>("demo");
275
+
276
+ const defaultJsonText = useMemo(() => JSON.stringify(defaultJson, null, 2), []);
277
+ const exampleJsonText = useMemo(() => JSON.stringify(exampleJson, null, 2), []);
278
+ const defaultComponentsText = useMemo(() => JSON.stringify(defaultJson.components ?? [], null, 2), []);
279
+ const defaultDataModelText = useMemo(() => JSON.stringify(defaultJson.dataModel ?? {}, null, 2), []);
280
+ const exampleComponentsText = useMemo(() => JSON.stringify(exampleJson.components ?? [], null, 2), []);
281
+ const exampleDataModelText = useMemo(() => JSON.stringify(exampleJson.dataModel ?? {}, null, 2), []);
282
+
283
+ const safeParseJson = <T,>(text: string) => {
284
+ try {
285
+ return { ok: true, value: JSON.parse(text) as T };
286
+ } catch (error) {
287
+ const message = error instanceof Error ? error.message : "Unknown JSON error";
288
+ return { ok: false, error: message };
289
+ }
290
+ };
291
+
292
+ useEffect(() => {
293
+ const handleHash = () => {
294
+ const sectionId = window.location.hash.replace("#", "");
295
+ if (sectionId) {
296
+ setActiveSection(sectionId);
297
+ }
298
+ };
299
+
300
+ handleHash();
301
+ window.addEventListener("hashchange", handleHash);
302
+ return () => window.removeEventListener("hashchange", handleHash);
303
+ }, []);
304
+
305
+ useEffect(() => {
306
+ if (!terminalRef.current || termInstanceRef.current) {
307
+ return;
308
+ }
309
+
310
+ const term = new Terminal({
311
+ theme: {
312
+ background: "#000000",
313
+ foreground: "#eaeaea",
314
+ cursor: "#00d9ff",
315
+ cursorAccent: "#000000",
316
+ selection: "rgba(0, 217, 255, 0.3)",
317
+ black: "#000000",
318
+ red: "#ff4757",
319
+ green: "#00ff88",
320
+ yellow: "#ffd700",
321
+ blue: "#00d9ff",
322
+ magenta: "#7b2cbf",
323
+ cyan: "#00d9ff",
324
+ white: "#eaeaea",
325
+ brightBlack: "#6c757d",
326
+ brightRed: "#ff6b81",
327
+ brightGreen: "#00ff88",
328
+ brightYellow: "#ffd700",
329
+ brightBlue: "#00d9ff",
330
+ brightMagenta: "#a855f7",
331
+ brightCyan: "#00d9ff",
332
+ brightWhite: "#ffffff"
333
+ },
334
+ fontFamily: '"Fira Code", "Consolas", monospace',
335
+ fontSize: 14,
336
+ lineHeight: 1.2,
337
+ cursorBlink: true,
338
+ cursorStyle: "bar",
339
+ scrollback: 2000,
340
+ convertEol: true
341
+ });
342
+
343
+ const fitAddon = new FitAddon();
344
+ term.loadAddon(fitAddon);
345
+ term.open(terminalRef.current);
346
+ fitAddon.fit();
347
+ fitAddonRef.current = fitAddon;
348
+ term.focus();
349
+ termInstanceRef.current = term;
350
+
351
+ term.attachCustomKeyEventHandler((event) => {
352
+ if (event.key === "Tab") {
353
+ event.preventDefault();
354
+ }
355
+ return true;
356
+ });
357
+
358
+ const stdout = new PassThrough();
359
+ const stderr = new PassThrough();
360
+ const stdin = new PassThrough();
361
+
362
+ process.stdout = stdout as unknown as NodeJS.WriteStream;
363
+ process.stderr = stderr as unknown as NodeJS.WriteStream;
364
+ process.stdin = stdin as unknown as NodeJS.ReadStream;
365
+ process.env = process.env || {};
366
+ process.env.TERM = process.env.TERM || "xterm-256color";
367
+
368
+ stdout.isTTY = true;
369
+ stderr.isTTY = true;
370
+ stdin.isTTY = true;
371
+
372
+ stdout.ref = () => {};
373
+ stdout.unref = () => {};
374
+ stderr.ref = () => {};
375
+ stderr.unref = () => {};
376
+ stdin.ref = () => {};
377
+ stdin.unref = () => {};
378
+
379
+ stdout.hasColors = () => true;
380
+ stderr.hasColors = () => true;
381
+ stdout.getColorDepth = () => 24;
382
+ stderr.getColorDepth = () => 24;
383
+
384
+ stdout.columns = term.cols;
385
+ stdout.rows = term.rows;
386
+ stderr.columns = term.cols;
387
+ stderr.rows = term.rows;
388
+
389
+ stdin.setRawMode = () => {};
390
+ stdin.resume = () => {};
391
+ stdin.pause = () => {};
392
+
393
+ stdout.on("data", (chunk: Buffer) => {
394
+ term.write(chunk.toString());
395
+ });
396
+
397
+ stderr.on("data", (chunk: Buffer) => {
398
+ term.write(`\x1b[31m${chunk.toString()}\x1b[0m`);
399
+ });
400
+
401
+ term.onData((data) => {
402
+ stdin.write(data);
403
+ });
404
+
405
+ term.onResize(({ cols, rows }) => {
406
+ stdout.columns = cols;
407
+ stdout.rows = rows;
408
+ stderr.columns = cols;
409
+ stderr.rows = rows;
410
+ stdout.emit("resize");
411
+ stderr.emit("resize");
412
+ stdin.emit("resize");
413
+ fitAddon.fit();
414
+ });
415
+
416
+ rendererRef.current = createA2uiInkRenderer({
417
+ stdout: stdout as unknown as NodeJS.WriteStream,
418
+ stderr: stderr as unknown as NodeJS.WriteStream,
419
+ stdin: stdin as unknown as NodeJS.ReadStream,
420
+ exitOnCtrlC: false,
421
+ patchConsole: false,
422
+ onUserAction: (action) => {
423
+ setActions((prev) => [...prev, action as ActionEntry]);
424
+ }
425
+ });
426
+
427
+ window.addEventListener("resize", () => fitAddon.fit());
428
+
429
+ return () => {
430
+ rendererRef.current?.dispose?.();
431
+ rendererRef.current = null;
432
+ term.dispose();
433
+ termInstanceRef.current = null;
434
+ };
435
+ }, []);
436
+
437
+ useEffect(() => {
438
+ if (activeSection === "playground") {
439
+ setTimeout(() => fitAddonRef.current?.fit(), 0);
440
+ }
441
+ }, [activeSection]);
442
+
443
+ useEffect(() => {
444
+ setJsonText(defaultJsonText);
445
+ setComponentsText(defaultComponentsText);
446
+ setDataModelText(defaultDataModelText);
447
+ }, [defaultJsonText, defaultComponentsText, defaultDataModelText]);
448
+
449
+ useEffect(() => {
450
+ if (inputMode === "simple") {
451
+ const parsed = safeParseJson<DemoSurface>(jsonText);
452
+ if (parsed.ok) {
453
+ setComponentsText(JSON.stringify(parsed.value.components ?? [], null, 2));
454
+ setDataModelText(JSON.stringify(parsed.value.dataModel ?? {}, null, 2));
455
+ }
456
+ return;
457
+ }
458
+
459
+ const parsedComponents = safeParseJson<unknown>(componentsText);
460
+ const parsedDataModel = safeParseJson<Record<string, unknown>>(dataModelText);
461
+ setJsonText(
462
+ JSON.stringify(
463
+ {
464
+ surfaceId: SIMPLE_SURFACE_ID,
465
+ rootComponentId: SIMPLE_ROOT_ID,
466
+ components: parsedComponents.ok ? parsedComponents.value : [],
467
+ dataModel: parsedDataModel.ok ? parsedDataModel.value : {}
468
+ },
469
+ null,
470
+ 2
471
+ )
472
+ );
473
+ }, [inputMode]);
474
+
475
+ const handleNavigate = (sectionId: string) => {
476
+ setActiveSection(sectionId);
477
+ window.location.hash = sectionId;
478
+ };
479
+
480
+ const renderSurface = (surface: DemoSurface) => {
481
+ if (!rendererRef.current || !termInstanceRef.current) {
482
+ return;
483
+ }
484
+
485
+ if (!surface.components || !Array.isArray(surface.components)) {
486
+ termInstanceRef.current.write("\x1b[31mJSON must include a components array.\x1b[0m\r\n");
487
+ return;
488
+ }
489
+
490
+ const surfaceId = surface.surfaceId || "demo";
491
+ currentSurfaceIdRef.current = surfaceId;
492
+ termInstanceRef.current.clear();
493
+
494
+ rendererRef.current.handleMessage({
495
+ type: "surfaceUpdate",
496
+ surfaceId,
497
+ rootComponentId: surface.rootComponentId || "root",
498
+ components: surface.components as ComponentDef[]
499
+ });
500
+
501
+ rendererRef.current.handleMessage({
502
+ type: "dataModelUpdate",
503
+ surfaceId,
504
+ dataModel: surface.dataModel || {}
505
+ });
506
+
507
+ rendererRef.current.handleMessage({
508
+ type: "beginRendering",
509
+ surfaceId,
510
+ catalogId: surface.catalogId
511
+ });
512
+ };
513
+
514
+ const renderFromText = (text: string) => {
515
+ const parsed = safeParseJson<DemoSurface>(text);
516
+ if (!parsed.ok) {
517
+ termInstanceRef.current?.write(`\x1b[31mInvalid JSON: ${parsed.error}\x1b[0m\r\n`);
518
+ return;
519
+ }
520
+ renderSurface(parsed.value);
521
+ };
522
+
523
+ const renderFromSimpleInputs = () => {
524
+ const componentsParsed = safeParseJson<unknown>(componentsText);
525
+ if (!componentsParsed.ok) {
526
+ termInstanceRef.current?.write(`\x1b[31mInvalid components JSON: ${componentsParsed.error}\x1b[0m\r\n`);
527
+ return;
528
+ }
529
+
530
+ const dataModelParsed = safeParseJson<Record<string, unknown>>(dataModelText);
531
+ if (!dataModelParsed.ok) {
532
+ termInstanceRef.current?.write(`\x1b[31mInvalid dataModel JSON: ${dataModelParsed.error}\x1b[0m\r\n`);
533
+ return;
534
+ }
535
+
536
+ const componentValue = componentsParsed.value as unknown;
537
+ const componentsArray = Array.isArray(componentValue)
538
+ ? componentValue
539
+ : (componentValue as { components?: unknown }).components;
540
+
541
+ if (!Array.isArray(componentsArray)) {
542
+ termInstanceRef.current?.write("\x1b[31mComponents must be an array.\x1b[0m\r\n");
543
+ return;
544
+ }
545
+
546
+ renderSurface({
547
+ surfaceId: SIMPLE_SURFACE_ID,
548
+ rootComponentId: SIMPLE_ROOT_ID,
549
+ components: componentsArray as ComponentDef[],
550
+ dataModel: dataModelParsed.value
551
+ });
552
+ };
553
+
554
+ const handleRender = () => {
555
+ if (inputMode === "simple") {
556
+ renderFromSimpleInputs();
557
+ } else {
558
+ renderFromText(jsonText);
559
+ }
560
+ };
561
+
562
+ const handleLoadExample = () => {
563
+ if (inputMode === "simple") {
564
+ setComponentsText(exampleComponentsText);
565
+ setDataModelText(exampleDataModelText);
566
+ renderFromSimpleInputs();
567
+ } else {
568
+ setJsonText(exampleJsonText);
569
+ renderFromText(exampleJsonText);
570
+ }
571
+ };
572
+
573
+ const handleClearTerminal = () => {
574
+ termInstanceRef.current?.clear();
575
+ };
576
+
577
+ return (
578
+ <div className="app">
579
+ <aside className="sidebar">
580
+ <div className="logo">
581
+ <h1>A2UI Ink</h1>
582
+ <span className="version">v0.1.0</span>
583
+ </div>
584
+
585
+ <nav className="nav">
586
+ {navSections.map((section) => (
587
+ <div className="nav-section" key={section.title}>
588
+ <h3>{section.title}</h3>
589
+ <ul>
590
+ {section.items.map((item) => (
591
+ <li key={item.id}>
592
+ <button
593
+ type="button"
594
+ className={`nav-link ${activeSection === item.id ? "active" : ""}`}
595
+ onClick={() => handleNavigate(item.id)}
596
+ >
597
+ {item.label}
598
+ </button>
599
+ </li>
600
+ ))}
601
+ </ul>
602
+ </div>
603
+ ))}
604
+ </nav>
605
+ </aside>
606
+
607
+ <main className="main-content">
608
+ <div className="content-wrapper">
609
+ <section id="overview" className={`section ${activeSection === "overview" ? "active" : ""}`}>
610
+ <h2>Overview</h2>
611
+ <p className="lead">
612
+ A2UI Ink renders A2UI v0.8 surfaces in a terminal using Ink. It&apos;s a lightweight client that
613
+ accepts A2UI JSON messages, builds a UI tree from the adjacency list, resolves data bindings,
614
+ renders the result, and emits <code>userAction</code> events.
615
+ </p>
616
+
617
+ <div className="feature-grid">
618
+ <div className="feature-card">
619
+ <div className="feature-icon">📦</div>
620
+ <h4>Component-Based</h4>
621
+ <p>Build terminal UIs with declarative JSON components</p>
622
+ </div>
623
+ <div className="feature-card">
624
+ <div className="feature-icon">🔗</div>
625
+ <h4>Data Binding</h4>
626
+ <p>Bind component props to your data model with path expressions</p>
627
+ </div>
628
+ <div className="feature-card">
629
+ <div className="feature-icon">⚡</div>
630
+ <h4>Reactive</h4>
631
+ <p>UI updates automatically when data changes</p>
632
+ </div>
633
+ <div className="feature-card">
634
+ <div className="feature-icon">🔌</div>
635
+ <h4>Transport Agnostic</h4>
636
+ <p>Works with any A2UI message source (MCP, WebSocket, etc.)</p>
637
+ </div>
638
+ </div>
639
+
640
+ <h3>Message Flow</h3>
641
+ <div className="message-flow">
642
+ <div className="flow-step">
643
+ <span className="step-num">1</span>
644
+ <code>surfaceUpdate</code>
645
+ <p>Define component catalog</p>
646
+ </div>
647
+ <div className="flow-arrow">→</div>
648
+ <div className="flow-step">
649
+ <span className="step-num">2</span>
650
+ <code>dataModelUpdate</code>
651
+ <p>Provide data for bindings</p>
652
+ </div>
653
+ <div className="flow-arrow">→</div>
654
+ <div className="flow-step">
655
+ <span className="step-num">3</span>
656
+ <code>beginRendering</code>
657
+ <p>Start rendering the surface</p>
658
+ </div>
659
+ </div>
660
+ </section>
661
+
662
+ <section id="installation" className={`section ${activeSection === "installation" ? "active" : ""}`}>
663
+ <h2>Installation</h2>
664
+ <div className="code-block">
665
+ <pre><code>npm install a2uink</code></pre>
666
+ </div>
667
+
668
+ <h3>Peer Dependencies</h3>
669
+ <p>A2UI Ink requires the following peer dependencies:</p>
670
+ <div className="code-block">
671
+ <pre><code>{`{
672
+ "ink": "^4.0.0",
673
+ "react": "^18.2.0"
674
+ }`}</code></pre>
675
+ </div>
676
+ </section>
677
+
678
+ <section id="quick-start" className={`section ${activeSection === "quick-start" ? "active" : ""}`}>
679
+ <h2>Quick Start</h2>
680
+ <p>Here&apos;s a minimal example to get you started:</p>
681
+ <div className="code-block">
682
+ <pre><code>{`import { createA2uiInkRenderer } from "a2uink";
683
+
684
+ // 1. Create the renderer
685
+ const renderer = createA2uiInkRenderer({
686
+ onUserAction: (action) => {
687
+ // Forward actions to your server/agent
688
+ sendAction(action);
689
+ }
690
+ });
691
+
692
+ // 2. Handle incoming messages
693
+ receiveMessage((message) => {
694
+ renderer.handleMessage(message);
695
+ });
696
+
697
+ // 3. Send a surface update
698
+ renderer.handleMessage({
699
+ type: "surfaceUpdate",
700
+ surfaceId: "main",
701
+ rootComponentId: "root",
702
+ components: [
703
+ {
704
+ id: "root",
705
+ type: "Box",
706
+ props: { direction: "column" },
707
+ children: { explicitList: ["greeting"] }
708
+ },
709
+ {
710
+ id: "greeting",
711
+ type: "Text",
712
+ props: { text: { path: "message" } }
713
+ }
714
+ ]
715
+ });
716
+
717
+ // 4. Send data
718
+ renderer.handleMessage({
719
+ type: "dataModelUpdate",
720
+ surfaceId: "main",
721
+ dataModel: { message: "Hello, A2UI!" }
722
+ });
723
+
724
+ // 5. Start rendering
725
+ renderer.handleMessage({
726
+ type: "beginRendering",
727
+ surfaceId: "main"
728
+ });`}</code></pre>
729
+ </div>
730
+ </section>
731
+
732
+ <section id="text" className={`section ${activeSection === "text" ? "active" : ""}`}>
733
+ <h2>Text</h2>
734
+ <p className="component-desc">Renders styled text content. Supports bold, italic, underline, strikethrough, and color styling.</p>
735
+ <h3>Props</h3>
736
+ <table className="props-table">
737
+ <thead>
738
+ <tr><th>Prop</th><th>Type</th><th>Description</th></tr>
739
+ </thead>
740
+ <tbody>
741
+ <tr><td><code>text</code></td><td>BoundValue</td><td>The text content to display</td></tr>
742
+ <tr><td><code>bold</code></td><td>boolean</td><td>Make text bold</td></tr>
743
+ <tr><td><code>italic</code></td><td>boolean</td><td>Make text italic</td></tr>
744
+ <tr><td><code>underline</code></td><td>boolean</td><td>Underline the text</td></tr>
745
+ <tr><td><code>strikethrough</code></td><td>boolean</td><td>Strikethrough the text</td></tr>
746
+ <tr><td><code>color</code></td><td>string</td><td>Text color (e.g., "green", "red", "#ff0")</td></tr>
747
+ </tbody>
748
+ </table>
749
+ <h3>Example</h3>
750
+ <div className="example-container">
751
+ <div className="code-block">
752
+ <pre><code>{`{
753
+ "id": "greeting",
754
+ "type": "Text",
755
+ "props": {
756
+ "text": { "path": "message" },
757
+ "bold": true,
758
+ "color": "green"
759
+ }
760
+ }`}</code></pre>
761
+ </div>
762
+ <div className="terminal-preview" data-component="text">
763
+ <div className="terminal-output">
764
+ <span className="term-bold term-green">Hello, World!</span>
765
+ </div>
766
+ </div>
767
+ </div>
768
+ </section>
769
+
770
+ <section id="box" className={`section ${activeSection === "box" ? "active" : ""}`}>
771
+ <h2>Box</h2>
772
+ <p className="component-desc">A flexible container component for layout. Supports flexbox-style properties and borders.</p>
773
+ <h3>Props</h3>
774
+ <table className="props-table">
775
+ <thead>
776
+ <tr><th>Prop</th><th>Type</th><th>Description</th></tr>
777
+ </thead>
778
+ <tbody>
779
+ <tr><td><code>direction</code></td><td>"row" | "column"</td><td>Flex direction</td></tr>
780
+ <tr><td><code>padding</code></td><td>number</td><td>Padding on all sides</td></tr>
781
+ <tr><td><code>paddingX</code></td><td>number</td><td>Horizontal padding</td></tr>
782
+ <tr><td><code>paddingY</code></td><td>number</td><td>Vertical padding</td></tr>
783
+ <tr><td><code>borderStyle</code></td><td>string</td><td>"single", "double", "round", "bold", etc.</td></tr>
784
+ <tr><td><code>borderColor</code></td><td>string</td><td>Border color</td></tr>
785
+ </tbody>
786
+ </table>
787
+ <h3>Example</h3>
788
+ <div className="example-container">
789
+ <div className="code-block">
790
+ <pre><code>{`{
791
+ "id": "container",
792
+ "type": "Box",
793
+ "props": {
794
+ "direction": "column",
795
+ "padding": 1,
796
+ "borderStyle": "round"
797
+ },
798
+ "children": {
799
+ "explicitList": ["child1", "child2"]
800
+ }
801
+ }`}</code></pre>
802
+ </div>
803
+ <div className="terminal-preview" data-component="box">
804
+ <div className="terminal-output">
805
+ <pre>{"╭─────────────────────╮\n│ Child 1 │\n│ Child 2 │\n╰─────────────────────╯"}</pre>
806
+ </div>
807
+ </div>
808
+ </div>
809
+ </section>
810
+
811
+ <section id="spacer" className={`section ${activeSection === "spacer" ? "active" : ""}`}>
812
+ <h2>Spacer</h2>
813
+ <p className="component-desc">Adds vertical spacing between components.</p>
814
+ <h3>Props</h3>
815
+ <table className="props-table">
816
+ <thead>
817
+ <tr><th>Prop</th><th>Type</th><th>Description</th></tr>
818
+ </thead>
819
+ <tbody>
820
+ <tr><td><code>—</code></td><td>—</td><td>No props</td></tr>
821
+ </tbody>
822
+ </table>
823
+ <h3>Example</h3>
824
+ <div className="example-container">
825
+ <div className="code-block">
826
+ <pre><code>{`{
827
+ "id": "gap",
828
+ "type": "Spacer"
829
+ }`}</code></pre>
830
+ </div>
831
+ <div className="terminal-preview" data-component="spacer">
832
+ <div className="terminal-output">
833
+ <pre>{"Line above\n\nLine below"}</pre>
834
+ </div>
835
+ </div>
836
+ </div>
837
+ </section>
838
+
839
+ <section id="button" className={`section ${activeSection === "button" ? "active" : ""}`}>
840
+ <h2>Button</h2>
841
+ <p className="component-desc">An interactive button that emits actions when clicked or focused.</p>
842
+ <h3>Props</h3>
843
+ <table className="props-table">
844
+ <thead>
845
+ <tr><th>Prop</th><th>Type</th><th>Description</th></tr>
846
+ </thead>
847
+ <tbody>
848
+ <tr><td><code>label</code></td><td>BoundValue</td><td>Button label text</td></tr>
849
+ <tr><td><code>onPress</code></td><td>ActionDef</td><td>Action to emit when pressed</td></tr>
850
+ <tr><td><code>disabled</code></td><td>boolean</td><td>Disable the button</td></tr>
851
+ </tbody>
852
+ </table>
853
+ <h3>Example</h3>
854
+ <div className="example-container">
855
+ <div className="code-block">
856
+ <pre><code>{`{
857
+ "id": "submitBtn",
858
+ "type": "Button",
859
+ "props": {
860
+ "label": { "literalString": "Submit" },
861
+ "onPress": { "actionId": "submit" }
862
+ }
863
+ }`}</code></pre>
864
+ </div>
865
+ <div className="terminal-preview" data-component="button">
866
+ <div className="terminal-output">
867
+ <span className="term-button">[ Submit ]</span>
868
+ </div>
869
+ </div>
870
+ </div>
871
+ </section>
872
+
873
+ <section id="input" className={`section ${activeSection === "input" ? "active" : ""}`}>
874
+ <h2>Input</h2>
875
+ <p className="component-desc">A text input field for capturing user input.</p>
876
+ <h3>Props</h3>
877
+ <table className="props-table">
878
+ <thead>
879
+ <tr><th>Prop</th><th>Type</th><th>Description</th></tr>
880
+ </thead>
881
+ <tbody>
882
+ <tr><td><code>value</code></td><td>BoundValue</td><td>Current input value</td></tr>
883
+ <tr><td><code>placeholder</code></td><td>string</td><td>Placeholder text</td></tr>
884
+ <tr><td><code>onChange</code></td><td>ActionDef</td><td>Action on value change</td></tr>
885
+ <tr><td><code>onSubmit</code></td><td>ActionDef</td><td>Action on enter key</td></tr>
886
+ </tbody>
887
+ </table>
888
+ <h3>Example</h3>
889
+ <div className="example-container">
890
+ <div className="code-block">
891
+ <pre><code>{`{
892
+ "id": "nameInput",
893
+ "type": "Input",
894
+ "props": {
895
+ "value": { "path": "form.name" },
896
+ "placeholder": "Enter your name",
897
+ "onChange": { "actionId": "nameChange" },
898
+ "onSubmit": { "actionId": "nameSubmit" }
899
+ }
900
+ }`}</code></pre>
901
+ </div>
902
+ <div className="terminal-preview" data-component="input">
903
+ <div className="terminal-output">
904
+ <span className="term-input">John Doe<span className="cursor">│</span></span>
905
+ </div>
906
+ </div>
907
+ </div>
908
+ </section>
909
+
910
+ <section id="textfield" className={`section ${activeSection === "textfield" ? "active" : ""}`}>
911
+ <h2>TextField</h2>
912
+ <p className="component-desc">Composer-friendly input with label and bound text.</p>
913
+ <h3>Props</h3>
914
+ <table className="props-table">
915
+ <thead>
916
+ <tr><th>Prop</th><th>Type</th><th>Description</th></tr>
917
+ </thead>
918
+ <tbody>
919
+ <tr><td><code>label</code></td><td>BoundValue</td><td>Field label</td></tr>
920
+ <tr><td><code>text</code></td><td>BoundValue</td><td>Bound input value</td></tr>
921
+ <tr><td><code>action</code></td><td>ActionDef</td><td>Action on change</td></tr>
922
+ </tbody>
923
+ </table>
924
+ <h3>Example</h3>
925
+ <div className="example-container">
926
+ <div className="code-block">
927
+ <pre><code>{`{
928
+ "id": "email",
929
+ "component": {
930
+ "TextField": {
931
+ "label": { "literalString": "Email" },
932
+ "text": { "path": "/form/email" },
933
+ "textFieldType": "shortText"
934
+ }
935
+ }
936
+ }`}</code></pre>
937
+ </div>
938
+ <div className="terminal-preview" data-component="textfield">
939
+ <div className="terminal-output">
940
+ <span className="term-label">Email</span>
941
+ <span className="term-input">name@example.com<span className="cursor">│</span></span>
942
+ </div>
943
+ </div>
944
+ </div>
945
+ </section>
946
+
947
+ <section id="select" className={`section ${activeSection === "select" ? "active" : ""}`}>
948
+ <h2>Select</h2>
949
+ <p className="component-desc">A dropdown-style selection component.</p>
950
+ <h3>Props</h3>
951
+ <table className="props-table">
952
+ <thead>
953
+ <tr><th>Prop</th><th>Type</th><th>Description</th></tr>
954
+ </thead>
955
+ <tbody>
956
+ <tr><td><code>items</code></td><td>BoundValue (array)</td><td>Array of options</td></tr>
957
+ <tr><td><code>selectedIndex</code></td><td>BoundValue (number)</td><td>Currently selected index</td></tr>
958
+ <tr><td><code>onSelect</code></td><td>ActionDef</td><td>Action on selection change</td></tr>
959
+ </tbody>
960
+ </table>
961
+ <h3>Example</h3>
962
+ <div className="example-container">
963
+ <div className="code-block">
964
+ <pre><code>{`{
965
+ "id": "colorSelect",
966
+ "type": "Select",
967
+ "props": {
968
+ "items": { "path": "colors" },
969
+ "selectedIndex": { "path": "selectedColor" },
970
+ "onSelect": { "actionId": "colorChange" }
971
+ }
972
+ }`}</code></pre>
973
+ </div>
974
+ <div className="terminal-preview" data-component="select">
975
+ <div className="terminal-output">
976
+ <pre>▶ Red
977
+ Green
978
+ Blue</pre>
979
+ </div>
980
+ </div>
981
+ </div>
982
+ </section>
983
+
984
+ <section id="multiple-choice" className={`section ${activeSection === "multiple-choice" ? "active" : ""}`}>
985
+ <h2>MultipleChoice</h2>
986
+ <p className="component-desc">Composer selection component mapped to Ink Select.</p>
987
+ <h3>Props</h3>
988
+ <table className="props-table">
989
+ <thead>
990
+ <tr><th>Prop</th><th>Type</th><th>Description</th></tr>
991
+ </thead>
992
+ <tbody>
993
+ <tr><td><code>options</code></td><td>array</td><td>Selectable options</td></tr>
994
+ <tr><td><code>selections</code></td><td>BoundValue</td><td>Selected value(s)</td></tr>
995
+ <tr><td><code>action</code></td><td>ActionDef</td><td>Action on selection</td></tr>
996
+ </tbody>
997
+ </table>
998
+ <h3>Example</h3>
999
+ <div className="example-container">
1000
+ <div className="code-block">
1001
+ <pre><code>{`{
1002
+ "id": "menu",
1003
+ "component": {
1004
+ "MultipleChoice": {
1005
+ "options": [
1006
+ { "label": { "literalString": "Pizza" }, "value": "pizza" },
1007
+ { "label": { "literalString": "Sushi" }, "value": "sushi" }
1008
+ ],
1009
+ "maxAllowedSelections": 1
1010
+ }
1011
+ }
1012
+ }`}</code></pre>
1013
+ </div>
1014
+ <div className="terminal-preview" data-component="multiple-choice">
1015
+ <div className="terminal-output">
1016
+ <pre>▶ Pizza
1017
+ Sushi</pre>
1018
+ </div>
1019
+ </div>
1020
+ </div>
1021
+ </section>
1022
+
1023
+ <section id="checkbox" className={`section ${activeSection === "checkbox" ? "active" : ""}`}>
1024
+ <h2>Checkbox</h2>
1025
+ <p className="component-desc">A checkbox for boolean input.</p>
1026
+ <h3>Props</h3>
1027
+ <table className="props-table">
1028
+ <thead>
1029
+ <tr><th>Prop</th><th>Type</th><th>Description</th></tr>
1030
+ </thead>
1031
+ <tbody>
1032
+ <tr><td><code>label</code></td><td>string</td><td>Checkbox label</td></tr>
1033
+ <tr><td><code>checked</code></td><td>BoundValue (boolean)</td><td>Checked state</td></tr>
1034
+ <tr><td><code>onChange</code></td><td>ActionDef</td><td>Action on toggle</td></tr>
1035
+ </tbody>
1036
+ </table>
1037
+ <h3>Example</h3>
1038
+ <div className="example-container">
1039
+ <div className="code-block">
1040
+ <pre><code>{`{
1041
+ "id": "enableFeature",
1042
+ "type": "Checkbox",
1043
+ "props": {
1044
+ "label": "Enable notifications",
1045
+ "checked": { "path": "settings.notifications" },
1046
+ "onChange": { "actionId": "toggleNotifications" }
1047
+ }
1048
+ }`}</code></pre>
1049
+ </div>
1050
+ <div className="terminal-preview" data-component="checkbox">
1051
+ <div className="terminal-output">
1052
+ <span className="term-green">[x]</span> Enable notifications
1053
+ </div>
1054
+ </div>
1055
+ </div>
1056
+ </section>
1057
+
1058
+ <section id="radiogroup" className={`section ${activeSection === "radiogroup" ? "active" : ""}`}>
1059
+ <h2>RadioGroup</h2>
1060
+ <p className="component-desc">A group of mutually exclusive radio options.</p>
1061
+ <h3>Props</h3>
1062
+ <table className="props-table">
1063
+ <thead>
1064
+ <tr><th>Prop</th><th>Type</th><th>Description</th></tr>
1065
+ </thead>
1066
+ <tbody>
1067
+ <tr><td><code>options</code></td><td>BoundValue (array)</td><td>Array of option labels</td></tr>
1068
+ <tr><td><code>selectedIndex</code></td><td>BoundValue (number)</td><td>Selected option index</td></tr>
1069
+ <tr><td><code>onChange</code></td><td>ActionDef</td><td>Action on selection change</td></tr>
1070
+ </tbody>
1071
+ </table>
1072
+ <h3>Example</h3>
1073
+ <div className="example-container">
1074
+ <div className="code-block">
1075
+ <pre><code>{`{
1076
+ "id": "sizeRadio",
1077
+ "type": "RadioGroup",
1078
+ "props": {
1079
+ "options": { "path": "sizeOptions" },
1080
+ "selectedIndex": { "path": "selectedSize" },
1081
+ "onChange": { "actionId": "sizeChange" }
1082
+ }
1083
+ }`}</code></pre>
1084
+ </div>
1085
+ <div className="terminal-preview" data-component="radiogroup">
1086
+ <div className="terminal-output">
1087
+ <pre> ( ) Small
1088
+ ▶ (o) Medium
1089
+ ( ) Large</pre>
1090
+ </div>
1091
+ </div>
1092
+ </div>
1093
+ </section>
1094
+
1095
+ <section id="slider" className={`section ${activeSection === "slider" ? "active" : ""}`}>
1096
+ <h2>Slider</h2>
1097
+ <p className="component-desc">An interactive slider for numeric values.</p>
1098
+ <h3>Props</h3>
1099
+ <table className="props-table">
1100
+ <thead>
1101
+ <tr><th>Prop</th><th>Type</th><th>Description</th></tr>
1102
+ </thead>
1103
+ <tbody>
1104
+ <tr><td><code>min</code></td><td>number</td><td>Minimum value</td></tr>
1105
+ <tr><td><code>max</code></td><td>number</td><td>Maximum value</td></tr>
1106
+ <tr><td><code>step</code></td><td>number</td><td>Step size</td></tr>
1107
+ <tr><td><code>value</code></td><td>BoundValue</td><td>Current value</td></tr>
1108
+ <tr><td><code>onChange</code></td><td>ActionDef</td><td>Action on change</td></tr>
1109
+ </tbody>
1110
+ </table>
1111
+ <h3>Example</h3>
1112
+ <div className="example-container">
1113
+ <div className="code-block">
1114
+ <pre><code>{`{
1115
+ "id": "priority",
1116
+ "type": "Slider",
1117
+ "props": {
1118
+ "min": 1,
1119
+ "max": 5,
1120
+ "step": 1,
1121
+ "value": { "path": "form.priority" },
1122
+ "onChange": { "actionId": "priorityChange" }
1123
+ }
1124
+ }`}</code></pre>
1125
+ </div>
1126
+ <div className="terminal-preview" data-component="slider">
1127
+ <div className="terminal-output">
1128
+ <pre>██████░░░░░░░░░░░░░ 3</pre>
1129
+ </div>
1130
+ </div>
1131
+ </div>
1132
+ </section>
1133
+
1134
+ <section id="list" className={`section ${activeSection === "list" ? "active" : ""}`}>
1135
+ <h2>List</h2>
1136
+ <p className="component-desc">Renders a bulleted list of items.</p>
1137
+ <h3>Props</h3>
1138
+ <table className="props-table">
1139
+ <thead>
1140
+ <tr><th>Prop</th><th>Type</th><th>Description</th></tr>
1141
+ </thead>
1142
+ <tbody>
1143
+ <tr><td><code>items</code></td><td>BoundValue (array)</td><td>Array of list items</td></tr>
1144
+ </tbody>
1145
+ </table>
1146
+ <h3>Example</h3>
1147
+ <div className="example-container">
1148
+ <div className="code-block">
1149
+ <pre><code>{`{
1150
+ "id": "todoList",
1151
+ "type": "List",
1152
+ "props": {
1153
+ "items": { "path": "todos" }
1154
+ }
1155
+ }`}</code></pre>
1156
+ </div>
1157
+ <div className="terminal-preview" data-component="list">
1158
+ <div className="terminal-output">
1159
+ <pre>{"• Buy groceries\n• Walk the dog\n• Write code"}</pre>
1160
+ </div>
1161
+ </div>
1162
+ </div>
1163
+ </section>
1164
+
1165
+ <section id="tabs" className={`section ${activeSection === "tabs" ? "active" : ""}`}>
1166
+ <h2>Tabs</h2>
1167
+ <p className="component-desc">A tabbed interface for organizing content into panels.</p>
1168
+ <h3>Props</h3>
1169
+ <table className="props-table">
1170
+ <thead>
1171
+ <tr><th>Prop</th><th>Type</th><th>Description</th></tr>
1172
+ </thead>
1173
+ <tbody>
1174
+ <tr><td><code>tabs</code></td><td>BoundValue (array)</td><td>Array of tab labels</td></tr>
1175
+ <tr><td><code>selectedIndex</code></td><td>BoundValue (number)</td><td>Active tab index</td></tr>
1176
+ <tr><td><code>onChange</code></td><td>ActionDef</td><td>Action on tab change</td></tr>
1177
+ </tbody>
1178
+ </table>
1179
+ <h3>Example</h3>
1180
+ <div className="example-container">
1181
+ <div className="code-block">
1182
+ <pre><code>{`{
1183
+ "id": "mainTabs",
1184
+ "type": "Tabs",
1185
+ "props": {
1186
+ "tabs": { "path": "tabLabels" },
1187
+ "selectedIndex": { "path": "activeTab" },
1188
+ "onChange": { "actionId": "tabChange" }
1189
+ },
1190
+ "children": {
1191
+ "explicitList": ["panel1", "panel2", "panel3"]
1192
+ }
1193
+ }`}</code></pre>
1194
+ </div>
1195
+ <div className="terminal-preview" data-component="tabs">
1196
+ <div className="terminal-output">
1197
+ <pre>
1198
+ <span className="term-cyan">Overview</span>
1199
+ {" Settings Help\nWelcome to the overview panel!"}
1200
+ </pre>
1201
+ </div>
1202
+ </div>
1203
+ </div>
1204
+ </section>
1205
+
1206
+ <section id="table" className={`section ${activeSection === "table" ? "active" : ""}`}>
1207
+ <h2>Table</h2>
1208
+ <p className="component-desc">Renders tabular data with columns and rows.</p>
1209
+ <h3>Props</h3>
1210
+ <table className="props-table">
1211
+ <thead>
1212
+ <tr><th>Prop</th><th>Type</th><th>Description</th></tr>
1213
+ </thead>
1214
+ <tbody>
1215
+ <tr><td><code>columns</code></td><td>BoundValue (array)</td><td>Array of column definitions</td></tr>
1216
+ <tr><td><code>rows</code></td><td>BoundValue (array)</td><td>Array of row data objects</td></tr>
1217
+ </tbody>
1218
+ </table>
1219
+ <h3>Example</h3>
1220
+ <div className="example-container">
1221
+ <div className="code-block">
1222
+ <pre><code>{`{
1223
+ "id": "statusTable",
1224
+ "type": "Table",
1225
+ "props": {
1226
+ "columns": { "path": "table.columns" },
1227
+ "rows": { "path": "table.rows" }
1228
+ }
1229
+ }`}</code></pre>
1230
+ </div>
1231
+ <div className="terminal-preview" data-component="table">
1232
+ <div className="terminal-output">
1233
+ <pre>
1234
+ {"Name Status\nAPI "}
1235
+ <span className="term-green">OK</span>
1236
+ {"\nDatabase "}
1237
+ <span className="term-yellow">Warn</span>
1238
+ {"\nCache "}
1239
+ <span className="term-green">OK</span>
1240
+ </pre>
1241
+ </div>
1242
+ </div>
1243
+ </div>
1244
+ </section>
1245
+
1246
+ <section id="modal" className={`section ${activeSection === "modal" ? "active" : ""}`}>
1247
+ <h2>Modal</h2>
1248
+ <p className="component-desc">A boxed container for important content.</p>
1249
+ <h3>Props</h3>
1250
+ <table className="props-table">
1251
+ <thead>
1252
+ <tr><th>Prop</th><th>Type</th><th>Description</th></tr>
1253
+ </thead>
1254
+ <tbody>
1255
+ <tr><td><code>title</code></td><td>string</td><td>Modal title</td></tr>
1256
+ <tr><td><code>description</code></td><td>string</td><td>Modal body text</td></tr>
1257
+ </tbody>
1258
+ </table>
1259
+ <h3>Example</h3>
1260
+ <div className="example-container">
1261
+ <div className="code-block">
1262
+ <pre><code>{`{
1263
+ "id": "confirm",
1264
+ "type": "Modal",
1265
+ "props": {
1266
+ "title": "Order placed",
1267
+ "description": "Your order is on its way."
1268
+ },
1269
+ "children": { "explicitList": ["modalAction"] }
1270
+ }`}</code></pre>
1271
+ </div>
1272
+ <div className="terminal-preview" data-component="modal">
1273
+ <div className="terminal-output">
1274
+ <pre>Order placed
1275
+ Your order is on its way.</pre>
1276
+ </div>
1277
+ </div>
1278
+ </div>
1279
+ </section>
1280
+
1281
+ <section id="image" className={`section ${activeSection === "image" ? "active" : ""}`}>
1282
+ <h2>Image</h2>
1283
+ <p className="component-desc">Displays an image URL label in Ink.</p>
1284
+ <h3>Example</h3>
1285
+ <div className="example-container">
1286
+ <div className="code-block">
1287
+ <pre><code>{`{
1288
+ "id": "heroImage",
1289
+ "type": "Image",
1290
+ "props": {
1291
+ "label": "Menu",
1292
+ "url": "https://example.com/menu.png"
1293
+ }
1294
+ }`}</code></pre>
1295
+ </div>
1296
+ <div className="terminal-preview" data-component="image">
1297
+ <div className="terminal-output">
1298
+ <pre>[Image] https://example.com/menu.png</pre>
1299
+ </div>
1300
+ </div>
1301
+ </div>
1302
+ </section>
1303
+
1304
+ <section id="playground" className={`section ${activeSection === "playground" ? "active" : ""}`}>
1305
+ <h2>JSON Playground</h2>
1306
+ <p className="lead">Paste your A2UI JSON to preview how it renders in the terminal.</p>
1307
+ <p className="component-desc">Simple Mode accepts a components array and a dataModel object. It uses surface ID <code>demo</code> and root ID <code>root</code>.</p>
1308
+
1309
+ <div className="playground-container">
1310
+ <div className="playground-editor">
1311
+ <div className="editor-header">
1312
+ <span>A2UI JSON</span>
1313
+ <div className="editor-actions">
1314
+ <div className="editor-mode">
1315
+ <button
1316
+ type="button"
1317
+ className={`btn btn-secondary btn-sm ${inputMode === "full" ? "is-active" : ""}`}
1318
+ onClick={() => setInputMode("full")}
1319
+ >
1320
+ Full JSON
1321
+ </button>
1322
+ <button
1323
+ type="button"
1324
+ className={`btn btn-secondary btn-sm ${inputMode === "simple" ? "is-active" : ""}`}
1325
+ onClick={() => setInputMode("simple")}
1326
+ >
1327
+ Simple Mode
1328
+ </button>
1329
+ </div>
1330
+ <button type="button" className="btn btn-secondary" onClick={handleLoadExample}>Load Example</button>
1331
+ <button type="button" className="btn btn-primary" onClick={handleRender}>Render</button>
1332
+ </div>
1333
+ </div>
1334
+ {inputMode === "full" ? (
1335
+ <textarea
1336
+ id="jsonEditor"
1337
+ className="editor-textarea"
1338
+ spellCheck={false}
1339
+ value={jsonText}
1340
+ onChange={(event) => setJsonText(event.target.value)}
1341
+ />
1342
+ ) : (
1343
+ <div className="simple-editor-grid">
1344
+ <label className="editor-field">
1345
+ <span>Components (array)</span>
1346
+ <textarea
1347
+ className="editor-textarea"
1348
+ spellCheck={false}
1349
+ value={componentsText}
1350
+ onChange={(event) => setComponentsText(event.target.value)}
1351
+ />
1352
+ </label>
1353
+ <label className="editor-field">
1354
+ <span>dataModel (object)</span>
1355
+ <textarea
1356
+ className="editor-textarea"
1357
+ spellCheck={false}
1358
+ value={dataModelText}
1359
+ onChange={(event) => setDataModelText(event.target.value)}
1360
+ />
1361
+ </label>
1362
+ </div>
1363
+ )}
1364
+ </div>
1365
+
1366
+ <div className="playground-preview">
1367
+ <div className="preview-header">
1368
+ <span>Terminal Preview</span>
1369
+ <div className="preview-actions">
1370
+ <button type="button" className="btn btn-secondary btn-sm" onClick={handleClearTerminal}>Clear</button>
1371
+ </div>
1372
+ </div>
1373
+ <div ref={terminalRef} id="terminal" className="terminal-container" />
1374
+ <div className="terminal-help">
1375
+ <strong>Ink interactivity:</strong>
1376
+ <code>Tab</code> / <code>Shift+Tab</code> to move focus, <code>Enter</code> or <code>Space</code> to activate,
1377
+ type directly in inputs.
1378
+ </div>
1379
+ </div>
1380
+ </div>
1381
+
1382
+ <div className="playground-actions">
1383
+ <h3>User Actions Log</h3>
1384
+ <div id="actionsLog" className="actions-log">
1385
+ {actions.length === 0 ? (
1386
+ <p className="placeholder">User actions will appear here when you interact with components...</p>
1387
+ ) : (
1388
+ actions.map((action, index) => (
1389
+ <div className="action-entry" key={`${action.componentId}-${action.actionId}-${index}`}>
1390
+ <span className="action-type">{action.type}</span> →
1391
+ <span className="action-component">{action.componentId}</span>
1392
+ {Object.keys(action).length ? <code>{JSON.stringify(action)}</code> : null}
1393
+ </div>
1394
+ ))
1395
+ )}
1396
+ </div>
1397
+ </div>
1398
+ </section>
1399
+ </div>
1400
+ </main>
1401
+ </div>
1402
+ );
1403
+ }