@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,253 @@
1
+ ---
2
+ import TissueSpecimen from './components/TissueSpecimen.astro';
3
+ import TissueViewer from './components/TissueViewer.astro';
4
+ import LabReport from './components/LabReport.astro';
5
+
6
+ interface Props {
7
+ ui: Record<string, string>;
8
+ }
9
+
10
+ const { ui } = Astro.props;
11
+ ---
12
+
13
+ <div class="tg">
14
+ <div class="tg-bench">
15
+ <div class="tg-bench-inner">
16
+ <div class="tg-grid"></div>
17
+
18
+ <div class="tg-layout">
19
+ <TissueSpecimen ui={ui} />
20
+ <TissueViewer />
21
+ <LabReport ui={ui} />
22
+ </div>
23
+ </div>
24
+ </div>
25
+ </div>
26
+
27
+ <script is:inline define:vars={{ ui }}>
28
+ const TISSUE = {
29
+ 'red-meat': { base: '#b91c1c', dark: '#991b1b', fiber: '#fca5a5' },
30
+ 'poultry': { base: '#d97706', dark: '#b45309', fiber: '#fde68a' },
31
+ 'fish': { base: '#d6d3d1', dark: '#a8a29e', fiber: '#f5f5f4' },
32
+ 'shellfish': { base: '#f97316', dark: '#ea580c', fiber: '#fed7aa' },
33
+ };
34
+ const RATIO_MAP = { 'red-meat': [0.0075, 0.01], 'poultry': [0.01, 0.012], 'fish': [0.012, 0.015], 'shellfish': [0.01, 0.015] };
35
+ const TEMP_RANGES = [
36
+ { min: 0, max: 5, hMin: 6, hMax: 24 },
37
+ { min: 5, max: 15, hMin: 3, hMax: 8 },
38
+ { min: 15, max: 25, hMin: 1, hMax: 4 },
39
+ { min: 25, max: 40, hMin: 0.5, hMax: 2 },
40
+ ];
41
+ const BOND_IDS = ['tg-bond-1','tg-bond-2','tg-bond-3','tg-bond-4','tg-bond-5'];
42
+ const NODE_IDS = ['tg-node-1a','tg-node-1b','tg-node-2a','tg-node-2b','tg-node-3a','tg-node-3b','tg-node-4a','tg-node-4b','tg-node-5a','tg-node-5b'];
43
+
44
+ const $ = (id) => document.getElementById(id);
45
+ const unitMetric = $('tg-unit-metric'), unitImperial = $('tg-unit-imperial');
46
+ const methodDry = $('tg-method-dry'), methodSlurry = $('tg-method-slurry');
47
+ const weightInput = $('tg-weight-input'), proteinSelect = $('tg-protein-select'), tempInput = $('tg-temp-input');
48
+ const weightUnit = $('tg-weight-unit'), tempUnit = $('tg-temp-unit');
49
+ const rTg=$('tg-r-tg'), rWater=$('tg-r-water'), rTotal=$('tg-r-total'), rRatio=$('tg-r-ratio');
50
+ const rIncMin=$('tg-r-inc-min'), rIncMax=$('tg-r-inc-max'), rWaterRow=$('tg-r-water-row');
51
+ const rIncubation=$('tg-r-incubation'), rAlert=$('tg-r-alert'), methodDesc=$('tg-method-desc');
52
+ const statusLabel=$('tg-status-label'), tempDisplay=$('tg-temp-display'), tempFill=$('tg-temp-fill');
53
+ const bondsFormed=$('tg-bonds-formed'), denaturedOverlay=$('tg-denatured-overlay'), methodLabel=$('tg-method-label');
54
+ const bondEls = BOND_IDS.map($), nodeEls = NODE_IDS.map($);
55
+ const particles = ['tg-particle-1','tg-particle-2','tg-particle-3','tg-particle-4'].map($);
56
+
57
+ let unit = 'metric', method = 'dry';
58
+ const KEY = 'cooking-tg-v3';
59
+
60
+ function cToF(c) { return c * 9/5 + 32; }
61
+ function fToC(f) { return (f - 32) * 5/9; }
62
+ function gToOz(g) { return g * 0.035274; }
63
+ function ozToG(oz) { return oz / 0.035274; }
64
+
65
+ function saveState() {
66
+ try { localStorage.setItem(KEY, JSON.stringify({ unit, method, weight: weightInput.value, protein: proteinSelect.value, temp: tempInput.value })); } catch {}
67
+ }
68
+
69
+ function loadState() {
70
+ try {
71
+ const data = localStorage.getItem(KEY);
72
+ if (!data) return;
73
+ const s = JSON.parse(data);
74
+ if (s.unit) unit = s.unit;
75
+ if (s.method) method = s.method;
76
+ if (s.weight) weightInput.value = s.weight;
77
+ if (s.protein) proteinSelect.value = s.protein;
78
+ if (s.temp) tempInput.value = s.temp;
79
+ } catch {}
80
+ }
81
+
82
+ function applyStateUI() {
83
+ if (unit === 'imperial') { unitImperial.classList.add('active'); unitMetric.classList.remove('active'); }
84
+ if (method === 'slurry') { methodSlurry.classList.add('active'); methodDry.classList.remove('active'); }
85
+ }
86
+
87
+ function updateUnits() {
88
+ weightUnit.textContent = unit === 'imperial' ? 'oz' : 'g';
89
+ tempUnit.textContent = unit === 'imperial' ? '°F' : '°C';
90
+ weightInput.setAttribute('max', unit === 'imperial' ? '1760' : '50000');
91
+ tempInput.setAttribute('max', unit === 'imperial' ? '176' : '80');
92
+ $('tg-unit-tg').textContent = unit === 'imperial' ? 'oz' : 'g';
93
+ $('tg-unit-water').textContent = unit === 'imperial' ? 'fl oz' : 'ml';
94
+ $('tg-unit-total').textContent = unit === 'imperial' ? 'oz' : 'g';
95
+ }
96
+
97
+ function getWeightGrams() {
98
+ const v = parseFloat(weightInput.value) || 0;
99
+ return unit === 'imperial' ? ozToG(v) : v;
100
+ }
101
+
102
+ function getTempCelsius() {
103
+ const v = parseFloat(tempInput.value) || 0;
104
+ return unit === 'imperial' ? fToC(v) : v;
105
+ }
106
+
107
+ function setDisplayWeight(g) {
108
+ weightInput.value = unit === 'imperial' ? gToOz(g).toFixed(1) : Math.round(g).toString();
109
+ }
110
+
111
+ function updateTissueColors(p) {
112
+ const c = TISSUE[p] || TISSUE['red-meat'];
113
+ const root = document.querySelector('.tg');
114
+ root.style.setProperty('--tg-tissue-a-current', c.base);
115
+ root.style.setProperty('--tg-tissue-b-current', c.dark);
116
+ root.style.setProperty('--tg-fiber-current', c.fiber);
117
+ }
118
+
119
+ function updateBonds(n) {
120
+ bondEls.forEach((b, i) => b.classList.toggle('active', i < n));
121
+ nodeEls.forEach((nd, i) => nd.classList.toggle('active', i < n * 2));
122
+ bondsFormed.textContent = Math.min(n, bondEls.length);
123
+ }
124
+
125
+ let particleInterval = null;
126
+
127
+ function startParticles() {
128
+ stopParticles();
129
+ const baseX = 188, baseY = [75, 115, 155, 195], targets = [85, 125, 160, 200];
130
+ let frame = 0;
131
+ particleInterval = setInterval(() => {
132
+ frame = (frame + 1) % 120;
133
+ particles.forEach((p, i) => {
134
+ const idx = (frame + i * 30) % 120;
135
+ if (idx >= 60) { p.setAttribute('opacity', '0'); return; }
136
+ const t = idx / 60;
137
+ p.setAttribute('cx', (baseX + t * 124).toString());
138
+ p.setAttribute('cy', (baseY[i] + t * (targets[i] - baseY[i])).toString());
139
+ p.setAttribute('opacity', (Math.sin(t * Math.PI) * 0.9).toString());
140
+ p.classList.add('active');
141
+ });
142
+ }, 25);
143
+ }
144
+
145
+ function stopParticles() {
146
+ if (particleInterval) { clearInterval(particleInterval); particleInterval = null; }
147
+ particles.forEach(p => { p.setAttribute('opacity', '0'); p.classList.remove('active'); });
148
+ }
149
+
150
+ function updateTempGauge(tempC) {
151
+ const display = unit === 'imperial' ? cToF(tempC) : tempC;
152
+ tempDisplay.textContent = display.toFixed(1) + (unit === 'imperial' ? '°F' : '°C');
153
+ const pct = Math.max(0, Math.min(100, (tempC / 60) * 100));
154
+ tempFill.style.height = pct + '%';
155
+ tempFill.classList.remove('warm', 'hot');
156
+ if (tempC >= 55) tempFill.classList.add('hot');
157
+ else if (tempC >= 35) tempFill.classList.add('warm');
158
+ }
159
+
160
+ function computeResult(meatWeightG, tempC, proteinType) {
161
+ if (tempC >= 60) return { enzymeDestroyed: true, tgGrams: 0, waterMl: 0, totalG: meatWeightG, ratioPct: 0, hMin: 0, hMax: 0 };
162
+
163
+ const ratios = RATIO_MAP[proteinType] || RATIO_MAP['red-meat'];
164
+ const midRatio = (ratios[0] + ratios[1]) / 2;
165
+ const tgGrams = parseFloat((meatWeightG * midRatio).toFixed(2));
166
+ const waterMl = method === 'slurry' ? parseFloat((tgGrams * 4).toFixed(1)) : 0;
167
+ const totalG = method === 'slurry'
168
+ ? parseFloat((meatWeightG + tgGrams + waterMl).toFixed(2))
169
+ : parseFloat((meatWeightG + tgGrams).toFixed(2));
170
+
171
+ let hMin = 6, hMax = 24;
172
+ for (const r of TEMP_RANGES) {
173
+ if (tempC >= r.min && tempC < r.max) { hMin = r.hMin; hMax = r.hMax; break; }
174
+ }
175
+ return { enzymeDestroyed: false, tgGrams, waterMl, totalG, ratioPct: parseFloat((midRatio * 100).toFixed(2)), hMin, hMax };
176
+ }
177
+
178
+ function renderDenatured(meatWeightG) {
179
+ rTg.textContent = rWater.textContent = '0';
180
+ rTotal.textContent = (unit === 'imperial' ? gToOz(meatWeightG) : meatWeightG).toFixed(2);
181
+ rRatio.textContent = rIncMin.textContent = rIncMax.textContent = '0';
182
+ rAlert.style.display = 'block'; rIncubation.style.display = 'none';
183
+ rWaterRow.style.display = method === 'slurry' ? 'flex' : 'none';
184
+ statusLabel.textContent = 'denatured'; statusLabel.style.color = '#dc2626';
185
+ denaturedOverlay.classList.add('active');
186
+ updateBonds(0); stopParticles();
187
+ }
188
+
189
+ function renderResult(r, meatWeightG) {
190
+ if (r.enzymeDestroyed) { renderDenatured(meatWeightG); return; }
191
+ denaturedOverlay.classList.remove('active');
192
+ statusLabel.textContent = 'incubating'; statusLabel.style.color = '';
193
+ rAlert.style.display = 'none'; rIncubation.style.display = 'block';
194
+ const f = unit === 'imperial' ? 0.035274 : 1;
195
+ const fWater = unit === 'imperial' ? 0.033814 : 1;
196
+ rTg.textContent = (r.tgGrams * f).toFixed(2);
197
+ rTotal.textContent = (r.totalG * f).toFixed(2);
198
+ rRatio.textContent = r.ratioPct.toFixed(2);
199
+ rIncMin.textContent = r.hMin; rIncMax.textContent = r.hMax;
200
+ if (method === 'slurry') {
201
+ rWater.textContent = (r.waterMl * fWater).toFixed(1); rWaterRow.style.display = 'flex';
202
+ methodDesc.textContent = ui.slurryDesc;
203
+ } else {
204
+ rWater.textContent = '0'; rWaterRow.style.display = 'none';
205
+ methodDesc.textContent = ui.dryDustingDesc;
206
+ }
207
+ const n = Math.round(Math.min(1, meatWeightG / 4000) * 5);
208
+ updateBonds(n);
209
+ if (n > 0) startParticles(); else stopParticles();
210
+ }
211
+
212
+ function updateView() {
213
+ updateUnits();
214
+ methodLabel.textContent = method === 'dry' ? 'DRY DUSTING' : 'SLURRY BONDING';
215
+ const meatWeightG = getWeightGrams(), tempC = getTempCelsius(), proteinType = proteinSelect.value;
216
+ updateTissueColors(proteinType);
217
+ updateTempGauge(tempC);
218
+ renderResult(computeResult(meatWeightG, tempC, proteinType), meatWeightG);
219
+ saveState();
220
+ }
221
+
222
+ function switchUnit(to) {
223
+ if (unit === to) return;
224
+ const prevG = getWeightGrams(), prevC = getTempCelsius();
225
+ unit = to;
226
+ unitMetric.classList.toggle('active', to === 'metric');
227
+ unitImperial.classList.toggle('active', to === 'imperial');
228
+ setDisplayWeight(prevG);
229
+ tempInput.value = to === 'imperial' ? cToF(prevC).toFixed(1) : prevC.toFixed(1);
230
+ updateView();
231
+ }
232
+
233
+ function switchMethod(to) {
234
+ if (method === to) return;
235
+ method = to;
236
+ methodDry.classList.toggle('active', to === 'dry');
237
+ methodSlurry.classList.toggle('active', to === 'slurry');
238
+ updateView();
239
+ }
240
+
241
+ unitMetric.addEventListener('click', () => switchUnit('metric'));
242
+ unitImperial.addEventListener('click', () => switchUnit('imperial'));
243
+ methodDry.addEventListener('click', () => switchMethod('dry'));
244
+ methodSlurry.addEventListener('click', () => switchMethod('slurry'));
245
+ weightInput.addEventListener('input', updateView);
246
+ proteinSelect.addEventListener('change', updateView);
247
+ tempInput.addEventListener('input', updateView);
248
+
249
+ loadState();
250
+ applyStateUI();
251
+ updateUnits();
252
+ updateView();
253
+ </script>
@@ -0,0 +1,59 @@
1
+ ---
2
+ interface Props {
3
+ ui: Record<string, string>;
4
+ }
5
+
6
+ const { ui } = Astro.props;
7
+ ---
8
+
9
+ <div class="tg-report">
10
+ <div class="tg-report-header">
11
+ <div class="tg-report-dot"></div>
12
+ <span class="tg-report-label">Lab Report</span>
13
+ </div>
14
+ <div class="tg-report-body">
15
+ <div class="tg-measurement">
16
+ <span class="tg-measurement-label">{ui.tgAmount}</span>
17
+ <span class="tg-measurement-value">
18
+ <span id="tg-r-tg">7.50</span>
19
+ <span id="tg-unit-tg" class="tg-unit">{ui.grams}</span>
20
+ </span>
21
+ </div>
22
+
23
+ <div id="tg-r-water-row" class="tg-measurement">
24
+ <span class="tg-measurement-label">{ui.waterAmount}</span>
25
+ <span class="tg-measurement-value">
26
+ <span id="tg-r-water">30.0</span>
27
+ <span id="tg-unit-water" class="tg-unit">{ui.milliliters}</span>
28
+ </span>
29
+ </div>
30
+
31
+ <div class="tg-measurement">
32
+ <span class="tg-measurement-label">{ui.totalWeight}</span>
33
+ <span class="tg-measurement-value">
34
+ <span id="tg-r-total">1037.50</span>
35
+ <span id="tg-unit-total" class="tg-unit">{ui.grams}</span>
36
+ </span>
37
+ </div>
38
+
39
+ <div class="tg-measurement">
40
+ <span class="tg-measurement-label">{ui.ratioLabel}</span>
41
+ <span class="tg-measurement-value">
42
+ <span id="tg-r-ratio">0.75</span>
43
+ <span class="tg-unit">%</span>
44
+ </span>
45
+ </div>
46
+
47
+ <div id="tg-r-incubation" class="tg-incubation-badge">
48
+ <div class="tg-incubation-badge-label">{ui.incubationLabel}</div>
49
+ <div class="tg-incubation-badge-value">
50
+ <span id="tg-r-inc-min">6</span> - <span id="tg-r-inc-max">24</span>
51
+ <span style="font-size: 0.55rem; color: var(--tg-ink-muted); margin-left: 0.15rem;">{ui.hours}</span>
52
+ </div>
53
+ </div>
54
+
55
+ <div id="tg-r-alert" class="tg-alert" style="display: none;">
56
+ <p class="tg-alert-text">{ui.enzymeDestroyed}</p>
57
+ </div>
58
+ </div>
59
+ </div>
@@ -0,0 +1,67 @@
1
+ ---
2
+ interface Props {
3
+ ui: Record<string, string>;
4
+ }
5
+
6
+ const { ui } = Astro.props;
7
+ ---
8
+
9
+ <div class="tg-specimen">
10
+ <div class="tg-specimen-header">
11
+ <div class="tg-specimen-dot"></div>
12
+ <span class="tg-specimen-label">Specimen Data</span>
13
+ </div>
14
+ <div class="tg-specimen-body">
15
+ <div class="tg-field">
16
+ <span class="tg-field-label">{ui.unitLabel}</span>
17
+ <div class="tg-toggle-group">
18
+ <button type="button" id="tg-unit-metric" class="tg-toggle active" data-unit="metric">
19
+ {ui.metricUnit}
20
+ </button>
21
+ <button type="button" id="tg-unit-imperial" class="tg-toggle" data-unit="imperial">
22
+ {ui.imperialUnit}
23
+ </button>
24
+ </div>
25
+ </div>
26
+
27
+ <div class="tg-field">
28
+ <span class="tg-field-label">{ui.methodLabel}</span>
29
+ <div class="tg-toggle-group">
30
+ <button type="button" id="tg-method-dry" class="tg-toggle active" data-method="dry">
31
+ {ui.dryMethod}
32
+ </button>
33
+ <button type="button" id="tg-method-slurry" class="tg-toggle" data-method="slurry">
34
+ {ui.slurryMethod}
35
+ </button>
36
+ </div>
37
+ </div>
38
+
39
+ <div class="tg-field">
40
+ <label for="tg-weight-input" class="tg-field-label">
41
+ {ui.weightLabel} (<span id="tg-weight-unit">g</span>)
42
+ </label>
43
+ <input type="number" id="tg-weight-input" min="0" max="50000" step="any" value="1000" class="tg-input" inputmode="decimal" />
44
+ </div>
45
+
46
+ <div class="tg-field">
47
+ <label for="tg-protein-select" class="tg-field-label">{ui.proteinLabel}</label>
48
+ <select id="tg-protein-select" class="tg-select">
49
+ <option value="red-meat">{ui.redMeat}</option>
50
+ <option value="poultry">{ui.poultry}</option>
51
+ <option value="fish">{ui.fish}</option>
52
+ <option value="shellfish">{ui.shellfish}</option>
53
+ </select>
54
+ </div>
55
+
56
+ <div class="tg-field">
57
+ <label for="tg-temp-input" class="tg-field-label">
58
+ {ui.tempLabel} (<span id="tg-temp-unit">°C</span>)
59
+ </label>
60
+ <input type="number" id="tg-temp-input" min="0" max="80" step="0.5" value="4" class="tg-input" inputmode="decimal" />
61
+ </div>
62
+ </div>
63
+
64
+ <div class="tg-annotation">
65
+ <p id="tg-method-desc" class="tg-annotation-text">{ui.dryDustingDesc}</p>
66
+ </div>
67
+ </div>
@@ -0,0 +1,137 @@
1
+ <div class="tg-visualizer">
2
+ <div class="tg-visualizer-header">
3
+ <span class="tg-visualizer-label">Tissue Cross-Linking</span>
4
+ <span id="tg-status-label" class="tg-visualizer-status">incubating / 4°C</span>
5
+ </div>
6
+ <div class="tg-visualizer-body">
7
+ <svg class="tg-visualizer-svg" viewBox="0 0 500 320" xmlns="http://www.w3.org/2000/svg">
8
+ <defs>
9
+ <filter id="tg-shadow">
10
+ <feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.15" />
11
+ </filter>
12
+ <filter id="tg-glow">
13
+ <feGaussianBlur stdDeviation="2.5" result="blur" />
14
+ <feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge>
15
+ </filter>
16
+ <clipPath id="clip-a">
17
+ <path d="M 15 50 C 15 15, 75 20, 120 30 C 160 40, 185 60, 188 85 C 190 110, 182 135, 175 160 C 168 185, 155 205, 135 225 C 115 245, 85 260, 55 265 C 25 270, 15 255, 15 230 Z" />
18
+ </clipPath>
19
+ <clipPath id="clip-b">
20
+ <path d="M 485 55 C 485 20, 425 25, 380 35 C 340 45, 315 65, 312 90 C 310 115, 318 140, 325 165 C 332 190, 345 210, 365 230 C 385 250, 415 265, 445 270 C 475 275, 485 260, 485 235 Z" />
21
+ </clipPath>
22
+ </defs>
23
+
24
+ <g filter="url(#tg-shadow)">
25
+ <path id="tg-shape-a" d="M 15 50 C 15 15, 75 20, 120 30 C 160 40, 185 60, 188 85 C 190 110, 182 135, 175 160 C 168 185, 155 205, 135 225 C 115 245, 85 260, 55 265 C 25 270, 15 255, 15 230 Z" class="tg-tissue-a" />
26
+ </g>
27
+
28
+ <g clip-path="url(#clip-a)" id="tg-fibers-a">
29
+ <path d="M 25 65 Q 60 60, 100 70 T 180 75" stroke-width="1.2" />
30
+ <path d="M 20 95 Q 55 90, 95 100 T 185 105" stroke-width="1.2" />
31
+ <path d="M 25 125 Q 65 120, 105 130 T 180 135" stroke-width="1.2" />
32
+ <path d="M 20 155 Q 60 150, 100 160 T 185 165" stroke-width="1.2" />
33
+ <path d="M 25 185 Q 65 180, 105 190 T 175 195" stroke-width="1.2" />
34
+ <path d="M 20 215 Q 55 210, 95 220 T 165 225" stroke-width="1.2" />
35
+ <path d="M 25 245 Q 55 240, 85 250 T 130 255" stroke-width="1.2" />
36
+ <line x1="60" y1="63" x2="60" y2="78" stroke-width="0.6" opacity="0.4" />
37
+ <line x1="60" y1="93" x2="60" y2="108" stroke-width="0.6" opacity="0.4" />
38
+ <line x1="60" y1="123" x2="60" y2="138" stroke-width="0.6" opacity="0.4" />
39
+ <line x1="60" y1="153" x2="60" y2="168" stroke-width="0.6" opacity="0.4" />
40
+ <line x1="60" y1="183" x2="60" y2="198" stroke-width="0.6" opacity="0.4" />
41
+ <line x1="60" y1="213" x2="60" y2="228" stroke-width="0.6" opacity="0.4" />
42
+ <line x1="110" y1="63" x2="110" y2="78" stroke-width="0.6" opacity="0.4" />
43
+ <line x1="110" y1="93" x2="110" y2="108" stroke-width="0.6" opacity="0.4" />
44
+ <line x1="110" y1="123" x2="110" y2="138" stroke-width="0.6" opacity="0.4" />
45
+ <line x1="110" y1="153" x2="110" y2="168" stroke-width="0.6" opacity="0.4" />
46
+ <line x1="110" y1="183" x2="110" y2="198" stroke-width="0.6" opacity="0.4" />
47
+ <line x1="110" y1="213" x2="110" y2="228" stroke-width="0.6" opacity="0.4" />
48
+ <line x1="150" y1="63" x2="150" y2="78" stroke-width="0.6" opacity="0.4" />
49
+ <line x1="150" y1="93" x2="150" y2="108" stroke-width="0.6" opacity="0.4" />
50
+ <line x1="150" y1="123" x2="150" y2="138" stroke-width="0.6" opacity="0.4" />
51
+ <line x1="150" y1="183" x2="150" y2="198" stroke-width="0.6" opacity="0.4" />
52
+ </g>
53
+
54
+ <g filter="url(#tg-shadow)">
55
+ <path id="tg-shape-b" d="M 485 55 C 485 20, 425 25, 380 35 C 340 45, 315 65, 312 90 C 310 115, 318 140, 325 165 C 332 190, 345 210, 365 230 C 385 250, 415 265, 445 270 C 475 275, 485 260, 485 235 Z" class="tg-tissue-b" />
56
+ </g>
57
+
58
+ <g clip-path="url(#clip-b)" id="tg-fibers-b">
59
+ <path d="M 475 70 Q 440 65, 400 75 T 320 80" stroke-width="1.2" />
60
+ <path d="M 480 100 Q 445 95, 405 105 T 315 110" stroke-width="1.2" />
61
+ <path d="M 475 130 Q 435 125, 395 135 T 320 140" stroke-width="1.2" />
62
+ <path d="M 480 160 Q 440 155, 400 165 T 315 170" stroke-width="1.2" />
63
+ <path d="M 475 190 Q 435 185, 395 195 T 325 200" stroke-width="1.2" />
64
+ <path d="M 480 220 Q 445 215, 405 225 T 335 230" stroke-width="1.2" />
65
+ <path d="M 475 250 Q 445 245, 415 255 T 370 260" stroke-width="1.2" />
66
+ <line x1="440" y1="68" x2="440" y2="83" stroke-width="0.6" opacity="0.4" />
67
+ <line x1="440" y1="98" x2="440" y2="113" stroke-width="0.6" opacity="0.4" />
68
+ <line x1="440" y1="128" x2="440" y2="143" stroke-width="0.6" opacity="0.4" />
69
+ <line x1="440" y1="158" x2="440" y2="173" stroke-width="0.6" opacity="0.4" />
70
+ <line x1="440" y1="188" x2="440" y2="203" stroke-width="0.6" opacity="0.4" />
71
+ <line x1="440" y1="218" x2="440" y2="233" stroke-width="0.6" opacity="0.4" />
72
+ <line x1="390" y1="68" x2="390" y2="83" stroke-width="0.6" opacity="0.4" />
73
+ <line x1="390" y1="98" x2="390" y2="113" stroke-width="0.6" opacity="0.4" />
74
+ <line x1="390" y1="128" x2="390" y2="143" stroke-width="0.6" opacity="0.4" />
75
+ <line x1="390" y1="158" x2="390" y2="173" stroke-width="0.6" opacity="0.4" />
76
+ <line x1="390" y1="188" x2="390" y2="203" stroke-width="0.6" opacity="0.4" />
77
+ <line x1="390" y1="218" x2="390" y2="233" stroke-width="0.6" opacity="0.4" />
78
+ <line x1="350" y1="68" x2="350" y2="83" stroke-width="0.6" opacity="0.4" />
79
+ <line x1="350" y1="98" x2="350" y2="113" stroke-width="0.6" opacity="0.4" />
80
+ <line x1="350" y1="128" x2="350" y2="143" stroke-width="0.6" opacity="0.4" />
81
+ <line x1="350" y1="188" x2="350" y2="203" stroke-width="0.6" opacity="0.4" />
82
+ </g>
83
+
84
+ <rect id="tg-denatured-overlay" x="0" y="0" width="500" height="320" class="tg-denatured-overlay" />
85
+
86
+ <g id="tg-bonds">
87
+ <line x1="188" y1="75" x2="312" y2="85" id="tg-bond-1" class="tg-bond-line" stroke-width="2" />
88
+ <line x1="185" y1="115" x2="315" y2="125" id="tg-bond-2" class="tg-bond-line" stroke-width="2" />
89
+ <line x1="188" y1="155" x2="312" y2="160" id="tg-bond-3" class="tg-bond-line" stroke-width="2" />
90
+ <line x1="182" y1="195" x2="318" y2="200" id="tg-bond-4" class="tg-bond-line" stroke-width="2" />
91
+ <line x1="175" y1="235" x2="325" y2="230" id="tg-bond-5" class="tg-bond-line" stroke-width="2" />
92
+ </g>
93
+
94
+ <g id="tg-nodes">
95
+ <circle cx="188" cy="75" r="4" id="tg-node-1a" class="tg-bond-node" />
96
+ <circle cx="312" cy="85" r="4" id="tg-node-1b" class="tg-bond-node" />
97
+ <circle cx="185" cy="115" r="4" id="tg-node-2a" class="tg-bond-node" />
98
+ <circle cx="315" cy="125" r="4" id="tg-node-2b" class="tg-bond-node" />
99
+ <circle cx="188" cy="155" r="4" id="tg-node-3a" class="tg-bond-node" />
100
+ <circle cx="312" cy="160" r="4" id="tg-node-3b" class="tg-bond-node" />
101
+ <circle cx="182" cy="195" r="4" id="tg-node-4a" class="tg-bond-node" />
102
+ <circle cx="318" cy="200" r="4" id="tg-node-4b" class="tg-bond-node" />
103
+ <circle cx="175" cy="235" r="4" id="tg-node-5a" class="tg-bond-node" />
104
+ <circle cx="325" cy="230" r="4" id="tg-node-5b" class="tg-bond-node" />
105
+ </g>
106
+
107
+ <g id="tg-particles">
108
+ <circle id="tg-particle-1" cx="0" cy="0" r="4" class="tg-particle" filter="url(#tg-glow)" />
109
+ <circle id="tg-particle-2" cx="0" cy="0" r="4" class="tg-particle" filter="url(#tg-glow)" />
110
+ <circle id="tg-particle-3" cx="0" cy="0" r="4" class="tg-particle" filter="url(#tg-glow)" />
111
+ <circle id="tg-particle-4" cx="0" cy="0" r="4" class="tg-particle" filter="url(#tg-glow)" />
112
+ </g>
113
+
114
+ <text id="tg-method-label" x="250" y="20" text-anchor="middle" fill="var(--tg-ink-faint)" font-size="8" letter-spacing="0.15em" font-weight="600">DRY DUSTING</text>
115
+ <line x1="195" y1="28" x2="305" y2="28" stroke="var(--tg-ink-faint)" stroke-width="0.5" />
116
+ <line x1="195" y1="25" x2="195" y2="31" stroke="var(--tg-ink-faint)" stroke-width="0.5" />
117
+ <line x1="305" y1="25" x2="305" y2="31" stroke="var(--tg-ink-faint)" stroke-width="0.5" />
118
+
119
+ <text x="250" y="303" text-anchor="middle" fill="var(--tg-ink-faint)" font-size="8" letter-spacing="0.08em">
120
+ <tspan id="tg-bonds-formed">0</tspan>
121
+ <tspan> / 5 covalent bonds formed</tspan>
122
+ </text>
123
+ </svg>
124
+
125
+ <div class="tg-temp-bar">
126
+ <div class="tg-temp-bar-label">TEMP</div>
127
+ <div class="tg-temp-bar-track">
128
+ <div id="tg-temp-fill" class="tg-temp-bar-fill" style="height: 14%; bottom: 0;"></div>
129
+ <div class="tg-temp-bar-tick" style="bottom: 0%;"><span>0°C</span></div>
130
+ <div class="tg-temp-bar-tick" style="bottom: 33%;"><span>20°C</span></div>
131
+ <div class="tg-temp-bar-tick" style="bottom: 67%;"><span>40°C</span></div>
132
+ <div class="tg-temp-bar-tick" style="bottom: 100%;"><span>60°C</span></div>
133
+ </div>
134
+ <div id="tg-temp-display" class="tg-temp-bar-value">4°C</div>
135
+ </div>
136
+ </div>
137
+ </div>
@@ -0,0 +1,26 @@
1
+ import type { CookingToolEntry } from '../../types';
2
+
3
+ export const meatBinder: CookingToolEntry = {
4
+ id: 'meat-binder-transglutaminase-calculator',
5
+ icons: {
6
+ bg: 'mdi:meat-outline',
7
+ fg: 'mdi:link-variant',
8
+ },
9
+ i18n: {
10
+ en: () => import('./i18n/en').then((m) => m.content),
11
+ de: () => import('./i18n/de').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
+ };