jekyll-theme-zer0 1.28.0 → 1.30.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 +1143 -13
- data/_data/README.md +2 -0
- data/_data/ai.yml +5 -3
- data/_data/ai_pricing.yml +36 -0
- data/_data/backlog.yml +507 -2
- data/_data/consumers.yml +157 -9
- data/_data/features.yml +303 -20
- data/_data/feedback_types.yml +17 -12
- data/_data/i18n/fr.yml +12 -7
- data/_data/i18n/manifest.yml +39 -12
- data/_data/ingredient_densities.yml +122 -0
- data/_data/landing.yml +5 -2
- data/_data/navigation/main.yml +16 -0
- data/_data/navigation/quickstart.yml +4 -0
- data/_data/recipe_courses.yml +64 -0
- data/_data/site_builder.yml +874 -0
- data/_data/theme-manifest.yml +160 -124
- data/_data/ui-text.yml +26 -0
- data/_includes/README.md +26 -2
- data/_includes/analytics/posthog.html +2 -2
- data/_includes/components/admin-links.html +2 -2
- data/_includes/components/admin-tabs.html +2 -2
- data/_includes/components/ai-chat.html +14 -11
- data/_includes/components/analytics-dashboard.html +8 -8
- data/_includes/components/author-bio.html +1 -1
- data/_includes/components/author-card.html +10 -2
- data/_includes/components/author-eeat.html +4 -4
- data/_includes/components/background-customizer.html +10 -10
- data/_includes/components/background-image.html +114 -0
- data/_includes/components/background-settings.html +28 -15
- data/_includes/components/collection-manager.html +5 -5
- data/_includes/components/component-showcase.html +13 -13
- data/_includes/components/config-editor.html +12 -12
- data/_includes/components/config-viewer.html +8 -8
- data/_includes/components/cookie-consent.html +15 -15
- data/_includes/components/cta-button.html +7 -2
- data/_includes/components/dev-shortcuts.html +7 -7
- data/_includes/components/env-dashboard.html +8 -8
- data/_includes/components/env-switcher.html +9 -9
- data/_includes/components/feature-card.html +2 -2
- data/_includes/components/halfmoon.html +2 -2
- data/_includes/components/info-section.html +42 -37
- data/_includes/components/js-cdn.html +15 -15
- data/_includes/components/language-toggle.html +168 -21
- data/_includes/components/mermaid.html +72 -435
- data/_includes/components/nanobar.html +5 -5
- data/_includes/components/nav-editor.html +2 -2
- data/_includes/components/nav-export.html +2 -2
- data/_includes/components/nav-overview.html +2 -2
- data/_includes/components/page-feedback.html +45 -30
- data/_includes/components/page-views-init.html +55 -0
- data/_includes/components/page-views.html +33 -0
- data/_includes/components/post-card.html +22 -22
- data/_includes/components/post-type-badge.html +2 -2
- data/_includes/components/powered-by.html +2 -2
- data/_includes/components/preview-image.html +6 -0
- data/_includes/components/quick-index.html +2 -2
- data/_includes/components/recipe-card.html +67 -0
- data/_includes/components/recipe-duration.html +50 -0
- data/_includes/components/recipe-grams.html +58 -0
- data/_includes/components/recipe-index.html +96 -0
- data/_includes/components/recipe-ingredients.html +90 -0
- data/_includes/components/recipe-meta.html +96 -0
- data/_includes/components/recipe-nutrition.html +57 -0
- data/_includes/components/recipe-qty.html +73 -0
- data/_includes/components/recipe-ratio.html +151 -0
- data/_includes/components/recipe-scaler.html +73 -0
- data/_includes/components/recipe-steps.html +86 -0
- data/_includes/components/recipe-temp.html +45 -0
- data/_includes/components/search-modal.html +29 -4
- data/_includes/components/searchbar.html +2 -2
- data/_includes/components/shortcuts-modal.html +3 -0
- data/_includes/components/svg-background.html +2 -2
- data/_includes/components/theme-customizer.html +2 -2
- data/_includes/components/theme-info.html +14 -7
- data/_includes/components/theme-preview-gallery.html +22 -22
- data/_includes/content/giscus.html +2 -2
- data/_includes/content/intro.html +8 -8
- data/_includes/content/jsonld-faq.html +2 -2
- data/_includes/content/jsonld-software.html +24 -5
- data/_includes/content/seo.html +4 -4
- data/_includes/content/sitemap.html +27 -27
- data/_includes/content/toc.html +183 -183
- data/_includes/core/branding.html +6 -6
- data/_includes/core/console-capture.html +32 -74
- data/_includes/core/favicon.html +49 -7
- data/_includes/core/footer-fabs.html +17 -3
- data/_includes/core/footer.html +49 -34
- data/_includes/core/head.html +110 -86
- data/_includes/core/header.html +76 -54
- data/_includes/docs/bootstrap-docs.html +8 -8
- data/_includes/landing/landing-install-cards.html +2 -2
- data/_includes/landing/landing-quick-links.html +1 -1
- data/_includes/navigation/admin-nav.html +2 -2
- data/_includes/navigation/nav-tree.html +8 -8
- data/_includes/navigation/navbar.html +12 -12
- data/_includes/navigation/section-sidebar.html +109 -27
- data/_includes/navigation/sidebar-config.html +36 -2
- data/_includes/navigation/sidebar-left.html +17 -16
- data/_includes/navigation/sidebar-right.html +8 -7
- data/_includes/obsidian/full-graph.html +2 -2
- data/_includes/setup/claude-session.html +72 -0
- data/_includes/setup/prereq-checklist.html +90 -0
- data/_includes/setup/wizard.html +924 -222
- data/_includes/stats/stats-categories.html +8 -8
- data/_includes/stats/stats-header.html +14 -14
- data/_includes/stats/stats-metrics.html +14 -14
- data/_includes/stats/stats-no-data.html +12 -12
- data/_includes/stats/stats-overview.html +6 -6
- data/_includes/stats/stats-tags.html +8 -8
- data/_layouts/404.html +38 -24
- data/_layouts/README.md +2 -0
- data/_layouts/admin.html +24 -24
- data/_layouts/article.html +43 -33
- data/_layouts/author.html +20 -20
- data/_layouts/authors.html +2 -2
- data/_layouts/book-abc.html +12 -12
- data/_layouts/book-story.html +15 -15
- data/_layouts/book.html +12 -12
- data/_layouts/collection.html +33 -33
- data/_layouts/cookbook.html +88 -0
- data/_layouts/default.html +31 -28
- data/_layouts/home.html +23 -23
- data/_layouts/index.html +10 -10
- data/_layouts/landing.html +17 -17
- data/_layouts/news.html +44 -44
- data/_layouts/note.html +38 -38
- data/_layouts/notebook.html +34 -34
- data/_layouts/recipe.html +274 -0
- data/_layouts/root.html +92 -55
- data/_layouts/section.html +62 -33
- data/_layouts/setup.html +3 -3
- data/_layouts/sitemap-collection.html +49 -49
- data/_layouts/stats.html +40 -40
- data/_layouts/tag.html +12 -12
- data/_layouts/welcome.html +21 -21
- data/_sass/components/_callout.scss +1 -1
- data/_sass/components/_footer.scss +37 -1
- data/_sass/components/_mermaid.scss +375 -0
- data/_sass/components/_page-views.scss +36 -0
- data/_sass/components/_recipe.scss +506 -0
- data/_sass/components/_setup-wizard.scss +764 -0
- data/_sass/components/_ui-enhancements.scss +6 -6
- data/_sass/core/_navbar.scss +261 -46
- data/_sass/layouts/_landing.scss +2 -2
- data/_sass/layouts/_navbar-extras.scss +14 -4
- data/_sass/tokens/_color.scss +6 -0
- data/_sass/tokens/_index.scss +2 -0
- data/_sass/tokens/_radius.scss +21 -0
- data/_sass/tokens/_typography.scss +4 -0
- data/_sass/utilities/_focus.scss +14 -0
- data/assets/css/main.scss +4 -0
- data/assets/js/ai-chat.js +47 -5
- data/assets/js/fleet-feedback-capture.js +124 -0
- data/assets/js/fleet-feedback.js +853 -0
- data/assets/js/mermaid-diagrams.js +1267 -0
- data/assets/js/modules/navigation/config.js +9 -6
- data/assets/js/modules/navigation/navbar.js +55 -0
- data/assets/js/modules/navigation/scroll-spy.js +315 -80
- data/assets/js/modules/theme/appearance.js +8 -2
- data/assets/js/obsidian-wiki-links.js +8 -3
- data/assets/js/page-feedback.js +125 -192
- data/assets/js/page-views.js +372 -0
- data/assets/js/recipe-scaler.js +501 -0
- data/assets/js/search-modal.js +36 -0
- data/assets/js/setup-wizard.js +2279 -226
- data/assets/js/site-builder.js +1834 -0
- data/assets/js/ui-enhancements.js +11 -3
- data/scripts/README.md +44 -0
- data/scripts/ai/README.md +38 -0
- data/scripts/ai/api_call.rb +124 -0
- data/scripts/ai/usage.rb +314 -0
- data/scripts/ai/usage_report.rb +225 -0
- data/scripts/bin/audit-consumer +39 -7
- data/scripts/bin/giscus-discussions +213 -14
- data/scripts/bin/manifest +35 -12
- data/scripts/ci/agent_review_result.py +164 -0
- data/scripts/ci/test_agent_review_result.py +172 -0
- data/scripts/ci/test_visual_evidence_autogen.py +341 -0
- data/scripts/ci/visual_evidence_autogen.py +1060 -0
- data/scripts/content-review.rb +20 -1
- data/scripts/design-system-check.rb +170 -0
- data/scripts/lib/audit.sh +42 -2
- data/scripts/lint-liquid-raw.rb +137 -0
- data/scripts/test/integration/mermaid +22 -8
- data/scripts/test/lib/run_tests.sh +3 -1
- data/scripts/test/lib/test_agent_review_result.sh +27 -0
- data/scripts/test/lib/test_visual_evidence_autogen.sh +24 -0
- data/scripts/translate.rb +94 -16
- metadata +48 -2
|
@@ -51,12 +51,15 @@ export const config = {
|
|
|
51
51
|
// SCROLL SPY SETTINGS
|
|
52
52
|
// ===================================================================
|
|
53
53
|
scrollSpy: {
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
// Reading line: distance (px) below the top of the viewport at which a
|
|
55
|
+
// heading becomes the "current" one. `null` derives it from the
|
|
56
|
+
// document's `scroll-padding-top` so the highlight matches where
|
|
57
|
+
// anchor navigation parks a heading; set a number to pin it.
|
|
58
|
+
offset: null,
|
|
59
|
+
|
|
60
|
+
// Slack (px) when comparing a heading against the reading line and
|
|
61
|
+
// when detecting the bottom of the page.
|
|
62
|
+
tolerance: 4
|
|
60
63
|
},
|
|
61
64
|
|
|
62
65
|
// ===================================================================
|
|
@@ -55,6 +55,7 @@ export class Navbar {
|
|
|
55
55
|
this._setupMobileDropdowns();
|
|
56
56
|
this._setupOutsideClickClose();
|
|
57
57
|
this._setupOffcanvasReset();
|
|
58
|
+
this._setupSettingsExclusivity();
|
|
58
59
|
this._setupNavTooltips();
|
|
59
60
|
this._setupDropdownHoverDelay();
|
|
60
61
|
this._setupFocusTrap();
|
|
@@ -224,6 +225,60 @@ export class Navbar {
|
|
|
224
225
|
});
|
|
225
226
|
}
|
|
226
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Mobile: opening Settings from inside a nav panel must not leave the two
|
|
230
|
+
* stacked on top of each other.
|
|
231
|
+
*
|
|
232
|
+
* #bdNavbar (and the optional unified drawer) and #info-section are both
|
|
233
|
+
* `offcanvas-end` at Bootstrap's $zindex-offcanvas. #bdNavbar additionally
|
|
234
|
+
* lives inside the fixed header, which _navbar.scss lifts above the
|
|
235
|
+
* offcanvas backdrop while the menu is open — so a tap on the menu's own
|
|
236
|
+
* "Settings" item used to open Settings *behind* the still-open menu, with
|
|
237
|
+
* nothing on screen to show it had worked.
|
|
238
|
+
*
|
|
239
|
+
* Intercept the Bootstrap `show` event (capture phase, before Bootstrap's
|
|
240
|
+
* own handler runs) and defer it until the host panel has finished sliding
|
|
241
|
+
* out. Doing it on the event rather than on the trigger keeps the plain
|
|
242
|
+
* data-api working for every other entry point — footer, keyboard, a
|
|
243
|
+
* consumer's own button — and leaves the markup functional if this module
|
|
244
|
+
* never loads.
|
|
245
|
+
*/
|
|
246
|
+
_setupSettingsExclusivity() {
|
|
247
|
+
const settingsEl = document.getElementById('info-section');
|
|
248
|
+
if (!settingsEl) return;
|
|
249
|
+
|
|
250
|
+
const hostEls = ['bdNavbar', 'zer0UnifiedDrawer']
|
|
251
|
+
.map((id) => document.getElementById(id))
|
|
252
|
+
.filter(Boolean);
|
|
253
|
+
if (!hostEls.length) return;
|
|
254
|
+
|
|
255
|
+
this._on(
|
|
256
|
+
settingsEl,
|
|
257
|
+
'show.bs.offcanvas',
|
|
258
|
+
(event) => {
|
|
259
|
+
const Offcanvas = window.bootstrap?.Offcanvas;
|
|
260
|
+
if (!Offcanvas) return;
|
|
261
|
+
const openHost = hostEls.find((el) => el.classList.contains('show'));
|
|
262
|
+
if (!openHost) return;
|
|
263
|
+
|
|
264
|
+
event.preventDefault();
|
|
265
|
+
event.stopImmediatePropagation();
|
|
266
|
+
|
|
267
|
+
const hostInstance = Offcanvas.getInstance(openHost);
|
|
268
|
+
const show = () => Offcanvas.getOrCreateInstance(settingsEl).show();
|
|
269
|
+
if (!hostInstance) {
|
|
270
|
+
show();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
// `hidden.bs.offcanvas` fires after the slide-out transition, so
|
|
274
|
+
// the header's z-index lift has already been released by then.
|
|
275
|
+
openHost.addEventListener('hidden.bs.offcanvas', show, { once: true });
|
|
276
|
+
hostInstance.hide();
|
|
277
|
+
},
|
|
278
|
+
true,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
227
282
|
_setupNavTooltips() {
|
|
228
283
|
if (typeof window.bootstrap === 'undefined' || !window.bootstrap.Tooltip) return;
|
|
229
284
|
const navLinks = document.querySelectorAll('#bdNavbar .nav-link[title]');
|
|
@@ -1,27 +1,52 @@
|
|
|
1
1
|
// Feature: ZER0-008
|
|
2
2
|
/**
|
|
3
3
|
* ===================================================================
|
|
4
|
-
* SCROLL SPY -
|
|
4
|
+
* SCROLL SPY - Reading-position TOC highlighting
|
|
5
5
|
* ===================================================================
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* File: scroll-spy.js
|
|
8
8
|
* Path: assets/js/modules/navigation/scroll-spy.js
|
|
9
|
-
* Purpose: Track
|
|
10
|
-
*
|
|
9
|
+
* Purpose: Track the section being read and bold its TOC link
|
|
10
|
+
*
|
|
11
|
+
* How the active heading is chosen:
|
|
12
|
+
* The active heading is the LAST heading whose top has crossed the
|
|
13
|
+
* "reading line" - a horizontal line `offset` pixels below the top of the
|
|
14
|
+
* viewport, matching the `scroll-padding-top` that anchor navigation uses.
|
|
15
|
+
* Once the page is scrolled to the bottom, the last heading wins (trailing
|
|
16
|
+
* sections can be shorter than the viewport and would never reach the
|
|
17
|
+
* line). The answer is recomputed from scratch on every scroll frame, so
|
|
18
|
+
* the highlight cannot drift out of sync with the page.
|
|
19
|
+
*
|
|
20
|
+
* The previous implementation asked IntersectionObserver for the "most
|
|
21
|
+
* visible" heading. Headings are only a few pixels tall, so every heading
|
|
22
|
+
* inside the observer band reported the same intersectionRatio and the
|
|
23
|
+
* winner was whichever entry happened to be in that callback's batch -
|
|
24
|
+
* headings leaving the band never triggered a re-evaluation at all. That
|
|
25
|
+
* is what made the bolding jump around and stick on the wrong entry.
|
|
26
|
+
*
|
|
11
27
|
* Features:
|
|
12
|
-
* -
|
|
13
|
-
* -
|
|
14
|
-
*
|
|
15
|
-
*
|
|
28
|
+
* - Deterministic, position-based active section (no ratio guessing)
|
|
29
|
+
* - rAF-throttled passive scroll listener; heading offsets are cached and
|
|
30
|
+
* re-measured on resize / content reflow
|
|
31
|
+
* - Holds the clicked link active while a smooth scroll animates, so
|
|
32
|
+
* intermediate headings don't flash
|
|
33
|
+
* - Keeps the active link visible inside the TOC without ever scrolling
|
|
34
|
+
* the page itself
|
|
35
|
+
*
|
|
16
36
|
* Usage:
|
|
17
37
|
* import { ScrollSpy } from './scroll-spy.js';
|
|
18
38
|
* const scrollSpy = new ScrollSpy();
|
|
19
|
-
*
|
|
39
|
+
*
|
|
20
40
|
* ===================================================================
|
|
21
41
|
*/
|
|
22
42
|
|
|
23
43
|
import { config } from './config.js';
|
|
24
44
|
|
|
45
|
+
const ACTIVE_CLASS = 'active';
|
|
46
|
+
|
|
47
|
+
/** How long (ms) to keep a clicked TOC link active while the page animates. */
|
|
48
|
+
const CLICK_GUARD_MS = 1200;
|
|
49
|
+
|
|
25
50
|
/**
|
|
26
51
|
* Get element safely with error handling
|
|
27
52
|
* @param {string} selector - CSS selector
|
|
@@ -50,90 +75,243 @@ function getElements(selector) {
|
|
|
50
75
|
}
|
|
51
76
|
}
|
|
52
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Current vertical scroll position, normalized across browsers.
|
|
80
|
+
* @returns {number}
|
|
81
|
+
*/
|
|
82
|
+
function scrollTop() {
|
|
83
|
+
return window.scrollY !== undefined ? window.scrollY : window.pageYOffset;
|
|
84
|
+
}
|
|
85
|
+
|
|
53
86
|
export class ScrollSpy {
|
|
54
87
|
constructor() {
|
|
55
88
|
this.tocLinks = getElements(config.selectors.tocLinks);
|
|
56
89
|
this.headings = this._getHeadings();
|
|
57
90
|
this.currentActive = null;
|
|
58
|
-
|
|
59
|
-
|
|
91
|
+
|
|
92
|
+
// Cached measurements - invalidated (not recomputed) by resize and
|
|
93
|
+
// content reflow, then refreshed inside the next animation frame.
|
|
94
|
+
this._offsetPx = 0;
|
|
95
|
+
this._needsMeasure = true;
|
|
96
|
+
this._frame = null;
|
|
97
|
+
|
|
98
|
+
// Click guard: id of the heading a TOC click is scrolling towards.
|
|
99
|
+
this._pendingId = null;
|
|
100
|
+
this._pendingExpires = 0;
|
|
101
|
+
|
|
102
|
+
this._resizeObserver = null;
|
|
103
|
+
|
|
60
104
|
if (this.headings.length === 0 || this.tocLinks.length === 0) {
|
|
61
105
|
console.log('ScrollSpy: No TOC or headings found, skipping initialization');
|
|
62
106
|
return;
|
|
63
107
|
}
|
|
64
108
|
|
|
109
|
+
this._onScroll = () => this._requestUpdate();
|
|
110
|
+
this._onReflow = () => this._invalidate();
|
|
111
|
+
this._onNavigationScroll = event => this._handleNavigationScroll(event);
|
|
112
|
+
this._onUserScroll = () => this._cancelClickGuard();
|
|
113
|
+
|
|
65
114
|
this._init();
|
|
66
115
|
}
|
|
67
116
|
|
|
68
117
|
/**
|
|
69
|
-
* Get all headings that have corresponding TOC links
|
|
118
|
+
* Get all headings that have corresponding TOC links, in document order.
|
|
119
|
+
* Duplicate links to the same heading keep the first link.
|
|
70
120
|
* @private
|
|
71
|
-
* @returns {Array<{element: Element, link: Element, id: string}>}
|
|
121
|
+
* @returns {Array<{element: Element, link: Element, id: string, top: number}>}
|
|
72
122
|
*/
|
|
73
123
|
_getHeadings() {
|
|
74
124
|
const headings = [];
|
|
125
|
+
const seen = new Set();
|
|
126
|
+
|
|
75
127
|
this.tocLinks.forEach(link => {
|
|
76
128
|
const href = link.getAttribute('href');
|
|
77
|
-
if (href
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
129
|
+
if (!href || !href.startsWith('#') || href.length < 2) return;
|
|
130
|
+
|
|
131
|
+
const id = decodeURIComponent(href.substring(1));
|
|
132
|
+
if (seen.has(id)) return;
|
|
133
|
+
|
|
134
|
+
const heading = document.getElementById(id);
|
|
135
|
+
if (!heading) return;
|
|
136
|
+
|
|
137
|
+
seen.add(id);
|
|
138
|
+
headings.push({ element: heading, link: link, id: id, top: 0 });
|
|
88
139
|
});
|
|
140
|
+
|
|
89
141
|
return headings;
|
|
90
142
|
}
|
|
91
143
|
|
|
92
144
|
/**
|
|
93
|
-
*
|
|
145
|
+
* Attach listeners and paint the initial active link.
|
|
94
146
|
* @private
|
|
95
147
|
*/
|
|
96
148
|
_init() {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
149
|
+
window.addEventListener('scroll', this._onScroll, { passive: true });
|
|
150
|
+
window.addEventListener('resize', this._onReflow);
|
|
151
|
+
window.addEventListener('load', this._onReflow);
|
|
152
|
+
window.addEventListener('hashchange', this._onScroll);
|
|
153
|
+
|
|
154
|
+
// A TOC click starts an animated scroll; hold that link active until
|
|
155
|
+
// the page lands (see _handleNavigationScroll)...
|
|
156
|
+
document.addEventListener('navigation:scroll', this._onNavigationScroll);
|
|
157
|
+
|
|
158
|
+
// ...but a reader who grabs the wheel mid-animation has taken over —
|
|
159
|
+
// drop the hold and follow them immediately.
|
|
160
|
+
window.addEventListener('wheel', this._onUserScroll, { passive: true });
|
|
161
|
+
window.addEventListener('touchstart', this._onUserScroll, { passive: true });
|
|
162
|
+
|
|
163
|
+
// Images, embeds, mermaid diagrams and collapsibles change heading
|
|
164
|
+
// offsets after load - re-measure when the content box resizes.
|
|
165
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
166
|
+
const content = getElement(config.selectors.mainContent) || document.body;
|
|
167
|
+
this._resizeObserver = new ResizeObserver(this._onReflow);
|
|
168
|
+
this._resizeObserver.observe(content);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this._update();
|
|
172
|
+
|
|
173
|
+
console.log(`ScrollSpy: Tracking ${this.headings.length} headings`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Mark cached offsets stale and schedule a recompute.
|
|
178
|
+
* @private
|
|
179
|
+
*/
|
|
180
|
+
_invalidate() {
|
|
181
|
+
this._needsMeasure = true;
|
|
182
|
+
this._requestUpdate();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Coalesce updates into one per animation frame.
|
|
187
|
+
* @private
|
|
188
|
+
*/
|
|
189
|
+
_requestUpdate() {
|
|
190
|
+
if (this._frame !== null) return;
|
|
191
|
+
this._frame = requestAnimationFrame(() => this._update());
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Distance (px) from the top of the viewport to the reading line.
|
|
196
|
+
* Derived from the document's `scroll-padding-top` so the highlight lines
|
|
197
|
+
* up with where anchor navigation parks a heading; `config.scrollSpy.offset`
|
|
198
|
+
* pins it explicitly when a fork needs to.
|
|
199
|
+
* @private
|
|
200
|
+
* @returns {number}
|
|
201
|
+
*/
|
|
202
|
+
_readingOffset() {
|
|
203
|
+
const configured = config.scrollSpy.offset;
|
|
204
|
+
if (typeof configured === 'number' && Number.isFinite(configured)) {
|
|
205
|
+
return configured;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const padding = parseFloat(
|
|
209
|
+
getComputedStyle(document.documentElement).scrollPaddingTop
|
|
106
210
|
);
|
|
211
|
+
if (Number.isFinite(padding)) return padding;
|
|
212
|
+
|
|
213
|
+
return config.smoothScroll.offset;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Cache each heading's document offset and sort into document order.
|
|
218
|
+
* @private
|
|
219
|
+
*/
|
|
220
|
+
_measure() {
|
|
221
|
+
const offset = scrollTop();
|
|
107
222
|
|
|
108
|
-
// Observe all headings
|
|
109
223
|
this.headings.forEach(heading => {
|
|
110
|
-
|
|
224
|
+
heading.top = heading.element.getBoundingClientRect().top + offset;
|
|
111
225
|
});
|
|
226
|
+
this.headings.sort((a, b) => a.top - b.top);
|
|
112
227
|
|
|
113
|
-
|
|
228
|
+
this._offsetPx = this._readingOffset();
|
|
229
|
+
this._needsMeasure = false;
|
|
114
230
|
}
|
|
115
231
|
|
|
116
232
|
/**
|
|
117
|
-
*
|
|
233
|
+
* True when the page cannot scroll any further down.
|
|
118
234
|
* @private
|
|
119
|
-
* @
|
|
235
|
+
* @returns {boolean}
|
|
120
236
|
*/
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
237
|
+
_atBottom() {
|
|
238
|
+
const doc = document.documentElement;
|
|
239
|
+
const tolerance = config.scrollSpy.tolerance;
|
|
240
|
+
return window.innerHeight + scrollTop() >= doc.scrollHeight - tolerance;
|
|
241
|
+
}
|
|
125
242
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
243
|
+
/**
|
|
244
|
+
* Resolve which heading the reader is currently in.
|
|
245
|
+
* @private
|
|
246
|
+
* @returns {{element: Element, link: Element, id: string, top: number}|null}
|
|
247
|
+
*/
|
|
248
|
+
_resolveActive() {
|
|
249
|
+
if (this.headings.length === 0) return null;
|
|
250
|
+
|
|
251
|
+
// Trailing sections shorter than the viewport can never reach the
|
|
252
|
+
// reading line, so the bottom of the page belongs to the last heading.
|
|
253
|
+
if (this._atBottom()) return this.headings[this.headings.length - 1];
|
|
254
|
+
|
|
255
|
+
const line = scrollTop() + this._offsetPx + config.scrollSpy.tolerance;
|
|
256
|
+
|
|
257
|
+
let active = null;
|
|
258
|
+
for (let i = 0; i < this.headings.length; i++) {
|
|
259
|
+
if (this.headings[i].top > line) break;
|
|
260
|
+
active = this.headings[i];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Above the first heading, the first section is the one being read.
|
|
264
|
+
return active || this.headings[0];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Recompute and apply the active link. Runs at most once per frame.
|
|
269
|
+
* @private
|
|
270
|
+
*/
|
|
271
|
+
_update() {
|
|
272
|
+
this._frame = null;
|
|
273
|
+
|
|
274
|
+
if (this._needsMeasure) this._measure();
|
|
275
|
+
|
|
276
|
+
const active = this._resolveActive();
|
|
277
|
+
if (!active) return;
|
|
132
278
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
279
|
+
if (this._pendingId !== null) {
|
|
280
|
+
const arrived = active.id === this._pendingId;
|
|
281
|
+
const expired = performance.now() > this._pendingExpires;
|
|
282
|
+
if (!arrived && !expired) return;
|
|
283
|
+
this._pendingId = null;
|
|
136
284
|
}
|
|
285
|
+
|
|
286
|
+
this._setActiveLink(active.link);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Light the clicked link immediately and hold it until the animated
|
|
291
|
+
* scroll lands on it, so headings passed on the way don't flash.
|
|
292
|
+
* @private
|
|
293
|
+
* @param {CustomEvent} event
|
|
294
|
+
*/
|
|
295
|
+
_handleNavigationScroll(event) {
|
|
296
|
+
const targetId = event && event.detail ? event.detail.targetId : null;
|
|
297
|
+
if (!targetId) return;
|
|
298
|
+
|
|
299
|
+
const heading = this.headings.find(h => h.id === targetId);
|
|
300
|
+
if (!heading) return;
|
|
301
|
+
|
|
302
|
+
this._pendingId = targetId;
|
|
303
|
+
this._pendingExpires = performance.now() + CLICK_GUARD_MS;
|
|
304
|
+
this._setActiveLink(heading.link);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Release the click guard so the positional rule takes over again.
|
|
309
|
+
* @private
|
|
310
|
+
*/
|
|
311
|
+
_cancelClickGuard() {
|
|
312
|
+
if (this._pendingId === null) return;
|
|
313
|
+
this._pendingId = null;
|
|
314
|
+
this._requestUpdate();
|
|
137
315
|
}
|
|
138
316
|
|
|
139
317
|
/**
|
|
@@ -142,44 +320,78 @@ export class ScrollSpy {
|
|
|
142
320
|
* @param {Element} link
|
|
143
321
|
*/
|
|
144
322
|
_setActiveLink(link) {
|
|
145
|
-
if (
|
|
323
|
+
if (!link) return;
|
|
324
|
+
// Re-apply if something else stripped the class, so the highlight
|
|
325
|
+
// heals itself rather than silently disappearing.
|
|
326
|
+
if (this.currentActive === link && link.classList.contains(ACTIVE_CLASS)) return;
|
|
146
327
|
|
|
147
328
|
// Remove previous active state
|
|
148
|
-
this.tocLinks.forEach(l =>
|
|
329
|
+
this.tocLinks.forEach(l => {
|
|
330
|
+
if (l === link) return;
|
|
331
|
+
l.classList.remove(ACTIVE_CLASS);
|
|
332
|
+
l.removeAttribute('aria-current');
|
|
333
|
+
});
|
|
149
334
|
|
|
150
335
|
// Add active state
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
336
|
+
link.classList.add(ACTIVE_CLASS);
|
|
337
|
+
link.setAttribute('aria-current', 'true');
|
|
338
|
+
this.currentActive = link;
|
|
339
|
+
|
|
340
|
+
// Scroll TOC to show active link (if needed)
|
|
341
|
+
this._scrollTocToActiveLink(link);
|
|
342
|
+
|
|
343
|
+
// Dispatch custom event for other modules
|
|
344
|
+
document.dispatchEvent(new CustomEvent('navigation:sectionChange', {
|
|
345
|
+
detail: {
|
|
346
|
+
link: link,
|
|
347
|
+
href: link.getAttribute('href')
|
|
348
|
+
}
|
|
349
|
+
}));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Nearest scrollable ancestor of the TOC link, if any.
|
|
354
|
+
* `.bd-toc` scrolls on desktop and `.offcanvas-body` on mobile, so the
|
|
355
|
+
* container is resolved at call time rather than assumed.
|
|
356
|
+
* @private
|
|
357
|
+
* @param {Element} link
|
|
358
|
+
* @returns {Element|null}
|
|
359
|
+
*/
|
|
360
|
+
_getTocScrollContainer(link) {
|
|
361
|
+
const hinted = getElement(config.selectors.tocContainer);
|
|
362
|
+
const scrolls = el =>
|
|
363
|
+
el.scrollHeight > el.clientHeight + 1 &&
|
|
364
|
+
/(auto|scroll)/.test(getComputedStyle(el).overflowY);
|
|
365
|
+
|
|
366
|
+
if (hinted && hinted.contains(link) && scrolls(hinted)) return hinted;
|
|
367
|
+
|
|
368
|
+
let node = link.parentElement;
|
|
369
|
+
while (node && node !== document.body) {
|
|
370
|
+
if (scrolls(node)) return node;
|
|
371
|
+
node = node.parentElement;
|
|
165
372
|
}
|
|
373
|
+
return null;
|
|
166
374
|
}
|
|
167
375
|
|
|
168
376
|
/**
|
|
169
|
-
*
|
|
377
|
+
* Keep the active link visible inside the TOC's own scroll container.
|
|
378
|
+
* Adjusts `scrollTop` directly - `scrollIntoView()` would bubble up and
|
|
379
|
+
* scroll the page, which feeds straight back into the spy.
|
|
170
380
|
* @private
|
|
171
381
|
* @param {Element} link
|
|
172
382
|
*/
|
|
173
383
|
_scrollTocToActiveLink(link) {
|
|
174
|
-
const
|
|
175
|
-
if (!
|
|
384
|
+
const container = this._getTocScrollContainer(link);
|
|
385
|
+
if (!container) return;
|
|
176
386
|
|
|
177
387
|
const linkRect = link.getBoundingClientRect();
|
|
178
|
-
const containerRect =
|
|
388
|
+
const containerRect = container.getBoundingClientRect();
|
|
389
|
+
const margin = 8;
|
|
179
390
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
391
|
+
if (linkRect.top < containerRect.top + margin) {
|
|
392
|
+
container.scrollTop -= (containerRect.top + margin) - linkRect.top;
|
|
393
|
+
} else if (linkRect.bottom > containerRect.bottom - margin) {
|
|
394
|
+
container.scrollTop += linkRect.bottom - (containerRect.bottom - margin);
|
|
183
395
|
}
|
|
184
396
|
}
|
|
185
397
|
|
|
@@ -204,14 +416,37 @@ export class ScrollSpy {
|
|
|
204
416
|
}
|
|
205
417
|
|
|
206
418
|
/**
|
|
207
|
-
* Cleanup
|
|
419
|
+
* Cleanup listeners and observers
|
|
208
420
|
*/
|
|
209
421
|
destroy() {
|
|
210
|
-
if (this.
|
|
211
|
-
this.
|
|
212
|
-
this.
|
|
422
|
+
if (this._onScroll) {
|
|
423
|
+
window.removeEventListener('scroll', this._onScroll);
|
|
424
|
+
window.removeEventListener('hashchange', this._onScroll);
|
|
425
|
+
}
|
|
426
|
+
if (this._onReflow) {
|
|
427
|
+
window.removeEventListener('resize', this._onReflow);
|
|
428
|
+
window.removeEventListener('load', this._onReflow);
|
|
213
429
|
}
|
|
214
|
-
this.
|
|
430
|
+
if (this._onNavigationScroll) {
|
|
431
|
+
document.removeEventListener('navigation:scroll', this._onNavigationScroll);
|
|
432
|
+
}
|
|
433
|
+
if (this._onUserScroll) {
|
|
434
|
+
window.removeEventListener('wheel', this._onUserScroll);
|
|
435
|
+
window.removeEventListener('touchstart', this._onUserScroll);
|
|
436
|
+
}
|
|
437
|
+
if (this._resizeObserver) {
|
|
438
|
+
this._resizeObserver.disconnect();
|
|
439
|
+
this._resizeObserver = null;
|
|
440
|
+
}
|
|
441
|
+
if (this._frame !== null) {
|
|
442
|
+
cancelAnimationFrame(this._frame);
|
|
443
|
+
this._frame = null;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
this.tocLinks.forEach(l => {
|
|
447
|
+
l.classList.remove(ACTIVE_CLASS);
|
|
448
|
+
l.removeAttribute('aria-current');
|
|
449
|
+
});
|
|
215
450
|
this.currentActive = null;
|
|
216
451
|
console.log('ScrollSpy: Destroyed');
|
|
217
452
|
}
|
|
@@ -123,9 +123,15 @@
|
|
|
123
123
|
// the panel's other h6 section headings.
|
|
124
124
|
wrapper.className = 'zer0-appearance-panel mb-4';
|
|
125
125
|
wrapper.innerHTML =
|
|
126
|
-
|
|
126
|
+
// h3, not h6: this panel mounts inside the Settings offcanvas,
|
|
127
|
+
// whose title is an <h2>, alongside the Liquid-rendered section
|
|
128
|
+
// labels that are also <h3 class="h6">. The tag carries DEPTH,
|
|
129
|
+
// the .h6 class carries SIZE (#436). Injected at runtime, so no
|
|
130
|
+
// source grep finds it -- only a rendered test does, which is
|
|
131
|
+
// how this one was caught.
|
|
132
|
+
'<h3 id="zer0-appearance-heading" class="h6 text-body-secondary small text-uppercase fw-semibold mb-2">' +
|
|
127
133
|
'<i class="bi bi-eyedropper me-1" aria-hidden="true"></i>Primary Color' +
|
|
128
|
-
'</
|
|
134
|
+
'</h3>' +
|
|
129
135
|
pickerHtml('visually-hidden') +
|
|
130
136
|
'<button type="button" class="btn btn-link btn-sm p-0" data-appearance-reset>' +
|
|
131
137
|
'<i class="bi bi-arrow-counterclockwise me-1" aria-hidden="true"></i>Reset color' +
|
|
@@ -213,9 +213,14 @@
|
|
|
213
213
|
if (node.nodeType !== Node.TEXT_NODE) return false;
|
|
214
214
|
var parent = node.parentNode;
|
|
215
215
|
while (parent && parent !== document.body) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
216
|
+
// Upper-case the name: HTML elements report "STYLE" but elements in
|
|
217
|
+
// the SVG namespace report "style", so an SVG <style> slipped through
|
|
218
|
+
// and had its `#id` selectors rewritten into tag links. Inline SVG
|
|
219
|
+
// (Mermaid diagrams, icons) is graphics, never prose — skip the whole
|
|
220
|
+
// subtree rather than special-casing its children.
|
|
221
|
+
var tag = String(parent.nodeName || '').toUpperCase();
|
|
222
|
+
if (tag === 'CODE' || tag === 'PRE' || tag === 'A' || tag === 'SCRIPT' || tag === 'STYLE' || tag === 'SVG') return false;
|
|
223
|
+
if (parent.classList && (parent.classList.contains('mermaid') || parent.classList.contains('zer0-diagram') || parent.classList.contains('obsidian-embed'))) return false;
|
|
219
224
|
parent = parent.parentNode;
|
|
220
225
|
}
|
|
221
226
|
var text = node.nodeValue;
|