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.