@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/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; /* Zed-slate (matches graphl-studio/aws) */
2
+ --bg: #1a1d23;
15
3
  --fg: #eceef2;
16
- /* The foreground's RGB CHANNELS, so every `rgba(var(--ink-rgb), a)` in this file flips with the
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
- /* Surfaces lifted OFF --bg: the slide panel, and a catalog card's hover. Literals before 0.4.0. */
6
+
22
7
  --surface-lift: #2f333d;
23
8
  --surface-lift-2: #22262e;
24
- --slide-ink: #c8cdd6; /* the slide body's text colour */
25
- --foot-dim: #5b616f; /* the capture footer's section counter */
26
- --foot-dimmer: #4b515e; /* the catalog chevron at rest */
27
- /* A SCRIM, not ink: the page colour at 90%, sat behind the floating controls so they stay legible
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
- /* --- the three tokens a CONTENT REPO owns -------------------------------------------------
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); /* the current nav item */
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; /* the bar's row height (its 1px rule is extra) — .idx subtracts it from 100vh */
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; /* identical to ui-flow's light surface, so the scene pane and the page agree */
31
+ --bg: #f7f8fa;
80
32
  --fg: #1a1d23;
81
33
  --ink-rgb: 26, 29, 35;
82
- /* On dark the slide panel is LIFTED off the canvas; on light the same relationship means going
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
- /* Platform blue darkened until it carries text contrast on off-white; #5b8cff is tuned to glow
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%; /* portrait: full-screen reading surface (landscape overrides to a 42% column) */
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); /* lifted slide surface, off the #1a1d23 scene canvas */
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
- position: absolute;
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
- color: var(--idx-accent);
190
- border-color: var(--idx-accent);
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
- outline: 2px solid var(--idx-accent);
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
- width: 36px;
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
- /* The track is the hit target, so it gets vertical padding (a 4px line is not tappable) with the
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; /* the footer container is pointer-events:none */
284
- touch-action: none; /* a drag scrubs; it must not scroll the page */
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
- /* The thumb is an affordance, not decoration: it appears on hover/focus/scrub and otherwise the bar
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
- /* THE FOCUS INDICATOR IS THE THUMB, NOT A BOX ROUND THE BAR. An outline on the track draws a brand
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); /* Python-blue, matching the catalog accent */
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); /* Python-blue accent, matching the catalog brand */
392
- pointer-events: auto; /* the one clickable element in the header → back to catalog */
281
+ color: var(--brand);
282
+ pointer-events: auto;
393
283
  cursor: pointer;
394
284
  transition: color 0.2s;
395
285
  }
@@ -416,27 +306,18 @@ 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
- /* Reserve the bands so fitView packs the scene BETWEEN them (never under the title). Every section
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;
@@ -448,16 +329,13 @@ body {
448
329
  height: 100%;
449
330
  inset: auto;
450
331
  }
451
- /* Header/footer carry over but constrained to the SCENE column (left 58%; the slide is the right
452
- 42%) so they brand the diagram without straddling the slide. The section title is dropped here —
453
- it already headlines the slide — leaving the eyebrow on top and the GraphL + §n/N line below,
454
- sitting in the scene's fitView margins. Font sizes (vh) are shared with portrait. */
332
+
455
333
  .reel-head {
456
- right: 42%; /* right edge at the scene/slide boundary → spans only the scene column */
334
+ right: 42%;
457
335
  padding: 2.8vh 0 0 1.9vw;
458
336
  }
459
337
  .reel-head__title {
460
- display: none; /* the section title lives in the slide in landscape */
338
+ display: none;
461
339
  }
462
340
  .reel-head__eyebrow {
463
341
  margin-bottom: 0;
@@ -466,14 +344,13 @@ body {
466
344
  right: 42%;
467
345
  padding: 0 1.9vw 2.6vh;
468
346
  }
469
- /* The scene column is wide enough for the scrub bar between the two clusters (see the
470
- portrait-only note on .reel-foot__audio). */
347
+
471
348
  .reel-foot__audio {
472
349
  display: flex;
473
350
  flex: 1 1 auto;
474
351
  margin: 0 1.4vw;
475
352
  }
476
- /* Scene reserve is portrait-only; in landscape the scene-area is the full-height flex column. */
353
+
477
354
  .stage--section .scene-area {
478
355
  top: auto;
479
356
  bottom: auto;
@@ -490,25 +367,18 @@ body {
490
367
  .slide-toggle {
491
368
  display: none;
492
369
  }
493
- /* Restore the capture-tuned slide metrics (the base carries the larger portrait values). Scoped
494
- with .stage--section so it outranks the base .slide-panel__scaler rule, which is defined LATER
495
- in the file — at equal specificity that base would otherwise win here too. */
370
+
496
371
  .stage--section .slide-panel__scaler {
497
372
  padding: 0 60px;
498
373
  --fs-body: 26px;
499
374
  }
500
375
  }
501
376
 
502
- /* --- Slide scaler + markdown ----------------------------------------------- */
503
- /* The scaler is authored at DESIGN_PANE px (see useSlideScale) and `zoom`-scaled to the live pane,
504
- so every dimension below is DESIGN px and stays proportional to the target frame at any size.
505
- One type scale drives the whole slide — every size steps off --fs-body, so retuning the slide for
506
- capture legibility is a ONE-NUMBER change (adapted from the studio reveal-engine). */
507
377
  .slide-panel__scaler {
508
378
  box-sizing: border-box;
509
- padding: 0 40px; /* portrait: trimmed side padding to fill the phone width (landscape restores 60) */
379
+ padding: 0 40px;
510
380
  color: var(--slide-ink);
511
- --fs-body: 31px; /* portrait/reels: larger so the slide fills the phone (landscape restores 26) */
381
+ --fs-body: 31px;
512
382
  --fs-title: calc(var(--fs-body) * 1.73);
513
383
  --fs-heading: calc(var(--fs-body) * 1.2);
514
384
  --fs-code: calc(var(--fs-body) * 0.78);
@@ -521,25 +391,23 @@ body {
521
391
  .slide-panel__scaler > :last-child {
522
392
  margin-bottom: 0;
523
393
  }
524
- /* `##` → the slide title. */
394
+
525
395
  .slide-panel__scaler h2 {
526
396
  margin: 0 0 20px;
527
397
  font-size: var(--fs-title);
528
398
  font-weight: 700;
529
399
  line-height: 1.15;
530
400
  letter-spacing: -0.01em;
531
- color: var(--brand); /* Python-blue brand accent — same anchor as the reel eyebrow + catalog */
401
+ color: var(--brand);
532
402
  }
533
- /* `###` → a section heading in Python-yellow: a warm counter to the cool blue title (the Python
534
- logo's blue + yellow). Sentence-case and sized above the body (a real heading, not a quiet
535
- eyebrow) — the warm/cool pair structures the slide at a glance while the body stays neutral. */
403
+
536
404
  .slide-panel__scaler h3 {
537
405
  margin: 30px 0 12px;
538
406
  font-size: var(--fs-heading);
539
407
  font-weight: 700;
540
408
  line-height: 1.2;
541
409
  letter-spacing: -0.01em;
542
- color: var(--accent-2); /* Python-yellow */
410
+ color: var(--accent-2);
543
411
  }
544
412
  .slide-panel__scaler p {
545
413
  margin: 0 0 16px;
@@ -579,17 +447,6 @@ body {
579
447
  color: rgba(var(--ink-rgb), 0.9);
580
448
  }
581
449
 
582
- /* --- Course catalog / landing page (CourseIndex) ---------------------------- */
583
- /* The index is a scrollable page (unlike the fixed stage): the catalog of courses, each course a
584
- card listing its ordered sections. Python-blue (--idx-accent) is the one accent. */
585
- /* ── Site chrome (SiteHeader) ──────────────────────────────────────────────────────────────────
586
- Ported from ui-graphl/styles.css, class for class. ui-graphl is buildless vanilla and this is a
587
- React package, so one shared component is impossible — but one shared DESIGN is not, and it only
588
- survives if neither copy drifts. Change this, change that. The single deliberate deviation is
589
- --site-h: over there the 60px row height is a literal, here .idx has to subtract it.
590
-
591
- Traditional chrome: brand on the left, section nav on the right, full-bleed rule underneath. The
592
- inner row matches the catalog's own max-width so the brand lines up with the cards below it. */
593
450
  .site {
594
451
  border-bottom: 1px solid var(--line);
595
452
  background: var(--header-bg);
@@ -597,10 +454,7 @@ body {
597
454
  .site__inner {
598
455
  max-width: 940px;
599
456
  margin: 0 auto;
600
- /* Explicit, and NOT optional: ui-graphl has a global `* { box-sizing: border-box }` and this
601
- stylesheet deliberately does not (a global reset here would move the section view, which is
602
- burned into video). Without it the bar's column is 940 + 64 of padding while .idx is 940
603
- inclusive, and the brand sits 32px left of the cards it is supposed to line up with. */
457
+
604
458
  box-sizing: border-box;
605
459
  padding: 0 32px;
606
460
  min-height: var(--site-h);
@@ -609,7 +463,7 @@ body {
609
463
  gap: 20px;
610
464
  }
611
465
  .site__brand {
612
- margin-right: auto; /* holds the brand left; nav + actions collect on the right */
466
+ margin-right: auto;
613
467
  display: inline-flex;
614
468
  align-items: center;
615
469
  gap: 10px;
@@ -652,16 +506,13 @@ body {
652
506
  outline: 2px solid var(--accent);
653
507
  outline-offset: 2px;
654
508
  }
655
- /* The icon buttons — the theme toggle and the account control when they exist. Kept separate from
656
- the nav so that on a phone the nav can drop to its own row while these stay up on the brand row.
657
- Empty until something is passed to SiteHeader's `actions` slot. */
509
+
658
510
  .site__actions {
659
511
  display: flex;
660
512
  align-items: center;
661
513
  gap: 6px;
662
514
  }
663
- /* Square icon button, styled here so the slot's occupant does not have to carry its own CSS. Sized
664
- 36px so it is a comfortable target without towering over the 60px header row. */
515
+
665
516
  .site__icon {
666
517
  flex: none;
667
518
  width: 36px;
@@ -688,9 +539,6 @@ body {
688
539
  outline-offset: 2px;
689
540
  }
690
541
 
691
- /* Phone: the nav row scrolls horizontally rather than collapsing into a hamburger. There are three
692
- sections today and there will be more, but they are the primary navigation and the whole point of
693
- the current one is being visible; a menu would hide "you are here" behind a tap. */
694
542
  @media (max-width: 560px) {
695
543
  .site__inner {
696
544
  padding: 8px 20px;
@@ -698,9 +546,9 @@ body {
698
546
  gap: 0 12px;
699
547
  }
700
548
  .site__nav {
701
- order: 3; /* after brand and actions ⇒ its own row */
549
+ order: 3;
702
550
  flex: 1 0 100%;
703
- margin: 2px -20px 0; /* bleed to the screen edges so the row can scroll fully */
551
+ margin: 2px -20px 0;
704
552
  padding: 0 20px 4px;
705
553
  overflow-x: auto;
706
554
  scrollbar-width: none;
@@ -711,7 +559,7 @@ body {
711
559
  .site__link {
712
560
  display: flex;
713
561
  align-items: center;
714
- min-height: 40px; /* comfortable touch target */
562
+ min-height: 40px;
715
563
  padding: 8px 10px;
716
564
  font-size: 14px;
717
565
  }
@@ -723,12 +571,10 @@ body {
723
571
 
724
572
  .idx {
725
573
  --idx-accent: var(--brand);
726
- --idx-muted: var(--muted); /* was the literal #8b909c — the same value --muted already holds, so
727
- aliasing is byte-identical on dark and picks up the light block */
574
+ --idx-muted: var(--muted);
728
575
  --idx-line: rgba(var(--ink-rgb), 0.09);
729
- /* The bar is a sibling above this column, so the viewport it can fill is what the bar leaves.
730
- On a phone the bar wraps to two rows and this under-subtracts — harmless, it is a min. */
731
- 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);
732
578
  max-width: 940px;
733
579
  margin: 0 auto;
734
580
  padding: 28px 32px 64px;
@@ -737,8 +583,7 @@ body {
737
583
  .idx__head {
738
584
  margin-bottom: 36px;
739
585
  }
740
- /* Catalog header: just the subject title now — the GraphL wordmark it used to sit under is in
741
- the site bar above (SiteHeader), which is the same bar graphl.in carries. */
586
+
742
587
  .idx__subject {
743
588
  margin: 0;
744
589
  font-size: 40px;
@@ -754,7 +599,6 @@ body {
754
599
  border-radius: 14px;
755
600
  }
756
601
 
757
- /* Courses grid — a single column of accordion cards (tight gap since most sit collapsed). */
758
602
  .idx__grid {
759
603
  list-style: none;
760
604
  margin: 0;
@@ -763,8 +607,6 @@ body {
763
607
  gap: 12px;
764
608
  }
765
609
 
766
- /* A course card = an accordion: a header button (number · title · count · caret) that toggles the
767
- section list below it. */
768
610
  .idx-card {
769
611
  border: 1px solid var(--idx-line);
770
612
  border-radius: 14px;
@@ -789,7 +631,7 @@ body {
789
631
  .idx-card__head:hover {
790
632
  background: var(--surface-lift-2);
791
633
  }
792
- /* Divider only when open — a collapsed row is a clean single bar. */
634
+
793
635
  .idx-card--open .idx-card__head {
794
636
  border-bottom: 1px solid var(--idx-line);
795
637
  }
@@ -829,7 +671,6 @@ body {
829
671
  color: var(--idx-accent);
830
672
  }
831
673
 
832
- /* Section list inside a card — thin numbered rows, hover slides the chevron. */
833
674
  .idx-sec {
834
675
  list-style: none;
835
676
  margin: 0;
@@ -889,10 +730,6 @@ body {
889
730
  color: rgba(var(--ink-rgb), 0.9);
890
731
  }
891
732
 
892
- /* The bar's phone gutter is 20px, so the catalog column's has to be too — otherwise the brand and
893
- the subject title below it sit on two different left edges. Matches ui-graphl's own phone rule.
894
- It lives down HERE, not up with the rest of the .site phone rules, because .idx's own padding is
895
- declared after them and would otherwise win at equal specificity. */
896
733
  @media (max-width: 560px) {
897
734
  .idx {
898
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) => import('@graphlearning/flow').Scene | undefined;
15
- export declare function slugOf(course: Course, section: Section): string;
16
- export declare function allSections(course: Course): {
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
  }[];
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graphlearning/shell",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "description": "The GraphL concept-app shell — hash router, section view (scene left / slide right), slide panel, course catalog and narration channel, plus the capture/record/publish toolchain that drives them. A content repo supplies courses + scenes.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {