/* help.jsx — modernized HEMS help page. Loads content from help-content.json and renders all sections with collapsible cards, sticky pill nav, and consistent dashboard chrome. */ const HELP_GRADIENTS = { 'from-energy-charge to-energy-solar': 'linear-gradient(90deg, #5BA8FF, #F4C84B)', 'from-energy-grid to-energy-consumption': 'linear-gradient(90deg, #5BA8FF, #FF8A47)', 'from-energy-solar to-energy-discharge': 'linear-gradient(90deg, #F4C84B, #3FCC8B)', 'from-energy-charge to-energy-hold': 'linear-gradient(90deg, #5BA8FF, #9D7BFF)', 'from-destructive to-energy-solar': 'linear-gradient(90deg, #FF5A65, #F4C84B)', 'from-energy-discharge to-destructive': 'linear-gradient(90deg, #3FCC8B, #FF5A65)', 'from-primary to-energy-solar': 'linear-gradient(90deg, #5BA8FF, #F4C84B)', 'from-energy-hold to-primary': 'linear-gradient(90deg, #9D7BFF, #5BA8FF)', }; // Section icon-circle bg + color const SECTION_ICON = { quickstart: { bg: 'rgba(91,168,255,.14)', color: 'var(--blue)', icon: 'gear' }, commands: { bg: 'rgba(255,138,71,.14)', color: 'var(--c-cover)', icon: 'flash' }, 'flow-examples': { bg: 'rgba(244,200,75,.14)', color: 'var(--c-charge-sol)', icon: 'sun' }, 'multi-battery': { bg: 'rgba(157,123,255,.14)', color: 'var(--violet)', icon: 'battery' }, 'peak-shaving': { bg: 'rgba(255,90,101,.14)', color: 'var(--c-peak)', icon: 'lightning' }, troubleshooting: { bg: 'rgba(63,204,139,.14)', color: 'var(--c-sell)', icon: 'gear' }, faq: { bg: 'rgba(91,168,255,.14)', color: 'var(--blue)', icon: 'eye' }, more: { bg: 'rgba(157,123,255,.14)', color: 'var(--violet)', icon: 'chart' }, }; // ─── Section wrapper ──────────────────────────────────────────────── const Section = ({ id, data, open, onToggle, children }) => { const meta = SECTION_ICON[id] || {}; return (
{open &&
{children}
}
); }; // ─── Q&A row ──────────────────────────────────────────────────────── const QA = ({ q, a }) => { const [open, setOpen] = React.useState(false); return (
{open &&
{a}
}
); }; // ─── Quickstart section ──────────────────────────────────────────── const QuickstartBody = ({ data }) => ( <> {/* ⚠️ Lyft det enda som förstör allt — näteffekt-sensorn — högst upp */} {data.criticalCallout && (
{data.criticalCallout.title}
{data.criticalCallout.lead}
✓ Rätt: {data.criticalCallout.good}
✕ Fel: {data.criticalCallout.bad}
{data.criticalCallout.check}
)} {/* Grundflöde — 4 korta steg */} {data.steps.map((s, i) => (
{s.n}
{s.title}
{s.body}
{s.image && (
{}}/>
{s.imageHint}
)}
))} {data.stepNote && (
{data.stepNote}
)}
{data.successNote}
{/* Fält-tabell med tydlig teckenkonvention */}

{data.sensorTable.heading}

{data.sensorTable.rows.map((r,i)=>{ const sc = r.signKind==='grid' ? '#2ED47A' : r.signKind==='batt' ? '#F5B544' : 'var(--ink-2)'; return ( ); })}
FältVad det ärTeckenExempel
{r.field} {r.what} {r.sign} {r.example}
{data.sensorTable.signLegend && (
{data.sensorTable.signLegend.map((l,i)=>( {l.t} ))}
)}
{/* Avancerat — hopfällt */} {(data.advanced||[]).map((a,i)=>( ))} ); // ─── Commands section ────────────────────────────────────────────── const CommandsBody = ({ data }) => ( <>
{data.commands.map((c, i) => (
{c.emoji} {c.cmd}
NÄR-kort: "{c.narCard}"
Växelriktaren: {c.inverterAction}
Effekt-tagg:{' '} {c.hasPower ? 'Ja' : 'Nej'} {c.powerNote && {c.powerNote}}
))}
{data.tagInfo.heading}
{data.tagInfo.body}
    {data.tagInfo.tags.map((t,i)=>(
  • {t.name} — {t.desc}
  • ))}
{data.updateInterval}
{data.expandables.map((e,i)=>)}
); // ─── Flow examples section ───────────────────────────────────────── const FlowExamplesBody = ({ data, onLightbox }) => ( <> {data.flows.map((f, i) => (

{f.title}

{f.description}

{/* Textkort (image:null, t.ex. egenförbrukningsrådet) hoppar över bildslotten — img med src=null renderar en trasig-bild-ikon. */} {f.image && } {f.imageHint &&
{f.imageHint}
}
))} {data.comingSoon && (
{data.comingSoon}
)} ); const FlowImage = ({ src, alt, onLightbox }) => { const [error, setError] = React.useState(false); if (error){ return (
Bilden kunde inte laddas.
({src})
); } return ( {alt}onLightbox(src, alt)} onError={()=>setError(true)}/> ); }; // ─── Multi-battery section ───────────────────────────────────────── const MultiBatteryBody = ({ data }) => ( <>

{data.howItWorks.heading}

{data.buildFlows.heading}

{data.buildFlows.body}
{data.buildFlows.flowExample.map((row, i)=>( {row.type}: {row.text} ))}
Antal flöden: Ett per batteri per kommando.
Exempel: 2 batterier × 3 kommandon (charge, pause, selfconsumption) = 6 flöden.
Utan OCH-kort körs SEDAN-kortet för ALLA batterier — båda får samma kommando, vilket kan bli fel om HEMS vill ladda B1 men pausa B2.

{data.socExample.heading}

{data.socExample.table.map((r,i)=>( ))}
SituationB1 (SOC 30%)B2 (SOC 100%)
{r.situation} {r.b1} {r.b2}
{data.socExample.note}

{data.aiBatteries.heading}

{data.config.heading}

); // ─── Peak shaving section ────────────────────────────────────────── const PeakShavingBody = ({ data }) => ( <>
{data.whatIs.heading} {data.whatIs.body}

{data.howHelps.heading}

{data.reserve.heading}

{data.reserve.body}
{data.reserve.phases.map((p,i)=>(
{p.phase}
{p.value}
{p.desc}
))}

{data.reactiveProtection.heading}

{data.reactiveProtection.body}
{data.reactiveProtection.steps.map((s,i)=>(
{s.n}
{s.text}
))}

{data.modes.heading}

{data.modes.items.map((m,i)=>(
{m.name}
{m.desc}
))}
Konfigurera under Inställningar → Effektavgift.
); // ─── Troubleshooting section ─────────────────────────────────────── const TroubleshootingBody = ({ data }) => ( <> {data.problems.map((p,i)=>(
{p.n}
{p.title} {p.symptom && (
Symptom
{p.symptom}
)} {p.explanation && (
Förklaring
{p.explanation}
)} {p.example && (
Exempel
{p.example.map((line, j) => ( (borde vara $1)') .replace(/← FEL/g, '← FEL') }}/> ))}
)} {p.info && (
Info
{p.info}
)} {p.action && (
Åtgärd
$1')}}/>
)} {p.solution && (
Lösning
$1') }}/> {p.solutionBullets && (
    {p.solutionBullets.map((b,j)=>(
  • $1')}}/>
  • ))}
)}
)} {p.solutionSteps && (
Lösning
    {p.solutionSteps.map((s,j)=>
  1. {s}
  2. )}
)} {p.test && (
Test
{p.test}
)}
))} {/* Quick troubleshooting */}

{data.quickTroubleshooting.heading}

{data.quickTroubleshooting.items.map((it,i)=>(
{it.problem}
{it.solution}
))}
); // ─── FAQ section ─────────────────────────────────────────────────── const FaqBody = ({ data }) => (
{data.items.map((q,i)=>)}
); // ─── More info section ───────────────────────────────────────────── const MoreBody = ({ data }) => (
{data.expandables.map((e,i)=>( ))}
); const Expandable = ({ title, content }) => { const [open, setOpen] = React.useState(false); return (
{open && (
{content.warning && (
{content.warning}
)} {content.body &&
{content.body}
} {content.bullets && (
    {content.bullets.map((b,i)=>
  • {b}
  • )}
)} {content.rule && (
{content.rule}
)} {content.example && (
{content.example.heading}
{content.example.rows.map((r,i)=>(
{r.label} {r.value}
))}
)} {content.codeBlock && (
{content.codeBlock}
)}
)}
); }; // ─── Lightbox ────────────────────────────────────────────────────── const Lightbox = ({ src, alt, onClose }) => { React.useEffect(() => { const onKey = (e) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, []); return (
{alt}e.stopPropagation()}/>
); }; // ─── Main app ────────────────────────────────────────────────────── const SECTION_RENDERERS = { quickstart: QuickstartBody, commands: CommandsBody, 'flow-examples': FlowExamplesBody, 'multi-battery': MultiBatteryBody, 'peak-shaving': PeakShavingBody, troubleshooting: TroubleshootingBody, faq: FaqBody, more: MoreBody, }; function HelpApp(){ const [content, setContent] = React.useState(null); const [open, setOpen] = React.useState({}); const [lightbox, setLightbox] = React.useState(null); const [activeId, setActiveId] = React.useState('quickstart'); React.useEffect(() => { fetch('help-content.json') .then(r => r.json()) .then(d => { setContent(d); // Initialise open state from defaultOpen const o = {}; for (const k of Object.keys(d.sections)){ o[k] = !!d.sections[k].defaultOpen; } setOpen(o); }) .catch(e => console.error('Failed to load help content', e)); }, []); // Scroll-spy React.useEffect(() => { if (!content) return; const onScroll = () => { const fromTop = 130; let cur = activeId; for (const id of Object.keys(content.sections)){ const el = document.getElementById(id); if (!el) continue; const r = el.getBoundingClientRect(); if (r.top < fromTop) cur = id; } if (cur !== activeId) setActiveId(cur); }; window.addEventListener('scroll', onScroll, {passive:true}); return () => window.removeEventListener('scroll', onScroll); }, [content, activeId]); if (!content){ return (
Laddar hjälp…
); } const toggle = (id) => setOpen(o => ({...o, [id]: !o[id]})); const openOnly = (id) => { setOpen(o => ({...o, [id]: true})); setTimeout(() => { const el = document.getElementById(id); if (el) el.scrollIntoView({behavior:'smooth', block:'start'}); }, 30); }; return (
Gravio

{content.meta.title}

{content.meta.subtitle}

{content.meta.intro.heading}

{content.meta.intro.body}

{content.sectionNav.map(navItem => { const data = content.sections[navItem.id]; if (!data) return null; const Body = SECTION_RENDERERS[navItem.id]; return (
toggle(navItem.id)}> {Body && setLightbox({src,alt})}/>}
); })}

Snabblänkar

{content.quickLinks.map((ql, i) => ( {ql.label} ))}
{lightbox && setLightbox(null)}/>}
); } ReactDOM.createRoot(document.getElementById('root')).render();