tweakit 0.5.2 → 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 (120) hide show
  1. package/AGENTS.md +32 -30
  2. package/README.md +55 -29
  3. package/THIRD-PARTY-NOTICES.md +0 -1
  4. package/dist/tweaks/auto.js +2 -0
  5. package/dist/tweaks/auto.js.map +6 -0
  6. package/dist/tweaks/bezier-S2BOXQBM.js +2 -0
  7. package/dist/tweaks/bezier-S2BOXQBM.js.map +6 -0
  8. package/dist/tweaks/chunk-3MZAAMC2.js +2 -0
  9. package/dist/tweaks/chunk-3MZAAMC2.js.map +6 -0
  10. package/dist/tweaks/chunk-ETB35KFV.js +2 -0
  11. package/dist/tweaks/chunk-ETB35KFV.js.map +6 -0
  12. package/dist/tweaks/chunk-YB5GKWZI.js +2 -0
  13. package/dist/tweaks/chunk-YB5GKWZI.js.map +6 -0
  14. package/dist/tweaks/clip-A3VI53BK.js +2 -0
  15. package/dist/tweaks/clip-A3VI53BK.js.map +6 -0
  16. package/dist/tweaks/color-UGYMAF3W.js +2 -0
  17. package/dist/tweaks/color-UGYMAF3W.js.map +6 -0
  18. package/dist/tweaks/core.js +2 -1
  19. package/dist/tweaks/core.js.map +6 -0
  20. package/dist/tweaks/fold-GY47HIQA.js +2 -0
  21. package/dist/tweaks/fold-GY47HIQA.js.map +6 -0
  22. package/dist/tweaks/gradient-BTX25WSH.js +2 -0
  23. package/dist/tweaks/gradient-BTX25WSH.js.map +6 -0
  24. package/dist/tweaks/heavy-NPFIGHRM.js +2 -0
  25. package/dist/tweaks/heavy-NPFIGHRM.js.map +6 -0
  26. package/dist/tweaks/image-HTQQ4WX6.js +4 -0
  27. package/dist/tweaks/image-HTQQ4WX6.js.map +6 -0
  28. package/dist/tweaks/image-store-QD5SMZXG.js +2 -0
  29. package/dist/tweaks/image-store-QD5SMZXG.js.map +6 -0
  30. package/dist/tweaks/interval-BHPI4LRN.js +2 -0
  31. package/dist/tweaks/interval-BHPI4LRN.js.map +6 -0
  32. package/dist/tweaks/markup-QM53TQJZ.js +2 -0
  33. package/dist/tweaks/markup-QM53TQJZ.js.map +6 -0
  34. package/dist/tweaks/monitor-722WIYGW.js +3 -0
  35. package/dist/tweaks/monitor-722WIYGW.js.map +6 -0
  36. package/dist/tweaks/motion-HMCZK6HJ.js +2 -0
  37. package/dist/tweaks/motion-HMCZK6HJ.js.map +6 -0
  38. package/dist/tweaks/plot-D3NQ6XNN.js +3 -0
  39. package/dist/tweaks/plot-D3NQ6XNN.js.map +6 -0
  40. package/dist/tweaks/point-XDXKMDXH.js +2 -0
  41. package/dist/tweaks/point-XDXKMDXH.js.map +6 -0
  42. package/dist/tweaks/presets-EUWNSCUW.js +2 -0
  43. package/dist/tweaks/presets-EUWNSCUW.js.map +6 -0
  44. package/dist/tweaks/shadow-AM7VSRNS.js +2 -0
  45. package/dist/tweaks/shadow-AM7VSRNS.js.map +6 -0
  46. package/dist/tweaks/shadow-root-RYUV3AGS.js +3 -0
  47. package/dist/tweaks/shadow-root-RYUV3AGS.js.map +6 -0
  48. package/dist/tweaks/sides-JWIURDA4.js +2 -0
  49. package/dist/tweaks/sides-JWIURDA4.js.map +6 -0
  50. package/dist/tweaks/spring-BF5GBTPM.js +2 -0
  51. package/dist/tweaks/spring-BF5GBTPM.js.map +6 -0
  52. package/dist/tweaks/tabs-PRVUUZXZ.js +2 -0
  53. package/dist/tweaks/tabs-PRVUUZXZ.js.map +6 -0
  54. package/dist/tweaks.css +1 -1
  55. package/dist/tweaks.js +7 -3
  56. package/dist/tweaks.js.map +6 -0
  57. package/dist/types/tweaks/easing.d.ts +8 -11
  58. package/dist/types/tweaks/enhance.d.ts +3 -2
  59. package/dist/types/tweaks/panel.d.ts +6 -4
  60. package/dist/types/tweaks/schema.d.ts +4 -8
  61. package/dist/types/tweaks/standalone.d.ts +3 -3
  62. package/dist/types/tweaks/types.d.ts +499 -116
  63. package/package.json +30 -5
  64. package/src/tweaks/auto.ts +16 -0
  65. package/src/tweaks/clip.ts +156 -0
  66. package/src/tweaks/controls/basic.ts +727 -0
  67. package/src/tweaks/controls/bezier.ts +204 -0
  68. package/src/tweaks/controls/color.ts +861 -0
  69. package/src/tweaks/controls/gradient.ts +419 -0
  70. package/src/tweaks/controls/image.ts +555 -0
  71. package/src/tweaks/controls/interval.ts +189 -0
  72. package/src/tweaks/controls/monitor.ts +211 -0
  73. package/src/tweaks/controls/motion.ts +316 -0
  74. package/src/tweaks/controls/plot.ts +440 -0
  75. package/src/tweaks/controls/point.ts +208 -0
  76. package/src/tweaks/controls/shadow.ts +156 -0
  77. package/src/tweaks/controls/sides.ts +196 -0
  78. package/src/tweaks/controls/spring.ts +308 -0
  79. package/src/tweaks/controls/tabs.ts +68 -0
  80. package/src/tweaks/core.ts +9 -0
  81. package/src/tweaks/easing.ts +118 -0
  82. package/src/tweaks/enhance.ts +35 -0
  83. package/src/tweaks/feedback.ts +218 -0
  84. package/src/tweaks/fold.ts +33 -0
  85. package/src/tweaks/heavy.ts +626 -0
  86. package/src/tweaks/icons.ts +26 -0
  87. package/src/tweaks/image-store.ts +189 -0
  88. package/src/tweaks/lazy.ts +63 -0
  89. package/src/tweaks/markup.ts +317 -0
  90. package/src/tweaks/panel.ts +1693 -0
  91. package/src/tweaks/presets.ts +85 -0
  92. package/src/tweaks/schema.ts +351 -0
  93. package/src/tweaks/shadow-root.ts +178 -0
  94. package/src/tweaks/shared.ts +1137 -0
  95. package/src/tweaks/single.ts +22 -0
  96. package/src/tweaks/standalone.ts +179 -0
  97. package/src/tweaks/types.ts +1027 -0
  98. package/src/tweaks/undo.ts +104 -0
  99. package/src/tweaks.css +965 -0
  100. package/src/wide-gamut.ts +658 -0
  101. package/dist/tweaks/bezier-2RABC6LD.js +0 -1
  102. package/dist/tweaks/chunk-4ATI34PB.js +0 -1
  103. package/dist/tweaks/chunk-7OSCMZ2J.js +0 -1
  104. package/dist/tweaks/chunk-W3JNIJ2F.js +0 -1
  105. package/dist/tweaks/clip-ECGCECEC.js +0 -1
  106. package/dist/tweaks/colour-2RE3FQ5E.js +0 -1
  107. package/dist/tweaks/fold-UGN7TXGX.js +0 -1
  108. package/dist/tweaks/gradient-DB6XTDHO.js +0 -1
  109. package/dist/tweaks/heavy-PKOMHB3C.js +0 -1
  110. package/dist/tweaks/image-7MCLBOZ3.js +0 -1
  111. package/dist/tweaks/interval-L2E4L3AZ.js +0 -1
  112. package/dist/tweaks/markup-ZJJ6CCWM.js +0 -1
  113. package/dist/tweaks/monitor-IEK742PG.js +0 -2
  114. package/dist/tweaks/motion-N5UNVR55.js +0 -1
  115. package/dist/tweaks/plot-VVTOY3RA.js +0 -2
  116. package/dist/tweaks/point-YMMHSLO7.js +0 -1
  117. package/dist/tweaks/shadow-IKYOV3VN.js +0 -1
  118. package/dist/tweaks/sides-FH227Z54.js +0 -1
  119. package/dist/tweaks/spring-3PKAFIH7.js +0 -1
  120. package/dist/tweaks/tabs-BKK5NIIY.js +0 -1
@@ -0,0 +1,316 @@
1
+ // ── Motion — a transition's timing: an easing curve with a duration, or a spring, in one popover over one value. The
2
+ // value carries the CSS it resolves to — `duration` in ms and an `easing` string, cubic-bezier() or a sampled linear(). Lazy. ──
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";
15
+ import { numField, triggerRow, svgEl, springCurve, springRun, timeFromSpring, notice, SPRING_DUR } from "../heavy.js";
16
+ import { KEYWORDS } from "../easing.js";
17
+ import type { OnChange } from "../shared.js";
18
+ import type { Meta } from "../schema.js";
19
+ import type { Control, MotionValue, MotionInput } from "../types.js";
20
+
21
+ type Mode = "easing" | "spring";
22
+ type Curve = [number, number, number, number];
23
+ interface SpringOut {
24
+ stiffness: number;
25
+ damping: number;
26
+ mass: number;
27
+ visualDuration?: number;
28
+ bounce?: number;
29
+ }
30
+
31
+ const DUR_DEF = 300,
32
+ DUR_MAX = 10000,
33
+ STAGE_PAD = 16; // ms; the stage's side padding (tweaks.css)
34
+ const BOUNCE_MAX = 1; // Motion's whole range: its damping-ratio floor of 0.05 keeps a bounce of 1 settling
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);
38
+ const curveOf = (c: unknown): 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
+ }
48
+ return null;
49
+ };
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.
54
+ const springTime = (v: MotionInput) => {
55
+ const out: { mode: "time"; visualDuration?: number; bounce?: number } = { mode: "time" };
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
+ }
63
+ if (has(v.visualDuration)) out.visualDuration = +v.visualDuration!;
64
+ if (has(v.bounce)) out.bounce = clamp(+v.bounce!, -1, BOUNCE_MAX);
65
+ return out;
66
+ };
67
+ // A stiffness and damping the host wrote, kept as written rather than resolved through the editor's time pair, which
68
+ // rounds and clamps: unless a time pair rides along that isn't the one they resolve to (this control's own value back).
69
+ const exactSpring = (v: MotionInput): SpringOut | null => {
70
+ if (!has(v.stiffness) || !has(v.damping)) return null;
71
+ const k = +v.stiffness!,
72
+ d = +v.damping!,
73
+ m = has(v.mass) ? +v.mass! : 1;
74
+ if (!(k > 0 && d >= 0 && m > 0)) return null;
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 };
80
+ };
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;
84
+
85
+ function createMotion(meta: Meta, onChange: OnChange): Control {
86
+ const tr = words();
87
+ const Bezier = getControl("cubicbezier"),
88
+ Spring = getControl("spring");
89
+ if (!Bezier || !Spring) throw new Error("[tweaks] motion: the bezier and spring controls are not loaded");
90
+ const init: MotionInput = isObj(meta.value) ? meta.value : {};
91
+ let mode = modeOf(init, "easing");
92
+ let curve = curveOf(init.curve) ?? (KEYWORDS.ease.slice() as Curve),
93
+ dur = has(init.duration) ? clamp(+init.duration!, 0, DUR_MAX) : DUR_DEF;
94
+
95
+ // ── Row — label, a mini curve, a summary. ──
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");
106
+ right.append(chip, valueEl);
107
+
108
+ // ── Popover — the mode switch, the preview, one editor at a time. ──
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
+ });
117
+ curve = bezier.get() as Curve; // as the editor fitted it, so no invalid cubic-bezier()
118
+ // Host-written physics the editor can't draw exactly: it shows the nearest, clamped here so it doesn't warn as well.
119
+ let exact: SpringOut | null = null,
120
+ said = false;
121
+ const editorTime = (inp: MotionInput) => {
122
+ exact = exactSpring(inp);
123
+ if (!exact) return springTime(inp);
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
+ }
130
+ return { mode: "time" as const, visualDuration: vd, bounce: exact.bounce };
131
+ };
132
+ // Time mode only; its Mode row is hidden (tweaks.css). An edit there drops the kept physics.
133
+ const spring = Spring({ ...meta, type: "spring", label: "", value: editorTime(init) } as Meta, () => {
134
+ exact = null;
135
+ update();
136
+ emit();
137
+ });
138
+ const springNow = () => exact ?? (spring.get() as SpringOut);
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
+ );
147
+ dur = durFld.get(); // as the field snapped it
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);
175
+ body.append(typeRow, easingPane, springPane, stage);
176
+ root.append(pop);
177
+
178
+ const value = (): MotionValue => {
179
+ if (mode === "easing")
180
+ return { mode, curve: curve.slice() as Curve, duration: dur, easing: `cubic-bezier(${curve.join(", ")})` };
181
+ // The run lasts until the spring is at rest (the drawn curve stops at 2.2 s): springRun.
182
+ const s = springNow();
183
+ return { mode, ...s, ...springRun(s.stiffness, s.damping, s.mass) };
184
+ };
185
+ const emit = () => onChange(value());
186
+
187
+ // Web Animations, no frame loop to own. A click or an open flips direction and restarts; an edit lets a run in flight
188
+ // finish — a handle drag fires per pointer move, and restarting each strobed the box.
189
+ let far = false,
190
+ anim: Animation | null = null;
191
+ const replay = (flip: boolean) => {
192
+ if (flip) far = !far;
193
+ else if (anim && anim.playState === "running") return;
194
+ const v = value();
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
+ }
203
+ anim?.cancel();
204
+ const frames = [{ transform: from }, { transform: to }];
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
+ }
211
+ };
212
+ stage.addEventListener("click", () => 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
+ });
222
+
223
+ // The chip's mini curve, y up, scaled so an overshoot still fits.
224
+ const curvePath = () => {
225
+ let pts: [number, number][];
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(" ");
248
+ };
249
+ // The revealed editor built at 0×0 and onLive may have dropped its resize listener while the pop was detached, so
250
+ // re-point it at its own value, which redraws at real size.
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
+ };
260
+ // The summary shows the authored duration; a spring's emitted run (its settle) is longer.
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
+ };
270
+
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();
283
+
284
+ // Set/restore: either mode's shape or a bare keyword; derived fields (easing, a spring's duration) are ignored.
285
+ const set = (v: unknown) => {
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;
295
+ if (!inp) return;
296
+ mode = modeOf(inp, mode);
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
+ }
308
+ if (springKeys(inp)) spring.set(editorTime(inp));
309
+ modeSeg.set(mode);
310
+ showMode();
311
+ update();
312
+ };
313
+ return { el: root, set, get: value, destroy: () => spring.destroy && spring.destroy() };
314
+ }
315
+
316
+ registerControl("motion", createMotion);