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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +812 -13
- data/_data/backlog.yml +356 -2
- data/_data/consumers.yml +252 -0
- data/_data/features.yml +234 -5
- data/_data/i18n/fr.yml +103 -0
- data/_data/i18n/manifest.yml +1529 -0
- data/_data/ingredient_densities.yml +122 -0
- data/_data/navigation/docs.yml +2 -0
- data/_data/navigation/main.yml +22 -0
- data/_data/recipe_courses.yml +64 -0
- data/_data/series.yml +19 -0
- data/_data/theme-manifest.yml +387 -277
- data/_data/ui-text.yml +8 -0
- data/_includes/README.md +36 -3
- data/_includes/analytics/google-tag-manager-body.html +11 -2
- data/_includes/analytics/google-tag-manager-head.html +16 -6
- data/_includes/analytics/posthog.html +33 -4
- data/_includes/components/abc-letter.html +43 -0
- data/_includes/components/background-customizer.html +2 -2
- data/_includes/components/book-card.html +42 -0
- data/_includes/components/book-nav.html +80 -0
- data/_includes/components/book-plate.html +31 -0
- data/_includes/components/book-toc.html +47 -0
- data/_includes/components/bookshelf.html +68 -0
- data/_includes/components/card-grid.html +60 -0
- data/_includes/components/cookie-consent.html +4 -4
- data/_includes/components/data-card.html +95 -0
- data/_includes/components/halfmoon.html +5 -1
- data/_includes/components/info-section.html +8 -3
- data/_includes/components/language-toggle.html +168 -21
- data/_includes/components/page-feedback.html +65 -2
- 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-controls-bar.html +10 -2
- data/_includes/components/theme-customizer.html +8 -2
- data/_includes/components/theme-info.html +8 -1
- data/_includes/content/jsonld-software.html +22 -3
- data/_includes/content/seo.html +9 -3
- data/_includes/core/color-mode-init.html +13 -4
- data/_includes/core/favicon.html +46 -0
- data/_includes/core/footer.html +18 -16
- data/_includes/core/head.html +21 -0
- data/_includes/core/header.html +7 -4
- data/_includes/custom/body-end.html +18 -0
- data/_includes/custom/body-start.html +17 -0
- data/_includes/custom/footer.html +18 -0
- data/_includes/custom/head.html +18 -0
- data/_includes/navigation/local-graph.html +28 -2
- data/_includes/navigation/nav-tree.html +3 -3
- data/_includes/navigation/section-sidebar.html +93 -11
- data/_includes/navigation/sidebar-config.html +21 -0
- data/_includes/navigation/sidebar-left.html +2 -1
- data/_includes/navigation/sidebar-nav.html +4 -0
- data/_includes/navigation/sidebar-pagetree.html +150 -0
- data/_includes/navigation/sidebar-right.html +2 -1
- data/_includes/navigation/unified-drawer.html +8 -2
- data/_includes/obsidian/full-graph.html +165 -136
- data/_includes/setup/wizard.html +173 -86
- data/_includes/stats/stats-metrics.html +2 -2
- data/_layouts/404.html +260 -0
- data/_layouts/README.md +4 -0
- data/_layouts/admin.html +2 -2
- data/_layouts/article.html +5 -0
- data/_layouts/book-abc.html +106 -0
- data/_layouts/book-story.html +91 -0
- data/_layouts/book.html +113 -0
- data/_layouts/collection.html +17 -5
- data/_layouts/cookbook.html +88 -0
- data/_layouts/default.html +4 -4
- data/_layouts/home.html +5 -2
- data/_layouts/landing.html +9 -0
- data/_layouts/news.html +155 -31
- data/_layouts/recipe.html +274 -0
- data/_layouts/root.html +38 -5
- data/_layouts/section.html +96 -25
- data/_sass/components/_book.scss +423 -0
- 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 +263 -15
- data/_sass/core/_obsidian.scss +286 -6
- data/_sass/layouts/_landing.scss +2 -2
- data/_sass/theme/_backgrounds.scss +21 -8
- 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 +4 -0
- data/assets/js/auto-hide-nav.js +5 -1
- data/assets/js/halfmoon.js +26 -0
- data/assets/js/modules/navigation/navbar.js +55 -0
- data/assets/js/obsidian-graph.js +702 -264
- data/assets/js/obsidian-local-graph.js +161 -54
- data/assets/js/page-views.js +372 -0
- data/assets/js/recipe-scaler.js +501 -0
- data/assets/js/search-modal.js +14 -1
- 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 +66 -16
- data/scripts/bin/validate +5 -1
- 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/install/README.md +47 -6
- data/scripts/install/ai/client.sh +302 -93
- data/scripts/install/ai/prompts/spec.schema.json +1 -1
- data/scripts/install/ai/wizard.sh +10 -5
- data/scripts/install/apply.sh +7 -3
- data/scripts/install/cli.sh +54 -4
- data/scripts/install/config.sh +167 -0
- data/scripts/install/doctor.sh +38 -0
- data/scripts/install/plan.sh +10 -0
- data/scripts/install/spec.sh +15 -7
- data/scripts/install/template.sh +4 -0
- data/scripts/lib/audit.sh +42 -2
- data/scripts/lint-liquid-raw.rb +137 -0
- data/scripts/propagate.rb +277 -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 +161 -15
- metadata +56 -2
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
// Feature: ZER0-084
|
|
2
|
+
/**
|
|
3
|
+
* Recipe scaling and unit conversion.
|
|
4
|
+
*
|
|
5
|
+
* Rewrites every [data-recipe-qty] and [data-recipe-temp] inside a
|
|
6
|
+
* [data-recipe] root when the reader changes the yield or the unit system.
|
|
7
|
+
* The server-rendered markup is already the correct recipe as written, so this
|
|
8
|
+
* file is pure progressive enhancement — it reveals the controls
|
|
9
|
+
* (.recipe-scaler is `hidden` until then) and never runs without them.
|
|
10
|
+
*
|
|
11
|
+
* Contract with the Liquid components (_includes/components/recipe-*.html):
|
|
12
|
+
* [data-recipe] root; data-recipe-id keys the saved scale
|
|
13
|
+
* [data-recipe-scaler] control bar; data-base-yield, data-yield-unit
|
|
14
|
+
* [data-recipe-qty] data-qty, data-qty-max, data-unit,
|
|
15
|
+
* data-grams-per-cup, data-weigh, data-scale
|
|
16
|
+
* [data-recipe-temp] data-temp-f, data-temp-c
|
|
17
|
+
* [data-recipe-name] data-singular, data-plural (countables only)
|
|
18
|
+
*
|
|
19
|
+
* The unit table below mirrors _includes/components/recipe-grams.html — change
|
|
20
|
+
* the two together.
|
|
21
|
+
*/
|
|
22
|
+
(function () {
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
var UNITS_KEY = 'zer0-recipe-units';
|
|
26
|
+
var SCALE_KEY_PREFIX = 'zer0-recipe-scale:';
|
|
27
|
+
var ML_PER_CUP = 236.588;
|
|
28
|
+
|
|
29
|
+
// Canonical unit table. `dim` is the dimension; `base` is the size of one
|
|
30
|
+
// unit in grams (mass) or millilitres (volume).
|
|
31
|
+
var UNITS = {
|
|
32
|
+
g: { dim: 'mass', base: 1, label: 'g' },
|
|
33
|
+
kg: { dim: 'mass', base: 1000, label: 'kg' },
|
|
34
|
+
mg: { dim: 'mass', base: 0.001, label: 'mg' },
|
|
35
|
+
oz: { dim: 'mass', base: 28.3495, label: 'oz' },
|
|
36
|
+
lb: { dim: 'mass', base: 453.592, label: 'lb' },
|
|
37
|
+
ml: { dim: 'volume', base: 1, label: 'ml' },
|
|
38
|
+
dl: { dim: 'volume', base: 100, label: 'dl' },
|
|
39
|
+
l: { dim: 'volume', base: 1000, label: 'l' },
|
|
40
|
+
tsp: { dim: 'volume', base: 4.92892, label: 'tsp' },
|
|
41
|
+
tbsp: { dim: 'volume', base: 14.7868, label: 'tbsp' },
|
|
42
|
+
cup: { dim: 'volume', base: ML_PER_CUP, label: 'cups' },
|
|
43
|
+
floz: { dim: 'volume', base: 29.5735, label: 'fl oz' },
|
|
44
|
+
pint: { dim: 'volume', base: 473.176, label: 'pints' },
|
|
45
|
+
quart: { dim: 'volume', base: 946.353, label: 'quarts' },
|
|
46
|
+
gallon: { dim: 'volume', base: 3785.41, label: 'gallons' }
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Everything an author might type, mapped to a canonical key above.
|
|
50
|
+
var ALIASES = {
|
|
51
|
+
g: 'g', gr: 'g', gram: 'g', grams: 'g', gramme: 'g', grammes: 'g',
|
|
52
|
+
kg: 'kg', kilo: 'kg', kilos: 'kg', kilogram: 'kg', kilograms: 'kg',
|
|
53
|
+
mg: 'mg', milligram: 'mg', milligrams: 'mg',
|
|
54
|
+
oz: 'oz', ounce: 'oz', ounces: 'oz',
|
|
55
|
+
lb: 'lb', lbs: 'lb', pound: 'lb', pounds: 'lb',
|
|
56
|
+
ml: 'ml', cc: 'ml', milliliter: 'ml', milliliters: 'ml',
|
|
57
|
+
millilitre: 'ml', millilitres: 'ml',
|
|
58
|
+
dl: 'dl', deciliter: 'dl', deciliters: 'dl',
|
|
59
|
+
l: 'l', liter: 'l', liters: 'l', litre: 'l', litres: 'l',
|
|
60
|
+
tsp: 'tsp', teaspoon: 'tsp', teaspoons: 'tsp',
|
|
61
|
+
tbsp: 'tbsp', tbs: 'tbsp', tablespoon: 'tbsp', tablespoons: 'tbsp',
|
|
62
|
+
cup: 'cup', cups: 'cup',
|
|
63
|
+
floz: 'floz', 'fl oz': 'floz', 'fluid ounce': 'floz', 'fluid ounces': 'floz',
|
|
64
|
+
pt: 'pint', pint: 'pint', pints: 'pint',
|
|
65
|
+
qt: 'quart', quart: 'quart', quarts: 'quart',
|
|
66
|
+
gal: 'gallon', gallon: 'gallon', gallons: 'gallon'
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Ladders used to pick a readable unit after converting. Ordered small→large.
|
|
70
|
+
var LADDERS = {
|
|
71
|
+
'metric:mass': ['g', 'kg'],
|
|
72
|
+
'metric:volume': ['ml', 'l'],
|
|
73
|
+
'us:mass': ['oz', 'lb'],
|
|
74
|
+
'us:volume': ['tsp', 'tbsp', 'cup', 'quart', 'gallon']
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
var VULGAR = {
|
|
78
|
+
0.125: '⅛', 0.25: '¼', 0.333: '⅓', 0.375: '⅜',
|
|
79
|
+
0.5: '½', 0.625: '⅝', 0.667: '⅔', 0.75: '¾',
|
|
80
|
+
0.875: '⅞'
|
|
81
|
+
};
|
|
82
|
+
// Denominators US amounts are rounded to, largest quantity first. A whole
|
|
83
|
+
// cup is measured in quarters; anything under one, in eighths. Finer than
|
|
84
|
+
// that is precision no measuring spoon can deliver.
|
|
85
|
+
var US_STEPS = [
|
|
86
|
+
{ min: 1, step: 0.25 },
|
|
87
|
+
{ min: 0, step: 0.125 }
|
|
88
|
+
];
|
|
89
|
+
// Units that read as fractions on a measuring cup or spoon rather than as
|
|
90
|
+
// decimals on a scale.
|
|
91
|
+
var FRACTIONAL_UNITS = ['tsp', 'tbsp', 'cup', 'floz', 'pint', 'quart', 'gallon', 'oz', 'lb'];
|
|
92
|
+
|
|
93
|
+
/* ------------------------------------------------------------------ *
|
|
94
|
+
* Unit helpers
|
|
95
|
+
* ------------------------------------------------------------------ */
|
|
96
|
+
|
|
97
|
+
function canonicalUnit(raw) {
|
|
98
|
+
if (!raw) return null;
|
|
99
|
+
var key = String(raw).toLowerCase().trim()
|
|
100
|
+
.replace(/\./g, '')
|
|
101
|
+
.replace(/-/g, ' ')
|
|
102
|
+
.replace(/\s+/g, ' ');
|
|
103
|
+
if (ALIASES[key]) return ALIASES[key];
|
|
104
|
+
if (ALIASES[key.replace(/\s/g, '')]) return ALIASES[key.replace(/\s/g, '')];
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Pick the friendliest unit on a ladder for a magnitude in base units. */
|
|
109
|
+
function pickUnit(ladder, baseAmount) {
|
|
110
|
+
var chosen = ladder[0];
|
|
111
|
+
for (var i = 0; i < ladder.length; i++) {
|
|
112
|
+
var unit = UNITS[ladder[i]];
|
|
113
|
+
var value = baseAmount / unit.base;
|
|
114
|
+
// Step up while the amount would still read as at least one whole unit,
|
|
115
|
+
// so 1500 g becomes 1.5 kg but 900 g stays 900 g.
|
|
116
|
+
if (value >= 1) chosen = ladder[i];
|
|
117
|
+
}
|
|
118
|
+
return chosen;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* ------------------------------------------------------------------ *
|
|
122
|
+
* Number formatting
|
|
123
|
+
* ------------------------------------------------------------------ */
|
|
124
|
+
|
|
125
|
+
function roundTo(value, step) {
|
|
126
|
+
return Math.round(value / step) * step;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** "1½", "¾", "2" — US amounts read as fractions, never as 0.75. */
|
|
130
|
+
function formatFraction(value) {
|
|
131
|
+
var step = 0.0625;
|
|
132
|
+
for (var i = 0; i < US_STEPS.length; i++) {
|
|
133
|
+
if (value >= US_STEPS[i].min) { step = US_STEPS[i].step; break; }
|
|
134
|
+
}
|
|
135
|
+
var rounded = roundTo(value, step);
|
|
136
|
+
// Too small to round to even an eighth — show the decimal rather than "0".
|
|
137
|
+
if (rounded === 0) return trimNumber(value, 2);
|
|
138
|
+
var whole = Math.floor(rounded + 1e-9);
|
|
139
|
+
var frac = rounded - whole;
|
|
140
|
+
var key = Math.round(frac * 1000) / 1000;
|
|
141
|
+
if (key < 1e-6) return String(whole);
|
|
142
|
+
var glyph = VULGAR[key];
|
|
143
|
+
if (glyph) return (whole > 0 ? whole : '') + glyph;
|
|
144
|
+
return trimNumber(rounded, 2);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Metric amounts: precise when small, round when large. */
|
|
148
|
+
function formatMetric(value) {
|
|
149
|
+
if (value >= 100) return String(Math.round(value));
|
|
150
|
+
if (value >= 10) return trimNumber(Math.round(value * 2) / 2, 1);
|
|
151
|
+
if (value >= 1) return trimNumber(Math.round(value * 10) / 10, 1);
|
|
152
|
+
return trimNumber(Math.round(value * 100) / 100, 2);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Countable things ("3 eggs"): whole numbers, halves when small. */
|
|
156
|
+
function formatCount(value) {
|
|
157
|
+
if (value >= 10) return String(Math.round(value));
|
|
158
|
+
return formatFraction(value);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function trimNumber(value, decimals) {
|
|
162
|
+
var text = value.toFixed(decimals);
|
|
163
|
+
if (text.indexOf('.') !== -1) {
|
|
164
|
+
text = text.replace(/0+$/, '').replace(/\.$/, '');
|
|
165
|
+
}
|
|
166
|
+
return text;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
var COUNTED_UNITS = { cup: 'cup', pint: 'pint', quart: 'quart', gallon: 'gallon' };
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Recipe usage, not strict grammar: anything up to and including one takes
|
|
173
|
+
* the singular — "¾ cup", "1 cup", "1½ cups".
|
|
174
|
+
*/
|
|
175
|
+
function pluralize(unitKey, value) {
|
|
176
|
+
var singular = COUNTED_UNITS[unitKey];
|
|
177
|
+
if (!singular) return UNITS[unitKey].label;
|
|
178
|
+
return value <= 1 + 1e-9 ? singular : singular + 's';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* ------------------------------------------------------------------ *
|
|
182
|
+
* Quantity conversion
|
|
183
|
+
* ------------------------------------------------------------------ */
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Convert one amount into the requested system.
|
|
187
|
+
* Returns { value, unit } where `unit` is display text (may be the
|
|
188
|
+
* author's original string when nothing convertible was found).
|
|
189
|
+
*/
|
|
190
|
+
function convert(amount, rawUnit, system, density, allowWeigh) {
|
|
191
|
+
var key = canonicalUnit(rawUnit);
|
|
192
|
+
|
|
193
|
+
// "As written" still scales — it keeps the author's own unit words, but
|
|
194
|
+
// a scaled amount gets the matching singular/plural so halving "2 cups"
|
|
195
|
+
// reads "1 cup" rather than "1 cups".
|
|
196
|
+
if (!key || system === 'original') {
|
|
197
|
+
var label = rawUnit || '';
|
|
198
|
+
if (key && COUNTED_UNITS[key]) label = pluralize(key, amount);
|
|
199
|
+
return { value: amount, unit: label, key: null, sourceKey: key, keepLabel: true };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
var unit = UNITS[key];
|
|
203
|
+
var target = system === 'metric' ? 'metric' : 'us';
|
|
204
|
+
|
|
205
|
+
// Volume ↔ weight, but only when we know how heavy a cup of it is.
|
|
206
|
+
var dim = unit.dim;
|
|
207
|
+
var baseAmount = amount * unit.base; // grams or millilitres
|
|
208
|
+
|
|
209
|
+
if (density && allowWeigh) {
|
|
210
|
+
if (target === 'metric' && dim === 'volume') {
|
|
211
|
+
baseAmount = (baseAmount / ML_PER_CUP) * density; // ml → g
|
|
212
|
+
dim = 'mass';
|
|
213
|
+
} else if (target === 'us' && dim === 'mass') {
|
|
214
|
+
baseAmount = (baseAmount / density) * ML_PER_CUP; // g → ml
|
|
215
|
+
dim = 'volume';
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
var ladder = LADDERS[target + ':' + dim];
|
|
220
|
+
if (!ladder) {
|
|
221
|
+
return { value: amount, unit: rawUnit || '', key: null, sourceKey: key, keepLabel: true };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
var chosen = pickUnit(ladder, baseAmount);
|
|
226
|
+
var value = baseAmount / UNITS[chosen].base;
|
|
227
|
+
return { value: value, unit: pluralize(chosen, value), key: chosen };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Format a converted amount, choosing the notation its unit is measured in. */
|
|
231
|
+
function renderAmount(result) {
|
|
232
|
+
var unitKey = result.key || result.sourceKey;
|
|
233
|
+
var text;
|
|
234
|
+
if (!unitKey) {
|
|
235
|
+
text = formatCount(result.value); // "3 eggs", "1½ lemons"
|
|
236
|
+
} else if (FRACTIONAL_UNITS.indexOf(unitKey) !== -1) {
|
|
237
|
+
text = formatFraction(result.value); // "1¾ cups"
|
|
238
|
+
} else {
|
|
239
|
+
text = formatMetric(result.value); // "375 g", "1.5 l"
|
|
240
|
+
}
|
|
241
|
+
return result.unit ? text + ' ' + result.unit : text;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/* ------------------------------------------------------------------ *
|
|
245
|
+
* DOM updates
|
|
246
|
+
* ------------------------------------------------------------------ */
|
|
247
|
+
|
|
248
|
+
function updateQuantity(el, factor, system) {
|
|
249
|
+
var base = parseFloat(el.getAttribute('data-qty'));
|
|
250
|
+
if (isNaN(base)) return;
|
|
251
|
+
|
|
252
|
+
var scales = el.getAttribute('data-scale') !== 'false';
|
|
253
|
+
var allowWeigh = el.getAttribute('data-weigh') !== 'false';
|
|
254
|
+
var rawUnit = el.getAttribute('data-unit') || '';
|
|
255
|
+
var density = parseFloat(el.getAttribute('data-grams-per-cup'));
|
|
256
|
+
if (isNaN(density)) density = null;
|
|
257
|
+
|
|
258
|
+
var multiplier = scales ? factor : 1;
|
|
259
|
+
var parts = [renderAmount(convert(base * multiplier, rawUnit, system, density, allowWeigh))];
|
|
260
|
+
|
|
261
|
+
var max = parseFloat(el.getAttribute('data-qty-max'));
|
|
262
|
+
if (!isNaN(max) && max > base) {
|
|
263
|
+
var hi = renderAmount(convert(max * multiplier, rawUnit, system, density, allowWeigh));
|
|
264
|
+
// "2–3 cups", not "2 cups–3 cups".
|
|
265
|
+
var lo = parts[0];
|
|
266
|
+
var loParts = lo.split(' ');
|
|
267
|
+
var hiParts = hi.split(' ');
|
|
268
|
+
if (loParts.length > 1 && loParts.slice(1).join(' ') === hiParts.slice(1).join(' ')) {
|
|
269
|
+
parts = [loParts[0] + '–' + hi];
|
|
270
|
+
} else {
|
|
271
|
+
parts = [lo + '–' + hi];
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
var text = parts[0];
|
|
276
|
+
if (el.textContent !== text) el.textContent = text;
|
|
277
|
+
|
|
278
|
+
// Keep what the recipe actually said one hover away.
|
|
279
|
+
var original = el.getAttribute('data-original-text');
|
|
280
|
+
if (original && original !== text) {
|
|
281
|
+
el.title = 'As written: ' + original;
|
|
282
|
+
} else {
|
|
283
|
+
el.removeAttribute('title');
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* "2 large eggs" halved is "1 large egg", not "1 large eggs". Only applies to
|
|
289
|
+
* countable ingredients — where there is a unit, the unit carries the number
|
|
290
|
+
* and the ingredient name never changes.
|
|
291
|
+
*/
|
|
292
|
+
function updateName(qtyEl, factor) {
|
|
293
|
+
var item = qtyEl.closest('.recipe-ingredient');
|
|
294
|
+
if (!item) return;
|
|
295
|
+
var name = item.querySelector('[data-recipe-name]');
|
|
296
|
+
if (!name) return;
|
|
297
|
+
if (qtyEl.getAttribute('data-unit')) return;
|
|
298
|
+
|
|
299
|
+
var base = parseFloat(qtyEl.getAttribute('data-qty'));
|
|
300
|
+
if (isNaN(base)) return;
|
|
301
|
+
var scales = qtyEl.getAttribute('data-scale') !== 'false';
|
|
302
|
+
var amount = base * (scales ? factor : 1);
|
|
303
|
+
// Same rule as the units: one or less takes the singular, so half an egg
|
|
304
|
+
// is "½ egg yolk" rather than "½ egg yolks".
|
|
305
|
+
var singular = amount <= 1 + 1e-9;
|
|
306
|
+
var text = singular ? name.getAttribute('data-singular') : name.getAttribute('data-plural');
|
|
307
|
+
if (text && name.textContent !== text) name.textContent = text;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function updateTemperature(el, system) {
|
|
311
|
+
var f = parseFloat(el.getAttribute('data-temp-f'));
|
|
312
|
+
var c = parseFloat(el.getAttribute('data-temp-c'));
|
|
313
|
+
if (isNaN(f) && isNaN(c)) return;
|
|
314
|
+
var label = el.getAttribute('data-temp-label') || '';
|
|
315
|
+
var text;
|
|
316
|
+
if (system === 'metric') {
|
|
317
|
+
text = c + ' °C';
|
|
318
|
+
} else if (system === 'us') {
|
|
319
|
+
text = f + ' °F';
|
|
320
|
+
} else {
|
|
321
|
+
text = f + ' °F (' + c + ' °C)';
|
|
322
|
+
}
|
|
323
|
+
el.textContent = label ? label + ' ' + text : text;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/* ------------------------------------------------------------------ *
|
|
327
|
+
* Storage (never fatal — private windows throw on access)
|
|
328
|
+
* ------------------------------------------------------------------ */
|
|
329
|
+
|
|
330
|
+
function readStore(key) {
|
|
331
|
+
try { return window.localStorage.getItem(key); } catch (e) { return null; }
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function writeStore(key, value) {
|
|
335
|
+
try { window.localStorage.setItem(key, value); } catch (e) { /* ignore */ }
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/* ------------------------------------------------------------------ *
|
|
339
|
+
* Controller
|
|
340
|
+
* ------------------------------------------------------------------ */
|
|
341
|
+
|
|
342
|
+
function setupRecipe(scaler) {
|
|
343
|
+
var scope = scaler.closest('[data-recipe]') || document;
|
|
344
|
+
var baseYield = parseFloat(scaler.getAttribute('data-base-yield'));
|
|
345
|
+
if (isNaN(baseYield) || baseYield <= 0) baseYield = 1;
|
|
346
|
+
|
|
347
|
+
var yieldUnit = scaler.getAttribute('data-yield-unit') || 'servings';
|
|
348
|
+
var recipeId = scaler.getAttribute('data-recipe-id') || 'recipe';
|
|
349
|
+
var scaleKey = SCALE_KEY_PREFIX + recipeId;
|
|
350
|
+
|
|
351
|
+
var input = scaler.querySelector('[data-recipe-yield-input]');
|
|
352
|
+
var status = scaler.querySelector('[data-recipe-status]');
|
|
353
|
+
var quantities = scope.querySelectorAll('[data-recipe-qty]');
|
|
354
|
+
var temperatures = scope.querySelectorAll('[data-recipe-temp]');
|
|
355
|
+
|
|
356
|
+
// Remember what the page shipped with so "As written" is exactly that.
|
|
357
|
+
var i;
|
|
358
|
+
for (i = 0; i < quantities.length; i++) {
|
|
359
|
+
quantities[i].setAttribute('data-original-text', quantities[i].textContent.trim());
|
|
360
|
+
}
|
|
361
|
+
for (i = 0; i < temperatures.length; i++) {
|
|
362
|
+
temperatures[i].setAttribute('data-original-text', temperatures[i].textContent.trim());
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
var state = {
|
|
366
|
+
target: baseYield,
|
|
367
|
+
system: readStore(UNITS_KEY) || 'original'
|
|
368
|
+
};
|
|
369
|
+
if (['original', 'metric', 'us'].indexOf(state.system) === -1) state.system = 'original';
|
|
370
|
+
|
|
371
|
+
var saved = parseFloat(readStore(scaleKey));
|
|
372
|
+
if (!isNaN(saved) && saved > 0) state.target = saved;
|
|
373
|
+
|
|
374
|
+
// ?scale=2 or ?servings=24 wins over the remembered value — that is what a
|
|
375
|
+
// shared link is for.
|
|
376
|
+
var params = new URLSearchParams(window.location.search);
|
|
377
|
+
var fromUrl = parseFloat(params.get('servings'));
|
|
378
|
+
var scaleParam = parseFloat(params.get('scale'));
|
|
379
|
+
if (!isNaN(fromUrl) && fromUrl > 0) state.target = fromUrl;
|
|
380
|
+
else if (!isNaN(scaleParam) && scaleParam > 0) state.target = baseYield * scaleParam;
|
|
381
|
+
|
|
382
|
+
function factor() {
|
|
383
|
+
return state.target / baseYield;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function render(announce) {
|
|
387
|
+
var f = factor();
|
|
388
|
+
var j;
|
|
389
|
+
for (j = 0; j < quantities.length; j++) {
|
|
390
|
+
// Nothing to recompute when the reader is looking at the authored
|
|
391
|
+
// units AND this amount is not moving — either because the scale is
|
|
392
|
+
// 1x, or because the amount is pinned (data-scale="false", e.g. the
|
|
393
|
+
// ratio table's per-unit weight). Re-formatting it anyway would nudge
|
|
394
|
+
// the authored 81.9 g to 82 g for no reason.
|
|
395
|
+
var pinned = quantities[j].getAttribute('data-scale') === 'false';
|
|
396
|
+
var still = Math.abs(f - 1) < 1e-9 || pinned;
|
|
397
|
+
if (state.system === 'original' && still) {
|
|
398
|
+
quantities[j].textContent = quantities[j].getAttribute('data-original-text');
|
|
399
|
+
quantities[j].removeAttribute('title');
|
|
400
|
+
} else {
|
|
401
|
+
updateQuantity(quantities[j], f, state.system);
|
|
402
|
+
}
|
|
403
|
+
updateName(quantities[j], f);
|
|
404
|
+
}
|
|
405
|
+
for (j = 0; j < temperatures.length; j++) {
|
|
406
|
+
updateTemperature(temperatures[j], state.system);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (input) input.value = trimNumber(state.target, 2);
|
|
410
|
+
|
|
411
|
+
var multiplierButtons = scaler.querySelectorAll('[data-recipe-multiplier]');
|
|
412
|
+
for (j = 0; j < multiplierButtons.length; j++) {
|
|
413
|
+
var m = parseFloat(multiplierButtons[j].getAttribute('data-recipe-multiplier'));
|
|
414
|
+
var active = Math.abs(f - m) < 1e-6;
|
|
415
|
+
multiplierButtons[j].classList.toggle('active', active);
|
|
416
|
+
multiplierButtons[j].setAttribute('aria-pressed', active ? 'true' : 'false');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
var unitButtons = scaler.querySelectorAll('[data-recipe-units]');
|
|
420
|
+
for (j = 0; j < unitButtons.length; j++) {
|
|
421
|
+
var on = unitButtons[j].getAttribute('data-recipe-units') === state.system;
|
|
422
|
+
unitButtons[j].classList.toggle('active', on);
|
|
423
|
+
unitButtons[j].setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
scaler.setAttribute('data-current-system', state.system);
|
|
427
|
+
if (scope !== document) scope.setAttribute('data-scaled', Math.abs(f - 1) < 1e-9 ? 'false' : 'true');
|
|
428
|
+
|
|
429
|
+
if (announce && status) {
|
|
430
|
+
status.textContent = 'Scaled to ' + trimNumber(state.target, 2) + ' ' + yieldUnit +
|
|
431
|
+
', shown in ' + (state.system === 'original' ? 'the original units' :
|
|
432
|
+
state.system === 'metric' ? 'metric units' : 'US units') + '.';
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function setYield(value, announce) {
|
|
437
|
+
if (isNaN(value) || value <= 0) return;
|
|
438
|
+
state.target = Math.round(value * 100) / 100;
|
|
439
|
+
writeStore(scaleKey, String(state.target));
|
|
440
|
+
render(announce);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
scaler.addEventListener('click', function (event) {
|
|
444
|
+
var button = event.target.closest('button');
|
|
445
|
+
if (!button || !scaler.contains(button)) return;
|
|
446
|
+
|
|
447
|
+
if (button.hasAttribute('data-recipe-step')) {
|
|
448
|
+
var direction = parseFloat(button.getAttribute('data-recipe-step'));
|
|
449
|
+
var stepSize = state.target <= 2 ? 0.5 : 1;
|
|
450
|
+
setYield(state.target + direction * stepSize, true);
|
|
451
|
+
} else if (button.hasAttribute('data-recipe-multiplier')) {
|
|
452
|
+
setYield(baseYield * parseFloat(button.getAttribute('data-recipe-multiplier')), true);
|
|
453
|
+
} else if (button.hasAttribute('data-recipe-units')) {
|
|
454
|
+
state.system = button.getAttribute('data-recipe-units');
|
|
455
|
+
writeStore(UNITS_KEY, state.system);
|
|
456
|
+
render(true);
|
|
457
|
+
} else if (button.hasAttribute('data-recipe-reset')) {
|
|
458
|
+
state.system = 'original';
|
|
459
|
+
writeStore(UNITS_KEY, state.system);
|
|
460
|
+
setYield(baseYield, true);
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
if (input) {
|
|
465
|
+
input.addEventListener('change', function () {
|
|
466
|
+
setYield(parseFloat(input.value), true);
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
scaler.hidden = false;
|
|
471
|
+
scaler.classList.add('recipe-scaler--ready');
|
|
472
|
+
render(false);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Cross off an ingredient as you use it. */
|
|
476
|
+
function setupChecklist(root) {
|
|
477
|
+
root.addEventListener('change', function (event) {
|
|
478
|
+
var box = event.target;
|
|
479
|
+
if (!box.hasAttribute || !box.hasAttribute('data-recipe-check')) return;
|
|
480
|
+
var item = box.closest('.recipe-ingredient');
|
|
481
|
+
if (item) item.classList.toggle('recipe-ingredient--done', box.checked);
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function init() {
|
|
486
|
+
var scalers = document.querySelectorAll('[data-recipe-scaler]');
|
|
487
|
+
for (var i = 0; i < scalers.length; i++) {
|
|
488
|
+
setupRecipe(scalers[i]);
|
|
489
|
+
}
|
|
490
|
+
var roots = document.querySelectorAll('[data-recipe]');
|
|
491
|
+
for (var j = 0; j < roots.length; j++) {
|
|
492
|
+
setupChecklist(roots[j]);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (document.readyState === 'loading') {
|
|
497
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
498
|
+
} else {
|
|
499
|
+
init();
|
|
500
|
+
}
|
|
501
|
+
})();
|
data/assets/js/search-modal.js
CHANGED
|
@@ -74,8 +74,11 @@
|
|
|
74
74
|
if (typeof bootstrap === 'undefined') return;
|
|
75
75
|
const cookieEl = document.getElementById('cookieSettingsModal');
|
|
76
76
|
const infoEl = document.getElementById('info-section');
|
|
77
|
+
const drawerEl = document.getElementById('zer0UnifiedDrawer');
|
|
77
78
|
afterModalClosed(cookieEl, () => {
|
|
78
|
-
afterOffcanvasClosed(infoEl,
|
|
79
|
+
afterOffcanvasClosed(infoEl, () => {
|
|
80
|
+
afterOffcanvasClosed(drawerEl, showSearchModal);
|
|
81
|
+
});
|
|
79
82
|
});
|
|
80
83
|
};
|
|
81
84
|
|
|
@@ -106,6 +109,16 @@
|
|
|
106
109
|
});
|
|
107
110
|
});
|
|
108
111
|
|
|
112
|
+
// Hover hints ship as "Ctrl+K" in the markup; swap to the macOS glyph
|
|
113
|
+
// so the affordance matches the key the visitor actually has.
|
|
114
|
+
if (/Mac|iPhone|iPad/i.test(navigator.platform || navigator.userAgent)) {
|
|
115
|
+
document.querySelectorAll('[data-search-toggle]').forEach((button) => {
|
|
116
|
+
if (button.title) {
|
|
117
|
+
button.title = button.title.replace(/Ctrl\+K/i, '⌘K');
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
109
122
|
// Fallback keyboard shortcut ("/" or Cmd/Ctrl+K) in case other modules are unavailable
|
|
110
123
|
document.addEventListener('keydown', (event) => {
|
|
111
124
|
if (event.target.matches('input, textarea, select, [contenteditable="true"]')) {
|