/* gc-bil-ui.jsx — byggstenar för Bil-fliken (v766). Förenklad efter mockupen Bil-forenklad.html; André 3/10 lät mig bestämma. Bara presentation: sparvägen, reglerna och texterna bor i gc-bil.jsx. Namnen börjar på Gbu eftersom text/babel-filerna delar scope. */ const GBU_I = { chev:'M9 6l6 6-6 6', down:'M6 9l6 6 6-6', bolt:'M13 2L4 14h7l-1 8 9-12h-7z', sun:'M12 3v2M12 19v2M3 12h2M19 12h2M5.6 5.6L7 7M17 17l1.4 1.4M5.6 18.4L7 17M17 7l1.4-1.4M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8z', x:'M6 6l12 12M18 6L6 18', plus:'M12 5v14M5 12h14', minus:'M5 12h14', plug:'M9 2v5M15 2v5M6 7h12v4a6 6 0 0 1-12 0zM12 17v5' }; function GbuIcon({ d, size = 16, className = '', sw = 1.9 }){ return ; } /* Med onClick en knapp (rollen switch), utan en ren bild inne i en radknapp. */ function GbuSwitch({ on, onClick, label, tone = 'emerald', small }){ const bg = on ? (tone === 'amber' ? 'bg-amber-400' : 'bg-emerald-500') : 'bg-slate-600'; const cls = `relative shrink-0 rounded-full transition-colors ${small ? 'h-6 w-10' : 'h-7 w-12'} ${bg}`; const knob = ; return onClick ? : ; } /* En stapel för båda talen: fyllt = nuvarande laddning, ljusgrönt = det som ska laddas, linje = mål. Stapeln ändrar det tal som är valt under den. Tryck eller dra i sidled; ett lodrätt svep scrollar sidan och ändrar inget (pan-y + pointercancel). Piltangenterna ändrar en procent, med skift tio. */ function GbuBattery({ cur, target, edit, onChange }){ const ref = React.useRef(null), drag = React.useRef(null); const c = cur === '' || cur == null ? null : Number(cur); const t = Number(target) || 0; const val = edit === 'cur' ? (c ?? 0) : t; const at = (x) => { const r = ref.current.getBoundingClientRect(); return Math.round(Math.max(0, Math.min(1, (x - r.left) / r.width)) * 100); }; return (
{ const s = e.shiftKey ? 10 : 1; if (e.key === 'ArrowRight' || e.key === 'ArrowUp'){ e.preventDefault(); onChange(Math.min(100, val + s)); } if (e.key === 'ArrowLeft' || e.key === 'ArrowDown'){ e.preventDefault(); onChange(Math.max(0, val - s)); } }} onPointerDown={(e) => { drag.current = { id:e.pointerId, x:e.clientX, moved:false }; }} onPointerMove={(e) => { const d = drag.current; if (!d || d.id !== e.pointerId) return; if (!d.moved && Math.abs(e.clientX - d.x) < 4) return; if (!d.moved){ d.moved = true; try { ref.current.setPointerCapture(e.pointerId); } catch(_){} } onChange(at(e.clientX)); }} onPointerUp={(e) => { const d = drag.current; drag.current = null; if (d && !d.moved) onChange(at(e.clientX)); }} onPointerCancel={() => { drag.current = null; }} style={{ touchAction:'pan-y' }} className="relative h-14 cursor-pointer select-none rounded-2xl bg-slate-800 outline-none focus-visible:ring-2 focus-visible:ring-sky-400">
{[25, 50, 75].map(k =>
)} {c != null &&
} {t > (c ?? 0) &&
}
{/* Handtaget står mitt över strecket (mål) eller fyllningens kant (nu), samma x som fingret ger (v769). Bara vid 0 och 100 % hålls det 4 px innanför kanten. */}
); } function GbuValTab({ label, value, on, tone, onSelect, onStep }){ const col = tone === 'sky' ? 'text-sky-200' : 'text-emerald-300'; const ring = tone === 'sky' ? 'ring-sky-400/70' : 'ring-emerald-400/70'; const empty = value === '' || value == null; const step = (s) => ( ); return (
{on && step(-1)} {on && step(1)}
); } /* Backendens plan ritad från nu till fönstrets slut. Inga egna tal: blocken är planens kvartar. depQ = avresans kvart i fönstret (dag 0 = i dag), ritas bara inne i fönstret. */ function GbuTimeline({ ranges, from, to, depQ, depTime, tomLbl, colOf }){ const span = Math.max(1, to - from); const xp = (q) => Math.max(0, Math.min(100, ((q - from) / span) * 100)); const ticks = [24, 48, 72, 96, 120, 144, 168].filter(q => q > from + 6 && q < to); const depX = depQ != null && depQ > from && depQ <= to ? xp(depQ) : null; const lbl = (q) => q % 96 === 0 ? tomLbl : String(Math.floor((q % 96) / 4)).padStart(2, '0'); return (
{depX != null && 80 ? 'translateX(-100%)' : depX < 15 ? 'none' : 'translateX(-50%)' }}>avresa {depTime}}
{ticks.map(q =>
)} {ranges.map((r, k) =>
)} {depX != null &&
}
nu {ticks.map(q => {lbl(q)})}
); } /* Det man ställer in en gång: ihopfällt med valt värde, förklaringarna i den öppnade raden. */ function GbuSetRow({ id, title, value, note, tag, tagBad, open, onToggle, children }){ return (
{open &&
{children}
}
); } function GbuToggleRow({ on, tone, title, desc, onClick }){ const onCls = tone === 'amber' ? 'border-amber-400/50 bg-amber-500/10' : 'border-emerald-400/50 bg-emerald-500/10'; const tCol = on ? (tone === 'amber' ? 'text-amber-200' : 'text-emerald-200') : 'text-slate-200'; return ( ); } /* Två laddboxar: en flik per box, en box i taget. items: [{key, name, sub, col}]. */ function GbuBoxTabs({ items, sel, onSel }){ return (
{items.map((it, k) => { const on = k === sel; return ( ); })}
); } /* Spara och Ångra i en rad ovanför bottenflikarna. Spärren (v697/v698) står i den gula ramen. */ function GbuSaveBar({ dirty, blocked, toast, onSave, onUndo }){ if (!dirty && !toast) return null; const tc = toast ? ({ ok:'text-emerald-300', warn:'text-yellow-300', err:'text-rose-300' }[toast.kind] || 'text-slate-300') : 'text-slate-200'; return (
{dirty && blocked &&

Lägg till en avresedag i veckoschemat eller välj ett annat laddläge för att spara.

}
{toast ? toast.msg : 'Osparade ändringar'} {toast && toast.link && {toast.link.txt}} {dirty && } {dirty && }
); } Object.assign(window, { GBU_I, GbuIcon, GbuSwitch, GbuBattery, GbuValTab, GbuTimeline, GbuSetRow, GbuToggleRow, GbuBoxTabs, GbuSaveBar });