jekyll-theme-zer0 0.22.19 → 0.22.20
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 +9 -0
- data/README.md +4 -4
- data/_includes/components/cookie-consent.html +35 -100
- data/_includes/components/nanobar.html +117 -0
- data/_includes/core/footer.html +9 -11
- data/_includes/core/head.html +4 -25
- data/_includes/core/header.html +16 -10
- data/_includes/navigation/navbar.html +2 -1
- data/_layouts/landing.html +14 -8
- data/_sass/core/_navbar.scss +6 -0
- data/_sass/custom.scss +59 -4
- data/assets/js/nanobar-init.js +63 -0
- data/scripts/lint-pages +19 -16
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 980b31bacaacb76414bff954016a5a98c83e175ec3ef2361e15e1ec175718073
|
|
4
|
+
data.tar.gz: a2b223a9ff562ce39b12a3106ca343c62c0d76060d50550576ee853504ece84f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 16db6007e716e176b0188981a391b8aafab303f646c900a110e5386ea68eb6aeea8fcbb542d5529df1309bcf9518a8adbbcc47eaf85b2625b42eadce9544a121
|
|
7
|
+
data.tar.gz: a2ea2ab6d1caf59e13a9d14e80e4ecc0870ff13c80f5eefa703de8416f289e9f382c23f9f34cfdddba08ee8186f3b3fcb73fd995bfaa559b01b032add3e56b90
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.22.20] - 2026-04-19
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Version bump: patch release
|
|
7
|
+
|
|
8
|
+
### Commits in this release
|
|
9
|
+
- f5d5e97 fix(ui): UI/UX fixes — navbar dropdown, landing hero, cookie banner, nanobar, footer (#72)
|
|
10
|
+
|
|
11
|
+
|
|
3
12
|
## [0.22.19] - 2026-04-18
|
|
4
13
|
|
|
5
14
|
### Changed
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
title: zer0-mistakes
|
|
3
3
|
sub-title: Jekyll Theme
|
|
4
4
|
description: GitHub Pages compatible Jekyll theme with Bootstrap 5.3, featuring automated installation and comprehensive documentation.
|
|
5
|
-
version: 0.22.
|
|
5
|
+
version: 0.22.20
|
|
6
6
|
layout: landing
|
|
7
7
|
tags:
|
|
8
8
|
- jekyll
|
|
@@ -14,7 +14,7 @@ categories:
|
|
|
14
14
|
- docker
|
|
15
15
|
- bootstrap
|
|
16
16
|
created: 2024-02-10T23:51:11.480Z
|
|
17
|
-
lastmod: 2026-04-
|
|
17
|
+
lastmod: 2026-04-19T16:29:09.000Z
|
|
18
18
|
draft: false
|
|
19
19
|
permalink: /
|
|
20
20
|
slug: zer0
|
|
@@ -789,7 +789,7 @@ git push origin feature/awesome-feature
|
|
|
789
789
|
|
|
790
790
|
| Metric | Value |
|
|
791
791
|
|--------|-------|
|
|
792
|
-
| **Current Version** | 0.22.
|
|
792
|
+
| **Current Version** | 0.22.20 ([RubyGems](https://rubygems.org/gems/jekyll-theme-zer0), [CHANGELOG](/CHANGELOG)) |
|
|
793
793
|
| **Documented Features** | 43 ([Feature Registry](https://github.com/bamr87/zer0-mistakes/blob/main/_data/features.yml)) |
|
|
794
794
|
| **Setup Time** | 2-5 minutes ([install.sh benchmarks](https://github.com/bamr87/zer0-mistakes/blob/main/install.sh)) |
|
|
795
795
|
| **Documentation Pages** | 70+ ([browse docs](/pages/)) |
|
|
@@ -832,6 +832,6 @@ Built with these amazing technologies:
|
|
|
832
832
|
|
|
833
833
|
**Built with ❤️ for the Jekyll community**
|
|
834
834
|
|
|
835
|
-
**v0.22.
|
|
835
|
+
**v0.22.20** • [Changelog](CHANGELOG.md) • [License](LICENSE) • [Contributing](CONTRIBUTING.md)
|
|
836
836
|
|
|
837
837
|
|
|
@@ -49,7 +49,7 @@ Usage: Include in root.html layout
|
|
|
49
49
|
Configuration: Uses site.posthog settings from _config.yml
|
|
50
50
|
{% endcomment %}
|
|
51
51
|
|
|
52
|
-
<div id="cookieConsent" class="cookie-consent-banner position-fixed bottom-0 start-0 end-0 bg-dark text-light py-3 px-3 shadow-lg">
|
|
52
|
+
<div id="cookieConsent" class="cookie-consent-banner position-fixed bottom-0 start-0 end-0 bg-dark text-light py-3 px-3 shadow-lg" hidden>
|
|
53
53
|
<div class="container-xl px-3 px-md-4">
|
|
54
54
|
<div class="row align-items-center g-2">
|
|
55
55
|
<div class="col-12 col-lg-8">
|
|
@@ -249,42 +249,41 @@ Configuration: Uses site.posthog settings from _config.yml
|
|
|
249
249
|
function showConsentBanner() {
|
|
250
250
|
const banner = document.getElementById('cookieConsent');
|
|
251
251
|
if (!banner) return;
|
|
252
|
-
|
|
253
|
-
//
|
|
254
|
-
banner.
|
|
255
|
-
banner.
|
|
256
|
-
|
|
257
|
-
// Wait for the delay, then show and animate in one smooth motion
|
|
252
|
+
|
|
253
|
+
// Reveal the element (CSS keeps it translated off-screen + opacity:0).
|
|
254
|
+
banner.hidden = false;
|
|
255
|
+
banner.classList.remove('cookie-banner-visible');
|
|
256
|
+
|
|
258
257
|
setTimeout(() => {
|
|
259
|
-
//
|
|
260
|
-
banner.classList.add('cookie-banner-showing');
|
|
261
|
-
|
|
262
|
-
// Force a reflow to ensure the display change is applied
|
|
258
|
+
// Force reflow so the next class change actually animates.
|
|
263
259
|
void banner.offsetHeight;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
requestAnimationFrame(() => {
|
|
267
|
-
requestAnimationFrame(() => {
|
|
268
|
-
banner.classList.add('cookie-banner-visible');
|
|
269
|
-
});
|
|
270
|
-
});
|
|
271
|
-
}, 1000); // Show after 1 second
|
|
260
|
+
banner.classList.add('cookie-banner-visible');
|
|
261
|
+
}, 1000);
|
|
272
262
|
}
|
|
273
263
|
|
|
274
264
|
// Hide consent banner
|
|
275
265
|
function hideConsentBanner() {
|
|
276
266
|
const banner = document.getElementById('cookieConsent');
|
|
277
|
-
if (banner)
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
267
|
+
if (!banner) return;
|
|
268
|
+
|
|
269
|
+
banner.classList.remove('cookie-banner-visible');
|
|
270
|
+
|
|
271
|
+
let done = false;
|
|
272
|
+
const finish = () => {
|
|
273
|
+
if (done) return;
|
|
274
|
+
done = true;
|
|
275
|
+
banner.hidden = true;
|
|
276
|
+
banner.removeEventListener('transitionend', onEnd);
|
|
277
|
+
};
|
|
278
|
+
const onEnd = (e) => {
|
|
279
|
+
if (e.target !== banner) return;
|
|
280
|
+
if (e.propertyName !== 'transform' && e.propertyName !== 'opacity') return;
|
|
281
|
+
finish();
|
|
282
|
+
};
|
|
283
|
+
banner.addEventListener('transitionend', onEnd);
|
|
284
|
+
// Fallback in case transitionend never fires (reduced-motion, interrupted transition,
|
|
285
|
+
// background tab, etc.). Slightly longer than the longest CSS transition (0.4s).
|
|
286
|
+
setTimeout(finish, 500);
|
|
288
287
|
}
|
|
289
288
|
|
|
290
289
|
// Update modal UI with current preferences
|
|
@@ -306,26 +305,13 @@ Configuration: Uses site.posthog settings from _config.yml
|
|
|
306
305
|
// Initialize on DOM content loaded
|
|
307
306
|
document.addEventListener('DOMContentLoaded', function() {
|
|
308
307
|
const banner = document.getElementById('cookieConsent');
|
|
309
|
-
|
|
310
|
-
// Ensure banner starts completely hidden BEFORE any checks
|
|
311
|
-
// Remove all classes and inline styles - let CSS handle hiding
|
|
312
|
-
if (banner) {
|
|
313
|
-
banner.classList.remove('cookie-banner-showing', 'cookie-banner-visible');
|
|
314
|
-
banner.removeAttribute('style');
|
|
315
|
-
}
|
|
316
|
-
|
|
317
308
|
const existingConsent = getConsentState();
|
|
318
|
-
|
|
309
|
+
|
|
319
310
|
if (existingConsent) {
|
|
320
|
-
// Apply existing consent and ensure banner stays hidden
|
|
321
311
|
applyConsent(existingConsent);
|
|
322
312
|
updateModalUI(existingConsent);
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
banner.removeAttribute('style');
|
|
326
|
-
}
|
|
327
|
-
} else {
|
|
328
|
-
// Show consent banner for new visitors (with delay)
|
|
313
|
+
// Banner already starts hidden via the `hidden` attribute — nothing to do.
|
|
314
|
+
} else if (banner) {
|
|
329
315
|
showConsentBanner();
|
|
330
316
|
}
|
|
331
317
|
|
|
@@ -385,70 +371,19 @@ Configuration: Uses site.posthog settings from _config.yml
|
|
|
385
371
|
</script>
|
|
386
372
|
|
|
387
373
|
<style>
|
|
388
|
-
/*
|
|
389
|
-
#cookieConsent {
|
|
390
|
-
z-index: 9999;
|
|
391
|
-
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
/* Banner starts completely hidden - CSS enforces this */
|
|
395
|
-
#cookieConsent:not(.cookie-banner-showing) {
|
|
396
|
-
display: none !important;
|
|
397
|
-
visibility: hidden !important;
|
|
398
|
-
opacity: 0 !important;
|
|
399
|
-
transform: translateY(100%) !important;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
.cookie-consent-banner {
|
|
403
|
-
background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
|
|
404
|
-
backdrop-filter: blur(10px);
|
|
405
|
-
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/* Only show banner when explicitly enabled via JavaScript class */
|
|
409
|
-
#cookieConsent.cookie-banner-showing {
|
|
410
|
-
display: block !important;
|
|
411
|
-
visibility: visible !important;
|
|
412
|
-
opacity: 0;
|
|
413
|
-
transform: translateY(100%);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/* Animate in when visible class is added */
|
|
417
|
-
#cookieConsent.cookie-banner-showing.cookie-banner-visible {
|
|
418
|
-
opacity: 1 !important;
|
|
419
|
-
transform: translateY(0) !important;
|
|
420
|
-
}
|
|
421
|
-
|
|
374
|
+
/* Modal-only helpers; banner styling lives in _sass/custom.scss */
|
|
422
375
|
.cursor-pointer {
|
|
423
376
|
cursor: pointer;
|
|
424
377
|
}
|
|
425
|
-
|
|
378
|
+
|
|
426
379
|
.cookie-category {
|
|
427
380
|
border: 1px solid #dee2e6;
|
|
428
381
|
border-radius: 8px;
|
|
429
382
|
padding: 1rem;
|
|
430
383
|
}
|
|
431
|
-
|
|
384
|
+
|
|
432
385
|
.form-check-input:checked {
|
|
433
386
|
background-color: var(--bs-success);
|
|
434
387
|
border-color: var(--bs-success);
|
|
435
388
|
}
|
|
436
|
-
|
|
437
|
-
@media (max-width: 768px) {
|
|
438
|
-
.cookie-consent-banner .btn {
|
|
439
|
-
width: 100%;
|
|
440
|
-
margin-bottom: 0.5rem;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
.cookie-consent-banner .btn:last-child {
|
|
444
|
-
margin-bottom: 0;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/* Respect reduced motion preferences */
|
|
449
|
-
@media (prefers-reduced-motion: reduce) {
|
|
450
|
-
.cookie-consent-banner {
|
|
451
|
-
transition: none !important;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
389
|
</style>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
===================================================================
|
|
3
|
+
NANOBAR - Config-Driven Page Loading Progress Bar
|
|
4
|
+
===================================================================
|
|
5
|
+
|
|
6
|
+
File: nanobar.html
|
|
7
|
+
Path: _includes/components/nanobar.html
|
|
8
|
+
Purpose: Visual loading indicator shown on every page load,
|
|
9
|
+
fully configurable via site.nanobar in _config.yml
|
|
10
|
+
|
|
11
|
+
Template Logic:
|
|
12
|
+
- Conditionally renders only when site.nanobar.enabled != false
|
|
13
|
+
- Injects CSS custom properties from _config.yml values
|
|
14
|
+
- Loads nanobar.min.js (library) and nanobar-init.js (initializer)
|
|
15
|
+
- Passes config to JS via window.zer0Nanobar object
|
|
16
|
+
|
|
17
|
+
Dependencies:
|
|
18
|
+
- assets/js/nanobar.min.js — third-party Nanobar library
|
|
19
|
+
- assets/js/nanobar-init.js — theme initializer script
|
|
20
|
+
- _includes/core/header.html — #top-progress-target mount point
|
|
21
|
+
(rendered only when position == "navbar")
|
|
22
|
+
|
|
23
|
+
Configuration (_config.yml → nanobar):
|
|
24
|
+
enabled : true | false # master switch
|
|
25
|
+
color : CSS color value # bar fill colour
|
|
26
|
+
background : CSS color value # track background
|
|
27
|
+
height : CSS length # bar thickness
|
|
28
|
+
position : top | bottom | navbar # placement mode
|
|
29
|
+
z_index : integer # stacking order
|
|
30
|
+
steps : [int, …] # progress percentages
|
|
31
|
+
step_delay_ms : integer # ms between steps
|
|
32
|
+
classname : string # CSS class on wrapper
|
|
33
|
+
id : string # DOM id
|
|
34
|
+
target : CSS selector | "" # explicit mount target
|
|
35
|
+
|
|
36
|
+
Performance Notes:
|
|
37
|
+
- Both scripts loaded with `defer` to avoid blocking rendering
|
|
38
|
+
- CSS custom properties keep the inline <style> minimal
|
|
39
|
+
- Library injects its own baseline CSS; theme overrides via
|
|
40
|
+
specificity and !important where needed
|
|
41
|
+
|
|
42
|
+
Library: https://github.com/jacoborus/nanobar
|
|
43
|
+
===================================================================
|
|
44
|
+
-->
|
|
45
|
+
|
|
46
|
+
{%- assign nb = site.nanobar | default: empty -%}
|
|
47
|
+
{%- if nb == empty or nb.enabled != false -%}
|
|
48
|
+
|
|
49
|
+
{%- comment -%} ── Resolve config with safe defaults ── {%- endcomment -%}
|
|
50
|
+
{%- assign nb_color = nb.color | default: "var(--bs-primary)" -%}
|
|
51
|
+
{%- assign nb_bg = nb.background | default: "transparent" -%}
|
|
52
|
+
{%- assign nb_height = nb.height | default: "3px" -%}
|
|
53
|
+
{%- assign nb_position = nb.position | default: "top" -%}
|
|
54
|
+
{%- assign nb_zindex = nb.z_index | default: 9999 -%}
|
|
55
|
+
{%- assign nb_classname = nb.classname | default: "nanobar" -%}
|
|
56
|
+
{%- assign nb_id = nb.id | default: "top-progress-bar" -%}
|
|
57
|
+
{%- assign nb_target = nb.target | default: "" -%}
|
|
58
|
+
{%- assign nb_step_delay = nb.step_delay_ms | default: 0 -%}
|
|
59
|
+
{%- assign nb_steps = nb.steps -%}
|
|
60
|
+
{%- if nb_steps == nil or nb_steps == empty -%}
|
|
61
|
+
{%- assign nb_steps = "30,76,100" | split: "," -%}
|
|
62
|
+
{%- endif -%}
|
|
63
|
+
|
|
64
|
+
<!-- ── Nanobar theme CSS (config-driven custom properties) ── -->
|
|
65
|
+
<style id="nanobar-theme">
|
|
66
|
+
:root {
|
|
67
|
+
--nanobar-color: {{ nb_color }};
|
|
68
|
+
--nanobar-bg: {{ nb_bg }};
|
|
69
|
+
--nanobar-height: {{ nb_height }};
|
|
70
|
+
--nanobar-z: {{ nb_zindex }};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* Override library defaults with theme values */
|
|
74
|
+
.{{ nb_classname }} {
|
|
75
|
+
height: var(--nanobar-height) !important;
|
|
76
|
+
background: var(--nanobar-bg);
|
|
77
|
+
z-index: var(--nanobar-z);
|
|
78
|
+
}
|
|
79
|
+
.{{ nb_classname }} .bar {
|
|
80
|
+
background: var(--nanobar-color) !important;
|
|
81
|
+
transition: width .2s ease, height .3s ease;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* Position modifiers */
|
|
85
|
+
.{{ nb_classname }}--bottom { top: auto !important; bottom: 0; }
|
|
86
|
+
|
|
87
|
+
/* Navbar mount: inline strip under the header */
|
|
88
|
+
.nanobar-mount {
|
|
89
|
+
position: relative;
|
|
90
|
+
width: 100%;
|
|
91
|
+
height: var(--nanobar-height);
|
|
92
|
+
overflow: hidden;
|
|
93
|
+
}
|
|
94
|
+
.nanobar-mount > .{{ nb_classname }}--navbar {
|
|
95
|
+
position: absolute !important;
|
|
96
|
+
top: 0; left: 0; right: 0;
|
|
97
|
+
width: 100%;
|
|
98
|
+
}
|
|
99
|
+
</style>
|
|
100
|
+
|
|
101
|
+
<!-- ── Nanobar scripts ── -->
|
|
102
|
+
<script defer src="{{ '/assets/js/nanobar.min.js' | relative_url }}"></script>
|
|
103
|
+
<script defer src="{{ '/assets/js/nanobar-init.js' | relative_url }}"></script>
|
|
104
|
+
|
|
105
|
+
<!-- ── Bridge _config.yml values to JS ── -->
|
|
106
|
+
<script>
|
|
107
|
+
window.zer0Nanobar = {
|
|
108
|
+
classname: {{ nb_classname | jsonify }},
|
|
109
|
+
id: {{ nb_id | jsonify }},
|
|
110
|
+
position: {{ nb_position | jsonify }},
|
|
111
|
+
target: {{ nb_target | jsonify }},
|
|
112
|
+
steps: [{% for s in nb_steps %}{{ s | strip | plus: 0 }}{% unless forloop.last %},{% endunless %}{% endfor %}],
|
|
113
|
+
stepDelay: {{ nb_step_delay | plus: 0 }}
|
|
114
|
+
};
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
{%- endif -%}
|
data/_includes/core/footer.html
CHANGED
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
===================================================================
|
|
31
31
|
-->
|
|
32
32
|
|
|
33
|
-
<footer class="bd-footer
|
|
33
|
+
<footer class="bd-footer border-top" role="contentinfo">
|
|
34
34
|
<!-- Powered by Row -->
|
|
35
|
-
<div class="container
|
|
36
|
-
<ul class="nav
|
|
35
|
+
<div class="container-xl my-3">
|
|
36
|
+
<ul class="nav justify-content-end list-unstyled d-flex align-items-center flex-wrap" aria-label="Powered by technologies">
|
|
37
37
|
<span class="align-start">
|
|
38
38
|
© {{ site.time | date: "%Y" }} {{ site.name | default: site.title }} — Powered by:
|
|
39
39
|
</span>
|
|
@@ -67,10 +67,9 @@
|
|
|
67
67
|
</ul>
|
|
68
68
|
</div>
|
|
69
69
|
|
|
70
|
-
<!-- Branding and Navigation Block -->
|
|
71
|
-
<div class="
|
|
72
|
-
<div class="container
|
|
73
|
-
<div class="container">
|
|
70
|
+
<!-- Branding and Navigation Block — full-width dark background -->
|
|
71
|
+
<div class="bg-dark text-light py-5">
|
|
72
|
+
<div class="container-xl">
|
|
74
73
|
<!-- Top: Site info + Quick Links + Social -->
|
|
75
74
|
<div class="row mb-4">
|
|
76
75
|
<!-- Site Info -->
|
|
@@ -163,10 +162,9 @@
|
|
|
163
162
|
</div>
|
|
164
163
|
</div>
|
|
165
164
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
</div>
|
|
165
|
+
<!-- Bottom: Copyright -->
|
|
166
|
+
<div class="text-center">
|
|
167
|
+
<p class="mb-0">© {{ site.time | date: "%Y" }} {{ site.title | default: site.name }}. All Rights Reserved.</p>
|
|
170
168
|
</div>
|
|
171
169
|
</div>
|
|
172
170
|
</div>
|
data/_includes/core/head.html
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
- Loads critical JavaScript libraries before page render
|
|
13
13
|
- Includes SEO optimization and social media meta tags
|
|
14
14
|
- Configures third-party integrations (Analytics, MathJax)
|
|
15
|
-
- Sets up progress bar
|
|
15
|
+
- Sets up progress bar via components/nanobar.html include
|
|
16
16
|
|
|
17
17
|
Dependencies:
|
|
18
18
|
- seo.html: SEO meta tags and Open Graph data
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
Performance Notes:
|
|
23
23
|
- Scripts loaded in head for immediate availability
|
|
24
24
|
- MathJax loaded asynchronously to prevent render blocking
|
|
25
|
-
- Nanobar provides visual loading feedback
|
|
25
|
+
- Nanobar provides visual loading feedback (see components/nanobar.html)
|
|
26
26
|
===================================================================
|
|
27
27
|
-->
|
|
28
28
|
|
|
@@ -52,29 +52,8 @@
|
|
|
52
52
|
{% include components/mermaid.html %}
|
|
53
53
|
{% endif %}
|
|
54
54
|
|
|
55
|
-
<!-- Nano Progress Bar - Visual loading indicator -->
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
<!-- Progress Bar Initialization - Creates visual loading feedback -->
|
|
59
|
-
<script>
|
|
60
|
-
// Wait for DOM to be ready before initializing Nanobar
|
|
61
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
62
|
-
// Check if the progress bar element exists before initializing
|
|
63
|
-
var progressElement = document.getElementById('top-progress-bar');
|
|
64
|
-
if (progressElement) {
|
|
65
|
-
var options = {
|
|
66
|
-
classname: 'nanobar',
|
|
67
|
-
id: 'top-progress-bar'
|
|
68
|
-
};
|
|
69
|
-
var nanobar = new Nanobar(options);
|
|
70
|
-
nanobar.go( 30 ); // Initial loading state
|
|
71
|
-
nanobar.go( 76 ); // Partial completion
|
|
72
|
-
nanobar.go(100); // Complete loading
|
|
73
|
-
} else {
|
|
74
|
-
console.warn('Progress bar element #top-progress-bar not found. Skipping Nanobar initialization.');
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
</script>
|
|
55
|
+
<!-- Nano Progress Bar - Visual loading indicator (config-driven) -->
|
|
56
|
+
{% include components/nanobar.html %}
|
|
78
57
|
|
|
79
58
|
<!-- MathJax - Mathematical notation rendering (async; bundled under assets/vendor) -->
|
|
80
59
|
<script id="MathJax-script" async src="{{ '/assets/vendor/mathjax/es5/tex-mml-chtml.js' | relative_url }}"></script>
|
data/_includes/core/header.html
CHANGED
|
@@ -30,7 +30,10 @@
|
|
|
30
30
|
- ARIA labels for screen readers
|
|
31
31
|
- Focus management for offcanvas
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
Nanobar:
|
|
34
|
+
- Configured via `site.nanobar.*` in _config.yml
|
|
35
|
+
- When `position: navbar`, the bar mounts inside `#top-progress-target` below
|
|
36
|
+
- Otherwise it floats fixed to the viewport (top or bottom)
|
|
34
37
|
===================================================================
|
|
35
38
|
-->
|
|
36
39
|
|
|
@@ -48,15 +51,7 @@
|
|
|
48
51
|
<!-- TOP NAVIGATION BAR -->
|
|
49
52
|
<!-- ================================ -->
|
|
50
53
|
<div class="navbar navbar-expand-lg bg-body-tertiary flex-nowrap justify-content-center bottom-shadow">
|
|
51
|
-
|
|
52
|
-
<!-- ========================== -->
|
|
53
|
-
<!-- PROGRESS BAR INDICATOR -->
|
|
54
|
-
<!-- ========================== -->
|
|
55
|
-
<!-- Fixed position progress bar for page loading feedback -->
|
|
56
|
-
<div class="nanobar" id="top-progress-bar" style="position: fixed;" role="progressbar" aria-label="Page loading progress" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
|
|
57
|
-
<div class="bar"></div>
|
|
58
|
-
</div>
|
|
59
|
-
|
|
54
|
+
|
|
60
55
|
<!-- ========================== -->
|
|
61
56
|
<!-- MAIN NAVIGATION CONTAINER -->
|
|
62
57
|
<!-- ========================== -->
|
|
@@ -186,4 +181,15 @@
|
|
|
186
181
|
</div>
|
|
187
182
|
</nav>
|
|
188
183
|
</div>
|
|
184
|
+
|
|
185
|
+
<!-- ========================== -->
|
|
186
|
+
<!-- PROGRESS BAR MOUNT POINT -->
|
|
187
|
+
<!-- ========================== -->
|
|
188
|
+
<!-- Nanobar mounts here when site.nanobar.position == "navbar" so the bar
|
|
189
|
+
renders as a thin strip directly under the header. For "top" / "bottom"
|
|
190
|
+
the bar floats fixed to the viewport and no mount is needed. -->
|
|
191
|
+
{%- assign _nb_pos = site.nanobar.position | default: "top" -%}
|
|
192
|
+
{%- if site.nanobar.enabled != false and _nb_pos == "navbar" -%}
|
|
193
|
+
<div id="top-progress-target" class="nanobar-mount" aria-hidden="true"></div>
|
|
194
|
+
{%- endif -%}
|
|
189
195
|
</header>
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|
{%- for link in nav_main -%}
|
|
40
40
|
{%- assign has_children = link.children and link.children.size > 0 -%}
|
|
41
|
+
{%- assign items_remaining = nav_main.size | minus: forloop.index -%}
|
|
41
42
|
|
|
42
43
|
{%- if has_children -%}
|
|
43
44
|
<li class="nav-item dropdown d-flex align-items-center nav-hover-dropdown" role="none">
|
|
@@ -69,7 +70,7 @@
|
|
|
69
70
|
<span class="visually-hidden">Toggle {{ link.title }} submenu</span>
|
|
70
71
|
</button>
|
|
71
72
|
|
|
72
|
-
<ul class="dropdown-menu dropdown-menu-start" aria-labelledby="dropdown-{{ link.title | slugify }}" role="menu">
|
|
73
|
+
<ul class="dropdown-menu {% if items_remaining < 2 %}dropdown-menu-end{% else %}dropdown-menu-start{% endif %}" aria-labelledby="dropdown-{{ link.title | slugify }}" role="menu">
|
|
73
74
|
{%- for child in link.children -%}
|
|
74
75
|
<li role="none">
|
|
75
76
|
<a
|
data/_layouts/landing.html
CHANGED
|
@@ -66,26 +66,32 @@ layout: root
|
|
|
66
66
|
</div>
|
|
67
67
|
</div>
|
|
68
68
|
<div class="col-lg-6 text-center order-2">
|
|
69
|
+
{%- comment -%}
|
|
70
|
+
Inline `aspect-ratio` + `max-width` reserve the final box size BEFORE
|
|
71
|
+
external CSS parses, eliminating the post-load "jerk" caused by the
|
|
72
|
+
vendored Bootstrap `.ratio` class loading after first paint.
|
|
73
|
+
{%- endcomment -%}
|
|
69
74
|
{% if page.hero_image %}
|
|
70
|
-
<div class="landing-hero-media
|
|
75
|
+
<div class="landing-hero-media mx-auto shadow-lg rounded overflow-hidden"
|
|
76
|
+
style="aspect-ratio: 4 / 3; max-width: min(100%, 28rem); width: 100%;">
|
|
71
77
|
<img
|
|
72
78
|
src="{{ page.hero_image | relative_url }}"
|
|
73
79
|
alt="{{ page.title }}"
|
|
74
|
-
class="w-100 h-100"
|
|
80
|
+
class="landing-hero-img w-100 h-100"
|
|
75
81
|
width="800"
|
|
76
82
|
height="600"
|
|
77
83
|
loading="eager"
|
|
78
84
|
decoding="async"
|
|
79
85
|
fetchpriority="high"
|
|
80
|
-
|
|
86
|
+
onload="this.classList.add('is-loaded')"
|
|
87
|
+
style="object-fit: contain; display: block;"
|
|
81
88
|
>
|
|
82
89
|
</div>
|
|
83
90
|
{% else %}
|
|
84
|
-
<div class="landing-hero-media
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
</div>
|
|
91
|
+
<div class="landing-hero-media mx-auto shadow-lg rounded overflow-hidden d-flex flex-column align-items-center justify-content-center bg-secondary bg-opacity-25 text-body"
|
|
92
|
+
style="aspect-ratio: 4 / 3; max-width: min(100%, 28rem); width: 100%;">
|
|
93
|
+
<i class="bi bi-code-square display-1"></i>
|
|
94
|
+
<p class="mt-3 mb-0 px-3">Jekyll Theme</p>
|
|
89
95
|
</div>
|
|
90
96
|
{% endif %}
|
|
91
97
|
</div>
|
data/_sass/core/_navbar.scss
CHANGED
|
@@ -367,6 +367,12 @@
|
|
|
367
367
|
box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.175);
|
|
368
368
|
border: 1px solid var(--bs-border-color-translucent);
|
|
369
369
|
}
|
|
370
|
+
|
|
371
|
+
// Right-align dropdowns near the end of the nav to prevent overflow clipping
|
|
372
|
+
.nav-hover-dropdown > .dropdown-menu.dropdown-menu-end {
|
|
373
|
+
left: auto;
|
|
374
|
+
right: 0;
|
|
375
|
+
}
|
|
370
376
|
|
|
371
377
|
// Smooth hover reveal animation
|
|
372
378
|
.nav-hover-dropdown .dropdown-menu {
|
data/_sass/custom.scss
CHANGED
|
@@ -40,20 +40,33 @@ html, body {
|
|
|
40
40
|
min-height: 50vh;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
// Landing layout: stable hero media slot (ratio) +
|
|
43
|
+
// Landing layout: stable hero media slot (CSS aspect-ratio) + smooth image fade-in.
|
|
44
|
+
// The inline `aspect-ratio` + `max-width` on .landing-hero-media in landing.html
|
|
45
|
+
// reserve the box size before this stylesheet loads — these rules are progressive
|
|
46
|
+
// enhancement on top of that.
|
|
44
47
|
.landing-hero {
|
|
45
48
|
.landing-hero-media {
|
|
46
49
|
max-width: min(100%, 28rem);
|
|
50
|
+
// Subtle background while the image streams in so the box isn't a flash of
|
|
51
|
+
// empty colored space against the hero gradient.
|
|
52
|
+
background-color: rgba(255, 255, 255, 0.06);
|
|
47
53
|
}
|
|
48
54
|
|
|
49
|
-
.landing-hero-
|
|
55
|
+
.landing-hero-img {
|
|
50
56
|
object-position: center;
|
|
57
|
+
opacity: 0;
|
|
58
|
+
transition: opacity 0.45s ease-out;
|
|
59
|
+
|
|
60
|
+
&.is-loaded {
|
|
61
|
+
opacity: 1;
|
|
62
|
+
}
|
|
51
63
|
}
|
|
52
64
|
}
|
|
53
65
|
|
|
54
66
|
@media (prefers-reduced-motion: reduce) {
|
|
55
|
-
.landing-hero .landing-hero-
|
|
67
|
+
.landing-hero .landing-hero-img {
|
|
56
68
|
transition: none;
|
|
69
|
+
opacity: 1;
|
|
57
70
|
}
|
|
58
71
|
}
|
|
59
72
|
|
|
@@ -170,9 +183,51 @@ html, body {
|
|
|
170
183
|
height: 56px;
|
|
171
184
|
}
|
|
172
185
|
|
|
173
|
-
// Cookie banner: above fixed navbar (1030), below offcanvas (1045) so drawers cover it
|
|
186
|
+
// Cookie banner: above fixed navbar (1030), below offcanvas (1045) so drawers cover it.
|
|
187
|
+
// Show/hide is driven by:
|
|
188
|
+
// - `[hidden]` attribute on initial render and after dismissal (no layout cost)
|
|
189
|
+
// - `.cookie-banner-visible` class to slide / fade into view
|
|
174
190
|
.cookie-consent-banner {
|
|
175
191
|
z-index: 1036;
|
|
192
|
+
background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
|
|
193
|
+
backdrop-filter: blur(10px);
|
|
194
|
+
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
195
|
+
|
|
196
|
+
// Off-screen + transparent until JS adds .cookie-banner-visible.
|
|
197
|
+
// Using `transform` + `opacity` (and NOT `display: none`) keeps the transition smooth.
|
|
198
|
+
opacity: 0;
|
|
199
|
+
transform: translateY(100%);
|
|
200
|
+
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1),
|
|
201
|
+
opacity 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
|
202
|
+
will-change: transform, opacity;
|
|
203
|
+
|
|
204
|
+
// The `hidden` attribute (set in markup + after dismissal) removes the banner
|
|
205
|
+
// from the layout entirely so it can't intercept clicks while invisible.
|
|
206
|
+
&[hidden] {
|
|
207
|
+
display: none !important;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
&.cookie-banner-visible {
|
|
211
|
+
opacity: 1;
|
|
212
|
+
transform: translateY(0);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@media (max-width: 768px) {
|
|
216
|
+
.btn {
|
|
217
|
+
width: 100%;
|
|
218
|
+
margin-bottom: 0.5rem;
|
|
219
|
+
|
|
220
|
+
&:last-child {
|
|
221
|
+
margin-bottom: 0;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
@media (prefers-reduced-motion: reduce) {
|
|
228
|
+
.cookie-consent-banner {
|
|
229
|
+
transition: none;
|
|
230
|
+
}
|
|
176
231
|
}
|
|
177
232
|
|
|
178
233
|
// Active TOC link highlighting
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nanobar Initialization — config-driven page-load progress bar.
|
|
3
|
+
*
|
|
4
|
+
* Reads settings from `window.zer0Nanobar` (injected by the
|
|
5
|
+
* _includes/components/nanobar.html Liquid template) and instantiates the
|
|
6
|
+
* Nanobar library that was loaded via <script defer>.
|
|
7
|
+
*
|
|
8
|
+
* Placement modes (site.nanobar.position):
|
|
9
|
+
* "top" – fixed bar at the top of the viewport (default)
|
|
10
|
+
* "bottom" – fixed bar at the bottom of the viewport
|
|
11
|
+
* "navbar" – inline strip mounted inside #top-progress-target under header
|
|
12
|
+
*
|
|
13
|
+
* @see _config.yml → nanobar section
|
|
14
|
+
* @see _includes/components/nanobar.html
|
|
15
|
+
* @see _includes/core/header.html → #top-progress-target mount point
|
|
16
|
+
*/
|
|
17
|
+
(function () {
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
21
|
+
if (typeof Nanobar !== 'function') { return; }
|
|
22
|
+
|
|
23
|
+
var cfg = window.zer0Nanobar || {};
|
|
24
|
+
var classname = cfg.classname || 'nanobar';
|
|
25
|
+
|
|
26
|
+
// ----- Resolve mount target -----
|
|
27
|
+
// Priority: explicit selector → position "navbar" → none (fixed to viewport)
|
|
28
|
+
var targetEl = null;
|
|
29
|
+
if (cfg.target) {
|
|
30
|
+
targetEl = document.querySelector(cfg.target);
|
|
31
|
+
} else if (cfg.position === 'navbar') {
|
|
32
|
+
targetEl = document.getElementById('top-progress-target');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ----- Position modifier class -----
|
|
36
|
+
var positionMod = '';
|
|
37
|
+
if (cfg.position === 'bottom') { positionMod = classname + '--bottom'; }
|
|
38
|
+
if (cfg.position === 'navbar') { positionMod = classname + '--navbar'; }
|
|
39
|
+
|
|
40
|
+
// ----- Create the Nanobar instance -----
|
|
41
|
+
var nanobar = new Nanobar({
|
|
42
|
+
classname: classname,
|
|
43
|
+
id: cfg.id,
|
|
44
|
+
target: targetEl || undefined
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (positionMod && nanobar.el && nanobar.el.classList) {
|
|
48
|
+
nanobar.el.classList.add(positionMod);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ----- Animate progress steps -----
|
|
52
|
+
var steps = (cfg.steps && cfg.steps.length) ? cfg.steps : [30, 76, 100];
|
|
53
|
+
var delay = cfg.stepDelay || 0;
|
|
54
|
+
|
|
55
|
+
if (delay > 0) {
|
|
56
|
+
steps.forEach(function (pct, i) {
|
|
57
|
+
setTimeout(function () { nanobar.go(pct); }, i * delay);
|
|
58
|
+
});
|
|
59
|
+
} else {
|
|
60
|
+
steps.forEach(function (pct) { nanobar.go(pct); });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
})();
|
data/scripts/lint-pages
CHANGED
|
@@ -383,24 +383,27 @@ scan_collection() {
|
|
|
383
383
|
|
|
384
384
|
info "Scanning collection: $collection ($pattern)"
|
|
385
385
|
|
|
386
|
-
# Use find to match the glob pattern
|
|
387
|
-
local search_dir="$REPO_ROOT"
|
|
388
386
|
local files_found=0
|
|
389
387
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
388
|
+
# Derive the base directory by stripping everything from the first glob
|
|
389
|
+
# character onward. e.g. "pages/_notes/**/*.md" → "pages/_notes"
|
|
390
|
+
# (${pattern%%\**} strips the longest suffix starting with *)
|
|
391
|
+
local prefix="${pattern%%\**}"
|
|
392
|
+
local dir_part="${prefix%/}" # strip trailing slash
|
|
393
|
+
|
|
394
|
+
# For flat patterns like "pages/*.md" limit to the single directory level
|
|
395
|
+
# so we don't accidentally recurse into sub-collections.
|
|
396
|
+
local find_depth_flag=""
|
|
397
|
+
if [[ "$pattern" != *"**"* ]]; then
|
|
398
|
+
find_depth_flag="-maxdepth 1"
|
|
399
|
+
fi
|
|
400
|
+
|
|
401
|
+
if [[ -d "$REPO_ROOT/$dir_part" ]]; then
|
|
402
|
+
while IFS= read -r -d '' filepath; do
|
|
403
|
+
validate_file "$filepath" "$collection"
|
|
404
|
+
files_found=$((files_found + 1))
|
|
405
|
+
# shellcheck disable=SC2086
|
|
406
|
+
done < <(find "$REPO_ROOT/$dir_part" $find_depth_flag -name "*.md" -print0 2>/dev/null)
|
|
404
407
|
fi
|
|
405
408
|
|
|
406
409
|
debug " Found $files_found files in $collection"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jekyll-theme-zer0
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.22.
|
|
4
|
+
version: 0.22.20
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Amr Abdel
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: jekyll
|
|
@@ -125,6 +125,7 @@ files:
|
|
|
125
125
|
- _includes/components/info-section.html
|
|
126
126
|
- _includes/components/js-cdn.html
|
|
127
127
|
- _includes/components/mermaid.html
|
|
128
|
+
- _includes/components/nanobar.html
|
|
128
129
|
- _includes/components/nav-editor.html
|
|
129
130
|
- _includes/components/post-card.html
|
|
130
131
|
- _includes/components/post-type-badge.html
|
|
@@ -291,6 +292,7 @@ files:
|
|
|
291
292
|
- assets/js/modules/navigation/sidebar-state.js
|
|
292
293
|
- assets/js/modules/navigation/smooth-scroll.js
|
|
293
294
|
- assets/js/myScript.js
|
|
295
|
+
- assets/js/nanobar-init.js
|
|
294
296
|
- assets/js/nanobar.min.js
|
|
295
297
|
- assets/js/nav-editor.js
|
|
296
298
|
- assets/js/navigation.js
|