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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +430 -13
- data/_data/backlog.yml +356 -2
- data/_data/consumers.yml +109 -4
- data/_data/features.yml +157 -0
- data/_data/i18n/fr.yml +12 -7
- data/_data/i18n/manifest.yml +39 -12
- data/_data/ingredient_densities.yml +122 -0
- data/_data/navigation/main.yml +16 -0
- data/_data/recipe_courses.yml +64 -0
- data/_data/theme-manifest.yml +68 -40
- data/_data/ui-text.yml +8 -0
- data/_includes/README.md +15 -1
- data/_includes/components/background-customizer.html +2 -2
- data/_includes/components/cookie-consent.html +4 -4
- data/_includes/components/info-section.html +8 -3
- data/_includes/components/language-toggle.html +168 -21
- data/_includes/components/page-views-init.html +55 -0
- data/_includes/components/page-views.html +33 -0
- data/_includes/components/recipe-card.html +67 -0
- data/_includes/components/recipe-duration.html +50 -0
- data/_includes/components/recipe-grams.html +58 -0
- data/_includes/components/recipe-index.html +96 -0
- data/_includes/components/recipe-ingredients.html +90 -0
- data/_includes/components/recipe-meta.html +96 -0
- data/_includes/components/recipe-nutrition.html +57 -0
- data/_includes/components/recipe-qty.html +73 -0
- data/_includes/components/recipe-ratio.html +151 -0
- data/_includes/components/recipe-scaler.html +73 -0
- data/_includes/components/recipe-steps.html +86 -0
- data/_includes/components/recipe-temp.html +45 -0
- data/_includes/components/search-modal.html +2 -2
- data/_includes/components/shortcuts-modal.html +3 -0
- data/_includes/components/theme-info.html +8 -1
- data/_includes/content/jsonld-software.html +22 -3
- data/_includes/core/footer.html +18 -16
- data/_includes/core/head.html +12 -0
- data/_includes/core/header.html +7 -4
- data/_includes/navigation/section-sidebar.html +93 -11
- data/_includes/navigation/sidebar-left.html +2 -1
- data/_includes/navigation/sidebar-right.html +2 -1
- data/_includes/setup/wizard.html +173 -86
- data/_includes/stats/stats-metrics.html +2 -2
- data/_layouts/README.md +2 -0
- data/_layouts/admin.html +2 -2
- data/_layouts/article.html +5 -0
- data/_layouts/cookbook.html +88 -0
- data/_layouts/default.html +4 -4
- data/_layouts/recipe.html +274 -0
- data/_layouts/root.html +22 -3
- data/_layouts/section.html +39 -10
- data/_sass/components/_callout.scss +1 -1
- data/_sass/components/_footer.scss +37 -1
- data/_sass/components/_page-views.scss +36 -0
- data/_sass/components/_recipe.scss +506 -0
- data/_sass/components/_setup-wizard.scss +235 -0
- data/_sass/components/_ui-enhancements.scss +6 -6
- data/_sass/core/_navbar.scss +250 -15
- data/_sass/layouts/_landing.scss +2 -2
- data/_sass/tokens/_color.scss +6 -0
- data/_sass/tokens/_index.scss +2 -0
- data/_sass/tokens/_radius.scss +21 -0
- data/_sass/tokens/_typography.scss +4 -0
- data/_sass/utilities/_focus.scss +14 -0
- data/assets/css/main.scss +3 -0
- data/assets/js/modules/navigation/navbar.js +55 -0
- data/assets/js/page-views.js +372 -0
- data/assets/js/recipe-scaler.js +501 -0
- data/assets/js/search-modal.js +10 -0
- data/assets/js/setup-wizard.js +322 -20
- data/scripts/README.md +29 -0
- data/scripts/bin/audit-consumer +39 -7
- data/scripts/bin/giscus-discussions +213 -14
- data/scripts/bin/manifest +35 -12
- data/scripts/ci/agent_review_result.py +164 -0
- data/scripts/ci/test_agent_review_result.py +172 -0
- data/scripts/design-system-check.rb +170 -0
- data/scripts/lib/audit.sh +42 -2
- data/scripts/lint-liquid-raw.rb +137 -0
- data/scripts/test/lib/run_tests.sh +2 -1
- data/scripts/test/lib/test_agent_review_result.sh +27 -0
- data/scripts/translate.rb +71 -15
- metadata +31 -2
|
@@ -2,24 +2,61 @@
|
|
|
2
2
|
{%- comment -%}
|
|
3
3
|
Component: language-toggle
|
|
4
4
|
Path: _includes/components/language-toggle.html
|
|
5
|
-
Purpose:
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
Purpose: Switches between the English original and its machine-generated
|
|
6
|
+
translations (scripts/translate.rb). Shows the source language
|
|
7
|
+
plus every configured target language. Every entry is a live
|
|
8
|
+
link: a target without a generated translation for the current
|
|
9
|
+
page falls back to the source page and is explained by one
|
|
10
|
+
footnote (it used to render as a disabled entry — see T-038).
|
|
11
|
+
Params: variant (string, optional, default "navbar")
|
|
12
|
+
- "navbar" — compact dropdown for the header utility cluster
|
|
13
|
+
- "panel" — self-contained "Language" section for the
|
|
14
|
+
Settings offcanvas (components/info-section.html),
|
|
15
|
+
rendered as an always-visible list of choices
|
|
16
|
+
with its own <h6> heading
|
|
17
|
+
Reads: page.lang / page.url / page.translation_source_url and the
|
|
18
|
+
generated _data/i18n/manifest.yml.
|
|
12
19
|
Depends on: site.translation.* config, site.data.i18n.manifest,
|
|
13
20
|
_data/i18n/languages.yml (display names), core/i18n.html,
|
|
14
|
-
Bootstrap 5 dropdown, Bootstrap Icons
|
|
21
|
+
Bootstrap 5 dropdown (navbar variant only), Bootstrap Icons
|
|
22
|
+
(bi-translate)
|
|
15
23
|
Notes: Renders nothing when translation is disabled or no target
|
|
16
24
|
languages are configured. The chosen language is remembered in
|
|
17
25
|
localStorage("zer0-lang") for future UX (no auto-redirect).
|
|
18
26
|
Must NOT be include_cached — the menu is page-dependent.
|
|
27
|
+
The theme ships the "panel" variant (the navbar is reserved for
|
|
28
|
+
main navigation); "navbar" stays for consumers that prefer the
|
|
29
|
+
header dropdown.
|
|
30
|
+
|
|
31
|
+
MENU CONTRACT (T-038 / #406) — shared by both variants:
|
|
32
|
+
* No DISABLED rows. A target language without a generated
|
|
33
|
+
translation is still a link; it goes to the source-language
|
|
34
|
+
page and still records the preference in zer0-lang, so the
|
|
35
|
+
menu never contains a dead end.
|
|
36
|
+
* The current language is marked with a check icon and a
|
|
37
|
+
subtle tint — NOT Bootstrap's `.active` primary fill, which
|
|
38
|
+
read as a selected nav item rather than "you are here".
|
|
39
|
+
* Machine-generated translations carry a small "auto" chip.
|
|
40
|
+
The source language never does (a human wrote it), and
|
|
41
|
+
neither do untranslated rows (nothing was generated).
|
|
42
|
+
* The per-row "(Not yet translated)" text is replaced by ONE
|
|
43
|
+
footnote under a divider, referenced from each untranslated
|
|
44
|
+
row via aria-describedby. Per-row text was the single
|
|
45
|
+
biggest contributor to menu width.
|
|
46
|
+
Trigger changes (icon-only, no caret) apply to the navbar
|
|
47
|
+
variant only — the panel has an <h6> heading, not a button.
|
|
48
|
+
|
|
49
|
+
New ui keys (lang_machine_translated, lang_machine_translated_title,
|
|
50
|
+
lang_untranslated_note) live in _data/ui-text.yml `en`. core/i18n.html
|
|
51
|
+
REPLACES the whole `ui` map on a translated page rather than merging
|
|
52
|
+
it, so until scripts/translate.rb regenerates _data/i18n/<lang>.yml
|
|
53
|
+
these keys are absent there — every use below therefore carries a
|
|
54
|
+
literal `| default:` fallback. Do not remove them.
|
|
19
55
|
{%- endcomment -%}
|
|
20
56
|
{%- assign _lt_langs = site.translation.languages -%}
|
|
21
57
|
{%- if site.translation.enabled and _lt_langs and _lt_langs.size > 0 -%}
|
|
22
58
|
{%- include core/i18n.html -%}
|
|
59
|
+
{%- assign _lt_variant = include.variant | default: 'navbar' -%}
|
|
23
60
|
{%- assign _lt_source = site.translation.source_lang | default: 'en' -%}
|
|
24
61
|
{%- assign _lt_current = page.lang | default: _lt_source -%}
|
|
25
62
|
{%- if _lt_current == _lt_source -%}
|
|
@@ -30,25 +67,117 @@
|
|
|
30
67
|
{%- assign _lt_entry = site.data.i18n.manifest.pages[_lt_src_url] -%}
|
|
31
68
|
{%- assign _lt_names = site.data.i18n.languages -%}
|
|
32
69
|
{%- assign _lt_source_name = _lt_names[_lt_source].native | default: _lt_source -%}
|
|
70
|
+
{%- comment -%}
|
|
71
|
+
Display name of the language being read, for the trigger's title/aria — the
|
|
72
|
+
icon-only button has no visible text, so the accessible name has to carry it.
|
|
73
|
+
{%- endcomment -%}
|
|
74
|
+
{%- if _lt_current == _lt_source -%}
|
|
75
|
+
{%- assign _lt_current_name = _lt_source_name -%}
|
|
76
|
+
{%- else -%}
|
|
77
|
+
{%- assign _lt_current_name = _lt_names[_lt_current].native | default: _lt_current -%}
|
|
78
|
+
{%- endif -%}
|
|
79
|
+
{%- comment -%}
|
|
80
|
+
Does ANY target language lack a translation of this page? Drives whether the
|
|
81
|
+
footnote + divider render at all — an all-translated menu shows neither.
|
|
82
|
+
A target we are currently reading is by definition translated.
|
|
83
|
+
{%- endcomment -%}
|
|
84
|
+
{%- assign _lt_untranslated = 0 -%}
|
|
85
|
+
{%- for _lt_l in _lt_langs -%}
|
|
86
|
+
{%- unless _lt_l == _lt_current or _lt_entry[_lt_l] -%}
|
|
87
|
+
{%- assign _lt_untranslated = _lt_untranslated | plus: 1 -%}
|
|
88
|
+
{%- endunless -%}
|
|
89
|
+
{%- endfor -%}
|
|
90
|
+
{%- assign _lt_note_id = 'zer0-lang-untranslated-note' -%}
|
|
91
|
+
{%- assign _lt_auto_label = ui.lang_machine_translated | default: 'auto' -%}
|
|
92
|
+
{%- assign _lt_auto_title = ui.lang_machine_translated_title | default: 'Machine-generated translation' -%}
|
|
93
|
+
{%- assign _lt_note_text = ui.lang_untranslated_note | default: 'Marked languages are not yet translated — those links open the original.' -%}
|
|
94
|
+
{%- assign _lt_fallback_url = _lt_src_url | default: '/' | relative_url -%}
|
|
95
|
+
{%- if _lt_variant == 'panel' -%}
|
|
96
|
+
<div class="mb-4 nav-lang-panel" id="zer0-lang-toggle">
|
|
97
|
+
<h6 class="text-body-secondary small text-uppercase fw-semibold mb-2">
|
|
98
|
+
<i class="bi bi-translate me-1" aria-hidden="true"></i>{{ ui.lang_toggle_label | default: "Language" }}
|
|
99
|
+
</h6>
|
|
100
|
+
<div class="list-group nav-lang-options" role="group" aria-label="{{ ui.lang_toggle_aria | default: 'Change language' }}">
|
|
101
|
+
{%- if _lt_current == _lt_source %}
|
|
102
|
+
<span class="list-group-item nav-lang-item is-current" aria-current="true">
|
|
103
|
+
<span class="nav-lang-code">{{ _lt_source | slice: 0, 2 | upcase }}</span>
|
|
104
|
+
<span class="nav-lang-name">{{ _lt_source_name }}</span>
|
|
105
|
+
<i class="bi bi-check2 nav-lang-check" aria-hidden="true"></i>
|
|
106
|
+
</span>
|
|
107
|
+
{%- else %}
|
|
108
|
+
<a class="list-group-item list-group-item-action nav-lang-item" data-lang="{{ _lt_source }}" hreflang="{{ _lt_source }}" href="{{ _lt_fallback_url }}">
|
|
109
|
+
<span class="nav-lang-code">{{ _lt_source | slice: 0, 2 | upcase }}</span>
|
|
110
|
+
<span class="nav-lang-name">{{ _lt_source_name }}</span>
|
|
111
|
+
</a>
|
|
112
|
+
{%- endif %}
|
|
113
|
+
{%- for _lt_lang in _lt_langs -%}
|
|
114
|
+
{%- assign _lt_name = _lt_names[_lt_lang].native | default: _lt_lang -%}
|
|
115
|
+
{%- assign _lt_t = _lt_entry[_lt_lang] %}
|
|
116
|
+
{%- if _lt_current == _lt_lang %}
|
|
117
|
+
<span class="list-group-item nav-lang-item is-current" aria-current="true">
|
|
118
|
+
<span class="nav-lang-code">{{ _lt_lang | slice: 0, 2 | upcase }}</span>
|
|
119
|
+
<span class="nav-lang-name">{{ _lt_name }}</span>
|
|
120
|
+
<span class="nav-lang-auto" title="{{ _lt_auto_title }}">{{ _lt_auto_label }}</span>
|
|
121
|
+
<i class="bi bi-check2 nav-lang-check" aria-hidden="true"></i>
|
|
122
|
+
</span>
|
|
123
|
+
{%- elsif _lt_t %}
|
|
124
|
+
<a class="list-group-item list-group-item-action nav-lang-item" data-lang="{{ _lt_lang }}" hreflang="{{ _lt_lang }}" href="{{ _lt_t.url | relative_url }}">
|
|
125
|
+
<span class="nav-lang-code">{{ _lt_lang | slice: 0, 2 | upcase }}</span>
|
|
126
|
+
<span class="nav-lang-name">{{ _lt_name }}</span>
|
|
127
|
+
<span class="nav-lang-auto" title="{{ _lt_auto_title }}">{{ _lt_auto_label }}</span>
|
|
128
|
+
</a>
|
|
129
|
+
{%- else %}
|
|
130
|
+
{%- comment -%}
|
|
131
|
+
No translation yet — still a LINK, not a disabled row. It goes to the
|
|
132
|
+
source page and records the preference, so the reader lands on real
|
|
133
|
+
content instead of a dead entry. hreflang describes the DESTINATION,
|
|
134
|
+
which is the source language, not the one that was clicked.
|
|
135
|
+
{%- endcomment -%}
|
|
136
|
+
<a class="list-group-item list-group-item-action nav-lang-item is-untranslated" data-lang="{{ _lt_lang }}" hreflang="{{ _lt_source }}" href="{{ _lt_fallback_url }}" aria-describedby="{{ _lt_note_id }}-panel">
|
|
137
|
+
<span class="nav-lang-code">{{ _lt_lang | slice: 0, 2 | upcase }}</span>
|
|
138
|
+
<span class="nav-lang-name">{{ _lt_name }}</span>
|
|
139
|
+
<span class="nav-lang-mark" aria-hidden="true">*</span>
|
|
140
|
+
</a>
|
|
141
|
+
{%- endif %}
|
|
142
|
+
{%- endfor %}
|
|
143
|
+
</div>
|
|
144
|
+
{%- if _lt_untranslated > 0 %}
|
|
145
|
+
<hr class="nav-lang-divider">
|
|
146
|
+
<p class="nav-lang-note" id="{{ _lt_note_id }}-panel">{{ _lt_note_text }}</p>
|
|
147
|
+
{%- endif %}
|
|
148
|
+
</div>
|
|
149
|
+
{%- else -%}
|
|
33
150
|
<div class="dropdown nav-lang-toggle" id="zer0-lang-toggle">
|
|
151
|
+
{%- comment -%}
|
|
152
|
+
Icon-only, square, and caret-free. `dropdown-toggle` is what draws the
|
|
153
|
+
Bootstrap caret; it is NOT what wires the dropdown — Bootstrap binds on
|
|
154
|
+
[data-bs-toggle="dropdown"], so dropping the class costs no behaviour.
|
|
155
|
+
The visible EN/FR label is gone at every width (it was already hidden
|
|
156
|
+
below xl), so title + aria-label are the only accessible name and both
|
|
157
|
+
name the language being read.
|
|
158
|
+
{%- endcomment -%}
|
|
34
159
|
<button
|
|
35
|
-
class="btn nav-lang-button
|
|
160
|
+
class="btn nav-lang-button"
|
|
36
161
|
type="button"
|
|
37
162
|
id="langToggleButton"
|
|
38
163
|
data-bs-toggle="dropdown"
|
|
39
164
|
aria-expanded="false"
|
|
40
165
|
aria-label="{{ ui.lang_toggle_aria | default: 'Change language' }}"
|
|
41
|
-
title="{{ ui.lang_toggle_label | default: 'Language' }}"
|
|
166
|
+
title="{{ ui.lang_toggle_label | default: 'Language' }}: {{ _lt_current_name }}"
|
|
42
167
|
>
|
|
43
168
|
<i class="{{ site.default_icon }} bi-translate" aria-hidden="true"></i>
|
|
44
|
-
<span class="nav-link-text d-none d-xl-inline ms-1 text-uppercase">{{ _lt_current | slice: 0, 2 }}</span>
|
|
45
169
|
</button>
|
|
46
|
-
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="langToggleButton">
|
|
170
|
+
<ul class="dropdown-menu dropdown-menu-end nav-lang-menu" aria-labelledby="langToggleButton">
|
|
47
171
|
<li>
|
|
48
172
|
{%- if _lt_current == _lt_source %}
|
|
49
|
-
<span class="dropdown-item
|
|
173
|
+
<span class="dropdown-item nav-lang-item is-current" aria-current="true">
|
|
174
|
+
<span class="nav-lang-name">{{ _lt_source_name }}</span>
|
|
175
|
+
<i class="bi bi-check2 nav-lang-check" aria-hidden="true"></i>
|
|
176
|
+
</span>
|
|
50
177
|
{%- else %}
|
|
51
|
-
<a class="dropdown-item" data-lang="{{ _lt_source }}" hreflang="{{ _lt_source }}" href="{{
|
|
178
|
+
<a class="dropdown-item nav-lang-item" data-lang="{{ _lt_source }}" hreflang="{{ _lt_source }}" href="{{ _lt_fallback_url }}">
|
|
179
|
+
<span class="nav-lang-name">{{ _lt_source_name }}</span>
|
|
180
|
+
</a>
|
|
52
181
|
{%- endif %}
|
|
53
182
|
</li>
|
|
54
183
|
{%- for _lt_lang in _lt_langs -%}
|
|
@@ -56,23 +185,41 @@
|
|
|
56
185
|
{%- assign _lt_t = _lt_entry[_lt_lang] %}
|
|
57
186
|
<li>
|
|
58
187
|
{%- if _lt_current == _lt_lang %}
|
|
59
|
-
<span class="dropdown-item
|
|
188
|
+
<span class="dropdown-item nav-lang-item is-current" aria-current="true">
|
|
189
|
+
<span class="nav-lang-name">{{ _lt_name }}</span>
|
|
190
|
+
<span class="nav-lang-auto" title="{{ _lt_auto_title }}">{{ _lt_auto_label }}</span>
|
|
191
|
+
<i class="bi bi-check2 nav-lang-check" aria-hidden="true"></i>
|
|
192
|
+
</span>
|
|
60
193
|
{%- elsif _lt_t %}
|
|
61
|
-
<a class="dropdown-item" data-lang="{{ _lt_lang }}" hreflang="{{ _lt_lang }}" href="{{ _lt_t.url | relative_url }}">
|
|
194
|
+
<a class="dropdown-item nav-lang-item" data-lang="{{ _lt_lang }}" hreflang="{{ _lt_lang }}" href="{{ _lt_t.url | relative_url }}">
|
|
195
|
+
<span class="nav-lang-name">{{ _lt_name }}</span>
|
|
196
|
+
<span class="nav-lang-auto" title="{{ _lt_auto_title }}">{{ _lt_auto_label }}</span>
|
|
197
|
+
</a>
|
|
62
198
|
{%- else %}
|
|
63
|
-
|
|
199
|
+
{%- comment -%} See the panel variant above — link, never a dead row. {%- endcomment -%}
|
|
200
|
+
<a class="dropdown-item nav-lang-item is-untranslated" data-lang="{{ _lt_lang }}" hreflang="{{ _lt_source }}" href="{{ _lt_fallback_url }}" aria-describedby="{{ _lt_note_id }}">
|
|
201
|
+
<span class="nav-lang-name">{{ _lt_name }}</span>
|
|
202
|
+
<span class="nav-lang-mark" aria-hidden="true">*</span>
|
|
203
|
+
</a>
|
|
64
204
|
{%- endif %}
|
|
65
205
|
</li>
|
|
66
206
|
{%- endfor %}
|
|
207
|
+
{%- if _lt_untranslated > 0 %}
|
|
208
|
+
<li><hr class="dropdown-divider"></li>
|
|
209
|
+
<li><p class="nav-lang-note" id="{{ _lt_note_id }}">{{ _lt_note_text }}</p></li>
|
|
210
|
+
{%- endif %}
|
|
67
211
|
</ul>
|
|
68
212
|
</div>
|
|
213
|
+
{%- endif -%}
|
|
69
214
|
<script>
|
|
70
215
|
// Remember the visitor's language pick (no auto-redirect — just preference).
|
|
216
|
+
// Delegated at document level so it covers both variants and stays correct
|
|
217
|
+
// if a consumer renders the component more than once per page.
|
|
71
218
|
(function () {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
var item = event.target.closest('a
|
|
219
|
+
if (window.__zer0LangPrefWired) return;
|
|
220
|
+
window.__zer0LangPrefWired = true;
|
|
221
|
+
document.addEventListener('click', function (event) {
|
|
222
|
+
var item = event.target.closest('#zer0-lang-toggle a[data-lang]');
|
|
76
223
|
if (!item) return;
|
|
77
224
|
try { localStorage.setItem('zer0-lang', item.getAttribute('data-lang')); } catch (e) { /* private mode */ }
|
|
78
225
|
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{% comment %} Feature: ZER0-083 {% endcomment %}
|
|
2
|
+
{%- comment -%}
|
|
3
|
+
Component: page-views-init
|
|
4
|
+
Path: _includes/components/page-views-init.html
|
|
5
|
+
Purpose: Bootstrap the page-view counter — inject its configuration and
|
|
6
|
+
load the behaviour. Renders no visible markup; the badge lives
|
|
7
|
+
in components/page-views.html.
|
|
8
|
+
Params: none — configured via site.page_views
|
|
9
|
+
Depends on: assets/js/page-views.js
|
|
10
|
+
Included by: _includes/core/head.html (once per page)
|
|
11
|
+
Notes: Split from the badge include on purpose: the badge may appear
|
|
12
|
+
many times on a page, this must appear exactly once.
|
|
13
|
+
{%- endcomment -%}
|
|
14
|
+
|
|
15
|
+
{%- unless site.page_views.enabled -%}{%- comment -%} counter disabled {%- endcomment -%}
|
|
16
|
+
{%- else -%}
|
|
17
|
+
{%- comment -%}
|
|
18
|
+
`respect_dnt` / `respect_gpc` / `require_consent` default to the PostHog
|
|
19
|
+
privacy block so a site configures its privacy posture once. Liquid's
|
|
20
|
+
`default` filter treats `false` as "missing", so each flag is resolved with
|
|
21
|
+
an if-tag instead — otherwise an explicit `false` would silently flip true.
|
|
22
|
+
{%- endcomment -%}
|
|
23
|
+
{%- assign pv_dnt = true -%}
|
|
24
|
+
{%- if site.page_views.respect_dnt == false -%}{%- assign pv_dnt = false -%}{%- endif -%}
|
|
25
|
+
{%- assign pv_gpc = true -%}
|
|
26
|
+
{%- if site.page_views.respect_gpc == false -%}{%- assign pv_gpc = false -%}{%- endif -%}
|
|
27
|
+
{%- assign pv_consent = false -%}
|
|
28
|
+
{%- if site.page_views.require_consent -%}{%- assign pv_consent = true -%}{%- endif -%}
|
|
29
|
+
{%- assign pv_track = true -%}
|
|
30
|
+
{%- if site.page_views.track == false -%}{%- assign pv_track = false -%}{%- endif -%}
|
|
31
|
+
{%- assign pv_fallback = false -%}
|
|
32
|
+
{%- if site.page_views.fallback_local -%}{%- assign pv_fallback = true -%}{%- endif -%}
|
|
33
|
+
|
|
34
|
+
<!-- Page-view counter configuration consumed by assets/js/page-views.js -->
|
|
35
|
+
<script id="pageViewsConfig" type="application/json">
|
|
36
|
+
{
|
|
37
|
+
"enabled": true,
|
|
38
|
+
"provider": {{ site.page_views.provider | default: "local" | jsonify }},
|
|
39
|
+
"endpoint": {{ site.page_views.endpoint | default: "" | jsonify }},
|
|
40
|
+
"method": {{ site.page_views.method | default: "POST" | jsonify }},
|
|
41
|
+
"countKey": {{ site.page_views.count_key | default: "views" | jsonify }},
|
|
42
|
+
"dedupe": {{ site.page_views.dedupe | default: "session" | jsonify }},
|
|
43
|
+
"maxEntries": {{ site.page_views.max_entries | default: 500 }},
|
|
44
|
+
"track": {{ pv_track | jsonify }},
|
|
45
|
+
"respectDnt": {{ pv_dnt | jsonify }},
|
|
46
|
+
"respectGpc": {{ pv_gpc | jsonify }},
|
|
47
|
+
"requireConsent": {{ pv_consent | jsonify }},
|
|
48
|
+
"fallbackLocal": {{ pv_fallback | jsonify }},
|
|
49
|
+
"baseurl": {{ site.baseurl | default: "" | jsonify }},
|
|
50
|
+
"labelOne": {{ site.page_views.label_one | default: "view" | jsonify }},
|
|
51
|
+
"labelOther": {{ site.page_views.label_other | default: "views" | jsonify }}
|
|
52
|
+
}
|
|
53
|
+
</script>
|
|
54
|
+
<script defer src="{{ '/assets/js/page-views.js' | relative_url }}"></script>
|
|
55
|
+
{%- endunless -%}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{% comment %} Feature: ZER0-083 {% endcomment %}
|
|
2
|
+
{%- comment -%}
|
|
3
|
+
Component: page-views
|
|
4
|
+
Path: _includes/components/page-views.html
|
|
5
|
+
Purpose: Inline "N views" badge for a page. Ships hidden and is revealed
|
|
6
|
+
by assets/js/page-views.js once a count is known, so a page
|
|
7
|
+
with no data never flashes "0 views".
|
|
8
|
+
Params: url (string, optional, default: page.url) — page to show
|
|
9
|
+
class (string, optional, default: "") — extra classes
|
|
10
|
+
icon (string, optional, default: "bi-eye")
|
|
11
|
+
separator (bool, optional, default: false) — render a
|
|
12
|
+
leading "•" INSIDE the badge, so the separator hides
|
|
13
|
+
with it in a meta row
|
|
14
|
+
Depends on: components/page-views-init.html (config + behaviour, loaded once
|
|
15
|
+
from core/head.html), _sass/components/_page-views.scss
|
|
16
|
+
Notes: Safe to include more than once per page — each badge resolves
|
|
17
|
+
its own `data-page-views` path. Paths are site-relative and
|
|
18
|
+
baseurl-free (page.url); page-views.js normalizes the runtime
|
|
19
|
+
location the same way.
|
|
20
|
+
{%- endcomment -%}
|
|
21
|
+
|
|
22
|
+
{%- unless site.page_views.enabled -%}{%- comment -%} counter disabled {%- endcomment -%}
|
|
23
|
+
{%- else -%}
|
|
24
|
+
{%- assign pv_path = include.url | default: page.url -%}
|
|
25
|
+
{%- assign pv_class = include.class | default: "" -%}
|
|
26
|
+
{%- assign pv_icon = include.icon | default: "bi-eye" -%}
|
|
27
|
+
<span class="page-views{% if pv_class != "" %} {{ pv_class }}{% endif %}" data-page-views="{{ pv_path | escape }}" hidden>
|
|
28
|
+
{%- if include.separator -%}<span class="page-views__sep mx-2" aria-hidden="true">•</span>{%- endif -%}
|
|
29
|
+
<i class="bi {{ pv_icon }} me-1" aria-hidden="true"></i>
|
|
30
|
+
<span class="page-views__count" data-page-views-count>0</span>
|
|
31
|
+
<span class="page-views__label" data-page-views-label>{{ site.page_views.label_other | default: "views" }}</span>
|
|
32
|
+
</span>
|
|
33
|
+
{%- endunless -%}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{% comment %} Feature: ZER0-084 {% endcomment %}
|
|
2
|
+
{%- comment -%}
|
|
3
|
+
Component: recipe-card
|
|
4
|
+
Path: _includes/components/recipe-card.html
|
|
5
|
+
Purpose: Index card for one recipe — photo, title, one-line description,
|
|
6
|
+
and the three facts a cook scans for: total time, what it makes,
|
|
7
|
+
and how hard it is.
|
|
8
|
+
Params: recipe (document, required) — a `recipes` collection doc
|
|
9
|
+
heading_level (number, optional, default: 3) — the title's
|
|
10
|
+
heading level, so a card never skips a level under whatever
|
|
11
|
+
heading precedes it
|
|
12
|
+
class (string, optional, default: "")
|
|
13
|
+
Depends on: components/preview-image.html, components/recipe-duration.html,
|
|
14
|
+
_sass/components/_recipe.scss (.recipe-card)
|
|
15
|
+
Notes: Total time comes from `times.total` or prep + cook + rest, so
|
|
16
|
+
the card never disagrees with the recipe page. The whole card is
|
|
17
|
+
one link for a large tap target; the facts row is plain text so
|
|
18
|
+
it stays readable when images fail to load.
|
|
19
|
+
{%- endcomment -%}
|
|
20
|
+
{%- if include.recipe == nil -%}
|
|
21
|
+
{%- comment -%} ERROR: recipe-card requires `recipe` {%- endcomment -%}
|
|
22
|
+
{%- else -%}
|
|
23
|
+
{%- assign _rc_r = include.recipe -%}
|
|
24
|
+
{%- assign _rc_img = _rc_r.image | default: _rc_r.preview | default: site.teaser -%}
|
|
25
|
+
{%- assign _rc_prep = _rc_r.times.prep | default: 0 -%}
|
|
26
|
+
{%- assign _rc_cook = _rc_r.times.cook | default: 0 -%}
|
|
27
|
+
{%- assign _rc_rest = _rc_r.times.rest | default: 0 -%}
|
|
28
|
+
{%- if _rc_r.times.total -%}
|
|
29
|
+
{%- assign _rc_total = _rc_r.times.total -%}
|
|
30
|
+
{%- else -%}
|
|
31
|
+
{%- assign _rc_total = _rc_prep | plus: _rc_cook | plus: _rc_rest -%}
|
|
32
|
+
{%- endif -%}
|
|
33
|
+
{%- capture _rc_total_h -%}{%- include components/recipe-duration.html minutes=_rc_total -%}{%- endcapture -%}
|
|
34
|
+
{%- assign _rc_total_h = _rc_total_h | strip -%}
|
|
35
|
+
<article class="recipe-card h-100 {{ include.class | default: '' }}">
|
|
36
|
+
<a href="{{ _rc_r.url | relative_url }}" class="recipe-card-link d-flex flex-column h-100 text-decoration-none text-body">
|
|
37
|
+
{%- if _rc_img -%}
|
|
38
|
+
<div class="recipe-card-media">
|
|
39
|
+
{%- include components/preview-image.html src=_rc_img alt=_rc_r.title class="" -%}
|
|
40
|
+
</div>
|
|
41
|
+
{%- endif -%}
|
|
42
|
+
<div class="recipe-card-body">
|
|
43
|
+
{%- assign _rc_hl = include.heading_level | default: 3 -%}
|
|
44
|
+
<h{{ _rc_hl }} class="h5 recipe-card-title">{{ _rc_r.title }}</h{{ _rc_hl }}>
|
|
45
|
+
{%- if _rc_r.description -%}
|
|
46
|
+
<p class="recipe-card-description small text-body-secondary">{{ _rc_r.description | strip_html | truncate: 120 }}</p>
|
|
47
|
+
{%- endif -%}
|
|
48
|
+
<p class="recipe-card-facts small text-body-secondary mb-0">
|
|
49
|
+
{%- assign _rc_sep = false -%}
|
|
50
|
+
{%- if _rc_total_h != "" -%}
|
|
51
|
+
<span class="recipe-card-fact"><i class="bi bi-clock-history me-1" aria-hidden="true"></i>{{ _rc_total_h }}</span>
|
|
52
|
+
{%- assign _rc_sep = true -%}
|
|
53
|
+
{%- endif -%}
|
|
54
|
+
{%- if _rc_r.yield.amount -%}
|
|
55
|
+
{%- if _rc_sep %}<span class="mx-1" aria-hidden="true">•</span>{% endif -%}
|
|
56
|
+
<span class="recipe-card-fact"><i class="bi bi-pie-chart me-1" aria-hidden="true"></i>{{ _rc_r.yield.amount }} {{ _rc_r.yield.unit | default: "servings" | escape }}</span>
|
|
57
|
+
{%- assign _rc_sep = true -%}
|
|
58
|
+
{%- endif -%}
|
|
59
|
+
{%- if _rc_r.difficulty -%}
|
|
60
|
+
{%- if _rc_sep %}<span class="mx-1" aria-hidden="true">•</span>{% endif -%}
|
|
61
|
+
<span class="recipe-difficulty recipe-difficulty--{{ _rc_r.difficulty | downcase | strip }}">{{ _rc_r.difficulty | capitalize }}</span>
|
|
62
|
+
{%- endif -%}
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
</a>
|
|
66
|
+
</article>
|
|
67
|
+
{%- endif -%}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{% comment %} Feature: ZER0-084 {% endcomment %}
|
|
2
|
+
{%- comment -%}
|
|
3
|
+
Component: recipe-duration (value resolver — emits a bare string)
|
|
4
|
+
Path: _includes/components/recipe-duration.html
|
|
5
|
+
Purpose: Format a duration given in MINUTES, either for humans
|
|
6
|
+
("1 hr 30 min") or as an ISO 8601 duration for schema.org
|
|
7
|
+
("PT1H30M").
|
|
8
|
+
Params: minutes (number, required)
|
|
9
|
+
format (string, optional, default: "human") — "human" | "iso"
|
|
10
|
+
Depends on: nothing (pure Liquid arithmetic)
|
|
11
|
+
Notes: Emits nothing for a missing or zero duration, so callers can
|
|
12
|
+
test the captured value and skip the whole row. Days are not
|
|
13
|
+
special-cased — a 3-day cure reads as "72 hr", which is how
|
|
14
|
+
recipes actually write long ferments.
|
|
15
|
+
|
|
16
|
+
Liquid `assign` writes to the GLOBAL scope even inside an
|
|
17
|
+
include, so a helper called several times from one caller will
|
|
18
|
+
clobber the caller's variables if the names collide. Every
|
|
19
|
+
variable here is therefore prefixed `_rd_`, and the other recipe
|
|
20
|
+
components use their own prefixes. Keep that up when editing.
|
|
21
|
+
|
|
22
|
+
Capture it (this comment is brace-free so it cannot self-execute):
|
|
23
|
+
capture total -> include components/recipe-duration.html minutes=90
|
|
24
|
+
{%- endcomment -%}
|
|
25
|
+
{%- assign _rd_m = include.minutes -%}
|
|
26
|
+
{%- if _rd_m == nil or _rd_m == "" or _rd_m <= 0 -%}
|
|
27
|
+
{%- comment -%} recipe-duration: no duration — emitting nothing {%- endcomment -%}
|
|
28
|
+
{%- else -%}
|
|
29
|
+
{%- assign _rd_total = _rd_m | round -%}
|
|
30
|
+
{%- assign _rd_hours = _rd_total | divided_by: 60 -%}
|
|
31
|
+
{%- assign _rd_mins = _rd_total | modulo: 60 -%}
|
|
32
|
+
{%- if include.format == "iso" -%}
|
|
33
|
+
PT{% if _rd_hours > 0 %}{{ _rd_hours }}H{% endif %}{% if _rd_mins > 0 %}{{ _rd_mins }}M{% endif %}
|
|
34
|
+
{%- else -%}
|
|
35
|
+
{%- comment -%}
|
|
36
|
+
Built by appending rather than by emitting the separator from inside an
|
|
37
|
+
`if`: Liquid treats a block whose body is only whitespace as blank and
|
|
38
|
+
drops it, which silently swallowed the space in "12 hr 45 min".
|
|
39
|
+
{%- endcomment -%}
|
|
40
|
+
{%- assign _rd_out = "" -%}
|
|
41
|
+
{%- if _rd_hours > 0 -%}
|
|
42
|
+
{%- assign _rd_out = _rd_out | append: _rd_hours | append: " hr" -%}
|
|
43
|
+
{%- endif -%}
|
|
44
|
+
{%- if _rd_mins > 0 -%}
|
|
45
|
+
{%- if _rd_out != "" -%}{%- assign _rd_out = _rd_out | append: " " -%}{%- endif -%}
|
|
46
|
+
{%- assign _rd_out = _rd_out | append: _rd_mins | append: " min" -%}
|
|
47
|
+
{%- endif -%}
|
|
48
|
+
{{- _rd_out -}}
|
|
49
|
+
{%- endif -%}
|
|
50
|
+
{%- endif -%}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{% comment %} Feature: ZER0-084 {% endcomment %}
|
|
2
|
+
{%- comment -%}
|
|
3
|
+
Component: recipe-grams (value resolver — emits a bare number)
|
|
4
|
+
Path: _includes/components/recipe-grams.html
|
|
5
|
+
Purpose: Convert one recipe quantity to grams so ratio tables and
|
|
6
|
+
baker's percentages can be computed at BUILD time (no JS needed).
|
|
7
|
+
Params: qty (number, required) — the amount as authored
|
|
8
|
+
unit (string, optional) — g, kg, oz, lb, ml, l, tsp,
|
|
9
|
+
tbsp, cup, fl oz, pint, …
|
|
10
|
+
grams_per_cup (number, optional) — density, required to convert
|
|
11
|
+
a VOLUME into a weight
|
|
12
|
+
Depends on: nothing (pure Liquid arithmetic)
|
|
13
|
+
Notes: Outputs a bare number rounded to 1 decimal, or nothing at all
|
|
14
|
+
when the unit is unknown/countable ("clove", "egg") or a volume
|
|
15
|
+
with no density. Callers MUST treat empty output as "unknown".
|
|
16
|
+
Mirrors the unit table in assets/js/recipe-scaler.js — change
|
|
17
|
+
both together.
|
|
18
|
+
|
|
19
|
+
Liquid `assign` writes to the GLOBAL scope even inside an
|
|
20
|
+
include, so every variable here is prefixed `_rg_` to keep
|
|
21
|
+
repeated calls from clobbering the caller's own variables.
|
|
22
|
+
|
|
23
|
+
Capture it (this comment is brace-free so it cannot self-execute):
|
|
24
|
+
capture grams -> include components/recipe-grams.html
|
|
25
|
+
qty=i.qty unit=i.unit grams_per_cup=density
|
|
26
|
+
{%- endcomment -%}
|
|
27
|
+
{%- if include.qty == nil or include.qty == "" -%}
|
|
28
|
+
{%- comment -%} ERROR: recipe-grams requires `qty` {%- endcomment -%}
|
|
29
|
+
{%- else -%}
|
|
30
|
+
{%- assign _rg_u = include.unit | default: "" | downcase | strip | replace: ".", "" | replace: "-", " " -%}
|
|
31
|
+
{%- assign _rg_mass = 0 -%}
|
|
32
|
+
{%- assign _rg_ml = 0 -%}
|
|
33
|
+
{%- case _rg_u -%}
|
|
34
|
+
{%- when "g" or "gram" or "grams" or "gr" -%}{%- assign _rg_mass = 1 -%}
|
|
35
|
+
{%- when "kg" or "kilogram" or "kilograms" -%}{%- assign _rg_mass = 1000 -%}
|
|
36
|
+
{%- when "mg" or "milligram" or "milligrams" -%}{%- assign _rg_mass = 0.001 -%}
|
|
37
|
+
{%- when "oz" or "ounce" or "ounces" -%}{%- assign _rg_mass = 28.3495 -%}
|
|
38
|
+
{%- when "lb" or "lbs" or "pound" or "pounds" -%}{%- assign _rg_mass = 453.592 -%}
|
|
39
|
+
{%- when "ml" or "milliliter" or "milliliters" or "millilitre" or "millilitres" or "cc" -%}{%- assign _rg_ml = 1 -%}
|
|
40
|
+
{%- when "l" or "liter" or "liters" or "litre" or "litres" -%}{%- assign _rg_ml = 1000 -%}
|
|
41
|
+
{%- when "dl" or "deciliter" or "deciliters" -%}{%- assign _rg_ml = 100 -%}
|
|
42
|
+
{%- when "tsp" or "teaspoon" or "teaspoons" -%}{%- assign _rg_ml = 4.92892 -%}
|
|
43
|
+
{%- when "tbsp" or "tbs" or "tablespoon" or "tablespoons" -%}{%- assign _rg_ml = 14.7868 -%}
|
|
44
|
+
{%- when "cup" or "cups" -%}{%- assign _rg_ml = 236.588 -%}
|
|
45
|
+
{%- when "fl oz" or "floz" or "fluid ounce" or "fluid ounces" -%}{%- assign _rg_ml = 29.5735 -%}
|
|
46
|
+
{%- when "pint" or "pints" or "pt" -%}{%- assign _rg_ml = 473.176 -%}
|
|
47
|
+
{%- when "quart" or "quarts" or "qt" -%}{%- assign _rg_ml = 946.353 -%}
|
|
48
|
+
{%- when "gallon" or "gallons" or "gal" -%}{%- assign _rg_ml = 3785.41 -%}
|
|
49
|
+
{%- endcase -%}
|
|
50
|
+
{%- if _rg_mass > 0 -%}
|
|
51
|
+
{%- assign _rg_g = include.qty | times: _rg_mass -%}
|
|
52
|
+
{{- _rg_g | round: 1 -}}
|
|
53
|
+
{%- elsif _rg_ml > 0 and include.grams_per_cup -%}
|
|
54
|
+
{%- assign _rg_vol = include.qty | times: _rg_ml -%}
|
|
55
|
+
{%- assign _rg_g = _rg_vol | times: include.grams_per_cup | divided_by: 236.588 -%}
|
|
56
|
+
{{- _rg_g | round: 1 -}}
|
|
57
|
+
{%- endif -%}
|
|
58
|
+
{%- endif -%}
|
|
@@ -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 -%}
|