@yahoo/uds-create-config 5.5.0 → 5.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,7 @@ import { Composite } from "../entities/system/Composite.js";
4
4
  import { Modifier } from "../entities/system/Modifier.js";
5
5
  import { Settings } from "../entities/system/Settings.js";
6
6
  import { StyleProperty } from "../entities/system/StyleProperty.js";
7
+ import { Motion } from "../entities/system/Motion.js";
7
8
  import { File } from "../entities/system/File.js";
8
9
  import { WEB_CONFIG_NAME } from "./platform.js";
9
10
  import { Token } from "../entities/system/Token.js";
@@ -19,7 +20,6 @@ import { GlobalStyle } from "../entities/system/GlobalStyle.js";
19
20
  import { Guidance } from "../entities/system/Guidance.js";
20
21
  import { GuidanceStyle } from "../entities/system/GuidanceStyle.js";
21
22
  import { LinkedSystem } from "../entities/system/LinkedSystem.js";
22
- import { Motion } from "../entities/system/Motion.js";
23
23
  import { loadV1Artifact } from "../migrations/2.0.0/v1-artifact.js";
24
24
  import { settingsAsKindMigration } from "../migrations/20260908171448_settings_as_kind.js";
25
25
  import { visibleStatePointerMigration } from "../migrations/20260908194245_visible_state_pointer.js";
@@ -57,6 +57,7 @@ import { oklchAndEmitFieldsMigration } from "../migrations/20261001162046_oklch_
57
57
  import { globalStyleOnMigration } from "../migrations/20261005212405_global_style_on.js";
58
58
  import { addAndSubtractMigration } from "../migrations/20261005220312_add_and_subtract.js";
59
59
  import { systemIncludeMigration } from "../migrations/20261005225949_system_include.js";
60
+ import { motionVariantsAndLayoutMigration } from "../migrations/20261006125609_motion_variants_and_layout.js";
60
61
  const System = defineConfig({
61
62
  name: WEB_CONFIG_NAME,
62
63
  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.",
@@ -125,5 +126,6 @@ if (!registeredSchemaMigrations().has(20261001162046)) registerSchemaMigrations(
125
126
  if (!registeredSchemaMigrations().has(20261005212405)) registerSchemaMigrations(globalStyleOnMigration);
126
127
  if (!registeredSchemaMigrations().has(20261005220312)) registerSchemaMigrations(addAndSubtractMigration);
127
128
  if (!registeredSchemaMigrations().has(20261005225949)) registerSchemaMigrations(systemIncludeMigration);
129
+ if (!registeredSchemaMigrations().has(20261006125609)) registerSchemaMigrations(motionVariantsAndLayoutMigration);
128
130
  //#endregion
129
131
  export { System };
@@ -874,8 +874,11 @@ type ComponentReference<Contract = ComponentContractOf<string>, Path extends str
874
874
  type ExtensionDefaults<Contract, Delta> = {
875
875
  readonly defaultProps?: DefaultsOf<ResolvedProps<Contract, Delta>>;
876
876
  };
877
- /** Style rules as a list, the form `defineComponent.styles` takes, in the stored form: keyed by each
878
- * rule's condition. Two rules with the same condition are one rule, the later one. */
877
+ /**
878
+ * Two style rules with the same condition, as the one rule they are: each layer's bag merged, the
879
+ * later rule's keys winning ({@link mergeRuleBags}). The list form of `styles` keys rules by their
880
+ * condition, so a second `{ when: { disabled: true } }` would otherwise replace the first whole.
881
+ */
879
882
  /** A component body as `register` takes one. */
880
883
  type ComponentCreateInput = CreateInput<typeof componentFields>;
881
884
  /** `Component.item`'s body: the stored fields, with `styles` also taking the rule list
@@ -15,6 +15,7 @@ import { Style, addBagKeyIssues, anatomyPropValue, bagNameEdges, bagRewriteName,
15
15
  import { storedPreview } from "../../framework/preview-store.js";
16
16
  import { glyphPropsOf, propOwner, propValueDomain, refListOf, resolveComponentProps, stylePropValueLeaves, validateComponentProps } from "../../framework/projections.js";
17
17
  import { carriesContent, intoName, isValueSlot, renderBoundProp, routedPropIn, routesContent } from "../../framework/prop-surface.js";
18
+ import { inlineJsMotion } from "./Motion.js";
18
19
  import { slotAnatomyPropOf, slotTargetsOf, withoutTruthyTerm } from "../../framework/render-spec.js";
19
20
  import { listSlots, namedSlots, slotHostedBy, slotOf, slotsFor } from "../../spec/slot.js";
20
21
  import { cellAt } from "../../spec/cell.js";
@@ -730,6 +731,7 @@ function buildBagFor(config, layerElement) {
730
731
  }).optional();
731
732
  }
732
733
  shape.as ??= z.string().optional();
734
+ shape.motion = z.union([shape.motion ?? z.union([refSchema, z.string()]), inlineJsMotion], { error: "a layer's `motion` is a preset (`Motion.ref('<path>')`) or a JS preset written in place — `{ \"whileHover\": { \"scale\": 1.05 }, \"transition\": { … } }`, the fields `Motion.item` takes with `runtime: 'js'`. A CSS preset is named with `Motion.item`." }).optional();
733
735
  let bag;
734
736
  const nested = z.lazy(() => bag).optional();
735
737
  for (const mod of config.list("modifier")) shape[`_${mod.leaf}`] = nested;
@@ -1541,7 +1543,7 @@ const componentFields = z.strictObject({
1541
1543
  * forwards nothing rather than being quietly translated.
1542
1544
  */
1543
1545
  forwards: Forward.optional(),
1544
- styles: orList(StyleRule).optional(),
1546
+ styles: orList(StyleRule, { merge: mergeStyleRules }).optional(),
1545
1547
  /** The props the render reads that the component neither declares nor forwards — its render
1546
1548
  * extras (`.render<{ swatchColor?: string }>()`). Read off the render at build like `anatomy`,
1547
1549
  * never authored as data. The module consumes them; a component painted out of its anatomy has
@@ -2344,14 +2346,52 @@ subtree: z.boolean().default(true) }).optional()
2344
2346
  ]
2345
2347
  });
2346
2348
  const baseComponentRef = ComponentEntity.ref.bind(ComponentEntity);
2349
+ /**
2350
+ * Two layer bags of rules with one condition, as one: the later bag's keys win, key by key, and a
2351
+ * modifier block (`_hover`) is merged the same way inside. Rules apply in order and a later one wins
2352
+ * on the property it sets, so this is what the two said; replacing the earlier bag dropped every key
2353
+ * only it set.
2354
+ */
2355
+ function mergeRuleBags(earlier, later) {
2356
+ const out = { ...earlier };
2357
+ for (const [key, value] of Object.entries(later)) {
2358
+ const before = out[key];
2359
+ out[key] = key.startsWith("_") && typeof before === "object" && before !== null && !Array.isArray(before) && typeof value === "object" && value !== null && !Array.isArray(value) ? mergeRuleBags(before, value) : value;
2360
+ }
2361
+ return out;
2362
+ }
2363
+ /**
2364
+ * Two style rules with the same condition, as the one rule they are: each layer's bag merged, the
2365
+ * later rule's keys winning ({@link mergeRuleBags}). The list form of `styles` keys rules by their
2366
+ * condition, so a second `{ when: { disabled: true } }` would otherwise replace the first whole.
2367
+ */
2368
+ function mergeStyleRules(earlier, later) {
2369
+ const layers = { ...earlier.layers ?? {} };
2370
+ for (const [layer, bag] of Object.entries(later.layers ?? {})) {
2371
+ const before = layers[layer];
2372
+ layers[layer] = typeof before === "object" && before !== null ? mergeRuleBags(before, bag ?? {}) : bag;
2373
+ }
2374
+ return {
2375
+ ...earlier,
2376
+ ...later,
2377
+ layers
2378
+ };
2379
+ }
2347
2380
  /** Style rules as a list, the form `defineComponent.styles` takes, in the stored form: keyed by each
2348
- * rule's condition. Two rules with the same condition are one rule, the later one. */
2381
+ * rule's condition. Two rules with the same condition are one rule ({@link mergeStyleRules}), in the
2382
+ * later one's place. */
2349
2383
  function storedStyleRules(rules) {
2350
2384
  const styles = {};
2351
- for (const rule of rules) styles[canonicalWhen(rule.when)] = {
2352
- when: rule.when ?? null,
2353
- layers: rule.layers
2354
- };
2385
+ for (const rule of rules) {
2386
+ const key = canonicalWhen(rule.when);
2387
+ const stored = {
2388
+ when: rule.when ?? null,
2389
+ layers: rule.layers
2390
+ };
2391
+ const earlier = styles[key];
2392
+ delete styles[key];
2393
+ styles[key] = earlier ? mergeStyleRules(earlier, stored) : stored;
2394
+ }
2355
2395
  return styles;
2356
2396
  }
2357
2397
  function componentItem(def) {
@@ -3,6 +3,28 @@ import { Named } from "../../framework/named.js";
3
3
  import { z } from "zod";
4
4
 
5
5
  //#region src/entities/system/Motion.d.ts
6
+ /**
7
+ * A JS preset written in place, as a style rule's `motion` on a layer rather than a `Motion.ref`: the
8
+ * same fields, with `runtime` optional since in place it can only be `js`. A CSS preset is a class
9
+ * the stylesheet emits under the preset's name, so one written in place has nothing to emit as; name
10
+ * it with `Motion.item` instead.
11
+ */
12
+ declare const inlineJsMotion: z.ZodObject<{
13
+ transition: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
14
+ exit: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodString, z.ZodArray<z.ZodString>]>>;
15
+ layout: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"position">, z.ZodLiteral<"size">, z.ZodLiteral<"preserve-aspect">]>>;
16
+ initial: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodString, z.ZodArray<z.ZodString>, z.ZodLiteral<false>]>>;
17
+ variants: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
18
+ animate: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodString, z.ZodArray<z.ZodString>]>>;
19
+ whileHover: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodString, z.ZodArray<z.ZodString>]>>;
20
+ whileTap: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodString, z.ZodArray<z.ZodString>]>>;
21
+ whileFocus: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodString, z.ZodArray<z.ZodString>]>>;
22
+ whileDrag: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodString, z.ZodArray<z.ZodString>]>>;
23
+ drag: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodString]>>;
24
+ dragConstraints: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
25
+ dragElastic: z.ZodOptional<z.ZodNumber>;
26
+ runtime: z.ZodOptional<z.ZodLiteral<"js">>;
27
+ }, z.core.$strict>;
6
28
  /** The fields of a JS preset that `motion.create` reads as props, in schema order. The emitter
7
29
  * reads this rather than restating the schema, so a field added here reaches every emitted preset. */
8
30
  declare const jsMotionPropFields: readonly string[];
@@ -17,19 +39,29 @@ declare const MotionDefinition: EntityClass<z.ZodDiscriminatedUnion<[z.ZodObject
17
39
  fillMode: z.ZodOptional<z.ZodString>;
18
40
  }, z.core.$strict>, z.ZodObject<{
19
41
  runtime: z.ZodLiteral<"js">;
20
- initial: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
21
- animate: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
22
- exit: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
42
+ initial: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodString, z.ZodArray<z.ZodString>, z.ZodLiteral<false>]>>;
43
+ animate: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodString, z.ZodArray<z.ZodString>]>>;
44
+ exit: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodString, z.ZodArray<z.ZodString>]>>;
23
45
  transition: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
24
- whileHover: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
25
- whileTap: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
26
- whileFocus: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
27
- whileDrag: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
46
+ whileHover: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodString, z.ZodArray<z.ZodString>]>>;
47
+ whileTap: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodString, z.ZodArray<z.ZodString>]>>;
48
+ whileFocus: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodString, z.ZodArray<z.ZodString>]>>;
49
+ whileDrag: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodString, z.ZodArray<z.ZodString>]>>;
50
+ variants: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
51
+ layout: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"position">, z.ZodLiteral<"size">, z.ZodLiteral<"preserve-aspect">]>>;
28
52
  drag: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodString]>>;
29
53
  dragConstraints: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
30
54
  dragElastic: z.ZodOptional<z.ZodNumber>;
31
- }, z.core.$strict>], "runtime">, z.ZodObject<Record<never, never>, z.core.$strip>, "motion", {}, Record<never, never>, false, Record<never, never>, Record<never, never>>;
55
+ }, z.core.$strict>], "runtime">, z.ZodObject<Record<never, never>, z.core.$strip>, "motion", {
56
+ readonly 'rename-variant': {
57
+ readonly input: z.ZodObject<{
58
+ path: z.ZodType<string, string | undefined, z.core.$ZodTypeInternals<string, string | undefined>>;
59
+ key: z.ZodString;
60
+ to: z.ZodString;
61
+ }, z.core.$strip>;
62
+ };
63
+ }, Record<never, never>, false, Record<never, never>, Record<never, never>>;
32
64
  interface MotionClass extends Named<typeof MotionDefinition> {}
33
65
  declare const Motion: MotionClass;
34
66
  //#endregion
35
- export { Motion, MotionClass, jsMotionPropFields };
67
+ export { Motion, MotionClass, inlineJsMotion, jsMotionPropFields };
@@ -1,4 +1,6 @@
1
+ import { pathInput } from "../../framework/schemas.js";
1
2
  import { defineEntity } from "../../framework/defineEntity.js";
3
+ import { renameMotionVariant } from "./motion-variants.js";
2
4
  import { z } from "zod";
3
5
  //#region src/entities/system/Motion.ts
4
6
  /**
@@ -7,7 +9,9 @@ import { z } from "zod";
7
9
  * - `css` → `keyframes` REQUIRED (codegen emits `@keyframes` from them); a css preset with
8
10
  * no keyframes — which used to validate and silently emit nothing — is now rejected.
9
11
  * - `js` → no keyframes; carries the interaction state the client drives, `exit` included,
10
- * which runs under an `AnimatePresence` ancestor when the element unmounts.
12
+ * which runs under an `AnimatePresence` ancestor when the element unmounts. Each state is a set
13
+ * of values or the name of one of the preset's `variants`, and `layout` animates the element's
14
+ * own size and position as they change.
11
15
  * Illegal cross-runtime mixes (a css preset with `animate`, a js preset with `keyframes`)
12
16
  * can't be represented — each member is strict.
13
17
  *
@@ -15,7 +19,17 @@ import { z } from "zod";
15
19
  * it silently: the first version declared three js fields, so every gesture preset in a real system
16
20
  * lost `whileHover`/`whileTap`/`drag` on the way in and animated nothing.
17
21
  */
18
- const gesture = z.record(z.string(), z.unknown()).optional();
22
+ const values = z.record(z.string(), z.unknown());
23
+ /**
24
+ * A state: the values themselves, or the name of a variant — or several, applied in order — which
25
+ * `motion/react` looks up in the preset's `variants` and, for a child that declares none of its own
26
+ * gestures, in the variants its parent is animating to.
27
+ */
28
+ const gesture = z.union([
29
+ values,
30
+ z.string(),
31
+ z.array(z.string())
32
+ ]).optional();
19
33
  const RUNTIME = "Which runtime plays the preset, and so which fields apply.";
20
34
  const CssMotion = z.strictObject({
21
35
  runtime: z.literal("css").describe(RUNTIME),
@@ -29,18 +43,37 @@ const CssMotion = z.strictObject({
29
43
  });
30
44
  const JsMotion = z.strictObject({
31
45
  runtime: z.literal("js").describe(RUNTIME),
32
- initial: gesture.describe("The starting values, as `motion/react` reads them."),
46
+ initial: z.union([
47
+ values,
48
+ z.string(),
49
+ z.array(z.string()),
50
+ z.literal(false)
51
+ ]).optional().describe("The starting values, as `motion/react` reads them, or `false` to start at `animate` without playing on mount."),
33
52
  animate: gesture.describe("The values to animate to."),
34
53
  exit: gesture.describe("The values to animate to as the element leaves."),
35
- transition: gesture.describe("How the animation moves, such as a spring."),
54
+ transition: values.optional().describe("How the animation moves, such as a spring."),
36
55
  whileHover: gesture.describe("Values held while the element is hovered."),
37
56
  whileTap: gesture.describe("Values held while the element is pressed."),
38
57
  whileFocus: gesture.describe("Values held while the element has focus."),
39
58
  whileDrag: gesture.describe("Values held while the element is dragged."),
59
+ variants: z.record(z.string(), values).optional().describe("Named states, each a set of values, that `initial`, `animate`, `exit` and the gestures name."),
60
+ layout: z.union([
61
+ z.boolean(),
62
+ z.literal("position"),
63
+ z.literal("size"),
64
+ z.literal("preserve-aspect")
65
+ ]).optional().describe("Animates the element to its new size and position when they change, such as a height that follows its content."),
40
66
  drag: z.union([z.boolean(), z.string()]).optional().describe("Whether, and along which axis, the element can be dragged."),
41
67
  dragConstraints: z.record(z.string(), z.number()).optional().describe("The box a drag stays within."),
42
68
  dragElastic: z.number().optional().describe("How far past its constraints a drag can stretch.")
43
69
  });
70
+ /**
71
+ * A JS preset written in place, as a style rule's `motion` on a layer rather than a `Motion.ref`: the
72
+ * same fields, with `runtime` optional since in place it can only be `js`. A CSS preset is a class
73
+ * the stylesheet emits under the preset's name, so one written in place has nothing to emit as; name
74
+ * it with `Motion.item` instead.
75
+ */
76
+ const inlineJsMotion = JsMotion.omit({ runtime: true }).extend({ runtime: z.literal("js").optional().describe(RUNTIME) }).describe("A JS motion preset written in place: `initial`, `animate`, `while*`, `variants`, `transition` and the rest, as `Motion.item` takes them with `runtime: 'js'`.");
44
77
  /** The fields of a JS preset that `motion.create` reads as props, in schema order. The emitter
45
78
  * reads this rather than restating the schema, so a field added here reaches every emitted preset. */
46
79
  const jsMotionPropFields = Object.keys(JsMotion.shape).filter((field) => field !== "runtime");
@@ -48,7 +81,41 @@ const Motion = defineEntity({
48
81
  kind: "motion",
49
82
  label: "Motion",
50
83
  labelPlural: "Motion presets",
51
- fields: z.discriminatedUnion("runtime", [CssMotion, JsMotion])
84
+ fields: z.discriminatedUnion("runtime", [CssMotion, JsMotion]),
85
+ schemas: {
86
+ /**
87
+ * A variant name is a string the presets that animate together match by, not a ref, so nothing
88
+ * cascades it. Renaming one in a single preset's `variants` leaves its own states and every inner
89
+ * layer that answers it naming the old one, which animates to nothing and says nothing.
90
+ */
91
+ "rename-variant": {
92
+ input: z.object({
93
+ path: pathInput("motion"),
94
+ key: z.string().min(1),
95
+ to: z.string().min(1)
96
+ }),
97
+ readOnly: false,
98
+ label: "Rename variant",
99
+ description: "Rename one of a JS preset's `variants` (`key` → `to`) wherever the name is used: the preset's own `variants` and states (`initial`, `animate`, `exit`, `while*`), and every preset that animates with it, since a root's gestures name the variants its inner layers' presets declare. \"Animates with\" is the presets a component's style rules put on its layers, joined across a component that renders another. Refused when a preset in that set already declares `to`.",
100
+ scope: "item",
101
+ title: (entity) => `Rename ${entity} variant`,
102
+ example: ({ config }) => {
103
+ for (const preset of config.list("motion")) {
104
+ const variants = preset.toJSON().variants;
105
+ const [key] = Object.keys(variants ?? {});
106
+ if (key !== void 0) return {
107
+ path: preset.path,
108
+ key,
109
+ to: `${key}-renamed`
110
+ };
111
+ }
112
+ },
113
+ handler: (input, config) => renameMotionVariant(config, {
114
+ path: String(input.path),
115
+ from: String(input.key),
116
+ to: String(input.to)
117
+ })
118
+ } }
52
119
  });
53
120
  //#endregion
54
- export { Motion, jsMotionPropFields };
121
+ export { Motion, inlineJsMotion, jsMotionPropFields };
@@ -4,9 +4,9 @@ import { defineEntity } from "../../framework/defineEntity.js";
4
4
  import { defineSubEntity } from "../../framework/defineSubEntity.js";
5
5
  import { Font } from "./Font.js";
6
6
  import { Modifier } from "./Modifier.js";
7
+ import { spellTokenRead } from "./token-values.js";
7
8
  import { WEB_CONFIG_NAME } from "../../configs/platform.js";
8
9
  import { authoringPhantom } from "../../framework/utils/authoring-phantom.js";
9
- import { spellTokenRead } from "./token-values.js";
10
10
  import { prepareLinkedTokenBody } from "./linked-token-values.js";
11
11
  import { z } from "zod";
12
12
  //#region src/entities/system/Token.ts
@@ -1,7 +1,7 @@
1
1
  import { canonicalCondition, isRef, kindOf, memberRef, pathOf } from "../../framework/utils/refs.js";
2
2
  import { glyphIdentityOf, glyphPackageRefOf, isComponentType } from "./glyph-identity.js";
3
3
  import "../../framework/preview-store.js";
4
- import { Component, canonicalWhen, layerRef } from "./Component.js";
4
+ import { Component, layerRef, storedStyleRules } from "./Component.js";
5
5
  //#region src/entities/system/defineComponent.ts
6
6
  function build(body) {
7
7
  const handle = (() => null);
@@ -364,14 +364,9 @@ function toStored(body) {
364
364
  if (body.forwards && typeof body.forwards === "object") stored.forwards = storedForwards(body.forwards, body.layers);
365
365
  const rules = body.styles;
366
366
  if (!Array.isArray(rules)) return stored;
367
- const styles = {};
368
- for (const rule of rules) styles[canonicalWhen(rule.when)] = {
369
- when: rule.when ?? null,
370
- layers: rule.layers
371
- };
372
367
  return {
373
368
  ...stored,
374
- styles
369
+ styles: storedStyleRules(rules)
375
370
  };
376
371
  }
377
372
  /**
@@ -2,8 +2,8 @@ import { hasRefMarker } from "../../framework/utils/refs.js";
2
2
  import { isCalc } from "./calc.js";
3
3
  import { isOklchColor } from "./color.js";
4
4
  import { literalValueOf } from "../../framework/projections.js";
5
- import "../../configs/platform.js";
6
5
  import { rootFontSize } from "./token-values.js";
6
+ import "../../configs/platform.js";
7
7
  //#region src/entities/system/linked-token-values.ts
8
8
  /**
9
9
  * Adapt a linked token for the system reading it. The source keeps its authored value; only the
@@ -0,0 +1,32 @@
1
+ import { Config } from "../../framework/Config.js";
2
+
3
+ //#region src/entities/system/motion-presets.d.ts
4
+ /**
5
+ * A value a JS preset holds that `motion/react` cannot be handed as written: a token a mode or state
6
+ * decides (`conditional`), which a preset animates as its base value; a token that comes to nothing a
7
+ * preset can carry (`unresolved`); or an `ease` that is no easing `motion/react` reads (`easing`),
8
+ * from a token or written in place. The last two leave the field out.
9
+ */
10
+ interface MotionPresetToken {
11
+ readonly preset: string;
12
+ /** The field the value sits under, as a dotted path: `transition.stiffness`. */
13
+ readonly at: string;
14
+ /** The token the value comes from; absent for an `ease` written in place. */
15
+ readonly token?: string;
16
+ readonly problem: 'conditional' | 'unresolved' | 'easing';
17
+ }
18
+ /**
19
+ * Every JS-runtime preset, by path, as the props the wrapper spreads onto the animated component.
20
+ * A token a preset names reaches `motion/react` as its value — `Token.ref('springStiffness/snappy')`
21
+ * as `400`, a `200ms` duration as `0.2` — since it reads numbers and seconds, not variables.
22
+ */
23
+ declare function jsMotionPresets(config: Config): Record<string, Record<string, unknown>>;
24
+ /** Every JS preset a style rule writes in place, by its id, resolved as {@link jsMotionPresets} are. */
25
+ declare function inlineMotionProps(config: Config): Record<string, Record<string, unknown>>;
26
+ /** Whether anything in the system animates through `motion/react`: a JS preset, named or written in
27
+ * place in a style rule. */
28
+ declare function hasJsMotion(config: Config): boolean;
29
+ /** The tokens JS presets name that a preset cannot carry as one value, for the build to report. */
30
+ declare function motionPresetTokens(config: Config): MotionPresetToken[];
31
+ //#endregion
32
+ export { MotionPresetToken, hasJsMotion, inlineMotionProps, jsMotionPresets, motionPresetTokens };
@@ -0,0 +1,239 @@
1
+ import { collectRefs, isRef, kindOf, pathOf } from "../../framework/utils/refs.js";
2
+ import { composedValueFormOf } from "./composed-values.js";
3
+ import { createSliceMemo } from "../../framework/memo.js";
4
+ import { inlineMotions } from "./motion-variants.js";
5
+ import { jsMotionPropFields } from "./Motion.js";
6
+ import { tokenValueRow } from "./token-values.js";
7
+ import { renderValue } from "./value-render.js";
8
+ //#region src/entities/system/motion-presets.ts
9
+ /** A time spelled in CSS, which `motion/react` reads in seconds. */
10
+ const CSS_TIME = /^(-?(?:\d+(?:\.\d*)?|\.\d+))(ms|s)$/;
11
+ /** A bare number, which a token's value is often written as a string of. */
12
+ const BARE_NUMBER = /^-?(?:\d+(?:\.\d*)?|\.\d+)$/;
13
+ /**
14
+ * One token's value as `motion/react` reads it: a time in seconds, a bare number as a number, and
15
+ * anything else as its CSS, which `motion/react` reads for a length or a colour (`'8px'`, `'#fff'`).
16
+ */
17
+ function motionValue(css) {
18
+ if (typeof css === "number") return css;
19
+ const text = css.trim();
20
+ const time = CSS_TIME.exec(text);
21
+ if (time) return time[2] === "ms" ? Number(time[1]) / 1e3 : Number(time[1]);
22
+ return BARE_NUMBER.test(text) ? Number(text) : text;
23
+ }
24
+ /** The easings `motion/react` names for itself, which a preset may write as they are. */
25
+ const MOTION_EASINGS = new Set([
26
+ "linear",
27
+ "easeIn",
28
+ "easeOut",
29
+ "easeInOut",
30
+ "circIn",
31
+ "circOut",
32
+ "circInOut",
33
+ "backIn",
34
+ "backOut",
35
+ "backInOut",
36
+ "anticipate"
37
+ ]);
38
+ /** CSS's easing keywords, as `motion/react` spells them. `ease` has no name there, so it is its curve. */
39
+ const CSS_EASINGS = {
40
+ linear: "linear",
41
+ "ease-in": "easeIn",
42
+ "ease-out": "easeOut",
43
+ "ease-in-out": "easeInOut",
44
+ ease: [
45
+ .25,
46
+ .1,
47
+ .25,
48
+ 1
49
+ ]
50
+ };
51
+ const CUBIC_BEZIER = /^cubic-bezier\(\s*([^,\s]+)\s*,\s*([^,\s]+)\s*,\s*([^,\s]+)\s*,\s*([^,\s)]+)\s*\)$/;
52
+ /**
53
+ * An `ease` as `motion/react` reads it: one of its own names, a CSS keyword as its name, a
54
+ * `cubic-bezier(a, b, c, d)` as `[a, b, c, d]`, a curve already written as four numbers, or a list of
55
+ * any of those (one per keyframe). `undefined` for anything else, which `motion/react` would ignore.
56
+ */
57
+ function motionEasing(value) {
58
+ if (typeof value === "string") {
59
+ const text = value.trim();
60
+ if (MOTION_EASINGS.has(text)) return text;
61
+ if (Object.hasOwn(CSS_EASINGS, text)) {
62
+ const easing = CSS_EASINGS[text];
63
+ return Array.isArray(easing) ? [...easing] : easing;
64
+ }
65
+ const curve = CUBIC_BEZIER.exec(text);
66
+ if (!curve) return void 0;
67
+ const points = curve.slice(1).map(Number);
68
+ return points.every(Number.isFinite) ? points : void 0;
69
+ }
70
+ if (Array.isArray(value)) {
71
+ if (value.length === 4 && value.every((n) => typeof n === "number")) return value;
72
+ const easings = value.map(motionEasing);
73
+ return easings.length > 0 && easings.every((e) => e !== void 0) ? easings : void 0;
74
+ }
75
+ }
76
+ /**
77
+ * A preset's value with every token it names resolved, or `undefined` when one of them resolves to
78
+ * nothing. Values and keyframe lists are walked; a ref, a string with refs in it and a composed value
79
+ * are each read to their literal. A token a mode decides is read as its base value and reported.
80
+ */
81
+ function resolvePresetValue({ config, value, at, report }) {
82
+ const refs = collectRefs(value).filter((target) => kindOf(target) === "token");
83
+ const container = Array.isArray(value) || typeof value === "object" && value !== null && !isRef(value) && !composedValueFormOf(value);
84
+ if (refs.length === 0 && !container) return { value };
85
+ if (isRef(value)) {
86
+ const row = tokenValueRow({
87
+ config,
88
+ target: value.__ref
89
+ });
90
+ if (row?.state.kind === "value") return { value: motionValue(row.state.value.css) };
91
+ }
92
+ if (Array.isArray(value)) {
93
+ const items = [];
94
+ for (const [index, item] of value.entries()) {
95
+ const resolved = resolvePresetValue({
96
+ config,
97
+ value: item,
98
+ at: `${at}.${index}`,
99
+ report
100
+ });
101
+ if (!resolved) return void 0;
102
+ items.push(resolved.value);
103
+ }
104
+ return { value: items };
105
+ }
106
+ if (typeof value === "object" && value !== null && !isRef(value) && !composedValueFormOf(value)) {
107
+ const fields = {};
108
+ for (const [key, inner] of Object.entries(value)) {
109
+ const resolved = resolvePresetValue({
110
+ config,
111
+ value: inner,
112
+ at: `${at}.${key}`,
113
+ report
114
+ });
115
+ if (!resolved) continue;
116
+ if (key !== "ease") {
117
+ fields[key] = resolved.value;
118
+ continue;
119
+ }
120
+ const easing = motionEasing(resolved.value);
121
+ if (easing !== void 0) {
122
+ fields[key] = easing;
123
+ continue;
124
+ }
125
+ const tokens = collectRefs(inner).filter((target) => kindOf(target) === "token");
126
+ if (tokens.length === 0) report({
127
+ at: `${at}.${key}`,
128
+ problem: "easing"
129
+ });
130
+ for (const target of tokens) report({
131
+ at: `${at}.${key}`,
132
+ token: pathOf(target),
133
+ problem: "easing"
134
+ });
135
+ }
136
+ return { value: fields };
137
+ }
138
+ for (const target of refs) if (tokenValueRow({
139
+ config,
140
+ target
141
+ })?.state.kind === "conditional") report({
142
+ at,
143
+ token: pathOf(target),
144
+ problem: "conditional"
145
+ });
146
+ const literal = renderValue({
147
+ config,
148
+ value,
149
+ refs: "literal"
150
+ });
151
+ if (literal === void 0) {
152
+ const missing = refs.filter((target) => {
153
+ const kind = tokenValueRow({
154
+ config,
155
+ target
156
+ })?.state.kind;
157
+ return kind === void 0 || kind === "unresolved";
158
+ });
159
+ for (const target of missing.length > 0 ? missing : refs) report({
160
+ at,
161
+ token: pathOf(target),
162
+ problem: "unresolved"
163
+ });
164
+ return;
165
+ }
166
+ return { value: motionValue(literal) };
167
+ }
168
+ /** One preset body as the props `motion.create` reads, reporting under `preset` what it could not carry. */
169
+ function resolveBody(config, preset, body, tokens) {
170
+ const props = {};
171
+ for (const field of jsMotionPropFields) {
172
+ if (body[field] === void 0) continue;
173
+ const resolved = resolvePresetValue({
174
+ config,
175
+ value: body[field],
176
+ at: field,
177
+ report: (problem) => tokens.push({
178
+ preset,
179
+ ...problem
180
+ })
181
+ });
182
+ if (resolved) props[field] = resolved.value;
183
+ }
184
+ return props;
185
+ }
186
+ /**
187
+ * Every JS preset as the props `motion.create` reads, with the tokens they name resolved, and the
188
+ * tokens that could not be handed over as one value: the named presets by path, and the ones a style
189
+ * rule writes in place by the id `inlineMotionId` gives them.
190
+ */
191
+ const resolveJsMotionPresets = createSliceMemo({
192
+ kinds: [
193
+ "motion",
194
+ "component",
195
+ "token",
196
+ "settings",
197
+ "font"
198
+ ],
199
+ compute: (config) => {
200
+ const presets = {};
201
+ const inline = {};
202
+ const tokens = [];
203
+ for (const preset of config.list("motion")) {
204
+ const body = preset.toJSON();
205
+ if (body.runtime !== "js") continue;
206
+ presets[preset.path] = resolveBody(config, preset.path, body, tokens);
207
+ }
208
+ for (const entry of inlineMotions(config).valid) inline[entry.id] = resolveBody(config, entry.id, entry.body, tokens);
209
+ return {
210
+ presets,
211
+ inline,
212
+ tokens
213
+ };
214
+ }
215
+ });
216
+ /**
217
+ * Every JS-runtime preset, by path, as the props the wrapper spreads onto the animated component.
218
+ * A token a preset names reaches `motion/react` as its value — `Token.ref('springStiffness/snappy')`
219
+ * as `400`, a `200ms` duration as `0.2` — since it reads numbers and seconds, not variables.
220
+ */
221
+ function jsMotionPresets(config) {
222
+ return resolveJsMotionPresets(config).presets;
223
+ }
224
+ /** Every JS preset a style rule writes in place, by its id, resolved as {@link jsMotionPresets} are. */
225
+ function inlineMotionProps(config) {
226
+ return resolveJsMotionPresets(config).inline;
227
+ }
228
+ /** Whether anything in the system animates through `motion/react`: a JS preset, named or written in
229
+ * place in a style rule. */
230
+ function hasJsMotion(config) {
231
+ const { presets, inline } = resolveJsMotionPresets(config);
232
+ return Object.keys(presets).length > 0 || Object.keys(inline).length > 0;
233
+ }
234
+ /** The tokens JS presets name that a preset cannot carry as one value, for the build to report. */
235
+ function motionPresetTokens(config) {
236
+ return resolveJsMotionPresets(config).tokens;
237
+ }
238
+ //#endregion
239
+ export { hasJsMotion, inlineMotionProps, jsMotionPresets, motionPresetTokens };