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,5 +1,22 @@
1
1
  // ── Interval / range slider — dual-handle [lo,hi]. Lazy.
2
- import { el, txt, clamp, roundToStep, stepPrecision, gridEnds, normalizeRange, rangeStep, overlapsText, wireHoverClass, onReady, onLive, registerControl, rubberBand, rubberRelease } from "../shared.js";
2
+ import {
3
+ el,
4
+ txt,
5
+ clamp,
6
+ words,
7
+ roundToStep,
8
+ stepPrecision,
9
+ gridEnds,
10
+ normalizeRange,
11
+ rangeStep,
12
+ overlapsText,
13
+ wireHoverClass,
14
+ onReady,
15
+ onLive,
16
+ registerControl,
17
+ rubberBand,
18
+ rubberRelease,
19
+ } from "../shared.js";
3
20
  import { dragGesture } from "../heavy.js";
4
21
  import type { OnChange } from "../shared.js";
5
22
  import type { Meta } from "../schema.js";
@@ -7,45 +24,70 @@ import type { Control } from "../types.js";
7
24
 
8
25
  function createInterval(meta: Meta, onChange: OnChange): Control {
9
26
  const label = meta.label;
10
- const t0 = +(meta.value && meta.value[0]), t1 = +(meta.value && meta.value[1]);
11
- const lo0 = Number.isFinite(t0) ? t0 : 0, hi0 = Number.isFinite(t1) ? t1 : 1;
12
- // normalizeRange, the slider's guard: an absent or non-finite bound (data-min="abc") derives from the values, each end
13
- // widening it, for the shorthand and the verbose form alike: [20, 80] spans 0–240, not 20–80 (which pinned both handles).
27
+ const t0 = +(meta.value && meta.value[0]),
28
+ t1 = +(meta.value && meta.value[1]);
29
+ const lo0 = Number.isFinite(t0) ? t0 : 0,
30
+ hi0 = Number.isFinite(t1) ? t1 : 1;
31
+ // normalizeRange, the slider's guard: an absent or non-finite bound (data-min="abc") derives from the values, each
32
+ // end widening it, for the shorthand and the verbose form alike: [20, 80] spans 0–240, not 20–80 (which pinned both
33
+ // handles).
14
34
  let { min, max, step } = normalizeRange(meta.min, meta.max, meta.step, [lo0, hi0], label);
15
35
  [min, max] = gridEnds(min, max, step); // the slider's rule: no value past an off-grid bound
16
36
  const decimals = stepPrecision(step);
17
37
  const q = (v: number) => roundToStep(v, min, step);
18
- let lo = 0, hi = 0;
19
- const put = (a: number, b: number) => { lo = clamp(q(Math.min(a, b)), min, max); hi = clamp(q(Math.max(a, b)), min, max); }; // ordered, on the grid, in bounds
38
+ let lo = 0,
39
+ hi = 0;
40
+ // ordered, on the grid, in bounds
41
+ const put = (a: number, b: number) => {
42
+ lo = clamp(q(Math.min(a, b)), min, max);
43
+ hi = clamp(q(Math.max(a, b)), min, max);
44
+ };
20
45
  // Missing tuple entries fall back to the bounds: value:[5] gives [5, max], not [5, NaN].
21
46
  put(Number.isFinite(t0) ? t0 : min, Number.isFinite(t1) ? t1 : max);
22
47
 
23
- const wrap = el("div", "tw-slider-wrap");
24
- const track = el("div", "tw-slider tw-interval");
25
- const fill = el("div", "tw-slider-fill");
26
- const hLo = el("div", "tw-slider-handle"), hHi = el("div", "tw-slider-handle");
27
- const labelEl = txt("span", "tw-slider-label", label);
28
- const valueEl = el("span", "tw-slider-value");
48
+ const wrap = el("div", "twk-slider-wrap");
49
+ const track = el("div", "twk-slider twk-interval");
50
+ const fill = el("div", "twk-slider-fill");
51
+ const hLo = el("div", "twk-slider-handle"),
52
+ hHi = el("div", "twk-slider-handle");
53
+ const labelEl = txt("span", "twk-slider-label", label);
54
+ const valueEl = el("span", "twk-slider-value");
29
55
  track.append(fill, hLo, hHi, labelEl, valueEl);
30
56
  wrap.append(track);
31
57
 
32
- ([["minimum", hLo], ["maximum", hHi]] as const).forEach(([lab, h]) => {
33
- h.tabIndex = 0; h.setAttribute("role", "slider"); h.setAttribute("aria-label", `${label} ${lab}`);
34
- h.setAttribute("aria-valuemin", String(min)); h.setAttribute("aria-valuemax", String(max));
58
+ const tr = words();
59
+ (
60
+ [
61
+ ["intervalMin", "{label} minimum", hLo],
62
+ ["intervalMax", "{label} maximum", hHi],
63
+ ] as const
64
+ ).forEach(([k, d, h]) => {
65
+ h.tabIndex = 0;
66
+ h.setAttribute("role", "slider");
67
+ h.setAttribute("aria-label", tr(k, d, { label }));
68
+ h.setAttribute("aria-valuemin", String(min));
69
+ h.setAttribute("aria-valuemax", String(max));
35
70
  });
36
71
 
37
- const pctOf = (v: number) => ((v - min) / ((max - min) || 1)) * 100;
38
- const handleLeft = (pct: number) => `clamp(5px, calc(${pct}% - 1.5px), calc(100% - 9px))`; // stay inset at the extremes, like the slider handle
72
+ const pctOf = (v: number) => ((v - min) / (max - min || 1)) * 100;
73
+ // stay inset at the extremes, like the slider handle
74
+ const handleLeft = (pct: number) => `clamp(5px, calc(${pct}% - 1.5px), calc(100% - 9px))`;
39
75
  const render = () => {
40
- const a = pctOf(lo), b = pctOf(hi);
41
- fill.style.left = a + "%"; fill.style.width = Math.max(0, b - a) + "%";
42
- hLo.style.left = handleLeft(a); hHi.style.left = handleLeft(b);
76
+ const a = pctOf(lo),
77
+ b = pctOf(hi);
78
+ fill.style.left = a + "%";
79
+ fill.style.width = Math.max(0, b - a) + "%";
80
+ hLo.style.left = handleLeft(a);
81
+ hHi.style.left = handleLeft(b);
43
82
  valueEl.textContent = `${lo.toFixed(decimals)} – ${hi.toFixed(decimals)}`;
44
- hLo.setAttribute("aria-valuenow", String(lo)); hHi.setAttribute("aria-valuenow", String(hi));
45
- // Value-dodge (overlapsText), per handle: one sliding under the label or the readout yields so the text stays legible.
83
+ hLo.setAttribute("aria-valuenow", String(lo));
84
+ hHi.setAttribute("aria-valuenow", String(hi));
85
+ // Value-dodge (overlapsText), per handle: one sliding under the label or the readout yields so the text stays
86
+ // legible.
46
87
  const trackW = wrap.offsetWidth;
47
88
  if (trackW) {
48
- // The label's room is what the dual readout leaves, measured: 12px label inset + 8px gap + the readout's right inset.
89
+ // The label's room is what the dual readout leaves, measured: 12px label inset + 8px gap + the readout's right
90
+ // inset.
49
91
  labelEl.style.maxWidth = Math.max(0, trackW - valueEl.offsetWidth - 12 - 8 - 10) + "px";
50
92
  const dodges = (pct: number) => overlapsText(labelEl, valueEl, (pct / 100) * trackW - 1.5, 3);
51
93
  hLo.classList.toggle("is-dodge", dodges(pctOf(lo)));
@@ -55,50 +97,93 @@ function createInterval(meta: Meta, onChange: OnChange): Control {
55
97
  render();
56
98
 
57
99
  const emit = () => onChange([lo, hi]);
58
- const setLo = (v: number) => { lo = clamp(Math.min(q(v), hi), min, max); render(); };
59
- const setHi = (v: number) => { hi = clamp(Math.max(q(v), lo), min, max); render(); };
100
+ const setLo = (v: number) => {
101
+ lo = clamp(Math.min(q(v), hi), min, max);
102
+ render();
103
+ };
104
+ const setHi = (v: number) => {
105
+ hi = clamp(Math.max(q(v), lo), min, max);
106
+ render();
107
+ };
60
108
 
61
- let rect: DOMRect | null = null, scale = 1, active: "lo" | "hi" | null = null;
109
+ let rect: DOMRect | null = null,
110
+ scale = 1,
111
+ active: "lo" | "hi" | null = null;
62
112
  // Divide out any ancestor CSS transform (rect is visual px, offsetWidth layout px), as the single slider does.
63
- const valFromX = (x: number) => { const native = wrap.offsetWidth || rect.width; const p = clamp((x - rect.left) / scale / native, 0, 1); return clamp(min + p * (max - min), min, max); };
113
+ const valFromX = (x: number) => {
114
+ const native = wrap.offsetWidth || rect.width;
115
+ const p = clamp((x - rect.left) / scale / native, 0, 1);
116
+ return clamp(min + p * (max - min), min, max);
117
+ };
64
118
  const grab = (x: number) => {
65
119
  const v = valFromX(x);
66
- if (!active) active = v < lo ? "lo" : v > hi ? "hi" : (Math.abs(v - lo) <= Math.abs(v - hi) ? "lo" : "hi");
67
- active === "lo" ? setLo(v) : setHi(v); emit();
120
+ if (!active) active = v < lo ? "lo" : v > hi ? "hi" : Math.abs(v - lo) <= Math.abs(v - hi) ? "lo" : "hi";
121
+ active === "lo" ? setLo(v) : setHi(v);
122
+ emit();
68
123
  };
69
124
  dragGesture(track, {
70
125
  onDown: (e) => {
71
126
  e.preventDefault();
72
- rect = wrap.getBoundingClientRect(); scale = rect.width / (wrap.offsetWidth || rect.width);
127
+ rect = wrap.getBoundingClientRect();
128
+ scale = rect.width / (wrap.offsetWidth || rect.width);
73
129
  track.style.transition = ""; // a re-press mid spring-home stretches 1:1
74
- track.classList.add("is-active", "is-dragging"); active = null;
130
+ track.classList.add("is-active", "is-dragging");
131
+ active = null;
75
132
  grab(e.clientX);
76
- (active === "lo" ? hLo : hHi).focus({ focusVisible: false }); // preventDefault killed click-to-focus; focus without a ring
133
+ // preventDefault killed click-to-focus; focus without a ring
134
+ (active === "lo" ? hLo : hHi).focus({ focusVisible: false });
135
+ },
136
+ onMove: (e) => {
137
+ grab(e.clientX);
138
+ rubberBand(track, rect, e.clientX);
139
+ },
140
+ onEnd: () => {
141
+ rubberRelease(track);
142
+ rect = null;
143
+ active = null;
144
+ track.classList.remove("is-active", "is-dragging");
77
145
  },
78
- onMove: (e) => { grab(e.clientX); rubberBand(track, rect, e.clientX); },
79
- onEnd: () => { rubberRelease(track); rect = null; active = null; track.classList.remove("is-active", "is-dragging"); },
80
146
  });
81
147
  wireHoverClass(track, render); // re-run the dodge with the real track width on hover
82
- // Recompute once layout and fonts settle (a web-font swap shifts the widths the dodge measures) and on track-width change.
148
+ // Recompute once layout and fonts settle (a web-font swap shifts the widths the dodge measures) and on track-width
149
+ // change.
83
150
  onReady(render);
84
- onLive(track, [[window, "resize"], [window, "tw-reflow"]], render); // tw-reflow: a tab page revealing this re-measures it
151
+ // twk-reflow: a tab page revealing this re-measures it
152
+ onLive(
153
+ track,
154
+ [
155
+ [window, "resize"],
156
+ [window, "twk-reflow"],
157
+ ],
158
+ render,
159
+ );
85
160
 
86
161
  // Home/End snap a handle to its neighbor-or-limit, so the two can't cross.
87
162
  const page = (max - min) / 10 || step * 10; // PageUp/Down: a tenth of the range, as the slider jumps
88
163
  const onKey = (which: "lo" | "hi") => (e: KeyboardEvent) => {
89
164
  const nv = which === "lo" ? rangeStep(e, lo, step, min, hi, page) : rangeStep(e, hi, step, lo, max, page);
90
165
  if (nv == null) return;
91
- e.preventDefault(); which === "lo" ? setLo(nv) : setHi(nv); emit();
166
+ e.preventDefault();
167
+ which === "lo" ? setLo(nv) : setHi(nv);
168
+ emit();
92
169
  };
93
- hLo.addEventListener("keydown", onKey("lo")); hHi.addEventListener("keydown", onKey("hi"));
170
+ hLo.addEventListener("keydown", onKey("lo"));
171
+ hHi.addEventListener("keydown", onKey("hi"));
94
172
 
95
173
  return {
96
174
  el: wrap,
97
- set: (v: unknown) => { if (Array.isArray(v)) { const a = +v[0], b = +v[1]; if (!Number.isFinite(a) || !Number.isFinite(b)) return; put(a, b); render(); } },
175
+ set: (v: unknown) => {
176
+ if (Array.isArray(v)) {
177
+ const a = +v[0],
178
+ b = +v[1];
179
+ if (!Number.isFinite(a) || !Number.isFinite(b)) return;
180
+ put(a, b);
181
+ render();
182
+ }
183
+ },
98
184
  get: () => [lo, hi],
99
185
  range: [min, max],
100
186
  };
101
187
  }
102
188
 
103
189
  registerControl("interval", createInterval);
104
-
@@ -1,105 +1,211 @@
1
1
  // ── Monitor + FPS graph — live sparkline / readout. Lazy; registers both types.
2
- import { el, txt, clamp, blade, json, registerControl } from "../shared.js";
2
+ import { el, txt, clamp, blade, json, registerControl, words } from "../shared.js";
3
3
  import { fitCanvas, accentColor } from "../heavy.js";
4
4
  import type { Meta } from "../schema.js";
5
5
 
6
6
  // NaN means no sample yet: the pen lifts rather than drawing a false zero.
7
- const strokeSeries = (ctx: CanvasRenderingContext2D, node: Element, w: number, h: number, samples: number[], start: number, frac: (s: number) => number) => {
7
+ const strokeSeries = (
8
+ ctx: CanvasRenderingContext2D,
9
+ node: Element,
10
+ w: number,
11
+ h: number,
12
+ samples: number[],
13
+ start: number,
14
+ frac: (s: number) => number,
15
+ ) => {
8
16
  const N = samples.length;
9
17
  ctx.clearRect(0, 0, w, h);
10
18
  ctx.strokeStyle = accentColor(node);
11
- ctx.lineWidth = 1.5; ctx.lineJoin = "round"; ctx.beginPath();
19
+ ctx.lineWidth = 1.5;
20
+ ctx.lineJoin = "round";
21
+ ctx.beginPath();
12
22
  let pen = false;
13
23
  for (let k = 0; k < N; k++) {
14
24
  const s = samples[(start + k) % N];
15
25
  if (Number.isNaN(s)) continue;
16
- const x = (k / (N - 1)) * w, y = h - clamp(frac(s), 0, 1) * h;
17
- pen ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), pen = true);
26
+ const x = (k / (N - 1)) * w,
27
+ y = h - clamp(frac(s), 0, 1) * h;
28
+ pen ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), (pen = true));
18
29
  }
19
30
  ctx.stroke();
20
31
  };
21
32
 
22
33
  // Manual, not onLive: these blades idle through the built-but-not-mounted window, which onLive would read as gone.
23
34
  const watchReflow = (fn: () => void) => {
24
- for (const t of ["resize", "tw-reflow"]) window.addEventListener(t, fn);
25
- return () => { for (const t of ["resize", "tw-reflow"]) window.removeEventListener(t, fn); };
35
+ for (const t of ["resize", "twk-reflow"]) window.addEventListener(t, fn);
36
+ return () => {
37
+ for (const t of ["resize", "twk-reflow"]) window.removeEventListener(t, fn);
38
+ };
26
39
  };
27
40
 
28
41
  // ── Monitor — a number as a sparkline or a readout, a string as a buffer of the last few values; read on an interval
29
42
  // or, `perFrame` (the FPS graph), once per animation frame. ──
30
43
  function createMonitor(meta: Meta, perFrame = false) {
31
44
  const get: (now?: number) => unknown = typeof meta.get === "function" ? meta.get : () => meta.value;
32
- const interval = Math.max(30, Number.isFinite(+meta.interval) ? +meta.interval : 200); // NaN would make setInterval a 0 ms busy-poll
33
- let probe: unknown; try { probe = get(); } catch {}
45
+ // NaN would make setInterval a 0 ms busy-poll
46
+ const interval = Math.max(30, Number.isFinite(+meta.interval) ? +meta.interval : 200);
47
+ let probe: unknown;
48
+ try {
49
+ probe = get();
50
+ } catch {}
34
51
  const isNum = typeof probe === "number";
35
52
  const graph = meta.view === "graph" || (isNum && meta.graph !== false && meta.view !== "text" && meta.rows == null);
36
53
 
37
- const wrap = el("div", "tw-fps tw-monitor");
38
- const val = txt("span", "tw-fps-val", "—");
39
- wrap.append(txt("span", "tw-fps-label", meta.label ?? "Monitor"), val);
54
+ const wrap = el("div", "twk-fps twk-monitor");
55
+ const val = txt("span", "twk-fps-val", "—");
56
+ wrap.append(txt("span", "twk-fps-label", meta.label ?? words()("monitor", "Monitor")), val);
40
57
 
41
- let timer = 0, raf = 0, unwatch = () => {}, wasConnected = false;
58
+ let timer = 0,
59
+ raf = 0,
60
+ unwatch = () => {},
61
+ wasConnected = false;
42
62
  // toFixed throws past 100; a missing value reads as a dash.
43
63
  const decimals = Number.isFinite(+meta.decimals) ? Math.min(20, Math.max(0, Math.floor(+meta.decimals))) : 2;
44
- const fmt = (v: unknown) => (v == null ? "—" : typeof v === "number" ? (Number.isInteger(v) || !Number.isFinite(v) ? String(v) : v.toFixed(decimals)) : typeof v === "object" ? json(v) : String(v));
64
+ const fmt = (v: unknown) =>
65
+ v == null
66
+ ? "—"
67
+ : typeof v === "number"
68
+ ? Number.isInteger(v) || !Number.isFinite(v)
69
+ ? String(v)
70
+ : v.toFixed(decimals)
71
+ : typeof v === "object"
72
+ ? json(v)
73
+ : String(v);
45
74
  // Also the blade's `destroy`: a panel torn down before it ever connected can never self-stop below.
46
- const stop = () => { if (timer) clearInterval(timer); timer = 0; if (raf) cancelAnimationFrame(raf); raf = 0; unwatch(); };
75
+ const stop = () => {
76
+ if (timer) clearInterval(timer);
77
+ timer = 0;
78
+ if (raf) cancelAnimationFrame(raf);
79
+ raf = 0;
80
+ unwatch();
81
+ };
47
82
  // Never-mounted-yet idles the tick; only a mount-then-remove, or destroy(), stops it. The next frame is booked before
48
83
  // the read, so a stop() inside the tick cancels it.
49
84
  const poll = (fn: (v: unknown) => void) => {
50
85
  const tick = (now?: number) => {
51
86
  if (perFrame) raf = requestAnimationFrame(tick);
52
- if (!wrap.isConnected) { if (wasConnected) stop(); return; }
87
+ if (!wrap.isConnected) {
88
+ if (wasConnected) stop();
89
+ return;
90
+ }
53
91
  wasConnected = true;
54
- let v: unknown; try { v = get(now); } catch { return; }
92
+ let v: unknown;
93
+ try {
94
+ v = get(now);
95
+ } catch {
96
+ return;
97
+ }
55
98
  fn(v);
56
99
  };
57
- if (perFrame) raf = requestAnimationFrame(tick); else timer = setInterval(tick, interval);
100
+ if (perFrame) raf = requestAnimationFrame(tick);
101
+ else timer = setInterval(tick, interval);
58
102
  };
59
103
 
60
104
  if (!graph && meta.rows) {
61
105
  val.remove();
62
106
  // A negative or NaN rows spun the trim loop forever.
63
107
  const rows = Math.max(1, Math.floor(+meta.rows) || 1);
64
- const buf = el("pre", "tw-monitor-buffer"); buf.style.setProperty("--tw-monitor-rows", rows);
108
+ const buf = el("pre", "twk-monitor-buffer");
109
+ buf.style.setProperty("--twk-monitor-rows", rows);
65
110
  wrap.append(buf);
66
111
  const lines: string[] = [];
67
- poll((v) => { lines.push(fmt(v)); while (lines.length > rows) lines.shift(); buf.textContent = lines.join("\n"); });
112
+ poll((v) => {
113
+ lines.push(fmt(v));
114
+ while (lines.length > rows) lines.shift();
115
+ buf.textContent = lines.join("\n");
116
+ });
117
+ return blade(wrap, stop);
118
+ }
119
+ const readout = (v: unknown) => {
120
+ val.textContent = fmt(v);
121
+ };
122
+ if (!graph) {
123
+ poll(readout);
68
124
  return blade(wrap, stop);
69
125
  }
70
- const readout = (v: unknown) => { val.textContent = fmt(v); };
71
- if (!graph) { poll(readout); return blade(wrap, stop); }
72
126
 
73
- const canvas = document.createElement("canvas"); canvas.className = "tw-fps-canvas";
127
+ const canvas = document.createElement("canvas");
128
+ canvas.className = "twk-fps-canvas";
74
129
  wrap.append(canvas);
75
130
  const ctx = canvas.getContext("2d");
76
131
  // No 2D context (a headless DOM, a blocked canvas) degrades to the text readout.
77
- if (!ctx) { poll(readout); return blade(wrap, stop); }
78
- const N = 80, samples: number[] = new Array(N).fill(NaN);
79
- let idx = 0, w = 0, h = 0;
80
- const onResize = () => { [w, h] = fitCanvas(canvas, ctx, 2); };
132
+ if (!ctx) {
133
+ poll(readout);
134
+ return blade(wrap, stop);
135
+ }
136
+ const N = 80,
137
+ samples: number[] = new Array(N).fill(NaN);
138
+ let idx = 0,
139
+ w = 0,
140
+ h = 0;
141
+ const onResize = () => {
142
+ [w, h] = fitCanvas(canvas, ctx, 2);
143
+ };
81
144
  unwatch = watchReflow(onResize);
82
145
  const draw = () => {
83
146
  if (!w) onResize(); // measured 0 while detached: fit on the first live draw
84
147
  if (!w) return;
85
- let lo = meta.min, hi = meta.max;
148
+ let lo = meta.min,
149
+ hi = meta.max;
86
150
  if (lo == null || hi == null) {
87
- let mn = Infinity, mx = -Infinity;
88
- for (const s of samples) if (Number.isFinite(s)) { if (s < mn) mn = s; if (s > mx) mx = s; } // an Infinity sample would blank the auto-range
89
- if (mn === Infinity) { mn = 0; mx = 1; } else if (mn === mx) { mn -= 0.5; mx += 0.5; }
151
+ let mn = Infinity,
152
+ mx = -Infinity;
153
+ // an Infinity sample would blank the auto-range
154
+ for (const s of samples)
155
+ if (Number.isFinite(s)) {
156
+ if (s < mn) mn = s;
157
+ if (s > mx) mx = s;
158
+ }
159
+ if (mn === Infinity) {
160
+ mn = 0;
161
+ mx = 1;
162
+ } else if (mn === mx) {
163
+ mn -= 0.5;
164
+ mx += 0.5;
165
+ }
90
166
  const pad = (mx - mn) * 0.1;
91
- if (lo == null) lo = mn - pad; if (hi == null) hi = mx + pad;
167
+ if (lo == null) lo = mn - pad;
168
+ if (hi == null) hi = mx + pad;
92
169
  }
93
- const span = (hi - lo) || 1;
170
+ const span = hi - lo || 1;
94
171
  strokeSeries(ctx, wrap, w, h, samples, idx, (s) => (s - lo) / span);
95
172
  };
96
- poll((v) => { readout(v); if (typeof v === "number" && Number.isFinite(v)) { samples[idx] = v; idx = (idx + 1) % N; draw(); } }); // a non-finite sample shows in the readout, not the graph
173
+ // a non-finite sample shows in the readout, not the graph
174
+ poll((v) => {
175
+ readout(v);
176
+ if (typeof v === "number" && Number.isFinite(v)) {
177
+ samples[idx] = v;
178
+ idx = (idx + 1) % N;
179
+ draw();
180
+ }
181
+ });
97
182
  return blade(wrap, stop);
98
183
  }
99
184
 
100
185
  // ── FPS graph — each frame's timestamp becomes the rate since the last (none on the first or for the probe), 0–120. ──
101
- const frameRate = () => { let last = 0; return (now?: number) => { if (!(now! > 0)) return undefined; const fps = last ? 1000 / (now! - last) : undefined; last = now!; return fps; }; };
102
- const createFps = (meta: Meta) => createMonitor({ ...meta, label: meta.label ?? "FPS", get: frameRate(), view: "graph", min: 0, max: 120, decimals: 0 }, true); // ??: an explicit "" label renders none
186
+ const frameRate = () => {
187
+ let last = 0;
188
+ return (now?: number) => {
189
+ if (!(now! > 0)) return undefined;
190
+ const fps = last ? 1000 / (now! - last) : undefined;
191
+ last = now!;
192
+ return fps;
193
+ };
194
+ };
195
+ // ??: an explicit "" label renders none
196
+ const createFps = (meta: Meta) =>
197
+ createMonitor(
198
+ {
199
+ ...meta,
200
+ label: meta.label ?? words()("fps", "FPS"),
201
+ get: frameRate(),
202
+ view: "graph",
203
+ min: 0,
204
+ max: 120,
205
+ decimals: 0,
206
+ },
207
+ true,
208
+ );
103
209
 
104
210
  registerControl("fpsgraph", createFps);
105
211
  registerControl("monitor", (meta: Meta) => createMonitor(meta)); // the registry passes (meta, onChange), not perFrame