@fluid-app/fluid-cli-theme-dev 0.1.20 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,131 @@
1
+ # Blocks vs. sections
2
+
3
+ > Part of the `themes-review` skill. See [`../SKILL.md`](../SKILL.md) for the review workflow, severity ladder, and validator rules.
4
+
5
+ ## Contents
6
+
7
+ - The rule — blocks first
8
+ - Standalone blocks over inline blocks
9
+ - Threshold
10
+ - Bad — settings-panel wall of fields
11
+ - Good — block per feature
12
+ - Benefits the reviewer should cite when suggesting the fix
13
+ - When sections-with-many-fields _is_ the right answer
14
+
15
+
16
+ Fluid themes are **blocks-first**: a section is a layout shell, and its *content* lives in blocks. A section that bundles every piece of content into one giant `settings: [...]` array becomes a wall of fields the merchant can't navigate, can't reorder, and can't extend without a developer.
17
+
18
+ ### The rule — blocks first
19
+
20
+ - **Block settings** hold **content** — anything a merchant might add, remove, reorder, or repeat: headings, paragraphs, images, buttons/CTAs, cards, FAQs, features, logos. Model content as blocks **even when there is only one today.** "There's just one heading" is not a reason to make it a section setting — a merchant who later wants a second heading, wants to drop the image, or wants the CTA above the text should do it in the editor, not in a PR.
21
+ - **Section settings** hold only **true whole-section configuration**: width, background, padding, column count, vertical alignment — values that are singular for the whole section by nature and that a merchant would never duplicate or reorder.
22
+
23
+ That split is the whole point: the **section** carries only a handful of settings (its whole-section config), while every **element-specific** setting lives on the block it belongs to. The section panel stays short and scannable, and each setting sits next to the element it controls.
24
+
25
+ **Litmus test:** _"Could a merchant reasonably want two of these, none of these, or these in a different order?"_ If yes → block. If it is genuinely one-per-section plumbing → section setting.
26
+
27
+ `setting_a_1` / `setting_a_2` / `setting_a_3` (or `show_card_1` / `show_card_2`) is the obvious failure mode — but the blocks-first bar is higher: reach for blocks _before_ the duplication ever appears.
28
+
29
+ ### Standalone blocks over inline blocks
30
+
31
+ Once content is a block, prefer a **standalone block** — `blocks/{name}/index.liquid`, referenced from the section's `"blocks"` array via `@theme` or a named-block reference (`{ "type": "<name>" }`) — over an **inline block** defined inside the section's `{% schema %}`.
32
+
33
+ - **Default to standalone.** It is reusable across sections, testable on its own, keeps the section schema small, and is how Fluid themes are meant to be composed.
34
+ - **Inline blocks only when the block is very small** (a couple of settings, a few lines of markup) **and** clearly specific to one section. If there's any chance of reuse, go standalone.
35
+ - Extracting an inline block into `blocks/` is always a valid, encouraged fix; never flag the reverse.
36
+
37
+ | You see | Severity | Fix |
38
+ | ---------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------ |
39
+ | Section content (a heading / text / image / CTA a merchant would add, remove, or reorder) modeled as fixed section settings | `should` | Model it as a block. |
40
+ | A non-trivial inline block (many settings or substantial markup) defined inside a section schema | `should` | Extract to a standalone `blocks/{name}/index.liquid` and reference it. |
41
+ | The same inline block duplicated across 2+ sections | `should` | Extract to one standalone `blocks/{name}/`. |
42
+ | A tiny, genuinely one-off inline block | `nit` | Acceptable; suggest standalone only if reuse is likely. |
43
+
44
+ ### Threshold
45
+
46
+ **`should`** when:
47
+
48
+ - A section has **more than ~15 top-level settings**, _and_ any of them look like variations of the same thing
49
+ - A section has 2+ pairs of settings of the form `X_1` / `X_2`
50
+ - A section has a fixed N copies of "card", "feature", "tile", "testimonial", "FAQ", "logo" — anything that semantically wants to be a list
51
+
52
+ ### Bad — settings-panel wall of fields
53
+
54
+ ```json
55
+ {% schema %}
56
+ {
57
+ "name": "Three Features",
58
+ "settings": [
59
+ { "type": "image_picker", "id": "feat_1_icon", "label": "Feature 1 icon" },
60
+ { "type": "text", "id": "feat_1_title", "label": "Feature 1 title" },
61
+ { "type": "textarea", "id": "feat_1_body", "label": "Feature 1 body" },
62
+ { "type": "image_picker", "id": "feat_2_icon", "label": "Feature 2 icon" },
63
+ { "type": "text", "id": "feat_2_title", "label": "Feature 2 title" },
64
+ { "type": "textarea", "id": "feat_2_body", "label": "Feature 2 body" },
65
+ { "type": "image_picker", "id": "feat_3_icon", "label": "Feature 3 icon" },
66
+ { "type": "text", "id": "feat_3_title", "label": "Feature 3 title" },
67
+ { "type": "textarea", "id": "feat_3_body", "label": "Feature 3 body" }
68
+ ]
69
+ }
70
+ {% endschema %}
71
+ ```
72
+
73
+ ### Good — block per feature
74
+
75
+ ```json
76
+ {% schema %}
77
+ {
78
+ "name": "Features",
79
+ "settings": [
80
+ { "type": "text", "id": "heading", "label": "Section heading", "default": "Why us" },
81
+ { "type": "select", "id": "columns", "label": "Columns",
82
+ "options": [{ "value": "2", "label": "2" }, { "value": "3", "label": "3" }, { "value": "4", "label": "4" }],
83
+ "default": "3" }
84
+ ],
85
+ "blocks": [
86
+ {
87
+ "type": "feature",
88
+ "name": "Feature",
89
+ "settings": [
90
+ { "type": "image_picker", "id": "icon", "label": "Icon" },
91
+ { "type": "text", "id": "title", "label": "Title" },
92
+ { "type": "textarea", "id": "body", "label": "Description" }
93
+ ]
94
+ }
95
+ ]
96
+ }
97
+ {% endschema %}
98
+ ```
99
+
100
+ Rendering:
101
+
102
+ ```liquid
103
+ <div class="features features--cols-{{ section.settings.columns }}">
104
+ {%- for block in section.blocks -%}
105
+ <article class="feature" {{ block.fluid_attributes }}>
106
+ {%- if block.settings.icon != blank -%}
107
+ <img src="{{ block.settings.icon }}" alt="{{ block.settings.title | escape }}" />
108
+ {%- endif -%}
109
+ <h3>{{ block.settings.title }}</h3>
110
+ <p>{{ block.settings.body }}</p>
111
+ </article>
112
+ {%- endfor -%}
113
+ </div>
114
+ ```
115
+
116
+ **Prefer this as a standalone block.** Define `feature` at `blocks/feature/index.liquid` and reference it from the section's `"blocks"` array (`{ "type": "feature" }`) instead of inlining it — see [Standalone blocks over inline blocks](#standalone-blocks-over-inline-blocks). (A stricter blocks-first read would make `heading` a block too; it's kept as a section setting here only for brevity.)
117
+
118
+ ### Benefits the reviewer should cite when suggesting the fix
119
+
120
+ - **Editor UX:** Each block shows up as a collapsible item with its own fields — far easier to scan than a 27-field wall.
121
+ - **Drag-and-drop reorder for free.**
122
+ - **Add/remove without code changes:** users can add a 4th feature without a developer.
123
+ - **Per-block dropzone hooks:** `{{ block.fluid_attributes }}` gives the editor click-to-edit on each one.
124
+ - **`limit:` on the block** (optional) enforces a max count without baking it into Liquid.
125
+ - **Reusable across sections** when defined standalone (`blocks/{name}/index.liquid`) instead of inline.
126
+
127
+ ### When sections-with-many-fields _is_ the right answer
128
+
129
+ Some sections genuinely have many settings that _all_ apply to the whole section — e.g. a configuration-heavy hero with background, overlay, video poster, mobile alt, autoplay, mute, etc. That's fine: those are whole-section **config**, not content. The test isn't field count — it's whether each field is one-per-section plumbing (section setting) or a piece of content a merchant would add / remove / reorder (block).
130
+
131
+ ---
@@ -0,0 +1,153 @@
1
+ # CSS / JS asset hygiene
2
+
3
+ > Part of the `themes-review` skill. See [`../SKILL.md`](../SKILL.md) for the review workflow, severity ladder, and validator rules.
4
+
5
+ ## Contents
6
+
7
+ - 1. Naming and scoping — everything lives under `assets/`
8
+ - 1a. Deprecated — co-located `styles.css` / `style.css`
9
+ - 2. Tailwind class duplication
10
+ - 3. Dead code
11
+ - 4. Inline `<style>` blocks — extract to assets
12
+ - 5. Inline `<script>` blocks — extract to assets
13
+ - 6. Hardcoded URLs — use `asset_url`
14
+ - 7. Hardcoded copy in markup
15
+
16
+
17
+ ### 1. Naming and scoping — everything lives under `assets/`
18
+
19
+ **All stylesheets live under `assets/`.** Co-located `styles.css` / `style.css` files inside `sections/{name}/`, `components/{name}/`, etc. **are deprecated** and should be migrated.
20
+
21
+ - Section CSS: `assets/{section_name}.css`
22
+ - Component CSS: `assets/{component_name}.css`
23
+ - Global / theme-wide CSS: `assets/theme.css` (or named after intent — `assets/typography.css`, `assets/utilities.css`)
24
+ - Custom stylesheets: `assets/{name}.css`
25
+ - Scope class names by section: `.cart-page__line-item`, not `.line-item`
26
+
27
+ Referenced via `asset_url`:
28
+
29
+ ```liquid
30
+ {{ 'featured_products.css' | asset_url | stylesheet_tag }}
31
+ ````
32
+
33
+ ### 1a. Deprecated — co-located `styles.css` / `style.css`
34
+
35
+ The earlier theme convention placed a `styles.css` next to each template, section, or component (e.g. `sections/featured/styles.css`). **This is being phased out** in favor of `assets/`-only. The reference `base-theme` still contains `styles.css` files inside section / page-variant directories — these are being migrated.
36
+
37
+ **`should`** when a PR adds or modifies a co-located stylesheet. **`blocker`** for any new file added under this pattern (new code must follow the assets-only convention).
38
+
39
+ | You see | Severity | Fix |
40
+ | ------------------------------------------------------------------------------------------------------------------ | --------- | ----------------------------------------------------------------------------------------------------------------- |
41
+ | New file `sections/{name}/styles.css` | `blocker` | Place it under `assets/{name}.css` instead and reference via `{{ '{name}.css' \| asset_url \| stylesheet_tag }}`. |
42
+ | New file `components/{name}/style.css` | `blocker` | Same — `assets/{name}.css` and `asset_url`. |
43
+ | New file under a page-variant directory like `{type}/{variant}/styles.css` | `blocker` | Move to `assets/{name}.css`. |
44
+ | Existing co-located stylesheet edited in the PR | `should` | Suggest migrating to `assets/` as part of the same PR if the diff is small, or a follow-up if not. |
45
+ | Global / custom stylesheets anywhere outside `assets/` (other than the engine-recognized root `global_styles.css`) | `blocker` | Move to `assets/`. |
46
+ | Hardcoded path like `<link rel="stylesheet" href="/sections/{name}/styles.css">` (bypassing `asset_url`) | `blocker` | Move the file and switch to `asset_url`. |
47
+
48
+ Migration recipe (suggest in the comment when applicable):
49
+
50
+ ```bash
51
+ # 1. Move the file
52
+ git mv sections/featured/styles.css assets/featured.css
53
+
54
+ # 2. Update the reference in the section template
55
+ # Find: {{ 'sections/featured/styles.css' | asset_url | stylesheet_tag }}
56
+ # Replace:{{ 'featured.css' | asset_url | stylesheet_tag }}
57
+
58
+ # 3. Validate
59
+ fluid theme lint --json
60
+ ```
61
+
62
+ ### 2. Tailwind class duplication
63
+
64
+ Long, identical `class="…"` strings repeated across cards → extract via `{% capture %}` or a component. **`should`** when 4+ identical class strings appear.
65
+
66
+ ### 3. Dead code
67
+
68
+ See [Dead code — unused blocks, components, sections, assets](#dead-code--unused-blocks-components-sections-assets) for the unified audit. Assets are one category among four.
69
+
70
+ ### 4. Inline `<style>` blocks — extract to assets
71
+
72
+ **`should`** when any inline `<style>` block exceeds **10 lines**, or when the same inline rules repeat across multiple sections. The engine deduplicates external stylesheets across the page, so external is almost always better.
73
+
74
+ ```liquid
75
+ {%- comment -%} Bad: 40 lines of CSS shipped per section render {%- endcomment -%}
76
+ <style>
77
+ .featured-products { padding: 64px 0; }
78
+ .featured-products__heading { font-size: 48px; }
79
+ .featured-products__grid { display: grid; gap: 24px; ... }
80
+ /* ... 30 more lines ... */
81
+ </style>
82
+
83
+ {%- comment -%} Good {%- endcomment -%}
84
+ {{ 'featured_products.css' | asset_url | stylesheet_tag }}
85
+ ```
86
+
87
+ **Exception:** A small block of _dynamic_ CSS that depends on schema settings is fine inline — the dynamism is the whole point. Even then, extract the static base to a stylesheet and leave only the per-section overrides inline:
88
+
89
+ ```liquid
90
+ {{ 'featured_products.css' | asset_url | stylesheet_tag }}
91
+ <style>
92
+ .featured-products[data-section-id="{{ section.id }}"] {
93
+ --section-bg: {{ section.settings.bg_color }};
94
+ --section-pad: {{ section.settings.section_pad }};
95
+ }
96
+ </style>
97
+ ```
98
+
99
+ ### 5. Inline `<script>` blocks — extract to assets
100
+
101
+ Same rule, lower threshold. **`should`** when any inline `<script>` block exceeds **5 lines**. Inline JS bypasses caching, defeats `defer`, and runs render-blocking unless explicitly deferred via `setTimeout`.
102
+
103
+ ```liquid
104
+ {%- comment -%} Bad {%- endcomment -%}
105
+ <script>
106
+ document.addEventListener('DOMContentLoaded', function() {
107
+ const slider = document.querySelector('.featured-slider');
108
+ new Splide(slider, { type: 'loop', perPage: 3 }).mount();
109
+ // ...
110
+ });
111
+ </script>
112
+
113
+ {%- comment -%} Good {%- endcomment -%}
114
+ <script src="{{ 'featured_slider.js' | asset_url }}" defer></script>
115
+ ```
116
+
117
+ **Exception:** Tiny dynamic config bridges — passing schema settings into a global config object — are acceptable inline if under 5 lines. Keep the runtime in an asset:
118
+
119
+ ```liquid
120
+ <script>
121
+ window.fluidFeaturedConfig = {{ section.settings | json }};
122
+ </script>
123
+ <script src="{{ 'featured_slider.js' | asset_url }}" defer></script>
124
+ ```
125
+
126
+ ### 6. Hardcoded URLs — use `asset_url`
127
+
128
+ Any reference to an external CDN URL, hardcoded path, or `https://cdn.example.com/...` for assets the theme owns is a finding. The asset belongs in `assets/` and the URL should resolve through `| asset_url`.
129
+
130
+ | You see | Severity | Fix |
131
+ | -------------------------------------------------------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
132
+ | `src="https://cdn.example.com/img/banner.jpg"` (external CDN, theme-owned asset) | `blocker` | Download the file into `assets/banner.jpg` and use `{{ 'banner.jpg' \| asset_url }}`. External CDNs can disappear or change CORS. |
133
+ | `src="/assets/banner.jpg"` (hardcoded path) | `blocker` | Use `{{ 'banner.jpg' \| asset_url }}`. The engine versions/CDN-fronts the URL. |
134
+ | `href="https://fonts.googleapis.com/css?family=..."` | `should` | Either upload the font files to `assets/` and `@font-face` them via stylesheet, or use `font_picker`. |
135
+ | `<link rel="stylesheet" href="/some-static.css">` | `blocker` | `{{ 'some-static.css' \| asset_url \| stylesheet_tag }}`. |
136
+ | `<script src="https://unpkg.com/some-lib"></script>` (third-party library) | `should` | Vendor it into `assets/` and reference via `asset_url`. unpkg/jsDelivr can go down and you have no control over what loads. |
137
+ | Image URL hardcoded in CSS (`background-image: url(/assets/x.png)`) | `should` | Move the rule into Liquid where `asset_url` is available, or pre-process the CSS, or use a CSS variable injected via inline `<style>` block. |
138
+ | Hardcoded route paths (`href="/products"`, `href="/cart"`) | `nit` | Acceptable when they're well-known engine routes. Surface only if the route ever changes per company. |
139
+
140
+ **Why these matter:**
141
+
142
+ - `asset_url` returns a fingerprinted CDN URL — cache-busted on deploy, CDN-fronted globally.
143
+ - External CDNs introduce a dependency on a server you don't control.
144
+ - Hardcoded paths break when the theme is renamed or moved.
145
+
146
+ ### 7. Hardcoded copy in markup
147
+
148
+ Already covered as **`should`** under [Dynamism](#dynamism--settings-over-hardcoded-values), but it's worth restating here because reviewers often miss it during a CSS/JS pass: literal English (or any language) hardcoded in markup is the same shape of problem as a hardcoded URL — it locks the theme to one company / one locale / one campaign.
149
+
150
+ - User-visible string → `text` / `textarea` / `richtext` setting
151
+ - Repeated brand-agnostic UI string (Add to cart, Read more) → `locales/en.json` and `{{ 'add_to_cart' | t }}`
152
+
153
+ ---
@@ -0,0 +1,161 @@
1
+ # Dead code — unused blocks, components, sections, assets
2
+
3
+ > Part of the `themes-review` skill. See [`../SKILL.md`](../SKILL.md) for the review workflow, severity ladder, and validator rules.
4
+
5
+ ## Contents
6
+
7
+ - What to check
8
+ - Sections — unused
9
+ - Components — unused
10
+ - Block types — declared but never rendered
11
+ - Assets — unused
12
+ - Output format for removal recommendations
13
+ - When dead-code findings turn into fixes
14
+
15
+
16
+ A theme accumulates dead code faster than almost any other codebase: schemas evolve, blocks get renamed, components get inlined, CSS files outlive their consumers. Every dead artifact is **download weight, cognitive load, and a maintenance trap** (someone will eventually edit the dead one wondering why their changes don't show up).
17
+
18
+ The reviewer's posture: **suggest removal, default to `should`, never delete from a PR without explicit approval.** Most "unused" detections have a small false-positive rate (dynamic strings, future-use), so the recommendation is always "consider removing" with the audit command attached.
19
+
20
+ ### What to check
21
+
22
+ There are four categories. Each has the same recipe: enumerate what's defined, enumerate what's referenced, take the set difference.
23
+
24
+ | Category | Defined | Referenced via | Severity |
25
+ | --------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------- | --------------------------------- |
26
+ | **Sections** | `sections/{name}/index.liquid` | `{% section 'name' %}` in any `.liquid` | `should` |
27
+ | **Components** | `components/{name}/index.liquid` | `{% render 'name' %}` / `{% include 'name' %}` | `should` |
28
+ | **Block types** | `{% schema %}` `blocks: [{ "type": "X", ... }]` arrays | `{% case block.type %}` / `{% if block.type == 'X' %}` in the same section file | `should` |
29
+ | **Assets** | files in `assets/` (flat) | `{{ 'name' \| asset_url }}` in any `.liquid` | `nit` to flag, `should` to remove |
30
+
31
+ ### Sections — unused
32
+
33
+ A section file with no `{% section 'name' %}` consumer is dormant. Either it's been deprecated and forgotten, or it was never wired into any page template.
34
+
35
+ ```bash
36
+ # From the theme repo root
37
+ for d in sections/*/; do
38
+ name=$(basename "$d")
39
+ count=$(grep -rE "\\{%\\s*section\\s+['\"]${name}['\"]" sections/ components/ layouts/ */default/ 2>/dev/null | wc -l)
40
+ [ "$count" -eq 0 ] && echo "UNUSED $d"
41
+ done
42
+ ```
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".
45
+
46
+ ### Components — unused
47
+
48
+ A component partial that nothing renders is dead weight. Lower false-positive rate than sections (no editor-driven instantiation).
49
+
50
+ ```bash
51
+ for d in components/*/; do
52
+ name=$(basename "$d")
53
+ count=$(grep -rE "\\{%\\s*(render|include)\\s+['\"]${name}['\"]" sections/ components/ layouts/ */default/ 2>/dev/null | wc -l)
54
+ [ "$count" -eq 0 ] && echo "UNUSED $d"
55
+ done
56
+ ```
57
+
58
+ **Watch for dynamic render:** `{% render snippet_name %}` where `snippet_name` is a variable. Grep for the literal first; if zero hits, also grep for the bare name as a string anywhere:
59
+
60
+ ```bash
61
+ grep -rE "['\"]${name}['\"]" sections/ components/ layouts/ */default/ 2>/dev/null | grep -v "components/${name}/"
62
+ ```
63
+
64
+ If that's also zero, it's safe to recommend removal.
65
+
66
+ ### Block types — declared but never rendered
67
+
68
+ The most insidious form of dead code: a section's `{% schema %}` declares a block type, the editor exposes it to users, users add it, and the template renders **nothing** because the `{% case block.type %}` switch has no matching `when`.
69
+
70
+ Inside each section file:
71
+
72
+ ```liquid
73
+ {% schema %}
74
+ {
75
+ "blocks": [
76
+ { "type": "page_header", "name": "Page Header" },
77
+ { "type": "product_grid", "name": "Product Grid" },
78
+ { "type": "cta_banner", "name": "CTA Banner" } ← declared
79
+ ]
80
+ }
81
+ {% endschema %}
82
+
83
+ {% for block in section.blocks %}
84
+ {% case block.type %}
85
+ {% when 'page_header' %} ...
86
+ {% when 'product_grid' %} ...
87
+ {%- comment -%} cta_banner has no `when` branch — silently rendered as nothing {%- endcomment -%}
88
+ {% endcase %}
89
+ {% endfor %}
90
+ ```
91
+
92
+ **`should`** when the schema declares a block type that the template never handles. **`blocker`** if users have already populated this block in production (visible as orphan data in `settings_data.json` / per-section metadata) — they're staring at an empty section right now.
93
+
94
+ Quick audit per section:
95
+
96
+ ```bash
97
+ for f in sections/*/index.liquid; do
98
+ # block types declared in schema (very rough — schema is JSON inside Liquid)
99
+ declared=$(awk '/{% schema %}/,/{% endschema %}/' "$f" | grep -oE '"type":\s*"[^"]+"' | grep -v '"type": *"[a-z_]+"$' | sed 's/.*"type":\s*"\([^"]*\)".*/\1/' | sort -u)
100
+ # block types referenced in template
101
+ referenced=$(grep -oE "block\.type\s*==\s*['\"][^'\"]+['\"]|when\s+['\"][^'\"]+['\"]" "$f" | grep -oE "['\"][^'\"]+['\"]" | tr -d '"'"'" | sort -u)
102
+ for d in $declared; do
103
+ echo "$referenced" | grep -qx "$d" || echo "UNHANDLED $f block type: $d"
104
+ done
105
+ done
106
+ ```
107
+
108
+ (The above is a heuristic — verify each hit manually before recommending. Block schema lives inside JSON-in-Liquid, which is hard to parse with shell tools.)
109
+
110
+ ### Assets — unused
111
+
112
+ Files in `assets/` that no Liquid reference. Lowest severity because removal is cheapest and risk lowest.
113
+
114
+ ```bash
115
+ for f in assets/*; do
116
+ name=$(basename "$f")
117
+ count=$(grep -rE "['\"]${name}['\"]" --include='*.liquid' --include='*.css' . 2>/dev/null | wc -l)
118
+ echo "$count $name"
119
+ done | sort -n | awk '$1 == 0 { print "UNUSED " $2 }'
120
+ ```
121
+
122
+ **False positives to filter:**
123
+
124
+ - Asset referenced via concatenation: `{% assign css = section.id | append: '.css' %}` — rare, but real.
125
+ - Asset referenced from another asset: a CSS file `@import`'ing another CSS file in `assets/`. Re-grep `assets/` itself before recommending removal:
126
+ ```bash
127
+ grep -rl "$name" assets/
128
+ ```
129
+
130
+ ### Output format for removal recommendations
131
+
132
+ Always frame as "consider removing" with the audit command. Never propose deletion without leaving the human a way to verify:
133
+
134
+ ````
135
+ [should] `components/old_card/index.liquid` appears unused
136
+
137
+ Audit:
138
+
139
+ ```bash
140
+ grep -rE "\{%\s*(render|include)\s+['\"]old_card['\"]" sections/ components/ layouts/ */default/ 2>/dev/null
141
+ # 0 results
142
+ ```
143
+
144
+ If the grep stays at 0 after this PR, consider removing the file. If it's
145
+ referenced dynamically (variable name into `render`), keep it and add a
146
+ comment at the top noting that.
147
+
148
+ ````
149
+
150
+ ### When dead-code findings turn into fixes
151
+
152
+ If the user says `open the fix PR`:
153
+
154
+ 1. **Remove sections, components, and assets** in separate commits, one per artifact, so reverting any one is a one-commit revert.
155
+ 2. **For block types**, the fix is to either:
156
+ - Add the missing `when` branch in the template (preferred — the block was meant to render something), or
157
+ - Remove the declaration from the `{% schema %}` `blocks:` array (only if confirmed no users have populated it).
158
+ 3. **Run `fluid theme lint --json` after each removal** — the validator catches `{% section 'name' %}` references that became dangling.
159
+ 4. **Never bulk-delete** across categories in one commit. Each removal is its own decision.
160
+
161
+ ---
@@ -0,0 +1,88 @@
1
+ # Dynamism — settings over hardcoded values
2
+
3
+ > Part of the `themes-review` skill. See [`../SKILL.md`](../SKILL.md) for the review workflow, severity ladder, and validator rules.
4
+
5
+ ## Contents
6
+
7
+ - What "hardcoded" looks like
8
+ - What dynamic looks like
9
+ - Findings to surface
10
+ - When _not_ to make something a setting
11
+
12
+
13
+ The point of a theme is that the _company_ can edit it without touching code. Anything user-visible that's hardcoded into Liquid is a missed setting. The reviewer should be relentless here: every literal string, color, image URL, link, count, or toggle in markup is a candidate.
14
+
15
+ **The principle:** if a company employee, designer, or marketer might ever want to change it, it belongs in `{% schema %}`.
16
+
17
+ ### What "hardcoded" looks like
18
+
19
+ ```liquid
20
+ {%- comment -%} Bad: every value here is locked in code {%- endcomment -%}
21
+ <section style="background:#0a0a0a;padding:64px 0;">
22
+ <h2 style="font-size:48px;color:white;">Our Bestsellers</h2>
23
+ <p>Browse this season's most popular picks.</p>
24
+ <img src="https://cdn.example.com/static/banner.jpg" alt="Banner" />
25
+ <a href="/shop?sort=popular" class="btn">Shop now</a>
26
+ </section>
27
+ ```
28
+
29
+ Every one of those values — background, padding, heading text, body text, image, button text, button link — is a hardcoded decision the company can never override without a code change.
30
+
31
+ ### What dynamic looks like
32
+
33
+ ```json
34
+ {% schema %}
35
+ {
36
+ "name": "Bestsellers Banner",
37
+ "settings": [
38
+ { "type": "text", "id": "heading", "label": "Heading", "default": "Our Bestsellers" },
39
+ { "type": "textarea", "id": "body", "label": "Body text", "default": "Browse this season's most popular picks." },
40
+ { "type": "image_picker", "id": "banner_image", "label": "Banner image" },
41
+ { "type": "color_background", "id": "bg_color", "label": "Background", "default": "#0a0a0a" },
42
+ { "type": "color", "id": "text_color", "label": "Text color", "default": "#ffffff" },
43
+ { "type": "padding", "id": "section_pad", "label": "Section padding" },
44
+ { "type": "text", "id": "cta_label", "label": "Button label", "default": "Shop now" },
45
+ { "type": "url", "id": "cta_url", "label": "Button link", "default": "/shop?sort=popular" },
46
+ { "type": "checkbox", "id": "show_cta", "label": "Show button", "default": true }
47
+ ]
48
+ }
49
+ {% endschema %}
50
+ ```
51
+
52
+ ```liquid
53
+ {%- assign s = section.settings -%}
54
+ <section style="background:{{ s.bg_color }};padding:{{ s.section_pad }};">
55
+ <h2 style="color:{{ s.text_color }};">{{ s.heading | default: 'Our Bestsellers' }}</h2>
56
+ <p>{{ s.body }}</p>
57
+ {%- if s.banner_image != blank -%}
58
+ <img src="{{ s.banner_image }}" alt="{{ s.heading | escape }}" />
59
+ {%- endif -%}
60
+ {%- if s.show_cta -%}
61
+ <a href="{{ s.cta_url }}" class="btn">{{ s.cta_label }}</a>
62
+ {%- endif -%}
63
+ </section>
64
+ ```
65
+
66
+ ### Findings to surface
67
+
68
+ | You see | Severity | Why |
69
+ | ---------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------- |
70
+ | User-visible literal text in markup (heading, paragraph, button label) | `should` | Should be a `text` / `textarea` / `richtext` setting. |
71
+ | Hardcoded colors (`#0a0a0a`, `rgb(...)`, `red`) in style/attribute | `should` | Should be a `color` or `color_background` setting. |
72
+ | Hardcoded image URL (external CDN, hardcoded path) | `blocker` if external CDN, `should` if internal | External CDNs can break, get blocked, or change. Upload to `assets/` or expose as `image_picker`. |
73
+ | Hardcoded `href` to a fixed page or `?sort=popular`-style URL | `should` | Use a `url` setting. |
74
+ | Hardcoded counts (`limit: 6`, `limit: 12`) in `for` loops | `nit` → `should` | Expose as a `range` setting if the company might want to change density. |
75
+ | Boolean branches with no user toggle (`{% if true %}`) | `should` | Either delete or expose as a `checkbox`. |
76
+ | Tailwind class strings that bake in color (`bg-black text-white`) | `should` | Use CSS variables driven by `color` settings. |
77
+
78
+ ### When _not_ to make something a setting
79
+
80
+ Adding settings has a real cost — they clutter the editor and shift the burden to non-technical users. Skip settings for:
81
+
82
+ - Internal layout primitives (grid gaps, micro-margins) the design system already controls
83
+ - Engineering plumbing (data attributes, schema markup hooks)
84
+ - Values that _must_ stay in lockstep with code (icon class names, route slugs the theme relies on)
85
+
86
+ The test: _would I expect a non-developer to want to change this?_ No → leave it hardcoded. Yes → make it a setting.
87
+
88
+ ---