@jjlmoya/utils-cooking 1.32.0 → 1.33.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 (34) hide show
  1. package/package.json +2 -2
  2. package/src/category/index.ts +2 -0
  3. package/src/entries.ts +3 -1
  4. package/src/index.ts +1 -0
  5. package/src/tests/i18n-titles.test.ts +1 -1
  6. package/src/tests/locale_completeness.test.ts +2 -2
  7. package/src/tests/tool_validation.test.ts +2 -2
  8. package/src/tool/meat-binder-transglutaminase-calculator/bibliography.astro +6 -0
  9. package/src/tool/meat-binder-transglutaminase-calculator/bibliography.ts +14 -0
  10. package/src/tool/meat-binder-transglutaminase-calculator/component.astro +249 -0
  11. package/src/tool/meat-binder-transglutaminase-calculator/components/LabReport.astro +59 -0
  12. package/src/tool/meat-binder-transglutaminase-calculator/components/TissueSpecimen.astro +67 -0
  13. package/src/tool/meat-binder-transglutaminase-calculator/components/TissueViewer.astro +137 -0
  14. package/src/tool/meat-binder-transglutaminase-calculator/entry.ts +26 -0
  15. package/src/tool/meat-binder-transglutaminase-calculator/i18n/de.ts +178 -0
  16. package/src/tool/meat-binder-transglutaminase-calculator/i18n/en.ts +178 -0
  17. package/src/tool/meat-binder-transglutaminase-calculator/i18n/es.ts +178 -0
  18. package/src/tool/meat-binder-transglutaminase-calculator/i18n/fr.ts +178 -0
  19. package/src/tool/meat-binder-transglutaminase-calculator/i18n/id.ts +178 -0
  20. package/src/tool/meat-binder-transglutaminase-calculator/i18n/it.ts +178 -0
  21. package/src/tool/meat-binder-transglutaminase-calculator/i18n/ja.ts +178 -0
  22. package/src/tool/meat-binder-transglutaminase-calculator/i18n/ko.ts +178 -0
  23. package/src/tool/meat-binder-transglutaminase-calculator/i18n/nl.ts +178 -0
  24. package/src/tool/meat-binder-transglutaminase-calculator/i18n/pl.ts +178 -0
  25. package/src/tool/meat-binder-transglutaminase-calculator/i18n/pt.ts +178 -0
  26. package/src/tool/meat-binder-transglutaminase-calculator/i18n/ru.ts +178 -0
  27. package/src/tool/meat-binder-transglutaminase-calculator/i18n/sv.ts +178 -0
  28. package/src/tool/meat-binder-transglutaminase-calculator/i18n/tr.ts +178 -0
  29. package/src/tool/meat-binder-transglutaminase-calculator/i18n/zh.ts +178 -0
  30. package/src/tool/meat-binder-transglutaminase-calculator/index.ts +11 -0
  31. package/src/tool/meat-binder-transglutaminase-calculator/logic.ts +66 -0
  32. package/src/tool/meat-binder-transglutaminase-calculator/meat-binder-transglutaminase-calculator.css +785 -0
  33. package/src/tool/meat-binder-transglutaminase-calculator/seo.astro +15 -0
  34. package/src/tools.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-cooking",
3
- "version": "1.32.0",
3
+ "version": "1.33.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -17,7 +17,7 @@
17
17
  "access": "public"
18
18
  },
19
19
  "scripts": {
20
- "dev": "astro dev",
20
+ "dev": "astro dev --host",
21
21
  "start": "astro dev",
22
22
  "build": "astro build",
23
23
  "preview": "astro preview",
@@ -15,6 +15,7 @@ import { lactoFermentationSalt } from '../tool/lacto-fermentation-salt-calculato
15
15
  import { spherificationBath } from '../tool/spherification-bath-calculator/entry';
16
16
  import { iceCreamPacPod } from '../tool/ice-cream-pac-pod/entry';
17
17
  import { botulismCanningSafety } from '../tool/botulism-canning-safety/entry';
18
+ import { meatBinder } from '../tool/meat-binder-transglutaminase-calculator/entry';
18
19
 
19
20
  export const cookingCategory: CookingCategoryEntry = {
20
21
  icon: 'mdi:chef-hat',
@@ -35,6 +36,7 @@ export const cookingCategory: CookingCategoryEntry = {
35
36
  spherificationBath,
36
37
  iceCreamPacPod,
37
38
  botulismCanningSafety,
39
+ meatBinder,
38
40
  ],
39
41
 
40
42
  i18n: {
package/src/entries.ts CHANGED
@@ -14,6 +14,7 @@ export { yeastConverter } from './tool/yeast-converter/entry';
14
14
  export { lactoFermentationSalt } from './tool/lacto-fermentation-salt-calculator/entry';
15
15
  export { spherificationBath } from './tool/spherification-bath-calculator/entry';
16
16
  export { iceCreamPacPod } from './tool/ice-cream-pac-pod/entry';
17
+ export { meatBinder } from './tool/meat-binder-transglutaminase-calculator/entry';
17
18
  export { botulismCanningSafety } from './tool/botulism-canning-safety/entry';
18
19
  export { cookingCategory } from './category';
19
20
  import { americanKitchenConverter } from './tool/american-kitchen-converter/entry';
@@ -33,5 +34,6 @@ import { lactoFermentationSalt } from './tool/lacto-fermentation-salt-calculator
33
34
  import { spherificationBath } from './tool/spherification-bath-calculator/entry';
34
35
  import { iceCreamPacPod } from './tool/ice-cream-pac-pod/entry';
35
36
  import { botulismCanningSafety } from './tool/botulism-canning-safety/entry';
36
- export const ALL_ENTRIES = [americanKitchenConverter, bananaCare, brine, cookwareGuide, eggTimer, ingredientRescaler, kitchenTimer, meringuePeak, moldScaler, pizza, rouxGuide, sourdoughCalculator, yeastConverter, lactoFermentationSalt, spherificationBath, iceCreamPacPod, botulismCanningSafety];
37
+ import { meatBinder } from './tool/meat-binder-transglutaminase-calculator/entry';
38
+ export const ALL_ENTRIES = [americanKitchenConverter, bananaCare, brine, cookwareGuide, eggTimer, ingredientRescaler, kitchenTimer, meringuePeak, moldScaler, pizza, rouxGuide, sourdoughCalculator, yeastConverter, lactoFermentationSalt, spherificationBath, iceCreamPacPod, botulismCanningSafety, meatBinder];
37
39
 
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ export { LACTO_FERMENTATION_SALT_TOOL } from './tool/lacto-fermentation-salt-cal
18
18
  export { SPHERIFICATION_BATH_TOOL } from './tool/spherification-bath-calculator';
19
19
  export { ICE_CREAM_PAC_POD_TOOL } from './tool/ice-cream-pac-pod';
20
20
  export { BOTULISM_CANNING_SAFETY_TOOL } from './tool/botulism-canning-safety';
21
+ export { MEAT_BINDER_TRANSGLUTAMINASE_TOOL } from './tool/meat-binder-transglutaminase-calculator';
21
22
 
22
23
 
23
24
  export type {
@@ -36,7 +36,7 @@ describe("i18n titles for FAQ", () => {
36
36
 
37
37
  it("should have 17 tools with complete i18n setup", async () => {
38
38
  const completeTools = ALL_TOOLS.filter((t) => Object.keys(t.entry.i18n).length > 1);
39
- expect(completeTools.length).toBe(17);
39
+ expect(completeTools.length).toBe(18);
40
40
  });
41
41
 
42
42
  it("tool IDs should be correctly registered", () => {
@@ -24,8 +24,8 @@ describe('Locale Completeness Validation', () => {
24
24
  });
25
25
  });
26
26
 
27
- it('all 17 tools registered', () => {
28
- expect(ALL_TOOLS.length).toBe(17);
27
+ it('all 18 tools registered', () => {
28
+ expect(ALL_TOOLS.length).toBe(18);
29
29
  });
30
30
 
31
31
  });
@@ -4,8 +4,8 @@ import { cookingCategory } from '../data';
4
4
 
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
- it('should have 17 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(17);
7
+ it('should have 18 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(18);
9
9
  });
10
10
 
11
11
 
@@ -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: 'Modernist Cuisine: The Art and Science of Cooking',
4
+ url: 'https://modernistcuisine.com/books/modernist-cuisine/',
5
+ },
6
+ {
7
+ name: 'Cooking for Geeks: Real Science, Great Hacks, and Good Food',
8
+ url: 'https://www.cookingforgeeks.com/',
9
+ },
10
+ {
11
+ name: 'The Science of Transglutaminase in Food Processing',
12
+ url: 'https://www.sciencedirect.com/science/article/pii/S0924224498000387',
13
+ },
14
+ ];
@@ -0,0 +1,249 @@
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
+ }
93
+
94
+ function getWeightGrams() {
95
+ const v = parseFloat(weightInput.value) || 0;
96
+ return unit === 'imperial' ? ozToG(v) : v;
97
+ }
98
+
99
+ function getTempCelsius() {
100
+ const v = parseFloat(tempInput.value) || 0;
101
+ return unit === 'imperial' ? fToC(v) : v;
102
+ }
103
+
104
+ function setDisplayWeight(g) {
105
+ weightInput.value = unit === 'imperial' ? gToOz(g).toFixed(1) : Math.round(g).toString();
106
+ }
107
+
108
+ function updateTissueColors(p) {
109
+ const c = TISSUE[p] || TISSUE['red-meat'];
110
+ const root = document.querySelector('.tg');
111
+ root.style.setProperty('--tg-tissue-a-current', c.base);
112
+ root.style.setProperty('--tg-tissue-b-current', c.dark);
113
+ root.style.setProperty('--tg-fiber-current', c.fiber);
114
+ }
115
+
116
+ function updateBonds(n) {
117
+ bondEls.forEach((b, i) => b.classList.toggle('active', i < n));
118
+ nodeEls.forEach((nd, i) => nd.classList.toggle('active', i < n * 2));
119
+ bondsFormed.textContent = Math.min(n, bondEls.length);
120
+ }
121
+
122
+ let particleInterval = null;
123
+
124
+ function startParticles() {
125
+ stopParticles();
126
+ const baseX = 188, baseY = [75, 115, 155, 195], targets = [85, 125, 160, 200];
127
+ let frame = 0;
128
+ particleInterval = setInterval(() => {
129
+ frame = (frame + 1) % 120;
130
+ particles.forEach((p, i) => {
131
+ const idx = (frame + i * 30) % 120;
132
+ if (idx >= 60) { p.setAttribute('opacity', '0'); return; }
133
+ const t = idx / 60;
134
+ p.setAttribute('cx', (baseX + t * 124).toString());
135
+ p.setAttribute('cy', (baseY[i] + t * (targets[i] - baseY[i])).toString());
136
+ p.setAttribute('opacity', (Math.sin(t * Math.PI) * 0.9).toString());
137
+ p.classList.add('active');
138
+ });
139
+ }, 25);
140
+ }
141
+
142
+ function stopParticles() {
143
+ if (particleInterval) { clearInterval(particleInterval); particleInterval = null; }
144
+ particles.forEach(p => { p.setAttribute('opacity', '0'); p.classList.remove('active'); });
145
+ }
146
+
147
+ function updateTempGauge(tempC) {
148
+ const display = unit === 'imperial' ? cToF(tempC) : tempC;
149
+ tempDisplay.textContent = display.toFixed(1) + (unit === 'imperial' ? '°F' : '°C');
150
+ const pct = Math.max(0, Math.min(100, (tempC / 60) * 100));
151
+ tempFill.style.height = pct + '%';
152
+ tempFill.classList.remove('warm', 'hot');
153
+ if (tempC >= 55) tempFill.classList.add('hot');
154
+ else if (tempC >= 35) tempFill.classList.add('warm');
155
+ }
156
+
157
+ function computeResult(meatWeightG, tempC, proteinType) {
158
+ if (tempC >= 60) return { enzymeDestroyed: true, tgGrams: 0, waterMl: 0, totalG: meatWeightG, ratioPct: 0, hMin: 0, hMax: 0 };
159
+
160
+ const ratios = RATIO_MAP[proteinType] || RATIO_MAP['red-meat'];
161
+ const midRatio = (ratios[0] + ratios[1]) / 2;
162
+ const tgGrams = parseFloat((meatWeightG * midRatio).toFixed(2));
163
+ const waterMl = method === 'slurry' ? parseFloat((tgGrams * 4).toFixed(1)) : 0;
164
+ const totalG = method === 'slurry'
165
+ ? parseFloat((meatWeightG + tgGrams + waterMl).toFixed(2))
166
+ : parseFloat((meatWeightG + tgGrams).toFixed(2));
167
+
168
+ let hMin = 6, hMax = 24;
169
+ for (const r of TEMP_RANGES) {
170
+ if (tempC >= r.min && tempC < r.max) { hMin = r.hMin; hMax = r.hMax; break; }
171
+ }
172
+ return { enzymeDestroyed: false, tgGrams, waterMl, totalG, ratioPct: parseFloat((midRatio * 100).toFixed(2)), hMin, hMax };
173
+ }
174
+
175
+ function renderDenatured(meatWeightG) {
176
+ rTg.textContent = rWater.textContent = '0';
177
+ rTotal.textContent = (unit === 'imperial' ? gToOz(meatWeightG) : meatWeightG).toFixed(2);
178
+ rRatio.textContent = rIncMin.textContent = rIncMax.textContent = '0';
179
+ rAlert.style.display = 'block'; rIncubation.style.display = 'none';
180
+ rWaterRow.style.display = method === 'slurry' ? 'flex' : 'none';
181
+ statusLabel.textContent = 'denatured'; statusLabel.style.color = '#dc2626';
182
+ denaturedOverlay.classList.add('active');
183
+ updateBonds(0); stopParticles();
184
+ }
185
+
186
+ function renderResult(r, meatWeightG) {
187
+ if (r.enzymeDestroyed) { renderDenatured(meatWeightG); return; }
188
+ denaturedOverlay.classList.remove('active');
189
+ statusLabel.textContent = 'incubating'; statusLabel.style.color = '';
190
+ rAlert.style.display = 'none'; rIncubation.style.display = 'block';
191
+ const f = unit === 'imperial' ? 0.035274 : 1;
192
+ rTg.textContent = (r.tgGrams * f).toFixed(2);
193
+ rTotal.textContent = (r.totalG * f).toFixed(2);
194
+ rRatio.textContent = r.ratioPct.toFixed(2);
195
+ rIncMin.textContent = r.hMin; rIncMax.textContent = r.hMax;
196
+ if (method === 'slurry') {
197
+ rWater.textContent = (r.waterMl * f).toFixed(1); rWaterRow.style.display = 'flex';
198
+ methodDesc.textContent = ui.slurryDesc;
199
+ } else {
200
+ rWater.textContent = '0'; rWaterRow.style.display = 'none';
201
+ methodDesc.textContent = ui.dryDustingDesc;
202
+ }
203
+ const n = Math.round(Math.min(1, meatWeightG / 4000) * 5);
204
+ updateBonds(n);
205
+ if (n > 0) startParticles(); else stopParticles();
206
+ }
207
+
208
+ function updateView() {
209
+ updateUnits();
210
+ methodLabel.textContent = method === 'dry' ? 'DRY DUSTING' : 'SLURRY BONDING';
211
+ const meatWeightG = getWeightGrams(), tempC = getTempCelsius(), proteinType = proteinSelect.value;
212
+ updateTissueColors(proteinType);
213
+ updateTempGauge(tempC);
214
+ renderResult(computeResult(meatWeightG, tempC, proteinType), meatWeightG);
215
+ saveState();
216
+ }
217
+
218
+ function switchUnit(to) {
219
+ if (unit === to) return;
220
+ const prevG = getWeightGrams(), prevC = getTempCelsius();
221
+ unit = to;
222
+ unitMetric.classList.toggle('active', to === 'metric');
223
+ unitImperial.classList.toggle('active', to === 'imperial');
224
+ setDisplayWeight(prevG);
225
+ tempInput.value = to === 'imperial' ? cToF(prevC).toFixed(1) : prevC.toFixed(1);
226
+ updateView();
227
+ }
228
+
229
+ function switchMethod(to) {
230
+ if (method === to) return;
231
+ method = to;
232
+ methodDry.classList.toggle('active', to === 'dry');
233
+ methodSlurry.classList.toggle('active', to === 'slurry');
234
+ updateView();
235
+ }
236
+
237
+ unitMetric.addEventListener('click', () => switchUnit('metric'));
238
+ unitImperial.addEventListener('click', () => switchUnit('imperial'));
239
+ methodDry.addEventListener('click', () => switchMethod('dry'));
240
+ methodSlurry.addEventListener('click', () => switchMethod('slurry'));
241
+ weightInput.addEventListener('input', updateView);
242
+ proteinSelect.addEventListener('change', updateView);
243
+ tempInput.addEventListener('input', updateView);
244
+
245
+ loadState();
246
+ applyStateUI();
247
+ updateUnits();
248
+ updateView();
249
+ </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 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 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 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>