jekyll-theme-zer0 1.30.0 → 1.31.1

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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +748 -571
  3. data/README.md +17 -3
  4. data/_data/backlog.yml +33 -10
  5. data/_data/consumers.yml +99 -0
  6. data/_data/features.yml +1 -1
  7. data/_data/i18n/fr.yml +30 -14
  8. data/_data/i18n/manifest.yml +63 -45
  9. data/_data/theme-manifest.yml +41 -39
  10. data/_data/ui-text.yml +1 -0
  11. data/_includes/README.md +1 -1
  12. data/_includes/analytics/posthog.html +28 -9
  13. data/_includes/components/README.md +1 -1
  14. data/_includes/components/background-image.html +7 -1
  15. data/_includes/components/cookie-consent.html +30 -30
  16. data/_includes/components/halfmoon.html +5 -3
  17. data/_includes/components/mermaid.html +14 -7
  18. data/_includes/components/preview-image.html +22 -3
  19. data/_includes/components/theme-preview-gallery.html +1 -1
  20. data/_includes/content/intro.html +21 -3
  21. data/_includes/content/seo.html +8 -0
  22. data/_includes/content/sitemap.html +220 -263
  23. data/_includes/core/branding.html +8 -15
  24. data/_includes/core/footer.html +3 -1
  25. data/_includes/core/head.html +33 -22
  26. data/_includes/core/header.html +14 -2
  27. data/_includes/core/tokens-inline.html +16 -1
  28. data/_includes/navigation/breadcrumbs.html +21 -15
  29. data/_includes/navigation/navbar.html +9 -6
  30. data/_includes/setup/wizard.html +5 -1
  31. data/_layouts/article.html +15 -3
  32. data/_layouts/home.html +13 -1
  33. data/_layouts/note.html +2 -1
  34. data/_layouts/notebook.html +2 -1
  35. data/_layouts/root.html +22 -0
  36. data/_sass/components/_cookie-banner.scss +48 -0
  37. data/_sass/components/_setup-wizard.scss +19 -1
  38. data/_sass/components/_ui-enhancements.scss +11 -0
  39. data/_sass/core/_navbar.scss +163 -70
  40. data/_sass/core/_sidebar-extras.scss +2 -14
  41. data/_sass/core/code-copy.scss +27 -8
  42. data/_sass/theme/_skins.scss +6 -2
  43. data/assets/css/extension-points-demo.css +27 -0
  44. data/assets/data/wiki-index.json +19 -0
  45. data/assets/js/auto-hide-nav.js +53 -13
  46. data/assets/js/code-copy.js +72 -1
  47. data/assets/js/extension-points-demo.js +48 -0
  48. data/assets/js/halfmoon.js +11 -1
  49. data/assets/js/mermaid-diagrams.js +128 -22
  50. data/assets/js/obsidian-local-graph.js +27 -5
  51. data/assets/js/search-modal.js +35 -3
  52. data/assets/js/setup-wizard.js +7 -2
  53. data/scripts/bin/validate +1 -0
  54. data/scripts/ci/test_visual_evidence_autogen.py +64 -2
  55. data/scripts/ci/visual_evidence_autogen.py +14 -4
  56. data/scripts/features/install-preview-generator +1 -1
  57. data/scripts/lib/README.md +12 -0
  58. data/scripts/lib/preview_generator.py +2 -2
  59. data/scripts/validate-features.rb +5 -8
  60. metadata +4 -2
@@ -50,36 +50,36 @@ Usage: Include in root.html layout
50
50
  Configuration: Uses site.posthog settings from _config.yml
51
51
  {% endcomment %}
52
52
 
53
- <div id="cookieConsent" class="cookie-consent-banner position-fixed bottom-0 start-0 end-0 py-3 px-3 shadow-lg" hidden>
54
- <div class="container-xl px-3 px-md-4">
55
- <div class="row align-items-center g-2">
56
- <div class="col-12 col-lg-8">
57
- <p class="h6 fw-semibold mb-2 mb-lg-0">
58
- <i class="bi bi-shield-check me-2" aria-hidden="true"></i>
59
- We value your privacy
60
- </p>
61
- <p class="mb-2 mb-lg-0 small text-body-secondary">
62
- This website uses cookies and similar technologies to enhance your browsing experience, analyze traffic, and provide personalized content.
63
- {% assign cookie_page_urls = site.html_pages | map: "url" | join: "|" | prepend: "|" | append: "|" %}
64
- {% if site.privacy_policy_url or cookie_page_urls contains "|/privacy-policy/|" or cookie_page_urls contains "|/privacy-policy|" %}
65
- <a href="{{ site.privacy_policy_url | default: '/privacy-policy/' | relative_url }}" class="link-body-emphasis text-decoration-underline">Learn more in our Privacy Policy</a>.
66
- {% endif %}
67
- </p>
68
- </div>
69
- <div class="col-12 col-lg-4">
70
- <div class="d-flex flex-wrap gap-2 justify-content-lg-end">
71
- <button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#cookieSettingsModal">
72
- <i class="bi bi-gear me-1"></i>
73
- Manage Cookies
74
- </button>
75
- <button type="button" class="btn btn-sm btn-secondary" id="rejectAllCookies">
76
- Reject All
77
- </button>
78
- <button type="button" class="btn btn-sm btn-primary" id="acceptAllCookies">
79
- Accept All
80
- </button>
81
- </div>
82
- </div>
53
+ {% comment %}
54
+ Compact bottom bar. One row from md up (copy left, actions right); on phones
55
+ a two-line summary over a single row of three equal buttons. The banner used
56
+ to stack full-width buttons under a heading and a long sentence: 324px tall
57
+ at 400px wide (most of the first screen) and 94px on desktop, covering hero
58
+ copy. The per-category detail lives in the Manage modal.
59
+ {% endcomment %}
60
+ <div id="cookieConsent" class="cookie-consent-banner position-fixed bottom-0 start-0 end-0 shadow-lg" role="region" aria-label="Cookie consent" hidden>
61
+ <div class="container-xl cookie-consent-banner__inner">
62
+ <p class="cookie-consent-banner__text small mb-0">
63
+ <i class="bi bi-shield-check me-1" aria-hidden="true"></i>
64
+ <strong class="fw-semibold">We value your privacy.</strong>
65
+ <span class="text-body-secondary">
66
+ We use cookies to analyze traffic and improve the site.
67
+ {% assign cookie_page_urls = site.html_pages | map: "url" | join: "|" | prepend: "|" | append: "|" %}
68
+ {% if site.privacy_policy_url or cookie_page_urls contains "|/privacy-policy/|" or cookie_page_urls contains "|/privacy-policy|" %}
69
+ <a href="{{ site.privacy_policy_url | default: '/privacy-policy/' | relative_url }}" class="link-body-emphasis text-decoration-underline">Privacy Policy</a>
70
+ {% endif %}
71
+ </span>
72
+ </p>
73
+ <div class="cookie-consent-banner__actions">
74
+ <button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#cookieSettingsModal">
75
+ <i class="bi bi-gear me-1" aria-hidden="true"></i>Manage<span class="d-none d-sm-inline"> Cookies</span>
76
+ </button>
77
+ <button type="button" class="btn btn-sm btn-secondary" id="rejectAllCookies">
78
+ Reject All
79
+ </button>
80
+ <button type="button" class="btn btn-sm btn-primary" id="acceptAllCookies">
81
+ Accept All
82
+ </button>
83
83
  </div>
84
84
  </div>
85
85
  </div>
@@ -14,16 +14,18 @@
14
14
  (halfmoon.js + core/color-mode-init.html enforce the same).
15
15
  {% endcomment %}
16
16
  {% unless site.color_mode_lock %}
17
+ {% comment %} Server-render the configured mode as selected (#521) so no-JS and pre-hydration state is right; halfmoon.js then applies any stored preference. {% endcomment %}
18
+ {%- assign _cm = site.color_mode_default | default: 'auto' -%}
17
19
  <div class="btn-group bd-theme-switch {{ include.class | default: '' }}" role="group" aria-label="Color mode">
18
- <button type="button" class="btn btn-outline-secondary btn-sm d-flex align-items-center justify-content-center gap-1" data-bs-theme-value="light" aria-pressed="false">
20
+ <button type="button" class="btn btn-outline-secondary btn-sm d-flex align-items-center justify-content-center gap-1{% if _cm == 'light' %} active{% endif %}" data-bs-theme-value="light" aria-pressed="{% if _cm == 'light' %}true{% else %}false{% endif %}">
19
21
  <svg class="bi flex-shrink-0" width="1em" height="1em" fill="currentColor" aria-hidden="true"><use href="#sun-fill"></use></svg>
20
22
  <span>Light</span>
21
23
  </button>
22
- <button type="button" class="btn btn-outline-secondary btn-sm d-flex align-items-center justify-content-center gap-1" data-bs-theme-value="dark" aria-pressed="false">
24
+ <button type="button" class="btn btn-outline-secondary btn-sm d-flex align-items-center justify-content-center gap-1{% if _cm == 'dark' %} active{% endif %}" data-bs-theme-value="dark" aria-pressed="{% if _cm == 'dark' %}true{% else %}false{% endif %}">
23
25
  <svg class="bi flex-shrink-0" width="1em" height="1em" fill="currentColor" aria-hidden="true"><use href="#moon-stars-fill"></use></svg>
24
26
  <span>Dark</span>
25
27
  </button>
26
- <button type="button" class="btn btn-outline-secondary btn-sm d-flex align-items-center justify-content-center gap-1" data-bs-theme-value="auto" aria-pressed="false">
28
+ <button type="button" class="btn btn-outline-secondary btn-sm d-flex align-items-center justify-content-center gap-1{% if _cm == 'auto' %} active{% endif %}" data-bs-theme-value="auto" aria-pressed="{% if _cm == 'auto' %}true{% else %}false{% endif %}">
27
29
  <svg class="bi flex-shrink-0" width="1em" height="1em" fill="currentColor" aria-hidden="true"><use href="#circle-half"></use></svg>
28
30
  <span>Auto</span>
29
31
  </button>
@@ -4,7 +4,7 @@
4
4
  Path: _includes/components/mermaid.html
5
5
  Purpose: Load Mermaid and the theme's diagram-figure behaviour on pages
6
6
  that opt in with `mermaid: true` (gated in core/head.html).
7
- Emits a config block and two deferred scripts; renders no
7
+ Emits a config block and one deferred script; renders no
8
8
  markup itself — every ```mermaid fence (and legacy
9
9
  <div class="mermaid">) becomes an accessible <figure> with a
10
10
  toolbar (zoom, fullscreen, copy source, download SVG), a
@@ -22,11 +22,15 @@
22
22
  Depends on: assets/vendor/mermaid/mermaid.min.js (vendored, no CDN —
23
23
  GitHub Pages-safe), assets/js/mermaid-diagrams.js,
24
24
  _sass/components/_mermaid.scss
25
- Notes: Both scripts are `defer`: Mermaid is a 3.3 MB bundle and must
26
- never block parsing (it used to load synchronously in <head>).
27
- Order matters — the vendor bundle first, then the behaviour,
28
- which converts fences at execution time, before code-copy.js's
29
- DOMContentLoaded pass decorates <pre> blocks. Diagram colours
25
+ Notes: The Mermaid bundle (~1 MB) is NOT emitted as a <script> any
26
+ more. mermaid-diagrams.js (deferred) converts fences into
27
+ figures at execution time, before code-copy.js's
28
+ DOMContentLoaded pass decorates <pre> blocks, and injects the
29
+ bundle from `src` below only when the page has a diagram and
30
+ the first one nears the viewport (IntersectionObserver). A
31
+ `mermaid: true` page without fences never downloads it. It used
32
+ to be a parse-time `defer` script on every opted-in page, and
33
+ before that a synchronous one in <head>. Diagram colours
30
34
  are not configured here: the script derives them from the live
31
35
  --zer0-*/--bs-* tokens so light/dark/wizard modes and skins
32
36
  work without per-mode overrides.
@@ -71,5 +75,8 @@
71
75
  }
72
76
  }
73
77
  </script>
74
- <script defer src="{{ mm_src | relative_url }}"></script>
78
+ {% comment %} Bundle URL for the lazy loader. Passed as a script global rather than
79
+ through the JSON block above: a URL read back out of DOM text and assigned
80
+ to script.src is a script-injection sink (CodeQL js/xss-through-dom). {% endcomment %}
81
+ <script>window.zer0MermaidSrc = {{ mm_src | relative_url | jsonify }};</script>
75
82
  <script defer src="{{ '/assets/js/mermaid-diagrams.js' | relative_url }}"></script>
@@ -7,9 +7,18 @@
7
7
  src=post.preview
8
8
  alt=post.title
9
9
  class="card-img-top"
10
- style="height: 180px; object-fit: cover;"
10
+ style="height: 180px; object-fit: cover;"
11
11
  -%}
12
-
12
+
13
+ Optional: loading (default "lazy"), fetchpriority, decoding, width, height.
14
+ All but `loading` are emitted only when passed, so existing call sites
15
+ render unchanged. width/height are the image's intrinsic size (or its
16
+ aspect ratio): they let the browser reserve the box before the file
17
+ arrives (Lighthouse `unsized-images`) and never distort the image, since
18
+ CSS height:auto / object-fit still win once it has loaded.
19
+ An above-the-fold hero (the LCP element) should pass loading="eager"
20
+ fetchpriority="high" -- see the article.html hero.
21
+
13
22
  WHICH INCLUDE: this one renders an <img> in the content flow. For cover art
14
23
  painted as a CSS background on an otherwise empty box, use the sibling
15
24
  components/background-image.html -- a background image is invisible to
@@ -20,6 +29,8 @@
20
29
  The component will auto-prepend the assets_prefix from _config.yml.
21
30
  Examples:
22
31
  - /images/previews/my-image.png → /assets/images/previews/my-image.png
32
+ - images/previews/my-image.png → /assets/images/previews/my-image.png
33
+ (a missing leading slash used to produce /assetsimages/previews/…)
23
34
  - /assets/images/previews/my-image.png → unchanged
24
35
  - https://example.com/image.png → unchanged (external URL)
25
36
  {% endcomment %}
@@ -33,6 +44,14 @@
33
44
  {% assign assets_prefix = site.preview_images.assets_prefix | default: '/assets' %}
34
45
  {% assign auto_prefix = site.preview_images.auto_prefix | default: true %}
35
46
 
47
+ {% comment %} Normalise a site-relative path to start with "/" so joining it to
48
+ assets_prefix can never glue two segments together ("images/x.png" became
49
+ "/assetsimages/x.png"). URLs and data: URIs are left alone. {% endcomment %}
50
+ {% unless img == nil or img == "" or img contains '://' or img contains 'data:' %}
51
+ {% assign img_first = img | slice: 0 %}
52
+ {% if img_first != '/' %}{% assign img = '/' | append: img %}{% endif %}
53
+ {% endunless %}
54
+
36
55
  {% comment %} Determine final image source {% endcomment %}
37
56
  {% if img contains '://' %}
38
57
  {% comment %} External URL - use as-is {% endcomment %}
@@ -53,4 +72,4 @@
53
72
  class="{{ class }}"
54
73
  alt="{{ alt | escape }}"
55
74
  {% if style %}style="{{ style }}"{% endif %}
56
- loading="{{ loading }}">
75
+ loading="{{ loading }}"{% if include.width %} width="{{ include.width }}"{% endif %}{% if include.height %} height="{{ include.height }}"{% endif %}{% if include.fetchpriority %} fetchpriority="{{ include.fetchpriority }}"{% endif %}{% if include.decoding %} decoding="{{ include.decoding }}"{% endif %}>
@@ -519,7 +519,7 @@ docker-compose up
519
519
  <i class="bi bi-person-circle" title="bi-person-circle"></i>
520
520
  <i class="bi bi-palette" title="bi-palette"></i>
521
521
  <i class="bi bi-code-slash" title="bi-code-slash"></i>
522
- <i class="bi bi-file-earmark-text"title="bi-file-earmark-text"></i>
522
+ <i class="bi bi-file-earmark-text" title="bi-file-earmark-text"></i>
523
523
  <i class="bi bi-check-circle text-success" title="success"></i>
524
524
  <i class="bi bi-x-circle text-danger" title="danger"></i>
525
525
  <i class="bi bi-exclamation-triangle text-warning" title="warning"></i>
@@ -29,6 +29,13 @@
29
29
  {% assign assets_prefix = site.preview_images.assets_prefix | default: '/assets' %}
30
30
  {% assign auto_prefix = site.preview_images.auto_prefix | default: true %}
31
31
 
32
+ {% comment %} Leading-slash normalisation, as in components/preview-image.html:
33
+ `preview: images/x.png` used to resolve to /assetsimages/x.png. {% endcomment %}
34
+ {% unless preview_image == nil or preview_image == "" or preview_image contains '://' or preview_image contains 'data:' %}
35
+ {% assign preview_first = preview_image | slice: 0 %}
36
+ {% if preview_first != '/' %}{% assign preview_image = '/' | append: preview_image %}{% endif %}
37
+ {% endunless %}
38
+
32
39
  {% if preview_image contains '://' %}
33
40
  {% assign preview_path = preview_image %}
34
41
  {% elsif auto_prefix and preview_image != blank %}
@@ -45,7 +52,16 @@
45
52
  Build the file path, branch, and repo owner for action buttons.
46
53
  {% endcomment %}
47
54
  {% assign repo_branch = site.branch | default: "main" %}
48
- {% assign file_path = page_dir | append: "/" | append: page.path %}
55
+ {% comment %}
56
+ Only join with "/" when there is a collection directory. Pages outside a
57
+ collection have an empty page_dir, and the unconditional join produced
58
+ ".../blob/main//pages/tags.md" (double slash) in the edit/source links.
59
+ {% endcomment %}
60
+ {% if page_dir and page_dir != "" %}
61
+ {% assign file_path = page_dir | append: "/" | append: page.path %}
62
+ {% else %}
63
+ {% assign file_path = page.path %}
64
+ {% endif %}
49
65
  {% assign repo_parts = site.repository | split: "/" %}
50
66
  {% assign repo_owner = repo_parts[0] %}
51
67
  {% assign page_absolute_url = page.url | absolute_url %}
@@ -232,7 +248,8 @@
232
248
  <div class="bd-intro-actions">
233
249
  {% comment %} Share Button {% endcomment %}
234
250
  <div class="dropdown">
235
- <button class="btn btn-sm btn-info dropdown-toggle" type="button" id="shareDropdownBottom" data-bs-toggle="dropdown" aria-expanded="false">
251
+ {%- comment -%} aria-label keeps an accessible name below 576px, where the visible text is d-none. {%- endcomment -%}
252
+ <button class="btn btn-sm btn-info dropdown-toggle" type="button" id="shareDropdownBottom" data-bs-toggle="dropdown" aria-expanded="false" aria-label="{{ ui.share_label | default: 'Share' }}">
236
253
  <i class="bi bi-share" aria-hidden="true"></i>
237
254
  <span class="d-none d-sm-inline ms-1">{{ ui.share_label | default: "Share" }}</span>
238
255
  </button>
@@ -293,7 +310,8 @@
293
310
  <a href="https://github.com/{{ site.repository }}/blob/{{ repo_branch }}/{{ file_path }}"
294
311
  class="btn btn-sm btn-dark bd-intro-action-link"
295
312
  target="_blank"
296
- rel="noopener noreferrer">
313
+ rel="noopener noreferrer"
314
+ aria-label="{{ ui.edit_on_github | default: 'Edit on GitHub' }}">
297
315
  <i class="bi bi-github" aria-hidden="true"></i>
298
316
  <span class="d-none d-sm-inline ms-1">{{ ui.edit_on_github | default: "Edit on GitHub" }}</span>
299
317
  </a>
@@ -43,6 +43,14 @@
43
43
  nil seo_image — og:image was silently dropped on every page without a
44
44
  page.header image, defeating the site.og_image fallback entirely.
45
45
  {%- endcomment -%}
46
+ {%- comment -%} Leading-slash normalisation, as in components/preview-image.html:
47
+ `preview: images/x.png` used to become og:image …/assetsimages/x.png. {%- endcomment -%}
48
+ {%- if page_teaser_image and page_teaser_image != "" -%}
49
+ {%- unless page_teaser_image contains '://' or page_teaser_image contains 'data:' -%}
50
+ {%- assign teaser_first = page_teaser_image | slice: 0 -%}
51
+ {%- if teaser_first != '/' -%}{%- assign page_teaser_image = '/' | append: page_teaser_image -%}{%- endif -%}
52
+ {%- endunless -%}
53
+ {%- endif -%}
46
54
  {%- if page_teaser_image contains '://' -%}
47
55
  {%- comment -%} External URL - use as-is {%- endcomment -%}
48
56
  {%- elsif auto_prefix and page_teaser_image and page_teaser_image != blank -%}