const { useState: useStateSc, useEffect: useEffectSc, useMemo: useMemoSc, useRef: useRefSc } = React; function uid() { return Math.random().toString(36).slice(2, 10); } const MAP_CODE_RE = /^\d{4}-\d{4}-\d{4}$/; const ScheduleAPI = { async list() { const r = await fetch("/api/schedules"); if (!r.ok) throw new Error(`list HTTP ${r.status}`); const data = await r.json(); return (data.schedules || []).map(srvToFE); }, async create(fe) { const body = feToSrv(fe); const r = await fetch("/api/schedules", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!r.ok) throw new Error(`create HTTP ${r.status}: ${await r.text()}`); return r.json(); }, async update(id, fe) { const body = feToSrv(fe); const r = await fetch(`/api/schedules/${encodeURIComponent(id)}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!r.ok) throw new Error(`update HTTP ${r.status}: ${await r.text()}`); return r.json(); }, async remove(id) { const r = await fetch(`/api/schedules/${encodeURIComponent(id)}`, { method: "DELETE" }); if (!r.ok) throw new Error(`delete HTTP ${r.status}`); }, async start(id) { const r = await fetch(`/api/schedules/${encodeURIComponent(id)}/start`, { method: "POST" }); if (!r.ok) throw new Error(`start HTTP ${r.status}`); }, async stop(id) { const r = await fetch(`/api/schedules/${encodeURIComponent(id)}/stop`, { method: "POST" }); if (!r.ok) throw new Error(`stop HTTP ${r.status}`); }, async skipStage(id) { const r = await fetch(`/api/schedules/${encodeURIComponent(id)}/skip-stage`, { method: "POST" }); if (!r.ok) throw new Error(`skip HTTP ${r.status}`); }, }; function srvToFE(srv) { return { id: srv.id, name: srv.name, instances: srv.instance_count, whenFinished: srv.when_finished, startAt: srv.start_at, finishedAt: srv.finished_at || undefined, createdAt: srv.created_at, state: srv.state, currentStage: srv.current_stage, stageStartedAt: srv.stage_started_at || undefined, boundPads: (srv.pads || []).map(p => p.pad_code), stages: (srv.stages || []).map(s => ({ id: s.id, code: s.map_code, hours: Math.floor(s.duration_sec / 3600), minutes: Math.floor((s.duration_sec % 3600) / 60), })), rampMode: "immediate", startMode: srv.start_at > Math.floor(Date.now() / 1000) ? "later" : "now", }; } function feToSrv(fe) { return { name: fe.name, instance_count: Number(fe.instances) || 1, when_finished: fe.whenFinished || "stop", start_at: fe.startAt || null, stages: (fe.stages || []).map(s => ({ map_code: s.code, duration_sec: Math.max(60, (s.hours * 3600) + (s.minutes * 60)), })), }; } function stageDurationSec(s) { return (s.hours * 3600) + (s.minutes * 60); } function totalDurationSec(stages) { return stages.reduce((a, s) => a + stageDurationSec(s), 0); } function classifySession(session, now = Math.floor(Date.now() / 1000)) { if (session.state === "active") return "active"; if (session.state === "pending") return "upcoming"; if (session.state === "finished" || session.state === "stopped") return "history"; if (session.finishedAt) return "history"; const total = totalDurationSec(session.stages); if (session.whenFinished === "loop") { return session.startAt <= now ? "active" : "upcoming"; } if (session.startAt > now) return "upcoming"; if (session.startAt + total <= now) return "history"; return "active"; } function currentStageInfo(session, now = Math.floor(Date.now() / 1000)) { const total = totalDurationSec(session.stages); if (!total || !session.stages.length) return null; let elapsed = Math.max(0, now - session.startAt); let loop = 1; if (session.whenFinished === "loop" && total > 0) { loop = Math.floor(elapsed / total) + 1; elapsed = elapsed % total; } else if (elapsed > total) { elapsed = total; } let acc = 0; for (let i = 0; i < session.stages.length; i++) { const dur = stageDurationSec(session.stages[i]); if (elapsed < acc + dur) { return { index: i, loop, stageElapsed: elapsed - acc, stageDuration: dur, totalElapsed: now - session.startAt, totalDuration: total, }; } acc += dur; } const last = session.stages.length - 1; return { index: last, loop, stageElapsed: stageDurationSec(session.stages[last]), stageDuration: stageDurationSec(session.stages[last]), totalElapsed: total, totalDuration: total, }; } function fmtDur(secs) { if (secs <= 0) return "0m"; const h = Math.floor(secs / 3600); const m = Math.floor((secs % 3600) / 60); if (h && m) return `${h}h ${m}m`; if (h) return `${h}h`; return `${m}m`; } function fmtClock(secs) { const h = Math.floor(secs / 3600); const m = Math.floor((secs % 3600) / 60); return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}`; } function fmtAbsTime(ts) { if (!ts) return "—"; const d = new Date(ts * 1000); const now = new Date(); const sameDay = d.toDateString() === now.toDateString(); const time = `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`; if (sameDay) return time; const days = Math.round((d - now) / 86400000); if (days === 1) return `tomorrow ${time}`; if (days === -1) return `yesterday ${time}`; return `${d.getMonth()+1}/${d.getDate()} ${time}`; } function useNowTick(periodMs = 1000) { const [n, setN] = useStateSc(0); useEffectSc(() => { const id = setInterval(() => setN((x) => x + 1), periodMs); return () => clearInterval(id); }, [periodMs]); return n; } function SchedulerScreen({ codes, instances }) { const [sessions, setSessions] = useStateSc([]); const [tab, setTab] = useStateSc("active"); const [editing, setEditing] = useStateSc(null); const [expanded, setExpanded] = useStateSc(() => new Set()); const [loadError, setLoadError] = useStateSc(null); const [statsHistory, setStatsHistory] = useStateSc({}); const refetch = React.useCallback(async () => { try { const list = await ScheduleAPI.list(); setSessions(list); setLoadError(null); } catch (e) { setLoadError(String(e)); } }, []); useEffectSc(() => { refetch(); }, [refetch]); useEffectSc(() => { if (!window.Live || !window.Live.subscribe) return; const off = window.Live.subscribe((m) => { if (m && (m.type === "schedule_state" || m.type === "schedule_pads" || m.type === "schedule_deleted")) { refetch(); } if (m && m.type === "schedule_stats") { setStatsHistory((prev) => { const arr = prev[m.id] ? prev[m.id].slice() : []; arr.push({ ts: m.ts, in_game: m.in_game, total: m.total, assigned: m.assigned }); if (arr.length > 240) arr.splice(0, arr.length - 240); return { ...prev, [m.id]: arr }; }); } }); return off; }, [refetch]); const tick = useNowTick(1000); const buckets = useMemoSc(() => { const out = { active: [], upcoming: [], history: [] }; const now = Math.floor(Date.now() / 1000); sessions.forEach((s) => out[classifySession(s, now)].push(s)); out.active.sort((a, b) => a.startAt - b.startAt); out.upcoming.sort((a, b) => a.startAt - b.startAt); out.history.sort((a, b) => (b.finishedAt || b.startAt) - (a.finishedAt || a.startAt)); return out; }, [sessions, tick]); const visible = buckets[tab]; useEffectSc(() => { if (tab === "active" && buckets.active.length > 0 && expanded.size === 0) { setExpanded(new Set([buckets.active[0].id])); } }, [tab]); const toggle = (id) => setExpanded((prev) => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; }); const handleSave = async (session) => { for (const st of (session.stages || [])) { if (!MAP_CODE_RE.test(st.code || "")) { alert(`Map code должен быть в формате 0000-0000-0000 (текущее: "${st.code}")`); return; } } try { const isNew = !sessions.some((s) => s.id === session.id); if (isNew) { await ScheduleAPI.create(session); if (session.startMode === "now") { await refetch(); const created = (await ScheduleAPI.list()).find( (s) => s.name === session.name && s.state === "pending" ); if (created) await ScheduleAPI.start(created.id); } } else { await ScheduleAPI.update(session.id, session); } await refetch(); } catch (e) { alert(`Сохранение не удалось: ${e}`); return; } setEditing(null); }; const handleDelete = async (id) => { try { await ScheduleAPI.remove(id); } catch (e) { alert(`Удаление не удалось: ${e}`); } await refetch(); }; const handleStop = async (id) => { try { await ScheduleAPI.stop(id); } catch (e) { alert(`Stop не удалось: ${e}`); } await refetch(); }; const handleStart = async (id) => { try { await ScheduleAPI.start(id); } catch (e) { alert(`Start не удалось: ${e}`); } await refetch(); }; const handleSkipStage = async (id) => { if (!confirm("Перейти на следующую стадию? Текущие аккаунты будут освобождены, боты перезапустятся с новыми.")) return; try { await ScheduleAPI.skipStage(id); } catch (e) { alert(`Skip не удалось: ${e}`); } await refetch(); }; return (
setEditing("new")} />
{visible.length === 0 && (
{tab === "active" && "No active sessions"} {tab === "upcoming" && "Nothing scheduled"} {tab === "history" && "No finished sessions yet"}
{tab !== "history" && ( )}
)} {visible.map((s) => ( toggle(s.id)} onEdit={() => setEditing(s)} onDelete={() => handleDelete(s.id)} onStart={() => handleStart(s.id)} onStop={() => handleStop(s.id)} onSkipStage={() => handleSkipStage(s.id)} /> ))}
{editing && ( setEditing(null)} onSave={handleSave} /> )}
); } // ─── Topbar ────────────────────────────────────────────────────────────── function SchedulerTopbar({ counts, onNew }) { return (

Scheduler

); } function SchedulerTabs({ tab, setTab, counts }) { const items = [ { id: "active", label: "Active", count: counts.active }, { id: "upcoming", label: "Upcoming", count: counts.upcoming }, { id: "history", label: "History", count: counts.history }, ]; return (
{items.map((it) => { const on = tab === it.id; return ( ); })}
); } function SessionCard({ session, kind, instances, history, expanded, onToggle, onEdit, onDelete, onStart, onStop, onSkipStage }) { const info = currentStageInfo(session); const total = totalDurationSec(session.stages); return (
{expanded && ( )}
); } function CollapsedStageList({ stages }) { const containerRef = useRefSc(null); const [visible, setVisible] = useStateSc(stages.length); const colorForCode = useColorForCode(); useEffectSc(() => { const el = containerRef.current; if (!el) return; const measure = () => { const items = el.querySelectorAll('[data-stage-chip]'); if (!items.length) return; const containerRight = el.getBoundingClientRect().right; // Estimate dots width ~30px let last = stages.length; for (let i = 0; i < items.length; i++) { const r = items[i].getBoundingClientRect().right; if (r > containerRight - 34) { last = i; break; } } setVisible(last === stages.length ? stages.length : last); }; measure(); const ro = new ResizeObserver(measure); ro.observe(el); return () => ro.disconnect(); }, [stages]); const shown = stages.slice(0, visible); const hasMore = visible < stages.length; return (
{shown.map((s, i) => { const c = colorForCode(s.code); return ( {s.code || '—'} {i < shown.length - 1 && !hasMore && ( )} {i < shown.length - 1 && hasMore && ( )} ); })} {hasMore && ( ··· )}
); } function SessionCardHeader({ session, kind, info, total, expanded, onToggle }) { const isActive = kind === "active"; const isUpcoming = kind === "upcoming"; const isHistory = kind === "history"; return (
e.currentTarget.style.background = 'rgba(255,255,255,.025)'} onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'} >
{session.name} {isUpcoming && ( scheduled )} {isHistory && ( finished )}
{!expanded && ( )}
{isActive && ( {session.instances} /{session.instances} )} {isUpcoming && ( {fmtAbsTime(session.startAt)} )} {isHistory && ( {fmtAbsTime(session.finishedAt || session.startAt + total)} )}
); } function SessionCardBody({ session, kind, info, total, instances, history, onEdit, onDelete, onStart, onStop, onSkipStage }) { const [stageView, setStageView] = useStateSc(info ? info.index : 0); useEffectSc(() => { if (info) setStageView(info.index); }, [info?.index]); const isActive = kind === "active"; const isUpcoming = kind === "upcoming"; const isHistory = kind === "history"; const currentStage = session.stages[stageView] || session.stages[0]; const stageRemaining = isActive && info && info.index === stageView ? (info.stageDuration - info.stageElapsed) : null; const eta = isActive && info ? (total - info.totalElapsed) : null; const elapsedNice = isActive && info ? fmtClock(info.totalElapsed) : null; const totalNice = fmtClock(total); return (
{}
{/* Stage segmented progress */}
{/* Footer actions */}
{isActive && ( <> )} {isUpcoming && ( <> {} {(session.boundPads || []).length > 0 && ( )} {onStart && ( )} )} {isHistory && ( <> )}
); } function BotStatusBar({ instances, total, assigned, boundPads }) { const boundSet = useMemoSc(() => new Set(boundPads || []), [boundPads]); const myInstances = (instances || []).filter(i => boundSet.has(i.id)); const hasData = myInstances.length > 0; const raw = { running: myInstances.filter(i => i.status === 'running').length, booting: myInstances.filter(i => i.status === 'booting').length, idle: myInstances.filter(i => i.status === 'idle').length, error: myInstances.filter(i => i.status === 'error').length, }; const missingPads = (boundPads || []).filter(p => !(instances || []).some(i => i.id === p)).length; const queued = raw.idle + missingPads; const items = [ { label: 'in game', value: hasData || missingPads ? raw.running : '—', dot: '#5EE083' }, { label: 'starting', value: hasData || missingPads ? raw.booting : '—', dot: '#D7B074' }, { label: 'queued', value: hasData || missingPads ? queued : '—', dot: '#777' }, { label: 'error', value: hasData || missingPads ? raw.error : '—', dot: '#FF6B6B' }, ]; const shortage = typeof assigned === "number" && typeof total === "number" && assigned < total; return (
{typeof assigned === "number" && (
{assigned}/{total} assigned
)} {items.map(({ label, value, dot }) => (
{value} {label}
))}
); } // ─── Stage buttons ──────────────────────────────────────────────────────── function StageProgress({ session, info, kind, stageView, onSelect }) { const isActive = kind === "active"; const currentIdx = isActive && info ? info.index : -1; return (
{session.stages.map((s, i) => { const isCurrent = i === currentIdx; return (
{isCurrent && ( )} {s.code || '—'}
); })}
); } // ─── Chart ─────────────────────────────────────────────────────────────── // Будет получать данные через API. Пока показывает заглушку с осями. function SessionChart({ session, info, stageIndex, kind, history }) { // history: [{ts, in_game, total}] — приходит через WS schedule_stats. // Берём последние N точек для текущей стадии и нормализуем v = in_game/total. const stageStartTs = session.stageStartedAt || session.startAt; const stageDurSec = session.stages[stageIndex] ? stageDurationSec(session.stages[stageIndex]) : 1; const points = useMemoSc(() => { if (!history || !history.length) return []; return history .filter((h) => h.ts >= stageStartTs) .map((h) => ({ t: Math.min(1, Math.max(0, (h.ts - stageStartTs) / Math.max(1, stageDurSec))), v: h.total > 0 ? h.in_game / h.total : 0, })); }, [history, stageStartTs, stageDurSec]); const hasData = points.length > 0; const stageDur = session.stages[stageIndex] ? stageDurationSec(session.stages[stageIndex]) : 0; const W = 1000, H = 150, PAD_L = 32, PAD_R = 8, PAD_T = 10, PAD_B = 22; const innerW = W - PAD_L - PAD_R; const innerH = H - PAD_T - PAD_B; const progressX = (kind === "active" && info && info.index === stageIndex) ? Math.min(1, info.stageElapsed / Math.max(1, info.stageDuration)) : null; return (
{} {[0.25, 0.5, 0.75].map((g, i) => ( ))} {} {[{v:1,l:'100%'},{v:.5,l:'50%'},{v:0,l:'0%'}].map((yl,i) => ( {yl.l} ))} {hasData ? ( /* Реальные данные от API */ (() => { const path = points.map((p, i) => { const x = PAD_L + p.t * innerW; const y = PAD_T + (1 - p.v) * innerH; return `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`; }).join(' '); const area = path + ` L${PAD_L + innerW},${PAD_T + innerH} L${PAD_L},${PAD_T + innerH} Z`; return ( <> ); })() ) : ( /* Заглушка — пунктирная baseline по центру */ )} {} {progressX !== null && ( )} {} 00:00 {fmtClock(Math.floor(stageDur / 2))} {fmtClock(stageDur)} {!hasData && (
waiting for data
)}
); } // ─── New / Edit modal ─────────────────────────────────────────────────── function SessionModal({ codes, existing, onClose, onSave, instances: allInstances, sessions }) { const occupiedByOthers = useMemoSc(() => { const occ = new Set(); (sessions || []).forEach((s) => { if (s.id === existing?.id) return; if (s.state === "active" || s.state === "pending") { (s.boundPads || []).forEach(p => occ.add(p)); } }); return occ; }, [sessions, existing?.id]); const availableBots = useMemoSc(() => { if (!allInstances) return null; return allInstances.filter(i => !occupiedByOthers.has(i.id)).length; }, [allInstances, occupiedByOthers]); const [name, setName] = useStateSc(existing?.name || ""); const [instances, setInstances] = useStateSc(existing?.instances || 60); const [stages, setStages] = useStateSc(() => existing?.stages?.length ? existing.stages.map((s) => ({ ...s })) : [{ id: uid(), code: codes[0] || "", hours: 1, minutes: 30 }] ); const [whenFinished, setWhenFinished] = useStateSc(existing?.whenFinished || "stop"); const [rampMode, setRampMode] = useStateSc(existing?.rampMode || "immediate"); const [startMode, setStartMode] = useStateSc(existing?.startMode || "now"); const [startInMin, setStartInMin] = useStateSc(() => { if (existing?.startMode === "later" && existing.startAt) { const diff = Math.max(0, Math.floor((existing.startAt - Date.now() / 1000) / 60)); return diff; } return 60; }); const total = totalDurationSec(stages); const stagesValid = stages.length > 0 && stages.every((s) => s.code && stageDurationSec(s) > 0); const canSave = instances > 0 && stagesValid; const autoName = useMemoSc(() => { if (!stages.length) return "session"; const codesList = stages.map((s) => s.code).filter(Boolean); if (!codesList.length) return "session"; return `auto · ${codesList[0]}${codesList.length > 1 ? " +" + (codesList.length - 1) : ""}`; }, [stages]); const updateStage = (id, patch) => setStages((prev) => prev.map((s) => s.id === id ? { ...s, ...patch } : s)); const removeStage = (id) => setStages((prev) => prev.filter((s) => s.id !== id)); const addStage = () => setStages((prev) => [...prev, { id: uid(), code: codes[prev.length % Math.max(1, codes.length)] || "", hours: 0, minutes: 30, }]); const submit = () => { if (!canSave) return; const now = Math.floor(Date.now() / 1000); const startAt = startMode === "now" ? now : now + startInMin * 60; const out = { id: existing?.id || uid(), name: name.trim() || autoName, instances, stages, whenFinished, rampMode, startMode, startAt, createdAt: existing?.createdAt || now, }; onSave(out); }; return ( <>
{/* Header */}

{existing ? "Edit session" : "New session"}

{}
{}
bots {availableBots !== null && ( availableBots ? 'var(--warn)' : 'var(--ink-4)', transition: 'color var(--t) var(--ease)', }}> {instances > availableBots ? `⚠ available: ${availableBots}` : `available: ${availableBots}`} )}
start {startMode === "later" && (
setStartInMin(Math.max(1, parseInt(e.target.value) || 0))} style={{ width: 44, textAlign: 'right', background: 'transparent', border: 0, outline: 0, color: 'var(--ink)', fontSize: 13, padding: 0, }} /> min
)}
{}
rotation chain {stagesValid && ( {`${stages.length} stage${stages.length !== 1 ? 's' : ''} · ${fmtDur(total)} total`} )}
{stages.map((s, i) => ( 1} onChange={(patch) => updateStage(s.id, patch)} onRemove={() => removeStage(s.id)} /> ))}
{/* Footer */}
); } function Field({ label, hint, children }) { return (
{label} {hint && {hint}}
{children}
); } function NumberStepper({ value, onChange, min = 0, max = 9999, step = 1, bigStep = 10 }) { const clamp = (v) => Math.max(min, Math.min(max, v)); return (
onChange(clamp(parseInt(e.target.value) || 0))} style={{ width: 60, textAlign: 'center', background: 'transparent', border: 0, outline: 0, color: 'var(--ink)', fontSize: 14, padding: '8px 0', borderLeft: '1px solid var(--line)', borderRight: '1px solid var(--line)', }} />
); } function StageRow({ index, stage, codes, canRemove, onChange, onRemove }) { const colorForCode = useColorForCode(); const c = colorForCode(stage.code); const wrapRef = useRefSc(null); const [listOpen, setListOpen] = useStateSc(false); useEffectSc(() => { if (!listOpen) return; const onDoc = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) setListOpen(false); }; document.addEventListener('mousedown', onDoc); return () => document.removeEventListener('mousedown', onDoc); }, [listOpen]); const value = stage.code || ""; const valid = !value || MAP_CODE_RE.test(value); const formatMapCode = (raw) => { const digits = (raw || "").replace(/\D/g, "").slice(0, 12); const parts = []; if (digits.length > 0) parts.push(digits.slice(0, 4)); if (digits.length > 4) parts.push(digits.slice(4, 8)); if (digits.length > 8) parts.push(digits.slice(8, 12)); return parts.join("-"); }; const suggestions = useMemoSc(() => { if (!codes || !codes.length) return []; const q = value; return codes.filter((x) => MAP_CODE_RE.test(x) && (!q || x.includes(q))).slice(0, 8); }, [codes, value]); return (
{index + 1} {/* Inline-инпут кода + dropdown подсказок */}
setListOpen(true)} onChange={(e) => { onChange({ code: formatMapCode(e.target.value) }); setListOpen(true); }} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === 'Escape') { e.preventDefault(); setListOpen(false); e.target.blur(); } }} className="mono" style={{ flex: 1, width: '100%', background: 'transparent', border: 0, outline: 0, fontSize: 13, color: !valid ? 'var(--bad)' : (stage.code ? c.text : 'var(--ink)'), padding: 0, }} /> setListOpen((o) => !o)} style={{ cursor: 'pointer' }}/>
{listOpen && suggestions.length > 0 && (
{suggestions.map((code) => { const col = colorForCode(code); const on = code === stage.code; return (
{ e.preventDefault(); onChange({ code }); setListOpen(false); }} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '6px 8px', borderRadius: 4, cursor: 'pointer', fontSize: 13, background: on ? 'var(--panel-2)' : 'transparent', color: on ? 'var(--ink)' : 'var(--ink-2)', }} onMouseEnter={(e) => e.currentTarget.style.background = 'var(--panel-2)'} onMouseLeave={(e) => { if (!on) e.currentTarget.style.background = 'transparent'; }}> {code} {on && }
); })}
)}
onChange({ hours: Math.max(0, Math.min(99, v)) })}/> onChange({ minutes: Math.max(0, Math.min(59, v)) })} step={5}/>
); } function DurationPart({ label, value, onChange, step = 1 }) { return (
onChange(parseInt(e.target.value) || 0)} onKeyDown={(e) => { if (e.key === 'ArrowUp') { e.preventDefault(); onChange(value + step); } if (e.key === 'ArrowDown') { e.preventDefault(); onChange(Math.max(0, value - step)); } }} style={{ width: 32, textAlign: 'right', background: 'transparent', border: 0, outline: 0, color: 'var(--ink)', fontSize: 13, padding: '3px 0', }} /> {label}
); } function SegToggle({ value, onChange, options }) { return (
{options.map((opt) => { const on = value === opt.value; return ( ); })}
); } function SelectInput({ value, onChange, options }) { const ref = useRefSc(null); const [open, setOpen] = useStateSc(false); const current = options.find((o) => o.value === value); return ( <> setOpen(false)} anchorRef={ref} align="left" width={220}> {options.map((o) => (
{ onChange(o.value); setOpen(false); }} style={{ padding: '8px 11px', borderRadius: 5, cursor: 'pointer', fontSize: 13.5, background: o.value === value ? 'var(--panel-2)' : 'transparent', color: o.value === value ? 'var(--ink)' : 'var(--ink-2)', display: 'flex', alignItems: 'center', gap: 10, }} onMouseEnter={(e) => e.currentTarget.style.background = 'var(--panel-2)'} onMouseLeave={(e) => { if (o.value !== value) e.currentTarget.style.background = 'transparent'; }} > {o.label} {o.value === value && }
))}
); } Object.assign(window, { SchedulerScreen });