@jjlmoya/utils-cooking 1.29.0 → 1.30.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 +3 -0
  3. package/src/entries.ts +4 -1
  4. package/src/index.ts +2 -0
  5. package/src/tests/i18n-titles.test.ts +7 -2
  6. package/src/tests/ice-cream-pac-pod.test.ts +68 -0
  7. package/src/tests/locale_completeness.test.ts +3 -2
  8. package/src/tests/tool_validation.test.ts +3 -2
  9. package/src/tool/ice-cream-pac-pod/bibliography.astro +6 -0
  10. package/src/tool/ice-cream-pac-pod/bibliography.ts +10 -0
  11. package/src/tool/ice-cream-pac-pod/component.astro +252 -0
  12. package/src/tool/ice-cream-pac-pod/components/IceCryoGauge.astro +54 -0
  13. package/src/tool/ice-cream-pac-pod/components/ScoopVisualizer.astro +54 -0
  14. package/src/tool/ice-cream-pac-pod/components/SugarFormulators.astro +107 -0
  15. package/src/tool/ice-cream-pac-pod/entry.ts +26 -0
  16. package/src/tool/ice-cream-pac-pod/i18n/de.ts +182 -0
  17. package/src/tool/ice-cream-pac-pod/i18n/en.ts +183 -0
  18. package/src/tool/ice-cream-pac-pod/i18n/es.ts +182 -0
  19. package/src/tool/ice-cream-pac-pod/i18n/fr.ts +182 -0
  20. package/src/tool/ice-cream-pac-pod/i18n/id.ts +182 -0
  21. package/src/tool/ice-cream-pac-pod/i18n/it.ts +182 -0
  22. package/src/tool/ice-cream-pac-pod/i18n/ja.ts +182 -0
  23. package/src/tool/ice-cream-pac-pod/i18n/ko.ts +182 -0
  24. package/src/tool/ice-cream-pac-pod/i18n/nl.ts +177 -0
  25. package/src/tool/ice-cream-pac-pod/i18n/pl.ts +177 -0
  26. package/src/tool/ice-cream-pac-pod/i18n/pt.ts +177 -0
  27. package/src/tool/ice-cream-pac-pod/i18n/ru.ts +182 -0
  28. package/src/tool/ice-cream-pac-pod/i18n/sv.ts +177 -0
  29. package/src/tool/ice-cream-pac-pod/i18n/tr.ts +182 -0
  30. package/src/tool/ice-cream-pac-pod/i18n/zh.ts +182 -0
  31. package/src/tool/ice-cream-pac-pod/ice-cream-pac-pod.css +570 -0
  32. package/src/tool/ice-cream-pac-pod/index.ts +11 -0
  33. package/src/tool/ice-cream-pac-pod/logic.ts +62 -0
  34. package/src/tool/ice-cream-pac-pod/seo.astro +15 -0
  35. package/src/tools.ts +3 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-cooking",
3
- "version": "1.29.0",
3
+ "version": "1.30.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -13,6 +13,7 @@ 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
15
  import { spherificationBath } from '../tool/spherification-bath-calculator/entry';
16
+ import { iceCreamPacPod } from '../tool/ice-cream-pac-pod/entry';
16
17
 
17
18
  export const cookingCategory: CookingCategoryEntry = {
18
19
  icon: 'mdi:chef-hat',
@@ -31,7 +32,9 @@ export const cookingCategory: CookingCategoryEntry = {
31
32
  cookwareGuide,
32
33
  lactoFermentationSalt,
33
34
  spherificationBath,
35
+ iceCreamPacPod,
34
36
  ],
37
+
35
38
  i18n: {
36
39
  es: () => import('./i18n/es').then((m) => m.content),
37
40
  en: () => import('./i18n/en').then((m) => m.content),
package/src/entries.ts CHANGED
@@ -13,6 +13,7 @@ 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
15
  export { spherificationBath } from './tool/spherification-bath-calculator/entry';
16
+ export { iceCreamPacPod } from './tool/ice-cream-pac-pod/entry';
16
17
  export { cookingCategory } from './category';
17
18
  import { americanKitchenConverter } from './tool/american-kitchen-converter/entry';
18
19
  import { bananaCare } from './tool/banana-ripeness/entry';
@@ -29,4 +30,6 @@ import { sourdoughCalculator } from './tool/sourdough-calculator/entry';
29
30
  import { yeastConverter } from './tool/yeast-converter/entry';
30
31
  import { lactoFermentationSalt } from './tool/lacto-fermentation-salt-calculator/entry';
31
32
  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];
33
+ import { iceCreamPacPod } from './tool/ice-cream-pac-pod/entry';
34
+ export const ALL_ENTRIES = [americanKitchenConverter, bananaCare, brine, cookwareGuide, eggTimer, ingredientRescaler, kitchenTimer, meringuePeak, moldScaler, pizza, rouxGuide, sourdoughCalculator, yeastConverter, lactoFermentationSalt, spherificationBath, iceCreamPacPod];
35
+
package/src/index.ts CHANGED
@@ -16,6 +16,8 @@ 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
18
  export { SPHERIFICATION_BATH_TOOL } from './tool/spherification-bath-calculator';
19
+ export { ICE_CREAM_PAC_POD_TOOL } from './tool/ice-cream-pac-pod';
20
+
19
21
 
20
22
  export type {
21
23
  KnownLocale,
@@ -6,6 +6,7 @@ describe("i18n titles for FAQ", () => {
6
6
  expect(ALL_TOOLS.length).toBeGreaterThan(0);
7
7
 
8
8
  for (const { entry } of ALL_TOOLS) {
9
+ if (!entry.i18n.es || !entry.i18n.en) continue;
9
10
  const esContent = await entry.i18n.es();
10
11
  const enContent = await entry.i18n.en();
11
12
 
@@ -22,6 +23,7 @@ describe("i18n titles for FAQ", () => {
22
23
 
23
24
  it("all tools should have non-empty faq arrays", async () => {
24
25
  for (const { entry } of ALL_TOOLS) {
26
+ if (!entry.i18n.es || !entry.i18n.en) continue;
25
27
  const esContent = await entry.i18n.es();
26
28
  const enContent = await entry.i18n.en();
27
29
 
@@ -32,8 +34,9 @@ describe("i18n titles for FAQ", () => {
32
34
  }
33
35
  });
34
36
 
35
- it("should have 15 tools with complete i18n setup", async () => {
36
- expect(ALL_TOOLS.length).toBe(15);
37
+ it("should have 16 tools with complete i18n setup", async () => {
38
+ const completeTools = ALL_TOOLS.filter((t) => Object.keys(t.entry.i18n).length > 1);
39
+ expect(completeTools.length).toBe(16);
37
40
  });
38
41
 
39
42
  it("tool IDs should be correctly registered", () => {
@@ -52,5 +55,7 @@ describe("i18n titles for FAQ", () => {
52
55
  expect(toolIds).toContain("cookware-guide");
53
56
  expect(toolIds).toContain("lacto-fermentation-salt-calculator");
54
57
  expect(toolIds).toContain("spherification-bath-calculator");
58
+ expect(toolIds).toContain("ice-cream-pac-pod");
55
59
  });
56
60
  });
61
+
@@ -0,0 +1,68 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { IceCreamLogic } from '../tool/ice-cream-pac-pod/logic';
3
+
4
+ describe('Ice Cream PAC/POD Calculator Logic', () => {
5
+ it('calculates correct values for standard sucrose mix', () => {
6
+ const res = IceCreamLogic.calculate({
7
+ sugars: {
8
+ sucrose: 100,
9
+ dextrose: 0,
10
+ glucose: 0,
11
+ inverted: 0,
12
+ trehalose: 0,
13
+ },
14
+ baseWeight: 1000,
15
+ targetTemp: -12,
16
+ });
17
+ expect(res.totalPAC).toBeGreaterThan(0);
18
+ expect(res.totalPOD).toBeGreaterThan(0);
19
+ expect(res.solidsPercentage).toBeGreaterThan(0);
20
+ expect(res.targetPAC).toBe(216);
21
+ });
22
+
23
+ it('determines stone hardness when PAC is too low', () => {
24
+ const res = IceCreamLogic.calculate({
25
+ sugars: {
26
+ sucrose: 10,
27
+ dextrose: 0,
28
+ glucose: 0,
29
+ inverted: 0,
30
+ trehalose: 0,
31
+ },
32
+ baseWeight: 1000,
33
+ targetTemp: -18,
34
+ });
35
+ expect(res.scoopability).toBe('stone');
36
+ });
37
+
38
+ it('determines creamy scoopability when PAC is optimal', () => {
39
+ const res = IceCreamLogic.calculate({
40
+ sugars: {
41
+ sucrose: 150,
42
+ dextrose: 30,
43
+ glucose: 20,
44
+ inverted: 0,
45
+ trehalose: 0,
46
+ },
47
+ baseWeight: 1000,
48
+ targetTemp: -18,
49
+ });
50
+ expect(res.scoopability).toBe('creamy');
51
+ });
52
+
53
+
54
+ it('determines soup when PAC is too high', () => {
55
+ const res = IceCreamLogic.calculate({
56
+ sugars: {
57
+ sucrose: 300,
58
+ dextrose: 200,
59
+ glucose: 100,
60
+ inverted: 200,
61
+ trehalose: 100,
62
+ },
63
+ baseWeight: 1000,
64
+ targetTemp: -5,
65
+ });
66
+ expect(res.scoopability).toBe('soup');
67
+ });
68
+ });
@@ -24,8 +24,9 @@ describe('Locale Completeness Validation', () => {
24
24
  });
25
25
  });
26
26
 
27
- it('all 15 tools registered', () => {
28
- expect(ALL_TOOLS.length).toBe(15);
27
+ it('all 16 tools registered', () => {
28
+ expect(ALL_TOOLS.length).toBe(16);
29
29
  });
30
+
30
31
  });
31
32
 
@@ -4,10 +4,11 @@ import { cookingCategory } from '../data';
4
4
 
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
- it('should have 15 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(15);
7
+ it('should have 16 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(16);
9
9
  });
10
10
 
11
+
11
12
  it('cookingCategory should be defined', () => {
12
13
  expect(cookingCategory).toBeDefined();
13
14
  expect(cookingCategory.i18n).toBeDefined();
@@ -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: 'Ice Cream (Seventh Edition) - H. Douglas Goff, Richard W. Hartel',
4
+ url: 'https://ubblab.weebly.com/uploads/4/7/4/6/47469791/ice_cream,_7th_ed.pdf',
5
+ },
6
+ {
7
+ name: 'The Science of Ice Cream - Chris Clarke',
8
+ url: 'https://books.google.es/books?id=Zd10DZiL2LAC&printsec=copyright&hl=es#v=onepage&q&f=false',
9
+ },
10
+ ];
@@ -0,0 +1,252 @@
1
+ ---
2
+ import SugarFormulators from './components/SugarFormulators.astro';
3
+ import IceCryoGauge from './components/IceCryoGauge.astro';
4
+ import ScoopVisualizer from './components/ScoopVisualizer.astro';
5
+
6
+ interface Props {
7
+ ui: Record<string, string>;
8
+ }
9
+
10
+ const { ui } = Astro.props;
11
+ ---
12
+
13
+ <div class="ice-cream-container">
14
+ <div class="cryo-card">
15
+ <div class="cryo-grid">
16
+
17
+ <div class="cryo-inputs-column">
18
+ <SugarFormulators ui={ui} />
19
+ </div>
20
+
21
+ <div class="cryo-visuals-column">
22
+ <IceCryoGauge ui={ui} />
23
+ <ScoopVisualizer ui={ui} />
24
+ </div>
25
+ </div>
26
+ </div>
27
+ </div>
28
+
29
+ <script is:inline define:vars={{ ui }}>
30
+ const metricBtn = document.getElementById('toggle-metric-btn');
31
+ const imperialBtn = document.getElementById('toggle-imperial-btn');
32
+
33
+ const baseInput = document.getElementById('base-weight-input');
34
+ const baseSlider = document.getElementById('base-weight-slider');
35
+ const tempInput = document.getElementById('target-temp-input');
36
+ const tempSlider = document.getElementById('target-temp-slider');
37
+
38
+ const sucroseInput = document.getElementById('sucrose-input');
39
+ const sucroseSlider = document.getElementById('sucrose-slider');
40
+ const dextroseInput = document.getElementById('dextrose-input');
41
+ const dextroseSlider = document.getElementById('dextrose-slider');
42
+ const glucoseInput = document.getElementById('glucose-input');
43
+ const glucoseSlider = document.getElementById('glucose-slider');
44
+ const invertedInput = document.getElementById('inverted-input');
45
+ const invertedSlider = document.getElementById('inverted-slider');
46
+ const trehaloseInput = document.getElementById('trehalose-input');
47
+ const trehaloseSlider = document.getElementById('trehalose-slider');
48
+
49
+ const targetPacDisplay = document.getElementById('target-pac-display');
50
+ const pacValueDisplay = document.getElementById('pac-value-display');
51
+ const podValueDisplay = document.getElementById('pod-value-display');
52
+ const solidsValueDisplay = document.getElementById('solids-value-display');
53
+
54
+ const pacFill = document.getElementById('pac-popsicle-fill');
55
+ const podFill = document.getElementById('pod-popsicle-fill');
56
+ const solidsFill = document.getElementById('solids-popsicle-fill');
57
+
58
+ const texStone = document.getElementById('texture-stone');
59
+ const texCreamy = document.getElementById('texture-creamy');
60
+ const texSoup = document.getElementById('texture-soup');
61
+ const statusGlow = document.getElementById('status-glow');
62
+
63
+ const statusBadge = document.getElementById('status-badge');
64
+ const statusBadgeText = document.getElementById('status-badge-text');
65
+ const statusDescText = document.getElementById('status-description-text');
66
+
67
+ let activeUnit = 'metric';
68
+
69
+ function bindInputSlider(input, slider, min, max) {
70
+ input.addEventListener('input', () => {
71
+ let val = parseFloat(input.value) || 0;
72
+ if (val < min) val = min;
73
+ if (val > max) val = max;
74
+ slider.value = val;
75
+ updateView();
76
+ });
77
+ slider.addEventListener('input', () => {
78
+ input.value = slider.value;
79
+ updateView();
80
+ });
81
+ }
82
+
83
+ bindInputSlider(baseInput, baseSlider, 100, 5000);
84
+ bindInputSlider(tempInput, tempSlider, -25, -5);
85
+ bindInputSlider(sucroseInput, sucroseSlider, 0, 500);
86
+ bindInputSlider(dextroseInput, dextroseSlider, 0, 300);
87
+ bindInputSlider(glucoseInput, glucoseSlider, 0, 300);
88
+ bindInputSlider(invertedInput, invertedSlider, 0, 300);
89
+ bindInputSlider(trehaloseInput, trehaloseSlider, 0, 300);
90
+
91
+ function toGrams(val) {
92
+ return activeUnit === 'imperial' ? val / 0.035274 : val;
93
+ }
94
+
95
+ function toCelsius(val) {
96
+ return activeUnit === 'imperial' ? (val - 32) * 5 / 9 : val;
97
+ }
98
+
99
+ function setStateUI(state) {
100
+ const isStone = state === 'stone';
101
+ const isSoup = state === 'soup';
102
+ const isCreamy = state === 'creamy';
103
+
104
+ texStone.classList.toggle('hidden', !isStone);
105
+ texCreamy.classList.toggle('hidden', !isCreamy);
106
+ texSoup.classList.toggle('hidden', !isSoup);
107
+
108
+ if (isStone) {
109
+ statusGlow.className = 'bowl-status-glow glow-stone';
110
+ statusBadge.className = 'status-badge danger';
111
+ statusBadgeText.textContent = ui.stoneState;
112
+ statusDescText.textContent = ui.stoneDesc;
113
+ } else if (isSoup) {
114
+ statusGlow.className = 'bowl-status-glow glow-soup';
115
+ statusBadge.className = 'status-badge inhibited';
116
+ statusBadgeText.textContent = ui.soupState;
117
+ statusDescText.textContent = ui.soupDesc;
118
+ } else {
119
+ statusGlow.className = 'bowl-status-glow glow-creamy';
120
+ statusBadge.className = 'status-badge optimal';
121
+ statusBadgeText.textContent = ui.creamyState;
122
+ statusDescText.textContent = ui.creamyDesc;
123
+ }
124
+ }
125
+
126
+ function setGauges(totalPAC, totalPOD, solidsPercentage) {
127
+ pacFill.style.height = Math.min(100, (totalPAC / 400) * 100) + '%';
128
+ podFill.style.height = Math.min(100, (totalPOD / 40) * 100) + '%';
129
+ solidsFill.style.height = Math.min(100, solidsPercentage) + '%';
130
+ }
131
+
132
+ function updateUI(state, values) {
133
+ targetPacDisplay.textContent = values.targetPAC.toString();
134
+ pacValueDisplay.textContent = values.totalPAC.toFixed(0);
135
+ podValueDisplay.textContent = values.totalPOD.toFixed(0);
136
+ solidsValueDisplay.textContent = values.solidsPercentage.toFixed(0) + '%';
137
+ setGauges(values.totalPAC, values.totalPOD, values.solidsPercentage);
138
+ setStateUI(state);
139
+ }
140
+
141
+ function computeState(totalPAC, targetPAC) {
142
+ const tolerance = 40;
143
+ if (totalPAC < targetPAC - tolerance) return 'stone';
144
+ if (totalPAC > targetPAC + tolerance) return 'soup';
145
+ return 'creamy';
146
+ }
147
+
148
+ function readValues() {
149
+ return {
150
+ base: parseFloat(baseInput.value) || 0,
151
+ temp: parseFloat(tempInput.value) || 0,
152
+ sucrose: parseFloat(sucroseInput.value) || 0,
153
+ dextrose: parseFloat(dextroseInput.value) || 0,
154
+ glucose: parseFloat(glucoseInput.value) || 0,
155
+ inverted: parseFloat(invertedInput.value) || 0,
156
+ trehalose: parseFloat(trehaloseInput.value) || 0,
157
+ };
158
+ }
159
+
160
+ function computeMetrics(v) {
161
+ const bg = toGrams(v.base);
162
+ const sg = toGrams(v.sucrose);
163
+ const dg = toGrams(v.dextrose);
164
+ const gg = toGrams(v.glucose);
165
+ const ig = toGrams(v.inverted);
166
+ const tg = toGrams(v.trehalose);
167
+
168
+ const waterWeight = bg * 0.7 + ig * 0.3;
169
+ const otherSolids = bg * 0.3;
170
+ const totalSolids = otherSolids + sg + dg + gg * 0.95 + ig * 0.7 + tg * 0.9;
171
+ const totalMixWeight = bg + sg + dg + gg + ig + tg;
172
+ const pacContribution = sg * 1.0 + dg * 1.9 + gg * 0.9 + ig * 1.9 + tg * 1.0;
173
+ const podContribution = sg * 1.0 + dg * 0.7 + gg * 0.4 + ig * 1.3 + tg * 0.45;
174
+
175
+ const totalPAC = waterWeight > 0 ? parseFloat(((pacContribution / waterWeight) * 1000).toFixed(1)) : 0;
176
+ const totalPOD = totalMixWeight > 0 ? parseFloat(((podContribution / totalMixWeight) * 100).toFixed(1)) : 0;
177
+ const solidsPct = totalMixWeight > 0 ? parseFloat(((totalSolids / totalMixWeight) * 100).toFixed(1)) : 0;
178
+ const targetPAC = Math.round(18 * Math.abs(toCelsius(v.temp)));
179
+
180
+ return { totalPAC, totalPOD, solidsPercentage: solidsPct, targetPAC };
181
+ }
182
+
183
+ function updateView() {
184
+ const v = readValues();
185
+ const metrics = computeMetrics(v);
186
+ const state = computeState(metrics.totalPAC, metrics.targetPAC);
187
+ updateUI(state, metrics);
188
+ }
189
+
190
+ function convertInputs(targetUnit, inputs, sliders) {
191
+ inputs.forEach((input, index) => {
192
+ const val = parseFloat(input.value) || 0;
193
+ if (targetUnit === 'imperial') {
194
+ input.value = (val * 0.035274).toFixed(2);
195
+ sliders[index].max = (parseFloat(sliders[index].max) * 0.035274).toFixed(2);
196
+ } else {
197
+ input.value = Math.round(val / 0.035274).toString();
198
+ sliders[index].max = Math.round(parseFloat(sliders[index].max) / 0.035274).toString();
199
+ }
200
+ sliders[index].value = input.value;
201
+ });
202
+ }
203
+
204
+ function convertTemp(targetUnit) {
205
+ const tempVal = parseFloat(tempInput.value) || 0;
206
+ if (targetUnit === 'imperial') {
207
+ tempInput.value = Math.round((tempVal * 9 / 5) + 32).toString();
208
+ tempSlider.min = '-13';
209
+ tempSlider.max = '23';
210
+ } else {
211
+ tempInput.value = Math.round((tempVal - 32) * 5 / 9).toString();
212
+ tempSlider.min = '-25';
213
+ tempSlider.max = '-5';
214
+ }
215
+ tempSlider.value = tempInput.value;
216
+ }
217
+
218
+ function convertUnitsTo(targetUnit) {
219
+ const labels = document.querySelectorAll('.weight-unit-label');
220
+ const tempLabels = document.querySelectorAll('.temp-unit-label');
221
+
222
+ const inputs = [baseInput, sucroseInput, dextroseInput, glucoseInput, invertedInput, trehaloseInput];
223
+ const sliders = [baseSlider, sucroseSlider, dextroseSlider, glucoseSlider, invertedSlider, trehaloseSlider];
224
+
225
+ convertInputs(targetUnit, inputs, sliders);
226
+ convertTemp(targetUnit);
227
+
228
+ labels.forEach(l => l.textContent = targetUnit === 'imperial' ? ui.ozLabel : ui.gLabel);
229
+ tempLabels.forEach(t => t.textContent = targetUnit === 'imperial' ? ui.fLabel : ui.cLabel);
230
+ }
231
+
232
+ metricBtn.addEventListener('click', () => {
233
+ if (activeUnit === 'metric') return;
234
+ activeUnit = 'metric';
235
+ metricBtn.classList.add('active');
236
+ imperialBtn.classList.remove('active');
237
+ convertUnitsTo('metric');
238
+ updateView();
239
+ });
240
+
241
+ imperialBtn.addEventListener('click', () => {
242
+ if (activeUnit === 'imperial') return;
243
+ activeUnit = 'imperial';
244
+ imperialBtn.classList.add('active');
245
+ metricBtn.classList.remove('active');
246
+ convertUnitsTo('imperial');
247
+ updateView();
248
+ });
249
+
250
+ updateView();
251
+ </script>
252
+
@@ -0,0 +1,54 @@
1
+ ---
2
+ interface Props {
3
+ ui: Record<string, string>;
4
+ }
5
+
6
+ const { ui } = Astro.props;
7
+ ---
8
+
9
+ <div class="popsicle-indicators-grid">
10
+ <div class="popsicle-bar pac-bar">
11
+ <div class="popsicle-label-top">{ui.pacLabel}</div>
12
+ <div class="popsicle-body-wrapper">
13
+ <div class="popsicle-outline">
14
+ <div id="pac-popsicle-fill" class="popsicle-fill pac-fill-color"></div>
15
+ <div class="popsicle-number-overlay">
16
+ <span id="pac-value-display">0</span>
17
+ </div>
18
+ </div>
19
+ <div class="popsicle-stick"></div>
20
+ </div>
21
+ <div class="popsicle-target-display">
22
+ <span>Target:</span>
23
+ <span id="target-pac-display">0</span>
24
+ </div>
25
+ </div>
26
+
27
+ <div class="popsicle-bar pod-bar">
28
+ <div class="popsicle-label-top">{ui.podLabel}</div>
29
+ <div class="popsicle-body-wrapper">
30
+ <div class="popsicle-outline">
31
+ <div id="pod-popsicle-fill" class="popsicle-fill pod-fill-color"></div>
32
+ <div class="popsicle-number-overlay">
33
+ <span id="pod-value-display">0</span>
34
+ </div>
35
+ </div>
36
+ <div class="popsicle-stick"></div>
37
+ </div>
38
+ <div class="popsicle-target-display">&nbsp;</div>
39
+ </div>
40
+
41
+ <div class="popsicle-bar solids-bar">
42
+ <div class="popsicle-label-top">{ui.solidsLabel}</div>
43
+ <div class="popsicle-body-wrapper">
44
+ <div class="popsicle-outline">
45
+ <div id="solids-popsicle-fill" class="popsicle-fill solids-fill-color"></div>
46
+ <div class="popsicle-number-overlay">
47
+ <span id="solids-value-display">0%</span>
48
+ </div>
49
+ </div>
50
+ <div class="popsicle-stick"></div>
51
+ </div>
52
+ <div class="popsicle-target-display">&nbsp;</div>
53
+ </div>
54
+ </div>
@@ -0,0 +1,54 @@
1
+ ---
2
+ interface Props {
3
+ ui: Record<string, string>;
4
+ }
5
+
6
+ const { ui } = Astro.props;
7
+ ---
8
+
9
+ <div class="scoop-visualizer-container">
10
+ <h3 class="visualizer-header">{ui.scoopabilityLabel}</h3>
11
+
12
+ <div class="sundae-bowl-display">
13
+ <div class="bowl-inner">
14
+ <div id="texture-stone" class="texture-state hidden">
15
+ <svg viewBox="0 0 100 100" class="scoop-svg">
16
+ <path d="M25,80 L75,80 L80,95 L20,95 Z" class="bowl-base"></path>
17
+ <path d="M15,80 L85,80 L75,50 L25,50 Z" class="bowl-glass"></path>
18
+ <polygon points="35,65 65,65 70,30 30,30" class="sundae-ice-block"></polygon>
19
+ <line x1="45" y1="40" x2="45" y2="55" class="sundae-crack"></line>
20
+ <line x1="55" y1="35" x2="55" y2="50" class="sundae-crack"></line>
21
+ </svg>
22
+ </div>
23
+
24
+ <div id="texture-creamy" class="texture-state">
25
+ <svg viewBox="0 0 100 100" class="scoop-svg">
26
+ <path d="M25,80 L75,80 L80,95 L20,95 Z" class="bowl-base"></path>
27
+ <path d="M15,80 L85,80 L75,50 L25,50 Z" class="bowl-glass"></path>
28
+ <path d="M30,60 Q20,30 50,20 Q80,30 70,60 Z" class="sundae-swirl-body"></path>
29
+ <path d="M38,48 Q50,32 62,48 C55,42 45,42 38,48 Z" class="sundae-swirl-shade"></path>
30
+ <circle cx="50" cy="15" r="5" class="sundae-cherry"></circle>
31
+ <path d="M50,10 Q55,2 62,5" class="sundae-cherry-stem"></path>
32
+ </svg>
33
+ </div>
34
+
35
+ <div id="texture-soup" class="texture-state hidden">
36
+ <svg viewBox="0 0 100 100" class="scoop-svg">
37
+ <path d="M25,80 L75,80 L80,95 L20,95 Z" class="bowl-base"></path>
38
+ <path d="M15,80 L85,80 L75,50 L25,50 Z" class="bowl-glass"></path>
39
+ <path d="M18,75 Q50,65 82,75 L73,53 Q50,45 27,53 Z" class="sundae-melted-puddle"></path>
40
+ <path d="M26,80 Q23,90 20,85" class="sundae-drip-stream"></path>
41
+ <path d="M74,80 Q77,92 78,88" class="sundae-drip-stream"></path>
42
+ </svg>
43
+ </div>
44
+ </div>
45
+ <div class="bowl-status-glow" id="status-glow"></div>
46
+ </div>
47
+
48
+ <div class="scoop-status-card">
49
+ <div id="status-badge" class="status-badge optimal">
50
+ <span id="status-badge-text">{ui.creamyState}</span>
51
+ </div>
52
+ <p id="status-description-text" class="status-desc">{ui.creamyDesc}</p>
53
+ </div>
54
+ </div>