@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.
- package/dist/components/Calendar/calendar.variants.d.ts +63 -63
- package/dist/components/Chat/A2UIView/A2UINode.svelte +703 -0
- package/dist/components/Chat/A2UIView/A2UINode.svelte.d.ts +10 -0
- package/dist/components/Chat/A2UIView/A2UIView.svelte +293 -0
- package/dist/components/Chat/A2UIView/A2UIView.svelte.d.ts +4 -0
- package/dist/components/Chat/A2UIView/a2ui-data.d.ts +65 -0
- package/dist/components/Chat/A2UIView/a2ui-data.js +283 -0
- package/dist/components/Chat/A2UIView/a2ui-prompt.d.ts +18 -0
- package/dist/components/Chat/A2UIView/a2ui-prompt.js +147 -0
- package/dist/components/Chat/A2UIView/a2ui-registry.d.ts +64 -0
- package/dist/components/Chat/A2UIView/a2ui-registry.js +390 -0
- package/dist/components/Chat/A2UIView/a2ui-render.d.ts +96 -0
- package/dist/components/Chat/A2UIView/a2ui-render.js +140 -0
- package/dist/components/Chat/A2UIView/a2ui-validate.d.ts +75 -0
- package/dist/components/Chat/A2UIView/a2ui-validate.js +773 -0
- package/dist/components/Chat/A2UIView/a2ui-view.variants.d.ts +80 -0
- package/dist/components/Chat/A2UIView/a2ui-view.variants.js +62 -0
- package/dist/components/Chat/A2UIView/a2ui.types.d.ts +119 -0
- package/dist/components/Chat/A2UIView/a2ui.types.js +85 -0
- package/dist/components/Chat/A2UIView/index.d.ts +102 -0
- package/dist/components/Chat/A2UIView/index.js +7 -0
- package/dist/components/Chat/ChatMessage/ChatMessage.svelte +4 -1
- package/dist/components/Chat/index.d.ts +2 -0
- package/dist/components/Chat/index.js +1 -0
- package/dist/components/CopyButton/copy-button.variants.d.ts +4 -4
- package/dist/i18n/index.d.ts +398 -2
- package/dist/primitives/Alert/alert.variants.d.ts +8 -8
- package/dist/primitives/JourneyTimeline/journey-timeline.variants.d.ts +22 -22
- package/dist/primitives/JourneyTimeline/journey-timeline.variants.js +3 -1
- package/dist/primitives/Spinner/spinner.variants.d.ts +16 -16
- package/dist/primitives/Stepper/stepper.variants.d.ts +11 -11
- package/dist/primitives/Toast/toast.variants.d.ts +12 -12
- package/dist/primitives/Tooltip/tooltip.variants.d.ts +3 -3
- package/dist/style/semantic.css +11 -3
- 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}
|