From 65fd532dbe8e155ce950c1094d9dc251bef269ca Mon Sep 17 00:00:00 2001 From: Jonathan Hvid Date: Thu, 18 Jun 2026 12:16:15 +0200 Subject: [PATCH] deck: navigation runtime + docs Bottom dot-nav built from the slides (Danish hover labels, every slide reachable), IntersectionObserver scroll-spy + slide counter, keyboard nav, and reveal-on-enter that degrades gracefully without JS. README covers how to preview, the slide map, and the placeholdered logos. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 79 ++++++++++++++++++++++++++++++++ assets/js/deck.js | 113 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 README.md create mode 100644 assets/js/deck.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..c6fdf9f --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# 001 Bifrost Advisory Meeting + +En fuldskærms, scroll-snap HTML-præsentation til Fenja AI’s første +Advisory Board-møde. Statisk site — ingen build, ingen server, ingen +afhængigheder. Alt synligt indhold er på dansk (med de få engelske +brand-fraser, der er valgt bevidst, fx *Sovereign. Trusted. Innovative.*). + +## Åbn / preview + +Åbn `index.html` direkte i en browser — det virker uden server. + +Vil du undgå browserens lokale fil-restriktioner (anbefales), så start en +simpel lokal server fra projektmappen: + +```bash +python3 -m http.server 8000 +# åbn derefter http://localhost:8000 +``` + +Vis den gerne på en stor mødeskærm. Slide 1 (ambient velkomst) er +designet til at køre i baggrunden, mens folk ankommer. + +## Navigation + +- **Scroll** — hjulet/touchpad snapper til hver slide. +- **Bund-menuen** — 17 prikker; hold musen over en prik for at se det + danske navn, og klik for at hoppe direkte til den slide. +- **Tastatur** — ↑/↓, PageUp/PageDown, mellemrum, Home/End. + +## Struktur + +| # | Slide | Menu-label | +|---|-------|-----------| +| 1 | Ambient velkomst (rainbow-baggrund + Bifrost-logo) | Velkomst | +| 2 | Agenda | Agenda | +| 3 | Introduktioner — Advisory Board | Introduktioner | +| 4 | Fenja AI mission (Fenja · Bifrost + partnerlogoer) | Mission | +| 5 | Stort spørgsmål — success | Spørgsmål | +| 6 | Pause | Pause | +| 7 | Platformarkitektur | Arkitektur | +| 8 | Spørgsmål — features | Spørgsmål | +| 9 | Spørgsmål — barrierer | Spørgsmål | +| 10 | Sektionstitel — Suverænitet, Sikkerhed & Governance | Suverænitet | +| 11 | Highlight-bokse (Cloud Act, geopolitik, Fable 5, lock-in) | Risici | +| 12 | Meme | Meme | +| 13 | Spørgsmål — sikker brug | Spørgsmål | +| 14 | Vores tilgang (+ plads til punkter) | Vores tilgang | +| 15 | Regulatorisk sandkasse | Sandkasse | +| 16 | Spørgsmål — krav & bekymringer | Spørgsmål | +| 17 | Tak for i dag | Tak | + +## Filer + +``` +index.html alle 17 slides +assets/css/tokens.css designsystem (farver, type, fonts) — fra Fenja AI +assets/css/deck.css deck-layout, spørgsmåls-komponent, slide-styles +assets/js/deck.js dot-nav, scroll-spy, tastatur, reveal +assets/fonts/ Manrope + Newsreader (variable) +assets/board/ portrætter af Advisory Board +assets/img/ Fenja-wordmark + meme +``` + +## Genbrugt fra det gamle projekt + +- Designsystemet (`colors_and_type.css`) — farver, type, fonts. +- Bifrost-regnbuen (aurora-buen) — genbrugt som logo-element og som + animeret ambient-baggrund. +- Advisory Board-portrætter + navne. +- Platformarkitekturen (Foundation / Tools / Agents) — gengivet som et + roligt, statisk diagram. +- Fenja-wordmark og meme. + +## Manglende assets + +Partnerlogoerne (Innovationsfonden, BioInnovationsfonden, Datatilsynet, +Digitaliseringsstyrelsen, Gefion) findes ikke i projektet og er vist som +rene, tydeligt markerede pladsholdere i korrekt størrelse. Drop de rigtige +SVG/PNG-filer i `assets/logos/` og erstat pladsholderne i `index.html`. diff --git a/assets/js/deck.js b/assets/js/deck.js new file mode 100644 index 0000000..51b53c5 --- /dev/null +++ b/assets/js/deck.js @@ -0,0 +1,113 @@ +// ───────────────────────────────────────────────────────────── +// deck.js — Bifrost Advisory Board presentation runtime +// +// 1. Builds the bottom dot-nav from the slides in the DOM (one +// dot per slide, Danish label from data-label). +// 2. Scroll-snap is CSS-driven; here we keep the active dot + +// slide counter in sync via an IntersectionObserver, and +// smooth-scroll to a slide when its dot is clicked. +// 3. Adds .in-view to the active slide so its .reveal children +// animate in. +// 4. Keyboard: ↑/↓, PageUp/PageDown, Home/End jump between slides. +// +// No external libraries; no inline scripts (CSP-friendly). +// ───────────────────────────────────────────────────────────── +(function () { + 'use strict'; + + // Mark that JS is live — the .reveal hiding rule is gated on this, so a + // failed/blocked script leaves the deck fully visible rather than blank. + document.documentElement.classList.add('js'); + + const deck = document.getElementById('deck'); + const nav = document.getElementById('dot-nav'); + const counter = document.getElementById('counter-now'); + const slides = Array.from(document.querySelectorAll('.slide')); + if (!deck || !nav || !slides.length) return; + + const pad = (n) => String(n).padStart(2, '0'); + + // ─── Build the dot-nav ─────────────────────────────────────── + const dots = slides.map((slide, i) => { + const btn = document.createElement('button'); + btn.className = 'dot-btn'; + btn.type = 'button'; + btn.dataset.index = String(i); + btn.setAttribute('aria-label', `${i + 1}. ${slide.dataset.label || ''}`.trim()); + + const label = document.createElement('span'); + label.className = 'label'; + label.textContent = slide.dataset.label || `Slide ${i + 1}`; + + const dot = document.createElement('span'); + dot.className = 'dot'; + + btn.appendChild(label); + btn.appendChild(dot); + btn.addEventListener('click', () => goTo(i)); + nav.appendChild(btn); + return btn; + }); + + let activeIndex = 0; + + function setActive(i) { + if (i === activeIndex) return; + activeIndex = i; + dots.forEach((d, j) => d.classList.toggle('is-active', j === i)); + if (counter) counter.textContent = pad(i + 1); + } + + function goTo(i) { + const target = slides[i]; + if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + + // ─── Active-slide tracking + reveal ────────────────────────── + const io = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + const i = slides.indexOf(entry.target); + if (entry.isIntersecting) { + entry.target.classList.add('in-view'); + // A slide counts as "active" once it owns most of the viewport. + if (entry.intersectionRatio >= 0.5) setActive(i); + } + }); + }, { root: deck, threshold: [0, 0.5, 0.9] }); + + slides.forEach((s) => io.observe(s)); + + // First slide is visible immediately on load. + slides[0].classList.add('in-view'); + setActive(0); + if (counter) counter.textContent = pad(1); + + // ─── Keyboard navigation ───────────────────────────────────── + window.addEventListener('keydown', (e) => { + // Ignore when a control/input has focus (none here, but future-proof). + const tag = (e.target && e.target.tagName) || ''; + if (tag === 'INPUT' || tag === 'TEXTAREA') return; + + switch (e.key) { + case 'ArrowDown': + case 'PageDown': + case ' ': + e.preventDefault(); + goTo(Math.min(activeIndex + 1, slides.length - 1)); + break; + case 'ArrowUp': + case 'PageUp': + e.preventDefault(); + goTo(Math.max(activeIndex - 1, 0)); + break; + case 'Home': + e.preventDefault(); + goTo(0); + break; + case 'End': + e.preventDefault(); + goTo(slides.length - 1); + break; + } + }); +})();