@yahoo/uds-create-config 5.40.4 → 5.41.1

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.
Files changed (49) hide show
  1. package/dist/configs/react-native-system.d.ts +970 -0
  2. package/dist/configs/react-native-system.js +18 -0
  3. package/dist/configs/system.d.ts +903 -536
  4. package/dist/configs/system.js +17 -1
  5. package/dist/entities/system/LinkedSystem.d.ts +2 -17
  6. package/dist/entities/system/Motion.d.ts +903 -536
  7. package/dist/entities/system/Motion.js +81 -101
  8. package/dist/entities/system/css-motion-reach.js +7 -2
  9. package/dist/entities/system/css-motion.js +8 -3
  10. package/dist/entities/system/motion-convert.d.ts +20 -14
  11. package/dist/entities/system/motion-convert.js +27 -58
  12. package/dist/entities/system/motion-presets.js +4 -3
  13. package/dist/entities/system/motion-types.d.ts +7 -0
  14. package/dist/entities/system/motion-types.js +132 -0
  15. package/dist/entities/system/motion-variants.js +28 -12
  16. package/dist/entities/system/reanimated-motion.d.ts +777 -0
  17. package/dist/entities/system/reanimated-motion.js +100 -0
  18. package/dist/framework/Config.d.ts +4 -4
  19. package/dist/framework/Config.js +4 -4
  20. package/dist/framework/class-names.js +4 -2
  21. package/dist/framework/registered.d.ts +1 -8
  22. package/dist/framework/registry.d.ts +1 -2
  23. package/dist/framework/schema-version.d.ts +1 -1
  24. package/dist/framework/schema-version.js +5 -5
  25. package/dist/framework/utils/field-schema.js +2 -2
  26. package/dist/index.d.ts +2 -2
  27. package/dist/index.js +1 -1
  28. package/dist/migrations/2.0.0/v1-artifact.d.ts +903 -536
  29. package/dist/migrations/20261015203142_shared_motion.d.ts +4 -0
  30. package/dist/migrations/20261015203142_shared_motion.js +8 -0
  31. package/dist/migrations/20261015205522_declarative_reanimated.d.ts +4 -0
  32. package/dist/migrations/20261015205522_declarative_reanimated.js +8 -0
  33. package/dist/migrations/20261015212352_native_motion.d.ts +4 -0
  34. package/dist/migrations/20261015212352_native_motion.js +10 -0
  35. package/dist/migrations/20261015212414_motion_types.d.ts +5 -0
  36. package/dist/migrations/20261015212414_motion_types.js +8 -0
  37. package/dist/migrations/20261015212722_reanimated_cubic_bezier.d.ts +4 -0
  38. package/dist/migrations/20261015212722_reanimated_cubic_bezier.js +8 -0
  39. package/dist/migrations/20261015221052_motion_json_schema.d.ts +7 -0
  40. package/dist/migrations/20261015221052_motion_json_schema.js +8 -0
  41. package/dist/migrations/20261016135620_motion_type_only.d.ts +9 -0
  42. package/dist/migrations/20261016135620_motion_type_only.js +9 -0
  43. package/dist/migrations/20261016164316_motion_compatibility.d.ts +8 -0
  44. package/dist/migrations/20261016164316_motion_compatibility.js +9 -0
  45. package/dist/migrations/motion-source-codemod.d.ts +3 -0
  46. package/dist/spec/motion.d.ts +3 -2
  47. package/dist/spec/motion.js +2 -1
  48. package/dist/tsconfig.tsbuildinfo +1 -1
  49. package/package.json +2 -2
@@ -1,29 +1,20 @@
1
1
  import { isRef, refSchema } from "../../framework/utils/refs.js";
2
+ import { METADATA_KEYS } from "../../framework/utils/field-schema.js";
2
3
  import { pathInput } from "../../framework/schemas.js";
3
4
  import { defineEntity } from "../../framework/defineEntity.js";
5
+ import { isNativeConfig } from "../../configs/platform.js";
4
6
  import { authoredValue } from "../../framework/projections.js";
5
- import { convertMotion, convertMotionPreset, planMotionConversion } from "./motion-convert.js";
7
+ import { convertMotion } from "./motion-convert.js";
8
+ import { cssMotionValue, motionTransition, reanimatedMotion, webMotionBody } from "./motion-types.js";
6
9
  import { renameMotionVariant } from "./motion-variants.js";
7
10
  import { cssMotionVariantGroups } from "./css-motion-reach.js";
8
11
  import { REDUCED_MOTION_ATTRIBUTE, REDUCED_MOTION_VALUES } from "./reduced-motion.js";
9
12
  import { z } from "zod";
10
13
  //#region src/entities/system/Motion.ts
11
14
  /**
12
- * Motion (`fade/in`) — an animation preset, discriminated by `runtime`:
13
- * - `css` → played by the stylesheet. Either `keyframes`, which codegen emits as `@keyframes`, or
14
- * states (`variants`, `initial`, `animate`, `exit`, `whileHover`, `whileTap`, `whileFocus`) with a
15
- * `transition` written as CSS, or both. The states mean what a JS preset's do.
16
- * - `js` → played by `motion/react`: the interaction state the client drives, `exit` included,
17
- * which runs under a `Presence` ancestor when the element unmounts (the build makes it motion's
18
- * `AnimatePresence`). Each state is a set of values or the name of one of the preset's
19
- * `variants`, and `layout` animates the element's own size and position as they change.
20
- * Each takes only its own fields. `motion/convert` rewrites a preset from one to the other, and a
21
- * link's `{ override: { runtime: 'css' } }` converts the source's preset as it reads
22
- * (`motion-convert.ts`). A CSS preset with neither keyframes nor a state is refused, since it would
23
- * emit nothing.
24
- *
25
- * Each member declares its whole surface, because `strictObject` drops what it doesn't name and does
26
- * it silently.
15
+ * Named motion implementations use `{ type, value }`. A type owns its payload and execution path;
16
+ * selecting another implementation is an explicit reference override, never an implicit conversion.
17
+ * `runtime` bodies below are the compatibility schema for existing configs and sealed sources.
27
18
  */
28
19
  const values = z.record(z.string(), z.unknown());
29
20
  /**
@@ -36,7 +27,7 @@ const gesture = z.union([
36
27
  z.string(),
37
28
  z.array(z.string())
38
29
  ]).optional();
39
- const RUNTIME = "Which runtime plays the preset, and so which fields apply.";
30
+ const RUNTIME = "Deprecated compatibility format. New presets use type and value.";
40
31
  const initial = z.union([
41
32
  values,
42
33
  z.string(),
@@ -171,30 +162,41 @@ const inlineJsMotion = JsMotion.omit({ runtime: true }).extend({ runtime: z.lite
171
162
  /** The fields of a JS preset that `motion.create` reads as props, in schema order. The emitter
172
163
  * reads this rather than restating the schema, so a field added here reaches every emitted preset. */
173
164
  const jsMotionPropFields = Object.keys(JsMotion.shape).filter((field) => field !== "runtime" && field !== "reducedMotion");
174
- const convertInput = z.object({
175
- path: pathInput("motion"),
176
- to: z.enum(["css", "js"]).describe("The runtime to convert the preset to.")
165
+ const legacyType = z.undefined();
166
+ legacyType._zod.toJSONSchema = () => ({ not: {} });
167
+ const LegacyMotion = z.strictObject({
168
+ ...CssMotion.shape,
169
+ ...JsMotion.shape,
170
+ type: legacyType.optional().describe("Absent for a legacy preset; new presets select their implementation with type."),
171
+ runtime: z.enum(["css", "js"]).describe(RUNTIME)
172
+ }).superRefine((body, context) => {
173
+ const legacy = Object.fromEntries(Object.entries(body).filter(([key]) => key !== "type" && !METADATA_KEYS.includes(key)));
174
+ const parsed = (body.runtime === "css" ? CssMotion : JsMotion).safeParse(legacy);
175
+ if (!parsed.success) for (const issue of parsed.error.issues) context.addIssue({
176
+ code: "custom",
177
+ message: issue.message,
178
+ path: issue.path
179
+ });
177
180
  });
178
- /** A real preset converted to the runtime it isn't, so the example runs; none when there is none. */
179
- const convertExample = ({ config }) => {
180
- for (const preset of config.list("motion")) {
181
- const body = preset.toJSON();
182
- const to = body.runtime === "js" ? "css" : "js";
183
- if (convertMotion({
184
- body,
185
- to
186
- }).refused.length === 0) return {
187
- path: preset.path,
188
- to
189
- };
190
- }
191
- };
192
181
  const Motion = defineEntity({
193
182
  kind: "motion",
194
183
  label: "Motion",
195
184
  labelPlural: "Motion presets",
196
- fields: z.discriminatedUnion("runtime", [CssMotion, JsMotion]),
185
+ fields: z.discriminatedUnion("type", [
186
+ reanimatedMotion,
187
+ z.strictObject({
188
+ type: z.literal("css").describe("The animation implementation that reads this preset."),
189
+ value: cssMotionValue.describe("CSS keyframes and transition/animation longhands; interaction targets belong in component styles."),
190
+ reducedMotion
191
+ }),
192
+ z.strictObject({
193
+ type: z.literal("motion/react").describe("The animation implementation that reads this preset."),
194
+ value: JsMotion.omit({ runtime: true }).extend({ transition: motionTransition.optional() }).describe("Motion states, gestures, variants and serializable transitions.")
195
+ }),
196
+ LegacyMotion
197
+ ]),
197
198
  rebaseLinkedBody: ({ body, extend }) => {
199
+ if (body.type !== void 0 || extend.type !== void 0) return body;
198
200
  const to = extend.runtime;
199
201
  if (to !== "css" && to !== "js" || body.runtime === to) return body;
200
202
  return convertMotion({
@@ -204,81 +206,59 @@ const Motion = defineEntity({
204
206
  },
205
207
  layerKeys: {
206
208
  motion: {
207
- schema: (declared) => z.union([declared ?? 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`." }),
209
+ schema: (declared) => z.union([
210
+ declared ?? z.union([refSchema, z.string()]),
211
+ inlineJsMotion,
212
+ z.literal(false)
213
+ ], { 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`." }),
208
214
  reach: ({ config, value, element }) => {
209
- return (config.resolve("motion", String(authoredValue(config, "motion", value)))?.toJSON())?.runtime === "css" && isRef(element) ? "prop" : "none";
215
+ if (isNativeConfig(config)) return "prop";
216
+ const preset = config.resolve("motion", String(authoredValue(config, "motion", value)))?.toJSON();
217
+ if (preset?.type === "react-native-reanimated") throw new Error("No UDS web motion integration for react-native-reanimated. Select a supported preset or disable motion.");
218
+ return preset && webMotionBody(preset).runtime === "css" && isRef(element) ? "prop" : "none";
210
219
  },
211
220
  propType: "motion"
212
221
  },
213
222
  reducedMotion: {
214
223
  schema: () => z.enum(REDUCED_MOTION_VALUES).describe("How the layer's motion, and the motion inside it, treats the viewer's reduced-motion setting: `'user'` follows the OS, `'always'` reduces, `'never'` plays in full."),
215
- reach: () => ({ attribute: REDUCED_MOTION_ATTRIBUTE })
224
+ reach: ({ config }) => isNativeConfig(config) ? "prop" : { attribute: REDUCED_MOTION_ATTRIBUTE }
216
225
  }
217
226
  },
218
- schemas: {
219
- /**
220
- * A variant name is a string the presets that animate together match by, not a ref, so nothing
221
- * cascades it. Renaming one in a single preset's `variants` leaves its own states and every inner
222
- * layer that answers it naming the old one, which animates to nothing and says nothing.
223
- */
224
- "rename-variant": {
225
- input: z.object({
226
- path: pathInput("motion"),
227
- key: z.string().min(1),
228
- to: z.string().min(1)
229
- }),
230
- readOnly: false,
231
- label: "Rename variant",
232
- description: "Rename one of a 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 of the same runtime 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`.",
233
- scope: "item",
234
- title: (entity) => `Rename ${entity} variant`,
235
- example: ({ config }) => {
236
- for (const preset of config.list("motion")) {
237
- const variants = preset.toJSON().variants;
238
- const [key] = Object.keys(variants ?? {});
239
- if (key !== void 0) return {
240
- path: preset.path,
241
- key,
242
- to: `${key}-renamed`
243
- };
244
- }
245
- },
246
- handler: (input, config) => renameMotionVariant(config, {
247
- path: String(input.path),
248
- from: String(input.key),
249
- to: String(input.to),
250
- groupOf: (config, preset) => cssMotionVariantGroups(config).find((group) => group.has(preset)) ?? new Set([preset])
251
- })
252
- },
253
- convert: {
254
- input: convertInput,
255
- readOnly: false,
256
- label: "Convert runtime",
257
- description: "Rewrite a preset for the other runtime. `to: 'css'` keeps its states and writes its transition as CSS timing, a spring as `{ spring: { … } }`, and drops what CSS has no counterpart for: `layout`, `drag*`, `whileDrag`, a state's `transitionEnd`, and a transition's orchestration (`when`, `delayChildren`, `staggerChildren`). `to: 'js'` does the reverse. Refused when the preset holds something the other runtime can't represent, such as a repeat for CSS or a `linear()` easing for JS. A preset borrowed from a linked system takes `runtime` on the link instead, so it keeps following the source. `convert-plan` lists what would be dropped, approximated or refused.",
258
- scope: "item",
259
- title: (entity) => `Convert ${entity}`,
260
- example: convertExample,
261
- handler: (input, config) => convertMotionPreset({
262
- config,
263
- path: String(input.path),
264
- to: input.to
265
- })
227
+ schemas: {
228
+ /**
229
+ * A variant name is a string the presets that animate together match by, not a ref, so nothing
230
+ * cascades it. Renaming one in a single preset's `variants` leaves its own states and every inner
231
+ * layer that answers it naming the old one, which animates to nothing and says nothing.
232
+ */
233
+ "rename-variant": {
234
+ input: z.object({
235
+ path: pathInput("motion"),
236
+ key: z.string().min(1),
237
+ to: z.string().min(1)
238
+ }),
239
+ readOnly: false,
240
+ label: "Rename variant",
241
+ description: "Rename one of a 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 of the same runtime 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`.",
242
+ scope: "item",
243
+ title: (entity) => `Rename ${entity} variant`,
244
+ example: ({ config }) => {
245
+ for (const preset of config.list("motion")) {
246
+ const variants = webMotionBody(preset.toJSON()).variants;
247
+ const [key] = Object.keys(variants ?? {});
248
+ if (key !== void 0) return {
249
+ path: preset.path,
250
+ key,
251
+ to: `${key}-renamed`
252
+ };
253
+ }
266
254
  },
267
- "convert-plan": {
268
- input: convertInput,
269
- readOnly: true,
270
- label: "Plan a conversion",
271
- description: "What `convert` with the same input would drop, approximate or refuse, field by field, and whether it writes to a link. Writes nothing.",
272
- scope: "item",
273
- title: (entity) => `Plan converting ${entity}`,
274
- example: convertExample,
275
- handler: (input, config) => planMotionConversion({
276
- config,
277
- path: String(input.path),
278
- to: input.to
279
- })
280
- }
281
- }
255
+ handler: (input, config) => renameMotionVariant(config, {
256
+ path: String(input.path),
257
+ from: String(input.key),
258
+ to: String(input.to),
259
+ groupOf: (config, preset) => cssMotionVariantGroups(config).find((group) => group.has(preset)) ?? new Set([preset])
260
+ })
261
+ } }
282
262
  });
283
263
  //#endregion
284
264
  export { JS_ONLY_FIELDS, Motion, inlineJsMotion, jsMotionPropFields };
@@ -2,6 +2,7 @@ import { isRef, kindOf, pathOf } from "../../framework/utils/refs.js";
2
2
  import { isPlainObject } from "../../framework/utils/field-path.js";
3
3
  import { createSliceMemo } from "../../framework/memo.js";
4
4
  import { convertMotion } from "./motion-convert.js";
5
+ import { webMotionBody } from "./motion-types.js";
5
6
  import { placedMotions, variantGroupsOf } from "./motion-variants.js";
6
7
  import { requiredContentLayers } from "./required-content-layers.js";
7
8
  //#region src/entities/system/css-motion-reach.ts
@@ -54,7 +55,7 @@ const resolvePlacements = createSliceMemo({
54
55
  for (const entry of Object.values(styles)) for (const [layer, bag] of Object.entries(entry?.layers ?? {})) {
55
56
  const preset = namedPreset(isPlainObject(bag) ? bag.motion : void 0);
56
57
  if (preset === void 0) continue;
57
- if ((config.resolve("motion", preset)?.toJSON())?.runtime !== "css" || !(layer in layers)) continue;
58
+ if (motionBodyAt(config, preset)?.runtime !== "css" || !(layer in layers)) continue;
58
59
  const key = `${component.path}\u0000${layer}\u0000${preset}`;
59
60
  if (seen.has(key)) continue;
60
61
  seen.add(key);
@@ -96,7 +97,7 @@ const resolveExits = createSliceMemo({
96
97
  for (const entry of Object.values(styles)) for (const [layer, bag] of Object.entries(entry?.layers ?? {})) {
97
98
  const preset = namedPreset(isPlainObject(bag) ? bag.motion : void 0);
98
99
  if (preset === void 0) continue;
99
- const body = config.resolve("motion", preset)?.toJSON();
100
+ const body = motionBodyAt(config, preset);
100
101
  if (body?.runtime !== "css" || body.exit === void 0) continue;
101
102
  unmountable ??= unmountableLayers(config, component.path);
102
103
  if (!unmountable.has(layer)) continue;
@@ -191,5 +192,9 @@ const runtimeSet = (body) => {
191
192
  function linkedMotionSwaps(config) {
192
193
  return resolveLinkedSwaps(config);
193
194
  }
195
+ function motionBodyAt(config, path) {
196
+ const body = config.resolve("motion", path)?.toJSON();
197
+ return body ? webMotionBody(body) : void 0;
198
+ }
194
199
  //#endregion
195
200
  export { cssMotionExitsOnUnmount, cssMotionPlacements, cssMotionVariantGroups, linkedMotionSwaps, swappedLinkJsMotions };
@@ -1,6 +1,7 @@
1
1
  import { isPlainObject } from "../../framework/utils/field-path.js";
2
2
  import { createSliceMemo } from "../../framework/memo.js";
3
3
  import { cssEasing, easingCurve, linearEasing, springTiming } from "./css-timing.js";
4
+ import { webMotionBody } from "./motion-types.js";
4
5
  import { stateVariantNames } from "./motion-variants.js";
5
6
  import { resolvePresetValue } from "./motion-presets.js";
6
7
  //#region src/entities/system/css-motion.ts
@@ -394,8 +395,8 @@ const resolveCssMotionPresets = createSliceMemo({
394
395
  tokens.push(entry);
395
396
  };
396
397
  for (const preset of config.list("motion")) {
397
- const body = preset.toJSON();
398
- if (body.runtime !== "css") continue;
398
+ const body = webMotionBody(preset.toJSON());
399
+ if (preset.toJSON().runtime !== "css") continue;
399
400
  if (!hasStates(body)) continue;
400
401
  lists.push(...keyframeLists({
401
402
  preset: preset.path,
@@ -428,7 +429,7 @@ function cssMotionTransitions(config) {
428
429
  */
429
430
  function cssMotionExitDuration({ config, preset }) {
430
431
  const transitions = cssMotionTransitions(config)[preset];
431
- const exit = (config.resolve("motion", preset)?.toJSON())?.exit;
432
+ const exit = motionBodyAt(config, preset)?.exit;
432
433
  if (!transitions || exit === void 0) return 0;
433
434
  const timings = isPlainObject(exit) ? [transitions.states.exit ?? {}] : stateVariantNames(exit).map((name) => transitions.variants[name] ?? {});
434
435
  return Math.max(0, ...timings.flatMap((group) => Object.values(group).map((timing) => (milliseconds(timing.duration) ?? 0) + (milliseconds(timing.delay) ?? 0))));
@@ -441,5 +442,9 @@ function cssMotionKeyframeLists(config) {
441
442
  function cssMotionTokens(config) {
442
443
  return resolveCssMotionPresets(config).tokens;
443
444
  }
445
+ function motionBodyAt(config, path) {
446
+ const body = config.resolve("motion", path)?.toJSON();
447
+ return body ? webMotionBody(body) : void 0;
448
+ }
444
449
  //#endregion
445
450
  export { CSS_MOTION_STATES, cssMotionExitDuration, cssMotionKeyframeLists, cssMotionTokens, cssMotionTransitions };
@@ -1,4 +1,23 @@
1
1
  //#region src/entities/system/motion-convert.d.ts
2
+ /**
3
+ * A motion preset rewritten for the other runtime: a JS preset as the CSS preset that plays closest
4
+ * to it, and a CSS preset as a JS one. Retained only for existing links that set a borrowed
5
+ * legacy preset's runtime. Explicit type/value presets never enter this compatibility path.
6
+ *
7
+ * The states (`variants`, `initial`, `animate`, `exit`, `while*`) mean the same in both runtimes, so
8
+ * they carry over as they are, and what changes is the timing:
9
+ *
10
+ * - JS → CSS. A spring stays a spring (`easing: { spring: { … } }`), a tween's `duration` becomes a
11
+ * CSS time, its `ease` a CSS easing, and a value's own transition goes under `properties`. What CSS
12
+ * has no counterpart for is dropped: `layout`, `drag*` and `whileDrag`, a state's `transitionEnd`,
13
+ * and a transition's orchestration (`when`, `delayChildren`, `staggerChildren`). A repeat is refused,
14
+ * since a transition plays once.
15
+ * - CSS → JS. The same in reverse. An easing `motion/react` can't read (`linear()`, `steps()`, a
16
+ * `var()`) is refused, and so are `@keyframes` that aren't one list of values per property.
17
+ *
18
+ * What a conversion approximates is played differently than written: a list of easings, one per
19
+ * keyframe, and a keyframe list CSS plays only as its last value.
20
+ */
2
21
  /** One field a conversion leaves out, plays differently, or can't carry over. */
3
22
  interface MotionConversionNote {
4
23
  /** Where the field sits, as a dotted path: `transition.opacity.repeat`. */
@@ -25,18 +44,5 @@ declare function convertMotion({
25
44
  body: Body;
26
45
  to: MotionRuntime;
27
46
  }): MotionConversion;
28
- /** What `motion/convert-plan` answers: the conversion `motion/convert` would make, and where it
29
- * would write it. */
30
- interface MotionConversionPlan {
31
- readonly path: string;
32
- readonly to: MotionRuntime;
33
- /** The linked system the preset is borrowed from, whose link the conversion is written to. */
34
- readonly link?: string;
35
- readonly dropped: readonly MotionConversionNote[];
36
- readonly approximated: readonly MotionConversionNote[];
37
- readonly refused: readonly MotionConversionNote[];
38
- /** Why the conversion would be refused. `undefined` when it would go through. */
39
- readonly blocked?: string;
40
- }
41
47
  //#endregion
42
- export { MotionConversion, MotionConversionNote, MotionConversionPlan, MotionRuntime, convertMotion };
48
+ export { MotionConversion, MotionConversionNote, MotionRuntime, convertMotion };
@@ -1,10 +1,28 @@
1
- import { reject } from "../../framework/rejection.js";
2
1
  import { isRef } from "../../framework/utils/refs.js";
3
2
  import { isPlainObject } from "../../framework/utils/field-path.js";
4
3
  import { MOTION_EASING_NAMES, bezierPoints, motionOfCssKeyword } from "./easings.js";
5
4
  import { cssEasing } from "./css-timing.js";
6
5
  import { JS_ONLY_FIELDS } from "./Motion.js";
7
6
  //#region src/entities/system/motion-convert.ts
7
+ /**
8
+ * A motion preset rewritten for the other runtime: a JS preset as the CSS preset that plays closest
9
+ * to it, and a CSS preset as a JS one. Retained only for existing links that set a borrowed
10
+ * legacy preset's runtime. Explicit type/value presets never enter this compatibility path.
11
+ *
12
+ * The states (`variants`, `initial`, `animate`, `exit`, `while*`) mean the same in both runtimes, so
13
+ * they carry over as they are, and what changes is the timing:
14
+ *
15
+ * - JS → CSS. A spring stays a spring (`easing: { spring: { … } }`), a tween's `duration` becomes a
16
+ * CSS time, its `ease` a CSS easing, and a value's own transition goes under `properties`. What CSS
17
+ * has no counterpart for is dropped: `layout`, `drag*` and `whileDrag`, a state's `transitionEnd`,
18
+ * and a transition's orchestration (`when`, `delayChildren`, `staggerChildren`). A repeat is refused,
19
+ * since a transition plays once.
20
+ * - CSS → JS. The same in reverse. An easing `motion/react` can't read (`linear()`, `steps()`, a
21
+ * `var()`) is refused, and so are `@keyframes` that aren't one list of values per property.
22
+ *
23
+ * What a conversion approximates is played differently than written: a list of easings, one per
24
+ * keyframe, and a keyframe list CSS plays only as its last value.
25
+ */
8
26
  /** The states both runtimes share. */
9
27
  const STATES = [
10
28
  "initial",
@@ -53,6 +71,13 @@ var Notes = class {
53
71
  /** `body` as the `to` runtime takes it, with what the conversion left out, changed or refused. */
54
72
  function convertMotion({ body, to }) {
55
73
  const notes = new Notes();
74
+ if (body.type !== void 0) {
75
+ notes.refused.push({
76
+ at: "type",
77
+ reason: "Typed presets use explicit replacement refs; automatic conversion is a legacy operation."
78
+ });
79
+ return { ...notes };
80
+ }
56
81
  if (body.runtime === to) return {
57
82
  body,
58
83
  ...notes
@@ -457,61 +482,5 @@ function offsetOf(offset) {
457
482
  return value >= 0 && value <= 1 ? value : void 0;
458
483
  }
459
484
  const camelCase = (property) => property.startsWith("--") ? property : property.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
460
- /** What converting the preset at `path` would do, without doing it. */
461
- function planMotionConversion({ config, path, to }) {
462
- const body = config.resolve("motion", path)?.toJSON();
463
- if (!body) reject("NOT_FOUND", `There is no motion preset "${path}".`);
464
- const link = config.borrowedFrom("motion", path);
465
- const conversion = convertMotion({
466
- body,
467
- to
468
- });
469
- return {
470
- path,
471
- to,
472
- ...link === void 0 ? {} : { link },
473
- dropped: conversion.dropped,
474
- approximated: conversion.approximated,
475
- refused: conversion.refused,
476
- ...conversion.refused.length > 0 ? { blocked: refusal({
477
- path,
478
- to,
479
- refused: conversion.refused
480
- }) } : {}
481
- };
482
- }
483
- /**
484
- * The preset at `path` rewritten for `to`. A preset of this config's own is replaced by its
485
- * conversion. One borrowed from a linked system takes `runtime: to` on the link, which converts the
486
- * source's preset as it reads, so it keeps following what the source changes.
487
- */
488
- function convertMotionPreset({ config, path, to }) {
489
- const plan = planMotionConversion({
490
- config,
491
- path,
492
- to
493
- });
494
- if (plan.blocked !== void 0) reject("UNSUPPORTED", plan.blocked);
495
- const body = config.resolve("motion", path)?.toJSON();
496
- if (body.runtime === to) return config;
497
- if (plan.link !== void 0) return config.apply({
498
- kind: "motion",
499
- operation: "update",
500
- path,
501
- data: { runtime: to }
502
- });
503
- return config.apply({
504
- kind: "motion",
505
- operation: "replace",
506
- path,
507
- data: convertMotion({
508
- body,
509
- to
510
- }).body
511
- });
512
- }
513
- function refusal({ path, to, refused }) {
514
- return `"${path}" can't be converted to ${to === "css" ? "CSS" : "JS"}. ${refused.map((note) => `\`${note.at}\`: ${note.reason}`).join(" ")}`;
515
- }
516
485
  //#endregion
517
- export { convertMotion, convertMotionPreset, planMotionConversion };
486
+ export { convertMotion };
@@ -3,6 +3,7 @@ import { composedValueFormOf } from "./composed-values.js";
3
3
  import { createSliceMemo } from "../../framework/memo.js";
4
4
  import { tokenValueRow } from "./token-values.js";
5
5
  import { MOTION_EASING_NAMES, bezierPoints, motionOfCssKeyword } from "./easings.js";
6
+ import { webMotionBody } from "./motion-types.js";
6
7
  import { inlineMotions } from "./motion-variants.js";
7
8
  import { isReducedMotion } from "./reduced-motion.js";
8
9
  import { jsMotionPropFields } from "./Motion.js";
@@ -221,8 +222,8 @@ const resolveJsMotionPresets = createSliceMemo({
221
222
  const inline = {};
222
223
  const tokens = [];
223
224
  for (const preset of config.list("motion")) {
224
- const body = preset.toJSON();
225
- if (body.runtime !== "js") continue;
225
+ const body = webMotionBody(preset.toJSON());
226
+ if (body.type !== "motion/react") continue;
226
227
  presets[preset.path] = resolveBody(config, preset.path, body, tokens);
227
228
  }
228
229
  for (const entry of inlineMotions(config).valid) inline[entry.id] = resolveBody(config, entry.id, entry.body, tokens);
@@ -267,7 +268,7 @@ const presetReducedMotions = createSliceMemo({
267
268
  compute: (config) => {
268
269
  const out = {};
269
270
  for (const preset of config.list("motion")) {
270
- const { reducedMotion } = preset.toJSON();
271
+ const { reducedMotion } = webMotionBody(preset.toJSON());
271
272
  if (isReducedMotion(reducedMotion)) out[preset.path] = reducedMotion;
272
273
  }
273
274
  for (const entry of inlineMotions(config).valid) if (isReducedMotion(entry.body.reducedMotion)) out[entry.id] = entry.body.reducedMotion;
@@ -0,0 +1,7 @@
1
+ import { z } from "zod";
2
+
3
+ //#region src/entities/system/motion-types.d.ts
4
+ /** Read a typed preset's payload without translating between implementations. */
5
+ declare function webMotionBody(body: Record<string, unknown>): Record<string, unknown>;
6
+ //#endregion
7
+ export { webMotionBody };
@@ -0,0 +1,132 @@
1
+ import { refTo } from "../../framework/utils/refs.js";
2
+ import { isCssProperty } from "../../css/values.js";
3
+ import { reanimatedStyle } from "./reanimated-motion.js";
4
+ import { z } from "zod";
5
+ //#region src/entities/system/motion-types.ts
6
+ const number = z.union([z.number(), refTo("token")]);
7
+ const positive = z.union([z.number().positive(), refTo("token")]);
8
+ const nonnegative = z.union([z.number().nonnegative(), refTo("token")]);
9
+ /** Compatibility for the initial hook-based experiment; new presets use reanimatedStyle. */
10
+ const legacyReanimatedValue = z.discriminatedUnion("method", [z.strictObject({
11
+ method: z.literal("withTiming"),
12
+ config: z.strictObject({ duration: nonnegative.optional() })
13
+ }), z.strictObject({
14
+ method: z.literal("withSpring"),
15
+ config: z.strictObject({
16
+ stiffness: positive.optional(),
17
+ damping: nonnegative.optional(),
18
+ mass: positive.optional(),
19
+ overshootClamping: z.boolean().optional(),
20
+ energyThreshold: positive.optional(),
21
+ velocity: number.optional()
22
+ })
23
+ })]);
24
+ const reanimatedMotion = z.strictObject({
25
+ type: z.literal("react-native-reanimated").describe("The animation implementation that reads this preset."),
26
+ value: z.union([reanimatedStyle, legacyReanimatedValue]).describe("Serializable Reanimated transitions and keyframes, or a legacy timing/spring preset."),
27
+ reducedMotion: z.enum([
28
+ "user",
29
+ "always",
30
+ "never"
31
+ ]).optional().describe("Whether native motion follows the OS preference, is always reduced, or is never reduced.")
32
+ });
33
+ /** CSS keyframes and CSS animation longhands. No Framer gesture or transition vocabulary. */
34
+ const cssMotionValue = z.strictObject({
35
+ keyframes: z.record(z.string(), z.record(z.string().refine((key) => isCssProperty(key) || key.startsWith("--"), "Expected a CSS property; use translate instead of x/y."), z.union([
36
+ z.string(),
37
+ z.number(),
38
+ refTo("token")
39
+ ]))).optional(),
40
+ transitionProperty: z.string().optional(),
41
+ transitionDuration: z.string().optional(),
42
+ transitionDelay: z.string().optional(),
43
+ transitionTimingFunction: z.string().optional(),
44
+ transitionBehavior: z.enum(["normal", "allow-discrete"]).optional(),
45
+ animationDuration: z.string().optional(),
46
+ animationDelay: z.string().optional(),
47
+ animationTimingFunction: z.string().optional(),
48
+ animationIterationCount: z.union([z.number(), z.literal("infinite")]).optional(),
49
+ animationDirection: z.enum([
50
+ "normal",
51
+ "reverse",
52
+ "alternate",
53
+ "alternate-reverse"
54
+ ]).optional(),
55
+ animationFillMode: z.enum([
56
+ "none",
57
+ "forwards",
58
+ "backwards",
59
+ "both"
60
+ ]).optional()
61
+ });
62
+ /** Read a typed preset's payload without translating between implementations. */
63
+ function webMotionBody(body) {
64
+ if (body.type === void 0) return {
65
+ ...body,
66
+ type: body.runtime === "js" ? "motion/react" : body.runtime
67
+ };
68
+ return {
69
+ ...body.value,
70
+ type: body.type,
71
+ runtime: body.type === "motion/react" ? "js" : body.type,
72
+ ...body.reducedMotion === void 0 ? {} : { reducedMotion: body.reducedMotion }
73
+ };
74
+ }
75
+ /** Serializable subset of Motion's transition options, with seconds for time values. */
76
+ const transitionOptions = z.strictObject({
77
+ type: z.union([z.enum([
78
+ "tween",
79
+ "spring",
80
+ "inertia"
81
+ ]), z.literal(false)]).optional(),
82
+ duration: nonnegative.optional(),
83
+ delay: number.optional(),
84
+ ease: z.union([
85
+ z.string(),
86
+ z.tuple([
87
+ z.number(),
88
+ z.number(),
89
+ z.number(),
90
+ z.number()
91
+ ]),
92
+ refTo("token"),
93
+ z.array(z.union([z.string(), z.tuple([
94
+ z.number(),
95
+ z.number(),
96
+ z.number(),
97
+ z.number()
98
+ ])]))
99
+ ]).optional(),
100
+ times: z.array(z.number()).optional(),
101
+ repeat: nonnegative.optional(),
102
+ repeatType: z.enum([
103
+ "loop",
104
+ "reverse",
105
+ "mirror"
106
+ ]).optional(),
107
+ repeatDelay: nonnegative.optional(),
108
+ stiffness: positive.optional(),
109
+ damping: nonnegative.optional(),
110
+ mass: positive.optional(),
111
+ velocity: number.optional(),
112
+ bounce: number.optional(),
113
+ visualDuration: nonnegative.optional(),
114
+ restSpeed: nonnegative.optional(),
115
+ restDelta: nonnegative.optional(),
116
+ when: z.enum(["beforeChildren", "afterChildren"]).optional(),
117
+ delayChildren: number.optional(),
118
+ staggerChildren: number.optional(),
119
+ staggerDirection: z.union([z.literal(1), z.literal(-1)]).optional()
120
+ });
121
+ const motionTransition = transitionOptions.loose().superRefine((value, ctx) => {
122
+ for (const [key, options] of Object.entries(value)) {
123
+ if (key in transitionOptions.shape) continue;
124
+ if (!transitionOptions.safeParse(options).success) ctx.addIssue({
125
+ code: "custom",
126
+ path: [key],
127
+ message: "Expected per-property Motion transition options."
128
+ });
129
+ }
130
+ });
131
+ //#endregion
132
+ export { cssMotionValue, motionTransition, reanimatedMotion, webMotionBody };