@yahoo/uds-create-config 4.6.0 → 4.8.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/configs/CanvasConfig.d.ts +900 -80
- package/dist/configs/react-native-system.d.ts +603 -55
- package/dist/configs/system.d.ts +603 -55
- package/dist/configs/system.js +7 -1
- package/dist/entities/system/Component.d.ts +480 -39
- package/dist/entities/system/Component.js +264 -93
- package/dist/entities/system/File.d.ts +1 -1
- package/dist/entities/system/LinkedSystem.d.ts +2 -2
- package/dist/entities/system/Operation.d.ts +1 -1
- package/dist/entities/system/StyleProperty.d.ts +8 -0
- package/dist/entities/system/StyleProperty.js +12 -1
- package/dist/entities/system/Tool.d.ts +1 -1
- package/dist/entities/system/component-module.js +1 -1
- package/dist/entities/system/defineComponent.d.ts +45 -7
- package/dist/entities/system/defineComponent.js +52 -25
- package/dist/framework/Config.d.ts +2 -2
- package/dist/framework/Config.js +59 -7
- package/dist/framework/class-names.d.ts +4 -2
- package/dist/framework/class-names.js +56 -17
- package/dist/framework/defineDerivedEntity.d.ts +1 -1
- package/dist/framework/defineEntity.d.ts +2 -2
- package/dist/framework/defineSubEntity.d.ts +3 -2
- package/dist/framework/preview-store.d.ts +19 -0
- package/dist/framework/preview-store.js +18 -0
- package/dist/framework/projections.d.ts +28 -8
- package/dist/framework/projections.js +50 -16
- package/dist/framework/prop-surface.d.ts +21 -1
- package/dist/framework/prop-surface.js +99 -21
- package/dist/framework/render-spec.d.ts +103 -3
- package/dist/framework/render-spec.js +285 -37
- package/dist/framework/schema-version.d.ts +1 -1
- package/dist/framework/schema-version.js +5 -5
- package/dist/framework/utils/refs.js +10 -1
- package/dist/framework/validate-spec.d.ts +2 -0
- package/dist/framework/validate-spec.js +13 -0
- package/dist/index.d.ts +14 -11
- package/dist/index.js +6 -5
- package/dist/migrations/2.0.0/v1-artifact.d.ts +305 -29
- package/dist/migrations/20260919051027_prop_conditions_and_anatomy_positions_at_rest.d.ts +22 -0
- package/dist/migrations/20260919051027_prop_conditions_and_anatomy_positions_at_rest.js +10 -0
- package/dist/migrations/20260920004019_preview_conditions_keyed.d.ts +27 -0
- package/dist/migrations/20260920004019_preview_conditions_keyed.js +148 -0
- package/dist/migrations/20260920161309_prefixed_token_group.d.ts +12 -0
- package/dist/migrations/20260920161309_prefixed_token_group.js +10 -0
- package/dist/renderer/wrapRegistry.js +8 -2
- package/dist/renderer/wrappers/component-refs.d.ts +14 -0
- package/dist/renderer/wrappers/component-refs.js +79 -0
- package/dist/renderer/wrappers/hex-normalize.d.ts +5 -7
- package/dist/renderer/wrappers/hex-normalize.js +11 -10
- package/dist/spec/cell.d.ts +171 -0
- package/dist/spec/cell.js +361 -0
- package/dist/spec/detach.d.ts +125 -0
- package/dist/spec/detach.js +511 -0
- package/dist/spec/empty-node-slots.d.ts +11 -23
- package/dist/spec/empty-node-slots.js +12 -39
- package/dist/spec/index.d.ts +5 -3
- package/dist/spec/index.js +3 -3
- package/dist/spec/instance.d.ts +77 -0
- package/dist/spec/instance.js +80 -0
- package/dist/spec/preview-content.d.ts +95 -0
- package/dist/spec/preview-content.js +149 -0
- package/dist/spec/preview-elements.d.ts +54 -15
- package/dist/spec/preview-elements.js +109 -29
- package/dist/spec/prop-conditions.d.ts +28 -0
- package/dist/spec/prop-conditions.js +39 -0
- package/dist/spec/slot.d.ts +206 -0
- package/dist/spec/slot.js +542 -0
- package/dist/spec/visibility.d.ts +82 -0
- package/dist/spec/visibility.js +109 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +18 -2
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import { canonicalCondition, isRef, kindOf, pathOf, splitRef } from "../framework/utils/refs.js";
|
|
2
|
+
import { rootLayerOf } from "../entities/system/element.js";
|
|
3
|
+
import { createSliceMemo } from "../framework/memo.js";
|
|
4
|
+
import "../framework/preview-store.js";
|
|
5
|
+
import { resolveComponentProps } from "../framework/projections.js";
|
|
6
|
+
import { routesIntoText } from "../framework/prop-surface.js";
|
|
7
|
+
import { isPreviewElement } from "./preview-elements.js";
|
|
8
|
+
import { slotTargetsOf } from "../framework/render-spec.js";
|
|
9
|
+
import { previewConditionAt, previewContentList, previewValueSource } from "./preview-content.js";
|
|
10
|
+
//#region src/spec/slot.ts
|
|
11
|
+
/**
|
|
12
|
+
* A slot of a component: one prop that content is placed in, as the config states it.
|
|
13
|
+
*
|
|
14
|
+
* A `Slot` is a value over one config, so every question about a slot has one owner: where its
|
|
15
|
+
* content lands (`layer`, `into`), what it takes (`accepts`, `takes`), what a cell shows in it
|
|
16
|
+
* (`content`, `source`) and how that content changes (`append`, `insertAt`, `replaceAt`, `removeAt`,
|
|
17
|
+
* `set`).
|
|
18
|
+
* Its content is a list of preview elements stored on the `preview` entry the cell reads it from;
|
|
19
|
+
* a bare element stored on the prop reads as a list of one.
|
|
20
|
+
*
|
|
21
|
+
* The list of a component's slots is cached per component slice, the way every component
|
|
22
|
+
* projection is, so a surface may ask per render. `Component.slots`, `Component.slot`,
|
|
23
|
+
* `Component.slotHostedBy` and `Component.namedSlots` are the door; the functions below back them.
|
|
24
|
+
*/
|
|
25
|
+
const factsCache = createSliceMemo({
|
|
26
|
+
kinds: ["component"],
|
|
27
|
+
compute: (_config) => /* @__PURE__ */ new Map()
|
|
28
|
+
});
|
|
29
|
+
const listsByConfig = /* @__PURE__ */ new WeakMap();
|
|
30
|
+
var Slot = class {
|
|
31
|
+
/** The component that declares the slot. */
|
|
32
|
+
component;
|
|
33
|
+
/** The prop the content arrives as on the instance (`children`, `startIcon`). */
|
|
34
|
+
from;
|
|
35
|
+
/** The layer that holds the content. */
|
|
36
|
+
layer;
|
|
37
|
+
/** The prop the content lands under on the layer's element. */
|
|
38
|
+
into;
|
|
39
|
+
/** The refs the slot accepts (`component:X`, `icon:phosphor`); empty when unconstrained. */
|
|
40
|
+
accepts;
|
|
41
|
+
/** Built by {@link listSlots}; a surface reaches one through `Component.slots` or `Component.slot`. */
|
|
42
|
+
constructor(config, component, facts) {
|
|
43
|
+
this.config = config;
|
|
44
|
+
this.component = component;
|
|
45
|
+
this.from = facts.from;
|
|
46
|
+
this.layer = facts.layer;
|
|
47
|
+
this.into = facts.into;
|
|
48
|
+
this.accepts = facts.accepts;
|
|
49
|
+
}
|
|
50
|
+
/** Whether this is the component's `children`, positioned inside one of its layers. */
|
|
51
|
+
get isChildren() {
|
|
52
|
+
return this.from === "children";
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Whether the slot takes an element: anything when it accepts nothing in particular, else an
|
|
56
|
+
* element whose ref one of the accepted refs names, exactly or as a member of a group the ref
|
|
57
|
+
* names (`icon:phosphor` takes `icon:phosphor/StarIcon`). A `@source` marker on either side is
|
|
58
|
+
* not part of the name. `element` is a spec type, a bare component path, or the ref a preview
|
|
59
|
+
* element stores.
|
|
60
|
+
*/
|
|
61
|
+
takes(element) {
|
|
62
|
+
if (this.accepts.length === 0) return true;
|
|
63
|
+
const named = elementRefOf(this.config, element);
|
|
64
|
+
if (!named) return false;
|
|
65
|
+
const kind = kindOf(named);
|
|
66
|
+
const elementPath = pathOf(named);
|
|
67
|
+
return this.accepts.some((accepted) => {
|
|
68
|
+
if (kindOf(accepted) !== kind) return false;
|
|
69
|
+
const acceptedPath = pathOf(accepted);
|
|
70
|
+
if (acceptedPath === elementPath) return true;
|
|
71
|
+
return !(this.config.resolve(kind, acceptedPath) !== void 0) && elementPath.startsWith(`${acceptedPath}/`);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** The stored field a cell reads the slot's content from, or `undefined` when nothing is stored. */
|
|
75
|
+
source(cell) {
|
|
76
|
+
return previewValueSource({
|
|
77
|
+
config: this.config,
|
|
78
|
+
path: this.component,
|
|
79
|
+
cell,
|
|
80
|
+
prop: this.from
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** The elements a cell shows in the slot, in order. */
|
|
84
|
+
content(cell) {
|
|
85
|
+
return previewContentList(this.stored(this.source(cell)));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Append `element` to the content under `cell`: the entry keyed on exactly that coordinate, `*`
|
|
89
|
+
* for none, created where none spells it yet and seeded with the list the coordinate shows, so the
|
|
90
|
+
* cells it covers gain the element and every other cell keeps what it had.
|
|
91
|
+
*/
|
|
92
|
+
append({ cell, element }) {
|
|
93
|
+
return this.set({
|
|
94
|
+
cell,
|
|
95
|
+
elements: [...this.content(cell), element]
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The content shown under `cell` with `element` placed at `address`: at `index` of the list when
|
|
100
|
+
* the path is empty, past the end appending; otherwise inside the element at `index`, where a
|
|
101
|
+
* number step is the position among that element's children and a string step the element-valued
|
|
102
|
+
* prop it becomes. `undefined` when the address names another slot or a path that reaches nothing.
|
|
103
|
+
* What a surface paints while a drop is still being decided, and what {@link insertAt} writes.
|
|
104
|
+
*/
|
|
105
|
+
placed({ cell, address, element }) {
|
|
106
|
+
if (address.prop !== this.from) return void 0;
|
|
107
|
+
return placeIn(this.content(cell), address, element);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The element at `from` moved to `to`, both in this slot's content under `cell`, on the entry the
|
|
111
|
+
* cell reads the content from. `to` names a position in the content with the moved element
|
|
112
|
+
* already taken out, the way a drop between the remaining elements resolves it. Empty when either
|
|
113
|
+
* address names nothing, or `to` reaches inside the moved element.
|
|
114
|
+
*/
|
|
115
|
+
moveAt({ cell, from, to }) {
|
|
116
|
+
if (from.prop !== this.from || to.prop !== this.from) return [];
|
|
117
|
+
if (reachesInside(from, to)) return [];
|
|
118
|
+
const source = this.source(cell);
|
|
119
|
+
if (!source) return [];
|
|
120
|
+
const list = previewContentList(this.stored(source));
|
|
121
|
+
const element = elementIn(list, from);
|
|
122
|
+
if (!element) return [];
|
|
123
|
+
const without = removeIn(list, from);
|
|
124
|
+
if (!without) return [];
|
|
125
|
+
const content = placeIn(without, to, element);
|
|
126
|
+
if (!content) return [];
|
|
127
|
+
return this.writeSource({
|
|
128
|
+
source,
|
|
129
|
+
content
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/** The element at `address` in the content shown under `cell`, or `undefined` when it names nothing. */
|
|
133
|
+
elementAt({ cell, address }) {
|
|
134
|
+
if (address.prop !== this.from) return void 0;
|
|
135
|
+
return elementIn(this.content(cell), address);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* A copy of the element at `address`, placed right after it: the next index of the list, or the
|
|
139
|
+
* next position among its parent's children. Written on the entry the cell reads the content
|
|
140
|
+
* from, like {@link replaceAt}, so a copy made in a conditional cell of `*` content is every
|
|
141
|
+
* cell's. An element held as a prop has no "after", so it is not duplicated. Empty when the
|
|
142
|
+
* address names nothing.
|
|
143
|
+
*/
|
|
144
|
+
duplicateAt({ cell, address }) {
|
|
145
|
+
if (address.prop !== this.from) return [];
|
|
146
|
+
const last = address.path[address.path.length - 1];
|
|
147
|
+
if (typeof last === "string") return [];
|
|
148
|
+
const source = this.source(cell);
|
|
149
|
+
if (!source) return [];
|
|
150
|
+
const list = previewContentList(this.stored(source));
|
|
151
|
+
const element = elementIn(list, address);
|
|
152
|
+
if (!element) return [];
|
|
153
|
+
const content = placeIn(list, last === void 0 ? {
|
|
154
|
+
...address,
|
|
155
|
+
index: address.index + 1
|
|
156
|
+
} : {
|
|
157
|
+
...address,
|
|
158
|
+
path: [...address.path.slice(0, -1), last + 1]
|
|
159
|
+
}, structuredClone(element));
|
|
160
|
+
if (!content) return [];
|
|
161
|
+
return this.writeSource({
|
|
162
|
+
source,
|
|
163
|
+
content
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/** Write the content {@link placed} gives, onto the cell's own entry the way `append` writes.
|
|
167
|
+
* Empty when nothing is placed. */
|
|
168
|
+
insertAt({ cell, address, element }) {
|
|
169
|
+
const content = this.placed({
|
|
170
|
+
cell,
|
|
171
|
+
address,
|
|
172
|
+
element
|
|
173
|
+
});
|
|
174
|
+
return content ? this.set({
|
|
175
|
+
cell,
|
|
176
|
+
elements: content
|
|
177
|
+
}) : [];
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Swap the element at `address` for `element`, on the entry the cell reads the content from —
|
|
181
|
+
* `*` included, so a `*` value shown in a conditional cell changes for every cell rather than
|
|
182
|
+
* being copied into a new keyed entry. Empty when the address names nothing.
|
|
183
|
+
*/
|
|
184
|
+
replaceAt({ cell, address, element }) {
|
|
185
|
+
return this.rewrite({
|
|
186
|
+
cell,
|
|
187
|
+
address,
|
|
188
|
+
next: element
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Take the element at `address` out, on the entry the cell reads the content from. A list emptied
|
|
193
|
+
* this way clears the prop, and an entry left with no props goes with it. Empty when the address
|
|
194
|
+
* names nothing.
|
|
195
|
+
*/
|
|
196
|
+
removeAt({ cell, address }) {
|
|
197
|
+
return this.rewrite({
|
|
198
|
+
cell,
|
|
199
|
+
address,
|
|
200
|
+
next: null
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Store `elements` as the whole content under `cell`: one `sub-update` on the `preview` entry
|
|
205
|
+
* keyed on the coordinate, `*` for none, created where none spells it and joined where one does.
|
|
206
|
+
* What a picker that shows the whole list writes; the verbs above build on it.
|
|
207
|
+
*/
|
|
208
|
+
set({ cell, elements }) {
|
|
209
|
+
const conditional = Object.keys(cell).length > 0;
|
|
210
|
+
const when = conditional ? { ...cell } : null;
|
|
211
|
+
const exact = conditional ? previewConditionAt({
|
|
212
|
+
config: this.config,
|
|
213
|
+
path: this.component,
|
|
214
|
+
cell
|
|
215
|
+
}) : void 0;
|
|
216
|
+
let key = "*";
|
|
217
|
+
if (exact) key = exact.key;
|
|
218
|
+
else if (when) key = canonicalCondition(when);
|
|
219
|
+
return [{
|
|
220
|
+
kind: "component",
|
|
221
|
+
operation: "sub-update",
|
|
222
|
+
path: this.component,
|
|
223
|
+
field: "preview",
|
|
224
|
+
key,
|
|
225
|
+
data: {
|
|
226
|
+
...exact ? {} : { when },
|
|
227
|
+
[`props.${this.from}`]: storedContent(elements)
|
|
228
|
+
}
|
|
229
|
+
}];
|
|
230
|
+
}
|
|
231
|
+
stored(source) {
|
|
232
|
+
if (!source) return void 0;
|
|
233
|
+
const body = this.config.resolve("component", this.component)?.body;
|
|
234
|
+
if (source.kind === "defaultProps") return body?.defaultProps?.[this.from];
|
|
235
|
+
return body?.preview?.[source.key]?.props?.[this.from];
|
|
236
|
+
}
|
|
237
|
+
/** `replaceAt` and `removeAt`: one element rewritten or dropped, written back where it was read. */
|
|
238
|
+
rewrite({ cell, address, next }) {
|
|
239
|
+
if (address.prop !== this.from) return [];
|
|
240
|
+
const source = this.source(cell);
|
|
241
|
+
if (!source) return [];
|
|
242
|
+
const list = previewContentList(this.stored(source));
|
|
243
|
+
const target = list[address.index];
|
|
244
|
+
if (!target) return [];
|
|
245
|
+
const rewritten = rewriteAt(target, address.path, next);
|
|
246
|
+
if (rewritten === void 0) return [];
|
|
247
|
+
const content = [...list];
|
|
248
|
+
if (rewritten === null) content.splice(address.index, 1);
|
|
249
|
+
else content[address.index] = rewritten;
|
|
250
|
+
return this.writeSource({
|
|
251
|
+
source,
|
|
252
|
+
content
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
/** The edits that store `content` on `source`: the field the content came from, cleared when the
|
|
256
|
+
* list is empty, and the entry it leaves with nothing else going with it. */
|
|
257
|
+
writeSource({ source, content }) {
|
|
258
|
+
const value = content.length === 0 ? null : storedContent(content);
|
|
259
|
+
if (source.kind === "defaultProps") return [{
|
|
260
|
+
kind: "component",
|
|
261
|
+
operation: "update",
|
|
262
|
+
path: this.component,
|
|
263
|
+
data: { [`defaultProps.${this.from}`]: value }
|
|
264
|
+
}];
|
|
265
|
+
const body = this.config.resolve("component", this.component)?.body;
|
|
266
|
+
const remaining = Object.keys(body?.preview?.[source.key]?.props ?? {}).filter((name) => name !== this.from);
|
|
267
|
+
if (value === null && remaining.length === 0) return [{
|
|
268
|
+
kind: "component",
|
|
269
|
+
operation: "sub-delete",
|
|
270
|
+
path: this.component,
|
|
271
|
+
field: "preview",
|
|
272
|
+
key: source.key
|
|
273
|
+
}];
|
|
274
|
+
return [{
|
|
275
|
+
kind: "component",
|
|
276
|
+
operation: "sub-update",
|
|
277
|
+
path: this.component,
|
|
278
|
+
field: "preview",
|
|
279
|
+
key: source.key,
|
|
280
|
+
data: { [`props.${this.from}`]: value }
|
|
281
|
+
}];
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
/**
|
|
285
|
+
* Every slot of a component that content can be placed in: its named node slots, and its `children`
|
|
286
|
+
* when a layer of several positions them. A text slot is typed in place rather than filled, so it is
|
|
287
|
+
* not one; nor is `children` landing on the root, which is the open hole every container has. The
|
|
288
|
+
* same array comes back for the same config.
|
|
289
|
+
*/
|
|
290
|
+
function listSlots({ config, path }) {
|
|
291
|
+
let byPath = listsByConfig.get(config);
|
|
292
|
+
if (!byPath) {
|
|
293
|
+
byPath = /* @__PURE__ */ new Map();
|
|
294
|
+
listsByConfig.set(config, byPath);
|
|
295
|
+
}
|
|
296
|
+
const hit = byPath.get(path);
|
|
297
|
+
if (hit) return hit;
|
|
298
|
+
const slots = slotFacts(config, path).map((facts) => new Slot(config, path, facts));
|
|
299
|
+
byPath.set(path, slots);
|
|
300
|
+
return slots;
|
|
301
|
+
}
|
|
302
|
+
/** The named node slots alone: what a design node's render injects an empty placeholder for. */
|
|
303
|
+
function namedSlots({ config, path }) {
|
|
304
|
+
return listSlots({
|
|
305
|
+
config,
|
|
306
|
+
path
|
|
307
|
+
}).filter((slot) => !slot.isChildren);
|
|
308
|
+
}
|
|
309
|
+
/** The slot a prop names, or `undefined` when the prop is not a slot content is placed in. */
|
|
310
|
+
function slotOf({ config, path, slot }) {
|
|
311
|
+
return listSlots({
|
|
312
|
+
config,
|
|
313
|
+
path
|
|
314
|
+
}).find((entry) => entry.from === slot);
|
|
315
|
+
}
|
|
316
|
+
/** The slot whose content lands in `layer`, or `undefined` when the layer hosts none. */
|
|
317
|
+
function slotHostedBy({ config, path, layer }) {
|
|
318
|
+
return listSlots({
|
|
319
|
+
config,
|
|
320
|
+
path
|
|
321
|
+
}).find((entry) => entry.layer === layer);
|
|
322
|
+
}
|
|
323
|
+
/** A list stored back the way content is authored: one element bare, several as the list. */
|
|
324
|
+
function storedContent(list) {
|
|
325
|
+
return list.length === 1 ? list[0] : [...list];
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* `root` with the element at `steps` replaced by `next`, or removed when `next` is `null`;
|
|
329
|
+
* `undefined` when a step names nothing. A number steps into a child, a string into an
|
|
330
|
+
* element-valued prop.
|
|
331
|
+
*/
|
|
332
|
+
function rewriteAt(root, steps, next) {
|
|
333
|
+
const [step, ...rest] = steps;
|
|
334
|
+
if (step === void 0) return next;
|
|
335
|
+
if (typeof step === "number") {
|
|
336
|
+
const child = root.children?.[step];
|
|
337
|
+
if (!child) return void 0;
|
|
338
|
+
const replaced = rewriteAt(child, rest, next);
|
|
339
|
+
if (replaced === void 0) return void 0;
|
|
340
|
+
const children = [...root.children ?? []];
|
|
341
|
+
if (replaced === null) children.splice(step, 1);
|
|
342
|
+
else children[step] = replaced;
|
|
343
|
+
const out = {
|
|
344
|
+
...root,
|
|
345
|
+
children
|
|
346
|
+
};
|
|
347
|
+
if (children.length === 0) delete out.children;
|
|
348
|
+
return out;
|
|
349
|
+
}
|
|
350
|
+
const held = root.props?.[step];
|
|
351
|
+
if (!isPreviewElement(held)) return void 0;
|
|
352
|
+
const replaced = rewriteAt(held, rest, next);
|
|
353
|
+
if (replaced === void 0) return void 0;
|
|
354
|
+
const props = { ...root.props };
|
|
355
|
+
if (replaced === null) delete props[step];
|
|
356
|
+
else props[step] = replaced;
|
|
357
|
+
const out = {
|
|
358
|
+
...root,
|
|
359
|
+
props
|
|
360
|
+
};
|
|
361
|
+
if (Object.keys(props).length === 0) delete out.props;
|
|
362
|
+
return out;
|
|
363
|
+
}
|
|
364
|
+
/** The element `address` names in `content`, or `undefined`. */
|
|
365
|
+
function elementIn(content, address) {
|
|
366
|
+
let current = content[address.index];
|
|
367
|
+
for (const step of address.path) {
|
|
368
|
+
if (!current) return void 0;
|
|
369
|
+
const next = typeof step === "number" ? current.children?.[step] : current.props?.[step];
|
|
370
|
+
current = isPreviewElement(next) ? next : void 0;
|
|
371
|
+
}
|
|
372
|
+
return current;
|
|
373
|
+
}
|
|
374
|
+
/** `content` with `element` placed at `address`; `undefined` when a step reaches nothing. */
|
|
375
|
+
function placeIn(content, address, element) {
|
|
376
|
+
const next = [...content];
|
|
377
|
+
if (address.path.length === 0) {
|
|
378
|
+
const index = Math.min(Math.max(address.index, 0), next.length);
|
|
379
|
+
next.splice(index, 0, element);
|
|
380
|
+
return next;
|
|
381
|
+
}
|
|
382
|
+
const target = next[address.index];
|
|
383
|
+
if (!target) return void 0;
|
|
384
|
+
const inner = insertInto(target, address.path, element);
|
|
385
|
+
if (inner === void 0) return void 0;
|
|
386
|
+
next[address.index] = inner;
|
|
387
|
+
return next;
|
|
388
|
+
}
|
|
389
|
+
/** `content` without the element at `address`; `undefined` when it names nothing. */
|
|
390
|
+
function removeIn(content, address) {
|
|
391
|
+
const next = [...content];
|
|
392
|
+
const target = next[address.index];
|
|
393
|
+
if (!target) return void 0;
|
|
394
|
+
if (address.path.length === 0) {
|
|
395
|
+
next.splice(address.index, 1);
|
|
396
|
+
return next;
|
|
397
|
+
}
|
|
398
|
+
const inner = rewriteAt(target, address.path, null);
|
|
399
|
+
if (inner === void 0 || inner === null) return void 0;
|
|
400
|
+
next[address.index] = inner;
|
|
401
|
+
return next;
|
|
402
|
+
}
|
|
403
|
+
/** Whether `to` is `from` or a position inside the element `from` names. */
|
|
404
|
+
function reachesInside(from, to) {
|
|
405
|
+
if (to.index !== from.index) return false;
|
|
406
|
+
if (to.path.length < from.path.length) return false;
|
|
407
|
+
return from.path.every((step, i) => to.path[i] === step);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* `root` with `element` placed along `steps`: the last step is where it goes, a number among the
|
|
411
|
+
* children there (past the end appending) or a string as that element-valued prop; the steps
|
|
412
|
+
* before it walk the same way {@link rewriteAt} does. `undefined` when a step reaches nothing.
|
|
413
|
+
*/
|
|
414
|
+
function insertInto(root, steps, element) {
|
|
415
|
+
const [step, ...rest] = steps;
|
|
416
|
+
if (step === void 0) return void 0;
|
|
417
|
+
if (rest.length === 0) {
|
|
418
|
+
if (typeof step === "number") {
|
|
419
|
+
const children = [...root.children ?? []];
|
|
420
|
+
children.splice(Math.min(Math.max(step, 0), children.length), 0, element);
|
|
421
|
+
return {
|
|
422
|
+
...root,
|
|
423
|
+
children
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
...root,
|
|
428
|
+
props: {
|
|
429
|
+
...root.props,
|
|
430
|
+
[step]: element
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
if (typeof step === "number") {
|
|
435
|
+
const child = root.children?.[step];
|
|
436
|
+
if (!child) return void 0;
|
|
437
|
+
const placed = insertInto(child, rest, element);
|
|
438
|
+
if (placed === void 0) return void 0;
|
|
439
|
+
const children = [...root.children ?? []];
|
|
440
|
+
children[step] = placed;
|
|
441
|
+
return {
|
|
442
|
+
...root,
|
|
443
|
+
children
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
const held = root.props?.[step];
|
|
447
|
+
if (!isPreviewElement(held)) return void 0;
|
|
448
|
+
const placed = insertInto(held, rest, element);
|
|
449
|
+
if (placed === void 0) return void 0;
|
|
450
|
+
return {
|
|
451
|
+
...root,
|
|
452
|
+
props: {
|
|
453
|
+
...root.props,
|
|
454
|
+
[step]: placed
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
/** The `kind:path` an element names, or `undefined` for an intrinsic tag, which no `accepts` can name. */
|
|
459
|
+
function elementRefOf(config, element) {
|
|
460
|
+
if (isRef(element)) return element.__ref;
|
|
461
|
+
const { kind } = splitRef(element);
|
|
462
|
+
if (kind && kind !== element && config.entityOf(kind)) return element;
|
|
463
|
+
return config.resolve("component", element) ? `component:${element}` : void 0;
|
|
464
|
+
}
|
|
465
|
+
/** Every ref an `accepts` declaration names, one or a list. */
|
|
466
|
+
function acceptsRefsOf(accepts) {
|
|
467
|
+
return (Array.isArray(accepts) ? accepts : [accepts]).filter(isRef).map((entry) => entry.__ref);
|
|
468
|
+
}
|
|
469
|
+
/** The slots of a component as facts, computed once per component slice. */
|
|
470
|
+
function slotFacts(config, path) {
|
|
471
|
+
const cache = factsCache(config);
|
|
472
|
+
const hit = cache.get(path);
|
|
473
|
+
if (hit) return hit;
|
|
474
|
+
const body = config.resolve("component", path)?.body;
|
|
475
|
+
const facts = factsOfBody({
|
|
476
|
+
config,
|
|
477
|
+
body: body ?? {},
|
|
478
|
+
routes: slotTargetsOf(config, path)
|
|
479
|
+
});
|
|
480
|
+
cache.set(path, facts);
|
|
481
|
+
return facts;
|
|
482
|
+
}
|
|
483
|
+
function factsOfBody({ config, body, routes }) {
|
|
484
|
+
const props = body.props;
|
|
485
|
+
const layers = body.layers;
|
|
486
|
+
const fillable = routes.filter((route) => {
|
|
487
|
+
if ((props?.[route.from])?.text === true) return false;
|
|
488
|
+
return !routesToText(config, layers, route);
|
|
489
|
+
});
|
|
490
|
+
const root = layers ? rootLayerOf(layers)[0] : void 0;
|
|
491
|
+
const positionedChildren = layers !== void 0 && Object.keys(layers).length >= 2;
|
|
492
|
+
return fillable.filter((route) => route.from !== "children" || positionedChildren && route.layer !== root).map((route) => ({
|
|
493
|
+
...route,
|
|
494
|
+
accepts: acceptsOf({
|
|
495
|
+
config,
|
|
496
|
+
props,
|
|
497
|
+
layers,
|
|
498
|
+
route
|
|
499
|
+
})
|
|
500
|
+
}));
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* The refs a slot accepts: its own `accepts`, else the `accepts` of the prop at the far end of its
|
|
504
|
+
* route, read in the config that owns the host layer's component, since a borrowed host with no
|
|
505
|
+
* derived row is spelled in its source's namespace.
|
|
506
|
+
*/
|
|
507
|
+
function acceptsOf({ config, props, layers, route }) {
|
|
508
|
+
const decl = props?.[route.from];
|
|
509
|
+
const own = acceptsRefsOf(decl?.accepts);
|
|
510
|
+
if (own.length > 0) return own;
|
|
511
|
+
return acceptsRefsOf(farEndProp(config, layers, route)?.accepts);
|
|
512
|
+
}
|
|
513
|
+
/** The component a layer's element names, resolved where it lives; `undefined` for an intrinsic tag. */
|
|
514
|
+
function hostComponent(config, hostRef) {
|
|
515
|
+
if (!isRef(hostRef) && !hostRef.includes(":")) return config.resolve("component", hostRef);
|
|
516
|
+
return (config.configFor(hostRef) ?? config).resolve("component", pathOf(hostRef));
|
|
517
|
+
}
|
|
518
|
+
/** The declaration a route lands on, when the host layer is a component; `undefined` for a tag. */
|
|
519
|
+
function farEndProp(config, layers, route) {
|
|
520
|
+
const hostRef = layers?.[route.layer];
|
|
521
|
+
if (!hostRef || !hostComponent(config, hostRef)) return void 0;
|
|
522
|
+
return resolveComponentProps(config.configFor(hostRef) ?? config, pathOf(hostRef))[route.into];
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Whether a route ends in text: the host layer is a component whose prop at the far end is a text
|
|
526
|
+
* slot, or forwards on to one (a badge's `children` onto its text layer), so the layer holds a
|
|
527
|
+
* string. An intrinsic host has no declaration and takes nodes; a host that declares no such prop
|
|
528
|
+
* cannot take the route at all, which reads the same way here.
|
|
529
|
+
*/
|
|
530
|
+
function routesToText(config, layers, route) {
|
|
531
|
+
const hostRef = layers?.[route.layer];
|
|
532
|
+
if (!hostRef || !hostComponent(config, hostRef)) return false;
|
|
533
|
+
const into = farEndProp(config, layers, route);
|
|
534
|
+
if (!into) return true;
|
|
535
|
+
if (into.text === true) return true;
|
|
536
|
+
if (into.type !== "forward") return false;
|
|
537
|
+
const owner = config.configFor(hostRef) ?? config;
|
|
538
|
+
const hostLayers = (owner.resolve("component", pathOf(hostRef))?.body)?.layers;
|
|
539
|
+
return routesIntoText(owner, hostLayers, into);
|
|
540
|
+
}
|
|
541
|
+
//#endregion
|
|
542
|
+
export { listSlots, namedSlots, slotHostedBy, slotOf };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { ComponentStyleBody, ProjectedVisibility } from "../entities/system/Component.js";
|
|
2
|
+
import { StateCondition } from "@json-render/core";
|
|
3
|
+
|
|
4
|
+
//#region src/spec/visibility.d.ts
|
|
5
|
+
/** One `axis differs from value` test: json-render's `$state` test carrying only `neq`. */
|
|
6
|
+
type VariantExclusionTest = Pick<StateCondition, '$state' | 'neq'>;
|
|
7
|
+
/** The clause that hides a layer at one coordinate: one test, or an `$or` of one per pinned axis. */
|
|
8
|
+
type VariantExclusion = VariantExclusionTest | {
|
|
9
|
+
$or: VariantExclusionTest[];
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The clause that hides a layer at one coordinate, or `null` for a cell that pins nothing: a
|
|
13
|
+
* zero-axis component has one cell, and hiding the layer there is deleting it.
|
|
14
|
+
*/
|
|
15
|
+
declare function variantExclusion(cellProps: Readonly<Record<string, unknown>>): VariantExclusion | null;
|
|
16
|
+
/**
|
|
17
|
+
* A condition split into the clauses the eye writes, one per variant a layer is hidden in, and the
|
|
18
|
+
* rest, which is what a boolean binding reads and edits. A surface that rewrites the rest puts the
|
|
19
|
+
* exclusions back with `spelledWith`, so a hide in one variant survives a change of binding.
|
|
20
|
+
*/
|
|
21
|
+
declare function splitVariantExclusions(visible: ProjectedVisibility | undefined): {
|
|
22
|
+
exclusions: unknown[];
|
|
23
|
+
rest: unknown[];
|
|
24
|
+
};
|
|
25
|
+
/** The stored spelling of `clauses` followed by `exclusions`: `null` for none, a bare clause for one,
|
|
26
|
+
* the and-list for several. */
|
|
27
|
+
declare function spelledWith({
|
|
28
|
+
clauses,
|
|
29
|
+
exclusions
|
|
30
|
+
}: {
|
|
31
|
+
clauses: readonly unknown[];
|
|
32
|
+
exclusions: readonly unknown[];
|
|
33
|
+
}): unknown | null;
|
|
34
|
+
/** Whether the node's condition carries the eye's exclusion for this cell. */
|
|
35
|
+
declare function excludesVariant({
|
|
36
|
+
visible,
|
|
37
|
+
cellProps
|
|
38
|
+
}: {
|
|
39
|
+
visible: ProjectedVisibility | undefined;
|
|
40
|
+
cellProps: Readonly<Record<string, unknown>>;
|
|
41
|
+
}): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* The condition with this cell excluded: the stored one plus the clause, or unchanged when the
|
|
44
|
+
* clause is already there. `undefined` when the cell pins nothing, since there is no coordinate to
|
|
45
|
+
* exclude.
|
|
46
|
+
*/
|
|
47
|
+
declare function withVariantHidden({
|
|
48
|
+
visible,
|
|
49
|
+
cellProps
|
|
50
|
+
}: {
|
|
51
|
+
visible: ProjectedVisibility | undefined;
|
|
52
|
+
cellProps: Readonly<Record<string, unknown>>;
|
|
53
|
+
}): unknown | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* The condition with this cell's exclusion taken out, `null` when nothing is left (which clears the
|
|
56
|
+
* field), or `undefined` when the condition never excluded it.
|
|
57
|
+
*/
|
|
58
|
+
declare function withVariantShown({
|
|
59
|
+
visible,
|
|
60
|
+
cellProps
|
|
61
|
+
}: {
|
|
62
|
+
visible: ProjectedVisibility | undefined;
|
|
63
|
+
cellProps: Readonly<Record<string, unknown>>;
|
|
64
|
+
}): unknown | null | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* The style rules that hide `layer` with `display: none` under `state`, in stored order. The
|
|
67
|
+
* projection keeps no source for a hidden layer, so a surface asking which rule hid it reads them
|
|
68
|
+
* here.
|
|
69
|
+
*/
|
|
70
|
+
declare function layerHidingRules({
|
|
71
|
+
component,
|
|
72
|
+
layer,
|
|
73
|
+
state
|
|
74
|
+
}: {
|
|
75
|
+
component: {
|
|
76
|
+
readonly styles?: Readonly<Record<string, ComponentStyleBody>> | null;
|
|
77
|
+
};
|
|
78
|
+
layer: string; /** The instance's prop bag, defaults included, the same bag `layerStyles` resolves against. */
|
|
79
|
+
state: Record<string, unknown>;
|
|
80
|
+
}): ComponentStyleBody[];
|
|
81
|
+
//#endregion
|
|
82
|
+
export { VariantExclusion, VariantExclusionTest, excludesVariant, layerHidingRules, spelledWith, splitVariantExclusions, variantExclusion, withVariantHidden, withVariantShown };
|