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.
- package/README.md +8 -6
- package/bin/version.txt +1 -1
- package/package.json +1 -1
- package/src/commands/add-cursor-skill.js +12 -7
- package/src/commands/init.js +10 -5
- package/src/cursor/rules/00-rule-index.mdc +24 -0
- package/src/cursor/rules/accessibility-rules.mdc +527 -0
- package/src/cursor/rules/commit-rules.mdc +286 -0
- package/src/cursor/rules/cursor-rule-template.mdc +66 -0
- package/src/cursor/rules/examples/section-example.liquid +52 -0
- package/src/cursor/rules/examples/snippet-example.liquid +83 -0
- package/src/cursor/rules/figma-design-system.mdc +182 -0
- package/src/cursor/rules/global-rules-reference.mdc +62 -0
- package/src/cursor/rules/javascript-standards.mdc +1125 -0
- package/src/cursor/rules/js-refactor-tasks.mdc +123 -0
- package/src/cursor/rules/linear-task-creation.mdc +105 -0
- package/src/cursor/rules/liquid-doc-rules.mdc +595 -0
- package/src/cursor/rules/liquid.mdc +228 -0
- package/src/cursor/rules/project-overview.mdc +81 -0
- package/src/cursor/rules/schemas.mdc +150 -0
- package/src/cursor/rules/sections.mdc +25 -0
- package/src/cursor/rules/snippets.mdc +134 -0
- package/src/cursor/rules/tailwindcss-rules.mdc +410 -0
- package/src/cursor/skills/accessibility-pass/SKILL.md +54 -0
- package/src/cursor/skills/changelog-release/SKILL.md +50 -0
- package/src/cursor/skills/commit/SKILL.md +27 -0
- package/src/cursor/skills/commit-in-groups/SKILL.md +55 -0
- package/src/cursor/skills/linear-create-task/SKILL.md +81 -0
- package/src/cursor/skills/liquid-doc-comments/SKILL.md +37 -0
- package/src/cursor/skills/locale-translation-prep/SKILL.md +49 -0
- package/src/cursor/skills/schema-section-change/SKILL.md +39 -0
- package/src/cursor/skills/section-from-spec/SKILL.md +39 -0
- package/src/cursor/skills/theme-check-fix/SKILL.md +47 -0
- package/src/index.js +3 -2
- package/src/lib/commit-tooling.js +0 -44
- package/src/lib/config.js +1 -1
- package/src/lib/cursor-bundle.js +47 -0
- package/src/lib/prompts.js +3 -2
- package/src/workflows/build/reusable-build.yml +1 -1
- 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
|
|
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
|
|
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
|
|
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
|
|
112
|
+
### `climaybe add-cursor`
|
|
113
113
|
|
|
114
|
-
|
|
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
|
|
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.
|
|
1
|
+
1.8.1
|
package/package.json
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import pc from 'picocolors';
|
|
2
2
|
import { writeConfig } from '../lib/config.js';
|
|
3
|
-
import {
|
|
3
|
+
import { scaffoldCursorBundle } from '../lib/cursor-bundle.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
* Can be run standalone or after 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
|
|
10
|
+
console.log(pc.bold('\n climaybe — Add Cursor rules + skills\n'));
|
|
11
11
|
|
|
12
12
|
writeConfig({ cursor_skills: true });
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
101
|
-
|
|
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
|
|
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
|
+
---
|