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.
- package/AGENTS.md +5 -5
- package/README.md +3 -3
- package/dist/tweaks/auto.js +1 -1
- package/dist/tweaks/auto.js.map +1 -1
- package/dist/tweaks/bezier-S2BOXQBM.js +2 -0
- package/dist/tweaks/bezier-S2BOXQBM.js.map +6 -0
- package/dist/tweaks/chunk-3MZAAMC2.js +2 -0
- package/dist/tweaks/chunk-3MZAAMC2.js.map +6 -0
- package/dist/tweaks/chunk-ETB35KFV.js +2 -0
- package/dist/tweaks/chunk-ETB35KFV.js.map +6 -0
- package/dist/tweaks/chunk-YB5GKWZI.js +2 -0
- package/dist/tweaks/chunk-YB5GKWZI.js.map +6 -0
- package/dist/tweaks/clip-A3VI53BK.js +2 -0
- package/dist/tweaks/clip-A3VI53BK.js.map +6 -0
- package/dist/tweaks/color-UGYMAF3W.js +2 -0
- package/dist/tweaks/core.js +1 -1
- package/dist/tweaks/core.js.map +3 -3
- package/dist/tweaks/fold-GY47HIQA.js +2 -0
- package/dist/tweaks/fold-GY47HIQA.js.map +6 -0
- package/dist/tweaks/gradient-BTX25WSH.js +2 -0
- package/dist/tweaks/gradient-BTX25WSH.js.map +6 -0
- package/dist/tweaks/{heavy-CLD7UAVI.js → heavy-NPFIGHRM.js} +2 -2
- package/dist/tweaks/image-HTQQ4WX6.js +4 -0
- package/dist/tweaks/image-HTQQ4WX6.js.map +6 -0
- package/dist/tweaks/{image-store-WRKY2NPU.js → image-store-QD5SMZXG.js} +1 -1
- package/dist/tweaks/image-store-QD5SMZXG.js.map +6 -0
- package/dist/tweaks/interval-BHPI4LRN.js +2 -0
- package/dist/tweaks/interval-BHPI4LRN.js.map +6 -0
- package/dist/tweaks/markup-QM53TQJZ.js +2 -0
- package/dist/tweaks/markup-QM53TQJZ.js.map +6 -0
- package/dist/tweaks/monitor-722WIYGW.js +3 -0
- package/dist/tweaks/monitor-722WIYGW.js.map +6 -0
- package/dist/tweaks/motion-HMCZK6HJ.js +2 -0
- package/dist/tweaks/motion-HMCZK6HJ.js.map +6 -0
- package/dist/tweaks/plot-D3NQ6XNN.js +3 -0
- package/dist/tweaks/plot-D3NQ6XNN.js.map +6 -0
- package/dist/tweaks/point-XDXKMDXH.js +2 -0
- package/dist/tweaks/point-XDXKMDXH.js.map +6 -0
- package/dist/tweaks/presets-EUWNSCUW.js +2 -0
- package/dist/tweaks/presets-EUWNSCUW.js.map +6 -0
- package/dist/tweaks/shadow-AM7VSRNS.js +2 -0
- package/dist/tweaks/shadow-AM7VSRNS.js.map +6 -0
- package/dist/tweaks/shadow-root-RYUV3AGS.js +3 -0
- package/dist/tweaks/shadow-root-RYUV3AGS.js.map +6 -0
- package/dist/tweaks/sides-JWIURDA4.js +2 -0
- package/dist/tweaks/sides-JWIURDA4.js.map +6 -0
- package/dist/tweaks/spring-BF5GBTPM.js +2 -0
- package/dist/tweaks/spring-BF5GBTPM.js.map +6 -0
- package/dist/tweaks/tabs-PRVUUZXZ.js +2 -0
- package/dist/tweaks/tabs-PRVUUZXZ.js.map +6 -0
- package/dist/tweaks.css +1 -1
- package/dist/tweaks.js +5 -4
- package/dist/tweaks.js.map +3 -3
- package/dist/types/tweaks/enhance.d.ts +3 -2
- package/dist/types/tweaks/types.d.ts +278 -72
- package/package.json +5 -2
- package/src/tweaks/auto.ts +9 -5
- package/src/tweaks/clip.ts +84 -22
- package/src/tweaks/controls/basic.ts +463 -139
- package/src/tweaks/controls/bezier.ts +152 -47
- package/src/tweaks/controls/color.ts +610 -161
- package/src/tweaks/controls/gradient.ts +256 -77
- package/src/tweaks/controls/image.ts +388 -132
- package/src/tweaks/controls/interval.ts +126 -41
- package/src/tweaks/controls/monitor.ts +142 -36
- package/src/tweaks/controls/motion.ts +231 -56
- package/src/tweaks/controls/plot.ts +321 -101
- package/src/tweaks/controls/point.ts +134 -45
- package/src/tweaks/controls/shadow.ts +103 -30
- package/src/tweaks/controls/sides.ts +150 -40
- package/src/tweaks/controls/spring.ts +220 -60
- package/src/tweaks/controls/tabs.ts +39 -18
- package/src/tweaks/core.ts +1 -1
- package/src/tweaks/easing.ts +45 -20
- package/src/tweaks/enhance.ts +21 -6
- package/src/tweaks/feedback.ts +133 -34
- package/src/tweaks/fold.ts +22 -10
- package/src/tweaks/heavy.ts +461 -137
- package/src/tweaks/icons.ts +12 -5
- package/src/tweaks/image-store.ts +131 -43
- package/src/tweaks/lazy.ts +36 -19
- package/src/tweaks/markup.ts +255 -92
- package/src/tweaks/panel.ts +1262 -438
- package/src/tweaks/presets.ts +85 -0
- package/src/tweaks/schema.ts +213 -51
- package/src/tweaks/shadow-root.ts +178 -0
- package/src/tweaks/shared.ts +802 -190
- package/src/tweaks/standalone.ts +120 -30
- package/src/tweaks/types.ts +644 -143
- package/src/tweaks/undo.ts +104 -0
- package/src/tweaks.css +556 -550
- package/src/wide-gamut.ts +473 -125
- package/dist/tweaks/bezier-JFEZMCLP.js +0 -2
- package/dist/tweaks/bezier-JFEZMCLP.js.map +0 -6
- package/dist/tweaks/chunk-6UO4DHBN.js +0 -2
- package/dist/tweaks/chunk-6UO4DHBN.js.map +0 -6
- package/dist/tweaks/chunk-DIQMLBAS.js +0 -2
- package/dist/tweaks/chunk-DIQMLBAS.js.map +0 -6
- package/dist/tweaks/chunk-XE6OUJBG.js +0 -2
- package/dist/tweaks/chunk-XE6OUJBG.js.map +0 -6
- package/dist/tweaks/clip-WW7JYO4X.js +0 -2
- package/dist/tweaks/clip-WW7JYO4X.js.map +0 -6
- package/dist/tweaks/color-YMMFURP6.js +0 -2
- package/dist/tweaks/fold-U6YRZLCJ.js +0 -2
- package/dist/tweaks/fold-U6YRZLCJ.js.map +0 -6
- package/dist/tweaks/gradient-JER7PW4D.js +0 -2
- package/dist/tweaks/gradient-JER7PW4D.js.map +0 -6
- package/dist/tweaks/image-H6QSLGO3.js +0 -4
- package/dist/tweaks/image-H6QSLGO3.js.map +0 -6
- package/dist/tweaks/image-store-WRKY2NPU.js.map +0 -6
- package/dist/tweaks/interval-TEUHQGZN.js +0 -2
- package/dist/tweaks/interval-TEUHQGZN.js.map +0 -6
- package/dist/tweaks/markup-NIBJD6E2.js +0 -2
- package/dist/tweaks/markup-NIBJD6E2.js.map +0 -6
- package/dist/tweaks/monitor-3BY7YCGX.js +0 -3
- package/dist/tweaks/monitor-3BY7YCGX.js.map +0 -6
- package/dist/tweaks/motion-FQPUBKKM.js +0 -2
- package/dist/tweaks/motion-FQPUBKKM.js.map +0 -6
- package/dist/tweaks/plot-2HBNBZSG.js +0 -3
- package/dist/tweaks/plot-2HBNBZSG.js.map +0 -6
- package/dist/tweaks/point-VV6XV3I6.js +0 -2
- package/dist/tweaks/point-VV6XV3I6.js.map +0 -6
- package/dist/tweaks/shadow-5E4E55I2.js +0 -2
- package/dist/tweaks/shadow-5E4E55I2.js.map +0 -6
- package/dist/tweaks/sides-P3STEVYY.js +0 -2
- package/dist/tweaks/sides-P3STEVYY.js.map +0 -6
- package/dist/tweaks/spring-HUVSXNVN.js +0 -2
- package/dist/tweaks/spring-HUVSXNVN.js.map +0 -6
- package/dist/tweaks/tabs-76FW7LQ7.js +0 -2
- package/dist/tweaks/tabs-76FW7LQ7.js.map +0 -6
- /package/dist/tweaks/{color-YMMFURP6.js.map → color-UGYMAF3W.js.map} +0 -0
- /package/dist/tweaks/{heavy-CLD7UAVI.js.map → heavy-NPFIGHRM.js.map} +0 -0
package/src/tweaks/markup.ts
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
|
-
/* Markup-driven enhancement — the work behind enhance(): it turns [data-
|
|
2
|
-
* panel's meta derivation through the kit. A lazy chunk in the code-split build, fetched by the first enhance() on a
|
|
3
|
-
* with markup (single.ts imports it statically). It imports shared.ts alone: a panel module imported here as well
|
|
4
|
-
* core would become a shared chunk every basic panel fetches, so core lends what it needs (enhance.ts builds the
|
|
5
|
-
|
|
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;
|
|
16
|
-
|
|
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-
|
|
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
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
52
|
-
const
|
|
53
|
-
|
|
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,
|
|
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)
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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 =
|
|
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
|
|
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("
|
|
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[] => [
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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-
|
|
96
|
-
const body = el("div", "
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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-
|
|
104
|
-
if (!header.querySelector(".
|
|
105
|
-
const live = () =>
|
|
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
|
-
|
|
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,
|
|
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 = () => {
|
|
116
|
-
|
|
117
|
-
|
|
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-
|
|
122
|
-
host.setAttribute("data-
|
|
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-
|
|
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-
|
|
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)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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);
|