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
package/.eslintignore ADDED
@@ -0,0 +1,4 @@
1
+ node_modules
2
+ dist
3
+ build
4
+ .husky
package/.eslintrc.cjs ADDED
@@ -0,0 +1,21 @@
1
+ module.exports = {
2
+ root: true,
3
+ parser: '@typescript-eslint/parser',
4
+ parserOptions: {
5
+ ecmaVersion: 2024,
6
+ sourceType: 'module'
7
+ },
8
+ plugins: ['@typescript-eslint'],
9
+ extends: [
10
+ 'eslint:recommended',
11
+ 'plugin:@typescript-eslint/recommended',
12
+ 'prettier'
13
+ ],
14
+ env: {
15
+ node: true,
16
+ es2024: true
17
+ },
18
+ rules: {
19
+ // project-specific rules can be added here
20
+ }
21
+ };
package/.gitattributes ADDED
@@ -0,0 +1,5 @@
1
+ * text=auto eol=lf
2
+ *.ts linguist-language=TypeScript
3
+ *.js linguist-language=JavaScript
4
+ package.json text
5
+ *.md text
@@ -0,0 +1,21 @@
1
+ - [x] Verify that the copilot-instructions.md file in the .github directory is created.
2
+
3
+ - [x] Clarify Project Requirements
4
+
5
+ - [x] Scaffold the Project
6
+
7
+ - [x] Customize the Project
8
+
9
+ - [x] Install Required Extensions
10
+
11
+ - [x] Compile the Project
12
+
13
+ - [x] Create and Run Task
14
+
15
+ - [x] Launch the Project
16
+
17
+ - [x] Ensure Documentation is Complete
18
+
19
+ - Work through each checklist item systematically.
20
+ - Keep communication concise and focused.
21
+ - Follow development best practices.
@@ -0,0 +1,31 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ main, master ]
6
+ pull_request:
7
+ branches: [ main, master ]
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: windows-latest
12
+ strategy:
13
+ matrix:
14
+ node-version: [18.x, 20.x]
15
+ steps:
16
+ - name: Checkout
17
+ uses: actions/checkout@v4
18
+
19
+ - name: Setup Node
20
+ uses: actions/setup-node@v4
21
+ with:
22
+ node-version: ${{ matrix.node-version }}
23
+
24
+ - name: Install dependencies
25
+ run: npm ci
26
+
27
+ - name: Build
28
+ run: npm run build --if-present
29
+
30
+ - name: Test
31
+ run: npm test --if-present
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env sh
2
+ . "$(dirname "$0")/_/husky.sh"
3
+
4
+ # Run tests if present; adjust as needed (lint-staged can be added later)
5
+ npx --no-install lint-staged || true
6
+ npm test --if-present
@@ -0,0 +1,6 @@
1
+ node_modules
2
+ dist
3
+ build
4
+ .husky
5
+ .git
6
+ *.log
package/.prettierrc ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "trailingComma": "all",
3
+ "tabWidth": 2,
4
+ "semi": true,
5
+ "singleQuote": true,
6
+ "printWidth": 100
7
+ }
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ A2UI v0.8 Ink wrapper MVP.
2
+
3
+ This library renders a minimal subset of A2UI components using Ink and React. It supports beginRendering, surfaceUpdate, dataModelUpdate, adjacency-list rendering, and basic user actions for input, selection, and buttons.
4
+
5
+ Use npm run demo to launch the sample surface renderer.
6
+
7
+ ## Supported Features
8
+ - A2UI v0.8 render gate: beginRendering, surfaceUpdate, dataModelUpdate
9
+ - Adjacency-list tree reconstruction with explicit children and template lists
10
+ - Data binding via `BoundValue` (path + literal fallbacks)
11
+ - Ink-backed components: Text, Box, Spacer, Input, Select, Button, Checkbox, RadioGroup, List, Tabs, Table, Slider, Modal, Divider, Image, Chart
12
+ - Focus management with Tab / Shift+Tab navigation
13
+ - Action dispatch: onPress, onChange, onSubmit, onSelect
14
+
15
+ ## Implementation Guide
16
+ 1. Create a renderer instance with `createA2uiInkRenderer`.
17
+ 2. Send A2UI server messages in this order:
18
+ - surfaceUpdate: component catalog and root ID
19
+ - dataModelUpdate: initial state
20
+ - beginRendering: unlock rendering
21
+ 3. Handle `userAction` callbacks to round-trip input back to your A2UI server.
22
+
23
+ ## Usage with A2UI
24
+ Create the renderer, stream A2UI messages into `handleMessage`, and forward user actions back to your server.
25
+
26
+ Example flow:
27
+ 1. `surfaceUpdate` provides the component graph.
28
+ 2. `dataModelUpdate` supplies initial data.
29
+ 3. `beginRendering` allows the client to render.
30
+
31
+ Use the `onUserAction` callback to capture input and selection events.
32
+
33
+ Recommended entry points:
34
+ - Public API: [src/index.ts](src/index.ts)
35
+ - Renderer core: [src/renderer.ts](src/renderer.ts)
36
+ - Component registry: [src/catalog.ts](src/catalog.ts)
37
+ - Binding and tree build: [src/binding.ts](src/binding.ts) and [src/tree.ts](src/tree.ts)
38
+
39
+ ## Future Plan
40
+ - Add more standard catalog components (checkbox, radio, list view, tabs)
41
+ - Styling parity improvements (alignment, borders, padding presets)
42
+ - Better input controls (multi-line input, validation, masking)
43
+ - Multi-surface support in a single process
44
+ - Performance tuning for large dynamic lists
@@ -0,0 +1,3 @@
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>;
@@ -0,0 +1,73 @@
1
+ const literalKeys = [
2
+ "literalString",
3
+ "literalNumber",
4
+ "literalBoolean",
5
+ "literalObject",
6
+ "literalArray"
7
+ ];
8
+ export function resolveBoundValue(boundValue, dataModel, context) {
9
+ if (!boundValue) {
10
+ return undefined;
11
+ }
12
+ if (boundValue.path) {
13
+ const resolved = getPathValue(boundValue.path, dataModel, context);
14
+ if (resolved !== undefined) {
15
+ return resolved;
16
+ }
17
+ }
18
+ for (const key of literalKeys) {
19
+ if (key in boundValue) {
20
+ return boundValue[key];
21
+ }
22
+ }
23
+ return undefined;
24
+ }
25
+ export function resolveProps(props, dataModel, context) {
26
+ const resolved = {};
27
+ for (const [key, value] of Object.entries(props)) {
28
+ if (isBoundValue(value)) {
29
+ resolved[key] = resolveBoundValue(value, dataModel, context);
30
+ }
31
+ else {
32
+ resolved[key] = value;
33
+ }
34
+ }
35
+ return resolved;
36
+ }
37
+ function isBoundValue(value) {
38
+ if (!value || typeof value !== "object") {
39
+ return false;
40
+ }
41
+ if ("path" in value) {
42
+ return true;
43
+ }
44
+ return literalKeys.some((key) => key in value);
45
+ }
46
+ function getPathValue(path, dataModel, context) {
47
+ const normalized = path.replace(/^\$\.?/, "");
48
+ if (normalized === "") {
49
+ return dataModel;
50
+ }
51
+ const parts = normalized.split(".");
52
+ const root = resolveContextRoot(parts[0], context, dataModel);
53
+ const startIndex = root === dataModel ? 0 : 1;
54
+ let current = root;
55
+ for (let i = startIndex; i < parts.length; i += 1) {
56
+ if (current && typeof current === "object" && parts[i] in current) {
57
+ current = current[parts[i]];
58
+ }
59
+ else {
60
+ return undefined;
61
+ }
62
+ }
63
+ return current;
64
+ }
65
+ function resolveContextRoot(firstPart, context, dataModel) {
66
+ if (firstPart === "item" && (context === null || context === void 0 ? void 0 : context.item) !== undefined) {
67
+ return context.item;
68
+ }
69
+ if (firstPart === "index" && (context === null || context === void 0 ? void 0 : context.index) !== undefined) {
70
+ return context.index;
71
+ }
72
+ return dataModel;
73
+ }
@@ -0,0 +1,6 @@
1
+ import React from "react";
2
+ import type { ActionDef, ResolvedNode } from "./types.js";
3
+ export interface CatalogRenderOptions {
4
+ dispatchAction: (action: ActionDef, node: ResolvedNode, value?: unknown) => void;
5
+ }
6
+ export declare function renderNode(node: ResolvedNode, options: CatalogRenderOptions): React.ReactElement;
@@ -0,0 +1,165 @@
1
+ import React, { useEffect, useMemo, useState } from "react";
2
+ import { Box, Spacer, Text } from "ink";
3
+ import { useFocusRegistry } from "./focus.js";
4
+ export function renderNode(node, options) {
5
+ const { type } = node;
6
+ switch (type) {
7
+ case "Text":
8
+ return React.createElement(A2uiText, { key: node.instanceKey, node });
9
+ case "Box":
10
+ return React.createElement(A2uiBox, { key: node.instanceKey, node, options });
11
+ case "Spacer":
12
+ return React.createElement(Spacer, { key: node.instanceKey });
13
+ case "Input":
14
+ return React.createElement(A2uiInput, { key: node.instanceKey, node, options });
15
+ case "Button":
16
+ return React.createElement(A2uiButton, { key: node.instanceKey, node, options });
17
+ case "Select":
18
+ return React.createElement(A2uiSelect, { key: node.instanceKey, node, options });
19
+ default:
20
+ return React.createElement(Text, { key: node.instanceKey }, `Unsupported: ${type}`);
21
+ }
22
+ }
23
+ const A2uiText = ({ node }) => {
24
+ var _a, _b;
25
+ const props = node.props;
26
+ const text = ((_b = (_a = props.text) !== null && _a !== void 0 ? _a : props.value) !== null && _b !== void 0 ? _b : "");
27
+ return React.createElement(Text, {
28
+ color: props.color,
29
+ backgroundColor: props.backgroundColor,
30
+ bold: props.bold,
31
+ dimColor: props.dim,
32
+ italic: props.italic,
33
+ underline: props.underline
34
+ }, text);
35
+ };
36
+ const A2uiBox = ({ node, options }) => {
37
+ var _a;
38
+ const props = node.props;
39
+ return React.createElement(Box, {
40
+ flexDirection: (_a = props.direction) !== null && _a !== void 0 ? _a : "column",
41
+ padding: props.padding,
42
+ paddingX: props.paddingX,
43
+ paddingY: props.paddingY,
44
+ margin: props.margin,
45
+ marginX: props.marginX,
46
+ marginY: props.marginY,
47
+ borderStyle: props.borderStyle,
48
+ borderColor: props.borderColor,
49
+ width: props.width,
50
+ height: props.height,
51
+ flexGrow: props.flexGrow,
52
+ flexShrink: props.flexShrink,
53
+ justifyContent: props.justifyContent,
54
+ alignItems: props.alignItems
55
+ }, node.children.map((child) => renderNode(child, options)));
56
+ };
57
+ const A2uiButton = ({ node, options }) => {
58
+ var _a, _b;
59
+ const focus = useFocusRegistry();
60
+ const label = ((_b = (_a = node.props.label) !== null && _a !== void 0 ? _a : node.props.text) !== null && _b !== void 0 ? _b : "Button");
61
+ const action = node.props.onPress;
62
+ const isFocused = focus.isFocused(node.instanceKey);
63
+ useEffect(() => {
64
+ const handler = (input, key) => {
65
+ if (!action) {
66
+ return;
67
+ }
68
+ if (key.return || input === " ") {
69
+ options.dispatchAction(action, node);
70
+ }
71
+ };
72
+ focus.register(node.instanceKey, handler);
73
+ return () => focus.unregister(node.instanceKey);
74
+ }, [action, focus, node, options]);
75
+ return React.createElement(Box, { borderStyle: "round", borderColor: isFocused ? "cyan" : undefined, paddingX: 1 }, React.createElement(Text, { inverse: isFocused }, label));
76
+ };
77
+ const A2uiInput = ({ node, options }) => {
78
+ var _a;
79
+ const focus = useFocusRegistry();
80
+ const action = node.props.onChange;
81
+ const submitAction = node.props.onSubmit;
82
+ const initial = ((_a = node.props.value) !== null && _a !== void 0 ? _a : "");
83
+ const [value, setValue] = useState(initial);
84
+ useEffect(() => {
85
+ setValue(initial);
86
+ }, [initial]);
87
+ useEffect(() => {
88
+ const handler = (input, key) => {
89
+ if (key.return) {
90
+ if (submitAction) {
91
+ options.dispatchAction(submitAction, node, value);
92
+ }
93
+ return;
94
+ }
95
+ if (key.backspace || key.delete) {
96
+ setValue((current) => current.slice(0, -1));
97
+ if (action) {
98
+ options.dispatchAction(action, node, value.slice(0, -1));
99
+ }
100
+ return;
101
+ }
102
+ if (input) {
103
+ const next = `${value}${input}`;
104
+ setValue(next);
105
+ if (action) {
106
+ options.dispatchAction(action, node, next);
107
+ }
108
+ }
109
+ };
110
+ focus.register(node.instanceKey, handler);
111
+ return () => focus.unregister(node.instanceKey);
112
+ }, [action, focus, node, options, submitAction, value]);
113
+ const isFocused = focus.isFocused(node.instanceKey);
114
+ return React.createElement(Box, null, React.createElement(Text, { color: isFocused ? "cyan" : undefined }, value || " "));
115
+ };
116
+ const A2uiSelect = ({ node, options }) => {
117
+ var _a, _b;
118
+ const focus = useFocusRegistry();
119
+ const items = ((_a = node.props.items) !== null && _a !== void 0 ? _a : []);
120
+ const action = node.props.onSelect;
121
+ const initialIndex = ((_b = node.props.selectedIndex) !== null && _b !== void 0 ? _b : 0);
122
+ const [index, setIndex] = useState(initialIndex);
123
+ useEffect(() => {
124
+ setIndex(initialIndex);
125
+ }, [initialIndex]);
126
+ useEffect(() => {
127
+ const handler = (input, key) => {
128
+ if (key.upArrow) {
129
+ setIndex((current) => (current <= 0 ? items.length - 1 : current - 1));
130
+ return;
131
+ }
132
+ if (key.downArrow) {
133
+ setIndex((current) => (current + 1) % items.length);
134
+ return;
135
+ }
136
+ if (key.return && action) {
137
+ const value = items[index];
138
+ options.dispatchAction(action, node, value);
139
+ }
140
+ if (input === " " && action) {
141
+ const value = items[index];
142
+ options.dispatchAction(action, node, value);
143
+ }
144
+ };
145
+ focus.register(node.instanceKey, handler);
146
+ return () => focus.unregister(node.instanceKey);
147
+ }, [action, focus, index, items, node, options]);
148
+ const isFocused = focus.isFocused(node.instanceKey);
149
+ const labels = useMemo(() => items.map(getItemLabel), [items]);
150
+ return React.createElement(Box, { flexDirection: "column" }, labels.map((label, itemIndex) => React.createElement(Text, { key: `${node.instanceKey}_${itemIndex}`, inverse: isFocused && itemIndex === index }, label)));
151
+ };
152
+ function getItemLabel(item) {
153
+ var _a, _b;
154
+ if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") {
155
+ return String(item);
156
+ }
157
+ if (item && typeof item === "object") {
158
+ const record = item;
159
+ const label = (_b = (_a = record.label) !== null && _a !== void 0 ? _a : record.text) !== null && _b !== void 0 ? _b : record.name;
160
+ if (label) {
161
+ return String(label);
162
+ }
163
+ }
164
+ return JSON.stringify(item);
165
+ }
@@ -0,0 +1 @@
1
+ export {};