wineater-bot 4.24.1 → 4.24.2

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 (57) hide show
  1. package/package.json +5 -1
  2. package/.claude/settings.local.json +0 -10
  3. package/.prettierignore +0 -9
  4. package/.prettierrc +0 -16
  5. package/CLAUDE.md +0 -115
  6. package/docs/CUSTOMIZATION.md +0 -133
  7. package/docs/SVELTE.md +0 -146
  8. package/e2e/demo.test.ts +0 -6
  9. package/eslint.config.js +0 -41
  10. package/index.html +0 -23
  11. package/playwright.config.ts +0 -9
  12. package/pnpm-workspace.yaml +0 -2
  13. package/src/App.svelte +0 -784
  14. package/src/App.svelte.test.ts +0 -13
  15. package/src/assets/biobio.png +0 -0
  16. package/src/components/CookieConsent.svelte +0 -73
  17. package/src/components/Footer.svelte +0 -122
  18. package/src/components/Header.svelte +0 -99
  19. package/src/components/Search.svelte +0 -177
  20. package/src/components/SkeletonTiles.svelte +0 -94
  21. package/src/components/WineBottle.svelte +0 -56
  22. package/src/components/WinePopup.svelte +0 -192
  23. package/src/components/WineTile.svelte +0 -458
  24. package/src/components/icons/Budget.svelte +0 -35
  25. package/src/components/icons/Diamond.svelte +0 -35
  26. package/src/components/icons/Fish.svelte +0 -56
  27. package/src/components/icons/Heart.svelte +0 -28
  28. package/src/components/icons/Meat.svelte +0 -32
  29. package/src/components/ui/Action.svelte +0 -83
  30. package/src/demo.spec.ts +0 -13
  31. package/src/index.js +0 -7
  32. package/src/services/analytics/analytics.ts +0 -142
  33. package/src/services/analytics/constants.ts +0 -18
  34. package/src/services/api/api.ts +0 -298
  35. package/src/services/api/deserialize.ts +0 -120
  36. package/src/services/bottle/bottle.spec.ts +0 -32
  37. package/src/services/bottle/bottle.ts +0 -78
  38. package/src/services/cache.ts +0 -28
  39. package/src/services/i18n/i18n.ts +0 -146
  40. package/src/services/i18n/translations.json +0 -658
  41. package/src/services/profile/consent.svelte.ts +0 -115
  42. package/src/services/profile/events.ts +0 -73
  43. package/src/services/profile/uid.ts +0 -87
  44. package/src/services/shopify/cart.ts +0 -105
  45. package/src/services/shopify/shopify.spec.ts +0 -66
  46. package/src/services/shopify/shopify.ts +0 -150
  47. package/src/services/store/app-store.svelte.ts +0 -172
  48. package/src/services/store/deserialize.ts +0 -25
  49. package/src/services/store/types.ts +0 -12
  50. package/src/services/ui-config/get-config.ts +0 -25
  51. package/src/services/ui-config/projects-config.ts +0 -156
  52. package/src/services/ui-config/types.ts +0 -15
  53. package/src/services/ui-config/utils.ts +0 -40
  54. package/tsconfig.json +0 -23
  55. package/vite.config.js +0 -84
  56. package/vite.config.js.timestamp-1772121492941-e00587396dc48.mjs +0 -71
  57. package/vitest-setup-client.ts +0 -2
@@ -1,13 +0,0 @@
1
- import { page } from '@vitest/browser/context';
2
- import { describe, expect, it } from 'vitest';
3
- import { render } from 'vitest-browser-svelte';
4
- import App from './App.svelte';
5
-
6
- describe('App.svelte', () => {
7
- it('should render h1', async () => {
8
- render(App);
9
-
10
- const heading = page.getByRole('heading', { level: 1 });
11
- await expect.element(heading).toBeInTheDocument();
12
- });
13
- });
Binary file
@@ -1,73 +0,0 @@
1
- <script lang="ts">
2
- // Consent banner for shops with Stores.requires_cookie_consent (EU).
3
- // Rendered inside the widget, not over the host page: the shop has its own
4
- // site-wide banner, ours covers only what the widget itself stores/sends.
5
- import Action from './ui/Action.svelte';
6
- import { getTranslation } from '../services/i18n/i18n';
7
- import { setConsent } from '../services/profile/consent.svelte';
8
-
9
- let { lang }: { lang: string } = $props();
10
-
11
- // optional link to the shop's privacy policy
12
- const privacyUrl: string | undefined = window?.wineaterData?.PRIVACY_URL;
13
-
14
- const t = (key: string) => getTranslation({ key, lang });
15
- </script>
16
-
17
- <div class="wr-Consent" role="region" aria-label="Cookies">
18
- <p class="wr-Consent-text">
19
- {t('consentText')}
20
- {#if privacyUrl}
21
- <a class="wr-Consent-link" href={privacyUrl} target="_blank" rel="noopener noreferrer">
22
- {t('consentPrivacy')}
23
- </a>
24
- {/if}
25
- </p>
26
- <div class="wr-Consent-actions">
27
- <Action variant="outlined" handleClick={() => setConsent('denied')}>
28
- {t('consentDecline')}
29
- </Action>
30
- <Action variant="primary" handleClick={() => setConsent('granted')}>
31
- {t('consentAccept')}
32
- </Action>
33
- </div>
34
- </div>
35
-
36
- <style>
37
- .wr-Consent {
38
- display: flex;
39
- align-items: center;
40
- justify-content: space-between;
41
- gap: 16px;
42
- margin: 12px 0 0;
43
- padding: 12px 16px;
44
- border-radius: 12px;
45
- background: #f6f4f2;
46
- font-size: 13px;
47
- line-height: 1.4;
48
- color: #333;
49
- }
50
- .wr-Consent-text {
51
- margin: 0;
52
- flex: 1;
53
- }
54
- .wr-Consent-link {
55
- color: inherit;
56
- text-decoration: underline;
57
- margin-left: 4px;
58
- }
59
- .wr-Consent-actions {
60
- display: flex;
61
- gap: 8px;
62
- flex-shrink: 0;
63
- }
64
- @media (max-width: 640px) {
65
- .wr-Consent {
66
- flex-direction: column;
67
- align-items: stretch;
68
- }
69
- .wr-Consent-actions {
70
- justify-content: flex-end;
71
- }
72
- }
73
- </style>
@@ -1,122 +0,0 @@
1
- <script lang="ts">
2
- import { trackWidgetEvent } from '../services/analytics/analytics';
3
- import { WIDGET_EVENTS } from '../services/analytics/constants';
4
- import { appStore } from '../services/store/app-store.svelte';
5
- import { getTranslation } from '../services/i18n/i18n';
6
-
7
- type Props = {
8
- countryCode?: string | null;
9
- };
10
-
11
- let { countryCode = null }: Props = $props();
12
-
13
- let appLanguage = $derived(appStore.getAppLanguage());
14
- let supportedLanguages = $derived(appStore.getSupportedLanguages());
15
- let storeId = $derived(appStore.getStoreId());
16
-
17
- function handleLanguageChange(event: Event) {
18
- const target = event.target as HTMLSelectElement;
19
- const selectedLang = target.value;
20
-
21
- trackWidgetEvent(WIDGET_EVENTS.LANGUAGE_CHANGED, storeId, {
22
- from_language: appLanguage,
23
- to_language: selectedLang
24
- });
25
-
26
- appStore.setStoreLanguage(selectedLang);
27
- }
28
- </script>
29
-
30
- <footer class="wr-Footer">
31
- <div class="wr-Footer-center">
32
- <a href="https://wineater.com" class="wr-Footer-link" target="_blank">
33
- <span class="wr-Footer-text">{getTranslation({ key: 'poweredBy', lang: appLanguage })}</span>
34
- <img
35
- class="wr-Footer-logo"
36
- src={countryCode === 'ru'
37
- ? 'https://czvgkhagwvmknscoerfy.supabase.co/storage/v1/object/public/static-media/divino_logo.svg?x=5'
38
- : 'https://czvgkhagwvmknscoerfy.supabase.co/storage/v1/object/public/static-media/logo_medium_gray.svg'}
39
- alt="Wineater Logo"
40
- loading="lazy"
41
- decoding="async"
42
- />
43
- <span class="wr-Footer-text wr-Footer-text--sommelier">
44
- {getTranslation({ key: 'ai_sommelier', lang: appLanguage })}
45
- </span>
46
- </a>
47
- </div>
48
- {#if supportedLanguages.length > 1}
49
- <select class="wr-Footer-dropdown" value={appLanguage} onchange={handleLanguageChange}>
50
- {#each supportedLanguages as lang}
51
- <option class="wr-Footer-lang" value={lang} selected={lang === appLanguage}>
52
- {lang.toUpperCase()}
53
- </option>
54
- {/each}
55
- </select>
56
- {/if}
57
- </footer>
58
-
59
- <style>
60
- .wr-Footer {
61
- position: relative;
62
- border-top: 2px solid #f6f7fd;
63
- font-weight: 200;
64
- padding: 12px 24px;
65
- min-height: 40px;
66
- }
67
-
68
- .wr-Footer-center {
69
- position: absolute;
70
- left: 50%;
71
- transform: translateX(-50%);
72
- text-align: center;
73
- }
74
-
75
- .wr-Footer-dropdown {
76
- position: absolute;
77
- right: 0px;
78
- top: 50%;
79
- transform: translateY(-50%);
80
- padding: 4px;
81
- border-radius: 6px;
82
- border: 2px solid var(--wr-brand-color);
83
- background-color: white;
84
- color: var(--wr-brand-color);
85
- font-weight: 500;
86
- font-size: 12px;
87
- cursor: pointer;
88
- transition: all 0.2s ease;
89
- }
90
-
91
- .wr-Footer-dropdown:hover {
92
- box-shadow: 0 4px 8px rgba(123, 43, 235, 0.2);
93
- }
94
-
95
- .wr-Footer-link {
96
- text-decoration: none;
97
- display: flex;
98
- align-items: center;
99
- white-space: nowrap;
100
- gap: 8px;
101
- color: #a8a8a8;
102
- font-size: 12px;
103
- line-height: 16px;
104
- transition: opacity 0.2s ease;
105
- }
106
-
107
- .wr-Footer-link:hover {
108
- opacity: 0.8;
109
- }
110
-
111
- .wr-Footer-logo {
112
- height: 16px;
113
- width: auto;
114
- color: #a8a8a8;
115
- }
116
-
117
- @media screen and (max-width: 768px) {
118
- .wr-Footer-text--sommelier {
119
- display: none;
120
- }
121
- }
122
- </style>
@@ -1,99 +0,0 @@
1
- <script lang="ts">
2
- import { getTranslation } from '../services/i18n/i18n';
3
- import { appStore } from '../services/store/app-store.svelte';
4
- import Diamond from './icons/Diamond.svelte';
5
- import Heart from './icons/Heart.svelte';
6
- import Meat from './icons/Meat.svelte';
7
- import Fish from './icons/Fish.svelte';
8
-
9
- let lang = $derived(appStore.getAppLanguage());
10
- let uiConfig = $derived(appStore.getUiConfig());
11
- let titleText = $derived(window?.wineaterData?.TITLE_TEXT || uiConfig?.titleText || null);
12
- let ShowedIcon = $state(Meat);
13
-
14
- // splits a custom title into text/keyword segments so the meal/mood/occasion
15
- // icons can be placed right next to their matching word
16
- function splitTitleText(text: string) {
17
- return text
18
- .split(/(meal|mood|occasion)/gi)
19
- .map((part) => ({ keyword: /^(meal|mood|occasion)$/i.test(part) ? part.toLowerCase() : null, text: part }));
20
- }
21
-
22
- let titleSegments = $derived(titleText ? splitTitleText(titleText) : null);
23
- </script>
24
-
25
- <div class="wr-Header">
26
- {#if titleSegments}
27
- <h1 class="wr-Header-title">
28
- {#each titleSegments as segment}
29
- {#if segment.keyword === 'meal'}
30
- <span onmouseenter={() => (ShowedIcon = Fish)} onmouseleave={() => (ShowedIcon = Meat)}>
31
- <ShowedIcon className="wr-Header-icon" size="28" />
32
- </span><span class="wr-Header-label">{segment.text}</span>
33
- {:else if segment.keyword === 'mood'}
34
- <Heart className="wr-Header-icon" size="28" /><span class="wr-Header-label"
35
- >{segment.text}</span
36
- >
37
- {:else if segment.keyword === 'occasion'}
38
- <Diamond className="wr-Header-icon" size="28" /><span class="wr-Header-label"
39
- >{segment.text}</span
40
- >
41
- {:else}
42
- {segment.text}
43
- {/if}
44
- {/each}
45
- </h1>
46
- {:else}
47
- <h1 class="wr-Header-title">
48
- {getTranslation({ key: 'headerLine1', lang })}
49
- <!-- <span class="wr-Header-icon wr-Header-icon--meal"></span> -->
50
- <!-- TODO: show fish on hover -->
51
-
52
- <span onmouseenter={() => (ShowedIcon = Fish)} onmouseleave={() => (ShowedIcon = Meat)}>
53
- <ShowedIcon className="wr-Header-icon" size="28" />
54
- </span>
55
-
56
- <span class="wr-Header-label">{getTranslation({ key: 'headerMeal', lang })}</span>,
57
- <!-- <span class="wr-Header-icon wr-Header-icon--mood"></span> -->
58
- <Heart className="wr-Header-icon" size="28" />
59
-
60
- <span class="wr-Header-label">{getTranslation({ key: 'headerMood', lang })}</span>
61
- <br />
62
- {getTranslation({ key: 'headerOr', lang })}
63
- <!-- <span class="wr-Header-icon wr-Header-icon--occasion"></span> -->
64
- <Diamond className="wr-Header-icon" size="28" />
65
- <span class="wr-Header-label">{getTranslation({ key: 'headerOccasion', lang })}</span>
66
- {getTranslation({ key: 'headerLine2', lang })}
67
- </h1>
68
- {/if}
69
- </div>
70
-
71
- <style>
72
- .wr-Header-title {
73
- font-weight: 600;
74
- font-size: 24px;
75
- margin: 0 auto;
76
- line-height: 1.3;
77
- color: #333;
78
- text-align: center;
79
-
80
- /* TODO: add wineater-color to svg */
81
- }
82
-
83
- :global(.wr-Header-icon) {
84
- display: inline-block;
85
- vertical-align: middle;
86
- color: var(--wr-wineater-color);
87
- }
88
-
89
- .wr-Header-label {
90
- color: var(--wr-wineater-color);
91
- font-weight: 500;
92
- }
93
-
94
- @media screen and (max-width: 768px) {
95
- .wr-Header-title {
96
- display: none;
97
- }
98
- }
99
- </style>
@@ -1,177 +0,0 @@
1
- <script lang="ts">
2
- import { onMount } from 'svelte';
3
- import { appStore } from '../services/store/app-store.svelte';
4
- import { getTranslation } from '../services/i18n/i18n';
5
- import Action from './ui/Action.svelte';
6
-
7
- interface Props {
8
- searchQuery: string;
9
- onSubmit: (event: SubmitEvent) => void;
10
- initialized: boolean;
11
- }
12
-
13
- let { searchQuery = $bindable(), onSubmit, initialized }: Props = $props();
14
- let searchEl: HTMLInputElement;
15
- let isInputFocused = $state(false);
16
- let animIndex = $state(0);
17
- let animChar = $state(0);
18
- let animForward = $state(true);
19
- let animRunning = $state(false);
20
- let lang = $derived(appStore.getAppLanguage());
21
- let animationToken = 0;
22
-
23
- export function getSearchElement() {
24
- return searchEl;
25
- }
26
-
27
- function initializeSearchAnimation() {
28
- const localToken = ++animationToken;
29
- const suggestions = getTranslation({ key: 'placeholderSuggestions', lang });
30
- if (!searchEl || !suggestions.length) return;
31
- animRunning = true;
32
- const loop = () => {
33
- // abort if a new animation has started or conditions changed
34
- if (localToken !== animationToken) return;
35
- if (!searchEl || !animRunning || isInputFocused || searchQuery.length) return;
36
- const current = suggestions[animIndex];
37
- if (animForward) {
38
- searchEl.placeholder = current.substring(0, animChar++);
39
- if (animChar > current.length) {
40
- animForward = false;
41
- setTimeout(loop, 1600);
42
- return;
43
- }
44
- setTimeout(loop, 90);
45
- } else {
46
- searchEl.placeholder = current.substring(0, animChar--);
47
- if (animChar < 0) {
48
- animForward = true;
49
- animIndex = (animIndex + 1) % suggestions.length;
50
- setTimeout(loop, 450);
51
- return;
52
- }
53
- setTimeout(loop, 45);
54
- }
55
- };
56
- loop();
57
- }
58
-
59
- function stopSearchAnimation() {
60
- animRunning = false;
61
- }
62
-
63
- function onSearchFocus() {
64
- isInputFocused = true;
65
- stopSearchAnimation();
66
- if (searchEl) searchEl.placeholder = '';
67
- }
68
-
69
- function onSearchBlur() {
70
- isInputFocused = false;
71
- animChar = 0;
72
- animForward = true;
73
- animRunning = false;
74
- }
75
-
76
- $effect(() => {
77
- // update placeholder whenever lang or searchEl change
78
- if (!searchEl) return;
79
- if (searchQuery.length > 0) return;
80
- // cancel any running animation on language change
81
- animationToken++;
82
- animRunning = false;
83
- animIndex = 0;
84
- animChar = 0;
85
- animForward = true;
86
- const suggestions = getTranslation({ key: 'placeholderSuggestions', lang });
87
- if (suggestions && suggestions.length) {
88
- searchEl.placeholder = suggestions[0];
89
- } else {
90
- searchEl.placeholder = getTranslation({ key: 'searchPlaceholder', lang });
91
- }
92
- });
93
-
94
- $effect(() => {
95
- // control typing animation separately with latest lang
96
- if (searchEl && initialized && !isInputFocused && !searchQuery.length && !animRunning) {
97
- initializeSearchAnimation();
98
- }
99
- if (searchQuery.length > 0 && animRunning) {
100
- animRunning = false;
101
- animationToken++;
102
- }
103
- });
104
-
105
- onMount(() => {
106
- // effects handle initial placeholder and animation
107
- });
108
- </script>
109
-
110
- <form class="wr-Search" onsubmit={onSubmit}>
111
- <span class="wr-Search-icon"></span>
112
- <input
113
- class="wr-Search-input"
114
- bind:this={searchEl}
115
- type="text"
116
- bind:value={searchQuery}
117
- onfocus={onSearchFocus}
118
- onblur={onSearchBlur}
119
- />
120
- <Action className="wr-Search-button" type="submit" variant="primary">
121
- {getTranslation({ key: 'searchButton', lang })}
122
- </Action>
123
- </form>
124
-
125
- <style>
126
- .wr-Search {
127
- position: relative;
128
- margin: 12px auto;
129
- padding: 0 6px;
130
- display: flex;
131
- align-items: center;
132
- border: 2px solid #d5d9ee;
133
- border-radius: 10px;
134
- }
135
-
136
- .wr-Search-icon {
137
- left: 12px;
138
- width: 24px;
139
- height: 24px;
140
- flex: 0 0 auto;
141
- background-image: url("data:image/svg+xml,%3Csvg%20width=%2224%22%20height=%2225%22%20viewBox=%220%200%2024%2025%22%20fill=%22none%22%20xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cpath%20d=%22M11%2019.3884C15.4183%2019.3884%2019%2015.8067%2019%2011.3884C19%206.97015%2015.4183%203.38843%2011%203.38843C6.58172%203.38843%203%206.97015%203%2011.3884C3%2015.8067%206.58172%2019.3884%2011%2019.3884Z%22%20stroke=%22%238250D2%22%20stroke-width=%222%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22/%3E%3Cpath%20d=%22M21%2021.3884L16.7%2017.0884%22%20stroke=%22%238250D2%22%20stroke-width=%222%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22/%3E%3C/svg%3E");
142
- background-repeat: no-repeat;
143
- background-size: contain;
144
- z-index: 2;
145
- }
146
-
147
- .wr-Search-input {
148
- flex: 1 1 0%;
149
- min-width: 0;
150
- padding: 12px;
151
- font-size: 16px;
152
- border: none;
153
- border-radius: 10px;
154
- outline: none;
155
- transition: border 0.2s ease;
156
- }
157
-
158
- .wr-Search-input::placeholder {
159
- color: var(--wr-wineater-color);
160
- opacity: 1;
161
- transition:
162
- color 0.3s ease,
163
- opacity 0.3s ease-in-out;
164
- }
165
-
166
- .wr-Search-input:focus {
167
- border-color: var(--wr-brand-color);
168
- }
169
-
170
- .wr-Search-input:focus::placeholder {
171
- opacity: 0.5;
172
- }
173
-
174
- .wr-Search-button {
175
- flex: 0 0 auto;
176
- }
177
- </style>
@@ -1,94 +0,0 @@
1
- <!-- Placeholder cards that match the WineTile layout so the grid does not jump when real data arrives -->
2
- <div class="wr-Skeleton" aria-hidden="true">
3
- {#each [0, 1, 2, 3] as n (n)}
4
- <div class="wr-Skeleton-tile">
5
- <div class="wr-Skeleton-block wr-Skeleton-image"></div>
6
- <div class="wr-Skeleton-block wr-Skeleton-price"></div>
7
- <div class="wr-Skeleton-block wr-Skeleton-title"></div>
8
- <div class="wr-Skeleton-block wr-Skeleton-title wr-Skeleton-title--short"></div>
9
- <div class="wr-Skeleton-block wr-Skeleton-button"></div>
10
- </div>
11
- {/each}
12
- </div>
13
-
14
- <style>
15
- .wr-Skeleton {
16
- display: grid;
17
- grid-template-columns: repeat(4, 1fr);
18
- }
19
-
20
- .wr-Skeleton-tile {
21
- display: flex;
22
- flex-direction: column;
23
- align-items: center;
24
- gap: 10px;
25
- padding: 16px 8px;
26
- border-right: 2px solid #f6f7fd;
27
- }
28
-
29
- .wr-Skeleton-tile:last-child {
30
- border-right: none;
31
- }
32
-
33
- .wr-Skeleton-block {
34
- border-radius: 6px;
35
- background: linear-gradient(90deg, #f1f2f8 25%, #f8f9fc 50%, #f1f2f8 75%);
36
- background-size: 200% 100%;
37
- animation: wr-shimmer 1.2s linear infinite;
38
- }
39
-
40
- .wr-Skeleton-image {
41
- width: 60%;
42
- height: 200px;
43
- margin-bottom: 6px;
44
- }
45
-
46
- .wr-Skeleton-price {
47
- width: 40px;
48
- height: 16px;
49
- }
50
-
51
- .wr-Skeleton-title {
52
- width: 80%;
53
- height: 14px;
54
- }
55
-
56
- .wr-Skeleton-title--short {
57
- width: 55%;
58
- }
59
-
60
- .wr-Skeleton-button {
61
- width: 96px;
62
- height: 36px;
63
- margin-top: 14px;
64
- border-radius: 10px;
65
- }
66
-
67
- @keyframes wr-shimmer {
68
- from {
69
- background-position: 200% 0;
70
- }
71
- to {
72
- background-position: -200% 0;
73
- }
74
- }
75
-
76
- @media (prefers-reduced-motion: reduce) {
77
- .wr-Skeleton-block {
78
- animation: none;
79
- }
80
- }
81
-
82
- @media screen and (max-width: 768px) {
83
- .wr-Skeleton {
84
- display: flex;
85
- gap: 8px;
86
- overflow: hidden;
87
- padding: 8px 0;
88
- }
89
-
90
- .wr-Skeleton-tile {
91
- flex: 0 0 240px;
92
- }
93
- }
94
- </style>
@@ -1,56 +0,0 @@
1
- <script lang="ts">
2
- import { BOTTLES, BUBBLES, SHAPES, bottleKind } from '../services/bottle/bottle';
3
-
4
- let { type }: { type?: string | null } = $props();
5
-
6
- const kind = $derived(bottleKind(type));
7
- const k = $derived(BOTTLES[kind]);
8
- const sh = $derived(SHAPES[k.shoulder]);
9
- const gid = `wr-bottle-${Math.random().toString(36).slice(2, 8)}`;
10
- </script>
11
-
12
- <svg class="wr-WineBottle" viewBox="0 0 44 130" aria-hidden="true" focusable="false">
13
- <defs>
14
- <linearGradient id={gid} x1="0" x2="1" y1="0" y2="0">
15
- <stop offset="0" stop-color={k.glass} stop-opacity="0.78" />
16
- <stop offset="0.45" stop-color={k.glass} />
17
- <stop offset="1" stop-color={k.glass} stop-opacity="0.88" />
18
- </linearGradient>
19
- </defs>
20
- <path d={sh.body} fill="url(#{gid})" />
21
- {#if kind === 'sparkling'}
22
- <path d="M16.5 10h11l1.5 26h-14z" fill={k.foil} />
23
- <rect x="14" y="2" width="16" height="9" rx="3.5" fill={k.foil} />
24
- <rect x="14" y="8" width="16" height="2.4" fill="#000" opacity="0.12" />
25
- <path
26
- d="M16 15h12M15.6 20h12.8M15.2 25h13.6M14.8 30h14.4M22 11v21"
27
- stroke="#8a6d12"
28
- stroke-opacity="0.55"
29
- stroke-width="0.9"
30
- fill="none"
31
- />
32
- <circle cx="22" cy="33" r="1.6" fill="none" stroke="#8a6d12" stroke-opacity="0.55" stroke-width="0.9" />
33
- <g fill="#fff" opacity="0.4">
34
- {#each BUBBLES as [cx, cy, r] (`${cx}-${cy}`)}
35
- <circle {cx} {cy} {r} />
36
- {/each}
37
- </g>
38
- {:else}
39
- <rect x={sh.cap.x} y={sh.cap.y} width={sh.cap.w} height={sh.cap.h} rx="2.5" fill={k.foil} />
40
- <rect x={sh.cap.x} y={sh.cap.y + sh.cap.h - 3} width={sh.cap.w} height="2.5" fill="#000" opacity="0.12" />
41
- {/if}
42
- <rect x={sh.label.x} y={sh.label.y} width={sh.label.w} height={sh.label.h} rx="2.5" fill="#fffdf8" opacity="0.94" />
43
- <rect x={sh.label.x + sh.label.w * 0.2} y={sh.label.y + 8} width={sh.label.w * 0.6} height="2.4" rx="1.2" fill={k.accent} />
44
- <rect x={sh.label.x + sh.label.w * 0.3} y={sh.label.y + 14} width={sh.label.w * 0.4} height="1.8" rx="0.9" fill={k.accent} opacity="0.5" />
45
- <rect x={sh.label.x + sh.label.w * 0.3} y={sh.label.y + 19} width={sh.label.w * 0.4} height="1.8" rx="0.9" fill={k.accent} opacity="0.5" />
46
- <path d={sh.shine} stroke="#fff" stroke-opacity="0.22" stroke-width="2.2" stroke-linecap="round" fill="none" />
47
- </svg>
48
-
49
- <style>
50
- .wr-WineBottle {
51
- display: block;
52
- height: 100%;
53
- width: auto;
54
- margin: 0 auto;
55
- }
56
- </style>