@getaster/elements 0.0.0-stage → 0.1.0-preview.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/LICENSE +202 -0
- package/NOTICE +10 -0
- package/README.md +6 -2
- package/bin/aster-theme-check.js +317 -0
- package/dist/base/drag.d.ts +45 -0
- package/dist/base/drag.js +139 -0
- package/dist/base/element.d.ts +51 -0
- package/dist/base/element.js +133 -0
- package/dist/breadcrumbs/breadcrumbs.d.ts +23 -0
- package/dist/breadcrumbs/breadcrumbs.js +51 -0
- package/dist/button/button.d.ts +37 -0
- package/dist/button/button.js +108 -0
- package/dist/caret/caret-group.d.ts +14 -0
- package/dist/caret/caret-group.js +71 -0
- package/dist/carousel/carousel.d.ts +28 -0
- package/dist/carousel/carousel.js +134 -0
- package/dist/component/component.d.ts +73 -0
- package/dist/component/component.js +190 -0
- package/dist/component/echarts.d.ts +46 -0
- package/dist/component/echarts.js +95 -0
- package/dist/composition/authoring.d.ts +157 -0
- package/dist/composition/authoring.js +291 -0
- package/dist/composition/builtins.d.ts +1130 -0
- package/dist/composition/builtins.js +318 -0
- package/dist/composition/command-bar.d.ts +18 -0
- package/dist/composition/command-bar.js +34 -0
- package/dist/composition/commands.d.ts +14 -0
- package/dist/composition/commands.js +59 -0
- package/dist/composition/composite.d.ts +48 -0
- package/dist/composition/composite.js +409 -0
- package/dist/composition/contracts.d.ts +63 -0
- package/dist/composition/contracts.js +97 -0
- package/dist/composition/definition.d.ts +94 -0
- package/dist/composition/definition.js +291 -0
- package/dist/composition/element.d.ts +35 -0
- package/dist/composition/element.js +51 -0
- package/dist/composition/registry.d.ts +83 -0
- package/dist/composition/registry.js +99 -0
- package/dist/composition/schema.d.ts +9 -0
- package/dist/composition/schema.js +139 -0
- package/dist/composition/sessions.d.ts +15 -0
- package/dist/composition/sessions.js +61 -0
- package/dist/dashboard/dashboard.d.ts +26 -0
- package/dist/dashboard/dashboard.js +296 -0
- package/dist/dashboard/layout.d.ts +19 -0
- package/dist/dashboard/layout.js +48 -0
- package/dist/dashboard/widget.d.ts +42 -0
- package/dist/dashboard/widget.js +165 -0
- package/dist/date/calendar.d.ts +34 -0
- package/dist/date/calendar.js +137 -0
- package/dist/date/date-field.d.ts +18 -0
- package/dist/date/date-field.js +81 -0
- package/dist/date/dates.d.ts +22 -0
- package/dist/date/dates.js +80 -0
- package/dist/dialog/dialog.d.ts +23 -0
- package/dist/dialog/dialog.js +70 -0
- package/dist/field/bound-field.d.ts +55 -0
- package/dist/field/bound-field.js +139 -0
- package/dist/field/check.d.ts +11 -0
- package/dist/field/check.js +26 -0
- package/dist/field/composite-field.d.ts +35 -0
- package/dist/field/composite-field.js +88 -0
- package/dist/field/field.d.ts +29 -0
- package/dist/field/field.js +75 -0
- package/dist/field/input.d.ts +15 -0
- package/dist/field/input.js +18 -0
- package/dist/field/number-field.d.ts +33 -0
- package/dist/field/number-field.js +179 -0
- package/dist/field/numeric.d.ts +4 -0
- package/dist/field/numeric.js +57 -0
- package/dist/field/values.d.ts +14 -0
- package/dist/field/values.js +60 -0
- package/dist/filter/filter-builder.d.ts +18 -0
- package/dist/filter/filter-builder.js +305 -0
- package/dist/form/controls.d.ts +20 -0
- package/dist/form/controls.js +34 -0
- package/dist/form/form.d.ts +46 -0
- package/dist/form/form.js +85 -0
- package/dist/format.d.ts +10 -0
- package/dist/format.js +57 -0
- package/dist/grid/filters.d.ts +16 -0
- package/dist/grid/filters.js +66 -0
- package/dist/grid/grid.d.ts +209 -0
- package/dist/grid/grid.js +2587 -0
- package/dist/grid/rows.d.ts +32 -0
- package/dist/grid/rows.js +89 -0
- package/dist/grid/view.d.ts +69 -0
- package/dist/grid/view.js +172 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +61 -0
- package/dist/layout/stack.d.ts +112 -0
- package/dist/layout/stack.js +21 -0
- package/dist/list-edit/list-edit.d.ts +42 -0
- package/dist/list-edit/list-edit.js +220 -0
- package/dist/listbox/listbox.d.ts +45 -0
- package/dist/listbox/listbox.js +187 -0
- package/dist/menu/menu.d.ts +55 -0
- package/dist/menu/menu.js +286 -0
- package/dist/motion.d.ts +8 -0
- package/dist/motion.js +23 -0
- package/dist/overlay/anchor.d.ts +18 -0
- package/dist/overlay/anchor.js +41 -0
- package/dist/picker/options.d.ts +59 -0
- package/dist/picker/options.js +97 -0
- package/dist/picker/picker.d.ts +46 -0
- package/dist/picker/picker.js +492 -0
- package/dist/picker/tree-choices.d.ts +38 -0
- package/dist/picker/tree-choices.js +72 -0
- package/dist/picker/tree-picker.d.ts +29 -0
- package/dist/picker/tree-picker.js +282 -0
- package/dist/progress/policy.d.ts +5 -0
- package/dist/progress/policy.js +28 -0
- package/dist/progress/status.d.ts +20 -0
- package/dist/progress/status.js +38 -0
- package/dist/rule/operators.d.ts +8 -0
- package/dist/rule/operators.js +9 -0
- package/dist/rule/rule-editor.d.ts +57 -0
- package/dist/rule/rule-editor.js +290 -0
- package/dist/section/accordion.d.ts +24 -0
- package/dist/section/accordion.js +90 -0
- package/dist/section/section.d.ts +30 -0
- package/dist/section/section.js +99 -0
- package/dist/select/choice-element.d.ts +20 -0
- package/dist/select/choice-element.js +55 -0
- package/dist/select/choice.d.ts +34 -0
- package/dist/select/choice.js +122 -0
- package/dist/select/many.d.ts +13 -0
- package/dist/select/many.js +20 -0
- package/dist/select/select.d.ts +16 -0
- package/dist/select/select.js +40 -0
- package/dist/sidebar/sidebar.d.ts +47 -0
- package/dist/sidebar/sidebar.js +105 -0
- package/dist/slicer/slicer.d.ts +26 -0
- package/dist/slicer/slicer.js +140 -0
- package/dist/split/split.d.ts +41 -0
- package/dist/split/split.js +237 -0
- package/dist/styles/contract.d.ts +36 -0
- package/dist/styles/contract.js +39 -0
- package/dist/switch/switch.d.ts +22 -0
- package/dist/switch/switch.js +69 -0
- package/dist/tabs/tabs.d.ts +30 -0
- package/dist/tabs/tabs.js +202 -0
- package/dist/tile-grid/scroll.d.ts +9 -0
- package/dist/tile-grid/scroll.js +21 -0
- package/dist/tile-grid/tile-grid.d.ts +64 -0
- package/dist/tile-grid/tile-grid.js +734 -0
- package/dist/toast/toast.d.ts +25 -0
- package/dist/toast/toast.js +92 -0
- package/dist/tree/navigator.d.ts +41 -0
- package/dist/tree/navigator.js +228 -0
- package/dist/window/chrome.d.ts +3 -0
- package/dist/window/chrome.js +7 -0
- package/dist/window/window.d.ts +46 -0
- package/dist/window/window.js +347 -0
- package/dist/window/workspace.d.ts +29 -0
- package/dist/window/workspace.js +137 -0
- package/package.json +45 -4
- package/styles/aster-ui.css +1147 -0
- package/styles/base.css +88 -0
- package/styles/contract.ts +41 -0
- package/styles/elements.css +1058 -0
- package/styles/harbour.css +126 -0
- package/styles/lamplight.css +118 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
export class CompositeError extends Error {
|
|
2
|
+
path;
|
|
3
|
+
constructor(path, message) {
|
|
4
|
+
super(`${path}: ${message}`);
|
|
5
|
+
this.path = path;
|
|
6
|
+
this.name = "CompositeError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function located(error, sources = {}) {
|
|
10
|
+
if (!(error instanceof CompositeError))
|
|
11
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
12
|
+
let path = error.path;
|
|
13
|
+
while (path && !sources[path])
|
|
14
|
+
path = path.includes(".") ? path.slice(0, path.lastIndexOf(".")) : "";
|
|
15
|
+
const at = sources[path];
|
|
16
|
+
return at ? new Error(`${at.file}:${at.line}:${at.column}: ${error.message}`, { cause: error }) : error;
|
|
17
|
+
}
|
|
18
|
+
export function record(value, path) {
|
|
19
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
|
|
20
|
+
throw new CompositeError(path, "expected a mapping");
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
export function keys(value, allowed, path) {
|
|
24
|
+
for (const key of Object.keys(value))
|
|
25
|
+
if (!allowed.includes(key))
|
|
26
|
+
throw new CompositeError(`${path}.${key}`, "unknown property");
|
|
27
|
+
}
|
|
28
|
+
export function name(value, path) {
|
|
29
|
+
if (typeof value !== "string" || !/^[a-zA-Z][\w-]*$/.test(value) || ["constructor", "prototype", "__proto__"].includes(value))
|
|
30
|
+
throw new CompositeError(path, "expected a safe name");
|
|
31
|
+
}
|
|
32
|
+
function text(value, path) {
|
|
33
|
+
if (typeof value !== "string" || !value.length)
|
|
34
|
+
throw new CompositeError(path, "expected text");
|
|
35
|
+
}
|
|
36
|
+
function condition(value, path) {
|
|
37
|
+
if (typeof value === "string") {
|
|
38
|
+
text(value, path);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const entry = record(value, path);
|
|
42
|
+
switch (entry.op) {
|
|
43
|
+
case "and":
|
|
44
|
+
case "or":
|
|
45
|
+
keys(entry, ["op", "args"], path);
|
|
46
|
+
if (!Array.isArray(entry.args))
|
|
47
|
+
throw new CompositeError(`${path}.args`, "expected an array");
|
|
48
|
+
entry.args.forEach((arg, i) => condition(arg, `${path}.args.${i}`));
|
|
49
|
+
return;
|
|
50
|
+
case "not":
|
|
51
|
+
keys(entry, ["op", "arg"], path);
|
|
52
|
+
condition(entry.arg, `${path}.arg`);
|
|
53
|
+
return;
|
|
54
|
+
case "eq":
|
|
55
|
+
case "ne":
|
|
56
|
+
case "gt":
|
|
57
|
+
case "ge":
|
|
58
|
+
case "lt":
|
|
59
|
+
case "le":
|
|
60
|
+
keys(entry, ["op", "field", "value", "other"], path);
|
|
61
|
+
text(entry.field, `${path}.field`);
|
|
62
|
+
if (Object.hasOwn(entry, "value") === Object.hasOwn(entry, "other"))
|
|
63
|
+
throw new CompositeError(path, "comparison needs exactly one value or other path");
|
|
64
|
+
if (entry.other !== undefined)
|
|
65
|
+
text(entry.other, `${path}.other`);
|
|
66
|
+
return;
|
|
67
|
+
case "contains":
|
|
68
|
+
case "startswith":
|
|
69
|
+
case "endswith":
|
|
70
|
+
keys(entry, ["op", "field", "value", "caseInsensitive"], path);
|
|
71
|
+
text(entry.field, `${path}.field`);
|
|
72
|
+
if (typeof entry.value !== "string")
|
|
73
|
+
throw new CompositeError(`${path}.value`, "expected text");
|
|
74
|
+
if (entry.caseInsensitive !== undefined && typeof entry.caseInsensitive !== "boolean")
|
|
75
|
+
throw new CompositeError(path, "caseInsensitive must be boolean");
|
|
76
|
+
return;
|
|
77
|
+
case "in":
|
|
78
|
+
keys(entry, ["op", "field", "values"], path);
|
|
79
|
+
text(entry.field, `${path}.field`);
|
|
80
|
+
if (!Array.isArray(entry.values))
|
|
81
|
+
throw new CompositeError(`${path}.values`, "expected an array");
|
|
82
|
+
return;
|
|
83
|
+
case "any":
|
|
84
|
+
case "all":
|
|
85
|
+
case "via":
|
|
86
|
+
keys(entry, ["op", "field", "where"], path);
|
|
87
|
+
text(entry.field, `${path}.field`);
|
|
88
|
+
if (typeof entry.where === "string")
|
|
89
|
+
throw new CompositeError(`${path}.where`, "related criteria require an explicit comparison");
|
|
90
|
+
condition(entry.where, `${path}.where`);
|
|
91
|
+
return;
|
|
92
|
+
default: throw new CompositeError(`${path}.op`, "unknown rule operator");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export function defineComposite(value) {
|
|
96
|
+
let nodes = 0;
|
|
97
|
+
const seen = new Set();
|
|
98
|
+
function json(v, path, depth) {
|
|
99
|
+
if (++nodes > 10000 || depth > 40)
|
|
100
|
+
throw new CompositeError(path, "definition exceeds size/depth limit");
|
|
101
|
+
if (v === null || typeof v === "string" || typeof v === "boolean")
|
|
102
|
+
return;
|
|
103
|
+
if (typeof v === "number" && Number.isFinite(v) && Math.abs(v) <= Number.MAX_SAFE_INTEGER)
|
|
104
|
+
return;
|
|
105
|
+
if (!v || typeof v !== "object")
|
|
106
|
+
throw new CompositeError(path, "expected JSON-compatible data");
|
|
107
|
+
if (seen.has(v))
|
|
108
|
+
throw new CompositeError(path, "cyclic objects are not supported");
|
|
109
|
+
seen.add(v);
|
|
110
|
+
if (Array.isArray(v))
|
|
111
|
+
v.forEach((item, i) => json(item, `${path}.${i}`, depth + 1));
|
|
112
|
+
else
|
|
113
|
+
for (const [key, item] of Object.entries(record(v, path))) {
|
|
114
|
+
if (["__proto__", "prototype", "constructor", "<<"].includes(key))
|
|
115
|
+
throw new CompositeError(`${path}.${key}`, "unsafe key");
|
|
116
|
+
json(item, `${path}.${key}`, depth + 1);
|
|
117
|
+
}
|
|
118
|
+
seen.delete(v);
|
|
119
|
+
}
|
|
120
|
+
json(value, "definition", 0);
|
|
121
|
+
const d = record(value, "definition");
|
|
122
|
+
keys(d, ["version", "name", "model", "publish", "commands", "children", "layout", "customize"], "definition");
|
|
123
|
+
if (d.version !== 1)
|
|
124
|
+
throw new CompositeError("definition.version", "unsupported version");
|
|
125
|
+
name(d.name, "definition.name");
|
|
126
|
+
const model = record(d.model, "definition.model");
|
|
127
|
+
keys(model, ["type", "inputs"], "definition.model");
|
|
128
|
+
name(model.type, "definition.model.type");
|
|
129
|
+
if (model.inputs !== undefined)
|
|
130
|
+
record(model.inputs, "definition.model.inputs");
|
|
131
|
+
for (const [key, item] of Object.entries(record(d.publish ?? {}, "definition.publish"))) {
|
|
132
|
+
name(key, `definition.publish.${key}`);
|
|
133
|
+
const entry = record(item, `definition.publish.${key}`);
|
|
134
|
+
keys(entry, ["ref"], `definition.publish.${key}`);
|
|
135
|
+
text(entry.ref, `definition.publish.${key}.ref`);
|
|
136
|
+
}
|
|
137
|
+
for (const [key, item] of Object.entries(record(d.commands ?? {}, "definition.commands"))) {
|
|
138
|
+
name(key, `definition.commands.${key}`);
|
|
139
|
+
const path = `definition.commands.${key}`;
|
|
140
|
+
const entry = record(item, path);
|
|
141
|
+
keys(entry, ["use", "enabledWhen", "reason"], path);
|
|
142
|
+
text(entry.use, `${path}.use`);
|
|
143
|
+
if (entry.enabledWhen !== undefined)
|
|
144
|
+
condition(entry.enabledWhen, `${path}.enabledWhen`);
|
|
145
|
+
if (entry.reason !== undefined)
|
|
146
|
+
text(entry.reason, `${path}.reason`);
|
|
147
|
+
}
|
|
148
|
+
const children = record(d.children, "definition.children");
|
|
149
|
+
for (const [key, item] of Object.entries(children)) {
|
|
150
|
+
name(key, `definition.children.${key}`);
|
|
151
|
+
const path = `definition.children.${key}`;
|
|
152
|
+
const entry = record(item, path);
|
|
153
|
+
keys(entry, ["type", "props", "create", "retain", "enabled"], path);
|
|
154
|
+
name(entry.type, `${path}.type`);
|
|
155
|
+
if (entry.props !== undefined)
|
|
156
|
+
record(entry.props, `${path}.props`);
|
|
157
|
+
if (entry.create !== undefined && entry.create !== "eager" && entry.create !== "on-demand")
|
|
158
|
+
throw new CompositeError(`${path}.create`, "expected eager or on-demand");
|
|
159
|
+
if (entry.retain !== undefined && typeof entry.retain !== "boolean")
|
|
160
|
+
throw new CompositeError(`${path}.retain`, "expected boolean");
|
|
161
|
+
if (entry.enabled !== undefined && typeof entry.enabled !== "boolean")
|
|
162
|
+
throw new CompositeError(`${path}.enabled`, "expected boolean");
|
|
163
|
+
}
|
|
164
|
+
function names(v, path) {
|
|
165
|
+
if (!Array.isArray(v))
|
|
166
|
+
throw new CompositeError(path, "expected an array of names");
|
|
167
|
+
v.forEach((item, i) => name(item, `${path}.${i}`));
|
|
168
|
+
}
|
|
169
|
+
if (d.customize !== undefined) {
|
|
170
|
+
const c = record(d.customize, "definition.customize");
|
|
171
|
+
keys(c, ["children", "modelInputs", "layout"], "definition.customize");
|
|
172
|
+
if (c.layout !== undefined && typeof c.layout !== "boolean")
|
|
173
|
+
throw new CompositeError("definition.customize.layout", "expected boolean");
|
|
174
|
+
if (c.modelInputs !== undefined) {
|
|
175
|
+
names(c.modelInputs, "definition.customize.modelInputs");
|
|
176
|
+
for (const key of c.modelInputs)
|
|
177
|
+
if (!Object.hasOwn(model.inputs ?? {}, key))
|
|
178
|
+
throw new CompositeError("definition.customize.modelInputs", `unknown model input '${key}'`);
|
|
179
|
+
}
|
|
180
|
+
for (const [key, value] of Object.entries(record(c.children ?? {}, "definition.customize.children"))) {
|
|
181
|
+
const path = `definition.customize.children.${key}`;
|
|
182
|
+
if (!Object.hasOwn(children, key))
|
|
183
|
+
throw new CompositeError(path, "unknown child");
|
|
184
|
+
const spec = record(value, path);
|
|
185
|
+
keys(spec, ["props", "replace", "optional", "lifecycle"], path);
|
|
186
|
+
if (spec.props !== undefined)
|
|
187
|
+
names(spec.props, `${path}.props`);
|
|
188
|
+
if (spec.replace !== undefined)
|
|
189
|
+
names(spec.replace, `${path}.replace`);
|
|
190
|
+
for (const flag of ["optional", "lifecycle"])
|
|
191
|
+
if (spec[flag] !== undefined && typeof spec[flag] !== "boolean")
|
|
192
|
+
throw new CompositeError(`${path}.${flag}`, "expected boolean");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const placed = new Set();
|
|
196
|
+
function layout(v, path) {
|
|
197
|
+
const entry = record(v, path);
|
|
198
|
+
if (entry.child !== undefined) {
|
|
199
|
+
keys(entry, ["child"], path);
|
|
200
|
+
name(entry.child, `${path}.child`);
|
|
201
|
+
if (!Object.hasOwn(children, entry.child))
|
|
202
|
+
throw new CompositeError(path, `unknown child '${entry.child}'`);
|
|
203
|
+
if (placed.has(entry.child))
|
|
204
|
+
throw new CompositeError(path, `duplicate placement '${entry.child}'`);
|
|
205
|
+
placed.add(entry.child);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
switch (entry.type) {
|
|
209
|
+
case "stack":
|
|
210
|
+
keys(entry, ["type", "items"], path);
|
|
211
|
+
if (!Array.isArray(entry.items))
|
|
212
|
+
throw new CompositeError(`${path}.items`, "expected an array");
|
|
213
|
+
entry.items.forEach((item, i) => layout(item, `${path}.items.${i}`));
|
|
214
|
+
break;
|
|
215
|
+
case "deck":
|
|
216
|
+
keys(entry, ["type", "active", "items"], path);
|
|
217
|
+
if (entry.active === undefined)
|
|
218
|
+
throw new CompositeError(`${path}.active`, "required");
|
|
219
|
+
for (const [key, item] of Object.entries(record(entry.items, `${path}.items`))) {
|
|
220
|
+
name(key, path);
|
|
221
|
+
layout(item, `${path}.items.${key}`);
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
case "outlet":
|
|
225
|
+
keys(entry, ["type", "active", "content"], path);
|
|
226
|
+
if (entry.active === undefined)
|
|
227
|
+
throw new CompositeError(`${path}.active`, "required");
|
|
228
|
+
layout(entry.content, `${path}.content`);
|
|
229
|
+
break;
|
|
230
|
+
default: throw new CompositeError(path, "unknown layout type");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
layout(d.layout, "definition.layout");
|
|
234
|
+
for (const key of Object.keys(children))
|
|
235
|
+
if (!placed.has(key))
|
|
236
|
+
throw new CompositeError(`definition.children.${key}`, "child has no placement");
|
|
237
|
+
// Detach caller-owned objects and freeze every level of the public definition.
|
|
238
|
+
const copy = JSON.parse(JSON.stringify(value));
|
|
239
|
+
const normalized = copy.customize?.children ? { ...copy, customize: { ...copy.customize,
|
|
240
|
+
children: Object.fromEntries(Object.entries(copy.customize.children).map(([key, spec]) => [key, spec.replace ? { ...spec, replace: [...new Set([copy.children[key].type, ...spec.replace])] } : spec])),
|
|
241
|
+
} } : copy;
|
|
242
|
+
function freeze(v) {
|
|
243
|
+
if (!v || typeof v !== "object")
|
|
244
|
+
return;
|
|
245
|
+
Object.values(v).forEach(freeze);
|
|
246
|
+
Object.freeze(v);
|
|
247
|
+
}
|
|
248
|
+
freeze(normalized);
|
|
249
|
+
return normalized;
|
|
250
|
+
}
|
|
251
|
+
/** Immutable public customization: base → derived definition → instance overrides. */
|
|
252
|
+
export function deriveComposite(base, overrides) {
|
|
253
|
+
const definition = defineComposite(base);
|
|
254
|
+
const patch = record(overrides, "overrides");
|
|
255
|
+
keys(patch, ["name", "modelInputs", "children", "layout"], "overrides");
|
|
256
|
+
const customize = definition.customize;
|
|
257
|
+
const children = { ...definition.children };
|
|
258
|
+
for (const [key, value] of Object.entries(record(patch.children ?? {}, "overrides.children"))) {
|
|
259
|
+
const path = `overrides.children.${key}`;
|
|
260
|
+
const child = children[key];
|
|
261
|
+
const permission = customize?.children?.[key];
|
|
262
|
+
if (!child || !permission)
|
|
263
|
+
throw new CompositeError(path, "child is not a public customization point");
|
|
264
|
+
const next = record(value, path);
|
|
265
|
+
keys(next, ["type", "props", "enabled", "create", "retain"], path);
|
|
266
|
+
if (next.type !== undefined && next.type !== child.type && !permission.replace?.includes(String(next.type)))
|
|
267
|
+
throw new CompositeError(`${path}.type`, "replacement type is not permitted");
|
|
268
|
+
if (next.enabled !== undefined && !permission.optional)
|
|
269
|
+
throw new CompositeError(`${path}.enabled`, "child suppression is not public");
|
|
270
|
+
if ((next.create !== undefined || next.retain !== undefined) && !permission.lifecycle)
|
|
271
|
+
throw new CompositeError(path, "lifecycle policy is not public");
|
|
272
|
+
const props = record(next.props ?? {}, `${path}.props`);
|
|
273
|
+
for (const property of Object.keys(props))
|
|
274
|
+
if (!permission.props?.includes(property))
|
|
275
|
+
throw new CompositeError(`${path}.props.${property}`, "property is not public");
|
|
276
|
+
children[key] = { ...child, ...next, props: { ...child.props, ...props } };
|
|
277
|
+
}
|
|
278
|
+
const modelInputs = record(patch.modelInputs ?? {}, "overrides.modelInputs");
|
|
279
|
+
for (const key of Object.keys(modelInputs))
|
|
280
|
+
if (!customize?.modelInputs?.includes(key))
|
|
281
|
+
throw new CompositeError(`overrides.modelInputs.${key}`, "model input is not public");
|
|
282
|
+
if (patch.layout !== undefined && !customize?.layout)
|
|
283
|
+
throw new CompositeError("overrides.layout", "placement is not public");
|
|
284
|
+
return defineComposite({
|
|
285
|
+
...definition,
|
|
286
|
+
...(patch.name !== undefined ? { name: patch.name } : {}),
|
|
287
|
+
model: { ...definition.model, inputs: { ...definition.model.inputs, ...modelInputs } },
|
|
288
|
+
children,
|
|
289
|
+
...(patch.layout !== undefined ? { layout: patch.layout } : {}),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AsterElement, type View } from "../base/element.ts";
|
|
2
|
+
import type { CompositeType } from "./contracts.ts";
|
|
3
|
+
export interface ElementProperty<T, C extends CompositeType<T> = CompositeType<T>> {
|
|
4
|
+
readonly type: C;
|
|
5
|
+
/** Called separately for each element; mutable defaults must be newly created. */
|
|
6
|
+
readonly initial: () => T;
|
|
7
|
+
readonly description: string;
|
|
8
|
+
}
|
|
9
|
+
export type ElementProperties = Readonly<Record<string, ElementProperty<unknown>>>;
|
|
10
|
+
export type ElementValues<P extends ElementProperties> = {
|
|
11
|
+
-readonly [K in keyof P]: P[K] extends ElementProperty<infer T> ? T : never;
|
|
12
|
+
};
|
|
13
|
+
export type ElementContract<P extends ElementProperties> = {
|
|
14
|
+
readonly [K in keyof P]: P[K]["type"] & {
|
|
15
|
+
readonly optional: true;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export interface DeclaredElement<P extends ElementProperties> {
|
|
19
|
+
new (): AsterElement & ElementValues<P>;
|
|
20
|
+
readonly tag: string;
|
|
21
|
+
readonly properties: P;
|
|
22
|
+
readonly contract: ElementContract<P>;
|
|
23
|
+
}
|
|
24
|
+
/** Declare a validated property, its per-instance default and its documentation once. */
|
|
25
|
+
export declare function elementProperty<const C extends CompositeType<unknown>>(type: C, initial: () => C extends CompositeType<infer T> ? T : never, description: string): {
|
|
26
|
+
readonly type: C;
|
|
27
|
+
readonly initial: typeof initial;
|
|
28
|
+
readonly description: string;
|
|
29
|
+
};
|
|
30
|
+
/** Define a light-DOM element with reactive accessors and an inferred YAML contract. */
|
|
31
|
+
export declare function defineElement<const P extends ElementProperties>(spec: {
|
|
32
|
+
readonly tag: string;
|
|
33
|
+
readonly properties: P;
|
|
34
|
+
readonly render: (props: Readonly<ElementValues<P>>, element: AsterElement & ElementValues<P>) => View;
|
|
35
|
+
}): DeclaredElement<P>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { signal } from "@preact/signals-core";
|
|
2
|
+
import { AsterElement, define } from "../base/element.js";
|
|
3
|
+
/** Declare a validated property, its per-instance default and its documentation once. */
|
|
4
|
+
export function elementProperty(type, initial, description) {
|
|
5
|
+
return Object.freeze({ type: Object.freeze({ ...type }), initial, description });
|
|
6
|
+
}
|
|
7
|
+
/** Define a light-DOM element with reactive accessors and an inferred YAML contract. */
|
|
8
|
+
export function defineElement(spec) {
|
|
9
|
+
const { tag, render } = spec;
|
|
10
|
+
if (customElements.get(tag))
|
|
11
|
+
throw new Error(`Element '${tag}' is already defined`);
|
|
12
|
+
const properties = Object.freeze(Object.fromEntries(Object.entries(spec.properties).map(([key, property]) => {
|
|
13
|
+
// Native DOM capabilities and lifecycle hooks cannot become YAML inputs.
|
|
14
|
+
if (key in AsterElement.prototype || ["__proto__", "prototype", "constructor"].includes(key))
|
|
15
|
+
throw new Error(`Reserved element property '${key}'`);
|
|
16
|
+
return [key, Object.freeze({ ...property, type: Object.freeze({ ...property.type }) })];
|
|
17
|
+
})));
|
|
18
|
+
const contract = Object.freeze(Object.fromEntries(Object.entries(properties).map(([key, property]) => [key,
|
|
19
|
+
Object.freeze({ ...property.type, optional: true, description: property.description }),
|
|
20
|
+
])));
|
|
21
|
+
class Element extends AsterElement {
|
|
22
|
+
static tag = tag;
|
|
23
|
+
static properties = properties;
|
|
24
|
+
static contract = contract;
|
|
25
|
+
constructor() {
|
|
26
|
+
super();
|
|
27
|
+
for (const [key, property] of Object.entries(properties)) {
|
|
28
|
+
// Upgrade pre-definition assignments before installing our own accessors.
|
|
29
|
+
const initial = Object.hasOwn(this, key) ? Reflect.get(this, key) : property.initial();
|
|
30
|
+
const validate = (value) => {
|
|
31
|
+
if (!property.type.validate(value))
|
|
32
|
+
throw new TypeError(`${tag}.${key}: expected ${property.type.description ?? property.description}`);
|
|
33
|
+
};
|
|
34
|
+
validate(initial);
|
|
35
|
+
const state = signal(initial);
|
|
36
|
+
Object.defineProperty(this, key, {
|
|
37
|
+
enumerable: true, configurable: false,
|
|
38
|
+
get: () => state.value,
|
|
39
|
+
set: (value) => { validate(value); state.value = value; },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
render() {
|
|
44
|
+
const element = this;
|
|
45
|
+
return render(element, element);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Accessors belong to each newly constructed instance; existing prototypes are untouched.
|
|
49
|
+
define(tag, Element);
|
|
50
|
+
return Element;
|
|
51
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Command, Lifetime, PageScope, Publication, SessionActivity } from "@getaster/data";
|
|
2
|
+
export type CompositeValues = Readonly<Record<string, unknown>>;
|
|
3
|
+
export interface CompositeProperty {
|
|
4
|
+
readonly validate: (value: unknown) => boolean;
|
|
5
|
+
readonly optional?: boolean;
|
|
6
|
+
readonly description?: string;
|
|
7
|
+
readonly schema?: Readonly<Record<string, unknown>>;
|
|
8
|
+
}
|
|
9
|
+
export type CompositeContract = Readonly<Record<string, CompositeProperty>>;
|
|
10
|
+
export declare function snapshotContract<C extends CompositeContract>(contract: C): C;
|
|
11
|
+
export declare function checkContract(values: CompositeValues, contract: CompositeContract, path: string): void;
|
|
12
|
+
export interface CompositeModel {
|
|
13
|
+
readonly values: CompositeValues;
|
|
14
|
+
readonly publications?: Readonly<Record<string, Publication>>;
|
|
15
|
+
readonly commands?: Readonly<Record<string, Command>>;
|
|
16
|
+
}
|
|
17
|
+
export interface CompositeModelType {
|
|
18
|
+
readonly inputs: CompositeContract;
|
|
19
|
+
readonly exports: CompositeContract;
|
|
20
|
+
readonly commands?: CompositeContract;
|
|
21
|
+
readonly publications?: CompositeContract;
|
|
22
|
+
/** Register owned work with lifetime; inputs are borrowed. */
|
|
23
|
+
readonly create: (inputs: CompositeValues, lifetime: Lifetime, activity?: SessionActivity) => CompositeModel;
|
|
24
|
+
}
|
|
25
|
+
export interface CompositeMount {
|
|
26
|
+
readonly activity?: SessionActivity;
|
|
27
|
+
readonly props: CompositeValues;
|
|
28
|
+
readonly lifetime: Lifetime;
|
|
29
|
+
readonly scope: PageScope;
|
|
30
|
+
}
|
|
31
|
+
export interface CompositeSession {
|
|
32
|
+
readonly setActive?: (active: boolean) => void;
|
|
33
|
+
readonly canClose?: (signal: AbortSignal) => boolean | Promise<boolean>;
|
|
34
|
+
readonly element: HTMLElement;
|
|
35
|
+
readonly dispose?: () => void;
|
|
36
|
+
/** A nested composite's local namespace, preserved when it is placed. */
|
|
37
|
+
readonly scope?: PageScope;
|
|
38
|
+
}
|
|
39
|
+
export type CompositeFactory = (mount: CompositeMount) => CompositeSession | Promise<CompositeSession>;
|
|
40
|
+
export interface CompositeComponentType {
|
|
41
|
+
/** Only these inputs may change while a keyed session retains its identity. */
|
|
42
|
+
readonly mutableInputs?: readonly string[];
|
|
43
|
+
readonly props: CompositeContract;
|
|
44
|
+
readonly create?: CompositeFactory;
|
|
45
|
+
readonly load?: () => Promise<CompositeFactory>;
|
|
46
|
+
/** Custom composites can consume props themselves; ordinary elements receive property bindings. */
|
|
47
|
+
readonly apply?: (element: HTMLElement, props: CompositeValues) => void;
|
|
48
|
+
}
|
|
49
|
+
export interface CompositeRenderer {
|
|
50
|
+
readonly render: (...args: never[]) => unknown;
|
|
51
|
+
readonly accepts: (property: string) => boolean;
|
|
52
|
+
readonly properties?: readonly string[];
|
|
53
|
+
}
|
|
54
|
+
/** Runtime lookup capabilities; registration is complete before a catalog is exposed. */
|
|
55
|
+
export interface CompositeResolver<C extends CompositeContract = CompositeContract> {
|
|
56
|
+
readonly inputs: C | undefined;
|
|
57
|
+
model(name: string): CompositeModelType;
|
|
58
|
+
component(name: string): CompositeComponentType;
|
|
59
|
+
renderer(name: string): CompositeRenderer;
|
|
60
|
+
load(name: string): Promise<CompositeFactory>;
|
|
61
|
+
}
|
|
62
|
+
/** A host-owned allowlist; definitions can use only these capabilities. */
|
|
63
|
+
export declare class CompositeRegistry {
|
|
64
|
+
#private;
|
|
65
|
+
get inputs(): CompositeContract | undefined;
|
|
66
|
+
/** Seal registrations and optionally carry a host input contract into mounting. */
|
|
67
|
+
seal(): CompositeResolver;
|
|
68
|
+
seal<const C extends CompositeContract>(inputs: C): CompositeResolver<C>;
|
|
69
|
+
model(name: string, entry?: CompositeModelType): CompositeModelType;
|
|
70
|
+
component(name: string, entry?: CompositeComponentType): CompositeComponentType;
|
|
71
|
+
renderer(name: string, entry?: CompositeRenderer): CompositeRenderer;
|
|
72
|
+
load(name: string): Promise<CompositeFactory>;
|
|
73
|
+
}
|
|
74
|
+
/** Adapter for existing public custom-element property APIs. */
|
|
75
|
+
export declare function compositeElement<const C extends CompositeContract>(element: {
|
|
76
|
+
readonly tag: string;
|
|
77
|
+
readonly contract: C;
|
|
78
|
+
}): Omit<CompositeComponentType, "props"> & {
|
|
79
|
+
readonly props: C;
|
|
80
|
+
};
|
|
81
|
+
export declare function compositeElement<const C extends CompositeContract>(tag: string, props: C): Omit<CompositeComponentType, "props"> & {
|
|
82
|
+
readonly props: C;
|
|
83
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { CompositeError } from "./definition.js";
|
|
2
|
+
export function snapshotContract(contract) {
|
|
3
|
+
const freeze = (value) => {
|
|
4
|
+
if (!value || typeof value !== "object")
|
|
5
|
+
return;
|
|
6
|
+
Object.values(value).forEach(freeze);
|
|
7
|
+
Object.freeze(value);
|
|
8
|
+
};
|
|
9
|
+
return Object.freeze(Object.fromEntries(Object.entries(contract).map(([key, field]) => {
|
|
10
|
+
const schema = structuredClone(field.schema ?? {});
|
|
11
|
+
freeze(schema);
|
|
12
|
+
return [key, Object.freeze({ ...field, schema })];
|
|
13
|
+
})));
|
|
14
|
+
}
|
|
15
|
+
export function checkContract(values, contract, path) {
|
|
16
|
+
for (const key of Object.keys(values))
|
|
17
|
+
if (!Object.hasOwn(contract, key))
|
|
18
|
+
throw new CompositeError(`${path}.${key}`, "unknown input/property");
|
|
19
|
+
for (const [key, spec] of Object.entries(contract)) {
|
|
20
|
+
if (!Object.hasOwn(values, key) && spec.optional)
|
|
21
|
+
continue;
|
|
22
|
+
if (!spec.validate(values[key]))
|
|
23
|
+
throw new CompositeError(`${path}.${key}`, spec.description ? `expected ${spec.description}` : "value does not match the registered contract");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** A host-owned allowlist; definitions can use only these capabilities. */
|
|
27
|
+
export class CompositeRegistry {
|
|
28
|
+
#models = new Map();
|
|
29
|
+
#components = new Map();
|
|
30
|
+
#renderers = new Map();
|
|
31
|
+
#loads = new Map();
|
|
32
|
+
#sealed = false;
|
|
33
|
+
#inputs;
|
|
34
|
+
get inputs() { return this.#inputs; }
|
|
35
|
+
seal(inputs) {
|
|
36
|
+
if (this.#sealed)
|
|
37
|
+
throw new Error("Registry is already sealed");
|
|
38
|
+
this.#inputs = inputs ? snapshotContract(inputs) : undefined;
|
|
39
|
+
this.#sealed = true;
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
model(name, entry) {
|
|
43
|
+
if (entry)
|
|
44
|
+
this.#add(this.#models, name, entry);
|
|
45
|
+
const value = this.#models.get(name);
|
|
46
|
+
if (!value)
|
|
47
|
+
throw new Error(`Unknown model '${name}'`);
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
component(name, entry) {
|
|
51
|
+
if (entry) {
|
|
52
|
+
if (!!entry.create === !!entry.load)
|
|
53
|
+
throw new Error(`Component '${name}' needs exactly one create or load factory`);
|
|
54
|
+
this.#add(this.#components, name, entry);
|
|
55
|
+
}
|
|
56
|
+
const value = this.#components.get(name);
|
|
57
|
+
if (!value)
|
|
58
|
+
throw new Error(`Unknown component '${name}'`);
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
renderer(name, entry) {
|
|
62
|
+
if (entry)
|
|
63
|
+
this.#add(this.#renderers, name, entry);
|
|
64
|
+
const value = this.#renderers.get(name);
|
|
65
|
+
if (!value)
|
|
66
|
+
throw new Error(`Unknown renderer '${name}'`);
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
load(name) {
|
|
70
|
+
const entry = this.component(name);
|
|
71
|
+
if (entry.create)
|
|
72
|
+
return Promise.resolve(entry.create);
|
|
73
|
+
let pending = this.#loads.get(name);
|
|
74
|
+
if (!pending) {
|
|
75
|
+
pending = Promise.resolve().then(() => entry.load()).catch(error => { this.#loads.delete(name); throw error; });
|
|
76
|
+
this.#loads.set(name, pending);
|
|
77
|
+
}
|
|
78
|
+
return pending;
|
|
79
|
+
}
|
|
80
|
+
#add(map, name, entry) {
|
|
81
|
+
if (this.#sealed)
|
|
82
|
+
throw new Error("Registry is sealed; declare capabilities in the catalog before creating it");
|
|
83
|
+
if (map.has(name))
|
|
84
|
+
throw new Error(`Duplicate registration '${name}'`);
|
|
85
|
+
map.set(name, entry);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export function compositeElement(element, contract) {
|
|
89
|
+
const tag = typeof element === "string" ? element : element.tag;
|
|
90
|
+
const props = typeof element === "string" ? contract : element.contract;
|
|
91
|
+
return {
|
|
92
|
+
props,
|
|
93
|
+
create: () => ({ element: document.createElement(tag) }),
|
|
94
|
+
apply: (element, values) => {
|
|
95
|
+
for (const [key, value] of Object.entries(values))
|
|
96
|
+
Reflect.set(element, key, value);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CompositeProperty } from "./registry.ts";
|
|
2
|
+
export type Schema = Readonly<Record<string, unknown>>;
|
|
3
|
+
export declare function asSchema(value: unknown): Schema;
|
|
4
|
+
export declare function schemaProperty(schema: Schema): CompositeProperty;
|
|
5
|
+
export declare function matchesSchema(value: unknown, schema: Schema): boolean;
|
|
6
|
+
/** Unknown/custom refinements are runtime checks; metadata cannot prove their assignability. */
|
|
7
|
+
export declare function compatibility(source: Schema, target: Schema): boolean | undefined;
|
|
8
|
+
/** JSON cannot express instance identity, signals or undefined as literal values. */
|
|
9
|
+
export declare function literalSchema(schema: Schema): Schema;
|