jekyll-theme-zer0 1.28.0 → 1.30.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.
Files changed (191) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1143 -13
  3. data/_data/README.md +2 -0
  4. data/_data/ai.yml +5 -3
  5. data/_data/ai_pricing.yml +36 -0
  6. data/_data/backlog.yml +507 -2
  7. data/_data/consumers.yml +157 -9
  8. data/_data/features.yml +303 -20
  9. data/_data/feedback_types.yml +17 -12
  10. data/_data/i18n/fr.yml +12 -7
  11. data/_data/i18n/manifest.yml +39 -12
  12. data/_data/ingredient_densities.yml +122 -0
  13. data/_data/landing.yml +5 -2
  14. data/_data/navigation/main.yml +16 -0
  15. data/_data/navigation/quickstart.yml +4 -0
  16. data/_data/recipe_courses.yml +64 -0
  17. data/_data/site_builder.yml +874 -0
  18. data/_data/theme-manifest.yml +160 -124
  19. data/_data/ui-text.yml +26 -0
  20. data/_includes/README.md +26 -2
  21. data/_includes/analytics/posthog.html +2 -2
  22. data/_includes/components/admin-links.html +2 -2
  23. data/_includes/components/admin-tabs.html +2 -2
  24. data/_includes/components/ai-chat.html +14 -11
  25. data/_includes/components/analytics-dashboard.html +8 -8
  26. data/_includes/components/author-bio.html +1 -1
  27. data/_includes/components/author-card.html +10 -2
  28. data/_includes/components/author-eeat.html +4 -4
  29. data/_includes/components/background-customizer.html +10 -10
  30. data/_includes/components/background-image.html +114 -0
  31. data/_includes/components/background-settings.html +28 -15
  32. data/_includes/components/collection-manager.html +5 -5
  33. data/_includes/components/component-showcase.html +13 -13
  34. data/_includes/components/config-editor.html +12 -12
  35. data/_includes/components/config-viewer.html +8 -8
  36. data/_includes/components/cookie-consent.html +15 -15
  37. data/_includes/components/cta-button.html +7 -2
  38. data/_includes/components/dev-shortcuts.html +7 -7
  39. data/_includes/components/env-dashboard.html +8 -8
  40. data/_includes/components/env-switcher.html +9 -9
  41. data/_includes/components/feature-card.html +2 -2
  42. data/_includes/components/halfmoon.html +2 -2
  43. data/_includes/components/info-section.html +42 -37
  44. data/_includes/components/js-cdn.html +15 -15
  45. data/_includes/components/language-toggle.html +168 -21
  46. data/_includes/components/mermaid.html +72 -435
  47. data/_includes/components/nanobar.html +5 -5
  48. data/_includes/components/nav-editor.html +2 -2
  49. data/_includes/components/nav-export.html +2 -2
  50. data/_includes/components/nav-overview.html +2 -2
  51. data/_includes/components/page-feedback.html +45 -30
  52. data/_includes/components/page-views-init.html +55 -0
  53. data/_includes/components/page-views.html +33 -0
  54. data/_includes/components/post-card.html +22 -22
  55. data/_includes/components/post-type-badge.html +2 -2
  56. data/_includes/components/powered-by.html +2 -2
  57. data/_includes/components/preview-image.html +6 -0
  58. data/_includes/components/quick-index.html +2 -2
  59. data/_includes/components/recipe-card.html +67 -0
  60. data/_includes/components/recipe-duration.html +50 -0
  61. data/_includes/components/recipe-grams.html +58 -0
  62. data/_includes/components/recipe-index.html +96 -0
  63. data/_includes/components/recipe-ingredients.html +90 -0
  64. data/_includes/components/recipe-meta.html +96 -0
  65. data/_includes/components/recipe-nutrition.html +57 -0
  66. data/_includes/components/recipe-qty.html +73 -0
  67. data/_includes/components/recipe-ratio.html +151 -0
  68. data/_includes/components/recipe-scaler.html +73 -0
  69. data/_includes/components/recipe-steps.html +86 -0
  70. data/_includes/components/recipe-temp.html +45 -0
  71. data/_includes/components/search-modal.html +29 -4
  72. data/_includes/components/searchbar.html +2 -2
  73. data/_includes/components/shortcuts-modal.html +3 -0
  74. data/_includes/components/svg-background.html +2 -2
  75. data/_includes/components/theme-customizer.html +2 -2
  76. data/_includes/components/theme-info.html +14 -7
  77. data/_includes/components/theme-preview-gallery.html +22 -22
  78. data/_includes/content/giscus.html +2 -2
  79. data/_includes/content/intro.html +8 -8
  80. data/_includes/content/jsonld-faq.html +2 -2
  81. data/_includes/content/jsonld-software.html +24 -5
  82. data/_includes/content/seo.html +4 -4
  83. data/_includes/content/sitemap.html +27 -27
  84. data/_includes/content/toc.html +183 -183
  85. data/_includes/core/branding.html +6 -6
  86. data/_includes/core/console-capture.html +32 -74
  87. data/_includes/core/favicon.html +49 -7
  88. data/_includes/core/footer-fabs.html +17 -3
  89. data/_includes/core/footer.html +49 -34
  90. data/_includes/core/head.html +110 -86
  91. data/_includes/core/header.html +76 -54
  92. data/_includes/docs/bootstrap-docs.html +8 -8
  93. data/_includes/landing/landing-install-cards.html +2 -2
  94. data/_includes/landing/landing-quick-links.html +1 -1
  95. data/_includes/navigation/admin-nav.html +2 -2
  96. data/_includes/navigation/nav-tree.html +8 -8
  97. data/_includes/navigation/navbar.html +12 -12
  98. data/_includes/navigation/section-sidebar.html +109 -27
  99. data/_includes/navigation/sidebar-config.html +36 -2
  100. data/_includes/navigation/sidebar-left.html +17 -16
  101. data/_includes/navigation/sidebar-right.html +8 -7
  102. data/_includes/obsidian/full-graph.html +2 -2
  103. data/_includes/setup/claude-session.html +72 -0
  104. data/_includes/setup/prereq-checklist.html +90 -0
  105. data/_includes/setup/wizard.html +924 -222
  106. data/_includes/stats/stats-categories.html +8 -8
  107. data/_includes/stats/stats-header.html +14 -14
  108. data/_includes/stats/stats-metrics.html +14 -14
  109. data/_includes/stats/stats-no-data.html +12 -12
  110. data/_includes/stats/stats-overview.html +6 -6
  111. data/_includes/stats/stats-tags.html +8 -8
  112. data/_layouts/404.html +38 -24
  113. data/_layouts/README.md +2 -0
  114. data/_layouts/admin.html +24 -24
  115. data/_layouts/article.html +43 -33
  116. data/_layouts/author.html +20 -20
  117. data/_layouts/authors.html +2 -2
  118. data/_layouts/book-abc.html +12 -12
  119. data/_layouts/book-story.html +15 -15
  120. data/_layouts/book.html +12 -12
  121. data/_layouts/collection.html +33 -33
  122. data/_layouts/cookbook.html +88 -0
  123. data/_layouts/default.html +31 -28
  124. data/_layouts/home.html +23 -23
  125. data/_layouts/index.html +10 -10
  126. data/_layouts/landing.html +17 -17
  127. data/_layouts/news.html +44 -44
  128. data/_layouts/note.html +38 -38
  129. data/_layouts/notebook.html +34 -34
  130. data/_layouts/recipe.html +274 -0
  131. data/_layouts/root.html +92 -55
  132. data/_layouts/section.html +62 -33
  133. data/_layouts/setup.html +3 -3
  134. data/_layouts/sitemap-collection.html +49 -49
  135. data/_layouts/stats.html +40 -40
  136. data/_layouts/tag.html +12 -12
  137. data/_layouts/welcome.html +21 -21
  138. data/_sass/components/_callout.scss +1 -1
  139. data/_sass/components/_footer.scss +37 -1
  140. data/_sass/components/_mermaid.scss +375 -0
  141. data/_sass/components/_page-views.scss +36 -0
  142. data/_sass/components/_recipe.scss +506 -0
  143. data/_sass/components/_setup-wizard.scss +764 -0
  144. data/_sass/components/_ui-enhancements.scss +6 -6
  145. data/_sass/core/_navbar.scss +261 -46
  146. data/_sass/layouts/_landing.scss +2 -2
  147. data/_sass/layouts/_navbar-extras.scss +14 -4
  148. data/_sass/tokens/_color.scss +6 -0
  149. data/_sass/tokens/_index.scss +2 -0
  150. data/_sass/tokens/_radius.scss +21 -0
  151. data/_sass/tokens/_typography.scss +4 -0
  152. data/_sass/utilities/_focus.scss +14 -0
  153. data/assets/css/main.scss +4 -0
  154. data/assets/js/ai-chat.js +47 -5
  155. data/assets/js/fleet-feedback-capture.js +124 -0
  156. data/assets/js/fleet-feedback.js +853 -0
  157. data/assets/js/mermaid-diagrams.js +1267 -0
  158. data/assets/js/modules/navigation/config.js +9 -6
  159. data/assets/js/modules/navigation/navbar.js +55 -0
  160. data/assets/js/modules/navigation/scroll-spy.js +315 -80
  161. data/assets/js/modules/theme/appearance.js +8 -2
  162. data/assets/js/obsidian-wiki-links.js +8 -3
  163. data/assets/js/page-feedback.js +125 -192
  164. data/assets/js/page-views.js +372 -0
  165. data/assets/js/recipe-scaler.js +501 -0
  166. data/assets/js/search-modal.js +36 -0
  167. data/assets/js/setup-wizard.js +2279 -226
  168. data/assets/js/site-builder.js +1834 -0
  169. data/assets/js/ui-enhancements.js +11 -3
  170. data/scripts/README.md +44 -0
  171. data/scripts/ai/README.md +38 -0
  172. data/scripts/ai/api_call.rb +124 -0
  173. data/scripts/ai/usage.rb +314 -0
  174. data/scripts/ai/usage_report.rb +225 -0
  175. data/scripts/bin/audit-consumer +39 -7
  176. data/scripts/bin/giscus-discussions +213 -14
  177. data/scripts/bin/manifest +35 -12
  178. data/scripts/ci/agent_review_result.py +164 -0
  179. data/scripts/ci/test_agent_review_result.py +172 -0
  180. data/scripts/ci/test_visual_evidence_autogen.py +341 -0
  181. data/scripts/ci/visual_evidence_autogen.py +1060 -0
  182. data/scripts/content-review.rb +20 -1
  183. data/scripts/design-system-check.rb +170 -0
  184. data/scripts/lib/audit.sh +42 -2
  185. data/scripts/lint-liquid-raw.rb +137 -0
  186. data/scripts/test/integration/mermaid +22 -8
  187. data/scripts/test/lib/run_tests.sh +3 -1
  188. data/scripts/test/lib/test_agent_review_result.sh +27 -0
  189. data/scripts/test/lib/test_visual_evidence_autogen.sh +24 -0
  190. data/scripts/translate.rb +94 -16
  191. metadata +48 -2
@@ -1,5 +1,5 @@
1
1
  {% comment %} Feature: ZER0-046 {% endcomment %}
2
- <!--
2
+ {% comment %}
3
3
  ===================================================================
4
4
  INFO SECTION - Settings Offcanvas with Tabbed Interface
5
5
  ===================================================================
@@ -7,13 +7,14 @@
7
7
  File: info-section.html
8
8
  Path: _includes/components/info-section.html
9
9
  Purpose: Unified settings offcanvas panel with three tabs —
10
- Appearance (color mode, skin, backgrounds, primary color),
10
+ Appearance (language, color mode, skin, backgrounds, primary color),
11
11
  Site (environment, theme & build info, admin links) and
12
12
  Developer (page location, metadata, source shortcuts).
13
13
 
14
14
  Dependencies:
15
15
  - Bootstrap 5 Offcanvas, Tabs
16
16
  - _includes/components/env-detect.html (sets is_production/build_env)
17
+ - _includes/components/language-toggle.html (panel variant — Language)
17
18
  - _includes/components/halfmoon.html (color-mode segmented control)
18
19
  - _includes/components/background-settings.html (skin + backgrounds)
19
20
  - _includes/components/env-switcher.html
@@ -34,23 +35,23 @@
34
35
  - https://getbootstrap.com/docs/5.3/components/offcanvas/
35
36
  - https://getbootstrap.com/docs/5.3/components/navs-tabs/
36
37
  ===================================================================
37
- -->
38
+ {% endcomment %}
38
39
 
39
40
  {% include components/env-detect.html %}
40
41
 
41
- <!-- Settings Offcanvas -->
42
+ {% comment %} Settings Offcanvas {% endcomment %}
42
43
  <div class="offcanvas offcanvas-end" tabindex="-1" id="info-section" aria-labelledby="infoSectionLabel">
43
44
 
44
- <!-- Offcanvas Header -->
45
+ {% comment %} Offcanvas Header {% endcomment %}
45
46
  <div class="offcanvas-header border-bottom pb-0 d-block">
46
47
  <div class="d-flex align-items-center justify-content-between mb-2">
47
- <h5 class="offcanvas-title" id="infoSectionLabel">
48
+ <h2 class="offcanvas-title h5" id="infoSectionLabel">
48
49
  <i class="bi bi-gear-wide-connected me-2" aria-hidden="true"></i>Settings
49
- </h5>
50
+ </h2>
50
51
  <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
51
52
  </div>
52
53
 
53
- <!-- Tab Navigation -->
54
+ {% comment %} Tab Navigation {% endcomment %}
54
55
  <ul class="nav nav-tabs nav-fill" id="infoTabs" role="tablist">
55
56
  <li class="nav-item" role="presentation">
56
57
  <button class="nav-link active" id="appearance-tab" data-bs-toggle="tab" data-bs-target="#appearance-pane" type="button" role="tab" aria-controls="appearance-pane" aria-selected="true">
@@ -75,25 +76,29 @@
75
76
  </ul>
76
77
  </div>
77
78
 
78
- <!-- Offcanvas Body with Tab Content -->
79
+ {% comment %} Offcanvas Body with Tab Content {% endcomment %}
79
80
  <div class="offcanvas-body">
80
81
  <div class="tab-content" id="infoTabContent">
81
82
 
82
- <!-- Appearance Tab — everything that changes how the site looks -->
83
+ {% comment %} Appearance Tab — everything that changes how the site looks {% endcomment %}
83
84
  <div class="tab-pane fade show active" id="appearance-pane" role="tabpanel" aria-labelledby="appearance-tab" tabindex="0">
84
85
 
85
- <!-- Color Mode -->
86
+ {% comment %} Language — renders its own heading, and nothing at all when
87
+ site.translation is disabled. NOT cached: page-dependent. {% endcomment %}
88
+ {% include components/language-toggle.html variant="panel" %}
89
+
90
+ {% comment %} Color Mode {% endcomment %}
86
91
  <div class="mb-4">
87
- <h6 class="text-body-secondary small text-uppercase fw-semibold mb-2">
92
+ <h3 class="h6 text-body-secondary small text-uppercase fw-semibold mb-2">
88
93
  <i class="bi bi-moon-stars me-1" aria-hidden="true"></i>Color Mode
89
- </h6>
94
+ </h3>
90
95
  {% include components/halfmoon.html class="w-100" %}
91
96
  </div>
92
97
 
93
- <!-- Theme skin + SVG backgrounds (skin buttons, toggle, opacity) -->
98
+ {% comment %} Theme skin + SVG backgrounds (skin buttons, toggle, opacity) {% endcomment %}
94
99
  {% include_cached components/background-settings.html %}
95
100
 
96
- <!-- Primary color picker mounts here (appearance.js, optional) -->
101
+ {% comment %} Primary color picker mounts here (appearance.js, optional) {% endcomment %}
97
102
  <div id="zer0AppearanceSlot"></div>
98
103
 
99
104
  <p class="text-body-secondary small mb-0 mt-4">
@@ -102,49 +107,49 @@
102
107
  </p>
103
108
  </div>
104
109
 
105
- <!-- Site Tab — where the site runs and what it is built from -->
110
+ {% comment %} Site Tab — where the site runs and what it is built from {% endcomment %}
106
111
  <div class="tab-pane fade" id="site-pane" role="tabpanel" aria-labelledby="site-tab" tabindex="0">
107
112
 
108
- <!-- Environment -->
113
+ {% comment %} Environment {% endcomment %}
109
114
  <div class="mb-4">
110
- <h6 class="text-body-secondary small text-uppercase fw-semibold mb-2">
115
+ <h3 class="h6 text-body-secondary small text-uppercase fw-semibold mb-2">
111
116
  <i class="bi bi-hdd-network me-1" aria-hidden="true"></i>Environment
112
- </h6>
117
+ </h3>
113
118
  {% include components/env-switcher.html %}
114
119
  </div>
115
120
 
116
- <!-- Theme & Build -->
121
+ {% comment %} Theme & Build {% endcomment %}
117
122
  <div class="mb-4">
118
- <h6 class="text-body-secondary small text-uppercase fw-semibold mb-2">
123
+ <h3 class="h6 text-body-secondary small text-uppercase fw-semibold mb-2">
119
124
  <i class="bi bi-info-circle me-1" aria-hidden="true"></i>Theme &amp; Build
120
- </h6>
125
+ </h3>
121
126
  {% include components/theme-info.html %}
122
127
  </div>
123
128
 
124
- <!-- Admin quick links — cached include; renders only links to pages
125
- that exist in the build (see components/admin-links.html) -->
129
+ {% comment %} Admin quick links — cached include; renders only links to pages
130
+ that exist in the build (see components/admin-links.html) {% endcomment %}
126
131
  {% include_cached components/admin-links.html %}
127
132
  </div>
128
133
 
129
- <!-- Developer Tab — tools scoped to the current page -->
134
+ {% comment %} Developer Tab — tools scoped to the current page {% endcomment %}
130
135
  <div class="tab-pane fade" id="developer-pane" role="tabpanel" aria-labelledby="developer-tab" tabindex="0">
131
136
 
132
- <!-- Page Location — breadcrumbs render nothing on the homepage or
133
- when site.breadcrumbs is off, so the section mirrors that gate -->
137
+ {% comment %} Page Location — breadcrumbs render nothing on the homepage or
138
+ when site.breadcrumbs is off, so the section mirrors that gate {% endcomment %}
134
139
  {% if page.url != "/" and site.breadcrumbs %}
135
140
  <div class="mb-4">
136
- <h6 class="text-body-secondary small text-uppercase fw-semibold mb-2">
141
+ <h3 class="h6 text-body-secondary small text-uppercase fw-semibold mb-2">
137
142
  <i class="bi bi-signpost-split me-1" aria-hidden="true"></i>Page Location
138
- </h6>
143
+ </h3>
139
144
  {% include navigation/breadcrumbs.html %}
140
145
  </div>
141
146
  {% endif %}
142
147
 
143
- <!-- Page Metadata -->
148
+ {% comment %} Page Metadata {% endcomment %}
144
149
  <div class="mb-4">
145
- <h6 class="text-body-secondary small text-uppercase fw-semibold mb-2">
150
+ <h3 class="h6 text-body-secondary small text-uppercase fw-semibold mb-2">
146
151
  <i class="bi bi-file-earmark-code me-1" aria-hidden="true"></i>Page Info
147
- </h6>
152
+ </h3>
148
153
  <div class="table-responsive">
149
154
  <table class="table table-sm table-hover mb-0 small">
150
155
  <tbody>
@@ -175,11 +180,11 @@
175
180
  </div>
176
181
  </div>
177
182
 
178
- <!-- Source Code Shortcuts -->
183
+ {% comment %} Source Code Shortcuts {% endcomment %}
179
184
  <div class="mb-3">
180
- <h6 class="text-body-secondary small text-uppercase fw-semibold mb-2">
185
+ <h3 class="h6 text-body-secondary small text-uppercase fw-semibold mb-2">
181
186
  <i class="bi bi-code-square me-1" aria-hidden="true"></i>Source Code
182
- </h6>
187
+ </h3>
183
188
  {% include components/dev-shortcuts.html %}
184
189
  </div>
185
190
  </div>
@@ -189,9 +194,9 @@
189
194
 
190
195
  </div>
191
196
 
192
- <!-- Copy-to-clipboard for the panel's URL fields (event delegation; no
197
+ {% comment %} Copy-to-clipboard for the panel's URL fields (event delegation; no
193
198
  inline handlers). Buttons declare data-zer0-copy="<text>" or
194
- data-zer0-copy-target="#input-id". -->
199
+ data-zer0-copy-target="#input-id". {% endcomment %}
195
200
  <script>
196
201
  document.addEventListener("click", function (e) {
197
202
  var btn = e.target.closest("[data-zer0-copy], [data-zer0-copy-target]");
@@ -1,4 +1,4 @@
1
- <!--
1
+ {% comment %}
2
2
  ===================================================================
3
3
  JAVASCRIPT - Local vendor libraries
4
4
  ===================================================================
@@ -12,43 +12,43 @@
12
12
 
13
13
  Refresh: ./scripts/vendor-install.sh (see vendor-manifest.json)
14
14
  ===================================================================
15
- -->
15
+ {% endcomment %}
16
16
 
17
- <!-- Bootstrap 5.3.3 Bundle (includes Popper.js) -->
17
+ {% comment %} Bootstrap 5.3.3 Bundle (includes Popper.js) {% endcomment %}
18
18
  <script src="{{ '/assets/vendor/bootstrap/js/bootstrap.bundle.min.js' | relative_url }}"></script>
19
19
 
20
- <!-- Navigation ES Modules — orchestrator for sidebar, scroll spy, keyboard
20
+ {% comment %} Navigation ES Modules — orchestrator for sidebar, scroll spy, keyboard
21
21
  shortcuts, gestures, focus, AND the navbar dropdowns/mobile menu that
22
- formerly lived in /assets/js/navigation.js (now deprecated). -->
22
+ formerly lived in /assets/js/navigation.js (now deprecated). {% endcomment %}
23
23
  <script type="module" src="{{ '/assets/js/modules/navigation/index.js' | relative_url }}?v={{ site.time | date: '%s' }}"></script>
24
24
 
25
- <!-- Generic UI helpers (toast, copy-to-clipboard, image fade-in) -->
25
+ {% comment %} Generic UI helpers (toast, copy-to-clipboard, image fade-in) {% endcomment %}
26
26
  <script defer src="{{ '/assets/js/ui-helpers.js' | relative_url }}"></script>
27
27
 
28
- <!-- Posts-archive pagination (auto-activates only on pages with a
29
- [data-posts-archive] container; safe to load globally). -->
28
+ {% comment %} Posts-archive pagination (auto-activates only on pages with a
29
+ [data-posts-archive] container; safe to load globally). {% endcomment %}
30
30
  <script defer src="{{ '/assets/js/posts-pagination.js' | relative_url }}"></script>
31
31
 
32
- <!-- Search modal controller -->
32
+ {% comment %} Search modal controller {% endcomment %}
33
33
  <script src="{{ '/assets/js/search-modal.js' | relative_url }}"></script>
34
34
 
35
- <!-- Share helpers -->
35
+ {% comment %} Share helpers {% endcomment %}
36
36
  <script src="{{ '/assets/js/share-actions.js' | relative_url }}"></script>
37
37
 
38
- <!-- fffuel-style background customizer (skin switching, toggle, opacity) -->
38
+ {% comment %} fffuel-style background customizer (skin switching, toggle, opacity) {% endcomment %}
39
39
  <script src="{{ '/assets/js/background-customizer.js' | relative_url }}"></script>
40
40
 
41
- <!-- Optional runtime Appearance panel (enabled via site.appearance_panel) -->
41
+ {% comment %} Optional runtime Appearance panel (enabled via site.appearance_panel) {% endcomment %}
42
42
  {% if site.appearance_panel %}
43
43
  <script defer src="{{ '/assets/js/modules/theme/appearance.js' | relative_url }}"></script>
44
44
  {% endif %}
45
45
 
46
- <!-- Obsidian wiki-link / embed / tag client-side resolver (fallback for the
46
+ {% comment %} Obsidian wiki-link / embed / tag client-side resolver (fallback for the
47
47
  remote_theme GH Pages build, where _plugins/obsidian_links.rb cannot run).
48
48
  Honours `obsidian: { enabled: false }` in _config.yml — when disabled the
49
49
  resolver script is not loaded and the flag self-disables any fork that
50
50
  loads it another way. Note: `| default: true` would flip `false` back to
51
- true (Liquid treats false as falsy), so test enabled == false explicitly. -->
51
+ true (Liquid treats false as falsy), so test enabled == false explicitly. {% endcomment %}
52
52
  {%- assign obsidian_attachments_path = site.obsidian.attachments_path | default: '/assets/images/notes' -%}
53
53
  <script>
54
54
  window.OBSIDIAN_CONFIG = window.OBSIDIAN_CONFIG || {};
@@ -62,7 +62,7 @@
62
62
  <script defer src="{{ '/assets/js/obsidian-wiki-links.js' | relative_url }}?v={{ site.time | date: '%s' }}"></script>
63
63
  {%- endif -%}
64
64
 
65
- <!-- Optional fork overrides: opt-in via `site.user_overrides: true` in _config.yml. -->
65
+ {% comment %} Optional fork overrides: opt-in via `site.user_overrides: true` in _config.yml. {% endcomment %}
66
66
  {% if site.user_overrides %}
67
67
  <script defer src="{{ '/assets/js/user-overrides.js' | relative_url }}"></script>
68
68
  {% endif %}
@@ -2,24 +2,61 @@
2
2
  {%- comment -%}
3
3
  Component: language-toggle
4
4
  Path: _includes/components/language-toggle.html
5
- Purpose: Navbar dropdown that switches between the English original and
6
- its machine-generated translations (scripts/translate.rb).
7
- Shows the source language plus every configured target
8
- language; targets without a generated translation for the
9
- current page render as disabled entries.
10
- Params: none — reads page.lang / page.url / page.translation_source_url
11
- and the generated _data/i18n/manifest.yml.
5
+ Purpose: Switches between the English original and its machine-generated
6
+ translations (scripts/translate.rb). Shows the source language
7
+ plus every configured target language. Every entry is a live
8
+ link: a target without a generated translation for the current
9
+ page falls back to the source page and is explained by one
10
+ footnote (it used to render as a disabled entry — see T-038).
11
+ Params: variant (string, optional, default "navbar")
12
+ - "navbar" — compact dropdown for the header utility cluster
13
+ - "panel" — self-contained "Language" section for the
14
+ Settings offcanvas (components/info-section.html),
15
+ rendered as an always-visible list of choices
16
+ with its own section heading
17
+ Reads: page.lang / page.url / page.translation_source_url and the
18
+ generated _data/i18n/manifest.yml.
12
19
  Depends on: site.translation.* config, site.data.i18n.manifest,
13
20
  _data/i18n/languages.yml (display names), core/i18n.html,
14
- Bootstrap 5 dropdown, Bootstrap Icons (bi-translate)
21
+ Bootstrap 5 dropdown (navbar variant only), Bootstrap Icons
22
+ (bi-translate)
15
23
  Notes: Renders nothing when translation is disabled or no target
16
24
  languages are configured. The chosen language is remembered in
17
25
  localStorage("zer0-lang") for future UX (no auto-redirect).
18
26
  Must NOT be include_cached — the menu is page-dependent.
27
+ The theme ships the "panel" variant (the navbar is reserved for
28
+ main navigation); "navbar" stays for consumers that prefer the
29
+ header dropdown.
30
+
31
+ MENU CONTRACT (T-038 / #406) — shared by both variants:
32
+ * No DISABLED rows. A target language without a generated
33
+ translation is still a link; it goes to the source-language
34
+ page and still records the preference in zer0-lang, so the
35
+ menu never contains a dead end.
36
+ * The current language is marked with a check icon and a
37
+ subtle tint — NOT Bootstrap's `.active` primary fill, which
38
+ read as a selected nav item rather than "you are here".
39
+ * Machine-generated translations carry a small "auto" chip.
40
+ The source language never does (a human wrote it), and
41
+ neither do untranslated rows (nothing was generated).
42
+ * The per-row "(Not yet translated)" text is replaced by ONE
43
+ footnote under a divider, referenced from each untranslated
44
+ row via aria-describedby. Per-row text was the single
45
+ biggest contributor to menu width.
46
+ Trigger changes (icon-only, no caret) apply to the navbar
47
+ variant only — the panel has a section heading, not a button.
48
+
49
+ New ui keys (lang_machine_translated, lang_machine_translated_title,
50
+ lang_untranslated_note) live in _data/ui-text.yml `en`. core/i18n.html
51
+ REPLACES the whole `ui` map on a translated page rather than merging
52
+ it, so until scripts/translate.rb regenerates _data/i18n/<lang>.yml
53
+ these keys are absent there — every use below therefore carries a
54
+ literal `| default:` fallback. Do not remove them.
19
55
  {%- endcomment -%}
20
56
  {%- assign _lt_langs = site.translation.languages -%}
21
57
  {%- if site.translation.enabled and _lt_langs and _lt_langs.size > 0 -%}
22
58
  {%- include core/i18n.html -%}
59
+ {%- assign _lt_variant = include.variant | default: 'navbar' -%}
23
60
  {%- assign _lt_source = site.translation.source_lang | default: 'en' -%}
24
61
  {%- assign _lt_current = page.lang | default: _lt_source -%}
25
62
  {%- if _lt_current == _lt_source -%}
@@ -30,25 +67,117 @@
30
67
  {%- assign _lt_entry = site.data.i18n.manifest.pages[_lt_src_url] -%}
31
68
  {%- assign _lt_names = site.data.i18n.languages -%}
32
69
  {%- assign _lt_source_name = _lt_names[_lt_source].native | default: _lt_source -%}
70
+ {%- comment -%}
71
+ Display name of the language being read, for the trigger's title/aria — the
72
+ icon-only button has no visible text, so the accessible name has to carry it.
73
+ {%- endcomment -%}
74
+ {%- if _lt_current == _lt_source -%}
75
+ {%- assign _lt_current_name = _lt_source_name -%}
76
+ {%- else -%}
77
+ {%- assign _lt_current_name = _lt_names[_lt_current].native | default: _lt_current -%}
78
+ {%- endif -%}
79
+ {%- comment -%}
80
+ Does ANY target language lack a translation of this page? Drives whether the
81
+ footnote + divider render at all — an all-translated menu shows neither.
82
+ A target we are currently reading is by definition translated.
83
+ {%- endcomment -%}
84
+ {%- assign _lt_untranslated = 0 -%}
85
+ {%- for _lt_l in _lt_langs -%}
86
+ {%- unless _lt_l == _lt_current or _lt_entry[_lt_l] -%}
87
+ {%- assign _lt_untranslated = _lt_untranslated | plus: 1 -%}
88
+ {%- endunless -%}
89
+ {%- endfor -%}
90
+ {%- assign _lt_note_id = 'zer0-lang-untranslated-note' -%}
91
+ {%- assign _lt_auto_label = ui.lang_machine_translated | default: 'auto' -%}
92
+ {%- assign _lt_auto_title = ui.lang_machine_translated_title | default: 'Machine-generated translation' -%}
93
+ {%- assign _lt_note_text = ui.lang_untranslated_note | default: 'Marked languages are not yet translated — those links open the original.' -%}
94
+ {%- assign _lt_fallback_url = _lt_src_url | default: '/' | relative_url -%}
95
+ {%- if _lt_variant == 'panel' -%}
96
+ <div class="mb-4 nav-lang-panel" id="zer0-lang-toggle">
97
+ <h3 class="h6 text-body-secondary small text-uppercase fw-semibold mb-2">
98
+ <i class="bi bi-translate me-1" aria-hidden="true"></i>{{ ui.lang_toggle_label | default: "Language" }}
99
+ </h3>
100
+ <div class="list-group nav-lang-options" role="group" aria-label="{{ ui.lang_toggle_aria | default: 'Change language' }}">
101
+ {%- if _lt_current == _lt_source %}
102
+ <span class="list-group-item nav-lang-item is-current" aria-current="true">
103
+ <span class="nav-lang-code">{{ _lt_source | slice: 0, 2 | upcase }}</span>
104
+ <span class="nav-lang-name">{{ _lt_source_name }}</span>
105
+ <i class="bi bi-check2 nav-lang-check" aria-hidden="true"></i>
106
+ </span>
107
+ {%- else %}
108
+ <a class="list-group-item list-group-item-action nav-lang-item" data-lang="{{ _lt_source }}" hreflang="{{ _lt_source }}" href="{{ _lt_fallback_url }}">
109
+ <span class="nav-lang-code">{{ _lt_source | slice: 0, 2 | upcase }}</span>
110
+ <span class="nav-lang-name">{{ _lt_source_name }}</span>
111
+ </a>
112
+ {%- endif %}
113
+ {%- for _lt_lang in _lt_langs -%}
114
+ {%- assign _lt_name = _lt_names[_lt_lang].native | default: _lt_lang -%}
115
+ {%- assign _lt_t = _lt_entry[_lt_lang] %}
116
+ {%- if _lt_current == _lt_lang %}
117
+ <span class="list-group-item nav-lang-item is-current" aria-current="true">
118
+ <span class="nav-lang-code">{{ _lt_lang | slice: 0, 2 | upcase }}</span>
119
+ <span class="nav-lang-name">{{ _lt_name }}</span>
120
+ <span class="nav-lang-auto" title="{{ _lt_auto_title }}">{{ _lt_auto_label }}</span>
121
+ <i class="bi bi-check2 nav-lang-check" aria-hidden="true"></i>
122
+ </span>
123
+ {%- elsif _lt_t %}
124
+ <a class="list-group-item list-group-item-action nav-lang-item" data-lang="{{ _lt_lang }}" hreflang="{{ _lt_lang }}" href="{{ _lt_t.url | relative_url }}">
125
+ <span class="nav-lang-code">{{ _lt_lang | slice: 0, 2 | upcase }}</span>
126
+ <span class="nav-lang-name">{{ _lt_name }}</span>
127
+ <span class="nav-lang-auto" title="{{ _lt_auto_title }}">{{ _lt_auto_label }}</span>
128
+ </a>
129
+ {%- else %}
130
+ {%- comment -%}
131
+ No translation yet — still a LINK, not a disabled row. It goes to the
132
+ source page and records the preference, so the reader lands on real
133
+ content instead of a dead entry. hreflang describes the DESTINATION,
134
+ which is the source language, not the one that was clicked.
135
+ {%- endcomment -%}
136
+ <a class="list-group-item list-group-item-action nav-lang-item is-untranslated" data-lang="{{ _lt_lang }}" hreflang="{{ _lt_source }}" href="{{ _lt_fallback_url }}" aria-describedby="{{ _lt_note_id }}-panel">
137
+ <span class="nav-lang-code">{{ _lt_lang | slice: 0, 2 | upcase }}</span>
138
+ <span class="nav-lang-name">{{ _lt_name }}</span>
139
+ <span class="nav-lang-mark" aria-hidden="true">*</span>
140
+ </a>
141
+ {%- endif %}
142
+ {%- endfor %}
143
+ </div>
144
+ {%- if _lt_untranslated > 0 %}
145
+ <hr class="nav-lang-divider">
146
+ <p class="nav-lang-note" id="{{ _lt_note_id }}-panel">{{ _lt_note_text }}</p>
147
+ {%- endif %}
148
+ </div>
149
+ {%- else -%}
33
150
  <div class="dropdown nav-lang-toggle" id="zer0-lang-toggle">
151
+ {%- comment -%}
152
+ Icon-only, square, and caret-free. `dropdown-toggle` is what draws the
153
+ Bootstrap caret; it is NOT what wires the dropdown — Bootstrap binds on
154
+ [data-bs-toggle="dropdown"], so dropping the class costs no behaviour.
155
+ The visible EN/FR label is gone at every width (it was already hidden
156
+ below xl), so title + aria-label are the only accessible name and both
157
+ name the language being read.
158
+ {%- endcomment -%}
34
159
  <button
35
- class="btn nav-lang-button dropdown-toggle"
160
+ class="btn nav-lang-button"
36
161
  type="button"
37
162
  id="langToggleButton"
38
163
  data-bs-toggle="dropdown"
39
164
  aria-expanded="false"
40
165
  aria-label="{{ ui.lang_toggle_aria | default: 'Change language' }}"
41
- title="{{ ui.lang_toggle_label | default: 'Language' }}"
166
+ title="{{ ui.lang_toggle_label | default: 'Language' }}: {{ _lt_current_name }}"
42
167
  >
43
168
  <i class="{{ site.default_icon }} bi-translate" aria-hidden="true"></i>
44
- <span class="nav-link-text d-none d-xl-inline ms-1 text-uppercase">{{ _lt_current | slice: 0, 2 }}</span>
45
169
  </button>
46
- <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="langToggleButton">
170
+ <ul class="dropdown-menu dropdown-menu-end nav-lang-menu" aria-labelledby="langToggleButton">
47
171
  <li>
48
172
  {%- if _lt_current == _lt_source %}
49
- <span class="dropdown-item active" aria-current="true">{{ _lt_source_name }}</span>
173
+ <span class="dropdown-item nav-lang-item is-current" aria-current="true">
174
+ <span class="nav-lang-name">{{ _lt_source_name }}</span>
175
+ <i class="bi bi-check2 nav-lang-check" aria-hidden="true"></i>
176
+ </span>
50
177
  {%- else %}
51
- <a class="dropdown-item" data-lang="{{ _lt_source }}" hreflang="{{ _lt_source }}" href="{{ _lt_src_url | default: '/' | relative_url }}">{{ _lt_source_name }}</a>
178
+ <a class="dropdown-item nav-lang-item" data-lang="{{ _lt_source }}" hreflang="{{ _lt_source }}" href="{{ _lt_fallback_url }}">
179
+ <span class="nav-lang-name">{{ _lt_source_name }}</span>
180
+ </a>
52
181
  {%- endif %}
53
182
  </li>
54
183
  {%- for _lt_lang in _lt_langs -%}
@@ -56,23 +185,41 @@
56
185
  {%- assign _lt_t = _lt_entry[_lt_lang] %}
57
186
  <li>
58
187
  {%- if _lt_current == _lt_lang %}
59
- <span class="dropdown-item active" aria-current="true">{{ _lt_name }}</span>
188
+ <span class="dropdown-item nav-lang-item is-current" aria-current="true">
189
+ <span class="nav-lang-name">{{ _lt_name }}</span>
190
+ <span class="nav-lang-auto" title="{{ _lt_auto_title }}">{{ _lt_auto_label }}</span>
191
+ <i class="bi bi-check2 nav-lang-check" aria-hidden="true"></i>
192
+ </span>
60
193
  {%- elsif _lt_t %}
61
- <a class="dropdown-item" data-lang="{{ _lt_lang }}" hreflang="{{ _lt_lang }}" href="{{ _lt_t.url | relative_url }}">{{ _lt_name }}</a>
194
+ <a class="dropdown-item nav-lang-item" data-lang="{{ _lt_lang }}" hreflang="{{ _lt_lang }}" href="{{ _lt_t.url | relative_url }}">
195
+ <span class="nav-lang-name">{{ _lt_name }}</span>
196
+ <span class="nav-lang-auto" title="{{ _lt_auto_title }}">{{ _lt_auto_label }}</span>
197
+ </a>
62
198
  {%- else %}
63
- <span class="dropdown-item disabled" aria-disabled="true" title="{{ ui.lang_not_available | default: 'Not yet translated' }}">{{ _lt_name }} <small class="text-body-secondary">({{ ui.lang_not_available | default: 'Not yet translated' }})</small></span>
199
+ {%- comment -%} See the panel variant above — link, never a dead row. {%- endcomment -%}
200
+ <a class="dropdown-item nav-lang-item is-untranslated" data-lang="{{ _lt_lang }}" hreflang="{{ _lt_source }}" href="{{ _lt_fallback_url }}" aria-describedby="{{ _lt_note_id }}">
201
+ <span class="nav-lang-name">{{ _lt_name }}</span>
202
+ <span class="nav-lang-mark" aria-hidden="true">*</span>
203
+ </a>
64
204
  {%- endif %}
65
205
  </li>
66
206
  {%- endfor %}
207
+ {%- if _lt_untranslated > 0 %}
208
+ <li><hr class="dropdown-divider"></li>
209
+ <li><p class="nav-lang-note" id="{{ _lt_note_id }}">{{ _lt_note_text }}</p></li>
210
+ {%- endif %}
67
211
  </ul>
68
212
  </div>
213
+ {%- endif -%}
69
214
  <script>
70
215
  // Remember the visitor's language pick (no auto-redirect — just preference).
216
+ // Delegated at document level so it covers both variants and stays correct
217
+ // if a consumer renders the component more than once per page.
71
218
  (function () {
72
- var toggle = document.getElementById('zer0-lang-toggle');
73
- if (!toggle) return;
74
- toggle.addEventListener('click', function (event) {
75
- var item = event.target.closest('a.dropdown-item[data-lang]');
219
+ if (window.__zer0LangPrefWired) return;
220
+ window.__zer0LangPrefWired = true;
221
+ document.addEventListener('click', function (event) {
222
+ var item = event.target.closest('#zer0-lang-toggle a[data-lang]');
76
223
  if (!item) return;
77
224
  try { localStorage.setItem('zer0-lang', item.getAttribute('data-lang')); } catch (e) { /* private mode */ }
78
225
  });