rebly-sections 1.1.0 → 1.2.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/assets/data/design-tokens.csv +3 -0
- package/assets/data/settings-profiles.csv +4 -0
- package/assets/data/shopify-best-practices.csv +5 -0
- package/assets/scripts/core.py +17 -7
- package/assets/scripts/quality-gate-checks.py +55 -0
- package/assets/scripts/quality-gate.py +56 -2
- package/assets/scripts/section-generator-helpers.py +20 -6
- package/assets/scripts/section-generator.py +41 -50
- package/assets/templates/generation-prompt.md +75 -20
- package/package.json +1 -1
|
@@ -88,3 +88,6 @@ No,Token Name,Category,Value,CSS Variable,Keywords,Usage,Notes
|
|
|
88
88
|
87,z-overlay,z-index,30,--z-overlay,z-index overlay backdrop,z-index: var(--z-overlay),Z-index layer scale
|
|
89
89
|
88,z-modal,z-index,40,--z-modal,z-index modal dialog,z-index: var(--z-modal),Z-index layer scale
|
|
90
90
|
89,z-toast,z-index,50,--z-toast,z-index toast notification alert,z-index: var(--z-toast),Z-index layer scale
|
|
91
|
+
90,font-family-sans,typography,"system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif",--font-family-sans,font family sans serif system stack body,font-family: var(--font-family-sans),System sans-serif stack for body text
|
|
92
|
+
91,font-family-serif,typography,"Georgia, 'Times New Roman', serif",--font-family-serif,font family serif classic editorial heading,font-family: var(--font-family-serif),Classic serif stack for editorial headings
|
|
93
|
+
92,font-family-mono,typography,"'SF Mono', 'Fira Code', 'Courier New', monospace",--font-family-mono,font family monospace code technical,font-family: var(--font-family-mono),Monospace stack for code and technical content
|
|
@@ -119,6 +119,10 @@ hero,title_1,text,Title 1,,False,Appears in 296/2150 hero sections,7
|
|
|
119
119
|
hero,url_banner2,url,Url Banner2,,False,Appears in 276/2150 hero sections,8
|
|
120
120
|
hero,button_2,text,Button 2,,False,Appears in 261/2150 hero sections,9
|
|
121
121
|
hero,url_button1,url,Url Button1,,False,Appears in 254/2150 hero sections,10
|
|
122
|
+
hero,heading_font,font_picker,Heading Font,,False,Typography control for hero heading,11
|
|
123
|
+
hero,heading_size,select,Heading Size,,False,Small/Medium/Large heading scale,12
|
|
124
|
+
hero,heading_tag,select,Heading Tag,,False,H1/H2/H3 for SEO hierarchy,13
|
|
125
|
+
hero,color_scheme,color_scheme,Color Scheme,,False,Theme-wide color scheme,14
|
|
122
126
|
logo,title,text,Title,,True,Appears in 19/32 logo sections,1
|
|
123
127
|
logo,logo_max_width,image_picker,Logo Max Width,,True,Appears in 7/32 logo sections,2
|
|
124
128
|
logo,logo,image_picker,Logo,,True,Appears in 6/32 logo sections,3
|
|
@@ -51,3 +51,8 @@ No,Rule,Category,Severity,Keywords,Description,Do,Dont,Code Example
|
|
|
51
51
|
50,Structured data JSON-LD,seo,recommended,structured data schema.org json-ld rich snippets,"Add JSON-LD structured data for products, articles, FAQs","Include <script type=""application/ld+json""> with schema.org markup",Rely solely on meta tags without structured data,
|
|
52
52
|
51,Heading per section,seo,important,heading h2 section title SEO crawl hierarchy,Each section should have a configurable heading element,Use a heading_tag setting (h2/h3/h4) for section headings,Hardcode heading levels — breaks hierarchy across sections,
|
|
53
53
|
52,Image alt from settings,seo,important,image alt text setting SEO accessible,Provide alt text settings or use Shopify image.alt,"Output alt=""{{ section.settings.image.alt | escape }}""",Omit alt attributes on section images,
|
|
54
|
+
53,Typography controls required,schema,high,"font_picker heading typography control",Sections with heading text should include font_picker setting,Include font_picker setting for primary heading,Hardcode font-family in CSS only,
|
|
55
|
+
54,Heading tag configurable,seo,high,"heading tag h1 h2 h3 SEO hierarchy select",Heading level must be configurable via select setting for SEO,Add heading_tag select setting (h1/h2/h3/h4),Hardcode <h1> or <h2> without merchant control,
|
|
56
|
+
55,Color scheme over color picker,os2-architecture,high,"color_scheme color picker theme consistency",Prefer color_scheme setting over individual color pickers for theme consistency,Use type: color_scheme for section colors,Use individual color pickers for every color,
|
|
57
|
+
56,Button styling controls,schema,high,"button style color background CTA",CTA buttons should have configurable style/color settings,Add button_style select and button color settings,Hardcode button background and text colors,
|
|
58
|
+
57,No hardcoded font sizes,css,high,"font size hardcoded clamp responsive typography",Font sizes should use clamp() or CSS vars — never fixed px,Use clamp() or CSS custom properties for font-size,Use fixed px values like font-size: 48px,
|
package/assets/scripts/core.py
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import csv
|
|
6
6
|
import re
|
|
7
|
+
import sys
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from math import log
|
|
9
10
|
from collections import defaultdict
|
|
@@ -124,7 +125,7 @@ def _load_csv(filepath):
|
|
|
124
125
|
return list(csv.DictReader(f))
|
|
125
126
|
|
|
126
127
|
|
|
127
|
-
def _search_csv(filepath, search_cols, output_cols, query, max_results):
|
|
128
|
+
def _search_csv(filepath, search_cols, output_cols, query, max_results, min_score=0.0):
|
|
128
129
|
"""Core search function using BM25"""
|
|
129
130
|
if not filepath.exists():
|
|
130
131
|
return []
|
|
@@ -136,11 +137,20 @@ def _search_csv(filepath, search_cols, output_cols, query, max_results):
|
|
|
136
137
|
bm25.fit(documents)
|
|
137
138
|
ranked = bm25.score(query)
|
|
138
139
|
|
|
140
|
+
# Filter by min_score threshold
|
|
141
|
+
candidates = [(idx, score) for idx, score in ranked[:max_results * 2] if score > 0]
|
|
142
|
+
if min_score > 0:
|
|
143
|
+
filtered = [(idx, score) for idx, score in candidates if score >= min_score]
|
|
144
|
+
if len(candidates) != len(filtered):
|
|
145
|
+
print(f"[DEBUG] BM25: filtered {len(candidates) - len(filtered)} low-score results (threshold={min_score})", file=sys.stderr)
|
|
146
|
+
candidates = filtered
|
|
147
|
+
|
|
139
148
|
results = []
|
|
140
|
-
for idx, score in
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
149
|
+
for idx, score in candidates[:max_results]:
|
|
150
|
+
row = data[idx]
|
|
151
|
+
entry = {col: row.get(col, "") for col in output_cols if col in row}
|
|
152
|
+
entry['_score'] = score
|
|
153
|
+
results.append(entry)
|
|
144
154
|
|
|
145
155
|
return results
|
|
146
156
|
|
|
@@ -166,7 +176,7 @@ def detect_domain(query):
|
|
|
166
176
|
return best if scores[best] > 0 else "components"
|
|
167
177
|
|
|
168
178
|
|
|
169
|
-
def search(query, domain=None, max_results=MAX_RESULTS):
|
|
179
|
+
def search(query, domain=None, max_results=MAX_RESULTS, min_score=0.0):
|
|
170
180
|
"""Main search function with auto-domain detection"""
|
|
171
181
|
if domain is None:
|
|
172
182
|
domain = detect_domain(query)
|
|
@@ -177,7 +187,7 @@ def search(query, domain=None, max_results=MAX_RESULTS):
|
|
|
177
187
|
if not filepath.exists():
|
|
178
188
|
return {"error": f"File not found: {filepath}", "domain": domain}
|
|
179
189
|
|
|
180
|
-
results = _search_csv(filepath, config["search_cols"], config["output_cols"], query, max_results)
|
|
190
|
+
results = _search_csv(filepath, config["search_cols"], config["output_cols"], query, max_results, min_score=min_score)
|
|
181
191
|
|
|
182
192
|
return {
|
|
183
193
|
"domain": domain,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Additional quality gate checks — WARN-level advisory checks for section quality."""
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def check_has_color_scheme(content, schema_json):
|
|
8
|
+
"""Check if section schema includes color_scheme setting."""
|
|
9
|
+
if not schema_json:
|
|
10
|
+
return ("WARN", "No schema to check for color_scheme")
|
|
11
|
+
settings = schema_json.get("settings", [])
|
|
12
|
+
has_cs = any(s.get("type") == "color_scheme" for s in settings)
|
|
13
|
+
if has_cs:
|
|
14
|
+
return ("PASS", "color_scheme setting present")
|
|
15
|
+
return ("WARN", "Missing color_scheme setting — recommended for theme consistency")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def check_has_font_picker(content, schema_json):
|
|
19
|
+
"""Check if section schema includes at least one font_picker."""
|
|
20
|
+
if not schema_json:
|
|
21
|
+
return ("WARN", "No schema to check for font_picker")
|
|
22
|
+
settings = schema_json.get("settings", [])
|
|
23
|
+
has_fp = any(s.get("type") == "font_picker" for s in settings)
|
|
24
|
+
if has_fp:
|
|
25
|
+
return ("PASS", "font_picker setting present")
|
|
26
|
+
return ("WARN", "Missing font_picker — sections with headings should include typography control")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def check_has_heading_tag(content, schema_json):
|
|
30
|
+
"""Check if section has configurable heading tag for SEO."""
|
|
31
|
+
if not schema_json:
|
|
32
|
+
return ("WARN", "No schema to check for heading_tag")
|
|
33
|
+
settings = schema_json.get("settings", [])
|
|
34
|
+
has_ht = any(
|
|
35
|
+
s.get("id") in ("heading_tag", "heading_element", "tag")
|
|
36
|
+
and s.get("type") == "select"
|
|
37
|
+
for s in settings
|
|
38
|
+
)
|
|
39
|
+
if has_ht:
|
|
40
|
+
return ("PASS", "Heading tag configurable")
|
|
41
|
+
return ("WARN", "Missing heading_tag select — heading level should be merchant-configurable")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def check_no_hardcoded_colors(content, schema_json):
|
|
45
|
+
"""Check CSS doesn't contain hardcoded hex colors (outside Liquid variables)."""
|
|
46
|
+
style_match = re.search(r'<style[^>]*>(.*?)</style>', content, re.DOTALL)
|
|
47
|
+
if not style_match:
|
|
48
|
+
return ("PASS", "No style block found")
|
|
49
|
+
css = style_match.group(1)
|
|
50
|
+
# Strip Liquid variable references ({{ ... }}) before scanning
|
|
51
|
+
css_no_liquid = re.sub(r'\{\{.*?\}\}', '', css)
|
|
52
|
+
hex_colors = re.findall(r'#[0-9a-fA-F]{3,8}\b', css_no_liquid)
|
|
53
|
+
if hex_colors:
|
|
54
|
+
return ("WARN", f"Hardcoded colors in CSS: {', '.join(hex_colors[:5])} — use CSS variables instead")
|
|
55
|
+
return ("PASS", "No hardcoded colors in CSS")
|
|
@@ -6,12 +6,23 @@ Usage: python3 quality-gate.py path/to/section.liquid [--json]
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import argparse
|
|
9
|
+
import importlib.util
|
|
9
10
|
import json
|
|
10
11
|
import re
|
|
11
12
|
import sys
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
# Import advisory checks from helper module (graceful fallback if missing)
|
|
16
|
+
_checks_mod = None
|
|
17
|
+
try:
|
|
18
|
+
_checks_path = Path(__file__).parent / 'quality-gate-checks.py'
|
|
19
|
+
_spec = importlib.util.spec_from_file_location('qg_checks', _checks_path)
|
|
20
|
+
_checks_mod = importlib.util.module_from_spec(_spec)
|
|
21
|
+
_spec.loader.exec_module(_checks_mod)
|
|
22
|
+
except (FileNotFoundError, AttributeError):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
# Core checks (PASS/FAIL)
|
|
15
26
|
QUALITY_CHECKS = [
|
|
16
27
|
("schema_valid_json", "Schema block contains valid JSON"),
|
|
17
28
|
("has_presets", "Schema has non-empty presets array"),
|
|
@@ -26,6 +37,14 @@ QUALITY_CHECKS = [
|
|
|
26
37
|
("text_elements_explicit_color", "Text elements have explicit color in CSS"),
|
|
27
38
|
]
|
|
28
39
|
|
|
40
|
+
# Advisory checks (PASS/WARN) — imported from quality-gate-checks.py
|
|
41
|
+
ADVISORY_CHECKS = [
|
|
42
|
+
("has_color_scheme", "color_scheme setting present"),
|
|
43
|
+
("has_font_picker", "font_picker setting present"),
|
|
44
|
+
("has_heading_tag", "Heading tag configurable via select"),
|
|
45
|
+
("no_hardcoded_colors", "No hardcoded hex colors in CSS"),
|
|
46
|
+
]
|
|
47
|
+
|
|
29
48
|
|
|
30
49
|
def extract_schema_json(content):
|
|
31
50
|
"""Extract JSON from {% schema %} block"""
|
|
@@ -161,10 +180,13 @@ def run_quality_gate(filepath):
|
|
|
161
180
|
return f"Error: File not found: {filepath}"
|
|
162
181
|
|
|
163
182
|
content = path.read_text(encoding="utf-8")
|
|
183
|
+
schema_json = extract_schema_json(content)
|
|
164
184
|
results = []
|
|
165
185
|
passed = 0
|
|
166
186
|
failed = 0
|
|
187
|
+
warned = 0
|
|
167
188
|
|
|
189
|
+
# Core checks (PASS/FAIL)
|
|
168
190
|
for check_id, description in QUALITY_CHECKS:
|
|
169
191
|
check_fn = CHECK_FUNCTIONS[check_id]
|
|
170
192
|
try:
|
|
@@ -179,12 +201,44 @@ def run_quality_gate(filepath):
|
|
|
179
201
|
failed += 1
|
|
180
202
|
results.append(f"✗ {description}: ERROR ({e})")
|
|
181
203
|
|
|
204
|
+
# Advisory checks (PASS/WARN) — skip if helper module unavailable
|
|
205
|
+
if not _checks_mod:
|
|
206
|
+
advisory_fns = {}
|
|
207
|
+
else:
|
|
208
|
+
advisory_fns = {
|
|
209
|
+
"has_color_scheme": _checks_mod.check_has_color_scheme,
|
|
210
|
+
"has_font_picker": _checks_mod.check_has_font_picker,
|
|
211
|
+
"has_heading_tag": _checks_mod.check_has_heading_tag,
|
|
212
|
+
"no_hardcoded_colors": _checks_mod.check_no_hardcoded_colors,
|
|
213
|
+
}
|
|
214
|
+
for check_id, description in ADVISORY_CHECKS:
|
|
215
|
+
if check_id not in advisory_fns:
|
|
216
|
+
continue
|
|
217
|
+
check_fn = advisory_fns[check_id]
|
|
218
|
+
try:
|
|
219
|
+
status, message = check_fn(content, schema_json)
|
|
220
|
+
if status == "PASS":
|
|
221
|
+
passed += 1
|
|
222
|
+
else:
|
|
223
|
+
warned += 1
|
|
224
|
+
icon = "✓" if status == "PASS" else "⚠"
|
|
225
|
+
results.append(f"{icon} {message}: {status}")
|
|
226
|
+
except Exception as e:
|
|
227
|
+
warned += 1
|
|
228
|
+
results.append(f"⚠ {description}: ERROR ({e})")
|
|
229
|
+
|
|
230
|
+
total = passed + failed + warned
|
|
182
231
|
summary = f"\n## Quality Gate: {path.name}\n"
|
|
183
|
-
summary += f"**Score:** {passed}/{
|
|
232
|
+
summary += f"**Score:** {passed}/{total} checks passed"
|
|
233
|
+
if warned:
|
|
234
|
+
summary += f" ({warned} warnings)"
|
|
235
|
+
summary += "\n\n"
|
|
184
236
|
summary += "\n".join(results)
|
|
185
237
|
|
|
186
238
|
if failed == 0:
|
|
187
239
|
summary += "\n\n**Result: PASS** ✓"
|
|
240
|
+
if warned:
|
|
241
|
+
summary += f" (with {warned} advisory warnings)"
|
|
188
242
|
else:
|
|
189
243
|
summary += f"\n\n**Result: FAIL** — {failed} issue(s) found"
|
|
190
244
|
|
|
@@ -15,7 +15,7 @@ def search_block_patterns(description):
|
|
|
15
15
|
try:
|
|
16
16
|
result = search(description, domain="block-patterns", max_results=5)
|
|
17
17
|
if "error" in result or not result.get("results"):
|
|
18
|
-
return "
|
|
18
|
+
return ""
|
|
19
19
|
lines = ["**Relevant Block Patterns:**"]
|
|
20
20
|
for r in result["results"]:
|
|
21
21
|
lines.append(f"- [{r.get('Category', '')} / {r.get('Block Type', '')}] {r.get('Use Case', '')}")
|
|
@@ -25,7 +25,7 @@ def search_block_patterns(description):
|
|
|
25
25
|
lines.append(f" Example: `{ex[:200]}`")
|
|
26
26
|
return "\n".join(lines)
|
|
27
27
|
except (FileNotFoundError, KeyError):
|
|
28
|
-
return "
|
|
28
|
+
return ""
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def search_settings_profile(section_name):
|
|
@@ -33,7 +33,7 @@ def search_settings_profile(section_name):
|
|
|
33
33
|
try:
|
|
34
34
|
result = search(section_name, domain="settings-profiles", max_results=8)
|
|
35
35
|
if "error" in result or not result.get("results"):
|
|
36
|
-
return "
|
|
36
|
+
return ""
|
|
37
37
|
lines = ["**Recommended Settings Profile:**",
|
|
38
38
|
"| Setting ID | Type | Label | Default | Required |",
|
|
39
39
|
"|------------|------|-------|---------|----------|"]
|
|
@@ -42,7 +42,7 @@ def search_settings_profile(section_name):
|
|
|
42
42
|
f"{r.get('Label', '')} | {r.get('Default', '')} | {r.get('Required', '')} |")
|
|
43
43
|
return "\n".join(lines)
|
|
44
44
|
except (FileNotFoundError, KeyError):
|
|
45
|
-
return "
|
|
45
|
+
return ""
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def search_theme_dna(theme_name):
|
|
@@ -50,11 +50,25 @@ def search_theme_dna(theme_name):
|
|
|
50
50
|
try:
|
|
51
51
|
result = search(theme_name, domain="themes", max_results=1)
|
|
52
52
|
if "error" in result or not result.get("results"):
|
|
53
|
-
return "
|
|
53
|
+
return ""
|
|
54
54
|
r = result["results"][0]
|
|
55
55
|
return (f"**Theme DNA:** {r.get('Theme', '')} — "
|
|
56
56
|
f"CSS Prefix: {r.get('CSS Variable Prefix', '')} | "
|
|
57
57
|
f"Naming: {r.get('Section Naming', '')} | "
|
|
58
58
|
f"Blocks: {r.get('Block Conventions', '')}")
|
|
59
59
|
except (FileNotFoundError, KeyError):
|
|
60
|
-
return "
|
|
60
|
+
return ""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def search_design_tokens(description):
|
|
64
|
+
"""Search design-tokens.csv for relevant CSS tokens."""
|
|
65
|
+
try:
|
|
66
|
+
result = search(description, domain="tokens", max_results=5, min_score=0.3)
|
|
67
|
+
if "error" in result or not result.get("results"):
|
|
68
|
+
return ""
|
|
69
|
+
lines = ["**Relevant Design Tokens:**"]
|
|
70
|
+
for r in result["results"]:
|
|
71
|
+
lines.append(f"- `{r.get('CSS Variable', '')}`: {r.get('Value', '')} — {r.get('Usage', '')}")
|
|
72
|
+
return "\n".join(lines)
|
|
73
|
+
except (FileNotFoundError, KeyError):
|
|
74
|
+
return ""
|
|
@@ -6,6 +6,7 @@ Usage: python3 section-generator.py "name" "description" [--theme-profile path]
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import argparse
|
|
9
|
+
import re
|
|
9
10
|
import sys
|
|
10
11
|
import io
|
|
11
12
|
from pathlib import Path
|
|
@@ -25,6 +26,7 @@ _hs.loader.exec_module(_hm)
|
|
|
25
26
|
search_block_patterns = _hm.search_block_patterns
|
|
26
27
|
search_settings_profile = _hm.search_settings_profile
|
|
27
28
|
search_theme_dna = _hm.search_theme_dna
|
|
29
|
+
search_design_tokens = _hm.search_design_tokens
|
|
28
30
|
|
|
29
31
|
TEMPLATE_DIR = Path(__file__).parent.parent / "templates"
|
|
30
32
|
|
|
@@ -48,8 +50,8 @@ def load_template(name):
|
|
|
48
50
|
return ""
|
|
49
51
|
|
|
50
52
|
|
|
51
|
-
def search_components(description, max_results=
|
|
52
|
-
"""Search component library for closest pattern"""
|
|
53
|
+
def search_components(description, max_results=1):
|
|
54
|
+
"""Search component library for closest pattern — 1 full untruncated example"""
|
|
53
55
|
result = search_component(description, max_results=max_results)
|
|
54
56
|
if "error" in result or not result.get("results"):
|
|
55
57
|
return "No matching component patterns found."
|
|
@@ -59,14 +61,14 @@ def search_components(description, max_results=2):
|
|
|
59
61
|
output.append(f"**{r.get('Name', 'Unknown')}** ({r.get('Category', '')}, {r.get('Difficulty', '')})")
|
|
60
62
|
output.append(f"Blocks: {r.get('Blocks Used', '')} | Settings: {r.get('Settings Count', '')}")
|
|
61
63
|
if 'liquid_content' in r:
|
|
62
|
-
output.append(f"```liquid\n{r['liquid_content'][:
|
|
64
|
+
output.append(f"```liquid\n{r['liquid_content'][:6000]}\n```")
|
|
63
65
|
output.append("")
|
|
64
66
|
return "\n".join(output)
|
|
65
67
|
|
|
66
68
|
|
|
67
69
|
def search_schema_settings(description, max_results=5):
|
|
68
70
|
"""Search schema-library for relevant setting types"""
|
|
69
|
-
result = search(description, domain="schema", max_results=max_results)
|
|
71
|
+
result = search(description, domain="schema", max_results=max_results, min_score=0.3)
|
|
70
72
|
if "error" in result or not result.get("results"):
|
|
71
73
|
return "No matching schema settings found."
|
|
72
74
|
|
|
@@ -81,7 +83,7 @@ def search_schema_settings(description, max_results=5):
|
|
|
81
83
|
|
|
82
84
|
def search_best_practices(description, max_results=5):
|
|
83
85
|
"""Search best practices for applicable rules"""
|
|
84
|
-
result = search(description, domain="practices", max_results=max_results)
|
|
86
|
+
result = search(description, domain="practices", max_results=max_results, min_score=0.3)
|
|
85
87
|
if "error" in result or not result.get("results"):
|
|
86
88
|
return "No matching best practices found."
|
|
87
89
|
|
|
@@ -98,7 +100,7 @@ def search_best_practices(description, max_results=5):
|
|
|
98
100
|
|
|
99
101
|
|
|
100
102
|
def try_ui_ux_bridge(ui_style_keywords):
|
|
101
|
-
"""Optional: query ui-ux-pro-max if available"""
|
|
103
|
+
"""Optional: query ui-ux-pro-max if available. Returns empty string if N/A."""
|
|
102
104
|
try:
|
|
103
105
|
ux_search_path = Path(__file__).parent.parent.parent.parent / ".claude" / "skills" / "ui-ux-pro-max" / "scripts" / "core.py"
|
|
104
106
|
if not ux_search_path.exists():
|
|
@@ -111,19 +113,27 @@ def try_ui_ux_bridge(ui_style_keywords):
|
|
|
111
113
|
spec.loader.exec_module(ux_module)
|
|
112
114
|
result = ux_module.search(ui_style_keywords, domain="style", max_results=2)
|
|
113
115
|
if result.get("results"):
|
|
114
|
-
lines = ["**UI/UX Style Recommendations
|
|
116
|
+
lines = ["<design_recommendations>", "**UI/UX Style Recommendations:**"]
|
|
115
117
|
for r in result["results"]:
|
|
116
118
|
lines.append(f"- {r.get('Style Category', '')}: {r.get('Keywords', '')}")
|
|
117
119
|
lines.append(f" Effects: {r.get('Effects & Animation', '')}")
|
|
120
|
+
lines.append("</design_recommendations>")
|
|
118
121
|
return "\n".join(lines)
|
|
119
122
|
except Exception:
|
|
120
123
|
pass
|
|
121
|
-
|
|
124
|
+
# Return empty string instead of N/A to avoid wasting prompt tokens
|
|
125
|
+
return ""
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _strip_empty_xml_tags(content):
|
|
129
|
+
"""Remove XML tags whose body is empty or whitespace-only"""
|
|
130
|
+
return re.sub(r'<([\w-]+)>\s*</\1>', '', content)
|
|
122
131
|
|
|
123
132
|
|
|
124
133
|
def assemble_context(name, description, theme_profile, component, schema,
|
|
125
|
-
practices, design_recs, block_patterns="
|
|
126
|
-
settings_profile="
|
|
134
|
+
practices, design_recs, block_patterns="",
|
|
135
|
+
settings_profile="", theme_dna="",
|
|
136
|
+
design_tokens="", refinement=""):
|
|
127
137
|
"""Merge all context into generation prompt"""
|
|
128
138
|
template = load_template("generation-prompt.md")
|
|
129
139
|
base_template = load_template("section-base.liquid")
|
|
@@ -140,44 +150,16 @@ def assemble_context(name, description, theme_profile, component, schema,
|
|
|
140
150
|
content = content.replace("{{ block_patterns_results }}", block_patterns)
|
|
141
151
|
content = content.replace("{{ settings_profile_results }}", settings_profile)
|
|
142
152
|
content = content.replace("{{ theme_dna_context }}", theme_dna)
|
|
153
|
+
content = content.replace("{{ design_tokens_results }}", design_tokens)
|
|
154
|
+
content = content.replace("{{ refinement_context }}", refinement)
|
|
143
155
|
base_block = f"```liquid\n{base_template}\n```" if base_template else "See standard OS2.0 section structure"
|
|
144
156
|
content = content.replace("{{ section_base_content }}", base_block)
|
|
157
|
+
# Clean up empty XML tags from unused sections
|
|
158
|
+
content = _strip_empty_xml_tags(content)
|
|
145
159
|
return content
|
|
146
160
|
|
|
147
|
-
# Fallback: manual assembly
|
|
148
|
-
return f"
|
|
149
|
-
|
|
150
|
-
## Description
|
|
151
|
-
{description}
|
|
152
|
-
|
|
153
|
-
## Theme Context
|
|
154
|
-
{theme_profile}
|
|
155
|
-
|
|
156
|
-
## Closest Pattern Template
|
|
157
|
-
{component}
|
|
158
|
-
|
|
159
|
-
## Relevant Schema Settings
|
|
160
|
-
{schema}
|
|
161
|
-
|
|
162
|
-
## Applicable Best Practices
|
|
163
|
-
{practices}
|
|
164
|
-
|
|
165
|
-
## Design Recommendations
|
|
166
|
-
{design_recs}
|
|
167
|
-
|
|
168
|
-
## Base Template
|
|
169
|
-
```liquid
|
|
170
|
-
{base_template}
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
## Requirements
|
|
174
|
-
- OS2.0 compliant (presets, @app blocks, block.shopify_attributes)
|
|
175
|
-
- Scoped CSS via #section-{{{{ section.id }}}}
|
|
176
|
-
- Responsive (mobile-first)
|
|
177
|
-
- Accessible (semantic HTML, aria-labels, alt text)
|
|
178
|
-
- Performance (lazy-load non-hero images, no Liquid in CSS/JS)
|
|
179
|
-
- Use {{% render %}} not {{% include %}}
|
|
180
|
-
"""
|
|
161
|
+
# Fallback: manual assembly (shouldn't reach here with template present)
|
|
162
|
+
return f"# Generate Shopify Section: {name}\n\n{description}\n\n{component}\n\n{schema}\n\n{practices}"
|
|
181
163
|
|
|
182
164
|
|
|
183
165
|
def main():
|
|
@@ -189,14 +171,18 @@ def main():
|
|
|
189
171
|
args = parser.parse_args()
|
|
190
172
|
|
|
191
173
|
theme = load_theme_profile(args.theme_profile)
|
|
174
|
+
# Tailored BM25 queries per domain for better relevance
|
|
192
175
|
component = search_components(f"{args.name} {args.description}")
|
|
193
|
-
schema = search_schema_settings(args.description)
|
|
194
|
-
practices = search_best_practices(args.
|
|
176
|
+
schema = search_schema_settings(f"shopify section settings {args.description}")
|
|
177
|
+
practices = search_best_practices(f"shopify section {args.name} best practices")
|
|
195
178
|
design_recs = try_ui_ux_bridge(args.name + " " + args.description)
|
|
196
179
|
blocks = search_block_patterns(f"{args.name} {args.description}")
|
|
197
180
|
profile = search_settings_profile(args.name)
|
|
198
181
|
dna = search_theme_dna(args.name)
|
|
182
|
+
tokens = search_design_tokens(f"css typography spacing color {args.description}")
|
|
199
183
|
|
|
184
|
+
# Build refinement context as dedicated section (not appended to component)
|
|
185
|
+
refinement = ""
|
|
200
186
|
if args.refine:
|
|
201
187
|
refine_path = Path(args.refine)
|
|
202
188
|
try:
|
|
@@ -207,13 +193,18 @@ def main():
|
|
|
207
193
|
try:
|
|
208
194
|
from quality_gate import run_quality_gate
|
|
209
195
|
gate_result = run_quality_gate(args.refine)
|
|
210
|
-
|
|
211
|
-
|
|
196
|
+
refinement = (f"<refinement>\n## Existing Section (for refinement)\n"
|
|
197
|
+
f"```liquid\n{existing[:3000]}\n```\n\n"
|
|
198
|
+
f"## Quality Gate Results\n{gate_result}\n\n"
|
|
199
|
+
f"Please fix the issues identified above while preserving functionality.\n"
|
|
200
|
+
f"</refinement>")
|
|
212
201
|
except ImportError:
|
|
213
|
-
|
|
202
|
+
refinement = (f"<refinement>\n## Existing Section (for refinement)\n"
|
|
203
|
+
f"```liquid\n{existing[:3000]}\n```\n</refinement>")
|
|
214
204
|
|
|
215
205
|
context = assemble_context(args.name, args.description, theme, component, schema,
|
|
216
|
-
practices, design_recs, blocks, profile, dna
|
|
206
|
+
practices, design_recs, blocks, profile, dna,
|
|
207
|
+
design_tokens=tokens, refinement=refinement)
|
|
217
208
|
print(context)
|
|
218
209
|
|
|
219
210
|
|
|
@@ -1,41 +1,96 @@
|
|
|
1
|
-
|
|
1
|
+
<role>
|
|
2
|
+
You are an expert Shopify OS2.0 theme developer. Generate production-ready section .liquid files that follow Shopify best practices, are fully accessible, responsive, and theme-editor friendly.
|
|
3
|
+
</role>
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
{{ description }}
|
|
5
|
-
|
|
6
|
-
## Theme Context
|
|
7
|
-
{{ theme_profile_summary }}
|
|
8
|
-
|
|
9
|
-
## Closest Pattern Template
|
|
5
|
+
<reference_components>
|
|
10
6
|
{{ component_pattern_content }}
|
|
7
|
+
</reference_components>
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
<schema_types>
|
|
13
10
|
{{ schema_search_results }}
|
|
11
|
+
</schema_types>
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
{{ settings_profile_results }}
|
|
17
|
-
|
|
18
|
-
## Block Pattern References
|
|
13
|
+
<block_patterns>
|
|
19
14
|
{{ block_patterns_results }}
|
|
15
|
+
</block_patterns>
|
|
20
16
|
|
|
21
|
-
|
|
22
|
-
{{
|
|
17
|
+
<settings_profile>
|
|
18
|
+
{{ settings_profile_results }}
|
|
19
|
+
</settings_profile>
|
|
23
20
|
|
|
24
|
-
|
|
21
|
+
<design_tokens>
|
|
22
|
+
{{ design_tokens_results }}
|
|
23
|
+
</design_tokens>
|
|
24
|
+
|
|
25
|
+
<best_practices>
|
|
25
26
|
{{ best_practices_results }}
|
|
27
|
+
</best_practices>
|
|
28
|
+
|
|
29
|
+
<theme_context>
|
|
30
|
+
{{ theme_profile_summary }}
|
|
31
|
+
</theme_context>
|
|
32
|
+
|
|
33
|
+
<theme_dna>
|
|
34
|
+
{{ theme_dna_context }}
|
|
35
|
+
</theme_dna>
|
|
26
36
|
|
|
27
|
-
## Design Recommendations
|
|
28
37
|
{{ ui_ux_recommendations }}
|
|
29
38
|
|
|
30
|
-
|
|
39
|
+
<task>
|
|
40
|
+
<name>{{ name }}</name>
|
|
41
|
+
<description>{{ description }}</description>
|
|
42
|
+
</task>
|
|
43
|
+
|
|
44
|
+
<base_template>
|
|
31
45
|
{{ section_base_content }}
|
|
46
|
+
</base_template>
|
|
32
47
|
|
|
33
|
-
|
|
48
|
+
{{ refinement_context }}
|
|
49
|
+
|
|
50
|
+
<mandatory_settings>
|
|
51
|
+
CONDITIONAL RULES (apply based on section content):
|
|
52
|
+
- IF section has heading or subheading text settings:
|
|
53
|
+
-> MUST include font_picker for primary heading
|
|
54
|
+
-> MUST include heading_tag (type: select, options: h1/h2/h3/h4) for SEO
|
|
55
|
+
- ALWAYS include:
|
|
56
|
+
-> color_scheme (type: color_scheme) for theme-wide color customization
|
|
57
|
+
-> padding_top + padding_bottom (type: range, 0-100px, step 4)
|
|
58
|
+
-> All colors via CSS custom properties, NEVER hardcoded hex
|
|
59
|
+
- IF section has CTA button:
|
|
60
|
+
-> Button settings: button_label, button_link, button_style
|
|
61
|
+
</mandatory_settings>
|
|
62
|
+
|
|
63
|
+
<pre_output_checklist>
|
|
64
|
+
Before writing code, verify your plan includes ALL:
|
|
65
|
+
- color_scheme setting in schema
|
|
66
|
+
- font_picker for heading text (if section has headings)
|
|
67
|
+
- heading_tag select (h1/h2/h3/h4) (if section has headings)
|
|
68
|
+
- Padding/margin controls
|
|
69
|
+
- presets array with at least one preset
|
|
70
|
+
- @app block type
|
|
71
|
+
- CSS: color + font-size set directly on h1-h6, p, a selectors (not just parent containers)
|
|
72
|
+
- CSS: scoped via #section-{{ section.id }}
|
|
73
|
+
- Images: loading="lazy" except hero; hero uses fetchpriority="high"
|
|
74
|
+
- All block wrappers include {{ block.shopify_attributes }}
|
|
75
|
+
- No hardcoded hex colors in CSS
|
|
76
|
+
- Uses {% render %} not {% include %}
|
|
77
|
+
</pre_output_checklist>
|
|
78
|
+
|
|
79
|
+
<requirements>
|
|
34
80
|
- OS2.0 compliant (presets, @app blocks, block.shopify_attributes)
|
|
35
81
|
- Scoped CSS via #section-{{ section.id }}
|
|
36
|
-
- Text element specificity: ALWAYS set
|
|
82
|
+
- Text element specificity: ALWAYS set color and font-size directly on text element selectors (h1-h6, p, a), not just parent containers
|
|
37
83
|
- Responsive (mobile-first)
|
|
38
84
|
- Accessible (semantic HTML, aria-labels, alt text)
|
|
39
85
|
- Performance (lazy-load non-hero images, no Liquid in CSS/JS)
|
|
40
86
|
- Use {% render %} not {% include %}
|
|
41
87
|
- Include {{ block.shopify_attributes }} on ALL block wrappers
|
|
88
|
+
</requirements>
|
|
89
|
+
|
|
90
|
+
<output_format>
|
|
91
|
+
Output ONLY the raw .liquid file content.
|
|
92
|
+
Do not wrap in markdown code blocks.
|
|
93
|
+
Do not add explanations before or after.
|
|
94
|
+
Begin with the first line of the liquid file.
|
|
95
|
+
End with the {% endschema %} tag.
|
|
96
|
+
</output_format>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rebly-sections",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Shopify section AI coding skill installer for Claude Code and Antigravity Kit",
|
|
5
5
|
"author": "Rebly Sections",
|
|
6
6
|
"homepage": "https://github.com/rebly-sections/sections-ai#readme",
|