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,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 -%}
@@ -0,0 +1,96 @@
1
+ {% comment %} Feature: ZER0-084 {% endcomment %}
2
+ {%- comment -%}
3
+ Component: recipe-index
4
+ Path: _includes/components/recipe-index.html
5
+ Purpose: Responsive grid of every recipe, grouped into course sections
6
+ ("Breads & Doughs", "Desserts") — drop it on a cookbook landing
7
+ page, a home page, or anywhere else recipes should be browsable.
8
+ Params: cookbook (string, optional) — a cookbook slug; only recipes
9
+ whose `cookbook:` matches are listed. Omit for every recipe.
10
+ heading (string, optional) — overall section heading (h2)
11
+ heading_level (string, optional, default: "h2")
12
+ grouped (bool, optional, default: true) — false renders one flat
13
+ grid with no course headings
14
+ class (string, optional, default: "")
15
+ Depends on: components/recipe-card.html, the `recipes` collection,
16
+ _data/recipe_courses.yml, _sass/components/_recipe.scss
17
+ Notes: Cookbook landing pages (`layout: cookbook`) are excluded — this
18
+ lists recipes, not covers. Courses render in the order they
19
+ appear in _data/recipe_courses.yml, then any course a recipe
20
+ used but the data file never declared. A recipe with no
21
+ `course:` falls under `uncategorized`. Recipes sort by title
22
+ within a course.
23
+ {%- endcomment -%}
24
+ {%- assign _rx_all = site.recipes | where_exp: "doc", "doc.layout != 'cookbook'" -%}
25
+ {%- if include.cookbook -%}
26
+ {%- assign _rx_all = _rx_all | where: "cookbook", include.cookbook -%}
27
+ {%- endif -%}
28
+ {%- if _rx_all == nil or _rx_all.size == 0 -%}
29
+ {%- comment -%} recipe-index: no recipes found{% if include.cookbook %} for cookbook "{{ include.cookbook }}"{% endif %} — skipping render {%- endcomment -%}
30
+ {%- else -%}
31
+ {%- assign _rx_hl = include.heading_level | default: "h2" -%}
32
+ {%- comment -%}
33
+ Heading levels must not skip. Course headings sit one level below the
34
+ section heading when there is one, and AT the section level when there is
35
+ not — otherwise a cookbook page goes h1 → h3. Card titles always sit one
36
+ level below whatever the course headings ended up being.
37
+ {%- endcomment -%}
38
+ {%- if include.heading -%}
39
+ {%- assign _rx_course_level = _rx_hl | remove: "h" | plus: 1 -%}
40
+ {%- else -%}
41
+ {%- assign _rx_course_level = _rx_hl | remove: "h" | plus: 0 -%}
42
+ {%- endif -%}
43
+ {%- assign _rx_card_level = _rx_course_level | plus: 1 -%}
44
+ <section class="recipe-index {{ include.class | default: '' }}">
45
+ {%- if include.heading -%}
46
+ <{{ _rx_hl }} class="mb-4">{{ include.heading }}</{{ _rx_hl }}>
47
+ {%- endif -%}
48
+
49
+ {%- if include.grouped == false -%}
50
+ <div class="row row-cols-1 row-cols-sm-2 row-cols-lg-3 g-4">
51
+ {%- assign _rx_sorted = _rx_all | sort: "title" -%}
52
+ {%- for _rx_r in _rx_sorted -%}
53
+ <div class="col">{%- include components/recipe-card.html recipe=_rx_r heading_level=_rx_course_level -%}</div>
54
+ {%- endfor -%}
55
+ </div>
56
+ {%- else -%}
57
+ {%- assign _rx_default_course = "uncategorized" -%}
58
+ {%- assign _rx_groups = _rx_all | group_by_exp: "doc", "doc.course | default: _rx_default_course" -%}
59
+
60
+ {%- comment -%} Shelf order: courses declared in _data/recipe_courses.yml first (file order), then any others. {%- endcomment -%}
61
+ {%- assign _rx_order = "," -%}
62
+ {%- for _rx_pair in site.data.recipe_courses -%}
63
+ {%- assign _rx_needle = _rx_pair[0] | prepend: "," | append: "," -%}
64
+ {%- unless _rx_order contains _rx_needle -%}
65
+ {%- for _rx_g in _rx_groups -%}{%- if _rx_g.name == _rx_pair[0] -%}{%- assign _rx_order = _rx_order | append: _rx_pair[0] | append: "," -%}{%- endif -%}{%- endfor -%}
66
+ {%- endunless -%}
67
+ {%- endfor -%}
68
+ {%- for _rx_g in _rx_groups -%}
69
+ {%- assign _rx_needle = _rx_g.name | prepend: "," | append: "," -%}
70
+ {%- unless _rx_order contains _rx_needle -%}{%- assign _rx_order = _rx_order | append: _rx_g.name | append: "," -%}{%- endunless -%}
71
+ {%- endfor -%}
72
+ {%- assign _rx_order_list = _rx_order | split: "," -%}
73
+
74
+ {%- for _rx_name in _rx_order_list -%}
75
+ {%- if _rx_name == "" -%}{%- continue -%}{%- endif -%}
76
+ {%- assign _rx_group = _rx_groups | where: "name", _rx_name | first -%}
77
+ {%- if _rx_group == nil -%}{%- continue -%}{%- endif -%}
78
+ {%- assign _rx_meta = site.data.recipe_courses[_rx_name] -%}
79
+ {%- assign _rx_recipes = _rx_group.items | sort: "title" -%}
80
+ <div class="recipe-course mb-5" id="course-{{ _rx_name }}">
81
+ <h{{ _rx_course_level }} class="h4 mb-1">
82
+ <i class="bi {{ _rx_meta.icon | default: 'bi-journal-bookmark' }} me-2" aria-hidden="true"></i>{{ _rx_meta.title | default: _rx_name | escape }}
83
+ </h{{ _rx_course_level }}>
84
+ {%- if _rx_meta.tagline -%}
85
+ <p class="text-body-secondary mb-3">{{ _rx_meta.tagline | escape }}</p>
86
+ {%- endif -%}
87
+ <div class="row row-cols-1 row-cols-sm-2 row-cols-lg-3 g-4">
88
+ {%- for _rx_r in _rx_recipes -%}
89
+ <div class="col">{%- include components/recipe-card.html recipe=_rx_r heading_level=_rx_card_level -%}</div>
90
+ {%- endfor -%}
91
+ </div>
92
+ </div>
93
+ {%- endfor -%}
94
+ {%- endif -%}
95
+ </section>
96
+ {%- endif -%}
@@ -0,0 +1,90 @@
1
+ {% comment %} Feature: ZER0-084 {% endcomment %}
2
+ {%- comment -%}
3
+ Component: recipe-ingredients
4
+ Path: _includes/components/recipe-ingredients.html
5
+ Purpose: The ingredient list for one recipe — optionally grouped into
6
+ sub-lists ("Dough", "Topping"), each amount scalable and
7
+ convertible, each line check-off-able as you cook.
8
+ Params: ingredients (array, optional, default: page.ingredients)
9
+ heading (string, optional, default: "Ingredients")
10
+ heading_level (string, optional, default: "h2")
11
+ id_prefix (string, optional, default: page.slug) — checkbox ids
12
+ checklist (bool, optional, default: true) — false renders a
13
+ plain list with no checkboxes
14
+ Depends on: components/recipe-qty.html, _data/ingredient_densities.yml,
15
+ assets/js/recipe-scaler.js, _sass/components/_recipe.scss
16
+ Notes: Accepts a flat list of items or a list of {group, items} groups,
17
+ and tolerates the two mixed. Densities resolve as
18
+ item.grams_per_cup → _data/ingredient_densities[slug] → none;
19
+ without one, a volume can never be converted to a weight.
20
+ Each line carries itemprop="recipeIngredient" for schema.org.
21
+ Author `singular:` alongside a countable `item:` ("large eggs"
22
+ + "large egg") and the scaler swaps the wording once the amount
23
+ drops to one or below, so halving a batch does not print
24
+ "1 large eggs".
25
+ {%- endcomment -%}
26
+ {%- assign _ri_ingredients = include.ingredients | default: page.ingredients -%}
27
+ {%- if _ri_ingredients == nil or _ri_ingredients.size == 0 -%}
28
+ {%- comment -%} recipe-ingredients: no `ingredients` front matter — skipping render {%- endcomment -%}
29
+ {%- else -%}
30
+ {%- assign _ri_hl = include.heading_level | default: "h2" -%}
31
+ {%- assign _ri_prefix = include.id_prefix | default: page.slug | default: "recipe" -%}
32
+ {%- assign _ri_checklist = true -%}
33
+ {%- if include.checklist == false -%}{%- assign _ri_checklist = false -%}{%- endif -%}
34
+ {%- assign _ri_n = 0 -%}
35
+ <section class="recipe-ingredients" aria-labelledby="{{ _ri_prefix }}-ingredients-heading">
36
+ <{{ _ri_hl }} id="{{ _ri_prefix }}-ingredients-heading" class="recipe-section-heading">
37
+ <i class="bi bi-basket3 me-2" aria-hidden="true"></i>{{ include.heading | default: "Ingredients" }}
38
+ </{{ _ri_hl }}>
39
+
40
+ {%- for _ri_entry in _ri_ingredients -%}
41
+ {%- if _ri_entry.items -%}
42
+ {%- assign _ri_group_name = _ri_entry.group | default: _ri_entry.title -%}
43
+ {%- assign _ri_loop_items = _ri_entry.items -%}
44
+ {%- else -%}
45
+ {%- assign _ri_group_name = nil -%}
46
+ {%- assign _ri_loop_items = "" | split: "" | push: _ri_entry -%}
47
+ {%- endif -%}
48
+ {%- assign _ri_sub = _ri_hl | remove: "h" | plus: 1 -%}
49
+
50
+ {%- if _ri_group_name -%}
51
+ <h{{ _ri_sub }} class="recipe-ingredient-group-title">{{ _ri_group_name | escape }}</h{{ _ri_sub }}>
52
+ {%- endif -%}
53
+
54
+ <ul class="recipe-ingredient-list list-unstyled">
55
+ {%- for _ri_i in _ri_loop_items -%}
56
+ {%- assign _ri_n = _ri_n | plus: 1 -%}
57
+ {%- assign _ri_id = _ri_prefix | append: "-ing-" | append: _ri_n -%}
58
+ {%- assign _ri_slug = _ri_i.item | default: "" | downcase | strip | replace: " ", "-" -%}
59
+ {%- assign _ri_gpc = _ri_i.grams_per_cup | default: site.data.ingredient_densities[_ri_slug] -%}
60
+ <li class="recipe-ingredient{% if _ri_i.optional %} recipe-ingredient--optional{% endif %}">
61
+ <div class="form-check">
62
+ {%- if _ri_checklist -%}
63
+ <input class="form-check-input recipe-ingredient-check" type="checkbox" id="{{ _ri_id }}" data-recipe-check>
64
+ {%- endif -%}
65
+ <label class="form-check-label" {% if _ri_checklist %}for="{{ _ri_id }}"{% endif %} itemprop="recipeIngredient">
66
+ {%- comment -%}
67
+ The space before `endif` is load-bearing: it separates the amount
68
+ from the ingredient name in the accessible text and in the
69
+ recipeIngredient microdata, not just visually. Neither the
70
+ include nor the closing tag may trim it, so both are written
71
+ without the whitespace-stripping dash on that side.
72
+ {%- endcomment -%}
73
+ {%- if _ri_i.qty -%}
74
+ {%- include components/recipe-qty.html qty=_ri_i.qty qty_max=_ri_i.qty_max unit=_ri_i.unit grams_per_cup=_ri_gpc weigh=_ri_i.weigh scale=_ri_i.scale class="recipe-ingredient-amount" %} {% endif -%}
75
+ <span class="recipe-ingredient-name"{% if _ri_i.singular %} data-recipe-name data-singular="{{ _ri_i.singular | escape }}" data-plural="{{ _ri_i.item | escape }}"{% endif %}>
76
+ {%- if _ri_i.link -%}<a href="{{ _ri_i.link }}">{{ _ri_i.item | escape }}</a>{%- else -%}{{ _ri_i.item | escape }}{%- endif -%}
77
+ </span>
78
+ {%- if _ri_i.prep %}<span class="recipe-ingredient-prep">, {{ _ri_i.prep | escape }}</span>{% endif -%}
79
+ {%- if _ri_i.optional %} <span class="badge rounded-pill text-bg-secondary recipe-ingredient-badge">optional</span>{% endif -%}
80
+ </label>
81
+ </div>
82
+ {%- if _ri_i.note -%}
83
+ <p class="recipe-ingredient-note small text-body-secondary mb-0">{{ _ri_i.note | escape }}</p>
84
+ {%- endif -%}
85
+ </li>
86
+ {%- endfor -%}
87
+ </ul>
88
+ {%- endfor -%}
89
+ </section>
90
+ {%- endif -%}
@@ -0,0 +1,96 @@
1
+ {% comment %} Feature: ZER0-084 {% endcomment %}
2
+ {%- comment -%}
3
+ Component: recipe-meta
4
+ Path: _includes/components/recipe-meta.html
5
+ Purpose: The fact bar for one recipe — prep/cook/rest/total time, what it
6
+ makes, difficulty, cuisine, and oven temperature.
7
+ Params: recipe (doc, optional, default: page) — any recipes-collection
8
+ document; pass one to render the bar for a different recipe
9
+ class (string, optional, default: "")
10
+ Depends on: components/recipe-duration.html, components/recipe-qty.html,
11
+ components/recipe-temp.html, _sass/components/_recipe.scss
12
+ Notes: Total time is `times.total` when authored, otherwise the sum of
13
+ prep + cook + rest. The "Makes" tile is a scalable quantity, so
14
+ it tracks the serving scaler rather than going stale. Every tile
15
+ is skipped when its front matter is absent — a recipe with only
16
+ a cook time renders one tile, not five empty ones.
17
+ {%- endcomment -%}
18
+ {%- assign _rm_r = include.recipe | default: page -%}
19
+ {%- assign _rm_t = _rm_r.times -%}
20
+ {%- assign _rm_prep = _rm_t.prep | default: 0 -%}
21
+ {%- assign _rm_cook = _rm_t.cook | default: 0 -%}
22
+ {%- assign _rm_rest = _rm_t.rest | default: 0 -%}
23
+ {%- if _rm_t.total -%}
24
+ {%- assign _rm_total = _rm_t.total -%}
25
+ {%- else -%}
26
+ {%- assign _rm_total = _rm_prep | plus: _rm_cook | plus: _rm_rest -%}
27
+ {%- endif -%}
28
+ {%- capture _rm_prep_h -%}{%- include components/recipe-duration.html minutes=_rm_prep -%}{%- endcapture -%}
29
+ {%- capture _rm_cook_h -%}{%- include components/recipe-duration.html minutes=_rm_cook -%}{%- endcapture -%}
30
+ {%- capture _rm_rest_h -%}{%- include components/recipe-duration.html minutes=_rm_rest -%}{%- endcapture -%}
31
+ {%- capture _rm_total_h -%}{%- include components/recipe-duration.html minutes=_rm_total -%}{%- endcapture -%}
32
+ {%- assign _rm_prep_h = _rm_prep_h | strip -%}
33
+ {%- assign _rm_cook_h = _rm_cook_h | strip -%}
34
+ {%- assign _rm_rest_h = _rm_rest_h | strip -%}
35
+ {%- assign _rm_total_h = _rm_total_h | strip -%}
36
+ {%- assign _rm_has_any = false -%}
37
+ {%- if _rm_prep_h != "" or _rm_cook_h != "" or _rm_total_h != "" or _rm_r.yield or _rm_r.difficulty or _rm_r.cuisine or _rm_r.oven -%}
38
+ {%- assign _rm_has_any = true -%}
39
+ {%- endif -%}
40
+ {%- if _rm_has_any == false -%}
41
+ {%- comment -%} recipe-meta: no time/yield/difficulty front matter — skipping render {%- endcomment -%}
42
+ {%- else -%}
43
+ <dl class="recipe-meta {{ include.class | default: '' }}">
44
+ {%- if _rm_prep_h != "" -%}
45
+ <div class="recipe-meta-item">
46
+ <dt><i class="bi bi-scissors me-1" aria-hidden="true"></i>Prep</dt>
47
+ <dd itemprop="prepTime">{{ _rm_prep_h }}</dd>
48
+ </div>
49
+ {%- endif -%}
50
+ {%- if _rm_cook_h != "" -%}
51
+ <div class="recipe-meta-item">
52
+ <dt><i class="bi bi-fire me-1" aria-hidden="true"></i>Cook</dt>
53
+ <dd itemprop="cookTime">{{ _rm_cook_h }}</dd>
54
+ </div>
55
+ {%- endif -%}
56
+ {%- if _rm_rest_h != "" -%}
57
+ <div class="recipe-meta-item">
58
+ <dt><i class="bi bi-moon-stars me-1" aria-hidden="true"></i>{{ _rm_r.times.rest_label | default: "Rest" }}</dt>
59
+ <dd>{{ _rm_rest_h }}</dd>
60
+ </div>
61
+ {%- endif -%}
62
+ {%- if _rm_total_h != "" -%}
63
+ <div class="recipe-meta-item recipe-meta-item--total">
64
+ <dt><i class="bi bi-clock-history me-1" aria-hidden="true"></i>Total</dt>
65
+ <dd itemprop="totalTime">{{ _rm_total_h }}</dd>
66
+ </div>
67
+ {%- endif -%}
68
+ {%- if _rm_r.yield and _rm_r.yield.amount -%}
69
+ <div class="recipe-meta-item">
70
+ <dt><i class="bi bi-pie-chart me-1" aria-hidden="true"></i>Makes</dt>
71
+ <dd itemprop="recipeYield">{%- include components/recipe-qty.html qty=_rm_r.yield.amount unit=_rm_r.yield.unit -%}</dd>
72
+ </div>
73
+ {%- endif -%}
74
+ {%- if _rm_r.difficulty -%}
75
+ <div class="recipe-meta-item">
76
+ <dt><i class="bi bi-bar-chart me-1" aria-hidden="true"></i>Difficulty</dt>
77
+ <dd><span class="recipe-difficulty recipe-difficulty--{{ _rm_r.difficulty | downcase | strip }}">{{ _rm_r.difficulty | capitalize }}</span></dd>
78
+ </div>
79
+ {%- endif -%}
80
+ {%- if _rm_r.cuisine -%}
81
+ <div class="recipe-meta-item">
82
+ <dt><i class="bi bi-globe2 me-1" aria-hidden="true"></i>Cuisine</dt>
83
+ <dd itemprop="recipeCuisine">{{ _rm_r.cuisine | escape }}</dd>
84
+ </div>
85
+ {%- endif -%}
86
+ {%- if _rm_r.oven.temp_f or _rm_r.oven.temp_c -%}
87
+ <div class="recipe-meta-item">
88
+ <dt><i class="bi bi-thermometer-half me-1" aria-hidden="true"></i>Oven</dt>
89
+ <dd>
90
+ {%- include components/recipe-temp.html f=_rm_r.oven.temp_f c=_rm_r.oven.temp_c -%}
91
+ {%- if _rm_r.oven.mode %} <span class="text-body-secondary">({{ _rm_r.oven.mode | escape }})</span>{% endif -%}
92
+ </dd>
93
+ </div>
94
+ {%- endif -%}
95
+ </dl>
96
+ {%- endif -%}
@@ -0,0 +1,57 @@
1
+ {% comment %} Feature: ZER0-084 {% endcomment %}
2
+ {%- comment -%}
3
+ Component: recipe-nutrition
4
+ Path: _includes/components/recipe-nutrition.html
5
+ Purpose: Per-serving nutrition panel, marked up as schema.org
6
+ NutritionInformation.
7
+ Params: recipe (doc, optional, default: page)
8
+ heading (string, optional, default: "Nutrition")
9
+ heading_level (string, optional, default: "h2")
10
+ Depends on: _sass/components/_recipe.scss
11
+ Notes: Always shown PER SERVING, and deliberately NOT scaled — a
12
+ serving is a serving however many you make. Author
13
+ `nutrition.basis: total` and the figures are divided by
14
+ `yield.amount` at build time. Values are whatever the author
15
+ entered; the component adds an estimate disclaimer rather than
16
+ implying a lab analysis.
17
+ {%- endcomment -%}
18
+ {%- assign _rn_r = include.recipe | default: page -%}
19
+ {%- assign _rn_n = _rn_r.nutrition -%}
20
+ {%- if _rn_n == nil or _rn_n.calories == nil -%}
21
+ {%- comment -%} recipe-nutrition: no `nutrition` front matter — skipping render {%- endcomment -%}
22
+ {%- else -%}
23
+ {%- assign _rn_hl = include.heading_level | default: "h2" -%}
24
+ {%- assign _rn_divisor = 1 -%}
25
+ {%- if _rn_n.basis == "total" and _rn_r.yield.amount and _rn_r.yield.amount > 0 -%}
26
+ {%- assign _rn_divisor = _rn_r.yield.amount -%}
27
+ {%- endif -%}
28
+ {%- comment -%} Force float division so 2410 / 12 is not floored to 200. {%- endcomment -%}
29
+ {%- assign _rn_divisor_f = _rn_divisor | times: 1.0 -%}
30
+ {%- assign _rn_rows = "calories,Calories,kcal|protein,Protein,g|carbohydrates,Carbohydrates,g|sugar,Sugars,g|fiber,Fibre,g|fat,Fat,g|saturated_fat,Saturated fat,g|sodium,Sodium,mg|cholesterol,Cholesterol,mg" | split: "|" -%}
31
+ {%- assign _rn_props = "calories,proteinContent,carbohydrateContent,sugarContent,fiberContent,fatContent,saturatedFatContent,sodiumContent,cholesterolContent" | split: "," -%}
32
+ <section class="recipe-nutrition" itemprop="nutrition" itemscope itemtype="https://schema.org/NutritionInformation">
33
+ <{{ _rn_hl }} class="recipe-section-heading">
34
+ <i class="bi bi-heart-pulse me-2" aria-hidden="true"></i>{{ include.heading | default: "Nutrition" }}
35
+ </{{ _rn_hl }}>
36
+ <p class="recipe-nutrition-basis text-body-secondary small">
37
+ Per {{ _rn_r.yield.singular | default: "serving" | escape }}, estimated{% if _rn_divisor != 1 %} from the batch total{% endif %}.
38
+ </p>
39
+ <dl class="recipe-nutrition-grid">
40
+ {%- for _rn_row in _rn_rows -%}
41
+ {%- assign _rn_parts = _rn_row | split: "," -%}
42
+ {%- assign _rn_key = _rn_parts[0] -%}
43
+ {%- assign _rn_value = _rn_n[_rn_key] -%}
44
+ {%- if _rn_value -%}
45
+ {%- assign _rn_shown = _rn_value | divided_by: _rn_divisor_f -%}
46
+ {%- assign _rn_shown_i = _rn_shown | round -%}
47
+ <div class="recipe-nutrition-item">
48
+ <dt>{{ _rn_parts[1] }}</dt>
49
+ <dd itemprop="{{ _rn_props[forloop.index0] }}">
50
+ {%- if _rn_shown == _rn_shown_i -%}{{ _rn_shown_i }}{%- else -%}{{ _rn_shown | round: 1 }}{%- endif %} {{ _rn_parts[2] }}
51
+ </dd>
52
+ </div>
53
+ {%- endif -%}
54
+ {%- endfor -%}
55
+ </dl>
56
+ </section>
57
+ {%- endif -%}
@@ -0,0 +1,73 @@
1
+ {% comment %} Feature: ZER0-084 {% endcomment %}
2
+ {%- comment -%}
3
+ Component: recipe-qty
4
+ Path: _includes/components/recipe-qty.html
5
+ Purpose: One scalable, convertible quantity — the atom every other recipe
6
+ component is built from. Renders the amount exactly as authored
7
+ (so the page is complete and correct with JavaScript disabled)
8
+ plus the data attributes assets/js/recipe-scaler.js needs to
9
+ rescale and convert it in place.
10
+ Params: qty (number, required) — base amount for 1× yield
11
+ qty_max (number, optional) — upper bound; renders "2–3"
12
+ unit (string, optional, default: "") — as authored: g, cups, …
13
+ grams_per_cup (number, optional) — density for volume↔weight
14
+ weigh (bool, optional, default: true) — false opts this
15
+ quantity out of volume→weight
16
+ scale (bool, optional, default: true) — false pins the amount
17
+ (per-serving nutrition, pan
18
+ sizes, "a pinch")
19
+ class (string, optional, default: "") — extra wrapper classes
20
+ Depends on: assets/js/recipe-scaler.js, _sass/components/_recipe.scss
21
+ Notes: Cup, spoon and countable amounts render with vulgar fractions
22
+ (0.5 → ½) so recipes read like recipes; unrecognised decimals
23
+ fall back to two-decimal rounding. Metric units stay decimal —
24
+ a scale shows 40.5 g, not 40½ g. The unit is echoed verbatim —
25
+ the scaler only rewrites it when the reader converts units.
26
+ {%- endcomment -%}
27
+ {%- if include.qty == nil or include.qty == "" -%}
28
+ {%- comment -%} ERROR: recipe-qty requires `qty` {%- endcomment -%}
29
+ {%- else -%}
30
+ {%- assign _rq_unit = include.unit | default: "" | strip -%}
31
+ {%- assign _rq_values = "" | split: "" | push: include.qty -%}
32
+ {%- comment -%}
33
+ Metric units are read off a scale, so they stay decimal: "40.5 g", never
34
+ "40½ g". Cups, spoons and countable things read as fractions.
35
+ {%- endcomment -%}
36
+ {%- assign _rq_metric = false -%}
37
+ {%- assign _rq_u = _rq_unit | downcase | strip | replace: ".", "" -%}
38
+ {%- case _rq_u -%}
39
+ {%- when "g" or "gr" or "gram" or "grams" or "gramme" or "grammes" -%}{%- assign _rq_metric = true -%}
40
+ {%- when "kg" or "kilo" or "kilos" or "kilogram" or "kilograms" -%}{%- assign _rq_metric = true -%}
41
+ {%- when "mg" or "milligram" or "milligrams" -%}{%- assign _rq_metric = true -%}
42
+ {%- when "ml" or "cc" or "milliliter" or "milliliters" or "millilitre" or "millilitres" -%}{%- assign _rq_metric = true -%}
43
+ {%- when "dl" or "deciliter" or "deciliters" -%}{%- assign _rq_metric = true -%}
44
+ {%- when "l" or "liter" or "liters" or "litre" or "litres" -%}{%- assign _rq_metric = true -%}
45
+ {%- endcase -%}
46
+ {%- if include.qty_max and include.qty_max > include.qty -%}
47
+ {%- assign _rq_values = _rq_values | push: include.qty_max -%}
48
+ {%- endif -%}
49
+ {%- capture _rq_text -%}
50
+ {%- for _rq_v in _rq_values -%}
51
+ {%- if forloop.index > 1 -%}–{%- endif -%}
52
+ {%- if _rq_metric -%}{{ _rq_v | round: 2 }}{%- continue -%}{%- endif -%}
53
+ {%- assign _rq_w = _rq_v | floor -%}
54
+ {%- assign _rq_f = _rq_v | minus: _rq_w | times: 1000 | round -%}
55
+ {%- case _rq_f -%}
56
+ {%- when 0 -%}{{ _rq_w }}
57
+ {%- when 125 -%}{% if _rq_w > 0 %}{{ _rq_w }}{% endif %}⅛
58
+ {%- when 167 or 166 -%}{% if _rq_w > 0 %}{{ _rq_w }}{% endif %}⅙
59
+ {%- when 200 -%}{% if _rq_w > 0 %}{{ _rq_w }}{% endif %}⅕
60
+ {%- when 250 -%}{% if _rq_w > 0 %}{{ _rq_w }}{% endif %}¼
61
+ {%- when 333 or 334 -%}{% if _rq_w > 0 %}{{ _rq_w }}{% endif %}⅓
62
+ {%- when 375 -%}{% if _rq_w > 0 %}{{ _rq_w }}{% endif %}⅜
63
+ {%- when 500 -%}{% if _rq_w > 0 %}{{ _rq_w }}{% endif %}½
64
+ {%- when 625 -%}{% if _rq_w > 0 %}{{ _rq_w }}{% endif %}⅝
65
+ {%- when 667 or 666 -%}{% if _rq_w > 0 %}{{ _rq_w }}{% endif %}⅔
66
+ {%- when 750 -%}{% if _rq_w > 0 %}{{ _rq_w }}{% endif %}¾
67
+ {%- when 875 -%}{% if _rq_w > 0 %}{{ _rq_w }}{% endif %}⅞
68
+ {%- else -%}{{ _rq_v | round: 2 }}
69
+ {%- endcase -%}
70
+ {%- endfor -%}
71
+ {%- endcapture -%}
72
+ <span class="recipe-qty {{ include.class | default: '' }}" data-recipe-qty data-qty="{{ include.qty }}"{% if include.qty_max %} data-qty-max="{{ include.qty_max }}"{% endif %} data-unit="{{ _rq_unit | escape }}"{% if include.grams_per_cup %} data-grams-per-cup="{{ include.grams_per_cup }}"{% endif %}{% if include.weigh == false %} data-weigh="false"{% endif %}{% if include.scale == false %} data-scale="false"{% endif %}>{{ _rq_text | strip }}{% if _rq_unit != "" %} {{ _rq_unit | escape }}{% endif %}</span>
73
+ {%- endif -%}