@fluid-app/fluid-cli-theme-dev 0.1.62 → 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
@@ -18,79 +18,60 @@ The most-overlooked layer of a theme is **`config/settings_schema.json` + `layou
18
18
 
19
19
  ### The pattern
20
20
 
21
- 1. **`config/settings_schema.json`** declares theme-wide settings, grouped by `name` (typography, spacing, color_schema, shadows, border_radius, cards, etc.).
22
- 2. **`layouts/theme.liquid`** reads `{{ settings.* }}` and emits **CSS custom properties on `:root`** inside a `<style>` block.
23
- 3. **Sections and components** consume those custom properties via `var(--font-body)`, `var(--color-primary)`, etc. — never re-reading the underlying `settings.*` value.
21
+ 1. **`config/settings_schema.json`** declares theme-wide settings, grouped by `name`. The editor gives four group names special meaning — `typography` (text-preset cards), `color_schema`, `padding`, and `corner_radius` (the presets offered by section and block controls). Other groups (`shadows`, `cards`, `appearance`, …) render as plain lists.
22
+ 2. **`layouts/theme.liquid`** reads `{{ settings.* }}` and emits **CSS custom properties on `:root`** inside a `{% style %}` or `<style>` block, one `--<setting_id>` per setting. The editor only offers a setting as a preset when such a declaration exists, and it writes `--<setting_id>` itself when it manages a variable.
23
+ 3. **Sections and components** consume those custom properties via `var(--font_family_body)`, `var(--color_primary)`, etc. — never re-reading the underlying `settings.*` value.
24
+
25
+ The `themes-settings-schema` skill (installed as a sibling: [`../../themes-settings-schema/SKILL.md`](../../themes-settings-schema/SKILL.md)) has the full typography-preset contract.
24
26
 
25
27
  ### What `config/settings_schema.json` looks like
26
28
 
27
29
  ```json
28
30
  [
29
- {
30
- "name": "theme_info",
31
- "theme_name": "My Theme",
32
- "theme_version": "1.0.0",
33
- "theme_author": "..."
34
- },
35
31
  {
36
32
  "name": "typography",
37
33
  "settings": [
38
- { "type": "header", "content": "Body" },
39
34
  {
40
35
  "type": "font_picker",
41
36
  "id": "font_family_body",
42
- "label": "Font family",
37
+ "label": "Body font",
38
+ "default": "Inter"
39
+ },
40
+ {
41
+ "type": "font_picker",
42
+ "id": "font_family_heading",
43
+ "label": "Heading font",
43
44
  "default": "Inter"
44
45
  },
45
46
  {
46
47
  "type": "range",
47
48
  "id": "font_weight_body",
48
- "label": "Weight",
49
+ "label": "Body weight",
49
50
  "min": 100,
50
51
  "max": 900,
51
52
  "step": 100,
52
53
  "default": 400
53
54
  },
54
- { "type": "header", "content": "Headings" },
55
- {
56
- "type": "font_picker",
57
- "id": "font_family_heading",
58
- "label": "Font family",
59
- "default": "Inter"
60
- },
61
55
  {
62
56
  "type": "range",
63
57
  "id": "font_size_h1",
64
- "label": "H1 size",
58
+ "label": "H1",
65
59
  "min": 24,
66
60
  "max": 96,
67
61
  "step": 1,
68
62
  "default": 48,
69
- "unit": "px"
63
+ "unit": "px",
64
+ "role": "heading",
65
+ "font_family_ref": "font_family_heading"
70
66
  }
71
67
  ]
72
68
  },
73
69
  {
74
70
  "name": "color_schema",
75
71
  "settings": [
76
- {
77
- "type": "color",
78
- "id": "color_primary",
79
- "label": "Primary",
80
- "default": "#0a0a0a"
81
- },
82
- {
83
- "type": "color",
84
- "id": "color_secondary",
85
- "label": "Secondary",
86
- "default": "#ffffff"
87
- },
88
- {
89
- "type": "color",
90
- "id": "color_text",
91
- "label": "Body text",
92
- "default": "#111111"
93
- }
72
+ { "type": "color", "id": "color_primary", "label": "Primary", "default": "#0a0a0a" },
73
+ { "type": "color", "id": "color_secondary", "label": "Secondary", "default": "#ffffff" },
74
+ { "type": "color", "id": "color_text", "label": "Body text", "default": "#111111" }
94
75
  ]
95
76
  },
96
77
  {
@@ -99,7 +80,7 @@ The most-overlooked layer of a theme is **`config/settings_schema.json` + `layou
99
80
  {
100
81
  "type": "checkbox",
101
82
  "id": "enable_dark_mode",
102
- "label": "Enable dark mode toggle",
83
+ "label": "Enable dark mode",
103
84
  "default": false
104
85
  }
105
86
  ]
@@ -107,53 +88,59 @@ The most-overlooked layer of a theme is **`config/settings_schema.json` + `layou
107
88
  ]
108
89
  ```
109
90
 
110
- Note: the **outer array is grouped**, not flat. Each object with a `name:` becomes a settings group in the editor UI. The `theme_info` group is metadata and contains no `settings:` array.
91
+ Note: the **outer array is grouped**, not flat. Each object with a `name:` becomes a settings group in the theme panel. The theme panel drops any setting without an `id`, `type`, and `label` — so `{"type": "header"}` entries render nothing there; use separate groups instead. Fluid does not read a Shopify-style `theme_info` group.
111
92
 
112
93
  ### What `layouts/theme.liquid` does with them
113
94
 
114
95
  ```liquid
115
96
  <head>
116
97
  ...
117
- {{ settings.font_family_body | font_face: font_display: 'swap' }}
118
- {{ settings.font_family_heading | font_face: font_display: 'swap' }}
98
+ {%- comment -%} Load the default fonts yourself; fonts a merchant adds in the Theme panel are linked into this <head> by the editor {%- endcomment -%}
99
+ <link rel="preconnect" href="https://fonts.googleapis.com">
100
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
101
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
119
102
 
120
- <style>
103
+ {% style %}
121
104
  :root {
122
105
  /* Typography */
123
- --font-body: {{ settings.font_family_body | font_family | default: "Inter, system-ui, sans-serif" }};
124
- --font-heading: {{ settings.font_family_heading | font_family | default: "Inter, system-ui, sans-serif" }};
125
- --font-weight-body: {{ settings.font_weight_body | default: 400 }};
126
- --font-size-h1: {{ settings.font_size_h1 | default: 48 | append: 'px' }};
106
+ --font_family_body: {{ settings.font_family_body | default: "Inter" }}, system-ui, sans-serif;
107
+ --font_family_heading: {{ settings.font_family_heading | default: "Inter" }}, system-ui, sans-serif;
108
+ --font_weight_body: {{ settings.font_weight_body | default: 400 }};
109
+ --font_size_h1: {{ settings.font_size_h1 | default: 48 }}px;
127
110
 
128
111
  /* Color */
129
- --color-primary: {{ settings.color_primary | default: '#0a0a0a' }};
130
- --color-secondary: {{ settings.color_secondary | default: '#ffffff' }};
131
- --color-text: {{ settings.color_text | default: '#111111' }};
112
+ --color_primary: {{ settings.color_primary }};
113
+ --color_secondary: {{ settings.color_secondary }};
114
+ --color_text: {{ settings.color_text }};
132
115
  }
133
116
 
134
117
  {%- if settings.enable_dark_mode -%}
135
118
  @media (prefers-color-scheme: dark) {
136
119
  :root {
137
- --color-primary: {{ settings.color_primary_dark | default: '#ffffff' }};
138
- --color-secondary: {{ settings.color_secondary_dark | default: '#0a0a0a' }};
139
- --color-text: {{ settings.color_text_dark | default: '#fafafa' }};
120
+ --color_primary: #ffffff;
121
+ --color_secondary: #0a0a0a;
122
+ --color_text: #fafafa;
140
123
  }
141
124
  }
142
125
  {%- endif -%}
143
- </style>
126
+ {% endstyle %}
144
127
  </head>
145
128
  ```
146
129
 
130
+ A `font_picker` value is a plain family-name string. Do not use Shopify's font filters: `font_family` does not exist in Fluid (unknown filters are silently ignored), and Fluid's `font_face` expects a font object with `family` and `src`, so `{{ settings.font_family_body | font_face }}` emits an empty `@font-face`. A global `color` setting is a color value: it renders as the saved color and also exposes `.rgb`, `.rgba`, `.hue`, `.saturation`, `.lightness`.
131
+
147
132
  And sections then look like:
148
133
 
149
134
  ```css
150
135
  .featured-products h2 {
151
- font-family: var(--font-heading);
152
- font-size: var(--font-size-h1);
153
- color: var(--color-text);
136
+ font-family: var(--font_size_h1_font_family, var(--font_family_heading));
137
+ font-size: var(--font_size_h1);
138
+ color: var(--color_text);
154
139
  }
155
140
  ```
156
141
 
142
+ Read a text preset's own variable (`--<preset_id>_font_family`, written by the editor once a merchant applies the preset) before the font variable, so repointing the preset in the editor moves the theme's own headings too.
143
+
157
144
  ### What belongs at the global level
158
145
 
159
146
  Anything that should stay consistent across the whole theme:
@@ -162,13 +149,12 @@ Anything that should stay consistent across the whole theme:
162
149
  | --------------- | ------------------------------------------------------------------------------------------------------------ |
163
150
  | `typography` | Body + heading font families, font weights, font size scale (`xs`–`9xl`), heading sizes (h1–h6), line height |
164
151
  | `color_schema` | Brand palette (primary, secondary, accent, text, surface, border), plus dark-mode variants |
165
- | `spacing` | Spacing scale, section padding scale |
152
+ | `padding` | Spacing scale used by section/block `padding` controls (preset group) |
166
153
  | `layout` | Max container width, gutter, grid breakpoints |
167
- | `border_radius` | Radius scale (sm, md, lg, full) |
154
+ | `corner_radius` | Radius scale used by `corner_radius` controls (preset group) |
168
155
  | `shadows` | Shadow scale (sm, md, lg) |
169
156
  | `cards` | Card-wide defaults (radius, shadow, padding) that per-card-type groups inherit |
170
157
  | `appearance` | Dark mode toggle, motion-reduce, animation preferences |
171
- | `theme_info` | Theme name, version, author, docs URL — **metadata only, no `settings:`** |
172
158
 
173
159
  ### What does NOT belong at the global level
174
160
 
@@ -183,13 +169,13 @@ The test: _would changing this break the visual coherence of the rest of the the
183
169
 
184
170
  | You see | Severity | Fix |
185
171
  | ------------------------------------------------------------------------------------------------------------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
186
- | Section adds its own `font_family` / `font_size` / `color_*` setting when the global already exists | `should` | Use `var(--font-body)`, `var(--color-primary)`, etc. Per-section overrides should be the _exception_, configured via section setting `color_override` that defaults to `unset`. |
172
+ | Section adds its own `font_family` / `font_size` / `color_*` setting when the global already exists | `should` | Use `var(--font_family_body)`, `var(--color_primary)`, etc. Per-section overrides should be the _exception_, configured via section setting `color_override` that defaults to `unset`. |
187
173
  | Theme has 0 global typography settings but every section hardcodes fonts in inline styles | `blocker` | Add a `typography` group to `config/settings_schema.json` and a `:root` `<style>` block in `layouts/theme.liquid`. |
188
174
  | Theme has 0 global color settings but uses hardcoded hex everywhere | `blocker` | Add a `color_schema` group. |
189
- | `layouts/theme.liquid` reads `settings.*` but emits no `<style>` / CSS variables | `should` | Wire the settings into `--var` declarations on `:root`. Without this layer, sections have no way to consume globals. |
190
- | `config/settings_schema.json` is a **flat array of settings** (no `name:` groups) | `should` | The engine accepts it, but the editor UI flattens to one giant panel. Group by `name:` (typography, color, spacing, …) for usability. |
191
- | `theme_info` group missing | `nit` | Adds version + author metadata visible to admins. |
192
- | Two `font_family_body` and `font_family_heading` settings exist but every section uses a hardcoded `font-family: 'Inter'` | `blocker` | Rewire sections to `var(--font-heading)` / `var(--font-body)`. Globals that nothing consumes are dead weight. |
175
+ | `layouts/theme.liquid` reads `settings.*` but emits no `{% style %}` / `<style>` CSS variables | `should` | Wire the settings into `--var` declarations on `:root`. Without this layer, sections have no way to consume globals. |
176
+ | `config/settings_schema.json` is a **flat array of settings** (no `name:` groups) | `blocker` | The theme panel drops every entry without a group `name`, so a flat array renders an empty Theme panel. Group the settings by `name:` (typography, color_schema, …). |
177
+ | Theme panel groups named `spacing`, `border_radius`, `Typography`, or `colors` | `should` | The editor builds presets only from `typography`, `color_schema`, `padding`, and `corner_radius` (exact, case-sensitive). Rename so section controls offer the presets. |
178
+ | `font_family_body` and `font_family_heading` settings exist but every section uses a hardcoded `font-family: 'Inter'` | `blocker` | Rewire sections to `var(--font_family_heading)` / `var(--font_family_body)`. Globals that nothing consumes are dead weight. |
193
179
  | Dark-mode toggle exists in schema but no `@media (prefers-color-scheme: dark)` / `[data-theme="dark"]` block in `theme.liquid` | `blocker` | Setting is a lie — wire it. |
194
180
 
195
181
  ### Dark-mode patterns — two acceptable shapes
@@ -209,7 +195,10 @@ jq -r '.[].name' config/settings_schema.json
209
195
  grep -E 'settings\.' layouts/theme.liquid | head
210
196
 
211
197
  # 3. Does the layout emit CSS variables?
212
- grep -E '^\s*--[a-z-]+:' layouts/theme.liquid | head
198
+ grep -E '^\s*--[a-z_-]+:' layouts/theme.liquid | head
199
+
200
+ # 3b. Shopify font filters that don't work in Fluid
201
+ grep -nE '\| *(font_face|font_family)\b' layouts/theme.liquid
213
202
 
214
203
  # 4. Do sections actually USE the variables (vs. hardcoded)?
215
204
  grep -rE 'font-family:\s*[A-Za-z]' --include='*.liquid' --include='*.css' . 2>/dev/null # hardcoded fonts
@@ -23,7 +23,7 @@ The variables a template can read fall into three buckets:
23
23
 
24
24
  | Bucket | Available in… | Examples |
25
25
  | ---------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
26
- | **Always available globals** | every template | `request`, `cart`, `customer`, `shop`, `settings` (global), `linklists` |
26
+ | **Always available globals** | every storefront template | `company`, `request`, `params`, `localization`, `country`, `affiliate`, `username`, `routes`, `settings` (global), and the on-demand collections `products`, `collections`, `categories`, `posts`, `enrollment_packs`. No `cart` (only in `cart_page`), and no Shopify `shop`, `customer`, or `linklists` |
27
27
  | **Resource context** | the matching page template, plus any section it renders | `product` (only on `product/{variant}/index.liquid` and sections rendered from it); `collection`, `category`, `post`, etc. |
28
28
  | **Section / block scope** | inside that section only | `section`, `section.settings`, `section.blocks`, `block`, `block.settings` |
29
29
  | **Component args** | inside a component only when explicitly passed | only the keys passed to `{% render 'name', key: value %}` |
@@ -34,7 +34,7 @@ Things that go wrong:
34
34
  | -------------------------------------------------------------------------------------------------------------------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
35
35
  | `{{ product.title }}` inside a section that's rendered on the homepage (`home_page/default/index.liquid`) without an explicit `product:` arg | `blocker` | `product` is only in scope on product page templates. On `home_page` it's `nil`, silently rendering empty. |
36
36
  | `{{ collection.products }}` inside `sections/main_navbar/index.liquid` | `blocker` | `collection` is not in scope in a navbar section. Pass the resource explicitly via a block setting (`collection` type) and read `block.settings.collection.products`. |
37
- | `{{ cart.item_count }}` in a component without `cart` passed as a `render` arg | `should` | `cart` is global, so this often works, but if the component is intended to be self-contained, accept `cart:` explicitly. |
37
+ | `{{ cart.item_count }}` (or any `cart.*`) outside a `cart_page` template | `blocker` | `cart` exists only in `cart_page` templates; elsewhere it renders empty. Use the SDK's `<span id="fluid-cart-count">` for a live count. |
38
38
  | `{{ section.settings.thing }}` where the schema does not declare `thing` | `blocker` | Typo or stale reference. Validator does not catch this — it only checks the schema, not the template. Grep the schema for the `id` and fix. |
39
39
  | `{{ block.settings.thing }}` where the block's `settings:` array does not declare `thing` | `blocker` | Same as above for blocks. |
40
40
  | `{{ block.settings.x }}` inside a section that has `blocks` declared but NOT inside a `{% for block in section.blocks %}` loop | `blocker` | `block` is only in scope inside the iteration; outside it's `nil`. |
@@ -81,19 +81,21 @@ Liquid is forgiving. `nil.foo` does not raise — it renders empty. That's worse
81
81
  <h2>{{ section.settings.heading | default: 'Featured products' }}</h2>
82
82
  ```
83
83
 
84
- **`blocker`** when a method-chain on a resource picker is unguarded and the page can render with a blank picker:
84
+ **`blocker`** when markup around a resource picker is unguarded and the page can render with a blank picker. An unset resolved setting is an empty object — the chain renders empty rather than raising, leaving an empty `<span>` or a broken link behind. Guard with `!= blank` (a bare `{% if p %}` is always true for an empty object):
85
85
 
86
86
  ```liquid
87
- {%- comment -%} Bad: `.first_variant.price` chain blows up if product is blank {%- endcomment -%}
88
- <span>{{ section.settings.product.first_variant.price | money }}</span>
87
+ {%- comment -%} Bad: renders an empty <span> when no product is picked {%- endcomment -%}
88
+ <span>{{ section.settings.product.selected_or_first_available_variant.price }}</span>
89
89
 
90
90
  {%- comment -%} Good {%- endcomment -%}
91
91
  {%- assign p = section.settings.product -%}
92
- {%- if p != blank and p.first_variant != blank -%}
93
- <span>{{ p.first_variant.price | money }}</span>
92
+ {%- if p != blank -%}
93
+ <span>{{ p.selected_or_first_available_variant.price }}</span>
94
94
  {%- endif -%}
95
95
  ```
96
96
 
97
+ Prices on products and variants are already formatted for the visitor's currency. Fluid has no `money` filter — an unknown filter is silently ignored.
98
+
97
99
  ### 2. Whitespace control
98
100
 
99
101
  Liquid keeps every whitespace character outside tags. In tight loops, this inflates HTML by KBs. **`should`** when a `for`/`if` block in markup context lacks `-`:
@@ -116,14 +118,7 @@ Rule of thumb: control tags (`{%- ... -%}`) get dashes; output tags (`{{ ... }}`
116
118
 
117
119
  ### 3. Unclosed tags / mismatched conditionals
118
120
 
119
- **`blocker`.** Liquid's parser is permissive — an unclosed `{% if %}` will consume the rest of the template silently. Quick count check:
120
-
121
- ```bash
122
- grep -cE '{%-?\s*(if|for|case|capture|comment|unless)\b' file.liquid
123
- grep -cE '{%-?\s*end(if|for|case|capture|comment|unless)\b' file.liquid
124
- ```
125
-
126
- The two counts must match.
121
+ **`blocker`.** An unclosed `{% if %}` (or `for`, `case`, `capture`, `unless`, …) is a Liquid syntax error. The upload still succeeds, but the storefront can no longer render that file correctly. `fluid theme lint --json` reports each unbalanced block tag with its line — run it rather than counting tags by hand.
127
122
 
128
123
  ### 4. Filter ordering
129
124
 
@@ -2,9 +2,9 @@
2
2
 
3
3
  > Part of the `themes-review` skill. See [`../SKILL.md`](../SKILL.md) for the review workflow, severity ladder, and validator rules.
4
4
 
5
- Render every setting- or resource-backed image and video with the `media_tag` filter. It turns an image or video into a complete, responsive HTML element — `srcset`, WebP/AVIF negotiation, `loading`, `decoding="async"`, and `alt` — so a hand-rolled `<img>` / `<video>` in a section is almost always the wrong call.
5
+ Render every setting- or resource-backed image and video with the `media_tag` filter — except a Fluid media item from a `media_picker`, which renders as `<fluid-media-widget>` (see the rule below). It turns an image or video into a complete, responsive HTML element — `srcset`, WebP/AVIF negotiation, `loading`, `decoding="async"`, and `alt` — so a hand-rolled `<img>` / `<video>` in a section is almost always the wrong call.
6
6
 
7
- Docs: <https://docs.fluid.app/docs/themes/media-tag>
7
+ Docs: <https://docs.fluid.app/themes/media-tag>
8
8
 
9
9
  ## Contents
10
10
 
@@ -15,14 +15,14 @@ Docs: <https://docs.fluid.app/docs/themes/media-tag>
15
15
 
16
16
  ### 1. The rule
17
17
 
18
- **`should`.** A raw `<img src="{{ ... }}">` or `<video>` that renders an `image` / `image_picker` / `video_picker` / `media_picker` setting, or a resource image (`product.image`, `collection.image`, …), should be `| media_tag`. Hand-rolled markup ships one fixed-size original — no responsive `srcset`, no format negotiation — which bloats the download and hurts LCP, and forces you to remember `loading` / `decoding` / `alt` every time.
18
+ **`should`.** A raw `<img src="{{ ... }}">` or `<video>` that renders an `image` / `image_picker` / `video_picker` setting, a plain-file `media_picker` value, or a resource image (`product.image`, `collection.image`, …), should be `| media_tag`. Hand-rolled markup ships one fixed-size original — no responsive `srcset`, no format negotiation — which bloats the download and hurts LCP, and forces you to remember `loading` / `decoding` / `alt` every time.
19
19
 
20
20
  ```liquid
21
21
  {%- comment -%} Bad: fixed original, manual (easy-to-forget) attributes {%- endcomment -%}
22
22
  <img src="{{ section.settings.banner_image }}" alt="{{ section.settings.heading | escape }}" loading="lazy" decoding="async" />
23
23
 
24
24
  {%- comment -%} Good: responsive, format-negotiated, accessible — one filter {%- endcomment -%}
25
- {{ section.settings.banner_image | media_tag: sizes: '(max-width: 768px) 100vw, 1200px', alt: section.settings.heading }}
25
+ {{ section.settings.banner_image | media_tag: sizes: '(max-width: 768px) 100vw, 1200px', alt: section.settings.banner_image_alt }}
26
26
  ```
27
27
 
28
28
  `media_tag` escapes its own output, so do **not** wrap the setting in `| escape` yourself. It renders nothing on blank input, so the `{%- if image != blank -%}` guard is optional — keep it only when surrounding markup must be suppressed too.
@@ -41,14 +41,14 @@ Pass options as filter arguments. Anything not in this table becomes a plain HTM
41
41
  | `format` | `auto` | WebP/AVIF negotiation. |
42
42
  | `crop` | — | Focus point. |
43
43
  | `loading` | `lazy` | Use `eager` for the hero / above-the-fold image — `lazy` there delays LCP. |
44
- | `alt` | image's alt text | Set it for `*_picker` settings; `alt: ''` for decorative. |
44
+ | `alt` | image's alt text | For `*_picker` settings pass the `<id>_alt` companion; `alt: ''` for decorative. |
45
45
 
46
46
  ```liquid
47
47
  {%- comment -%} Fixed-width logo, retina srcset {%- endcomment -%}
48
48
  {{ section.settings.logo | media_tag: width: 240, alt: 'Logo' }}
49
49
 
50
50
  {%- comment -%} Hero — eager so it doesn't delay LCP {%- endcomment -%}
51
- {{ section.settings.hero | media_tag: sizes: '100vw', loading: 'eager', alt: section.settings.heading }}
51
+ {{ section.settings.hero | media_tag: sizes: '100vw', loading: 'eager', alt: section.settings.hero_alt }}
52
52
  ```
53
53
 
54
54
  ### 3. Video
@@ -61,7 +61,8 @@ A video URL (`.mp4` / `.mov` / `.webm` / `.m4v`) or video object renders a `<vid
61
61
 
62
62
  ### 4. What it does *not* replace
63
63
 
64
+ - **Fluid media items.** A `media_picker` value with a positive `fluid_media_id` is a Fluid media item. Render it as `<fluid-media-widget media-id="{{ media.fluid_media_id }}" embed-type="{{ media.embed_type | default: 'default' }}">`, passing the saved `embed_settings` through as widget attributes (`autoOpen` → `auto-open`, `width`/`height` → `px` lengths when not responsive, `hideCta` → a disabled `cta-options-manual-override`; see [Setting types](setting-types.md#visual--media)) — `media_tag` would emit a bare `<img>`/`<video>` from its `url` and drop the embed (popover, inline shopping, CTAs). Only a plain file (`fluid_media_id` `0` or absent) goes through `media_tag`. Compare with `> 0`: Liquid treats `0` as true. Flag a `media_picker` rendered only through `media_tag` as a `blocker`.
64
65
  - **Static theme assets** (`'logo.svg' | asset_url`) are not ImageKit media — `media_tag` passes non-ImageKit URLs through as a plain tag, so `asset_url` + a plain `<img>` stays correct for files shipped in `assets/`.
65
- - **`alt` judgement** is still yours — `media_tag` defaults `alt` from the image, but `*_picker` settings carry none, so pass one (or `alt: ''` for decorative). The alt rule in [security & accessibility](security-accessibility.md) still applies.
66
+ - **`alt` judgement** is still yours — `media_tag` defaults `alt` from the image, but `*_picker` values carry none. Pass the editor-managed `<id>_alt` setting (for `banner_image`, `section.settings.banner_image_alt`), or `alt: ''` for decorative. The alt rule in [security & accessibility](security-accessibility.md) still applies.
66
67
 
67
68
  ---
@@ -14,7 +14,13 @@ Prefer a `link_list` setting — **the engine's menu selector** — for navigati
14
14
 
15
15
  The findings below are maintainability recommendations. Explain the benefit of managed menus, respect the user's chosen implementation, and verify destinations, dropdowns, accessibility, and desktop/mobile rendering. Hardcoded navigation is supported; it is not a validation blocker.
16
16
 
17
- `link_list` is one of the canonical setting types (a single-resource picker). It points at a _menu_ the company configures in admin; the theme reads it as an iterable of menu items.
17
+ `link_list` is one of the canonical setting types (a single-resource picker). It saves a menu's slug, and Fluid resolves it before the template reads it:
18
+
19
+ ```text
20
+ { title, handle, menu_items: [ { title, url, sub_menu_items: [ { title, url, sub_menu_items }, … ] }, … ] }
21
+ ```
22
+
23
+ This is **not** Shopify's shape: there is no `menu.links`, `link.links`, `link.active`, or `linklists` global. Those render empty.
18
24
 
19
25
  ### Bad — hardcoded nav
20
26
 
@@ -57,33 +63,32 @@ Or this — better, but still wrong: company-level text/URL settings paired into
57
63
 
58
64
  ```liquid
59
65
  {%- assign menu = section.settings.menu -%}
60
- <nav class="main-nav" aria-label="Main navigation">
61
- {%- for link in menu.links -%}
62
- <a href="{{ link.url }}"
63
- class="main-nav__link {% if link.active %}is-active{% endif %}">
64
- {{ link.title | escape }}
65
- </a>
66
- {%- endfor -%}
67
- </nav>
66
+ {%- if menu != blank -%}
67
+ <nav class="main-nav" aria-label="{{ menu.title | escape }}">
68
+ {%- for item in menu.menu_items -%}
69
+ <a href="{{ item.url }}" class="main-nav__link">{{ item.title | escape }}</a>
70
+ {%- endfor -%}
71
+ </nav>
72
+ {%- endif -%}
68
73
  ```
69
74
 
70
- The company now picks an existing menu in admin (the editor surfaces all menus the store has configured), and adds/reorders/renames items without ever touching code. Multi-level dropdowns work via `link.links` (children).
75
+ The company now picks an existing menu in admin (the editor surfaces all menus the store has configured), and adds/reorders/renames items without ever touching code. Multi-level dropdowns work via `item.sub_menu_items`. There is no `active` flag on menu items. An unset `link_list` is an empty object, so guard with `!= blank`.
71
76
 
72
77
  ### Findings to surface
73
78
 
74
79
  | You see | Severity | Fix |
75
80
  | ----------------------------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
76
- | 2+ hardcoded `<a href="/...">` tags that visually form a navigation (header, footer columns, mobile drawer, social row) | `should` | Replace with a `link_list` setting and a `{% for link in menu.links %}` loop. |
81
+ | 2+ hardcoded `<a href="/...">` tags that visually form a navigation (header, footer columns, mobile drawer, social row) | `should` | Replace with a `link_list` setting and a `{% for item in menu.menu_items %}` loop. |
82
+ | `menu.links`, `link.links`, `link.active`, or `linklists` (Shopify names) | `blocker` | Renders nothing in Fluid. Use `menu.menu_items` and `item.sub_menu_items`; menu items have no `active` flag. |
77
83
  | Multiple parallel `text` + `url` settings used to fake a menu (`nav_label_1` / `nav_url_1`, `nav_label_2` / `nav_url_2`, ...) | `should` | Collapse to one `link_list`. Each menu item gains drag-reorder, depth, and active-state tracking. |
78
84
  | Section that lists footer columns by hardcoding the same column twice | `should` | One block per column with a `link_list` inside the block. Users add/remove/rename columns. |
79
- | Iterating `linklists` (the global) without exposing a `link_list` picker | `nit` | Acceptable for "site-wide menu" cases, but giving the user a `link_list` setting is more flexible — the section can be reused for different menus. |
80
85
  | Breadcrumbs, social-link rows, related-links rails — anything that's a list of `{ label, url }` pairs | `should` | All belong in a `link_list`. Don't reinvent. |
81
86
 
82
87
  ### When to keep nav items singular
83
88
 
84
89
  The `link_list` heuristic does not apply to:
85
90
 
86
- - **Single CTAs** in marketing sections — these are real "hero button" / "secondary button" roles. A standalone `text` + `url` (or one `link_list` with `limit: 1` if you want consistency) is fine.
91
+ - **Single CTAs** in marketing sections — these are real "hero button" / "secondary button" roles. A standalone `plaintext` + `url` pair is fine.
87
92
  - **Brand logo links** — the logo's `href` is almost always "go home"; a fixed `/` is acceptable, or expose a single `url` setting.
88
93
  - **Legal/footer links that are _required by policy_** (Terms, Privacy) and not the company's call to omit — but even these are usually better as a `link_list` so they can be reordered or extended.
89
94
 
@@ -15,12 +15,17 @@
15
15
 
16
16
  ### 1. Unescaped user content
17
17
 
18
- **`blocker`.** Any string that originated from a user (product title, comment, custom heading) outside of a `richtext`/`html`/`html_textarea` field must be escaped. Default to `| escape` for any string field. The only exception is the HTML-shaped types — those are HTML by design.
18
+ **`blocker`.** Escape plain strings: resource fields such as `product.title`, comments, `plaintext` settings, and global `settings.<id>` values of type `text` (the Theme panel renders `text` as a plain input).
19
19
 
20
20
  ```liquid
21
21
  <h2>{{ product.title | escape }}</h2>
22
+ <input placeholder="{{ section.settings.email_placeholder | escape }}"> {%- comment -%} plaintext setting {%- endcomment -%}
22
23
  ```
23
24
 
25
+ Do **not** escape the HTML-shaped **section and block** setting types: `text`, `textarea`, `richtext` / `rich_text`, and `html` / `html_textarea`. In section and block schemas, `text` and `textarea` are rich-text editors — they save HTML such as `Free delivery on <strong>every</strong> order` — so `| escape` shows merchants `&lt;strong&gt;`. Render them raw, in element content only. A section or block value that must go into an attribute, a CSS value, a URL, or a script needs a `plaintext` setting instead; flag `text`/`textarea` in those positions as a `blocker`.
26
+
27
+ Global settings from `config/settings_schema.json` are different: the Theme panel renders `text` as a plain input and `textarea` as rich text, and offers no `plaintext`. Escape a global `text` value (`{{ settings.announcement | escape }}`); render a global `textarea` raw.
28
+
24
29
  ### 2. Raw output of URLs in `href` / `src`
25
30
 
26
31
  **`should`.** Escape:
@@ -44,14 +49,14 @@
44
49
  Render the image with `| media_tag` (see [Media rendering](media-tag.md)) and pass `alt`:
45
50
 
46
51
  ```liquid
47
- {%- comment -%} Bad: image_picker carries no stored alt, so this ships alt-less {%- endcomment -%}
52
+ {%- comment -%} Bad: the picker value is a bare URL, so this ships alt-less {%- endcomment -%}
48
53
  {{ section.settings.image | media_tag }}
49
54
 
50
- {%- comment -%} Good {%- endcomment -%}
51
- {{ section.settings.image | media_tag: alt: section.settings.heading }}
55
+ {%- comment -%} Good: the editor stores alt text in the `<id>_alt` companion setting {%- endcomment -%}
56
+ {{ section.settings.image | media_tag: alt: section.settings.image_alt }}
52
57
  ```
53
58
 
54
- `media_tag` defaults `alt` from a resource image (`product.featured_image`), but `*_picker` settings carry none — pass one explicitly. Decorative images: `alt: ''`.
59
+ Every `image_picker`, `video_picker`, and `media_picker` gets an editor-managed `<id>_alt` setting that merchants fill in next to the picker; don't declare it, read it. Don't reuse a heading as alt text — a `text` heading is HTML. `media_tag` takes `alt` from an image value that carries one (an entry in `product.images`, for example); picker values carry none. Decorative images: `alt: ''`.
55
60
 
56
61
  ### 2. Heading hierarchy
57
62
 
@@ -66,7 +71,7 @@ A clickable `<div>` is `blocker`. Use `<button>` for actions, `<a href>` for nav
66
71
  `| media_tag` applies `decoding="async"` and `loading="lazy"` for you — don't hand-roll them. For hero/above-the-fold images pass `loading: 'eager'`, since lazy loading there delays LCP:
67
72
 
68
73
  ```liquid
69
- {{ section.settings.hero | media_tag: loading: 'eager', alt: section.settings.heading }}
74
+ {{ section.settings.hero | media_tag: loading: 'eager', alt: section.settings.hero_alt }}
70
75
  ```
71
76
 
72
77
  ---