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.
- package/.eslintignore +4 -0
- package/.eslintrc.cjs +21 -0
- package/.gitattributes +5 -0
- package/.github/copilot-instructions.md +21 -0
- package/.github/workflows/ci.yml +31 -0
- package/.husky/pre-commit +6 -0
- package/.prettierignore +6 -0
- package/.prettierrc +7 -0
- package/README.md +44 -0
- package/dist/binding.d.ts +3 -0
- package/dist/binding.js +73 -0
- package/dist/catalog.d.ts +6 -0
- package/dist/catalog.js +165 -0
- package/dist/examples/demo.d.ts +1 -0
- package/dist/examples/demo.js +309 -0
- package/dist/focus.d.ts +15 -0
- package/dist/focus.js +68 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/renderer.d.ts +6 -0
- package/dist/renderer.js +144 -0
- package/dist/src/binding.d.ts +8 -0
- package/dist/src/binding.js +141 -0
- package/dist/src/catalog.d.ts +2 -0
- package/dist/src/catalog.js +1 -0
- package/dist/src/components/Box.d.ts +6 -0
- package/dist/src/components/Box.js +23 -0
- package/dist/src/components/Button.d.ts +7 -0
- package/dist/src/components/Button.js +71 -0
- package/dist/src/components/Chart.d.ts +5 -0
- package/dist/src/components/Chart.js +65 -0
- package/dist/src/components/Checkbox.d.ts +7 -0
- package/dist/src/components/Checkbox.js +51 -0
- package/dist/src/components/DateTimeInput.d.ts +1 -0
- package/dist/src/components/DateTimeInput.js +1 -0
- package/dist/src/components/Divider.d.ts +5 -0
- package/dist/src/components/Divider.js +7 -0
- package/dist/src/components/Image.d.ts +5 -0
- package/dist/src/components/Image.js +8 -0
- package/dist/src/components/Input.d.ts +7 -0
- package/dist/src/components/Input.js +124 -0
- package/dist/src/components/List.d.ts +5 -0
- package/dist/src/components/List.js +9 -0
- package/dist/src/components/Modal.d.ts +6 -0
- package/dist/src/components/Modal.js +13 -0
- package/dist/src/components/RadioGroup.d.ts +7 -0
- package/dist/src/components/RadioGroup.js +56 -0
- package/dist/src/components/Select.d.ts +7 -0
- package/dist/src/components/Select.js +66 -0
- package/dist/src/components/Slider.d.ts +7 -0
- package/dist/src/components/Slider.js +74 -0
- package/dist/src/components/Spacer.d.ts +1 -0
- package/dist/src/components/Spacer.js +1 -0
- package/dist/src/components/Table.d.ts +5 -0
- package/dist/src/components/Table.js +14 -0
- package/dist/src/components/Tabs.d.ts +7 -0
- package/dist/src/components/Tabs.js +56 -0
- package/dist/src/components/Text.d.ts +5 -0
- package/dist/src/components/Text.js +15 -0
- package/dist/src/components/helpers.d.ts +4 -0
- package/dist/src/components/helpers.js +39 -0
- package/dist/src/components/index.d.ts +16 -0
- package/dist/src/components/index.js +15 -0
- package/dist/src/components/renderNode.d.ts +4 -0
- package/dist/src/components/renderNode.js +61 -0
- package/dist/src/components/types.d.ts +7 -0
- package/dist/src/components/types.js +1 -0
- package/dist/src/components.d.ts +1 -0
- package/dist/src/components.js +1 -0
- package/dist/src/focus.d.ts +15 -0
- package/dist/src/focus.js +68 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/renderer.d.ts +6 -0
- package/dist/src/renderer.js +673 -0
- package/dist/src/tree.d.ts +2 -0
- package/dist/src/tree.js +47 -0
- package/dist/src/types.d.ts +92 -0
- package/dist/src/types.js +1 -0
- package/dist/tree.d.ts +2 -0
- package/dist/tree.js +45 -0
- package/dist/types.d.ts +73 -0
- package/dist/types.js +1 -0
- package/docs/demo/README.md +90 -0
- package/docs/demo/app.js +268 -0
- package/docs/demo/index.html +14 -0
- package/docs/demo/package-lock.json +4512 -0
- package/docs/demo/package.json +32 -0
- package/docs/demo/src/App.tsx +1403 -0
- package/docs/demo/src/main.tsx +9 -0
- package/docs/demo/src/setEnv.ts +29 -0
- package/docs/demo/src/shims/fs.js +16 -0
- package/docs/demo/src/shims/process.js +10 -0
- package/docs/demo/src/styles.css +720 -0
- package/docs/demo/styles.css +1 -0
- package/docs/demo/tsconfig.json +14 -0
- package/docs/demo/vite-plugin-node-polyfills/shims/buffer +2 -0
- package/docs/demo/vite-plugin-node-polyfills/shims/global +2 -0
- package/docs/demo/vite-plugin-node-polyfills/shims/process +10 -0
- package/docs/demo/vite.config.js +200 -0
- package/docs/overview.md +277 -0
- package/examples/demo.d.ts +1 -0
- package/examples/demo.js +66 -0
- package/examples/demo.ts +315 -0
- package/package.json +48 -0
- package/src/binding.ts +191 -0
- package/src/catalog.ts +2 -0
- package/src/components/Box.ts +39 -0
- package/src/components/Button.ts +84 -0
- package/src/components/Checkbox.ts +66 -0
- package/src/components/DateTimeInput.ts +1 -0
- package/src/components/Divider.ts +8 -0
- package/src/components/Image.ts +15 -0
- package/src/components/Input.ts +148 -0
- package/src/components/List.ts +15 -0
- package/src/components/Modal.ts +21 -0
- package/src/components/RadioGroup.ts +77 -0
- package/src/components/Select.ts +94 -0
- package/src/components/Slider.ts +98 -0
- package/src/components/Spacer.ts +1 -0
- package/src/components/Table.ts +22 -0
- package/src/components/Tabs.ts +82 -0
- package/src/components/Text.ts +21 -0
- package/src/components/helpers.ts +42 -0
- package/src/components/index.ts +16 -0
- package/src/components/renderNode.ts +73 -0
- package/src/components/types.ts +8 -0
- package/src/components.ts +1 -0
- package/src/focus.ts +94 -0
- package/src/index.ts +12 -0
- package/src/renderer.ts +779 -0
- package/src/tree.ts +63 -0
- package/src/types.ts +110 -0
- 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
|
+
});
|
package/dist/focus.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
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;
|
package/dist/renderer.js
ADDED
|
@@ -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>;
|