@graphlearning/shell 0.1.0 → 0.3.0

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/README.md CHANGED
@@ -29,6 +29,31 @@ import { getScene } from './scenes'
29
29
  That is the whole integration. A content repo's `src/` is then `content/`, `scenes/`, `main.tsx`
30
30
  and `theme.css`.
31
31
 
32
+ ## The site bar
33
+
34
+ `ConceptApp` renders the platform's header — brand, section nav, an empty actions slot — above its
35
+ catalog page, and nowhere else. It is a port of the bar at graphl.in, class for class, so a reader
36
+ crossing from the catalog into a concept app never leaves "the site". Nothing to pass: a content
37
+ repo is a course, which is the default.
38
+
39
+ It is deliberately platform-coloured, not `--brand`: looking identical on all eight sites is the
40
+ whole reason it exists. It renders on the catalog page ONLY — never on a section, which is a video
41
+ frame — so the recorder cannot see it.
42
+
43
+ An app that is not a course (`python-lab` is a lab) says so, and one that is not a `ConceptApp` at
44
+ all imports the bar directly — the one part of this package that is exported on its own:
45
+
46
+ ```tsx
47
+ <ConceptApp … kind="labs" /> // marks Labs as the current nav item
48
+
49
+ import { SiteHeader } from '@graphlearning/shell'
50
+ <SiteHeader kind="labs" actions={<ThemeToggle />} />
51
+ ```
52
+
53
+ `ui-graphl` is buildless vanilla and this is a React package, so the bar exists twice by necessity.
54
+ One design, two implementations — **when you change one, change the other**, or they drift and the
55
+ seam shows on every crossing.
56
+
32
57
  ## The three contracts
33
58
 
34
59
  **1. Import order.** The three stylesheets cascade: engine, shell, theme. The theme must come last
@@ -77,3 +102,43 @@ regression is visible here before it reaches any content repo.
77
102
  second react-flow store) inside this package. The content repo pins the engine itself and must get
78
103
  exactly one copy — and a wide peer range (`>=0.5.0 <1.0.0`) means an engine minor does not force a
79
104
  republish of this package.
105
+
106
+ ## The toolchain
107
+
108
+ The capture / record / publish scripts ship with this package too — they were byte-identical in all
109
+ seven repos and they drive the route contract above, so they version with the shell that defines it.
110
+ They are `bin` entries, wired through each repo's `npm run`:
111
+
112
+ | command | what it does |
113
+ |---|---|
114
+ | `npm run record` | one course → a 3840×2160 MP4 |
115
+ | `npm run record:reels` | one course → portrait reels |
116
+ | `npm run shots:4k` | one PNG per section, no ffmpeg |
117
+ | `npm run thumb` | a branded YouTube thumbnail |
118
+ | `npm run gen:desc` | video descriptions + chapters |
119
+ | `npm run gen:audio` | the narration manifest |
120
+
121
+ **Where things live.** Scripts resolve two roots explicitly (`scripts/_paths.mjs`), because from
122
+ `node_modules` they can no longer use their own directory:
123
+
124
+ - `repoDir` = `process.cwd()` — the content repo. `npm run` sets cwd to the package root.
125
+ - `dataDir` = `repoDir/scripts` — the repo's script data (`concept.json`, `titles.json`,
126
+ `audio-manifest.json`) **and every output** (`out/`, `segments/`, `.tmp/`), at exactly the paths
127
+ they used before the extraction, so existing `.gitignore` entries still hold.
128
+ - `pkgDir` — machinery that ships here (`thumb-template.html`).
129
+
130
+ **Peer resolution.** `puppeteer` and `esbuild` are optional peers, loaded via `loadPeer()` which
131
+ resolves them **from the repo**, not from this package. A bare `import('puppeteer')` resolves by
132
+ walking up from the importing file, which under a local `file:` install lands in `ui-shell/` and
133
+ never reaches the repo's `node_modules`.
134
+
135
+ **`scripts/concept.json`** is the per-repo publishing identity — the values that used to be
136
+ hardcoded defaults in each repo's copy, which is precisely why they drifted:
137
+
138
+ ```json
139
+ { "concept": "Apache Spark", "kicker": "SPARK", "site": "https://graphl.in",
140
+ "appPath": "/apache-spark", "hashtags": "#ApacheSpark …", "panelBg": "radial-gradient(…)" }
141
+ ```
142
+
143
+ `concept` is the name used in descriptions; `kicker` is the thumbnail panel's label and defaults to
144
+ `concept` — they differ only where a long name will not fit the panel (apache-spark).
@@ -1,8 +1,9 @@
1
1
  import type { Course, SceneLookup } from './types';
2
- export declare function ConceptApp({ subject, eyebrow, courses: COURSES, getScene, audioBase, }: {
2
+ export declare function ConceptApp({ subject, eyebrow, courses: COURSES, getScene, audioBase, kind, }: {
3
3
  subject: string;
4
4
  courses: Record<string, Course>;
5
5
  eyebrow?: string;
6
6
  getScene: SceneLookup;
7
7
  audioBase?: string;
8
+ kind?: string;
8
9
  }): import("react").JSX.Element;
@@ -1,5 +1,6 @@
1
1
  import type { Course } from './types';
2
- export declare function CourseIndex({ courses, subject }: {
2
+ export declare function CourseIndex({ courses, subject, kind, }: {
3
3
  courses: Course[];
4
4
  subject: string;
5
+ kind?: string;
5
6
  }): import("react").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import type { ReactNode } from 'react';
2
+ export declare function SiteHeader({ kind, nav, home, actions, }: {
3
+ kind?: string;
4
+ nav?: {
5
+ id: string;
6
+ label: string;
7
+ }[];
8
+ home?: string;
9
+ actions?: ReactNode;
10
+ }): import("react").JSX.Element;
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export type { Section, Course, SceneLookup } from './types';
2
2
  export { slugOf, allSections } from './types';
3
3
  export { ConceptApp } from './ConceptApp';
4
+ export { SiteHeader } from './SiteHeader';
package/dist/index.js CHANGED
@@ -1,269 +1,318 @@
1
- import { jsx as e, jsxs as h, Fragment as T } from "react/jsx-runtime";
2
- import { useRef as y, useState as x, useLayoutEffect as G, useEffect as _, useMemo as L, useCallback as C } from "react";
3
- import { SceneView as z } from "@graphlearning/flow";
4
- import { Home as V, Volume2 as D, VolumeX as K, ChevronLeft as M, ChevronRight as R } from "lucide-react";
1
+ import { jsx as e, jsxs as i, Fragment as I } from "react/jsx-runtime";
2
+ import { useRef as L, useState as b, useLayoutEffect as D, useEffect as N, useMemo as E, useCallback as z } from "react";
3
+ import { SceneView as S } from "@graphlearning/flow";
4
+ import { Home as G, Volume2 as K, VolumeX as V, ChevronLeft as R, ChevronRight as W } from "lucide-react";
5
5
  import X from "react-markdown";
6
6
  import F from "remark-gfm";
7
- function I(a, i) {
8
- return `${a.id}-${i.id}`;
7
+ function j(a, c) {
8
+ return `${a.id}-${c.id}`;
9
9
  }
10
- function E(a) {
11
- return a.sections.map((i) => ({ section: i, slug: I(a, i) }));
10
+ function P(a) {
11
+ return a.sections.map((c) => ({ section: c, slug: j(a, c) }));
12
12
  }
13
- const j = 806;
13
+ const B = 806;
14
14
  function O() {
15
- const a = y(null), [i, r] = x(1);
16
- return G(() => {
17
- const d = a.current;
18
- if (!d) return;
19
- const p = () => r(d.clientWidth / j);
20
- p();
21
- const c = new ResizeObserver(p);
22
- return c.observe(d), () => c.disconnect();
23
- }, []), { ref: a, zoom: i };
15
+ const a = L(null), [c, s] = b(1);
16
+ return D(() => {
17
+ const l = a.current;
18
+ if (!l) return;
19
+ const h = () => s(l.clientWidth / B);
20
+ h();
21
+ const f = new ResizeObserver(h);
22
+ return f.observe(l), () => f.disconnect();
23
+ }, []), { ref: a, zoom: c };
24
24
  }
25
- function W({ slide: a, open: i }) {
26
- const { ref: r, zoom: d } = O();
27
- return /* @__PURE__ */ e("aside", { ref: r, className: `slide-panel${i ? " slide-panel--open" : ""}`, children: /* @__PURE__ */ e("div", { className: "slide-panel__scaler", style: { width: j, zoom: d }, children: /* @__PURE__ */ e(X, { remarkPlugins: [F], children: a }) }) });
25
+ function Z({ slide: a, open: c }) {
26
+ const { ref: s, zoom: l } = O();
27
+ return /* @__PURE__ */ e("aside", { ref: s, className: `slide-panel${c ? " slide-panel--open" : ""}`, children: /* @__PURE__ */ e("div", { className: "slide-panel__scaler", style: { width: B, zoom: l }, children: /* @__PURE__ */ e(X, { remarkPlugins: [F], children: a }) }) });
28
28
  }
29
- function Z({
29
+ function q({
30
30
  section: a,
31
- getScene: i,
32
- capture: r = !1,
33
- eyebrow: d,
34
- index: p = 0,
35
- total: c = 0,
36
- onHome: f,
37
- onPrev: l,
38
- onNext: o,
39
- narrating: t = !1,
40
- onToggleNarration: u
31
+ getScene: c,
32
+ capture: s = !1,
33
+ eyebrow: l,
34
+ index: h = 0,
35
+ total: f = 0,
36
+ onHome: o,
37
+ onPrev: _,
38
+ onNext: u,
39
+ narrating: n = !1,
40
+ onToggleNarration: p
41
41
  }) {
42
- const [m, v] = x(!1), N = i(a.scene);
43
- return N ? /* @__PURE__ */ h("div", { className: "stage stage--section", children: [
44
- /* @__PURE__ */ h("header", { className: "reel-head", children: [
45
- d && /* @__PURE__ */ e("button", { className: "reel-head__eyebrow", onClick: f, "aria-label": "Back to catalog (Esc)", title: "Back to catalog (Esc)", children: d }),
42
+ const [d, m] = b(!1), v = c(a.scene);
43
+ return v ? /* @__PURE__ */ i("div", { className: "stage stage--section", children: [
44
+ /* @__PURE__ */ i("header", { className: "reel-head", children: [
45
+ l && /* @__PURE__ */ e("button", { className: "reel-head__eyebrow", onClick: o, "aria-label": "Back to catalog (Esc)", title: "Back to catalog (Esc)", children: l }),
46
46
  /* @__PURE__ */ e("h1", { className: "reel-head__title", children: a.title })
47
47
  ] }),
48
- !r && /* @__PURE__ */ h("footer", { className: "reel-foot reel-foot--controls", children: [
49
- /* @__PURE__ */ h("span", { className: "reel-foot__grp", children: [
50
- /* @__PURE__ */ e("button", { className: "reel-foot__ctrl", onClick: f, "aria-label": "Back to catalog (Esc)", title: "Back to catalog (Esc)", children: /* @__PURE__ */ e(V, { size: 17 }) }),
48
+ !s && /* @__PURE__ */ i("footer", { className: "reel-foot reel-foot--controls", children: [
49
+ /* @__PURE__ */ i("span", { className: "reel-foot__grp", children: [
50
+ /* @__PURE__ */ e("button", { className: "reel-foot__ctrl", onClick: o, "aria-label": "Back to catalog (Esc)", title: "Back to catalog (Esc)", children: /* @__PURE__ */ e(G, { size: 17 }) }),
51
51
  /* @__PURE__ */ e(
52
52
  "button",
53
53
  {
54
54
  className: "reel-foot__ctrl",
55
- onClick: u,
56
- "aria-label": t ? "Pause narration (Space)" : "Play narration (Space)",
57
- title: t ? "Pause narration (Space)" : "Play narration (Space)",
58
- children: t ? /* @__PURE__ */ e(D, { size: 18 }) : /* @__PURE__ */ e(K, { size: 18 })
55
+ onClick: p,
56
+ "aria-label": n ? "Pause narration (Space)" : "Play narration (Space)",
57
+ title: n ? "Pause narration (Space)" : "Play narration (Space)",
58
+ children: n ? /* @__PURE__ */ e(K, { size: 18 }) : /* @__PURE__ */ e(V, { size: 18 })
59
59
  }
60
60
  )
61
61
  ] }),
62
- /* @__PURE__ */ h("span", { className: "reel-foot__nav", children: [
63
- /* @__PURE__ */ e("button", { className: "reel-foot__ctrl", onClick: l, "aria-label": "Previous section (←)", title: "Previous section (←)", children: /* @__PURE__ */ e(M, { size: 19 }) }),
64
- /* @__PURE__ */ h("span", { className: "reel-foot__count reel-foot__count--live", children: [
65
- p + 1,
62
+ /* @__PURE__ */ i("span", { className: "reel-foot__nav", children: [
63
+ /* @__PURE__ */ e("button", { className: "reel-foot__ctrl", onClick: _, "aria-label": "Previous section (←)", title: "Previous section (←)", children: /* @__PURE__ */ e(R, { size: 19 }) }),
64
+ /* @__PURE__ */ i("span", { className: "reel-foot__count reel-foot__count--live", children: [
65
+ h + 1,
66
66
  " / ",
67
- c
67
+ f
68
68
  ] }),
69
- /* @__PURE__ */ e("button", { className: "reel-foot__ctrl", onClick: o, "aria-label": "Next section (→)", title: "Next section (→)", children: /* @__PURE__ */ e(R, { size: 19 }) })
69
+ /* @__PURE__ */ e("button", { className: "reel-foot__ctrl", onClick: u, "aria-label": "Next section (→)", title: "Next section (→)", children: /* @__PURE__ */ e(W, { size: 19 }) })
70
70
  ] })
71
71
  ] }),
72
- /* @__PURE__ */ e("div", { className: "scene-area", children: /* @__PURE__ */ e(z, { scene: N, focusId: a.focus }) }),
73
- !r && /* @__PURE__ */ e(
72
+ /* @__PURE__ */ e("div", { className: "scene-area", children: /* @__PURE__ */ e(S, { scene: v, focusId: a.focus }) }),
73
+ !s && /* @__PURE__ */ e(
74
74
  "button",
75
75
  {
76
76
  className: "slide-toggle",
77
- onClick: () => v((w) => !w),
78
- "aria-label": m ? "Hide slide" : "Show slide",
79
- children: m ? "›" : "‹"
77
+ onClick: () => m((w) => !w),
78
+ "aria-label": d ? "Hide slide" : "Show slide",
79
+ children: d ? "›" : "‹"
80
80
  }
81
81
  ),
82
- /* @__PURE__ */ e(W, { slide: a.slide, open: m })
83
- ] }) : /* @__PURE__ */ h("div", { className: "stage stage--missing", children: [
82
+ /* @__PURE__ */ e(Z, { slide: a.slide, open: d })
83
+ ] }) : /* @__PURE__ */ i("div", { className: "stage stage--missing", children: [
84
84
  "no scene: ",
85
85
  a.scene
86
86
  ] });
87
87
  }
88
- function q({ courses: a, subject: i }) {
89
- const [r, d] = x(() => /* @__PURE__ */ new Set()), p = (c) => d((f) => {
90
- const l = new Set(f);
91
- return l.has(c) ? l.delete(c) : l.add(c), l;
92
- });
93
- return /* @__PURE__ */ h("div", { className: "idx", children: [
94
- /* @__PURE__ */ h("header", { className: "idx__head", children: [
95
- /* @__PURE__ */ e("a", { className: "idx__brand", href: "/", children: "GraphL" }),
96
- /* @__PURE__ */ e("h1", { className: "idx__subject", children: i })
97
- ] }),
98
- a.length === 0 ? /* @__PURE__ */ e("p", { className: "idx__empty", children: "No courses authored yet — the app shell is ready." }) : /* @__PURE__ */ e("ol", { className: "idx__grid", children: a.map((c, f) => {
99
- const l = E(c), o = r.has(c.id);
100
- return /* @__PURE__ */ h("li", { className: `idx-card${o ? " idx-card--open" : ""}`, children: [
101
- /* @__PURE__ */ h(
102
- "button",
103
- {
104
- className: "idx-card__head",
105
- onClick: () => p(c.id),
106
- "aria-expanded": o,
107
- children: [
108
- /* @__PURE__ */ e("span", { className: "idx-card__num", children: String(f + 1).padStart(2, "0") }),
109
- /* @__PURE__ */ e("span", { className: "idx-card__title", children: c.title }),
110
- /* @__PURE__ */ h("span", { className: "idx-card__count", children: [
111
- l.length,
112
- " sections"
113
- ] }),
114
- /* @__PURE__ */ e("span", { className: "idx-card__toggle", "aria-hidden": "true", children: "⌄" })
115
- ]
116
- }
117
- ),
118
- o && /* @__PURE__ */ e("ol", { className: "idx-sec", children: l.map(({ section: t, slug: u }, m) => /* @__PURE__ */ e("li", { className: "idx-sec__row", children: /* @__PURE__ */ h("a", { href: `#/${u}`, children: [
119
- /* @__PURE__ */ e("span", { className: "idx-sec__num", children: m + 1 }),
120
- /* @__PURE__ */ e("span", { className: "idx-sec__title", children: t.title }),
121
- /* @__PURE__ */ e("span", { className: "idx-sec__chev", "aria-hidden": "true", children: "›" })
122
- ] }) }, u)) })
123
- ] }, c.id);
124
- }) }),
125
- /* @__PURE__ */ e("footer", { className: "idx__foot", children: /* @__PURE__ */ h("span", { className: "idx__hint", children: [
126
- /* @__PURE__ */ e("kbd", { children: "→" }),
127
- " next · ",
128
- /* @__PURE__ */ e("kbd", { children: "←" }),
129
- " prev · ",
130
- /* @__PURE__ */ e("kbd", { children: "Space" }),
131
- " narration"
132
- ] }) })
88
+ const J = [
89
+ { id: "courses", label: "Courses" },
90
+ { id: "labs", label: "Labs" },
91
+ { id: "coach", label: "Coach" }
92
+ ];
93
+ function Q() {
94
+ return /* @__PURE__ */ i("svg", { className: "site__logo", width: "28", height: "28", viewBox: "0 0 512 512", "aria-hidden": "true", children: [
95
+ /* @__PURE__ */ e("rect", { width: "512", height: "512", rx: "80", fill: "#e8804f" }),
96
+ /* @__PURE__ */ i("g", { fill: "none", stroke: "#ffffff", strokeWidth: "58", strokeLinecap: "butt", strokeLinejoin: "miter", children: [
97
+ /* @__PURE__ */ e("path", { d: "M 373 200 A 130 130 0 1 0 373 312" }),
98
+ /* @__PURE__ */ e("path", { d: "M 283 312 L 373 312 L 373 410" })
99
+ ] })
133
100
  ] });
134
101
  }
135
- function J(a, i) {
136
- const r = y(null), d = y(i);
137
- d.current = i;
138
- const [p, c] = x(!1), f = y(p);
139
- return f.current = p, _(() => {
140
- const t = new Audio();
141
- return t.addEventListener("ended", () => d.current()), t.addEventListener("error", () => c(!1)), r.current = t, () => t.pause();
142
- }, []), _(() => {
143
- const t = r.current;
144
- t && (t.pause(), a ? (t.src = a, t.currentTime = 0, f.current && t.play().catch(() => {
145
- })) : (t.removeAttribute("src"), t.load(), c(!1)));
146
- }, [a]), { playing: p, toggle: () => {
147
- const t = r.current;
148
- !t || !t.getAttribute("src") || (f.current ? (t.pause(), c(!1)) : (t.play().catch(() => {
149
- }), c(!0)));
102
+ function Y({
103
+ kind: a = "courses",
104
+ nav: c = J,
105
+ home: s = "/",
106
+ actions: l
107
+ }) {
108
+ return /* @__PURE__ */ e("header", { className: "site", children: /* @__PURE__ */ i("div", { className: "site__inner", children: [
109
+ /* @__PURE__ */ i("a", { className: "site__brand", href: s, children: [
110
+ /* @__PURE__ */ e(Q, {}),
111
+ /* @__PURE__ */ e("span", { className: "site__name", children: "GraphL" })
112
+ ] }),
113
+ /* @__PURE__ */ e("nav", { className: "site__nav", "aria-label": "Sections", children: c.map((h) => /* @__PURE__ */ e(
114
+ "a",
115
+ {
116
+ className: "site__link",
117
+ href: `${s}#${h.id}`,
118
+ ...h.id === a ? { "aria-current": "page" } : {},
119
+ children: h.label
120
+ },
121
+ h.id
122
+ )) }),
123
+ /* @__PURE__ */ e("div", { className: "site__actions", children: l })
124
+ ] }) });
125
+ }
126
+ function U({
127
+ courses: a,
128
+ subject: c,
129
+ kind: s
130
+ }) {
131
+ const [l, h] = b(() => /* @__PURE__ */ new Set()), f = (o) => h((_) => {
132
+ const u = new Set(_);
133
+ return u.has(o) ? u.delete(o) : u.add(o), u;
134
+ });
135
+ return (
136
+ // The site bar is a SIBLING of .idx, not a child: it is full-bleed (its rule spans the
137
+ // viewport) while .idx is a 940px centred column. Both use the same width and gutters, so the
138
+ // brand lines up with the card edge below it.
139
+ /* @__PURE__ */ i(I, { children: [
140
+ /* @__PURE__ */ e(Y, { kind: s }),
141
+ /* @__PURE__ */ i("div", { className: "idx", children: [
142
+ /* @__PURE__ */ e("header", { className: "idx__head", children: /* @__PURE__ */ e("h1", { className: "idx__subject", children: c }) }),
143
+ a.length === 0 ? /* @__PURE__ */ e("p", { className: "idx__empty", children: "No courses authored yet — the app shell is ready." }) : /* @__PURE__ */ e("ol", { className: "idx__grid", children: a.map((o, _) => {
144
+ const u = P(o), n = l.has(o.id);
145
+ return /* @__PURE__ */ i("li", { className: `idx-card${n ? " idx-card--open" : ""}`, children: [
146
+ /* @__PURE__ */ i(
147
+ "button",
148
+ {
149
+ className: "idx-card__head",
150
+ onClick: () => f(o.id),
151
+ "aria-expanded": n,
152
+ children: [
153
+ /* @__PURE__ */ e("span", { className: "idx-card__num", children: String(_ + 1).padStart(2, "0") }),
154
+ /* @__PURE__ */ e("span", { className: "idx-card__title", children: o.title }),
155
+ /* @__PURE__ */ i("span", { className: "idx-card__count", children: [
156
+ u.length,
157
+ " sections"
158
+ ] }),
159
+ /* @__PURE__ */ e("span", { className: "idx-card__toggle", "aria-hidden": "true", children: "⌄" })
160
+ ]
161
+ }
162
+ ),
163
+ n && /* @__PURE__ */ e("ol", { className: "idx-sec", children: u.map(({ section: p, slug: d }, m) => /* @__PURE__ */ e("li", { className: "idx-sec__row", children: /* @__PURE__ */ i("a", { href: `#/${d}`, children: [
164
+ /* @__PURE__ */ e("span", { className: "idx-sec__num", children: m + 1 }),
165
+ /* @__PURE__ */ e("span", { className: "idx-sec__title", children: p.title }),
166
+ /* @__PURE__ */ e("span", { className: "idx-sec__chev", "aria-hidden": "true", children: "›" })
167
+ ] }) }, d)) })
168
+ ] }, o.id);
169
+ }) }),
170
+ /* @__PURE__ */ e("footer", { className: "idx__foot", children: /* @__PURE__ */ i("span", { className: "idx__hint", children: [
171
+ /* @__PURE__ */ e("kbd", { children: "→" }),
172
+ " next · ",
173
+ /* @__PURE__ */ e("kbd", { children: "←" }),
174
+ " prev · ",
175
+ /* @__PURE__ */ e("kbd", { children: "Space" }),
176
+ " narration"
177
+ ] }) })
178
+ ] })
179
+ ] })
180
+ );
181
+ }
182
+ function ee(a, c) {
183
+ const s = L(null), l = L(c);
184
+ l.current = c;
185
+ const [h, f] = b(!1), o = L(h);
186
+ return o.current = h, N(() => {
187
+ const n = new Audio();
188
+ return n.addEventListener("ended", () => l.current()), n.addEventListener("error", () => f(!1)), s.current = n, () => n.pause();
189
+ }, []), N(() => {
190
+ const n = s.current;
191
+ n && (n.pause(), a ? (n.src = a, n.currentTime = 0, o.current && n.play().catch(() => {
192
+ })) : (n.removeAttribute("src"), n.load(), f(!1)));
193
+ }, [a]), { playing: h, toggle: () => {
194
+ const n = s.current;
195
+ !n || !n.getAttribute("src") || (o.current ? (n.pause(), f(!1)) : (n.play().catch(() => {
196
+ }), f(!0)));
150
197
  }, stop: () => {
151
- var t;
152
- (t = r.current) == null || t.pause(), c(!1);
198
+ var n;
199
+ (n = s.current) == null || n.pause(), f(!1);
153
200
  } };
154
201
  }
155
- function ae({
202
+ function ce({
156
203
  subject: a,
157
- eyebrow: i,
158
- courses: r,
159
- getScene: d,
160
- audioBase: p = "/"
204
+ eyebrow: c,
205
+ courses: s,
206
+ getScene: l,
207
+ audioBase: h = "/",
208
+ kind: f = "courses"
161
209
  }) {
162
- const [c, f] = x(() => location.hash);
163
- _(() => {
164
- const n = () => f(location.hash);
165
- return window.addEventListener("hashchange", n), () => window.removeEventListener("hashchange", n);
210
+ const [o, _] = b(() => location.hash);
211
+ N(() => {
212
+ const t = () => _(location.hash);
213
+ return window.addEventListener("hashchange", t), () => window.removeEventListener("hashchange", t);
166
214
  }, []);
167
- const l = new URLSearchParams(location.search).get("capture") === "1", o = c.replace(/^#\/?/, ""), t = L(() => {
168
- if (r[o]) return r[o];
169
- for (const n of Object.values(r))
170
- if (n.sections.some((s) => I(n, s) === o)) return n;
171
- return Object.values(r)[0];
172
- }, [o, r]), u = L(() => E(t), [t]), m = L(() => Object.values(r).flatMap(E), [r]);
173
- _(() => {
174
- if (l || !r[o]) return;
175
- const n = u[0];
176
- n && (history.replaceState(null, "", `${location.pathname}${location.search}#/${n.slug}`), f(`#/${n.slug}`));
177
- }, [l, o, u]), _(() => {
178
- l && (window.__scene = {
179
- plan: () => u.map(({ slug: n, section: s }) => ({
180
- slug: n,
181
- course: t.id,
215
+ const u = new URLSearchParams(location.search).get("capture") === "1", n = o.replace(/^#\/?/, ""), p = E(() => {
216
+ if (s[n]) return s[n];
217
+ for (const t of Object.values(s))
218
+ if (t.sections.some((r) => j(t, r) === n)) return t;
219
+ return Object.values(s)[0];
220
+ }, [n, s]), d = E(() => P(p), [p]), m = E(() => Object.values(s).flatMap(P), [s]);
221
+ N(() => {
222
+ if (u || !s[n]) return;
223
+ const t = d[0];
224
+ t && (history.replaceState(null, "", `${location.pathname}${location.search}#/${t.slug}`), _(`#/${t.slug}`));
225
+ }, [u, n, d]), N(() => {
226
+ u && (window.__scene = {
227
+ plan: () => d.map(({ slug: t, section: r }) => ({
228
+ slug: t,
229
+ course: p.id,
182
230
  // audio lives at audio/<course>/<id>.wav
183
- id: s.id,
184
- scene: s.scene,
185
- focus: s.focus ?? null
231
+ id: r.id,
232
+ scene: r.scene,
233
+ focus: r.focus ?? null
186
234
  }))
187
235
  });
188
- }, [l, u, t]);
189
- const v = C(
190
- (n) => {
191
- const s = location.hash.replace(/^#\/?/, ""), g = m.findIndex(({ slug: k }) => k === s);
236
+ }, [u, d, p]);
237
+ const v = z(
238
+ (t) => {
239
+ const r = location.hash.replace(/^#\/?/, ""), g = m.findIndex(({ slug: y }) => y === r);
192
240
  if (g !== -1) {
193
- const k = m[g + n];
194
- location.hash = k ? `#/${k.slug}` : "";
241
+ const y = m[g + t];
242
+ location.hash = y ? `#/${y.slug}` : "";
195
243
  return;
196
244
  }
197
- if (s === "") {
198
- n === 1 && m[0] && (location.hash = `#/${m[0].slug}`);
245
+ if (r === "") {
246
+ t === 1 && m[0] && (location.hash = `#/${m[0].slug}`);
199
247
  return;
200
248
  }
201
- const b = n === 1 ? u[0] : u[u.length - 1];
202
- b && (location.hash = `#/${b.slug}`);
249
+ const k = t === 1 ? d[0] : d[d.length - 1];
250
+ k && (location.hash = `#/${k.slug}`);
203
251
  },
204
- [u, m]
252
+ [d, m]
205
253
  );
206
- _(() => {
207
- const n = (s) => {
208
- s.key !== "ArrowRight" && s.key !== "ArrowLeft" || (s.preventDefault(), v(s.key === "ArrowRight" ? 1 : -1));
254
+ N(() => {
255
+ const t = (r) => {
256
+ r.key !== "ArrowRight" && r.key !== "ArrowLeft" || (r.preventDefault(), v(r.key === "ArrowRight" ? 1 : -1));
209
257
  };
210
- return window.addEventListener("keydown", n), () => window.removeEventListener("keydown", n);
258
+ return window.addEventListener("keydown", t), () => window.removeEventListener("keydown", t);
211
259
  }, [v]);
212
- const N = C(() => {
260
+ const w = z(() => {
213
261
  location.hash ? location.hash = "" : location.pathname !== "/" && (location.href = "/");
214
262
  }, []);
215
- _(() => {
216
- const n = (s) => {
217
- s.key === "Escape" && N();
263
+ N(() => {
264
+ const t = (r) => {
265
+ r.key === "Escape" && w();
218
266
  };
219
- return window.addEventListener("keydown", n), () => window.removeEventListener("keydown", n);
220
- }, [N]);
221
- const w = u.find(({ slug: n }) => n === o), B = w ? `${p}audio/${t.id}/${w.section.id}.wav` : void 0, { playing: H, toggle: P, stop: S } = J(B, () => {
222
- const n = location.hash.replace(/^#\/?/, ""), s = m.findIndex(({ slug: b }) => b === n), g = m[s + 1];
223
- g ? location.hash = `#/${g.slug}` : S();
267
+ return window.addEventListener("keydown", t), () => window.removeEventListener("keydown", t);
268
+ }, [w]);
269
+ const x = d.find(({ slug: t }) => t === n), H = x ? `${h}audio/${p.id}/${x.section.id}.wav` : void 0, { playing: M, toggle: A, stop: T } = ee(H, () => {
270
+ const t = location.hash.replace(/^#\/?/, ""), r = m.findIndex(({ slug: k }) => k === t), g = m[r + 1];
271
+ g ? location.hash = `#/${g.slug}` : T();
224
272
  });
225
- if (_(() => {
226
- const n = (s) => {
227
- if (s.key !== " " && s.code !== "Space") return;
228
- const g = s.target;
229
- g && (g.tagName === "INPUT" || g.tagName === "TEXTAREA" || g.isContentEditable) || (s.preventDefault(), P());
273
+ if (N(() => {
274
+ const t = (r) => {
275
+ if (r.key !== " " && r.code !== "Space") return;
276
+ const g = r.target;
277
+ g && (g.tagName === "INPUT" || g.tagName === "TEXTAREA" || g.isContentEditable) || (r.preventDefault(), A());
230
278
  };
231
- return window.addEventListener("keydown", n), () => window.removeEventListener("keydown", n);
232
- }), !o) return /* @__PURE__ */ e(q, { courses: Object.values(r), subject: a });
233
- let $, A = null;
234
- if (w) {
235
- const n = u.findIndex(({ slug: s }) => s === o);
279
+ return window.addEventListener("keydown", t), () => window.removeEventListener("keydown", t);
280
+ }), !n) return /* @__PURE__ */ e(U, { courses: Object.values(s), subject: a, kind: f });
281
+ let $, C = null;
282
+ if (x) {
283
+ const t = d.findIndex(({ slug: r }) => r === n);
236
284
  $ = /* @__PURE__ */ e(
237
- Z,
285
+ q,
238
286
  {
239
- section: w.section,
240
- getScene: d,
241
- capture: l,
242
- eyebrow: `${i ?? a.toUpperCase()} · ${t.id.toUpperCase()}`,
243
- index: n,
244
- total: u.length,
245
- onHome: N,
287
+ section: x.section,
288
+ getScene: l,
289
+ capture: u,
290
+ eyebrow: `${c ?? a.toUpperCase()} · ${p.id.toUpperCase()}`,
291
+ index: t,
292
+ total: d.length,
293
+ onHome: w,
246
294
  onPrev: () => v(-1),
247
295
  onNext: () => v(1),
248
- narrating: H,
249
- onToggleNarration: P
296
+ narrating: M,
297
+ onToggleNarration: A
250
298
  }
251
299
  );
252
300
  } else {
253
- const n = d(o);
254
- $ = n ? /* @__PURE__ */ e("div", { className: "stage", children: /* @__PURE__ */ e(z, { scene: n }) }) : /* @__PURE__ */ h("div", { className: "stage stage--missing", children: [
301
+ const t = l(n);
302
+ $ = t ? /* @__PURE__ */ e("div", { className: "stage", children: /* @__PURE__ */ e(S, { scene: t }) }) : /* @__PURE__ */ i("div", { className: "stage stage--missing", children: [
255
303
  "no scene or slug: ",
256
- o
257
- ] }), A = l ? null : /* @__PURE__ */ e("button", { className: "brand-home", onClick: N, "aria-label": "Back to catalog (Esc)", title: "Back to catalog (Esc)", children: "GraphL" });
304
+ n
305
+ ] }), C = u ? null : /* @__PURE__ */ e("button", { className: "brand-home", onClick: w, "aria-label": "Back to catalog (Esc)", title: "Back to catalog (Esc)", children: "GraphL" });
258
306
  }
259
- return /* @__PURE__ */ h(T, { children: [
260
- A,
307
+ return /* @__PURE__ */ i(I, { children: [
308
+ C,
261
309
  $
262
310
  ] });
263
311
  }
264
312
  export {
265
- ae as ConceptApp,
266
- E as allSections,
267
- I as slugOf
313
+ ce as ConceptApp,
314
+ Y as SiteHeader,
315
+ P as allSections,
316
+ j as slugOf
268
317
  };
269
318
  //# sourceMappingURL=index.js.map