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) <noreply@anthropic.com>
This commit is contained in:
parent
7a83b06a3f
commit
65fd532dbe
2 changed files with 192 additions and 0 deletions
79
README.md
Normal file
79
README.md
Normal file
|
|
@ -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`.
|
||||||
113
assets/js/deck.js
Normal file
113
assets/js/deck.js
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
Loading…
Add table
Reference in a new issue