@jjlmoya/utils-cooking 1.37.0 → 1.39.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 (59) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +4 -0
  3. package/src/entries.ts +5 -1
  4. package/src/index.ts +2 -0
  5. package/src/tests/i18n-titles.test.ts +1 -1
  6. package/src/tests/leavener-acid-neutralizer.test.ts +42 -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/leavener-acid-neutralizer/bibliography.astro +6 -0
  10. package/src/tool/leavener-acid-neutralizer/bibliography.ts +10 -0
  11. package/src/tool/leavener-acid-neutralizer/component.astro +335 -0
  12. package/src/tool/leavener-acid-neutralizer/entry.ts +26 -0
  13. package/src/tool/leavener-acid-neutralizer/i18n/de.ts +279 -0
  14. package/src/tool/leavener-acid-neutralizer/i18n/en.ts +279 -0
  15. package/src/tool/leavener-acid-neutralizer/i18n/es.ts +275 -0
  16. package/src/tool/leavener-acid-neutralizer/i18n/fr.ts +279 -0
  17. package/src/tool/leavener-acid-neutralizer/i18n/id.ts +279 -0
  18. package/src/tool/leavener-acid-neutralizer/i18n/it.ts +275 -0
  19. package/src/tool/leavener-acid-neutralizer/i18n/ja.ts +279 -0
  20. package/src/tool/leavener-acid-neutralizer/i18n/ko.ts +279 -0
  21. package/src/tool/leavener-acid-neutralizer/i18n/nl.ts +279 -0
  22. package/src/tool/leavener-acid-neutralizer/i18n/pl.ts +279 -0
  23. package/src/tool/leavener-acid-neutralizer/i18n/pt.ts +279 -0
  24. package/src/tool/leavener-acid-neutralizer/i18n/ru.ts +279 -0
  25. package/src/tool/leavener-acid-neutralizer/i18n/sv.ts +279 -0
  26. package/src/tool/leavener-acid-neutralizer/i18n/tr.ts +279 -0
  27. package/src/tool/leavener-acid-neutralizer/i18n/zh.ts +279 -0
  28. package/src/tool/leavener-acid-neutralizer/index.ts +10 -0
  29. package/src/tool/leavener-acid-neutralizer/leavener-acid-neutralizer.css +424 -0
  30. package/src/tool/leavener-acid-neutralizer/logic.ts +57 -0
  31. package/src/tool/leavener-acid-neutralizer/seo.astro +15 -0
  32. package/src/tool/pectin-jam-setting-calculator/bibliography.astro +6 -0
  33. package/src/tool/pectin-jam-setting-calculator/bibliography.ts +10 -0
  34. package/src/tool/pectin-jam-setting-calculator/component.astro +170 -0
  35. package/src/tool/pectin-jam-setting-calculator/components/CalculatorInputs.astro +44 -0
  36. package/src/tool/pectin-jam-setting-calculator/components/DropTestVisualizer.astro +40 -0
  37. package/src/tool/pectin-jam-setting-calculator/components/FruitSelector.astro +38 -0
  38. package/src/tool/pectin-jam-setting-calculator/components/RecipeResults.astro +72 -0
  39. package/src/tool/pectin-jam-setting-calculator/entry.ts +26 -0
  40. package/src/tool/pectin-jam-setting-calculator/i18n/de.ts +248 -0
  41. package/src/tool/pectin-jam-setting-calculator/i18n/en.ts +248 -0
  42. package/src/tool/pectin-jam-setting-calculator/i18n/es.ts +248 -0
  43. package/src/tool/pectin-jam-setting-calculator/i18n/fr.ts +248 -0
  44. package/src/tool/pectin-jam-setting-calculator/i18n/id.ts +248 -0
  45. package/src/tool/pectin-jam-setting-calculator/i18n/it.ts +248 -0
  46. package/src/tool/pectin-jam-setting-calculator/i18n/ja.ts +248 -0
  47. package/src/tool/pectin-jam-setting-calculator/i18n/ko.ts +248 -0
  48. package/src/tool/pectin-jam-setting-calculator/i18n/nl.ts +248 -0
  49. package/src/tool/pectin-jam-setting-calculator/i18n/pl.ts +248 -0
  50. package/src/tool/pectin-jam-setting-calculator/i18n/pt.ts +248 -0
  51. package/src/tool/pectin-jam-setting-calculator/i18n/ru.ts +248 -0
  52. package/src/tool/pectin-jam-setting-calculator/i18n/sv.ts +248 -0
  53. package/src/tool/pectin-jam-setting-calculator/i18n/tr.ts +248 -0
  54. package/src/tool/pectin-jam-setting-calculator/i18n/zh.ts +248 -0
  55. package/src/tool/pectin-jam-setting-calculator/index.ts +11 -0
  56. package/src/tool/pectin-jam-setting-calculator/logic.ts +96 -0
  57. package/src/tool/pectin-jam-setting-calculator/pectin-jam-setting-calculator.css +730 -0
  58. package/src/tool/pectin-jam-setting-calculator/seo.astro +15 -0
  59. package/src/tools.ts +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-cooking",
3
- "version": "1.37.0",
3
+ "version": "1.39.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -21,6 +21,8 @@ import { maillardReaction } from '../tool/maillard-reaction-optimizer/entry';
21
21
  import { macaronDrying } from '../tool/macaron-drying-predictor/entry';
22
22
  import { brixSorbetDensity } from '../tool/brix-sorbet-density-calculator/entry';
23
23
  import { oilSmokePoint } from '../tool/oil-smoke-point-tracker/entry';
24
+ import { leavenerAcidNeutralizer } from '../tool/leavener-acid-neutralizer/entry';
25
+ import { pectinJam } from '../tool/pectin-jam-setting-calculator/entry';
24
26
 
25
27
  export const cookingCategory: CookingCategoryEntry = {
26
28
  icon: 'mdi:chef-hat',
@@ -47,6 +49,8 @@ export const cookingCategory: CookingCategoryEntry = {
47
49
  macaronDrying,
48
50
  brixSorbetDensity,
49
51
  oilSmokePoint,
52
+ leavenerAcidNeutralizer,
53
+ pectinJam,
50
54
  ],
51
55
 
52
56
  i18n: {
package/src/entries.ts CHANGED
@@ -21,6 +21,8 @@ export { macaronDrying } from './tool/macaron-drying-predictor/entry';
21
21
  export { brixSorbetDensity } from './tool/brix-sorbet-density-calculator/entry';
22
22
  export { botulismCanningSafety } from './tool/botulism-canning-safety/entry';
23
23
  export { oilSmokePoint } from './tool/oil-smoke-point-tracker/entry';
24
+ export { leavenerAcidNeutralizer } from './tool/leavener-acid-neutralizer/entry';
25
+ export { pectinJam } from './tool/pectin-jam-setting-calculator/entry';
24
26
  export { cookingCategory } from './category';
25
27
  import { americanKitchenConverter } from './tool/american-kitchen-converter/entry';
26
28
  import { bananaCare } from './tool/banana-ripeness/entry';
@@ -45,5 +47,7 @@ import { maillardReaction } from './tool/maillard-reaction-optimizer/entry';
45
47
  import { macaronDrying } from './tool/macaron-drying-predictor/entry';
46
48
  import { brixSorbetDensity } from './tool/brix-sorbet-density-calculator/entry';
47
49
  import { oilSmokePoint } from './tool/oil-smoke-point-tracker/entry';
48
- export const ALL_ENTRIES = [americanKitchenConverter, bananaCare, brine, cookwareGuide, eggTimer, ingredientRescaler, kitchenTimer, meringuePeak, moldScaler, pizza, rouxGuide, sourdoughCalculator, yeastConverter, lactoFermentationSalt, spherificationBath, iceCreamPacPod, botulismCanningSafety, meatBinder, carryOverCooking, maillardReaction, macaronDrying, brixSorbetDensity, oilSmokePoint];
50
+ import { leavenerAcidNeutralizer } from './tool/leavener-acid-neutralizer/entry';
51
+ import { pectinJam } from './tool/pectin-jam-setting-calculator/entry';
52
+ export const ALL_ENTRIES = [americanKitchenConverter, bananaCare, brine, cookwareGuide, eggTimer, ingredientRescaler, kitchenTimer, meringuePeak, moldScaler, pizza, rouxGuide, sourdoughCalculator, yeastConverter, lactoFermentationSalt, spherificationBath, iceCreamPacPod, botulismCanningSafety, meatBinder, carryOverCooking, maillardReaction, macaronDrying, brixSorbetDensity, oilSmokePoint, leavenerAcidNeutralizer, pectinJam];
49
53
 
package/src/index.ts CHANGED
@@ -24,6 +24,8 @@ export { MAILLARD_REACTION_TOOL } from './tool/maillard-reaction-optimizer';
24
24
  export { MACARON_DRYING_TOOL } from './tool/macaron-drying-predictor';
25
25
  export { BRIX_SORBET_DENSITY_CALCULATOR_TOOL } from './tool/brix-sorbet-density-calculator';
26
26
  export { OIL_SMOKE_POINT_TRACKER_TOOL } from './tool/oil-smoke-point-tracker';
27
+ export { LEAVENER_ACID_NEUTRALIZER_TOOL } from './tool/leavener-acid-neutralizer';
28
+ export { PECTIN_JAM_SETTING_TOOL } from './tool/pectin-jam-setting-calculator';
27
29
 
28
30
 
29
31
  export type {
@@ -36,7 +36,7 @@ describe("i18n titles for FAQ", () => {
36
36
 
37
37
  it("should have 18 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(23);
39
+ expect(completeTools.length).toBe(25);
40
40
  });
41
41
 
42
42
  it("tool IDs should be correctly registered", () => {
@@ -0,0 +1,42 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { LeavenerLogic } from '../tool/leavener-acid-neutralizer/logic';
3
+
4
+ describe('Leavener Acid Neutralizer Logic', () => {
5
+ it('calculates correct baking soda for buttermilk and yogurt', () => {
6
+ const res = LeavenerLogic.calculate({
7
+ acidIngredients: [
8
+ { type: 'yogurt', weight: 240 }
9
+ ],
10
+ flour: 150
11
+ });
12
+ expect(res.neutralizedBakingSoda).toBe(3);
13
+ expect(res.boosterBakingPowder).toBe(0);
14
+ });
15
+
16
+ it('calculates correct booster baking powder when acid is insufficient', () => {
17
+ const res = LeavenerLogic.calculate({
18
+ acidIngredients: [
19
+ { type: 'yogurt', weight: 50 }
20
+ ],
21
+ flour: 150
22
+ });
23
+ expect(res.neutralizedBakingSoda).toBe(0.63);
24
+ expect(res.requiredBakingPowder).toBe(6);
25
+ expect(res.providedBakingPowderEquivalent).toBe(2.52);
26
+ expect(res.boosterBakingPowder).toBe(3.48);
27
+ });
28
+
29
+ it('scales correctly with multiple acid ingredients', () => {
30
+ const res = LeavenerLogic.calculate({
31
+ acidIngredients: [
32
+ { type: 'lemon_juice', weight: 15 },
33
+ { type: 'yogurt', weight: 120 }
34
+ ],
35
+ flour: 200
36
+ });
37
+ expect(res.neutralizedBakingSoda).toBe(3);
38
+ expect(res.requiredBakingPowder).toBe(8);
39
+ expect(res.providedBakingPowderEquivalent).toBe(12);
40
+ expect(res.boosterBakingPowder).toBe(0);
41
+ });
42
+ });
@@ -24,8 +24,8 @@ describe('Locale Completeness Validation', () => {
24
24
  });
25
25
  });
26
26
 
27
- it('all 23 tools registered', () => {
28
- expect(ALL_TOOLS.length).toBe(23);
27
+ it('all 25 tools registered', () => {
28
+ expect(ALL_TOOLS.length).toBe(25);
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 23 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(23);
7
+ it('should have 25 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(25);
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,10 @@
1
+ export const bibliography = [
2
+ {
3
+ name: 'Changing chemical leavening to improve the structural, textural and sensory properties of functional cakes with blackcurrant pomace',
4
+ url: 'https://www.sciencedirect.com/science/article/abs/pii/S0023643820303674',
5
+ },
6
+ {
7
+ name: 'Understanding Baking Soda vs. Baking Powder',
8
+ url: 'https://www.utas.edu.au/about/news-and-stories/articles/2025/whats-the-difference-between-baking-powder-and-baking-soda-its-subtle,-but-significant',
9
+ },
10
+ ];
@@ -0,0 +1,335 @@
1
+ ---
2
+ import './leavener-acid-neutralizer.css';
3
+
4
+ interface Props {
5
+ ui: Record<string, string>;
6
+ }
7
+
8
+ const { ui } = Astro.props;
9
+ ---
10
+
11
+ <div class="leavener-container">
12
+ <div class="leavener-card">
13
+ <div class="recipe-setup-card">
14
+ <div class="top-row">
15
+ <div class="luxury-title-group">
16
+ <span class="luxury-badge">{ui.stoichiometryBadge}</span>
17
+ </div>
18
+ <div class="unit-selector">
19
+ <button id="unit-metric-btn" class="unit-btn active">{ui.metricUnit}</button>
20
+ <button id="unit-imperial-btn" class="unit-btn">{ui.imperialUnit}</button>
21
+ </div>
22
+ </div>
23
+
24
+ <div class="flour-slider-group">
25
+ <div class="flour-header">
26
+ <span class="flour-title-label">{ui.flourLabel}</span>
27
+ <span id="flour-val-display" class="flour-val-num">150g</span>
28
+ </div>
29
+ <input type="range" id="flour-slider" min="50" max="1000" step="10" value="150" class="premium-range-slider" />
30
+ </div>
31
+ </div>
32
+
33
+ <div class="main-workspace-layout">
34
+ <div class="control-panel-card">
35
+ <h4 class="panel-subtitle">Select Acidic Ingredients</h4>
36
+ <div class="ingredients-pills">
37
+ <button class="pill-btn" data-type="buttermilk" data-name={ui.acid_buttermilk}>{ui.acid_buttermilk}</button>
38
+ <button class="pill-btn active" data-type="yogurt" data-name={ui.acid_yogurt}>{ui.acid_yogurt}</button>
39
+ <button class="pill-btn" data-type="sour_cream" data-name={ui.acid_sour_cream}>{ui.acid_sour_cream}</button>
40
+ <button class="pill-btn" data-type="honey" data-name={ui.acid_honey}>{ui.acid_honey}</button>
41
+ <button class="pill-btn" data-type="molasses" data-name={ui.acid_molasses}>{ui.acid_molasses}</button>
42
+ <button class="pill-btn" data-type="cocoa" data-name={ui.acid_cocoa}>{ui.acid_cocoa}</button>
43
+ <button class="pill-btn" data-type="lemon_juice" data-name={ui.acid_lemon_juice}>{ui.acid_lemon_juice}</button>
44
+ <button class="pill-btn" data-type="vinegar" data-name={ui.acid_vinegar}>{ui.acid_vinegar}</button>
45
+ </div>
46
+
47
+ <div id="active-sliders-container" class="active-sliders-list"></div>
48
+ </div>
49
+
50
+ <div class="analysis-panel-card">
51
+ <h4 class="panel-subtitle">{ui.scaleBalanceTitle}</h4>
52
+
53
+ <div class="linear-ph-scale-card">
54
+ <div class="ph-track">
55
+ <div class="ph-zone acid-zone"></div>
56
+ <div class="ph-zone neutral-zone"></div>
57
+ <div class="ph-zone alkaline-zone"></div>
58
+ <div id="ph-pointer-orb" class="ph-pointer-orb"></div>
59
+ </div>
60
+ <div class="ph-labels">
61
+ <span>{ui.statusAcidic}</span>
62
+ <span class="neutral-label">{ui.statusBalanced}</span>
63
+ <span>{ui.statusAlkaline}</span>
64
+ </div>
65
+ </div>
66
+
67
+ <div class="simulation-adjuster">
68
+ <div class="slider-header-row">
69
+ <span class="adjuster-label">{ui.simulateSodaLabel}</span>
70
+ <div class="adjuster-controls">
71
+ <span id="simulator-val" class="adjuster-val">3.00g</span>
72
+ <button id="auto-balance-btn" class="btn-auto-balance">{ui.autoBalanceBtn}</button>
73
+ </div>
74
+ </div>
75
+ <input type="range" id="simulator-slider" min="0" max="10" step="0.05" value="3.0" class="premium-range-slider" />
76
+ </div>
77
+
78
+ <div class="perfect-recipe-card">
79
+ <h4 class="recipe-title">{ui.resultsTitle}</h4>
80
+
81
+ <div class="recipe-stat-row">
82
+ <div class="recipe-stat-info">
83
+ <span class="stat-main-label">{ui.bakingSodaNeeded}</span>
84
+ <span class="stat-sub-label">Required to neutralize all acid</span>
85
+ </div>
86
+ <div class="recipe-stat-numbers">
87
+ <span id="formula-soda-g" class="num-large">0.00g</span>
88
+ <span id="formula-soda-tsp" class="num-small">0.00 tsp</span>
89
+ </div>
90
+ </div>
91
+
92
+ <div class="recipe-stat-row">
93
+ <div class="recipe-stat-info">
94
+ <span class="stat-main-label">{ui.boosterBakingPowder}</span>
95
+ <span class="stat-sub-label">Additional booster for leavening</span>
96
+ </div>
97
+ <div class="recipe-stat-numbers">
98
+ <span id="formula-booster-g" class="num-large">0.00g</span>
99
+ <span id="formula-booster-tsp" class="num-small">0.00 tsp</span>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ <script is:inline define:vars={{ ui }}>
109
+ const flourSlider = document.getElementById('flour-slider');
110
+ const flourValDisplay = document.getElementById('flour-val-display');
111
+ const unitMetricBtn = document.getElementById('unit-metric-btn');
112
+ const unitImperialBtn = document.getElementById('unit-imperial-btn');
113
+ const pillBtns = document.querySelectorAll('.pill-btn');
114
+ const activeSlidersContainer = document.getElementById('active-sliders-container');
115
+
116
+ const phPointerOrb = document.getElementById('ph-pointer-orb');
117
+ const simulatorSlider = document.getElementById('simulator-slider');
118
+ const simulatorVal = document.getElementById('simulator-val');
119
+ const autoBalanceBtn = document.getElementById('auto-balance-btn');
120
+
121
+ const formulaSodaG = document.getElementById('formula-soda-g');
122
+ const formulaSodaTsp = document.getElementById('formula-soda-tsp');
123
+ const formulaBoosterG = document.getElementById('formula-booster-g');
124
+ const formulaBoosterTsp = document.getElementById('formula-booster-tsp');
125
+
126
+ let currentUnit = 'metric';
127
+ let targetRecommendedSoda = 3.0;
128
+
129
+ const activeIngredients = {
130
+ yogurt: 240
131
+ };
132
+
133
+ const ACID_RATIOS = {
134
+ buttermilk: 1.5 / 120,
135
+ yogurt: 1.5 / 120,
136
+ sour_cream: 1.5 / 120,
137
+ honey: 3 / 340,
138
+ molasses: 3 / 328,
139
+ cocoa: 3 / 80,
140
+ lemon_juice: 1.5 / 15,
141
+ vinegar: 1.5 / 15
142
+ };
143
+
144
+ const MAX_LIMITS = {
145
+ buttermilk: 500,
146
+ yogurt: 500,
147
+ sour_cream: 500,
148
+ honey: 400,
149
+ molasses: 400,
150
+ cocoa: 100,
151
+ lemon_juice: 100,
152
+ vinegar: 100
153
+ };
154
+
155
+ unitMetricBtn.addEventListener('click', () => {
156
+ if (currentUnit !== 'metric') {
157
+ const oldFlour = parseFloat(flourSlider.value) || 0;
158
+ flourSlider.min = "50";
159
+ flourSlider.max = "1000";
160
+ flourSlider.step = "10";
161
+ flourSlider.value = (oldFlour * 28.3495).toFixed(0);
162
+
163
+ for (const key in activeIngredients) {
164
+ activeIngredients[key] = parseFloat((activeIngredients[key] * 28.3495).toFixed(0));
165
+ }
166
+
167
+ const oldSlider = parseFloat(simulatorSlider.value);
168
+ simulatorSlider.value = (oldSlider * 28.3495).toFixed(2);
169
+
170
+ currentUnit = 'metric';
171
+ unitMetricBtn.classList.add('active');
172
+ unitImperialBtn.classList.remove('active');
173
+ renderSliders();
174
+ update();
175
+ }
176
+ });
177
+
178
+ unitImperialBtn.addEventListener('click', () => {
179
+ if (currentUnit !== 'imperial') {
180
+ const oldFlour = parseFloat(flourSlider.value) || 0;
181
+ flourSlider.min = "2";
182
+ flourSlider.max = "36";
183
+ flourSlider.step = "0.5";
184
+ flourSlider.value = (oldFlour / 28.3495).toFixed(1);
185
+
186
+ for (const key in activeIngredients) {
187
+ activeIngredients[key] = parseFloat((activeIngredients[key] / 28.3495).toFixed(1));
188
+ }
189
+
190
+ const oldSlider = parseFloat(simulatorSlider.value);
191
+ simulatorSlider.value = (oldSlider / 28.3495).toFixed(3);
192
+
193
+ currentUnit = 'imperial';
194
+ unitImperialBtn.classList.add('active');
195
+ unitMetricBtn.classList.remove('active');
196
+ renderSliders();
197
+ update();
198
+ }
199
+ });
200
+
201
+ pillBtns.forEach(btn => {
202
+ btn.addEventListener('click', () => {
203
+ const type = btn.getAttribute('data-type');
204
+ if (activeIngredients[type] !== undefined) {
205
+ delete activeIngredients[type];
206
+ btn.classList.remove('active');
207
+ } else {
208
+ activeIngredients[type] = currentUnit === 'metric' ? 100 : 3.5;
209
+ btn.classList.add('active');
210
+ }
211
+ renderSliders();
212
+ update();
213
+ });
214
+ });
215
+
216
+ function createSliderElement(key, isMetric, unitText) {
217
+ const wrapper = document.createElement('div');
218
+ wrapper.className = 'slider-wrapper-card';
219
+ const maxVal = isMetric ? MAX_LIMITS[key] : parseFloat((MAX_LIMITS[key] / 28.3495).toFixed(1));
220
+ const stepVal = isMetric ? 5 : 0.1;
221
+ wrapper.innerHTML = `<div class="slider-header-row"><span class="slider-label-name">${ui[`acid_${key}`] || key}</span><span class="slider-value-num">${activeIngredients[key]}${unitText}</span></div><input type="range" class="premium-range-slider" min="0" max="${maxVal}" stroke-linecap="round" step="${stepVal}" value="${activeIngredients[key]}" />`;
222
+ wrapper.querySelector('input').addEventListener('input', (e) => {
223
+ activeIngredients[key] = parseFloat(e.target.value) || 0;
224
+ wrapper.querySelector('.slider-value-num').textContent = `${activeIngredients[key]}${unitText}`;
225
+ update();
226
+ });
227
+ return wrapper;
228
+ }
229
+
230
+ function renderSliders() {
231
+ activeSlidersContainer.innerHTML = '';
232
+ const isMetric = currentUnit === 'metric';
233
+ const unitText = isMetric ? 'g' : 'oz';
234
+
235
+ for (const key in activeIngredients) {
236
+ const element = createSliderElement(key, isMetric, unitText);
237
+ activeSlidersContainer.appendChild(element);
238
+ }
239
+ }
240
+
241
+ autoBalanceBtn.addEventListener('click', () => {
242
+ const isMetric = currentUnit === 'metric';
243
+ if (isMetric) {
244
+ simulatorSlider.value = targetRecommendedSoda.toFixed(2);
245
+ } else {
246
+ simulatorSlider.value = (targetRecommendedSoda / 28.3495).toFixed(3);
247
+ }
248
+ update();
249
+ });
250
+
251
+ function getNeutralizedSoda(multiplier) {
252
+ let neutralized = 0;
253
+ for (const key in activeIngredients) {
254
+ const weightGrams = activeIngredients[key] * multiplier;
255
+ const ratio = ACID_RATIOS[key] || 0;
256
+ neutralized += weightGrams * ratio;
257
+ }
258
+ return neutralized;
259
+ }
260
+
261
+ function updatePointer(diff, neutralizedSoda, simulatedSoda) {
262
+ let leftPercent = 50;
263
+ if (neutralizedSoda > 0) {
264
+ const ratio = simulatedSoda / neutralizedSoda;
265
+ if (ratio > 1) {
266
+ leftPercent = 50 + Math.min(45, (ratio - 1) * 35);
267
+ } else if (ratio < 1) {
268
+ leftPercent = 50 - Math.min(45, (1 - ratio) * 50);
269
+ }
270
+ } else {
271
+ leftPercent = simulatedSoda > 0 ? 95 : 50;
272
+ }
273
+
274
+ phPointerOrb.style.left = `${leftPercent}%`;
275
+
276
+ if (Math.abs(diff) <= 0.15) {
277
+ phPointerOrb.style.backgroundColor = '#10b981';
278
+ phPointerOrb.style.boxShadow = '0 0 20px #10b981';
279
+ } else if (diff > 0.15) {
280
+ phPointerOrb.style.backgroundColor = '#3b82f6';
281
+ phPointerOrb.style.boxShadow = '0 0 20px #3b82f6';
282
+ } else {
283
+ phPointerOrb.style.backgroundColor = '#ec4899';
284
+ phPointerOrb.style.boxShadow = '0 0 20px #ec4899';
285
+ }
286
+ }
287
+
288
+ function updateOutputs(data) {
289
+ const isMetric = data.isMetric;
290
+ const neutralizedSoda = data.neutralizedSoda;
291
+ const boosterPowder = data.boosterPowder;
292
+ const simulatedSoda = data.simulatedSoda;
293
+ const sodaTsp = (neutralizedSoda / 6);
294
+ const boosterTsp = (boosterPowder / 4.8);
295
+
296
+ if (isMetric) {
297
+ formulaSodaG.textContent = `${neutralizedSoda.toFixed(2)}${ui.gramsUnit}`;
298
+ formulaBoosterG.textContent = `${boosterPowder.toFixed(2)}${ui.gramsUnit}`;
299
+ simulatorVal.textContent = `${simulatedSoda.toFixed(2)}${ui.gramsUnit}`;
300
+ formulaSodaTsp.style.display = 'none';
301
+ formulaBoosterTsp.style.display = 'none';
302
+ sodaSliderLabel.textContent = `${ui.sodaAddedLabel} (${ui.gramsUnit})`;
303
+ } else {
304
+ formulaSodaG.textContent = `${(neutralizedSoda / 28.3495).toFixed(3)}${ui.ouncesUnit}`;
305
+ formulaBoosterG.textContent = `${(boosterPowder / 28.3495).toFixed(3)}${ui.ouncesUnit}`;
306
+ simulatorVal.textContent = `${(simulatedSoda / 28.3495).toFixed(3)}${ui.ouncesUnit}`;
307
+ formulaSodaTsp.style.display = 'block';
308
+ formulaBoosterTsp.style.display = 'block';
309
+ formulaSodaTsp.textContent = `${sodaTsp.toFixed(2)} ${ui.teaspoonsUnit}`;
310
+ formulaBoosterTsp.textContent = `${boosterTsp.toFixed(2)} ${ui.teaspoonsUnit}`;
311
+ sodaSliderLabel.textContent = `${ui.sodaAddedLabel} (${ui.ouncesUnit})`;
312
+ }
313
+ }
314
+
315
+ function update() {
316
+ const isMetric = currentUnit === 'metric';
317
+ const multiplier = isMetric ? 1 : 28.3495;
318
+ const flourVal = parseFloat(flourSlider.value) || 0;
319
+ flourValDisplay.textContent = `${flourVal}${isMetric ? 'g' : 'oz'}`;
320
+ const neutralizedSoda = getNeutralizedSoda(multiplier);
321
+ targetRecommendedSoda = neutralizedSoda;
322
+ const requiredPowder = flourVal * multiplier * 0.04;
323
+ const boosterPowder = Math.max(0, requiredPowder - (neutralizedSoda * 4));
324
+ simulatorSlider.max = Math.max(10, Math.ceil((neutralizedSoda / multiplier) * 2)).toString();
325
+ const simulatedSoda = parseFloat(simulatorSlider.value) * multiplier;
326
+ updateOutputs({ isMetric, neutralizedSoda, requiredPowder, boosterPowder, simulatedSoda });
327
+ updatePointer(simulatedSoda - neutralizedSoda, neutralizedSoda, simulatedSoda);
328
+ }
329
+
330
+ flourSlider.addEventListener('input', update);
331
+ simulatorSlider.addEventListener('input', update);
332
+
333
+ renderSliders();
334
+ update();
335
+ </script>
@@ -0,0 +1,26 @@
1
+ import type { CookingToolEntry } from '../../types';
2
+
3
+ export const leavenerAcidNeutralizer: CookingToolEntry = {
4
+ id: 'leavener-acid-neutralizer',
5
+ icons: {
6
+ bg: 'mdi:flask-outline',
7
+ fg: 'mdi:scale-balance',
8
+ },
9
+ i18n: {
10
+ es: () => import('./i18n/es').then((m) => m.content),
11
+ en: () => import('./i18n/en').then((m) => m.content),
12
+ fr: () => import('./i18n/fr').then((m) => m.content),
13
+ de: () => import('./i18n/de').then((m) => m.content),
14
+ it: () => import('./i18n/it').then((m) => m.content),
15
+ pt: () => import('./i18n/pt').then((m) => m.content),
16
+ nl: () => import('./i18n/nl').then((m) => m.content),
17
+ sv: () => import('./i18n/sv').then((m) => m.content),
18
+ ru: () => import('./i18n/ru').then((m) => m.content),
19
+ tr: () => import('./i18n/tr').then((m) => m.content),
20
+ pl: () => import('./i18n/pl').then((m) => m.content),
21
+ id: () => import('./i18n/id').then((m) => m.content),
22
+ ja: () => import('./i18n/ja').then((m) => m.content),
23
+ zh: () => import('./i18n/zh').then((m) => m.content),
24
+ ko: () => import('./i18n/ko').then((m) => m.content),
25
+ },
26
+ };