rebly-sections 1.6.0 → 1.6.1

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.
@@ -90,15 +90,24 @@ def check_has_responsive_visibility(content, schema_json):
90
90
 
91
91
 
92
92
  def check_has_section_dimensions(content, schema_json):
93
- """Check if section has width and height dimension controls."""
93
+ """Check if section has width and height dimension controls.
94
+ Height should use section_height select (vh options), NOT a px-only range.
95
+ """
94
96
  if not schema_json:
95
97
  return ("WARN", "No schema to check for section dimensions")
96
98
  settings = schema_json.get("settings", [])
97
99
  setting_ids = [s.get("id", "") for s in settings]
98
- required = ["section_width", "section_width_custom", "section_min_height"]
99
- missing = [r for r in required if r not in setting_ids]
100
- if missing:
101
- return ("WARN", f"Missing section dimension settings: {', '.join(missing)} — merchants can't control section size")
100
+ # Width: both required
101
+ for req in ["section_width", "section_width_custom"]:
102
+ if req not in setting_ids:
103
+ return ("WARN", f"Missing {req} — merchants can't control section width")
104
+ # Height: prefer section_height (vh select), accept section_min_height for legacy
105
+ has_vh = "section_height" in setting_ids
106
+ has_px = "section_min_height" in setting_ids
107
+ if not has_vh and not has_px:
108
+ return ("WARN", "Missing section height setting — add section_height select (auto/50vh/75vh/100vh/custom) for responsive control")
109
+ if has_px and not has_vh:
110
+ return ("WARN", "section_min_height (px) is not responsive — replace with section_height select (auto/50vh/75vh/100vh/custom) + section_height_custom range (step:20)")
102
111
  return ("PASS", "Section dimension settings present")
103
112
 
104
113
 
@@ -117,3 +126,58 @@ def check_has_font_size_controls(content, schema_json):
117
126
  if has_headings and not has_heading_size:
118
127
  return ("WARN", "Missing heading font size range — merchants can't adjust heading size")
119
128
  return ("PASS", "Font size controls present")
129
+
130
+
131
+ def check_no_overflow_clip_heading(content, schema_json):
132
+ """Heading selectors must not have max-height + overflow:hidden combo — causes text clipping."""
133
+ style_match = re.search(r'<style[^>]*>(.*?)</style>', content, re.DOTALL)
134
+ if not style_match:
135
+ return ("PASS", "No overflow clipping on heading — no style block")
136
+ css = style_match.group(1)
137
+ # Find CSS blocks whose selector contains 'heading'
138
+ heading_blocks = re.findall(r'[^}]*heading[^{]*\{[^}]+\}', css, re.DOTALL)
139
+ for block in heading_blocks:
140
+ has_max_height = bool(re.search(r'max-height\s*:', block))
141
+ has_overflow = bool(re.search(r'overflow\s*:\s*hidden', block))
142
+ if has_max_height and has_overflow:
143
+ return ("WARN", "Heading selector has max-height + overflow:hidden — will clip descenders. Remove max-height or use min-height instead.")
144
+ return ("PASS", "No heading overflow clipping detected")
145
+
146
+
147
+ def check_font_applied_directly(content, schema_json):
148
+ """font-family must be set directly on the heading selector, not only inherited from parent."""
149
+ if not schema_json:
150
+ return ("WARN", "No schema — cannot check font-family specificity")
151
+ settings = schema_json.get("settings", [])
152
+ has_font_picker = any(s.get("type") == "font_picker" for s in settings)
153
+ if not has_font_picker:
154
+ return ("PASS", "No font_picker — font-family specificity check skipped")
155
+ style_match = re.search(r'<style[^>]*>(.*?)</style>', content, re.DOTALL)
156
+ if not style_match:
157
+ return ("WARN", "font_picker found but no <style> block — font not applied")
158
+ css = style_match.group(1)
159
+ # Check that a heading-class selector directly declares font-family (not just parent section)
160
+ has_heading_font = bool(re.search(
161
+ r'\.rebly-[\w-]+__heading[\w\s,]*\{[^}]*font-family', css, re.DOTALL
162
+ ))
163
+ if not has_heading_font:
164
+ return ("WARN", "font-family not set directly on __heading selector — theme CSS may override. Declare font-family inside .rebly-{slug}__heading { } rule.")
165
+ return ("PASS", "font-family applied directly on heading selector")
166
+
167
+
168
+ def check_css_vars_in_gradients(content, schema_json):
169
+ """Gradient stops should use CSS custom properties, not hardcoded high opacity values."""
170
+ style_match = re.search(r'<style[^>]*>(.*?)</style>', content, re.DOTALL)
171
+ if not style_match:
172
+ return ("PASS", "No gradient opacity hardcode — no style block")
173
+ # Strip Liquid output tags before scanning
174
+ css_no_liquid = re.sub(r'\{\{.*?\}\}', '', style_match.group(1))
175
+ # Flag rgba() with literal opacity >= 0.6 that appears inside a gradient (linear/radial)
176
+ gradient_blocks = re.findall(r'(?:linear|radial)-gradient\([^;]+\)', css_no_liquid, re.DOTALL)
177
+ hardcoded = []
178
+ for block in gradient_blocks:
179
+ hits = re.findall(r'rgba\([^)]+,\s*(0\.[6-9]\d*|1\.0?)\)', block)
180
+ hardcoded.extend(hits)
181
+ if hardcoded:
182
+ return ("WARN", f"Gradient stops with hardcoded high opacity {hardcoded[:3]} — use calc(var(--overlay-opacity) * N) so merchant slider controls the overlay")
183
+ return ("PASS", "Gradient stops use CSS variables or acceptable opacity values")
@@ -34,6 +34,7 @@ QUALITY_CHECKS = [
34
34
  ("has_section_tag", "Uses <section> HTML element"),
35
35
  ("images_lazy_load", "Non-hero images use loading='lazy'"),
36
36
  ("settings_have_labels", "All settings have label property"),
37
+ ("range_steps_valid", "Range settings have at most 101 steps (Shopify limit)"),
37
38
  ("text_elements_explicit_color", "Text elements have explicit color in CSS"),
38
39
  ("layout_responsive_breakpoints", "Responsive breakpoints (749px/989px) present"),
39
40
  ("layout_reduced_motion", "prefers-reduced-motion media query present"),
@@ -50,6 +51,9 @@ ADVISORY_CHECKS = [
50
51
  ("has_section_dimensions", "Section dimension settings present"),
51
52
  ("naming_rebly_prefix", "Schema name uses Rebly: prefix"),
52
53
  ("naming_bem_classes", "CSS classes follow BEM rebly-{component} convention"),
54
+ ("no_overflow_clip_heading", "Heading has no max-height + overflow:hidden clipping"),
55
+ ("font_applied_directly", "font-family set directly on heading selector"),
56
+ ("css_vars_in_gradients", "Gradient stops use CSS vars, not hardcoded opacity"),
53
57
  ]
54
58
 
55
59
 
@@ -145,6 +149,43 @@ def check_settings_have_labels(content):
145
149
  return True
146
150
 
147
151
 
152
+ def check_range_steps_valid(content):
153
+ """Shopify hard limit: range settings must have at most 101 steps (max-min)/step <= 100.
154
+ Violation causes upload failure: 'step invalid. Range settings must have at most 101 steps'
155
+ """
156
+ schema = extract_schema_json(content)
157
+ if not schema:
158
+ return True # Can't check without valid schema
159
+ violations = []
160
+ all_settings = list(schema.get("settings", []))
161
+ for block in schema.get("blocks", []):
162
+ all_settings += block.get("settings", [])
163
+ for s in all_settings:
164
+ if s.get("type") != "range":
165
+ continue
166
+ try:
167
+ mn = float(s.get("min", 0))
168
+ mx = float(s.get("max", 100))
169
+ st = float(s.get("step", 1))
170
+ if st <= 0:
171
+ violations.append(f"{s.get('id','?')}: step={st} must be > 0")
172
+ continue
173
+ num_steps = (mx - mn) / st
174
+ if num_steps > 100:
175
+ violations.append(
176
+ f"{s.get('id','?')}: min={mn}, max={mx}, step={st} → {num_steps:.0f} steps (max 100)"
177
+ )
178
+ except (TypeError, ValueError):
179
+ pass
180
+ if violations:
181
+ # Print details to help developer fix it
182
+ import sys
183
+ for v in violations:
184
+ print(f" ✗ Range steps violation: {v}", file=sys.stderr)
185
+ return False
186
+ return True
187
+
188
+
148
189
  def check_text_elements_explicit_color(content):
149
190
  """Verify text elements within section scope have explicit color in CSS."""
150
191
  style_match = re.search(r'<style[^>]*>(.*?)</style>', content, re.DOTALL)
@@ -228,6 +269,7 @@ CHECK_FUNCTIONS = {
228
269
  "has_section_tag": check_has_section_tag,
229
270
  "images_lazy_load": check_images_lazy_load,
230
271
  "settings_have_labels": check_settings_have_labels,
272
+ "range_steps_valid": check_range_steps_valid,
231
273
  "text_elements_explicit_color": check_text_elements_explicit_color,
232
274
  "layout_responsive_breakpoints": check_layout_responsive_breakpoints,
233
275
  "layout_reduced_motion": check_layout_reduced_motion,
@@ -274,6 +316,9 @@ def run_quality_gate(filepath):
274
316
  "has_font_size_controls": _checks_mod.check_has_font_size_controls,
275
317
  "has_responsive_visibility": _checks_mod.check_has_responsive_visibility,
276
318
  "has_section_dimensions": _checks_mod.check_has_section_dimensions,
319
+ "no_overflow_clip_heading": _checks_mod.check_no_overflow_clip_heading,
320
+ "font_applied_directly": _checks_mod.check_font_applied_directly,
321
+ "css_vars_in_gradients": _checks_mod.check_css_vars_in_gradients,
277
322
  }
278
323
  # Built-in naming convention advisory checks (always available)
279
324
  advisory_fns.update({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rebly-sections",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
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",