jekyll-theme-zer0 1.22.0 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +55 -0
- data/_data/ai.yml +10 -0
- data/_data/backlog.yml +65 -37
- data/_data/features.yml +847 -34
- data/_data/navigation/docs.yml +4 -0
- data/_data/theme_backgrounds.yml +24 -0
- data/_includes/components/component-showcase.html +77 -16
- data/_includes/components/feature-card.html +15 -0
- data/_includes/components/search-modal.html +28 -4
- data/_includes/components/zer0-env-var.html +7 -1
- data/_includes/core/color-mode-init.html +35 -0
- data/_includes/core/head.html +9 -0
- data/_includes/core/tokens-inline.html +5 -0
- data/_layouts/author.html +22 -1
- data/_layouts/root.html +15 -1
- data/scripts/bin/validate +18 -1
- data/scripts/ci/classify_changes.py +129 -0
- data/scripts/issues/dispatch.py +478 -0
- data/scripts/issues/test_verify_close.py +118 -0
- data/scripts/issues/triage.py +1046 -0
- data/scripts/issues/verify_close.py +179 -0
- data/scripts/validate-features.rb +142 -0
- metadata +10 -2
data/_data/navigation/docs.yml
CHANGED
|
@@ -70,6 +70,10 @@
|
|
|
70
70
|
url: /docs/deployment/
|
|
71
71
|
- title: GitHub Pages
|
|
72
72
|
url: /docs/deployment/github-pages/
|
|
73
|
+
- title: Remote-Theme Consumer Checklist
|
|
74
|
+
url: /docs/deployment/remote-theme-checklist/
|
|
75
|
+
- title: Safe-Mode Build Overlay
|
|
76
|
+
url: /docs/deployment/build-overlay/
|
|
73
77
|
- title: Netlify
|
|
74
78
|
url: /docs/deployment/netlify/
|
|
75
79
|
- title: Custom Domain
|
data/_data/theme_backgrounds.yml
CHANGED
|
@@ -99,3 +99,27 @@ skins:
|
|
|
99
99
|
gradient: "assets/backgrounds/gradients/sunrise.svg"
|
|
100
100
|
pattern: "assets/backgrounds/patterns/sunrise.svg"
|
|
101
101
|
noise: "assets/backgrounds/noise/sunrise.svg"
|
|
102
|
+
|
|
103
|
+
# `dark` is the DEFAULT theme_skin, and `contrast` is advertised as valid, but
|
|
104
|
+
# neither was mapped here — so the SVG-background feature had no entry for them
|
|
105
|
+
# (issue #240). The gradient/pattern/noise assets already ship; these entries
|
|
106
|
+
# wire them up. Colors are taken from each skin's gradient stops.
|
|
107
|
+
dark:
|
|
108
|
+
label: "Dark"
|
|
109
|
+
colors:
|
|
110
|
+
primary: "#1a1a2e"
|
|
111
|
+
secondary: "#16213e"
|
|
112
|
+
accent: "#0f3460"
|
|
113
|
+
gradient: "assets/backgrounds/gradients/dark.svg"
|
|
114
|
+
pattern: "assets/backgrounds/patterns/dark.svg"
|
|
115
|
+
noise: "assets/backgrounds/noise/dark.svg"
|
|
116
|
+
|
|
117
|
+
contrast:
|
|
118
|
+
label: "Contrast"
|
|
119
|
+
colors:
|
|
120
|
+
primary: "#111111"
|
|
121
|
+
secondary: "#333333"
|
|
122
|
+
accent: "#ffcc00"
|
|
123
|
+
gradient: "assets/backgrounds/gradients/contrast.svg"
|
|
124
|
+
pattern: "assets/backgrounds/patterns/contrast.svg"
|
|
125
|
+
noise: "assets/backgrounds/noise/contrast.svg"
|
|
@@ -25,11 +25,12 @@
|
|
|
25
25
|
- Bootstrap 5.3 CSS & JS (loaded by theme)
|
|
26
26
|
- Bootstrap Icons (loaded by theme)
|
|
27
27
|
|
|
28
|
-
Note: The breadcrumb and list-group links are
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
Note: The breadcrumb and list-group demo links are existence-guarded (issue #219).
|
|
29
|
+
Each target URL is resolved against site.html_pages (and collection docs
|
|
30
|
+
for collection-backed routes). When the page exists in the build the link
|
|
31
|
+
is real; when it does not the label renders as plain text. This keeps the
|
|
32
|
+
showcase safe for remote-theme consumers who may lack /docs/, /posts/,
|
|
33
|
+
/categories/, /tags/, or /docs/customization/.
|
|
33
34
|
===================================================================
|
|
34
35
|
-->
|
|
35
36
|
{% assign sections = include.sections | default: "alerts,buttons,badges,cards,accordion,tabs,progress,breadcrumbs,table,tooltips,list-group" %}
|
|
@@ -326,11 +327,29 @@ theme: jekyll-theme-zer0</code></pre>
|
|
|
326
327
|
<!-- ===== BREADCRUMBS ===== -->
|
|
327
328
|
<h3 id="showcase-breadcrumbs">Breadcrumbs</h3>
|
|
328
329
|
<p>Hierarchical navigation trail showing current page position:</p>
|
|
330
|
+
{% comment %} Existence-guard the demo breadcrumb links so they never 404 on
|
|
331
|
+
remote-theme consumers that lack /docs/ or /docs/customization/. When the
|
|
332
|
+
target page is in the build the crumb is a real link; otherwise plain text.
|
|
333
|
+
Home (/) is always present. Pattern mirrors _includes/navigation/breadcrumbs.html. {% endcomment %}
|
|
334
|
+
{% assign _demo_docs_page = site.html_pages | where: "url", "/docs/" | first %}
|
|
335
|
+
{% unless _demo_docs_page %}
|
|
336
|
+
{% for _col in site.collections %}
|
|
337
|
+
{% assign _demo_docs_page = _col.docs | where: "url", "/docs/" | first %}
|
|
338
|
+
{% if _demo_docs_page %}{% break %}{% endif %}
|
|
339
|
+
{% endfor %}
|
|
340
|
+
{% endunless %}
|
|
341
|
+
{% assign _demo_custom_page = site.html_pages | where: "url", "/docs/customization/" | first %}
|
|
342
|
+
{% unless _demo_custom_page %}
|
|
343
|
+
{% for _col in site.collections %}
|
|
344
|
+
{% assign _demo_custom_page = _col.docs | where: "url", "/docs/customization/" | first %}
|
|
345
|
+
{% if _demo_custom_page %}{% break %}{% endif %}
|
|
346
|
+
{% endfor %}
|
|
347
|
+
{% endunless %}
|
|
329
348
|
<nav aria-label="breadcrumb example" class="mb-4">
|
|
330
349
|
<ol class="breadcrumb">
|
|
331
|
-
<li class="breadcrumb-item"><a href="
|
|
332
|
-
<li class="breadcrumb-item"
|
|
333
|
-
<li class="breadcrumb-item"
|
|
350
|
+
<li class="breadcrumb-item"><a href="{{ '/' | relative_url }}" class="text-decoration-none"><i class="bi bi-house me-1"></i>Home</a></li>
|
|
351
|
+
<li class="breadcrumb-item">{% if _demo_docs_page %}<a href="{{ '/docs/' | relative_url }}" class="text-decoration-none">Documentation</a>{% else %}Documentation{% endif %}</li>
|
|
352
|
+
<li class="breadcrumb-item">{% if _demo_custom_page %}<a href="{{ '/docs/customization/' | relative_url }}" class="text-decoration-none">Customization</a>{% else %}Customization{% endif %}</li>
|
|
334
353
|
<li class="breadcrumb-item active" aria-current="page">Theme Colors</li>
|
|
335
354
|
</ol>
|
|
336
355
|
</nav>
|
|
@@ -419,25 +438,67 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
419
438
|
<!-- ===== LIST GROUP ===== -->
|
|
420
439
|
<h3 id="showcase-list-group">List Group</h3>
|
|
421
440
|
<p>Organized content lists with icons, badges, and action states:</p>
|
|
441
|
+
{% comment %} Existence-guard the demo list-group links (issue #219). Each target
|
|
442
|
+
URL is resolved against site.html_pages + collection docs. When present the
|
|
443
|
+
item is a real <a>; when absent it renders as a non-linking <div> so no 404
|
|
444
|
+
is injected on remote-theme consumers that lack /posts/, /docs/, /categories/,
|
|
445
|
+
or /tags/. Pattern mirrors section-sidebar.html and author-bio.html. {% endcomment %}
|
|
446
|
+
{% assign _demo_posts_page = site.html_pages | where: "url", "/posts/" | first %}
|
|
447
|
+
{% unless _demo_posts_page %}
|
|
448
|
+
{% for _col in site.collections %}
|
|
449
|
+
{% assign _demo_posts_page = _col.docs | where: "url", "/posts/" | first %}
|
|
450
|
+
{% if _demo_posts_page %}{% break %}{% endif %}
|
|
451
|
+
{% endfor %}
|
|
452
|
+
{% endunless %}
|
|
453
|
+
{% assign _demo_cats_page = site.html_pages | where: "url", "/categories/" | first %}
|
|
454
|
+
{% unless _demo_cats_page %}
|
|
455
|
+
{% for _col in site.collections %}
|
|
456
|
+
{% assign _demo_cats_page = _col.docs | where: "url", "/categories/" | first %}
|
|
457
|
+
{% if _demo_cats_page %}{% break %}{% endif %}
|
|
458
|
+
{% endfor %}
|
|
459
|
+
{% endunless %}
|
|
460
|
+
{% assign _demo_tags_page = site.html_pages | where: "url", "/tags/" | first %}
|
|
461
|
+
{% unless _demo_tags_page %}
|
|
462
|
+
{% for _col in site.collections %}
|
|
463
|
+
{% assign _demo_tags_page = _col.docs | where: "url", "/tags/" | first %}
|
|
464
|
+
{% if _demo_tags_page %}{% break %}{% endif %}
|
|
465
|
+
{% endfor %}
|
|
466
|
+
{% endunless %}
|
|
422
467
|
<div class="row g-4 mb-4">
|
|
423
468
|
<div class="col-md-6">
|
|
424
469
|
<div class="list-group">
|
|
425
|
-
|
|
470
|
+
{% if _demo_posts_page %}
|
|
471
|
+
<a href="{{ '/posts/' | relative_url }}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
|
472
|
+
{% else %}
|
|
473
|
+
<div class="list-group-item d-flex justify-content-between align-items-center">
|
|
474
|
+
{% endif %}
|
|
426
475
|
<span><i class="bi bi-journal-richtext text-primary me-2"></i>Blog Posts</span>
|
|
427
476
|
<span class="badge bg-primary rounded-pill">{{ site.posts.size | default: "12" }}</span>
|
|
428
|
-
</a>
|
|
429
|
-
|
|
477
|
+
{% if _demo_posts_page %}</a>{% else %}</div>{% endif %}
|
|
478
|
+
{% if _demo_docs_page %}
|
|
479
|
+
<a href="{{ '/docs/' | relative_url }}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
|
480
|
+
{% else %}
|
|
481
|
+
<div class="list-group-item d-flex justify-content-between align-items-center">
|
|
482
|
+
{% endif %}
|
|
430
483
|
<span><i class="bi bi-book text-success me-2"></i>Documentation</span>
|
|
431
484
|
<span class="badge bg-success rounded-pill">{{ site.documents.size | default: "8" }}</span>
|
|
432
|
-
</a>
|
|
433
|
-
|
|
485
|
+
{% if _demo_docs_page %}</a>{% else %}</div>{% endif %}
|
|
486
|
+
{% if _demo_cats_page %}
|
|
487
|
+
<a href="{{ '/categories/' | relative_url }}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
|
488
|
+
{% else %}
|
|
489
|
+
<div class="list-group-item d-flex justify-content-between align-items-center">
|
|
490
|
+
{% endif %}
|
|
434
491
|
<span><i class="bi bi-collection text-info me-2"></i>Categories</span>
|
|
435
492
|
<span class="badge bg-info rounded-pill">{{ site.categories.size | default: "6" }}</span>
|
|
436
|
-
</a>
|
|
437
|
-
|
|
493
|
+
{% if _demo_cats_page %}</a>{% else %}</div>{% endif %}
|
|
494
|
+
{% if _demo_tags_page %}
|
|
495
|
+
<a href="{{ '/tags/' | relative_url }}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
|
496
|
+
{% else %}
|
|
497
|
+
<div class="list-group-item d-flex justify-content-between align-items-center">
|
|
498
|
+
{% endif %}
|
|
438
499
|
<span><i class="bi bi-tags text-warning me-2"></i>Tags</span>
|
|
439
500
|
<span class="badge bg-warning text-dark rounded-pill">{{ site.tags.size | default: "24" }}</span>
|
|
440
|
-
</a>
|
|
501
|
+
{% if _demo_tags_page %}</a>{% else %}</div>{% endif %}
|
|
441
502
|
</div>
|
|
442
503
|
</div>
|
|
443
504
|
<div class="col-md-6">
|
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
- compact (bool) : Compact mode — hides sub-features list (optional, default: false)
|
|
20
20
|
- features_limit (int) : Max sub-features shown in the list (optional, default: 5)
|
|
21
21
|
|
|
22
|
+
Feature fields surfaced (from _data/features.yml): title, description,
|
|
23
|
+
features[], references (when show_refs), id, version, tags, docs, link, and
|
|
24
|
+
provenance (provenance.pr / provenance.commit / provenance.issue → GitHub
|
|
25
|
+
links built from site.github_user + site.repository_name).
|
|
26
|
+
|
|
22
27
|
Usage:
|
|
23
28
|
{% assign f = site.data.features.features | where: "id", "ZER0-001" | first %}
|
|
24
29
|
{% include components/feature-card.html feature=f style="primary" icon="bi-bootstrap" icon_color="text-primary" %}
|
|
@@ -78,6 +83,16 @@
|
|
|
78
83
|
<span class="badge bg-light text-dark">{{ tag }}</span>
|
|
79
84
|
{% endfor %}
|
|
80
85
|
</div>
|
|
86
|
+
|
|
87
|
+
{% if f.provenance %}
|
|
88
|
+
{% assign gh = "https://github.com/" | append: site.github_user | append: "/" | append: site.repository_name %}
|
|
89
|
+
<div class="mt-2 small text-muted feature-provenance">
|
|
90
|
+
<i class="bi bi-clock-history me-1" aria-hidden="true"></i>
|
|
91
|
+
{% if f.provenance.pr %}<a href="{{ gh }}/pull/{{ f.provenance.pr }}" class="text-decoration-none me-2">PR #{{ f.provenance.pr }}</a>{% endif %}
|
|
92
|
+
{% if f.provenance.commit %}<a href="{{ gh }}/commit/{{ f.provenance.commit }}" class="text-decoration-none me-2"><code>{{ f.provenance.commit }}</code></a>{% endif %}
|
|
93
|
+
{% if f.provenance.issue %}<a href="{{ gh }}/issues/{{ f.provenance.issue }}" class="text-decoration-none me-2">#{{ f.provenance.issue }}</a>{% endif %}
|
|
94
|
+
</div>
|
|
95
|
+
{% endif %}
|
|
81
96
|
</div>
|
|
82
97
|
{% if f.docs %}
|
|
83
98
|
<div class="card-footer">
|
|
@@ -2,19 +2,43 @@
|
|
|
2
2
|
===================================================================
|
|
3
3
|
SEARCH MODAL - Site-wide Search Popup
|
|
4
4
|
===================================================================
|
|
5
|
-
|
|
5
|
+
|
|
6
6
|
File: search-modal.html
|
|
7
7
|
Path: _includes/components/search-modal.html
|
|
8
8
|
Purpose: Provide a modal search experience with keyboard shortcut support
|
|
9
|
-
|
|
9
|
+
|
|
10
10
|
Notes:
|
|
11
|
-
-
|
|
11
|
+
- Form action targets /sitemap/ when that page exists in the build;
|
|
12
|
+
falls back to /sitemap.xml if available, otherwise '#' (safe no-op).
|
|
13
|
+
JS (search-modal.js) always intercepts submit and keeps results
|
|
14
|
+
in-modal, so the action is only a no-JS fallback. The gate here
|
|
15
|
+
prevents a dead link on remote-theme Pages builds that lack /sitemap/.
|
|
12
16
|
- Keyboard shortcut handled by navigation keyboard module ("/")
|
|
13
17
|
===================================================================
|
|
14
18
|
-->
|
|
15
19
|
|
|
16
20
|
{%- assign ui_text = site.data.ui-text[site.locale] | default: site.data.ui-text.en -%}
|
|
17
21
|
{%- assign search_placeholder = ui_text.search_placeholder_text | default: "Enter your search term..." -%}
|
|
22
|
+
{%- comment -%}
|
|
23
|
+
Existence-gate the form action to /sitemap/ only when that page is present
|
|
24
|
+
in the build (mirroring the footer Quick Links guard, and the section-sidebar
|
|
25
|
+
/tags/ gate). Remote-theme Pages consumers that haven't committed a /sitemap/
|
|
26
|
+
stub get a safe '#' fallback; the JS always intercepts submit anyway, so this
|
|
27
|
+
only affects the no-JS code path.
|
|
28
|
+
{%- endcomment -%}
|
|
29
|
+
{%- assign _search_sitemap_url = "/sitemap/" -%}
|
|
30
|
+
{%- assign _search_sitemap_page = site.html_pages | where: "url", _search_sitemap_url | first -%}
|
|
31
|
+
{%- unless _search_sitemap_page -%}
|
|
32
|
+
{%- for col in site.collections -%}
|
|
33
|
+
{%- assign _search_sitemap_page = col.docs | where: "url", _search_sitemap_url | first -%}
|
|
34
|
+
{%- if _search_sitemap_page -%}{%- break -%}{%- endif -%}
|
|
35
|
+
{%- endfor -%}
|
|
36
|
+
{%- endunless -%}
|
|
37
|
+
{%- if _search_sitemap_page -%}
|
|
38
|
+
{%- assign _search_form_action = "/sitemap/" | relative_url -%}
|
|
39
|
+
{%- else -%}
|
|
40
|
+
{%- assign _search_form_action = "#" -%}
|
|
41
|
+
{%- endif -%}
|
|
18
42
|
|
|
19
43
|
<div class="modal fade search-modal" id="siteSearchModal" tabindex="-1" aria-labelledby="siteSearchModalLabel" aria-hidden="true">
|
|
20
44
|
<div class="modal-dialog modal-dialog-centered">
|
|
@@ -26,7 +50,7 @@
|
|
|
26
50
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
27
51
|
</div>
|
|
28
52
|
<div class="modal-body">
|
|
29
|
-
<form action="{{
|
|
53
|
+
<form action="{{ _search_form_action }}" method="get" data-search-form>
|
|
30
54
|
<label class="visually-hidden" for="site-search-input">Search</label>
|
|
31
55
|
<div class="input-group">
|
|
32
56
|
<span class="input-group-text">
|
|
@@ -57,6 +57,12 @@
|
|
|
57
57
|
document.querySelector('#envTable tbody').appendChild(row);
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
+
// Escape user-entered text before it is interpolated into innerHTML, so a
|
|
61
|
+
// key/value containing markup cannot inject script (DOM XSS).
|
|
62
|
+
const escapeHtml = (s) => String(s).replace(/[&<>"']/g, (c) => (
|
|
63
|
+
{ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]
|
|
64
|
+
));
|
|
65
|
+
|
|
60
66
|
document.getElementById('submit').addEventListener('click', function() {
|
|
61
67
|
const rows = document.querySelectorAll('#envTable tbody tr');
|
|
62
68
|
let codeBlockText = '';
|
|
@@ -64,7 +70,7 @@
|
|
|
64
70
|
const key = row.querySelector('td:nth-child(1) input').value;
|
|
65
71
|
const value = row.querySelector('td:nth-child(2) input').value;
|
|
66
72
|
sessionStorage.setItem(key, value);
|
|
67
|
-
codeBlockText += `<span class="nb">export </span><span class="nv">${key}</span><span class="o">=</span>${value}\n`;
|
|
73
|
+
codeBlockText += `<span class="nb">export </span><span class="nv">${escapeHtml(key)}</span><span class="o">=</span>${escapeHtml(value)}\n`;
|
|
68
74
|
});
|
|
69
75
|
document.getElementById('codeBlock').innerHTML = codeBlockText;
|
|
70
76
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{%- comment -%}
|
|
2
|
+
Component: color-mode-init
|
|
3
|
+
Path: _includes/core/color-mode-init.html
|
|
4
|
+
Purpose: Early inline script that applies data-bs-theme BEFORE Bootstrap CSS
|
|
5
|
+
loads, preventing FOUC (flash of unstyled/wrong-themed content).
|
|
6
|
+
Params: none — reads data-color-mode-default from the <html> element,
|
|
7
|
+
which root.html sets from site.color_mode_default.
|
|
8
|
+
Depends on: root.html (sets data-color-mode-default attribute server-side)
|
|
9
|
+
Notes: Must be included BEFORE any Bootstrap CSS link tag. The script
|
|
10
|
+
is tiny, synchronous, and runs inline before the browser requests
|
|
11
|
+
external stylesheets, so the correct theme is already applied
|
|
12
|
+
when Bootstrap's [data-bs-theme] selectors are first evaluated.
|
|
13
|
+
|
|
14
|
+
Priority order (highest → lowest):
|
|
15
|
+
1. localStorage["theme"] — user's explicit override via Appearance panel
|
|
16
|
+
2. data-color-mode-default — site.color_mode_default config value
|
|
17
|
+
3. "auto" fallback — resolves via prefers-color-scheme
|
|
18
|
+
{%- endcomment -%}
|
|
19
|
+
<script>
|
|
20
|
+
(function () {
|
|
21
|
+
'use strict';
|
|
22
|
+
try {
|
|
23
|
+
var root = document.documentElement;
|
|
24
|
+
var configDefault = root.getAttribute('data-color-mode-default') || 'auto';
|
|
25
|
+
var stored = window.localStorage.getItem('theme');
|
|
26
|
+
var mode = stored || configDefault;
|
|
27
|
+
if (mode === 'auto') {
|
|
28
|
+
root.setAttribute('data-bs-theme',
|
|
29
|
+
window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
|
30
|
+
} else {
|
|
31
|
+
root.setAttribute('data-bs-theme', mode);
|
|
32
|
+
}
|
|
33
|
+
} catch (e) { /* localStorage / matchMedia unavailable — server-rendered value stands */ }
|
|
34
|
+
})();
|
|
35
|
+
</script>
|
data/_includes/core/head.html
CHANGED
|
@@ -127,6 +127,15 @@ window.MathJax = {
|
|
|
127
127
|
<!-- CSS FRAMEWORKS AND LIBRARIES -->
|
|
128
128
|
<!-- ================================ -->
|
|
129
129
|
|
|
130
|
+
<!-- ========================== -->
|
|
131
|
+
<!-- COLOR MODE INIT (FOUC FIX) -->
|
|
132
|
+
<!-- ========================== -->
|
|
133
|
+
<!-- Must run BEFORE Bootstrap CSS so the correct data-bs-theme is applied
|
|
134
|
+
when [data-bs-theme=dark/light] selectors are first evaluated.
|
|
135
|
+
Reads site.color_mode_default (via data-color-mode-default attr on <html>)
|
|
136
|
+
and falls back to localStorage["theme"] for the user's explicit preference. -->
|
|
137
|
+
{% include core/color-mode-init.html %}
|
|
138
|
+
|
|
130
139
|
<!-- ========================== -->
|
|
131
140
|
<!-- BOOTSTRAP 5 FRAMEWORK -->
|
|
132
141
|
<!-- ========================== -->
|
|
@@ -35,6 +35,11 @@
|
|
|
35
35
|
{%- endif -%}
|
|
36
36
|
|
|
37
37
|
{%- comment -%}
|
|
38
|
+
NOTE: The data-bs-theme FOUC-prevention script lives in
|
|
39
|
+
_includes/core/color-mode-init.html (included from head.html BEFORE Bootstrap
|
|
40
|
+
CSS) so the correct theme is applied before any [data-bs-theme] selectors are
|
|
41
|
+
evaluated. Do not duplicate it here.
|
|
42
|
+
|
|
38
43
|
Restore any user-saved Appearance overrides before paint to avoid a flash
|
|
39
44
|
of the default palette. The Appearance panel (Track 6) writes JSON like
|
|
40
45
|
{"primary":"#ff5722"} to localStorage["zer0-appearance"].
|
data/_layouts/author.html
CHANGED
|
@@ -68,10 +68,31 @@ hide_intro: true
|
|
|
68
68
|
<!-- ========================== -->
|
|
69
69
|
<!-- BREADCRUMB -->
|
|
70
70
|
<!-- ========================== -->
|
|
71
|
+
{%- comment -%}
|
|
72
|
+
Only link the /authors/ breadcrumb crumb when that index page actually exists
|
|
73
|
+
in the build. A pure remote-theme Pages consumer doesn't get the
|
|
74
|
+
plugin-generated /authors/ index, so rendering the link would produce a 404.
|
|
75
|
+
Mirror the existence guard used in _includes/navigation/breadcrumbs.html
|
|
76
|
+
and _includes/components/author-bio.html. See issue #204.
|
|
77
|
+
{%- endcomment -%}
|
|
78
|
+
{%- assign _authors_url = '/authors/' -%}
|
|
79
|
+
{%- assign _authors_page = site.html_pages | where: "url", _authors_url | first -%}
|
|
80
|
+
{%- unless _authors_page -%}
|
|
81
|
+
{%- for _col in site.collections -%}
|
|
82
|
+
{%- assign _authors_page = _col.docs | where: "url", _authors_url | first -%}
|
|
83
|
+
{%- if _authors_page -%}{%- break -%}{%- endif -%}
|
|
84
|
+
{%- endfor -%}
|
|
85
|
+
{%- endunless -%}
|
|
71
86
|
<nav aria-label="breadcrumb" class="mb-3">
|
|
72
87
|
<ol class="breadcrumb">
|
|
73
88
|
<li class="breadcrumb-item"><a href="{{ '/' | relative_url }}">Home</a></li>
|
|
74
|
-
<li class="breadcrumb-item"
|
|
89
|
+
<li class="breadcrumb-item">
|
|
90
|
+
{%- if _authors_page -%}
|
|
91
|
+
<a href="{{ _authors_url | relative_url }}">Authors</a>
|
|
92
|
+
{%- else -%}
|
|
93
|
+
Authors
|
|
94
|
+
{%- endif -%}
|
|
95
|
+
</li>
|
|
75
96
|
<li class="breadcrumb-item active" aria-current="page">{{ author_name }}</li>
|
|
76
97
|
</ol>
|
|
77
98
|
</nav>
|
data/_layouts/root.html
CHANGED
|
@@ -31,7 +31,21 @@
|
|
|
31
31
|
-->
|
|
32
32
|
|
|
33
33
|
<!doctype html>
|
|
34
|
-
|
|
34
|
+
{%- comment -%}
|
|
35
|
+
Resolve the server-side data-bs-theme:
|
|
36
|
+
dark → dark
|
|
37
|
+
light → light
|
|
38
|
+
auto → dark (inline script in tokens-inline.html corrects this before first paint)
|
|
39
|
+
We also store the raw config value in data-color-mode-default so the inline
|
|
40
|
+
script can read it without a Liquid evaluation.
|
|
41
|
+
{%- endcomment -%}
|
|
42
|
+
{%- assign _cm = site.color_mode_default | default: 'auto' -%}
|
|
43
|
+
{%- if _cm == 'light' -%}
|
|
44
|
+
{%- assign _bs_theme = 'light' -%}
|
|
45
|
+
{%- else -%}
|
|
46
|
+
{%- assign _bs_theme = 'dark' -%}
|
|
47
|
+
{%- endif -%}
|
|
48
|
+
<html lang="{{ site.locale | slice: 0,2 | default: 'en' }}" class="no-js" data-bs-theme="{{ _bs_theme }}" data-color-mode-default="{{ _cm }}" data-theme-skin="{{ site.theme_skin | default: 'dark' }}" data-zer0-bg="{{ site.theme_background.enabled | default: true }}">
|
|
35
49
|
<head>
|
|
36
50
|
<!-- =============================================== -->
|
|
37
51
|
<!-- HEAD SECTION: Meta tags, styles, and SEO setup -->
|
data/scripts/bin/validate
CHANGED
|
@@ -413,6 +413,7 @@ classified_files = %w[
|
|
|
413
413
|
_config_secrets_local.yml
|
|
414
414
|
pages/_about/settings/_config.yml
|
|
415
415
|
.github/ISSUE_TEMPLATE/config.yml
|
|
416
|
+
.issues/config.yml
|
|
416
417
|
]
|
|
417
418
|
|
|
418
419
|
# T-018: the admin config page renders pages/_about/settings/_config.yml via
|
|
@@ -499,6 +500,19 @@ RUBY
|
|
|
499
500
|
success "Navigation data is valid"
|
|
500
501
|
}
|
|
501
502
|
|
|
503
|
+
validate_features_registry() {
|
|
504
|
+
step "Validating feature registry (features.yml)..."
|
|
505
|
+
|
|
506
|
+
# Delegates to the shared canonical checker so this gate and the `features`
|
|
507
|
+
# test suite never drift. Hard-fails on master/_data drift, schema
|
|
508
|
+
# violations, stale references, and missing provenance; warns (until
|
|
509
|
+
# FEATURES_STRICT=1) on missing tests. See
|
|
510
|
+
# .github/instructions/features.instructions.md.
|
|
511
|
+
FEATURES_STRICT="${FEATURES_STRICT:-0}" ruby "$REPO_ROOT/scripts/validate-features.rb"
|
|
512
|
+
|
|
513
|
+
success "Feature registry is valid"
|
|
514
|
+
}
|
|
515
|
+
|
|
502
516
|
run_jekyll_build() {
|
|
503
517
|
if [[ "$SKIP_JEKYLL" == "true" ]]; then
|
|
504
518
|
warn "Skipping Jekyll build"
|
|
@@ -611,6 +625,9 @@ main() {
|
|
|
611
625
|
validate_navigation_data
|
|
612
626
|
echo ""
|
|
613
627
|
|
|
628
|
+
validate_features_registry
|
|
629
|
+
echo ""
|
|
630
|
+
|
|
614
631
|
run_jekyll_build
|
|
615
632
|
echo ""
|
|
616
633
|
|
|
@@ -627,7 +644,7 @@ main() {
|
|
|
627
644
|
print_summary "Validation Complete" \
|
|
628
645
|
"Repository files: pass" \
|
|
629
646
|
"Version consistency: pass" \
|
|
630
|
-
"YAML, config contract, and
|
|
647
|
+
"YAML, config contract, navigation data, and feature registry: pass" \
|
|
631
648
|
"Jekyll build: $([[ "$SKIP_JEKYLL" == "true" ]] && echo skipped || echo pass)" \
|
|
632
649
|
"Jekyll doctor: $([[ "$SKIP_DOCTOR" == "true" ]] && echo skipped || echo pass)" \
|
|
633
650
|
"Compiled assets: $([[ "$SKIP_ASSETS" == "true" ]] && echo skipped || echo pass)" \
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
classify_changes.py — the auto-merge smuggle guard.
|
|
4
|
+
|
|
5
|
+
Reads a list of changed file paths (one per line on stdin, e.g. from
|
|
6
|
+
`gh pr diff <n> --name-only`) and prints the distinct CATEGORIES present, one
|
|
7
|
+
per line, from:
|
|
8
|
+
|
|
9
|
+
content — pages/**, _data/{quests,navigation,...}, assets/** (the site)
|
|
10
|
+
infra — .github/**, scripts/**, Dockerfile, docker-compose, Gemfile*, Makefile
|
|
11
|
+
config — _config*.yml, _data/ai.yml, _data/brand/**, .cms/**, *.json config
|
|
12
|
+
data — other _data/**
|
|
13
|
+
|
|
14
|
+
The content auto-merge workflow refuses to merge any PR whose categories include
|
|
15
|
+
anything other than `content` — so an `auto:content` PR can never quietly carry a
|
|
16
|
+
workflow, dependency, brand-rule, or AI-config change to main without a human.
|
|
17
|
+
Unknown paths classify as `infra` (fail safe — when unsure, require a human).
|
|
18
|
+
|
|
19
|
+
The content category is deliberately broad (it includes theme files like
|
|
20
|
+
`_layouts/**`, `_includes/**`, `_sass/**`). A caller that needs a TIGHTER scope
|
|
21
|
+
than "any content" — e.g. the issue autopilot, whose resolver may only edit
|
|
22
|
+
`pages/**`, `assets/**`, `_data/quests/**` — adds `--allow-globs` to require that
|
|
23
|
+
every changed path also matches one of the given globs. `--content-only` and
|
|
24
|
+
`--allow-globs` compose: both must pass.
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
gh pr diff 123 --name-only | python3 scripts/ci/classify_changes.py
|
|
28
|
+
python3 scripts/ci/classify_changes.py a.md b.yml # paths as args
|
|
29
|
+
... | python3 scripts/ci/classify_changes.py --content-only # exit 0 iff content-only
|
|
30
|
+
... | python3 scripts/ci/classify_changes.py --content-only \
|
|
31
|
+
--allow-globs 'pages/**' 'assets/**' '_data/quests/**' # tighter scope
|
|
32
|
+
"""
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import argparse
|
|
36
|
+
import sys
|
|
37
|
+
from fnmatch import fnmatch
|
|
38
|
+
|
|
39
|
+
# Order matters: first matching rule wins. Most specific first.
|
|
40
|
+
# THEME REPO: the theme itself (_layouts/_includes/_sass/_plugins/lib/assets) is
|
|
41
|
+
# the PRODUCT, so it is `infra` (never auto-mergeable) — only docs/pages Markdown
|
|
42
|
+
# counts as `content`. This is the key difference from a content site.
|
|
43
|
+
RULES = [
|
|
44
|
+
("config", [
|
|
45
|
+
"_config.yml", "_config*.yml",
|
|
46
|
+
"_data/ai.yml",
|
|
47
|
+
".github/config/*", ".github/config/**",
|
|
48
|
+
"frontmatter.json", ".frontmatter/*", ".frontmatter/**",
|
|
49
|
+
]),
|
|
50
|
+
("infra", [
|
|
51
|
+
".github/*", ".github/**",
|
|
52
|
+
".claude/*", ".claude/**",
|
|
53
|
+
"scripts/*", "scripts/**",
|
|
54
|
+
"lib/*", "lib/**",
|
|
55
|
+
"_layouts/*", "_layouts/**", "_includes/*", "_includes/**",
|
|
56
|
+
"_sass/*", "_sass/**", "_plugins/*", "_plugins/**",
|
|
57
|
+
"assets/*", "assets/**",
|
|
58
|
+
"test/*", "test/**", "templates/*", "templates/**",
|
|
59
|
+
"Dockerfile", "docker-compose*.yml", "docker-compose*.yaml",
|
|
60
|
+
"Gemfile", "Gemfile.lock", "Makefile", "Rakefile", "install.sh",
|
|
61
|
+
"*.gemspec", "package.json", "package-lock.json",
|
|
62
|
+
]),
|
|
63
|
+
("content", [
|
|
64
|
+
"docs/*", "docs/**",
|
|
65
|
+
"pages/*", "pages/**",
|
|
66
|
+
"index.html", "index.md", "*.md",
|
|
67
|
+
]),
|
|
68
|
+
]
|
|
69
|
+
FALLBACK = "infra" # unknown -> require a human (fail safe)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def classify_one(path: str) -> str:
|
|
73
|
+
p = path.strip()
|
|
74
|
+
if not p:
|
|
75
|
+
return ""
|
|
76
|
+
for category, patterns in RULES:
|
|
77
|
+
for pat in patterns:
|
|
78
|
+
if fnmatch(p, pat):
|
|
79
|
+
return category
|
|
80
|
+
return "data" if p.startswith("_data/") else FALLBACK
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def main(argv=None) -> int:
|
|
84
|
+
ap = argparse.ArgumentParser(description="Classify changed paths for the auto-merge guard.")
|
|
85
|
+
ap.add_argument("paths", nargs="*", help="paths (else read stdin, one per line)")
|
|
86
|
+
ap.add_argument("--content-only", action="store_true",
|
|
87
|
+
help="exit 0 iff every path is content; exit 1 otherwise")
|
|
88
|
+
ap.add_argument("--allow-globs", nargs="*", default=None, metavar="GLOB",
|
|
89
|
+
help="require every changed path to match one of these globs "
|
|
90
|
+
"(tighter than --content-only); composes with it")
|
|
91
|
+
args = ap.parse_args(argv)
|
|
92
|
+
|
|
93
|
+
raw = [line for line in (args.paths if args.paths else sys.stdin.read().splitlines())
|
|
94
|
+
if line.strip()]
|
|
95
|
+
cats = []
|
|
96
|
+
for line in raw:
|
|
97
|
+
c = classify_one(line)
|
|
98
|
+
if c and c not in cats:
|
|
99
|
+
cats.append(c)
|
|
100
|
+
|
|
101
|
+
# When either gate is requested, ALL requested gates must pass (exit 0/1).
|
|
102
|
+
if args.content_only or args.allow_globs is not None:
|
|
103
|
+
ok = True
|
|
104
|
+
if args.content_only:
|
|
105
|
+
content_ok = bool(cats) and cats == ["content"]
|
|
106
|
+
if not content_ok:
|
|
107
|
+
others = [c for c in cats if c != "content"] or ["<empty diff>"]
|
|
108
|
+
print(f"not content-only: also touches {', '.join(others)}", file=sys.stderr)
|
|
109
|
+
ok = ok and content_ok
|
|
110
|
+
if args.allow_globs is not None:
|
|
111
|
+
offenders = [p.strip() for p in raw
|
|
112
|
+
if not any(fnmatch(p.strip(), g) for g in args.allow_globs)]
|
|
113
|
+
# An empty diff is never "allowed" — there is nothing legitimate to merge.
|
|
114
|
+
if not raw:
|
|
115
|
+
print("no changed paths — refusing (nothing to merge)", file=sys.stderr)
|
|
116
|
+
ok = False
|
|
117
|
+
elif offenders:
|
|
118
|
+
print(f"outside allowed scope ({', '.join(args.allow_globs)}): "
|
|
119
|
+
f"{', '.join(offenders)}", file=sys.stderr)
|
|
120
|
+
ok = False
|
|
121
|
+
return 0 if ok else 1
|
|
122
|
+
|
|
123
|
+
for c in cats:
|
|
124
|
+
print(c)
|
|
125
|
+
return 0
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
raise SystemExit(main())
|