jekyll-theme-zer0 1.30.0 → 1.31.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +171 -0
- data/_data/backlog.yml +30 -7
- data/_data/consumers.yml +99 -0
- data/_data/features.yml +1 -1
- data/_data/theme-manifest.yml +35 -33
- data/_data/ui-text.yml +1 -0
- data/_includes/README.md +1 -1
- data/_includes/analytics/posthog.html +28 -9
- data/_includes/components/README.md +1 -1
- data/_includes/components/background-image.html +7 -1
- data/_includes/components/cookie-consent.html +30 -30
- data/_includes/components/mermaid.html +14 -7
- data/_includes/components/preview-image.html +22 -3
- data/_includes/components/theme-preview-gallery.html +1 -1
- data/_includes/content/intro.html +7 -0
- data/_includes/content/seo.html +8 -0
- data/_includes/content/sitemap.html +220 -263
- data/_includes/core/branding.html +8 -15
- data/_includes/core/head.html +29 -5
- data/_includes/core/header.html +14 -2
- data/_includes/core/tokens-inline.html +16 -1
- data/_includes/navigation/navbar.html +9 -6
- data/_includes/setup/wizard.html +5 -1
- data/_layouts/article.html +15 -3
- data/_layouts/home.html +13 -1
- data/_layouts/note.html +2 -1
- data/_layouts/notebook.html +2 -1
- data/_layouts/root.html +22 -0
- data/_sass/components/_cookie-banner.scss +48 -0
- data/_sass/components/_setup-wizard.scss +19 -1
- data/_sass/components/_ui-enhancements.scss +11 -0
- data/_sass/core/_navbar.scss +137 -70
- data/_sass/core/_sidebar-extras.scss +2 -14
- data/_sass/core/code-copy.scss +27 -8
- data/assets/css/extension-points-demo.css +27 -0
- data/assets/data/wiki-index.json +19 -0
- data/assets/js/auto-hide-nav.js +53 -13
- data/assets/js/code-copy.js +72 -1
- data/assets/js/extension-points-demo.js +48 -0
- data/assets/js/mermaid-diagrams.js +128 -22
- data/assets/js/obsidian-local-graph.js +27 -5
- data/assets/js/setup-wizard.js +7 -2
- data/scripts/ci/test_visual_evidence_autogen.py +64 -2
- data/scripts/ci/visual_evidence_autogen.py +14 -4
- data/scripts/features/install-preview-generator +1 -1
- data/scripts/lib/README.md +12 -0
- data/scripts/lib/preview_generator.py +2 -2
- metadata +4 -2
data/_sass/core/_navbar.scss
CHANGED
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
// Breakpoints:
|
|
7
7
|
// - Desktop >= 992px (inline nav; label density via @container bd-nav)
|
|
8
8
|
// - Mobile < 992px (offcanvas)
|
|
9
|
-
// Nav label tiers (center track / bd-nav container width):
|
|
9
|
+
// Nav label tiers (center track / bd-nav container width) — TWO, never truncating:
|
|
10
10
|
// - >= 51rem (~816px): icon + full label
|
|
11
|
-
// -
|
|
12
|
-
//
|
|
11
|
+
// - < 51rem: icon-only (tooltips on lg–xl viewports)
|
|
12
|
+
// Icons are present in BOTH tiers. The old bare-label middle tier and the
|
|
13
|
+
// `text-overflow: ellipsis` safety net were removed in #405 — a truncated
|
|
14
|
+
// label ("Quicksta…") is worse than an icon with a tooltip.
|
|
13
15
|
// ==============================================================================
|
|
14
16
|
|
|
15
17
|
// -----------------------------------------------------------------------------
|
|
@@ -92,6 +94,20 @@
|
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
// The below-lg menu toggle carries a visible "Menu" label beside its glyph, so
|
|
98
|
+
// it cannot be confused with the sidebar hamburger sitting next to it (#405).
|
|
99
|
+
.navbar-toggler.navbar-toggler-labeled {
|
|
100
|
+
display: inline-flex;
|
|
101
|
+
align-items: center;
|
|
102
|
+
gap: 0.375rem;
|
|
103
|
+
width: auto;
|
|
104
|
+
|
|
105
|
+
.navbar-toggler-text {
|
|
106
|
+
font-size: 0.9375rem;
|
|
107
|
+
line-height: 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
95
111
|
// Shared icon-only nav styling (applied via @container bd-nav below)
|
|
96
112
|
@mixin navbar-nav-icon-only {
|
|
97
113
|
#bdNavbar .nav-link .nav-link-text {
|
|
@@ -396,16 +412,29 @@
|
|
|
396
412
|
transform: rotate(180deg);
|
|
397
413
|
}
|
|
398
414
|
|
|
399
|
-
// Label density
|
|
400
|
-
//
|
|
415
|
+
// Label density has TWO tiers, measured on the center track (the `bd-nav`
|
|
416
|
+
// container), so items either fit whole or drop to icons — never truncate:
|
|
401
417
|
// >= 51rem (816px) icon + label — the roomy default
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
|
|
418
|
+
// < 51rem icon only — icon + label no longer fits
|
|
419
|
+
// 51rem is the MEASURED width the icon+label row needs for the theme's seven
|
|
420
|
+
// demo items (815px), so the upper tier engages exactly when its own content
|
|
421
|
+
// fits and the lower one takes over otherwise.
|
|
422
|
+
//
|
|
423
|
+
// There used to be a middle 41-50.99rem tier that dropped the ICONS to buy
|
|
424
|
+
// ~20px per item, plus `text-overflow: ellipsis` as a last-resort safety net.
|
|
425
|
+
// Both are gone (#405): the ellipsis produced "Quicksta…", and a clipped
|
|
426
|
+
// label is worse than an icon with a tooltip — the icon is at least honest
|
|
427
|
+
// about being an abbreviation. Icons now survive at every tier, and the
|
|
428
|
+
// icon-only cutoff moved 41rem -> 51rem to take over the band the middle
|
|
429
|
+
// tier used to hold. That is a deliberate behaviour change at 656-816px of
|
|
430
|
+
// center track: those widths used to show bare labels and now show icons,
|
|
431
|
+
// with `.nav-tooltip` (992-1199px) carrying the name on hover.
|
|
432
|
+
//
|
|
433
|
+
// Consumers with materially more (or longer) items than the theme's seven
|
|
434
|
+
// demo entries can exceed 51rem of content; with shrink and ellipsis both
|
|
435
|
+
// removed, the row overflows rather than truncates. A container query
|
|
436
|
+
// cannot measure content, so raise the boundary below if that happens.
|
|
437
|
+
@container bd-nav (min-width: 51rem) {
|
|
409
438
|
#bdNavbar .navbar-nav {
|
|
410
439
|
gap: 0.125rem;
|
|
411
440
|
}
|
|
@@ -414,28 +443,34 @@
|
|
|
414
443
|
padding: 0.375rem;
|
|
415
444
|
}
|
|
416
445
|
|
|
446
|
+
// No `overflow: hidden` / `text-overflow: ellipsis` here: this tier only
|
|
447
|
+
// engages once the whole icon+label row fits, so a clipped label would be
|
|
448
|
+
// a bug rather than a fallback. Labels stay whole or the row goes
|
|
449
|
+
// icon-only (#405).
|
|
417
450
|
#bdNavbar .nav-link .nav-link-text {
|
|
418
451
|
display: inline !important;
|
|
419
452
|
margin-left: 0;
|
|
420
453
|
min-width: 0;
|
|
421
|
-
overflow: hidden;
|
|
422
|
-
text-overflow: ellipsis;
|
|
423
454
|
white-space: nowrap;
|
|
424
455
|
transition: opacity 0.15s ease-in-out;
|
|
425
456
|
}
|
|
426
457
|
|
|
458
|
+
// Items do NOT shrink in this tier. Shrinking is what produced the
|
|
459
|
+
// clipped label this tier removes: the tier only engages once the full
|
|
460
|
+
// icon+label row fits, so an item that shrinks here is being cut for no
|
|
461
|
+
// reason.
|
|
427
462
|
#bdNavbar .nav-item,
|
|
428
463
|
#bdNavbar .nav-hover-dropdown {
|
|
429
|
-
flex: 0
|
|
464
|
+
flex: 0 0 auto;
|
|
430
465
|
min-width: 0;
|
|
431
466
|
}
|
|
432
467
|
|
|
433
468
|
#bdNavbar .nav-item > .nav-link:not(.dropdown-toggle-split),
|
|
434
469
|
#bdNavbar .nav-hover-dropdown > .nav-link:first-child {
|
|
435
|
-
flex:
|
|
470
|
+
flex: 0 0 auto;
|
|
436
471
|
min-width: 0;
|
|
437
472
|
max-width: 100%;
|
|
438
|
-
overflow:
|
|
473
|
+
overflow: visible;
|
|
439
474
|
gap: 0.25rem;
|
|
440
475
|
}
|
|
441
476
|
|
|
@@ -444,52 +479,69 @@
|
|
|
444
479
|
font-size: 1rem;
|
|
445
480
|
}
|
|
446
481
|
|
|
447
|
-
|
|
482
|
+
// Hover lives on the ROW, not on each control. With the chevron overlaying
|
|
483
|
+
// the link's padding box (below), a per-control background would light
|
|
484
|
+
// only half the row as the pointer crossed it.
|
|
485
|
+
#bdNavbar .nav-item:not(.nav-hover-dropdown) > .nav-link:hover,
|
|
486
|
+
#bdNavbar .nav-hover-dropdown:hover > .nav-link:first-child {
|
|
448
487
|
background-color: var(--bs-tertiary-bg);
|
|
449
488
|
border-radius: 0.375rem;
|
|
450
489
|
}
|
|
451
490
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
padding-right: 0;
|
|
465
|
-
margin-left: 0 !important;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// Middle tier — the labels fit but the icons no longer do. Dropping the
|
|
470
|
-
// decorative icon (the label is the accessible name) and tightening the
|
|
471
|
-
// chevron buys back roughly 20px per item, which is the difference between
|
|
472
|
-
// "Notebooks" and "N…" at 992-1200px viewports.
|
|
473
|
-
@container bd-nav (min-width: 41rem) and (max-width: 50.99rem) {
|
|
474
|
-
#bdNavbar .nav-link i {
|
|
475
|
-
display: none;
|
|
491
|
+
// Chevron merged into the parent link's box (#405). It stays a real
|
|
492
|
+
// <button> — keyboard reachable, `aria-expanded`/`aria-haspopup` intact —
|
|
493
|
+
// and is only repositioned: absolutely placed over the right end of the
|
|
494
|
+
// link's padding, which the link reserves with extra padding-right. There
|
|
495
|
+
// is no longer a gap between the two, so the dead zone between label and
|
|
496
|
+
// chevron is gone and the whole row is one hover target.
|
|
497
|
+
// Every rule below is scoped with #bdNavbar deliberately: the tier's own
|
|
498
|
+
// `#bdNavbar .nav-link { padding: 0.375rem }` above carries an id, so a
|
|
499
|
+
// class-only `padding-right` here would lose to it and the chevron would
|
|
500
|
+
// have no reserved space to sit in.
|
|
501
|
+
#bdNavbar .nav-hover-dropdown {
|
|
502
|
+
position: relative;
|
|
476
503
|
}
|
|
477
504
|
|
|
478
|
-
#bdNavbar .nav-item > .nav-link:not(.dropdown-toggle-split),
|
|
479
505
|
#bdNavbar .nav-hover-dropdown > .nav-link:first-child {
|
|
480
|
-
|
|
506
|
+
border-radius: 0.375rem;
|
|
507
|
+
padding-right: 1.25rem;
|
|
481
508
|
}
|
|
482
509
|
|
|
483
|
-
// Scoped with #bdNavbar to outrank the shared base rule at the top of this
|
|
484
|
-
// file, which sets the chevron to 1.5rem.
|
|
485
510
|
#bdNavbar .nav-hover-dropdown > .dropdown-toggle-split {
|
|
486
|
-
|
|
487
|
-
|
|
511
|
+
position: absolute;
|
|
512
|
+
top: 0;
|
|
513
|
+
right: 0;
|
|
514
|
+
bottom: 0;
|
|
515
|
+
z-index: 1;
|
|
516
|
+
display: flex;
|
|
517
|
+
align-items: center;
|
|
518
|
+
justify-content: center;
|
|
519
|
+
width: 1.25rem;
|
|
520
|
+
min-width: 1.25rem;
|
|
521
|
+
min-height: 0;
|
|
522
|
+
margin: 0 !important;
|
|
523
|
+
padding: 0;
|
|
524
|
+
background-color: transparent;
|
|
525
|
+
border: 0;
|
|
526
|
+
border-radius: 0 0.375rem 0.375rem 0;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// The overlay must not paint its own hover background — the row already
|
|
530
|
+
// does. Without this the chevron reads as a separate control again, which
|
|
531
|
+
// is the whole defect. `!important` because the shared base block at the
|
|
532
|
+
// top of this file sets the chevron's own hover background with equal
|
|
533
|
+
// specificity and later source order.
|
|
534
|
+
#bdNavbar .nav-hover-dropdown > .dropdown-toggle-split:hover,
|
|
535
|
+
#bdNavbar .nav-hover-dropdown > .dropdown-toggle-split:focus-visible,
|
|
536
|
+
#bdNavbar .nav-hover-dropdown > .dropdown-toggle-split[aria-expanded="true"] {
|
|
537
|
+
background-color: transparent !important;
|
|
488
538
|
}
|
|
489
539
|
}
|
|
490
540
|
|
|
491
|
-
// Icon-only when the center track
|
|
492
|
-
|
|
541
|
+
// Icon-only when the center track cannot fit the icon+label row. The
|
|
542
|
+
// boundary is the complement of the tier above so the two tiers tile the
|
|
543
|
+
// whole range with no gap.
|
|
544
|
+
@container bd-nav (max-width: 50.99rem) {
|
|
493
545
|
@include navbar-nav-icon-only;
|
|
494
546
|
}
|
|
495
547
|
}
|
|
@@ -1099,8 +1151,7 @@
|
|
|
1099
1151
|
align-items: center;
|
|
1100
1152
|
}
|
|
1101
1153
|
|
|
1102
|
-
#navbar .site-title-text
|
|
1103
|
-
#navbar .site-subtitle-text {
|
|
1154
|
+
#navbar .site-title-text {
|
|
1104
1155
|
display: inline-block;
|
|
1105
1156
|
}
|
|
1106
1157
|
|
|
@@ -1137,9 +1188,28 @@
|
|
|
1137
1188
|
|
|
1138
1189
|
// Desktop (lg+): 3-column grid — [brand/title | main nav | utilities] so nav cannot paint over Search/Settings
|
|
1139
1190
|
@media (min-width: 992px) {
|
|
1191
|
+
// Optical centering (#405) WITHOUT breaking the container queries.
|
|
1192
|
+
//
|
|
1193
|
+
// The obvious spelling — `1fr auto 1fr` — is a trap here. Track 2 must stay
|
|
1194
|
+
// a `1fr` (a DEFINITE width): `.bd-navbar-nav-viewport` inside it is
|
|
1195
|
+
// `width: 100%` + `container-type: inline-size`, and that percentage only
|
|
1196
|
+
// resolves against a definite track. Make track 2 `auto` and it becomes
|
|
1197
|
+
// content-sized, size containment hands `bd-nav` a degenerate width, and
|
|
1198
|
+
// EVERY `@container bd-nav` label tier stops firing — the bar collapses to
|
|
1199
|
+
// icon-only at all widths, which is the opposite of what #405 is for.
|
|
1200
|
+
//
|
|
1201
|
+
// So the nav keeps the elastic middle track and the two side tracks are
|
|
1202
|
+
// equalised instead, via a shared floor. Whenever the brand and the
|
|
1203
|
+
// utility cluster both fit inside it (the common case), the side tracks
|
|
1204
|
+
// are the same width and the menubar is centred in the bar, not merely
|
|
1205
|
+
// centred in its own track. The floor is deliberately small so it never
|
|
1206
|
+
// steals the width the label tier needs at 1280px.
|
|
1140
1207
|
#navbar .navbar-main {
|
|
1141
1208
|
display: grid !important;
|
|
1142
|
-
grid-template-columns:
|
|
1209
|
+
grid-template-columns:
|
|
1210
|
+
minmax(var(--zer0-navbar-side-min, 9rem), auto)
|
|
1211
|
+
minmax(0, 1fr)
|
|
1212
|
+
minmax(var(--zer0-navbar-side-min, 9rem), auto);
|
|
1143
1213
|
align-items: center;
|
|
1144
1214
|
column-gap: 0.75rem;
|
|
1145
1215
|
row-gap: 0.25rem;
|
|
@@ -1208,8 +1278,7 @@
|
|
|
1208
1278
|
display: none;
|
|
1209
1279
|
}
|
|
1210
1280
|
|
|
1211
|
-
#navbar .site-title
|
|
1212
|
-
#navbar .site-subtitle {
|
|
1281
|
+
#navbar .site-title {
|
|
1213
1282
|
flex-shrink: 1;
|
|
1214
1283
|
min-width: 0;
|
|
1215
1284
|
}
|
|
@@ -1227,15 +1296,6 @@
|
|
|
1227
1296
|
max-width: none;
|
|
1228
1297
|
}
|
|
1229
1298
|
|
|
1230
|
-
#navbar .site-subtitle .site-subtitle-text {
|
|
1231
|
-
overflow: hidden;
|
|
1232
|
-
text-overflow: ellipsis;
|
|
1233
|
-
white-space: nowrap;
|
|
1234
|
-
display: inline-block;
|
|
1235
|
-
vertical-align: bottom;
|
|
1236
|
-
max-width: none;
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
1299
|
// Progressive brand degradation (container width, not viewport).
|
|
1240
1300
|
// The home-link icons add ~108px to the brand cluster, so they only appear
|
|
1241
1301
|
// once the bar is wide enough to carry them AND the full menubar
|
|
@@ -1247,12 +1307,6 @@
|
|
|
1247
1307
|
}
|
|
1248
1308
|
}
|
|
1249
1309
|
|
|
1250
|
-
@container navbar-main (max-width: 68rem) {
|
|
1251
|
-
#navbar .site-subtitle {
|
|
1252
|
-
display: none !important;
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
1310
|
@container navbar-main (max-width: 50rem) {
|
|
1257
1311
|
#navbar .site-title .site-title-text {
|
|
1258
1312
|
max-width: 100%;
|
|
@@ -1318,6 +1372,19 @@
|
|
|
1318
1372
|
height: 2.5rem;
|
|
1319
1373
|
}
|
|
1320
1374
|
|
|
1375
|
+
// The square above is for BARE-GLYPH togglers. The labelled menu toggle
|
|
1376
|
+
// (#405) also carries a "Menu" span, and 2.5rem cannot hold glyph + gap +
|
|
1377
|
+
// label — the text would escape the button box and push the page past the
|
|
1378
|
+
// viewport at every width below lg, which is exactly what makes a
|
|
1379
|
+
// fixed-top bar read as "cut off". `.navbar-toggler-labeled`'s own
|
|
1380
|
+
// `width: auto` loses to the ID selector above on specificity (0,2,0 vs
|
|
1381
|
+
// 1,1,0); re-assert it here, where it outranks.
|
|
1382
|
+
#navbar .navbar-toggler.navbar-toggler-labeled {
|
|
1383
|
+
width: auto;
|
|
1384
|
+
min-width: 2.5rem;
|
|
1385
|
+
flex-shrink: 0;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1321
1388
|
#navbar .nav-search-button {
|
|
1322
1389
|
width: 2.5rem;
|
|
1323
1390
|
height: 2.5rem;
|
|
@@ -20,20 +20,8 @@
|
|
|
20
20
|
|
|
21
21
|
// MOVED → _sass/layouts/_navbar-extras.scss (token-aware FAB stacking)
|
|
22
22
|
// MOVED → _sass/components/_cookie-banner.scss (token-aware, dark-mode-safe)
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
.cookie-consent-banner {
|
|
26
|
-
@media (max-width: 768px) {
|
|
27
|
-
.btn {
|
|
28
|
-
width: 100%;
|
|
29
|
-
margin-bottom: 0.5rem;
|
|
30
|
-
|
|
31
|
-
&:last-child {
|
|
32
|
-
margin-bottom: 0;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
23
|
+
// The banner's internal layout (formerly a full-width stacked-button shim
|
|
24
|
+
// here) lives with the shell in _sass/components/_cookie-banner.scss.
|
|
37
25
|
|
|
38
26
|
// Active TOC link highlighting
|
|
39
27
|
.bd-toc nav a.active {
|
data/_sass/core/code-copy.scss
CHANGED
|
@@ -357,14 +357,33 @@ pre.highlight > button:focus {
|
|
|
357
357
|
opacity: 1;
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
-
.
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
360
|
+
// Extra actions inside a code block — SCOPED TO THIS COMPONENT (#412).
|
|
361
|
+
//
|
|
362
|
+
// This block used to read `.button, button:not(.copy)` with no scope at all.
|
|
363
|
+
// An element selector in a component partial is a site-wide rule: it restyled
|
|
364
|
+
// EVERY button on a consumer site that was not the theme's own copy button —
|
|
365
|
+
// `padding: 0 20px`, `font-size: 11px`, and a hardcoded `#bbb` border that
|
|
366
|
+
// bypassed the design tokens entirely. At specificity (0,1,1) it also outranked
|
|
367
|
+
// `.btn` and any consumer's own `.my-button`, so escaping it meant a
|
|
368
|
+
// specificity fight rather than a cascade (it-journey#634 had to scope to
|
|
369
|
+
// `.code-block-header .itj-clip-btn` to win). A component partial styles its
|
|
370
|
+
// own component and nothing else.
|
|
371
|
+
//
|
|
372
|
+
// The rule is kept, not deleted: a consumer that adds an action to the header
|
|
373
|
+
// (see the `zer0:code-block-ready` contract in assets/js/code-copy.js) should
|
|
374
|
+
// inherit the copy button's shape instead of arriving unstyled. Values are
|
|
375
|
+
// tokens now, so a skin moves them.
|
|
376
|
+
.code-block-header,
|
|
377
|
+
pre.highlight {
|
|
378
|
+
> .button,
|
|
379
|
+
> button:not(.copy) {
|
|
380
|
+
display: inline-block;
|
|
381
|
+
padding: 0 var(--zer0-space-3);
|
|
382
|
+
border-radius: var(--zer0-radius-sm);
|
|
383
|
+
border: 1px solid var(--zer0-color-border);
|
|
384
|
+
font-size: var(--zer0-text-sm);
|
|
385
|
+
cursor: pointer;
|
|
386
|
+
}
|
|
368
387
|
}
|
|
369
388
|
|
|
370
389
|
// Non-highlight fenced code blocks
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
extension-points-demo.css — a live consumer of the per-page `styles:` hook
|
|
3
|
+
==========================================================================
|
|
4
|
+
|
|
5
|
+
Loaded ONLY by /docs/customization/extension-points/, through that page's
|
|
6
|
+
`styles:` frontmatter. Its job is to prove the hook works and to mark the
|
|
7
|
+
demo action button so the page's own prose can point at it.
|
|
8
|
+
|
|
9
|
+
Note what it does NOT have to do: fight a specificity war. Before #412,
|
|
10
|
+
`_sass/core/code-copy.scss` shipped a bare `button:not(.copy)` rule at
|
|
11
|
+
specificity (0,1,1), which outranked any single class a consumer could
|
|
12
|
+
write — so downstream code had to scope to `.code-block-header .my-btn`
|
|
13
|
+
just to set its own padding. That rule is scoped to the component now, and
|
|
14
|
+
a plain class is enough again.
|
|
15
|
+
========================================================================== */
|
|
16
|
+
|
|
17
|
+
.zer0-demo-action {
|
|
18
|
+
background: transparent;
|
|
19
|
+
color: var(--zer0-color-text-muted);
|
|
20
|
+
font-family: var(--zer0-font-mono);
|
|
21
|
+
line-height: 1.8;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.zer0-demo-action:hover,
|
|
25
|
+
.zer0-demo-action:focus-visible {
|
|
26
|
+
color: inherit;
|
|
27
|
+
}
|
data/assets/data/wiki-index.json
CHANGED
|
@@ -142,11 +142,30 @@ sitemap: false
|
|
|
142
142
|
{%- endif -%}
|
|
143
143
|
{%- endfor -%}
|
|
144
144
|
{%- assign outgoing = outgoing | uniq -%}
|
|
145
|
+
{%- comment -%}
|
|
146
|
+
`lastmod` and `description` (#412) — both already on `doc` in this loop,
|
|
147
|
+
so neither costs a new traversal. A notes/library UI needs them:
|
|
148
|
+
`lastmod` to sort by recency or show staleness, and `description`
|
|
149
|
+
because `excerpt` is body-derived while `description` is the AUTHORED
|
|
150
|
+
subtitle, which is the better card line whenever it exists.
|
|
151
|
+
|
|
152
|
+
Source order for the date follows the rest of the theme
|
|
153
|
+
(_includes/core/head.html, _layouts/article.html): `last_modified_at`,
|
|
154
|
+
then `lastmod`, then the document's own `date`. Emitted as ISO-8601 via
|
|
155
|
+
`date_to_xmlschema` so a consumer can sort strings without parsing.
|
|
156
|
+
Both keys are always present and always defined — `null` when the
|
|
157
|
+
document carries neither — rather than being conditionally omitted,
|
|
158
|
+
because a key that sometimes disappears is a worse contract than one
|
|
159
|
+
that is sometimes null.
|
|
160
|
+
{%- endcomment -%}
|
|
161
|
+
{%- assign doc_lastmod = doc.last_modified_at | default: doc.lastmod | default: doc.date -%}
|
|
145
162
|
{
|
|
146
163
|
"title": {{ doc.title | default: basename | jsonify }},
|
|
147
164
|
"basename": {{ basename | jsonify }},
|
|
148
165
|
"url": {{ doc.url | jsonify }},
|
|
149
166
|
"collection": {{ doc.collection | default: nil | jsonify }},
|
|
167
|
+
"lastmod": {% if doc_lastmod %}{{ doc_lastmod | date_to_xmlschema | jsonify }}{% else %}null{% endif %},
|
|
168
|
+
"description": {{ doc.description | default: nil | jsonify }},
|
|
150
169
|
"tags": {{ doc.tags | default: empty | jsonify }},
|
|
151
170
|
"categories": {{ doc.categories | default: empty | jsonify }},
|
|
152
171
|
"aliases": {{ aliases | jsonify }},
|
data/assets/js/auto-hide-nav.js
CHANGED
|
@@ -34,21 +34,61 @@
|
|
|
34
34
|
// The measured height is also published as --zer0-header-height so
|
|
35
35
|
// stylesheets (theme or consumer) can align overlays with the real
|
|
36
36
|
// header instead of hard-coding its pixel height.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
//
|
|
38
|
+
// Layout is never read synchronously here. Reading navbar.offsetHeight
|
|
39
|
+
// at DOMContentLoaded (and again on every resize) forced a style +
|
|
40
|
+
// layout pass while stylesheets were still settling (Lighthouse
|
|
41
|
+
// "forced reflow", ~233ms per read on a throttled phone). Instead:
|
|
42
|
+
// - ResizeObserver delivers the navbar's border-box height after the
|
|
43
|
+
// browser's own layout pass, before paint, so the padding still
|
|
44
|
+
// lands in the first frame;
|
|
45
|
+
// - without ResizeObserver, the read happens in a requestAnimationFrame
|
|
46
|
+
// callback, and the write in the frame after it, so a read never
|
|
47
|
+
// follows a write in the same pass;
|
|
48
|
+
// - the last height is cached and nothing is written when it is
|
|
49
|
+
// unchanged.
|
|
50
|
+
let lastNavbarHeight = -1;
|
|
51
|
+
|
|
52
|
+
function applyNavbarHeight(height) {
|
|
53
|
+
height = Math.round(height);
|
|
54
|
+
if (height === lastNavbarHeight) return;
|
|
55
|
+
lastNavbarHeight = height;
|
|
56
|
+
document.body.style.paddingTop = height + 'px';
|
|
57
|
+
document.documentElement.style.setProperty('--zer0-header-height', height + 'px');
|
|
41
58
|
}
|
|
42
59
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
60
|
+
if (typeof window.ResizeObserver === 'function') {
|
|
61
|
+
const navbarObserver = new ResizeObserver(function(entries) {
|
|
62
|
+
const entry = entries[entries.length - 1];
|
|
63
|
+
const box = entry.borderBoxSize && (entry.borderBoxSize[0] || entry.borderBoxSize);
|
|
64
|
+
// Layout is already clean inside a ResizeObserver callback, so
|
|
65
|
+
// the offsetHeight fallback (old engines without borderBoxSize)
|
|
66
|
+
// does not force a reflow.
|
|
67
|
+
applyNavbarHeight(box && box.blockSize ? box.blockSize : entry.target.offsetHeight);
|
|
68
|
+
});
|
|
69
|
+
navbarObserver.observe(navbar);
|
|
70
|
+
} else {
|
|
71
|
+
let measureQueued = false;
|
|
72
|
+
const measureNavbar = function() {
|
|
73
|
+
if (measureQueued) return;
|
|
74
|
+
measureQueued = true;
|
|
75
|
+
window.requestAnimationFrame(function() {
|
|
76
|
+
const height = navbar.offsetHeight; // read
|
|
77
|
+
window.requestAnimationFrame(function() {
|
|
78
|
+
measureQueued = false;
|
|
79
|
+
applyNavbarHeight(height); // write, next frame
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
measureNavbar();
|
|
84
|
+
|
|
85
|
+
// Update padding on window resize with debounce
|
|
86
|
+
let resizeTimeout;
|
|
87
|
+
window.addEventListener('resize', function() {
|
|
88
|
+
clearTimeout(resizeTimeout);
|
|
89
|
+
resizeTimeout = setTimeout(measureNavbar, 150);
|
|
90
|
+
}, { passive: true });
|
|
91
|
+
}
|
|
52
92
|
|
|
53
93
|
// Enhanced scroll handler with better logic
|
|
54
94
|
function handleScroll() {
|
data/assets/js/code-copy.js
CHANGED
|
@@ -1,4 +1,53 @@
|
|
|
1
1
|
// Feature: ZER0-030
|
|
2
|
+
//
|
|
3
|
+
// CONSUMER CONTRACT — code-block extension points (#412).
|
|
4
|
+
// ------------------------------------------------------
|
|
5
|
+
// `.code-block-header` is built HERE, at runtime, not emitted at build time, so
|
|
6
|
+
// a consumer adding a second action to code blocks has nothing in the markup to
|
|
7
|
+
// hook. Before this contract existed the only route was a `MutationObserver`
|
|
8
|
+
// over `#main-content` plus a guess at the wrapper's shape — and wrapper
|
|
9
|
+
// nesting is exactly what a theme refactor changes, so the guess was one
|
|
10
|
+
// release away from breaking. it-journey#634 got it wrong and silently rendered
|
|
11
|
+
// two buttons on every block (26 across 13) until it added a claim marker.
|
|
12
|
+
//
|
|
13
|
+
// Two ways in, both stable:
|
|
14
|
+
//
|
|
15
|
+
// document.addEventListener('zer0:code-block-ready', function (e) {
|
|
16
|
+
// e.detail; // { wrapper, header, pre, code, lang }
|
|
17
|
+
// });
|
|
18
|
+
//
|
|
19
|
+
// window.zer0OnCodeBlock(function (detail) { … }); // same shape, REPLAYED
|
|
20
|
+
//
|
|
21
|
+
// The event bubbles from the wrapper, fires exactly once per code block, and is
|
|
22
|
+
// dispatched after the block is fully decorated — line numbers, a11y attributes
|
|
23
|
+
// and the copy button are all in place before a consumer sees it. `header` is
|
|
24
|
+
// null for a standalone `<pre>` outside a Rouge wrapper, which has no header
|
|
25
|
+
// row; `lang` is null when the block declares no language.
|
|
26
|
+
//
|
|
27
|
+
// Because the sweep runs on `DOMContentLoaded`, a listener registered later
|
|
28
|
+
// would miss every block — so `window.__zer0CodeBlocks` holds the detail of
|
|
29
|
+
// every block already processed, and `zer0OnCodeBlock` replays that array
|
|
30
|
+
// before subscribing. Late registration is the NORMAL case for a deferred
|
|
31
|
+
// consumer script, not an edge case.
|
|
32
|
+
//
|
|
33
|
+
// Docs: docs/development/EXTENSION-POINTS.md
|
|
34
|
+
window.__zer0CodeBlocks = window.__zer0CodeBlocks || [];
|
|
35
|
+
|
|
36
|
+
window.zer0OnCodeBlock = function (listener) {
|
|
37
|
+
if (typeof listener !== 'function') return function () {};
|
|
38
|
+
window.__zer0CodeBlocks.forEach(function (detail) {
|
|
39
|
+
listener(detail);
|
|
40
|
+
});
|
|
41
|
+
var handler = function (event) {
|
|
42
|
+
listener(event.detail);
|
|
43
|
+
};
|
|
44
|
+
document.addEventListener('zer0:code-block-ready', handler);
|
|
45
|
+
// Returned so a consumer can stop listening; harmless to ignore.
|
|
46
|
+
return function () {
|
|
47
|
+
document.removeEventListener('zer0:code-block-ready', handler);
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
2
51
|
document.addEventListener('DOMContentLoaded', function () {
|
|
3
52
|
var LANG_LABELS = {
|
|
4
53
|
shell: 'bash',
|
|
@@ -157,12 +206,18 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
157
206
|
});
|
|
158
207
|
});
|
|
159
208
|
|
|
209
|
+
var wrapper;
|
|
210
|
+
var header = null;
|
|
160
211
|
if (rougeWrapper) {
|
|
161
|
-
|
|
212
|
+
wrapper = rougeWrapper;
|
|
213
|
+
header = ensureHeader(rougeWrapper, lang);
|
|
162
214
|
header.appendChild(button);
|
|
163
215
|
rougeWrapper.classList.toggle('code-block--single-line', isSingleLine);
|
|
164
216
|
rougeWrapper.closest('.highlighter-rouge').classList.add('has-code-header');
|
|
165
217
|
} else {
|
|
218
|
+
// A standalone <pre> is its own wrapper and has no header row: the copy
|
|
219
|
+
// button is positioned against the <pre> itself.
|
|
220
|
+
wrapper = preElement;
|
|
166
221
|
if (getComputedStyle(preElement).position === 'static') {
|
|
167
222
|
preElement.style.position = 'relative';
|
|
168
223
|
}
|
|
@@ -171,5 +226,21 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
171
226
|
}
|
|
172
227
|
|
|
173
228
|
preElement.classList.add('has-copy-button');
|
|
229
|
+
|
|
230
|
+
// Published LAST, so a consumer never sees a half-decorated block. Exactly
|
|
231
|
+
// once per block: `preElements` is a Set, and the `.copy` guard above makes
|
|
232
|
+
// a second sweep over the same block a no-op. See the contract at the top.
|
|
233
|
+
var detail = {
|
|
234
|
+
wrapper: wrapper,
|
|
235
|
+
header: header,
|
|
236
|
+
pre: preElement,
|
|
237
|
+
code: codeElement,
|
|
238
|
+
lang: lang
|
|
239
|
+
};
|
|
240
|
+
window.__zer0CodeBlocks.push(detail);
|
|
241
|
+
wrapper.dispatchEvent(new CustomEvent('zer0:code-block-ready', {
|
|
242
|
+
bubbles: true,
|
|
243
|
+
detail: detail
|
|
244
|
+
}));
|
|
174
245
|
});
|
|
175
246
|
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// extension-points-demo.js — a live consumer of the theme's extension points
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Loaded ONLY by /docs/customization/extension-points/, through that page's
|
|
6
|
+
// `scripts:` frontmatter — which makes this file a demonstration of two
|
|
7
|
+
// contracts at once: the per-page script hook that loads it, and the
|
|
8
|
+
// `zer0:code-block-ready` event it subscribes to.
|
|
9
|
+
//
|
|
10
|
+
// It is deliberately the whole pattern in a dozen lines. The point of #412 is
|
|
11
|
+
// that a consumer adding an action to a code block should not need a
|
|
12
|
+
// MutationObserver, a timeout fallback, or a guess at how deeply the theme
|
|
13
|
+
// nests its wrappers — all three of which it-journey#634 had to carry, and the
|
|
14
|
+
// wrapper guess is what silently produced two buttons per block.
|
|
15
|
+
//
|
|
16
|
+
// Copy this shape. `zer0OnCodeBlock` replays the blocks that were decorated
|
|
17
|
+
// before this script ran, then subscribes for any that follow, so it does not
|
|
18
|
+
// matter whether you load early or late.
|
|
19
|
+
// =============================================================================
|
|
20
|
+
(function () {
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
if (typeof window.zer0OnCodeBlock !== 'function') return;
|
|
24
|
+
|
|
25
|
+
window.zer0OnCodeBlock(function (detail) {
|
|
26
|
+
// A standalone <pre> outside a Rouge wrapper has no header row to hang an
|
|
27
|
+
// action on. `header` is null there, and that is the documented signal.
|
|
28
|
+
if (!detail.header) return;
|
|
29
|
+
if (detail.header.querySelector('.zer0-demo-action')) return;
|
|
30
|
+
|
|
31
|
+
var lines = (detail.code.textContent || '').replace(/\n$/, '').split('\n').length;
|
|
32
|
+
|
|
33
|
+
var button = document.createElement('button');
|
|
34
|
+
button.type = 'button';
|
|
35
|
+
button.className = 'zer0-demo-action';
|
|
36
|
+
button.textContent = lines + (lines === 1 ? ' line' : ' lines');
|
|
37
|
+
button.setAttribute(
|
|
38
|
+
'aria-label',
|
|
39
|
+
'This ' + (detail.lang || 'code') + ' block has ' + lines + ' lines'
|
|
40
|
+
);
|
|
41
|
+
button.addEventListener('click', function () {
|
|
42
|
+
detail.pre.focus();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Before the copy button, so Copy keeps its place at the end of the row.
|
|
46
|
+
detail.header.insertBefore(button, detail.header.querySelector('.copy'));
|
|
47
|
+
});
|
|
48
|
+
})();
|