@urbicon-ui/blocks 6.40.4 → 6.43.2

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 (107) hide show
  1. package/README.md +1 -1
  2. package/dist/components/Calendar/calendar.variants.d.ts +63 -63
  3. package/dist/components/Chat/A2UIView/A2UINode.svelte +20 -39
  4. package/dist/components/Chat/A2UIView/A2UIView.svelte +47 -50
  5. package/dist/components/Chat/A2UIView/a2ui-basic-catalog.d.ts +17 -0
  6. package/dist/components/Chat/A2UIView/a2ui-basic-catalog.js +69 -0
  7. package/dist/components/Chat/A2UIView/a2ui-catalog.d.ts +95 -0
  8. package/dist/components/Chat/A2UIView/a2ui-catalog.js +87 -0
  9. package/dist/components/Chat/A2UIView/a2ui-node-common.d.ts +40 -0
  10. package/dist/components/Chat/A2UIView/a2ui-node-common.js +72 -0
  11. package/dist/components/Chat/A2UIView/a2ui-prompt.d.ts +21 -10
  12. package/dist/components/Chat/A2UIView/a2ui-prompt.js +170 -24
  13. package/dist/components/Chat/A2UIView/a2ui-registry.d.ts +1 -1
  14. package/dist/components/Chat/A2UIView/a2ui-registry.js +2 -1
  15. package/dist/components/Chat/A2UIView/a2ui-render.d.ts +30 -0
  16. package/dist/components/Chat/A2UIView/a2ui-render.js +61 -23
  17. package/dist/components/Chat/A2UIView/a2ui-router.d.ts +136 -0
  18. package/dist/components/Chat/A2UIView/a2ui-router.js +264 -0
  19. package/dist/components/Chat/A2UIView/a2ui-schema.d.ts +45 -0
  20. package/dist/components/Chat/A2UIView/a2ui-schema.js +133 -0
  21. package/dist/components/Chat/A2UIView/a2ui-stream.d.ts +120 -0
  22. package/dist/components/Chat/A2UIView/a2ui-stream.js +406 -0
  23. package/dist/components/Chat/A2UIView/a2ui-transcript.d.ts +65 -0
  24. package/dist/components/Chat/A2UIView/a2ui-transcript.js +101 -0
  25. package/dist/components/Chat/A2UIView/a2ui-validate.d.ts +33 -1
  26. package/dist/components/Chat/A2UIView/a2ui-validate.js +167 -63
  27. package/dist/components/Chat/A2UIView/a2ui.types.d.ts +50 -0
  28. package/dist/components/Chat/A2UIView/a2ui.types.js +38 -1
  29. package/dist/components/Chat/A2UIView/index.d.ts +35 -2
  30. package/dist/components/Chat/A2UIView/index.js +13 -0
  31. package/dist/components/Chat/A2UIView/urbicon/UrbiconA2UINode.svelte +831 -0
  32. package/dist/components/Chat/A2UIView/urbicon/UrbiconA2UINode.svelte.d.ts +4 -0
  33. package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-catalog.d.ts +14 -0
  34. package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-catalog.js +87 -0
  35. package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-registry.d.ts +78 -0
  36. package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-registry.js +783 -0
  37. package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon.variants.d.ts +43 -0
  38. package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon.variants.js +33 -0
  39. package/dist/components/Chat/A2UIView/urbicon/urbicon-axes.gen.d.ts +7 -0
  40. package/dist/components/Chat/A2UIView/urbicon/urbicon-axes.gen.js +86 -0
  41. package/dist/components/Chat/ChatMessage/ChatMessage.svelte +93 -67
  42. package/dist/components/Chat/ChatMessage/chat-message.variants.d.ts +26 -19
  43. package/dist/components/Chat/ChatMessage/chat-message.variants.js +45 -12
  44. package/dist/components/Chat/ChatMessage/index.d.ts +20 -5
  45. package/dist/components/Chat/CodeBlock/CodeBlock.svelte +59 -33
  46. package/dist/components/Chat/CodeBlock/code-block.variants.d.ts +7 -0
  47. package/dist/components/Chat/CodeBlock/code-block.variants.js +28 -13
  48. package/dist/components/Chat/CodeBlock/index.d.ts +39 -4
  49. package/dist/components/Chat/PromptInput/PromptInput.svelte +9 -16
  50. package/dist/components/Chat/PromptInput/prompt-input.variants.js +16 -15
  51. package/dist/components/Chat/ToolCallCard/ToolCallCard.svelte +9 -4
  52. package/dist/components/Chat/ToolCallCard/index.d.ts +4 -3
  53. package/dist/components/Chat/ToolCallCard/tool-call-card.variants.d.ts +0 -3
  54. package/dist/components/Chat/ToolCallCard/tool-call-card.variants.js +4 -4
  55. package/dist/components/CopyButton/CopyButton.svelte +9 -19
  56. package/dist/components/CopyButton/copy-button.variants.d.ts +4 -4
  57. package/dist/components/FileUpload/fileUpload.variants.js +15 -4
  58. package/dist/components/PinInput/PinInput.svelte +1 -0
  59. package/dist/components/PinInput/pin-input.variants.js +14 -1
  60. package/dist/components/TimeInput/time-input.variants.js +16 -1
  61. package/dist/i18n/index.d.ts +398 -2
  62. package/dist/internal/copy-state.svelte.d.ts +60 -0
  63. package/dist/internal/copy-state.svelte.js +76 -0
  64. package/dist/internal/field-chrome.d.ts +18 -1
  65. package/dist/internal/field-chrome.js +19 -2
  66. package/dist/primitives/Alert/alert.variants.d.ts +8 -8
  67. package/dist/primitives/Avatar/avatar.variants.js +4 -1
  68. package/dist/primitives/Badge/badge.variants.js +11 -2
  69. package/dist/primitives/Button/button.variants.js +4 -1
  70. package/dist/primitives/Card/card.variants.js +4 -1
  71. package/dist/primitives/Checkbox/checkbox.variants.js +7 -1
  72. package/dist/primitives/Combobox/Combobox.svelte +9 -1
  73. package/dist/primitives/Combobox/combobox.variants.d.ts +25 -0
  74. package/dist/primitives/Combobox/combobox.variants.js +43 -7
  75. package/dist/primitives/Combobox/index.d.ts +8 -2
  76. package/dist/primitives/Dialog/dialog.variants.js +4 -1
  77. package/dist/primitives/Drawer/drawer.variants.js +4 -1
  78. package/dist/primitives/Input/input.variants.js +17 -1
  79. package/dist/primitives/JourneyTimeline/journey-timeline.variants.d.ts +22 -22
  80. package/dist/primitives/Menu/menu.variants.js +5 -1
  81. package/dist/primitives/Progress/progress.variants.js +5 -2
  82. package/dist/primitives/RadioGroup/radioGroup.variants.js +74 -10
  83. package/dist/primitives/Select/select.variants.js +14 -5
  84. package/dist/primitives/Slider/Slider.svelte +4 -1
  85. package/dist/primitives/Slider/slider.variants.js +9 -2
  86. package/dist/primitives/Spinner/spinner.variants.d.ts +16 -16
  87. package/dist/primitives/Stepper/stepper.variants.d.ts +11 -11
  88. package/dist/primitives/Stepper/stepper.variants.js +2 -1
  89. package/dist/primitives/Tab/tab.variants.js +4 -1
  90. package/dist/primitives/Textarea/textarea.variants.js +16 -2
  91. package/dist/primitives/Toast/toast.variants.d.ts +12 -12
  92. package/dist/primitives/Toast/toast.variants.js +5 -1
  93. package/dist/primitives/Toggle/toggle.variants.js +94 -18
  94. package/dist/primitives/Tooltip/tooltip.variants.d.ts +3 -3
  95. package/dist/primitives/Tooltip/tooltip.variants.js +5 -1
  96. package/dist/style/foundation.css +19 -9
  97. package/dist/style/semantic.css +49 -2
  98. package/dist/style/themes/forest.css +2 -2
  99. package/dist/style/themes/neutral.css +2 -2
  100. package/dist/style/themes/ocean.css +2 -2
  101. package/dist/style/themes/rose.css +2 -2
  102. package/dist/style/themes/sunset.css +2 -2
  103. package/dist/utils/figma-token-export.js +7 -3
  104. package/dist/utils/persistent-state.svelte.d.ts +25 -1
  105. package/dist/utils/persistent-state.svelte.js +98 -10
  106. package/docs/MIGRATION-v5.md +29 -0
  107. package/package.json +3 -3
@@ -0,0 +1,831 @@
1
+ <script lang="ts">
2
+ import {
3
+ Accordion,
4
+ AccordionItem,
5
+ Alert,
6
+ Avatar,
7
+ Badge,
8
+ Button,
9
+ Card,
10
+ Checkbox,
11
+ Input,
12
+ Progress,
13
+ RadioGroup,
14
+ RadioItem,
15
+ Select,
16
+ Separator,
17
+ Skeleton,
18
+ Slider,
19
+ Stepper,
20
+ StepperStep,
21
+ Textarea,
22
+ Toggle
23
+ } from '../../../../primitives';
24
+ import { resolveIcon } from '../../../../icons';
25
+ import DangerCircleIconDefault from '../../../../icons/DangerCircleIcon.svelte';
26
+ import { fromDateInputValue, toDateInputValue } from '../../../../utils/date';
27
+ import DatePicker from '../../../DatePicker/DatePicker.svelte';
28
+ import EmptyState from '../../../EmptyState/EmptyState.svelte';
29
+ import TimeInput from '../../../TimeInput/TimeInput.svelte';
30
+ import StreamingMarkdown from '../../StreamingMarkdown/StreamingMarkdown.svelte';
31
+ import { checkImageUrl, checkLinkUrl } from '../../markdown/url-policy.js';
32
+ import type { A2uiActionEvent } from '../a2ui.types';
33
+ import { coerceText, dedupeOptions, splitDateTime } from '../a2ui-node-common';
34
+ import type { A2uiNodeProps, A2uiRenderNode } from '../a2ui-render';
35
+ import { bindingPointer, toInputString, toStringArray } from '../a2ui-render';
36
+ import { A2UI_SVG_PATH_RE } from '../a2ui-registry';
37
+ import { URBICON_A2UI_REGISTRY } from './a2ui-urbicon-registry';
38
+ import { a2uiUrbiconVariants } from './a2ui-urbicon.variants';
39
+
40
+ // Recursive dispatcher for the Urbicon A2UI catalog — the sibling of A2UINode,
41
+ // mapping the richer Urbicon vocabulary (real intents/variants/sizes, Section,
42
+ // RichText, Accordion) onto real Urbicon components. Same contract as A2UINode:
43
+ // children come pre-expanded on `node.children`; recursion runs through the
44
+ // `renderChild` snippet A2UIView threads down; the render-time plumbing is
45
+ // deliberately duplicated (not shared) so this stays a self-contained renderer.
46
+ let { node, context, renderChild }: A2uiNodeProps = $props();
47
+
48
+ const styles = a2uiUrbiconVariants();
49
+ const DangerIcon = resolveIcon('danger', DangerCircleIconDefault);
50
+
51
+ // Narrow a validated enum string to the exact component union it feeds. Safe:
52
+ // every value is registry-validated to be a subset of the real component axis
53
+ // (the drift test enforces `values ⊆ axis`), so this cast can never widen past
54
+ // what the component accepts.
55
+ function u<T extends string>(value: string): T {
56
+ return value as T;
57
+ }
58
+
59
+ // Local fallback state for inputs whose `value`/`current` is a literal (no
60
+ // data-model path to write back into). Bound inputs bypass these entirely.
61
+ let localText = $state<string | undefined>(undefined);
62
+ let localBool = $state<boolean | undefined>(undefined);
63
+ let localNum = $state<number | undefined>(undefined);
64
+ let localStr = $state<string | undefined>(undefined);
65
+ let localList = $state<string[] | undefined>(undefined);
66
+
67
+ const instance = $derived(node.instance);
68
+ const nodeProps = $derived(instance?.props);
69
+ const component = $derived(instance?.component ?? '');
70
+ const spec = $derived(component ? URBICON_A2UI_REGISTRY[component] : undefined);
71
+
72
+ const missingRequired = $derived.by(() => {
73
+ if (!spec || !instance) return false;
74
+ for (const [key, propSpec] of Object.entries(spec.props)) {
75
+ if (propSpec.required && !instance.props.has(key)) return true;
76
+ }
77
+ return false;
78
+ });
79
+
80
+ function raw(key: string): unknown {
81
+ return nodeProps?.get(key);
82
+ }
83
+ /** The resolved child node that came from a named childId slot (Card header/footer/child, …). */
84
+ function childByKey(slot: string): A2uiRenderNode | undefined {
85
+ return node.children.find((child) => child.slot === slot);
86
+ }
87
+ function enumOr(key: string, fallback: string): string {
88
+ const value = nodeProps?.get(key);
89
+ return typeof value === 'string' ? value : fallback;
90
+ }
91
+ function numOr(value: unknown, fallback: number): number {
92
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
93
+ }
94
+ function resolved(key: string): unknown {
95
+ return context.resolve(nodeProps?.get(key), node.scopePrefix).value;
96
+ }
97
+ function resolvedText(key: string): string {
98
+ return coerceText(resolved(key));
99
+ }
100
+
101
+ const ariaLabel = $derived.by(() => {
102
+ const a11y = nodeProps?.get('accessibility');
103
+ if (a11y !== null && typeof a11y === 'object') {
104
+ const label = (a11y as Record<string, unknown>).label;
105
+ if (typeof label === 'string' && label.length > 0) return label;
106
+ }
107
+ return undefined;
108
+ });
109
+
110
+ const weightStyle = $derived(node.weight != null ? `flex-grow:${node.weight}` : undefined);
111
+
112
+ const blockCtx = $derived(context.inline ? { ...context, inline: false } : context);
113
+ const inlineCtx = $derived({ ...context, inline: true });
114
+
115
+ // ── Enum → complete class-string lookups (literals for Tailwind's scanner) ──
116
+ const JUSTIFY: Record<string, string> = {
117
+ start: 'justify-start',
118
+ center: 'justify-center',
119
+ end: 'justify-end',
120
+ spaceBetween: 'justify-between',
121
+ spaceAround: 'justify-around',
122
+ spaceEvenly: 'justify-evenly',
123
+ stretch: 'justify-stretch'
124
+ };
125
+ const ALIGN: Record<string, string> = {
126
+ start: 'items-start',
127
+ center: 'items-center',
128
+ end: 'items-end',
129
+ stretch: 'items-stretch'
130
+ };
131
+ const DIRECTION: Record<string, string> = { vertical: 'flex-col', horizontal: 'flex-row' };
132
+ const FIT: Record<string, string> = {
133
+ contain: 'object-contain',
134
+ cover: 'object-cover',
135
+ fill: 'object-fill',
136
+ none: 'object-none',
137
+ scaleDown: 'object-scale-down'
138
+ };
139
+ const IMAGE_SIZE: Record<string, string> = {
140
+ icon: 'h-6 w-6',
141
+ avatar: 'h-10 w-10 rounded-full',
142
+ smallFeature: 'h-24 w-auto',
143
+ mediumFeature: 'h-40 w-auto',
144
+ largeFeature: 'h-64 w-auto',
145
+ header: 'h-48 w-full'
146
+ };
147
+ const HEADING_SIZE: Record<string, string> = {
148
+ h1: 'text-2xl',
149
+ h2: 'text-xl',
150
+ h3: 'text-lg',
151
+ h4: 'text-base',
152
+ h5: 'text-sm'
153
+ };
154
+ const HEADING_TAG: Record<string, string> = { h1: 'h1', h2: 'h2', h3: 'h3', h4: 'h4', h5: 'h5' };
155
+
156
+ // ── Icon resolution (Icon.name and EmptyState.icon) ────────────────────────
157
+ function iconNameFrom(key: string): string | undefined {
158
+ const value = raw(key);
159
+ if (typeof value === 'string') return value;
160
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
161
+ const object = value as Record<string, unknown>;
162
+ if (typeof object.svgPath === 'string') return undefined;
163
+ if (typeof object.path === 'string') {
164
+ const r = context.resolve(value, node.scopePrefix).value;
165
+ return typeof r === 'string' ? r : undefined;
166
+ }
167
+ }
168
+ return undefined;
169
+ }
170
+ const iconName = $derived(iconNameFrom('name'));
171
+ const iconSvgPath = $derived.by<string | undefined>(() => {
172
+ const value = raw('name');
173
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
174
+ const svgPath = (value as Record<string, unknown>).svgPath;
175
+ if (typeof svgPath === 'string' && A2UI_SVG_PATH_RE.test(svgPath)) return svgPath;
176
+ }
177
+ return undefined;
178
+ });
179
+ const IconComp = $derived((iconName && context.icons[iconName]) || context.fallbackIcon);
180
+ const emptyStateIcon = $derived.by(() => {
181
+ const name = iconNameFrom('icon');
182
+ return name ? context.icons[name] : undefined;
183
+ });
184
+
185
+ // ── Action (Button) ─────────────────────────────────────────────────────────
186
+ const actionEvent = $derived.by(() => {
187
+ const action = raw('action');
188
+ if (action === null || typeof action !== 'object') return undefined;
189
+ const event = (action as Record<string, unknown>).event;
190
+ if (event === null || typeof event !== 'object') return undefined;
191
+ const name = (event as Record<string, unknown>).name;
192
+ return typeof name === 'string' ? (event as Record<string, unknown>) : undefined;
193
+ });
194
+ const openUrlArg = $derived.by<string | undefined>(() => {
195
+ const action = raw('action');
196
+ if (action === null || typeof action !== 'object') return undefined;
197
+ const fc = (action as Record<string, unknown>).functionCall;
198
+ if (fc === null || typeof fc !== 'object') return undefined;
199
+ if ((fc as Record<string, unknown>).call !== 'openUrl') return undefined;
200
+ const args = (fc as Record<string, unknown>).args;
201
+ const url =
202
+ args !== null && typeof args === 'object' ? (args as Record<string, unknown>).url : undefined;
203
+ const resolvedUrl = context.resolve(url, node.scopePrefix).value;
204
+ return typeof resolvedUrl === 'string' ? resolvedUrl : undefined;
205
+ });
206
+ const buttonUsable = $derived(actionEvent !== undefined || openUrlArg !== undefined);
207
+ const buttonDisabled = $derived(!buttonUsable || Boolean(resolved('disabled')));
208
+
209
+ function dispatchButton() {
210
+ if (actionEvent !== undefined) {
211
+ dispatchAction();
212
+ return;
213
+ }
214
+ if (openUrlArg !== undefined) {
215
+ const check = checkLinkUrl(openUrlArg, context.urlPolicy);
216
+ if (check.ok) window.open(check.href, '_blank', 'noopener,noreferrer');
217
+ }
218
+ }
219
+ function dispatchAction() {
220
+ const event = actionEvent;
221
+ if (!event || !instance) return;
222
+ const resolvedContext: Record<string, unknown> = {};
223
+ const rawContext = event.context;
224
+ if (rawContext !== null && typeof rawContext === 'object' && !Array.isArray(rawContext)) {
225
+ for (const key of Object.keys(rawContext as Record<string, unknown>)) {
226
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
227
+ resolvedContext[key] = context.resolve(
228
+ (rawContext as Record<string, unknown>)[key],
229
+ node.scopePrefix
230
+ ).value;
231
+ }
232
+ }
233
+ // `sendDataModel` surfaces ship the whole model alongside the context, so a
234
+ // context the agent under-specified still tells it what the user entered.
235
+ const dataModel = context.actionDataModel?.();
236
+ const payload: A2uiActionEvent = {
237
+ ...(dataModel === undefined ? {} : { dataModel }),
238
+ name: event.name as string,
239
+ surfaceId: context.surfaceId,
240
+ sourceComponentId: instance.id,
241
+ timestamp: new Date().toISOString(),
242
+ context: resolvedContext
243
+ };
244
+ context.onAction?.(payload);
245
+ }
246
+
247
+ // ── Two-way inputs ──────────────────────────────────────────────────────────
248
+ function valuePointer(): string | undefined {
249
+ return bindingPointer(raw('value'), node.scopePrefix);
250
+ }
251
+
252
+ const textValue = $derived.by(() => {
253
+ const pointer = valuePointer();
254
+ if (pointer) return toInputString(resolved('value'));
255
+ return localText ?? toInputString(raw('value'));
256
+ });
257
+ function onTextInput(event: Event) {
258
+ const target = event.currentTarget as HTMLInputElement | HTMLTextAreaElement;
259
+ const pointer = valuePointer();
260
+ if (pointer) context.write(pointer, target.value);
261
+ else localText = target.value;
262
+ }
263
+
264
+ const boolValue = $derived.by(() => {
265
+ const pointer = valuePointer();
266
+ if (pointer) return Boolean(resolved('value'));
267
+ return localBool ?? (typeof raw('value') === 'boolean' ? (raw('value') as boolean) : false);
268
+ });
269
+ function onBoolChange(checked: boolean) {
270
+ const pointer = valuePointer();
271
+ if (pointer) context.write(pointer, checked);
272
+ else localBool = checked;
273
+ }
274
+
275
+ const sliderMin = $derived(numOr(raw('min'), 0));
276
+ const sliderMax = $derived(numOr(raw('max'), 100));
277
+ const sliderValue = $derived.by(() => {
278
+ const pointer = valuePointer();
279
+ if (pointer) {
280
+ const value = resolved('value');
281
+ return typeof value === 'number' && Number.isFinite(value) ? value : sliderMin;
282
+ }
283
+ return localNum ?? (typeof raw('value') === 'number' ? (raw('value') as number) : sliderMin);
284
+ });
285
+ function onSliderChange(value: number | [number, number]) {
286
+ const scalar = Array.isArray(value) ? value[0] : value;
287
+ const pointer = valuePointer();
288
+ if (pointer) context.write(pointer, scalar);
289
+ else localNum = scalar;
290
+ }
291
+
292
+ // RadioGroup: value is a single string in the data model.
293
+ const radioValue = $derived.by(() => {
294
+ const pointer = valuePointer();
295
+ if (pointer) {
296
+ const v = resolved('value');
297
+ return typeof v === 'string' ? v : undefined;
298
+ }
299
+ return localStr ?? (typeof raw('value') === 'string' ? (raw('value') as string) : undefined);
300
+ });
301
+ function onRadioChange(value: string) {
302
+ const pointer = valuePointer();
303
+ if (pointer) context.write(pointer, value);
304
+ else localStr = value;
305
+ }
306
+
307
+ // Select: value is ALWAYS a string array in the data model. Single-select
308
+ // adapts to Select's single-value shape (value = first, write a one-element
309
+ // array); multi-select maps straight through.
310
+ const selectValues = $derived.by(() => {
311
+ const pointer = valuePointer();
312
+ if (pointer) return toStringArray(resolved('value'));
313
+ return localList ?? toStringArray(raw('value'));
314
+ });
315
+ const selectMultiple = $derived(raw('multiple') === true);
316
+ function writeSelect(next: string[]) {
317
+ const pointer = valuePointer();
318
+ if (pointer) context.write(pointer, next);
319
+ else localList = next;
320
+ }
321
+ // Select is generic over string | number | boolean; our option values are
322
+ // always strings, so coerce on write-back to keep the model a string array.
323
+ function onSelectSingle(value: string | number | boolean | null) {
324
+ // Only null clears; an empty-string option value is a real (if degenerate)
325
+ // selection and must round-trip as [''], not [] (which would deselect it).
326
+ writeSelect(value === null ? [] : [String(value)]);
327
+ }
328
+ function onSelectMultiple(value: Array<string | number | boolean>) {
329
+ writeSelect(value.map((item) => String(item)));
330
+ }
331
+
332
+ const choiceOptions = $derived(
333
+ // `options` may itself be a { path } binding — that is how an agent shows a
334
+ // list it fetched mid-conversation (free slots, search hits) without
335
+ // rewriting the component. Resolve it first, then the per-option labels.
336
+ dedupeOptions(resolved('options'), (label) =>
337
+ coerceText(context.resolve(label, node.scopePrefix).value)
338
+ )
339
+ );
340
+
341
+ // ── DateTime (separate DatePicker / TimeInput) ─────────────────────────────
342
+ const dtValue = $derived.by(() => {
343
+ const pointer = valuePointer();
344
+ if (pointer) return toInputString(resolved('value'));
345
+ return localText ?? toInputString(raw('value'));
346
+ });
347
+ const dtParts = $derived(splitDateTime(dtValue));
348
+ const dtMinParts = $derived(splitDateTime(resolvedText('min')));
349
+ const dtMaxParts = $derived(splitDateTime(resolvedText('max')));
350
+ const dtMinDate = $derived(
351
+ dtMinParts.date ? (fromDateInputValue(dtMinParts.date) ?? undefined) : undefined
352
+ );
353
+ const dtMaxDate = $derived(
354
+ dtMaxParts.date ? (fromDateInputValue(dtMaxParts.date) ?? undefined) : undefined
355
+ );
356
+ function writeDateTime(next: string): void {
357
+ const pointer = valuePointer();
358
+ if (pointer) context.write(pointer, next);
359
+ else localText = next;
360
+ }
361
+ function onDateChange(picked: Date | undefined): void {
362
+ writeDateTime(picked ? toDateInputValue(picked) : '');
363
+ }
364
+ function onTimeChange(next: string | null): void {
365
+ writeDateTime(next ?? '');
366
+ }
367
+
368
+ // ── Stepper / Accordion display data ───────────────────────────────────────
369
+ const stepperSteps = $derived.by(() => {
370
+ const steps = raw('steps');
371
+ if (!Array.isArray(steps)) return [] as string[];
372
+ return steps.filter((s): s is string => typeof s === 'string');
373
+ });
374
+ const stepperCurrent = $derived.by(() => {
375
+ const v = resolved('current');
376
+ return typeof v === 'number' && Number.isFinite(v) ? Math.max(0, Math.floor(v)) : 0;
377
+ });
378
+
379
+ // Accordion: pair each labeledChildren item's label with the resolved child
380
+ // node (aligned by index — childRefs pushes one child per item). Each item
381
+ // gets a stable, unique AccordionItem value so open-state and the keyed each
382
+ // never collide, even on duplicate/dangling child ids.
383
+ const accordionItems = $derived.by(() => {
384
+ const items = raw('items');
385
+ if (!Array.isArray(items))
386
+ return [] as Array<{ value: string; label: string; child: A2uiRenderNode | undefined }>;
387
+ // Pair each item's label with its resolved panel by consuming child nodes by
388
+ // id (in order), NOT by raw index: a cyclic item.child is dropped from
389
+ // node.children entirely (no placeholder, unlike a dangling ref), which would
390
+ // shift every later panel up by one under a positional zip. Consume-by-id
391
+ // keeps labels aligned and leaves a cyclic/missing panel empty.
392
+ const pool = node.children.filter((child) => child.slot === 'items');
393
+ const used = new Set<number>();
394
+ return items.map((item, i) => {
395
+ const childId =
396
+ item !== null && typeof item === 'object' ? (item as { child?: unknown }).child : undefined;
397
+ let matched: A2uiRenderNode | undefined;
398
+ if (typeof childId === 'string') {
399
+ for (let j = 0; j < pool.length; j++) {
400
+ if (!used.has(j) && pool[j].id === childId) {
401
+ matched = pool[j];
402
+ used.add(j);
403
+ break;
404
+ }
405
+ }
406
+ }
407
+ return {
408
+ value: `item-${i}`,
409
+ label:
410
+ item !== null && typeof item === 'object'
411
+ ? coerceText(
412
+ context.resolve((item as { label?: unknown }).label, node.scopePrefix).value
413
+ )
414
+ : '',
415
+ child: matched
416
+ };
417
+ });
418
+ });
419
+ const accordionMultiple = $derived(raw('multiple') === true);
420
+
421
+ // ── Field-error (Input/Textarea use the native prop; date/time show a line) ──
422
+ const fieldError = $derived(resolvedText('error'));
423
+
424
+ // ── Image ──────────────────────────────────────────────────────────────────
425
+ const imageCheck = $derived(checkImageUrl(resolvedText('url'), context.urlPolicy));
426
+ // Avatar src is gated through the same policy; a blocked URL falls back to initials.
427
+ const avatarSrc = $derived.by(() => {
428
+ const src = resolvedText('src');
429
+ if (!src) return undefined;
430
+ const check = checkImageUrl(src, context.urlPolicy);
431
+ return check.ok ? check.href : undefined;
432
+ });
433
+ </script>
434
+
435
+ {#snippet faultChip(label: string)}
436
+ <span class={context.classes.errorChip}>
437
+ <DangerIcon size={14} class={context.classes.errorIcon} />
438
+ {label}
439
+ </span>
440
+ {/snippet}
441
+
442
+ {#if instance === null}
443
+ {#if context.streaming}
444
+ <span class={context.classes.pending}>
445
+ <Skeleton variant="text" width="6rem" />
446
+ <span class="sr-only">{context.labels.pending}</span>
447
+ </span>
448
+ {:else}
449
+ {@render faultChip(`${context.labels.unsupported}: ${node.id}`)}
450
+ {/if}
451
+ {:else if !spec || missingRequired}
452
+ {@render faultChip(`${context.labels.unsupported}: ${component || node.id}`)}
453
+ {:else if component === 'Column'}
454
+ <div
455
+ class={[
456
+ context.classes.column,
457
+ JUSTIFY[enumOr('justify', 'start')],
458
+ ALIGN[enumOr('align', 'stretch')]
459
+ ]}
460
+ style={weightStyle}
461
+ role={ariaLabel ? 'group' : undefined}
462
+ aria-label={ariaLabel}
463
+ >
464
+ {#each node.children as child (child.key)}
465
+ {@render renderChild(child, blockCtx)}
466
+ {/each}
467
+ </div>
468
+ {:else if component === 'Row'}
469
+ <div
470
+ class={[
471
+ context.classes.row,
472
+ JUSTIFY[enumOr('justify', 'start')],
473
+ ALIGN[enumOr('align', 'center')]
474
+ ]}
475
+ style={weightStyle}
476
+ role={ariaLabel ? 'group' : undefined}
477
+ aria-label={ariaLabel}
478
+ >
479
+ {#each node.children as child (child.key)}
480
+ {@render renderChild(child, blockCtx)}
481
+ {/each}
482
+ </div>
483
+ {:else if component === 'List'}
484
+ <ul
485
+ class={[context.classes.list, DIRECTION[enumOr('direction', 'vertical')]]}
486
+ style={weightStyle}
487
+ aria-label={ariaLabel}
488
+ >
489
+ {#each node.children as child (child.key)}
490
+ <li class={context.classes.listItem}>
491
+ {@render renderChild(child, blockCtx)}
492
+ </li>
493
+ {/each}
494
+ </ul>
495
+ {:else if component === 'Card'}
496
+ {@const headerNode = childByKey('header')}
497
+ {@const footerNode = childByKey('footer')}
498
+ {@const bodyNode = childByKey('child')}
499
+ {#snippet cardHeader()}
500
+ {#if headerNode}{@render renderChild(headerNode, blockCtx)}{/if}
501
+ {/snippet}
502
+ {#snippet cardFooter()}
503
+ {#if footerNode}{@render renderChild(footerNode, blockCtx)}{/if}
504
+ {/snippet}
505
+ <Card
506
+ variant={u(enumOr('variant', 'quiet'))}
507
+ aria-label={ariaLabel}
508
+ header={headerNode ? cardHeader : undefined}
509
+ footer={footerNode ? cardFooter : undefined}
510
+ >
511
+ {#if bodyNode}{@render renderChild(bodyNode, blockCtx)}{/if}
512
+ </Card>
513
+ {:else if component === 'Separator'}
514
+ <Separator
515
+ orientation={enumOr('orientation', 'horizontal') === 'vertical' ? 'vertical' : 'horizontal'}
516
+ style={weightStyle}
517
+ />
518
+ {:else if component === 'Section'}
519
+ <section class={styles.section()} style={weightStyle} aria-label={ariaLabel}>
520
+ <div class={styles.sectionHeader()}>
521
+ <h3 class={styles.sectionTitle()}>{resolvedText('title')}</h3>
522
+ {#if resolvedText('description')}
523
+ <p class={styles.sectionDescription()}>{resolvedText('description')}</p>
524
+ {/if}
525
+ </div>
526
+ {#if childByKey('child')}
527
+ {@render renderChild(childByKey('child')!, blockCtx)}
528
+ {/if}
529
+ </section>
530
+ {:else if component === 'EmptyState'}
531
+ {@const ctaNode = childByKey('cta')}
532
+ {#snippet emptyCta()}
533
+ {#if ctaNode}{@render renderChild(ctaNode, blockCtx)}{/if}
534
+ {/snippet}
535
+ <EmptyState
536
+ title={resolvedText('title')}
537
+ description={resolvedText('description') || undefined}
538
+ icon={emptyStateIcon}
539
+ cta={ctaNode ? emptyCta : undefined}
540
+ />
541
+ {:else if component === 'Text'}
542
+ {@const value = resolvedText('text')}
543
+ {@const variant = enumOr('variant', 'body')}
544
+ {#if context.inline}
545
+ <span class={context.classes.inlineText} style={weightStyle}>{value}</span>
546
+ {:else if variant === 'caption'}
547
+ <span class={context.classes.caption} style={weightStyle}>{value}</span>
548
+ {:else if variant === 'body'}
549
+ <p class={styles.body()} style={weightStyle}>{value}</p>
550
+ {:else}
551
+ <svelte:element
552
+ this={HEADING_TAG[variant] ?? 'h3'}
553
+ class={[context.classes.heading, HEADING_SIZE[variant]]}
554
+ style={weightStyle}
555
+ >
556
+ {value}
557
+ </svelte:element>
558
+ {/if}
559
+ {:else if component === 'RichText'}
560
+ <StreamingMarkdown
561
+ content={resolvedText('content')}
562
+ size="sm"
563
+ urlPolicy={context.urlPolicy}
564
+ streaming={false}
565
+ style={weightStyle}
566
+ />
567
+ {:else if component === 'Button'}
568
+ <Button
569
+ intent={u(enumOr('intent', 'neutral'))}
570
+ variant={u(enumOr('variant', 'filled'))}
571
+ size={u(enumOr('size', 'md'))}
572
+ style={weightStyle}
573
+ aria-label={ariaLabel}
574
+ disabled={buttonDisabled}
575
+ title={buttonUsable ? undefined : 'This action is not supported'}
576
+ onclick={buttonUsable ? dispatchButton : undefined}
577
+ >
578
+ {#if node.children[0]}
579
+ {@render renderChild(node.children[0], inlineCtx)}
580
+ {/if}
581
+ </Button>
582
+ {#if !buttonUsable}
583
+ <span class="sr-only">This action is not supported</span>
584
+ {/if}
585
+ {:else if component === 'Input'}
586
+ <Input
587
+ label={resolvedText('label')}
588
+ type={u(enumOr('inputType', 'text'))}
589
+ value={textValue}
590
+ placeholder={resolvedText('placeholder') || undefined}
591
+ error={fieldError || undefined}
592
+ oninput={onTextInput}
593
+ style={weightStyle}
594
+ aria-label={ariaLabel}
595
+ />
596
+ {:else if component === 'Textarea'}
597
+ <Textarea
598
+ label={resolvedText('label')}
599
+ value={textValue}
600
+ placeholder={resolvedText('placeholder') || undefined}
601
+ error={fieldError || undefined}
602
+ rows={typeof raw('rows') === 'number' ? (raw('rows') as number) : undefined}
603
+ oninput={onTextInput}
604
+ style={weightStyle}
605
+ aria-label={ariaLabel}
606
+ />
607
+ {:else if component === 'Checkbox'}
608
+ <Checkbox
609
+ label={resolvedText('label')}
610
+ checked={boolValue}
611
+ onCheckedChange={onBoolChange}
612
+ style={weightStyle}
613
+ aria-label={ariaLabel}
614
+ />
615
+ {:else if component === 'RadioGroup'}
616
+ <RadioGroup
617
+ label={resolvedText('label') || ariaLabel || undefined}
618
+ value={radioValue}
619
+ onValueChange={onRadioChange}
620
+ orientation={enumOr('orientation', 'vertical') === 'horizontal' ? 'horizontal' : 'vertical'}
621
+ style={weightStyle}
622
+ >
623
+ {#each choiceOptions as option (option.value)}
624
+ <RadioItem value={option.value} label={option.label} />
625
+ {/each}
626
+ </RadioGroup>
627
+ {:else if component === 'Select'}
628
+ {#if selectMultiple}
629
+ <Select
630
+ multiple
631
+ label={resolvedText('label') || undefined}
632
+ placeholder={resolvedText('placeholder') || undefined}
633
+ options={choiceOptions}
634
+ value={selectValues}
635
+ onValueChange={onSelectMultiple}
636
+ style={weightStyle}
637
+ aria-label={ariaLabel}
638
+ />
639
+ {:else}
640
+ <Select
641
+ label={resolvedText('label') || undefined}
642
+ placeholder={resolvedText('placeholder') || undefined}
643
+ options={choiceOptions}
644
+ value={selectValues[0] ?? null}
645
+ onValueChange={onSelectSingle}
646
+ style={weightStyle}
647
+ aria-label={ariaLabel}
648
+ />
649
+ {/if}
650
+ {:else if component === 'Slider'}
651
+ <!-- showValue is forced on: a generated surface has no other place to state
652
+ the current number, and the agent cannot ask for it (not a catalog prop). -->
653
+ <Slider
654
+ label={resolvedText('label') || undefined}
655
+ value={sliderValue}
656
+ min={sliderMin}
657
+ max={sliderMax}
658
+ step={typeof raw('step') === 'number' ? (raw('step') as number) : undefined}
659
+ showValue
660
+ onValueChange={onSliderChange}
661
+ style={weightStyle}
662
+ aria-label={ariaLabel}
663
+ />
664
+ {:else if component === 'Toggle'}
665
+ <Toggle
666
+ label={resolvedText('label')}
667
+ checked={boolValue}
668
+ onCheckedChange={onBoolChange}
669
+ style={weightStyle}
670
+ aria-label={ariaLabel}
671
+ />
672
+ {:else if component === 'DatePicker'}
673
+ {@const label = resolvedText('label')}
674
+ <div class={styles.field()} style={weightStyle}>
675
+ <DatePicker
676
+ label={label || ariaLabel || undefined}
677
+ value={dtParts.date || null}
678
+ onValueChange={onDateChange}
679
+ minDate={dtMinDate}
680
+ maxDate={dtMaxDate}
681
+ />
682
+ {#if fieldError}
683
+ <span class={styles.fieldError()}>{fieldError}</span>
684
+ {/if}
685
+ </div>
686
+ {:else if component === 'TimeInput'}
687
+ {@const label = resolvedText('label')}
688
+ <div
689
+ class={styles.field()}
690
+ style={weightStyle}
691
+ role={ariaLabel ? 'group' : undefined}
692
+ aria-label={ariaLabel}
693
+ >
694
+ <TimeInput
695
+ label={label || undefined}
696
+ value={dtParts.time || null}
697
+ onValueChange={onTimeChange}
698
+ withSeconds={dtParts.time.length > 5}
699
+ min={dtMinParts.time && !dtMinParts.date ? dtMinParts.time : undefined}
700
+ max={dtMaxParts.time && !dtMaxParts.date ? dtMaxParts.time : undefined}
701
+ />
702
+ {#if fieldError}
703
+ <span class={styles.fieldError()}>{fieldError}</span>
704
+ {/if}
705
+ </div>
706
+ {:else if component === 'Badge'}
707
+ {@const variant = enumOr('variant', 'filled')}
708
+ {#if variant === 'dot'}
709
+ <Badge
710
+ variant="dot"
711
+ intent={u(enumOr('intent', 'primary'))}
712
+ aria-label={ariaLabel || resolvedText('text') || undefined}
713
+ />
714
+ {:else}
715
+ <Badge
716
+ variant={variant as 'filled' | 'soft' | 'outlined'}
717
+ intent={u(enumOr('intent', 'primary'))}
718
+ >
719
+ {resolvedText('text')}
720
+ </Badge>
721
+ {/if}
722
+ {:else if component === 'Alert'}
723
+ {@const childNode = childByKey('child')}
724
+ <Alert
725
+ title={resolvedText('title')}
726
+ intent={u(enumOr('intent', 'primary'))}
727
+ variant={u(enumOr('variant', 'soft'))}
728
+ style={weightStyle}
729
+ >
730
+ {#if resolvedText('description')}
731
+ <span>{resolvedText('description')}</span>
732
+ {/if}
733
+ {#if childNode}
734
+ {@render renderChild(childNode, blockCtx)}
735
+ {/if}
736
+ </Alert>
737
+ {:else if component === 'Progress'}
738
+ {@const indeterminate = raw('indeterminate') === true}
739
+ {@const progressValue = resolved('value')}
740
+ <Progress
741
+ value={indeterminate || typeof progressValue !== 'number' || !Number.isFinite(progressValue)
742
+ ? undefined
743
+ : progressValue}
744
+ max={numOr(raw('max'), 100)}
745
+ intent={u(enumOr('intent', 'primary'))}
746
+ label={resolvedText('label') || undefined}
747
+ style={weightStyle}
748
+ aria-label={ariaLabel}
749
+ />
750
+ {:else if component === 'Stepper'}
751
+ <Stepper
752
+ activeStep={stepperCurrent}
753
+ orientation={enumOr('orientation', 'horizontal') === 'vertical' ? 'vertical' : 'horizontal'}
754
+ variant={u(enumOr('variant', 'default'))}
755
+ style={weightStyle}
756
+ aria-label={ariaLabel}
757
+ >
758
+ {#each stepperSteps as step, i (`${step}#${i}`)}
759
+ <StepperStep label={step} />
760
+ {/each}
761
+ </Stepper>
762
+ {:else if component === 'Accordion'}
763
+ <Accordion
764
+ type={accordionMultiple ? 'multiple' : 'single'}
765
+ variant={u(enumOr('variant', 'default'))}
766
+ style={weightStyle}
767
+ aria-label={ariaLabel}
768
+ >
769
+ {#each accordionItems as item (item.value)}
770
+ <AccordionItem value={item.value} title={item.label}>
771
+ {#if item.child}
772
+ {@render renderChild(item.child, blockCtx)}
773
+ {/if}
774
+ </AccordionItem>
775
+ {/each}
776
+ </Accordion>
777
+ {:else if component === 'Avatar'}
778
+ <Avatar
779
+ src={avatarSrc}
780
+ name={resolvedText('name') || undefined}
781
+ alt={resolvedText('name') || ariaLabel || undefined}
782
+ variant={u(enumOr('variant', 'circle'))}
783
+ size={u(enumOr('size', 'md'))}
784
+ />
785
+ {:else if component === 'Icon'}
786
+ {#if iconSvgPath}
787
+ <svg
788
+ viewBox="0 0 24 24"
789
+ width="20"
790
+ height="20"
791
+ class={context.classes.svgIcon}
792
+ style={weightStyle}
793
+ stroke-width="2"
794
+ stroke-linecap="round"
795
+ stroke-linejoin="round"
796
+ role={ariaLabel ? 'img' : undefined}
797
+ aria-label={ariaLabel}
798
+ aria-hidden={ariaLabel ? undefined : 'true'}
799
+ >
800
+ <path d={iconSvgPath} />
801
+ </svg>
802
+ {:else}
803
+ <span
804
+ class={context.classes.icon}
805
+ style={weightStyle}
806
+ role={ariaLabel ? 'img' : undefined}
807
+ aria-label={ariaLabel}
808
+ aria-hidden={ariaLabel ? undefined : 'true'}
809
+ >
810
+ <IconComp size={20} />
811
+ </span>
812
+ {/if}
813
+ {:else if component === 'Image'}
814
+ {@const description = resolvedText('description')}
815
+ {#if imageCheck.ok}
816
+ <img
817
+ src={imageCheck.href}
818
+ alt={description}
819
+ class={[
820
+ context.classes.image,
821
+ FIT[enumOr('fit', 'cover')],
822
+ IMAGE_SIZE[enumOr('variant', 'mediumFeature')]
823
+ ]}
824
+ style={weightStyle}
825
+ />
826
+ {:else}
827
+ <span class={context.classes.blockedChip} style={weightStyle}>
828
+ {context.labels.blockedImage}{description ? `: ${description}` : ''}
829
+ </span>
830
+ {/if}
831
+ {/if}