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,8 +1,24 @@
1
- /* Markup-driven enhancement — the work behind enhance(): it turns [data-tw] hosts into live controls, sharing the
2
- * panel's meta derivation through the kit. A lazy chunk in the code-split build, fetched by the first enhance() on a page
3
- * with markup (single.ts imports it statically). It imports shared.ts alone: a panel module imported here as well as by
4
- * core would become a shared chunk every basic panel fetches, so core lends what it needs (enhance.ts builds the kit). */
5
- import { el, btn, foldBody, getControl, titleCase, setMarkup, controlTypes, hasOwn, valueChanged, restoreDefault, SPRING_KEYS } from "./shared.js";
1
+ /* Markup-driven enhancement — the work behind enhance(): it turns [data-twk] hosts into live controls, sharing the
2
+ * panel's meta derivation through the kit. A lazy chunk in the code-split build, fetched by the first enhance() on a
3
+ * page with markup (single.ts imports it statically). It imports shared.ts alone: a panel module imported here as well
4
+ * as by core would become a shared chunk every basic panel fetches, so core lends what it needs (enhance.ts builds the
5
+ * kit). */
6
+ import {
7
+ el,
8
+ btn,
9
+ foldBody,
10
+ getControl,
11
+ titleCase,
12
+ setMarkup,
13
+ controlTypes,
14
+ hasOwn,
15
+ valueChanged,
16
+ restoreDefault,
17
+ SPRING_KEYS,
18
+ using,
19
+ words,
20
+ } from "./shared.js";
21
+ import type { Words } from "./shared.js";
6
22
  import type { metaFor } from "./schema.js";
7
23
  import type { ensureForMetas, LAZY_IMPORT } from "./lazy.js";
8
24
  import type { createFolder, createControl } from "./controls/basic.js";
@@ -12,141 +28,288 @@ import type { Control, SchemaObject } from "./types.js";
12
28
  /** What core lends the markup chunk. */
13
29
  export interface MarkupKit {
14
30
  metaFor: typeof metaFor;
15
- ensureForMetas: typeof ensureForMetas; LAZY_IMPORT: typeof LAZY_IMPORT; createFolder: typeof createFolder; createControl: typeof createControl;
16
- makeCopyBtn: typeof makeCopyBtn; makeResetBtn: typeof makeResetBtn; addHintMarker: typeof addHintMarker;
31
+ ensureForMetas: typeof ensureForMetas;
32
+ LAZY_IMPORT: typeof LAZY_IMPORT;
33
+ createFolder: typeof createFolder;
34
+ createControl: typeof createControl;
35
+ makeCopyBtn: typeof makeCopyBtn;
36
+ makeResetBtn: typeof makeResetBtn;
37
+ addHintMarker: typeof addHintMarker;
17
38
  }
18
39
 
19
- // ── The markup forms — each [data-tw] type parses its dataset into the same verbose
40
+ // ── The markup forms — each [data-twk] type parses its dataset into the same verbose
20
41
  // schema value TYPED_META consumes, then rides metaFor: ONE derivation, so markup defaults can't drift. ──
21
- const springData = (d: DOMStringMap) => ({ ...Object.fromEntries(SPRING_KEYS.map((k) => [k, num(d[k])])), mode: d.mode }); // the motion control reads the same markup
22
- const choices = (d: DOMStringMap) => ({ options: splitList(d.options), value: d.value, cols: num(d.cols) }); // the radio grid, by either name
23
- const DATA_VALUE: Partial<Record<SchemaObject["type"], (d: DOMStringMap, host: HTMLElement, label: string) => object>> = {
24
- slider: (d) => ({ value: num(d.value), min: num(d.min), max: num(d.max), step: num(d.step), soft: flag(d.soft), unit: d.unit }), // absent bounds derive from the value, like the schema
25
- checkbox: (d) => (d.options ? { type: "radiogrid", ...choices(d) } : { value: d.checked === "true" }),
26
- radiogrid: choices,
27
- segmented: choices,
28
- list: (d) => ({ options: splitList(d.options), value: d.value }),
29
- // data-options is a comma list, so it takes URLs, not data URLs (they carry a comma).
30
- image: (d) => ({ value: d.value, options: d.options ? splitList(d.options) : undefined, cols: num(d.cols), maxSize: num(d.maxSize), fit: d.fit === "false" ? false : num(d.fit), quality: num(d.quality), upload: d.upload === "false" ? false : undefined, clear: d.clear === "false" ? false : undefined }),
31
- buttongroup: (d) => ({ buttons: splitList(d.buttons).map((lab) => ({ label: lab })) }),
32
- separator: () => ({}),
33
- number: (d) => ({ value: num(d.value), min: num(d.min), max: num(d.max), step: num(d.step), unit: d.unit }),
34
- text: (d) => ({ value: d.value ?? "", placeholder: d.placeholder, rows: num(d.rows) }),
35
- fpsgraph: (d) => ({ label: d.label ?? "FPS" }),
36
- interval: (d) => ({ value: d.value ? d.value.split(",").map(Number) : undefined, min: num(d.min), max: num(d.max), step: num(d.step) }),
37
- sides: (d) => ({ value: d.value ?? d, corners: flag(d.corners), unit: d.unit, step: num(d.step), min: num(d.min), max: num(d.max) }), // "8px 16px", or data-top / -right / -bottom / -left
38
- shadow: (d) => ({ value: d.value ?? { ...d, inset: flag(d.inset) } }), // a box-shadow string, or data-x / -y / -blur / -color
39
- spring: springData,
40
- motion: (d) => ({ ...springData(d), curve: d.curve && d.curve.includes(",") ? d.curve.split(",").map(Number) : d.curve, duration: num(d.duration) }), // plus data-curve and data-duration
41
- cubicbezier: (d) => ({ value: d.value ? d.value.split(",").map(Number) : undefined }),
42
- point: (d) => {
43
- const vals = (d.value || "").split(",").map((s) => parseFloat(s));
44
- return {
45
- components: splitList(d.components || "X,Y").map((lab, i) => ({ key: lab.toLowerCase(), label: lab, value: isNaN(vals[i]) ? 0 : vals[i], step: num(d.step), min: num(d.min), max: num(d.max) })), // no step: the control derives a continuous one
46
- pad: flag(d.pad), invertY: d.invertY === "true",
47
- };
48
- },
49
- plot: (d) => ({ expr: d.expr, xMin: num(d.xmin), xMax: num(d.xmax), yMin: num(d.ymin), yMax: num(d.ymax), samples: num(d.samples), editable: d.editable !== "false" }),
42
+ // the motion control reads the same markup
43
+ const springData = (d: DOMStringMap) => ({
44
+ ...Object.fromEntries(SPRING_KEYS.map((k) => [k, num(d[k])])),
45
+ mode: d.mode,
46
+ });
47
+ // the radio grid, by either name
48
+ const choices = (d: DOMStringMap) => ({ options: splitList(d.options), value: d.value, cols: num(d.cols) });
49
+ const DATA_VALUE: Partial<Record<SchemaObject["type"], (d: DOMStringMap, host: HTMLElement, label: string) => object>> =
50
+ {
51
+ // absent bounds derive from the value, like the schema
52
+ slider: (d) => ({
53
+ value: num(d.value),
54
+ min: num(d.min),
55
+ max: num(d.max),
56
+ step: num(d.step),
57
+ soft: flag(d.soft),
58
+ unit: d.unit,
59
+ }),
60
+ checkbox: (d) => (d.options ? { type: "radiogrid", ...choices(d) } : { value: d.checked === "true" }),
61
+ radiogrid: choices,
62
+ segmented: choices,
63
+ list: (d) => ({ options: splitList(d.options), value: d.value }),
64
+ // data-options is a comma list, so it takes URLs, not data URLs (they carry a comma).
65
+ image: (d) => ({
66
+ value: d.value,
67
+ options: d.options ? splitList(d.options) : undefined,
68
+ cols: num(d.cols),
69
+ maxSize: num(d.maxSize),
70
+ fit: d.fit === "false" ? false : num(d.fit),
71
+ quality: num(d.quality),
72
+ upload: d.upload === "false" ? false : undefined,
73
+ clear: d.clear === "false" ? false : undefined,
74
+ }),
75
+ buttongroup: (d) => ({ buttons: splitList(d.buttons).map((lab) => ({ label: lab })) }),
76
+ separator: () => ({}),
77
+ number: (d) => ({ value: num(d.value), min: num(d.min), max: num(d.max), step: num(d.step), unit: d.unit }),
78
+ text: (d) => ({ value: d.value ?? "", placeholder: d.placeholder, rows: num(d.rows) }),
79
+ fpsgraph: (d) => ({ label: d.label ?? "FPS" }),
80
+ interval: (d) => ({
81
+ value: d.value ? d.value.split(",").map(Number) : undefined,
82
+ min: num(d.min),
83
+ max: num(d.max),
84
+ step: num(d.step),
85
+ }),
86
+ // "8px 16px", or data-top / -right / -bottom / -left
87
+ sides: (d) => ({
88
+ value: d.value ?? d,
89
+ corners: flag(d.corners),
90
+ unit: d.unit,
91
+ step: num(d.step),
92
+ min: num(d.min),
93
+ max: num(d.max),
94
+ }),
95
+ // a box-shadow string, or data-x / -y / -blur / -color
96
+ shadow: (d) => ({ value: d.value ?? { ...d, inset: flag(d.inset) } }),
97
+ spring: springData,
98
+ // plus data-curve and data-duration
99
+ motion: (d) => ({
100
+ ...springData(d),
101
+ curve: d.curve && d.curve.includes(",") ? d.curve.split(",").map(Number) : d.curve,
102
+ duration: num(d.duration),
103
+ }),
104
+ cubicbezier: (d) => ({ value: d.value ? d.value.split(",").map(Number) : undefined }),
105
+ point: (d) => {
106
+ const vals = (d.value || "").split(",").map((s) => parseFloat(s));
107
+ return {
108
+ // no step: the control derives a continuous one
109
+ components: splitList(d.components || "X,Y").map((lab, i) => ({
110
+ key: lab.toLowerCase(),
111
+ label: lab,
112
+ value: isNaN(vals[i]) ? 0 : vals[i],
113
+ step: num(d.step),
114
+ min: num(d.min),
115
+ max: num(d.max),
116
+ })),
117
+ pad: flag(d.pad),
118
+ invertY: d.invertY === "true",
119
+ };
120
+ },
121
+ plot: (d) => ({
122
+ expr: d.expr,
123
+ xMin: num(d.xmin),
124
+ xMax: num(d.xmax),
125
+ yMin: num(d.ymin),
126
+ yMax: num(d.ymax),
127
+ samples: num(d.samples),
128
+ editable: d.editable !== "false",
129
+ }),
130
+ };
131
+ // non-numeric → undefined, so the default applies
132
+ const num = (s: string | undefined) => {
133
+ if (s == null || s === "") return undefined;
134
+ const n = +s;
135
+ return Number.isFinite(n) ? n : undefined;
50
136
  };
51
- const num = (s: string | undefined) => { if (s == null || s === "") return undefined; const n = +s; return Number.isFinite(n) ? n : undefined; }; // non-numeric → undefined, so the default applies
52
- const flag = (s: string | undefined) => s === "true" || s === ""; // boolean attributes: data-x / data-x="true"
53
- const splitList = (s: string | undefined) => (s || "").split(",").map((t) => t.trim()).filter(Boolean);
137
+ const flag = (s: string | undefined) => s === "true" || s === ""; // boolean attributes: data-x / data-x="true"
138
+ const splitList = (s: string | undefined) =>
139
+ (s || "")
140
+ .split(",")
141
+ .map((t) => t.trim())
142
+ .filter(Boolean);
54
143
  const dataMeta = (kit: MarkupKit, host: HTMLElement) => {
55
- const d = host.dataset, type = d.tw;
144
+ const d = host.dataset,
145
+ type = d.twk;
56
146
  // Tabs are panel-only, so a tabs host is left untouched, as the markup page says; built, it fell through to a folder
57
147
  // whose missing get() threw out of enhance() and left every later host claimed but empty.
58
148
  if (type === "tabs") return null;
59
149
  const label = d.label ?? titleCase(d.key || type); // data-label="" means no label, like `label: ""`
60
150
  // A lazy type with no parser takes the dataset as its fields.
61
- const v = hasOwn(DATA_VALUE, type) ? DATA_VALUE[type]!(d, host, label) : (kit.LAZY_IMPORT[type] || getControl(type)) ? { ...d } : null;
62
- // A typo (`data-tw="slidr"`) rendered nothing and said nothing. The list names what markup builds, so not tabs.
63
- if (!v) { import("./heavy.js").then((m) => m.unknownType(`data-tw="${type}"`, String(type), [...Object.keys(DATA_VALUE), ...Object.keys(kit.LAZY_IMPORT), ...controlTypes()].filter((t) => t !== "tabs"), "is skipped"), () => {}); return null; }
151
+ const v = hasOwn(DATA_VALUE, type)
152
+ ? DATA_VALUE[type]!(d, host, label)
153
+ : kit.LAZY_IMPORT[type] || getControl(type)
154
+ ? { ...d }
155
+ : null;
156
+ // A typo (`data-twk="slidr"`) rendered nothing and said nothing. The list names what markup builds, so not tabs.
157
+ if (!v) {
158
+ import("./heavy.js").then(
159
+ (m) =>
160
+ m.unknownType(
161
+ `data-twk="${type}"`,
162
+ String(type),
163
+ [...Object.keys(DATA_VALUE), ...Object.keys(kit.LAZY_IMPORT), ...controlTypes()].filter((t) => t !== "tabs"),
164
+ "markup",
165
+ ),
166
+ () => {},
167
+ );
168
+ return null;
169
+ }
64
170
  // The label rides the verbose value; v spreads after, so a parser's own type or label wins. A form that comes back a
65
171
  // folder (a lazy type's verbose form missing the field it hinges on) is no control either.
66
172
  const m = kit.metaFor("v", { type, label, ...v });
67
173
  return m && m.type !== "folder" ? m : null;
68
174
  };
69
175
 
70
-
71
- /** The handle enhance() hangs on a bound [data-tw] host: its control, the reset pair and its key. */
72
- interface HostHandle { ctrl: Control; raw: unknown; def: unknown; key: string; host: HTMLElement; reset: () => void }
73
- interface HostEl extends HTMLElement { _tw?: HostHandle }
176
+ /** The handle enhance() hangs on a bound [data-twk] host: its control, the reset pair and its key. */
177
+ interface HostHandle {
178
+ ctrl: Control;
179
+ raw: unknown;
180
+ def: unknown;
181
+ key: string;
182
+ host: HTMLElement;
183
+ reset: () => void;
184
+ }
185
+ interface HostEl extends HTMLElement {
186
+ _tw?: HostHandle;
187
+ }
74
188
 
75
189
  // Echo a control's value onto its host's data-value, in the comma form schema.ts parses. A plain
76
190
  // object (a point, a spring) would stringify as "[object Object]", so take its values in order.
77
191
  const writeDataValue = (host: HTMLElement, v: unknown) => {
78
- host.dataset.value = v == null ? "" : Array.isArray(v) ? v.join(",") : typeof v === "object" ? Object.values(v).join(",") : String(v);
192
+ host.dataset.value =
193
+ v == null ? "" : Array.isArray(v) ? v.join(",") : typeof v === "object" ? Object.values(v).join(",") : String(v);
79
194
  };
80
- // One change path for a markup host: echo onto data-value, then a bubbling tw:change with { key, value }.
195
+ // One change path for a markup host: echo onto data-value, then a bubbling twk:change with { key, value }.
81
196
  const announce = (host: HTMLElement, key: string, v: unknown) => {
82
197
  writeDataValue(host, v);
83
- host.dispatchEvent(new CustomEvent("tw:change", { bubbles: true, detail: { key, value: v } }));
198
+ host.dispatchEvent(new CustomEvent("twk:change", { bubbles: true, detail: { key, value: v } }));
84
199
  };
85
200
  // querySelectorAll sees descendants only — a root that is itself a host counts too (a Document has no matches()).
86
- const select = <T extends Element = HTMLElement>(root: Document | Element, sel: string): T[] => [...(typeof (root as Element).matches === "function" && (root as Element).matches(sel) ? [root as T] : []), ...root.querySelectorAll<T>(sel)];
87
- /** Bind every unbound [data-tw] host under `root`. */
88
- async function enhance(root: Document | Element, kit: MarkupKit): Promise<void> {
89
- const { createFolder, createControl, makeCopyBtn, makeResetBtn, addHintMarker, ensureForMetas } = kit;
90
- // Static showcase panels collapse like the real one: wrap the controls in a .tw-body, make the title the toggle.
91
- select(root, '.tw-panel[data-mode="inline"]:not([data-tw-panel-bound])').forEach((panel) => {
92
- const header = panel.querySelector(":scope > .tw-header");
93
- const controls = panel.querySelector(":scope > .tw-controls");
201
+ const select = <T extends Element = HTMLElement>(root: Document | Element, sel: string): T[] => [
202
+ ...(typeof (root as Element).matches === "function" && (root as Element).matches(sel) ? [root as T] : []),
203
+ ...root.querySelectorAll<T>(sel),
204
+ ];
205
+ /** Bind every unbound [data-twk] host under `root`, in `strings` (enhance()'s option) over the English defaults. */
206
+ async function enhance(root: Document | Element, kit: MarkupKit, strings?: Words): Promise<void> {
207
+ const { createFolder, createControl, makeCopyBtn, makeResetBtn, addHintMarker, ensureForMetas } = kit,
208
+ tr = words(strings);
209
+ // Static showcase panels collapse like the real one: wrap the controls in a .twk-body, make the title the toggle.
210
+ select(root, '.twk-panel[data-mode="inline"]:not([data-twk-panel-bound])').forEach((panel) => {
211
+ const header = panel.querySelector(":scope > .twk-header");
212
+ const controls = panel.querySelector(":scope > .twk-controls");
94
213
  if (!header || !controls) return;
95
- panel.setAttribute("data-tw-panel-bound", "");
96
- const body = el("div", "tw-body"); panel.insertBefore(body, controls); body.append(controls);
97
- let toggle = header.querySelector<HTMLButtonElement>(".tw-header-toggle");
98
- const title = header.querySelector(".tw-title");
99
- if (!toggle && title) { toggle = btn("tw-header-toggle"); title.replaceWith(toggle); toggle.append(title); }
214
+ panel.setAttribute("data-twk-panel-bound", "");
215
+ const body = el("div", "twk-body");
216
+ panel.insertBefore(body, controls);
217
+ body.append(controls);
218
+ let toggle = header.querySelector<HTMLButtonElement>(".twk-header-toggle");
219
+ const title = header.querySelector(".twk-title");
220
+ if (!toggle && title) {
221
+ toggle = btn("twk-header-toggle");
222
+ title.replaceWith(toggle);
223
+ toggle.append(title);
224
+ }
100
225
  if (!toggle) return;
101
226
  toggle.setAttribute("aria-expanded", "true");
102
227
  toggle.addEventListener("click", () => foldBody(panel, toggle, body, !panel.classList.contains("is-collapsed")));
103
- // The same toolbar tweaks() builds, over this panel's own [data-tw] controls (gathered at click time).
104
- if (!header.querySelector(".tw-toolbar")) {
105
- const live = () => [...panel.querySelectorAll<HostEl>("[data-tw]")].map((h) => h._tw).filter((t) => t && t.ctrl.get() !== undefined);
228
+ // The same toolbar tweaks() builds, over this panel's own [data-twk] controls (gathered at click time).
229
+ if (!header.querySelector(".twk-toolbar")) {
230
+ const live = () =>
231
+ [...panel.querySelectorAll<HostEl>("[data-twk]")]
232
+ .map((h) => h._tw)
233
+ .filter((t) => t && t.ctrl.get() !== undefined);
106
234
  // Two hosts can share a key (a repeated data-key, or the same label): suffix the duplicates.
107
235
  const values = () => {
108
- const vals: Record<string, unknown> = Object.create(null); // null-proto: a "__proto__" data-key is an ordinary key
236
+ // null-proto: a "__proto__" data-key is an ordinary key
237
+ const vals: Record<string, unknown> = Object.create(null);
109
238
  for (const t of live()) {
110
- let k = t.key, n = 2; while (hasOwn(vals, k)) k = `${t.key}-${n++}`;
239
+ let k = t.key,
240
+ n = 2;
241
+ while (hasOwn(vals, k)) k = `${t.key}-${n++}`;
111
242
  vals[k] = t.ctrl.get();
112
243
  }
113
244
  return JSON.stringify(vals, null, 2);
114
245
  };
115
- const reset = () => { for (const t of live()) t.reset(); };
116
- const toolbar = el("div", "tw-toolbar");
117
- toolbar.append(makeCopyBtn(() => [values(), -1]), makeResetBtn(reset)); header.append(toolbar); // no defaults to diff against: the copy is every value
246
+ const reset = () => {
247
+ for (const t of live()) t.reset();
248
+ };
249
+ const toolbar = el("div", "twk-toolbar");
250
+ // no defaults to diff against: the copy is every value
251
+ toolbar.append(
252
+ makeCopyBtn(() => [values(), -1], tr),
253
+ makeResetBtn(reset, tr),
254
+ );
255
+ header.append(toolbar);
118
256
  }
119
257
  });
120
258
  // Folders first: move child hosts into the collapsible chrome.
121
- select(root, '[data-tw="folder"]:not([data-tw-bound])').forEach((host) => {
122
- host.setAttribute("data-tw-bound", "");
123
- const f = createFolder({ label: host.dataset.label || "Folder" });
259
+ select(root, '[data-twk="folder"]:not([data-twk-bound])').forEach((host) => {
260
+ host.setAttribute("data-twk-bound", "");
261
+ const f = createFolder({ label: host.dataset.label || tr("folder", "Folder") });
124
262
  [...host.children].forEach((c) => f.body.append(c));
125
263
  host.append(f.el);
126
264
  });
127
265
  // Claim each host synchronously, so a re-entrant enhance can't double-bind; then load and build.
128
- const hosts = select<HostEl>(root, "[data-tw]:not([data-tw-bound])")
266
+ const hosts = select<HostEl>(root, "[data-twk]:not([data-twk-bound])")
129
267
  .map((h) => ({ host: h, meta: dataMeta(kit, h) }))
130
268
  .filter((x) => x.meta);
131
- hosts.forEach(({ host }) => host.setAttribute("data-tw-bound", ""));
269
+ hosts.forEach(({ host }) => host.setAttribute("data-twk-bound", ""));
132
270
  const pend = ensureForMetas(hosts.map((x) => x.meta));
133
271
  if (pend) await pend.catch(() => {}); // a failed chunk skips its controls instead of rejecting
134
272
  // One host that fails to build costs only itself: the rest still bind.
135
- for (const { host, meta } of hosts) try {
136
- const key = host.dataset.key || meta.label;
137
- // Gated the way the panel gates notify(): a same-value emit announces nothing.
138
- let last: unknown;
139
- const emit = (v: unknown) => { if (!valueChanged(last, v)) return; last = v; announce(host, key, v); };
140
- const ctrl = createControl(meta, emit);
141
- if (!ctrl) continue;
142
- if (!host.closest(".tw-panel, .tw-portal")) host.classList.add("tw-portal"); // a bare host gets the token scope
143
- host.append(ctrl.el); if (host.dataset.hint) addHintMarker(ctrl.el, host.dataset.hint);
144
- // raw = the markup's value, def = the form the control opened on: the panel's reset pair.
145
- const def = ctrl.get(); last = def;
146
- // The handle's set() runs the change path too, where the bare control's set() is silent.
147
- const handle = { ...ctrl, set: (v: unknown) => { ctrl.set(v); emit(ctrl.get()); } };
148
- host._tw = { ctrl: handle, raw: meta.value, def, key, host, reset: () => { restoreDefault(ctrl, meta.value, def); emit(ctrl.get()); } };
149
- } catch (err) { console.error(`[tweaks] data-tw="${host.dataset.tw}" could not be built — skipped:`, err); }
273
+ for (const { host, meta } of hosts)
274
+ try {
275
+ const key = host.dataset.key || meta.label;
276
+ // Gated the way the panel gates notify(): a same-value emit announces nothing.
277
+ let last: unknown;
278
+ const emit = (v: unknown) => {
279
+ if (!valueChanged(last, v)) return;
280
+ last = v;
281
+ announce(host, key, v);
282
+ };
283
+ const ctrl = using(strings, () => createControl(meta, emit));
284
+ if (!ctrl) continue;
285
+ if (!host.closest(".twk-panel, .twk-portal")) host.classList.add("twk-portal"); // a bare host gets the token scope
286
+ host.append(ctrl.el);
287
+ if (host.dataset.hint) addHintMarker(ctrl.el, host.dataset.hint);
288
+ // raw = the markup's value, def = the form the control opened on: the panel's reset pair.
289
+ const def = ctrl.get();
290
+ last = def;
291
+ // The handle's set() runs the change path too, where the bare control's set() is silent.
292
+ const handle = {
293
+ ...ctrl,
294
+ set: (v: unknown) => {
295
+ ctrl.set(v);
296
+ emit(ctrl.get());
297
+ },
298
+ };
299
+ host._tw = {
300
+ ctrl: handle,
301
+ raw: meta.value,
302
+ def,
303
+ key,
304
+ host,
305
+ reset: () => {
306
+ restoreDefault(ctrl, meta.value, def);
307
+ emit(ctrl.get());
308
+ },
309
+ };
310
+ } catch (err) {
311
+ console.error(`[tweaks] data-twk="${host.dataset.twk}" could not be built — skipped:`, err);
312
+ }
150
313
  }
151
314
 
152
315
  setMarkup(enhance);