@yahoo/uds-create-config 5.25.2 → 5.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/configs/system.js +2 -0
- package/dist/entities/system/Component.d.ts +35 -2
- package/dist/entities/system/Component.js +224 -9
- package/dist/entities/system/Settings.js +1 -1
- package/dist/entities/system/StyleProperty.js +1 -1
- package/dist/entities/system/component-module.d.ts +17 -1
- package/dist/entities/system/component-module.js +54 -2
- package/dist/entities/system/defineComponent.d.ts +53 -13
- package/dist/entities/system/defineComponent.js +1 -0
- package/dist/entities/system/forward-route-invariants.js +1 -1
- package/dist/entities/system/icon-domain-invariants.js +1 -1
- package/dist/entities/system/root-layer-invariants.js +1 -1
- package/dist/entities/system/token-values.js +1 -1
- package/dist/framework/Config.d.ts +1 -1
- package/dist/framework/Config.js +1 -1
- package/dist/framework/class-names.d.ts +27 -1
- package/dist/framework/class-names.js +56 -3
- package/dist/framework/component-states.d.ts +40 -0
- package/dist/framework/component-states.js +53 -0
- package/dist/framework/interaction-conditions.d.ts +48 -0
- package/dist/framework/interaction-conditions.js +104 -0
- package/dist/framework/layer-styles.d.ts +5 -1
- package/dist/framework/layer-styles.js +18 -4
- package/dist/framework/projections.js +44 -2
- package/dist/framework/render-spec.js +14 -4
- package/dist/framework/schema-version.d.ts +1 -1
- package/dist/framework/schema-version.js +5 -5
- package/dist/framework/validate-spec.js +14 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +6 -4
- package/dist/migrations/20261013154037_component_states.d.ts +19 -0
- package/dist/migrations/20261013154037_component_states.js +74 -0
- package/dist/renderer/componentEntry.d.ts +7 -1
- package/dist/renderer/componentEntry.js +30 -1
- package/dist/renderer/forced-stamp.d.ts +26 -0
- package/dist/renderer/forced-stamp.js +50 -0
- package/dist/renderer/index.d.ts +3 -1
- package/dist/renderer/index.js +3 -1
- package/dist/renderer/use-forced-stamp.d.ts +11 -0
- package/dist/renderer/use-forced-stamp.js +50 -0
- package/dist/spec/specToJsx.d.ts +6 -0
- package/dist/spec/specToJsx.js +5 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
package/dist/configs/system.js
CHANGED
|
@@ -66,6 +66,7 @@ import { startingStyleModifierMigration } from "../migrations/20261009022722_sta
|
|
|
66
66
|
import { cssMotionStrictMigration } from "../migrations/20261009181635_css_motion_strict.js";
|
|
67
67
|
import { linkOverrideRefMigration } from "../migrations/20261009193004_link_override_ref.js";
|
|
68
68
|
import { linkedComponentModulesMigration } from "../migrations/20261012200606_linked_component_modules.js";
|
|
69
|
+
import { componentStatesMigration } from "../migrations/20261013154037_component_states.js";
|
|
69
70
|
const System = defineConfig({
|
|
70
71
|
name: WEB_CONFIG_NAME,
|
|
71
72
|
description: "A design system: the values, primitives and compositions an app is built from. A ref is written `{ \"__ref\": \"<kind>:<path>\" }`, so a token is `{ \"__ref\": \"token:<group>/<name>\" }` and a token group is `{ \"__ref\": \"token:<group>\" }`. Build it in this order. Tokens first, in groups by purpose (colors, spacing, radii, type sizes), since everything else names them. Then style properties, each with `values` holding one ref to the token group whose members it accepts (`{ \"__ref\": \"token:color\" }`), so a prop takes token names rather than raw CSS; add a string keyword only for a value no token holds. A style rule's bag holds a prop's values (`\"padding\": \"4\"`, `\"bg\": \"brand\"`), never raw CSS. Then a few primitive components, one layer each, whose props are style properties (a box for layout and surface, a text for type, a pressable for interaction). Then every other component composed from those primitives: its layers name a primitive or an element, its anatomy places them (a node renders a layer, a tag or `Slot` where a slot prop's content lands; `children` is the edge list; `visible` is `{ \"$state\": \"<prop>\" }`; a prop shown in a nested layer is a `forward` route, and `$state` never sits in a node's props), and its style rules set style-property values per layer, keyed by prop values when a variant changes them. Modes (a dark mode is `{ type: \"selector\", value: \".dark\" }`) change token values through a token's `overrides`, so a component never restyles for a mode; modifiers (hover, focus, disabled) name a state, and a style rule applies under one as a `_hover` block inside a layer bag. Composites bundle style values that recur. Last, register canvas roles: the canvas draws with them, one entity per tool (`frame` for the layout primitive, `text` for the text primitive, `image`) whose entries name the components that tool inserts, and a tool whose role is unregistered is disabled. Give each component a `preview` entry under `*` so it shows content, and never write a CSS property name where a style-property name belongs.",
|
|
@@ -143,5 +144,6 @@ if (!registeredSchemaMigrations().has(20261009022722)) registerSchemaMigrations(
|
|
|
143
144
|
if (!registeredSchemaMigrations().has(20261009181635)) registerSchemaMigrations(cssMotionStrictMigration);
|
|
144
145
|
if (!registeredSchemaMigrations().has(20261009193004)) registerSchemaMigrations(linkOverrideRefMigration);
|
|
145
146
|
if (!registeredSchemaMigrations().has(20261012200606)) registerSchemaMigrations(linkedComponentModulesMigration);
|
|
147
|
+
if (!registeredSchemaMigrations().has(20261013154037)) registerSchemaMigrations(componentStatesMigration);
|
|
146
148
|
//#endregion
|
|
147
149
|
export { System };
|
|
@@ -11,7 +11,7 @@ import { StyleClass } from "./style-bag.js";
|
|
|
11
11
|
import { TokenClass } from "./Token.js";
|
|
12
12
|
import { StoredPreviewCondition } from "../../framework/preview-store.js";
|
|
13
13
|
import { RepeatSpec } from "../../framework/render-spec.js";
|
|
14
|
-
import { AsInput, DefaultsOf, LayerMap, PhantomCallProps, PhantomReadProps, PropMap, RenderArgs, StyleRuleInput } from "./defineComponent.js";
|
|
14
|
+
import { AsInput, DefaultsOf, LayerMap, PhantomCallProps, PhantomReadProps, PropMap, RenderArgs, StateMap, StyleRuleInput } from "./defineComponent.js";
|
|
15
15
|
import { listSlots, namedSlots, slotHostedBy, slotOf, slotsFor } from "../../spec/slot.js";
|
|
16
16
|
import { cellAt } from "../../spec/cell.js";
|
|
17
17
|
import { instanceAt } from "../../spec/instance.js";
|
|
@@ -645,6 +645,13 @@ interface AnatomyNodeClass extends Named<typeof AnatomyNodeDefinition> {}
|
|
|
645
645
|
declare const componentFields: z.ZodObject<{
|
|
646
646
|
layers: LayerClass;
|
|
647
647
|
props: z.ZodOptional<PropClass>;
|
|
648
|
+
states: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
649
|
+
type: z.ZodLiteral<"boolean">;
|
|
650
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
651
|
+
type: z.ZodLiteral<"variant">;
|
|
652
|
+
value: z.ZodArray<z.ZodString>;
|
|
653
|
+
default: z.ZodOptional<z.ZodString>;
|
|
654
|
+
}, z.core.$strict>], "type">>>;
|
|
648
655
|
forwards: z.ZodOptional<ForwardClass>;
|
|
649
656
|
styles: z.ZodOptional<z.ZodUnion<readonly [z.ZodPipe<z.ZodArray<z.ZodType<ComponentStyleBody, unknown, z.core.$ZodTypeInternals<ComponentStyleBody, unknown>>>, z.ZodTransform<Record<string, ComponentStyleBody>, ComponentStyleBody[]>>, SubEntityClass<z.ZodType<ComponentStyleBody, unknown, z.core.$ZodTypeInternals<ComponentStyleBody, unknown>>>]>>;
|
|
650
657
|
renderProps: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -679,6 +686,13 @@ declare function blankComponentBody(): ComponentBody;
|
|
|
679
686
|
declare const ComponentEntity: EntityClass<z.ZodObject<{
|
|
680
687
|
layers: LayerClass;
|
|
681
688
|
props: z.ZodOptional<PropClass>;
|
|
689
|
+
states: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
690
|
+
type: z.ZodLiteral<"boolean">;
|
|
691
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
692
|
+
type: z.ZodLiteral<"variant">;
|
|
693
|
+
value: z.ZodArray<z.ZodString>;
|
|
694
|
+
default: z.ZodOptional<z.ZodString>;
|
|
695
|
+
}, z.core.$strict>], "type">>>;
|
|
682
696
|
forwards: z.ZodOptional<ForwardClass>;
|
|
683
697
|
styles: z.ZodOptional<z.ZodUnion<readonly [z.ZodPipe<z.ZodArray<z.ZodType<ComponentStyleBody, unknown, z.core.$ZodTypeInternals<ComponentStyleBody, unknown>>>, z.ZodTransform<Record<string, ComponentStyleBody>, ComponentStyleBody[]>>, SubEntityClass<z.ZodType<ComponentStyleBody, unknown, z.core.$ZodTypeInternals<ComponentStyleBody, unknown>>>]>>;
|
|
684
698
|
renderProps: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -780,6 +794,19 @@ declare const ComponentEntity: EntityClass<z.ZodObject<{
|
|
|
780
794
|
}, z.core.$strip>>;
|
|
781
795
|
}, z.core.$strip>;
|
|
782
796
|
};
|
|
797
|
+
readonly 'element-wrap': {
|
|
798
|
+
readonly input: z.ZodObject<{
|
|
799
|
+
path: z.ZodType<string, string | undefined, z.core.$ZodTypeInternals<string, string | undefined>>;
|
|
800
|
+
key: z.ZodOptional<z.ZodString>;
|
|
801
|
+
data: z.ZodObject<{
|
|
802
|
+
elements: z.ZodArray<z.ZodString>;
|
|
803
|
+
element: z.ZodDefault<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
804
|
+
__ref: z.ZodString;
|
|
805
|
+
}, z.core.$strip>]>>;
|
|
806
|
+
props: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
807
|
+
}, z.core.$strip>;
|
|
808
|
+
}, z.core.$strip>;
|
|
809
|
+
};
|
|
783
810
|
readonly validate: {
|
|
784
811
|
readonly input: z.ZodObject<{
|
|
785
812
|
path: z.ZodType<string, string | undefined, z.core.$ZodTypeInternals<string, string | undefined>>;
|
|
@@ -841,6 +868,12 @@ type ResolvedForwards<Contract, Delta> = Delta extends {
|
|
|
841
868
|
} ? Forwards : Contract extends {
|
|
842
869
|
forwards: infer Forwards;
|
|
843
870
|
} ? Forwards : Record<never, never>;
|
|
871
|
+
/** The states an extension ends up with: its delta's, else the linked contract's. */
|
|
872
|
+
type ResolvedStates<Contract, Delta> = Delta extends {
|
|
873
|
+
readonly states: infer States extends StateMap;
|
|
874
|
+
} ? States : Contract extends {
|
|
875
|
+
states: infer States extends StateMap;
|
|
876
|
+
} ? States : Record<never, never>;
|
|
844
877
|
type ForwardedLayerNames<Contract, Delta> = ResolvedForwards<Contract, Delta> extends Record<string, unknown> ? keyof ResolvedForwards<Contract, Delta> & string : never;
|
|
845
878
|
/** The source-only half of a linked component extension. The callback is deliberately absent from
|
|
846
879
|
* {@link ExtendedRef}: it is code, so ts-morph reads it from the authoring file while Config stores
|
|
@@ -852,7 +885,7 @@ type ExtendedComponentRef<Contract = ComponentContractOf<string>, Delta extends
|
|
|
852
885
|
* read signature for `ComponentProps`. */
|
|
853
886
|
<As extends AsInput = never>(props: PhantomCallProps<ResolvedLayers<Contract, Delta>, ResolvedProps<Contract, Delta>, ResolvedDefaults<Contract, Delta>, Extra, ForwardedLayerNames<Contract, Delta>, As>): null;
|
|
854
887
|
<As extends AsInput = never>(props: PhantomReadProps<ResolvedLayers<Contract, Delta>, ResolvedProps<Contract, Delta>, ResolvedDefaults<Contract, Delta>, Extra, ForwardedLayerNames<Contract, Delta>, As>): null;
|
|
855
|
-
render<E = Extra>(fn: (args: RenderArgs<ResolvedLayers<Contract, Delta>, ResolvedProps<Contract, Delta>> & E) => unknown): ExtendedComponentRef<Contract, Delta, E>;
|
|
888
|
+
render<E = Extra>(fn: (args: RenderArgs<ResolvedLayers<Contract, Delta>, ResolvedProps<Contract, Delta>, ResolvedStates<Contract, Delta>> & E) => unknown): ExtendedComponentRef<Contract, Delta, E>;
|
|
856
889
|
/**
|
|
857
890
|
* Style rules for the extension, typed against the layers it ends up with — the linked contract's,
|
|
858
891
|
* with the delta's replacing them when it declares its own.
|
|
@@ -7,6 +7,7 @@ import { shadowValue } from "./shadow.js";
|
|
|
7
7
|
import { defineEntity } from "../../framework/defineEntity.js";
|
|
8
8
|
import { defineSubEntity, orList } from "../../framework/defineSubEntity.js";
|
|
9
9
|
import { childrenPolicy, forwardedPropName } from "./element.js";
|
|
10
|
+
import { interactionKey, interactionModifiers } from "../../framework/interaction-conditions.js";
|
|
10
11
|
import { listedIn } from "../../framework/utils/enumerated.js";
|
|
11
12
|
import { valueSchemaOf } from "../../framework/value-domain.js";
|
|
12
13
|
import { Package } from "./Package.js";
|
|
@@ -712,25 +713,38 @@ function bagFor(config, layerElement) {
|
|
|
712
713
|
bags.set(key, built);
|
|
713
714
|
return built;
|
|
714
715
|
}
|
|
716
|
+
/**
|
|
717
|
+
* A content prop's value in a rule, or `false` for none: a state that drops a Button's start icon
|
|
718
|
+
* says so with `startIcon: false`, since leaving the key out keeps whatever the rule beneath gives.
|
|
719
|
+
* Not for a required prop, whose content is the component's to have.
|
|
720
|
+
*/
|
|
721
|
+
function withNone(value, declaration) {
|
|
722
|
+
if (declaration?.required === true) return value;
|
|
723
|
+
return z.union([value, z.literal(false)]);
|
|
724
|
+
}
|
|
715
725
|
/** One element's bag, built. Reached only through the memo above. */
|
|
716
726
|
function buildBagFor(config, layerElement) {
|
|
717
727
|
const shape = { ...stylePropShape(config) };
|
|
718
728
|
const component = isRef(layerElement) && config.resolveRef(layerElement.__ref) ? pathOf(layerElement.__ref) : void 0;
|
|
719
729
|
const glyphProps = component ? glyphPropsOf(config, component) : {};
|
|
720
|
-
for (const [name, decl] of Object.entries(component ? resolveComponentProps(config, component) : {}))
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
730
|
+
for (const [name, decl] of Object.entries(component ? resolveComponentProps(config, component) : {})) {
|
|
731
|
+
const forwarded = decl?.type === "forward";
|
|
732
|
+
const value = styleBagValue({
|
|
733
|
+
config,
|
|
734
|
+
declaration: decl,
|
|
735
|
+
name,
|
|
736
|
+
glyphLibraries: forwarded ? glyphProps[name] : void 0
|
|
737
|
+
});
|
|
738
|
+
shape[name] = (forwarded ? withNone(value, decl) : value).optional();
|
|
739
|
+
}
|
|
726
740
|
const declared = component ? (config.resolve("component", component)?.toJSON())?.props ?? {} : {};
|
|
727
741
|
for (const [name, decl] of Object.entries(declared)) {
|
|
728
742
|
if (decl?.type !== "slot" || name in shape) continue;
|
|
729
|
-
shape[name] = styleBagValue({
|
|
743
|
+
shape[name] = withNone(styleBagValue({
|
|
730
744
|
config,
|
|
731
745
|
declaration: decl,
|
|
732
746
|
name
|
|
733
|
-
}).optional();
|
|
747
|
+
}), decl).optional();
|
|
734
748
|
}
|
|
735
749
|
shape.as ??= z.string().optional();
|
|
736
750
|
for (const [key, owned] of config.layerKeys()) shape[key] = owned.schema(shape[key]).optional();
|
|
@@ -808,6 +822,18 @@ function conditionValue(prop, declaration, config) {
|
|
|
808
822
|
if (!ownsItsValues(declaration)) return domain;
|
|
809
823
|
return z.union([domain, refSchema]).transform((value) => isRef(value) ? value : valueRef(prop, String(value)));
|
|
810
824
|
}
|
|
825
|
+
/** The condition on a STATE: a boolean's `true`/`false`, or one of a variant state's names. A state
|
|
826
|
+
* has no value members to ref, so a variant's condition is the name itself. */
|
|
827
|
+
function stateConditionValue(declaration) {
|
|
828
|
+
const decl = declaration;
|
|
829
|
+
if (decl?.type === "variant" && Array.isArray(decl.value)) return z.enum(decl.value);
|
|
830
|
+
return scalarConditionValue({ type: "boolean" }) ?? z.boolean();
|
|
831
|
+
}
|
|
832
|
+
/** An interaction condition's value: `true` (the root), or the layer whose interaction it is. */
|
|
833
|
+
function interactionConditionValue(layers) {
|
|
834
|
+
const truthy = z.union([z.literal(true), z.literal("true").transform(() => true)]);
|
|
835
|
+
return layers.length > 0 ? z.union([truthy, z.enum(layers)], { error: `an interaction condition is \`true\` (the component's root) or one of its layers: ${layers.join(", ")}` }) : truthy;
|
|
836
|
+
}
|
|
811
837
|
/**
|
|
812
838
|
* The condition on a boolean or number prop, in the prop's type. A preview axis spells every value
|
|
813
839
|
* as a string and a rule's key stringifies it, so a writer reading either hands back `'true'` where
|
|
@@ -1022,6 +1048,13 @@ const StyleRule = defineSubEntity({
|
|
|
1022
1048
|
build: () => {
|
|
1023
1049
|
const when = {};
|
|
1024
1050
|
for (const [name, decl] of Object.entries(body.props ?? {})) when[name] = conditionValue(name, decl, config).optional();
|
|
1051
|
+
for (const [name, decl] of Object.entries(body.states ?? {})) when[name] = stateConditionValue(decl).optional();
|
|
1052
|
+
const layerNames = Object.keys(body.layers ?? {});
|
|
1053
|
+
for (const modifier of interactionModifiers(config)) {
|
|
1054
|
+
const key = interactionKey(modifier);
|
|
1055
|
+
if (key in when) continue;
|
|
1056
|
+
when[key] = interactionConditionValue(layerNames).optional();
|
|
1057
|
+
}
|
|
1025
1058
|
const layers = {};
|
|
1026
1059
|
for (const [name, el] of Object.entries(body.layers ?? {})) layers[name] = bagFor(config, el).optional();
|
|
1027
1060
|
return z.strictObject({
|
|
@@ -1049,10 +1082,11 @@ function perOwner({ cache, config, owner, build }) {
|
|
|
1049
1082
|
cache.set(config, perConfig);
|
|
1050
1083
|
}
|
|
1051
1084
|
const hit = perConfig.get(owner);
|
|
1052
|
-
if (hit && hit.props === owner.props && hit.layers === owner.layers) return hit.schema;
|
|
1085
|
+
if (hit && hit.props === owner.props && hit.states === owner.states && hit.layers === owner.layers) return hit.schema;
|
|
1053
1086
|
const schema = build();
|
|
1054
1087
|
perConfig.set(owner, {
|
|
1055
1088
|
props: owner.props,
|
|
1089
|
+
states: owner.states,
|
|
1056
1090
|
layers: owner.layers,
|
|
1057
1091
|
schema
|
|
1058
1092
|
});
|
|
@@ -1089,6 +1123,7 @@ const Preview = defineSubEntity({
|
|
|
1089
1123
|
build: () => {
|
|
1090
1124
|
const when = {};
|
|
1091
1125
|
for (const [name, decl] of Object.entries(body.props ?? {})) when[name] = conditionValue(name, decl, config).optional();
|
|
1126
|
+
for (const [name, decl] of Object.entries(body.states ?? {})) when[name] = stateConditionValue(decl).optional();
|
|
1092
1127
|
return z.strictObject({
|
|
1093
1128
|
when: z.strictObject(when, { error: previewShapeError }).nullable().default(null),
|
|
1094
1129
|
props: z.record(z.string(), z.unknown()).transform((props) => bindPreviewGlyphs({
|
|
@@ -1347,6 +1382,83 @@ function anatomySubtreeContains(anatomy, ancestor, key) {
|
|
|
1347
1382
|
function resolveMoveBody(config, path) {
|
|
1348
1383
|
return config.resolve("component", path)?.toJSON();
|
|
1349
1384
|
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Wrap a component's root: the container becomes `root`, and what the root was — its element, its
|
|
1387
|
+
* children and slots, its props, the style rules and forwards that name it — moves onto `inner`, the
|
|
1388
|
+
* container's one child. Props the config doesn't model (`id`, an `onClick`) keep landing on `root`,
|
|
1389
|
+
* which is now the container.
|
|
1390
|
+
*/
|
|
1391
|
+
function wrapRoot({ config, path, inner, element, props }) {
|
|
1392
|
+
const body = config.resolve("component", path)?.toJSON();
|
|
1393
|
+
const root = body?.anatomy?.root;
|
|
1394
|
+
if (!body || !root) reject("NOT_FOUND", `"${path}" has no root to wrap.`);
|
|
1395
|
+
const { element: _element, ...rootRest } = root;
|
|
1396
|
+
let next = config.apply({
|
|
1397
|
+
kind: "component",
|
|
1398
|
+
operation: "sub-create",
|
|
1399
|
+
path,
|
|
1400
|
+
field: "layers",
|
|
1401
|
+
key: inner,
|
|
1402
|
+
data: body?.layers?.root
|
|
1403
|
+
}).apply({
|
|
1404
|
+
kind: "component",
|
|
1405
|
+
operation: "sub-create",
|
|
1406
|
+
path,
|
|
1407
|
+
field: "anatomy",
|
|
1408
|
+
key: inner,
|
|
1409
|
+
data: {
|
|
1410
|
+
...rootRest,
|
|
1411
|
+
element: layerRef(inner)
|
|
1412
|
+
}
|
|
1413
|
+
}).apply({
|
|
1414
|
+
kind: "component",
|
|
1415
|
+
operation: "sub-upsert",
|
|
1416
|
+
path,
|
|
1417
|
+
field: "layers",
|
|
1418
|
+
key: "root",
|
|
1419
|
+
data: asElementRef(config, element)
|
|
1420
|
+
}).apply({
|
|
1421
|
+
kind: "component",
|
|
1422
|
+
operation: "sub-upsert",
|
|
1423
|
+
path,
|
|
1424
|
+
field: "anatomy",
|
|
1425
|
+
key: "root",
|
|
1426
|
+
data: {
|
|
1427
|
+
element: layerRef("root"),
|
|
1428
|
+
children: [inner],
|
|
1429
|
+
...props ? { props } : {}
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1432
|
+
for (const [key, rule] of Object.entries(body?.styles ?? {})) {
|
|
1433
|
+
const bag = rule.layers?.root;
|
|
1434
|
+
if (bag === void 0) continue;
|
|
1435
|
+
next = next.apply({
|
|
1436
|
+
kind: "component",
|
|
1437
|
+
operation: "sub-update",
|
|
1438
|
+
path,
|
|
1439
|
+
field: "styles",
|
|
1440
|
+
key,
|
|
1441
|
+
data: {
|
|
1442
|
+
when: rule.when ?? null,
|
|
1443
|
+
[`layers.${inner}`]: bag,
|
|
1444
|
+
"layers.root": null
|
|
1445
|
+
}
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
for (const [name, decl] of Object.entries(body?.props ?? {})) {
|
|
1449
|
+
const target = decl.target;
|
|
1450
|
+
if (!(isRef(target) ? kindOf(target) === "layer" && pathOf(target) === "root" : typeof target === "string" && target.startsWith("root/"))) continue;
|
|
1451
|
+
next = next.apply({
|
|
1452
|
+
kind: "component",
|
|
1453
|
+
operation: "sub-update",
|
|
1454
|
+
path,
|
|
1455
|
+
field: "props",
|
|
1456
|
+
key: name,
|
|
1457
|
+
data: { target: isRef(target) ? layerRef(inner) : `${inner}/${String(target).slice(5)}` }
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
return next;
|
|
1461
|
+
}
|
|
1350
1462
|
const AnatomyNode = defineSubEntity({
|
|
1351
1463
|
name: "element",
|
|
1352
1464
|
label: "Element",
|
|
@@ -1538,6 +1650,18 @@ const componentFields = z.strictObject({
|
|
|
1538
1650
|
}),
|
|
1539
1651
|
props: Prop.optional(),
|
|
1540
1652
|
/**
|
|
1653
|
+
* What the RENDER knows and a style rule keys on — the field has a value, the image loaded, the
|
|
1654
|
+
* indicator is sliding. Not props: a caller never passes one, so none is part of the component's
|
|
1655
|
+
* API. The render passes them to `states(props, { … })` and spreads the layers it hands back;
|
|
1656
|
+
* the rules naming one follow the values given, and the root carries each as a `data-<name>`
|
|
1657
|
+
* attribute. Studio and tests force one through the module's internal channel, never a prop.
|
|
1658
|
+
*/
|
|
1659
|
+
states: z.record(z.string(), z.discriminatedUnion("type", [z.strictObject({ type: z.literal("boolean") }), z.strictObject({
|
|
1660
|
+
type: z.literal("variant"),
|
|
1661
|
+
value: z.array(z.string()).min(1),
|
|
1662
|
+
default: z.string().optional()
|
|
1663
|
+
})])).optional().describe("What the render knows and a style rule keys on, by name: `{ \"filled\": { \"type\": \"boolean\" } }`, or `{ \"phase\": { \"type\": \"variant\", \"value\": [\"idle\", \"sliding\"], \"default\": \"idle\" } }`. Not props, so not part of the component's API: the render passes its `props` and the values it knows to `const layers = states(props, { filled: value !== \"\" })` and spreads those layers (`{...layers.root}`) instead of `props`; a rule naming a state in `when` (`{ \"filled\": true }`, `{ \"phase\": \"sliding\" }`) is tested against the values given, and the root carries each as a `data-<name>` attribute. A value a caller controls is a prop instead, which the render reports the state from."),
|
|
1664
|
+
/**
|
|
1541
1665
|
* Which layers' element props this component also exposes — see {@link Forward}.
|
|
1542
1666
|
*
|
|
1543
1667
|
* Replaced `inherits`, which named one layer and meant `{ [layer]: '*' }`. That field and its
|
|
@@ -1598,6 +1722,34 @@ const componentFields = z.strictObject({
|
|
|
1598
1722
|
message: `\`${name}\` conditions on itself. \`when\` names a sibling prop and one of its values.`
|
|
1599
1723
|
});
|
|
1600
1724
|
}
|
|
1725
|
+
if (isPlainObject(body.states) && Object.keys(body.states).length > 0) {
|
|
1726
|
+
const props = isPlainObject(body.props) ? body.props : {};
|
|
1727
|
+
if (Object.hasOwn(props, "states")) ctx.issues.push({
|
|
1728
|
+
code: "custom",
|
|
1729
|
+
input: props.states,
|
|
1730
|
+
path: ["props", "states"],
|
|
1731
|
+
message: "A component with states hands its render a `states` helper, so no prop of its own can be named `states`."
|
|
1732
|
+
});
|
|
1733
|
+
for (const [name, decl] of Object.entries(body.states)) {
|
|
1734
|
+
if (Object.hasOwn(props, name)) ctx.issues.push({
|
|
1735
|
+
code: "custom",
|
|
1736
|
+
input: decl,
|
|
1737
|
+
path: ["states", name],
|
|
1738
|
+
message: `\`${name}\` is both a prop and a state. A state is what the render knows, never a value a caller passes; a caller-controlled value is a prop the render reports the state from.`
|
|
1739
|
+
});
|
|
1740
|
+
const variant = decl;
|
|
1741
|
+
if (variant?.type === "variant" && variant.default !== void 0 && Array.isArray(variant.value) && !variant.value.includes(variant.default)) ctx.issues.push({
|
|
1742
|
+
code: "custom",
|
|
1743
|
+
input: variant.default,
|
|
1744
|
+
path: [
|
|
1745
|
+
"states",
|
|
1746
|
+
name,
|
|
1747
|
+
"default"
|
|
1748
|
+
],
|
|
1749
|
+
message: `\`${name}\`'s default \`${String(variant.default)}\` is not one of its values (${variant.value.join(", ")}).`
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1601
1753
|
if (body.forwards || body.file || isRef(body.props)) return;
|
|
1602
1754
|
const declared = body.props ?? {};
|
|
1603
1755
|
if (Object.hasOwn(declared, "children") && declared.children !== null) return;
|
|
@@ -2328,6 +2480,69 @@ subtree: z.boolean().default(true) }).optional()
|
|
|
2328
2480
|
});
|
|
2329
2481
|
}
|
|
2330
2482
|
},
|
|
2483
|
+
"element-wrap": {
|
|
2484
|
+
input: z.object({
|
|
2485
|
+
path: pathInput("component"),
|
|
2486
|
+
/** The new layer's name: the wrapper's, or — wrapping the root — the old root's new name. The
|
|
2487
|
+
* entity mints a free one when the caller doesn't care. */
|
|
2488
|
+
key: z.string().optional(),
|
|
2489
|
+
data: z.object({
|
|
2490
|
+
/** The layers to wrap: siblings under one parent, wrapped in their order, or `['root']`. */
|
|
2491
|
+
elements: z.array(z.string()).min(1),
|
|
2492
|
+
/** What the wrapper renders — a frame component or a tag. */
|
|
2493
|
+
element: z.union([z.string(), refSchema]).default("div"),
|
|
2494
|
+
/** The wrapper's own props. */
|
|
2495
|
+
props: z.record(z.string(), z.unknown()).optional()
|
|
2496
|
+
})
|
|
2497
|
+
}),
|
|
2498
|
+
readOnly: false,
|
|
2499
|
+
label: "Wrap elements",
|
|
2500
|
+
description: "Wrap a component's sibling layers in a new container. Wrapping `root` makes the container the root and moves the old root — its element, children, slots, styles and forwards — onto a new layer inside it.",
|
|
2501
|
+
scope: "item",
|
|
2502
|
+
creates: true,
|
|
2503
|
+
title: (entity) => `Wrap ${entity} elements`,
|
|
2504
|
+
handler: (input, config) => {
|
|
2505
|
+
const payload = input.data;
|
|
2506
|
+
const path = String(input.path);
|
|
2507
|
+
const keys = payload.elements;
|
|
2508
|
+
const anatomy = resolveMoveBody(config, path)?.anatomy ?? {};
|
|
2509
|
+
for (const k of keys) if (!anatomy[k]) reject("NOT_FOUND", `"${path}" has no element "${k}" to wrap.`);
|
|
2510
|
+
if (keys.includes("root")) {
|
|
2511
|
+
if (keys.length > 1) rejectData("The root wraps on its own: it has no siblings to wrap with it.", ["elements"]);
|
|
2512
|
+
return wrapRoot({
|
|
2513
|
+
config,
|
|
2514
|
+
path,
|
|
2515
|
+
inner: input.key ? String(input.key) : freeLayerKey(config, path, "content"),
|
|
2516
|
+
element: payload.element,
|
|
2517
|
+
props: payload.props
|
|
2518
|
+
});
|
|
2519
|
+
}
|
|
2520
|
+
const parent = Object.entries(anatomy).find(([, node]) => keys.every((k) => node.children?.includes(k)));
|
|
2521
|
+
if (!parent) rejectData("Only layers that share a parent wrap together — wrap each parent's layers on their own.", ["elements"]);
|
|
2522
|
+
const [parentKey, parentNode] = parent;
|
|
2523
|
+
const order = (parentNode.children ?? []).filter((c) => keys.includes(c));
|
|
2524
|
+
const index = (parentNode.children ?? []).indexOf(order[0]);
|
|
2525
|
+
const wrapper = input.key ? String(input.key) : freeLayerKey(config, path, "wrap");
|
|
2526
|
+
return config.run([{
|
|
2527
|
+
kind: "component",
|
|
2528
|
+
operation: "element-insert",
|
|
2529
|
+
path,
|
|
2530
|
+
key: wrapper,
|
|
2531
|
+
data: {
|
|
2532
|
+
element: payload.element,
|
|
2533
|
+
parent: parentKey,
|
|
2534
|
+
index,
|
|
2535
|
+
...payload.props ? { props: payload.props } : {}
|
|
2536
|
+
}
|
|
2537
|
+
}, ...order.map((k) => ({
|
|
2538
|
+
kind: "component",
|
|
2539
|
+
operation: "element-move",
|
|
2540
|
+
path,
|
|
2541
|
+
key: k,
|
|
2542
|
+
data: { parent: wrapper }
|
|
2543
|
+
}))]);
|
|
2544
|
+
}
|
|
2545
|
+
},
|
|
2331
2546
|
validate: {
|
|
2332
2547
|
input: z.object({
|
|
2333
2548
|
path: pathInput("component"),
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { splitRef } from "../../framework/utils/refs.js";
|
|
2
2
|
import { defineEntity } from "../../framework/defineEntity.js";
|
|
3
3
|
import { defineSubEntity } from "../../framework/defineSubEntity.js";
|
|
4
|
+
import { createSliceMemo } from "../../framework/memo.js";
|
|
4
5
|
import { buildFields } from "../../configs/build-options.js";
|
|
5
6
|
import { parseRootFontSize } from "../../css/root-font-size.js";
|
|
6
|
-
import { createSliceMemo } from "../../framework/memo.js";
|
|
7
7
|
import { resolveInputDir } from "../../framework/registry-dir.js";
|
|
8
8
|
import { safelistSchema } from "./safelist.js";
|
|
9
9
|
import { z } from "zod";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { declarationText, isRef, kindOf, pathOf, ref, refOfKey, refSchema, replaceRefMarkers, sourceOf, splitRef } from "../../framework/utils/refs.js";
|
|
2
2
|
import { defineEntity } from "../../framework/defineEntity.js";
|
|
3
|
+
import { createSliceMemo } from "../../framework/memo.js";
|
|
3
4
|
import { cssValueIssue, isCssProperty } from "../../css/values.js";
|
|
4
5
|
import { camelCase } from "./style-bag.js";
|
|
5
|
-
import { createSliceMemo } from "../../framework/memo.js";
|
|
6
6
|
import { isIdentifier } from "../../framework/utils/identifier.js";
|
|
7
7
|
import { cssVar, declaresCssVariable } from "../../framework/projections.js";
|
|
8
8
|
import { z } from "zod";
|
|
@@ -26,6 +26,22 @@ interface ComponentModuleSource {
|
|
|
26
26
|
readonly version: string;
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
+
/** The directory, under the system's input root, a component made in Create is built from. No file
|
|
30
|
+
* is ever written there: the server writes the source `uds pull` would, and builds it in place. */
|
|
31
|
+
declare const CREATED_COMPONENT_DIRECTORY = "__created__";
|
|
32
|
+
/** The virtual source path a component made in Create builds from. */
|
|
33
|
+
declare function createdComponentFilePath(config: Config, path: string): string;
|
|
34
|
+
/** The component a virtual created path names, when it names one this config made in Create. */
|
|
35
|
+
declare function createdComponentPathOf(config: Config, filePath: string): string | undefined;
|
|
36
|
+
/** Whether a component was made in Create: a web component no file holds, whose structure is its
|
|
37
|
+
* anatomy. */
|
|
38
|
+
declare function isCreatedComponent(config: Config, path: string): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* A component made in Create's module key: the runtime every module embeds, the component's own
|
|
41
|
+
* build body, and the bodies of the components its layers place — whose props decide how it routes
|
|
42
|
+
* and emits. Computed from the config alone, so the browser and the server spell it the same.
|
|
43
|
+
*/
|
|
44
|
+
declare function createdModuleKey(config: Config, path: string): string | undefined;
|
|
29
45
|
/**
|
|
30
46
|
* The static an emitted module stamps a component's registry-entry facts on: what
|
|
31
47
|
* `makeComponentEntry` takes beyond the component (`childless`, `computed`). A static and not an
|
|
@@ -110,4 +126,4 @@ declare function linkedComponentModule(config: Config, componentPathOrRef: strin
|
|
|
110
126
|
readonly view: Config;
|
|
111
127
|
} | undefined;
|
|
112
128
|
//#endregion
|
|
113
|
-
export { COMPONENT_ENTRY_STATIC, ComponentModuleSource, ComponentRegistryImport, ComponentRegistryImportBinding, componentModuleFilePaths, componentModuleKey, componentModuleSource, componentRegistryImports, linkedComponentModule, linkedModuleView };
|
|
129
|
+
export { COMPONENT_ENTRY_STATIC, CREATED_COMPONENT_DIRECTORY, ComponentModuleSource, ComponentRegistryImport, ComponentRegistryImportBinding, componentModuleFilePaths, componentModuleKey, componentModuleSource, componentRegistryImports, createdComponentFilePath, createdComponentPathOf, createdModuleKey, isCreatedComponent, linkedComponentModule, linkedModuleView };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ref, routedSource, sourceHops, splitRef } from "../../framework/utils/refs.js";
|
|
1
|
+
import { isRef, kindOf, pathOf, ref, routedSource, sourceHops, splitRef } from "../../framework/utils/refs.js";
|
|
2
2
|
import { createSliceMemo } from "../../framework/memo.js";
|
|
3
3
|
import { Settings, classPrefixOf, prefixed, varPrefixOf } from "./Settings.js";
|
|
4
4
|
import { File } from "./File.js";
|
|
@@ -28,6 +28,58 @@ const WEB_RUNTIME_KINDS = [
|
|
|
28
28
|
"motion",
|
|
29
29
|
"composite"
|
|
30
30
|
];
|
|
31
|
+
/** The directory, under the system's input root, a component made in Create is built from. No file
|
|
32
|
+
* is ever written there: the server writes the source `uds pull` would, and builds it in place. */
|
|
33
|
+
const CREATED_COMPONENT_DIRECTORY = "__created__";
|
|
34
|
+
/** The virtual source path a component made in Create builds from. */
|
|
35
|
+
function createdComponentFilePath(config, path) {
|
|
36
|
+
return `${createdComponentDirectory(config)}${path}.tsx`;
|
|
37
|
+
}
|
|
38
|
+
/** The directory, with its trailing slash, a config's created paths start with. An empty input
|
|
39
|
+
* root puts it at the top, since a sealed path never starts with a slash. */
|
|
40
|
+
function createdComponentDirectory(config) {
|
|
41
|
+
const root = (Settings.get(config)?.inputDir ?? "src").replace(/\/+$/, "");
|
|
42
|
+
return root ? `${root}/${CREATED_COMPONENT_DIRECTORY}/` : `${CREATED_COMPONENT_DIRECTORY}/`;
|
|
43
|
+
}
|
|
44
|
+
/** The component a virtual created path names, when it names one this config made in Create. */
|
|
45
|
+
function createdComponentPathOf(config, filePath) {
|
|
46
|
+
const directory = createdComponentDirectory(config);
|
|
47
|
+
if (!filePath.startsWith(directory) || !filePath.endsWith(".tsx")) return;
|
|
48
|
+
const path = filePath.slice(directory.length, -4);
|
|
49
|
+
return isCreatedComponent(config, path) ? path : void 0;
|
|
50
|
+
}
|
|
51
|
+
/** Whether a component was made in Create: a web component no file holds, whose structure is its
|
|
52
|
+
* anatomy. */
|
|
53
|
+
function isCreatedComponent(config, path) {
|
|
54
|
+
if (isNativeConfig(config)) return false;
|
|
55
|
+
const component = Component.get(config, path);
|
|
56
|
+
if (!component || component.source !== void 0) return false;
|
|
57
|
+
const { file, export: exportName, anatomy } = component.body;
|
|
58
|
+
return !file && !exportName && Object.keys(anatomy ?? {}).length > 0;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A component made in Create's module key: the runtime every module embeds, the component's own
|
|
62
|
+
* build body, and the bodies of the components its layers place — whose props decide how it routes
|
|
63
|
+
* and emits. Computed from the config alone, so the browser and the server spell it the same.
|
|
64
|
+
*/
|
|
65
|
+
function createdModuleKey(config, path) {
|
|
66
|
+
const component = Component.get(config, path);
|
|
67
|
+
if (!component) return void 0;
|
|
68
|
+
const body = component.toJSON();
|
|
69
|
+
const placed = {};
|
|
70
|
+
for (const element of Object.values(body.layers ?? {})) {
|
|
71
|
+
if (!isRef(element) || kindOf(element) !== "component") continue;
|
|
72
|
+
const target = Component.get(config, pathOf(element.__ref));
|
|
73
|
+
if (target) placed[target.path] = buildEntityBody("component", target.toJSON());
|
|
74
|
+
}
|
|
75
|
+
return contentKey({
|
|
76
|
+
version: COMPONENT_MODULE_BUILD_VERSION,
|
|
77
|
+
runtimeKey: componentRuntimeKey(config),
|
|
78
|
+
created: path,
|
|
79
|
+
body: buildEntityBody("component", body),
|
|
80
|
+
placed
|
|
81
|
+
});
|
|
82
|
+
}
|
|
31
83
|
/**
|
|
32
84
|
* The static an emitted module stamps a component's registry-entry facts on: what
|
|
33
85
|
* `makeComponentEntry` takes beyond the component (`childless`, `computed`). A static and not an
|
|
@@ -393,4 +445,4 @@ function publishedPin(config, slug) {
|
|
|
393
445
|
}
|
|
394
446
|
}
|
|
395
447
|
//#endregion
|
|
396
|
-
export { COMPONENT_ENTRY_STATIC, componentModuleFilePaths, componentModuleKey, componentModuleSource, componentRegistryImports, linkedComponentModule, linkedModuleView };
|
|
448
|
+
export { COMPONENT_ENTRY_STATIC, CREATED_COMPONENT_DIRECTORY, componentModuleFilePaths, componentModuleKey, componentModuleSource, componentRegistryImports, createdComponentFilePath, createdComponentPathOf, createdModuleKey, isCreatedComponent, linkedComponentModule, linkedModuleView };
|