@graphlearning/shell 0.10.0 → 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/ConceptApp.d.ts +1 -1
- package/dist/CourseIndex.d.ts +2 -2
- package/dist/SectionView.d.ts +11 -11
- package/dist/ThemeToggle.d.ts +1 -0
- package/dist/index.js +224 -304
- package/dist/index.js.map +1 -1
- package/dist/styles.css +73 -253
- package/dist/types.d.ts +4 -3
- package/dist/useTheme.d.ts +0 -12
- package/package.json +1 -1
- package/scripts/_paths.mjs +106 -38
- package/scripts/capture-shots.mjs +1 -14
- package/scripts/gen-audio-manifest.mjs +2 -30
- package/scripts/gen-descriptions.mjs +5 -73
- package/scripts/record-all.mjs +1 -53
- package/scripts/record-course.mjs +6 -168
- package/scripts/record-reels.mjs +5 -139
- package/scripts/shots-4k.mjs +6 -7
- package/scripts/thumb-template.html +4 -11
- package/scripts/thumb.mjs +10 -105
package/dist/styles.css
CHANGED
|
@@ -1,103 +1,49 @@
|
|
|
1
|
-
/* @graphlearning/shell — the concept-app stylesheet.
|
|
2
|
-
*
|
|
3
|
-
* Import ONCE in a content repo's main.tsx, and import it BEFORE the repo's own theme.css:
|
|
4
|
-
* import '@graphlearning/flow/styles.css' // engine: react-flow + the IBM Plex faces
|
|
5
|
-
* import '@graphlearning/shell/styles.css' // this file: the shell's layout + surface
|
|
6
|
-
* import './theme.css' // the repo: --brand / --brand-hover / --accent-2
|
|
7
|
-
*
|
|
8
|
-
* THE SHELL PAINTS THE CANVAS. ui-flow's SceneView draws its dots at #2a2f38 and assumes a dark
|
|
9
|
-
* surface behind it; that surface (--bg) is defined here. This is the contract each content repo
|
|
10
|
-
* used to re-declare by hand in its own index.css.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
1
|
:root {
|
|
14
|
-
--bg: #1a1d23;
|
|
2
|
+
--bg: #1a1d23;
|
|
15
3
|
--fg: #eceef2;
|
|
16
|
-
|
|
17
|
-
theme. There were ~20 literal rgba(255,255,255,a) values here — all of them "ink at some alpha"
|
|
18
|
-
— and one token converts every one by substitution rather than needing eleven alpha tokens.
|
|
19
|
-
Dark keeps the exact values it always had. */
|
|
4
|
+
|
|
20
5
|
--ink-rgb: 255, 255, 255;
|
|
21
|
-
|
|
6
|
+
|
|
22
7
|
--surface-lift: #2f333d;
|
|
23
8
|
--surface-lift-2: #22262e;
|
|
24
|
-
--slide-ink: #c8cdd6;
|
|
25
|
-
--foot-dim: #5b616f;
|
|
26
|
-
--foot-dimmer: #4b515e;
|
|
27
|
-
|
|
28
|
-
over whatever the scene puts under them. It has to follow --bg, so it cannot be alpha over
|
|
29
|
-
--ink-rgb like the rest — on light, ink-at-90% would be a near-black button. */
|
|
9
|
+
--slide-ink: #c8cdd6;
|
|
10
|
+
--foot-dim: #5b616f;
|
|
11
|
+
--foot-dimmer: #4b515e;
|
|
12
|
+
|
|
30
13
|
--scrim: rgba(26, 29, 35, 0.9);
|
|
31
|
-
|
|
32
|
-
These are NEUTRAL DEFAULTS. Each repo overrides them in its own theme.css, imported AFTER
|
|
33
|
-
this stylesheet so the cascade lets the repo win. They are the entire per-repo design
|
|
34
|
-
surface — everything else in this file is identical for every concept.
|
|
35
|
-
--brand the one accent: section eyebrow, slide title (h2), catalog accent, brand hover
|
|
36
|
-
--brand-hover a lighter tint of --brand, for the eyebrow's hover state
|
|
37
|
-
--accent-2 a counter tone to --brand, for the slide's `###` subheading
|
|
38
|
-
Defaults rather than nothing: a repo that forgets theme.css renders in slate and is obviously
|
|
39
|
-
unbranded, instead of painting invalid colours and losing the text entirely. */
|
|
14
|
+
|
|
40
15
|
--brand: #8b909c;
|
|
41
16
|
--brand-hover: #a8adb8;
|
|
42
17
|
--accent-2: #c8cdd6;
|
|
43
18
|
--font-sans: 'IBM Plex Sans', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
|
44
19
|
|
|
45
|
-
/* --- site-chrome tokens (SiteHeader) ------------------------------------------------------
|
|
46
|
-
The same names and the same values ui-graphl declares, so the bar is identical on the catalog
|
|
47
|
-
and inside every app. Do NOT rename them: when light mode arrives it is purely additive here
|
|
48
|
-
— a light :root plus prefers-color-scheme and [data-theme] blocks, same names — and the
|
|
49
|
-
header's own CSS never has to change again. --text aliases the shell's existing --fg (same
|
|
50
|
-
value) rather than duplicating the colour. These are PLATFORM tokens: the bar stays
|
|
51
|
-
platform-blue on every site, deliberately NOT --brand, because looking the same everywhere is
|
|
52
|
-
the whole reason it exists. */
|
|
53
20
|
--text: var(--fg);
|
|
54
21
|
--muted: #8b909c;
|
|
55
22
|
--accent: #5b8cff;
|
|
56
|
-
--accent-soft: rgba(91, 140, 255, 0.14);
|
|
23
|
+
--accent-soft: rgba(91, 140, 255, 0.14);
|
|
57
24
|
--line: rgba(var(--ink-rgb), 0.09);
|
|
58
25
|
--header-bg: rgba(var(--ink-rgb), 0.014);
|
|
59
26
|
--hover: rgba(var(--ink-rgb), 0.05);
|
|
60
|
-
--site-h: 60px;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/* LIGHT — same token NAMES, different values, exactly as the note above anticipated. Set by
|
|
64
|
-
ConceptApp on <html> from its `theme` prop; nothing selects on it but this block.
|
|
65
|
-
*
|
|
66
|
-
* WHY AN EXPLICIT ATTRIBUTE AND NOT prefers-color-scheme: a deck is a VIDEO. The recorded frame
|
|
67
|
-
* cannot depend on the machine that renders it, or the same slug captures differently on two
|
|
68
|
-
* laptops. The repo chooses the theme at build time and the recorder inherits it.
|
|
69
|
-
*
|
|
70
|
-
* --line / --header-bg / --hover / --idx-line are NOT redeclared: they are alpha over --ink-rgb,
|
|
71
|
-
* so flipping the channels flips them too. That is the whole reason --ink-rgb exists.
|
|
72
|
-
*
|
|
73
|
-
* NOT TOUCHED HERE: --brand, --brand-hover and --accent-2. Those belong to the content repo
|
|
74
|
-
* (theme.css, loaded after this file), and a brand tuned for glow on slate can be unreadable on
|
|
75
|
-
* off-white — python's is a mid-blue that drops to ~2.5:1 on #f7f8fa. A repo going light must
|
|
76
|
-
* re-pick its three tokens; the shell cannot do it for them without taking ownership of the one
|
|
77
|
-
* surface it deliberately leaves alone. */
|
|
27
|
+
--site-h: 60px;
|
|
28
|
+
}
|
|
29
|
+
|
|
78
30
|
:root[data-theme='light'] {
|
|
79
|
-
--bg: #f7f8fa;
|
|
31
|
+
--bg: #f7f8fa;
|
|
80
32
|
--fg: #1a1d23;
|
|
81
33
|
--ink-rgb: 26, 29, 35;
|
|
82
|
-
|
|
83
|
-
brighter than the page, not darker — so white, not a grey. */
|
|
34
|
+
|
|
84
35
|
--surface-lift: #ffffff;
|
|
85
36
|
--surface-lift-2: #eef1f5;
|
|
86
37
|
--slide-ink: #333a45;
|
|
87
38
|
--foot-dim: #7a8493;
|
|
88
39
|
--foot-dimmer: #98a2b1;
|
|
89
40
|
--muted: #5a6675;
|
|
90
|
-
|
|
91
|
-
on slate and reads washed out here. Still platform blue, still not --brand. */
|
|
41
|
+
|
|
92
42
|
--accent: #2f6fe0;
|
|
93
43
|
--accent-soft: rgba(47, 111, 224, 0.12);
|
|
94
44
|
--scrim: rgba(247, 248, 250, 0.9);
|
|
95
45
|
}
|
|
96
46
|
|
|
97
|
-
/* Set for the single frame a theme swap lands in, so the whole page changes at once instead of the
|
|
98
|
-
ground flipping while everything with a hover transition cross-fades after it. Ported from
|
|
99
|
-
ui-graphl's styles.css — the site bar exists three times and they only stay one design if every
|
|
100
|
-
change lands in all of them. */
|
|
101
47
|
.theme-swap * {
|
|
102
48
|
transition: none !important;
|
|
103
49
|
}
|
|
@@ -116,8 +62,6 @@ body {
|
|
|
116
62
|
-webkit-font-smoothing: antialiased;
|
|
117
63
|
}
|
|
118
64
|
|
|
119
|
-
/* The stage fills the viewport; at capture time the viewport is the target frame (4K landscape or
|
|
120
|
-
1080×1920 portrait), so the composition fills it exactly. */
|
|
121
65
|
.stage {
|
|
122
66
|
width: 100vw;
|
|
123
67
|
height: 100vh;
|
|
@@ -131,14 +75,10 @@ body {
|
|
|
131
75
|
opacity: 0.7;
|
|
132
76
|
}
|
|
133
77
|
|
|
134
|
-
/* react-flow theming to sit on the dark stage */
|
|
135
78
|
.react-flow {
|
|
136
79
|
background: var(--bg);
|
|
137
80
|
}
|
|
138
81
|
|
|
139
|
-
/* --- Section layout (scene + fixed-right slide surface) -------------------- */
|
|
140
|
-
/* The slide is a full-height surface LIFTED off the scene canvas (figure-ground), fixed right.
|
|
141
|
-
Base = PORTRAIT (mobile → reel): scene fills the frame; slide is an off-canvas right drawer. */
|
|
142
82
|
.stage--section {
|
|
143
83
|
position: relative;
|
|
144
84
|
overflow: hidden;
|
|
@@ -152,13 +92,13 @@ body {
|
|
|
152
92
|
top: 0;
|
|
153
93
|
right: 0;
|
|
154
94
|
height: 100%;
|
|
155
|
-
width: 100%;
|
|
95
|
+
width: 100%;
|
|
156
96
|
display: flex;
|
|
157
97
|
align-items: safe center;
|
|
158
98
|
justify-content: center;
|
|
159
99
|
overflow: auto;
|
|
160
100
|
box-sizing: border-box;
|
|
161
|
-
background: var(--surface-lift);
|
|
101
|
+
background: var(--surface-lift);
|
|
162
102
|
border-left: 1px solid rgba(var(--ink-rgb), 0.09);
|
|
163
103
|
transform: translateX(100%);
|
|
164
104
|
transition: transform 0.32s cubic-bezier(0.4, 0, 0.2, 1);
|
|
@@ -167,41 +107,43 @@ body {
|
|
|
167
107
|
.slide-panel--open {
|
|
168
108
|
transform: translateX(0);
|
|
169
109
|
}
|
|
170
|
-
.slide-toggle
|
|
171
|
-
|
|
172
|
-
top: 14px;
|
|
173
|
-
right: 14px;
|
|
174
|
-
z-index: 11;
|
|
110
|
+
.slide-toggle,
|
|
111
|
+
.reel-foot__ctrl {
|
|
175
112
|
width: 36px;
|
|
176
113
|
height: 36px;
|
|
177
114
|
border-radius: 10px;
|
|
178
115
|
border: 1px solid rgba(var(--ink-rgb), 0.12);
|
|
179
116
|
background: var(--scrim);
|
|
180
117
|
color: rgba(var(--ink-rgb), 0.82);
|
|
181
|
-
font-size: 19px;
|
|
182
|
-
line-height: 1;
|
|
183
118
|
cursor: pointer;
|
|
184
119
|
transition: color 0.15s, border-color 0.15s;
|
|
185
120
|
-webkit-tap-highlight-color: transparent;
|
|
186
121
|
}
|
|
122
|
+
.slide-toggle {
|
|
123
|
+
position: absolute;
|
|
124
|
+
top: 14px;
|
|
125
|
+
right: 14px;
|
|
126
|
+
z-index: 11;
|
|
127
|
+
font-size: 19px;
|
|
128
|
+
line-height: 1;
|
|
129
|
+
}
|
|
187
130
|
@media (hover: hover) {
|
|
188
|
-
.slide-toggle:hover
|
|
189
|
-
|
|
190
|
-
|
|
131
|
+
.slide-toggle:hover,
|
|
132
|
+
.reel-foot__ctrl:hover {
|
|
133
|
+
color: var(--brand);
|
|
134
|
+
border-color: var(--brand);
|
|
191
135
|
}
|
|
192
136
|
}
|
|
193
|
-
.slide-toggle:focus:not(:focus-visible)
|
|
137
|
+
.slide-toggle:focus:not(:focus-visible),
|
|
138
|
+
.reel-foot__ctrl:focus:not(:focus-visible) {
|
|
194
139
|
outline: none;
|
|
195
140
|
}
|
|
196
|
-
.slide-toggle:focus-visible
|
|
197
|
-
|
|
141
|
+
.slide-toggle:focus-visible,
|
|
142
|
+
.reel-foot__ctrl:focus-visible {
|
|
143
|
+
outline: 2px solid var(--brand);
|
|
198
144
|
outline-offset: 2px;
|
|
199
145
|
}
|
|
200
146
|
|
|
201
|
-
/* Footer control bar (interactive only). Raised above the portrait drawer so Home + narration + nav
|
|
202
|
-
stay usable while the slide is open. Compound selector (.reel-foot.reel-foot--controls, 0,2,0) so it
|
|
203
|
-
outranks the base .reel-foot rule (z-index 8 / align baseline), which is defined LATER in the file —
|
|
204
|
-
at equal specificity that base would win here. */
|
|
205
147
|
.reel-foot.reel-foot--controls {
|
|
206
148
|
z-index: 12;
|
|
207
149
|
align-items: center;
|
|
@@ -216,35 +158,7 @@ body {
|
|
|
216
158
|
display: inline-flex;
|
|
217
159
|
align-items: center;
|
|
218
160
|
justify-content: center;
|
|
219
|
-
|
|
220
|
-
height: 36px;
|
|
221
|
-
border-radius: 10px;
|
|
222
|
-
border: 1px solid rgba(var(--ink-rgb), 0.12);
|
|
223
|
-
background: var(--scrim);
|
|
224
|
-
color: rgba(var(--ink-rgb), 0.82);
|
|
225
|
-
cursor: pointer;
|
|
226
|
-
pointer-events: auto; /* the footer container is pointer-events:none; re-enable the buttons */
|
|
227
|
-
transition: color 0.15s, border-color 0.15s;
|
|
228
|
-
-webkit-tap-highlight-color: transparent; /* no grey/blue tap flash on mobile */
|
|
229
|
-
}
|
|
230
|
-
/* Hover feedback only on real-pointer devices — on touch, :hover "sticks" after a tap and leaves the
|
|
231
|
-
icon highlighted until you tap elsewhere.
|
|
232
|
-
--brand, NOT --idx-accent: that token is declared on `.idx` (the catalog column) and does not
|
|
233
|
-
exist on a section, so every --idx-accent here resolved to nothing and the hover/focus accent was
|
|
234
|
-
silently dead. --brand is what .idx aliases it to anyway. */
|
|
235
|
-
@media (hover: hover) {
|
|
236
|
-
.reel-foot__ctrl:hover {
|
|
237
|
-
color: var(--brand);
|
|
238
|
-
border-color: var(--brand);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
/* Drop the focus ring on click/tap; keep a keyboard-only ring for accessibility. */
|
|
242
|
-
.reel-foot__ctrl:focus:not(:focus-visible) {
|
|
243
|
-
outline: none;
|
|
244
|
-
}
|
|
245
|
-
.reel-foot__ctrl:focus-visible {
|
|
246
|
-
outline: 2px solid var(--brand);
|
|
247
|
-
outline-offset: 2px;
|
|
161
|
+
pointer-events: auto;
|
|
248
162
|
}
|
|
249
163
|
.reel-foot__count--live {
|
|
250
164
|
pointer-events: none;
|
|
@@ -252,12 +166,6 @@ body {
|
|
|
252
166
|
text-align: center;
|
|
253
167
|
}
|
|
254
168
|
|
|
255
|
-
/* Narration scrub bar — the middle child of the control cluster (NarrationBar). LANDSCAPE ONLY: it
|
|
256
|
-
is `display: none` here and switched on in the landscape block below. In portrait the row is
|
|
257
|
-
already full at phone width (three controls + the pager leave ~60px), and the only other place to
|
|
258
|
-
put it is a second row — which would grow this absolutely-positioned footer UP into the frame,
|
|
259
|
-
since the portrait scene reserve (.stage--section .scene-area, bottom: 7.5vh) is sized for one
|
|
260
|
-
row. Widening that reserve would re-frame every recorded reel, so the bar yields instead. */
|
|
261
169
|
.reel-foot__audio {
|
|
262
170
|
display: none;
|
|
263
171
|
align-items: center;
|
|
@@ -270,8 +178,7 @@ body {
|
|
|
270
178
|
text-align: center;
|
|
271
179
|
font-size: 1.3vh;
|
|
272
180
|
}
|
|
273
|
-
|
|
274
|
-
visible rail drawn as a pseudo-element inside it. */
|
|
181
|
+
|
|
275
182
|
.reel-foot__track {
|
|
276
183
|
position: relative;
|
|
277
184
|
flex: 1 1 auto;
|
|
@@ -280,8 +187,8 @@ body {
|
|
|
280
187
|
display: flex;
|
|
281
188
|
align-items: center;
|
|
282
189
|
cursor: pointer;
|
|
283
|
-
pointer-events: auto;
|
|
284
|
-
touch-action: none;
|
|
190
|
+
pointer-events: auto;
|
|
191
|
+
touch-action: none;
|
|
285
192
|
-webkit-tap-highlight-color: transparent;
|
|
286
193
|
}
|
|
287
194
|
.reel-foot__track::before {
|
|
@@ -312,8 +219,7 @@ body {
|
|
|
312
219
|
transition: transform 0.12s;
|
|
313
220
|
pointer-events: none;
|
|
314
221
|
}
|
|
315
|
-
|
|
316
|
-
reads as a thin progress line (the footer sits over the scene). */
|
|
222
|
+
|
|
317
223
|
@media (hover: hover) {
|
|
318
224
|
.reel-foot__track:hover .reel-foot__thumb {
|
|
319
225
|
transform: translateY(-50%) scale(1);
|
|
@@ -323,13 +229,7 @@ body {
|
|
|
323
229
|
.reel-foot__track:focus-visible .reel-foot__thumb {
|
|
324
230
|
transform: translateY(-50%) scale(1);
|
|
325
231
|
}
|
|
326
|
-
|
|
327
|
-
rectangle ~500px wide across the scene, and it showed up where nothing had been clicked: Chrome
|
|
328
|
-
promotes an ALREADY mouse-focused element to :focus-visible on the next key press, so a reader who
|
|
329
|
-
click-scrubbed and then pressed Space (narration — a window-level shortcut with nothing to do with
|
|
330
|
-
this bar) got the rectangle, and it stayed for the rest of the section. The thumb carries the same
|
|
331
|
-
information at the size of the control, so keyboard focus is still visible without painting the
|
|
332
|
-
frame. Keep the ring on `.reel-foot__ctrl` — those are 28px buttons where a box IS the control. */
|
|
232
|
+
|
|
333
233
|
.reel-foot__track:focus {
|
|
334
234
|
outline: none;
|
|
335
235
|
}
|
|
@@ -337,9 +237,6 @@ body {
|
|
|
337
237
|
box-shadow: 0 0 0 3px rgba(var(--ink-rgb), 0.3);
|
|
338
238
|
}
|
|
339
239
|
|
|
340
|
-
/* GraphL wordmark — top-left of the bare SCENE view; click or Esc → back to catalog. (The section
|
|
341
|
-
view brands via its own header/footer, whose eyebrow is the home link.)
|
|
342
|
-
Fixed to the viewport so it sits above the stage regardless of layout; hidden under ?capture=1. */
|
|
343
240
|
.brand-home {
|
|
344
241
|
position: fixed;
|
|
345
242
|
top: 14px;
|
|
@@ -358,16 +255,9 @@ body {
|
|
|
358
255
|
transition: color 0.2s;
|
|
359
256
|
}
|
|
360
257
|
.brand-home:hover {
|
|
361
|
-
color: var(--brand);
|
|
258
|
+
color: var(--brand);
|
|
362
259
|
}
|
|
363
260
|
|
|
364
|
-
/* Section header/footer — shared chrome for the interactive app AND the captured video. It brands the
|
|
365
|
-
empty canvas around the fitView-centred scene: an "AWS · FOUNDATIONS" eyebrow (also the back-to-
|
|
366
|
-
catalog link) + the section title up top, a GraphL brand + §n/N counter below. Sizes are in vh so
|
|
367
|
-
they hold the SAME pixels at the capture frames (3840×2160 / 1080×1920) yet scale proportionally to
|
|
368
|
-
any interactive window. The bands are pointer-events:none (they overlay the scene) except the
|
|
369
|
-
eyebrow button, which re-enables clicks; z-index sits below the portrait slide drawer (z 10) so an
|
|
370
|
-
open drawer covers them. */
|
|
371
261
|
.reel-head {
|
|
372
262
|
position: absolute;
|
|
373
263
|
top: 0;
|
|
@@ -388,8 +278,8 @@ body {
|
|
|
388
278
|
font-weight: 700;
|
|
389
279
|
letter-spacing: 0.18em;
|
|
390
280
|
text-transform: uppercase;
|
|
391
|
-
color: var(--brand);
|
|
392
|
-
pointer-events: auto;
|
|
281
|
+
color: var(--brand);
|
|
282
|
+
pointer-events: auto;
|
|
393
283
|
cursor: pointer;
|
|
394
284
|
transition: color 0.2s;
|
|
395
285
|
}
|
|
@@ -416,81 +306,51 @@ body {
|
|
|
416
306
|
justify-content: space-between;
|
|
417
307
|
pointer-events: none;
|
|
418
308
|
}
|
|
419
|
-
.reel-foot__brand {
|
|
420
|
-
font-size: 1.55vh;
|
|
421
|
-
font-weight: 700;
|
|
422
|
-
letter-spacing: -0.01em;
|
|
423
|
-
color: rgba(var(--ink-rgb), 0.9);
|
|
424
|
-
}
|
|
425
309
|
.reel-foot__count {
|
|
426
310
|
font-size: 1.4vh;
|
|
427
311
|
color: rgba(var(--ink-rgb), 0.5);
|
|
428
312
|
font-variant-numeric: tabular-nums;
|
|
429
313
|
letter-spacing: 0.04em;
|
|
430
314
|
}
|
|
431
|
-
|
|
432
|
-
view shows the header/footer, so the reserve is scoped to the section (not just capture). */
|
|
315
|
+
|
|
433
316
|
.stage--section .scene-area {
|
|
434
317
|
top: 12.5vh;
|
|
435
318
|
bottom: 7.5vh;
|
|
436
319
|
}
|
|
437
320
|
|
|
438
|
-
/* LANDSCAPE (laptop → 4K video): the slide is a true 42% COLUMN — no absolute cap, so it stays 42%
|
|
439
|
-
of whatever the frame is, 4K included. The scene fills the rest; drawer transform/toggle off. */
|
|
440
321
|
@media (orientation: landscape) {
|
|
441
322
|
.stage--section {
|
|
442
323
|
display: flex;
|
|
443
324
|
}
|
|
444
|
-
/* The scene is a 9:16 PHONE FRAME at full height — the shape a reel is — centred in the left 58%
|
|
445
|
-
(the slide keeps its 42% column). Width follows height (56.25vh), capped at the column so a
|
|
446
|
-
squat window shrinks the frame's width, never its height. --frame-gap is the empty band each
|
|
447
|
-
side of it; the header and footer are pulled in to the frame's edges so they brand the frame,
|
|
448
|
-
not the empty bands. */
|
|
449
|
-
.stage--section {
|
|
450
|
-
--frame-w: min(56.25vh, 58vw);
|
|
451
|
-
--frame-gap: calc((58vw - var(--frame-w)) / 2);
|
|
452
|
-
}
|
|
453
325
|
.scene-area {
|
|
454
326
|
position: relative;
|
|
455
|
-
flex:
|
|
456
|
-
width:
|
|
457
|
-
margin: 0 var(--frame-gap);
|
|
327
|
+
flex: 1 1 auto;
|
|
328
|
+
min-width: 0;
|
|
458
329
|
height: 100%;
|
|
459
330
|
inset: auto;
|
|
460
331
|
}
|
|
461
|
-
|
|
462
|
-
can never paint over it and the frame keeps its exact 9:16 size). Interactive only — a recorded
|
|
463
|
-
frame must not carry a border the reader-facing bands exist to show. */
|
|
464
|
-
.stage--section:not(.stage--capture) .scene-area {
|
|
465
|
-
box-shadow: 0 0 0 1px rgba(var(--ink-rgb), 0.2);
|
|
466
|
-
}
|
|
467
|
-
/* Header/footer carry over but constrained to the FRAME. The section title is dropped here — it
|
|
468
|
-
already headlines the slide — leaving the eyebrow on top and the GraphL + §n/N line below,
|
|
469
|
-
sitting in the scene's fitView margins. Font sizes (vh) are shared with portrait. */
|
|
332
|
+
|
|
470
333
|
.reel-head {
|
|
471
|
-
|
|
472
|
-
right: calc(42% + var(--frame-gap));
|
|
334
|
+
right: 42%;
|
|
473
335
|
padding: 2.8vh 0 0 1.9vw;
|
|
474
336
|
}
|
|
475
337
|
.reel-head__title {
|
|
476
|
-
display: none;
|
|
338
|
+
display: none;
|
|
477
339
|
}
|
|
478
340
|
.reel-head__eyebrow {
|
|
479
341
|
margin-bottom: 0;
|
|
480
342
|
}
|
|
481
343
|
.reel-foot {
|
|
482
|
-
|
|
483
|
-
right: calc(42% + var(--frame-gap));
|
|
344
|
+
right: 42%;
|
|
484
345
|
padding: 0 1.9vw 2.6vh;
|
|
485
346
|
}
|
|
486
|
-
|
|
487
|
-
note on .reel-foot__audio). */
|
|
347
|
+
|
|
488
348
|
.reel-foot__audio {
|
|
489
349
|
display: flex;
|
|
490
350
|
flex: 1 1 auto;
|
|
491
351
|
margin: 0 1.4vw;
|
|
492
352
|
}
|
|
493
|
-
|
|
353
|
+
|
|
494
354
|
.stage--section .scene-area {
|
|
495
355
|
top: auto;
|
|
496
356
|
bottom: auto;
|
|
@@ -507,25 +367,18 @@ body {
|
|
|
507
367
|
.slide-toggle {
|
|
508
368
|
display: none;
|
|
509
369
|
}
|
|
510
|
-
|
|
511
|
-
with .stage--section so it outranks the base .slide-panel__scaler rule, which is defined LATER
|
|
512
|
-
in the file — at equal specificity that base would otherwise win here too. */
|
|
370
|
+
|
|
513
371
|
.stage--section .slide-panel__scaler {
|
|
514
372
|
padding: 0 60px;
|
|
515
373
|
--fs-body: 26px;
|
|
516
374
|
}
|
|
517
375
|
}
|
|
518
376
|
|
|
519
|
-
/* --- Slide scaler + markdown ----------------------------------------------- */
|
|
520
|
-
/* The scaler is authored at DESIGN_PANE px (see useSlideScale) and `zoom`-scaled to the live pane,
|
|
521
|
-
so every dimension below is DESIGN px and stays proportional to the target frame at any size.
|
|
522
|
-
One type scale drives the whole slide — every size steps off --fs-body, so retuning the slide for
|
|
523
|
-
capture legibility is a ONE-NUMBER change (adapted from the studio reveal-engine). */
|
|
524
377
|
.slide-panel__scaler {
|
|
525
378
|
box-sizing: border-box;
|
|
526
|
-
padding: 0 40px;
|
|
379
|
+
padding: 0 40px;
|
|
527
380
|
color: var(--slide-ink);
|
|
528
|
-
--fs-body: 31px;
|
|
381
|
+
--fs-body: 31px;
|
|
529
382
|
--fs-title: calc(var(--fs-body) * 1.73);
|
|
530
383
|
--fs-heading: calc(var(--fs-body) * 1.2);
|
|
531
384
|
--fs-code: calc(var(--fs-body) * 0.78);
|
|
@@ -538,25 +391,23 @@ body {
|
|
|
538
391
|
.slide-panel__scaler > :last-child {
|
|
539
392
|
margin-bottom: 0;
|
|
540
393
|
}
|
|
541
|
-
|
|
394
|
+
|
|
542
395
|
.slide-panel__scaler h2 {
|
|
543
396
|
margin: 0 0 20px;
|
|
544
397
|
font-size: var(--fs-title);
|
|
545
398
|
font-weight: 700;
|
|
546
399
|
line-height: 1.15;
|
|
547
400
|
letter-spacing: -0.01em;
|
|
548
|
-
color: var(--brand);
|
|
401
|
+
color: var(--brand);
|
|
549
402
|
}
|
|
550
|
-
|
|
551
|
-
logo's blue + yellow). Sentence-case and sized above the body (a real heading, not a quiet
|
|
552
|
-
eyebrow) — the warm/cool pair structures the slide at a glance while the body stays neutral. */
|
|
403
|
+
|
|
553
404
|
.slide-panel__scaler h3 {
|
|
554
405
|
margin: 30px 0 12px;
|
|
555
406
|
font-size: var(--fs-heading);
|
|
556
407
|
font-weight: 700;
|
|
557
408
|
line-height: 1.2;
|
|
558
409
|
letter-spacing: -0.01em;
|
|
559
|
-
color: var(--accent-2);
|
|
410
|
+
color: var(--accent-2);
|
|
560
411
|
}
|
|
561
412
|
.slide-panel__scaler p {
|
|
562
413
|
margin: 0 0 16px;
|
|
@@ -596,17 +447,6 @@ body {
|
|
|
596
447
|
color: rgba(var(--ink-rgb), 0.9);
|
|
597
448
|
}
|
|
598
449
|
|
|
599
|
-
/* --- Course catalog / landing page (CourseIndex) ---------------------------- */
|
|
600
|
-
/* The index is a scrollable page (unlike the fixed stage): the catalog of courses, each course a
|
|
601
|
-
card listing its ordered sections. Python-blue (--idx-accent) is the one accent. */
|
|
602
|
-
/* ── Site chrome (SiteHeader) ──────────────────────────────────────────────────────────────────
|
|
603
|
-
Ported from ui-graphl/styles.css, class for class. ui-graphl is buildless vanilla and this is a
|
|
604
|
-
React package, so one shared component is impossible — but one shared DESIGN is not, and it only
|
|
605
|
-
survives if neither copy drifts. Change this, change that. The single deliberate deviation is
|
|
606
|
-
--site-h: over there the 60px row height is a literal, here .idx has to subtract it.
|
|
607
|
-
|
|
608
|
-
Traditional chrome: brand on the left, section nav on the right, full-bleed rule underneath. The
|
|
609
|
-
inner row matches the catalog's own max-width so the brand lines up with the cards below it. */
|
|
610
450
|
.site {
|
|
611
451
|
border-bottom: 1px solid var(--line);
|
|
612
452
|
background: var(--header-bg);
|
|
@@ -614,10 +454,7 @@ body {
|
|
|
614
454
|
.site__inner {
|
|
615
455
|
max-width: 940px;
|
|
616
456
|
margin: 0 auto;
|
|
617
|
-
|
|
618
|
-
stylesheet deliberately does not (a global reset here would move the section view, which is
|
|
619
|
-
burned into video). Without it the bar's column is 940 + 64 of padding while .idx is 940
|
|
620
|
-
inclusive, and the brand sits 32px left of the cards it is supposed to line up with. */
|
|
457
|
+
|
|
621
458
|
box-sizing: border-box;
|
|
622
459
|
padding: 0 32px;
|
|
623
460
|
min-height: var(--site-h);
|
|
@@ -626,7 +463,7 @@ body {
|
|
|
626
463
|
gap: 20px;
|
|
627
464
|
}
|
|
628
465
|
.site__brand {
|
|
629
|
-
margin-right: auto;
|
|
466
|
+
margin-right: auto;
|
|
630
467
|
display: inline-flex;
|
|
631
468
|
align-items: center;
|
|
632
469
|
gap: 10px;
|
|
@@ -669,16 +506,13 @@ body {
|
|
|
669
506
|
outline: 2px solid var(--accent);
|
|
670
507
|
outline-offset: 2px;
|
|
671
508
|
}
|
|
672
|
-
|
|
673
|
-
the nav so that on a phone the nav can drop to its own row while these stay up on the brand row.
|
|
674
|
-
Empty until something is passed to SiteHeader's `actions` slot. */
|
|
509
|
+
|
|
675
510
|
.site__actions {
|
|
676
511
|
display: flex;
|
|
677
512
|
align-items: center;
|
|
678
513
|
gap: 6px;
|
|
679
514
|
}
|
|
680
|
-
|
|
681
|
-
36px so it is a comfortable target without towering over the 60px header row. */
|
|
515
|
+
|
|
682
516
|
.site__icon {
|
|
683
517
|
flex: none;
|
|
684
518
|
width: 36px;
|
|
@@ -705,9 +539,6 @@ body {
|
|
|
705
539
|
outline-offset: 2px;
|
|
706
540
|
}
|
|
707
541
|
|
|
708
|
-
/* Phone: the nav row scrolls horizontally rather than collapsing into a hamburger. There are three
|
|
709
|
-
sections today and there will be more, but they are the primary navigation and the whole point of
|
|
710
|
-
the current one is being visible; a menu would hide "you are here" behind a tap. */
|
|
711
542
|
@media (max-width: 560px) {
|
|
712
543
|
.site__inner {
|
|
713
544
|
padding: 8px 20px;
|
|
@@ -715,9 +546,9 @@ body {
|
|
|
715
546
|
gap: 0 12px;
|
|
716
547
|
}
|
|
717
548
|
.site__nav {
|
|
718
|
-
order: 3;
|
|
549
|
+
order: 3;
|
|
719
550
|
flex: 1 0 100%;
|
|
720
|
-
margin: 2px -20px 0;
|
|
551
|
+
margin: 2px -20px 0;
|
|
721
552
|
padding: 0 20px 4px;
|
|
722
553
|
overflow-x: auto;
|
|
723
554
|
scrollbar-width: none;
|
|
@@ -728,7 +559,7 @@ body {
|
|
|
728
559
|
.site__link {
|
|
729
560
|
display: flex;
|
|
730
561
|
align-items: center;
|
|
731
|
-
min-height: 40px;
|
|
562
|
+
min-height: 40px;
|
|
732
563
|
padding: 8px 10px;
|
|
733
564
|
font-size: 14px;
|
|
734
565
|
}
|
|
@@ -740,12 +571,10 @@ body {
|
|
|
740
571
|
|
|
741
572
|
.idx {
|
|
742
573
|
--idx-accent: var(--brand);
|
|
743
|
-
--idx-muted: var(--muted);
|
|
744
|
-
aliasing is byte-identical on dark and picks up the light block */
|
|
574
|
+
--idx-muted: var(--muted);
|
|
745
575
|
--idx-line: rgba(var(--ink-rgb), 0.09);
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
min-height: calc(100vh - var(--site-h) - 1px); /* the bar's row, plus its 1px rule */
|
|
576
|
+
|
|
577
|
+
min-height: calc(100vh - var(--site-h) - 1px);
|
|
749
578
|
max-width: 940px;
|
|
750
579
|
margin: 0 auto;
|
|
751
580
|
padding: 28px 32px 64px;
|
|
@@ -754,8 +583,7 @@ body {
|
|
|
754
583
|
.idx__head {
|
|
755
584
|
margin-bottom: 36px;
|
|
756
585
|
}
|
|
757
|
-
|
|
758
|
-
the site bar above (SiteHeader), which is the same bar graphl.in carries. */
|
|
586
|
+
|
|
759
587
|
.idx__subject {
|
|
760
588
|
margin: 0;
|
|
761
589
|
font-size: 40px;
|
|
@@ -771,7 +599,6 @@ body {
|
|
|
771
599
|
border-radius: 14px;
|
|
772
600
|
}
|
|
773
601
|
|
|
774
|
-
/* Courses grid — a single column of accordion cards (tight gap since most sit collapsed). */
|
|
775
602
|
.idx__grid {
|
|
776
603
|
list-style: none;
|
|
777
604
|
margin: 0;
|
|
@@ -780,8 +607,6 @@ body {
|
|
|
780
607
|
gap: 12px;
|
|
781
608
|
}
|
|
782
609
|
|
|
783
|
-
/* A course card = an accordion: a header button (number · title · count · caret) that toggles the
|
|
784
|
-
section list below it. */
|
|
785
610
|
.idx-card {
|
|
786
611
|
border: 1px solid var(--idx-line);
|
|
787
612
|
border-radius: 14px;
|
|
@@ -806,7 +631,7 @@ body {
|
|
|
806
631
|
.idx-card__head:hover {
|
|
807
632
|
background: var(--surface-lift-2);
|
|
808
633
|
}
|
|
809
|
-
|
|
634
|
+
|
|
810
635
|
.idx-card--open .idx-card__head {
|
|
811
636
|
border-bottom: 1px solid var(--idx-line);
|
|
812
637
|
}
|
|
@@ -846,7 +671,6 @@ body {
|
|
|
846
671
|
color: var(--idx-accent);
|
|
847
672
|
}
|
|
848
673
|
|
|
849
|
-
/* Section list inside a card — thin numbered rows, hover slides the chevron. */
|
|
850
674
|
.idx-sec {
|
|
851
675
|
list-style: none;
|
|
852
676
|
margin: 0;
|
|
@@ -906,10 +730,6 @@ body {
|
|
|
906
730
|
color: rgba(var(--ink-rgb), 0.9);
|
|
907
731
|
}
|
|
908
732
|
|
|
909
|
-
/* The bar's phone gutter is 20px, so the catalog column's has to be too — otherwise the brand and
|
|
910
|
-
the subject title below it sit on two different left edges. Matches ui-graphl's own phone rule.
|
|
911
|
-
It lives down HERE, not up with the rest of the .site phone rules, because .idx's own padding is
|
|
912
|
-
declared after them and would otherwise win at equal specificity. */
|
|
913
733
|
@media (max-width: 560px) {
|
|
914
734
|
.idx {
|
|
915
735
|
padding: 20px 20px 48px;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Scene } from '@graphlearning/flow';
|
|
1
2
|
export interface Section {
|
|
2
3
|
id: string;
|
|
3
4
|
title: string;
|
|
@@ -11,9 +12,9 @@ export interface Course {
|
|
|
11
12
|
title: string;
|
|
12
13
|
sections: Section[];
|
|
13
14
|
}
|
|
14
|
-
export type SceneLookup = (id: string) =>
|
|
15
|
-
export declare
|
|
16
|
-
export declare
|
|
15
|
+
export type SceneLookup = (id: string) => Scene | undefined;
|
|
16
|
+
export declare const slugOf: (course: Course, section: Section) => string;
|
|
17
|
+
export declare const allSections: (course: Course) => {
|
|
17
18
|
section: Section;
|
|
18
19
|
slug: string;
|
|
19
20
|
}[];
|
package/dist/useTheme.d.ts
CHANGED
|
@@ -1,16 +1,4 @@
|
|
|
1
1
|
import type { ThemeKey } from '@graphlearning/flow';
|
|
2
|
-
export declare function readStored(): ThemeKey | null;
|
|
3
|
-
/**
|
|
4
|
-
* The theme actually painted. CAPTURE PINS IT TO THE DECK, ignoring the stored choice and the reader
|
|
5
|
-
* entirely — and that is not a nicety. A recorder runs a fresh headless profile with empty storage,
|
|
6
|
-
* so without this pin what a capture painted would depend on the machine that rendered it. The
|
|
7
|
-
* declared prop is the only thing burned into a video.
|
|
8
|
-
*/
|
|
9
|
-
export declare const resolveTheme: (deck: ThemeKey, stored: ThemeKey | null, capture: boolean) => ThemeKey;
|
|
10
|
-
/**
|
|
11
|
-
* Owns the reader's choice and keeps <html data-theme> in step with it. Returns the theme to hand
|
|
12
|
-
* SceneView, plus a two-state flip for the toggle.
|
|
13
|
-
*/
|
|
14
2
|
export declare function useTheme(deck: ThemeKey, capture: boolean): {
|
|
15
3
|
theme: ThemeKey;
|
|
16
4
|
toggle: () => void;
|