rebly-sections 1.3.1 → 1.4.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.
@@ -1,85 +1,253 @@
1
- {%- comment -%} rebly-sections: Testimonials Slider | Category: social-proof | OS2.0 {%- endcomment -%}
1
+ {%- comment -%} rebly-sections: Testimonials Slider | Category: social-proof | OS2.0 | CSS scroll-snap carousel {%- endcomment -%}
2
+ {%- assign font = section.settings.heading_font -%}
3
+ {%- assign heading_tag = section.settings.heading_tag -%}
4
+ {%- assign block_count = section.blocks | where: 'type', 'testimonial' | size -%}
2
5
 
3
- <section id="section-{{ section.id }}" class="testimonials-slider" style="--bg: {{ section.settings.background_color }};">
4
- <div class="testimonials-slider__container">
6
+ {{ font | font_face: font_display: 'swap' }}
7
+
8
+ <section id="section-{{ section.id }}" class="testimonials-slider color-{{ section.settings.color_scheme }}">
9
+ <div class="testimonials-slider__container" style="
10
+ --pt: {{ section.settings.padding_top }}px;
11
+ --pb: {{ section.settings.padding_bottom }}px;
12
+ --heading-size: {{ section.settings.heading_font_size }}px;
13
+ --heading-font: {{ font.family }}, {{ font.fallback_families }};
14
+ --heading-weight: {{ font.weight }};
15
+ --slide-count: {{ block_count }};
16
+ ">
5
17
 
6
18
  {%- if section.settings.heading != blank -%}
7
- <h2 class="testimonials-slider__heading">{{ section.settings.heading | escape }}</h2>
19
+ <{{ heading_tag }} class="testimonials-slider__heading">{{ section.settings.heading | escape }}</{{ heading_tag }}>
8
20
  {%- endif -%}
9
21
 
10
- <div class="testimonials-slider__track">
11
- {%- for block in section.blocks -%}
12
- {%- if block.type == 'testimonial' -%}
13
- <div class="testimonials-slider__slide" {{ block.shopify_attributes }}>
14
- <blockquote class="testimonials-slider__quote">
15
- {%- if block.settings.quote != blank -%}
16
- <p class="testimonials-slider__text">{{ block.settings.quote | escape }}</p>
22
+ {%- comment -%} CSS scroll-snap carousel — no JS library, GPU-only scroll {%- endcomment -%}
23
+ <div class="testimonials-slider__viewport" id="ts-viewport-{{ section.id }}">
24
+ <div class="testimonials-slider__track">
25
+ {%- for block in section.blocks -%}
26
+ {%- if block.type == 'testimonial' -%}
27
+ <article class="testimonials-slider__slide" {{ block.shopify_attributes }} id="ts-slide-{{ section.id }}-{{ forloop.index }}">
28
+ {%- comment -%} Decorative oversized quote mark {%- endcomment -%}
29
+ <span class="testimonials-slider__quote-mark" aria-hidden="true">&#8220;</span>
30
+
31
+ {%- if block.settings.rating > 0 -%}
32
+ <div class="testimonials-slider__stars" aria-label="{{ block.settings.rating }} out of 5 stars">
33
+ {%- for i in (1..5) -%}
34
+ <span class="testimonials-slider__star{% if i <= block.settings.rating %} testimonials-slider__star--filled{% endif %}" aria-hidden="true">&#9733;</span>
35
+ {%- endfor -%}
36
+ </div>
17
37
  {%- endif -%}
18
- <footer class="testimonials-slider__author">
19
- {%- if block.settings.avatar != blank -%}
20
- <div class="testimonials-slider__avatar">
21
- {{- block.settings.avatar | image_url: width: 80 | image_tag: loading: 'lazy', alt: block.settings.author -}}
22
- </div>
38
+
39
+ <blockquote class="testimonials-slider__blockquote">
40
+ {%- if block.settings.quote != blank -%}
41
+ <p class="testimonials-slider__text">{{ block.settings.quote | escape }}</p>
23
42
  {%- endif -%}
24
- <div>
25
- {%- if block.settings.author != blank -%}
26
- <cite class="testimonials-slider__name">{{ block.settings.author | escape }}</cite>
27
- {%- endif -%}
28
- {%- if block.settings.role != blank -%}
29
- <span class="testimonials-slider__role">{{ block.settings.role | escape }}</span>
43
+ <footer class="testimonials-slider__author">
44
+ {%- if block.settings.avatar != blank -%}
45
+ <div class="testimonials-slider__avatar">
46
+ <img
47
+ src="{{ block.settings.avatar | image_url: width: 96 }}"
48
+ srcset="{{ block.settings.avatar | image_url: width: 96 }} 1x, {{ block.settings.avatar | image_url: width: 192 }} 2x"
49
+ loading="lazy"
50
+ alt="{{ block.settings.author | escape }}"
51
+ width="48" height="48"
52
+ >
53
+ </div>
54
+ {%- else -%}
55
+ <div class="testimonials-slider__avatar testimonials-slider__avatar--initials" aria-hidden="true">
56
+ {{ block.settings.author | slice: 0 | upcase }}
57
+ </div>
30
58
  {%- endif -%}
31
- </div>
32
- </footer>
33
- </blockquote>
34
- </div>
35
- {%- endif -%}
36
- {%- endfor -%}
59
+ <div class="testimonials-slider__meta">
60
+ {%- if block.settings.author != blank -%}
61
+ <cite class="testimonials-slider__name">{{ block.settings.author | escape }}</cite>
62
+ {%- endif -%}
63
+ {%- if block.settings.role != blank -%}
64
+ <span class="testimonials-slider__role">{{ block.settings.role | escape }}</span>
65
+ {%- endif -%}
66
+ </div>
67
+ </footer>
68
+ </blockquote>
69
+ </article>
70
+ {%- endif -%}
71
+ {%- endfor -%}
72
+ </div>
37
73
  </div>
38
74
 
75
+ {%- comment -%} Navigation dots — JS-driven active state, CSS-only styling {%- endcomment -%}
76
+ {%- if block_count > 1 -%}
77
+ <nav class="testimonials-slider__dots" aria-label="Testimonial navigation">
78
+ {%- for block in section.blocks -%}
79
+ {%- if block.type == 'testimonial' -%}
80
+ <button
81
+ class="testimonials-slider__dot{% if forloop.first %} is-active{% endif %}"
82
+ data-index="{{ forloop.index0 }}"
83
+ aria-label="Go to testimonial {{ forloop.index }}"
84
+ ></button>
85
+ {%- endif -%}
86
+ {%- endfor -%}
87
+ </nav>
88
+ {%- endif -%}
89
+
39
90
  </div>
40
91
  </section>
41
92
 
42
93
  <style>
43
- #section-{{ section.id }} { background: var(--bg, #f9f9f9); }
44
- #section-{{ section.id }} .testimonials-slider__container { max-width: 1100px; margin: 0 auto; padding: 4rem 2rem; }
45
- #section-{{ section.id }} .testimonials-slider__heading { text-align: center; font-size: 2rem; margin: 0 0 2.5rem; color: inherit; }
94
+ /* ── Layout ── */
95
+ #section-{{ section.id }} .testimonials-slider__container {
96
+ max-width: 1100px; margin: 0 auto;
97
+ padding: var(--pt, 80px) 2rem var(--pb, 80px);
98
+ }
99
+ #section-{{ section.id }} .testimonials-slider__heading {
100
+ font-family: var(--heading-font); font-weight: var(--heading-weight);
101
+ font-size: clamp(1.75rem, 3vw, var(--heading-size, 2.5rem));
102
+ text-align: center; margin: 0 0 3rem; color: rgb(var(--color-foreground));
103
+ }
104
+ /* ── Scroll-snap viewport ── */
105
+ #section-{{ section.id }} .testimonials-slider__viewport {
106
+ overflow: hidden; /* JS scrolls underlying track */
107
+ }
46
108
  #section-{{ section.id }} .testimonials-slider__track {
47
- display: flex; gap: 1.5rem; overflow-x: auto;
48
- scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;
49
- scrollbar-width: none; padding-bottom: 1rem;
109
+ display: flex; gap: 1.5rem;
110
+ /* scroll-snap on the track so CSS-only fallback works too */
111
+ overflow-x: auto; scroll-snap-type: x mandatory;
112
+ -webkit-overflow-scrolling: touch; scrollbar-width: none;
113
+ scroll-behavior: smooth;
50
114
  }
51
115
  #section-{{ section.id }} .testimonials-slider__track::-webkit-scrollbar { display: none; }
116
+ /* ── Slide cards ── */
52
117
  #section-{{ section.id }} .testimonials-slider__slide {
53
118
  flex: 0 0 calc(33.333% - 1rem); min-width: 280px;
54
- scroll-snap-align: start; background: #fff;
55
- border-radius: 8px; padding: 2rem; box-shadow: 0 2px 8px rgba(0,0,0,0.08);
119
+ scroll-snap-align: start;
120
+ background: rgb(var(--color-background));
121
+ border: 1px solid rgba(var(--color-foreground), 0.1);
122
+ border-radius: 16px; padding: 2.25rem; position: relative;
123
+ overflow: hidden;
124
+ transition: transform 0.35s cubic-bezier(0.22,1,0.36,1), box-shadow 0.35s ease;
125
+ will-change: transform;
126
+ }
127
+ #section-{{ section.id }} .testimonials-slider__slide:hover {
128
+ transform: translateY(-6px);
129
+ box-shadow: 0 20px 60px rgba(var(--color-foreground), 0.1);
130
+ }
131
+ /* ── Decorative quote mark ── */
132
+ #section-{{ section.id }} .testimonials-slider__quote-mark {
133
+ position: absolute; top: -0.25rem; right: 1.25rem;
134
+ font-size: 6rem; line-height: 1; opacity: 0.08;
135
+ color: rgb(var(--color-foreground)); pointer-events: none;
136
+ font-family: Georgia, serif;
137
+ }
138
+ /* ── Stars ── */
139
+ #section-{{ section.id }} .testimonials-slider__stars { margin-bottom: 1rem; }
140
+ #section-{{ section.id }} .testimonials-slider__star { font-size: 1rem; opacity: 0.25; color: rgb(var(--color-foreground)); }
141
+ #section-{{ section.id }} .testimonials-slider__star--filled { opacity: 1; color: #f5a623; }
142
+ /* ── Quote text ── */
143
+ #section-{{ section.id }} .testimonials-slider__blockquote { margin: 0; }
144
+ #section-{{ section.id }} .testimonials-slider__text {
145
+ font-size: 1rem; line-height: 1.75; margin: 0 0 1.75rem;
146
+ font-style: italic; color: rgb(var(--color-foreground)); opacity: 0.85;
147
+ }
148
+ /* ── Author ── */
149
+ #section-{{ section.id }} .testimonials-slider__author { display: flex; align-items: center; gap: 0.875rem; }
150
+ #section-{{ section.id }} .testimonials-slider__avatar {
151
+ width: 48px; height: 48px; border-radius: 50%; overflow: hidden;
152
+ flex-shrink: 0; background: rgba(var(--color-button), 0.15);
56
153
  }
57
- #section-{{ section.id }} .testimonials-slider__text { font-size: 1.05rem; line-height: 1.7; margin: 0 0 1.5rem; font-style: italic; color: inherit; }
58
- #section-{{ section.id }} .testimonials-slider__author { display: flex; align-items: center; gap: 0.75rem; }
59
- #section-{{ section.id }} .testimonials-slider__avatar { width: 48px; height: 48px; border-radius: 50%; overflow: hidden; flex-shrink: 0; }
60
154
  #section-{{ section.id }} .testimonials-slider__avatar img { width: 100%; height: 100%; object-fit: cover; }
61
- #section-{{ section.id }} .testimonials-slider__name { display: block; font-weight: 600; font-style: normal; color: inherit; }
62
- #section-{{ section.id }} .testimonials-slider__role { font-size: 0.875rem; opacity: 0.65; color: inherit; }
63
- @media (max-width: 749px) { #section-{{ section.id }} .testimonials-slider__slide { flex: 0 0 85%; } }
155
+ #section-{{ section.id }} .testimonials-slider__avatar--initials {
156
+ display: grid; place-items: center;
157
+ font-weight: 700; font-size: 1.1rem; color: rgb(var(--color-button));
158
+ }
159
+ #section-{{ section.id }} .testimonials-slider__meta { display: flex; flex-direction: column; gap: 0.15rem; }
160
+ #section-{{ section.id }} .testimonials-slider__name {
161
+ font-style: normal; font-weight: 700; font-size: 0.9375rem;
162
+ color: rgb(var(--color-foreground));
163
+ }
164
+ #section-{{ section.id }} .testimonials-slider__role { font-size: 0.8125rem; opacity: 0.6; color: rgb(var(--color-foreground)); }
165
+ /* ── Navigation dots ── */
166
+ #section-{{ section.id }} .testimonials-slider__dots {
167
+ display: flex; justify-content: center; gap: 0.5rem; margin-top: 2rem;
168
+ }
169
+ #section-{{ section.id }} .testimonials-slider__dot {
170
+ width: 8px; height: 8px; border-radius: 50%; border: none; cursor: pointer;
171
+ background: rgba(var(--color-foreground), 0.2);
172
+ transition: background 0.3s ease, transform 0.3s ease;
173
+ padding: 0;
174
+ }
175
+ #section-{{ section.id }} .testimonials-slider__dot.is-active {
176
+ background: rgb(var(--color-button)); transform: scale(1.4);
177
+ }
178
+ /* ── Reduced motion ── */
179
+ @media (prefers-reduced-motion: reduce) {
180
+ #section-{{ section.id }} .testimonials-slider__track { scroll-behavior: auto; }
181
+ #section-{{ section.id }} .testimonials-slider__slide { transition: none; }
182
+ #section-{{ section.id }} .testimonials-slider__dot { transition: none; }
183
+ }
184
+ /* ── Responsive ── */
185
+ @media (max-width: 989px) {
186
+ #section-{{ section.id }} .testimonials-slider__slide { flex: 0 0 calc(50% - 0.75rem); }
187
+ }
188
+ @media (max-width: 749px) {
189
+ #section-{{ section.id }} .testimonials-slider__slide { flex: 0 0 88%; }
190
+ #section-{{ section.id }} .testimonials-slider__container { padding-left: 1.25rem; padding-right: 1.25rem; }
191
+ }
64
192
  </style>
65
193
 
194
+ <script>
195
+ /* Dot-nav + keyboard navigation for scroll-snap carousel */
196
+ (function() {
197
+ var viewport = document.getElementById('ts-viewport-{{ section.id }}');
198
+ var track = viewport && viewport.querySelector('.testimonials-slider__track');
199
+ var dots = document.querySelectorAll('#section-{{ section.id }} .testimonials-slider__dot');
200
+ var slides = document.querySelectorAll('#section-{{ section.id }} .testimonials-slider__slide');
201
+ if (!track || !slides.length) return;
202
+
203
+ function scrollToSlide(index) {
204
+ var slide = slides[index];
205
+ if (!slide) return;
206
+ track.scrollTo({ left: slide.offsetLeft, behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth' });
207
+ }
208
+
209
+ dots.forEach(function(dot) {
210
+ dot.addEventListener('click', function() { scrollToSlide(parseInt(this.dataset.index, 10)); });
211
+ });
212
+
213
+ /* Update active dot on scroll */
214
+ var io = new IntersectionObserver(function(entries) {
215
+ entries.forEach(function(entry) {
216
+ if (!entry.isIntersecting) return;
217
+ var idx = Array.prototype.indexOf.call(slides, entry.target);
218
+ dots.forEach(function(d, i) { d.classList.toggle('is-active', i === idx); });
219
+ });
220
+ }, { root: track, threshold: 0.6 });
221
+ slides.forEach(function(s) { io.observe(s); });
222
+ })();
223
+ </script>
224
+
66
225
  {% schema %}
67
226
  {
68
227
  "name": "Testimonials Slider",
69
228
  "tag": "section",
70
229
  "class": "section-testimonials-slider",
71
230
  "settings": [
231
+ { "type": "color_scheme", "id": "color_scheme", "label": "Color Scheme", "default": "scheme-1" },
232
+ { "type": "font_picker", "id": "heading_font", "label": "Heading Font", "default": "helvetica_n4" },
233
+ { "type": "range", "id": "heading_font_size", "label": "Heading Size (px)", "min": 20, "max": 56, "step": 2, "default": 40 },
234
+ { "type": "select", "id": "heading_tag", "label": "Heading Tag", "options": [
235
+ { "value": "h1", "label": "H1" }, { "value": "h2", "label": "H2" },
236
+ { "value": "h3", "label": "H3" }, { "value": "h4", "label": "H4" }
237
+ ], "default": "h2" },
72
238
  { "type": "text", "id": "heading", "label": "Heading", "default": "What our customers say" },
73
- { "type": "color_background", "id": "background_color", "label": "Background Color", "default": "#f9f9f9" }
239
+ { "type": "range", "id": "padding_top", "label": "Padding Top (px)", "min": 0, "max": 160, "step": 8, "default": 80 },
240
+ { "type": "range", "id": "padding_bottom", "label": "Padding Bottom (px)", "min": 0, "max": 160, "step": 8, "default": 80 }
74
241
  ],
75
242
  "blocks": [
76
243
  {
77
244
  "type": "testimonial",
78
245
  "name": "Testimonial",
79
246
  "settings": [
80
- { "type": "textarea", "id": "quote", "label": "Quote", "default": "This product changed my life. Absolutely love it!" },
247
+ { "type": "range", "id": "rating", "label": "Star Rating", "min": 0, "max": 5, "step": 1, "default": 5 },
248
+ { "type": "textarea", "id": "quote", "label": "Quote", "default": "This product completely transformed how I work. Absolutely worth every penny." },
81
249
  { "type": "text", "id": "author", "label": "Author Name", "default": "Happy Customer" },
82
- { "type": "text", "id": "role", "label": "Role / Title" },
250
+ { "type": "text", "id": "role", "label": "Role / Title", "default": "Verified Buyer" },
83
251
  { "type": "image_picker", "id": "avatar", "label": "Avatar Image" }
84
252
  ]
85
253
  },
@@ -87,7 +255,9 @@
87
255
  ],
88
256
  "presets": [{
89
257
  "name": "Testimonials Slider",
90
- "blocks": [{ "type": "testimonial" }, { "type": "testimonial" }, { "type": "testimonial" }]
258
+ "blocks": [
259
+ { "type": "testimonial" }, { "type": "testimonial" }, { "type": "testimonial" }
260
+ ]
91
261
  }]
92
262
  }
93
263
  {% endschema %}
@@ -91,3 +91,45 @@ No,Token Name,Category,Value,CSS Variable,Keywords,Usage,Notes
91
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
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
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
94
+ 93,color-foreground,color,var(--color-scheme-foreground),--color-foreground,"color foreground text primary scheme","color: var(--color-foreground)",Primary text color mapped to Shopify color_scheme foreground
95
+ 94,color-background-scheme,color,var(--color-scheme-background),--color-background-scheme,"color background surface scheme page","background: var(--color-background-scheme)",Page/section background mapped to Shopify color_scheme background
96
+ 95,color-primary-scheme,color,var(--color-scheme-primary),--color-primary-scheme,"color primary brand accent cta button scheme","background: var(--color-primary-scheme); color: var(--color-primary-scheme)",Brand primary / CTA buttons mapped to Shopify color_scheme primary
97
+ 96,color-secondary-scheme,color,var(--color-scheme-secondary),--color-secondary-scheme,"color secondary scheme complement","color: var(--color-secondary-scheme)",Secondary elements mapped to Shopify color_scheme secondary
98
+ 97,color-primary-text,color,var(--color-scheme-primary-text),--color-primary-text,"color text contrast primary on-primary scheme","color: var(--color-primary-text)",Text on primary/brand background for contrast
99
+ 98,color-overlay,color,"rgba(var(--color-foreground-rgb), 0.5)",--color-overlay,"color overlay modal hero backdrop dim","background: var(--color-overlay)",Semi-transparent overlay for modals and hero backgrounds
100
+ 99,color-border-subtle,color,"rgba(var(--color-foreground-rgb), 0.1)",--color-border-subtle,"color border divider line subtle thin","border-color: var(--color-border-subtle)",Subtle borders and dividers using foreground RGB
101
+ 100,color-surface-elevated,color,"color-mix(in srgb, var(--color-scheme-background) 95%, var(--color-scheme-foreground))",--color-surface-elevated,"color surface card elevated panel raised","background: var(--color-surface-elevated)",Elevated surfaces / cards; slightly different from page background
102
+ 101,color-muted,color,"rgba(var(--color-foreground-rgb), 0.6)",--color-muted,"color muted secondary text subdued hint","color: var(--color-muted)",Muted/secondary text color at 60% opacity
103
+ 102,color-success-semantic,color,#16a34a,--color-success-semantic,"color success green status positive valid","color: var(--color-success-semantic); background: var(--color-success-semantic)",Success states; accessible green
104
+ 103,color-warning-semantic,color,#ca8a04,--color-warning-semantic,"color warning yellow status caution alert","color: var(--color-warning-semantic); background: var(--color-warning-semantic)",Warning states; accessible amber
105
+ 104,color-error-semantic,color,#dc2626,--color-error-semantic,"color error red status danger invalid negative","color: var(--color-error-semantic); border-color: var(--color-error-semantic)",Error states; accessible red
106
+ 105,color-focus-ring-semantic,color,var(--color-scheme-primary),--color-focus-ring-semantic,"color focus ring accessibility keyboard outline","outline-color: var(--color-focus-ring-semantic)",Focus outline for keyboard navigation using brand primary
107
+ 106,font-size-xs-fluid,typography,"clamp(0.7rem, 0.65rem + 0.25vw, 0.75rem)",--font-size-xs-fluid,"font size xs caption small fluid responsive clamp","font-size: var(--font-size-xs-fluid)",Fluid extra small text; captions and fine print; scales with viewport
108
+ 107,font-size-sm-fluid,typography,"clamp(0.8rem, 0.75rem + 0.25vw, 0.875rem)",--font-size-sm-fluid,"font size sm label small fluid responsive clamp","font-size: var(--font-size-sm-fluid)",Fluid small text; labels and metadata; scales with viewport
109
+ 108,font-size-base-fluid,typography,"clamp(0.9rem, 0.85rem + 0.25vw, 1rem)",--font-size-base-fluid,"font size base body paragraph fluid responsive clamp","font-size: var(--font-size-base-fluid)",Fluid body text; responsive base size
110
+ 109,font-size-md-fluid,typography,"clamp(1rem, 0.9rem + 0.5vw, 1.125rem)",--font-size-md-fluid,"font size md medium fluid responsive clamp lead","font-size: var(--font-size-md-fluid)",Fluid medium text; slightly larger body and lead text
111
+ 110,font-size-lg-fluid,typography,"clamp(1.1rem, 1rem + 0.5vw, 1.25rem)",--font-size-lg-fluid,"font size lg large fluid responsive clamp heading","font-size: var(--font-size-lg-fluid)",Fluid large text; small headings and prominent labels
112
+ 111,font-size-xl-fluid,typography,"clamp(1.25rem, 1rem + 1.25vw, 1.75rem)",--font-size-xl-fluid,"font size xl heading h4 fluid responsive clamp","font-size: var(--font-size-xl-fluid)",Fluid H4 headings; scales from 1.25rem to 1.75rem
113
+ 112,font-size-2xl-fluid,typography,"clamp(1.5rem, 1.2rem + 1.5vw, 2.25rem)",--font-size-2xl-fluid,"font size 2xl heading h3 fluid responsive clamp","font-size: var(--font-size-2xl-fluid)",Fluid H3 headings; scales from 1.5rem to 2.25rem
114
+ 113,font-size-3xl-fluid,typography,"clamp(1.875rem, 1.5rem + 1.875vw, 3rem)",--font-size-3xl-fluid,"font size 3xl heading h2 fluid responsive clamp","font-size: var(--font-size-3xl-fluid)",Fluid H2 headings; scales from 1.875rem to 3rem
115
+ 114,font-size-4xl-fluid,typography,"clamp(2.25rem, 1.75rem + 2.5vw, 4rem)",--font-size-4xl-fluid,"font size 4xl heading h1 fluid responsive clamp hero","font-size: var(--font-size-4xl-fluid)",Fluid H1 headings; scales from 2.25rem to 4rem for maximum impact
116
+ 115,font-weight-normal-scale,typography,400,--font-weight-normal-scale,"font weight normal regular 400 body","font-weight: var(--font-weight-normal-scale)",Normal weight 400 for body text; part of semantic weight scale
117
+ 116,font-weight-medium-scale,typography,500,--font-weight-medium-scale,"font weight medium 500 emphasis label","font-weight: var(--font-weight-medium-scale)",Medium weight 500 for UI labels and moderate emphasis
118
+ 117,font-weight-bold-scale,typography,700,--font-weight-bold-scale,"font weight bold 700 heading strong","font-weight: var(--font-weight-bold-scale)",Bold weight 700 for headings and strong emphasis
119
+ 118,duration-instant,animation,50ms,--duration-instant,"duration instant fast micro feedback toggle checkbox","transition-duration: var(--duration-instant)",Instant feedback transitions for checkboxes and toggles
120
+ 119,duration-fast,animation,150ms,--duration-fast,"duration fast hover focus interactive quick","transition-duration: var(--duration-fast)",Fast transitions for hover and focus states
121
+ 120,duration-normal,animation,300ms,--duration-normal,"duration normal standard expand reveal transition","transition-duration: var(--duration-normal)",Standard animation duration for expand and reveal effects
122
+ 121,duration-slow,animation,500ms,--duration-slow,"duration slow complex modal slide panel","transition-duration: var(--duration-slow)",Slower transitions for complex elements like modals and slide panels
123
+ 122,duration-slower,animation,800ms,--duration-slower,"duration slower entrance scroll reveal animation","transition-duration: var(--duration-slower)",Entrance animations for scroll-triggered reveal effects
124
+ 123,ease-out,animation,"cubic-bezier(0.4, 0, 0.2, 1)",--ease-out,"easing ease out decelerate standard material","transition-timing-function: var(--ease-out)",Standard deceleration easing; elements entering the screen
125
+ 124,ease-in-out,animation,"cubic-bezier(0.4, 0, 0.2, 1)",--ease-in-out,"easing ease in out symmetric balanced","transition-timing-function: var(--ease-in-out)",Symmetric easing for elements moving across the screen
126
+ 125,ease-spring,animation,"cubic-bezier(0.34, 1.56, 0.64, 1)",--ease-spring,"easing spring bounce overshoot playful","transition-timing-function: var(--ease-spring)",Bouncy spring easing with slight overshoot for playful interactions
127
+ 126,shadow-sm-semantic,shadow,"0 1px 2px rgba(0,0,0,0.05)",--shadow-sm-semantic,"shadow small subtle elevation minimal","box-shadow: var(--shadow-sm-semantic)",Minimal shadow for slight elevation; form inputs and badges
128
+ 127,shadow-md-semantic,shadow,"0 4px 6px -1px rgba(0,0,0,0.1)",--shadow-md-semantic,"shadow medium card elevation default","box-shadow: var(--shadow-md-semantic)",Card-level elevation shadow; standard depth for content cards
129
+ 128,shadow-lg-semantic,shadow,"0 10px 15px -3px rgba(0,0,0,0.1)",--shadow-lg-semantic,"shadow large dropdown modal overlay floating","box-shadow: var(--shadow-lg-semantic)",Dropdown and modal elevation; floating UI elements
130
+ 129,shadow-xl-semantic,shadow,"0 20px 25px -5px rgba(0,0,0,0.1)",--shadow-xl-semantic,"shadow extra large hero overlay dramatic high","box-shadow: var(--shadow-xl-semantic)",Hero and overlay elevation; maximum depth for prominent elements
131
+ 130,shadow-glow,shadow,"0 0 15px rgba(var(--color-primary-rgb),0.3)",--shadow-glow,"shadow glow primary cta attention highlight brand","box-shadow: var(--shadow-glow)",Glow effect for CTA buttons using brand primary color RGB
132
+ 131,breakpoint-sm-shopify,breakpoint,640px,--breakpoint-sm-shopify,"breakpoint small mobile 640px sm","@media (min-width: 640px) { }",Small breakpoint; mobile landscape and small mobile devices
133
+ 132,breakpoint-md-shopify,breakpoint,749px,--breakpoint-md-shopify,"breakpoint medium tablet 749px shopify mobile","@media (min-width: 749px) { }",Shopify Dawn mobile/tablet breakpoint; official Shopify threshold
134
+ 133,breakpoint-lg-shopify,breakpoint,990px,--breakpoint-lg-shopify,"breakpoint large desktop 990px shopify desktop","@media (min-width: 990px) { }",Shopify Dawn desktop breakpoint; official Shopify desktop threshold
135
+ 134,breakpoint-xl-shopify,breakpoint,1200px,--breakpoint-xl-shopify,"breakpoint extra large wide 1200px desktop wide","@media (min-width: 1200px) { }",Wide desktop breakpoint; large monitor and wide viewport layouts
@@ -52,6 +52,11 @@ CSV_CONFIG = {
52
52
  "file": "settings-profiles.csv",
53
53
  "search_cols": ["Section Type", "Setting ID", "Notes"],
54
54
  "output_cols": ["Section Type", "Setting ID", "Setting Type", "Label", "Default", "Required", "Notes", "Frequency Rank"]
55
+ },
56
+ "animations": {
57
+ "file": "animations.csv",
58
+ "search_cols": ["name", "use_case", "keywords", "category"],
59
+ "output_cols": ["name", "category", "css_keyframes", "easing", "duration", "trigger", "reduced_motion_fallback", "use_case"]
55
60
  }
56
61
  }
57
62
 
@@ -164,7 +169,8 @@ domain_keywords = {
164
169
  "themes": ["theme", "dawn", "impulse", "impact", "craft", "detect", "dna", "theme profile"],
165
170
  "practices": ["best practice", "performance", "accessibility", "seo", "os2", "lazy load", "responsive", "a11y"],
166
171
  "block-patterns": ["block", "blocks", "block type", "block config", "block pattern", "block setting"],
167
- "settings-profiles": ["profile", "settings profile", "recommended settings", "section settings", "setting profile"]
172
+ "settings-profiles": ["profile", "settings profile", "recommended settings", "section settings", "setting profile"],
173
+ "animations": ["animation", "animate", "motion", "transition", "easing", "keyframe", "hover", "scroll", "entrance", "parallax", "fade", "slide", "reveal"]
168
174
  }
169
175
 
170
176
 
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env python3
2
+ """Design quality advisory checks — GPU animations, motion, responsive, etc.
3
+
4
+ Each function returns (passed: bool, message: str).
5
+ DESIGN_CHECKS is a list of (name, check_fn) tuples consumed by quality-gate.py.
6
+ """
7
+
8
+ import re
9
+
10
+
11
+ def check_animation_performance(content):
12
+ """ADVISORY: Animations should use GPU-accelerated properties only."""
13
+ bad_props = re.findall(
14
+ r'transition[^;]*(?:top|left|right|bottom|width|height|margin|padding)[^;]*;',
15
+ content
16
+ )
17
+ if not bad_props:
18
+ return True, "GPU-accelerated animations (transform/opacity)"
19
+ return False, f"non-GPU animated properties found: use transform instead of top/left/width/height"
20
+
21
+
22
+ def check_reduced_motion(content):
23
+ """ADVISORY: Should include prefers-reduced-motion when animations are present."""
24
+ has_animation = bool(re.search(r'@keyframes|animation:|transition:', content))
25
+ if not has_animation:
26
+ return True, "no animations — reduced-motion not needed"
27
+ has_reduced_motion = 'prefers-reduced-motion' in content
28
+ if has_reduced_motion:
29
+ return True, "prefers-reduced-motion support present"
30
+ return False, "has animations but missing @media (prefers-reduced-motion: reduce)"
31
+
32
+
33
+ def check_responsive_breakpoints(content):
34
+ """ADVISORY: Should have at least 2 responsive breakpoints."""
35
+ breakpoints = re.findall(
36
+ r'@media[^{]*(?:max-width|min-width)\s*:\s*(\d+)',
37
+ content
38
+ )
39
+ unique = set(breakpoints)
40
+ count = len(unique)
41
+ if count >= 2:
42
+ return True, f"responsive: {count} breakpoints ({', '.join(sorted(unique, key=int))}px)"
43
+ if count == 1:
44
+ return False, f"only 1 breakpoint ({list(unique)[0]}px) — add tablet/desktop breakpoint (990px)"
45
+ return False, "no responsive breakpoints found — add @media queries for mobile + tablet"
46
+
47
+
48
+ def check_css_custom_properties(content):
49
+ """ADVISORY: Should use CSS custom properties for colors instead of hardcoded hex."""
50
+ # Isolate CSS portion (before schema block)
51
+ if '{%- schema -%}' in content:
52
+ css_part = content.split('{%- schema -%}')[0]
53
+ elif '{% schema %}' in content:
54
+ css_part = content.split('{% schema %}')[0]
55
+ else:
56
+ css_part = content
57
+
58
+ # Strip Liquid output tags to avoid false positives from {{ section.id }}
59
+ css_part = re.sub(r'\{\{.*?\}\}', '', css_part)
60
+
61
+ hardcoded = re.findall(r'#[0-9a-fA-F]{3,8}\b', css_part)
62
+ # Filter out CSS ID selectors (e.g. #section-xxx) — length >= 4 = color, not selector
63
+ real_hardcoded = [h for h in hardcoded if not h.startswith('#section') and len(h) >= 4]
64
+ if not real_hardcoded:
65
+ return True, "no hardcoded colors in CSS — using custom properties"
66
+ return False, f"{len(real_hardcoded)} hardcoded color(s) in CSS — use CSS custom properties instead"
67
+
68
+
69
+ def check_responsive_images(content):
70
+ """ADVISORY: Images should use srcset or image_url with width for responsive loading."""
71
+ has_images = bool(re.search(r'<img|image_url|img_url', content))
72
+ if not has_images:
73
+ return True, "no images — srcset not needed"
74
+ has_responsive = bool(re.search(r'srcset|image_url.*width|img_url.*\d+x', content))
75
+ if has_responsive:
76
+ return True, "responsive images (srcset/image_url)"
77
+ return False, "images found but no srcset or image_url width — add responsive image handling"
78
+
79
+
80
+ def check_intersection_observer(content):
81
+ """ADVISORY: Scroll animations should use Intersection Observer for performance."""
82
+ has_scroll_animation = bool(re.search(
83
+ r'\.is-visible|\.animate|scroll.*animation|animation.*scroll',
84
+ content, re.IGNORECASE
85
+ ))
86
+ if not has_scroll_animation:
87
+ return True, "no scroll animations — Intersection Observer not needed"
88
+ has_observer = 'IntersectionObserver' in content
89
+ if has_observer:
90
+ return True, "uses Intersection Observer for scroll animations"
91
+ return False, "scroll animations detected but no Intersection Observer — add for performance"
92
+
93
+
94
+ DESIGN_CHECKS = [
95
+ ("animation_performance", check_animation_performance),
96
+ ("reduced_motion", check_reduced_motion),
97
+ ("responsive_breakpoints", check_responsive_breakpoints),
98
+ ("css_custom_properties", check_css_custom_properties),
99
+ ("responsive_images", check_responsive_images),
100
+ ("intersection_observer", check_intersection_observer),
101
+ ]
@@ -22,6 +22,16 @@ try:
22
22
  except (FileNotFoundError, AttributeError):
23
23
  pass
24
24
 
25
+ # Import design quality checks (animation, responsive, a11y)
26
+ _design_mod = None
27
+ try:
28
+ _dc_path = Path(__file__).parent / 'quality-gate-design-checks.py'
29
+ _dc_spec = importlib.util.spec_from_file_location('qg_design', _dc_path)
30
+ _design_mod = importlib.util.module_from_spec(_dc_spec)
31
+ _dc_spec.loader.exec_module(_design_mod)
32
+ except (FileNotFoundError, AttributeError):
33
+ pass
34
+
25
35
  # Core checks (PASS/FAIL)
26
36
  QUALITY_CHECKS = [
27
37
  ("schema_valid_json", "Schema block contains valid JSON"),
@@ -229,6 +239,22 @@ def run_quality_gate(filepath):
229
239
  warned += 1
230
240
  results.append(f"⚠ {description}: ERROR ({e})")
231
241
 
242
+ # Design quality advisory checks (separate section)
243
+ design_passed = 0
244
+ design_total = 0
245
+ design_results = []
246
+ if _design_mod and hasattr(_design_mod, "DESIGN_CHECKS"):
247
+ for name, check_fn in _design_mod.DESIGN_CHECKS:
248
+ design_total += 1
249
+ try:
250
+ ok, msg = check_fn(content)
251
+ if ok:
252
+ design_passed += 1
253
+ icon = "✓" if ok else "⚠"
254
+ design_results.append(f"{icon} {msg}")
255
+ except Exception as e:
256
+ design_results.append(f"⚠ {name}: ERROR ({e})")
257
+
232
258
  total = passed + failed + warned
233
259
  summary = f"\n## Quality Gate: {path.name}\n"
234
260
  summary += f"**Score:** {passed}/{total} checks passed"
@@ -237,6 +263,11 @@ def run_quality_gate(filepath):
237
263
  summary += "\n\n"
238
264
  summary += "\n".join(results)
239
265
 
266
+ if design_results:
267
+ summary += "\n\n--- Design Quality Advisory ---\n"
268
+ summary += "\n".join(design_results)
269
+ summary += f"\n\nDesign Quality: {design_passed}/{design_total} advisory checks passed"
270
+
240
271
  if failed == 0:
241
272
  summary += "\n\n**Result: PASS** ✓"
242
273
  if warned:
@@ -28,6 +28,13 @@ search_settings_profile = _hm.search_settings_profile
28
28
  search_theme_dna = _hm.search_theme_dna
29
29
  search_design_tokens = _hm.search_design_tokens
30
30
 
31
+ # Import ux-bridge module (kebab-case filename requires importlib)
32
+ _ub_path = Path(__file__).parent / 'ux-bridge.py'
33
+ _ub_spec = _ilu.spec_from_file_location('ux_bridge', _ub_path)
34
+ _ub_mod = _ilu.module_from_spec(_ub_spec)
35
+ _ub_spec.loader.exec_module(_ub_mod)
36
+ build_ux_context = _ub_mod.build_ux_context
37
+
31
38
  TEMPLATE_DIR = Path(__file__).parent.parent / "templates"
32
39
 
33
40
  # UTF-8 stdout
@@ -99,39 +106,14 @@ def search_best_practices(description, max_results=5):
99
106
  return "\n".join(output)
100
107
 
101
108
 
102
- def try_ui_ux_bridge(ui_style_keywords):
103
- """Optional: query ui-ux-pro-max if available. Returns empty string if N/A."""
104
- try:
105
- ux_search_path = Path(__file__).parent.parent.parent.parent / ".claude" / "skills" / "ui-ux-pro-max" / "scripts" / "core.py"
106
- if not ux_search_path.exists():
107
- ux_search_path = Path(__file__).parent.parent.parent.parent / ".research" / "ui-ux-pro-max-skill" / "src" / "ui-ux-pro-max" / "scripts" / "core.py"
108
-
109
- if ux_search_path.exists():
110
- import importlib.util
111
- spec = importlib.util.spec_from_file_location("ux_core", ux_search_path)
112
- ux_module = importlib.util.module_from_spec(spec)
113
- spec.loader.exec_module(ux_module)
114
- result = ux_module.search(ui_style_keywords, domain="style", max_results=2)
115
- if result.get("results"):
116
- lines = ["<design_recommendations>", "**UI/UX Style Recommendations:**"]
117
- for r in result["results"]:
118
- lines.append(f"- {r.get('Style Category', '')}: {r.get('Keywords', '')}")
119
- lines.append(f" Effects: {r.get('Effects & Animation', '')}")
120
- lines.append("</design_recommendations>")
121
- return "\n".join(lines)
122
- except Exception:
123
- pass
124
- # Return empty string instead of N/A to avoid wasting prompt tokens
125
- return ""
126
-
127
-
128
109
  def _strip_empty_xml_tags(content):
129
110
  """Remove XML tags whose body is empty or whitespace-only"""
130
111
  return re.sub(r'<([\w-]+)>\s*</\1>', '', content)
131
112
 
132
113
 
133
114
  def assemble_context(name, description, theme_profile, component, schema,
134
- practices, design_recs, block_patterns="",
115
+ practices, design_system_ctx="", animation_ctx="",
116
+ ux_guidelines_ctx="", block_patterns="",
135
117
  settings_profile="", theme_dna="",
136
118
  design_tokens="", refinement=""):
137
119
  """Merge all context into generation prompt"""
@@ -146,7 +128,9 @@ def assemble_context(name, description, theme_profile, component, schema,
146
128
  content = content.replace("{{ component_pattern_content }}", component)
147
129
  content = content.replace("{{ schema_search_results }}", schema)
148
130
  content = content.replace("{{ best_practices_results }}", practices)
149
- content = content.replace("{{ ui_ux_recommendations }}", design_recs)
131
+ content = content.replace("{{ design_system_context }}", design_system_ctx)
132
+ content = content.replace("{{ animation_framework_context }}", animation_ctx)
133
+ content = content.replace("{{ ux_guidelines_context }}", ux_guidelines_ctx)
150
134
  content = content.replace("{{ block_patterns_results }}", block_patterns)
151
135
  content = content.replace("{{ settings_profile_results }}", settings_profile)
152
136
  content = content.replace("{{ theme_dna_context }}", theme_dna)
@@ -175,7 +159,7 @@ def main():
175
159
  component = search_components(f"{args.name} {args.description}")
176
160
  schema = search_schema_settings(f"shopify section settings {args.description}")
177
161
  practices = search_best_practices(f"shopify section {args.name} best practices")
178
- design_recs = try_ui_ux_bridge(args.name + " " + args.description)
162
+ ux_ctx = build_ux_context(args.name, args.description)
179
163
  blocks = search_block_patterns(f"{args.name} {args.description}")
180
164
  profile = search_settings_profile(args.name)
181
165
  dna = search_theme_dna(args.name)
@@ -203,8 +187,13 @@ def main():
203
187
  f"```liquid\n{existing[:3000]}\n```\n</refinement>")
204
188
 
205
189
  context = assemble_context(args.name, args.description, theme, component, schema,
206
- practices, design_recs, blocks, profile, dna,
207
- design_tokens=tokens, refinement=refinement)
190
+ practices,
191
+ design_system_ctx=ux_ctx.get("design_system", ""),
192
+ animation_ctx=ux_ctx.get("animations", ""),
193
+ ux_guidelines_ctx=ux_ctx.get("ux_guidelines", ""),
194
+ block_patterns=blocks, settings_profile=profile,
195
+ theme_dna=dna, design_tokens=tokens,
196
+ refinement=refinement)
208
197
  print(context)
209
198
 
210
199