@yahoo/uds-create-config 4.6.0 → 4.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/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
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { kindOf, memberOf, pathOf, ref, splitRef } from "./utils/refs.js";
|
|
1
|
+
import { isRef, kindOf, memberOf, pathOf, ref, splitRef } from "./utils/refs.js";
|
|
2
|
+
import { renderedElement } from "../entities/system/element.js";
|
|
2
3
|
import { createSliceMemo } from "./memo.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
4
|
+
import { unconditionalPreviewProps } from "./preview-store.js";
|
|
5
|
+
import { authoredBag, authoredValue, declaredPropDefaults, memberFromLeaf, resolveComponentProps, stylePropertyPathFor } from "./projections.js";
|
|
6
|
+
import { forwardedLayers, intoName, routesContent } from "./prop-surface.js";
|
|
5
7
|
import { graftPreviewElements } from "../spec/preview-elements.js";
|
|
6
8
|
import { componentLayerClass, componentPropClasses, componentRuleClasses, layerRoutedProps, splitStyleProps, styleRuleMotionClasses } from "./class-names.js";
|
|
7
9
|
//#region src/framework/render-spec.ts
|
|
@@ -181,6 +183,27 @@ function authoredProps(config, props) {
|
|
|
181
183
|
* and that was always stated rather than run.
|
|
182
184
|
*/
|
|
183
185
|
function deriveSpec(config, path) {
|
|
186
|
+
const projected = componentSpec(config, path);
|
|
187
|
+
return projected.kind === "composition" ? projected.spec : void 0;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Per slice, the projected spec of each component asked for so far. The matrix, the layers rail and
|
|
191
|
+
* the designer panel each ask per cell or per row, and a projection walks the anatomy and reads every
|
|
192
|
+
* node prop's leaf back, so one answer serves them all until the config they read from changes.
|
|
193
|
+
*
|
|
194
|
+
* Keyed on the kinds a projection reads: the component's body, and the style properties and the
|
|
195
|
+
* token and composite members its node props' refs are read back through.
|
|
196
|
+
*/
|
|
197
|
+
const componentSpecCache = createSliceMemo({
|
|
198
|
+
kinds: [
|
|
199
|
+
"component",
|
|
200
|
+
"style-property",
|
|
201
|
+
"token",
|
|
202
|
+
"composite"
|
|
203
|
+
],
|
|
204
|
+
compute: (_config) => /* @__PURE__ */ new Map()
|
|
205
|
+
});
|
|
206
|
+
function projectSpec(config, path) {
|
|
184
207
|
const body = bodyOf(config, path) ?? {};
|
|
185
208
|
const layers = body.layers ?? {};
|
|
186
209
|
const anatomy = body.anatomy;
|
|
@@ -210,11 +233,13 @@ function deriveSpec(config, path) {
|
|
|
210
233
|
const props = {
|
|
211
234
|
...authoredProps(config, node.props),
|
|
212
235
|
...node.text !== void 0 ? { children: node.text } : {},
|
|
236
|
+
...layer === void 0 ? { [ANATOMY_MARKER]: key } : {},
|
|
213
237
|
...layer !== void 0 ? {
|
|
214
238
|
"data-uds-layer": layer,
|
|
215
239
|
"data-uds-component": path
|
|
216
240
|
} : {}
|
|
217
241
|
};
|
|
242
|
+
markReadOnlyField(config, type, props);
|
|
218
243
|
const children = node.children?.length ? [...node.children] : void 0;
|
|
219
244
|
elements[key] = {
|
|
220
245
|
type,
|
|
@@ -291,7 +316,15 @@ function instanceSpec(path, props = {}) {
|
|
|
291
316
|
* surface reads `kind` to decide whether to offer composition tools, and `reason` to explain why not.
|
|
292
317
|
*/
|
|
293
318
|
function componentSpec(config, path) {
|
|
294
|
-
const
|
|
319
|
+
const cache = componentSpecCache(config);
|
|
320
|
+
const hit = cache.get(path);
|
|
321
|
+
if (hit) return hit;
|
|
322
|
+
const built = buildComponentSpec(config, path);
|
|
323
|
+
cache.set(path, built);
|
|
324
|
+
return built;
|
|
325
|
+
}
|
|
326
|
+
function buildComponentSpec(config, path) {
|
|
327
|
+
const derived = projectSpec(config, path);
|
|
295
328
|
if (derived) return {
|
|
296
329
|
spec: derived,
|
|
297
330
|
kind: "composition"
|
|
@@ -334,7 +367,7 @@ function componentSpec(config, path) {
|
|
|
334
367
|
function previewSpec(config, path) {
|
|
335
368
|
const body = bodyOf(config, path);
|
|
336
369
|
if (!body) return void 0;
|
|
337
|
-
const values = body
|
|
370
|
+
const values = unconditionalPreviewProps(body) ?? {};
|
|
338
371
|
return graftPreviewElements(instanceSpec(path, values), values);
|
|
339
372
|
}
|
|
340
373
|
/** A key no emitted element is using yet. Anatomy keys are namespaced under the instance they belong
|
|
@@ -439,6 +472,63 @@ function readStatePointer(state, pointer) {
|
|
|
439
472
|
function statePointerProp(pointer) {
|
|
440
473
|
return pointer.replace(/^\//, "").split("/")[0] ?? "";
|
|
441
474
|
}
|
|
475
|
+
/**
|
|
476
|
+
* The marker on an element a render placed inline, naming its anatomy node. A layer is addressed by
|
|
477
|
+
* `data-uds-layer`; this is for the node between layers that has no bag of its own. Stamped by the
|
|
478
|
+
* emitted module and by both projections, so a rendered element and a projected one read alike.
|
|
479
|
+
*/
|
|
480
|
+
const ANATOMY_MARKER = "data-uds-anatomy";
|
|
481
|
+
/** The string or number prop `node` binds into `into` (`{ $state: '/<prop>' }`), when it does. */
|
|
482
|
+
function boundValueProp({ config, path, node, into }) {
|
|
483
|
+
const pointer = (node.props?.[into])?.$state;
|
|
484
|
+
if (typeof pointer !== "string") return void 0;
|
|
485
|
+
const prop = statePointerProp(pointer);
|
|
486
|
+
const decl = resolveComponentProps(config, path)[prop];
|
|
487
|
+
return decl?.type === "string" || decl?.type === "number" ? prop : void 0;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* The value prop a component paints as a layer's text: the anatomy node rendering `layer` binds its
|
|
491
|
+
* `children` to `{ $state: '/<prop>' }`, and that prop is declared a string or number. A forward
|
|
492
|
+
* surfaces its layer on the prop itself; this answers for a render that reads a value prop and
|
|
493
|
+
* writes it into a layer by hand, the way a `title` string reaches a heading.
|
|
494
|
+
*/
|
|
495
|
+
function layerTextProp({ config, path, layer }) {
|
|
496
|
+
const body = bodyOf(config, path);
|
|
497
|
+
if (!body) return void 0;
|
|
498
|
+
for (const node of Object.values(body.anatomy ?? {})) {
|
|
499
|
+
if (layerOf(node.element) !== layer) continue;
|
|
500
|
+
const prop = boundValueProp({
|
|
501
|
+
config,
|
|
502
|
+
path,
|
|
503
|
+
node,
|
|
504
|
+
into: "children"
|
|
505
|
+
});
|
|
506
|
+
if (prop) return prop;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* The value prop a component hands an element it placed inline: the anatomy node `element` binds
|
|
511
|
+
* `into` to `{ $state: '/<prop>' }`, and that prop is a string or number. `into` is `children` for
|
|
512
|
+
* text the element shows itself, or the far component's prop the render passed it to
|
|
513
|
+
* (`<CommentHeader title={title} />` binds `title`).
|
|
514
|
+
*/
|
|
515
|
+
function anatomyTextProp({ config, path, element, into }) {
|
|
516
|
+
const node = bodyOf(config, path)?.anatomy?.[element];
|
|
517
|
+
if (!node) return void 0;
|
|
518
|
+
return boundValueProp({
|
|
519
|
+
config,
|
|
520
|
+
path,
|
|
521
|
+
node,
|
|
522
|
+
into
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
/** The component an inline anatomy element renders, when it is one (`component:<path>`). */
|
|
526
|
+
function anatomyElementComponent({ config, path, element }) {
|
|
527
|
+
const target = refOf(bodyOf(config, path)?.anatomy?.[element]?.element);
|
|
528
|
+
if (target === void 0) return void 0;
|
|
529
|
+
const { kind, qualifiedPath } = splitRef(target);
|
|
530
|
+
return kind === "component" ? qualifiedPath : void 0;
|
|
531
|
+
}
|
|
442
532
|
/** A value in json-render's `visible` grammar, resolved against a prop bag. `undefined` when this
|
|
443
533
|
* reader can't settle it, which leaves the condition on the element for the renderer to judge. */
|
|
444
534
|
function resolveVisibility(condition, state) {
|
|
@@ -593,18 +683,28 @@ function collectTerms({ condition, junction, group, sink }) {
|
|
|
593
683
|
* spellings are the same test.
|
|
594
684
|
*/
|
|
595
685
|
function withoutTruthyTerm({ condition, prop }) {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
prop
|
|
599
|
-
})) return null;
|
|
600
|
-
if (!Array.isArray(condition)) return void 0;
|
|
601
|
-
const kept = condition.filter((entry) => !spentBy({
|
|
686
|
+
const list = andListOf(condition);
|
|
687
|
+
const kept = list.filter((entry) => !spentBy({
|
|
602
688
|
condition: entry,
|
|
603
689
|
prop
|
|
604
690
|
}));
|
|
605
|
-
if (kept.length ===
|
|
606
|
-
|
|
607
|
-
|
|
691
|
+
if (kept.length === list.length) return void 0;
|
|
692
|
+
return spelled(kept);
|
|
693
|
+
}
|
|
694
|
+
/** A condition as the and-list it is: a bare clause is a list of one, nothing (or a projection's
|
|
695
|
+
* baked `true`) is empty. `false` is a position nothing at rest shows, which no clause can add to
|
|
696
|
+
* or take from, so it reads as a list holding itself. */
|
|
697
|
+
function andListOf(condition) {
|
|
698
|
+
if (condition === void 0 || condition === true) return [];
|
|
699
|
+
if (Array.isArray(condition)) return [...condition];
|
|
700
|
+
return [condition];
|
|
701
|
+
}
|
|
702
|
+
/** The stored spelling of an and-list: nothing clears the field, one clause is bare, several stay
|
|
703
|
+
* the list. */
|
|
704
|
+
function spelled(list) {
|
|
705
|
+
if (list.length === 0) return null;
|
|
706
|
+
if (list.length === 1) return list[0];
|
|
707
|
+
return list;
|
|
608
708
|
}
|
|
609
709
|
/** Whether a single term of an and-list is one `prop` being present makes true: the bare test, an
|
|
610
710
|
* `eq: true` test (content is never the boolean `true`, so presence is the only reading that can
|
|
@@ -672,9 +772,31 @@ function computeConsumedProps(config, path, body) {
|
|
|
672
772
|
const consumed = new Set(["className", "layerProps"]);
|
|
673
773
|
const declared = body.props ?? {};
|
|
674
774
|
for (const [name, decl] of Object.entries(declared)) if (decl && decl.type !== "styleProperty") consumed.add(name);
|
|
775
|
+
for (const name of body.renderProps ?? []) consumed.add(name);
|
|
675
776
|
for (const rule of componentRuleClasses(config, path)) for (const prop of Object.keys(rule.when)) if (declared[prop]) consumed.add(prop);
|
|
676
777
|
return consumed;
|
|
677
778
|
}
|
|
779
|
+
/** The elements React treats as controlled once given a `value`. HTML's list, not the config's. */
|
|
780
|
+
const FORM_FIELDS = new Set([
|
|
781
|
+
"input",
|
|
782
|
+
"textarea",
|
|
783
|
+
"select"
|
|
784
|
+
]);
|
|
785
|
+
/**
|
|
786
|
+
* A projected field has no handler: the render's `onChange` does not survive into the anatomy, so a
|
|
787
|
+
* controlled `value` is read-only here, and the element says so, which is what React requires of a
|
|
788
|
+
* value it cannot change. The element counts as a field when it renders as one of
|
|
789
|
+
* {@link FORM_FIELDS}, through a component or as the tag.
|
|
790
|
+
*/
|
|
791
|
+
function markReadOnlyField(config, type, props) {
|
|
792
|
+
if (props.value === void 0 || props.readOnly !== void 0) return;
|
|
793
|
+
const tag = kindOf(type) === "component" ? renderedElement(config, pathOf(type)) : type;
|
|
794
|
+
if (FORM_FIELDS.has(tag)) props.readOnly = true;
|
|
795
|
+
}
|
|
796
|
+
/** A slot value that is text rather than elements. */
|
|
797
|
+
function isText(value) {
|
|
798
|
+
return typeof value === "string" || typeof value === "number";
|
|
799
|
+
}
|
|
678
800
|
/** The prop JSX children arrive on — json-render's content channel, and the same name
|
|
679
801
|
* `childrenPolicy` reads to answer whether a component accepts any. Not a system-defined name: a
|
|
680
802
|
* design system names its components and tokens, and this one names the channel. */
|
|
@@ -731,8 +853,9 @@ function computeSlotTargets(config, body) {
|
|
|
731
853
|
* the layer meant to hold it renders empty. Every one of Studio's own components shaped that way
|
|
732
854
|
* (`Badge`, `StudioButton`, `Toggle`, …) painted as an empty shell in the component editor.
|
|
733
855
|
*
|
|
734
|
-
* Exported so the answer has ONE owner. An untargeted slot
|
|
735
|
-
*
|
|
856
|
+
* Exported so the answer has ONE owner. An untargeted slot is hosted by the layer the anatomy
|
|
857
|
+
* places its content-position node in — a row's `leading` slot sits in its `leading` Box — and by the
|
|
858
|
+
* root when the anatomy records no position for it, which is where the expansion renders it.
|
|
736
859
|
*/
|
|
737
860
|
function slotTargetsOf(config, path) {
|
|
738
861
|
const body = bodyOf(config, path);
|
|
@@ -743,11 +866,85 @@ function slotTargetsOf(config, path) {
|
|
|
743
866
|
layer,
|
|
744
867
|
from: decl.from,
|
|
745
868
|
into: intoName(decl.into)
|
|
746
|
-
})), ...
|
|
747
|
-
layer
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
869
|
+
})), ...untargeted.flatMap((decl) => {
|
|
870
|
+
const layer = slotHostLayer(body.anatomy, decl.from) ?? root;
|
|
871
|
+
return layer ? [{
|
|
872
|
+
layer,
|
|
873
|
+
from: decl.from,
|
|
874
|
+
into: intoName(decl.into)
|
|
875
|
+
}] : [];
|
|
876
|
+
})];
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* The layer holding a slot prop's content-position node: the nearest layer above the `Slot` node
|
|
880
|
+
* named for the prop, itself included. `undefined` when the anatomy records no position for the
|
|
881
|
+
* prop, or the node hangs under no layer.
|
|
882
|
+
*/
|
|
883
|
+
function slotHostLayer(anatomy, from) {
|
|
884
|
+
if (!anatomy) return void 0;
|
|
885
|
+
const parents = /* @__PURE__ */ new Map();
|
|
886
|
+
for (const [key, node] of Object.entries(anatomy)) for (const child of heldKeys(node)) parents.set(child, key);
|
|
887
|
+
let key = Object.keys(anatomy).find((candidate) => slotAnatomyPropOf(anatomy[candidate]) === from);
|
|
888
|
+
const seen = /* @__PURE__ */ new Set();
|
|
889
|
+
while (key !== void 0 && !seen.has(key)) {
|
|
890
|
+
seen.add(key);
|
|
891
|
+
const layer = layerOf(anatomy[key]?.element);
|
|
892
|
+
if (layer !== void 0) return layer;
|
|
893
|
+
key = parents.get(key);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Layer → the forwards that hand a VALUE to one of its props.
|
|
898
|
+
*
|
|
899
|
+
* `startIcon: { type: 'forward', target: 'startIcon/name' }` and `swatch: { type: 'forward', target:
|
|
900
|
+
* 'swatch/swatchColor' }` are this shape: the instance's value lands on the layer's element as the
|
|
901
|
+
* prop the forward names, the way the emitted module spreads it into that layer's bundle. A forward
|
|
902
|
+
* whose target prop is a slot is content instead, and {@link slotTargetsOf} answers for it.
|
|
903
|
+
*/
|
|
904
|
+
function valueForwardsOf(config, path) {
|
|
905
|
+
const body = bodyOf(config, path);
|
|
906
|
+
return body ? valueForwards(config, body) : [];
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* The instance props whose values or content reach `layer`: content routed into it, values
|
|
910
|
+
* forwarded onto one of its props, and the props the layer exposes through `forwards`. A file
|
|
911
|
+
* editing a placed instance can change exactly these about the layer; the layer's own styles are
|
|
912
|
+
* the component's.
|
|
913
|
+
*/
|
|
914
|
+
/**
|
|
915
|
+
* The instance prop whose content or value lands on `into` of `layer`, or `undefined` when the
|
|
916
|
+
* layer owns that prop itself. A change to the layer's prop in a cell is then a change to the
|
|
917
|
+
* instance prop's preview value, not a style.
|
|
918
|
+
*/
|
|
919
|
+
function propInto({ config, path, layer, into }) {
|
|
920
|
+
return [...slotTargetsOf(config, path), ...valueForwardsOf(config, path)].find((route) => route.layer === layer && route.into === into)?.from;
|
|
921
|
+
}
|
|
922
|
+
/** The route a prop's content or value takes into a layer, or `undefined` for a prop that reaches
|
|
923
|
+
* no layer. */
|
|
924
|
+
function routeOf({ config, path, prop }) {
|
|
925
|
+
return [...slotTargetsOf(config, path), ...valueForwardsOf(config, path)].find((route) => route.from === prop);
|
|
926
|
+
}
|
|
927
|
+
function propsIntoLayer({ config, path, layer }) {
|
|
928
|
+
const names = /* @__PURE__ */ new Set();
|
|
929
|
+
for (const route of slotTargetsOf(config, path)) if (route.layer === layer) names.add(route.from);
|
|
930
|
+
for (const route of valueForwardsOf(config, path)) if (route.layer === layer) names.add(route.from);
|
|
931
|
+
for (const name of forwardedLayers(config, path).get(layer) ?? []) names.add(name);
|
|
932
|
+
return [...names];
|
|
933
|
+
}
|
|
934
|
+
function valueForwards(config, body) {
|
|
935
|
+
const routes = [];
|
|
936
|
+
for (const [from, decl] of Object.entries(body.props ?? {})) {
|
|
937
|
+
if (decl?.type !== "forward") continue;
|
|
938
|
+
if (routesContent(config, body.layers, decl)) continue;
|
|
939
|
+
const target = refOf(decl.target);
|
|
940
|
+
if (!target) continue;
|
|
941
|
+
routes.push({
|
|
942
|
+
layer: pathOf(target),
|
|
943
|
+
from,
|
|
944
|
+
into: intoName(decl.into)
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
return routes;
|
|
751
948
|
}
|
|
752
949
|
/** The className a layer wears for one instance: its own class, every style rule whose condition
|
|
753
950
|
* holds, and — on the root only — what the call site passed. */
|
|
@@ -779,12 +976,53 @@ function place(sink, type, props, children, hint, slots) {
|
|
|
779
976
|
const key = freeKey(sink, hint);
|
|
780
977
|
sink.elements[key] = {
|
|
781
978
|
type: tag,
|
|
782
|
-
props: own,
|
|
979
|
+
props: moduleProps(sink.config, tag, own),
|
|
783
980
|
...children.length ? { children: [...children] } : {},
|
|
784
981
|
...slots && Object.keys(slots).length ? { slots } : {}
|
|
785
982
|
};
|
|
786
983
|
return key;
|
|
787
984
|
}
|
|
985
|
+
/**
|
|
986
|
+
* An element's props as the component behind it reads them.
|
|
987
|
+
*
|
|
988
|
+
* A style prop's token arrives as the stored ref when an instance's defaults come from the config
|
|
989
|
+
* (`bg` as `token:bg/primary`). The expansion reads such a ref through the prop's domain and paints
|
|
990
|
+
* the class; a compiled module reads a value string and paints nothing for an object. So an element
|
|
991
|
+
* left to its registry entry gets the same leaf the expansion would have named, resolved through
|
|
992
|
+
* the component's own prop routing (`gap` on a `VStack` writes `gapY`). A modifier block nests a bag
|
|
993
|
+
* of the same shape. Only a token ref on a key that writes a style property is touched: a component
|
|
994
|
+
* ref handed as a prop value stays the ref it is.
|
|
995
|
+
*/
|
|
996
|
+
function moduleProps(config, type, props) {
|
|
997
|
+
const through = type.includes(":") && kindOf(type) === "component" ? [pathOf(type)] : [];
|
|
998
|
+
const modifiers = new Set(config.list("modifier").map((modifier) => `_${modifier.leaf}`));
|
|
999
|
+
const walk = (bag) => {
|
|
1000
|
+
let changed = false;
|
|
1001
|
+
const out = {};
|
|
1002
|
+
for (const [key, value] of Object.entries(bag)) {
|
|
1003
|
+
if (modifiers.has(key) && value && typeof value === "object" && !Array.isArray(value) && !isRef(value)) {
|
|
1004
|
+
const inner = walk(value);
|
|
1005
|
+
out[key] = inner;
|
|
1006
|
+
changed ||= inner !== value;
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
1009
|
+
if (isRef(value) && kindOf(value.__ref) === "token") {
|
|
1010
|
+
const property = stylePropertyPathFor({
|
|
1011
|
+
config,
|
|
1012
|
+
through,
|
|
1013
|
+
key
|
|
1014
|
+
});
|
|
1015
|
+
const leaf = property === void 0 ? value : authoredValue(config, property, value);
|
|
1016
|
+
out[key] = leaf;
|
|
1017
|
+
changed ||= leaf !== value;
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
out[key] = value;
|
|
1021
|
+
}
|
|
1022
|
+
return changed ? out : bag;
|
|
1023
|
+
};
|
|
1024
|
+
return walk(props);
|
|
1025
|
+
}
|
|
788
1026
|
/** Whether any node under `key` (itself excluded) binds `prop` as its content. */
|
|
789
1027
|
function subtreeBinds(anatomy, key, prop) {
|
|
790
1028
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -812,10 +1050,13 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
|
|
|
812
1050
|
const { config } = sink;
|
|
813
1051
|
const body = bodyOf(config, path) ?? {};
|
|
814
1052
|
const layers = body.layers ?? {};
|
|
815
|
-
const
|
|
1053
|
+
const stored = body.anatomy && Object.keys(body.anatomy).length > 0;
|
|
1054
|
+
const anatomy = stored ? body.anatomy : { root: { element: { __ref: `layer:${rootKeyOf(layers)}` } } };
|
|
816
1055
|
const filled = {};
|
|
817
1056
|
for (const [from, fills] of Object.entries(instanceSlots ?? {})) if (fills.length > 0 && instanceProps[from] === void 0) filled[from] = true;
|
|
1057
|
+
const declaredDefaults = declaredPropDefaults(body.props);
|
|
818
1058
|
const state = {
|
|
1059
|
+
...declaredDefaults,
|
|
819
1060
|
...body.defaultProps,
|
|
820
1061
|
...filled,
|
|
821
1062
|
...instanceProps
|
|
@@ -823,19 +1064,13 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
|
|
|
823
1064
|
const consumed = consumedProps(config, path, body);
|
|
824
1065
|
const { byLayer: targets } = slotTargets(config, path, body);
|
|
825
1066
|
const valueRoutes = /* @__PURE__ */ new Map();
|
|
826
|
-
for (const
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
if (!target) continue;
|
|
831
|
-
const routes = valueRoutes.get(pathOf(target)) ?? [];
|
|
832
|
-
routes.push({
|
|
833
|
-
from,
|
|
834
|
-
into: intoName(decl.into)
|
|
835
|
-
});
|
|
836
|
-
valueRoutes.set(pathOf(target), routes);
|
|
1067
|
+
for (const route of valueForwards(config, body)) {
|
|
1068
|
+
const routes = valueRoutes.get(route.layer) ?? [];
|
|
1069
|
+
routes.push(route);
|
|
1070
|
+
valueRoutes.set(route.layer, routes);
|
|
837
1071
|
}
|
|
838
1072
|
const supplied = {
|
|
1073
|
+
...declaredDefaults,
|
|
839
1074
|
...body.defaultProps ?? {},
|
|
840
1075
|
...instanceProps
|
|
841
1076
|
};
|
|
@@ -887,6 +1122,7 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
|
|
|
887
1122
|
const layer = layerOf(node.element);
|
|
888
1123
|
const isRoot = key === hostNode;
|
|
889
1124
|
let type = typeOf(layers, node.element);
|
|
1125
|
+
if (!stored && isRoot && type !== void 0 && kindOf(type) === "component") type = renderedElement(config, path);
|
|
890
1126
|
const bound = boundPropOf(node.element);
|
|
891
1127
|
if (bound) type = boundChoice(config, path, node.element, state[bound]);
|
|
892
1128
|
type = wrappedTypeOf(node, type);
|
|
@@ -904,7 +1140,7 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
|
|
|
904
1140
|
return key;
|
|
905
1141
|
}
|
|
906
1142
|
const slot = layer ? targets.get(layer) : void 0;
|
|
907
|
-
const slotValue = slot && !subtreeBinds(anatomy, key, slot.from) ? instanceProps[slot.from] : void 0;
|
|
1143
|
+
const slotValue = slot && !subtreeBinds(anatomy, key, slot.from) && !(positioned.has(slot.from) && isText(instanceProps[slot.from])) ? instanceProps[slot.from] : void 0;
|
|
908
1144
|
const props = layer ? {
|
|
909
1145
|
...authoredProps(config, node.props),
|
|
910
1146
|
"data-uds-component": path,
|
|
@@ -919,12 +1155,16 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
|
|
|
919
1155
|
isRoot ? instanceProps.className : void 0,
|
|
920
1156
|
layerProps[layer]?.className
|
|
921
1157
|
]) || void 0
|
|
922
|
-
} :
|
|
1158
|
+
} : {
|
|
1159
|
+
...authoredProps(config, node.props),
|
|
1160
|
+
[ANATOMY_MARKER]: key
|
|
1161
|
+
};
|
|
923
1162
|
if (node.text !== void 0) props.children = node.text;
|
|
924
1163
|
for (const [name, value] of Object.entries(props)) {
|
|
925
1164
|
const settled = resolveChosenValue(value, state);
|
|
926
1165
|
if (settled !== value) props[name] = settled;
|
|
927
1166
|
}
|
|
1167
|
+
markReadOnlyField(config, type, props);
|
|
928
1168
|
const children = [];
|
|
929
1169
|
const boundFills = {};
|
|
930
1170
|
for (const [name, value] of Object.entries(props)) {
|
|
@@ -935,9 +1175,16 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
|
|
|
935
1175
|
if (name === "children") children.push(...fills);
|
|
936
1176
|
else boundFills[name] = [...fills];
|
|
937
1177
|
}
|
|
1178
|
+
let positionedText;
|
|
938
1179
|
for (const child of node.children ?? []) {
|
|
939
1180
|
const childSlot = slotAnatomyPropOf(anatomy[child]);
|
|
940
1181
|
if (childSlot !== void 0) {
|
|
1182
|
+
if (resolveVisibility(anatomy[child]?.visible, state) === false) continue;
|
|
1183
|
+
const given = state[childSlot];
|
|
1184
|
+
if (isText(given)) {
|
|
1185
|
+
positionedText = String(given);
|
|
1186
|
+
contentPlaced = contentPlaced || childSlot === untargetedSlot;
|
|
1187
|
+
}
|
|
941
1188
|
if (childSlot === untargetedSlot) {
|
|
942
1189
|
const fills = instanceSlots?.[childSlot] ?? [];
|
|
943
1190
|
if (allContent.length > 0 || fills.length > 0) {
|
|
@@ -951,6 +1198,7 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
|
|
|
951
1198
|
if (placed) children.push(placed);
|
|
952
1199
|
}
|
|
953
1200
|
if (isRoot && allContent.length > 0 && !contentPlaced) children.push(...allContent);
|
|
1201
|
+
if (positionedText !== void 0 && props.children === void 0) props.children = positionedText;
|
|
954
1202
|
const slots = { ...boundFills };
|
|
955
1203
|
const routed = slot ? instanceSlots?.[slot.from] : void 0;
|
|
956
1204
|
const overridden = routed?.length && slot && !slot.untargeted ? intoName(slot.into) : void 0;
|
|
@@ -1041,4 +1289,4 @@ function expandComponent(config, path, props = {}, options = {}) {
|
|
|
1041
1289
|
return expandSpec(config, instanceSpec(path, props), options);
|
|
1042
1290
|
}
|
|
1043
1291
|
//#endregion
|
|
1044
|
-
export { boundElementType, componentSpec, deriveSpec, entryKeyOf, expandComponent, expandSpec, instanceSpec, liveStateProps, previewSpec, readStatePointer, resolveVisibility, slotAnatomyPropOf, slotTargetsOf, specWithResolvedVisibility, statePointerProp, visibilityStateProps, visibilityTerms, withoutTruthyTerm, wrappedElementKey };
|
|
1292
|
+
export { ANATOMY_MARKER, anatomyElementComponent, anatomyTextProp, andListOf, boundElementType, componentSpec, deriveSpec, entryKeyOf, expandComponent, expandSpec, instanceSpec, layerTextProp, liveStateProps, previewSpec, propInto, propsIntoLayer, readStatePointer, resolveVisibility, routeOf, slotAnatomyPropOf, slotTargetsOf, specWithResolvedVisibility, spelled, statePointerProp, valueForwardsOf, visibilityStateProps, visibilityTerms, withoutTruthyTerm, wrappedElementKey };
|
|
@@ -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 = 20260920161309;
|
|
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 = 20260920161309;
|
|
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 > 20260920161309) 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 ?? 20260920161309;
|
|
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 ?? 20260920161309;
|
|
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 ?? 20260920161309;
|
|
183
183
|
const entries = [];
|
|
184
184
|
const held = [];
|
|
185
185
|
const retired = [];
|
|
@@ -399,5 +399,14 @@ function rewriteRefSource(value, from, to) {
|
|
|
399
399
|
}
|
|
400
400
|
return value;
|
|
401
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* A condition's identity: its entries sorted by prop, each value read to its leaf, so a bare name
|
|
404
|
+
* and the ref it is stored as spell the same key. `*` for no condition. A style rule and a preview
|
|
405
|
+
* condition are both keyed by this.
|
|
406
|
+
*/
|
|
407
|
+
function canonicalCondition(when) {
|
|
408
|
+
const entries = Object.entries(when ?? {}).filter(([, v]) => v !== void 0 && v !== null).sort(([a], [b]) => a.localeCompare(b));
|
|
409
|
+
return entries.length === 0 ? "*" : entries.map(([k, v]) => `${k}=${String(refLeaf(v))}`).join("&");
|
|
410
|
+
}
|
|
402
411
|
//#endregion
|
|
403
|
-
export { ENTITY_PATH_MESSAGE, MINTED, collectRefs, entityRefOf, isEntityPath, isExtendedRef, isMintedRow, isRef, kindOf, memberOf, memberRef, orRef, pathOf, qualifyRefs, ref, refLeaf, refSchema, rewriteRefNamespace, rewriteRefSource, rewriteRefs, singletonRef, sourceOf, splitRef };
|
|
412
|
+
export { ENTITY_PATH_MESSAGE, MINTED, canonicalCondition, collectRefs, entityRefOf, isEntityPath, isExtendedRef, isMintedRow, isRef, kindOf, memberOf, memberRef, orRef, pathOf, qualifyRefs, ref, refLeaf, refSchema, rewriteRefNamespace, rewriteRefSource, rewriteRefs, singletonRef, sourceOf, splitRef };
|
|
@@ -18,6 +18,8 @@ interface SpecNode {
|
|
|
18
18
|
readonly type: string;
|
|
19
19
|
readonly props?: Record<string, unknown>;
|
|
20
20
|
readonly children?: readonly SpecNode[];
|
|
21
|
+
/** Named slot fills, each a list of nodes under the slot prop they fill. */
|
|
22
|
+
readonly slots?: Readonly<Record<string, readonly SpecNode[]>>;
|
|
21
23
|
readonly text?: string;
|
|
22
24
|
}
|
|
23
25
|
/** Validate a design-file spec against the config — every node's `type` a real component and its
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { kindOf, memberOf, pathOf, splitRef } from "./utils/refs.js";
|
|
2
2
|
import { childrenPolicy } from "../entities/system/element.js";
|
|
3
3
|
import { resolveComponentProps } from "./projections.js";
|
|
4
|
+
import { slotOf } from "../spec/slot.js";
|
|
4
5
|
import { refResolves } from "./ref-integrity.js";
|
|
5
6
|
//#region src/framework/validate-spec.ts
|
|
6
7
|
/**
|
|
@@ -29,6 +30,18 @@ function walk(config, node, path, errors) {
|
|
|
29
30
|
const props = resolveComponentProps(config, target);
|
|
30
31
|
for (const name of Object.keys(node.props ?? {})) if (!(name in props)) errors.push(`${path}: unknown prop "${name}" on "${node.type}"`);
|
|
31
32
|
if (node.children?.length && childrenPolicy(config, target) === "none") errors.push(`${path}: "${node.type}" takes no children`);
|
|
33
|
+
for (const [name, fills] of Object.entries(node.slots ?? {})) {
|
|
34
|
+
const slot = slotOf({
|
|
35
|
+
config,
|
|
36
|
+
path: target,
|
|
37
|
+
slot: name
|
|
38
|
+
});
|
|
39
|
+
if (!slot) errors.push(`${path}: "${node.type}" has no slot "${name}"`);
|
|
40
|
+
fills.forEach((fill, i) => {
|
|
41
|
+
if (slot && !slot.takes(fill.type)) errors.push(`${path}: slot "${name}" of "${node.type}" takes ${slot.accepts.join(", ")}, not "${fill.type}"`);
|
|
42
|
+
walk(config, fill, `${path} > ${node.type}.${name}[${i}]`, errors);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
32
45
|
}
|
|
33
46
|
(node.children ?? []).forEach((child, i) => {
|
|
34
47
|
walk(config, child, `${path} > ${node.type}[${i}]`, errors);
|