similarbuild 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +110 -0
- package/LICENSE +21 -0
- package/README.md +301 -0
- package/bin/install.js +256 -0
- package/lib/copy-templates.mjs +52 -0
- package/lib/install-deps.mjs +62 -0
- package/lib/prompt-config.mjs +83 -0
- package/lib/verify-env.mjs +19 -0
- package/package.json +63 -0
- package/scripts/sync-templates.mjs +71 -0
- package/templates/commands/build-page.md +490 -0
- package/templates/commands/build-site.md +548 -0
- package/templates/commands/clip-section.md +519 -0
- package/templates/memory/anti-patterns.md +212 -0
- package/templates/memory/design-knowledge.md +225 -0
- package/templates/memory/fixes.md +163 -0
- package/templates/memory/patterns.md +681 -0
- package/templates/presets/shopify-section.yaml +51 -0
- package/templates/presets/wp-elementor.yaml +49 -0
- package/templates/reports/fixtures/mock-run-1.json +115 -0
- package/templates/reports/fixtures/mock-run-2.json +72 -0
- package/templates/reports/report-renderer.mjs +218 -0
- package/templates/reports/report-template.html +571 -0
- package/templates/skills/sb-build-shopify/SKILL.md +104 -0
- package/templates/skills/sb-build-shopify/references/shopify-build-rules.md +563 -0
- package/templates/skills/sb-build-shopify/scripts/build-shopify.mjs +637 -0
- package/templates/skills/sb-build-shopify/scripts/tests/test-build-shopify.mjs +424 -0
- package/templates/skills/sb-build-wp/SKILL.md +83 -0
- package/templates/skills/sb-build-wp/references/wp-build-rules.md +376 -0
- package/templates/skills/sb-build-wp/scripts/build-wp.mjs +327 -0
- package/templates/skills/sb-build-wp/scripts/tests/test-build-wp.mjs +224 -0
- package/templates/skills/sb-compare-visual/SKILL.md +121 -0
- package/templates/skills/sb-compare-visual/scripts/compare-visual.mjs +387 -0
- package/templates/skills/sb-compare-visual/scripts/lib/compare-tokens.mjs +273 -0
- package/templates/skills/sb-compare-visual/scripts/tests/test-compare-tokens.mjs +350 -0
- package/templates/skills/sb-compare-visual/scripts/tests/test-compare-visual.mjs +626 -0
- package/templates/skills/sb-crawl-and-list/SKILL.md +99 -0
- package/templates/skills/sb-crawl-and-list/scripts/crawl-and-list.mjs +437 -0
- package/templates/skills/sb-crawl-and-list/scripts/lib/blocklist-filter.mjs +176 -0
- package/templates/skills/sb-crawl-and-list/scripts/lib/fallback-crawler.mjs +107 -0
- package/templates/skills/sb-crawl-and-list/scripts/lib/page-classifier.mjs +89 -0
- package/templates/skills/sb-crawl-and-list/scripts/lib/sitemap-parser.mjs +118 -0
- package/templates/skills/sb-crawl-and-list/scripts/tests/test-blocklist-filter.mjs +204 -0
- package/templates/skills/sb-crawl-and-list/scripts/tests/test-crawl-and-list.mjs +276 -0
- package/templates/skills/sb-crawl-and-list/scripts/tests/test-fallback-crawler.mjs +243 -0
- package/templates/skills/sb-crawl-and-list/scripts/tests/test-page-classifier.mjs +120 -0
- package/templates/skills/sb-crawl-and-list/scripts/tests/test-sitemap-parser.mjs +157 -0
- package/templates/skills/sb-extract-assets/SKILL.md +112 -0
- package/templates/skills/sb-extract-assets/scripts/extract-assets.mjs +484 -0
- package/templates/skills/sb-extract-assets/scripts/tests/test-extract-assets.mjs +112 -0
- package/templates/skills/sb-inspect-live/SKILL.md +105 -0
- package/templates/skills/sb-inspect-live/scripts/inspect-live.mjs +693 -0
- package/templates/skills/sb-inspect-live/scripts/tests/test-inspect-live.mjs +181 -0
- package/templates/skills/sb-review-checks/SKILL.md +113 -0
- package/templates/skills/sb-review-checks/references/review-rules.md +195 -0
- package/templates/skills/sb-review-checks/scripts/lib/anti-patterns.mjs +379 -0
- package/templates/skills/sb-review-checks/scripts/lib/cross-reference.mjs +115 -0
- package/templates/skills/sb-review-checks/scripts/lib/design-quality.mjs +541 -0
- package/templates/skills/sb-review-checks/scripts/review-checks.mjs +250 -0
- package/templates/skills/sb-review-checks/scripts/tests/test-anti-patterns.mjs +343 -0
- package/templates/skills/sb-review-checks/scripts/tests/test-cross-reference.mjs +170 -0
- package/templates/skills/sb-review-checks/scripts/tests/test-design-quality.mjs +493 -0
- package/templates/skills/sb-review-checks/scripts/tests/test-review-checks.mjs +267 -0
- package/templates/skills/sb-tweak/SKILL.md +130 -0
- package/templates/skills/sb-tweak/references/tweak-patterns.md +157 -0
- package/templates/skills/sb-tweak/scripts/lib/diff-summarizer.mjs +140 -0
- package/templates/skills/sb-tweak/scripts/lib/element-locator.mjs +507 -0
- package/templates/skills/sb-tweak/scripts/lib/intent-parser.mjs +324 -0
- package/templates/skills/sb-tweak/scripts/tests/test-diff-summarizer.mjs +248 -0
- package/templates/skills/sb-tweak/scripts/tests/test-element-locator.mjs +418 -0
- package/templates/skills/sb-tweak/scripts/tests/test-intent-parser.mjs +496 -0
- package/templates/skills/sb-tweak/scripts/tests/test-tweak.mjs +407 -0
- package/templates/skills/sb-tweak/scripts/tweak.mjs +656 -0
- package/templates/skills/sb-validate-render/SKILL.md +120 -0
- package/templates/skills/sb-validate-render/scripts/tests/test-validate-render.mjs +304 -0
- package/templates/skills/sb-validate-render/scripts/validate-render.mjs +645 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
# Shopify Section Build Rules
|
|
2
|
+
|
|
3
|
+
Everything `sb-build-shopify` needs to compose a production-grade `.liquid` file ready to drop into a Shopify theme's `sections/` folder. The output must (a) render correctly inside Dawn / Online Store 2.0 themes without theme-CSS bleed clobbering it, and (b) expose the merchant-facing values (text, image, color, link, toggle) as `{% schema %}` settings so the merchant can edit them from the theme editor without touching code.
|
|
4
|
+
|
|
5
|
+
These rules are non-negotiable — they encode the Shopify theme stack's particular failure modes and the editor-customization promise that makes Shopify sections worth shipping in the first place.
|
|
6
|
+
|
|
7
|
+
If the host project provides these files, prefer them — they reflect the user's most recent curated knowledge:
|
|
8
|
+
|
|
9
|
+
- `<plugin>/memory/patterns.md` (overrides patterns A-H below — adapt to Liquid)
|
|
10
|
+
- `<plugin>/memory/anti-patterns.md` (overrides anti-patterns #1-9 below)
|
|
11
|
+
- `<plugin>/memory/design-knowledge.md` (subset is usually passed by the orchestrator instead)
|
|
12
|
+
- `<plugin>/presets/shopify-section.yaml` (overrides the Dawn theme reset and known overrides below)
|
|
13
|
+
|
|
14
|
+
When they're absent (early in the framework's life), use everything below as the source of truth.
|
|
15
|
+
|
|
16
|
+
## Composition contract
|
|
17
|
+
|
|
18
|
+
The output is a **single `.liquid` file** containing exactly:
|
|
19
|
+
|
|
20
|
+
1. Optional `{% style %}{% endstyle %}` block (Shopify-native CSS injection — preferred over `<style>` because it allows Liquid expressions inside CSS).
|
|
21
|
+
2. The scoped markup, with `{{ section.settings.* }}` Liquid placeholders for every editable value.
|
|
22
|
+
3. Optional `<script>` block for vanilla-JS interactivity (no frameworks).
|
|
23
|
+
4. A `{% schema %}...{% endschema %}` JSON block at the end. **This is mandatory** — without it Shopify will refuse to register the section.
|
|
24
|
+
|
|
25
|
+
No `<html>`, no `<head>`, no `<body>` wrappers. No `<link rel="preconnect">` or `<link rel="stylesheet">` for Google Fonts — Shopify owns `<head>` and the theme already loads fonts via `font_face` filter or the theme's font settings. If a custom font is genuinely needed, expose it as a `font_picker` setting and consume it via `{{ section.settings.font | font_face }}` inside `{% style %}`.
|
|
26
|
+
|
|
27
|
+
The scope class IS the boundary. Shopify will additionally wrap the rendered section in `<div id="shopify-section-{{ section.id }}" class="shopify-section">`, which gives a second layer of natural scoping.
|
|
28
|
+
|
|
29
|
+
## Scope class naming
|
|
30
|
+
|
|
31
|
+
- Pick a kebab-case name from the section type detected in `inspection.sectionType` and a short content hint (e.g. `sb-hero-mobile`, `sb-pdp-variant`, `sb-faq`). Prefix with `sb-` so the class is recognizably ours and unlikely to collide with the theme's own classes.
|
|
32
|
+
- The scope class appears on the outermost element and prefixes EVERY selector inside `{% style %}`.
|
|
33
|
+
- BEM-ish modifiers: `.{scope}__title`, `.{scope}__cta`, `.{scope}--variant-active`. Pick what reads cleanly.
|
|
34
|
+
|
|
35
|
+
## Defensive specificity (anti-pattern #5b — non-negotiable)
|
|
36
|
+
|
|
37
|
+
EVERY selector that styles user-visible content (typography, color, layout, spacing, borders) MUST chain the scope class as ancestor — not just as the element's own class.
|
|
38
|
+
|
|
39
|
+
```css
|
|
40
|
+
/* WRONG — single class, theme overrides will win */
|
|
41
|
+
.sb-hero__title { font-size: 32px; }
|
|
42
|
+
|
|
43
|
+
/* RIGHT — chained scope, beats theme specificity */
|
|
44
|
+
.sb-hero .sb-hero__title { font-size: 32px; }
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Optionally chain Shopify's own wrapper for an extra specificity boost when the theme is especially aggressive:
|
|
48
|
+
|
|
49
|
+
```css
|
|
50
|
+
.shopify-section .sb-hero .sb-hero__title { font-size: 32px; }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This costs nothing and wins against Dawn's and most third-party themes' typography overrides.
|
|
54
|
+
|
|
55
|
+
## `!important` policy
|
|
56
|
+
|
|
57
|
+
Dawn (and most OS 2.0 themes) injects `!important` on these properties via the theme's typography and section padding rules. Match them only on the four font properties when applied to critical text:
|
|
58
|
+
|
|
59
|
+
- `font-family` — overridden by `--font-heading-family` / `--font-body-family` rules
|
|
60
|
+
- `font-size` — set by typography scale CSS variables
|
|
61
|
+
- `font-weight` — set by `--font-heading-weight` / `--font-body-weight`
|
|
62
|
+
- `color` — set on links and headings via theme color schemes
|
|
63
|
+
|
|
64
|
+
Do NOT spray `!important` everywhere. Specifically: do NOT `!important` margins, paddings, widths, heights, backgrounds, transforms, transitions. Theme `section-spacing` rules use CSS variables, not `!important`, so chained-scope specificity alone wins.
|
|
65
|
+
|
|
66
|
+
## Reset universal
|
|
67
|
+
|
|
68
|
+
First rule inside `{% style %}`, always:
|
|
69
|
+
|
|
70
|
+
```liquid
|
|
71
|
+
{% style %}
|
|
72
|
+
.{scope}, .{scope} * {
|
|
73
|
+
box-sizing: border-box;
|
|
74
|
+
margin: 0;
|
|
75
|
+
padding: 0;
|
|
76
|
+
}
|
|
77
|
+
{% endstyle %}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
This neutralizes the most common theme bleed without touching properties Dawn needs.
|
|
81
|
+
|
|
82
|
+
## Full-bleed container (escape Dawn's `page-width` cap)
|
|
83
|
+
|
|
84
|
+
When the section spans viewport edge-to-edge, Dawn's `.page-width` wrapper caps it at the theme's `--page-width` (default 1200px). Escape with:
|
|
85
|
+
|
|
86
|
+
```css
|
|
87
|
+
.{scope} {
|
|
88
|
+
width: 100vw;
|
|
89
|
+
margin-left: calc(50% - 50vw);
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
This works regardless of how the parent template wrapped the section. Do NOT use negative margins of fixed pixel values — they break on mobile breakpoints.
|
|
94
|
+
|
|
95
|
+
## Mobile-first + desktop breakpoint
|
|
96
|
+
|
|
97
|
+
Base styles target mobile (390px viewport). Desktop overrides go in:
|
|
98
|
+
|
|
99
|
+
```css
|
|
100
|
+
@media (min-width: 1000px) {
|
|
101
|
+
/* desktop-only overrides */
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
1000px is the threshold the framework standardizes on (sibling skill `sb-build-wp` uses the same). Tablets in portrait get the mobile layout (safe), landscape tablets and small laptops get desktop.
|
|
106
|
+
|
|
107
|
+
## Schema settings — the editable promise
|
|
108
|
+
|
|
109
|
+
This is the central differentiator from `sb-build-wp`. Identify every value in the inspection that a merchant might reasonably want to edit, and lift it into `{% schema %}` as a setting. The Liquid template references the setting; the JSON schema declares its type, label, and default.
|
|
110
|
+
|
|
111
|
+
### What to lift into settings (and the right type)
|
|
112
|
+
|
|
113
|
+
| Source in inspection | Liquid setting type | Default |
|
|
114
|
+
| ----------------------------------------------------------------- | ------------------- | -------------------------------------------------------- |
|
|
115
|
+
| Headings, sub-headings, single-line copy | `text` | the literal string from `inspection.dom` |
|
|
116
|
+
| Multi-paragraph body copy or copy with inline links | `richtext` | wrapped in `<p>...</p>` |
|
|
117
|
+
| CTA labels | `text` | the literal label |
|
|
118
|
+
| CTA / link destinations | `url` | the original href if same-origin, else `/` |
|
|
119
|
+
| Hero / content images | `image_picker` | no default (the merchant uploads — use a base64 SVG placeholder in the Liquid `{% else %}` branch, see below) |
|
|
120
|
+
| Visible color values that match an inspection token | `color` | the literal hex |
|
|
121
|
+
| Visual on/off toggles (show/hide a sub-element) | `checkbox` | `true` (matches the inspected state) |
|
|
122
|
+
| Numeric values worth tuning (padding scale, items shown) | `range` | inspected value, with sensible `min`/`max`/`step` |
|
|
123
|
+
| Choice between a small finite set (left / center / right) | `select` | inspected value as `default` |
|
|
124
|
+
|
|
125
|
+
Pick `richtext` over `text` whenever the source contains a link, bold, or italic — `richtext` lets the merchant preserve those without HTML knowledge. Pick `text` for short single-line strings.
|
|
126
|
+
|
|
127
|
+
`image_picker` settings have **no `default`**. The fallback for the `{% else %}` branch is a **base64-inlined SVG placeholder** (a small neutral-grey rectangle). Do NOT emit the dev's local `assetsMap.assets[url].localPath` here — that path:
|
|
128
|
+
|
|
129
|
+
(a) does not resolve in `sb-validate-render` (no document base for `file://`), so the hero renders as a black rectangle and inflates `compare-visual` diff%;
|
|
130
|
+
(b) is the developer's filesystem path, completely broken in a real merchant install.
|
|
131
|
+
|
|
132
|
+
Use this exact data URI (208 bytes — generic 1170×900 grey, neutral fill `#d4d4d4`, mid-slice preserve-aspect):
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMTcwIDkwMCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQgc2xpY2UiPjxyZWN0IHdpZHRoPSIxMTcwIiBoZWlnaHQ9IjkwMCIgZmlsbD0iI2Q0ZDRkNCIvPjwvc3ZnPg==
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```liquid
|
|
139
|
+
{% if section.settings.hero_image %}
|
|
140
|
+
<img src="{{ section.settings.hero_image | image_url: width: 1200 }}" alt="{{ section.settings.hero_image.alt | default: section.settings.heading | escape }}" width="{{ section.settings.hero_image.width }}" height="{{ section.settings.hero_image.height }}" loading="eager" fetchpriority="high">
|
|
141
|
+
{% else %}
|
|
142
|
+
<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMTcwIDkwMCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQgc2xpY2UiPjxyZWN0IHdpZHRoPSIxMTcwIiBoZWlnaHQ9IjkwMCIgZmlsbD0iI2Q0ZDRkNCIvPjwvc3ZnPg==" alt="" width="1170" height="900" loading="eager">
|
|
143
|
+
{% endif %}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The `{% else %}` `<img>` uses `alt=""` (decorative — it's a placeholder, not real content) and matches the wrapper's `aspect-ratio` so the layout shifts gracefully when the merchant uploads the real image. Same data URI applies whether the branch renders an `<img>` or sets `background-image: url(...)` in `{% style %}` (see Hero rules).
|
|
147
|
+
|
|
148
|
+
### Naming conventions for setting `id`
|
|
149
|
+
|
|
150
|
+
- snake_case, semantic, never UI-driven (`heading` not `top_text`, `cta_label` not `button_1_text`).
|
|
151
|
+
- Stable across schema revisions — once shipped, renaming an `id` orphans the merchant's edits.
|
|
152
|
+
|
|
153
|
+
### Ordering inside `settings`
|
|
154
|
+
|
|
155
|
+
Group related settings with `header` separators so the editor sidebar reads top-down:
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"type": "header",
|
|
160
|
+
"content": "Heading"
|
|
161
|
+
},
|
|
162
|
+
{ "type": "text", "id": "heading", "label": "Heading", "default": "Welcome" },
|
|
163
|
+
{ "type": "richtext", "id": "subheading", "label": "Sub-heading", "default": "<p>...</p>" }
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Presets — non-negotiable
|
|
167
|
+
|
|
168
|
+
Every section ships with at least one preset. Without a preset, merchants cannot insert the section from "Add section" in the theme editor — it stays invisible. Use the inspected values as the preset's defaults.
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
"presets": [
|
|
172
|
+
{ "name": "Hero — clone of source", "category": "Image" }
|
|
173
|
+
]
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`category` is purely organizational in the editor sidebar. Reasonable values: `Image`, `Text`, `Banners`, `Promotional`.
|
|
177
|
+
|
|
178
|
+
## Defensive specificity against Dawn / OS 2.0
|
|
179
|
+
|
|
180
|
+
The Shopify wrapper `<div id="shopify-section-{{ section.id }}" class="shopify-section">` gives you a free outer scope. Two acceptable forms:
|
|
181
|
+
|
|
182
|
+
```css
|
|
183
|
+
/* Form 1 — chained custom scope (recommended, theme-agnostic) */
|
|
184
|
+
.{scope} .{scope}__title { ... }
|
|
185
|
+
|
|
186
|
+
/* Form 2 — chained custom scope inside Shopify's wrapper (extra defense) */
|
|
187
|
+
.shopify-section .{scope} .{scope}__title { ... }
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Use Form 1 by default. Use Form 2 only when you explicitly need to override a Dawn rule that wins via `.shopify-section` ancestor.
|
|
191
|
+
|
|
192
|
+
Do NOT use the dynamic id selector (`.shopify-section-{{ section.id }}`) for scoping — the id is per-render, and the same section markup may be reused across pages.
|
|
193
|
+
|
|
194
|
+
## Asset substitution
|
|
195
|
+
|
|
196
|
+
For every `<img>` or `<source>`, the URL must come from `assetsMap.assets[originalUrl].localPath` — NEVER the original CDN URL, never an inline `data:` URI for raster, never a hand-drawn SVG approximation (anti-pattern #8).
|
|
197
|
+
|
|
198
|
+
For SVGs: Shopify accepts SVG uploads as theme assets, BUT until the merchant uploads, the inline SVG markup from `assetsMap.assets[url].inline` is the fastest path to a working default. Inline it directly for icons; for hero or content images, prefer the file-based path.
|
|
199
|
+
|
|
200
|
+
If a URL appears in `assetsMap.failed[]`, you have two choices:
|
|
201
|
+
|
|
202
|
+
1. Drop the image entirely (preferred for decorative).
|
|
203
|
+
2. Use a `<div>` placeholder with a literal background-color from inspection tokens.
|
|
204
|
+
|
|
205
|
+
Never fabricate an asset. If a hero image failed, surface that to the orchestrator via a comment in the output: `<!-- sb-build-shopify: hero asset failed download -->`.
|
|
206
|
+
|
|
207
|
+
### Responsive images via Shopify's `image_url` filter
|
|
208
|
+
|
|
209
|
+
Shopify generates and serves variants automatically through the `image_url` filter — never hardcode a `srcset` for merchant-uploaded images. The pattern:
|
|
210
|
+
|
|
211
|
+
```liquid
|
|
212
|
+
<img
|
|
213
|
+
src="{{ section.settings.hero_image | image_url: width: 1200 }}"
|
|
214
|
+
srcset="
|
|
215
|
+
{{ section.settings.hero_image | image_url: width: 400 }} 400w,
|
|
216
|
+
{{ section.settings.hero_image | image_url: width: 800 }} 800w,
|
|
217
|
+
{{ section.settings.hero_image | image_url: width: 1200 }} 1200w,
|
|
218
|
+
{{ section.settings.hero_image | image_url: width: 1600 }} 1600w
|
|
219
|
+
"
|
|
220
|
+
sizes="(min-width: 1000px) 50vw, 100vw"
|
|
221
|
+
alt="{{ section.settings.hero_image.alt | default: section.settings.heading | escape }}"
|
|
222
|
+
width="{{ section.settings.hero_image.width }}"
|
|
223
|
+
height="{{ section.settings.hero_image.height }}"
|
|
224
|
+
loading="lazy">
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
For the LCP / hero image, swap `loading="lazy"` for `loading="eager" fetchpriority="high"`.
|
|
228
|
+
|
|
229
|
+
For the `{% else %}` fallback branch, use the **base64 SVG placeholder** documented in "Schema settings — `image_picker`". No `srcset` — the placeholder is a single asset, and `width` / `height` should match the placeholder's intrinsic dimensions (1170×900) so the layout doesn't shift when the merchant uploads. Do NOT emit `assetsMap.assets[url].localPath` here — see anti-pattern #16.
|
|
230
|
+
|
|
231
|
+
## Hero rules (anti-pattern #1)
|
|
232
|
+
|
|
233
|
+
NEVER use `100vh` for hero height. Mobile browser chrome makes `100vh` taller than the visible viewport, causing the hero to clip below the fold. Use:
|
|
234
|
+
|
|
235
|
+
```css
|
|
236
|
+
.{scope} .{scope}__hero {
|
|
237
|
+
aspect-ratio: 1 / 1.3;
|
|
238
|
+
max-height: 700px;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
@media (min-width: 1000px) {
|
|
242
|
+
.{scope} .{scope}__hero {
|
|
243
|
+
aspect-ratio: 16 / 9;
|
|
244
|
+
max-height: 720px;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Prefer `background-image` on the container over an `<img>` with `height: 100%` (anti-pattern #2). Dawn injects `img { max-width: 100%; height: auto; }` which clobbers the `<img>` approach.
|
|
250
|
+
|
|
251
|
+
For background images set via Liquid setting, use the same `{% if %}{% else %}` fallback as the `<img>` form — the SVG data URI is the placeholder when the merchant hasn't uploaded yet:
|
|
252
|
+
|
|
253
|
+
```liquid
|
|
254
|
+
{% style %}
|
|
255
|
+
.{scope} .{scope}__hero {
|
|
256
|
+
{% if section.settings.hero_image %}
|
|
257
|
+
background-image: url({{ section.settings.hero_image | image_url: width: 1600 }});
|
|
258
|
+
{% else %}
|
|
259
|
+
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMTcwIDkwMCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQgc2xpY2UiPjxyZWN0IHdpZHRoPSIxMTcwIiBoZWlnaHQ9IjkwMCIgZmlsbD0iI2Q0ZDRkNCIvPjwvc3ZnPg==');
|
|
260
|
+
{% endif %}
|
|
261
|
+
background-size: cover;
|
|
262
|
+
background-position: center;
|
|
263
|
+
}
|
|
264
|
+
{% endstyle %}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
The fact that `{% style %}` allows Liquid expressions inside CSS is exactly why we use it instead of `<style>`. Never emit a developer filesystem path (`/Users/...`, `C:\Users\...`) into `url(...)` — see anti-pattern #16.
|
|
268
|
+
|
|
269
|
+
## Fonts
|
|
270
|
+
|
|
271
|
+
Do NOT emit `<link rel="preconnect">` or `<link rel="stylesheet">` for Google Fonts. Shopify owns `<head>` and the theme already loads fonts. Two acceptable patterns:
|
|
272
|
+
|
|
273
|
+
1. **Use the theme's font** (default — match Dawn's `--font-heading-family` / `--font-body-family` via `font-family: inherit` on headings, then chain-scope-override the weight/size/family only on text we control). Cleanest, least chance of breaking the merchant's brand.
|
|
274
|
+
|
|
275
|
+
2. **Expose a `font_picker` setting** when the inspection's font is genuinely critical and unlikely to be in the theme:
|
|
276
|
+
|
|
277
|
+
```json
|
|
278
|
+
{ "type": "font_picker", "id": "heading_font", "label": "Heading font", "default": "assistant_n4" }
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
```liquid
|
|
282
|
+
{% style %}
|
|
283
|
+
{{ section.settings.heading_font | font_face: font_display: 'swap' }}
|
|
284
|
+
.{scope} .{scope}__title {
|
|
285
|
+
font-family: {{ section.settings.heading_font.family }}, {{ section.settings.heading_font.fallback_families }};
|
|
286
|
+
}
|
|
287
|
+
{% endstyle %}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
`font_face` filter automatically emits `font-display: swap`. Never use Google Fonts links inside a Shopify section.
|
|
291
|
+
|
|
292
|
+
## Design knowledge — applied automatically
|
|
293
|
+
|
|
294
|
+
These come from the `designKnowledge` subset the orchestrator passed in. When that subset is absent or thin, use these defaults — they are baseline web standards, not opinions.
|
|
295
|
+
|
|
296
|
+
### Accessibility
|
|
297
|
+
|
|
298
|
+
- Every `<img>` MUST have an `alt` attribute. For settings-driven images: `alt="{{ section.settings.hero_image.alt | default: section.settings.heading | escape }}"`. For decorative images: `alt=""` literal (no Liquid).
|
|
299
|
+
- Every `<button>` MUST have `type="button"` (so it doesn't submit a parent form).
|
|
300
|
+
- Buttons that are icon-only MUST have `aria-label="..."` describing the action. If the label might be reworded by the merchant, expose an `aria_label` text setting alongside.
|
|
301
|
+
- Heading hierarchy: a section that starts with H2 cannot contain an H1. Don't skip levels. The orchestrator may tell you this is a sub-section that should start at H2 — respect that.
|
|
302
|
+
- Use `<nav>` for navigation regions. Do NOT emit `<main>` from a section.
|
|
303
|
+
- Use `<dialog>` for modals — close with `dialog.close()`, open with `dialog.showModal()`. No library.
|
|
304
|
+
|
|
305
|
+
### Performance
|
|
306
|
+
|
|
307
|
+
- The hero image gets `loading="eager"` AND `fetchpriority="high"`. Every other image gets `loading="lazy"`.
|
|
308
|
+
- Use `width` and `height` attributes on every `<img>` (from `inspection.imgUrls[].naturalWidth/Height` for fallback path, or `{{ image.width }}`/`{{ image.height }}` for setting-driven). Prevents layout shift.
|
|
309
|
+
- Do NOT preload anything. Shopify's theme handles preloading at the page level.
|
|
310
|
+
|
|
311
|
+
### Web standards
|
|
312
|
+
|
|
313
|
+
- Responsive images: always via `image_url: width: N` for setting-driven, hardcoded single-size for local fallback.
|
|
314
|
+
- Use `<details>/<summary>` for FAQ collapsibles. Reset summary's marker.
|
|
315
|
+
|
|
316
|
+
## fixHints (when present)
|
|
317
|
+
|
|
318
|
+
`fixHints` arrives as a JSON array from `sb-review-checks` after a failed validation cycle. Each hint has the shape:
|
|
319
|
+
|
|
320
|
+
```json
|
|
321
|
+
{ "area": "string", "issue": "string", "fix": "string", "severity": "high|medium|low" }
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
When fixHints are present, you are NOT building from scratch — you are patching a previous build. Apply each fix surgically:
|
|
325
|
+
|
|
326
|
+
- Re-read your previous output (the orchestrator passes its path).
|
|
327
|
+
- For each fixHint, identify the exact selector, markup, or schema entry it references and apply the fix.
|
|
328
|
+
- Keep everything else identical — don't refactor, don't rename setting `id`s, don't restructure the schema. The reviewer's diff should be the only diff.
|
|
329
|
+
- High-severity fixes are mandatory. Medium are strongly recommended. Low are optional unless they conflict with high-severity ones.
|
|
330
|
+
|
|
331
|
+
## Patterns (A-H — adapted for Shopify)
|
|
332
|
+
|
|
333
|
+
These are the canonical recipes adapted to Liquid + schema. When `<plugin>/memory/patterns.md` exists, prefer it.
|
|
334
|
+
|
|
335
|
+
### A. Sticky Header with announcement bar
|
|
336
|
+
|
|
337
|
+
```liquid
|
|
338
|
+
<header class="sb-hdr">
|
|
339
|
+
{% if section.settings.show_announcement %}
|
|
340
|
+
<div class="sb-hdr__bar">{{ section.settings.announcement_text }}</div>
|
|
341
|
+
{% endif %}
|
|
342
|
+
<nav class="sb-hdr__nav" aria-label="Main">
|
|
343
|
+
<a href="{{ section.settings.logo_link | default: '/' }}" class="sb-hdr__logo">
|
|
344
|
+
{% if section.settings.logo %}
|
|
345
|
+
<img src="{{ section.settings.logo | image_url: width: 200 }}" alt="{{ shop.name | escape }}" width="100" height="40">
|
|
346
|
+
{% endif %}
|
|
347
|
+
</a>
|
|
348
|
+
<button class="sb-hdr__menu-btn" type="button" aria-label="Open menu" aria-expanded="false">☰</button>
|
|
349
|
+
</nav>
|
|
350
|
+
</header>
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Settings: `show_announcement` (checkbox), `announcement_text` (text), `logo` (image_picker), `logo_link` (url).
|
|
354
|
+
|
|
355
|
+
### B. Full-bleed Hero (aspect-ratio, background-image, settings-driven)
|
|
356
|
+
|
|
357
|
+
Use `background-image` on the container via `{% style %}` interpolation. See Hero rules above.
|
|
358
|
+
|
|
359
|
+
### C. Carousel mobile / Grid desktop (scroll-snap → grid)
|
|
360
|
+
|
|
361
|
+
Identical CSS to `sb-build-wp` Pattern C. The repeating cards become `blocks` in the schema:
|
|
362
|
+
|
|
363
|
+
```json
|
|
364
|
+
"blocks": [
|
|
365
|
+
{
|
|
366
|
+
"type": "product",
|
|
367
|
+
"name": "Product card",
|
|
368
|
+
"settings": [
|
|
369
|
+
{ "type": "image_picker", "id": "image", "label": "Image" },
|
|
370
|
+
{ "type": "text", "id": "title", "label": "Title", "default": "Product" },
|
|
371
|
+
{ "type": "text", "id": "price", "label": "Price", "default": "$24" },
|
|
372
|
+
{ "type": "url", "id": "link", "label": "Link" }
|
|
373
|
+
]
|
|
374
|
+
}
|
|
375
|
+
]
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
Iterated in Liquid:
|
|
379
|
+
|
|
380
|
+
```liquid
|
|
381
|
+
<div class="sb-products">
|
|
382
|
+
{% for block in section.blocks %}
|
|
383
|
+
<article class="sb-products__item" {{ block.shopify_attributes }}>
|
|
384
|
+
{% if block.settings.image %}
|
|
385
|
+
<img src="{{ block.settings.image | image_url: width: 600 }}" alt="{{ block.settings.title | escape }}" loading="lazy">
|
|
386
|
+
{% endif %}
|
|
387
|
+
<h3>{{ block.settings.title }}</h3>
|
|
388
|
+
<p>{{ block.settings.price }}</p>
|
|
389
|
+
</article>
|
|
390
|
+
{% endfor %}
|
|
391
|
+
</div>
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
`{{ block.shopify_attributes }}` is required — without it, the editor cannot link the block UI to the rendered element.
|
|
395
|
+
|
|
396
|
+
### D. Image-with-text overlap (`::before` band)
|
|
397
|
+
|
|
398
|
+
Same CSS as `sb-build-wp` Pattern D. Image source comes from a `image_picker` setting.
|
|
399
|
+
|
|
400
|
+
### E. Defensive button (chain class + appearance + !important on font props)
|
|
401
|
+
|
|
402
|
+
```css
|
|
403
|
+
.{scope} .{scope}__cta.{scope}__cta {
|
|
404
|
+
appearance: none;
|
|
405
|
+
background: {{ section.settings.cta_bg_color | default: '#000' }};
|
|
406
|
+
color: {{ section.settings.cta_text_color | default: '#fff' }};
|
|
407
|
+
font-family: inherit !important;
|
|
408
|
+
font-weight: 700 !important;
|
|
409
|
+
font-size: 16px !important;
|
|
410
|
+
border: 0;
|
|
411
|
+
padding: 14px 28px;
|
|
412
|
+
border-radius: 999px;
|
|
413
|
+
cursor: pointer;
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
The `{% style %}` Liquid interpolation makes button colors live-editable from the editor.
|
|
418
|
+
|
|
419
|
+
### F. FAQ collapsible (`<details>/<summary>` + blocks)
|
|
420
|
+
|
|
421
|
+
```liquid
|
|
422
|
+
<div class="sb-faq">
|
|
423
|
+
{% for block in section.blocks %}
|
|
424
|
+
<details class="sb-faq__item" {{ block.shopify_attributes }}>
|
|
425
|
+
<summary class="sb-faq__q">{{ block.settings.question }}</summary>
|
|
426
|
+
<div class="sb-faq__a">{{ block.settings.answer }}</div>
|
|
427
|
+
</details>
|
|
428
|
+
{% endfor %}
|
|
429
|
+
</div>
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
`question` is `text`, `answer` is `richtext`.
|
|
433
|
+
|
|
434
|
+
### G. Trust banner with modal
|
|
435
|
+
|
|
436
|
+
`<dialog>` and vanilla JS — same pattern as `sb-build-wp` Pattern G. The headline and body inside `<dialog>` come from settings.
|
|
437
|
+
|
|
438
|
+
### H. Variant card (radio + photo)
|
|
439
|
+
|
|
440
|
+
Real product photos from `assetsMap`, settings-driven, no SVG illustrations.
|
|
441
|
+
|
|
442
|
+
## Anti-patterns (1-9 fallback)
|
|
443
|
+
|
|
444
|
+
1. **Hero `100vh`** — clips on mobile. Use `aspect-ratio` + `max-height`.
|
|
445
|
+
2. **`<img>` with `height: 100%` for full-bleed hero** — Dawn's `img { height: auto }` clobbers it. Use `background-image` on the container.
|
|
446
|
+
3. **Guessing overlay opacity** — read the exact alpha from `inspection.tokens` or computed style.
|
|
447
|
+
4. **`.scope a { color: inherit }` clobbers button color** — buttons use `<button>`, not `<a>`. If a CTA must be an `<a>`, set its color literally.
|
|
448
|
+
5. **`<button>` decorative without defensive specificity** — chain the class, `!important` on the four font properties (Pattern E).
|
|
449
|
+
6. **`!important` alone without ancestor prefix** — useless. Theme has both. You need both too.
|
|
450
|
+
7. **Modeling overlap-image as a sibling box** — it's a `::before` on the section.
|
|
451
|
+
8. **Replicating an image as inline SVG** — download the actual asset. Inline SVG only when source already was SVG.
|
|
452
|
+
9. **Hardcoding `srcset` for setting-driven images** — use `image_url: width: N` and let Shopify generate the variants.
|
|
453
|
+
|
|
454
|
+
### Shopify-specific anti-patterns
|
|
455
|
+
|
|
456
|
+
10. **Schema setting `id` rename** — once shipped, renaming `id` orphans merchant edits. Pick stable, semantic ids upfront.
|
|
457
|
+
11. **`image_picker` with `default`** — invalid. `image_picker` does not accept `default`. Use a `{% if %}{% else %}` Liquid fallback.
|
|
458
|
+
12. **Missing `presets`** — section becomes uninstallable from the editor. Always ship at least one.
|
|
459
|
+
13. **Missing `{{ block.shopify_attributes }}` on iterated block elements** — editor cannot link block UI to rendered element. Mandatory.
|
|
460
|
+
14. **Google Fonts `<link>` inside the section** — Shopify owns `<head>`. Use `font_face` filter or theme inheritance.
|
|
461
|
+
15. **Hardcoded CDN URLs in markup** — never. For setting-driven images use the `image_url` filter; for fallbacks use the base64 SVG placeholder (anti-pattern #16).
|
|
462
|
+
16. **Developer filesystem paths in fallbacks** — never emit `/Users/...`, `C:\Users\...`, or any absolute disk path into `<img src>`, `url(...)`, or `src=` for the `{% else %}` branch of an `image_picker` setting. The path (a) doesn't resolve in `sb-validate-render` (no `file://` document base, so the build screenshot is black and inflates `compare-visual` diff%), and (b) is the dev's local filesystem, completely broken in a real merchant install. The correct fallback is the 208-byte base64 SVG placeholder defined in "Schema settings — image_picker fallback" above.
|
|
463
|
+
|
|
464
|
+
## Theme reset (preset fallback — shopify-section.yaml)
|
|
465
|
+
|
|
466
|
+
When `<plugin>/presets/shopify-section.yaml` exists, prefer its `theme_reset` block. Otherwise these are the known overrides Dawn / OS 2.0 themes inject at the section level:
|
|
467
|
+
|
|
468
|
+
```css
|
|
469
|
+
/* What Dawn / typical OS 2.0 themes inject (informational — do NOT include in your output) */
|
|
470
|
+
.shopify-section { --section-padding-top: 36px; --section-padding-bottom: 36px; }
|
|
471
|
+
.shopify-section img { max-width: 100%; height: auto; vertical-align: middle; }
|
|
472
|
+
.shopify-section h1, .shopify-section h2, .shopify-section h3 {
|
|
473
|
+
font-family: var(--font-heading-family);
|
|
474
|
+
font-weight: var(--font-heading-weight);
|
|
475
|
+
}
|
|
476
|
+
.shopify-section button { font-family: inherit; }
|
|
477
|
+
.shopify-section a { color: rgb(var(--color-link)); text-decoration: underline; }
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
This is what you are defending against. Every selector in your output that styles a property on this list MUST win on specificity (chained scope) and may need `!important` (font properties only).
|
|
481
|
+
|
|
482
|
+
## Output skeleton
|
|
483
|
+
|
|
484
|
+
```liquid
|
|
485
|
+
{% style %}
|
|
486
|
+
/* 1. Reset */
|
|
487
|
+
.{scope}, .{scope} * { box-sizing: border-box; margin: 0; padding: 0; }
|
|
488
|
+
|
|
489
|
+
/* 2. Scope container (full-bleed if applicable) */
|
|
490
|
+
.{scope} { width: 100vw; margin-left: calc(50% - 50vw); }
|
|
491
|
+
|
|
492
|
+
/* 3. Element styles, mobile-first, with defensive specificity.
|
|
493
|
+
Liquid interpolations live here for setting-driven values. */
|
|
494
|
+
.{scope} .{scope}__title { font-size: 32px !important; font-weight: 700 !important; }
|
|
495
|
+
.{scope} .{scope}__hero {
|
|
496
|
+
aspect-ratio: 1 / 1.3;
|
|
497
|
+
max-height: 700px;
|
|
498
|
+
{% if section.settings.hero_image %}
|
|
499
|
+
background-image: url({{ section.settings.hero_image | image_url: width: 1600 }});
|
|
500
|
+
{% else %}
|
|
501
|
+
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMTcwIDkwMCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQgc2xpY2UiPjxyZWN0IHdpZHRoPSIxMTcwIiBoZWlnaHQ9IjkwMCIgZmlsbD0iI2Q0ZDRkNCIvPjwvc3ZnPg==');
|
|
502
|
+
{% endif %}
|
|
503
|
+
background-size: cover;
|
|
504
|
+
background-position: center;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/* 4. Desktop overrides */
|
|
508
|
+
@media (min-width: 1000px) {
|
|
509
|
+
.{scope} .{scope}__hero { aspect-ratio: 16 / 9; max-height: 720px; }
|
|
510
|
+
}
|
|
511
|
+
{% endstyle %}
|
|
512
|
+
|
|
513
|
+
<section class="{scope}">
|
|
514
|
+
<h1 class="{scope}__title">{{ section.settings.heading | escape }}</h1>
|
|
515
|
+
{% if section.settings.cta_label != blank %}
|
|
516
|
+
<a href="{{ section.settings.cta_link | default: '/' }}" class="{scope}__cta">
|
|
517
|
+
{{ section.settings.cta_label | escape }}
|
|
518
|
+
</a>
|
|
519
|
+
{% endif %}
|
|
520
|
+
</section>
|
|
521
|
+
|
|
522
|
+
<script>
|
|
523
|
+
/* optional vanilla JS */
|
|
524
|
+
</script>
|
|
525
|
+
|
|
526
|
+
{% schema %}
|
|
527
|
+
{
|
|
528
|
+
"name": "Hero (cloned)",
|
|
529
|
+
"tag": "section",
|
|
530
|
+
"class": "{scope}-wrapper",
|
|
531
|
+
"settings": [
|
|
532
|
+
{ "type": "header", "content": "Content" },
|
|
533
|
+
{ "type": "text", "id": "heading", "label": "Heading", "default": "Welcome" },
|
|
534
|
+
{ "type": "image_picker", "id": "hero_image", "label": "Hero image" },
|
|
535
|
+
{ "type": "text", "id": "cta_label", "label": "CTA label", "default": "Shop now" },
|
|
536
|
+
{ "type": "url", "id": "cta_link", "label": "CTA link" }
|
|
537
|
+
],
|
|
538
|
+
"presets": [
|
|
539
|
+
{ "name": "Hero — clone of source", "category": "Image" }
|
|
540
|
+
]
|
|
541
|
+
}
|
|
542
|
+
{% endschema %}
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
If there's no JS, omit the `<script>` block entirely. Settings inside the schema follow the lift-into-settings table above.
|
|
546
|
+
|
|
547
|
+
## What "production-grade" means
|
|
548
|
+
|
|
549
|
+
The output must score high on Lighthouse the moment it lands — not after follow-up tweaks. Concretely:
|
|
550
|
+
|
|
551
|
+
- All images have alt + correct loading attr
|
|
552
|
+
- Hero uses `loading="eager" fetchpriority="high"`
|
|
553
|
+
- No `<link rel="preconnect">` or external font stylesheet (Shopify owns `<head>`)
|
|
554
|
+
- No layout-shift bombs (`width`/`height` on images, `aspect-ratio` on media)
|
|
555
|
+
- Buttons have type="button"
|
|
556
|
+
- No console errors from missing assets (every src is a real `localPath` or settings-driven via `image_url`)
|
|
557
|
+
- Schema validates as JSON (the validator parses it strictly)
|
|
558
|
+
- Liquid syntax is balanced (`{% if %}` matched by `{% endif %}`, etc.)
|
|
559
|
+
- Every editable value has a setting AND a Liquid placeholder referencing it
|
|
560
|
+
- At least one preset
|
|
561
|
+
- No setting `id` collisions; no `image_picker` with `default`; every block-iterated element has `{{ block.shopify_attributes }}`
|
|
562
|
+
|
|
563
|
+
The structural validator (`scripts/build-shopify.mjs validate`) checks the mechanical subset of these. The rest is judgment calls.
|