/* settings-atoms.jsx — reusable form atoms for the settings page.
All components expect plain values + onChange callbacks (controlled mode). */
// ── Card with gradient accent + header ──────────────────────────────────
const SCard = ({ accent, iconBg, iconColor, iconName, title, subtitle, right, children, style }) => (
{title}
{subtitle &&
{subtitle}
}
{right && {right}
}
{children}
);
// ── Generic text/number input ───────────────────────────────────────────
const SField = ({label, help, value, onChange, type='text', unit, placeholder, readOnly, disabled}) => {
const handle = (e) => {
if (readOnly || disabled) return;
if (type === 'number'){
const v = e.target.value;
if (v === '' || v === '-') { onChange(v); return; }
const n = parseFloat(v);
onChange(isNaN(n) ? 0 : n);
} else {
onChange(e.target.value);
}
};
return (
{label &&
{label} }
{unit && {unit} }
{help &&
{help}
}
);
};
// ── Select ──────────────────────────────────────────────────────────────
const SSelect = ({label, help, value, onChange, options}) => (
{label &&
{label} }
onChange(e.target.value)}>
{options.map(o => (
typeof o === 'string'
? {o}
: {o.label}
))}
{help &&
{help}
}
);
// ── Pill toggle (left of label) ─────────────────────────────────────────
const SToggle = ({label, value, onChange}) => (
onChange(e.target.checked)}/>
{label && {label} }
);
// ── Check row (green circle with checkmark on the left) ────────────────
const SCheck = ({label, help, value, onChange}) => (
onChange(e.target.checked)}/>
{label}
{help && {help} }
);
// ── Ja/Nej-segment (tydligt svar på ja/nej-frågor) ────────────────────
const SYesNo = ({label, help, value, onChange, yes='Ja', no='Nej'}) => (
{label}
{help && {help} }
onChange(true)}>{yes}
onChange(false)}>{no}
);
// ── 12-month picker, value är KALENDERMÅNADER 1..12 (backend 31/8: chipsen
// sparade index 0..11, vilket försköt säsongsgränserna en månad) ────────
const SMonthPicker = ({value=[], onChange}) => {
const names = ['Jan','Feb','Mar','Apr','Maj','Jun','Jul','Aug','Sep','Okt','Nov','Dec'];
const set = new Set(value || []);
const toggle = (m) => {
if (set.has(m)) set.delete(m); else set.add(m);
onChange([...set].sort((a,b)=>a-b));
};
return (
{names.map((m,i)=>(
toggle(i+1)}>
{m}
))}
);
};
// ── Read-only field with copy-to-clipboard button ──────────────────────
const SCopyField = ({label, value, help}) => {
const [copied, setCopied] = React.useState(false);
const onCopy = async () => {
try {
await navigator.clipboard.writeText(value || '');
setCopied(true);
setTimeout(()=>setCopied(false), 1500);
} catch(e){ /* ignore */ }
};
return (
{label &&
{label} }
{help &&
{help}
}
);
};
/* SNav — kategorinav för inställningssidan (2026-06-11, flik-läge).
Kontrollerad komponent: föräldern äger aktiv kategori och visar BARA den
kategorins kort (egen "sida" per kategori — användarönskemål, ersätter
scrollspy-långrullen). Desktop (≥1160px): vertikal rail till vänster.
Smalare: horisontell chip-rad under topbaren.
OBS (bevarat lärdomsmönster, se DESIGN_STATE 7h): rAF och
scrollTo({behavior:'smooth'}) är opålitliga i inbäddade vyer — undvik
dem för all framtida scroll-logik här. ALDRIG scrollIntoView. */
const SNav = ({ sections, active, onSelect }) => (
{sections.map((sec) => (
onSelect(sec.id)}>
{sec.label}
))}
);
/* ── BETA-märkning (André 7/9) ───────────────────────────────────────────────
Samma två delar som på dashboarden (GcBeta/GcBetaNote i gc-core): pill vid
rubriken är märkningen, notisen säger vad användaren ska göra åt den. */
const SBeta = ({ title }) => (
beta
);
const SBetaNote = ({ children }) => (
Beta
{children}
);
Object.assign(window, { SCard, SField, SSelect, SToggle, SCheck, SYesNo, SMonthPicker, SCopyField, SNav, SBeta, SBetaNote });