// Shared building blocks for the Demenskalender Family-app // Light, modern, calm Scandinavian aesthetic. const TOKENS = { bg: '#FAF8F4', // warm off-white surface: '#FFFFFF', surfaceAlt: '#F2EEE7', // soft sand border: '#E5DFD4', borderStrong: '#D4CCBC', ink: '#1F2733', // ink near-black with cool tilt inkMuted: '#5C6573', inkSoft: '#8A8F99', primary: '#3F7A82', // dusty teal primaryHover: '#346168', primarySoft: '#E3EEF0', accent: '#C98A3A', // warm amber accentSoft: '#F6E8D2', danger: '#B5483F', success: '#5C8B5C', fontDisplay: '"Inter Tight", "Inter", system-ui, sans-serif', fontBody: '"Inter", system-ui, sans-serif', fontMono: '"JetBrains Mono", ui-monospace, monospace', }; // Global font import + base CSS injection if (typeof document !== 'undefined' && !document.getElementById('app-base-styles')) { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Inter+Tight:wght@500;600;700&family=JetBrains+Mono:wght@400;500&display=swap'; document.head.appendChild(link); const s = document.createElement('style'); s.id = 'app-base-styles'; s.textContent = ` .app-root { font-family: ${TOKENS.fontBody}; color: ${TOKENS.ink}; -webkit-font-smoothing: antialiased; } .app-root *, .app-root *::before, .app-root *::after { box-sizing: border-box; } .app-display { font-family: ${TOKENS.fontDisplay}; letter-spacing: -0.01em; } .app-mono { font-family: ${TOKENS.fontMono}; } .app-btn { font-family: inherit; cursor: pointer; border: none; } .app-btn:focus-visible { outline: 2px solid ${TOKENS.primary}; outline-offset: 2px; } .app-input { font-family: inherit; font-size: 14px; padding: 10px 12px; border-radius: 8px; border: 1px solid ${TOKENS.border}; background: ${TOKENS.surface}; color: ${TOKENS.ink}; width: 100%; } .app-input:focus { outline: none; border-color: ${TOKENS.primary}; box-shadow: 0 0 0 3px ${TOKENS.primarySoft}; } /* Demo-mode: alle inputs er låste, viser pegefinger ved hover så de stadig føles klikbare */ .app-root input, .app-root textarea { caret-color: transparent !important; } .app-root input:focus, .app-root textarea:focus { outline: none !important; box-shadow: none !important; border-color: ${TOKENS.border} !important; } @keyframes app-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.4; transform: scale(0.85); } } .app-card { background: ${TOKENS.surface}; border: 1px solid ${TOKENS.border}; border-radius: 14px; } .app-divider { height: 1px; background: ${TOKENS.border}; } .app-scroll::-webkit-scrollbar { width: 8px; height: 8px; } .app-scroll::-webkit-scrollbar-thumb { background: ${TOKENS.borderStrong}; border-radius: 8px; } .app-scroll::-webkit-scrollbar-track { background: transparent; } /* ── Mobile (≤760px) ──────────────────────────────────────────── */ @media (max-width: 760px) { /* Mobile: drop fixed-viewport scroll lock — let page flow naturally */ html, body { height: auto !important; overflow: auto !important; } #root { height: auto !important; } .app-root { height: auto !important; min-height: 100vh; overflow: visible !important; } .app-root > div { overflow: visible !important; } .app-burger { display: flex !important; } .app-sidebar { position: fixed !important; top: 0; left: 0; bottom: 0; z-index: 60; width: 280px !important; max-width: 86vw; transform: translateX(-100%); transition: transform 0.25s ease; box-shadow: 0 8px 32px rgba(20,30,50,0.18); } .app-sidebar.is-open { transform: translateX(0); } .app-sidebar-close { display: flex !important; } /* Inner scroll containers: release fixed-height scroll, flow with body */ .app-scroll { overflow: visible !important; flex: none !important; } /* Tighter padding inside main content on mobile */ .app-main-pad { padding: 70px 16px 24px !important; } .app-main-pad-tight { padding: 70px 14px 18px !important; } .app-topbar { padding: 16px 16px 14px 64px !important; } .app-topbar-edit { padding: 16px 16px 14px 64px !important; } .app-topbar-edit .app-topbar-actions { gap: 6px !important; } .app-topbar-edit .app-topbar-actions .app-changes-count { display: none; } /* Stack two-column layouts */ .app-grid-main { grid-template-columns: 1fr !important; padding: 70px 14px 24px !important; gap: 16px !important; } .app-stats-3 { grid-template-columns: 1fr 1fr 1fr !important; gap: 8px !important; } /* Edit screen: smaller hero metrics */ .app-edit-scroll { padding: 18px 16px 32px !important; } /* Documents: stack folder list above file list */ .app-docs-grid { grid-template-columns: 1fr !important; padding: 70px 14px 24px !important; gap: 16px !important; } .app-docs-folders { flex-direction: row !important; gap: 10px !important; overflow-x: auto; padding: 4px 14px 14px; margin: 0 -14px; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch; scrollbar-width: thin; position: relative; } .app-docs-folders::-webkit-scrollbar { height: 4px; } .app-docs-folders::-webkit-scrollbar-thumb { background: ${TOKENS.borderStrong}; border-radius: 4px; } .app-docs-folders::-webkit-scrollbar-track { background: transparent; } .app-docs-folders > div:first-child { display: flex !important; align-items: center; gap: 6px; flex: 0 0 auto !important; padding: 0 4px 0 0 !important; font-size: 11px !important; color: ${TOKENS.inkSoft}; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; white-space: nowrap; align-self: center; } .app-docs-folders .app-docs-label-desktop { display: none; } .app-docs-folders .app-docs-label-mobile { display: inline-flex !important; } .app-docs-folders button { flex: 0 0 78%; max-width: 280px; scroll-snap-align: start; } .app-docs-fade { display: block !important; position: sticky; right: 0; top: 0; bottom: 0; flex: 0 0 36px; align-self: stretch; margin-left: -36px; background: linear-gradient(to right, transparent, ${TOKENS.bg} 80%); pointer-events: none; } .app-docs-size { display: none; } } `; document.head.appendChild(s); // Demo-mode: gør alle inputs readonly så brugeren ikke kan skrive i felterne const lockInputs = (root) => { root.querySelectorAll('input, textarea').forEach(el => { const t = (el.getAttribute('type') || 'text').toLowerCase(); if (t === 'checkbox' || t === 'radio' || t === 'button' || t === 'submit') { el.addEventListener('click', e => e.preventDefault()); } else { el.readOnly = true; } }); }; const initLock = () => { lockInputs(document); new MutationObserver(muts => { muts.forEach(m => m.addedNodes.forEach(n => { if (n.nodeType === 1) lockInputs(n); })); }).observe(document.body, { childList: true, subtree: true }); }; if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initLock); else initLock(); } // ── Icons (minimal stroke set) ───────────────────────────────────── const Icon = ({ name, size = 18, color = 'currentColor', strokeWidth = 1.75 }) => { const paths = { home: 'M3 11l9-8 9 8M5 9.5V20a1 1 0 001 1h4v-6h4v6h4a1 1 0 001-1V9.5', calendar: 'M4 6h16v14H4zM4 10h16M8 3v4M16 3v4', note: 'M5 4h11l4 4v12H5zM16 4v4h4', tv: 'M3 5h18v12H3zM8 21h8M12 17v4', cake: 'M5 21h14v-8H5zM12 13V9M9 5a3 3 0 016 0M5 17h14', phone: 'M5 4h4l2 5-3 2a11 11 0 005 5l2-3 5 2v4a2 2 0 01-2 2A16 16 0 013 6a2 2 0 012-2z', plus: 'M12 5v14M5 12h14', edit: 'M4 20h4l10-10-4-4L4 16zM14 6l4 4', settings: 'M12 8a4 4 0 100 8 4 4 0 000-8zM19 12c0 .5-.05 1-.13 1.5l2.07 1.5-2 3.46-2.4-.97c-.78.62-1.66 1.1-2.6 1.4l-.34 2.6h-3l-.34-2.6c-.95-.3-1.83-.78-2.6-1.4l-2.4.97-2-3.46 2.07-1.5C5.05 13 5 12.5 5 12s.05-1 .13-1.5L3.06 9 5.06 5.54l2.4.97c.78-.62 1.66-1.1 2.6-1.4l.34-2.6h3l.34 2.6c.95.3 1.83.78 2.6 1.4l2.4-.97 2 3.46-2.07 1.5c.08.5.13 1 .13 1.5z', chevronRight: 'M9 6l6 6-6 6', chevronLeft: 'M15 6l-9 6 9 6', check: 'M5 12l4 4 10-10', x: 'M6 6l12 12M18 6L6 18', clock: 'M12 6v6l4 2M12 22a10 10 0 110-20 10 10 0 010 20z', user: 'M12 12a4 4 0 100-8 4 4 0 000 8zM4 21a8 8 0 0116 0', bell: 'M6 16V11a6 6 0 0112 0v5l2 2H4zM10 20a2 2 0 004 0', screen: 'M3 4h18v12H3zM8 20h8M12 16v4', history: 'M3 12a9 9 0 109-9 9 9 0 00-7 3.5M3 4v4h4M12 7v5l3 2', activity: 'M3 12h4l3-9 4 18 3-9h4', mic: 'M12 2a3 3 0 00-3 3v6a3 3 0 006 0V5a3 3 0 00-3-3zM5 11a7 7 0 0014 0M12 18v3', eye: 'M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7zM12 9a3 3 0 100 6 3 3 0 000-6z', arrowRight: 'M5 12h14M13 6l6 6-6 6', pill: 'M10.5 13.5l3-3M5.5 8.5a4 4 0 015.66-5.66l5.5 5.5a4 4 0 01-5.66 5.66l-5.5-5.5zM3 21l4-4', folder: 'M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V7z', file: 'M14 3H6a2 2 0 00-2 2v14a2 2 0 002 2h12a2 2 0 002-2V9zM14 3v6h6', download: 'M12 4v12M7 11l5 5 5-5M4 20h16', droplet: 'M12 3l5 7a6 6 0 11-10 0z', nurse: 'M9 10h6M12 7v6M5 21a7 7 0 0114 0M12 3a4 4 0 100 8 4 4 0 000-8z', search: 'M11 4a7 7 0 105 12l5 5M11 4a7 7 0 017 7', upload: 'M12 20V8M7 13l5-5 5 5M4 4h16', }; return ( ); }; // ── Avatar (initials) ───────────────────────────────────────────── const Avatar = ({ name, size = 28, tone = 'teal' }) => { const initials = name.split(' ').map(s => s[0]).slice(0, 2).join('').toUpperCase(); const tones = { teal: { bg: '#D8E7E9', fg: '#2F5C61' }, amber: { bg: '#F2DCB6', fg: '#7A5418' }, rose: { bg: '#EDD3CE', fg: '#7A3F37' }, olive: { bg: '#DCE2C7', fg: '#5C6638' }, slate: { bg: '#D9DCE3', fg: '#3F4858' }, }; const t = tones[tone] || tones.teal; return (
{initials}
); }; // ── Button ────────────────────────────────────────────────────────── const Button = ({ children, variant = 'primary', size = 'md', icon, iconRight, onClick, style, full }) => { const sizes = { sm: { padding: '7px 12px', fontSize: 13, gap: 6 }, md: { padding: '10px 16px', fontSize: 14, gap: 8 }, lg: { padding: '13px 20px', fontSize: 15, gap: 10 }, }; const variants = { primary: { background: TOKENS.primary, color: '#fff' }, secondary: { background: TOKENS.surface, color: TOKENS.ink, boxShadow: `inset 0 0 0 1px ${TOKENS.border}` }, ghost: { background: 'transparent', color: TOKENS.inkMuted }, danger: { background: 'transparent', color: TOKENS.danger, boxShadow: `inset 0 0 0 1px ${TOKENS.border}` }, }; return ( ); }; // ── Tag / Pill ───────────────────────────────────────────────────── const Pill = ({ children, tone = 'neutral' }) => { const tones = { neutral: { bg: TOKENS.surfaceAlt, fg: TOKENS.inkMuted }, primary: { bg: TOKENS.primarySoft, fg: TOKENS.primaryHover }, accent: { bg: TOKENS.accentSoft, fg: '#7A5418' }, success: { bg: '#DCE7DC', fg: '#3F6A3F' }, }; const t = tones[tone]; return ( {children} ); }; // ── Sample data ───────────────────────────────────────────────────── const PEOPLE = { lene: { name: 'Lene M.', role: 'datter', tone: 'teal' }, morten: { name: 'Morten K.', role: 'barnebarn', tone: 'amber' }, anna: { name: 'Anna S.', role: 'datter', tone: 'rose' }, pleje: { name: 'Pleje, Sofie', role: 'plejer', tone: 'olive' }, }; const TODAY = { weekday: 'Torsdag', date: '8. maj 2026', appointments: [ { id: 1, time: '10:00', title: 'Lene kommer på besøg', who: 'Lene M.', tone: 'primary' }, { id: 2, time: '14:30', title: 'Øjenlæge — husk brillerne', who: 'Aalborg', tone: 'accent' }, ], routines: [ { id: 1, time: '08:00', title: 'Husk morgenpiller', type: 'pille', who: 'med morgenmaden' }, { id: 2, time: '10:00', title: 'Drik et stort glas vand', type: 'vand', who: 'efter morgenmad' }, { id: 3, time: '13:00', title: 'Hjemmesygeplejerske', type: 'pleje', who: 'Sofie kigger forbi' }, { id: 4, time: '20:00', title: 'Aftenpiller', type: 'pille', who: 'med vand' }, ], notes: [ { id: 1, text: 'Drik et stort glas vand om morgenen', by: 'Lene', when: 'i går' }, { id: 2, text: 'Skraldemand kommer fredag', by: 'Anna', when: '2 dage siden' }, { id: 3, text: 'Vandkanden står på køkkenbordet', by: 'Lene', when: '3 dage siden' }, ], birthdays: [ { id: 1, name: 'Morten', age: 32, when: 'Om 4 dage' }, ], }; const ACTIVITY = [ { who: 'Lene M.', tone: 'teal', action: 'tilføjede aftale', detail: 'Øjenlæge kl. 14:30', when: '14 min siden' }, { who: 'Lene M.', tone: 'teal', action: 'tilføjede note', detail: '"Drik et stort glas vand…"', when: 'i går 09:12' }, { who: 'Anna S.', tone: 'rose', action: 'redigerede note', detail: '"Skraldemand kommer fredag"',when: '2 dage siden' }, { who: 'Sofie (pleje)', tone: 'olive', action: 'startede videoopkald', detail: 'varede 4 min', when: '3 dage siden' }, { who: 'Morten K.', tone: 'amber', action: 'tilføjede fødselsdag', detail: 'Morten — 32 år', when: '5 dage siden' }, ]; const DOCS = { folders: [ { id: 'recepter', label: 'Recepter', count: 4, hint: 'Senest opdateret 12. apr' }, { id: 'hospital', label: 'Hospital', count: 6, hint: 'Aalborg Sygehus, øjenafd.' }, { id: 'læge', label: 'Egen læge', count: 3, hint: 'Dr. Birgitte Holm' }, { id: 'pleje', label: 'Hjemmepleje', count: 5, hint: 'Aftaler og besøgsplan' }, { id: 'andet', label: 'Andet', count: 2, hint: 'Forsikring, fuldmagter' }, ], files: { recepter: [ { id: 1, title: 'Donepezil 10 mg — daglig dosis', type: 'PDF', from: 'Apoteket Aalborg', date: '12. apr 2026', size: '142 kB' }, { id: 2, title: 'Memantin 20 mg', type: 'PDF', from: 'Apoteket Aalborg', date: '03. apr 2026', size: '128 kB' }, { id: 3, title: 'Blodtryksmedicin — Ramipril', type: 'PDF', from: 'Dr. Birgitte Holm', date: '21. mar 2026', size: '96 kB' }, { id: 4, title: 'Sovemedicin — kun ved behov', type: 'PDF', from: 'Dr. Birgitte Holm', date: '14. feb 2026', size: '88 kB' }, ], hospital: [ { id: 1, title: 'Indkaldelse — øjenafdelingen 8. maj', type: 'PDF', from: 'Aalborg Sygehus', date: '15. apr 2026', size: '210 kB' }, { id: 2, title: 'Udskrivningsbrev — januar 2026', type: 'PDF', from: 'Aalborg Sygehus', date: '28. jan 2026', size: '0,9 MB' }, { id: 3, title: 'MR-scanning — svar', type: 'PDF', from: 'Aalborg Sygehus', date: '12. dec 2025', size: '1,4 MB' }, { id: 4, title: 'Vejledning ved svimmelhed', type: 'PDF', from: 'Geriatrisk afd.', date: '03. nov 2025', size: '180 kB' }, { id: 5, title: 'Pårørendebrev — demens-udredning', type: 'PDF', from: 'Geriatrisk afd.', date: '12. okt 2025', size: '320 kB' }, { id: 6, title: 'Foto af recept — Memantin', type: 'JPG', from: 'Lene M.', date: '20. sep 2025', size: '2,1 MB' }, ], læge: [ { id: 1, title: 'Årsstatus 2026 — kognitive prøver', type: 'PDF', from: 'Dr. Birgitte Holm', date: '08. apr 2026', size: '240 kB' }, { id: 2, title: 'Henvisning til hjemmepleje', type: 'PDF', from: 'Dr. Birgitte Holm', date: '14. feb 2026', size: '120 kB' }, { id: 3, title: 'Vaccinations­oversigt', type: 'PDF', from: 'Dr. Birgitte Holm', date: '02. nov 2025', size: '85 kB' }, ], pleje: [ { id: 1, title: 'Besøgsplan — uge 19', type: 'PDF', from: 'Hjemmeplejen Aalborg', date: '04. maj 2026', size: '64 kB' }, { id: 2, title: 'Kontaktliste — vagtplan', type: 'PDF', from: 'Hjemmeplejen Aalborg', date: '01. apr 2026', size: '52 kB' }, { id: 3, title: 'Daglig rutine — bad og påklædning', type: 'PDF', from: 'Sofie (pleje)', date: '12. mar 2026', size: '110 kB' }, { id: 4, title: 'Notat efter besøg 2. maj', type: 'TXT', from: 'Sofie (pleje)', date: '02. maj 2026', size: '4 kB' }, { id: 5, title: 'Kontaktoplysninger — vikar', type: 'PDF', from: 'Hjemmeplejen Aalborg', date: '20. feb 2026', size: '32 kB' }, ], andet: [ { id: 1, title: 'Fuldmagt — pårørende', type: 'PDF', from: 'Familieadvokaten', date: '10. jan 2026', size: '380 kB' }, { id: 2, title: 'Forsikringspolice — sundhed', type: 'PDF', from: 'Tryg Forsikring', date: '02. jan 2026', size: '1,2 MB' }, ], }, }; // expose Object.assign(window, { TOKENS, Icon, Avatar, Button, Pill, PEOPLE, TODAY, ACTIVITY, DOCS });