@erclx/canon 4.109.0 → 4.110.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.
@@ -200,10 +200,20 @@ export const COMPONENTS: readonly Component[] = [
200
200
  HAND_DRAWN_FIGURE,
201
201
  ]
202
202
 
203
+ /**
204
+ * Where the course sidebar stops being a column and becomes an overlay.
205
+ *
206
+ * The media query below and both `matchMedia` calls in `@/teach/nav` read this
207
+ * one value. Three literals is what it was, and each had a test asserting its
208
+ * own copy, so a half-finished move passed green while the scripts switched at
209
+ * one width and the layout at another.
210
+ */
211
+ export const TEACH_SIDEBAR_BREAKPOINT = 1100
212
+
203
213
  const TEACH_CHROME: Component = {
204
214
  name: 'teach-chrome',
205
215
  note: [
206
- 'The masthead, breadcrumb, jump menu, theme toggle, progress track, and footer',
216
+ 'The masthead, breadcrumb, jump menu, theme toggle, course sidebar, and footer',
207
217
  'navigation shared by every teach page. Recovered from two gitignored',
208
218
  'course.css files that predate a regression that dropped this layer from the',
209
219
  "generator, and rewritten onto this module's tokens rather than the",
@@ -230,6 +240,7 @@ const TEACH_CHROME: Component = {
230
240
  '--t3',
231
241
  '--t4',
232
242
  '--t5',
243
+ '--t6',
233
244
  ],
234
245
  rules: `:root {
235
246
  --teach-sans: var(--type-body-family);
@@ -267,11 +278,16 @@ const TEACH_CHROME: Component = {
267
278
 
268
279
  html { scroll-padding-top: calc(var(--teach-mast-h) + 1rem); }
269
280
 
270
- /* No horizontal padding on the body, so the sticky bar reaches both window
271
- edges. The gutter moves onto \`main\`, which is the only thing that needed it. */
281
+ /* No padding at all on the body, so the sticky bar reaches both window edges
282
+ and the sidebar can travel the full height. The gutter moves onto \`main\` and
283
+ the bottom room onto \`.pane\`: a flex item's sticky containing block is the
284
+ flex container's content box, so padding here is space the sidebar could
285
+ never reach and it would ride up by exactly that much near the foot. */
272
286
  body {
273
287
  margin: 0;
274
- padding: 0 0 7rem;
288
+ padding: 0;
289
+ display: flex;
290
+ align-items: flex-start;
275
291
  background: var(--color-background);
276
292
  color: var(--color-text-body);
277
293
  font-family: var(--teach-sans);
@@ -287,6 +303,24 @@ body {
287
303
  main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); margin: 0 auto; padding: 2.25rem 1.5rem 0; }
288
304
  .wide-body { max-width: calc(var(--teach-measure) + 3rem); }
289
305
 
306
+ /* Everything but the sidebar. \`flow-root\` rather than a bare block, because a
307
+ bottom margin collapsing out of here makes the document taller than the flex
308
+ container and a sticky sidebar can only travel inside its containing block,
309
+ so it would ride up by exactly the escaped margin on a tall window. The
310
+ border box is what keeps \`min-height: 100vh\` plus the bottom room from
311
+ scrolling every short page by that padding. */
312
+ .pane {
313
+ flex: 1 1 auto;
314
+ min-width: 0;
315
+ display: flow-root;
316
+ box-sizing: border-box;
317
+ min-height: 100vh;
318
+ padding-bottom: 7rem;
319
+ border-left: 1px solid var(--color-border);
320
+ }
321
+
322
+ html.sb-shut .pane { border-left: 0; }
323
+
290
324
  .bar {
291
325
  position: sticky;
292
326
  top: 0;
@@ -296,10 +330,27 @@ main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); mar
296
330
  padding: 0 1.5rem;
297
331
  }
298
332
 
333
+ /* The reading bar rides the masthead's own bottom edge rather than arriving as
334
+ a second element. It reports position inside the lesson, which nothing
335
+ reported, where the retired track reported position in the course, which the
336
+ sidebar now reports. */
337
+ .bar::after {
338
+ content: "";
339
+ position: absolute;
340
+ left: 0;
341
+ bottom: -1px;
342
+ height: 2px;
343
+ width: var(--read, 0%);
344
+ background: var(--color-accent);
345
+ }
346
+
347
+ /* The height is the token rather than the sum of two paddings, so the sidebar's
348
+ own top row lands on the same seam the bar's bottom border draws. */
299
349
  .mast {
300
350
  max-width: var(--teach-chrome);
301
351
  margin: 0 auto;
302
- padding: 0.8rem 0 0.7rem;
352
+ min-height: var(--teach-mast-h);
353
+ padding: 0;
303
354
  display: flex;
304
355
  justify-content: space-between;
305
356
  align-items: center;
@@ -454,28 +505,225 @@ main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); mar
454
505
  :root:not([data-theme="light"]) .theme .moon { display: block; }
455
506
  }
456
507
 
457
- /* ---- Track: where you are in the course ---- */
508
+ /* ---- The course sidebar ---- */
458
509
 
459
- /* The track rides inside the bar, so it needs no sticky position, no seam
460
- overlap, and no stacking context of its own. */
510
+ /* A segmented progress strip used to sit here, one \`flex: 1\` segment per
511
+ lesson, and it stopped working at the scale a reader most needs it: fifty
512
+ lessons rendered as a row of dots. The sidebar reports course position
513
+ instead, at any length, and the bar reports position inside the lesson. */
461
514
 
462
- .track {
515
+ /* The toggle in the masthead, left of the breadcrumb. */
516
+ .sb-fold {
517
+ flex: none;
518
+ width: 1.75rem;
519
+ height: 1.75rem;
520
+ margin-right: 0.35rem;
521
+ font: inherit;
522
+ font-size: var(--t5);
523
+ line-height: 1;
524
+ color: var(--color-muted);
525
+ background: transparent;
526
+ border: 0;
527
+ border-radius: 6px;
528
+ cursor: pointer;
529
+ }
530
+
531
+ .sb-fold:hover { color: var(--color-text); background: var(--color-surface); }
532
+
533
+ .sb {
534
+ flex: 0 0 var(--sb-w, 16rem);
535
+ width: var(--sb-w, 16rem);
536
+ position: sticky;
537
+ top: 0;
538
+ align-self: flex-start;
539
+ height: 100vh;
463
540
  display: flex;
464
- gap: 0.25rem;
465
- max-width: var(--teach-chrome);
466
- margin: 0 auto;
467
- padding: 0 0 0.75rem;
541
+ flex-direction: column;
542
+ overflow: visible;
543
+ background: transparent;
544
+ font-family: var(--teach-sans);
545
+ transition: flex-basis 0.14s, width 0.14s;
468
546
  }
469
547
 
470
- .track i {
471
- flex: 1;
472
- height: 3px;
548
+ html.sb-shut .sb { flex-basis: 0; width: 0; overflow: hidden; }
549
+
550
+ .sb-top {
551
+ flex: none;
552
+ display: flex;
553
+ align-items: center;
554
+ height: var(--teach-mast-h);
555
+ padding: 0 0.95rem;
556
+ border-bottom: 1px solid var(--color-border);
557
+ }
558
+
559
+ .sb-ws { position: relative; }
560
+
561
+ .sb-ws > summary {
562
+ list-style: none;
563
+ cursor: pointer;
564
+ display: inline-flex;
565
+ align-items: center;
566
+ gap: 0.35rem;
567
+ padding: 0.2rem 0.4rem;
568
+ border-radius: 6px;
569
+ font-size: var(--t4);
570
+ font-weight: 650;
571
+ color: var(--color-text);
572
+ }
573
+
574
+ .sb-ws > summary::-webkit-details-marker { display: none; }
575
+ .sb-ws > summary:hover, .sb-ws[open] > summary { background: var(--color-surface); color: var(--color-text); }
576
+ .sb-ws .car { color: var(--color-muted); display: inline-flex; align-items: center; }
577
+
578
+ .sb-wl {
579
+ position: absolute;
580
+ top: calc(100% + 0.35rem);
581
+ left: 0;
582
+ z-index: 40;
583
+ min-width: 13rem;
584
+ background: var(--color-background);
585
+ border: 1px solid var(--color-border);
586
+ border-radius: 9px;
587
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14);
588
+ padding: 0.25rem;
589
+ }
590
+
591
+ :root[data-theme="dark"] .sb-wl { box-shadow: 0 8px 28px rgba(0, 0, 0, 0.5); }
592
+
593
+ /* The popover chrome comes from \`.sb-wl\`, so the list nested inside it drops
594
+ its own rather than drawing a second border inside the first. */
595
+ .sb-wl .jump-list {
596
+ position: static;
597
+ min-width: 0;
598
+ max-width: none;
599
+ border: 0;
600
+ box-shadow: none;
601
+ padding: 0;
602
+ background: transparent;
603
+ }
604
+
605
+ .sb-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-bottom: 1rem; }
606
+
607
+ .sb-meta {
608
+ padding: 1rem 1.25rem 0.55rem;
609
+ color: var(--color-muted);
610
+ font-size: var(--t6);
611
+ font-variant-numeric: tabular-nums;
612
+ }
613
+
614
+ .sb-filter { padding: 0 0.95rem 0.6rem; }
615
+
616
+ .sb-filter input {
617
+ width: 100%;
618
+ box-sizing: border-box;
619
+ font: inherit;
620
+ font-size: var(--t6);
621
+ color: var(--color-text);
622
+ background: transparent;
623
+ border: 1px solid var(--color-border);
624
+ border-radius: 7px;
625
+ padding: 0.3rem 0.55rem;
626
+ }
627
+
628
+ .sb-filter input:focus { outline: none; border-color: var(--color-accent); }
629
+
630
+ .sb-list { list-style: none; margin: 0; padding: 0 0.7rem; }
631
+ .sb-list li { margin: 0; }
632
+ .sb-list li.hide { display: none; }
633
+
634
+ .sb-l {
635
+ position: relative;
636
+ display: flex;
637
+ gap: 0.55rem;
638
+ align-items: baseline;
639
+ padding: 0.38rem 0.55rem;
640
+ border-radius: 6px;
641
+ text-decoration: none;
642
+ color: var(--color-text-body);
643
+ font-size: var(--t5);
644
+ line-height: 1.45;
645
+ }
646
+
647
+ .sb-l:hover { background: var(--color-surface); color: var(--color-text); }
648
+
649
+ .sb-n {
650
+ flex: none;
651
+ font-family: var(--teach-mono);
652
+ font-size: var(--t6);
653
+ color: var(--color-muted);
654
+ font-variant-numeric: tabular-nums;
655
+ }
656
+
657
+ .sb-on { color: var(--color-text); font-weight: 650; }
658
+
659
+ .sb-on::before {
660
+ content: "";
661
+ position: absolute;
662
+ left: -0.6rem;
663
+ top: 0.28rem;
664
+ bottom: 0.28rem;
665
+ width: 2px;
473
666
  border-radius: 2px;
667
+ background: var(--color-accent);
668
+ }
669
+
670
+ .sb-on .sb-n { color: var(--color-accent); }
671
+
672
+ .sb-empty { padding: 0.3rem 1.2rem; color: var(--color-muted); font-size: var(--t6); }
673
+
674
+ .sb-foot {
675
+ flex: none;
676
+ padding: 0.7rem 1.25rem;
677
+ border-top: 1px solid var(--color-border);
678
+ color: var(--color-muted);
679
+ font-size: var(--t6);
680
+ font-variant-numeric: tabular-nums;
681
+ }
682
+
683
+ /* The panel is resizable between a width that still fits a lesson title and one
684
+ that would start eating the reading measure. */
685
+ .sb-grip {
686
+ position: absolute;
687
+ top: 0;
688
+ bottom: 0;
689
+ right: -3px;
690
+ width: 7px;
691
+ z-index: 25;
692
+ padding: 0;
693
+ border: 0;
694
+ background: transparent;
695
+ cursor: col-resize;
696
+ touch-action: none;
697
+ }
698
+
699
+ .sb-grip::before {
700
+ content: "";
701
+ position: absolute;
702
+ left: 3px;
703
+ top: 50%;
704
+ transform: translateY(-50%);
705
+ width: 1px;
706
+ height: 2.2rem;
707
+ border-radius: 1px;
474
708
  background: var(--color-border);
709
+ opacity: 0;
710
+ transition: opacity 0.12s;
711
+ }
712
+
713
+ .sb-grip:hover::before, .sb-grip:focus-visible::before, html.sb-drag .sb-grip::before {
714
+ opacity: 1;
715
+ background: var(--color-accent);
475
716
  }
476
717
 
477
- .track i.done { background: var(--color-accent); }
478
- .track i.here { background: var(--color-accent); box-shadow: 0 0 0 2px var(--color-teach-accent-bg); }
718
+ .sb-grip:focus-visible { outline: none; }
719
+ html.sb-drag { cursor: col-resize; user-select: none; }
720
+ html.sb-shut .sb-grip { display: none; }
721
+
722
+ /* Both belong to the overlay and the script appends both on every page, so
723
+ each is hidden until the breakpoint hands it a job. Without this the close
724
+ control renders as an unstyled button stretched across the column, and the
725
+ scrim becomes a flex item of the body. */
726
+ .sb-close, .sb-scrim { display: none; }
479
727
 
480
728
  /* ---- Footer navigation, the only place it appears ---- */
481
729
 
@@ -556,8 +804,17 @@ main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); mar
556
804
 
557
805
  .jump-list a:hover { background: var(--color-surface); }
558
806
  .jump-list .n { font-family: var(--teach-mono); font-size: 0.75rem; color: var(--color-muted); }
559
- .jump-list .dot { width: 6px; height: 6px; border-radius: 99px; background: var(--color-border); }
560
- .jump-list .dot.done { background: var(--color-accent); }
807
+ /* The third column used to hold a status dot that read the same on every row,
808
+ so it reported nothing. It is a per-entry trailing slot now: the workspace
809
+ menu puts a lesson count there and the lesson menu leaves it empty. */
810
+ .jump-list .ct {
811
+ font-family: var(--teach-mono);
812
+ font-size: var(--t6);
813
+ color: var(--color-muted);
814
+ font-variant-numeric: tabular-nums;
815
+ text-align: right;
816
+ }
817
+
561
818
  .jump-list li.at a { background: var(--color-teach-accent-bg); }
562
819
  .jump-list li.at .n { color: var(--color-accent); }
563
820
  .jump-list li.soon a { color: var(--color-muted); }
@@ -606,6 +863,94 @@ main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); mar
606
863
  min-width: 0;
607
864
  max-width: none;
608
865
  }
866
+ }
867
+
868
+ /* ---- What gives way below 1100px ---- */
869
+
870
+ /* Hiding the sidebar outright leaves the toggle painted exactly where it cannot
871
+ act, and it deletes the filter and the per-lesson outline, which the
872
+ breadcrumb caret does not carry. The panel becomes an overlay instead, and it
873
+ states its own ground because \`.sb\` is transparent by design for a column
874
+ sitting on the page rather than floating over the lesson. */
875
+ @media (max-width: ${TEACH_SIDEBAR_BREAKPOINT}px) {
876
+ .pane { border-left: 0; }
877
+
878
+ .sb {
879
+ position: fixed;
880
+ top: 0;
881
+ left: 0;
882
+ bottom: 0;
883
+ z-index: 60;
884
+ width: min(21rem, 86vw);
885
+ height: 100dvh;
886
+ overflow-y: auto;
887
+ background: var(--color-background);
888
+ border-right: 1px solid var(--color-border);
889
+ box-shadow: 0 0 40px rgb(0 0 0 / 35%);
890
+ transform: translateX(-100%);
891
+ transition: transform 0.18s;
892
+ }
893
+
894
+ html:not(.sb-shut) .sb { transform: none; }
895
+
896
+ /* The wide rule collapses a shut panel to \`width: 0\` and outranks a bare
897
+ \`.sb\` in here, which produced a one-pixel panel whose contents spilled out
898
+ and intercepted every click on the toggle. The shut state restates the
899
+ width so the transform is what moves the panel off screen. */
900
+ html.sb-shut .sb { width: min(21rem, 86vw); flex-basis: auto; overflow-y: auto; }
901
+
902
+ /* A transform moves the panel out of sight and leaves every control inside it
903
+ in the tab order, so the first Tab from a shut page landed on the workspace
904
+ switcher inside an invisible panel. Visibility takes it out of the
905
+ sequence, delayed on the way out so the slide still shows. */
906
+ html.sb-shut .sb { visibility: hidden; transition: transform 0.18s, visibility 0s linear 0.18s; }
907
+ html:not(.sb-shut) .sb { visibility: visible; transition: transform 0.18s, visibility 0s; }
908
+
909
+ .sb-grip { display: none; }
910
+
911
+ /* The panel covers the masthead and the toggle that opened it, so it carries
912
+ its own way out rather than leaving the scrim as the only route on a phone.
913
+ It carries no border, at 1.75rem to match \`.sb-fold\` and \`.theme\`, which also clears
914
+ the 24 pixel pointer target floor. */
915
+ .sb-close {
916
+ display: inline-flex;
917
+ align-items: center;
918
+ justify-content: center;
919
+ position: absolute;
920
+ top: 0.55rem;
921
+ right: 0.6rem;
922
+ z-index: 61;
923
+ appearance: none;
924
+ width: 1.75rem;
925
+ height: 1.75rem;
926
+ font: inherit;
927
+ font-size: var(--t5);
928
+ line-height: 1;
929
+ color: var(--color-muted);
930
+ background: transparent;
931
+ border: 0;
932
+ border-radius: 6px;
933
+ cursor: pointer;
934
+ }
935
+
936
+ .sb-close:hover { color: var(--color-text); background: var(--color-surface); }
937
+
938
+ .sb-scrim {
939
+ display: block;
940
+ position: fixed;
941
+ inset: 0;
942
+ z-index: 55;
943
+ background: rgb(0 0 0 / 45%);
944
+ opacity: 0;
945
+ pointer-events: none;
946
+ transition: opacity 0.18s;
947
+ }
948
+
949
+ html:not(.sb-shut) .sb-scrim { opacity: 1; pointer-events: auto; }
950
+ }
951
+
952
+ @media (prefers-reduced-motion: reduce) {
953
+ .sb, .sb-scrim, .sb-grip::before { transition: none; }
609
954
  }`,
610
955
  }
611
956
 
@@ -1148,83 +1493,30 @@ ol.succ li { margin-bottom: 0.4rem; }
1148
1493
  const TEACH_OUTLINE: Component = {
1149
1494
  name: 'teach-outline',
1150
1495
  note: [
1151
- 'The in-lesson outline rail in the gutter the reading measure leaves over, and',
1152
- 'the workspace-switcher row inside the lesson jump menu.',
1496
+ 'The in-lesson outline, folded under the lesson being read in the course',
1497
+ 'sidebar. It used to be a fixed rail in the gutter the reading measure left',
1498
+ 'over, which reported the same thing twice once the sidebar carried the',
1499
+ 'course and disappeared entirely below 1420px.',
1153
1500
  ].join('\n '),
1154
- reads: [
1155
- '--color-border',
1156
- '--color-text-body',
1157
- '--color-muted',
1158
- '--color-accent',
1159
- '--teach-sans',
1160
- '--teach-measure',
1161
- '--teach-mast-h',
1162
- '--t5',
1163
- ],
1164
- rules: `/* ---- In-lesson outline, in the gutter the measure leaves over ---- */
1165
-
1166
- .outline {
1167
- position: fixed;
1168
- top: calc(var(--teach-mast-h) + 2.2rem);
1169
- /* Four pixels of padding with the position pulled back by the same amount,
1170
- so a focus ring at the rail's edge has somewhere to draw. \`overflow-y\`
1171
- clips horizontally too, which took a bite out of every ring in here. */
1172
- left: calc(50% + var(--teach-measure) / 2 + 2.5rem - 4px);
1173
- padding: 0 4px;
1174
- width: calc(13rem + 8px);
1175
- max-height: calc(100vh - var(--teach-mast-h) - 4rem);
1176
- overflow-y: auto;
1177
- font-size: var(--t5);
1178
- line-height: 1.45;
1179
- }
1180
-
1181
- /* The heading is a control rather than a label, because returning to the top
1182
- of a long lesson is the one thing the rail could not do. */
1183
- .outline .to-top {
1184
- display: flex;
1185
- align-items: center;
1186
- gap: 0.4rem;
1187
- width: 100%;
1188
- font-family: var(--teach-sans);
1189
- font-size: 0.6563rem;
1190
- font-weight: 700;
1191
- letter-spacing: 0.09em;
1192
- text-transform: uppercase;
1193
- color: var(--color-muted);
1194
- background: none;
1195
- border: 0;
1196
- border-bottom: 1px solid var(--color-border);
1197
- border-radius: 3px 3px 0 0;
1198
- margin: 0 0 0.6rem;
1199
- padding: 0.2rem 0 0.55rem;
1200
- cursor: pointer;
1201
- text-align: left;
1202
- }
1501
+ reads: ['--color-text', '--color-text-secondary', '--color-accent', '--t6'],
1502
+ rules: `/* ---- In-lesson outline, folded under the lesson being read ---- */
1203
1503
 
1204
- .outline .to-top:hover { color: var(--color-accent); border-bottom-color: var(--color-border); }
1504
+ /* The rail this replaces was \`position: fixed\` in the right-hand gutter, which
1505
+ only existed above 1420px and reported nothing at all below it. Folding the
1506
+ headings under the current lesson puts the course and the lesson in one
1507
+ column, so a reader tracks both in one place at any window width. */
1508
+ .sb-out { list-style: none; margin: 0.1rem 0 0.55rem; padding: 0 0 0 1.45rem; }
1205
1509
 
1206
- .outline .to-top::before {
1207
- content: "";
1208
- width: 11px;
1209
- height: 8px;
1210
- background:
1211
- linear-gradient(var(--color-muted) 0 0) 0 0 / 11px 1.5px no-repeat,
1212
- linear-gradient(var(--color-muted) 0 0) 0 3.2px / 8px 1.5px no-repeat,
1213
- linear-gradient(var(--color-muted) 0 0) 0 6.5px / 11px 1.5px no-repeat;
1214
- }
1215
-
1216
- .outline a {
1510
+ .sb-out a {
1217
1511
  display: block;
1218
- padding: 0.3rem 0 0.3rem 0.7rem;
1219
- border-left: 2px solid var(--color-border);
1220
- color: var(--color-muted);
1512
+ padding: 0.18rem 0.5rem;
1221
1513
  text-decoration: none;
1514
+ color: var(--color-text-secondary);
1515
+ font-size: var(--t6);
1222
1516
  }
1223
1517
 
1224
- .outline a:hover { color: var(--color-text-body); border-left-color: var(--color-border); }
1225
- .outline a.on { color: var(--color-accent); border-left-color: var(--color-accent); }
1226
-
1227
- @media (max-width: 1420px) { .outline { display: none; } }`,
1518
+ .sb-out a:hover { color: var(--color-text); }
1519
+ .sb-out a.on { color: var(--color-accent); }`,
1228
1520
  }
1229
1521
 
1230
1522
  const TEACH_REFERENCES: Component = {