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,681 @@
|
|
|
1
|
+
# Validated patterns — bundled canon
|
|
2
|
+
|
|
3
|
+
This file is the **bundled global canon** of section composition patterns for the SimilarBuild framework. It is read by `sb-build-wp` and `sb-build-shopify` via the cascade defined in Pattern #21:
|
|
4
|
+
|
|
5
|
+
1. `<plugin>/memory/patterns.md` (this file — versioned, distributed by the installer)
|
|
6
|
+
2. `~/.claude/similarbuild-memory/patterns.md` (per-user auto-learn — overrides/adds)
|
|
7
|
+
3. `<skill>/references/*.md` (bundled fallback inside the skill itself — only when neither of the above exists)
|
|
8
|
+
|
|
9
|
+
Each entry has both a **WP/Elementor variant** and a **Shopify/Liquid variant**, side by side. Markup is intentionally minimal — adapt class names and content to the inspected section. Defensive specificity (anti-pattern #5/#5b) and asset substitution rules (anti-patterns #2, #8, #16) apply uniformly.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Entry shape
|
|
14
|
+
|
|
15
|
+
Every pattern below has these fields, in this order:
|
|
16
|
+
|
|
17
|
+
- **id** — stable identifier (`A`..`H`).
|
|
18
|
+
- **name** — short human-readable title.
|
|
19
|
+
- **applies-when** — section type or composition context where the pattern is the right fit.
|
|
20
|
+
- **markup-wp** — HTML skeleton for `sb-build-wp` (no Liquid, scope class on the outermost element).
|
|
21
|
+
- **css-wp** — scoped CSS for the WP variant (mobile-first, defensive specificity, `!important` only on the four font properties).
|
|
22
|
+
- **markup-shopify** — Liquid skeleton for `sb-build-shopify` (settings-driven values via `{{ section.settings.X }}`, blocks where applicable).
|
|
23
|
+
- **css-shopify** — scoped CSS inside `{% style %}` (Liquid interpolation allowed for setting-driven values).
|
|
24
|
+
- **schema-shopify** — `{% schema %}` settings list (and `blocks` when the pattern repeats).
|
|
25
|
+
- **notes** — failure modes to watch (cross-references to anti-patterns), behavioral details (JS, ARIA, etc).
|
|
26
|
+
|
|
27
|
+
`{scope}` is a placeholder — pick a kebab-case name from `inspection.sectionType` + a content hint (e.g. `hero-mobile-shop`, `pdp-variant-card`, `sb-faq`). The `sb-` prefix on Shopify variants makes the class unmistakably ours and avoids collision with theme classes.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Pattern A — Sticky Header (announcement bar + nav + mobile drawer)
|
|
32
|
+
|
|
33
|
+
- **id:** A
|
|
34
|
+
- **name:** Sticky Header
|
|
35
|
+
- **applies-when:** site-wide header section with announcement bar above and primary navigation below; mobile drawer toggle.
|
|
36
|
+
|
|
37
|
+
### markup-wp
|
|
38
|
+
|
|
39
|
+
```html
|
|
40
|
+
<header class="hdr">
|
|
41
|
+
<div class="hdr__bar">Free shipping over $50</div>
|
|
42
|
+
<nav class="hdr__nav" aria-label="Main">
|
|
43
|
+
<a href="/" class="hdr__logo"><img src="{logo localPath}" alt="Brand" width="120" height="32"></a>
|
|
44
|
+
<button class="hdr__menu-btn" type="button" aria-label="Open menu" aria-expanded="false">☰</button>
|
|
45
|
+
<ul class="hdr__menu" hidden>
|
|
46
|
+
<li><a href="/shop">Shop</a></li>
|
|
47
|
+
<li><a href="/about">About</a></li>
|
|
48
|
+
</ul>
|
|
49
|
+
</nav>
|
|
50
|
+
</header>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### css-wp
|
|
54
|
+
|
|
55
|
+
```css
|
|
56
|
+
.hdr { position: sticky; top: 0; z-index: 100; }
|
|
57
|
+
.hdr .hdr__bar { background: #000; color: #fff; text-align: center; padding: 8px 16px; font-size: 13px !important; }
|
|
58
|
+
.hdr .hdr__nav { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: #fff; }
|
|
59
|
+
.hdr .hdr__menu[hidden] { display: none; }
|
|
60
|
+
.hdr .hdr__menu:not([hidden]) { display: flex; flex-direction: column; gap: 12px; padding: 16px; }
|
|
61
|
+
@media (min-width: 1000px) {
|
|
62
|
+
.hdr .hdr__menu-btn { display: none; }
|
|
63
|
+
.hdr .hdr__menu { display: flex !important; flex-direction: row; gap: 24px; }
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### markup-shopify
|
|
68
|
+
|
|
69
|
+
```liquid
|
|
70
|
+
<header class="sb-hdr">
|
|
71
|
+
{% if section.settings.show_announcement %}
|
|
72
|
+
<div class="sb-hdr__bar">{{ section.settings.announcement_text }}</div>
|
|
73
|
+
{% endif %}
|
|
74
|
+
<nav class="sb-hdr__nav" aria-label="Main">
|
|
75
|
+
<a href="{{ section.settings.logo_link | default: '/' }}" class="sb-hdr__logo">
|
|
76
|
+
{% if section.settings.logo %}
|
|
77
|
+
<img src="{{ section.settings.logo | image_url: width: 200 }}" alt="{{ shop.name | escape }}" width="100" height="40">
|
|
78
|
+
{% endif %}
|
|
79
|
+
</a>
|
|
80
|
+
<button class="sb-hdr__menu-btn" type="button" aria-label="Open menu" aria-expanded="false">☰</button>
|
|
81
|
+
</nav>
|
|
82
|
+
</header>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### css-shopify
|
|
86
|
+
|
|
87
|
+
Same CSS as WP variant; replace scope class `.hdr` with `.sb-hdr` and wrap inside `{% style %}` instead of `<style>`.
|
|
88
|
+
|
|
89
|
+
### schema-shopify
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"settings": [
|
|
94
|
+
{ "type": "header", "content": "Announcement" },
|
|
95
|
+
{ "type": "checkbox", "id": "show_announcement", "label": "Show bar", "default": true },
|
|
96
|
+
{ "type": "text", "id": "announcement_text", "label": "Bar text", "default": "Free shipping over $50" },
|
|
97
|
+
{ "type": "header", "content": "Logo" },
|
|
98
|
+
{ "type": "image_picker", "id": "logo", "label": "Logo" },
|
|
99
|
+
{ "type": "url", "id": "logo_link", "label": "Logo link" }
|
|
100
|
+
],
|
|
101
|
+
"presets": [{ "name": "Sticky header", "category": "Header" }]
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### notes
|
|
106
|
+
|
|
107
|
+
JS toggle: ~10 lines vanilla — toggle `hidden` and `aria-expanded` on the button click. No library. Logo height capped via explicit `width`/`height` attributes to prevent CLS. Anti-pattern #16: logo `image_picker` MUST NOT have a `default` (Shopify); the `{% if %}` guard handles the empty case.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Pattern B — Full-bleed Hero (aspect-ratio, background-image, escape container)
|
|
112
|
+
|
|
113
|
+
- **id:** B
|
|
114
|
+
- **name:** Full-bleed Hero
|
|
115
|
+
- **applies-when:** above-the-fold hero with edge-to-edge background image, headline, CTA.
|
|
116
|
+
|
|
117
|
+
### markup-wp
|
|
118
|
+
|
|
119
|
+
```html
|
|
120
|
+
<link rel="preload" as="image" href="{hero localPath}">
|
|
121
|
+
<section class="hero">
|
|
122
|
+
<div class="hero__inner">
|
|
123
|
+
<h1 class="hero__title">Headline</h1>
|
|
124
|
+
<a class="hero__cta hero__cta" href="/shop">Shop now</a>
|
|
125
|
+
</div>
|
|
126
|
+
</section>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### css-wp
|
|
130
|
+
|
|
131
|
+
```css
|
|
132
|
+
.hero {
|
|
133
|
+
width: 100vw; margin-left: calc(50% - 50vw);
|
|
134
|
+
aspect-ratio: 1 / 1.3; max-height: 700px;
|
|
135
|
+
background-image: url("{hero localPath}");
|
|
136
|
+
background-size: cover; background-position: center;
|
|
137
|
+
display: grid; place-items: end center; padding-bottom: 48px;
|
|
138
|
+
}
|
|
139
|
+
.hero .hero__title { font-family: 'Playfair', serif !important; font-size: 36px !important; font-weight: 700 !important; color: #fff !important; }
|
|
140
|
+
.hero .hero__cta.hero__cta { appearance: none; background: #d8112a !important; color: #fff !important; font-family: inherit !important; font-weight: 700 !important; font-size: 16px !important; border: 0; padding: 14px 28px; border-radius: 999px; }
|
|
141
|
+
@media (min-width: 1000px) {
|
|
142
|
+
.hero { aspect-ratio: 16 / 9; max-height: 720px; }
|
|
143
|
+
.hero .hero__title { font-size: 56px !important; }
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### markup-shopify
|
|
148
|
+
|
|
149
|
+
```liquid
|
|
150
|
+
<section class="sb-hero">
|
|
151
|
+
<div class="sb-hero__inner">
|
|
152
|
+
<h1 class="sb-hero__title">{{ section.settings.heading | escape }}</h1>
|
|
153
|
+
{% if section.settings.cta_label != blank %}
|
|
154
|
+
<a class="sb-hero__cta sb-hero__cta" href="{{ section.settings.cta_link | default: '/' }}">{{ section.settings.cta_label | escape }}</a>
|
|
155
|
+
{% endif %}
|
|
156
|
+
</div>
|
|
157
|
+
</section>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### css-shopify
|
|
161
|
+
|
|
162
|
+
```liquid
|
|
163
|
+
{% style %}
|
|
164
|
+
.sb-hero {
|
|
165
|
+
width: 100vw; margin-left: calc(50% - 50vw);
|
|
166
|
+
aspect-ratio: 1 / 1.3; max-height: 700px;
|
|
167
|
+
{% if section.settings.hero_image %}
|
|
168
|
+
background-image: url({{ section.settings.hero_image | image_url: width: 1600 }});
|
|
169
|
+
{% else %}
|
|
170
|
+
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMTcwIDkwMCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQgc2xpY2UiPjxyZWN0IHdpZHRoPSIxMTcwIiBoZWlnaHQ9IjkwMCIgZmlsbD0iI2Q0ZDRkNCIvPjwvc3ZnPg==');
|
|
171
|
+
{% endif %}
|
|
172
|
+
background-size: cover; background-position: center;
|
|
173
|
+
display: grid; place-items: end center; padding-bottom: 48px;
|
|
174
|
+
}
|
|
175
|
+
.sb-hero .sb-hero__title { font-family: inherit !important; font-size: 36px !important; font-weight: 700 !important; color: #fff !important; }
|
|
176
|
+
.sb-hero .sb-hero__cta.sb-hero__cta { appearance: none; background: {{ section.settings.cta_bg_color | default: '#000' }}; color: {{ section.settings.cta_text_color | default: '#fff' }}; font-family: inherit !important; font-weight: 700 !important; font-size: 16px !important; border: 0; padding: 14px 28px; border-radius: 999px; }
|
|
177
|
+
@media (min-width: 1000px) {
|
|
178
|
+
.sb-hero { aspect-ratio: 16 / 9; max-height: 720px; }
|
|
179
|
+
.sb-hero .sb-hero__title { font-size: 56px !important; }
|
|
180
|
+
}
|
|
181
|
+
{% endstyle %}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### schema-shopify
|
|
185
|
+
|
|
186
|
+
```json
|
|
187
|
+
{
|
|
188
|
+
"settings": [
|
|
189
|
+
{ "type": "header", "content": "Content" },
|
|
190
|
+
{ "type": "text", "id": "heading", "label": "Heading", "default": "Welcome" },
|
|
191
|
+
{ "type": "image_picker", "id": "hero_image", "label": "Hero image" },
|
|
192
|
+
{ "type": "text", "id": "cta_label", "label": "CTA label", "default": "Shop now" },
|
|
193
|
+
{ "type": "url", "id": "cta_link", "label": "CTA link" },
|
|
194
|
+
{ "type": "header", "content": "Colors" },
|
|
195
|
+
{ "type": "color", "id": "cta_bg_color", "label": "CTA background", "default": "#000000" },
|
|
196
|
+
{ "type": "color", "id": "cta_text_color", "label": "CTA text", "default": "#ffffff" }
|
|
197
|
+
],
|
|
198
|
+
"presets": [{ "name": "Hero — clone of source", "category": "Image" }]
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### notes
|
|
203
|
+
|
|
204
|
+
Anti-patterns covered: #1 (no `100vh`), #2 (background-image not `<img height: 100%>`), #5 (chained scope + doubled CTA class), #11 (image_picker without `default`), #16 (base64 SVG fallback, no localPath leak). Shopify build emits **no** `<link rel="preload">` and **no** Google Fonts `<link>` — Shopify owns `<head>`, theme handles font loading and preloading.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Pattern C — Carousel mobile / Grid desktop (scroll-snap → grid)
|
|
209
|
+
|
|
210
|
+
- **id:** C
|
|
211
|
+
- **name:** Carousel mobile / Grid desktop
|
|
212
|
+
- **applies-when:** product grid, feature grid, testimonial row — repeating cards that scroll horizontally on mobile and tile on desktop.
|
|
213
|
+
|
|
214
|
+
### markup-wp
|
|
215
|
+
|
|
216
|
+
```html
|
|
217
|
+
<div class="products">
|
|
218
|
+
<article class="products__item">
|
|
219
|
+
<img src="{p1 localPath}" alt="Product 1" loading="lazy" width="600" height="600">
|
|
220
|
+
<h3>Product 1</h3><p>$24</p>
|
|
221
|
+
</article>
|
|
222
|
+
<article class="products__item">
|
|
223
|
+
<img src="{p2 localPath}" alt="Product 2" loading="lazy" width="600" height="600">
|
|
224
|
+
<h3>Product 2</h3><p>$28</p>
|
|
225
|
+
</article>
|
|
226
|
+
</div>
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### css-wp
|
|
230
|
+
|
|
231
|
+
```css
|
|
232
|
+
.products { display: flex; overflow-x: auto; scroll-snap-type: x mandatory; gap: 16px; padding: 16px; -webkit-overflow-scrolling: touch; }
|
|
233
|
+
.products .products__item { flex: 0 0 80vw; scroll-snap-align: start; }
|
|
234
|
+
.products .products__item img { width: 100%; height: auto; }
|
|
235
|
+
@media (min-width: 1000px) {
|
|
236
|
+
.products { display: grid; grid-template-columns: repeat(4, 1fr); overflow: visible; }
|
|
237
|
+
.products .products__item { flex: none; }
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### markup-shopify
|
|
242
|
+
|
|
243
|
+
```liquid
|
|
244
|
+
<div class="sb-products">
|
|
245
|
+
{% for block in section.blocks %}
|
|
246
|
+
<article class="sb-products__item" {{ block.shopify_attributes }}>
|
|
247
|
+
{% if block.settings.image %}
|
|
248
|
+
<img src="{{ block.settings.image | image_url: width: 600 }}" alt="{{ block.settings.title | escape }}" loading="lazy" width="600" height="600">
|
|
249
|
+
{% endif %}
|
|
250
|
+
<h3>{{ block.settings.title }}</h3>
|
|
251
|
+
<p>{{ block.settings.price }}</p>
|
|
252
|
+
{% if block.settings.link != blank %}
|
|
253
|
+
<a href="{{ block.settings.link }}" class="sb-products__link">Shop</a>
|
|
254
|
+
{% endif %}
|
|
255
|
+
</article>
|
|
256
|
+
{% endfor %}
|
|
257
|
+
</div>
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### css-shopify
|
|
261
|
+
|
|
262
|
+
Same CSS as WP variant inside `{% style %}`; replace `.products` with `.sb-products`.
|
|
263
|
+
|
|
264
|
+
### schema-shopify
|
|
265
|
+
|
|
266
|
+
```json
|
|
267
|
+
{
|
|
268
|
+
"settings": [
|
|
269
|
+
{ "type": "text", "id": "heading", "label": "Section heading", "default": "Featured products" }
|
|
270
|
+
],
|
|
271
|
+
"blocks": [
|
|
272
|
+
{
|
|
273
|
+
"type": "product",
|
|
274
|
+
"name": "Product card",
|
|
275
|
+
"settings": [
|
|
276
|
+
{ "type": "image_picker", "id": "image", "label": "Image" },
|
|
277
|
+
{ "type": "text", "id": "title", "label": "Title", "default": "Product" },
|
|
278
|
+
{ "type": "text", "id": "price", "label": "Price", "default": "$24" },
|
|
279
|
+
{ "type": "url", "id": "link", "label": "Link" }
|
|
280
|
+
]
|
|
281
|
+
}
|
|
282
|
+
],
|
|
283
|
+
"max_blocks": 12,
|
|
284
|
+
"presets": [{ "name": "Product grid", "category": "Image" }]
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### notes
|
|
289
|
+
|
|
290
|
+
`{{ block.shopify_attributes }}` on the outer `<article>` is **mandatory** (anti-pattern #13) — without it, the merchant cannot click/move/reorder cards in the editor. `loading="lazy"` on every card image — only the hero (Pattern B) gets `loading="eager" fetchpriority="high"`.
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Pattern D — Image-with-text overlap (`::before` band on the section)
|
|
295
|
+
|
|
296
|
+
- **id:** D
|
|
297
|
+
- **name:** Image-with-text overlap
|
|
298
|
+
- **applies-when:** section where an image overlaps a colored band, the band visually spans full bleed and the image floats in front.
|
|
299
|
+
|
|
300
|
+
### markup-wp
|
|
301
|
+
|
|
302
|
+
```html
|
|
303
|
+
<section class="story">
|
|
304
|
+
<div class="story__copy">
|
|
305
|
+
<h2 class="story__title">Our story</h2>
|
|
306
|
+
<p class="story__body">...</p>
|
|
307
|
+
</div>
|
|
308
|
+
<img class="story__image" src="{story localPath}" alt="Our team" loading="lazy">
|
|
309
|
+
</section>
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### css-wp
|
|
313
|
+
|
|
314
|
+
```css
|
|
315
|
+
.story { position: relative; padding: 48px 16px; }
|
|
316
|
+
.story::before { content: ""; position: absolute; inset: 30% 0 0 0; background: #f4eee8; z-index: -1; }
|
|
317
|
+
.story .story__title { font-size: 28px !important; font-weight: 700 !important; }
|
|
318
|
+
.story .story__image { width: 100%; height: auto; display: block; margin-top: 24px; }
|
|
319
|
+
@media (min-width: 1000px) {
|
|
320
|
+
.story { display: grid; grid-template-columns: 1fr 1fr; gap: 48px; align-items: center; padding: 96px 48px; }
|
|
321
|
+
.story::before { inset: 0 50% 0 0; }
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### markup-shopify
|
|
326
|
+
|
|
327
|
+
```liquid
|
|
328
|
+
<section class="sb-story">
|
|
329
|
+
<div class="sb-story__copy">
|
|
330
|
+
<h2 class="sb-story__title">{{ section.settings.heading | escape }}</h2>
|
|
331
|
+
<div class="sb-story__body">{{ section.settings.body }}</div>
|
|
332
|
+
</div>
|
|
333
|
+
{% if section.settings.image %}
|
|
334
|
+
<img class="sb-story__image" src="{{ section.settings.image | image_url: width: 1200 }}" alt="{{ section.settings.image.alt | default: section.settings.heading | escape }}" loading="lazy" width="{{ section.settings.image.width }}" height="{{ section.settings.image.height }}">
|
|
335
|
+
{% endif %}
|
|
336
|
+
</section>
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### css-shopify
|
|
340
|
+
|
|
341
|
+
Same CSS inside `{% style %}`; replace `.story` with `.sb-story`. The band color can be lifted to a `color` setting (`section.settings.band_color`) and interpolated.
|
|
342
|
+
|
|
343
|
+
### schema-shopify
|
|
344
|
+
|
|
345
|
+
```json
|
|
346
|
+
{
|
|
347
|
+
"settings": [
|
|
348
|
+
{ "type": "text", "id": "heading", "label": "Heading", "default": "Our story" },
|
|
349
|
+
{ "type": "richtext", "id": "body", "label": "Body", "default": "<p>...</p>" },
|
|
350
|
+
{ "type": "image_picker", "id": "image", "label": "Image" },
|
|
351
|
+
{ "type": "color", "id": "band_color", "label": "Band color", "default": "#f4eee8" }
|
|
352
|
+
],
|
|
353
|
+
"presets": [{ "name": "Story with image overlap", "category": "Text" }]
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### notes
|
|
358
|
+
|
|
359
|
+
The band is a `::before` on the SECTION, not a sibling box on the column wrapper (anti-pattern #6). The exact `inset` values come from `inspection.pseudoElements` — never eyeballed. `richtext` setting type for the body so the merchant can keep links/bold without HTML knowledge.
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Pattern E — Defensive button (chain class + appearance + `!important` on font props)
|
|
364
|
+
|
|
365
|
+
- **id:** E
|
|
366
|
+
- **name:** Defensive button
|
|
367
|
+
- **applies-when:** any styled CTA / primary button / secondary button.
|
|
368
|
+
|
|
369
|
+
### markup-wp
|
|
370
|
+
|
|
371
|
+
```html
|
|
372
|
+
<button class="cta" type="button">Shop now</button>
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### css-wp
|
|
376
|
+
|
|
377
|
+
```css
|
|
378
|
+
.{scope} .cta.cta {
|
|
379
|
+
appearance: none;
|
|
380
|
+
background: #d8112a !important;
|
|
381
|
+
color: #fff !important;
|
|
382
|
+
font-family: inherit !important;
|
|
383
|
+
font-weight: 700 !important;
|
|
384
|
+
font-size: 16px !important;
|
|
385
|
+
border: 0;
|
|
386
|
+
padding: 14px 28px;
|
|
387
|
+
border-radius: 999px;
|
|
388
|
+
cursor: pointer;
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### markup-shopify
|
|
393
|
+
|
|
394
|
+
```liquid
|
|
395
|
+
<button class="sb-cta sb-cta" type="button">{{ section.settings.cta_label | escape }}</button>
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### css-shopify
|
|
399
|
+
|
|
400
|
+
```liquid
|
|
401
|
+
{% style %}
|
|
402
|
+
.{scope} .sb-cta.sb-cta {
|
|
403
|
+
appearance: none;
|
|
404
|
+
background: {{ section.settings.cta_bg_color | default: '#000' }};
|
|
405
|
+
color: {{ section.settings.cta_text_color | default: '#fff' }};
|
|
406
|
+
font-family: inherit !important;
|
|
407
|
+
font-weight: 700 !important;
|
|
408
|
+
font-size: 16px !important;
|
|
409
|
+
border: 0;
|
|
410
|
+
padding: 14px 28px;
|
|
411
|
+
border-radius: 999px;
|
|
412
|
+
cursor: pointer;
|
|
413
|
+
}
|
|
414
|
+
{% endstyle %}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### schema-shopify
|
|
418
|
+
|
|
419
|
+
```json
|
|
420
|
+
{
|
|
421
|
+
"settings": [
|
|
422
|
+
{ "type": "text", "id": "cta_label", "label": "Label", "default": "Shop now" },
|
|
423
|
+
{ "type": "color", "id": "cta_bg_color", "label": "Background", "default": "#000000" },
|
|
424
|
+
{ "type": "color", "id": "cta_text_color", "label": "Text", "default": "#ffffff" }
|
|
425
|
+
]
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### notes
|
|
430
|
+
|
|
431
|
+
The double-class chain (`.cta.cta`) raises specificity beyond `.elementor button` and `.shopify-section button`. `!important` on font properties only — never on background/color (anti-pattern #5b). Always `type="button"` to avoid accidental form submits in Elementor / Dawn pages with hidden forms. On Shopify, button colors live in `color` settings so the merchant can re-skin without touching code.
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## Pattern F — FAQ collapsible (`<details>/<summary>`)
|
|
436
|
+
|
|
437
|
+
- **id:** F
|
|
438
|
+
- **name:** FAQ collapsible
|
|
439
|
+
- **applies-when:** any list of expandable Q/A items.
|
|
440
|
+
|
|
441
|
+
### markup-wp
|
|
442
|
+
|
|
443
|
+
```html
|
|
444
|
+
<div class="faq">
|
|
445
|
+
<details class="faq__item">
|
|
446
|
+
<summary class="faq__q">How do I return an order?</summary>
|
|
447
|
+
<div class="faq__a">Email us within 30 days...</div>
|
|
448
|
+
</details>
|
|
449
|
+
<details class="faq__item">
|
|
450
|
+
<summary class="faq__q">Do you ship internationally?</summary>
|
|
451
|
+
<div class="faq__a">Yes, we ship to ...</div>
|
|
452
|
+
</details>
|
|
453
|
+
</div>
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### css-wp
|
|
457
|
+
|
|
458
|
+
```css
|
|
459
|
+
.faq .faq__item { border-bottom: 1px solid #eee; }
|
|
460
|
+
.faq .faq__item summary { list-style: none; cursor: pointer; padding: 16px 0; font-weight: 600 !important; font-size: 16px !important; }
|
|
461
|
+
.faq .faq__item summary::-webkit-details-marker { display: none; }
|
|
462
|
+
.faq .faq__item[open] summary { color: #d8112a !important; }
|
|
463
|
+
.faq .faq__item .faq__a { padding: 0 0 16px 0; font-size: 14px !important; }
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### markup-shopify
|
|
467
|
+
|
|
468
|
+
```liquid
|
|
469
|
+
<div class="sb-faq">
|
|
470
|
+
{% for block in section.blocks %}
|
|
471
|
+
<details class="sb-faq__item" {{ block.shopify_attributes }}>
|
|
472
|
+
<summary class="sb-faq__q">{{ block.settings.question }}</summary>
|
|
473
|
+
<div class="sb-faq__a">{{ block.settings.answer }}</div>
|
|
474
|
+
</details>
|
|
475
|
+
{% endfor %}
|
|
476
|
+
</div>
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### css-shopify
|
|
480
|
+
|
|
481
|
+
Same CSS inside `{% style %}`; replace `.faq` with `.sb-faq`.
|
|
482
|
+
|
|
483
|
+
### schema-shopify
|
|
484
|
+
|
|
485
|
+
```json
|
|
486
|
+
{
|
|
487
|
+
"blocks": [
|
|
488
|
+
{
|
|
489
|
+
"type": "qa",
|
|
490
|
+
"name": "Q&A",
|
|
491
|
+
"settings": [
|
|
492
|
+
{ "type": "text", "id": "question", "label": "Question", "default": "Question?" },
|
|
493
|
+
{ "type": "richtext", "id": "answer", "label": "Answer", "default": "<p>Answer.</p>" }
|
|
494
|
+
]
|
|
495
|
+
}
|
|
496
|
+
],
|
|
497
|
+
"max_blocks": 20,
|
|
498
|
+
"presets": [{ "name": "FAQ", "category": "Text" }]
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### notes
|
|
503
|
+
|
|
504
|
+
Native `<details>/<summary>` — no JS, no library, accessible by default. The marker reset (`::-webkit-details-marker { display: none }`) is required because every theme styles it differently and the rendered triangle clashes with the design.
|
|
505
|
+
|
|
506
|
+
---
|
|
507
|
+
|
|
508
|
+
## Pattern G — Trust banner with modal (vanilla JS ~30 lines, native `<dialog>`)
|
|
509
|
+
|
|
510
|
+
- **id:** G
|
|
511
|
+
- **name:** Trust banner with modal
|
|
512
|
+
- **applies-when:** trust strip / guarantee banner that opens a modal with details on click.
|
|
513
|
+
|
|
514
|
+
### markup-wp
|
|
515
|
+
|
|
516
|
+
```html
|
|
517
|
+
<div class="trust">
|
|
518
|
+
<p class="trust__copy">100% money-back guarantee</p>
|
|
519
|
+
<button type="button" class="trust__more" data-open="trust-modal">Learn more</button>
|
|
520
|
+
<dialog id="trust-modal" class="trust__modal">
|
|
521
|
+
<button type="button" class="trust__close" data-close aria-label="Close">×</button>
|
|
522
|
+
<h2>Our guarantee</h2>
|
|
523
|
+
<p>Not happy? Email us within 30 days for a full refund.</p>
|
|
524
|
+
</dialog>
|
|
525
|
+
</div>
|
|
526
|
+
<script>
|
|
527
|
+
document.querySelectorAll('[data-open]').forEach(btn =>
|
|
528
|
+
btn.addEventListener('click', () => document.getElementById(btn.dataset.open).showModal())
|
|
529
|
+
);
|
|
530
|
+
document.querySelectorAll('[data-close]').forEach(btn =>
|
|
531
|
+
btn.addEventListener('click', () => btn.closest('dialog').close())
|
|
532
|
+
);
|
|
533
|
+
</script>
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### css-wp
|
|
537
|
+
|
|
538
|
+
```css
|
|
539
|
+
.trust { display: flex; align-items: center; gap: 12px; justify-content: center; padding: 16px; }
|
|
540
|
+
.trust .trust__more { background: none; border: 0; text-decoration: underline; cursor: pointer; }
|
|
541
|
+
.trust__modal { padding: 32px; max-width: 480px; border: 0; border-radius: 8px; }
|
|
542
|
+
.trust__modal::backdrop { background: rgba(0,0,0,0.5); }
|
|
543
|
+
.trust__modal .trust__close { position: absolute; top: 12px; right: 12px; background: none; border: 0; font-size: 24px; cursor: pointer; }
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
### markup-shopify
|
|
547
|
+
|
|
548
|
+
```liquid
|
|
549
|
+
<div class="sb-trust">
|
|
550
|
+
<p class="sb-trust__copy">{{ section.settings.banner_copy | escape }}</p>
|
|
551
|
+
<button type="button" class="sb-trust__more" data-open="sb-trust-modal-{{ section.id }}">{{ section.settings.cta_label | default: 'Learn more' }}</button>
|
|
552
|
+
<dialog id="sb-trust-modal-{{ section.id }}" class="sb-trust__modal">
|
|
553
|
+
<button type="button" class="sb-trust__close" data-close aria-label="Close">×</button>
|
|
554
|
+
<h2>{{ section.settings.modal_heading | escape }}</h2>
|
|
555
|
+
<div>{{ section.settings.modal_body }}</div>
|
|
556
|
+
</dialog>
|
|
557
|
+
</div>
|
|
558
|
+
<script>
|
|
559
|
+
document.querySelectorAll('[data-open]').forEach(btn =>
|
|
560
|
+
btn.addEventListener('click', () => document.getElementById(btn.dataset.open).showModal())
|
|
561
|
+
);
|
|
562
|
+
document.querySelectorAll('[data-close]').forEach(btn =>
|
|
563
|
+
btn.addEventListener('click', () => btn.closest('dialog').close())
|
|
564
|
+
);
|
|
565
|
+
</script>
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### css-shopify
|
|
569
|
+
|
|
570
|
+
Same CSS inside `{% style %}`; replace `.trust` with `.sb-trust`.
|
|
571
|
+
|
|
572
|
+
### schema-shopify
|
|
573
|
+
|
|
574
|
+
```json
|
|
575
|
+
{
|
|
576
|
+
"settings": [
|
|
577
|
+
{ "type": "text", "id": "banner_copy", "label": "Banner copy", "default": "100% money-back guarantee" },
|
|
578
|
+
{ "type": "text", "id": "cta_label", "label": "CTA label", "default": "Learn more" },
|
|
579
|
+
{ "type": "text", "id": "modal_heading", "label": "Modal heading", "default": "Our guarantee" },
|
|
580
|
+
{ "type": "richtext", "id": "modal_body", "label": "Modal body", "default": "<p>...</p>" }
|
|
581
|
+
],
|
|
582
|
+
"presets": [{ "name": "Trust banner", "category": "Promotional" }]
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### notes
|
|
587
|
+
|
|
588
|
+
Native `<dialog>` — `dialog.showModal()` opens with backdrop and traps focus; `dialog.close()` closes. No library, no custom backdrop logic. The Shopify variant suffixes the dialog `id` with `{{ section.id }}` so multiple instances on the same page don't collide. Anti-pattern web-modal-dialog: never use `<div role="dialog">` — always native `<dialog>`.
|
|
589
|
+
|
|
590
|
+
---
|
|
591
|
+
|
|
592
|
+
## Pattern H — Variant card e-commerce (radio + product photo)
|
|
593
|
+
|
|
594
|
+
- **id:** H
|
|
595
|
+
- **name:** Variant card
|
|
596
|
+
- **applies-when:** variant picker on a PDP — sizes, colors, scents — radio inputs paired with real product photos.
|
|
597
|
+
|
|
598
|
+
### markup-wp
|
|
599
|
+
|
|
600
|
+
```html
|
|
601
|
+
<fieldset class="variants">
|
|
602
|
+
<legend class="variants__title">Choose your size</legend>
|
|
603
|
+
<label class="variants__opt">
|
|
604
|
+
<input type="radio" name="size" value="30ml" checked>
|
|
605
|
+
<img src="{30ml localPath}" alt="30ml bottle" loading="lazy">
|
|
606
|
+
<span>30ml — $24</span>
|
|
607
|
+
</label>
|
|
608
|
+
<label class="variants__opt">
|
|
609
|
+
<input type="radio" name="size" value="100ml">
|
|
610
|
+
<img src="{100ml localPath}" alt="100ml bottle" loading="lazy">
|
|
611
|
+
<span>100ml — $54</span>
|
|
612
|
+
</label>
|
|
613
|
+
</fieldset>
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
### css-wp
|
|
617
|
+
|
|
618
|
+
```css
|
|
619
|
+
.variants { border: 0; padding: 0; }
|
|
620
|
+
.variants .variants__title { font-size: 14px !important; font-weight: 600 !important; margin-bottom: 12px; }
|
|
621
|
+
.variants .variants__opt { display: grid; grid-template-columns: 80px 1fr; gap: 12px; align-items: center; padding: 12px; border: 1px solid #eee; border-radius: 8px; cursor: pointer; }
|
|
622
|
+
.variants .variants__opt:has(input:checked) { border-color: #d8112a; background: #fef2f3; }
|
|
623
|
+
.variants .variants__opt input { display: none; }
|
|
624
|
+
.variants .variants__opt img { width: 80px; height: 80px; object-fit: cover; }
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
### markup-shopify
|
|
628
|
+
|
|
629
|
+
```liquid
|
|
630
|
+
<fieldset class="sb-variants">
|
|
631
|
+
<legend class="sb-variants__title">{{ section.settings.heading | default: 'Choose option' | escape }}</legend>
|
|
632
|
+
{% for block in section.blocks %}
|
|
633
|
+
<label class="sb-variants__opt" {{ block.shopify_attributes }}>
|
|
634
|
+
<input type="radio" name="variant" value="{{ block.settings.value | escape }}" {% if forloop.first %}checked{% endif %}>
|
|
635
|
+
{% if block.settings.image %}
|
|
636
|
+
<img src="{{ block.settings.image | image_url: width: 160 }}" alt="{{ block.settings.label | escape }}" loading="lazy" width="80" height="80">
|
|
637
|
+
{% endif %}
|
|
638
|
+
<span>{{ block.settings.label }}</span>
|
|
639
|
+
</label>
|
|
640
|
+
{% endfor %}
|
|
641
|
+
</fieldset>
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
### css-shopify
|
|
645
|
+
|
|
646
|
+
Same CSS inside `{% style %}`; replace `.variants` with `.sb-variants`.
|
|
647
|
+
|
|
648
|
+
### schema-shopify
|
|
649
|
+
|
|
650
|
+
```json
|
|
651
|
+
{
|
|
652
|
+
"settings": [
|
|
653
|
+
{ "type": "text", "id": "heading", "label": "Heading", "default": "Choose option" }
|
|
654
|
+
],
|
|
655
|
+
"blocks": [
|
|
656
|
+
{
|
|
657
|
+
"type": "variant",
|
|
658
|
+
"name": "Variant",
|
|
659
|
+
"settings": [
|
|
660
|
+
{ "type": "image_picker", "id": "image", "label": "Image" },
|
|
661
|
+
{ "type": "text", "id": "label", "label": "Label", "default": "Variant" },
|
|
662
|
+
{ "type": "text", "id": "value", "label": "Value", "default": "variant-1" }
|
|
663
|
+
]
|
|
664
|
+
}
|
|
665
|
+
],
|
|
666
|
+
"max_blocks": 8,
|
|
667
|
+
"presets": [{ "name": "Variant picker", "category": "Image" }]
|
|
668
|
+
}
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
### notes
|
|
672
|
+
|
|
673
|
+
Real product photos from `assetsMap[url].localPath` (WP) or `image_url` filter (Shopify) — never SVG illustrations (anti-pattern #8). `:has(input:checked)` for the active state — no JS, modern CSS. The hidden radio input is still focusable; for keyboard users, the focus ring on the `<label>` is what matters.
|
|
674
|
+
|
|
675
|
+
---
|
|
676
|
+
|
|
677
|
+
## Maintenance
|
|
678
|
+
|
|
679
|
+
- New patterns get appended in alphabetical order (next: `I`, `J`, ...).
|
|
680
|
+
- When a pattern needs a Shopify-specific tweak the WP version doesn't have (or vice versa), document it in the `notes` field rather than forking the entry.
|
|
681
|
+
- When `<plugin>/memory/patterns.md` and per-user override file disagree, per-user wins; this file is the floor.
|