@intentface/ui 0.0.0-stage → 0.7.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/LICENSE +21 -0
- package/README.md +147 -2
- package/dist/ask-user/ask-user.d.ts +104 -0
- package/dist/ask-user/ask-user.js +245 -0
- package/dist/ask-user/index.d.ts +3 -0
- package/dist/ask-user/index.js +3 -0
- package/dist/ask-user/index.parts.d.ts +1 -0
- package/dist/ask-user/index.parts.js +5 -0
- package/dist/attachments/attachments.d.ts +73 -0
- package/dist/attachments/attachments.js +121 -0
- package/dist/attachments/index.d.ts +3 -0
- package/dist/attachments/index.js +3 -0
- package/dist/attachments/index.parts.d.ts +1 -0
- package/dist/attachments/index.parts.js +5 -0
- package/dist/chip/chip.d.ts +45 -0
- package/dist/chip/chip.js +43 -0
- package/dist/chip/index.d.ts +2 -0
- package/dist/chip/index.js +2 -0
- package/dist/chip/index.parts.d.ts +1 -0
- package/dist/chip/index.parts.js +5 -0
- package/dist/chip-markdown.d.ts +25 -0
- package/dist/chip-markdown.js +54 -0
- package/dist/composer/actions.d.ts +35 -0
- package/dist/composer/actions.js +76 -0
- package/dist/composer/ask-user-machine.d.ts +52 -0
- package/dist/composer/ask-user-machine.js +164 -0
- package/dist/composer/attachments-machine.d.ts +34 -0
- package/dist/composer/attachments-machine.js +91 -0
- package/dist/composer/attachments.d.ts +16 -0
- package/dist/composer/attachments.js +32 -0
- package/dist/composer/command-list.d.ts +49 -0
- package/dist/composer/command-list.js +360 -0
- package/dist/composer/commands.d.ts +28 -0
- package/dist/composer/commands.js +56 -0
- package/dist/composer/container.d.ts +3 -0
- package/dist/composer/container.js +32 -0
- package/dist/composer/controller.d.ts +3 -0
- package/dist/composer/controller.js +6 -0
- package/dist/composer/editor-dom.d.ts +57 -0
- package/dist/composer/editor-dom.js +517 -0
- package/dist/composer/fuzzy.d.ts +4 -0
- package/dist/composer/fuzzy.js +49 -0
- package/dist/composer/index.d.ts +25 -0
- package/dist/composer/index.js +13 -0
- package/dist/composer/index.parts.d.ts +11 -0
- package/dist/composer/index.parts.js +15 -0
- package/dist/composer/internals.d.ts +30 -0
- package/dist/composer/internals.js +102 -0
- package/dist/composer/keyboard.d.ts +61 -0
- package/dist/composer/keyboard.js +75 -0
- package/dist/composer/panel.d.ts +28 -0
- package/dist/composer/panel.js +76 -0
- package/dist/composer/placeholder.d.ts +10 -0
- package/dist/composer/placeholder.js +8 -0
- package/dist/composer/popover.d.ts +12 -0
- package/dist/composer/popover.js +62 -0
- package/dist/composer/prefix-detection.d.ts +23 -0
- package/dist/composer/prefix-detection.js +67 -0
- package/dist/composer/root.d.ts +21 -0
- package/dist/composer/root.js +124 -0
- package/dist/composer/segments.d.ts +108 -0
- package/dist/composer/segments.js +366 -0
- package/dist/composer/store.d.ts +98 -0
- package/dist/composer/store.js +457 -0
- package/dist/composer/textarea.d.ts +39 -0
- package/dist/composer/textarea.js +64 -0
- package/dist/composer/trigger-tracker.d.ts +12 -0
- package/dist/composer/trigger-tracker.js +90 -0
- package/dist/composer/types.d.ts +127 -0
- package/dist/composer/types.js +4 -0
- package/dist/composer/use-composer-editor.d.ts +43 -0
- package/dist/composer/use-composer-editor.js +653 -0
- package/dist/generative/catalog.d.ts +177 -0
- package/dist/generative/catalog.js +20 -0
- package/dist/generative/index.d.ts +26 -0
- package/dist/generative/index.js +20 -0
- package/dist/generative/json-schema.d.ts +45 -0
- package/dist/generative/json-schema.js +216 -0
- package/dist/generative/markdown.d.ts +21 -0
- package/dist/generative/markdown.js +76 -0
- package/dist/generative/tree.d.ts +56 -0
- package/dist/generative/tree.js +365 -0
- package/dist/generative/use-tree.d.ts +12 -0
- package/dist/generative/use-tree.js +64 -0
- package/dist/internal/change-event-details.d.ts +12 -0
- package/dist/internal/change-event-details.js +16 -0
- package/dist/internal/collapsible.d.ts +22 -0
- package/dist/internal/collapsible.js +94 -0
- package/dist/internal/collection.d.ts +32 -0
- package/dist/internal/collection.js +72 -0
- package/dist/internal/iso-layout-effect.d.ts +2 -0
- package/dist/internal/iso-layout-effect.js +6 -0
- package/dist/internal/portal.d.ts +6 -0
- package/dist/internal/portal.js +18 -0
- package/dist/internal/primitive-props.d.ts +4 -0
- package/dist/internal/primitive-props.js +3 -0
- package/dist/internal/render/baseTypes.d.ts +18 -0
- package/dist/internal/render/baseTypes.js +4 -0
- package/dist/internal/render/empty.d.ts +3 -0
- package/dist/internal/render/empty.js +5 -0
- package/dist/internal/render/getReactElementRef.d.ts +5 -0
- package/dist/internal/render/getReactElementRef.js +15 -0
- package/dist/internal/render/getStateAttributesProps.d.ts +4 -0
- package/dist/internal/render/getStateAttributesProps.js +23 -0
- package/dist/internal/render/mergeObjects.d.ts +1 -0
- package/dist/internal/render/mergeObjects.js +14 -0
- package/dist/internal/render/mergeProps.d.ts +55 -0
- package/dist/internal/render/mergeProps.js +183 -0
- package/dist/internal/render/positioning.d.ts +33 -0
- package/dist/internal/render/positioning.js +112 -0
- package/dist/internal/render/reactVersion.d.ts +3 -0
- package/dist/internal/render/reactVersion.js +7 -0
- package/dist/internal/render/resolveClassName.d.ts +8 -0
- package/dist/internal/render/resolveClassName.js +12 -0
- package/dist/internal/render/resolveStyle.d.ts +8 -0
- package/dist/internal/render/resolveStyle.js +12 -0
- package/dist/internal/render/transition.d.ts +47 -0
- package/dist/internal/render/transition.js +259 -0
- package/dist/internal/render/types.d.ts +74 -0
- package/dist/internal/render/types.js +3 -0
- package/dist/internal/render/useMergedRefs.d.ts +21 -0
- package/dist/internal/render/useMergedRefs.js +102 -0
- package/dist/internal/render/useRefWithInit.d.ts +10 -0
- package/dist/internal/render/useRefWithInit.js +12 -0
- package/dist/internal/render/useRender.d.ts +71 -0
- package/dist/internal/render/useRender.js +12 -0
- package/dist/internal/render/useRenderElement.d.ts +62 -0
- package/dist/internal/render/useRenderElement.js +157 -0
- package/dist/internal/render/warn.d.ts +1 -0
- package/dist/internal/render/warn.js +15 -0
- package/dist/internal/safe-polygon.d.ts +21 -0
- package/dist/internal/safe-polygon.js +179 -0
- package/dist/internal/scroll-into-view-within.d.ts +1 -0
- package/dist/internal/scroll-into-view-within.js +26 -0
- package/dist/internal/standard-schema.d.ts +38 -0
- package/dist/internal/standard-schema.js +4 -0
- package/dist/internal/state-mappings.d.ts +8 -0
- package/dist/internal/state-mappings.js +17 -0
- package/dist/internal/use-controlled.d.ts +19 -0
- package/dist/internal/use-controlled.js +33 -0
- package/dist/message/index.d.ts +3 -0
- package/dist/message/index.js +3 -0
- package/dist/message/index.parts.d.ts +1 -0
- package/dist/message/index.parts.js +5 -0
- package/dist/message/message.d.ts +58 -0
- package/dist/message/message.js +93 -0
- package/dist/message-utils.d.ts +55 -0
- package/dist/message-utils.js +79 -0
- package/dist/nav/index.d.ts +4 -0
- package/dist/nav/index.js +5 -0
- package/dist/nav/index.parts.d.ts +3 -0
- package/dist/nav/index.parts.js +7 -0
- package/dist/nav/nav.d.ts +152 -0
- package/dist/nav/nav.js +660 -0
- package/dist/nav/store.d.ts +50 -0
- package/dist/nav/store.js +95 -0
- package/dist/reasoning/index.d.ts +3 -0
- package/dist/reasoning/index.js +3 -0
- package/dist/reasoning/index.parts.d.ts +1 -0
- package/dist/reasoning/index.parts.js +5 -0
- package/dist/reasoning/reasoning.d.ts +38 -0
- package/dist/reasoning/reasoning.js +67 -0
- package/dist/shell/index.d.ts +5 -0
- package/dist/shell/index.js +5 -0
- package/dist/shell/index.parts.d.ts +3 -0
- package/dist/shell/index.parts.js +7 -0
- package/dist/shell/shell.d.ts +108 -0
- package/dist/shell/shell.js +390 -0
- package/dist/shell/store.d.ts +112 -0
- package/dist/shell/store.js +134 -0
- package/dist/steps/index.d.ts +2 -0
- package/dist/steps/index.js +2 -0
- package/dist/steps/index.parts.d.ts +1 -0
- package/dist/steps/index.parts.js +5 -0
- package/dist/steps/steps.d.ts +90 -0
- package/dist/steps/steps.js +116 -0
- package/dist/tabs/anchored.d.ts +59 -0
- package/dist/tabs/anchored.js +109 -0
- package/dist/tabs/hover.d.ts +33 -0
- package/dist/tabs/hover.js +169 -0
- package/dist/tabs/index.d.ts +5 -0
- package/dist/tabs/index.js +4 -0
- package/dist/tabs/index.parts.d.ts +4 -0
- package/dist/tabs/index.parts.js +8 -0
- package/dist/tabs/store.d.ts +131 -0
- package/dist/tabs/store.js +248 -0
- package/dist/tabs/tabs.d.ts +217 -0
- package/dist/tabs/tabs.js +458 -0
- package/dist/thread/geometry.d.ts +23 -0
- package/dist/thread/geometry.js +48 -0
- package/dist/thread/index.d.ts +4 -0
- package/dist/thread/index.js +3 -0
- package/dist/thread/index.parts.d.ts +1 -0
- package/dist/thread/index.parts.js +5 -0
- package/dist/thread/stores.d.ts +21 -0
- package/dist/thread/stores.js +63 -0
- package/dist/thread/thread.d.ts +57 -0
- package/dist/thread/thread.js +663 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.js +17 -0
- package/package.json +125 -4
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
// Reading what the model wrote: a whole call checked against the catalog, a
|
|
2
|
+
// partial one read as nodes to draw, and its data bindings resolved.
|
|
3
|
+
import { componentOf, isBinding, isObject, limitsOf, } from "./catalog.js";
|
|
4
|
+
import { dropNulls, jsonSchemaOf } from "./json-schema.js";
|
|
5
|
+
/** The id a single-component call gets. */
|
|
6
|
+
const SINGLE_ID = "root";
|
|
7
|
+
const issue = (code, message, path) => ({
|
|
8
|
+
code,
|
|
9
|
+
message,
|
|
10
|
+
path,
|
|
11
|
+
});
|
|
12
|
+
/** Issues as text for the model, one per line: where, then what's wrong. */
|
|
13
|
+
export const formatIssues = (issues, max = 8) => {
|
|
14
|
+
const lines = issues
|
|
15
|
+
.slice(0, max)
|
|
16
|
+
.map((entry) => `${entry.path.join(".") || "(input)"}: ${entry.message}`);
|
|
17
|
+
if (issues.length > max)
|
|
18
|
+
lines.push(`…and ${issues.length - max} more.`);
|
|
19
|
+
return lines.join("\n");
|
|
20
|
+
};
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Bindings
|
|
23
|
+
const bindingIssues = (value, path) => {
|
|
24
|
+
const columnsOk = value.columns === undefined ||
|
|
25
|
+
(Array.isArray(value.columns) && value.columns.every((column) => typeof column === "string"));
|
|
26
|
+
const columnOk = value.column === undefined || typeof value.column === "string";
|
|
27
|
+
return columnsOk && columnOk
|
|
28
|
+
? []
|
|
29
|
+
: [
|
|
30
|
+
issue("invalid_binding", "A binding takes columns as a list of names, or one column.", path),
|
|
31
|
+
];
|
|
32
|
+
};
|
|
33
|
+
/** A row's value at a column name, which may be a dotted path (`owner.name`). */
|
|
34
|
+
const cell = (row, column) => {
|
|
35
|
+
if (!isObject(row))
|
|
36
|
+
return null;
|
|
37
|
+
if (column in row)
|
|
38
|
+
return row[column] ?? null;
|
|
39
|
+
const value = column
|
|
40
|
+
.split(".")
|
|
41
|
+
.reduce((current, key) => (isObject(current) ? current[key] : undefined), row);
|
|
42
|
+
return value ?? null;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* What a binding points at, picked: rows of objects become rows of the named
|
|
46
|
+
* `columns` in order, or one `column`'s values. Anything else comes as it is.
|
|
47
|
+
*/
|
|
48
|
+
const pickColumns = (data, binding) => {
|
|
49
|
+
const { columns, column } = binding;
|
|
50
|
+
if (!Array.isArray(data))
|
|
51
|
+
return data;
|
|
52
|
+
if (columns)
|
|
53
|
+
return data.map((row) => columns.map((name) => cell(row, name)));
|
|
54
|
+
if (column)
|
|
55
|
+
return data.map((row) => cell(row, column));
|
|
56
|
+
return data;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Props with each binding's data in place and its columns picked, and the
|
|
60
|
+
* bindings `lookup` found nothing for, which stay as they are.
|
|
61
|
+
*/
|
|
62
|
+
const substitute = (props, bindings, lookup) => {
|
|
63
|
+
const next = { ...props };
|
|
64
|
+
const missing = [];
|
|
65
|
+
for (const binding of bindings) {
|
|
66
|
+
const value = lookup(binding);
|
|
67
|
+
if (value === undefined)
|
|
68
|
+
missing.push(binding);
|
|
69
|
+
else
|
|
70
|
+
next[binding.prop] = pickColumns(value, binding);
|
|
71
|
+
}
|
|
72
|
+
return { props: next, missing };
|
|
73
|
+
};
|
|
74
|
+
/** Every binding in a tree, for the host to resolve. */
|
|
75
|
+
export const findBindings = (tree) => Object.entries(tree.elements).flatMap(([id, element]) => Object.entries(element.props)
|
|
76
|
+
.filter((entry) => isBinding(entry[1]))
|
|
77
|
+
.map(([prop, binding]) => ({ ...binding, id, prop })));
|
|
78
|
+
/**
|
|
79
|
+
* The tree with every binding replaced by its data, each element validated
|
|
80
|
+
* again with the data in place. `resolve` returns what a ref names, such as a
|
|
81
|
+
* tool result's rows, and may fetch it; the binding's columns are picked from
|
|
82
|
+
* it. `undefined` means not found.
|
|
83
|
+
*/
|
|
84
|
+
export const resolveBindings = async (tree, catalog, resolve) => {
|
|
85
|
+
const issues = [];
|
|
86
|
+
// An unbound element already holds its data; each bound one is replaced below.
|
|
87
|
+
const elements = { ...tree.elements };
|
|
88
|
+
const bound = new Map();
|
|
89
|
+
for (const binding of findBindings(tree)) {
|
|
90
|
+
bound.set(binding.id, [...(bound.get(binding.id) ?? []), binding]);
|
|
91
|
+
}
|
|
92
|
+
// Every element's data looked up at once, then validated in place; read back in order.
|
|
93
|
+
const entries = [...bound];
|
|
94
|
+
const settled = await Promise.all(entries.map(async ([id, bindings]) => {
|
|
95
|
+
const element = tree.elements[id];
|
|
96
|
+
const definition = element && componentOf(catalog, element.type);
|
|
97
|
+
if (!element || !definition)
|
|
98
|
+
return undefined;
|
|
99
|
+
const values = await Promise.all(bindings.map((binding) => resolve(binding)));
|
|
100
|
+
const { props, missing } = substitute(element.props, bindings, (binding) => values[bindings.indexOf(binding)]);
|
|
101
|
+
return { element, missing, result: await definition.props["~standard"].validate(props) };
|
|
102
|
+
}));
|
|
103
|
+
entries.forEach(([id], index) => {
|
|
104
|
+
const entry = settled[index];
|
|
105
|
+
if (!entry)
|
|
106
|
+
return;
|
|
107
|
+
const path = ["elements", id, "props"];
|
|
108
|
+
for (const binding of entry.missing) {
|
|
109
|
+
const message = `No data for "${binding.$data}".`;
|
|
110
|
+
issues.push(issue("unresolved_binding", message, [...path, binding.prop]));
|
|
111
|
+
}
|
|
112
|
+
// A prop whose data wasn't found is reported above, not again as a schema failure.
|
|
113
|
+
const unresolved = new Set(entry.missing.map((binding) => binding.prop));
|
|
114
|
+
if (entry.result.issues) {
|
|
115
|
+
issues.push(...propIssues(issuesOutside(entry.result.issues, unresolved), path));
|
|
116
|
+
}
|
|
117
|
+
else
|
|
118
|
+
elements[id] = { ...entry.element, props: entry.result.value };
|
|
119
|
+
});
|
|
120
|
+
return issues.length > 0 ? { issues } : { value: { root: tree.root, elements } };
|
|
121
|
+
};
|
|
122
|
+
const ELEMENT_KEYS = new Set(["id", "type", "component", "props", "children"]);
|
|
123
|
+
/** Two slips models make, read as meant: `component` for `type`, and props beside `type`. */
|
|
124
|
+
const normalizeElement = (raw) => {
|
|
125
|
+
const beside = Object.fromEntries(Object.entries(raw).filter(([key]) => !ELEMENT_KEYS.has(key)));
|
|
126
|
+
return {
|
|
127
|
+
type: raw.type ?? raw.component,
|
|
128
|
+
props: { ...beside, ...(isObject(raw.props) ? raw.props : {}) },
|
|
129
|
+
children: raw.children,
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
const inElements = (id) => ["elements", id];
|
|
133
|
+
// One component on its own is the input itself.
|
|
134
|
+
const atTop = () => [];
|
|
135
|
+
/** Elements as a list or a map keyed by id, or one component on its own. */
|
|
136
|
+
const readInput = (input) => {
|
|
137
|
+
const elements = new Map();
|
|
138
|
+
if (!isObject(input)) {
|
|
139
|
+
const problem = issue("invalid_input", "The input must be an object.", []);
|
|
140
|
+
return { root: undefined, elements, pathOf: inElements, issues: [problem] };
|
|
141
|
+
}
|
|
142
|
+
if (input.elements === undefined && (input.type ?? input.component) !== undefined) {
|
|
143
|
+
elements.set(SINGLE_ID, normalizeElement(input));
|
|
144
|
+
return { root: SINGLE_ID, elements, pathOf: atTop, issues: [] };
|
|
145
|
+
}
|
|
146
|
+
const issues = [];
|
|
147
|
+
if (Array.isArray(input.elements)) {
|
|
148
|
+
input.elements.forEach((raw, index) => {
|
|
149
|
+
if (!isObject(raw) || typeof raw.id !== "string") {
|
|
150
|
+
issues.push(issue("missing_id", 'Each element needs an "id".', ["elements", index]));
|
|
151
|
+
}
|
|
152
|
+
else if (elements.has(raw.id)) {
|
|
153
|
+
const message = `More than one element has the id "${raw.id}".`;
|
|
154
|
+
issues.push(issue("duplicate_id", message, ["elements", index, "id"]));
|
|
155
|
+
}
|
|
156
|
+
else
|
|
157
|
+
elements.set(raw.id, normalizeElement(raw));
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
else if (isObject(input.elements)) {
|
|
161
|
+
for (const [id, raw] of Object.entries(input.elements)) {
|
|
162
|
+
if (isObject(raw))
|
|
163
|
+
elements.set(id, normalizeElement(raw));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return { root: input.root, elements, pathOf: inElements, issues };
|
|
167
|
+
};
|
|
168
|
+
const childIds = (element) => Array.isArray(element.children)
|
|
169
|
+
? element.children.filter((child) => typeof child === "string")
|
|
170
|
+
: [];
|
|
171
|
+
/**
|
|
172
|
+
* What a schema can't check, element by element: a known type, and children
|
|
173
|
+
* that exist, are listed once, belong to one parent and stay within the limits.
|
|
174
|
+
*/
|
|
175
|
+
const checkElements = (read, catalog, limits) => {
|
|
176
|
+
const issues = [];
|
|
177
|
+
const types = Object.keys(catalog.components);
|
|
178
|
+
const parents = new Map();
|
|
179
|
+
if (read.elements.size > limits.nodes) {
|
|
180
|
+
const message = `There are ${read.elements.size} elements; the limit is ${limits.nodes}. Show less at once.`;
|
|
181
|
+
issues.push(issue("too_many_nodes", message, ["elements"]));
|
|
182
|
+
}
|
|
183
|
+
for (const [id, element] of read.elements) {
|
|
184
|
+
const path = read.pathOf(id);
|
|
185
|
+
const definition = componentOf(catalog, element.type);
|
|
186
|
+
if (!definition) {
|
|
187
|
+
const message = `"${String(element.type)}" is not a component. Components: ${types.join(", ")}.`;
|
|
188
|
+
issues.push(issue("unknown_type", message, [...path, "type"]));
|
|
189
|
+
}
|
|
190
|
+
const children = childIds(element);
|
|
191
|
+
if (children.length > 0 && definition && !definition.container) {
|
|
192
|
+
const message = `${String(element.type)} doesn't hold other elements.`;
|
|
193
|
+
issues.push(issue("unexpected_children", message, [...path, "children"]));
|
|
194
|
+
}
|
|
195
|
+
if (children.length > limits.children) {
|
|
196
|
+
const message = `${children.length} children; the limit is ${limits.children}. Group them into more containers.`;
|
|
197
|
+
issues.push(issue("too_many_children", message, [...path, "children"]));
|
|
198
|
+
}
|
|
199
|
+
const listed = new Set();
|
|
200
|
+
for (const child of children) {
|
|
201
|
+
if (listed.has(child)) {
|
|
202
|
+
const message = `"${child}" is listed more than once; list each child once.`;
|
|
203
|
+
issues.push(issue("duplicate_child", message, [...path, "children"]));
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
listed.add(child);
|
|
207
|
+
if (!read.elements.has(child)) {
|
|
208
|
+
const message = `"${child}" is not one of the elements.`;
|
|
209
|
+
issues.push(issue("dangling_child", message, [...path, "children"]));
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const other = parents.get(child);
|
|
213
|
+
if (other && other !== id) {
|
|
214
|
+
const message = `"${child}" is a child of both "${other}" and "${id}"; give each its own element.`;
|
|
215
|
+
issues.push(issue("shared_child", message, [...path, "children"]));
|
|
216
|
+
}
|
|
217
|
+
parents.set(child, id);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return issues;
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* The elements reached depth-first from the root, and the cycles and too-deep
|
|
224
|
+
* nesting met on the way. Each step carries its path, so a cycle is caught
|
|
225
|
+
* where it closes.
|
|
226
|
+
*/
|
|
227
|
+
const walkFromRoot = (read, root, limits) => {
|
|
228
|
+
// In walk order. Each element is walked once, so a child shared between
|
|
229
|
+
// parents, reported as `shared_child`, can't multiply the work.
|
|
230
|
+
const reached = new Set();
|
|
231
|
+
const issues = [];
|
|
232
|
+
const walk = (id, path) => {
|
|
233
|
+
if (path.includes(id)) {
|
|
234
|
+
const message = `"${id}" contains itself (${[...path, id].join(" → ")}).`;
|
|
235
|
+
issues.push(issue("cycle", message, [...read.pathOf(id), "children"]));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (reached.has(id))
|
|
239
|
+
return;
|
|
240
|
+
if (path.length + 1 > limits.depth) {
|
|
241
|
+
const message = `"${id}" is nested ${path.length + 1} deep; the limit is ${limits.depth}. Flatten it.`;
|
|
242
|
+
issues.push(issue("too_deep", message, read.pathOf(id)));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const element = read.elements.get(id);
|
|
246
|
+
if (!element)
|
|
247
|
+
return;
|
|
248
|
+
reached.add(id);
|
|
249
|
+
for (const child of childIds(element))
|
|
250
|
+
walk(child, [...path, id]);
|
|
251
|
+
};
|
|
252
|
+
walk(root, []);
|
|
253
|
+
return { reached: [...reached], issues };
|
|
254
|
+
};
|
|
255
|
+
/** A validator's issues, less those under the given props. */
|
|
256
|
+
const issuesOutside = (issues, props) => issues.filter((entry) => {
|
|
257
|
+
const first = entry.path?.[0];
|
|
258
|
+
return !props.has(String(isObject(first) ? first.key : first));
|
|
259
|
+
});
|
|
260
|
+
const propIssues = (issues, path) => issues.map((entry) => ({
|
|
261
|
+
code: "invalid_props",
|
|
262
|
+
message: entry.message,
|
|
263
|
+
path: [
|
|
264
|
+
...path,
|
|
265
|
+
...(entry.path ?? []).map((segment) => isObject(segment) ? String(segment.key) : segment),
|
|
266
|
+
],
|
|
267
|
+
}));
|
|
268
|
+
const readBound = (definition, raw, id) => {
|
|
269
|
+
const props = dropNulls(raw, jsonSchemaOf(definition));
|
|
270
|
+
const bindings = (definition.bind ?? []).flatMap((prop) => {
|
|
271
|
+
const value = props[prop];
|
|
272
|
+
return isBinding(value) ? [{ ...value, id, prop }] : [];
|
|
273
|
+
});
|
|
274
|
+
return { props, bindings };
|
|
275
|
+
};
|
|
276
|
+
/** Props validated, with bound props left in place for `bindings.resolve`. */
|
|
277
|
+
const checkProps = async (definition, { props, bindings }, path) => {
|
|
278
|
+
const issues = bindings.flatMap((binding) => bindingIssues(binding, [...path, binding.prop]));
|
|
279
|
+
const result = await definition.props["~standard"].validate(props);
|
|
280
|
+
if (!result.issues)
|
|
281
|
+
return { props: result.value, issues };
|
|
282
|
+
// A bound prop fails until resolved; only its binding's own shape counts now.
|
|
283
|
+
const bound = new Set(bindings.map((binding) => binding.prop));
|
|
284
|
+
return { props, issues: [...issues, ...propIssues(issuesOutside(result.issues, bound), path)] };
|
|
285
|
+
};
|
|
286
|
+
/**
|
|
287
|
+
* A whole call checked against the catalog: its shape read as meant, its
|
|
288
|
+
* structure and limits checked, and each element's props validated. Issues
|
|
289
|
+
* name the field, so `tree.formatIssues` can send them straight back to the model.
|
|
290
|
+
*/
|
|
291
|
+
export const validateTree = async (input, catalog) => {
|
|
292
|
+
const limits = limitsOf(catalog);
|
|
293
|
+
const read = readInput(input);
|
|
294
|
+
if (read.issues.length > 0)
|
|
295
|
+
return { issues: read.issues };
|
|
296
|
+
const { root } = read;
|
|
297
|
+
if (typeof root !== "string") {
|
|
298
|
+
return { issues: [issue("missing_root", "Name the id of the element at the top.", ["root"])] };
|
|
299
|
+
}
|
|
300
|
+
if (!read.elements.has(root)) {
|
|
301
|
+
const message = `"${root}" is not one of the elements.`;
|
|
302
|
+
return { issues: [issue("root_not_found", message, ["root"])] };
|
|
303
|
+
}
|
|
304
|
+
const walked = walkFromRoot(read, root, limits);
|
|
305
|
+
const issues = [...checkElements(read, catalog, limits), ...walked.issues];
|
|
306
|
+
// Every element's props validated at once; read back in tree order.
|
|
307
|
+
const checked = await Promise.all(walked.reached.map((id) => {
|
|
308
|
+
const element = read.elements.get(id);
|
|
309
|
+
const definition = componentOf(catalog, element.type);
|
|
310
|
+
const path = [...read.pathOf(id), "props"];
|
|
311
|
+
return definition && checkProps(definition, readBound(definition, element.props, id), path);
|
|
312
|
+
}));
|
|
313
|
+
const elements = {};
|
|
314
|
+
walked.reached.forEach((id, index) => {
|
|
315
|
+
const result = checked[index];
|
|
316
|
+
if (!result)
|
|
317
|
+
return;
|
|
318
|
+
const element = read.elements.get(id);
|
|
319
|
+
issues.push(...result.issues);
|
|
320
|
+
const children = childIds(element);
|
|
321
|
+
elements[id] = {
|
|
322
|
+
type: element.type,
|
|
323
|
+
props: result.props,
|
|
324
|
+
...(children.length > 0 ? { children } : {}),
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
return issues.length > 0 ? { issues } : { value: { root, elements } };
|
|
328
|
+
};
|
|
329
|
+
const isPromise = (value) => isObject(value) && typeof value.then === "function";
|
|
330
|
+
/** A node's props: ready once they validate and their bindings resolve. */
|
|
331
|
+
const readProps = (definition, { props, bindings }, resolve) => {
|
|
332
|
+
const resolved = substitute(props, bindings, (binding) => {
|
|
333
|
+
const value = resolve?.(binding);
|
|
334
|
+
return isPromise(value) ? undefined : value;
|
|
335
|
+
});
|
|
336
|
+
if (resolved.missing.length > 0)
|
|
337
|
+
return { status: "pending", props };
|
|
338
|
+
const result = definition.props["~standard"].validate(resolved.props);
|
|
339
|
+
// An async validator can't answer within a render, so its node waits.
|
|
340
|
+
if (isPromise(result) || result.issues)
|
|
341
|
+
return { status: "pending", props: resolved.props };
|
|
342
|
+
return { status: "ready", props: result.value };
|
|
343
|
+
};
|
|
344
|
+
/**
|
|
345
|
+
* Partial or whole input as nodes to draw, without throwing. A node whose type
|
|
346
|
+
* isn't known yet is skipped; one whose props don't validate yet, or whose
|
|
347
|
+
* data isn't resolved, is pending.
|
|
348
|
+
*/
|
|
349
|
+
export const readTree = (input, catalog, options = {}) => {
|
|
350
|
+
const read = readInput(input);
|
|
351
|
+
// Each element draws once. A cycle, a shared child or a child listed twice
|
|
352
|
+
// stops at the first, so the work stays linear and React never sees a key twice.
|
|
353
|
+
const drawn = new Set();
|
|
354
|
+
const build = (id) => {
|
|
355
|
+
const element = read.elements.get(id);
|
|
356
|
+
const definition = element && componentOf(catalog, element.type);
|
|
357
|
+
if (!element || !definition || drawn.has(id))
|
|
358
|
+
return undefined;
|
|
359
|
+
drawn.add(id);
|
|
360
|
+
const children = childIds(element).flatMap((child) => build(child) ?? []);
|
|
361
|
+
const props = readProps(definition, readBound(definition, element.props, id), options.resolve);
|
|
362
|
+
return { id, type: element.type, ...props, children };
|
|
363
|
+
};
|
|
364
|
+
return typeof read.root === "string" ? { root: build(read.root) } : {};
|
|
365
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AnyCatalog } from "./catalog.js";
|
|
2
|
+
import { type ReadTreeOptions, type TreeRead } from "./tree.js";
|
|
3
|
+
export type UseTreeOptions = ReadTreeOptions & {
|
|
4
|
+
/** The input is still being written, so nodes keep their last valid props. */
|
|
5
|
+
streaming?: boolean;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* `tree.read` in a component: the call's input as nodes to draw. While
|
|
9
|
+
* `streaming`, a node that was ready keeps its last valid props until newer
|
|
10
|
+
* ones validate, so a value caught mid-word doesn't blink back to pending.
|
|
11
|
+
*/
|
|
12
|
+
export declare const useTree: <C extends AnyCatalog>(input: unknown, catalog: C, options?: UseTreeOptions) => TreeRead<C>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import { readTree } from "./tree.js";
|
|
4
|
+
const NOTHING_HELD = new Map();
|
|
5
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype;
|
|
6
|
+
/** Equal as data, so recomputed props that read the same don't count as new. */
|
|
7
|
+
const isEqual = (a, b) => {
|
|
8
|
+
if (Object.is(a, b))
|
|
9
|
+
return true;
|
|
10
|
+
if (a instanceof Date && b instanceof Date)
|
|
11
|
+
return a.getTime() === b.getTime();
|
|
12
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
13
|
+
return a.length === b.length && a.every((item, index) => isEqual(item, b[index]));
|
|
14
|
+
}
|
|
15
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
16
|
+
const keys = Object.keys(a);
|
|
17
|
+
return keys.length === Object.keys(b).length && keys.every((key) => isEqual(a[key], b[key]));
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
};
|
|
21
|
+
const sameHeld = (a, b) => a.size === b.size &&
|
|
22
|
+
[...a].every(([id, entry]) => {
|
|
23
|
+
const other = b.get(id);
|
|
24
|
+
return other?.type === entry.type && isEqual(entry.props, other.props);
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* The tree with a pending node's held props back in place, and what to hold
|
|
28
|
+
* next. Props are kept only for the type they were valid for: while a type is
|
|
29
|
+
* still being written it can read as a shorter one (`Card` on the way to
|
|
30
|
+
* `CardGrid`).
|
|
31
|
+
*/
|
|
32
|
+
const holdProps = (root, held) => {
|
|
33
|
+
const next = new Map();
|
|
34
|
+
const visit = (node) => {
|
|
35
|
+
const entry = held.get(node.id);
|
|
36
|
+
const kept = node.status === "pending" && entry?.type === node.type ? entry : undefined;
|
|
37
|
+
if (node.status === "ready")
|
|
38
|
+
next.set(node.id, { type: node.type, props: node.props });
|
|
39
|
+
else if (kept)
|
|
40
|
+
next.set(node.id, kept);
|
|
41
|
+
return {
|
|
42
|
+
...node,
|
|
43
|
+
...(kept ? { status: "ready", props: kept.props } : {}),
|
|
44
|
+
children: node.children.map(visit),
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
return { root: visit(root), next };
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* `tree.read` in a component: the call's input as nodes to draw. While
|
|
51
|
+
* `streaming`, a node that was ready keeps its last valid props until newer
|
|
52
|
+
* ones validate, so a value caught mid-word doesn't blink back to pending.
|
|
53
|
+
*/
|
|
54
|
+
export const useTree = (input, catalog, options = {}) => {
|
|
55
|
+
const { resolve, streaming = false } = options;
|
|
56
|
+
const read = useMemo(() => readTree(input, catalog, { resolve }), [input, catalog, resolve]);
|
|
57
|
+
const [held, setHeld] = useState(NOTHING_HELD);
|
|
58
|
+
const holding = streaming && read.root ? holdProps(read.root, held) : undefined;
|
|
59
|
+
const next = holding?.next ?? NOTHING_HELD;
|
|
60
|
+
// Kept from earlier renders the way React documents: set during render, guarded.
|
|
61
|
+
if (!sameHeld(next, held))
|
|
62
|
+
setHeld(next);
|
|
63
|
+
return holding ? { root: holding.root } : read;
|
|
64
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Why a change happened, and the chance to `cancel()` it before it lands. */
|
|
2
|
+
export type ChangeEventDetails<Reason extends string> = {
|
|
3
|
+
reason: Reason;
|
|
4
|
+
/** The native event behind the change. */
|
|
5
|
+
event: Event;
|
|
6
|
+
/** The element that started it, where there is one. */
|
|
7
|
+
trigger: Element | undefined;
|
|
8
|
+
/** Keeps the change from landing. */
|
|
9
|
+
cancel: () => void;
|
|
10
|
+
readonly isCanceled: boolean;
|
|
11
|
+
};
|
|
12
|
+
export declare const createChangeEventDetails: <Reason extends string>(reason: Reason, event?: Event, trigger?: Element) => ChangeEventDetails<Reason>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Adapted from @base-ui/react v1.6.0 (MIT) — packages/react/src/internals/createBaseUIEventDetails.ts
|
|
2
|
+
// https://github.com/mui/base-ui — trimmed to the change details our primitives emit.
|
|
3
|
+
export const createChangeEventDetails = (reason, event = new Event("change"), trigger) => {
|
|
4
|
+
let canceled = false;
|
|
5
|
+
return {
|
|
6
|
+
reason,
|
|
7
|
+
event,
|
|
8
|
+
trigger,
|
|
9
|
+
cancel: () => {
|
|
10
|
+
canceled = true;
|
|
11
|
+
},
|
|
12
|
+
get isCanceled() {
|
|
13
|
+
return canceled;
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { PrimitiveProps } from "./primitive-props.js";
|
|
2
|
+
import { type TransitionStatus } from "./render/transition.js";
|
|
3
|
+
export type CollapsibleState = {
|
|
4
|
+
open: boolean;
|
|
5
|
+
};
|
|
6
|
+
export type CollapsibleRootProps = PrimitiveProps<"div", CollapsibleState> & {
|
|
7
|
+
open?: boolean;
|
|
8
|
+
defaultOpen?: boolean;
|
|
9
|
+
onOpenChange?: (open: boolean) => void;
|
|
10
|
+
};
|
|
11
|
+
export type CollapsibleTriggerProps = PrimitiveProps<"button", CollapsibleState>;
|
|
12
|
+
export type CollapsiblePanelProps = PrimitiveProps<"div", CollapsiblePanelState> & {
|
|
13
|
+
/** Keep the panel in the DOM (hidden) when closed. */
|
|
14
|
+
keepMounted?: boolean;
|
|
15
|
+
};
|
|
16
|
+
export type CollapsiblePanelState = CollapsibleState & {
|
|
17
|
+
transitionStatus: TransitionStatus;
|
|
18
|
+
};
|
|
19
|
+
export declare const Collapsible: (({ open: controlledOpen, defaultOpen, onOpenChange, className, render, style, ...elementProps }: CollapsibleRootProps) => import("react").JSX.Element) & {
|
|
20
|
+
Trigger: ({ className, render, style, ...elementProps }: CollapsibleTriggerProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
21
|
+
Panel: ({ keepMounted, className, render, style, ...elementProps }: CollapsiblePanelProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | null;
|
|
22
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
// Minimal internal disclosure primitive — replaces the @base-ui/react
|
|
4
|
+
// Collapsible dependency for the package's own parts. Trigger toggles, Panel
|
|
5
|
+
// hides (or unmounts) its content; state is exposed Base UI-style via
|
|
6
|
+
// presence attributes (data-open / data-closed) generated from state, plus
|
|
7
|
+
// the animation contract: data-starting-style on the panel's first open
|
|
8
|
+
// frame, data-ending-style while its exit animations run (hide/unmount waits
|
|
9
|
+
// for them), and the panel's natural height as --panel-height. That value is
|
|
10
|
+
// released once the open transition settles, so an open panel falls back to
|
|
11
|
+
// `auto` and tracks content that grows underneath it.
|
|
12
|
+
// Every part supports the Base UI render prop.
|
|
13
|
+
import { createContext, use, useCallback, useId, useMemo, useRef, useState, } from "react";
|
|
14
|
+
import { useOpenTransition } from "./render/transition.js";
|
|
15
|
+
import { useRenderElement } from "./render/useRenderElement.js";
|
|
16
|
+
import { openStateMapping, transitionStatusMapping } from "./state-mappings.js";
|
|
17
|
+
const CollapsibleContext = createContext(null);
|
|
18
|
+
const useCollapsible = () => {
|
|
19
|
+
const ctx = use(CollapsibleContext);
|
|
20
|
+
if (!ctx)
|
|
21
|
+
throw new Error("Collapsible parts must be used within <Collapsible>");
|
|
22
|
+
return ctx;
|
|
23
|
+
};
|
|
24
|
+
const CollapsibleRoot = ({ open: controlledOpen, defaultOpen = false, onOpenChange, className, render, style, ...elementProps }) => {
|
|
25
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
26
|
+
const isControlled = controlledOpen !== undefined;
|
|
27
|
+
const open = isControlled ? controlledOpen : internalOpen;
|
|
28
|
+
const panelId = useId();
|
|
29
|
+
const setOpen = useCallback((value) => {
|
|
30
|
+
if (!isControlled)
|
|
31
|
+
setInternalOpen(value);
|
|
32
|
+
onOpenChange?.(value);
|
|
33
|
+
}, [isControlled, onOpenChange]);
|
|
34
|
+
const element = useRenderElement("div", { className, render, style }, {
|
|
35
|
+
state: { open },
|
|
36
|
+
stateAttributesMapping: openStateMapping,
|
|
37
|
+
props: elementProps,
|
|
38
|
+
});
|
|
39
|
+
const context = useMemo(() => ({ open, setOpen, panelId }), [open, setOpen, panelId]);
|
|
40
|
+
return _jsx(CollapsibleContext, { value: context, children: element });
|
|
41
|
+
};
|
|
42
|
+
const CollapsibleTrigger = ({ className, render, style, ...elementProps }) => {
|
|
43
|
+
const { open, setOpen, panelId } = useCollapsible();
|
|
44
|
+
return useRenderElement("button", { className, render, style }, {
|
|
45
|
+
state: { open },
|
|
46
|
+
stateAttributesMapping: openStateMapping,
|
|
47
|
+
props: [
|
|
48
|
+
{
|
|
49
|
+
"aria-expanded": open,
|
|
50
|
+
"aria-controls": panelId,
|
|
51
|
+
// mergeProps runs the consumer's onClick first (rightmost wins), so
|
|
52
|
+
// preventDefault there still cancels the toggle — same contract as
|
|
53
|
+
// the previous inline `if (!event.defaultPrevented)` handler.
|
|
54
|
+
onClick: (event) => {
|
|
55
|
+
if (!event.defaultPrevented)
|
|
56
|
+
setOpen(!open);
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
elementProps,
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
const CollapsiblePanel = ({ keepMounted = false, className, render, style, ...elementProps }) => {
|
|
64
|
+
const { open, panelId } = useCollapsible();
|
|
65
|
+
const panelRef = useRef(null);
|
|
66
|
+
// The shared transition hook owns the open/starting/ending/close lifecycle: it keeps the
|
|
67
|
+
// panel mounted through its exit (waiting on getAnimations), measures the natural height
|
|
68
|
+
// for the CSS var, and reports the status that drives data-starting/ending-style.
|
|
69
|
+
const { mounted, transitionStatus, height } = useOpenTransition(open, panelRef, {
|
|
70
|
+
measureHeight: true,
|
|
71
|
+
});
|
|
72
|
+
const hidden = !open && !mounted;
|
|
73
|
+
return useRenderElement("div", { className, render, style }, {
|
|
74
|
+
enabled: mounted || keepMounted,
|
|
75
|
+
state: { open, transitionStatus },
|
|
76
|
+
stateAttributesMapping: { ...openStateMapping, ...transitionStatusMapping },
|
|
77
|
+
ref: panelRef,
|
|
78
|
+
props: [
|
|
79
|
+
{
|
|
80
|
+
id: panelId,
|
|
81
|
+
hidden,
|
|
82
|
+
// Once the open transition settles the height is released back to null, so the
|
|
83
|
+
// property stops being written and a `height: var(…)` consumer falls back to auto
|
|
84
|
+
// — that is what lets an open panel track content that grows.
|
|
85
|
+
style: height !== null ? { "--panel-height": `${height}px` } : undefined,
|
|
86
|
+
},
|
|
87
|
+
elementProps,
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
export const Collapsible = Object.assign(CollapsibleRoot, {
|
|
92
|
+
Trigger: CollapsibleTrigger,
|
|
93
|
+
Panel: CollapsiblePanel,
|
|
94
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List maths for an ordered, mutable collection.
|
|
3
|
+
*
|
|
4
|
+
* Base UI derives collection order from the DOM, walking
|
|
5
|
+
* `compareDocumentPosition`, because its collections are declarative children.
|
|
6
|
+
* Ours are not: the order is an array of ids on the store, so order is *data*
|
|
7
|
+
* and none of that machinery is needed. What is left is arithmetic, which is
|
|
8
|
+
* pure and therefore easy to be sure about.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Which item to land on when `removed` leaves the collection.
|
|
12
|
+
*
|
|
13
|
+
* The item that slides into the vacated slot, or the last one if the tail was
|
|
14
|
+
* removed — the same thing an editor does when you close a tab, and the
|
|
15
|
+
* closest thing to "stay where you were".
|
|
16
|
+
*/
|
|
17
|
+
export declare const adjacentTo: <Id>(items: readonly Id[], removed: Id,
|
|
18
|
+
/** Skips anything it rejects, searching outwards from the vacated slot. */
|
|
19
|
+
isEligible?: (item: Id) => boolean) => (Id & {}) | null;
|
|
20
|
+
/**
|
|
21
|
+
* Step one place through the collection.
|
|
22
|
+
*
|
|
23
|
+
* From nothing, arrive at whichever end you are heading away from. At an edge,
|
|
24
|
+
* either wrap or stay put.
|
|
25
|
+
*/
|
|
26
|
+
export declare const step: <Id>(items: readonly Id[], from: Id | null, direction: 1 | -1, { loop }?: {
|
|
27
|
+
loop?: boolean | undefined;
|
|
28
|
+
}) => NonNullable<Id> | null;
|
|
29
|
+
/** Move an item to a new position, clamping the target into range. */
|
|
30
|
+
export declare const moveTo: <Id>(items: readonly Id[], id: Id, toIndex: number) => Id[];
|
|
31
|
+
/** Insert an item, or move it if it is already present. Defaults to the end. */
|
|
32
|
+
export declare const insertAt: <Id>(items: readonly Id[], id: Id, index?: number) => Id[];
|