@jjlmoya/utils-cooking 1.28.0 → 1.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/package.json +1 -1
  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 +3 -2
  6. package/src/tests/locale_completeness.test.ts +2 -2
  7. package/src/tests/spherification-bath-calculator.test.ts +57 -0
  8. package/src/tests/tool_validation.test.ts +2 -2
  9. package/src/tool/spherification-bath-calculator/bibliography.astro +6 -0
  10. package/src/tool/spherification-bath-calculator/bibliography.ts +10 -0
  11. package/src/tool/spherification-bath-calculator/component.astro +213 -0
  12. package/src/tool/spherification-bath-calculator/components/PrecisionControls.astro +85 -0
  13. package/src/tool/spherification-bath-calculator/components/RecipeSummary.astro +60 -0
  14. package/src/tool/spherification-bath-calculator/components/SpherificationReactor.astro +73 -0
  15. package/src/tool/spherification-bath-calculator/entry.ts +26 -0
  16. package/src/tool/spherification-bath-calculator/i18n/de.ts +180 -0
  17. package/src/tool/spherification-bath-calculator/i18n/en.ts +180 -0
  18. package/src/tool/spherification-bath-calculator/i18n/es.ts +179 -0
  19. package/src/tool/spherification-bath-calculator/i18n/fr.ts +179 -0
  20. package/src/tool/spherification-bath-calculator/i18n/id.ts +180 -0
  21. package/src/tool/spherification-bath-calculator/i18n/it.ts +180 -0
  22. package/src/tool/spherification-bath-calculator/i18n/ja.ts +180 -0
  23. package/src/tool/spherification-bath-calculator/i18n/ko.ts +180 -0
  24. package/src/tool/spherification-bath-calculator/i18n/nl.ts +180 -0
  25. package/src/tool/spherification-bath-calculator/i18n/pl.ts +180 -0
  26. package/src/tool/spherification-bath-calculator/i18n/pt.ts +180 -0
  27. package/src/tool/spherification-bath-calculator/i18n/ru.ts +180 -0
  28. package/src/tool/spherification-bath-calculator/i18n/sv.ts +180 -0
  29. package/src/tool/spherification-bath-calculator/i18n/tr.ts +180 -0
  30. package/src/tool/spherification-bath-calculator/i18n/zh.ts +180 -0
  31. package/src/tool/spherification-bath-calculator/index.ts +11 -0
  32. package/src/tool/spherification-bath-calculator/logic.ts +54 -0
  33. package/src/tool/spherification-bath-calculator/seo.astro +15 -0
  34. package/src/tool/spherification-bath-calculator/spherification-bath-calculator.css +568 -0
  35. 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.28.0",
3
+ "version": "1.29.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -12,6 +12,7 @@ import { sourdoughCalculator } from '../tool/sourdough-calculator/entry';
12
12
  import { rouxGuide } from '../tool/roux-guide/entry';
13
13
  import { cookwareGuide } from '../tool/cookware-guide/entry';
14
14
  import { lactoFermentationSalt } from '../tool/lacto-fermentation-salt-calculator/entry';
15
+ import { spherificationBath } from '../tool/spherification-bath-calculator/entry';
15
16
 
16
17
  export const cookingCategory: CookingCategoryEntry = {
17
18
  icon: 'mdi:chef-hat',
@@ -29,6 +30,7 @@ export const cookingCategory: CookingCategoryEntry = {
29
30
  rouxGuide,
30
31
  cookwareGuide,
31
32
  lactoFermentationSalt,
33
+ spherificationBath,
32
34
  ],
33
35
  i18n: {
34
36
  es: () => import('./i18n/es').then((m) => m.content),
package/src/entries.ts CHANGED
@@ -12,6 +12,7 @@ export { rouxGuide } from './tool/roux-guide/entry';
12
12
  export { sourdoughCalculator } from './tool/sourdough-calculator/entry';
13
13
  export { yeastConverter } from './tool/yeast-converter/entry';
14
14
  export { lactoFermentationSalt } from './tool/lacto-fermentation-salt-calculator/entry';
15
+ export { spherificationBath } from './tool/spherification-bath-calculator/entry';
15
16
  export { cookingCategory } from './category';
16
17
  import { americanKitchenConverter } from './tool/american-kitchen-converter/entry';
17
18
  import { bananaCare } from './tool/banana-ripeness/entry';
@@ -27,4 +28,5 @@ import { rouxGuide } from './tool/roux-guide/entry';
27
28
  import { sourdoughCalculator } from './tool/sourdough-calculator/entry';
28
29
  import { yeastConverter } from './tool/yeast-converter/entry';
29
30
  import { lactoFermentationSalt } from './tool/lacto-fermentation-salt-calculator/entry';
30
- export const ALL_ENTRIES = [americanKitchenConverter, bananaCare, brine, cookwareGuide, eggTimer, ingredientRescaler, kitchenTimer, meringuePeak, moldScaler, pizza, rouxGuide, sourdoughCalculator, yeastConverter, lactoFermentationSalt];
31
+ import { spherificationBath } from './tool/spherification-bath-calculator/entry';
32
+ export const ALL_ENTRIES = [americanKitchenConverter, bananaCare, brine, cookwareGuide, eggTimer, ingredientRescaler, kitchenTimer, meringuePeak, moldScaler, pizza, rouxGuide, sourdoughCalculator, yeastConverter, lactoFermentationSalt, spherificationBath];
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export { ROUX_GUIDE_TOOL } from './tool/roux-guide';
15
15
  export { COOKWARE_GUIDE_TOOL } from './tool/cookware-guide';
16
16
  export { YEAST_CONVERTER_TOOL } from './tool/yeast-converter';
17
17
  export { LACTO_FERMENTATION_SALT_TOOL } from './tool/lacto-fermentation-salt-calculator';
18
+ export { SPHERIFICATION_BATH_TOOL } from './tool/spherification-bath-calculator';
18
19
 
19
20
  export type {
20
21
  KnownLocale,
@@ -32,8 +32,8 @@ describe("i18n titles for FAQ", () => {
32
32
  }
33
33
  });
34
34
 
35
- it("should have 14 tools with complete i18n setup", async () => {
36
- expect(ALL_TOOLS.length).toBe(14);
35
+ it("should have 15 tools with complete i18n setup", async () => {
36
+ expect(ALL_TOOLS.length).toBe(15);
37
37
  });
38
38
 
39
39
  it("tool IDs should be correctly registered", () => {
@@ -51,5 +51,6 @@ describe("i18n titles for FAQ", () => {
51
51
  expect(toolIds).toContain("roux-guide");
52
52
  expect(toolIds).toContain("cookware-guide");
53
53
  expect(toolIds).toContain("lacto-fermentation-salt-calculator");
54
+ expect(toolIds).toContain("spherification-bath-calculator");
54
55
  });
55
56
  });
@@ -24,8 +24,8 @@ describe('Locale Completeness Validation', () => {
24
24
  });
25
25
  });
26
26
 
27
- it('all 14 tools registered', () => {
28
- expect(ALL_TOOLS.length).toBe(14);
27
+ it('all 15 tools registered', () => {
28
+ expect(ALL_TOOLS.length).toBe(15);
29
29
  });
30
30
  });
31
31
 
@@ -0,0 +1,57 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { SpherificationLogic } from '../tool/spherification-bath-calculator/logic';
3
+
4
+ describe('Spherification Bath Calculator Logic', () => {
5
+ it('calculates direct spherification values correctly', () => {
6
+ const res = SpherificationLogic.calculate({
7
+ baseWeight: 500,
8
+ bathWeight: 1000,
9
+ method: 'direct',
10
+ useXanthan: false,
11
+ useCitrate: false,
12
+ });
13
+ expect(res.baseAgentName).toBe('Sodium Alginate');
14
+ expect(res.baseAgentGrams).toBe(2.5);
15
+ expect(res.bathAgentName).toBe('Calcium Chloride');
16
+ expect(res.bathAgentGrams).toBe(10);
17
+ expect(res.xanthanGrams).toBe(0);
18
+ expect(res.citrateGrams).toBe(0);
19
+ expect(res.solubilityWarning).toBe(false);
20
+ });
21
+
22
+ it('calculates reverse spherification values correctly', () => {
23
+ const res = SpherificationLogic.calculate({
24
+ baseWeight: 500,
25
+ bathWeight: 1000,
26
+ method: 'reverse',
27
+ useXanthan: true,
28
+ useCitrate: true,
29
+ });
30
+ expect(res.baseAgentName).toBe('Calcium Lactate Gluconate');
31
+ expect(res.baseAgentGrams).toBe(10);
32
+ expect(res.bathAgentName).toBe('Sodium Alginate');
33
+ expect(res.bathAgentGrams).toBe(5);
34
+ expect(res.xanthanGrams).toBe(1);
35
+ expect(res.citrateGrams).toBe(2.5);
36
+ });
37
+
38
+ it('triggers solubility warning under high concentration', () => {
39
+ const directRes = SpherificationLogic.calculate({
40
+ baseWeight: 1200,
41
+ bathWeight: 1000,
42
+ method: 'direct',
43
+ useXanthan: false,
44
+ useCitrate: false,
45
+ });
46
+ expect(directRes.solubilityWarning).toBe(true);
47
+
48
+ const reverseRes = SpherificationLogic.calculate({
49
+ baseWeight: 500,
50
+ bathWeight: 2200,
51
+ method: 'reverse',
52
+ useXanthan: false,
53
+ useCitrate: false,
54
+ });
55
+ expect(reverseRes.solubilityWarning).toBe(true);
56
+ });
57
+ });
@@ -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 14 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(14);
7
+ it('should have 15 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(15);
9
9
  });
10
10
 
11
11
  it('cookingCategory should be defined', () => {
@@ -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,10 @@
1
+ export const bibliography = [
2
+ {
3
+ name: 'Modernist Cuisine: The Art and Science of Cooking (Nathan Myhrvold)',
4
+ url: 'https://modernistcuisine.com/books/modernist-cuisine/',
5
+ },
6
+ {
7
+ name: 'Science and Cooking: Physics Meets Food (Michael Brenner et al.)',
8
+ url: 'https://www.scribd.com/document/522544303/Science-and-Cooking-Michael-Brenner',
9
+ },
10
+ ];
@@ -0,0 +1,213 @@
1
+ ---
2
+ import PrecisionControls from './components/PrecisionControls.astro';
3
+ import RecipeSummary from './components/RecipeSummary.astro';
4
+ import SpherificationReactor from './components/SpherificationReactor.astro';
5
+
6
+ interface Props {
7
+ ui: Record<string, string>;
8
+ }
9
+
10
+ const { ui } = Astro.props;
11
+ ---
12
+
13
+ <div class="spherification-container">
14
+ <div class="spherification-console">
15
+ <div class="spherification-glow-1"></div>
16
+ <div class="spherification-glow-2"></div>
17
+
18
+ <div class="console-column controls-col">
19
+ <PrecisionControls ui={ui} />
20
+ </div>
21
+
22
+ <div class="console-column reactor-col">
23
+ <SpherificationReactor ui={ui} />
24
+ </div>
25
+
26
+ <div class="console-column summary-col">
27
+ <RecipeSummary ui={ui} />
28
+ </div>
29
+ </div>
30
+ </div>
31
+
32
+ <script is:inline define:vars={{ ui }}>
33
+ const metricBtn = document.getElementById('toggle-metric-btn');
34
+ const imperialBtn = document.getElementById('toggle-imperial-btn');
35
+ const directBtn = document.getElementById('toggle-direct-btn');
36
+ const reverseBtn = document.getElementById('toggle-reverse-btn');
37
+ const xanthanBtn = document.getElementById('toggle-xanthan-btn');
38
+ const citrateBtn = document.getElementById('toggle-citrate-btn');
39
+
40
+ const baseInput = document.getElementById('base-weight-input');
41
+ const bathInput = document.getElementById('bath-weight-input');
42
+
43
+ const baseAgentName = document.getElementById('base-agent-name-display');
44
+ const bathAgentName = document.getElementById('bath-agent-name-display');
45
+ const baseAgentVal = document.getElementById('base-agent-grams-display');
46
+ const bathAgentVal = document.getElementById('bath-agent-grams-display');
47
+ const xanthanVal = document.getElementById('xanthan-grams-display');
48
+ const citrateVal = document.getElementById('citrate-grams-display');
49
+
50
+ const xanthanRow = document.getElementById('xanthan-summary-item');
51
+ const citrateRow = document.getElementById('citrate-summary-item');
52
+ const solubilityWarning = document.getElementById('solubility-warning');
53
+
54
+ const beakerBath = document.getElementById('beaker-bath');
55
+ const sphereCore = document.getElementById('reactor-sphere-core');
56
+ const sphereMembrane = document.getElementById('reactor-sphere-membrane');
57
+
58
+ const methodBadge = document.getElementById('method-badge');
59
+ const methodBadgeText = document.getElementById('method-badge-text');
60
+ const methodDescText = document.getElementById('method-description-text');
61
+
62
+ let activeUnit = 'metric';
63
+ let activeMethod = 'direct';
64
+ let useXanthan = false;
65
+ let useCitrate = false;
66
+
67
+ function updateReactorUI() {
68
+ if (activeMethod === 'direct') {
69
+ beakerBath.setAttribute('fill', 'url(#bath-direct-grad)');
70
+ sphereCore.setAttribute('fill', 'url(#liquid-direct-grad)');
71
+ sphereMembrane.className.baseVal = 'sphere-gel-membrane direct';
72
+ methodBadge.className = 'method-badge direct';
73
+ methodBadgeText.textContent = ui.directMethod;
74
+ methodDescText.textContent = ui.directDesc;
75
+ } else {
76
+ beakerBath.setAttribute('fill', 'url(#bath-reverse-grad)');
77
+ sphereCore.setAttribute('fill', 'url(#liquid-reverse-grad)');
78
+ sphereMembrane.className.baseVal = 'sphere-gel-membrane reverse';
79
+ methodBadge.className = 'method-badge reverse';
80
+ methodBadgeText.textContent = ui.reverseMethod;
81
+ methodDescText.textContent = ui.reverseDesc;
82
+ }
83
+ }
84
+
85
+ function updateRecipeList(result, displayFactor) {
86
+ baseAgentName.textContent = activeMethod === 'direct' ? 'Sodium Alginate' : 'Calcium Lactate';
87
+ bathAgentName.textContent = activeMethod === 'direct' ? 'Calcium Chloride' : 'Sodium Alginate';
88
+
89
+ baseAgentVal.textContent = (result.baseAgentGrams * displayFactor).toFixed(2);
90
+ bathAgentVal.textContent = (result.bathAgentGrams * displayFactor).toFixed(2);
91
+
92
+ if (useXanthan) {
93
+ xanthanRow.classList.remove('hidden');
94
+ xanthanVal.textContent = (result.xanthanGrams * displayFactor).toFixed(2);
95
+ } else {
96
+ xanthanRow.classList.add('hidden');
97
+ }
98
+
99
+ if (useCitrate) {
100
+ citrateRow.classList.remove('hidden');
101
+ citrateVal.textContent = (result.citrateGrams * displayFactor).toFixed(2);
102
+ } else {
103
+ citrateRow.classList.add('hidden');
104
+ }
105
+
106
+ if (result.solubilityWarning) {
107
+ solubilityWarning.classList.remove('hidden');
108
+ } else {
109
+ solubilityWarning.classList.add('hidden');
110
+ }
111
+ }
112
+
113
+ function computeResult(baseWeight, bathWeight) {
114
+ const agentRatioBase = activeMethod === 'direct' ? 0.005 : 0.02;
115
+ const agentRatioBath = activeMethod === 'direct' ? 0.01 : 0.005;
116
+ const directWarning = activeMethod === 'direct' && baseWeight * 0.005 > 5;
117
+ const reverseWarning = activeMethod === 'reverse' && bathWeight * 0.005 > 10;
118
+ return {
119
+ baseAgentGrams: baseWeight * agentRatioBase,
120
+ bathAgentGrams: bathWeight * agentRatioBath,
121
+ xanthanGrams: useXanthan ? baseWeight * 0.002 : 0,
122
+ citrateGrams: useCitrate ? baseWeight * 0.005 : 0,
123
+ solubilityWarning: directWarning || reverseWarning,
124
+ };
125
+ }
126
+
127
+ function updateView() {
128
+ let baseWeight = Math.max(0, parseFloat(baseInput.value) || 0);
129
+ let bathWeight = Math.max(0, parseFloat(bathInput.value) || 0);
130
+
131
+ if (activeUnit === 'imperial') {
132
+ baseWeight = baseWeight / 0.035274;
133
+ bathWeight = bathWeight / 0.035274;
134
+ }
135
+
136
+ const result = computeResult(baseWeight, bathWeight);
137
+ const displayFactor = activeUnit === 'imperial' ? 0.035274 : 1.0;
138
+ updateRecipeList(result, displayFactor);
139
+ updateReactorUI();
140
+ }
141
+
142
+ function convertInputsTo(targetUnit) {
143
+ const currentBase = parseFloat(baseInput.value) || 0;
144
+ const currentBath = parseFloat(bathInput.value) || 0;
145
+ const labels = document.querySelectorAll('.weight-unit-label');
146
+ const recipeLabels = document.querySelectorAll('.recipe-unit-label');
147
+
148
+ if (targetUnit === 'imperial') {
149
+ baseInput.value = (currentBase * 0.035274).toFixed(1);
150
+ bathInput.value = (currentBath * 0.035274).toFixed(1);
151
+ labels.forEach(label => label.textContent = 'oz');
152
+ recipeLabels.forEach(label => label.textContent = 'oz');
153
+ } else {
154
+ baseInput.value = Math.round(currentBase / 0.035274).toString();
155
+ bathInput.value = Math.round(currentBath / 0.035274).toString();
156
+ labels.forEach(label => label.textContent = 'g');
157
+ recipeLabels.forEach(label => label.textContent = 'g');
158
+ }
159
+ }
160
+
161
+ metricBtn.addEventListener('click', () => {
162
+ if (activeUnit === 'metric') return;
163
+ activeUnit = 'metric';
164
+ metricBtn.classList.add('active');
165
+ imperialBtn.classList.remove('active');
166
+ convertInputsTo('metric');
167
+ updateView();
168
+ });
169
+
170
+ imperialBtn.addEventListener('click', () => {
171
+ if (activeUnit === 'imperial') return;
172
+ activeUnit = 'imperial';
173
+ imperialBtn.classList.add('active');
174
+ metricBtn.classList.remove('active');
175
+ convertInputsTo('imperial');
176
+ updateView();
177
+ });
178
+
179
+ directBtn.addEventListener('click', () => {
180
+ if (activeMethod === 'direct') return;
181
+ activeMethod = 'direct';
182
+ directBtn.classList.add('active');
183
+ reverseBtn.classList.remove('active');
184
+ updateView();
185
+ });
186
+
187
+ reverseBtn.addEventListener('click', () => {
188
+ if (activeMethod === 'reverse') return;
189
+ activeMethod = 'reverse';
190
+ reverseBtn.classList.add('active');
191
+ directBtn.classList.remove('active');
192
+ updateView();
193
+ });
194
+
195
+ xanthanBtn.addEventListener('click', () => {
196
+ useXanthan = !useXanthan;
197
+ xanthanBtn.classList.toggle('active', useXanthan);
198
+ document.getElementById('xanthan-switch-circle').classList.toggle('active', useXanthan);
199
+ updateView();
200
+ });
201
+
202
+ citrateBtn.addEventListener('click', () => {
203
+ useCitrate = !useCitrate;
204
+ citrateBtn.classList.toggle('active', useCitrate);
205
+ document.getElementById('citrate-switch-circle').classList.toggle('active', useCitrate);
206
+ updateView();
207
+ });
208
+
209
+ baseInput.addEventListener('input', updateView);
210
+ bathInput.addEventListener('input', updateView);
211
+
212
+ updateView();
213
+ </script>
@@ -0,0 +1,85 @@
1
+ ---
2
+ interface Props {
3
+ ui: Record<string, string>;
4
+ }
5
+
6
+ const { ui } = Astro.props;
7
+ ---
8
+
9
+ <div class="spherification-controls">
10
+ <div class="controls-group">
11
+ <label class="controls-label">{ui.unitLabel}</label>
12
+ <div class="unit-toggle-container">
13
+ <button type="button" id="toggle-metric-btn" class="mode-btn active" data-unit="metric">
14
+ {ui.metricUnit}
15
+ </button>
16
+ <button type="button" id="toggle-imperial-btn" class="mode-btn" data-unit="imperial">
17
+ {ui.imperialUnit}
18
+ </button>
19
+ </div>
20
+ </div>
21
+
22
+ <div class="controls-group">
23
+ <label class="controls-label">{ui.methodLabel}</label>
24
+ <div class="method-toggle-container">
25
+ <button type="button" id="toggle-direct-btn" class="mode-btn active" data-method="direct">
26
+ {ui.directMethod}
27
+ </button>
28
+ <button type="button" id="toggle-reverse-btn" class="mode-btn" data-method="reverse">
29
+ {ui.reverseMethod}
30
+ </button>
31
+ </div>
32
+ </div>
33
+
34
+ <div class="controls-group">
35
+ <label for="base-weight-input" class="controls-label">
36
+ {ui.baseWeightLabel} (<span class="weight-unit-label">g</span>)
37
+ </label>
38
+ <input
39
+ type="number"
40
+ id="base-weight-input"
41
+ min="0"
42
+ max="100000"
43
+ step="any"
44
+ value="500"
45
+ class="controls-number-input"
46
+ inputmode="decimal"
47
+ />
48
+ </div>
49
+
50
+ <div class="controls-group">
51
+ <label for="bath-weight-input" class="controls-label">
52
+ {ui.bathWeightLabel} (<span class="weight-unit-label">g</span>)
53
+ </label>
54
+ <input
55
+ type="number"
56
+ id="bath-weight-input"
57
+ min="0"
58
+ max="100000"
59
+ step="any"
60
+ value="1000"
61
+ class="controls-number-input"
62
+ inputmode="decimal"
63
+ />
64
+ </div>
65
+
66
+ <div class="switches-grid">
67
+ <button type="button" id="toggle-xanthan-btn" class="switch-row-btn">
68
+ <div class="switch-row-info">
69
+ <span class="switch-row-title">{ui.xanthanLabel}</span>
70
+ </div>
71
+ <div class="toggle-switch">
72
+ <div id="xanthan-switch-circle" class="switch-circle"></div>
73
+ </div>
74
+ </button>
75
+
76
+ <button type="button" id="toggle-citrate-btn" class="switch-row-btn">
77
+ <div class="switch-row-info">
78
+ <span class="switch-row-title">{ui.citrateLabel}</span>
79
+ </div>
80
+ <div class="toggle-switch">
81
+ <div id="citrate-switch-circle" class="switch-circle"></div>
82
+ </div>
83
+ </button>
84
+ </div>
85
+ </div>
@@ -0,0 +1,60 @@
1
+ ---
2
+ interface Props {
3
+ ui: Record<string, string>;
4
+ }
5
+
6
+ const { ui } = Astro.props;
7
+ ---
8
+
9
+ <div class="recipe-summary-container">
10
+ <div class="summary-header">
11
+ <span class="summary-title">{ui.recipeTitle}</span>
12
+ </div>
13
+
14
+ <div class="recipe-list">
15
+ <div class="recipe-item">
16
+ <div class="recipe-item-name">
17
+ <span id="base-agent-name-display">{ui.baseGellingAgent}</span>
18
+ </div>
19
+ <div class="recipe-item-val">
20
+ <span id="base-agent-grams-display">2.50</span>
21
+ <span class="recipe-unit-label">g</span>
22
+ </div>
23
+ </div>
24
+
25
+ <div class="recipe-item">
26
+ <div class="recipe-item-name">
27
+ <span id="bath-agent-name-display">{ui.bathGellingAgent}</span>
28
+ </div>
29
+ <div class="recipe-item-val">
30
+ <span id="bath-agent-grams-display">10.00</span>
31
+ <span class="recipe-unit-label">g</span>
32
+ </div>
33
+ </div>
34
+
35
+ <div id="xanthan-summary-item" class="recipe-item hidden">
36
+ <div class="recipe-item-name">
37
+ <span>{ui.xanthanGum}</span>
38
+ </div>
39
+ <div class="recipe-item-val">
40
+ <span id="xanthan-grams-display">0.00</span>
41
+ <span class="recipe-unit-label">g</span>
42
+ </div>
43
+ </div>
44
+
45
+ <div id="citrate-summary-item" class="recipe-item hidden">
46
+ <div class="recipe-item-name">
47
+ <span>{ui.sodiumCitrate}</span>
48
+ </div>
49
+ <div class="recipe-item-val">
50
+ <span id="citrate-grams-display">0.00</span>
51
+ <span class="recipe-unit-label">g</span>
52
+ </div>
53
+ </div>
54
+ </div>
55
+
56
+ <div id="solubility-warning" class="solubility-warning hidden">
57
+ <div class="warning-badge">{ui.warningLabel}</div>
58
+ <p class="warning-text">{ui.warningDesc}</p>
59
+ </div>
60
+ </div>
@@ -0,0 +1,73 @@
1
+ ---
2
+ interface Props {
3
+ ui: Record<string, string>;
4
+ }
5
+
6
+ const { ui } = Astro.props;
7
+ ---
8
+
9
+ <div class="reactor-section">
10
+ <div class="reactor-header">
11
+ <span class="reactor-title">{ui.methodLabel}</span>
12
+ </div>
13
+
14
+ <div class="reactor-container">
15
+ <div class="beaker-vessel">
16
+ <svg viewBox="0 0 200 240" class="reactor-svg" xmlns="http://www.w3.org/2000/svg">
17
+ <defs>
18
+ <linearGradient id="beaker-glass-grad" x1="0%" y1="0%" x2="100%" y2="0%">
19
+ <stop offset="0%" stop-color="rgba(255,255,255,0.4)"></stop>
20
+ <stop offset="10%" stop-color="rgba(255,255,255,0.1)"></stop>
21
+ <stop offset="90%" stop-color="rgba(255,255,255,0.1)"></stop>
22
+ <stop offset="100%" stop-color="rgba(255,255,255,0.4)"></stop>
23
+ </linearGradient>
24
+
25
+ <linearGradient id="liquid-direct-grad" x1="0%" y1="0%" x2="100%" y2="100%">
26
+ <stop offset="0%" stop-color="#fb923c"></stop>
27
+ <stop offset="100%" stop-color="#ea580c"></stop>
28
+ </linearGradient>
29
+
30
+ <linearGradient id="liquid-reverse-grad" x1="0%" y1="0%" x2="100%" y2="100%">
31
+ <stop offset="0%" stop-color="#f472b6"></stop>
32
+ <stop offset="100%" stop-color="#db2777"></stop>
33
+ </linearGradient>
34
+
35
+ <linearGradient id="bath-direct-grad" x1="0%" y1="0%" x2="0%" y2="100%">
36
+ <stop offset="0%" stop-color="rgba(56,189,248,0.15)"></stop>
37
+ <stop offset="100%" stop-color="rgba(14,165,233,0.3)"></stop>
38
+ </linearGradient>
39
+
40
+ <linearGradient id="bath-reverse-grad" x1="0%" y1="0%" x2="0%" y2="100%">
41
+ <stop offset="0%" stop-color="rgba(168,85,247,0.15)"></stop>
42
+ <stop offset="100%" stop-color="rgba(126,34,206,0.3)"></stop>
43
+ </linearGradient>
44
+ </defs>
45
+
46
+ <rect x="30" y="70" width="140" height="150" rx="15" id="beaker-bath" class="beaker-liquid" fill="url(#bath-direct-grad)"></rect>
47
+
48
+ <rect x="26" y="66" width="148" height="158" rx="18" fill="none" stroke="url(#beaker-glass-grad)" stroke-width="4"></rect>
49
+ <line x1="20" y1="66" x2="180" y2="66" stroke="url(#beaker-glass-grad)" stroke-width="4"></line>
50
+
51
+ <g class="nozzle-assembly">
52
+ <rect x="94" y="10" width="12" height="30" fill="#94a3b8" rx="2"></rect>
53
+ <path d="M96,40 L104,40 L102,52 L98,52 Z" fill="#64748b"></path>
54
+ <circle cx="100" cy="56" r="3" class="droplet"></circle>
55
+ </g>
56
+
57
+ <g id="sphere-chamber" class="sphere-chamber">
58
+ <circle cx="100" cy="140" r="35" id="reactor-sphere-core" class="sphere-fluid-core" fill="url(#liquid-direct-grad)"></circle>
59
+ <circle cx="100" cy="140" r="35" id="reactor-sphere-membrane" class="sphere-gel-membrane"></circle>
60
+ </g>
61
+ </svg>
62
+ </div>
63
+ </div>
64
+
65
+ <div class="method-info-card">
66
+ <div id="method-badge" class="method-badge direct">
67
+ <span id="method-badge-text">{ui.directMethod}</span>
68
+ </div>
69
+ <p id="method-description-text" class="method-desc-text">
70
+ {ui.directDesc}
71
+ </p>
72
+ </div>
73
+ </div>
@@ -0,0 +1,26 @@
1
+ import type { CookingToolEntry } from '../../types';
2
+
3
+ export const spherificationBath: CookingToolEntry = {
4
+ id: 'spherification-bath-calculator',
5
+ icons: {
6
+ bg: 'mdi:flask-outline',
7
+ fg: 'mdi:dots-circle',
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
+ };