@jjlmoya/utils-alcohol 1.25.0 → 1.26.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 (139) hide show
  1. package/package.json +1 -1
  2. package/src/entries.ts +7 -1
  3. package/src/index.ts +1 -0
  4. package/src/pages/[locale]/[slug].astro +7 -3
  5. package/src/tests/locale_completeness.test.ts +4 -9
  6. package/src/tests/tool_validation.test.ts +2 -2
  7. package/src/tool/alcoholClearance/bibliography.ts +6 -0
  8. package/src/tool/alcoholClearance/entry.ts +1 -0
  9. package/src/tool/alcoholClearance/i18n/de.ts +1 -12
  10. package/src/tool/alcoholClearance/i18n/en.ts +1 -12
  11. package/src/tool/alcoholClearance/i18n/es.ts +1 -12
  12. package/src/tool/alcoholClearance/i18n/fr.ts +1 -12
  13. package/src/tool/alcoholClearance/i18n/id.ts +1 -12
  14. package/src/tool/alcoholClearance/i18n/it.ts +1 -12
  15. package/src/tool/alcoholClearance/i18n/ja.ts +1 -12
  16. package/src/tool/alcoholClearance/i18n/ko.ts +1 -12
  17. package/src/tool/alcoholClearance/i18n/nl.ts +1 -12
  18. package/src/tool/alcoholClearance/i18n/pl.ts +1 -12
  19. package/src/tool/alcoholClearance/i18n/pt.ts +1 -12
  20. package/src/tool/alcoholClearance/i18n/ru.ts +1 -12
  21. package/src/tool/alcoholClearance/i18n/sv.ts +1 -12
  22. package/src/tool/alcoholClearance/i18n/tr.ts +1 -12
  23. package/src/tool/alcoholClearance/i18n/zh.ts +1 -12
  24. package/src/tool/beerCooler/bibliography.ts +6 -0
  25. package/src/tool/beerCooler/entry.ts +1 -0
  26. package/src/tool/beerCooler/i18n/de.ts +1 -12
  27. package/src/tool/beerCooler/i18n/en.ts +1 -12
  28. package/src/tool/beerCooler/i18n/es.ts +1 -12
  29. package/src/tool/beerCooler/i18n/fr.ts +1 -12
  30. package/src/tool/beerCooler/i18n/id.ts +1 -12
  31. package/src/tool/beerCooler/i18n/it.ts +1 -12
  32. package/src/tool/beerCooler/i18n/ja.ts +1 -12
  33. package/src/tool/beerCooler/i18n/ko.ts +1 -12
  34. package/src/tool/beerCooler/i18n/nl.ts +1 -12
  35. package/src/tool/beerCooler/i18n/pl.ts +1 -12
  36. package/src/tool/beerCooler/i18n/pt.ts +1 -12
  37. package/src/tool/beerCooler/i18n/ru.ts +1 -12
  38. package/src/tool/beerCooler/i18n/sv.ts +1 -12
  39. package/src/tool/beerCooler/i18n/tr.ts +1 -12
  40. package/src/tool/beerCooler/i18n/zh.ts +1 -12
  41. package/src/tool/carbonationCalculator/bibliography.ts +6 -0
  42. package/src/tool/carbonationCalculator/entry.ts +1 -0
  43. package/src/tool/carbonationCalculator/i18n/de.ts +1 -12
  44. package/src/tool/carbonationCalculator/i18n/en.ts +1 -12
  45. package/src/tool/carbonationCalculator/i18n/es.ts +1 -12
  46. package/src/tool/carbonationCalculator/i18n/fr.ts +1 -12
  47. package/src/tool/carbonationCalculator/i18n/id.ts +1 -12
  48. package/src/tool/carbonationCalculator/i18n/it.ts +1 -12
  49. package/src/tool/carbonationCalculator/i18n/ja.ts +1 -12
  50. package/src/tool/carbonationCalculator/i18n/ko.ts +1 -12
  51. package/src/tool/carbonationCalculator/i18n/nl.ts +1 -12
  52. package/src/tool/carbonationCalculator/i18n/pl.ts +1 -12
  53. package/src/tool/carbonationCalculator/i18n/pt.ts +1 -12
  54. package/src/tool/carbonationCalculator/i18n/ru.ts +1 -12
  55. package/src/tool/carbonationCalculator/i18n/sv.ts +1 -12
  56. package/src/tool/carbonationCalculator/i18n/tr.ts +1 -12
  57. package/src/tool/carbonationCalculator/i18n/zh.ts +1 -12
  58. package/src/tool/cocktailBalancer/bibliography.ts +7 -0
  59. package/src/tool/cocktailBalancer/entry.ts +1 -0
  60. package/src/tool/cocktailBalancer/i18n/de.ts +1 -16
  61. package/src/tool/cocktailBalancer/i18n/en.ts +1 -16
  62. package/src/tool/cocktailBalancer/i18n/es.ts +1 -16
  63. package/src/tool/cocktailBalancer/i18n/fr.ts +1 -16
  64. package/src/tool/cocktailBalancer/i18n/id.ts +1 -16
  65. package/src/tool/cocktailBalancer/i18n/it.ts +1 -16
  66. package/src/tool/cocktailBalancer/i18n/ja.ts +1 -16
  67. package/src/tool/cocktailBalancer/i18n/ko.ts +1 -16
  68. package/src/tool/cocktailBalancer/i18n/nl.ts +1 -16
  69. package/src/tool/cocktailBalancer/i18n/pl.ts +1 -16
  70. package/src/tool/cocktailBalancer/i18n/pt.ts +1 -16
  71. package/src/tool/cocktailBalancer/i18n/ru.ts +1 -16
  72. package/src/tool/cocktailBalancer/i18n/sv.ts +1 -16
  73. package/src/tool/cocktailBalancer/i18n/tr.ts +1 -16
  74. package/src/tool/cocktailBalancer/i18n/zh.ts +1 -16
  75. package/src/tool/fortifiedWine/bibliography.astro +14 -0
  76. package/src/tool/fortifiedWine/bibliography.ts +7 -0
  77. package/src/tool/fortifiedWine/component.astro +331 -0
  78. package/src/tool/fortifiedWine/entry.ts +62 -0
  79. package/src/tool/fortifiedWine/fortified-wine-builder.css +534 -0
  80. package/src/tool/fortifiedWine/i18n/de.ts +66 -0
  81. package/src/tool/fortifiedWine/i18n/en.ts +140 -0
  82. package/src/tool/fortifiedWine/i18n/es.ts +140 -0
  83. package/src/tool/fortifiedWine/i18n/fr.ts +91 -0
  84. package/src/tool/fortifiedWine/i18n/id.ts +91 -0
  85. package/src/tool/fortifiedWine/i18n/it.ts +91 -0
  86. package/src/tool/fortifiedWine/i18n/ja.ts +91 -0
  87. package/src/tool/fortifiedWine/i18n/ko.ts +91 -0
  88. package/src/tool/fortifiedWine/i18n/nl.ts +91 -0
  89. package/src/tool/fortifiedWine/i18n/pl.ts +91 -0
  90. package/src/tool/fortifiedWine/i18n/pt.ts +91 -0
  91. package/src/tool/fortifiedWine/i18n/ru.ts +91 -0
  92. package/src/tool/fortifiedWine/i18n/sv.ts +91 -0
  93. package/src/tool/fortifiedWine/i18n/tr.ts +91 -0
  94. package/src/tool/fortifiedWine/i18n/zh.ts +91 -0
  95. package/src/tool/fortifiedWine/index.ts +8 -0
  96. package/src/tool/fortifiedWine/logic.ts +46 -0
  97. package/src/tool/fortifiedWine/seo.astro +41 -0
  98. package/src/tool/jelloShotLab/bibliography.astro +14 -0
  99. package/src/tool/jelloShotLab/bibliography.ts +8 -0
  100. package/src/tool/jelloShotLab/component.astro +183 -0
  101. package/src/tool/jelloShotLab/entry.ts +62 -0
  102. package/src/tool/jelloShotLab/i18n/de.ts +156 -0
  103. package/src/tool/jelloShotLab/i18n/en.ts +156 -0
  104. package/src/tool/jelloShotLab/i18n/es.ts +156 -0
  105. package/src/tool/jelloShotLab/i18n/fr.ts +156 -0
  106. package/src/tool/jelloShotLab/i18n/id.ts +156 -0
  107. package/src/tool/jelloShotLab/i18n/it.ts +156 -0
  108. package/src/tool/jelloShotLab/i18n/ja.ts +156 -0
  109. package/src/tool/jelloShotLab/i18n/ko.ts +156 -0
  110. package/src/tool/jelloShotLab/i18n/nl.ts +156 -0
  111. package/src/tool/jelloShotLab/i18n/pl.ts +156 -0
  112. package/src/tool/jelloShotLab/i18n/pt.ts +156 -0
  113. package/src/tool/jelloShotLab/i18n/ru.ts +156 -0
  114. package/src/tool/jelloShotLab/i18n/sv.ts +156 -0
  115. package/src/tool/jelloShotLab/i18n/tr.ts +156 -0
  116. package/src/tool/jelloShotLab/i18n/zh.ts +156 -0
  117. package/src/tool/jelloShotLab/index.ts +11 -0
  118. package/src/tool/jelloShotLab/jello-shot-lab.css +229 -0
  119. package/src/tool/jelloShotLab/logic.ts +29 -0
  120. package/src/tool/jelloShotLab/seo.astro +53 -0
  121. package/src/tool/partyKeg/bibliography.ts +6 -0
  122. package/src/tool/partyKeg/entry.ts +1 -0
  123. package/src/tool/partyKeg/i18n/de.ts +1 -12
  124. package/src/tool/partyKeg/i18n/en.ts +1 -12
  125. package/src/tool/partyKeg/i18n/es.ts +1 -12
  126. package/src/tool/partyKeg/i18n/fr.ts +1 -12
  127. package/src/tool/partyKeg/i18n/id.ts +1 -12
  128. package/src/tool/partyKeg/i18n/it.ts +1 -12
  129. package/src/tool/partyKeg/i18n/ja.ts +1 -12
  130. package/src/tool/partyKeg/i18n/ko.ts +1 -12
  131. package/src/tool/partyKeg/i18n/nl.ts +1 -12
  132. package/src/tool/partyKeg/i18n/pl.ts +1 -12
  133. package/src/tool/partyKeg/i18n/pt.ts +1 -12
  134. package/src/tool/partyKeg/i18n/ru.ts +1 -12
  135. package/src/tool/partyKeg/i18n/sv.ts +1 -12
  136. package/src/tool/partyKeg/i18n/tr.ts +1 -12
  137. package/src/tool/partyKeg/i18n/zh.ts +1 -12
  138. package/src/tools.ts +5 -0
  139. package/src/types.ts +1 -1
@@ -0,0 +1,331 @@
1
+ ---
2
+ import { Icon } from "astro-icon/components";
3
+ import type { FortifiedWineBuilderUI } from "./index";
4
+
5
+ interface Props {
6
+ ui: FortifiedWineBuilderUI;
7
+ }
8
+ const { ui } = Astro.props;
9
+ ---
10
+
11
+ <div class="fw-app" id="fw-calculator">
12
+ <div class="fw-card">
13
+
14
+ <div class="fw-intention">
15
+ <div class="fw-section-label">{ui.intentionTitle}</div>
16
+ <div class="fw-intention-btns">
17
+ <button class="fw-intention-btn active" data-intention="vermouth" data-target="16">{ui.intentionVermouth}</button>
18
+ <button class="fw-intention-btn" data-intention="port" data-target="19">{ui.intentionPort}</button>
19
+ <button class="fw-intention-btn" data-intention="sherry" data-target="16">{ui.intentionSherry}</button>
20
+ <button class="fw-intention-btn" data-intention="custom" data-target="">{ui.intentionCustom}</button>
21
+ </div>
22
+ </div>
23
+
24
+ <div class="fw-mode">
25
+ <button class="fw-mode-btn active" data-mode="a">{ui.modeALabel}</button>
26
+ <button class="fw-mode-btn" data-mode="b">{ui.modeBLabel}</button>
27
+ </div>
28
+
29
+ <div class="fw-grid">
30
+
31
+ <div class="fw-left">
32
+
33
+ <div class="fw-sec">
34
+ <div class="fw-sec-title">{ui.wineSection}</div>
35
+ <div class="fw-input-row">
36
+ <div class="fw-field">
37
+ <label class="fw-label" for="fw-wine-vol">{ui.wineVolumeLabel}</label>
38
+ <div class="fw-slider-row">
39
+ <input type="range" class="fw-slider" id="fw-wine-vol-slider" min="0.1" max="100" step="0.1" value="5" />
40
+ <input type="number" class="fw-num-input" id="fw-wine-vol" min="0.1" max="9999" step="0.1" value="5" />
41
+ </div>
42
+ </div>
43
+ <div class="fw-field">
44
+ <label class="fw-label" for="fw-wine-abv">{ui.wineAbvLabel}</label>
45
+ <div class="fw-slider-row">
46
+ <input type="range" class="fw-slider" id="fw-wine-abv-slider" min="0" max="25" step="0.5" value="12" />
47
+ <input type="number" class="fw-num-input" id="fw-wine-abv" min="0" max="25" step="0.1" value="12" />
48
+ </div>
49
+ </div>
50
+ </div>
51
+ </div>
52
+
53
+ <div class="fw-sec">
54
+ <div class="fw-sec-title">{ui.spiritSection}</div>
55
+ <div class="fw-presets">
56
+ <button class="fw-preset-btn" data-abv="38">{ui.brandyPreset}</button>
57
+ <button class="fw-preset-btn" data-abv="96">{ui.neutralPreset}</button>
58
+ <button class="fw-preset-btn" data-abv="42">{ui.aguardientePreset}</button>
59
+ </div>
60
+ <div class="fw-field">
61
+ <label class="fw-label" for="fw-spirit-abv">{ui.spiritAbvLabel}</label>
62
+ <div class="fw-slider-row">
63
+ <input type="range" class="fw-slider" id="fw-spirit-abv-slider" min="15" max="96" step="1" value="38" />
64
+ <input type="number" class="fw-num-input" id="fw-spirit-abv" min="15" max="96" step="0.1" value="38" />
65
+ </div>
66
+ </div>
67
+ </div>
68
+
69
+ <div class="fw-sec">
70
+ <div class="fw-field">
71
+ <label class="fw-label" for="fw-target-abv">{ui.targetAbvLabel}</label>
72
+ <div class="fw-slider-row">
73
+ <input type="range" class="fw-slider" id="fw-target-abv-slider" min="1" max="40" step="0.5" value="16" />
74
+ <input type="number" class="fw-num-input" id="fw-target-abv" min="1" max="40" step="0.1" value="16" />
75
+ </div>
76
+ </div>
77
+ <div class="fw-field" id="fw-target-vol-field" style="display:none; margin-top:0.75rem;">
78
+ <label class="fw-label" for="fw-target-vol">{ui.targetVolumeLabel}</label>
79
+ <div class="fw-slider-row">
80
+ <input type="range" class="fw-slider" id="fw-target-vol-slider" min="0.1" max="100" step="0.1" value="6" />
81
+ <input type="number" class="fw-num-input" id="fw-target-vol" min="0.1" max="9999" step="0.1" value="6" />
82
+ </div>
83
+ </div>
84
+ </div>
85
+
86
+ </div>
87
+
88
+ <div class="fw-right">
89
+
90
+ <div class="fw-pearson-wrap">
91
+ <div class="fw-pearson-title">{ui.pearsonTitle}</div>
92
+ <div id="fw-pearson-empty" class="fw-empty-state">{ui.emptyState}</div>
93
+ <svg id="fw-pearson-svg" class="fw-pearson-svg" viewBox="0 0 340 220" style="display:none" aria-hidden="true">
94
+ <rect x="60" y="30" width="180" height="160" fill="none" stroke="currentColor" stroke-width="1.5" stroke-opacity="0.2" rx="4" />
95
+ <line id="fw-diag1" x1="60" y1="30" x2="240" y2="190" stroke="#7f1d1d" stroke-width="1.5" stroke-dasharray="4 3" />
96
+ <line id="fw-diag2" x1="60" y1="190" x2="240" y2="30" stroke="#b45309" stroke-width="1.5" stroke-dasharray="4 3" />
97
+ <circle cx="150" cy="110" r="5" fill="#1e293b" id="fw-center-dot" />
98
+ <text id="fw-lbl-wine-abv" x="58" y="26" text-anchor="end" font-size="12" font-weight="700" fill="#7f1d1d">12%</text>
99
+ <text x="58" y="40" text-anchor="end" font-size="10" fill="currentColor" opacity="0.5" id="fw-lbl-wine-name">{ui.wineCornerLabel}</text>
100
+ <text id="fw-lbl-spirit-abv" x="58" y="186" text-anchor="end" font-size="12" font-weight="700" fill="#b45309">38%</text>
101
+ <text x="58" y="200" text-anchor="end" font-size="10" fill="currentColor" opacity="0.5" id="fw-lbl-spirit-name">{ui.spiritCornerLabel}</text>
102
+ <text id="fw-lbl-target" x="150" y="104" text-anchor="middle" font-size="13" font-weight="800" fill="currentColor">16%</text>
103
+ <text id="fw-lbl-parts-wine" x="244" y="26" font-size="11" font-weight="700" fill="#7f1d1d">22 parts</text>
104
+ <text id="fw-lbl-parts-spirit" x="244" y="186" font-size="11" font-weight="700" fill="#b45309">4 parts</text>
105
+ </svg>
106
+ <div id="fw-prop-wrap" style="display:none">
107
+ <div class="fw-prop-bar">
108
+ <div class="fw-prop-wine" id="fw-prop-wine-bar" style="width:85%"></div>
109
+ <div class="fw-prop-spirit" id="fw-prop-spirit-bar" style="width:15%"></div>
110
+ </div>
111
+ <div class="fw-prop-labels">
112
+ <span class="fw-prop-wine-pct" id="fw-prop-wine-pct">85% wine</span>
113
+ <span class="fw-prop-spirit-pct" id="fw-prop-spirit-pct">15% spirit</span>
114
+ </div>
115
+ </div>
116
+ <div id="fw-error" class="fw-error" style="display:none">{ui.errorAbv}</div>
117
+ </div>
118
+
119
+ <div class="fw-results" id="fw-results" style="display:none">
120
+ <div class="fw-results-title">{ui.resultsTitle}</div>
121
+ <div class="fw-result-grid">
122
+ <div class="fw-result-card accent">
123
+ <div class="fw-result-label">{ui.addLabel}</div>
124
+ <div class="fw-result-value"><span id="fw-res-spirit">—</span><span class="fw-result-unit">L</span></div>
125
+ </div>
126
+ <div class="fw-result-card">
127
+ <div class="fw-result-label">{ui.finalVolumeLabel}</div>
128
+ <div class="fw-result-value"><span id="fw-res-total">—</span><span class="fw-result-unit">L</span></div>
129
+ </div>
130
+ </div>
131
+ </div>
132
+
133
+ <div class="fw-bottles" id="fw-bottles" style="display:none">
134
+ <span class="fw-bottles-label">{ui.bottlesSection}</span>
135
+ <span class="fw-bottle-badge">
136
+ <Icon name="mdi:bottle-wine-outline" class="fw-bottle-icon" />
137
+ <span id="fw-bottles-75">—</span> × 75cl
138
+ </span>
139
+ <span class="fw-bottle-badge">
140
+ <Icon name="mdi:bottle-wine-outline" class="fw-bottle-icon" />
141
+ <span id="fw-bottles-50">—</span> × 50cl
142
+ </span>
143
+ </div>
144
+
145
+ <div class="fw-copy-wrap" id="fw-copy-wrap" style="display:none">
146
+ <button class="fw-copy-btn" id="fw-copy-btn" data-copied-text={ui.copiedBtn}>
147
+ <Icon name="mdi:content-copy" />
148
+ <span id="fw-copy-label">{ui.copyBtn}</span>
149
+ </button>
150
+ </div>
151
+
152
+ </div>
153
+ </div>
154
+ </div>
155
+ </div>
156
+
157
+ <script>
158
+ type Mode = 'a' | 'b';
159
+ let currentMode: Mode = 'a';
160
+
161
+ function getVal(id: string) { return parseFloat((document.getElementById(id) as HTMLInputElement).value); }
162
+ function setVal(id: string, v: number) { (document.getElementById(id) as HTMLInputElement).value = String(v); }
163
+ function el(id: string) { return document.getElementById(id)!; }
164
+
165
+ function setSliderPct(sliderId: string, v: number, min: number, max: number) {
166
+ const pct = ((v - min) / (max - min)) * 100;
167
+ (document.getElementById(sliderId) as HTMLInputElement).style.setProperty('--val', `${pct}%`);
168
+ }
169
+
170
+ function syncSlider(sliderId: string, inputId: string, min: number, max: number) {
171
+ const slider = document.getElementById(sliderId) as HTMLInputElement;
172
+ const input = document.getElementById(inputId) as HTMLInputElement;
173
+ setSliderPct(sliderId, Number(slider.value), min, max);
174
+ input.value = slider.value;
175
+ calculate();
176
+ }
177
+
178
+ function syncInput(sliderId: string, inputId: string, min: number, max: number) {
179
+ const input = document.getElementById(inputId) as HTMLInputElement;
180
+ let v = parseFloat(input.value);
181
+ if (isNaN(v)) return;
182
+ v = Math.min(max, Math.max(min, v));
183
+ (document.getElementById(sliderId) as HTMLInputElement).value = String(v);
184
+ setSliderPct(sliderId, v, min, max);
185
+ calculate();
186
+ }
187
+
188
+ function initSlider(sliderId: string, inputId: string, min: number, max: number) {
189
+ const slider = document.getElementById(sliderId) as HTMLInputElement;
190
+ const input = document.getElementById(inputId) as HTMLInputElement;
191
+ slider.addEventListener('input', () => syncSlider(sliderId, inputId, min, max));
192
+ input.addEventListener('input', () => syncInput(sliderId, inputId, min, max));
193
+ setSliderPct(sliderId, parseFloat(slider.value), min, max);
194
+ }
195
+
196
+ function showError() {
197
+ el('fw-error').style.display = '';
198
+ el('fw-pearson-svg').style.display = 'none';
199
+ el('fw-pearson-empty').style.display = '';
200
+ el('fw-prop-wrap').style.display = 'none';
201
+ el('fw-results').style.display = 'none';
202
+ el('fw-bottles').style.display = 'none';
203
+ el('fw-copy-wrap').style.display = 'none';
204
+ }
205
+
206
+ function updatePearson(abvs: { wine: number; spirit: number; target: number }, parts: { wine: number; spirit: number }) {
207
+ el('fw-lbl-wine-abv').textContent = `${abvs.wine}%`;
208
+ el('fw-lbl-spirit-abv').textContent = `${abvs.spirit}%`;
209
+ el('fw-lbl-target').textContent = `${abvs.target}%`;
210
+ el('fw-lbl-parts-wine').textContent = `${parts.wine.toFixed(1)} parts`;
211
+ el('fw-lbl-parts-spirit').textContent = `${parts.spirit.toFixed(1)} parts`;
212
+ el('fw-pearson-svg').style.display = '';
213
+ el('fw-pearson-empty').style.display = 'none';
214
+ el('fw-error').style.display = 'none';
215
+ }
216
+
217
+ function updatePropBar(partsWine: number, totalParts: number) {
218
+ const winePct = Math.round((partsWine / totalParts) * 100);
219
+ const spiritPct = 100 - winePct;
220
+ el('fw-prop-wine-bar').style.width = `${winePct}%`;
221
+ el('fw-prop-spirit-bar').style.width = `${spiritPct}%`;
222
+ el('fw-prop-wine-pct').textContent = `${winePct}% wine`;
223
+ el('fw-prop-spirit-pct').textContent = `${spiritPct}% spirit`;
224
+ el('fw-prop-wrap').style.display = '';
225
+ }
226
+
227
+ function updateResults(vols: { wine: number; spirit: number; total: number }, abvs: { wine: number; spirit: number; target: number }) {
228
+ el('fw-res-spirit').textContent = vols.spirit.toFixed(2);
229
+ el('fw-res-total').textContent = vols.total.toFixed(2);
230
+ el('fw-bottles-75').textContent = String(Math.ceil(vols.total / 0.75));
231
+ el('fw-bottles-50').textContent = String(Math.ceil(vols.total / 0.5));
232
+ el('fw-results').style.display = '';
233
+ el('fw-bottles').style.display = '';
234
+ el('fw-copy-wrap').style.display = '';
235
+ const btn = el('fw-copy-btn') as HTMLElement;
236
+ Object.assign(btn.dataset, { wine: vols.wine.toFixed(2), spirit: vols.spirit.toFixed(2), total: vols.total.toFixed(2), wineAbv: String(abvs.wine), spiritAbv: String(abvs.spirit), targetAbv: String(abvs.target) });
237
+ }
238
+
239
+ function calculate() {
240
+ const wineAbv = getVal('fw-wine-abv');
241
+ const spiritAbv = getVal('fw-spirit-abv');
242
+ const targetAbv = getVal('fw-target-abv');
243
+
244
+ el('fw-lbl-wine-abv').textContent = `${wineAbv}%`;
245
+ el('fw-lbl-spirit-abv').textContent = `${spiritAbv}%`;
246
+ el('fw-lbl-target').textContent = `${targetAbv}%`;
247
+
248
+ if (spiritAbv <= targetAbv || targetAbv <= wineAbv) { showError(); return; }
249
+
250
+ const partsWine = spiritAbv - targetAbv;
251
+ const partsSpirit = targetAbv - wineAbv;
252
+ const totalParts = partsWine + partsSpirit;
253
+
254
+ updatePearson({ wine: wineAbv, spirit: spiritAbv, target: targetAbv }, { wine: partsWine, spirit: partsSpirit });
255
+ updatePropBar(partsWine, totalParts);
256
+
257
+ let volWine = 0, volSpirit = 0, volTotal = 0;
258
+ if (currentMode === 'a') {
259
+ const wineVol = getVal('fw-wine-vol');
260
+ if (isNaN(wineVol) || wineVol <= 0) { el('fw-results').style.display = 'none'; el('fw-bottles').style.display = 'none'; el('fw-copy-wrap').style.display = 'none'; return; }
261
+ volWine = wineVol;
262
+ volSpirit = wineVol * (partsSpirit / partsWine);
263
+ volTotal = volWine + volSpirit;
264
+ } else {
265
+ const targetVol = getVal('fw-target-vol');
266
+ if (isNaN(targetVol) || targetVol <= 0) { el('fw-results').style.display = 'none'; el('fw-bottles').style.display = 'none'; el('fw-copy-wrap').style.display = 'none'; return; }
267
+ volWine = targetVol * (partsWine / totalParts);
268
+ volSpirit = targetVol * (partsSpirit / totalParts);
269
+ volTotal = targetVol;
270
+ }
271
+ updateResults({ wine: volWine, spirit: volSpirit, total: volTotal }, { wine: wineAbv, spirit: spiritAbv, target: targetAbv });
272
+ }
273
+
274
+ initSlider('fw-wine-vol-slider', 'fw-wine-vol', 0.1, 100);
275
+ initSlider('fw-wine-abv-slider', 'fw-wine-abv', 0, 25);
276
+ initSlider('fw-spirit-abv-slider', 'fw-spirit-abv', 15, 96);
277
+ initSlider('fw-target-abv-slider', 'fw-target-abv', 1, 40);
278
+ initSlider('fw-target-vol-slider', 'fw-target-vol', 0.1, 100);
279
+
280
+ document.querySelectorAll<HTMLButtonElement>('.fw-intention-btn').forEach(btn => {
281
+ btn.addEventListener('click', () => {
282
+ document.querySelectorAll('.fw-intention-btn').forEach(b => b.classList.remove('active'));
283
+ btn.classList.add('active');
284
+ const target = btn.dataset.target;
285
+ if (target) {
286
+ setVal('fw-target-abv', Number(target));
287
+ setVal('fw-target-abv-slider', Number(target));
288
+ setSliderPct('fw-target-abv-slider', Number(target), 1, 40);
289
+ }
290
+ calculate();
291
+ });
292
+ });
293
+
294
+ document.querySelectorAll<HTMLButtonElement>('.fw-mode-btn').forEach(btn => {
295
+ btn.addEventListener('click', () => {
296
+ document.querySelectorAll('.fw-mode-btn').forEach(b => b.classList.remove('active'));
297
+ btn.classList.add('active');
298
+ currentMode = btn.dataset.mode as Mode;
299
+ const wineVolField = (document.getElementById('fw-wine-vol') as HTMLElement)?.closest('.fw-field') as HTMLElement;
300
+ const targetVolField = el('fw-target-vol-field');
301
+ if (currentMode === 'b') { if (wineVolField) wineVolField.style.display = 'none'; targetVolField.style.display = ''; }
302
+ else { if (wineVolField) wineVolField.style.display = ''; targetVolField.style.display = 'none'; }
303
+ calculate();
304
+ });
305
+ });
306
+
307
+ document.querySelectorAll<HTMLButtonElement>('.fw-preset-btn').forEach(btn => {
308
+ btn.addEventListener('click', () => {
309
+ const abv = Number(btn.dataset.abv);
310
+ setVal('fw-spirit-abv', abv);
311
+ setVal('fw-spirit-abv-slider', abv);
312
+ setSliderPct('fw-spirit-abv-slider', abv, 15, 96);
313
+ calculate();
314
+ });
315
+ });
316
+
317
+ el('fw-copy-btn').addEventListener('click', function () {
318
+ const btn = this as HTMLElement;
319
+ const label = el('fw-copy-label');
320
+ const copiedText = btn.dataset.copiedText || 'Copied!';
321
+ const copyText = label.textContent || 'Copy Recipe';
322
+ const text = `Recipe: ${btn.dataset.wine}L wine ${btn.dataset.wineAbv}% + ${btn.dataset.spirit}L spirit ${btn.dataset.spiritAbv}% = ${btn.dataset.total}L @ ${btn.dataset.targetAbv}%`;
323
+ navigator.clipboard.writeText(text).then(() => {
324
+ btn.classList.add('copied');
325
+ label.textContent = copiedText;
326
+ setTimeout(() => { btn.classList.remove('copied'); label.textContent = copyText; }, 2000);
327
+ });
328
+ });
329
+
330
+ calculate();
331
+ </script>
@@ -0,0 +1,62 @@
1
+ import type { AlcoholToolEntry, ToolLocaleContent } from '../../types';
2
+ export { bibliography } from './bibliography';
3
+
4
+ export interface FortifiedWineBuilderUI {
5
+ [key: string]: string;
6
+ intentionTitle: string;
7
+ intentionVermouth: string;
8
+ intentionPort: string;
9
+ intentionSherry: string;
10
+ intentionCustom: string;
11
+ modeALabel: string;
12
+ modeBLabel: string;
13
+ wineSection: string;
14
+ wineVolumeLabel: string;
15
+ wineAbvLabel: string;
16
+ spiritSection: string;
17
+ spiritAbvLabel: string;
18
+ brandyPreset: string;
19
+ neutralPreset: string;
20
+ aguardientePreset: string;
21
+ targetAbvLabel: string;
22
+ targetVolumeLabel: string;
23
+ resultsTitle: string;
24
+ addLabel: string;
25
+ finalVolumeLabel: string;
26
+ bottlesSection: string;
27
+ copyBtn: string;
28
+ copiedBtn: string;
29
+ pearsonTitle: string;
30
+ wineCornerLabel: string;
31
+ spiritCornerLabel: string;
32
+ emptyState: string;
33
+ errorAbv: string;
34
+ errorMode: string;
35
+ }
36
+
37
+ export type FortifiedWineBuilderLocaleContent = ToolLocaleContent<FortifiedWineBuilderUI>;
38
+
39
+ export const fortifiedWine: AlcoholToolEntry<FortifiedWineBuilderUI> = {
40
+ id: 'fortified-wine-builder',
41
+ icons: {
42
+ bg: 'mdi:bottle-wine',
43
+ fg: 'mdi:glass-wine',
44
+ },
45
+ i18n: {
46
+ de: () => import('./i18n/de').then((m) => m.content),
47
+ en: () => import('./i18n/en').then((m) => m.content),
48
+ es: () => import('./i18n/es').then((m) => m.content),
49
+ fr: () => import('./i18n/fr').then((m) => m.content),
50
+ id: () => import('./i18n/id').then((m) => m.content),
51
+ it: () => import('./i18n/it').then((m) => m.content),
52
+ ja: () => import('./i18n/ja').then((m) => m.content),
53
+ ko: () => import('./i18n/ko').then((m) => m.content),
54
+ nl: () => import('./i18n/nl').then((m) => m.content),
55
+ pl: () => import('./i18n/pl').then((m) => m.content),
56
+ pt: () => import('./i18n/pt').then((m) => m.content),
57
+ ru: () => import('./i18n/ru').then((m) => m.content),
58
+ sv: () => import('./i18n/sv').then((m) => m.content),
59
+ tr: () => import('./i18n/tr').then((m) => m.content),
60
+ zh: () => import('./i18n/zh').then((m) => m.content),
61
+ },
62
+ };