jekyll-theme-zer0 1.27.0 → 1.29.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 (142) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +812 -13
  3. data/_data/backlog.yml +356 -2
  4. data/_data/consumers.yml +252 -0
  5. data/_data/features.yml +234 -5
  6. data/_data/i18n/fr.yml +103 -0
  7. data/_data/i18n/manifest.yml +1529 -0
  8. data/_data/ingredient_densities.yml +122 -0
  9. data/_data/navigation/docs.yml +2 -0
  10. data/_data/navigation/main.yml +22 -0
  11. data/_data/recipe_courses.yml +64 -0
  12. data/_data/series.yml +19 -0
  13. data/_data/theme-manifest.yml +387 -277
  14. data/_data/ui-text.yml +8 -0
  15. data/_includes/README.md +36 -3
  16. data/_includes/analytics/google-tag-manager-body.html +11 -2
  17. data/_includes/analytics/google-tag-manager-head.html +16 -6
  18. data/_includes/analytics/posthog.html +33 -4
  19. data/_includes/components/abc-letter.html +43 -0
  20. data/_includes/components/background-customizer.html +2 -2
  21. data/_includes/components/book-card.html +42 -0
  22. data/_includes/components/book-nav.html +80 -0
  23. data/_includes/components/book-plate.html +31 -0
  24. data/_includes/components/book-toc.html +47 -0
  25. data/_includes/components/bookshelf.html +68 -0
  26. data/_includes/components/card-grid.html +60 -0
  27. data/_includes/components/cookie-consent.html +4 -4
  28. data/_includes/components/data-card.html +95 -0
  29. data/_includes/components/halfmoon.html +5 -1
  30. data/_includes/components/info-section.html +8 -3
  31. data/_includes/components/language-toggle.html +168 -21
  32. data/_includes/components/page-feedback.html +65 -2
  33. data/_includes/components/page-views-init.html +55 -0
  34. data/_includes/components/page-views.html +33 -0
  35. data/_includes/components/recipe-card.html +67 -0
  36. data/_includes/components/recipe-duration.html +50 -0
  37. data/_includes/components/recipe-grams.html +58 -0
  38. data/_includes/components/recipe-index.html +96 -0
  39. data/_includes/components/recipe-ingredients.html +90 -0
  40. data/_includes/components/recipe-meta.html +96 -0
  41. data/_includes/components/recipe-nutrition.html +57 -0
  42. data/_includes/components/recipe-qty.html +73 -0
  43. data/_includes/components/recipe-ratio.html +151 -0
  44. data/_includes/components/recipe-scaler.html +73 -0
  45. data/_includes/components/recipe-steps.html +86 -0
  46. data/_includes/components/recipe-temp.html +45 -0
  47. data/_includes/components/search-modal.html +2 -2
  48. data/_includes/components/shortcuts-modal.html +3 -0
  49. data/_includes/components/theme-controls-bar.html +10 -2
  50. data/_includes/components/theme-customizer.html +8 -2
  51. data/_includes/components/theme-info.html +8 -1
  52. data/_includes/content/jsonld-software.html +22 -3
  53. data/_includes/content/seo.html +9 -3
  54. data/_includes/core/color-mode-init.html +13 -4
  55. data/_includes/core/favicon.html +46 -0
  56. data/_includes/core/footer.html +18 -16
  57. data/_includes/core/head.html +21 -0
  58. data/_includes/core/header.html +7 -4
  59. data/_includes/custom/body-end.html +18 -0
  60. data/_includes/custom/body-start.html +17 -0
  61. data/_includes/custom/footer.html +18 -0
  62. data/_includes/custom/head.html +18 -0
  63. data/_includes/navigation/local-graph.html +28 -2
  64. data/_includes/navigation/nav-tree.html +3 -3
  65. data/_includes/navigation/section-sidebar.html +93 -11
  66. data/_includes/navigation/sidebar-config.html +21 -0
  67. data/_includes/navigation/sidebar-left.html +2 -1
  68. data/_includes/navigation/sidebar-nav.html +4 -0
  69. data/_includes/navigation/sidebar-pagetree.html +150 -0
  70. data/_includes/navigation/sidebar-right.html +2 -1
  71. data/_includes/navigation/unified-drawer.html +8 -2
  72. data/_includes/obsidian/full-graph.html +165 -136
  73. data/_includes/setup/wizard.html +173 -86
  74. data/_includes/stats/stats-metrics.html +2 -2
  75. data/_layouts/404.html +260 -0
  76. data/_layouts/README.md +4 -0
  77. data/_layouts/admin.html +2 -2
  78. data/_layouts/article.html +5 -0
  79. data/_layouts/book-abc.html +106 -0
  80. data/_layouts/book-story.html +91 -0
  81. data/_layouts/book.html +113 -0
  82. data/_layouts/collection.html +17 -5
  83. data/_layouts/cookbook.html +88 -0
  84. data/_layouts/default.html +4 -4
  85. data/_layouts/home.html +5 -2
  86. data/_layouts/landing.html +9 -0
  87. data/_layouts/news.html +155 -31
  88. data/_layouts/recipe.html +274 -0
  89. data/_layouts/root.html +38 -5
  90. data/_layouts/section.html +96 -25
  91. data/_sass/components/_book.scss +423 -0
  92. data/_sass/components/_callout.scss +1 -1
  93. data/_sass/components/_footer.scss +37 -1
  94. data/_sass/components/_page-views.scss +36 -0
  95. data/_sass/components/_recipe.scss +506 -0
  96. data/_sass/components/_setup-wizard.scss +235 -0
  97. data/_sass/components/_ui-enhancements.scss +6 -6
  98. data/_sass/core/_navbar.scss +263 -15
  99. data/_sass/core/_obsidian.scss +286 -6
  100. data/_sass/layouts/_landing.scss +2 -2
  101. data/_sass/theme/_backgrounds.scss +21 -8
  102. data/_sass/tokens/_color.scss +6 -0
  103. data/_sass/tokens/_index.scss +2 -0
  104. data/_sass/tokens/_radius.scss +21 -0
  105. data/_sass/tokens/_typography.scss +4 -0
  106. data/_sass/utilities/_focus.scss +14 -0
  107. data/assets/css/main.scss +4 -0
  108. data/assets/js/auto-hide-nav.js +5 -1
  109. data/assets/js/halfmoon.js +26 -0
  110. data/assets/js/modules/navigation/navbar.js +55 -0
  111. data/assets/js/obsidian-graph.js +702 -264
  112. data/assets/js/obsidian-local-graph.js +161 -54
  113. data/assets/js/page-views.js +372 -0
  114. data/assets/js/recipe-scaler.js +501 -0
  115. data/assets/js/search-modal.js +14 -1
  116. data/assets/js/setup-wizard.js +322 -20
  117. data/scripts/README.md +29 -0
  118. data/scripts/bin/audit-consumer +39 -7
  119. data/scripts/bin/giscus-discussions +213 -14
  120. data/scripts/bin/manifest +66 -16
  121. data/scripts/bin/validate +5 -1
  122. data/scripts/ci/agent_review_result.py +164 -0
  123. data/scripts/ci/test_agent_review_result.py +172 -0
  124. data/scripts/design-system-check.rb +170 -0
  125. data/scripts/install/README.md +47 -6
  126. data/scripts/install/ai/client.sh +302 -93
  127. data/scripts/install/ai/prompts/spec.schema.json +1 -1
  128. data/scripts/install/ai/wizard.sh +10 -5
  129. data/scripts/install/apply.sh +7 -3
  130. data/scripts/install/cli.sh +54 -4
  131. data/scripts/install/config.sh +167 -0
  132. data/scripts/install/doctor.sh +38 -0
  133. data/scripts/install/plan.sh +10 -0
  134. data/scripts/install/spec.sh +15 -7
  135. data/scripts/install/template.sh +4 -0
  136. data/scripts/lib/audit.sh +42 -2
  137. data/scripts/lint-liquid-raw.rb +137 -0
  138. data/scripts/propagate.rb +277 -0
  139. data/scripts/test/lib/run_tests.sh +2 -1
  140. data/scripts/test/lib/test_agent_review_result.sh +27 -0
  141. data/scripts/translate.rb +161 -15
  142. metadata +56 -2
@@ -0,0 +1,151 @@
1
+ {% comment %} Feature: ZER0-084 {% endcomment %}
2
+ {%- comment -%}
3
+ Component: recipe-ratio
4
+ Path: _includes/components/recipe-ratio.html
5
+ Purpose: The formula behind the recipe — every ingredient reduced to a
6
+ weight and a percentage of one reference ingredient. This is the
7
+ baker's-percentage table bread, pastry and sauce recipes are
8
+ really built on, and it is what lets a reader rebuild the recipe
9
+ at any size instead of only at multiples of the printed one.
10
+ Params: recipe (doc, optional, default: page)
11
+ basis (string, optional, default: page.ratio_basis) — the
12
+ ingredient that counts as 100%; matched as a substring, so
13
+ "flour" covers "bread flour" + "whole-wheat flour"
14
+ heading (string, optional, default: "Ratio")
15
+ heading_level (string, optional, default: "h2")
16
+ Depends on: components/recipe-grams.html, components/recipe-qty.html,
17
+ _data/ingredient_densities.yml, _sass/components/_recipe.scss
18
+ Notes: Percentages are computed at BUILD time, so the table is right
19
+ with JavaScript disabled and never drifts from the ingredient
20
+ list. They are also scale-invariant — only the weight column
21
+ moves when the reader rescales. Ingredients that cannot be
22
+ resolved to a weight (a clove of garlic, "1 egg", a volume with
23
+ no density) are listed without a percentage rather than guessed
24
+ at. Set `ratio: false` on an ingredient to leave it out, or
25
+ author `baker_percent:` to pin a value.
26
+ {%- endcomment -%}
27
+ {%- assign _rr_r = include.recipe | default: page -%}
28
+ {%- assign _rr_basis = include.basis | default: _rr_r.ratio_basis -%}
29
+ {%- assign _rr_ingredients = _rr_r.ingredients -%}
30
+ {%- if _rr_ingredients == nil or _rr_ingredients.size == 0 or _rr_basis == nil -%}
31
+ {%- comment -%} recipe-ratio: needs `ingredients` and a `ratio_basis` — skipping render {%- endcomment -%}
32
+ {%- else -%}
33
+ {%- assign _rr_basis_needle = _rr_basis | downcase | strip -%}
34
+ {%- assign _rr_hl = include.heading_level | default: "h2" -%}
35
+
36
+ {%- comment -%} Flatten grouped ingredients into one list. {%- endcomment -%}
37
+ {%- assign _rr_flat = "" | split: "" -%}
38
+ {%- for _rr_entry in _rr_ingredients -%}
39
+ {%- if _rr_entry.items -%}
40
+ {%- for _rr_i in _rr_entry.items -%}{%- assign _rr_flat = _rr_flat | push: _rr_i -%}{%- endfor -%}
41
+ {%- else -%}
42
+ {%- assign _rr_flat = _rr_flat | push: _rr_entry -%}
43
+ {%- endif -%}
44
+ {%- endfor -%}
45
+
46
+ {%- comment -%} Pass 1: total the basis weight and the batch weight. {%- endcomment -%}
47
+ {%- assign _rr_basis_grams = 0 -%}
48
+ {%- assign _rr_total_grams = 0 -%}
49
+ {%- assign _rr_weighed = 0 -%}
50
+ {%- for _rr_i in _rr_flat -%}
51
+ {%- if _rr_i.ratio == false -%}{%- continue -%}{%- endif -%}
52
+ {%- assign _rr_slug = _rr_i.item | default: "" | downcase | strip | replace: " ", "-" -%}
53
+ {%- assign _rr_gpc = _rr_i.grams_per_cup | default: site.data.ingredient_densities[_rr_slug] -%}
54
+ {%- capture _rr_g -%}{%- include components/recipe-grams.html qty=_rr_i.qty unit=_rr_i.unit grams_per_cup=_rr_gpc -%}{%- endcapture -%}
55
+ {%- assign _rr_g = _rr_g | strip -%}
56
+ {%- if _rr_g != "" -%}
57
+ {%- assign _rr_weighed = _rr_weighed | plus: 1 -%}
58
+ {%- assign _rr_total_grams = _rr_total_grams | plus: _rr_g -%}
59
+ {%- assign _rr_name = _rr_i.item | default: "" | downcase -%}
60
+ {%- if _rr_name contains _rr_basis_needle -%}
61
+ {%- assign _rr_basis_grams = _rr_basis_grams | plus: _rr_g -%}
62
+ {%- endif -%}
63
+ {%- endif -%}
64
+ {%- endfor -%}
65
+
66
+ {%- if _rr_weighed < 2 or _rr_basis_grams <= 0 -%}
67
+ {%- comment -%} recipe-ratio: fewer than two weighable ingredients, or the basis "{{ _rr_basis }}" matched nothing — skipping render {%- endcomment -%}
68
+ {%- else -%}
69
+ <section class="recipe-ratio">
70
+ <{{ _rr_hl }} class="recipe-section-heading">
71
+ <i class="bi bi-percent me-2" aria-hidden="true"></i>{{ include.heading | default: "Ratio" }}
72
+ </{{ _rr_hl }}>
73
+ <p class="recipe-ratio-intro text-body-secondary">
74
+ Every weight below is a percentage of the {{ _rr_basis | escape }}
75
+ {%- if _rr_r.ratio %}, which gives this recipe its <strong>{{ _rr_r.ratio | escape }}</strong> ratio{% endif -%}.
76
+ Rescale the recipe and the weights follow; the percentages are what stay true.
77
+ </p>
78
+ <div class="table-responsive">
79
+ <table class="table table-sm recipe-ratio-table align-middle">
80
+ <thead>
81
+ <tr>
82
+ <th scope="col">Ingredient</th>
83
+ <th scope="col" class="text-end">Weight</th>
84
+ <th scope="col" class="text-end">% of {{ _rr_basis | escape }}</th>
85
+ </tr>
86
+ </thead>
87
+ <tbody>
88
+ {%- for _rr_i in _rr_flat -%}
89
+ {%- if _rr_i.ratio == false -%}{%- continue -%}{%- endif -%}
90
+ {%- assign _rr_slug = _rr_i.item | default: "" | downcase | strip | replace: " ", "-" -%}
91
+ {%- assign _rr_gpc = _rr_i.grams_per_cup | default: site.data.ingredient_densities[_rr_slug] -%}
92
+ {%- capture _rr_g -%}{%- include components/recipe-grams.html qty=_rr_i.qty unit=_rr_i.unit grams_per_cup=_rr_gpc -%}{%- endcapture -%}
93
+ {%- assign _rr_g = _rr_g | strip -%}
94
+ <tr>
95
+ <th scope="row" class="fw-normal">{{ _rr_i.item | escape }}{% if _rr_i.optional %} <span class="text-body-secondary small">(optional)</span>{% endif %}</th>
96
+ <td class="text-end">
97
+ {%- if _rr_g != "" -%}
98
+ {%- include components/recipe-qty.html qty=_rr_g unit="g" -%}
99
+ {%- else -%}
100
+ <span class="text-body-secondary">—</span>
101
+ {%- endif -%}
102
+ </td>
103
+ <td class="text-end">
104
+ {%- if _rr_i.baker_percent -%}
105
+ {{ _rr_i.baker_percent }}%
106
+ {%- elsif _rr_g != "" -%}
107
+ {%- assign _rr_pct = _rr_g | times: 100.0 | divided_by: _rr_basis_grams -%}
108
+ {%- assign _rr_pct_i = _rr_pct | round -%}
109
+ {%- if _rr_pct == _rr_pct_i -%}{{ _rr_pct_i }}{%- else -%}{{ _rr_pct | round: 1 }}{%- endif -%}%
110
+ {%- else -%}
111
+ <span class="text-body-secondary">—</span>
112
+ {%- endif -%}
113
+ </td>
114
+ </tr>
115
+ {%- endfor -%}
116
+ </tbody>
117
+ <tfoot>
118
+ <tr>
119
+ <th scope="row">Total weighed</th>
120
+ <td class="text-end">{%- include components/recipe-qty.html qty=_rr_total_grams unit="g" -%}</td>
121
+ <td class="text-end">
122
+ {%- assign _rr_tpct = _rr_total_grams | times: 100.0 | divided_by: _rr_basis_grams -%}
123
+ {%- assign _rr_tpct_i = _rr_tpct | round -%}
124
+ {%- if _rr_tpct == _rr_tpct_i -%}{{ _rr_tpct_i }}{%- else -%}{{ _rr_tpct | round: 1 }}{%- endif -%}%
125
+ </td>
126
+ </tr>
127
+ {%- if _rr_r.yield.amount and _rr_r.yield.amount > 0 -%}
128
+ {%- assign _rr_per_unit = _rr_total_grams | divided_by: _rr_r.yield.amount | round: 1 -%}
129
+ {%- comment -%} Naive singular for the row label: "slices" → "slice". {%- endcomment -%}
130
+ {%- assign _rr_yu = _rr_r.yield.singular | default: _rr_r.yield.unit | default: "serving" | strip -%}
131
+ {%- unless _rr_r.yield.singular -%}
132
+ {%- if _rr_yu.size > 1 -%}
133
+ {%- assign _rr_tail = _rr_yu | slice: -1 -%}
134
+ {%- if _rr_tail == "s" -%}
135
+ {%- assign _rr_cut = _rr_yu.size | minus: 1 -%}
136
+ {%- assign _rr_yu = _rr_yu | slice: 0, _rr_cut -%}
137
+ {%- endif -%}
138
+ {%- endif -%}
139
+ {%- endunless -%}
140
+ <tr>
141
+ <th scope="row">Per {{ _rr_yu | escape }}</th>
142
+ <td class="text-end">{%- include components/recipe-qty.html qty=_rr_per_unit unit="g" scale=false -%}</td>
143
+ <td class="text-end"><span class="text-body-secondary">—</span></td>
144
+ </tr>
145
+ {%- endif -%}
146
+ </tfoot>
147
+ </table>
148
+ </div>
149
+ </section>
150
+ {%- endif -%}
151
+ {%- endif -%}
@@ -0,0 +1,73 @@
1
+ {% comment %} Feature: ZER0-084 {% endcomment %}
2
+ {%- comment -%}
3
+ Component: recipe-scaler
4
+ Path: _includes/components/recipe-scaler.html
5
+ Purpose: The control bar that drives every quantity on a recipe page —
6
+ scale the yield up or down, and switch the whole recipe between
7
+ the units it was written in, metric, and US customary.
8
+ Params: base_yield (number, optional, default: page.yield.amount, then 1)
9
+ yield_unit (string, optional, default: page.yield.unit, then
10
+ "servings")
11
+ recipe_id (string, optional, default: page.slug) — key the
12
+ remembered scale is stored under
13
+ units (bool, optional, default: true) — false hides the unit
14
+ switch (for a recipe that is already all-metric, say)
15
+ class (string, optional, default: "")
16
+ Depends on: assets/js/recipe-scaler.js, _sass/components/_recipe.scss
17
+ Notes: Progressive enhancement: with JavaScript off the controls are
18
+ hidden (.recipe-scaler is display:none until the script marks it
19
+ ready) and the page still shows every amount exactly as written.
20
+ The status paragraph is an aria-live region so screen-reader
21
+ users hear "Scaled to 24 cookies" rather than silently changing
22
+ numbers underneath them.
23
+ {%- endcomment -%}
24
+ {%- assign _rsc_base = include.base_yield | default: page.yield.amount | default: 1 -%}
25
+ {%- assign _rsc_unit = include.yield_unit | default: page.yield.unit | default: "servings" -%}
26
+ {%- assign _rsc_id = include.recipe_id | default: page.slug | default: "recipe" -%}
27
+ {%- assign _rsc_show_units = true -%}
28
+ {%- if include.units == false -%}{%- assign _rsc_show_units = false -%}{%- endif -%}
29
+ <div class="recipe-scaler {{ include.class | default: '' }}" data-recipe-scaler data-base-yield="{{ _rsc_base }}" data-yield-unit="{{ _rsc_unit | escape }}" data-recipe-id="{{ _rsc_id | escape }}" hidden>
30
+ <div class="recipe-scaler-group">
31
+ <span class="recipe-scaler-label" id="{{ _rsc_id }}-scale-label">Scale</span>
32
+ <div class="input-group input-group-sm recipe-scaler-stepper">
33
+ <button class="btn btn-outline-secondary" type="button" data-recipe-step="-1" aria-label="Decrease {{ _rsc_unit | escape }}">
34
+ <i class="bi bi-dash-lg" aria-hidden="true"></i>
35
+ </button>
36
+ <input class="form-control text-center" type="number" inputmode="decimal" min="0.25" step="1"
37
+ value="{{ _rsc_base }}" data-recipe-yield-input
38
+ aria-labelledby="{{ _rsc_id }}-scale-label" aria-describedby="{{ _rsc_id }}-yield-unit">
39
+ <span class="input-group-text" id="{{ _rsc_id }}-yield-unit" data-recipe-yield-unit>{{ _rsc_unit | escape }}</span>
40
+ <button class="btn btn-outline-secondary" type="button" data-recipe-step="1" aria-label="Increase {{ _rsc_unit | escape }}">
41
+ <i class="bi bi-plus-lg" aria-hidden="true"></i>
42
+ </button>
43
+ </div>
44
+ </div>
45
+
46
+ <div class="recipe-scaler-group">
47
+ <div class="btn-group btn-group-sm" role="group" aria-label="Quick scale">
48
+ <button type="button" class="btn btn-outline-secondary" data-recipe-multiplier="0.5">½×</button>
49
+ <button type="button" class="btn btn-outline-secondary" data-recipe-multiplier="1">1×</button>
50
+ <button type="button" class="btn btn-outline-secondary" data-recipe-multiplier="2">2×</button>
51
+ <button type="button" class="btn btn-outline-secondary" data-recipe-multiplier="3">3×</button>
52
+ </div>
53
+ </div>
54
+
55
+ {%- if _rsc_show_units -%}
56
+ <div class="recipe-scaler-group">
57
+ <span class="recipe-scaler-label" id="{{ _rsc_id }}-units-label">Units</span>
58
+ <div class="btn-group btn-group-sm" role="group" aria-labelledby="{{ _rsc_id }}-units-label">
59
+ <button type="button" class="btn btn-outline-secondary" data-recipe-units="original" title="Show every amount exactly as the recipe was written">As written</button>
60
+ <button type="button" class="btn btn-outline-secondary" data-recipe-units="metric" title="Grams, millilitres and °C — volumes become weights wherever a density is known">Metric</button>
61
+ <button type="button" class="btn btn-outline-secondary" data-recipe-units="us" title="Cups, spoons, ounces and °F">US</button>
62
+ </div>
63
+ </div>
64
+ {%- endif -%}
65
+
66
+ <div class="recipe-scaler-group recipe-scaler-group--reset">
67
+ <button type="button" class="btn btn-sm btn-link" data-recipe-reset>
68
+ <i class="bi bi-arrow-counterclockwise me-1" aria-hidden="true"></i>Reset
69
+ </button>
70
+ </div>
71
+
72
+ <p class="visually-hidden" role="status" aria-live="polite" data-recipe-status></p>
73
+ </div>
@@ -0,0 +1,86 @@
1
+ {% comment %} Feature: ZER0-084 {% endcomment %}
2
+ {%- comment -%}
3
+ Component: recipe-steps
4
+ Path: _includes/components/recipe-steps.html
5
+ Purpose: The numbered method for one recipe — optionally split into
6
+ sections ("Day 1", "Assembly"), with per-step timings,
7
+ convertible temperatures, and step photos.
8
+ Params: steps (array, optional, default: page.steps)
9
+ heading (string, optional, default: "Method")
10
+ heading_level (string, optional, default: "h2")
11
+ Depends on: components/recipe-temp.html, components/recipe-duration.html,
12
+ _sass/components/_recipe.scss
13
+ Notes: A step is either a plain string or a hash of
14
+ {text, title, time, temp_f, temp_c, image, image_alt, note}.
15
+ Sections are {section, items} hashes; numbering continues across
16
+ them via the <ol start> attribute so "step 7" means step 7 no
17
+ matter which section it lives in. Step text is markdownified, so
18
+ links and emphasis work. itemprop="recipeInstructions" marks
19
+ each step for schema.org.
20
+ {%- endcomment -%}
21
+ {%- assign _rs_steps = include.steps | default: page.steps -%}
22
+ {%- if _rs_steps == nil or _rs_steps.size == 0 -%}
23
+ {%- comment -%} recipe-steps: no `steps` front matter — skipping render {%- endcomment -%}
24
+ {%- else -%}
25
+ {%- assign _rs_hl = include.heading_level | default: "h2" -%}
26
+ {%- assign _rs_sub = _rs_hl | remove: "h" | plus: 1 -%}
27
+ {%- assign _rs_n = 0 -%}
28
+ <section class="recipe-steps">
29
+ <{{ _rs_hl }} class="recipe-section-heading">
30
+ <i class="bi bi-list-ol me-2" aria-hidden="true"></i>{{ include.heading | default: "Method" }}
31
+ </{{ _rs_hl }}>
32
+
33
+ {%- for _rs_entry in _rs_steps -%}
34
+ {%- if _rs_entry.items -%}
35
+ {%- assign _rs_section = _rs_entry.section | default: _rs_entry.group | default: _rs_entry.title -%}
36
+ {%- assign _rs_loop_steps = _rs_entry.items -%}
37
+ {%- else -%}
38
+ {%- assign _rs_section = nil -%}
39
+ {%- assign _rs_loop_steps = "" | split: "" | push: _rs_entry -%}
40
+ {%- endif -%}
41
+
42
+ {%- if _rs_section -%}
43
+ <h{{ _rs_sub }} class="recipe-steps-section-title">{{ _rs_section | escape }}</h{{ _rs_sub }}>
44
+ {%- endif -%}
45
+
46
+ <ol class="recipe-step-list" start="{{ _rs_n | plus: 1 }}">
47
+ {%- for _rs_s in _rs_loop_steps -%}
48
+ {%- assign _rs_n = _rs_n | plus: 1 -%}
49
+ {%- if _rs_s.text or _rs_s.title or _rs_s.image -%}
50
+ {%- assign _rs_text = _rs_s.text -%}
51
+ {%- else -%}
52
+ {%- assign _rs_text = _rs_s -%}
53
+ {%- endif -%}
54
+ <li class="recipe-step" itemprop="recipeInstructions">
55
+ {%- if _rs_s.title -%}
56
+ <p class="recipe-step-title">{{ _rs_s.title | escape }}</p>
57
+ {%- endif -%}
58
+ {%- if _rs_text -%}
59
+ <div class="recipe-step-text">{{ _rs_text | markdownify }}</div>
60
+ {%- endif -%}
61
+ {%- if _rs_s.time or _rs_s.temp_f or _rs_s.temp_c -%}
62
+ <p class="recipe-step-facts">
63
+ {%- if _rs_s.time -%}
64
+ {%- capture _rs_dur -%}{%- include components/recipe-duration.html minutes=_rs_s.time -%}{%- endcapture -%}
65
+ <span class="recipe-step-fact"><i class="bi bi-stopwatch me-1" aria-hidden="true"></i>{{ _rs_dur | strip }}</span>
66
+ {%- endif -%}
67
+ {%- if _rs_s.temp_f or _rs_s.temp_c -%}
68
+ <span class="recipe-step-fact"><i class="bi bi-thermometer-half me-1" aria-hidden="true"></i>{%- include components/recipe-temp.html f=_rs_s.temp_f c=_rs_s.temp_c -%}</span>
69
+ {%- endif -%}
70
+ </p>
71
+ {%- endif -%}
72
+ {%- if _rs_s.image -%}
73
+ <figure class="recipe-step-figure">
74
+ <img src="{{ _rs_s.image | relative_url }}" alt="{{ _rs_s.image_alt | default: '' | escape }}" loading="lazy" decoding="async" class="img-fluid rounded">
75
+ {%- if _rs_s.caption -%}<figcaption class="small text-body-secondary mt-1">{{ _rs_s.caption | escape }}</figcaption>{%- endif -%}
76
+ </figure>
77
+ {%- endif -%}
78
+ {%- if _rs_s.note -%}
79
+ <p class="recipe-step-note small">{{ _rs_s.note | escape }}</p>
80
+ {%- endif -%}
81
+ </li>
82
+ {%- endfor -%}
83
+ </ol>
84
+ {%- endfor -%}
85
+ </section>
86
+ {%- endif -%}
@@ -0,0 +1,45 @@
1
+ {% comment %} Feature: ZER0-084 {% endcomment %}
2
+ {%- comment -%}
3
+ Component: recipe-temp
4
+ Path: _includes/components/recipe-temp.html
5
+ Purpose: A temperature that follows the reader's unit choice — oven
6
+ settings, water temperature, doneness targets. Renders BOTH
7
+ scales by default so the page is complete without JavaScript;
8
+ the scaler then narrows it to whichever system is selected.
9
+ Params: f (number, optional) — degrees Fahrenheit
10
+ c (number, optional) — degrees Celsius
11
+ (give either one; the other is derived)
12
+ label (string, optional, default: "") — prefix, e.g. "Oven"
13
+ class (string, optional, default: "")
14
+ Depends on: assets/js/recipe-scaler.js, _sass/components/_recipe.scss
15
+ Notes: Derived values round to the nearest 5° above 100° — the
16
+ precision an oven dial actually has — and to the nearest degree
17
+ below it. Author both `f:` and `c:` to pin exact values.
18
+ Drop it straight into recipe prose:
19
+ include components/recipe-temp.html f=375
20
+ {%- endcomment -%}
21
+ {%- if include.f == nil and include.c == nil -%}
22
+ {%- comment -%} ERROR: recipe-temp requires `f` or `c` {%- endcomment -%}
23
+ {%- else -%}
24
+ {%- if include.f != nil -%}
25
+ {%- assign _rt_f = include.f | round -%}
26
+ {%- else -%}
27
+ {%- assign _rt_fraw = include.c | times: 9.0 | divided_by: 5.0 | plus: 32 -%}
28
+ {%- if _rt_fraw >= 100 -%}
29
+ {%- assign _rt_f = _rt_fraw | divided_by: 5.0 | round | times: 5 -%}
30
+ {%- else -%}
31
+ {%- assign _rt_f = _rt_fraw | round -%}
32
+ {%- endif -%}
33
+ {%- endif -%}
34
+ {%- if include.c != nil -%}
35
+ {%- assign _rt_c = include.c | round -%}
36
+ {%- else -%}
37
+ {%- assign _rt_craw = include.f | minus: 32 | times: 5.0 | divided_by: 9.0 -%}
38
+ {%- if _rt_craw >= 100 -%}
39
+ {%- assign _rt_c = _rt_craw | divided_by: 5.0 | round | times: 5 -%}
40
+ {%- else -%}
41
+ {%- assign _rt_c = _rt_craw | round -%}
42
+ {%- endif -%}
43
+ {%- endif -%}
44
+ <span class="recipe-temp {{ include.class | default: '' }}" data-recipe-temp data-temp-f="{{ _rt_f }}" data-temp-c="{{ _rt_c }}">{% if include.label %}{{ include.label | escape }} {% endif %}{{ _rt_f }} °F ({{ _rt_c }} °C)</span>
45
+ {%- endif -%}
@@ -45,9 +45,9 @@
45
45
  <div class="modal-dialog modal-dialog-centered">
46
46
  <div class="modal-content">
47
47
  <div class="modal-header">
48
- <h5 class="modal-title" id="siteSearchModalLabel">
48
+ <h2 class="modal-title h5" id="siteSearchModalLabel">
49
49
  <i class="bi bi-search me-2"></i>Search
50
- </h5>
50
+ </h2>
51
51
  <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
52
52
  </div>
53
53
  <div class="modal-body">
@@ -30,6 +30,9 @@
30
30
  <dt class="col-4 col-sm-3 text-end"><kbd>/</kbd></dt>
31
31
  <dd class="col-8 col-sm-9 mb-2">{{ ui.shortcuts_search | default: "Open search" }}</dd>
32
32
 
33
+ <dt class="col-4 col-sm-3 text-end"><kbd>⌘</kbd>/<kbd>Ctrl</kbd>+<kbd>K</kbd></dt>
34
+ <dd class="col-8 col-sm-9 mb-2">{{ ui.shortcuts_search | default: "Open search" }}</dd>
35
+
33
36
  <dt class="col-4 col-sm-3 text-end"><kbd>[</kbd></dt>
34
37
  <dd class="col-8 col-sm-9 mb-2">{{ ui.shortcuts_prev_section | default: "Previous section" }}</dd>
35
38
 
@@ -13,16 +13,24 @@
13
13
  Used by theme settings and theme preview pages.
14
14
  ===================================================================
15
15
  {%- endcomment -%}
16
- {% assign active_skin = site.theme_skin | default: site.data.theme_skins.default | default: "dark" %}
17
- {% assign skin_order = site.data.theme_skins.order %}
16
+ {%- comment -%}
17
+ _data/theme_skins.yml is optional for consumers (remote_theme cannot ship
18
+ _data), so fall back to the compiled skin registry. Keep this list in sync
19
+ with the [data-theme-skin] blocks in _sass/theme/_skins.scss.
20
+ {%- endcomment -%}
21
+ {% assign _skin_fallback = "air|aqua|dirt|neon|mint|plum|sunrise" | split: "|" %}
22
+ {% assign active_skin = site.theme_skin | default: site.data.theme_skins.default | default: "air" %}
23
+ {% assign skin_order = site.data.theme_skins.order | default: _skin_fallback %}
18
24
 
19
25
  <div class="theme-controls-bar d-flex flex-wrap align-items-center gap-3 mb-4 p-3 bg-body-tertiary rounded-3 border">
26
+ {% unless site.color_mode_lock %}
20
27
  <div class="d-flex align-items-center gap-2">
21
28
  <i class="bi bi-moon-stars" aria-hidden="true"></i>
22
29
  <span class="fw-semibold small">Mode:</span>
23
30
  {% include components/halfmoon.html %}
24
31
  </div>
25
32
  <div class="vr d-none d-sm-block" aria-hidden="true"></div>
33
+ {% endunless %}
26
34
  <div class="d-flex align-items-center gap-2 flex-grow-1">
27
35
  <i class="bi bi-palette2" aria-hidden="true"></i>
28
36
  <span class="fw-semibold small">Skin:</span>
@@ -14,8 +14,14 @@
14
14
  ===================================================================
15
15
  -->
16
16
 
17
- {% assign current_skin = site.theme_skin | default: site.data.theme_skins.default | default: "dark" %}
18
- {% assign skins = site.data.theme_skins.order %}
17
+ {%- comment -%}
18
+ _data/theme_skins.yml is optional for consumers (remote_theme cannot ship
19
+ _data), so fall back to the compiled skin registry. Keep this list in sync
20
+ with the [data-theme-skin] blocks in _sass/theme/_skins.scss.
21
+ {%- endcomment -%}
22
+ {% assign _skin_fallback = "air|aqua|dirt|neon|mint|plum|sunrise" | split: "|" %}
23
+ {% assign current_skin = site.theme_skin | default: site.data.theme_skins.default | default: "air" %}
24
+ {% assign skins = site.data.theme_skins.order | default: _skin_fallback %}
19
25
 
20
26
  <p class="text-body-secondary mb-3">
21
27
  Current skin: <strong>{{ current_skin }}</strong>. Click a card to preview (page-level only — does not persist).
@@ -54,7 +54,14 @@
54
54
  <div class="col-6">
55
55
  <div class="p-2 bg-body-tertiary rounded text-center">
56
56
  <small class="text-body-secondary d-block">Last Build</small>
57
- <code class="small">{{ site.time | date: "%b %d, %H:%M" }}</code>
57
+ {%- comment -%}
58
+ data-testid is the stable hook the pixel-snapshot tier masks on
59
+ (test/visual/features/appearance-snapshot.spec.js). This stamp has
60
+ minute resolution, so it becomes an unconditional snapshot flake the
61
+ moment this panel is shown by default rather than inside a collapsed
62
+ offcanvas. Don't remove the attribute without updating that mask.
63
+ {%- endcomment -%}
64
+ <code class="small" data-testid="theme-build-stamp">{{ site.time | date: "%b %d, %H:%M" }}</code>
58
65
  </div>
59
66
  </div>
60
67
 
@@ -9,22 +9,41 @@
9
9
  engines to accurately cite and describe the zer0-mistakes
10
10
  Jekyll theme as a SoftwareApplication entity.
11
11
 
12
+ OPT-IN ONLY — this describes THE THEME, not the site rendering it.
13
+ The payload below is hardcoded to zer0-mistakes: its RubyGems download
14
+ URL, its GitHub repository, its MIT licence, its feature list, its
15
+ mascot screenshot, and a Person node naming the theme's author. Emitted
16
+ from a consumer's homepage it asserts, in machine-readable form, that
17
+ THAT site is a free open-source Ruby gem authored by someone else — e.g.
18
+ a consulting practice's homepage claiming to be a developer tool.
19
+
20
+ It therefore renders only when a site explicitly opts in with
21
+ `jsonld_software_application: true` in _config.yml, which the theme's own
22
+ site does. Consumers get no software schema (their standard SEO metadata
23
+ is unaffected) unless they deliberately turn this on.
24
+
12
25
  Template Logic:
26
+ - Requires site.jsonld_software_application == true
13
27
  - Renders SoftwareApplication schema on the homepage only
14
28
  - Links to a WebPage wrapper via @id references
15
29
  - Pulls version, author, and description from site config
16
30
 
17
31
  Dependencies:
18
- - _config.yml: site.url, site.title, site.description, site.author
32
+ - _config.yml: jsonld_software_application, site.url, site.title,
33
+ site.description, site.author
19
34
  - lib/jekyll-theme-zer0/version.rb (version referenced in config)
20
35
 
21
36
  Usage:
22
37
  - Included automatically in _includes/core/head.html
23
- - Conditional: only renders when page.permalink == "/"
38
+ - Conditional: opted in AND page.permalink == "/"
24
39
  ===================================================================
25
40
  -->
26
41
 
27
- {% if page.permalink == "/" or page.url == "/" %}
42
+ {%- assign _is_homepage = false -%}
43
+ {%- if page.permalink == "/" or page.url == "/" -%}
44
+ {%- assign _is_homepage = true -%}
45
+ {%- endif -%}
46
+ {% if site.jsonld_software_application and _is_homepage %}
28
47
  <script type="application/ld+json">
29
48
  {
30
49
  "@context": "https://schema.org",
@@ -37,17 +37,23 @@
37
37
  {%- assign assets_prefix = site.preview_images.assets_prefix | default: '/assets' -%}
38
38
  {%- assign auto_prefix = site.preview_images.auto_prefix | default: true -%}
39
39
 
40
+ {%- comment -%}
41
+ Guard with truthiness AND blank: in Liquid 4 `nil != blank` is TRUE, so a
42
+ bare `!= blank` check on an unset variable takes the branch and assigns a
43
+ nil seo_image — og:image was silently dropped on every page without a
44
+ page.header image, defeating the site.og_image fallback entirely.
45
+ {%- endcomment -%}
40
46
  {%- if page_teaser_image contains '://' -%}
41
47
  {%- comment -%} External URL - use as-is {%- endcomment -%}
42
- {%- elsif auto_prefix and page_teaser_image != blank -%}
48
+ {%- elsif auto_prefix and page_teaser_image and page_teaser_image != blank -%}
43
49
  {%- unless page_teaser_image contains assets_prefix -%}
44
50
  {%- assign page_teaser_image = assets_prefix | append: page_teaser_image -%}
45
51
  {%- endunless -%}
46
52
  {%- endif -%}
47
53
 
48
- {%- if page_large_image != blank -%}
54
+ {%- if page_large_image and page_large_image != blank -%}
49
55
  {%- assign seo_image = page_large_image | absolute_url | escape -%}
50
- {%- elsif page_teaser_image != blank -%}
56
+ {%- elsif page_teaser_image and page_teaser_image != blank -%}
51
57
  {%- assign seo_image = page_teaser_image | absolute_url | escape -%}
52
58
  {%- endif -%}
53
59
 
@@ -13,9 +13,12 @@
13
13
  when Bootstrap's [data-bs-theme] selectors are first evaluated.
14
14
 
15
15
  Priority order (highest → lowest):
16
- 1. localStorage["theme"] — user's explicit override via Appearance panel
17
- 2. data-color-mode-default — site.color_mode_default config value
18
- 3. "auto" fallback — resolves via prefers-color-scheme
16
+ 1. site.color_mode_lock — when true, config wins unconditionally:
17
+ visitor preference and OS scheme are ignored
18
+ (data-color-mode-lock attribute, set by root.html)
19
+ 2. localStorage["theme"] — user's explicit override via Appearance panel
20
+ 3. data-color-mode-default — site.color_mode_default config value
21
+ 4. "auto" fallback — resolves via prefers-color-scheme
19
22
  {%- endcomment -%}
20
23
  <script>
21
24
  (function () {
@@ -23,8 +26,14 @@
23
26
  try {
24
27
  var root = document.documentElement;
25
28
  var configDefault = root.getAttribute('data-color-mode-default') || 'auto';
26
- var stored = window.localStorage.getItem('theme');
29
+ var locked = root.getAttribute('data-color-mode-lock') === 'true';
30
+ var stored = locked ? null : window.localStorage.getItem('theme');
27
31
  var mode = stored || configDefault;
32
+ if (mode === 'auto' && locked) {
33
+ {%- comment -%} A locked "auto" is contradictory; treat it as dark so
34
+ the brand-pinned mode never flips with the OS. {%- endcomment %}
35
+ mode = 'dark';
36
+ }
28
37
  if (mode === 'auto') {
29
38
  root.setAttribute('data-bs-theme',
30
39
  window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
@@ -0,0 +1,46 @@
1
+ {% comment %} Feature: ZER0-079 {% endcomment %}
2
+ {%- comment -%}
3
+ Component: favicon
4
+ Path: _includes/core/favicon.html
5
+ Purpose: Emit the browser-identity tags (favicon, SVG icon, Apple touch
6
+ icon, web manifest, theme-color) for every page. Before this
7
+ include existed, sites relied on the browser's implicit
8
+ /favicon.ico probe — which silently 404s on sites without the
9
+ file and never works for baseurl (project-page) deployments.
10
+ Params: none — reads the optional `favicon:` block in _config.yml:
11
+
12
+ favicon:
13
+ ico : /favicon.ico # legacy .ico (default)
14
+ svg : /assets/images/favicon.svg # scalable icon (optional)
15
+ png : /assets/images/favicon-32.png # PNG icon (optional)
16
+ png_size : 32x32 # sizes attr for png (optional)
17
+ apple_touch : /assets/images/apple-touch.png # iOS home-screen icon (optional)
18
+ manifest : /site.webmanifest # PWA manifest (optional)
19
+ theme_color : "#0d1117" # browser chrome color (optional)
20
+
21
+ With NO config at all this still emits the explicit
22
+ /favicon.ico link (every consumer site carries one at root),
23
+ and theme_color falls back to site.theme_color.main so the
24
+ mobile address bar matches the site's design tokens.
25
+ Notes: Page-invariant — safe to render via include_cached. All paths
26
+ run through relative_url so baseurl deployments resolve.
27
+ {%- endcomment -%}
28
+ {%- assign _fav = site.favicon -%}
29
+ {%- assign _fav_ico = _fav.ico | default: '/favicon.ico' -%}
30
+ <link rel="icon" href="{{ _fav_ico | relative_url }}" sizes="32x32">
31
+ {%- if _fav.svg %}
32
+ <link rel="icon" type="image/svg+xml" href="{{ _fav.svg | relative_url }}">
33
+ {%- endif %}
34
+ {%- if _fav.png %}
35
+ <link rel="icon" type="image/png" href="{{ _fav.png | relative_url }}"{% if _fav.png_size %} sizes="{{ _fav.png_size }}"{% endif %}>
36
+ {%- endif %}
37
+ {%- if _fav.apple_touch %}
38
+ <link rel="apple-touch-icon" href="{{ _fav.apple_touch | relative_url }}">
39
+ {%- endif %}
40
+ {%- if _fav.manifest %}
41
+ <link rel="manifest" href="{{ _fav.manifest | relative_url }}">
42
+ {%- endif %}
43
+ {%- assign _fav_theme_color = _fav.theme_color | default: site.theme_color.main -%}
44
+ {%- if _fav_theme_color %}
45
+ <meta name="theme-color" content="{{ _fav_theme_color }}">
46
+ {%- endif %}