@fluid-app/fluid-cli-theme-dev 0.1.29 → 0.1.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,452 @@
1
+ ---
2
+ name: themes-cart-feedback
3
+ description: |
4
+ Wire up shopper feedback for cart mutations in a Fluid Liquid theme, using the FairShare
5
+ web-widget SDK. Two surfaces: (1) the built-in feedback — a toast that confirms or reports
6
+ a cart add/update/remove and add-to-cart buttons that spin while the request is in flight,
7
+ enabled with `FairShareSDK.configureCartFeedback(...)` or `data-fluid-toast` /
8
+ `data-fluid-button-loading` script attributes; (2) the cart operation events —
9
+ `CART_OPERATION_SUCCESS` and `CART_OPERATION_ERROR` window events that fire on every cart
10
+ mutation regardless of toast config, so custom UI can react. Use when building or reviewing
11
+ anything that adds to cart or reacts to a cart change: custom product cards, shop / collection
12
+ grids, cart drawers and cart sections, mini-cart badges, "added to cart" confirmations, and
13
+ enrollment-pack join flows. Companion to `themes-review`'s FairShare `data-fluid-*` attributes
14
+ (those trigger the mutation; this skill handles the result). Edits are local Liquid/JS/CSS;
15
+ never run `fluid theme push` without explicit approval.
16
+ ---
17
+
18
+ # Cart Feedback & Cart Operation Events
19
+
20
+ When a shopper adds, updates, or removes a cart item on a Fluid theme, the FairShare web-widget SDK can surface the result two ways:
21
+
22
+ 1. **Built-in feedback** — a **toast** confirms success or reports a failure, and add-to-cart **buttons** show a spinner while the request is in flight. Opt in once; zero custom JS. This is the right default for most themes.
23
+ 2. **Cart operation events** — the SDK dispatches `CART_OPERATION_SUCCESS` and `CART_OPERATION_ERROR` on `window` for **every** mutation, whether or not the toast is enabled. Listen to these when you're building custom commerce UI that has to react — update a mini-cart badge, re-render a cart drawer, flash an inline confirmation on the specific product card, or route an enrollment flow forward.
24
+
25
+ This skill is the **result** half of theme commerce. The **trigger** half — the declarative `data-fluid-add-to-cart`, `data-fluid-add-enrollment-pack`, `data-fluid-cart` attributes and the CDN `<script>` — lives in the `themes-review` skill's [FairShare attributes reference](../themes-review/references/fairshare-attributes.md). Read that first if the theme doesn't yet load the SDK or wire up add-to-cart buttons.
26
+
27
+ ## Which path do I need?
28
+
29
+ | You want to… | Use |
30
+ | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------- |
31
+ | Confirm every add/update/remove with a standard toast, and spin add-to-cart buttons | **Built-in feedback** ([Part 1](#part-1--built-in-feedback)) |
32
+ | Localize or restyle that toast, or move it to a corner | **Built-in feedback** + config |
33
+ | Update a custom cart-count badge / mini-cart when the cart changes | **Events** ([Part 2](#part-2--cart-operation-events)) |
34
+ | Flash an inline "Added ✓" on the exact product card the shopper clicked | **Events**, matched by `variantIds` |
35
+ | Re-render a hand-built cart drawer or cart section on update/remove | **Events**, filtered by `category` |
36
+ | Advance an enrollment-pack join flow after the pack lands (or show its error) | **Events**, keyed on `enrollmentPackId` |
37
+ | Report a failure that the SDK would otherwise swallow (`addCartItems`, `decrementCartItem`) | **Events** — `CART_OPERATION_ERROR` (a `try/catch` won't see it) |
38
+
39
+ Most themes want the built-in feedback for the common case **and** a small event listener or two for their custom UI. The two coexist — enabling the toast does not stop the events from firing.
40
+
41
+ ## Prerequisite — the SDK must be loaded
42
+
43
+ Everything here depends on exactly one `<script id="fluid-cdn-script">` with `data-fluid-shop` set, loaded by `layouts/theme.liquid`:
44
+
45
+ ```html
46
+ <script
47
+ id="fluid-cdn-script"
48
+ src="https://assets.fluid.app/scripts/fluid-sdk/latest/web-widgets/index.js"
49
+ data-fluid-shop="{{ shop.handle }}"
50
+ defer
51
+ ></script>
52
+ ```
53
+
54
+ Without it, `window.FairShareSDK` is undefined and no events fire. See the [FairShare attributes reference](../themes-review/references/fairshare-attributes.md) for the full script-tag contract. **Always guard SDK access with `window.FairShareSDK?.`** — with `defer`, the theme's own inline scripts can run before the SDK finishes initializing.
55
+
56
+ ---
57
+
58
+ # Part 1 — Built-in feedback
59
+
60
+ The built-in toast + button-loading covers the 80% case with no custom JavaScript. Turn it on once, localize the strings, optionally restyle, and you're done.
61
+
62
+ ## Enable it
63
+
64
+ Two ways, and they merge (**precedence: `configureCartFeedback()` call > script-tag attributes > defaults**).
65
+
66
+ ### Runtime (recommended — lets you localize)
67
+
68
+ Put this in `layouts/theme.liquid` (or a section that renders on every commerce page), inside a `DOMContentLoaded` listener so the SDK is ready and Liquid can inject translated strings:
69
+
70
+ ```liquid
71
+ <script>
72
+ window.addEventListener("DOMContentLoaded", () => {
73
+ window.FairShareSDK?.configureCartFeedback({
74
+ toast: true,
75
+ buttonLoading: true,
76
+ position: "bottom-right",
77
+ messages: {
78
+ add: {{ 'cart.added' | t | json }},
79
+ update: {{ 'cart.updated' | t | json }},
80
+ remove: {{ 'cart.removed' | t | json }},
81
+ error: {{ 'cart.error' | t | json }},
82
+ },
83
+ });
84
+ });
85
+ </script>
86
+ ```
87
+
88
+ - The optional-chaining `?.` is required — the `defer`'d SDK may not have attached `configureCartFeedback` yet on a slow load; the guard makes a no-op instead of a `TypeError`.
89
+ - `{{ '…' | t | json }}` runs the locale lookup **and** JSON-escapes the result, so quotes/newlines/apostrophes in a translation can't break the script. Never hand-quote a `{{ '…' | t }}` string. See [i18n](#localize-the-messages).
90
+
91
+ ### Script-tag attributes (no localization, quickest)
92
+
93
+ Add attributes to the same `<script id="fluid-cdn-script">` tag:
94
+
95
+ ```html
96
+ <script
97
+ id="fluid-cdn-script"
98
+ src="https://assets.fluid.app/scripts/fluid-sdk/latest/web-widgets/index.js"
99
+ data-fluid-shop="{{ shop.handle }}"
100
+ data-fluid-toast="true"
101
+ data-fluid-button-loading="true"
102
+ data-fluid-toast-position="bottom-right"
103
+ defer
104
+ ></script>
105
+ ```
106
+
107
+ Use this only when the theme doesn't need translated strings. Anything you can pass as a `data-fluid-toast-*` attribute you can also pass to `configureCartFeedback()`; the full attribute catalog is in [references/cart-operation-events.md](references/cart-operation-events.md).
108
+
109
+ ## The knobs
110
+
111
+ | Key (`configureCartFeedback`) | Type | Default | Notes |
112
+ | ----------------------------- | --------- | ---------------- | ---------------------------------------------------------------------------------- |
113
+ | `toast` | boolean | `false` | Master switch for the toast. |
114
+ | `buttonLoading` | boolean | `false` | Auto-spinner on `data-fluid-add-to-cart` / `data-fluid-add-enrollment-pack` buttons. |
115
+ | `position` | string | `bottom-center` | `bottom-center` \| `bottom-left` \| `bottom-right` \| `top-center` \| `top-left` \| `top-right`. |
116
+ | `duration` | number | `4000` | Auto-dismiss delay in ms. |
117
+ | `class` | string | — | Extra CSS class on the toast, for styling. |
118
+ | `icon` | boolean | `true` | Show the variant icon. |
119
+ | `successIcon` / `errorIcon` / `closeIcon` | string | built-ins | Emoji, text, or SVG markup. |
120
+ | `messages` | object | English defaults | `{ add, update, remove, error }` string overrides. |
121
+
122
+ Default messages: `add` → "Added to cart", `update` → "Cart updated", `remove` → "Removed from cart", `error` → "Something went wrong. Please try again."
123
+
124
+ **Success toasts are suppressed while the cart drawer is open** (the drawer already shows the change); **error toasts always display**.
125
+
126
+ ## Localize the messages
127
+
128
+ Drive every string through the theme's `locales/*.json` with the `t` filter, and always chain `| json`:
129
+
130
+ ```liquid
131
+ messages: {
132
+ add: {{ 'cart.added' | t | json }},
133
+ update: {{ 'cart.updated' | t | json }},
134
+ remove: {{ 'cart.removed' | t | json }},
135
+ error: {{ 'cart.error' | t | json }},
136
+ },
137
+ ```
138
+
139
+ `| json` is not optional — it's what makes a French `"Ajouté à votre panier"` or an apostrophe-bearing string safe to drop into JavaScript. Add the four keys to `locales/en.json` (and every other locale the theme ships).
140
+
141
+ ## Button loading
142
+
143
+ Two ways to spin a button while its cart request is in flight.
144
+
145
+ ### Declarative — for `data-fluid-*` buttons
146
+
147
+ With `buttonLoading: true` (or `data-fluid-button-loading="true"`), any button carrying `data-fluid-add-to-cart` or `data-fluid-add-enrollment-pack` spins automatically. Customize the label with `data-fluid-loading-text`:
148
+
149
+ ```liquid
150
+ <button
151
+ type="button"
152
+ data-fluid-add-to-cart="{{ product.first_variant.id }}"
153
+ data-fluid-loading-text="{{ 'product.adding' | t }}"
154
+ >
155
+ {{ 'product.add_to_cart' | t }}
156
+ </button>
157
+ ```
158
+
159
+ ### Programmatic — for buttons that call the SDK directly
160
+
161
+ When you drive the mutation from your own JS (not via a `data-fluid-*` attribute), wrap the call so the spinner clears no matter how it resolves:
162
+
163
+ ```html
164
+ <button id="custom-add" type="button">Add to cart</button>
165
+ <script>
166
+ document.getElementById("custom-add")?.addEventListener("click", (e) => {
167
+ window.FairShareSDK?.withButtonLoading(e.currentTarget, () =>
168
+ window.FairShareSDK?.addCartItems(11111, { quantity: 1 }),
169
+ );
170
+ });
171
+ </script>
172
+ ```
173
+
174
+ - `withButtonLoading(el, fn)` — runs `fn`, shows the spinner, and clears it in a `finally` (so an error still stops the spin). Prefer this.
175
+ - `setButtonLoading(el, on)` — manual toggle; idempotent, sets `aria-busy`. Use only when you can't wrap the call in a single function (then clear it in your own `finally`).
176
+
177
+ The spinner inherits the button's text color, so it fits any button style with no extra CSS.
178
+
179
+ ## Style the toast
180
+
181
+ The toast renders in **light DOM** with `id="fluid-toast"`, so plain CSS in an `assets/*.css` reaches it. Target state with the data attributes it carries — `data-variant` (`success` / `error`), `data-position`, `data-state` (`open` / `closed`) — plus any `class` you passed:
182
+
183
+ ```css
184
+ #fluid-toast {
185
+ border-radius: var(--border_radius, 8px);
186
+ font-family: var(--font_family);
187
+ box-shadow: 0 8px 24px rgb(0 0 0 / 0.15);
188
+ }
189
+ #fluid-toast[data-variant="success"] {
190
+ background: var(--primary_color, #0f9d58);
191
+ color: #fff;
192
+ }
193
+ #fluid-toast[data-variant="error"] {
194
+ background: #d93025;
195
+ color: #fff;
196
+ }
197
+ ```
198
+
199
+ Reuse the theme's own CSS variables (`var(--primary_color)`, `var(--border_radius)`) so the toast matches the theme instead of hardcoding brand colors.
200
+
201
+ ## Suppress one toast without killing its event
202
+
203
+ Pass `{ toast: false }` to a single mutation to skip its toast while the event still fires (handy when your custom UI already confirms that specific action):
204
+
205
+ ```javascript
206
+ await window.FairShareSDK?.addCartItems(11111, { quantity: 1, toast: false });
207
+ ```
208
+
209
+ ---
210
+
211
+ # Part 2 — Cart operation events
212
+
213
+ For anything the built-in toast can't express — a live cart-count badge, a hand-built cart drawer, a per-card confirmation, an enrollment flow — listen to the two window events. **They fire on every mutation regardless of toast config.**
214
+
215
+ ## The two events
216
+
217
+ ```javascript
218
+ window.addEventListener("CART_OPERATION_SUCCESS", (e) => {
219
+ // e.detail.operation, e.detail.category, e.detail.cart, …
220
+ });
221
+
222
+ window.addEventListener("CART_OPERATION_ERROR", (e) => {
223
+ // e.detail.operation, e.detail.category, e.detail.message, …
224
+ });
225
+ ```
226
+
227
+ The event names are **exact, uppercase, underscored** — `CART_OPERATION_SUCCESS` / `CART_OPERATION_ERROR`. `cart_operation_success`, `CartOperationSuccess`, or any other casing silently never fires.
228
+
229
+ ## The `detail` payload
230
+
231
+ Both events carry a `CustomEvent.detail`. The fields you'll actually use:
232
+
233
+ | Field | On | Use |
234
+ | ------------------ | ------------- | --------------------------------------------------------------------- |
235
+ | `operation` | both | The mutation name, e.g. `"addCartItems"`. |
236
+ | `category` | both | `"add"` \| `"update"` \| `"remove"` — the coarse bucket to switch on. |
237
+ | `cart` | success only | The resulting cart object. |
238
+ | `message` | error only | Best-effort human-readable error text — surface it or fall back to your own copy. |
239
+ | `variantIds` | when known | `number[]` of affected variants — match a success back to a card. |
240
+ | `enrollmentPackId` | when known | The pack id, on `addEnrollmentPack`. |
241
+ | `itemId` | when known | Cart item id for item-scoped ops. |
242
+
243
+ Full field list (plus `toast`, `timestamp`, `error`) is in [references/cart-operation-events.md](references/cart-operation-events.md).
244
+
245
+ > **Reading the cart count:** rather than guessing the shape of `e.detail.cart`, the SDK exposes `window.FairShareSDK.getCartItemCount()` — call it from the success handler for a reliable count. Use `e.detail.cart` only for fields you've confirmed exist.
246
+
247
+ ## Attach the listener once
248
+
249
+ Register the listener a single time, at the theme/layout level — **not** inside a section that can render more than once, or per product card. Duplicate listeners double-fire.
250
+
251
+ ```html
252
+ {%- comment -%} In layouts/theme.liquid or a once-per-page section {%- endcomment -%}
253
+ <script>
254
+ window.addEventListener("CART_OPERATION_SUCCESS", (e) => {
255
+ const badge = document.getElementById("fluid-cart-count");
256
+ if (badge) badge.textContent = String(window.FairShareSDK?.getCartItemCount() ?? "0");
257
+ });
258
+ </script>
259
+ ```
260
+
261
+ If you must attach from a section that may appear multiple times, guard against re-registering:
262
+
263
+ ```javascript
264
+ if (!window.__themeCartListenerAttached) {
265
+ window.__themeCartListenerAttached = true;
266
+ window.addEventListener("CART_OPERATION_SUCCESS", handleCartChange);
267
+ }
268
+ ```
269
+
270
+ ## Error handling — the swallow-vs-reject rule
271
+
272
+ **This is why the error event matters.** Some mutations reject their promise on failure (you can `try/catch`); two **swallow** the failure and resolve `undefined` for backward compatibility, so a `try/catch` sees nothing:
273
+
274
+ | Mutation | On failure | Can `try/catch` see it? |
275
+ | ----------------------- | --------------------------------- | ----------------------- |
276
+ | `addCartItems` | resolves `undefined` (swallowed) | **No** — use the event |
277
+ | `decrementCartItem` | resolves `undefined` (swallowed) | **No** — use the event |
278
+ | `addEnrollmentPack` | rethrows | Yes |
279
+ | `updateCartItems` | rethrows | Yes |
280
+ | `updateCartItemVariant` | rethrows | Yes |
281
+ | `removeCartItemById` | rethrows | Yes |
282
+
283
+ `CART_OPERATION_ERROR` fires for **all six**. So the durable rule: **surface failures from the event, not from a `try/catch`.** A `catch` block after `addCartItems` is dead code.
284
+
285
+ ```javascript
286
+ window.addEventListener("CART_OPERATION_ERROR", (e) => {
287
+ showBanner(e.detail.message); // fires even for the swallowing mutations
288
+ });
289
+ ```
290
+
291
+ ## Worked examples
292
+
293
+ ### A. Live cart-count badge (product cards, shop grids, navbar)
294
+
295
+ The single most common need. One listener updates the count wherever the shopper adds from — a product card, a collection grid, a quick-add. Put the badge markup where you want it and the listener once in the layout:
296
+
297
+ ```html
298
+ <span id="fluid-cart-count">{{ cart.item_count }}</span>
299
+
300
+ <script>
301
+ window.addEventListener("CART_OPERATION_SUCCESS", () => {
302
+ const badge = document.getElementById("fluid-cart-count");
303
+ if (badge) badge.textContent = String(window.FairShareSDK?.getCartItemCount() ?? "0");
304
+ });
305
+ </script>
306
+ ```
307
+
308
+ ### B. Inline "Added ✓" on the exact card clicked (`variantIds` match)
309
+
310
+ When a grid of product cards shares one listener, use `variantIds` to light up only the card the shopper acted on. Reveal a **separate** confirmation element rather than overwriting the button text — the button's own label is managed by `withButtonLoading`, so leave it alone:
311
+
312
+ ```html
313
+ {%- comment -%} Each card carries its variant id; a hidden confirmation sits alongside the button {%- endcomment -%}
314
+ <div class="product-card" data-variant-id="{{ product.first_variant.id }}">
315
+ <button
316
+ type="button"
317
+ onclick="window.FairShareSDK?.withButtonLoading(this, () => window.FairShareSDK.addCartItems({{ product.first_variant.id }}, { quantity: 1 }))"
318
+ >
319
+ {{ 'product.add_to_cart' | t }}
320
+ </button>
321
+ <span class="added-flag" hidden>{{ 'product.added' | t }} ✓</span>
322
+ </div>
323
+
324
+ <script>
325
+ window.addEventListener("CART_OPERATION_SUCCESS", (e) => {
326
+ for (const id of e.detail.variantIds ?? []) {
327
+ const card = document.querySelector(`.product-card[data-variant-id="${id}"]`);
328
+ card?.querySelector(".added-flag")?.removeAttribute("hidden");
329
+ }
330
+ });
331
+ </script>
332
+ ```
333
+
334
+ ### C. Re-render a custom cart drawer / cart section (`category` filter)
335
+
336
+ A hand-built cart section refetches its own markup whenever the cart changes. Every success is an add / update / remove, so all three want a refresh — don't gate on `category` unless you're treating one differently (e.g. animating a removal):
337
+
338
+ ```javascript
339
+ window.addEventListener("CART_OPERATION_SUCCESS", (e) => {
340
+ // e.detail.category is "add" | "update" | "remove" — switch on it only to
341
+ // vary the treatment; any of them changes what the cart section should show.
342
+ refreshCartSection(); // your fetch/re-render of the cart section
343
+ });
344
+ ```
345
+
346
+ Because success toasts self-suppress while the drawer is open, a re-rendering drawer and the toast don't fight.
347
+
348
+ ### D. Enrollment-pack join flow (`enrollmentPackId`)
349
+
350
+ An enrollment CTA adds the pack, spins while it's in flight, then advances on success or shows the error:
351
+
352
+ ```html
353
+ <button
354
+ type="button"
355
+ data-fluid-add-enrollment-pack="{{ section.settings.starter_pack.id }}"
356
+ data-fluid-loading-text="{{ 'enroll.joining' | t }}"
357
+ >
358
+ {{ 'enroll.join_with_pack' | t }}
359
+ </button>
360
+
361
+ <script>
362
+ window.addEventListener("CART_OPERATION_SUCCESS", (e) => {
363
+ if (e.detail.operation === "addEnrollmentPack") {
364
+ window.location.href = "/checkout"; // or reveal the next step
365
+ }
366
+ });
367
+ window.addEventListener("CART_OPERATION_ERROR", (e) => {
368
+ if (e.detail.operation === "addEnrollmentPack") {
369
+ showEnrollError(e.detail.message);
370
+ }
371
+ });
372
+ </script>
373
+ ```
374
+
375
+ Combine `data-fluid-button-loading` (declarative spinner) with the events (routing) — the attribute handles the in-flight state, the event handles the outcome.
376
+
377
+ ---
378
+
379
+ ## Anti-patterns
380
+
381
+ - **Rolling your own `fetch` to the cart API.** You lose the events, the toast, button loading, BFF-mode handling, and locale settings. Always go through `FairShareSDK.addCartItems` / `addEnrollmentPack` / `updateCartItems` / `removeCartItemById`, or the declarative `data-fluid-*` attributes.
382
+ - **Wrong event-name casing.** Only `CART_OPERATION_SUCCESS` / `CART_OPERATION_ERROR` fire. `cart_operation_success`, `CartOperationSuccess`, `cartOperationSuccess` bind a listener that never runs.
383
+ - **`try/catch` after `addCartItems` / `decrementCartItem`.** They swallow failures and resolve `undefined`; the `catch` is dead code. Listen for `CART_OPERATION_ERROR`.
384
+ - **Registering the listener per section / per card.** Sections re-render; you'll double- or triple-fire. Attach once at layout level, or guard with a `window.__…Attached` flag.
385
+ - **Calling `configureCartFeedback` / the SDK without `?.` or before `DOMContentLoaded`.** The `defer`'d SDK may not be ready; guard every access with `window.FairShareSDK?.`.
386
+ - **Hardcoding toast strings.** Use `messages: { add: {{ 'cart.added' | t | json }} }`. Hand-quoting `{{ '…' | t }}` breaks on apostrophes and defeats localization.
387
+ - **Reimplementing a button spinner.** Use `data-fluid-button-loading` or `withButtonLoading` — they set `aria-busy` and inherit the button color for free.
388
+ - **Guessing `e.detail.cart` field names.** Use `getCartItemCount()` for the count; only read cart fields you've verified in a running theme.
389
+
390
+ ## Quick audit
391
+
392
+ From the theme repo root:
393
+
394
+ ```bash
395
+ # Wrong event-name casing (should be exact CART_OPERATION_SUCCESS / _ERROR)
396
+ grep -rnE "addEventListener\((['\"])(cart_operation|CartOperation|cartOperation)" --include='*.liquid' --include='*.js' . 2>/dev/null
397
+
398
+ # Dead try/catch around a swallowing mutation
399
+ grep -rnE -B2 -A4 'addCartItems|decrementCartItem' --include='*.liquid' --include='*.js' . 2>/dev/null | grep -i 'catch'
400
+
401
+ # SDK access without an optional-chaining guard
402
+ grep -rnE 'window\.FairShareSDK\.' --include='*.liquid' --include='*.js' . 2>/dev/null
403
+
404
+ # Hardcoded toast messages (missing | t | json)
405
+ grep -rnE 'messages:[[:space:]]*\{' --include='*.liquid' . 2>/dev/null
406
+
407
+ # Hand-rolled cart fetches that bypass the SDK
408
+ grep -rnE "fetch\(['\"][^'\"]*cart" --include='*.liquid' --include='*.js' . 2>/dev/null
409
+ ```
410
+
411
+ Each hit is a candidate finding — confirm in context before flagging.
412
+
413
+ ## Checklist
414
+
415
+ **SDK & setup**
416
+
417
+ - [ ] Exactly one `<script id="fluid-cdn-script">` with `data-fluid-shop` set (see `themes-review` FairShare reference)
418
+ - [ ] Every `FairShareSDK` access is guarded with `?.` and runs on/after `DOMContentLoaded`
419
+
420
+ **Built-in feedback (Part 1)**
421
+
422
+ - [ ] `configureCartFeedback({ toast, buttonLoading, … })` called once, in a `DOMContentLoaded` listener
423
+ - [ ] `messages` sourced from `locales/*.json` via `{{ 'key' | t | json }}` (never hand-quoted)
424
+ - [ ] The four message keys (`cart.added` / `cart.updated` / `cart.removed` / `cart.error`) exist in every locale file
425
+ - [ ] Toast styled through `#fluid-toast` + `data-variant`, reusing theme CSS variables (not hardcoded brand colors)
426
+ - [ ] Button spinners use `data-fluid-button-loading` or `withButtonLoading` (no hand-rolled spinner)
427
+
428
+ **Events (Part 2)**
429
+
430
+ - [ ] Event names are exactly `CART_OPERATION_SUCCESS` / `CART_OPERATION_ERROR`
431
+ - [ ] Listener attached once (layout level or guarded), never per-section / per-card
432
+ - [ ] Failures surfaced from `CART_OPERATION_ERROR`, not a `try/catch` (mandatory for `addCartItems` / `decrementCartItem`)
433
+ - [ ] Per-card / per-item reactions matched via `variantIds` / `itemId`; enrollment via `enrollmentPackId`
434
+ - [ ] Cart count read via `getCartItemCount()`, not a guessed `e.detail.cart` field
435
+
436
+ **Correctness**
437
+
438
+ - [ ] Cart mutations go through the SDK (or `data-fluid-*`), never a hand-rolled `fetch`
439
+ - [ ] Verified in a running theme with `fluid theme dev` (lint does not check runtime JS — see below)
440
+
441
+ ---
442
+
443
+ ## Boundaries — local edits, lint, never push
444
+
445
+ - Everything here is a **local file edit** — Liquid, JS, and CSS in the theme repo. This skill never runs `fluid theme push`, never calls the API, and never writes to the live theme **without explicit approval**.
446
+ - Run `fluid theme lint --json` after schema-adjacent edits and parse the JSON, as with any theme change. **But note:** the linter validates `{% schema %}` and section references only — it does **not** execute or check the JavaScript, the event wiring, or the toast config. The real check for this skill is **`fluid theme dev`**: open the storefront, add/update/remove an item, and watch the toast, the spinner, the badge, and (via the console) the events actually fire.
447
+ - When reviewing a PR rather than authoring, follow the `themes-review` workflow — inline `file:line` findings with a `blocker` / `should` / `nit` severity, and don't push to the author's branch.
448
+
449
+ ## Reference
450
+
451
+ - **[references/cart-operation-events.md](references/cart-operation-events.md)** — the complete catalogs: every mutation's success/failure behavior, the full `detail` field list, the full `configureCartFeedback` config keys, every `data-fluid-toast-*` script attribute, and the toast styling hooks. Read it when you need an exhaustive lookup rather than the common-case guidance above.
452
+ - **[themes-review FairShare attributes](../themes-review/references/fairshare-attributes.md)** — the declarative `data-fluid-*` attributes that *trigger* cart mutations, and the CDN script contract. The trigger half of this skill's result half.
@@ -0,0 +1,209 @@
1
+ # Cart operation events & feedback — full reference
2
+
3
+ > Part of the `themes-cart-feedback` skill. See [`../SKILL.md`](../SKILL.md) for the authoring workflow, decision table, and worked examples. This file is the exhaustive lookup: mutation behavior, the complete `detail` payload, every config key and script attribute, and the toast styling hooks.
4
+
5
+ ## Contents
6
+
7
+ - The two events
8
+ - Mutations that emit events (success/failure behavior)
9
+ - Event `detail` — complete field list
10
+ - `configureCartFeedback()` — complete config keys
11
+ - Script-tag `data-fluid-*` attributes — complete list
12
+ - Precedence & merging
13
+ - Toast styling hooks
14
+ - Button-loading APIs
15
+ - Per-call toast suppression
16
+
17
+ ---
18
+
19
+ ## The two events
20
+
21
+ The SDK dispatches two uniform `window` `CustomEvent`s for **all** cart mutations:
22
+
23
+ - **`CART_OPERATION_SUCCESS`** — fires when a mutation completes successfully.
24
+ - **`CART_OPERATION_ERROR`** — fires when a mutation fails.
25
+
26
+ Both fire **regardless of toast configuration** — enabling or disabling the built-in toast has no effect on the events. Names are exact, uppercase, underscore-separated; any other casing binds a listener that never runs.
27
+
28
+ ```javascript
29
+ window.addEventListener("CART_OPERATION_SUCCESS", (e) => {
30
+ console.log(e.detail.operation, "succeeded", e.detail.cart);
31
+ });
32
+
33
+ window.addEventListener("CART_OPERATION_ERROR", (e) => {
34
+ console.log(e.detail.operation, "failed:", e.detail.message);
35
+ });
36
+ ```
37
+
38
+ ## Mutations that emit events
39
+
40
+ Every cart mutation dispatches one of the two events. The **failure column matters**: two mutations swallow the failure (resolve `undefined`) for backward compatibility, so a `try/catch` around them sees nothing — the error surfaces **only** through `CART_OPERATION_ERROR`. The other four rethrow, so you can `try/catch` *or* use the event.
41
+
42
+ | Function | Category | On success | On failure |
43
+ | ----------------------- | -------- | ------------------------ | ----------------------------------------------------- |
44
+ | `addCartItems` | add | `CART_OPERATION_SUCCESS` | `CART_OPERATION_ERROR` — **resolves `undefined`** (swallowed) |
45
+ | `addEnrollmentPack` | add | `CART_OPERATION_SUCCESS` | `CART_OPERATION_ERROR` — **rethrows** |
46
+ | `updateCartItems` | update | `CART_OPERATION_SUCCESS` | `CART_OPERATION_ERROR` — **rethrows** |
47
+ | `decrementCartItem` | update | `CART_OPERATION_SUCCESS` | `CART_OPERATION_ERROR` — **resolves `undefined`** (swallowed) |
48
+ | `updateCartItemVariant` | update | `CART_OPERATION_SUCCESS` | `CART_OPERATION_ERROR` — **rethrows** |
49
+ | `removeCartItemById` | remove | `CART_OPERATION_SUCCESS` | `CART_OPERATION_ERROR` — **rethrows** |
50
+
51
+ **Rule:** to report a cart failure to the shopper, listen for `CART_OPERATION_ERROR`. It covers all six functions; a `try/catch` covers only the four that rethrow.
52
+
53
+ ## Event `detail` — complete field list
54
+
55
+ Both events expose an identical `detail` object structure. Fields marked "when known" are present only when the SDK has that value for the operation.
56
+
57
+ | Field | Type | Presence | Purpose |
58
+ | ------------------ | ------------------------------- | ------------ | -------------------------------------------------- |
59
+ | `operation` | `string` | always | Function name, e.g. `"addCartItems"`. |
60
+ | `category` | `"add" \| "update" \| "remove"` | always | Coarse operation bucket. |
61
+ | `toast` | `boolean` | always | `false` if the call passed `{ toast: false }`. |
62
+ | `timestamp` | `string` | always | ISO 8601 timestamp. |
63
+ | `cart` | `Cart` | success only | The resulting cart object. |
64
+ | `error` | `unknown` | error only | The raw thrown value. |
65
+ | `message` | `string` | error only | Best-effort human-readable error text. |
66
+ | `variantIds` | `number[]` | when known | Variant ids affected. |
67
+ | `itemId` | `number` | when known | Cart item id (item-scoped ops). |
68
+ | `enrollmentPackId` | `number` | when known | Enrollment pack id (`addEnrollmentPack`). |
69
+
70
+ > Prefer `window.FairShareSDK.getCartItemCount()` over reading a count out of `e.detail.cart` — the count getter is a stable, documented API, whereas the `cart` object's shape is not enumerated here. Read `e.detail.cart` fields only after confirming them in a running theme.
71
+
72
+ ## `configureCartFeedback()` — complete config keys
73
+
74
+ ```javascript
75
+ window.addEventListener("DOMContentLoaded", () => {
76
+ window.FairShareSDK?.configureCartFeedback({
77
+ toast: true,
78
+ buttonLoading: true,
79
+ position: "bottom-right",
80
+ class: "theme-toast",
81
+ duration: 5000,
82
+ icon: true,
83
+ successIcon: "✓",
84
+ errorIcon: "⚠",
85
+ closeIcon: "✕",
86
+ messages: {
87
+ add: "Added! 🎉",
88
+ update: "Cart updated",
89
+ remove: "Removed from cart",
90
+ error: "Couldn't add that — try again.",
91
+ },
92
+ });
93
+ });
94
+ ```
95
+
96
+ All keys optional.
97
+
98
+ | Key | Type | Default | Notes |
99
+ | --------------- | --------- | ---------------- | -------------------------------------------------------------------------------------------------- |
100
+ | `toast` | boolean | `false` | Enable the built-in toast. |
101
+ | `buttonLoading` | boolean | `false` | Auto-spinner on `data-fluid-add-to-cart` / `data-fluid-add-enrollment-pack` buttons. |
102
+ | `position` | string | `bottom-center` | `bottom-center` \| `bottom-left` \| `bottom-right` \| `top-center` \| `top-left` \| `top-right`. |
103
+ | `class` | string | — | Extra CSS class on the toast element. |
104
+ | `duration` | number | `4000` | Auto-dismiss delay (ms). |
105
+ | `icon` | boolean | `true` | Show the variant icon. |
106
+ | `successIcon` | string | built-in | Emoji, text, or SVG markup for the success variant. |
107
+ | `errorIcon` | string | built-in | Emoji, text, or SVG markup for the error variant. |
108
+ | `closeIcon` | string | `✕` | Close-button glyph. |
109
+ | `messages` | object | English defaults | `{ add, update, remove, error }`. Default: "Added to cart" / "Cart updated" / "Removed from cart" / "Something went wrong. Please try again." |
110
+
111
+ ## Script-tag `data-fluid-*` attributes — complete list
112
+
113
+ Set on the `<script id="fluid-cdn-script">` tag. Use these when no localization is needed; otherwise prefer `configureCartFeedback()` so Liquid can inject translated strings.
114
+
115
+ | Attribute | Maps to | Notes |
116
+ | -------------------------------- | -------------------------- | -------------------------------------------------------- |
117
+ | `data-fluid-toast` | `toast` | `"true"` to enable. |
118
+ | `data-fluid-button-loading` | `buttonLoading` | `"true"` to enable. |
119
+ | `data-fluid-toast-class` | `class` | Custom class name. |
120
+ | `data-fluid-toast-position` | `position` | Same six values as `position` (default `bottom-center`). |
121
+ | `data-fluid-toast-duration` | `duration` | Milliseconds (default `4000`). |
122
+ | `data-fluid-toast-icon` | `icon` | `"false"` to hide the icon. |
123
+ | `data-fluid-toast-success-icon` | `successIcon` | Custom success icon. |
124
+ | `data-fluid-toast-error-icon` | `errorIcon` | Custom error icon. |
125
+ | `data-fluid-toast-close-icon` | `closeIcon` | Custom close glyph (default `✕`). |
126
+ | `data-fluid-toast-msg-add` | `messages.add` | Default "Added to cart". |
127
+ | `data-fluid-toast-msg-update` | `messages.update` | Default "Cart updated". |
128
+ | `data-fluid-toast-msg-remove` | `messages.remove` | Default "Removed from cart". |
129
+ | `data-fluid-toast-msg-error` | `messages.error` | Default "Something went wrong. Please try again." |
130
+
131
+ Example:
132
+
133
+ ```html
134
+ <script
135
+ id="fluid-cdn-script"
136
+ src="https://assets.fluid.app/scripts/fluid-sdk/latest/web-widgets/index.js"
137
+ data-fluid-shop="your-shop-id"
138
+ data-fluid-toast="true"
139
+ data-fluid-toast-position="bottom-right"
140
+ data-fluid-toast-duration="4000"
141
+ data-fluid-toast-msg-add="Added! 🎉"
142
+ defer
143
+ ></script>
144
+ ```
145
+
146
+ ## Precedence & merging
147
+
148
+ When both are present, they merge per key: **`configureCartFeedback()` call > `data-fluid-*` attributes > defaults.** So you can set the base config on the script tag and override just the localized strings at runtime.
149
+
150
+ ## Toast styling hooks
151
+
152
+ The toast renders in **light DOM** (no shadow root), so plain theme CSS reaches it. It carries:
153
+
154
+ - `id="fluid-toast"` — the always-present selector hook.
155
+ - any `class` you passed via `class:` / `data-fluid-toast-class`.
156
+ - `data-variant` — `"success"` or `"error"`.
157
+ - `data-position` — the configured position value.
158
+ - `data-state` — `"open"` or `"closed"`.
159
+
160
+ ```css
161
+ #fluid-toast {
162
+ border-radius: var(--border_radius, 8px);
163
+ font-family: var(--font_family);
164
+ box-shadow: 0 8px 24px rgb(0 0 0 / 0.15);
165
+ }
166
+ #fluid-toast[data-variant="success"] { background: var(--primary_color, #0f9d58); color: #fff; }
167
+ #fluid-toast[data-variant="error"] { background: #d93025; color: #fff; }
168
+ ```
169
+
170
+ **Behavior:** success toasts are suppressed while the cart drawer is open (the drawer already reflects the change); error toasts always display.
171
+
172
+ ## Button-loading APIs
173
+
174
+ | API | Behavior |
175
+ | -------------------------- | ------------------------------------------------------------------------------------------------ |
176
+ | `data-fluid-button-loading="true"` (script attr) / `buttonLoading: true` (config) | Global opt-in. Buttons with `data-fluid-add-to-cart` or `data-fluid-add-enrollment-pack` auto-spin. |
177
+ | `data-fluid-loading-text` | Per-button label shown while loading. |
178
+ | `withButtonLoading(el, fn)`| Runs `fn` with the spinner shown; clears it in a `finally`. **Preferred** for custom mutations. |
179
+ | `setButtonLoading(el, on)` | Manual toggle; idempotent; sets `aria-busy`. Clear it yourself in a `finally`. |
180
+
181
+ The spinner inherits the button's text color, so it needs no per-theme CSS.
182
+
183
+ ```javascript
184
+ // Automated cleanup (preferred)
185
+ button.addEventListener("click", (e) => {
186
+ window.FairShareSDK?.withButtonLoading(e.currentTarget, () =>
187
+ window.FairShareSDK.addCartItems(11111, { quantity: 1 }),
188
+ );
189
+ });
190
+
191
+ // Manual control
192
+ button.addEventListener("click", async (e) => {
193
+ const btn = e.currentTarget;
194
+ window.FairShareSDK?.setButtonLoading(btn, true);
195
+ try {
196
+ await window.FairShareSDK?.addCartItems(11111, { quantity: 1 });
197
+ } finally {
198
+ window.FairShareSDK?.setButtonLoading(btn, false);
199
+ }
200
+ });
201
+ ```
202
+
203
+ ## Per-call toast suppression
204
+
205
+ Pass `{ toast: false }` to a single mutation to suppress just that toast. The event **still fires** (with `detail.toast === false`); only the toast is skipped.
206
+
207
+ ```javascript
208
+ await window.FairShareSDK?.addCartItems(11111, { quantity: 1, toast: false });
209
+ ```