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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba91ba02a96ff1b026fe82adedbebd79b476a48bc3534ff7b1799ebbf9e593d7
4
- data.tar.gz: 4d1470d33ae3e6d41def0fbd22e05c07bf9597efa9f5c5e655f3dd61088896a4
3
+ metadata.gz: 980b31bacaacb76414bff954016a5a98c83e175ec3ef2361e15e1ec175718073
4
+ data.tar.gz: a2b223a9ff562ce39b12a3106ca343c62c0d76060d50550576ee853504ece84f
5
5
  SHA512:
6
- metadata.gz: f643278addd297e92c075dc79f9802daf61bc9ca2bb4c5cde4dec6842da25895e0c9ea75f597cdc718fbc5dcc4b078f752f3e32ae310b3ca884d9fd82fc1c1d9
7
- data.tar.gz: e2318d7d03c184b9a053d59c2130b818486d1cb20ce6938ec38e88e0b2459bee62108abfa223941857e4707f32313b5fefa5bef01682c4800f3a9c25f112159f
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.19
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-18T21:22:34.000Z
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.19 ([RubyGems](https://rubygems.org/gems/jekyll-theme-zer0), [CHANGELOG](/CHANGELOG)) |
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.19** • [Changelog](CHANGELOG.md) • [License](LICENSE) • [Contributing](CONTRIBUTING.md)
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
- // Ensure banner starts completely hidden - remove all classes and inline styles
254
- banner.classList.remove('cookie-banner-showing', 'cookie-banner-visible');
255
- banner.removeAttribute('style'); // Remove all inline styles to let CSS take over
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
- // First, make it visible but still off-screen (using class for CSS control)
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
- // On the next frame, animate it in smoothly
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
- // Remove visible class to trigger transition out
279
- banner.classList.remove('cookie-banner-visible');
280
-
281
- // Hide after transition completes
282
- banner.addEventListener('transitionend', function hideAfterTransition() {
283
- banner.classList.remove('cookie-banner-showing');
284
- banner.removeAttribute('style'); // Clean up inline styles
285
- banner.removeEventListener('transitionend', hideAfterTransition);
286
- }, { once: true });
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
- if (banner) {
324
- banner.classList.remove('cookie-banner-showing', 'cookie-banner-visible');
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
- /* Banner base styles */
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 -%}
@@ -30,10 +30,10 @@
30
30
  ===================================================================
31
31
  -->
32
32
 
33
- <footer class="bd-footer container-xl border-top" role="contentinfo">
33
+ <footer class="bd-footer border-top" role="contentinfo">
34
34
  <!-- Powered by Row -->
35
- <div class="container row my-3">
36
- <ul class="nav col-sm justify-content-end list-unstyled d-flex align-items-center" aria-label="Powered by technologies">
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
  &copy; {{ 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="container row">
72
- <div class="container bg-dark text-light py-5 rounded-3">
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
- <!-- Bottom: Copyright -->
167
- <div class="text-center">
168
- <p class="mb-0">&copy; {{ site.time | date: "%Y" }} {{ site.title | default: site.name }}. All Rights Reserved.</p>
169
- </div>
165
+ <!-- Bottom: Copyright -->
166
+ <div class="text-center">
167
+ <p class="mb-0">&copy; {{ site.time | date: "%Y" }} {{ site.title | default: site.name }}. All Rights Reserved.</p>
170
168
  </div>
171
169
  </div>
172
170
  </div>
@@ -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 and UI enhancement scripts
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
- <script defer src="{{'/assets/js/nanobar.min.js' | relative_url }}"></script>
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>
@@ -30,7 +30,10 @@
30
30
  - ARIA labels for screen readers
31
31
  - Focus management for offcanvas
32
32
 
33
- TODO: Fix Nanobar progress bar positioning and animation
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
@@ -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 ratio ratio-4x3 mx-auto shadow-lg rounded overflow-hidden">
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
- style="object-fit: contain;"
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 ratio ratio-4x3 mx-auto shadow-lg rounded overflow-hidden">
85
- <div class="position-absolute top-0 start-0 w-100 h-100 d-flex flex-column align-items-center justify-content-center bg-secondary bg-opacity-25 text-body rounded">
86
- <i class="bi bi-code-square display-1"></i>
87
- <p class="mt-3 mb-0 px-3">Jekyll Theme</p>
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>
@@ -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) + no reliance on scroll script for first paint
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-media.ratio > img {
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-media {
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
- while IFS= read -r -d '' filepath; do
391
- validate_file "$filepath" "$collection"
392
- files_found=$((files_found + 1))
393
- done < <(find "$REPO_ROOT" -path "$REPO_ROOT/$pattern" -name "*.md" -print0 2>/dev/null || true)
394
-
395
- # Fallback: use a simpler find if the glob didn't work
396
- if [[ $files_found -eq 0 ]]; then
397
- local dir_part="${pattern%%/**}"
398
- if [[ -d "$REPO_ROOT/$dir_part" ]]; then
399
- while IFS= read -r -d '' filepath; do
400
- validate_file "$filepath" "$collection"
401
- files_found=$((files_found + 1))
402
- done < <(find "$REPO_ROOT/$dir_part" -name "*.md" -print0 2>/dev/null)
403
- fi
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.19
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-18 00:00:00.000000000 Z
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