@tanstack/devtools-a11y 0.0.1 → 0.1.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/LICENSE +21 -0
- package/dist/esm/core/components/IssueCard.d.ts +10 -0
- package/dist/esm/core/components/IssueCard.js +83 -0
- package/dist/esm/core/components/IssueCard.js.map +1 -0
- package/dist/esm/core/components/IssueList.d.ts +6 -0
- package/dist/esm/core/components/IssueList.js +134 -0
- package/dist/esm/core/components/IssueList.js.map +1 -0
- package/dist/esm/core/components/Settings.d.ts +6 -0
- package/dist/esm/core/components/Settings.js +251 -0
- package/dist/esm/core/components/Settings.js.map +1 -0
- package/dist/esm/core/components/Shell.d.ts +2 -0
- package/dist/esm/core/components/Shell.js +214 -0
- package/dist/esm/core/components/Shell.js.map +1 -0
- package/dist/esm/core/components/index.d.ts +2 -0
- package/dist/esm/core/components/index.js +14 -0
- package/dist/esm/core/components/index.js.map +1 -0
- package/dist/esm/core/contexts/allyContext.d.ts +17 -0
- package/dist/esm/core/contexts/allyContext.js +66 -0
- package/dist/esm/core/contexts/allyContext.js.map +1 -0
- package/dist/esm/core/core.d.ts +19 -0
- package/dist/esm/core/core.js +8 -0
- package/dist/esm/core/core.js.map +1 -0
- package/dist/esm/core/index.d.ts +9 -0
- package/dist/esm/core/index.js +9 -0
- package/dist/esm/core/index.js.map +1 -0
- package/dist/esm/core/production.d.ts +2 -0
- package/dist/esm/core/production.js +4 -0
- package/dist/esm/core/styles/styles.d.ts +85 -0
- package/dist/esm/core/styles/styles.js +547 -0
- package/dist/esm/core/styles/styles.js.map +1 -0
- package/dist/esm/core/types/types.d.ts +141 -0
- package/dist/esm/core/utils/ally-audit.utils.d.ts +19 -0
- package/dist/esm/core/utils/ally-audit.utils.js +226 -0
- package/dist/esm/core/utils/ally-audit.utils.js.map +1 -0
- package/dist/esm/core/utils/config.utils.d.ts +17 -0
- package/dist/esm/core/utils/config.utils.js +63 -0
- package/dist/esm/core/utils/config.utils.js.map +1 -0
- package/dist/esm/core/utils/custom-audit.utils.d.ts +13 -0
- package/dist/esm/core/utils/custom-audit.utils.js +426 -0
- package/dist/esm/core/utils/custom-audit.utils.js.map +1 -0
- package/dist/esm/core/utils/export-audit.uitls.d.ts +17 -0
- package/dist/esm/core/utils/export-audit.uitls.js +83 -0
- package/dist/esm/core/utils/export-audit.uitls.js.map +1 -0
- package/dist/esm/core/utils/ui.utils.d.ts +24 -0
- package/dist/esm/core/utils/ui.utils.js +330 -0
- package/dist/esm/core/utils/ui.utils.js.map +1 -0
- package/dist/esm/react/A11yDevtools.d.ts +5 -0
- package/dist/esm/react/A11yDevtools.js +8 -0
- package/dist/esm/react/A11yDevtools.js.map +1 -0
- package/dist/esm/react/index.d.ts +8 -0
- package/dist/esm/react/index.js +11 -0
- package/dist/esm/react/index.js.map +1 -0
- package/dist/esm/react/plugin.d.ts +12 -0
- package/dist/esm/react/plugin.js +11 -0
- package/dist/esm/react/plugin.js.map +1 -0
- package/dist/esm/react/production/A11yDevtools.d.ts +5 -0
- package/dist/esm/react/production/A11yDevtools.js +8 -0
- package/dist/esm/react/production/A11yDevtools.js.map +1 -0
- package/dist/esm/react/production/plugin.d.ts +7 -0
- package/dist/esm/react/production/plugin.js +11 -0
- package/dist/esm/react/production/plugin.js.map +1 -0
- package/dist/esm/react/production.d.ts +3 -0
- package/dist/esm/react/production.js +5 -0
- package/dist/esm/solid/A11yDevtools.d.ts +5 -0
- package/dist/esm/solid/A11yDevtools.js +8 -0
- package/dist/esm/solid/A11yDevtools.js.map +1 -0
- package/dist/esm/solid/index.d.ts +8 -0
- package/dist/esm/solid/index.js +9 -0
- package/dist/esm/solid/index.js.map +1 -0
- package/dist/esm/solid/plugin.d.ts +12 -0
- package/dist/esm/solid/plugin.js +11 -0
- package/dist/esm/solid/plugin.js.map +1 -0
- package/dist/esm/solid/production/A11yDevtools.d.ts +5 -0
- package/dist/esm/solid/production/A11yDevtools.js +8 -0
- package/dist/esm/solid/production/A11yDevtools.js.map +1 -0
- package/dist/esm/solid/production/plugin.d.ts +7 -0
- package/dist/esm/solid/production/plugin.js +11 -0
- package/dist/esm/solid/production/plugin.js.map +1 -0
- package/dist/esm/solid/production.d.ts +3 -0
- package/dist/esm/solid/production.js +3 -0
- package/package.json +110 -7
- package/src/core/components/IssueCard.tsx +75 -0
- package/src/core/components/IssueList.tsx +155 -0
- package/src/core/components/Settings.tsx +221 -0
- package/src/core/components/Shell.tsx +154 -0
- package/src/core/components/index.tsx +12 -0
- package/src/core/contexts/allyContext.tsx +118 -0
- package/src/core/core.tsx +11 -0
- package/src/core/index.ts +10 -0
- package/src/core/production.ts +5 -0
- package/src/core/styles/styles.ts +556 -0
- package/src/core/types/types.ts +177 -0
- package/src/core/utils/ally-audit.utils.ts +345 -0
- package/src/core/utils/config.utils.ts +68 -0
- package/src/core/utils/custom-audit.utils.ts +643 -0
- package/src/core/utils/export-audit.uitls.ts +180 -0
- package/src/core/utils/ui.utils.ts +483 -0
- package/src/react/A11yDevtools.ts +12 -0
- package/src/react/index.ts +16 -0
- package/src/react/plugin.ts +9 -0
- package/src/react/production/A11yDevtools.ts +11 -0
- package/src/react/production/plugin.ts +9 -0
- package/src/react/production.ts +7 -0
- package/src/solid/A11yDevtools.ts +11 -0
- package/src/solid/index.ts +14 -0
- package/src/solid/plugin.ts +9 -0
- package/src/solid/production/A11yDevtools.ts +10 -0
- package/src/solid/production/plugin.ts +9 -0
- package/src/solid/production.ts +5 -0
- package/README.md +0 -45
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import axe from 'axe-core'
|
|
2
|
+
import {
|
|
3
|
+
getCustomRules as getCustomRulesInternal,
|
|
4
|
+
runCustomRules,
|
|
5
|
+
} from './custom-audit.utils.js'
|
|
6
|
+
import { SEVERITY_ORDER } from './ui.utils.js'
|
|
7
|
+
|
|
8
|
+
// types
|
|
9
|
+
import type { AxeResults, RuleObject, RunOptions } from 'axe-core'
|
|
10
|
+
import type {
|
|
11
|
+
A11yAuditOptions,
|
|
12
|
+
A11yAuditResult,
|
|
13
|
+
A11yIssue,
|
|
14
|
+
A11yNode,
|
|
15
|
+
A11ySummary,
|
|
16
|
+
CustomRulesConfig,
|
|
17
|
+
RuleSetPreset,
|
|
18
|
+
SeverityThreshold,
|
|
19
|
+
} from '../types/types.js'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Severity levels mapped to numeric values for comparison
|
|
23
|
+
*/
|
|
24
|
+
const IMPACT_SEVERITY: Record<SeverityThreshold, number> = {
|
|
25
|
+
critical: 4,
|
|
26
|
+
serious: 3,
|
|
27
|
+
moderate: 2,
|
|
28
|
+
minor: 1,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Rule set configurations for different presets
|
|
33
|
+
*/
|
|
34
|
+
const RULE_SET_CONFIGS: Record<RuleSetPreset, Partial<RunOptions>> = {
|
|
35
|
+
wcag2a: {
|
|
36
|
+
runOnly: {
|
|
37
|
+
type: 'tag',
|
|
38
|
+
values: ['wcag2a'],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
wcag2aa: {
|
|
42
|
+
runOnly: {
|
|
43
|
+
type: 'tag',
|
|
44
|
+
values: ['wcag2a', 'wcag2aa'],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
wcag21aa: {
|
|
48
|
+
runOnly: {
|
|
49
|
+
type: 'tag',
|
|
50
|
+
values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
wcag22aa: {
|
|
54
|
+
runOnly: {
|
|
55
|
+
type: 'tag',
|
|
56
|
+
values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
section508: {
|
|
60
|
+
runOnly: {
|
|
61
|
+
type: 'tag',
|
|
62
|
+
values: ['section508'],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
'best-practice': {
|
|
66
|
+
runOnly: {
|
|
67
|
+
type: 'tag',
|
|
68
|
+
values: ['best-practice'],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
all: {
|
|
72
|
+
// Run all rules
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if an impact level meets or exceeds the threshold
|
|
78
|
+
*/
|
|
79
|
+
export function meetsThreshold(
|
|
80
|
+
impact: SeverityThreshold | null | undefined,
|
|
81
|
+
threshold: SeverityThreshold,
|
|
82
|
+
): boolean {
|
|
83
|
+
if (!impact) return false
|
|
84
|
+
return IMPACT_SEVERITY[impact] >= IMPACT_SEVERITY[threshold]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Convert axe-core results to our issue format
|
|
89
|
+
*/
|
|
90
|
+
function convertToIssues(
|
|
91
|
+
results: AxeResults,
|
|
92
|
+
threshold: SeverityThreshold,
|
|
93
|
+
): Array<A11yIssue> {
|
|
94
|
+
const issues: Array<A11yIssue> = []
|
|
95
|
+
|
|
96
|
+
for (const violation of results.violations) {
|
|
97
|
+
const impact = violation.impact as SeverityThreshold | undefined
|
|
98
|
+
|
|
99
|
+
for (let i = 0; i < violation.nodes.length; i++) {
|
|
100
|
+
const node = violation.nodes[i]!
|
|
101
|
+
const selector = node.target.join(', ')
|
|
102
|
+
|
|
103
|
+
const a11yNode: A11yNode = {
|
|
104
|
+
selector,
|
|
105
|
+
html: node.html,
|
|
106
|
+
xpath: node.xpath?.join(' > '),
|
|
107
|
+
failureSummary: node.failureSummary,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
issues.push({
|
|
111
|
+
id: `${violation.id}-${i}-${Date.now()}`,
|
|
112
|
+
ruleId: violation.id,
|
|
113
|
+
impact: impact || 'minor',
|
|
114
|
+
message: node.failureSummary || violation.description,
|
|
115
|
+
help: violation.help,
|
|
116
|
+
helpUrl: violation.helpUrl,
|
|
117
|
+
wcagTags: violation.tags.filter(
|
|
118
|
+
(tag) => tag.startsWith('wcag') || tag.startsWith('section508'),
|
|
119
|
+
),
|
|
120
|
+
nodes: [a11yNode],
|
|
121
|
+
meetsThreshold: meetsThreshold(impact, threshold),
|
|
122
|
+
timestamp: Date.now(),
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return issues
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Create summary statistics from issues array
|
|
132
|
+
* Used when combining axe-core results with custom rule results
|
|
133
|
+
*/
|
|
134
|
+
function createSummary(
|
|
135
|
+
axeResults: AxeResults,
|
|
136
|
+
issues: Array<A11yIssue>,
|
|
137
|
+
): A11ySummary {
|
|
138
|
+
const summary: A11ySummary = {
|
|
139
|
+
total: issues.length,
|
|
140
|
+
critical: 0,
|
|
141
|
+
serious: 0,
|
|
142
|
+
moderate: 0,
|
|
143
|
+
minor: 0,
|
|
144
|
+
passes: axeResults.passes.length,
|
|
145
|
+
incomplete: axeResults.incomplete.length,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const issue of issues) {
|
|
149
|
+
const impact = issue.impact
|
|
150
|
+
if (impact === 'critical') summary.critical++
|
|
151
|
+
else if (impact === 'serious') summary.serious++
|
|
152
|
+
else if (impact === 'moderate') summary.moderate++
|
|
153
|
+
else {
|
|
154
|
+
summary.minor++
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return summary
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get the context description for logging
|
|
163
|
+
*/
|
|
164
|
+
function getContextDescription(context: Document | Element | string): string {
|
|
165
|
+
if (typeof context === 'string') {
|
|
166
|
+
return context
|
|
167
|
+
}
|
|
168
|
+
if (context instanceof Document) {
|
|
169
|
+
return 'document'
|
|
170
|
+
}
|
|
171
|
+
if (context instanceof Element) {
|
|
172
|
+
return context.tagName.toLowerCase() + (context.id ? `#${context.id}` : '')
|
|
173
|
+
}
|
|
174
|
+
return 'unknown'
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Default selectors to exclude from auditing (devtools panels, overlays, etc.)
|
|
179
|
+
*/
|
|
180
|
+
const DEFAULT_EXCLUDE_SELECTORS = [
|
|
181
|
+
// TanStack Devtools root container
|
|
182
|
+
'[data-testid="tanstack_devtools"]',
|
|
183
|
+
// A11y overlay elements
|
|
184
|
+
'[data-a11y-overlay]',
|
|
185
|
+
// Common devtools patterns
|
|
186
|
+
'[data-devtools]',
|
|
187
|
+
'[data-devtools-panel]',
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Run an accessibility audit using axe-core
|
|
192
|
+
*/
|
|
193
|
+
export async function runAudit(
|
|
194
|
+
options: A11yAuditOptions = {},
|
|
195
|
+
): Promise<A11yAuditResult> {
|
|
196
|
+
const {
|
|
197
|
+
threshold = 'serious',
|
|
198
|
+
context = document,
|
|
199
|
+
ruleSet = 'wcag21aa',
|
|
200
|
+
enabledRules,
|
|
201
|
+
disabledRules,
|
|
202
|
+
exclude = [],
|
|
203
|
+
customRules = {},
|
|
204
|
+
} = options
|
|
205
|
+
|
|
206
|
+
// Merge user exclusions with default devtools exclusions
|
|
207
|
+
const allExclusions = [...DEFAULT_EXCLUDE_SELECTORS, ...exclude]
|
|
208
|
+
|
|
209
|
+
const startTime = performance.now()
|
|
210
|
+
const contextDescription = getContextDescription(context)
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
// Build axe-core options
|
|
214
|
+
const axeOptions: RunOptions = {
|
|
215
|
+
resultTypes: ['violations', 'passes', 'incomplete'],
|
|
216
|
+
...RULE_SET_CONFIGS[ruleSet],
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Handle specific rule configurations
|
|
220
|
+
if (enabledRules && enabledRules.length > 0) {
|
|
221
|
+
axeOptions.runOnly = {
|
|
222
|
+
type: 'rule',
|
|
223
|
+
values: enabledRules,
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Build rules configuration for disabled rules
|
|
228
|
+
if (disabledRules && disabledRules.length > 0) {
|
|
229
|
+
const rules: RuleObject = {}
|
|
230
|
+
for (const ruleId of disabledRules) {
|
|
231
|
+
rules[ruleId] = { enabled: false }
|
|
232
|
+
}
|
|
233
|
+
axeOptions.rules = rules
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Determine the context to audit
|
|
237
|
+
let auditContext: axe.ElementContext = context as axe.ElementContext
|
|
238
|
+
|
|
239
|
+
// Add exclusions if specified (always include devtools exclusions)
|
|
240
|
+
if (allExclusions.length > 0) {
|
|
241
|
+
auditContext = {
|
|
242
|
+
include: [auditContext as Element],
|
|
243
|
+
exclude: allExclusions.map((sel) => [sel]),
|
|
244
|
+
} as axe.ElementContext
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Run the axe-core audit
|
|
248
|
+
const results = await axe.run(auditContext, axeOptions)
|
|
249
|
+
|
|
250
|
+
// Convert axe-core results to our format
|
|
251
|
+
const axeIssues = convertToIssues(results, threshold)
|
|
252
|
+
|
|
253
|
+
// Run custom rules (if not all disabled)
|
|
254
|
+
const customRulesConfig: CustomRulesConfig = {
|
|
255
|
+
clickHandlerOnNonInteractive:
|
|
256
|
+
customRules.clickHandlerOnNonInteractive !== false &&
|
|
257
|
+
!disabledRules?.includes('click-handler-on-non-interactive'),
|
|
258
|
+
mouseOnlyEventHandlers:
|
|
259
|
+
customRules.mouseOnlyEventHandlers !== false &&
|
|
260
|
+
!disabledRules?.includes('mouse-only-event-handlers'),
|
|
261
|
+
staticElementInteraction:
|
|
262
|
+
customRules.staticElementInteraction !== false &&
|
|
263
|
+
!disabledRules?.includes('static-element-interaction'),
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const contextElement =
|
|
267
|
+
typeof context === 'string'
|
|
268
|
+
? document.querySelector(context) || document
|
|
269
|
+
: context
|
|
270
|
+
|
|
271
|
+
const customIssues = runCustomRules(
|
|
272
|
+
contextElement,
|
|
273
|
+
customRulesConfig,
|
|
274
|
+
threshold,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
// Merge all issues
|
|
278
|
+
const allIssues = [...axeIssues, ...customIssues]
|
|
279
|
+
|
|
280
|
+
const duration = performance.now() - startTime
|
|
281
|
+
|
|
282
|
+
// Create summary from combined issues
|
|
283
|
+
const summary = createSummary(results, allIssues)
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
issues: allIssues,
|
|
287
|
+
summary,
|
|
288
|
+
timestamp: Date.now(),
|
|
289
|
+
url: typeof window !== 'undefined' ? window.location.href : '',
|
|
290
|
+
context: contextDescription,
|
|
291
|
+
duration,
|
|
292
|
+
}
|
|
293
|
+
} catch (error) {
|
|
294
|
+
const duration = performance.now() - startTime
|
|
295
|
+
console.error('[A11y Audit] Error running axe-core:', error)
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
issues: [],
|
|
299
|
+
summary: {
|
|
300
|
+
total: 0,
|
|
301
|
+
critical: 0,
|
|
302
|
+
serious: 0,
|
|
303
|
+
moderate: 0,
|
|
304
|
+
minor: 0,
|
|
305
|
+
passes: 0,
|
|
306
|
+
incomplete: 0,
|
|
307
|
+
},
|
|
308
|
+
timestamp: Date.now(),
|
|
309
|
+
url: typeof window !== 'undefined' ? window.location.href : '',
|
|
310
|
+
context: contextDescription,
|
|
311
|
+
duration,
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Get a list of all available axe-core rules plus custom rules
|
|
318
|
+
*/
|
|
319
|
+
export function getAvailableRules(): Array<{
|
|
320
|
+
id: string
|
|
321
|
+
description: string
|
|
322
|
+
tags: Array<string>
|
|
323
|
+
}> {
|
|
324
|
+
// Get axe-core rules
|
|
325
|
+
const axeRules = axe.getRules().map((rule) => ({
|
|
326
|
+
id: rule.ruleId,
|
|
327
|
+
description: rule.description,
|
|
328
|
+
tags: rule.tags,
|
|
329
|
+
}))
|
|
330
|
+
|
|
331
|
+
// Get custom rules
|
|
332
|
+
const customRules = getCustomRulesInternal()
|
|
333
|
+
|
|
334
|
+
return [...axeRules, ...customRules]
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export const IMPACTS = ['critical', 'serious', 'moderate', 'minor'] as const
|
|
338
|
+
|
|
339
|
+
export const filterIssuesAboveThreshold = (
|
|
340
|
+
issues: A11yAuditResult['issues'],
|
|
341
|
+
threshold: SeverityThreshold,
|
|
342
|
+
) =>
|
|
343
|
+
issues.filter(
|
|
344
|
+
(issue) => SEVERITY_ORDER[issue.impact] >= SEVERITY_ORDER[threshold],
|
|
345
|
+
)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { A11yPluginOptions } from '../types/types'
|
|
2
|
+
|
|
3
|
+
const STORAGE_KEY = 'tanstack-devtools-a11y-config'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Default plugin configuration
|
|
7
|
+
*/
|
|
8
|
+
export const DEFAULT_CONFIG: Required<A11yPluginOptions> = {
|
|
9
|
+
threshold: 'serious',
|
|
10
|
+
ruleSet: 'wcag21aa',
|
|
11
|
+
showOverlays: true,
|
|
12
|
+
persistSettings: true,
|
|
13
|
+
disabledRules: [],
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Load configuration from localStorage
|
|
18
|
+
*/
|
|
19
|
+
export function loadConfig(): Required<A11yPluginOptions> {
|
|
20
|
+
if (typeof localStorage === 'undefined') {
|
|
21
|
+
return DEFAULT_CONFIG
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const stored = localStorage.getItem(STORAGE_KEY)
|
|
26
|
+
if (stored) {
|
|
27
|
+
const parsed = JSON.parse(stored) as Partial<A11yPluginOptions>
|
|
28
|
+
return { ...DEFAULT_CONFIG, ...parsed }
|
|
29
|
+
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.warn(
|
|
32
|
+
'[A11y Config] Failed to load config from localStorage:',
|
|
33
|
+
error,
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return DEFAULT_CONFIG
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Save configuration to localStorage
|
|
42
|
+
*/
|
|
43
|
+
export function saveConfig(config: Partial<A11yPluginOptions>): void {
|
|
44
|
+
if (typeof localStorage === 'undefined') {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const current = loadConfig()
|
|
50
|
+
const updated = { ...current, ...config }
|
|
51
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated))
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.warn('[A11y Config] Failed to save config to localStorage:', error)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Merge user options with defaults
|
|
59
|
+
*/
|
|
60
|
+
export function mergeConfig(
|
|
61
|
+
options: A11yPluginOptions = {},
|
|
62
|
+
): Required<A11yPluginOptions> {
|
|
63
|
+
if (options.persistSettings !== false) {
|
|
64
|
+
const saved = loadConfig()
|
|
65
|
+
return { ...saved, ...options }
|
|
66
|
+
}
|
|
67
|
+
return { ...DEFAULT_CONFIG, ...options }
|
|
68
|
+
}
|