
// tweaks-panel.jsx
// Reusable Tweaks shell + form-control helpers.
//
// Owns the host protocol (listens for __activate_edit_mode / __deactivate_edit_mode,
// posts __edit_mode_available / __edit_mode_set_keys / __edit_mode_dismissed) so
// individual prototypes don't re-roll it. Ships a consistent set of controls so you
// don't hand-draw <input type="range">, segmented radios, steppers, etc.
//
// Usage (in an HTML file that loads React + Babel):
//
//   const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
//     "primaryColor": "#D97757",
//     "palette": ["#D97757", "#29261b", "#f6f4ef"],
//     "fontSize": 16,
//     "density": "regular",
//     "dark": false
//   }/*EDITMODE-END*/;
//
//   function App() {
//     const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
//     return (
//       <div style={{ fontSize: t.fontSize, color: t.primaryColor }}>
//         Hello
//         <TweaksPanel>
//           <TweakSection label="Typography" />
//           <TweakSlider label="Font size" value={t.fontSize} min={10} max={32} unit="px"
//                        onChange={(v) => setTweak('fontSize', v)} />
//           <TweakRadio  label="Density" value={t.density}
//                        options={['compact', 'regular', 'comfy']}
//                        onChange={(v) => setTweak('density', v)} />
//           <TweakSection label="Theme" />
//           <TweakColor  label="Primary" value={t.primaryColor}
//                        options={['#D97757', '#2A6FDB', '#1F8A5B', '#7A5AE0']}
//                        onChange={(v) => setTweak('primaryColor', v)} />
//           <TweakColor  label="Palette" value={t.palette}
//                        options={[['#D97757', '#29261b', '#f6f4ef'],
//                                  ['#475569', '#0f172a', '#f1f5f9']]}
//                        onChange={(v) => setTweak('palette', v)} />
//           <TweakToggle label="Dark mode" value={t.dark}
//                        onChange={(v) => setTweak('dark', v)} />
//         </TweaksPanel>
//       </div>
//     );
//   }
//
// ─────────────────────────────────────────────────────────────────────────────

const __TWEAKS_STYLE = `
	.twk-panel{position:fixed;right:16px;bottom:16px;z-index:2147483646;width:280px;
		max-height:calc(100vh - 32px);display:flex;flex-direction:column;
		transform:scale(var(--dc-inv-zoom,1));transform-origin:bottom right;
		background:rgba(250,249,247,.78);color:#29261b;
		-webkit-backdrop-filter:blur(24px) saturate(160%);backdrop-filter:blur(24px) saturate(160%);
		border:.5px solid rgba(255,255,255,.6);border-radius:14px;
		box-shadow:0 1px 0 rgba(255,255,255,.5) inset,0 12px 40px rgba(0,0,0,.18);
		font:11.5px/1.4 ui-sans-serif,system-ui,-apple-system,sans-serif;overflow:hidden}
	.twk-hd{display:flex;align-items:center;justify-content:space-between;
		padding:10px 8px 10px 14px;cursor:move;user-select:none}
	.twk-hd b{font-size:12px;font-weight:600;letter-spacing:.01em}
	.twk-x{appearance:none;border:0;background:transparent;color:rgba(41,38,27,.55);
		width:22px;height:22px;border-radius:6px;cursor:default;font-size:13px;line-height:1}
	.twk-x:hover{background:rgba(0,0,0,.06);color:#29261b}
	.twk-body{padding:2px 14px 14px;display:flex;flex-direction:column;gap:10px;
		overflow-y:auto;overflow-x:hidden;min-height:0;
		scrollbar-width:thin;scrollbar-color:rgba(0,0,0,.15) transparent}
	.twk-body::-webkit-scrollbar{width:8px}
	.twk-body::-webkit-scrollbar-track{background:transparent;margin:2px}
	.twk-body::-webkit-scrollbar-thumb{background:rgba(0,0,0,.15);border-radius:4px;
		border:2px solid transparent;background-clip:content-box}
	.twk-body::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.25);
		border:2px solid transparent;background-clip:content-box}
	.twk-row{display:flex;flex-direction:column;gap:5px}
	.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:rgba(41,38,27,.72)}
	.twk-lbl>span:first-child{font-weight:500}
	.twk-val{color:rgba(41,38,27,.5);font-variant-numeric:tabular-nums}

	.twk-sect{font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;
		color:rgba(41,38,27,.45);padding:10px 0 0}
	.twk-sect:first-child{padding-top:0}

	.twk-field{appearance:none;box-sizing:border-box;width:100%;min-width:0;height:26px;padding:0 8px;
		border:.5px solid rgba(0,0,0,.1);border-radius:7px;
		background:rgba(255,255,255,.6);color:inherit;font:inherit;outline:none}
	.twk-field:focus{border-color:rgba(0,0,0,.25);background:rgba(255,255,255,.85)}
	select.twk-field{padding-right:22px;
		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>");
		background-repeat:no-repeat;background-position:right 8px center}

	.twk-slider{appearance:none;-webkit-appearance:none;width:100%;height:4px;margin:6px 0;
		border-radius:999px;background:rgba(0,0,0,.12);outline:none}
	.twk-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;
		width:14px;height:14px;border-radius:50%;background:#fff;
		border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default}
	.twk-slider::-moz-range-thumb{width:14px;height:14px;border-radius:50%;
		background:#fff;border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default}

	.twk-seg{position:relative;display:flex;padding:2px;border-radius:8px;
		background:rgba(0,0,0,.06);user-select:none}
	.twk-seg-thumb{position:absolute;top:2px;bottom:2px;border-radius:6px;
		background:rgba(255,255,255,.9);box-shadow:0 1px 2px rgba(0,0,0,.12);
		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:inherit;font:inherit;font-weight:500;min-height:22px;
		border-radius:6px;cursor:default;padding:4px 6px;line-height:1.2;
		overflow-wrap:anywhere}

	.twk-toggle{position:relative;width:32px;height:18px;border:0;border-radius:999px;
		background:rgba(0,0,0,.15);transition:background .15s;cursor:default;padding:0}
	.twk-toggle[data-on="1"]{background:#34c759}
	.twk-toggle i{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;
		background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .15s}
	.twk-toggle[data-on="1"] i{transform:translateX(14px)}

	.twk-num{display:flex;align-items:center;box-sizing:border-box;min-width:0;height:26px;padding:0 0 0 8px;
		border:.5px solid rgba(0,0,0,.1);border-radius:7px;background:rgba(255,255,255,.6)}
	.twk-num-lbl{font-weight:500;color:rgba(41,38,27,.6);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:inherit;-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:8px;color:rgba(41,38,27,.45)}

	.twk-btn{appearance:none;height:26px;padding:0 12px;border:0;border-radius:7px;
		background:rgba(0,0,0,.78);color:#fff;font:inherit;font-weight:500;cursor:default}
	.twk-btn:hover{background:rgba(0,0,0,.88)}
	.twk-btn.secondary{background:rgba(0,0,0,.06);color:inherit}
	.twk-btn.secondary:hover{background:rgba(0,0,0,.1)}

	.twk-swatch{appearance:none;-webkit-appearance:none;width:56px;height:22px;
		border:.5px solid rgba(0,0,0,.1);border-radius:6px;padding:0;cursor:default;
		background:transparent;flex-shrink:0}
	.twk-swatch::-webkit-color-swatch-wrapper{padding:0}
	.twk-swatch::-webkit-color-swatch{border:0;border-radius:5.5px}
	.twk-swatch::-moz-color-swatch{border:0;border-radius:5.5px}

	.twk-chips{display:flex;gap:6px}
	.twk-chip{position:relative;appearance:none;flex:1;min-width:0;height:46px;
		padding:0;border:0;border-radius:6px;overflow:hidden;cursor:default;
		box-shadow:0 0 0 .5px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.06);
		transition:transform .12s cubic-bezier(.3,.7,.4,1),box-shadow .12s}
	.twk-chip:hover{transform:translateY(-1px);
		box-shadow:0 0 0 .5px rgba(0,0,0,.18),0 4px 10px rgba(0,0,0,.12)}
	.twk-chip[data-on="1"]{box-shadow:0 0 0 1.5px rgba(0,0,0,.85),
		0 2px 6px rgba(0,0,0,.15)}
	.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,.1)}
	.twk-chip>span>i{flex:1;box-shadow:0 -1px 0 rgba(0,0,0,.1)}
	.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,.3))}
`;

// ── useTweaks ───────────────────────────────────────────────────────────────
// Single source of truth for tweak values. setTweak persists via the host
// (__edit_mode_set_keys → host rewrites the EDITMODE block on disk).
function useTweaks(defaults) {
	const [values, setValues] = React.useState(defaults);
	// Accepts either setTweak('key', value) or setTweak({ key: value, ... }) so a
	// useState-style call doesn't write a "[object Object]" key into the persisted
	// JSON block.
	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 }, '*');
		// Same-window signal so in-page listeners (deck-stage rail thumbnails)
		// can react — the parent message only reaches the host, not peers.
		window.dispatchEvent(new CustomEvent('tweakchange', { detail: edits }));
	}, []);
	return [values, setTweak];
}

// ── TweaksPanel ─────────────────────────────────────────────────────────────
// Floating shell. Registers the protocol listener BEFORE announcing
// availability — if the announce ran first, the host's activate could land
// before our handler exists and the toolbar toggle would silently no-op.
// The close button posts __edit_mode_dismissed so the host's toolbar toggle
// flips off in lockstep; the host echoes __deactivate_edit_mode back which
// is what actually hides the panel.
function TweaksPanel({ title = 'Tweaks', noDeckControls = false, children }) {
	const [open, setOpen] = React.useState(false);
	const dragRef = React.useRef(null);
	// Auto-inject a rail toggle when a <deck-stage> is on the page. The
	// toggle drives the deck's per-viewer _railVisible via window message;
	// state is mirrored from the same localStorage key the deck reads so
	// the control reflects reality across reloads. The mechanism is the
	// message — authors who want custom placement can post it directly
	// and pass noDeckControls to suppress this one.
	const hasDeckStage = React.useMemo(
		() => typeof document !== 'undefined' && !!document.querySelector('deck-stage'),
		[],
	);
	// deck-stage enables its rail in connectedCallback, but this panel can
	// mount before that element has upgraded. The initial read catches the
	// common case; the listener covers mounting first. (Older deck-stage.js
	// copies still wait for the host's __omelette_rail_enabled postMessage —
	// same listener handles those.)
	const [railEnabled, setRailEnabled] = React.useState(
		() => hasDeckStage && !!document.querySelector('deck-stage')?._railEnabled,
	);
	React.useEffect(() => {
		if (!hasDeckStage || railEnabled) return undefined;
		const onMsg = (e) => {
			if (e.data && e.data.type === '__omelette_rail_enabled') setRailEnabled(true);
		};
		window.addEventListener('message', onMsg);
		return () => window.removeEventListener('message', onMsg);
	}, [hasDeckStage, railEnabled]);
	const [railVisible, setRailVisible] = React.useState(() => {
		try { return localStorage.getItem('deck-stage.railVisible') !== '0'; } catch (e) { return true; }
	});
	const toggleRail = (on) => {
		setRailVisible(on);
		window.postMessage({ type: '__deck_rail_visible', on }, '*');
	};
	const offsetRef = React.useRef({ x: 16, y: 16 });
	const PAD = 16;

	const clampToViewport = React.useCallback(() => {
		const panel = dragRef.current;
		if (!panel) return;
		const w = panel.offsetWidth, h = panel.offsetHeight;
		const maxRight = Math.max(PAD, window.innerWidth - w - PAD);
		const maxBottom = Math.max(PAD, window.innerHeight - h - PAD);
		offsetRef.current = {
			x: Math.min(maxRight, Math.max(PAD, offsetRef.current.x)),
			y: Math.min(maxBottom, Math.max(PAD, offsetRef.current.y)),
		};
		panel.style.right = offsetRef.current.x + 'px';
		panel.style.bottom = offsetRef.current.y + 'px';
	}, []);

	React.useEffect(() => {
		if (!open) return;
		clampToViewport();
		if (typeof ResizeObserver === 'undefined') {
			window.addEventListener('resize', clampToViewport);
			return () => window.removeEventListener('resize', clampToViewport);
		}
		const ro = new ResizeObserver(clampToViewport);
		ro.observe(document.documentElement);
		return () => ro.disconnect();
	}, [open, clampToViewport]);

	React.useEffect(() => {
		const onMsg = (e) => {
			const t = 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 = () => {
		setOpen(false);
		window.parent.postMessage({ type: '__edit_mode_dismissed' }, '*');
	};

	const onDragStart = (e) => {
		const panel = dragRef.current;
		if (!panel) return;
		const r = panel.getBoundingClientRect();
		const sx = e.clientX, sy = e.clientY;
		const startRight = window.innerWidth - r.right;
		const startBottom = window.innerHeight - r.bottom;
		const move = (ev) => {
			offsetRef.current = {
				x: startRight - (ev.clientX - sx),
				y: startBottom - (ev.clientY - sy),
			};
			clampToViewport();
		};
		const up = () => {
			window.removeEventListener('mousemove', move);
			window.removeEventListener('mouseup', up);
		};
		window.addEventListener('mousemove', move);
		window.addEventListener('mouseup', up);
	};

	if (!open) return null;
	return (
		<>
			<style>{__TWEAKS_STYLE}</style>
			<div ref={dragRef} className="twk-panel" data-noncommentable=""
					 style={{ right: offsetRef.current.x, bottom: offsetRef.current.y }}>
				<div className="twk-hd" onMouseDown={onDragStart}>
					<b>{title}</b>
					<button className="twk-x" aria-label="Close tweaks"
									onMouseDown={(e) => e.stopPropagation()}
									onClick={dismiss}>✕</button>
				</div>
				<div className="twk-body">
					{children}
					{hasDeckStage && railEnabled && !noDeckControls && (
						<TweakSection label="Deck">
							<TweakToggle label="Thumbnail rail" value={railVisible} onChange={toggleRail} />
						</TweakSection>
					)}
				</div>
			</div>
		</>
	);
}

// ── Layout helpers ──────────────────────────────────────────────────────────

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>
	);
}

// ── Controls ────────────────────────────────────────────────────────────────

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);
	// The active value is read by pointer-move handlers attached for the lifetime
	// of a drag — ref it so a stale closure doesn't fire onChange for every move.
	const valueRef = React.useRef(value);
	valueRef.current = value;

	// Segments wrap mid-word once per-segment width runs out. The track is
	// ~248px (280 panel − 28 body pad − 4 seg pad), each button loses 12px
	// to its own padding, and 11.5px system-ui averages ~6.3px/char — so 2
	// options fit ~16 chars each, 3 fit ~10. Past that (or >3 options), fall
	// back to a dropdown rather than wrap.
	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) {
		// <select> emits strings — map back to the original option value so the
		// fallback stays type-preserving (numbers, booleans) like the segment path.
		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) => {
			const v = segAt(ev.clientX);
			if (v !== valueRef.current) onChange(v);
		};
		const up = () => {
			setDragging(false);
			window.removeEventListener('mousemove', move);
			window.removeEventListener('mouseup', up);
		};
		window.addEventListener('mousemove', move);
		window.addEventListener('mouseup', up);
	};

	return (
		<TweakRow label={label} inline={false}>
			<div className={`twk-seg ${dragging ? 'dragging' : ''}`} ref={trackRef} onMouseDown={onPointerDown}>
				<div className="twk-seg-thumb" style={{ left: `${(100 / n) * idx}%`, width: `${100 / n}%` }} />
				{opts.map((o) => (
					<button key={o.value} type="button" onClick={() => onChange(o.value)}>{o.label}</button>
				))}
			</div>
		</TweakRow>
	);
}

function TweakSelect({ label, value, options, onChange }) {
	const opts = options.map((o) => (typeof o === 'object' ? o : { value: o, label: o }));
	return (
		<TweakRow label={label}>
			<select className="twk-field" value={value} onChange={(e) => onChange(e.target.value)}>
				{opts.map((o) => (
					<option key={o.value} value={o.value}>{o.label}</option>
				))}
			</select>
		</TweakRow>
	);
}

function TweakColor({ label, value, options = [], onChange }) {
	const isArray = Array.isArray(value);
	return (
		<TweakRow label={label}>
			{isArray ? (
				<div className="twk-chips">
					{options.map((palette, i) => (
						<button key={i} type="button" className="twk-chip"
										data-on={palette.join(',') === value.join(',') ? '1' : '0'}
										onClick={() => onChange(palette)}>
							<span>
								{palette.map((c, j) => (
									<i key={j} style={{ background: c }} />
								))}
							</span>
						</button>
					))}
				</div>
			) : (
				<div style={{ display:'flex', gap:6, alignItems:'center' }}>
					<input type="color" className="twk-swatch" value={value} onChange={(e) => onChange(e.target.value)} />
					{options.length > 0 && (
						<div className="twk-chips">
							{options.map((c) => (
								<button key={c} type="button" className="twk-chip" data-on={c === value ? '1' : '0'} onClick={() => onChange(c)}>
									<span><i style={{ background: c }} /></span>
								</button>
							))}
						</div>
					)}
				</div>
			)}
		</TweakRow>
	);
}

window.useTweaks = useTweaks;
window.TweaksPanel = TweaksPanel;
window.TweakSection = TweakSection;
window.TweakSlider = TweakSlider;
window.TweakToggle = TweakToggle;
window.TweakRadio = TweakRadio;
window.TweakSelect = TweakSelect;
window.TweakColor = TweakColor;
