@jjlmoya/utils-alcohol 1.1.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 (62) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +19 -0
  3. package/src/category/i18n/es.ts +28 -0
  4. package/src/category/i18n/fr.ts +19 -0
  5. package/src/category/index.ts +12 -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 +19 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +155 -0
  14. package/src/pages/[locale].astro +271 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/content_mandatory.test.ts +32 -0
  17. package/src/tests/faq_count.test.ts +17 -0
  18. package/src/tests/mocks/astro_mock.js +2 -0
  19. package/src/tests/seo_length.test.ts +39 -0
  20. package/src/tests/tool_validation.test.ts +17 -0
  21. package/src/tool/alcoholClearance/component.astro +219 -0
  22. package/src/tool/alcoholClearance/component.css +369 -0
  23. package/src/tool/alcoholClearance/i18n/en.ts +172 -0
  24. package/src/tool/alcoholClearance/i18n/es.ts +181 -0
  25. package/src/tool/alcoholClearance/i18n/fr.ts +163 -0
  26. package/src/tool/alcoholClearance/index.ts +50 -0
  27. package/src/tool/alcoholClearance/logic.ts +59 -0
  28. package/src/tool/beerCooler/component.astro +236 -0
  29. package/src/tool/beerCooler/component.css +381 -0
  30. package/src/tool/beerCooler/i18n/en.ts +168 -0
  31. package/src/tool/beerCooler/i18n/es.ts +181 -0
  32. package/src/tool/beerCooler/i18n/fr.ts +168 -0
  33. package/src/tool/beerCooler/index.ts +49 -0
  34. package/src/tool/beerCooler/logic.ts +34 -0
  35. package/src/tool/carbonationCalculator/component.astro +225 -0
  36. package/src/tool/carbonationCalculator/component.css +483 -0
  37. package/src/tool/carbonationCalculator/i18n/en.ts +175 -0
  38. package/src/tool/carbonationCalculator/i18n/es.ts +179 -0
  39. package/src/tool/carbonationCalculator/i18n/fr.ts +175 -0
  40. package/src/tool/carbonationCalculator/index.ts +48 -0
  41. package/src/tool/carbonationCalculator/logic.ts +40 -0
  42. package/src/tool/cocktailBalancer/bibliography.astro +14 -0
  43. package/src/tool/cocktailBalancer/component.astro +396 -0
  44. package/src/tool/cocktailBalancer/component.css +1218 -0
  45. package/src/tool/cocktailBalancer/data/IngredientRepository.ts +83 -0
  46. package/src/tool/cocktailBalancer/data/Presets.ts +122 -0
  47. package/src/tool/cocktailBalancer/domain/Ingredient.ts +29 -0
  48. package/src/tool/cocktailBalancer/i18n/en.ts +193 -0
  49. package/src/tool/cocktailBalancer/i18n/es.ts +193 -0
  50. package/src/tool/cocktailBalancer/i18n/fr.ts +193 -0
  51. package/src/tool/cocktailBalancer/index.ts +68 -0
  52. package/src/tool/cocktailBalancer/logic.ts +118 -0
  53. package/src/tool/cocktailBalancer/seo.astro +53 -0
  54. package/src/tool/partyKeg/component.astro +269 -0
  55. package/src/tool/partyKeg/component.css +660 -0
  56. package/src/tool/partyKeg/i18n/en.ts +162 -0
  57. package/src/tool/partyKeg/i18n/es.ts +166 -0
  58. package/src/tool/partyKeg/i18n/fr.ts +162 -0
  59. package/src/tool/partyKeg/index.ts +46 -0
  60. package/src/tool/partyKeg/logic.ts +36 -0
  61. package/src/tools.ts +14 -0
  62. package/src/types.ts +72 -0
@@ -0,0 +1,396 @@
1
+ ---
2
+ import { getAllIngredients } from "./data/IngredientRepository";
3
+ import { Icon } from "astro-icon/components";
4
+ import { COCKTAIL_PRESETS } from "./data/Presets";
5
+ import type { CocktailBalancerUI } from "./index";
6
+ import './component.css';
7
+
8
+ interface Props {
9
+ ui: CocktailBalancerUI;
10
+ }
11
+
12
+ const { ui } = Astro.props;
13
+ const allIngredients = getAllIngredients();
14
+
15
+ function getIngTypeClass(type: string): string {
16
+ if (type === "spirit") return "ing-icon-spirit";
17
+ if (type === "citrus") return "ing-icon-citrus";
18
+ if (type === "syrup") return "ing-icon-syrup";
19
+ return "ing-icon-other";
20
+ }
21
+
22
+ function getIngIconName(type: string): string {
23
+ if (type === "spirit") return "mdi:glass-cocktail";
24
+ if (type === "citrus") return "mdi:fruit-citrus";
25
+ return "mdi:bottle-tonic";
26
+ }
27
+ ---
28
+
29
+ <div class="balancer-app">
30
+ <div class="balancer-card">
31
+ <div class="balancer-header">
32
+ <h2 class="balancer-title">
33
+ <span class="balancer-title-icon"><Icon name="mdi:scale-balance" /></span>
34
+ {ui.title} <span class="balancer-version">v2.1</span>
35
+ </h2>
36
+
37
+ <div class="balancer-actions">
38
+ <button id="open-presets-btn" class="btn-secondary">
39
+ <Icon name="mdi:book-open-page-variant" /> {ui.presetsBtn}
40
+ </button>
41
+ <button id="save-recipe-btn" class="btn-primary">
42
+ <Icon name="mdi:content-save" /> {ui.saveBtn}
43
+ </button>
44
+ <button id="reset-btn" class="btn-danger">
45
+ <Icon name="mdi:trash-can-outline" />
46
+ </button>
47
+ </div>
48
+ </div>
49
+
50
+ <div class="balancer-grid">
51
+ <div class="recipe-column">
52
+ <div id="recipe-container" class="recipe-container">
53
+ <div id="empty-state" class="empty-state">
54
+ <div class="empty-icon-wrap">
55
+ <Icon name="mdi:flask-outline" class="empty-flask-icon" />
56
+ </div>
57
+ <h3 class="empty-title">{ui.emptyStateTitle}</h3>
58
+ <p class="empty-desc">{ui.emptyStateDescription}</p>
59
+ <button id="add-first-btn" class="btn-primary btn-add-first">
60
+ <Icon name="mdi:plus-circle" /> {ui.addBtn}
61
+ </button>
62
+ </div>
63
+ </div>
64
+
65
+ <button id="open-modal-btn" class="btn-add-more" style="display:none">
66
+ <Icon name="mdi:plus-circle-outline" class="add-more-icon" />
67
+ {ui.addMoreBtn}
68
+ </button>
69
+ </div>
70
+
71
+ <div class="dashboard-column">
72
+ <div class="flavor-card">
73
+ <div class="flavor-card-accent"></div>
74
+ <div class="flavor-card-header">
75
+ <h3 class="flavor-card-title">{ui.flavorProfileTitle}</h3>
76
+ <div class="abv-badge" id="stat-abv-badge">0.0% ABV</div>
77
+ </div>
78
+
79
+ <div class="radar-wrap">
80
+ <svg viewBox="0 0 200 200" class="radar-svg">
81
+ <polygon points="100,20 176,75 147,164 53,164 24,75" fill="none" class="radar-bg-poly" stroke-width="1"></polygon>
82
+ <polygon id="radar-shape" points="100,100 100,100 100,100 100,100 100,100" class="radar-shape" stroke-width="2" stroke-linejoin="round"></polygon>
83
+ <circle id="pt-sweet" cx="100" cy="100" r="3" class="radar-dot"></circle>
84
+ <circle id="pt-bitter" cx="100" cy="100" r="3" class="radar-dot"></circle>
85
+ <circle id="pt-complex" cx="100" cy="100" r="3" class="radar-dot"></circle>
86
+ <circle id="pt-alc" cx="100" cy="100" r="3" class="radar-dot"></circle>
87
+ <circle id="pt-acid" cx="100" cy="100" r="3" class="radar-dot"></circle>
88
+ </svg>
89
+ <span class="radar-label radar-top">Sweet</span>
90
+ <span class="radar-label radar-right-top">Bitter</span>
91
+ <span class="radar-label radar-right-bot">Complex</span>
92
+ <span class="radar-label radar-left-bot">Alcohol</span>
93
+ <span class="radar-label radar-left-top">Acid</span>
94
+ </div>
95
+ </div>
96
+
97
+ <div class="stats-grid">
98
+ <div class="stat-card">
99
+ <div class="stat-card-label">{ui.volLabel}</div>
100
+ <div class="stat-card-value"><span id="stat-vol">0</span><span class="stat-card-unit">ml</span></div>
101
+ </div>
102
+ <div class="stat-card">
103
+ <div class="stat-card-label">{ui.sugarLabel}</div>
104
+ <div class="stat-card-value stat-value-amber"><span id="stat-sugar">0</span><span class="stat-card-unit">g</span></div>
105
+ </div>
106
+ <div class="stat-card stat-card-color">
107
+ <div class="stat-card-label">{ui.colorLabel}</div>
108
+ <div class="color-swatch"><div id="sim-color-bg" class="color-swatch-inner"></div></div>
109
+ </div>
110
+ </div>
111
+
112
+ <div class="balance-card">
113
+ <div class="balance-card-header">
114
+ <h3 class="balance-card-title">{ui.sourLawTitle}</h3>
115
+ <div id="balance-verdict" class="balance-verdict">--</div>
116
+ </div>
117
+
118
+ <div class="balance-track">
119
+ <div class="balance-zone balance-zone-left"></div>
120
+ <div class="balance-zone balance-zone-right"></div>
121
+ <div class="balance-center-line"></div>
122
+ <div id="balance-needle" class="balance-needle" style="left:50%"></div>
123
+ </div>
124
+
125
+ <div class="balance-labels">
126
+ <span class="balance-lbl-acid">{ui.acidDryLabel}</span>
127
+ <span class="balance-lbl-mid">{ui.balanceLabel}</span>
128
+ <span class="balance-lbl-sweet">{ui.sweetLabel}</span>
129
+ </div>
130
+ </div>
131
+
132
+ <div id="fix-box" class="fix-box" style="display:none">
133
+ <div class="fix-icon-wrap">
134
+ <Icon name="mdi:lightbulb-on-outline" class="fix-icon" />
135
+ </div>
136
+ <div>
137
+ <h4 class="fix-title">{ui.aiSuggestionTitle}</h4>
138
+ <p id="fix-text" class="fix-text"></p>
139
+ </div>
140
+ </div>
141
+ </div>
142
+ </div>
143
+
144
+ </div>
145
+
146
+ <dialog id="ingredient-modal" class="modal-dialog">
147
+ <div class="modal-body">
148
+ <div class="modal-header">
149
+ <div class="modal-header-row">
150
+ <h3 class="modal-title">{ui.addIngredientTitle}</h3>
151
+ <button id="close-modal-btn" class="modal-close-btn">
152
+ <Icon name="mdi:close" />
153
+ </button>
154
+ </div>
155
+ <div class="modal-search-wrap">
156
+ <Icon name="mdi:magnify" class="modal-search-icon" />
157
+ <input type="text" id="modal-search" placeholder={ui.searchPlaceholder} class="modal-search-input" />
158
+ </div>
159
+ </div>
160
+
161
+ <div class="modal-scroll">
162
+ <div class="ing-grid">
163
+ {allIngredients.map((ing) => (
164
+ <button class="ing-select-btn" data-id={ing.id}>
165
+ <div class={`ing-icon-wrap ${getIngTypeClass(ing.type)}`}>
166
+ <Icon name={getIngIconName(ing.type)} />
167
+ </div>
168
+ <div class="ing-info">
169
+ <div class="ing-name">{ing.name}</div>
170
+ <div class="ing-meta">{ing.type} • {ing.abv}%</div>
171
+ </div>
172
+ </button>
173
+ ))}
174
+ </div>
175
+ </div>
176
+ </div>
177
+ </dialog>
178
+
179
+ <dialog id="presets-modal" class="modal-dialog modal-dialog-wide">
180
+ <div class="modal-body">
181
+ <div class="modal-header modal-header-simple">
182
+ <h3 class="modal-title modal-title-with-icon">
183
+ <Icon name="mdi:book-open-variant" class="modal-title-icon" /> {ui.presetsTitle}
184
+ </h3>
185
+ <button id="close-presets-btn" class="modal-close-btn"><Icon name="mdi:close" /></button>
186
+ </div>
187
+ <div class="modal-scroll modal-scroll-bg">
188
+ <div id="saved-section" class="saved-section" style="display:none">
189
+ <h4 class="section-heading">{ui.savedSectionTitle}</h4>
190
+ <div id="saved-grid" class="presets-grid"></div>
191
+ </div>
192
+ <h4 class="section-heading">{ui.classicsSectionTitle}</h4>
193
+ <div class="presets-grid">
194
+ {COCKTAIL_PRESETS.map((p) => (
195
+ <button class="preset-load-btn" data-preset-id={p.id}>
196
+ <div class="preset-icon-row">
197
+ <Icon name={p.icon} class="preset-icon" /> <span class="preset-name">{p.name}</span>
198
+ </div>
199
+ <p class="preset-desc">{p.description}</p>
200
+ </button>
201
+ ))}
202
+ </div>
203
+ </div>
204
+ </div>
205
+ </dialog>
206
+
207
+ <dialog id="confirm-modal" class="modal-dialog modal-dialog-sm">
208
+ <div class="confirm-body">
209
+ <div class="confirm-icon-wrap">
210
+ <Icon name="mdi:alert-circle-outline" class="confirm-icon" />
211
+ </div>
212
+ <div>
213
+ <h3 class="confirm-title">{ui.confirmDeleteTitle}</h3>
214
+ <p class="confirm-text">{ui.confirmDeleteText}</p>
215
+ </div>
216
+ <div class="confirm-actions">
217
+ <button id="cancel-reset-btn" class="btn-secondary btn-confirm">{ui.cancelBtn}</button>
218
+ <button id="confirm-reset-btn" class="btn-danger-solid btn-confirm">{ui.deleteBtn}</button>
219
+ </div>
220
+ </div>
221
+ </dialog>
222
+
223
+ <div style="display:none">
224
+ <div id="tpl-icon-trash"><Icon name="mdi:trash-can-outline" /></div>
225
+ <div id="ui-labels" data-ui={JSON.stringify(ui)}></div>
226
+ </div>
227
+ </div>
228
+
229
+ <script>
230
+ import { getIngredientById } from "./data/IngredientRepository";
231
+ import { calculateCocktail, getBalanceVerdict, getFixSuggestion } from "./logic";
232
+ import { COCKTAIL_PRESETS } from "./data/Presets";
233
+ import type { CocktailBalancerUI } from "./index";
234
+ import type { CocktailComponent } from "./domain/Ingredient";
235
+
236
+ interface RecipeItem { uId: string; id: string; vol: number; }
237
+
238
+ class CocktailApp {
239
+ recipe: RecipeItem[] = [];
240
+ STORAGE_KEY = "cocktail_tool_current";
241
+ ui = {} as CocktailBalancerUI;
242
+
243
+ constructor() {
244
+ const uiEl = document.getElementById("ui-labels");
245
+ if (uiEl) this.ui = JSON.parse(uiEl.dataset.ui || "{}") as CocktailBalancerUI;
246
+ this.bindEvents();
247
+ this.loadState();
248
+ }
249
+
250
+ loadState() {
251
+ const saved = localStorage.getItem(this.STORAGE_KEY);
252
+ if (saved) { try { this.recipe = JSON.parse(saved) as RecipeItem[]; } catch { this.recipe = []; } }
253
+ this.refreshUI();
254
+ }
255
+
256
+ saveState() { localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.recipe)); }
257
+
258
+ addIngredient(id: string) {
259
+ const ing = getIngredientById(id);
260
+ if (!ing) return;
261
+ this.recipe.push({ uId: Math.random().toString(36).slice(2, 11), id, vol: ing.type === "spirit" ? 45 : 30 });
262
+ this.saveState(); this.refreshUI();
263
+ }
264
+
265
+ removeIngredient(uId: string) {
266
+ this.recipe = this.recipe.filter(item => item.uId !== uId);
267
+ this.saveState(); this.refreshUI();
268
+ }
269
+
270
+ updateVolume(uId: string, newVol: string) {
271
+ const item = this.recipe.find(i => i.uId === uId);
272
+ if (item) { item.vol = parseFloat(newVol); this.saveState(); this.updateDashboard(); }
273
+ }
274
+
275
+ refreshUI() {
276
+ this.renderList(); this.updateDashboard();
277
+ const addBtn = document.getElementById("open-modal-btn");
278
+ if (addBtn) addBtn.style.display = this.recipe.length > 0 ? "flex" : "none";
279
+ }
280
+
281
+ buildRecipeRow(item: RecipeItem): HTMLElement {
282
+ const ing = getIngredientById(item.id);
283
+ if (!ing) return document.createElement("div");
284
+ const trashIcon = document.getElementById("tpl-icon-trash")?.innerHTML ?? '';
285
+ const row = document.createElement("div");
286
+ row.className = "recipe-row";
287
+ row.innerHTML = `<div class="recipe-row-top"><div class="recipe-row-info"><div class="recipe-row-name">${ing.name}</div><span class="recipe-row-type">${ing.type}</span></div><button class="btn-danger recipe-row-del btn-remove" data-uid="${item.uId}">${trashIcon}</button></div><div class="recipe-row-controls"><input type="range" class="recipe-range input-range" min="0" max="150" step="0.5" value="${item.vol}" data-uid="${item.uId}"><div class="recipe-number-wrap"><input type="number" class="recipe-number input-number" value="${item.vol}" data-uid="${item.uId}"></div></div>`;
288
+ return row;
289
+ }
290
+
291
+ renderList() {
292
+ const container = document.getElementById("recipe-container");
293
+ const emptyState = document.getElementById("empty-state");
294
+ if (!container || !emptyState) return;
295
+ const empDisplay = this.recipe.length === 0 ? "flex" : "none";
296
+ emptyState.style.display = empDisplay;
297
+ Array.from(container.children).forEach(child => { if ((child as HTMLElement).id !== "empty-state") child.remove(); });
298
+ if (this.recipe.length === 0) return;
299
+ this.recipe.forEach(item => container.appendChild(this.buildRecipeRow(item)));
300
+ container.querySelectorAll<HTMLButtonElement>(".btn-remove").forEach(b => {
301
+ b.addEventListener("click", () => this.removeIngredient(b.dataset.uid ?? ''));
302
+ });
303
+ container.querySelectorAll<HTMLInputElement>(".input-range, .input-number").forEach(i => {
304
+ i.addEventListener("input", () => this.updateVolume(i.dataset.uid ?? '', i.value));
305
+ });
306
+ }
307
+
308
+ updateNeedle(stats: ReturnType<typeof calculateCocktail>) {
309
+ const verdict = getBalanceVerdict(stats);
310
+ const verdictEl = document.getElementById("balance-verdict");
311
+ const needleEl = document.getElementById("balance-needle");
312
+ if (!verdictEl || !needleEl) return;
313
+ if (this.recipe.length === 0) { verdictEl.innerText = "--"; verdictEl.className = "balance-verdict"; needleEl.style.left = "50%"; return; }
314
+ verdictEl.innerText = this.ui[verdict.labelKey] || verdict.labelKey;
315
+ verdictEl.className = "balance-verdict " + verdict.colorClass;
316
+ needleEl.style.left = `${Math.max(2, Math.min(98, (stats.balanceRatio / 15) * 100))}%`;
317
+ }
318
+
319
+ updateDashboard() {
320
+ const calcItems = this.recipe.map(r => ({ ingredient: getIngredientById(r.id), volumeMl: r.vol })).filter(c => !!c.ingredient) as CocktailComponent[];
321
+ const stats = calculateCocktail(calcItems);
322
+ const volEl = document.getElementById("stat-vol");
323
+ const abvEl = document.getElementById("stat-abv-badge");
324
+ const sugarEl = document.getElementById("stat-sugar");
325
+ const colorEl = document.getElementById("sim-color-bg");
326
+ if (volEl) volEl.innerText = String(Math.round(stats.totalVolumeMl));
327
+ if (abvEl) abvEl.innerText = stats.finalAbv.toFixed(1) + "% ABV";
328
+ if (sugarEl) sugarEl.innerText = String(Math.round(stats.totalSugarGrams));
329
+ if (colorEl) colorEl.style.backgroundColor = stats.finalColor;
330
+ this.renderRadar(stats);
331
+ this.updateNeedle(stats);
332
+ this.updateFix(stats);
333
+ }
334
+
335
+ updateFix(stats: ReturnType<typeof calculateCocktail>) {
336
+ const fixBox = document.getElementById("fix-box");
337
+ const fixText = document.getElementById("fix-text");
338
+ const fix = getFixSuggestion(stats);
339
+ if (!fixBox || !fixText) return;
340
+ if (fix) {
341
+ fixBox.style.display = "flex";
342
+ fixText.innerHTML = `<strong>${this.ui[fix.actionKey]}:</strong> ${fix.amountValue}`;
343
+ } else {
344
+ fixBox.style.display = "none";
345
+ }
346
+ }
347
+
348
+ renderRadar(stats: ReturnType<typeof calculateCocktail>) {
349
+ const R = 80, C = 100;
350
+ const rad = (deg: number) => (deg * Math.PI) / 180;
351
+ const vSweet = Math.min((stats.sugarConcentration / 18) * 100, 100);
352
+ const vBitter = Math.min((stats.bitternessIndex / 8) * 100, 100);
353
+ const vComplex = Math.min((stats.complexityIndex / 8) * 100, 100);
354
+ const vAlc = Math.min((stats.finalAbv / 40) * 100, 100);
355
+ const vAcid = Math.min((stats.acidConcentration / 1.5) * 100, 100);
356
+ const getPt = (val: number, deg: number) => ({ x: C + (val / 100) * R * Math.cos(rad(deg)), y: C + (val / 100) * R * Math.sin(rad(deg)) });
357
+ const pts = [getPt(vSweet, -90), getPt(vBitter, -18), getPt(vComplex, 54), getPt(vAlc, 126), getPt(vAcid, 198)];
358
+ document.getElementById("radar-shape")?.setAttribute("points", pts.map(p => `${p.x},${p.y}`).join(" "));
359
+ ["pt-sweet", "pt-bitter", "pt-complex", "pt-alc", "pt-acid"].forEach((id, i) => {
360
+ const pt = pts[i];
361
+ if (!pt) return;
362
+ document.getElementById(id)?.setAttribute("cx", String(pt.x));
363
+ document.getElementById(id)?.setAttribute("cy", String(pt.y));
364
+ });
365
+ }
366
+
367
+ getDialog(id: string): HTMLDialogElement | null {
368
+ return document.getElementById(id) as HTMLDialogElement | null;
369
+ }
370
+
371
+ bindModalEvents() {
372
+ document.getElementById("open-presets-btn")?.addEventListener("click", () => this.getDialog("presets-modal")?.showModal());
373
+ document.getElementById("close-presets-btn")?.addEventListener("click", () => this.getDialog("presets-modal")?.close());
374
+ document.getElementById("open-modal-btn")?.addEventListener("click", () => { this.getDialog("ingredient-modal")?.showModal(); (document.getElementById("modal-search") as HTMLInputElement | null)?.focus(); });
375
+ document.getElementById("add-first-btn")?.addEventListener("click", () => this.getDialog("ingredient-modal")?.showModal());
376
+ document.getElementById("close-modal-btn")?.addEventListener("click", () => this.getDialog("ingredient-modal")?.close());
377
+ }
378
+
379
+ bindEvents() {
380
+ this.bindModalEvents();
381
+ document.querySelectorAll<HTMLButtonElement>(".ing-select-btn").forEach(btn => btn.addEventListener("click", () => { this.addIngredient(btn.dataset.id ?? ''); this.getDialog("ingredient-modal")?.close(); }));
382
+ document.querySelectorAll<HTMLButtonElement>(".preset-load-btn").forEach(btn => btn.addEventListener("click", () => this.loadPreset(btn.dataset.presetId ?? '')));
383
+ document.getElementById("reset-btn")?.addEventListener("click", () => this.getDialog("confirm-modal")?.showModal());
384
+ document.getElementById("cancel-reset-btn")?.addEventListener("click", () => this.getDialog("confirm-modal")?.close());
385
+ document.getElementById("confirm-reset-btn")?.addEventListener("click", () => { this.recipe = []; this.saveState(); this.refreshUI(); this.getDialog("confirm-modal")?.close(); });
386
+ }
387
+
388
+ loadPreset(presetId: string) {
389
+ const p = COCKTAIL_PRESETS.find(x => x.id === presetId);
390
+ if (p) { this.recipe = p.ingredients.map(i => ({ uId: Math.random().toString(36).slice(2, 11), id: i.id, vol: i.vol })); this.saveState(); this.refreshUI(); }
391
+ (document.getElementById("presets-modal") as HTMLDialogElement)?.close();
392
+ }
393
+ }
394
+
395
+ new CocktailApp();
396
+ </script>