@lifeonlars/prime-yggdrasil 0.3.0 → 0.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.
- package/.ai/agents/accessibility.md +15 -8
- package/.ai/agents/interaction-patterns.md +16 -8
- package/README.md +15 -16
- package/cli/commands/audit.js +23 -1
- package/cli/commands/validate.js +29 -1
- package/cli/rules/accessibility/index.js +15 -0
- package/cli/rules/accessibility/missing-alt-text.js +201 -0
- package/cli/rules/accessibility/missing-form-labels.js +238 -0
- package/cli/rules/interaction-patterns/focus-management.js +187 -0
- package/cli/rules/interaction-patterns/generic-copy.js +190 -0
- package/cli/rules/interaction-patterns/index.js +17 -0
- package/cli/rules/interaction-patterns/state-completeness.js +194 -0
- package/docs/AESTHETICS.md +4 -4
- package/docs/PHASE-6-PLAN.md +456 -0
- package/package.json +1 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interaction Patterns Rule: Focus Management
|
|
3
|
+
*
|
|
4
|
+
* Validates proper focus management in Dialogs, Modals, and form flows.
|
|
5
|
+
* Ensures keyboard navigation works correctly and focus is trapped/returned appropriately.
|
|
6
|
+
*
|
|
7
|
+
* Phase 6 - Interaction Patterns Agent
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
name: 'interaction-patterns/focus-management',
|
|
12
|
+
description: 'Validate Dialog/Modal focus patterns and keyboard navigation',
|
|
13
|
+
category: 'interaction-patterns',
|
|
14
|
+
severity: 'warning',
|
|
15
|
+
documentation: 'https://github.com/lifeonlars/prime-yggdrasil/blob/master/.ai/agents/interaction-patterns.md#5-focus-management',
|
|
16
|
+
|
|
17
|
+
validate(fileContent, filePath) {
|
|
18
|
+
const violations = [];
|
|
19
|
+
|
|
20
|
+
// Skip non-React/TSX files
|
|
21
|
+
if (!filePath.match(/\.(jsx|tsx|js|ts)$/)) {
|
|
22
|
+
return violations;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Pattern 1: Check Dialog components for focus management
|
|
26
|
+
const dialogRegex = /<Dialog[^>]*>/g;
|
|
27
|
+
let match;
|
|
28
|
+
|
|
29
|
+
while ((match = dialogRegex.exec(fileContent)) !== null) {
|
|
30
|
+
const dialogStart = match.index;
|
|
31
|
+
const dialogEnd = fileContent.indexOf('</Dialog>', dialogStart);
|
|
32
|
+
|
|
33
|
+
if (dialogEnd === -1) continue;
|
|
34
|
+
|
|
35
|
+
const dialogContent = fileContent.substring(dialogStart, dialogEnd);
|
|
36
|
+
const line = this.getLineNumber(fileContent, dialogStart);
|
|
37
|
+
|
|
38
|
+
// Check 1: Dialog should have modal prop (focus trap)
|
|
39
|
+
if (!dialogContent.includes('modal')) {
|
|
40
|
+
violations.push({
|
|
41
|
+
line,
|
|
42
|
+
column: 1,
|
|
43
|
+
message: 'Dialog missing modal prop. Add modal={true} to trap focus within dialog.',
|
|
44
|
+
severity: 'warning',
|
|
45
|
+
rule: this.name,
|
|
46
|
+
suggestion: '<Dialog visible={visible} onHide={onHide} modal>',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check 2: First interactive element should have autoFocus
|
|
51
|
+
const hasAutoFocus = /autoFocus/.test(dialogContent);
|
|
52
|
+
const hasInteractiveElement = /(<InputText|<Button|<Dropdown|<Calendar|<Checkbox)/.test(dialogContent);
|
|
53
|
+
|
|
54
|
+
if (hasInteractiveElement && !hasAutoFocus) {
|
|
55
|
+
violations.push({
|
|
56
|
+
line,
|
|
57
|
+
column: 1,
|
|
58
|
+
message: 'Dialog first interactive element should have autoFocus for keyboard navigation.',
|
|
59
|
+
severity: 'info',
|
|
60
|
+
rule: this.name,
|
|
61
|
+
suggestion: '<InputText autoFocus /> // First field in dialog',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check 3: Dialog should have onHide handler (ESC key support)
|
|
66
|
+
if (!dialogContent.includes('onHide')) {
|
|
67
|
+
violations.push({
|
|
68
|
+
line,
|
|
69
|
+
column: 1,
|
|
70
|
+
message: 'Dialog missing onHide handler. Required for ESC key support.',
|
|
71
|
+
severity: 'error',
|
|
72
|
+
rule: this.name,
|
|
73
|
+
suggestion: '<Dialog visible={visible} onHide={() => setVisible(false)}>',
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Pattern 2: Check for interactive divs without keyboard handlers
|
|
79
|
+
const interactiveDivRegex = /<div[^>]*onClick[^>]*>/g;
|
|
80
|
+
|
|
81
|
+
while ((match = interactiveDivRegex.exec(fileContent)) !== null) {
|
|
82
|
+
const divTag = match[0];
|
|
83
|
+
const line = this.getLineNumber(fileContent, match.index);
|
|
84
|
+
|
|
85
|
+
// Check if div has keyboard handlers
|
|
86
|
+
const hasKeyboard = divTag.includes('onKeyDown') || divTag.includes('onKeyPress');
|
|
87
|
+
const hasRole = divTag.includes('role="button"') || divTag.includes('role=\\"button\\"');
|
|
88
|
+
const hasTabIndex = divTag.includes('tabIndex');
|
|
89
|
+
|
|
90
|
+
if (!hasKeyboard || !hasRole || !hasTabIndex) {
|
|
91
|
+
violations.push({
|
|
92
|
+
line,
|
|
93
|
+
column: 1,
|
|
94
|
+
message: 'Interactive div must have role="button", tabIndex, and keyboard handlers (onKeyDown).',
|
|
95
|
+
severity: 'error',
|
|
96
|
+
rule: this.name,
|
|
97
|
+
suggestion: this.getKeyboardHandlerSuggestion(),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Pattern 3: Check form fields for logical tab order
|
|
103
|
+
const formRegex = /<form[^>]*>/g;
|
|
104
|
+
|
|
105
|
+
while ((match = formRegex.exec(fileContent)) !== null) {
|
|
106
|
+
const formStart = match.index;
|
|
107
|
+
const formEnd = fileContent.indexOf('</form>', formStart);
|
|
108
|
+
|
|
109
|
+
if (formEnd === -1) continue;
|
|
110
|
+
|
|
111
|
+
const formContent = fileContent.substring(formStart, formEnd);
|
|
112
|
+
const line = this.getLineNumber(fileContent, formStart);
|
|
113
|
+
|
|
114
|
+
// Check for explicit tabIndex that might break natural order
|
|
115
|
+
const explicitTabIndex = /tabIndex=\{?(\d+)\}?/.exec(formContent);
|
|
116
|
+
|
|
117
|
+
if (explicitTabIndex && parseInt(explicitTabIndex[1]) > 0) {
|
|
118
|
+
violations.push({
|
|
119
|
+
line: line + this.getLineNumber(formContent, explicitTabIndex.index),
|
|
120
|
+
column: 1,
|
|
121
|
+
message: 'Avoid positive tabIndex values. Use natural DOM order or tabIndex={0} for custom elements.',
|
|
122
|
+
severity: 'warning',
|
|
123
|
+
rule: this.name,
|
|
124
|
+
suggestion: 'Remove tabIndex or use tabIndex={0} for same-level focus order.',
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Pattern 4: Check for custom focus styles
|
|
130
|
+
const hasFocusStyles = /:focus/.test(fileContent) || /focus-visible/.test(fileContent);
|
|
131
|
+
const hasInteractiveComponents = /<(Button|InputText|Dropdown|Calendar)/.test(fileContent);
|
|
132
|
+
|
|
133
|
+
if (hasInteractiveComponents && fileContent.includes('outline: none') && !hasFocusStyles) {
|
|
134
|
+
const line = this.getLineNumber(fileContent, fileContent.indexOf('outline: none'));
|
|
135
|
+
|
|
136
|
+
violations.push({
|
|
137
|
+
line,
|
|
138
|
+
column: 1,
|
|
139
|
+
message: 'Removing outline without custom focus styles breaks keyboard navigation visibility.',
|
|
140
|
+
severity: 'error',
|
|
141
|
+
rule: this.name,
|
|
142
|
+
suggestion: 'Replace with: boxShadow: "0 0 0 2px var(--border-state-focus)"',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return violations;
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
getKeyboardHandlerSuggestion() {
|
|
150
|
+
return `
|
|
151
|
+
<div
|
|
152
|
+
role="button"
|
|
153
|
+
tabIndex={0}
|
|
154
|
+
onClick={handleClick}
|
|
155
|
+
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && handleClick()}
|
|
156
|
+
>
|
|
157
|
+
Click me
|
|
158
|
+
</div>`;
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
getLineNumber(content, index) {
|
|
162
|
+
return content.substring(0, index).split('\n').length;
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
autofix(fileContent, violation) {
|
|
166
|
+
if (violation.message.includes('Dialog missing modal prop')) {
|
|
167
|
+
const dialogRegex = /<Dialog([^>]*)>/;
|
|
168
|
+
const match = dialogRegex.exec(fileContent);
|
|
169
|
+
|
|
170
|
+
if (match) {
|
|
171
|
+
const dialogTag = match[0];
|
|
172
|
+
const fixedTag = dialogTag.replace('<Dialog', '<Dialog modal');
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
fixed: true,
|
|
176
|
+
content: fileContent.replace(dialogTag, fixedTag),
|
|
177
|
+
message: 'Added modal prop to Dialog',
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
fixed: false,
|
|
184
|
+
message: 'Manual fix required. See suggestion in violation message.',
|
|
185
|
+
};
|
|
186
|
+
},
|
|
187
|
+
};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interaction Patterns Rule: Generic Copy
|
|
3
|
+
*
|
|
4
|
+
* Enforces specific, action-oriented button labels and copy instead of generic terms.
|
|
5
|
+
* Aligns with AESTHETICS.md: "Be specific, be concise, be functional"
|
|
6
|
+
*
|
|
7
|
+
* Phase 6 - Interaction Patterns Agent
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
name: 'interaction-patterns/generic-copy',
|
|
12
|
+
description: 'Enforce specific button labels over generic ones ("Save Changes" not "Submit")',
|
|
13
|
+
category: 'interaction-patterns',
|
|
14
|
+
severity: 'warning',
|
|
15
|
+
documentation: 'https://github.com/lifeonlars/prime-yggdrasil/blob/master/.ai/agents/interaction-patterns.md#7-copy-tone--content',
|
|
16
|
+
|
|
17
|
+
// Generic terms to avoid
|
|
18
|
+
genericTerms: {
|
|
19
|
+
// Button labels
|
|
20
|
+
'Submit': 'Use specific action like "Save Changes", "Create Account", "Send Message"',
|
|
21
|
+
'OK': 'Use specific action like "Confirm Delete", "Apply Settings", "Close"',
|
|
22
|
+
'Cancel': 'OK in most cases, but consider "Keep Editing", "Go Back" for clarity',
|
|
23
|
+
'Continue': 'Use specific action like "Next: Payment", "Proceed to Checkout"',
|
|
24
|
+
'Done': 'Use specific action like "Finish Setup", "Complete"',
|
|
25
|
+
'Click here': 'Use descriptive link text like "View documentation", "Learn more about tokens"',
|
|
26
|
+
'Click': 'Avoid instructional copy - buttons are self-evident',
|
|
27
|
+
|
|
28
|
+
// Messages
|
|
29
|
+
'Success!': 'Be specific: "Settings saved", "User created", "Email sent"',
|
|
30
|
+
'Error': 'Explain what failed: "Unable to save. Try again.", "Connection failed."',
|
|
31
|
+
'Warning': 'Be specific about the warning: "Unsaved changes will be lost"',
|
|
32
|
+
'Oops!': 'Avoid casual tone - be clear and pragmatic',
|
|
33
|
+
'Something went wrong': 'Explain the issue: "Unable to connect to server", "Invalid format"',
|
|
34
|
+
'Loading...': 'Be specific: "Loading users...", "Saving changes...", "Processing..."',
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
validate(fileContent, filePath) {
|
|
38
|
+
const violations = [];
|
|
39
|
+
|
|
40
|
+
// Skip non-React/TSX files
|
|
41
|
+
if (!filePath.match(/\.(jsx|tsx|js|ts)$/)) {
|
|
42
|
+
return violations;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Pattern 1: Check Button component labels
|
|
46
|
+
const buttonLabelRegex = /<Button[^>]*label=["']([^"']+)["']/g;
|
|
47
|
+
let match;
|
|
48
|
+
|
|
49
|
+
while ((match = buttonLabelRegex.exec(fileContent)) !== null) {
|
|
50
|
+
const label = match[1];
|
|
51
|
+
const line = this.getLineNumber(fileContent, match.index);
|
|
52
|
+
|
|
53
|
+
if (this.genericTerms[label]) {
|
|
54
|
+
violations.push({
|
|
55
|
+
line,
|
|
56
|
+
column: 1,
|
|
57
|
+
message: `Generic button label "${label}". ${this.genericTerms[label]}`,
|
|
58
|
+
severity: 'warning',
|
|
59
|
+
rule: this.name,
|
|
60
|
+
suggestion: this.getSuggestion(label),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Pattern 2: Check button element text content
|
|
66
|
+
const buttonTextRegex = /<button[^>]*>([^<]+)<\/button>/gi;
|
|
67
|
+
|
|
68
|
+
while ((match = buttonTextRegex.exec(fileContent)) !== null) {
|
|
69
|
+
const text = match[1].trim();
|
|
70
|
+
const line = this.getLineNumber(fileContent, match.index);
|
|
71
|
+
|
|
72
|
+
if (this.genericTerms[text]) {
|
|
73
|
+
violations.push({
|
|
74
|
+
line,
|
|
75
|
+
column: 1,
|
|
76
|
+
message: `Generic button text "${text}". ${this.genericTerms[text]}`,
|
|
77
|
+
severity: 'warning',
|
|
78
|
+
rule: this.name,
|
|
79
|
+
suggestion: this.getSuggestion(text),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Pattern 3: Check Message/Toast content
|
|
85
|
+
const messagePatterns = [
|
|
86
|
+
/severity=["']success["'][^>]*text=["']([^"']+)["']/g,
|
|
87
|
+
/severity=["']error["'][^>]*text=["']([^"']+)["']/g,
|
|
88
|
+
/severity=["']warn["'][^>]*text=["']([^"']+)["']/g,
|
|
89
|
+
/summary=["']([^"']+)["']/g,
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
messagePatterns.forEach(pattern => {
|
|
93
|
+
while ((match = pattern.exec(fileContent)) !== null) {
|
|
94
|
+
const text = match[1];
|
|
95
|
+
const line = this.getLineNumber(fileContent, match.index);
|
|
96
|
+
|
|
97
|
+
if (this.genericTerms[text]) {
|
|
98
|
+
violations.push({
|
|
99
|
+
line,
|
|
100
|
+
column: 1,
|
|
101
|
+
message: `Generic message "${text}". ${this.genericTerms[text]}`,
|
|
102
|
+
severity: 'warning',
|
|
103
|
+
rule: this.name,
|
|
104
|
+
suggestion: this.getSuggestion(text),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check for emoji/fluffy language
|
|
109
|
+
if (/[🎉😅❤️👍✨]/.test(text)) {
|
|
110
|
+
violations.push({
|
|
111
|
+
line,
|
|
112
|
+
column: 1,
|
|
113
|
+
message: `Avoid emoji in UI copy. Keep tone clear and pragmatic.`,
|
|
114
|
+
severity: 'warning',
|
|
115
|
+
rule: this.name,
|
|
116
|
+
suggestion: `Remove emoji from: "${text}"`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check for casual/marketing language
|
|
121
|
+
const casualPhrases = [
|
|
122
|
+
/let's/i,
|
|
123
|
+
/awesome/i,
|
|
124
|
+
/amazing/i,
|
|
125
|
+
/yay/i,
|
|
126
|
+
/congrats/i,
|
|
127
|
+
/you're all set/i,
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
casualPhrases.forEach(phrase => {
|
|
131
|
+
if (phrase.test(text)) {
|
|
132
|
+
violations.push({
|
|
133
|
+
line,
|
|
134
|
+
column: 1,
|
|
135
|
+
message: `Avoid casual/marketing language in UI copy: "${text}"`,
|
|
136
|
+
severity: 'warning',
|
|
137
|
+
rule: this.name,
|
|
138
|
+
suggestion: 'Use clear, functional copy. See docs/AESTHETICS.md for examples.',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Pattern 4: Check link text
|
|
146
|
+
const linkTextRegex = /<a[^>]*>([^<]+)<\/a>/gi;
|
|
147
|
+
|
|
148
|
+
while ((match = linkTextRegex.exec(fileContent)) !== null) {
|
|
149
|
+
const text = match[1].trim();
|
|
150
|
+
const line = this.getLineNumber(fileContent, match.index);
|
|
151
|
+
|
|
152
|
+
if (this.genericTerms[text]) {
|
|
153
|
+
violations.push({
|
|
154
|
+
line,
|
|
155
|
+
column: 1,
|
|
156
|
+
message: `Generic link text "${text}". ${this.genericTerms[text]}`,
|
|
157
|
+
severity: 'warning',
|
|
158
|
+
rule: this.name,
|
|
159
|
+
suggestion: 'Use descriptive link text that explains the destination.',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return violations;
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
getSuggestion(genericTerm) {
|
|
168
|
+
const examples = {
|
|
169
|
+
'Submit': 'Examples: "Save Changes", "Create Project", "Send Email"',
|
|
170
|
+
'OK': 'Examples: "Confirm", "Apply", "Close Dialog"',
|
|
171
|
+
'Continue': 'Examples: "Next: Payment", "Proceed to Review"',
|
|
172
|
+
'Success!': 'Examples: "Settings saved", "User created successfully"',
|
|
173
|
+
'Error': 'Examples: "Unable to save. Try again.", "Invalid email format."',
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
return examples[genericTerm] || 'Use specific, action-oriented copy.';
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
getLineNumber(content, index) {
|
|
180
|
+
return content.substring(0, index).split('\n').length;
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
autofix(fileContent, violation) {
|
|
184
|
+
// No automatic fix for copy - requires human judgment
|
|
185
|
+
return {
|
|
186
|
+
fixed: false,
|
|
187
|
+
message: 'Manual fix required. Replace generic copy with specific, action-oriented text.',
|
|
188
|
+
};
|
|
189
|
+
},
|
|
190
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interaction Patterns Rules
|
|
3
|
+
*
|
|
4
|
+
* Phase 6 - Behavioral consistency enforcement
|
|
5
|
+
*
|
|
6
|
+
* All rules enforce patterns from .ai/agents/interaction-patterns.md
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import stateCompleteness from './state-completeness.js';
|
|
10
|
+
import genericCopy from './generic-copy.js';
|
|
11
|
+
import focusManagement from './focus-management.js';
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
'interaction-patterns/state-completeness': stateCompleteness,
|
|
15
|
+
'interaction-patterns/generic-copy': genericCopy,
|
|
16
|
+
'interaction-patterns/focus-management': focusManagement,
|
|
17
|
+
};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interaction Patterns Rule: State Completeness
|
|
3
|
+
*
|
|
4
|
+
* Enforces that async operations and data-driven components handle all required states:
|
|
5
|
+
* - Loading state
|
|
6
|
+
* - Error state
|
|
7
|
+
* - Empty state
|
|
8
|
+
* - Disabled state (when applicable)
|
|
9
|
+
*
|
|
10
|
+
* Phase 6 - Interaction Patterns Agent
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
name: 'interaction-patterns/state-completeness',
|
|
15
|
+
description: 'Enforce state completeness (loading/error/empty/disabled) for async operations',
|
|
16
|
+
category: 'interaction-patterns',
|
|
17
|
+
severity: 'warning',
|
|
18
|
+
documentation: 'https://github.com/lifeonlars/prime-yggdrasil/blob/master/.ai/agents/interaction-patterns.md#1-state-management-patterns',
|
|
19
|
+
|
|
20
|
+
validate(fileContent, filePath) {
|
|
21
|
+
const violations = [];
|
|
22
|
+
|
|
23
|
+
// Skip non-React/TSX files
|
|
24
|
+
if (!filePath.match(/\.(jsx|tsx|js|ts)$/)) {
|
|
25
|
+
return violations;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Pattern 1: Detect async operations (useEffect, fetch, axios, async functions)
|
|
29
|
+
const asyncPatterns = [
|
|
30
|
+
/useEffect\s*\(/g,
|
|
31
|
+
/async\s+function/g,
|
|
32
|
+
/async\s+\(/g,
|
|
33
|
+
/\.then\s*\(/g,
|
|
34
|
+
/await\s+fetch/g,
|
|
35
|
+
/await\s+axios/g,
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const hasAsyncOperation = asyncPatterns.some(pattern => pattern.test(fileContent));
|
|
39
|
+
|
|
40
|
+
if (!hasAsyncOperation) {
|
|
41
|
+
// No async operations, skip validation
|
|
42
|
+
return violations;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Pattern 2: Check for state variables that suggest data loading
|
|
46
|
+
const statePatterns = {
|
|
47
|
+
loading: /const\s+\[\s*\w*loading\w*\s*,/gi,
|
|
48
|
+
error: /const\s+\[\s*\w*error\w*\s*,/gi,
|
|
49
|
+
data: /const\s+\[\s*\w*(data|items|users|records)\w*\s*,/gi,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const hasLoadingState = statePatterns.loading.test(fileContent);
|
|
53
|
+
const hasErrorState = statePatterns.error.test(fileContent);
|
|
54
|
+
const hasDataState = statePatterns.data.test(fileContent);
|
|
55
|
+
|
|
56
|
+
// Pattern 3: Detect DataTable, List, or other data-driven components
|
|
57
|
+
const dataComponents = [
|
|
58
|
+
'DataTable',
|
|
59
|
+
'DataView',
|
|
60
|
+
'Tree',
|
|
61
|
+
'TreeTable',
|
|
62
|
+
'OrderList',
|
|
63
|
+
'PickList',
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const hasDataComponent = dataComponents.some(component =>
|
|
67
|
+
fileContent.includes(`<${component}`)
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Violation detection
|
|
71
|
+
if (hasAsyncOperation || hasDataComponent) {
|
|
72
|
+
const missingStates = [];
|
|
73
|
+
|
|
74
|
+
if (!hasLoadingState) {
|
|
75
|
+
missingStates.push('loading');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!hasErrorState) {
|
|
79
|
+
missingStates.push('error');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check for empty state handling (conditional rendering when data is empty)
|
|
83
|
+
const hasEmptyCheck = /\.length\s*===\s*0/.test(fileContent) ||
|
|
84
|
+
/\.length\s*<\s*1/.test(fileContent) ||
|
|
85
|
+
/!.*\.length/.test(fileContent);
|
|
86
|
+
|
|
87
|
+
if (hasDataState && !hasEmptyCheck) {
|
|
88
|
+
missingStates.push('empty');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (missingStates.length > 0) {
|
|
92
|
+
violations.push({
|
|
93
|
+
line: 1, // Could be improved with AST parsing for exact line
|
|
94
|
+
column: 1,
|
|
95
|
+
message: `Missing state handling: ${missingStates.join(', ')}`,
|
|
96
|
+
severity: 'warning',
|
|
97
|
+
rule: this.name,
|
|
98
|
+
suggestion: this.getSuggestion(missingStates),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Pattern 4: Check for proper conditional rendering of states
|
|
104
|
+
if (hasLoadingState) {
|
|
105
|
+
const hasLoadingRender = fileContent.includes('loading') &&
|
|
106
|
+
(fileContent.includes('ProgressSpinner') ||
|
|
107
|
+
fileContent.includes('Skeleton') ||
|
|
108
|
+
fileContent.includes('loading={'));
|
|
109
|
+
|
|
110
|
+
if (!hasLoadingRender) {
|
|
111
|
+
violations.push({
|
|
112
|
+
line: 1,
|
|
113
|
+
column: 1,
|
|
114
|
+
message: 'Loading state defined but not rendered. Add <ProgressSpinner /> or loading indicator.',
|
|
115
|
+
severity: 'warning',
|
|
116
|
+
rule: this.name,
|
|
117
|
+
suggestion: 'if (loading) return <ProgressSpinner />;',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (hasErrorState) {
|
|
123
|
+
const hasErrorRender = fileContent.includes('error') &&
|
|
124
|
+
(fileContent.includes('Message') ||
|
|
125
|
+
fileContent.includes('InlineMessage') ||
|
|
126
|
+
fileContent.includes('Toast'));
|
|
127
|
+
|
|
128
|
+
if (!hasErrorRender) {
|
|
129
|
+
violations.push({
|
|
130
|
+
line: 1,
|
|
131
|
+
column: 1,
|
|
132
|
+
message: 'Error state defined but not rendered. Add <Message severity="error" /> or error display.',
|
|
133
|
+
severity: 'warning',
|
|
134
|
+
rule: this.name,
|
|
135
|
+
suggestion: 'if (error) return <Message severity="error" text="Unable to load data. Try again." />;',
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return violations;
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
getSuggestion(missingStates) {
|
|
144
|
+
const suggestions = {
|
|
145
|
+
loading: 'Add: const [loading, setLoading] = useState(false);',
|
|
146
|
+
error: 'Add: const [error, setError] = useState(null);',
|
|
147
|
+
empty: 'Add empty state check: if (data.length === 0) return <EmptyState />;',
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
return missingStates.map(state => suggestions[state]).join('\n');
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
autofix(fileContent, violation) {
|
|
154
|
+
// Basic autofix: Add state variable declarations at the beginning of the component
|
|
155
|
+
|
|
156
|
+
if (violation.message.includes('Missing state handling')) {
|
|
157
|
+
const missingStates = violation.message.match(/: (.+)$/)[1].split(', ');
|
|
158
|
+
|
|
159
|
+
let additions = [];
|
|
160
|
+
|
|
161
|
+
if (missingStates.includes('loading')) {
|
|
162
|
+
additions.push("const [loading, setLoading] = useState(false);");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (missingStates.includes('error')) {
|
|
166
|
+
additions.push("const [error, setError] = useState(null);");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Find the first function component or class component
|
|
170
|
+
const componentMatch = fileContent.match(/(function|const)\s+\w+.*\{/);
|
|
171
|
+
|
|
172
|
+
if (componentMatch) {
|
|
173
|
+
const insertPosition = componentMatch.index + componentMatch[0].length;
|
|
174
|
+
const indentation = ' ';
|
|
175
|
+
|
|
176
|
+
const fixedContent =
|
|
177
|
+
fileContent.slice(0, insertPosition) +
|
|
178
|
+
'\n' + additions.map(line => indentation + line).join('\n') +
|
|
179
|
+
fileContent.slice(insertPosition);
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
fixed: true,
|
|
183
|
+
content: fixedContent,
|
|
184
|
+
message: `Added missing state declarations: ${missingStates.join(', ')}`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
fixed: false,
|
|
191
|
+
message: 'Manual fix required. See suggestion in violation message.',
|
|
192
|
+
};
|
|
193
|
+
},
|
|
194
|
+
};
|
package/docs/AESTHETICS.md
CHANGED
|
@@ -94,9 +94,9 @@ Agents must ensure all guidance, validation, and generated code consistently fol
|
|
|
94
94
|
|
|
95
95
|
**Drift Validator** — Detect violations of architectural rules. Flag custom components that duplicate PrimeReact. Validate token-first approach.
|
|
96
96
|
|
|
97
|
-
**Interaction Patterns** *(
|
|
97
|
+
**Interaction Patterns** *(Phase 6 - Active ✅)* — Enforce state completeness (loading/error/empty/disabled). Detect generic copy. Validate focus management and keyboard navigation. Ensure copy is clear and pragmatic.
|
|
98
98
|
|
|
99
|
-
**Accessibility** *(
|
|
99
|
+
**Accessibility** *(Phase 6 - Active ✅)* — Validate WCAG 2.1 AA compliance. Check alt text and form labels. Verify contrast ratios. Ensure color is not the only cue. Validate ARIA and keyboard navigation.
|
|
100
100
|
|
|
101
101
|
### Quick reference for agents
|
|
102
102
|
|
|
@@ -164,5 +164,5 @@ Button labels: Specific actions ("Save Changes", "Delete Item"), not generic ("O
|
|
|
164
164
|
|
|
165
165
|
---
|
|
166
166
|
|
|
167
|
-
**Status:** Mandatory reference for all agents
|
|
168
|
-
**Last updated:** 2026-01-
|
|
167
|
+
**Status:** Mandatory reference for all agents (6/6 active - Phase 6 complete ✅)
|
|
168
|
+
**Last updated:** 2026-01-11
|