@a11yfred/neighbor 0.3.0 → 1.0.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.
@@ -1,256 +1,257 @@
1
- /**
2
- * @a11yfred/neighborStylelint plugin
3
- *
4
- * Rules:
5
- * ulam/user-preferences Warn when motion, transparency, or alpha colors
6
- * are used without @media (prefers-*) fallbacks.
7
- * ulam/no-outline-noneDisallow bare outline:none/0 outside :focus selectors.
8
- *
9
- * Sources and credits:
10
- * WCAG 2.1 / 2.2 w3.org/TR/WCAG21, w3.org/TR/WCAG22
11
- * WebAIM webaim.org
12
- * double-great/stylelint-a11y github.com/double-great/stylelint-a11y
13
- */
14
-
15
- const defined = (x) => x !== undefined && x !== null;
16
-
17
- /** True if the node or any ancestor is a prefers-* / forced-colors media block */
18
- function insidePreferencesMedia(node) {
19
- let current = node.parent;
20
- while (defined(current)) {
21
- if (
22
- current.type === 'atrule' &&
23
- current.name === 'media' &&
24
- /prefers-|forced-colors/.test(current.params)
25
- ) {
26
- return true;
27
- }
28
- current = current.parent;
29
- }
30
- return false;
31
- }
32
-
33
- /** True if the value string contains an alpha channel (rgb/hsl with slash, or 8-digit hex) */
34
- function hasAlphaChannel(value) {
35
- // rgb(r g b / a) or rgba() or hsl(h s l / a)
36
- if (/\b(rgb|hsl)a?\s*\(/.test(value) && /\/\s*[01]?\.?\d+[^)]*\)/.test(value)) return true;
37
- // 8-digit hex #rrggbbaa
38
- if (/#[0-9a-fA-F]{8}\b/.test(value)) return true;
39
- return false;
40
- }
41
-
42
- /** True if the opacity value is a structural endpoint (0 or 1), not a dim */
43
- function isStructuralOpacity(value) {
44
- const n = parseFloat(value.trim());
45
- return n === 0 || n === 1;
46
- }
47
-
48
- const ruleName = 'ulam/user-preferences';
49
-
50
- const messages = {
51
- opacity: (value) =>
52
- `opacity: ${value} creates a transparency effect. Add a fallback in @media (prefers-reduced-transparency: reduce) that uses an explicit color token instead. See src/components/ui/user-preferences.css.`,
53
- animation: (prop, value) =>
54
- `${prop}: ${value} uses motion. Add a fallback in @media (prefers-reduced-motion: reduce) that disables or stills this animation. See src/components/ui/user-preferences.css.`,
55
- alpha: (value) =>
56
- `Color value "${value}" uses an alpha channel. Add an opaque fallback in @media (prefers-reduced-transparency: reduce). See src/components/ui/user-preferences.css.`,
57
- };
58
-
59
- const meta = { url: 'https://github.com/a11yfred/neighbor' };
60
-
61
- /**
62
- * Collect all selectors that appear inside a prefers-reduced-* or forced-colors
63
- * media block anywhere in the file. Used to suppress warnings when an override exists.
64
- * Splits comma-separated selector lists so each individual selector is tracked.
65
- */
66
- function collectCoveredSelectors(root) {
67
- const covered = new Set();
68
- root.walkAtRules('media', (atRule) => {
69
- if (!/prefers-|forced-colors/.test(atRule.params)) return;
70
- atRule.walkRules((ruleNode) => {
71
- // Split comma-separated selector lists
72
- for (const part of ruleNode.selector.split(',')) {
73
- const sel = part.trim();
74
- covered.add(sel);
75
- // Also add bare selector without pseudo-classes/pseudo-elements
76
- covered.add(sel.replace(/::[^,\s{]+|:[^,\s{(]+(\([^)]*\))?/g, '').trim());
77
- }
78
- });
79
- });
80
- return covered;
81
- }
82
-
83
- /** True if the given selector (or its bare form) is in the covered set */
84
- function isCovered(selector, covered) {
85
- // Split comma lists in the base selector too
86
- for (const part of selector.split(',')) {
87
- const norm = part.trim();
88
- if (covered.has(norm)) return true;
89
- const bare = norm.replace(/::[^,\s{]+|:[^,\s{(]+(\([^)]*\))?/g, '').trim();
90
- if (covered.has(bare)) return true;
91
- }
92
- return false;
93
- }
94
-
95
- /** @type {import('stylelint').Rule} */
96
- function rule(primaryOption) {
97
- return (root, result) => {
98
- // Only enforce inside src/components/ui/
99
- const filePath = (root.source?.input?.file ?? '').replace(/\\/g, '/');
100
- if (!filePath.includes('src/components/ui')) return;
101
- // Never enforce inside user-preferences.css itself
102
- if (filePath.includes('user-preferences.css')) return;
103
-
104
- // Pre-scan: collect selectors already covered by prefers overrides in this file
105
- const covered = collectCoveredSelectors(root);
106
-
107
- root.walkDecls((decl) => {
108
- if (insidePreferencesMedia(decl)) return;
109
-
110
- // If the containing rule's selector is already overridden in a prefers block, skip
111
- const parentSelector = decl.parent?.selector ?? '';
112
- if (parentSelector && isCovered(parentSelector, covered)) return;
113
-
114
- const prop = decl.prop.toLowerCase();
115
- const value = decl.value;
116
-
117
- // opacity — warn on non-structural values (i.e. dims like 0.5, 0.75)
118
- if (prop === 'opacity' && !isStructuralOpacity(value)) {
119
- decl.warn(result, messages.opacity(value), { rule: ruleName });
120
- return;
121
- }
122
-
123
- // animation or transition
124
- if (prop === 'animation' || prop === 'transition' || prop === 'animation-name') {
125
- // Skip "none" values they're already the reduced state
126
- if (/^none\b/i.test(value.trim())) return;
127
- decl.warn(result, messages.animation(prop, value), { rule: ruleName });
128
- return;
129
- }
130
-
131
- // Alpha-channel color values on visual properties
132
- const visualProps = new Set([
133
- 'background', 'background-color', 'color', 'border', 'border-color',
134
- 'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',
135
- 'outline-color', 'box-shadow', 'text-shadow', 'fill', 'stroke',
136
- ]);
137
- if (visualProps.has(prop) && hasAlphaChannel(value)) {
138
- decl.warn(result, messages.alpha(value), { rule: ruleName });
139
- }
140
- });
141
- };
142
- }
143
-
144
- const userPreferences = { ruleName, rule, meta };
145
-
146
- // ─── Rule: ulam/no-outline-none ──────────────────────────────────────────────
147
- // outline: none / outline: 0 removes the browser's default keyboard focus
148
- // indicator. This is one of the most common keyboard accessibility failures —
149
- // keyboard users lose all visual indication of where focus is.
150
- //
151
- // Only fires when the declaration is NOT inside a :focus-visible, :focus, or
152
- // :focus-within selector, and no sibling :focus-visible rule overrides it in
153
- // the same block.
154
- //
155
- // Ref: WCAG 2.4.7 (Focus Visible); WebAIM; Roselli; cross-practitioner consensus
156
-
157
- const noOutlineNoneRuleName = 'ulam/no-outline-none';
158
-
159
- const noOutlineNoneMessages = {
160
- removed: (value) =>
161
- `outline: ${value} removes the keyboard focus indicator. Add a :focus-visible rule with a visible outline or custom focus style. (WCAG 2.4.7 / WebAIM)`,
162
- };
163
-
164
- const noOutlineNoneMeta = { url: 'https://github.com/a11yfred/neighbor' };
165
-
166
- /** Returns true if the selector string targets a focus state. */
167
- function isFocusSelector(selector) {
168
- return /:focus(?:-visible|-within)?/i.test(selector);
169
- }
170
-
171
- /** @type {import('stylelint').Rule} */
172
- function noOutlineNoneRule(_primaryOption) {
173
- return (root, result) => {
174
- root.walkDecls(/^outline$/i, (decl) => {
175
- const value = decl.value.trim().toLowerCase();
176
- if (value !== 'none' && value !== '0') return;
177
-
178
- // If this declaration is already inside a :focus / :focus-visible rule, it's fine —
179
- // the author is intentionally restyling focus, which is acceptable as long as they
180
- // provide an alternative (we can't verify the alternative statically, so we allow it).
181
- const parent = decl.parent;
182
- if (parent?.type === 'rule' && isFocusSelector(parent.selector ?? '')) return;
183
-
184
- // Flag it
185
- decl.warn(result, noOutlineNoneMessages.removed(decl.value), { rule: noOutlineNoneRuleName });
186
- });
187
- };
188
- }
189
-
190
- const noOutlineNone = {
191
- ruleName: noOutlineNoneRuleName,
192
- rule: noOutlineNoneRule,
193
- meta: noOutlineNoneMeta,
194
- };
195
-
196
- // ─── Rule: ulam/no-forced-colors-none ────────────────────────────────────────
197
- // forced-color-adjust: none inside @media (forced-colors) actively opts out of
198
- // Windows High Contrast Mode and other forced-colors user settings. For users
199
- // who depend on forced colors this is their last resort for viewing content —
200
- // overriding it is a serious accessibility regression.
201
- //
202
- // Legitimate narrow exceptions exist (e.g. color pickers where all swatches
203
- // would collapse to CanvasText). Those should be scoped tightly to the specific
204
- // element, not a whole section, and are typically few enough to suppress inline.
205
- //
206
- // Ref: Sarah Higley — forced-color-adjust: none (sarahmhigley.com)
207
- // Adrian Roselli WHCM and System Colors (adrianroselli.com)
208
- // WCAG SC 1.4.11 Non-text Contrast; SC 1.4.3 Contrast (Minimum)
209
-
210
- const noForcedColorsNoneRuleName = 'ulam/no-forced-colors-none';
211
-
212
- const noForcedColorsNoneMessages = {
213
- none: (selector) =>
214
- `forced-color-adjust: none on "${selector}" inside @media (forced-colors) opts out of ` +
215
- `Windows High Contrast Mode, removing all forced-color overrides for these elements. ` +
216
- `Users who depend on forced colors lose visibility entirely. ` +
217
- `Remove forced-color-adjust: none, or scope it to the narrowest possible element ` +
218
- `(e.g. a color-picker swatch) and add a comment explaining why. ` +
219
- `(Higley / Roselli WCAG SC 1.4.11 / SC 1.4.3)`,
220
- };
221
-
222
- const noForcedColorsNoneMeta = { url: 'https://github.com/a11yfred/neighbor' };
223
-
224
- /** Returns true if the node is directly inside a @media (forced-colors) block. */
225
- function insideForcedColorsMedia(node) {
226
- let current = node.parent;
227
- while (current) {
228
- if (
229
- current.type === 'atrule' &&
230
- current.name === 'media' &&
231
- /forced-colors/.test(current.params)
232
- ) return true;
233
- current = current.parent;
234
- }
235
- return false;
236
- }
237
-
238
- /** @type {import('stylelint').Rule} */
239
- function noForcedColorsNoneRule(_primaryOption) {
240
- return (root, result) => {
241
- root.walkDecls(/^forced-color-adjust$/i, (decl) => {
242
- if (decl.value.trim().toLowerCase() !== 'none') return;
243
- if (!insideForcedColorsMedia(decl)) return;
244
- const selector = decl.parent?.selector ?? decl.parent?.name ?? '(unknown)';
245
- decl.warn(result, noForcedColorsNoneMessages.none(selector), { rule: noForcedColorsNoneRuleName });
246
- });
247
- };
248
- }
249
-
250
- const noForcedColorsNone = {
251
- ruleName: noForcedColorsNoneRuleName,
252
- rule: noForcedColorsNoneRule,
253
- meta: noForcedColorsNoneMeta,
254
- };
255
-
256
- export default [userPreferences, noOutlineNone, noForcedColorsNone];
1
+ /**
2
+ * @a11yfred/neighbor - Stylelint plugin
3
+ *
4
+ * Rules:
5
+ * neighbor/user-preferences - Warn when motion, transparency, or alpha colors
6
+ * are used without @media (prefers-*) fallbacks.
7
+ * neighbor/no-outline-none - Disallow bare outline:none/0 outside :focus selectors.
8
+ * neighbor/no-forced-colors-none - Disallow forced-color-adjust:none inside @media (forced-colors).
9
+ *
10
+ * Sources and credits:
11
+ * WCAG 2.1 / 2.2 w3.org/TR/WCAG21, w3.org/TR/WCAG22
12
+ * WebAIM webaim.org
13
+ * double-great/stylelint-a11y github.com/double-great/stylelint-a11y
14
+ */
15
+
16
+ const defined = (x) => x !== undefined && x !== null;
17
+
18
+ /** True if the node or any ancestor is a prefers-* / forced-colors media block */
19
+ function insidePreferencesMedia(node) {
20
+ let current = node.parent;
21
+ while (defined(current)) {
22
+ if (
23
+ current.type === 'atrule' &&
24
+ current.name === 'media' &&
25
+ /prefers-|forced-colors/.test(current.params)
26
+ ) {
27
+ return true;
28
+ }
29
+ current = current.parent;
30
+ }
31
+ return false;
32
+ }
33
+
34
+ /** True if the value string contains an alpha channel (rgb/hsl with slash, or 8-digit hex) */
35
+ function hasAlphaChannel(value) {
36
+ // rgb(r g b / a) or rgba() or hsl(h s l / a)
37
+ if (/\b(rgb|hsl)a?\s*\(/.test(value) && /\/\s*[01]?\.?\d+[^)]*\)/.test(value)) return true;
38
+ // 8-digit hex #rrggbbaa
39
+ if (/#[0-9a-fA-F]{8}\b/.test(value)) return true;
40
+ return false;
41
+ }
42
+
43
+ /** True if the opacity value is a structural endpoint (0 or 1), not a dim */
44
+ function isStructuralOpacity(value) {
45
+ const n = parseFloat(value.trim());
46
+ return n === 0 || n === 1;
47
+ }
48
+
49
+ const ruleName = 'neighbor/user-preferences';
50
+
51
+ const messages = {
52
+ opacity: (value) =>
53
+ `opacity: ${value} creates a transparency effect. Add a fallback in @media (prefers-reduced-transparency: reduce) that uses an explicit color token instead. See src/components/ui/user-preferences.css.`,
54
+ animation: (prop, value) =>
55
+ `${prop}: ${value} uses motion. Add a fallback in @media (prefers-reduced-motion: reduce) that disables or stills this animation. See src/components/ui/user-preferences.css.`,
56
+ alpha: (value) =>
57
+ `Color value "${value}" uses an alpha channel. Add an opaque fallback in @media (prefers-reduced-transparency: reduce). See src/components/ui/user-preferences.css.`,
58
+ };
59
+
60
+ const meta = { url: 'https://github.com/a11yfred/neighbor' };
61
+
62
+ /**
63
+ * Collect all selectors that appear inside a prefers-reduced-* or forced-colors
64
+ * media block anywhere in the file. Used to suppress warnings when an override exists.
65
+ * Splits comma-separated selector lists so each individual selector is tracked.
66
+ */
67
+ function collectCoveredSelectors(root) {
68
+ const covered = new Set();
69
+ root.walkAtRules('media', (atRule) => {
70
+ if (!/prefers-|forced-colors/.test(atRule.params)) return;
71
+ atRule.walkRules((ruleNode) => {
72
+ // Split comma-separated selector lists
73
+ for (const part of ruleNode.selector.split(',')) {
74
+ const sel = part.trim();
75
+ covered.add(sel);
76
+ // Also add bare selector without pseudo-classes/pseudo-elements
77
+ covered.add(sel.replace(/::[^,\s{]+|:[^,\s{(]+(\([^)]*\))?/g, '').trim());
78
+ }
79
+ });
80
+ });
81
+ return covered;
82
+ }
83
+
84
+ /** True if the given selector (or its bare form) is in the covered set */
85
+ function isCovered(selector, covered) {
86
+ // Split comma lists in the base selector too
87
+ for (const part of selector.split(',')) {
88
+ const norm = part.trim();
89
+ if (covered.has(norm)) return true;
90
+ const bare = norm.replace(/::[^,\s{]+|:[^,\s{(]+(\([^)]*\))?/g, '').trim();
91
+ if (covered.has(bare)) return true;
92
+ }
93
+ return false;
94
+ }
95
+
96
+ /** @type {import('stylelint').Rule} */
97
+ function rule(primaryOption) {
98
+ return (root, result) => {
99
+ // Only enforce inside src/components/ui/
100
+ const filePath = (root.source?.input?.file ?? '').replace(/\\/g, '/');
101
+ if (!filePath.includes('src/components/ui')) return;
102
+ // Never enforce inside user-preferences.css itself
103
+ if (filePath.includes('user-preferences.css')) return;
104
+
105
+ // Pre-scan: collect selectors already covered by prefers overrides in this file
106
+ const covered = collectCoveredSelectors(root);
107
+
108
+ root.walkDecls((decl) => {
109
+ if (insidePreferencesMedia(decl)) return;
110
+
111
+ // If the containing rule's selector is already overridden in a prefers block, skip
112
+ const parentSelector = decl.parent?.selector ?? '';
113
+ if (parentSelector && isCovered(parentSelector, covered)) return;
114
+
115
+ const prop = decl.prop.toLowerCase();
116
+ const value = decl.value;
117
+
118
+ // opacity - warn on non-structural values (i.e. dims like 0.5, 0.75)
119
+ if (prop === 'opacity' && !isStructuralOpacity(value)) {
120
+ decl.warn(result, messages.opacity(value), { rule: ruleName });
121
+ return;
122
+ }
123
+
124
+ // animation or transition
125
+ if (prop === 'animation' || prop === 'transition' || prop === 'animation-name') {
126
+ // Skip "none" values - they're already the reduced state
127
+ if (/^none\b/i.test(value.trim())) return;
128
+ decl.warn(result, messages.animation(prop, value), { rule: ruleName });
129
+ return;
130
+ }
131
+
132
+ // Alpha-channel color values on visual properties
133
+ const visualProps = new Set([
134
+ 'background', 'background-color', 'color', 'border', 'border-color',
135
+ 'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',
136
+ 'outline-color', 'box-shadow', 'text-shadow', 'fill', 'stroke',
137
+ ]);
138
+ if (visualProps.has(prop) && hasAlphaChannel(value)) {
139
+ decl.warn(result, messages.alpha(value), { rule: ruleName });
140
+ }
141
+ });
142
+ };
143
+ }
144
+
145
+ const userPreferences = { ruleName, rule, meta };
146
+
147
+ // ─── Rule: neighbor/no-outline-none ──────────────────────────────────────────
148
+ // outline: none / outline: 0 removes the browser's default keyboard focus
149
+ // indicator. This is one of the most common keyboard accessibility failures -
150
+ // keyboard users lose all visual indication of where focus is.
151
+ //
152
+ // Only fires when the declaration is NOT inside a :focus-visible, :focus, or
153
+ // :focus-within selector, and no sibling :focus-visible rule overrides it in
154
+ // the same block.
155
+ //
156
+ // Ref: WCAG 2.4.7 (Focus Visible); WebAIM; Roselli; cross-practitioner consensus
157
+
158
+ const noOutlineNoneRuleName = 'neighbor/no-outline-none';
159
+
160
+ const noOutlineNoneMessages = {
161
+ removed: (value) =>
162
+ `outline: ${value} removes the keyboard focus indicator. Add a :focus-visible rule with a visible outline or custom focus style. (WCAG 2.4.7 / WebAIM)`,
163
+ };
164
+
165
+ const noOutlineNoneMeta = { url: 'https://github.com/a11yfred/neighbor' };
166
+
167
+ /** Returns true if the selector string targets a focus state. */
168
+ function isFocusSelector(selector) {
169
+ return /:focus(?:-visible|-within)?/i.test(selector);
170
+ }
171
+
172
+ /** @type {import('stylelint').Rule} */
173
+ function noOutlineNoneRule(_primaryOption) {
174
+ return (root, result) => {
175
+ root.walkDecls(/^outline$/i, (decl) => {
176
+ const value = decl.value.trim().toLowerCase();
177
+ if (value !== 'none' && value !== '0') return;
178
+
179
+ // If this declaration is already inside a :focus / :focus-visible rule, it's fine -
180
+ // the author is intentionally restyling focus, which is acceptable as long as they
181
+ // provide an alternative (we can't verify the alternative statically, so we allow it).
182
+ const parent = decl.parent;
183
+ if (parent?.type === 'rule' && isFocusSelector(parent.selector ?? '')) return;
184
+
185
+ // Flag it
186
+ decl.warn(result, noOutlineNoneMessages.removed(decl.value), { rule: noOutlineNoneRuleName });
187
+ });
188
+ };
189
+ }
190
+
191
+ const noOutlineNone = {
192
+ ruleName: noOutlineNoneRuleName,
193
+ rule: noOutlineNoneRule,
194
+ meta: noOutlineNoneMeta,
195
+ };
196
+
197
+ // ─── Rule: neighbor/no-forced-colors-none ────────────────────────────────────
198
+ // forced-color-adjust: none inside @media (forced-colors) actively opts out of
199
+ // Windows High Contrast Mode and other forced-colors user settings. For users
200
+ // who depend on forced colors this is their last resort for viewing content -
201
+ // overriding it is a serious accessibility regression.
202
+ //
203
+ // Legitimate narrow exceptions exist (e.g. color pickers where all swatches
204
+ // would collapse to CanvasText). Those should be scoped tightly to the specific
205
+ // element, not a whole section, and are typically few enough to suppress inline.
206
+ //
207
+ // Ref: Sarah Higley - forced-color-adjust: none (sarahmhigley.com)
208
+ // Adrian Roselli - WHCM and System Colors (adrianroselli.com)
209
+ // WCAG SC 1.4.11 Non-text Contrast; SC 1.4.3 Contrast (Minimum)
210
+
211
+ const noForcedColorsNoneRuleName = 'neighbor/no-forced-colors-none';
212
+
213
+ const noForcedColorsNoneMessages = {
214
+ none: (selector) =>
215
+ `forced-color-adjust: none on "${selector}" inside @media (forced-colors) opts out of ` +
216
+ `Windows High Contrast Mode, removing all forced-color overrides for these elements. ` +
217
+ `Users who depend on forced colors lose visibility entirely. ` +
218
+ `Remove forced-color-adjust: none, or scope it to the narrowest possible element ` +
219
+ `(e.g. a color-picker swatch) and add a comment explaining why. ` +
220
+ `(Higley / Roselli - WCAG SC 1.4.11 / SC 1.4.3)`,
221
+ };
222
+
223
+ const noForcedColorsNoneMeta = { url: 'https://github.com/a11yfred/neighbor' };
224
+
225
+ /** Returns true if the node is directly inside a @media (forced-colors) block. */
226
+ function insideForcedColorsMedia(node) {
227
+ let current = node.parent;
228
+ while (current) {
229
+ if (
230
+ current.type === 'atrule' &&
231
+ current.name === 'media' &&
232
+ /forced-colors/.test(current.params)
233
+ ) return true;
234
+ current = current.parent;
235
+ }
236
+ return false;
237
+ }
238
+
239
+ /** @type {import('stylelint').Rule} */
240
+ function noForcedColorsNoneRule(_primaryOption) {
241
+ return (root, result) => {
242
+ root.walkDecls(/^forced-color-adjust$/i, (decl) => {
243
+ if (decl.value.trim().toLowerCase() !== 'none') return;
244
+ if (!insideForcedColorsMedia(decl)) return;
245
+ const selector = decl.parent?.selector ?? decl.parent?.name ?? '(unknown)';
246
+ decl.warn(result, noForcedColorsNoneMessages.none(selector), { rule: noForcedColorsNoneRuleName });
247
+ });
248
+ };
249
+ }
250
+
251
+ const noForcedColorsNone = {
252
+ ruleName: noForcedColorsNoneRuleName,
253
+ rule: noForcedColorsNoneRule,
254
+ meta: noForcedColorsNoneMeta,
255
+ };
256
+
257
+ export default [userPreferences, noOutlineNone, noForcedColorsNone];
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@a11yfred/neighbor",
3
- "version": "0.3.0",
4
- "description": "Accessibility linting plugin for a11yfred Stylelint (user-preference fallbacks) and ESLint (bad ARIA patterns). Won't you be my neighbor?",
3
+ "version": "1.0.1",
4
+ "description": "Accessibility linting for a11yfred - ESLint (markup + content), Stylelint (CSS). Won't you be my neighbor?",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
- "files": ["lib", "*.mjs", "LICENSE", "README.md", "CHANGELOG.md", "CONTRIBUTING.md"],
7
+ "files": ["lib", "*.mjs", "LICENSE", "README.md", "CHANGELOG.md", "CONTRIBUTING.md", "RULES.md", "RULES-MARKUP.md", "RULES-CSS.md", "RULES-CONTENT.md"],
8
8
  "main": "./neighbor-stylelint.mjs",
9
9
  "repository": {
10
10
  "type": "git",
@@ -19,7 +19,8 @@
19
19
  "./stylelint": "./neighbor-stylelint.mjs",
20
20
  "./eslint": "./neighbor-eslint.mjs",
21
21
  "./eslint-vue": "./neighbor-eslint-vue.mjs",
22
- "./eslint-angular": "./neighbor-eslint-angular.mjs"
22
+ "./eslint-angular": "./neighbor-eslint-angular.mjs",
23
+ "./content": "./neighbor-content.mjs"
23
24
  },
24
25
  "keywords": [
25
26
  "stylelint",
@@ -30,7 +31,11 @@
30
31
  "css",
31
32
  "aria",
32
33
  "accessibility",
33
- "a11y"
34
+ "a11y",
35
+ "content",
36
+ "plain-language",
37
+ "inclusive-language",
38
+ "ableist"
34
39
  ],
35
40
  "peerDependencies": {
36
41
  "@angular-eslint/eslint-plugin-template": ">=17",
@@ -43,6 +48,9 @@
43
48
  "eslint": {
44
49
  "optional": true
45
50
  },
51
+ "eslint-plugin-jsx-a11y": {
52
+ "optional": true
53
+ },
46
54
  "eslint-plugin-vuejs-accessibility": {
47
55
  "optional": true
48
56
  },
@@ -53,6 +61,11 @@
53
61
  "optional": true
54
62
  }
55
63
  },
64
+ "overrides": {
65
+ "eslint-plugin-jsx-a11y": {
66
+ "eslint": ">=8"
67
+ }
68
+ },
56
69
  "devDependencies": {
57
70
  "eslint": "^9.39.4",
58
71
  "eslint-plugin-jsx-a11y": "^6.10.2",