@jjlmoya/utils-cooking 1.33.0 → 1.35.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 (61) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +4 -0
  3. package/src/entries.ts +5 -1
  4. package/src/index.ts +2 -0
  5. package/src/tests/i18n-titles.test.ts +1 -1
  6. package/src/tests/i18n_coverage.test.ts +1 -0
  7. package/src/tests/locale_completeness.test.ts +2 -2
  8. package/src/tests/tool_validation.test.ts +2 -2
  9. package/src/tool/carry-over-cooking-predictor/bibliography.astro +6 -0
  10. package/src/tool/carry-over-cooking-predictor/bibliography.ts +10 -0
  11. package/src/tool/carry-over-cooking-predictor/carry-over-cooking-predictor.css +513 -0
  12. package/src/tool/carry-over-cooking-predictor/component.astro +362 -0
  13. package/src/tool/carry-over-cooking-predictor/entry.ts +26 -0
  14. package/src/tool/carry-over-cooking-predictor/i18n/de.ts +286 -0
  15. package/src/tool/carry-over-cooking-predictor/i18n/en.ts +286 -0
  16. package/src/tool/carry-over-cooking-predictor/i18n/es.ts +286 -0
  17. package/src/tool/carry-over-cooking-predictor/i18n/fr.ts +286 -0
  18. package/src/tool/carry-over-cooking-predictor/i18n/id.ts +286 -0
  19. package/src/tool/carry-over-cooking-predictor/i18n/it.ts +286 -0
  20. package/src/tool/carry-over-cooking-predictor/i18n/ja.ts +286 -0
  21. package/src/tool/carry-over-cooking-predictor/i18n/ko.ts +286 -0
  22. package/src/tool/carry-over-cooking-predictor/i18n/nl.ts +286 -0
  23. package/src/tool/carry-over-cooking-predictor/i18n/pl.ts +286 -0
  24. package/src/tool/carry-over-cooking-predictor/i18n/pt.ts +286 -0
  25. package/src/tool/carry-over-cooking-predictor/i18n/ru.ts +286 -0
  26. package/src/tool/carry-over-cooking-predictor/i18n/sv.ts +286 -0
  27. package/src/tool/carry-over-cooking-predictor/i18n/tr.ts +286 -0
  28. package/src/tool/carry-over-cooking-predictor/i18n/zh.ts +286 -0
  29. package/src/tool/carry-over-cooking-predictor/index.ts +11 -0
  30. package/src/tool/carry-over-cooking-predictor/logic.ts +63 -0
  31. package/src/tool/carry-over-cooking-predictor/seo.astro +15 -0
  32. package/src/tool/egg-timer/component.astro +19 -17
  33. package/src/tool/egg-timer/perfect-boiled-egg-timer-altitude-calculator.css +336 -502
  34. package/src/tool/maillard-reaction-optimizer/bibliography.astro +6 -0
  35. package/src/tool/maillard-reaction-optimizer/bibliography.ts +14 -0
  36. package/src/tool/maillard-reaction-optimizer/component.astro +391 -0
  37. package/src/tool/maillard-reaction-optimizer/entry.ts +26 -0
  38. package/src/tool/maillard-reaction-optimizer/i18n/de.ts +307 -0
  39. package/src/tool/maillard-reaction-optimizer/i18n/en.ts +307 -0
  40. package/src/tool/maillard-reaction-optimizer/i18n/es.ts +307 -0
  41. package/src/tool/maillard-reaction-optimizer/i18n/fr.ts +307 -0
  42. package/src/tool/maillard-reaction-optimizer/i18n/id.ts +307 -0
  43. package/src/tool/maillard-reaction-optimizer/i18n/it.ts +307 -0
  44. package/src/tool/maillard-reaction-optimizer/i18n/ja.ts +307 -0
  45. package/src/tool/maillard-reaction-optimizer/i18n/ko.ts +307 -0
  46. package/src/tool/maillard-reaction-optimizer/i18n/nl.ts +308 -0
  47. package/src/tool/maillard-reaction-optimizer/i18n/pl.ts +307 -0
  48. package/src/tool/maillard-reaction-optimizer/i18n/pt.ts +307 -0
  49. package/src/tool/maillard-reaction-optimizer/i18n/ru.ts +307 -0
  50. package/src/tool/maillard-reaction-optimizer/i18n/sv.ts +307 -0
  51. package/src/tool/maillard-reaction-optimizer/i18n/tr.ts +307 -0
  52. package/src/tool/maillard-reaction-optimizer/i18n/zh.ts +307 -0
  53. package/src/tool/maillard-reaction-optimizer/index.ts +11 -0
  54. package/src/tool/maillard-reaction-optimizer/logic.ts +57 -0
  55. package/src/tool/maillard-reaction-optimizer/maillard-reaction-optimizer.css +694 -0
  56. package/src/tool/maillard-reaction-optimizer/seo.astro +15 -0
  57. package/src/tool/meat-binder-transglutaminase-calculator/bibliography.ts +10 -6
  58. package/src/tool/meat-binder-transglutaminase-calculator/component.astro +5 -1
  59. package/src/tool/meat-binder-transglutaminase-calculator/components/LabReport.astro +3 -3
  60. package/src/tools.ts +4 -0
  61. package/src/types.ts +1 -1
@@ -0,0 +1,6 @@
1
+ ---
2
+ import { Bibliography as BibliographyComponent } from '@jjlmoya/utils-shared';
3
+ import { bibliography } from './bibliography';
4
+ ---
5
+
6
+ <BibliographyComponent links={bibliography} />
@@ -0,0 +1,14 @@
1
+ export const bibliography = [
2
+ {
3
+ name: 'The Maillard Reaction: Chemistry, Biochemistry and Implications ',
4
+ url: 'https://pubs.acs.org/doi/10.1021/ja059794d',
5
+ },
6
+ {
7
+ name: 'On Food and Cooking: The Science and Lore of the Kitchen',
8
+ url: 'https://www.academia.edu/40192621/ON_FOOD_AND_COOKING_The_Science_and_Lore_of_the_Kitchen_COMPLETELY_REVISED_AND_UPDATED',
9
+ },
10
+ {
11
+ name: 'Effects of pH, Temperature, and Reactant Molar Ratio on l-Leucine and d-Glucose Maillard Browning Reaction in an Aqueous System',
12
+ url: 'https://pubs.acs.org/doi/10.1021/jf9608231',
13
+ },
14
+ ];
@@ -0,0 +1,391 @@
1
+ ---
2
+ interface Props {
3
+ ui: Record<string, string>;
4
+ }
5
+
6
+ const { ui } = Astro.props;
7
+ ---
8
+
9
+ <div class="mr">
10
+ <div class="mr-card">
11
+
12
+ <div class="mr-header">
13
+ <div class="mr-header-top">
14
+ <span class="mr-header-label">{ui.headerLabel}</span>
15
+ <span id="mr-flavor-badge" class="mr-flavor-badge safe">{ui.flavorSafe}</span>
16
+ </div>
17
+ <div class="mr-unit-row">
18
+ <span class="mr-unit-label">{ui.unitLabel}</span>
19
+ <div class="mr-unit-toggle">
20
+ <button type="button" id="mr-unit-metric" class="mr-unit-btn active" data-unit="metric">{ui.metricUnit}</button>
21
+ <button type="button" id="mr-unit-imperial" class="mr-unit-btn" data-unit="imperial">{ui.imperialUnit}</button>
22
+ </div>
23
+ </div>
24
+ </div>
25
+
26
+ <div class="mr-body">
27
+ <div class="mr-controls">
28
+ <div class="mr-control-group">
29
+ <label for="mr-weight" class="mr-control-label">{ui.weightLabel}</label>
30
+ <div class="mr-slider-group">
31
+ <input type="range" id="mr-weight" min="50" max="5000" step="10" value="1000" class="mr-range" />
32
+ <span class="mr-range-value"><span id="mr-weight-display">1000</span> <span class="mr-range-unit" id="mr-weight-unit">{ui.weightUnit}</span></span>
33
+ </div>
34
+ </div>
35
+ <div class="mr-control-group">
36
+ <label for="mr-temp" class="mr-control-label">{ui.tempLabel}</label>
37
+ <div class="mr-slider-group">
38
+ <input type="range" id="mr-temp" min="100" max="250" step="5" value="160" class="mr-range" />
39
+ <span class="mr-range-value"><span id="mr-temp-display">160</span><span class="mr-range-unit" id="mr-temp-unit">{ui.tempUnit}</span></span>
40
+ </div>
41
+ <div class="mr-temp-hints">
42
+ <span id="mr-hint-low" class="mr-temp-hint">{ui.tempLow}</span>
43
+ <span id="mr-hint-opt" class="mr-temp-hint active">{ui.tempOpt}</span>
44
+ <span id="mr-hint-high" class="mr-temp-hint">{ui.tempHigh}</span>
45
+ </div>
46
+ </div>
47
+ <div class="mr-control-group">
48
+ <label for="mr-soda" class="mr-control-label">{ui.sodaLabel}</label>
49
+ <div class="mr-slider-group">
50
+ <input type="range" id="mr-soda" min="0" max="10" step="0.1" value="0.5" class="mr-range" />
51
+ <span class="mr-range-value"><span id="mr-soda-display">0.5</span> <span class="mr-range-unit" id="mr-soda-unit">{ui.sodaUnit}</span></span>
52
+ </div>
53
+ </div>
54
+ <div class="mr-control-group" id="mr-soda-note-wrap" style="display:flex;">
55
+ <span id="mr-soda-note" class="mr-soda-rec-max">{ui.sodaNote}</span>
56
+ </div>
57
+ </div>
58
+
59
+ <div id="mr-preview" class="mr-preview">
60
+ <div class="mr-gauge-wrap">
61
+ <svg class="mr-gauge-svg" viewBox="0 0 200 130">
62
+ <path d="M 30 110 A 70 70 0 0 1 170 110" class="mr-gauge-track-bg" />
63
+ <path id="mr-gauge-arc" d="M 30 110 A 70 70 0 0 1 170 110" class="mr-gauge-track"
64
+ stroke-dasharray="220" stroke-dashoffset="220"
65
+ stroke="url(#mr-speed-grad)" />
66
+ <defs>
67
+ <linearGradient id="mr-speed-grad" x1="0%" y1="0%" x2="100%" y2="0%">
68
+ <stop offset="0%" stop-color="var(--mr-green)" />
69
+ <stop offset="50%" stop-color="var(--mr-amber)" />
70
+ <stop offset="100%" stop-color="var(--mr-danger)" />
71
+ </linearGradient>
72
+ </defs>
73
+ <text x="100" y="90" text-anchor="middle" class="mr-gauge-center-val"><tspan id="mr-gauge-val">x1.0</tspan></text>
74
+ <text x="100" y="106" text-anchor="middle" class="mr-gauge-center-label">{ui.speedLabel}</text>
75
+ </svg>
76
+ </div>
77
+
78
+ <div class="mr-browning-section">
79
+ <div class="mr-browning-label">{ui.browningLabel}</div>
80
+ <svg class="mr-browning-svg" viewBox="0 0 260 60" xmlns="http://www.w3.org/2000/svg">
81
+ <defs>
82
+ <linearGradient id="mr-browning-grad" x1="0%" y1="0%" x2="100%" y2="0%">
83
+ <stop offset="0%" stop-color="#fef3c7" />
84
+ <stop offset="20%" stop-color="#fde68a" />
85
+ <stop offset="40%" stop-color="#d4a44a" />
86
+ <stop offset="60%" stop-color="#b8803a" />
87
+ <stop offset="80%" stop-color="#7c3a1e" />
88
+ <stop offset="100%" stop-color="#3d1a08" />
89
+ </linearGradient>
90
+ <clipPath id="mr-food-clip">
91
+ <ellipse cx="130" cy="28" rx="110" ry="20" />
92
+ </clipPath>
93
+ </defs>
94
+ <g clip-path="url(#mr-food-clip)">
95
+ <rect id="mr-food-rect" x="20" y="12" width="220" height="30" fill="url(#mr-browning-grad)" />
96
+ </g>
97
+ <ellipse cx="130" cy="28" rx="110" ry="20" fill="none" stroke="var(--mr-border)" stroke-width="1" />
98
+ <text x="20" y="55" class="mr-browning-bar-label">{ui.rawLabel}</text>
99
+ <text x="130" y="55" text-anchor="middle" class="mr-browning-bar-label">{ui.goldenLabel}</text>
100
+ <text x="240" y="55" text-anchor="end" class="mr-browning-bar-label">{ui.burntLabel}</text>
101
+ <circle id="mr-browning-indicator" cx="130" cy="28" r="5" fill="var(--mr-amber)" stroke="var(--mr-surface)" stroke-width="2.5" />
102
+ </svg>
103
+ </div>
104
+
105
+ <div class="mr-stats-row">
106
+ <div class="mr-stat-card">
107
+ <span class="mr-stat-label">{ui.phEstimated}</span>
108
+ <span class="mr-stat-number" id="mr-ph-val" style="color: var(--mr-amber);">6.0</span>
109
+ </div>
110
+ <div class="mr-stat-card">
111
+ <span class="mr-stat-label">{ui.timeSaved}</span>
112
+ <span class="mr-stat-number"><span class="mr-stat-badge fast"><span id="mr-time-saved">0</span>%</span></span>
113
+ </div>
114
+ </div>
115
+
116
+ <div class="mr-soda-rec">
117
+ <span class="mr-soda-rec-label">{ui.sodaRecommended}</span>
118
+ <span class="mr-soda-rec-value" id="mr-soda-rec-val">0.75 g</span>
119
+ <br/>
120
+ <span class="mr-soda-rec-max" id="mr-soda-max-val">{ui.sodaMax}: 1.50 g</span>
121
+ </div>
122
+ </div>
123
+ </div>
124
+
125
+ <div id="mr-error" class="mr-error" style="display: none;">
126
+ <p id="mr-error-text" class="mr-error-text"></p>
127
+ </div>
128
+
129
+ <div class="mr-footer">
130
+ <div id="mr-desc" class="mr-footer-text">{ui.footerTemplate}</div>
131
+ </div>
132
+
133
+ </div>
134
+ </div>
135
+
136
+ <script is:inline define:vars={{ ui }}>
137
+ const $ = (id) => document.getElementById(id);
138
+ const weight = $('mr-weight'), temp = $('mr-temp'), soda = $('mr-soda');
139
+ const wDisp = $('mr-weight-display'), tDisp = $('mr-temp-display'), sDisp = $('mr-soda-display');
140
+ const wUnit = $('mr-weight-unit'), tUnit = $('mr-temp-unit'), sUnit = $('mr-soda-unit');
141
+ const gaugeArc = $('mr-gauge-arc'), gaugeVal = $('mr-gauge-val');
142
+ const timeSaved = $('mr-time-saved'), phVal = $('mr-ph-val');
143
+ const sodaRec = $('mr-soda-rec-val'), sodaMax = $('mr-soda-max-val');
144
+ const flavorBadge = $('mr-flavor-badge'), browningInd = $('mr-browning-indicator');
145
+ const hintLow = $('mr-hint-low'), hintOpt = $('mr-hint-opt'), hintHigh = $('mr-hint-high');
146
+ const preview = $('mr-preview'), errorBox = $('mr-error'), errorText = $('mr-error-text');
147
+ const desc = $('mr-desc');
148
+ const sodaNote = $('mr-soda-note');
149
+ const unitMetric = $('mr-unit-metric'), unitImperial = $('mr-unit-imperial');
150
+
151
+ const arcLen = 220;
152
+ const MAX_SODA_PER_KG = 1.5;
153
+ const DEG = '\u00B0';
154
+
155
+ function cToF(c) { return c * 9/5 + 32; }
156
+ function fToC(f) { return (f - 32) * 5/9; }
157
+ function gToOz(g) { return g * 0.035274; }
158
+ function ozToG(oz) { return oz / 0.035274; }
159
+
160
+ let unit = 'metric';
161
+
162
+ function getWunit() { return unit === 'imperial' ? ui.weightOz : ui.weightUnit; }
163
+ function getTunit() { return DEG + (unit === 'imperial' ? 'F' : 'C'); }
164
+ function getSunit() { return unit === 'imperial' ? ui.sodaOz : ui.sodaUnit; }
165
+
166
+ function save() {
167
+ try {
168
+ localStorage.setItem('mr-v2', JSON.stringify({
169
+ w: weight.value, t: temp.value, s: soda.value, u: unit
170
+ }));
171
+ } catch {}
172
+ }
173
+
174
+ function load() {
175
+ try {
176
+ const d = localStorage.getItem('mr-v2');
177
+ if (!d) return;
178
+ const v = JSON.parse(d);
179
+ if (v.u === 'imperial') switchUnit('imperial', false);
180
+ if (v.w) weight.value = v.w;
181
+ if (v.t) temp.value = v.t;
182
+ if (v.s) soda.value = v.s;
183
+ } catch {}
184
+ }
185
+
186
+ function updateUnits() {
187
+ const wu = getWunit();
188
+ const tu = getTunit();
189
+ const su = getSunit();
190
+ wUnit.textContent = wu;
191
+ tUnit.textContent = tu;
192
+ sUnit.textContent = su;
193
+ }
194
+
195
+ function updateTempHints(t) {
196
+ const isImp = unit === 'imperial';
197
+ const lo = isImp ? Math.round(cToF(110)) : 110;
198
+ const midLo = isImp ? Math.round(cToF(140)) : 140;
199
+ const midHi = isImp ? Math.round(cToF(180)) : 180;
200
+ const hi = isImp ? Math.round(cToF(180)) : 180;
201
+ const d = isImp ? DEG + 'F' : DEG + 'C';
202
+
203
+ hintLow.textContent = ui.tempLow.replace(/\{min\}/g, lo + d).replace(/\{max\}/g, midLo + d);
204
+ hintOpt.textContent = ui.tempOpt.replace(/\{min\}/g, midLo + d).replace(/\{max\}/g, midHi + d);
205
+ hintHigh.textContent = ui.tempHigh.replace(/\{min\}/g, hi + d);
206
+
207
+ hintLow.classList.toggle('active', t >= 110 && t < 140);
208
+ hintOpt.classList.toggle('active', t >= 140 && t <= 180);
209
+ hintHigh.classList.toggle('active', t > 180);
210
+ }
211
+
212
+ function setWeightBounds(isImp) {
213
+ weight.setAttribute('min', isImp ? '1' : '50');
214
+ weight.setAttribute('max', isImp ? '176' : '5000');
215
+ weight.setAttribute('step', isImp ? '0.1' : '10');
216
+ }
217
+
218
+ function setTempBounds(isImp) {
219
+ temp.setAttribute('min', isImp ? '212' : '100');
220
+ temp.setAttribute('max', isImp ? '482' : '250');
221
+ temp.setAttribute('step', isImp ? '10' : '5');
222
+ }
223
+
224
+ function setSodaBounds(isImp) {
225
+ soda.setAttribute('min', '0');
226
+ soda.setAttribute('max', isImp ? '0.35' : '10');
227
+ soda.setAttribute('step', isImp ? '0.01' : '0.1');
228
+ }
229
+
230
+ function setSliderBounds() {
231
+ const isImp = unit === 'imperial';
232
+ setWeightBounds(isImp);
233
+ setTempBounds(isImp);
234
+ setSodaBounds(isImp);
235
+ }
236
+
237
+ function formatWeight(wv) { return unit === 'imperial' ? gToOz(wv).toFixed(1) : Math.round(wv).toString(); }
238
+ function formatTemp(tv) { return unit === 'imperial' ? Math.round(cToF(tv)).toString() : tv.toString(); }
239
+ function formatSoda(sv) { return unit === 'imperial' ? gToOz(sv).toFixed(2) : sv.toFixed(1); }
240
+
241
+ function switchUnit(to, persist = true) {
242
+ if (unit === to) return;
243
+ const wv = parseFloat(weight.value) || 1000;
244
+ const tv = parseFloat(temp.value) || 160;
245
+ const sv = parseFloat(soda.value) || 0;
246
+
247
+ unit = to;
248
+ unitMetric.classList.toggle('active', to === 'metric');
249
+ unitImperial.classList.toggle('active', to === 'imperial');
250
+
251
+ setSliderBounds();
252
+ weight.value = formatWeight(wv);
253
+ temp.value = formatTemp(tv);
254
+ soda.value = formatSoda(sv);
255
+
256
+ updateUnits();
257
+ updateView();
258
+ if (persist) save();
259
+ }
260
+
261
+ function getWeightGrams() {
262
+ const v = parseFloat(weight.value) || 0;
263
+ return unit === 'imperial' ? ozToG(v) : v;
264
+ }
265
+
266
+ function getTempCelsius() {
267
+ const v = parseFloat(temp.value) || 0;
268
+ return unit === 'imperial' ? fToC(v) : v;
269
+ }
270
+
271
+ function getSodaGrams() {
272
+ const v = parseFloat(soda.value) || 0;
273
+ return unit === 'imperial' ? ozToG(v) : v;
274
+ }
275
+
276
+ function getFlavorRisk(pH) {
277
+ if (pH > 8.5) return 'danger';
278
+ if (pH > 7.5) return 'caution';
279
+ return 'safe';
280
+ }
281
+
282
+ function updateFlavorBadge(pH) {
283
+ const map = { safe: ui.flavorSafe, caution: ui.flavorCaution, danger: ui.flavorDanger };
284
+ const risk = getFlavorRisk(pH);
285
+ flavorBadge.className = 'mr-flavor-badge ' + risk;
286
+ flavorBadge.textContent = map[risk];
287
+ }
288
+
289
+ function updateBrowning(pct) {
290
+ const cx = 20 + pct * 220;
291
+ browningInd.setAttribute('cx', cx.toString());
292
+ }
293
+
294
+ function updateGauge(multiplier) {
295
+ const pct = Math.min(1, (multiplier - 1) / 2);
296
+ gaugeArc.setAttribute('stroke-dashoffset', (arcLen * (1 - pct)).toString());
297
+ gaugeVal.textContent = 'x' + multiplier.toFixed(1);
298
+ }
299
+
300
+ function computeResult(wv, sv) {
301
+ const gPerKg = sv / (wv / 1000);
302
+ const ph = Math.min(9, 6 + gPerKg * 2);
303
+ const mult = Math.max(1, parseFloat((Math.log2(Math.max(1.1, ph - 5)) * 2).toFixed(1)));
304
+ const pct = Math.max(0, parseFloat(((1 - 1 / mult) * 100).toFixed(0)));
305
+ return { ph, mult, pct };
306
+ }
307
+
308
+ function showError() {
309
+ errorBox.style.display = 'block';
310
+ preview.style.display = 'none';
311
+ errorText.textContent = ui.errorTempTooLow;
312
+ const su = getSunit();
313
+ desc.innerHTML = ui.footerTemplate
314
+ .replace(/\{weight\}/g, '\u2014')
315
+ .replace(/\{wunit\}/g, getWunit())
316
+ .replace(/\{temp\}/g, '\u2014')
317
+ .replace(/\{tunit\}/g, getTunit())
318
+ .replace(/\{soda\}/g, '\u2014')
319
+ .replace(/\{sunit\}/g, su)
320
+ .replace(/\{multiplier\}/g, '0')
321
+ .replace(/\{timeSaved\}/g, '0');
322
+ }
323
+
324
+ function updateSodaNote(ph) {
325
+ if (ph > 8.5) { sodaNote.textContent = ui.sodaDangerNote; return; }
326
+ if (ph > 7.5) { sodaNote.textContent = ui.sodaCautionNote; return; }
327
+ sodaNote.textContent = ui.sodaNote;
328
+ }
329
+
330
+ function renderResult(wv, tv, sv, safeMax) {
331
+ const { ph, mult, pct } = computeResult(wv, sv);
332
+ const half = safeMax / 2;
333
+ phVal.textContent = ph.toFixed(1);
334
+ timeSaved.textContent = pct.toString();
335
+ const su = getSunit();
336
+ sodaRec.textContent = (unit === 'imperial' ? gToOz(half).toFixed(2) : half.toFixed(2)) + ' ' + su;
337
+ updateFlavorBadge(ph);
338
+ updateGauge(mult);
339
+ updateBrowning(Math.min(1, Math.max(0, (ph - 6) / 3)));
340
+
341
+ const wu = getWunit();
342
+ const tu = getTunit();
343
+ desc.innerHTML = ui.footerTemplate
344
+ .replace(/\{weight\}/g, formatWeight(wv))
345
+ .replace(/\{wunit\}/g, wu)
346
+ .replace(/\{temp\}/g, formatTemp(tv))
347
+ .replace(/\{tunit\}/g, tu)
348
+ .replace(/\{soda\}/g, formatSoda(sv))
349
+ .replace(/\{sunit\}/g, su)
350
+ .replace(/\{multiplier\}/g, mult.toFixed(1))
351
+ .replace(/\{timeSaved\}/g, pct.toString());
352
+
353
+ sDisp.textContent = formatSoda(sv);
354
+ updateSodaNote(ph);
355
+ }
356
+
357
+ function updateView() {
358
+ const wv = getWeightGrams();
359
+ const tv = getTempCelsius();
360
+ const sv = getSodaGrams();
361
+
362
+ wDisp.textContent = formatWeight(wv);
363
+ tDisp.textContent = formatTemp(tv);
364
+ sDisp.textContent = formatSoda(sv);
365
+ updateTempHints(tv);
366
+
367
+ const safeMax = MAX_SODA_PER_KG * (wv / 1000);
368
+ const su = getSunit();
369
+ const isImp = unit === 'imperial';
370
+ soda.setAttribute('max', isImp ? gToOz(safeMax).toFixed(2) : (Math.ceil(safeMax * 10) / 10).toString());
371
+ sodaMax.textContent = ui.sodaMax + ': ' + (isImp ? gToOz(safeMax).toFixed(2) : safeMax.toFixed(2)) + ' ' + su;
372
+
373
+ if (tv < 110) { showError(); return; }
374
+ if (sv > safeMax) { soda.value = formatSoda(safeMax); return; }
375
+
376
+ errorBox.style.display = 'none';
377
+ preview.style.display = 'flex';
378
+
379
+ renderResult(wv, tv, sv, safeMax);
380
+ save();
381
+ }
382
+
383
+ unitMetric.addEventListener('click', function() { switchUnit('metric'); });
384
+ unitImperial.addEventListener('click', function() { switchUnit('imperial'); });
385
+ weight.addEventListener('input', updateView);
386
+ temp.addEventListener('input', updateView);
387
+ soda.addEventListener('input', updateView);
388
+
389
+ load();
390
+ updateView();
391
+ </script>
@@ -0,0 +1,26 @@
1
+ import type { CookingToolEntry } from '../../types';
2
+
3
+ export const maillardReaction: CookingToolEntry = {
4
+ id: 'maillard-reaction-optimizer',
5
+ icons: {
6
+ bg: 'mdi:food-croissant',
7
+ fg: 'mdi:flask',
8
+ },
9
+ i18n: {
10
+ de: () => import('./i18n/de').then((m) => m.content),
11
+ en: () => import('./i18n/en').then((m) => m.content),
12
+ es: () => import('./i18n/es').then((m) => m.content),
13
+ fr: () => import('./i18n/fr').then((m) => m.content),
14
+ id: () => import('./i18n/id').then((m) => m.content),
15
+ it: () => import('./i18n/it').then((m) => m.content),
16
+ ja: () => import('./i18n/ja').then((m) => m.content),
17
+ ko: () => import('./i18n/ko').then((m) => m.content),
18
+ nl: () => import('./i18n/nl').then((m) => m.content),
19
+ pl: () => import('./i18n/pl').then((m) => m.content),
20
+ pt: () => import('./i18n/pt').then((m) => m.content),
21
+ ru: () => import('./i18n/ru').then((m) => m.content),
22
+ sv: () => import('./i18n/sv').then((m) => m.content),
23
+ tr: () => import('./i18n/tr').then((m) => m.content),
24
+ zh: () => import('./i18n/zh').then((m) => m.content),
25
+ },
26
+ };