@adia-ai/a2ui-compose 0.0.1

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.
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Transpiler Maps — Static tag mapping tables and attribute extractors
3
+ * for the HTML → A2UI transpiler.
4
+ *
5
+ * Three mapping layers:
6
+ * 1. Reverse A2UI registry (tag → type, for *-n elements)
7
+ * 2. HTML tag map (native HTML → A2UI type)
8
+ * 3. Input type / ARIA role sub-maps
9
+ */
10
+
11
+ import { registry } from '@adia-ai/a2ui-utils';
12
+
13
+ // ── Alias types to skip in reverse registry (prefer primary type) ────────
14
+
15
+ const ALIAS_TYPES = new Set([
16
+ 'Select', 'LoadingIndicator', 'ErrorContainer', 'Keyboard',
17
+ 'DatePicker', 'CommandPalette', 'Segmented', 'OTP', 'SideNav', 'IconSource',
18
+ ]);
19
+
20
+ // ── Reverse Registry: tag → primary A2UI type ────────────────────────────
21
+
22
+ /** @type {Map<string, string>} */
23
+ export const reverseRegistry = new Map();
24
+
25
+ for (const [type, tag] of registry) {
26
+ if (ALIAS_TYPES.has(type)) continue;
27
+ // First-write wins — skip if tag already mapped (avoids alias overwrite)
28
+ if (!reverseRegistry.has(tag)) {
29
+ reverseRegistry.set(tag, type);
30
+ }
31
+ }
32
+
33
+ // ── HTML Tag → A2UI Type ─────────────────────────────────────────────────
34
+
35
+ /** @type {Record<string, { type: string, props?: Record<string, unknown> }>} */
36
+ export const HTML_TAG_MAP = {
37
+ h1: { type: 'Text', props: { variant: 'h1' } },
38
+ h2: { type: 'Text', props: { variant: 'h2' } },
39
+ h3: { type: 'Text', props: { variant: 'h3' } },
40
+ h4: { type: 'Text', props: { variant: 'h4' } },
41
+ h5: { type: 'Text', props: { variant: 'h5' } },
42
+ h6: { type: 'Text', props: { variant: 'h6' } },
43
+ p: { type: 'Text', props: { variant: 'body' } },
44
+ small: { type: 'Text', props: { variant: 'caption' } },
45
+ strong: { type: 'Text', props: { variant: 'body' } },
46
+ em: { type: 'Text', props: { variant: 'body' } },
47
+ blockquote: { type: 'Text', props: { variant: 'body' } },
48
+ code: { type: 'Code' },
49
+ pre: { type: 'Code' },
50
+ kbd: { type: 'Kbd' },
51
+ button: { type: 'Button' },
52
+ select: { type: 'ChoicePicker' },
53
+ textarea: { type: 'TextArea' },
54
+ form: { type: 'FormContainer' },
55
+ img: { type: 'Image' },
56
+ video: { type: 'Embed' },
57
+ iframe: { type: 'Embed' },
58
+ audio: { type: 'Embed' },
59
+ table: { type: 'Table' },
60
+ ul: { type: 'List' },
61
+ ol: { type: 'List' },
62
+ li: { type: 'Text', props: { variant: 'body' } },
63
+ nav: { type: 'Nav' },
64
+ aside: { type: 'Sidebar' },
65
+ main: { type: 'Column' },
66
+ article: { type: 'Card' },
67
+ details: { type: 'Accordion' },
68
+ summary: { type: 'Text', props: { variant: 'h5' } },
69
+ dialog: { type: 'Modal' },
70
+ hr: { type: 'Divider' },
71
+ a: { type: 'Button', props: { variant: 'ghost' } },
72
+ header: { type: 'Header' },
73
+ footer: { type: 'Footer' },
74
+ section: { type: 'Section' },
75
+ label: { type: 'Text', props: { variant: 'body' } },
76
+ span: { type: 'Text', props: { variant: 'body' } },
77
+ };
78
+
79
+ // ── Input Type Sub-Map ───────────────────────────────────────────────────
80
+
81
+ /** @type {Record<string, { type: string, props?: Record<string, unknown> }>} */
82
+ export const INPUT_TYPE_MAP = {
83
+ text: { type: 'TextField' },
84
+ email: { type: 'TextField', props: { type: 'email' } },
85
+ password: { type: 'TextField', props: { type: 'password' } },
86
+ number: { type: 'TextField', props: { type: 'number' } },
87
+ tel: { type: 'TextField', props: { type: 'tel' } },
88
+ url: { type: 'TextField', props: { type: 'url' } },
89
+ search: { type: 'Search' },
90
+ checkbox: { type: 'CheckBox' },
91
+ radio: { type: 'Radio' },
92
+ range: { type: 'Slider' },
93
+ date: { type: 'DateTimeInput' },
94
+ time: { type: 'DateTimeInput' },
95
+ 'datetime-local': { type: 'DateTimeInput' },
96
+ file: { type: 'Upload' },
97
+ color: { type: 'ColorPicker' },
98
+ submit: { type: 'Button', props: { variant: 'primary', type: 'submit' } },
99
+ reset: { type: 'Button', props: { variant: 'outline', type: 'reset' } },
100
+ };
101
+
102
+ // ── ARIA Role Map ────────────────────────────────────────────────────────
103
+
104
+ /** @type {Record<string, string>} */
105
+ export const ARIA_ROLE_MAP = {
106
+ tablist: 'Tabs',
107
+ tab: 'Tab',
108
+ tabpanel: 'Panel',
109
+ alert: 'Alert',
110
+ alertdialog: 'Dialog',
111
+ dialog: 'Modal',
112
+ navigation: 'Nav',
113
+ progressbar: 'Progress',
114
+ slider: 'Slider',
115
+ switch: 'Toggle',
116
+ menu: 'Menu',
117
+ menubar: 'Menu',
118
+ toolbar: 'Toolbar',
119
+ tooltip: 'Tooltip',
120
+ search: 'Search',
121
+ banner: 'Header',
122
+ contentinfo: 'Footer',
123
+ complementary: 'Sidebar',
124
+ };
125
+
126
+ // ── Skip Tags ────────────────────────────────────────────────────────────
127
+
128
+ export const SKIP_TAGS = new Set([
129
+ 'script', 'style', 'link', 'meta', 'head', 'br', 'wbr',
130
+ 'noscript', 'template', 'slot', 'svg', 'path', 'circle',
131
+ 'rect', 'line', 'polyline', 'polygon', 'g', 'defs', 'use',
132
+ ]);
133
+
134
+ // ── Attribute Extractors ─────────────────────────────────────────────────
135
+
136
+ /**
137
+ * Extract A2UI props from an HTML element based on the resolved A2UI type.
138
+ *
139
+ * @param {object} el — DOM element or MinimalElement
140
+ * @param {string} a2uiType — Resolved A2UI type name
141
+ * @returns {Record<string, unknown>}
142
+ */
143
+ export function extractProps(el, a2uiType) {
144
+ const props = {};
145
+ const attr = (name) => el.getAttribute?.(name) ?? el.attributes?.get?.(name) ?? null;
146
+
147
+ // ── Text content (for leaf elements) ──
148
+ if (['Text', 'Kbd', 'Code'].includes(a2uiType)) {
149
+ const text = (el.textContent || '').trim().replace(/\s+/g, ' ');
150
+ if (text) props.textContent = text;
151
+ }
152
+
153
+ // ── Button ──
154
+ if (a2uiType === 'Button') {
155
+ const text = (el.textContent || '').trim().replace(/\s+/g, ' ');
156
+ if (text) props.text = text;
157
+ const type = attr('type');
158
+ if (type && type !== 'button') props.type = type;
159
+ if (attr('disabled') !== null) props.disabled = true;
160
+ // Variant inference from class
161
+ const cls = attr('class') || '';
162
+ if (cls.includes('primary')) props.variant = 'primary';
163
+ else if (cls.includes('danger')) props.variant = 'danger';
164
+ else if (cls.includes('ghost')) props.variant = 'ghost';
165
+ else if (cls.includes('outline')) props.variant = 'outline';
166
+ // Link href
167
+ if (el.tagName?.toLowerCase() === 'a') {
168
+ const href = attr('href');
169
+ if (href) props.href = href;
170
+ }
171
+ }
172
+
173
+ // ── TextField / Input ──
174
+ if (['TextField', 'Search'].includes(a2uiType)) {
175
+ for (const name of ['placeholder', 'name', 'value', 'pattern', 'min', 'max', 'minlength', 'maxlength', 'autocomplete']) {
176
+ const v = attr(name);
177
+ if (v !== null && v !== '') props[name] = v;
178
+ }
179
+ if (attr('required') !== null) props.required = true;
180
+ if (attr('disabled') !== null) props.disabled = true;
181
+ if (attr('readonly') !== null) props.readonly = true;
182
+ const type = attr('type');
183
+ if (type && type !== 'text') props.type = type;
184
+ }
185
+
186
+ // ── CheckBox / Toggle / Radio ──
187
+ if (['CheckBox', 'Toggle', 'Radio'].includes(a2uiType)) {
188
+ const name = attr('name');
189
+ if (name) props.name = name;
190
+ const value = attr('value');
191
+ if (value) props.value = value;
192
+ if (attr('checked') !== null) props.checked = true;
193
+ if (attr('disabled') !== null) props.disabled = true;
194
+ if (attr('required') !== null) props.required = true;
195
+ }
196
+
197
+ // ── Slider ──
198
+ if (a2uiType === 'Slider') {
199
+ for (const name of ['min', 'max', 'value', 'step', 'name']) {
200
+ const v = attr(name);
201
+ if (v !== null && v !== '') props[name] = name === 'step' ? v : Number(v) || v;
202
+ }
203
+ if (attr('disabled') !== null) props.disabled = true;
204
+ }
205
+
206
+ // ── Image ──
207
+ if (a2uiType === 'Image') {
208
+ const src = attr('src');
209
+ if (src) props.src = src;
210
+ const alt = attr('alt');
211
+ if (alt) props.alt = alt;
212
+ }
213
+
214
+ // ── TextArea ──
215
+ if (a2uiType === 'TextArea') {
216
+ for (const name of ['placeholder', 'name', 'rows', 'cols', 'minlength', 'maxlength']) {
217
+ const v = attr(name);
218
+ if (v !== null && v !== '') props[name] = v;
219
+ }
220
+ if (attr('required') !== null) props.required = true;
221
+ if (attr('disabled') !== null) props.disabled = true;
222
+ const text = (el.textContent || '').trim();
223
+ if (text) props.value = text;
224
+ }
225
+
226
+ // ── Embed ──
227
+ if (a2uiType === 'Embed') {
228
+ const src = attr('src');
229
+ if (src) props.src = src;
230
+ }
231
+
232
+ // ── Layout (Column, Row, Grid) ──
233
+ if (['Column', 'Row', 'Grid', 'List'].includes(a2uiType)) {
234
+ const gap = attr('gap');
235
+ if (gap) props.gap = gap;
236
+ }
237
+
238
+ // ── Label extraction from adjacent <label> or aria-label ──
239
+ if (['TextField', 'CheckBox', 'Toggle', 'Radio', 'Slider', 'Search', 'TextArea', 'ChoicePicker', 'DateTimeInput'].includes(a2uiType)) {
240
+ const ariaLabel = attr('aria-label');
241
+ if (ariaLabel) props.label = ariaLabel;
242
+ }
243
+
244
+ // ── Data attributes pass-through ──
245
+ if (el.attributes) {
246
+ const attrs = el.attributes instanceof Map ? el.attributes : el.attributes;
247
+ if (typeof attrs[Symbol.iterator] === 'function') {
248
+ for (const a of attrs) {
249
+ const name = a.name || a[0];
250
+ const value = a.value || a[1];
251
+ if (name?.startsWith('data-') && name !== 'data-a2ui-id') {
252
+ props[name] = value;
253
+ }
254
+ }
255
+ }
256
+ }
257
+
258
+ return props;
259
+ }
260
+
261
+ // ── Gap Inference ────────────────────────────────────────────────────────
262
+
263
+ /**
264
+ * Map a pixel gap value to a token size.
265
+ * @param {string} gapValue — CSS gap value (e.g., "8px", "1rem")
266
+ * @returns {string} — "xs" | "sm" | "md" | "lg" | "xl"
267
+ */
268
+ export function inferGap(gapValue) {
269
+ if (!gapValue) return 'md';
270
+ const px = parseFloat(gapValue);
271
+ if (isNaN(px)) return 'md';
272
+ if (px <= 4) return 'xs';
273
+ if (px <= 8) return 'sm';
274
+ if (px <= 16) return 'md';
275
+ if (px <= 24) return 'lg';
276
+ return 'xl';
277
+ }