@fluid-app/fluid-cli-theme-dev 0.1.63 → 0.1.64

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +1 -1
  2. package/dist/dist-cli-preview/.vite/manifest.json +225 -225
  3. package/dist/dist-cli-preview/account-screens.js +1 -1
  4. package/dist/dist-cli-preview/chunks/{Breadcrumb-CZ-skiUy.js → Breadcrumb-BQKo8hPc.js} +1 -1
  5. package/dist/dist-cli-preview/chunks/{Combobox-BGiCYa56.js → Combobox-cf11L_T-.js} +4 -4
  6. package/dist/dist-cli-preview/chunks/{ContactsScreen-vsNgy_gn.js → ContactsScreen-gIiy4u7c.js} +1160 -1155
  7. package/dist/dist-cli-preview/chunks/{MemberManagementProviders-DMwkh3JW.js → MemberManagementProviders-C1bwc3tB.js} +258 -224
  8. package/dist/dist-cli-preview/chunks/{MessagingScreen-DzJOwPGH.js → MessagingScreen-B8ZxAE0L.js} +3904 -3927
  9. package/dist/dist-cli-preview/chunks/{MobileActionSheet-DqVPgFKI.js → MobileActionSheet-DMeTsLlK.js} +100 -100
  10. package/dist/dist-cli-preview/chunks/{MySiteScreen-BSmWlCKA.js → MySiteScreen-Dtw1rsF9.js} +69 -69
  11. package/dist/dist-cli-preview/chunks/OrdersScreen-C-b6i5jB.js +9 -0
  12. package/dist/dist-cli-preview/chunks/{OrdersScreen-CVEl9FnN.js → OrdersScreen-D9HfcTaa.js} +247 -249
  13. package/dist/dist-cli-preview/chunks/{ProfileScreen-BC5gyJDY.js → ProfileScreen-DwXozaLs.js} +1051 -965
  14. package/dist/dist-cli-preview/chunks/{SubscriptionsScreen-CFM4Pufg.js → SubscriptionsScreen-CIdYNpWT.js} +3 -3
  15. package/dist/dist-cli-preview/chunks/{account-screen-runtime-DxhJm6Qd.js → account-screen-runtime-DYMunv0A.js} +2482 -2407
  16. package/dist/dist-cli-preview/chunks/ar-DaN3xEy4.js +351 -0
  17. package/dist/dist-cli-preview/chunks/{arrow-left-UFKI-PQf.js → arrow-left-Bd7dJtIV.js} +1 -1
  18. package/dist/dist-cli-preview/chunks/bg-D3Np0de3.js +351 -0
  19. package/dist/dist-cli-preview/chunks/bn-BDSehnZ3.js +351 -0
  20. package/dist/dist-cli-preview/chunks/{cli-preview-DunPfnjU.js → cli-preview-BT_ARKjk.js} +13 -13
  21. package/dist/dist-cli-preview/chunks/{client.gen-CSTBgfxj.js → client.gen-Ck6Pm2jV.js} +246 -236
  22. package/dist/dist-cli-preview/chunks/contacts-ChSWj70z.js +4 -0
  23. package/dist/dist-cli-preview/chunks/{countries-api-adapter-B3usO25w.js → countries-api-adapter-lxSE748x.js} +2 -2
  24. package/dist/dist-cli-preview/chunks/{create-card-entry-container-DHgsbSyr.js → create-card-entry-container-D6kSC2Aw.js} +20726 -20509
  25. package/dist/dist-cli-preview/chunks/cs-CrmATj4L.js +351 -0
  26. package/dist/dist-cli-preview/chunks/da-CmJdErjd.js +351 -0
  27. package/dist/dist-cli-preview/chunks/de-C0KTNBZq.js +351 -0
  28. package/dist/dist-cli-preview/chunks/el-BOQs49GU.js +351 -0
  29. package/dist/dist-cli-preview/chunks/es-Dt3VSQPu.js +351 -0
  30. package/dist/dist-cli-preview/chunks/fi-B8UZ7eJL.js +351 -0
  31. package/dist/dist-cli-preview/chunks/fr-BomEiNL0.js +351 -0
  32. package/dist/dist-cli-preview/chunks/he-CfmnXjsr.js +351 -0
  33. package/dist/dist-cli-preview/chunks/{hey-api-client-DSvKWGPq.js → hey-api-client-DGRBsZ7Y.js} +3 -3
  34. package/dist/dist-cli-preview/chunks/hi-CTk5zGoJ.js +351 -0
  35. package/dist/dist-cli-preview/chunks/hr-DaNToqEm.js +351 -0
  36. package/dist/dist-cli-preview/chunks/hu-lcAuIwez.js +351 -0
  37. package/dist/dist-cli-preview/chunks/id-CEThcKZG.js +351 -0
  38. package/dist/dist-cli-preview/chunks/it-BCRCoDOg.js +351 -0
  39. package/dist/dist-cli-preview/chunks/ja-BIkFdIfq.js +351 -0
  40. package/dist/dist-cli-preview/chunks/ko-CD3bk6gv.js +351 -0
  41. package/dist/dist-cli-preview/chunks/{log-out-CBUq5Hd-.js → log-out-CSPZjXTK.js} +1 -1
  42. package/dist/dist-cli-preview/chunks/{map-pin-DHfjSyoR.js → map-pin-B2ScoijR.js} +1 -1
  43. package/dist/dist-cli-preview/chunks/messaging-Bq5UPkvG.js +4 -0
  44. package/dist/dist-cli-preview/chunks/{mount-account-screen-pc1VISwF.js → mount-account-screen-C_CdoGir.js} +2453 -2392
  45. package/dist/dist-cli-preview/chunks/ms-DI4mGa5X.js +351 -0
  46. package/dist/dist-cli-preview/chunks/mysite-DcpuKsuW.js +4 -0
  47. package/dist/dist-cli-preview/chunks/nl-B-XAD3Ra.js +351 -0
  48. package/dist/dist-cli-preview/chunks/no-C_NmYGbe.js +351 -0
  49. package/dist/dist-cli-preview/chunks/orders-B_57xKkq.js +4 -0
  50. package/dist/dist-cli-preview/chunks/pl-mQRzJZEW.js +351 -0
  51. package/dist/dist-cli-preview/chunks/{plus-C-lo0pFM.js → plus-CocoE5gC.js} +1 -1
  52. package/dist/dist-cli-preview/chunks/profile-BfM2KxKe.js +4 -0
  53. package/dist/dist-cli-preview/chunks/pt-BYMWPjog.js +351 -0
  54. package/dist/dist-cli-preview/chunks/ro-B98EuFQI.js +351 -0
  55. package/dist/dist-cli-preview/chunks/ru-DBO9IasC.js +351 -0
  56. package/dist/dist-cli-preview/chunks/schemas-D94nZk5x.js +5374 -0
  57. package/dist/dist-cli-preview/chunks/{screen-route-slug-utils-B254xI_J.js → screen-route-slug-utils-BDjSpDoe.js} +1 -1
  58. package/dist/dist-cli-preview/chunks/{sheet-haptics-context-D2WUiXJm.js → sheet-haptics-context-iOjKcNDj.js} +1 -1
  59. package/dist/dist-cli-preview/chunks/{sidebar-CMSWZCPS.js → sidebar-Ckzom4Xc.js} +12 -12
  60. package/dist/dist-cli-preview/chunks/sk-hRvNJfiN.js +351 -0
  61. package/dist/dist-cli-preview/chunks/{store-api-context-BIdILNWL.js → store-api-context-B96Ip2FB.js} +1 -1
  62. package/dist/dist-cli-preview/chunks/subscriptions-8VtIA4w9.js +4 -0
  63. package/dist/dist-cli-preview/chunks/sv-noYtTQbO.js +351 -0
  64. package/dist/dist-cli-preview/chunks/th-CJCAKsR-.js +351 -0
  65. package/dist/dist-cli-preview/chunks/tl-Bx8pJ7p7.js +351 -0
  66. package/dist/dist-cli-preview/chunks/tr-C4tCNiV7.js +351 -0
  67. package/dist/dist-cli-preview/chunks/{trash-2-Bq4IqH0j.js → trash-2-wVweQZMw.js} +4 -4
  68. package/dist/dist-cli-preview/chunks/uk-xFjXQDP3.js +351 -0
  69. package/dist/dist-cli-preview/chunks/{use-optional-store-Csor1Z3z.js → use-optional-store-lZUz976C.js} +2 -2
  70. package/dist/dist-cli-preview/chunks/use-store-DVmgsDXy.js +103 -0
  71. package/dist/dist-cli-preview/chunks/{useInfiniteQuery-BpFtQ7DH.js → useInfiniteQuery-Dtzov9uh.js} +1 -1
  72. package/dist/dist-cli-preview/chunks/{user-BGrQJ1GJ.js → user-B81IrSgp.js} +1 -1
  73. package/dist/dist-cli-preview/chunks/{users-DBL0isYy.js → users-BXTjoCUz.js} +1 -1
  74. package/dist/dist-cli-preview/chunks/vi-BAvhRTgb.js +351 -0
  75. package/dist/dist-cli-preview/chunks/zh_CN-Cyq9zeex.js +351 -0
  76. package/dist/dist-cli-preview/chunks/zh_TW-DFM5MhMA.js +351 -0
  77. package/dist/index.mjs +346 -32
  78. package/dist/index.mjs.map +1 -1
  79. package/dist/skills/themes-cart-feedback/SKILL.md +9 -14
  80. package/dist/skills/themes-review/SKILL.md +40 -34
  81. package/dist/skills/themes-review/references/blocks-vs-sections.md +2 -2
  82. package/dist/skills/themes-review/references/dead-code.md +1 -1
  83. package/dist/skills/themes-review/references/dynamism.md +9 -3
  84. package/dist/skills/themes-review/references/editor-attributes.md +25 -26
  85. package/dist/skills/themes-review/references/examples.md +3 -4
  86. package/dist/skills/themes-review/references/fairshare-attributes.md +5 -5
  87. package/dist/skills/themes-review/references/global-settings.md +56 -67
  88. package/dist/skills/themes-review/references/liquid-correctness.md +10 -15
  89. package/dist/skills/themes-review/references/media-tag.md +8 -7
  90. package/dist/skills/themes-review/references/navigation.md +18 -13
  91. package/dist/skills/themes-review/references/security-accessibility.md +11 -6
  92. package/dist/skills/themes-review/references/setting-types.md +136 -77
  93. package/dist/skills/themes-settings-schema/SKILL.md +15 -2
  94. package/package.json +2 -2
  95. package/dist/dist-cli-preview/chunks/OrdersScreen-d4fJE_dm.js +0 -9
  96. package/dist/dist-cli-preview/chunks/ar-B7NQmkZ1.js +0 -341
  97. package/dist/dist-cli-preview/chunks/bg-CxfL8rZN.js +0 -341
  98. package/dist/dist-cli-preview/chunks/bn-CdhHCK--.js +0 -341
  99. package/dist/dist-cli-preview/chunks/contacts-CpYlEgAq.js +0 -4
  100. package/dist/dist-cli-preview/chunks/cs-DTe-xGeI.js +0 -341
  101. package/dist/dist-cli-preview/chunks/da-B8Cl_vkl.js +0 -341
  102. package/dist/dist-cli-preview/chunks/de-D8cYy8G-.js +0 -341
  103. package/dist/dist-cli-preview/chunks/el-Chjin2JW.js +0 -341
  104. package/dist/dist-cli-preview/chunks/es-mp8RwFN4.js +0 -341
  105. package/dist/dist-cli-preview/chunks/fi-91gcpxoy.js +0 -341
  106. package/dist/dist-cli-preview/chunks/fr-ekvUCKTH.js +0 -341
  107. package/dist/dist-cli-preview/chunks/he-B7QiuquT.js +0 -341
  108. package/dist/dist-cli-preview/chunks/hi-DNNhi7ud.js +0 -341
  109. package/dist/dist-cli-preview/chunks/hr-CAwxMKX_.js +0 -341
  110. package/dist/dist-cli-preview/chunks/hu-D_oQwH4Q.js +0 -341
  111. package/dist/dist-cli-preview/chunks/id-oeAszBoo.js +0 -341
  112. package/dist/dist-cli-preview/chunks/isAfter-BUGgByEf.js +0 -1414
  113. package/dist/dist-cli-preview/chunks/it-Dme6GXSG.js +0 -341
  114. package/dist/dist-cli-preview/chunks/ja-CRnvQNCa.js +0 -341
  115. package/dist/dist-cli-preview/chunks/ko-8mk8A3_I.js +0 -341
  116. package/dist/dist-cli-preview/chunks/messaging-DkyH4CPP.js +0 -4
  117. package/dist/dist-cli-preview/chunks/ms-Bs0GvdOw.js +0 -341
  118. package/dist/dist-cli-preview/chunks/mysite-wvfITV7T.js +0 -4
  119. package/dist/dist-cli-preview/chunks/nl-tsI6lLxl.js +0 -341
  120. package/dist/dist-cli-preview/chunks/no-DMot6qX0.js +0 -341
  121. package/dist/dist-cli-preview/chunks/orders-c13r499A.js +0 -4
  122. package/dist/dist-cli-preview/chunks/pl-DmTxONNg.js +0 -341
  123. package/dist/dist-cli-preview/chunks/profile-CoWo9Sxe.js +0 -4
  124. package/dist/dist-cli-preview/chunks/pt-DgmwnAV-.js +0 -341
  125. package/dist/dist-cli-preview/chunks/ro-B4M17oRc.js +0 -341
  126. package/dist/dist-cli-preview/chunks/ru-BS2gZnbs.js +0 -341
  127. package/dist/dist-cli-preview/chunks/schemas-9_ZwC5nC.js +0 -3977
  128. package/dist/dist-cli-preview/chunks/sk-BdDjjHfn.js +0 -341
  129. package/dist/dist-cli-preview/chunks/subscriptions-BHALQrcU.js +0 -4
  130. package/dist/dist-cli-preview/chunks/sv-CW7FL5HA.js +0 -341
  131. package/dist/dist-cli-preview/chunks/th-BYBMKmSh.js +0 -341
  132. package/dist/dist-cli-preview/chunks/tl-CDigaLsK.js +0 -341
  133. package/dist/dist-cli-preview/chunks/tr-PxiurNCr.js +0 -341
  134. package/dist/dist-cli-preview/chunks/uk-B4aYnJ8t.js +0 -341
  135. package/dist/dist-cli-preview/chunks/vi-BoKSdzH3.js +0 -341
  136. package/dist/dist-cli-preview/chunks/zh_CN-DrCrXsZC.js +0 -341
  137. package/dist/dist-cli-preview/chunks/zh_TW-JmyC1W5K.js +0 -341
@@ -46,7 +46,7 @@ Everything here depends on exactly one `<script id="fluid-cdn-script">` with `da
46
46
  <script
47
47
  id="fluid-cdn-script"
48
48
  src="https://assets.fluid.app/scripts/fluid-sdk/latest/web-widgets/index.js"
49
- data-fluid-shop="{{ shop.handle }}"
49
+ data-fluid-shop="{{ company.subdomain }}"
50
50
  defer
51
51
  ></script>
52
52
  ```
@@ -95,7 +95,7 @@ Add attributes to the same `<script id="fluid-cdn-script">` tag:
95
95
  <script
96
96
  id="fluid-cdn-script"
97
97
  src="https://assets.fluid.app/scripts/fluid-sdk/latest/web-widgets/index.js"
98
- data-fluid-shop="{{ shop.handle }}"
98
+ data-fluid-shop="{{ company.subdomain }}"
99
99
  data-fluid-toast="true"
100
100
  data-fluid-toast-position="bottom-right"
101
101
  defer
@@ -147,7 +147,7 @@ In web-widgets 0.16.0 and later, any button carrying `data-fluid-add-to-cart` or
147
147
  ```liquid
148
148
  <button
149
149
  type="button"
150
- data-fluid-add-to-cart="{{ product.first_variant.id }}"
150
+ data-fluid-add-to-cart="{{ product.selected_or_first_available_variant.id }}"
151
151
  data-fluid-loading-text="{{ 'product.adding' | t }}"
152
152
  >
153
153
  {{ 'product.add_to_cart' | t }}
@@ -292,29 +292,24 @@ window.addEventListener("CART_OPERATION_ERROR", (e) => {
292
292
 
293
293
  ### A. Live cart-count badge (product cards, shop grids, navbar)
294
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:
295
+ The single most common need, and it needs no JavaScript. The SDK keeps the text of every element with `id="fluid-cart-count"` in sync with the cart — whenever the cart is created, refreshed, cleared, or its items change from anywhere on the page:
296
296
 
297
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>
298
+ <span id="fluid-cart-count">0</span>
306
299
  ```
307
300
 
301
+ Do not seed it with `{{ cart.item_count }}`: `cart` exists only in `cart_page` templates, so everywhere else it renders empty until the SDK fills it. To drive a different element, listen for the cart state events (`UPDATE_CART_ITEM`, `ADD_TO_CART`, `REMOVE_FROM_CART`) and read `window.FairShareSDK.getCartItemCount()`.
302
+
308
303
  ### B. Inline "Added ✓" on the exact card clicked (`variantIds` match)
309
304
 
310
305
  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
306
 
312
307
  ```html
313
308
  {%- 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 }}">
309
+ <div class="product-card" data-variant-id="{{ product.selected_or_first_available_variant.id }}">
315
310
  <button
316
311
  type="button"
317
- onclick="window.FairShareSDK?.withButtonLoading(this, () => window.FairShareSDK.addCartItems({{ product.first_variant.id }}, { quantity: 1 }))"
312
+ onclick="window.FairShareSDK?.withButtonLoading(this, () => window.FairShareSDK.addCartItems({{ product.selected_or_first_available_variant.id }}, { quantity: 1 }))"
318
313
  >
319
314
  {{ 'product.add_to_cart' | t }}
320
315
  </button>
@@ -35,7 +35,7 @@ Don't run end to end silently. **Once in a while — after finishing a section,
35
35
 
36
36
  ## Where themes actually live
37
37
 
38
- Themes are **separate Git repos**, one repo per theme. The reference starter is `git@github.com:fluid-commerce/base-theme.git`. They are not part of the fluid Rails monorepo and not part of fluid-mono — fluid-mono only ships the _tooling_ (CLI + validator) that operates on them.
38
+ Themes are **separate Git repos**, one repo per theme. The reference starter is `git@github.com:fluid-commerce/base-theme.git`. They are not part of this monorepo — this monorepo only ships the _tooling_ (CLI + validator) that operates on them.
39
39
 
40
40
  A typical theme repo looks like this (verified against the `base-theme` starter cloned by `fluid theme init`):
41
41
 
@@ -152,28 +152,29 @@ If you feel the urge to nest, rename: `assets/icon-social-twitter.svg`, `compone
152
152
 
153
153
  Every layout file under `layouts/` **must emit both magic drops** for the engine to inject head content and page content:
154
154
 
155
- - `{{ content_for_header }}` — placed inside `<head>`. The engine injects meta tags, analytics, editor-mode scripts, asset preloads, and the FairShare runtime initialization here.
155
+ - `{{ content_for_header }}` — placed inside `<head>`. The engine injects the page `<title>`, meta and Open Graph tags, canonical and `hreflang` links, structured data, the CSRF tag, Fluid's JavaScript globals (`window.fluid_shop`, …), theme stylesheets, and head Global Embeds here. It does **not** load the FairShare SDK — the layout or a Global Embed does.
156
156
  - `{{ content_for_layout }}` — placed where the page content goes (inside `<body>`, typically inside the main container). The selected page template renders into this slot.
157
157
 
158
158
  A layout missing either one is broken. Missing `content_for_header` breaks editor mode, analytics, and head-injected assets. Missing `content_for_layout` renders an empty page.
159
159
 
160
+ Do not write your own `<title>` (or `og:*` tags): `content_for_header` already emits them, so a second one duplicates or overrides Fluid's. `page_title` is not a Fluid variable — it renders empty. Liquid comments are `{% comment %}…{% endcomment %}` or `{% # … %}`; Jinja-style `{# … #}` is printed as literal text.
161
+
160
162
  ```liquid
161
163
  <!doctype html>
162
- <html lang="{{ request.locale.iso_code }}">
164
+ <html lang="{{ localization.language.iso_code | default: 'en' }}">
163
165
  <head>
164
166
  <meta charset="utf-8">
165
167
  <meta name="viewport" content="width=device-width, initial-scale=1">
166
- <title>{{ page_title }}</title>
167
168
 
168
169
  {{ 'theme.css' | asset_url | stylesheet_tag }}
169
170
 
170
- {{ content_for_header }} {# ← required #}
171
+ {{ content_for_header }}
171
172
  </head>
172
173
  <body>
173
174
  {% section 'main_navbar' %}
174
175
 
175
176
  <main>
176
- {{ content_for_layout }} {# ← required #}
177
+ {{ content_for_layout }}
177
178
  </main>
178
179
 
179
180
  {% section 'main_footer' %}
@@ -267,7 +268,7 @@ The `fluid theme` command group is how a theme repo syncs with the Fluid API. Th
267
268
 
268
269
  | Tag | Meaning | Examples |
269
270
  | --------- | ----------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
270
- | `blocker` | The validator would reject it on `fluid theme push`, OR it renders wrong / breaks a page / exposes user data. Must be fixed before merge. | Invalid `type:` value, missing `id`, duplicate `id`, missing `type:` on a block, settings shape is object not array, referenced section not on disk, unescaped user HTML, unclosed `{% if %}` |
271
+ | `blocker` | The validator would reject it on `fluid theme push`, OR it renders wrong / breaks a page / exposes user data. Must be fixed before merge. | Invalid `type:` value, duplicate `id`, missing `id` (lint misses it, the editor can't save it), missing `type:` on a block, settings shape is object not array, referenced section not on disk, `escape` on a section/block `text`/`textarea` value, unclosed `{% if %}` |
271
272
  | `should` | Works today but will hurt later: perf cliff, DRY violation, missing default, missing whitespace control in a hot loop. | `asset_url` inside a `for` loop, 3+ identical `{% render %}` snippets, multiple `product` settings playing the same role, missing `default:` on a user-facing setting |
272
273
  | `nit` | Cosmetic, style, or low-impact. Leave one comment, do not block. | Comment style, optional `default` on internal-only setting, naming nits |
273
274
 
@@ -288,8 +289,8 @@ Decision rule: if `fluid theme lint --json` would error on it, it's `blocker`. I
288
289
 
289
290
  For each entry in `"settings": [...]`:
290
291
 
291
- - `id` is required, non-empty, non-whitespace — **except** for `type: "header"` settings, which carry no `id` and use `content:` instead (the validator accepts them without one).
292
- - `id` must be unique across all settings in the same file (excluding `header` entries, which have no `id`).
292
+ - `id`, when present, must be non-empty and non-whitespace. The validator does **not** flag a *missing* `id`, but a value-producing setting without one cannot be saved by the editor — treat it as a `blocker` yourself. `type: "header"` settings carry no `id` and use `content:` instead.
293
+ - `id` must be unique across all settings in the same array.
293
294
  - `type` is required.
294
295
  - `type` must be one of the canonical types — see the [full list below](references/setting-types.md).
295
296
 
@@ -308,6 +309,10 @@ For each entry in `"blocks": [...]`:
308
309
 
309
310
  - Every `{% section 'name' %}` in a Liquid template must have a matching section file on disk. Missing sections produce errors.
310
311
 
312
+ ### Liquid block tags
313
+
314
+ - Paired block tags (`if`/`endif`, `for`/`endfor`, `case`, `capture`, `unless`, …) must balance. The validator reports an unbalanced tag with its line. The server accepts the upload anyway, and the storefront stops rendering that file correctly, so treat it as a `blocker`.
315
+
311
316
  ### Template vs section block shape
312
317
 
313
318
  - **Section files** (`sections/{name}/index.liquid`): `blocks` is an **array** `[]`.
@@ -329,7 +334,7 @@ touches them. Read the matching file when you hit its topic:
329
334
  - **[Global settings](references/global-settings.md)** — typography/color/spacing tokens, dark mode, `config/settings_schema.json` + `theme.liquid` wiring.
330
335
  - **[Blocks vs. sections](references/blocks-vs-sections.md)** — when a setting belongs in a block; settings-panel ergonomics.
331
336
  - **[Liquid correctness](references/liquid-correctness.md)** — variable scope, balanced tags, whitespace control, typoed ids.
332
- - **[Editor attributes](references/editor-attributes.md)** — `section.fluid_attributes` / `block.fluid_attributes`.
337
+ - **[Editor attributes](references/editor-attributes.md)** — `block.fluid_attributes` (sections are wrapped automatically).
333
338
  - **[FairShare attributes](references/fairshare-attributes.md)** — `data-fluid-*` cart / add-to-cart behavioral attributes + the CDN script.
334
339
  - **[Performance](references/performance.md)** — `asset_url` in loops, render hygiene.
335
340
  - **[Media rendering](references/media-tag.md)** — the `media_tag` filter for responsive, format-negotiated images and video; when raw `<img>`/`<video>` is wrong.
@@ -347,7 +352,7 @@ eyeballing the text.
347
352
 
348
353
  This is one of the most common smells.
349
354
 
350
- **If a section or block has two or more singular resource pickers playing the _same role_, collapse them to one `*_list` setting.**
355
+ **If a section or block has two or more singular `product` pickers playing the _same role_, collapse them to one `product_list` setting.** For every other resource, collapse to **blocks** instead — see the table below.
351
356
 
352
357
  The test: would a user reasonably want N+1 items in the same role? If yes, it's a list. If the section needs _exactly one_ hero product _and separately_ one upsell product, those are two different roles — keep them singular.
353
358
 
@@ -395,18 +400,19 @@ The test: would a user reasonably want N+1 items in the same role? If yes, it's
395
400
  {%- endfor -%}
396
401
  ```
397
402
 
398
- ### Same rule for every resource
403
+ ### Other resources: blocks, not lists
404
+
405
+ `product_list` is the **only** list type Fluid resolves into objects. `collection_list`, `category_list`, `posts_list`, `enrollment_packs_list`, and the rest hand Liquid an array of **IDs**, so a loop that reads `item.title` or `item.url` renders nothing. Do not "fix" repeated singular pickers into those lists.
399
406
 
400
- | Singular smell | List fix |
401
- | ------------------------------------------------------- | ------------------------------------------- |
402
- | 2+ `product` (or `products`) settings, same role | `product_list` |
403
- | 2+ `collection` settings, same role | `collection_list` |
404
- | 2+ `category` settings, same role | `category_list` |
405
- | 2+ `post` settings, same role | `posts_list` |
406
- | 2+ `enrollment` / `enrollment_pack` settings, same role | `enrollment_list` / `enrollment_packs_list` |
407
- | 2+ `blog` settings, same role | `blog_list` |
407
+ | Singular smell | Fix |
408
+ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
409
+ | 2+ `product` settings, same role | One `product_list` (resolved) |
410
+ | 2+ `collection` settings, same role | One block type with a single `collection` setting; the merchant adds a block per collection |
411
+ | 2+ `category` settings, same role | One block type with a single `category` setting |
412
+ | 2+ `enrollment_pack` settings, same role | One block type with a single `enrollment_pack` setting |
413
+ | 2+ `post` / `blog` settings, same role | One block type with a single `post` setting (still an unresolved ID — look it up in `posts`) |
408
414
 
409
- The benefit isn't just lines of code — a `*_list` setting lets the merchant **add or remove** items without a code change and drops the fixed per-slot count. It does **not** give per-item drag-and-drop reorder, though — that's a *blocks* feature. If the merchant needs to reorder items, model them as blocks instead.
415
+ `product_list` lets the merchant **add or remove** products without a code change, but it does **not** give per-item drag-and-drop reorder — that's a *blocks* feature. If the merchant needs to reorder products, model them as blocks with a `product` setting instead.
410
416
 
411
417
  ---
412
418
 
@@ -539,7 +545,7 @@ For a fast first pass: `fluid theme lint --json` from the theme repo root.
539
545
  **Only after explicit approval** (`open the fix PR`, `yes do it`, `apply the fixes`):
540
546
 
541
547
  1. Branch from the PR's head (not main): `git checkout -b fix/<short-slug>`
542
- 2. Apply ONE fix at a time, one commit per finding. Commit messages must follow Conventional Commits (this is the fluid-mono style and matches what most theme repos use):
548
+ 2. Apply ONE fix at a time, one commit per finding. Commit messages must follow Conventional Commits (this monorepo's style, and what most theme repos use):
543
549
 
544
550
  ```
545
551
 
@@ -621,15 +627,15 @@ When reviewing any theme file, run through this in order. Anything unchecked is
621
627
  - [ ] No user-visible literal text in markup (use `text`/`textarea`/`richtext`)
622
628
  - [ ] No hardcoded colors in styles (use `color` / `color_background` or a global token)
623
629
  - [ ] No hardcoded image URLs (use `image_picker` or upload to `assets/`)
624
- - [ ] Setting/resource images and videos render through `| media_tag` (not hand-rolled `<img>`/`<video>`)
630
+ - [ ] Setting/resource images and videos render through `| media_tag` (not hand-rolled `<img>`/`<video>`); a `media_picker` with `fluid_media_id > 0` renders `<fluid-media-widget>` instead
625
631
  - [ ] No external CDN URLs for theme-owned assets
626
632
  - [ ] No hardcoded `href` to fixed paths the company might want to change (use `url`)
627
633
 
628
634
  **Global settings (config + layout)**
629
635
 
630
- - [ ] `config/settings_schema.json` defines `typography`, `color_schema`, and `spacing` groups (at minimum)
631
- - [ ] `layouts/theme.liquid` reads those settings and emits CSS variables on `:root`
632
- - [ ] Sections consume `var(--font-*)`, `var(--color-*)`, `var(--space-*)` rather than re-reading `settings.*` directly
636
+ - [ ] `config/settings_schema.json` defines `typography` and `color_schema` groups, plus `padding` and `corner_radius` when sections use those controls — the editor builds presets only from these exact group names
637
+ - [ ] `layouts/theme.liquid` reads those settings and emits a `--<setting_id>` CSS variable for each on `:root`
638
+ - [ ] Sections consume those variables rather than re-reading `settings.*` directly
633
639
  - [ ] No section-level setting duplicates a global token (per-section overrides are the exception, default `unset`)
634
640
  - [ ] Schema is grouped by `name:` (not a flat array)
635
641
  - [ ] If dark mode is in schema, it's wired in `theme.liquid` (`@media (prefers-color-scheme: dark)` or `[data-theme="dark"]`)
@@ -644,13 +650,13 @@ When reviewing any theme file, run through this in order. Anything unchecked is
644
650
  **Settings ergonomics**
645
651
 
646
652
  - [ ] Section settings panel under ~15 fields, OR repeated variations moved to blocks
647
- - [ ] No 2+ singular resource pickers playing the same role (collapse to `*_list`)
653
+ - [ ] No 2+ singular `product` pickers playing the same role (collapse to `product_list`); other repeated resource pickers become blocks
648
654
  - [ ] No `X_1` / `X_2` / `X_3` parallel-named settings (use blocks)
649
- - [ ] No `*s_list` where the canonical `*_list` exists (e.g. `product_list`, not `products_list`)
655
+ - [ ] No `products_list` / `products` where resolved products were meant (`product_list` is the only resolved list)
650
656
 
651
657
  **Dead code**
652
658
 
653
- - [ ] No section files with zero `{% section 'name' %}` references (after confirming editor metadata)
659
+ - [ ] No section files with zero `{% section 'name' %}` references in a freshly pulled theme (editor-added sections are saved into page templates)
654
660
  - [ ] No component files with zero `{% render 'name' %}` references
655
661
  - [ ] No `{% schema %}` `blocks:` declaring a `type:` the template never handles
656
662
  - [ ] No `assets/` files with zero references in any `.liquid` or `.css` (greppable from the theme root)
@@ -661,7 +667,7 @@ When reviewing any theme file, run through this in order. Anything unchecked is
661
667
  - [ ] No identical `{% render %}` snippet appearing 3+ times
662
668
  - [ ] No `asset_url` inside a `for` loop
663
669
  - [ ] All `for`/`if` in markup context use `{%-` / `-%}` whitespace control
664
- - [ ] `{% if %}` / `{% endif %}` (and `for`/`case`/`capture`) tags balance
670
+ - [ ] `{% if %}` / `{% endif %}` (and `for`/`case`/`capture`) tags balance — `fluid theme lint` reports any that don't
665
671
 
666
672
  **Variable scope**
667
673
 
@@ -683,9 +689,9 @@ When reviewing any theme file, run through this in order. Anything unchecked is
683
689
  - [ ] JS tags have `defer` (or a justification for not)
684
690
  - [ ] No dead assets under `assets/`
685
691
 
686
- **Editor attributes (`section.fluid_attributes` / `block.fluid_attributes`)**
692
+ **Editor attributes (`block.fluid_attributes`)**
687
693
 
688
- - [ ] Every section file emits `{{ section.fluid_attributes }}` on its root wrapper element
694
+ - [ ] Sections carry no editor attributes of their own — Fluid wraps each section with `data-fluid-section` automatically; `section.fluid_attributes` does not exist and renders empty
689
695
  - [ ] Every block iterated via `{% for block in section.blocks %}` carries `{{ block.fluid_attributes }}` on its own root element
690
696
  - [ ] No typos (`fluid_attribute`, `fluid_attr`, `fluidAttributes`)
691
697
  - [ ] No hand-rolled `data-fluid-section-*` attributes — always go through the helper
@@ -699,11 +705,11 @@ When reviewing any theme file, run through this in order. Anything unchecked is
699
705
  - [ ] `data-fluid-subscription-plan-id` carries a single integer (never comma-separated)
700
706
  - [ ] JSON-valued attributes (`bundled-items`, `bundle-selections`) are valid JSON with numeric IDs and quantities
701
707
  - [ ] Attributes live on `<button>` or `<a>` (not `<div>` without semantics)
702
- - [ ] Exactly one `<script id="fluid-cdn-script">` with `data-fluid-shop` set is loaded by the layout
708
+ - [ ] Exactly one `<script id="fluid-cdn-script">` with `data-fluid-shop="{{ company.subdomain }}"` is loaded by the layout (or by a head Global Embed — not both)
703
709
 
704
710
  **Security & a11y**
705
711
 
706
- - [ ] User content escaped (`| escape`) unless it's a HTML-shaped type (`richtext`/`html`/`html_textarea`)
712
+ - [ ] Plain strings escaped (`| escape`): `plaintext` settings, global `settings.*` of type `text` (a plain input in the Theme panel), and resource fields such as `product.title`. HTML-shaped section/block settings — `text`, `textarea`, `richtext`/`rich_text`, `html`/`html_textarea` — render raw and never sit inside an attribute
707
713
  - [ ] Images have `alt`; below-the-fold images have `loading="lazy"`
708
714
  - [ ] Clickable elements use `<button>` (action) or `<a href>` (nav) — never `<div>`
709
715
 
@@ -103,9 +103,9 @@ Rendering:
103
103
  <div class="features features--cols-{{ section.settings.columns }}">
104
104
  {%- for block in section.blocks -%}
105
105
  <article class="feature" {{ block.fluid_attributes }}>
106
- {{ block.settings.icon | media_tag: alt: block.settings.title }}
106
+ {{ block.settings.icon | media_tag: alt: block.settings.icon_alt }}
107
107
  <h3>{{ block.settings.title }}</h3>
108
- <p>{{ block.settings.body }}</p>
108
+ <div>{{ block.settings.body }}</div>
109
109
  </article>
110
110
  {%- endfor -%}
111
111
  </div>
@@ -41,7 +41,7 @@ for d in sections/*/; do
41
41
  done
42
42
  ```
43
43
 
44
- **False-positive guard:** some sections are added dynamically by users through the editor — these don't appear in any `{% section %}` literal but are still live. Before recommending removal, check `config/sections/*.json` (per-section metadata) — if metadata exists, the section is registered with the editor and probably in use. Drop the severity to `nit` and frame it as "appears unused in templates — confirm before removing".
44
+ **False-positive guard:** sections a merchant adds in the Page Editor are saved into the page template itself — as a `{% section 'name', id: "…" %}` tag plus an entry in that template's `{% schema %}` `"sections"` map — so they only show up in a theme you have pulled recently. Run `fluid theme pull` before the audit, and grep the page templates' schemas for `"type": "name"` as well. If you cannot pull, drop the severity to `nit` and frame it as "appears unused in templates on disk — confirm in the editor before removing".
45
45
 
46
46
  ### Components — unused
47
47
 
@@ -51,10 +51,16 @@ Every one of those values — background, padding, heading text, body text, imag
51
51
 
52
52
  ```liquid
53
53
  {%- assign s = section.settings -%}
54
- <section style="background:{{ s.bg_color }};padding:{{ s.section_pad }};">
54
+ {%- assign pt = s.section_pad.top -%}
55
+ {%- assign pb = s.section_pad.bottom -%}
56
+ <section style="
57
+ background: {{ s.bg_color }};
58
+ padding-top: {% if pt contains 'var' %}{{ pt }}{% else %}{{ pt | default: 0 }}px{% endif %};
59
+ padding-bottom: {% if pb contains 'var' %}{{ pb }}{% else %}{{ pb | default: 0 }}px{% endif %};
60
+ ">
55
61
  <h2 style="color:{{ s.text_color }};">{{ s.heading | default: 'Our Bestsellers' }}</h2>
56
- <p>{{ s.body }}</p>
57
- {{ s.banner_image | media_tag: alt: s.heading }}
62
+ <div>{{ s.body }}</div>
63
+ {{ s.banner_image | media_tag: alt: s.banner_image_alt }}
58
64
  {%- if s.show_cta -%}
59
65
  <a href="{{ s.cta_url }}" class="btn">{{ s.cta_label }}</a>
60
66
  {%- endif -%}
@@ -5,16 +5,16 @@
5
5
  ## Contents
6
6
 
7
7
  - What `fluid_attributes` emits
8
- - The two drops
8
+ - Sections are wrapped for you
9
9
  - Correct usage
10
10
  - Findings to surface
11
11
  - Examples from the reference theme
12
12
  - Quick audit
13
13
 
14
14
 
15
- Sections and blocks have a Liquid drop called `fluid_attributes` that emits the `data-fluid-section-*` attributes the visual editor uses to find, select, click-to-edit, and drag-to-reorder the element. **Without these attributes on the right element, the editor cannot interact with the content** — click handlers don't bind, inspector panels don't open, drag-and-drop is dead.
15
+ Blocks have a Liquid value called `fluid_attributes` that emits the `data-fluid-section-*` attributes the visual editor uses to find, select, click-to-edit, and drag-to-reorder the block. **Without these attributes on the block's root element, the editor cannot interact with the block** — click handlers don't bind, inspector panels don't open, drag-and-drop is dead.
16
16
 
17
- > **Note:** Despite the `data-fluid-*` prefix, these are _editor_ attributes — not the same as the runtime [FairShare behavioral attributes](#fairshare-behavioral-attributes--data-fluid-) below (which wire up cart, checkout, enrollment behavior). Keep them straight: `section.fluid_attributes` / `block.fluid_attributes` are Liquid drops the _theme engine_ emits in editor mode; FairShare attributes are _hand-written_ HTML attributes that the FairShare runtime SDK reads.
17
+ > **Note:** Despite the `data-fluid-*` prefix, these are _editor_ attributes — not the same as the runtime [FairShare behavioral attributes](fairshare-attributes.md) (which wire up cart, checkout, enrollment behavior). Keep them straight: `block.fluid_attributes` is emitted by the _theme engine_ in editor mode; FairShare attributes are _hand-written_ HTML attributes that the FairShare runtime SDK reads.
18
18
 
19
19
  These attributes are only populated in editor preview mode; in production rendering, the drops emit an empty string. So a missing call has zero visible effect at runtime — the bug only surfaces in the editor. That's why reviewers must catch it.
20
20
 
@@ -32,25 +32,26 @@ In editor mode, `{{ block.fluid_attributes }}` expands to the full set of `data-
32
32
  ></div>
33
33
  ```
34
34
 
35
- ### The two drops
35
+ ### Sections are wrapped for you
36
36
 
37
- | Drop | Where it's available | Where to emit it |
38
- | -------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------- |
39
- | `{{ section.fluid_attributes }}` | Inside a section file (`sections/{name}/index.liquid`) | On the **root** wrapper element of the section |
40
- | `{{ block.fluid_attributes }}` | Inside a `{% for block in section.blocks %}` loop, or when a block context is passed to a render | On the **root** wrapper element of _each_ block |
37
+ Fluid renders every section inside a wrapper element carrying `data-fluid-section="<id>"` and `data-fluid-section-type="<name>"`, in every render, editor or not. **A section needs no editor attributes of its own.** `section.fluid_attributes` does not exist — `{{ section.fluid_attributes }}` renders an empty string, so adding it changes nothing.
41
38
 
42
- **These are the only two.** There is no `component.fluid_attributes`, no dropzone drop, no slot drop, no inline-editing helper. Components are pure partials — they don't have editor presence on their own; the section/block that _renders_ them carries the attributes.
39
+ | Value | Where it's available | Where to emit it |
40
+ | ------------------------------ | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------- |
41
+ | `{{ block.fluid_attributes }}` | Inside a `{% for block in section.blocks %}` loop, a standalone block template, or a render that receives the block | On the **root** wrapper element of _each_ block |
42
+
43
+ **This is the only one.** There is no `section.fluid_attributes`, `component.fluid_attributes`, dropzone drop, slot drop, or inline-editing helper. Components are pure partials — they don't have editor presence on their own; the block that _renders_ them carries the attributes.
43
44
 
44
45
  ### Correct usage
45
46
 
46
47
  ```liquid
47
- {%- comment -%} Section root: section's attributes on the outer wrapper {%- endcomment -%}
48
- <section class="featured-products" {{ section.fluid_attributes }}>
48
+ {%- comment -%} Section root: no attributes needed — Fluid wraps the section {%- endcomment -%}
49
+ <section class="featured-products">
49
50
  {%- for block in section.blocks -%}
50
51
  {%- case block.type -%}
51
52
  {%- when 'heading' -%}
52
53
  <h2 class="featured-products__heading" {{ block.fluid_attributes }}>
53
- {{ block.settings.text | escape }}
54
+ {{ block.settings.text }}
54
55
  </h2>
55
56
 
56
57
  {%- when 'product_card' -%}
@@ -76,19 +77,19 @@ When a block's content is fully delegated to a component, the standard pattern i
76
77
 
77
78
  | You see | Severity | Fix |
78
79
  | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
79
- | Section file has no `{{ section.fluid_attributes }}` anywhere | `blocker` | Add it to the outer wrapper element of the section. Editor cannot select the section without it. |
80
+ | Section file emits `{{ section.fluid_attributes }}` | `nit` | Remove it. It renders nothing; Fluid already wraps the section with `data-fluid-section`. |
80
81
  | Section uses `{% for block in section.blocks %}` and the block's root element has no `{{ block.fluid_attributes }}` | `blocker` | Each block's outer element must carry its own `block.fluid_attributes`. Without it the editor can't click-to-edit individual blocks. |
81
- | Typo: `{{ section.fluid_attribute }}` (singular) | `blocker` | The drop name is `fluid_attributes` (plural). The singular form renders empty. |
82
- | Typo: `{{ block.fluid_attr }}` / `{{ section.fluidAttributes }}` / `{{ section.fluid_attribute_set }}` | `blocker` | Only `fluid_attributes` exists. |
83
- | Attributes applied on a child element instead of the root | `blocker` | Move to the outermost element of the section/block. The editor walks ancestors looking for the closest match; placing it on a child means the editor picks up _that_ element as the section bounds, not the real one. |
84
- | Same `{{ section.fluid_attributes }}` applied to multiple elements | `should` | Apply it once — to the root. Duplicates render the same `data-*` attributes twice in the DOM. |
85
- | Hardcoded `data-fluid-*` attributes (`data-section-id="{{ section.id }}"`) instead of the helper | `blocker` | Replace with `{{ section.fluid_attributes }}` / `{{ block.fluid_attributes }}`. The helper emits a full attribute set; hand-rolling drops most of them. |
82
+ | Typo: `{{ block.fluid_attribute }}` (singular) | `blocker` | The name is `fluid_attributes` (plural). The singular form renders empty. |
83
+ | Typo: `{{ block.fluid_attr }}` / `{{ block.fluidAttributes }}` / `{{ block.fluid_attribute_set }}` | `blocker` | Only `fluid_attributes` exists. |
84
+ | Attributes applied on a child element instead of the block's root | `blocker` | Move to the outermost element of the block. The editor walks ancestors looking for the closest match; placing it on a child means the editor picks up _that_ element as the block bounds, not the real one. |
85
+ | Same `{{ block.fluid_attributes }}` applied to multiple elements | `should` | Apply it once — to the root. Duplicates render the same `data-*` attributes twice in the DOM. |
86
+ | Hardcoded editor attributes (`data-fluid-section-block-id`, `data-fluid-section="{{ section.id }}"`) instead of the helper | `blocker` | Remove them. Use `{{ block.fluid_attributes }}` for blocks; sections are wrapped automatically. Hand-rolling drops most of the set. |
86
87
  | Block-aware component (renders inside `{% for block %}`) has no `attr` parameter to receive `block.fluid_attributes` | `should` | Add an `attr` argument to the component's signature and apply it on the component's root element. |
87
- | Conditional render without protecting against `nil`: `<div {{ section.fluid_attributes }}>` where `section` may be undefined | `should` (rarely `blocker`) | Wrap: `{% if section %}{{ section.fluid_attributes }}{% endif %}`. Same for `block`. Only relevant where the value can legitimately be nil. |
88
+ | Conditional render without protecting against `nil`: `<div {{ header_block.fluid_attributes }}>` where the block may be absent | `should` (rarely `blocker`) | Wrap: `{% if header_block %}{{ header_block.fluid_attributes }}{% endif %}`. Only relevant where the value can legitimately be nil. |
88
89
 
89
90
  ### Examples from the reference theme
90
91
 
91
- Section root with attributes — `sections/main_product/index.liquid:49-51` in the reference fluid theme:
92
+ Block root with attributes — `sections/main_product/index.liquid:49-51` in the reference fluid theme:
92
93
 
93
94
  ```liquid
94
95
  <div
@@ -133,10 +134,8 @@ Multiple blocks each carrying their own attrs — `sections/main_category/index.
133
134
  From the theme repo root:
134
135
 
135
136
  ```bash
136
- # Sections missing section.fluid_attributes entirely
137
- for f in sections/*/index.liquid; do
138
- grep -q 'section\.fluid_attributes' "$f" || echo "MISSING section.fluid_attributes $f"
139
- done
137
+ # Leftover section.fluid_attributes (renders nothing; remove)
138
+ grep -rn 'section\.fluid_attributes' sections/
140
139
 
141
140
  # Sections that iterate blocks but never emit block.fluid_attributes
142
141
  for f in sections/*/index.liquid; do
@@ -150,8 +149,8 @@ done
150
149
  # Common typos
151
150
  grep -rE 'fluid_attribute[^s]|fluidAttributes|fluid_attribute_set' sections/ components/
152
151
 
153
- # Hardcoded data-fluid-* attributes (likely should use the helper)
154
- grep -rE 'data-fluid-section-id|data-fluid-section-block-id|data-fluid-parent-section-type' sections/ components/ \
152
+ # Hardcoded editor attributes (likely should use the helper)
153
+ grep -rE 'data-fluid-section=|data-fluid-section-id|data-fluid-section-block-id|data-fluid-parent-section-type' sections/ components/ \
155
154
  | grep -v 'fluid_attributes'
156
155
  ```
157
156
 
@@ -47,16 +47,15 @@
47
47
  ```
48
48
  ## Theme review — featured_products.liquid
49
49
 
50
- **8 findings** — 3 blocker, 4 should, 1 nit
50
+ **7 findings** — 2 blocker, 4 should, 1 nit
51
51
 
52
52
  Reproduce locally with `fluid theme lint --json`.
53
53
 
54
54
  | Severity | Line | Finding |
55
55
  |---|---|---|
56
56
  | blocker | 9 | Invalid type `text_area` — use `textarea`. Validator rejects. |
57
- | blocker | 16 | Missing `{{ section.fluid_attributes }}` on the section root — editor cannot select or click-to-edit this section. Replace `<section>` with `<section {{ section.fluid_attributes }}>`. |
58
- | blocker | 18 | `{% for product in products %}` — `products` is undefined in this scope. It is not a global drop, and the schema declares no `products` setting. Loop silently iterates `nil`. Collapse to the `product_list` fix below (`section.settings.products`) or remove. |
59
- | should | 10 | Type `image` is valid but `image_picker` is clearer for new code |
57
+ | blocker | 18 | `{% for product in products %}` — `products` is Fluid's global product collection, not the products picked in this section, so the loop renders the store's catalog above the picked cards. Remove it; render the picked products through the `product_list` fix below (`section.settings.products`). |
58
+ | should | 10 | Type `image` works in sections only (blocks reject it) — use `image_picker` |
60
59
  | should | 5-8 | Four `product` settings, same role — collapse to one `product_list` with `limit: 4` |
61
60
  | should | 19 | `asset_url` resolved inside loop — hoist above |
62
61
  | should | 16-21 | Missing whitespace control on `{% for %}` / `{% endfor %}` |
@@ -17,7 +17,7 @@ These are the **runtime behavioral attributes** consumed by the FairShare web-wi
17
17
 
18
18
  > These attributes **trigger** cart mutations. The **result** — confirming the change with a toast, spinning the button, or reacting in custom UI via the `CART_OPERATION_SUCCESS` / `CART_OPERATION_ERROR` events — is the `themes-cart-feedback` skill (installed as a sibling: [`../../themes-cart-feedback/SKILL.md`](../../themes-cart-feedback/SKILL.md), when present). Reach for it when reviewing product cards, shop grids, cart sections, or enrollment flows that add to cart.
19
19
 
20
- **This is different from `section.fluid_attributes` above.** The editor attributes are emitted by Liquid drops in editor mode. These are static HTML attributes you write into the template that the runtime SDK scans for on `DOMContentLoaded`.
20
+ **This is different from `block.fluid_attributes`** (see [Editor attributes](editor-attributes.md)). The editor attributes are emitted by the theme engine in editor mode. These are static HTML attributes you write into the template that the runtime SDK scans for on `DOMContentLoaded`.
21
21
 
22
22
  ### Naming convention
23
23
 
@@ -29,12 +29,12 @@ All FairShare runtime attributes are `data-fluid-*` (kebab-case, `data-` prefix)
29
29
  <script
30
30
  id="fluid-cdn-script"
31
31
  src="https://assets.fluid.app/scripts/fluid-sdk/latest/web-widgets/index.js"
32
- data-fluid-shop="{{ shop.handle }}"
32
+ data-fluid-shop="{{ company.subdomain }}"
33
33
  defer
34
34
  ></script>
35
35
  ```
36
36
 
37
- `data-fluid-shop` is required. Optional script-level attributes: `data-fluid-api-base-url`, `data-fluid-country` (2-letter ISO), `data-fluid-language` (2–3 letter), `data-fluid-rep-id`, `data-share-guid`, `data-debug`.
37
+ `data-fluid-shop` is required — use `{{ company.subdomain }}`. There is no `shop` global in Fluid, so `{{ shop.handle }}` (a Shopify habit) renders an empty value and the SDK cannot resolve the store. `content_for_header` sets `window.fluid_shop` but does **not** load the SDK: add this tag to the layout once, unless the company already injects it as a head Global Embed — never both. Optional script-level attributes: `data-fluid-api-base-url`, `data-fluid-country` (2-letter ISO), `data-fluid-language` (2–3 letter), `data-fluid-rep-id`, `data-share-guid`, `data-debug`.
38
38
 
39
39
  ### The attributes
40
40
 
@@ -55,10 +55,10 @@ All FairShare runtime attributes are `data-fluid-*` (kebab-case, `data-` prefix)
55
55
 
56
56
  ```html
57
57
  {%- comment -%} Open cart drawer {%- endcomment -%}
58
- <button data-fluid-cart="open">Cart ({{ cart.item_count }})</button>
58
+ <button data-fluid-cart="open">Cart</button>
59
59
 
60
60
  {%- comment -%} Add one item {%- endcomment -%}
61
- <button data-fluid-add-to-cart="{{ product.first_variant.id }}">
61
+ <button data-fluid-add-to-cart="{{ product.selected_or_first_available_variant.id }}">
62
62
  Add to cart
63
63
  </button>
64
64