notify-me-wl 3.5.0 → 3.6.1
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.
- package/README.md +251 -326
- package/build/notify-me-wl.min.js +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,413 +1,338 @@
|
|
|
1
|
-
|
|
1
|
+
# Notify Me / Coming Soon widget
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A drop-in widget that lets shoppers register interest in an out-of-stock size ("Notify Me") or an upcoming product ("Coming Soon"). It opens as a modal or a slide-in panel, collects an email (and optionally more), and sends it to The Wishlist's Customer Interest API.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Full integration guide: [TWC docs — Notify Me & Coming Soon](https://the-wishlist-documentation.gitbook.io/the-wishlist-documentation-docs/notify-me-and-coming-soon/overview)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**Contents:** [Quick start](#quick-start) · [Product data](#product-data) · [Opening the widget](#opening-the-widget) · [Configuration](#configuration) · [Copy](#copy) · [Styling](#styling) · [Authentication](#authentication) · [Recommendations](#recommendations) · [What gets sent](#what-gets-sent) · [Development](#development) · [Upgrading](#upgrading)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
---
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
site is the primary reference for integration, the Customer Interest API, and
|
|
13
|
-
related guides.
|
|
11
|
+
## Quick start
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
- Clean, centered modal that inherits the host store's font and stays out of the theme's way (all styles are namespaced under `#twc-nm-overlay`).
|
|
17
|
-
- Dynamically display a labeled form to collect user information (email, mobile, first/last name, country, province).
|
|
18
|
-
- Adjust form copy and button text based on the widget type (`notify-me` or `coming-soon`).
|
|
19
|
-
- Inline success/error feedback (no browser `alert`s); the submit button shows a "Sending…" loading state.
|
|
20
|
-
- Closes on the × button, clicking the backdrop, or pressing `Esc`; locks background scroll while open and respects `prefers-reduced-motion`.
|
|
21
|
-
- Optional email/SMS marketing opt-ins.
|
|
22
|
-
- Two display modes: a centred modal (default) or a right-hand slide-in panel — see [Display modes](#display-modes).
|
|
23
|
-
- Opens from any element marked `data-twc-notify-me` — PLP size buttons, "Size out of stock?" links — for the product it belongs to, with the right size pre-selected. See [Opening from product listing pages](#opening-from-product-listing-pages).
|
|
24
|
-
- Two auth modes: Shopify App Proxy (default) or a merchant-supplied token — see [Configuration](#configuration).
|
|
25
|
-
- Optional "Shop similar styles" section under the form, showing personalised product recommendations for the customer.
|
|
13
|
+
Three pieces go into the theme. This example is a Shopify product page.
|
|
26
14
|
|
|
27
|
-
|
|
15
|
+
**1. The product**, so the widget knows which sizes are out of stock:
|
|
28
16
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
npm run build # emit build/notify-me-wl.min.js
|
|
42
|
-
npm run dev # rebuild on change (watch mode)
|
|
43
|
-
npm run typecheck # type-check without emitting
|
|
44
|
-
npm run selftest # build, then run the harness pages in headless Chrome
|
|
17
|
+
```liquid
|
|
18
|
+
<script>
|
|
19
|
+
window.currentProduct = {
|
|
20
|
+
id: {{ product.id | json }},
|
|
21
|
+
title: {{ product.title | json }},
|
|
22
|
+
variants: [
|
|
23
|
+
{%- for v in product.variants -%}
|
|
24
|
+
{ id: {{ v.id | json }}, title: {{ v.title | json }}, available: {{ v.available | json }} }{% unless forloop.last %},{% endunless %}
|
|
25
|
+
{%- endfor -%}
|
|
26
|
+
]
|
|
27
|
+
};
|
|
28
|
+
</script>
|
|
45
29
|
```
|
|
46
30
|
|
|
47
|
-
|
|
48
|
-
> `build/notify-me-wl.min.js`. Update embeds to the new CDN URL above.
|
|
49
|
-
|
|
50
|
-
### Usage
|
|
31
|
+
**2. The widget and its configuration**, once per page:
|
|
51
32
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
33
|
+
```liquid
|
|
34
|
+
<div id="notification-widget"
|
|
35
|
+
data-market-id="{{ localization.market.id }}"
|
|
36
|
+
data-market-handle="{{ localization.market.handle }}"
|
|
37
|
+
data-country-code="{{ localization.country.iso_code }}">
|
|
38
|
+
<script type="application/json" data-twc-config>
|
|
39
|
+
{
|
|
40
|
+
"tenant": "your-tenant",
|
|
41
|
+
"type": "notify-me",
|
|
42
|
+
"display": "panel",
|
|
43
|
+
"fields": ["email"]
|
|
44
|
+
}
|
|
45
|
+
</script>
|
|
46
|
+
</div>
|
|
47
|
+
<script src="https://cdn.jsdelivr.net/npm/notify-me-wl@3.6.0/build/notify-me-wl.min.js" defer></script>
|
|
57
48
|
```
|
|
58
49
|
|
|
59
|
-
|
|
60
|
-
include Shopify's `available` flag — the widget then lists only out-of-stock sizes
|
|
61
|
-
itself — or be pre-filtered in Liquid as older installs do.
|
|
62
|
-
|
|
63
|
-
### Configuration
|
|
64
|
-
|
|
65
|
-
Every attribute below can be set on `#notification-widget` or on `#popup-open`. When
|
|
66
|
-
both carry the same attribute, `#popup-open` wins. `#popup-open` is optional.
|
|
67
|
-
|
|
68
|
-
- `data-fields`: A JSON array specifying the optional fields to include in the form. Possible values are `"email"`, `"mobile"`, `"firstName"`, `"lastName"`, `"countryCode"`, and `"provinceCode"`. Defaults to `["email"]`. (A required Size selector is always shown and populated from the product's variants.)
|
|
69
|
-
- `data-type`: Specifies the type of the widget. Possible values are `"notify-me"` and `"coming-soon"`.
|
|
70
|
-
- `data-display`: How the widget presents itself. `"modal"` (default) is a centred dialog; `"panel"` slides in from the right edge at full height. Any other value falls back to `"modal"`. See [Display modes](#display-modes).
|
|
71
|
-
- `data-auth`: Auth mode. `"proxy"` (default) uses the Shopify App Proxy at `/apps/<proxy-app>/auth/token` to obtain a tenant-scoped token (requires the customer to be logged in to the storefront). `"token"` sends a token you supply via `data-access-token` or `window.TWC_ACCESS_TOKEN`. **The widget bundles no credential of its own** — see [Auth modes](#auth-modes).
|
|
72
|
-
- `data-tenant`: The TWC tenant sent as the `X-Twc-Tenant` header. Used in both auth modes. Falls back to the bundled `TENANT_ID` if omitted.
|
|
73
|
-
- `data-proxy-app`: The Shopify App Proxy subpath, forming the `/apps/<proxy-app>/...` URL prefix used by the `"proxy"` auth mode. Falls back to the bundled `PROXY_APP_NAME` (`twc-sdk`) if omitted.
|
|
74
|
-
- `data-access-token`: Access token for the `"token"` auth mode, with or without the `Bearer ` prefix. Takes precedence over `window.TWC_ACCESS_TOKEN`. Ignored in `"proxy"` mode. Anything rendered here is visible to anyone viewing the page — use a short-lived, narrowly scoped token, never a staff or POS credential.
|
|
75
|
-
- `data-market-id`: The Shopify market the shopper is browsing in. Accepts either the numeric id or the full GID — `gid://shopify/Market/12345` and `12345` both send `12345`. Falls back to `window.Shopify.markets.currentMarket.id` when omitted. See [Shopify markets](#shopify-markets).
|
|
76
|
-
- `data-market-handle`: The market handle (e.g. `"au"`). Attribute only — there is no runtime fallback, so a theme that does not render it reports no handle.
|
|
77
|
-
- `data-country-code`: Two-letter ISO country code for the market the shopper is browsing in. Takes priority over the automatic detection chain, but a country the shopper picks in the form still wins over both.
|
|
78
|
-
- `data-recommendations`: Set to `"false"` to disable the "Shop similar styles" section. Enabled by default.
|
|
79
|
-
- `data-recommendations-count`: How many products to show. Defaults to `8`. Non-numeric or non-positive values fall back to `8`.
|
|
80
|
-
- `data-recommendations-profile`: Athos/Searchspring recommendation profile tag for this install (e.g. `"similar-snap"`). Only consulted when the tenant config carries no `profileTag`/`tags`; see [Choosing an engine](#choosing-an-engine). Ignored by the `TWC` engine.
|
|
81
|
-
- `data-currency`: Fallback ISO currency code (e.g. `"AUD"`) for formatting recommendation prices, used only when the storefront does not expose `window.Shopify.currency.active`. Without either, prices render as bare numbers rather than risking the wrong symbol.
|
|
82
|
-
- `data-customer-email`: Overrides the customer email used to request recommendations. Intended for testing — in production the email comes from the Shopify customer context, or from the email the shopper submits.
|
|
83
|
-
|
|
84
|
-
### Auth modes
|
|
85
|
-
|
|
86
|
-
Every TWC API call needs an `Authorization` header and an `X-Twc-Tenant` header.
|
|
87
|
-
`data-auth` chooses where the token comes from. **The distributed bundle contains no
|
|
88
|
-
access token** — a `"token"` install that supplies none will fail to submit.
|
|
89
|
-
|
|
90
|
-
#### `"proxy"` (default)
|
|
91
|
-
|
|
92
|
-
The widget lazily fetches a tenant-scoped token from the same-origin Shopify App Proxy
|
|
93
|
-
endpoint `/apps/<proxy-app>/auth/token` on form submit (where `<proxy-app>` comes from
|
|
94
|
-
`data-proxy-app`, defaulting to `twc-sdk`; Shopify signs and forwards the request). The
|
|
95
|
-
token is cached for the page session and reused across submissions.
|
|
96
|
-
|
|
97
|
-
Because the proxy only issues a token for a logged-in customer, if the token cannot be
|
|
98
|
-
obtained the popup stays open and shows an inline "Please log in to your account to
|
|
99
|
-
continue." message; submission is blocked until a token is available.
|
|
100
|
-
|
|
101
|
-
#### `"token"`
|
|
102
|
-
|
|
103
|
-
Sends a token you supply, from `data-access-token` on `#notification-widget` (or `#popup-open`) or
|
|
104
|
-
`window.TWC_ACCESS_TOKEN`. The attribute wins if both are present, and the `Bearer `
|
|
105
|
-
prefix is added if you omit it.
|
|
50
|
+
**3. A button that opens it:**
|
|
106
51
|
|
|
107
52
|
```html
|
|
108
|
-
<button
|
|
109
|
-
data-access-token="{{ twc_widget_token }}">Notify Me</button>
|
|
53
|
+
<button type="button" data-twc-notify-me>Notify me</button>
|
|
110
54
|
```
|
|
111
55
|
|
|
112
|
-
|
|
113
|
-
page or the bundle. Mint a short-lived token scoped to customer-interest writes for the
|
|
114
|
-
single tenant — never a staff, POS, or store-owner credential. If you cannot mint one
|
|
115
|
-
that narrow, use `"proxy"` instead.
|
|
56
|
+
That's a working install. Always pin the script to an exact version (`@3.6.0`), so a new release never reaches a live store until you choose to upgrade.
|
|
116
57
|
|
|
117
|
-
|
|
118
|
-
submission fails with the generic error message.
|
|
58
|
+
---
|
|
119
59
|
|
|
120
|
-
|
|
121
|
-
> `"proxy"`, and the previously bundled access token was removed. Installs that relied
|
|
122
|
-
> on the bundled token must either set up the App Proxy or supply their own token via
|
|
123
|
-
> `data-access-token`.
|
|
60
|
+
## Product data
|
|
124
61
|
|
|
125
|
-
|
|
62
|
+
The widget reads the product from the clicked element's closest `[data-twc-product]`, or from `window.currentProduct` on a product page.
|
|
126
63
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
64
|
+
| Field | Required | Meaning |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `id`, `title` | yes | Product id and name |
|
|
67
|
+
| `variants[].id`, `variants[].title` | yes | Each size. `title` is what the size dropdown shows |
|
|
68
|
+
| `variants[].available` | recommended | Only sizes that are not `true` are offered. Without it, every listed variant is offered (older installs pre-filter in Liquid) |
|
|
69
|
+
| `image` | for the summary | Product image URL, used when a variant has none |
|
|
70
|
+
| `variants[].price` | for the summary | Already-formatted price, e.g. `"$199.96 AUD"` |
|
|
71
|
+
| `variants[].compareAtPrice` | for the summary | Already-formatted "was" price. Only send it when it is a real discount |
|
|
72
|
+
| `variants[].image` | for the summary | Variant image URL |
|
|
130
73
|
|
|
131
|
-
|
|
132
|
-
| --- | --- | --- |
|
|
133
|
-
| Position | Centred | Docked to the right edge |
|
|
134
|
-
| Size | 400px wide, fits the viewport | 420px wide, full height |
|
|
135
|
-
| Corners | Rounded | Square |
|
|
136
|
-
| Entry | Fades and lifts | Slides in from the right |
|
|
137
|
-
| Below 480px | Unchanged | Spans the full viewport width |
|
|
74
|
+
The last four only matter with `"productSummary": true` (see [Configuration](#configuration)). Prices are passed pre-formatted so they always match the theme's own price display. Image URLs must start with `https:`, `http:` or `//`.
|
|
138
75
|
|
|
139
|
-
|
|
140
|
-
<button id="popup-open" data-type="notify-me" data-display="panel"
|
|
141
|
-
data-fields='["email"]'>Notify Me</button>
|
|
142
|
-
```
|
|
76
|
+
A full capture, including summary fields, that works for both product pages and cards:
|
|
143
77
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
78
|
+
```liquid
|
|
79
|
+
{%- capture twc_product -%}
|
|
80
|
+
{"id":{{ product.id | json }},"title":{{ product.title | json }}
|
|
81
|
+
{%- if product.featured_image -%},"image":{{ product.featured_image | image_url: width: 300 | json }}{%- endif -%},"variants":[
|
|
82
|
+
{%- for v in product.variants -%}
|
|
83
|
+
{"id":{{ v.id | json }},"title":{{ v.title | json }},"available":{{ v.available | json }},
|
|
84
|
+
"price":{{ v.price | money_with_currency | json }}
|
|
85
|
+
{%- if v.compare_at_price > v.price -%},"compareAtPrice":{{ v.compare_at_price | money_with_currency | json }}{%- endif -%}
|
|
86
|
+
{%- if v.featured_image -%},"image":{{ v.featured_image | image_url: width: 300 | json }}{%- endif -%}}
|
|
87
|
+
{%- unless forloop.last -%},{%- endunless -%}
|
|
88
|
+
{%- endfor -%}]}
|
|
89
|
+
{%- endcapture -%}
|
|
150
90
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
them to the widget as attributes on `#notification-widget` (or the legacy `#popup-open` button):
|
|
91
|
+
{%- comment -%} Product page {%- endcomment -%}
|
|
92
|
+
<script>window.currentProduct = {{ twc_product }};</script>
|
|
154
93
|
|
|
155
|
-
|
|
156
|
-
<
|
|
157
|
-
data-type="notify-me"
|
|
158
|
-
data-market-id="{{ localization.market.id }}"
|
|
159
|
-
data-market-handle="{{ localization.market.handle }}"
|
|
160
|
-
data-country-code="{{ localization.country.iso_code }}">
|
|
161
|
-
Notify Me
|
|
162
|
-
</button>
|
|
94
|
+
{%- comment -%} Product card — | escape is required inside an attribute {%- endcomment -%}
|
|
95
|
+
<div class="product-card" data-twc-product="{{ twc_product | escape }}">…</div>
|
|
163
96
|
```
|
|
164
97
|
|
|
165
|
-
|
|
166
|
-
`gid://shopify/Market/12345`. The widget strips the GID prefix itself, so it does not
|
|
167
|
-
matter which form the theme renders.
|
|
168
|
-
|
|
169
|
-
Any value that does not look right — an empty string from a storefront with no market
|
|
170
|
-
configured, or an unrendered Liquid tag — is dropped, and the field is left out of the
|
|
171
|
-
payload rather than sent as `null`. All three attributes are optional: without them,
|
|
172
|
-
`marketId` and `countryCode` still resolve from `window.Shopify` and the browser locale,
|
|
173
|
-
and `marketHandle` is simply not reported.
|
|
174
|
-
|
|
175
|
-
When `data-fields` includes `"countryCode"`, the country the shopper picks in the form
|
|
176
|
-
takes precedence over `data-country-code`. The dropdown is pre-selected to the market's
|
|
177
|
-
country, so leaving it untouched sends the market country either way.
|
|
178
|
-
|
|
179
|
-
### Opening from product listing pages
|
|
180
|
-
|
|
181
|
-
Three attributes connect a theme to the widget. No theme JavaScript is needed.
|
|
98
|
+
Keep the `{%- if … -%}` guards: `image_url` on a missing image renders a Liquid error and breaks the JSON.
|
|
182
99
|
|
|
183
|
-
|
|
184
|
-
| --- | --- | --- |
|
|
185
|
-
| `data-twc-product` | A product container, e.g. the card | JSON `{ id, title, variants: [{ id, title, available }] }` |
|
|
186
|
-
| `data-twc-notify-me` | Any clickable element | Clicking it opens the widget |
|
|
187
|
-
| `data-variant-id` | Optional, on the same element | Variant to pre-select |
|
|
100
|
+
---
|
|
188
101
|
|
|
189
|
-
|
|
190
|
-
falling back to `window.currentProduct`. It listens at page level in the capture phase,
|
|
191
|
-
so cards added later (filters, infinite scroll) work, and a theme handler that stops
|
|
192
|
-
propagation does not block it. It calls `preventDefault()` on the click — `href="#"`
|
|
193
|
-
does not jump — but lets the theme's own handlers run.
|
|
102
|
+
## Opening the widget
|
|
194
103
|
|
|
195
|
-
|
|
196
|
-
too heavy to repeat on every card, and escape it into a double-quoted attribute so a
|
|
197
|
-
title with a quote or apostrophe cannot break the markup:
|
|
104
|
+
Any element with `data-twc-notify-me` opens the widget for its product. No theme JavaScript is needed, and cards added later (filters, infinite scroll) work too.
|
|
198
105
|
|
|
199
106
|
```liquid
|
|
200
|
-
{%- capture twc_product -%}
|
|
201
|
-
{"id":{{ product.id | json }},"title":{{ product.title | json }},"variants":[
|
|
202
|
-
{%- for v in product.variants -%}
|
|
203
|
-
{"id":{{ v.id | json }},"title":{{ v.title | json }},"available":{{ v.available | json }}}
|
|
204
|
-
{%- unless forloop.last -%},{%- endunless -%}
|
|
205
|
-
{%- endfor -%}]}
|
|
206
|
-
{%- endcapture -%}
|
|
207
107
|
<div class="product-card" data-twc-product="{{ twc_product | escape }}">
|
|
208
|
-
{%- for
|
|
209
|
-
<button type="button" data-variant-id="{{
|
|
210
|
-
{% unless variant.available %}data-twc-notify-me{% endunless %}>{{ variant.title }}</button>
|
|
108
|
+
{%- for v in product.variants -%}
|
|
109
|
+
<button type="button" data-variant-id="{{ v.id }}" {% unless v.available %}data-twc-notify-me{% endunless %}>{{ v.title }}</button>
|
|
211
110
|
{%- endfor -%}
|
|
212
111
|
<a href="#" data-twc-notify-me>Size out of stock?</a>
|
|
213
112
|
</div>
|
|
214
113
|
```
|
|
215
114
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
instead.
|
|
115
|
+
- Don't put `disabled` on a sold-out size button. Browsers don't fire clicks on disabled buttons, so use a class or `aria-disabled="true"` instead.
|
|
116
|
+
- The click's default action is prevented (`href="#"` won't jump), but the theme's own handlers still run.
|
|
219
117
|
|
|
220
|
-
|
|
221
|
-
`theme.liquid`), and load the script with `defer` or no attribute:
|
|
118
|
+
**Which size is pre-selected.** The first of these that matches an out-of-stock size wins:
|
|
222
119
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
data-fields='["email"]'
|
|
228
|
-
data-currency="{{ cart.currency.iso_code }}"
|
|
229
|
-
data-market-id="{{ localization.market.id }}"
|
|
230
|
-
data-market-handle="{{ localization.market.handle }}"
|
|
231
|
-
data-country-code="{{ localization.country.iso_code }}"></div>
|
|
232
|
-
<script src="https://cdn.jsdelivr.net/npm/notify-me-wl@3.5.0/build/notify-me-wl.min.js" defer></script>
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
A page must have exactly one `#notification-widget`. A second, empty one earlier in
|
|
236
|
-
the page would be the one the widget reads its configuration from.
|
|
120
|
+
1. `data-variant-id` on the clicked element.
|
|
121
|
+
2. On a product page only: `?variant=` in the URL, then the product form's `select[name="id"]` (inside `form[action*="/cart/add"]`).
|
|
122
|
+
3. The only out-of-stock size, if there is exactly one.
|
|
123
|
+
4. Otherwise "Select a size".
|
|
237
124
|
|
|
238
|
-
|
|
125
|
+
A product with nothing out of stock shows "All sizes are currently in stock." instead of the form.
|
|
239
126
|
|
|
240
|
-
|
|
241
|
-
them wins:
|
|
127
|
+
---
|
|
242
128
|
|
|
243
|
-
|
|
244
|
-
2. Product page only (the product came from `window.currentProduct`): `?variant=` in
|
|
245
|
-
the URL, then the value of the first `select[name="id"]` inside a
|
|
246
|
-
`form[action*="/cart/add"]`. Hidden `input[name="id"]` fields are ignored — some
|
|
247
|
-
themes keep a stale one in an installments form. Never applied to a card, so page
|
|
248
|
-
state cannot leak into a listing.
|
|
249
|
-
3. Exactly one out-of-stock size — that size.
|
|
250
|
-
4. Otherwise, "Select a size".
|
|
129
|
+
## Configuration
|
|
251
130
|
|
|
252
|
-
|
|
253
|
-
of the form. A missing or malformed product shows "Product information is unavailable.
|
|
254
|
-
Please refresh the page."
|
|
131
|
+
Put settings in the JSON block inside `#notification-widget`. Every setting except `productSummary`, `copy` and `classes` can also be an attribute: the attribute name is `data-` plus the key in kebab-case (`proxyApp` → `data-proxy-app`). Attributes are handy for values Liquid renders, like the market id.
|
|
255
132
|
|
|
256
|
-
|
|
133
|
+
| Key | Attribute | Default | Description |
|
|
134
|
+
|---|---|---|---|
|
|
135
|
+
| `tenant` | `data-tenant` | bundled demo tenant | Your TWC tenant. **Always set it** |
|
|
136
|
+
| `type` | `data-type` | `"notify-me"` | `"notify-me"` or `"coming-soon"`. Changes the default copy and the payload flag |
|
|
137
|
+
| `display` | `data-display` | `"modal"` | `"modal"` (centred dialog) or `"panel"` (full-height, slides in from the right) |
|
|
138
|
+
| `fields` | `data-fields` | `["email"]` | Form fields, any of `"email"`, `"mobile"`, `"firstName"`, `"lastName"`, `"countryCode"`, `"provinceCode"`. The size dropdown is always shown |
|
|
139
|
+
| `auth` | `data-auth` | `"proxy"` | `"proxy"` or `"token"`; see [Authentication](#authentication) |
|
|
140
|
+
| `proxyApp` | `data-proxy-app` | `"twc-sdk"` | Shopify App Proxy subpath (`/apps/<proxyApp>/…`) |
|
|
141
|
+
| `accessToken` | `data-access-token` | — | Token for `"token"` auth. Falls back to `window.TWC_ACCESS_TOKEN` |
|
|
142
|
+
| `marketId` | `data-market-id` | `window.Shopify` market | Shopify market id; a full `gid://…` is fine |
|
|
143
|
+
| `marketHandle` | `data-market-handle` | — | Market handle, e.g. `"au"` |
|
|
144
|
+
| `countryCode` | `data-country-code` | detected | ISO country code. A country chosen in the form wins |
|
|
145
|
+
| `currency` | `data-currency` | `window.Shopify.currency.active` | Fallback currency for recommendation prices |
|
|
146
|
+
| `customerEmail` | `data-customer-email` | Shopify customer | Overrides the shopper's email (testing) |
|
|
147
|
+
| `recommendations` | `data-recommendations` | `true` | `false` hides the "Shop similar styles" row |
|
|
148
|
+
| `recommendationsCount` | `data-recommendations-count` | `8` | Products in that row |
|
|
149
|
+
| `recommendationsProfile` | `data-recommendations-profile` | — | Searchspring profile tag, if the tenant config has none |
|
|
150
|
+
| `productSummary` | — | `false` | Show image, name, size and price at the top of the form |
|
|
151
|
+
| `copy` | — | — | Text overrides; see [Copy](#copy) |
|
|
152
|
+
| `classes` | — | — | Theme classes per element; see [Styling](#styling) |
|
|
257
153
|
|
|
258
|
-
|
|
259
|
-
beneath the form, fetched from:
|
|
154
|
+
**Rules**
|
|
260
155
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
156
|
+
- **Precedence:** an attribute on the legacy `#popup-open` button wins, then an attribute on `#notification-widget`, then the JSON block, then the default. An empty attribute counts as not set.
|
|
157
|
+
- **Use Liquid's `| json` filter** for any value you render into the block, so quotes and apostrophes can't break it.
|
|
158
|
+
- **Bad values are skipped, not fatal.** A value of the wrong type is ignored with a console warning, and `null` counts as not set. Invalid JSON is logged and the widget runs with the remaining settings.
|
|
159
|
+
- **One `#notification-widget` per page.** If there are two, the first is used.
|
|
264
160
|
|
|
265
|
-
|
|
266
|
-
customer-interest call, and the tenant doubles as the retailer id in the path.
|
|
161
|
+
---
|
|
267
162
|
|
|
268
|
-
|
|
163
|
+
## Copy
|
|
269
164
|
|
|
270
|
-
|
|
271
|
-
2. `window.customer.email` — a logged-in Shopify customer. The section then appears
|
|
272
|
-
as soon as the modal opens.
|
|
273
|
-
3. `window.customerEmail` — the same address exposed as a bare global, which is how
|
|
274
|
-
some themes already publish it. Also appears as soon as the modal opens.
|
|
275
|
-
4. The email the shopper submits through the form. The section appears after a
|
|
276
|
-
successful submit, and the modal stays open instead of auto-closing so they can
|
|
277
|
-
browse.
|
|
165
|
+
`copy` overrides the widget's main text. Everything is shown as plain text, never HTML.
|
|
278
166
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
167
|
+
```json
|
|
168
|
+
"copy": {
|
|
169
|
+
"title": "Notify me when available",
|
|
170
|
+
"intro": "We'll email you as soon as {product} is back.",
|
|
171
|
+
"emailLabel": false,
|
|
172
|
+
"emailPlaceholder": "Email",
|
|
173
|
+
"submit": "NOTIFY ME",
|
|
174
|
+
"disclaimer": "You can unsubscribe at any time."
|
|
175
|
+
}
|
|
176
|
+
```
|
|
282
177
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
178
|
+
| Key | Default |
|
|
179
|
+
|---|---|
|
|
180
|
+
| `title` | `"{product}"` (the product name) |
|
|
181
|
+
| `intro` | "Register to receive a notification as soon as this item is back in stock" / "Register your interest to hear more about this item" |
|
|
182
|
+
| `sizeLabel` / `sizePlaceholder` | `"Size"` / `"Select a size"` |
|
|
183
|
+
| `emailLabel` | `"Email"`. `false` hides the label; the input keeps an accessible name |
|
|
184
|
+
| `emailPlaceholder` | `"you@example.com"` |
|
|
185
|
+
| `emailOptIn` | `"Email me store updates"` (shown when `fields` includes `email`) |
|
|
186
|
+
| `smsOptIn` | `"Text me store updates"` (shown when `fields` includes `mobile`) |
|
|
187
|
+
| `submit` | `"Notify Me"` / `"Register Interest"` |
|
|
188
|
+
| `success` | `"You're on the list. We'll be in touch."` |
|
|
189
|
+
| `disclaimer` | none. When set, it is shown under the submit button |
|
|
190
|
+
|
|
191
|
+
`{product}` in `title` or `intro` becomes the product name. Not yet configurable: the status messages ("Sending…", "Please select a size.", errors), the close button label, and the mobile, name, country and province field labels.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Styling
|
|
196
|
+
|
|
197
|
+
All widget styles are scoped under `#twc-nm-overlay`, and the widget uses the page's font. There are two ways to match a theme. They can be combined.
|
|
198
|
+
|
|
199
|
+
### CSS variables
|
|
200
|
+
|
|
201
|
+
Set any of these on `:root` (or any ancestor, inside media queries if needed). Anything you don't set keeps the default look.
|
|
202
|
+
|
|
203
|
+
| Variable | Default |
|
|
204
|
+
|---|---|
|
|
205
|
+
| `--twc-nm-panel-width` | `420px` |
|
|
206
|
+
| `--twc-nm-panel-width-mobile` | `100%` (at ≤480px) |
|
|
207
|
+
| `--twc-nm-padding` | `24px` |
|
|
208
|
+
| `--twc-nm-overlay-bg` | `rgba(17, 17, 17, 0.55)` |
|
|
209
|
+
| `--twc-nm-panel-shadow` | `-10px 0 40px rgba(0, 0, 0, 0.18)` |
|
|
210
|
+
| `--twc-nm-transition` | `200ms ease` |
|
|
211
|
+
| `--twc-nm-bg` / `--twc-nm-text` / `--twc-nm-muted` / `--twc-nm-border` | `#fff` / `#111827` / `#6b7280` / `#e5e7eb` |
|
|
212
|
+
| `--twc-nm-font` | `inherit` |
|
|
213
|
+
| `--twc-nm-radius` | `8px` |
|
|
214
|
+
| `--twc-nm-header-border` | `none` |
|
|
215
|
+
|
|
216
|
+
```css
|
|
217
|
+
:root { --twc-nm-panel-width: 400px; --twc-nm-radius: 0; --twc-nm-overlay-bg: rgba(54, 54, 54, 0.5); }
|
|
289
218
|
```
|
|
290
219
|
|
|
291
|
-
|
|
292
|
-
it boolean `false` for guests. That is handled: the value is accepted only when it is a
|
|
293
|
-
non-empty string, so `false` resolves to "no email" rather than the string `"false"`.
|
|
220
|
+
### Theme classes
|
|
294
221
|
|
|
295
|
-
|
|
296
|
-
**prefilled** with it. The field stays editable, and submit uses whatever it contains at
|
|
297
|
-
that moment, so a shopper can send the notification to a different address.
|
|
222
|
+
`classes` adds your theme's own classes to the widget's elements, so buttons, inputs and headings look exactly like the rest of the store:
|
|
298
223
|
|
|
299
|
-
|
|
224
|
+
```json
|
|
225
|
+
"classes": {
|
|
226
|
+
"title": "Heading u-h4",
|
|
227
|
+
"input": "Form__Input",
|
|
228
|
+
"select": "Form__Input",
|
|
229
|
+
"submit": "Button Button--primary Button--full"
|
|
230
|
+
}
|
|
231
|
+
```
|
|
300
232
|
|
|
301
|
-
|
|
302
|
-
|
|
233
|
+
**Keys:**
|
|
234
|
+
- Form elements: `title`, `intro`, `label`, `input`, `select`, `checkbox`, `submit`, `close`, `disclaimer`.
|
|
235
|
+
- Product summary: `summary`, `summaryImage`, `summaryName`, `summaryVariant`, `summaryPrice`, `summaryCompareAt`.
|
|
236
|
+
- `label`, `input` and `select` apply to every element of that kind.
|
|
303
237
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
238
|
+
**How styling is split:**
|
|
239
|
+
- An element with classes drops the widget's own colours, fonts, borders, padding and hover styles, so the theme's apply cleanly.
|
|
240
|
+
- Layout (width, positioning, text truncation) always stays the widget's.
|
|
241
|
+
- A themed select keeps the widget's dropdown arrow, so give it about 36px of right padding.
|
|
242
|
+
- Class names may only contain letters, digits, `-` and `_`. Anything else is dropped, with a warning in the console.
|
|
307
243
|
|
|
308
|
-
|
|
309
|
-
the path, this endpoint is not anonymous — and reads `websiteRecommendations`:
|
|
244
|
+
**Header row.** When the JSON block is present, the title and close button sit in a `.twc-nm-header` row you can style. This is also what `--twc-nm-header-border` applies to. Without a JSON block, the markup is exactly as in earlier versions.
|
|
310
245
|
|
|
311
|
-
|
|
312
|
-
"websiteRecommendations": { "engine": "TWC" }
|
|
313
|
-
```
|
|
246
|
+
**Overriding widget rules.** The widget injects its stylesheet when it loads, after your theme's CSS. A theme rule must therefore be more specific than the widget's to win, e.g. `#twc-nm-overlay .twc-nm-card .twc-nm-title`.
|
|
314
247
|
|
|
315
|
-
|
|
316
|
-
"websiteRecommendations": { "engine": "ATHOS", "siteIdentifier": "abc123" }
|
|
317
|
-
```
|
|
248
|
+
---
|
|
318
249
|
|
|
319
|
-
|
|
320
|
-
request is made and no placeholders appear. The older top-level `recommendationsEngine`
|
|
321
|
-
field is ignored. The section is likewise skipped when the engine is unrecognised, or
|
|
322
|
-
when `ATHOS` has no `siteIdentifier`. The config is fetched once per page session, and a
|
|
323
|
-
failed config request is not cached, so a transient outage does not disable the section
|
|
324
|
-
for the rest of the visit.
|
|
250
|
+
## Authentication
|
|
325
251
|
|
|
326
|
-
|
|
327
|
-
subdomain and the path:
|
|
252
|
+
Every request carries `Authorization: Bearer <token>` and `X-Twc-Tenant: <tenant>`. The bundle ships no credential.
|
|
328
253
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
```
|
|
254
|
+
- **`"proxy"` (default):** on submit, the widget fetches a tenant-scoped token from the store's Shopify App Proxy at `/apps/<proxyApp>/auth/token` and reuses it for the page session. If the proxy returns no token, the widget shows "Please log in to your account to continue." and keeps the form open.
|
|
255
|
+
- **`"token"`:** the widget sends the token you supply in `accessToken` / `data-access-token` or `window.TWC_ACCESS_TOKEN`. Anything rendered into the page is public, so only use a short-lived token scoped to customer-interest writes for one tenant. Never use a staff or POS credential. If you can't mint one that narrow, use `"proxy"`.
|
|
332
256
|
|
|
333
|
-
|
|
257
|
+
---
|
|
334
258
|
|
|
335
|
-
|
|
336
|
-
2. `tags` on `websiteRecommendations`.
|
|
337
|
-
3. `data-recommendations-profile` on `#notification-widget` (or `#popup-open`).
|
|
338
|
-
4. The bundled default, `similar`.
|
|
259
|
+
## Recommendations
|
|
339
260
|
|
|
340
|
-
|
|
341
|
-
value, so a tenant that carries it must win. The attribute exists for merchants whose
|
|
342
|
-
config schema cannot express `profileTag` yet, letting a theme supply the tag without a
|
|
343
|
-
widget release.
|
|
261
|
+
A "Shop similar styles" row can appear under the form. It shows only when **all** of these hold:
|
|
344
262
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
263
|
+
- `recommendations` is not `false`.
|
|
264
|
+
- The tenant's config defines an engine (`websiteRecommendations`, see below).
|
|
265
|
+
- A shopper email is known. It comes from the first of these that is set:
|
|
266
|
+
- `customerEmail`.
|
|
267
|
+
- `window.customer.email`.
|
|
268
|
+
- `window.customerEmail`.
|
|
269
|
+
- The email just submitted. In this case the row appears after submit, and the widget stays open instead of auto-closing.
|
|
348
270
|
|
|
349
|
-
|
|
350
|
-
|
|
271
|
+
A known email also pre-fills the form's email field. Shopify doesn't expose the customer's email by default; add it in the theme:
|
|
272
|
+
|
|
273
|
+
```liquid
|
|
274
|
+
<script>{% if customer %}window.customer = { email: {{ customer.email | json }} };{% endif %}</script>
|
|
351
275
|
```
|
|
352
276
|
|
|
353
|
-
|
|
354
|
-
called without `products=`, so the section only populates where `window.currentProduct`
|
|
355
|
-
is available at widget init.
|
|
277
|
+
The row never blocks or changes the notify-me submission. If anything fails, it simply doesn't appear.
|
|
356
278
|
|
|
357
|
-
|
|
358
|
-
|
|
279
|
+
<details>
|
|
280
|
+
<summary>Engine selection and internals</summary>
|
|
359
281
|
|
|
360
|
-
|
|
282
|
+
- **Engine:** read once per page from `GET /services/eventcollector/api/v1/custom/configs/public` (authenticated like every other call) → `websiteRecommendations`:
|
|
283
|
+
- `{ "engine": "TWC" }`: TWC's recommendations API, `/services/recommendations/api/v1/recommendations/<tenant>/<email>`.
|
|
284
|
+
- `{ "engine": "ATHOS", "siteIdentifier": "abc123" }`: Searchspring, `https://abc123.a.searchspring.io/boost/abc123/recommend`, called with **no** TWC credentials.
|
|
285
|
+
- When the field is missing, the engine is unknown, or ATHOS has no `siteIdentifier`, no row is shown. A failed config request is retried on the next open.
|
|
286
|
+
- **Searchspring profile tag:** `profileTag`, then `tags` from the tenant config, then `recommendationsProfile`, then `"similar"`. The tag must exist on the Searchspring site; an unknown tag returns no results. Check with:
|
|
287
|
+
`curl -s "https://{site}.a.searchspring.io/boost/{site}/recommend?tags={tag}&limits=3&products={productId}"`
|
|
288
|
+
- **TWC results** come per variant, so the widget over-fetches (up to the API's limit of `n=20`) and dedupes by product.
|
|
289
|
+
- **Prices** use `window.Shopify.currency.active`, then `currency`; with neither they render as bare numbers.
|
|
361
290
|
|
|
362
|
-
|
|
363
|
-
the same product in different colourways. The widget requests three times the display
|
|
364
|
-
count and dedupes by `product_ref`, keeping the highest-scoring entry, so a row never
|
|
365
|
-
repeats a product. **That API rejects `n` above 20 with a 422**, so the over-fetch is
|
|
366
|
-
clamped to 20 — at the default count of 8 that means `n=20`. Athos returns products
|
|
367
|
-
rather than variants, so it is asked for exactly `limits={count}` with no over-fetch.
|
|
291
|
+
</details>
|
|
368
292
|
|
|
369
|
-
|
|
370
|
-
dragged with the mouse (click and drag, or use the scrollbar). A drag of more than a few
|
|
371
|
-
pixels suppresses the click that would otherwise open the product it started on.
|
|
293
|
+
---
|
|
372
294
|
|
|
373
|
-
|
|
374
|
-
space instead of the modal jumping when the products land. If the request returns
|
|
375
|
-
nothing usable, the placeholders are removed and the section disappears.
|
|
295
|
+
## What gets sent
|
|
376
296
|
|
|
377
|
-
|
|
378
|
-
to `data-currency`. With neither, prices render as bare numbers — showing `A$` on a GBP
|
|
379
|
-
price would be worse than showing no symbol at all.
|
|
297
|
+
`POST https://api.au-aws.thewishlist.io/services/wsservice/api/wishlist/items/customerInterest`
|
|
380
298
|
|
|
381
|
-
|
|
382
|
-
|
|
299
|
+
| Field | When |
|
|
300
|
+
|---|---|
|
|
301
|
+
| `variantRef` | Always: the size selected in the widget |
|
|
302
|
+
| `email` | Always (empty if the form has no email field) |
|
|
303
|
+
| `subscribe`, `subscribeSms` | Always: the opt-in checkboxes (`false` when not shown) |
|
|
304
|
+
| `notifyMe: true` or `comingSoon: true` | Depending on `type` |
|
|
305
|
+
| `mobile`, `firstName`, `lastName` | When those fields are in the form |
|
|
306
|
+
| `countryCode`, `provinceCode` | From the form if shown, otherwise detected |
|
|
307
|
+
| `marketId`, `marketHandle` | When known. `marketId` is always the bare number, never a `gid://` |
|
|
383
308
|
|
|
384
|
-
|
|
309
|
+
Marketing opt-ins are separate from the notify-me registration: shoppers can register without subscribing.
|
|
385
310
|
|
|
386
|
-
|
|
387
|
-
<div id="notification-widget"></div>
|
|
388
|
-
<button id="popup-open" data-fields='["mobile"]' data-type="notify-me">Open Popup</button>
|
|
311
|
+
---
|
|
389
312
|
|
|
390
|
-
|
|
391
|
-
// The script provided will be included here
|
|
392
|
-
</script>
|
|
393
|
-
```
|
|
313
|
+
## Development
|
|
394
314
|
|
|
395
|
-
|
|
315
|
+
TypeScript in `src/`, bundled by Rollup into `build/notify-me-wl.min.js`. The bundle is committed and is what jsDelivr serves from npm.
|
|
396
316
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
317
|
+
```bash
|
|
318
|
+
npm install
|
|
319
|
+
npm run build # build/notify-me-wl.min.js
|
|
320
|
+
npm run dev # rebuild on change
|
|
321
|
+
npm run typecheck
|
|
322
|
+
npm run selftest # build, then run harness/*.html cases in headless Chrome
|
|
323
|
+
npm run visual # build, then pixel-compare default rendering against main
|
|
324
|
+
```
|
|
403
325
|
|
|
404
|
-
|
|
326
|
+
- **`selftest`** runs each page listed in `harness/run-selftest.sh`. Each page defines `window.SELFTEST_CASES`, and `harness/selftest.js` stubs every network call. Add a page to the list when you add one.
|
|
327
|
+
- **`visual`** screenshots the open widget (modal and panel, desktop and mobile, with and without hostile host CSS) using `main`'s bundle and yours, and fails on any pixel difference. It protects every existing install from unintended visual change. Use `BASELINE_REF=<ref>` to compare against another ref.
|
|
328
|
+
- Both scripts need Chrome. Set `CHROME=/path/to/chrome` off macOS.
|
|
329
|
+
- `index.html` is a manual demo page. Add `?mockSubmit=1` to log the payload instead of sending it.
|
|
405
330
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Upgrading
|
|
409
334
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
`
|
|
335
|
+
- **3.6.0:** adds the JSON config block, `copy`, `productSummary`, CSS variables and `classes`. Existing installs need no changes: without the new settings the widget renders and behaves as 3.5.
|
|
336
|
+
- **3.0.0:** the default auth moved from `"token"` to `"proxy"`, and the bundled access token was removed. Set up the App Proxy or supply your own token.
|
|
337
|
+
- **2.0.0:** the bundle moved from `index.js` to `build/notify-me-wl.min.js`.
|
|
338
|
+
- **Legacy `#popup-open` button:** older installs put their attributes on a `<button id="popup-open">`. This still works, and its attributes win over everything else. New installs should use `data-twc-notify-me` triggers instead.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
!function(){"use strict";const e="data-twc-product";function n(e){if(!e||"object"!=typeof e)return!1;const n=e;return("number"==typeof n.id||"string"==typeof n.id)&&"string"==typeof n.title&&(void 0===n.variants||Array.isArray(n.variants))}function t(){const e=new URLSearchParams(window.location.search).get("variant"),n=document.querySelector('form[action*="/cart/add"] select[name="id"]');return[e,n?n.value:null]}const o=[{code:"AF",name:"Afghanistan"},{code:"AX",name:"Åland Islands"},{code:"AL",name:"Albania"},{code:"DZ",name:"Algeria"},{code:"AS",name:"American Samoa"},{code:"AD",name:"Andorra"},{code:"AO",name:"Angola"},{code:"AI",name:"Anguilla"},{code:"AG",name:"Antigua and Barbuda"},{code:"AR",name:"Argentina"},{code:"AM",name:"Armenia"},{code:"AW",name:"Aruba"},{code:"AU",name:"Australia"},{code:"AT",name:"Austria"},{code:"AZ",name:"Azerbaijan"},{code:"BS",name:"Bahamas"},{code:"BH",name:"Bahrain"},{code:"BD",name:"Bangladesh"},{code:"BB",name:"Barbados"},{code:"BY",name:"Belarus"},{code:"BE",name:"Belgium"},{code:"BZ",name:"Belize"},{code:"BJ",name:"Benin"},{code:"BM",name:"Bermuda"},{code:"BT",name:"Bhutan"},{code:"BO",name:"Bolivia"},{code:"BA",name:"Bosnia and Herzegovina"},{code:"BW",name:"Botswana"},{code:"BR",name:"Brazil"},{code:"BN",name:"Brunei"},{code:"BG",name:"Bulgaria"},{code:"BF",name:"Burkina Faso"},{code:"BI",name:"Burundi"},{code:"CV",name:"Cabo Verde"},{code:"KH",name:"Cambodia"},{code:"CM",name:"Cameroon"},{code:"CA",name:"Canada"},{code:"KY",name:"Cayman Islands"},{code:"CF",name:"Central African Republic"},{code:"TD",name:"Chad"},{code:"CL",name:"Chile"},{code:"CN",name:"China"},{code:"CO",name:"Colombia"},{code:"KM",name:"Comoros"},{code:"CG",name:"Congo"},{code:"CD",name:"Congo, DR"},{code:"CK",name:"Cook Islands"},{code:"CR",name:"Costa Rica"},{code:"HR",name:"Croatia"},{code:"CU",name:"Cuba"},{code:"CW",name:"Curaçao"},{code:"CY",name:"Cyprus"},{code:"CZ",name:"Czech Republic"},{code:"DK",name:"Denmark"},{code:"DJ",name:"Djibouti"},{code:"DM",name:"Dominica"},{code:"DO",name:"Dominican Republic"},{code:"EC",name:"Ecuador"},{code:"EG",name:"Egypt"},{code:"SV",name:"El Salvador"},{code:"GQ",name:"Equatorial Guinea"},{code:"ER",name:"Eritrea"},{code:"EE",name:"Estonia"},{code:"SZ",name:"Eswatini"},{code:"ET",name:"Ethiopia"},{code:"FO",name:"Faroe Islands"},{code:"FJ",name:"Fiji"},{code:"FI",name:"Finland"},{code:"FR",name:"France"},{code:"GF",name:"French Guiana"},{code:"PF",name:"French Polynesia"},{code:"GA",name:"Gabon"},{code:"GM",name:"Gambia"},{code:"GE",name:"Georgia"},{code:"DE",name:"Germany"},{code:"GH",name:"Ghana"},{code:"GI",name:"Gibraltar"},{code:"GR",name:"Greece"},{code:"GL",name:"Greenland"},{code:"GD",name:"Grenada"},{code:"GP",name:"Guadeloupe"},{code:"GU",name:"Guam"},{code:"GT",name:"Guatemala"},{code:"GN",name:"Guinea"},{code:"GW",name:"Guinea-Bissau"},{code:"GY",name:"Guyana"},{code:"HT",name:"Haiti"},{code:"HN",name:"Honduras"},{code:"HK",name:"Hong Kong"},{code:"HU",name:"Hungary"},{code:"IS",name:"Iceland"},{code:"IN",name:"India"},{code:"ID",name:"Indonesia"},{code:"IR",name:"Iran"},{code:"IQ",name:"Iraq"},{code:"IE",name:"Ireland"},{code:"IM",name:"Isle of Man"},{code:"IL",name:"Israel"},{code:"IT",name:"Italy"},{code:"JM",name:"Jamaica"},{code:"JP",name:"Japan"},{code:"JE",name:"Jersey"},{code:"JO",name:"Jordan"},{code:"KZ",name:"Kazakhstan"},{code:"KE",name:"Kenya"},{code:"KI",name:"Kiribati"},{code:"KR",name:"Korea, South"},{code:"XK",name:"Kosovo"},{code:"KW",name:"Kuwait"},{code:"KG",name:"Kyrgyzstan"},{code:"LA",name:"Laos"},{code:"LV",name:"Latvia"},{code:"LB",name:"Lebanon"},{code:"LS",name:"Lesotho"},{code:"LR",name:"Liberia"},{code:"LY",name:"Libya"},{code:"LI",name:"Liechtenstein"},{code:"LT",name:"Lithuania"},{code:"LU",name:"Luxembourg"},{code:"MO",name:"Macao"},{code:"MG",name:"Madagascar"},{code:"MW",name:"Malawi"},{code:"MY",name:"Malaysia"},{code:"MV",name:"Maldives"},{code:"ML",name:"Mali"},{code:"MT",name:"Malta"},{code:"MH",name:"Marshall Islands"},{code:"MQ",name:"Martinique"},{code:"MR",name:"Mauritania"},{code:"MU",name:"Mauritius"},{code:"MX",name:"Mexico"},{code:"FM",name:"Micronesia"},{code:"MD",name:"Moldova"},{code:"MC",name:"Monaco"},{code:"MN",name:"Mongolia"},{code:"ME",name:"Montenegro"},{code:"MS",name:"Montserrat"},{code:"MA",name:"Morocco"},{code:"MZ",name:"Mozambique"},{code:"MM",name:"Myanmar"},{code:"NA",name:"Namibia"},{code:"NR",name:"Nauru"},{code:"NP",name:"Nepal"},{code:"NL",name:"Netherlands"},{code:"NC",name:"New Caledonia"},{code:"NZ",name:"New Zealand"},{code:"NI",name:"Nicaragua"},{code:"NE",name:"Niger"},{code:"NG",name:"Nigeria"},{code:"NU",name:"Niue"},{code:"MK",name:"North Macedonia"},{code:"NO",name:"Norway"},{code:"OM",name:"Oman"},{code:"PK",name:"Pakistan"},{code:"PW",name:"Palau"},{code:"PS",name:"Palestine"},{code:"PA",name:"Panama"},{code:"PG",name:"Papua New Guinea"},{code:"PY",name:"Paraguay"},{code:"PE",name:"Peru"},{code:"PH",name:"Philippines"},{code:"PL",name:"Poland"},{code:"PT",name:"Portugal"},{code:"PR",name:"Puerto Rico"},{code:"QA",name:"Qatar"},{code:"RE",name:"Réunion"},{code:"RO",name:"Romania"},{code:"RW",name:"Rwanda"},{code:"KN",name:"Saint Kitts and Nevis"},{code:"LC",name:"Saint Lucia"},{code:"VC",name:"Saint Vincent and the Grenadines"},{code:"WS",name:"Samoa"},{code:"SM",name:"San Marino"},{code:"SA",name:"Saudi Arabia"},{code:"SN",name:"Senegal"},{code:"RS",name:"Serbia"},{code:"SC",name:"Seychelles"},{code:"SL",name:"Sierra Leone"},{code:"SG",name:"Singapore"},{code:"SK",name:"Slovakia"},{code:"SI",name:"Slovenia"},{code:"SB",name:"Solomon Islands"},{code:"SO",name:"Somalia"},{code:"ZA",name:"South Africa"},{code:"SS",name:"South Sudan"},{code:"ES",name:"Spain"},{code:"LK",name:"Sri Lanka"},{code:"SD",name:"Sudan"},{code:"SR",name:"Suriname"},{code:"SE",name:"Sweden"},{code:"CH",name:"Switzerland"},{code:"SY",name:"Syria"},{code:"TW",name:"Taiwan"},{code:"TJ",name:"Tajikistan"},{code:"TZ",name:"Tanzania"},{code:"TH",name:"Thailand"},{code:"TL",name:"Timor-Leste"},{code:"TG",name:"Togo"},{code:"TO",name:"Tonga"},{code:"TT",name:"Trinidad and Tobago"},{code:"TN",name:"Tunisia"},{code:"TR",name:"Turkey"},{code:"TM",name:"Turkmenistan"},{code:"TC",name:"Turks and Caicos Islands"},{code:"TV",name:"Tuvalu"},{code:"UG",name:"Uganda"},{code:"UA",name:"Ukraine"},{code:"AE",name:"United Arab Emirates"},{code:"GB",name:"United Kingdom"},{code:"US",name:"United States"},{code:"UY",name:"Uruguay"},{code:"UZ",name:"Uzbekistan"},{code:"VU",name:"Vanuatu"},{code:"VE",name:"Venezuela"},{code:"VN",name:"Vietnam"},{code:"VG",name:"Virgin Islands, British"},{code:"VI",name:"Virgin Islands, U.S."},{code:"YE",name:"Yemen"},{code:"ZM",name:"Zambia"},{code:"ZW",name:"Zimbabwe"}];let a=null;async function c(e){if("proxy"===e.mode){const n=await async function(e){if(a)return a;try{const n=await fetch(`/apps/${e}/auth/token`,{method:"GET",headers:{Accept:"application/json"}}),t=await n.json().catch(()=>null);return n.ok&&t&&t.success&&t.data&&t.data.access_token?(a=t.data.access_token,a):null}catch(e){return console.error("Error fetching proxy access token:",e),null}}(e.proxyApp);return n?`Bearer ${n}`:null}return e.accessToken?/^Bearer /i.test(e.accessToken)?e.accessToken:`Bearer ${e.accessToken}`:(console.error('notify-me-wl: data-auth="token" requires an access token. Set data-access-token on the open button or window.TWC_ACCESS_TOKEN, or use data-auth="proxy" to obtain one via the Shopify App Proxy.'),null)}const r=new Map;function i(e){return"string"==typeof e?e.trim():""}async function d(e,n,t){const o=`${e}|${t||""}`,a=r.get(o);if(void 0!==a)return a;const d=await c(n);if(!d)return null;try{const n=await fetch("https://api.au-aws.thewishlist.io/services/eventcollector/api/v1/custom/configs/public",{method:"GET",headers:{Accept:"application/json",Authorization:d,"X-Twc-Tenant":e}});if(!n.ok)return console.error("Tenant config request failed:",n.status),null;const a=function(e,n=null){if(!e||"object"!=typeof e)return null;const t=e.websiteRecommendations;if(!t||"object"!=typeof t)return null;const o=i(t.engine).toUpperCase();if("TWC"===o)return{engine:"TWC"};if("ATHOS"===o){const e=i(t.siteIdentifier);return e?{engine:"ATHOS",siteIdentifier:e,profileTag:i(t.profileTag)||i(t.tags)||i(n)||"similar"}:null}return null}(await n.json().catch(()=>null),t);return r.set(o,a),a}catch(e){return console.error("Error fetching tenant config:",e),null}}function s(e){return"string"==typeof e?e.trim():""}function m(e){if("number"==typeof e&&Number.isFinite(e))return e;if("string"==typeof e){const n=parseFloat(e);if(Number.isFinite(n))return n}return null}function l(e){return Boolean(e.name&&e.imageUrl&&e.productUrl)}function u(e,n){const t=new Map;return e.forEach(e=>{const n=t.get(e.product.ref);n&&n.score>=e.score||t.set(e.product.ref,e)}),Array.from(t.values()).sort((e,n)=>n.score-e.score).slice(0,n).map(e=>e.product)}async function p(e){const{email:n,tenant:t,auth:o,count:a}=e,r=await c(o);if(!r)return null;const i=Math.min(3*a,20),d=`https://api.au-aws.thewishlist.io/services/recommendations/api/v1/recommendations/${encodeURIComponent(t)}/${encodeURIComponent(n)}?n=${i}`;try{const e=await fetch(d,{method:"GET",headers:{Accept:"application/json",Authorization:r,"X-Twc-Tenant":t}});if(!e.ok)return console.error("Recommendations request failed:",e.status),null;return function(e,n){const t=e&&e.recommendations;if(!Array.isArray(t))return[];const o=[];return t.forEach(e=>{var n;const t=e&&e.product;if(!t)return;const a=s(t.name),c=s(t.image_url),r=s(t.product_url);if(!l({name:a,imageUrl:c,productUrl:r}))return;const i=s(t.product_ref)||s(t.product_id)||r;o.push({score:"number"==typeof e.score?e.score:0,product:{id:s(t.product_id)||i,ref:i,name:a,imageUrl:c,productUrl:r,price:null!==(n=m(t.price))&&void 0!==n?n:0,originalPrice:m(t.original_price)}})}),u(o,n)}(await e.json().catch(()=>null),a)}catch(e){return console.error("Error fetching recommendations:",e),null}}async function f(e){const{engine:n,email:t,count:o,productId:a}=e,c=encodeURIComponent(n.siteIdentifier),r=[`tags=${encodeURIComponent(n.profileTag)}`,`limits=${o}`];a&&r.push(`products=${encodeURIComponent(a)}`),t&&r.push(`shopper=${encodeURIComponent(t)}`);const i=`https://${c}.a.searchspring.io/boost/${c}/recommend?`+r.join("&");try{const e=await fetch(i,{method:"GET",headers:{Accept:"application/json"}});if(!e.ok)return console.error("Athos recommendations request failed:",e.status),null;return function(e,n){const t=Array.isArray(e)?e[0]:e,o=t&&t.results;if(!Array.isArray(o))return[];const a=[];return o.forEach((e,n)=>{var t;const c=e&&e.mappings&&e.mappings.core;if(!c)return;const r=s(c.name),i=s(c.imageUrl)||s(c.thumbnailImageUrl),d=s(c.url);if(!l({name:r,imageUrl:i,productUrl:d}))return;const u=s(c.uid)||s(c.sku)||d,p=null!==(t=m(c.price))&&void 0!==t?t:0,f=m(c.msrp);a.push({score:o.length-n,product:{id:s(c.uid)||u,ref:u,name:r,imageUrl:i,productUrl:d,price:p,originalPrice:null!==f&&f>p?f:null}})}),u(a,n)}(await e.json().catch(()=>null),o)}catch(e){return console.error("Error fetching Athos recommendations:",e),null}}const w=new Map;const h="twc-nm-recs",y="twc-nm-recs-title";function g(e,n){if(n)try{return new Intl.NumberFormat(void 0,{style:"currency",currency:n}).format(e)}catch(e){}return new Intl.NumberFormat(void 0,{minimumFractionDigits:0,maximumFractionDigits:2}).format(e)}function v(){const e=document.createElement("section");e.id=h,e.className="twc-nm-recs",e.setAttribute("aria-labelledby",y);const n=document.createElement("h3");n.id=y,n.className="twc-nm-recs-title",n.textContent="Shop similar styles",e.appendChild(n);const t=document.createElement("div");return t.className="twc-nm-recs-row",function(e){let n=0,t=0,o=!1,a=!1,c=!1;e.addEventListener("pointerdown",r=>{"mouse"===r.pointerType&&0===r.button&&(o=!0,a=!1,c=!1,n=r.clientX,t=e.scrollLeft,e.classList.add("twc-nm-recs-row--dragging"))}),e.addEventListener("pointermove",c=>{if(!o)return;const r=c.clientX-n;!a&&Math.abs(r)<5||(a=!0,e.scrollLeft=t-r,c.preventDefault())});const r=()=>{o&&(o=!1,c=a,e.classList.remove("twc-nm-recs-row--dragging"))};e.addEventListener("pointerup",r),e.addEventListener("pointercancel",r),e.addEventListener("pointerleave",r),e.addEventListener("click",e=>{c&&(c=!1,e.preventDefault(),e.stopPropagation())},!0),e.addEventListener("dragstart",e=>{o&&a&&e.preventDefault()})}(t),e.appendChild(t),{section:e,row:t}}function b(){const e=document.createElement("div");e.className="twc-nm-rec twc-nm-rec--skeleton",e.setAttribute("aria-hidden","true");const n=document.createElement("div");n.className="twc-nm-rec-imgbox twc-nm-skel",e.appendChild(n);const t=document.createElement("span");t.className="twc-nm-skel twc-nm-skel-line",e.appendChild(t);const o=document.createElement("span");return o.className="twc-nm-skel twc-nm-skel-line twc-nm-skel-line--short",e.appendChild(o),e}function x(e,n,t){const o=e.querySelector(".twc-nm-recs-row");if(!o)return;const a=(c=t,window.Shopify&&window.Shopify.currency&&window.Shopify.currency.active||c||null);var c;const r=document.createDocumentFragment();n.forEach(e=>{r.appendChild(function(e,n){const t=document.createElement("a");t.className="twc-nm-rec",t.href=e.productUrl,t.target="_blank",t.rel="noopener noreferrer";const o=document.createElement("div");o.className="twc-nm-rec-imgbox";const a=document.createElement("img");a.className="twc-nm-rec-img",a.src=e.imageUrl,a.alt=e.name,a.loading="lazy",a.decoding="async",o.appendChild(a),t.appendChild(o);const c=document.createElement("span");c.className="twc-nm-rec-name",c.textContent=e.name,t.appendChild(c);const r=document.createElement("span");if(r.className="twc-nm-rec-price",null!==e.originalPrice&&e.originalPrice>e.price){const t=document.createElement("s");t.className="twc-nm-rec-was",t.textContent=g(e.originalPrice,n),r.appendChild(t),r.appendChild(document.createTextNode(" "))}return r.appendChild(document.createTextNode(g(e.price,n))),t.appendChild(r),t}(e,a))}),o.textContent="",o.appendChild(r),e.classList.remove("twc-nm-recs--loading"),e.removeAttribute("aria-busy")}function C(){const e=document.getElementById(h);e&&e.parentNode&&e.parentNode.removeChild(e)}const k={"notify-me":{text:"Register to receive a notification as soon as this item is back in stock",buttonText:"Notify Me"},"coming-soon":{text:"Register your interest to hear more about this item",buttonText:"Register Interest"}};function S(a){const{wrapper:r,config:i,localization:s}=a,{fields:m,type:l,display:u,auth:y,tenant:g,recommendationsEnabled:S,recommendationsCount:E,recommendationsProfile:T,currency:A,customerEmail:M}=i,N=document.createElement("div");N.innerHTML='\n <div id="twc-nm-overlay" role="dialog" aria-modal="true" aria-labelledby="popup-title">\n <div class="twc-nm-card">\n <button type="button" id="popup-close" class="twc-nm-close" aria-label="Close"><svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" focusable="false"><path d="M3 3l10 10M13 3L3 13" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" fill="none"/></svg></button>\n <h2 id="popup-title" class="twc-nm-title"></h2>\n <p id="popup-text" class="twc-nm-text"></p>\n <div id="popup-notice" class="twc-nm-status" role="status" aria-live="polite" hidden></div>\n <form id="popup-form" class="twc-nm-form">\n <div class="twc-nm-field">\n <label class="twc-nm-label" for="twc-nm-size">Size</label>\n <select class="twc-nm-select" id="twc-nm-size" name="select-size" required>\n <option value="" disabled selected>Select a size</option>\n </select>\n </div>\n </form>\n </div>\n </div>\n ',r.appendChild(N);const L=document.getElementById("twc-nm-overlay"),I=document.getElementById("popup-close"),B=document.getElementById("popup-title"),G=document.getElementById("popup-text"),P=document.querySelector("select[name='select-size']"),U=document.getElementById("popup-form"),z=document.getElementById("popup-notice");if(!(L&&I&&B&&G&&P&&U&&z))return null;"panel"===u&&L.classList.add("twc-nm--panel"),G.innerText=k[l].text;const R=o.map(e=>`<option value="${e.code}"${e.code===s.countryCode?" selected":""}>${e.name}</option>`).join(""),$=(e,n,t)=>`<div class="twc-nm-field"><label class="twc-nm-label" for="${e}">${n}</label>${t}</div>`,H={email:$("twc-nm-email","Email",'<input class="twc-nm-input" id="twc-nm-email" name="email" type="email" placeholder="you@example.com" required />'),mobile:$("twc-nm-mobile","Mobile",'<input class="twc-nm-input" id="twc-nm-mobile" name="mobile" type="tel" placeholder="Mobile number" required />'),firstName:$("twc-nm-firstname","First name",'<input class="twc-nm-input" id="twc-nm-firstname" name="firstName" type="text" placeholder="First name" required />'),lastName:$("twc-nm-lastname","Last name",'<input class="twc-nm-input" id="twc-nm-lastname" name="lastName" type="text" placeholder="Last name" required />'),countryCode:$("twc-nm-country","Country",`<select class="twc-nm-select" id="twc-nm-country" name="countryCode"><option value="">Select country</option>${R}</select>`),provinceCode:$("twc-nm-province","State / Province",`<input class="twc-nm-input" id="twc-nm-province" name="provinceCode" type="text" placeholder="State / province code" value="${s.provinceCode||""}" />`)};if(m.forEach(e=>{H[e]&&U.insertAdjacentHTML("beforeend",H[e])}),U.insertAdjacentHTML("beforeend",'<div class="twc-nm-checks">\n <label class="twc-nm-check"><input type="checkbox" name="mailList" /> Email me store updates</label>\n <label class="twc-nm-check"><input type="checkbox" name="mailListSms" /> Text me store updates</label>\n </div>'),U.insertAdjacentHTML("beforeend",'<div id="popup-status" class="twc-nm-status" role="status" aria-live="polite" hidden></div>'),U.insertAdjacentHTML("beforeend",`<button type="submit" class="twc-nm-submit">${k[l].buttonText}</button>`),M){const e=U.querySelector("input[name='email']");e&&(e.value=M)}const K=document.getElementById("popup-status"),D=U.querySelector(".twc-nm-submit");if(!K||!D)return null;function F(e,n){K.textContent=n,K.className=`twc-nm-status twc-nm-status--${e}`,K.hidden=!1}function j(){K.hidden=!0,K.textContent=""}function O(e,n){U.hidden=!0,z.textContent=n,z.className=`twc-nm-status twc-nm-status--${e}`,z.hidden=!1}const q=e=>{const n=U.querySelector(e);return n?n.value:""},V=e=>{const n=U.querySelector(e);return!!n&&n.checked},_=L.querySelector(".twc-nm-card");let J=null,W=[],Z=null;async function Y(e){const n=await function(e,n,t){return d(e,n,t)}(g,y,T);if(!n)return!1;if(!L.classList.contains("is-open"))return!1;const t=function(e){const{section:n,row:t}=v();n.classList.add("twc-nm-recs--loading"),n.setAttribute("aria-busy","true");for(let n=0;n<e;n++)t.appendChild(b());return n}(E);_.appendChild(t);const o=await async function(e,n){const{email:t,tenant:o,auth:a,count:c,productId:r}=n,i=`${e.engine}|${o}|${t}|${c}|${r||""}`,d=w.get(i);if(d)return d;let s=null;return s="ATHOS"===e.engine?await f({engine:e,email:t,count:c,productId:r}):await p({email:t,tenant:o,auth:a,count:c}),null===s?[]:(w.set(i,s),s)}(n,{email:e,tenant:g,auth:y,count:E,productId:J?String(J.id):null});return!!t.isConnected&&(o.length?(x(t,o,A),!0):(C(),!1))}function X(e){if(!S||!_)return Promise.resolve(!1);if(Z)return Z;if(document.getElementById(h))return Promise.resolve(!0);const n=Y(e);return Z=n,n.then(e=>(Z===n&&(Z=null),e),e=>(Z===n&&(Z=null),console.error("Error rendering recommendations:",e),!1))}let Q=null,ee=0;function ne(e){"Escape"===e.key&&te()}function te(){ee++,L.classList.remove("is-open"),document.body.style.overflow="",document.removeEventListener("keydown",ne),j(),C(),Z=null,Q&&"function"==typeof Q.focus&&Q.focus()}return I.addEventListener("click",te),L.addEventListener("click",e=>{e.target===L&&te()}),U.addEventListener("submit",async e=>{e.preventDefault();const n=W.find(e=>String(e.id)===P.value);if(!n)return void F("error","Please select a size.");const t={variantRef:n.id,email:q("input[name='email']"),subscribe:V("input[name='mailList']"),subscribeSms:V("input[name='mailListSms']")};if("coming-soon"===l?t.comingSoon=!0:t.notifyMe=!0,m.includes("firstName")&&(t.firstName=q("input[name='firstName']")),m.includes("lastName")&&(t.lastName=q("input[name='lastName']")),m.includes("mobile")&&(t.mobile=q("input[name='mobile']")),m.includes("countryCode")){const e=q("select[name='countryCode']");e&&(t.countryCode=e)}else s.countryCode&&(t.countryCode=s.countryCode);if(m.includes("provinceCode")){const e=q("input[name='provinceCode']");e&&(t.provinceCode=e)}else s.provinceCode&&(t.provinceCode=s.provinceCode);s.marketId&&(t.marketId=s.marketId),s.marketHandle&&(t.marketHandle=s.marketHandle),j();const o=ee,a=D.textContent||"";D.disabled=!0,D.textContent="Sending…";let r={ok:!1,reason:"error"};try{r=await async function(e,n,t){const o=await c(n);if(!o)return{ok:!1,reason:"auth"};try{return(await fetch("https://api.au-aws.thewishlist.io/services/wsservice/api/wishlist/items/customerInterest",{method:"POST",headers:{"Content-Type":"application/json",Authorization:o,"X-Twc-Tenant":t},body:JSON.stringify(e)})).ok?{ok:!0}:{ok:!1,reason:"error"}}catch(e){return console.error("Error submitting form:",e),{ok:!1,reason:"error"}}}(t,y,g)}finally{o===ee&&(D.disabled=!1,D.textContent=a)}if(o===ee)if(r.ok){F("success","You're on the list. We'll be in touch.");const e=M||q("input[name='email']");!!e&&await X(e)||o!==ee||setTimeout(()=>{o===ee&&te()},1500)}else"auth"===r.reason?F("error","proxy"===y.mode?"Please log in to your account to continue.":"Something went wrong. Please try again."):F("error","Something went wrong. Please try again.")}),{open:function(o){ee++,D.disabled=!1,D.textContent=k[l].buttonText,j(),U.hidden=!1,z.hidden=!0,z.textContent="",C(),Z=null,L.classList.contains("is-open")||(Q=document.activeElement),L.classList.add("is-open"),document.body.style.overflow="hidden",document.addEventListener("keydown",ne);const a=function(t){const o=t?t.closest(`[${e}]`):null;if(o){try{const t=JSON.parse(o.getAttribute(e)||"");if(n(t))return{ok:!0,product:t,source:"card"}}catch(e){}return console.error(`notify-me-wl: invalid ${e} JSON on`,o),{ok:!1}}const a=window.currentProduct;return n(a)?{ok:!0,product:a,source:"page"}:{ok:!1}}(o);if(!a.ok)return J=null,W=[],B.textContent="",void O("error","Product information is unavailable. Please refresh the page.");if(J=a.product,W=(J.variants||[]).filter(e=>!!e&&("number"==typeof e.id||"string"==typeof e.id)&&"string"==typeof e.title&&!0!==e.available),B.textContent=J.title,!W.length)return void O("info","All sizes are currently in stock.");const c=o?o.getAttribute("data-variant-id"):null,r="page"===a.source?[c,...t()]:[c];var i,d;i=W,d=function(e,n){const t=e.map(e=>String(e.id));for(const e of n){const n=e?e.trim():"";if(n&&t.includes(n))return n}return 1===t.length?t[0]:null}(W,r),P.innerHTML='<option value="" disabled selected>Select a size</option>',i.forEach(e=>{const n=document.createElement("option");n.value=String(e.id),n.textContent=e.title,P.appendChild(n)}),d&&(P.value=d),M&&X(M);const s=U.querySelector("select, input");s&&s.focus()}}}function E(e){var n;const t=(null!=e?e:"").trim();if(!t)return null;const o=null!==(n=t.split("?")[0].split("/").pop())&&void 0!==n?n:"";return/^\d+$/.test(o)?o:null}function T(e){const n=(null!=e?e:"").trim();return/^[a-z0-9_-]+$/i.test(n)?n:null}function A(e){const n=(null!=e?e:"").trim();return/^[a-z]{2}$/i.test(n)?n.toUpperCase():null}function M(e){var n,t,o;const a={countryCode:A(e("data-country-code")),provinceCode:null,marketId:E(e("data-market-id")),marketHandle:T(e("data-market-handle"))},c=window.Shopify;if(c){const e=null===(n=c.markets)||void 0===n?void 0:n.currentMarket;if(e&&(!a.countryCode&&e.countryCode&&(a.countryCode=e.countryCode),a.marketId||(a.marketId=E(String(null!==(t=e.id)&&void 0!==t?t:"")))),a.countryCode||"string"!=typeof c.country||(a.countryCode=c.country),!a.countryCode&&"string"==typeof c.locale){const e=c.locale.split("-");e.length>1&&(a.countryCode=e[e.length-1].toUpperCase())}}if(!a.countryCode||!a.provinceCode){const e=null===(o=window.customer)||void 0===o?void 0:o.default_address;e&&(!a.countryCode&&e.country_code&&(a.countryCode=e.country_code),!a.provinceCode&&e.province_code&&(a.provinceCode=e.province_code))}if(!a.countryCode){const e=(navigator.language||"").split("-");e.length>1&&(a.countryCode=e[e.length-1].toUpperCase())}return a}function N(e){const n=e("data-customer-email");if(n&&n.trim())return n.trim();const t=window.customer&&window.customer.email;if(t&&t.trim())return t.trim();const o=window.customerEmail;return"string"==typeof o&&o.trim()?o.trim():null}const L="[data-twc-notify-me]";function I(){const e=document.getElementById("notification-widget");if(!e)return;const n=document.getElementById("popup-open"),t=(o=n,a=e,e=>o&&o.hasAttribute(e)?o.getAttribute(e):a.getAttribute(e));var o,a;!function(){const e=document.createElement("style");e.type="text/css",e.innerText="\n .notification-btn {\n display: inline-block;\n width: 100%;\n padding: 12px 24px;\n background: #fff;\n border: 1px solid #111827;\n border-radius: 8px;\n color: #111827;\n font: inherit;\n text-align: center;\n text-transform: uppercase;\n cursor: pointer;\n }\n .notification-btn:hover {\n background: #111827;\n color: #fff;\n }\n #twc-nm-overlay {\n position: fixed;\n inset: 0;\n z-index: 99999;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n background: rgba(17, 17, 17, 0.55);\n opacity: 0;\n visibility: hidden;\n transition: opacity 200ms ease;\n }\n #twc-nm-overlay.is-open {\n opacity: 1;\n visibility: visible;\n }\n #twc-nm-overlay * {\n box-sizing: border-box;\n }\n #twc-nm-overlay .twc-nm-card {\n position: relative;\n width: 100%;\n max-width: 400px;\n max-height: calc(100vh - 32px);\n overflow-y: auto;\n padding: 24px;\n background: #fff;\n color: #111827;\n border-radius: 12px;\n box-shadow: 0 10px 40px rgba(0, 0, 0, 0.18);\n font: inherit;\n transform: translateY(8px);\n transition: transform 200ms ease;\n }\n #twc-nm-overlay.is-open .twc-nm-card {\n transform: translateY(0);\n }\n /* Panel mode: same blocking backdrop, docked to the right edge and\n full height instead of centred. Declared after the modal rules so\n the shared properties are overridden. */\n #twc-nm-overlay.twc-nm--panel {\n align-items: stretch;\n justify-content: flex-end;\n padding: 0;\n }\n #twc-nm-overlay.twc-nm--panel .twc-nm-card {\n width: 420px;\n max-width: 100%;\n max-height: none;\n border-radius: 0;\n box-shadow: -10px 0 40px rgba(0, 0, 0, 0.18);\n transform: translateX(100%);\n }\n /* Needs the extra class to beat both the .is-open rule above and the\n panel's own resting transform. */\n #twc-nm-overlay.twc-nm--panel.is-open .twc-nm-card {\n transform: translateX(0);\n }\n @media (max-width: 480px) {\n #twc-nm-overlay.twc-nm--panel .twc-nm-card {\n width: 100%;\n }\n }\n #twc-nm-overlay .twc-nm-close {\n position: absolute;\n top: 12px;\n right: 12px;\n width: 32px;\n height: 32px;\n min-width: 0;\n min-height: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0;\n padding: 0;\n border: none;\n background: transparent;\n box-shadow: none;\n border-radius: 6px;\n text-indent: 0;\n line-height: 1;\n color: #6b7280;\n opacity: 1;\n visibility: visible;\n cursor: pointer;\n }\n /* The icon is an inline SVG rather than a text glyph so host-theme\n button resets (font-size: 0, icon fonts, text-indent) can't hide it. */\n #twc-nm-overlay .twc-nm-close svg {\n display: block;\n width: 16px;\n height: 16px;\n flex: none;\n pointer-events: none;\n }\n #twc-nm-overlay .twc-nm-close::before,\n #twc-nm-overlay .twc-nm-close::after {\n content: none;\n }\n #twc-nm-overlay .twc-nm-close:hover {\n background: #f3f4f6;\n color: #111827;\n }\n #twc-nm-overlay .twc-nm-title {\n margin: 0 32px 4px 0;\n font-size: 18px;\n font-weight: 600;\n }\n #twc-nm-overlay .twc-nm-text {\n margin: 0 0 16px;\n font-size: 14px;\n line-height: 1.5;\n color: #6b7280;\n }\n #twc-nm-overlay .twc-nm-form {\n display: flex;\n flex-direction: column;\n gap: 14px;\n }\n #twc-nm-overlay .twc-nm-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n #twc-nm-overlay .twc-nm-label {\n font-size: 13px;\n font-weight: 500;\n color: #374151;\n }\n #twc-nm-overlay .twc-nm-input,\n #twc-nm-overlay .twc-nm-select {\n width: 100%;\n padding: 10px 12px;\n font-size: 14px;\n font-family: inherit;\n color: #111827;\n background: #fff;\n border: 1px solid #e5e7eb;\n border-radius: 8px;\n }\n #twc-nm-overlay .twc-nm-input:focus,\n #twc-nm-overlay .twc-nm-select:focus {\n outline: none;\n border-color: #111827;\n box-shadow: 0 0 0 3px rgba(17, 24, 39, 0.08);\n }\n #twc-nm-overlay .twc-nm-select {\n appearance: none;\n -webkit-appearance: none;\n padding-right: 36px;\n cursor: pointer;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2 4l4 4 4-4' stroke='%236b7280' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 12px center;\n }\n #twc-nm-overlay .twc-nm-checks {\n display: flex;\n flex-direction: column;\n gap: 10px;\n }\n #twc-nm-overlay .twc-nm-check {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 13px;\n color: #374151;\n cursor: pointer;\n }\n #twc-nm-overlay .twc-nm-check input {\n width: 16px;\n height: 16px;\n margin: 0;\n accent-color: #111827;\n }\n #twc-nm-overlay .twc-nm-status {\n font-size: 13px;\n line-height: 1.5;\n padding: 10px 12px;\n border-radius: 8px;\n }\n #twc-nm-overlay .twc-nm-status--error {\n color: #b91c1c;\n background: #fef2f2;\n }\n #twc-nm-overlay .twc-nm-status--success {\n color: #15803d;\n background: #f0fdf4;\n }\n #twc-nm-overlay .twc-nm-status--info {\n color: #374151;\n background: #f3f4f6;\n }\n #twc-nm-overlay .twc-nm-submit {\n width: 100%;\n padding: 12px 16px;\n font-size: 14px;\n font-weight: 600;\n font-family: inherit;\n color: #fff;\n background: #111827;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n transition: background 150ms ease;\n }\n #twc-nm-overlay .twc-nm-submit:hover {\n background: #374151;\n }\n #twc-nm-overlay .twc-nm-submit:disabled {\n opacity: 0.6;\n cursor: default;\n }\n #twc-nm-overlay .twc-nm-recs {\n margin-top: 20px;\n padding-top: 20px;\n border-top: 1px solid #e5e7eb;\n }\n #twc-nm-overlay .twc-nm-recs-title {\n margin: 0 0 12px;\n font-size: 14px;\n font-weight: 600;\n color: #111827;\n }\n #twc-nm-overlay .twc-nm-recs-row {\n display: flex;\n gap: 12px;\n overflow-x: auto;\n scroll-snap-type: x mandatory;\n -webkit-overflow-scrolling: touch;\n padding-bottom: 8px;\n scrollbar-width: thin;\n }\n /* Only where a real pointer exists — the affordance is meaningless\n on touch, which pans the row natively. */\n @media (hover: hover) {\n #twc-nm-overlay .twc-nm-recs-row {\n cursor: grab;\n }\n }\n #twc-nm-overlay .twc-nm-recs-row--dragging,\n #twc-nm-overlay .twc-nm-recs-row--dragging .twc-nm-rec {\n cursor: grabbing;\n }\n #twc-nm-overlay .twc-nm-recs-row--dragging {\n /* Snapping fights a manually driven scrollLeft and feels sticky. */\n scroll-snap-type: none;\n user-select: none;\n -webkit-user-select: none;\n }\n #twc-nm-overlay .twc-nm-rec {\n flex: 0 0 132px;\n scroll-snap-align: start;\n display: flex;\n flex-direction: column;\n gap: 6px;\n color: inherit;\n text-decoration: none;\n }\n #twc-nm-overlay .twc-nm-rec:focus-visible {\n outline: 2px solid #111827;\n outline-offset: 2px;\n border-radius: 8px;\n }\n #twc-nm-overlay .twc-nm-rec-imgbox {\n aspect-ratio: 3 / 4;\n overflow: hidden;\n border-radius: 8px;\n background: #f3f4f6;\n }\n #twc-nm-overlay .twc-nm-rec-img {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: cover;\n transition: opacity 150ms ease;\n }\n #twc-nm-overlay .twc-nm-rec:hover .twc-nm-rec-img {\n opacity: 0.85;\n }\n #twc-nm-overlay .twc-nm-rec-name {\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n font-size: 13px;\n line-height: 1.35;\n color: #111827;\n }\n #twc-nm-overlay .twc-nm-rec-price {\n font-size: 13px;\n font-weight: 600;\n color: #111827;\n }\n #twc-nm-overlay .twc-nm-rec-was {\n margin-right: 4px;\n font-weight: 400;\n color: #9ca3af;\n }\n #twc-nm-overlay .twc-nm-rec--skeleton {\n pointer-events: none;\n }\n #twc-nm-overlay .twc-nm-skel {\n background-color: #f3f4f6;\n background-image: linear-gradient(\n 90deg,\n #f3f4f6 0%,\n #e8eaee 50%,\n #f3f4f6 100%\n );\n background-size: 200% 100%;\n border-radius: 4px;\n animation: twc-nm-shimmer 1.3s ease-in-out infinite;\n }\n #twc-nm-overlay .twc-nm-rec-imgbox.twc-nm-skel {\n border-radius: 8px;\n }\n #twc-nm-overlay .twc-nm-skel-line {\n height: 12px;\n width: 100%;\n }\n #twc-nm-overlay .twc-nm-skel-line--short {\n width: 45%;\n }\n @keyframes twc-nm-shimmer {\n from {\n background-position: 200% 0;\n }\n to {\n background-position: -200% 0;\n }\n }\n @media (prefers-reduced-motion: reduce) {\n #twc-nm-overlay,\n #twc-nm-overlay .twc-nm-card,\n #twc-nm-overlay .twc-nm-rec-img {\n transition: none;\n }\n #twc-nm-overlay .twc-nm-card,\n #twc-nm-overlay.twc-nm--panel .twc-nm-card {\n transform: none;\n }\n #twc-nm-overlay .twc-nm-skel {\n animation: none;\n }\n }\n ",document.head.appendChild(e)}();const c=JSON.parse(t("data-fields")||'["email"]'),r=t("data-type")||"notify-me",i="panel"===t("data-display")?"panel":"modal",d=t("data-auth")||"proxy",s=t("data-tenant")||"victoria-woods",m=t("data-proxy-app")||"twc-sdk",l=t("data-access-token")||window.TWC_ACCESS_TOKEN||null,u="false"!==t("data-recommendations"),p=parseInt(t("data-recommendations-count")||"",10),f=S({wrapper:e,config:{fields:c,type:r,display:i,auth:{mode:d,proxyApp:m,accessToken:l},tenant:s,recommendationsEnabled:u,recommendationsCount:Number.isFinite(p)&&p>0?p:8,recommendationsProfile:(t("data-recommendations-profile")||"").trim()||null,currency:t("data-currency")||null,customerEmail:N(t)},localization:M(t)});f&&(document.addEventListener("click",e=>{const n=e.target,t=n instanceof Element?n.closest(L):null;t&&(e.preventDefault(),f.open(t))},!0),n&&!n.matches(L)&&n.addEventListener("click",()=>f.open(n)))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",I):I()}();
|
|
1
|
+
!function(){"use strict";const e="data-twc-product";function n(e){if(!e||"object"!=typeof e)return!1;const n=e;return("number"==typeof n.id||"string"==typeof n.id)&&"string"==typeof n.title&&(void 0===n.variants||Array.isArray(n.variants))}function t(){const e=new URLSearchParams(window.location.search).get("variant"),n=document.querySelector('form[action*="/cart/add"] select[name="id"]');return[e,n?n.value:null]}function o(e){return e.replace(/^data-/,"").replace(/-([a-z])/g,(e,n)=>n.toUpperCase())}function a(e,n){console.warn(`notify-me-wl: ignoring data-twc-config "${e}": expected ${n}`)}function r(e){return Array.isArray(e)&&e.every(e=>"string"==typeof e)}const c={title:".twc-nm-title",intro:".twc-nm-text",label:".twc-nm-label",input:".twc-nm-input",select:".twc-nm-select",checkbox:".twc-nm-check input",submit:".twc-nm-submit",close:".twc-nm-close",disclaimer:".twc-nm-disclaimer",summary:".twc-nm-summary",summaryImage:".twc-nm-summary-image",summaryName:".twc-nm-summary-name",summaryVariant:".twc-nm-summary-variant",summaryPrice:".twc-nm-summary-price",summaryCompareAt:".twc-nm-summary-compare"},i=/^[A-Za-z_-][A-Za-z0-9_-]*$/;function m(e){const n={};if(null==e)return n;if("object"!=typeof e||Array.isArray(e))return a("classes","an object"),n;const t=e;return Object.keys(c).forEach(e=>{const o=t[e];if(null==o)return;if("string"!=typeof o)return void a(`classes.${e}`,"a string");const r=o.split(/\s+/).filter(Boolean),c=r.filter(e=>i.test(e));c.length<r.length&&a(`classes.${e}`,'class names of letters, digits, "-" and "_"'),c.length&&(n[e]=c)}),n}const s=[{code:"AF",name:"Afghanistan"},{code:"AX",name:"Åland Islands"},{code:"AL",name:"Albania"},{code:"DZ",name:"Algeria"},{code:"AS",name:"American Samoa"},{code:"AD",name:"Andorra"},{code:"AO",name:"Angola"},{code:"AI",name:"Anguilla"},{code:"AG",name:"Antigua and Barbuda"},{code:"AR",name:"Argentina"},{code:"AM",name:"Armenia"},{code:"AW",name:"Aruba"},{code:"AU",name:"Australia"},{code:"AT",name:"Austria"},{code:"AZ",name:"Azerbaijan"},{code:"BS",name:"Bahamas"},{code:"BH",name:"Bahrain"},{code:"BD",name:"Bangladesh"},{code:"BB",name:"Barbados"},{code:"BY",name:"Belarus"},{code:"BE",name:"Belgium"},{code:"BZ",name:"Belize"},{code:"BJ",name:"Benin"},{code:"BM",name:"Bermuda"},{code:"BT",name:"Bhutan"},{code:"BO",name:"Bolivia"},{code:"BA",name:"Bosnia and Herzegovina"},{code:"BW",name:"Botswana"},{code:"BR",name:"Brazil"},{code:"BN",name:"Brunei"},{code:"BG",name:"Bulgaria"},{code:"BF",name:"Burkina Faso"},{code:"BI",name:"Burundi"},{code:"CV",name:"Cabo Verde"},{code:"KH",name:"Cambodia"},{code:"CM",name:"Cameroon"},{code:"CA",name:"Canada"},{code:"KY",name:"Cayman Islands"},{code:"CF",name:"Central African Republic"},{code:"TD",name:"Chad"},{code:"CL",name:"Chile"},{code:"CN",name:"China"},{code:"CO",name:"Colombia"},{code:"KM",name:"Comoros"},{code:"CG",name:"Congo"},{code:"CD",name:"Congo, DR"},{code:"CK",name:"Cook Islands"},{code:"CR",name:"Costa Rica"},{code:"HR",name:"Croatia"},{code:"CU",name:"Cuba"},{code:"CW",name:"Curaçao"},{code:"CY",name:"Cyprus"},{code:"CZ",name:"Czech Republic"},{code:"DK",name:"Denmark"},{code:"DJ",name:"Djibouti"},{code:"DM",name:"Dominica"},{code:"DO",name:"Dominican Republic"},{code:"EC",name:"Ecuador"},{code:"EG",name:"Egypt"},{code:"SV",name:"El Salvador"},{code:"GQ",name:"Equatorial Guinea"},{code:"ER",name:"Eritrea"},{code:"EE",name:"Estonia"},{code:"SZ",name:"Eswatini"},{code:"ET",name:"Ethiopia"},{code:"FO",name:"Faroe Islands"},{code:"FJ",name:"Fiji"},{code:"FI",name:"Finland"},{code:"FR",name:"France"},{code:"GF",name:"French Guiana"},{code:"PF",name:"French Polynesia"},{code:"GA",name:"Gabon"},{code:"GM",name:"Gambia"},{code:"GE",name:"Georgia"},{code:"DE",name:"Germany"},{code:"GH",name:"Ghana"},{code:"GI",name:"Gibraltar"},{code:"GR",name:"Greece"},{code:"GL",name:"Greenland"},{code:"GD",name:"Grenada"},{code:"GP",name:"Guadeloupe"},{code:"GU",name:"Guam"},{code:"GT",name:"Guatemala"},{code:"GN",name:"Guinea"},{code:"GW",name:"Guinea-Bissau"},{code:"GY",name:"Guyana"},{code:"HT",name:"Haiti"},{code:"HN",name:"Honduras"},{code:"HK",name:"Hong Kong"},{code:"HU",name:"Hungary"},{code:"IS",name:"Iceland"},{code:"IN",name:"India"},{code:"ID",name:"Indonesia"},{code:"IR",name:"Iran"},{code:"IQ",name:"Iraq"},{code:"IE",name:"Ireland"},{code:"IM",name:"Isle of Man"},{code:"IL",name:"Israel"},{code:"IT",name:"Italy"},{code:"JM",name:"Jamaica"},{code:"JP",name:"Japan"},{code:"JE",name:"Jersey"},{code:"JO",name:"Jordan"},{code:"KZ",name:"Kazakhstan"},{code:"KE",name:"Kenya"},{code:"KI",name:"Kiribati"},{code:"KR",name:"Korea, South"},{code:"XK",name:"Kosovo"},{code:"KW",name:"Kuwait"},{code:"KG",name:"Kyrgyzstan"},{code:"LA",name:"Laos"},{code:"LV",name:"Latvia"},{code:"LB",name:"Lebanon"},{code:"LS",name:"Lesotho"},{code:"LR",name:"Liberia"},{code:"LY",name:"Libya"},{code:"LI",name:"Liechtenstein"},{code:"LT",name:"Lithuania"},{code:"LU",name:"Luxembourg"},{code:"MO",name:"Macao"},{code:"MG",name:"Madagascar"},{code:"MW",name:"Malawi"},{code:"MY",name:"Malaysia"},{code:"MV",name:"Maldives"},{code:"ML",name:"Mali"},{code:"MT",name:"Malta"},{code:"MH",name:"Marshall Islands"},{code:"MQ",name:"Martinique"},{code:"MR",name:"Mauritania"},{code:"MU",name:"Mauritius"},{code:"MX",name:"Mexico"},{code:"FM",name:"Micronesia"},{code:"MD",name:"Moldova"},{code:"MC",name:"Monaco"},{code:"MN",name:"Mongolia"},{code:"ME",name:"Montenegro"},{code:"MS",name:"Montserrat"},{code:"MA",name:"Morocco"},{code:"MZ",name:"Mozambique"},{code:"MM",name:"Myanmar"},{code:"NA",name:"Namibia"},{code:"NR",name:"Nauru"},{code:"NP",name:"Nepal"},{code:"NL",name:"Netherlands"},{code:"NC",name:"New Caledonia"},{code:"NZ",name:"New Zealand"},{code:"NI",name:"Nicaragua"},{code:"NE",name:"Niger"},{code:"NG",name:"Nigeria"},{code:"NU",name:"Niue"},{code:"MK",name:"North Macedonia"},{code:"NO",name:"Norway"},{code:"OM",name:"Oman"},{code:"PK",name:"Pakistan"},{code:"PW",name:"Palau"},{code:"PS",name:"Palestine"},{code:"PA",name:"Panama"},{code:"PG",name:"Papua New Guinea"},{code:"PY",name:"Paraguay"},{code:"PE",name:"Peru"},{code:"PH",name:"Philippines"},{code:"PL",name:"Poland"},{code:"PT",name:"Portugal"},{code:"PR",name:"Puerto Rico"},{code:"QA",name:"Qatar"},{code:"RE",name:"Réunion"},{code:"RO",name:"Romania"},{code:"RW",name:"Rwanda"},{code:"KN",name:"Saint Kitts and Nevis"},{code:"LC",name:"Saint Lucia"},{code:"VC",name:"Saint Vincent and the Grenadines"},{code:"WS",name:"Samoa"},{code:"SM",name:"San Marino"},{code:"SA",name:"Saudi Arabia"},{code:"SN",name:"Senegal"},{code:"RS",name:"Serbia"},{code:"SC",name:"Seychelles"},{code:"SL",name:"Sierra Leone"},{code:"SG",name:"Singapore"},{code:"SK",name:"Slovakia"},{code:"SI",name:"Slovenia"},{code:"SB",name:"Solomon Islands"},{code:"SO",name:"Somalia"},{code:"ZA",name:"South Africa"},{code:"SS",name:"South Sudan"},{code:"ES",name:"Spain"},{code:"LK",name:"Sri Lanka"},{code:"SD",name:"Sudan"},{code:"SR",name:"Suriname"},{code:"SE",name:"Sweden"},{code:"CH",name:"Switzerland"},{code:"SY",name:"Syria"},{code:"TW",name:"Taiwan"},{code:"TJ",name:"Tajikistan"},{code:"TZ",name:"Tanzania"},{code:"TH",name:"Thailand"},{code:"TL",name:"Timor-Leste"},{code:"TG",name:"Togo"},{code:"TO",name:"Tonga"},{code:"TT",name:"Trinidad and Tobago"},{code:"TN",name:"Tunisia"},{code:"TR",name:"Turkey"},{code:"TM",name:"Turkmenistan"},{code:"TC",name:"Turks and Caicos Islands"},{code:"TV",name:"Tuvalu"},{code:"UG",name:"Uganda"},{code:"UA",name:"Ukraine"},{code:"AE",name:"United Arab Emirates"},{code:"GB",name:"United Kingdom"},{code:"US",name:"United States"},{code:"UY",name:"Uruguay"},{code:"UZ",name:"Uzbekistan"},{code:"VU",name:"Vanuatu"},{code:"VE",name:"Venezuela"},{code:"VN",name:"Vietnam"},{code:"VG",name:"Virgin Islands, British"},{code:"VI",name:"Virgin Islands, U.S."},{code:"YE",name:"Yemen"},{code:"ZM",name:"Zambia"},{code:"ZW",name:"Zimbabwe"}];let l=null;async function d(e){if("proxy"===e.mode){const n=await async function(e){if(l)return l;try{const n=await fetch(`/apps/${e}/auth/token`,{method:"GET",headers:{Accept:"application/json"}}),t=await n.json().catch(()=>null);return n.ok&&t&&t.success&&t.data&&t.data.access_token?(l=t.data.access_token,l):null}catch(e){return console.error("Error fetching proxy access token:",e),null}}(e.proxyApp);return n?`Bearer ${n}`:null}return e.accessToken?/^Bearer /i.test(e.accessToken)?e.accessToken:`Bearer ${e.accessToken}`:(console.error('notify-me-wl: data-auth="token" requires an access token. Set data-access-token on the open button or window.TWC_ACCESS_TOKEN, or use data-auth="proxy" to obtain one via the Shopify App Proxy.'),null)}const u=new Map;function p(e){return"string"==typeof e?e.trim():""}async function w(e,n,t){const o=`${e}|${t||""}`,a=u.get(o);if(void 0!==a)return a;const r=await d(n);if(!r)return null;try{const n=await fetch("https://api.au-aws.thewishlist.io/services/eventcollector/api/v1/custom/configs/public",{method:"GET",headers:{Accept:"application/json",Authorization:r,"X-Twc-Tenant":e}});if(!n.ok)return console.error("Tenant config request failed:",n.status),null;const a=function(e,n=null){if(!e||"object"!=typeof e)return null;const t=e.websiteRecommendations;if(!t||"object"!=typeof t)return null;const o=p(t.engine).toUpperCase();if("TWC"===o)return{engine:"TWC"};if("ATHOS"===o){const e=p(t.siteIdentifier);return e?{engine:"ATHOS",siteIdentifier:e,profileTag:p(t.profileTag)||p(t.tags)||p(n)||"similar"}:null}return null}(await n.json().catch(()=>null),t);return u.set(o,a),a}catch(e){return console.error("Error fetching tenant config:",e),null}}function f(e){return"string"==typeof e?e.trim():""}function y(e){if("number"==typeof e&&Number.isFinite(e))return e;if("string"==typeof e){const n=parseFloat(e);if(Number.isFinite(n))return n}return null}function h(e){return Boolean(e.name&&e.imageUrl&&e.productUrl)}function g(e,n){const t=new Map;return e.forEach(e=>{const n=t.get(e.product.ref);n&&n.score>=e.score||t.set(e.product.ref,e)}),Array.from(t.values()).sort((e,n)=>n.score-e.score).slice(0,n).map(e=>e.product)}async function v(e){const{email:n,tenant:t,auth:o,count:a}=e,r=await d(o);if(!r)return null;const c=Math.min(3*a,20),i=`https://api.au-aws.thewishlist.io/services/recommendations/api/v1/recommendations/${encodeURIComponent(t)}/${encodeURIComponent(n)}?n=${c}`;try{const e=await fetch(i,{method:"GET",headers:{Accept:"application/json",Authorization:r,"X-Twc-Tenant":t}});if(!e.ok)return console.error("Recommendations request failed:",e.status),null;return function(e,n){const t=e&&e.recommendations;if(!Array.isArray(t))return[];const o=[];return t.forEach(e=>{var n;const t=e&&e.product;if(!t)return;const a=f(t.name),r=f(t.image_url),c=f(t.product_url);if(!h({name:a,imageUrl:r,productUrl:c}))return;const i=f(t.product_ref)||f(t.product_id)||c;o.push({score:"number"==typeof e.score?e.score:0,product:{id:f(t.product_id)||i,ref:i,name:a,imageUrl:r,productUrl:c,price:null!==(n=y(t.price))&&void 0!==n?n:0,originalPrice:y(t.original_price)}})}),g(o,n)}(await e.json().catch(()=>null),a)}catch(e){return console.error("Error fetching recommendations:",e),null}}async function b(e){const{engine:n,email:t,count:o,productId:a}=e,r=encodeURIComponent(n.siteIdentifier),c=[`tags=${encodeURIComponent(n.profileTag)}`,`limits=${o}`];a&&c.push(`products=${encodeURIComponent(a)}`),t&&c.push(`shopper=${encodeURIComponent(t)}`);const i=`https://${r}.a.searchspring.io/boost/${r}/recommend?`+c.join("&");try{const e=await fetch(i,{method:"GET",headers:{Accept:"application/json"}});if(!e.ok)return console.error("Athos recommendations request failed:",e.status),null;return function(e,n){const t=Array.isArray(e)?e[0]:e,o=t&&t.results;if(!Array.isArray(o))return[];const a=[];return o.forEach((e,n)=>{var t;const r=e&&e.mappings&&e.mappings.core;if(!r)return;const c=f(r.name),i=f(r.imageUrl)||f(r.thumbnailImageUrl),m=f(r.url);if(!h({name:c,imageUrl:i,productUrl:m}))return;const s=f(r.uid)||f(r.sku)||m,l=null!==(t=y(r.price))&&void 0!==t?t:0,d=y(r.msrp);a.push({score:o.length-n,product:{id:f(r.uid)||s,ref:s,name:c,imageUrl:i,productUrl:m,price:l,originalPrice:null!==d&&d>l?d:null}})}),g(a,n)}(await e.json().catch(()=>null),o)}catch(e){return console.error("Error fetching Athos recommendations:",e),null}}const x=new Map;const C="twc-nm-recs",k="twc-nm-recs-title";function S(e,n){if(n)try{return new Intl.NumberFormat(void 0,{style:"currency",currency:n}).format(e)}catch(e){}return new Intl.NumberFormat(void 0,{minimumFractionDigits:0,maximumFractionDigits:2}).format(e)}function E(){const e=document.createElement("section");e.id=C,e.className="twc-nm-recs",e.setAttribute("aria-labelledby",k);const n=document.createElement("h3");n.id=k,n.className="twc-nm-recs-title",n.textContent="Shop similar styles",e.appendChild(n);const t=document.createElement("div");return t.className="twc-nm-recs-row",function(e){let n=0,t=0,o=!1,a=!1,r=!1;e.addEventListener("pointerdown",c=>{"mouse"===c.pointerType&&0===c.button&&(o=!0,a=!1,r=!1,n=c.clientX,t=e.scrollLeft,e.classList.add("twc-nm-recs-row--dragging"))}),e.addEventListener("pointermove",r=>{if(!o)return;const c=r.clientX-n;!a&&Math.abs(c)<5||(a=!0,e.scrollLeft=t-c,r.preventDefault())});const c=()=>{o&&(o=!1,r=a,e.classList.remove("twc-nm-recs-row--dragging"))};e.addEventListener("pointerup",c),e.addEventListener("pointercancel",c),e.addEventListener("pointerleave",c),e.addEventListener("click",e=>{r&&(r=!1,e.preventDefault(),e.stopPropagation())},!0),e.addEventListener("dragstart",e=>{o&&a&&e.preventDefault()})}(t),e.appendChild(t),{section:e,row:t}}function A(){const e=document.createElement("div");e.className="twc-nm-rec twc-nm-rec--skeleton",e.setAttribute("aria-hidden","true");const n=document.createElement("div");n.className="twc-nm-rec-imgbox twc-nm-skel",e.appendChild(n);const t=document.createElement("span");t.className="twc-nm-skel twc-nm-skel-line",e.appendChild(t);const o=document.createElement("span");return o.className="twc-nm-skel twc-nm-skel-line twc-nm-skel-line--short",e.appendChild(o),e}function L(e,n,t){const o=e.querySelector(".twc-nm-recs-row");if(!o)return;const a=(r=t,window.Shopify&&window.Shopify.currency&&window.Shopify.currency.active||r||null);var r;const c=document.createDocumentFragment();n.forEach(e=>{c.appendChild(function(e,n){const t=document.createElement("a");t.className="twc-nm-rec",t.href=e.productUrl,t.target="_blank",t.rel="noopener noreferrer";const o=document.createElement("div");o.className="twc-nm-rec-imgbox";const a=document.createElement("img");a.className="twc-nm-rec-img",a.src=e.imageUrl,a.alt=e.name,a.loading="lazy",a.decoding="async",o.appendChild(a),t.appendChild(o);const r=document.createElement("span");r.className="twc-nm-rec-name",r.textContent=e.name,t.appendChild(r);const c=document.createElement("span");if(c.className="twc-nm-rec-price",null!==e.originalPrice&&e.originalPrice>e.price){const t=document.createElement("s");t.className="twc-nm-rec-was",t.textContent=S(e.originalPrice,n),c.appendChild(t),c.appendChild(document.createTextNode(" "))}return c.appendChild(document.createTextNode(S(e.price,n))),t.appendChild(c),t}(e,a))}),o.textContent="",o.appendChild(c),e.classList.remove("twc-nm-recs--loading"),e.removeAttribute("aria-busy")}function T(){const e=document.getElementById(C);e&&e.parentNode&&e.parentNode.removeChild(e)}const N={"notify-me":{intro:"Register to receive a notification as soon as this item is back in stock",submit:"Notify Me"},"coming-soon":{intro:"Register your interest to hear more about this item",submit:"Register Interest"}},M=["title","intro","sizeLabel","sizePlaceholder","emailPlaceholder","emailOptIn","smsOptIn","submit","success","disclaimer"];function I(e,n){const t={title:"{product}",intro:N[n].intro,sizeLabel:"Size",sizePlaceholder:"Select a size",emailLabel:"Email",emailPlaceholder:"you@example.com",emailOptIn:"Email me store updates",smsOptIn:"Text me store updates",submit:N[n].submit,success:"You're on the list. We'll be in touch.",disclaimer:null};if(void 0===e)return t;if(!e||"object"!=typeof e||Array.isArray(e))return a("copy","an object"),t;const o=e;M.forEach(e=>{const n=o[e];null!=n&&("string"==typeof n?t[e]=n:a(`copy.${e}`,"a string"))});const r=o.emailLabel;return!1===r||"string"==typeof r?t.emailLabel=r:null!=r&&a("copy.emailLabel","a string or false"),t}function B(e,n){return e.split("{product}").join(n)}function P(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function $(e){if("string"!=typeof e)return null;const n=e.trim();return/^(https?:)?\/\//i.test(n)?n:null}function z(e){return"string"==typeof e?e.trim():""}function G(o){const{wrapper:a,config:r,localization:i}=o,{fields:m,type:l,display:u,auth:p,tenant:f,recommendationsEnabled:y,recommendationsCount:h,recommendationsProfile:g,currency:k,customerEmail:S,copy:N,productSummary:M,classes:I,jsonConfigured:G}=r,U='<button type="button" id="popup-close" class="twc-nm-close" aria-label="Close"><svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" focusable="false"><path d="M3 3l10 10M13 3L3 13" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" fill="none"/></svg></button>',j='<h2 id="popup-title" class="twc-nm-title"></h2>',R=G?`<div class="twc-nm-header">${U}${j}</div>`:`${U}\n ${j}`,O=document.createElement("div");O.innerHTML=`\n <div id="twc-nm-overlay" role="dialog" aria-modal="true" aria-labelledby="popup-title">\n <div class="twc-nm-card">\n ${R}\n <p id="popup-text" class="twc-nm-text"></p>\n <div id="popup-notice" class="twc-nm-status" role="status" aria-live="polite" hidden></div>\n <form id="popup-form" class="twc-nm-form">\n <div class="twc-nm-field">\n <label class="twc-nm-label" for="twc-nm-size">${P(N.sizeLabel)}</label>\n <select class="twc-nm-select" id="twc-nm-size" name="select-size" required>\n <option value="" disabled selected>${P(N.sizePlaceholder)}</option>\n </select>\n </div>\n </form>\n </div>\n </div>\n `,a.appendChild(O);const H=document.getElementById("twc-nm-overlay"),F=document.getElementById("popup-close"),K=document.getElementById("popup-title"),D=document.getElementById("popup-text"),q=document.querySelector("select[name='select-size']"),_=document.getElementById("popup-form"),V=document.getElementById("popup-notice");if(!(H&&F&&K&&D&&q&&_&&V))return null;"panel"===u&&H.classList.add("twc-nm--panel");const J=M?function(){const e=document.createElement("div");return e.className="twc-nm-summary",e.innerHTML='<div class="twc-nm-summary-image"><img alt="" /></div><div class="twc-nm-summary-info"><p class="twc-nm-summary-name"></p><p class="twc-nm-summary-variant"></p><p class="twc-nm-summary-price"><span class="twc-nm-summary-now"></span> <s class="twc-nm-summary-compare"></s></p></div>',e}():null;J&&_.insertBefore(J,_.firstChild);const Z=s.map(e=>`<option value="${e.code}"${e.code===i.countryCode?" selected":""}>${e.name}</option>`).join(""),W=(e,n,t)=>`<div class="twc-nm-field"><label class="twc-nm-label" for="${e}">${n}</label>${t}</div>`,Y={email:!1===N.emailLabel?`<div class="twc-nm-field"><input class="twc-nm-input" id="twc-nm-email" name="email" type="email" placeholder="${P(N.emailPlaceholder)}" aria-label="${P(N.emailPlaceholder||"Email")}" required /></div>`:W("twc-nm-email",P(N.emailLabel),`<input class="twc-nm-input" id="twc-nm-email" name="email" type="email" placeholder="${P(N.emailPlaceholder)}" required />`),mobile:W("twc-nm-mobile","Mobile",'<input class="twc-nm-input" id="twc-nm-mobile" name="mobile" type="tel" placeholder="Mobile number" required />'),firstName:W("twc-nm-firstname","First name",'<input class="twc-nm-input" id="twc-nm-firstname" name="firstName" type="text" placeholder="First name" required />'),lastName:W("twc-nm-lastname","Last name",'<input class="twc-nm-input" id="twc-nm-lastname" name="lastName" type="text" placeholder="Last name" required />'),countryCode:W("twc-nm-country","Country",`<select class="twc-nm-select" id="twc-nm-country" name="countryCode"><option value="">Select country</option>${Z}</select>`),provinceCode:W("twc-nm-province","State / Province",`<input class="twc-nm-input" id="twc-nm-province" name="provinceCode" type="text" placeholder="State / province code" value="${i.provinceCode||""}" />`)};m.forEach(e=>{Y[e]&&_.insertAdjacentHTML("beforeend",Y[e])});const X=[m.includes("email")?`<label class="twc-nm-check"><input type="checkbox" name="mailList" /> ${P(N.emailOptIn)}</label>`:"",m.includes("mobile")?`<label class="twc-nm-check"><input type="checkbox" name="mailListSms" /> ${P(N.smsOptIn)}</label>`:""].join("");if(X&&_.insertAdjacentHTML("beforeend",`<div class="twc-nm-checks">${X}</div>`),_.insertAdjacentHTML("beforeend",'<div id="popup-status" class="twc-nm-status" role="status" aria-live="polite" hidden></div>'),_.insertAdjacentHTML("beforeend",`<button type="submit" class="twc-nm-submit">${P(N.submit)}</button>`),N.disclaimer&&_.insertAdjacentHTML("beforeend",`<p class="twc-nm-disclaimer">${P(N.disclaimer)}</p>`),function(e,n){Object.keys(n).forEach(t=>{const o=n[t];o&&e.querySelectorAll(c[t]).forEach(e=>{e.classList.add(...o,"twc-nm-themed")})})}(H,I),S){const e=_.querySelector("input[name='email']");e&&(e.value=S)}const Q=document.getElementById("popup-status"),ee=_.querySelector(".twc-nm-submit");if(!Q||!ee)return null;function ne(e,n){Q.textContent=n,Q.className=`twc-nm-status twc-nm-status--${e}`,Q.hidden=!1}function te(){Q.hidden=!0,Q.textContent=""}function oe(e,n){_.hidden=!0,V.textContent=n,V.className=`twc-nm-status twc-nm-status--${e}`,V.hidden=!1}const ae=e=>{const n=_.querySelector(e);return n?n.value:""},re=e=>{const n=_.querySelector(e);return!!n&&n.checked},ce=H.querySelector(".twc-nm-card");let ie=null,me=[],se=null;async function le(e){const n=await function(e,n,t){return w(e,n,t)}(f,p,g);if(!n)return!1;if(!H.classList.contains("is-open"))return!1;const t=function(e){const{section:n,row:t}=E();n.classList.add("twc-nm-recs--loading"),n.setAttribute("aria-busy","true");for(let n=0;n<e;n++)t.appendChild(A());return n}(h);ce.appendChild(t);const o=await async function(e,n){const{email:t,tenant:o,auth:a,count:r,productId:c}=n,i=`${e.engine}|${o}|${t}|${r}|${c||""}`,m=x.get(i);if(m)return m;let s=null;return s="ATHOS"===e.engine?await b({engine:e,email:t,count:r,productId:c}):await v({email:t,tenant:o,auth:a,count:r}),null===s?[]:(x.set(i,s),s)}(n,{email:e,tenant:f,auth:p,count:h,productId:ie?String(ie.id):null});return!!t.isConnected&&(o.length?(L(t,o,k),!0):(T(),!1))}function de(e){if(!y||!ce)return Promise.resolve(!1);if(se)return se;if(document.getElementById(C))return Promise.resolve(!0);const n=le(e);return se=n,n.then(e=>(se===n&&(se=null),e),e=>(se===n&&(se=null),console.error("Error rendering recommendations:",e),!1))}let ue=null,pe=0;function we(e){"Escape"===e.key&&fe()}function fe(){pe++,H.classList.remove("is-open"),document.body.style.overflow="",document.removeEventListener("keydown",we),te(),T(),se=null,ue&&"function"==typeof ue.focus&&ue.focus()}function ye(){if(!J||!ie)return;const e=me.find(e=>String(e.id)===q.value)||null;!function(e,n,t,o){const a=n=>e.querySelector(n),r=a(".twc-nm-summary-image"),c=a(".twc-nm-summary-image img"),i=t&&$(t.image)||$(n.image);i?(c.src=i,c.alt=n.title):c.removeAttribute("src"),r.hidden=!i,a(".twc-nm-summary-name").textContent=n.title;const m=a(".twc-nm-summary-variant");m.textContent=t?t.title:"",m.hidden=!t;let s="",l="";if(t)s=z(t.price),l=z(t.compareAtPrice);else{const e=o.map(e=>z(e.price));e.length&&e[0]&&e.every(n=>n===e[0])&&(s=e[0])}l===s&&(l=""),a(".twc-nm-summary-now").textContent=s;const d=a(".twc-nm-summary-compare");d.textContent=l,d.hidden=!l,a(".twc-nm-summary-price").hidden=!s}(J,ie,e,me)}return F.addEventListener("click",fe),q.addEventListener("change",ye),H.addEventListener("click",e=>{e.target===H&&fe()}),_.addEventListener("submit",async e=>{e.preventDefault();const n=me.find(e=>String(e.id)===q.value);if(!n)return void ne("error","Please select a size.");const t={variantRef:n.id,email:ae("input[name='email']"),subscribe:re("input[name='mailList']"),subscribeSms:re("input[name='mailListSms']")};if("coming-soon"===l?t.comingSoon=!0:t.notifyMe=!0,m.includes("firstName")&&(t.firstName=ae("input[name='firstName']")),m.includes("lastName")&&(t.lastName=ae("input[name='lastName']")),m.includes("mobile")&&(t.mobile=ae("input[name='mobile']")),m.includes("countryCode")){const e=ae("select[name='countryCode']");e&&(t.countryCode=e)}else i.countryCode&&(t.countryCode=i.countryCode);if(m.includes("provinceCode")){const e=ae("input[name='provinceCode']");e&&(t.provinceCode=e)}else i.provinceCode&&(t.provinceCode=i.provinceCode);i.marketId&&(t.marketId=i.marketId),i.marketHandle&&(t.marketHandle=i.marketHandle),te();const o=pe,a=ee.textContent||"";ee.disabled=!0,ee.textContent="Sending…";let r={ok:!1,reason:"error"};try{r=await async function(e,n,t){const o=await d(n);if(!o)return{ok:!1,reason:"auth"};try{return(await fetch("https://api.au-aws.thewishlist.io/services/wsservice/api/wishlist/items/customerInterest",{method:"POST",headers:{"Content-Type":"application/json",Authorization:o,"X-Twc-Tenant":t},body:JSON.stringify(e)})).ok?{ok:!0}:{ok:!1,reason:"error"}}catch(e){return console.error("Error submitting form:",e),{ok:!1,reason:"error"}}}(t,p,f)}finally{o===pe&&(ee.disabled=!1,ee.textContent=a)}if(o===pe)if(r.ok){ne("success",N.success);const e=S||ae("input[name='email']");!!e&&await de(e)||o!==pe||setTimeout(()=>{o===pe&&fe()},1500)}else"auth"===r.reason?ne("error","proxy"===p.mode?"Please log in to your account to continue.":"Something went wrong. Please try again."):ne("error","Something went wrong. Please try again.")}),{open:function(o){pe++,ee.disabled=!1,ee.textContent=N.submit,te(),_.hidden=!1,V.hidden=!0,V.textContent="",T(),se=null,H.classList.contains("is-open")||(ue=document.activeElement),H.classList.add("is-open"),document.body.style.overflow="hidden",document.addEventListener("keydown",we);const a=function(t){const o=t?t.closest(`[${e}]`):null;if(o){try{const t=JSON.parse(o.getAttribute(e)||"");if(n(t))return{ok:!0,product:t,source:"card"}}catch(e){}return console.error(`notify-me-wl: invalid ${e} JSON on`,o),{ok:!1}}const a=window.currentProduct;return n(a)?{ok:!0,product:a,source:"page"}:{ok:!1}}(o);if(!a.ok)return ie=null,me=[],K.textContent="",D.textContent=B(N.intro,""),void oe("error","Product information is unavailable. Please refresh the page.");if(ie=a.product,me=(ie.variants||[]).filter(e=>!!e&&("number"==typeof e.id||"string"==typeof e.id)&&"string"==typeof e.title&&!0!==e.available),K.textContent=B(N.title,ie.title),D.textContent=B(N.intro,ie.title),!me.length)return void oe("info","All sizes are currently in stock.");const r=o?o.getAttribute("data-variant-id"):null,c="page"===a.source?[r,...t()]:[r];var i,m;i=me,m=function(e,n){const t=e.map(e=>String(e.id));for(const e of n){const n=e?e.trim():"";if(n&&t.includes(n))return n}return 1===t.length?t[0]:null}(me,c),q.innerHTML=`<option value="" disabled selected>${P(N.sizePlaceholder)}</option>`,i.forEach(e=>{const n=document.createElement("option");n.value=String(e.id),n.textContent=e.title,q.appendChild(n)}),m&&(q.value=m),ye(),S&&de(S);const s=_.querySelector("select, input");s&&s.focus()}}}function U(e){var n;const t=(null!=e?e:"").trim();if(!t)return null;const o=null!==(n=t.split("?")[0].split("/").pop())&&void 0!==n?n:"";return/^\d+$/.test(o)?o:null}function j(e){const n=(null!=e?e:"").trim();return/^[a-z0-9_-]+$/i.test(n)?n:null}function R(e){const n=(null!=e?e:"").trim();return/^[a-z]{2}$/i.test(n)?n.toUpperCase():null}function O(e){var n,t,o;const a={countryCode:R(e("data-country-code")),provinceCode:null,marketId:U(e("data-market-id")),marketHandle:j(e("data-market-handle"))},r=window.Shopify;if(r){const e=null===(n=r.markets)||void 0===n?void 0:n.currentMarket;if(e&&(!a.countryCode&&e.countryCode&&(a.countryCode=e.countryCode),a.marketId||(a.marketId=U(String(null!==(t=e.id)&&void 0!==t?t:"")))),a.countryCode||"string"!=typeof r.country||(a.countryCode=r.country),!a.countryCode&&"string"==typeof r.locale){const e=r.locale.split("-");e.length>1&&(a.countryCode=e[e.length-1].toUpperCase())}}if(!a.countryCode||!a.provinceCode){const e=null===(o=window.customer)||void 0===o?void 0:o.default_address;e&&(!a.countryCode&&e.country_code&&(a.countryCode=e.country_code),!a.provinceCode&&e.province_code&&(a.provinceCode=e.province_code))}if(!a.countryCode){const e=(navigator.language||"").split("-");e.length>1&&(a.countryCode=e[e.length-1].toUpperCase())}return a}function H(e){const n=e("data-customer-email");if(n&&n.trim())return n.trim();const t=window.customer&&window.customer.email;if(t&&t.trim())return t.trim();const o=window.customerEmail;return"string"==typeof o&&o.trim()?o.trim():null}const F="[data-twc-notify-me]";function K(){const e=document.getElementById("notification-widget");if(!e)return;const n=document.getElementById("popup-open"),t=function(e){const n=e.querySelector('script[type="application/json"][data-twc-config]');if(!n)return null;try{const e=JSON.parse(n.textContent||"");if(e&&"object"==typeof e&&!Array.isArray(e))return e}catch(e){}return console.error("notify-me-wl: invalid data-twc-config JSON"),null}(e),c=function(e,n,t){const c=t=>(e?e.getAttribute(t):null)||n.getAttribute(t)||null,i=e=>null===t[e]?void 0:t[e],m=e=>{const n=i(e);return void 0===n?null:"boolean"==typeof n?n:(a(e,"true or false"),null)};return{string(e){const n=c(e);if(null!==n)return n;const t=o(e),r=i(t);return void 0===r?null:"string"==typeof r?r:"number"==typeof r&&Number.isFinite(r)?String(r):(a(t,"a string"),null)},boolean(e){const n=c(e);return null!==n?"false"!==n:m(o(e))},number(e){const n=c(e);if(null!==n){const e=parseInt(n,10);if(Number.isFinite(e))return e}const t=o(e),r=i(t);return void 0===r?null:"number"==typeof r&&Number.isFinite(r)?r:(a(t,"a number"),null)},stringArray(e){const n=c(e);if(null!==n){try{const e=JSON.parse(n);if(r(e))return e}catch(e){}console.warn(`notify-me-wl: ignoring invalid ${e} attribute`)}const t=o(e),m=i(t);return void 0===m?null:r(m)?m:(a(t,"an array of strings"),null)},json:i,jsonBoolean:m}}(n,e,t||{}),i=c.string;!function(){const e=document.createElement("style");e.type="text/css",e.innerText="\n .notification-btn {\n display: inline-block;\n width: 100%;\n padding: 12px 24px;\n background: #fff;\n border: 1px solid #111827;\n border-radius: 8px;\n color: #111827;\n font: inherit;\n text-align: center;\n text-transform: uppercase;\n cursor: pointer;\n }\n .notification-btn:hover {\n background: #111827;\n color: #fff;\n }\n #twc-nm-overlay {\n position: fixed;\n inset: 0;\n z-index: 99999;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n background: var(--twc-nm-overlay-bg, rgba(17, 17, 17, 0.55));\n opacity: 0;\n visibility: hidden;\n transition: opacity var(--twc-nm-transition, 200ms ease);\n }\n #twc-nm-overlay.is-open {\n opacity: 1;\n visibility: visible;\n }\n #twc-nm-overlay * {\n box-sizing: border-box;\n }\n #twc-nm-overlay .twc-nm-card {\n position: relative;\n width: 100%;\n max-width: 400px;\n max-height: calc(100vh - 32px);\n overflow-y: auto;\n padding: var(--twc-nm-padding, 24px);\n background: var(--twc-nm-bg, #fff);\n color: var(--twc-nm-text, #111827);\n border-radius: 12px;\n box-shadow: 0 10px 40px rgba(0, 0, 0, 0.18);\n font: inherit;\n font-family: var(--twc-nm-font, inherit);\n transform: translateY(8px);\n transition: transform var(--twc-nm-transition, 200ms ease);\n }\n #twc-nm-overlay.is-open .twc-nm-card {\n transform: translateY(0);\n }\n /* Panel mode: same blocking backdrop, docked to the right edge and\n full height instead of centred. Declared after the modal rules so\n the shared properties are overridden. */\n #twc-nm-overlay.twc-nm--panel {\n align-items: stretch;\n justify-content: flex-end;\n padding: 0;\n }\n #twc-nm-overlay.twc-nm--panel .twc-nm-card {\n width: var(--twc-nm-panel-width, 420px);\n max-width: 100%;\n max-height: none;\n border-radius: 0;\n box-shadow: var(--twc-nm-panel-shadow, -10px 0 40px rgba(0, 0, 0, 0.18));\n transform: translateX(100%);\n }\n /* Needs the extra class to beat both the .is-open rule above and the\n panel's own resting transform. */\n #twc-nm-overlay.twc-nm--panel.is-open .twc-nm-card {\n transform: translateX(0);\n }\n @media (max-width: 480px) {\n #twc-nm-overlay.twc-nm--panel .twc-nm-card {\n width: var(--twc-nm-panel-width-mobile, 100%);\n }\n }\n /* Groups the title and close button so a theme can lay them out as a\n row. With no border it changes nothing: the close button stays\n positioned against the card. */\n #twc-nm-overlay .twc-nm-header {\n border-bottom: var(--twc-nm-header-border, none);\n }\n /* Layout below is never themed — it keeps the close button usable\n whatever classes a theme attaches. Cosmetics follow :not(themed). */\n #twc-nm-overlay .twc-nm-close {\n position: absolute;\n top: 12px;\n right: 12px;\n width: 32px;\n height: 32px;\n min-width: 0;\n min-height: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0;\n padding: 0;\n border: none;\n background: transparent;\n box-shadow: none;\n text-indent: 0;\n line-height: 1;\n opacity: 1;\n visibility: visible;\n cursor: pointer;\n }\n #twc-nm-overlay .twc-nm-close:where(:not(.twc-nm-themed)) {\n border-radius: 6px;\n color: var(--twc-nm-muted, #6b7280);\n }\n /* The icon is an inline SVG rather than a text glyph so host-theme\n button resets (font-size: 0, icon fonts, text-indent) can't hide it. */\n #twc-nm-overlay .twc-nm-close svg {\n display: block;\n width: 16px;\n height: 16px;\n flex: none;\n pointer-events: none;\n }\n #twc-nm-overlay .twc-nm-close::before,\n #twc-nm-overlay .twc-nm-close::after {\n content: none;\n }\n #twc-nm-overlay .twc-nm-close:where(:not(.twc-nm-themed)):hover {\n background: #f3f4f6;\n color: var(--twc-nm-text, #111827);\n }\n #twc-nm-overlay .twc-nm-title {\n margin: 0 32px 4px 0;\n }\n #twc-nm-overlay .twc-nm-title:where(:not(.twc-nm-themed)) {\n font-size: 18px;\n font-weight: 600;\n }\n #twc-nm-overlay .twc-nm-text {\n margin: 0 0 16px;\n }\n #twc-nm-overlay .twc-nm-text:where(:not(.twc-nm-themed)) {\n font-size: 14px;\n line-height: 1.5;\n color: var(--twc-nm-muted, #6b7280);\n }\n #twc-nm-overlay .twc-nm-form {\n display: flex;\n flex-direction: column;\n gap: 14px;\n }\n #twc-nm-overlay .twc-nm-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n #twc-nm-overlay .twc-nm-label:where(:not(.twc-nm-themed)) {\n font-size: 13px;\n font-weight: 500;\n color: #374151;\n }\n #twc-nm-overlay .twc-nm-input,\n #twc-nm-overlay .twc-nm-select {\n width: 100%;\n }\n /* The shorthand resets every background sub-property, so host CSS\n can't leak into an unconfigured widget; the select's arrow is added\n by the rule after it. */\n #twc-nm-overlay .twc-nm-input:where(:not(.twc-nm-themed)),\n #twc-nm-overlay .twc-nm-select:where(:not(.twc-nm-themed)) {\n padding: 10px 12px;\n font-size: 14px;\n font-family: inherit;\n color: var(--twc-nm-text, #111827);\n background: #fff;\n border: 1px solid var(--twc-nm-border, #e5e7eb);\n border-radius: var(--twc-nm-radius, 8px);\n }\n #twc-nm-overlay .twc-nm-input:where(:not(.twc-nm-themed)):focus,\n #twc-nm-overlay .twc-nm-select:where(:not(.twc-nm-themed)):focus {\n outline: none;\n border-color: var(--twc-nm-text, #111827);\n box-shadow: 0 0 0 3px rgba(17, 24, 39, 0.08);\n }\n #twc-nm-overlay .twc-nm-select {\n appearance: none;\n -webkit-appearance: none;\n cursor: pointer;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2 4l4 4 4-4' stroke='%236b7280' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 12px center;\n }\n /* After the shared padding above, which it must override. */\n #twc-nm-overlay .twc-nm-select:where(:not(.twc-nm-themed)) {\n padding-right: 36px;\n }\n #twc-nm-overlay .twc-nm-checks {\n display: flex;\n flex-direction: column;\n gap: 10px;\n }\n #twc-nm-overlay .twc-nm-check {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 13px;\n color: #374151;\n cursor: pointer;\n }\n #twc-nm-overlay .twc-nm-check input {\n width: 16px;\n height: 16px;\n margin: 0;\n }\n #twc-nm-overlay .twc-nm-check input:where(:not(.twc-nm-themed)) {\n accent-color: var(--twc-nm-text, #111827);\n }\n #twc-nm-overlay .twc-nm-status {\n font-size: 13px;\n line-height: 1.5;\n padding: 10px 12px;\n border-radius: var(--twc-nm-radius, 8px);\n }\n #twc-nm-overlay .twc-nm-status--error {\n color: #b91c1c;\n background: #fef2f2;\n }\n #twc-nm-overlay .twc-nm-status--success {\n color: #15803d;\n background: #f0fdf4;\n }\n #twc-nm-overlay .twc-nm-status--info {\n color: #374151;\n background: #f3f4f6;\n }\n #twc-nm-overlay .twc-nm-submit {\n width: 100%;\n cursor: pointer;\n }\n #twc-nm-overlay .twc-nm-submit:where(:not(.twc-nm-themed)) {\n padding: 12px 16px;\n font-size: 14px;\n font-weight: 600;\n font-family: inherit;\n color: #fff;\n background: #111827;\n border: none;\n border-radius: var(--twc-nm-radius, 8px);\n transition: background 150ms ease;\n }\n #twc-nm-overlay .twc-nm-submit:where(:not(.twc-nm-themed)):hover {\n background: #374151;\n }\n #twc-nm-overlay .twc-nm-submit:disabled {\n opacity: 0.6;\n cursor: default;\n }\n #twc-nm-overlay .twc-nm-disclaimer {\n margin: 0;\n }\n #twc-nm-overlay .twc-nm-disclaimer:where(:not(.twc-nm-themed)) {\n font-size: 12px;\n line-height: 1.5;\n color: var(--twc-nm-muted, #6b7280);\n }\n #twc-nm-overlay .twc-nm-summary {\n display: flex;\n align-items: flex-start;\n }\n #twc-nm-overlay .twc-nm-summary:where(:not(.twc-nm-themed)) {\n gap: 16px;\n }\n /* Theme classes often set display; hidden parts must stay hidden. */\n #twc-nm-overlay .twc-nm-summary [hidden] {\n display: none !important;\n }\n #twc-nm-overlay .twc-nm-summary-image {\n flex: none;\n }\n #twc-nm-overlay .twc-nm-summary-image:where(:not(.twc-nm-themed)) {\n width: 96px;\n }\n #twc-nm-overlay .twc-nm-summary-image img {\n display: block;\n width: 100%;\n height: auto;\n }\n #twc-nm-overlay .twc-nm-summary-info {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n #twc-nm-overlay .twc-nm-summary-info p {\n margin: 0;\n }\n #twc-nm-overlay .twc-nm-summary-name {\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n }\n #twc-nm-overlay .twc-nm-summary-name:where(:not(.twc-nm-themed)) {\n font-size: 14px;\n font-weight: 600;\n }\n #twc-nm-overlay .twc-nm-summary-variant:where(:not(.twc-nm-themed)) {\n font-size: 13px;\n color: var(--twc-nm-muted, #6b7280);\n }\n #twc-nm-overlay .twc-nm-summary-price:where(:not(.twc-nm-themed)) {\n font-size: 14px;\n }\n #twc-nm-overlay .twc-nm-summary-compare:where(:not(.twc-nm-themed)) {\n margin-left: 6px;\n color: #9ca3af;\n }\n #twc-nm-overlay .twc-nm-recs {\n margin-top: 20px;\n padding-top: 20px;\n border-top: 1px solid var(--twc-nm-border, #e5e7eb);\n }\n #twc-nm-overlay .twc-nm-recs-title {\n margin: 0 0 12px;\n font-size: 14px;\n font-weight: 600;\n color: #111827;\n }\n #twc-nm-overlay .twc-nm-recs-row {\n display: flex;\n gap: 12px;\n overflow-x: auto;\n scroll-snap-type: x mandatory;\n -webkit-overflow-scrolling: touch;\n padding-bottom: 8px;\n scrollbar-width: thin;\n }\n /* Only where a real pointer exists — the affordance is meaningless\n on touch, which pans the row natively. */\n @media (hover: hover) {\n #twc-nm-overlay .twc-nm-recs-row {\n cursor: grab;\n }\n }\n #twc-nm-overlay .twc-nm-recs-row--dragging,\n #twc-nm-overlay .twc-nm-recs-row--dragging .twc-nm-rec {\n cursor: grabbing;\n }\n #twc-nm-overlay .twc-nm-recs-row--dragging {\n /* Snapping fights a manually driven scrollLeft and feels sticky. */\n scroll-snap-type: none;\n user-select: none;\n -webkit-user-select: none;\n }\n #twc-nm-overlay .twc-nm-rec {\n flex: 0 0 132px;\n scroll-snap-align: start;\n display: flex;\n flex-direction: column;\n gap: 6px;\n color: inherit;\n text-decoration: none;\n }\n #twc-nm-overlay .twc-nm-rec:focus-visible {\n outline: 2px solid #111827;\n outline-offset: 2px;\n border-radius: 8px;\n }\n #twc-nm-overlay .twc-nm-rec-imgbox {\n aspect-ratio: 3 / 4;\n overflow: hidden;\n border-radius: 8px;\n background: #f3f4f6;\n }\n #twc-nm-overlay .twc-nm-rec-img {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: cover;\n transition: opacity 150ms ease;\n }\n #twc-nm-overlay .twc-nm-rec:hover .twc-nm-rec-img {\n opacity: 0.85;\n }\n #twc-nm-overlay .twc-nm-rec-name {\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n font-size: 13px;\n line-height: 1.35;\n color: #111827;\n }\n #twc-nm-overlay .twc-nm-rec-price {\n font-size: 13px;\n font-weight: 600;\n color: #111827;\n }\n #twc-nm-overlay .twc-nm-rec-was {\n margin-right: 4px;\n font-weight: 400;\n color: #9ca3af;\n }\n #twc-nm-overlay .twc-nm-rec--skeleton {\n pointer-events: none;\n }\n #twc-nm-overlay .twc-nm-skel {\n background-color: #f3f4f6;\n background-image: linear-gradient(\n 90deg,\n #f3f4f6 0%,\n #e8eaee 50%,\n #f3f4f6 100%\n );\n background-size: 200% 100%;\n border-radius: 4px;\n animation: twc-nm-shimmer 1.3s ease-in-out infinite;\n }\n #twc-nm-overlay .twc-nm-rec-imgbox.twc-nm-skel {\n border-radius: 8px;\n }\n #twc-nm-overlay .twc-nm-skel-line {\n height: 12px;\n width: 100%;\n }\n #twc-nm-overlay .twc-nm-skel-line--short {\n width: 45%;\n }\n @keyframes twc-nm-shimmer {\n from {\n background-position: 200% 0;\n }\n to {\n background-position: -200% 0;\n }\n }\n @media (prefers-reduced-motion: reduce) {\n #twc-nm-overlay,\n #twc-nm-overlay .twc-nm-card,\n #twc-nm-overlay .twc-nm-rec-img {\n transition: none;\n }\n #twc-nm-overlay .twc-nm-card,\n #twc-nm-overlay.twc-nm--panel .twc-nm-card {\n transform: none;\n }\n #twc-nm-overlay .twc-nm-skel {\n animation: none;\n }\n }\n ",document.head.appendChild(e)}();const s=c.stringArray("data-fields")||["email"],l=i("data-type")||"notify-me",d="panel"===i("data-display")?"panel":"modal",u=i("data-auth")||"proxy",p=i("data-tenant")||"victoria-woods",w=i("data-proxy-app")||"twc-sdk",f=i("data-access-token")||window.TWC_ACCESS_TOKEN||null,y=!1!==c.boolean("data-recommendations"),h=c.number("data-recommendations-count"),g=G({wrapper:e,config:{fields:s,type:l,display:d,auth:{mode:u,proxyApp:w,accessToken:f},tenant:p,recommendationsEnabled:y,recommendationsCount:null!==h&&h>0?h:8,recommendationsProfile:(i("data-recommendations-profile")||"").trim()||null,currency:i("data-currency")||null,customerEmail:H(i),copy:I(c.json("copy"),l),productSummary:!0===c.jsonBoolean("productSummary"),classes:m(c.json("classes")),jsonConfigured:null!==t},localization:O(i)});g&&(document.addEventListener("click",e=>{const n=e.target,t=n instanceof Element?n.closest(F):null;t&&(e.preventDefault(),g.open(t))},!0),n&&!n.matches(F)&&n.addEventListener("click",()=>g.open(n)))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",K):K()}();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "notify-me-wl",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.1",
|
|
4
4
|
"description": "Coming Soon / Notify Me wishlist widget for retailer product pages.",
|
|
5
5
|
"main": "build/notify-me-wl.min.js",
|
|
6
6
|
"files": [
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"dev": "rollup -c -w",
|
|
13
13
|
"typecheck": "tsc --noEmit",
|
|
14
14
|
"selftest": "npm run build && sh harness/run-selftest.sh",
|
|
15
|
+
"visual": "npm run build && sh harness/run-visual.sh",
|
|
15
16
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
16
17
|
},
|
|
17
18
|
"author": "",
|