@graphlearning/shell 0.2.0 → 0.4.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/dist/styles.css CHANGED
@@ -13,6 +13,21 @@
13
13
  :root {
14
14
  --bg: #1a1d23; /* Zed-slate (matches graphl-studio/aws) */
15
15
  --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. */
20
+ --ink-rgb: 255, 255, 255;
21
+ /* Surfaces lifted OFF --bg: the slide panel, and a catalog card's hover. Literals before 0.4.0. */
22
+ --surface-lift: #2f333d;
23
+ --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. */
30
+ --scrim: rgba(26, 29, 35, 0.9);
16
31
  /* --- the three tokens a CONTENT REPO owns -------------------------------------------------
17
32
  These are NEUTRAL DEFAULTS. Each repo overrides them in its own theme.css, imported AFTER
18
33
  this stylesheet so the cascade lets the repo win. They are the entire per-repo design
@@ -26,6 +41,65 @@
26
41
  --brand-hover: #a8adb8;
27
42
  --accent-2: #c8cdd6;
28
43
  --font-sans: 'IBM Plex Sans', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
44
+
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
+ --text: var(--fg);
54
+ --muted: #8b909c;
55
+ --accent: #5b8cff;
56
+ --accent-soft: rgba(91, 140, 255, 0.14); /* the current nav item */
57
+ --line: rgba(var(--ink-rgb), 0.09);
58
+ --header-bg: rgba(var(--ink-rgb), 0.014);
59
+ --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. */
78
+ :root[data-theme='light'] {
79
+ --bg: #f7f8fa; /* identical to ui-flow's light surface, so the scene pane and the page agree */
80
+ --fg: #1a1d23;
81
+ --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. */
84
+ --surface-lift: #ffffff;
85
+ --surface-lift-2: #eef1f5;
86
+ --slide-ink: #333a45;
87
+ --foot-dim: #7a8493;
88
+ --foot-dimmer: #98a2b1;
89
+ --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. */
92
+ --accent: #2f6fe0;
93
+ --accent-soft: rgba(47, 111, 224, 0.12);
94
+ --scrim: rgba(247, 248, 250, 0.9);
95
+ }
96
+
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
+ .theme-swap * {
102
+ transition: none !important;
29
103
  }
30
104
 
31
105
  html,
@@ -84,8 +158,8 @@ body {
84
158
  justify-content: center;
85
159
  overflow: auto;
86
160
  box-sizing: border-box;
87
- background: #2f333d; /* lifted slide surface, off the #1a1d23 scene canvas */
88
- border-left: 1px solid rgba(255, 255, 255, 0.09);
161
+ background: var(--surface-lift); /* lifted slide surface, off the #1a1d23 scene canvas */
162
+ border-left: 1px solid rgba(var(--ink-rgb), 0.09);
89
163
  transform: translateX(100%);
90
164
  transition: transform 0.32s cubic-bezier(0.4, 0, 0.2, 1);
91
165
  z-index: 10;
@@ -101,9 +175,9 @@ body {
101
175
  width: 36px;
102
176
  height: 36px;
103
177
  border-radius: 10px;
104
- border: 1px solid rgba(255, 255, 255, 0.12);
105
- background: rgba(26, 29, 35, 0.9);
106
- color: rgba(255, 255, 255, 0.82);
178
+ border: 1px solid rgba(var(--ink-rgb), 0.12);
179
+ background: var(--scrim);
180
+ color: rgba(var(--ink-rgb), 0.82);
107
181
  font-size: 19px;
108
182
  line-height: 1;
109
183
  cursor: pointer;
@@ -145,9 +219,9 @@ body {
145
219
  width: 36px;
146
220
  height: 36px;
147
221
  border-radius: 10px;
148
- border: 1px solid rgba(255, 255, 255, 0.12);
149
- background: rgba(26, 29, 35, 0.9);
150
- color: rgba(255, 255, 255, 0.82);
222
+ border: 1px solid rgba(var(--ink-rgb), 0.12);
223
+ background: var(--scrim);
224
+ color: rgba(var(--ink-rgb), 0.82);
151
225
  cursor: pointer;
152
226
  pointer-events: auto; /* the footer container is pointer-events:none; re-enable the buttons */
153
227
  transition: color 0.15s, border-color 0.15s;
@@ -186,7 +260,7 @@ body {
186
260
  padding: 4px 6px;
187
261
  border: none;
188
262
  background: transparent;
189
- color: rgba(255, 255, 255, 0.82);
263
+ color: rgba(var(--ink-rgb), 0.82);
190
264
  font-family: var(--font-sans);
191
265
  font-size: 17px;
192
266
  font-weight: 700;
@@ -258,11 +332,11 @@ body {
258
332
  font-size: 1.55vh;
259
333
  font-weight: 700;
260
334
  letter-spacing: -0.01em;
261
- color: rgba(255, 255, 255, 0.9);
335
+ color: rgba(var(--ink-rgb), 0.9);
262
336
  }
263
337
  .reel-foot__count {
264
338
  font-size: 1.4vh;
265
- color: rgba(255, 255, 255, 0.5);
339
+ color: rgba(var(--ink-rgb), 0.5);
266
340
  font-variant-numeric: tabular-nums;
267
341
  letter-spacing: 0.04em;
268
342
  }
@@ -338,7 +412,7 @@ body {
338
412
  .slide-panel__scaler {
339
413
  box-sizing: border-box;
340
414
  padding: 0 40px; /* portrait: trimmed side padding to fill the phone width (landscape restores 60) */
341
- color: #c8cdd6;
415
+ color: var(--slide-ink);
342
416
  --fs-body: 31px; /* portrait/reels: larger so the slide fills the phone (landscape restores 26) */
343
417
  --fs-title: calc(var(--fs-body) * 1.73);
344
418
  --fs-heading: calc(var(--fs-body) * 1.2);
@@ -391,56 +465,185 @@ body {
391
465
  content: '—';
392
466
  position: absolute;
393
467
  left: 0;
394
- color: rgba(255, 255, 255, 0.28);
468
+ color: rgba(var(--ink-rgb), 0.28);
395
469
  }
396
470
  .slide-panel__scaler strong {
397
- color: rgba(255, 255, 255, 0.95);
471
+ color: rgba(var(--ink-rgb), 0.95);
398
472
  font-weight: 700;
399
473
  }
400
474
  .slide-panel__scaler em {
401
475
  font-style: italic;
402
- color: rgba(255, 255, 255, 0.85);
476
+ color: rgba(var(--ink-rgb), 0.85);
403
477
  }
404
478
  .slide-panel__scaler code {
405
479
  font-family: 'IBM Plex Mono', ui-monospace, monospace;
406
480
  font-size: var(--fs-code);
407
481
  padding: 2px 7px;
408
482
  border-radius: 5px;
409
- background: rgba(255, 255, 255, 0.08);
410
- color: rgba(255, 255, 255, 0.9);
483
+ background: rgba(var(--ink-rgb), 0.08);
484
+ color: rgba(var(--ink-rgb), 0.9);
411
485
  }
412
486
 
413
487
  /* --- Course catalog / landing page (CourseIndex) ---------------------------- */
414
488
  /* The index is a scrollable page (unlike the fixed stage): the catalog of courses, each course a
415
489
  card listing its ordered sections. Python-blue (--idx-accent) is the one accent. */
416
- .idx {
417
- --idx-accent: var(--brand);
418
- --idx-muted: #8b909c;
419
- --idx-line: rgba(255, 255, 255, 0.09);
420
- min-height: 100vh;
490
+ /* ── Site chrome (SiteHeader) ──────────────────────────────────────────────────────────────────
491
+ Ported from ui-graphl/styles.css, class for class. ui-graphl is buildless vanilla and this is a
492
+ React package, so one shared component is impossible — but one shared DESIGN is not, and it only
493
+ survives if neither copy drifts. Change this, change that. The single deliberate deviation is
494
+ --site-h: over there the 60px row height is a literal, here .idx has to subtract it.
495
+
496
+ Traditional chrome: brand on the left, section nav on the right, full-bleed rule underneath. The
497
+ inner row matches the catalog's own max-width so the brand lines up with the cards below it. */
498
+ .site {
499
+ border-bottom: 1px solid var(--line);
500
+ background: var(--header-bg);
501
+ }
502
+ .site__inner {
421
503
  max-width: 940px;
422
504
  margin: 0 auto;
423
- padding: 40px 32px 64px;
505
+ /* Explicit, and NOT optional: ui-graphl has a global `* { box-sizing: border-box }` and this
506
+ stylesheet deliberately does not (a global reset here would move the section view, which is
507
+ burned into video). Without it the bar's column is 940 + 64 of padding while .idx is 940
508
+ inclusive, and the brand sits 32px left of the cards it is supposed to line up with. */
424
509
  box-sizing: border-box;
510
+ padding: 0 32px;
511
+ min-height: var(--site-h);
512
+ display: flex;
513
+ align-items: center;
514
+ gap: 20px;
425
515
  }
426
- .idx__head {
427
- margin-bottom: 36px;
516
+ .site__brand {
517
+ margin-right: auto; /* holds the brand left; nav + actions collect on the right */
518
+ display: inline-flex;
519
+ align-items: center;
520
+ gap: 10px;
521
+ text-decoration: none;
522
+ color: inherit;
428
523
  }
429
- /* Catalog header: GraphL platform eyebrow over the AWS subject title (no marketing subtitle). */
430
- .idx__brand {
431
- display: inline-block;
432
- font-size: 13px;
524
+ .site__logo {
525
+ display: block;
526
+ border-radius: 6px;
527
+ }
528
+ .site__name {
529
+ font-size: 17px;
433
530
  font-weight: 700;
434
- letter-spacing: 0.16em;
435
- text-transform: uppercase;
436
- color: var(--idx-accent);
437
- margin-bottom: 10px;
531
+ letter-spacing: -0.01em;
532
+ }
533
+ .site__nav {
534
+ display: flex;
535
+ gap: 4px;
536
+ }
537
+ .site__link {
538
+ flex: none;
539
+ white-space: nowrap;
540
+ padding: 8px 12px;
541
+ border-radius: 8px;
542
+ font-size: 15px;
543
+ font-weight: 600;
544
+ color: var(--muted);
438
545
  text-decoration: none;
439
- transition: opacity 0.15s ease;
546
+ transition: color 0.15s ease, background 0.15s ease;
440
547
  }
441
- .idx__brand:hover {
442
- opacity: 0.72;
548
+ .site__link:hover {
549
+ color: var(--text);
550
+ background: var(--hover);
443
551
  }
552
+ .site__link[aria-current='page'] {
553
+ color: var(--text);
554
+ background: var(--accent-soft);
555
+ }
556
+ .site__link:focus-visible {
557
+ outline: 2px solid var(--accent);
558
+ outline-offset: 2px;
559
+ }
560
+ /* The icon buttons — the theme toggle and the account control when they exist. Kept separate from
561
+ the nav so that on a phone the nav can drop to its own row while these stay up on the brand row.
562
+ Empty until something is passed to SiteHeader's `actions` slot. */
563
+ .site__actions {
564
+ display: flex;
565
+ align-items: center;
566
+ gap: 6px;
567
+ }
568
+ /* Square icon button, styled here so the slot's occupant does not have to carry its own CSS. Sized
569
+ 36px so it is a comfortable target without towering over the 60px header row. */
570
+ .site__icon {
571
+ flex: none;
572
+ width: 36px;
573
+ height: 36px;
574
+ display: inline-flex;
575
+ align-items: center;
576
+ justify-content: center;
577
+ font-size: 16px;
578
+ line-height: 1;
579
+ font-family: inherit;
580
+ color: var(--muted);
581
+ background: transparent;
582
+ border: 1px solid transparent;
583
+ border-radius: 9px;
584
+ cursor: pointer;
585
+ transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
586
+ }
587
+ .site__icon:hover {
588
+ color: var(--text);
589
+ background: var(--hover);
590
+ }
591
+ .site__icon:focus-visible {
592
+ outline: 2px solid var(--accent);
593
+ outline-offset: 2px;
594
+ }
595
+
596
+ /* Phone: the nav row scrolls horizontally rather than collapsing into a hamburger. There are three
597
+ sections today and there will be more, but they are the primary navigation and the whole point of
598
+ the current one is being visible; a menu would hide "you are here" behind a tap. */
599
+ @media (max-width: 560px) {
600
+ .site__inner {
601
+ padding: 8px 20px;
602
+ flex-wrap: wrap;
603
+ gap: 0 12px;
604
+ }
605
+ .site__nav {
606
+ order: 3; /* after brand and actions ⇒ its own row */
607
+ flex: 1 0 100%;
608
+ margin: 2px -20px 0; /* bleed to the screen edges so the row can scroll fully */
609
+ padding: 0 20px 4px;
610
+ overflow-x: auto;
611
+ scrollbar-width: none;
612
+ }
613
+ .site__nav::-webkit-scrollbar {
614
+ display: none;
615
+ }
616
+ .site__link {
617
+ display: flex;
618
+ align-items: center;
619
+ min-height: 40px; /* comfortable touch target */
620
+ padding: 8px 10px;
621
+ font-size: 14px;
622
+ }
623
+ .site__icon {
624
+ width: 40px;
625
+ height: 40px;
626
+ }
627
+ }
628
+
629
+ .idx {
630
+ --idx-accent: var(--brand);
631
+ --idx-muted: var(--muted); /* was the literal #8b909c — the same value --muted already holds, so
632
+ aliasing is byte-identical on dark and picks up the light block */
633
+ --idx-line: rgba(var(--ink-rgb), 0.09);
634
+ /* The bar is a sibling above this column, so the viewport it can fill is what the bar leaves.
635
+ On a phone the bar wraps to two rows and this under-subtracts — harmless, it is a min. */
636
+ min-height: calc(100vh - var(--site-h) - 1px); /* the bar's row, plus its 1px rule */
637
+ max-width: 940px;
638
+ margin: 0 auto;
639
+ padding: 28px 32px 64px;
640
+ box-sizing: border-box;
641
+ }
642
+ .idx__head {
643
+ margin-bottom: 36px;
644
+ }
645
+ /* Catalog header: just the subject title now — the GraphL wordmark it used to sit under is in
646
+ the site bar above (SiteHeader), which is the same bar graphl.in carries. */
444
647
  .idx__subject {
445
648
  margin: 0;
446
649
  font-size: 40px;
@@ -470,7 +673,7 @@ body {
470
673
  .idx-card {
471
674
  border: 1px solid var(--idx-line);
472
675
  border-radius: 14px;
473
- background: rgba(255, 255, 255, 0.018);
676
+ background: rgba(var(--ink-rgb), 0.018);
474
677
  overflow: hidden;
475
678
  }
476
679
  .idx-card__head {
@@ -489,7 +692,7 @@ body {
489
692
  transition: background 0.15s ease;
490
693
  }
491
694
  .idx-card__head:hover {
492
- background: #22262e;
695
+ background: var(--surface-lift-2);
493
696
  }
494
697
  /* Divider only when open — a collapsed row is a clean single bar. */
495
698
  .idx-card--open .idx-card__head {
@@ -556,7 +759,7 @@ body {
556
759
  font-size: 13px;
557
760
  font-weight: 600;
558
761
  font-variant-numeric: tabular-nums;
559
- color: #5b616f;
762
+ color: var(--foot-dim);
560
763
  }
561
764
  .idx-sec__row a:hover .idx-sec__num {
562
765
  color: var(--idx-accent);
@@ -569,7 +772,7 @@ body {
569
772
  .idx-sec__chev {
570
773
  flex: none;
571
774
  font-size: 19px;
572
- color: #4b515e;
775
+ color: var(--foot-dimmer);
573
776
  transition: color 0.12s ease, transform 0.12s ease;
574
777
  }
575
778
  .idx-sec__row a:hover .idx-sec__chev {
@@ -587,7 +790,16 @@ body {
587
790
  font-size: 12px;
588
791
  padding: 2px 7px;
589
792
  border-radius: 5px;
590
- background: rgba(255, 255, 255, 0.08);
591
- color: rgba(255, 255, 255, 0.9);
793
+ background: rgba(var(--ink-rgb), 0.08);
794
+ color: rgba(var(--ink-rgb), 0.9);
592
795
  }
593
796
 
797
+ /* The bar's phone gutter is 20px, so the catalog column's has to be too — otherwise the brand and
798
+ the subject title below it sit on two different left edges. Matches ui-graphl's own phone rule.
799
+ It lives down HERE, not up with the rest of the .site phone rules, because .idx's own padding is
800
+ declared after them and would otherwise win at equal specificity. */
801
+ @media (max-width: 560px) {
802
+ .idx {
803
+ padding: 20px 20px 48px;
804
+ }
805
+ }
@@ -0,0 +1,17 @@
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
+ export declare function useTheme(deck: ThemeKey, capture: boolean): {
15
+ theme: ThemeKey;
16
+ toggle: () => void;
17
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@graphlearning/shell",
3
- "version": "0.2.0",
4
- "description": "The GraphL concept-app shell \u2014 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.",
3
+ "version": "0.4.0",
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": {
7
7
  "type": "git",
@@ -28,17 +28,17 @@
28
28
  "watch": "vite build --watch"
29
29
  },
30
30
  "peerDependencies": {
31
- "@graphlearning/flow": ">=0.5.0 <1.0.0",
31
+ "@graphlearning/flow": ">=0.8.0 <1.0.0",
32
+ "esbuild": ">=0.20",
32
33
  "lucide-react": "^1.28.0",
34
+ "puppeteer": ">=24",
33
35
  "react": "^19.2.0",
34
36
  "react-dom": "^19.2.0",
35
37
  "react-markdown": "^10.1.0",
36
- "remark-gfm": "^4.0.1",
37
- "puppeteer": ">=24",
38
- "esbuild": ">=0.20"
38
+ "remark-gfm": "^4.0.1"
39
39
  },
40
40
  "devDependencies": {
41
- "@graphlearning/flow": "^0.5.0",
41
+ "@graphlearning/flow": "^0.8.0",
42
42
  "@types/react": "^19.2.0",
43
43
  "@types/react-dom": "^19.2.0",
44
44
  "@vitejs/plugin-react": "^4.3.4",