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
@@ -61,24 +61,34 @@
61
61
  {%- assign pf_theme = site.theme | default: site.remote_theme | default: "" -%}
62
62
 
63
63
  {%- if pf_show_fab -%}
64
- <!-- Global feedback FAB (page-invariant; opens the modal) -->
65
- <button type="button"
66
- id="pageFeedbackFab"
67
- class="pf-fab btn shadow-lg position-fixed"
68
- data-pf-open
69
- aria-haspopup="dialog"
70
- aria-controls="pageFeedbackModal"
71
- title="{{ site.page_feedback.fab_label | default: 'Improve this page' | escape }}"
72
- aria-label="{{ site.page_feedback.fab_label | default: 'Improve this page' | escape }}">
64
+ {% comment %} Global feedback FAB (page-invariant; opens the modal) {% endcomment %}
65
+ {%- comment -%}
66
+ An anchor, not a button, and the href is load-bearing: the widget now
67
+ delegates issue assembly to assets/js/fleet-feedback.js, so if that script
68
+ fails to load (or JavaScript is off entirely) the FAB still reaches the
69
+ issue form instead of becoming a decorative circle. page-feedback.js
70
+ intercepts [data-pf-open] and opens the richer modal when it is running.
71
+ {%- endcomment -%}
72
+ <a id="pageFeedbackFab"
73
+ class="pf-fab btn shadow-lg position-fixed"
74
+ href="https://github.com/{{ pf_repo }}/issues/new?template=page_improvement.yml&labels=page-feedback"
75
+ role="button"
76
+ data-pf-open
77
+ target="_blank"
78
+ rel="noopener noreferrer"
79
+ aria-haspopup="dialog"
80
+ aria-controls="pageFeedbackModal"
81
+ title="{{ site.page_feedback.fab_label | default: 'Improve this page' | escape }}"
82
+ aria-label="{{ site.page_feedback.fab_label | default: 'Improve this page' | escape }}">
73
83
  <i class="bi {{ site.page_feedback.fab_icon | default: 'bi-megaphone' }}" aria-hidden="true"></i>
74
- </button>
84
+ </a>
75
85
  {%- endif -%}
76
86
 
77
- <!-- Feedback modal (self-contained overlay; controlled by page-feedback.js).
87
+ {% comment %} Feedback modal (self-contained overlay; controlled by page-feedback.js).
78
88
  `position-fixed` is required, not cosmetic: it opts this <body> child out of
79
89
  the .zer0-bg-body flattening rule (_sass/theme/_backgrounds.scss) that would
80
90
  otherwise force position:relative; z-index:1 and let the cookie banner steal
81
- the dialog's clicks. -->
91
+ the dialog's clicks. {% endcomment %}
82
92
  <div class="pf-overlay position-fixed" id="pageFeedbackModal" role="dialog" aria-modal="true"
83
93
  aria-labelledby="pfTitle" aria-describedby="pfIntro" hidden>
84
94
  <div class="pf-dialog shadow-lg" role="document">
@@ -100,16 +110,18 @@
100
110
 
101
111
  <form id="pfForm" class="pf-body" novalidate>
102
112
 
103
- <!-- Step 1 — request type -->
113
+ {% comment %} Step 1 — request type {% endcomment %}
104
114
  <fieldset class="pf-field pf-types">
105
115
  <legend class="pf-legend">What kind of feedback?</legend>
106
116
  {%- comment -%}
107
117
  _data/feedback_types.yml is optional for consumers (remote_theme
108
118
  cannot ship _data). Without it the widget falls back to a minimal
109
119
  generic taxonomy (see pf_fallback_types below) instead of rendering
110
- an empty selector. The fallback entries carry no GitHub labels and
111
- no agent directive — labels must exist in the target repo or GitHub
112
- silently drops them from prefilled issue URLs.
120
+ an empty selector. The fallback carries the four fleet-taxonomy
121
+ labels (bug/docs/feature/question) and no agent directive: a label
122
+ the consumer repo does not have is silently dropped by GitHub, so
123
+ naming them costs nothing and makes the issue pipeline-eligible the
124
+ moment the feedback fan-out creates them.
113
125
  {%- endcomment -%}
114
126
  {%- assign pf_group = "" -%}
115
127
  {%- for t in site.data.feedback_types -%}
@@ -138,8 +150,7 @@
138
150
  cannot ship _data). Without it, render this minimal generic
139
151
  taxonomy instead of an empty selector. Ids/markup must stay in
140
152
  sync with the fallback JSON in the #pageFeedbackTypes script
141
- below. Fallback entries carry no GitHub labels (labels must exist
142
- in the target repo or GitHub silently drops them) and no agent
153
+ below, which carries the fleet-taxonomy labels and no agent
143
154
  directive.
144
155
  {%- endcomment -%}
145
156
  <p class="pf-types-group">This page</p>
@@ -183,7 +194,7 @@
183
194
  {%- endfor -%}
184
195
  </fieldset>
185
196
 
186
- <!-- Step 2 — description -->
197
+ {% comment %} Step 2 — description {% endcomment %}
187
198
  <div class="pf-field">
188
199
  <label for="pfDescription" class="pf-label">Describe it</label>
189
200
  <textarea id="pfDescription" class="form-control" rows="4"
@@ -194,14 +205,14 @@
194
205
  </p>
195
206
  </div>
196
207
 
197
- <!-- Step 3 — what gets attached -->
208
+ {% comment %} Step 3 — what gets attached {% endcomment %}
198
209
  <details class="pf-field pf-context" id="pfContextWrap">
199
210
  <summary class="pf-summary">
200
211
  <i class="bi bi-paperclip me-1" aria-hidden="true"></i>What gets attached
201
212
  <span class="pf-context-count" id="pfContextCount"></span>
202
213
  </summary>
203
214
  <ul class="pf-context-list" id="pfContextList" aria-live="polite">
204
- <!-- page title / url / path filled in by JS -->
215
+ {% comment %} page title / url / path filled in by JS {% endcomment %}
205
216
  </ul>
206
217
 
207
218
  {%- if site.page_feedback.capture_logs -%}
@@ -221,7 +232,7 @@
221
232
  </details>
222
233
 
223
234
  {%- if pf_ai_available -%}
224
- <!-- Step 4 — optional AI review (proxy + Claude Code OAuth) -->
235
+ {% comment %} Step 4 — optional AI review (proxy + Claude Code OAuth) {% endcomment %}
225
236
  <div class="pf-field pf-ai" id="pfAiWrap">
226
237
  <div class="pf-ai-head">
227
238
  <button type="button" class="btn btn-sm btn-outline-primary" id="pfAnalyze">
@@ -253,7 +264,7 @@
253
264
  </div>
254
265
  </div>
255
266
 
256
- <!-- Widget configuration consumed by assets/js/page-feedback.js -->
267
+ {% comment %} Widget configuration consumed by assets/js/page-feedback.js {% endcomment %}
257
268
  <script id="pageFeedbackConfig" type="application/json">
258
269
  {
259
270
  "enabled": true,
@@ -275,19 +286,19 @@
275
286
  }
276
287
  </script>
277
288
 
278
- <!-- Request-type taxonomy (from _data/feedback_types.yml; generic fallback
289
+ {% comment %} Request-type taxonomy (from _data/feedback_types.yml; generic fallback
279
290
  when the site ships no data file — keep in sync with the fallback markup
280
- in the pf-types fieldset above) -->
291
+ in the pf-types fieldset above) {% endcomment %}
281
292
  <script id="pageFeedbackTypes" type="application/json">
282
293
  {% if site.data.feedback_types %}{{ site.data.feedback_types | jsonify }}{% else %}[
283
- {"id":"fix-page","label":"Report a problem","icon":"bi-bug","group":"This page","scope":"page","description":"A typo, broken link, wrong information, or something rendering badly","labels":[],"agent":false,"placeholder":"What is wrong, where on the page, and what did you expect instead?"},
284
- {"id":"improve-page","label":"Improve this page","icon":"bi-stars","group":"This page","scope":"page","description":"Polish the copy, structure, or presentation","labels":[],"agent":false,"placeholder":"What would make this page clearer, tighter, or more useful?"},
285
- {"id":"feature","label":"Suggest a feature","icon":"bi-lightbulb","group":"The site","scope":"site","description":"An idea that would make the site more useful","labels":[],"agent":false,"placeholder":"Describe the feature and the problem it would solve."},
286
- {"id":"question","label":"Ask a question","icon":"bi-question-circle","group":"The site","scope":"site","description":"Something unclear that is not exactly a bug","labels":[],"agent":false,"placeholder":"What would you like to know?"}
294
+ {"id":"fix-page","label":"Report a problem","icon":"bi-bug","group":"This page","scope":"page","description":"A typo, broken link, wrong information, or something rendering badly","labels":["bug"],"agent":false,"placeholder":"What is wrong, where on the page, and what did you expect instead?"},
295
+ {"id":"improve-page","label":"Improve this page","icon":"bi-stars","group":"This page","scope":"page","description":"Polish the copy, structure, or presentation","labels":["docs"],"agent":false,"placeholder":"What would make this page clearer, tighter, or more useful?"},
296
+ {"id":"feature","label":"Suggest a feature","icon":"bi-lightbulb","group":"The site","scope":"site","description":"An idea that would make the site more useful","labels":["feature"],"agent":false,"placeholder":"Describe the feature and the problem it would solve."},
297
+ {"id":"question","label":"Ask a question","icon":"bi-question-circle","group":"The site","scope":"site","description":"Something unclear that is not exactly a bug","labels":["question"],"agent":false,"placeholder":"What would you like to know?"}
287
298
  ]{% endif %}
288
299
  </script>
289
300
 
290
- <!-- Live page context (build-time front matter; runtime detail added by JS) -->
301
+ {% comment %} Live page context (build-time front matter; runtime detail added by JS) {% endcomment %}
291
302
  <script id="pageFeedbackContext" type="application/json">
292
303
  {
293
304
  "title": {{ page.title | default: site.title | jsonify }},
@@ -310,6 +321,10 @@
310
321
  }
311
322
  </script>
312
323
 
324
+ {% comment %} The shared fleet builder must be parsed before the widget: page-feedback.js
325
+ reads window.FleetFeedbackCore at load and bails without it. Both are `defer`, and
326
+ deferred scripts execute in document order, so this ordering is the dependency. {% endcomment %}
327
+ <script defer src="{{ '/assets/js/fleet-feedback.js' | relative_url }}"></script>
313
328
  <script defer src="{{ '/assets/js/page-feedback.js' | relative_url }}"></script>
314
329
 
315
330
  {%- endunless -%}
@@ -0,0 +1,55 @@
1
+ {% comment %} Feature: ZER0-083 {% endcomment %}
2
+ {%- comment -%}
3
+ Component: page-views-init
4
+ Path: _includes/components/page-views-init.html
5
+ Purpose: Bootstrap the page-view counter — inject its configuration and
6
+ load the behaviour. Renders no visible markup; the badge lives
7
+ in components/page-views.html.
8
+ Params: none — configured via site.page_views
9
+ Depends on: assets/js/page-views.js
10
+ Included by: _includes/core/head.html (once per page)
11
+ Notes: Split from the badge include on purpose: the badge may appear
12
+ many times on a page, this must appear exactly once.
13
+ {%- endcomment -%}
14
+
15
+ {%- unless site.page_views.enabled -%}{%- comment -%} counter disabled {%- endcomment -%}
16
+ {%- else -%}
17
+ {%- comment -%}
18
+ `respect_dnt` / `respect_gpc` / `require_consent` default to the PostHog
19
+ privacy block so a site configures its privacy posture once. Liquid's
20
+ `default` filter treats `false` as "missing", so each flag is resolved with
21
+ an if-tag instead — otherwise an explicit `false` would silently flip true.
22
+ {%- endcomment -%}
23
+ {%- assign pv_dnt = true -%}
24
+ {%- if site.page_views.respect_dnt == false -%}{%- assign pv_dnt = false -%}{%- endif -%}
25
+ {%- assign pv_gpc = true -%}
26
+ {%- if site.page_views.respect_gpc == false -%}{%- assign pv_gpc = false -%}{%- endif -%}
27
+ {%- assign pv_consent = false -%}
28
+ {%- if site.page_views.require_consent -%}{%- assign pv_consent = true -%}{%- endif -%}
29
+ {%- assign pv_track = true -%}
30
+ {%- if site.page_views.track == false -%}{%- assign pv_track = false -%}{%- endif -%}
31
+ {%- assign pv_fallback = false -%}
32
+ {%- if site.page_views.fallback_local -%}{%- assign pv_fallback = true -%}{%- endif -%}
33
+
34
+ {% comment %} Page-view counter configuration consumed by assets/js/page-views.js {% endcomment %}
35
+ <script id="pageViewsConfig" type="application/json">
36
+ {
37
+ "enabled": true,
38
+ "provider": {{ site.page_views.provider | default: "local" | jsonify }},
39
+ "endpoint": {{ site.page_views.endpoint | default: "" | jsonify }},
40
+ "method": {{ site.page_views.method | default: "POST" | jsonify }},
41
+ "countKey": {{ site.page_views.count_key | default: "views" | jsonify }},
42
+ "dedupe": {{ site.page_views.dedupe | default: "session" | jsonify }},
43
+ "maxEntries": {{ site.page_views.max_entries | default: 500 }},
44
+ "track": {{ pv_track | jsonify }},
45
+ "respectDnt": {{ pv_dnt | jsonify }},
46
+ "respectGpc": {{ pv_gpc | jsonify }},
47
+ "requireConsent": {{ pv_consent | jsonify }},
48
+ "fallbackLocal": {{ pv_fallback | jsonify }},
49
+ "baseurl": {{ site.baseurl | default: "" | jsonify }},
50
+ "labelOne": {{ site.page_views.label_one | default: "view" | jsonify }},
51
+ "labelOther": {{ site.page_views.label_other | default: "views" | jsonify }}
52
+ }
53
+ </script>
54
+ <script defer src="{{ '/assets/js/page-views.js' | relative_url }}"></script>
55
+ {%- endunless -%}
@@ -0,0 +1,33 @@
1
+ {% comment %} Feature: ZER0-083 {% endcomment %}
2
+ {%- comment -%}
3
+ Component: page-views
4
+ Path: _includes/components/page-views.html
5
+ Purpose: Inline "N views" badge for a page. Ships hidden and is revealed
6
+ by assets/js/page-views.js once a count is known, so a page
7
+ with no data never flashes "0 views".
8
+ Params: url (string, optional, default: page.url) — page to show
9
+ class (string, optional, default: "") — extra classes
10
+ icon (string, optional, default: "bi-eye")
11
+ separator (bool, optional, default: false) — render a
12
+ leading "•" INSIDE the badge, so the separator hides
13
+ with it in a meta row
14
+ Depends on: components/page-views-init.html (config + behaviour, loaded once
15
+ from core/head.html), _sass/components/_page-views.scss
16
+ Notes: Safe to include more than once per page — each badge resolves
17
+ its own `data-page-views` path. Paths are site-relative and
18
+ baseurl-free (page.url); page-views.js normalizes the runtime
19
+ location the same way.
20
+ {%- endcomment -%}
21
+
22
+ {%- unless site.page_views.enabled -%}{%- comment -%} counter disabled {%- endcomment -%}
23
+ {%- else -%}
24
+ {%- assign pv_path = include.url | default: page.url -%}
25
+ {%- assign pv_class = include.class | default: "" -%}
26
+ {%- assign pv_icon = include.icon | default: "bi-eye" -%}
27
+ <span class="page-views{% if pv_class != "" %} {{ pv_class }}{% endif %}" data-page-views="{{ pv_path | escape }}" hidden>
28
+ {%- if include.separator -%}<span class="page-views__sep mx-2" aria-hidden="true">•</span>{%- endif -%}
29
+ <i class="bi {{ pv_icon }} me-1" aria-hidden="true"></i>
30
+ <span class="page-views__count" data-page-views-count>0</span>
31
+ <span class="page-views__label" data-page-views-label>{{ site.page_views.label_other | default: "views" }}</span>
32
+ </span>
33
+ {%- endunless -%}
@@ -1,4 +1,4 @@
1
- <!--
1
+ {% comment %}
2
2
  ===================================================================
3
3
  POST CARD COMPONENT - Reusable blog post card
4
4
  ===================================================================
@@ -41,7 +41,7 @@
41
41
  - site.public_folder for image paths
42
42
  - site.teaser for fallback image
43
43
  ===================================================================
44
- -->
44
+ {% endcomment %}
45
45
 
46
46
  {% comment %} Parameter defaults {% endcomment %}
47
47
  {% assign show_category = include.show_category | default: true %}
@@ -70,44 +70,44 @@
70
70
  {% if render_col %}<div class="col">{% endif %}
71
71
  <div class="card h-100 post-card border-0 shadow-sm {{ card_class }}">
72
72
 
73
- <!-- ====================== -->
74
- <!-- CARD IMAGE & BADGES -->
75
- <!-- ====================== -->
73
+ {% comment %} ====================== {% endcomment %}
74
+ {% comment %} CARD IMAGE & BADGES {% endcomment %}
75
+ {% comment %} ====================== {% endcomment %}
76
76
  <div class="position-relative">
77
- <!-- Breaking News Badge -->
77
+ {% comment %} Breaking News Badge {% endcomment %}
78
78
  {% if include.post.breaking %}
79
79
  <span class="badge bg-danger position-absolute top-0 start-0 m-2 z-1">
80
80
  <i class="bi bi-lightning-fill me-1"></i>Breaking
81
81
  </span>
82
82
  {% endif %}
83
83
 
84
- <!-- Featured Badge -->
84
+ {% comment %} Featured Badge {% endcomment %}
85
85
  {% if include.post.featured %}
86
86
  <span class="badge bg-warning text-dark position-absolute top-0 end-0 m-2 z-1">
87
87
  <i class="bi bi-star-fill me-1"></i>Featured
88
88
  </span>
89
89
  {% endif %}
90
90
 
91
- <!-- Post Type Badge -->
91
+ {% comment %} Post Type Badge {% endcomment %}
92
92
  {% if show_post_type and include.post.post_type and include.post.post_type != "standard" and include.post.post_type != "featured" and include.post.post_type != "breaking" %}
93
93
  <span class="position-absolute bottom-0 start-0 m-2 z-1">
94
94
  {% include components/post-type-badge.html post_type=include.post.post_type %}
95
95
  </span>
96
96
  {% endif %}
97
97
 
98
- <!-- Preview Image -->
98
+ {% comment %} Preview Image {% endcomment %}
99
99
  <a href="{{ include.post.url | relative_url }}" class="text-decoration-none">
100
100
  {% assign card_img = include.post.preview | default: site.teaser %}
101
101
  {% include components/preview-image.html src=card_img alt=include.post.title class="card-img-top" %}
102
102
  </a>
103
103
  </div>
104
104
 
105
- <!-- ====================== -->
106
- <!-- CARD BODY -->
107
- <!-- ====================== -->
105
+ {% comment %} ====================== {% endcomment %}
106
+ {% comment %} CARD BODY {% endcomment %}
107
+ {% comment %} ====================== {% endcomment %}
108
108
  <div class="card-body d-flex flex-column">
109
109
 
110
- <!-- Category Badge -->
110
+ {% comment %} Category Badge {% endcomment %}
111
111
  {% comment %} Category base is configurable (`category_base`, default `/news`).
112
112
  Only link the badge when that category index page exists in the build;
113
113
  otherwise render a plain badge so remote-theme consumers (whose category
@@ -135,7 +135,7 @@
135
135
  </div>
136
136
  {% endif %}
137
137
 
138
- <!-- Post Title -->
138
+ {% comment %} Post Title {% endcomment %}
139
139
  <h5 class="card-title mb-2">
140
140
  <a href="{{ include.post.url | relative_url }}"
141
141
  class="text-decoration-none text-body-emphasis stretched-link">
@@ -143,14 +143,14 @@
143
143
  </a>
144
144
  </h5>
145
145
 
146
- <!-- Subtitle if available -->
146
+ {% comment %} Subtitle if available {% endcomment %}
147
147
  {% if include.post.sub-title %}
148
148
  <p class="card-subtitle text-muted small mb-2">
149
149
  {{ include.post.sub-title }}
150
150
  </p>
151
151
  {% endif %}
152
152
 
153
- <!-- Post Excerpt -->
153
+ {% comment %} Post Excerpt {% endcomment %}
154
154
  {% if show_excerpt %}
155
155
  <p class="card-text text-muted small flex-grow-1">
156
156
  {{ include.post.excerpt | strip_html | truncate: 120 }}
@@ -159,13 +159,13 @@
159
159
 
160
160
  </div>
161
161
 
162
- <!-- ====================== -->
163
- <!-- CARD FOOTER -->
164
- <!-- ====================== -->
162
+ {% comment %} ====================== {% endcomment %}
163
+ {% comment %} CARD FOOTER {% endcomment %}
164
+ {% comment %} ====================== {% endcomment %}
165
165
  <div class="card-footer bg-transparent border-top-0">
166
166
  <div class="d-flex justify-content-between align-items-center small text-muted">
167
167
 
168
- <!-- Left: Author & Date -->
168
+ {% comment %} Left: Author & Date {% endcomment %}
169
169
  <div>
170
170
  {% if show_author and include.post.author %}
171
171
  <span class="me-2">
@@ -177,7 +177,7 @@
177
177
  </span>
178
178
  </div>
179
179
 
180
- <!-- Right: Reading Time -->
180
+ {% comment %} Right: Reading Time {% endcomment %}
181
181
  {% if show_reading_time %}
182
182
  <span>
183
183
  <i class="bi bi-clock me-1"></i>{{ reading_time }}
@@ -186,7 +186,7 @@
186
186
 
187
187
  </div>
188
188
 
189
- <!-- Tags (compact) -->
189
+ {% comment %} Tags (compact) {% endcomment %}
190
190
  {% if include.post.tags.size > 0 %}
191
191
  <div class="mt-2">
192
192
  {% for tag in include.post.tags limit: 3 %}
@@ -1,5 +1,5 @@
1
1
  {% comment %} Feature: ZER0-064 {% endcomment %}
2
- <!--
2
+ {% comment %}
3
3
  ===================================================================
4
4
  POST TYPE BADGE COMPONENT - Visual indicator for article types
5
5
  ===================================================================
@@ -29,7 +29,7 @@
29
29
  - Bootstrap 5 badge component
30
30
  - Bootstrap Icons
31
31
  ===================================================================
32
- -->
32
+ {% endcomment %}
33
33
 
34
34
  {% assign type = include.post_type | default: "standard" %}
35
35
 
@@ -1,8 +1,8 @@
1
- <!--
1
+ {% comment %}
2
2
  file: powered-by.html
3
3
  path:_includes/powered-by.html
4
4
  description: This file is used to display the powered by section on the home page.
5
- -->
5
+ {% endcomment %}
6
6
 
7
7
  <div class="row row-cols-1 row-cols-md-3 g-4">
8
8
  {% for power in site.powered_by %}
@@ -10,6 +10,12 @@
10
10
  style="height: 180px; object-fit: cover;"
11
11
  -%}
12
12
 
13
+ WHICH INCLUDE: this one renders an <img> in the content flow. For cover art
14
+ painted as a CSS background on an otherwise empty box, use the sibling
15
+ components/background-image.html -- a background image is invisible to
16
+ assistive tech, so that case needs role="img" + aria-label (or aria-hidden
17
+ when purely decorative) rather than an alt attribute.
18
+
13
19
  Convention: Preview paths can omit the /assets/ prefix.
14
20
  The component will auto-prepend the assets_prefix from _config.yml.
15
21
  Examples:
@@ -1,4 +1,4 @@
1
- <!-- Quick index of pages in the collection -->
1
+ {% comment %} Quick index of pages in the collection {% endcomment %}
2
2
  {% if site.collections.pages %}
3
3
  <ul>
4
4
  <p>Quick Index</p>
@@ -10,7 +10,7 @@
10
10
  </ul>
11
11
  {% endif %}
12
12
 
13
- <!-- Post Categories -->
13
+ {% comment %} Post Categories {% endcomment %}
14
14
  {% if page.sidebar == "searchCats" %}
15
15
  <div id="categories">
16
16
  </div>
@@ -0,0 +1,67 @@
1
+ {% comment %} Feature: ZER0-084 {% endcomment %}
2
+ {%- comment -%}
3
+ Component: recipe-card
4
+ Path: _includes/components/recipe-card.html
5
+ Purpose: Index card for one recipe — photo, title, one-line description,
6
+ and the three facts a cook scans for: total time, what it makes,
7
+ and how hard it is.
8
+ Params: recipe (document, required) — a `recipes` collection doc
9
+ heading_level (number, optional, default: 3) — the title's
10
+ heading level, so a card never skips a level under whatever
11
+ heading precedes it
12
+ class (string, optional, default: "")
13
+ Depends on: components/preview-image.html, components/recipe-duration.html,
14
+ _sass/components/_recipe.scss (.recipe-card)
15
+ Notes: Total time comes from `times.total` or prep + cook + rest, so
16
+ the card never disagrees with the recipe page. The whole card is
17
+ one link for a large tap target; the facts row is plain text so
18
+ it stays readable when images fail to load.
19
+ {%- endcomment -%}
20
+ {%- if include.recipe == nil -%}
21
+ {%- comment -%} ERROR: recipe-card requires `recipe` {%- endcomment -%}
22
+ {%- else -%}
23
+ {%- assign _rc_r = include.recipe -%}
24
+ {%- assign _rc_img = _rc_r.image | default: _rc_r.preview | default: site.teaser -%}
25
+ {%- assign _rc_prep = _rc_r.times.prep | default: 0 -%}
26
+ {%- assign _rc_cook = _rc_r.times.cook | default: 0 -%}
27
+ {%- assign _rc_rest = _rc_r.times.rest | default: 0 -%}
28
+ {%- if _rc_r.times.total -%}
29
+ {%- assign _rc_total = _rc_r.times.total -%}
30
+ {%- else -%}
31
+ {%- assign _rc_total = _rc_prep | plus: _rc_cook | plus: _rc_rest -%}
32
+ {%- endif -%}
33
+ {%- capture _rc_total_h -%}{%- include components/recipe-duration.html minutes=_rc_total -%}{%- endcapture -%}
34
+ {%- assign _rc_total_h = _rc_total_h | strip -%}
35
+ <article class="recipe-card h-100 {{ include.class | default: '' }}">
36
+ <a href="{{ _rc_r.url | relative_url }}" class="recipe-card-link d-flex flex-column h-100 text-decoration-none text-body">
37
+ {%- if _rc_img -%}
38
+ <div class="recipe-card-media">
39
+ {%- include components/preview-image.html src=_rc_img alt=_rc_r.title class="" -%}
40
+ </div>
41
+ {%- endif -%}
42
+ <div class="recipe-card-body">
43
+ {%- assign _rc_hl = include.heading_level | default: 3 -%}
44
+ <h{{ _rc_hl }} class="h5 recipe-card-title">{{ _rc_r.title }}</h{{ _rc_hl }}>
45
+ {%- if _rc_r.description -%}
46
+ <p class="recipe-card-description small text-body-secondary">{{ _rc_r.description | strip_html | truncate: 120 }}</p>
47
+ {%- endif -%}
48
+ <p class="recipe-card-facts small text-body-secondary mb-0">
49
+ {%- assign _rc_sep = false -%}
50
+ {%- if _rc_total_h != "" -%}
51
+ <span class="recipe-card-fact"><i class="bi bi-clock-history me-1" aria-hidden="true"></i>{{ _rc_total_h }}</span>
52
+ {%- assign _rc_sep = true -%}
53
+ {%- endif -%}
54
+ {%- if _rc_r.yield.amount -%}
55
+ {%- if _rc_sep %}<span class="mx-1" aria-hidden="true">•</span>{% endif -%}
56
+ <span class="recipe-card-fact"><i class="bi bi-pie-chart me-1" aria-hidden="true"></i>{{ _rc_r.yield.amount }} {{ _rc_r.yield.unit | default: "servings" | escape }}</span>
57
+ {%- assign _rc_sep = true -%}
58
+ {%- endif -%}
59
+ {%- if _rc_r.difficulty -%}
60
+ {%- if _rc_sep %}<span class="mx-1" aria-hidden="true">•</span>{% endif -%}
61
+ <span class="recipe-difficulty recipe-difficulty--{{ _rc_r.difficulty | downcase | strip }}">{{ _rc_r.difficulty | capitalize }}</span>
62
+ {%- endif -%}
63
+ </p>
64
+ </div>
65
+ </a>
66
+ </article>
67
+ {%- endif -%}
@@ -0,0 +1,50 @@
1
+ {% comment %} Feature: ZER0-084 {% endcomment %}
2
+ {%- comment -%}
3
+ Component: recipe-duration (value resolver — emits a bare string)
4
+ Path: _includes/components/recipe-duration.html
5
+ Purpose: Format a duration given in MINUTES, either for humans
6
+ ("1 hr 30 min") or as an ISO 8601 duration for schema.org
7
+ ("PT1H30M").
8
+ Params: minutes (number, required)
9
+ format (string, optional, default: "human") — "human" | "iso"
10
+ Depends on: nothing (pure Liquid arithmetic)
11
+ Notes: Emits nothing for a missing or zero duration, so callers can
12
+ test the captured value and skip the whole row. Days are not
13
+ special-cased — a 3-day cure reads as "72 hr", which is how
14
+ recipes actually write long ferments.
15
+
16
+ Liquid `assign` writes to the GLOBAL scope even inside an
17
+ include, so a helper called several times from one caller will
18
+ clobber the caller's variables if the names collide. Every
19
+ variable here is therefore prefixed `_rd_`, and the other recipe
20
+ components use their own prefixes. Keep that up when editing.
21
+
22
+ Capture it (this comment is brace-free so it cannot self-execute):
23
+ capture total -> include components/recipe-duration.html minutes=90
24
+ {%- endcomment -%}
25
+ {%- assign _rd_m = include.minutes -%}
26
+ {%- if _rd_m == nil or _rd_m == "" or _rd_m <= 0 -%}
27
+ {%- comment -%} recipe-duration: no duration — emitting nothing {%- endcomment -%}
28
+ {%- else -%}
29
+ {%- assign _rd_total = _rd_m | round -%}
30
+ {%- assign _rd_hours = _rd_total | divided_by: 60 -%}
31
+ {%- assign _rd_mins = _rd_total | modulo: 60 -%}
32
+ {%- if include.format == "iso" -%}
33
+ PT{% if _rd_hours > 0 %}{{ _rd_hours }}H{% endif %}{% if _rd_mins > 0 %}{{ _rd_mins }}M{% endif %}
34
+ {%- else -%}
35
+ {%- comment -%}
36
+ Built by appending rather than by emitting the separator from inside an
37
+ `if`: Liquid treats a block whose body is only whitespace as blank and
38
+ drops it, which silently swallowed the space in "12 hr 45 min".
39
+ {%- endcomment -%}
40
+ {%- assign _rd_out = "" -%}
41
+ {%- if _rd_hours > 0 -%}
42
+ {%- assign _rd_out = _rd_out | append: _rd_hours | append: " hr" -%}
43
+ {%- endif -%}
44
+ {%- if _rd_mins > 0 -%}
45
+ {%- if _rd_out != "" -%}{%- assign _rd_out = _rd_out | append: " " -%}{%- endif -%}
46
+ {%- assign _rd_out = _rd_out | append: _rd_mins | append: " min" -%}
47
+ {%- endif -%}
48
+ {{- _rd_out -}}
49
+ {%- endif -%}
50
+ {%- endif -%}
@@ -0,0 +1,58 @@
1
+ {% comment %} Feature: ZER0-084 {% endcomment %}
2
+ {%- comment -%}
3
+ Component: recipe-grams (value resolver — emits a bare number)
4
+ Path: _includes/components/recipe-grams.html
5
+ Purpose: Convert one recipe quantity to grams so ratio tables and
6
+ baker's percentages can be computed at BUILD time (no JS needed).
7
+ Params: qty (number, required) — the amount as authored
8
+ unit (string, optional) — g, kg, oz, lb, ml, l, tsp,
9
+ tbsp, cup, fl oz, pint, …
10
+ grams_per_cup (number, optional) — density, required to convert
11
+ a VOLUME into a weight
12
+ Depends on: nothing (pure Liquid arithmetic)
13
+ Notes: Outputs a bare number rounded to 1 decimal, or nothing at all
14
+ when the unit is unknown/countable ("clove", "egg") or a volume
15
+ with no density. Callers MUST treat empty output as "unknown".
16
+ Mirrors the unit table in assets/js/recipe-scaler.js — change
17
+ both together.
18
+
19
+ Liquid `assign` writes to the GLOBAL scope even inside an
20
+ include, so every variable here is prefixed `_rg_` to keep
21
+ repeated calls from clobbering the caller's own variables.
22
+
23
+ Capture it (this comment is brace-free so it cannot self-execute):
24
+ capture grams -> include components/recipe-grams.html
25
+ qty=i.qty unit=i.unit grams_per_cup=density
26
+ {%- endcomment -%}
27
+ {%- if include.qty == nil or include.qty == "" -%}
28
+ {%- comment -%} ERROR: recipe-grams requires `qty` {%- endcomment -%}
29
+ {%- else -%}
30
+ {%- assign _rg_u = include.unit | default: "" | downcase | strip | replace: ".", "" | replace: "-", " " -%}
31
+ {%- assign _rg_mass = 0 -%}
32
+ {%- assign _rg_ml = 0 -%}
33
+ {%- case _rg_u -%}
34
+ {%- when "g" or "gram" or "grams" or "gr" -%}{%- assign _rg_mass = 1 -%}
35
+ {%- when "kg" or "kilogram" or "kilograms" -%}{%- assign _rg_mass = 1000 -%}
36
+ {%- when "mg" or "milligram" or "milligrams" -%}{%- assign _rg_mass = 0.001 -%}
37
+ {%- when "oz" or "ounce" or "ounces" -%}{%- assign _rg_mass = 28.3495 -%}
38
+ {%- when "lb" or "lbs" or "pound" or "pounds" -%}{%- assign _rg_mass = 453.592 -%}
39
+ {%- when "ml" or "milliliter" or "milliliters" or "millilitre" or "millilitres" or "cc" -%}{%- assign _rg_ml = 1 -%}
40
+ {%- when "l" or "liter" or "liters" or "litre" or "litres" -%}{%- assign _rg_ml = 1000 -%}
41
+ {%- when "dl" or "deciliter" or "deciliters" -%}{%- assign _rg_ml = 100 -%}
42
+ {%- when "tsp" or "teaspoon" or "teaspoons" -%}{%- assign _rg_ml = 4.92892 -%}
43
+ {%- when "tbsp" or "tbs" or "tablespoon" or "tablespoons" -%}{%- assign _rg_ml = 14.7868 -%}
44
+ {%- when "cup" or "cups" -%}{%- assign _rg_ml = 236.588 -%}
45
+ {%- when "fl oz" or "floz" or "fluid ounce" or "fluid ounces" -%}{%- assign _rg_ml = 29.5735 -%}
46
+ {%- when "pint" or "pints" or "pt" -%}{%- assign _rg_ml = 473.176 -%}
47
+ {%- when "quart" or "quarts" or "qt" -%}{%- assign _rg_ml = 946.353 -%}
48
+ {%- when "gallon" or "gallons" or "gal" -%}{%- assign _rg_ml = 3785.41 -%}
49
+ {%- endcase -%}
50
+ {%- if _rg_mass > 0 -%}
51
+ {%- assign _rg_g = include.qty | times: _rg_mass -%}
52
+ {{- _rg_g | round: 1 -}}
53
+ {%- elsif _rg_ml > 0 and include.grams_per_cup -%}
54
+ {%- assign _rg_vol = include.qty | times: _rg_ml -%}
55
+ {%- assign _rg_g = _rg_vol | times: include.grams_per_cup | divided_by: 236.588 -%}
56
+ {{- _rg_g | round: 1 -}}
57
+ {%- endif -%}
58
+ {%- endif -%}