@jjlmoya/utils-cooking 1.32.0 → 1.34.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 (84) hide show
  1. package/package.json +2 -2
  2. package/src/category/index.ts +6 -0
  3. package/src/entries.ts +7 -1
  4. package/src/index.ts +3 -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 +12 -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.astro +6 -0
  58. package/src/tool/meat-binder-transglutaminase-calculator/bibliography.ts +18 -0
  59. package/src/tool/meat-binder-transglutaminase-calculator/component.astro +253 -0
  60. package/src/tool/meat-binder-transglutaminase-calculator/components/LabReport.astro +59 -0
  61. package/src/tool/meat-binder-transglutaminase-calculator/components/TissueSpecimen.astro +67 -0
  62. package/src/tool/meat-binder-transglutaminase-calculator/components/TissueViewer.astro +137 -0
  63. package/src/tool/meat-binder-transglutaminase-calculator/entry.ts +26 -0
  64. package/src/tool/meat-binder-transglutaminase-calculator/i18n/de.ts +178 -0
  65. package/src/tool/meat-binder-transglutaminase-calculator/i18n/en.ts +178 -0
  66. package/src/tool/meat-binder-transglutaminase-calculator/i18n/es.ts +178 -0
  67. package/src/tool/meat-binder-transglutaminase-calculator/i18n/fr.ts +178 -0
  68. package/src/tool/meat-binder-transglutaminase-calculator/i18n/id.ts +178 -0
  69. package/src/tool/meat-binder-transglutaminase-calculator/i18n/it.ts +178 -0
  70. package/src/tool/meat-binder-transglutaminase-calculator/i18n/ja.ts +178 -0
  71. package/src/tool/meat-binder-transglutaminase-calculator/i18n/ko.ts +178 -0
  72. package/src/tool/meat-binder-transglutaminase-calculator/i18n/nl.ts +178 -0
  73. package/src/tool/meat-binder-transglutaminase-calculator/i18n/pl.ts +178 -0
  74. package/src/tool/meat-binder-transglutaminase-calculator/i18n/pt.ts +178 -0
  75. package/src/tool/meat-binder-transglutaminase-calculator/i18n/ru.ts +178 -0
  76. package/src/tool/meat-binder-transglutaminase-calculator/i18n/sv.ts +178 -0
  77. package/src/tool/meat-binder-transglutaminase-calculator/i18n/tr.ts +178 -0
  78. package/src/tool/meat-binder-transglutaminase-calculator/i18n/zh.ts +178 -0
  79. package/src/tool/meat-binder-transglutaminase-calculator/index.ts +11 -0
  80. package/src/tool/meat-binder-transglutaminase-calculator/logic.ts +66 -0
  81. package/src/tool/meat-binder-transglutaminase-calculator/meat-binder-transglutaminase-calculator.css +785 -0
  82. package/src/tool/meat-binder-transglutaminase-calculator/seo.astro +15 -0
  83. package/src/tools.ts +6 -0
  84. package/src/types.ts +1 -1
@@ -0,0 +1,362 @@
1
+ ---
2
+ interface Props {
3
+ ui: Record<string, string>;
4
+ }
5
+
6
+ const { ui } = Astro.props;
7
+ ---
8
+
9
+ <div class="co">
10
+ <div class="co-card">
11
+
12
+ <div class="co-timeline-section">
13
+ <div class="co-timeline-header">
14
+ <span class="co-timeline-title">Roast Timeline</span>
15
+ <span id="co-phase" class="co-timeline-phase">roasting</span>
16
+ </div>
17
+ <svg class="co-timeline-svg" viewBox="0 0 800 150" xmlns="http://www.w3.org/2000/svg">
18
+ <defs>
19
+ <linearGradient id="co-heat-grad" x1="0%" y1="0%" x2="100%" y2="0%">
20
+ <stop offset="0%" stop-color="#1e1e1e" />
21
+ <stop offset="15%" stop-color="#450a0a" />
22
+ <stop offset="40%" stop-color="#dc2626" />
23
+ <stop offset="65%" stop-color="#f97316" />
24
+ <stop offset="85%" stop-color="#facc15" />
25
+ <stop offset="100%" stop-color="#22c55e" />
26
+ </linearGradient>
27
+ <clipPath id="co-clip">
28
+ <rect x="40" y="30" width="720" height="42" rx="4" />
29
+ </clipPath>
30
+ </defs>
31
+
32
+ <rect x="40" y="30" width="720" height="42" rx="4" fill="url(#co-heat-grad)" opacity="0.75" />
33
+
34
+ <g clip-path="url(#co-clip)">
35
+ <rect x="40" y="30" width="720" height="42" rx="4" fill="url(#co-heat-grad)" />
36
+ <line id="co-pull-line" x1="40" y1="26" x2="40" y2="76" stroke="#fff" stroke-width="3" opacity="0" />
37
+ <line id="co-pull-target" x1="40" y1="32" x2="40" y2="70" stroke="rgba(255,255,255,0.35)" stroke-width="1.5" stroke-dasharray="4 4" />
38
+ </g>
39
+
40
+ <rect x="36" y="26" width="728" height="50" rx="8" fill="none" stroke="var(--co-border)" stroke-width="1" />
41
+
42
+ <text x="40" y="118" fill="var(--co-muted)" font-size="13" font-family="inherit" font-weight="700" letter-spacing="0.08em">OVEN</text>
43
+ <text x="400" y="118" text-anchor="middle" fill="var(--co-muted)" font-size="13" font-family="inherit" font-weight="700" letter-spacing="0.08em">PULL</text>
44
+ <text x="760" y="118" text-anchor="end" fill="var(--co-muted)" font-size="13" font-family="inherit" font-weight="700" letter-spacing="0.08em">REST</text>
45
+
46
+ <line x1="40" y1="125" x2="760" y2="125" stroke="var(--co-border)" stroke-width="0.5" />
47
+ <circle id="co-cursor" cx="40" cy="125" r="5" fill="var(--co-orange)" stroke="var(--co-surface)" stroke-width="2" />
48
+
49
+ <text id="co-temp-label" x="400" y="146" text-anchor="middle" fill="var(--co-muted)" font-size="12" font-family="inherit" font-weight="700" letter-spacing="0.04em" stroke="var(--co-surface)" stroke-width="3" paint-order="stroke">center: --</text>
50
+ </svg>
51
+ </div>
52
+
53
+ <div class="co-body">
54
+
55
+ <div class="co-controls">
56
+ <div class="co-control-group">
57
+ <span class="co-control-label">{ui.geometryLabel}</span>
58
+ <select id="co-geometry" class="co-select">
59
+ <option value="whole-bird">{ui.wholeBird}</option>
60
+ <option value="cylindrical-roast">{ui.cylindricalRoast}</option>
61
+ <option value="flat-cut">{ui.flatCut}</option>
62
+ </select>
63
+ </div>
64
+ <div class="co-control-group">
65
+ <label for="co-weight" class="co-control-label">{ui.weightLabel}</label>
66
+ <div class="co-slider-group">
67
+ <input type="range" id="co-weight" min="150" max="8000" step="50" value="2000" class="co-range" />
68
+ <span class="co-range-value"><span id="co-weight-display">2000</span> <span class="co-range-unit" id="co-weight-unit">g</span></span>
69
+ </div>
70
+ </div>
71
+ <div class="co-control-group">
72
+ <label for="co-oven" class="co-control-label">{ui.ovenTempLabel}</label>
73
+ <div class="co-slider-group">
74
+ <input type="range" id="co-oven" min="0" max="350" step="5" value="200" class="co-range" />
75
+ <span class="co-range-value"><span id="co-oven-display">200</span><span class="co-range-unit" id="co-oven-unit">C</span></span>
76
+ </div>
77
+ </div>
78
+ <div class="co-control-group">
79
+ <label for="co-target" class="co-control-label">{ui.targetTempLabel}</label>
80
+ <div class="co-slider-group">
81
+ <input type="range" id="co-target" min="0" max="100" step="0.5" value="54" class="co-range" />
82
+ <span class="co-range-value"><span id="co-target-display">54.0</span><span class="co-range-unit" id="co-target-unit">C</span></span>
83
+ </div>
84
+ </div>
85
+ <div style="display: flex; justify-content: flex-end;">
86
+ <div class="co-unit-toggle">
87
+ <button type="button" id="co-unit-metric" class="co-unit-btn active" data-unit="metric">{ui.metricUnit}</button>
88
+ <button type="button" id="co-unit-imperial" class="co-unit-btn" data-unit="imperial">{ui.imperialUnit}</button>
89
+ </div>
90
+ </div>
91
+ </div>
92
+
93
+ <div id="co-verdict" class="co-result">
94
+ <div class="co-gauge-wrap">
95
+ <svg class="co-gauge-svg" viewBox="0 0 170 150">
96
+ <circle cx="85" cy="75" r="65" class="co-gauge-track" />
97
+ <circle id="co-gauge-arc" cx="85" cy="75" r="65" class="co-gauge-arc"
98
+ stroke-dasharray="408" stroke-dashoffset="204" transform="rotate(-90 85 75)" />
99
+ <line id="co-gauge-target" x1="85" y1="10" x2="85" y2="17" class="co-gauge-target-line" transform="rotate(0 85 75)" />
100
+ <text id="co-gauge-val" x="85" y="72" text-anchor="middle" class="co-gauge-center">48.5</text>
101
+ <text id="co-gauge-deg" x="85" y="97" text-anchor="middle" class="co-gauge-unit">C</text>
102
+ </svg>
103
+ <div class="co-gauge-label">{ui.pullTemp}</div>
104
+ </div>
105
+
106
+ <div class="co-stats-row">
107
+ <div class="co-stat-card">
108
+ <span class="co-stat-label">{ui.carryOver}</span>
109
+ <span class="co-stat-number"><span class="co-stat-badge">+<span id="co-r-carry">5.5</span> <span class="co-stat-unit" id="co-r-carry-unit">C</span></span></span>
110
+ </div>
111
+ <div class="co-stat-card">
112
+ <span class="co-stat-label">{ui.restTime}</span>
113
+ <span class="co-stat-number"><span id="co-r-rest">16</span> <span class="co-stat-unit">{ui.minutes}</span></span>
114
+ </div>
115
+ </div>
116
+ </div>
117
+ </div>
118
+
119
+ <div id="co-error" class="co-error" style="display: none;">
120
+ <p id="co-error-text" class="co-error-text"></p>
121
+ </div>
122
+
123
+ <div class="co-footer">
124
+ <p id="co-desc" class="co-footer-text">{ui.footerTemplate}</p>
125
+ </div>
126
+
127
+ </div>
128
+ </div>
129
+
130
+ <script is:inline define:vars={{ ui }}>
131
+ const $ = (id) => document.getElementById(id);
132
+ const geo = $('co-geometry'), w = $('co-weight'), oven = $('co-oven'), target = $('co-target');
133
+ const wDisp = $('co-weight-display'), ovenDisp = $('co-oven-display'), targetDisp = $('co-target-display');
134
+ const wUnit = $('co-weight-unit'), ovenUnit = $('co-oven-unit'), targetUnit = $('co-target-unit');
135
+ const rCarry = $('co-r-carry'), rRest = $('co-r-rest');
136
+ const verdict = $('co-verdict'), errorBox = $('co-error'), errorText = $('co-error-text');
137
+ const phase = $('co-phase'), desc = $('co-desc'), cursor = $('co-cursor');
138
+ const pullLine = $('co-pull-line'), pullTarget = $('co-pull-target'), tempLabel = $('co-temp-label');
139
+ const unitMetric = $('co-unit-metric'), unitImperial = $('co-unit-imperial');
140
+ const rCarryUnit = $('co-r-carry-unit');
141
+ const gaugeArc = $('co-gauge-arc'), gaugeVal = $('co-gauge-val'), gaugeDeg = $('co-gauge-deg');
142
+ const gaugeTarget = $('co-gauge-target');
143
+
144
+ const GEOM = { 'whole-bird': 0.045, 'cylindrical-roast': 0.035, 'flat-cut': 0.025 };
145
+ const MIN = { 'whole-bird': 500, 'cylindrical-roast': 300, 'flat-cut': 150 };
146
+ const MAX = { 'whole-bird': 8000, 'cylindrical-roast': 6000, 'flat-cut': 2000 };
147
+ const WUNIT = { 'metric': ui.weightG, 'imperial': ui.weightOz };
148
+ const KEY = 'co-v6';
149
+ const CIRCUMFERENCE = 2 * Math.PI * 65;
150
+
151
+ function cToF(c) { return c * 9/5 + 32; }
152
+ function fToC(f) { return (f - 32) * 5/9; }
153
+ function gToOz(g) { return g * 0.035274; }
154
+ function ozToG(oz) { return oz / 0.035274; }
155
+
156
+ let unit = 'metric';
157
+ const DEG = '\u00B0';
158
+
159
+ function save() {
160
+ try {
161
+ const wv = getWeightGrams(), ov = getOvenCelsius(), tv = getTargetCelsius();
162
+ localStorage.setItem(KEY, JSON.stringify({ g: geo.value, w: wv, o: ov, t: tv, u: unit }));
163
+ } catch {}
164
+ }
165
+
166
+ function loadImperial() {
167
+ unit = 'imperial';
168
+ unitMetric.classList.remove('active');
169
+ unitImperial.classList.add('active');
170
+ w.setAttribute('step', '0.1');
171
+ updateTempBounds();
172
+ }
173
+
174
+ function setFieldVal(field, metricVal, convertFn, precision) {
175
+ field.value = unit === 'imperial' ? convertFn(metricVal).toFixed(precision) : Math.round(metricVal).toString();
176
+ }
177
+
178
+ function loadWeight(wv) {
179
+ if (!wv) return;
180
+ const v = parseFloat(wv) || 2000;
181
+ w.value = unit === 'imperial' ? (gToOz(v) * 10 / 10).toFixed(1) : Math.round(v).toString();
182
+ }
183
+
184
+ function loadTemp(s) {
185
+ if (s.o) setFieldVal(oven, parseFloat(s.o) || 200, cToF, 0);
186
+ if (s.t) setFieldVal(target, parseFloat(s.t) || 54, cToF, 1);
187
+ }
188
+
189
+ function load() {
190
+ try {
191
+ const d = localStorage.getItem(KEY);
192
+ if (!d) return;
193
+ const s = JSON.parse(d);
194
+ if (s.g) geo.value = s.g;
195
+ if (s.u === 'imperial') loadImperial();
196
+ updateWeightBounds();
197
+ loadWeight(s.w);
198
+ loadTemp(s);
199
+ } catch {}
200
+ }
201
+
202
+ function updateWeightBounds() {
203
+ const gv = geo.value;
204
+ const minW = unit === 'imperial' ? Math.round(gToOz(MIN[gv]) * 10) / 10 : MIN[gv];
205
+ const maxW = unit === 'imperial' ? Math.round(gToOz(MAX[gv]) * 10) / 10 : MAX[gv];
206
+ w.setAttribute('min', minW);
207
+ w.setAttribute('max', maxW);
208
+ }
209
+
210
+ function getWeightGrams() {
211
+ const v = parseFloat(w.value) || 0;
212
+ return unit === 'imperial' ? ozToG(v) : v;
213
+ }
214
+
215
+ function getOvenCelsius() {
216
+ const v = parseFloat(oven.value) || 0;
217
+ return unit === 'imperial' ? fToC(v) : v;
218
+ }
219
+
220
+ function getTargetCelsius() {
221
+ const v = parseFloat(target.value) || 0;
222
+ return unit === 'imperial' ? fToC(v) : v;
223
+ }
224
+
225
+ function setDisplayWeight(g) {
226
+ w.value = unit === 'imperial' ? (gToOz(g) * 10 / 10).toFixed(1) : Math.round(g).toString();
227
+ }
228
+
229
+ function getErr(gv, wv, ov, tv) {
230
+ if (tv >= ov) return ui.errorTargetExceedsOven;
231
+ if (ov > 350) return ui.errorOvenTooHot;
232
+ if (wv < MIN[gv]) return ui.errorWeightTooLow;
233
+ if (wv > MAX[gv]) return ui.errorWeightTooHigh;
234
+ return null;
235
+ }
236
+
237
+ function updateTempBounds() {
238
+ const isImp = unit === 'imperial';
239
+ oven.setAttribute('min', isImp ? '32' : '0');
240
+ oven.setAttribute('max', isImp ? '662' : '350');
241
+ oven.setAttribute('step', isImp ? '10' : '5');
242
+ target.setAttribute('min', isImp ? '32' : '0');
243
+ target.setAttribute('max', isImp ? '212' : '100');
244
+ target.setAttribute('step', isImp ? '1' : '0.5');
245
+ }
246
+
247
+ function switchUnit(to) {
248
+ if (unit === to) return;
249
+ const prevG = getWeightGrams(), prevOC = getOvenCelsius(), prevTC = getTargetCelsius();
250
+ unit = to;
251
+ unitMetric.classList.toggle('active', to === 'metric');
252
+ unitImperial.classList.toggle('active', to === 'imperial');
253
+ updateWeightBounds();
254
+ updateTempBounds();
255
+ setDisplayWeight(prevG);
256
+ w.setAttribute('step', unit === 'imperial' ? '0.1' : '50');
257
+ oven.value = to === 'imperial' ? Math.round(cToF(prevOC)).toString() : Math.round(prevOC).toString();
258
+ target.value = to === 'imperial' ? cToF(prevTC).toFixed(1) : prevTC.toFixed(1);
259
+ updateUnits();
260
+ updateView();
261
+ }
262
+
263
+ function updateUnits() {
264
+ const deg = DEG + (unit === 'imperial' ? 'F' : 'C');
265
+ const wu = unit === 'imperial' ? ui.weightOz : ui.weightG;
266
+ wUnit.textContent = wu;
267
+ ovenUnit.textContent = deg;
268
+ targetUnit.textContent = deg;
269
+ rCarryUnit.textContent = deg;
270
+ gaugeDeg.textContent = deg;
271
+ }
272
+
273
+ function updateGauge(pullCelsius, targetCelsius) {
274
+ const isImp = unit === 'imperial';
275
+ const maxTemp = isImp ? 212 : 100;
276
+ const pullValue = isImp ? cToF(pullCelsius) : pullCelsius;
277
+ const targetValue = isImp ? cToF(targetCelsius) : targetCelsius;
278
+ const pct = Math.max(0, Math.min(1, pullValue / maxTemp));
279
+ gaugeArc.setAttribute('stroke-dashoffset', (CIRCUMFERENCE * (1 - pct)).toString());
280
+ gaugeTarget.setAttribute('transform', 'rotate(' + (Math.min(1, targetValue / maxTemp) * 180) + ' 85 75)');
281
+ }
282
+
283
+ function renderFooter(opts) {
284
+ return ui.footerTemplate
285
+ .replace(/\{carry\}/g, '+' + opts.carry + opts.deg)
286
+ .replace(/\{rest\}/g, opts.rest + ' min')
287
+ .replace(/\{weight\}/g, opts.weight)
288
+ .replace(/\{wunit\}/g, WUNIT[unit])
289
+ .replace(/\{oven\}/g, opts.oven + opts.deg)
290
+ .replace(/\{target\}/g, opts.target + opts.deg);
291
+ }
292
+
293
+ function showError() {
294
+ errorBox.style.display = 'block';
295
+ verdict.style.display = 'none';
296
+ phase.textContent = 'error';
297
+ pullLine.setAttribute('opacity', '0');
298
+ cursor.setAttribute('opacity', '0');
299
+ tempLabel.textContent = 'center: --';
300
+ }
301
+
302
+ function updateTimeline(pct, pullDisp, targetDisp, deg) {
303
+ pullLine.setAttribute('x1', (40 + pct * 360).toString());
304
+ pullLine.setAttribute('x2', (40 + pct * 360).toString());
305
+ pullTarget.setAttribute('x1', (40 + pct * 360).toString());
306
+ pullTarget.setAttribute('x2', (40 + pct * 360).toString());
307
+ pullLine.setAttribute('opacity', '1');
308
+ cursor.setAttribute('opacity', '1');
309
+ cursor.setAttribute('cx', (40 + pct * 720).toString());
310
+ tempLabel.textContent = 'pull at ' + pullDisp + deg + ' / target ' + targetDisp + deg;
311
+ }
312
+
313
+ function disp(v, toF) {
314
+ return unit === 'imperial' ? toF(v).toFixed(1) : v.toFixed(1);
315
+ }
316
+
317
+ function updateView() {
318
+ const gv = geo.value, wv = getWeightGrams(), ov = getOvenCelsius(), tv = getTargetCelsius();
319
+ wDisp.textContent = parseFloat(w.value) % 1 === 0 ? parseFloat(w.value).toString() : parseFloat(w.value).toFixed(1);
320
+ ovenDisp.textContent = oven.value;
321
+ targetDisp.textContent = parseFloat(target.value).toFixed(1);
322
+
323
+ const err = getErr(gv, wv, ov, tv);
324
+ if (err) { errorText.textContent = err; showError(); save(); return; }
325
+
326
+ errorBox.style.display = 'none';
327
+ verdict.style.display = 'flex';
328
+
329
+ const wF = Math.min(1, wv / 3000);
330
+ const carry = parseFloat(((ov - tv) * GEOM[gv] * wF).toFixed(1));
331
+ const pull = parseFloat((tv - carry).toFixed(1));
332
+ const rest = Math.round(Math.min(45, Math.max(10, wv / 100 * 0.8)));
333
+
334
+ const deg = DEG + (unit === 'imperial' ? 'F' : 'C');
335
+ const pullDisp = disp(pull, cToF);
336
+ const targetDisp2 = disp(tv, cToF);
337
+ const carryDisp = unit === 'imperial' ? (carry * 9/5).toFixed(1) : carry.toFixed(1);
338
+
339
+ rCarry.textContent = carryDisp;
340
+ rRest.textContent = rest.toString();
341
+ desc.textContent = renderFooter({ carry: carryDisp, rest: rest.toString(), weight: wDisp.textContent, oven: oven.value, target: targetDisp.textContent, deg: deg });
342
+ gaugeVal.textContent = disp(pull, cToF);
343
+ updateGauge(pull, tv);
344
+
345
+ const pct = Math.min(1, carry / 12);
346
+ updateTimeline(pct, pullDisp, targetDisp2, deg);
347
+ save();
348
+ }
349
+
350
+ geo.addEventListener('change', () => { updateWeightBounds(); updateView(); });
351
+ w.addEventListener('input', updateView);
352
+ oven.addEventListener('input', updateView);
353
+ target.addEventListener('input', updateView);
354
+ unitMetric.addEventListener('click', () => switchUnit('metric'));
355
+ unitImperial.addEventListener('click', () => switchUnit('imperial'));
356
+
357
+ load();
358
+ updateWeightBounds();
359
+ updateTempBounds();
360
+ updateUnits();
361
+ updateView();
362
+ </script>
@@ -0,0 +1,26 @@
1
+ import type { CookingToolEntry } from '../../types';
2
+
3
+ export const carryOverCooking: CookingToolEntry = {
4
+ id: 'carry-over-cooking-predictor',
5
+ icons: {
6
+ bg: 'mdi:thermometer',
7
+ fg: 'mdi:chart-bell-curve',
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
+ };