@erclx/canon 4.109.0 → 4.111.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/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/{canon-feedback-file → canon-feedback}/REQUIREMENT.md +7 -6
- package/claude/skills/{canon-feedback-file → canon-feedback}/SKILL.md +9 -19
- package/claude/skills/canon-feedback-triage/REQUIREMENT.md +1 -1
- package/claude/skills/canon-feedback-triage/SKILL.md +1 -1
- package/claude/skills/canon-operator/REQUIREMENT.md +2 -2
- package/claude/skills/canon-operator/SKILL.md +3 -4
- package/claude/skills/draft-wiki/REQUIREMENT.md +42 -0
- package/claude/skills/draft-wiki/SKILL.md +83 -0
- package/claude/skills/git-issue/REQUIREMENT.md +1 -1
- package/claude/skills/git-issue/SKILL.md +1 -1
- package/claude/skills/target-check/references/domains.md +2 -2
- package/docs/agents/install-and-sync.md +4 -13
- package/docs/agents/teach.md +4 -2
- package/docs/target-projects.md +0 -2
- package/docs/workflow/ai-workflow.md +8 -11
- package/package.json +1 -1
- package/scripts/core/capture-home.sh +59 -0
- package/src/claude/cases/authoring.ts +5 -0
- package/src/claude/cases/setup.ts +3 -24
- package/src/commands/sync.ts +0 -16
- package/src/design/components.ts +459 -147
- package/src/shipped/references.ts +1 -2
- package/src/sync/reverse.ts +3 -74
- package/src/teach/nav.ts +410 -58
- package/claude/skills/migration-claude-md/REQUIREMENT.md +0 -40
- package/claude/skills/migration-claude-md/SKILL.md +0 -79
- package/claude/skills/migration-context/REQUIREMENT.md +0 -36
- package/claude/skills/migration-context/SKILL.md +0 -97
- package/claude/skills/migration-standards-drop/REQUIREMENT.md +0 -55
- package/claude/skills/migration-standards-drop/SKILL.md +0 -113
- package/claude/skills/migration-superseded/REQUIREMENT.md +0 -44
- package/claude/skills/migration-superseded/SKILL.md +0 -115
package/src/design/components.ts
CHANGED
|
@@ -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,
|
|
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",
|
|
@@ -213,6 +223,7 @@ const TEACH_CHROME: Component = {
|
|
|
213
223
|
reads: [
|
|
214
224
|
'--color-background',
|
|
215
225
|
'--color-surface',
|
|
226
|
+
'--color-chrome',
|
|
216
227
|
'--color-border',
|
|
217
228
|
'--color-text',
|
|
218
229
|
'--color-text-body',
|
|
@@ -222,7 +233,6 @@ const TEACH_CHROME: Component = {
|
|
|
222
233
|
'--teach-sans',
|
|
223
234
|
'--teach-mono',
|
|
224
235
|
'--teach-measure',
|
|
225
|
-
'--teach-chrome',
|
|
226
236
|
'--teach-shadow',
|
|
227
237
|
'--color-teach-accent-bg',
|
|
228
238
|
'--type-body-family',
|
|
@@ -230,14 +240,14 @@ 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);
|
|
236
247
|
--teach-hand: 'Virgil', 'Excalifont', cursive;
|
|
237
248
|
--teach-mono: var(--type-code-family);
|
|
238
249
|
--teach-measure: 52rem;
|
|
239
|
-
--teach-
|
|
240
|
-
--teach-mast-h: 4.4rem;
|
|
250
|
+
--teach-mast-h: 3.5rem;
|
|
241
251
|
--teach-shadow: 0 1px 2px rgba(20, 20, 20, 0.04);
|
|
242
252
|
--color-teach-accent-bg: color-mix(in srgb, var(--color-accent) 14%, var(--color-background));
|
|
243
253
|
|
|
@@ -267,11 +277,16 @@ const TEACH_CHROME: Component = {
|
|
|
267
277
|
|
|
268
278
|
html { scroll-padding-top: calc(var(--teach-mast-h) + 1rem); }
|
|
269
279
|
|
|
270
|
-
/* No
|
|
271
|
-
|
|
280
|
+
/* No padding at all on the body, so the sticky bar reaches both window edges
|
|
281
|
+
and the sidebar can travel the full height. The gutter moves onto \`main\` and
|
|
282
|
+
the bottom room onto \`.pane\`: a flex item's sticky containing block is the
|
|
283
|
+
flex container's content box, so padding here is space the sidebar could
|
|
284
|
+
never reach and it would ride up by exactly that much near the foot. */
|
|
272
285
|
body {
|
|
273
286
|
margin: 0;
|
|
274
|
-
padding: 0
|
|
287
|
+
padding: 0;
|
|
288
|
+
display: flex;
|
|
289
|
+
align-items: flex-start;
|
|
275
290
|
background: var(--color-background);
|
|
276
291
|
color: var(--color-text-body);
|
|
277
292
|
font-family: var(--teach-sans);
|
|
@@ -287,6 +302,24 @@ body {
|
|
|
287
302
|
main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); margin: 0 auto; padding: 2.25rem 1.5rem 0; }
|
|
288
303
|
.wide-body { max-width: calc(var(--teach-measure) + 3rem); }
|
|
289
304
|
|
|
305
|
+
/* Everything but the sidebar. \`flow-root\` rather than a bare block, because a
|
|
306
|
+
bottom margin collapsing out of here makes the document taller than the flex
|
|
307
|
+
container and a sticky sidebar can only travel inside its containing block,
|
|
308
|
+
so it would ride up by exactly the escaped margin on a tall window. The
|
|
309
|
+
border box is what keeps \`min-height: 100vh\` plus the bottom room from
|
|
310
|
+
scrolling every short page by that padding. */
|
|
311
|
+
.pane {
|
|
312
|
+
flex: 1 1 auto;
|
|
313
|
+
min-width: 0;
|
|
314
|
+
display: flow-root;
|
|
315
|
+
box-sizing: border-box;
|
|
316
|
+
min-height: 100vh;
|
|
317
|
+
padding-bottom: 7rem;
|
|
318
|
+
border-left: 1px solid var(--color-border);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
html.sb-shut .pane { border-left: 0; }
|
|
322
|
+
|
|
290
323
|
.bar {
|
|
291
324
|
position: sticky;
|
|
292
325
|
top: 0;
|
|
@@ -294,37 +327,44 @@ main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); mar
|
|
|
294
327
|
background: var(--color-background);
|
|
295
328
|
border-bottom: 1px solid var(--color-border);
|
|
296
329
|
padding: 0 1.5rem;
|
|
330
|
+
cursor: default;
|
|
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. The row
|
|
349
|
+
spans the window rather than the reading measure: capped, the toggle and the
|
|
350
|
+
theme control floated in from the edges by an amount that changed per page. */
|
|
299
351
|
.mast {
|
|
300
|
-
max-width:
|
|
301
|
-
margin: 0
|
|
302
|
-
|
|
352
|
+
max-width: none;
|
|
353
|
+
margin: 0;
|
|
354
|
+
min-height: var(--teach-mast-h);
|
|
355
|
+
padding: 0 0.9rem;
|
|
303
356
|
display: flex;
|
|
304
357
|
justify-content: space-between;
|
|
305
358
|
align-items: center;
|
|
306
|
-
gap:
|
|
359
|
+
gap: 0.55rem;
|
|
307
360
|
font-size: var(--t5);
|
|
308
361
|
color: var(--color-muted);
|
|
362
|
+
cursor: default;
|
|
309
363
|
}
|
|
310
364
|
|
|
311
365
|
.mast a { color: var(--color-accent); text-decoration: none; font-weight: 600; }
|
|
312
366
|
.mast a:hover { text-decoration: underline; }
|
|
313
367
|
|
|
314
|
-
.theme {
|
|
315
|
-
font-family: var(--teach-sans);
|
|
316
|
-
font-size: 0.75rem;
|
|
317
|
-
font-weight: 600;
|
|
318
|
-
color: var(--color-muted);
|
|
319
|
-
background: transparent;
|
|
320
|
-
border: 1px solid var(--color-border);
|
|
321
|
-
border-radius: 99px;
|
|
322
|
-
padding: 0.25rem 0.7rem;
|
|
323
|
-
cursor: pointer;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
.theme:hover { border-color: var(--color-accent); color: var(--color-accent); }
|
|
327
|
-
|
|
328
368
|
.nav {
|
|
329
369
|
width: 100%;
|
|
330
370
|
margin: 0;
|
|
@@ -371,7 +411,7 @@ main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); mar
|
|
|
371
411
|
restates either. */
|
|
372
412
|
/* A breadcrumb rather than a row of links. The gap tightens because the
|
|
373
413
|
separators now carry the spacing the gap used to. */
|
|
374
|
-
.mast-left { display: flex; align-items: center; gap: 0.
|
|
414
|
+
.mast-left { display: flex; align-items: center; gap: 0.3rem; min-width: 0; flex-wrap: wrap; cursor: default; }
|
|
375
415
|
|
|
376
416
|
/* \`--rule\` measured 1.27:1 here. It is a border token, and a border is a
|
|
377
417
|
shape a reader infers rather than a glyph they resolve, so it is the wrong
|
|
@@ -379,6 +419,7 @@ main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); mar
|
|
|
379
419
|
.crumb-sep {
|
|
380
420
|
color: var(--color-muted);
|
|
381
421
|
font-size: var(--t4);
|
|
422
|
+
margin: 0 0.1rem;
|
|
382
423
|
user-select: none;
|
|
383
424
|
}
|
|
384
425
|
|
|
@@ -391,27 +432,55 @@ main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); mar
|
|
|
391
432
|
|
|
392
433
|
.crumb { display: inline-flex; align-items: center; }
|
|
393
434
|
|
|
394
|
-
/* The label and its caret are one
|
|
395
|
-
the label navigates, the caret opens the menu
|
|
396
|
-
|
|
435
|
+
/* The label and its caret are one component with two zones, like a split
|
|
436
|
+
button: the label navigates, the caret opens the menu, and one surface fills
|
|
437
|
+
for both so the pair reads as a single chip rather than two loose pieces.
|
|
438
|
+
The pointer covers the gap between them, which is interactive on both sides. */
|
|
439
|
+
.crumb-item {
|
|
440
|
+
display: inline-flex;
|
|
441
|
+
align-items: center;
|
|
442
|
+
gap: 0;
|
|
443
|
+
border-radius: 7px;
|
|
444
|
+
padding: 0.1rem 0.15rem 0.1rem 0.35rem;
|
|
445
|
+
cursor: pointer;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.crumb-item:has(details.jump):hover,
|
|
449
|
+
.crumb-item:has(details.jump[open]) { background: var(--color-chrome); }
|
|
397
450
|
|
|
398
|
-
.crumb
|
|
451
|
+
/* Accent reports where you are. A crumb is navigation you can take, so it is
|
|
452
|
+
neutral, and \`.mast a\` keeps the accent for any other bar link. */
|
|
453
|
+
.mast a.crumb { color: var(--color-text-secondary); padding: 0.1rem 0.15rem 0.1rem 0; }
|
|
454
|
+
.mast a.crumb:hover { text-decoration: none; }
|
|
455
|
+
.crumb-item:hover a.crumb { color: var(--color-text); }
|
|
399
456
|
|
|
400
|
-
|
|
401
|
-
|
|
457
|
+
.crumb-here { color: var(--color-text); font-weight: 650; }
|
|
458
|
+
|
|
459
|
+
/* \`align-items: center\` centres boxes, and a label's line box carries
|
|
460
|
+
half-leading above the cap and descender room below, so its capitals sit
|
|
461
|
+
above its own centre by an amount that scales with the font size. Trimming
|
|
462
|
+
the label to its cap band fixes the box rather than nudging the mark. It
|
|
463
|
+
lands on a span holding the text alone, since the link's own \`inline-flex\`
|
|
464
|
+
ignores it. Firefox ships no \`text-box\` and keeps the caret a pixel low. */
|
|
465
|
+
.crumb-t, .crumb-here, .sb-ws .ws-name {
|
|
466
|
+
display: block;
|
|
467
|
+
text-box: trim-both cap alphabetic;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/* The caret is the whole control, so it carries the 24 pixel minimum on its
|
|
471
|
+
own rather than inheriting a label's width. It fills nothing itself, since
|
|
472
|
+
the chip around it does. */
|
|
402
473
|
.crumb-item > .jump > summary {
|
|
403
474
|
min-width: 1.5rem;
|
|
404
475
|
min-height: 1.5rem;
|
|
405
476
|
display: inline-flex;
|
|
406
477
|
align-items: center;
|
|
407
478
|
justify-content: center;
|
|
408
|
-
padding: 0;
|
|
479
|
+
padding: 0.15rem 0.3rem 0.15rem 0;
|
|
409
480
|
border: 0;
|
|
410
481
|
background: transparent;
|
|
411
482
|
}
|
|
412
483
|
|
|
413
|
-
.crumb-item > .jump > summary:hover { background: var(--color-surface); border-radius: 5px; }
|
|
414
|
-
|
|
415
484
|
/* A bar link is its own tap target rather than a line of text, so it carries
|
|
416
485
|
the 24 pixel minimum the a11y rule sets. */
|
|
417
486
|
.mast a,
|
|
@@ -427,23 +496,25 @@ main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); mar
|
|
|
427
496
|
color: var(--color-muted);
|
|
428
497
|
}
|
|
429
498
|
|
|
430
|
-
.mast-right { display: flex; align-items: center; gap: 0.
|
|
499
|
+
.mast-right { display: flex; align-items: center; gap: 0.2rem; margin-left: auto; }
|
|
431
500
|
|
|
501
|
+
/* No border, at the fold control's size and radius, so every control on the
|
|
502
|
+
bar is drawn one way. */
|
|
432
503
|
.theme {
|
|
433
|
-
width: 1.
|
|
434
|
-
height: 1.
|
|
504
|
+
width: 1.75rem;
|
|
505
|
+
height: 1.75rem;
|
|
435
506
|
display: inline-flex;
|
|
436
507
|
align-items: center;
|
|
437
508
|
justify-content: center;
|
|
438
509
|
padding: 0;
|
|
439
|
-
border:
|
|
440
|
-
border-radius:
|
|
510
|
+
border: 0;
|
|
511
|
+
border-radius: 6px;
|
|
441
512
|
background: transparent;
|
|
442
513
|
color: var(--color-muted);
|
|
443
514
|
cursor: pointer;
|
|
444
515
|
}
|
|
445
516
|
|
|
446
|
-
.theme:hover {
|
|
517
|
+
.theme:hover { color: var(--color-text); background: var(--color-chrome); }
|
|
447
518
|
.theme svg { width: 0.95rem; height: 0.95rem; display: block; }
|
|
448
519
|
.theme .moon { display: none; }
|
|
449
520
|
:root[data-theme="dark"] .theme .sun { display: none; }
|
|
@@ -454,28 +525,225 @@ main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); mar
|
|
|
454
525
|
:root:not([data-theme="light"]) .theme .moon { display: block; }
|
|
455
526
|
}
|
|
456
527
|
|
|
457
|
-
/* ----
|
|
528
|
+
/* ---- The course sidebar ---- */
|
|
529
|
+
|
|
530
|
+
/* A segmented progress strip used to sit here, one \`flex: 1\` segment per
|
|
531
|
+
lesson, and it stopped working at the scale a reader most needs it: fifty
|
|
532
|
+
lessons rendered as a row of dots. The sidebar reports course position
|
|
533
|
+
instead, at any length, and the bar reports position inside the lesson. */
|
|
534
|
+
|
|
535
|
+
/* The toggle in the masthead, left of the breadcrumb. */
|
|
536
|
+
.sb-fold {
|
|
537
|
+
flex: none;
|
|
538
|
+
width: 1.75rem;
|
|
539
|
+
height: 1.75rem;
|
|
540
|
+
margin-right: 0.35rem;
|
|
541
|
+
font: inherit;
|
|
542
|
+
font-size: var(--t5);
|
|
543
|
+
line-height: 1;
|
|
544
|
+
color: var(--color-muted);
|
|
545
|
+
background: transparent;
|
|
546
|
+
border: 0;
|
|
547
|
+
border-radius: 6px;
|
|
548
|
+
cursor: pointer;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.sb-fold:hover { color: var(--color-text); background: var(--color-chrome); }
|
|
552
|
+
|
|
553
|
+
.sb {
|
|
554
|
+
flex: 0 0 var(--sb-w, 16rem);
|
|
555
|
+
width: var(--sb-w, 16rem);
|
|
556
|
+
position: sticky;
|
|
557
|
+
top: 0;
|
|
558
|
+
align-self: flex-start;
|
|
559
|
+
height: 100vh;
|
|
560
|
+
display: flex;
|
|
561
|
+
flex-direction: column;
|
|
562
|
+
overflow: visible;
|
|
563
|
+
background: transparent;
|
|
564
|
+
font-family: var(--teach-sans);
|
|
565
|
+
transition: flex-basis 0.14s, width 0.14s;
|
|
566
|
+
}
|
|
458
567
|
|
|
459
|
-
|
|
460
|
-
overlap, and no stacking context of its own. */
|
|
568
|
+
html.sb-shut .sb { flex-basis: 0; width: 0; overflow: hidden; }
|
|
461
569
|
|
|
462
|
-
.
|
|
570
|
+
.sb-top {
|
|
571
|
+
flex: none;
|
|
463
572
|
display: flex;
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
573
|
+
align-items: center;
|
|
574
|
+
height: var(--teach-mast-h);
|
|
575
|
+
padding: 0 0.95rem;
|
|
576
|
+
border-bottom: 1px solid var(--color-border);
|
|
468
577
|
}
|
|
469
578
|
|
|
470
|
-
.
|
|
471
|
-
|
|
472
|
-
|
|
579
|
+
.sb-ws { position: relative; }
|
|
580
|
+
|
|
581
|
+
.sb-ws > summary {
|
|
582
|
+
list-style: none;
|
|
583
|
+
cursor: pointer;
|
|
584
|
+
display: inline-flex;
|
|
585
|
+
align-items: center;
|
|
586
|
+
gap: 0.35rem;
|
|
587
|
+
padding: 0.25rem 0.3rem;
|
|
588
|
+
border-radius: 6px;
|
|
589
|
+
font-size: var(--t4);
|
|
590
|
+
font-weight: 650;
|
|
591
|
+
color: var(--color-text);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
.sb-ws > summary::-webkit-details-marker { display: none; }
|
|
595
|
+
.sb-ws > summary:hover, .sb-ws[open] > summary { background: var(--color-chrome); color: var(--color-text); }
|
|
596
|
+
.sb-ws .car { color: var(--color-muted); display: inline-flex; align-items: center; }
|
|
597
|
+
|
|
598
|
+
.sb-wl {
|
|
599
|
+
position: absolute;
|
|
600
|
+
top: calc(100% + 0.35rem);
|
|
601
|
+
left: 0;
|
|
602
|
+
z-index: 40;
|
|
603
|
+
min-width: 13rem;
|
|
604
|
+
background: var(--color-background);
|
|
605
|
+
border: 1px solid var(--color-border);
|
|
606
|
+
border-radius: 9px;
|
|
607
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14);
|
|
608
|
+
padding: 0.3rem;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
:root[data-theme="dark"] .sb-wl { box-shadow: 0 8px 28px rgba(0, 0, 0, 0.5); }
|
|
612
|
+
|
|
613
|
+
/* The popover chrome comes from \`.sb-wl\`, so the list nested inside it drops
|
|
614
|
+
its own rather than drawing a second border inside the first. */
|
|
615
|
+
.sb-wl .jump-list {
|
|
616
|
+
position: static;
|
|
617
|
+
min-width: 0;
|
|
618
|
+
max-width: none;
|
|
619
|
+
border: 0;
|
|
620
|
+
box-shadow: none;
|
|
621
|
+
padding: 0;
|
|
622
|
+
background: transparent;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
.sb-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-bottom: 1rem; }
|
|
626
|
+
|
|
627
|
+
.sb-meta {
|
|
628
|
+
padding: 1rem 1.25rem 0.55rem;
|
|
629
|
+
color: var(--color-muted);
|
|
630
|
+
font-size: var(--t6);
|
|
631
|
+
font-variant-numeric: tabular-nums;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
.sb-filter { padding: 0 0.95rem 0.6rem; }
|
|
635
|
+
|
|
636
|
+
.sb-filter input {
|
|
637
|
+
width: 100%;
|
|
638
|
+
box-sizing: border-box;
|
|
639
|
+
font: inherit;
|
|
640
|
+
font-size: var(--t6);
|
|
641
|
+
color: var(--color-text);
|
|
642
|
+
background: transparent;
|
|
643
|
+
border: 1px solid var(--color-border);
|
|
644
|
+
border-radius: 7px;
|
|
645
|
+
padding: 0.3rem 0.55rem;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.sb-filter input:focus { outline: none; border-color: var(--color-accent); }
|
|
649
|
+
|
|
650
|
+
.sb-list { list-style: none; margin: 0; padding: 0 0.7rem; }
|
|
651
|
+
.sb-list li { margin: 0; }
|
|
652
|
+
.sb-list li.hide { display: none; }
|
|
653
|
+
|
|
654
|
+
.sb-l {
|
|
655
|
+
position: relative;
|
|
656
|
+
display: flex;
|
|
657
|
+
gap: 0.55rem;
|
|
658
|
+
align-items: baseline;
|
|
659
|
+
padding: 0.38rem 0.55rem;
|
|
660
|
+
border-radius: 6px;
|
|
661
|
+
text-decoration: none;
|
|
662
|
+
color: var(--color-text-body);
|
|
663
|
+
font-size: var(--t5);
|
|
664
|
+
line-height: 1.45;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
.sb-l:hover { background: var(--color-chrome); color: var(--color-text); }
|
|
668
|
+
|
|
669
|
+
.sb-n {
|
|
670
|
+
flex: none;
|
|
671
|
+
font-family: var(--teach-mono);
|
|
672
|
+
font-size: var(--t6);
|
|
673
|
+
color: var(--color-muted);
|
|
674
|
+
font-variant-numeric: tabular-nums;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
.sb-on { color: var(--color-text); font-weight: 650; }
|
|
678
|
+
|
|
679
|
+
.sb-on::before {
|
|
680
|
+
content: "";
|
|
681
|
+
position: absolute;
|
|
682
|
+
left: -0.6rem;
|
|
683
|
+
top: 0.28rem;
|
|
684
|
+
bottom: 0.28rem;
|
|
685
|
+
width: 2px;
|
|
473
686
|
border-radius: 2px;
|
|
687
|
+
background: var(--color-accent);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
.sb-on .sb-n { color: var(--color-accent); }
|
|
691
|
+
|
|
692
|
+
.sb-empty { padding: 0.3rem 1.2rem; color: var(--color-muted); font-size: var(--t6); }
|
|
693
|
+
|
|
694
|
+
.sb-foot {
|
|
695
|
+
flex: none;
|
|
696
|
+
padding: 0.7rem 1.25rem;
|
|
697
|
+
border-top: 1px solid var(--color-border);
|
|
698
|
+
color: var(--color-muted);
|
|
699
|
+
font-size: var(--t6);
|
|
700
|
+
font-variant-numeric: tabular-nums;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/* The panel is resizable between a width that still fits a lesson title and one
|
|
704
|
+
that would start eating the reading measure. */
|
|
705
|
+
.sb-grip {
|
|
706
|
+
position: absolute;
|
|
707
|
+
top: 0;
|
|
708
|
+
bottom: 0;
|
|
709
|
+
right: -3px;
|
|
710
|
+
width: 7px;
|
|
711
|
+
z-index: 25;
|
|
712
|
+
padding: 0;
|
|
713
|
+
border: 0;
|
|
714
|
+
background: transparent;
|
|
715
|
+
cursor: col-resize;
|
|
716
|
+
touch-action: none;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
.sb-grip::before {
|
|
720
|
+
content: "";
|
|
721
|
+
position: absolute;
|
|
722
|
+
left: 3px;
|
|
723
|
+
top: 50%;
|
|
724
|
+
transform: translateY(-50%);
|
|
725
|
+
width: 1px;
|
|
726
|
+
height: 2.2rem;
|
|
727
|
+
border-radius: 1px;
|
|
474
728
|
background: var(--color-border);
|
|
729
|
+
opacity: 0;
|
|
730
|
+
transition: opacity 0.12s;
|
|
475
731
|
}
|
|
476
732
|
|
|
477
|
-
.
|
|
478
|
-
|
|
733
|
+
.sb-grip:hover::before, .sb-grip:focus-visible::before, html.sb-drag .sb-grip::before {
|
|
734
|
+
opacity: 1;
|
|
735
|
+
background: var(--color-accent);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
.sb-grip:focus-visible { outline: none; }
|
|
739
|
+
html.sb-drag { cursor: col-resize; user-select: none; }
|
|
740
|
+
html.sb-shut .sb-grip { display: none; }
|
|
741
|
+
|
|
742
|
+
/* Both belong to the overlay and the script appends both on every page, so
|
|
743
|
+
each is hidden until the breakpoint hands it a job. Without this the close
|
|
744
|
+
control renders as an unstyled button stretched across the column, and the
|
|
745
|
+
scrim becomes a flex item of the body. */
|
|
746
|
+
.sb-close, .sb-scrim { display: none; }
|
|
479
747
|
|
|
480
748
|
/* ---- Footer navigation, the only place it appears ---- */
|
|
481
749
|
|
|
@@ -516,48 +784,58 @@ main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); mar
|
|
|
516
784
|
}
|
|
517
785
|
|
|
518
786
|
.jump summary::-webkit-details-marker { display: none; }
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
.jump summary .caret {
|
|
524
|
-
.jump summary:hover .caret { opacity: 1; }
|
|
525
|
-
.jump[open] summary { border-color: var(--color-accent); color: var(--color-accent); }
|
|
526
|
-
.jump[open] summary .caret { opacity: 1; }
|
|
787
|
+
/* The caret says "this is a menu" and nothing else, so it is drawn at full
|
|
788
|
+
strength in every state and stays muted. A reveal on hover hid the only
|
|
789
|
+
route into the menu from a reader who cannot hover, and an affordance is not
|
|
790
|
+
a state, so it takes no accent on hover or while open. */
|
|
791
|
+
.jump summary .caret { color: var(--color-muted); }
|
|
527
792
|
|
|
528
793
|
.jump-list {
|
|
529
794
|
position: absolute;
|
|
530
795
|
top: calc(100% + 0.4rem);
|
|
531
796
|
left: 0;
|
|
532
797
|
z-index: 20;
|
|
533
|
-
min-width: 21rem;
|
|
534
798
|
background: var(--color-background);
|
|
535
799
|
border: 1px solid var(--color-border);
|
|
536
800
|
border-radius: 9px;
|
|
537
801
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14);
|
|
538
|
-
padding: 0.
|
|
802
|
+
padding: 0.25rem;
|
|
539
803
|
list-style: none;
|
|
540
804
|
margin: 0;
|
|
541
805
|
}
|
|
542
806
|
|
|
543
807
|
:root[data-theme="dark"] .jump-list { box-shadow: 0 8px 28px rgba(0, 0, 0, 0.5); }
|
|
544
808
|
|
|
809
|
+
/* One row recipe wherever the menu is mounted, on the steps the lesson list
|
|
810
|
+
beside it already reads: the label at \`--t5\` and the numeral at \`--t6\` mono.
|
|
811
|
+
The weight is restated because \`.mast a\` sets 600 for the bar's own links. */
|
|
545
812
|
.jump-list a {
|
|
546
813
|
display: grid;
|
|
547
814
|
grid-template-columns: 1.65rem 1fr auto;
|
|
548
|
-
gap: 0.
|
|
815
|
+
gap: 0.55rem;
|
|
549
816
|
align-items: center;
|
|
550
|
-
padding: 0.
|
|
817
|
+
padding: 0.38rem 0.55rem;
|
|
551
818
|
border-radius: 6px;
|
|
552
819
|
text-decoration: none;
|
|
553
820
|
color: var(--color-text-body);
|
|
554
|
-
font-size:
|
|
821
|
+
font-size: var(--t5);
|
|
822
|
+
font-weight: 400;
|
|
823
|
+
line-height: 1.45;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
.jump-list a:hover { background: var(--color-chrome); }
|
|
827
|
+
.jump-list .n { font-family: var(--teach-mono); font-size: var(--t6); color: var(--color-muted); }
|
|
828
|
+
/* The third column used to hold a status dot that read the same on every row,
|
|
829
|
+
so it reported nothing. It is a per-entry trailing slot now: the workspace
|
|
830
|
+
menu puts a lesson count there and the lesson menu leaves it empty. */
|
|
831
|
+
.jump-list .ct {
|
|
832
|
+
font-family: var(--teach-mono);
|
|
833
|
+
font-size: var(--t6);
|
|
834
|
+
color: var(--color-muted);
|
|
835
|
+
font-variant-numeric: tabular-nums;
|
|
836
|
+
text-align: right;
|
|
555
837
|
}
|
|
556
838
|
|
|
557
|
-
.jump-list a:hover { background: var(--color-surface); }
|
|
558
|
-
.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); }
|
|
561
839
|
.jump-list li.at a { background: var(--color-teach-accent-bg); }
|
|
562
840
|
.jump-list li.at .n { color: var(--color-accent); }
|
|
563
841
|
.jump-list li.soon a { color: var(--color-muted); }
|
|
@@ -568,11 +846,11 @@ main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); mar
|
|
|
568
846
|
.jump-list li.all a { color: var(--color-accent); }
|
|
569
847
|
.jump-list li.all .n { color: var(--color-accent); }
|
|
570
848
|
|
|
571
|
-
/* The menu
|
|
572
|
-
|
|
573
|
-
|
|
849
|
+
/* The menu fits its content above an 11rem floor rather than padding three
|
|
850
|
+
short titles out to a wide panel. It is anchored to its summary, so the
|
|
851
|
+
clamp keeps a long row wrapping on screen without a breakpoint. */
|
|
574
852
|
.jump-list {
|
|
575
|
-
min-width: min(
|
|
853
|
+
min-width: min(11rem, calc(100vw - 2rem));
|
|
576
854
|
max-width: calc(100vw - 2rem);
|
|
577
855
|
}
|
|
578
856
|
|
|
@@ -593,8 +871,7 @@ main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); mar
|
|
|
593
871
|
@media (max-width: 640px) {
|
|
594
872
|
.bar { padding: 0 1rem; }
|
|
595
873
|
main { padding-left: 1rem; padding-right: 1rem; }
|
|
596
|
-
.mast { font-size: 0.75rem;
|
|
597
|
-
.mast-left { gap: 0.5rem; row-gap: 0.35rem; }
|
|
874
|
+
.mast { font-size: 0.75rem; }
|
|
598
875
|
}
|
|
599
876
|
|
|
600
877
|
@media (max-width: 640px) {
|
|
@@ -606,6 +883,94 @@ main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); mar
|
|
|
606
883
|
min-width: 0;
|
|
607
884
|
max-width: none;
|
|
608
885
|
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/* ---- What gives way below 1100px ---- */
|
|
889
|
+
|
|
890
|
+
/* Hiding the sidebar outright leaves the toggle painted exactly where it cannot
|
|
891
|
+
act, and it deletes the filter and the per-lesson outline, which the
|
|
892
|
+
breadcrumb caret does not carry. The panel becomes an overlay instead, and it
|
|
893
|
+
states its own ground because \`.sb\` is transparent by design for a column
|
|
894
|
+
sitting on the page rather than floating over the lesson. */
|
|
895
|
+
@media (max-width: ${TEACH_SIDEBAR_BREAKPOINT}px) {
|
|
896
|
+
.pane { border-left: 0; }
|
|
897
|
+
|
|
898
|
+
.sb {
|
|
899
|
+
position: fixed;
|
|
900
|
+
top: 0;
|
|
901
|
+
left: 0;
|
|
902
|
+
bottom: 0;
|
|
903
|
+
z-index: 60;
|
|
904
|
+
width: min(21rem, 86vw);
|
|
905
|
+
height: 100dvh;
|
|
906
|
+
overflow-y: auto;
|
|
907
|
+
background: var(--color-background);
|
|
908
|
+
border-right: 1px solid var(--color-border);
|
|
909
|
+
box-shadow: 0 0 40px rgb(0 0 0 / 35%);
|
|
910
|
+
transform: translateX(-100%);
|
|
911
|
+
transition: transform 0.18s;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
html:not(.sb-shut) .sb { transform: none; }
|
|
915
|
+
|
|
916
|
+
/* The wide rule collapses a shut panel to \`width: 0\` and outranks a bare
|
|
917
|
+
\`.sb\` in here, which produced a one-pixel panel whose contents spilled out
|
|
918
|
+
and intercepted every click on the toggle. The shut state restates the
|
|
919
|
+
width so the transform is what moves the panel off screen. */
|
|
920
|
+
html.sb-shut .sb { width: min(21rem, 86vw); flex-basis: auto; overflow-y: auto; }
|
|
921
|
+
|
|
922
|
+
/* A transform moves the panel out of sight and leaves every control inside it
|
|
923
|
+
in the tab order, so the first Tab from a shut page landed on the workspace
|
|
924
|
+
switcher inside an invisible panel. Visibility takes it out of the
|
|
925
|
+
sequence, delayed on the way out so the slide still shows. */
|
|
926
|
+
html.sb-shut .sb { visibility: hidden; transition: transform 0.18s, visibility 0s linear 0.18s; }
|
|
927
|
+
html:not(.sb-shut) .sb { visibility: visible; transition: transform 0.18s, visibility 0s; }
|
|
928
|
+
|
|
929
|
+
.sb-grip { display: none; }
|
|
930
|
+
|
|
931
|
+
/* The panel covers the masthead and the toggle that opened it, so it carries
|
|
932
|
+
its own way out rather than leaving the scrim as the only route on a phone.
|
|
933
|
+
It carries no border, at 1.75rem to match \`.sb-fold\` and \`.theme\`, which also clears
|
|
934
|
+
the 24 pixel pointer target floor. */
|
|
935
|
+
.sb-close {
|
|
936
|
+
display: inline-flex;
|
|
937
|
+
align-items: center;
|
|
938
|
+
justify-content: center;
|
|
939
|
+
position: absolute;
|
|
940
|
+
top: 0.55rem;
|
|
941
|
+
right: 0.6rem;
|
|
942
|
+
z-index: 61;
|
|
943
|
+
appearance: none;
|
|
944
|
+
width: 1.75rem;
|
|
945
|
+
height: 1.75rem;
|
|
946
|
+
font: inherit;
|
|
947
|
+
font-size: var(--t5);
|
|
948
|
+
line-height: 1;
|
|
949
|
+
color: var(--color-muted);
|
|
950
|
+
background: transparent;
|
|
951
|
+
border: 0;
|
|
952
|
+
border-radius: 6px;
|
|
953
|
+
cursor: pointer;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
.sb-close:hover { color: var(--color-text); background: var(--color-chrome); }
|
|
957
|
+
|
|
958
|
+
.sb-scrim {
|
|
959
|
+
display: block;
|
|
960
|
+
position: fixed;
|
|
961
|
+
inset: 0;
|
|
962
|
+
z-index: 55;
|
|
963
|
+
background: rgb(0 0 0 / 45%);
|
|
964
|
+
opacity: 0;
|
|
965
|
+
pointer-events: none;
|
|
966
|
+
transition: opacity 0.18s;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
html:not(.sb-shut) .sb-scrim { opacity: 1; pointer-events: auto; }
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
@media (prefers-reduced-motion: reduce) {
|
|
973
|
+
.sb, .sb-scrim, .sb-grip::before { transition: none; }
|
|
609
974
|
}`,
|
|
610
975
|
}
|
|
611
976
|
|
|
@@ -1148,83 +1513,30 @@ ol.succ li { margin-bottom: 0.4rem; }
|
|
|
1148
1513
|
const TEACH_OUTLINE: Component = {
|
|
1149
1514
|
name: 'teach-outline',
|
|
1150
1515
|
note: [
|
|
1151
|
-
'The in-lesson outline
|
|
1152
|
-
'
|
|
1516
|
+
'The in-lesson outline, folded under the lesson being read in the course',
|
|
1517
|
+
'sidebar. It used to be a fixed rail in the gutter the reading measure left',
|
|
1518
|
+
'over, which reported the same thing twice once the sidebar carried the',
|
|
1519
|
+
'course and disappeared entirely below 1420px.',
|
|
1153
1520
|
].join('\n '),
|
|
1154
|
-
reads: [
|
|
1155
|
-
|
|
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
|
-
}
|
|
1521
|
+
reads: ['--color-text', '--color-text-secondary', '--color-accent', '--t6'],
|
|
1522
|
+
rules: `/* ---- In-lesson outline, folded under the lesson being read ---- */
|
|
1203
1523
|
|
|
1204
|
-
|
|
1524
|
+
/* The rail this replaces was \`position: fixed\` in the right-hand gutter, which
|
|
1525
|
+
only existed above 1420px and reported nothing at all below it. Folding the
|
|
1526
|
+
headings under the current lesson puts the course and the lesson in one
|
|
1527
|
+
column, so a reader tracks both in one place at any window width. */
|
|
1528
|
+
.sb-out { list-style: none; margin: 0.1rem 0 0.55rem; padding: 0 0 0 1.45rem; }
|
|
1205
1529
|
|
|
1206
|
-
.
|
|
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 {
|
|
1530
|
+
.sb-out a {
|
|
1217
1531
|
display: block;
|
|
1218
|
-
padding: 0.
|
|
1219
|
-
border-left: 2px solid var(--color-border);
|
|
1220
|
-
color: var(--color-muted);
|
|
1532
|
+
padding: 0.18rem 0.5rem;
|
|
1221
1533
|
text-decoration: none;
|
|
1534
|
+
color: var(--color-text-secondary);
|
|
1535
|
+
font-size: var(--t6);
|
|
1222
1536
|
}
|
|
1223
1537
|
|
|
1224
|
-
.
|
|
1225
|
-
.
|
|
1226
|
-
|
|
1227
|
-
@media (max-width: 1420px) { .outline { display: none; } }`,
|
|
1538
|
+
.sb-out a:hover { color: var(--color-text); }
|
|
1539
|
+
.sb-out a.on { color: var(--color-accent); }`,
|
|
1228
1540
|
}
|
|
1229
1541
|
|
|
1230
1542
|
const TEACH_REFERENCES: Component = {
|