@yahoo/uds-create-config 5.5.0 → 5.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/dist/configs/system.js +3 -1
- package/dist/entities/system/Component.d.ts +5 -2
- package/dist/entities/system/Component.js +46 -6
- package/dist/entities/system/Motion.d.ts +41 -9
- package/dist/entities/system/Motion.js +73 -6
- package/dist/entities/system/Token.js +1 -1
- package/dist/entities/system/defineComponent.js +2 -7
- package/dist/entities/system/linked-token-values.js +1 -1
- package/dist/entities/system/motion-presets.d.ts +32 -0
- package/dist/entities/system/motion-presets.js +239 -0
- package/dist/entities/system/motion-variants.d.ts +73 -0
- package/dist/entities/system/motion-variants.js +270 -0
- package/dist/framework/Config.d.ts +8 -1
- package/dist/framework/Config.js +34 -3
- package/dist/framework/class-names.d.ts +11 -1
- package/dist/framework/class-names.js +42 -1
- package/dist/framework/defineSubEntity.d.ts +7 -2
- package/dist/framework/defineSubEntity.js +11 -3
- package/dist/framework/projections.js +1 -0
- package/dist/framework/render-spec.d.ts +5 -1
- package/dist/framework/render-spec.js +21 -2
- package/dist/framework/schema-version.d.ts +1 -1
- package/dist/framework/schema-version.js +5 -5
- package/dist/index.d.ts +6 -4
- package/dist/index.js +8 -6
- package/dist/migrations/2.0.0/v1-artifact.js +1 -1
- package/dist/migrations/20261006125609_motion_variants_and_layout.d.ts +10 -0
- package/dist/migrations/20261006125609_motion_variants_and_layout.js +10 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Config } from "../../framework/Config.js";
|
|
2
|
+
|
|
3
|
+
//#region src/entities/system/motion-variants.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* A JS preset a component's style rule puts on one of its layers — `checkbox: { motion: 'press' }`.
|
|
6
|
+
* `preset` is the preset's path, or for one written in place the id {@link inlineMotionId} gives it.
|
|
7
|
+
*/
|
|
8
|
+
interface PlacedMotion {
|
|
9
|
+
readonly component: string;
|
|
10
|
+
readonly layer: string;
|
|
11
|
+
readonly preset: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A JS preset written in place as a rule's `motion` on a layer. It has no path, so it is named by
|
|
15
|
+
* where it sits: the component, the rule's key (its condition, `*` for the unconditional rule) and
|
|
16
|
+
* the layer. `invalid` holds why a body the build cannot read was left out, for the build to report.
|
|
17
|
+
*/
|
|
18
|
+
interface InlineMotion {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly component: string;
|
|
21
|
+
readonly rule: string;
|
|
22
|
+
readonly layer: string;
|
|
23
|
+
readonly body: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
/** The id an in-place preset goes by — `Btn.root inline [disabled=true]`. Never a preset path, since a
|
|
26
|
+
* path holds no space. */
|
|
27
|
+
declare function inlineMotionId(component: string, rule: string, layer: string): string;
|
|
28
|
+
/** Whether a rule's `motion` value is a preset written in place, rather than a ref or a name. */
|
|
29
|
+
declare function isInlineMotion(value: unknown): value is Record<string, unknown>;
|
|
30
|
+
/** Every JS preset a rule writes in place, and every one the build cannot read as a JS preset. */
|
|
31
|
+
declare const inlineMotions: (config: Config) => {
|
|
32
|
+
readonly valid: readonly InlineMotion[];
|
|
33
|
+
readonly invalid: readonly (Omit<InlineMotion, 'body'> & {
|
|
34
|
+
reason: string;
|
|
35
|
+
})[];
|
|
36
|
+
};
|
|
37
|
+
/** A JS preset's body by its path, or an in-place one's by its id; `undefined` for anything else. */
|
|
38
|
+
declare function jsMotionBody(config: Config, preset: string): Record<string, unknown> | undefined;
|
|
39
|
+
/** The states a preset names a variant in. */
|
|
40
|
+
declare const MOTION_VARIANT_STATES: readonly ["initial", "animate", "exit", "whileHover", "whileTap", "whileFocus", "whileDrag"];
|
|
41
|
+
type Body = Record<string, unknown>;
|
|
42
|
+
/** The variant names one state holds: a name, a list of names, or none (values, `false`). */
|
|
43
|
+
declare function stateVariantNames(value: unknown): string[];
|
|
44
|
+
/** The variants a preset declares. */
|
|
45
|
+
declare function declaredVariants(body: Body | undefined): string[];
|
|
46
|
+
/** Every JS preset a style rule puts on a layer, across the system. */
|
|
47
|
+
declare const placedMotions: (config: Config) => readonly PlacedMotion[];
|
|
48
|
+
/**
|
|
49
|
+
* The presets that share variant names, each set closed over placement and composition.
|
|
50
|
+
*
|
|
51
|
+
* Two presets on one component's layers are in one set. So is a preset a component places and one a
|
|
52
|
+
* component it renders places, at any depth: the label passes through a component that places nothing
|
|
53
|
+
* on its way down, so the walk goes through those without joining them. A component that places
|
|
54
|
+
* nothing joins nothing, which keeps Button and Checkbox apart although both render Box.
|
|
55
|
+
*/
|
|
56
|
+
declare const motionVariantGroups: (config: Config) => readonly ReadonlySet<string>[];
|
|
57
|
+
/** A variant a placed preset declares that nothing animating with it ever names. */
|
|
58
|
+
interface OrphanedMotionVariant {
|
|
59
|
+
readonly preset: string;
|
|
60
|
+
readonly variant: string;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Every variant a placed preset declares that no state in its set names — what a rename of the name
|
|
64
|
+
* on one side leaves on the other, and what `motion/react` then never animates to, silently.
|
|
65
|
+
*
|
|
66
|
+
* Only presets a style rule places are checked: an unplaced preset's variants may answer whatever the
|
|
67
|
+
* app renders around it, and neither is a set holding a component with a `motion` prop. A placed one
|
|
68
|
+
* may be driven from outside the system too, by an app ancestor animating to the same name, which is
|
|
69
|
+
* why this warns and never refuses.
|
|
70
|
+
*/
|
|
71
|
+
declare function orphanedMotionVariants(config: Config): OrphanedMotionVariant[];
|
|
72
|
+
//#endregion
|
|
73
|
+
export { InlineMotion, MOTION_VARIANT_STATES, OrphanedMotionVariant, PlacedMotion, declaredVariants, inlineMotionId, inlineMotions, isInlineMotion, jsMotionBody, motionVariantGroups, orphanedMotionVariants, placedMotions, stateVariantNames };
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { reject } from "../../framework/rejection.js";
|
|
2
|
+
import { isRef, kindOf, pathOf } from "../../framework/utils/refs.js";
|
|
3
|
+
import { isPlainObject } from "../../framework/utils/field-path.js";
|
|
4
|
+
import { createSliceMemo } from "../../framework/memo.js";
|
|
5
|
+
import { inlineJsMotion } from "./Motion.js";
|
|
6
|
+
//#region src/entities/system/motion-variants.ts
|
|
7
|
+
/** The id an in-place preset goes by — `Btn.root inline [disabled=true]`. Never a preset path, since a
|
|
8
|
+
* path holds no space. */
|
|
9
|
+
function inlineMotionId(component, rule, layer) {
|
|
10
|
+
return `${component}.${layer} inline [${rule}]`;
|
|
11
|
+
}
|
|
12
|
+
/** Whether a rule's `motion` value is a preset written in place, rather than a ref or a name. */
|
|
13
|
+
function isInlineMotion(value) {
|
|
14
|
+
return isPlainObject(value) && !isRef(value);
|
|
15
|
+
}
|
|
16
|
+
/** Every JS preset a rule writes in place, and every one the build cannot read as a JS preset. */
|
|
17
|
+
const inlineMotions = createSliceMemo({
|
|
18
|
+
kinds: ["component"],
|
|
19
|
+
compute: (config) => {
|
|
20
|
+
const valid = [];
|
|
21
|
+
const invalid = [];
|
|
22
|
+
for (const component of config.list("component")) {
|
|
23
|
+
const { styles = {} } = component.toJSON();
|
|
24
|
+
for (const [rule, entry] of Object.entries(styles)) for (const [layer, bag] of Object.entries(entry?.layers ?? {})) {
|
|
25
|
+
const value = bag?.motion;
|
|
26
|
+
if (!isInlineMotion(value)) continue;
|
|
27
|
+
const id = inlineMotionId(component.path, rule, layer);
|
|
28
|
+
const parsed = inlineJsMotion.safeParse(value);
|
|
29
|
+
if (!parsed.success) {
|
|
30
|
+
invalid.push({
|
|
31
|
+
id,
|
|
32
|
+
component: component.path,
|
|
33
|
+
rule,
|
|
34
|
+
layer,
|
|
35
|
+
reason: parsed.error.issues.map((issue) => [issue.path.join("."), issue.message].filter(Boolean).join(": ")).join("; ")
|
|
36
|
+
});
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
valid.push({
|
|
40
|
+
id,
|
|
41
|
+
component: component.path,
|
|
42
|
+
rule,
|
|
43
|
+
layer,
|
|
44
|
+
body: {
|
|
45
|
+
...value,
|
|
46
|
+
runtime: "js"
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
valid,
|
|
53
|
+
invalid
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
/** A JS preset's body by its path, or an in-place one's by its id; `undefined` for anything else. */
|
|
58
|
+
function jsMotionBody(config, preset) {
|
|
59
|
+
const named = config.resolve("motion", preset)?.toJSON();
|
|
60
|
+
if (named) return named.runtime === "js" ? named : void 0;
|
|
61
|
+
return inlineMotions(config).valid.find((entry) => entry.id === preset)?.body;
|
|
62
|
+
}
|
|
63
|
+
/** The states a preset names a variant in. */
|
|
64
|
+
const MOTION_VARIANT_STATES = [
|
|
65
|
+
"initial",
|
|
66
|
+
"animate",
|
|
67
|
+
"exit",
|
|
68
|
+
"whileHover",
|
|
69
|
+
"whileTap",
|
|
70
|
+
"whileFocus",
|
|
71
|
+
"whileDrag"
|
|
72
|
+
];
|
|
73
|
+
const jsBody = (config, path) => jsMotionBody(config, path);
|
|
74
|
+
/** The variant names one state holds: a name, a list of names, or none (values, `false`). */
|
|
75
|
+
function stateVariantNames(value) {
|
|
76
|
+
if (typeof value === "string") return [value];
|
|
77
|
+
if (Array.isArray(value)) return value.filter((name) => typeof name === "string");
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
/** The variants a preset declares. */
|
|
81
|
+
function declaredVariants(body) {
|
|
82
|
+
const variants = body?.variants;
|
|
83
|
+
return variants && typeof variants === "object" ? Object.keys(variants) : [];
|
|
84
|
+
}
|
|
85
|
+
/** Every variant a preset's states name. */
|
|
86
|
+
function namedVariants(body) {
|
|
87
|
+
if (!body) return [];
|
|
88
|
+
return MOTION_VARIANT_STATES.flatMap((state) => stateVariantNames(body[state]));
|
|
89
|
+
}
|
|
90
|
+
/** Every JS preset a style rule puts on a layer, across the system. */
|
|
91
|
+
const placedMotions = createSliceMemo({
|
|
92
|
+
kinds: ["component", "motion"],
|
|
93
|
+
compute: (config) => {
|
|
94
|
+
const out = [];
|
|
95
|
+
for (const component of config.list("component")) {
|
|
96
|
+
const { styles = {} } = component.toJSON();
|
|
97
|
+
for (const [rule, entry] of Object.entries(styles)) for (const [layer, bag] of Object.entries(entry?.layers ?? {})) {
|
|
98
|
+
const named = bag?.motion;
|
|
99
|
+
const preset = isRef(named) ? pathOf(named.__ref) : typeof named === "string" ? named : isInlineMotion(named) ? inlineMotionId(component.path, rule, layer) : void 0;
|
|
100
|
+
if (preset === void 0 || !jsBody(config, preset)) continue;
|
|
101
|
+
out.push({
|
|
102
|
+
component: component.path,
|
|
103
|
+
layer,
|
|
104
|
+
preset
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
/** The components a component's layers render, resolved to the path each lives at. */
|
|
112
|
+
const composedComponents = (config, path) => {
|
|
113
|
+
const { layers = {} } = config.resolve("component", path)?.toJSON() ?? {};
|
|
114
|
+
return Object.values(layers).flatMap((element) => isRef(element) && kindOf(element) === "component" ? [config.resolve("component", pathOf(element))?.path ?? pathOf(element)] : []);
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* The presets that share variant names, each set closed over placement and composition.
|
|
118
|
+
*
|
|
119
|
+
* Two presets on one component's layers are in one set. So is a preset a component places and one a
|
|
120
|
+
* component it renders places, at any depth: the label passes through a component that places nothing
|
|
121
|
+
* on its way down, so the walk goes through those without joining them. A component that places
|
|
122
|
+
* nothing joins nothing, which keeps Button and Checkbox apart although both render Box.
|
|
123
|
+
*/
|
|
124
|
+
const motionVariantGroups = createSliceMemo({
|
|
125
|
+
kinds: ["component", "motion"],
|
|
126
|
+
compute: (config) => {
|
|
127
|
+
const parent = /* @__PURE__ */ new Map();
|
|
128
|
+
const find = (node) => {
|
|
129
|
+
let root = node;
|
|
130
|
+
while (parent.has(root) && parent.get(root) !== root) root = parent.get(root);
|
|
131
|
+
parent.set(node, root);
|
|
132
|
+
return root;
|
|
133
|
+
};
|
|
134
|
+
const join = (a, b) => {
|
|
135
|
+
const ra = find(a);
|
|
136
|
+
const rb = find(b);
|
|
137
|
+
if (ra !== rb) parent.set(ra, rb);
|
|
138
|
+
};
|
|
139
|
+
const placing = /* @__PURE__ */ new Set();
|
|
140
|
+
for (const { component, preset } of placedMotions(config)) {
|
|
141
|
+
placing.add(component);
|
|
142
|
+
join(`c\u0000${component}`, `p\u0000${preset}`);
|
|
143
|
+
}
|
|
144
|
+
for (const component of placing) {
|
|
145
|
+
const seen = new Set([component]);
|
|
146
|
+
const queue = composedComponents(config, component);
|
|
147
|
+
while (queue.length > 0) {
|
|
148
|
+
const next = queue.shift();
|
|
149
|
+
if (seen.has(next)) continue;
|
|
150
|
+
seen.add(next);
|
|
151
|
+
if (placing.has(next)) join(`c\u0000${component}`, `c\u0000${next}`);
|
|
152
|
+
queue.push(...composedComponents(config, next));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const groups = /* @__PURE__ */ new Map();
|
|
156
|
+
for (const node of parent.keys()) {
|
|
157
|
+
if (node[0] !== "p") continue;
|
|
158
|
+
const root = find(node);
|
|
159
|
+
const group = groups.get(root) ?? /* @__PURE__ */ new Set();
|
|
160
|
+
group.add(node.slice(2));
|
|
161
|
+
groups.set(root, group);
|
|
162
|
+
}
|
|
163
|
+
return [...groups.values()];
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
/** The presets that share variant names with `preset` — itself alone when no rule places it. */
|
|
167
|
+
function motionVariantGroupOf(config, preset) {
|
|
168
|
+
return motionVariantGroups(config).find((group) => group.has(preset)) ?? new Set([preset]);
|
|
169
|
+
}
|
|
170
|
+
/** A preset's body with one variant name renamed, in its `variants` and in every state naming it. */
|
|
171
|
+
function renamedBody(body, from, to) {
|
|
172
|
+
const next = { ...body };
|
|
173
|
+
const variants = body.variants;
|
|
174
|
+
if (variants && Object.hasOwn(variants, from)) next.variants = Object.fromEntries(Object.entries(variants).map(([name, value]) => [name === from ? to : name, value]));
|
|
175
|
+
for (const state of MOTION_VARIANT_STATES) {
|
|
176
|
+
const value = body[state];
|
|
177
|
+
if (value === from) next[state] = to;
|
|
178
|
+
else if (Array.isArray(value)) next[state] = value.map((name) => name === from ? to : name);
|
|
179
|
+
}
|
|
180
|
+
return next;
|
|
181
|
+
}
|
|
182
|
+
const mentions = (body, name) => declaredVariants(body).includes(name) || namedVariants(body).includes(name);
|
|
183
|
+
/**
|
|
184
|
+
* Rename a variant across every preset that shares names with `path` ({@link motionVariantGroupOf}):
|
|
185
|
+
* the key in each `variants` that declares it, and each state that names it. Refused when `path` is
|
|
186
|
+
* not a JS preset that declares `from`, or when a preset in the set already declares `to`, since the
|
|
187
|
+
* two would become one variant.
|
|
188
|
+
*/
|
|
189
|
+
function renameMotionVariant(config, { path, from, to }) {
|
|
190
|
+
const own = jsBody(config, path);
|
|
191
|
+
if (!own) reject("NOT_FOUND", `"${path}" is not a JS motion preset, so it has no variants.`);
|
|
192
|
+
if (!declaredVariants(own).includes(from)) reject("NOT_FOUND", `"${path}" declares no variant "${from}".`);
|
|
193
|
+
if (from === to) return config;
|
|
194
|
+
const group = [...motionVariantGroupOf(config, path)];
|
|
195
|
+
const taken = group.filter((preset) => declaredVariants(jsBody(config, preset)).includes(to));
|
|
196
|
+
if (taken.length > 0) reject("ALREADY_EXISTS", `"${to}" is already a variant of ${taken.map((preset) => `"${preset}"`).join(", ")}, which animate${taken.length === 1 ? "s" : ""} with "${path}". Renaming "${from}" onto it would make the two one variant.`);
|
|
197
|
+
let next = config;
|
|
198
|
+
for (const preset of group) {
|
|
199
|
+
const body = jsBody(next, preset);
|
|
200
|
+
if (!body || !mentions(body, from)) continue;
|
|
201
|
+
const inline = inlineMotions(next).valid.find((entry) => entry.id === preset);
|
|
202
|
+
if (!inline) {
|
|
203
|
+
next = next.apply({
|
|
204
|
+
kind: "motion",
|
|
205
|
+
operation: "replace",
|
|
206
|
+
path: preset,
|
|
207
|
+
data: renamedBody(body, from, to)
|
|
208
|
+
});
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const component = next.resolve("component", inline.component)?.toJSON();
|
|
212
|
+
const bag = component?.styles?.[inline.rule]?.layers?.[inline.layer];
|
|
213
|
+
if (!component || !bag || !isInlineMotion(bag.motion)) continue;
|
|
214
|
+
next = next.apply({
|
|
215
|
+
kind: "component",
|
|
216
|
+
operation: "replace",
|
|
217
|
+
path: inline.component,
|
|
218
|
+
data: {
|
|
219
|
+
...component,
|
|
220
|
+
styles: {
|
|
221
|
+
...component.styles,
|
|
222
|
+
[inline.rule]: {
|
|
223
|
+
...component.styles?.[inline.rule],
|
|
224
|
+
layers: {
|
|
225
|
+
...component.styles?.[inline.rule]?.layers,
|
|
226
|
+
[inline.layer]: {
|
|
227
|
+
...bag,
|
|
228
|
+
motion: renamedBody(bag.motion, from, to)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
return next;
|
|
237
|
+
}
|
|
238
|
+
const declaresMotionProp = (config, path) => {
|
|
239
|
+
const { props = {} } = config.resolve("component", path)?.toJSON() ?? {};
|
|
240
|
+
return Object.values(props).some((prop) => prop?.type === "motion");
|
|
241
|
+
};
|
|
242
|
+
/**
|
|
243
|
+
* Every variant a placed preset declares that no state in its set names — what a rename of the name
|
|
244
|
+
* on one side leaves on the other, and what `motion/react` then never animates to, silently.
|
|
245
|
+
*
|
|
246
|
+
* Only presets a style rule places are checked: an unplaced preset's variants may answer whatever the
|
|
247
|
+
* app renders around it, and neither is a set holding a component with a `motion` prop. A placed one
|
|
248
|
+
* may be driven from outside the system too, by an app ancestor animating to the same name, which is
|
|
249
|
+
* why this warns and never refuses.
|
|
250
|
+
*/
|
|
251
|
+
function orphanedMotionVariants(config) {
|
|
252
|
+
const entries = placedMotions(config);
|
|
253
|
+
const placed = new Set(entries.map((entry) => entry.preset));
|
|
254
|
+
const open = new Set(entries.filter((entry) => declaresMotionProp(config, entry.component)).map((entry) => entry.preset));
|
|
255
|
+
const out = [];
|
|
256
|
+
for (const group of motionVariantGroups(config)) {
|
|
257
|
+
if ([...group].some((preset) => open.has(preset))) continue;
|
|
258
|
+
const named = new Set([...group].flatMap((preset) => namedVariants(jsBody(config, preset))));
|
|
259
|
+
for (const preset of group) {
|
|
260
|
+
if (!placed.has(preset)) continue;
|
|
261
|
+
for (const variant of declaredVariants(jsBody(config, preset))) if (!named.has(variant)) out.push({
|
|
262
|
+
preset,
|
|
263
|
+
variant
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
//#endregion
|
|
270
|
+
export { MOTION_VARIANT_STATES, declaredVariants, inlineMotionId, inlineMotions, isInlineMotion, jsMotionBody, motionVariantGroups, orphanedMotionVariants, placedMotions, renameMotionVariant, stateVariantNames };
|
|
@@ -72,7 +72,7 @@ interface RecordedSource {
|
|
|
72
72
|
* The dense-counter `2` a short-lived scheme stamped before the chain existed reads as the mint
|
|
73
73
|
* that replaced it (see `detectedWireVersion`).
|
|
74
74
|
*/
|
|
75
|
-
declare const SERIALIZED_CONFIG_VERSION =
|
|
75
|
+
declare const SERIALIZED_CONFIG_VERSION = 20261006125609;
|
|
76
76
|
interface SerializedConfig {
|
|
77
77
|
/** See {@link SERIALIZED_CONFIG_VERSION}. Always written; a stored artifact may lack it. */
|
|
78
78
|
readonly version: number;
|
|
@@ -1092,6 +1092,13 @@ declare class Config {
|
|
|
1092
1092
|
* batch wrote once it has landed, every kind after a registration. Never called on hydration.
|
|
1093
1093
|
*/
|
|
1094
1094
|
private assertInvariants;
|
|
1095
|
+
/**
|
|
1096
|
+
* One sentence per item of `kind` that extends itself through local extensions — `A` extending `B`
|
|
1097
|
+
* extending `A`. Each base resolves only once the one it extends has, so a loop has no body at all,
|
|
1098
|
+
* and a read answers nothing for every item on it. Refused where it's written instead, naming the
|
|
1099
|
+
* loop. An extension through a linked system can't close one: the source is its own config.
|
|
1100
|
+
*/
|
|
1101
|
+
private extensionCycles;
|
|
1095
1102
|
/** The {@link EntityKind.uniqueLeaves} half of {@link assertInvariants} alone. */
|
|
1096
1103
|
private assertUniqueLeaves;
|
|
1097
1104
|
/** One sentence per leaf that two items of `kind` hold, since a leaf has to name one item. */
|
package/dist/framework/Config.js
CHANGED
|
@@ -1618,10 +1618,41 @@ var Config = class Config {
|
|
|
1618
1618
|
const kinds = kind === void 0 ? [...this.state.entities.keys()] : [kind];
|
|
1619
1619
|
for (const name of kinds) {
|
|
1620
1620
|
const entry = this.entityOf(name);
|
|
1621
|
-
const problems = [
|
|
1621
|
+
const problems = [
|
|
1622
|
+
...entry?.uniqueLeaves ? this.duplicateLeaves(name) : [],
|
|
1623
|
+
...this.extensionCycles(name),
|
|
1624
|
+
...entry?.invariants?.(this) ?? []
|
|
1625
|
+
];
|
|
1622
1626
|
if (problems.length > 0) rejectData(`Invalid ${name}: ${problems.join(" ")}`);
|
|
1623
1627
|
}
|
|
1624
1628
|
}
|
|
1629
|
+
/**
|
|
1630
|
+
* One sentence per item of `kind` that extends itself through local extensions — `A` extending `B`
|
|
1631
|
+
* extending `A`. Each base resolves only once the one it extends has, so a loop has no body at all,
|
|
1632
|
+
* and a read answers nothing for every item on it. Refused where it's written instead, naming the
|
|
1633
|
+
* loop. An extension through a linked system can't close one: the source is its own config.
|
|
1634
|
+
*/
|
|
1635
|
+
extensionCycles(kind) {
|
|
1636
|
+
const items = this.state.snapshot.slice(kind).items;
|
|
1637
|
+
const localBase = (path) => {
|
|
1638
|
+
const stored = items.get(path)?.toJSON();
|
|
1639
|
+
if (!isExtendedRef(stored) || sourceOf(stored) !== void 0) return;
|
|
1640
|
+
return pathOf(stored);
|
|
1641
|
+
};
|
|
1642
|
+
const problems = [];
|
|
1643
|
+
for (const path of items.keys()) {
|
|
1644
|
+
const chain = [path];
|
|
1645
|
+
for (let base = localBase(path); base !== void 0; base = localBase(base)) {
|
|
1646
|
+
if (base === path) {
|
|
1647
|
+
problems.push(`"${path}" extends itself (${[...chain, path].join(" → ")}); an extension needs a base that doesn't extend it.`);
|
|
1648
|
+
break;
|
|
1649
|
+
}
|
|
1650
|
+
if (chain.includes(base)) break;
|
|
1651
|
+
chain.push(base);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return problems;
|
|
1655
|
+
}
|
|
1625
1656
|
/** The {@link EntityKind.uniqueLeaves} half of {@link assertInvariants} alone. */
|
|
1626
1657
|
assertUniqueLeaves(kind) {
|
|
1627
1658
|
if (!this.entityOf(kind)?.uniqueLeaves) return;
|
|
@@ -3600,6 +3631,7 @@ var Config = class Config {
|
|
|
3600
3631
|
const extension = stored.toJSON();
|
|
3601
3632
|
if (!isExtendedRef(extension)) continue;
|
|
3602
3633
|
if (slug !== void 0 && sourceOf(extension) !== slug) continue;
|
|
3634
|
+
if (only === void 0 && sourceOf(extension) === void 0) continue;
|
|
3603
3635
|
const resolved = this.resolveExtension({
|
|
3604
3636
|
kind,
|
|
3605
3637
|
path,
|
|
@@ -5314,7 +5346,7 @@ var Config = class Config {
|
|
|
5314
5346
|
* links from the provided sources. A declared `linkedKind` with no provided source throws. */
|
|
5315
5347
|
hydrateFrom(stored, options) {
|
|
5316
5348
|
const claimed = schemaVersionOf(stored);
|
|
5317
|
-
if (claimed >
|
|
5349
|
+
if (claimed > 20261006125609) throw new SchemaVersionTooNew(claimed);
|
|
5318
5350
|
const wire = upgradeSerializedConfig(stored);
|
|
5319
5351
|
if (!Array.isArray(wire.ownedKinds) || typeof wire.items !== "object" || wire.items === null) throw new ConfigFormatError(`Config "${stored.name ?? "system-config"}" cannot be hydrated: this is not a serialized config-v2 config (no "ownedKinds"/"items"). A pre-cutover build artifact converts through the registered cutover migration — import the config type's module (e.g. \`configs/system\`) before hydrating, or port the repo once with \`uds migrate\`.`);
|
|
5320
5352
|
const leftover = wire.options;
|
|
@@ -5596,7 +5628,6 @@ function comparableBody(value) {
|
|
|
5596
5628
|
function extensionChange(entry, kind, path, raw, validate = false) {
|
|
5597
5629
|
if (kindOf(raw) !== kind) rejectData(`Invalid ${kind} extension: base ref "${raw.__ref}" names ${kindOf(raw)}, not ${kind}`);
|
|
5598
5630
|
if (memberOf(raw) !== void 0) rejectData(`Invalid ${kind} extension: base ref "${raw.__ref}" names a member; an item extension needs a whole-entity ref`);
|
|
5599
|
-
if (sourceOf(raw) === void 0) rejectData(`Invalid ${kind} extension: base ref "${raw.__ref}" is local; extend a ref through a declared linked system`);
|
|
5600
5631
|
let delta = raw.extend;
|
|
5601
5632
|
if (validate === "author") {
|
|
5602
5633
|
const parsed = dataSchema(entry.schemas, "update")?.safeParse(delta);
|
|
@@ -199,6 +199,16 @@ declare function inlineRuleComposites(config: Config, element: unknown, bag: Rec
|
|
|
199
199
|
* nothing until a rebuild, while a prop json-render applies on the spot.
|
|
200
200
|
*/
|
|
201
201
|
declare function layerRoutedProps(config: Config, path: string, state?: Record<string, unknown>): Record<string, Record<string, unknown>>;
|
|
202
|
+
/**
|
|
203
|
+
* The JS preset each layer animates by, for one instance: the `motion` of the last style rule that
|
|
204
|
+
* applies and names a JS preset for the layer, the unconditional rule first and the conditional ones
|
|
205
|
+
* in declaration order, which is the order the emitted module routes them in. A CSS preset is a
|
|
206
|
+
* class and stays with {@link styleRuleMotionClasses}.
|
|
207
|
+
*
|
|
208
|
+
* What lets the canvas play a preset a rule put on a layer before any build: the expansion renders
|
|
209
|
+
* that layer through motion's factory and hands it the preset's props.
|
|
210
|
+
*/
|
|
211
|
+
declare function layerJsMotionPresets(config: Config, path: string, state?: Record<string, unknown>): Record<string, string>;
|
|
202
212
|
/**
|
|
203
213
|
* The CSS MOTION classes a style rule puts on a layer, for one instance.
|
|
204
214
|
*
|
|
@@ -337,4 +347,4 @@ declare function modifierUtilitiesUsed(config: Config): Set<string>;
|
|
|
337
347
|
*/
|
|
338
348
|
declare function splitVariants(cls: string): string[];
|
|
339
349
|
//#endregion
|
|
340
|
-
export { ComponentRuleClass, PickedComposite, SplitStyleProps, arbitraryClassName, arbitraryNeedsClass, classLeaf, classSelector, componentClassBase, componentCompositeClasses, componentLayerClass, componentMotionClasses, componentPropClasses, componentPropComposites, componentRuleClasses, forceModeAttribute, forceModeAttributeOf, forceModeProp, forceModePropGroup, forceModePropValue, forceModeProps, forceStateAttribute, inlineRuleComposites, isForceModeProp, kebabComponent, layerRoutedProps, modifierUtilitiesUsed, motionClassName, ruleCondition, splitStyleProps, splitVariants, stylePropClassBase, stylePropClassName, stylePropClasses, stylePropOwnClasses, styleRuleMotionClasses };
|
|
350
|
+
export { ComponentRuleClass, PickedComposite, SplitStyleProps, arbitraryClassName, arbitraryNeedsClass, classLeaf, classSelector, componentClassBase, componentCompositeClasses, componentLayerClass, componentMotionClasses, componentPropClasses, componentPropComposites, componentRuleClasses, forceModeAttribute, forceModeAttributeOf, forceModeProp, forceModePropGroup, forceModePropValue, forceModeProps, forceStateAttribute, inlineRuleComposites, isForceModeProp, kebabComponent, layerJsMotionPresets, layerRoutedProps, modifierUtilitiesUsed, motionClassName, ruleCondition, splitStyleProps, splitVariants, stylePropClassBase, stylePropClassName, stylePropClasses, stylePropOwnClasses, styleRuleMotionClasses };
|
|
@@ -4,6 +4,7 @@ import { createSliceMemo } from "./memo.js";
|
|
|
4
4
|
import { classPrefixOf, prefixed, varPrefixOf } from "../entities/system/Settings.js";
|
|
5
5
|
import { storedPreview } from "./preview-store.js";
|
|
6
6
|
import { authoredValue, className, isCustomPropertyKey, negatedCssValue, renderStyleValue, resolveComponentProps, stylePropEntries, stylePropLeafForToken, stylePropValueLeaves, stylePropertyPathFor } from "./projections.js";
|
|
7
|
+
import { inlineMotionId, isInlineMotion, jsMotionBody } from "../entities/system/motion-variants.js";
|
|
7
8
|
import { ruleApplies } from "./layer-styles.js";
|
|
8
9
|
//#region src/framework/class-names.ts
|
|
9
10
|
/**
|
|
@@ -529,6 +530,9 @@ function layerRoutedProps(config, path, state = {}) {
|
|
|
529
530
|
const declared = isRef(element) ? resolveComponentProps(config, pathOf(element.__ref)) : {};
|
|
530
531
|
for (const [prop, value] of Object.entries(bag ?? {})) {
|
|
531
532
|
if (prop.startsWith("_") || isCustomPropertyKey(prop)) continue;
|
|
533
|
+
if (prop === "motion") {
|
|
534
|
+
if ((config.resolve("motion", String(authoredValue(config, prop, value)))?.toJSON())?.runtime !== "css" || !isRef(element)) continue;
|
|
535
|
+
}
|
|
532
536
|
const styleKey = stylePropertyPathFor({
|
|
533
537
|
config,
|
|
534
538
|
key: prop
|
|
@@ -543,6 +547,43 @@ function layerRoutedProps(config, path, state = {}) {
|
|
|
543
547
|
return out;
|
|
544
548
|
}
|
|
545
549
|
/**
|
|
550
|
+
* The JS preset each layer animates by, for one instance: the `motion` of the last style rule that
|
|
551
|
+
* applies and names a JS preset for the layer, the unconditional rule first and the conditional ones
|
|
552
|
+
* in declaration order, which is the order the emitted module routes them in. A CSS preset is a
|
|
553
|
+
* class and stays with {@link styleRuleMotionClasses}.
|
|
554
|
+
*
|
|
555
|
+
* What lets the canvas play a preset a rule put on a layer before any build: the expansion renders
|
|
556
|
+
* that layer through motion's factory and hands it the preset's props.
|
|
557
|
+
*/
|
|
558
|
+
function layerJsMotionPresets(config, path, state = {}) {
|
|
559
|
+
const { styles = {}, defaultProps = {} } = config.resolve("component", path)?.toJSON() ?? {};
|
|
560
|
+
const resolved = {
|
|
561
|
+
...defaultProps,
|
|
562
|
+
...state
|
|
563
|
+
};
|
|
564
|
+
const rules = Object.entries(styles);
|
|
565
|
+
const unconditional = ([, rule]) => !rule.when || Object.keys(rule.when).length === 0;
|
|
566
|
+
const ordered = [...rules.filter(unconditional), ...rules.filter((entry) => !unconditional(entry))];
|
|
567
|
+
const out = {};
|
|
568
|
+
for (const [key, rule] of ordered) {
|
|
569
|
+
if (!ruleApplies(rule.when, resolved)) continue;
|
|
570
|
+
for (const [layer, bag] of Object.entries(rule.layers ?? {})) {
|
|
571
|
+
const named = bag?.motion;
|
|
572
|
+
if (isInlineMotion(named)) {
|
|
573
|
+
const id = inlineMotionId(path, key, layer);
|
|
574
|
+
if (jsMotionBody(config, id)) out[layer] = id;
|
|
575
|
+
else delete out[layer];
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
const preset = isRef(named) ? pathOf(named.__ref) : typeof named === "string" ? named : void 0;
|
|
579
|
+
if (preset === void 0) continue;
|
|
580
|
+
if ((config.resolve("motion", preset)?.toJSON())?.runtime === "js") out[layer] = preset;
|
|
581
|
+
else delete out[layer];
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return out;
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
546
587
|
* The CSS MOTION classes a style rule puts on a layer, for one instance.
|
|
547
588
|
*
|
|
548
589
|
* A preset's whole runtime is a class too, and a composite's route (forward it, and let the receiver
|
|
@@ -929,4 +970,4 @@ function splitVariants(cls) {
|
|
|
929
970
|
return segments;
|
|
930
971
|
}
|
|
931
972
|
//#endregion
|
|
932
|
-
export { arbitraryClassName, arbitraryNeedsClass, classLeaf, classSelector, componentClassBase, componentCompositeClasses, componentLayerClass, componentMotionClasses, componentPropClasses, componentPropComposites, componentRuleClasses, forceModeAttribute, forceModeAttributeOf, forceModeProp, forceModePropGroup, forceModePropValue, forceModeProps, forceStateAttribute, inlineRuleComposites, isForceModeProp, kebabComponent, layerRoutedProps, modifierUtilitiesUsed, motionClassName, ruleCondition, splitStyleProps, splitVariants, stylePropClassBase, stylePropClassName, stylePropClasses, stylePropOwnClasses, styleRuleMotionClasses };
|
|
973
|
+
export { arbitraryClassName, arbitraryNeedsClass, classLeaf, classSelector, componentClassBase, componentCompositeClasses, componentLayerClass, componentMotionClasses, componentPropClasses, componentPropComposites, componentRuleClasses, forceModeAttribute, forceModeAttributeOf, forceModeProp, forceModePropGroup, forceModePropValue, forceModeProps, forceStateAttribute, inlineRuleComposites, isForceModeProp, kebabComponent, layerJsMotionPresets, layerRoutedProps, modifierUtilitiesUsed, motionClassName, ruleCondition, splitStyleProps, splitVariants, stylePropClassBase, stylePropClassName, stylePropClasses, stylePropOwnClasses, styleRuleMotionClasses };
|
|
@@ -159,8 +159,13 @@ declare function defineSubEntity<F extends z.ZodTypeAny>(spec: {
|
|
|
159
159
|
* every way into stored state takes it: `register`, an item's create or update, and op data.
|
|
160
160
|
*
|
|
161
161
|
* Two members with the same key are one member, the later one, as the key derivation on write
|
|
162
|
-
* already treats a record whose keys disagree with their members
|
|
162
|
+
* already treats a record whose keys disagree with their members — unless `merge` says how the two
|
|
163
|
+
* combine, in which case the member is `merge(earlier, later)`, placed where the later one was.
|
|
163
164
|
*/
|
|
164
|
-
declare function orList<F extends z.ZodTypeAny>(collection: SubEntityClass<F
|
|
165
|
+
declare function orList<F extends z.ZodTypeAny>(collection: SubEntityClass<F>, {
|
|
166
|
+
merge
|
|
167
|
+
}?: {
|
|
168
|
+
readonly merge?: (earlier: z.output<F>, later: z.output<F>) => z.output<F>;
|
|
169
|
+
}): z.ZodUnion<readonly [z.ZodPipe<z.ZodArray<F>, z.ZodTransform<Record<string, z.output<F>>, z.core.output<F>[]>>, SubEntityClass<F>]>;
|
|
165
170
|
//#endregion
|
|
166
171
|
export { SubEntityClass, defineSubEntity, orList };
|
|
@@ -124,14 +124,22 @@ function defineSubEntity(spec) {
|
|
|
124
124
|
* every way into stored state takes it: `register`, an item's create or update, and op data.
|
|
125
125
|
*
|
|
126
126
|
* Two members with the same key are one member, the later one, as the key derivation on write
|
|
127
|
-
* already treats a record whose keys disagree with their members
|
|
127
|
+
* already treats a record whose keys disagree with their members — unless `merge` says how the two
|
|
128
|
+
* combine, in which case the member is `merge(earlier, later)`, placed where the later one was.
|
|
128
129
|
*/
|
|
129
|
-
function orList(collection) {
|
|
130
|
+
function orList(collection, { merge } = {}) {
|
|
130
131
|
const key = collection.key;
|
|
131
132
|
if (!key) throw new Error(`orList: "${collection.name}" declares no \`key\`, so a list of its members has nothing to be keyed by.`);
|
|
132
133
|
const listed = z.array(collection.valueType).transform((members) => {
|
|
133
134
|
const keyed = {};
|
|
134
|
-
for (const member of members)
|
|
135
|
+
for (const member of members) {
|
|
136
|
+
const at = key(member);
|
|
137
|
+
const earlier = keyed[at];
|
|
138
|
+
if (earlier !== void 0 && merge) {
|
|
139
|
+
delete keyed[at];
|
|
140
|
+
keyed[at] = merge(earlier, member);
|
|
141
|
+
} else keyed[at] = member;
|
|
142
|
+
}
|
|
135
143
|
return keyed;
|
|
136
144
|
});
|
|
137
145
|
return z.union([listed, collection]);
|
|
@@ -476,6 +476,7 @@ function authoredValue(config, key, value) {
|
|
|
476
476
|
function authoredLeaf(config, key, stored) {
|
|
477
477
|
const path = pathOf(stored);
|
|
478
478
|
if (kindOf(stored) === "token") return stylePropLeafForToken(config, key, path);
|
|
479
|
+
if (kindOf(stored) === "motion") return path;
|
|
479
480
|
const slash = path.indexOf("/");
|
|
480
481
|
return slash === -1 ? path : path.slice(slash + 1);
|
|
481
482
|
}
|
|
@@ -92,6 +92,10 @@ interface ResolveContext {
|
|
|
92
92
|
* one — since it names a derived thing, not an entity.
|
|
93
93
|
*/
|
|
94
94
|
declare function wrappedElementKey(wrapper: string, type: string): string;
|
|
95
|
+
/** The factory a layer renders through when a style rule gives it a JS preset. The registry holds a
|
|
96
|
+
* pair of it with every element a layer renders, when the system has a JS preset (codegen's
|
|
97
|
+
* registry emitter). */
|
|
98
|
+
declare const MOTION_FACTORY = "package:motion/react#motion";
|
|
95
99
|
/**
|
|
96
100
|
* The node a tree ENTERS at — the one nothing else claims as a child.
|
|
97
101
|
*
|
|
@@ -497,4 +501,4 @@ declare function expandSpec(config: Config, spec: RenderSpec, options?: ExpandOp
|
|
|
497
501
|
*/
|
|
498
502
|
declare function expandComponent(config: Config, path: string, props?: Record<string, unknown>, options?: ExpandOptions): ExpansionResult;
|
|
499
503
|
//#endregion
|
|
500
|
-
export { ANATOMY_MARKER, ComponentSpec, ComponentSpecs, ComputedFunctions, ComputedValue, ExpandOptions, ExpansionResult, RenderElement, RenderSpec, RepeatRow, RepeatSpec, ResolveContext, SlotTarget, VisibilityOperator, VisibilityTerm, anatomyElementComponent, anatomyTextProp, boundElementType, componentSpec, deriveSpec, entryKeyOf, expandComponent, expandSpec, instanceSpec, layerTextProp, liveStateProps, previewSpec, propInto, propsIntoLayer, readStatePointer, resolveValue, resolveVisibility, routeOf, routesOf, slotTargetsOf, specWithResolvedVisibility, statePointerProp, valueForwardsOf, visibilityStateProps, visibilityTerms, withoutTruthyTerm, wrappedElementKey };
|
|
504
|
+
export { ANATOMY_MARKER, ComponentSpec, ComponentSpecs, ComputedFunctions, ComputedValue, ExpandOptions, ExpansionResult, MOTION_FACTORY, RenderElement, RenderSpec, RepeatRow, RepeatSpec, ResolveContext, SlotTarget, VisibilityOperator, VisibilityTerm, anatomyElementComponent, anatomyTextProp, boundElementType, componentSpec, deriveSpec, entryKeyOf, expandComponent, expandSpec, instanceSpec, layerTextProp, liveStateProps, previewSpec, propInto, propsIntoLayer, readStatePointer, resolveValue, resolveVisibility, routeOf, routesOf, slotTargetsOf, specWithResolvedVisibility, statePointerProp, valueForwardsOf, visibilityStateProps, visibilityTerms, withoutTruthyTerm, wrappedElementKey };
|