@yahoo/uds-create-config 2.45.0 → 3.0.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/README.md +129 -0
- package/dist/ai-lanes.d.ts +23 -0
- package/dist/ai-lanes.js +34 -0
- package/dist/authoring/define-component.d.ts +85 -0
- package/dist/configs/CanvasConfig.d.ts +3237 -0
- package/dist/configs/CanvasConfig.js +332 -0
- package/dist/configs/build-options.d.ts +10 -0
- package/dist/configs/build-options.js +68 -0
- package/dist/configs/platform.d.ts +15 -0
- package/dist/configs/platform.js +17 -0
- package/dist/configs/react-native-system.d.ts +3026 -0
- package/dist/configs/react-native-system.js +64 -0
- package/dist/configs/system.d.ts +2771 -0
- package/dist/configs/system.js +74 -0
- package/dist/css/properties.generated.d.ts +14 -0
- package/dist/css/properties.generated.js +6260 -0
- package/dist/css/value-type.d.ts +48 -0
- package/dist/css/value-type.js +131 -0
- package/dist/css/values.d.ts +33 -0
- package/dist/css/values.js +116 -0
- package/dist/entities/native/NativeModifier.d.ts +25 -0
- package/dist/entities/native/NativeModifier.js +41 -0
- package/dist/entities/native/NativeSettings.d.ts +44 -0
- package/dist/entities/native/NativeSettings.js +39 -0
- package/dist/entities/native/NativeStyleProperty.d.ts +355 -0
- package/dist/entities/native/NativeStyleProperty.js +28 -0
- package/dist/entities/native/NativeToken.d.ts +31 -0
- package/dist/entities/native/NativeToken.js +49 -0
- package/dist/entities/system/CanvasRole.d.ts +34 -0
- package/dist/entities/system/CanvasRole.js +45 -0
- package/dist/entities/system/Component.d.ts +1287 -0
- package/dist/entities/system/Component.js +1840 -0
- package/dist/entities/system/Composite.d.ts +12 -0
- package/dist/entities/system/Composite.js +26 -0
- package/dist/entities/system/Device.d.ts +11 -0
- package/dist/entities/system/Device.js +18 -0
- package/dist/entities/system/File.d.ts +72 -0
- package/dist/entities/system/File.js +136 -0
- package/dist/entities/system/Font.d.ts +48 -0
- package/dist/entities/system/Font.js +56 -0
- package/dist/entities/system/GlobalStyle.d.ts +26 -0
- package/dist/entities/system/GlobalStyle.js +54 -0
- package/dist/entities/system/Guidance.d.ts +85 -0
- package/dist/entities/system/Guidance.js +59 -0
- package/dist/entities/system/GuidanceStyle.d.ts +14 -0
- package/dist/entities/system/GuidanceStyle.js +8 -0
- package/dist/entities/system/Icon.d.ts +46 -0
- package/dist/entities/system/Icon.js +46 -0
- package/dist/entities/system/LinkedSystem.d.ts +504 -0
- package/dist/entities/system/LinkedSystem.js +814 -0
- package/dist/entities/system/Modifier.d.ts +92 -0
- package/dist/entities/system/Modifier.js +120 -0
- package/dist/entities/system/Motion.d.ts +28 -0
- package/dist/entities/system/Motion.js +48 -0
- package/dist/entities/system/Operation.d.ts +100 -0
- package/dist/entities/system/Operation.js +236 -0
- package/dist/entities/system/Package.d.ts +42 -0
- package/dist/entities/system/Package.js +75 -0
- package/dist/entities/system/Settings.d.ts +94 -0
- package/dist/entities/system/Settings.js +166 -0
- package/dist/entities/system/StyleProperty.d.ts +135 -0
- package/dist/entities/system/StyleProperty.js +438 -0
- package/dist/entities/system/Token.d.ts +67 -0
- package/dist/entities/system/Token.js +129 -0
- package/dist/entities/system/Tool.d.ts +134 -0
- package/dist/entities/system/Tool.js +124 -0
- package/dist/entities/system/color.d.ts +64 -0
- package/dist/entities/system/color.js +123 -0
- package/dist/entities/system/component-module.d.ts +63 -0
- package/dist/entities/system/component-module.js +258 -0
- package/dist/entities/system/copy-plan.d.ts +98 -0
- package/dist/entities/system/copy-plan.js +451 -0
- package/dist/entities/system/defineComponent.d.ts +428 -0
- package/dist/entities/system/defineComponent.js +337 -0
- package/dist/entities/system/element.d.ts +101 -0
- package/dist/entities/system/element.js +171 -0
- package/dist/entities/system/icon-library.d.ts +64 -0
- package/dist/entities/system/icon-library.js +112 -0
- package/dist/entities/system/icon-metadata.d.ts +72 -0
- package/dist/entities/system/icon-metadata.js +111 -0
- package/dist/entities/system/link-declaration.d.ts +27 -0
- package/dist/entities/system/link-declaration.js +64 -0
- package/dist/entities/system/link-plan.d.ts +160 -0
- package/dist/entities/system/link-plan.js +770 -0
- package/dist/entities/system/runtime-modules.d.ts +25 -0
- package/dist/entities/system/runtime-modules.js +38 -0
- package/dist/entities/system/style-bag.d.ts +20 -0
- package/dist/entities/system/style-bag.js +246 -0
- package/dist/entities/system/token-index.d.ts +89 -0
- package/dist/entities/system/token-index.js +112 -0
- package/dist/framework/Config.d.ts +2472 -0
- package/dist/framework/Config.js +5023 -0
- package/dist/framework/Entity.d.ts +127 -0
- package/dist/framework/Entity.js +125 -0
- package/dist/framework/authoring.d.ts +24 -0
- package/dist/framework/authoring.js +7 -0
- package/dist/framework/change-hooks.d.ts +119 -0
- package/dist/framework/change-hooks.js +131 -0
- package/dist/framework/changes.d.ts +88 -0
- package/dist/framework/changes.js +246 -0
- package/dist/framework/class-names.d.ts +260 -0
- package/dist/framework/class-names.js +630 -0
- package/dist/framework/coalesce.d.ts +25 -0
- package/dist/framework/coalesce.js +60 -0
- package/dist/framework/config-op.d.ts +76 -0
- package/dist/framework/config-op.js +43 -0
- package/dist/framework/config-operation-kind.d.ts +14 -0
- package/dist/framework/config-operation-kind.js +14 -0
- package/dist/framework/config-ops.d.ts +34 -0
- package/dist/framework/config-ops.js +187 -0
- package/dist/framework/defineConfig.d.ts +196 -0
- package/dist/framework/defineConfig.js +100 -0
- package/dist/framework/defineDerivedEntity.d.ts +74 -0
- package/dist/framework/defineDerivedEntity.js +83 -0
- package/dist/framework/defineEntity.d.ts +318 -0
- package/dist/framework/defineEntity.js +183 -0
- package/dist/framework/defineSubEntity.d.ts +148 -0
- package/dist/framework/defineSubEntity.js +121 -0
- package/dist/framework/derived-mutations.d.ts +20 -0
- package/dist/framework/derived-mutations.js +38 -0
- package/dist/framework/entity-search.d.ts +29 -0
- package/dist/framework/entity-search.js +117 -0
- package/dist/framework/inline-sources.d.ts +49 -0
- package/dist/framework/inline-sources.js +388 -0
- package/dist/framework/layer-styles.d.ts +25 -0
- package/dist/framework/layer-styles.js +49 -0
- package/dist/framework/memo.d.ts +48 -0
- package/dist/framework/memo.js +84 -0
- package/dist/framework/overlay.d.ts +49 -0
- package/dist/framework/overlay.js +17 -0
- package/dist/framework/plan.d.ts +49 -0
- package/dist/framework/plan.js +73 -0
- package/dist/framework/projections.d.ts +480 -0
- package/dist/framework/projections.js +974 -0
- package/dist/framework/prop-surface.d.ts +132 -0
- package/dist/framework/prop-surface.js +278 -0
- package/dist/framework/ref-graph.d.ts +71 -0
- package/dist/framework/ref-graph.js +158 -0
- package/dist/framework/ref-integrity.d.ts +55 -0
- package/dist/framework/ref-integrity.js +171 -0
- package/dist/framework/registered.d.ts +95 -0
- package/dist/framework/registry-dir.d.ts +42 -0
- package/dist/framework/registry-dir.js +48 -0
- package/dist/framework/registry.d.ts +300 -0
- package/dist/framework/registry.js +104 -0
- package/dist/framework/rejection.d.ts +58 -0
- package/dist/framework/rejection.js +111 -0
- package/dist/framework/rename-inference.d.ts +26 -0
- package/dist/framework/rename-inference.js +50 -0
- package/dist/framework/render-spec.d.ts +296 -0
- package/dist/framework/render-spec.js +809 -0
- package/dist/framework/schema-version.d.ts +140 -0
- package/dist/framework/schema-version.js +214 -0
- package/dist/framework/schemas.d.ts +109 -0
- package/dist/framework/schemas.js +615 -0
- package/dist/framework/session.d.ts +196 -0
- package/dist/framework/session.js +367 -0
- package/dist/framework/signature.d.ts +15 -0
- package/dist/framework/signature.js +83 -0
- package/dist/framework/snapshot.d.ts +115 -0
- package/dist/framework/snapshot.js +277 -0
- package/dist/framework/source-integrity.d.ts +226 -0
- package/dist/framework/source-integrity.js +478 -0
- package/dist/framework/sources.d.ts +211 -0
- package/dist/framework/sources.js +89 -0
- package/dist/framework/utils/compiled-schema.d.ts +23 -0
- package/dist/framework/utils/compiled-schema.js +46 -0
- package/dist/framework/utils/content-key.d.ts +15 -0
- package/dist/framework/utils/content-key.js +33 -0
- package/dist/framework/utils/enumerated.d.ts +13 -0
- package/dist/framework/utils/enumerated.js +19 -0
- package/dist/framework/utils/field-path.d.ts +95 -0
- package/dist/framework/utils/field-path.js +136 -0
- package/dist/framework/utils/field-schema.d.ts +55 -0
- package/dist/framework/utils/field-schema.js +183 -0
- package/dist/framework/utils/group.d.ts +37 -0
- package/dist/framework/utils/group.js +13 -0
- package/dist/framework/utils/package-path.d.ts +37 -0
- package/dist/framework/utils/package-path.js +61 -0
- package/dist/framework/utils/refs.d.ts +194 -0
- package/dist/framework/utils/refs.js +403 -0
- package/dist/framework/validate-spec.d.ts +27 -0
- package/dist/framework/validate-spec.js +45 -0
- package/dist/framework/value-domain.d.ts +26 -0
- package/dist/framework/value-domain.js +202 -0
- package/dist/framework/views-facade.d.ts +15 -0
- package/dist/framework/views-facade.js +19 -0
- package/dist/index.d.ts +90 -42
- package/dist/index.js +86 -37
- package/dist/migrations/2.0.0/raw-css.d.ts +11 -0
- package/dist/migrations/2.0.0/raw-css.js +72 -0
- package/dist/migrations/2.0.0/v1-artifact.d.ts +1559 -0
- package/dist/migrations/2.0.0/v1-artifact.js +1018 -0
- package/dist/migrations/20260908171448_settings_as_kind.d.ts +22 -0
- package/dist/migrations/20260908171448_settings_as_kind.js +89 -0
- package/dist/migrations/20260908194245_visible_state_pointer.d.ts +19 -0
- package/dist/migrations/20260908194245_visible_state_pointer.js +115 -0
- package/dist/migrations/20260909183001_icon_knockout_variants.d.ts +18 -0
- package/dist/migrations/20260909183001_icon_knockout_variants.js +8 -0
- package/dist/migrations/20260910212453_linked_system_link_ops.d.ts +13 -0
- package/dist/migrations/20260910212453_linked_system_link_ops.js +8 -0
- package/dist/migrations/index.d.ts +2 -0
- package/dist/migrations/index.js +2 -0
- package/dist/react-native/style-keys.generated.d.ts +10 -0
- package/dist/react-native/style-keys.generated.js +167 -0
- package/dist/renderer/RendererErrorBoundary.d.ts +32 -5
- package/dist/renderer/RendererErrorBoundary.js +59 -10
- package/dist/renderer/UdsRenderer.d.ts +11 -10
- package/dist/renderer/UdsRenderer.js +1 -1
- package/dist/renderer/assetEntries.d.ts +37 -0
- package/dist/renderer/assetEntries.js +39 -0
- package/dist/renderer/assetRenderable.d.ts +14 -6
- package/dist/renderer/assetRenderable.js +8 -2
- package/dist/renderer/componentEntry.d.ts +58 -0
- package/dist/renderer/componentEntry.js +71 -0
- package/dist/renderer/icon-library.d.ts +20 -0
- package/dist/renderer/index.d.ts +7 -7
- package/dist/renderer/index.js +5 -5
- package/dist/renderer/primitives/SlotRenderer.d.ts +1 -1
- package/dist/renderer/primitives/SlotRenderer.js +1 -1
- package/dist/renderer/runtimeComponent.d.ts +33 -0
- package/dist/renderer/runtimeComponent.js +113 -0
- package/dist/renderer/spec-content.d.ts +43 -0
- package/dist/renderer/spec-content.js +95 -0
- package/dist/renderer/wrapRegistry.d.ts +22 -37
- package/dist/renderer/wrapRegistry.js +10 -20
- package/dist/renderer/wrappers/event-bridge.js +6 -5
- package/dist/renderer/wrappers/hex-normalize.js +6 -5
- package/dist/renderer/wrappers/html-aliases.d.ts +7 -2
- package/dist/renderer/wrappers/html-aliases.js +36 -19
- package/dist/renderer/wrappers/inline-styles.js +7 -6
- package/dist/spec/asset-jsx.d.ts +18 -5
- package/dist/spec/asset-jsx.js +27 -16
- package/dist/spec/collapse-text-labels.d.ts +5 -15
- package/dist/spec/collapse-text-labels.js +26 -19
- package/dist/spec/empty-node-slots.d.ts +56 -34
- package/dist/spec/empty-node-slots.js +87 -47
- package/dist/spec/index.d.ts +21 -10
- package/dist/spec/index.js +21 -10
- package/dist/spec/jsxToSpec.d.ts +14 -9
- package/dist/spec/jsxToSpec.js +20 -3
- package/dist/spec/preview-elements.d.ts +63 -0
- package/dist/spec/preview-elements.js +89 -0
- package/dist/spec/rewrite-refs.d.ts +58 -0
- package/dist/spec/rewrite-refs.js +250 -0
- package/dist/spec/specToJsx.js +18 -29
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +39 -52
- package/dist/AssetGroup.d.ts +0 -77
- package/dist/AssetGroup.js +0 -125
- package/dist/Component.d.ts +0 -333
- package/dist/Component.js +0 -1061
- package/dist/ComponentGroup.d.ts +0 -22
- package/dist/ComponentGroup.js +0 -51
- package/dist/CompositeStyle.d.ts +0 -30
- package/dist/CompositeStyle.js +0 -59
- package/dist/Config.d.ts +0 -528
- package/dist/Config.js +0 -1712
- package/dist/Mode.d.ts +0 -41
- package/dist/Mode.js +0 -81
- package/dist/Modifier.d.ts +0 -51
- package/dist/Modifier.js +0 -97
- package/dist/MotionDef.d.ts +0 -81
- package/dist/MotionDef.js +0 -212
- package/dist/Props.d.ts +0 -332
- package/dist/Props.js +0 -35
- package/dist/Provider.d.ts +0 -20
- package/dist/Provider.js +0 -14
- package/dist/StyleProp.d.ts +0 -145
- package/dist/StyleProp.js +0 -239
- package/dist/Token.d.ts +0 -76
- package/dist/Token.js +0 -133
- package/dist/TokenGroup.d.ts +0 -33
- package/dist/TokenGroup.js +0 -69
- package/dist/asset-kind.d.ts +0 -55
- package/dist/asset-kind.js +0 -29
- package/dist/asset-value.d.ts +0 -23
- package/dist/asset-value.js +0 -40
- package/dist/brands.d.ts +0 -30
- package/dist/brands.js +0 -20
- package/dist/captureCallerPath.d.ts +0 -48
- package/dist/captureCallerPath.js +0 -95
- package/dist/colorExpressions.d.ts +0 -131
- package/dist/colorExpressions.js +0 -148
- package/dist/component-referrers.d.ts +0 -30
- package/dist/component-referrers.js +0 -40
- package/dist/config-diagnostics.d.ts +0 -21
- package/dist/config-diagnostics.js +0 -12
- package/dist/defineAssetGroup.d.ts +0 -200
- package/dist/defineAssetGroup.js +0 -323
- package/dist/defineProvider.d.ts +0 -29
- package/dist/defineProvider.js +0 -60
- package/dist/element-marker.d.ts +0 -63
- package/dist/element-marker.js +0 -113
- package/dist/entity-utils.d.ts +0 -56
- package/dist/entity-utils.js +0 -105
- package/dist/extension.d.ts +0 -55
- package/dist/extension.js +0 -214
- package/dist/factories.d.ts +0 -863
- package/dist/factories.js +0 -475
- package/dist/font-face.d.ts +0 -25
- package/dist/font-face.js +0 -72
- package/dist/foreign-component-name.d.ts +0 -21
- package/dist/foreign-component-name.js +0 -42
- package/dist/interpolate.d.ts +0 -20
- package/dist/interpolate.js +0 -10
- package/dist/jsx/__fixtures__/cross-component-preview.d.ts +0 -3
- package/dist/jsx/__fixtures__/cross-component-preview.js +0 -15
- package/dist/jsx/jsx-dev-runtime.d.ts +0 -15
- package/dist/jsx/jsx-dev-runtime.js +0 -11
- package/dist/jsx/jsx-runtime.d.ts +0 -48
- package/dist/jsx/jsx-runtime.js +0 -305
- package/dist/linked-system-pins.d.ts +0 -30
- package/dist/linked-system-pins.js +0 -45
- package/dist/linkedNodePrefixes.d.ts +0 -73
- package/dist/linkedNodePrefixes.js +0 -99
- package/dist/markers.d.ts +0 -235
- package/dist/markers.js +0 -70
- package/dist/motion-constants.d.ts +0 -37
- package/dist/motion-constants.js +0 -61
- package/dist/native/NativeConfig.d.ts +0 -56
- package/dist/native/NativeConfig.js +0 -162
- package/dist/native/assets.d.ts +0 -20
- package/dist/native/assets.js +0 -17
- package/dist/native/components.d.ts +0 -139
- package/dist/native/components.js +0 -67
- package/dist/native/index.d.ts +0 -18
- package/dist/native/index.js +0 -17
- package/dist/native/modes.d.ts +0 -10
- package/dist/native/modes.js +0 -11
- package/dist/native/types.d.ts +0 -95
- package/dist/native/types.js +0 -0
- package/dist/native/validation.d.ts +0 -8
- package/dist/native/validation.js +0 -885
- package/dist/native/values.d.ts +0 -7
- package/dist/native/values.js +0 -15
- package/dist/native.d.ts +0 -12
- package/dist/native.js +0 -10
- package/dist/refs.d.ts +0 -204
- package/dist/refs.js +0 -129
- package/dist/registry-aliases.d.ts +0 -33
- package/dist/registry-aliases.js +0 -35
- package/dist/registry-dir.d.ts +0 -22
- package/dist/registry-dir.js +0 -10
- package/dist/renderer/makeRegistry.d.ts +0 -34
- package/dist/renderer/makeRegistry.js +0 -52
- package/dist/renderer/makeUdsRenderer.d.ts +0 -13
- package/dist/renderer/makeUdsRenderer.js +0 -51
- package/dist/renderer/wrappers/component-slots.d.ts +0 -41
- package/dist/renderer/wrappers/component-slots.js +0 -66
- package/dist/renderer/wrappers/slot-resolution.d.ts +0 -25
- package/dist/renderer/wrappers/slot-resolution.js +0 -68
- package/dist/renderer/wrappers/void-elements.d.ts +0 -37
- package/dist/renderer/wrappers/void-elements.js +0 -40
- package/dist/spec/layer-props.d.ts +0 -52
- package/dist/spec/layer-props.js +0 -149
- package/dist/spec/preview-controls.d.ts +0 -44
- package/dist/spec/preview-controls.js +0 -139
- package/dist/spec/slot-refs.d.ts +0 -39
- package/dist/spec/slot-refs.js +0 -56
- package/dist/token-override-rows.d.ts +0 -68
- package/dist/token-override-rows.js +0 -233
- package/dist/tokenValueType.d.ts +0 -34
- package/dist/tokenValueType.js +0 -138
- package/dist/types/css-properties.d.ts +0 -233
- package/dist/types/css-properties.js +0 -14
- package/dist/types/css-property-keywords.d.ts +0 -156
- package/dist/types/css-property-keywords.js +0 -616
- package/dist/types/css-values.d.ts +0 -63
- package/dist/types/css-values.js +0 -16
- package/dist/types.d.ts +0 -1138
- package/dist/types.js +0 -12
- package/dist/units.d.ts +0 -14
- package/dist/units.js +0 -16
- package/dist/utils/index.d.ts +0 -4
- package/dist/utils/index.js +0 -4
|
@@ -0,0 +1,1840 @@
|
|
|
1
|
+
import { reject, rejectData } from "../../framework/rejection.js";
|
|
2
|
+
import { isRef, kindOf, memberOf, pathOf, ref, refLeaf, refSchema, splitRef } from "../../framework/utils/refs.js";
|
|
3
|
+
import { isPlainObject } from "../../framework/utils/field-path.js";
|
|
4
|
+
import { pathInput } from "../../framework/schemas.js";
|
|
5
|
+
import { defineEntity } from "../../framework/defineEntity.js";
|
|
6
|
+
import { defineSubEntity } from "../../framework/defineSubEntity.js";
|
|
7
|
+
import { derivedColor, gradient } from "./color.js";
|
|
8
|
+
import { childrenPolicy } from "./element.js";
|
|
9
|
+
import { listedIn } from "../../framework/utils/enumerated.js";
|
|
10
|
+
import { valueSchemaOf } from "../../framework/value-domain.js";
|
|
11
|
+
import { Style, addBagKeyIssues, bagNameEdges, bagRewriteName, bindBagTokens, collectBag, isPlainRecord, openBag, styleValue } from "./style-bag.js";
|
|
12
|
+
import { propValueDomain, resolveComponentProps, stylePropValueLeaves, validateComponentProps } from "../../framework/projections.js";
|
|
13
|
+
import { intoName, routedPropIn, routesContent } from "../../framework/prop-surface.js";
|
|
14
|
+
import { slotAnatomyPropOf, slotTargetsOf, withoutTruthyTerm } from "../../framework/render-spec.js";
|
|
15
|
+
import { Package } from "./Package.js";
|
|
16
|
+
import { File } from "./File.js";
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
//#region src/entities/system/Component.ts
|
|
19
|
+
/**
|
|
20
|
+
* Component — three things, in order:
|
|
21
|
+
*
|
|
22
|
+
* 1. `layers` — a custom name → the element it points at. A layer is its element.
|
|
23
|
+
* 2. `props` — what this component exposes (its public API). Each carries its own domain
|
|
24
|
+
* under `value`, so what may be assigned through it needs no second declaration.
|
|
25
|
+
* 3. `styles` — *when props are X, apply these props to these layers.*
|
|
26
|
+
*
|
|
27
|
+
* Step 3 replaces four shapes that were the same statement with a different condition: `base`
|
|
28
|
+
* (no condition), a variant value's bags (one), a boolean's `true`/`false` (one), and
|
|
29
|
+
* `compoundProps` (two or more). They are now one collection of rules, each keyed by its own
|
|
30
|
+
* condition — so a rule's identity is its condition, duplicates are structurally impossible, and
|
|
31
|
+
* precedence is specificity rather than stored order.
|
|
32
|
+
*
|
|
33
|
+
* There is no leaf/composite union: `inherits` names a layer whose element's props this component
|
|
34
|
+
* also exposes, which is the only thing that distinction really encoded (a leaf `extends`ing a
|
|
35
|
+
* Component inherited its API; a composite's layer pointing at one did not).
|
|
36
|
+
*
|
|
37
|
+
* The style-rule schema is built per owner (a `fields` factory): a rule may only target layers this
|
|
38
|
+
* Component declares, may only set props those layers' elements actually expose, and may only
|
|
39
|
+
* condition on this component's own props with their own values — all enforced by the schema
|
|
40
|
+
* itself, so none of it is a refinement.
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* Write an inserted subtree: every node first, then every edge.
|
|
44
|
+
*
|
|
45
|
+
* Two passes because a node's stored `children` is narrowed to the component's existing node names —
|
|
46
|
+
* so an edge written before its target exists is refused. Creating all of them first means the edges
|
|
47
|
+
* validate against a component that already has every node they name, and no ordering of the caller's
|
|
48
|
+
* map matters.
|
|
49
|
+
*/
|
|
50
|
+
function insertSubtree(config, path, nodes) {
|
|
51
|
+
let next = config;
|
|
52
|
+
for (const [key, node] of Object.entries(nodes)) next = next.apply({
|
|
53
|
+
kind: "component",
|
|
54
|
+
operation: "sub-create",
|
|
55
|
+
path,
|
|
56
|
+
field: "layers",
|
|
57
|
+
key,
|
|
58
|
+
data: asElementRef(config, node.element ?? "div")
|
|
59
|
+
}).apply({
|
|
60
|
+
kind: "component",
|
|
61
|
+
operation: "sub-create",
|
|
62
|
+
path,
|
|
63
|
+
field: "anatomy",
|
|
64
|
+
key,
|
|
65
|
+
data: {
|
|
66
|
+
element: layerRef(key),
|
|
67
|
+
...node.props ? { props: node.props } : {},
|
|
68
|
+
...node.visible !== void 0 ? { visible: node.visible } : {},
|
|
69
|
+
...node.text !== void 0 ? { text: node.text } : {}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
for (const [key, node] of Object.entries(nodes)) {
|
|
73
|
+
if (!node.children?.length && !node.slots) continue;
|
|
74
|
+
next = next.apply({
|
|
75
|
+
kind: "component",
|
|
76
|
+
operation: "sub-update",
|
|
77
|
+
path,
|
|
78
|
+
field: "anatomy",
|
|
79
|
+
key,
|
|
80
|
+
data: {
|
|
81
|
+
...node.children?.length ? { children: node.children } : {},
|
|
82
|
+
...node.slots ? { slots: node.slots } : {}
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return next;
|
|
87
|
+
}
|
|
88
|
+
/** A component-relative reference to one of the enclosing component's own layers — a slot's
|
|
89
|
+
* `target`. Relative (no component path) because a slot only ever targets a sibling layer, so a
|
|
90
|
+
* Component rename can't break it; a layer rename cascades through it. */
|
|
91
|
+
function layerRef(layerKey) {
|
|
92
|
+
return ref("layer", layerKey);
|
|
93
|
+
}
|
|
94
|
+
/** The layer a node is styled through — the target of a `layer:` ref — or undefined for anything else. */
|
|
95
|
+
function layerRefTarget(element) {
|
|
96
|
+
const target = element?.__ref;
|
|
97
|
+
if (typeof target !== "string") return void 0;
|
|
98
|
+
const { kind, qualifiedPath } = splitRef(target);
|
|
99
|
+
return kind === "layer" ? qualifiedPath : void 0;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The name a subtree-local key lands under once grafted onto a component.
|
|
103
|
+
*
|
|
104
|
+
* A caller holds a subtree whose keys are local to it (`root`, `label`) and the component holds one
|
|
105
|
+
* flat namespace, so the two have to be reconciled. Both directions live here, together, because a
|
|
106
|
+
* convention split across packages drifts: the writer grafts and a reader ungrafts, and the reader
|
|
107
|
+
* only works if it inverts exactly what the writer did.
|
|
108
|
+
*/
|
|
109
|
+
function graftKey(layer, local) {
|
|
110
|
+
return `${layer}__${local}`;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* A layer name nothing is using yet.
|
|
114
|
+
*
|
|
115
|
+
* The entity owns the anatomy namespace, so it is the only thing that can answer this without
|
|
116
|
+
* guessing — callers were threading a `mintLayerKey` through six planner signatures to do it from
|
|
117
|
+
* outside, each defaulting to its own copy of the same `el_ins_<uuid8>` convention.
|
|
118
|
+
*
|
|
119
|
+
* An ordinal rather than a uuid, for two reasons: the key becomes part of the emitted class name, so
|
|
120
|
+
* it is read by people; and a uuid makes two replays of one patch produce different names, where the
|
|
121
|
+
* point of a stored patch is that replaying it lands the same config.
|
|
122
|
+
*
|
|
123
|
+
* `layers` and `anatomy` share a namespace in practice — an insert writes both under one key — so
|
|
124
|
+
* both are checked.
|
|
125
|
+
*/
|
|
126
|
+
function freeLayerKey(config, path, hint = "el") {
|
|
127
|
+
const body = config.resolve("component", path)?.toJSON();
|
|
128
|
+
const taken = new Set([...Object.keys(body?.layers ?? {}), ...Object.keys(body?.anatomy ?? {})]);
|
|
129
|
+
if (!taken.has(hint)) return hint;
|
|
130
|
+
let ordinal = 1;
|
|
131
|
+
while (taken.has(`${hint}${ordinal}`)) ordinal += 1;
|
|
132
|
+
return `${hint}${ordinal}`;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* A `kind:path` marker string as the ref it means.
|
|
136
|
+
*
|
|
137
|
+
* A spec element stores its type as a marker (`component:primitives/Box`), and `element` accepts a
|
|
138
|
+
* union of string-or-ref whose first arm is the string — so a marker matched the string arm and was
|
|
139
|
+
* stored as one. It still rendered, because the readers accept markers too, which is what made this
|
|
140
|
+
* invisible: the value was simply absent from the dependency graph, so deleting the target passed its
|
|
141
|
+
* referrer check and renaming it never cascaded.
|
|
142
|
+
*
|
|
143
|
+
* Coerced against the config rather than by pattern, so a tag that merely contains a colon isn't
|
|
144
|
+
* mistaken for a reference to a kind nothing has registered.
|
|
145
|
+
*/
|
|
146
|
+
function asElementRef(config, value) {
|
|
147
|
+
if (typeof value !== "string") return value;
|
|
148
|
+
const { kind, qualifiedPath } = splitRef(value);
|
|
149
|
+
if (!kind || kind === value) return value;
|
|
150
|
+
return config.entityOf(kind) ? ref(kind, qualifiedPath) : value;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* A component-relative reference to one value of one of the enclosing component's own variant
|
|
154
|
+
* props — what a style rule conditions on.
|
|
155
|
+
*
|
|
156
|
+
* Relative for the same reason {@link layerRef} is: a value only ever belongs to a sibling prop, so
|
|
157
|
+
* a component rename can't break it. What it buys is the other direction — a value rename is an
|
|
158
|
+
* ordinary ref rewrite, and because a rule's key is derived from its condition, the rules holding it
|
|
159
|
+
* re-key themselves. Without the ref the name sits in a plain string, invisible to that rewrite, and
|
|
160
|
+
* every rule conditioned on the old name silently stops applying.
|
|
161
|
+
*/
|
|
162
|
+
function valueRef(prop, value) {
|
|
163
|
+
return ref("value", `${prop}/${value}`);
|
|
164
|
+
}
|
|
165
|
+
/** What a layer renders as: a native tag (`'div'`), a ref to another Component, or a package
|
|
166
|
+
* export (a `package:` ref). A native import (`import { View } from 'react-native'`) arrives as
|
|
167
|
+
* that ref already: the loader virtualizes the package and binds each export to
|
|
168
|
+
* `package:<module>#<export>` (codegen's `nativePackagesPlugin`). Lazy + typed on the Component
|
|
169
|
+
* arm because this is the same self-referential kind. */
|
|
170
|
+
const element = z.union([
|
|
171
|
+
z.string(),
|
|
172
|
+
z.lazy(() => Component),
|
|
173
|
+
Package
|
|
174
|
+
], { error: "a layer renders an intrinsic tag (`div`), a component (`{ \"__ref\": \"component:<path>\" }`) or a package export (`{ \"__ref\": \"package:<export>\" }`). A glyph is not a layer: it is an anatomy node, `{ \"element\": { \"__ref\": \"icon:<library>/<Name>\" } }`, listed in the `children` of the layer node that holds it." });
|
|
175
|
+
/** A `children` or `slots` entry that is not a string: the writer inlined the node instead of naming it. */
|
|
176
|
+
const childKeyError = (issue) => issue.code === "invalid_type" ? "a child is the KEY of a sibling node, written as a string; define the node under its own key in `anatomy` and name that key here" : void 0;
|
|
177
|
+
const Layer = defineSubEntity({
|
|
178
|
+
name: "layer",
|
|
179
|
+
label: "Layer",
|
|
180
|
+
fields: element,
|
|
181
|
+
/**
|
|
182
|
+
* A slot's `target` is a `layer:` ref, so deleting the layer has to take the slot with it — the
|
|
183
|
+
* same cascade `VariantValue` declares for the rules conditioned on it. Without this, deleting a
|
|
184
|
+
* targeted layer left the prop pointing at nothing: `dependentsOf` cannot see it (a `layer:` ref
|
|
185
|
+
* is component-relative and so is not in the graph), and nothing else was checking, so the
|
|
186
|
+
* Component kept a slot that routed content to an element that no longer existed.
|
|
187
|
+
*/
|
|
188
|
+
orphans: ["props", "forwards"],
|
|
189
|
+
keyedByNameIn: ["forwards", "styles/*/layers"]
|
|
190
|
+
});
|
|
191
|
+
/**
|
|
192
|
+
* Which of a layer's element's props this component also exposes at its own top level, so
|
|
193
|
+
* `<Field disabled />` means the `input` layer's `disabled` and no caller reaches through
|
|
194
|
+
* `layerProps`.
|
|
195
|
+
*
|
|
196
|
+
* Keyed by layer, like `layers` itself — a layer cannot acquire two conflicting entries by
|
|
197
|
+
* construction, each entry is addressable on its own (`sub-update` on `forwards`, key `input`), and
|
|
198
|
+
* nothing downstream has to learn a new shape.
|
|
199
|
+
*
|
|
200
|
+
* The member is the selection, with no options wrapper, because there is exactly one thing to say
|
|
201
|
+
* and there always will be: `'*'` for everything that element exposes, or a list of member refs
|
|
202
|
+
* naming the props to take. Removing a forwarded prop is the existing `props: { x: null }` tombstone,
|
|
203
|
+
* so there is no `except` — and renaming is the singular `bind()` form, so there is no rename here.
|
|
204
|
+
*
|
|
205
|
+
* A list holds refs rather than names so the dependency graph sees the edge: `collectRefs` harvests
|
|
206
|
+
* every `{__ref}` generically, which is what makes `dependents('component:Input#props/disabled')`
|
|
207
|
+
* name this component, so a rename cascades and a delete is refused. `'*'` holds no ref and so
|
|
208
|
+
* cannot rot — it tracks a rename for free.
|
|
209
|
+
*/
|
|
210
|
+
/** The prop name a forwarding member ref identifies, or `undefined` for any other ref shape. */
|
|
211
|
+
function forwardedPropName(value) {
|
|
212
|
+
if (!isRef(value) || kindOf(value.__ref) !== "component") return void 0;
|
|
213
|
+
const member = memberOf(value);
|
|
214
|
+
if (!member?.startsWith("props/")) return void 0;
|
|
215
|
+
const name = member.slice(6);
|
|
216
|
+
return name && !name.includes("/") ? name : void 0;
|
|
217
|
+
}
|
|
218
|
+
const forwardedPropRef = refSchema.check((ctx) => {
|
|
219
|
+
if (forwardedPropName(ctx.value) !== void 0) return;
|
|
220
|
+
ctx.issues.push({
|
|
221
|
+
code: "custom",
|
|
222
|
+
input: ctx.value,
|
|
223
|
+
message: "a narrowed forward must contain component prop MEMBER REFS (`component:Input#props/disabled`)"
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
const forwardMember = z.union([z.literal("*"), z.array(forwardedPropRef)], { error: "a forward is '*' or a list of component prop MEMBER REFS — a bare prop name is neither. `.forwards({ layer: ['disabled'] })` mints the refs from names; a patch supplies them. `forwards` exposes a layer's element props (`{ \"<layer>\": \"*\" }`); to route a prop's content into a layer, declare the prop in `props` as `{ \"type\": \"forward\", \"target\": \"<layer>/children\" }`." });
|
|
227
|
+
/** The prop names one entry exposes — `null` when the config can't say (an unresolved element). */
|
|
228
|
+
function forwardedNames(config, element, entry) {
|
|
229
|
+
if (Array.isArray(entry)) return entry.map(forwardedPropName).filter((name) => !!name);
|
|
230
|
+
if (entry !== "*" || !isRef(element)) return null;
|
|
231
|
+
return Object.keys(resolveComponentProps(config, pathOf(element.__ref)));
|
|
232
|
+
}
|
|
233
|
+
const Forward = defineSubEntity({
|
|
234
|
+
name: "forward",
|
|
235
|
+
label: "Forward",
|
|
236
|
+
labelPlural: "Forwards",
|
|
237
|
+
base: forwardMember,
|
|
238
|
+
fields: ({ config, parent, key }) => {
|
|
239
|
+
const body = parent;
|
|
240
|
+
if (!config || !body || !key) return forwardMember;
|
|
241
|
+
const layers = body.layers ?? {};
|
|
242
|
+
const element = layers[key];
|
|
243
|
+
return forwardMember.check((ctx) => {
|
|
244
|
+
if (!(key in layers)) {
|
|
245
|
+
ctx.issues.push({
|
|
246
|
+
code: "custom",
|
|
247
|
+
input: ctx.value,
|
|
248
|
+
message: `"${key}" is not one of this component's layers (${Object.keys(layers).join(", ") || "none"}).`
|
|
249
|
+
});
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (!isRef(element)) {
|
|
253
|
+
ctx.issues.push({
|
|
254
|
+
code: "custom",
|
|
255
|
+
input: ctx.value,
|
|
256
|
+
message: `layer "${key}" renders ${typeof element === "string" ? `<${element}>` : "no component"}, which declares no props to forward.`
|
|
257
|
+
});
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const exposes = resolveComponentProps(config, pathOf(element.__ref));
|
|
261
|
+
if (Array.isArray(ctx.value)) for (const entry of ctx.value) {
|
|
262
|
+
const name = forwardedPropName(entry);
|
|
263
|
+
if (name && !(name in exposes)) ctx.issues.push({
|
|
264
|
+
code: "custom",
|
|
265
|
+
input: entry,
|
|
266
|
+
message: `"${name}" is not a prop of layer "${key}"'s element (${pathOf(element.__ref)}).`
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
const mine = forwardedNames(config, element, ctx.value);
|
|
270
|
+
if (!mine) return;
|
|
271
|
+
for (const [otherKey, otherEntry] of Object.entries(body.forwards ?? {})) {
|
|
272
|
+
if (otherKey === key) continue;
|
|
273
|
+
if (ctx.value === "*" && otherEntry === "*") {
|
|
274
|
+
ctx.issues.push({
|
|
275
|
+
code: "custom",
|
|
276
|
+
input: ctx.value,
|
|
277
|
+
message: `only one layer may forward '*' — "${otherKey}" already does. List the props this layer should expose instead, or route one with bind().`
|
|
278
|
+
});
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const theirs = forwardedNames(config, layers[otherKey], otherEntry);
|
|
282
|
+
if (!theirs) continue;
|
|
283
|
+
const shared = mine.filter((name) => theirs.includes(name));
|
|
284
|
+
if (shared.length > 0) ctx.issues.push({
|
|
285
|
+
code: "custom",
|
|
286
|
+
input: ctx.value,
|
|
287
|
+
message: `layers "${key}" and "${otherKey}" would both forward ${shared.slice(0, 3).map((n) => `"${n}"`).join(", ")}${shared.length > 3 ? ` (+${shared.length - 3} more)` : ""}. Route one explicitly with bind().`
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
const VariantValue = defineSubEntity({
|
|
294
|
+
name: "value",
|
|
295
|
+
label: "Value",
|
|
296
|
+
fields: z.strictObject({
|
|
297
|
+
/** What to call it, and what it's for, where it is offered — a picker row, the AI's catalogue.
|
|
298
|
+
* The name is an identifier and reads like one (`ui6`, `xs`); this is the human half. */
|
|
299
|
+
label: z.string().optional(),
|
|
300
|
+
description: z.string().optional(),
|
|
301
|
+
/** Withdrawn from new work, exactly as a component's own `deprecated` is: still valid wherever
|
|
302
|
+
* it is already used, left out of surfaces that offer values. The alternative to deleting it,
|
|
303
|
+
* which takes its styling with it. */
|
|
304
|
+
deprecated: z.boolean().optional()
|
|
305
|
+
}),
|
|
306
|
+
namedValuesIn: ["defaultProps", "previewProps"],
|
|
307
|
+
orphans: ["styles"]
|
|
308
|
+
});
|
|
309
|
+
/**
|
|
310
|
+
* A variant's domain: the values it owns, or a ref that delegates to whatever owns them.
|
|
311
|
+
*
|
|
312
|
+
* The array form is what an author writes (`['solid', 'ghost']`) and is coerced here into the
|
|
313
|
+
* collection it is stored as, so every reader below this line sees one shape. In the schema rather
|
|
314
|
+
* than in the authoring adapter because there are four doors into stored state — `register`, an
|
|
315
|
+
* item create/update, a sub-op, and the artifact loader — and a coercion outside the schema only
|
|
316
|
+
* covers the one it is written in.
|
|
317
|
+
*/
|
|
318
|
+
const variantDomain = z.union([
|
|
319
|
+
z.array(z.string()).transform((names) => Object.fromEntries(names.map((name) => [name, {}]))),
|
|
320
|
+
refSchema,
|
|
321
|
+
VariantValue
|
|
322
|
+
]);
|
|
323
|
+
/** Every prop type names its domain `value`, so one generic rule (`valueSchemaOf`) derives what
|
|
324
|
+
* may be assigned through it: a keyed collection enumerates, a ref delegates to whatever owns the
|
|
325
|
+
* values, a plain value gives its type (and doubles as the default). */
|
|
326
|
+
/**
|
|
327
|
+
* A routed prop's target, as authored: `<layer>/<prop>`.
|
|
328
|
+
*
|
|
329
|
+
* One segment either side, so `'label'` alone is refused rather than silently splitting into a layer
|
|
330
|
+
* with an empty prop name — the mistake the `layer()` helper's type used to catch.
|
|
331
|
+
*/
|
|
332
|
+
const layerPath = z.string().regex(/^[^/]+\/[^/]+$/, "expected `<layer>/<prop>`");
|
|
333
|
+
/**
|
|
334
|
+
* Split an authored `<layer>/<prop>` target into the pair the readers want.
|
|
335
|
+
*
|
|
336
|
+
* In the schema rather than in the authoring adapter, for the reason `variantDomain` gives: there are
|
|
337
|
+
* four doors into stored state — `register`, an item create/update, a sub-op, and the artifact loader —
|
|
338
|
+
* and a coercion outside the schema only covers the one it is written in. A prop written by a Studio
|
|
339
|
+
* Patch comes through a different door than one written in a config file, and both mean the same thing.
|
|
340
|
+
*/
|
|
341
|
+
function routedTarget(decl) {
|
|
342
|
+
if (!("target" in decl) || typeof decl.target !== "string") return decl;
|
|
343
|
+
const separator = decl.target.indexOf("/");
|
|
344
|
+
return {
|
|
345
|
+
...decl,
|
|
346
|
+
target: layerRef(decl.target.slice(0, separator)),
|
|
347
|
+
into: decl.target.slice(separator + 1)
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
const propShape = z.discriminatedUnion("type", [
|
|
351
|
+
z.strictObject({
|
|
352
|
+
type: z.literal("slot"),
|
|
353
|
+
text: z.boolean().optional(),
|
|
354
|
+
accepts: z.union([refSchema, z.array(refSchema)]).optional(),
|
|
355
|
+
required: z.boolean().optional()
|
|
356
|
+
}),
|
|
357
|
+
z.strictObject({
|
|
358
|
+
type: z.literal("variant"),
|
|
359
|
+
value: variantDomain,
|
|
360
|
+
required: z.boolean().optional()
|
|
361
|
+
}),
|
|
362
|
+
z.strictObject({
|
|
363
|
+
type: z.literal("forward"),
|
|
364
|
+
target: z.union([layerPath, refSchema]),
|
|
365
|
+
into: z.union([z.string(), refSchema]).optional(),
|
|
366
|
+
accepts: z.union([refSchema, z.array(refSchema)]).optional(),
|
|
367
|
+
text: z.boolean().optional(),
|
|
368
|
+
required: z.boolean().optional()
|
|
369
|
+
}).transform(routedTarget),
|
|
370
|
+
z.strictObject({
|
|
371
|
+
type: z.literal("styleProperty"),
|
|
372
|
+
value: refSchema
|
|
373
|
+
}),
|
|
374
|
+
z.strictObject({
|
|
375
|
+
type: z.literal("composite"),
|
|
376
|
+
value: refSchema
|
|
377
|
+
}),
|
|
378
|
+
z.strictObject({
|
|
379
|
+
type: z.literal("motion"),
|
|
380
|
+
value: refSchema.default({ __ref: "motion:" })
|
|
381
|
+
}),
|
|
382
|
+
z.strictObject({
|
|
383
|
+
type: z.literal("string"),
|
|
384
|
+
value: z.string().optional(),
|
|
385
|
+
required: z.boolean().optional()
|
|
386
|
+
}),
|
|
387
|
+
z.strictObject({
|
|
388
|
+
type: z.literal("number"),
|
|
389
|
+
value: z.number().optional(),
|
|
390
|
+
required: z.boolean().optional()
|
|
391
|
+
}),
|
|
392
|
+
z.strictObject({
|
|
393
|
+
type: z.literal("boolean"),
|
|
394
|
+
value: z.boolean().optional(),
|
|
395
|
+
required: z.boolean().optional()
|
|
396
|
+
})
|
|
397
|
+
], { error: (issue) => {
|
|
398
|
+
const type = issue.input?.type;
|
|
399
|
+
if (typeof type !== "string") return void 0;
|
|
400
|
+
const types = PROP_TYPES.join(", ");
|
|
401
|
+
if (/^style-?property$/i.test(type)) return `\`${type}\` is not a prop type; a style property is exposed as \`{ "type": "styleProperty", "value": { "__ref": "style-property:<name>" } }\`. The types are ${types}.`;
|
|
402
|
+
return `\`${type}\` is not a prop type. The types are ${types}; a style property is \`{ "type": "styleProperty", "value": { "__ref": "style-property:<name>" } }\`, content is \`{ "type": "slot", "text": true }\`.`;
|
|
403
|
+
} });
|
|
404
|
+
/** The prop types, for the refusal that names them. */
|
|
405
|
+
const PROP_TYPES = [
|
|
406
|
+
"slot",
|
|
407
|
+
"variant",
|
|
408
|
+
"forward",
|
|
409
|
+
"styleProperty",
|
|
410
|
+
"composite",
|
|
411
|
+
"motion",
|
|
412
|
+
"string",
|
|
413
|
+
"number",
|
|
414
|
+
"boolean"
|
|
415
|
+
];
|
|
416
|
+
const PROP_DESCRIPTION = "What the prop is. A style property the component exposes, `{ \"type\": \"styleProperty\", \"value\": { \"__ref\": \"style-property:<name>\" } }`, which is what a primitive's props mostly are and what makes a style rule's bag key valid on it; a closed set of names a style rule keys on, `{ \"type\": \"variant\", \"value\": [\"sm\", \"lg\"] }`; content, `{ \"type\": \"slot\", \"text\": true }`; or a plain `string`, `number` or `boolean`. `null` hides a prop an inherited layer would expose.";
|
|
417
|
+
/** Nullable because `null` is the omit tombstone: a component that `inherits` its element's props
|
|
418
|
+
* drops one by declaring it `null`, rather than needing a second "don't expose" mechanism. */
|
|
419
|
+
const Prop = defineSubEntity({
|
|
420
|
+
name: "prop",
|
|
421
|
+
label: "Prop",
|
|
422
|
+
/**
|
|
423
|
+
* A factory, for one rule the shape alone can't state: `accepts` narrows content, so it is valid on a
|
|
424
|
+
* slot and on a route whose target prop is a slot — and meaningless on a route that carries a value,
|
|
425
|
+
* where what may be passed is the target prop's own domain.
|
|
426
|
+
*
|
|
427
|
+
* Whether a route carries content is computed (`routesContent`) from the target's layer and prop, which
|
|
428
|
+
* is the same question every reader asks, so the rule and the readers cannot disagree about an answer
|
|
429
|
+
* neither of them stores. It needs the owner's layers, which is what makes this a factory rather than a
|
|
430
|
+
* schema — the check is part of the schema, not a refinement bolted beside it.
|
|
431
|
+
*/
|
|
432
|
+
fields: ({ config, parent }) => {
|
|
433
|
+
if (!config || !isPlainObject(parent)) return propShape.nullable().describe(PROP_DESCRIPTION);
|
|
434
|
+
return perOwner({
|
|
435
|
+
cache: propSchemas,
|
|
436
|
+
config,
|
|
437
|
+
owner: parent,
|
|
438
|
+
build: () => propSchemaFor(config, parent).describe(PROP_DESCRIPTION)
|
|
439
|
+
});
|
|
440
|
+
},
|
|
441
|
+
namespaces: "value",
|
|
442
|
+
subEntities: { value: VariantValue },
|
|
443
|
+
keyedByNameIn: ["defaultProps", "previewProps"],
|
|
444
|
+
orphans: ["styles"]
|
|
445
|
+
});
|
|
446
|
+
/** {@link Prop}'s per-owner schema. Reached only through the memo in its `fields`. */
|
|
447
|
+
function propSchemaFor(config, owner) {
|
|
448
|
+
const layers = owner.layers;
|
|
449
|
+
return propShape.nullable().check((ctx) => {
|
|
450
|
+
const decl = ctx.value;
|
|
451
|
+
if (decl === null || !isPlainObject(decl) || decl.type !== "forward" || decl.accepts === void 0 || routesContent(config, layers, decl)) return;
|
|
452
|
+
const layer = isRef(decl.target) ? pathOf(decl.target) : String(decl.target ?? "?");
|
|
453
|
+
const prop = intoName(decl.into);
|
|
454
|
+
const routed = routedPropIn(config, layers, decl);
|
|
455
|
+
ctx.issues.push({
|
|
456
|
+
code: "custom",
|
|
457
|
+
input: decl,
|
|
458
|
+
path: ["accepts"],
|
|
459
|
+
message: routed ? `\`accepts\` says what may FILL a content hole — an icon set, so any glyph it registers can be dropped in (\`Button.startIcon\` accepts \`Icon.ref('phosphor')\`), or a component. \`${layer}/${prop}\` is a ${routed.type} prop, so it takes a value rather than content. Remove \`accepts\`, or point \`target\` at a prop declared \`{ type: 'slot' }\`.` : `\`accepts\` says what may FILL a content hole — an icon set, or a component. The \`${layer}\` layer is a plain tag, and \`children\` is the only prop a tag carries content through — so either target \`${layer}/children\`, or remove \`accepts\`.`
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
const propSchemas = /* @__PURE__ */ new WeakMap();
|
|
464
|
+
/** A rule's identity is its condition, as a canonical query string (`*`, `size=xs`,
|
|
465
|
+
* `size=xs&variant=tertiary`) — sorted, so the same condition always yields the same key however
|
|
466
|
+
* the editor collected it. */
|
|
467
|
+
function canonicalWhen(when) {
|
|
468
|
+
const entries = Object.entries(when ?? {}).filter(([, v]) => v !== void 0 && v !== null).sort(([a], [b]) => a.localeCompare(b));
|
|
469
|
+
return entries.length === 0 ? "*" : entries.map(([k, v]) => `${k}=${String(refLeaf(v))}`).join("&");
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* What a rule may set one prop to: the prop's own value domain, plus — for a prop that writes CSS —
|
|
473
|
+
* the value forms a style value takes whatever values it enumerates.
|
|
474
|
+
*
|
|
475
|
+
* `styleValue`, the bag a layer gets when its element isn'T a resolvable component, has always
|
|
476
|
+
* modelled a computed colour and a gradient. Narrowing to a prop's leaves has to add precision to
|
|
477
|
+
* the leaf case rather than drop those forms, or the same rule validates or not depending on whether
|
|
478
|
+
* the layer's element happens to resolve — which is a property of the config, not of the rule.
|
|
479
|
+
*/
|
|
480
|
+
function styleBagValue(config, declaration, name = "<prop>") {
|
|
481
|
+
const domain = valueSchemaOf(declaration, config);
|
|
482
|
+
const bound = declaration?.value;
|
|
483
|
+
if (declaration?.type === "slot") return z.union([domain, z.never()], { error: () => `\`${name}\` is a slot, so a rule may set it to a string of content and nothing else. A glyph or a nested element is an anatomy node under the layer's node — \`{ "element": { "__ref": "icon:<library>/<Name>" }, "visible": { "$state": "<prop>", "eq": "<value>" } }\` to show one per variant value — and the instance's own content arrives through the prop.` });
|
|
484
|
+
if (!isRef(bound) || kindOf(bound.__ref) !== "style-property") return domain;
|
|
485
|
+
const property = pathOf(bound.__ref);
|
|
486
|
+
return z.union([
|
|
487
|
+
domain,
|
|
488
|
+
derivedColor,
|
|
489
|
+
gradient,
|
|
490
|
+
refSchema
|
|
491
|
+
], { error: () => {
|
|
492
|
+
const leaves = stylePropValueLeaves(config, property);
|
|
493
|
+
const shown = leaves.slice(0, 12).map((leaf) => `\`${leaf}\``);
|
|
494
|
+
const more = leaves.length > 12 ? `, … (${leaves.length} in all)` : "";
|
|
495
|
+
const arbitrary = (config.resolve("style-property", property)?.toJSON())?.arbitrary === true ? ", or a bracketed literal such as `[13px]`" : "";
|
|
496
|
+
return `\`${name}\` takes one of ${shown.join(", ")}${more}${arbitrary}, or a token ref. A value the system has no token for is added to the style property's \`values\` first.`;
|
|
497
|
+
} });
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* What a rule may set on one layer.
|
|
501
|
+
*
|
|
502
|
+
* A layer bag is props for that layer, under the rule's condition — not a stylesheet fragment. Some
|
|
503
|
+
* of those props are style properties, and a style property is only a projection rule (prop value →
|
|
504
|
+
* CSS property), so the emitter can bake them into the layer's class instead of passing them; the
|
|
505
|
+
* rest are routed to the element as ordinary props. Both halves are the same kind of thing, which is
|
|
506
|
+
* why they live in one bag.
|
|
507
|
+
*
|
|
508
|
+
* So the bag is OPEN, like the raw-tag case below, and narrowing is about values rather than keys:
|
|
509
|
+
*
|
|
510
|
+
* - a prop the element declares is typed by its own domain, so `variant: 'bogus'` is still caught;
|
|
511
|
+
* - every registered style property is typed as a style value, whatever the element declares —
|
|
512
|
+
* a layer backed by a primitive and one backed by a closed component must accept the same rule,
|
|
513
|
+
* since which of the two it happens to be says nothing about the rule (the same objection
|
|
514
|
+
* {@link styleBagValue} already records for the resolvable-vs-not case);
|
|
515
|
+
* - anything else is an attribute of the element (`type`, `aria-*`, `data-*`), which can't be
|
|
516
|
+
* enumerated and so can't be checked — exactly the position a `div` layer is already in.
|
|
517
|
+
*
|
|
518
|
+
* A modifier block (`_hover`) nests a bag of the same shape.
|
|
519
|
+
*/
|
|
520
|
+
function bagFor(config, layerElement) {
|
|
521
|
+
const key = isRef(layerElement) ? layerElement.__ref : "";
|
|
522
|
+
let bags = layerBags.get(config);
|
|
523
|
+
if (!bags) {
|
|
524
|
+
bags = /* @__PURE__ */ new Map();
|
|
525
|
+
layerBags.set(config, bags);
|
|
526
|
+
}
|
|
527
|
+
const cached = bags.get(key);
|
|
528
|
+
if (cached) return cached;
|
|
529
|
+
const built = buildBagFor(config, layerElement);
|
|
530
|
+
bags.set(key, built);
|
|
531
|
+
return built;
|
|
532
|
+
}
|
|
533
|
+
/** One element's bag, built. Reached only through the memo above. */
|
|
534
|
+
function buildBagFor(config, layerElement) {
|
|
535
|
+
const shape = { ...stylePropShape(config) };
|
|
536
|
+
const component = isRef(layerElement) && config.resolveRef(layerElement.__ref) ? pathOf(layerElement.__ref) : void 0;
|
|
537
|
+
for (const [name, decl] of Object.entries(component ? resolveComponentProps(config, component) : {})) shape[name] = styleBagValue(config, decl, name).optional();
|
|
538
|
+
const declared = component ? (config.resolve("component", component)?.toJSON())?.props ?? {} : {};
|
|
539
|
+
for (const [name, decl] of Object.entries(declared)) {
|
|
540
|
+
if (decl?.type !== "slot" || name in shape) continue;
|
|
541
|
+
shape[name] = styleBagValue(config, decl, name).optional();
|
|
542
|
+
}
|
|
543
|
+
shape.as ??= z.string().optional();
|
|
544
|
+
let bag;
|
|
545
|
+
const nested = z.lazy(() => bag).optional();
|
|
546
|
+
for (const mod of config.list("modifier")) shape[`_${mod.leaf}`] = nested;
|
|
547
|
+
bag = z.object(shape).catchall(styleValue).superRefine((parsed, ctx) => addBagKeyIssues({
|
|
548
|
+
config,
|
|
549
|
+
bag: parsed,
|
|
550
|
+
ctx,
|
|
551
|
+
nested: false,
|
|
552
|
+
declared: new Set(Object.keys(shape))
|
|
553
|
+
})).transform((parsed) => bindBagTokens(config, parsed));
|
|
554
|
+
return bag;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Every registered style property as a bag key, built once per config.
|
|
558
|
+
*
|
|
559
|
+
* `bagFor` runs per layer and there are a couple of hundred style properties, so building their
|
|
560
|
+
* value schemas per bag would put the cost back that pointing the modifier keys at one bag took out
|
|
561
|
+
* (see below). The shape is a pure function of the config's style properties, so one copy serves
|
|
562
|
+
* every layer of every component — callers spread it and then overwrite with the element's own
|
|
563
|
+
* declarations, so the shared object is never mutated.
|
|
564
|
+
*/
|
|
565
|
+
const stylePropShapes = /* @__PURE__ */ new WeakMap();
|
|
566
|
+
/** {@link bagFor}'s memo — per config, then per element ref (`''` for every element that resolves to
|
|
567
|
+
* no component, which all share the one open bag). */
|
|
568
|
+
const layerBags = /* @__PURE__ */ new WeakMap();
|
|
569
|
+
function stylePropShape(config) {
|
|
570
|
+
const cached = stylePropShapes.get(config);
|
|
571
|
+
if (cached) return cached;
|
|
572
|
+
const shape = {};
|
|
573
|
+
for (const prop of config.list("style-property")) shape[prop.path] = styleBagValue(config, {
|
|
574
|
+
type: "styleProperty",
|
|
575
|
+
value: { __ref: `style-property:${prop.path}` }
|
|
576
|
+
}, prop.path).optional();
|
|
577
|
+
stylePropShapes.set(config, shape);
|
|
578
|
+
return shape;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Whether a prop's values belong to this component — an enumerated variant, whose values exist
|
|
582
|
+
* nowhere else and so have no identity until one is given here.
|
|
583
|
+
*
|
|
584
|
+
* A variant that delegates its domain to a ref (an icon prop drawing on the icon library) names
|
|
585
|
+
* values another entity already owns; those rename through their owner's own cascade, and minting a
|
|
586
|
+
* Component-relative identity for them would be a second name for the same thing.
|
|
587
|
+
*/
|
|
588
|
+
function ownsItsValues(declaration) {
|
|
589
|
+
const decl = declaration;
|
|
590
|
+
if (decl?.type !== "variant") return false;
|
|
591
|
+
return decl.value !== void 0 && !isRef(decl.value);
|
|
592
|
+
}
|
|
593
|
+
/** The values a variant prop declares, in order — empty for one that delegates its domain. */
|
|
594
|
+
function ownValues(declaration) {
|
|
595
|
+
if (!ownsItsValues(declaration)) return [];
|
|
596
|
+
return listedIn(declaration.value) ?? [];
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* What a rule may condition one prop on: any of that prop's values, written bare and stored as a
|
|
600
|
+
* {@link valueRef}.
|
|
601
|
+
*
|
|
602
|
+
* The coercion is the whole point of putting it in the schema. `Config.apply` stores the parsed
|
|
603
|
+
* output, so authors, AI tools and patches keep writing `{ variant: 'solid' }` and the ref form
|
|
604
|
+
* never surfaces above this line — while the ref graph, the rename cascade and `dependentsOf` all
|
|
605
|
+
* see a real edge. Accepting a ref as well is what lets stored data re-validate on update.
|
|
606
|
+
*/
|
|
607
|
+
function conditionValue(prop, declaration, config) {
|
|
608
|
+
const domain = valueSchemaOf(declaration, config);
|
|
609
|
+
if (!ownsItsValues(declaration)) return domain;
|
|
610
|
+
return z.union([domain, refSchema]).transform((value) => isRef(value) ? value : valueRef(prop, String(value)));
|
|
611
|
+
}
|
|
612
|
+
/** `ExampleComponent`, or the first numbered name no component holds. Models copy examples
|
|
613
|
+
* literally, so the path has to be one a create would take. */
|
|
614
|
+
function freeExamplePath(config) {
|
|
615
|
+
let path = "ExampleComponent";
|
|
616
|
+
for (let n = 2; config.resolve("component", path); n += 1) path = `ExampleComponent${n}`;
|
|
617
|
+
return path;
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* What a `forwards` entry is, for the ops that write one. Shared by add, set and update, since the
|
|
621
|
+
* member is the same value under each verb and a model reads whichever it reaches first.
|
|
622
|
+
*/
|
|
623
|
+
const FORWARDS_DESCRIPTION = "Expose a layer's props at this component's top level. `key` is one of the component's own layer names; `data` is `\"*\"` for every prop that layer's element takes, or a list of component prop member refs (`[{ \"__ref\": \"component:Box#props/width\" }]`) for a selection, each naming the component that declares the prop — `Button` forwarding `width` from a `Pressable` root names `Box`, where `Pressable` takes it from. This is how `<Button width=\"full\">` reaches the root's `width` with no `width` entry in `props`; a prop the layer already exposes is forwarded, never redeclared. Two entries may not offer the same prop, and removing one forwarded prop is a `null` tombstone under `props`.";
|
|
624
|
+
/**
|
|
625
|
+
* A runnable `sub-create/forwards` example: the first local component that forwards nothing and has
|
|
626
|
+
* a layer rendering a component, forwarding all of that layer. A component that already forwards is
|
|
627
|
+
* skipped rather than reasoned about, since a second `"*"` entry collides with the first. `undefined`
|
|
628
|
+
* when no component qualifies, since an example the op would refuse teaches a refusal.
|
|
629
|
+
*/
|
|
630
|
+
function forwardsCreateExample(config) {
|
|
631
|
+
for (const component of config.list("component")) {
|
|
632
|
+
if (component.borrowedFrom !== void 0) continue;
|
|
633
|
+
const body = component.toJSON();
|
|
634
|
+
if (Object.keys(body.forwards ?? {}).length) continue;
|
|
635
|
+
for (const [key, element] of Object.entries(body.layers ?? {})) {
|
|
636
|
+
if (!isRef(element) || kindOf(element.__ref) !== "component") continue;
|
|
637
|
+
return {
|
|
638
|
+
path: component.path,
|
|
639
|
+
key,
|
|
640
|
+
data: "*"
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* A small, valid style bag for a layer that renders `primitivePath`: up to three of the style props
|
|
647
|
+
* that primitive exposes, each set to the first value it accepts. Drawn from the config so the bag is
|
|
648
|
+
* copy-paste-valid, and only there to show that a composed component sets style-prop values in a rule.
|
|
649
|
+
*/
|
|
650
|
+
function exampleStyleBag(config, primitivePath) {
|
|
651
|
+
const props = (config.resolve("component", primitivePath)?.toJSON())?.props ?? {};
|
|
652
|
+
const inert = new Set([
|
|
653
|
+
"inherit",
|
|
654
|
+
"initial",
|
|
655
|
+
"unset",
|
|
656
|
+
"none",
|
|
657
|
+
"auto",
|
|
658
|
+
"normal",
|
|
659
|
+
"transparent",
|
|
660
|
+
"current",
|
|
661
|
+
"currentColor",
|
|
662
|
+
"0"
|
|
663
|
+
]);
|
|
664
|
+
const bag = {};
|
|
665
|
+
for (const [name, prop] of Object.entries(props)) {
|
|
666
|
+
if (prop?.type !== "styleProperty" || !isRef(prop.value)) continue;
|
|
667
|
+
const leaves = stylePropValueLeaves(config, pathOf(prop.value.__ref));
|
|
668
|
+
const value = leaves.find((leaf) => !inert.has(leaf)) ?? leaves[0];
|
|
669
|
+
if (value !== void 0) bag[name] = value;
|
|
670
|
+
if (Object.keys(bag).length >= 3) break;
|
|
671
|
+
}
|
|
672
|
+
return bag;
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* The create example on a system that has primitives, built on them: a root and an icon layer on the
|
|
676
|
+
* first local component that takes nodes, a label and a body on the first that takes text, a glyph from
|
|
677
|
+
* the first icon library when there is one. It is the grammar the build records off a render, in the
|
|
678
|
+
* forms a real component in this system would use — a route for text (`forward`), a node gated on a
|
|
679
|
+
* prop (`visible`), a glyph as a node of its own, the edge list, a style rule that sets the primitives'
|
|
680
|
+
* own style props, and preview content. `undefined` when the system has no primitive of each kind,
|
|
681
|
+
* which the intrinsic-tag example then covers.
|
|
682
|
+
*/
|
|
683
|
+
function composedCreateExample(config, path) {
|
|
684
|
+
const locals = config.list("component").filter((component) => component.borrowedFrom === void 0);
|
|
685
|
+
const nodesHost = locals.find((component) => childrenPolicy(config, component.path) === "nodes");
|
|
686
|
+
const textHost = locals.find((component) => childrenPolicy(config, component.path) === "text");
|
|
687
|
+
if (!nodesHost || !textHost) return void 0;
|
|
688
|
+
const glyph = config.list("icon")[0];
|
|
689
|
+
const container = { __ref: `component:${nodesHost.path}` };
|
|
690
|
+
const text = { __ref: `component:${textHost.path}` };
|
|
691
|
+
const styleLayers = {};
|
|
692
|
+
const rootStyles = exampleStyleBag(config, nodesHost.path);
|
|
693
|
+
const labelStyles = exampleStyleBag(config, textHost.path);
|
|
694
|
+
if (Object.keys(rootStyles).length) styleLayers.root = rootStyles;
|
|
695
|
+
if (Object.keys(labelStyles).length) styleLayers.label = labelStyles;
|
|
696
|
+
return {
|
|
697
|
+
path,
|
|
698
|
+
data: {
|
|
699
|
+
layers: {
|
|
700
|
+
root: container,
|
|
701
|
+
icon: container,
|
|
702
|
+
label: text,
|
|
703
|
+
body: container
|
|
704
|
+
},
|
|
705
|
+
props: {
|
|
706
|
+
icon: { type: "boolean" },
|
|
707
|
+
label: {
|
|
708
|
+
type: "forward",
|
|
709
|
+
target: "label/children"
|
|
710
|
+
},
|
|
711
|
+
children: { type: "slot" }
|
|
712
|
+
},
|
|
713
|
+
anatomy: {
|
|
714
|
+
root: {
|
|
715
|
+
element: { __ref: "layer:root" },
|
|
716
|
+
children: [
|
|
717
|
+
"icon",
|
|
718
|
+
"label",
|
|
719
|
+
"body"
|
|
720
|
+
]
|
|
721
|
+
},
|
|
722
|
+
icon: {
|
|
723
|
+
element: { __ref: "layer:icon" },
|
|
724
|
+
visible: { $state: "icon" },
|
|
725
|
+
...glyph ? { children: ["glyph"] } : {}
|
|
726
|
+
},
|
|
727
|
+
...glyph ? { glyph: { element: { __ref: `icon:${glyph.path}` } } } : {},
|
|
728
|
+
label: { element: { __ref: "layer:label" } },
|
|
729
|
+
body: {
|
|
730
|
+
element: { __ref: "layer:body" },
|
|
731
|
+
children: ["content"]
|
|
732
|
+
},
|
|
733
|
+
content: {
|
|
734
|
+
element: "Slot",
|
|
735
|
+
props: { name: "children" }
|
|
736
|
+
}
|
|
737
|
+
},
|
|
738
|
+
...Object.keys(styleLayers).length ? { styles: { "*": {
|
|
739
|
+
when: null,
|
|
740
|
+
layers: styleLayers
|
|
741
|
+
} } } : {},
|
|
742
|
+
previewProps: {
|
|
743
|
+
icon: true,
|
|
744
|
+
label: "Example",
|
|
745
|
+
children: "Body text"
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* The shape of an anatomy node, for one written in the canvas spec's spelling. A spec element is
|
|
752
|
+
* `{ type, props, children }` with `$prop` bindings; an anatomy node is `{ element, props, children,
|
|
753
|
+
* visible, text, slots }` with keys naming sibling nodes, and content routed by the prop.
|
|
754
|
+
*/
|
|
755
|
+
const nodeShapeError = (issue) => {
|
|
756
|
+
if (issue.code !== "unrecognized_keys") return void 0;
|
|
757
|
+
const keys = issue.keys ?? [];
|
|
758
|
+
if (keys.filter((key) => [
|
|
759
|
+
"type",
|
|
760
|
+
"tag",
|
|
761
|
+
"name",
|
|
762
|
+
"$prop"
|
|
763
|
+
].includes(key)).length === 0) return void 0;
|
|
764
|
+
return `an anatomy node is \`{ "element", "children"?, "visible"?, "props"?, "text"?, "slots"? }\`, not a canvas spec element: \`element\` is what the node renders (\`{ "__ref": "layer:<name>" }\`, \`{ "__ref": "icon:<library>/<Name>" }\`, an intrinsic tag, or \`Slot\`), \`children\` lists sibling node KEYS, and a prop's content is routed by declaring the prop \`{ "type": "forward", "target": "<layer>/children" }\` rather than bound with \`$prop\`. Unknown here: ${keys.map((key) => `\`${key}\``).join(", ")}.`;
|
|
765
|
+
};
|
|
766
|
+
/** Whether two layer elements spell the same thing — a tag, or a ref to the same target. */
|
|
767
|
+
function sameElement(left, right) {
|
|
768
|
+
if (isRef(left) && isRef(right)) return left.__ref === right.__ref;
|
|
769
|
+
return typeof left === "string" && left === right;
|
|
770
|
+
}
|
|
771
|
+
/** Whether a node keyed like a layer spells that layer — as the layer's own element, or as the bare
|
|
772
|
+
* layer name where an intrinsic tag would go. Either is read as the layer. */
|
|
773
|
+
function spellsLayer(element, key, layer) {
|
|
774
|
+
return element === key || sameElement(element, layer);
|
|
775
|
+
}
|
|
776
|
+
/** The shape of a rule, for a bag written where the rule belongs or a rule with no `layers`. */
|
|
777
|
+
const ruleShapeError = (issue) => issue.code === "invalid_type" || issue.code === "unrecognized_keys" ? "a style rule is `{ \"when\": null, \"layers\": { \"<layer>\": { \"<styleProp>\": \"<value>\" } } }` under a key of your own (`*` for the unconditional rule), or with `\"when\": { \"<prop>\": \"<value>\" }` for a variant; the layer bags sit under `layers`, never directly under the rule" : void 0;
|
|
778
|
+
const StyleRule = defineSubEntity({
|
|
779
|
+
name: "style",
|
|
780
|
+
label: "Style",
|
|
781
|
+
labelPlural: "Styles",
|
|
782
|
+
key: (rule) => canonicalWhen(rule?.when),
|
|
783
|
+
vacant: (rule) => {
|
|
784
|
+
const layers = rule?.layers;
|
|
785
|
+
if (!layers) return true;
|
|
786
|
+
return Object.values(layers).every((bag) => !bag || Object.keys(bag).length === 0);
|
|
787
|
+
},
|
|
788
|
+
namesMembersOf: { when: "props" },
|
|
789
|
+
fields: ({ config, parent }) => {
|
|
790
|
+
const body = parent;
|
|
791
|
+
if (!config || !body) return z.object({
|
|
792
|
+
when: z.record(z.string(), z.unknown()).nullable().default(null),
|
|
793
|
+
layers: z.record(z.string(), z.record(z.string(), z.union([styleValue, z.never()], { error: "a layer bag value is a style-prop value — a token leaf such as `surface`, a keyword, a bracketed literal such as `[13px]` — or a modifier block holding more of them. Content is not set in a style rule: a glyph or a nested element is an anatomy node under the layer's node, `{ \"element\": { \"__ref\": \"icon:<library>/<Name>\" }, \"visible\": { \"$state\": \"<prop>\", \"eq\": \"<value>\" } }` to show one per variant value." })), { error: ruleShapeError })
|
|
794
|
+
}, { error: ruleShapeError });
|
|
795
|
+
return perOwner({
|
|
796
|
+
cache: ruleSchemas,
|
|
797
|
+
config,
|
|
798
|
+
owner: body,
|
|
799
|
+
build: () => {
|
|
800
|
+
const when = {};
|
|
801
|
+
for (const [name, decl] of Object.entries(body.props ?? {})) when[name] = conditionValue(name, decl, config).optional();
|
|
802
|
+
const layers = {};
|
|
803
|
+
for (const [name, el] of Object.entries(body.layers ?? {})) layers[name] = bagFor(config, el).optional();
|
|
804
|
+
return z.strictObject({
|
|
805
|
+
when: z.strictObject(when).nullable().default(null),
|
|
806
|
+
layers: z.strictObject(layers, { error: ruleShapeError })
|
|
807
|
+
}, { error: ruleShapeError });
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
/**
|
|
813
|
+
* A per-owner member schema, built once per owner rather than once per member.
|
|
814
|
+
*
|
|
815
|
+
* A factory is called for every member it validates, and Zod does its first-parse setup on each fresh
|
|
816
|
+
* schema, so a component with thirty rules would build its `when` shape and its layer bags thirty times
|
|
817
|
+
* over. The schema reads only the owner's `props` and `layers`, so it is keyed on those two objects: the
|
|
818
|
+
* owner is a body under construction whose collections are swapped as each one is coerced, and a key on
|
|
819
|
+
* the body alone would answer for `props` it has since replaced. A `Config` is immutable, so under one
|
|
820
|
+
* config the same two objects always build the same schema.
|
|
821
|
+
*/
|
|
822
|
+
function perOwner({ cache, config, owner, build }) {
|
|
823
|
+
let perConfig = cache.get(config);
|
|
824
|
+
if (!perConfig) {
|
|
825
|
+
perConfig = /* @__PURE__ */ new WeakMap();
|
|
826
|
+
cache.set(config, perConfig);
|
|
827
|
+
}
|
|
828
|
+
const hit = perConfig.get(owner);
|
|
829
|
+
if (hit && hit.props === owner.props && hit.layers === owner.layers) return hit.schema;
|
|
830
|
+
const schema = build();
|
|
831
|
+
perConfig.set(owner, {
|
|
832
|
+
props: owner.props,
|
|
833
|
+
layers: owner.layers,
|
|
834
|
+
schema
|
|
835
|
+
});
|
|
836
|
+
return schema;
|
|
837
|
+
}
|
|
838
|
+
const ruleSchemas = /* @__PURE__ */ new WeakMap();
|
|
839
|
+
/** What one node renders. The order is the resolution order the build reads a tag by: one of this
|
|
840
|
+
* Component's layers, then something the config names, then a package export — plus `$state` for an
|
|
841
|
+
* element the instance chooses (`GLYPHS[name]`), whose legal values are the prop's own domain
|
|
842
|
+
* rather than a list copied in here. A content-position node is the string `'Slot'` — the registry
|
|
843
|
+
* primitive — with the prop it stands for in `props.name`; the place an untargeted slot prop's
|
|
844
|
+
* instance content lands, whose position among its siblings only the edge list can state. */
|
|
845
|
+
const nodeElement = z.union([
|
|
846
|
+
refSchema.describe("A ref. `layer:<name>` renders through one of this component's layers, which is the only way a node can be styled; `component:<path>`, `icon:<path>` or `package:<export>` renders that directly, unstyled."),
|
|
847
|
+
z.string().describe("An intrinsic tag such as `div`, rendered unstyled, or `Slot`: the position where an untargeted slot prop's content lands, with the prop named in `props.name`, listed in the `children` of a layer that takes nodes. A text layer shows a prop through a `forward` declared on the prop instead."),
|
|
848
|
+
z.strictObject({ $state: z.string() }).describe("An element the instance chooses: the value of the prop `$state` names, from that prop's own domain (an icon prop whose value is a glyph). It carries no per-value table; to show a different fixed glyph per variant value, give each glyph its own node with `visible: { \"$state\": \"<prop>\", \"eq\": \"<value>\" }`.")
|
|
849
|
+
], { error: "an element is `{ \"__ref\": \"layer:<name>\" }` for one of this component's layers, `{ \"__ref\": \"icon:<library>/<Name>\" }` for a glyph, `{ \"__ref\": \"component:<path>\" }` for a component rendered unstyled, an intrinsic tag such as `div`, `Slot` for where a slot prop's content lands, or `{ \"$state\": \"<prop>\" }` for an element the prop chooses" }).describe("What this node renders. Name a layer (`{ \"__ref\": \"layer:<name>\" }`) for anything a style rule should reach.");
|
|
850
|
+
/** The operand each comparison accepts. Checked against the framework's operator set both ways, so
|
|
851
|
+
* the schema can neither drop an operator the readers handle nor accept one they don't. */
|
|
852
|
+
const comparisonOperands = {
|
|
853
|
+
eq: z.unknown().optional(),
|
|
854
|
+
neq: z.unknown().optional(),
|
|
855
|
+
gt: z.number().optional(),
|
|
856
|
+
gte: z.number().optional(),
|
|
857
|
+
lt: z.number().optional(),
|
|
858
|
+
lte: z.number().optional()
|
|
859
|
+
};
|
|
860
|
+
/** json-render's own visibility grammar, which is why nothing here is invented: a bare condition is
|
|
861
|
+
* a truthiness test, an array is the implicit and, `$or` is the explicit or.
|
|
862
|
+
*
|
|
863
|
+
* `$state` is stored as a JSON pointer, so a missing leading slash is added. Both spellings name the
|
|
864
|
+
* same prop, and storing one of them is what lets a reader compare conditions as values. */
|
|
865
|
+
const stateCondition = z.strictObject({
|
|
866
|
+
$state: z.string().refine((s) => !s.replace(/^\//, "").includes("/"), { message: "`$state` names one prop of this component; a nested JSON pointer has nothing to descend into." }),
|
|
867
|
+
...comparisonOperands,
|
|
868
|
+
not: z.literal(true).optional()
|
|
869
|
+
}).transform((test) => ({
|
|
870
|
+
...test,
|
|
871
|
+
$state: test.$state.startsWith("/") ? test.$state : `/${test.$state}`
|
|
872
|
+
}));
|
|
873
|
+
const orCondition = z.strictObject({ $or: z.array(stateCondition) });
|
|
874
|
+
/**
|
|
875
|
+
* The flat condition an editor collects — `{ variant: 'solid' }` — compiled to the grammar above.
|
|
876
|
+
*
|
|
877
|
+
* The same shape a style rule's `when` takes, and for the same reason: a surface scoping something to
|
|
878
|
+
* a variant has a prop and a value, not a query language. Compiled here rather than at the caller so
|
|
879
|
+
* both spellings mean the same thing wherever a condition is written — and so the value goes through
|
|
880
|
+
* `refLeaf`, which is what lets a condition hold the ref form it is stored as.
|
|
881
|
+
*
|
|
882
|
+
* A `true` operand compiles to the bare truthiness test rather than to `eq: true`, so the condition a
|
|
883
|
+
* surface writes for "this prop is set" is the one spelling of it. `false` keeps its `eq`, since
|
|
884
|
+
* unset and explicitly false are different states.
|
|
885
|
+
*/
|
|
886
|
+
const flatCondition = z.record(z.string(), z.union([
|
|
887
|
+
z.string(),
|
|
888
|
+
z.number(),
|
|
889
|
+
z.boolean(),
|
|
890
|
+
refSchema
|
|
891
|
+
])).check((ctx) => {
|
|
892
|
+
for (const key of Object.keys(ctx.value)) {
|
|
893
|
+
if (!key.startsWith("$")) continue;
|
|
894
|
+
ctx.issues.push({
|
|
895
|
+
code: "custom",
|
|
896
|
+
input: ctx.value,
|
|
897
|
+
message: `\`${key}\` is json-render's grammar, so this condition is read as one — and it has an operator that isn't part of it. Use \`$state\` with one of eq/neq/gt/gte/lt/lte/not, or drop the \`$\` keys for a plain { prop: value } condition.`
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
}).transform((flat) => {
|
|
901
|
+
const conditions = Object.entries(flat).sort(([a], [b]) => a.localeCompare(b)).map(([state, value]) => {
|
|
902
|
+
const operand = refLeaf(value);
|
|
903
|
+
if (operand === true) return { $state: `/${state}` };
|
|
904
|
+
return {
|
|
905
|
+
$state: `/${state}`,
|
|
906
|
+
eq: operand
|
|
907
|
+
};
|
|
908
|
+
});
|
|
909
|
+
return conditions.length === 1 ? conditions[0] : conditions;
|
|
910
|
+
});
|
|
911
|
+
const visibility = z.union([
|
|
912
|
+
stateCondition,
|
|
913
|
+
orCondition,
|
|
914
|
+
z.array(z.union([stateCondition, orCondition])),
|
|
915
|
+
flatCondition
|
|
916
|
+
], { error: "a condition is `{ \"$state\": \"<prop>\" }` for a prop that is set, `{ \"$state\": \"<prop>\", \"eq\": \"<value>\" }` for one value of it (`neq`, `gt`, `gte`, `lt`, `lte` and `not: true` also work), `{ \"$or\": [ … ] }` for any of several, or a list of conditions that must all hold" });
|
|
917
|
+
/** One inserted node's accepted shape — {@link InsertedNode}, as the op validates it. Declared after
|
|
918
|
+
* {@link visibility} because a descendant may carry its own condition. */
|
|
919
|
+
const insertedNode = z.strictObject({
|
|
920
|
+
element: z.union([z.string(), refSchema]).default("div"),
|
|
921
|
+
props: z.record(z.string(), z.unknown()).optional(),
|
|
922
|
+
children: z.array(z.string()).optional(),
|
|
923
|
+
/** Slot fills among the inserted nodes — the same native grammar the stored node keeps. */
|
|
924
|
+
slots: z.record(z.string(), z.array(z.string())).optional(),
|
|
925
|
+
visible: visibility.optional(),
|
|
926
|
+
/** A declared field of the stored node that the op had no input for, so an inserted element could
|
|
927
|
+
* never arrive with its text and had to be given it by a second write. */
|
|
928
|
+
text: z.string().optional()
|
|
929
|
+
});
|
|
930
|
+
/** The component a node renders, for asking what it may be given as children. `undefined` when the
|
|
931
|
+
* node isn't a component (an intrinsic tag, a glyph, a bound element) — none of which this config
|
|
932
|
+
* can answer for, so nothing is claimed about them. */
|
|
933
|
+
function nodeComponent(body, element) {
|
|
934
|
+
let target = element;
|
|
935
|
+
if (isRef(target) && kindOf(target.__ref) === "layer") target = body?.layers?.[pathOf(target.__ref)];
|
|
936
|
+
return isRef(target) && kindOf(target.__ref) === "component" ? pathOf(target.__ref) : void 0;
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* The write a slot fill makes on the host node, shared by `element-insert` and `element-move`:
|
|
940
|
+
* the host's `slots` entry pointing at the filled node, in json-render's own native grammar — the
|
|
941
|
+
* renderer resolves the element's `slots` field itself — so this writes a mechanism the renderer
|
|
942
|
+
* has, rather than inventing one.
|
|
943
|
+
*
|
|
944
|
+
* Whose prop `slotProp` is varies with how the fill was addressed, which makes it the field most
|
|
945
|
+
* likely to be wrong — and it was the one never checked, so a typo stored a fill under a name no
|
|
946
|
+
* prop routes and it simply never appeared. Checked only when the parent resolves to a component
|
|
947
|
+
* this config describes: an unknown target is not "a component with no props", so claiming
|
|
948
|
+
* otherwise would refuse a legitimate write.
|
|
949
|
+
*
|
|
950
|
+
* `visible`: filling the slot is the statement that the host now holds content, so the same write
|
|
951
|
+
* spends the host's collapse-while-empty gate on the prop the config routes into this fill — the
|
|
952
|
+
* bare truthiness test of that prop, and nothing else. Button's `startIcon` Box carries
|
|
953
|
+
* `visible: { $state: '/startIcon' }`, and the fill writes the slot ref rather than the prop, so
|
|
954
|
+
* without this the accepted fill paints nowhere. In an and-list the terms beside it survive; an `$or`
|
|
955
|
+
* group holding it goes whole, since the fill makes the group true. A test that compares the prop to
|
|
956
|
+
* anything but `true`, or negates it, is untouched. `undefined` leaves
|
|
957
|
+
* the field alone and `null` clears it, so a caller writes the field only when there is a change.
|
|
958
|
+
* Owned by the op rather than by each fill surface, so one gesture stays one patch per node and
|
|
959
|
+
* every caller agrees the fill will actually paint.
|
|
960
|
+
*/
|
|
961
|
+
function slotFillHostWrites({ config, path, parent, key, slotProp, node, layers }) {
|
|
962
|
+
const host = nodeComponent({ layers }, node?.element ?? layers?.[parent]);
|
|
963
|
+
if (host && config.resolve("component", host)) {
|
|
964
|
+
const declared = resolveComponentProps(config, host);
|
|
965
|
+
if (!declared[slotProp]) reject("NOT_FOUND", `"${host}" has no \`${slotProp}\` prop to fill — its slots are ${Object.keys(declared).join(", ") || "(none)"}.`);
|
|
966
|
+
}
|
|
967
|
+
const current = node?.slots?.[slotProp] ?? [];
|
|
968
|
+
const slots = {
|
|
969
|
+
...node?.slots,
|
|
970
|
+
[slotProp]: current.includes(key) ? current : [...current, key]
|
|
971
|
+
};
|
|
972
|
+
const parentElement = node?.element;
|
|
973
|
+
const parentLayer = isRef(parentElement) && kindOf(parentElement.__ref) === "layer" ? pathOf(parentElement.__ref) : parent;
|
|
974
|
+
const route = slotTargetsOf(config, path).find((t) => t.layer === parentLayer && t.into === slotProp);
|
|
975
|
+
if (!route) return { slots };
|
|
976
|
+
return {
|
|
977
|
+
slots,
|
|
978
|
+
visible: withoutTruthyTerm({
|
|
979
|
+
condition: node?.visible,
|
|
980
|
+
prop: route.from
|
|
981
|
+
})
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* The `slots` entries of `node` naming any of `doomed` — each answered with what survives, so a
|
|
986
|
+
* caller writes the filtered list back at the entry's own address (or `null` when nothing does)
|
|
987
|
+
* and the node's other slots are never restated.
|
|
988
|
+
*/
|
|
989
|
+
function staleSlotEntries(slots, doomed) {
|
|
990
|
+
return Object.entries(slots ?? {}).map(([name, fills]) => [name, fills.filter((fill) => !doomed.has(fill))]).filter(([name, kept]) => kept.length !== (slots?.[name]?.length ?? 0));
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Whether `key` sits at or inside `ancestor`'s subtree in the stored anatomy — following both the
|
|
994
|
+
* `children` edges and the `slots` entries, since a slotted node nests in render without a child
|
|
995
|
+
* edge. What it guards: a move whose target parent lives inside the moved node
|
|
996
|
+
* would store a cycle no parent names, and everything in it silently vanishes from every surface
|
|
997
|
+
* that walks from the root.
|
|
998
|
+
*/
|
|
999
|
+
function anatomySubtreeContains(anatomy, ancestor, key) {
|
|
1000
|
+
if (ancestor === key) return true;
|
|
1001
|
+
const queue = [ancestor];
|
|
1002
|
+
const seen = new Set(queue);
|
|
1003
|
+
while (queue.length > 0) {
|
|
1004
|
+
const current = queue.shift();
|
|
1005
|
+
if (current === void 0) continue;
|
|
1006
|
+
const node = anatomy[current];
|
|
1007
|
+
const reached = [...node?.children ?? []];
|
|
1008
|
+
for (const fills of Object.values(node?.slots ?? {})) reached.push(...fills);
|
|
1009
|
+
for (const child of reached) {
|
|
1010
|
+
if (child === key) return true;
|
|
1011
|
+
if (seen.has(child)) continue;
|
|
1012
|
+
seen.add(child);
|
|
1013
|
+
queue.push(child);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return false;
|
|
1017
|
+
}
|
|
1018
|
+
/** The component body as `element-move` reads it — re-resolved between writes so each step builds
|
|
1019
|
+
* on the previous one's result rather than the pre-move snapshot. */
|
|
1020
|
+
function resolveMoveBody(config, path) {
|
|
1021
|
+
return config.resolve("component", path)?.toJSON();
|
|
1022
|
+
}
|
|
1023
|
+
const AnatomyNode = defineSubEntity({
|
|
1024
|
+
name: "element",
|
|
1025
|
+
label: "Element",
|
|
1026
|
+
labelPlural: "Anatomy",
|
|
1027
|
+
fields: ({ config, parent, key }) => {
|
|
1028
|
+
const body = parent;
|
|
1029
|
+
const shape = {
|
|
1030
|
+
element: nodeElement,
|
|
1031
|
+
visible: visibility.optional().describe("When the node renders: a condition on a prop value (`{ \"$state\": \"<prop>\", \"eq\": <value> }`), an `$or` of them, or a list that must all hold. Omit for a node that always renders."),
|
|
1032
|
+
props: (config ? openBag(config, { cssBehind: false }) : z.record(z.string(), styleValue)).optional().describe("Props passed to what the node renders, keyed by prop or style-property name, as literal values. Content is not set here: a `slot` prop's content lands where a `{ \"element\": \"Slot\", \"props\": { \"name\": \"<prop>\" } }` node sits among a parent's `children` (on the root when there is none), and a `forward` prop (`{ \"type\": \"forward\", \"target\": \"label/children\" }`) routes it into that layer with no node entry. `$state` belongs to `visible` and to an `element` a prop chooses."),
|
|
1033
|
+
text: z.string().optional().describe("Literal text content, for a node that renders a fixed string.")
|
|
1034
|
+
};
|
|
1035
|
+
if (!config || !body) return z.object({
|
|
1036
|
+
...shape,
|
|
1037
|
+
children: z.array(z.string({ error: childKeyError })).optional().describe("The keys of sibling nodes nested here, in render order — never a prop name. A slot prop's content is not listed here: once the slot prop is declared in `props`, its content fills `root` by default, so it needs no anatomy node — a `{ \"element\": \"Slot\", \"props\": { \"name\": \"<prop>\" } }` node listed here only moves that content into a nested layer. The tree is this edge list: the node keyed `root` is the top, and a node no other node names does not render."),
|
|
1038
|
+
slots: z.record(z.string(), z.array(z.string())).optional().describe("Default fills for this node's slot props: the slot prop's name to the keys of the sibling nodes that fill it — node keys, never the prop's own name, and never where the instance's content lands (that fills `root` or a `Slot` node). A node placed by a slot is not also listed in `children`.")
|
|
1039
|
+
});
|
|
1040
|
+
const siblings = Object.keys(body.anatomy ?? {});
|
|
1041
|
+
const siblingKey = siblings.length === 0 ? z.string() : z.string({ error: childKeyError }).check((ctx) => {
|
|
1042
|
+
if (siblings.includes(ctx.value)) return;
|
|
1043
|
+
if (body.props?.[ctx.value]) {
|
|
1044
|
+
ctx.issues.push({
|
|
1045
|
+
code: "custom",
|
|
1046
|
+
input: ctx.value,
|
|
1047
|
+
message: `"${ctx.value}" is a prop, not an anatomy node. Where a slot prop's content lands is a node of its own, \`"${ctx.value}Slot": { "element": "Slot", "props": { "name": "${ctx.value}" } }\`, and its key is what goes here; text a prop supplies to a text layer is routed by declaring the prop \`{ "type": "forward", "target": "<layer>/children" }\`, with nothing listed for it.`
|
|
1048
|
+
});
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
ctx.issues.push({
|
|
1052
|
+
code: "custom",
|
|
1053
|
+
input: ctx.value,
|
|
1054
|
+
message: `anatomy node "${ctx.value}" does not exist yet — create it earlier in the batch. \`children\` and \`slots\` list the keys of sibling nodes in this component's anatomy.`
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
1057
|
+
return z.strictObject({
|
|
1058
|
+
...shape,
|
|
1059
|
+
children: z.array(siblingKey).optional().describe("The keys of sibling nodes nested here, in render order — never a prop name. A slot prop's content is not listed here: once the slot prop is declared in `props`, its content fills `root` by default, so it needs no anatomy node — a `{ \"element\": \"Slot\", \"props\": { \"name\": \"<prop>\" } }` node listed here only moves that content into a nested layer. The tree is this edge list: the node keyed `root` is the top, and a node no other node names does not render."),
|
|
1060
|
+
slots: z.record(z.string(), z.array(siblingKey)).optional().describe("Default fills for this node's slot props: the slot prop's name to the keys of the sibling nodes that fill it — node keys, never the prop's own name, and never where the instance's content lands (that fills `root` or a `Slot` node). A node placed by a slot is not also listed in `children`.")
|
|
1061
|
+
}, { error: nodeShapeError }).check((ctx) => {
|
|
1062
|
+
const node = ctx.value;
|
|
1063
|
+
const current = key ? body.anatomy?.[key] : void 0;
|
|
1064
|
+
const element = node.element ?? current?.element;
|
|
1065
|
+
if (key !== void 0 && body.layers && key in body.layers) {
|
|
1066
|
+
const rendersLayer = isRef(element) && element.__ref === `layer:${key}`;
|
|
1067
|
+
if (element !== void 0 && !rendersLayer && !spellsLayer(element, key, body.layers[key])) {
|
|
1068
|
+
const spelled = isRef(element) ? `\`${element.__ref}\`` : JSON.stringify(element);
|
|
1069
|
+
ctx.issues.push({
|
|
1070
|
+
code: "custom",
|
|
1071
|
+
input: node,
|
|
1072
|
+
path: ["element"],
|
|
1073
|
+
message: `node "${key}" is keyed like the layer "${key}", so it renders that layer: \`{ "__ref": "layer:${key}" }\`. Written as ${spelled} it would paint the element directly, unstyled, outside every style rule that reaches the layer.`
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
if (typeof node.text === "string" && body.props !== void 0 && node.text in body.props) ctx.issues.push({
|
|
1078
|
+
code: "custom",
|
|
1079
|
+
input: node,
|
|
1080
|
+
path: ["text"],
|
|
1081
|
+
message: `\`text\` is a fixed string, and "${node.text}" is one of this component's props. To show the prop's value here, declare it \`{ "type": "forward", "target": "${isRef(element) && kindOf(element.__ref) === "layer" ? pathOf(element.__ref) : "<layer>"}/children" }\` and give this node no \`text\`.`
|
|
1082
|
+
});
|
|
1083
|
+
for (const [prop, value] of Object.entries(node.props ?? {})) {
|
|
1084
|
+
if (isRef(value) && [
|
|
1085
|
+
"icon",
|
|
1086
|
+
"component",
|
|
1087
|
+
"package",
|
|
1088
|
+
"layer"
|
|
1089
|
+
].includes(kindOf(value.__ref))) {
|
|
1090
|
+
ctx.issues.push({
|
|
1091
|
+
code: "custom",
|
|
1092
|
+
input: value,
|
|
1093
|
+
path: ["props", prop],
|
|
1094
|
+
message: `\`${prop}\` holds a ref to \`${value.__ref}\`, which would reach the element as an object and paint nothing. What a node contains is a node: add \`"${key ?? "<key>"}Glyph": { "element": { "__ref": "${value.__ref}" } }\` to \`anatomy\` and list that key in this node's \`children\`.`
|
|
1095
|
+
});
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
if (!isPlainObject(value) || !("$state" in value)) continue;
|
|
1099
|
+
ctx.issues.push({
|
|
1100
|
+
code: "custom",
|
|
1101
|
+
input: value,
|
|
1102
|
+
path: ["props", prop],
|
|
1103
|
+
message: `\`${prop}\` is bound with \`$state\`, which anatomy uses for \`visible\` and for an \`element\` a prop chooses, never for a prop value: the object would reach the element as-is and paint nothing. Content reaches a node through the prop's declaration — \`{ "type": "forward", "target": "<layer>/children" }\` lands it in that layer, and a \`{ "element": "Slot", "props": { "name": "<prop>" } }\` node among a parent's \`children\` marks where a slot's content goes. To vary content by a prop, give each state its own node and choose between them with \`visible\`.`
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
const slotAnatomyProp = slotAnatomyPropOf({
|
|
1107
|
+
element,
|
|
1108
|
+
props: {
|
|
1109
|
+
...current?.props,
|
|
1110
|
+
...node.props
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
if (slotAnatomyProp !== void 0) {
|
|
1114
|
+
if (node.children?.length || node.slots || node.text !== void 0 || node.visible !== void 0 || Object.keys(node.props ?? {}).some((prop) => prop !== "name")) ctx.issues.push({
|
|
1115
|
+
code: "custom",
|
|
1116
|
+
input: node,
|
|
1117
|
+
message: "a Slot node carries nothing of its own — its content arrives from the instance"
|
|
1118
|
+
});
|
|
1119
|
+
const decl = body.props?.[slotAnatomyProp];
|
|
1120
|
+
if (decl && decl.type !== "slot" && !(decl.type === "forward" && decl.target === void 0)) ctx.issues.push({
|
|
1121
|
+
code: "custom",
|
|
1122
|
+
input: node,
|
|
1123
|
+
message: `a Slot node must name a slot prop — "${slotAnatomyProp}" routes no content`
|
|
1124
|
+
});
|
|
1125
|
+
for (const [sibling, member] of Object.entries(body.anatomy ?? {})) {
|
|
1126
|
+
if (sibling === key) continue;
|
|
1127
|
+
if (slotAnatomyPropOf(member) === slotAnatomyProp) {
|
|
1128
|
+
ctx.issues.push({
|
|
1129
|
+
code: "custom",
|
|
1130
|
+
input: node,
|
|
1131
|
+
message: `"${sibling}" already places the content of "${slotAnatomyProp}"`
|
|
1132
|
+
});
|
|
1133
|
+
break;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
const target = nodeComponent(body, element);
|
|
1139
|
+
if (!target) return;
|
|
1140
|
+
const policy = childrenPolicy(config, target);
|
|
1141
|
+
if (policy === "none" && (node.children?.length || node.text)) ctx.issues.push({
|
|
1142
|
+
code: "custom",
|
|
1143
|
+
input: node,
|
|
1144
|
+
message: `"${target}" takes no children — it declares no \`children\` slot, so what this node nests inside it would never render. Declare \`children: { "type": "slot" }\` on "${target}" (with \`"text": true\` for text only), or nest these under a layer whose element takes content.`
|
|
1145
|
+
});
|
|
1146
|
+
if (policy === "text" && node.children?.length) {
|
|
1147
|
+
const layer = isRef(element) && kindOf(element.__ref) === "layer" ? pathOf(element.__ref) : "<layer>";
|
|
1148
|
+
ctx.issues.push({
|
|
1149
|
+
code: "custom",
|
|
1150
|
+
input: node,
|
|
1151
|
+
message: `"${target}" takes text, not nodes, so nothing nested under this node would render. Text a prop supplies is routed into the layer by declaring the prop \`{ "type": "forward", "target": "${layer}/children" }\`, with no node for it; a fixed string is this node's \`text\`.`
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
}).overwrite((node) => key !== void 0 && body.layers && key in body.layers && node.element !== void 0 && spellsLayer(node.element, key, body.layers[key]) ? {
|
|
1155
|
+
...node,
|
|
1156
|
+
element: { __ref: `layer:${key}` }
|
|
1157
|
+
} : node);
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
const componentFields = z.strictObject({
|
|
1161
|
+
/**
|
|
1162
|
+
* name → element. **One of them must be called `root`.**
|
|
1163
|
+
*
|
|
1164
|
+
* The root layer is where every prop the config does not model ends up — `id`, `onClick`, `aria-*`,
|
|
1165
|
+
* `data-*`, `ref`. No config can enumerate those, so their destination is the only part that can be
|
|
1166
|
+
* made explicit, and requiring the name is how. Without it the root was whichever layer happened to
|
|
1167
|
+
* be declared first, so reordering `layers` silently moved every un-modelled prop to a different
|
|
1168
|
+
* element — the one implicit thing left in a model whose point is that routing is declared.
|
|
1169
|
+
*
|
|
1170
|
+
* Required for every component, single-layer ones included. A single layer is unambiguous today,
|
|
1171
|
+
* but the day it grows a second layer the addition would first have to rename the existing one —
|
|
1172
|
+
* a restructuring event for what should be an additive change, and one every editing surface would
|
|
1173
|
+
* have to special-case. Naming the root up front makes layer structure uniform: growing a
|
|
1174
|
+
* Component never moves its root, and swapping the root element looks the same everywhere.
|
|
1175
|
+
*
|
|
1176
|
+
* All 100 components in a real system already name one `root`, so this refuses nothing anybody
|
|
1177
|
+
* wrote. It also removes the only gesture it would have blocked — moving a component's root —
|
|
1178
|
+
* which is not a thing we want to be possible.
|
|
1179
|
+
*
|
|
1180
|
+
* Enforced where components are defined (authoring, `create`, `update`, `hydrate`). A per-member
|
|
1181
|
+
* `sub-delete` does not re-validate its owner, so `rootLayerOf` still answers defensively rather
|
|
1182
|
+
* than assuming.
|
|
1183
|
+
*/
|
|
1184
|
+
layers: Layer.check((ctx) => {
|
|
1185
|
+
if (isRef(ctx.value)) return;
|
|
1186
|
+
const names = Object.keys(ctx.value);
|
|
1187
|
+
if (names.includes("root")) return;
|
|
1188
|
+
ctx.issues.push({
|
|
1189
|
+
code: "custom",
|
|
1190
|
+
input: ctx.value,
|
|
1191
|
+
message: `a component's layers must name one \`root\` — it is where every prop the config doesn't model (\`id\`, \`onClick\`, \`data-*\`) is sent, and otherwise that is decided by declaration ORDER. Declared: ${names.join(", ") || "(none)"}.`
|
|
1192
|
+
});
|
|
1193
|
+
}),
|
|
1194
|
+
props: Prop.optional(),
|
|
1195
|
+
/**
|
|
1196
|
+
* Which layers' element props this component also exposes — see {@link Forward}.
|
|
1197
|
+
*
|
|
1198
|
+
* Replaced `inherits`, which named one layer and meant `{ [layer]: '*' }`. That field and its
|
|
1199
|
+
* compatibility reader are gone: nothing mints it, and a stored config still carrying one now
|
|
1200
|
+
* forwards nothing rather than being quietly translated.
|
|
1201
|
+
*/
|
|
1202
|
+
forwards: Forward.optional(),
|
|
1203
|
+
styles: StyleRule.optional(),
|
|
1204
|
+
anatomy: AnatomyNode.optional().describe("The tree of what this component renders, keyed by node. Each node names what it renders (a layer, for anything a style rule reaches) and lists its `children`; the node keyed `root` is the top. A one-layer component may omit it."),
|
|
1205
|
+
defaultProps: Style.optional(),
|
|
1206
|
+
/** The state a person chose to show one catalogue tile in. Not anatomy: the capture bakes these
|
|
1207
|
+
* into the tree by invoking a render with them, so a tile's example icon ends up stored as
|
|
1208
|
+
* part of the component itself. */
|
|
1209
|
+
previewProps: z.record(z.string(), z.unknown()).optional().describe("The props the catalogue tile and the variant matrix render with. A component whose root takes children renders empty until this or `defaultProps` carries some."),
|
|
1210
|
+
/**
|
|
1211
|
+
* The source that implements this component — a ref, because the file is an entity the seal owns.
|
|
1212
|
+
*
|
|
1213
|
+
* Kind-typed rather than `orRef(...)`, the way a token override's `on: Modifier` is: only a file
|
|
1214
|
+
* can implement a component. Being a ref is also what makes a borrowed implementation ordinary —
|
|
1215
|
+
* `configFor(ref)` answers with the config that owns it, so its root comes from that config's own
|
|
1216
|
+
* pin, and a chain never has to resolve a bare relative path against the wrong tree.
|
|
1217
|
+
*
|
|
1218
|
+
* Absent for a data-authored component, which has a definition and no render of its own.
|
|
1219
|
+
*/
|
|
1220
|
+
file: File.optional(),
|
|
1221
|
+
/**
|
|
1222
|
+
* The binding inside that file — `export const Card = …`.
|
|
1223
|
+
*
|
|
1224
|
+
* On the component rather than on the file because that is where it belongs: one file may export
|
|
1225
|
+
* two components, and a borrower registers the publisher's `Card` under its own name. Nothing
|
|
1226
|
+
* recovers that binding from a path.
|
|
1227
|
+
*/
|
|
1228
|
+
export: z.string().optional()
|
|
1229
|
+
}).check((ctx) => {
|
|
1230
|
+
const body = ctx.value;
|
|
1231
|
+
if (body.forwards || body.file || isRef(body.props)) return;
|
|
1232
|
+
const declared = body.props ?? {};
|
|
1233
|
+
for (const field of ["previewProps", "defaultProps"]) {
|
|
1234
|
+
const values = body[field];
|
|
1235
|
+
if (!isPlainObject(values) || !("children" in values)) continue;
|
|
1236
|
+
if (Object.hasOwn(declared, "children") && declared.children !== null) continue;
|
|
1237
|
+
ctx.issues.push({
|
|
1238
|
+
code: "custom",
|
|
1239
|
+
input: values,
|
|
1240
|
+
path: [field, "children"],
|
|
1241
|
+
message: `\`${field}.children\` has nowhere to land — this component declares no \`children\` prop, so the content is dropped and the component renders empty. Declare \`"children": { "type": "slot", "text": true }\` in \`props\` — or, to land it in a nested layer, \`"children": { "type": "forward", "target": "<layer>/children" }\`.`
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
});
|
|
1245
|
+
/**
|
|
1246
|
+
* Where a component keeps its bags: each style rule's per-layer bag, each anatomy node's props, and
|
|
1247
|
+
* the component's own defaults.
|
|
1248
|
+
*
|
|
1249
|
+
* The three are the same kind of thing — a set of declarations keyed by the property being set — so
|
|
1250
|
+
* they share one walk, and the hooks built from it are the shared ones. Missing a position is not a
|
|
1251
|
+
* partial fix but a silent one: the graph reports the positions it knows and says nothing about the
|
|
1252
|
+
* rest, which reads as "nothing depends on this".
|
|
1253
|
+
*/
|
|
1254
|
+
const componentBags = (data) => {
|
|
1255
|
+
const body = data;
|
|
1256
|
+
const out = [];
|
|
1257
|
+
for (const rule of Object.values(body?.styles ?? {})) {
|
|
1258
|
+
const layers = rule?.layers;
|
|
1259
|
+
if (!isPlainRecord(layers)) continue;
|
|
1260
|
+
for (const [layer, bag] of Object.entries(layers)) collectBag(bag, (next) => {
|
|
1261
|
+
layers[layer] = next;
|
|
1262
|
+
}, out);
|
|
1263
|
+
}
|
|
1264
|
+
for (const node of Object.values(body?.anatomy ?? {})) {
|
|
1265
|
+
if (!isPlainRecord(node)) continue;
|
|
1266
|
+
collectBag(node.props, (next) => {
|
|
1267
|
+
node.props = next;
|
|
1268
|
+
}, out);
|
|
1269
|
+
}
|
|
1270
|
+
collectBag(body?.defaultProps, (next) => {
|
|
1271
|
+
if (body) body.defaultProps = next;
|
|
1272
|
+
}, out);
|
|
1273
|
+
return out;
|
|
1274
|
+
};
|
|
1275
|
+
const ComponentEntity = defineEntity({
|
|
1276
|
+
kind: "component",
|
|
1277
|
+
fields: componentFields,
|
|
1278
|
+
groupFields: z.object({ icon: z.string().optional() }),
|
|
1279
|
+
subEntities: {
|
|
1280
|
+
layers: Layer,
|
|
1281
|
+
props: Prop,
|
|
1282
|
+
forwards: Forward,
|
|
1283
|
+
styles: StyleRule,
|
|
1284
|
+
anatomy: AnatomyNode,
|
|
1285
|
+
defaultProps: Style
|
|
1286
|
+
},
|
|
1287
|
+
computed: {
|
|
1288
|
+
/**
|
|
1289
|
+
* The prop names an instance may fill through a spec element's `slots` field — every prop that
|
|
1290
|
+
* routes content: a `slot`, or a `forward` whose far end takes content. Resolved over the
|
|
1291
|
+
* inheritance chain, since an inherited slot is as fillable as a declared one. This is the
|
|
1292
|
+
* `slots: string[]` json-render's catalog metadata declares per component, computed here so the
|
|
1293
|
+
* emitted catalog, the validator, and every picker read one answer.
|
|
1294
|
+
*/
|
|
1295
|
+
slots: ({ config, path, member }) => {
|
|
1296
|
+
const layers = member.layers;
|
|
1297
|
+
const names = [];
|
|
1298
|
+
for (const [name, decl] of Object.entries(resolveComponentProps(config, path))) {
|
|
1299
|
+
const body = decl;
|
|
1300
|
+
if (!body) continue;
|
|
1301
|
+
if (body.type === "slot" || body.type === "forward" && routesContent(config, layers, body)) names.push(name);
|
|
1302
|
+
}
|
|
1303
|
+
return names;
|
|
1304
|
+
} },
|
|
1305
|
+
nameEdges: bagNameEdges(componentBags),
|
|
1306
|
+
styleBags: componentBags,
|
|
1307
|
+
rewriteName: bagRewriteName(componentBags),
|
|
1308
|
+
schemas: {
|
|
1309
|
+
create: {
|
|
1310
|
+
data: componentFields,
|
|
1311
|
+
description: "Create a component at a new qualified `path`. When its complete body is known, include all layers, anatomy, props, defaults, and styles in this one operation. `data.layers` maps each stable layer name to the element it renders and must include `root` — a tag for a primitive (`{ \"layers\": { \"root\": \"div\" } }`), and for anything built on top, a ref to a primitive (`{ \"root\": { \"__ref\": \"component:Box\" }, \"label\": { \"__ref\": \"component:Text\" } }`), which is what lets the layer take that primitive's style props. A primitive exposes style props as `{ \"type\": \"styleProperty\", \"value\": { \"__ref\": \"style-property:bg\" } }` and declares `children: { \"type\": \"slot\", \"text\": true }` when it holds content — without the slot, nothing nested inside it renders. A composed component exposes a layer's props at its own top level through `forwards`, keyed by layer: `{ \"forwards\": { \"root\": \"*\" } }` passes every prop the root's element takes (so `<Button width=\"full\">` reaches the root's `width`), and a list of member refs (`[{ \"__ref\": \"component:Box#props/width\" }]`, each naming the component that declares the prop) passes only those. A prop a layer already exposes is forwarded, never redeclared in `props`; a `props` entry of `{ \"type\": \"forward\" }` is the reverse route, one of this component's props into a layer's. `anatomy` is the tree the build would read off a render: each node renders a layer (`{ \"__ref\": \"layer:<name>\" }`), an intrinsic tag, or `Slot` (`{ \"element\": \"Slot\", \"props\": { \"name\": \"children\" } }`, where a slot prop's content lands — but a declared slot fills `root` by default, so a primitive still declares its `children` slot prop yet needs no `Slot` node or `children` entry for it, and a `Slot` node is only for landing that content in a nested layer); `children` lists sibling node keys from the `root` node, never a prop name; `text` is a fixed string; `visible` is a condition on a prop (`{ \"$state\": \"<prop>\" }`, or with `eq`); content shown in a nested layer is a route, `children: { \"type\": \"forward\", \"target\": \"label/children\" }`. `$state` appears only in `visible` and in an `element` a prop chooses, never as a prop value. A glyph is a node of its own, `{ \"element\": { \"__ref\": \"icon:<library>/<Name>\" } }`, listed in the `children` of the layer node that holds it, never a prop value. Text a prop supplies to a text layer is never a node either: declare the prop `{ \"type\": \"forward\", \"target\": \"<layer>/children\" }` and the layer shows it; a `Slot` node goes only under a layer that takes nodes. A style rule is `\"styles\": { \"*\": { \"when\": null, \"layers\": { \"root\": { \"bg\": \"surface\" } } } }`, keyed by a name, with each layer's bag under `layers`. A composed component carries its own styles: give it an unconditional (`\"*\"`) rule that lays out and surfaces it through the system's style properties — spacing, alignment and gap on the container, a background and color where it has one — rather than leaning on the bare primitives, which paint flat. Style a layer only with the style props the primitive it renders exposes, set to the values those props accept: `component/get` on the primitive lists both, so read it and choose from that rather than reaching for a CSS name or value the system has no style property for. Set `previewProps` (or `defaultProps`) so the component previews with content; a root that takes children renders empty otherwise. A style rule's layer bag is keyed by style-prop name (`fontWeight`, `color`), never by CSS property name, and each value is one of that prop's values.",
|
|
1312
|
+
example: ({ config }) => {
|
|
1313
|
+
const path = freeExamplePath(config);
|
|
1314
|
+
const composed = composedCreateExample(config, path);
|
|
1315
|
+
if (composed) return composed;
|
|
1316
|
+
return {
|
|
1317
|
+
path,
|
|
1318
|
+
data: {
|
|
1319
|
+
layers: {
|
|
1320
|
+
root: "button",
|
|
1321
|
+
icon: "span"
|
|
1322
|
+
},
|
|
1323
|
+
props: {
|
|
1324
|
+
icon: {
|
|
1325
|
+
type: "forward",
|
|
1326
|
+
target: "icon/children"
|
|
1327
|
+
},
|
|
1328
|
+
children: {
|
|
1329
|
+
type: "slot",
|
|
1330
|
+
text: true
|
|
1331
|
+
}
|
|
1332
|
+
},
|
|
1333
|
+
anatomy: {
|
|
1334
|
+
icon: {
|
|
1335
|
+
element: { __ref: "layer:icon" },
|
|
1336
|
+
visible: { $state: "icon" }
|
|
1337
|
+
},
|
|
1338
|
+
content: {
|
|
1339
|
+
element: "Slot",
|
|
1340
|
+
props: { name: "children" }
|
|
1341
|
+
},
|
|
1342
|
+
root: {
|
|
1343
|
+
element: { __ref: "layer:root" },
|
|
1344
|
+
children: ["icon", "content"]
|
|
1345
|
+
}
|
|
1346
|
+
},
|
|
1347
|
+
previewProps: { children: "Example" }
|
|
1348
|
+
}
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
},
|
|
1352
|
+
"sub-create/anatomy": { description: "Add one anatomy node (`key` + `data`) to a component. In a batch, create child nodes before a parent whose `children` or `slots` names them; each referenced node must already exist when this operation runs." },
|
|
1353
|
+
"sub-create/props": { description: "Declare a prop this component owns (`key` + `data`): a `variant`, a `slot`, a `styleProperty` or `composite` on a primitive, a `forward` routing this prop into a layer's (`{ \"type\": \"forward\", \"target\": \"<layer>/<prop>\" }`), or `null` to drop a forwarded prop from the surface. A prop one of its layers already exposes — `width` on a root that renders `Box` — is not declared here; `sub-create/forwards` exposes it at the top level." },
|
|
1354
|
+
"sub-create/forwards": {
|
|
1355
|
+
description: FORWARDS_DESCRIPTION,
|
|
1356
|
+
example: ({ config }) => forwardsCreateExample(config)
|
|
1357
|
+
},
|
|
1358
|
+
"sub-upsert/forwards": { description: `Set what one layer forwards, added when absent and replaced whole when present. ${FORWARDS_DESCRIPTION}` },
|
|
1359
|
+
"sub-update/forwards": { description: `Replace what one layer forwards: the member is \`'*'\` or a list, so \`data\` is the whole new value. ${FORWARDS_DESCRIPTION}` },
|
|
1360
|
+
/**
|
|
1361
|
+
* Add an element to a composition — a layer and the node that targets it, together.
|
|
1362
|
+
*
|
|
1363
|
+
* Two writes, one op, because either alone is broken: a node whose element is a component ref
|
|
1364
|
+
* directly can never be styled (`styles` keys on layer names, and it has none), and a layer no
|
|
1365
|
+
* node places renders nowhere. Making it one op is also what keeps the parent's `children` from
|
|
1366
|
+
* being left dangling between two patches.
|
|
1367
|
+
*
|
|
1368
|
+
* That the new element gets a layer is the whole difference between a read component and an
|
|
1369
|
+
* authored one. A render's inline glyph stays unnamed because the source never named it; a thing
|
|
1370
|
+
* a person adds is added in order to be styled.
|
|
1371
|
+
*
|
|
1372
|
+
* The layer name is what makes this survive a second platform: native replaces `layers` (a `div`
|
|
1373
|
+
* becomes a `View`) and inherits the styles and the structure, because both address the name
|
|
1374
|
+
* rather than the element.
|
|
1375
|
+
*/
|
|
1376
|
+
"element-insert": {
|
|
1377
|
+
input: z.object({
|
|
1378
|
+
path: pathInput("component"),
|
|
1379
|
+
/** The layer name to insert under. Optional: the entity owns the namespace, so it mints a
|
|
1380
|
+
* free one ({@link freeLayerKey}) when the caller doesn't care what it is called. */
|
|
1381
|
+
key: z.string().optional(),
|
|
1382
|
+
data: z.object({
|
|
1383
|
+
element: z.union([z.string(), refSchema]).default("div"),
|
|
1384
|
+
parent: z.string().optional(),
|
|
1385
|
+
index: z.number().optional(),
|
|
1386
|
+
props: z.record(z.string(), z.unknown()).optional(),
|
|
1387
|
+
/**
|
|
1388
|
+
* Fill the parent's slot with the new element, by name of the parent's prop.
|
|
1389
|
+
*
|
|
1390
|
+
* A slot fill is one user action — "put a Text in the label slot" — and it is two writes:
|
|
1391
|
+
* the element has to exist, and the host's `slots` entry has to point at it. Splitting them
|
|
1392
|
+
* would be two rows in the changes panel and two things to undo for one gesture, which is
|
|
1393
|
+
* the same reason this op already writes the layer and the anatomy node together rather
|
|
1394
|
+
* than making a caller do both.
|
|
1395
|
+
*
|
|
1396
|
+
* The entry is json-render's own `slots` field, which its renderer resolves itself — so
|
|
1397
|
+
* this writes a mechanism the renderer has, rather than inventing one.
|
|
1398
|
+
*/
|
|
1399
|
+
slotProp: z.string().optional(),
|
|
1400
|
+
/**
|
|
1401
|
+
* A condition on the new node — json-render's `visible` grammar, or the flat
|
|
1402
|
+
* `{ prop: value }` an editor collects, which is compiled to it.
|
|
1403
|
+
*
|
|
1404
|
+
* Typed rather than `z.unknown()`, which is the whole fix: an untyped input accepted the
|
|
1405
|
+
* flat form at the op boundary and failed two writes later inside the anatomy create, so
|
|
1406
|
+
* every variant-scoped fill was refused while its draft row still displayed it.
|
|
1407
|
+
*/
|
|
1408
|
+
visible: visibility.optional(),
|
|
1409
|
+
/** Text content for the new element — a declared field of the stored node the op had no
|
|
1410
|
+
* input for, so an inserted element could not arrive with its text. */
|
|
1411
|
+
text: z.string().optional(),
|
|
1412
|
+
/** The new element's own children, by key — the same edge list the anatomy node stores. */
|
|
1413
|
+
children: z.array(z.string()).optional(),
|
|
1414
|
+
/** The new element's own slot fills — json-render's native grammar, keys into `nodes`. */
|
|
1415
|
+
slots: z.record(z.string(), z.array(z.string())).optional(),
|
|
1416
|
+
/** The subtree that lands with it: every descendant, keyed, in the stored shape. See
|
|
1417
|
+
* {@link InsertedNode}. */
|
|
1418
|
+
nodes: z.record(z.string(), insertedNode).optional(),
|
|
1419
|
+
/**
|
|
1420
|
+
* The same subtree in the caller's own key space — `root` names which of `elements` is the
|
|
1421
|
+
* one being inserted, and every key is grafted onto the component's namespace by
|
|
1422
|
+
* {@link graftKey}.
|
|
1423
|
+
*
|
|
1424
|
+
* The form a caller actually holds. Without it, Studio re-keyed the subtree itself, hoisted
|
|
1425
|
+
* its root out into the flat fields and renamed `type` to `element` before every insert —
|
|
1426
|
+
* the graft convention living at the caller, where the reader that inverts it could drift
|
|
1427
|
+
* from it. Mutually exclusive with `key`/`element`/`props`/`children`/`nodes`.
|
|
1428
|
+
*/
|
|
1429
|
+
subtree: z.object({
|
|
1430
|
+
root: z.string(),
|
|
1431
|
+
elements: z.record(z.string(), insertedNode)
|
|
1432
|
+
}).optional()
|
|
1433
|
+
}).prefault({})
|
|
1434
|
+
}),
|
|
1435
|
+
readOnly: false,
|
|
1436
|
+
label: "Add element",
|
|
1437
|
+
description: "Add an element to a component: creates the LAYER it is styled through and the anatomy node that places it, under `parent`.",
|
|
1438
|
+
scope: "item",
|
|
1439
|
+
creates: true,
|
|
1440
|
+
title: (entity) => `Add ${entity} element`,
|
|
1441
|
+
handler: (input, config) => {
|
|
1442
|
+
const payload = input.data ?? {};
|
|
1443
|
+
const path = String(input.path);
|
|
1444
|
+
const parent = payload.parent ? String(payload.parent) : void 0;
|
|
1445
|
+
const key = input.key ? String(input.key) : freeLayerKey(config, path, "el");
|
|
1446
|
+
const given = payload.subtree;
|
|
1447
|
+
let element = payload.element;
|
|
1448
|
+
let props = payload.props;
|
|
1449
|
+
let text = payload.text;
|
|
1450
|
+
let visible = payload.visible;
|
|
1451
|
+
let ownChildren = payload.children;
|
|
1452
|
+
let ownSlots = payload.slots;
|
|
1453
|
+
let nodes = payload.nodes ?? {};
|
|
1454
|
+
if (given) {
|
|
1455
|
+
const local = (k) => k === given.root ? key : graftKey(key, k);
|
|
1456
|
+
const localSlots = (slots) => slots ? Object.fromEntries(Object.entries(slots).map(([name, fills]) => [name, fills.map(local)])) : void 0;
|
|
1457
|
+
const rootNode = given.elements[given.root];
|
|
1458
|
+
element = rootNode?.element ?? element;
|
|
1459
|
+
props = rootNode?.props ?? props;
|
|
1460
|
+
text = rootNode?.text ?? text;
|
|
1461
|
+
visible = rootNode?.visible ?? visible;
|
|
1462
|
+
ownChildren = rootNode?.children?.map(local) ?? ownChildren;
|
|
1463
|
+
ownSlots = localSlots(rootNode?.slots) ?? ownSlots;
|
|
1464
|
+
nodes = Object.fromEntries(Object.entries(given.elements).filter(([k]) => k !== given.root).map(([k, node]) => [local(k), {
|
|
1465
|
+
...node,
|
|
1466
|
+
children: node.children?.map(local),
|
|
1467
|
+
slots: localSlots(node.slots)
|
|
1468
|
+
}]));
|
|
1469
|
+
}
|
|
1470
|
+
let next = config.apply({
|
|
1471
|
+
kind: "component",
|
|
1472
|
+
operation: "sub-create",
|
|
1473
|
+
path,
|
|
1474
|
+
field: "layers",
|
|
1475
|
+
key,
|
|
1476
|
+
data: asElementRef(config, element)
|
|
1477
|
+
}).apply({
|
|
1478
|
+
kind: "component",
|
|
1479
|
+
operation: "sub-create",
|
|
1480
|
+
path,
|
|
1481
|
+
field: "anatomy",
|
|
1482
|
+
key,
|
|
1483
|
+
data: {
|
|
1484
|
+
element: layerRef(key),
|
|
1485
|
+
...props ? { props } : {},
|
|
1486
|
+
...visible !== void 0 ? { visible } : {},
|
|
1487
|
+
...text !== void 0 ? { text } : {}
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
next = insertSubtree(next, path, nodes);
|
|
1491
|
+
if (ownChildren?.length || ownSlots) next = next.apply({
|
|
1492
|
+
kind: "component",
|
|
1493
|
+
operation: "sub-update",
|
|
1494
|
+
path,
|
|
1495
|
+
field: "anatomy",
|
|
1496
|
+
key,
|
|
1497
|
+
data: {
|
|
1498
|
+
...ownChildren?.length ? { children: ownChildren } : {},
|
|
1499
|
+
...ownSlots ? { slots: ownSlots } : {}
|
|
1500
|
+
}
|
|
1501
|
+
});
|
|
1502
|
+
if (!parent) return next;
|
|
1503
|
+
let body = next.resolve("component", path)?.toJSON();
|
|
1504
|
+
if (!body?.anatomy?.[parent] && body?.layers && parent in body.layers) {
|
|
1505
|
+
next = next.apply({
|
|
1506
|
+
kind: "component",
|
|
1507
|
+
operation: "sub-create",
|
|
1508
|
+
path,
|
|
1509
|
+
field: "anatomy",
|
|
1510
|
+
key: parent,
|
|
1511
|
+
data: { element: layerRef(parent) }
|
|
1512
|
+
});
|
|
1513
|
+
body = next.resolve("component", path)?.toJSON();
|
|
1514
|
+
}
|
|
1515
|
+
const throughSlot = Boolean(payload.slotProp);
|
|
1516
|
+
const siblings = [...body?.anatomy?.[parent]?.children ?? []];
|
|
1517
|
+
if (!throughSlot) {
|
|
1518
|
+
const at = typeof payload.index === "number" ? payload.index : siblings.length;
|
|
1519
|
+
siblings.splice(at, 0, key);
|
|
1520
|
+
}
|
|
1521
|
+
const slotProp = payload.slotProp ? String(payload.slotProp) : void 0;
|
|
1522
|
+
const fill = slotProp ? slotFillHostWrites({
|
|
1523
|
+
config: next,
|
|
1524
|
+
path,
|
|
1525
|
+
parent,
|
|
1526
|
+
key,
|
|
1527
|
+
slotProp,
|
|
1528
|
+
node: body?.anatomy?.[parent],
|
|
1529
|
+
layers: body?.layers
|
|
1530
|
+
}) : void 0;
|
|
1531
|
+
return next.apply({
|
|
1532
|
+
kind: "component",
|
|
1533
|
+
operation: "sub-update",
|
|
1534
|
+
path,
|
|
1535
|
+
field: "anatomy",
|
|
1536
|
+
key: parent,
|
|
1537
|
+
data: {
|
|
1538
|
+
...throughSlot ? {} : { children: siblings },
|
|
1539
|
+
...fill ? { slots: fill.slots } : {},
|
|
1540
|
+
...fill?.visible !== void 0 ? { visible: fill.visible } : {}
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
},
|
|
1545
|
+
/**
|
|
1546
|
+
* Remove an element from a composition — the node, its layer, and everything that named it.
|
|
1547
|
+
*
|
|
1548
|
+
* A custom op for the same reason `element-insert` is: it writes more than one thing and either
|
|
1549
|
+
* half alone is broken. `sub-delete/layers` drops the layer and leaves the anatomy node holding
|
|
1550
|
+
* a dangling `layer:<key>` ref, its parent's `children` still naming it, and every style rule
|
|
1551
|
+
* still carrying a bag for a layer that no longer exists. `sub-delete/anatomy` leaves the parent
|
|
1552
|
+
* edge — a value the stored `children` enum would now refuse on the next write to that node.
|
|
1553
|
+
*
|
|
1554
|
+
* The rule bags are the part `orphans` cannot express: a rule targeting a deleted layer is not
|
|
1555
|
+
* itself orphaned (it has other layers to style), so what has to go is the layer's entry inside
|
|
1556
|
+
* each rule. That is a delta, which is what the merge verb is for — the rule is left alone when
|
|
1557
|
+
* it still styles something and pruned by the caller's own `sub-delete` when it doesn't.
|
|
1558
|
+
*/
|
|
1559
|
+
"element-delete": {
|
|
1560
|
+
input: z.object({
|
|
1561
|
+
path: pathInput("component"),
|
|
1562
|
+
key: z.string(),
|
|
1563
|
+
data: z.object({
|
|
1564
|
+
/** Remove everything under it too. Default true: a node's children are placed by it and
|
|
1565
|
+
* nothing else, so leaving them would strand a subtree no parent names. */
|
|
1566
|
+
subtree: z.boolean().default(true) }).optional()
|
|
1567
|
+
}),
|
|
1568
|
+
readOnly: false,
|
|
1569
|
+
label: "Remove element",
|
|
1570
|
+
description: "Remove an element from a component: its anatomy node, the layer it is styled through, its subtree, every parent edge naming it, and its entry in every style rule.",
|
|
1571
|
+
scope: "item",
|
|
1572
|
+
approval: "destructive",
|
|
1573
|
+
title: (entity) => `Remove ${entity} element`,
|
|
1574
|
+
handler: (input, config) => {
|
|
1575
|
+
const payload = input.data ?? {};
|
|
1576
|
+
const path = String(input.path);
|
|
1577
|
+
const key = String(input.key);
|
|
1578
|
+
const body = config.resolve("component", path)?.toJSON();
|
|
1579
|
+
const anatomy = body?.anatomy ?? {};
|
|
1580
|
+
const doomed = new Set([key]);
|
|
1581
|
+
if (payload.subtree !== false) {
|
|
1582
|
+
const queue = [key];
|
|
1583
|
+
while (queue.length > 0) {
|
|
1584
|
+
const current = queue.shift();
|
|
1585
|
+
if (current === void 0) continue;
|
|
1586
|
+
for (const child of anatomy[current]?.children ?? []) {
|
|
1587
|
+
if (doomed.has(child)) continue;
|
|
1588
|
+
doomed.add(child);
|
|
1589
|
+
queue.push(child);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
const stillBound = /* @__PURE__ */ new Set();
|
|
1594
|
+
for (const [name, node] of Object.entries(anatomy)) {
|
|
1595
|
+
if (doomed.has(name)) continue;
|
|
1596
|
+
const bound = layerRefTarget(node.element);
|
|
1597
|
+
if (bound !== void 0) stillBound.add(bound);
|
|
1598
|
+
}
|
|
1599
|
+
const layersGoing = new Set([...doomed].filter((name) => body?.layers !== void 0 && name in body.layers && !stillBound.has(name)));
|
|
1600
|
+
let next = config;
|
|
1601
|
+
for (const [name, node] of Object.entries(anatomy)) {
|
|
1602
|
+
if (doomed.has(name)) continue;
|
|
1603
|
+
const keptChildren = (node.children ?? []).filter((child) => !doomed.has(child));
|
|
1604
|
+
const staleSlots = staleSlotEntries(node.slots, doomed);
|
|
1605
|
+
if (keptChildren.length === (node.children ?? []).length && staleSlots.length === 0) continue;
|
|
1606
|
+
next = next.apply({
|
|
1607
|
+
kind: "component",
|
|
1608
|
+
operation: "sub-update",
|
|
1609
|
+
path,
|
|
1610
|
+
field: "anatomy",
|
|
1611
|
+
key: name,
|
|
1612
|
+
data: {
|
|
1613
|
+
...keptChildren.length !== (node.children ?? []).length ? { children: keptChildren } : {},
|
|
1614
|
+
...Object.fromEntries(staleSlots.map(([slot, kept]) => [`slots.${slot}`, kept.length ? kept : null]))
|
|
1615
|
+
}
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
for (const [condition, rule] of Object.entries(body?.styles ?? {})) {
|
|
1619
|
+
const targeted = Object.keys(rule.layers ?? {}).filter((layer) => layersGoing.has(layer));
|
|
1620
|
+
if (targeted.length === 0) continue;
|
|
1621
|
+
next = next.apply({
|
|
1622
|
+
kind: "component",
|
|
1623
|
+
operation: "sub-update",
|
|
1624
|
+
path,
|
|
1625
|
+
field: "styles",
|
|
1626
|
+
key: condition,
|
|
1627
|
+
data: Object.fromEntries(targeted.map((layer) => [`layers.${layer}`, null]))
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
for (const name of doomed) {
|
|
1631
|
+
if (anatomy[name]) next = next.apply({
|
|
1632
|
+
kind: "component",
|
|
1633
|
+
operation: "sub-delete",
|
|
1634
|
+
path,
|
|
1635
|
+
field: "anatomy",
|
|
1636
|
+
key: name
|
|
1637
|
+
});
|
|
1638
|
+
if (layersGoing.has(name)) next = next.apply({
|
|
1639
|
+
kind: "component",
|
|
1640
|
+
operation: "sub-delete",
|
|
1641
|
+
path,
|
|
1642
|
+
field: "layers",
|
|
1643
|
+
key: name
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
return next;
|
|
1647
|
+
}
|
|
1648
|
+
},
|
|
1649
|
+
/**
|
|
1650
|
+
* Move a node to a different parent, or to a different place under the same one.
|
|
1651
|
+
*
|
|
1652
|
+
* Sibling order lives in a parent's `children`, so a move is two updates — one per parent — and
|
|
1653
|
+
* both have to land or the node is in neither list or in both.
|
|
1654
|
+
*
|
|
1655
|
+
* A node can also be held by a slot rather than a child edge (`slots: { icon: [key] }`),
|
|
1656
|
+
* and a move treats both as the home being left: every child edge naming the key is detached,
|
|
1657
|
+
* and every stale slot entry naming it is cleared, whichever new home the move names. With
|
|
1658
|
+
* `slotProp` the new home is the parent's slot instead of its `children` — the same statement
|
|
1659
|
+
* `element-insert` makes about a new node, made about one that already exists, so the moved
|
|
1660
|
+
* element keeps its key, props, styles and subtree.
|
|
1661
|
+
*/
|
|
1662
|
+
"element-move": {
|
|
1663
|
+
input: z.object({
|
|
1664
|
+
path: pathInput("component"),
|
|
1665
|
+
key: z.string(),
|
|
1666
|
+
data: z.object({
|
|
1667
|
+
parent: z.string().default("root"),
|
|
1668
|
+
index: z.number().optional(),
|
|
1669
|
+
/** Fill the parent's slot with the moved node — by name of the parent's prop — instead of
|
|
1670
|
+
* splicing it into `children`. `index` is inert when set: a slot holds one ref, not a
|
|
1671
|
+
* position. */
|
|
1672
|
+
slotProp: z.string().optional()
|
|
1673
|
+
}).prefault({})
|
|
1674
|
+
}),
|
|
1675
|
+
readOnly: false,
|
|
1676
|
+
label: "Move element",
|
|
1677
|
+
description: "Move a component's anatomy node under a different parent, to a different position under the same one, or — with `slotProp` — into a parent's slot.",
|
|
1678
|
+
scope: "item",
|
|
1679
|
+
approval: "reference-change",
|
|
1680
|
+
title: (entity) => `Move ${entity} element`,
|
|
1681
|
+
handler: (input, config) => {
|
|
1682
|
+
const payload = input.data ?? {};
|
|
1683
|
+
const path = String(input.path);
|
|
1684
|
+
const key = String(input.key);
|
|
1685
|
+
const parent = String(payload.parent);
|
|
1686
|
+
const slotProp = payload.slotProp ? String(payload.slotProp) : void 0;
|
|
1687
|
+
const body = resolveMoveBody(config, path);
|
|
1688
|
+
const anatomy = body?.anatomy ?? {};
|
|
1689
|
+
const isKnown = (name) => Boolean(anatomy[name] || body?.layers && name in body.layers);
|
|
1690
|
+
if (!isKnown(key)) reject("NOT_FOUND", `"${path}" has no element "${key}" to move.`);
|
|
1691
|
+
if (!isKnown(parent)) reject("NOT_FOUND", `"${path}" has no element "${parent}" to move under.`);
|
|
1692
|
+
if (anatomySubtreeContains(anatomy, key, parent)) rejectData(`Moving "${key}" under "${parent}" would nest it inside itself.`, ["parent"]);
|
|
1693
|
+
let next = config;
|
|
1694
|
+
for (const [name, node] of Object.entries(anatomy)) {
|
|
1695
|
+
const children = !(name === parent && !slotProp) && node.children?.includes(key) ? node.children.filter((c) => c !== key) : void 0;
|
|
1696
|
+
const staleSlots = staleSlotEntries(node.slots, new Set([key]));
|
|
1697
|
+
if (children === void 0 && staleSlots.length === 0) continue;
|
|
1698
|
+
next = next.apply({
|
|
1699
|
+
kind: "component",
|
|
1700
|
+
operation: "sub-update",
|
|
1701
|
+
path,
|
|
1702
|
+
field: "anatomy",
|
|
1703
|
+
key: name,
|
|
1704
|
+
data: {
|
|
1705
|
+
...children !== void 0 ? { children } : {},
|
|
1706
|
+
...Object.fromEntries(staleSlots.map(([slot, kept]) => [`slots.${slot}`, kept.length ? kept : null]))
|
|
1707
|
+
}
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
let moved = next === config ? body : resolveMoveBody(next, path);
|
|
1711
|
+
if (!slotProp) {
|
|
1712
|
+
const siblings = [...(moved?.anatomy?.[parent]?.children ?? []).filter((c) => c !== key)];
|
|
1713
|
+
const at = typeof payload.index === "number" ? payload.index : siblings.length;
|
|
1714
|
+
siblings.splice(at, 0, key);
|
|
1715
|
+
return next.apply({
|
|
1716
|
+
kind: "component",
|
|
1717
|
+
operation: "sub-update",
|
|
1718
|
+
path,
|
|
1719
|
+
field: "anatomy",
|
|
1720
|
+
key: parent,
|
|
1721
|
+
data: { children: siblings }
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
if (!moved?.anatomy?.[parent] && moved?.layers && parent in moved.layers) {
|
|
1725
|
+
next = next.apply({
|
|
1726
|
+
kind: "component",
|
|
1727
|
+
operation: "sub-create",
|
|
1728
|
+
path,
|
|
1729
|
+
field: "anatomy",
|
|
1730
|
+
key: parent,
|
|
1731
|
+
data: { element: layerRef(parent) }
|
|
1732
|
+
});
|
|
1733
|
+
moved = resolveMoveBody(next, path);
|
|
1734
|
+
}
|
|
1735
|
+
const occupant = moved?.anatomy?.[parent]?.slots?.[slotProp];
|
|
1736
|
+
if (occupant?.length) reject("ALREADY_EXISTS", `"${parent}" already fills \`${slotProp}\` with "${occupant[0]}" — move that element out first.`);
|
|
1737
|
+
const fill = slotFillHostWrites({
|
|
1738
|
+
config: next,
|
|
1739
|
+
path,
|
|
1740
|
+
parent,
|
|
1741
|
+
key,
|
|
1742
|
+
slotProp,
|
|
1743
|
+
node: moved?.anatomy?.[parent],
|
|
1744
|
+
layers: moved?.layers
|
|
1745
|
+
});
|
|
1746
|
+
return next.apply({
|
|
1747
|
+
kind: "component",
|
|
1748
|
+
operation: "sub-update",
|
|
1749
|
+
path,
|
|
1750
|
+
field: "anatomy",
|
|
1751
|
+
key: parent,
|
|
1752
|
+
data: {
|
|
1753
|
+
slots: fill.slots,
|
|
1754
|
+
...fill.visible !== void 0 ? { visible: fill.visible } : {}
|
|
1755
|
+
}
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
},
|
|
1759
|
+
validate: {
|
|
1760
|
+
input: z.object({
|
|
1761
|
+
path: pathInput("component"),
|
|
1762
|
+
data: z.object({ props: z.record(z.string(), z.unknown()) })
|
|
1763
|
+
}),
|
|
1764
|
+
readOnly: true,
|
|
1765
|
+
label: "Validate props",
|
|
1766
|
+
description: "Check a bag of prop VALUES against a component — rejects unknown props and out-of-domain values.",
|
|
1767
|
+
scope: "item",
|
|
1768
|
+
title: (entity) => `Validate ${entity} props`,
|
|
1769
|
+
example: ({ config, path }) => ({
|
|
1770
|
+
path,
|
|
1771
|
+
data: { props: exampleProps(config, path) }
|
|
1772
|
+
}),
|
|
1773
|
+
handler: (input, config) => validateComponentProps(config, String(input.path), input.data.props ?? {})
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
});
|
|
1777
|
+
const baseComponentRef = ComponentEntity.ref.bind(ComponentEntity);
|
|
1778
|
+
/** Component refs add the source-only authoring steps to the generic ref-extension value. All of them
|
|
1779
|
+
* are non-enumerable: evaluating the config produces the same small wire marker that a hydrated config
|
|
1780
|
+
* receives, while the build independently reads `.render(fn)` from this file. */
|
|
1781
|
+
function componentRef(path) {
|
|
1782
|
+
const base = baseComponentRef(path);
|
|
1783
|
+
const value = { __ref: base.__ref };
|
|
1784
|
+
/**
|
|
1785
|
+
* One extension value, with its authoring steps attached.
|
|
1786
|
+
*
|
|
1787
|
+
* `render` is a no-op that hands the value back — the build reads the callback out of this file
|
|
1788
|
+
* rather than from the stored data, which is why it is source-only. `styles` is not a no-op: it
|
|
1789
|
+
* folds the rules into the delta and re-extends, so however many steps a chain has, the wire form
|
|
1790
|
+
* stays one `{ __ref, extend }`.
|
|
1791
|
+
*/
|
|
1792
|
+
const step = (delta) => {
|
|
1793
|
+
const extended = base.extend(delta);
|
|
1794
|
+
Object.defineProperty(extended, "render", {
|
|
1795
|
+
enumerable: false,
|
|
1796
|
+
configurable: false,
|
|
1797
|
+
value: () => extended
|
|
1798
|
+
});
|
|
1799
|
+
Object.defineProperty(extended, "styles", {
|
|
1800
|
+
enumerable: false,
|
|
1801
|
+
configurable: false,
|
|
1802
|
+
value: (rules) => {
|
|
1803
|
+
const styles = {};
|
|
1804
|
+
for (const rule of rules) styles[canonicalWhen(rule.when)] = {
|
|
1805
|
+
when: rule.when ?? null,
|
|
1806
|
+
layers: rule.layers
|
|
1807
|
+
};
|
|
1808
|
+
return step({
|
|
1809
|
+
...delta,
|
|
1810
|
+
styles
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
return extended;
|
|
1815
|
+
};
|
|
1816
|
+
Object.defineProperty(value, "extend", {
|
|
1817
|
+
enumerable: false,
|
|
1818
|
+
configurable: false,
|
|
1819
|
+
value: (delta) => step(delta)
|
|
1820
|
+
});
|
|
1821
|
+
return value;
|
|
1822
|
+
}
|
|
1823
|
+
/** Component keeps the generic entity surface; only its ref authoring result knows about the
|
|
1824
|
+
* source-only `.render()` step. */
|
|
1825
|
+
const Component = Object.assign(ComponentEntity, { ref: componentRef });
|
|
1826
|
+
/** A valid example prop bag — the first variant prop's first value, else empty. Lives here because
|
|
1827
|
+
* it reads a component's stored props; the generic example filler only knows address fields.
|
|
1828
|
+
* Resolved through the domain (not `ownValues`): a variant that delegates its values to a group
|
|
1829
|
+
* (`name: Icon.ref('phosphor')`) owns nothing, but its leaves are exactly what an example should show. */
|
|
1830
|
+
function exampleProps(config, componentPath) {
|
|
1831
|
+
const props = (config.resolve("component", componentPath)?.toJSON())?.props ?? {};
|
|
1832
|
+
for (const [name, decl] of Object.entries(props)) {
|
|
1833
|
+
if (decl?.type !== "variant") continue;
|
|
1834
|
+
const leaves = propValueDomain(config, decl)?.leaves ?? [];
|
|
1835
|
+
if (leaves.length > 0) return { [name]: leaves[0] };
|
|
1836
|
+
}
|
|
1837
|
+
return {};
|
|
1838
|
+
}
|
|
1839
|
+
//#endregion
|
|
1840
|
+
export { Component, canonicalWhen, forwardedNames, layerRef, ownValues, ownsItsValues, valueRef };
|