@graphlearning/shell 0.10.1 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/types.ts","../src/useSlideScale.ts","../src/SlidePanel.tsx","../src/NarrationBar.tsx","../src/SectionView.tsx","../src/SiteHeader.tsx","../src/CourseIndex.tsx","../src/useNarration.ts","../src/useTheme.ts","../src/ThemeToggle.tsx","../src/ConceptApp.tsx"],"sourcesContent":["// The content model, owned by the shell because the shell is what renders it. A content repo\n// authors data that satisfies these types; it no longer declares them (every repo's copy of\n// content/types.ts was byte-identical, which is the definition of a shared contract).\n//\n// concept ⊃ course ⊃ section. A SECTION is the atomic unit of a video: one slug (course-section),\n// one scene, one slide, one narration clip.\n//\n// The authoring contract: .md (source of truth) → (.slide, .tts)\n// slide = terse markdown shown in the SlidePanel (for the eye)\n// narration = natural-flow spoken script for TTS / Chatterbox (for the ear)\n\nexport interface Section {\n id: string // \"what-is-cloud\" → slug: foundations-what-is-cloud\n title: string\n scene: string // scene id shown behind the panel (may be shared across sections)\n focus?: string // node the slide floats clear of (optional)\n slide: string // markdown → rendered in the SlidePanel\n narration: string // natural-flow TTS script (Chatterbox)\n}\n\nexport interface Course {\n id: string\n title: string\n sections: Section[]\n}\n\n// How a content repo hands its scenes to the shell. The shell never imports a scene registry — it\n// is given this lookup, so scenes stay entirely the repo's business.\nexport type SceneLookup = (id: string) => import('@graphlearning/flow').Scene | undefined\n\n// The slug for a section is `<courseId>-<sectionId>` — section IS the unit (one slide, one\n// narration), so no trailing beat index.\nexport function slugOf(course: Course, section: Section): string {\n return `${course.id}-${section.id}`\n}\n\nexport function allSections(course: Course): { section: Section; slug: string }[] {\n return course.sections.map((section) => ({ section, slug: slugOf(course, section) }))\n}\n","import { useLayoutEffect, useRef, useState } from 'react'\n\n// The slide is authored at a fixed DESIGN width and scaled with CSS `zoom` to the live pane width,\n// so it looks pixel-proportional at any frame — 1440 preview, 1080p, or 4K capture alike. This is\n// what makes the slide's dimensions a function of the TARGET video frame, not the browser window.\n//\n// DESIGN_PANE = 806 = 42% of a 1920-wide design frame (the slide pane's share). zoom = live/806:\n// 1440 window → pane ~605 → zoom 0.75 | 1080p → 806 → 1.0 | 4K → 1613 → 2.0\nexport const DESIGN_PANE = 806\n\nexport function useSlideScale() {\n const ref = useRef<HTMLElement>(null)\n const [zoom, setZoom] = useState(1)\n useLayoutEffect(() => {\n const el = ref.current\n if (!el) return\n const measure = () => setZoom(el.clientWidth / DESIGN_PANE)\n measure()\n const ro = new ResizeObserver(measure)\n ro.observe(el)\n return () => ro.disconnect()\n }, [])\n return { ref, zoom }\n}\n","import ReactMarkdown from 'react-markdown'\nimport remarkGfm from 'remark-gfm'\nimport { DESIGN_PANE, useSlideScale } from './useSlideScale'\n\n// The slide (a section's `.slide` markdown) — a full-height surface fixed to the RIGHT, lifted off\n// the scene canvas for figure-ground. Position is CSS-driven (see index.css):\n// landscape (laptop → video) — a 42% column beside the scene.\n// portrait (mobile → reel) — an off-canvas drawer that slides in from the right when `open`.\n// The inner scaler is authored at DESIGN_PANE px and `zoom`-scaled to the live pane, so the slide is\n// proportional to the target video frame at any size (see useSlideScale).\nexport function SlidePanel({ slide, open }: { slide: string; open: boolean }) {\n const { ref, zoom } = useSlideScale()\n return (\n <aside ref={ref} className={`slide-panel${open ? ' slide-panel--open' : ''}`}>\n <div className=\"slide-panel__scaler\" style={{ width: DESIGN_PANE, zoom }}>\n <ReactMarkdown remarkPlugins={[remarkGfm]}>{slide}</ReactMarkdown>\n </div>\n </aside>\n )\n}\n","import { useEffect, useRef, useState } from 'react'\n\n// The narration scrub bar — elapsed / position / duration for the current section's clip, in the\n// footer control cluster beside the volume toggle. Interactive ONLY: it lives inside SectionView's\n// `!capture` footer, so it can never appear in a recorded frame (same guarantee as the pager and the\n// theme toggle beside it).\n//\n// WHY IT TAKES THE ELEMENT, NOT STATE: `timeupdate` fires ~4x a second. Lifting that into\n// ConceptApp would re-render the whole section — SceneView (layout + react-flow) and the\n// react-markdown slide — four times a second for a 4px bar. So useNarration hands out the one\n// <audio> element and this component subscribes to it directly; the only thing that re-renders on a\n// tick is the bar. Nothing else in the shell learns that playback has a position.\n//\n// A section whose clip is missing or not generated yet (docker, java) has no duration, so the bar\n// renders NOTHING rather than a dead track — the volume toggle already carries that state.\n// The scrub step, in seconds — shared with ConceptApp's window-level ← / →, which seeks by the same\n// amount when a clip is loaded. One constant so the key does the same thing whether or not the track\n// happens to hold focus.\nexport const SEEK_S = 10\n\nexport function NarrationBar({ audio }: { audio: HTMLAudioElement | null }) {\n const [time, setTime] = useState(0)\n const [duration, setDuration] = useState(0)\n const trackRef = useRef<HTMLDivElement | null>(null)\n\n useEffect(() => {\n if (!audio) return\n const sync = () => {\n setTime(audio.currentTime)\n // A clip still loading (or 404'd, or unloaded between sections) reports NaN/Infinity.\n setDuration(Number.isFinite(audio.duration) ? audio.duration : 0)\n }\n sync()\n // `emptied` is the one that matters on navigation: useNarration removes the src for a section\n // with no clip, and without this the bar would keep showing the previous clip's length.\n const events = ['loadedmetadata', 'durationchange', 'timeupdate', 'seeked', 'ended', 'emptied', 'error'] as const\n events.forEach((e) => audio.addEventListener(e, sync))\n return () => events.forEach((e) => audio.removeEventListener(e, sync))\n }, [audio])\n\n if (!audio || !duration) return null\n\n const seekToClientX = (clientX: number) => {\n const el = trackRef.current\n if (!el) return\n const r = el.getBoundingClientRect()\n const f = Math.min(1, Math.max(0, (clientX - r.left) / r.width))\n audio.currentTime = f * duration\n setTime(audio.currentTime) // don't wait for the next timeupdate — the thumb must track the finger\n }\n\n // Arrow keys scrub by the same SEEK_S as the window-level handler; stopping propagation keeps\n // ConceptApp from ALSO seeking the same clip (it would double the step) and keeps Shift+← / →\n // from paging out of a section the reader is actively scrubbing. Every other key (Esc home, Space\n // narration) is left to bubble. Home/End are handled only here, where the track has focus.\n const onKeyDown = (e: React.KeyboardEvent) => {\n const step = (d: number) => {\n e.preventDefault()\n e.stopPropagation()\n audio.currentTime = Math.min(duration, Math.max(0, audio.currentTime + d))\n setTime(audio.currentTime)\n }\n if (e.key === 'ArrowLeft') step(-SEEK_S)\n else if (e.key === 'ArrowRight') step(SEEK_S)\n else if (e.key === 'Home') step(-duration)\n else if (e.key === 'End') step(duration)\n }\n\n const pct = `${Math.min(100, (time / duration) * 100)}%`\n return (\n <span className=\"reel-foot__audio\">\n <span className=\"reel-foot__count reel-foot__time\">{fmt(time)}</span>\n <div\n ref={trackRef}\n className=\"reel-foot__track\"\n role=\"slider\"\n tabIndex={0}\n aria-label=\"Narration position\"\n aria-valuemin={0}\n aria-valuemax={Math.round(duration)}\n aria-valuenow={Math.round(time)}\n aria-valuetext={`${fmt(time)} of ${fmt(duration)}`}\n onKeyDown={onKeyDown}\n onPointerDown={(e) => {\n // Capture so a drag that leaves the 4px track keeps scrubbing (the bar is thin; fingers\n // and mice both wander off it).\n e.currentTarget.setPointerCapture(e.pointerId)\n seekToClientX(e.clientX)\n }}\n onPointerMove={(e) => {\n if (e.currentTarget.hasPointerCapture(e.pointerId)) seekToClientX(e.clientX)\n }}\n >\n <span className=\"reel-foot__fill\" style={{ width: pct }} />\n <span className=\"reel-foot__thumb\" style={{ left: pct }} />\n </div>\n <span className=\"reel-foot__count reel-foot__time\">{fmt(duration)}</span>\n </span>\n )\n}\n\n/** m:ss — clips are seconds-to-minutes, never hours. */\nfunction fmt(s: number) {\n const t = Number.isFinite(s) && s > 0 ? s : 0\n return `${Math.floor(t / 60)}:${String(Math.floor(t % 60)).padStart(2, '0')}`\n}\n","import { useState } from 'react'\nimport { Home, Volume2, VolumeX, ChevronLeft, ChevronRight, Moon, Sun } from 'lucide-react'\nimport { SceneView, type ThemeKey } from '@graphlearning/flow'\nimport type { Section, SceneLookup } from './types'\nimport { SlidePanel } from './SlidePanel'\nimport { NarrationBar } from './NarrationBar'\n\n// The SECTION view = the final composited output of a slug (course-section): the vertical scene +\n// the fixed-right slide, with an eyebrow + section title header. Layout is responsive (CSS):\n// landscape (laptop → 4K video) — scene fills the left, slide is a right column; the title lives\n// in the slide, so the header shows just the eyebrow.\n// portrait (mobile → reel) — scene fills the frame, slide is a full-width drawer toggled at\n// top-right; the header shows the full title card.\n// Under ?capture=1 the frame is clean for video: just the eyebrow + title + scene — NO footer and NO\n// interactive controls (the drawer toggle and the footer control bar are both suppressed).\n// Interactively, a footer control bar carries Home + narration + prev/next (raised above the drawer\n// so it works while the slide is open), the drawer toggle appears in portrait, and the eyebrow also\n// links back to the catalog. Keyboard works everywhere (← / → seek the narration ±10s, or page when\n// the section has no clip; Shift+← / → always page; Space toggles narration).\nexport function SectionView({\n section,\n getScene,\n capture = false,\n eyebrow,\n index = 0,\n total = 0,\n onHome,\n onPrev,\n onNext,\n narrating = false,\n onToggleNarration,\n narrationAudio = null,\n theme = 'dark',\n onToggleTheme,\n}: {\n section: Section\n getScene: SceneLookup // the repo's scene registry, injected — the shell owns no scenes\n capture?: boolean // ?capture=1: the footer shows GraphL + §n/N branding (video); else live controls\n eyebrow?: string // header eyebrow (e.g. \"PYTHON · SETUP\"); also the back-to-catalog link\n index?: number // 0-based position of this section in the course\n total?: number // total sections in the course\n onHome?: () => void // back to the catalog (wired to the eyebrow)\n onPrev?: () => void // previous section (same as Shift+←) — drives the footer nav\n onNext?: () => void // next section (same as Shift+→)\n narrating?: boolean // is the section clip currently playing (drives the volume icon)\n onToggleNarration?: () => void // play/pause narration (same as Space)\n // The narration channel's <audio>, handed straight to NarrationBar so playback position never\n // becomes React state up here — a 4Hz tick would re-render the scene and the slide. Absent (or a\n // section with no clip) → no bar.\n narrationAudio?: HTMLAudioElement | null\n // Forwarded straight to SceneView. Withheld from the barrel like the rest of SectionView, so the\n // only way a repo sets it is ConceptApp's prop — which is what keeps the scene pane and the\n // shell's chrome on one value.\n theme?: ThemeKey\n // Flips the theme from the section itself. SiteHeader (and its toggle) never renders here — a\n // section is a video frame — but the footer control bar is ALREADY interactive-only and suppressed\n // under ?capture=1, exactly like the pager and the drawer handle beside it. So the control lands in\n // the cluster that exists for precisely this: things a reader may touch and a recording never sees.\n onToggleTheme?: () => void\n}) {\n const [open, setOpen] = useState(false) // drawer state; only affects the portrait layout\n const scene = getScene(section.scene)\n if (!scene) return <div className=\"stage stage--missing\">no scene: {section.scene}</div>\n return (\n <div className=\"stage stage--section\">\n {/* Header/footer — shared with the captured video. The eyebrow is a button (back to catalog);\n the section title shows on the portrait title card and is hidden in landscape (it headlines\n the slide there). GraphL + §n/N sit along the bottom. */}\n <header className=\"reel-head\">\n {eyebrow && (\n <button className=\"reel-head__eyebrow\" onClick={onHome} aria-label=\"Back to catalog (Esc)\" title=\"Back to catalog (Esc)\">\n {eyebrow}\n </button>\n )}\n <h1 className=\"reel-head__title\">{section.title}</h1>\n </header>\n {/* Footer control bar — interactive ONLY (narration toggle + prev/next + position). Suppressed\n under ?capture=1 so the captured video frame stays clean: no GraphL wordmark, no counter,\n just the eyebrow/title + scene. Raised above the portrait drawer so nav works while the\n slide is open. */}\n {!capture && (\n <footer className=\"reel-foot reel-foot--controls\">\n <span className=\"reel-foot__grp\">\n <button className=\"reel-foot__ctrl\" onClick={onHome} aria-label=\"Back to catalog (Esc)\" title=\"Back to catalog (Esc)\">\n <Home size={17} />\n </button>\n <button\n className=\"reel-foot__ctrl\"\n onClick={onToggleNarration}\n aria-label={narrating ? 'Pause narration (Space)' : 'Play narration (Space)'}\n title={narrating ? 'Pause narration (Space)' : 'Play narration (Space)'}\n >\n {narrating ? <Volume2 size={18} /> : <VolumeX size={18} />}\n </button>\n {/* Lucide here, a text glyph in SiteHeader's copy — deliberately. The bar is a port of\n ui-graphl and must match it character for character; this cluster's siblings are all\n lucide at 17-19px, and a text moon beside them would read as a different control. */}\n {onToggleTheme && (\n <button\n className=\"reel-foot__ctrl\"\n onClick={onToggleTheme}\n aria-label={theme === 'dark' ? 'Dark theme. Switch to light theme.' : 'Light theme. Switch to dark theme.'}\n title={theme === 'dark' ? 'Dark theme. Switch to light theme.' : 'Light theme. Switch to dark theme.'}\n >\n {theme === 'dark' ? <Moon size={17} /> : <Sun size={17} />}\n </button>\n )}\n </span>\n {/* Scrub bar — landscape only (CSS). In portrait the control row is already full at phone\n width, and the scene's bottom reserve (.stage--section .scene-area) is sized for ONE\n row: a second row would grow the footer up into the reel frame, and widening the\n reserve would re-frame every recorded portrait video. */}\n <NarrationBar audio={narrationAudio} />\n <span className=\"reel-foot__nav\">\n <button className=\"reel-foot__ctrl\" onClick={onPrev} aria-label=\"Previous section (Shift+←)\" title=\"Previous section (Shift+←)\">\n <ChevronLeft size={19} />\n </button>\n <span className=\"reel-foot__count reel-foot__count--live\">\n {index + 1} / {total}\n </span>\n <button className=\"reel-foot__ctrl\" onClick={onNext} aria-label=\"Next section (Shift+→)\" title=\"Next section (Shift+→)\">\n <ChevronRight size={19} />\n </button>\n </span>\n </footer>\n )}\n <div className=\"scene-area\">\n <SceneView scene={scene} focusId={section.focus} theme={theme} />\n </div>\n {/* Drawer toggle — portrait-only affordance to reveal the slide; suppressed at capture. */}\n {!capture && (\n <button\n className=\"slide-toggle\"\n onClick={() => setOpen((o) => !o)}\n aria-label={open ? 'Hide slide' : 'Show slide'}\n >\n {open ? '›' : '‹'}\n </button>\n )}\n <SlidePanel slide={section.slide} open={open} />\n </div>\n )\n}\n","import type { ReactNode } from 'react'\n\n// Platform chrome — the bar that sits above every GraphL page, here and at graphl.in. It is\n// deliberately ignorant of courses, scenes and this repo's subject: the whole point is that it\n// renders byte-identically on all eight sites, so a reader crossing from the catalog into a\n// concept app never leaves \"the site\". The repo's own name belongs in the <h1> below it.\n//\n// This is a PORT, not a new design. ui-graphl/index.html + styles.css are the original; the markup,\n// the class names and the CSS are the same, because two implementations of one design only stay one\n// design if neither drifts. When you change one, change the other.\n//\n// Not a component contract worth withholding: unlike SectionView, this has no route surface and the\n// recorder never sees it (it renders on the catalog page only), so exporting it costs nothing — and\n// python-lab, which is React but does not use ConceptApp, needs exactly this and nothing else.\n\n// The nav, mirroring catalog.json's `kinds`. Duplicated rather than fetched: the catalog lives at\n// graphl.in and this package is built separately, so reading it would mean a runtime fetch, a\n// loading state in the page's chrome, and a dev-server fallback — for a list that has been three\n// items all year. `nav` is a prop so a fourth section does not have to wait on seven repo upgrades.\nconst KINDS = [\n { id: 'courses', label: 'Courses' },\n { id: 'labs', label: 'Labs' },\n { id: 'coach', label: 'Coach' },\n]\n\n// The mark, inlined rather than referenced as /icon.svg. A content repo builds with `base` set to\n// its own subpath (\"/python/\"), so a relative src resolves somewhere else on every site and an\n// absolute one breaks the dev fixture; 335 bytes of SVG has neither problem, and nothing to flash.\nfunction Mark() {\n return (\n <svg className=\"site__logo\" width=\"28\" height=\"28\" viewBox=\"0 0 512 512\" aria-hidden=\"true\">\n <rect width=\"512\" height=\"512\" rx=\"80\" fill=\"#e8804f\" />\n <g fill=\"none\" stroke=\"#ffffff\" strokeWidth=\"58\" strokeLinecap=\"butt\" strokeLinejoin=\"miter\">\n <path d=\"M 373 200 A 130 130 0 1 0 373 312\" />\n <path d=\"M 283 312 L 373 312 L 373 410\" />\n </g>\n </svg>\n )\n}\n\nexport function SiteHeader({\n kind = 'courses',\n nav = KINDS,\n home = '/',\n actions,\n}: {\n // Which nav item is \"you are here\" — a catalog.json `kind` id. A content repo is 'courses';\n // python-lab is 'labs'. Defaulted so the seven content repos pass nothing at all.\n kind?: string\n nav?: { id: string; label: string }[]\n // Where the brand points. '/' is the root catalog and is correct for every app deployed under\n // graphl.in; it is a prop only because the ui-shell dev fixture is not deployed under it.\n home?: string\n // Right-hand slot. The theme toggle and the account control land here when they exist, injected\n // by whoever owns them — the shell itself will never import Firebase.\n actions?: ReactNode\n}) {\n return (\n <header className=\"site\">\n <div className=\"site__inner\">\n <a className=\"site__brand\" href={home}>\n <Mark />\n <span className=\"site__name\">GraphL</span>\n </a>\n {/* Real anchors to <home>#<kind>, the same links app.js writes at the catalog — so\n middle-click, copy-link and keyboard all work without a single handler here. */}\n <nav className=\"site__nav\" aria-label=\"Sections\">\n {nav.map((k) => (\n <a\n key={k.id}\n className=\"site__link\"\n href={`${home}#${k.id}`}\n {...(k.id === kind ? { 'aria-current': 'page' as const } : {})}\n >\n {k.label}\n </a>\n ))}\n </nav>\n <div className=\"site__actions\">{actions}</div>\n </div>\n </header>\n )\n}\n","import { useState } from 'react'\nimport type { Course } from './types'\nimport { allSections } from './types'\nimport { SiteHeader } from './SiteHeader'\nimport type { ReactNode } from 'react'\n\n// The landing page: the GraphL course catalog. Each course is an ACCORDION — a header row (number ·\n// title · section count) that expands to reveal its ordered sections; a section row routes to\n// `#/<courseId>-<sectionId>`. Kept deliberately spare so it scales to ~11 courses without clutter.\n// This is what an empty hash (`#/`) resolves to in App.tsx.\nexport function CourseIndex({\n courses,\n subject,\n kind,\n actions,\n}: {\n courses: Course[]\n subject: string\n kind?: string // which nav section this app belongs to; see SiteHeader\n // SiteHeader's right-hand slot — the theme toggle. It rides the catalog ONLY, which is the whole\n // reason a reader-facing theme control is compatible with a deck being a video: SiteHeader never\n // renders on a section, so the control can never appear in a captured frame.\n actions?: ReactNode\n}) {\n // Which courses are expanded (multiple may be open). All start collapsed so the catalog opens as a\n // compact one-row-per-course list — it stays tidy as courses grow to ~11.\n const [open, setOpen] = useState<Set<string>>(() => new Set())\n const toggle = (id: string) =>\n setOpen((prev) => {\n const next = new Set(prev)\n if (next.has(id)) next.delete(id)\n else next.add(id)\n return next\n })\n\n return (\n // The site bar is a SIBLING of .idx, not a child: it is full-bleed (its rule spans the\n // viewport) while .idx is a 940px centred column. Both use the same width and gutters, so the\n // brand lines up with the card edge below it.\n <>\n <SiteHeader kind={kind} actions={actions} />\n <div className=\"idx\">\n {/* The GraphL wordmark used to be an eyebrow here; it is in the bar above now. What stays\n is `subject` — the repo's own name (\"Python\", \"SQL\"), the one piece of this page the\n shell cannot know. */}\n <header className=\"idx__head\">\n <h1 className=\"idx__subject\">{subject}</h1>\n </header>\n\n {courses.length === 0 ? (\n <p className=\"idx__empty\">No courses authored yet — the app shell is ready.</p>\n ) : (\n <ol className=\"idx__grid\">\n {courses.map((course, i) => {\n const sections = allSections(course)\n const isOpen = open.has(course.id)\n return (\n <li key={course.id} className={`idx-card${isOpen ? ' idx-card--open' : ''}`}>\n <button\n className=\"idx-card__head\"\n onClick={() => toggle(course.id)}\n aria-expanded={isOpen}\n >\n <span className=\"idx-card__num\">{String(i + 1).padStart(2, '0')}</span>\n <span className=\"idx-card__title\">{course.title}</span>\n <span className=\"idx-card__count\">{sections.length} sections</span>\n <span className=\"idx-card__toggle\" aria-hidden=\"true\">\n ⌄\n </span>\n </button>\n\n {isOpen && (\n <ol className=\"idx-sec\">\n {sections.map(({ section, slug }, j) => (\n <li key={slug} className=\"idx-sec__row\">\n <a href={`#/${slug}`}>\n <span className=\"idx-sec__num\">{j + 1}</span>\n <span className=\"idx-sec__title\">{section.title}</span>\n <span className=\"idx-sec__chev\" aria-hidden=\"true\">\n ›\n </span>\n </a>\n </li>\n ))}\n </ol>\n )}\n </li>\n )\n })}\n </ol>\n )}\n\n <footer className=\"idx__foot\">\n <span className=\"idx__hint\">\n <kbd>→</kbd> next · <kbd>←</kbd> prev · <kbd>Space</kbd> narration\n </span>\n </footer>\n </div>\n </>\n )\n}\n","import { useEffect, useRef, useState } from 'react'\n\n// The narration channel: one <audio> for the app's lifetime, driven by SPACE (toggle) and by\n// section navigation. Loading a section's clip keeps the play state — if narration is on, changing\n// sections auto-plays the next clip — and a clip that finishes calls `onEnded` so playback\n// auto-advances to the next SECTION. Timing comes from the clip length itself (no timestamp\n// anchors), so regenerated audio never drifts. A missing clip (not yet generated, or a 404) fails\n// silently and resets the play state — paging still works, just no audio.\n//\n// Adapted from graphl-studio's reveal-engine useNarration: this repo has no beats, so the unit is\n// one clip per section (`<audioBase>/<section-id>.wav`) rather than per beat.\nexport function useNarration(src: string | undefined, onEnded: () => void) {\n const audioRef = useRef<HTMLAudioElement | null>(null)\n // The same element, as state, purely so NarrationBar can subscribe to it once it exists. The bar\n // reads position straight off the element rather than through this hook's state, so a ~4Hz\n // `timeupdate` never re-renders the section (see NarrationBar).\n const [audio, setAudio] = useState<HTMLAudioElement | null>(null)\n const endedRef = useRef(onEnded)\n endedRef.current = onEnded\n const [playing, setPlaying] = useState(false)\n const playingRef = useRef(playing)\n playingRef.current = playing\n\n useEffect(() => {\n const a = new Audio()\n a.addEventListener('ended', () => endedRef.current())\n // A clip that can't load (not generated yet, 404) must not leave a stale \"playing\" state.\n a.addEventListener('error', () => setPlaying(false))\n audioRef.current = a\n setAudio(a)\n return () => a.pause()\n }, [])\n\n // Load the current section's clip; resume playing if narration was on. Because playback is\n // gesture-initiated (SPACE), the browser lets the auto-advance chain keep playing.\n useEffect(() => {\n const a = audioRef.current\n if (!a) return\n a.pause()\n if (src) {\n a.src = src\n a.currentTime = 0\n if (playingRef.current) a.play().catch(() => {})\n } else {\n // Fully unload so a later play() doesn't resume a stale buffered clip.\n a.removeAttribute('src')\n a.load()\n setPlaying(false)\n }\n }, [src])\n\n const toggle = () => {\n const a = audioRef.current\n if (!a || !a.getAttribute('src')) return // nothing loaded (a section with no clip)\n if (playingRef.current) {\n a.pause()\n setPlaying(false)\n } else {\n a.play().catch(() => {})\n setPlaying(true)\n }\n }\n\n /** Hard stop — pause and clear the play state (used at the end of the course). */\n const stop = () => {\n audioRef.current?.pause()\n setPlaying(false)\n }\n\n return { playing, toggle, stop, audio }\n}\n","// The PLATFORM theme contract, shared with graphl.in. ui-graphl/theme.js is the original; this is\n// the React port, and the two only stay one behaviour if neither drifts — same storage key, same\n// values, same attribute, same cross-tab sync.\n//\n// key localStorage['graphl:theme']\n// values 'light' | 'dark' | absent\n// switch data-theme on <html>; absent means \"no explicit choice\"\n//\n// Every GraphL app is same-origin under graphl.in, so a reader who picks light on the catalog walks\n// into a concept app already light, with nothing passed between them. Keep all four stable.\n//\n// ONE DELIBERATE DIFFERENCE FROM THE CATALOG, and it is the important one. At graphl.in the neutral\n// state means \"follow the OS\" (prefers-color-scheme). Here it means \"follow the DECK\" — the theme\n// the repo declared on ConceptApp. A deck is authored: its scenes, slides and narration were built\n// and reviewed in one look, and letting a reader's OS silently repaint an authored deck is not the\n// same decision as letting it repaint a directory page. The stored VALUES stay identical, so the\n// two apps never disagree about an explicit choice; only the absence is read differently.\n//\n// The capture path ignores all of this — see resolveTheme.\n\nimport { useCallback, useEffect, useState } from 'react'\nimport type { ThemeKey } from '@graphlearning/flow'\n\nconst KEY = 'graphl:theme'\n\n// The stored choice, or null for \"nothing chosen yet\" — which means the deck's declared theme.\n//\n// THE TOGGLE IS TWO-STATE: dark <-> light, and nothing else. There is no reader-visible \"deck\" or\n// \"system\" step. A third state costs a reader a press to get where they were going and has to be\n// labelled something (\"Deck theme\") that means nothing to them. The ABSENCE of a stored value still\n// means the deck's theme — that is what a first visit and every capture get — but once a reader has\n// an opinion the control simply flips, which is what every dark-mode toggle they have ever used does.\n//\n// localStorage throws in some privacy modes. A broken toggle must never take the page with it, so\n// every access is guarded and the fallback is simply \"nothing chosen\".\nexport function readStored(): ThemeKey | null {\n try {\n const stored = localStorage.getItem(KEY)\n return stored === 'light' || stored === 'dark' ? stored : null\n } catch {\n return null\n }\n}\n\nfunction writeStored(choice: ThemeKey) {\n try {\n localStorage.setItem(KEY, choice)\n } catch {\n /* the choice will not survive a reload; everything else still works */\n }\n}\n\n/**\n * The theme actually painted. CAPTURE PINS IT TO THE DECK, ignoring the stored choice and the reader\n * entirely — and that is not a nicety. A recorder runs a fresh headless profile with empty storage,\n * so without this pin what a capture painted would depend on the machine that rendered it. The\n * declared prop is the only thing burned into a video.\n */\nexport const resolveTheme = (deck: ThemeKey, stored: ThemeKey | null, capture: boolean): ThemeKey =>\n capture || !stored ? deck : stored\n\n/**\n * Owns the reader's choice and keeps <html data-theme> in step with it. Returns the theme to hand\n * SceneView, plus a two-state flip for the toggle.\n */\nexport function useTheme(deck: ThemeKey, capture: boolean) {\n const [stored, setStored] = useState<ThemeKey | null>(() => (capture ? null : readStored()))\n const theme = resolveTheme(deck, stored, capture)\n\n // The attribute goes on <html>, not a wrapper: ConceptApp renders a fragment, and a wrapping\n // element would change the section view's box model — which is burned into every recorded video.\n useEffect(() => {\n document.documentElement.setAttribute('data-theme', theme)\n return () => document.documentElement.removeAttribute('data-theme')\n }, [theme])\n\n // Another GraphL app on the same origin changed the choice — follow it, so the platform does not\n // disagree with itself between two tabs. Never while capturing.\n useEffect(() => {\n if (capture) return\n const onStorage = (e: StorageEvent) => {\n if (e.key === KEY || e.key === null) setStored(readStored())\n }\n window.addEventListener('storage', onStorage)\n return () => window.removeEventListener('storage', onStorage)\n }, [capture])\n\n // Swapping must look instantaneous. Several elements carry a 0.15s colour transition for hover,\n // and without suppressing them the ground flips at once while the nav and cards cross-fade behind\n // it — the switch reads as a smear rather than a change. Two nested rAFs: the first runs before\n // the paint that applies the new colours, the second after it. Ported from ui-graphl's theme.js.\n const toggle = useCallback(() => {\n const next: ThemeKey = theme === 'dark' ? 'light' : 'dark'\n writeStored(next)\n const root = document.documentElement\n root.classList.add('theme-swap')\n setStored(next)\n requestAnimationFrame(() => requestAnimationFrame(() => root.classList.remove('theme-swap')))\n }, [theme])\n\n return { theme, toggle }\n}\n","// The theme control, for SiteHeader's `actions` slot. A PORT of ui-graphl's #theme button — same\n// square icon, same \"where you are AND what the press does\" accessible name.\n//\n// TWO STATES, dark <-> light. The catalog cycles through a third, `system`; this does not, and the\n// difference is deliberate. A concept app has an authored look, so the neutral step would have to be\n// labelled \"Deck theme\" — a phrase that means nothing to a reader and costs them a press to get\n// where they were going. The ABSENCE of a stored choice still resolves to the deck's theme, which is\n// what a first visit and every capture get; the control just never walks back to it.\n//\n// WHY A READER CONTROL IS SAFE AT ALL, given a deck is a video: this renders inside SiteHeader, and\n// SiteHeader renders on the CATALOG PAGE ONLY, never on a section. The control can therefore never\n// appear in a captured frame, and capture pins to the declared deck theme regardless (see useTheme).\n\nimport type { ThemeKey } from '@graphlearning/flow'\n\n// The glyph shows where you ARE, not where the press goes — a toggle that previews its destination\n// reads as already-switched at a glance.\nconst FACE: Record<ThemeKey, { glyph: string; label: string }> = {\n dark: { glyph: '☾', label: 'Dark theme' },\n light: { glyph: '☀', label: 'Light theme' },\n}\n\nexport function ThemeToggle({ theme, onToggle }: { theme: ThemeKey; onToggle: () => void }) {\n const next: ThemeKey = theme === 'dark' ? 'light' : 'dark'\n const label = `${FACE[theme].label}. Switch to ${FACE[next].label.toLowerCase()}.`\n return (\n <button className=\"site__icon\" type=\"button\" onClick={onToggle} aria-label={label} title={label}>\n {FACE[theme].glyph}\n </button>\n )\n}\n","import { useCallback, useEffect, useMemo, useState } from 'react'\nimport { SceneView, type ThemeKey } from '@graphlearning/flow'\nimport { SectionView } from './SectionView'\nimport type { Course, SceneLookup } from './types'\nimport { allSections, slugOf } from './types'\nimport { CourseIndex } from './CourseIndex'\nimport { useNarration } from './useNarration'\nimport { useTheme } from './useTheme'\nimport { ThemeToggle } from './ThemeToggle'\nimport { SEEK_S } from './NarrationBar'\n\n// Route contract (hash routing):\n// #/<course-section> → SECTION view — the final composited output of a slug (scene bg + slide)\n// #/<scene> → SCENE view — just the diagram\n// A bare id resolves to a section if it matches a slug, otherwise to a scene. Slugs (course-section)\n// and scene ids never collide, so no prefix is needed.\nexport function ConceptApp({\n subject,\n eyebrow,\n courses: COURSES,\n getScene,\n audioBase = '/',\n kind = 'courses',\n theme = 'dark',\n}: {\n subject: string // this repo's name, as the catalog's h1 shows it (\"Apache Spark\")\n courses: Record<string, Course> // the repo's course registry, in syllabus order\n // The section eyebrow's prefix (\"SPARK · EVOLUTION\"). Defaults to subject.toUpperCase(), which is\n // right for most repos — but NOT all: apache-spark titles itself \"Apache Spark\" and brands its\n // sections \"SPARK\", databricks-data-engineer brands its \"DATABRICKS\". The eyebrow is burned into\n // every recorded video, so it gets its own prop rather than being derived and quietly changed.\n eyebrow?: string\n getScene: SceneLookup // the repo's scene registry\n // Where the narration wavs are served from — a content repo passes import.meta.env.BASE_URL,\n // which is its vite `base` (\"/python/\"). It CANNOT be read here: import.meta.env is replaced at\n // the app's build time, and this package is built separately, so a literal here would bake in\n // the shell's own base and 404 on every deployed site. The layout under it is the content\n // model's, not the repo's: audio/<courseId>/<sectionId>.wav.\n audioBase?: string\n // Which section of the site this app belongs to — a catalog.json `kind` id, marked as the\n // current item in the site bar on the catalog page. Every content repo is a course, so the\n // default covers all seven; python-lab passes 'labs'.\n kind?: string\n // The surface the whole app is painted on — the scene pane (passed to SceneView) AND the shell's\n // own chrome (set as data-theme on <html>, which styles.css keys its light block off). One value\n // so the two cannot disagree: a light scene inside dark chrome is a white rectangle on a dark page.\n //\n // DECK-LEVEL, and deliberately not a viewer toggle. A deck is a video: the recorded frame must not\n // depend on the machine rendering it, or the same slug captures differently on two laptops. A repo\n // sets this once in main.tsx beside its theme.css import — it is the same per-repo design surface\n // as --brand/--brand-hover/--accent-2, and it stays OUT of the route contract, so no recorder\n // changes. A repo going light must also re-pick those three tokens: a brand tuned to glow on slate\n // can fall under 3:1 on off-white, and the shell cannot repick them without owning them.\n theme?: ThemeKey\n}) {\n\n const [hash, setHash] = useState(() => location.hash)\n useEffect(() => {\n const onHash = () => setHash(location.hash)\n window.addEventListener('hashchange', onHash)\n return () => window.removeEventListener('hashchange', onHash)\n }, [])\n\n const capture = new URLSearchParams(location.search).get('capture') === '1'\n\n // `theme` is the DECK's declared look; the reader may override it for live viewing via the toggle\n // on the catalog, and that choice is the platform-wide `graphl:theme` the catalog at graphl.in\n // writes — so crossing from there into a concept app carries it. Capture pins to the deck and\n // ignores the reader entirely; see useTheme.ts.\n const { theme: activeTheme, toggle: cycleTheme } = useTheme(theme, capture)\n const id = hash.replace(/^#\\/?/, '')\n\n // Which course are we in? The recorder lands on #/<course> to read the plan; a human lands on a\n // slug. So the active course is: a bare course id, else the course that OWNS the matched slug, else\n // the first course (the nav context for a bare scene view). Everything course-scoped below —\n // section nav, the capture plan, the audio path, the header eyebrow — reads from this.\n const activeCourse = useMemo(() => {\n if (COURSES[id]) return COURSES[id]\n for (const c of Object.values(COURSES)) {\n if (c.sections.some((s) => slugOf(c, s) === id)) return c\n }\n return Object.values(COURSES)[0]\n }, [id, COURSES])\n const sections = useMemo(() => allSections(activeCourse), [activeCourse])\n // The whole catalog as one ordered stream (in COURSES insertion\n // order). Section nav walks THIS so → past a course's last section rolls into the next course's\n // first — and narration flows chapter-to-chapter — instead of dead-ending at the index. The\n // per-course `sections` above still drives the capture plan and the §n/N header (chapter-relative).\n const globalSections = useMemo(() => Object.values(COURSES).flatMap(allSections), [COURSES])\n\n // A bare COURSE id in the hash has no page of its own — the GraphL catalog links each course to\n // `<slug>/#/<courseId>`, so a human arriving there should ENTER the course at its first section.\n // Guarded by !capture: the recorder deliberately lands on `#/<course>` to read window.__scene.plan\n // (below), so it must not be redirected. Replace (not push) so Back skips the bare course-id URL.\n useEffect(() => {\n if (capture || !COURSES[id]) return\n const first = sections[0]\n if (!first) return\n history.replaceState(null, '', `${location.pathname}${location.search}#/${first.slug}`)\n setHash(`#/${first.slug}`)\n }, [capture, id, sections])\n\n useEffect(() => {\n if (!capture) return\n // Capture contract (fleshed out in step 2): every slug + its scene + focus node, so the recorder\n // can screenshot each and knows which node the slide must avoid.\n ;(window as unknown as { __scene: unknown }).__scene = {\n plan: () =>\n sections.map(({ slug, section }) => ({\n slug,\n course: activeCourse.id, // audio lives at audio/<course>/<id>.wav\n id: section.id,\n scene: section.scene,\n focus: section.focus ?? null,\n })),\n }\n }, [capture, sections, activeCourse])\n\n // Section navigation, shared by the ← / → keys AND the on-screen pager. `dir` is +1 (next) / -1\n // (prev). Reads the LIVE hash at call time (not React state) so rapid presses can't act on a stale\n // index; from a non-section view, next enters the first section. Walks the GLOBAL stream, so a\n // course boundary is just another step — → past a course's last section enters the next course's\n // first, ← symmetrically crosses back. Only the two ENDS of the whole catalog fall back to the\n // catalog page (like Esc). Navigating just updates the hash, which re-renders via the listener.\n const go = useCallback(\n (dir: 1 | -1) => {\n const cur = location.hash.replace(/^#\\/?/, '')\n const gidx = globalSections.findIndex(({ slug }) => slug === cur)\n if (gidx !== -1) {\n const target = globalSections[gidx + dir]\n location.hash = target ? `#/${target.slug}` : '' // catalog only at the catalog's ends\n return\n }\n // The CATALOG index (empty hash) is the home/start: → begins the stream at its first section;\n // ← does nothing (there is nothing before home — it must not fall into foundations' last).\n if (cur === '') {\n if (dir === 1 && globalSections[0]) location.hash = `#/${globalSections[0].slug}`\n return\n }\n // Otherwise a bare SCENE view (a scene id with course context): → enters the active course's\n // first section, ← its last.\n const target = dir === 1 ? sections[0] : sections[sections.length - 1]\n if (target) location.hash = `#/${target.slug}`\n },\n [sections, globalSections],\n )\n // Back to the catalog: clear the hash (empty hash → CourseIndex). No-ops when already there. Wired\n // to the top-left GraphL brand AND the Esc key (below), so any inner page is one gesture from home.\n const goHome = useCallback(() => {\n if (location.hash) location.hash = '' // inner page → this concept's index\n else if (location.pathname !== '/') location.href = '/' // already at the index → up to the graphl.in catalog\n }, [])\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if (e.key === 'Escape') goHome()\n }\n window.addEventListener('keydown', onKey)\n return () => window.removeEventListener('keydown', onKey)\n }, [goHome])\n\n // Slug (section) first.\n const match = sections.find(({ slug }) => slug === id)\n\n // Narration channel. A section's clip lives at public/audio/<courseId>/<section-id>.wav (pushed\n // from the Colab generator). SPACE toggles it; when a clip finishes we roll into the NEXT section\n // (which auto-plays because the play state is preserved) so the whole course narrates hands-free.\n // Outside a section (scene view) src is undefined → the channel unloads.\n const audioUrl = match\n ? `${audioBase}audio/${activeCourse.id}/${match.section.id}.wav`\n : undefined\n const { playing, toggle, stop, audio } = useNarration(audioUrl, () => {\n const cur = location.hash.replace(/^#\\/?/, '')\n const idx = globalSections.findIndex(({ slug }) => slug === cur)\n const next = globalSections[idx + 1] // rolls across course boundaries into the next chapter\n if (next) location.hash = `#/${next.slug}`\n else stop() // last section of the whole catalog — nothing to advance to\n })\n\n // ← / → : SCRUB the narration when this section has a clip, PAGE when it does not. The footer's\n // < > buttons are the pager's real home and Esc still leaves, so the arrows are free to do the\n // thing a LISTENER reaches for — a ±SEEK_S step, the convention every podcast and video player\n // already taught them. The gate is the clip's duration, which makes the rule visible rather than\n // modal: the arrows scrub exactly when the scrub bar is on screen. Where there is nothing to\n // scrub the old meaning stands, so the keyboard never goes dead — the catalog, a bare scene view,\n // a section whose wav 404s, and a whole repo with no narration yet (docker, java) all still page.\n // SHIFT forces the pager even mid-clip, so a listener is never stranded without a key for it.\n //\n // Position is read straight off the element, like NarrationBar — it never becomes React state, so\n // a seek re-renders the bar alone and not SceneView + the markdown slide.\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return\n const t = e.target as HTMLElement | null\n if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return\n const dir = e.key === 'ArrowRight' ? 1 : -1\n e.preventDefault()\n const d = audio?.duration ?? 0 // NaN/Infinity while a clip loads, or when none is loaded\n if (audio && !e.shiftKey && Number.isFinite(d) && d > 0) {\n audio.currentTime = Math.min(d, Math.max(0, audio.currentTime + dir * SEEK_S))\n return\n }\n go(dir)\n }\n window.addEventListener('keydown', onKey)\n return () => window.removeEventListener('keydown', onKey)\n }, [go, audio])\n\n // SPACE enables/disables narration. Re-bound every render (no deps) so it always sees the current\n // `toggle`; guarded so it never hijacks the spacebar while typing in an input.\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== ' ' && e.code !== 'Space') return\n const t = e.target as HTMLElement | null\n if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return\n e.preventDefault()\n toggle()\n }\n window.addEventListener('keydown', onKey)\n return () => window.removeEventListener('keydown', onKey)\n })\n\n // Empty hash → the course catalog landing page (its own brand, no overlay, no home button here).\n if (!id)\n return (\n <CourseIndex\n courses={Object.values(COURSES)}\n subject={subject}\n kind={kind}\n // The toggle rides the catalog only — never a section, which is a video frame.\n actions={capture ? null : <ThemeToggle theme={activeTheme} onToggle={cycleTheme} />}\n />\n )\n\n // The bare SCENE view (a scene id, no course context) keeps the top-left GraphL brand as its route\n // home. The SECTION view instead carries the shared header/footer, whose eyebrow IS the home link,\n // so it renders no separate brand. Both are hidden at capture so the recorded frame stays clean.\n let content\n let brand = null\n if (match) {\n const index = sections.findIndex(({ slug }) => slug === id)\n content = (\n <SectionView\n section={match.section}\n getScene={getScene}\n // ?capture=1 suppresses the (interactive-only) drawer toggle so the recorded frame is clean.\n capture={capture}\n // Header eyebrow — subject · course; doubles as the back-to-catalog link.\n eyebrow={`${eyebrow ?? subject.toUpperCase()} · ${activeCourse.id.toUpperCase()}`}\n index={index}\n total={sections.length}\n onHome={goHome}\n onPrev={() => go(-1)}\n onNext={() => go(1)}\n narrating={playing}\n onToggleNarration={toggle}\n narrationAudio={audio}\n theme={activeTheme}\n // Suppressed under capture by SectionView's own !capture guard, but passed as undefined here\n // too so the button is not even constructed for a recording.\n onToggleTheme={capture ? undefined : cycleTheme}\n />\n )\n } else {\n // Otherwise a scene id.\n const scene = getScene(id)\n content = scene ? (\n <div className=\"stage\">\n <SceneView scene={scene} theme={activeTheme} />\n </div>\n ) : (\n <div className=\"stage stage--missing\">no scene or slug: {id}</div>\n )\n brand = capture ? null : (\n <button className=\"brand-home\" onClick={goHome} aria-label=\"Back to catalog (Esc)\" title=\"Back to catalog (Esc)\">\n GraphL\n </button>\n )\n }\n\n return (\n <>\n {brand}\n {content}\n </>\n )\n}\n"],"names":["slugOf","course","section","allSections","DESIGN_PANE","useSlideScale","ref","useRef","zoom","setZoom","useState","useLayoutEffect","el","measure","ro","SlidePanel","slide","open","jsx","ReactMarkdown","remarkGfm","SEEK_S","NarrationBar","audio","time","setTime","duration","setDuration","trackRef","useEffect","sync","events","e","seekToClientX","clientX","f","onKeyDown","step","d","pct","jsxs","fmt","s","t","SectionView","getScene","capture","eyebrow","index","total","onHome","onPrev","onNext","narrating","onToggleNarration","narrationAudio","theme","onToggleTheme","setOpen","scene","Home","Volume2","VolumeX","Moon","Sun","ChevronLeft","ChevronRight","SceneView","o","KINDS","Mark","SiteHeader","kind","nav","home","actions","k","CourseIndex","courses","subject","toggle","id","prev","next","Fragment","i","sections","isOpen","slug","j","useNarration","src","onEnded","audioRef","setAudio","endedRef","playing","setPlaying","playingRef","a","_a","KEY","readStored","stored","writeStored","choice","resolveTheme","deck","useTheme","setStored","onStorage","useCallback","root","FACE","ThemeToggle","onToggle","label","ConceptApp","COURSES","audioBase","hash","setHash","onHash","activeTheme","cycleTheme","activeCourse","useMemo","c","globalSections","first","go","dir","cur","gidx","target","goHome","onKey","match","audioUrl","stop","idx","content","brand"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../src/types.ts","../src/useSlideScale.ts","../src/SlidePanel.tsx","../src/NarrationBar.tsx","../src/ThemeToggle.tsx","../src/SectionView.tsx","../src/SiteHeader.tsx","../src/CourseIndex.tsx","../src/useNarration.ts","../src/useTheme.ts","../src/ConceptApp.tsx"],"sourcesContent":["import type { Scene } from '@graphlearning/flow'\n\nexport interface Section {\n id: string\n title: string\n scene: string\n focus?: string\n slide: string\n narration: string\n}\n\nexport interface Course {\n id: string\n title: string\n sections: Section[]\n}\n\nexport type SceneLookup = (id: string) => Scene | undefined\n\nexport const slugOf = (course: Course, section: Section) => `${course.id}-${section.id}`\n\nexport const allSections = (course: Course) =>\n course.sections.map((section) => ({ section, slug: slugOf(course, section) }))\n","import { useLayoutEffect, useRef, useState } from 'react'\n\nexport const DESIGN_PANE = 806\n\nexport function useSlideScale() {\n const ref = useRef<HTMLElement>(null)\n const [zoom, setZoom] = useState(1)\n useLayoutEffect(() => {\n const el = ref.current\n if (!el) return\n const measure = () => setZoom(el.clientWidth / DESIGN_PANE)\n measure()\n const ro = new ResizeObserver(measure)\n ro.observe(el)\n return () => ro.disconnect()\n }, [])\n return { ref, zoom }\n}\n","import ReactMarkdown from 'react-markdown'\nimport remarkGfm from 'remark-gfm'\nimport { DESIGN_PANE, useSlideScale } from './useSlideScale'\n\nexport function SlidePanel({ slide, open }: { slide: string; open: boolean }) {\n const { ref, zoom } = useSlideScale()\n return (\n <aside ref={ref} className={`slide-panel${open ? ' slide-panel--open' : ''}`}>\n <div className=\"slide-panel__scaler\" style={{ width: DESIGN_PANE, zoom }}>\n <ReactMarkdown remarkPlugins={[remarkGfm]}>{slide}</ReactMarkdown>\n </div>\n </aside>\n )\n}\n","import { useEffect, useState } from 'react'\n\nexport const SEEK_S = 10\n\nconst EVENTS = ['loadedmetadata', 'durationchange', 'timeupdate', 'seeked', 'ended', 'emptied', 'error'] as const\n\nconst fmt = (s: number) => {\n const t = Number.isFinite(s) && s > 0 ? s : 0\n return `${Math.floor(t / 60)}:${String(Math.floor(t % 60)).padStart(2, '0')}`\n}\n\nexport function NarrationBar({ audio }: { audio: HTMLAudioElement | null }) {\n const [time, setTime] = useState(0)\n const [duration, setDuration] = useState(0)\n\n useEffect(() => {\n if (!audio) return\n const sync = () => {\n setTime(audio.currentTime)\n setDuration(Number.isFinite(audio.duration) ? audio.duration : 0)\n }\n sync()\n EVENTS.forEach((e) => audio.addEventListener(e, sync))\n return () => EVENTS.forEach((e) => audio.removeEventListener(e, sync))\n }, [audio])\n\n if (!audio || !duration) return null\n\n const seek = (t: number) => {\n audio.currentTime = Math.min(duration, Math.max(0, t))\n setTime(audio.currentTime)\n }\n\n const seekToX = (track: HTMLElement, clientX: number) => {\n const { left, width } = track.getBoundingClientRect()\n seek(((clientX - left) / width) * duration)\n }\n\n const onKeyDown = (e: React.KeyboardEvent) => {\n const delta = { ArrowLeft: -SEEK_S, ArrowRight: SEEK_S, Home: -duration, End: duration }[e.key]\n if (delta === undefined) return\n e.preventDefault()\n e.stopPropagation()\n seek(audio.currentTime + delta)\n }\n\n const pct = `${Math.min(100, (time / duration) * 100)}%`\n return (\n <span className=\"reel-foot__audio\">\n <span className=\"reel-foot__count reel-foot__time\">{fmt(time)}</span>\n <div\n className=\"reel-foot__track\"\n role=\"slider\"\n tabIndex={0}\n aria-label=\"Narration position\"\n aria-valuemin={0}\n aria-valuemax={Math.round(duration)}\n aria-valuenow={Math.round(time)}\n aria-valuetext={`${fmt(time)} of ${fmt(duration)}`}\n onKeyDown={onKeyDown}\n onPointerDown={(e) => {\n e.currentTarget.setPointerCapture(e.pointerId)\n seekToX(e.currentTarget, e.clientX)\n }}\n onPointerMove={(e) => {\n if (e.currentTarget.hasPointerCapture(e.pointerId)) seekToX(e.currentTarget, e.clientX)\n }}\n >\n <span className=\"reel-foot__fill\" style={{ width: pct }} />\n <span className=\"reel-foot__thumb\" style={{ left: pct }} />\n </div>\n <span className=\"reel-foot__count reel-foot__time\">{fmt(duration)}</span>\n </span>\n )\n}\n","import type { ThemeKey } from '@graphlearning/flow'\n\nconst FACE: Record<ThemeKey, { glyph: string; label: string }> = {\n dark: { glyph: '☾', label: 'Dark theme' },\n light: { glyph: '☀', label: 'Light theme' },\n}\n\nexport const themeLabel = (theme: ThemeKey) => {\n const next: ThemeKey = theme === 'dark' ? 'light' : 'dark'\n return `${FACE[theme].label}. Switch to ${FACE[next].label.toLowerCase()}.`\n}\n\nexport function ThemeToggle({ theme, onToggle }: { theme: ThemeKey; onToggle: () => void }) {\n const label = themeLabel(theme)\n return (\n <button className=\"site__icon\" type=\"button\" onClick={onToggle} aria-label={label} title={label}>\n {FACE[theme].glyph}\n </button>\n )\n}\n","import { useState } from 'react'\nimport { Home, Volume2, VolumeX, ChevronLeft, ChevronRight, Moon, Sun } from 'lucide-react'\nimport { SceneView, type ThemeKey } from '@graphlearning/flow'\nimport type { Section, SceneLookup } from './types'\nimport { SlidePanel } from './SlidePanel'\nimport { NarrationBar } from './NarrationBar'\nimport { themeLabel } from './ThemeToggle'\n\nconst tip = (label: string) => ({ 'aria-label': label, title: label })\n\nexport function SectionView({\n section,\n getScene,\n capture,\n eyebrow,\n index,\n total,\n onHome,\n onPrev,\n onNext,\n narrating,\n onToggleNarration,\n narrationAudio,\n theme,\n onToggleTheme,\n}: {\n section: Section\n getScene: SceneLookup\n capture: boolean\n eyebrow: string\n index: number\n total: number\n onHome: () => void\n onPrev: () => void\n onNext: () => void\n narrating: boolean\n onToggleNarration: () => void\n narrationAudio: HTMLAudioElement | null\n theme: ThemeKey\n onToggleTheme?: () => void\n}) {\n const [open, setOpen] = useState(false)\n const scene = getScene(section.scene)\n if (!scene) return <div className=\"stage stage--missing\">no scene: {section.scene}</div>\n const dark = theme === 'dark'\n return (\n <div className=\"stage stage--section\">\n <header className=\"reel-head\">\n <button className=\"reel-head__eyebrow\" onClick={onHome} {...tip('Back to catalog (Esc)')}>\n {eyebrow}\n </button>\n <h1 className=\"reel-head__title\">{section.title}</h1>\n </header>\n {!capture && (\n <footer className=\"reel-foot reel-foot--controls\">\n <span className=\"reel-foot__grp\">\n <button className=\"reel-foot__ctrl\" onClick={onHome} {...tip('Back to catalog (Esc)')}>\n <Home size={17} />\n </button>\n <button\n className=\"reel-foot__ctrl\"\n onClick={onToggleNarration}\n {...tip(narrating ? 'Pause narration (Space)' : 'Play narration (Space)')}\n >\n {narrating ? <Volume2 size={18} /> : <VolumeX size={18} />}\n </button>\n {onToggleTheme && (\n <button className=\"reel-foot__ctrl\" onClick={onToggleTheme} {...tip(themeLabel(theme))}>\n {dark ? <Moon size={17} /> : <Sun size={17} />}\n </button>\n )}\n </span>\n <NarrationBar audio={narrationAudio} />\n <span className=\"reel-foot__nav\">\n <button className=\"reel-foot__ctrl\" onClick={onPrev} {...tip('Previous section (Shift+←)')}>\n <ChevronLeft size={19} />\n </button>\n <span className=\"reel-foot__count reel-foot__count--live\">\n {index + 1} / {total}\n </span>\n <button className=\"reel-foot__ctrl\" onClick={onNext} {...tip('Next section (Shift+→)')}>\n <ChevronRight size={19} />\n </button>\n </span>\n </footer>\n )}\n <div className=\"scene-area\">\n <SceneView scene={scene} focusId={section.focus} theme={theme} />\n </div>\n {!capture && (\n <button className=\"slide-toggle\" onClick={() => setOpen((o) => !o)} aria-label={open ? 'Hide slide' : 'Show slide'}>\n {open ? '›' : '‹'}\n </button>\n )}\n <SlidePanel slide={section.slide} open={open} />\n </div>\n )\n}\n","import type { ReactNode } from 'react'\n\nconst KINDS = [\n { id: 'courses', label: 'Courses' },\n { id: 'labs', label: 'Labs' },\n { id: 'coach', label: 'Coach' },\n]\n\nfunction Mark() {\n return (\n <svg className=\"site__logo\" width=\"28\" height=\"28\" viewBox=\"0 0 512 512\" aria-hidden=\"true\">\n <rect width=\"512\" height=\"512\" rx=\"80\" fill=\"#e8804f\" />\n <g fill=\"none\" stroke=\"#ffffff\" strokeWidth=\"58\" strokeLinecap=\"butt\" strokeLinejoin=\"miter\">\n <path d=\"M 373 200 A 130 130 0 1 0 373 312\" />\n <path d=\"M 283 312 L 373 312 L 373 410\" />\n </g>\n </svg>\n )\n}\n\nexport function SiteHeader({\n kind = 'courses',\n nav = KINDS,\n home = '/',\n actions,\n}: {\n kind?: string\n nav?: { id: string; label: string }[]\n home?: string\n actions?: ReactNode\n}) {\n return (\n <header className=\"site\">\n <div className=\"site__inner\">\n <a className=\"site__brand\" href={home}>\n <Mark />\n <span className=\"site__name\">GraphL</span>\n </a>\n <nav className=\"site__nav\" aria-label=\"Sections\">\n {nav.map((k) => (\n <a key={k.id} className=\"site__link\" href={`${home}#${k.id}`} aria-current={k.id === kind ? 'page' : undefined}>\n {k.label}\n </a>\n ))}\n </nav>\n <div className=\"site__actions\">{actions}</div>\n </div>\n </header>\n )\n}\n","import { useState, type ReactNode } from 'react'\nimport { allSections, type Course } from './types'\nimport { SiteHeader } from './SiteHeader'\n\nexport function CourseIndex({\n courses,\n subject,\n kind,\n actions,\n}: {\n courses: Course[]\n subject: string\n kind?: string\n actions?: ReactNode\n}) {\n const [open, setOpen] = useState<Set<string>>(new Set())\n const toggle = (id: string) =>\n setOpen((prev) => {\n const next = new Set(prev)\n if (!next.delete(id)) next.add(id)\n return next\n })\n\n return (\n <>\n <SiteHeader kind={kind} actions={actions} />\n <div className=\"idx\">\n <header className=\"idx__head\">\n <h1 className=\"idx__subject\">{subject}</h1>\n </header>\n\n {courses.length === 0 ? (\n <p className=\"idx__empty\">No courses authored yet — the app shell is ready.</p>\n ) : (\n <ol className=\"idx__grid\">\n {courses.map((course, i) => {\n const sections = allSections(course)\n const isOpen = open.has(course.id)\n return (\n <li key={course.id} className={`idx-card${isOpen ? ' idx-card--open' : ''}`}>\n <button className=\"idx-card__head\" onClick={() => toggle(course.id)} aria-expanded={isOpen}>\n <span className=\"idx-card__num\">{String(i + 1).padStart(2, '0')}</span>\n <span className=\"idx-card__title\">{course.title}</span>\n <span className=\"idx-card__count\">\n {sections.length} {sections.length === 1 ? 'section' : 'sections'}\n </span>\n <span className=\"idx-card__toggle\" aria-hidden=\"true\">\n ⌄\n </span>\n </button>\n\n {isOpen && (\n <ol className=\"idx-sec\">\n {sections.map(({ section, slug }, j) => (\n <li key={slug} className=\"idx-sec__row\">\n <a href={`#/${slug}`}>\n <span className=\"idx-sec__num\">{j + 1}</span>\n <span className=\"idx-sec__title\">{section.title}</span>\n <span className=\"idx-sec__chev\" aria-hidden=\"true\">\n ›\n </span>\n </a>\n </li>\n ))}\n </ol>\n )}\n </li>\n )\n })}\n </ol>\n )}\n\n <footer className=\"idx__foot\">\n <span className=\"idx__hint\">\n <kbd>→</kbd> next · <kbd>←</kbd> prev · <kbd>Space</kbd> narration\n </span>\n </footer>\n </div>\n </>\n )\n}\n","import { useEffect, useRef, useState } from 'react'\n\nexport function useNarration(src: string | undefined, onEnded: () => void) {\n const audioRef = useRef<HTMLAudioElement | null>(null)\n const [audio, setAudio] = useState<HTMLAudioElement | null>(null)\n const [playing, setPlaying] = useState(false)\n const endedRef = useRef(onEnded)\n const playingRef = useRef(playing)\n endedRef.current = onEnded\n playingRef.current = playing\n\n useEffect(() => {\n const a = new Audio()\n a.addEventListener('ended', () => endedRef.current())\n a.addEventListener('error', () => setPlaying(false))\n audioRef.current = a\n setAudio(a)\n return () => a.pause()\n }, [])\n\n useEffect(() => {\n const a = audioRef.current\n if (!a) return\n a.pause()\n if (src) {\n a.src = src\n a.currentTime = 0\n if (playingRef.current) a.play().catch(() => {})\n } else {\n a.removeAttribute('src')\n a.load()\n setPlaying(false)\n }\n }, [src])\n\n const toggle = () => {\n const a = audioRef.current\n if (!a?.getAttribute('src')) return\n if (playingRef.current) a.pause()\n else a.play().catch(() => {})\n setPlaying(!playingRef.current)\n }\n\n const stop = () => {\n audioRef.current?.pause()\n setPlaying(false)\n }\n\n return { playing, toggle, stop, audio }\n}\n","import { useCallback, useEffect, useState } from 'react'\nimport type { ThemeKey } from '@graphlearning/flow'\n\nconst KEY = 'graphl:theme'\n\nconst readStored = (): ThemeKey | null => {\n try {\n const stored = localStorage.getItem(KEY)\n return stored === 'light' || stored === 'dark' ? stored : null\n } catch {\n return null\n }\n}\n\nexport function useTheme(deck: ThemeKey, capture: boolean) {\n const [stored, setStored] = useState(readStored)\n const theme = (!capture && stored) || deck\n\n useEffect(() => {\n document.documentElement.setAttribute('data-theme', theme)\n return () => document.documentElement.removeAttribute('data-theme')\n }, [theme])\n\n useEffect(() => {\n if (capture) return\n const onStorage = (e: StorageEvent) => {\n if (e.key === KEY || e.key === null) setStored(readStored())\n }\n window.addEventListener('storage', onStorage)\n return () => window.removeEventListener('storage', onStorage)\n }, [capture])\n\n const toggle = useCallback(() => {\n const next: ThemeKey = theme === 'dark' ? 'light' : 'dark'\n try {\n localStorage.setItem(KEY, next)\n } catch {}\n const root = document.documentElement\n root.classList.add('theme-swap')\n setStored(next)\n requestAnimationFrame(() => requestAnimationFrame(() => root.classList.remove('theme-swap')))\n }, [theme])\n\n return { theme, toggle }\n}\n","import { useCallback, useEffect, useMemo, useState } from 'react'\nimport { SceneView, type ThemeKey } from '@graphlearning/flow'\nimport { SectionView } from './SectionView'\nimport { allSections, slugOf, type Course, type SceneLookup } from './types'\nimport { CourseIndex } from './CourseIndex'\nimport { useNarration } from './useNarration'\nimport { useTheme } from './useTheme'\nimport { ThemeToggle } from './ThemeToggle'\nimport { SEEK_S } from './NarrationBar'\n\nconst idOf = (hash: string) => hash.replace(/^#\\/?/, '')\n\nexport function ConceptApp({\n subject,\n eyebrow,\n courses: COURSES,\n getScene,\n audioBase = '/',\n kind = 'courses',\n theme = 'dark',\n}: {\n subject: string\n courses: Record<string, Course>\n eyebrow?: string\n getScene: SceneLookup\n audioBase?: string\n kind?: string\n theme?: ThemeKey\n}) {\n const [hash, setHash] = useState(() => location.hash)\n useEffect(() => {\n const onHash = () => setHash(location.hash)\n window.addEventListener('hashchange', onHash)\n return () => window.removeEventListener('hashchange', onHash)\n }, [])\n\n const capture = new URLSearchParams(location.search).get('capture') === '1'\n const { theme: activeTheme, toggle: toggleTheme } = useTheme(theme, capture)\n const id = idOf(hash)\n\n const activeCourse = useMemo(\n () =>\n (Object.hasOwn(COURSES, id) && COURSES[id]) ||\n Object.values(COURSES).find((c) => c.sections.some((s) => slugOf(c, s) === id)) ||\n Object.values(COURSES)[0],\n [id, COURSES],\n )\n const sections = useMemo(() => (activeCourse ? allSections(activeCourse) : []), [activeCourse])\n const globalSections = useMemo(() => Object.values(COURSES).flatMap(allSections), [COURSES])\n\n useEffect(() => {\n if (capture || !Object.hasOwn(COURSES, id) || !sections[0]) return\n history.replaceState(null, '', `${location.pathname}${location.search}#/${sections[0].slug}`)\n setHash(`#/${sections[0].slug}`)\n }, [capture, id, sections])\n\n useEffect(() => {\n if (!capture) return\n ;(window as unknown as { __scene: unknown }).__scene = {\n plan: () =>\n sections.map(({ slug, section }) => ({\n slug,\n course: activeCourse?.id,\n id: section.id,\n scene: section.scene,\n focus: section.focus ?? null,\n })),\n }\n }, [capture, sections, activeCourse])\n\n const go = useCallback(\n (dir: 1 | -1) => {\n const cur = idOf(location.hash)\n const gidx = globalSections.findIndex(({ slug }) => slug === cur)\n if (gidx !== -1) {\n const target = globalSections[gidx + dir]\n location.hash = target ? `#/${target.slug}` : ''\n } else if (cur === '') {\n if (dir === 1 && globalSections[0]) location.hash = `#/${globalSections[0].slug}`\n } else {\n const target = dir === 1 ? sections[0] : sections[sections.length - 1]\n if (target) location.hash = `#/${target.slug}`\n }\n },\n [sections, globalSections],\n )\n\n const goHome = useCallback(() => {\n if (location.hash) location.hash = ''\n else if (location.pathname !== '/') location.href = '/'\n }, [])\n\n const match = sections.find(({ slug }) => slug === id)\n const audioUrl = match && `${audioBase}audio/${activeCourse!.id}/${match.section.id}.wav`\n const { playing, toggle, stop, audio } = useNarration(audioUrl, () => {\n const cur = idOf(location.hash)\n const next = globalSections[globalSections.findIndex(({ slug }) => slug === cur) + 1]\n if (next) location.hash = `#/${next.slug}`\n else stop()\n })\n\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if (e.key === 'Escape') return goHome()\n const t = e.target as HTMLElement | null\n if (e.ctrlKey || e.metaKey || e.altKey || t?.tagName === 'INPUT' || t?.tagName === 'TEXTAREA' || t?.isContentEditable) return\n if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {\n e.preventDefault()\n const dir = e.key === 'ArrowRight' ? 1 : -1\n const d = audio?.duration ?? 0\n if (audio && !e.shiftKey && Number.isFinite(d) && d > 0) {\n audio.currentTime = Math.min(d, Math.max(0, audio.currentTime + dir * SEEK_S))\n } else go(dir)\n } else if (e.key === ' ' || e.code === 'Space') {\n e.preventDefault()\n toggle()\n }\n }\n window.addEventListener('keydown', onKey)\n return () => window.removeEventListener('keydown', onKey)\n })\n\n if (!id)\n return (\n <CourseIndex\n courses={Object.values(COURSES)}\n subject={subject}\n kind={kind}\n actions={capture ? null : <ThemeToggle theme={activeTheme} onToggle={toggleTheme} />}\n />\n )\n\n if (match)\n return (\n <SectionView\n section={match.section}\n getScene={getScene}\n capture={capture}\n eyebrow={`${eyebrow ?? subject.toUpperCase()} · ${activeCourse!.id.toUpperCase()}`}\n index={sections.indexOf(match)}\n total={sections.length}\n onHome={goHome}\n onPrev={() => go(-1)}\n onNext={() => go(1)}\n narrating={playing}\n onToggleNarration={toggle}\n narrationAudio={audio}\n theme={activeTheme}\n onToggleTheme={capture ? undefined : toggleTheme}\n />\n )\n\n const scene = getScene(id)\n return (\n <>\n {!capture && (\n <button className=\"brand-home\" onClick={goHome} aria-label=\"Back to catalog (Esc)\" title=\"Back to catalog (Esc)\">\n GraphL\n </button>\n )}\n {scene ? (\n <div className=\"stage\">\n <SceneView scene={scene} theme={activeTheme} />\n </div>\n ) : (\n <div className=\"stage stage--missing\">no scene or slug: {id}</div>\n )}\n </>\n )\n}\n"],"names":["slugOf","course","section","allSections","DESIGN_PANE","useSlideScale","ref","useRef","zoom","setZoom","useState","useLayoutEffect","el","measure","ro","SlidePanel","slide","open","jsx","ReactMarkdown","remarkGfm","SEEK_S","EVENTS","fmt","s","t","NarrationBar","audio","time","setTime","duration","setDuration","useEffect","sync","e","seek","seekToX","track","clientX","left","width","onKeyDown","delta","pct","jsxs","FACE","themeLabel","theme","next","ThemeToggle","onToggle","label","tip","SectionView","getScene","capture","eyebrow","index","total","onHome","onPrev","onNext","narrating","onToggleNarration","narrationAudio","onToggleTheme","setOpen","scene","dark","Home","Volume2","VolumeX","Moon","Sun","ChevronLeft","ChevronRight","SceneView","o","KINDS","Mark","SiteHeader","kind","nav","home","actions","k","CourseIndex","courses","subject","toggle","id","prev","Fragment","i","sections","isOpen","slug","j","useNarration","src","onEnded","audioRef","setAudio","playing","setPlaying","endedRef","playingRef","a","_a","KEY","readStored","stored","useTheme","deck","setStored","onStorage","useCallback","root","idOf","hash","ConceptApp","COURSES","audioBase","setHash","onHash","activeTheme","toggleTheme","activeCourse","useMemo","c","globalSections","go","dir","cur","gidx","target","goHome","match","audioUrl","stop","onKey","d"],"mappings":"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"}