@pixerate/editor 0.26.1 → 0.28.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.
Files changed (90) hide show
  1. package/README.md +32 -3
  2. package/dist/agent/index.cjs +56 -6
  3. package/dist/agent/index.cjs.map +1 -1
  4. package/dist/agent/index.d.cts +6 -4
  5. package/dist/agent/index.d.ts +6 -4
  6. package/dist/agent/index.js +51 -1
  7. package/dist/agent/index.js.map +1 -1
  8. package/dist/animation/index.cjs +62 -0
  9. package/dist/animation/index.cjs.map +1 -0
  10. package/dist/animation/index.d.cts +296 -0
  11. package/dist/animation/index.d.ts +296 -0
  12. package/dist/animation/index.js +62 -0
  13. package/dist/animation/index.js.map +1 -0
  14. package/dist/{builtins-Chj6zv_O.d.ts → builtins-haN82vjT.d.ts} +33 -3
  15. package/dist/{builtins-CdPcLwwF.d.cts → builtins-nMXh9sT3.d.cts} +33 -3
  16. package/dist/canvas/index.cjs +535 -737
  17. package/dist/canvas/index.cjs.map +1 -1
  18. package/dist/canvas/index.d.cts +74 -291
  19. package/dist/canvas/index.d.ts +74 -291
  20. package/dist/canvas/index.js +417 -619
  21. package/dist/canvas/index.js.map +1 -1
  22. package/dist/{chunk-X7HXL2SB.js → chunk-3RPOW4CS.js} +342 -16
  23. package/dist/chunk-3RPOW4CS.js.map +1 -0
  24. package/dist/{chunk-UBCFIABU.js → chunk-5H2OPMXZ.js} +183 -6
  25. package/dist/chunk-5H2OPMXZ.js.map +1 -0
  26. package/dist/chunk-A6PHCAGX.cjs +87 -0
  27. package/dist/chunk-A6PHCAGX.cjs.map +1 -0
  28. package/dist/{chunk-HRMEDT2L.js → chunk-D6AVOTSD.js} +6 -85
  29. package/dist/chunk-D6AVOTSD.js.map +1 -0
  30. package/dist/chunk-HXDMZQTD.cjs +497 -0
  31. package/dist/chunk-HXDMZQTD.cjs.map +1 -0
  32. package/dist/chunk-IB3ZHQAX.js +497 -0
  33. package/dist/chunk-IB3ZHQAX.js.map +1 -0
  34. package/dist/{chunk-4BVP64JJ.cjs → chunk-K2UYKJ6M.cjs} +24 -103
  35. package/dist/chunk-K2UYKJ6M.cjs.map +1 -0
  36. package/dist/{chunk-HDYMLS3X.cjs → chunk-MC2LQN5S.cjs} +196 -19
  37. package/dist/chunk-MC2LQN5S.cjs.map +1 -0
  38. package/dist/{chunk-XQFHLT53.js → chunk-MXZJ4PKN.js} +24 -10
  39. package/dist/chunk-MXZJ4PKN.js.map +1 -0
  40. package/dist/{chunk-LN2POBHT.cjs → chunk-OYEFRO3D.cjs} +28 -14
  41. package/dist/chunk-OYEFRO3D.cjs.map +1 -0
  42. package/dist/chunk-PWJARCI4.js +637 -0
  43. package/dist/chunk-PWJARCI4.js.map +1 -0
  44. package/dist/chunk-TGD52FEZ.js +87 -0
  45. package/dist/chunk-TGD52FEZ.js.map +1 -0
  46. package/dist/chunk-TTYCN6ZH.cjs +637 -0
  47. package/dist/chunk-TTYCN6ZH.cjs.map +1 -0
  48. package/dist/{chunk-NQF32I6Z.cjs → chunk-U7VGDSNY.cjs} +463 -137
  49. package/dist/chunk-U7VGDSNY.cjs.map +1 -0
  50. package/dist/commands/index.cjs +30 -410
  51. package/dist/commands/index.cjs.map +1 -1
  52. package/dist/commands/index.d.cts +1 -1
  53. package/dist/commands/index.d.ts +1 -1
  54. package/dist/commands/index.js +17 -397
  55. package/dist/commands/index.js.map +1 -1
  56. package/dist/{controller-CxXGOEmy.d.ts → controller-w6eRTjF8.d.cts} +67 -2
  57. package/dist/{controller-CxXGOEmy.d.cts → controller-w6eRTjF8.d.ts} +67 -2
  58. package/dist/extensions/index.cjs +25 -7
  59. package/dist/extensions/index.cjs.map +1 -1
  60. package/dist/extensions/index.d.cts +1 -1
  61. package/dist/extensions/index.d.ts +1 -1
  62. package/dist/extensions/index.js +32 -14
  63. package/dist/generation/index.cjs +3 -3
  64. package/dist/generation/index.cjs.map +1 -1
  65. package/dist/generation/index.js +3 -3
  66. package/dist/{index-jcJwmBYt.d.cts → index-C6gaQRH0.d.cts} +53 -4
  67. package/dist/{index-B273Zllk.d.ts → index-DXAv_R08.d.ts} +53 -4
  68. package/dist/index.cjs +57 -19
  69. package/dist/index.cjs.map +1 -1
  70. package/dist/index.d.cts +7 -5
  71. package/dist/index.d.ts +7 -5
  72. package/dist/index.js +132 -94
  73. package/dist/index.js.map +1 -1
  74. package/dist/spreadsheet/index.cjs +23 -3
  75. package/dist/spreadsheet/index.cjs.map +1 -1
  76. package/dist/spreadsheet/index.d.cts +76 -3
  77. package/dist/spreadsheet/index.d.ts +76 -3
  78. package/dist/spreadsheet/index.js +28 -8
  79. package/dist/tokens/index.cjs +4 -2
  80. package/dist/tokens/index.cjs.map +1 -1
  81. package/dist/tokens/index.js +6 -4
  82. package/package.json +6 -1
  83. package/dist/chunk-4BVP64JJ.cjs.map +0 -1
  84. package/dist/chunk-HDYMLS3X.cjs.map +0 -1
  85. package/dist/chunk-HRMEDT2L.js.map +0 -1
  86. package/dist/chunk-LN2POBHT.cjs.map +0 -1
  87. package/dist/chunk-NQF32I6Z.cjs.map +0 -1
  88. package/dist/chunk-UBCFIABU.js.map +0 -1
  89. package/dist/chunk-X7HXL2SB.js.map +0 -1
  90. package/dist/chunk-XQFHLT53.js.map +0 -1
@@ -1,17 +1,3 @@
1
- import {
2
- DEFAULT_ARRANGE_SHORTCUTS,
3
- matchArrangeShortcut
4
- } from "../chunk-7DWMJCAT.js";
5
- import {
6
- TokenError,
7
- getTokenCollection,
8
- getTokenVariable,
9
- hexToRgba,
10
- isTokenInScope,
11
- normalizeTokenColor,
12
- resolveToken,
13
- rgbaToHex
14
- } from "../chunk-HRMEDT2L.js";
15
1
  import {
16
2
  DEFAULT_NODE_HEIGHT,
17
3
  DEFAULT_NODE_WIDTH,
@@ -27,79 +13,50 @@ import {
27
13
  overlaps,
28
14
  placeBeside
29
15
  } from "../chunk-VHZCXHS4.js";
30
-
31
- // src/canvas/clock.ts
32
- var pageHidden = () => typeof document !== "undefined" && document.hidden;
33
- var realClock = {
34
- live: true,
35
- now: () => typeof performance !== "undefined" ? performance.now() : Date.now(),
36
- frame: () => new Promise((resolve) => {
37
- if (typeof requestAnimationFrame === "function" && !pageHidden()) {
38
- requestAnimationFrame(() => resolve());
39
- } else {
40
- setTimeout(resolve, 16);
41
- }
42
- }),
43
- sleep: (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)))
44
- };
45
- var settle = () => new Promise((resolve) => setTimeout(resolve, 0));
46
- var ManualClock = class {
47
- live = false;
48
- time;
49
- frames = [];
50
- sleepers = [];
51
- seq = 0;
52
- constructor(start = 0) {
53
- this.time = start;
54
- }
55
- now() {
56
- return this.time;
57
- }
58
- frame() {
59
- return new Promise((resolve) => this.frames.push(resolve));
60
- }
61
- sleep(ms) {
62
- return new Promise(
63
- (resolve) => this.sleepers.push({
64
- at: this.time + Math.max(0, ms),
65
- seq: this.seq++,
66
- resolve
67
- })
68
- );
69
- }
70
- /** Whether any frames or sleep timers are currently pending. */
71
- get pending() {
72
- return this.frames.length > 0 || this.sleepers.length > 0;
73
- }
74
- /** Advances time by `ms`, resolving due sleep timers in order, followed by queued frames. */
75
- async advance(ms) {
76
- const end = this.time + Math.max(0, ms);
77
- await settle();
78
- while (true) {
79
- let next;
80
- for (const s of this.sleepers) {
81
- if (s.at <= end && (!next || s.at < next.at || s.at === next.at && s.seq < next.seq)) {
82
- next = s;
83
- }
84
- }
85
- if (!next) break;
86
- this.sleepers.splice(this.sleepers.indexOf(next), 1);
87
- this.time = Math.max(this.time, next.at);
88
- next.resolve();
89
- await settle();
90
- }
91
- this.time = end;
92
- const dueFrames = this.frames.splice(0, this.frames.length);
93
- for (const resolveFrame of dueFrames) {
94
- resolveFrame();
95
- }
96
- await settle();
97
- }
98
- /** Convenience method to advance by 1 frame at a given fps (default: 60fps = 16.67ms). */
99
- async tick(fps = 60) {
100
- await this.advance(Math.round(1e3 / fps));
101
- }
102
- };
16
+ import {
17
+ DEFAULT_ARRANGE_SHORTCUTS,
18
+ matchArrangeShortcut
19
+ } from "../chunk-7DWMJCAT.js";
20
+ import {
21
+ TokenError,
22
+ getTokenCollection,
23
+ getTokenVariable,
24
+ isTokenInScope,
25
+ resolveToken
26
+ } from "../chunk-D6AVOTSD.js";
27
+ import {
28
+ ANIMATION_EASING_LABELS,
29
+ ANIMATION_EASING_PRESETS,
30
+ ANIMATION_TIME_EPSILON,
31
+ AnimationPlayer,
32
+ CUBIC_BEZIER_PRESETS,
33
+ ManualClock,
34
+ createAnimationPlayer,
35
+ cubicBezierAt,
36
+ easingFromInfluence,
37
+ easingLabel,
38
+ easingToCss,
39
+ evaluateEasing,
40
+ evaluateTrack,
41
+ formatOklab,
42
+ interpolateAnimationValue,
43
+ isAnimationEasing,
44
+ isAnimationKeyframe,
45
+ mixColorsOklab,
46
+ normalizeKeyframes,
47
+ oklabToRgb,
48
+ pageHidden,
49
+ parseCssEasing,
50
+ parseOklab,
51
+ realClock,
52
+ resolveEasing,
53
+ rgbToOklab,
54
+ springAt,
55
+ wrapAnimationTime
56
+ } from "../chunk-PWJARCI4.js";
57
+ import {
58
+ normalizeTokenColor
59
+ } from "../chunk-TGD52FEZ.js";
103
60
 
104
61
  // src/canvas/trajectory.ts
105
62
  var defaultEasing = (t) => --t * t * t + 1;
@@ -2277,7 +2234,7 @@ function fitSingleFrom(pts, u, t0, t1) {
2277
2234
  let best = generate(pts, u, t0, t1);
2278
2235
  let [bestErr, bestSplit] = maxError(pts, best, u);
2279
2236
  const params = u.slice();
2280
- for (let round3 = 0; round3 < MAX_REPARAM; round3++) {
2237
+ for (let round2 = 0; round2 < MAX_REPARAM; round2++) {
2281
2238
  for (let i = 0; i < params.length; i++) params[i] = newton(best, pts[i], params[i]);
2282
2239
  const c = generate(pts, params, t0, t1);
2283
2240
  const [e, s] = maxError(pts, c, params);
@@ -7843,244 +7800,8 @@ function pruneCanvasTokenModes(nodes, state) {
7843
7800
  return changed ? out : nodes;
7844
7801
  }
7845
7802
 
7846
- // src/canvas/animation/easing.ts
7847
- var ANIMATION_EASING_PRESETS = [
7848
- "linear",
7849
- "ease",
7850
- "ease-in",
7851
- "ease-out",
7852
- "ease-in-out",
7853
- "ease-in-back",
7854
- "ease-out-back",
7855
- "spring",
7856
- "hold"
7857
- ];
7858
- var ANIMATION_EASING_LABELS = {
7859
- linear: "Linear",
7860
- hold: "Hold",
7861
- ease: "Ease",
7862
- "ease-in": "Ease in",
7863
- "ease-out": "Ease out",
7864
- "ease-in-out": "Ease in-out",
7865
- "ease-in-back": "Back in",
7866
- "ease-out-back": "Back out",
7867
- spring: "Spring"
7868
- };
7869
- var CUBIC_BEZIER_PRESETS = {
7870
- linear: [0, 0, 1, 1],
7871
- ease: [0.25, 0.1, 0.25, 1],
7872
- "ease-in": [0.42, 0, 1, 1],
7873
- "ease-out": [0, 0, 0.58, 1],
7874
- "ease-in-out": [0.42, 0, 0.58, 1],
7875
- "ease-in-back": [0.36, 0, 0.66, -0.56],
7876
- "ease-out-back": [0.34, 1.56, 0.64, 1]
7877
- };
7878
- var DEFAULT_SPRING = { stiffness: 170, damping: 26, mass: 1, velocity: 0 };
7879
- var clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
7880
- var finite2 = (v, fallback) => typeof v === "number" && Number.isFinite(v) ? v : fallback;
7881
- function cubicBezierAt(x1, y1, x2, y2, x) {
7882
- if (x <= 0) return 0;
7883
- if (x >= 1) return 1;
7884
- x1 = clamp(x1, 0, 1);
7885
- x2 = clamp(x2, 0, 1);
7886
- const bx = (s2) => 3 * (1 - s2) * (1 - s2) * s2 * x1 + 3 * (1 - s2) * s2 * s2 * x2 + s2 * s2 * s2;
7887
- const by = (s2) => 3 * (1 - s2) * (1 - s2) * s2 * y1 + 3 * (1 - s2) * s2 * s2 * y2 + s2 * s2 * s2;
7888
- const dbx = (s2) => 3 * (1 - s2) * (1 - s2) * x1 + 6 * (1 - s2) * s2 * (x2 - x1) + 3 * s2 * s2 * (1 - x2);
7889
- let s = x;
7890
- for (let i = 0; i < 8; i++) {
7891
- const d = dbx(s);
7892
- if (Math.abs(d) < 1e-9) break;
7893
- const next = s - (bx(s) - x) / d;
7894
- if (!(next >= 0 && next <= 1)) break;
7895
- s = next;
7896
- }
7897
- if (Math.abs(bx(s) - x) > 1e-7) {
7898
- let lo = 0;
7899
- let hi = 1;
7900
- for (let i = 0; i < 60; i++) {
7901
- s = (lo + hi) / 2;
7902
- if (bx(s) < x) lo = s;
7903
- else hi = s;
7904
- }
7905
- }
7906
- return by(s);
7907
- }
7908
- function springAt(spring, seconds) {
7909
- const stiffness = Math.max(1e-6, finite2(spring.stiffness, DEFAULT_SPRING.stiffness));
7910
- const damping = Math.max(0, finite2(spring.damping, DEFAULT_SPRING.damping));
7911
- const mass = Math.max(1e-6, finite2(spring.mass, DEFAULT_SPRING.mass));
7912
- const v0 = -finite2(spring.velocity, DEFAULT_SPRING.velocity);
7913
- const t = Math.max(0, seconds);
7914
- const w0 = Math.sqrt(stiffness / mass);
7915
- const zeta = damping / (2 * Math.sqrt(stiffness * mass));
7916
- let x;
7917
- if (zeta < 1 - 1e-6) {
7918
- const wd = w0 * Math.sqrt(1 - zeta * zeta);
7919
- x = Math.exp(-zeta * w0 * t) * (Math.cos(wd * t) + (zeta * w0 + v0) / wd * Math.sin(wd * t));
7920
- } else if (zeta <= 1 + 1e-6) {
7921
- x = Math.exp(-w0 * t) * (1 + (w0 + v0) * t);
7922
- } else {
7923
- const r = Math.sqrt(zeta * zeta - 1);
7924
- const s1 = -w0 * (zeta - r);
7925
- const s2 = -w0 * (zeta + r);
7926
- const c2 = (v0 - s1) / (s2 - s1);
7927
- const c1 = 1 - c2;
7928
- x = c1 * Math.exp(s1 * t) + c2 * Math.exp(s2 * t);
7929
- }
7930
- return 1 - x;
7931
- }
7932
- function resolveEasing(easing) {
7933
- if (easing === "hold") return "hold";
7934
- if (easing === "spring") return { type: "spring", ...DEFAULT_SPRING };
7935
- if (typeof easing === "string") {
7936
- const preset = CUBIC_BEZIER_PRESETS[easing];
7937
- if (preset) return { type: "cubic-bezier", x1: preset[0], y1: preset[1], x2: preset[2], y2: preset[3] };
7938
- const parsed = parseCssEasing(easing);
7939
- return parsed ? resolveEasing(parsed) : { type: "cubic-bezier", x1: 0, y1: 0, x2: 1, y2: 1 };
7940
- }
7941
- if (easing && typeof easing === "object") {
7942
- if (easing.type === "spring") {
7943
- return {
7944
- type: "spring",
7945
- stiffness: finite2(easing.stiffness, DEFAULT_SPRING.stiffness),
7946
- damping: finite2(easing.damping, DEFAULT_SPRING.damping),
7947
- mass: finite2(easing.mass, DEFAULT_SPRING.mass),
7948
- velocity: finite2(easing.velocity, DEFAULT_SPRING.velocity)
7949
- };
7950
- }
7951
- if (easing.type === "cubic-bezier") {
7952
- return {
7953
- type: "cubic-bezier",
7954
- x1: clamp(finite2(easing.x1, 0), 0, 1),
7955
- y1: finite2(easing.y1, 0),
7956
- x2: clamp(finite2(easing.x2, 1), 0, 1),
7957
- y2: finite2(easing.y2, 1)
7958
- };
7959
- }
7960
- }
7961
- return { type: "cubic-bezier", x1: 0, y1: 0, x2: 1, y2: 1 };
7962
- }
7963
- function evaluateEasing(easing, u, durationMs = 1e3) {
7964
- if (!(u > 0)) return 0;
7965
- if (u >= 1) return 1;
7966
- const resolved = resolveEasing(easing);
7967
- if (resolved === "hold") return 0;
7968
- if (resolved.type === "spring") return springAt(resolved, u * Math.max(0, durationMs) / 1e3);
7969
- const { x1, y1, x2, y2 } = resolved;
7970
- if (x1 === y1 && x2 === y2) return u;
7971
- return cubicBezierAt(x1, y1, x2, y2, u);
7972
- }
7973
- function easingFromInfluence(outInfluence = 1 / 3, inInfluence = 1 / 3) {
7974
- return {
7975
- type: "cubic-bezier",
7976
- x1: clamp(outInfluence, 0.01, 1),
7977
- y1: 0,
7978
- x2: 1 - clamp(inInfluence, 0.01, 1),
7979
- y2: 1
7980
- };
7981
- }
7982
- var NUMBER = String.raw`\s*(-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?)\s*`;
7983
- var CUBIC_RE = new RegExp(`^cubic-bezier\\(${NUMBER},${NUMBER},${NUMBER},${NUMBER}\\)$`, "i");
7984
- function parseCssEasing(text) {
7985
- const value = text.trim().toLowerCase();
7986
- if (value.length > 200) return null;
7987
- if (ANIMATION_EASING_PRESETS.includes(value)) return value;
7988
- if (value === "step-end" || /^steps\(\s*1\s*(,\s*(end|jump-end)\s*)?\)$/.test(value)) return "hold";
7989
- const m = CUBIC_RE.exec(value);
7990
- if (!m) return null;
7991
- const [x1, y1, x2, y2] = m.slice(1).map(Number);
7992
- if (![x1, y1, x2, y2].every(Number.isFinite) || x1 < 0 || x1 > 1 || x2 < 0 || x2 > 1) return null;
7993
- return { type: "cubic-bezier", x1, y1, x2, y2 };
7994
- }
7995
- function isAnimationEasing(value) {
7996
- if (typeof value === "string") return parseCssEasing(value) !== null;
7997
- if (typeof value !== "object" || value === null) return false;
7998
- const e = value;
7999
- if (e.type === "spring") return true;
8000
- return e.type === "cubic-bezier" && [e.x1, e.y1, e.x2, e.y2].every((v) => typeof v === "number" && Number.isFinite(v));
8001
- }
8002
- var round = (v, digits = 4) => {
8003
- const p = 10 ** digits;
8004
- return Math.round(v * p) / p;
8005
- };
8006
- function easingToCss(easing, durationMs = 1e3, samples = 24) {
8007
- const resolved = resolveEasing(easing);
8008
- if (resolved === "hold") return "steps(1, end)";
8009
- if (resolved.type === "spring") {
8010
- const n = Math.max(2, Math.floor(samples));
8011
- const points = [];
8012
- for (let i = 0; i <= n; i++) points.push(String(round(i === n ? 1 : springAt(resolved, i / n * durationMs / 1e3))));
8013
- return `linear(${points.join(", ")})`;
8014
- }
8015
- const { x1, y1, x2, y2 } = resolved;
8016
- for (const [name, p] of Object.entries(CUBIC_BEZIER_PRESETS)) {
8017
- if (["linear", "ease", "ease-in", "ease-out", "ease-in-out"].includes(name) && p[0] === x1 && p[1] === y1 && p[2] === x2 && p[3] === y2) {
8018
- return name;
8019
- }
8020
- }
8021
- return `cubic-bezier(${round(x1)}, ${round(y1)}, ${round(x2)}, ${round(y2)})`;
8022
- }
8023
- function easingLabel(easing) {
8024
- if (easing === void 0 || easing === null) return ANIMATION_EASING_LABELS.linear;
8025
- if (typeof easing === "string") return ANIMATION_EASING_LABELS[easing] ?? easing;
8026
- return easing.type === "spring" ? "Spring" : "Custom";
8027
- }
8028
-
8029
- // src/canvas/animation/color.ts
8030
- var toLinear = (c) => c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
8031
- var toGamma = (c) => c <= 31308e-7 ? 12.92 * c : 1.055 * Math.sign(c) * Math.abs(c) ** (1 / 2.4) - 0.055;
8032
- function rgbToOklab(r, g, b, alpha = 1) {
8033
- const lr = toLinear(r);
8034
- const lg = toLinear(g);
8035
- const lb = toLinear(b);
8036
- const l = Math.cbrt(0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb);
8037
- const m = Math.cbrt(0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb);
8038
- const s = Math.cbrt(0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb);
8039
- return {
8040
- l: 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
8041
- a: 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
8042
- b: 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
8043
- alpha
8044
- };
8045
- }
8046
- function oklabToRgb(color) {
8047
- const l_ = color.l + 0.3963377774 * color.a + 0.2158037573 * color.b;
8048
- const m_ = color.l - 0.1055613458 * color.a - 0.0638541728 * color.b;
8049
- const s_ = color.l - 0.0894841775 * color.a - 1.291485548 * color.b;
8050
- const l = l_ ** 3;
8051
- const m = m_ ** 3;
8052
- const s = s_ ** 3;
8053
- const clamp01 = (v) => Math.min(1, Math.max(0, v));
8054
- return {
8055
- r: clamp01(toGamma(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s)),
8056
- g: clamp01(toGamma(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s)),
8057
- b: clamp01(toGamma(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s)),
8058
- a: clamp01(color.alpha)
8059
- };
8060
- }
8061
- function parseOklab(input) {
8062
- const hex = normalizeTokenColor(input);
8063
- if (hex === null) return null;
8064
- const c = hexToRgba(hex);
8065
- return rgbToOklab(c.r, c.g, c.b, c.a);
8066
- }
8067
- function formatOklab(color) {
8068
- const { r, g, b, a } = oklabToRgb(color);
8069
- return rgbaToHex(r, g, b, a);
8070
- }
8071
- function mixColorsOklab(from, to, t) {
8072
- const a = parseOklab(from);
8073
- const b = parseOklab(to);
8074
- if (!a || !b) return null;
8075
- const alpha = a.alpha + (b.alpha - a.alpha) * t;
8076
- const premul = (k) => a[k] * a.alpha + (b[k] * b.alpha - a[k] * a.alpha) * t;
8077
- if (alpha <= 1e-9) return formatOklab({ l: a.l + (b.l - a.l) * t, a: 0, b: 0, alpha: 0 });
8078
- return formatOklab({ l: premul("l") / alpha, a: premul("a") / alpha, b: premul("b") / alpha, alpha });
8079
- }
8080
-
8081
7803
  // src/canvas/animation/model.ts
8082
7804
  var ANIMATION_DATA_KEY = "animation";
8083
- var ANIMATION_TIME_EPSILON = 1e-6;
8084
7805
  var num = (value) => {
8085
7806
  if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
8086
7807
  if (typeof value === "string") {
@@ -8221,37 +7942,6 @@ function clampAnimationValue(property, value) {
8221
7942
  if (property.max !== void 0) v = Math.min(property.max, v);
8222
7943
  return v;
8223
7944
  }
8224
- function isValue(value) {
8225
- return typeof value === "number" && Number.isFinite(value) || typeof value === "string";
8226
- }
8227
- function isAnimationKeyframe(value) {
8228
- if (typeof value !== "object" || value === null) return false;
8229
- const k = value;
8230
- return typeof k.time === "number" && Number.isFinite(k.time) && isValue(k.value) && (k.easing === void 0 || isAnimationEasing(k.easing));
8231
- }
8232
- var sortedCache = /* @__PURE__ */ new WeakMap();
8233
- function normalizeKeyframes(keyframes) {
8234
- if (!Array.isArray(keyframes)) return [];
8235
- const cached = sortedCache.get(keyframes);
8236
- if (cached && cached.items.length === keyframes.length && cached.items.every((k, i) => k === keyframes[i])) return cached.out;
8237
- const indexed = keyframes.map((k, i) => ({ k, i })).filter(({ k }) => isAnimationKeyframe(k)).sort((a, b) => a.k.time - b.k.time || a.i - b.i);
8238
- const out = [];
8239
- let lastIndex = -1;
8240
- for (const { k, i } of indexed) {
8241
- const prev = out[out.length - 1];
8242
- if (prev && Math.abs(k.time - prev.time) <= ANIMATION_TIME_EPSILON) {
8243
- if (i > lastIndex) {
8244
- out[out.length - 1] = k;
8245
- lastIndex = i;
8246
- }
8247
- continue;
8248
- }
8249
- out.push(k);
8250
- lastIndex = i;
8251
- }
8252
- sortedCache.set(keyframes, { items: [...keyframes], out });
8253
- return out;
8254
- }
8255
7945
  function getNodeAnimation(node) {
8256
7946
  const raw = node?.data?.[ANIMATION_DATA_KEY];
8257
7947
  if (typeof raw !== "object" || raw === null || typeof raw.tracks !== "object" || raw.tracks === null) return null;
@@ -8289,45 +7979,6 @@ function withNodeAnimation(node, animation) {
8289
7979
 
8290
7980
  // src/canvas/animation/evaluate.ts
8291
7981
  var nodesOf = (doc) => Array.isArray(doc) ? doc : doc.nodes;
8292
- function interpolateAnimationValue(from, to, t, type = "number") {
8293
- if (typeof from === "number" && typeof to === "number") return from + (to - from) * t;
8294
- if (t <= 0) return from;
8295
- if (t >= 1) return to;
8296
- if (type === "color" || typeof from === "string" && typeof to === "string") {
8297
- const mixed = mixColorsOklab(from, to, t);
8298
- if (mixed !== null) return mixed;
8299
- }
8300
- return from;
8301
- }
8302
- function segmentIndex(keys, time) {
8303
- let lo = 0;
8304
- let hi = keys.length - 1;
8305
- let found = -1;
8306
- while (lo <= hi) {
8307
- const mid = lo + hi >> 1;
8308
- if (keys[mid].time <= time) {
8309
- found = mid;
8310
- lo = mid + 1;
8311
- } else hi = mid - 1;
8312
- }
8313
- return found;
8314
- }
8315
- function evaluateTrack(track, time, type = "number") {
8316
- const keyframes = Array.isArray(track) ? track : track?.keyframes;
8317
- const keys = normalizeKeyframes(keyframes);
8318
- if (keys.length === 0) return void 0;
8319
- if (!(time > keys[0].time)) return keys[0].value;
8320
- const last = keys[keys.length - 1];
8321
- if (time >= last.time) return last.value;
8322
- const i = segmentIndex(keys, time);
8323
- const a = keys[i];
8324
- const b = keys[i + 1];
8325
- if (time === a.time) return a.value;
8326
- const span = b.time - a.time;
8327
- const u = (time - a.time) / span;
8328
- if (a.easing === "hold") return a.value;
8329
- return interpolateAnimationValue(a.value, b.value, evaluateEasing(a.easing, u, span), type);
8330
- }
8331
7982
  function getAnimationDuration(doc) {
8332
7983
  let end = 0;
8333
7984
  for (const node of nodesOf(doc)) {
@@ -8341,17 +7992,6 @@ function getAnimationDuration(doc) {
8341
7992
  }
8342
7993
  return end;
8343
7994
  }
8344
- function wrapAnimationTime(time, duration, loop = "none") {
8345
- if (!Number.isFinite(time)) return 0;
8346
- if (!(duration > 0)) return 0;
8347
- if (loop === "loop") return (time % duration + duration) % duration;
8348
- if (loop === "ping-pong") {
8349
- const period = duration * 2;
8350
- const m = (time % period + period) % period;
8351
- return m <= duration ? m : period - m;
8352
- }
8353
- return Math.min(duration, Math.max(0, time));
8354
- }
8355
7995
  function evaluateAnimation(doc, time, options = {}) {
8356
7996
  const only = options.ids ? new Set(options.ids) : null;
8357
7997
  const out = {};
@@ -8873,212 +8513,6 @@ function animateNode(nodes, nodeId, options) {
8873
8513
  return { nodes: draft.commit(), refs };
8874
8514
  }
8875
8515
 
8876
- // src/canvas/animation/player.ts
8877
- var AnimationPlayer = class {
8878
- clock;
8879
- durationSource;
8880
- onTime;
8881
- onEnd;
8882
- time;
8883
- playing = false;
8884
- scrubbing = false;
8885
- resumeAfterScrub = false;
8886
- loop;
8887
- rate;
8888
- /** Unwrapped time at `anchorAt` (keeps the ping-pong direction across frames). */
8889
- anchorRaw = 0;
8890
- anchorAt = 0;
8891
- generation = 0;
8892
- cached;
8893
- listeners = /* @__PURE__ */ new Set();
8894
- constructor(options = {}) {
8895
- this.clock = options.clock ?? realClock;
8896
- this.durationSource = options.duration ?? 1e3;
8897
- this.loop = options.loop ?? "none";
8898
- this.rate = Number.isFinite(options.playbackRate) && options.playbackRate !== 0 ? options.playbackRate : 1;
8899
- this.onTime = options.onTime;
8900
- this.onEnd = options.onEnd;
8901
- this.time = this.clamp(options.time ?? 0);
8902
- this.cached = this.snapshot();
8903
- }
8904
- /** Current timeline length (ms). */
8905
- get duration() {
8906
- const d = typeof this.durationSource === "function" ? this.durationSource() : this.durationSource;
8907
- return Number.isFinite(d) && d > 0 ? d : 0;
8908
- }
8909
- get currentTime() {
8910
- return this.time;
8911
- }
8912
- get isPlaying() {
8913
- return this.playing;
8914
- }
8915
- getState() {
8916
- return this.cached;
8917
- }
8918
- subscribe(listener) {
8919
- this.listeners.add(listener);
8920
- return () => {
8921
- this.listeners.delete(listener);
8922
- };
8923
- }
8924
- /** Replaces the callbacks (hooks keep them current without recreating the player). */
8925
- setCallbacks(callbacks) {
8926
- this.onTime = callbacks.onTime;
8927
- this.onEnd = callbacks.onEnd;
8928
- }
8929
- setDuration(duration) {
8930
- this.durationSource = duration;
8931
- this.time = this.clamp(this.time);
8932
- this.emit(false);
8933
- }
8934
- setLoop(loop) {
8935
- if (loop === this.loop) return;
8936
- this.reanchor();
8937
- this.loop = loop;
8938
- this.emit(false);
8939
- }
8940
- setPlaybackRate(rate) {
8941
- if (!Number.isFinite(rate) || rate === 0 || rate === this.rate) return;
8942
- this.reanchor();
8943
- this.rate = rate;
8944
- this.emit(false);
8945
- }
8946
- /** Starts playing from the playhead (from the start again when a one-shot play had ended). */
8947
- play() {
8948
- if (this.playing) return;
8949
- const d = this.duration;
8950
- if (this.loop === "none" && d > 0) {
8951
- if (this.rate > 0 && this.time >= d) this.time = 0;
8952
- if (this.rate < 0 && this.time <= 0) this.time = d;
8953
- }
8954
- this.playing = true;
8955
- this.scrubbing = false;
8956
- this.anchorRaw = this.time;
8957
- this.anchorAt = this.clock.now();
8958
- this.emit(false);
8959
- this.schedule();
8960
- }
8961
- pause() {
8962
- if (!this.playing) return;
8963
- this.tick(false);
8964
- this.playing = false;
8965
- this.generation++;
8966
- this.emit(true);
8967
- }
8968
- toggle() {
8969
- if (this.playing) this.pause();
8970
- else this.play();
8971
- }
8972
- /** Pauses and returns to the start. */
8973
- stop() {
8974
- this.playing = false;
8975
- this.generation++;
8976
- this.seek(0);
8977
- }
8978
- /** Moves the playhead (clamped to the timeline); playback continues from there. */
8979
- seek(time) {
8980
- if (!Number.isFinite(time)) return;
8981
- this.time = this.clamp(time);
8982
- this.anchorRaw = this.time;
8983
- this.anchorAt = this.clock.now();
8984
- this.emit(true);
8985
- }
8986
- /** Seeks while dragging the playhead: playback pauses until `endScrub`. */
8987
- scrub(time) {
8988
- if (!this.scrubbing) {
8989
- this.resumeAfterScrub = this.playing;
8990
- if (this.playing) {
8991
- this.playing = false;
8992
- this.generation++;
8993
- }
8994
- this.scrubbing = true;
8995
- }
8996
- this.seek(time);
8997
- }
8998
- /** Ends a scrub, resuming playback if it was playing when the scrub began. */
8999
- endScrub() {
9000
- if (!this.scrubbing) return;
9001
- this.scrubbing = false;
9002
- const resume = this.resumeAfterScrub;
9003
- this.resumeAfterScrub = false;
9004
- if (resume) this.play();
9005
- else this.emit(false);
9006
- }
9007
- /** Stops scheduling frames and drops listeners. */
9008
- destroy() {
9009
- this.playing = false;
9010
- this.generation++;
9011
- this.listeners.clear();
9012
- }
9013
- clamp(time) {
9014
- return Math.min(this.duration, Math.max(0, Number.isFinite(time) ? time : 0));
9015
- }
9016
- reanchor() {
9017
- if (!this.playing) return;
9018
- this.tick(false);
9019
- this.anchorRaw = this.rawNow();
9020
- this.anchorAt = this.clock.now();
9021
- }
9022
- rawNow() {
9023
- return this.anchorRaw + (this.clock.now() - this.anchorAt) * this.rate;
9024
- }
9025
- schedule() {
9026
- const generation = this.generation;
9027
- this.clock.frame().then(() => {
9028
- if (generation !== this.generation || !this.playing) return;
9029
- this.tick(true);
9030
- if (this.playing) this.schedule();
9031
- });
9032
- }
9033
- /** Advances the playhead to the clock's time; `notify` emits the change. */
9034
- tick(notify) {
9035
- if (!this.playing) return;
9036
- const d = this.duration;
9037
- const raw = this.rawNow();
9038
- let ended = false;
9039
- if (this.loop === "none" || d <= 0) {
9040
- if (this.rate > 0 && raw >= d) {
9041
- this.time = d;
9042
- ended = true;
9043
- } else if (this.rate < 0 && raw <= 0) {
9044
- this.time = 0;
9045
- ended = true;
9046
- } else this.time = this.clamp(raw);
9047
- } else {
9048
- this.time = wrapAnimationTime(raw, d, this.loop);
9049
- }
9050
- if (ended) {
9051
- this.playing = false;
9052
- this.generation++;
9053
- }
9054
- if (notify || ended) this.emit(true);
9055
- if (ended) this.onEnd?.();
9056
- }
9057
- snapshot() {
9058
- return {
9059
- time: this.time,
9060
- playing: this.playing,
9061
- scrubbing: this.scrubbing,
9062
- loop: this.loop,
9063
- playbackRate: this.rate,
9064
- duration: this.duration
9065
- };
9066
- }
9067
- emit(timeChanged) {
9068
- const next = this.snapshot();
9069
- const prev = this.cached;
9070
- if (prev.time === next.time && prev.playing === next.playing && prev.scrubbing === next.scrubbing && prev.loop === next.loop && prev.playbackRate === next.playbackRate && prev.duration === next.duration) {
9071
- return;
9072
- }
9073
- this.cached = next;
9074
- if (timeChanged && prev.time !== next.time) this.onTime?.(next.time);
9075
- for (const listener of this.listeners) listener(next);
9076
- }
9077
- };
9078
- function createAnimationPlayer(options) {
9079
- return new AnimationPlayer(options);
9080
- }
9081
-
9082
8516
  // src/canvas/animation/export.ts
9083
8517
  function sampleAnimationFrames(doc, options = {}) {
9084
8518
  const fps = options.fps ?? 30;
@@ -9109,22 +8543,22 @@ var CSS_ANIMATION_PROPERTIES = {
9109
8543
  width: { css: "width", camel: "width" },
9110
8544
  height: { css: "height", camel: "height" }
9111
8545
  };
9112
- var round2 = (v) => Math.round(v * 1e3) / 1e3;
8546
+ var round = (v) => Math.round(v * 1e3) / 1e3;
9113
8547
  function cssValue(source, value, node, partner, partnerSource) {
9114
8548
  if (source === "x" || source === "y") {
9115
8549
  const dx = source === "x" ? value - node.position.x : partner !== void 0 ? partner - node.position.x : 0;
9116
8550
  const dy = source === "y" ? value - node.position.y : partner !== void 0 && partnerSource === "y" ? partner - node.position.y : 0;
9117
- return `${round2(dx)}px ${round2(dy)}px`;
8551
+ return `${round(dx)}px ${round(dy)}px`;
9118
8552
  }
9119
8553
  if (typeof value === "string") return value;
9120
8554
  switch (source) {
9121
8555
  case "rotation":
9122
- return `${round2(value)}deg`;
8556
+ return `${round(value)}deg`;
9123
8557
  case "scale":
9124
8558
  case "opacity":
9125
- return String(round2(value));
8559
+ return String(round(value));
9126
8560
  default:
9127
- return `${round2(value)}px`;
8561
+ return `${round(value)}px`;
9128
8562
  }
9129
8563
  }
9130
8564
  var sameTimes = (a, b) => a.length === b.length && a.every((k, i) => Math.abs(k.time - b[i].time) < 1e-6 && JSON.stringify(k.easing ?? "linear") === JSON.stringify(b[i].easing ?? "linear"));
@@ -9184,7 +8618,7 @@ function getCssPropertyAnimations(nodes, nodeId, options = {}) {
9184
8618
  if (sources.includes("motionPath")) {
9185
8619
  const position = (t) => {
9186
8620
  const moved = ANIMATABLE_PROPERTIES.find((p) => p.key === "motionPath").write(node, evalAt("motionPath", t));
9187
- return `${round2(moved.position.x - node.position.x)}px ${round2(moved.position.y - node.position.y)}px`;
8621
+ return `${round(moved.position.x - node.position.x)}px ${round(moved.position.y - node.position.y)}px`;
9188
8622
  };
9189
8623
  out.push({ property: css, camel: camel2, sources, stops: sampledStops(duration, fps, allTimes, position), exact: false });
9190
8624
  continue;
@@ -9219,7 +8653,7 @@ function timing(options, duration) {
9219
8653
  const rate = options.playbackRate && Number.isFinite(options.playbackRate) && options.playbackRate !== 0 ? options.playbackRate : 1;
9220
8654
  const loop = options.loop ?? "none";
9221
8655
  return {
9222
- duration: round2(duration / Math.abs(rate)),
8656
+ duration: round(duration / Math.abs(rate)),
9223
8657
  iterations: loop === "none" ? 1 : Infinity,
9224
8658
  direction: loop === "ping-pong" ? rate < 0 ? "alternate-reverse" : "alternate" : rate < 0 ? "reverse" : "normal"
9225
8659
  };
@@ -9236,7 +8670,7 @@ function exportCssAnimation(nodes, nodeId, options = {}) {
9236
8670
  for (const prop of props) {
9237
8671
  const name = `${base}-${cssIdent(prop.property)}`;
9238
8672
  const lines = prop.stops.map((s) => {
9239
- const pct = `${round2(s.offset * 100)}%`;
8673
+ const pct = `${round(s.offset * 100)}%`;
9240
8674
  const easing = s.easing ? ` animation-timing-function: ${s.easing};` : "";
9241
8675
  return ` ${pct} { ${prop.property}: ${s.value};${easing} }`;
9242
8676
  });
@@ -11382,6 +10816,289 @@ function restoreCanvasMainComponent(nodes, id, options = {}) {
11382
10816
  for (const nn of nested) after = rebaseInstance(after, nn, options.tokens);
11383
10817
  return done(nodes, after, [compId], options);
11384
10818
  }
10819
+ var VARIANT_GAP = 20;
10820
+ var variantKey = (values) => formatVariantName(values);
10821
+ function variantNameProblem(kind, s) {
10822
+ const label = kind === "property" ? "Variant property names" : "Variant values";
10823
+ if (!s) return `${label} must not be empty.`;
10824
+ if (/[,=]/.test(s)) return `${label} can't contain "," or "=".`;
10825
+ return void 0;
10826
+ }
10827
+ function cleanVariantPairs(pairs) {
10828
+ const out = {};
10829
+ for (const [rawKey, rawValue] of Object.entries(pairs)) {
10830
+ if (typeof rawValue !== "string") return `"${rawKey}" takes a text value.`;
10831
+ const k = rawKey.trim();
10832
+ const v = rawValue.trim();
10833
+ const problem = variantNameProblem("property", k) ?? variantNameProblem("value", v);
10834
+ if (problem) return problem;
10835
+ out[k] = v;
10836
+ }
10837
+ return out;
10838
+ }
10839
+ function variantOrder(ids, values) {
10840
+ const order = [];
10841
+ for (const id of ids) for (const k of Object.keys(values.get(id) ?? {})) if (!order.includes(k)) order.push(k);
10842
+ return order;
10843
+ }
10844
+ function withVariantValues(node, values, order) {
10845
+ const name = formatVariantName(values, order);
10846
+ const data = { ...dataOf(node), [COMPONENT_DATA_KEY]: { ...getComponentData(node) ?? {}, variant: values }, label: name };
10847
+ if (typeof data.name === "string" && data.name.includes("=")) data.name = name;
10848
+ return same(dataOf(node), data) ? node : withData(node, data);
10849
+ }
10850
+ function closestVariant(list, wanted) {
10851
+ const entries = Object.entries(wanted);
10852
+ let best;
10853
+ const better = (a, b) => {
10854
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return a[i] > b[i];
10855
+ return false;
10856
+ };
10857
+ for (const v of list) {
10858
+ const values = getVariantValues(v);
10859
+ const matches2 = entries.map(([k, val]) => values[k] === val ? 1 : 0);
10860
+ const score = [matches2.reduce((a, b) => a + b, 0), ...matches2];
10861
+ if (!best || better(score, best.score)) best = { score, id: v.id };
10862
+ }
10863
+ return best?.id;
10864
+ }
10865
+ function variantEditIn(g, setId) {
10866
+ const set = g.get(setId);
10867
+ if (!isCanvasComponentSet(set)) return { code: "not-found", reason: "Choose a component set." };
10868
+ if (owningInstanceIn(g, setId)) return { code: "inside-instance", reason: "A component set inside an instance can't be edited: edit its main." };
10869
+ const variants = variantsIn(g, setId);
10870
+ return { set, variants, values: new Map(variants.map((v) => [v.id, getVariantValues(v)])) };
10871
+ }
10872
+ function addMissingProperties(edit, pairs, defaultValue) {
10873
+ const known = /* @__PURE__ */ new Set();
10874
+ for (const v of edit.values.values()) for (const k of Object.keys(v)) known.add(k);
10875
+ const added = Object.keys(pairs).filter((k) => !known.has(k));
10876
+ if (added.length === 0) return void 0;
10877
+ const problem = variantNameProblem("value", defaultValue);
10878
+ if (problem) return problem;
10879
+ const defs = dataOf(edit.set)[COMPONENT_SET_DATA_KEY]?.properties;
10880
+ const clash = isRecord(defs) ? added.find((k) => k in defs) : void 0;
10881
+ if (clash) return `"${clash}" is a component property.`;
10882
+ for (const [id, v] of edit.values) {
10883
+ const next = { ...v };
10884
+ for (const k of added) next[k] = defaultValue;
10885
+ edit.values.set(id, next);
10886
+ }
10887
+ return void 0;
10888
+ }
10889
+ function duplicateAmong(values, ids) {
10890
+ for (const id of ids) {
10891
+ const key = variantKey(values.get(id));
10892
+ for (const [other, v] of values) {
10893
+ if (other !== id && variantKey(v) === key) return formatVariantName(values.get(id), variantOrder([...values.keys()], values));
10894
+ }
10895
+ }
10896
+ return void 0;
10897
+ }
10898
+ function writeVariantEdit(edit, extra = []) {
10899
+ const order = variantOrder([...edit.variants.map((v) => v.id), ...extra], edit.values);
10900
+ const updates = /* @__PURE__ */ new Map();
10901
+ for (const v of edit.variants) {
10902
+ const next = withVariantValues(v, edit.values.get(v.id), order);
10903
+ if (next !== v) updates.set(v.id, next);
10904
+ }
10905
+ return updates;
10906
+ }
10907
+ function nextVariantValuesIn(g, setId, fromId) {
10908
+ const variants = variantsIn(g, setId);
10909
+ const base = getVariantValues(g.get(fromId));
10910
+ const first = variantOptionsIn(g, setId)[0];
10911
+ const prop = first?.name ?? "Property 1";
10912
+ const used = new Set(variants.map((v) => variantKey(getVariantValues(v))));
10913
+ for (let k = variants.length + 1; ; k++) {
10914
+ const values = { ...base, [prop]: `Variant${k}` };
10915
+ if (!first?.values.includes(`Variant${k}`) && !used.has(variantKey(values))) return values;
10916
+ }
10917
+ }
10918
+ function getNextVariantValues(nodes, setId, from) {
10919
+ const g = new Graph(nodes);
10920
+ return nextVariantValuesIn(g, setId, from ?? defaultVariantIn(g, setId));
10921
+ }
10922
+ function relayoutSet(nodes, setId, options) {
10923
+ if (options.layout === false || !getAutoLayout(nodes.find((n) => n.id === setId))) return nodes;
10924
+ return applyNodeUpdates(nodes, layoutUpMany(nodes, [setId], typeof options.layout === "object" ? options.layout : {}));
10925
+ }
10926
+ function addCanvasVariant(nodes, setId, options = {}) {
10927
+ const g = new Graph(nodes);
10928
+ const edit = variantEditIn(g, setId);
10929
+ if (!("set" in edit)) return fail(nodes, edit.code, edit.reason);
10930
+ if (edit.variants.length === 0) return fail(nodes, "invalid", "The set has no variant to duplicate.");
10931
+ const fromId = options.from ?? defaultVariantIn(g, setId);
10932
+ const source = edit.variants.find((v) => v.id === fromId);
10933
+ if (!source) return fail(nodes, "not-found", `"${fromId}" is not a variant of this set.`);
10934
+ const wanted = cleanVariantPairs(options.variant ?? nextVariantValuesIn(g, setId, fromId));
10935
+ if (typeof wanted === "string") return fail(nodes, "invalid", wanted);
10936
+ const problem = addMissingProperties(edit, wanted, (options.defaultValue ?? "Default").trim());
10937
+ if (problem) return fail(nodes, "invalid", problem);
10938
+ const id = options.id ?? uniqueId(`${setId}-variant`, g.byId);
10939
+ if (g.get(id)) return fail(nodes, "invalid", `Id "${id}" is taken.`);
10940
+ edit.values.set(id, { ...edit.values.get(fromId), ...wanted });
10941
+ const dup = duplicateAmong(edit.values, [id]);
10942
+ if (dup) return fail(nodes, "invalid", `A variant with ${dup} exists already.`);
10943
+ const autoLayout = !!getAutoLayout(edit.set);
10944
+ const { width, height } = getNodeSize(source);
10945
+ let position = options.position ? { ...options.position } : { ...source.position };
10946
+ if (!options.position && !autoLayout) {
10947
+ const rects = edit.variants.map((v) => ({ ...v.position, ...getNodeSize(v) }));
10948
+ position = { x: source.position.x + width + VARIANT_GAP, y: source.position.y };
10949
+ for (let k = 0; k <= rects.length; k++) {
10950
+ const hit = rects.find((r) => position.x < r.x + r.width && r.x < position.x + width && position.y < r.y + r.height && r.y < position.y + height);
10951
+ if (!hit) break;
10952
+ position = { x: hit.x + hit.width + VARIANT_GAP, y: position.y };
10953
+ }
10954
+ }
10955
+ const taken = new Set(g.byId.keys());
10956
+ taken.add(id);
10957
+ const map = /* @__PURE__ */ new Map([[fromId, id]]);
10958
+ const sub3 = g.subtree(fromId);
10959
+ for (const d of sub3) {
10960
+ if (d === fromId) continue;
10961
+ const nid = uniqueId(`${id}-${d}`, taken);
10962
+ taken.add(nid);
10963
+ map.set(d, nid);
10964
+ }
10965
+ const order = variantOrder([...edit.variants.map((v) => v.id), id], edit.values);
10966
+ const subSet = new Set(sub3);
10967
+ const copies = nodes.filter((n) => subSet.has(n.id)).map((n) => {
10968
+ const next = { ...n, id: map.get(n.id) };
10969
+ if (n.parentId !== void 0 && n.id !== fromId) next.parentId = map.get(n.parentId) ?? n.parentId;
10970
+ for (const k of RUNTIME_KEYS) delete next[k];
10971
+ if (n.id !== fromId) return next;
10972
+ next.position = position;
10973
+ return withVariantValues(next, edit.values.get(id), order);
10974
+ });
10975
+ const updates = writeVariantEdit(edit, [id]);
10976
+ const inSet = new Set(g.subtree(setId));
10977
+ let last = -1;
10978
+ nodes.forEach((n, i) => {
10979
+ if (inSet.has(n.id)) last = i;
10980
+ });
10981
+ let after = [];
10982
+ nodes.forEach((n, i) => {
10983
+ after.push(updates.get(n.id) ?? n);
10984
+ if (i === last) after.push(...copies);
10985
+ });
10986
+ if (autoLayout && !options.position) after = relayoutSet(after, setId, options);
10987
+ else {
10988
+ const size = getNodeSize(edit.set);
10989
+ const w = Math.max(size.width, position.x + width + VARIANT_GAP);
10990
+ const h = Math.max(size.height, position.y + height + VARIANT_GAP);
10991
+ if (w !== size.width || h !== size.height) after = applyNodeUpdates(after, [{ id: setId, width: w, height: h }]);
10992
+ }
10993
+ return done(nodes, after, [id], options);
10994
+ }
10995
+ function removeCanvasVariant(nodes, variantId, options = {}) {
10996
+ const g = new Graph(nodes);
10997
+ const set = variantSetIn(g, variantId);
10998
+ if (!set) return fail(nodes, "not-found", "Choose a variant of a component set.");
10999
+ if (owningInstanceIn(g, set.id)) return fail(nodes, "inside-instance", "A component set inside an instance can't be edited: edit its main.");
11000
+ const others = variantsIn(g, set.id).filter((v) => v.id !== variantId);
11001
+ if (others.length === 0) return fail(nodes, "invalid", "A component set needs at least one variant: delete the set instead.");
11002
+ const replacement = closestVariant(others, getVariantValues(g.get(variantId)));
11003
+ const swap = (v) => v === variantId ? replacement : v;
11004
+ const repointed = nodes.map((n) => {
11005
+ const data = dataOf(n);
11006
+ let next;
11007
+ const inst = getInstanceData(n);
11008
+ if (inst) {
11009
+ const moved = { ...inst, componentId: swap(inst.componentId) };
11010
+ if (inst.properties) moved.properties = Object.fromEntries(Object.entries(inst.properties).map(([k, v]) => [k, swap(v)]));
11011
+ if (!same(moved, inst)) next = { ...data, [INSTANCE_DATA_KEY]: moved };
11012
+ }
11013
+ for (const key of [COMPONENT_DATA_KEY, COMPONENT_SET_DATA_KEY]) {
11014
+ const owner = data[key];
11015
+ if (!isRecord(owner) || !isRecord(owner.properties)) continue;
11016
+ const defs = {};
11017
+ for (const [k, def] of Object.entries(owner.properties)) {
11018
+ defs[k] = def.type === "instanceSwap" ? { ...def, default: swap(def.default), ...def.preferred ? { preferred: [...new Set(def.preferred.map(swap))] } : {} } : def;
11019
+ }
11020
+ if (!same(defs, owner.properties)) next = { ...next ?? data, [key]: { ...owner, properties: defs } };
11021
+ }
11022
+ return next ? withData(n, next) : n;
11023
+ });
11024
+ const mid = syncCanvasComponents(nodes, repointed, options);
11025
+ const gone = new Set(new Graph(mid).subtree(variantId));
11026
+ const after = relayoutSet(
11027
+ mid.filter((n) => !gone.has(n.id)),
11028
+ set.id,
11029
+ options
11030
+ );
11031
+ return { ok: true, nodes: syncCanvasComponents(mid, after, options), ids: [replacement] };
11032
+ }
11033
+ function setCanvasVariantProperties(nodes, variantId, values, options = {}) {
11034
+ const g = new Graph(nodes);
11035
+ const set = variantSetIn(g, variantId);
11036
+ if (!set) return fail(nodes, "not-found", "Choose a variant of a component set.");
11037
+ const edit = variantEditIn(g, set.id);
11038
+ if (!("set" in edit)) return fail(nodes, edit.code, edit.reason);
11039
+ const wanted = cleanVariantPairs(values);
11040
+ if (typeof wanted === "string") return fail(nodes, "invalid", wanted);
11041
+ const problem = addMissingProperties(edit, wanted, (options.defaultValue ?? "Default").trim());
11042
+ if (problem) return fail(nodes, "invalid", problem);
11043
+ edit.values.set(variantId, { ...edit.values.get(variantId), ...wanted });
11044
+ const dup = duplicateAmong(edit.values, [variantId]);
11045
+ if (dup) return fail(nodes, "invalid", `A variant with ${dup} exists already.`);
11046
+ const updates = writeVariantEdit(edit);
11047
+ if (updates.size === 0) return { ok: true, nodes, ids: [variantId] };
11048
+ return done(nodes, nodes.map((n) => updates.get(n.id) ?? n), [variantId], options);
11049
+ }
11050
+ function updateInstanceValues(g, setId, updates, fn) {
11051
+ const ids = new Set(variantsIn(g, setId).map((v) => v.id));
11052
+ for (const n of g.nodes) {
11053
+ const inst = getInstanceData(n);
11054
+ if (!inst?.properties || !ids.has(inst.componentId)) continue;
11055
+ const props = fn({ ...inst.properties });
11056
+ if (same(props, inst.properties)) continue;
11057
+ const cur = updates.get(n.id) ?? n;
11058
+ updates.set(n.id, withData(cur, { ...dataOf(cur), [INSTANCE_DATA_KEY]: { ...inst, properties: props } }));
11059
+ }
11060
+ }
11061
+ function renameCanvasVariantProperty(nodes, setId, name, newName, options = {}) {
11062
+ const g = new Graph(nodes);
11063
+ const edit = variantEditIn(g, setId);
11064
+ if (!("set" in edit)) return fail(nodes, edit.code, edit.reason);
11065
+ const known = variantOptionsIn(g, setId);
11066
+ if (!known.some((o) => o.name === name)) return fail(nodes, "unknown-property", `Unknown variant property "${name}".`);
11067
+ const to = newName.trim();
11068
+ const problem = variantNameProblem("property", to);
11069
+ if (problem) return fail(nodes, "invalid", problem);
11070
+ if (to === name) return { ok: true, nodes, ids: [setId] };
11071
+ if (known.some((o) => o.name === to)) return fail(nodes, "invalid", `"${to}" exists already.`);
11072
+ if (to in propertyDefsIn(g, edit.variants[0].id)) return fail(nodes, "invalid", `"${to}" is a component property.`);
11073
+ const rename = (v) => Object.fromEntries(Object.entries(v).map(([k, val]) => [k === name ? to : k, val]));
11074
+ for (const [id, v] of edit.values) edit.values.set(id, rename(v));
11075
+ const updates = writeVariantEdit(edit);
11076
+ updateInstanceValues(g, setId, updates, (props) => name in props ? rename(props) : props);
11077
+ return done(nodes, nodes.map((n) => updates.get(n.id) ?? n), [setId], options);
11078
+ }
11079
+ function renameCanvasVariantValue(nodes, setId, property, value, newValue, options = {}) {
11080
+ const g = new Graph(nodes);
11081
+ const edit = variantEditIn(g, setId);
11082
+ if (!("set" in edit)) return fail(nodes, edit.code, edit.reason);
11083
+ const option = variantOptionsIn(g, setId).find((o) => o.name === property);
11084
+ if (!option) return fail(nodes, "unknown-property", `Unknown variant property "${property}".`);
11085
+ if (!option.values.includes(value)) return fail(nodes, "invalid", `"${property}" has no value "${value}".`);
11086
+ const to = newValue.trim();
11087
+ const problem = variantNameProblem("value", to);
11088
+ if (problem) return fail(nodes, "invalid", problem);
11089
+ if (to === value) return { ok: true, nodes, ids: [setId] };
11090
+ const touched = [];
11091
+ for (const [id, v] of edit.values) {
11092
+ if (v[property] !== value) continue;
11093
+ edit.values.set(id, { ...v, [property]: to });
11094
+ touched.push(id);
11095
+ }
11096
+ const dup = duplicateAmong(edit.values, touched);
11097
+ if (dup) return fail(nodes, "invalid", `A variant with ${dup} exists already.`);
11098
+ const updates = writeVariantEdit(edit);
11099
+ updateInstanceValues(g, setId, updates, (props) => props[property] === value ? { ...props, [property]: to } : props);
11100
+ return done(nodes, nodes.map((n) => updates.get(n.id) ?? n), [setId], options);
11101
+ }
11385
11102
 
11386
11103
  // src/canvas/componentsController.ts
11387
11104
  function makePatch(from, to) {
@@ -11449,6 +11166,24 @@ function createCanvasComponentsController(options) {
11449
11166
  }
11450
11167
  const reason = (result) => result.ok ? true : result.reason;
11451
11168
  const nodes = () => options.getNodes();
11169
+ const refused = (why) => ({ ok: false, nodes: nodes(), code: "not-found", reason: why });
11170
+ const setOf = (id) => {
11171
+ const at = target(id);
11172
+ const node = at ? nodes().find((n) => n.id === at) : void 0;
11173
+ if (!node) return void 0;
11174
+ return isCanvasComponentSet(node) ? node.id : getVariantSet(nodes(), node.id);
11175
+ };
11176
+ const variantOf = (id) => {
11177
+ const at = target(id);
11178
+ return at && getVariantSet(nodes(), at) ? at : void 0;
11179
+ };
11180
+ const NO_SET = "Select a component set or one of its variants.";
11181
+ const NO_VARIANT = "Select a variant of a component set.";
11182
+ const addOptions = (setId, addOptions_) => {
11183
+ if (addOptions_.from !== void 0 || setId !== void 0) return addOptions_;
11184
+ const v = variantOf();
11185
+ return v ? { ...addOptions_, from: v } : addOptions_;
11186
+ };
11452
11187
  const controller = {
11453
11188
  sync,
11454
11189
  /** Rebuilds every instance from its main (e.g. after loading), not recorded in history. */
@@ -11563,6 +11298,63 @@ function createCanvasComponentsController(options) {
11563
11298
  (n) => setCanvasComponentProperty(n, componentId, name, def, { ...syncOptions(), ...propertyOptions })
11564
11299
  );
11565
11300
  },
11301
+ /**
11302
+ * Adds a variant to a set (default: the selection's set) by duplicating `from` (default:
11303
+ * the selected variant, else the default variant) with the given `variant` values.
11304
+ */
11305
+ addVariant(setId, variantOptions = {}) {
11306
+ const set2 = setOf(setId);
11307
+ if (!set2) return refused(NO_SET);
11308
+ const opts = addOptions(setId, variantOptions);
11309
+ return run("Add Variant", (n) => addCanvasVariant(n, set2, { ...syncOptions(), ...opts }));
11310
+ },
11311
+ canAddVariant(setId, variantOptions = {}) {
11312
+ const set2 = setOf(setId);
11313
+ return set2 ? reason(addCanvasVariant(nodes(), set2, { ...addOptions(setId, variantOptions), layout: false })) : NO_SET;
11314
+ },
11315
+ /** Removes a variant (default: the selected one); its instances switch to the closest variant. */
11316
+ removeVariant(variantId) {
11317
+ const v = variantOf(variantId);
11318
+ if (!v) return refused(NO_VARIANT);
11319
+ return run("Remove Variant", (n) => removeCanvasVariant(n, v, syncOptions()));
11320
+ },
11321
+ canRemoveVariant(variantId) {
11322
+ const v = variantOf(variantId);
11323
+ return v ? reason(removeCanvasVariant(nodes(), v, { layout: false })) : NO_VARIANT;
11324
+ },
11325
+ /** Edits a variant's property values (default: the selected variant). */
11326
+ setVariantProperties(values, variantId, variantOptions = {}) {
11327
+ const v = variantOf(variantId);
11328
+ if (!v) return refused(NO_VARIANT);
11329
+ return run("Edit Variant", (n) => setCanvasVariantProperties(n, v, values, { ...syncOptions(), ...variantOptions }));
11330
+ },
11331
+ canSetVariantProperties(values, variantId) {
11332
+ const v = variantOf(variantId);
11333
+ if (!v) return NO_VARIANT;
11334
+ return values ? reason(setCanvasVariantProperties(nodes(), v, values, { layout: false })) : true;
11335
+ },
11336
+ /** Renames a variant property across a set (default: the selection's set). */
11337
+ renameVariantProperty(name, newName, setId) {
11338
+ const set2 = setOf(setId);
11339
+ if (!set2) return refused(NO_SET);
11340
+ return run("Rename Variant Property", (n) => renameCanvasVariantProperty(n, set2, name, newName, syncOptions()));
11341
+ },
11342
+ canRenameVariantProperty(name, newName, setId) {
11343
+ const set2 = setOf(setId);
11344
+ if (!set2) return NO_SET;
11345
+ return name !== void 0 && newName !== void 0 ? reason(renameCanvasVariantProperty(nodes(), set2, name, newName, { layout: false })) : true;
11346
+ },
11347
+ /** Renames a value of a variant property across a set (default: the selection's set). */
11348
+ renameVariantValue(property, value, newValue, setId) {
11349
+ const set2 = setOf(setId);
11350
+ if (!set2) return refused(NO_SET);
11351
+ return run("Rename Variant Value", (n) => renameCanvasVariantValue(n, set2, property, value, newValue, syncOptions()));
11352
+ },
11353
+ canRenameVariantValue(property, value, newValue, setId) {
11354
+ const set2 = setOf(setId);
11355
+ if (!set2) return NO_SET;
11356
+ return property !== void 0 && value !== void 0 && newValue !== void 0 ? reason(renameCanvasVariantValue(nodes(), set2, property, value, newValue, { layout: false })) : true;
11357
+ },
11566
11358
  /** Binds (or with `null` unbinds) a main layer's field to a component property. */
11567
11359
  bindComponentProperty(layerId, field, name) {
11568
11360
  return run("Bind Component Property", (n) => bindCanvasComponentProperty(n, layerId, field, name, syncOptions()));
@@ -11644,6 +11436,7 @@ export {
11644
11436
  StreamingLayoutManager,
11645
11437
  TOKEN_BINDINGS_DATA_KEY,
11646
11438
  TOKEN_MODES_DATA_KEY,
11439
+ addCanvasVariant,
11647
11440
  addKeyframesAtTime,
11648
11441
  alignNodes,
11649
11442
  animateNode,
@@ -11767,6 +11560,7 @@ export {
11767
11560
  getLayoutParent,
11768
11561
  getLayoutSizing,
11769
11562
  getLayoutedNodes,
11563
+ getNextVariantValues,
11770
11564
  getNodeAnimation,
11771
11565
  getNodeOverrides,
11772
11566
  getNodeRect,
@@ -11880,7 +11674,10 @@ export {
11880
11674
  recordAnimationChanges,
11881
11675
  relayoutUpdates,
11882
11676
  remapPastedNodes,
11677
+ removeCanvasVariant,
11883
11678
  removeTrack,
11679
+ renameCanvasVariantProperty,
11680
+ renameCanvasVariantValue,
11884
11681
  reorderNodes,
11885
11682
  repositionNodesTo,
11886
11683
  resetCanvasInstanceOverrides,
@@ -11908,6 +11705,7 @@ export {
11908
11705
  setCanvasComponentProperty,
11909
11706
  setCanvasInstanceProperties,
11910
11707
  setCanvasTokenMode,
11708
+ setCanvasVariantProperties,
11911
11709
  setKeyframe,
11912
11710
  setKeyframeEasing,
11913
11711
  setMotionPath,