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 CHANGED
@@ -1,413 +1,338 @@
1
- ### Coming Soon/Notify Me Widget
1
+ # Notify Me / Coming Soon widget
2
2
 
3
- This JavaScript widget allows users to register their interest in a product that is either "coming soon" or "out of stock" (aka "notify me"). Depending on the `data-type` attribute provided, the widget will display appropriate messages and functionality.
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
- ### Official docs
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
- The full implementation guide lives in the TWC documentation site — start here:
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
- - [Notify Me & Coming Soon — Overview](https://the-wishlist-documentation.gitbook.io/the-wishlist-documentation-docs/notify-me-and-coming-soon/overview)
9
+ ---
10
10
 
11
- This README covers installation and the widget's configuration attributes; the docs
12
- site is the primary reference for integration, the Customer Interest API, and
13
- related guides.
11
+ ## Quick start
14
12
 
15
- #### Features:
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
- ### Installation
15
+ **1. The product**, so the widget knows which sizes are out of stock:
28
16
 
29
- Add the following script to your HTML to include the widget:
30
-
31
- ```html
32
- <script src="https://cdn.jsdelivr.net/npm/notify-me-wl/build/notify-me-wl.min.js"></script>
33
- ```
34
-
35
- ### Development / Build
36
-
37
- Source lives in `src/` (TypeScript) and is bundled with Rollup into `build/`.
38
-
39
- ```bash
40
- npm install # install dev dependencies
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
- > **v2.0.0 path change:** the distributed bundle moved from `index.js` to
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
- Add `<div id="notification-widget"></div>` once per page and put the configuration attributes below on it (or, as older installs do, on a `<button id="popup-open">` — its attributes win where both carry one). Anything marked `data-twc-notify-me` opens the widget; the legacy `#popup-open` button does too. `data-fields` is a JSON string of the fields to include, and `data-type` is `notify-me` or `coming-soon`.
53
-
54
- ```html
55
- <div id="notification-widget"></div>
56
- <button id="popup-open" data-fields='["mobile", "firstName", "lastName"]' data-type="notify-me">Open Popup</button>
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
- `window.currentProduct` supplies the product on a product page. Its `variants` may
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 id="popup-open" data-type="notify-me" data-auth="token"
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
- Anything you render here ships to the browser and is readable by anyone who views the
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
- When `"token"` mode has no token, the widget logs a setup error to the console and the
118
- submission fails with the generic error message.
58
+ ---
119
59
 
120
- > **Breaking change in 3.0.0:** the default `data-auth` moved from `"token"` to
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
- ### Display modes
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
- `data-display` selects the widget's geometry. Both modes are blocking and behave
128
- identically otherwise — same dimmed backdrop, same locked page scroll, and the same
129
- close paths (× button, backdrop click, `Esc`).
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
- | | `"modal"` (default) | `"panel"` |
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
- ```html
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
- Panel mode is a CSS-only variation — it adds a `twc-nm--panel` class to the overlay and
145
- changes nothing about the widget's behaviour, so anything documented elsewhere in this
146
- README applies to both modes. Under `prefers-reduced-motion` the panel appears without
147
- sliding.
148
-
149
- ### Shopify markets
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
- Market and country are captured on every submission so interest can be reported per
152
- market. The values come from Liquid, which renders server-side, so the theme passes
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
- ```html
156
- <button id="popup-open"
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
- **The market id is sent as the number only** — `12345`, never
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
- | Attribute | Where | Meaning |
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
- The widget finds the product from the clicked element's closest `data-twc-product`,
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
- Build the product JSON with a compact capture rather than `product | json`, which is
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 variant in product.variants -%}
209
- <button type="button" data-variant-id="{{ 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
- Out-of-stock size buttons must not carry the `disabled` attribute: browsers do not
217
- dispatch clicks on disabled buttons. Style them with a class or `aria-disabled="true"`
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
- Render `#notification-widget` with the configuration once per page (e.g. in
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
- ```liquid
224
- <div id="notification-widget"
225
- data-tenant="your-tenant"
226
- data-type="notify-me"
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
- ### Size pre-selection
125
+ A product with nothing out of stock shows "All sizes are currently in stock." instead of the form.
239
126
 
240
- The size list shows only out-of-stock sizes. On open, the first rule that yields one of
241
- them wins:
127
+ ---
242
128
 
243
- 1. `data-variant-id` on the clicked element.
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
- A product with no out-of-stock sizes shows "All sizes are currently in stock." instead
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
- ### Shop similar styles
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
- When enabled, the widget shows a horizontally scrolling row of recommended products
259
- beneath the form, fetched from:
154
+ **Rules**
260
155
 
261
- ```
262
- https://api.au-aws.thewishlist.io/services/recommendations/api/v1/recommendations/<tenant>/<customerEmail>
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
- The request carries the same `Authorization` and `X-Twc-Tenant` headers as the
266
- customer-interest call, and the tenant doubles as the retailer id in the path.
161
+ ---
267
162
 
268
- The API needs a customer email, resolved in this order:
163
+ ## Copy
269
164
 
270
- 1. `data-customer-email` on `#notification-widget` (or `#popup-open`) (testing).
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
- Shopify does **not** expose either global by default. To get recommendations for
280
- logged-in customers, the theme must inject one, the same way it injects
281
- `window.currentProduct`:
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
- ```liquid
284
- <script>
285
- {% if customer %}
286
- window.customer = { email: {{ customer.email | json }} };
287
- {% endif %}
288
- </script>
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
- Themes often write the bare form as `{{ customer.email | json }} || false`, which makes
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
- When an email resolves from any of the first three sources, the form's Email field is
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
- #### Choosing an engine
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
- Which engine powers the section is a per-tenant server-side setting, not a data
302
- attribute. On first open the widget fetches the tenant config from:
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
- GET /services/eventcollector/api/v1/custom/configs/public
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
- with the same `Authorization` and `X-Twc-Tenant` headers as every other call — despite
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
- ```json
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
- ```json
316
- "websiteRecommendations": { "engine": "ATHOS", "siteIdentifier": "abc123" }
317
- ```
248
+ ---
318
249
 
319
- **If `websiteRecommendations` is absent, the section does not render at all** — no engine
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
- For `ATHOS`, `siteIdentifier` is the Searchspring site ID and is used in both the
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
- https://{siteIdentifier}.a.searchspring.io/boost/{siteIdentifier}/recommend
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
- The recommendation profile tag is resolved in this order, first match winning:
257
+ ---
334
258
 
335
- 1. `profileTag` on `websiteRecommendations`.
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
- The tenant config outranks the attribute deliberately — it is the intended home for this
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
- **The tag must match a profile that exists on the Searchspring site.** An unknown tag
346
- returns `{"profile":{"type":"NotFound"},"results":[]}` with HTTP 200, which the widget
347
- treats as "nothing usable" and renders no section. Verify with:
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
- ```bash
350
- curl -s "https://{siteIdentifier}.a.searchspring.io/boost/{siteIdentifier}/recommend?tags={tag}&limits=3&products={productId}"
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
- Similar-items profiles are seeded by the product and return an empty `results` array when
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
- Searchspring is called with **no** TWC credentials — it is a third-party host and must
358
- never receive a tenant-scoped token.
279
+ <details>
280
+ <summary>Engine selection and internals</summary>
359
281
 
360
- #### Deduping and result counts
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
- The TWC service returns one entry per product variant, so several entries can describe
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
- The row scrolls horizontally. Touch devices pan it natively; on desktop it can also be
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
- Placeholder cards are shown while the request is in flight, so the section reserves its
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
- Prices use `window.Shopify.currency.active` when the storefront exposes it, falling back
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
- If no email can be resolved, or the request fails or returns nothing, the section is
382
- simply not rendered. It never blocks or alters the notify-me submission.
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
- ### Example
309
+ Marketing opt-ins are separate from the notify-me registration: shoppers can register without subscribing.
385
310
 
386
- ```html
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
- <script>
391
- // The script provided will be included here
392
- </script>
393
- ```
313
+ ## Development
394
314
 
395
- ### How It Works
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
- 1. **Initialization**: The script initializes on `DOMContentLoaded` (or immediately, if the page has already loaded), reading configuration from `#notification-widget` and `#popup-open`.
398
- 2. **Styles Injection**: It injects the modal/form styles, all namespaced under `#twc-nm-overlay` so they don't collide with the host theme.
399
- 3. **Modal Creation**: Creates the modal structure and appends it to the `notification-widget` div.
400
- 4. **Dynamic Content**: Based on the `data-type`, it sets the appropriate copy and button text, and builds the fields listed in `data-fields` plus the always-present Size selector.
401
- 5. **Opening**: A click on any `data-twc-notify-me` element (or `#popup-open`) resolves the product, rebuilds the Size selector with that product's out-of-stock sizes, and pre-selects one per [Size pre-selection](#size-pre-selection).
402
- 6. **Form Submission**: On submit, the button enters a "Sending…" state, the configured auth token is resolved, and the form data is POSTed to the API. The result is shown inline (success message then auto-close, or an error message); the popup is never closed on failure.
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
- ### API Request Example
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
- https://api.au-aws.thewishlist.io/services/wsservice/api/wishlist/items/customerInterest
408
- ```
331
+ ---
332
+
333
+ ## Upgrading
409
334
 
410
- Authenticate with an `Authorization: Bearer <token>` header and an
411
- `X-Twc-Tenant: <tenant>` header. The widget resolves the token per
412
- [Auth modes](#auth-modes) and the tenant from `data-tenant`; there is no
413
- `ACCESS_TOKEN` constant in the bundle to replace.
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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}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.5.0",
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": "",