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 (
);
}
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 (
{!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 });