@urbicon-ui/blocks 6.38.0 → 6.40.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.
Files changed (35) hide show
  1. package/dist/components/Calendar/calendar.variants.d.ts +63 -63
  2. package/dist/components/Chat/A2UIView/A2UINode.svelte +703 -0
  3. package/dist/components/Chat/A2UIView/A2UINode.svelte.d.ts +10 -0
  4. package/dist/components/Chat/A2UIView/A2UIView.svelte +293 -0
  5. package/dist/components/Chat/A2UIView/A2UIView.svelte.d.ts +4 -0
  6. package/dist/components/Chat/A2UIView/a2ui-data.d.ts +65 -0
  7. package/dist/components/Chat/A2UIView/a2ui-data.js +283 -0
  8. package/dist/components/Chat/A2UIView/a2ui-prompt.d.ts +18 -0
  9. package/dist/components/Chat/A2UIView/a2ui-prompt.js +147 -0
  10. package/dist/components/Chat/A2UIView/a2ui-registry.d.ts +64 -0
  11. package/dist/components/Chat/A2UIView/a2ui-registry.js +390 -0
  12. package/dist/components/Chat/A2UIView/a2ui-render.d.ts +96 -0
  13. package/dist/components/Chat/A2UIView/a2ui-render.js +140 -0
  14. package/dist/components/Chat/A2UIView/a2ui-validate.d.ts +75 -0
  15. package/dist/components/Chat/A2UIView/a2ui-validate.js +773 -0
  16. package/dist/components/Chat/A2UIView/a2ui-view.variants.d.ts +80 -0
  17. package/dist/components/Chat/A2UIView/a2ui-view.variants.js +62 -0
  18. package/dist/components/Chat/A2UIView/a2ui.types.d.ts +119 -0
  19. package/dist/components/Chat/A2UIView/a2ui.types.js +85 -0
  20. package/dist/components/Chat/A2UIView/index.d.ts +102 -0
  21. package/dist/components/Chat/A2UIView/index.js +7 -0
  22. package/dist/components/Chat/ChatMessage/ChatMessage.svelte +4 -1
  23. package/dist/components/Chat/index.d.ts +2 -0
  24. package/dist/components/Chat/index.js +1 -0
  25. package/dist/components/CopyButton/copy-button.variants.d.ts +4 -4
  26. package/dist/i18n/index.d.ts +398 -2
  27. package/dist/primitives/Alert/alert.variants.d.ts +8 -8
  28. package/dist/primitives/JourneyTimeline/journey-timeline.variants.d.ts +22 -22
  29. package/dist/primitives/JourneyTimeline/journey-timeline.variants.js +3 -1
  30. package/dist/primitives/Spinner/spinner.variants.d.ts +16 -16
  31. package/dist/primitives/Stepper/stepper.variants.d.ts +11 -11
  32. package/dist/primitives/Toast/toast.variants.d.ts +12 -12
  33. package/dist/primitives/Tooltip/tooltip.variants.d.ts +3 -3
  34. package/dist/style/semantic.css +11 -3
  35. package/package.json +3 -3
@@ -0,0 +1,703 @@
1
+ <script lang="ts">
2
+ import {
3
+ Button,
4
+ Card,
5
+ Checkbox,
6
+ Input,
7
+ RadioGroup,
8
+ RadioItem,
9
+ Separator,
10
+ Skeleton,
11
+ Slider,
12
+ Textarea
13
+ } from '../../../primitives';
14
+ import { resolveIcon } from '../../../icons';
15
+ import DangerCircleIconDefault from '../../../icons/DangerCircleIcon.svelte';
16
+ import { fromDateInputValue, toDateInputValue } from '../../../utils/date';
17
+ import type { Snippet } from 'svelte';
18
+ import DatePicker from '../../DatePicker/DatePicker.svelte';
19
+ import TimeInput from '../../TimeInput/TimeInput.svelte';
20
+ import StreamingMarkdown from '../StreamingMarkdown/StreamingMarkdown.svelte';
21
+ import { checkImageUrl, checkLinkUrl } from '../markdown/url-policy.js';
22
+ import type { A2uiActionEvent } from './a2ui.types';
23
+ import { A2UI_REGISTRY, A2UI_SVG_PATH_RE } from './a2ui-registry';
24
+ import type { A2uiRenderContext, A2uiRenderNode } from './a2ui-render';
25
+ import { bindingPointer, toInputString, toStringArray } from './a2ui-render';
26
+
27
+ // Recursive dispatcher: one instance renders one node of the assembled A2UI
28
+ // render tree, mapping the trusted catalog subset onto real Urbicon
29
+ // primitives. Children come pre-expanded on `node.children` (templates, scope,
30
+ // bounds all resolved upstream in `buildRenderTree`) and are rendered through
31
+ // the `renderChild` snippet (a self-referencing snippet A2UIView threads down)
32
+ // with stable keys, so a keystroke-triggered rebuild never remounts a focused
33
+ // input. Recursing via a snippet — not a self-import — sidesteps the
34
+ // circular-component-type resolution that self-imports trip in svelte-check.
35
+ // The single `context` prop threads resolved classes, the data-binding
36
+ // resolver, two-way write-back and the action sink (house pattern of
37
+ // `md-context.ts`, not a Svelte context).
38
+ let {
39
+ node,
40
+ context,
41
+ renderChild
42
+ }: {
43
+ node: A2uiRenderNode;
44
+ context: A2uiRenderContext;
45
+ renderChild: Snippet<[A2uiRenderNode, A2uiRenderContext]>;
46
+ } = $props();
47
+
48
+ const DangerIcon = resolveIcon('danger', DangerCircleIconDefault);
49
+
50
+ // Local fallback state for inputs whose `value` is a literal (no data-model
51
+ // path to write back into). Bound inputs bypass these entirely.
52
+ let localText = $state<string | undefined>(undefined);
53
+ let localBool = $state<boolean | undefined>(undefined);
54
+ let localNum = $state<number | undefined>(undefined);
55
+ let localList = $state<string[] | undefined>(undefined);
56
+
57
+ const instance = $derived(node.instance);
58
+ const nodeProps = $derived(instance?.props);
59
+ const component = $derived(instance?.component ?? '');
60
+ const spec = $derived(component ? A2UI_REGISTRY[component] : undefined);
61
+
62
+ // A component is a visible fault when it is unknown/unsupported or is missing
63
+ // a required prop (the processor already emitted the reportable issue).
64
+ const missingRequired = $derived.by(() => {
65
+ if (!spec || !instance) return false;
66
+ for (const [key, propSpec] of Object.entries(spec.props)) {
67
+ if (propSpec.required && !instance.props.has(key)) return true;
68
+ }
69
+ return false;
70
+ });
71
+
72
+ function raw(key: string): unknown {
73
+ return nodeProps?.get(key);
74
+ }
75
+ function enumOr(key: string, fallback: string): string {
76
+ const value = nodeProps?.get(key);
77
+ return typeof value === 'string' ? value : fallback;
78
+ }
79
+ function numOr(value: unknown, fallback: number): number {
80
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
81
+ }
82
+ function text(value: unknown): string {
83
+ if (typeof value === 'string') return value;
84
+ if (typeof value === 'number' && Number.isFinite(value)) return String(value);
85
+ return '';
86
+ }
87
+ function resolved(key: string): unknown {
88
+ return context.resolve(nodeProps?.get(key), node.scopePrefix).value;
89
+ }
90
+
91
+ const ariaLabel = $derived.by(() => {
92
+ const a11y = nodeProps?.get('accessibility');
93
+ if (a11y !== null && typeof a11y === 'object') {
94
+ const label = (a11y as Record<string, unknown>).label;
95
+ if (typeof label === 'string' && label.length > 0) return label;
96
+ }
97
+ return undefined;
98
+ });
99
+
100
+ const weightStyle = $derived(node.weight != null ? `flex-grow:${node.weight}` : undefined);
101
+
102
+ // Child contexts: block containers reset the inline flag; a Button label sets
103
+ // it (Text then renders as a plain inline span). `context` is a plain object,
104
+ // not a $state proxy, so spreading it is safe.
105
+ const blockCtx = $derived(context.inline ? { ...context, inline: false } : context);
106
+ const inlineCtx = $derived({ ...context, inline: true });
107
+
108
+ // ── Enum → complete class-string lookups (kept as literals for Tailwind's scanner) ──
109
+ const JUSTIFY: Record<string, string> = {
110
+ start: 'justify-start',
111
+ center: 'justify-center',
112
+ end: 'justify-end',
113
+ spaceBetween: 'justify-between',
114
+ spaceAround: 'justify-around',
115
+ spaceEvenly: 'justify-evenly',
116
+ stretch: 'justify-stretch'
117
+ };
118
+ const ALIGN: Record<string, string> = {
119
+ start: 'items-start',
120
+ center: 'items-center',
121
+ end: 'items-end',
122
+ stretch: 'items-stretch'
123
+ };
124
+ const DIRECTION: Record<string, string> = { vertical: 'flex-col', horizontal: 'flex-row' };
125
+ const FIT: Record<string, string> = {
126
+ contain: 'object-contain',
127
+ cover: 'object-cover',
128
+ fill: 'object-fill',
129
+ none: 'object-none',
130
+ scaleDown: 'object-scale-down'
131
+ };
132
+ const IMAGE_SIZE: Record<string, string> = {
133
+ icon: 'h-6 w-6',
134
+ avatar: 'h-10 w-10 rounded-full',
135
+ smallFeature: 'h-24 w-auto',
136
+ mediumFeature: 'h-40 w-auto',
137
+ largeFeature: 'h-64 w-auto',
138
+ header: 'h-48 w-full'
139
+ };
140
+ const HEADING_SIZE: Record<string, string> = {
141
+ h1: 'text-2xl',
142
+ h2: 'text-xl',
143
+ h3: 'text-lg',
144
+ h4: 'text-base',
145
+ h5: 'text-sm'
146
+ };
147
+ const HEADING_TAG: Record<string, string> = {
148
+ h1: 'h1',
149
+ h2: 'h2',
150
+ h3: 'h3',
151
+ h4: 'h4',
152
+ h5: 'h5'
153
+ };
154
+ const INPUT_TYPE: Record<string, string> = {
155
+ shortText: 'text',
156
+ number: 'number',
157
+ obscured: 'password'
158
+ };
159
+ const BUTTON_STYLE: Record<
160
+ string,
161
+ { variant: 'filled' | 'text'; intent: 'primary' | 'neutral' }
162
+ > = {
163
+ default: { variant: 'filled', intent: 'neutral' },
164
+ primary: { variant: 'filled', intent: 'primary' },
165
+ borderless: { variant: 'text', intent: 'neutral' }
166
+ };
167
+
168
+ // ── Icon resolution ──────────────────────────────────────────────────────
169
+ const iconName = $derived.by<string | undefined>(() => {
170
+ const value = raw('name');
171
+ if (typeof value === 'string') return value;
172
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
173
+ const object = value as Record<string, unknown>;
174
+ if (typeof object.svgPath === 'string') return undefined; // handled below
175
+ if (typeof object.path === 'string') {
176
+ const r = context.resolve(value, node.scopePrefix).value;
177
+ return typeof r === 'string' ? r : undefined;
178
+ }
179
+ }
180
+ return undefined;
181
+ });
182
+ const iconSvgPath = $derived.by<string | undefined>(() => {
183
+ const value = raw('name');
184
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
185
+ const svgPath = (value as Record<string, unknown>).svgPath;
186
+ // Re-apply the grammar guard before inlining — defense in depth.
187
+ if (typeof svgPath === 'string' && A2UI_SVG_PATH_RE.test(svgPath)) return svgPath;
188
+ }
189
+ return undefined;
190
+ });
191
+ const IconComp = $derived((iconName && context.icons[iconName]) || context.fallbackIcon);
192
+
193
+ // ── Action ────────────────────────────────────────────────────────────────
194
+ const actionEvent = $derived.by(() => {
195
+ const action = raw('action');
196
+ if (action === null || typeof action !== 'object') return undefined;
197
+ const event = (action as Record<string, unknown>).event;
198
+ if (event === null || typeof event !== 'object') return undefined;
199
+ const name = (event as Record<string, unknown>).name;
200
+ return typeof name === 'string' ? (event as Record<string, unknown>) : undefined;
201
+ });
202
+
203
+ // A local `openUrl` function-call action: the only client-side action the
204
+ // subset supports. Every other functionCall is unsupported (the validator
205
+ // already warns) and leaves the button inert.
206
+ const openUrlArg = $derived.by<string | undefined>(() => {
207
+ const action = raw('action');
208
+ if (action === null || typeof action !== 'object') return undefined;
209
+ const fc = (action as Record<string, unknown>).functionCall;
210
+ if (fc === null || typeof fc !== 'object') return undefined;
211
+ const call = (fc as Record<string, unknown>).call;
212
+ if (call !== 'openUrl') return undefined;
213
+ const args = (fc as Record<string, unknown>).args;
214
+ const url =
215
+ args !== null && typeof args === 'object' ? (args as Record<string, unknown>).url : undefined;
216
+ const resolvedUrl = context.resolve(url, node.scopePrefix).value;
217
+ return typeof resolvedUrl === 'string' ? resolvedUrl : undefined;
218
+ });
219
+ const buttonUsable = $derived(actionEvent !== undefined || openUrlArg !== undefined);
220
+
221
+ function dispatchButton() {
222
+ if (actionEvent !== undefined) {
223
+ dispatchAction();
224
+ return;
225
+ }
226
+ if (openUrlArg !== undefined) {
227
+ // Gate the URL through the same policy as markdown links before opening.
228
+ const check = checkLinkUrl(openUrlArg, context.urlPolicy);
229
+ if (check.ok) window.open(check.href, '_blank', 'noopener,noreferrer');
230
+ }
231
+ }
232
+
233
+ function dispatchAction() {
234
+ const event = actionEvent;
235
+ if (!event || !instance) return;
236
+ // Build context on a plain object; prototype keys are skipped (never used as
237
+ // a key, so no pollution) — the processor already flagged them.
238
+ const resolvedContext: Record<string, unknown> = {};
239
+ const rawContext = event.context;
240
+ if (rawContext !== null && typeof rawContext === 'object' && !Array.isArray(rawContext)) {
241
+ for (const key of Object.keys(rawContext as Record<string, unknown>)) {
242
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
243
+ resolvedContext[key] = context.resolve(
244
+ (rawContext as Record<string, unknown>)[key],
245
+ node.scopePrefix
246
+ ).value;
247
+ }
248
+ }
249
+ const payload: A2uiActionEvent = {
250
+ name: event.name as string,
251
+ surfaceId: context.surfaceId,
252
+ sourceComponentId: instance.id,
253
+ timestamp: new Date().toISOString(),
254
+ context: resolvedContext
255
+ };
256
+ context.onAction?.(payload);
257
+ }
258
+
259
+ // ── Two-way inputs ──────────────────────────────────────────────────────
260
+ function valuePointer(): string | undefined {
261
+ return bindingPointer(raw('value'), node.scopePrefix);
262
+ }
263
+
264
+ const textValue = $derived.by(() => {
265
+ const pointer = valuePointer();
266
+ if (pointer) return toInputString(resolved('value'));
267
+ return localText ?? toInputString(raw('value'));
268
+ });
269
+ function onTextInput(event: Event) {
270
+ const target = event.currentTarget as HTMLInputElement | HTMLTextAreaElement;
271
+ const pointer = valuePointer();
272
+ if (pointer) context.write(pointer, target.value);
273
+ else localText = target.value;
274
+ }
275
+
276
+ const boolValue = $derived.by(() => {
277
+ const pointer = valuePointer();
278
+ if (pointer) return Boolean(resolved('value'));
279
+ return localBool ?? (typeof raw('value') === 'boolean' ? (raw('value') as boolean) : false);
280
+ });
281
+ function onBoolChange(checked: boolean) {
282
+ const pointer = valuePointer();
283
+ if (pointer) context.write(pointer, checked);
284
+ else localBool = checked;
285
+ }
286
+
287
+ const sliderMin = $derived(numOr(raw('min'), 0));
288
+ const sliderMax = $derived(numOr(raw('max'), 100));
289
+ const sliderValue = $derived.by(() => {
290
+ const pointer = valuePointer();
291
+ if (pointer) {
292
+ const value = resolved('value');
293
+ return typeof value === 'number' && Number.isFinite(value) ? value : sliderMin;
294
+ }
295
+ return localNum ?? (typeof raw('value') === 'number' ? (raw('value') as number) : sliderMin);
296
+ });
297
+ function onSliderChange(value: number | [number, number]) {
298
+ const scalar = Array.isArray(value) ? value[0] : value;
299
+ const pointer = valuePointer();
300
+ if (pointer) context.write(pointer, scalar);
301
+ else localNum = scalar;
302
+ }
303
+
304
+ const choiceSelected = $derived.by(() => {
305
+ const pointer = valuePointer();
306
+ if (pointer) return toStringArray(resolved('value'));
307
+ return localList ?? toStringArray(raw('value'));
308
+ });
309
+ const choiceOptions = $derived.by(() => {
310
+ const options = raw('options');
311
+ if (!Array.isArray(options)) return [] as Array<{ value: string; label: string }>;
312
+ // Dedupe by value (first wins): the validator warns on duplicates, but the
313
+ // keyed `{#each}` below is on option.value — two equal keys throw Svelte's
314
+ // `each_key_duplicate` (a hard crash that would break the "never throw"
315
+ // contract for an untrusted payload).
316
+ const seen = new Set<string>();
317
+ const out: Array<{ value: string; label: string }> = [];
318
+ for (const option of options) {
319
+ if (option === null || typeof option !== 'object' || typeof option.value !== 'string')
320
+ continue;
321
+ if (seen.has(option.value)) continue;
322
+ seen.add(option.value);
323
+ out.push({
324
+ value: option.value,
325
+ label: text(context.resolve(option.label, node.scopePrefix).value)
326
+ });
327
+ }
328
+ return out;
329
+ });
330
+ const choiceLabel = $derived(text(resolved('label')));
331
+ function writeChoice(next: string[]) {
332
+ const pointer = valuePointer();
333
+ if (pointer) context.write(pointer, next);
334
+ else localList = next;
335
+ }
336
+ function onRadioChange(value: string) {
337
+ writeChoice([value]);
338
+ }
339
+ function onOptionToggle(value: string, checked: boolean) {
340
+ const current = choiceSelected;
341
+ if (checked) writeChoice(current.includes(value) ? current : [...current, value]);
342
+ else writeChoice(current.filter((item) => item !== value));
343
+ }
344
+
345
+ const buttonStyle = $derived(BUTTON_STYLE[enumOr('variant', 'default')] ?? BUTTON_STYLE.default);
346
+
347
+ // ── DateTimeInput ────────────────────────────────────────────────────────
348
+ // The spec's value is one ISO 8601 STRING covering date, time, or both. The
349
+ // renderer treats it as a timezone-NAIVE literal: a trailing offset/Z is
350
+ // stripped for display and never re-attached on write — timezone semantics
351
+ // belong to the agent, not to a UI renderer without timezone context.
352
+ const dtMode = $derived.by<'date' | 'time' | 'datetime'>(() => {
353
+ const enableDate = raw('enableDate') === true;
354
+ const enableTime = raw('enableTime') === true;
355
+ if (enableDate && enableTime) return 'datetime';
356
+ if (enableTime) return 'time';
357
+ // Includes the neither-flag payload: read tolerantly as a date input (the
358
+ // validator already reported DATETIME_NO_MODE).
359
+ return 'date';
360
+ });
361
+
362
+ function normalizeTimePart(value: string): string {
363
+ const match = /^(\d{2}:\d{2}(?::\d{2})?)/.exec(value);
364
+ return match ? match[1] : '';
365
+ }
366
+ function splitDateTime(value: string): { date: string; time: string } {
367
+ const trimmed = value.trim();
368
+ if (trimmed === '') return { date: '', time: '' };
369
+ const tIndex = trimmed.indexOf('T');
370
+ if (tIndex !== -1) {
371
+ return {
372
+ date: trimmed.slice(0, tIndex),
373
+ time: normalizeTimePart(trimmed.slice(tIndex + 1))
374
+ };
375
+ }
376
+ if (trimmed.includes(':')) return { date: '', time: normalizeTimePart(trimmed) };
377
+ return { date: trimmed, time: '' };
378
+ }
379
+
380
+ const dtParts = $derived(splitDateTime(textValue));
381
+ const dtMinParts = $derived(splitDateTime(text(resolved('min'))));
382
+ const dtMaxParts = $derived(splitDateTime(text(resolved('max'))));
383
+ const dtMinDate = $derived(
384
+ dtMinParts.date ? (fromDateInputValue(dtMinParts.date) ?? undefined) : undefined
385
+ );
386
+ const dtMaxDate = $derived(
387
+ dtMaxParts.date ? (fromDateInputValue(dtMaxParts.date) ?? undefined) : undefined
388
+ );
389
+ // Time bounds apply only when the bound itself is time-only: on a date-time
390
+ // bound the time limit would depend on the picked date — deliberately out of
391
+ // scope for this renderer (the date part is still enforced above).
392
+ const dtMinTime = $derived(dtMinParts.time && !dtMinParts.date ? dtMinParts.time : undefined);
393
+ const dtMaxTime = $derived(dtMaxParts.time && !dtMaxParts.date ? dtMaxParts.time : undefined);
394
+
395
+ function writeDateTime(nextDate: string, nextTime: string): void {
396
+ // Partial fills stay valid partial ISO: date-only / time-only / "".
397
+ const next = nextDate && nextTime ? `${nextDate}T${nextTime}` : nextDate || nextTime || '';
398
+ const pointer = valuePointer();
399
+ if (pointer) context.write(pointer, next);
400
+ else localText = next;
401
+ }
402
+ function onDtDateChange(picked: Date | undefined): void {
403
+ writeDateTime(
404
+ picked ? toDateInputValue(picked) : '',
405
+ dtMode === 'datetime' ? dtParts.time : ''
406
+ );
407
+ }
408
+ function onDtTimeChange(nextTime: string | null): void {
409
+ writeDateTime(dtMode === 'datetime' ? dtParts.date : '', nextTime ?? '');
410
+ }
411
+ </script>
412
+
413
+ {#snippet faultChip(label: string)}
414
+ <span class={context.classes.errorChip}>
415
+ <DangerIcon size={14} class={context.classes.errorIcon} />
416
+ {label}
417
+ </span>
418
+ {/snippet}
419
+
420
+ {#if instance === null}
421
+ {#if context.streaming}
422
+ <span class={context.classes.pending}>
423
+ <Skeleton variant="text" width="6rem" />
424
+ <span class="sr-only">{context.labels.pending}</span>
425
+ </span>
426
+ {:else}
427
+ {@render faultChip(`${context.labels.unsupported}: ${node.id}`)}
428
+ {/if}
429
+ {:else if !spec || missingRequired}
430
+ {@render faultChip(`${context.labels.unsupported}: ${component || node.id}`)}
431
+ {:else if component === 'Column'}
432
+ <!-- A plain <div> is role=generic, which forbids aria-label (axe
433
+ aria-prohibited-attr, ignored by SR); a labelled group needs role="group". -->
434
+ <div
435
+ class={[
436
+ context.classes.column,
437
+ JUSTIFY[enumOr('justify', 'start')],
438
+ ALIGN[enumOr('align', 'stretch')]
439
+ ]}
440
+ style={weightStyle}
441
+ role={ariaLabel ? 'group' : undefined}
442
+ aria-label={ariaLabel}
443
+ >
444
+ {#each node.children as child (child.key)}
445
+ {@render renderChild(child, blockCtx)}
446
+ {/each}
447
+ </div>
448
+ {:else if component === 'Row'}
449
+ <div
450
+ class={[
451
+ context.classes.row,
452
+ JUSTIFY[enumOr('justify', 'start')],
453
+ ALIGN[enumOr('align', 'stretch')]
454
+ ]}
455
+ style={weightStyle}
456
+ role={ariaLabel ? 'group' : undefined}
457
+ aria-label={ariaLabel}
458
+ >
459
+ {#each node.children as child (child.key)}
460
+ {@render renderChild(child, blockCtx)}
461
+ {/each}
462
+ </div>
463
+ {:else if component === 'List'}
464
+ <ul
465
+ class={[
466
+ context.classes.list,
467
+ DIRECTION[enumOr('direction', 'vertical')],
468
+ ALIGN[enumOr('align', 'stretch')]
469
+ ]}
470
+ style={weightStyle}
471
+ aria-label={ariaLabel}
472
+ >
473
+ {#each node.children as child (child.key)}
474
+ <li class={context.classes.listItem}>
475
+ {@render renderChild(child, blockCtx)}
476
+ </li>
477
+ {/each}
478
+ </ul>
479
+ {:else if component === 'Card'}
480
+ <Card style={weightStyle} role={ariaLabel ? 'group' : undefined} aria-label={ariaLabel}>
481
+ {#if node.children[0]}
482
+ {@render renderChild(node.children[0], blockCtx)}
483
+ {/if}
484
+ </Card>
485
+ {:else if component === 'Divider'}
486
+ <Separator
487
+ orientation={enumOr('axis', 'horizontal') === 'vertical' ? 'vertical' : 'horizontal'}
488
+ />
489
+ {:else if component === 'Text'}
490
+ {@const value = text(resolved('text'))}
491
+ {@const variant = enumOr('variant', 'body')}
492
+ {#if context.inline}
493
+ <span class={context.classes.inlineText} style={weightStyle}>{value}</span>
494
+ {:else if variant === 'caption'}
495
+ <span class={context.classes.caption} style={weightStyle}>{value}</span>
496
+ {:else if variant === 'body'}
497
+ <StreamingMarkdown
498
+ content={value}
499
+ size="sm"
500
+ urlPolicy={context.urlPolicy}
501
+ streaming={false}
502
+ style={weightStyle}
503
+ />
504
+ {:else}
505
+ <svelte:element
506
+ this={HEADING_TAG[variant] ?? 'h3'}
507
+ class={[context.classes.heading, HEADING_SIZE[variant]]}
508
+ style={weightStyle}
509
+ >
510
+ {value}
511
+ </svelte:element>
512
+ {/if}
513
+ {:else if component === 'Image'}
514
+ {@const url = text(resolved('url'))}
515
+ {@const description = text(resolved('description'))}
516
+ {@const check = checkImageUrl(url, context.urlPolicy)}
517
+ {#if check.ok}
518
+ <img
519
+ src={check.href}
520
+ alt={description}
521
+ class={[
522
+ context.classes.image,
523
+ FIT[enumOr('fit', 'fill')],
524
+ IMAGE_SIZE[enumOr('variant', 'mediumFeature')]
525
+ ]}
526
+ style={weightStyle}
527
+ />
528
+ {:else}
529
+ <span class={context.classes.blockedChip} style={weightStyle}>
530
+ {context.labels.blockedImage}{description ? `: ${description}` : ''}
531
+ </span>
532
+ {/if}
533
+ {:else if component === 'Icon'}
534
+ {#if iconSvgPath}
535
+ <svg
536
+ viewBox="0 0 24 24"
537
+ width="20"
538
+ height="20"
539
+ class={context.classes.svgIcon}
540
+ style={weightStyle}
541
+ stroke-width="2"
542
+ stroke-linecap="round"
543
+ stroke-linejoin="round"
544
+ role={ariaLabel ? 'img' : undefined}
545
+ aria-label={ariaLabel}
546
+ aria-hidden={ariaLabel ? undefined : 'true'}
547
+ >
548
+ <path d={iconSvgPath} />
549
+ </svg>
550
+ {:else}
551
+ <span
552
+ class={context.classes.icon}
553
+ style={weightStyle}
554
+ role={ariaLabel ? 'img' : undefined}
555
+ aria-label={ariaLabel}
556
+ aria-hidden={ariaLabel ? undefined : 'true'}
557
+ >
558
+ <IconComp size={20} />
559
+ </span>
560
+ {/if}
561
+ {:else if component === 'Button'}
562
+ <Button
563
+ variant={buttonStyle.variant}
564
+ intent={buttonStyle.intent}
565
+ style={weightStyle}
566
+ aria-label={ariaLabel}
567
+ disabled={!buttonUsable}
568
+ title={buttonUsable ? undefined : 'This action is not supported'}
569
+ onclick={buttonUsable ? dispatchButton : undefined}
570
+ >
571
+ {#if node.children[0]}
572
+ {@render renderChild(node.children[0], inlineCtx)}
573
+ {/if}
574
+ </Button>
575
+ {#if !buttonUsable}
576
+ <!-- A disabled button drops out of the tab order, so `title` is invisible to
577
+ screen readers; an adjacent sr-only note is reachable in browse mode. -->
578
+ <span class="sr-only">This action is not supported</span>
579
+ {/if}
580
+ {:else if component === 'TextField'}
581
+ {@const label = text(resolved('label'))}
582
+ {@const variant = enumOr('variant', 'shortText')}
583
+ {#if variant === 'longText'}
584
+ <Textarea
585
+ {label}
586
+ value={textValue}
587
+ oninput={onTextInput}
588
+ style={weightStyle}
589
+ aria-label={ariaLabel}
590
+ />
591
+ {:else}
592
+ <Input
593
+ {label}
594
+ type={INPUT_TYPE[variant] ?? 'text'}
595
+ value={textValue}
596
+ oninput={onTextInput}
597
+ style={weightStyle}
598
+ aria-label={ariaLabel}
599
+ />
600
+ {/if}
601
+ {:else if component === 'CheckBox'}
602
+ <Checkbox
603
+ label={text(resolved('label'))}
604
+ checked={boolValue}
605
+ onCheckedChange={onBoolChange}
606
+ style={weightStyle}
607
+ aria-label={ariaLabel}
608
+ />
609
+ {:else if component === 'ChoicePicker'}
610
+ {#if enumOr('variant', 'mutuallyExclusive') === 'multipleSelection'}
611
+ <div
612
+ class={context.classes.choiceGroup}
613
+ style={weightStyle}
614
+ role="group"
615
+ aria-label={ariaLabel || choiceLabel || undefined}
616
+ >
617
+ {#if choiceLabel}
618
+ <span class={context.classes.choiceLabel}>{choiceLabel}</span>
619
+ {/if}
620
+ {#each choiceOptions as option (option.value)}
621
+ <Checkbox
622
+ label={option.label}
623
+ checked={choiceSelected.includes(option.value)}
624
+ onCheckedChange={(checked) => onOptionToggle(option.value, checked)}
625
+ />
626
+ {/each}
627
+ </div>
628
+ {:else}
629
+ <RadioGroup
630
+ label={ariaLabel || choiceLabel || undefined}
631
+ value={choiceSelected[0]}
632
+ onValueChange={onRadioChange}
633
+ style={weightStyle}
634
+ >
635
+ {#each choiceOptions as option (option.value)}
636
+ <RadioItem value={option.value} label={option.label} />
637
+ {/each}
638
+ </RadioGroup>
639
+ {/if}
640
+ {:else if component === 'Slider'}
641
+ <Slider
642
+ label={text(resolved('label')) || undefined}
643
+ value={sliderValue}
644
+ min={sliderMin}
645
+ max={sliderMax}
646
+ onValueChange={onSliderChange}
647
+ style={weightStyle}
648
+ aria-label={ariaLabel}
649
+ />
650
+ {:else if component === 'DateTimeInput'}
651
+ {@const label = text(resolved('label'))}
652
+ {#if dtMode === 'datetime'}
653
+ <!-- The visible label sits on the date field; the group name gives the
654
+ time segments their context (TimeInput's segments are self-labelled). -->
655
+ <div
656
+ class={[context.classes.row, 'flex-wrap']}
657
+ style={weightStyle}
658
+ role="group"
659
+ aria-label={ariaLabel || label || undefined}
660
+ >
661
+ <DatePicker
662
+ label={label || undefined}
663
+ value={dtParts.date || null}
664
+ onValueChange={onDtDateChange}
665
+ minDate={dtMinDate}
666
+ maxDate={dtMaxDate}
667
+ />
668
+ <TimeInput
669
+ value={dtParts.time || null}
670
+ onValueChange={onDtTimeChange}
671
+ withSeconds={dtParts.time.length > 5}
672
+ min={dtMinTime}
673
+ max={dtMaxTime}
674
+ />
675
+ </div>
676
+ {:else if dtMode === 'time'}
677
+ <!-- TimeInput has no style/aria passthrough — weight + accessibility label
678
+ live on a wrapper (role=group only when it actually carries a name). -->
679
+ <div style={weightStyle} role={ariaLabel ? 'group' : undefined} aria-label={ariaLabel}>
680
+ <TimeInput
681
+ label={label || undefined}
682
+ value={dtParts.time || null}
683
+ onValueChange={onDtTimeChange}
684
+ withSeconds={dtParts.time.length > 5}
685
+ min={dtMinTime}
686
+ max={dtMaxTime}
687
+ />
688
+ </div>
689
+ {:else}
690
+ <!-- The accessibility label becomes the field label when no visible label
691
+ is given: DatePicker spreads an aria-label onto its role-less root div
692
+ (AT-ignored, axe aria-prohibited-attr), so the name must travel
693
+ through `label` to reach the actual input. -->
694
+ <DatePicker
695
+ label={label || ariaLabel || undefined}
696
+ value={dtParts.date || null}
697
+ onValueChange={onDtDateChange}
698
+ minDate={dtMinDate}
699
+ maxDate={dtMaxDate}
700
+ style={weightStyle}
701
+ />
702
+ {/if}
703
+ {/if}