PageSourceSearch

https://cristiangabriel.dev/tweaks-panel.compiled.js?v=a56f1d04

js cristiangabriel.dev collected 2026-10-02 01:52:21 UTC 28,837 bytes, 718 lines download raw bytes

1(function () {
2  // tweaks-panel.jsx
3  // Reusable Tweaks shell + form-control helpers.
4  //
5  // Owns the host protocol (listens for __activate_edit_mode / __deactivate_edit_mode,
6  // posts __edit_mode_available / __edit_mode_set_keys / __edit_mode_dismissed) so
7  // individual prototypes don't re-roll it. Ships a consistent set of controls so you
8  // don't hand-draw <input type="range">, segmented radios, steppers, etc.
9  //
10  // Usage (in an HTML file that loads React + Babel):
11  //
12  //   const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
13  //     "primaryColor": "#D97757",
14  //     "palette": ["#D97757", "#29261b", "#f6f4ef"],
15  //     "fontSize": 16,
16  //     "density": "regular",
17  //     "dark": false
18  //   }/*EDITMODE-END*/;
19  //
20  //   function App() {
21  //     const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
22  //     return (
23  //       <div style={{ fontSize: t.fontSize, color: t.primaryColor }}>
24  //         Hello
25  //         <TweaksPanel>
26  //           <TweakSection label="Typography" />
27  //           <TweakSlider label="Font size" value={t.fontSize} min={10} max={32} unit="px"
28  //                        onChange={(v) => setTweak('fontSize', v)} />
29  //           <TweakRadio  label="Density" value={t.density}
30  //                        options={['compact', 'regular', 'comfy']}
31  //                        onChange={(v) => setTweak('density', v)} />
32  //           <TweakSection label="Theme" />
33  //           <TweakColor  label="Primary" value={t.primaryColor}
34  //                        options={['#D97757', '#2A6FDB', '#1F8A5B', '#7A5AE0']}
35  //                        onChange={(v) => setTweak('primaryColor', v)} />
36  //           <TweakColor  label="Palette" value={t.palette}
37  //                        options={[['#D97757', '#29261b', '#f6f4ef'],
38  //                                  ['#475569', '#0f172a', '#f1f5f9']]}
39  //                        onChange={(v) => setTweak('palette', v)} />
40  //           <TweakToggle label="Dark mode" value={t.dark}
41  //                        onChange={(v) => setTweak('dark', v)} />
42  //         </TweaksPanel>
43  //       </div>
44  //     );
45  //   }
46  //
47  // ─────────────────────────────────────────────────────────────────────────────
48
49  const __TWEAKS_STYLE = `
50  .twk-panel{position:fixed;right:16px;bottom:16px;z-index:2147483646;width:280px;
51    max-height:calc(100vh - 32px);display:flex;flex-direction:column;
52    transform:scale(var(--dc-inv-zoom,1));transform-origin:bottom right;
53    background:rgba(250,249,247,.78);color:#29261b;
54    -webkit-backdrop-filter:blur(24px) saturate(160%);backdrop-filter:blur(24px) saturate(160%);
55    border:.5px solid rgba(255,255,255,.6);border-radius:14px;
56    box-shadow:0 1px 0 rgba(255,255,255,.5) inset,0 12px 40px rgba(0,0,0,.18);
57    font:11.5px/1.4 ui-sans-serif,system-ui,-apple-system,sans-serif;overflow:hidden}
58  .twk-hd{display:flex;align-items:center;justify-content:space-between;
59    padding:10px 8px 10px 14px;cursor:move;user-select:none}
60  .twk-hd b{font-size:12px;font-weight:600;letter-spacing:.01em}
61  .twk-x{appearance:none;border:0;background:transparent;color:rgba(41,38,27,.55);
62    width:22px;height:22px;border-radius:6px;cursor:default;font-size:13px;line-height:1}
63  .twk-x:hover{background:rgba(0,0,0,.06);color:#29261b}
64  .twk-body{padding:2px 14px 14px;display:flex;flex-direction:column;gap:10px;
65    overflow-y:auto;overflow-x:hidden;min-height:0;
66    scrollbar-width:thin;scrollbar-color:rgba(0,0,0,.15) transparent}
67  .twk-body::-webkit-scrollbar{width:8px}
68  .twk-body::-webkit-scrollbar-track{background:transparent;margin:2px}
69  .twk-body::-webkit-scrollbar-thumb{background:rgba(0,0,0,.15);border-radius:4px;
70    border:2px solid transparent;background-clip:content-box}
71  .twk-body::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.25);
72    border:2px solid transparent;background-clip:content-box}
73  .twk-row{display:flex;flex-direction:column;gap:5px}
74  .twk-row-h{flex-direction:row;align-items:center;justify-content:space-between;gap:10px}
75  .twk-lbl{display:flex;justify-content:space-between;align-items:baseline;
76    color:rgba(41,38,27,.72)}
77  .twk-lbl>span:first-child{font-weight:500}
78  .twk-val{color:rgba(41,38,27,.5);font-variant-numeric:tabular-nums}
79
80  .twk-sect{font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;
81    color:rgba(41,38,27,.45);padding:10px 0 0}
82  .twk-sect:first-child{padding-top:0}
83
84  .twk-field{appearance:none;box-sizing:border-box;width:100%;min-width:0;height:26px;padding:0 8px;
85    border:.5px solid rgba(0,0,0,.1);border-radius:7px;
86    background:rgba(255,255,255,.6);color:inherit;font:inherit;outline:none}
87  .twk-field:focus{border-color:rgba(0,0,0,.25);background:rgba(255,255,255,.85)}
88  select.twk-field{padding-right:22px;
89    background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='rgba(0,0,0,.5)' d='M0 0h10L5 6z'/></svg>");
90    background-repeat:no-repeat;background-position:right 8px center}
91
92  .twk-slider{appearance:none;-webkit-appearance:none;width:100%;height:4px;margin:6px 0;
93    border-radius:999px;background:rgba(0,0,0,.12);outline:none}
94  .twk-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;
95    width:14px;
95height:14px;border-radius:50%;background:#fff;
96    border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default}
97  .twk-slider::-moz-range-thumb{width:14px;height:14px;border-radius:50%;
98    background:#fff;border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default}
99
100  .twk-seg{position:relative;display:flex;padding:2px;border-radius:8px;
101    background:rgba(0,0,0,.06);user-select:none}
102  .twk-seg-thumb{position:absolute;top:2px;bottom:2px;border-radius:6px;
103    background:rgba(255,255,255,.9);box-shadow:0 1px 2px rgba(0,0,0,.12);
104    transition:left .15s cubic-bezier(.3,.7,.4,1),width .15s}
105  .twk-seg.dragging .twk-seg-thumb{transition:none}
106  .twk-seg button{appearance:none;position:relative;z-index:1;flex:1;border:0;
107    background:transparent;color:inherit;font:inherit;font-weight:500;min-height:22px;
108    border-radius:6px;cursor:default;padding:4px 6px;line-height:1.2;
109    overflow-wrap:anywhere}
110
111  .twk-toggle{position:relative;width:32px;height:18px;border:0;border-radius:999px;
112    background:rgba(0,0,0,.15);transition:background .15s;cursor:default;padding:0}
113  .twk-toggle[data-on="1"]{background:#34c759}
114  .twk-toggle i{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;
115    background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .15s}
116  .twk-toggle[data-on="1"] i{transform:translateX(14px)}
117
118  .twk-num{display:flex;align-items:center;box-sizing:border-box;min-width:0;height:26px;padding:0 0 0 8px;
119    border:.5px solid rgba(0,0,0,.1);border-radius:7px;background:rgba(255,255,255,.6)}
120  .twk-num-lbl{font-weight:500;color:rgba(41,38,27,.6);cursor:ew-resize;
121    user-select:none;padding-right:8px}
122  .twk-num input{flex:1;min-width:0;height:100%;border:0;background:transparent;
123    font:inherit;font-variant-numeric:tabular-nums;text-align:right;padding:0 8px 0 0;
124    outline:none;color:inherit;-moz-appearance:textfield}
125  .twk-num input::-webkit-inner-spin-button,.twk-num input::-webkit-outer-spin-button{
126    -webkit-appearance:none;margin:0}
127  .twk-num-unit{padding-right:8px;color:rgba(41,38,27,.45)}
128
129  .twk-btn{appearance:none;height:26px;padding:0 12px;border:0;border-radius:7px;
130    background:rgba(0,0,0,.78);color:#fff;font:inherit;font-weight:500;cursor:default}
131  .twk-btn:hover{background:rgba(0,0,0,.88)}
132  .twk-btn.secondary{background:rgba(0,0,0,.06);color:inherit}
133  .twk-btn.secondary:hover{background:rgba(0,0,0,.1)}
134
135  .twk-swatch{appearance:none;-webkit-appearance:none;width:56px;height:22px;
136    border:.5px solid rgba(0,0,0,.1);border-radius:6px;padding:0;cursor:default;
137    background:transparent;flex-shrink:0}
138  .twk-swatch::-webkit-color-swatch-wrapper{padding:0}
139  .twk-swatch::-webkit-color-swatch{border:0;border-radius:5.5px}
140  .twk-swatch::-moz-color-swatch{border:0;border-radius:5.5px}
141
142  .twk-chips{display:flex;gap:6px}
143  .twk-chip{position:relative;appearance:none;flex:1;min-width:0;height:46px;
144    padding:0;border:0;border-radius:6px;overflow:hidden;cursor:default;
145    box-shadow:0 0 0 .5px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.06);
146    transition:transform .12s cubic-bezier(.3,.7,.4,1),box-shadow .12s}
147  .twk-chip:hover{transform:translateY(-1px);
148    box-shadow:0 0 0 .5px rgba(0,0,0,.18),0 4px 10px rgba(0,0,0,.12)}
149  .twk-chip[data-on="1"]{box-shadow:0 0 0 1.5px rgba(0,0,0,.85),
150    0 2px 6px rgba(0,0,0,.15)}
151  .twk-chip>span{position:absolute;top:0;bottom:0;right:0;width:34%;
152    display:flex;flex-direction:column;box-shadow:-1px 0 0 rgba(0,0,0,.1)}
153  .twk-chip>span>i{flex:1;box-shadow:0 -1px 0 rgba(0,0,0,.1)}
154  .twk-chip>span>i:first-child{box-shadow:none}
155  .twk-chip svg{position:absolute;top:6px;left:6px;width:13px;height:13px;
156    filter:drop-shadow(0 1px 1px rgba(0,0,0,.3))}
157`;
158
159  // ── useTweaks ───────────────────────────────────────────────────────────────
160  // Single source of truth for tweak values. setTweak persists via the host
161  // (__edit_mode_set_keys → host rewrites the EDITMODE block on disk).
162  function useTweaks(defaults) {
163    const [values, setValues] = React.useState(defaults);
164    // Accepts either setTweak('key', value) or setTweak({ key: value, ... }) so a
165    // useState-style call doesn't write a "[object Object]" key into the persisted
166    // JSON block.
167    const setTweak = React.useCallback((keyOrEdits, val) => {
168      const edits = typeof keyOrEdits === "object" && keyOrEdits !== null ? keyOrEdits : {
169        [keyOrEdits]: val
170      };
171      setValues(prev => ({
172        ...prev,
173        ...edits
174      }));
175      window.parent.postMessage({
176        type: "__edit_mode_set_keys",
177        edits
178      }, "*");
179      // Same-window signal so in-page listeners (deck-stage rail thumbnails)
180      // can react — the parent message only reaches the host, not peers.
181      window.dispatchEvent(new CustomEvent("tweakchange", {
182        detail: edits
183      }));
184    }, []);
185    return [values, setTweak];
186  }
187
188  // ── TweaksPanel ─────────────────────────────────────────────────────────────
189  // Floating shell. Registers the protocol listener BEFORE announcing
190  // availability — if the announce ran first, the host's activate could land
191  // before our handler exists and the toolbar toggle would silently no-op.
192  // The close button posts __edit_mode_dismissed so the host's toolbar toggle
193  // flips off in lockstep; the host echoes __deactivate_edit_mode back which
194  // is what actually hides the panel.
195  function TweaksPanel({
196    title = "Tweaks",
197    noDeckControls = false,
198    children
199  }) {
200    const [open, setOpen] = React.useState(false);
201    const dragRef = React.useRef(null);
202    // Auto-inject a rail toggle when a <deck-stage> is on the page. The
203    // toggle drives the deck's per-viewer _railVisible via window message;
204    // state is mirrored from the same localStorage key the deck reads so
205    // the control reflects reality across reloads. The mechanism is the
206    // message — authors who want custom placement can post it directly
207    // and pass noDeckControls to suppress this one.
208    const hasDeckStage = React.useMemo(() => typeof document !== "undefined" && !!document.querySelector("deck-stage"), []);
209    // deck-stage enables its rail in connectedCallback, but this panel can
210    // mount before that element has upgraded. The initial read catches the
211    // common case; the listener covers mounting first. (Older deck-stage.js
212    // copies still wait for the host's __omelette_rail_enabled postMessage —
213    // same listener handles those.)
214    const [railEnabled, setRailEnabled] = React.useState(() => hasDeckStage && !!document.querySelector("deck-stage")?._railEnabled);
215    React.useEffect(() => {
216      if (!hasDeckStage || railEnabled) return undefined;
217      const onMsg = e => {
218        if (e.data && e.data.type === "__omelette_rail_enabled") setRailEnabled(true);
219      };
220      window.addEventListener("message", onMsg);
221      return () => window.removeEventListener("message", onMsg);
222    }, [hasDeckStage, railEnabled]);
223    const [railVisible, setRailVisible] = React.useState(() => {
224      try {
225        return localStorage.getItem("deck-stage.railVisible") !== "0";
226      } catch (e) {
227        return true;
228      }
229    });
230    const toggleRail = on => {
231      setRailVisible(on);
232      window.postMessage({
233        type: "__deck_rail_visible",
234        on
235      }, "*");
236    };
237    const offsetRef = React.useRef({
238      x: 16,
239      y: 16
240    });
241    const PAD = 16;
242    const clampToViewport = React.useCallback(() => {
243      const panel = dragRef.current;
244      if (!panel) return;
245      const w = panel.offsetWidth,
246        h = panel.offsetHeight;
247      const maxRight = Math.max(PAD, window.innerWidth - w - PAD);
248      const maxBottom = Math.max(PAD, window.innerHeight - h - PAD);
249      offsetRef.current = {
250        x: Math.min(maxRight, Math.max(PAD, offsetRef.current.x)),
251        y: Math.min(maxBottom, Math.max(PAD, offsetRef.current.y))
252      };
253      panel.style.right = offsetRef.current.x + "px";
254      panel.style.bottom = offsetRef.current.y + "px";
255    }, []);
256    React.useEffect(() => {
257      if (!open) return;
258      clampToViewport();
259      if (typeof ResizeObserver === "undefined") {
260        window.addEventListener("resize", clampToViewport);
261        return () => window.removeEventListener("resize", clampToViewport);
262      }
263      const ro = new ResizeObserver(clampToViewport);
264      ro.observe(document.documentElement);
265      return () => ro.disconnect();
266    }, [open, clampToViewport]);
267    React.useEffect(() => {
268      const onMsg = e => {
269        const t = e?.data?.type;
270        if (t === "__activate_edit_mode") setOpen(true);else if (t === "__deactivate_edit_mode") setOpen(false);
271      };
272      window.addEventListener("message", onMsg);
273      window.parent.postMessage({
274        type: "__edit_mode_available"
275      }, "*");
276      return () => window.removeEventListener("message", onMsg);
277    }, []);
278    const dismiss = () => {
279      setOpen(false);
280      window.parent.postMessage({
281        type: "__edit_mode_dismissed"
282      }, "*");
283    };
284    const onDragStart = e => {
285      const panel = dragRef.current;
286      if (!panel) return;
287      const r = panel.getBoundingClientRect();
288      const sx = e.clientX,
289        sy = e.clientY;
290      const startRight = window.innerWidth - r.right;
291      const startBottom = window.innerHeight - r.bottom;
292      const move = ev => {
293        offsetRef.current = {
294          x: startRight - (ev.clientX - sx),
295          y: startBottom - (ev.clientY - sy)
296        };
297        clampToViewport();
298      };
299      const up = () => {
300        window.removeEventListener("mousemove", move);
301        window.removeEventListener("mouseup", up);
302      };
303      window.addEventListener("mousemove", move);
304      window.addEventListener("mouseup", up);
305    };
306    if (!open) return null;
307    return /*#__PURE__*/ /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("style", null, __TWEAKS_STYLE), /*#__PURE__*/React.createElement("div", {
308      ref: dragRef,
309      className: "twk-panel",
310      "data-noncommentable": "",
311      style: {
312        right: offsetRef.current.x,
313        bottom: offsetRef.current.y
314      }
315    }, /*#__PURE__*/React.createElement("div", {
316      className: "twk-hd",
317      onMouseDown: onDragStart
318    }, /*#__PURE__*/React.createElement("b", null, title), /*#__PURE__*/React.createElement("button", {
319      className: "twk-x",
320      "aria-label": "Close tweaks",
321      onMouseDown: e => e.stopPropagation(),
322      onClick: dismiss
323    }, "\u2715")), /*#__PURE__*/React.createElement("div", {
324      className: "twk-body"
325    }, children, hasDeckStage && railEnabled && !noDeckControls &&
326    /*#__PURE__*/
327    /*#__PURE__*/
328    React.createElement(TweakSection, {
329      label: "Deck"
330    }, /*#__PURE__*/React.createElement(TweakToggle, {
331      label: "Thumbnail rail",
332      value: railVisible,
333      onChange: toggleRail
334    })))));
335  }
336
337  // ── Layout helpers ──────────────────────────────────────────────────────────
338
339  function TweakSection({
340    label,
341    children
342  }) {
343    return /*#__PURE__*/ /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
344      className: "twk-sect"
345    }, label), children);
346  }
347  function TweakRow({
348    label,
349    value,
350    children,
351    inline = false
352  }) {
353    return /*#__PURE__*/ /*#__PURE__*/React.createElement("div", {
354      className: inline ? "twk-row twk-row-h" : "twk-row"
355    }, /*#__PURE__*/React.createElement("div", {
356      className: "twk-lbl"
357    }, /*#__PURE__*/React.createElement("span", null, label), value != null &&
358    /*#__PURE__*/
359    /*#__PURE__*/
360    React.createElement("span", {
361      className: "twk-val"
362    }, value)), children);
363  }
364
365  // ── Controls ────────────────────────────────────────────────────────────────
366
367  function TweakSlider({
368    label,
369    value,
370    min = 0,
371    max = 100,
372    step = 1,
373    unit = "",
374    onChange
375  }) {
376    return /*#__PURE__*/ /*#__PURE__*/React.createElement(TweakRow, {
377      label: label,
378      value: `${value}${unit}`
379    }, /*#__PURE__*/React.createElement("input", {
380      type: "range",
381      className: "twk-slider",
382      min: min,
383      max: max,
384      step: step,
385      value: value,
386      onChange: e => onChange(Number(e.target.value))
387    }));
388  }
389  function TweakToggle({
390    label,
391    value,
392    onChange
393  }) {
394    return /*#__PURE__*/ /*#__PURE__*/React.createElement("div", {
395      className: "twk-row twk-row-h"
396    }, /*#__PURE__*/React.createElement("div", {
397      className: "twk-lbl"
398    }, /*#__PURE__*/React.createElement("span", null, label)), /*#__PURE__*/React.createElement("button", {
399      type: "button",
400      className: "twk-toggle",
401      "data-on": value ? "1" : "0",
402      role: "switch",
403      "aria-checked": !!value,
404      onClick: () =>
404 onChange(!value)
405    }, /*#__PURE__*/React.createElement("i", null)));
406  }
407  function TweakRadio({
408    label,
409    value,
410    options,
411    onChange
412  }) {
413    const trackRef = React.useRef(null);
414    const [dragging, setDragging] = React.useState(false);
415    // The active value is read by pointer-move handlers attached for the lifetime
416    // of a drag — ref it so a stale closure doesn't fire onChange for every move.
417    const valueRef = React.useRef(value);
418    valueRef.current = value;
419
420    // Segments wrap mid-word once per-segment width runs out. The track is
421    // ~248px (280 panel − 28 body pad − 4 seg pad), each button loses 12px
422    // to its own padding, and 11.5px system-ui averages ~6.3px/char — so 2
423    // options fit ~16 chars each, 3 fit ~10. Past that (or >3 options), fall
424    // back to a dropdown rather than wrap.
425    const labelLen = o => String(typeof o === "object" ? o.label : o).length;
426    const maxLen = options.reduce((m, o) => Math.max(m, labelLen(o)), 0);
427    const fitsAsSegments = maxLen <= ({
428      2: 16,
429      3: 10
430    }[options.length] ?? 0);
431    if (!fitsAsSegments) {
432      // <select> emits strings — map back to the original option value so the
433      // fallback stays type-preserving (numbers, booleans) like the segment path.
434      const resolve = s => {
435        const m = options.find(o => String(typeof o === "object" ? o.value : o) === s);
436        return m === undefined ? s : typeof m === "object" ? m.value : m;
437      };
438      return /*#__PURE__*/ /*#__PURE__*/React.createElement(TweakSelect, {
439        label: label,
440        value: value,
441        options: options,
442        onChange: s => onChange(resolve(s))
443      });
444    }
445    const opts = options.map(o => typeof o === "object" ? o : {
446      value: o,
447      label: o
448    });
449    const idx = Math.max(0, opts.findIndex(o => o.value === value));
450    const n = opts.length;
451    const segAt = clientX => {
452      const r = trackRef.current.getBoundingClientRect();
453      const inner = r.width - 4;
454      const i = Math.floor((clientX - r.left - 2) / inner * n);
455      return opts[Math.max(0, Math.min(n - 1, i))].value;
456    };
457    const onPointerDown = e => {
458      setDragging(true);
459      const v0 = segAt(e.clientX);
460      if (v0 !== valueRef.current) onChange(v0);
461      const move = ev => {
462        if (!trackRef.current) return;
463        const v = segAt(ev.clientX);
464        if (v !== valueRef.current) onChange(v);
465      };
466      const up = () => {
467        setDragging(false);
468        window.removeEventListener("pointermove", move);
469        window.removeEventListener("pointerup", up);
470      };
471      window.addEventListener("pointermove", move);
472      window.addEventListener("pointerup", up);
473    };
474    return /*#__PURE__*/ /*#__PURE__*/React.createElement(TweakRow, {
475      label: label
476    }, /*#__PURE__*/React.createElement("div", {
477      ref: trackRef,
478      role: "radiogroup",
479      onPointerDown: onPointerDown,
480      className: dragging ? "twk-seg dragging" : "twk-seg"
481    }, /*#__PURE__*/React.createElement("div", {
482      className: "twk-seg-thumb",
483      style: {
484        left: `calc(2px + ${idx} * (100% - 4px) / ${n})`,
485        width: `calc((100% - 4px) / ${n})`
486      }
487    }), opts.map(o =>
488    /*#__PURE__*/
489    /*#__PURE__*/
490    React.createElement("button", {
491      key: o.value,
492      type: "button",
493      role: "radio",
494      "aria-checked": o.value === value
495    }, o.label))));
496  }
497  function TweakSelect({
498    label,
499    value,
500    options,
501    onChange
502  }) {
503    return /*#__PURE__*/ /*#__PURE__*/React.createElement(TweakRow, {
504      label: label
505    }, /*#__PURE__*/React.createElement("select", {
506      className: "twk-field",
507      value: value,
508      onChange: e => onChange(e.target.value)
509    }, options.map(o => {
510      const v = typeof o === "object" ? o.value : o;
511      const l = typeof o === "object" ? o.label : o;
512      return /*#__PURE__*/ /*#__PURE__*/React.createElement("option", {
513        key: v,
514        value: v
515      }, l);
516    })));
517  }
518  function TweakText({
519    label,
520    value,
521    placeholder,
522    onChange
523  }) {
524    return /*#__PURE__*/ /*#__PURE__*/React.createElement(TweakRow, {
525      label: label
526    }, /*#__PURE__*/React.createElement("input", {
527      className: "twk-field",
528      type: "text",
529      value: value,
530      placeholder: placeholder,
531      onChange: e => onChange(e.target.value)
532    }));
533  }
534  function TweakNumber({
535    label,
536    value,
537    min,
538    max,
539    step = 1,
540    unit = "",
541    onChange
542  }) {
543    const clamp = n => {
544      if (min != null && n < min) return min;
545      if (max != null && n > max) return max;
546      return n;
547    };
548    const startRef = React.useRef({
549      x: 0,
550      val: 0
551    });
552    const onScrubStart = e => {
553      e.preventDefault();
554      startRef.current = {
555        x: e.clientX,
556        val: value
557      };
558      const decimals = (String(step).split(".")[1] || "").length;
559      const move = ev => {
560        const dx = ev.clientX - startRef.current.x;
561        const raw = startRef.current.val + dx * step;
562        const snapped = Math.round(raw / step) * step;
563        onChange(clamp(Number(snapped.toFixed(decimals))));
564      };
565      const up = () => {
566        window.removeEventListener("pointermove", move);
567        window.removeEventListener("pointerup", up);
568      };
569      window.addEventListener("pointermove", move);
570      window.addEventListener("pointerup", up);
571    };
572    return /*#__PURE__*/ /*#__PURE__*/React.createElement("div", {
573      className: "twk-num"
574    }, /*#__PURE__*/React.createElement("span", {
575      className: "twk-num-lbl",
576      onPointerDown: onScrubStart
577    }, label), /*#__PURE__*/React.createElement("input", {
578      type: "number",
579      value: value,
580      min: min,
581      max: max,
582      step: step,
583      onChange: e => onChange(clamp(Number(e.target.value)))
584    }), unit &&
585    /*#__PURE__*/
586    /*#__PURE__*/
587    React.createElement("span", {
588      className: "twk-num-unit"
589    }, unit));
590  }
591
592  // Relative-luminance contrast pick — checkmarks drawn over a swatch need to
593  // read on both #111 and #fafafa without per-option configuration. Hex input
594  // only (#rgb / #rrggbb); named or rgb()/hsl() colors fall through to "light".
595  function __twkIsLight(hex) {
596    const h = String(hex).replace("#", "");
597    const x = h.length === 3 ? h.replace(/./g, c => c + c) : h.padEnd(6, "0");
598    const n = parseInt(x.slice(0, 6), 16);
599    if (Number.isNaN(n)) return true;
600    const r = n >> 16 & 255,
601      g = n >> 8 & 255,
602      b = n & 255;
603    return r * 299 + g * 587 + b * 114 > 148000;
604  }
605  const __TwkCheck = ({
606    light
607  }) =>
608  /*#__PURE__*/
609  /*#__PURE__*/
610  React.createElement("svg", {
611    viewBox: "0 0 14 14",
612    "aria-hidden": "true"
613  }, /*#__PURE__*/React.createElement("path", {
614    d: "M3 7.2 5.8 10 11 4.2",
615    fill: "none",
616    strokeWidth: "2.2",
617    strokeLinecap: "round",
618    strokeLinejoin: "round",
619    stroke: light ? "rgba(0,0,0,.78)" : "#fff"
620  }));
621
622  // TweakColor — curated color/palette picker. Each option is either a single
623  // hex string or an array of 1-5 hex strings; the card adapts — a lone color
624  // renders solid, a palette renders colors[0] as the hero (left ~2/3) with the
625  // rest stacked in a sharp column on the right. onChange emits the
626  // option in the shape it was passed (string stays string, array stays array).
627  // Without options it falls back to the native color input for back-compat.
628  function TweakColor({
629    label,
630    value,
631    options,
632    onChange
633  }) {
634    if (!options || !options.length) {
635      return /*#__PURE__*/ /*#__PURE__*/React.createElement("div", {
636        className: "twk-row twk-row-h"
637      }, /*#__PURE__*/React.createElement("div", {
638        className: "twk-lbl"
639      }, /*#__PURE__*/React.createElement("span", null, label)), /*#__PURE__*/React.createElement("input", {
640        type: "color",
641        className: "twk-swatch",
642        value: value,
643        onChange: e => onChange(e.target.value)
644      }));
645    }
646    // Native <input type=color> emits lowercase hex per the HTML spec, so
647    // compare case-insensitively. String() guards JSON.stringify(undefined),
648    // which returns the primitive undefined (no .toLowerCase).
649    const key = o => String(JSON.stringify(o)).toLowerCase();
650    const cur = key(value);
651    return /*#__PURE__*/ /*#__PURE__*/React.createElement(TweakRow, {
652      label: label
653    }, /*#__PURE__*/React.createElement("div", {
654      className: "twk-chips",
655      role: "radiogroup"
656    }, options.map((o, i) => {
657      const colors = Array.isArray(o) ? o : [o];
658      const [hero, ...rest] = colors;
659      const sup = rest.slice(0, 4);
660      const on = key(o) === cur;
661      return /*#__PURE__*/ /*#__PURE__*/React.createElement("button", {
662        key: i,
663        type: "button",
664        className: "twk-chip",
665        role: "radio",
666        "aria-checked": on,
667        "data-on": on ? "1" : "0",
668        "aria-label": colors.join(", "),
669        title: colors.join(" · "),
670        style: {
671          background: hero
672        },
673        onClick: () => onChange(o)
674      }, sup.length > 0 &&
675      /*#__PURE__*/
676      /*#__PURE__*/
677      React.createElement("span", null, sup.map((c, j) =>
678      /*#__PURE__*/
679      /*#__PURE__*/
680      React.createElement("i", {
681        key: j,
682        style: {
683          background: c
684        }
685      }))), on &&
686      /*#__PURE__*/
687      /*#__PURE__*/
688      React.createElement(__TwkCheck, {
689        light: __twkIsLight(hero)
690      }));
691    }
691)));
692  }
693  function TweakButton({
694    label,
695    onClick,
696    secondary = false
697  }) {
698    return /*#__PURE__*/ /*#__PURE__*/React.createElement("button", {
699      type: "button",
700      className: secondary ? "twk-btn secondary" : "twk-btn",
701      onClick: onClick
702    }, label);
703  }
704  Object.assign(window, {
705    useTweaks,
706    TweaksPanel,
707    TweakSection,
708    TweakRow,
709    TweakSlider,
710    TweakToggle,
711    TweakRadio,
712    TweakSelect,
713    TweakText,
714    TweakNumber,
715    TweakColor,
716    TweakButton
717  });
718})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.