jekyll-theme-zer0 0.22.19 → 0.22.21
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 +26 -2
- data/README.md +35 -21
- data/_data/README.md +1 -0
- data/_data/roadmap.yml +215 -0
- 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/generate-roadmap.rb +200 -0
- data/scripts/generate-roadmap.sh +21 -0
- data/scripts/lint-pages +19 -16
- metadata +7 -2
|
@@ -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
|
+
})();
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# generate-roadmap.rb
|
|
6
|
+
# =============================================================================
|
|
7
|
+
#
|
|
8
|
+
# Reads `_data/roadmap.yml` and updates the README.md roadmap section in-place.
|
|
9
|
+
#
|
|
10
|
+
# It rewrites two regions delimited by HTML comment markers:
|
|
11
|
+
#
|
|
12
|
+
# <!-- ROADMAP_MERMAID:START --> ... <!-- ROADMAP_MERMAID:END -->
|
|
13
|
+
# <!-- ROADMAP_TABLE:START --> ... <!-- ROADMAP_TABLE:END -->
|
|
14
|
+
#
|
|
15
|
+
# Usage:
|
|
16
|
+
# ruby scripts/generate-roadmap.rb # update README.md in place
|
|
17
|
+
# ruby scripts/generate-roadmap.rb --check # exit non-zero if README is stale
|
|
18
|
+
# ruby scripts/generate-roadmap.rb --stdout # print regenerated sections only
|
|
19
|
+
#
|
|
20
|
+
# This script has no gem dependencies beyond the Ruby stdlib.
|
|
21
|
+
# =============================================================================
|
|
22
|
+
|
|
23
|
+
require 'yaml'
|
|
24
|
+
require 'date'
|
|
25
|
+
require 'optparse'
|
|
26
|
+
|
|
27
|
+
ROOT = File.expand_path('..', __dir__)
|
|
28
|
+
DATA_FILE = File.join(ROOT, '_data', 'roadmap.yml')
|
|
29
|
+
README = File.join(ROOT, 'README.md')
|
|
30
|
+
|
|
31
|
+
MERMAID_START = '<!-- ROADMAP_MERMAID:START -->'
|
|
32
|
+
MERMAID_END = '<!-- ROADMAP_MERMAID:END -->'
|
|
33
|
+
TABLE_START = '<!-- ROADMAP_TABLE:START -->'
|
|
34
|
+
TABLE_END = '<!-- ROADMAP_TABLE:END -->'
|
|
35
|
+
|
|
36
|
+
# Width used to right-pad the gantt label column so the `:status, start, end`
|
|
37
|
+
# tail aligns vertically. Adjust if very long version/title combinations show up.
|
|
38
|
+
GANTT_LABEL_WIDTH = 28
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Rendering helpers
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
# Mermaid gantt task line for a single milestone.
|
|
45
|
+
#
|
|
46
|
+
# v0.22 AIEO Optimization :active, 2026-03, 2026-04
|
|
47
|
+
# v1.0 Stable Release :milestone, 2027-01, 1d
|
|
48
|
+
#
|
|
49
|
+
def gantt_task(milestone)
|
|
50
|
+
label = "v#{milestone['version']} #{milestone['title']}"
|
|
51
|
+
status = milestone['status'].to_s
|
|
52
|
+
start = milestone['start']
|
|
53
|
+
finish = milestone['end'] || milestone['start']
|
|
54
|
+
|
|
55
|
+
prefix =
|
|
56
|
+
case status
|
|
57
|
+
when 'completed' then 'done, '
|
|
58
|
+
when 'active' then 'active, '
|
|
59
|
+
when 'milestone' then 'milestone, '
|
|
60
|
+
else ''
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
range = status == 'milestone' ? "#{start}, 1d" : "#{start}, #{finish}"
|
|
64
|
+
" #{label.ljust(GANTT_LABEL_WIDTH)} :#{prefix}#{range}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def render_mermaid(data)
|
|
68
|
+
title = data.dig('meta', 'title') || 'zer0-mistakes Roadmap'
|
|
69
|
+
milestones = data['milestones'] || []
|
|
70
|
+
|
|
71
|
+
# Group by section while preserving the order in which sections first appear.
|
|
72
|
+
sections = milestones.group_by { |m| m['section'] || 'Roadmap' }
|
|
73
|
+
ordered = milestones.map { |m| m['section'] }.uniq
|
|
74
|
+
|
|
75
|
+
lines = []
|
|
76
|
+
lines << '```mermaid'
|
|
77
|
+
lines << 'gantt'
|
|
78
|
+
lines << " title #{title}"
|
|
79
|
+
lines << ' dateFormat YYYY-MM'
|
|
80
|
+
ordered.each do |section|
|
|
81
|
+
lines << " section #{section}"
|
|
82
|
+
sections[section].each { |m| lines << gantt_task(m) }
|
|
83
|
+
end
|
|
84
|
+
lines << '```'
|
|
85
|
+
lines.join("\n")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Status → human-readable target column for the summary table.
|
|
89
|
+
def target_label(milestone)
|
|
90
|
+
case milestone['status']
|
|
91
|
+
when 'completed'
|
|
92
|
+
if (released = milestone['released'])
|
|
93
|
+
Date.parse(released.to_s).strftime('%b %Y')
|
|
94
|
+
else
|
|
95
|
+
'Completed'
|
|
96
|
+
end
|
|
97
|
+
when 'active'
|
|
98
|
+
milestone['target'] || 'In progress'
|
|
99
|
+
when 'milestone', 'planned'
|
|
100
|
+
milestone['target'] || milestone['start']
|
|
101
|
+
else
|
|
102
|
+
milestone['target'] || ''
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def render_table(data)
|
|
107
|
+
rows = (data['milestones'] || []).map do |m|
|
|
108
|
+
version = "**v#{m['version']}**"
|
|
109
|
+
target = target_label(m)
|
|
110
|
+
summary = m['summary'] || ''
|
|
111
|
+
status_emoji =
|
|
112
|
+
case m['status']
|
|
113
|
+
when 'completed' then '✅ Completed'
|
|
114
|
+
when 'active' then '🚧 In Progress'
|
|
115
|
+
when 'milestone' then '🎯 Milestone'
|
|
116
|
+
else '🗓 Planned'
|
|
117
|
+
end
|
|
118
|
+
"| #{version} | #{status_emoji} | #{target} | #{summary} |"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
header = [
|
|
122
|
+
'| Version | Status | Target | Highlights |',
|
|
123
|
+
'|---------|--------|--------|------------|'
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
(header + rows).join("\n")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def replace_block(content, marker_start, marker_end, replacement)
|
|
130
|
+
pattern = /(#{Regexp.escape(marker_start)})(.*?)(#{Regexp.escape(marker_end)})/m
|
|
131
|
+
unless content.match?(pattern)
|
|
132
|
+
raise "Markers not found in README: #{marker_start} ... #{marker_end}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Surround the replacement with blank lines so kramdown parses the following
|
|
136
|
+
# markdown (especially tables) instead of treating it as part of the HTML
|
|
137
|
+
# comment block. Without the blank line after `<!-- ... -->`, GFM tables
|
|
138
|
+
# collapse into a single paragraph.
|
|
139
|
+
content.sub(pattern, "\\1\n\n#{replacement}\n\n\\3")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
# Main
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
def main
|
|
147
|
+
options = { mode: :write }
|
|
148
|
+
OptionParser.new do |opts|
|
|
149
|
+
opts.banner = 'Usage: generate-roadmap.rb [--check|--stdout]'
|
|
150
|
+
opts.on('--check', 'Exit non-zero if README would change') { options[:mode] = :check }
|
|
151
|
+
opts.on('--stdout', 'Print regenerated sections to stdout') { options[:mode] = :stdout }
|
|
152
|
+
end.parse!
|
|
153
|
+
|
|
154
|
+
# The roadmap data only contains scalars and Date values; Symbol is not used,
|
|
155
|
+
# but Date/Time must be permitted because YAML's safe loader rejects them by default.
|
|
156
|
+
# Ruby >= 3.1 supports `permitted_classes:` on `YAML.load_file`. On older Rubies
|
|
157
|
+
# (e.g. macOS system Ruby 2.6), fall back to `safe_load` which accepted the
|
|
158
|
+
# keyword earlier, so the generator works for contributors without rbenv/rvm.
|
|
159
|
+
data =
|
|
160
|
+
begin
|
|
161
|
+
YAML.load_file(DATA_FILE, permitted_classes: [Date, Time])
|
|
162
|
+
rescue ArgumentError
|
|
163
|
+
YAML.safe_load(File.read(DATA_FILE), permitted_classes: [Date, Time], aliases: false)
|
|
164
|
+
end
|
|
165
|
+
mermaid = render_mermaid(data)
|
|
166
|
+
table = render_table(data)
|
|
167
|
+
|
|
168
|
+
if options[:mode] == :stdout
|
|
169
|
+
puts mermaid
|
|
170
|
+
puts
|
|
171
|
+
puts table
|
|
172
|
+
return 0
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
original = File.read(README)
|
|
176
|
+
updated = original.dup
|
|
177
|
+
updated = replace_block(updated, MERMAID_START, MERMAID_END, mermaid)
|
|
178
|
+
updated = replace_block(updated, TABLE_START, TABLE_END, table)
|
|
179
|
+
|
|
180
|
+
if options[:mode] == :check
|
|
181
|
+
if original == updated
|
|
182
|
+
puts '✓ README.md roadmap section is up to date with _data/roadmap.yml'
|
|
183
|
+
return 0
|
|
184
|
+
else
|
|
185
|
+
warn '✗ README.md roadmap section is out of date with _data/roadmap.yml'
|
|
186
|
+
warn ' Run: ./scripts/generate-roadmap.sh'
|
|
187
|
+
return 1
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
if original == updated
|
|
192
|
+
puts 'README.md roadmap section already up to date.'
|
|
193
|
+
else
|
|
194
|
+
File.write(README, updated)
|
|
195
|
+
puts "Updated README.md roadmap section from #{File.basename(DATA_FILE)}."
|
|
196
|
+
end
|
|
197
|
+
0
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
exit main if $PROGRAM_NAME == __FILE__
|