jekyll-theme-zer0 1.28.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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +430 -13
  3. data/_data/backlog.yml +356 -2
  4. data/_data/consumers.yml +109 -4
  5. data/_data/features.yml +157 -0
  6. data/_data/i18n/fr.yml +12 -7
  7. data/_data/i18n/manifest.yml +39 -12
  8. data/_data/ingredient_densities.yml +122 -0
  9. data/_data/navigation/main.yml +16 -0
  10. data/_data/recipe_courses.yml +64 -0
  11. data/_data/theme-manifest.yml +68 -40
  12. data/_data/ui-text.yml +8 -0
  13. data/_includes/README.md +15 -1
  14. data/_includes/components/background-customizer.html +2 -2
  15. data/_includes/components/cookie-consent.html +4 -4
  16. data/_includes/components/info-section.html +8 -3
  17. data/_includes/components/language-toggle.html +168 -21
  18. data/_includes/components/page-views-init.html +55 -0
  19. data/_includes/components/page-views.html +33 -0
  20. data/_includes/components/recipe-card.html +67 -0
  21. data/_includes/components/recipe-duration.html +50 -0
  22. data/_includes/components/recipe-grams.html +58 -0
  23. data/_includes/components/recipe-index.html +96 -0
  24. data/_includes/components/recipe-ingredients.html +90 -0
  25. data/_includes/components/recipe-meta.html +96 -0
  26. data/_includes/components/recipe-nutrition.html +57 -0
  27. data/_includes/components/recipe-qty.html +73 -0
  28. data/_includes/components/recipe-ratio.html +151 -0
  29. data/_includes/components/recipe-scaler.html +73 -0
  30. data/_includes/components/recipe-steps.html +86 -0
  31. data/_includes/components/recipe-temp.html +45 -0
  32. data/_includes/components/search-modal.html +2 -2
  33. data/_includes/components/shortcuts-modal.html +3 -0
  34. data/_includes/components/theme-info.html +8 -1
  35. data/_includes/content/jsonld-software.html +22 -3
  36. data/_includes/core/footer.html +18 -16
  37. data/_includes/core/head.html +12 -0
  38. data/_includes/core/header.html +7 -4
  39. data/_includes/navigation/section-sidebar.html +93 -11
  40. data/_includes/navigation/sidebar-left.html +2 -1
  41. data/_includes/navigation/sidebar-right.html +2 -1
  42. data/_includes/setup/wizard.html +173 -86
  43. data/_includes/stats/stats-metrics.html +2 -2
  44. data/_layouts/README.md +2 -0
  45. data/_layouts/admin.html +2 -2
  46. data/_layouts/article.html +5 -0
  47. data/_layouts/cookbook.html +88 -0
  48. data/_layouts/default.html +4 -4
  49. data/_layouts/recipe.html +274 -0
  50. data/_layouts/root.html +22 -3
  51. data/_layouts/section.html +39 -10
  52. data/_sass/components/_callout.scss +1 -1
  53. data/_sass/components/_footer.scss +37 -1
  54. data/_sass/components/_page-views.scss +36 -0
  55. data/_sass/components/_recipe.scss +506 -0
  56. data/_sass/components/_setup-wizard.scss +235 -0
  57. data/_sass/components/_ui-enhancements.scss +6 -6
  58. data/_sass/core/_navbar.scss +250 -15
  59. data/_sass/layouts/_landing.scss +2 -2
  60. data/_sass/tokens/_color.scss +6 -0
  61. data/_sass/tokens/_index.scss +2 -0
  62. data/_sass/tokens/_radius.scss +21 -0
  63. data/_sass/tokens/_typography.scss +4 -0
  64. data/_sass/utilities/_focus.scss +14 -0
  65. data/assets/css/main.scss +3 -0
  66. data/assets/js/modules/navigation/navbar.js +55 -0
  67. data/assets/js/page-views.js +372 -0
  68. data/assets/js/recipe-scaler.js +501 -0
  69. data/assets/js/search-modal.js +10 -0
  70. data/assets/js/setup-wizard.js +322 -20
  71. data/scripts/README.md +29 -0
  72. data/scripts/bin/audit-consumer +39 -7
  73. data/scripts/bin/giscus-discussions +213 -14
  74. data/scripts/bin/manifest +35 -12
  75. data/scripts/ci/agent_review_result.py +164 -0
  76. data/scripts/ci/test_agent_review_result.py +172 -0
  77. data/scripts/design-system-check.rb +170 -0
  78. data/scripts/lib/audit.sh +42 -2
  79. data/scripts/lint-liquid-raw.rb +137 -0
  80. data/scripts/test/lib/run_tests.sh +2 -1
  81. data/scripts/test/lib/test_agent_review_result.sh +27 -0
  82. data/scripts/translate.rb +71 -15
  83. metadata +31 -2
@@ -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 -%}
@@ -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 -%}