@jjlmoya/utils-cooking 1.2.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 (130) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +24 -0
  3. package/src/category/i18n/es.ts +208 -0
  4. package/src/category/i18n/fr.ts +24 -0
  5. package/src/category/index.ts +37 -0
  6. package/src/category/seo.astro +15 -0
  7. package/src/components/PreviewNavSidebar.astro +116 -0
  8. package/src/components/PreviewToolbar.astro +143 -0
  9. package/src/data.ts +11 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +32 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +146 -0
  14. package/src/pages/[locale].astro +251 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/faq_count.test.ts +19 -0
  17. package/src/tests/i18n-titles.test.ts +66 -0
  18. package/src/tests/locale_completeness.test.ts +42 -0
  19. package/src/tests/mocks/astro_mock.js +2 -0
  20. package/src/tests/no_h1_in_components.test.ts +48 -0
  21. package/src/tests/seo_length.test.ts +22 -0
  22. package/src/tests/tool_validation.test.ts +17 -0
  23. package/src/tool/american-kitchen-converter/AmericanKitchenEngine.ts +259 -0
  24. package/src/tool/american-kitchen-converter/bibliography.astro +6 -0
  25. package/src/tool/american-kitchen-converter/component.astro +838 -0
  26. package/src/tool/american-kitchen-converter/i18n/en.ts +282 -0
  27. package/src/tool/american-kitchen-converter/i18n/es.ts +281 -0
  28. package/src/tool/american-kitchen-converter/i18n/fr.ts +292 -0
  29. package/src/tool/american-kitchen-converter/index.ts +24 -0
  30. package/src/tool/american-kitchen-converter/seo.astro +8 -0
  31. package/src/tool/banana-ripeness/BananaCare.css +587 -0
  32. package/src/tool/banana-ripeness/BananaEngine.ts +79 -0
  33. package/src/tool/banana-ripeness/bibliography.astro +6 -0
  34. package/src/tool/banana-ripeness/component.astro +285 -0
  35. package/src/tool/banana-ripeness/i18n/en.ts +177 -0
  36. package/src/tool/banana-ripeness/i18n/es.ts +177 -0
  37. package/src/tool/banana-ripeness/i18n/fr.ts +177 -0
  38. package/src/tool/banana-ripeness/index.ts +24 -0
  39. package/src/tool/banana-ripeness/seo.astro +8 -0
  40. package/src/tool/brine/bibliography.astro +6 -0
  41. package/src/tool/brine/component.astro +884 -0
  42. package/src/tool/brine/i18n/en.ts +221 -0
  43. package/src/tool/brine/i18n/es.ts +222 -0
  44. package/src/tool/brine/i18n/fr.ts +221 -0
  45. package/src/tool/brine/index.ts +26 -0
  46. package/src/tool/brine/seo.astro +8 -0
  47. package/src/tool/cookware-guide/CookwareGuide.css +487 -0
  48. package/src/tool/cookware-guide/bibliography.astro +6 -0
  49. package/src/tool/cookware-guide/component.astro +164 -0
  50. package/src/tool/cookware-guide/i18n/en.ts +163 -0
  51. package/src/tool/cookware-guide/i18n/es.ts +163 -0
  52. package/src/tool/cookware-guide/i18n/fr.ts +164 -0
  53. package/src/tool/cookware-guide/index.ts +24 -0
  54. package/src/tool/cookware-guide/init.ts +174 -0
  55. package/src/tool/cookware-guide/seo.astro +8 -0
  56. package/src/tool/egg-timer/EggTimer.css +503 -0
  57. package/src/tool/egg-timer/bibliography.astro +14 -0
  58. package/src/tool/egg-timer/component.astro +281 -0
  59. package/src/tool/egg-timer/i18n/en.ts +230 -0
  60. package/src/tool/egg-timer/i18n/es.ts +222 -0
  61. package/src/tool/egg-timer/i18n/fr.ts +121 -0
  62. package/src/tool/egg-timer/index.ts +27 -0
  63. package/src/tool/egg-timer/seo.astro +39 -0
  64. package/src/tool/ingredient-rescaler/IngredientRescaler.css +308 -0
  65. package/src/tool/ingredient-rescaler/bibliography.astro +6 -0
  66. package/src/tool/ingredient-rescaler/component.astro +107 -0
  67. package/src/tool/ingredient-rescaler/i18n/en.ts +265 -0
  68. package/src/tool/ingredient-rescaler/i18n/es.ts +268 -0
  69. package/src/tool/ingredient-rescaler/i18n/fr.ts +207 -0
  70. package/src/tool/ingredient-rescaler/index.ts +24 -0
  71. package/src/tool/ingredient-rescaler/init.ts +200 -0
  72. package/src/tool/ingredient-rescaler/seo.astro +8 -0
  73. package/src/tool/kitchen-timer/KitchenTimer.css +325 -0
  74. package/src/tool/kitchen-timer/bibliography.astro +6 -0
  75. package/src/tool/kitchen-timer/component.astro +341 -0
  76. package/src/tool/kitchen-timer/i18n/en.ts +154 -0
  77. package/src/tool/kitchen-timer/i18n/es.ts +154 -0
  78. package/src/tool/kitchen-timer/i18n/fr.ts +154 -0
  79. package/src/tool/kitchen-timer/index.ts +26 -0
  80. package/src/tool/kitchen-timer/init.ts +55 -0
  81. package/src/tool/kitchen-timer/lib/AudioHelper.ts +27 -0
  82. package/src/tool/kitchen-timer/lib/DockManager.ts +97 -0
  83. package/src/tool/kitchen-timer/lib/KitchenTimer.ts +264 -0
  84. package/src/tool/kitchen-timer/seo.astro +8 -0
  85. package/src/tool/meringue-peak/MeringueCalculator.css +298 -0
  86. package/src/tool/meringue-peak/bibliography.astro +6 -0
  87. package/src/tool/meringue-peak/component.astro +169 -0
  88. package/src/tool/meringue-peak/i18n/en.ts +257 -0
  89. package/src/tool/meringue-peak/i18n/es.ts +234 -0
  90. package/src/tool/meringue-peak/i18n/fr.ts +234 -0
  91. package/src/tool/meringue-peak/index.ts +24 -0
  92. package/src/tool/meringue-peak/seo.astro +8 -0
  93. package/src/tool/mold-scaler/MoldScaler.css +406 -0
  94. package/src/tool/mold-scaler/bibliography.astro +6 -0
  95. package/src/tool/mold-scaler/component.astro +126 -0
  96. package/src/tool/mold-scaler/i18n/en.ts +268 -0
  97. package/src/tool/mold-scaler/i18n/es.ts +269 -0
  98. package/src/tool/mold-scaler/i18n/fr.ts +276 -0
  99. package/src/tool/mold-scaler/index.ts +26 -0
  100. package/src/tool/mold-scaler/init.ts +264 -0
  101. package/src/tool/mold-scaler/seo.astro +8 -0
  102. package/src/tool/pizza/Pizza.css +569 -0
  103. package/src/tool/pizza/bibliography.astro +6 -0
  104. package/src/tool/pizza/calculator.ts +143 -0
  105. package/src/tool/pizza/component.astro +237 -0
  106. package/src/tool/pizza/i18n/en.ts +288 -0
  107. package/src/tool/pizza/i18n/es.ts +289 -0
  108. package/src/tool/pizza/i18n/fr.ts +288 -0
  109. package/src/tool/pizza/index.ts +27 -0
  110. package/src/tool/pizza/seo.astro +8 -0
  111. package/src/tool/roux-guide/RouxGuide.css +483 -0
  112. package/src/tool/roux-guide/bibliography.astro +6 -0
  113. package/src/tool/roux-guide/component.astro +194 -0
  114. package/src/tool/roux-guide/i18n/en.ts +233 -0
  115. package/src/tool/roux-guide/i18n/es.ts +225 -0
  116. package/src/tool/roux-guide/i18n/fr.ts +225 -0
  117. package/src/tool/roux-guide/index.ts +24 -0
  118. package/src/tool/roux-guide/init.ts +187 -0
  119. package/src/tool/roux-guide/seo.astro +8 -0
  120. package/src/tool/sourdough-calculator/SourdoughCalculator.css +369 -0
  121. package/src/tool/sourdough-calculator/bibliography.astro +6 -0
  122. package/src/tool/sourdough-calculator/component.astro +198 -0
  123. package/src/tool/sourdough-calculator/i18n/en.ts +242 -0
  124. package/src/tool/sourdough-calculator/i18n/es.ts +243 -0
  125. package/src/tool/sourdough-calculator/i18n/fr.ts +248 -0
  126. package/src/tool/sourdough-calculator/index.ts +24 -0
  127. package/src/tool/sourdough-calculator/init.ts +131 -0
  128. package/src/tool/sourdough-calculator/seo.astro +8 -0
  129. package/src/tools.ts +29 -0
  130. package/src/types.ts +73 -0
@@ -0,0 +1,884 @@
1
+ ---
2
+ import type { ToolLocaleContent } from "../../types";
3
+
4
+ interface Props {
5
+ ui: ToolLocaleContent["ui"];
6
+ }
7
+
8
+ const { ui } = Astro.props;
9
+ ---
10
+
11
+ <div class="brine-container">
12
+ <div class="brine-grid">
13
+ <div class="brine-controls">
14
+ <div class="brine-header">
15
+ <h2 class="brine-title">{ui.calculator}</h2>
16
+ <p class="brine-subtitle">{ui.subtitle}</p>
17
+ </div>
18
+
19
+ <div class="brine-inputs">
20
+ <div class="brine-input-group">
21
+ <label for="weight-product" class="brine-label">{ui.productWeight}</label>
22
+ <div class="brine-input-wrapper">
23
+ <input
24
+ type="number"
25
+ id="weight-product"
26
+ value="500"
27
+ min="0"
28
+ class="brine-input"
29
+ />
30
+ <span class="brine-unit">{ui.unitGrams}</span>
31
+ </div>
32
+ </div>
33
+
34
+ <div class="brine-input-group">
35
+ <label for="weight-water" class="brine-label">{ui.waterWeight}</label>
36
+ <div class="brine-input-wrapper">
37
+ <input
38
+ type="number"
39
+ id="weight-water"
40
+ value="500"
41
+ min="0"
42
+ class="brine-input"
43
+ />
44
+ <span class="brine-unit">{ui.unitGrams}</span>
45
+ </div>
46
+ </div>
47
+
48
+ <div class="brine-salinity-section">
49
+ <div class="brine-salinity-header">
50
+ <label for="salinity-slider" class="brine-label">{ui.desiredSalinity}</label>
51
+ <div class="brine-salinity-display">
52
+ <span id="salinity-display" class="brine-salinity-value">2.0</span>
53
+ <span class="brine-salinity-unit">%</span>
54
+ </div>
55
+ </div>
56
+ <input
57
+ type="range"
58
+ id="salinity-slider"
59
+ min="0.5"
60
+ max="10"
61
+ step="0.1"
62
+ value="1.5"
63
+ class="brine-slider"
64
+ />
65
+
66
+ <div class="brine-presets">
67
+ <button class="preset-btn" data-value="1.5">{ui.meat}</button>
68
+ <button class="preset-btn" data-value="2.0">{ui.fermented}</button>
69
+ <button class="preset-btn" data-value="3.5">{ui.sauces}</button>
70
+ <button class="preset-btn" data-value="5.0">{ui.preserves}</button>
71
+ </div>
72
+ </div>
73
+
74
+ <div class="brine-sugar-toggle">
75
+ <div class="brine-toggle-content">
76
+ <label for="sugar-toggle" class="brine-toggle-label">{ui.addSugar}</label>
77
+ <span class="brine-toggle-hint">{ui.sugarHint}</span>
78
+ </div>
79
+ <div class="brine-switch">
80
+ <input type="checkbox" id="sugar-toggle" class="brine-switch-input" />
81
+ <div class="brine-switch-bg"></div>
82
+ <div class="brine-switch-circle"></div>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ </div>
87
+
88
+ <div class="brine-results">
89
+ <div class="brine-visualization">
90
+ <div class="brine-jar">
91
+ <div class="brine-jar-cap"></div>
92
+ <div id="vis-water" class="brine-water"></div>
93
+ <div id="vis-product" class="brine-product">
94
+ <span class="brine-product-label">{ui.product}</span>
95
+ </div>
96
+ <div id="vis-particles" class="brine-particles"></div>
97
+ </div>
98
+ <div class="brine-vis-info">
99
+ <span class="brine-vis-label">{ui.total}:</span>
100
+ <span id="vis-total-weight" class="brine-vis-value">1000</span>{ui.unitGrams}
101
+ </div>
102
+ </div>
103
+
104
+ <div class="brine-output">
105
+ <div class="brine-result-panel brine-salt-panel">
106
+ <div class="brine-result-icon brine-salt-icon">
107
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
108
+ <path d="M20 12V8H6a2 2 0 0 1-2-2c0-1.1.9-2 2-2h12v4"></path>
109
+ <path d="M4 6v12c0 1.1.9 2 2 2h14v-4"></path>
110
+ <path d="M18 12a2 2 0 0 0-2 2c0 1.1.9 2 2 2h4v-4h-4z"></path>
111
+ </svg>
112
+ </div>
113
+ <div class="brine-result-content">
114
+ <span class="brine-result-label">{ui.saltNeeded}</span>
115
+ <div class="brine-result-value">
116
+ <span id="result-salt" class="brine-result-number">20</span>
117
+ <span class="brine-result-unit">{ui.unitGrams}</span>
118
+ </div>
119
+ </div>
120
+ </div>
121
+
122
+ <div id="sugar-result-panel" class="brine-result-panel brine-sugar-panel brine-hidden">
123
+ <div class="brine-result-icon brine-sugar-icon">
124
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
125
+ <path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z"></path>
126
+ </svg>
127
+ </div>
128
+ <div class="brine-result-content">
129
+ <span class="brine-result-label brine-sugar-label">{ui.sugarOptional}</span>
130
+ <div class="brine-result-value">
131
+ <span id="result-sugar" class="brine-result-number brine-sugar-number">10</span>
132
+ <span class="brine-result-unit">{ui.unitGrams}</span>
133
+ </div>
134
+ </div>
135
+ </div>
136
+
137
+ <div class="brine-result-panel brine-time-panel">
138
+ <div class="brine-result-icon brine-time-icon">
139
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
140
+ <circle cx="12" cy="12" r="10"></circle>
141
+ <polyline points="12 6 12 12 16 14"></polyline>
142
+ </svg>
143
+ </div>
144
+ <div class="brine-result-content">
145
+ <span id="result-time-label" class="brine-result-label brine-time-label">{ui.estimatedTime}</span>
146
+ <div class="brine-result-value">
147
+ <span id="result-time" class="brine-result-time">{ui.timeVegetablesDuration}</span>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ </div>
154
+ </div>
155
+
156
+ <style>
157
+ .brine-container {
158
+ width: 100%;
159
+ max-width: 64rem;
160
+ margin: 0 auto;
161
+
162
+ --bg-base: #fff;
163
+ --bg-muted: #f8fafc;
164
+ --text-base: #1e293b;
165
+ --text-muted: #64748b;
166
+ --border-color: #e2e8f0;
167
+ --primary: #06b6d4;
168
+ --primary-rgb: 6, 182, 212;
169
+ --secondary: #a855f7;
170
+ --accent: #f59e0b;
171
+ }
172
+
173
+ html.theme-dark .brine-container,
174
+ body.theme-dark .brine-container,
175
+ .theme-dark .brine-container {
176
+ --bg-base: #1e293b;
177
+ --bg-muted: #0f172a;
178
+ --text-base: #f1f5f9;
179
+ --text-muted: #94a3b8;
180
+ --border-color: #334155;
181
+ }
182
+
183
+ .brine-grid {
184
+ display: grid;
185
+ grid-template-columns: 1fr;
186
+ gap: 0;
187
+ background: var(--bg-base);
188
+ border: 1px solid var(--border-color);
189
+ border-radius: 1.5rem;
190
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
191
+ overflow: hidden;
192
+ }
193
+
194
+ @media (min-width: 1024px) {
195
+ .brine-grid {
196
+ grid-template-columns: 1fr 1fr;
197
+ }
198
+ }
199
+
200
+ .brine-controls {
201
+ padding: 2rem;
202
+ display: flex;
203
+ flex-direction: column;
204
+ gap: 2rem;
205
+ }
206
+
207
+ @media (min-width: 1024px) {
208
+ .brine-controls {
209
+ padding: 2.5rem;
210
+ }
211
+ }
212
+
213
+ .brine-header {
214
+ display: flex;
215
+ flex-direction: column;
216
+ gap: 0.5rem;
217
+ }
218
+
219
+ .brine-title {
220
+ font-size: 1.5rem;
221
+ font-weight: 700;
222
+ color: var(--text-base);
223
+ }
224
+
225
+ .brine-subtitle {
226
+ font-size: 0.875rem;
227
+ color: var(--text-muted);
228
+ }
229
+
230
+ .brine-inputs {
231
+ display: flex;
232
+ flex-direction: column;
233
+ gap: 1.5rem;
234
+ }
235
+
236
+ .brine-input-group {
237
+ display: flex;
238
+ flex-direction: column;
239
+ gap: 0.5rem;
240
+ }
241
+
242
+ .brine-label {
243
+ font-size: 0.875rem;
244
+ font-weight: 500;
245
+ color: var(--text-muted);
246
+ }
247
+
248
+ .brine-input-wrapper {
249
+ position: relative;
250
+ }
251
+
252
+ .brine-input {
253
+ width: 100%;
254
+ padding: 1rem;
255
+ font-size: 1.25rem;
256
+ background: var(--bg-muted);
257
+ border: 1px solid var(--border-color);
258
+ border-radius: 1rem;
259
+ color: var(--text-base);
260
+ transition: all 0.2s;
261
+ }
262
+
263
+ .brine-input:focus {
264
+ outline: none;
265
+ box-shadow: 0 0 0 2px var(--primary);
266
+ border-color: var(--primary);
267
+ }
268
+
269
+ .brine-unit {
270
+ position: absolute;
271
+ right: 1rem;
272
+ top: 50%;
273
+ transform: translateY(-50%);
274
+ color: var(--text-muted);
275
+ font-weight: 500;
276
+ }
277
+
278
+ .brine-salinity-section {
279
+ padding-top: 1rem;
280
+ }
281
+
282
+ .brine-salinity-header {
283
+ display: flex;
284
+ justify-content: space-between;
285
+ align-items: flex-end;
286
+ margin-bottom: 1rem;
287
+ }
288
+
289
+ .brine-salinity-display {
290
+ display: flex;
291
+ align-items: baseline;
292
+ gap: 0.25rem;
293
+ }
294
+
295
+ .brine-salinity-value {
296
+ font-size: 1.875rem;
297
+ font-weight: 700;
298
+ color: var(--primary);
299
+ }
300
+
301
+ .brine-salinity-unit {
302
+ font-size: 1.125rem;
303
+ font-weight: 700;
304
+ color: var(--primary);
305
+ opacity: 0.7;
306
+ }
307
+
308
+ .brine-slider {
309
+ width: 100%;
310
+ height: 0.75rem;
311
+ background: var(--border-color);
312
+ border-radius: 9999px;
313
+ cursor: pointer;
314
+ accent-color: var(--primary);
315
+ }
316
+
317
+ .brine-slider:hover {
318
+ accent-color: var(--primary-hover);
319
+ }
320
+
321
+ .brine-presets {
322
+ display: flex;
323
+ flex-wrap: wrap;
324
+ gap: 0.5rem;
325
+ margin-top: 1rem;
326
+ }
327
+
328
+ .preset-btn {
329
+ padding: 0.375rem 0.75rem;
330
+ font-size: 0.75rem;
331
+ font-weight: 700;
332
+ background: var(--bg-muted);
333
+ color: var(--text-muted);
334
+ border: 1px solid transparent;
335
+ border-radius: 0.5rem;
336
+ cursor: pointer;
337
+ transition: all 0.2s;
338
+ }
339
+
340
+ .preset-btn:hover {
341
+ background: rgba(var(--primary-rgb), 0.1);
342
+ color: var(--primary);
343
+ border-color: var(--primary);
344
+ }
345
+
346
+ .preset-btn.active {
347
+ background: rgba(var(--primary-rgb), 0.15);
348
+ color: var(--primary);
349
+ border-color: var(--primary);
350
+ box-shadow: 0 0 0 2px var(--bg-base), 0 0 0 4px var(--primary);
351
+ }
352
+
353
+ .brine-sugar-toggle {
354
+ display: flex;
355
+ align-items: center;
356
+ justify-content: space-between;
357
+ padding: 1rem;
358
+ background: var(--bg-muted);
359
+ border-radius: 1rem;
360
+ border: 1px solid var(--border-color);
361
+ }
362
+
363
+ .brine-toggle-content {
364
+ display: flex;
365
+ flex-direction: column;
366
+ gap: 0.25rem;
367
+ }
368
+
369
+ .brine-toggle-label {
370
+ font-weight: 500;
371
+ color: var(--text-base);
372
+ cursor: pointer;
373
+ }
374
+
375
+ .brine-toggle-hint {
376
+ font-size: 0.75rem;
377
+ color: var(--text-muted);
378
+ }
379
+
380
+ .brine-switch {
381
+ position: relative;
382
+ width: 3rem;
383
+ height: 1.5rem;
384
+ }
385
+
386
+ .brine-switch-input {
387
+ position: absolute;
388
+ width: 100%;
389
+ height: 100%;
390
+ opacity: 0;
391
+ cursor: pointer;
392
+ z-index: 10;
393
+ }
394
+
395
+ .brine-switch-bg {
396
+ position: absolute;
397
+ width: 100%;
398
+ height: 100%;
399
+ background: var(--border-color);
400
+ border-radius: 9999px;
401
+ transition: background-color 0.2s;
402
+ }
403
+
404
+ .brine-switch-circle {
405
+ position: absolute;
406
+ top: 0.25rem;
407
+ left: 0.25rem;
408
+ width: 1rem;
409
+ height: 1rem;
410
+ background: white;
411
+ border-radius: 50%;
412
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
413
+ transition: transform 0.2s;
414
+ }
415
+
416
+ .brine-switch-input:checked ~ .brine-switch-bg {
417
+ background: var(--primary);
418
+ }
419
+
420
+ .brine-switch-input:checked ~ .brine-switch-circle {
421
+ transform: translateX(1.5rem);
422
+ }
423
+
424
+ .brine-results {
425
+ background: var(--bg-muted);
426
+ padding: 2rem;
427
+ display: flex;
428
+ flex-direction: column;
429
+ justify-content: space-between;
430
+ border-top: 1px solid var(--border-color);
431
+ }
432
+
433
+ @media (min-width: 1024px) {
434
+ .brine-results {
435
+ border-top: none;
436
+ border-left: 1px solid var(--border-color);
437
+ padding: 2.5rem;
438
+ }
439
+ }
440
+
441
+ .brine-visualization {
442
+ display: flex;
443
+ flex-direction: column;
444
+ align-items: center;
445
+ justify-content: center;
446
+ min-height: 300px;
447
+ margin-bottom: 2rem;
448
+ position: relative;
449
+ }
450
+
451
+ .brine-jar {
452
+ position: relative;
453
+ width: 12rem;
454
+ height: 16rem;
455
+ border: 4px solid var(--border-color);
456
+ border-radius: 1rem;
457
+ border-top: none;
458
+ background: rgba(255, 255, 255, 0.1);
459
+ backdrop-filter: blur(10px);
460
+ overflow: hidden;
461
+ }
462
+
463
+ .brine-jar-cap {
464
+ position: absolute;
465
+ top: -0.25rem;
466
+ left: 50%;
467
+ transform: translateX(-50%);
468
+ width: 13rem;
469
+ height: 1rem;
470
+ background: var(--border-color);
471
+ border-radius: 0.5rem 0.5rem 0 0;
472
+ }
473
+
474
+ .brine-water {
475
+ position: absolute;
476
+ bottom: 0;
477
+ left: 0;
478
+ width: 100%;
479
+ background: rgba(59, 130, 246, 0.2);
480
+ transition: height 0.5s ease-out;
481
+ height: 50%;
482
+ }
483
+
484
+ .brine-water::before {
485
+ content: '';
486
+ position: absolute;
487
+ top: 0;
488
+ left: 0;
489
+ width: 100%;
490
+ height: 100%;
491
+ background: repeating-linear-gradient(
492
+ 90deg,
493
+ transparent,
494
+ transparent 50%,
495
+ rgba(59, 130, 246, 0.1) 50%,
496
+ rgba(59, 130, 246, 0.1) 100%
497
+ );
498
+ animation: wave 10s linear infinite;
499
+ opacity: 0.5;
500
+ }
501
+
502
+ @keyframes wave {
503
+ 0% {
504
+ transform: translateX(0) scaleY(1);
505
+ }
506
+ 50% {
507
+ transform: translateX(-25%) scaleY(0.85);
508
+ }
509
+ 100% {
510
+ transform: translateX(-50%) scaleY(1);
511
+ }
512
+ }
513
+
514
+ .brine-product {
515
+ position: absolute;
516
+ bottom: 1rem;
517
+ left: 50%;
518
+ transform: translateX(-50%);
519
+ background: rgba(0, 0, 0, 0.1);
520
+ border-radius: 0.75rem;
521
+ backdrop-filter: blur(10px);
522
+ border: 1px solid rgba(0, 0, 0, 0.1);
523
+ display: flex;
524
+ align-items: center;
525
+ justify-content: center;
526
+ transition: all 0.5s ease-out;
527
+ width: 60%;
528
+ height: 40%;
529
+ }
530
+
531
+ .brine-product-label {
532
+ font-size: 0.75rem;
533
+ font-weight: 700;
534
+ color: rgba(0, 0, 0, 0.3);
535
+ text-transform: uppercase;
536
+ letter-spacing: 0.05em;
537
+ }
538
+
539
+ .brine-particles {
540
+ position: absolute;
541
+ inset: 0;
542
+ pointer-events: none;
543
+ }
544
+
545
+ .brine-vis-info {
546
+ position: absolute;
547
+ top: 1rem;
548
+ right: 1rem;
549
+ background: var(--bg-base);
550
+ padding: 0.375rem 0.75rem;
551
+ border-radius: 0.5rem;
552
+ border: 1px solid var(--border-color);
553
+ font-size: 0.75rem;
554
+ color: var(--text-muted);
555
+ }
556
+
557
+ .brine-vis-label {
558
+ font-weight: 500;
559
+ }
560
+
561
+ .brine-vis-value {
562
+ font-weight: 700;
563
+ color: var(--text-base);
564
+ }
565
+
566
+ .brine-output {
567
+ display: flex;
568
+ flex-direction: column;
569
+ gap: 1rem;
570
+ }
571
+
572
+ .brine-result-panel {
573
+ background: var(--bg-base);
574
+ padding: 1.5rem;
575
+ border-radius: 1rem;
576
+ border: 1px solid var(--border-color);
577
+ position: relative;
578
+ overflow: hidden;
579
+ display: flex;
580
+ gap: 1rem;
581
+ align-items: flex-start;
582
+ }
583
+
584
+ .brine-result-icon {
585
+ width: 3rem;
586
+ height: 3rem;
587
+ background: var(--bg-muted);
588
+ border-radius: 0.75rem;
589
+ display: flex;
590
+ align-items: center;
591
+ justify-content: center;
592
+ color: var(--primary);
593
+ flex-shrink: 0;
594
+ }
595
+
596
+ .brine-salt-icon {
597
+ color: var(--primary);
598
+ }
599
+
600
+ .brine-sugar-icon {
601
+ color: var(--secondary);
602
+ }
603
+
604
+ .brine-time-icon {
605
+ color: var(--accent);
606
+ }
607
+
608
+ .brine-result-content {
609
+ flex: 1;
610
+ }
611
+
612
+ .brine-result-label {
613
+ display: block;
614
+ font-size: 0.875rem;
615
+ font-weight: 700;
616
+ text-transform: uppercase;
617
+ letter-spacing: 0.05em;
618
+ color: var(--text-muted);
619
+ margin-bottom: 0.25rem;
620
+ }
621
+
622
+ .brine-sugar-label {
623
+ color: var(--secondary);
624
+ }
625
+
626
+ .brine-time-label {
627
+ color: var(--accent);
628
+ }
629
+
630
+ .brine-result-value {
631
+ display: flex;
632
+ align-items: baseline;
633
+ gap: 0.5rem;
634
+ }
635
+
636
+ .brine-result-number {
637
+ font-size: 2.5rem;
638
+ font-weight: 900;
639
+ color: var(--primary);
640
+ }
641
+
642
+ .brine-sugar-number {
643
+ color: var(--secondary);
644
+ }
645
+
646
+ .brine-result-unit {
647
+ font-size: 1rem;
648
+ font-weight: 500;
649
+ color: var(--text-muted);
650
+ }
651
+
652
+ .brine-result-time {
653
+ font-size: 1.875rem;
654
+ font-weight: 900;
655
+ color: var(--accent);
656
+ }
657
+
658
+ .brine-salt-panel {
659
+ border-color: color-mix(in srgb, var(--primary) 20%, transparent);
660
+ }
661
+
662
+ .brine-sugar-panel {
663
+ border-color: color-mix(in srgb, var(--secondary) 20%, transparent);
664
+ }
665
+
666
+ .brine-time-panel {
667
+ border-color: color-mix(in srgb, var(--accent) 20%, transparent);
668
+ }
669
+
670
+ .brine-hidden {
671
+ display: none;
672
+ }
673
+
674
+ @media (max-width: 640px) {
675
+ .brine-controls {
676
+ padding: 1.5rem;
677
+ }
678
+
679
+ .brine-results {
680
+ padding: 1.5rem;
681
+ }
682
+
683
+ .brine-result-number {
684
+ font-size: 2rem;
685
+ }
686
+
687
+ .brine-result-time {
688
+ font-size: 1.5rem;
689
+ }
690
+ }
691
+ </style>
692
+
693
+ <script is:inline define:vars={{ ui }}>
694
+ const state = {
695
+ product: 500,
696
+ water: 500,
697
+ salinity: 1.5,
698
+ sugar: false,
699
+ };
700
+
701
+ const els = {
702
+ inputs: {
703
+ product: document.getElementById("weight-product") as HTMLInputElement,
704
+ water: document.getElementById("weight-water") as HTMLInputElement,
705
+ slider: document.getElementById("salinity-slider") as HTMLInputElement,
706
+ sugar: document.getElementById("sugar-toggle") as HTMLInputElement,
707
+ presets: document.querySelectorAll(".preset-btn"),
708
+ },
709
+ displays: {
710
+ salinity: document.getElementById("salinity-display"),
711
+ saltResult: document.getElementById("result-salt"),
712
+ sugarResult: document.getElementById("result-sugar"),
713
+ sugarPanel: document.getElementById("sugar-result-panel"),
714
+ timeResult: document.getElementById("result-time"),
715
+ timeLabel: document.getElementById("result-time-label"),
716
+ visTotal: document.getElementById("vis-total-weight"),
717
+ },
718
+ vis: {
719
+ water: document.getElementById("vis-water"),
720
+ product: document.getElementById("vis-product"),
721
+ particles: document.getElementById("vis-particles"),
722
+ },
723
+ };
724
+
725
+ const calculate = () => {
726
+ const totalWeight = state.product + state.water;
727
+ const salt = totalWeight * (state.salinity / 100);
728
+ const sugar = state.sugar ? salt * 0.5 : 0;
729
+ return { salt, sugar, totalWeight };
730
+ };
731
+
732
+ const updateVisuals = (results: { salt: number; sugar: number; totalWeight: number }) => {
733
+ const maxCap = 2000;
734
+ const waterHeight = Math.min((state.water / maxCap) * 100, 90);
735
+ if (els.vis.water) els.vis.water.style.height = `${Math.max(waterHeight, 10)}%`;
736
+
737
+ const productSize = Math.min((state.product / maxCap) * 100, 80);
738
+ if (els.vis.product) {
739
+ els.vis.product.style.height = `${Math.max(productSize, 10)}%`;
740
+ els.vis.product.style.width = `${Math.max(productSize, 20)}%`;
741
+ }
742
+
743
+ if (els.vis.particles) els.vis.particles.innerHTML = "";
744
+
745
+ const saltCount = Math.min(Math.floor(results.salt / 2), 50);
746
+ for (let i = 0; i < saltCount; i++) {
747
+ createParticle("brine-particle-salt");
748
+ }
749
+
750
+ if (state.sugar) {
751
+ const sugarCount = Math.min(Math.floor(results.sugar / 2), 30);
752
+ for (let i = 0; i < sugarCount; i++) {
753
+ createParticle("brine-particle-sugar");
754
+ }
755
+ }
756
+ };
757
+
758
+ const createParticle = (className: string) => {
759
+ if (!els.vis.particles) return;
760
+ const p = document.createElement("div");
761
+ const size = Math.random() * 4 + 2;
762
+ p.className = `${className}`;
763
+ p.style.position = "absolute";
764
+ p.style.width = `${size}px`;
765
+ p.style.height = `${size}px`;
766
+ p.style.left = `${Math.random() * 100}%`;
767
+ p.style.bottom = `${Math.random() * 100}%`;
768
+ p.style.borderRadius = "50%";
769
+
770
+ if (className === "brine-particle-salt") {
771
+ p.style.background = "rgba(255, 255, 255, 0.6)";
772
+ p.style.animation = `brine-float 3s ease-in-out infinite`;
773
+ } else {
774
+ p.style.background = "rgba(168, 85, 247, 0.4)";
775
+ p.style.animation = `brine-pulse 1s ease-in-out infinite`;
776
+ }
777
+
778
+ p.style.animationDelay = `${Math.random() * 2}s`;
779
+ els.vis.particles.appendChild(p);
780
+ };
781
+
782
+ const updatePresets = () => {
783
+ els.inputs.presets.forEach((btn) => {
784
+ const val = parseFloat(btn.getAttribute("data-value") || "0");
785
+ const isSelected = Math.abs(val - state.salinity) < 0.01;
786
+
787
+ if (isSelected) {
788
+ btn.classList.add("active");
789
+ } else {
790
+ btn.classList.remove("active");
791
+ }
792
+ });
793
+ };
794
+
795
+ const getTimeEstimate = () => {
796
+ if (state.salinity < 2.0) {
797
+ return { label: ui.timeMeatsLabel, text: ui.timeMeatsDuration };
798
+ }
799
+ if (state.salinity <= 3.0) {
800
+ return { label: ui.timeVegetablesLabel, text: ui.timeVegetablesDuration };
801
+ }
802
+ return { label: ui.timePreservesLabel, text: ui.timePreservesDuration };
803
+ };
804
+
805
+ const updateResults = (results: { salt: number; sugar: number; totalWeight: number }) => {
806
+ if (els.displays.saltResult)
807
+ els.displays.saltResult.textContent = Math.round(results.salt).toString();
808
+ if (els.displays.sugarResult)
809
+ els.displays.sugarResult.textContent = Math.round(results.sugar).toString();
810
+ if (els.displays.salinity) els.displays.salinity.textContent = state.salinity.toFixed(1);
811
+ if (els.displays.visTotal)
812
+ els.displays.visTotal.textContent = Math.round(results.totalWeight).toString();
813
+ };
814
+
815
+ const updateSugarPanel = () => {
816
+ if (els.displays.sugarPanel) {
817
+ if (state.sugar) els.displays.sugarPanel.classList.remove("brine-hidden");
818
+ else els.displays.sugarPanel.classList.add("brine-hidden");
819
+ }
820
+ };
821
+
822
+ const updateTimeDisplay = () => {
823
+ const timeEstimate = getTimeEstimate();
824
+ if (els.displays.timeResult) els.displays.timeResult.textContent = timeEstimate.text;
825
+ if (els.displays.timeLabel) els.displays.timeLabel.textContent = timeEstimate.label;
826
+ };
827
+
828
+ const update = () => {
829
+ const results = calculate();
830
+ updateResults(results);
831
+ updateSugarPanel();
832
+ updateTimeDisplay();
833
+ updateVisuals(results);
834
+ updatePresets();
835
+ };
836
+
837
+ if (els.inputs.product)
838
+ els.inputs.product.addEventListener("input", (e) => {
839
+ state.product = parseFloat((e.target as HTMLInputElement).value) || 0;
840
+ update();
841
+ });
842
+
843
+ if (els.inputs.water)
844
+ els.inputs.water.addEventListener("input", (e) => {
845
+ state.water = parseFloat((e.target as HTMLInputElement).value) || 0;
846
+ update();
847
+ });
848
+
849
+ if (els.inputs.slider)
850
+ els.inputs.slider.addEventListener("input", (e) => {
851
+ state.salinity = parseFloat((e.target as HTMLInputElement).value);
852
+ update();
853
+ });
854
+
855
+ if (els.inputs.sugar)
856
+ els.inputs.sugar.addEventListener("change", (e) => {
857
+ state.sugar = (e.target as HTMLInputElement).checked;
858
+ update();
859
+ });
860
+
861
+ els.inputs.presets.forEach((btn) => {
862
+ btn.addEventListener("click", () => {
863
+ const val = parseFloat(btn.getAttribute("data-value") || "2.0");
864
+ state.salinity = val;
865
+ if (els.inputs.slider) els.inputs.slider.value = val.toString();
866
+ update();
867
+ });
868
+ });
869
+
870
+ update();
871
+
872
+ const style = document.createElement('style');
873
+ style.textContent = `
874
+ @keyframes brine-float {
875
+ 0%, 100% { transform: translateY(0px); }
876
+ 50% { transform: translateY(-5px); }
877
+ }
878
+ @keyframes brine-pulse {
879
+ 0%, 100% { opacity: 0.6; }
880
+ 50% { opacity: 0.3; }
881
+ }
882
+ `;
883
+ document.head.appendChild(style);
884
+ </script>