datalog-theme 0.6.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.
Files changed (180) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +162 -0
  3. data/CITATION.cff +29 -0
  4. data/LICENSE +21 -0
  5. data/README.md +257 -0
  6. data/_data/academic.yml +217 -0
  7. data/_data/cdn-integrity.yml +51 -0
  8. data/_data/config/author.yml +121 -0
  9. data/_data/config/features.yml +262 -0
  10. data/_data/config/site.yml +42 -0
  11. data/_data/config/theme.yml +181 -0
  12. data/_data/datasets.yml +28 -0
  13. data/_data/i18n/en.yml +438 -0
  14. data/_data/i18n/es.yml +438 -0
  15. data/_data/i18n/pt.yml +438 -0
  16. data/_data/js_manifest.json +22 -0
  17. data/_data/js_meta.json +371 -0
  18. data/_data/navigation.yml +145 -0
  19. data/_data/projects.yml +41 -0
  20. data/_data/publications.yml +28 -0
  21. data/_data/social.yml +73 -0
  22. data/_data/visualizations.yml +51 -0
  23. data/_includes/analytics/dashboard.html +149 -0
  24. data/_includes/analytics.html +9 -0
  25. data/_includes/components/academic-dashboard.html +347 -0
  26. data/_includes/components/advanced-search.html +682 -0
  27. data/_includes/components/api-function.html +94 -0
  28. data/_includes/components/author-bio.html +166 -0
  29. data/_includes/components/bookmark-system.html +96 -0
  30. data/_includes/components/breadcrumbs.html +89 -0
  31. data/_includes/components/citation-tools.html +94 -0
  32. data/_includes/components/comments.html +244 -0
  33. data/_includes/components/content-provenance.html +43 -0
  34. data/_includes/components/content-recommendations.html +228 -0
  35. data/_includes/components/difficulty-badge.html +84 -0
  36. data/_includes/components/email-preferences.html +200 -0
  37. data/_includes/components/enhanced-code-block.html +212 -0
  38. data/_includes/components/enhanced-metadata.html +228 -0
  39. data/_includes/components/enhanced-toc.html +209 -0
  40. data/_includes/components/hero.html +15 -0
  41. data/_includes/components/language-switcher.html +396 -0
  42. data/_includes/components/math-fallback.html +19 -0
  43. data/_includes/components/navigation-enhancements.html +454 -0
  44. data/_includes/components/newsletter-signup.html +178 -0
  45. data/_includes/components/open-science-badges.html +16 -0
  46. data/_includes/components/package-install.html +194 -0
  47. data/_includes/components/performance-monitor.html +170 -0
  48. data/_includes/components/popular-posts.html +233 -0
  49. data/_includes/components/post-hero.html +62 -0
  50. data/_includes/components/reading-progress.html +133 -0
  51. data/_includes/components/reading-time.html +121 -0
  52. data/_includes/components/responsive-image.html +63 -0
  53. data/_includes/components/search-facets.html +307 -0
  54. data/_includes/components/series-navigation.html +124 -0
  55. data/_includes/components/social-proof.html +34 -0
  56. data/_includes/components/social-share.html +119 -0
  57. data/_includes/components/user-preferences.html +566 -0
  58. data/_includes/components/visualization-card.html +24 -0
  59. data/_includes/components/viz-table-fallback.html +19 -0
  60. data/_includes/critical-css/default.html +0 -0
  61. data/_includes/critical-css/home.html +0 -0
  62. data/_includes/critical-css/post.html +0 -0
  63. data/_includes/csp-meta.html +17 -0
  64. data/_includes/footer/nav-column.html +31 -0
  65. data/_includes/footer.html +76 -0
  66. data/_includes/head.html +337 -0
  67. data/_includes/header/navigation.html +48 -0
  68. data/_includes/header.html +70 -0
  69. data/_includes/helpers/array-to-sentence.html +21 -0
  70. data/_includes/helpers/date-format.html +16 -0
  71. data/_includes/helpers/link-with-icon.html +26 -0
  72. data/_includes/layouts/default/article.html +67 -0
  73. data/_includes/meta/language-attributes.html +25 -0
  74. data/_includes/meta/math-config.html +21 -0
  75. data/_includes/meta/schema.html +153 -0
  76. data/_includes/meta/scripts-loader.html +52 -0
  77. data/_includes/post/related-posts.html +63 -0
  78. data/_includes/scripts.html +72 -0
  79. data/_includes/skip-link.html +1 -0
  80. data/_includes/toc.html +2 -0
  81. data/_layouts/archive.html +282 -0
  82. data/_layouts/dataset.html +52 -0
  83. data/_layouts/default.html +33 -0
  84. data/_layouts/home.html +48 -0
  85. data/_layouts/notebook.html +173 -0
  86. data/_layouts/package.html +238 -0
  87. data/_layouts/page.html +16 -0
  88. data/_layouts/portfolio.html +41 -0
  89. data/_layouts/post-sidebar.html +183 -0
  90. data/_layouts/post.html +338 -0
  91. data/_layouts/project.html +252 -0
  92. data/_layouts/research.html +504 -0
  93. data/_plugins/analytics_dashboard.rb +315 -0
  94. data/_plugins/config_validator.rb +397 -0
  95. data/_plugins/csp_generator.rb +115 -0
  96. data/_plugins/datalog_bibliography.rb +19 -0
  97. data/_plugins/datalog_comments.rb +18 -0
  98. data/_plugins/datalog_slides.rb +20 -0
  99. data/_plugins/front_matter_compat.rb +109 -0
  100. data/_plugins/i18n.rb +160 -0
  101. data/_plugins/image_optimizer.rb +445 -0
  102. data/_plugins/math_preprocessor.rb +185 -0
  103. data/_plugins/notebook_converter.rb +1107 -0
  104. data/_plugins/plugin_loader.rb +185 -0
  105. data/_plugins/publications_generator.rb +234 -0
  106. data/_plugins/reading_time.rb +15 -0
  107. data/_plugins/search_normalizer.rb +74 -0
  108. data/_plugins/sri_filter.rb +18 -0
  109. data/_plugins/toc_filter.rb +68 -0
  110. data/_plugins/warning_filter.rb +3 -0
  111. data/_sass/_base.scss +142 -0
  112. data/_sass/_components.scss +2380 -0
  113. data/_sass/_header.scss +470 -0
  114. data/_sass/_interactive.scss +344 -0
  115. data/_sass/_layout.scss +1760 -0
  116. data/_sass/_mathematical.scss +368 -0
  117. data/_sass/_mixins.scss +99 -0
  118. data/_sass/_package-docs.scss +715 -0
  119. data/_sass/_phase1-enhancements.scss +721 -0
  120. data/_sass/_phase3-enhancements.scss +874 -0
  121. data/_sass/_phase4-enhancements.scss +1214 -0
  122. data/_sass/_phase5-enhancements.scss +414 -0
  123. data/_sass/_search.scss +654 -0
  124. data/_sass/_syntax-highlighting.scss +128 -0
  125. data/_sass/_theme.scss +79 -0
  126. data/_sass/_typography.scss +148 -0
  127. data/_sass/_utilities.scss +334 -0
  128. data/_sass/_variables.scss +153 -0
  129. data/_sass/_visualizations.scss +242 -0
  130. data/assets/css/main.scss +4 -0
  131. data/assets/img/20220607123041_detail.001.png +0 -0
  132. data/assets/img/favicons/android-chrome-192x192.png +0 -0
  133. data/assets/img/favicons/android-chrome-512x512.png +0 -0
  134. data/assets/img/favicons/apple-touch-icon.png +0 -0
  135. data/assets/img/favicons/favicon-16x16.png +0 -0
  136. data/assets/img/favicons/favicon-32x32.png +0 -0
  137. data/assets/img/favicons/favicon.ico +0 -0
  138. data/assets/img/favicons/site.webmanifest +21 -0
  139. data/assets/img/portfolio-placeholder.svg +20 -0
  140. data/assets/js/academic.js +262 -0
  141. data/assets/js/analytics-dashboard.js +382 -0
  142. data/assets/js/core/dark-mode.js +79 -0
  143. data/assets/js/core/github-cards.js +123 -0
  144. data/assets/js/core/language-filter.js +69 -0
  145. data/assets/js/core/navigation.js +184 -0
  146. data/assets/js/core/scroll-progress.js +45 -0
  147. data/assets/js/core/search-hotkeys.js +62 -0
  148. data/assets/js/core/skip-links.js +62 -0
  149. data/assets/js/loader.js +163 -0
  150. data/assets/js/main.js +23 -0
  151. data/assets/js/math.js +818 -0
  152. data/assets/js/notebook.js +158 -0
  153. data/assets/js/search/analytics.js +91 -0
  154. data/assets/js/search/app.js +271 -0
  155. data/assets/js/search/autocomplete.js +120 -0
  156. data/assets/js/search/engine.js +260 -0
  157. data/assets/js/search/filters.js +38 -0
  158. data/assets/js/search/render.js +217 -0
  159. data/assets/js/search/utils.js +99 -0
  160. data/assets/js/search.js +354 -0
  161. data/assets/js/visualizations.js +816 -0
  162. data/assets/publications/datalog-publications.bib +8 -0
  163. data/assets/publications/datalog-publications.ris +9 -0
  164. data/assets/publications/publications.bib +30 -0
  165. data/assets/templates/diogo-ribeiro-cv.md +31 -0
  166. data/assets/templates/diogo-ribeiro-cv.tex +32 -0
  167. data/bin/datalog +26 -0
  168. data/datalog-theme.gemspec +72 -0
  169. data/lib/datalog/cli.rb +563 -0
  170. data/lib/datalog/plugin_system/dependency_resolver.rb +223 -0
  171. data/lib/datalog/plugin_system.rb +237 -0
  172. data/lib/datalog/plugins/citations.rb +182 -0
  173. data/lib/datalog/plugins/comments.rb +183 -0
  174. data/lib/datalog/plugins/search.rb +45 -0
  175. data/lib/datalog/plugins/slides.rb +121 -0
  176. data/lib/datalog/theme/theme.rb +18 -0
  177. data/lib/datalog/theme/version.rb +7 -0
  178. data/lib/datalog/warning_filter.rb +28 -0
  179. data/lib/datalog-theme.rb +17 -0
  180. metadata +555 -0
@@ -0,0 +1,79 @@
1
+ /**
2
+ * @fileoverview Dark mode toggle functionality.
3
+ * Persists user preference and respects system color scheme.
4
+ * @module core/dark-mode
5
+ */
6
+
7
+ /** @constant {string} LocalStorage key for color mode preference */
8
+ const STORAGE_KEY = "datalog-color-mode";
9
+ /** @constant {string} Class applied to body for dark mode */
10
+ const DARK_CLASS = "dark-mode";
11
+
12
+ /**
13
+ * Resolves the initial dark mode preference from storage or system settings.
14
+ * @param {MediaQueryList} prefersDarkScheme - Media query for dark scheme preference
15
+ * @returns {boolean} True if dark mode should be enabled
16
+ */
17
+ function resolveInitialPreference(prefersDarkScheme) {
18
+ const storedPreference = localStorage.getItem(STORAGE_KEY);
19
+ if (storedPreference === "dark") {
20
+ return true;
21
+ }
22
+ if (storedPreference === "light") {
23
+ return false;
24
+ }
25
+ return prefersDarkScheme.matches;
26
+ }
27
+
28
+ /**
29
+ * Initializes the dark mode toggle button and system preference listener.
30
+ * Reads initial preference from localStorage or system settings.
31
+ * @returns {void}
32
+ */
33
+ export function initDarkModeToggle() {
34
+ const toggleButton = document.querySelector("[data-toggle-dark-mode]");
35
+ const body = document.body;
36
+ const prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)");
37
+
38
+ if (resolveInitialPreference(prefersDarkScheme)) {
39
+ body.classList.add(DARK_CLASS);
40
+ }
41
+
42
+ /** Mirrors the effective theme on <body data-theme> for CSS hooks and tests. */
43
+ const syncThemeAttribute = () => {
44
+ const theme = body.classList.contains(DARK_CLASS) ? "dark" : "light";
45
+ body.dataset.theme = theme;
46
+ document.documentElement.dataset.theme = theme;
47
+ };
48
+
49
+ const syncButton = () => {
50
+ syncThemeAttribute();
51
+ if (!toggleButton) {
52
+ return;
53
+ }
54
+ toggleButton.setAttribute("aria-pressed", body.classList.contains(DARK_CLASS));
55
+ };
56
+
57
+ syncButton();
58
+
59
+ if (toggleButton) {
60
+ toggleButton.addEventListener("click", () => {
61
+ const isDark = body.classList.toggle(DARK_CLASS);
62
+ toggleButton.setAttribute("aria-pressed", String(isDark));
63
+ localStorage.setItem(STORAGE_KEY, isDark ? "dark" : "light");
64
+ syncThemeAttribute();
65
+ });
66
+ }
67
+
68
+ prefersDarkScheme.addEventListener("change", (event) => {
69
+ if (localStorage.getItem(STORAGE_KEY)) {
70
+ return;
71
+ }
72
+ if (event.matches) {
73
+ body.classList.add(DARK_CLASS);
74
+ } else {
75
+ body.classList.remove(DARK_CLASS);
76
+ }
77
+ syncButton();
78
+ });
79
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * @fileoverview GitHub repository cards with live stats.
3
+ * Fetches and displays repository statistics with caching.
4
+ * @module core/github-cards
5
+ */
6
+
7
+ /** @constant {string} Prefix for localStorage cache keys */
8
+ const CACHE_PREFIX = "datalog-github-";
9
+
10
+ /**
11
+ * Generates a cache key for a repository.
12
+ * @param {string} owner - Repository owner
13
+ * @param {string} repo - Repository name
14
+ * @returns {string} Cache key
15
+ */
16
+ function cacheKey(owner, repo) {
17
+ return `${CACHE_PREFIX}${owner}-${repo}`;
18
+ }
19
+
20
+ function readCache(owner, repo, cacheTtl) {
21
+ try {
22
+ const raw = localStorage.getItem(cacheKey(owner, repo));
23
+ if (!raw) {
24
+ return null;
25
+ }
26
+ const parsed = JSON.parse(raw);
27
+ if (!parsed || typeof parsed !== "object") {
28
+ return null;
29
+ }
30
+ if (Date.now() - parsed.timestamp > cacheTtl) {
31
+ localStorage.removeItem(cacheKey(owner, repo));
32
+ return null;
33
+ }
34
+ return parsed.data;
35
+ } catch (error) {
36
+ console.warn("GitHub cache read failed", error);
37
+ return null;
38
+ }
39
+ }
40
+
41
+ function writeCache(owner, repo, data) {
42
+ try {
43
+ localStorage.setItem(
44
+ cacheKey(owner, repo),
45
+ JSON.stringify({ timestamp: Date.now(), data })
46
+ );
47
+ } catch (error) {
48
+ console.warn("GitHub cache write failed", error);
49
+ }
50
+ }
51
+
52
+ async function fetchRepository(owner, repo, apiBase, cacheTtl) {
53
+ const cached = readCache(owner, repo, cacheTtl);
54
+ if (cached) {
55
+ return cached;
56
+ }
57
+ try {
58
+ const response = await fetch(`${apiBase}/${owner}/${repo}`);
59
+ if (!response.ok) {
60
+ throw new Error(`GitHub API error: ${response.status}`);
61
+ }
62
+ const json = await response.json();
63
+ writeCache(owner, repo, json);
64
+ return json;
65
+ } catch (error) {
66
+ console.warn(`Unable to load GitHub data for ${owner}/${repo}`);
67
+ return null;
68
+ }
69
+ }
70
+
71
+ function applyRepositoryData(card, data) {
72
+ const statElements = card.querySelectorAll("[data-github-stat]");
73
+ statElements.forEach((element) => {
74
+ const stat = element.getAttribute("data-github-stat");
75
+ if (!stat) {
76
+ return;
77
+ }
78
+ const value = data[stat];
79
+ if (typeof value === "number") {
80
+ element.textContent = value.toLocaleString();
81
+ } else if (typeof value === "string" && value.length > 0) {
82
+ element.textContent = value;
83
+ }
84
+ });
85
+ }
86
+
87
+ /**
88
+ * Initializes GitHub repository cards by fetching live stats.
89
+ * Uses localStorage caching to minimize API calls.
90
+ * @returns {Promise<void[]>} Resolves when all cards are populated
91
+ */
92
+ export function initGitHubCards() {
93
+ const cards = document.querySelectorAll("[data-github-owner][data-github-repo]");
94
+ if (!cards.length) {
95
+ return Promise.resolve();
96
+ }
97
+
98
+ const integrations = window.DatalogIntegrations || {};
99
+ const githubConfig = integrations.github || {};
100
+ if (githubConfig.enabled === false) {
101
+ return Promise.resolve();
102
+ }
103
+
104
+ const apiBase = githubConfig.api_base || "https://api.github.com/repos";
105
+ const cacheTtl = Number(githubConfig.cache_ttl || 43200) * 1000;
106
+
107
+ const promises = Array.from(cards).map((card) => {
108
+ const owner = card.getAttribute("data-github-owner");
109
+ const repo = card.getAttribute("data-github-repo");
110
+ if (!owner || !repo) {
111
+ return Promise.resolve();
112
+ }
113
+
114
+ return fetchRepository(owner, repo, apiBase, cacheTtl).then((data) => {
115
+ if (!data) {
116
+ return;
117
+ }
118
+ applyRepositoryData(card, data);
119
+ });
120
+ });
121
+
122
+ return Promise.all(promises);
123
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @fileoverview Navigate-on-change select handling.
3
+ * @module core/language-filter
4
+ */
5
+
6
+ const NAVIGATING_SELECTS = "[data-language-filter], [data-navigate-on-change]";
7
+
8
+ /**
9
+ * Resolves an option value to a target that is safe to navigate to.
10
+ *
11
+ * Option values come from site data and front matter, so they are not visitor
12
+ * controlled, but rejecting non-http(s) schemes keeps a stray `javascript:`
13
+ * value from turning a dropdown into a script sink. Relative values carry no
14
+ * scheme and pass through untouched, so existing links keep working.
15
+ *
16
+ * @param {unknown} value Raw option value.
17
+ * @returns {string|null} The value to navigate to, or null when unsafe.
18
+ */
19
+ function safeNavigationTarget(value) {
20
+ if (typeof value !== "string") {
21
+ return null;
22
+ }
23
+
24
+ // Browsers ignore control characters when parsing a scheme, so strip them
25
+ // first; otherwise "java script:alert(1)" would read as scheme-less here
26
+ // and still execute once assigned. Done by code point rather than a regex,
27
+ // which would trip no-control-regex.
28
+ const candidate = Array.from(value)
29
+ .filter((character) => {
30
+ const code = character.codePointAt(0);
31
+ return code > 0x1f && code !== 0x7f;
32
+ })
33
+ .join("")
34
+ .trim();
35
+ if (!candidate) {
36
+ return null;
37
+ }
38
+
39
+ const scheme = /^[a-z][a-z0-9+.-]*:/i.exec(candidate);
40
+ if (scheme && !/^https?:$/i.test(scheme[0])) {
41
+ return null;
42
+ }
43
+
44
+ return candidate;
45
+ }
46
+
47
+ /**
48
+ * Binds selects that navigate when their value changes.
49
+ *
50
+ * Replaces inline `onchange` handlers, which the site's CSP blocks because
51
+ * script-src carries no 'unsafe-inline'.
52
+ *
53
+ * @returns {void}
54
+ */
55
+ export function initLanguageFilter() {
56
+ const selects = document.querySelectorAll(NAVIGATING_SELECTS);
57
+ if (selects.length === 0) {
58
+ return;
59
+ }
60
+
61
+ selects.forEach((select) => {
62
+ select.addEventListener("change", (event) => {
63
+ const target = safeNavigationTarget(event.target && event.target.value);
64
+ if (target) {
65
+ window.location.href = target;
66
+ }
67
+ });
68
+ });
69
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * @fileoverview Navigation module for responsive site navigation.
3
+ * Handles mobile menu toggling, smooth scrolling, and keyboard navigation.
4
+ * @module core/navigation
5
+ */
6
+
7
+ /** @constant {string} Media query for desktop viewport */
8
+ const DESKTOP_MEDIA = "(min-width: 48rem)";
9
+ /** @constant {string} Selector for the main navigation element */
10
+ const NAV_SELECTOR = "#site-nav";
11
+ /** @constant {string} Selector for the navigation toggle button */
12
+ const TOGGLE_SELECTOR = ".nav-toggle";
13
+ /** @constant {string} Class applied to body when nav is open */
14
+ const NAV_OPEN_CLASS = "nav-open";
15
+
16
+ /**
17
+ * Normalizes a URL path by removing trailing slashes and index.html.
18
+ * @param {string} path - The path to normalize
19
+ * @returns {string} Normalized path
20
+ */
21
+ function normalizePath(path) {
22
+ return path.replace(/index\.html$/, "").replace(/\/$/, "");
23
+ }
24
+
25
+ function getAnchorTarget(link) {
26
+ const href = link.getAttribute("href");
27
+ if (!href) {
28
+ return null;
29
+ }
30
+ try {
31
+ const destination = new URL(href, window.location.origin);
32
+ if (!destination.hash) {
33
+ return null;
34
+ }
35
+ const current = new URL(window.location.href);
36
+ if (normalizePath(destination.pathname) !== normalizePath(current.pathname)) {
37
+ return null;
38
+ }
39
+ return destination.hash.replace("#", "");
40
+ } catch (error) {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ function smoothScrollToId(id) {
46
+ if (!id) {
47
+ return false;
48
+ }
49
+ const target = document.getElementById(id);
50
+ if (!target) {
51
+ return false;
52
+ }
53
+ target.scrollIntoView({ behavior: "smooth", block: "start" });
54
+ return true;
55
+ }
56
+
57
+ /**
58
+ * Initializes the navigation system including mobile toggle,
59
+ * smooth scrolling for anchor links, and keyboard accessibility.
60
+ * @returns {void}
61
+ */
62
+ export function initNavigation() {
63
+ const siteNav = document.querySelector(NAV_SELECTOR);
64
+ if (!siteNav) {
65
+ return;
66
+ }
67
+
68
+ const navToggle = document.querySelector(TOGGLE_SELECTOR);
69
+ const body = document.body;
70
+ const navMediaQuery = window.matchMedia(DESKTOP_MEDIA);
71
+
72
+ const syncNavForViewport = (isDesktop) => {
73
+ if (isDesktop) {
74
+ siteNav.dataset.open = "true";
75
+ body.classList.remove(NAV_OPEN_CLASS);
76
+ if (navToggle) {
77
+ navToggle.setAttribute("aria-expanded", "false");
78
+ navToggle.classList.remove("is-active");
79
+ }
80
+ return;
81
+ }
82
+
83
+ const isOpen = siteNav.dataset.open === "true";
84
+ if (!isOpen) {
85
+ siteNav.dataset.open = "false";
86
+ }
87
+ if (navToggle) {
88
+ navToggle.setAttribute("aria-expanded", String(isOpen));
89
+ navToggle.classList.toggle("is-active", isOpen);
90
+ }
91
+ };
92
+
93
+ const closeNav = ({ focusToggle = false } = {}) => {
94
+ if (navMediaQuery.matches) {
95
+ return;
96
+ }
97
+ siteNav.dataset.open = "false";
98
+ body.classList.remove(NAV_OPEN_CLASS);
99
+ if (navToggle) {
100
+ navToggle.setAttribute("aria-expanded", "false");
101
+ navToggle.classList.remove("is-active");
102
+ if (focusToggle) {
103
+ navToggle.focus();
104
+ }
105
+ }
106
+ };
107
+
108
+ const openNav = () => {
109
+ if (navMediaQuery.matches) {
110
+ return;
111
+ }
112
+ siteNav.dataset.open = "true";
113
+ body.classList.add(NAV_OPEN_CLASS);
114
+ if (navToggle) {
115
+ navToggle.setAttribute("aria-expanded", "true");
116
+ navToggle.classList.add("is-active");
117
+ }
118
+ };
119
+
120
+ const toggleNav = () => {
121
+ if (navMediaQuery.matches) {
122
+ return;
123
+ }
124
+ if (siteNav.dataset.open === "true") {
125
+ closeNav();
126
+ } else {
127
+ openNav();
128
+ }
129
+ };
130
+
131
+ if (!navMediaQuery.matches) {
132
+ siteNav.dataset.open = "false";
133
+ }
134
+
135
+ syncNavForViewport(navMediaQuery.matches);
136
+ navMediaQuery.addEventListener("change", (event) => {
137
+ if (event.matches) {
138
+ syncNavForViewport(true);
139
+ } else {
140
+ closeNav();
141
+ siteNav.dataset.open = "false";
142
+ syncNavForViewport(false);
143
+ }
144
+ });
145
+
146
+ const navLinks = siteNav.querySelectorAll(".nav-link");
147
+ navLinks.forEach((link) => {
148
+ const anchorId = getAnchorTarget(link);
149
+ link.addEventListener("click", (event) => {
150
+ if (!navMediaQuery.matches) {
151
+ closeNav();
152
+ }
153
+ if (anchorId && smoothScrollToId(anchorId)) {
154
+ event.preventDefault();
155
+ }
156
+ });
157
+ });
158
+
159
+ if (navToggle) {
160
+ navToggle.addEventListener("click", () => {
161
+ toggleNav();
162
+ });
163
+ }
164
+
165
+ document.addEventListener("keydown", (event) => {
166
+ if (event.key === "Escape") {
167
+ closeNav({ focusToggle: true });
168
+ }
169
+ });
170
+
171
+ document.addEventListener("click", (event) => {
172
+ if (navMediaQuery.matches || siteNav.dataset.open !== "true") {
173
+ return;
174
+ }
175
+ const target = event.target;
176
+ if (!target || typeof target.closest !== "function") {
177
+ return;
178
+ }
179
+ if (target.closest(NAV_SELECTOR) || (navToggle && target.closest(TOGGLE_SELECTOR))) {
180
+ return;
181
+ }
182
+ closeNav();
183
+ });
184
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @fileoverview Scroll progress indicator.
3
+ * Shows reading progress as user scrolls through content.
4
+ * @module core/scroll-progress
5
+ */
6
+
7
+ /**
8
+ * Calculates and updates the scroll progress indicator.
9
+ * @param {HTMLElement} progressBar - The progress bar element
10
+ * @param {HTMLElement|null} progressValue - Optional element to display percentage
11
+ */
12
+ function calculateProgress(progressBar, progressValue) {
13
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
14
+ const docHeight = document.documentElement.scrollHeight - window.innerHeight;
15
+ const progress = docHeight > 0 ? Math.min((scrollTop / docHeight) * 100, 100) : 0;
16
+
17
+ if (progressBar.tagName === "PROGRESS") {
18
+ progressBar.setAttribute("value", String(progress));
19
+ } else {
20
+ progressBar.style.setProperty("--progress", `${progress}%`);
21
+ }
22
+
23
+ if (progressValue) {
24
+ progressValue.textContent = Math.round(progress);
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Initializes the scroll progress indicator.
30
+ * Listens to scroll and resize events to update progress.
31
+ * @returns {void}
32
+ */
33
+ export function initScrollProgress() {
34
+ const progressBar = document.querySelector("[data-scroll-progress]");
35
+ if (!progressBar) {
36
+ return;
37
+ }
38
+
39
+ const progressValue = document.querySelector("[data-scroll-progress-value]");
40
+ const updateProgress = () => calculateProgress(progressBar, progressValue);
41
+
42
+ updateProgress();
43
+ window.addEventListener("scroll", updateProgress, { passive: true });
44
+ window.addEventListener("resize", updateProgress);
45
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * @fileoverview Search keyboard shortcuts.
3
+ * Enables "/" and Cmd/Ctrl+K hotkeys to focus search.
4
+ * @module core/search-hotkeys
5
+ */
6
+
7
+ /**
8
+ * Focuses the global search input or navigates to search page.
9
+ * @param {HTMLElement} siteSearchForm - The search form element
10
+ */
11
+ function focusGlobalSearch(siteSearchForm) {
12
+ if (!siteSearchForm) {
13
+ return;
14
+ }
15
+ const siteSearchInput = siteSearchForm.querySelector("input[type='search']");
16
+ const searchDestination = siteSearchForm.getAttribute("action");
17
+
18
+ if (siteSearchInput) {
19
+ siteSearchInput.focus();
20
+ siteSearchInput.select();
21
+ } else if (searchDestination) {
22
+ window.location.href = searchDestination;
23
+ }
24
+ }
25
+
26
+ function shouldIgnoreHotkey(activeElement) {
27
+ if (!activeElement) {
28
+ return false;
29
+ }
30
+ if (activeElement.isContentEditable) {
31
+ return true;
32
+ }
33
+ const tag = activeElement.tagName;
34
+ return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
35
+ }
36
+
37
+ /**
38
+ * Initializes search keyboard shortcuts (/ and Cmd/Ctrl+K).
39
+ * @returns {void}
40
+ */
41
+ export function initSearchHotkeys() {
42
+ const siteSearchForm = document.querySelector(".site-search");
43
+ if (!siteSearchForm) {
44
+ return;
45
+ }
46
+
47
+ document.addEventListener("keydown", (event) => {
48
+ if (shouldIgnoreHotkey(document.activeElement)) {
49
+ return;
50
+ }
51
+
52
+ if (event.key === "/" && !event.ctrlKey && !event.metaKey && !event.altKey) {
53
+ event.preventDefault();
54
+ focusGlobalSearch(siteSearchForm);
55
+ }
56
+
57
+ if ((event.key === "k" || event.key === "K") && (event.ctrlKey || event.metaKey)) {
58
+ event.preventDefault();
59
+ focusGlobalSearch(siteSearchForm);
60
+ }
61
+ });
62
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * @fileoverview Skip links for keyboard accessibility.
3
+ * Allows users to skip to main content areas.
4
+ * @module core/skip-links
5
+ */
6
+
7
+ /**
8
+ * Focuses a target element, temporarily making it focusable if needed.
9
+ * @param {HTMLElement|null} target - The element to focus
10
+ */
11
+ function focusTarget(target) {
12
+ if (!target) {
13
+ return;
14
+ }
15
+
16
+ const previousTabIndex = target.getAttribute("tabindex");
17
+ if (previousTabIndex === null) {
18
+ target.setAttribute("tabindex", "-1");
19
+ target.dataset.skipLinkTempTabindex = "true";
20
+ }
21
+
22
+ target.addEventListener(
23
+ "blur",
24
+ () => {
25
+ if (target.dataset.skipLinkTempTabindex) {
26
+ target.removeAttribute("tabindex");
27
+ delete target.dataset.skipLinkTempTabindex;
28
+ }
29
+ },
30
+ { once: true }
31
+ );
32
+
33
+ target.focus();
34
+ }
35
+
36
+ /**
37
+ * Initializes skip link functionality for accessibility.
38
+ * Enhances focus management when skip links are activated.
39
+ * @returns {void}
40
+ */
41
+ export function initSkipLinks() {
42
+ const skipLinks = document.querySelectorAll(".skip-link[href^='#']");
43
+ if (!skipLinks.length) {
44
+ return;
45
+ }
46
+
47
+ skipLinks.forEach((link) => {
48
+ link.addEventListener("click", (event) => {
49
+ const href = link.getAttribute("href") || "";
50
+ const targetId = href.slice(1);
51
+ if (!targetId) {
52
+ return;
53
+ }
54
+ const target = document.getElementById(targetId);
55
+ if (!target) {
56
+ return;
57
+ }
58
+ event.preventDefault();
59
+ focusTarget(target);
60
+ });
61
+ });
62
+ }