const __TWEAKS_STYLE = `
.twk-scrim{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:2147483646;animation:twkfade .25s ease}
.twk-panel{position:fixed;top:0;right:0;height:100%;width:330px;max-width:86vw;z-index:2147483647;
display:flex;flex-direction:column;
--tk-acc:var(--tk-accent,#6fd99a);--tk-surf:rgba(16,21,28,.94);--tk-surf2:rgba(30,38,52,.7);
--tk-line:rgba(255,255,255,.12);--tk-tx:#e8eef4;--tk-tx2:#9aa7b6;
background:var(--tk-surf);color:var(--tk-tx);
-webkit-backdrop-filter:blur(22px) saturate(1.3);backdrop-filter:blur(22px) saturate(1.3);
border-left:1px solid var(--tk-line);box-shadow:-20px 0 60px -30px rgba(0,0,0,.85);
font:13px/1.45 ui-sans-serif,system-ui,-apple-system,sans-serif;animation:twkslide .28s cubic-bezier(.4,0,.2,1)}
@keyframes twkslide{from{transform:translateX(105%)}to{transform:none}}
@keyframes twkfade{from{opacity:0}to{opacity:1}}
.twk-hd{display:flex;align-items:center;justify-content:space-between;padding:18px 18px 4px}
.twk-hd b{font-size:1.15rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--tk-tx)}
.twk-x{appearance:none;border:1px solid var(--tk-line);background:transparent;color:var(--tk-tx2);
width:30px;height:30px;border-radius:7px;cursor:pointer;font-size:14px;line-height:1}
.twk-swatches{display:flex;flex-wrap:wrap;gap:5px;align-items:center;margin-left:auto;margin-right:8px;width:113px;justify-content:flex-end}
.twk-accsw{width:17px;height:17px;border-radius:4px;cursor:pointer;padding:0;
border:1.5px solid rgba(255,255,255,.25);box-shadow:inset 0 0 0 1px rgba(0,0,0,.25);transition:transform .12s,border-color .12s}
.twk-accsw:hover{transform:scale(1.16)}
.twk-accsw[data-on="1"]{border-color:#fff;box-shadow:0 0 0 2px var(--tk-surf),0 0 0 3px #fff}
.twk-x:hover{border-color:var(--tk-acc);color:var(--tk-acc)}
.twk-body{padding:6px 18px 28px;display:flex;flex-direction:column;overflow-y:auto;overflow-x:hidden;min-height:0}
.twk-body::-webkit-scrollbar{width:7px}
.twk-body::-webkit-scrollbar-thumb{background:var(--tk-line);border-radius:4px}
.twk-sect{font-family:ui-monospace,monospace;font-size:10px;font-weight:700;letter-spacing:.12em;
text-transform:uppercase;color:var(--tk-acc);padding:16px 0 4px;border-top:1px solid var(--tk-line);margin-top:12px}
.twk-sect:first-child{padding-top:2px;border-top:0;margin-top:0}
.twk-row{display:flex;flex-direction:column;gap:6px;margin:9px 0}
.twk-row-h{flex-direction:row;align-items:center;justify-content:space-between;gap:10px}
.twk-lbl{display:flex;justify-content:space-between;align-items:baseline;color:var(--tk-tx)}
.twk-lbl>span:first-child{font-weight:500}
.twk-val{color:var(--tk-tx2);font-variant-numeric:tabular-nums;font-family:ui-monospace,monospace;font-size:11px}
.twk-field{appearance:none;box-sizing:border-box;width:100%;min-width:0;height:32px;padding:0 10px;
border:1px solid var(--tk-line);border-radius:8px;background:var(--tk-surf2);color:var(--tk-tx);font:inherit;outline:none}
.twk-field:focus{border-color:var(--tk-acc)}
select.twk-field{padding-right:24px;
background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6'><path fill='%239aa7b6' d='M0 0h10L5 6z'/></svg>");
background-repeat:no-repeat;background-position:right 9px center}
.twk-slider{appearance:none;-webkit-appearance:none;width:100%;height:4px;margin:8px 0;border-radius:999px;background:var(--tk-line);outline:none}
.twk-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:15px;height:15px;border-radius:50%;background:var(--tk-acc);border:0;cursor:pointer}
.twk-slider::-moz-range-thumb{width:15px;height:15px;border-radius:50%;background:var(--tk-acc);border:0;cursor:pointer}
.twk-seg{position:relative;display:flex;padding:3px;border-radius:8px;background:var(--tk-surf2);border:1px solid var(--tk-line);user-select:none}
.twk-seg-thumb{position:absolute;top:3px;bottom:3px;border-radius:6px;background:var(--tk-acc);box-shadow:0 1px 4px rgba(0,0,0,.3);
transition:left .15s cubic-bezier(.3,.7,.4,1),width .15s}
.twk-seg.dragging .twk-seg-thumb{transition:none}
.twk-seg button{appearance:none;position:relative;z-index:1;flex:1;border:0;background:transparent;color:var(--tk-tx2);
font:inherit;font-weight:600;min-height:24px;border-radius:6px;cursor:pointer;padding:4px 6px;line-height:1.2;overflow-wrap:anywhere;font-size:11.5px}
.twk-seg button[aria-checked="true"],.twk-seg button[aria-selected="true"]{color:#0b0f14}
.twk-toggle{position:relative;width:42px;height:23px;border:1px solid var(--tk-line);border-radius:999px;
background:var(--tk-surf2);transition:background .15s,border-color .15s;cursor:pointer;padding:0}
.twk-toggle[data-on="1"]{background:color-mix(in srgb,var(--tk-acc) 30%,transparent);border-color:var(--tk-acc)}
.twk-toggle i{position:absolute;top:2px;left:2px;width:17px;height:17px;border-radius:50%;background:var(--tk-tx2);transition:transform .15s,background .15s}
.twk-toggle[data-on="1"] i{transform:translateX(19px);background:var(--tk-acc)}
.twk-num{display:flex;align-items:center;box-sizing:border-box;min-width:0;height:32px;padding:0 0 0 10px;
border:1px solid var(--tk-line);border-radius:8px;background:var(--tk-surf2)}
.twk-num-lbl{font-weight:500;color:var(--tk-tx2);cursor:ew-resize;user-select:none;padding-right:8px}
.twk-num input{flex:1;min-width:0;height:100%;border:0;background:transparent;font:inherit;font-variant-numeric:tabular-nums;
text-align:right;padding:0 8px 0 0;outline:none;color:var(--tk-tx);-moz-appearance:textfield}
.twk-num input::-webkit-inner-spin-button,.twk-num input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}
.twk-num-unit{padding-right:10px;color:var(--tk-tx2)}
.twk-btn{appearance:none;height:32px;padding:0 14px;border:1px solid var(--tk-acc);border-radius:8px;
background:color-mix(in srgb,var(--tk-acc) 16%,transparent);color:var(--tk-acc);font:inherit;font-weight:700;cursor:pointer}
.twk-btn:hover{background:var(--tk-acc);color:#0b0f14}
.twk-btn.secondary{border-color:var(--tk-line);background:var(--tk-surf2);color:var(--tk-tx)}
.twk-btn.secondary:hover{border-color:var(--tk-acc);color:var(--tk-acc);background:var(--tk-surf2)}
.twk-swatch{appearance:none;-webkit-appearance:none;width:56px;height:24px;border:1px solid var(--tk-line);border-radius:6px;padding:0;cursor:pointer;background:transparent;flex-shrink:0}
.twk-swatch::-webkit-color-swatch-wrapper{padding:0}
.twk-swatch::-webkit-color-swatch{border:0;border-radius:5px}
.twk-swatch::-moz-color-swatch{border:0;border-radius:5px}
.twk-chips{display:flex;gap:6px}
.twk-chip{position:relative;appearance:none;flex:1;min-width:0;height:44px;padding:0;border:0;border-radius:7px;overflow:hidden;cursor:pointer;
box-shadow:0 0 0 1px var(--tk-line);transition:transform .12s,box-shadow .12s}
.twk-chip:hover{transform:translateY(-1px)}
.twk-chip[data-on="1"]{box-shadow:0 0 0 2px var(--tk-acc)}
.twk-chip>span{position:absolute;top:0;bottom:0;right:0;width:34%;display:flex;flex-direction:column;box-shadow:-1px 0 0 rgba(0,0,0,.25)}
.twk-chip>span>i{flex:1;box-shadow:0 -1px 0 rgba(0,0,0,.25)}
.twk-chip>span>i:first-child{box-shadow:none}
.twk-chip svg{position:absolute;top:6px;left:6px;width:13px;height:13px;filter:drop-shadow(0 1px 1px rgba(0,0,0,.4))}
@media (prefers-reduced-motion: reduce){.twk-panel,.twk-scrim{animation:none}}
`;
function useTweaks(defaults) {
const [values, setValues] = React.useState(defaults);
const setTweak = React.useCallback((keyOrEdits, val) => {
const edits = typeof keyOrEdits === 'object' && keyOrEdits !== null
? keyOrEdits : { [keyOrEdits]: val };
setValues((prev) => ({ ...prev, ...edits }));
window.parent.postMessage({ type: '__edit_mode_set_keys', edits }, '*');
window.dispatchEvent(new CustomEvent('tweakchange', { detail: edits }));
}, []);
return [values, setTweak];
}
function TweaksPanel({ title = 'Tweaks', headerExtra = null, children }) {
const [open, setOpen] = React.useState(false);
React.useEffect(() => {
const onMsg = (e) => {
const t = e && e.data && e.data.type;
if (t === '__activate_edit_mode') setOpen(true);
else if (t === '__deactivate_edit_mode') setOpen(false);
};
window.addEventListener('message', onMsg);
window.parent.postMessage({ type: '__edit_mode_available' }, '*');
return () => window.removeEventListener('message', onMsg);
}, []);
const dismiss = React.useCallback(() => {
setOpen(false);
window.parent.postMessage({ type: '__edit_mode_dismissed' }, '*');
}, []);
React.useEffect(() => {
if (!open) return undefined;
const onKey = (e) => { if (e.key === 'Escape') dismiss(); };
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [open, dismiss]);
if (!open) return null;
return (
<>
<style>{__TWEAKS_STYLE}</style>
<div className="twk-scrim" data-noncommentable="" onClick={dismiss} />
<aside className="twk-panel" data-noncommentable="" role="dialog" aria-label={title}>
<div className="twk-hd">
<b>{title}</b>
{headerExtra}
<button className="twk-x" aria-label="Close tweaks" onClick={dismiss}>✕</button>
</div>
<div className="twk-body">
{children}
</div>
</aside>
</>
);
}
function TweakSection({ label, children }) {
return (
<>
<div className="twk-sect">{label}</div>
{children}
</>
);
}
function TweakRow({ label, value, children, inline = false }) {
return (
<div className={inline ? 'twk-row twk-row-h' : 'twk-row'}>
<div className="twk-lbl">
<span>{label}</span>
{value != null && <span className="twk-val">{value}</span>}
</div>
{children}
</div>
);
}
function TweakSlider({ label, value, min = 0, max = 100, step = 1, unit = '', onChange }) {
return (
<TweakRow label={label} value={`${value}${unit}`}>
<input type="range" className="twk-slider" min={min} max={max} step={step}
value={value} onChange={(e) => onChange(Number(e.target.value))} />
</TweakRow>
);
}
function TweakToggle({ label, value, onChange }) {
return (
<div className="twk-row twk-row-h">
<div className="twk-lbl"><span>{label}</span></div>
<button type="button" className="twk-toggle" data-on={value ? '1' : '0'}
role="switch" aria-checked={!!value}
onClick={() => onChange(!value)}><i /></button>
</div>
);
}
function TweakRadio({ label, value, options, onChange }) {
const trackRef = React.useRef(null);
const [dragging, setDragging] = React.useState(false);
const valueRef = React.useRef(value);
valueRef.current = value;
const labelLen = (o) => String(typeof o === 'object' ? o.label : o).length;
const maxLen = options.reduce((m, o) => Math.max(m, labelLen(o)), 0);
const fitsAsSegments = maxLen <= ({ 2: 16, 3: 10 }[options.length] ?? 0);
if (!fitsAsSegments) {
const resolve = (s) => {
const m = options.find((o) => String(typeof o === 'object' ? o.value : o) === s);
return m === undefined ? s : typeof m === 'object' ? m.value : m;
};
return <TweakSelect label={label} value={value} options={options}
onChange={(s) => onChange(resolve(s))} />;
}
const opts = options.map((o) => (typeof o === 'object' ? o : { value: o, label: o }));
const idx = Math.max(0, opts.findIndex((o) => o.value === value));
const n = opts.length;
const segAt = (clientX) => {
const r = trackRef.current.getBoundingClientRect();
const inner = r.width - 4;
const i = Math.floor(((clientX - r.left - 2) / inner) * n);
return opts[Math.max(0, Math.min(n - 1, i))].value;
};
const onPointerDown = (e) => {
setDragging(true);
const v0 = segAt(e.clientX);
if (v0 !== valueRef.current) onChange(v0);
const move = (ev) => {
if (!trackRef.current) return;
const v = segAt(ev.clientX);
if (v !== valueRef.current) onChange(v);
};
const up = () => {
setDragging(false);
window.removeEventListener('pointermove', move);
window.removeEventListener('pointerup', up);
};
window.addEventListener('pointermove', move);
window.addEventListener('pointerup', up);
};
return (
<TweakRow label={label}>
<div ref={trackRef} role="radiogroup" onPointerDown={onPointerDown}
className={dragging ? 'twk-seg dragging' : 'twk-seg'}>
<div className="twk-seg-thumb"
style={{ left: `calc(2px + ${idx} * (100% - 4px) / ${n})`,
width: `calc((100% - 4px) / ${n})` }} />
{opts.map((o) => (
<button key={o.value} type="button" role="radio" aria-checked={o.value === value}>
{o.label}
</button>
))}
</div>
</TweakRow>
);
}
function TweakSelect({ label, value, options, onChange }) {
return (
<TweakRow label={label}>
<select className="twk-field" value={value} onChange={(e) => onChange(e.target.value)}>
{options.map((o) => {
const v = typeof o === 'object' ? o.value : o;
const l = typeof o === 'object' ? o.label : o;
return <option key={v} value={v}>{l}</option>;
})}
</select>
</TweakRow>
);
}
function TweakText({ label, value, placeholder, onChange }) {
return (
<TweakRow label={label}>
<input className="twk-field" type="text" value={value} placeholder={placeholder}
onChange={(e) => onChange(e.target.value)} />
</TweakRow>
);
}
function TweakNumber({ label, value, min, max, step = 1, unit = '', onChange }) {
const clamp = (n) => {
if (min != null && n < min) return min;
if (max != null && n > max) return max;
return n;
};
const startRef = React.useRef({ x: 0, val: 0 });
const onScrubStart = (e) => {
e.preventDefault();
startRef.current = { x: e.clientX, val: value };
const decimals = (String(step).split('.')[1] || '').length;
const move = (ev) => {
const dx = ev.clientX - startRef.current.x;
const raw = startRef.current.val + dx * step;
const snapped = Math.round(raw / step) * step;
onChange(clamp(Number(snapped.toFixed(decimals))));
};
const up = () => {
window.removeEventListener('pointermove', move);
window.removeEventListener('pointerup', up);
};
window.addEventListener('pointermove', move);
window.addEventListener('pointerup', up);
};
return (
<div className="twk-num">
<span className="twk-num-lbl" onPointerDown={onScrubStart}>{label}</span>
<input type="number" value={value} min={min} max={max} step={step}
onChange={(e) => onChange(clamp(Number(e.target.value)))} />
{unit && <span className="twk-num-unit">{unit}</span>}
</div>
);
}
function __twkIsLight(hex) {
const h = String(hex).replace('#', '');
const x = h.length === 3 ? h.replace(/./g, (c) => c + c) : h.padEnd(6, '0');
const n = parseInt(x.slice(0, 6), 16);
if (Number.isNaN(n)) return true;
const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255;
return r * 299 + g * 587 + b * 114 > 148000;
}
const __TwkCheck = ({ light }) => (
<svg viewBox="0 0 14 14" aria-hidden="true">
<path d="M3 7.2 5.8 10 11 4.2" fill="none" strokeWidth="2.2"
strokeLinecap="round" strokeLinejoin="round"
stroke={light ? 'rgba(0,0,0,.78)' : '#fff'} />
</svg>
);
function TweakColor({ label, value, options, onChange }) {
if (!options || !options.length) {
return (
<div className="twk-row twk-row-h">
<div className="twk-lbl"><span>{label}</span></div>
<input type="color" className="twk-swatch" value={value}
onChange={(e) => onChange(e.target.value)} />
</div>
);
}
const key = (o) => String(JSON.stringify(o)).toLowerCase();
const cur = key(value);
return (
<TweakRow label={label}>
<div className="twk-chips" role="radiogroup">
{options.map((o, i) => {
const colors = Array.isArray(o) ? o : [o];
const [hero, ...rest] = colors;
const sup = rest.slice(0, 4);
const on = key(o) === cur;
return (
<button key={i} type="button" className="twk-chip" role="radio"
aria-checked={on} data-on={on ? '1' : '0'}
aria-label={colors.join(', ')} title={colors.join(' · ')}
style={{ background: hero }}
onClick={() => onChange(o)}>
{sup.length > 0 && (
<span>
{sup.map((c, j) => <i key={j} style={{ background: c }} />)}
</span>
)}
{on && <__TwkCheck light={__twkIsLight(hero)} />}
</button>
);
})}
</div>
</TweakRow>
);
}
function TweakButton({ label, onClick, secondary = false }) {
return (
<button type="button" className={secondary ? 'twk-btn secondary' : 'twk-btn'}
onClick={onClick}>{label}</button>
);
}
Object.assign(window, {
useTweaks, TweaksPanel, TweakSection, TweakRow,
TweakSlider, TweakToggle, TweakRadio, TweakSelect,
TweakText, TweakNumber, TweakColor, TweakButton,
});