twiddle-panel 0.0.0-stage → 0.6.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.
@@ -0,0 +1,2384 @@
1
+ const qe = /* @__PURE__ */ new Set();
2
+ let C = En(), _ = [], ie = [], ke = null, Mt = 0;
3
+ function En() {
4
+ const t = new URLSearchParams(location.search).get("twd");
5
+ if (!t) return {};
6
+ try {
7
+ return JSON.parse(t);
8
+ } catch {
9
+ return {};
10
+ }
11
+ }
12
+ function $n() {
13
+ const t = new URL(location.href);
14
+ Object.keys(C).length ? t.searchParams.set("twd", JSON.stringify(C)) : t.searchParams.delete("twd"), history.replaceState(history.state, "", t);
15
+ }
16
+ function Qe() {
17
+ $n(), qe.forEach((t) => t(C));
18
+ }
19
+ function J(t, e) {
20
+ if (JSON.stringify(t) === JSON.stringify(C)) return;
21
+ const n = Date.now();
22
+ e && e === ke && n - Mt < 400 || (_.push(C), _.length > 100 && _.shift()), ie = [], ke = e, Mt = n, C = t, Qe();
23
+ }
24
+ function Ee(t, e) {
25
+ const n = { ...t, ...e };
26
+ return Object.keys(n).forEach((o) => {
27
+ n[o] == null && delete n[o];
28
+ }), Object.keys(n).length ? n : null;
29
+ }
30
+ function D(t, e, n) {
31
+ const o = { ...t };
32
+ return n == null ? delete o[e] : o[e] = n, o;
33
+ }
34
+ function He() {
35
+ return JSON.parse(JSON.stringify(C));
36
+ }
37
+ function Cn(t, e) {
38
+ J(D(C, t, Ee(C[t], e)), `${t}:${Object.keys(e)}`);
39
+ }
40
+ function Mn(t, e = `multi:${Object.keys(t)}`) {
41
+ const n = Object.entries(t).reduce((o, [a, s]) => D(o, a, Ee(o[a], s)), C);
42
+ J(n, e);
43
+ }
44
+ function Yt(t, e, n) {
45
+ const o = D(C.scopes?.[t], e, Ee(C.scopes?.[t]?.[e], n)), a = D(C.scopes, t, Object.keys(o).length ? o : null);
46
+ J(D(C, "scopes", Object.keys(a).length ? a : null), `${t}|${e}:${Object.keys(n)}`);
47
+ }
48
+ function An(t, e) {
49
+ if (e) return Yt(t, e, Object.fromEntries(
50
+ Object.keys(C.scopes?.[t]?.[e] || {}).map((o) => [o, null])
51
+ ));
52
+ const n = D(C.scopes, t, null);
53
+ J(D(C, "scopes", Object.keys(n).length ? n : null));
54
+ }
55
+ function Tn(t, e) {
56
+ const n = C.scopes?.[t];
57
+ if (!n || t === e) return;
58
+ let o = { ...C.scopes[e] };
59
+ Object.entries(n).forEach(([s, r]) => {
60
+ o = D(o, s, Ee(o[s], r));
61
+ });
62
+ const a = D(D(C.scopes, t, null), e, o);
63
+ J(D(C, "scopes", a));
64
+ }
65
+ function Xt() {
66
+ J({});
67
+ }
68
+ function Jt() {
69
+ _.length && (ie.push(C), C = _.pop(), ke = null, Qe());
70
+ }
71
+ function Zt() {
72
+ ie.length && (_.push(C), C = ie.pop(), ke = null, Qe());
73
+ }
74
+ const Ln = () => _.length > 0, Pn = () => ie.length > 0;
75
+ function Qt(t) {
76
+ return qe.add(t), () => qe.delete(t);
77
+ }
78
+ const Fn = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
79
+ __proto__: null,
80
+ canRedo: Pn,
81
+ canUndo: Ln,
82
+ getState: He,
83
+ moveScope: Tn,
84
+ onChange: Qt,
85
+ redo: Zt,
86
+ removeScope: An,
87
+ resetState: Xt,
88
+ setScope: Yt,
89
+ setSection: Cn,
90
+ setSections: Mn,
91
+ undo: Jt
92
+ }, Symbol.toStringTag, { value: "Module" })), en = [];
93
+ let _e = [], et = {}, R = null, Y = !1, tn = {};
94
+ function On(t) {
95
+ typeof t == "function" && en.push(t);
96
+ }
97
+ const At = (t) => Object.assign({}, ...en.map((e) => e(t) || {})), Ge = (t) => /^(\.[\w-]+|\[data-twd="[^"]+"\])$/.test(t), Rn = (t) => {
98
+ try {
99
+ return document.querySelector(t), !0;
100
+ } catch {
101
+ return !1;
102
+ }
103
+ };
104
+ function Nn() {
105
+ return (!R || !R.isConnected) && (R = document.createElement("style"), R.id = "twiddle-scoped", document.head.appendChild(R)), R;
106
+ }
107
+ function nn() {
108
+ const t = document.documentElement;
109
+ _e.forEach((e) => t.style.removeProperty(e)), _e = [], et = {};
110
+ }
111
+ function Ke(t) {
112
+ if (tn = t, Y) return;
113
+ const e = document.documentElement;
114
+ nn();
115
+ const n = At(t), o = Object.entries(t.scopes || {}).filter(([s]) => Rn(s)).sort(([s], [r]) => Ge(r) - Ge(s)).map(([s, r]) => [s, At(r)]);
116
+ Object.entries(n).forEach(([s, r]) => e.style.setProperty(s, r)), _e = Object.keys(n), et = n;
117
+ const a = o.filter(([, s]) => Object.keys(s).length).map(([s, r]) => `${s} {
118
+ ${Object.entries(r).map(([d, l]) => ` ${d}: ${l} !important;`).join(`
119
+ `)}
120
+ }`).join(`
121
+ `);
122
+ (a || R) && (Nn().textContent = a);
123
+ }
124
+ function on(t) {
125
+ const e = document.documentElement, n = Y, o = { ...et };
126
+ Object.keys(o).forEach((a) => e.style.removeProperty(a)), R && (R.disabled = !0);
127
+ try {
128
+ return t();
129
+ } finally {
130
+ n || (Object.entries(o).forEach(([a, s]) => e.style.setProperty(a, s)), R && (R.disabled = !1));
131
+ }
132
+ }
133
+ function jn(t, e = document.documentElement) {
134
+ return on(() => {
135
+ const n = getComputedStyle(e);
136
+ return Object.fromEntries(t.map((o) => [o, n.getPropertyValue(o).trim()]));
137
+ });
138
+ }
139
+ function zn() {
140
+ Y || (nn(), R && (R.disabled = !0), Y = !0);
141
+ }
142
+ function In() {
143
+ Y && (Y = !1, R && (R.disabled = !1), Ke(tn));
144
+ }
145
+ const Dn = `
146
+ :host { all: initial; } /* block fonts/colours inherited from the page */
147
+ * { box-sizing: border-box; }
148
+
149
+ .wrap {
150
+ --p-bg: #ffffff;
151
+ --p-fg: #18181b;
152
+ --p-muted: #71717a;
153
+ --p-line: #e4e4e7;
154
+ --p-hover: #f4f4f5;
155
+ --p-track: #f4f4f5;
156
+ --p-selected: #ffffff;
157
+ --p-accent: #18181b;
158
+ --p-on-accent: #ffffff;
159
+ --p-shadow: 0 12px 40px rgb(0 0 0 / 0.14), 0 2px 6px rgb(0 0 0 / 0.06);
160
+ position: fixed;
161
+ right: 16px;
162
+ bottom: 16px;
163
+ z-index: 2147483647;
164
+ font: 13px/1.4 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
165
+ color: var(--p-fg);
166
+ display: flex;
167
+ flex-direction: column;
168
+ align-items: flex-end;
169
+ gap: 10px;
170
+ }
171
+ @media (prefers-color-scheme: dark) {
172
+ .wrap {
173
+ --p-bg: #1c1c1f;
174
+ --p-fg: #f4f4f5;
175
+ --p-muted: #a1a1aa;
176
+ --p-line: #2e2e33;
177
+ --p-hover: #27272a;
178
+ --p-track: #27272a;
179
+ --p-selected: #3f3f46;
180
+ --p-accent: #f4f4f5;
181
+ --p-on-accent: #18181b;
182
+ --p-shadow: 0 12px 40px rgb(0 0 0 / 0.5), 0 2px 6px rgb(0 0 0 / 0.3);
183
+ }
184
+ }
185
+
186
+ /* Launcher */
187
+ .launcher {
188
+ width: 44px; height: 44px;
189
+ border-radius: 50%;
190
+ border: 1px solid var(--p-line);
191
+ background: var(--p-bg);
192
+ color: var(--p-fg);
193
+ box-shadow: var(--p-shadow);
194
+ display: grid; place-items: center;
195
+ cursor: pointer;
196
+ padding: 0;
197
+ }
198
+ .launcher svg { transition: transform .15s ease; }
199
+ .launcher:hover svg { transform: rotate(-20deg); }
200
+ .launcher[aria-expanded="true"] { background: var(--p-accent); color: var(--p-on-accent); border-color: var(--p-accent); }
201
+ .launcher:focus-visible, button:focus-visible, summary:focus-visible {
202
+ outline: 2px solid var(--p-accent); outline-offset: 2px;
203
+ }
204
+
205
+ /* Panel */
206
+ .panel {
207
+ width: 300px;
208
+ max-height: min(640px, calc(100vh - 90px));
209
+ background: var(--p-bg);
210
+ border: 1px solid var(--p-line);
211
+ border-radius: 14px;
212
+ box-shadow: var(--p-shadow);
213
+ display: flex; flex-direction: column;
214
+ overflow: hidden;
215
+ }
216
+ .panel[hidden] { display: none; }
217
+
218
+ .head {
219
+ display: flex; align-items: center; gap: 4px;
220
+ padding: 10px 10px 10px 14px;
221
+ border-bottom: 1px solid var(--p-line);
222
+ }
223
+ .brand { font-weight: 600; font-size: 14px; letter-spacing: -0.01em; flex: 1; }
224
+ .icon-btn {
225
+ width: 28px; height: 28px; padding: 0;
226
+ border: 0; border-radius: 6px;
227
+ background: transparent; color: var(--p-muted);
228
+ display: grid; place-items: center;
229
+ cursor: pointer;
230
+ }
231
+ .icon-btn:hover { background: var(--p-hover); color: var(--p-fg); }
232
+
233
+ .body { overflow-y: auto; padding: 4px 0; }
234
+
235
+ details { border-bottom: 1px solid var(--p-line); }
236
+ details:last-child { border-bottom: 0; }
237
+ summary {
238
+ list-style: none;
239
+ display: flex; align-items: center; gap: 8px;
240
+ padding: 11px 14px;
241
+ cursor: pointer;
242
+ font-weight: 500;
243
+ user-select: none;
244
+ }
245
+ summary::-webkit-details-marker { display: none; }
246
+ summary:hover { background: var(--p-hover); }
247
+ summary .chev { margin-left: auto; color: var(--p-muted); display: grid; transition: transform .15s ease; }
248
+ details[open] summary .chev { transform: rotate(90deg); }
249
+ .section-body { padding: 2px 14px 16px; color: var(--p-muted); display: grid; gap: 14px; }
250
+ .placeholder {
251
+ border: 1px dashed var(--p-line);
252
+ border-radius: 8px;
253
+ padding: 10px;
254
+ font-size: 12px;
255
+ }
256
+
257
+ /* Field = Figma "Field header" + control (gap 4) */
258
+ .field { display: grid; gap: 4px; }
259
+ .field-head {
260
+ display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
261
+ min-height: 18px;
262
+ }
263
+ .field-label { font-size: 12px; font-weight: 500; color: var(--p-muted); }
264
+ .field-value { font: 11px/1.4 ui-monospace, "SF Mono", Menlo, monospace; color: var(--p-muted); }
265
+
266
+ /* Segmented control (Figma 2:36) built from Segment (Figma 2:14) */
267
+ .seg {
268
+ display: flex; gap: 2px; padding: 2px;
269
+ border-radius: 6px;
270
+ background: var(--p-track);
271
+ }
272
+ .seg [role="radio"] {
273
+ flex: 1 1 0; min-width: 0;
274
+ height: 26px; padding: 0 10px;
275
+ border: 0; border-radius: 6px;
276
+ background: transparent; color: var(--p-muted);
277
+ font: inherit; font-size: 12px; font-weight: 500; line-height: 1;
278
+ white-space: nowrap;
279
+ /* Like Figma: a long label (e.g. "Rounded" in a 4-option control) may
280
+ spill into the 10px padding, staying centred, rather than truncating. */
281
+ display: flex; align-items: center; justify-content: center;
282
+ cursor: pointer;
283
+ }
284
+ .seg [role="radio"]:hover { background: var(--p-hover); color: var(--p-fg); }
285
+ .seg [aria-checked="true"] {
286
+ background: var(--p-bg); color: var(--p-fg);
287
+ box-shadow: 0 1px 2px rgb(0 0 0 / 0.08);
288
+ }
289
+ .seg [aria-checked="true"]:hover { background: var(--p-hover); }
290
+ .seg [role="radio"]:focus-visible { outline: 2px solid var(--p-accent); outline-offset: 0; position: relative; }
291
+ .seg [aria-checked="true"]:focus-visible { background: var(--p-selected); } /* as in Figma Selected=True, State=Focus */
292
+
293
+ /* Slider (Figma 4:135): 4px rail, 14px thumb, value label 36px */
294
+ .slider { display: flex; align-items: center; gap: 8px; height: 20px; }
295
+ .slider input {
296
+ -webkit-appearance: none; appearance: none;
297
+ flex: 1; min-width: 0; height: 20px; margin: 0; background: transparent; cursor: pointer;
298
+ --fill: 50%;
299
+ }
300
+ .slider input::-webkit-slider-runnable-track {
301
+ height: 4px; border-radius: 2px;
302
+ background: linear-gradient(to right, var(--p-accent) var(--fill), var(--p-line) var(--fill));
303
+ }
304
+ .slider input::-moz-range-track { height: 4px; border-radius: 2px; background: var(--p-line); }
305
+ .slider input::-moz-range-progress { height: 4px; border-radius: 2px; background: var(--p-accent); }
306
+ .slider input:hover::-webkit-slider-runnable-track {
307
+ background: linear-gradient(to right, var(--p-accent) var(--fill), var(--p-muted) var(--fill));
308
+ }
309
+ .slider input:hover::-moz-range-track { background: var(--p-muted); }
310
+ .slider input::-webkit-slider-thumb {
311
+ -webkit-appearance: none; appearance: none;
312
+ width: 14px; height: 14px; margin-top: -5px; border-radius: 50%;
313
+ background: var(--p-bg); border: 1.5px solid var(--p-line);
314
+ box-shadow: 0 1px 3px rgb(0 0 0 / 0.15);
315
+ transition: width .1s ease, height .1s ease, margin .1s ease;
316
+ }
317
+ .slider input::-moz-range-thumb {
318
+ width: 14px; height: 14px; border-radius: 50%; box-sizing: border-box;
319
+ background: var(--p-bg); border: 1.5px solid var(--p-line); box-shadow: 0 1px 3px rgb(0 0 0 / 0.15);
320
+ }
321
+ .slider input:active::-webkit-slider-thumb { width: 18px; height: 18px; margin-top: -7px; border-color: var(--p-accent); }
322
+ .slider input:active::-moz-range-thumb { width: 18px; height: 18px; border-color: var(--p-accent); }
323
+ .slider input:focus { outline: none; }
324
+ .slider input:focus-visible::-webkit-slider-thumb { border: 2px solid var(--p-accent); }
325
+ .slider input:focus-visible::-moz-range-thumb { border: 2px solid var(--p-accent); }
326
+ .slider-value {
327
+ width: 36px; flex: none; text-align: right;
328
+ font: 11px/1.4 ui-monospace, "SF Mono", Menlo, monospace; color: var(--p-muted);
329
+ }
330
+
331
+ /* Font select (Figma 3:78) + menu (3:79) + option (3:53) */
332
+ .font-trigger {
333
+ width: 100%; height: 36px; padding: 0 10px;
334
+ display: flex; align-items: center; gap: 8px;
335
+ border: 1px solid var(--p-line); border-radius: 6px;
336
+ background: var(--p-bg); color: var(--p-fg);
337
+ font: inherit; text-align: left; cursor: pointer;
338
+ }
339
+ .font-trigger:hover { background: var(--p-hover); }
340
+ .font-trigger[aria-expanded="true"] { border-color: var(--p-accent); }
341
+ .font-trigger:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--p-accent); }
342
+ .font-trigger .font-aa { font-size: 15px; font-weight: 600; }
343
+ .font-trigger .font-name { flex: 1; min-width: 0; font-size: 13px; font-weight: 500; }
344
+ .font-trigger .font-fonts { font-size: 12px; color: var(--p-muted); white-space: nowrap; }
345
+ .font-chevron { display: grid; color: var(--p-muted); transition: transform .15s ease; }
346
+ .font-trigger[aria-expanded="true"] .font-chevron { transform: rotate(180deg); }
347
+
348
+ .font-menu {
349
+ position: fixed; z-index: 1; max-height: calc(100vh - 16px); overflow-y: auto;
350
+ display: grid; gap: 2px; padding: 4px;
351
+ background: var(--p-bg); border: 1px solid var(--p-line); border-radius: 6px;
352
+ box-shadow: var(--p-shadow);
353
+ }
354
+ .font-menu[hidden] { display: none; }
355
+ .font-option {
356
+ display: flex; align-items: center; gap: 10px;
357
+ min-height: 40px; padding: 4px 8px; border-radius: 6px; cursor: pointer; color: var(--p-fg);
358
+ }
359
+ .font-option:hover, .font-option[aria-selected="true"] { background: var(--p-hover); }
360
+ .font-option:focus { outline: none; }
361
+ .font-option:focus-visible { box-shadow: inset 0 0 0 2px var(--p-accent); }
362
+ .font-option .font-aa { width: 34px; flex: none; text-align: center; font-size: 22px; font-weight: 600; line-height: 30px; }
363
+ .font-text { flex: 1; min-width: 0; display: grid; line-height: 1.25; } /* keeps each option at Figma's 40px */
364
+ .font-option .font-name { font-size: 13px; font-weight: 500; }
365
+ .font-option .font-fonts { font-size: 12px; color: var(--p-muted); }
366
+ .font-check { display: grid; visibility: hidden; }
367
+ .font-option[aria-selected="true"] .font-check { visibility: visible; }
368
+
369
+ /* Colour field (Figma 4:72) + ramp (4:73) */
370
+ .colour { display: grid; gap: 6px; }
371
+ .colour-field {
372
+ position: relative;
373
+ height: 36px; padding: 0 10px 0 6px;
374
+ display: flex; align-items: center; gap: 8px;
375
+ border: 1px solid var(--p-line); border-radius: 6px; background: var(--p-bg);
376
+ }
377
+ .colour-field:hover { background: var(--p-hover); }
378
+ .colour-field:focus-within { border-color: var(--p-accent); box-shadow: inset 0 0 0 1px var(--p-accent); }
379
+ .colour-picker { position: absolute; left: 6px; bottom: 0; width: 24px; height: 1px; opacity: 0; pointer-events: none; border: 0; padding: 0; }
380
+ .colour-swatch {
381
+ width: 24px; height: 24px; flex: none; padding: 0; cursor: pointer;
382
+ border: 0; border-radius: 6px; box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.08);
383
+ }
384
+ .colour-hex {
385
+ flex: 1; min-width: 0; height: 100%; padding: 0;
386
+ border: 0; background: transparent; color: var(--p-fg);
387
+ font: inherit; font-size: 13px; font-weight: 500;
388
+ }
389
+ .colour-hex:focus { outline: none; }
390
+ .colour-pipette {
391
+ width: 24px; height: 24px; margin-right: -4px; padding: 0; flex: none;
392
+ display: grid; place-items: center; cursor: pointer;
393
+ border: 0; border-radius: 6px; background: transparent; color: var(--p-muted);
394
+ }
395
+ .colour-pipette:hover { color: var(--p-fg); }
396
+ .ramp { display: flex; gap: 2px; height: 14px; border-radius: 6px; overflow: hidden; }
397
+ .ramp span { flex: 1; }
398
+
399
+ /* Swatch row (Figma 4:98) + swatch (4:97) */
400
+ .swatches { display: flex; gap: 14px; }
401
+ .swatch {
402
+ display: grid; justify-items: center; gap: 4px;
403
+ padding: 0; border: 0; background: none; cursor: pointer; font: inherit;
404
+ }
405
+ .swatch:focus { outline: none; }
406
+ .swatch-ring {
407
+ width: 32px; height: 32px; border-radius: 50%;
408
+ display: grid; place-items: center;
409
+ box-shadow: inset 0 0 0 2px transparent;
410
+ }
411
+ .swatch:hover .swatch-ring { box-shadow: inset 0 0 0 2px var(--p-line); }
412
+ .swatch[aria-checked="true"] .swatch-ring { box-shadow: inset 0 0 0 2px var(--p-accent); }
413
+ .swatch:focus-visible .swatch-ring { box-shadow: none; outline: 2px dashed var(--p-accent); outline-offset: -2px; }
414
+ .swatch-dot { width: 22px; height: 22px; border-radius: 50%; box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.08); }
415
+ .swatch-label { font-size: 12px; color: var(--p-muted); }
416
+ .swatch[aria-checked="true"] .swatch-label { color: var(--p-fg); }
417
+
418
+ .foot {
419
+ border-top: 1px solid var(--p-line);
420
+ padding: 8px 10px;
421
+ display: flex; align-items: center; gap: 2px;
422
+ color: var(--p-muted); font-size: 12px;
423
+ }
424
+ .spacer { flex: 1; }
425
+ .icon-btn:disabled { opacity: .35; cursor: default; background: transparent; }
426
+ .icon-btn[aria-pressed="true"] { background: var(--p-accent); color: var(--p-on-accent); }
427
+ kbd {
428
+ font: 11px ui-monospace, "SF Mono", Menlo, monospace;
429
+ border: 1px solid var(--p-line); border-radius: 4px;
430
+ padding: 1px 4px;
431
+ }
432
+
433
+ .toast {
434
+ position: absolute; bottom: 64px; right: 0;
435
+ background: var(--p-accent); color: var(--p-on-accent);
436
+ padding: 6px 10px; border-radius: 8px; font-size: 12px;
437
+ white-space: nowrap;
438
+ opacity: 0; transition: opacity .15s ease; pointer-events: none;
439
+ }
440
+ .toast.show { opacity: 1; }
441
+
442
+ /* ---------- Proposals below: not yet designed in Figma ---------- */
443
+
444
+ /* Text button + count badge (footer "Changes") */
445
+ .text-btn {
446
+ height: 28px; padding: 0 8px; margin-left: 4px;
447
+ display: inline-flex; align-items: center; gap: 6px;
448
+ border: 0; border-radius: 6px; background: transparent;
449
+ color: var(--p-muted); font: inherit; font-size: 12px; font-weight: 500; cursor: pointer;
450
+ }
451
+ .text-btn:hover { background: var(--p-hover); color: var(--p-fg); }
452
+ .text-btn[aria-pressed="true"] { background: var(--p-hover); color: var(--p-fg); }
453
+ .badge {
454
+ min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px;
455
+ display: inline-grid; place-items: center;
456
+ background: var(--p-accent); color: var(--p-on-accent);
457
+ font-size: 11px; font-weight: 600;
458
+ }
459
+ .badge[hidden] { display: none; }
460
+
461
+ /* Copy prompt split button + menu */
462
+ .apply { position: relative; display: flex; }
463
+ .apply-btn {
464
+ height: 28px; padding: 0 12px;
465
+ border: 0; border-radius: 6px;
466
+ background: var(--p-accent); color: var(--p-on-accent);
467
+ font: inherit; font-size: 12px; font-weight: 400; cursor: pointer;
468
+ }
469
+ .apply { flex: none; }
470
+ .apply .apply-btn { border-radius: 6px 0 0 6px; display: inline-flex; align-items: center; gap: 6px; padding: 0 10px; white-space: nowrap; }
471
+ .apply-menu-btn {
472
+ width: 22px; height: 28px; padding: 0;
473
+ display: grid; place-items: center;
474
+ border: 0; border-left: 1px solid color-mix(in srgb, var(--p-on-accent) 25%, transparent);
475
+ border-radius: 0 6px 6px 0;
476
+ background: var(--p-accent); color: var(--p-on-accent); cursor: pointer;
477
+ }
478
+ .menu {
479
+ position: absolute; right: 0; bottom: calc(100% + 6px); z-index: 2;
480
+ min-width: 180px; padding: 4px; display: grid; gap: 2px;
481
+ background: var(--p-bg); border: 1px solid var(--p-line); border-radius: 8px;
482
+ box-shadow: var(--p-shadow);
483
+ }
484
+ .menu[hidden] { display: none; }
485
+ .menu button {
486
+ display: flex; align-items: center; gap: 8px;
487
+ height: 30px; padding: 0 8px 0 24px; position: relative;
488
+ border: 0; border-radius: 6px; background: transparent;
489
+ color: var(--p-fg); font: inherit; font-size: 12px; text-align: left; cursor: pointer;
490
+ }
491
+ .menu button:hover { background: var(--p-hover); }
492
+ .menu button[aria-checked="true"]::before {
493
+ content: ""; position: absolute; left: 10px; top: 50%;
494
+ width: 6px; height: 6px; margin-top: -3px; border-radius: 50%; background: var(--p-fg);
495
+ }
496
+
497
+ /* Compare: "showing original" strip */
498
+ .original-pill {
499
+ padding: 6px 14px;
500
+ background: var(--p-accent); color: var(--p-on-accent);
501
+ font-size: 12px; font-weight: 500;
502
+ }
503
+ .original-pill[hidden] { display: none; }
504
+
505
+ /* Picker outline over the page */
506
+ .pick-box {
507
+ position: fixed; z-index: 2147483646; pointer-events: none;
508
+ outline: 2px solid #2563eb; outline-offset: 1px;
509
+ background: rgb(37 99 235 / 0.08);
510
+ border-radius: 2px;
511
+ }
512
+ .pick-box[hidden] { display: none; }
513
+ .pick-tag {
514
+ position: absolute; left: -2px; bottom: calc(100% + 4px);
515
+ padding: 2px 6px; border-radius: 4px;
516
+ background: #2563eb; color: #fff;
517
+ font: 11px/1.4 ui-monospace, "SF Mono", Menlo, monospace; white-space: nowrap;
518
+ }
519
+
520
+ /* Local view: what's selected and where tweaks go */
521
+ .scope-bar {
522
+ display: grid; gap: 10px;
523
+ padding: 6px 14px 12px;
524
+ border-bottom: 1px solid var(--p-line);
525
+ }
526
+ .scope-head { display: flex; align-items: center; gap: 4px; margin-left: -6px; }
527
+ .scope-name { min-width: 0; display: grid; line-height: 1.3; }
528
+ .scope-name code, .change-scope code, .change-token code, .empty code {
529
+ font: 11px/1.4 ui-monospace, "SF Mono", Menlo, monospace; color: var(--p-fg);
530
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
531
+ }
532
+ .scope-name .muted { font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
533
+ .scope-target:empty, .warnings:empty { display: none; }
534
+ .muted { color: var(--p-muted); }
535
+ .empty { padding: 14px; color: var(--p-muted); font-size: 12px; line-height: 1.5; }
536
+
537
+ /* Contrast warnings */
538
+ .warnings { display: grid; gap: 6px; }
539
+ .body > .warnings { padding: 10px 14px 0; }
540
+ .warning {
541
+ display: flex; align-items: flex-start; gap: 6px;
542
+ padding: 6px 8px; border-radius: 6px; /* Figma Warning (83:337), 31px tall with its 1px border */
543
+ border: 1px solid #d97706;
544
+ background: linear-gradient(rgb(217 119 6 / 0.04), rgb(217 119 6 / 0.04)), var(--p-bg);
545
+ color: #b45309;
546
+ font-size: 12px; line-height: 1.4;
547
+ }
548
+ .warning svg { flex: none; width: 16px; height: 16px; margin-top: 0.5px; }
549
+ @media (prefers-color-scheme: dark) { .warning { color: #fbbf24; } }
550
+
551
+ /* Changes list */
552
+ .change-group { padding: 10px 14px; border-bottom: 1px solid var(--p-line); display: grid; gap: 6px; }
553
+ .change-group:last-child { border-bottom: 0; }
554
+ .change-scope { display: flex; align-items: baseline; gap: 8px; min-width: 0; font-size: 12px; font-weight: 600; }
555
+ .change-scope code { color: var(--p-muted); font-weight: 400; }
556
+ .link-btn {
557
+ padding: 0; border: 0; background: none; font: inherit; color: var(--p-fg);
558
+ text-decoration: underline; text-underline-offset: 2px; cursor: pointer; white-space: nowrap;
559
+ }
560
+ .change-row { display: flex; align-items: flex-start; gap: 6px; }
561
+ .change-main { flex: 1; min-width: 0; display: grid; gap: 2px; }
562
+ .change-title { font-size: 12px; }
563
+ .change-title b { font-weight: 600; }
564
+ .change-token { font: 11px/1.4 ui-monospace, "SF Mono", Menlo, monospace; color: var(--p-muted); overflow-wrap: anywhere; }
565
+ .change-row .icon-btn { width: 22px; height: 22px; flex: none; }
566
+
567
+ /* Apply preview dialog */
568
+ .dialog-backdrop {
569
+ position: fixed; inset: 0; z-index: 2147483647;
570
+ display: grid; place-items: center; padding: 16px;
571
+ background: rgb(0 0 0 / 0.35);
572
+ font: 13px/1.4 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
573
+ --p-bg: #ffffff; --p-fg: #18181b; --p-muted: #71717a; --p-line: #e4e4e7; --p-hover: #f4f4f5;
574
+ --p-accent: #18181b; --p-on-accent: #ffffff;
575
+ --p-shadow: 0 12px 40px rgb(0 0 0 / 0.14), 0 2px 6px rgb(0 0 0 / 0.06);
576
+ }
577
+ @media (prefers-color-scheme: dark) {
578
+ .dialog-backdrop {
579
+ --p-bg: #1c1c1f; --p-fg: #f4f4f5; --p-muted: #a1a1aa; --p-line: #2e2e33; --p-hover: #27272a;
580
+ --p-accent: #f4f4f5; --p-on-accent: #18181b;
581
+ }
582
+ }
583
+ .dialog-backdrop[hidden] { display: none; }
584
+ .dialog {
585
+ width: min(640px, 100%); max-height: calc(100vh - 32px);
586
+ display: flex; flex-direction: column;
587
+ background: var(--p-bg); color: var(--p-fg);
588
+ border: 1px solid var(--p-line); border-radius: 14px; box-shadow: var(--p-shadow);
589
+ overflow: hidden;
590
+ }
591
+ .dialog-head {
592
+ display: flex; align-items: center; justify-content: space-between;
593
+ padding: 10px 10px 10px 16px; border-bottom: 1px solid var(--p-line);
594
+ font-weight: 600; font-size: 14px;
595
+ }
596
+ .prompt-text {
597
+ flex: 1; min-height: 320px; margin: 0; padding: 14px 16px; resize: none;
598
+ border: 0; background: var(--p-bg); color: var(--p-fg);
599
+ font: 12px/1.55 ui-monospace, "SF Mono", Menlo, monospace;
600
+ }
601
+ .prompt-text:focus { outline: none; }
602
+ .dialog-foot {
603
+ display: flex; align-items: center; gap: 12px; justify-content: space-between;
604
+ padding: 10px 16px; border-top: 1px solid var(--p-line); font-size: 12px;
605
+ }
606
+ .dialog-foot .apply-btn { flex: none; }
607
+
608
+ /* Setup hint (proposal): shown when the page isn't ready for Twiddle */
609
+ .setup-hint {
610
+ margin: 8px 10px 4px; padding: 10px 12px;
611
+ display: grid; gap: 8px;
612
+ border: 1px solid var(--p-line); border-radius: 10px;
613
+ background: var(--p-hover); color: var(--p-fg);
614
+ font-size: 12px; line-height: 1.45;
615
+ }
616
+ .setup-hint[hidden] { display: none; }
617
+ .setup-head { display: flex; align-items: center; gap: 6px; }
618
+ .setup-head svg { flex: none; color: #b45309; }
619
+ .setup-head b { flex: 1; font-size: 12.5px; font-weight: 600; }
620
+ .setup-head .icon-btn { width: 22px; height: 22px; margin: -4px -6px -4px 0; }
621
+ .setup-hint p { margin: 0; color: var(--p-muted); }
622
+ .setup-snippet { display: grid; gap: 3px; min-width: 0; }
623
+ .setup-snippet > span { font-size: 11px; color: var(--p-muted); }
624
+ .setup-code {
625
+ display: flex; align-items: center; gap: 4px; min-width: 0;
626
+ padding: 2px 2px 2px 8px; border-radius: 6px;
627
+ background: var(--p-bg); border: 1px solid var(--p-line);
628
+ }
629
+ .setup-code code {
630
+ flex: 1; min-width: 0; overflow-x: auto; white-space: nowrap; padding: 4px 0;
631
+ font: 11px/1.5 ui-monospace, "SF Mono", Menlo, monospace;
632
+ scrollbar-width: none;
633
+ }
634
+ .setup-code .text-btn { height: 24px; margin: 0; padding: 0 8px; flex: none; }
635
+ .setup-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
636
+ .setup-actions a { color: var(--p-fg); font-weight: 500; text-underline-offset: 2px; }
637
+ .launcher { position: relative; }
638
+ .launcher[data-hint]::after {
639
+ content: ""; position: absolute; top: 2px; right: 2px;
640
+ width: 10px; height: 10px; border-radius: 50%;
641
+ background: #d97706; box-shadow: 0 0 0 2px var(--p-bg);
642
+ }
643
+ @media (prefers-color-scheme: dark) { .setup-head svg { color: #fbbf24; } }
644
+
645
+ /* ---------- Layout direction A (Figma 80:245 › A1b, A2) ---------- */
646
+ .head[hidden], .page[hidden], .edit-row[hidden] { display: none; }
647
+ .subhead { padding-left: 8px; }
648
+ .page-fields { padding: 14px 14px 16px; display: grid; gap: 14px; color: var(--p-muted); }
649
+ .page-warnings:empty { display: none; }
650
+
651
+ /* Edit list rows look like the Figma Section row (6:134) */
652
+ .nav-row {
653
+ width: 100%; display: flex; align-items: center; gap: 8px;
654
+ padding: 11px 14px; border: 0; border-bottom: 1px solid var(--p-line);
655
+ background: transparent; color: var(--p-fg);
656
+ font: inherit; font-weight: 500; text-align: left; cursor: pointer;
657
+ }
658
+ .nav-row:last-child { border-bottom: 0; }
659
+ .nav-row:hover { background: var(--p-hover); }
660
+ .nav-row .chev { margin-left: auto; color: var(--p-muted); display: grid; }
661
+
662
+ /* "Edit every token" row above the footer (Figma: Edit row (proposal)) */
663
+ .edit-row {
664
+ display: flex; align-items: center; gap: 8px;
665
+ padding: 11px 14px; border: 0; border-top: 1px solid var(--p-line);
666
+ background: transparent; color: var(--p-fg);
667
+ font: inherit; font-weight: 500; text-align: left; cursor: pointer;
668
+ }
669
+ .edit-row:hover { background: var(--p-hover); }
670
+ .edit-row .chev { margin-left: auto; color: var(--p-muted); display: grid; }
671
+
672
+ /* Character pad (Figma: Character pad (proposal)): 170px tall, 9 × 5 dots */
673
+ .pad {
674
+ position: relative; height: 170px; border-radius: 10px;
675
+ background: var(--p-track); touch-action: none; cursor: pointer;
676
+ user-select: none;
677
+ }
678
+ .pad:focus-visible { outline: 2px solid var(--p-accent); outline-offset: 2px; }
679
+ .pad-area { position: absolute; inset: 33px 31px; }
680
+ .pad-dot {
681
+ position: absolute; width: 2px; height: 2px; margin: -1px 0 0 -1px; border-radius: 50%;
682
+ background: var(--p-muted); opacity: .35;
683
+ }
684
+ .pad-knob {
685
+ position: absolute; width: 20px; height: 20px; margin: -10px 0 0 -10px; border-radius: 50%;
686
+ background: var(--p-bg); border: 2px solid var(--p-accent);
687
+ box-shadow: 0 1px 3px rgb(0 0 0 / 0.15);
688
+ transition: left .08s ease, top .08s ease;
689
+ }
690
+ .pad.dragging .pad-knob { transition: none; }
691
+ .pad-edge { position: absolute; font-size: 11px; color: var(--p-muted); line-height: 1; pointer-events: none; }
692
+ .pad-edge.top { top: 9px; left: 50%; transform: translateX(-50%); }
693
+ .pad-edge.bottom { bottom: 9px; left: 50%; transform: translateX(-50%); }
694
+ .pad-edge.left { left: 9px; top: 50%; transform: translate(-50%, -50%) rotate(-90deg); transform-origin: center; margin-left: 5px; }
695
+ .pad-edge.right { right: 9px; top: 50%; transform: translate(50%, -50%) rotate(90deg); transform-origin: center; margin-right: 5px; }
696
+
697
+ /* Token rows (Figma: Token row 86:337, Value field 83:348, Custom tag 83:358) */
698
+ .token-rows { display: grid; gap: 2px; }
699
+ .token-row { display: flex; align-items: center; gap: 8px; min-height: 30px; }
700
+ .token-name {
701
+ min-width: 56px; flex: none;
702
+ font: 11px/1.4 ui-monospace, "SF Mono", Menlo, monospace; color: var(--p-muted);
703
+ white-space: nowrap;
704
+ }
705
+ .token-row.edited .token-name { color: var(--p-fg); }
706
+ .token-bar { flex: 1; min-width: 0; height: 6px; display: flex; }
707
+ .token-bar span { height: 6px; border-radius: 2px; background: var(--p-line); }
708
+ .token-row.edited .token-bar span { background: var(--p-accent); }
709
+ .token-gap { flex: 1; }
710
+ .custom-tag {
711
+ flex: none; height: 18px; padding: 0 6px; border-radius: 9px;
712
+ display: inline-flex; align-items: center;
713
+ background: var(--p-hover); color: var(--p-fg); font-size: 11px; font-weight: 500;
714
+ }
715
+ .custom-tag[hidden], .token-reset[hidden] { display: none; }
716
+ .token-value {
717
+ flex: none; width: 64px; height: 28px; padding: 0 8px;
718
+ display: flex; align-items: center; gap: 2px;
719
+ border: 1px solid var(--p-line); border-radius: 6px; background: var(--p-bg); cursor: text;
720
+ }
721
+ .token-row.edited .token-value, .token-value:focus-within { border-color: var(--p-accent); }
722
+ .token-value input {
723
+ flex: 1; min-width: 0; width: 100%; padding: 0; border: 0; background: transparent;
724
+ color: var(--p-fg); font: inherit; font-size: 13px; font-weight: 500;
725
+ }
726
+ .token-value input:focus { outline: none; }
727
+ .token-unit { font-size: 12px; color: var(--p-muted); }
728
+ .token-unit[hidden] { display: none; }
729
+ .kind-colour .token-value { width: 108px; padding-left: 4px; gap: 6px; }
730
+ .kind-text .token-gap { display: none; }
731
+ .kind-text .token-value { flex: 1; width: auto; min-width: 0; } /* fills what's left, so an edited row still fits */
732
+ .kind-text .token-value input, .kind-colour .token-value input { font: 11px ui-monospace, "SF Mono", Menlo, monospace; }
733
+ .token-swatch { width: 18px; height: 18px; flex: none; border-radius: 4px; box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.1); }
734
+ .token-reset { width: 22px; height: 22px; flex: none; }
735
+ .token-row:not(.edited) .token-value { margin-right: 30px; } /* keeps values aligned where the reset sits */
736
+
737
+ /* Spacing › Roles (Figma: Role row 86:349, Step select 83:357) */
738
+ .tab-pane { display: grid; gap: 14px; }
739
+ .page-fields > *, .tab-pane > *, .token-field > *, .token-rows > * { min-width: 0; } /* let long rows shrink (labels ellipsis) instead of widening the panel */
740
+ .tab-pane[hidden] { display: none; }
741
+ .role-row { display: flex; align-items: center; gap: 8px; min-height: 32px; }
742
+ .role-label { flex: 1; min-width: 0; font-size: 12px; color: var(--p-fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
743
+ .role-row.unused .role-label { color: var(--p-muted); }
744
+ .role-row.edited .role-label { font-weight: 500; }
745
+ .role-fixed { width: 56px; }
746
+ .role-row.is-fixed .step-select select { width: 74px; } /* "Fixed" needs less room, so the label fits */
747
+ .role-fixed[hidden] { display: none; }
748
+ .role-row:not(.edited) .step-select { margin-right: 30px; } /* aligned with rows that show a reset */
749
+ .step-select { position: relative; flex: none; display: flex; }
750
+ .step-select select {
751
+ appearance: none; -webkit-appearance: none;
752
+ height: 28px; padding: 0 24px 0 8px; max-width: 118px;
753
+ border: 1px solid var(--p-line); border-radius: 6px; background: var(--p-bg);
754
+ color: var(--p-fg); font: inherit; font-size: 13px; font-weight: 500; cursor: pointer;
755
+ }
756
+ .role-row.edited .step-select select, .step-select select:focus-visible { border-color: var(--p-accent); outline: none; }
757
+ .step-chevron { position: absolute; right: 7px; top: 50%; transform: translateY(-50%); display: grid; color: var(--p-muted); pointer-events: none; }
758
+ .role-note { margin: 0; font-size: 11px; line-height: 1.45; color: var(--p-muted); }
759
+ `, j = (t, e, n) => `<svg width="${t}" height="${t}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="${e}" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${n}</svg>`, $ = {
760
+ dial: j(20, 1.8, '<circle cx="12" cy="12" r="8"/><path d="M12 12l4-4"/><path d="M12 2v2M12 20v2M2 12h2M20 12h2"/>'),
761
+ link: j(15, 2, '<path d="M10 13a5 5 0 0 0 7.07 0l3-3a5 5 0 0 0-7.07-7.07l-1 1"/><path d="M14 11a5 5 0 0 0-7.07 0l-3 3a5 5 0 0 0 7.07 7.07l1-1"/>'),
762
+ reset: j(15, 2, '<path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 3v6h6"/>'),
763
+ close: j(15, 2, '<path d="M18 6L6 18M6 6l12 12"/>'),
764
+ chevron: j(12, 2.4, '<path d="M9 6l6 6-6 6"/>'),
765
+ // Proposals: not yet in the Figma Icon/* set.
766
+ pick: j(15, 2, '<circle cx="12" cy="12" r="7"/><path d="M12 2v4M12 18v4M2 12h4M18 12h4"/>'),
767
+ compare: j(15, 2, '<circle cx="12" cy="12" r="9"/><path d="M12 3v18" /><path d="M12 3a9 9 0 0 0 0 18z" fill="currentColor" stroke="none"/>'),
768
+ undo: j(15, 2, '<path d="M9 14L4 9l5-5"/><path d="M4 9h11a5 5 0 0 1 0 10h-3"/>'),
769
+ redo: j(15, 2, '<path d="M15 14l5-5-5-5"/><path d="M20 9H9a5 5 0 0 0 0 10h3"/>'),
770
+ back: j(15, 2, '<path d="M15 18l-6-6 6-6"/>'),
771
+ down: j(12, 2.4, '<path d="M6 9l6 6 6-6"/>'),
772
+ copy: j(13, 2, '<rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/>'),
773
+ warn: j(13, 2, '<path d="M12 3l9.5 17h-19z"/><path d="M12 10v4M12 17.5v.01"/>'),
774
+ sliders: j(15, 2, '<path d="M4 7h10M18 7h2M4 17h4M12 17h8"/><circle cx="16" cy="7" r="2"/><circle cx="10" cy="17" r="2"/>')
775
+ // Figma: Icon/Sliders (proposal)
776
+ }, an = "twiddle:";
777
+ function Fe(t) {
778
+ try {
779
+ return localStorage.getItem(an + t);
780
+ } catch {
781
+ return null;
782
+ }
783
+ }
784
+ function be(t, e) {
785
+ try {
786
+ localStorage.setItem(an + t, e);
787
+ } catch {
788
+ }
789
+ }
790
+ function X({ label: t, options: e, onSelect: n }) {
791
+ const o = document.createElement("div");
792
+ o.className = "seg", o.setAttribute("role", "radiogroup"), o.setAttribute("aria-label", t);
793
+ const a = e.map((l) => {
794
+ const c = document.createElement("button");
795
+ return c.type = "button", c.setAttribute("role", "radio"), c.dataset.value = l.value, c.textContent = l.label, l.title && (c.title = l.title), c.addEventListener("click", () => d(l.value)), c;
796
+ });
797
+ o.append(...a);
798
+ let s = null;
799
+ function r(l) {
800
+ s = l, a.forEach((p) => p.setAttribute("aria-checked", String(p.dataset.value === l)));
801
+ const c = Math.max(0, a.findIndex((p) => p.dataset.value === l));
802
+ a.forEach((p, m) => {
803
+ p.tabIndex = m === c ? 0 : -1;
804
+ });
805
+ }
806
+ function d(l, c) {
807
+ l !== s && (r(l), n(l)), c && a.find((p) => p.dataset.value === l).focus();
808
+ }
809
+ return o.addEventListener("keydown", (l) => {
810
+ const c = a.indexOf(l.target);
811
+ if (c < 0) return;
812
+ const p = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 }[l.key];
813
+ let m;
814
+ if (p) m = (c + p + a.length) % a.length;
815
+ else if (l.key === "Home") m = 0;
816
+ else if (l.key === "End") m = a.length - 1;
817
+ else return;
818
+ l.preventDefault(), d(a[m].dataset.value, !0);
819
+ }), r(null), { el: o, setValue: r };
820
+ }
821
+ const rn = /var\(\s*(--twd-[\w-]+)/g, Bn = /::?[\w-]+(\([^)]*\))?/g;
822
+ function* $e(t) {
823
+ for (const e of t)
824
+ e.selectorText != null && (yield e), e.cssRules && (yield* $e(e.cssRules));
825
+ }
826
+ function Vn() {
827
+ const t = [];
828
+ for (const e of document.styleSheets) {
829
+ if (e.ownerNode?.id === "twiddle-scoped") continue;
830
+ let n;
831
+ try {
832
+ n = e.cssRules;
833
+ } catch {
834
+ continue;
835
+ }
836
+ for (const o of $e(n)) {
837
+ const a = [...o.style.cssText.matchAll(rn)].map((s) => s[1]);
838
+ a.length && t.push({ selectors: o.selectorText.split(","), vars: a });
839
+ }
840
+ }
841
+ return t;
842
+ }
843
+ function Un(t, e) {
844
+ const n = e.replace(Bn, "").trim() || "*";
845
+ try {
846
+ return t.matches(n);
847
+ } catch {
848
+ return !1;
849
+ }
850
+ }
851
+ function sn(t) {
852
+ const e = /* @__PURE__ */ new Set(), n = [];
853
+ return Vn().forEach(({ selectors: o, vars: a }) => {
854
+ const s = o.find((r) => Un(t, r));
855
+ s && (a.forEach((r) => e.add(r)), n.push(s.trim()));
856
+ }), [...(t.getAttribute("style") || "").matchAll(rn)].forEach((o) => e.add(o[1])), { names: e, selectors: n };
857
+ }
858
+ const We = (t) => window.CSS && CSS.escape ? CSS.escape(t) : t;
859
+ function qn(t) {
860
+ const e = [];
861
+ let n = t;
862
+ for (; n && n.nodeType === 1 && n !== document.body && n !== document.documentElement; ) {
863
+ if (n.id && document.querySelectorAll(`#${We(n.id)}`).length === 1)
864
+ return e.unshift(`#${We(n.id)}`), e.join(" > ");
865
+ const o = n.localName, a = [...n.parentElement.children].filter((s) => s.localName === o);
866
+ e.unshift(a.length > 1 ? `${o}:nth-of-type(${a.indexOf(n) + 1})` : o), n = n.parentElement;
867
+ }
868
+ return ["body", ...e].join(" > ");
869
+ }
870
+ function Hn(t, e = sn(t).selectors) {
871
+ const n = t.getAttribute("data-twd");
872
+ if (n) return `[data-twd="${n.replace(/"/g, "")}"]`;
873
+ const o = [...t.classList], s = o.find((r) => e.some((d) => new RegExp(`\\.${r.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w-])`).test(d))) || o[0];
874
+ return s ? `.${We(s)}` : null;
875
+ }
876
+ function _n(t) {
877
+ const e = sn(t), n = Hn(t, e.selectors), o = (t.getAttribute("aria-label") || t.placeholder || t.textContent || "").trim().replace(/\s+/g, " ");
878
+ return {
879
+ el: t,
880
+ tag: t.localName,
881
+ classes: [...t.classList],
882
+ text: o.length > 40 ? `${o.slice(0, 40)}…` : o,
883
+ tokens: e.names,
884
+ elementSelector: qn(t),
885
+ classSelector: n,
886
+ classCount: n ? document.querySelectorAll(n).length : 0
887
+ };
888
+ }
889
+ const Ye = (t) => t.localName + [...t.classList].map((e) => `.${e}`).join(""), Gn = { radius: "px", intensity: "%", base: "px" }, ln = (t) => t.charAt(0).toUpperCase() + t.slice(1), Kn = (t, e) => typeof e == "number" || /^\d+$/.test(e) ? `${e}${Gn[t] || ""}` : ln(String(e));
890
+ function Tt(t, e, n) {
891
+ const o = on(() => t.tokens({ [t.id]: e }) || {}), a = Object.keys(o);
892
+ let s = {};
893
+ if (a.length) {
894
+ const r = n.selector ? document.querySelector(n.selector) : void 0;
895
+ s = jn(a, r || void 0);
896
+ }
897
+ return {
898
+ ...n,
899
+ sectionId: t.id,
900
+ title: t.title,
901
+ settings: Object.entries(e).map(([r, d]) => ({ key: r, label: ln(r), value: Kn(r, d), ...t.describe?.(r, d) || {} })),
902
+ tokens: a.map((r) => ({ name: r, from: s[r] || "", to: o[r] }))
903
+ };
904
+ }
905
+ function Lt(t, e) {
906
+ const n = [];
907
+ return e.forEach((o) => {
908
+ t[o.id] && n.push(Tt(o, t[o.id], { kind: "global", selector: null }));
909
+ }), Object.entries(t.scopes || {}).forEach(([o, a]) => {
910
+ const s = Ge(o) ? "class" : "element";
911
+ e.forEach((r) => {
912
+ a[r.id] && n.push(Tt(r, a[r.id], { kind: s, selector: o }));
913
+ });
914
+ }), n;
915
+ }
916
+ function Wn(t) {
917
+ const e = (a) => Object.values(a || {}).reduce((s, r) => s + Object.keys(r || {}).length, 0), { scopes: n, ...o } = t;
918
+ return e(o) + Object.values(n || {}).reduce((a, s) => a + e(s), 0);
919
+ }
920
+ const W = (t) => `\`${t}\``, Oe = (t) => t.map((e) => ` - ${W(e.name)}: ${e.from ? `${W(e.from)} → ` : ""}${W(e.to)}`), Re = (t) => `${t.title} › ${t.settings.map((e) => `${e.label}: ${e.value}`).join(", ")}`;
921
+ function Yn(t) {
922
+ let e = null;
923
+ try {
924
+ e = document.querySelector(t);
925
+ } catch {
926
+ }
927
+ if (!e) return "not found on the current page";
928
+ const n = (e.getAttribute("aria-label") || e.placeholder || e.textContent || "").trim().replace(/\s+/g, " ").slice(0, 60);
929
+ return `${W(Ye(e))}${n ? ` with text “${n}”` : ""}`;
930
+ }
931
+ function Pt(t, e) {
932
+ const n = /* @__PURE__ */ new Map();
933
+ return t.forEach((o) => {
934
+ const a = o[e];
935
+ n.has(a) || n.set(a, []), n.get(a).push(o);
936
+ }), [...n];
937
+ }
938
+ function Xn(t, { link: e = location.href, warnings: n = [] } = {}) {
939
+ if (!t.length) return "";
940
+ const o = t.filter((d) => d.kind === "global"), a = t.filter((d) => d.kind === "class"), s = t.filter((d) => d.kind === "element"), r = [];
941
+ if (r.push(
942
+ "# Apply design tweaks from Panel",
943
+ "",
944
+ "I explored design changes to this prototype with Panel (by Twiddle), a tweak panel that overrides the prototype's `--twd-*` CSS custom properties (design tokens) at runtime. Make these changes permanent in the source code.",
945
+ "",
946
+ `Preview of the tweaked version: ${e}`
947
+ ), o.length && (r.push(
948
+ "",
949
+ "## 1. Global token changes",
950
+ "",
951
+ "Update the default value of each token where it is defined (usually a `:root` rule or a `tokens.css` / `twiddle-tokens.css` file). Change the definition, not the places that use it.",
952
+ ""
953
+ ), o.forEach((d) => r.push(`- **${Re(d)}**`, ...Oe(d.tokens)))), a.length && (r.push(
954
+ "",
955
+ `## ${o.length ? 2 : 1}. Component-wide changes`,
956
+ "",
957
+ "These apply to every element matching the selector. In the CSS rule for that component, redeclare the token, e.g. `.input { --twd-radius-control: 999px; }`. Don't replace the `var(--twd-…)` usages with fixed values.",
958
+ ""
959
+ ), Pt(a, "selector").forEach(([d, l]) => {
960
+ r.push(`- ${W(d)} (${document.querySelectorAll(d).length} on this page)`), l.forEach((c) => r.push(` - **${Re(c)}**`, ...Oe(c.tokens).map((p) => ` ${p}`)));
961
+ })), s.length) {
962
+ const d = 1 + (o.length ? 1 : 0) + (a.length ? 1 : 0);
963
+ r.push(
964
+ "",
965
+ `## ${d}. Single-element changes`,
966
+ "",
967
+ "These apply to one element only. Add a modifier class to that element (e.g. `input--pill`) and redeclare the token in a rule for that class. The selectors below are runtime paths to help you find the element; don't copy them into the stylesheet.",
968
+ ""
969
+ ), Pt(s, "selector").forEach(([l, c]) => {
970
+ r.push(`- ${Yn(l)}, found at ${W(l)}`), c.forEach((p) => r.push(` - **${Re(p)}**`, ...Oe(p.tokens).map((m) => ` ${m}`)));
971
+ });
972
+ }
973
+ return n.length && r.push(
974
+ "",
975
+ "## Contrast to check",
976
+ "",
977
+ "Panel flagged these colour pairs as below WCAG AA with the new values:",
978
+ "",
979
+ ...n.map((d) => `- ${d.text}`)
980
+ ), r.push(
981
+ "",
982
+ "## Constraints",
983
+ "",
984
+ "- Keep styling through the `--twd-*` tokens; don't hard-code the new values in component rules.",
985
+ "- Change only what is listed above.",
986
+ "- If a value uses `color-mix()` and the project targets older browsers, ask before converting it.",
987
+ "- When done, list the files you changed."
988
+ ), r.join(`
989
+ `);
990
+ }
991
+ const ye = 4.5, Jn = 3, Zn = [
992
+ ["--twd-text", "--twd-bg", "Text on background"],
993
+ ["--twd-text", "--twd-surface", "Text on surface"],
994
+ ["--twd-text-muted", "--twd-surface", "Muted text on surface"],
995
+ ["--twd-on-primary", "--twd-primary", "Text on brand"]
996
+ ];
997
+ let H;
998
+ function tt(t) {
999
+ if (!t || (H ||= Object.assign(document.createElement("canvas"), { width: 1, height: 1 }).getContext("2d", { willReadFrequently: !0 }), !H)) return null;
1000
+ H.clearRect(0, 0, 1, 1), H.fillStyle = "#000", H.fillStyle = t, H.fillRect(0, 0, 1, 1);
1001
+ const [e, n, o, a] = H.getImageData(0, 0, 1, 1).data;
1002
+ return [e, n, o, a / 255];
1003
+ }
1004
+ function Ft(t) {
1005
+ const e = document.createElement("i");
1006
+ e.style.cssText = `position:absolute;visibility:hidden;color:var(${t})`, document.documentElement.appendChild(e);
1007
+ const n = getComputedStyle(e).color;
1008
+ return e.remove(), tt(n);
1009
+ }
1010
+ const Ot = ([t, e, n]) => {
1011
+ const o = (a) => (a /= 255, a <= 0.03928 ? a / 12.92 : ((a + 0.055) / 1.055) ** 2.4);
1012
+ return 0.2126 * o(t) + 0.7152 * o(e) + 0.0722 * o(n);
1013
+ }, dn = (t, e) => t.slice(0, 3).map((n, o) => n * t[3] + e[o] * (1 - t[3])).concat(1);
1014
+ function cn(t, e) {
1015
+ const n = Ot(t[3] < 1 ? dn(t, e) : t), o = Ot(e);
1016
+ return (Math.max(n, o) + 0.05) / (Math.min(n, o) + 0.05);
1017
+ }
1018
+ const pn = (t) => `${t.toFixed(1)}:1`;
1019
+ function Ne() {
1020
+ const t = [];
1021
+ return Zn.forEach(([e, n, o]) => {
1022
+ const a = Ft(e), s = Ft(n);
1023
+ if (!a || !s || s[3] === 0) return;
1024
+ const r = cn(a, s);
1025
+ r < ye && t.push({ label: o, ratio: r, needed: ye, text: `${o}: ${pn(r)} · AA needs ${ye}` });
1026
+ }), t;
1027
+ }
1028
+ function Qn(t) {
1029
+ const e = [];
1030
+ for (let n = t; n && n.nodeType === 1; n = n.parentElement) {
1031
+ const o = tt(getComputedStyle(n).backgroundColor);
1032
+ if (o && o[3] > 0 && (e.push(o), o[3] >= 1))
1033
+ break;
1034
+ }
1035
+ return e.reduceRight((n, o) => dn(o, n), [255, 255, 255, 1]);
1036
+ }
1037
+ function eo(t) {
1038
+ if (!t.textContent.trim() && !t.placeholder) return null;
1039
+ const e = getComputedStyle(t), n = tt(e.color);
1040
+ if (!n) return null;
1041
+ const o = parseFloat(e.fontSize), s = o >= 24 || o >= 18.66 && parseInt(e.fontWeight, 10) >= 700 ? Jn : ye, r = cn(n, Qn(t));
1042
+ return r < s ? { label: "This element", ratio: r, needed: s, text: `This element’s text: ${pn(r)} · AA needs ${s}` } : null;
1043
+ }
1044
+ const Rt = "twiddle-panel", to = "0.6.0", Nt = "https://panel.twiddle.in", no = "--twd-radius-control", oo = /var\(\s*--twd-/;
1045
+ function ao(t) {
1046
+ return t.style.cssText.split(";").some((e) => {
1047
+ const n = e.slice(0, e.indexOf(":")).trim();
1048
+ return n && !n.startsWith("--") && oo.test(e);
1049
+ });
1050
+ }
1051
+ function ro() {
1052
+ if (!getComputedStyle(document.documentElement).getPropertyValue(no).trim()) return "no-tokens";
1053
+ let t = !1;
1054
+ for (const e of document.styleSheets) {
1055
+ if (e.ownerNode?.id === "twiddle-scoped") continue;
1056
+ let n;
1057
+ try {
1058
+ n = e.cssRules;
1059
+ } catch {
1060
+ t = !0;
1061
+ continue;
1062
+ }
1063
+ for (const o of $e(n)) if (ao(o)) return "ok";
1064
+ }
1065
+ return document.querySelector('[style*="var(--twd-"]') ? "ok" : t ? "unknown" : "unused";
1066
+ }
1067
+ function so() {
1068
+ const t = /* @__PURE__ */ new Set(), e = (n) => {
1069
+ for (const o of n.matchAll(/var\(\s*(--twd-[\w-]+)/g)) t.add(o[1]);
1070
+ };
1071
+ for (const n of document.styleSheets) {
1072
+ if (n.ownerNode?.id === "twiddle-scoped") continue;
1073
+ let o;
1074
+ try {
1075
+ o = n.cssRules;
1076
+ } catch {
1077
+ continue;
1078
+ }
1079
+ for (const a of $e(o))
1080
+ a.style.cssText.split(";").forEach((s) => {
1081
+ const r = s.slice(0, s.indexOf(":")).trim();
1082
+ r && !r.startsWith("--") && e(s);
1083
+ });
1084
+ }
1085
+ return document.querySelectorAll('[style*="var(--twd-"]').forEach((n) => e(n.getAttribute("style"))), t;
1086
+ }
1087
+ const jt = [
1088
+ { label: "With a build tool, in your entry file", code: `import '${Rt}/tokens.css';` },
1089
+ // Pinned to this build's version: unversioned jsDelivr links can lag a release by days, and unpkg once failed for a fresh one.
1090
+ { label: "Plain HTML, in <head>", code: `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/${Rt}@${to}/dist/twiddle-tokens.css">` }
1091
+ ], io = `Convert this prototype's styles to use the Panel design tokens (the --twd-* CSS custom properties from twiddle-panel), so the panel can tweak them live.
1092
+
1093
+ 1. Keep the current look. In a :root rule loaded after twiddle-tokens.css, set each token below to the value this project already uses (for example, if buttons use #0f766e, set --twd-primary: #0f766e). Only set the tokens the project actually needs.
1094
+ 2. Replace fixed values in component styles with var(--twd-…). Map each value to the closest token by role:
1095
+ - Fonts: --twd-font-heading, --twd-font-body
1096
+ - Text sizes: --twd-text-xs, -sm, -base, -lg, -xl, -2xl, -3xl; line height: --twd-leading-tight, --twd-leading-normal
1097
+ - Colours: --twd-bg (page), --twd-surface (cards, inputs), --twd-surface-raised (menus, popovers), --twd-text, --twd-text-muted, --twd-border, --twd-primary, --twd-primary-hover, --twd-on-primary (text on primary), --twd-focus, --twd-success, --twd-warning, --twd-danger
1098
+ - Buttons, inputs, chips: --twd-radius-control, --twd-control-height, --twd-control-pad-x
1099
+ - Cards, menus, modals: --twd-radius-container, --twd-border-width-container
1100
+ - Borders: --twd-border-width
1101
+ - Spacing (padding, margin, gap): --twd-space-1 to --twd-space-8 (4, 8, 12, 16, 24, 32, 48, 64px)
1102
+ - Layout roles, preferred where they fit: --twd-page-pad (side padding of the page), --twd-section-gap (between page sections), --twd-card-pad (inside cards, menus, modals), --twd-card-gap (between items in a card), --twd-control-gap (between buttons, chips, inputs in a row)
1103
+ - Shadows: --twd-shadow-sm, --twd-shadow-md, --twd-shadow-lg
1104
+ 3. Leave values that don't match a role as they are (layout widths, one-off illustrations, icon sizes). Don't invent new --twd-* tokens.
1105
+ 4. Don't change the layout or how the page looks; after the change it should look the same until someone uses the panel.
1106
+ 5. When done, list the files you changed and any values you left fixed, with the reason.`;
1107
+ function B(t) {
1108
+ const e = document.createElement("div");
1109
+ e.className = "field";
1110
+ const n = document.createElement("div");
1111
+ n.className = "field-head";
1112
+ const o = document.createElement("span");
1113
+ o.className = "field-label", o.textContent = t;
1114
+ const a = document.createElement("span");
1115
+ return a.className = "field-value", a.hidden = !0, n.append(o, a), e.append(n), {
1116
+ el: e,
1117
+ setValue(s) {
1118
+ a.textContent = s || "", a.hidden = !s;
1119
+ },
1120
+ append(...s) {
1121
+ e.append(...s);
1122
+ }
1123
+ };
1124
+ }
1125
+ function lo({ label: t, ends: e, cols: n = 9, rows: o = 5, onInput: a }) {
1126
+ const s = document.createElement("div");
1127
+ s.className = "pad", s.tabIndex = 0, s.setAttribute("role", "group"), s.setAttribute("aria-roledescription", "2D slider");
1128
+ const r = (g, u) => {
1129
+ const y = document.createElement("span");
1130
+ return y.className = `pad-edge ${g}`, y.textContent = u, y.setAttribute("aria-hidden", "true"), y;
1131
+ };
1132
+ s.append(r("top", e.top), r("bottom", e.bottom), r("left", e.left), r("right", e.right));
1133
+ const d = document.createElement("div");
1134
+ d.className = "pad-area";
1135
+ for (let g = 0; g < o; g++)
1136
+ for (let u = 0; u < n; u++) {
1137
+ const y = document.createElement("i");
1138
+ y.className = "pad-dot", y.style.left = `${u / (n - 1) * 100}%`, y.style.top = `${g / (o - 1) * 100}%`, d.append(y);
1139
+ }
1140
+ const l = document.createElement("span");
1141
+ l.className = "pad-knob", d.append(l), s.append(d);
1142
+ let c = 0.5, p = 0.5;
1143
+ const m = (g, u) => Math.round(Math.min(1, Math.max(0, g)) * (u - 1)) / (u - 1);
1144
+ function v() {
1145
+ l.style.left = `${c * 100}%`, l.style.top = `${p * 100}%`;
1146
+ }
1147
+ function f(g, u, y) {
1148
+ g = y === "x" ? c : m(g, n), u = y === "y" ? p : m(u, o), !(g === c && u === p) && (c = g, p = u, v(), a(c, p));
1149
+ }
1150
+ const x = (g) => {
1151
+ const u = d.getBoundingClientRect();
1152
+ f((g.clientX - u.left) / u.width, (g.clientY - u.top) / u.height);
1153
+ };
1154
+ s.addEventListener("pointerdown", (g) => {
1155
+ s.setPointerCapture(g.pointerId), s.classList.add("dragging"), x(g);
1156
+ }), s.addEventListener("pointermove", (g) => {
1157
+ s.hasPointerCapture(g.pointerId) && x(g);
1158
+ });
1159
+ const b = (g) => {
1160
+ s.hasPointerCapture(g.pointerId) && s.releasePointerCapture(g.pointerId), s.classList.remove("dragging");
1161
+ };
1162
+ return s.addEventListener("pointerup", b), s.addEventListener("pointercancel", b), s.addEventListener("keydown", (g) => {
1163
+ const u = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] }[g.key];
1164
+ u && (g.preventDefault(), u[0] ? f(m(c, n) + u[0] / (n - 1), p, "y") : f(c, m(p, o) + u[1] / (o - 1), "x"));
1165
+ }), {
1166
+ el: s,
1167
+ /** Show a position without telling anyone (used to sync). Off-grid values are shown as they are. */
1168
+ setValue(g, u, y) {
1169
+ c = Math.min(1, Math.max(0, g)), p = Math.min(1, Math.max(0, u)), v(), s.setAttribute("aria-label", y ? `${t}: ${y}` : t);
1170
+ }
1171
+ };
1172
+ }
1173
+ function un({ label: t, min: e, max: n, step: o = 1, format: a = String, onInput: s }) {
1174
+ const r = document.createElement("div");
1175
+ r.className = "slider";
1176
+ const d = document.createElement("input");
1177
+ d.type = "range", d.min = String(e), d.max = String(n), d.step = String(o), d.setAttribute("aria-label", t);
1178
+ const l = document.createElement("span");
1179
+ l.className = "slider-value", l.setAttribute("aria-hidden", "true"), r.append(d, l);
1180
+ const c = (p) => {
1181
+ const m = (Number(d.value) - e) / (n - e) * 100;
1182
+ d.style.setProperty("--fill", `${m}%`), l.textContent = p ?? a(Number(d.value)), d.setAttribute("aria-valuetext", l.textContent);
1183
+ };
1184
+ return d.addEventListener("input", () => {
1185
+ c(), s(Number(d.value));
1186
+ }), {
1187
+ el: r,
1188
+ /** Show a value without reporting it (sync after reset / URL load). */
1189
+ setValue(p, m) {
1190
+ d.value = String(p), c(m);
1191
+ }
1192
+ };
1193
+ }
1194
+ const F = (t) => getComputedStyle(document.documentElement).getPropertyValue(t).trim();
1195
+ function Se(t, e) {
1196
+ const n = /^(-?\d*\.?\d+)([a-z%]*)$/i.exec(String(t).trim());
1197
+ if (!n) return null;
1198
+ const o = parseFloat(n[1]) * e, a = n[2] || "px";
1199
+ return a === "px" ? `${Math.round(o)}px` : `${+o.toFixed(3)}${a}`;
1200
+ }
1201
+ const co = 999;
1202
+ function hn({ id: t, title: e, hint: n, token: o, uses: a = [], presets: s, max: r, extra: d = [], extraTokens: l = () => ({}) }) {
1203
+ const c = (v) => s.find((f) => f.value === v), p = (v) => v >= co ? "full" : `${v}px`, m = (v) => {
1204
+ if (typeof v.radius == "number" && isFinite(v.radius))
1205
+ return { [o]: `${Math.min(Math.max(v.radius, 0), r)}px` };
1206
+ const f = c(v.corners);
1207
+ return f ? { [o]: `${f.radius}px` } : {};
1208
+ };
1209
+ return {
1210
+ id: t,
1211
+ title: e,
1212
+ hint: n,
1213
+ presets: s,
1214
+ uses: [o, ...a],
1215
+ tokens(v) {
1216
+ const f = v[t] || {};
1217
+ return { ...m(f), ...l(f) };
1218
+ },
1219
+ // One render function per knob, so a layout can place them separately.
1220
+ // Each takes an optional label (e.g. "Buttons" on the Corners page).
1221
+ parts: {
1222
+ corners(v, f, { label: x = "Corners" } = {}) {
1223
+ const b = B(x), g = X({
1224
+ label: `${e} corners`,
1225
+ options: s.map((k) => ({ value: k.value, label: k.label, title: p(k.radius) })),
1226
+ onSelect: (k) => f.setSection(t, { corners: k, radius: null })
1227
+ }), u = un({
1228
+ label: `${e} corner radius`,
1229
+ min: 0,
1230
+ max: r,
1231
+ format: p,
1232
+ onInput: (k) => f.setSection(t, { radius: k, corners: null })
1233
+ });
1234
+ b.append(g.el, u.el), v.append(b.el);
1235
+ const y = () => {
1236
+ const k = parseFloat((f.readToken || F)(o)), O = f.getState()[t] || {}, Z = c(O.corners) || s.find((Q) => Q.radius === k);
1237
+ if (g.setValue(Z ? Z.value : null), isNaN(k)) {
1238
+ u.setValue(0, "—"), b.setValue("");
1239
+ return;
1240
+ }
1241
+ u.setValue(Math.min(k, r), p(k)), b.setValue(p(k));
1242
+ };
1243
+ y(), f.onChange(y);
1244
+ },
1245
+ ...Object.fromEntries(d.map((v) => [v.key, (f, x, { label: b } = {}) => f.append(le(x, { section: t, ...v, label: b || v.label }))]))
1246
+ },
1247
+ render(v, f) {
1248
+ Object.values(this.parts).forEach((x) => x(v, f));
1249
+ }
1250
+ };
1251
+ }
1252
+ function le(t, { section: e, key: n, label: o, options: a, fallback: s, valueText: r }) {
1253
+ const d = B(o), l = X({
1254
+ label: o,
1255
+ options: a,
1256
+ onSelect: (p) => t.setSection(e, { [n]: p })
1257
+ });
1258
+ d.append(l.el);
1259
+ const c = () => {
1260
+ const p = (t.getState()[e] || {})[n] || s;
1261
+ l.setValue(p), d.setValue(r ? r(p) : "");
1262
+ };
1263
+ return c(), t.onChange(c), d.el;
1264
+ }
1265
+ function po(t, { section: e, key: n, label: o, min: a, max: s, fallback: r, format: d }) {
1266
+ const l = B(o), c = un({ label: o, min: a, max: s, format: d, onInput: (m) => t.setSection(e, { [n]: m }) });
1267
+ l.append(c.el);
1268
+ const p = () => {
1269
+ const m = (t.getState()[e] || {})[n];
1270
+ c.setValue(typeof m == "number" ? m : r);
1271
+ };
1272
+ return p(), t.onChange(p), l.el;
1273
+ }
1274
+ const zt = (t, e) => e.map((n) => `${t}${n}`), fn = {
1275
+ fonts: { label: "Tokens", tokens: ["font-heading", "font-body"], kind: "text" },
1276
+ type: { label: "Tokens", tokens: zt("text-", ["xs", "sm", "base", "lg", "xl", "2xl", "3xl"]), kind: "length" },
1277
+ leading: { label: "Line height", tokens: ["leading-tight", "leading-normal"], kind: "number" },
1278
+ colours: { label: "Tokens", tokens: ["bg", "surface", "surface-raised", "text", "text-muted", "border", "primary", "primary-hover", "on-primary", "focus", "success", "warning", "danger"], kind: "colour" },
1279
+ spacing: { label: "Steps", tokens: zt("space-", [1, 2, 3, 4, 5, 6, 7, 8]), kind: "length", bar: 64 },
1280
+ controls: { label: "Controls", tokens: ["control-height", "control-pad-x"], kind: "length" },
1281
+ corners: { label: "Tokens", tokens: ["radius-control", "radius-container"], kind: "length" },
1282
+ borders: { label: "Tokens", tokens: ["border-width", "border-width-container"], kind: "length" },
1283
+ shadows: { label: "Tokens", tokens: ["shadow-sm", "shadow-md", "shadow-lg"], kind: "text" }
1284
+ }, q = (t) => `--twd-${t}`, je = (t, e) => Object.prototype.hasOwnProperty.call(t.getState().custom || {}, e), uo = (t, e) => fn[e].tokens.some((n) => (t.custom || {})[n] != null);
1285
+ function ho(t, e) {
1286
+ const n = /^(-?\d*\.?\d+)(px|rem)?$/.exec(String(t).trim());
1287
+ return !n || e === "text" || e === "colour" ? null : n[2] === "rem" ? { n: +(parseFloat(n[1]) * 16).toFixed(1), unit: "px" } : { n: +parseFloat(n[1]).toFixed(3), unit: n[2] || (e === "length" ? "px" : "") };
1288
+ }
1289
+ function fo(t, e) {
1290
+ const n = t.trim();
1291
+ return n ? e === "length" && /^-?\d*\.?\d+$/.test(n) ? `${n}px` : n : null;
1292
+ }
1293
+ function go(t, e, n, o) {
1294
+ const a = document.createElement("div");
1295
+ a.className = `token-row kind-${n}`, a.innerHTML = `
1296
+ <code class="token-name" title="${q(e)}">${e}</code>
1297
+ ${o ? '<span class="token-bar"><span></span></span>' : '<span class="token-gap"></span>'}
1298
+ <span class="custom-tag" hidden>Custom</span>
1299
+ <label class="token-value">
1300
+ ${n === "colour" ? '<span class="token-swatch"></span>' : ""}
1301
+ <input type="text" spellcheck="false" aria-label="${q(e)}" ${n === "length" || n === "number" ? 'inputmode="decimal"' : ""}>
1302
+ <span class="token-unit"></span>
1303
+ </label>
1304
+ <button class="icon-btn token-reset" title="Back to the page’s value" aria-label="Reset ${q(e)}" hidden>${$.reset}</button>
1305
+ `;
1306
+ const s = a.querySelector("input"), r = a.querySelector(".token-unit"), d = a.querySelector(".token-bar span"), l = a.querySelector(".token-swatch"), c = a.querySelector(".custom-tag"), p = a.querySelector(".token-reset");
1307
+ let m = "";
1308
+ const v = () => {
1309
+ let x = s.value;
1310
+ m && /^-?\d*\.?\d+$/.test(x.trim()) && (x = `${x.trim()}${m}`);
1311
+ const b = fo(x, n), g = F(q(e));
1312
+ if (b === null) {
1313
+ je(t, e) ? t.setSection("custom", { [e]: null }) : f();
1314
+ return;
1315
+ }
1316
+ if (b === g && !je(t, e)) return f();
1317
+ t.setSection("custom", { [e]: b });
1318
+ };
1319
+ s.addEventListener("change", v), s.addEventListener("blur", () => f()), s.addEventListener("keydown", (x) => {
1320
+ x.key === "Enter" && (x.preventDefault(), s.blur()), x.key === "Escape" && (x.stopPropagation(), f(), s.blur());
1321
+ }), p.addEventListener("click", () => t.setSection("custom", { [e]: null }));
1322
+ function f() {
1323
+ const x = F(q(e)), b = je(t, e), g = ho(x, n);
1324
+ m = g ? g.unit : "", document.activeElement !== s && a.getRootNode().activeElement !== s && (s.value = g ? String(g.n) : x), s.title = x, r.textContent = m, r.hidden = !m, d && (d.style.width = `${Math.max(2, Math.min(100, (g ? g.n : 0) / o * 100))}%`), l && (l.style.background = x), a.classList.toggle("edited", b), c.hidden = !b, p.hidden = !b;
1325
+ }
1326
+ return f(), t.onChange(f), a;
1327
+ }
1328
+ const mo = {
1329
+ id: "custom",
1330
+ title: "Custom tokens",
1331
+ hint: "Any token, set to a fixed value",
1332
+ uses: [],
1333
+ // not offered for a single picked element (yet)
1334
+ tokens(t) {
1335
+ return Object.fromEntries(Object.entries(t.custom || {}).map(([e, n]) => [q(e), n]));
1336
+ },
1337
+ describe: (t, e) => ({ label: q(t), value: String(e) }),
1338
+ // One part per Edit page group, e.g. parts.spacing → rows for --twd-space-1 … 8.
1339
+ parts: Object.fromEntries(Object.entries(fn).map(([t, e]) => [t, (n, o, { label: a = e.label } = {}) => {
1340
+ const s = B(a);
1341
+ s.el.classList.add("token-field");
1342
+ const r = e.tokens[0], d = e.tokens[e.tokens.length - 1];
1343
+ s.setValue(e.tokens.length > 2 && /-\w+$/.test(r) ? `${q(r)} … ${d.split("-").pop()}` : "");
1344
+ const l = document.createElement("div");
1345
+ l.className = "token-rows", e.tokens.forEach((c) => l.append(go(o, c, e.kind, e.bar))), s.append(l), n.append(s.el);
1346
+ }]))
1347
+ }, Xe = { compact: 0.75, regular: 1, comfy: 1.25 }, Je = [0.75, 1.25], It = [1, 2, 3, 4, 5, 6, 7, 8].map((t) => `--twd-space-${t}`);
1348
+ function Ze(t = {}) {
1349
+ return typeof t.factor == "number" && isFinite(t.factor) ? Math.min(Je[1], Math.max(Je[0], t.factor)) : Xe[t.spacing] || 1;
1350
+ }
1351
+ const Dt = (t) => `${Math.round(t * 100)}%`, bo = {
1352
+ id: "density",
1353
+ title: "Density",
1354
+ hint: "Compact · regular · comfortable",
1355
+ uses: It,
1356
+ tokens(t) {
1357
+ const e = Ze(t.density);
1358
+ if (e === 1) return {};
1359
+ const n = {};
1360
+ return It.forEach((o) => {
1361
+ const a = Se(F(o), e);
1362
+ a && (n[o] = a);
1363
+ }), n;
1364
+ },
1365
+ /** How the Changes list and prompt describe a setting. */
1366
+ describe: (t, e) => t === "factor" ? { label: "Spacing", value: Dt(e) } : null,
1367
+ parts: {
1368
+ spacing(t, e, { label: n = "Spacing" } = {}) {
1369
+ const o = B(n), a = X({
1370
+ label: n,
1371
+ options: [{ value: "compact", label: "Compact" }, { value: "regular", label: "Regular" }, { value: "comfy", label: "Comfy" }],
1372
+ onSelect: (r) => e.setSection("density", { spacing: r, factor: null })
1373
+ });
1374
+ o.append(a.el);
1375
+ const s = () => {
1376
+ const r = e.getState().density || {}, d = Ze(r), l = Object.keys(Xe).find((c) => Xe[c] === d);
1377
+ a.setValue(l || null), o.setValue(uo(e.getState(), "spacing") ? "Custom" : l ? "" : Dt(d));
1378
+ };
1379
+ s(), e.onChange(s), t.append(o.el);
1380
+ }
1381
+ },
1382
+ render(t, e) {
1383
+ Object.values(this.parts).forEach((n) => n(t, e));
1384
+ }
1385
+ }, se = 60, xo = {
1386
+ soft: { sm: [0, 1, 2, 0, 0.06], md: [0, 4, 12, 0, 0.08], lg: [0, 12, 32, 0, 0.12] },
1387
+ // = tokens.css
1388
+ crisp: { sm: [0, 1, 1, 0, 0.12], md: [0, 2, 4, 0, 0.14], lg: [0, 4, 8, 0, 0.16] },
1389
+ bold: { sm: [2, 2, 0, 0, 0.85], md: [3, 3, 0, 0, 0.85], lg: [5, 5, 0, 0, 0.85] }
1390
+ }, ze = ([t, e, n, o, a], s) => {
1391
+ const r = Math.min(1, +(a * s).toFixed(3));
1392
+ return r <= 0 ? "none" : `${t}px ${e}px ${n}px ${o}px rgb(0 0 0 / ${r})`;
1393
+ }, wo = {
1394
+ id: "depth",
1395
+ title: "Depth",
1396
+ hint: "Flat · soft · crisp · bold",
1397
+ uses: ["--twd-shadow-sm", "--twd-shadow-md", "--twd-shadow-lg"],
1398
+ tokens(t) {
1399
+ const e = t.depth || {}, n = e.style || "soft", o = typeof e.intensity == "number" ? Math.min(100, Math.max(0, e.intensity)) : se;
1400
+ if (n === "flat")
1401
+ return { "--twd-shadow-sm": "none", "--twd-shadow-md": "none", "--twd-shadow-lg": "none" };
1402
+ const a = xo[n];
1403
+ if (!a) return {};
1404
+ if (n === "soft" && o === se) return {};
1405
+ const s = o / se;
1406
+ return {
1407
+ "--twd-shadow-sm": ze(a.sm, s),
1408
+ "--twd-shadow-md": ze(a.md, s),
1409
+ "--twd-shadow-lg": ze(a.lg, s)
1410
+ };
1411
+ },
1412
+ parts: {
1413
+ style(t, e, { label: n = "Style" } = {}) {
1414
+ t.append(le(e, {
1415
+ section: "depth",
1416
+ key: "style",
1417
+ label: n,
1418
+ fallback: "soft",
1419
+ options: [{ value: "flat", label: "Flat" }, { value: "soft", label: "Soft" }, { value: "crisp", label: "Crisp" }, { value: "bold", label: "Bold" }]
1420
+ }));
1421
+ },
1422
+ intensity(t, e, { label: n = "Intensity" } = {}) {
1423
+ t.append(po(e, { section: "depth", key: "intensity", label: n, min: 0, max: 100, fallback: se, format: (o) => `${o}%` }));
1424
+ }
1425
+ },
1426
+ render(t, e) {
1427
+ Object.values(this.parts).forEach((n) => n(t, e));
1428
+ }
1429
+ }, [Bt, xe] = Je, Vt = { square: "square", soft: "soft", rounded: "rounded", pill: "large" };
1430
+ function vo(t) {
1431
+ return t > 1.06 ? "Comfy" : t < 0.94 ? "Compact" : "Regular";
1432
+ }
1433
+ const yo = {
1434
+ character(t, e, { label: n = "Character" } = {}) {
1435
+ const o = B(n), a = lo({
1436
+ label: n,
1437
+ ends: { top: "Spacious", bottom: "Compact", left: "Subtle", right: "Bold" },
1438
+ onInput: (r, d) => {
1439
+ const l = +(xe - d * (xe - Bt)).toFixed(3), c = { intensity: Math.round(r * 100) };
1440
+ (e.getState().depth || {}).style === "flat" && (c.style = null), e.setSections({ density: { factor: l === 1 ? null : l, spacing: null }, depth: c }, "character");
1441
+ }
1442
+ });
1443
+ o.append(a.el);
1444
+ const s = () => {
1445
+ const r = e.getState(), d = Ze(r.density), l = r.depth || {}, c = l.style === "flat" ? 0 : typeof l.intensity == "number" ? l.intensity : se, p = `${vo(d)} · ${c}%`;
1446
+ a.setValue(c / 100, (xe - d) / (xe - Bt), p), o.setValue(p);
1447
+ };
1448
+ s(), e.onChange(s), t.append(o.el);
1449
+ },
1450
+ corners(t, e, { label: n = "Corners", buttons: o, cards: a }) {
1451
+ const s = B(n), r = X({
1452
+ label: n,
1453
+ options: o.presets.map((c) => ({ value: c.value, label: c.label })),
1454
+ onSelect: (c) => e.setSections({
1455
+ buttons: { corners: c, radius: null },
1456
+ cards: { corners: Vt[c], radius: null }
1457
+ })
1458
+ });
1459
+ s.append(r.el);
1460
+ const d = (c, p, m) => {
1461
+ if (p.radius != null) return null;
1462
+ if (p.corners) return p.corners;
1463
+ const v = parseFloat(F(m));
1464
+ return c.presets.find((f) => f.radius === v)?.value || null;
1465
+ }, l = () => {
1466
+ const c = e.getState(), p = d(o, c.buttons || {}, "--twd-radius-control"), m = d(a, c.cards || {}, "--twd-radius-container");
1467
+ r.setValue(p && Vt[p] === m ? p : null);
1468
+ const v = (b) => {
1469
+ const g = parseFloat(F(b));
1470
+ return isNaN(g) ? "" : g >= 999 ? "full" : `${g}px`;
1471
+ }, f = v("--twd-radius-control"), x = v("--twd-radius-container");
1472
+ s.setValue(f && x ? `${f} · ${x}` : "");
1473
+ };
1474
+ l(), e.onChange(l), t.append(s.el);
1475
+ }
1476
+ }, Ie = [
1477
+ { key: "page-pad", label: "Page padding", step: 5 },
1478
+ { key: "section-gap", label: "Section gap", step: 6 },
1479
+ { key: "card-pad", label: "Card padding", step: 5 },
1480
+ { key: "card-gap", label: "Card gap", step: 2 },
1481
+ { key: "control-gap", label: "Control gap", step: 3 },
1482
+ { key: "control-pad-x", label: "Control padding", step: null }
1483
+ ], ko = [1, 2, 3, 4, 5, 6, 7, 8], So = /^var\(\s*--twd-space-(\d)\s*\)$/, De = (t) => {
1484
+ const e = /^(-?\d*\.?\d+)(px|rem)?$/.exec(String(t).trim());
1485
+ return e ? +(parseFloat(e[1]) * (e[2] === "rem" ? 16 : 1)).toFixed(1) : null;
1486
+ };
1487
+ function Eo(t, e, n) {
1488
+ const o = `--twd-${e.key}`, a = document.createElement("div");
1489
+ a.className = "role-row", n || a.classList.add("unused"), a.innerHTML = `
1490
+ <span class="role-label" title="${o}${n ? "" : " (not used on this page)"}">${e.label}</span>
1491
+ <label class="token-value role-fixed" hidden><input type="text" inputmode="decimal" spellcheck="false" aria-label="${e.label}, fixed value"><span class="token-unit">px</span></label>
1492
+ <span class="step-select">
1493
+ <select aria-label="${e.label}"></select>
1494
+ <span class="step-chevron" aria-hidden="true">${$.down}</span>
1495
+ </span>
1496
+ <button class="icon-btn token-reset" title="Back to the default" aria-label="Reset ${e.label}" hidden>${$.reset}</button>
1497
+ `;
1498
+ const s = a.querySelector("select"), r = a.querySelector(".role-fixed"), d = r.querySelector("input"), l = a.querySelector(".token-reset"), c = () => (t.getState().custom || {})[e.key], p = () => {
1499
+ const b = c();
1500
+ if (b == null) return e.step ? { step: e.step } : { fixed: F(o) };
1501
+ const g = So.exec(b);
1502
+ return g ? { step: +g[1] } : { fixed: b };
1503
+ }, m = (b) => t.setSection("custom", { [e.key]: b }), v = (b) => e.step ? b === `var(--twd-space-${e.step})` : !1;
1504
+ s.addEventListener("change", () => {
1505
+ if (s.value === "fixed") {
1506
+ const g = De(F(o));
1507
+ m(g != null ? `${g}px` : F(o)), d.focus(), d.select();
1508
+ return;
1509
+ }
1510
+ const b = `var(--twd-space-${s.value})`;
1511
+ m(v(b) ? null : b);
1512
+ });
1513
+ const f = () => {
1514
+ const b = d.value.trim();
1515
+ if (!b) return m(null);
1516
+ m(/^-?\d*\.?\d+$/.test(b) ? `${b}px` : b);
1517
+ };
1518
+ d.addEventListener("change", f), d.addEventListener("blur", () => x()), d.addEventListener("keydown", (b) => {
1519
+ b.key === "Enter" && (b.preventDefault(), d.blur()), b.key === "Escape" && (b.stopPropagation(), x(), d.blur());
1520
+ }), l.addEventListener("click", () => m(null));
1521
+ function x() {
1522
+ const b = p();
1523
+ if (s.innerHTML = ko.map((u) => `<option value="${u}">space-${u} · ${De(F(`--twd-space-${u}`)) ?? "?"}</option>`).join("") + '<option value="fixed">Fixed</option>', s.value = b.step ? String(b.step) : "fixed", r.hidden = !b.fixed, a.classList.toggle("is-fixed", !!b.fixed), b.fixed && a.getRootNode().activeElement !== d) {
1524
+ const u = De(b.fixed);
1525
+ d.value = u != null ? String(u) : b.fixed;
1526
+ }
1527
+ const g = c() != null;
1528
+ a.classList.toggle("edited", g), l.hidden = !g;
1529
+ }
1530
+ return x(), t.onChange(x), a;
1531
+ }
1532
+ const $o = {
1533
+ spacing(t, e, { label: n = "Roles" } = {}) {
1534
+ const o = so(), a = B(n);
1535
+ a.el.classList.add("token-field");
1536
+ const s = Ie.filter((l) => !o.has(`--twd-${l.key}`));
1537
+ a.setValue(s.length === Ie.length ? "not used on this page" : "");
1538
+ const r = document.createElement("div");
1539
+ r.className = "token-rows", Ie.forEach((l) => r.append(Eo(e, l, o.has(`--twd-${l.key}`)))), a.append(r), t.append(a.el);
1540
+ const d = document.createElement("p");
1541
+ d.className = "role-note", d.textContent = s.length ? `Only elements styled with these tokens change. Not used on this page: ${s.map((l) => l.label).join(", ")}.` : "Each role points at a spacing step, so Density still scales it. Pick another step, or a fixed value.", t.append(d);
1542
+ }
1543
+ }, Co = [
1544
+ ["simple", "character"],
1545
+ // first, by Ravi's call (2026-10-13); the A1b mock has it last
1546
+ ["typography", "fonts"],
1547
+ ["colour", "brand"],
1548
+ ["colour", "neutral"],
1549
+ ["colour", "mode"],
1550
+ "warnings",
1551
+ ["simple", "corners"]
1552
+ ], we = [
1553
+ { id: "fonts", title: "Fonts", parts: [["typography", "fonts"], ["custom", "fonts"]] },
1554
+ { id: "type", title: "Type scale", parts: [["typography", "base"], ["typography", "scale"], ["custom", "type"], ["custom", "leading"]] },
1555
+ { id: "colours", title: "Colours", parts: [["colour", "brand"], ["colour", "neutral"], ["colour", "mode"], "warnings", ["custom", "colours"]] },
1556
+ { id: "spacing", title: "Spacing", parts: [{ tabs: [
1557
+ { label: "Scale", parts: [["density", "spacing"], ["custom", "spacing"], ["buttons", "size", { label: "Control size" }], ["custom", "controls"]] },
1558
+ { label: "Roles", parts: [["roles", "spacing"]] }
1559
+ ] }] },
1560
+ { id: "corners", title: "Corners", parts: [["buttons", "corners", { label: "Buttons" }], ["cards", "corners", { label: "Cards" }], ["custom", "corners"]] },
1561
+ { id: "borders", title: "Borders", parts: [["cards", "border", { label: "Cards" }], ["custom", "borders"]] },
1562
+ { id: "shadows", title: "Shadows", parts: [["depth", "style"], ["depth", "intensity"], ["custom", "shadows"]] }
1563
+ ], A = (t) => String(t).replace(/[&<>"]/g, (e) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[e]);
1564
+ function Mo({ sections: t, store: e, view: n }) {
1565
+ const o = document.createElement("div");
1566
+ o.id = "twiddle-root", document.body.appendChild(o);
1567
+ const a = o.attachShadow({ mode: "open" }), s = (i) => i.map((h) => `
1568
+ <details data-section="${h.id}">
1569
+ <summary>${h.title}<span class="chev">${$.chevron}</span></summary>
1570
+ <div class="section-body"><div class="placeholder">${h.hint}</div></div>
1571
+ </details>
1572
+ `).join("");
1573
+ a.innerHTML = `
1574
+ <style>${Dn}</style>
1575
+ <div class="pick-box" hidden><span class="pick-tag"></span></div>
1576
+ <div class="wrap">
1577
+ <div class="toast" role="status"></div>
1578
+ <div class="panel" role="dialog" aria-label="Panel" hidden>
1579
+ <div class="head">
1580
+ <span class="brand">Panel</span>
1581
+ <button class="icon-btn" data-action="pick" title="Pick an element to tweak on its own" aria-label="Pick an element" aria-pressed="false">${$.pick}</button>
1582
+ <button class="icon-btn" data-action="compare" title="Hold to see the original (Alt+C toggles)" aria-label="Compare with original" aria-pressed="false">${$.compare}</button>
1583
+ <button class="icon-btn" data-action="copy" title="Copy link to this version" aria-label="Copy link">${$.link}</button>
1584
+ <button class="icon-btn" data-action="reset" title="Reset to defaults" aria-label="Reset">${$.reset}</button>
1585
+ <button class="icon-btn" data-action="close" title="Close (Alt+T)" aria-label="Close">${$.close}</button>
1586
+ </div>
1587
+ <div class="head subhead" hidden>
1588
+ <button class="icon-btn" data-action="page-back" title="Back" aria-label="Back">${$.back}</button>
1589
+ <span class="brand page-title"></span>
1590
+ <button class="icon-btn" data-action="close" title="Close (Alt+T)" aria-label="Close">${$.close}</button>
1591
+ </div>
1592
+ <div class="original-pill" hidden>Showing original</div>
1593
+ <div class="body" data-view="global"><div class="setup-hint" role="status" hidden></div>
1594
+ <div class="page page-fields" data-page="simple"></div>
1595
+ <div class="page" data-page="edit" hidden>
1596
+ ${we.map((i) => `<button class="nav-row" data-go="${i.id}">${i.title}<span class="chev">${$.chevron}</span></button>`).join("")}
1597
+ </div>
1598
+ ${we.map((i) => `<div class="page page-fields" data-page="${i.id}" hidden></div>`).join("")}
1599
+ </div>
1600
+ <div class="body" data-view="local" hidden></div>
1601
+ <div class="body" data-view="changes" hidden></div>
1602
+ <button class="edit-row" data-action="edit">${$.sliders}<span>Edit every token</span><span class="chev">${$.chevron}</span></button>
1603
+ <div class="foot">
1604
+ <button class="icon-btn" data-action="undo" title="Undo (⌘Z)" aria-label="Undo">${$.undo}</button>
1605
+ <button class="icon-btn" data-action="redo" title="Redo (⇧⌘Z)" aria-label="Redo">${$.redo}</button>
1606
+ <button class="text-btn" data-action="changes" aria-pressed="false">Changes <span class="badge" hidden></span></button>
1607
+ <span class="spacer"></span>
1608
+ <div class="apply">
1609
+ <button class="apply-btn" data-action="apply" title="Copy these tweaks as a prompt for your coding agent">${$.copy}Copy prompt</button>
1610
+ <button class="apply-menu-btn" data-action="apply-menu" aria-label="Copy prompt options" aria-haspopup="true" aria-expanded="false">${$.down}</button>
1611
+ <div class="menu apply-menu" role="menu" hidden>
1612
+ <button role="menuitemradio" data-apply-mode="preview">Preview before copying</button>
1613
+ <button role="menuitemradio" data-apply-mode="copy">Copy without preview</button>
1614
+ </div>
1615
+ </div>
1616
+ </div>
1617
+ </div>
1618
+ <button class="launcher" aria-label="Open Panel" aria-expanded="false">${$.dial}</button>
1619
+ </div>
1620
+ <div class="dialog-backdrop" hidden>
1621
+ <div class="dialog" role="dialog" aria-modal="true" aria-labelledby="twd-dialog-title">
1622
+ <div class="dialog-head">
1623
+ <span id="twd-dialog-title">Prompt for your agent</span>
1624
+ <button class="icon-btn" data-action="dialog-close" aria-label="Close">${$.close}</button>
1625
+ </div>
1626
+ <textarea class="prompt-text" readonly spellcheck="false" aria-label="Generated prompt"></textarea>
1627
+ <div class="dialog-foot">
1628
+ <span class="muted">Paste it into Claude Code, Cursor or any agent working on this prototype.</span>
1629
+ <button class="apply-btn" data-action="dialog-copy">Copy prompt</button>
1630
+ </div>
1631
+ </div>
1632
+ </div>
1633
+ `;
1634
+ const r = (i) => a.querySelector(i), d = r(".panel"), l = r(".launcher"), c = r(".toast"), p = { global: r('[data-view="global"]'), local: r('[data-view="local"]'), changes: r('[data-view="changes"]') }, m = r(".pick-box"), v = r(".pick-tag"), f = { getState: e.getState, setSection: e.setSection, setSections: e.setSections, onChange: e.onChange };
1635
+ function x(i, h, w) {
1636
+ h.forEach((E) => {
1637
+ if (typeof E.render != "function") return;
1638
+ const T = i.querySelector(`[data-section="${E.id}"] .section-body`);
1639
+ T.innerHTML = "", E.render(T, w);
1640
+ });
1641
+ const S = [...i.querySelectorAll("details")];
1642
+ return S.forEach((E) => E.addEventListener("toggle", () => {
1643
+ E.open && S.forEach((T) => {
1644
+ T !== E && (T.open = !1);
1645
+ });
1646
+ })), S;
1647
+ }
1648
+ const b = Object.fromEntries(t.map((i) => [i.id, i]));
1649
+ function g(i, h) {
1650
+ h.forEach((w) => {
1651
+ if (w === "warnings") {
1652
+ const L = document.createElement("div");
1653
+ L.className = "warnings page-warnings", i.append(L);
1654
+ return;
1655
+ }
1656
+ if (w.tabs) {
1657
+ const L = w.tabs.map((P) => {
1658
+ const N = document.createElement("div");
1659
+ return N.className = "tab-pane", g(N, P.parts), N;
1660
+ }), re = X({
1661
+ label: "View",
1662
+ options: w.tabs.map((P, N) => ({ value: String(N), label: P.label })),
1663
+ onSelect: (P) => {
1664
+ re.setValue(P), L.forEach((N, Sn) => {
1665
+ N.hidden = String(Sn) !== P;
1666
+ });
1667
+ }
1668
+ });
1669
+ re.setValue("0"), L.forEach((P, N) => {
1670
+ P.hidden = N !== 0;
1671
+ }), i.append(re.el, ...L);
1672
+ return;
1673
+ }
1674
+ const [S, E, T = {}] = w, V = S === "simple" ? yo[E] : S === "roles" ? $o[E] : b[S]?.parts?.[E];
1675
+ V && V(i, f, { ...T, buttons: b.buttons, cards: b.cards });
1676
+ });
1677
+ }
1678
+ g(p.global.querySelector('[data-page="simple"]'), Co), we.forEach((i) => g(p.global.querySelector(`[data-page="${i.id}"]`), i.parts));
1679
+ const u = r(".head:not(.subhead)"), y = r(".subhead"), k = r(".edit-row");
1680
+ let O = "simple";
1681
+ function Z() {
1682
+ const i = ce === "global", h = i && O !== "simple";
1683
+ u.hidden = h, y.hidden = !h, k.hidden = !(i && O === "simple"), h && (r(".page-title").textContent = O === "edit" ? "Edit every token" : we.find((w) => w.id === O).title);
1684
+ }
1685
+ function Q(i) {
1686
+ O = i, p.global.querySelectorAll(".page").forEach((h) => {
1687
+ h.hidden = h.dataset.page !== i;
1688
+ }), p.global.scrollTop = 0, Z();
1689
+ }
1690
+ const gn = () => Q(O === "edit" ? "simple" : "edit"), de = (i) => i.map((h) => `<div class="warning">${$.warn}<span>${A(h.text)}</span></div>`).join("");
1691
+ let ce = "global";
1692
+ function Ce(i) {
1693
+ ce = i, Object.entries(p).forEach(([h, w]) => {
1694
+ w.hidden = h !== i;
1695
+ }), r('[data-action="changes"]').setAttribute("aria-pressed", String(i === "changes")), i !== "local" && bn(), i === "changes" && bt(), Z();
1696
+ }
1697
+ function ee() {
1698
+ d.hidden = !1, l.setAttribute("aria-expanded", "true"), l.setAttribute("aria-label", "Close Panel"), be("panel", "open");
1699
+ }
1700
+ function Me() {
1701
+ d.hidden = !0, l.setAttribute("aria-expanded", "false"), l.setAttribute("aria-label", "Open Panel"), be("panel", "closed"), pe();
1702
+ }
1703
+ function Ae() {
1704
+ d.hidden ? ee() : Me();
1705
+ }
1706
+ let nt;
1707
+ function U(i) {
1708
+ c.textContent = i, c.classList.add("show"), clearTimeout(nt), nt = setTimeout(() => c.classList.remove("show"), 1600);
1709
+ }
1710
+ function te(i, h) {
1711
+ navigator.clipboard.writeText(i).then(() => U(h)).catch(() => U("Couldn’t copy — select the text and copy it instead"));
1712
+ }
1713
+ let Te = null;
1714
+ function G(i, h) {
1715
+ if (Te = i, !i) {
1716
+ m.hidden = !0;
1717
+ return;
1718
+ }
1719
+ const w = i.getBoundingClientRect();
1720
+ Object.assign(m.style, { top: `${w.top}px`, left: `${w.left}px`, width: `${w.width}px`, height: `${w.height}px` }), v.textContent = h || Ye(i), m.hidden = !1;
1721
+ }
1722
+ const ot = () => {
1723
+ Te && !m.hidden && G(Te, v.textContent);
1724
+ };
1725
+ window.addEventListener("scroll", ot, !0), window.addEventListener("resize", ot);
1726
+ let ne = !1;
1727
+ const at = r('[data-action="pick"]'), rt = (i) => i.composedPath().includes(o);
1728
+ function st(i) {
1729
+ if (rt(i)) return;
1730
+ const h = i.target;
1731
+ h && h.nodeType === 1 && h !== document.documentElement && h !== document.body && G(h);
1732
+ }
1733
+ function it(i) {
1734
+ if (rt(i) || (i.preventDefault(), i.stopPropagation(), i.type !== "click")) return;
1735
+ const h = i.target;
1736
+ !h || h === document.documentElement || h === document.body || (pe(), pt(h));
1737
+ }
1738
+ function lt(i) {
1739
+ i.key === "Escape" && (i.preventDefault(), pe(), M && G(M.el));
1740
+ }
1741
+ const dt = ["pointerdown", "mousedown", "mouseup", "click"];
1742
+ function ct() {
1743
+ if (!ne) {
1744
+ if (ne = !0, !document.getElementById("twiddle-picking")) {
1745
+ const i = document.createElement("style");
1746
+ i.id = "twiddle-picking", i.textContent = "html.twd-picking, html.twd-picking * { cursor: crosshair !important; }", document.head.appendChild(i);
1747
+ }
1748
+ ee(), at.setAttribute("aria-pressed", "true"), document.documentElement.classList.add("twd-picking"), document.addEventListener("pointermove", st, !0), dt.forEach((i) => document.addEventListener(i, it, !0)), document.addEventListener("keydown", lt, !0), U("Click an element · Esc to cancel");
1749
+ }
1750
+ }
1751
+ function pe() {
1752
+ ne && (ne = !1, at.setAttribute("aria-pressed", "false"), document.documentElement.classList.remove("twd-picking"), document.removeEventListener("pointermove", st, !0), dt.forEach((i) => document.removeEventListener(i, it, !0)), document.removeEventListener("keydown", lt, !0), G(M ? M.el : null));
1753
+ }
1754
+ let M = null;
1755
+ function mn(i, h) {
1756
+ const w = [];
1757
+ return {
1758
+ ctx: {
1759
+ getState: () => (e.getState().scopes || {})[i] || {},
1760
+ setSection: (S, E) => e.setScope(i, S, E),
1761
+ onChange: (S) => {
1762
+ const E = e.onChange(S);
1763
+ return w.push(E), E;
1764
+ },
1765
+ readToken: (S) => getComputedStyle(h).getPropertyValue(S).trim()
1766
+ },
1767
+ dispose: () => w.forEach((S) => S())
1768
+ };
1769
+ }
1770
+ function pt(i, h) {
1771
+ const w = _n(i), S = e.getState().scopes || {};
1772
+ h || (h = !S[w.elementSelector] && w.classSelector && S[w.classSelector] ? "class" : "element"), M && M.dispose(), M = { el: i, info: w, target: h, dispose: () => {
1773
+ } }, Le(), Ce("local"), ee(), G(i);
1774
+ }
1775
+ const ut = () => M.target === "class" ? M.info.classSelector : M.info.elementSelector;
1776
+ function Le() {
1777
+ M.dispose();
1778
+ const { info: i } = M, h = t.filter((P) => (P.uses || []).some((N) => i.tokens.has(N))), w = p.local;
1779
+ if (w.innerHTML = `
1780
+ <div class="scope-bar">
1781
+ <div class="scope-head">
1782
+ <button class="icon-btn" data-action="back" title="Back to the whole page" aria-label="Back to the whole page">${$.back}</button>
1783
+ <div class="scope-name">
1784
+ <code>${A(Ye(i.el))}</code>
1785
+ ${i.text ? `<span class="muted">“${A(i.text)}”</span>` : ""}
1786
+ </div>
1787
+ </div>
1788
+ <div class="scope-target"></div>
1789
+ <div class="warnings"></div>
1790
+ </div>
1791
+ ${h.length ? s(h) : '<div class="empty">This element doesn’t use Panel tokens yet, so there’s nothing to tweak here. Style it with <code>var(--twd-…)</code> to make it tweakable.</div>'}
1792
+ `, i.classSelector) {
1793
+ const P = X({
1794
+ label: "Apply to",
1795
+ options: [
1796
+ { value: "element", label: "This element" },
1797
+ { value: "class", label: `All ${i.classSelector} (${i.classCount})`, title: `Every element matching ${i.classSelector}` }
1798
+ ],
1799
+ onSelect: (N) => {
1800
+ N === "class" && e.moveScope(i.elementSelector, i.classSelector), M.target = N, Le();
1801
+ }
1802
+ });
1803
+ P.setValue(M.target), w.querySelector(".scope-target").append(P.el);
1804
+ }
1805
+ const { ctx: S, dispose: E } = mn(ut(), i.el), T = x(w, h, S);
1806
+ T[0] && (T[0].open = !0);
1807
+ const V = w.querySelector(".warnings"), L = () => {
1808
+ const P = i.tokens.size ? eo(i.el) : null;
1809
+ V.innerHTML = P ? de([P]) : "";
1810
+ };
1811
+ L();
1812
+ const re = e.onChange(L);
1813
+ M.dispose = () => {
1814
+ E(), re();
1815
+ };
1816
+ }
1817
+ function ht() {
1818
+ if (!M || !M.info.classSelector) return;
1819
+ const i = e.getState().scopes || {}, h = M.target === "class" ? "element" : "class", w = h === "class" ? M.info.classSelector : M.info.elementSelector;
1820
+ !i[ut()] && i[w] && (M.target = h, Le());
1821
+ }
1822
+ const ft = () => {
1823
+ e.undo(), ht();
1824
+ }, gt = () => {
1825
+ e.redo(), ht();
1826
+ };
1827
+ function bn() {
1828
+ M && (M.dispose(), M = null, p.local.innerHTML = "", G(null));
1829
+ }
1830
+ const mt = (i) => i.kind === "global" ? "Whole page" : i.kind === "class" ? `All ${i.selector}` : "One element";
1831
+ function bt() {
1832
+ const i = Lt(e.getState(), t), h = Ne(), w = p.changes;
1833
+ if (!i.length) {
1834
+ w.innerHTML = `<div class="empty">No changes yet. Tweak a section, or pick an element to change it on its own.</div>${de(h)}`;
1835
+ return;
1836
+ }
1837
+ const S = /* @__PURE__ */ new Map();
1838
+ i.forEach((E) => {
1839
+ const T = E.selector || "";
1840
+ S.has(T) || S.set(T, []), S.get(T).push(E);
1841
+ }), w.innerHTML = (h.length ? `<div class="warnings">${de(h)}</div>` : "") + [...S].map(([E, T]) => `
1842
+ <div class="change-group">
1843
+ <div class="change-scope">
1844
+ ${E ? `<button class="link-btn" data-select="${A(E)}" title="Select it on the page">${A(mt(T[0]))}</button><code>${A(E)}</code>` : `<span>${mt(T[0])}</span>`}
1845
+ </div>
1846
+ ${T.map((V) => `
1847
+ <div class="change-row">
1848
+ <div class="change-main">
1849
+ <div class="change-title">${A(V.title)} <span class="muted">›</span> ${V.settings.map((L) => `${A(L.label)}: <b>${A(L.value)}</b>`).join(", ")}</div>
1850
+ ${V.tokens.map((L) => `<div class="change-token"><code>${A(L.name)}</code> ${L.from ? `${A(L.from)} → ` : ""}${A(L.to)}</div>`).join("")}
1851
+ </div>
1852
+ <button class="icon-btn" data-remove="${A(V.sectionId)}" data-remove-scope="${A(E)}" aria-label="Remove ${A(V.title)} change">${$.close}</button>
1853
+ </div>`).join("")}
1854
+ </div>`).join("");
1855
+ }
1856
+ function xn(i, h) {
1857
+ if (h) return e.removeScope(h, i);
1858
+ const w = e.getState()[i] || {};
1859
+ e.setSection(i, Object.fromEntries(Object.keys(w).map((S) => [S, null])));
1860
+ }
1861
+ const oe = r(".apply-menu"), xt = r('[data-action="apply-menu"]'), wt = () => Fe("apply-mode") === "copy" ? "copy" : "preview";
1862
+ function wn() {
1863
+ oe.querySelectorAll("[data-apply-mode]").forEach((i) => i.setAttribute("aria-checked", String(i.dataset.applyMode === wt())));
1864
+ }
1865
+ function ue(i) {
1866
+ oe.hidden = !i, xt.setAttribute("aria-expanded", String(i)), i && wn();
1867
+ }
1868
+ function vt() {
1869
+ return Xn(Lt(e.getState(), t), { warnings: Ne() });
1870
+ }
1871
+ const he = r(".dialog-backdrop"), yt = r(".prompt-text");
1872
+ function vn() {
1873
+ const i = vt();
1874
+ if (!i) return U("No changes to copy yet");
1875
+ if (wt() === "copy") return te(i, "Prompt copied · paste it into your agent");
1876
+ yt.value = i, he.hidden = !1, r('[data-action="dialog-copy"]').focus();
1877
+ }
1878
+ function Pe() {
1879
+ he.hidden = !0, r('[data-action="apply"]').focus();
1880
+ }
1881
+ const K = r('[data-action="compare"]'), yn = r(".original-pill");
1882
+ function fe(i) {
1883
+ i ? n.suspend() : n.resume(), K.setAttribute("aria-pressed", String(i)), yn.hidden = !i;
1884
+ }
1885
+ let kt = null, ae = !1;
1886
+ K.addEventListener("pointerdown", () => {
1887
+ ae = !1, kt = setTimeout(() => {
1888
+ ae = !0, fe(!0);
1889
+ }, 180);
1890
+ });
1891
+ const St = () => {
1892
+ clearTimeout(kt), ae && (fe(!1), ae = !1);
1893
+ };
1894
+ K.addEventListener("pointerup", St), K.addEventListener("pointerleave", St);
1895
+ const ge = r(".setup-hint"), Et = `setup-hint-dismissed:${location.pathname}`, kn = {
1896
+ "no-tokens": {
1897
+ title: "The Panel tokens aren’t loaded",
1898
+ text: "The knobs change the --twd-* variables, but this page doesn’t load them. Add the token stylesheet before your own CSS:",
1899
+ snippets: jt,
1900
+ link: `${Nt}/#install`
1901
+ },
1902
+ unused: {
1903
+ title: "Nothing here uses the Panel tokens yet",
1904
+ text: "The tokens are loaded, but no style on this page uses var(--twd-…), so the knobs won’t change anything. Ask your coding agent to convert the styles:",
1905
+ copy: "Copy prompt for your agent",
1906
+ copied: "Prompt copied",
1907
+ text2copy: io,
1908
+ link: `${Nt}/#tokens`
1909
+ }
1910
+ };
1911
+ let z = null;
1912
+ function me() {
1913
+ const i = ro();
1914
+ z = Fe(Et) === i ? null : kn[i] || null, l.toggleAttribute("data-hint", !!z), ge.hidden = !z, z && (ge.innerHTML = `
1915
+ <div class="setup-head">${$.warn}<b>${A(z.title)}</b>
1916
+ <button class="icon-btn" data-action="setup-dismiss" title="Hide this message" aria-label="Hide this message">${$.close}</button>
1917
+ </div>
1918
+ <p>${A(z.text)}</p>
1919
+ ${(z.snippets || []).map((h, w) => `
1920
+ <div class="setup-snippet">
1921
+ <span>${A(h.label)}</span>
1922
+ <div class="setup-code"><code>${A(h.code)}</code><button class="text-btn" data-setup-snippet="${w}">Copy</button></div>
1923
+ </div>`).join("")}
1924
+ <div class="setup-actions">
1925
+ ${z.copy ? `<button class="apply-btn" data-action="setup-copy">${A(z.copy)}</button>` : ""}
1926
+ <a href="${z.link}" target="_blank" rel="noopener">Setup guide</a>
1927
+ </div>`, ge.dataset.status = i);
1928
+ }
1929
+ me(), document.readyState !== "complete" && window.addEventListener("load", me, { once: !0 }), setTimeout(me, 1500);
1930
+ const $t = r(".badge");
1931
+ function Ct() {
1932
+ r('[data-action="undo"]').disabled = !e.canUndo(), r('[data-action="redo"]').disabled = !e.canRedo();
1933
+ const i = Wn(e.getState());
1934
+ $t.textContent = i, $t.hidden = !i;
1935
+ const h = de(Ne());
1936
+ p.global.querySelectorAll(".page-warnings").forEach((w) => {
1937
+ w.innerHTML = h;
1938
+ }), ce === "changes" && bt();
1939
+ }
1940
+ return e.onChange(Ct), Ct(), a.addEventListener("click", (i) => {
1941
+ if (i.target === he) return Pe();
1942
+ const h = i.target.closest("button");
1943
+ if (h) {
1944
+ if (h !== xt && !oe.contains(h) && ue(!1), h === l) return Ae();
1945
+ if (h.dataset.select) {
1946
+ const w = document.querySelector(h.dataset.select);
1947
+ return w ? pt(w) : U("That element isn’t on this page");
1948
+ }
1949
+ if (h.dataset.setupSnippet) return te(jt[h.dataset.setupSnippet].code, "Copied");
1950
+ if (h.dataset.go) return Q(h.dataset.go);
1951
+ if (h.dataset.remove) return xn(h.dataset.remove, h.dataset.removeScope);
1952
+ if (h.dataset.applyMode)
1953
+ return be("apply-mode", h.dataset.applyMode), ue(!1), U(h.dataset.applyMode === "copy" ? "Copy prompt copies straight away" : "Copy prompt shows a preview first");
1954
+ switch (h.dataset.action) {
1955
+ case "close":
1956
+ return Me();
1957
+ case "reset":
1958
+ return e.resetState(), U("Reset to defaults · ⌘Z to undo");
1959
+ case "copy":
1960
+ return te(location.href, "Link copied");
1961
+ case "pick":
1962
+ return ne ? pe() : ct();
1963
+ case "compare":
1964
+ ae || fe(K.getAttribute("aria-pressed") !== "true");
1965
+ return;
1966
+ case "undo":
1967
+ return ft();
1968
+ case "redo":
1969
+ return gt();
1970
+ case "back":
1971
+ return Ce("global");
1972
+ case "edit":
1973
+ return Q("edit");
1974
+ case "page-back":
1975
+ return gn();
1976
+ case "changes":
1977
+ return Ce(ce === "changes" ? "global" : "changes");
1978
+ case "apply":
1979
+ return vn();
1980
+ case "apply-menu":
1981
+ return ue(oe.hidden);
1982
+ case "dialog-close":
1983
+ return Pe();
1984
+ case "dialog-copy":
1985
+ return te(yt.value, "Prompt copied · paste it into your agent");
1986
+ case "setup-copy":
1987
+ return z && te(z.text2copy, z.copied);
1988
+ case "setup-dismiss":
1989
+ return be(Et, ge.dataset.status), me();
1990
+ }
1991
+ }
1992
+ }), a.addEventListener("keydown", (i) => {
1993
+ if (i.key === "Escape") {
1994
+ if (!he.hidden) return Pe();
1995
+ if (!oe.hidden) return ue(!1);
1996
+ }
1997
+ !(i.metaKey || i.ctrlKey) || i.key.toLowerCase() !== "z" || i.target.matches?.('input[type="text"], textarea') || (i.preventDefault(), i.shiftKey ? gt() : ft());
1998
+ }), document.addEventListener("keydown", (i) => {
1999
+ i.altKey && (i.code === "KeyT" && (i.preventDefault(), Ae()), i.code === "KeyC" && (i.preventDefault(), fe(K.getAttribute("aria-pressed") !== "true")));
2000
+ }), Fe("panel") === "open" && ee(), { open: ee, close: Me, toggle: Ae, toast: U, pick: ct, prompt: vt };
2001
+ }
2002
+ const Ao = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>', To = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>';
2003
+ let Lo = 0;
2004
+ function Po({ label: t, options: e, onSelect: n, placeholder: o, onOpen: a }) {
2005
+ const s = `twd-fonts-${++Lo}`, r = document.createElement("div");
2006
+ r.className = "font-select";
2007
+ const d = document.createElement("button");
2008
+ d.type = "button", d.className = "font-trigger", d.setAttribute("aria-haspopup", "listbox"), d.setAttribute("aria-expanded", "false"), d.setAttribute("aria-controls", s), d.innerHTML = `
2009
+ <span class="font-aa" aria-hidden="true">Aa</span>
2010
+ <span class="font-name"></span>
2011
+ <span class="font-fonts"></span>
2012
+ <span class="font-chevron">${Ao}</span>`;
2013
+ const l = document.createElement("div");
2014
+ l.className = "font-menu", l.id = s, l.setAttribute("role", "listbox"), l.setAttribute("aria-label", t), l.hidden = !0;
2015
+ const c = e.map((u) => {
2016
+ const y = document.createElement("div");
2017
+ return y.className = "font-option", y.setAttribute("role", "option"), y.tabIndex = -1, y.dataset.value = u.value, y.innerHTML = `
2018
+ <span class="font-aa" style="font-family:${u.preview}" aria-hidden="true">Aa</span>
2019
+ <span class="font-text"><span class="font-name">${u.name}</span><span class="font-fonts">${u.fonts}</span></span>
2020
+ <span class="font-check">${To}</span>`, y.addEventListener("click", () => g(u.value)), y;
2021
+ });
2022
+ l.append(...c), r.append(d, l);
2023
+ let p = null;
2024
+ function m(u) {
2025
+ const y = e.find((O) => O.value === u), k = y || o || e[0];
2026
+ p = y ? y.value : o ? null : e[0].value, d.querySelector(".font-name").textContent = k.name, d.querySelector(".font-fonts").textContent = k.fonts, d.querySelector(".font-aa").style.fontFamily = k.preview, d.setAttribute("aria-label", `${t}: ${k.name}, ${k.fonts}`), c.forEach((O) => O.setAttribute("aria-selected", String(O.dataset.value === p)));
2027
+ }
2028
+ function v() {
2029
+ const u = d.getBoundingClientRect(), y = l.offsetHeight, O = window.innerHeight - u.bottom - 8 >= y ? u.bottom + 4 : Math.max(8, u.top - 4 - y);
2030
+ l.style.top = `${O}px`, l.style.left = `${u.left}px`, l.style.width = `${u.width}px`;
2031
+ }
2032
+ function f() {
2033
+ l.hidden && (l.hidden = !1, d.setAttribute("aria-expanded", "true"), a && a(), v(), (c.find((u) => u.dataset.value === p) || c[0]).focus(), setTimeout(() => document.addEventListener("pointerdown", b, !0)), window.addEventListener("resize", x), r.closest(".body")?.addEventListener("scroll", x, { once: !0 }));
2034
+ }
2035
+ function x(u) {
2036
+ l.hidden || (l.hidden = !0, d.setAttribute("aria-expanded", "false"), document.removeEventListener("pointerdown", b, !0), window.removeEventListener("resize", x), u === !0 && d.focus());
2037
+ }
2038
+ function b(u) {
2039
+ u.composedPath().includes(r) || x();
2040
+ }
2041
+ function g(u) {
2042
+ u !== p && (m(u), n(u)), x(!0);
2043
+ }
2044
+ return d.addEventListener("click", () => l.hidden ? f() : x(!0)), d.addEventListener("keydown", (u) => {
2045
+ (u.key === "ArrowDown" || u.key === "ArrowUp") && (u.preventDefault(), f());
2046
+ }), l.addEventListener("keydown", (u) => {
2047
+ const y = c.indexOf(u.target);
2048
+ let k = null;
2049
+ if (u.key === "ArrowDown") k = Math.min(c.length - 1, y + 1);
2050
+ else if (u.key === "ArrowUp") k = Math.max(0, y - 1);
2051
+ else if (u.key === "Home") k = 0;
2052
+ else if (u.key === "End") k = c.length - 1;
2053
+ else if (u.key === "Enter" || u.key === " ") {
2054
+ u.preventDefault(), g(u.target.dataset.value);
2055
+ return;
2056
+ } else if (u.key === "Escape") {
2057
+ u.preventDefault(), u.stopPropagation(), x(!0);
2058
+ return;
2059
+ } else if (u.key === "Tab") {
2060
+ x();
2061
+ return;
2062
+ }
2063
+ k !== null && (u.preventDefault(), c[k].focus());
2064
+ }), m(o ? null : e[0].value), { el: r, setValue: m };
2065
+ }
2066
+ const I = 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif', ve = 'Georgia, "Times New Roman", serif', Be = [
2067
+ { value: "neutral", name: "Neutral", fonts: "Inter · Inter", heading: `"Inter", ${I}`, body: `"Inter", ${I}`, load: ["Inter:wght@400;500;600;700"] },
2068
+ { value: "modern", name: "Modern", fonts: "Geist · Geist", heading: `"Geist", ${I}`, body: `"Geist", ${I}`, load: ["Geist:wght@400;500;600;700"] },
2069
+ { value: "editorial", name: "Editorial", fonts: "Fraunces · Inter", heading: `"Fraunces", ${ve}`, body: `"Inter", ${I}`, load: ["Fraunces:wght@400;600;700", "Inter:wght@400;500;600;700"] },
2070
+ { value: "technical", name: "Technical", fonts: "Space Grotesk · DM Sans", heading: `"Space Grotesk", ${I}`, body: `"DM Sans", ${I}`, load: ["Space+Grotesk:wght@500;600;700", "DM+Sans:wght@400;500;600;700"] },
2071
+ { value: "classic", name: "Classic", fonts: "Playfair · DM Sans", heading: `"Playfair Display", ${ve}`, body: `"DM Sans", ${I}`, load: ["Playfair+Display:wght@600;700", "DM+Sans:wght@400;500;600;700"] },
2072
+ { value: "plex", name: "Plex", fonts: "Plex Serif · Plex Sans", heading: `"IBM Plex Serif", ${ve}`, body: `"IBM Plex Sans", ${I}`, load: ["IBM+Plex+Serif:wght@500;600;700", "IBM+Plex+Sans:wght@400;500;600"] },
2073
+ { value: "elegant", name: "Elegant", fonts: "Instrument Serif · Inter", heading: `"Instrument Serif", ${ve}`, body: `"Inter", ${I}`, load: ["Instrument+Serif", "Inter:wght@400;500;600;700"] },
2074
+ { value: "friendly", name: "Friendly", fonts: "Manrope · Manrope", heading: `"Manrope", ${I}`, body: `"Manrope", ${I}`, load: ["Manrope:wght@400;500;600;700"] }
2075
+ ], Ve = { compact: 1.125, balanced: 1.2, expressive: 1.333 }, Ut = { xs: -2, sm: -1, base: 0, lg: 1, xl: 2, "2xl": 3, "3xl": 4 }, qt = /* @__PURE__ */ new Set();
2076
+ function Ht(t) {
2077
+ const e = t.filter((o) => !qt.has(o));
2078
+ if (!e.length) return;
2079
+ e.forEach((o) => qt.add(o));
2080
+ const n = document.createElement("link");
2081
+ n.rel = "stylesheet", n.href = `https://fonts.googleapis.com/css2?${e.map((o) => `family=${o}`).join("&")}&display=swap`, n.dataset.twiddle = "fonts", document.head.appendChild(n);
2082
+ }
2083
+ const Fo = {
2084
+ id: "typography",
2085
+ title: "Typography",
2086
+ hint: "Font pairing · base size · scale",
2087
+ uses: ["--twd-font-heading", "--twd-font-body", ...Object.keys(Ut).map((t) => `--twd-text-${t}`)],
2088
+ tokens(t) {
2089
+ const e = t.typography || {}, n = {}, o = Be.find((a) => a.value === e.font);
2090
+ if (o && (Ht(o.load), n["--twd-font-heading"] = o.heading, n["--twd-font-body"] = o.body), e.base || e.scale) {
2091
+ const a = Number(e.base) || 16, s = Ve[e.scale] || Ve.balanced;
2092
+ Object.entries(Ut).forEach(([r, d]) => {
2093
+ n[`--twd-text-${r}`] = `${+(a * s ** d / 16).toFixed(3)}rem`;
2094
+ });
2095
+ }
2096
+ return n;
2097
+ },
2098
+ parts: {
2099
+ fonts(t, e, { label: n = "Fonts" } = {}) {
2100
+ const o = B(n), a = Po({
2101
+ label: "Font pairing",
2102
+ options: Be.map((r) => ({ value: r.value, name: r.name, fonts: r.fonts, preview: r.heading })),
2103
+ placeholder: { name: "Default", fonts: "Prototype fonts", preview: I },
2104
+ // Load every pairing's fonts when the list opens, so the "Aa" previews are real.
2105
+ onOpen: () => Ht([...new Set(Be.flatMap((r) => r.load))]),
2106
+ onSelect: (r) => e.setSection("typography", { font: r })
2107
+ });
2108
+ o.append(a.el);
2109
+ const s = () => a.setValue((e.getState().typography || {}).font || null);
2110
+ s(), e.onChange(s), t.append(o.el);
2111
+ },
2112
+ base(t, e, { label: n = "Base size" } = {}) {
2113
+ t.append(le(e, {
2114
+ section: "typography",
2115
+ key: "base",
2116
+ label: n,
2117
+ fallback: "16",
2118
+ options: ["14", "15", "16"].map((o) => ({ value: o, label: o })),
2119
+ valueText: (o) => `${o}px`
2120
+ }));
2121
+ },
2122
+ scale(t, e, { label: n = "Scale" } = {}) {
2123
+ t.append(le(e, {
2124
+ section: "typography",
2125
+ key: "scale",
2126
+ label: n,
2127
+ fallback: "balanced",
2128
+ options: [{ value: "compact", label: "Compact" }, { value: "balanced", label: "Balanced" }, { value: "expressive", label: "Expressive" }],
2129
+ valueText: (o) => String(Ve[o])
2130
+ }));
2131
+ }
2132
+ },
2133
+ render(t, e) {
2134
+ Object.values(this.parts).forEach((n) => n(t, e));
2135
+ }
2136
+ }, Oo = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 6l4 4M4 20l1-4 9.5-9.5a2.1 2.1 0 013 3L8 19l-4 1z"/></svg>', Ro = [
2137
+ [50, "white", 10],
2138
+ [100, "white", 20],
2139
+ [200, "white", 36],
2140
+ [300, "white", 56],
2141
+ [400, "white", 78],
2142
+ [500, null, 100],
2143
+ [600, "black", 86],
2144
+ [700, "black", 72],
2145
+ [800, "black", 58],
2146
+ [900, "black", 45],
2147
+ [950, "black", 30]
2148
+ ], Ue = (t) => {
2149
+ let e = String(t).trim().replace(/^#/, "");
2150
+ return /^[0-9a-f]{3}$/i.test(e) && (e = e.split("").map((n) => n + n).join("")), /^[0-9a-f]{6}$/i.test(e) ? `#${e.toUpperCase()}` : null;
2151
+ };
2152
+ function No({ label: t, onInput: e }) {
2153
+ const n = document.createElement("div");
2154
+ n.className = "colour";
2155
+ const o = document.createElement("div");
2156
+ o.className = "colour-field";
2157
+ const a = document.createElement("input");
2158
+ a.type = "color", a.className = "colour-picker", a.tabIndex = -1, a.setAttribute("aria-hidden", "true");
2159
+ const s = document.createElement("button");
2160
+ s.type = "button", s.className = "colour-swatch", s.setAttribute("aria-label", `${t}: open colour picker`);
2161
+ const r = document.createElement("input");
2162
+ r.type = "text", r.className = "colour-hex", r.spellcheck = !1, r.maxLength = 7, r.setAttribute("aria-label", `${t} hex value`);
2163
+ const d = document.createElement("button");
2164
+ d.type = "button", d.className = "colour-pipette", d.innerHTML = Oo, d.title = "Pick a colour from the screen", d.setAttribute("aria-label", "Pick a colour from the screen"), o.append(a, s, r, d);
2165
+ const l = document.createElement("div");
2166
+ l.className = "ramp", l.setAttribute("aria-hidden", "true"), l.innerHTML = Ro.map(([f, x, b]) => {
2167
+ const g = x ? `color-mix(in oklch, var(--brand) ${b}%, ${x})` : "var(--brand)";
2168
+ return `<span title="${f}" style="background:${g}"></span>`;
2169
+ }).join(""), n.append(o, l);
2170
+ let c = "#000000";
2171
+ const p = (f) => {
2172
+ c = f, a.value = f.toLowerCase(), s.style.background = f, n.getRootNode().activeElement !== r && (r.value = f), n.style.setProperty("--brand", f);
2173
+ }, m = (f) => {
2174
+ f !== c && (p(f), e(f));
2175
+ };
2176
+ s.addEventListener("click", () => {
2177
+ if (a.showPicker)
2178
+ try {
2179
+ a.showPicker();
2180
+ return;
2181
+ } catch {
2182
+ }
2183
+ a.click();
2184
+ }), a.addEventListener("input", () => m(a.value.toUpperCase())), r.addEventListener("input", () => {
2185
+ const f = Ue(r.value);
2186
+ f && m(f);
2187
+ });
2188
+ const v = () => {
2189
+ r.value = c;
2190
+ };
2191
+ return r.addEventListener("blur", v), r.addEventListener("keydown", (f) => {
2192
+ f.key === "Enter" && (v(), r.select());
2193
+ }), d.addEventListener("click", async () => {
2194
+ if ("EyeDropper" in window)
2195
+ try {
2196
+ const { sRGBHex: f } = await new window.EyeDropper().open(), x = Ue(f);
2197
+ x && m(x);
2198
+ } catch {
2199
+ }
2200
+ else
2201
+ s.click();
2202
+ }), { el: n, setValue: (f) => p(Ue(f) || c) };
2203
+ }
2204
+ function jo({ label: t, options: e, onSelect: n }) {
2205
+ const o = document.createElement("div");
2206
+ o.className = "swatches", o.setAttribute("role", "radiogroup"), o.setAttribute("aria-label", t);
2207
+ const a = e.map((l) => {
2208
+ const c = document.createElement("button");
2209
+ return c.type = "button", c.className = "swatch", c.setAttribute("role", "radio"), c.dataset.value = l.value, c.innerHTML = `<span class="swatch-ring"><span class="swatch-dot" style="background:${l.colour}"></span></span><span class="swatch-label">${l.label}</span>`, c.addEventListener("click", () => d(l.value)), c;
2210
+ });
2211
+ o.append(...a);
2212
+ let s = null;
2213
+ function r(l) {
2214
+ s = l, a.forEach((p) => p.setAttribute("aria-checked", String(p.dataset.value === l)));
2215
+ const c = Math.max(0, a.findIndex((p) => p.dataset.value === l));
2216
+ a.forEach((p, m) => {
2217
+ p.tabIndex = m === c ? 0 : -1;
2218
+ });
2219
+ }
2220
+ function d(l, c) {
2221
+ l !== s && (r(l), n(l)), c && a.find((p) => p.dataset.value === l).focus();
2222
+ }
2223
+ return o.addEventListener("keydown", (l) => {
2224
+ const c = a.indexOf(l.target.closest(".swatch"));
2225
+ if (c < 0) return;
2226
+ const p = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 }[l.key];
2227
+ let m;
2228
+ if (p) m = (c + p + a.length) % a.length;
2229
+ else if (l.key === "Home") m = 0;
2230
+ else if (l.key === "End") m = a.length - 1;
2231
+ else return;
2232
+ l.preventDefault(), d(a[m].dataset.value, !0);
2233
+ }), r(null), { el: o, setValue: r };
2234
+ }
2235
+ const _t = "#4F46E5", zo = "#A0743C", Io = [
2236
+ // Figma Swatch row sample colours
2237
+ { value: "pure", label: "Pure", colour: "#8E8E93" },
2238
+ { value: "subtle", label: "Subtle", colour: "#8A89A6" },
2239
+ { value: "warm", label: "Warm", colour: "#9C9389" }
2240
+ ], Gt = ["bg", "surface", "surface-raised", "text", "text-muted", "border"], Do = {
2241
+ // proposal
2242
+ bg: "#0F0F11",
2243
+ surface: "#18181B",
2244
+ "surface-raised": "#1F1F23",
2245
+ text: "#F4F4F5",
2246
+ "text-muted": "#A1A1AA",
2247
+ border: "#2E2E33"
2248
+ }, Bo = { bg: 4, surface: 2, "surface-raised": 2, text: 5, "text-muted": 12, border: 10 }, Kt = (t) => /^#[0-9a-f]{6}$/i.test(t || "");
2249
+ function Vo(t) {
2250
+ const e = (l) => (l /= 255, l <= 0.03928 ? l / 12.92 : ((l + 0.055) / 1.055) ** 2.4), [n, o, a] = [1, 3, 5].map((l) => e(parseInt(t.slice(l, l + 2), 16))), s = 0.2126 * n + 0.7152 * o + 0.0722 * a, r = 1.05 / (s + 0.05), d = (s + 0.05) / (98e-4 + 0.05);
2251
+ return r >= d ? "#FFFFFF" : "#18181B";
2252
+ }
2253
+ const Uo = {
2254
+ id: "colour",
2255
+ title: "Colour",
2256
+ hint: "Brand colour · neutral tint · mode",
2257
+ uses: ["--twd-primary", "--twd-primary-hover", "--twd-on-primary", "--twd-focus", ...Gt.map((t) => `--twd-${t}`)],
2258
+ tokens(t) {
2259
+ const e = t.colour || {}, n = {}, o = e.mode === "dark";
2260
+ Kt(e.brand) && (n["--twd-primary"] = e.brand, n["--twd-primary-hover"] = o ? `color-mix(in oklch, ${e.brand} 85%, white)` : `color-mix(in oklch, ${e.brand} 86%, black)`, n["--twd-on-primary"] = Vo(e.brand), n["--twd-focus"] = e.brand);
2261
+ const a = e.tint === "subtle" ? Kt(e.brand) ? e.brand : F("--twd-primary") || _t : e.tint === "warm" ? zo : null;
2262
+ return (o || a) && Gt.forEach((s) => {
2263
+ const r = o ? Do[s] : F(`--twd-${s}`);
2264
+ r && (n[`--twd-${s}`] = a ? `color-mix(in oklch, ${r} ${100 - Bo[s]}%, ${a})` : r);
2265
+ }), n;
2266
+ },
2267
+ parts: {
2268
+ brand(t, e, { label: n = "Brand" } = {}) {
2269
+ const o = B(n), a = No({ label: "Brand colour", onInput: (r) => e.setSection("colour", { brand: r }) });
2270
+ o.append(a.el);
2271
+ const s = () => a.setValue((e.getState().colour || {}).brand || (e.readToken || F)("--twd-primary") || _t);
2272
+ s(), e.onChange(s), t.append(o.el);
2273
+ },
2274
+ neutral(t, e, { label: n = "Neutral" } = {}) {
2275
+ const o = B(n), a = jo({ label: "Neutral tint", options: Io, onSelect: (r) => e.setSection("colour", { tint: r }) });
2276
+ o.append(a.el);
2277
+ const s = () => a.setValue((e.getState().colour || {}).tint || "pure");
2278
+ s(), e.onChange(s), t.append(o.el);
2279
+ },
2280
+ mode(t, e, { label: n = "Mode" } = {}) {
2281
+ t.append(le(e, {
2282
+ section: "colour",
2283
+ key: "mode",
2284
+ label: n,
2285
+ fallback: "light",
2286
+ options: [{ value: "light", label: "Light" }, { value: "dark", label: "Dark" }]
2287
+ }));
2288
+ }
2289
+ },
2290
+ render(t, e) {
2291
+ Object.values(this.parts).forEach((n) => n(t, e));
2292
+ }
2293
+ }, qo = { small: { height: 0.8, pad: 0.75 }, large: { height: 1.2, pad: 1.25 } }, Ho = hn({
2294
+ id: "buttons",
2295
+ title: "Buttons",
2296
+ hint: "Corners · size",
2297
+ token: "--twd-radius-control",
2298
+ uses: ["--twd-control-height", "--twd-control-pad-x"],
2299
+ max: 24,
2300
+ // slider range in px; "Pill" sits at the end and reads "full"
2301
+ presets: [
2302
+ { value: "square", label: "Square", radius: 0 },
2303
+ // proposal
2304
+ { value: "soft", label: "Soft", radius: 4 },
2305
+ // proposal
2306
+ { value: "rounded", label: "Rounded", radius: 8 },
2307
+ // Figma: "8px"
2308
+ { value: "pill", label: "Pill", radius: 999 }
2309
+ ],
2310
+ extraTokens(t) {
2311
+ const e = qo[t.size];
2312
+ if (!e) return {};
2313
+ const n = {}, o = Se(F("--twd-control-height"), e.height), a = Se(F("--twd-control-pad-x"), e.pad);
2314
+ return o && (n["--twd-control-height"] = o), a && (n["--twd-control-pad-x"] = a), n;
2315
+ },
2316
+ extra: [
2317
+ {
2318
+ key: "size",
2319
+ label: "Size",
2320
+ fallback: "medium",
2321
+ options: [{ value: "small", label: "Small" }, { value: "medium", label: "Medium" }, { value: "large", label: "Large" }]
2322
+ }
2323
+ ]
2324
+ }), _o = hn({
2325
+ id: "cards",
2326
+ title: "Cards",
2327
+ hint: "Corners · border",
2328
+ token: "--twd-radius-container",
2329
+ uses: ["--twd-border-width-container"],
2330
+ max: 32,
2331
+ presets: [
2332
+ { value: "square", label: "Square", radius: 0 },
2333
+ // proposal
2334
+ { value: "soft", label: "Soft", radius: 6 },
2335
+ // proposal
2336
+ { value: "rounded", label: "Rounded", radius: 12 },
2337
+ // = tokens.css default
2338
+ { value: "large", label: "Large", radius: 20 }
2339
+ // proposal (cards don't go fully pill)
2340
+ ],
2341
+ extraTokens(t) {
2342
+ if (t.border === "none") return { "--twd-border-width-container": "0px" };
2343
+ if (t.border === "thick") {
2344
+ const e = Se(F("--twd-border-width-container"), 2);
2345
+ return e ? { "--twd-border-width-container": e } : {};
2346
+ }
2347
+ return {};
2348
+ },
2349
+ extra: [
2350
+ {
2351
+ key: "border",
2352
+ label: "Border",
2353
+ fallback: "thin",
2354
+ options: [{ value: "none", label: "None" }, { value: "thin", label: "Thin" }, { value: "thick", label: "Thick" }]
2355
+ }
2356
+ ]
2357
+ }), Wt = [Fo, Uo, Ho, _o, bo, wo, mo], Go = ':root{--twd-font-heading: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;--twd-font-body: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;--twd-text-xs: .694rem;--twd-text-sm: .833rem;--twd-text-base: 1rem;--twd-text-lg: 1.2rem;--twd-text-xl: 1.44rem;--twd-text-2xl: 1.728rem;--twd-text-3xl: 2.074rem;--twd-leading-tight: 1.2;--twd-leading-normal: 1.5;--twd-bg: #fafafa;--twd-surface: #ffffff;--twd-surface-raised: #ffffff;--twd-text: #18181b;--twd-text-muted: #71717a;--twd-border: #e4e4e7;--twd-primary: #4f46e5;--twd-primary-hover: #4338ca;--twd-on-primary: #ffffff;--twd-focus: #6366f1;--twd-success: #16a34a;--twd-warning: #d97706;--twd-danger: #dc2626;--twd-radius-control: 8px;--twd-radius-container: 12px;--twd-space-1: 4px;--twd-space-2: 8px;--twd-space-3: 12px;--twd-space-4: 16px;--twd-space-5: 24px;--twd-space-6: 32px;--twd-space-7: 48px;--twd-space-8: 64px;--twd-control-height: 40px;--twd-control-pad-x: 16px;--twd-page-pad: var(--twd-space-5);--twd-section-gap: var(--twd-space-6);--twd-card-pad: var(--twd-space-5);--twd-card-gap: var(--twd-space-2);--twd-control-gap: var(--twd-space-3);--twd-border-width: 1px;--twd-border-width-container: var(--twd-border-width);--twd-shadow-sm: 0 1px 2px rgb(0 0 0 / .06);--twd-shadow-md: 0 4px 12px rgb(0 0 0 / .08);--twd-shadow-lg: 0 12px 32px rgb(0 0 0 / .12)}';
2358
+ function Ko() {
2359
+ if (getComputedStyle(document.documentElement).getPropertyValue("--twd-radius-control").trim() || document.getElementById("twiddle-tokens")) return;
2360
+ const e = document.createElement("style");
2361
+ e.id = "twiddle-tokens", e.textContent = Go, document.head.prepend(e);
2362
+ }
2363
+ if (!window.Twiddle) {
2364
+ Ko(), Wt.forEach((n) => On(n.tokens)), Qt(Ke);
2365
+ let t;
2366
+ const e = () => {
2367
+ Ke(He()), t = Mo({ sections: Wt, store: Fn, view: { suspend: zn, resume: In } });
2368
+ };
2369
+ document.readyState === "loading" ? document.addEventListener("DOMContentLoaded", e) : e(), window.Twiddle = {
2370
+ open: () => t && t.open(),
2371
+ close: () => t && t.close(),
2372
+ toggle: () => t && t.toggle(),
2373
+ reset: () => Xt(),
2374
+ undo: () => Jt(),
2375
+ redo: () => Zt(),
2376
+ pick: () => t && t.pick(),
2377
+ prompt: () => t ? t.prompt() : "",
2378
+ getState: He
2379
+ };
2380
+ }
2381
+ const Wo = window.Twiddle;
2382
+ export {
2383
+ Wo as Twiddle
2384
+ };