@urbicon-ui/blocks 6.37.2 → 6.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/AvatarGroup/AvatarGroup.svelte +60 -0
- package/dist/components/AvatarGroup/AvatarGroup.svelte.d.ts +4 -0
- package/dist/components/AvatarGroup/avatar-group.variants.d.ts +20 -0
- package/dist/components/AvatarGroup/avatar-group.variants.js +21 -0
- package/dist/components/AvatarGroup/index.d.ts +57 -0
- package/dist/components/AvatarGroup/index.js +2 -0
- package/dist/components/Calendar/calendar.variants.d.ts +126 -126
- package/dist/components/Chat/A2UIView/A2UINode.svelte +582 -0
- package/dist/components/Chat/A2UIView/A2UINode.svelte.d.ts +10 -0
- package/dist/components/Chat/A2UIView/A2UIView.svelte +272 -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 +141 -0
- package/dist/components/Chat/A2UIView/a2ui-registry.d.ts +64 -0
- package/dist/components/Chat/A2UIView/a2ui-registry.js +348 -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 +762 -0
- package/dist/components/Chat/A2UIView/a2ui-view.variants.d.ts +74 -0
- package/dist/components/Chat/A2UIView/a2ui-view.variants.js +58 -0
- package/dist/components/Chat/A2UIView/a2ui.types.d.ts +117 -0
- package/dist/components/Chat/A2UIView/a2ui.types.js +83 -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/CopyButton.svelte +139 -0
- package/dist/components/CopyButton/CopyButton.svelte.d.ts +4 -0
- package/dist/components/CopyButton/copy-button.variants.d.ts +29 -0
- package/dist/components/CopyButton/copy-button.variants.js +32 -0
- package/dist/components/CopyButton/index.d.ts +55 -0
- package/dist/components/CopyButton/index.js +2 -0
- package/dist/components/FileUpload/FileUpload.svelte.d.ts +1 -1
- package/dist/components/PinInput/pin-input.variants.d.ts +7 -7
- package/dist/components/Planner/planner.variants.d.ts +34 -34
- package/dist/components/TimeInput/time-input.variants.d.ts +9 -9
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.js +2 -0
- package/dist/i18n/index.d.ts +6 -390
- package/dist/primitives/Accordion/accordion.variants.d.ts +7 -7
- package/dist/primitives/Alert/alert.variants.d.ts +8 -8
- package/dist/primitives/Avatar/avatar.variants.d.ts +12 -12
- package/dist/primitives/Badge/badge.variants.d.ts +10 -10
- package/dist/primitives/Button/button.variants.d.ts +4 -4
- package/dist/primitives/Card/card.variants.d.ts +5 -5
- package/dist/primitives/Checkbox/checkbox.variants.d.ts +7 -7
- package/dist/primitives/Collapsible/collapsible.variants.d.ts +6 -6
- package/dist/primitives/Combobox/combobox.variants.d.ts +50 -50
- package/dist/primitives/Input/input.variants.d.ts +27 -27
- package/dist/primitives/JourneyTimeline/journey-timeline.variants.d.ts +22 -22
- package/dist/primitives/Kbd/Kbd.svelte +44 -0
- package/dist/primitives/Kbd/Kbd.svelte.d.ts +4 -0
- package/dist/primitives/Kbd/index.d.ts +51 -0
- package/dist/primitives/Kbd/index.js +2 -0
- package/dist/primitives/Kbd/kbd.variants.d.ts +20 -0
- package/dist/primitives/Kbd/kbd.variants.js +27 -0
- package/dist/primitives/Progress/progress.variants.d.ts +11 -11
- package/dist/primitives/RadioGroup/radioGroup.variants.d.ts +6 -6
- package/dist/primitives/Select/select.variants.d.ts +34 -34
- package/dist/primitives/Skeleton/skeleton.variants.d.ts +3 -3
- package/dist/primitives/Spinner/spinner.variants.d.ts +48 -48
- package/dist/primitives/Stepper/stepper.variants.d.ts +11 -11
- package/dist/primitives/Textarea/textarea.variants.d.ts +21 -21
- package/dist/primitives/Toast/toast.variants.d.ts +12 -12
- package/dist/primitives/Toggle/toggle.variants.d.ts +7 -7
- package/dist/primitives/Toolbar/toolbar.variants.d.ts +6 -6
- package/dist/primitives/Tooltip/tooltip.variants.d.ts +3 -3
- package/dist/primitives/index.d.ts +2 -0
- package/dist/primitives/index.js +1 -0
- package/dist/translations/de.d.ts +4 -0
- package/dist/translations/de.js +4 -0
- package/dist/translations/en.d.ts +4 -0
- package/dist/translations/en.js +4 -0
- package/package.json +3 -3
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Alert } from '../../../primitives';
|
|
3
|
+
import { untrack } from 'svelte';
|
|
4
|
+
import { getBlocksConfig, resolveSlotClasses } from '../../../provider';
|
|
5
|
+
import { type IconComponent, resolveIcon } from '../../../icons';
|
|
6
|
+
import PlusIconDefault from '../../../icons/PlusIcon.svelte';
|
|
7
|
+
import ArrowLeftIconDefault from '../../../icons/ArrowLeftIcon.svelte';
|
|
8
|
+
import CheckIconDefault from '../../../icons/CheckIcon.svelte';
|
|
9
|
+
import CloseIconDefault from '../../../icons/CloseIcon.svelte';
|
|
10
|
+
import TrashIconDefault from '../../../icons/TrashIcon.svelte';
|
|
11
|
+
import EditIconDefault from '../../../icons/EditIcon.svelte';
|
|
12
|
+
import DangerCircleIconDefault from '../../../icons/DangerCircleIcon.svelte';
|
|
13
|
+
import HomeIconDefault from '../../../icons/HomeIcon.svelte';
|
|
14
|
+
import InfoCircleIconDefault from '../../../icons/InfoCircleIcon.svelte';
|
|
15
|
+
import MailIconDefault from '../../../icons/MailIcon.svelte';
|
|
16
|
+
import MenuIconDefault from '../../../icons/MenuIcon.svelte';
|
|
17
|
+
import SearchIconDefault from '../../../icons/SearchIcon.svelte';
|
|
18
|
+
import SendIconDefault from '../../../icons/SendIcon.svelte';
|
|
19
|
+
import SettingsIconDefault from '../../../icons/SettingsIcon.svelte';
|
|
20
|
+
import StarIconDefault from '../../../icons/StarIcon.svelte';
|
|
21
|
+
import WarningTriangleIconDefault from '../../../icons/WarningTriangleIcon.svelte';
|
|
22
|
+
import CircleHelpIconDefault from '../../../icons/CircleHelpIcon.svelte';
|
|
23
|
+
import A2UINode from './A2UINode.svelte';
|
|
24
|
+
import type { A2uiValidationIssue } from './a2ui.types';
|
|
25
|
+
import { deleteAtPointer, resolveDynamic, setAtPointer } from './a2ui-data';
|
|
26
|
+
import {
|
|
27
|
+
type A2uiProcessor,
|
|
28
|
+
collectGraphIssues,
|
|
29
|
+
createA2uiProcessor,
|
|
30
|
+
normalizeA2uiPayload
|
|
31
|
+
} from './a2ui-validate';
|
|
32
|
+
import { type A2uiRenderContext, type A2uiRenderNode, buildRenderTree } from './a2ui-render';
|
|
33
|
+
import { a2uiViewVariants } from './a2ui-view.variants';
|
|
34
|
+
import type { A2UIViewProps } from './index';
|
|
35
|
+
|
|
36
|
+
let {
|
|
37
|
+
payload,
|
|
38
|
+
streaming: streamingProp = false,
|
|
39
|
+
urlPolicy,
|
|
40
|
+
onAction,
|
|
41
|
+
onValidationError,
|
|
42
|
+
errorTitle = 'Invalid UI payload',
|
|
43
|
+
unsupportedLabel = 'Unsupported component',
|
|
44
|
+
blockedImageLabel = 'Image blocked',
|
|
45
|
+
pendingLabel = 'Loading UI',
|
|
46
|
+
class: className,
|
|
47
|
+
unstyled: unstyledProp = false,
|
|
48
|
+
slotClasses: slotClassesProp = {},
|
|
49
|
+
preset,
|
|
50
|
+
...restProps
|
|
51
|
+
}: A2UIViewProps = $props();
|
|
52
|
+
|
|
53
|
+
const blocksConfig = getBlocksConfig();
|
|
54
|
+
const unstyled = $derived(unstyledProp || blocksConfig?.unstyled || false);
|
|
55
|
+
|
|
56
|
+
// A2UI icon-enum name → resolved Urbicon icon. Resolved ONCE via direct
|
|
57
|
+
// imports (tree-shakeable — never getIcon()); an IconProvider override still
|
|
58
|
+
// wins, the direct import is the fallback. Built here (not per node) and
|
|
59
|
+
// threaded through the render context.
|
|
60
|
+
const icons: Readonly<Record<string, IconComponent>> = {
|
|
61
|
+
add: resolveIcon('plus', PlusIconDefault),
|
|
62
|
+
arrowBack: resolveIcon('arrowLeft', ArrowLeftIconDefault),
|
|
63
|
+
check: resolveIcon('check', CheckIconDefault),
|
|
64
|
+
close: resolveIcon('close', CloseIconDefault),
|
|
65
|
+
delete: resolveIcon('trash', TrashIconDefault),
|
|
66
|
+
edit: resolveIcon('edit', EditIconDefault),
|
|
67
|
+
error: resolveIcon('danger', DangerCircleIconDefault),
|
|
68
|
+
home: resolveIcon('home', HomeIconDefault),
|
|
69
|
+
info: resolveIcon('info', InfoCircleIconDefault),
|
|
70
|
+
mail: resolveIcon('mail', MailIconDefault),
|
|
71
|
+
menu: resolveIcon('menu', MenuIconDefault),
|
|
72
|
+
search: resolveIcon('search', SearchIconDefault),
|
|
73
|
+
send: resolveIcon('send', SendIconDefault),
|
|
74
|
+
settings: resolveIcon('settings', SettingsIconDefault),
|
|
75
|
+
star: resolveIcon('star', StarIconDefault),
|
|
76
|
+
warning: resolveIcon('warning', WarningTriangleIconDefault)
|
|
77
|
+
};
|
|
78
|
+
const fallbackIcon = resolveIcon('circleHelp', CircleHelpIconDefault);
|
|
79
|
+
|
|
80
|
+
// ── Processor (plain, non-reactive) + version counter ────────────────────
|
|
81
|
+
// The processor mutates plain Maps in place (the streaming-markdown engine
|
|
82
|
+
// pattern). A $state version counter is the single reactive primitive; the
|
|
83
|
+
// render tree derives from it, so reading it is a real subscription (unlike a
|
|
84
|
+
// `void messages` proxy read). `processor`/`consumed` are plain lets: their
|
|
85
|
+
// reassignment must NOT itself trigger reactivity — the bump does.
|
|
86
|
+
let processor: A2uiProcessor = createA2uiProcessor();
|
|
87
|
+
let consumed: unknown[] = [];
|
|
88
|
+
let normalizeIssue = $state<A2uiValidationIssue | undefined>(undefined);
|
|
89
|
+
let version = $state(0);
|
|
90
|
+
// Bumped ONLY on a full rebuild (payload replaced, not appended). It keys the
|
|
91
|
+
// surface `{#each}`, so a rebuild remounts the node tree and A2UINode's local
|
|
92
|
+
// input fallbacks reset — otherwise a typed-then-replaced literal input keeps
|
|
93
|
+
// the stale text (a rebuild is documented to discard local edits). An
|
|
94
|
+
// incremental append leaves it unchanged, so focus and edits survive.
|
|
95
|
+
let generation = $state(0);
|
|
96
|
+
|
|
97
|
+
function bump(): void {
|
|
98
|
+
version++;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Incremental consumption: if the first `consumed.length` envelopes are
|
|
102
|
+
// referentially identical to the new payload, apply only the appended ones
|
|
103
|
+
// (two-way edits in the data model survive). Otherwise rebuild from scratch.
|
|
104
|
+
// Reads `payload` (tracked) to re-run on every immutable payload change; the
|
|
105
|
+
// mutation is untracked so the `version++`/`normalizeIssue` writes never make
|
|
106
|
+
// this effect depend on the state it drives (avoids an update-depth loop).
|
|
107
|
+
$effect(() => {
|
|
108
|
+
const { envelopes, issue } = normalizeA2uiPayload(payload);
|
|
109
|
+
untrack(() => {
|
|
110
|
+
const prefixMatches =
|
|
111
|
+
envelopes.length >= consumed.length && consumed.every((env, i) => env === envelopes[i]);
|
|
112
|
+
if (!prefixMatches) {
|
|
113
|
+
processor = createA2uiProcessor();
|
|
114
|
+
consumed = [];
|
|
115
|
+
generation++;
|
|
116
|
+
}
|
|
117
|
+
for (let i = consumed.length; i < envelopes.length; i++) {
|
|
118
|
+
processor.apply(envelopes[i], i);
|
|
119
|
+
}
|
|
120
|
+
consumed = envelopes.slice();
|
|
121
|
+
normalizeIssue = issue;
|
|
122
|
+
version++;
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
function isContainer(value: unknown): boolean {
|
|
127
|
+
return value !== null && typeof value === 'object';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Two-way write-back: mutate the surface data model in place, then bump so the
|
|
131
|
+
// render tree re-derives with the new value (bound labels update live).
|
|
132
|
+
function writeBinding(surfaceId: string, pointer: string, value: unknown): void {
|
|
133
|
+
const surface = processor.surfaces.get(surfaceId);
|
|
134
|
+
if (!surface) return;
|
|
135
|
+
if (!isContainer(surface.dataModel)) {
|
|
136
|
+
const firstSegment = pointer.replace(/^\//, '').split('/')[0];
|
|
137
|
+
surface.dataModel = /^(?:0|[1-9]\d*|-)$/.test(firstSegment) ? [] : {};
|
|
138
|
+
}
|
|
139
|
+
setAtPointer(surface.dataModel, pointer, value);
|
|
140
|
+
bump();
|
|
141
|
+
}
|
|
142
|
+
function removeBinding(surfaceId: string, pointer: string): void {
|
|
143
|
+
const surface = processor.surfaces.get(surfaceId);
|
|
144
|
+
if (!surface) return;
|
|
145
|
+
deleteAtPointer(surface.dataModel, pointer);
|
|
146
|
+
bump();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Resolved slot classes ────────────────────────────────────────────────
|
|
150
|
+
const styles = a2uiViewVariants();
|
|
151
|
+
const resolvedSlots = $derived(
|
|
152
|
+
resolveSlotClasses(blocksConfig, 'A2UIView', preset, {}, slotClassesProp)
|
|
153
|
+
);
|
|
154
|
+
const classes = $derived.by(() => {
|
|
155
|
+
const out: Record<string, string> = {};
|
|
156
|
+
for (const [name, slotFn] of Object.entries(styles)) {
|
|
157
|
+
const override = resolvedSlots?.[name];
|
|
158
|
+
out[name] = unstyled ? (override ?? '') : slotFn({ class: override });
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
});
|
|
162
|
+
const rootClass = $derived(
|
|
163
|
+
unstyled
|
|
164
|
+
? [resolvedSlots?.root, className].filter(Boolean).join(' ')
|
|
165
|
+
: styles.root({ class: [resolvedSlots?.root, className] })
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// ── Render tree + issue collection (re-derives on every version bump) ────
|
|
169
|
+
interface SurfaceView {
|
|
170
|
+
surfaceId: string;
|
|
171
|
+
root: A2uiRenderNode | null;
|
|
172
|
+
context: A2uiRenderContext;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const view = $derived.by(() => {
|
|
176
|
+
version; // subscription to processing + edits
|
|
177
|
+
const isStreaming = streamingProp;
|
|
178
|
+
const currentClasses = classes;
|
|
179
|
+
const surfaces: SurfaceView[] = [];
|
|
180
|
+
const summaryIssues: A2uiValidationIssue[] = [];
|
|
181
|
+
const allIssues: A2uiValidationIssue[] = [];
|
|
182
|
+
|
|
183
|
+
if (normalizeIssue) {
|
|
184
|
+
summaryIssues.push(normalizeIssue);
|
|
185
|
+
allIssues.push(normalizeIssue);
|
|
186
|
+
}
|
|
187
|
+
for (const globalIssue of processor.globalIssues) {
|
|
188
|
+
allIssues.push(globalIssue);
|
|
189
|
+
if (globalIssue.severity === 'error') summaryIssues.push(globalIssue);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for (const surface of processor.surfaces.values()) {
|
|
193
|
+
for (const surfaceIssue of surface.issues) allIssues.push(surfaceIssue);
|
|
194
|
+
for (const graphIssue of collectGraphIssues(surface, { streaming: isStreaming })) {
|
|
195
|
+
allIssues.push(graphIssue);
|
|
196
|
+
}
|
|
197
|
+
const root = buildRenderTree(surface);
|
|
198
|
+
const context: A2uiRenderContext = {
|
|
199
|
+
classes: currentClasses,
|
|
200
|
+
urlPolicy,
|
|
201
|
+
streaming: isStreaming,
|
|
202
|
+
inline: false,
|
|
203
|
+
surfaceId: surface.surfaceId,
|
|
204
|
+
onAction,
|
|
205
|
+
resolve: (value, scope) => resolveDynamic(value, surface.dataModel, scope),
|
|
206
|
+
write: (pointer, value) => writeBinding(surface.surfaceId, pointer, value),
|
|
207
|
+
remove: (pointer) => removeBinding(surface.surfaceId, pointer),
|
|
208
|
+
icons,
|
|
209
|
+
fallbackIcon,
|
|
210
|
+
labels: {
|
|
211
|
+
unsupported: unsupportedLabel,
|
|
212
|
+
blockedImage: blockedImageLabel,
|
|
213
|
+
pending: pendingLabel
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
surfaces.push({ surfaceId: surface.surfaceId, root, context });
|
|
217
|
+
}
|
|
218
|
+
return { surfaces, summaryIssues, allIssues };
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Fire onValidationError only when the issue list actually changes. Seeded
|
|
222
|
+
// with the empty signature so an issue-free first render stays silent.
|
|
223
|
+
let lastIssueSig = '[]';
|
|
224
|
+
$effect(() => {
|
|
225
|
+
const issues = view.allIssues;
|
|
226
|
+
const signature = JSON.stringify(
|
|
227
|
+
issues.map((issue) => [
|
|
228
|
+
issue.severity,
|
|
229
|
+
issue.code,
|
|
230
|
+
issue.surfaceId ?? '',
|
|
231
|
+
issue.path ?? '',
|
|
232
|
+
issue.message
|
|
233
|
+
])
|
|
234
|
+
);
|
|
235
|
+
if (signature !== lastIssueSig) {
|
|
236
|
+
lastIssueSig = signature;
|
|
237
|
+
onValidationError?.(issues);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
</script>
|
|
241
|
+
|
|
242
|
+
<!--
|
|
243
|
+
The render tree recurses through this self-referencing snippet rather than a
|
|
244
|
+
self-import inside A2UINode: a snippet that renders A2UINode and passes ITSELF
|
|
245
|
+
down as `renderChild` gives clean recursion without the circular component-type
|
|
246
|
+
resolution that self-imports trip in svelte-check.
|
|
247
|
+
-->
|
|
248
|
+
{#snippet renderNode(node: A2uiRenderNode, context: A2uiRenderContext)}
|
|
249
|
+
<A2UINode {node} {context} renderChild={renderNode} />
|
|
250
|
+
{/snippet}
|
|
251
|
+
|
|
252
|
+
<div class={rootClass} {...restProps}>
|
|
253
|
+
{#if view.summaryIssues.length}
|
|
254
|
+
<Alert intent="danger" title={errorTitle}>
|
|
255
|
+
<ul class={classes.errorList}>
|
|
256
|
+
{#each view.summaryIssues as issue, index (`${issue.code}#${index}`)}
|
|
257
|
+
<li>{issue.message}</li>
|
|
258
|
+
{/each}
|
|
259
|
+
</ul>
|
|
260
|
+
</Alert>
|
|
261
|
+
{/if}
|
|
262
|
+
|
|
263
|
+
{#each view.surfaces as surface (`${surface.surfaceId}#${generation}`)}
|
|
264
|
+
<div class={classes.surface}>
|
|
265
|
+
{#if surface.root}
|
|
266
|
+
{@render renderNode(surface.root, surface.context)}
|
|
267
|
+
{:else if !streamingProp}
|
|
268
|
+
<span class={classes.errorChip}>{errorTitle}</span>
|
|
269
|
+
{/if}
|
|
270
|
+
</div>
|
|
271
|
+
{/each}
|
|
272
|
+
</div>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data-model primitives for the A2UI engine: RFC 6901 JSON Pointer get/set/delete
|
|
3
|
+
* and the `resolveDynamic` union resolver. Pure TS, no Svelte.
|
|
4
|
+
*
|
|
5
|
+
* Security posture (this file is on the untrusted-payload path):
|
|
6
|
+
* - `__proto__` / `constructor` / `prototype` are rejected at **every** pointer
|
|
7
|
+
* segment — prototype pollution never reaches the model.
|
|
8
|
+
* - Writes never merge or spread payload objects; they navigate and assign a
|
|
9
|
+
* single leaf, creating intermediate containers explicitly.
|
|
10
|
+
* - Array index deletion leaves a hole (length preserved), matching the spec's
|
|
11
|
+
* sparse-array semantics.
|
|
12
|
+
*
|
|
13
|
+
* A2UI convention (deviation from strict RFC 6901): the pointers `''` **and**
|
|
14
|
+
* `'/'` both address the whole model. Strict RFC treats `'/'` as the key `""`;
|
|
15
|
+
* A2UI's `updateDataModel` uses `'/'` to mean "the entire data model", and no
|
|
16
|
+
* A2UI payload ever addresses an empty-string key, so we honor the spec's own
|
|
17
|
+
* convention here for ergonomics.
|
|
18
|
+
*/
|
|
19
|
+
import { type A2uiValidationIssue } from './a2ui.types.js';
|
|
20
|
+
/** Property names that must never be used as an object key on the write path. */
|
|
21
|
+
export declare const PROTO_KEYS: ReadonlySet<string>;
|
|
22
|
+
/**
|
|
23
|
+
* Read the value at `pointer`. `''` / `'/'` return the whole model. Missing
|
|
24
|
+
* paths, holes and type-mismatches resolve to `undefined` (never throw) — the
|
|
25
|
+
* spec renders an empty string / skeleton for unresolved bindings.
|
|
26
|
+
*/
|
|
27
|
+
export declare function getAtPointer(model: unknown, pointer: string): unknown;
|
|
28
|
+
/**
|
|
29
|
+
* Write `value` at `pointer`, creating intermediate objects/arrays as needed.
|
|
30
|
+
* The document root cannot be replaced through this function (the processor
|
|
31
|
+
* assigns `dataModel` directly for whole-model updates); a root pointer returns
|
|
32
|
+
* `ok: false`. Prototype-pollution segments are rejected with an issue.
|
|
33
|
+
*/
|
|
34
|
+
export declare function setAtPointer(model: unknown, pointer: string, value: unknown): {
|
|
35
|
+
ok: boolean;
|
|
36
|
+
issue?: A2uiValidationIssue;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Delete the leaf at `pointer`. Object keys are removed; array indices are
|
|
40
|
+
* `delete`d in place (leaving a hole, preserving length). No-ops for missing
|
|
41
|
+
* paths, non-container parents, and root pointers (the processor clears
|
|
42
|
+
* `dataModel` directly for whole-model deletes).
|
|
43
|
+
*/
|
|
44
|
+
export declare function deleteAtPointer(model: unknown, pointer: string): void;
|
|
45
|
+
/**
|
|
46
|
+
* Resolve a Dynamic value union to a concrete value.
|
|
47
|
+
* - `{ path }` (data binding) → the pointed-at model value (relative paths are
|
|
48
|
+
* resolved against `scopePrefix`, the current template item).
|
|
49
|
+
* - `{ call, ... }` (function call) → `undefined` + a warning (unsupported in v1).
|
|
50
|
+
* - anything else (string/number/boolean/array/plain object) → returned as-is.
|
|
51
|
+
*
|
|
52
|
+
* Never throws; unresolved bindings yield `undefined` for graceful rendering.
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveDynamic(value: unknown, model: unknown, scopePrefix: string | undefined): {
|
|
55
|
+
value: unknown;
|
|
56
|
+
issue?: A2uiValidationIssue;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Deep-clone JSON data for ingest into the model, stripping any own
|
|
60
|
+
* `__proto__`/`constructor`/`prototype` keys (harmless as own props, but never
|
|
61
|
+
* addressable, so we drop them). Functions/symbols are dropped. Used by the
|
|
62
|
+
* processor so that later two-way edits never mutate the source payload and no
|
|
63
|
+
* prototype key ever enters the model.
|
|
64
|
+
*/
|
|
65
|
+
export declare function cloneData(value: unknown): unknown;
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data-model primitives for the A2UI engine: RFC 6901 JSON Pointer get/set/delete
|
|
3
|
+
* and the `resolveDynamic` union resolver. Pure TS, no Svelte.
|
|
4
|
+
*
|
|
5
|
+
* Security posture (this file is on the untrusted-payload path):
|
|
6
|
+
* - `__proto__` / `constructor` / `prototype` are rejected at **every** pointer
|
|
7
|
+
* segment — prototype pollution never reaches the model.
|
|
8
|
+
* - Writes never merge or spread payload objects; they navigate and assign a
|
|
9
|
+
* single leaf, creating intermediate containers explicitly.
|
|
10
|
+
* - Array index deletion leaves a hole (length preserved), matching the spec's
|
|
11
|
+
* sparse-array semantics.
|
|
12
|
+
*
|
|
13
|
+
* A2UI convention (deviation from strict RFC 6901): the pointers `''` **and**
|
|
14
|
+
* `'/'` both address the whole model. Strict RFC treats `'/'` as the key `""`;
|
|
15
|
+
* A2UI's `updateDataModel` uses `'/'` to mean "the entire data model", and no
|
|
16
|
+
* A2UI payload ever addresses an empty-string key, so we honor the spec's own
|
|
17
|
+
* convention here for ergonomics.
|
|
18
|
+
*/
|
|
19
|
+
import { A2UI_ISSUE_CODES } from './a2ui.types.js';
|
|
20
|
+
/** Property names that must never be used as an object key on the write path. */
|
|
21
|
+
export const PROTO_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
22
|
+
/** RFC 6901 token unescape: `~1`→`/` then `~0`→`~` (order is significant). */
|
|
23
|
+
function unescapeToken(token) {
|
|
24
|
+
return token.replaceAll('~1', '/').replaceAll('~0', '~');
|
|
25
|
+
}
|
|
26
|
+
/** Non-negative array index per RFC 6901 (`0` or `[1-9][0-9]*`); else `null`. */
|
|
27
|
+
function toIndex(segment) {
|
|
28
|
+
return /^(?:0|[1-9]\d*)$/.test(segment) ? Number(segment) : null;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Parse a pointer into decoded segments, guarding every segment against
|
|
32
|
+
* prototype-pollution keys. Absolute pointers start with `/`; anything else is
|
|
33
|
+
* treated as relative (segments split on `/`). `''` yields zero segments.
|
|
34
|
+
*/
|
|
35
|
+
function parsePointer(pointer) {
|
|
36
|
+
if (typeof pointer !== 'string') {
|
|
37
|
+
return {
|
|
38
|
+
segments: [],
|
|
39
|
+
issue: {
|
|
40
|
+
severity: 'error',
|
|
41
|
+
code: A2UI_ISSUE_CODES.POINTER_ERROR,
|
|
42
|
+
message: 'Pointer must be a string'
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
let rawTokens;
|
|
47
|
+
if (pointer === '')
|
|
48
|
+
rawTokens = [];
|
|
49
|
+
else if (pointer.startsWith('/'))
|
|
50
|
+
rawTokens = pointer.slice(1).split('/');
|
|
51
|
+
else
|
|
52
|
+
rawTokens = pointer.split('/');
|
|
53
|
+
const segments = [];
|
|
54
|
+
for (const token of rawTokens) {
|
|
55
|
+
const segment = unescapeToken(token);
|
|
56
|
+
if (PROTO_KEYS.has(segment)) {
|
|
57
|
+
return {
|
|
58
|
+
segments: [],
|
|
59
|
+
issue: {
|
|
60
|
+
severity: 'error',
|
|
61
|
+
code: A2UI_ISSUE_CODES.PROTOTYPE_POLLUTION,
|
|
62
|
+
path: pointer,
|
|
63
|
+
message: `Prohibited property name "${segment}" in pointer "${pointer}"`
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
segments.push(segment);
|
|
68
|
+
}
|
|
69
|
+
return { segments };
|
|
70
|
+
}
|
|
71
|
+
function isContainer(value) {
|
|
72
|
+
return value !== null && typeof value === 'object';
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Read the value at `pointer`. `''` / `'/'` return the whole model. Missing
|
|
76
|
+
* paths, holes and type-mismatches resolve to `undefined` (never throw) — the
|
|
77
|
+
* spec renders an empty string / skeleton for unresolved bindings.
|
|
78
|
+
*/
|
|
79
|
+
export function getAtPointer(model, pointer) {
|
|
80
|
+
if (pointer === '' || pointer === '/')
|
|
81
|
+
return model;
|
|
82
|
+
const { segments, issue } = parsePointer(pointer);
|
|
83
|
+
if (issue)
|
|
84
|
+
return undefined;
|
|
85
|
+
let node = model;
|
|
86
|
+
for (const segment of segments) {
|
|
87
|
+
if (node == null)
|
|
88
|
+
return undefined;
|
|
89
|
+
if (Array.isArray(node)) {
|
|
90
|
+
const index = toIndex(segment);
|
|
91
|
+
node = index === null ? undefined : node[index];
|
|
92
|
+
}
|
|
93
|
+
else if (typeof node === 'object') {
|
|
94
|
+
node = node[segment];
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return node;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Write `value` at `pointer`, creating intermediate objects/arrays as needed.
|
|
104
|
+
* The document root cannot be replaced through this function (the processor
|
|
105
|
+
* assigns `dataModel` directly for whole-model updates); a root pointer returns
|
|
106
|
+
* `ok: false`. Prototype-pollution segments are rejected with an issue.
|
|
107
|
+
*/
|
|
108
|
+
export function setAtPointer(model, pointer, value) {
|
|
109
|
+
if (pointer === '' || pointer === '/') {
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
issue: {
|
|
113
|
+
severity: 'error',
|
|
114
|
+
code: A2UI_ISSUE_CODES.POINTER_ERROR,
|
|
115
|
+
path: pointer,
|
|
116
|
+
message: 'Cannot replace the document root through setAtPointer'
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const { segments, issue } = parsePointer(pointer);
|
|
121
|
+
if (issue)
|
|
122
|
+
return { ok: false, issue };
|
|
123
|
+
if (segments.length === 0) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
issue: {
|
|
127
|
+
severity: 'error',
|
|
128
|
+
code: A2UI_ISSUE_CODES.POINTER_ERROR,
|
|
129
|
+
path: pointer,
|
|
130
|
+
message: 'Cannot replace the document root through setAtPointer'
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (!isContainer(model)) {
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
issue: {
|
|
138
|
+
severity: 'error',
|
|
139
|
+
code: A2UI_ISSUE_CODES.POINTER_ERROR,
|
|
140
|
+
path: pointer,
|
|
141
|
+
message: 'Cannot write into a non-object data model'
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
let node = model;
|
|
146
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
147
|
+
const segment = segments[i];
|
|
148
|
+
const nextIsIndex = toIndex(segments[i + 1]) !== null || segments[i + 1] === '-';
|
|
149
|
+
if (Array.isArray(node)) {
|
|
150
|
+
const index = toIndex(segment);
|
|
151
|
+
if (index === null) {
|
|
152
|
+
return { ok: false, issue: pointerTypeIssue(pointer, segment) };
|
|
153
|
+
}
|
|
154
|
+
if (!isContainer(node[index]))
|
|
155
|
+
node[index] = nextIsIndex ? [] : {};
|
|
156
|
+
node = node[index];
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
if (!isContainer(node[segment]))
|
|
160
|
+
node[segment] = nextIsIndex ? [] : {};
|
|
161
|
+
node = node[segment];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const last = segments[segments.length - 1];
|
|
165
|
+
if (Array.isArray(node)) {
|
|
166
|
+
const index = last === '-' ? node.length : toIndex(last);
|
|
167
|
+
if (index === null)
|
|
168
|
+
return { ok: false, issue: pointerTypeIssue(pointer, last) };
|
|
169
|
+
node[index] = value;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
node[last] = value;
|
|
173
|
+
}
|
|
174
|
+
return { ok: true };
|
|
175
|
+
}
|
|
176
|
+
function pointerTypeIssue(pointer, segment) {
|
|
177
|
+
return {
|
|
178
|
+
severity: 'error',
|
|
179
|
+
code: A2UI_ISSUE_CODES.POINTER_ERROR,
|
|
180
|
+
path: pointer,
|
|
181
|
+
message: `Segment "${segment}" is not a valid array index in pointer "${pointer}"`
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Delete the leaf at `pointer`. Object keys are removed; array indices are
|
|
186
|
+
* `delete`d in place (leaving a hole, preserving length). No-ops for missing
|
|
187
|
+
* paths, non-container parents, and root pointers (the processor clears
|
|
188
|
+
* `dataModel` directly for whole-model deletes).
|
|
189
|
+
*/
|
|
190
|
+
export function deleteAtPointer(model, pointer) {
|
|
191
|
+
if (pointer === '' || pointer === '/')
|
|
192
|
+
return;
|
|
193
|
+
const { segments, issue } = parsePointer(pointer);
|
|
194
|
+
if (issue || segments.length === 0)
|
|
195
|
+
return;
|
|
196
|
+
let node = model;
|
|
197
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
198
|
+
if (!isContainer(node))
|
|
199
|
+
return;
|
|
200
|
+
if (Array.isArray(node)) {
|
|
201
|
+
const index = toIndex(segments[i]);
|
|
202
|
+
node = index === null ? undefined : node[index];
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
node = node[segments[i]];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (!isContainer(node))
|
|
209
|
+
return;
|
|
210
|
+
const last = segments[segments.length - 1];
|
|
211
|
+
if (Array.isArray(node)) {
|
|
212
|
+
const index = toIndex(last);
|
|
213
|
+
if (index !== null)
|
|
214
|
+
delete node[index];
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
delete node[last];
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Resolve a Dynamic value union to a concrete value.
|
|
222
|
+
* - `{ path }` (data binding) → the pointed-at model value (relative paths are
|
|
223
|
+
* resolved against `scopePrefix`, the current template item).
|
|
224
|
+
* - `{ call, ... }` (function call) → `undefined` + a warning (unsupported in v1).
|
|
225
|
+
* - anything else (string/number/boolean/array/plain object) → returned as-is.
|
|
226
|
+
*
|
|
227
|
+
* Never throws; unresolved bindings yield `undefined` for graceful rendering.
|
|
228
|
+
*/
|
|
229
|
+
export function resolveDynamic(value, model, scopePrefix) {
|
|
230
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
231
|
+
const object = value;
|
|
232
|
+
if (typeof object.call === 'string') {
|
|
233
|
+
return {
|
|
234
|
+
value: undefined,
|
|
235
|
+
issue: {
|
|
236
|
+
severity: 'warning',
|
|
237
|
+
code: A2UI_ISSUE_CODES.FUNCTION_CALL_UNSUPPORTED,
|
|
238
|
+
message: `Function call "${object.call}" is not supported; resolved to undefined`
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (typeof object.path === 'string') {
|
|
243
|
+
const relative = object.path;
|
|
244
|
+
const full = relative.startsWith('/') ? relative : `${scopePrefix ?? ''}/${relative}`;
|
|
245
|
+
const { issue } = parsePointer(full);
|
|
246
|
+
if (issue)
|
|
247
|
+
return { value: undefined, issue };
|
|
248
|
+
return { value: getAtPointer(model, full) };
|
|
249
|
+
}
|
|
250
|
+
// A literal object argument (rare, e.g. a raw config object) — pass through.
|
|
251
|
+
return { value };
|
|
252
|
+
}
|
|
253
|
+
return { value };
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Deep-clone JSON data for ingest into the model, stripping any own
|
|
257
|
+
* `__proto__`/`constructor`/`prototype` keys (harmless as own props, but never
|
|
258
|
+
* addressable, so we drop them). Functions/symbols are dropped. Used by the
|
|
259
|
+
* processor so that later two-way edits never mutate the source payload and no
|
|
260
|
+
* prototype key ever enters the model.
|
|
261
|
+
*/
|
|
262
|
+
export function cloneData(value) {
|
|
263
|
+
if (Array.isArray(value)) {
|
|
264
|
+
const out = [];
|
|
265
|
+
for (let i = 0; i < value.length; i++) {
|
|
266
|
+
if (i in value)
|
|
267
|
+
out[i] = cloneData(value[i]);
|
|
268
|
+
}
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
271
|
+
if (value !== null && typeof value === 'object') {
|
|
272
|
+
const out = {};
|
|
273
|
+
for (const key of Object.keys(value)) {
|
|
274
|
+
if (PROTO_KEYS.has(key))
|
|
275
|
+
continue;
|
|
276
|
+
out[key] = cloneData(value[key]);
|
|
277
|
+
}
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
if (typeof value === 'function' || typeof value === 'symbol')
|
|
281
|
+
return undefined;
|
|
282
|
+
return value;
|
|
283
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System-prompt generator for the A2UI subset. Renders the agent-facing catalog
|
|
3
|
+
* description straight from `A2UI_REGISTRY`, so the prompt and the validator can
|
|
4
|
+
* never drift (a drift-guard test asserts every component and enum value is
|
|
5
|
+
* covered). Pure TS, no Svelte — a server building the prompt has no DOM.
|
|
6
|
+
*
|
|
7
|
+
* Deliberately excludes any TRANSPORT section (how envelopes reach the client):
|
|
8
|
+
* that is app-specific and each app appends it (the chat-demo appends its fenced
|
|
9
|
+
* `a2ui` JSONL protocol).
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Build the A2UI system prompt for this subset. Pass `catalogId` to override the
|
|
13
|
+
* default `createSurface` catalog identifier (the value must round-trip
|
|
14
|
+
* unchanged in every envelope).
|
|
15
|
+
*/
|
|
16
|
+
export declare function a2uiSystemPrompt(options?: {
|
|
17
|
+
catalogId?: string;
|
|
18
|
+
}): string;
|