@jjlmoya/utils-cooking 1.29.0 → 1.31.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 (63) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +5 -0
  3. package/src/entries.ts +6 -1
  4. package/src/index.ts +3 -0
  5. package/src/tests/i18n-titles.test.ts +8 -2
  6. package/src/tests/i18n_coverage.test.ts +1 -0
  7. package/src/tests/ice-cream-pac-pod.test.ts +68 -0
  8. package/src/tests/locale_completeness.test.ts +3 -2
  9. package/src/tests/tool_validation.test.ts +3 -2
  10. package/src/tool/botulism-canning-safety/bibliography.astro +6 -0
  11. package/src/tool/botulism-canning-safety/bibliography.ts +10 -0
  12. package/src/tool/botulism-canning-safety/botulism-canning-safety.css +545 -0
  13. package/src/tool/botulism-canning-safety/component.astro +296 -0
  14. package/src/tool/botulism-canning-safety/components/AutoclaveDisplay.astro +62 -0
  15. package/src/tool/botulism-canning-safety/components/SporeVisualizer.astro +48 -0
  16. package/src/tool/botulism-canning-safety/components/ThermalControls.astro +60 -0
  17. package/src/tool/botulism-canning-safety/entry.ts +26 -0
  18. package/src/tool/botulism-canning-safety/i18n/de.ts +188 -0
  19. package/src/tool/botulism-canning-safety/i18n/en.ts +188 -0
  20. package/src/tool/botulism-canning-safety/i18n/es.ts +188 -0
  21. package/src/tool/botulism-canning-safety/i18n/fr.ts +188 -0
  22. package/src/tool/botulism-canning-safety/i18n/id.ts +188 -0
  23. package/src/tool/botulism-canning-safety/i18n/it.ts +188 -0
  24. package/src/tool/botulism-canning-safety/i18n/ja.ts +188 -0
  25. package/src/tool/botulism-canning-safety/i18n/ko.ts +188 -0
  26. package/src/tool/botulism-canning-safety/i18n/nl.ts +188 -0
  27. package/src/tool/botulism-canning-safety/i18n/pl.ts +188 -0
  28. package/src/tool/botulism-canning-safety/i18n/pt.ts +188 -0
  29. package/src/tool/botulism-canning-safety/i18n/ru.ts +188 -0
  30. package/src/tool/botulism-canning-safety/i18n/sv.ts +188 -0
  31. package/src/tool/botulism-canning-safety/i18n/tr.ts +188 -0
  32. package/src/tool/botulism-canning-safety/i18n/zh.ts +188 -0
  33. package/src/tool/botulism-canning-safety/index.ts +11 -0
  34. package/src/tool/botulism-canning-safety/logic.ts +47 -0
  35. package/src/tool/botulism-canning-safety/seo.astro +15 -0
  36. package/src/tool/ice-cream-pac-pod/bibliography.astro +6 -0
  37. package/src/tool/ice-cream-pac-pod/bibliography.ts +10 -0
  38. package/src/tool/ice-cream-pac-pod/component.astro +291 -0
  39. package/src/tool/ice-cream-pac-pod/components/IceCryoGauge.astro +54 -0
  40. package/src/tool/ice-cream-pac-pod/components/ScoopVisualizer.astro +54 -0
  41. package/src/tool/ice-cream-pac-pod/components/SugarFormulators.astro +107 -0
  42. package/src/tool/ice-cream-pac-pod/entry.ts +26 -0
  43. package/src/tool/ice-cream-pac-pod/i18n/de.ts +182 -0
  44. package/src/tool/ice-cream-pac-pod/i18n/en.ts +183 -0
  45. package/src/tool/ice-cream-pac-pod/i18n/es.ts +182 -0
  46. package/src/tool/ice-cream-pac-pod/i18n/fr.ts +182 -0
  47. package/src/tool/ice-cream-pac-pod/i18n/id.ts +182 -0
  48. package/src/tool/ice-cream-pac-pod/i18n/it.ts +182 -0
  49. package/src/tool/ice-cream-pac-pod/i18n/ja.ts +182 -0
  50. package/src/tool/ice-cream-pac-pod/i18n/ko.ts +182 -0
  51. package/src/tool/ice-cream-pac-pod/i18n/nl.ts +177 -0
  52. package/src/tool/ice-cream-pac-pod/i18n/pl.ts +177 -0
  53. package/src/tool/ice-cream-pac-pod/i18n/pt.ts +177 -0
  54. package/src/tool/ice-cream-pac-pod/i18n/ru.ts +182 -0
  55. package/src/tool/ice-cream-pac-pod/i18n/sv.ts +177 -0
  56. package/src/tool/ice-cream-pac-pod/i18n/tr.ts +182 -0
  57. package/src/tool/ice-cream-pac-pod/i18n/zh.ts +182 -0
  58. package/src/tool/ice-cream-pac-pod/ice-cream-pac-pod.css +570 -0
  59. package/src/tool/ice-cream-pac-pod/index.ts +11 -0
  60. package/src/tool/ice-cream-pac-pod/logic.ts +62 -0
  61. package/src/tool/ice-cream-pac-pod/seo.astro +15 -0
  62. package/src/tool/spherification-bath-calculator/component.astro +37 -0
  63. package/src/tools.ts +5 -0
@@ -0,0 +1,570 @@
1
+ .ice-cream-container {
2
+ --color-cone-bg: #fffbf2;
3
+ --color-panel-bg: #fff;
4
+ --color-border-neon: #ec4899;
5
+ --color-border-mute: #fcd34d;
6
+ --color-text-bright: #7c2d12;
7
+ --color-text-dim: #9a3412;
8
+ --color-neon-blue: #0284c7;
9
+ --color-neon-pink: #db2777;
10
+ --color-neon-yellow: #ca8a04;
11
+ --color-neon-green: #16a34a;
12
+ --color-fader-track: #fef3c7;
13
+ --color-fader-thumb: #ec4899;
14
+ --color-fader-thumb-hover: #db2777;
15
+ --color-stick-bg: #fed7aa;
16
+ --color-stick-border: #f97316;
17
+ --color-shadow-ambient: rgba(124, 45, 18, 0.08);
18
+ --color-cherry: #dc2626;
19
+ --bg-vanilla: #fef9c3;
20
+ --bg-strawberry: #fce7f3;
21
+ --bg-mint: #d1fae5;
22
+ --bg-chocolate: #ffedd5;
23
+ --bg-peach: #ffe4e6;
24
+ --color-scoop-text: #7c2d12;
25
+
26
+ display: flex;
27
+ flex-direction: column;
28
+ align-items: center;
29
+ justify-content: center;
30
+ width: 100%;
31
+ box-sizing: border-box;
32
+ }
33
+
34
+ .theme-dark .ice-cream-container {
35
+ --color-cone-bg: #0f172a;
36
+ --color-panel-bg: #1e293b;
37
+ --color-border-neon: #f472b6;
38
+ --color-border-mute: #334155;
39
+ --color-text-bright: #f8fafc;
40
+ --color-text-dim: #94a3b8;
41
+ --color-neon-blue: #38bdf8;
42
+ --color-neon-pink: #f472b6;
43
+ --color-neon-yellow: #facc15;
44
+ --color-neon-green: #4ade80;
45
+ --color-fader-track: #0f172a;
46
+ --color-fader-thumb: #f472b6;
47
+ --color-fader-thumb-hover: #ef4444;
48
+ --color-stick-bg: #475569;
49
+ --color-stick-border: #334155;
50
+ --color-shadow-ambient: rgba(0, 0, 0, 0.4);
51
+ --bg-vanilla: rgba(253, 244, 195, 0.15);
52
+ --bg-strawberry: rgba(252, 231, 243, 0.15);
53
+ --bg-mint: rgba(209, 250, 229, 0.15);
54
+ --bg-chocolate: rgba(255, 237, 213, 0.15);
55
+ --bg-peach: rgba(255, 228, 230, 0.15);
56
+ --color-scoop-text: #f8fafc;
57
+ }
58
+
59
+ .cryo-card {
60
+ position: relative;
61
+ width: 100%;
62
+ max-width: 950px;
63
+ background: var(--color-cone-bg);
64
+ border: 4px solid var(--color-border-mute);
65
+ border-radius: 28px;
66
+ padding: 2rem;
67
+ box-shadow: 0 25px 50px -12px var(--color-shadow-ambient);
68
+ box-sizing: border-box;
69
+ }
70
+
71
+ .cryo-grid {
72
+ display: grid;
73
+ grid-template-columns: 1.25fr 1fr;
74
+ gap: 2rem;
75
+ }
76
+
77
+ @media (max-width: 768px) {
78
+ .cryo-grid {
79
+ grid-template-columns: 1fr;
80
+ }
81
+ }
82
+
83
+ .ice-cream-recipe-stack {
84
+ display: flex;
85
+ flex-direction: column;
86
+ gap: 1.5rem;
87
+ }
88
+
89
+ .base-parameters-board {
90
+ background: var(--color-panel-bg);
91
+ border: 3px solid var(--color-border-mute);
92
+ border-radius: 20px;
93
+ padding: 1.5rem;
94
+ box-sizing: border-box;
95
+ }
96
+
97
+ .board-title {
98
+ font-size: 1.1rem;
99
+ font-weight: 900;
100
+ color: var(--color-text-bright);
101
+ text-transform: uppercase;
102
+ letter-spacing: 0.05em;
103
+ margin-bottom: 1.25rem;
104
+ border-bottom: 3px dashed var(--color-border-mute);
105
+ padding-bottom: 0.5rem;
106
+ }
107
+
108
+ .unit-toggle-container {
109
+ display: flex;
110
+ gap: 0.75rem;
111
+ margin-bottom: 1.5rem;
112
+ }
113
+
114
+ .unit-btn {
115
+ flex: 1;
116
+ padding: 0.6rem;
117
+ background: var(--color-cone-bg);
118
+ border: 2px solid var(--color-border-mute);
119
+ color: var(--color-text-dim);
120
+ font-weight: 900;
121
+ font-size: 0.95rem;
122
+ border-radius: 10px;
123
+ cursor: pointer;
124
+ transition: all 0.2s ease;
125
+ }
126
+
127
+ .unit-btn.active {
128
+ background: var(--color-border-neon);
129
+ border-color: var(--color-border-neon);
130
+ color: var(--color-panel-bg);
131
+ }
132
+
133
+ .base-field {
134
+ margin-bottom: 1.25rem;
135
+ display: flex;
136
+ flex-direction: column;
137
+ gap: 0.5rem;
138
+ }
139
+
140
+ .field-header {
141
+ display: flex;
142
+ justify-content: space-between;
143
+ align-items: center;
144
+ }
145
+
146
+ .field-label {
147
+ font-size: 1rem;
148
+ font-weight: 800;
149
+ color: var(--color-text-bright);
150
+ }
151
+
152
+ .field-value-wrap {
153
+ display: flex;
154
+ align-items: center;
155
+ gap: 0.25rem;
156
+ }
157
+
158
+ .field-number {
159
+ width: 70px;
160
+ background: var(--color-cone-bg);
161
+ border: 2px solid var(--color-border-mute);
162
+ color: var(--color-text-bright);
163
+ text-align: right;
164
+ padding: 0.25rem 0.5rem;
165
+ font-size: 1rem;
166
+ font-weight: 900;
167
+ border-radius: 8px;
168
+ }
169
+
170
+ .cone-slider {
171
+ -webkit-appearance: none;
172
+ appearance: none;
173
+ width: 100%;
174
+ height: 8px;
175
+ background: var(--color-fader-track);
176
+ border-radius: 4px;
177
+ outline: none;
178
+ border: 1px solid var(--color-border-mute);
179
+ }
180
+
181
+ .cone-slider::-webkit-slider-thumb {
182
+ -webkit-appearance: none;
183
+ appearance: none;
184
+ width: 18px;
185
+ height: 18px;
186
+ border-radius: 50%;
187
+ background: var(--color-border-neon);
188
+ border: 2px solid #fff;
189
+ cursor: pointer;
190
+ }
191
+
192
+ .scoop-recipe-cone {
193
+ display: flex;
194
+ flex-direction: column;
195
+ gap: 0.75rem;
196
+ }
197
+
198
+ .scoop-scoop {
199
+ border: 3px solid var(--color-border-mute);
200
+ border-radius: 24px;
201
+ padding: 1.25rem;
202
+ box-sizing: border-box;
203
+ }
204
+
205
+ .sucrose-scoop { background: var(--bg-vanilla); }
206
+ .dextrose-scoop { background: var(--bg-strawberry); }
207
+ .glucose-scoop { background: var(--bg-mint); }
208
+ .inverted-scoop { background: var(--bg-chocolate); }
209
+ .trehalose-scoop { background: var(--bg-peach); }
210
+
211
+ .scoop-inner-content {
212
+ display: flex;
213
+ flex-direction: column;
214
+ gap: 0.5rem;
215
+ }
216
+
217
+ .scoop-label-row {
218
+ display: flex;
219
+ justify-content: space-between;
220
+ align-items: center;
221
+ }
222
+
223
+ .scoop-text-label {
224
+ font-size: 1.05rem;
225
+ font-weight: 900;
226
+ color: var(--color-scoop-text);
227
+ }
228
+
229
+ .scoop-readout {
230
+ display: flex;
231
+ align-items: center;
232
+ gap: 0.25rem;
233
+ }
234
+
235
+ .scoop-number-input {
236
+ -moz-appearance: textfield;
237
+ width: 65px;
238
+ background: rgba(255, 255, 255, 0.5);
239
+ border: 2px solid var(--color-border-mute);
240
+ color: var(--color-scoop-text);
241
+ text-align: center;
242
+ font-size: 1rem;
243
+ font-weight: 900;
244
+ border-radius: 8px;
245
+ padding: 0.25rem;
246
+ }
247
+
248
+ .theme-dark .scoop-number-input {
249
+ background: rgba(0, 0, 0, 0.4);
250
+ }
251
+
252
+ .scoop-number-input::-webkit-outer-spin-button,
253
+ .scoop-number-input::-webkit-inner-spin-button {
254
+ -webkit-appearance: none;
255
+ margin: 0;
256
+ }
257
+
258
+ .scoop-slider-bar {
259
+ -webkit-appearance: none;
260
+ appearance: none;
261
+ width: 100%;
262
+ height: 8px;
263
+ background: rgba(255, 255, 255, 0.4);
264
+ border-radius: 4px;
265
+ outline: none;
266
+ border: 1px solid rgba(0, 0, 0, 0.05);
267
+ }
268
+
269
+ .theme-dark .scoop-slider-bar {
270
+ background: rgba(0, 0, 0, 0.4);
271
+ }
272
+
273
+ .scoop-slider-bar::-webkit-slider-thumb {
274
+ -webkit-appearance: none;
275
+ appearance: none;
276
+ width: 20px;
277
+ height: 20px;
278
+ border-radius: 50%;
279
+ background: #f00;
280
+ border: 2px solid #fff;
281
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
282
+ cursor: pointer;
283
+ }
284
+
285
+ .popsicle-indicators-grid {
286
+ display: grid;
287
+ grid-template-columns: repeat(3, 1fr);
288
+ gap: 0.75rem;
289
+ margin-bottom: 1.5rem;
290
+ }
291
+
292
+ .popsicle-bar {
293
+ display: flex;
294
+ flex-direction: column;
295
+ align-items: center;
296
+ }
297
+
298
+ .popsicle-label-top {
299
+ font-size: 0.95rem;
300
+ font-weight: 900;
301
+ text-transform: uppercase;
302
+ color: var(--color-text-dim);
303
+ margin-bottom: 0.5rem;
304
+ text-align: center;
305
+ min-height: 2.75rem;
306
+ display: flex;
307
+ align-items: center;
308
+ justify-content: center;
309
+ }
310
+
311
+
312
+ .popsicle-body-wrapper {
313
+ position: relative;
314
+ display: flex;
315
+ flex-direction: column;
316
+ align-items: center;
317
+ }
318
+
319
+ .popsicle-outline {
320
+ position: relative;
321
+ width: 70px;
322
+ height: 180px;
323
+ background: var(--color-panel-bg);
324
+ border: 3px solid var(--color-border-mute);
325
+ border-radius: 35px 35px 12px 12px;
326
+ overflow: hidden;
327
+ box-shadow: 0 4px 6px var(--color-shadow-ambient);
328
+ }
329
+
330
+ .popsicle-fill {
331
+ position: absolute;
332
+ bottom: 0;
333
+ left: 0;
334
+ width: 100%;
335
+ height: 0%;
336
+ transition: height 0.35s ease;
337
+ }
338
+
339
+ .pac-fill-color {
340
+ background: linear-gradient(0deg, var(--color-neon-blue) 0%, #a5f3fc 100%);
341
+ }
342
+
343
+ .pod-fill-color {
344
+ background: linear-gradient(0deg, var(--color-neon-pink) 0%, #fbcfe8 100%);
345
+ }
346
+
347
+ .solids-fill-color {
348
+ background: linear-gradient(0deg, var(--color-neon-green) 0%, #ccfbf1 100%);
349
+ }
350
+
351
+ .popsicle-number-overlay {
352
+ position: absolute;
353
+ top: 0;
354
+ left: 0;
355
+ width: 100%;
356
+ height: 100%;
357
+ display: flex;
358
+ align-items: center;
359
+ justify-content: center;
360
+ pointer-events: none;
361
+ }
362
+
363
+ .popsicle-number-overlay span {
364
+ font-size: 1.4rem;
365
+ font-weight: 950;
366
+ color: var(--color-text-bright);
367
+ text-shadow: 0 1px 2px var(--color-panel-bg);
368
+ }
369
+
370
+ .popsicle-stick {
371
+ width: 16px;
372
+ height: 35px;
373
+ background: var(--color-stick-bg);
374
+ border: 2px solid var(--color-stick-border);
375
+ border-top: none;
376
+ border-radius: 0 0 8px 8px;
377
+ }
378
+
379
+ .popsicle-target-display {
380
+ margin-top: 0.5rem;
381
+ font-size: 0.85rem;
382
+ font-weight: 800;
383
+ color: var(--color-text-dim);
384
+ }
385
+
386
+ .scoop-visualizer-container {
387
+ background: var(--color-panel-bg);
388
+ border: 3px solid var(--color-border-mute);
389
+ border-radius: 20px;
390
+ padding: 1.5rem;
391
+ display: flex;
392
+ flex-direction: column;
393
+ align-items: center;
394
+ box-sizing: border-box;
395
+ }
396
+
397
+ .visualizer-header {
398
+ font-size: 0.95rem;
399
+ font-weight: 900;
400
+ text-transform: uppercase;
401
+ color: var(--color-text-dim);
402
+ margin: 0 0 1rem;
403
+ }
404
+
405
+ .sundae-bowl-display {
406
+ position: relative;
407
+ width: 100%;
408
+ max-width: 220px;
409
+ aspect-ratio: 1;
410
+ background: var(--color-cone-bg);
411
+ border: 3px solid var(--color-border-mute);
412
+ border-radius: 16px;
413
+ display: flex;
414
+ align-items: center;
415
+ justify-content: center;
416
+ overflow: hidden;
417
+ margin-bottom: 1.25rem;
418
+ }
419
+
420
+ .bowl-inner {
421
+ position: relative;
422
+ width: 90%;
423
+ height: 90%;
424
+ }
425
+
426
+ .bowl-base {
427
+ fill: var(--color-border-mute);
428
+ stroke: var(--color-text-dim);
429
+ stroke-width: 2;
430
+ }
431
+
432
+ .bowl-glass {
433
+ fill: none;
434
+ stroke: var(--color-text-dim);
435
+ stroke-width: 2;
436
+ }
437
+
438
+ .sundae-ice-block {
439
+ fill: var(--color-neon-blue);
440
+ stroke: var(--color-text-bright);
441
+ stroke-width: 2;
442
+ opacity: 0.9;
443
+ }
444
+
445
+ .sundae-crack {
446
+ stroke: var(--color-panel-bg);
447
+ stroke-width: 2;
448
+ stroke-linecap: round;
449
+ }
450
+
451
+ .sundae-swirl-body {
452
+ fill: var(--color-neon-pink);
453
+ stroke: var(--color-text-bright);
454
+ stroke-width: 2;
455
+ animation: float-scoop 3s infinite ease-in-out;
456
+ }
457
+
458
+ .sundae-swirl-shade {
459
+ fill: var(--color-panel-bg);
460
+ opacity: 0.45;
461
+ }
462
+
463
+ .sundae-cherry {
464
+ fill: var(--color-cherry);
465
+ stroke: var(--color-text-bright);
466
+ stroke-width: 1.5;
467
+ }
468
+
469
+ .sundae-cherry-stem {
470
+ fill: none;
471
+ stroke: var(--color-text-bright);
472
+ stroke-width: 1.5;
473
+ stroke-linecap: round;
474
+ }
475
+
476
+ .sundae-melted-puddle {
477
+ fill: var(--color-neon-pink);
478
+ stroke: var(--color-text-bright);
479
+ stroke-width: 2;
480
+ opacity: 0.85;
481
+ }
482
+
483
+ .sundae-drip-stream {
484
+ fill: none;
485
+ stroke: var(--color-neon-pink);
486
+ stroke-width: 3;
487
+ stroke-linecap: round;
488
+ animation: drip-stream-fall 2s infinite ease-in;
489
+ }
490
+
491
+ @keyframes float-scoop {
492
+ 0%, 100% {
493
+ transform: translateY(0);
494
+ }
495
+ 50% {
496
+ transform: translateY(-4px);
497
+ }
498
+ }
499
+
500
+ .bowl-status-glow {
501
+ position: absolute;
502
+ top: 0;
503
+ left: 0;
504
+ width: 100%;
505
+ height: 100%;
506
+ pointer-events: none;
507
+ transition: background 0.3s ease;
508
+ }
509
+
510
+ .bowl-status-glow.glow-creamy {
511
+ background: radial-gradient(circle, var(--color-glow-solids) 0%, transparent 80%);
512
+ }
513
+
514
+ .bowl-status-glow.glow-stone {
515
+ background: radial-gradient(circle, var(--color-glow-pac) 0%, transparent 80%);
516
+ }
517
+
518
+ .bowl-status-glow.glow-soup {
519
+ background: radial-gradient(circle, var(--color-glow-pod) 0%, transparent 80%);
520
+ }
521
+
522
+ .scoop-status-card {
523
+ text-align: center;
524
+ width: 100%;
525
+ }
526
+
527
+ .status-badge {
528
+ display: inline-block;
529
+ padding: 0.4rem 1.25rem;
530
+ border-radius: 10px;
531
+ font-weight: 900;
532
+ font-size: 0.85rem;
533
+ text-transform: uppercase;
534
+ margin-bottom: 0.5rem;
535
+ }
536
+
537
+ .status-badge.optimal {
538
+ background: var(--color-neon-green);
539
+ color: var(--color-panel-bg);
540
+ }
541
+
542
+ .status-badge.danger {
543
+ background: var(--color-neon-blue);
544
+ color: var(--color-panel-bg);
545
+ }
546
+
547
+ .status-badge.inhibited {
548
+ background: var(--color-neon-pink);
549
+ color: var(--color-panel-bg);
550
+ }
551
+
552
+ .status-desc {
553
+ font-size: 0.95rem;
554
+ color: var(--color-text-dim);
555
+ line-height: 1.5;
556
+ margin: 0;
557
+ }
558
+
559
+ .texture-state {
560
+ width: 100%;
561
+ height: 100%;
562
+ display: flex;
563
+ align-items: center;
564
+ justify-content: center;
565
+ }
566
+
567
+ .texture-state.hidden {
568
+ display: none;
569
+ }
570
+
@@ -0,0 +1,11 @@
1
+ import type { ToolDefinition } from '../../types';
2
+ import { iceCreamPacPod } from './entry';
3
+
4
+ export * from './entry';
5
+
6
+ export const ICE_CREAM_PAC_POD_TOOL: ToolDefinition = {
7
+ entry: iceCreamPacPod,
8
+ Component: () => import('./component.astro'),
9
+ SEOComponent: () => import('./seo.astro'),
10
+ BibliographyComponent: () => import('./bibliography.astro'),
11
+ };
@@ -0,0 +1,62 @@
1
+ export interface SugarInputs {
2
+ sucrose: number;
3
+ dextrose: number;
4
+ glucose: number;
5
+ inverted: number;
6
+ trehalose: number;
7
+ }
8
+
9
+ export interface IceCreamInput {
10
+ sugars: SugarInputs;
11
+ baseWeight: number;
12
+ targetTemp: number;
13
+ }
14
+
15
+ export interface IceCreamResult {
16
+ totalPAC: number;
17
+ totalPOD: number;
18
+ solidsPercentage: number;
19
+ scoopability: 'stone' | 'creamy' | 'soup';
20
+ targetPAC: number;
21
+ }
22
+
23
+ export class IceCreamLogic {
24
+ static sumWeight(sugars: SugarInputs): number {
25
+ return sugars.sucrose + sugars.dextrose + sugars.glucose + sugars.inverted + sugars.trehalose;
26
+ }
27
+
28
+ static computeContributions(sugars: SugarInputs): { pac: number; pod: number } {
29
+ const pac = sugars.sucrose * 1.0 + sugars.dextrose * 1.9 + sugars.glucose * 0.9 + sugars.inverted * 1.9 + sugars.trehalose * 1.0;
30
+ const pod = sugars.sucrose * 1.0 + sugars.dextrose * 0.7 + sugars.glucose * 0.4 + sugars.inverted * 1.3 + sugars.trehalose * 0.45;
31
+ return { pac, pod };
32
+ }
33
+
34
+ static computeScoopability(totalPAC: number, targetPAC: number): 'stone' | 'creamy' | 'soup' {
35
+ const tolerance = 40;
36
+ if (totalPAC < targetPAC - tolerance) return 'stone';
37
+ if (totalPAC > targetPAC + tolerance) return 'soup';
38
+ return 'creamy';
39
+ }
40
+
41
+ static calculate(input: IceCreamInput): IceCreamResult {
42
+ const waterWeight = input.baseWeight * 0.7 + input.sugars.inverted * 0.3;
43
+ const otherSolids = input.baseWeight * 0.3;
44
+ const totalSolids = otherSolids + input.sugars.sucrose + input.sugars.dextrose + input.sugars.glucose * 0.95 + input.sugars.inverted * 0.7 + input.sugars.trehalose * 0.9;
45
+
46
+ const totalMixWeight = input.baseWeight + IceCreamLogic.sumWeight(input.sugars);
47
+ const contributions = IceCreamLogic.computeContributions(input.sugars);
48
+
49
+ const totalPAC = waterWeight > 0 ? parseFloat(((contributions.pac / waterWeight) * 1000).toFixed(1)) : 0;
50
+ const totalPOD = totalMixWeight > 0 ? parseFloat(((contributions.pod / totalMixWeight) * 100).toFixed(1)) : 0;
51
+ const solidsPercentage = totalMixWeight > 0 ? parseFloat(((totalSolids / totalMixWeight) * 100).toFixed(1)) : 0;
52
+ const targetPAC = Math.round(18 * Math.abs(input.targetTemp));
53
+
54
+ return {
55
+ totalPAC,
56
+ totalPOD,
57
+ solidsPercentage,
58
+ scoopability: IceCreamLogic.computeScoopability(totalPAC, targetPAC),
59
+ targetPAC,
60
+ };
61
+ }
62
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { iceCreamPacPod } from './entry';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props;
11
+ const content = await iceCreamPacPod.i18n[locale]?.();
12
+ if (!content) return null;
13
+ ---
14
+
15
+ {content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -124,6 +124,41 @@ const { ui } = Astro.props;
124
124
  };
125
125
  }
126
126
 
127
+ const STORAGE_KEY = 'cooking-spherification-state';
128
+
129
+ function saveState() {
130
+ const state = {
131
+ activeUnit,
132
+ activeMethod,
133
+ useXanthan,
134
+ useCitrate,
135
+ baseWeight: baseInput.value,
136
+ bathWeight: bathInput.value
137
+ };
138
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
139
+ }
140
+
141
+ function loadState() {
142
+ try {
143
+ const data = localStorage.getItem(STORAGE_KEY);
144
+ if (!data) return;
145
+ const state = JSON.parse(data);
146
+ if (state.activeUnit) activeUnit = state.activeUnit;
147
+ if (state.activeMethod) activeMethod = state.activeMethod;
148
+ [['xanthanBtn', 'xanthan-switch-circle', 'useXanthan'],
149
+ ['citrateBtn', 'citrate-switch-circle', 'useCitrate']].forEach(([btnId, circleId, key]) => {
150
+ if (state[key]) {
151
+ if (key === 'useXanthan') useXanthan = true; else useCitrate = true;
152
+ document.getElementById(btnId).classList.add('active');
153
+ document.getElementById(circleId).classList.add('active');
154
+ }
155
+ });
156
+ [['baseWeight', baseInput], ['bathWeight', bathInput]].forEach(([key, el]) => {
157
+ if (state[key]) el.value = state[key];
158
+ });
159
+ } catch {}
160
+ }
161
+
127
162
  function updateView() {
128
163
  let baseWeight = Math.max(0, parseFloat(baseInput.value) || 0);
129
164
  let bathWeight = Math.max(0, parseFloat(bathInput.value) || 0);
@@ -137,6 +172,7 @@ const { ui } = Astro.props;
137
172
  const displayFactor = activeUnit === 'imperial' ? 0.035274 : 1.0;
138
173
  updateRecipeList(result, displayFactor);
139
174
  updateReactorUI();
175
+ saveState();
140
176
  }
141
177
 
142
178
  function convertInputsTo(targetUnit) {
@@ -209,5 +245,6 @@ const { ui } = Astro.props;
209
245
  baseInput.addEventListener('input', updateView);
210
246
  bathInput.addEventListener('input', updateView);
211
247
 
248
+ loadState();
212
249
  updateView();
213
250
  </script>