@yahoo/uds-create-config 5.25.2 → 5.27.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/react-native-system.d.ts +22 -4
- package/dist/configs/system.js +4 -0
- package/dist/entities/native/NativeToken.d.ts +22 -4
- package/dist/entities/native/NativeToken.js +2 -1
- package/dist/entities/system/Component.d.ts +35 -2
- package/dist/entities/system/Component.js +224 -9
- package/dist/entities/system/LinkedSystem.js +3 -2
- package/dist/entities/system/Settings.js +1 -1
- package/dist/entities/system/StyleProperty.js +1 -1
- package/dist/entities/system/Token.d.ts +22 -4
- package/dist/entities/system/Token.js +4 -3
- package/dist/entities/system/component-module.d.ts +17 -1
- package/dist/entities/system/component-module.js +54 -2
- package/dist/entities/system/defineComponent.d.ts +53 -13
- package/dist/entities/system/defineComponent.js +1 -0
- package/dist/entities/system/forward-route-invariants.js +1 -1
- package/dist/entities/system/icon-domain-invariants.js +1 -1
- package/dist/entities/system/root-layer-invariants.js +1 -1
- package/dist/entities/system/token-values.js +1 -1
- package/dist/framework/Config.d.ts +1 -1
- package/dist/framework/Config.js +33 -2
- package/dist/framework/class-names.d.ts +27 -1
- package/dist/framework/class-names.js +56 -3
- package/dist/framework/component-states.d.ts +40 -0
- package/dist/framework/component-states.js +53 -0
- package/dist/framework/config-ops.d.ts +1 -1
- package/dist/framework/config-ops.js +25 -4
- package/dist/framework/defineSubEntity.d.ts +7 -0
- package/dist/framework/defineSubEntity.js +9 -0
- package/dist/framework/interaction-conditions.d.ts +48 -0
- package/dist/framework/interaction-conditions.js +104 -0
- package/dist/framework/layer-styles.d.ts +5 -1
- package/dist/framework/layer-styles.js +18 -4
- package/dist/framework/projections.js +44 -2
- package/dist/framework/render-spec.js +14 -4
- package/dist/framework/schema-version.d.ts +1 -1
- package/dist/framework/schema-version.js +5 -5
- package/dist/framework/utils/deprecated.d.ts +18 -0
- package/dist/framework/utils/deprecated.js +12 -0
- package/dist/framework/validate-spec.js +14 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +6 -4
- package/dist/migrations/20261013154037_component_states.d.ts +19 -0
- package/dist/migrations/20261013154037_component_states.js +74 -0
- package/dist/migrations/20261013164300_token_overrides_list.d.ts +7 -0
- package/dist/migrations/20261013164300_token_overrides_list.js +10 -0
- package/dist/renderer/componentEntry.d.ts +7 -1
- package/dist/renderer/componentEntry.js +30 -1
- package/dist/renderer/forced-stamp.d.ts +26 -0
- package/dist/renderer/forced-stamp.js +50 -0
- package/dist/renderer/index.d.ts +3 -1
- package/dist/renderer/index.js +3 -1
- package/dist/renderer/use-forced-stamp.d.ts +11 -0
- package/dist/renderer/use-forced-stamp.js +50 -0
- package/dist/spec/specToJsx.d.ts +6 -0
- package/dist/spec/specToJsx.js +5 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
|
@@ -75,6 +75,19 @@ function authoringSignature(schema, options) {
|
|
|
75
75
|
const json = jsonSchema(schema);
|
|
76
76
|
return json === void 0 ? void 0 : renderSignature(withoutMetadata(json), options);
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* A real entry, spelled the way it is authored. A collection whose schema offers only a list (see
|
|
80
|
+
* `withDeprecated`) is stored as the record keyed by its members, so the example writes its members
|
|
81
|
+
* as that list, and a reader copying it never learns the deprecated record.
|
|
82
|
+
*/
|
|
83
|
+
function authoredExample({ item, fields }) {
|
|
84
|
+
const body = item.toJSON();
|
|
85
|
+
const properties = fields?.properties;
|
|
86
|
+
if (!properties) return { [item.path]: body };
|
|
87
|
+
const authored = { ...body };
|
|
88
|
+
for (const [field, value] of Object.entries(body)) if (properties[field]?.type === "array" && value && typeof value === "object" && !Array.isArray(value)) authored[field] = Object.values(value);
|
|
89
|
+
return { [item.path]: authored };
|
|
90
|
+
}
|
|
78
91
|
/** Every owned kind's authoring shape as signatures, with a real example where the config has one. */
|
|
79
92
|
function authoringSignatures(config, { depth, memberDepth = depth } = {}) {
|
|
80
93
|
return config.ownedKinds().flatMap((kind) => {
|
|
@@ -87,14 +100,18 @@ function authoringSignatures(config, { depth, memberDepth = depth } = {}) {
|
|
|
87
100
|
if (signature) collections[field] = singleton ? `one section, no key (\`sub-update/${field}\`): ${signature}` : signature;
|
|
88
101
|
}
|
|
89
102
|
const first = config.list(kind)[0];
|
|
103
|
+
const create = dataSchemaOf(config, entity.schemas.create);
|
|
90
104
|
return [{
|
|
91
105
|
kind,
|
|
92
106
|
label: entity.label,
|
|
93
107
|
refHelper: entity.entityName,
|
|
94
|
-
create: authoringSignature(
|
|
108
|
+
create: authoringSignature(create, { depth }),
|
|
95
109
|
collections,
|
|
96
110
|
group: authoringSignature(entity.groupCreateSchema, { depth }),
|
|
97
|
-
example: first ? {
|
|
111
|
+
example: first ? authoredExample({
|
|
112
|
+
item: first,
|
|
113
|
+
fields: jsonSchema(create)
|
|
114
|
+
}) : void 0
|
|
98
115
|
}];
|
|
99
116
|
});
|
|
100
117
|
}
|
|
@@ -137,14 +154,18 @@ function authoringReference(config) {
|
|
|
137
154
|
collections[field] = jsonSchema(dataSchemaOf(config, entity.schemas[`sub-create/${field}`]));
|
|
138
155
|
}
|
|
139
156
|
const first = config.list(kind)[0];
|
|
157
|
+
const fields = jsonSchema(dataSchemaOf(config, entity.schemas.create));
|
|
140
158
|
return [{
|
|
141
159
|
kind,
|
|
142
160
|
label: entity.label,
|
|
143
161
|
refHelper: entity.entityName,
|
|
144
|
-
fields
|
|
162
|
+
fields,
|
|
145
163
|
collections,
|
|
146
164
|
groupFields: jsonSchema(entity.groupCreateSchema),
|
|
147
|
-
example: first ? {
|
|
165
|
+
example: first ? authoredExample({
|
|
166
|
+
item: first,
|
|
167
|
+
fields
|
|
168
|
+
}) : void 0
|
|
148
169
|
}];
|
|
149
170
|
}) };
|
|
150
171
|
}
|
|
@@ -36,6 +36,9 @@ type SubEntityClass<F extends z.ZodTypeAny = z.ZodTypeAny> = CollectionSchema<F>
|
|
|
36
36
|
* caller, so key and value can't drift, two members with the same identity can't both exist, and
|
|
37
37
|
* an edit that changes the identity moves the entry. Omit when the key is arbitrary. */
|
|
38
38
|
readonly key?: (member: unknown) => string;
|
|
39
|
+
/** The collection is authored as a list of its members, which carry their own `key`. The keyed
|
|
40
|
+
* record it is stored as is still read but no longer offered: see {@link orList}. */
|
|
41
|
+
readonly listed?: true;
|
|
39
42
|
/** Whether a member DECLARES nothing — a style rule whose every layer bag is empty. Asked only
|
|
40
43
|
* when a \`sub-update\` would bring the member into being: a delta made entirely of removals has
|
|
41
44
|
* nothing to remove from a member that is not there, so creating one files an entry that emits
|
|
@@ -143,6 +146,7 @@ declare function defineSubEntity<F extends z.ZodTypeAny>(spec: {
|
|
|
143
146
|
*/
|
|
144
147
|
base?: z.ZodTypeAny;
|
|
145
148
|
key?: (member: unknown) => string;
|
|
149
|
+
listed?: true;
|
|
146
150
|
vacant?: (member: unknown) => boolean;
|
|
147
151
|
schemas?: UserSchemas;
|
|
148
152
|
namespaces?: string;
|
|
@@ -161,6 +165,9 @@ declare function defineSubEntity<F extends z.ZodTypeAny>(spec: {
|
|
|
161
165
|
* Two members with the same key are one member, the later one, as the key derivation on write
|
|
162
166
|
* already treats a record whose keys disagree with their members — unless `merge` says how the two
|
|
163
167
|
* combine, in which case the member is `merge(earlier, later)`, placed where the later one was.
|
|
168
|
+
*
|
|
169
|
+
* A collection declared `listed` still takes the keyed record but offers only the list, see
|
|
170
|
+
* {@link withDeprecated}.
|
|
164
171
|
*/
|
|
165
172
|
declare function orList<F extends z.ZodTypeAny>(collection: SubEntityClass<F>, {
|
|
166
173
|
merge
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { kindOf, memberOf, memberRef, pathOf } from "./utils/refs.js";
|
|
2
2
|
import { resolveSchema } from "./schemas.js";
|
|
3
3
|
import { titleCase } from "./registry.js";
|
|
4
|
+
import { withDeprecated } from "./utils/deprecated.js";
|
|
4
5
|
import { z } from "zod";
|
|
5
6
|
//#region src/framework/defineSubEntity.ts
|
|
6
7
|
/**
|
|
@@ -78,6 +79,7 @@ function defineSubEntity(spec) {
|
|
|
78
79
|
fields: spec.fields,
|
|
79
80
|
base: spec.base,
|
|
80
81
|
key: spec.key,
|
|
82
|
+
...spec.listed ? { listed: true } : {},
|
|
81
83
|
vacant: spec.vacant,
|
|
82
84
|
schemas: spec.schemas,
|
|
83
85
|
namespaces: spec.namespaces,
|
|
@@ -126,6 +128,9 @@ function defineSubEntity(spec) {
|
|
|
126
128
|
* Two members with the same key are one member, the later one, as the key derivation on write
|
|
127
129
|
* already treats a record whose keys disagree with their members — unless `merge` says how the two
|
|
128
130
|
* combine, in which case the member is `merge(earlier, later)`, placed where the later one was.
|
|
131
|
+
*
|
|
132
|
+
* A collection declared `listed` still takes the keyed record but offers only the list, see
|
|
133
|
+
* {@link withDeprecated}.
|
|
129
134
|
*/
|
|
130
135
|
function orList(collection, { merge } = {}) {
|
|
131
136
|
const key = collection.key;
|
|
@@ -142,6 +147,10 @@ function orList(collection, { merge } = {}) {
|
|
|
142
147
|
}
|
|
143
148
|
return keyed;
|
|
144
149
|
});
|
|
150
|
+
if (collection.listed) return withDeprecated({
|
|
151
|
+
offered: listed,
|
|
152
|
+
deprecated: collection
|
|
153
|
+
});
|
|
145
154
|
return z.union([listed, collection]);
|
|
146
155
|
}
|
|
147
156
|
//#endregion
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Config } from "./Config.js";
|
|
2
|
+
|
|
3
|
+
//#region src/framework/interaction-conditions.d.ts
|
|
4
|
+
/** One interaction a rule keys on: the bag key, the modifier it resolves to, and whose it is. */
|
|
5
|
+
interface InteractionCondition {
|
|
6
|
+
/** As written in `when`: `_hover`. */
|
|
7
|
+
readonly key: string;
|
|
8
|
+
/** The modifier's path: `state/hover`. */
|
|
9
|
+
readonly modifier: string;
|
|
10
|
+
/** The layer whose interaction it is — the root for `true`. */
|
|
11
|
+
readonly layer: string;
|
|
12
|
+
}
|
|
13
|
+
/** Whether `modifier` (a path or leaf) can be a rule's interaction condition. */
|
|
14
|
+
declare function isInteractionModifier(config: Config, modifier: string): boolean;
|
|
15
|
+
/** The `when` key an interaction modifier is written under: its leaf, `_hover`. */
|
|
16
|
+
declare function interactionKey(modifier: string): string;
|
|
17
|
+
/** Every interaction modifier the config declares, by path. */
|
|
18
|
+
declare function interactionModifiers(config: Config): ReadonlySet<string>;
|
|
19
|
+
/**
|
|
20
|
+
* A rule's condition split in two: the props and states the render tests, and the interactions the
|
|
21
|
+
* stylesheet (or, for what CSS can't do, the module's own event tracking) holds it under.
|
|
22
|
+
*/
|
|
23
|
+
declare function whenParts({
|
|
24
|
+
config,
|
|
25
|
+
when,
|
|
26
|
+
layers
|
|
27
|
+
}: {
|
|
28
|
+
config: Config;
|
|
29
|
+
when: Readonly<Record<string, unknown>> | null | undefined; /** The component's layers, for the root an interaction's `true` names. */
|
|
30
|
+
layers?: Readonly<Record<string, unknown>>;
|
|
31
|
+
}): {
|
|
32
|
+
props: Record<string, unknown>;
|
|
33
|
+
interactions: InteractionCondition[];
|
|
34
|
+
};
|
|
35
|
+
/** Whether `when` names any interaction — what tells the readers a rule holds only under one. */
|
|
36
|
+
declare function hasInteraction(config: Config, when: Readonly<Record<string, unknown>> | null | undefined): boolean;
|
|
37
|
+
/** One interaction a component's rules key on, with the selector that tells whether it holds. */
|
|
38
|
+
interface ComponentInteraction extends InteractionCondition {
|
|
39
|
+
/** The modifier's own selector, without its leading `&` (`:hover`): what `element.matches` asks. */
|
|
40
|
+
readonly selector: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Every interaction a component's rules key on, once per key and layer — what a surface that renders
|
|
44
|
+
* the component from its config, with no module to track them, watches on the live element.
|
|
45
|
+
*/
|
|
46
|
+
declare function componentInteractions(config: Config, path: string): ComponentInteraction[];
|
|
47
|
+
//#endregion
|
|
48
|
+
export { ComponentInteraction, InteractionCondition, componentInteractions, hasInteraction, interactionKey, interactionModifiers, isInteractionModifier, whenParts };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { modifierCategory } from "../entities/system/modifier-axis.js";
|
|
2
|
+
import { rootLayerOf } from "../entities/system/element.js";
|
|
3
|
+
import { createSliceMemo } from "./memo.js";
|
|
4
|
+
//#region src/framework/interaction-conditions.ts
|
|
5
|
+
/**
|
|
6
|
+
* A style rule's interaction conditions: `when: { _hover: true }`, "while the component is hovered".
|
|
7
|
+
*
|
|
8
|
+
* A rule's `when` names props and states, which the render knows. An interaction is the browser's —
|
|
9
|
+
* the pointer is over an element, it is pressed, it has focus — and a rule may key on one the same
|
|
10
|
+
* way, so the rule's whole reach (styles, an instance's props, content) applies while it holds. The
|
|
11
|
+
* value names whose interaction it is: `true` is the component's own root, and a layer name is that
|
|
12
|
+
* layer (`_hover: 'closeButton'`, "while the close button is hovered").
|
|
13
|
+
*
|
|
14
|
+
* Only an interaction modifier qualifies: a selector modifier that isn't a mode and names a state of
|
|
15
|
+
* the element itself (`:hover`, `[aria-checked="true"]`), never a pseudo-element (`::placeholder`)
|
|
16
|
+
* or a relation to another element (`[data-group]:hover &`).
|
|
17
|
+
*/
|
|
18
|
+
/** Whether `modifier` (a path or leaf) can be a rule's interaction condition. */
|
|
19
|
+
function isInteractionModifier(config, modifier) {
|
|
20
|
+
return interactionModifiers(config).has(config.resolve("modifier", modifier)?.path ?? "");
|
|
21
|
+
}
|
|
22
|
+
/** The `when` key an interaction modifier is written under: its leaf, `_hover`. */
|
|
23
|
+
function interactionKey(modifier) {
|
|
24
|
+
return `_${modifier.slice(modifier.lastIndexOf("/") + 1)}`;
|
|
25
|
+
}
|
|
26
|
+
/** Every interaction modifier the config declares, by path. */
|
|
27
|
+
function interactionModifiers(config) {
|
|
28
|
+
return interactionMemo(config);
|
|
29
|
+
}
|
|
30
|
+
const interactionMemo = createSliceMemo({
|
|
31
|
+
kinds: ["modifier"],
|
|
32
|
+
compute: (config) => {
|
|
33
|
+
const paths = /* @__PURE__ */ new Set();
|
|
34
|
+
for (const modifier of config.list("modifier")) {
|
|
35
|
+
const { type, value } = modifier.toJSON() ?? {};
|
|
36
|
+
if (type !== "selector" || typeof value !== "string") continue;
|
|
37
|
+
if (modifierCategory(config, modifier.path) === "mode") continue;
|
|
38
|
+
const own = value.trim().replace(/^&/, "");
|
|
39
|
+
if (own.includes("::") || own.includes("&") || /\s/.test(own)) continue;
|
|
40
|
+
paths.add(modifier.path);
|
|
41
|
+
}
|
|
42
|
+
return paths;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* A rule's condition split in two: the props and states the render tests, and the interactions the
|
|
47
|
+
* stylesheet (or, for what CSS can't do, the module's own event tracking) holds it under.
|
|
48
|
+
*/
|
|
49
|
+
function whenParts({ config, when, layers }) {
|
|
50
|
+
const props = {};
|
|
51
|
+
const interactions = [];
|
|
52
|
+
const root = layers ? rootLayerOf(layers)[0] : "root";
|
|
53
|
+
for (const [key, value] of Object.entries(when ?? {})) {
|
|
54
|
+
const modifier = key.startsWith("_") ? config.resolve("modifier", key.slice(1)) : void 0;
|
|
55
|
+
if (modifier && interactionModifiers(config).has(modifier.path)) {
|
|
56
|
+
if (value === false || value === "false" || value == null) continue;
|
|
57
|
+
interactions.push({
|
|
58
|
+
key,
|
|
59
|
+
modifier: modifier.path,
|
|
60
|
+
layer: value === true || value === "true" ? root : String(value)
|
|
61
|
+
});
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
props[key] = value;
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
props,
|
|
68
|
+
interactions
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** Whether `when` names any interaction — what tells the readers a rule holds only under one. */
|
|
72
|
+
function hasInteraction(config, when) {
|
|
73
|
+
return whenParts({
|
|
74
|
+
config,
|
|
75
|
+
when
|
|
76
|
+
}).interactions.length > 0;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Every interaction a component's rules key on, once per key and layer — what a surface that renders
|
|
80
|
+
* the component from its config, with no module to track them, watches on the live element.
|
|
81
|
+
*/
|
|
82
|
+
function componentInteractions(config, path) {
|
|
83
|
+
const body = config.resolve("component", path)?.toJSON();
|
|
84
|
+
const out = [];
|
|
85
|
+
for (const rule of Object.values(body?.styles ?? {})) {
|
|
86
|
+
const { interactions } = whenParts({
|
|
87
|
+
config,
|
|
88
|
+
when: rule.when,
|
|
89
|
+
layers: body?.layers
|
|
90
|
+
});
|
|
91
|
+
for (const interaction of interactions) {
|
|
92
|
+
if (out.some((seen) => seen.key === interaction.key && seen.layer === interaction.layer)) continue;
|
|
93
|
+
const value = (config.resolve("modifier", interaction.modifier)?.toJSON())?.value;
|
|
94
|
+
if (typeof value !== "string") continue;
|
|
95
|
+
out.push({
|
|
96
|
+
...interaction,
|
|
97
|
+
selector: value.trim().replace(/^&/, "")
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
export { componentInteractions, hasInteraction, interactionKey, interactionModifiers, isInteractionModifier, whenParts };
|
|
@@ -2,7 +2,11 @@ import { Config } from "./Config.js";
|
|
|
2
2
|
|
|
3
3
|
//#region src/framework/layer-styles.d.ts
|
|
4
4
|
/** Whether every prop a rule names holds the value it asks for. Compared as strings, because a
|
|
5
|
-
* condition is canonicalized into the rule's key and a variant value is a string there.
|
|
5
|
+
* condition is canonicalized into the rule's key and a variant value is a string there.
|
|
6
|
+
*
|
|
7
|
+
* An interaction (`_hover`, see `interaction-conditions`) holds only when the state says it does —
|
|
8
|
+
* `{ _hover: true }`, a surface forcing it — whichever layer the rule names: no prop bag is ever
|
|
9
|
+
* hovered, so at rest the rule is out. */
|
|
6
10
|
declare function ruleApplies(when: Record<string, unknown> | null | undefined, state: Record<string, unknown>): boolean;
|
|
7
11
|
interface LayerStyleOptions {
|
|
8
12
|
/**
|
|
@@ -2,9 +2,20 @@ import { isRef, refLeaf } from "./utils/refs.js";
|
|
|
2
2
|
import { authoredBag } from "./projections.js";
|
|
3
3
|
//#region src/framework/layer-styles.ts
|
|
4
4
|
/** Whether every prop a rule names holds the value it asks for. Compared as strings, because a
|
|
5
|
-
* condition is canonicalized into the rule's key and a variant value is a string there.
|
|
5
|
+
* condition is canonicalized into the rule's key and a variant value is a string there.
|
|
6
|
+
*
|
|
7
|
+
* An interaction (`_hover`, see `interaction-conditions`) holds only when the state says it does —
|
|
8
|
+
* `{ _hover: true }`, a surface forcing it — whichever layer the rule names: no prop bag is ever
|
|
9
|
+
* hovered, so at rest the rule is out. */
|
|
6
10
|
function ruleApplies(when, state) {
|
|
7
|
-
return Object.entries(when ?? {}).every(([prop, value]) => String(state[prop]) === String(refLeaf(value)));
|
|
11
|
+
return Object.entries(when ?? {}).every(([prop, value]) => prop.startsWith("_") ? interactionHolds(state, prop, value) : String(state[prop]) === String(refLeaf(value)));
|
|
12
|
+
}
|
|
13
|
+
/** Whether an interaction holds: forced for the whole component (`_hover: true`, a surface's pin), or
|
|
14
|
+
* reported for the layer the condition names (`'_hover@close': true`, a live pointer). */
|
|
15
|
+
function interactionHolds(state, key, value) {
|
|
16
|
+
const on = (held) => held === true || held === "true";
|
|
17
|
+
if (on(state[key])) return true;
|
|
18
|
+
return on(state[`${key}@${value === true || value === "true" ? "root" : String(value)}`]);
|
|
8
19
|
}
|
|
9
20
|
/**
|
|
10
21
|
* Merge a bag over another, flattening any forced modifier block as it goes.
|
|
@@ -14,15 +25,17 @@ function ruleApplies(when, state) {
|
|
|
14
25
|
* with" for a panel and a matrix cell, and a picker's options are leaves.
|
|
15
26
|
*/
|
|
16
27
|
function mergeBag(into, bag, forced) {
|
|
28
|
+
const forcedBlocks = [];
|
|
17
29
|
for (const [key, value] of Object.entries(bag)) {
|
|
18
30
|
const modifier = key.startsWith("_") ? key.slice(1) : void 0;
|
|
19
31
|
if (modifier && value && typeof value === "object" && !isRef(value)) {
|
|
20
|
-
if (forced.includes(modifier))
|
|
32
|
+
if (forced.includes(modifier)) forcedBlocks.push(value);
|
|
21
33
|
else into[key] = value;
|
|
22
34
|
continue;
|
|
23
35
|
}
|
|
24
36
|
into[key] = value;
|
|
25
37
|
}
|
|
38
|
+
for (const block of forcedBlocks) mergeBag(into, block, forced);
|
|
26
39
|
}
|
|
27
40
|
/**
|
|
28
41
|
* The resolved style bag per layer, for the props an instance was given.
|
|
@@ -36,7 +49,8 @@ function layerStyles(config, path, props = {}, options = {}) {
|
|
|
36
49
|
...defaultProps,
|
|
37
50
|
...props
|
|
38
51
|
};
|
|
39
|
-
const forced = options.forced ?? [];
|
|
52
|
+
const forced = (options.forced ?? []).map((name) => name.replace(/^_/, ""));
|
|
53
|
+
for (const name of forced) state[`_${name}`] = true;
|
|
40
54
|
const applicable = Object.values(styles).filter((rule) => ruleApplies(rule.when, state)).sort((left, right) => Object.keys(left.when ?? {}).length - Object.keys(right.when ?? {}).length);
|
|
41
55
|
const out = {};
|
|
42
56
|
for (const rule of applicable) for (const [layer, bag] of Object.entries(rule.layers ?? {})) {
|
|
@@ -4,12 +4,13 @@ import { isOklchColor, renderOklch } from "../entities/system/color.js";
|
|
|
4
4
|
import { isComposedValue, renderComposedValue } from "../entities/system/composed-values.js";
|
|
5
5
|
import { modifierCategory } from "../entities/system/modifier-axis.js";
|
|
6
6
|
import { forwardedPropName, forwardsOf, rootLayerOf } from "../entities/system/element.js";
|
|
7
|
+
import { createSliceMemo } from "./memo.js";
|
|
7
8
|
import { fontFamilyStack } from "../entities/system/Font.js";
|
|
8
9
|
import { listedIn } from "./utils/enumerated.js";
|
|
9
10
|
import "../entities/system/Modifier.js";
|
|
10
|
-
import { createSliceMemo } from "./memo.js";
|
|
11
11
|
import { classPrefixOf, prefixed, varPrefixOf } from "../entities/system/Settings.js";
|
|
12
12
|
import { StyleProperty, leavesOfStylePropValue, numberUnitRefusal } from "../entities/system/StyleProperty.js";
|
|
13
|
+
import { componentStates } from "./component-states.js";
|
|
13
14
|
import { isConditionalPreview, storedPreview, unconditionalPreviewProps } from "./preview-store.js";
|
|
14
15
|
//#region src/framework/projections.ts
|
|
15
16
|
/**
|
|
@@ -797,6 +798,10 @@ function authoredBag(config, bag) {
|
|
|
797
798
|
continue;
|
|
798
799
|
}
|
|
799
800
|
}
|
|
801
|
+
if (isComposedValue(value)) {
|
|
802
|
+
out[key] = value;
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
800
805
|
if (value && typeof value === "object" && !Array.isArray(value) && !isRef(value)) {
|
|
801
806
|
const inner = authoredBag(config, value);
|
|
802
807
|
out[key] = inner;
|
|
@@ -1792,8 +1797,34 @@ function validateComponentProps(config, componentPath, props) {
|
|
|
1792
1797
|
value: props[LAYER_PROPS],
|
|
1793
1798
|
where: componentPath
|
|
1794
1799
|
});
|
|
1800
|
+
const states = Object.fromEntries(componentStates(config, componentPath).map((state) => [state.name, {
|
|
1801
|
+
type: state.type,
|
|
1802
|
+
value: state.values
|
|
1803
|
+
}]));
|
|
1804
|
+
const checkState = (name, value) => {
|
|
1805
|
+
if (name.startsWith("_")) {
|
|
1806
|
+
if (modifierCategory(config, name.slice(1)) !== "state") errors.push(`\`${name}\` is not a state modifier`);
|
|
1807
|
+
else if (!["true", "false"].includes(String(value))) errors.push(`\`${name}\` is \`true\` or \`false\`, not \`${String(value)}\``);
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
const state = states[name];
|
|
1811
|
+
if (!state) {
|
|
1812
|
+
errors.push(`\`${name}\` is not a state`);
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
const allowed = state.type === "variant" && Array.isArray(state.value) ? state.value.map(String) : ["true", "false"];
|
|
1816
|
+
if (!allowed.includes(String(value))) errors.push(`state \`${name}\` is ${allowed.map((v) => `\`${v}\``).join(" or ")}, not \`${String(value)}\``);
|
|
1817
|
+
};
|
|
1795
1818
|
for (const [name, value] of Object.entries(props)) {
|
|
1796
1819
|
if (name === "layerProps") continue;
|
|
1820
|
+
if (name === "__udsForceStates") {
|
|
1821
|
+
for (const [pinned, pin] of Object.entries(value ?? {})) checkState(pinned, pin);
|
|
1822
|
+
continue;
|
|
1823
|
+
}
|
|
1824
|
+
if (states[name]) {
|
|
1825
|
+
errors.push(`\`${name}\` is a state, not a prop — pin it as \`__udsForceStates: { ${name}: … }\``);
|
|
1826
|
+
continue;
|
|
1827
|
+
}
|
|
1797
1828
|
const decl = declared[name];
|
|
1798
1829
|
if (!decl) {
|
|
1799
1830
|
errors.push(`unknown prop \`${name}\``);
|
|
@@ -1957,7 +1988,13 @@ function previewMatrix(config, path) {
|
|
|
1957
1988
|
props[prop] = values;
|
|
1958
1989
|
};
|
|
1959
1990
|
for (const rule of Object.values(styles)) {
|
|
1960
|
-
for (const [prop, stored] of Object.entries(rule.when ?? {}))
|
|
1991
|
+
for (const [prop, stored] of Object.entries(rule.when ?? {})) {
|
|
1992
|
+
if (prop.startsWith("_")) {
|
|
1993
|
+
if (modifierCategory(config, prop.slice(1)) === "state") modifiers.add(prop);
|
|
1994
|
+
continue;
|
|
1995
|
+
}
|
|
1996
|
+
nameAxisPoint(prop, stored);
|
|
1997
|
+
}
|
|
1961
1998
|
for (const bag of Object.values(rule.layers ?? {})) for (const key of Object.keys(bag ?? {})) {
|
|
1962
1999
|
if (!key.startsWith("_")) continue;
|
|
1963
2000
|
if (modifierCategory(config, key.slice(1)) === "state") modifiers.add(key);
|
|
@@ -1975,6 +2012,10 @@ function previewMatrix(config, path) {
|
|
|
1975
2012
|
if (values.length > 0) props[name] = values;
|
|
1976
2013
|
}
|
|
1977
2014
|
}
|
|
2015
|
+
for (const state of componentStates(config, path)) {
|
|
2016
|
+
if (state.type === "boolean") props[state.name] = declaredFirst(["false", "true"], props[state.name] ?? []);
|
|
2017
|
+
if (state.type === "variant") props[state.name] = declaredFirst([...state.values ?? []], props[state.name] ?? []);
|
|
2018
|
+
}
|
|
1978
2019
|
return {
|
|
1979
2020
|
props,
|
|
1980
2021
|
modifiers: [...modifiers]
|
|
@@ -1995,6 +2036,7 @@ function declaredFirst(declared, ruleNamed) {
|
|
|
1995
2036
|
function propValueFromAxis(config, path, prop, value) {
|
|
1996
2037
|
const declared = resolveComponentProps(config, path)[prop];
|
|
1997
2038
|
if (declared?.type === "boolean") return value === "true";
|
|
2039
|
+
if (componentStates(config, path).some((state) => state.name === prop && state.type === "boolean")) return value === "true";
|
|
1998
2040
|
if (declared?.type === "number") {
|
|
1999
2041
|
const parsed = Number(value);
|
|
2000
2042
|
return Number.isFinite(parsed) ? parsed : value;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isRef, kindOf, memberOf, pathOf, ref, splitRef } from "./utils/refs.js";
|
|
2
2
|
import { renderedElement } from "../entities/system/element.js";
|
|
3
3
|
import { createSliceMemo } from "./memo.js";
|
|
4
|
+
import { componentStateDefaults, componentStates } from "./component-states.js";
|
|
4
5
|
import { unconditionalPreviewProps } from "./preview-store.js";
|
|
5
6
|
import { authoredBag, authoredValue, declaredPropDefaults, memberFromLeaf, refListOf, resolveComponentProps, stylePropertyPathFor } from "./projections.js";
|
|
6
7
|
import { slotHostLayer } from "./slot-host.js";
|
|
@@ -8,7 +9,7 @@ import { forwardRoutes, forwardedLayers, intoName, isValueSlot, renderBindingsOf
|
|
|
8
9
|
import { withRequiredContentHidden } from "../entities/system/required-content-layers.js";
|
|
9
10
|
import { inlineMotionProps, jsMotionPresets } from "../entities/system/motion-presets.js";
|
|
10
11
|
import { graftPreviewElements } from "../spec/preview-elements.js";
|
|
11
|
-
import { componentLayerClass, componentPropClasses, componentRuleClasses, layerJsMotionPresets, layerRoutedProps, splitStyleProps, styleRuleMotionClasses } from "./class-names.js";
|
|
12
|
+
import { componentLayerClass, componentPropClasses, componentRuleClasses, componentStateAttributes, forcedModifierAttributes, layerJsMotionPresets, layerRoutedProps, splitStyleProps, styleRuleMotionClasses } from "./class-names.js";
|
|
12
13
|
//#region src/framework/render-spec.ts
|
|
13
14
|
/** A repeat source that is one prop binding: `{ "$state": "/stops" }`, settled by the cell alone. */
|
|
14
15
|
function isStateSource(source) {
|
|
@@ -1202,11 +1203,14 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
|
|
|
1202
1203
|
const filled = {};
|
|
1203
1204
|
for (const [from, fills] of Object.entries(instanceSlots ?? {})) if (fills.length > 0 && instanceProps[from] === void 0) filled[from] = true;
|
|
1204
1205
|
const declaredDefaults = declaredPropDefaults(body.props);
|
|
1206
|
+
const pins = instanceProps["__udsForceStates"] ?? {};
|
|
1205
1207
|
const state = {
|
|
1206
1208
|
...declaredDefaults,
|
|
1207
1209
|
...body.defaultProps,
|
|
1210
|
+
...componentStateDefaults(config, path),
|
|
1208
1211
|
...filled,
|
|
1209
|
-
...instanceProps
|
|
1212
|
+
...instanceProps,
|
|
1213
|
+
...Object.fromEntries(Object.entries(pins).map(([key, value]) => key.startsWith("_") ? [key, value === true || value === "true"] : [key, value]))
|
|
1210
1214
|
};
|
|
1211
1215
|
const consumed = consumedProps(config, path, body);
|
|
1212
1216
|
const { byLayer: targets } = slotTargets(config, path, body);
|
|
@@ -1222,8 +1226,12 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
|
|
|
1222
1226
|
...body.defaultProps ?? {},
|
|
1223
1227
|
...instanceProps
|
|
1224
1228
|
};
|
|
1229
|
+
const stateNames = new Set(componentStates(config, path).map(({ name }) => name));
|
|
1225
1230
|
const bag = {};
|
|
1226
|
-
for (const [name, value] of Object.entries(supplied))
|
|
1231
|
+
for (const [name, value] of Object.entries(supplied)) {
|
|
1232
|
+
if (name === "__udsForceStates") continue;
|
|
1233
|
+
if (!consumed.has(name) && !stateNames.has(name)) bag[name] = value;
|
|
1234
|
+
}
|
|
1227
1235
|
const { className: styleClasses, rest } = splitStyleProps({
|
|
1228
1236
|
config,
|
|
1229
1237
|
bag,
|
|
@@ -1352,6 +1360,8 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
|
|
|
1352
1360
|
...forwarded[layer],
|
|
1353
1361
|
...motionPreset !== void 0 ? withoutKey(presetProps[motionPreset], "reducedMotion") : {},
|
|
1354
1362
|
...Object.fromEntries((valueRoutes.get(layer) ?? []).filter(({ from }) => state[from] !== void 0).map(({ from, into }) => [into, state[from]])),
|
|
1363
|
+
...isRoot ? componentStateAttributes(componentStates(config, path), state) : {},
|
|
1364
|
+
...isRoot || type === void 0 || kindOf(type) !== "component" ? forcedModifierAttributes(pins, path) : {},
|
|
1355
1365
|
...isRoot ? rest : {},
|
|
1356
1366
|
...slot && slotValue !== void 0 ? { [intoName(slot.into)]: slotValue } : {},
|
|
1357
1367
|
...layerProps[layer],
|
|
@@ -1365,7 +1375,7 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
|
|
|
1365
1375
|
...authoredProps(config, node.props),
|
|
1366
1376
|
[ANATOMY_MARKER]: key
|
|
1367
1377
|
};
|
|
1368
|
-
if (node.text !== void 0) props.children = node.text;
|
|
1378
|
+
if (node.text !== void 0 && props.children === void 0) props.children = node.text;
|
|
1369
1379
|
for (const [name, value] of Object.entries(props)) {
|
|
1370
1380
|
const settled = resolveValue(value, state, resolving);
|
|
1371
1381
|
if (settled !== value) props[name] = settled;
|
|
@@ -15,7 +15,7 @@ import { Patch, SerializedConfig } from "./Config.js";
|
|
|
15
15
|
* sets this constant to its own older timestamp; the merge ref then carries a registered migration
|
|
16
16
|
* newer than the constant, and the assertion fails until the author re-mints.
|
|
17
17
|
*/
|
|
18
|
-
declare const CURRENT_SCHEMA_VERSION =
|
|
18
|
+
declare const CURRENT_SCHEMA_VERSION = 20261013164300;
|
|
19
19
|
/** What the walkers need to see of an envelope — loose on purpose, so a typed `SerializedConfig`
|
|
20
20
|
* and a raw old-shape record both flow in without casts. */
|
|
21
21
|
interface VersionStamped {
|
|
@@ -13,7 +13,7 @@ import { ConfigFormatError } from "./rejection.js";
|
|
|
13
13
|
* sets this constant to its own older timestamp; the merge ref then carries a registered migration
|
|
14
14
|
* newer than the constant, and the assertion fails until the author re-mints.
|
|
15
15
|
*/
|
|
16
|
-
const CURRENT_SCHEMA_VERSION =
|
|
16
|
+
const CURRENT_SCHEMA_VERSION = 20261013164300;
|
|
17
17
|
/** The mint that moved `options` into the `settings` kind — restated here, rather than imported
|
|
18
18
|
* from the migration that owns it, because that migration imports this module. */
|
|
19
19
|
const SETTINGS_AS_KIND_VERSION = 20260908171448;
|
|
@@ -45,7 +45,7 @@ function registerSchemaMigrations(...migrations) {
|
|
|
45
45
|
for (const migration of migrations) {
|
|
46
46
|
if (migration.version !== 1 && !isMintTimestamp(migration.version)) throw new Error(`Schema migration version ${migration.version} is not a mint timestamp — mint one with \`date +%Y%m%d%H%M%S\`, like a supabase migration.`);
|
|
47
47
|
if (REGISTERED.has(migration.version)) throw new Error(`A schema migration at version ${migration.version} is already registered.`);
|
|
48
|
-
if (migration.version >
|
|
48
|
+
if (migration.version > 20261013164300) throw new Error(`Schema migration ${migration.version} is newer than CURRENT_SCHEMA_VERSION (${CURRENT_SCHEMA_VERSION}) — a stale branch merged, or the mint forgot to bump the constant. Re-mint: set CURRENT_SCHEMA_VERSION to the newest migration version.`);
|
|
49
49
|
REGISTERED.set(migration.version, migration);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -102,7 +102,7 @@ function pendingFor(version, target, options) {
|
|
|
102
102
|
* Already-current input passes through untouched — same reference, so hot paths pay nothing.
|
|
103
103
|
*/
|
|
104
104
|
function upgradeSerializedConfig(json, options) {
|
|
105
|
-
const target = options?.target ??
|
|
105
|
+
const target = options?.target ?? 20261013164300;
|
|
106
106
|
const version = detectedWireVersion(json);
|
|
107
107
|
if (version > target) throw new SchemaVersionTooNew(version, target);
|
|
108
108
|
const raw = json;
|
|
@@ -160,7 +160,7 @@ function upgradedSources(json, options) {
|
|
|
160
160
|
* meaning under the current schema and replay skips it — reported by the caller, not swallowed here.
|
|
161
161
|
*/
|
|
162
162
|
function upgradePatch(patch, writtenAt, options) {
|
|
163
|
-
const target = options?.target ??
|
|
163
|
+
const target = options?.target ?? 20261013164300;
|
|
164
164
|
if (writtenAt > target) throw new SchemaVersionTooNew(writtenAt, target);
|
|
165
165
|
let current = patch;
|
|
166
166
|
for (const migration of pendingFor(writtenAt, target, options)) {
|
|
@@ -179,7 +179,7 @@ function upgradePatch(patch, writtenAt, options) {
|
|
|
179
179
|
* stamp would cause.
|
|
180
180
|
*/
|
|
181
181
|
function upgradeDraftEntries(draft, options) {
|
|
182
|
-
const target = options?.target ??
|
|
182
|
+
const target = options?.target ?? 20261013164300;
|
|
183
183
|
const entries = [];
|
|
184
184
|
const held = [];
|
|
185
185
|
const retired = [];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A schema that still takes a deprecated form but no longer offers it.
|
|
3
|
+
*
|
|
4
|
+
* Parsing accepts `offered` or `deprecated`, so stored data and older config files keep loading.
|
|
5
|
+
* Rendered as JSON Schema, which is what tool signatures, `config/api`, the MCP's input schemas and
|
|
6
|
+
* the docs are built from, it shows only `offered`, so nothing that reads a schema learns the old
|
|
7
|
+
* form.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
/**
|
|
11
|
+
* `read` with every deprecated form shown in the JSON Schema it renders. The schema-version tripwire
|
|
12
|
+
* reads through this, since it guards everything a client accepts, offered or not.
|
|
13
|
+
*/
|
|
14
|
+
export declare function includingDeprecated<T>(read: () => T): T;
|
|
15
|
+
export declare function withDeprecated<O extends z.ZodTypeAny, D extends z.ZodTypeAny>({ offered, deprecated, }: {
|
|
16
|
+
offered: O;
|
|
17
|
+
deprecated: D;
|
|
18
|
+
}): z.ZodUnion<readonly [O, D]>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
function withDeprecated({ offered, deprecated }) {
|
|
3
|
+
const schema = z.union([offered, deprecated]);
|
|
4
|
+
const offeredOnly = () => {
|
|
5
|
+
const { $schema: _dialect, ...json } = z.toJSONSchema(offered, { io: "input" });
|
|
6
|
+
return json;
|
|
7
|
+
};
|
|
8
|
+
schema._zod.toJSONSchema = () => offeredOnly();
|
|
9
|
+
return schema;
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { withDeprecated };
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { kindOf, memberOf, pathOf, splitRef } from "./utils/refs.js";
|
|
2
|
+
import { modifierCategory } from "../entities/system/modifier-axis.js";
|
|
2
3
|
import { childrenPolicy } from "../entities/system/element.js";
|
|
4
|
+
import { UDS_FORCE_STATES, componentStates } from "./component-states.js";
|
|
3
5
|
import { LAYER_PROPS, layerPropsErrors, resolveComponentProps } from "./projections.js";
|
|
6
|
+
import "./class-names.js";
|
|
4
7
|
import { slotOf } from "../spec/slot.js";
|
|
5
8
|
import { refResolves } from "./ref-integrity.js";
|
|
6
9
|
//#region src/framework/validate-spec.ts
|
|
@@ -28,8 +31,19 @@ function walk(config, node, path, errors) {
|
|
|
28
31
|
errors.push(`${path}: unknown ${kind} "${named}"`);
|
|
29
32
|
} else if (kind === "component") {
|
|
30
33
|
const props = resolveComponentProps(config, target);
|
|
34
|
+
const states = new Set(componentStates(config, target).map(({ name }) => name));
|
|
31
35
|
for (const name of Object.keys(node.props ?? {})) {
|
|
32
36
|
if (name === "layerProps") continue;
|
|
37
|
+
if (name === "__udsForceStates") {
|
|
38
|
+
for (const pinned of Object.keys(node.props?.["__udsForceStates"] ?? {})) if (pinned.startsWith("_")) {
|
|
39
|
+
if (modifierCategory(config, pinned.slice(1)) !== "state") errors.push(`${path}: "${pinned}" is not a state modifier`);
|
|
40
|
+
} else if (!states.has(pinned)) errors.push(`${path}: "${pinned}" is not a state of "${node.type}"`);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (states.has(name)) {
|
|
44
|
+
errors.push(`${path}: "${name}" is a state of "${node.type}", not a prop — pin it as ${UDS_FORCE_STATES}: { ${name}: … }`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
33
47
|
if (!(name in props)) errors.push(`${path}: unknown prop "${name}" on "${node.type}"`);
|
|
34
48
|
}
|
|
35
49
|
errors.push(...layerPropsErrors({
|