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.
Files changed (76) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/LICENSE +21 -0
  3. package/README.md +301 -0
  4. package/bin/install.js +256 -0
  5. package/lib/copy-templates.mjs +52 -0
  6. package/lib/install-deps.mjs +62 -0
  7. package/lib/prompt-config.mjs +83 -0
  8. package/lib/verify-env.mjs +19 -0
  9. package/package.json +63 -0
  10. package/scripts/sync-templates.mjs +71 -0
  11. package/templates/commands/build-page.md +490 -0
  12. package/templates/commands/build-site.md +548 -0
  13. package/templates/commands/clip-section.md +519 -0
  14. package/templates/memory/anti-patterns.md +212 -0
  15. package/templates/memory/design-knowledge.md +225 -0
  16. package/templates/memory/fixes.md +163 -0
  17. package/templates/memory/patterns.md +681 -0
  18. package/templates/presets/shopify-section.yaml +51 -0
  19. package/templates/presets/wp-elementor.yaml +49 -0
  20. package/templates/reports/fixtures/mock-run-1.json +115 -0
  21. package/templates/reports/fixtures/mock-run-2.json +72 -0
  22. package/templates/reports/report-renderer.mjs +218 -0
  23. package/templates/reports/report-template.html +571 -0
  24. package/templates/skills/sb-build-shopify/SKILL.md +104 -0
  25. package/templates/skills/sb-build-shopify/references/shopify-build-rules.md +563 -0
  26. package/templates/skills/sb-build-shopify/scripts/build-shopify.mjs +637 -0
  27. package/templates/skills/sb-build-shopify/scripts/tests/test-build-shopify.mjs +424 -0
  28. package/templates/skills/sb-build-wp/SKILL.md +83 -0
  29. package/templates/skills/sb-build-wp/references/wp-build-rules.md +376 -0
  30. package/templates/skills/sb-build-wp/scripts/build-wp.mjs +327 -0
  31. package/templates/skills/sb-build-wp/scripts/tests/test-build-wp.mjs +224 -0
  32. package/templates/skills/sb-compare-visual/SKILL.md +121 -0
  33. package/templates/skills/sb-compare-visual/scripts/compare-visual.mjs +387 -0
  34. package/templates/skills/sb-compare-visual/scripts/lib/compare-tokens.mjs +273 -0
  35. package/templates/skills/sb-compare-visual/scripts/tests/test-compare-tokens.mjs +350 -0
  36. package/templates/skills/sb-compare-visual/scripts/tests/test-compare-visual.mjs +626 -0
  37. package/templates/skills/sb-crawl-and-list/SKILL.md +99 -0
  38. package/templates/skills/sb-crawl-and-list/scripts/crawl-and-list.mjs +437 -0
  39. package/templates/skills/sb-crawl-and-list/scripts/lib/blocklist-filter.mjs +176 -0
  40. package/templates/skills/sb-crawl-and-list/scripts/lib/fallback-crawler.mjs +107 -0
  41. package/templates/skills/sb-crawl-and-list/scripts/lib/page-classifier.mjs +89 -0
  42. package/templates/skills/sb-crawl-and-list/scripts/lib/sitemap-parser.mjs +118 -0
  43. package/templates/skills/sb-crawl-and-list/scripts/tests/test-blocklist-filter.mjs +204 -0
  44. package/templates/skills/sb-crawl-and-list/scripts/tests/test-crawl-and-list.mjs +276 -0
  45. package/templates/skills/sb-crawl-and-list/scripts/tests/test-fallback-crawler.mjs +243 -0
  46. package/templates/skills/sb-crawl-and-list/scripts/tests/test-page-classifier.mjs +120 -0
  47. package/templates/skills/sb-crawl-and-list/scripts/tests/test-sitemap-parser.mjs +157 -0
  48. package/templates/skills/sb-extract-assets/SKILL.md +112 -0
  49. package/templates/skills/sb-extract-assets/scripts/extract-assets.mjs +484 -0
  50. package/templates/skills/sb-extract-assets/scripts/tests/test-extract-assets.mjs +112 -0
  51. package/templates/skills/sb-inspect-live/SKILL.md +105 -0
  52. package/templates/skills/sb-inspect-live/scripts/inspect-live.mjs +693 -0
  53. package/templates/skills/sb-inspect-live/scripts/tests/test-inspect-live.mjs +181 -0
  54. package/templates/skills/sb-review-checks/SKILL.md +113 -0
  55. package/templates/skills/sb-review-checks/references/review-rules.md +195 -0
  56. package/templates/skills/sb-review-checks/scripts/lib/anti-patterns.mjs +379 -0
  57. package/templates/skills/sb-review-checks/scripts/lib/cross-reference.mjs +115 -0
  58. package/templates/skills/sb-review-checks/scripts/lib/design-quality.mjs +541 -0
  59. package/templates/skills/sb-review-checks/scripts/review-checks.mjs +250 -0
  60. package/templates/skills/sb-review-checks/scripts/tests/test-anti-patterns.mjs +343 -0
  61. package/templates/skills/sb-review-checks/scripts/tests/test-cross-reference.mjs +170 -0
  62. package/templates/skills/sb-review-checks/scripts/tests/test-design-quality.mjs +493 -0
  63. package/templates/skills/sb-review-checks/scripts/tests/test-review-checks.mjs +267 -0
  64. package/templates/skills/sb-tweak/SKILL.md +130 -0
  65. package/templates/skills/sb-tweak/references/tweak-patterns.md +157 -0
  66. package/templates/skills/sb-tweak/scripts/lib/diff-summarizer.mjs +140 -0
  67. package/templates/skills/sb-tweak/scripts/lib/element-locator.mjs +507 -0
  68. package/templates/skills/sb-tweak/scripts/lib/intent-parser.mjs +324 -0
  69. package/templates/skills/sb-tweak/scripts/tests/test-diff-summarizer.mjs +248 -0
  70. package/templates/skills/sb-tweak/scripts/tests/test-element-locator.mjs +418 -0
  71. package/templates/skills/sb-tweak/scripts/tests/test-intent-parser.mjs +496 -0
  72. package/templates/skills/sb-tweak/scripts/tests/test-tweak.mjs +407 -0
  73. package/templates/skills/sb-tweak/scripts/tweak.mjs +656 -0
  74. package/templates/skills/sb-validate-render/SKILL.md +120 -0
  75. package/templates/skills/sb-validate-render/scripts/tests/test-validate-render.mjs +304 -0
  76. 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.