jekyll-theme-zer0 1.19.1 → 1.20.2
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 +395 -0
- data/README.md +27 -19
- data/_data/authors.yml +154 -5
- data/_data/backlog.yml +5 -5
- data/_data/content_statistics.yml +273 -297
- data/_data/features.yml +4 -25
- data/_data/navigation/README.md +24 -0
- data/_data/navigation/about.yml +2 -0
- data/_data/navigation/main.yml +2 -7
- data/_data/roadmap.yml +86 -12
- data/_includes/components/author-avatar-url.html +28 -0
- data/_includes/components/author-bio.html +86 -0
- data/_includes/components/author-card.html +184 -121
- data/_includes/components/author-eeat.html +10 -4
- data/_includes/components/info-section.html +1 -1
- data/_includes/components/mermaid.html +0 -3
- data/_includes/components/post-card.html +19 -9
- data/_includes/content/giscus.html +3 -2
- data/_includes/core/footer-fabs.html +28 -0
- data/_includes/core/footer.html +7 -17
- data/_includes/core/head.html +2 -2
- data/_includes/navigation/breadcrumbs.html +20 -2
- data/_includes/navigation/local-graph.html +18 -2
- data/_includes/obsidian/full-graph.html +4 -6
- data/_layouts/article.html +44 -74
- data/_layouts/author.html +274 -0
- data/_layouts/authors.html +55 -0
- data/_layouts/news.html +3 -3
- data/_layouts/note.html +21 -6
- data/_layouts/notebook.html +21 -6
- data/_layouts/root.html +31 -17
- data/_layouts/section.html +3 -3
- data/_plugins/author_pages_generator.rb +121 -0
- data/_sass/components/_author.scss +219 -0
- data/_sass/components/_content-tables.scss +16 -1
- data/_sass/components/_notes-index.scss +102 -0
- data/_sass/components/_search-modal.scss +40 -0
- data/_sass/components/_ui-enhancements.scss +570 -0
- data/_sass/core/_docs-code-examples.scss +463 -0
- data/_sass/core/_docs-layout.scss +0 -453
- data/_sass/core/_navbar.scss +253 -0
- data/_sass/core/_sidebar-extras.scss +79 -0
- data/_sass/core/_toc.scss +87 -0
- data/_sass/core/_variables.scss +7 -142
- data/_sass/custom.scss +24 -1122
- data/_sass/layouts/_global-chrome.scss +59 -0
- data/assets/css/main.scss +19 -2
- data/assets/js/author-profile.js +190 -0
- data/assets/js/modules/navigation/navbar.js +104 -0
- data/assets/js/obsidian-graph.js +2 -2
- data/assets/js/obsidian-local-graph.js +11 -5
- data/assets/vendor/cytoscape/cytoscape.min.js +32 -0
- data/scripts/README.md +39 -0
- data/scripts/bin/validate +11 -1
- data/scripts/dev/css-diff.sh +49 -0
- data/scripts/dev/shot.js +37 -0
- data/scripts/features/generate-preview-images +110 -6
- data/scripts/features/pixelate-preview-images +126 -0
- data/scripts/features/pixelate_images.py +662 -0
- data/scripts/github-setup.sh +0 -0
- data/scripts/lib/preview_generator.py +47 -3
- data/scripts/pixelate-preview-images.sh +12 -0
- data/scripts/test/integration/auto-version +10 -8
- data/scripts/test/lib/run_tests.sh +2 -0
- data/scripts/test/lib/test_content_review.sh +205 -0
- data/scripts/test/lib/test_pixelate_images.sh +108 -0
- metadata +25 -20
- data/_data/hub.yml +0 -68
- data/_data/hub_index.yml +0 -203
- data/_data/navigation/hub.yml +0 -110
- data/assets/vendor/font-awesome/css/all.min.css +0 -9
- data/assets/vendor/font-awesome/webfonts/fa-brands-400.ttf +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-brands-400.woff2 +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-regular-400.ttf +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-regular-400.woff2 +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-solid-900.ttf +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-solid-900.woff2 +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-v4compatibility.ttf +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-v4compatibility.woff2 +0 -0
- data/assets/vendor/jquery/jquery-3.7.1.min.js +0 -2
- data/scripts/lib/hub.rb +0 -208
- data/scripts/provision-org-sites.rb +0 -252
- data/scripts/provision-org-sites.sh +0 -23
- data/scripts/sync-hub-metadata.rb +0 -184
- data/scripts/sync-hub-metadata.sh +0 -22
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
|
|
3
|
+
// Global page chrome — base element resets + sticky/shadow/min-height helpers
|
|
4
|
+
|
|
5
|
+
// ----------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
// Extracted from custom.scss (Phase 5). Imported via the custom.scss barrel at
|
|
8
|
+
|
|
9
|
+
// its original position so the cascade is unchanged.
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
html, body {
|
|
14
|
+
max-width: 100%;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Theme-wide horizontal-overflow safety net.
|
|
18
|
+
// A stray wide element (an unwrapped Bootstrap `.row`, a long code token, a
|
|
19
|
+
// markdown table) creates a horizontal page scrollbar. Because the header is
|
|
20
|
+
// `position: fixed` it only spans the viewport, so any page-level h-scroll
|
|
21
|
+
// makes the navbar look "cut off" on the right. `overflow-x: clip` removes
|
|
22
|
+
// that scrollbar at the root WITHOUT breaking `position: sticky` descendants
|
|
23
|
+
// (the docs sidebar / TOC) the way `overflow-x: hidden` would — clip does not
|
|
24
|
+
// create a scroll container. Genuinely wide content (tables, code blocks)
|
|
25
|
+
// keeps its own local `overflow-x: auto` so nothing is hidden by this clip.
|
|
26
|
+
html {
|
|
27
|
+
overflow-x: clip;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
* {
|
|
31
|
+
box-sizing: border-box;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.make-me-sticky {
|
|
35
|
+
position: -webkit-sticky;
|
|
36
|
+
position: sticky;
|
|
37
|
+
top: 0;
|
|
38
|
+
padding: 0 15px;
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-wrap: wrap;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.bottom-shadow {
|
|
44
|
+
// background-color: rgba(255,255,255,0.95);
|
|
45
|
+
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, .35), inset 0 -1px 0 rgba(0, 0, 0, .15);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Vendored Bootstrap build may omit vh utilities; landing hero uses this for stable min height
|
|
49
|
+
.min-vh-50 {
|
|
50
|
+
min-height: 50vh;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// MOVED → _sass/layouts/_landing.scss (token-aware version)
|
|
54
|
+
// Retained shim: keeps the inline-loading aspect-ratio fallback for the hero
|
|
55
|
+
// media slot so existing inline `aspect-ratio` markup in landing.html still
|
|
56
|
+
// has a visible placeholder background while the image streams in.
|
|
57
|
+
.landing-hero .landing-hero-media {
|
|
58
|
+
background-color: rgba(255, 255, 255, 0.06);
|
|
59
|
+
}
|
data/assets/css/main.scss
CHANGED
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
|
|
19
19
|
// 3. Bootstrap-docs-style layout (trimmed; was ~3.2k-line vendor snapshot)
|
|
20
20
|
@import "core/docs-layout";
|
|
21
|
+
// Code-example chrome split out of docs-layout; imported immediately after
|
|
22
|
+
// it so the concatenated cascade order is unchanged.
|
|
23
|
+
@import "core/docs-code-examples";
|
|
21
24
|
|
|
22
25
|
// 4. Reusable utilities (motion, focus rings) — used by components below
|
|
23
26
|
@import "utilities/motion";
|
|
@@ -30,9 +33,11 @@
|
|
|
30
33
|
@import "components/skeleton";
|
|
31
34
|
@import "components/callout";
|
|
32
35
|
@import "components/post-navigation";
|
|
36
|
+
@import "components/author";
|
|
33
37
|
@import "components/footer";
|
|
34
38
|
@import "components/theme-preview";
|
|
35
39
|
@import "components/content-tables";
|
|
40
|
+
@import "components/search-modal";
|
|
36
41
|
|
|
37
42
|
// 6. Layout partials
|
|
38
43
|
@import "layouts/landing";
|
|
@@ -42,8 +47,20 @@
|
|
|
42
47
|
// 7. Bootstrap Styles
|
|
43
48
|
// @import "bootstrap.scss";
|
|
44
49
|
|
|
45
|
-
//
|
|
46
|
-
//
|
|
50
|
+
// 7b. Core nav/content partials formerly imported from the top of custom.scss.
|
|
51
|
+
// Lifted here so assets/css/main.scss is the single assembly manifest; kept
|
|
52
|
+
// in their original relative order (immediately before the custom layer) so
|
|
53
|
+
// the compiled cascade is unchanged.
|
|
54
|
+
@import "notebooks";
|
|
55
|
+
@import "core/nav-tree";
|
|
56
|
+
@import "core/sidebar-categories";
|
|
57
|
+
@import "core/navbar";
|
|
58
|
+
@import "core/offcanvas-panels";
|
|
59
|
+
@import "core/obsidian";
|
|
60
|
+
|
|
61
|
+
// 8. Custom layer — thin barrel that imports the focused partials the old
|
|
62
|
+
// custom.scss monolith was decomposed into (global-chrome, toc,
|
|
63
|
+
// sidebar-extras, ui-enhancements, notes-index). See _sass/custom.scss.
|
|
47
64
|
@import "custom.scss";
|
|
48
65
|
|
|
49
66
|
// 9. Custom Features
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* author-profile.js
|
|
3
|
+
* ---------------------------------------------------------------------------
|
|
4
|
+
* Progressive-enhancement controller for the interactive author profile page
|
|
5
|
+
* (_layouts/author.html). Self-activates on any [data-author-profile] container,
|
|
6
|
+
* so it is safe to load globally and is a no-op everywhere else.
|
|
7
|
+
*
|
|
8
|
+
* Powers: type filters (the stat cards), free-text search (title + tags),
|
|
9
|
+
* sort (newest / oldest / A–Z), topic/tag chips, a live result count, a
|
|
10
|
+
* clear-filters control, deep-linkable type filter via the URL hash
|
|
11
|
+
* (#type=posts), and a reduced-motion-aware count-up on the stat numbers.
|
|
12
|
+
*
|
|
13
|
+
* No dependencies. With JS disabled every item stays visible (crawlable).
|
|
14
|
+
* ---------------------------------------------------------------------------
|
|
15
|
+
*/
|
|
16
|
+
(function () {
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
function init(root) {
|
|
20
|
+
var grid = root.querySelector('[data-author-grid]');
|
|
21
|
+
if (!grid) return;
|
|
22
|
+
|
|
23
|
+
var items = Array.prototype.slice.call(grid.querySelectorAll('.author-item'));
|
|
24
|
+
var filterBtns = Array.prototype.slice.call(root.querySelectorAll('[data-filter]'));
|
|
25
|
+
var tagBtns = Array.prototype.slice.call(root.querySelectorAll('[data-tag-filter]'));
|
|
26
|
+
var searchInput = root.querySelector('[data-author-search]');
|
|
27
|
+
var sortSelect = root.querySelector('[data-author-sort]');
|
|
28
|
+
var clearBtns = Array.prototype.slice.call(root.querySelectorAll('[data-author-clear]'));
|
|
29
|
+
var countEl = root.querySelector('[data-author-count]');
|
|
30
|
+
var noResults = root.querySelector('[data-author-noresults]');
|
|
31
|
+
var total = items.length;
|
|
32
|
+
|
|
33
|
+
var state = {
|
|
34
|
+
filter: 'all',
|
|
35
|
+
query: '',
|
|
36
|
+
sort: sortSelect ? sortSelect.value : 'newest',
|
|
37
|
+
tag: null
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function matches(item) {
|
|
41
|
+
if (state.filter !== 'all' && item.getAttribute('data-collection') !== state.filter) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (state.tag) {
|
|
45
|
+
var bag = '|' + (item.getAttribute('data-tags') || '') + '|';
|
|
46
|
+
if (bag.indexOf('|' + state.tag + '|') === -1) return false;
|
|
47
|
+
}
|
|
48
|
+
if (state.query) {
|
|
49
|
+
var hay = (item.getAttribute('data-title') || '') + '|' + (item.getAttribute('data-tags') || '');
|
|
50
|
+
if (hay.indexOf(state.query) === -1) return false;
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function sortItems() {
|
|
56
|
+
var ordered = items.slice();
|
|
57
|
+
ordered.sort(function (a, b) {
|
|
58
|
+
if (state.sort === 'az') {
|
|
59
|
+
return (a.getAttribute('data-title') || '').localeCompare(b.getAttribute('data-title') || '');
|
|
60
|
+
}
|
|
61
|
+
var da = parseInt(a.getAttribute('data-date') || '0', 10) || 0;
|
|
62
|
+
var db = parseInt(b.getAttribute('data-date') || '0', 10) || 0;
|
|
63
|
+
return state.sort === 'oldest' ? da - db : db - da;
|
|
64
|
+
});
|
|
65
|
+
ordered.forEach(function (el) { grid.appendChild(el); });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function apply() {
|
|
69
|
+
sortItems();
|
|
70
|
+
var visible = 0;
|
|
71
|
+
items.forEach(function (item) {
|
|
72
|
+
if (matches(item)) { item.classList.remove('d-none'); visible++; }
|
|
73
|
+
else { item.classList.add('d-none'); }
|
|
74
|
+
});
|
|
75
|
+
if (countEl) countEl.textContent = 'Showing ' + visible + ' of ' + total;
|
|
76
|
+
if (noResults) noResults.classList.toggle('d-none', visible !== 0);
|
|
77
|
+
var active = state.filter !== 'all' || state.query !== '' || state.tag !== null;
|
|
78
|
+
clearBtns.forEach(function (b) { b.hidden = !active; });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function setFilter(type) {
|
|
82
|
+
state.filter = type;
|
|
83
|
+
filterBtns.forEach(function (b) {
|
|
84
|
+
var on = b.getAttribute('data-filter') === type;
|
|
85
|
+
b.classList.toggle('is-active', on);
|
|
86
|
+
b.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
87
|
+
});
|
|
88
|
+
writeHash();
|
|
89
|
+
apply();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function setTag(tag) {
|
|
93
|
+
state.tag = (state.tag === tag) ? null : tag;
|
|
94
|
+
tagBtns.forEach(function (b) {
|
|
95
|
+
var on = b.getAttribute('data-tag-filter') === state.tag;
|
|
96
|
+
b.classList.toggle('is-active', on);
|
|
97
|
+
b.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
98
|
+
});
|
|
99
|
+
apply();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function clearAll() {
|
|
103
|
+
state.query = '';
|
|
104
|
+
state.tag = null;
|
|
105
|
+
if (searchInput) searchInput.value = '';
|
|
106
|
+
tagBtns.forEach(function (b) {
|
|
107
|
+
b.classList.remove('is-active');
|
|
108
|
+
b.setAttribute('aria-pressed', 'false');
|
|
109
|
+
});
|
|
110
|
+
setFilter('all'); // also re-applies + resets hash
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function writeHash() {
|
|
114
|
+
if (!window.history || !window.history.replaceState) return;
|
|
115
|
+
try {
|
|
116
|
+
if (state.filter && state.filter !== 'all') {
|
|
117
|
+
window.history.replaceState(null, '', '#type=' + state.filter);
|
|
118
|
+
} else {
|
|
119
|
+
window.history.replaceState(null, '', window.location.pathname + window.location.search);
|
|
120
|
+
}
|
|
121
|
+
} catch (e) { /* no-op */ }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function readHash() {
|
|
125
|
+
var m = /[#&]type=([a-z0-9_-]+)/i.exec(window.location.hash);
|
|
126
|
+
if (m && filterBtns.some(function (b) { return b.getAttribute('data-filter') === m[1]; })) {
|
|
127
|
+
setFilter(m[1]);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---- Wire events ----
|
|
134
|
+
filterBtns.forEach(function (b) {
|
|
135
|
+
b.addEventListener('click', function () { setFilter(b.getAttribute('data-filter')); });
|
|
136
|
+
});
|
|
137
|
+
tagBtns.forEach(function (b) {
|
|
138
|
+
b.addEventListener('click', function () { setTag(b.getAttribute('data-tag-filter')); });
|
|
139
|
+
});
|
|
140
|
+
clearBtns.forEach(function (b) { b.addEventListener('click', clearAll); });
|
|
141
|
+
if (sortSelect) {
|
|
142
|
+
sortSelect.addEventListener('change', function () { state.sort = sortSelect.value; apply(); });
|
|
143
|
+
}
|
|
144
|
+
if (searchInput) {
|
|
145
|
+
var t;
|
|
146
|
+
searchInput.addEventListener('input', function () {
|
|
147
|
+
clearTimeout(t);
|
|
148
|
+
t = setTimeout(function () {
|
|
149
|
+
state.query = searchInput.value.trim().toLowerCase();
|
|
150
|
+
apply();
|
|
151
|
+
}, 150);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
countUp(root);
|
|
156
|
+
|
|
157
|
+
// Deep link (e.g. /authors/bamr87/#type=docs); falls back to a plain apply.
|
|
158
|
+
if (!readHash()) apply();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Reduced-motion-aware count-up for the stat numbers.
|
|
162
|
+
function countUp(root) {
|
|
163
|
+
var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
164
|
+
var nums = root.querySelectorAll('.author-stat__num');
|
|
165
|
+
if (reduce || !nums.length || !window.requestAnimationFrame) return;
|
|
166
|
+
Array.prototype.forEach.call(nums, function (el) {
|
|
167
|
+
var target = parseInt(el.textContent, 10);
|
|
168
|
+
if (isNaN(target) || target <= 0) return;
|
|
169
|
+
var duration = 600, start = null;
|
|
170
|
+
el.textContent = '0';
|
|
171
|
+
function step(ts) {
|
|
172
|
+
if (start === null) start = ts;
|
|
173
|
+
var p = Math.min((ts - start) / duration, 1);
|
|
174
|
+
el.textContent = String(Math.round(p * target));
|
|
175
|
+
if (p < 1) window.requestAnimationFrame(step);
|
|
176
|
+
}
|
|
177
|
+
window.requestAnimationFrame(step);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function boot() {
|
|
182
|
+
Array.prototype.forEach.call(document.querySelectorAll('[data-author-profile]'), init);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (document.readyState === 'loading') {
|
|
186
|
+
document.addEventListener('DOMContentLoaded', boot);
|
|
187
|
+
} else {
|
|
188
|
+
boot();
|
|
189
|
+
}
|
|
190
|
+
})();
|
|
@@ -31,6 +31,8 @@ export class Navbar {
|
|
|
31
31
|
this._tooltips = [];
|
|
32
32
|
this._listeners = [];
|
|
33
33
|
this._resizeTimer = null;
|
|
34
|
+
this._fitTimer = null;
|
|
35
|
+
this._lastFitWarnings = new Set();
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
// -----------------------------------------------------------------
|
|
@@ -57,6 +59,7 @@ export class Navbar {
|
|
|
57
59
|
this._setupDropdownHoverDelay();
|
|
58
60
|
this._setupFocusTrap();
|
|
59
61
|
this._setupResponsiveReset();
|
|
62
|
+
this._setupFitWarnings();
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
destroy() {
|
|
@@ -277,4 +280,105 @@ export class Navbar {
|
|
|
277
280
|
}, 250);
|
|
278
281
|
});
|
|
279
282
|
}
|
|
283
|
+
|
|
284
|
+
// -----------------------------------------------------------------
|
|
285
|
+
// Dev-only constraint warnings
|
|
286
|
+
// -----------------------------------------------------------------
|
|
287
|
+
// Surfaces two failure modes that otherwise pass silently until a user
|
|
288
|
+
// hits a specific viewport: (1) the inline menubar has more items than can
|
|
289
|
+
// fit even icon-only, and (2) page content overflows the viewport, which
|
|
290
|
+
// makes the fixed-top navbar look "cut off" on the right. Logged to the
|
|
291
|
+
// console only on local/dev hosts — never on a deployed (e.g. *.github.io)
|
|
292
|
+
// site — so production consoles stay clean.
|
|
293
|
+
// -----------------------------------------------------------------
|
|
294
|
+
_isDevHost() {
|
|
295
|
+
try {
|
|
296
|
+
const h = window.location.hostname;
|
|
297
|
+
return h === 'localhost' || h === '127.0.0.1' || h === '0.0.0.0' ||
|
|
298
|
+
h === '::1' || h === '[::1]' || h === '' ||
|
|
299
|
+
/\.(local|test|localhost)$/.test(h);
|
|
300
|
+
} catch (e) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
_setupFitWarnings() {
|
|
306
|
+
if (!this._isDevHost()) return;
|
|
307
|
+
const run = () => this._checkFit();
|
|
308
|
+
// Let first paint + async chrome settle before the first check.
|
|
309
|
+
setTimeout(run, 600);
|
|
310
|
+
this._on(window, 'resize', () => {
|
|
311
|
+
clearTimeout(this._fitTimer);
|
|
312
|
+
this._fitTimer = setTimeout(run, 400);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Emit a warning at most once per distinct message until conditions change. */
|
|
317
|
+
_warnOnce(key, message, ...extra) {
|
|
318
|
+
if (this._lastFitWarnings.has(key)) return;
|
|
319
|
+
this._lastFitWarnings.add(key);
|
|
320
|
+
console.warn(message, ...extra);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
_checkFit() {
|
|
324
|
+
const PREFIX = '[zer0-mistakes navbar]';
|
|
325
|
+
const vw = window.innerWidth;
|
|
326
|
+
// Re-evaluate from scratch each pass so a fixed layout stops warning.
|
|
327
|
+
this._lastFitWarnings.clear();
|
|
328
|
+
|
|
329
|
+
// 1) Inline menubar overflow at lg+ — more/wider items than the track holds.
|
|
330
|
+
if (vw >= this.config.breakpoints.lg) {
|
|
331
|
+
const navList = document.querySelector('#bdNavbar .navbar-nav');
|
|
332
|
+
if (navList && navList.scrollWidth > navList.clientWidth + 2) {
|
|
333
|
+
const items = navList.querySelectorAll(':scope > li:not(.d-lg-none)').length;
|
|
334
|
+
this._warnOnce(
|
|
335
|
+
'menu-overflow',
|
|
336
|
+
`${PREFIX} Top navigation overflows the bar at ${vw}px (~${items} ` +
|
|
337
|
+
`visible items). Items are already icon-only here and may still clip. ` +
|
|
338
|
+
`Reduce top-level entries, shorten titles, or group them under ` +
|
|
339
|
+
`dropdowns in _data/navigation/main.yml.`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// 2) Page-level horizontal overflow (the usual "navbar looks cut off" cause).
|
|
345
|
+
// `main.scrollWidth` only exceeds its clientWidth when something overflows
|
|
346
|
+
// WITHOUT its own scroll container — locally-scrollable wide content
|
|
347
|
+
// (tables/code) does not trip this. Cheap gate before the DOM scan.
|
|
348
|
+
const main = document.getElementById('main-content');
|
|
349
|
+
if (main && main.scrollWidth > main.clientWidth + 2) {
|
|
350
|
+
const cw = document.documentElement.clientWidth;
|
|
351
|
+
let worst = null;
|
|
352
|
+
main.querySelectorAll('*').forEach((el) => {
|
|
353
|
+
const r = el.getBoundingClientRect();
|
|
354
|
+
if (r.width === 0 || r.right <= cw + 2) return;
|
|
355
|
+
if (getComputedStyle(el).position === 'fixed') return;
|
|
356
|
+
// Overflow contained by an ancestor clip/scroll box never reaches
|
|
357
|
+
// the page. Exclude the root <html> clip so we still report what
|
|
358
|
+
// the safety net is hiding.
|
|
359
|
+
let contained = false, n = el.parentElement;
|
|
360
|
+
while (n && n !== document.body && n !== document.documentElement) {
|
|
361
|
+
if (getComputedStyle(n).overflowX !== 'visible') { contained = true; break; }
|
|
362
|
+
n = n.parentElement;
|
|
363
|
+
}
|
|
364
|
+
if (contained) return;
|
|
365
|
+
if (!worst || r.right > worst.right) worst = { el, right: r.right };
|
|
366
|
+
});
|
|
367
|
+
if (worst) {
|
|
368
|
+
const el = worst.el;
|
|
369
|
+
const sel = el.tagName.toLowerCase() +
|
|
370
|
+
(el.id ? `#${el.id}` : '') +
|
|
371
|
+
(el.className && typeof el.className === 'string'
|
|
372
|
+
? '.' + el.className.trim().split(/\s+/).slice(0, 2).join('.') : '');
|
|
373
|
+
this._warnOnce(
|
|
374
|
+
'page-overflow',
|
|
375
|
+
`${PREFIX} Content overflows the viewport by ` +
|
|
376
|
+
`${Math.round(worst.right - cw)}px at ${vw}px (widest: ${sel}). This ` +
|
|
377
|
+
`forces a horizontal scrollbar and can make the fixed navbar look cut ` +
|
|
378
|
+
`off. Wrap wide content in a scroll container or a Bootstrap .container.`,
|
|
379
|
+
el
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
280
384
|
}
|
data/assets/js/obsidian-graph.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* /assets/data/wiki-index.json into the element with id `obsidian-graph`.
|
|
6
6
|
*
|
|
7
7
|
* Loaded only on the graph page (pages/_docs/obsidian/graph.md), which
|
|
8
|
-
* also pulls in cytoscape.js
|
|
9
|
-
* added to the rest of the site.
|
|
8
|
+
* also pulls in the vendored cytoscape.js (assets/vendor/cytoscape/). No
|
|
9
|
+
* runtime CDN dependencies are added to the rest of the site.
|
|
10
10
|
*
|
|
11
11
|
* Nodes: one per indexed entry (collection doc or standalone page)
|
|
12
12
|
* Edges: directed, source -> target, derived from `entry.outgoing`
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Loaded by _includes/navigation/local-graph.html inside a dedicated
|
|
9
9
|
* collapsible side panel. Cytoscape.js is loaded lazily (and only once) from
|
|
10
|
-
* the
|
|
10
|
+
* the vendored copy under assets/vendor/cytoscape/ (no CDN).
|
|
11
11
|
*
|
|
12
12
|
* Subgraph:
|
|
13
13
|
* - center = current page (matched against entry.url, falling back to
|
|
@@ -26,8 +26,16 @@
|
|
|
26
26
|
var PANEL_SELECTOR = '[data-obsidian-local-graph-panel]';
|
|
27
27
|
var TOGGLE_SELECTOR = '[data-obsidian-local-graph-toggle]';
|
|
28
28
|
var STATUS_SELECTOR = '[data-obsidian-local-graph-status]';
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
// Cytoscape is vendored locally (assets/vendor/cytoscape/) — no CDN, so the
|
|
30
|
+
// graph works under strict CSP and offline. Resolve the vendored URL from this
|
|
31
|
+
// script's own (trusted) src so it stays correct under any site baseurl,
|
|
32
|
+
// without flowing DOM-supplied text into a script element. See issues #152, #205.
|
|
33
|
+
var SELF_SRC = (document.currentScript && document.currentScript.src) || '';
|
|
34
|
+
var CYTOSCAPE_URL = '/assets/vendor/cytoscape/cytoscape.min.js';
|
|
35
|
+
var _selfMatch = SELF_SRC.match(/^(.*\/)assets\/js\/obsidian-local-graph\.js(?:[?#].*)?$/);
|
|
36
|
+
if (_selfMatch) {
|
|
37
|
+
CYTOSCAPE_URL = _selfMatch[1] + 'assets/vendor/cytoscape/cytoscape.min.js';
|
|
38
|
+
}
|
|
31
39
|
|
|
32
40
|
function companionElements(container) {
|
|
33
41
|
return {
|
|
@@ -256,8 +264,6 @@
|
|
|
256
264
|
}
|
|
257
265
|
var s = document.createElement('script');
|
|
258
266
|
s.src = CYTOSCAPE_URL;
|
|
259
|
-
s.integrity = CYTOSCAPE_SRI;
|
|
260
|
-
s.crossOrigin = 'anonymous';
|
|
261
267
|
s.defer = true;
|
|
262
268
|
s.onload = function () {
|
|
263
269
|
window.__obsidianCytoscapeLoading.forEach(function (fn) { fn(); });
|