climaybe 1.7.3 → 1.8.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.
Files changed (40) hide show
  1. package/README.md +8 -6
  2. package/bin/version.txt +1 -1
  3. package/package.json +1 -1
  4. package/src/commands/add-cursor-skill.js +12 -7
  5. package/src/commands/init.js +10 -5
  6. package/src/cursor/rules/00-rule-index.mdc +24 -0
  7. package/src/cursor/rules/accessibility-rules.mdc +527 -0
  8. package/src/cursor/rules/commit-rules.mdc +286 -0
  9. package/src/cursor/rules/cursor-rule-template.mdc +66 -0
  10. package/src/cursor/rules/examples/section-example.liquid +52 -0
  11. package/src/cursor/rules/examples/snippet-example.liquid +83 -0
  12. package/src/cursor/rules/figma-design-system.mdc +182 -0
  13. package/src/cursor/rules/global-rules-reference.mdc +62 -0
  14. package/src/cursor/rules/javascript-standards.mdc +1125 -0
  15. package/src/cursor/rules/js-refactor-tasks.mdc +123 -0
  16. package/src/cursor/rules/linear-task-creation.mdc +105 -0
  17. package/src/cursor/rules/liquid-doc-rules.mdc +595 -0
  18. package/src/cursor/rules/liquid.mdc +228 -0
  19. package/src/cursor/rules/project-overview.mdc +81 -0
  20. package/src/cursor/rules/schemas.mdc +150 -0
  21. package/src/cursor/rules/sections.mdc +25 -0
  22. package/src/cursor/rules/snippets.mdc +134 -0
  23. package/src/cursor/rules/tailwindcss-rules.mdc +410 -0
  24. package/src/cursor/skills/accessibility-pass/SKILL.md +54 -0
  25. package/src/cursor/skills/changelog-release/SKILL.md +50 -0
  26. package/src/cursor/skills/commit/SKILL.md +27 -0
  27. package/src/cursor/skills/commit-in-groups/SKILL.md +55 -0
  28. package/src/cursor/skills/linear-create-task/SKILL.md +81 -0
  29. package/src/cursor/skills/liquid-doc-comments/SKILL.md +37 -0
  30. package/src/cursor/skills/locale-translation-prep/SKILL.md +49 -0
  31. package/src/cursor/skills/schema-section-change/SKILL.md +39 -0
  32. package/src/cursor/skills/section-from-spec/SKILL.md +39 -0
  33. package/src/cursor/skills/theme-check-fix/SKILL.md +47 -0
  34. package/src/index.js +3 -2
  35. package/src/lib/commit-tooling.js +0 -44
  36. package/src/lib/config.js +1 -1
  37. package/src/lib/cursor-bundle.js +47 -0
  38. package/src/lib/prompts.js +3 -2
  39. package/src/workflows/build/reusable-build.yml +1 -1
  40. package/src/workflows/shared/version-bump.yml +5 -2
package/README.md CHANGED
@@ -5,7 +5,7 @@ Shopify CI/CD CLI — scaffolds GitHub Actions workflows, branch strategy, and s
5
5
  **Commit linting and AI-assisted commits are available as optional setup steps:**
6
6
 
7
7
  - **Conventional commit linting:** During `climaybe init`, you can choose to automatically install and configure [commitlint](https://commitlint.js.org/) and [Husky](https://typicode.github.io/husky) to enforce [Conventional Commits](https://www.conventionalcommits.org/) in your theme repository.
8
- - **Cursor AI commit skill:** You can also opt-in to installing the [Cursor AI commit skill](https://cursor.so/) (`.cursor/skills/commit/SKILL.md`) for AI-assisted, conventional commit message support in your project.
8
+ - **Cursor rules + skills:** You can opt in to installing Electric Maybe’s bundled [Cursor](https://cursor.com/) project rules and agent skills under `.cursor/rules/` and `.cursor/skills/` (Liquid, JS, a11y, commits, changelog, Linear, etc.).
9
9
 
10
10
  Both options streamline commit message quality and team workflows but are fully optional during setup.
11
11
 
@@ -42,12 +42,12 @@ Interactive setup that configures your repo for CI/CD.
42
42
  4. Asks whether to enable optional **preview + cleanup** workflows (default: yes)
43
43
  5. Asks whether to enable optional **build + Lighthouse** workflows (default: yes)
44
44
  6. Asks whether to enable **commitlint + Husky** (enforce [conventional commits](https://www.conventionalcommits.org/) on `git commit`)
45
- 7. Asks whether to add **Cursor commit skill** to the project (`.cursor/skills/commit/SKILL.md`) for AI-assisted conventional commits
45
+ 7. Asks whether to install **Cursor rules + skills** (`.cursor/rules/`, `.cursor/skills/`) — Electric Maybe conventions for themes and AI workflows
46
46
  8. Based on store count, sets up **single-store** or **multi-store** mode
47
47
  9. Writes `package.json` config
48
48
  10. Scaffolds GitHub Actions workflows
49
49
  11. Creates git branches and store directories (multi-store)
50
- 12. Optionally installs commitlint, Husky, and the Cursor skill
50
+ 12. Optionally installs commitlint, Husky, and the Cursor bundle (rules + skills)
51
51
 
52
52
  ### `climaybe add-store`
53
53
 
@@ -109,14 +109,16 @@ Set up **only** commitlint + Husky (conventional commits enforced on `git commit
109
109
  npx climaybe setup-commitlint
110
110
  ```
111
111
 
112
- ### `climaybe add-cursor-skill`
112
+ ### `climaybe add-cursor`
113
113
 
114
- Add **only** the Cursor commit skill to this project (`.cursor/skills/commit/SKILL.md`). Use this if you skipped it at init or want to add it later.
114
+ Install Electric Maybe **Cursor rules and skills** into `.cursor/rules/` and `.cursor/skills/`. Use this if you skipped the bundle at init or want to refresh from the version of climaybe you have installed.
115
115
 
116
116
  ```bash
117
- npx climaybe add-cursor-skill
117
+ npx climaybe add-cursor
118
118
  ```
119
119
 
120
+ The previous command name `add-cursor-skill` still works as an alias. Re-running replaces the bundled rule and skill files with the copies shipped by your installed climaybe version (same idea as `update-workflows`).
121
+
120
122
  ## Configuration
121
123
 
122
124
  The CLI writes config into the `config` field of your `package.json`:
package/bin/version.txt CHANGED
@@ -1 +1 @@
1
- 1.7.3
1
+ 1.8.1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "climaybe",
3
- "version": "1.7.3",
3
+ "version": "1.8.1",
4
4
  "description": "Shopify CI/CD CLI — scaffolds workflows, branch strategy, and store config for single-store and multi-store theme repos",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,17 +1,22 @@
1
1
  import pc from 'picocolors';
2
2
  import { writeConfig } from '../lib/config.js';
3
- import { scaffoldCursorCommitSkill } from '../lib/commit-tooling.js';
3
+ import { scaffoldCursorBundle } from '../lib/cursor-bundle.js';
4
4
 
5
5
  /**
6
- * Add only the Cursor commit skill to this project (.cursor/skills/commit/SKILL.md).
7
- * Can be run standalone or after init without having chosen Cursor skills at init.
6
+ * Install Electric Maybe Cursor rules and skills (.cursor/rules, .cursor/skills).
7
+ * Can be run standalone or after init if Cursor bundle was skipped.
8
8
  */
9
9
  export async function addCursorSkillCommand() {
10
- console.log(pc.bold('\n climaybe — Add Cursor commit skill\n'));
10
+ console.log(pc.bold('\n climaybe — Add Cursor rules + skills\n'));
11
11
 
12
12
  writeConfig({ cursor_skills: true });
13
13
 
14
- scaffoldCursorCommitSkill();
15
- console.log(pc.green(' Cursor commit skill added to .cursor/skills/commit/SKILL.md'));
16
- console.log(pc.dim(' Use "commit" or "group and commit" in Cursor to get conventional-commit assistance.\n'));
14
+ const ok = scaffoldCursorBundle();
15
+ if (ok) {
16
+ console.log(pc.green(' Installed .cursor/rules and .cursor/skills from climaybe bundle.'));
17
+ console.log(pc.dim(' See .cursor/rules/00-rule-index.mdc for which rules apply when.\n'));
18
+ } else {
19
+ console.log(pc.red(' Cursor bundle not found in this climaybe install.'));
20
+ console.log(pc.dim(' Reinstall climaybe or report an issue.\n'));
21
+ }
17
22
  }
@@ -15,7 +15,8 @@ import { readConfig, writeConfig } from '../lib/config.js';
15
15
  import { ensureGitRepo, ensureInitialCommit, ensureStagingBranch, createStoreBranches, getSuggestedTagForRelease } from '../lib/git.js';
16
16
  import { scaffoldWorkflows } from '../lib/workflows.js';
17
17
  import { createStoreDirectories } from '../lib/store-sync.js';
18
- import { scaffoldCommitlint, scaffoldCursorCommitSkill } from '../lib/commit-tooling.js';
18
+ import { scaffoldCommitlint } from '../lib/commit-tooling.js';
19
+ import { scaffoldCursorBundle } from '../lib/cursor-bundle.js';
19
20
  import {
20
21
  isGhAvailable,
21
22
  hasGitHubRemote,
@@ -87,7 +88,7 @@ async function runInitFlow() {
87
88
  includeBuild: enableBuildWorkflows,
88
89
  });
89
90
 
90
- // 7. Optional commitlint + Husky and Cursor commit skill
91
+ // 7. Optional commitlint + Husky and Cursor rules + skills bundle
91
92
  if (enableCommitlint) {
92
93
  console.log(pc.dim(' Setting up commitlint + Husky...'));
93
94
  if (scaffoldCommitlint()) {
@@ -97,8 +98,12 @@ async function runInitFlow() {
97
98
  }
98
99
  }
99
100
  if (enableCursorSkills) {
100
- scaffoldCursorCommitSkill();
101
- console.log(pc.green(' Cursor commit skill added to .cursor/skills/commit/SKILL.md'));
101
+ const cursorOk = scaffoldCursorBundle();
102
+ if (cursorOk) {
103
+ console.log(pc.green(' Electric Maybe Cursor rules + skills → .cursor/rules, .cursor/skills'));
104
+ } else {
105
+ console.log(pc.yellow(' Cursor bundle not found in package (skipped).'));
106
+ }
102
107
  }
103
108
 
104
109
  // Done
@@ -117,7 +122,7 @@ async function runInitFlow() {
117
122
  console.log(pc.dim(` Preview workflows: ${enablePreviewWorkflows ? 'enabled' : 'disabled'}`));
118
123
  console.log(pc.dim(` Build workflows: ${enableBuildWorkflows ? 'enabled' : 'disabled'}`));
119
124
  console.log(pc.dim(` commitlint + Husky: ${enableCommitlint ? 'enabled' : 'disabled'}`));
120
- console.log(pc.dim(` Cursor commit skill: ${enableCursorSkills ? 'added' : 'skipped'}`));
125
+ console.log(pc.dim(` Cursor rules + skills: ${enableCursorSkills ? 'installed' : 'skipped'}`));
121
126
 
122
127
  const suggestedTag = getSuggestedTagForRelease();
123
128
  const tagLabel = suggestedTag === 'v1.0.0' ? 'Tag your first release' : 'Tag your next release';
@@ -0,0 +1,24 @@
1
+ ---
2
+ description: Index of project rules — read the relevant rule file when performing the listed activities
3
+ globs:
4
+ - "**/*"
5
+ alwaysApply: true
6
+ ---
7
+ # Rule Index
8
+
9
+ When you perform any of the following, **read and apply** the corresponding rule file from `.cursor/rules/`:
10
+
11
+ - **Git commits, commit messages** → `commit-rules.mdc`
12
+ - **Accessibility, a11y, focus, WCAG, UI behavior** → `accessibility-rules.mdc`
13
+ - **JavaScript, web components, _scripts/, *.js** → `javascript-standards.mdc`
14
+ - **Tailwind CSS, theme tokens, Liquid/CSS classes, _styles/** → `tailwindcss-rules.mdc`
15
+ - **Liquid syntax and usage** → `liquid.mdc`
16
+ - **Section files (sections/*.liquid)** → `sections.mdc`
17
+ - **Snippets (snippets/*.liquid)** → `snippets.mdc`
18
+ - **Schema definitions (section/layout schemas)** → `schemas.mdc`
19
+ - **Liquid documentation comments** → `liquid-doc-rules.mdc`
20
+ - **JS refactor tasks / current refactoring work** → `js-refactor-tasks.mdc`
21
+ - **Adding or editing Cursor rules** → `cursor-rule-template.mdc`
22
+ - **Overview of global rules and sync** → `global-rules-reference.mdc` (optional)
23
+
24
+ Use the Read tool to load the relevant rule file when the task matches one of the above. Project overview and this index are always in context; other rules are applied when their scope applies.
@@ -0,0 +1,527 @@
1
+ ---
2
+ description: Accessibility rules for general languages used in the project
3
+ alwaysApply: false
4
+ ---
5
+ # Accessibility Rules for Electric Maybe Shopify Theme
6
+
7
+ ## Overview
8
+ This document establishes comprehensive accessibility standards for the Electric Maybe Shopify theme project, ensuring WCAG 2.1 AA compliance across all components, sections, and interactions. All code must prioritize accessibility alongside performance and user experience.
9
+
10
+ ## Core Accessibility Principles
11
+
12
+ ### 1. Semantic HTML Structure
13
+ - Use semantic HTML elements for their intended purpose
14
+ - Maintain proper heading hierarchy (h1 → h2 → h3, etc.)
15
+ - Use landmarks (header, nav, main, aside, footer) appropriately
16
+ - Implement proper list structures (ul, ol, dl)
17
+
18
+ ### 2. ARIA Implementation Standards
19
+ - Use ARIA attributes only when necessary (prefer semantic HTML)
20
+ - Ensure ARIA attributes are properly supported and tested
21
+ - Maintain ARIA state synchronization with JavaScript
22
+ - Use ARIA live regions for dynamic content updates
23
+
24
+ ### 3. Keyboard Navigation
25
+ - All interactive elements must be keyboard accessible
26
+ - Implement logical tab order
27
+ - Provide visible focus indicators
28
+ - Support keyboard shortcuts for common actions
29
+
30
+ ### 4. Screen Reader Support
31
+ - Provide meaningful alt text for images
32
+ - Use descriptive link text
33
+ - Implement proper form labels and error messages
34
+ - Ensure content is announced in logical order
35
+
36
+ ## Component-Specific Accessibility Rules
37
+
38
+ ### Web Components (JavaScript)
39
+ ```javascript
40
+ // Required accessibility implementation for all web components
41
+ class AccessibleComponent extends HTMLElement {
42
+ constructor() {
43
+ super();
44
+ this.#setupAccessibility();
45
+ }
46
+
47
+ #setupAccessibility() {
48
+ // Set ARIA attributes
49
+ this.setAttribute('role', 'region');
50
+ this.setAttribute('aria-label', 'Component description');
51
+
52
+ // Ensure keyboard navigation
53
+ this.addEventListener('keydown', this.#handleKeyboard.bind(this));
54
+
55
+ // Set tabindex for focusable elements
56
+ this.querySelectorAll('[data-focusable]').forEach(el => {
57
+ el.setAttribute('tabindex', '0');
58
+ });
59
+ }
60
+
61
+ #handleKeyboard(event) {
62
+ switch(event.key) {
63
+ case 'Enter':
64
+ case ' ':
65
+ event.preventDefault();
66
+ this.#activate();
67
+ break;
68
+ case 'Escape':
69
+ this.#close();
70
+ break;
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ ### Liquid Components
77
+ ```liquid
78
+ {%- comment -%}
79
+ Required accessibility attributes for all interactive components
80
+ {%- endcomment -%}
81
+
82
+ {%- comment -%}Buttons{%- endcomment -%}
83
+ <button
84
+ type="button"
85
+ aria-label="{{ button_label | default: 'Button' }}"
86
+ {% if disabled %}disabled aria-disabled="true"{% endif %}
87
+ {% if expanded %}aria-expanded="{{ expanded }}"{% endif %}
88
+ {% if controls %}aria-controls="{{ controls }}"{% endif %}
89
+ class="focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
90
+ >
91
+ {{ button_text }}
92
+ </button>
93
+
94
+ {%- comment -%}Images{%- endcomment -%}
95
+ <img
96
+ src="{{ image.src }}"
97
+ alt="{{ image.alt | default: 'Image description' }}"
98
+ {% if image.width %}width="{{ image.width }}"{% endif %}
99
+ {% if image.height %}height="{{ image.height }}"{% endif %}
100
+ loading="lazy"
101
+ decoding="async"
102
+ class="focus:outline-none focus:ring-2 focus:ring-blue-500"
103
+ >
104
+
105
+ {%- comment -%}Forms{%- endcomment -%}
106
+ <form role="search" aria-label="Search form">
107
+ <label for="search-input" class="sr-only">Search</label>
108
+ <input
109
+ type="search"
110
+ id="search-input"
111
+ name="q"
112
+ aria-describedby="search-help"
113
+ aria-invalid="{{ has_error }}"
114
+ aria-required="true"
115
+ required
116
+ class="focus:outline-none focus:ring-2 focus:ring-blue-500"
117
+ >
118
+ <div id="search-help" class="sr-only">Enter your search terms</div>
119
+ {% if has_error %}
120
+ <div role="alert" aria-live="polite" class="error-message">
121
+ {{ error_message }}
122
+ </div>
123
+ {% endif %}
124
+ </form>
125
+ ```
126
+
127
+ ## WCAG 2.1 AA Compliance Requirements
128
+
129
+ ### 1. Perceivable
130
+ - **Color Contrast**: Minimum 4.5:1 for normal text, 3:1 for large text
131
+ - **Text Alternatives**: All non-text content must have text alternatives
132
+ - **Adaptable**: Content must be adaptable without losing structure
133
+ - **Distinguishable**: Users must be able to see and hear content
134
+
135
+ ### 2. Operable
136
+ - **Keyboard Accessible**: All functionality available via keyboard
137
+ - **Enough Time**: Users must have enough time to read and use content
138
+ - **Seizures**: Content must not cause seizures or physical reactions
139
+ - **Navigable**: Users must be able to navigate and find content
140
+
141
+ ### 3. Understandable
142
+ - **Readable**: Text must be readable and understandable
143
+ - **Predictable**: Pages must operate in predictable ways
144
+ - **Input Assistance**: Users must be helped to avoid and correct mistakes
145
+
146
+ ### 4. Robust
147
+ - **Compatible**: Content must be compatible with current and future tools
148
+
149
+ ## Implementation Standards
150
+
151
+ ### Focus Management
152
+ ```javascript
153
+ // Proper focus management for modals and overlays
154
+ class ModalComponent extends HTMLElement {
155
+ #previousFocus = null;
156
+ #focusableElements = null;
157
+
158
+ #trapFocus() {
159
+ this.#focusableElements = this.querySelectorAll(
160
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
161
+ );
162
+
163
+ const firstElement = this.#focusableElements[0];
164
+ const lastElement = this.#focusableElements[this.#focusableElements.length - 1];
165
+
166
+ this.addEventListener('keydown', (e) => {
167
+ if (e.key === 'Tab') {
168
+ if (e.shiftKey) {
169
+ if (document.activeElement === firstElement) {
170
+ e.preventDefault();
171
+ lastElement.focus();
172
+ }
173
+ } else {
174
+ if (document.activeElement === lastElement) {
175
+ e.preventDefault();
176
+ firstElement.focus();
177
+ }
178
+ }
179
+ }
180
+ });
181
+ }
182
+
183
+ #restoreFocus() {
184
+ if (this.#previousFocus) {
185
+ this.#previousFocus.focus();
186
+ }
187
+ }
188
+ }
189
+ ```
190
+
191
+ ### Live Regions
192
+ ```javascript
193
+ // Announce dynamic content changes
194
+ class LiveRegionComponent extends HTMLElement {
195
+ #announce(message, priority = 'polite') {
196
+ const liveRegion = this.querySelector('[aria-live]') || this.#createLiveRegion();
197
+ liveRegion.setAttribute('aria-live', priority);
198
+ liveRegion.textContent = message;
199
+
200
+ // Clear after announcement
201
+ setTimeout(() => {
202
+ liveRegion.textContent = '';
203
+ }, 1000);
204
+ }
205
+
206
+ #createLiveRegion() {
207
+ const liveRegion = document.createElement('div');
208
+ liveRegion.setAttribute('aria-live', 'polite');
209
+ liveRegion.setAttribute('aria-atomic', 'true');
210
+ liveRegion.className = 'sr-only';
211
+ this.appendChild(liveRegion);
212
+ return liveRegion;
213
+ }
214
+ }
215
+ ```
216
+
217
+ ### Skip Links
218
+ ```html
219
+ <!-- Skip to main content link -->
220
+ <a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 bg-blue-600 text-white px-4 py-2 rounded z-50">
221
+ Skip to main content
222
+ </a>
223
+
224
+ <main id="main-content" tabindex="-1">
225
+ <!-- Main content here -->
226
+ </main>
227
+ ```
228
+
229
+ ## CSS Accessibility Standards
230
+
231
+ ### Focus Indicators
232
+ ```css
233
+ /* Visible focus indicators for all interactive elements */
234
+ .focus-visible {
235
+ outline: 2px solid #3b82f6;
236
+ outline-offset: 2px;
237
+ }
238
+
239
+ /* High contrast focus for better visibility */
240
+ @media (prefers-contrast: high) {
241
+ .focus-visible {
242
+ outline: 3px solid #000000;
243
+ outline-offset: 1px;
244
+ }
245
+ }
246
+
247
+ /* Reduced motion support */
248
+ @media (prefers-reduced-motion: reduce) {
249
+ *,
250
+ *::before,
251
+ *::after {
252
+ animation-duration: 0.01ms !important;
253
+ animation-iteration-count: 1 !important;
254
+ transition-duration: 0.01ms !important;
255
+ }
256
+ }
257
+ ```
258
+
259
+ ### Screen Reader Only Content
260
+ ```css
261
+ /* Hide content visually but keep it available to screen readers */
262
+ .sr-only {
263
+ position: absolute;
264
+ width: 1px;
265
+ height: 1px;
266
+ padding: 0;
267
+ margin: -1px;
268
+ overflow: hidden;
269
+ clip: rect(0, 0, 0, 0);
270
+ white-space: nowrap;
271
+ border: 0;
272
+ }
273
+
274
+ /* Show content on focus for keyboard navigation */
275
+ .sr-only:focus {
276
+ position: static;
277
+ width: auto;
278
+ height: auto;
279
+ padding: inherit;
280
+ margin: inherit;
281
+ overflow: visible;
282
+ clip: auto;
283
+ white-space: normal;
284
+ }
285
+ ```
286
+
287
+ ## Form Accessibility
288
+
289
+ ### Required Form Patterns
290
+ ```liquid
291
+ {%- comment -%}Accessible form field with proper labeling{%- endcomment -%}
292
+ <div class="form-field">
293
+ <label for="{{ field_id }}" class="block text-sm font-medium text-gray-700">
294
+ {{ field_label }}
295
+ {% if required %}<span class="text-red-500" aria-label="required">*</span>{% endif %}
296
+ </label>
297
+
298
+ <input
299
+ type="{{ field_type | default: 'text' }}"
300
+ id="{{ field_id }}"
301
+ name="{{ field_name }}"
302
+ {% if required %}required aria-required="true"{% endif %}
303
+ {% if placeholder %}placeholder="{{ placeholder }}"{% endif %}
304
+ {% if value %}value="{{ value }}"{% endif %}
305
+ {% if disabled %}disabled aria-disabled="true"{% endif %}
306
+ aria-describedby="{{ field_id }}-help {{ field_id }}-error"
307
+ aria-invalid="{{ has_error }}"
308
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
309
+ >
310
+
311
+ {% if help_text %}
312
+ <div id="{{ field_id }}-help" class="mt-1 text-sm text-gray-500">
313
+ {{ help_text }}
314
+ </div>
315
+ {% endif %}
316
+
317
+ {% if has_error %}
318
+ <div id="{{ field_id }}-error" role="alert" aria-live="polite" class="mt-1 text-sm text-system-red">
319
+ {{ error_message }}
320
+ </div>
321
+ {% endif %}
322
+ </div>
323
+ ```
324
+
325
+ ## Interactive Component Patterns
326
+
327
+ ### Toggle Components
328
+ ```javascript
329
+ class ToggleComponent extends HTMLElement {
330
+ #button = null;
331
+ #content = null;
332
+ #isExpanded = false;
333
+
334
+ connectedCallback() {
335
+ this.#button = this.querySelector('[data-toggle-button]');
336
+ this.#content = this.querySelector('[data-toggle-content]');
337
+
338
+ this.#setupAccessibility();
339
+ this.#bindEvents();
340
+ }
341
+
342
+ #setupAccessibility() {
343
+ this.#button.setAttribute('aria-expanded', 'false');
344
+ this.#button.setAttribute('aria-controls', this.#content.id);
345
+ this.#content.setAttribute('aria-hidden', 'true');
346
+ }
347
+
348
+ #toggle() {
349
+ this.#isExpanded = !this.#isExpanded;
350
+
351
+ this.#button.setAttribute('aria-expanded', this.#isExpanded.toString());
352
+ this.#content.setAttribute('aria-hidden', (!this.#isExpanded).toString());
353
+
354
+ // Announce state change
355
+ const message = this.#isExpanded ? 'Expanded' : 'Collapsed';
356
+ this.#announce(message);
357
+ }
358
+ }
359
+ ```
360
+
361
+ ### Carousel/Slider Components
362
+ ```javascript
363
+ class AccessibleCarousel extends HTMLElement {
364
+ #currentSlide = 0;
365
+ #slides = [];
366
+ #indicators = [];
367
+
368
+ #setupAccessibility() {
369
+ this.setAttribute('role', 'region');
370
+ this.setAttribute('aria-label', 'Image carousel');
371
+
372
+ this.#slides.forEach((slide, index) => {
373
+ slide.setAttribute('aria-hidden', (index !== 0).toString());
374
+ slide.setAttribute('aria-label', `Slide ${index + 1} of ${this.#slides.length}`);
375
+ });
376
+
377
+ this.#indicators.forEach((indicator, index) => {
378
+ indicator.setAttribute('role', 'tab');
379
+ indicator.setAttribute('aria-selected', (index === 0).toString());
380
+ indicator.setAttribute('aria-label', `Go to slide ${index + 1}`);
381
+ });
382
+ }
383
+
384
+ #goToSlide(index) {
385
+ this.#slides[this.#currentSlide].setAttribute('aria-hidden', 'true');
386
+ this.#indicators[this.#currentSlide].setAttribute('aria-selected', 'false');
387
+
388
+ this.#currentSlide = index;
389
+
390
+ this.#slides[this.#currentSlide].setAttribute('aria-hidden', 'false');
391
+ this.#indicators[this.#currentSlide].setAttribute('aria-selected', 'true');
392
+
393
+ this.#announce(`Slide ${index + 1} of ${this.#slides.length}`);
394
+ }
395
+ }
396
+ ```
397
+
398
+ ## Performance and Accessibility
399
+
400
+ ### Lazy Loading with Accessibility
401
+ ```javascript
402
+ // Lazy load images while maintaining accessibility
403
+ class LazyImage extends HTMLElement {
404
+ #observer = null;
405
+
406
+ connectedCallback() {
407
+ this.#observer = new IntersectionObserver(this.#handleIntersection.bind(this), {
408
+ rootMargin: '50px'
409
+ });
410
+ this.#observer.observe(this);
411
+ }
412
+
413
+ #handleIntersection(entries) {
414
+ entries.forEach(entry => {
415
+ if (entry.isIntersecting) {
416
+ this.#loadImage();
417
+ this.#observer.unobserve(this);
418
+ }
419
+ });
420
+ }
421
+
422
+ #loadImage() {
423
+ const img = this.querySelector('img');
424
+ if (img) {
425
+ img.src = img.dataset.src;
426
+ img.alt = img.dataset.alt || 'Image';
427
+ img.removeAttribute('data-src');
428
+ img.removeAttribute('data-alt');
429
+ }
430
+ }
431
+ }
432
+ ```
433
+
434
+ ## Testing and Validation
435
+
436
+ ### Automated Testing Checklist
437
+ - [ ] All images have alt text
438
+ - [ ] All form fields have labels
439
+ - [ ] All interactive elements are keyboard accessible
440
+ - [ ] Color contrast meets WCAG 2.1 AA standards
441
+ - [ ] Heading hierarchy is logical
442
+ - [ ] ARIA attributes are properly implemented
443
+ - [ ] Focus indicators are visible
444
+ - [ ] Screen reader announcements work correctly
445
+
446
+ ### Manual Testing Requirements
447
+ - [ ] Test with screen readers (NVDA, JAWS, VoiceOver)
448
+ - [ ] Test keyboard-only navigation
449
+ - [ ] Test with high contrast mode
450
+ - [ ] Test with reduced motion preferences
451
+ - [ ] Test with zoom levels up to 200%
452
+ - [ ] Test with different font sizes
453
+
454
+ ## Error Handling and Feedback
455
+
456
+ ### Accessible Error Messages
457
+ ```javascript
458
+ class FormValidator extends HTMLElement {
459
+ #showError(field, message) {
460
+ const errorElement = document.createElement('div');
461
+ errorElement.setAttribute('role', 'alert');
462
+ errorElement.setAttribute('aria-live', 'polite');
463
+ errorElement.className = 'error-message text-system-red text-sm mt-1';
464
+ errorElement.textContent = message;
465
+
466
+ field.setAttribute('aria-invalid', 'true');
467
+ field.parentNode.appendChild(errorElement);
468
+
469
+ // Announce error to screen readers
470
+ this.#announce(message, 'assertive');
471
+ }
472
+
473
+ #clearError(field) {
474
+ field.setAttribute('aria-invalid', 'false');
475
+ const errorElement = field.parentNode.querySelector('.error-message');
476
+ if (errorElement) {
477
+ errorElement.remove();
478
+ }
479
+ }
480
+ }
481
+ ```
482
+
483
+ ## Compliance Checklist
484
+
485
+ Before committing any code, ensure:
486
+
487
+ ### HTML/Liquid
488
+ - [ ] Semantic HTML elements used appropriately
489
+ - [ ] Proper heading hierarchy (h1 → h2 → h3)
490
+ - [ ] All images have meaningful alt text
491
+ - [ ] All form fields have associated labels
492
+ - [ ] Interactive elements have proper ARIA attributes
493
+ - [ ] Skip links implemented for main content
494
+ - [ ] Color is not the only way to convey information
495
+
496
+ ### JavaScript
497
+ - [ ] Keyboard navigation implemented for all interactions
498
+ - [ ] Focus management for modals and overlays
499
+ - [ ] ARIA state synchronization with JavaScript
500
+ - [ ] Live regions for dynamic content updates
501
+ - [ ] Error handling with accessible feedback
502
+ - [ ] Reduced motion preferences respected
503
+
504
+ ### CSS
505
+ - [ ] Visible focus indicators for all interactive elements
506
+ - [ ] High contrast mode support
507
+ - [ ] Reduced motion media queries implemented
508
+ - [ ] Screen reader only content properly hidden
509
+ - [ ] Color contrast meets WCAG 2.1 AA standards
510
+
511
+ ### Performance
512
+ - [ ] Lazy loading maintains accessibility
513
+ - [ ] Dynamic content updates are announced
514
+ - [ ] Loading states are communicated
515
+ - [ ] Error states are clearly indicated
516
+
517
+ ## Resources and References
518
+
519
+ - [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)
520
+ - [ARIA Authoring Practices Guide](https://www.w3.org/WAI/ARIA/apg/)
521
+ - [Shopify Accessibility Guidelines](https://shopify.dev/docs/storefronts/themes/best-practices/accessibility)
522
+ - [WebAIM Color Contrast Checker](https://webaim.org/resources/contrastchecker/)
523
+ - [axe-core Testing Library](https://github.com/dequelabs/axe-core)
524
+ description:
525
+ globs:
526
+ alwaysApply: false
527
+ ---