vinofyi-embed 1.0.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.
@@ -0,0 +1,1723 @@
1
+ /* vinofyi-embed v1.0.0 | MIT | https://widget.vinofyi.com */
2
+
3
+ // src/styles/modern.ts
4
+ function getModernCSS() {
5
+ return `
6
+ /* Modern: gradient accent header */
7
+ .drinkfyi-header {
8
+ background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 70%, #000));
9
+ border-radius: 12px 12px 0 0;
10
+ padding: 16px 20px;
11
+ display: flex;
12
+ align-items: flex-start;
13
+ gap: 14px;
14
+ }
15
+
16
+ .drinkfyi-header-title {
17
+ font-size: 15px;
18
+ font-weight: 700;
19
+ color: #fff;
20
+ margin: 0 0 4px 0;
21
+ line-height: 1.3;
22
+ }
23
+
24
+ .drinkfyi-header-subtitle {
25
+ font-size: 12px;
26
+ color: rgba(255, 255, 255, 0.8);
27
+ margin: 0;
28
+ }
29
+
30
+ /* Image area \u2014 for recipe/entity cards */
31
+ .drinkfyi-img {
32
+ width: 56px;
33
+ height: 56px;
34
+ border-radius: 8px;
35
+ object-fit: cover;
36
+ background: rgba(255, 255, 255, 0.15);
37
+ flex-shrink: 0;
38
+ display: flex;
39
+ align-items: center;
40
+ justify-content: center;
41
+ overflow: hidden;
42
+ }
43
+
44
+ .drinkfyi-img img {
45
+ width: 100%;
46
+ height: 100%;
47
+ object-fit: cover;
48
+ border-radius: 8px;
49
+ }
50
+
51
+ /* Body area */
52
+ .drinkfyi-body {
53
+ padding: 16px 20px;
54
+ }
55
+
56
+ /* Key-value rows \u2014 spacious */
57
+ .drinkfyi-row {
58
+ display: flex;
59
+ justify-content: space-between;
60
+ align-items: flex-start;
61
+ gap: 12px;
62
+ padding: 8px 0;
63
+ border-bottom: 1px solid var(--border);
64
+ }
65
+
66
+ .drinkfyi-row:last-child {
67
+ border-bottom: none;
68
+ }
69
+
70
+ .drinkfyi-label {
71
+ font-size: 12px;
72
+ font-weight: 500;
73
+ color: var(--muted);
74
+ white-space: nowrap;
75
+ flex-shrink: 0;
76
+ min-width: 30%;
77
+ }
78
+
79
+ .drinkfyi-value {
80
+ font-size: 13px;
81
+ color: var(--text);
82
+ text-align: right;
83
+ word-break: break-word;
84
+ }
85
+
86
+ /* Section title */
87
+ .drinkfyi-section-title {
88
+ font-size: 11px;
89
+ font-weight: 600;
90
+ color: var(--muted);
91
+ text-transform: uppercase;
92
+ letter-spacing: 0.06em;
93
+ margin: 0 0 10px 0;
94
+ }
95
+
96
+ /* Tags \u2014 colored rounded badges */
97
+ .drinkfyi-tag {
98
+ display: inline-block;
99
+ font-size: 11px;
100
+ font-weight: 600;
101
+ padding: 3px 10px;
102
+ border-radius: 12px;
103
+ background: color-mix(in srgb, var(--accent) 12%, transparent);
104
+ color: var(--accent);
105
+ margin: 2px 3px 2px 0;
106
+ letter-spacing: 0.02em;
107
+ }
108
+
109
+ /* Link */
110
+ .drinkfyi-link {
111
+ font-size: 13px;
112
+ font-weight: 500;
113
+ color: var(--link);
114
+ text-decoration: none;
115
+ display: inline-flex;
116
+ align-items: center;
117
+ gap: 4px;
118
+ }
119
+
120
+ .drinkfyi-link:hover {
121
+ opacity: 0.8;
122
+ text-decoration: underline;
123
+ }
124
+
125
+ .drinkfyi-link svg {
126
+ width: 12px;
127
+ height: 12px;
128
+ flex-shrink: 0;
129
+ }
130
+
131
+ /* Footer link row */
132
+ .drinkfyi-footer-link {
133
+ display: flex;
134
+ align-items: center;
135
+ justify-content: space-between;
136
+ padding: 12px 20px;
137
+ border-top: 1px solid var(--border);
138
+ gap: 8px;
139
+ }
140
+ `;
141
+ }
142
+
143
+ // src/styles/classic.ts
144
+ function getClassicCSS() {
145
+ return `
146
+ /* Classic: serif headings throughout */
147
+ .drinkfyi-widget {
148
+ font-family: Georgia, 'Times New Roman', serif;
149
+ }
150
+
151
+ /* Header \u2014 surface background with thin accent top border */
152
+ .drinkfyi-header {
153
+ background: var(--surface);
154
+ border-top: 3px solid var(--accent);
155
+ padding: 16px 20px;
156
+ display: flex;
157
+ align-items: flex-start;
158
+ gap: 14px;
159
+ }
160
+
161
+ .drinkfyi-header-title {
162
+ font-size: 16px;
163
+ font-weight: 700;
164
+ color: var(--text);
165
+ margin: 0 0 4px 0;
166
+ line-height: 1.3;
167
+ font-family: Georgia, 'Times New Roman', serif;
168
+ }
169
+
170
+ .drinkfyi-header-subtitle {
171
+ font-size: 12px;
172
+ color: var(--muted);
173
+ margin: 0;
174
+ font-style: italic;
175
+ }
176
+
177
+ /* Image area \u2014 for recipe/entity cards */
178
+ .drinkfyi-img {
179
+ width: 56px;
180
+ height: 56px;
181
+ border-radius: 4px;
182
+ object-fit: cover;
183
+ background: var(--badge-bg);
184
+ flex-shrink: 0;
185
+ display: flex;
186
+ align-items: center;
187
+ justify-content: center;
188
+ overflow: hidden;
189
+ border: 1px solid var(--border);
190
+ }
191
+
192
+ .drinkfyi-img img {
193
+ width: 100%;
194
+ height: 100%;
195
+ object-fit: cover;
196
+ border-radius: 4px;
197
+ }
198
+
199
+ /* Body area */
200
+ .drinkfyi-body {
201
+ padding: 14px 20px;
202
+ }
203
+
204
+ /* Key-value rows \u2014 dotted borders */
205
+ .drinkfyi-row {
206
+ display: flex;
207
+ justify-content: space-between;
208
+ align-items: flex-start;
209
+ gap: 12px;
210
+ padding: 8px 0;
211
+ border-bottom: 1px dotted var(--border);
212
+ }
213
+
214
+ .drinkfyi-row:last-child {
215
+ border-bottom: none;
216
+ }
217
+
218
+ .drinkfyi-label {
219
+ font-size: 11px;
220
+ font-weight: 600;
221
+ color: var(--muted);
222
+ white-space: nowrap;
223
+ flex-shrink: 0;
224
+ min-width: 30%;
225
+ font-variant: small-caps;
226
+ letter-spacing: 0.06em;
227
+ text-transform: lowercase;
228
+ }
229
+
230
+ .drinkfyi-value {
231
+ font-size: 13px;
232
+ color: var(--text);
233
+ text-align: right;
234
+ word-break: break-word;
235
+ }
236
+
237
+ /* Section title */
238
+ .drinkfyi-section-title {
239
+ font-size: 11px;
240
+ font-weight: 600;
241
+ color: var(--muted);
242
+ font-variant: small-caps;
243
+ letter-spacing: 0.08em;
244
+ text-transform: lowercase;
245
+ margin: 0 0 10px 0;
246
+ }
247
+
248
+ /* Tags \u2014 muted badges with border */
249
+ .drinkfyi-tag {
250
+ display: inline-block;
251
+ font-size: 11px;
252
+ font-weight: 500;
253
+ padding: 2px 8px;
254
+ border-radius: 3px;
255
+ background: var(--badge-bg);
256
+ color: var(--badge-text);
257
+ border: 1px solid var(--border);
258
+ margin: 2px 3px 2px 0;
259
+ letter-spacing: 0.02em;
260
+ }
261
+
262
+ /* Link */
263
+ .drinkfyi-link {
264
+ font-size: 13px;
265
+ font-weight: 500;
266
+ color: var(--link);
267
+ text-decoration: none;
268
+ display: inline-flex;
269
+ align-items: center;
270
+ gap: 4px;
271
+ font-style: normal;
272
+ }
273
+
274
+ .drinkfyi-link:hover {
275
+ text-decoration: underline;
276
+ }
277
+
278
+ .drinkfyi-link svg {
279
+ width: 12px;
280
+ height: 12px;
281
+ flex-shrink: 0;
282
+ }
283
+
284
+ /* Footer link row */
285
+ .drinkfyi-footer-link {
286
+ display: flex;
287
+ align-items: center;
288
+ justify-content: space-between;
289
+ padding: 10px 20px;
290
+ border-top: 1px solid var(--border);
291
+ gap: 8px;
292
+ }
293
+ `;
294
+ }
295
+
296
+ // src/themes.ts
297
+ function getStyleCSS(style) {
298
+ switch (style) {
299
+ case "classic":
300
+ return getClassicCSS();
301
+ case "modern":
302
+ default:
303
+ return getModernCSS();
304
+ }
305
+ }
306
+ function getThemeCSS(accent, style = "modern") {
307
+ return `
308
+ :host {
309
+ display: block;
310
+ --site-accent: ${accent};
311
+ }
312
+
313
+ /* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
314
+ Size variants
315
+ compact=280px, default=420px, large=720px
316
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
317
+ .drinkfyi-widget {
318
+ box-sizing: border-box;
319
+ min-width: 240px;
320
+ max-width: 420px;
321
+ border-radius: 8px;
322
+ overflow: hidden;
323
+ border: 1px solid var(--border);
324
+ background: var(--bg);
325
+ color: var(--text);
326
+ font-size: 14px;
327
+ line-height: 1.6;
328
+ transition: border-color 0.2s;
329
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
330
+ }
331
+
332
+ .drinkfyi-widget[data-size="compact"] {
333
+ max-width: 280px;
334
+ font-size: 13px;
335
+ }
336
+
337
+ .drinkfyi-widget[data-size="default"] {
338
+ max-width: 420px;
339
+ }
340
+
341
+ .drinkfyi-widget[data-size="large"] {
342
+ max-width: 720px;
343
+ }
344
+
345
+ /* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
346
+ Light theme (default)
347
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
348
+ .drinkfyi-widget[data-theme="light"] {
349
+ --bg: #fff;
350
+ --text: #1e293b;
351
+ --border: #e2e8f0;
352
+ --accent: var(--site-accent);
353
+ --muted: #64748b;
354
+ --surface: #f8fafc;
355
+ --badge-bg: #f1f5f9;
356
+ --badge-text: #374151;
357
+ --link: var(--site-accent);
358
+ --copy-bg: #f3f4f6;
359
+ --copy-hover: #e5e7eb;
360
+ --input-bg: #ffffff;
361
+ --input-border: #d1d5db;
362
+ --input-focus: var(--site-accent);
363
+ --shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
364
+ }
365
+
366
+ /* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
367
+ Dark theme
368
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
369
+ .drinkfyi-widget[data-theme="dark"] {
370
+ --bg: #1a1a1a;
371
+ --text: #f3f4f6;
372
+ --border: #374151;
373
+ --accent: var(--site-accent);
374
+ --muted: #9ca3af;
375
+ --surface: #111827;
376
+ --badge-bg: #374151;
377
+ --badge-text: #d1d5db;
378
+ --link: color-mix(in srgb, var(--site-accent) 80%, #fff);
379
+ --copy-bg: #374151;
380
+ --copy-hover: #4b5563;
381
+ --input-bg: #111111;
382
+ --input-border: #4b5563;
383
+ --input-focus: var(--site-accent);
384
+ --shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
385
+ }
386
+
387
+ /* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
388
+ Sepia theme
389
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
390
+ .drinkfyi-widget[data-theme="sepia"] {
391
+ --bg: #f5f0e8;
392
+ --text: #3d3529;
393
+ --border: #d4c5a9;
394
+ --accent: var(--site-accent);
395
+ --muted: #8b7d6b;
396
+ --surface: #ede8df;
397
+ --badge-bg: #e8e0d0;
398
+ --badge-text: #5c4f3d;
399
+ --link: color-mix(in srgb, var(--site-accent) 70%, #3d3529);
400
+ --copy-bg: #e8e0d0;
401
+ --copy-hover: #ddd4c0;
402
+ --input-bg: #f5f0e8;
403
+ --input-border: #c4b49a;
404
+ --input-focus: var(--site-accent);
405
+ --shadow: 0 1px 3px rgba(61, 53, 41, 0.12);
406
+ }
407
+
408
+ .drinkfyi-widget *, .drinkfyi-widget *::before, .drinkfyi-widget *::after {
409
+ box-sizing: border-box;
410
+ }
411
+
412
+ /* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
413
+ Loading state
414
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
415
+ .drinkfyi-loading {
416
+ padding: 20px 16px;
417
+ text-align: center;
418
+ color: var(--muted);
419
+ font-size: 13px;
420
+ display: flex;
421
+ align-items: center;
422
+ justify-content: center;
423
+ gap: 8px;
424
+ }
425
+
426
+ .drinkfyi-spinner {
427
+ width: 16px;
428
+ height: 16px;
429
+ border: 2px solid var(--border);
430
+ border-top-color: var(--accent);
431
+ border-radius: 50%;
432
+ animation: drinkfyi-spin 0.7s linear infinite;
433
+ display: inline-block;
434
+ flex-shrink: 0;
435
+ }
436
+
437
+ @keyframes drinkfyi-spin {
438
+ to { transform: rotate(360deg); }
439
+ }
440
+
441
+ /* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
442
+ Error state
443
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
444
+ .drinkfyi-error {
445
+ padding: 16px;
446
+ color: var(--muted);
447
+ font-size: 13px;
448
+ text-align: center;
449
+ }
450
+
451
+ .drinkfyi-error p {
452
+ margin: 0 0 8px 0;
453
+ }
454
+
455
+ .drinkfyi-error a {
456
+ color: var(--link);
457
+ text-decoration: none;
458
+ }
459
+
460
+ .drinkfyi-error a:hover {
461
+ text-decoration: underline;
462
+ }
463
+
464
+ /* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
465
+ Badge (generic)
466
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
467
+ .drinkfyi-badge {
468
+ display: inline-block;
469
+ font-size: 10px;
470
+ font-weight: 600;
471
+ padding: 2px 7px;
472
+ border-radius: 4px;
473
+ background: var(--badge-bg);
474
+ color: var(--badge-text);
475
+ text-transform: uppercase;
476
+ letter-spacing: 0.04em;
477
+ }
478
+
479
+ /* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
480
+ Search inputs
481
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
482
+ .drinkfyi-search-wrap {
483
+ padding: 12px 16px;
484
+ }
485
+
486
+ .drinkfyi-search-form {
487
+ display: flex;
488
+ gap: 8px;
489
+ }
490
+
491
+ .drinkfyi-search-input {
492
+ flex: 1;
493
+ padding: 8px 12px;
494
+ border: 1px solid var(--input-border);
495
+ border-radius: 6px;
496
+ background: var(--input-bg);
497
+ color: var(--text);
498
+ font-size: 13px;
499
+ font-family: inherit;
500
+ outline: none;
501
+ transition: border-color 0.15s;
502
+ }
503
+
504
+ .drinkfyi-search-input:focus {
505
+ border-color: var(--input-focus);
506
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--input-focus) 20%, transparent);
507
+ }
508
+
509
+ .drinkfyi-search-input::placeholder {
510
+ color: var(--muted);
511
+ }
512
+
513
+ .drinkfyi-search-btn {
514
+ background: var(--accent);
515
+ color: #fff;
516
+ border: none;
517
+ border-radius: 6px;
518
+ padding: 8px 14px;
519
+ font-size: 13px;
520
+ font-weight: 500;
521
+ cursor: pointer;
522
+ font-family: inherit;
523
+ transition: opacity 0.15s;
524
+ white-space: nowrap;
525
+ }
526
+
527
+ .drinkfyi-search-btn:hover {
528
+ opacity: 0.9;
529
+ }
530
+
531
+ /* Search results list */
532
+ .drinkfyi-search-results {
533
+ padding: 0 16px 12px;
534
+ }
535
+
536
+ .drinkfyi-result-item {
537
+ padding: 8px 0;
538
+ border-bottom: 1px solid var(--border);
539
+ }
540
+
541
+ .drinkfyi-result-item:last-child {
542
+ border-bottom: none;
543
+ }
544
+
545
+ .drinkfyi-result-title {
546
+ font-size: 13px;
547
+ font-weight: 600;
548
+ color: var(--text);
549
+ margin: 0 0 3px 0;
550
+ }
551
+
552
+ .drinkfyi-result-meta {
553
+ font-size: 11px;
554
+ color: var(--muted);
555
+ display: flex;
556
+ align-items: center;
557
+ gap: 6px;
558
+ flex-wrap: wrap;
559
+ }
560
+
561
+ /* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
562
+ Powered by footer
563
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
564
+ .drinkfyi-powered {
565
+ display: block;
566
+ text-align: center;
567
+ padding: 8px 16px;
568
+ font-size: 11px;
569
+ color: var(--muted);
570
+ border-top: 1px solid var(--border);
571
+ }
572
+
573
+ .drinkfyi-powered a {
574
+ color: var(--link);
575
+ text-decoration: none;
576
+ font-weight: 500;
577
+ }
578
+
579
+ .drinkfyi-powered a:hover {
580
+ text-decoration: underline;
581
+ }
582
+
583
+ /* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
584
+ Copy button
585
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
586
+ .drinkfyi-copy-btn {
587
+ background: var(--copy-bg);
588
+ color: var(--text);
589
+ border: none;
590
+ border-radius: 5px;
591
+ padding: 4px 9px;
592
+ font-size: 11px;
593
+ cursor: pointer;
594
+ display: inline-flex;
595
+ align-items: center;
596
+ gap: 4px;
597
+ transition: background 0.15s;
598
+ font-family: inherit;
599
+ }
600
+
601
+ .drinkfyi-copy-btn:hover {
602
+ background: var(--copy-hover);
603
+ }
604
+
605
+ .drinkfyi-copy-btn svg {
606
+ width: 11px;
607
+ height: 11px;
608
+ }
609
+
610
+ ${getStyleCSS(style)}
611
+ `;
612
+ }
613
+
614
+ // src/shadow.ts
615
+ function createShadow(el, config) {
616
+ const widgetStyle = el.dataset.style || "modern";
617
+ const shadow = el.attachShadow({ mode: "open" });
618
+ const style = document.createElement("style");
619
+ style.textContent = getThemeCSS(config.accent, widgetStyle);
620
+ shadow.appendChild(style);
621
+ return shadow;
622
+ }
623
+ function resolveTheme(el) {
624
+ const raw = el.dataset.theme || "light";
625
+ if (raw === "auto") {
626
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
627
+ }
628
+ return raw;
629
+ }
630
+ function createWidgetRoot(shadow, el, extraClass) {
631
+ const theme = resolveTheme(el);
632
+ const size = el.dataset.size || "default";
633
+ const div = document.createElement("div");
634
+ div.className = ["drinkfyi-widget", extraClass].filter(Boolean).join(" ");
635
+ div.setAttribute("data-theme", theme);
636
+ div.setAttribute("data-size", size);
637
+ shadow.appendChild(div);
638
+ if (el.dataset.theme === "auto") {
639
+ window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
640
+ div.setAttribute("data-theme", e.matches ? "dark" : "light");
641
+ });
642
+ }
643
+ return div;
644
+ }
645
+ function renderLoading(container) {
646
+ container.innerHTML = `
647
+ <div class="drinkfyi-loading">
648
+ <span class="drinkfyi-spinner"></span>
649
+ Loading\u2026
650
+ </div>
651
+ `;
652
+ }
653
+ function renderError(container, message, config) {
654
+ container.innerHTML = `
655
+ <div class="drinkfyi-error">
656
+ <p>${message}</p>
657
+ <a href="https://${config.domain}" target="_blank" rel="noopener">
658
+ Visit ${config.name}
659
+ </a>
660
+ </div>
661
+ `;
662
+ }
663
+ var externalLinkIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>`;
664
+ function poweredByHTML(config) {
665
+ return `<span class="drinkfyi-powered">Powered by <a href="https://${config.domain}" target="_blank" rel="noopener">${config.name}</a></span>`;
666
+ }
667
+
668
+ // src/api.ts
669
+ var CACHE_TTL_MS = 5 * 60 * 1e3;
670
+ function cacheKey(url) {
671
+ return `drinkfyi_embed_${url}`;
672
+ }
673
+ function getFromCache(url) {
674
+ try {
675
+ const raw = sessionStorage.getItem(cacheKey(url));
676
+ if (!raw) return null;
677
+ const entry = JSON.parse(raw);
678
+ if (Date.now() - entry.ts > CACHE_TTL_MS) {
679
+ sessionStorage.removeItem(cacheKey(url));
680
+ return null;
681
+ }
682
+ return entry.data;
683
+ } catch (e) {
684
+ return null;
685
+ }
686
+ }
687
+ function setInCache(url, data) {
688
+ try {
689
+ const entry = { data, ts: Date.now() };
690
+ sessionStorage.setItem(cacheKey(url), JSON.stringify(entry));
691
+ } catch (e) {
692
+ }
693
+ }
694
+ async function fetchAPI(baseUrl, path, params) {
695
+ const base = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
696
+ const relativePath = path.startsWith("/") ? path.slice(1) : path;
697
+ const url = new URL(relativePath, base);
698
+ if (params) {
699
+ Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
700
+ }
701
+ const urlStr = url.toString();
702
+ const cached = getFromCache(urlStr);
703
+ if (cached !== null) return cached;
704
+ const response = await fetch(urlStr, {
705
+ headers: { Accept: "application/json" }
706
+ });
707
+ if (!response.ok) {
708
+ throw new Error(`API error ${response.status}: ${urlStr}`);
709
+ }
710
+ const data = await response.json();
711
+ setInCache(urlStr, data);
712
+ return data;
713
+ }
714
+
715
+ // src/rich-snippets.ts
716
+ function injectRecipe(data, domain) {
717
+ if (document.querySelector('script[data-drinkfyi-snippet="recipe"]')) return;
718
+ const jsonLd = {
719
+ "@context": "https://schema.org",
720
+ "@type": "Recipe",
721
+ name: data.name,
722
+ url: `https://${domain}/${data.slug}/`
723
+ };
724
+ if (data.category) {
725
+ jsonLd.recipeCategory = data.category;
726
+ }
727
+ if (data.ingredients && data.ingredients.length > 0) {
728
+ jsonLd.recipeIngredient = data.ingredients;
729
+ }
730
+ if (data.prep_time_minutes) {
731
+ jsonLd.totalTime = `PT${data.prep_time_minutes}M`;
732
+ }
733
+ const script = document.createElement("script");
734
+ script.type = "application/ld+json";
735
+ script.setAttribute("data-drinkfyi-snippet", "recipe");
736
+ script.textContent = JSON.stringify(jsonLd);
737
+ document.head.appendChild(script);
738
+ }
739
+ function injectDefinedTerm(data, domain, siteName) {
740
+ if (document.querySelector('script[data-drinkfyi-snippet="term"]')) return;
741
+ const jsonLd = {
742
+ "@context": "https://schema.org",
743
+ "@type": "DefinedTerm",
744
+ name: data.name,
745
+ description: data.definition,
746
+ inDefinedTermSet: {
747
+ "@type": "DefinedTermSet",
748
+ name: `${siteName} Glossary`,
749
+ url: `https://${domain}/glossary/`
750
+ }
751
+ };
752
+ const script = document.createElement("script");
753
+ script.type = "application/ld+json";
754
+ script.setAttribute("data-drinkfyi-snippet", "term");
755
+ script.textContent = JSON.stringify(jsonLd);
756
+ document.head.appendChild(script);
757
+ }
758
+ function injectFAQPage(data, domain, siteName) {
759
+ if (document.querySelector('script[data-drinkfyi-snippet="faq"]')) return;
760
+ const jsonLd = {
761
+ "@context": "https://schema.org",
762
+ "@type": "FAQPage",
763
+ name: `${siteName} FAQ`,
764
+ url: `https://${domain}/faqs/`,
765
+ mainEntity: [
766
+ {
767
+ "@type": "Question",
768
+ name: data.question,
769
+ acceptedAnswer: {
770
+ "@type": "Answer",
771
+ text: data.answer
772
+ }
773
+ }
774
+ ]
775
+ };
776
+ const script = document.createElement("script");
777
+ script.type = "application/ld+json";
778
+ script.setAttribute("data-drinkfyi-snippet", "faq");
779
+ script.textContent = JSON.stringify(jsonLd);
780
+ document.head.appendChild(script);
781
+ }
782
+
783
+ // src/widgets/recipe.ts
784
+ function escapeHTML(str) {
785
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
786
+ }
787
+ var FIELD_MAP = {
788
+ cocktailfyi: [
789
+ { label: "Category", key: "category" },
790
+ { label: "Glass", key: "glass" },
791
+ { label: "Difficulty", key: "difficulty" },
792
+ { label: "Prep Time", key: "prep_time_minutes" }
793
+ ],
794
+ vinofyi: [
795
+ { label: "Style", key: "style" },
796
+ { label: "Region", key: "region" },
797
+ { label: "Country", key: "country" }
798
+ ],
799
+ beerfyi: [
800
+ { label: "BJCP", key: "bjcp_code" },
801
+ { label: "Category", key: "category_name" },
802
+ { label: "ABV", key: "abv_range" },
803
+ { label: "IBU", key: "ibu_range" },
804
+ { label: "SRM", key: "srm_range" }
805
+ ],
806
+ brewfyi: [
807
+ { label: "Species", key: "species" },
808
+ { label: "Country", key: "country" },
809
+ { label: "Region", key: "region" }
810
+ ],
811
+ whiskeyfyi: [
812
+ { label: "Type", key: "type" },
813
+ { label: "Distillery", key: "distillery" },
814
+ { label: "Region", key: "region" },
815
+ { label: "Country", key: "country" }
816
+ ],
817
+ teafyi: [
818
+ { label: "Category", key: "category" },
819
+ { label: "Country", key: "country" },
820
+ { label: "Region", key: "region" }
821
+ ],
822
+ nihonshufyi: [
823
+ { label: "Grade", key: "grade" },
824
+ { label: "Rice", key: "rice" },
825
+ { label: "Brewery", key: "brewery" }
826
+ ]
827
+ };
828
+ var DEFAULT_FIELDS = [
829
+ { label: "Category", key: "category" },
830
+ { label: "Country", key: "country" }
831
+ ];
832
+ function initRecipeWidget(el, config) {
833
+ var _a;
834
+ const dataset = el.dataset;
835
+ const slug = (_a = dataset.slug) != null ? _a : "";
836
+ if (!slug) {
837
+ const shadow2 = createShadow(el, config);
838
+ const container2 = createWidgetRoot(shadow2, el);
839
+ renderError(container2, "Missing data-slug attribute.", config);
840
+ return;
841
+ }
842
+ const shadow = createShadow(el, config);
843
+ const container = createWidgetRoot(shadow, el);
844
+ renderLoading(container);
845
+ const apiPath = `${config.entitySlug}/${slug}/`;
846
+ fetchAPI(config.apiBase, apiPath).then((data) => {
847
+ renderRecipe(container, data, slug, config, el);
848
+ }).catch(() => {
849
+ renderError(
850
+ container,
851
+ `Unable to load "${escapeHTML(slug)}". Please try again later.`,
852
+ config
853
+ );
854
+ });
855
+ }
856
+ function renderRecipe(container, data, slug, config, el) {
857
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
858
+ const name = data.name ? escapeHTML(String(data.name)) : escapeHTML(slug);
859
+ const badgeRaw = (_e = (_d = (_c = (_b = (_a = data.category) != null ? _a : data.style) != null ? _b : data.type) != null ? _c : data.grade) != null ? _d : data.category_name) != null ? _e : "";
860
+ const badge = badgeRaw ? escapeHTML(String(badgeRaw)) : "";
861
+ const description = (_g = (_f = data.description) != null ? _f : data.excerpt) != null ? _g : "";
862
+ const entityUrl = `https://${config.domain}/${config.entitySlug}/${slug}/`;
863
+ const fields = (_h = FIELD_MAP[config.site]) != null ? _h : DEFAULT_FIELDS;
864
+ const rows = fields.filter(({ key }) => {
865
+ const val = data[key];
866
+ return val !== null && val !== void 0 && val !== "";
867
+ }).map(({ label, key }) => {
868
+ const raw = data[key];
869
+ let displayVal;
870
+ if (Array.isArray(raw)) {
871
+ displayVal = escapeHTML(raw.join(", "));
872
+ } else {
873
+ displayVal = escapeHTML(String(raw));
874
+ }
875
+ const suffix = key === "prep_time_minutes" ? " min" : "";
876
+ return `
877
+ <div class="drinkfyi-kv-row">
878
+ <div class="drinkfyi-kv-key">${escapeHTML(label)}</div>
879
+ <div class="drinkfyi-kv-value">${displayVal}${suffix}</div>
880
+ </div>`;
881
+ }).join("");
882
+ container.innerHTML = `
883
+ <div class="drinkfyi-header">
884
+ <div class="drinkfyi-header-text">
885
+ <p class="drinkfyi-title">${name}</p>
886
+ ${badge ? `<p class="drinkfyi-subtitle">${badge}</p>` : ""}
887
+ </div>
888
+ ${badge ? `<span class="drinkfyi-badge">${badge}</span>` : ""}
889
+ </div>
890
+
891
+ ${rows ? `<div class="drinkfyi-kv-table">${rows}</div>` : ""}
892
+
893
+ ${description ? `<p class="drinkfyi-description">${escapeHTML(description)}</p>` : ""}
894
+
895
+ <div class="drinkfyi-actions">
896
+ <a class="drinkfyi-link" href="${entityUrl}" target="_blank" rel="noopener">
897
+ View on ${escapeHTML(config.name)} ${externalLinkIcon}
898
+ </a>
899
+ </div>
900
+
901
+ ${poweredByHTML(config)}
902
+ `;
903
+ if (config.site === "cocktailfyi" && el.dataset.noSnippet !== "true") {
904
+ const ingredients = Array.isArray(data.ingredients) ? data.ingredients.map((i) => String(i)) : [];
905
+ injectRecipe(
906
+ {
907
+ name: String((_i = data.name) != null ? _i : slug),
908
+ category: data.category ? String(data.category) : void 0,
909
+ ingredients,
910
+ prep_time_minutes: data.prep_time_minutes ? Number(data.prep_time_minutes) : void 0,
911
+ slug
912
+ },
913
+ config.domain
914
+ );
915
+ }
916
+ }
917
+
918
+ // src/widgets/compare.ts
919
+ function escapeHTML2(str) {
920
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
921
+ }
922
+ var COMPARE_FIELDS = {
923
+ cocktailfyi: [
924
+ ["Category", "category"],
925
+ ["Glass", "glass"],
926
+ ["Difficulty", "difficulty"],
927
+ ["Prep Time", "prep_time_minutes"],
928
+ ["ABV", "abv"]
929
+ ],
930
+ vinofyi: [
931
+ ["Style", "style"],
932
+ ["Region", "region"],
933
+ ["Country", "country"],
934
+ ["Grape", "grape"],
935
+ ["Vintage", "vintage"]
936
+ ],
937
+ beerfyi: [
938
+ ["BJCP", "bjcp_code"],
939
+ ["Category", "category_name"],
940
+ ["ABV", "abv_range"],
941
+ ["IBU", "ibu_range"],
942
+ ["SRM", "srm_range"]
943
+ ],
944
+ brewfyi: [
945
+ ["Species", "species"],
946
+ ["Country", "country"],
947
+ ["Region", "region"],
948
+ ["Process", "process"]
949
+ ],
950
+ whiskeyfyi: [
951
+ ["Type", "type"],
952
+ ["Distillery", "distillery"],
953
+ ["Region", "region"],
954
+ ["Country", "country"],
955
+ ["Age", "age_statement"]
956
+ ],
957
+ teafyi: [
958
+ ["Category", "category"],
959
+ ["Country", "country"],
960
+ ["Region", "region"],
961
+ ["Oxidation", "oxidation_level"]
962
+ ],
963
+ nihonshufyi: [
964
+ ["Grade", "grade"],
965
+ ["Rice", "rice"],
966
+ ["Brewery", "brewery"],
967
+ ["Prefecture", "prefecture"],
968
+ ["SMV", "nihonshu_do"]
969
+ ]
970
+ };
971
+ var DEFAULT_COMPARE_FIELDS = [
972
+ ["Category", "category"],
973
+ ["Country", "country"],
974
+ ["Region", "region"]
975
+ ];
976
+ function formatValue(val) {
977
+ if (val === null || val === void 0 || val === "") return "\u2014";
978
+ if (typeof val === "boolean") return val ? "\u2713" : "\u2717";
979
+ if (Array.isArray(val)) return val.join(", ");
980
+ return String(val);
981
+ }
982
+ function initCompareWidget(el, config) {
983
+ var _a;
984
+ const dataset = el.dataset;
985
+ const slugsRaw = (_a = dataset.slugs) != null ? _a : "";
986
+ const slugs = slugsRaw.split(",").map((s) => s.trim()).filter(Boolean);
987
+ if (slugs.length < 2) {
988
+ const shadow2 = createShadow(el, config);
989
+ const container2 = createWidgetRoot(shadow2, el, "drinkfyi-compare-widget");
990
+ renderError(container2, 'Provide at least 2 slugs in data-slugs="a,b"', config);
991
+ return;
992
+ }
993
+ const shadow = createShadow(el, config);
994
+ const container = createWidgetRoot(shadow, el, "drinkfyi-compare-widget");
995
+ renderLoading(container);
996
+ Promise.all(
997
+ slugs.map(
998
+ (slug) => fetchAPI(config.apiBase, `${config.entitySlug}/${slug}/`)
999
+ )
1000
+ ).then((entities) => {
1001
+ var _a2;
1002
+ const fields = (_a2 = COMPARE_FIELDS[config.site]) != null ? _a2 : DEFAULT_COMPARE_FIELDS;
1003
+ const presentFields = fields.filter(
1004
+ ([, key]) => entities.some(
1005
+ (e) => e[key] !== null && e[key] !== void 0 && e[key] !== ""
1006
+ )
1007
+ );
1008
+ const colWidth = Math.floor(100 / (entities.length + 1));
1009
+ const html = `
1010
+ <div class="drinkfyi-header">
1011
+ <div class="drinkfyi-header-text">
1012
+ <p class="drinkfyi-title">Comparison</p>
1013
+ <p class="drinkfyi-subtitle">${entities.length} items \xB7 ${escapeHTML2(config.name)}</p>
1014
+ </div>
1015
+ </div>
1016
+ <div class="drinkfyi-compare-scroll">
1017
+ <table class="drinkfyi-compare-table">
1018
+ <thead>
1019
+ <tr>
1020
+ <th class="drinkfyi-compare-th drinkfyi-compare-th--label" style="width:${colWidth}%">Field</th>
1021
+ ${entities.map(
1022
+ (e) => `
1023
+ <th class="drinkfyi-compare-th" style="width:${colWidth}%">
1024
+ <a href="https://${config.domain}/${config.entitySlug}/${escapeHTML2(e.slug)}/" target="_blank" rel="noopener" class="drinkfyi-compare-entity-link">
1025
+ ${escapeHTML2(String(e.name))}
1026
+ ${externalLinkIcon}
1027
+ </a>
1028
+ </th>`
1029
+ ).join("")}
1030
+ </tr>
1031
+ </thead>
1032
+ <tbody>
1033
+ ${presentFields.map(
1034
+ ([label, key]) => `
1035
+ <tr class="drinkfyi-compare-row">
1036
+ <td class="drinkfyi-kv-key">${escapeHTML2(label)}</td>
1037
+ ${entities.map((e) => {
1038
+ const val = escapeHTML2(formatValue(e[key]));
1039
+ return `<td class="drinkfyi-compare-td">${val}</td>`;
1040
+ }).join("")}
1041
+ </tr>`
1042
+ ).join("")}
1043
+ </tbody>
1044
+ </table>
1045
+ </div>
1046
+ ${poweredByHTML(config)}
1047
+ `;
1048
+ container.innerHTML = html;
1049
+ }).catch(() => {
1050
+ renderError(container, "Failed to load comparison data.", config);
1051
+ });
1052
+ }
1053
+
1054
+ // src/widgets/glossary.ts
1055
+ function escapeHTML3(str) {
1056
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1057
+ }
1058
+ function initGlossaryWidget(el, config) {
1059
+ var _a;
1060
+ const dataset = el.dataset;
1061
+ const slug = (_a = dataset.slug) != null ? _a : "";
1062
+ if (!slug) {
1063
+ const shadow2 = createShadow(el, config);
1064
+ const container2 = createWidgetRoot(shadow2, el);
1065
+ renderError(container2, "Missing data-slug attribute.", config);
1066
+ return;
1067
+ }
1068
+ const shadow = createShadow(el, config);
1069
+ const container = createWidgetRoot(shadow, el);
1070
+ renderLoading(container);
1071
+ fetchAPI(config.apiBase, `glossary/${slug}/`).then((data) => {
1072
+ renderGlossary(container, data, config);
1073
+ if (el.dataset.noSnippet !== "true") {
1074
+ injectDefinedTerm(
1075
+ { name: data.name, definition: data.definition },
1076
+ config.domain,
1077
+ config.name
1078
+ );
1079
+ }
1080
+ }).catch(() => {
1081
+ renderError(
1082
+ container,
1083
+ `Unable to load glossary term "${escapeHTML3(slug)}". Please try again later.`,
1084
+ config
1085
+ );
1086
+ });
1087
+ }
1088
+ function renderGlossary(container, data, config) {
1089
+ const termUrl = `https://${config.domain}/glossary/${data.slug}/`;
1090
+ const glossaryUrl = `https://${config.domain}/glossary/`;
1091
+ const category = data.category ? escapeHTML3(data.category) : "";
1092
+ const relatedPills = data.related_terms && data.related_terms.length > 0 ? data.related_terms.map(
1093
+ (rt) => `<a class="drinkfyi-pill" href="https://${config.domain}/glossary/${escapeHTML3(rt.slug)}/" target="_blank" rel="noopener">${escapeHTML3(rt.name)}</a>`
1094
+ ).join("") : "";
1095
+ container.innerHTML = `
1096
+ <div class="drinkfyi-header">
1097
+ <div class="drinkfyi-header-text">
1098
+ <p class="drinkfyi-title">${escapeHTML3(data.name)}</p>
1099
+ <p class="drinkfyi-subtitle">Glossary term${category ? ` \xB7 ${category}` : ""}</p>
1100
+ </div>
1101
+ ${category ? `<span class="drinkfyi-badge">${category}</span>` : ""}
1102
+ </div>
1103
+
1104
+ <div class="drinkfyi-summary">
1105
+ ${escapeHTML3(data.definition)}
1106
+ </div>
1107
+
1108
+ ${relatedPills ? `<div class="drinkfyi-pills-row">${relatedPills}</div>` : ""}
1109
+
1110
+ <div class="drinkfyi-actions">
1111
+ <a class="drinkfyi-link" href="${termUrl}" target="_blank" rel="noopener">
1112
+ ${escapeHTML3(data.name)} ${externalLinkIcon}
1113
+ </a>
1114
+ <a class="drinkfyi-link" href="${glossaryUrl}" target="_blank" rel="noopener">
1115
+ Full glossary on ${escapeHTML3(config.name)} ${externalLinkIcon}
1116
+ </a>
1117
+ </div>
1118
+
1119
+ ${poweredByHTML(config)}
1120
+ `;
1121
+ }
1122
+
1123
+ // src/widgets/faq.ts
1124
+ function escapeHTML4(str) {
1125
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1126
+ }
1127
+ function initFaqWidget(el, config) {
1128
+ var _a;
1129
+ const dataset = el.dataset;
1130
+ const slug = (_a = dataset.slug) != null ? _a : "";
1131
+ if (!slug) {
1132
+ const shadow2 = createShadow(el, config);
1133
+ const container2 = createWidgetRoot(shadow2, el);
1134
+ renderError(container2, "Missing data-slug attribute.", config);
1135
+ return;
1136
+ }
1137
+ const shadow = createShadow(el, config);
1138
+ const container = createWidgetRoot(shadow, el);
1139
+ renderLoading(container);
1140
+ fetchAPI(config.apiBase, `faqs/${slug}/`).then((data) => {
1141
+ renderFaq(container, data, config);
1142
+ if (el.dataset.noSnippet !== "true") {
1143
+ injectFAQPage(
1144
+ { question: data.question, answer: data.answer },
1145
+ config.domain,
1146
+ config.name
1147
+ );
1148
+ }
1149
+ }).catch(() => {
1150
+ renderError(
1151
+ container,
1152
+ `Unable to load FAQ "${escapeHTML4(slug)}". Please try again later.`,
1153
+ config
1154
+ );
1155
+ });
1156
+ }
1157
+ function renderFaq(container, data, config) {
1158
+ const faqUrl = `https://${config.domain}/faqs/${data.slug}/`;
1159
+ const faqsUrl = `https://${config.domain}/faqs/`;
1160
+ const category = data.category ? escapeHTML4(data.category) : "";
1161
+ container.innerHTML = `
1162
+ <div class="drinkfyi-header">
1163
+ <div class="drinkfyi-header-text">
1164
+ <p class="drinkfyi-title">${escapeHTML4(data.question)}</p>
1165
+ <p class="drinkfyi-subtitle">Frequently Asked Question${category ? ` \xB7 ${category}` : ""}</p>
1166
+ </div>
1167
+ </div>
1168
+
1169
+ <details class="drinkfyi-faq-details" open>
1170
+ <summary class="drinkfyi-faq-summary">Answer</summary>
1171
+ <div class="drinkfyi-summary">
1172
+ ${escapeHTML4(data.answer)}
1173
+ </div>
1174
+ </details>
1175
+
1176
+ <div class="drinkfyi-actions">
1177
+ <a class="drinkfyi-link" href="${faqUrl}" target="_blank" rel="noopener">
1178
+ Full answer ${externalLinkIcon}
1179
+ </a>
1180
+ <a class="drinkfyi-link" href="${faqsUrl}" target="_blank" rel="noopener">
1181
+ More FAQs on ${escapeHTML4(config.name)} ${externalLinkIcon}
1182
+ </a>
1183
+ </div>
1184
+
1185
+ ${poweredByHTML(config)}
1186
+ `;
1187
+ }
1188
+
1189
+ // src/widgets/guide.ts
1190
+ function escapeHTML5(str) {
1191
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1192
+ }
1193
+ function initGuideWidget(el, config) {
1194
+ var _a;
1195
+ const dataset = el.dataset;
1196
+ const slug = (_a = dataset.slug) != null ? _a : "";
1197
+ if (!slug) {
1198
+ const shadow2 = createShadow(el, config);
1199
+ const container2 = createWidgetRoot(shadow2, el, "drinkfyi-guide-widget");
1200
+ renderError(container2, "Missing data-slug attribute.", config);
1201
+ return;
1202
+ }
1203
+ const shadow = createShadow(el, config);
1204
+ const container = createWidgetRoot(shadow, el, "drinkfyi-guide-widget");
1205
+ renderLoading(container);
1206
+ fetchAPI(config.apiBase, `guides/${slug}/`).then((guide) => {
1207
+ var _a2, _b, _c, _d, _e, _f;
1208
+ const guideUrl = guide.url ? `https://${config.domain}${guide.url}` : `https://${config.domain}/guides/${escapeHTML5(guide.slug)}/`;
1209
+ const excerpt = (_b = (_a2 = guide.excerpt) != null ? _a2 : guide.description) != null ? _b : "";
1210
+ const readingTime = (_d = (_c = guide.reading_time_minutes) != null ? _c : guide.reading_time) != null ? _d : null;
1211
+ const category = (_f = (_e = guide.category_name) != null ? _e : guide.category) != null ? _f : null;
1212
+ const html = `
1213
+ <div class="drinkfyi-guide-card">
1214
+ <div class="drinkfyi-guide-header">
1215
+ <div class="drinkfyi-guide-badges">
1216
+ ${category ? `<span class="drinkfyi-badge">${escapeHTML5(category)}</span>` : ""}
1217
+ ${readingTime ? `<span class="drinkfyi-badge drinkfyi-badge--time">${escapeHTML5(String(readingTime))} min read</span>` : ""}
1218
+ </div>
1219
+ <h3 class="drinkfyi-guide-title">${escapeHTML5(String(guide.title))}</h3>
1220
+ </div>
1221
+ ${excerpt ? `<p class="drinkfyi-guide-excerpt">${escapeHTML5(excerpt)}</p>` : ""}
1222
+ <div class="drinkfyi-guide-footer">
1223
+ <a href="${guideUrl}" target="_blank" rel="noopener" class="drinkfyi-guide-cta">
1224
+ Read guide on ${escapeHTML5(config.name)}
1225
+ ${externalLinkIcon}
1226
+ </a>
1227
+ </div>
1228
+ </div>
1229
+ ${poweredByHTML(config)}
1230
+ `;
1231
+ container.innerHTML = html;
1232
+ }).catch(() => {
1233
+ renderError(container, `Guide "${escapeHTML5(slug)}" not found.`, config);
1234
+ });
1235
+ }
1236
+
1237
+ // src/widgets/ingredient.ts
1238
+ function escapeHTML6(str) {
1239
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1240
+ }
1241
+ var INGREDIENT_SLUG = {
1242
+ cocktailfyi: "ingredients",
1243
+ vinofyi: "grapes",
1244
+ beerfyi: "hops",
1245
+ brewfyi: "varieties",
1246
+ whiskeyfyi: "casks",
1247
+ teafyi: "compounds",
1248
+ nihonshufyi: "rice"
1249
+ };
1250
+ function initIngredientWidget(el, config) {
1251
+ var _a, _b;
1252
+ const dataset = el.dataset;
1253
+ const slug = (_a = dataset.slug) != null ? _a : "";
1254
+ if (!slug) {
1255
+ const shadow2 = createShadow(el, config);
1256
+ const container2 = createWidgetRoot(shadow2, el);
1257
+ renderError(container2, "Missing data-slug attribute.", config);
1258
+ return;
1259
+ }
1260
+ const endpoint = (_b = INGREDIENT_SLUG[config.site]) != null ? _b : "ingredients";
1261
+ const shadow = createShadow(el, config);
1262
+ const container = createWidgetRoot(shadow, el);
1263
+ renderLoading(container);
1264
+ fetchAPI(config.apiBase, `${endpoint}/${slug}/`).then((data) => {
1265
+ renderIngredient(container, data, endpoint, config);
1266
+ }).catch(() => {
1267
+ renderError(
1268
+ container,
1269
+ `Unable to load "${escapeHTML6(slug)}". Please try again later.`,
1270
+ config
1271
+ );
1272
+ });
1273
+ }
1274
+ function renderIngredient(container, data, endpoint, config) {
1275
+ var _a, _b, _c, _d, _e;
1276
+ const name = escapeHTML6(data.name);
1277
+ const badgeRaw = (_b = (_a = data.category) != null ? _a : data.type) != null ? _b : "";
1278
+ const badge = badgeRaw ? escapeHTML6(String(badgeRaw)) : "";
1279
+ const description = (_c = data.description) != null ? _c : "";
1280
+ const origin = (_e = (_d = data.origin) != null ? _d : data.country) != null ? _e : "";
1281
+ const entityUrl = `https://${config.domain}/${endpoint}/${data.slug}/`;
1282
+ container.innerHTML = `
1283
+ <div class="drinkfyi-header">
1284
+ <div class="drinkfyi-header-text">
1285
+ <p class="drinkfyi-title">${name}</p>
1286
+ ${badge ? `<p class="drinkfyi-subtitle">${badge}</p>` : ""}
1287
+ </div>
1288
+ ${badge ? `<span class="drinkfyi-badge">${badge}</span>` : ""}
1289
+ </div>
1290
+
1291
+ ${origin ? `<div class="drinkfyi-kv-table">
1292
+ <div class="drinkfyi-kv-row">
1293
+ <div class="drinkfyi-kv-key">Origin</div>
1294
+ <div class="drinkfyi-kv-value">${escapeHTML6(String(origin))}</div>
1295
+ </div>
1296
+ </div>` : ""}
1297
+
1298
+ ${description ? `<p class="drinkfyi-description">${escapeHTML6(description)}</p>` : ""}
1299
+
1300
+ <div class="drinkfyi-actions">
1301
+ <a class="drinkfyi-link" href="${entityUrl}" target="_blank" rel="noopener">
1302
+ View on ${escapeHTML6(config.name)} ${externalLinkIcon}
1303
+ </a>
1304
+ </div>
1305
+
1306
+ ${poweredByHTML(config)}
1307
+ `;
1308
+ }
1309
+
1310
+ // src/widgets/pairing.ts
1311
+ function escapeHTML7(str) {
1312
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1313
+ }
1314
+ var PAIRING_SITES = /* @__PURE__ */ new Set(["cocktailfyi", "vinofyi"]);
1315
+ function initPairingWidget(el, config) {
1316
+ var _a;
1317
+ const dataset = el.dataset;
1318
+ const food = (_a = dataset.food) != null ? _a : "";
1319
+ const shadow = createShadow(el, config);
1320
+ const container = createWidgetRoot(shadow, el, "drinkfyi-pairing-widget");
1321
+ if (!PAIRING_SITES.has(config.site)) {
1322
+ container.innerHTML = `
1323
+ <div class="drinkfyi-header">
1324
+ <div class="drinkfyi-header-text">
1325
+ <p class="drinkfyi-title">Food Pairings</p>
1326
+ <p class="drinkfyi-subtitle">Beverage pairing guide</p>
1327
+ </div>
1328
+ </div>
1329
+ <div class="drinkfyi-summary">
1330
+ Food pairings are available on
1331
+ <a href="https://cocktailfyi.com/" target="_blank" rel="noopener">CocktailFYI</a>
1332
+ and
1333
+ <a href="https://vinofyi.com/" target="_blank" rel="noopener">VinoFYI</a>.
1334
+ </div>
1335
+ ${poweredByHTML(config)}
1336
+ `;
1337
+ return;
1338
+ }
1339
+ if (!food) {
1340
+ renderError(container, "Missing data-food attribute.", config);
1341
+ return;
1342
+ }
1343
+ renderLoading(container);
1344
+ const apiPath = config.site === "vinofyi" ? `foods/${food}/` : `cocktails/?pairing=${encodeURIComponent(food)}&limit=5`;
1345
+ fetchAPI(config.apiBase, apiPath).then((data) => {
1346
+ renderPairing(container, data, food, config);
1347
+ }).catch(() => {
1348
+ renderError(
1349
+ container,
1350
+ `Unable to load pairing suggestions for "${escapeHTML7(food)}". Please try again later.`,
1351
+ config
1352
+ );
1353
+ });
1354
+ }
1355
+ function renderPairing(container, data, food, config) {
1356
+ var _a, _b;
1357
+ const foodName = escapeHTML7(
1358
+ String((_b = (_a = data.name) != null ? _a : data.food) != null ? _b : food)
1359
+ );
1360
+ const items = Array.isArray(data.pairings) ? data.pairings : Array.isArray(data.results) ? data.results : [];
1361
+ if (items.length === 0) {
1362
+ container.innerHTML = `
1363
+ <div class="drinkfyi-header">
1364
+ <div class="drinkfyi-header-text">
1365
+ <p class="drinkfyi-title">Pairings for ${foodName}</p>
1366
+ <p class="drinkfyi-subtitle">${escapeHTML7(config.name)}</p>
1367
+ </div>
1368
+ </div>
1369
+ <div class="drinkfyi-summary">
1370
+ No pairing suggestions found for "${foodName}".
1371
+ <a href="https://${config.domain}/" target="_blank" rel="noopener">Browse ${escapeHTML7(config.name)}</a>
1372
+ for more options.
1373
+ </div>
1374
+ ${poweredByHTML(config)}
1375
+ `;
1376
+ return;
1377
+ }
1378
+ const itemsHTML = items.map((item) => {
1379
+ var _a2, _b2;
1380
+ const href = item.url ? `https://${config.domain}${item.url}` : `https://${config.domain}/${config.entitySlug}/${escapeHTML7(item.slug)}/`;
1381
+ const note = (_b2 = (_a2 = item.pairing_notes) != null ? _a2 : item.description) != null ? _b2 : "";
1382
+ return `
1383
+ <div class="drinkfyi-pairing-item">
1384
+ <a class="drinkfyi-pairing-name" href="${href}" target="_blank" rel="noopener">
1385
+ ${escapeHTML7(item.name)}
1386
+ ${externalLinkIcon}
1387
+ </a>
1388
+ ${note ? `<p class="drinkfyi-pairing-note">${escapeHTML7(note)}</p>` : ""}
1389
+ </div>`;
1390
+ }).join("");
1391
+ container.innerHTML = `
1392
+ <div class="drinkfyi-header">
1393
+ <div class="drinkfyi-header-text">
1394
+ <p class="drinkfyi-title">Pairings for ${foodName}</p>
1395
+ <p class="drinkfyi-subtitle">${items.length} suggestion${items.length !== 1 ? "s" : ""} \xB7 ${escapeHTML7(config.name)}</p>
1396
+ </div>
1397
+ </div>
1398
+
1399
+ <div class="drinkfyi-pairing-list">
1400
+ ${itemsHTML}
1401
+ </div>
1402
+
1403
+ <div class="drinkfyi-actions">
1404
+ <a class="drinkfyi-link" href="https://${config.domain}/" target="_blank" rel="noopener">
1405
+ Explore ${escapeHTML7(config.name)} ${externalLinkIcon}
1406
+ </a>
1407
+ </div>
1408
+
1409
+ ${poweredByHTML(config)}
1410
+ `;
1411
+ }
1412
+
1413
+ // src/widgets/search.ts
1414
+ function escapeHTML8(str) {
1415
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1416
+ }
1417
+ var TYPE_LABELS = {
1418
+ cocktail: "Cocktail",
1419
+ wine: "Wine",
1420
+ beer: "Beer Style",
1421
+ coffee: "Coffee",
1422
+ whiskey: "Whiskey",
1423
+ tea: "Tea",
1424
+ sake: "Sake",
1425
+ ingredient: "Ingredient",
1426
+ grape: "Grape",
1427
+ hop: "Hop",
1428
+ glossary: "Glossary",
1429
+ guide: "Guide",
1430
+ faq: "FAQ"
1431
+ };
1432
+ var SEARCH_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="14" height="14"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`;
1433
+ function initSearchWidget(el, config) {
1434
+ var _a;
1435
+ const dataset = el.dataset;
1436
+ const placeholder = (_a = dataset.placeholder) != null ? _a : `Search ${config.entityName}\u2026`;
1437
+ const shadow = createShadow(el, config);
1438
+ const container = createWidgetRoot(shadow, el, "drinkfyi-search-widget");
1439
+ let isOpen = false;
1440
+ let query = "";
1441
+ let results = [];
1442
+ let selectedIndex = -1;
1443
+ let debounceTimer = null;
1444
+ container.innerHTML = `
1445
+ <div class="drinkfyi-search-wrap">
1446
+ <div class="drinkfyi-search-form">
1447
+ <span class="drinkfyi-search-icon" aria-hidden="true">${SEARCH_ICON}</span>
1448
+ <input
1449
+ class="drinkfyi-search-input"
1450
+ type="search"
1451
+ autocomplete="off"
1452
+ spellcheck="false"
1453
+ placeholder="${escapeHTML8(placeholder)}"
1454
+ aria-label="Search ${escapeHTML8(config.name)}"
1455
+ aria-autocomplete="list"
1456
+ aria-expanded="false"
1457
+ role="combobox"
1458
+ >
1459
+ </div>
1460
+ <div class="drinkfyi-search-dropdown" role="listbox" hidden></div>
1461
+ </div>
1462
+ ${poweredByHTML(config)}
1463
+ `;
1464
+ const input = container.querySelector(".drinkfyi-search-input");
1465
+ const dropdown = container.querySelector(".drinkfyi-search-dropdown");
1466
+ function getAllItems() {
1467
+ return Array.from(dropdown.querySelectorAll(".drinkfyi-search-result-item"));
1468
+ }
1469
+ function setSelectedIndex(idx) {
1470
+ const items = getAllItems();
1471
+ items.forEach((item, i) => {
1472
+ if (i === idx) {
1473
+ item.classList.add("drinkfyi-search-result-item--active");
1474
+ } else {
1475
+ item.classList.remove("drinkfyi-search-result-item--active");
1476
+ }
1477
+ });
1478
+ selectedIndex = idx;
1479
+ }
1480
+ function openDropdown() {
1481
+ isOpen = true;
1482
+ dropdown.hidden = false;
1483
+ input.setAttribute("aria-expanded", "true");
1484
+ }
1485
+ function closeDropdown() {
1486
+ isOpen = false;
1487
+ dropdown.hidden = true;
1488
+ input.setAttribute("aria-expanded", "false");
1489
+ selectedIndex = -1;
1490
+ }
1491
+ function renderDropdown() {
1492
+ var _a2, _b, _c;
1493
+ if (results.length === 0) {
1494
+ dropdown.innerHTML = `
1495
+ <div class="drinkfyi-search-empty">
1496
+ No results for <strong>${escapeHTML8(query)}</strong>
1497
+ </div>
1498
+ `;
1499
+ return;
1500
+ }
1501
+ let html = "";
1502
+ for (const item of results) {
1503
+ const typeLabel = item.type ? (_a2 = TYPE_LABELS[item.type]) != null ? _a2 : item.type : null;
1504
+ const desc = (_c = (_b = item.description) != null ? _b : item.excerpt) != null ? _c : "";
1505
+ const href = item.url ? `https://${config.domain}${item.url}` : `https://${config.domain}/${config.entitySlug}/${escapeHTML8(item.slug)}/`;
1506
+ html += `
1507
+ <a
1508
+ class="drinkfyi-search-result-item"
1509
+ href="${href}"
1510
+ target="_blank"
1511
+ rel="noopener"
1512
+ role="option"
1513
+ tabindex="-1"
1514
+ >
1515
+ <div class="drinkfyi-search-result-row">
1516
+ <span class="drinkfyi-result-title">${escapeHTML8(item.name)}</span>
1517
+ ${typeLabel ? `<span class="drinkfyi-badge">${escapeHTML8(typeLabel)}</span>` : ""}
1518
+ </div>
1519
+ ${desc ? `<div class="drinkfyi-result-meta">${escapeHTML8(desc)}</div>` : ""}
1520
+ </a>
1521
+ `;
1522
+ }
1523
+ dropdown.innerHTML = html;
1524
+ }
1525
+ async function doSearch(q) {
1526
+ var _a2;
1527
+ if (!q.trim()) {
1528
+ closeDropdown();
1529
+ return;
1530
+ }
1531
+ const searchUrl = `https://${config.domain}/api/v1/search/?q=${encodeURIComponent(q)}`;
1532
+ try {
1533
+ const response = await fetch(searchUrl, {
1534
+ headers: { Accept: "application/json" }
1535
+ });
1536
+ if (!response.ok) throw new Error(`Search failed: ${response.status}`);
1537
+ const data = await response.json();
1538
+ results = (_a2 = data.results) != null ? _a2 : [];
1539
+ } catch (e) {
1540
+ results = [];
1541
+ }
1542
+ renderDropdown();
1543
+ openDropdown();
1544
+ setSelectedIndex(-1);
1545
+ }
1546
+ input.addEventListener("input", () => {
1547
+ query = input.value;
1548
+ if (debounceTimer !== null) {
1549
+ clearTimeout(debounceTimer);
1550
+ }
1551
+ if (!query.trim()) {
1552
+ closeDropdown();
1553
+ return;
1554
+ }
1555
+ debounceTimer = setTimeout(() => {
1556
+ void doSearch(query);
1557
+ }, 300);
1558
+ });
1559
+ input.addEventListener("keydown", (e) => {
1560
+ if (!isOpen) return;
1561
+ const items = getAllItems();
1562
+ const total = items.length;
1563
+ if (e.key === "ArrowDown") {
1564
+ e.preventDefault();
1565
+ setSelectedIndex(selectedIndex < total - 1 ? selectedIndex + 1 : 0);
1566
+ } else if (e.key === "ArrowUp") {
1567
+ e.preventDefault();
1568
+ setSelectedIndex(selectedIndex > 0 ? selectedIndex - 1 : total - 1);
1569
+ } else if (e.key === "Enter") {
1570
+ e.preventDefault();
1571
+ if (selectedIndex >= 0 && items[selectedIndex]) {
1572
+ items[selectedIndex].click();
1573
+ } else {
1574
+ const siteSearchUrl = `https://${config.domain}${config.searchPath}?q=${encodeURIComponent(query)}`;
1575
+ window.open(siteSearchUrl, "_blank", "noopener");
1576
+ }
1577
+ } else if (e.key === "Escape") {
1578
+ closeDropdown();
1579
+ input.blur();
1580
+ }
1581
+ });
1582
+ document.addEventListener("click", (e) => {
1583
+ if (!isOpen) return;
1584
+ if (!el.contains(e.target)) {
1585
+ closeDropdown();
1586
+ }
1587
+ });
1588
+ }
1589
+
1590
+ // src/_entry_vinofyi.ts
1591
+ function initWidget(el, type, config) {
1592
+ const widgetStyle = el.dataset.style || "modern";
1593
+ switch (type) {
1594
+ case "recipe":
1595
+ initRecipeWidget(el, config);
1596
+ break;
1597
+ case "compare":
1598
+ initCompareWidget(el, config);
1599
+ break;
1600
+ case "glossary":
1601
+ initGlossaryWidget(el, config);
1602
+ break;
1603
+ case "faq":
1604
+ initFaqWidget(el, config);
1605
+ break;
1606
+ case "guide":
1607
+ initGuideWidget(el, config);
1608
+ break;
1609
+ case "ingredient":
1610
+ initIngredientWidget(el, config);
1611
+ break;
1612
+ case "pairing":
1613
+ initPairingWidget(el, config);
1614
+ break;
1615
+ case "search":
1616
+ initSearchWidget(el, config);
1617
+ break;
1618
+ default:
1619
+ break;
1620
+ }
1621
+ }
1622
+ function lazyInit(el, callback) {
1623
+ if ("IntersectionObserver" in window) {
1624
+ const observer = new IntersectionObserver((entries) => {
1625
+ entries.forEach((entry) => {
1626
+ if (entry.isIntersecting) {
1627
+ observer.unobserve(el);
1628
+ callback();
1629
+ }
1630
+ });
1631
+ }, { rootMargin: "200px" });
1632
+ observer.observe(el);
1633
+ } else {
1634
+ callback();
1635
+ }
1636
+ }
1637
+ function processElement(el, config) {
1638
+ if (el.shadowRoot) return;
1639
+ const dataKey = config.attribute.replace("data-", "");
1640
+ const camelKey = dataKey.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1641
+ const widgetType = el.dataset[camelKey];
1642
+ if (!widgetType) return;
1643
+ lazyInit(el, () => {
1644
+ if (!el.shadowRoot) initWidget(el, widgetType, config);
1645
+ });
1646
+ }
1647
+ function initAll(config) {
1648
+ document.querySelectorAll(`[${config.attribute}]`).forEach((el) => processElement(el, config));
1649
+ }
1650
+ (function bootstrap() {
1651
+ const config = '{"site":"vinofyi","name":"VinoFYI","domain":"vinofyi.com","accent":"#7F1D1D","attribute":"data-vinofyi","apiBase":"https://vinofyi.com/api/v1/","searchPath":"/search/","entityName":"Wines","entitySlug":"wines"}';
1652
+ if (document.readyState === "loading") {
1653
+ document.addEventListener("DOMContentLoaded", () => initAll(config));
1654
+ } else {
1655
+ initAll(config);
1656
+ }
1657
+ const observer = new MutationObserver((mutations) => {
1658
+ mutations.forEach((mutation) => {
1659
+ mutation.addedNodes.forEach((node) => {
1660
+ var _a;
1661
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
1662
+ const el = node;
1663
+ if (el.hasAttribute(config.attribute)) processElement(el, config);
1664
+ (_a = el.querySelectorAll) == null ? void 0 : _a.call(el, `[${config.attribute}]`).forEach((child) => processElement(child, config));
1665
+ });
1666
+ });
1667
+ });
1668
+ observer.observe(document.body || document.documentElement, { childList: true, subtree: true });
1669
+ })();
1670
+ function makeWidgetElement(widgetType, initFn, domainAttrs) {
1671
+ const observed = [...domainAttrs, "theme", "style-variant", "size"];
1672
+ return class extends HTMLElement {
1673
+ static get observedAttributes() {
1674
+ return observed;
1675
+ }
1676
+ connectedCallback() {
1677
+ if (this.shadowRoot) return;
1678
+ this._syncDataAttrs();
1679
+ initFn(this, '{"site":"vinofyi","name":"VinoFYI","domain":"vinofyi.com","accent":"#7F1D1D","attribute":"data-vinofyi","apiBase":"https://vinofyi.com/api/v1/","searchPath":"/search/","entityName":"Wines","entitySlug":"wines"}');
1680
+ }
1681
+ attributeChangedCallback(_name, oldVal, newVal) {
1682
+ if (oldVal === newVal || !this.shadowRoot) return;
1683
+ const shadow = this.shadowRoot;
1684
+ while (shadow.firstChild) shadow.firstChild.remove();
1685
+ this._syncDataAttrs();
1686
+ initFn(this, '{"site":"vinofyi","name":"VinoFYI","domain":"vinofyi.com","accent":"#7F1D1D","attribute":"data-vinofyi","apiBase":"https://vinofyi.com/api/v1/","searchPath":"/search/","entityName":"Wines","entitySlug":"wines"}');
1687
+ }
1688
+ _syncDataAttrs() {
1689
+ const attrKey = '{"site":"vinofyi","name":"VinoFYI","domain":"vinofyi.com","accent":"#7F1D1D","attribute":"data-vinofyi","apiBase":"https://vinofyi.com/api/v1/","searchPath":"/search/","entityName":"Wines","entitySlug":"wines"}'.attribute.replace("data-", "");
1690
+ this.dataset[attrKey] = widgetType;
1691
+ for (const a of domainAttrs) {
1692
+ const val = this.getAttribute(a);
1693
+ if (val !== null) this.dataset[a] = val;
1694
+ }
1695
+ const theme = this.getAttribute("theme");
1696
+ if (theme !== null) this.dataset.theme = theme;
1697
+ const styleVariant = this.getAttribute("style-variant");
1698
+ if (styleVariant !== null) this.dataset.style = styleVariant;
1699
+ const size = this.getAttribute("size");
1700
+ if (size !== null) this.dataset.size = size;
1701
+ }
1702
+ };
1703
+ }
1704
+ (function registerElements() {
1705
+ if (typeof customElements === "undefined") return;
1706
+ const site = '{"site":"vinofyi","name":"VinoFYI","domain":"vinofyi.com","accent":"#7F1D1D","attribute":"data-vinofyi","apiBase":"https://vinofyi.com/api/v1/","searchPath":"/search/","entityName":"Wines","entitySlug":"wines"}'.site;
1707
+ const defs = [
1708
+ [`${site}-recipe`, initRecipeWidget, ["slug"]],
1709
+ [`${site}-compare`, initCompareWidget, ["slugs"]],
1710
+ [`${site}-glossary`, initGlossaryWidget, ["slug", "letter"]],
1711
+ [`${site}-faq`, initFaqWidget, ["slug", "category"]],
1712
+ [`${site}-guide`, initGuideWidget, ["slug"]],
1713
+ [`${site}-ingredient`, initIngredientWidget, ["slug"]],
1714
+ [`${site}-pairing`, initPairingWidget, ["slug"]],
1715
+ [`${site}-search`, initSearchWidget, ["placeholder", "query"]]
1716
+ ];
1717
+ for (const [tagName, initFn, attrs] of defs) {
1718
+ if (!customElements.get(tagName)) {
1719
+ const widgetType = tagName.slice(site.length + 1);
1720
+ customElements.define(tagName, makeWidgetElement(widgetType, initFn, attrs));
1721
+ }
1722
+ }
1723
+ })();