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,15 @@
1
1
  // ── Point — 2D/3D/4D vector. Lazy.
2
- import { el, clamp, stepPrecision, defaultRange, popover, registerControl } from "../shared.js";
3
- import { numField, svgEl, grabSurface, boxFrac, arrowStep, sayWhenKeysStop, triggerRow, decimalStep } from "../heavy.js";
2
+ import { el, clamp, stepPrecision, defaultRange, popover, registerControl, words } from "../shared.js";
3
+ import {
4
+ numField,
5
+ svgEl,
6
+ grabSurface,
7
+ boxFrac,
8
+ arrowStep,
9
+ sayWhenKeysStop,
10
+ triggerRow,
11
+ decimalStep,
12
+ } from "../heavy.js";
4
13
  import type { OnChange, Built } from "../shared.js";
5
14
  import type { Meta, PointComponent } from "../schema.js";
6
15
  import type { Control } from "../types.js";
@@ -8,19 +17,21 @@ import type { Control } from "../types.js";
8
17
  // A trigger row opens the 2D pad over the component fields in a popover. The fields hold the values; readout, thumb
9
18
  // and emitted map read off them. `pad: false` opts out.
10
19
  function createPoint(meta: Meta, onChange: OnChange): Control {
20
+ const tr = words();
11
21
  const comps = meta.components; // [{ key, label, value, step, min, max }]
12
22
 
13
23
  // ── Trigger row — label + value + a mini pad preview. ──
14
- const { root, trigger, right } = triggerRow("tw-point", meta.label ?? "Point"); // ??: an explicit "" label renders none
15
- const valueEl = el("span", "tw-trigger-value");
16
- const preview = el("div", "tw-trigger-chip tw-point-preview");
17
- const previewDot = el("div", "tw-point-preview-dot");
24
+ // ??: an explicit "" label renders none
25
+ const { root, trigger, right } = triggerRow("twk-point", meta.label ?? tr("point", "Point"));
26
+ const valueEl = el("span", "twk-trigger-value");
27
+ const preview = el("div", "twk-trigger-chip twk-point-preview");
28
+ const previewDot = el("div", "twk-point-preview-dot");
18
29
  preview.append(previewDot);
19
30
  right.append(preview, valueEl); // chip first, as every chip row
20
31
 
21
32
  // ── Popover — the 2D pad over the component fields. ──
22
- const pop = el("div", "tw-color-pop tw-point-pop");
23
- const body = el("div", "tw-point-body");
33
+ const pop = el("div", "twk-color-pop twk-point-pop");
34
+ const body = el("div", "twk-point-body");
24
35
  pop.append(body);
25
36
 
26
37
  const hasPad = meta.pad !== false && comps.length >= 2;
@@ -29,78 +40,150 @@ function createPoint(meta: Meta, onChange: OnChange): Control {
29
40
  // its steps (337 over ±1011 stepped by 10 to 340), as the slider's step fits its default.
30
41
  const rangeOf = (c: PointComponent) => {
31
42
  const half = defaultRange(Math.abs(Number.isFinite(+c.value) ? +c.value : 0))[1];
32
- let lo = c.min ?? -half, hi = c.max ?? half;
33
- // A lone bound at or past the other end spans from itself instead, as the slider's does (min 5 over 0 spans 5–35, not 5–1, swapped).
34
- if ((c.min == null) !== (c.max == null) && !(hi > lo)) { const w = 2 * defaultRange(Math.abs(+(c.min ?? c.max)!))[1]; if (c.min == null) lo = hi - w; else hi = lo + w; }
35
- const fine = 10 ** Math.floor(Math.log10((hi - lo) / 200)), nudge = c.step ?? (fine > 0 && Number.isFinite(fine) ? fine : 1); // a degenerate range (hi ≤ lo, NaN) keeps a step of 1
36
- return { lo, hi, nudge, step: c.step ?? Math.min(nudge, decimalStep(c.value), decimalStep(c.min)) }; // the pad's arrows keep the decade: 3.14159's 0.00001 would crawl
43
+ let lo = c.min ?? -half,
44
+ hi = c.max ?? half;
45
+ // A lone bound at or past the other end spans from itself instead, as the slider's does (min 5 over 0 spans 5–35,
46
+ // not 5–1, swapped).
47
+ if ((c.min == null) !== (c.max == null) && !(hi > lo)) {
48
+ const w = 2 * defaultRange(Math.abs(+(c.min ?? c.max)!))[1];
49
+ if (c.min == null) lo = hi - w;
50
+ else hi = lo + w;
51
+ }
52
+ // a degenerate range (hi ≤ lo, NaN) keeps a step of 1
53
+ const fine = 10 ** Math.floor(Math.log10((hi - lo) / 200)),
54
+ nudge = c.step ?? (fine > 0 && Number.isFinite(fine) ? fine : 1);
55
+ // the pad's arrows keep the decade: 3.14159's 0.00001 would crawl
56
+ return { lo, hi, nudge, step: c.step ?? Math.min(nudge, decimalStep(c.value), decimalStep(c.min)) };
37
57
  };
38
58
  const ranges = comps.map(rangeOf);
39
- const rx = ranges[0], ry = ranges[1];
40
- const minX = rx?.lo ?? -1, maxX = rx?.hi ?? 1, minY = ry?.lo ?? -1, maxY = ry?.hi ?? 1;
59
+ const rx = ranges[0],
60
+ ry = ranges[1];
61
+ const minX = rx?.lo ?? -1,
62
+ maxX = rx?.hi ?? 1,
63
+ minY = ry?.lo ?? -1,
64
+ maxY = ry?.hi ?? 1;
41
65
  const frac = (v: number, lo: number, hi: number) => (hi > lo ? clamp((v - lo) / (hi - lo), 0, 1) : 0.5);
42
66
 
43
- let padHost: HTMLDivElement | null = null, padThumb: HTMLDivElement | null = null, padLine: Built<SVGLineElement> | null = null;
67
+ let padHost: HTMLDivElement | null = null,
68
+ padThumb: HTMLDivElement | null = null,
69
+ padLine: Built<SVGLineElement> | null = null;
44
70
  if (hasPad) {
45
- padHost = el("div", "tw-pad");
46
- padThumb = el("div", "tw-pad-thumb");
71
+ padHost = el("div", "twk-pad");
72
+ padThumb = el("div", "twk-pad-thumb");
47
73
  // A tether from the origin to the thumb, so the point reads as a vector; the pad is square so the angle is true.
48
- const padSvg = svgEl("svg", "tw-pad-line"); padSvg.setAttribute("viewBox", "0 0 100 100"); padSvg.setAttribute("preserveAspectRatio", "none");
49
- padLine = svgEl("line"); padSvg.append(padLine);
74
+ const padSvg = svgEl("svg", "twk-pad-line");
75
+ padSvg.setAttribute("viewBox", "0 0 100 100");
76
+ padSvg.setAttribute("preserveAspectRatio", "none");
77
+ padLine = svgEl("line");
78
+ padSvg.append(padLine);
50
79
  // The origin is fixed for the control's life: set x1/y1 once.
51
- const oy0 = frac(0, minY, maxY); padLine.setAttribute("x1", frac(0, minX, maxX) * 100); padLine.setAttribute("y1", (meta.invertY ? oy0 : 1 - oy0) * 100);
52
- padHost.append(el("div", "tw-pad-axis tw-pad-axis-x"), el("div", "tw-pad-axis tw-pad-axis-y"), padSvg, padThumb);
80
+ const oy0 = frac(0, minY, maxY);
81
+ padLine.setAttribute("x1", frac(0, minX, maxX) * 100);
82
+ padLine.setAttribute("y1", (meta.invertY ? oy0 : 1 - oy0) * 100);
83
+ padHost.append(
84
+ el("div", "twk-pad-axis twk-pad-axis-x"),
85
+ el("div", "twk-pad-axis twk-pad-axis-y"),
86
+ padSvg,
87
+ padThumb,
88
+ );
53
89
  body.append(padHost);
54
90
  }
55
91
 
56
- const fields = el("div", "tw-fields");
92
+ const fields = el("div", "twk-fields");
57
93
  body.append(fields);
58
- const sync = () => { positionPad(); paintValue(); };
94
+ const sync = () => {
95
+ positionPad();
96
+ paintValue();
97
+ };
59
98
  const emit = () => onChange(read());
60
99
  const flds = comps.map((c, k) => {
61
- const fld = numField({ label: c.label, value: c.value ?? 0, step: ranges[k].step, min: c.min, max: c.max }, () => { sync(); emit(); });
62
- fields.append(fld.el); return fld;
100
+ const fld = numField(
101
+ { label: c.label ?? c.key, value: c.value ?? 0, step: ranges[k].step, min: c.min, max: c.max },
102
+ () => {
103
+ sync();
104
+ emit();
105
+ },
106
+ );
107
+ fields.append(fld.el);
108
+ return fld;
63
109
  });
64
- const read = () => Object.fromEntries(comps.map((c, k) => [c.key, flds[k].get()])); // the emitted map, straight off the fields
110
+ // the emitted map, straight off the fields
111
+ const read = () => Object.fromEntries(comps.map((c, k) => [c.key, flds[k].get()]));
65
112
 
66
- // Readout: the components at each step's precision, so a value reads "−1.00, −0.46" (steady columns), not "−1, −0.46".
113
+ // Readout: the components at each step's precision, so a value reads "−1.00, −0.46" (steady columns), not
114
+ // "−1, −0.46".
67
115
  const fmt = (v: number, step: number) => (+v).toFixed(stepPrecision(step));
68
- const paintValue = () => { valueEl.textContent = flds.map((f, k) => fmt(f.get(), ranges[k].step)).join(", "); };
69
- let positionPad = () => { previewDot.style.left = "50%"; previewDot.style.top = "50%"; };
116
+ const paintValue = () => {
117
+ valueEl.textContent = flds.map((f, k) => fmt(f.get(), ranges[k].step)).join(", ");
118
+ };
119
+ let positionPad = () => {
120
+ previewDot.style.left = "50%";
121
+ previewDot.style.top = "50%";
122
+ };
70
123
 
71
124
  if (hasPad) {
72
125
  // The mini preview is clipped, so inset the 5px dot: its center travels from +2.5px at 0 to −2.5px at 1.
73
126
  const DOT = 5;
74
127
  const inset = (f: number) => `calc(${(f * 100).toFixed(2)}% + ${((0.5 - f) * DOT).toFixed(2)}px)`;
75
128
  positionPad = () => {
76
- const fx = frac(flds[0].get(), minX, maxX), fyv = frac(flds[1].get(), minY, maxY);
129
+ const fx = frac(flds[0].get(), minX, maxX),
130
+ fyv = frac(flds[1].get(), minY, maxY);
77
131
  const tyFrac = meta.invertY ? fyv : 1 - fyv;
78
- const tx = fx * 100, ty = tyFrac * 100;
79
- padThumb.style.left = tx + "%"; padThumb.style.top = ty + "%";
80
- previewDot.style.left = inset(fx); previewDot.style.top = inset(tyFrac); // mini pad: dot stays in bounds
81
- padLine.setAttribute("x2", tx); padLine.setAttribute("y2", ty);
82
- padHost.setAttribute("aria-description", `${comps[0].label ?? comps[0].key} ${fmt(flds[0].get(), rx.step)}, ${comps[1].label ?? comps[1].key} ${fmt(flds[1].get(), ry.step)}`); // the pad's live readout for assistive tech
132
+ const tx = fx * 100,
133
+ ty = tyFrac * 100;
134
+ padThumb.style.left = tx + "%";
135
+ padThumb.style.top = ty + "%";
136
+ previewDot.style.left = inset(fx);
137
+ previewDot.style.top = inset(tyFrac); // mini pad: dot stays in bounds
138
+ padLine.setAttribute("x2", tx);
139
+ padLine.setAttribute("y2", ty);
140
+ // the pad's live readout for assistive tech
141
+ padHost.setAttribute(
142
+ "aria-description",
143
+ `${comps[0].label ?? comps[0].key} ${fmt(flds[0].get(), rx.step)}, ${comps[1].label ?? comps[1].key} ${fmt(flds[1].get(), ry.step)}`,
144
+ );
83
145
  };
84
146
  const padSet = (e: PointerEvent) => {
85
- const [fx, fy] = boxFrac(e, padHost); const yFrac = meta.invertY ? fy : 1 - fy;
86
- flds[0].set(minX + fx * (maxX - minX)); flds[1].set(minY + yFrac * (maxY - minY));
87
- sync(); emit();
147
+ const [fx, fy] = boxFrac(e, padHost);
148
+ const yFrac = meta.invertY ? fy : 1 - fy;
149
+ flds[0].set(minX + fx * (maxX - minX));
150
+ flds[1].set(minY + yFrac * (maxY - minY));
151
+ sync();
152
+ emit();
88
153
  };
89
154
  grabSurface(padHost, padSet);
90
155
  // Keyboard: a focusable group (no ARIA role fits a 2D point; the readout rides on aria-description). Arrows move a
91
- // pad step (⇧ = ten), ↑ always up on screen; Home restores the default. Escape is the popover's — a blur here would close it first.
92
- padHost.tabIndex = 0; padHost.setAttribute("role", "group");
93
- padHost.setAttribute("aria-label", `${meta.label ?? "Point"} pad: ${comps[0].label ?? comps[0].key} and ${comps[1].label ?? comps[1].key}`);
156
+ // pad step (⇧ = ten), ↑ always up on screen; Home restores the default. Escape is the popover's — a blur here would
157
+ // close it first.
158
+ padHost.tabIndex = 0;
159
+ padHost.setAttribute("role", "group");
160
+ padHost.setAttribute(
161
+ "aria-label",
162
+ tr("pointPad", "{label} pad: {x} and {y}", {
163
+ label: meta.label ?? tr("point", "Point"),
164
+ x: comps[0].label ?? comps[0].key,
165
+ y: comps[1].label ?? comps[1].key,
166
+ }),
167
+ );
94
168
  const say = sayWhenKeysStop(padHost);
95
169
  padHost.addEventListener("keydown", (e) => {
96
- if (e.key === "Home") { e.preventDefault(); comps.forEach((c, k) => flds[k].set(c.value ?? 0)); sync(); emit(); say(); return; }
170
+ if (e.key === "Home") {
171
+ e.preventDefault();
172
+ comps.forEach((c, k) => flds[k].set(c.value ?? 0));
173
+ sync();
174
+ emit();
175
+ say();
176
+ return;
177
+ }
97
178
  const [dx, dy] = arrowStep(e);
98
179
  if (!dx && !dy) return;
99
180
  e.preventDefault();
100
181
  const k = e.shiftKey ? 10 : 1;
101
182
  if (dx) flds[0].set(clamp(flds[0].get() + dx * rx.nudge * k, minX, maxX));
102
183
  if (dy) flds[1].set(clamp(flds[1].get() + (meta.invertY ? -dy : dy) * ry.nudge * k, minY, maxY));
103
- sync(); emit(); say();
184
+ sync();
185
+ emit();
186
+ say();
104
187
  });
105
188
  }
106
189
 
@@ -110,7 +193,13 @@ function createPoint(meta: Meta, onChange: OnChange): Control {
110
193
 
111
194
  return {
112
195
  el: root,
113
- set: (v: Record<string, number | string>) => { if (v) comps.forEach((c, k) => { if (v[c.key] != null) flds[k].set(v[c.key]); }); sync(); },
196
+ set: (v: Record<string, number | string>) => {
197
+ if (v)
198
+ comps.forEach((c, k) => {
199
+ if (v[c.key] != null) flds[k].set(v[c.key]);
200
+ });
201
+ sync();
202
+ },
114
203
  get: read,
115
204
  range: Object.fromEntries(comps.map((c, k) => [c.key, flds[k].range])),
116
205
  };
@@ -1,7 +1,7 @@
1
1
  // ── Shadow — a box-shadow editor with the kit's own picker mounted inline (popover() is single-open). The value carries
2
2
  // the CSS as `css`, built only from the numbers and a color the picker accepted as one color token (isColor) — a host
3
3
  // string reaching a `style` sink could splice in a url(). Lazy. ──
4
- import { el, txt, popover, createSegmented, registerControl, btn, icon, isObj } from "../shared.js";
4
+ import { el, txt, popover, createSegmented, registerControl, btn, icon, isObj, words } from "../shared.js";
5
5
  import { numField, triggerRow } from "../heavy.js";
6
6
  import { createPickerBody, isColor } from "./color.js";
7
7
  import type { OnChange, NumField } from "../shared.js";
@@ -16,64 +16,137 @@ const COLOR_RE = /#[0-9a-f]{3,8}\b|(?:oklch|oklab|rgba?|hsla?|hwb|lab|lch|color)
16
16
  const LEN_RE = /^(-?\d*\.?\d+)(px)?$/;
17
17
  // Strict: optional `inset`, two to four px lengths, one color, one layer; anything else is null.
18
18
  const parseShadow = (s: string): ShadowInput | null => {
19
- const m = COLOR_RE.exec(s); if (!m || !isColor(m[0])) return null;
19
+ const m = COLOR_RE.exec(s);
20
+ if (!m || !isColor(m[0])) return null;
20
21
  const rest = (s.slice(0, m.index) + " " + s.slice(m.index + m[0].length)).trim().split(/\s+/).filter(Boolean);
21
- const inset = rest.includes("inset"), lens = rest.filter((t) => t !== "inset");
22
+ const inset = rest.includes("inset"),
23
+ lens = rest.filter((t) => t !== "inset");
22
24
  if (lens.length < 2 || lens.length > 4 || !lens.every((t) => LEN_RE.test(t))) return null;
23
25
  const n = lens.map((t) => +t.replace(/px$/, ""));
24
26
  return { inset, x: n[0], y: n[1], blur: n[2] ?? 0, spread: n[3] ?? 0, color: m[0] };
25
27
  };
26
28
 
27
- const CHEVRON = icon('<path d="M18 9s-4.4 6-6 6-6-6-6-6"/>', "tw-shadow-chevron", 2.5); // the kit's chevron; importing icons.ts splits a 4th chunk
29
+ // the kit's chevron; importing icons.ts splits a 4th chunk
30
+ const CHEVRON = icon('<path d="M18 9s-4.4 6-6 6-6-6-6-6"/>', "twk-shadow-chevron", 2.5);
28
31
  function createShadow(meta: Meta, onChange: OnChange): Control {
32
+ const tr = words();
29
33
  const read = (v: unknown): ShadowInput | null => (typeof v === "string" ? parseShadow(v) : isObj(v) ? v : null);
30
34
  const init = read(meta.value ?? meta) || {}; // the fields may sit on the meta itself (verbose form)
31
35
  let inset = !!init.inset;
32
36
 
33
37
  // ── Row — label, chip, summary. ──
34
- const { root, trigger, right } = triggerRow("tw-shadow", meta.label ?? "Shadow"); // ??: an explicit "" label renders none
35
- const chip = el("span", "tw-trigger-chip tw-shadow-chip"), chipTile = el("span", "tw-shadow-chip-tile"); chip.append(chipTile);
36
- const valueEl = el("span", "tw-trigger-value");
38
+ // ??: an explicit "" label renders none
39
+ const { root, trigger, right } = triggerRow("twk-shadow", meta.label ?? tr("shadow", "Shadow"));
40
+ const chip = el("span", "twk-trigger-chip twk-shadow-chip"),
41
+ chipTile = el("span", "twk-shadow-chip-tile");
42
+ chip.append(chipTile);
43
+ const valueEl = el("span", "twk-trigger-value");
37
44
  right.append(chip, valueEl);
38
45
 
39
46
  // ── Popover — a preview card, the position switch, the four lengths, the picker. ──
40
- const pop = el("div", "tw-color-pop tw-shadow-pop");
41
- const stage = el("div", "tw-shadow-stage"), card = el("div", "tw-shadow-card"); stage.append(card);
42
- const insetSeg = createSegmented([{ value: "outer", label: "Outer" }, { value: "inner", label: "Inner" }], inset ? "inner" : "outer", (m: unknown) => { if ((m === "inner") !== inset) { inset = m === "inner"; update(); emit(); } }, "Shadow position");
43
- const insetRow = el("div", "tw-row"); insetRow.append(txt("span", "tw-row-label", "Position"), insetSeg.el);
44
- const fields = el("div", "tw-fields tw-shadow-fields");
45
- // The four lengths live in their fields, read as the fields fit them: an authored 2.5 shows "3" and emits 3px, and the
46
- // blur's min clamps a negative one to 0.
47
- const field = (label: string, value: number, min?: number): NumField => { const f = numField({ label, value, step: 1, min, unit: "px" }, () => { update(); emit(); }); fields.append(f.el); return f; };
48
- const fx = field("X", num(init.x, DEF.x)), fy = field("Y", num(init.y, DEF.y)), fb = field("Blur", num(init.blur, DEF.blur), 0), fs = field("Spread", num(init.spread, DEF.spread));
47
+ const pop = el("div", "twk-color-pop twk-shadow-pop");
48
+ const stage = el("div", "twk-shadow-stage"),
49
+ card = el("div", "twk-shadow-card");
50
+ stage.append(card);
51
+ const insetSeg = createSegmented(
52
+ [
53
+ { value: "outer", label: tr("outer", "Outer") },
54
+ { value: "inner", label: tr("inner", "Inner") },
55
+ ],
56
+ inset ? "inner" : "outer",
57
+ (m: unknown) => {
58
+ if ((m === "inner") !== inset) {
59
+ inset = m === "inner";
60
+ update();
61
+ emit();
62
+ }
63
+ },
64
+ tr("shadowPosition", "Shadow position"),
65
+ );
66
+ const insetRow = el("div", "twk-row");
67
+ insetRow.append(txt("span", "twk-row-label", tr("position", "Position")), insetSeg.el);
68
+ const fields = el("div", "twk-fields twk-shadow-fields");
69
+ // The four lengths live in their fields, read as the fields fit them: an authored 2.5 shows "3" and emits 3px, and
70
+ // the blur's min clamps a negative one to 0.
71
+ const field = (label: string, value: number, min?: number): NumField => {
72
+ const f = numField({ label, value, step: 1, min, unit: "px" }, () => {
73
+ update();
74
+ emit();
75
+ });
76
+ fields.append(f.el);
77
+ return f;
78
+ };
79
+ const fx = field("X", num(init.x, DEF.x)),
80
+ fy = field("Y", num(init.y, DEF.y)),
81
+ fb = field(tr("blur", "Blur"), num(init.blur, DEF.blur), 0),
82
+ fs = field(tr("spread", "Spread"), num(init.spread, DEF.spread));
49
83
  const lengths = () => [fx, fy, fb, fs].map((f) => f.get());
50
84
  // The color row discloses the picker, so the pop opens compact.
51
- const colorRow = btn("tw-trigger tw-shadow-color"); colorRow.setAttribute("aria-expanded", "false");
52
- const cSwatch = el("span", "tw-trigger-chip tw-color-swatch"), cText = el("span", "tw-trigger-value"), cRight = el("span", "tw-trigger-right");
53
- cRight.append(cSwatch, cText); cRight.insertAdjacentHTML("beforeend", CHEVRON); colorRow.append(txt("span", "tw-trigger-label", "Color"), cRight);
54
- const body = createPickerBody({ value: typeof init.color === "string" && isColor(init.color) ? init.color : DEF.color, onMode: () => update() }, () => { update(); emit(); });
85
+ const colorRow = btn("twk-trigger twk-shadow-color");
86
+ colorRow.setAttribute("aria-expanded", "false");
87
+ const cSwatch = el("span", "twk-trigger-chip twk-color-swatch"),
88
+ cText = el("span", "twk-trigger-value"),
89
+ cRight = el("span", "twk-trigger-right");
90
+ cRight.append(cSwatch, cText);
91
+ cRight.insertAdjacentHTML("beforeend", CHEVRON);
92
+ colorRow.append(txt("span", "twk-trigger-label", tr("color", "Color")), cRight);
93
+ const body = createPickerBody(
94
+ { value: typeof init.color === "string" && isColor(init.color) ? init.color : DEF.color, onMode: () => update() },
95
+ () => {
96
+ update();
97
+ emit();
98
+ },
99
+ );
55
100
  body.el.hidden = true;
56
- colorRow.addEventListener("click", () => { const show = body.el.hidden; body.el.hidden = !show; colorRow.setAttribute("aria-expanded", String(show)); pop_.reflow(); }); // re-place for the new height; the picker lays out now
101
+ // re-place for the new height; the picker lays out now
102
+ colorRow.addEventListener("click", () => {
103
+ const show = body.el.hidden;
104
+ body.el.hidden = !show;
105
+ colorRow.setAttribute("aria-expanded", String(show));
106
+ pop_.reflow();
107
+ });
57
108
  pop.append(stage, insetRow, fields, colorRow, body.el);
58
109
  root.append(pop);
59
110
 
60
- const cssAt = (k: number) => `${inset ? "inset " : ""}${lengths().map((n) => `${fmt(n * k)}px`).join(" ")} ${body.get()}`;
61
- const value = (): ShadowValue => { const [x, y, blur, spread] = lengths().map((n) => +fmt(n)); return { inset, x, y, blur, spread, color: body.get(), css: cssAt(1) }; };
111
+ const cssAt = (k: number) =>
112
+ `${inset ? "inset " : ""}${lengths()
113
+ .map((n) => `${fmt(n * k)}px`)
114
+ .join(" ")} ${body.get()}`;
115
+ const value = (): ShadowValue => {
116
+ const [x, y, blur, spread] = lengths().map((n) => +fmt(n));
117
+ return { inset, x, y, blur, spread, color: body.get(), css: cssAt(1) };
118
+ };
62
119
  const emit = () => onChange(value());
63
120
  const update = () => {
64
- card.style.boxShadow = cssAt(1); chipTile.style.boxShadow = cssAt(0.3);
121
+ card.style.boxShadow = cssAt(1);
122
+ chipTile.style.boxShadow = cssAt(0.3);
65
123
  const [x, y, blur, spread] = lengths();
66
- valueEl.textContent = `${inset ? "in " : ""}${fmt(x)} ${fmt(y)} ${fmt(blur)}${spread ? " " + fmt(spread) : ""}`;
67
- cSwatch.style.background = body.swatchCss(); cText.textContent = body.valueText();
124
+ valueEl.textContent = `${inset ? tr("inset", "in") + " " : ""}${fmt(x)} ${fmt(y)} ${fmt(blur)}${spread ? " " + fmt(spread) : ""}`;
125
+ cSwatch.style.background = body.swatchCss();
126
+ cText.textContent = body.valueText();
68
127
  };
69
128
  const pop_ = popover(root, trigger, pop, { fallbackH: 300, onOpen: body.reflow, onReflow: body.reflow });
70
129
  update();
71
130
 
72
- // Set/restore: the object form or a strict box-shadow string; `css` is ignored, and the color goes through the picker.
131
+ // Set/restore: the object form or a strict box-shadow string; `css` is ignored, and the color goes through the
132
+ // picker.
73
133
  const set = (v: unknown) => {
74
- const inp = read(v); if (!inp) return;
75
- if (inp.inset != null) { inset = !!inp.inset; insetSeg.set(inset ? "inner" : "outer"); }
76
- ([["x", fx], ["y", fy], ["blur", fb], ["spread", fs]] as const).forEach(([k, f]) => { if (inp[k] != null) f.set(num(inp[k], f.get())); });
134
+ const inp = read(v);
135
+ if (!inp) return;
136
+ if (inp.inset != null) {
137
+ inset = !!inp.inset;
138
+ insetSeg.set(inset ? "inner" : "outer");
139
+ }
140
+ (
141
+ [
142
+ ["x", fx],
143
+ ["y", fy],
144
+ ["blur", fb],
145
+ ["spread", fs],
146
+ ] as const
147
+ ).forEach(([k, f]) => {
148
+ if (inp[k] != null) f.set(num(inp[k], f.get()));
149
+ });
77
150
  if (typeof inp.color === "string" && isColor(inp.color)) body.set(inp.color);
78
151
  update();
79
152
  };