tweakit 0.6.0 → 0.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.
Files changed (132) hide show
  1. package/AGENTS.md +5 -5
  2. package/README.md +3 -3
  3. package/dist/tweaks/auto.js +1 -1
  4. package/dist/tweaks/auto.js.map +1 -1
  5. package/dist/tweaks/bezier-S2BOXQBM.js +2 -0
  6. package/dist/tweaks/bezier-S2BOXQBM.js.map +6 -0
  7. package/dist/tweaks/chunk-3MZAAMC2.js +2 -0
  8. package/dist/tweaks/chunk-3MZAAMC2.js.map +6 -0
  9. package/dist/tweaks/chunk-ETB35KFV.js +2 -0
  10. package/dist/tweaks/chunk-ETB35KFV.js.map +6 -0
  11. package/dist/tweaks/chunk-YB5GKWZI.js +2 -0
  12. package/dist/tweaks/chunk-YB5GKWZI.js.map +6 -0
  13. package/dist/tweaks/clip-A3VI53BK.js +2 -0
  14. package/dist/tweaks/clip-A3VI53BK.js.map +6 -0
  15. package/dist/tweaks/color-UGYMAF3W.js +2 -0
  16. package/dist/tweaks/core.js +1 -1
  17. package/dist/tweaks/core.js.map +3 -3
  18. package/dist/tweaks/fold-GY47HIQA.js +2 -0
  19. package/dist/tweaks/fold-GY47HIQA.js.map +6 -0
  20. package/dist/tweaks/gradient-BTX25WSH.js +2 -0
  21. package/dist/tweaks/gradient-BTX25WSH.js.map +6 -0
  22. package/dist/tweaks/{heavy-CLD7UAVI.js → heavy-NPFIGHRM.js} +2 -2
  23. package/dist/tweaks/image-HTQQ4WX6.js +4 -0
  24. package/dist/tweaks/image-HTQQ4WX6.js.map +6 -0
  25. package/dist/tweaks/{image-store-WRKY2NPU.js → image-store-QD5SMZXG.js} +1 -1
  26. package/dist/tweaks/image-store-QD5SMZXG.js.map +6 -0
  27. package/dist/tweaks/interval-BHPI4LRN.js +2 -0
  28. package/dist/tweaks/interval-BHPI4LRN.js.map +6 -0
  29. package/dist/tweaks/markup-QM53TQJZ.js +2 -0
  30. package/dist/tweaks/markup-QM53TQJZ.js.map +6 -0
  31. package/dist/tweaks/monitor-722WIYGW.js +3 -0
  32. package/dist/tweaks/monitor-722WIYGW.js.map +6 -0
  33. package/dist/tweaks/motion-HMCZK6HJ.js +2 -0
  34. package/dist/tweaks/motion-HMCZK6HJ.js.map +6 -0
  35. package/dist/tweaks/plot-D3NQ6XNN.js +3 -0
  36. package/dist/tweaks/plot-D3NQ6XNN.js.map +6 -0
  37. package/dist/tweaks/point-XDXKMDXH.js +2 -0
  38. package/dist/tweaks/point-XDXKMDXH.js.map +6 -0
  39. package/dist/tweaks/presets-EUWNSCUW.js +2 -0
  40. package/dist/tweaks/presets-EUWNSCUW.js.map +6 -0
  41. package/dist/tweaks/shadow-AM7VSRNS.js +2 -0
  42. package/dist/tweaks/shadow-AM7VSRNS.js.map +6 -0
  43. package/dist/tweaks/shadow-root-RYUV3AGS.js +3 -0
  44. package/dist/tweaks/shadow-root-RYUV3AGS.js.map +6 -0
  45. package/dist/tweaks/sides-JWIURDA4.js +2 -0
  46. package/dist/tweaks/sides-JWIURDA4.js.map +6 -0
  47. package/dist/tweaks/spring-BF5GBTPM.js +2 -0
  48. package/dist/tweaks/spring-BF5GBTPM.js.map +6 -0
  49. package/dist/tweaks/tabs-PRVUUZXZ.js +2 -0
  50. package/dist/tweaks/tabs-PRVUUZXZ.js.map +6 -0
  51. package/dist/tweaks.css +1 -1
  52. package/dist/tweaks.js +5 -4
  53. package/dist/tweaks.js.map +3 -3
  54. package/dist/types/tweaks/enhance.d.ts +3 -2
  55. package/dist/types/tweaks/types.d.ts +278 -72
  56. package/package.json +5 -2
  57. package/src/tweaks/auto.ts +9 -5
  58. package/src/tweaks/clip.ts +84 -22
  59. package/src/tweaks/controls/basic.ts +463 -139
  60. package/src/tweaks/controls/bezier.ts +152 -47
  61. package/src/tweaks/controls/color.ts +610 -161
  62. package/src/tweaks/controls/gradient.ts +256 -77
  63. package/src/tweaks/controls/image.ts +388 -132
  64. package/src/tweaks/controls/interval.ts +126 -41
  65. package/src/tweaks/controls/monitor.ts +142 -36
  66. package/src/tweaks/controls/motion.ts +231 -56
  67. package/src/tweaks/controls/plot.ts +321 -101
  68. package/src/tweaks/controls/point.ts +134 -45
  69. package/src/tweaks/controls/shadow.ts +103 -30
  70. package/src/tweaks/controls/sides.ts +150 -40
  71. package/src/tweaks/controls/spring.ts +220 -60
  72. package/src/tweaks/controls/tabs.ts +39 -18
  73. package/src/tweaks/core.ts +1 -1
  74. package/src/tweaks/easing.ts +45 -20
  75. package/src/tweaks/enhance.ts +21 -6
  76. package/src/tweaks/feedback.ts +133 -34
  77. package/src/tweaks/fold.ts +22 -10
  78. package/src/tweaks/heavy.ts +461 -137
  79. package/src/tweaks/icons.ts +12 -5
  80. package/src/tweaks/image-store.ts +131 -43
  81. package/src/tweaks/lazy.ts +36 -19
  82. package/src/tweaks/markup.ts +255 -92
  83. package/src/tweaks/panel.ts +1262 -438
  84. package/src/tweaks/presets.ts +85 -0
  85. package/src/tweaks/schema.ts +213 -51
  86. package/src/tweaks/shadow-root.ts +178 -0
  87. package/src/tweaks/shared.ts +802 -190
  88. package/src/tweaks/standalone.ts +120 -30
  89. package/src/tweaks/types.ts +644 -143
  90. package/src/tweaks/undo.ts +104 -0
  91. package/src/tweaks.css +556 -550
  92. package/src/wide-gamut.ts +473 -125
  93. package/dist/tweaks/bezier-JFEZMCLP.js +0 -2
  94. package/dist/tweaks/bezier-JFEZMCLP.js.map +0 -6
  95. package/dist/tweaks/chunk-6UO4DHBN.js +0 -2
  96. package/dist/tweaks/chunk-6UO4DHBN.js.map +0 -6
  97. package/dist/tweaks/chunk-DIQMLBAS.js +0 -2
  98. package/dist/tweaks/chunk-DIQMLBAS.js.map +0 -6
  99. package/dist/tweaks/chunk-XE6OUJBG.js +0 -2
  100. package/dist/tweaks/chunk-XE6OUJBG.js.map +0 -6
  101. package/dist/tweaks/clip-WW7JYO4X.js +0 -2
  102. package/dist/tweaks/clip-WW7JYO4X.js.map +0 -6
  103. package/dist/tweaks/color-YMMFURP6.js +0 -2
  104. package/dist/tweaks/fold-U6YRZLCJ.js +0 -2
  105. package/dist/tweaks/fold-U6YRZLCJ.js.map +0 -6
  106. package/dist/tweaks/gradient-JER7PW4D.js +0 -2
  107. package/dist/tweaks/gradient-JER7PW4D.js.map +0 -6
  108. package/dist/tweaks/image-H6QSLGO3.js +0 -4
  109. package/dist/tweaks/image-H6QSLGO3.js.map +0 -6
  110. package/dist/tweaks/image-store-WRKY2NPU.js.map +0 -6
  111. package/dist/tweaks/interval-TEUHQGZN.js +0 -2
  112. package/dist/tweaks/interval-TEUHQGZN.js.map +0 -6
  113. package/dist/tweaks/markup-NIBJD6E2.js +0 -2
  114. package/dist/tweaks/markup-NIBJD6E2.js.map +0 -6
  115. package/dist/tweaks/monitor-3BY7YCGX.js +0 -3
  116. package/dist/tweaks/monitor-3BY7YCGX.js.map +0 -6
  117. package/dist/tweaks/motion-FQPUBKKM.js +0 -2
  118. package/dist/tweaks/motion-FQPUBKKM.js.map +0 -6
  119. package/dist/tweaks/plot-2HBNBZSG.js +0 -3
  120. package/dist/tweaks/plot-2HBNBZSG.js.map +0 -6
  121. package/dist/tweaks/point-VV6XV3I6.js +0 -2
  122. package/dist/tweaks/point-VV6XV3I6.js.map +0 -6
  123. package/dist/tweaks/shadow-5E4E55I2.js +0 -2
  124. package/dist/tweaks/shadow-5E4E55I2.js.map +0 -6
  125. package/dist/tweaks/sides-P3STEVYY.js +0 -2
  126. package/dist/tweaks/sides-P3STEVYY.js.map +0 -6
  127. package/dist/tweaks/spring-HUVSXNVN.js +0 -2
  128. package/dist/tweaks/spring-HUVSXNVN.js.map +0 -6
  129. package/dist/tweaks/tabs-76FW7LQ7.js +0 -2
  130. package/dist/tweaks/tabs-76FW7LQ7.js.map +0 -6
  131. /package/dist/tweaks/{color-YMMFURP6.js.map → color-UGYMAF3W.js.map} +0 -0
  132. /package/dist/tweaks/{heavy-CLD7UAVI.js.map → heavy-NPFIGHRM.js.map} +0 -0
@@ -1,6 +1,17 @@
1
1
  // ── Motion — a transition's timing: an easing curve with a duration, or a spring, in one popover over one value. The
2
2
  // value carries the CSS it resolves to — `duration` in ms and an `easing` string, cubic-bezier() or a sampled linear(). Lazy. ──
3
- import { el, txt, clamp, isObj, createSegmented, getControl, popover, registerControl, REDUCE_MOTION } from "../shared.js";
3
+ import {
4
+ el,
5
+ txt,
6
+ clamp,
7
+ isObj,
8
+ createSegmented,
9
+ getControl,
10
+ popover,
11
+ registerControl,
12
+ REDUCE_MOTION,
13
+ words,
14
+ } from "../shared.js";
4
15
  import { numField, triggerRow, svgEl, springCurve, springRun, timeFromSpring, notice, SPRING_DUR } from "../heavy.js";
5
16
  import { KEYWORDS } from "../easing.js";
6
17
  import type { OnChange } from "../shared.js";
@@ -9,21 +20,46 @@ import type { Control, MotionValue, MotionInput } from "../types.js";
9
20
 
10
21
  type Mode = "easing" | "spring";
11
22
  type Curve = [number, number, number, number];
12
- interface SpringOut { stiffness: number; damping: number; mass: number; visualDuration?: number; bounce?: number }
23
+ interface SpringOut {
24
+ stiffness: number;
25
+ damping: number;
26
+ mass: number;
27
+ visualDuration?: number;
28
+ bounce?: number;
29
+ }
13
30
 
14
- const DUR_DEF = 300, DUR_MAX = 10000, STAGE_PAD = 16; // ms; the stage's side padding (tweaks.css)
31
+ const DUR_DEF = 300,
32
+ DUR_MAX = 10000,
33
+ STAGE_PAD = 16; // ms; the stage's side padding (tweaks.css)
15
34
  const BOUNCE_MAX = 1; // Motion's whole range: its damping-ratio floor of 0.05 keeps a bounce of 1 settling
16
- const has = (x: unknown) => (typeof x === "number" ? Number.isFinite(x) : typeof x === "string" && x.trim() !== "" && Number.isFinite(+x)); // a number, or a numeric string — not `true`, `[]` or " "
35
+ // a number, or a numeric string — not `true`, `[]` or " "
36
+ const has = (x: unknown) =>
37
+ typeof x === "number" ? Number.isFinite(x) : typeof x === "string" && x.trim() !== "" && Number.isFinite(+x);
17
38
  const curveOf = (c: unknown): Curve | null => {
18
- if (typeof c === "string") { const k = c === "linear" ? [0, 0, 1, 1] : KEYWORDS[c]; return k ? (k.slice() as Curve) : null; } // the CSS keywords, linear's identity included; null-prototype, so "constructor" is not one
19
- if (Array.isArray(c) && c.length === 4) { const m = c.map(Number); return m.every(Number.isFinite) ? (m as Curve) : null; }
39
+ // the CSS keywords, linear's identity included; null-prototype, so "constructor" is not one
40
+ if (typeof c === "string") {
41
+ const k = c === "linear" ? [0, 0, 1, 1] : KEYWORDS[c];
42
+ return k ? (k.slice() as Curve) : null;
43
+ }
44
+ if (Array.isArray(c) && c.length === 4) {
45
+ const m = c.map(Number);
46
+ return m.every(Number.isFinite) ? (m as Curve) : null;
47
+ }
20
48
  return null;
21
49
  };
22
- const springKeys = (v: MotionInput) => has(v.stiffness) || has(v.damping) || has(v.mass) || has(v.visualDuration) || has(v.bounce);
23
- // Physics keys map to the visualDuration + bounce Motion would resolve to them (timeFromSpring). An explicit duration/bounce wins.
50
+ const springKeys = (v: MotionInput) =>
51
+ has(v.stiffness) || has(v.damping) || has(v.mass) || has(v.visualDuration) || has(v.bounce);
52
+ // Physics keys map to the visualDuration + bounce Motion would resolve to them (timeFromSpring). An explicit
53
+ // duration/bounce wins.
24
54
  const springTime = (v: MotionInput) => {
25
55
  const out: { mode: "time"; visualDuration?: number; bounce?: number } = { mode: "time" };
26
- if (has(v.stiffness)) { const k = +v.stiffness!, m = has(v.mass) ? +v.mass! : 1, t = timeFromSpring(k, has(v.damping) ? +v.damping! : 2 * Math.sqrt(k * m), m); out.visualDuration = t.visualDuration; if (has(v.damping)) out.bounce = clamp(t.bounce, -1, BOUNCE_MAX); }
56
+ if (has(v.stiffness)) {
57
+ const k = +v.stiffness!,
58
+ m = has(v.mass) ? +v.mass! : 1,
59
+ t = timeFromSpring(k, has(v.damping) ? +v.damping! : 2 * Math.sqrt(k * m), m);
60
+ out.visualDuration = t.visualDuration;
61
+ if (has(v.damping)) out.bounce = clamp(t.bounce, -1, BOUNCE_MAX);
62
+ }
27
63
  if (has(v.visualDuration)) out.visualDuration = +v.visualDuration!;
28
64
  if (has(v.bounce)) out.bounce = clamp(+v.bounce!, -1, BOUNCE_MAX);
29
65
  return out;
@@ -32,56 +68,116 @@ const springTime = (v: MotionInput) => {
32
68
  // rounds and clamps: unless a time pair rides along that isn't the one they resolve to (this control's own value back).
33
69
  const exactSpring = (v: MotionInput): SpringOut | null => {
34
70
  if (!has(v.stiffness) || !has(v.damping)) return null;
35
- const k = +v.stiffness!, d = +v.damping!, m = has(v.mass) ? +v.mass! : 1;
71
+ const k = +v.stiffness!,
72
+ d = +v.damping!,
73
+ m = has(v.mass) ? +v.mass! : 1;
36
74
  if (!(k > 0 && d >= 0 && m > 0)) return null;
37
- const t = timeFromSpring(k, d, m), off = (a: unknown, b: number) => has(a) && Math.abs(+a! - b) > 1e-6 * Math.max(1, Math.abs(b));
38
- return off(v.visualDuration, t.visualDuration) || off(v.bounce, t.bounce) ? null : { stiffness: k, damping: d, mass: m, ...t };
75
+ const t = timeFromSpring(k, d, m),
76
+ off = (a: unknown, b: number) => has(a) && Math.abs(+a! - b) > 1e-6 * Math.max(1, Math.abs(b));
77
+ return off(v.visualDuration, t.visualDuration) || off(v.bounce, t.bounce)
78
+ ? null
79
+ : { stiffness: k, damping: d, mass: m, ...t };
39
80
  };
40
- const modeOf = (v: MotionInput, keep: Mode): Mode => (v.mode === "spring" || v.mode === "easing" ? v.mode : springKeys(v) ? "spring" : curveOf(v.curve) ? "easing" : keep); // a curve that doesn't parse is no reason to switch
81
+ // a curve that doesn't parse is no reason to switch
82
+ const modeOf = (v: MotionInput, keep: Mode): Mode =>
83
+ v.mode === "spring" || v.mode === "easing" ? v.mode : springKeys(v) ? "spring" : curveOf(v.curve) ? "easing" : keep;
41
84
 
42
85
  function createMotion(meta: Meta, onChange: OnChange): Control {
43
- const Bezier = getControl("cubicbezier"), Spring = getControl("spring");
86
+ const tr = words();
87
+ const Bezier = getControl("cubicbezier"),
88
+ Spring = getControl("spring");
44
89
  if (!Bezier || !Spring) throw new Error("[tweaks] motion: the bezier and spring controls are not loaded");
45
90
  const init: MotionInput = isObj(meta.value) ? meta.value : {};
46
91
  let mode = modeOf(init, "easing");
47
- let curve = curveOf(init.curve) ?? (KEYWORDS.ease.slice() as Curve), dur = has(init.duration) ? clamp(+init.duration!, 0, DUR_MAX) : DUR_DEF;
92
+ let curve = curveOf(init.curve) ?? (KEYWORDS.ease.slice() as Curve),
93
+ dur = has(init.duration) ? clamp(+init.duration!, 0, DUR_MAX) : DUR_DEF;
48
94
 
49
95
  // ── Row — label, a mini curve, a summary. ──
50
- const { root, trigger, right } = triggerRow("tw-motion", meta.label ?? "Motion"); // ??: an explicit "" label renders none
51
- const chip = el("span", "tw-trigger-chip tw-motion-chip");
52
- const chipSvg = svgEl("svg"); chipSvg.setAttribute("viewBox", "0 0 100 100"); chipSvg.setAttribute("preserveAspectRatio", "none");
53
- const chipPath = svgEl("path"); chipSvg.append(chipPath); chip.append(chipSvg);
54
- const valueEl = el("span", "tw-trigger-value");
96
+ // ??: an explicit "" label renders none
97
+ const { root, trigger, right } = triggerRow("twk-motion", meta.label ?? tr("motion", "Motion"));
98
+ const chip = el("span", "twk-trigger-chip twk-motion-chip");
99
+ const chipSvg = svgEl("svg");
100
+ chipSvg.setAttribute("viewBox", "0 0 100 100");
101
+ chipSvg.setAttribute("preserveAspectRatio", "none");
102
+ const chipPath = svgEl("path");
103
+ chipSvg.append(chipPath);
104
+ chip.append(chipSvg);
105
+ const valueEl = el("span", "twk-trigger-value");
55
106
  right.append(chip, valueEl);
56
107
 
57
108
  // ── Popover — the mode switch, the preview, one editor at a time. ──
58
- const pop = el("div", "tw-color-pop tw-motion-pop"), body = el("div", "tw-motion-body"); pop.append(body);
59
- const bezier = Bezier({ ...meta, type: "cubicbezier", label: "", value: curve.slice() } as Meta, (v) => { curve = v as Curve; update(); emit(); });
109
+ const pop = el("div", "twk-color-pop twk-motion-pop"),
110
+ body = el("div", "twk-motion-body");
111
+ pop.append(body);
112
+ const bezier = Bezier({ ...meta, type: "cubicbezier", label: "", value: curve.slice() } as Meta, (v) => {
113
+ curve = v as Curve;
114
+ update();
115
+ emit();
116
+ });
60
117
  curve = bezier.get() as Curve; // as the editor fitted it, so no invalid cubic-bezier()
61
118
  // Host-written physics the editor can't draw exactly: it shows the nearest, clamped here so it doesn't warn as well.
62
- let exact: SpringOut | null = null, said = false;
119
+ let exact: SpringOut | null = null,
120
+ said = false;
63
121
  const editorTime = (inp: MotionInput) => {
64
122
  exact = exactSpring(inp);
65
123
  if (!exact) return springTime(inp);
66
- const vd = clamp(exact.visualDuration!, SPRING_DUR[0], SPRING_DUR[1]), z = exact.damping / (2 * Math.sqrt(exact.stiffness * exact.mass));
67
- if ((vd !== exact.visualDuration || z < 0.05 || z > 20) && !said) { said = true; notice("springKept", [meta.key, exact.stiffness, exact.damping, exact.mass]); }
124
+ const vd = clamp(exact.visualDuration!, SPRING_DUR[0], SPRING_DUR[1]),
125
+ z = exact.damping / (2 * Math.sqrt(exact.stiffness * exact.mass));
126
+ if ((vd !== exact.visualDuration || z < 0.05 || z > 20) && !said) {
127
+ said = true;
128
+ notice("springKept", [meta.key, exact.stiffness, exact.damping, exact.mass]);
129
+ }
68
130
  return { mode: "time" as const, visualDuration: vd, bounce: exact.bounce };
69
131
  };
70
132
  // Time mode only; its Mode row is hidden (tweaks.css). An edit there drops the kept physics.
71
- const spring = Spring({ ...meta, type: "spring", label: "", value: editorTime(init) } as Meta, () => { exact = null; update(); emit(); });
133
+ const spring = Spring({ ...meta, type: "spring", label: "", value: editorTime(init) } as Meta, () => {
134
+ exact = null;
135
+ update();
136
+ emit();
137
+ });
72
138
  const springNow = () => exact ?? (spring.get() as SpringOut);
73
- const durFld = numField({ label: "Duration", value: dur, step: 10, min: 0, max: DUR_MAX, unit: "ms" }, (v) => { dur = clamp(v, 0, DUR_MAX); update(); emit(); });
139
+ const durFld = numField(
140
+ { label: tr("duration", "Duration"), value: dur, step: 10, min: 0, max: DUR_MAX, unit: "ms" },
141
+ (v) => {
142
+ dur = clamp(v, 0, DUR_MAX);
143
+ update();
144
+ emit();
145
+ },
146
+ );
74
147
  dur = durFld.get(); // as the field snapped it
75
- const easingPane = el("div", "tw-motion-pane"), springPane = el("div", "tw-motion-pane"), durRow = el("div", "tw-fields");
76
- durRow.append(durFld.el); easingPane.append(bezier.el, durRow); springPane.append(spring.el);
77
- const stage = el("div", "tw-motion-stage"), box = el("div", "tw-motion-box"); stage.append(box);
78
- const modeSeg = createSegmented([{ value: "easing", label: "Easing" }, { value: "spring", label: "Spring" }], mode, (m: unknown) => { if ((m === "easing" || m === "spring") && m !== mode) { mode = m; showMode(); update(); emit(); } }, "Motion type");
79
- const typeRow = el("div", "tw-row"); typeRow.append(txt("span", "tw-row-label", "Type"), modeSeg.el);
148
+ const easingPane = el("div", "twk-motion-pane"),
149
+ springPane = el("div", "twk-motion-pane"),
150
+ durRow = el("div", "twk-fields");
151
+ durRow.append(durFld.el);
152
+ easingPane.append(bezier.el, durRow);
153
+ springPane.append(spring.el);
154
+ const stage = el("div", "twk-motion-stage"),
155
+ box = el("div", "twk-motion-box");
156
+ stage.append(box);
157
+ const modeSeg = createSegmented(
158
+ [
159
+ { value: "easing", label: tr("easing", "Easing") },
160
+ { value: "spring", label: tr("spring", "Spring") },
161
+ ],
162
+ mode,
163
+ (m: unknown) => {
164
+ if ((m === "easing" || m === "spring") && m !== mode) {
165
+ mode = m;
166
+ showMode();
167
+ update();
168
+ emit();
169
+ }
170
+ },
171
+ tr("motionType", "Motion type"),
172
+ );
173
+ const typeRow = el("div", "twk-row");
174
+ typeRow.append(txt("span", "twk-row-label", tr("type", "Type")), modeSeg.el);
80
175
  body.append(typeRow, easingPane, springPane, stage);
81
176
  root.append(pop);
82
177
 
83
178
  const value = (): MotionValue => {
84
- if (mode === "easing") return { mode, curve: curve.slice() as Curve, duration: dur, easing: `cubic-bezier(${curve.join(", ")})` };
179
+ if (mode === "easing")
180
+ return { mode, curve: curve.slice() as Curve, duration: dur, easing: `cubic-bezier(${curve.join(", ")})` };
85
181
  // The run lasts until the spring is at rest (the drawn curve stops at 2.2 s): springRun.
86
182
  const s = springNow();
87
183
  return { mode, ...s, ...springRun(s.stiffness, s.damping, s.mass) };
@@ -90,50 +186,129 @@ function createMotion(meta: Meta, onChange: OnChange): Control {
90
186
 
91
187
  // Web Animations, no frame loop to own. A click or an open flips direction and restarts; an edit lets a run in flight
92
188
  // finish — a handle drag fires per pointer move, and restarting each strobed the box.
93
- let far = false, anim: Animation | null = null;
189
+ let far = false,
190
+ anim: Animation | null = null;
94
191
  const replay = (flip: boolean) => {
95
- if (flip) far = !far; else if (anim && anim.playState === "running") return;
192
+ if (flip) far = !far;
193
+ else if (anim && anim.playState === "running") return;
96
194
  const v = value();
97
- const travel = Math.max(0, stage.clientWidth - box.offsetWidth - 2 * STAGE_PAD); // measured now: the stage is 0 wide until the pop opens
98
- const from = `translateX(${far ? 0 : travel}px)`, to = `translateX(${far ? travel : 0}px)`;
99
- if (REDUCE_MOTION.matches || typeof box.animate !== "function") { box.style.transform = to; return; }
195
+ // measured now: the stage is 0 wide until the pop opens
196
+ const travel = Math.max(0, stage.clientWidth - box.offsetWidth - 2 * STAGE_PAD);
197
+ const from = `translateX(${far ? 0 : travel}px)`,
198
+ to = `translateX(${far ? travel : 0}px)`;
199
+ if (REDUCE_MOTION.matches || typeof box.animate !== "function") {
200
+ box.style.transform = to;
201
+ return;
202
+ }
100
203
  anim?.cancel();
101
204
  const frames = [{ transform: from }, { transform: to }];
102
- try { anim = box.animate(frames, { duration: v.duration, easing: v.easing, fill: "forwards" }); }
103
- catch { anim = box.animate(frames, { duration: v.duration, fill: "forwards" }); } // no linear() support: a plain run rather than nothing
205
+ try {
206
+ anim = box.animate(frames, { duration: v.duration, easing: v.easing, fill: "forwards" });
207
+ } catch {
208
+ // no linear() support: a plain run rather than nothing
209
+ anim = box.animate(frames, { duration: v.duration, fill: "forwards" });
210
+ }
104
211
  };
105
212
  stage.addEventListener("click", () => replay(true));
106
- stage.tabIndex = 0; stage.setAttribute("role", "button"); stage.setAttribute("aria-label", "Replay the motion");
107
- stage.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); replay(true); } });
213
+ stage.tabIndex = 0;
214
+ stage.setAttribute("role", "button");
215
+ stage.setAttribute("aria-label", tr("replay", "Replay the motion"));
216
+ stage.addEventListener("keydown", (e) => {
217
+ if (e.key === "Enter" || e.key === " ") {
218
+ e.preventDefault();
219
+ replay(true);
220
+ }
221
+ });
108
222
 
109
223
  // The chip's mini curve, y up, scaled so an overshoot still fits.
110
224
  const curvePath = () => {
111
225
  let pts: [number, number][];
112
- if (mode === "easing") { const [x1, y1, x2, y2] = curve; pts = []; for (let i = 0; i <= 24; i++) { const t = i / 24, u = 1 - t, a = 3 * u * u * t, b = 3 * u * t * t, c = t * t * t; pts.push([a * x1 + b * x2 + c, a * y1 + b * y2 + c]); } }
113
- else { const s = springNow(); pts = springCurve(s.stiffness, s.damping, s.mass, 32).map((y, i) => [i / 31, y]); }
114
- const hi = Math.max(1, ...pts.map((p) => p[1])), lo = Math.min(0, ...pts.map((p) => p[1]));
115
- return pts.map(([x, y], i) => `${i ? "L" : "M"}${(10 + x * 80).toFixed(1)},${(90 - ((y - lo) / (hi - lo)) * 80).toFixed(1)}`).join(" ");
226
+ if (mode === "easing") {
227
+ const [x1, y1, x2, y2] = curve;
228
+ pts = [];
229
+ for (let i = 0; i <= 24; i++) {
230
+ const t = i / 24,
231
+ u = 1 - t,
232
+ a = 3 * u * u * t,
233
+ b = 3 * u * t * t,
234
+ c = t * t * t;
235
+ pts.push([a * x1 + b * x2 + c, a * y1 + b * y2 + c]);
236
+ }
237
+ } else {
238
+ const s = springNow();
239
+ pts = springCurve(s.stiffness, s.damping, s.mass, 32).map((y, i) => [i / 31, y]);
240
+ }
241
+ const hi = Math.max(1, ...pts.map((p) => p[1])),
242
+ lo = Math.min(0, ...pts.map((p) => p[1]));
243
+ return pts
244
+ .map(
245
+ ([x, y], i) => `${i ? "L" : "M"}${(10 + x * 80).toFixed(1)},${(90 - ((y - lo) / (hi - lo)) * 80).toFixed(1)}`,
246
+ )
247
+ .join(" ");
116
248
  };
117
249
  // The revealed editor built at 0×0 and onLive may have dropped its resize listener while the pop was detached, so
118
250
  // re-point it at its own value, which redraws at real size.
119
- const resync = () => { if (mode === "easing") bezier.set(curve.slice()); else spring.set(spring.get()); };
120
- const showMode = () => { easingPane.style.display = mode === "easing" ? "" : "none"; springPane.style.display = mode === "spring" ? "" : "none"; resync(); };
251
+ const resync = () => {
252
+ if (mode === "easing") bezier.set(curve.slice());
253
+ else spring.set(spring.get());
254
+ };
255
+ const showMode = () => {
256
+ easingPane.style.display = mode === "easing" ? "" : "none";
257
+ springPane.style.display = mode === "spring" ? "" : "none";
258
+ resync();
259
+ };
121
260
  // The summary shows the authored duration; a spring's emitted run (its settle) is longer.
122
- const summary = () => (mode === "easing" ? `Easing · ${dur} ms` : `Spring · ${Math.round((springNow().visualDuration ?? 0) * 1000)} ms`);
123
- const update = () => { chipPath.setAttribute("d", curvePath()); valueEl.textContent = summary(); replay(false); };
261
+ const summary = () =>
262
+ mode === "easing"
263
+ ? tr("easingSummary", "Easing · {ms} ms", { ms: dur })
264
+ : tr("springSummary", "Spring · {ms} ms", { ms: Math.round((springNow().visualDuration ?? 0) * 1000) });
265
+ const update = () => {
266
+ chipPath.setAttribute("d", curvePath());
267
+ valueEl.textContent = summary();
268
+ replay(false);
269
+ };
124
270
 
125
- popover(root, trigger, pop, { fallbackH: 380, onOpen: () => { modeSeg.set(mode); resync(); replay(true); }, initialFocus: () => modeSeg.el.querySelector<HTMLElement>('[tabindex="0"]') }); // hidden until open: measure now; focus the checked option
126
- showMode(); update();
271
+ // hidden until open: measure now; focus the checked option
272
+ popover(root, trigger, pop, {
273
+ fallbackH: 380,
274
+ onOpen: () => {
275
+ modeSeg.set(mode);
276
+ resync();
277
+ replay(true);
278
+ },
279
+ initialFocus: () => modeSeg.el.querySelector<HTMLElement>('[tabindex="0"]'),
280
+ });
281
+ showMode();
282
+ update();
127
283
 
128
284
  // Set/restore: either mode's shape or a bare keyword; derived fields (easing, a spring's duration) are ignored.
129
285
  const set = (v: unknown) => {
130
- const inp: MotionInput | null = typeof v === "string" ? { curve: v } : Array.isArray(v) && v.length === 4 ? { curve: v as Curve } : isObj(v) ? v : null; // a bare 4-array is the bezier's own shape
286
+ // a bare 4-array is the bezier's own shape
287
+ const inp: MotionInput | null =
288
+ typeof v === "string"
289
+ ? { curve: v }
290
+ : Array.isArray(v) && v.length === 4
291
+ ? { curve: v as Curve }
292
+ : isObj(v)
293
+ ? v
294
+ : null;
131
295
  if (!inp) return;
132
296
  mode = modeOf(inp, mode);
133
- const c = curveOf(inp.curve); if (c) { bezier.set(c.slice()); curve = bezier.get() as Curve; } // read back fitted
134
- if (mode === "easing" && has(inp.duration)) { durFld.set(clamp(+inp.duration!, 0, DUR_MAX)); dur = durFld.get(); } // read back snapped
297
+ const c = curveOf(inp.curve);
298
+ // read back fitted
299
+ if (c) {
300
+ bezier.set(c.slice());
301
+ curve = bezier.get() as Curve;
302
+ }
303
+ // read back snapped
304
+ if (mode === "easing" && has(inp.duration)) {
305
+ durFld.set(clamp(+inp.duration!, 0, DUR_MAX));
306
+ dur = durFld.get();
307
+ }
135
308
  if (springKeys(inp)) spring.set(editorTime(inp));
136
- modeSeg.set(mode); showMode(); update();
309
+ modeSeg.set(mode);
310
+ showMode();
311
+ update();
137
312
  };
138
313
  return { el: root, set, get: value, destroy: () => spring.destroy && spring.destroy() };
139
314
  }