@accesslint/storybook-addon 0.6.7 → 0.6.9

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 CHANGED
@@ -14,31 +14,65 @@ Catch accessibility violations in your Storybook stories as you develop. Powered
14
14
  npm install @accesslint/storybook-addon
15
15
  ```
16
16
 
17
- Then add it to your `.storybook/main.ts` (or `.storybook/main.js`):
17
+ Add the addon to your `.storybook/main.ts` (or `.storybook/main.js`):
18
18
 
19
19
  ```ts
20
20
  const config = {
21
- addons: ["@accesslint/storybook-addon"],
21
+ addons: ["@storybook/addon-vitest", "@accesslint/storybook-addon"],
22
22
  };
23
23
 
24
24
  export default config;
25
25
  ```
26
26
 
27
- That's it. Restart Storybook and an **AccessLint** panel will appear in the addon bar.
27
+ Add the vitest plugin to your `vite.config.ts`:
28
+
29
+ ```ts
30
+ import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
31
+ import { accesslintTest } from "@accesslint/storybook-addon/vitest-plugin";
32
+
33
+ export default defineConfig({
34
+ plugins: [
35
+ storybookTest({ configDir: ".storybook" }),
36
+ accesslintTest(),
37
+ ],
38
+ });
39
+ ```
40
+
41
+ Restart Storybook and an **AccessLint** panel will appear in the addon bar.
28
42
 
29
43
  ## Usage
30
44
 
31
- The addon automatically audits each story on render and displays violations sorted by severity. Expand any violation to see:
45
+ The addon audits each story after it renders and displays violations sorted by severity. Expand any violation to see:
32
46
 
33
47
  - **Impact level** — critical, serious, moderate, or minor
34
48
  - **WCAG criteria** and conformance level (A, AA, AAA)
35
49
  - **How to fix** guidance for each rule
36
50
  - **Element HTML** snippet of the failing element
37
51
 
38
- Selecting a violation highlights the affected element in the story preview.
39
-
40
52
  ## Configuration
41
53
 
54
+ ### Parameters
55
+
56
+ Control AccessLint behavior per-story or globally via `parameters.accesslint`:
57
+
58
+ ```ts
59
+ // .storybook/preview.ts
60
+ const preview = {
61
+ parameters: {
62
+ accesslint: {
63
+ // 'todo' - show violations as warnings in the test UI
64
+ // 'error' - fail CI on violations
65
+ // 'off' - skip checks entirely
66
+ test: "todo",
67
+ },
68
+ },
69
+ };
70
+
71
+ export default preview;
72
+ ```
73
+
74
+ ### Disabling rules
75
+
42
76
  Disable specific rules in your preview file:
43
77
 
44
78
  ```ts
package/dist/manager.js CHANGED
@@ -1,14 +1,14 @@
1
- import React, { useMemo, useState, useRef, useEffect, useCallback } from 'react';
2
- import { addons, types, useAddonState, useChannel, useStorybookApi } from 'storybook/internal/manager-api';
3
- import { ActionList, AddonPanel, Form } from 'storybook/internal/components';
1
+ import React, { useMemo, useState, useRef, useCallback } from 'react';
2
+ import { addons, types, useChannel, useStorybookApi } from 'storybook/internal/manager-api';
3
+ import { ActionList, Form } from 'storybook/internal/components';
4
4
  import { styled, useTheme } from 'storybook/internal/theming';
5
+ import { STORY_CHANGED, STORY_FINISHED } from 'storybook/internal/core-events';
5
6
 
6
7
  // src/manager.tsx
7
8
 
8
9
  // src/constants.ts
9
10
  var ADDON_ID = "accesslint/a11y";
10
- var PANEL_ID = `${ADDON_ID}/panel`;
11
- var TEST_PROVIDER_ID = `${ADDON_ID}/test-provider`;
11
+ var PARAM_KEY = "accesslint";
12
12
  var IMPACT_COLOR = {
13
13
  critical: "#d32f2f",
14
14
  serious: "#d32f2f",
@@ -32,8 +32,7 @@ var LEVEL_COLOR = {
32
32
  AA: "#1565c0",
33
33
  AAA: "#6a1b9a"
34
34
  };
35
- var HIGHLIGHT_ID = `${ADDON_ID}/highlight`;
36
- var Panel = ({ active, ...rest }) => {
35
+ var Panel = ({ active }) => {
37
36
  const theme = useTheme();
38
37
  const isDark = theme.base === "dark";
39
38
  const colors = useMemo(() => ({
@@ -49,41 +48,29 @@ var Panel = ({ active, ...rest }) => {
49
48
  tagText: isDark ? "#ccc" : "#616161",
50
49
  ruleId: isDark ? "#64b5f6" : "#1565c0"
51
50
  }), [isDark, theme]);
52
- const [violations, setViolations] = useAddonState(
53
- ADDON_ID,
54
- []
55
- );
56
- const [meta, setMeta] = useState(null);
51
+ const [violations, setViolations] = useState([]);
52
+ const [ruleCount, setRuleCount] = useState(0);
57
53
  const [expandedIndex, setExpandedIndex] = useState(null);
58
54
  const buttonRefs = useRef([]);
59
- const emit = useChannel({
60
- [`${ADDON_ID}/results`]: (results) => {
61
- setViolations(results);
55
+ useChannel({
56
+ [STORY_FINISHED]: ({ reporters }) => {
57
+ const report = reporters.find((r) => r.type === "accesslint");
58
+ if (!report) return;
59
+ const result = report.result;
60
+ setViolations(result.violations ?? []);
61
+ setRuleCount(result.ruleCount ?? 0);
62
62
  setExpandedIndex(null);
63
63
  },
64
- [`${ADDON_ID}/meta`]: (data) => {
65
- setMeta(data);
64
+ [STORY_CHANGED]: () => {
65
+ setViolations([]);
66
+ setRuleCount(0);
67
+ setExpandedIndex(null);
66
68
  }
67
69
  });
68
- const sorted = [...violations].sort(
69
- (a, b) => (IMPACT_ORDER[a.impact] ?? 4) - (IMPACT_ORDER[b.impact] ?? 4)
70
+ const sorted = useMemo(
71
+ () => [...violations].sort((a, b) => (IMPACT_ORDER[a.impact] ?? 4) - (IMPACT_ORDER[b.impact] ?? 4)),
72
+ [violations]
70
73
  );
71
- const expanded = expandedIndex !== null ? sorted[expandedIndex] : null;
72
- useEffect(() => {
73
- if (expanded?.selector) {
74
- const local = expanded.selector.replace(/^.*>>>\s*iframe>\s*/, "");
75
- emit("storybook/highlight/add", {
76
- id: HIGHLIGHT_ID,
77
- selectors: [local],
78
- styles: {
79
- outline: `2px solid ${IMPACT_COLOR[expanded.impact] || "#1565c0"}`,
80
- outlineOffset: "2px"
81
- }
82
- });
83
- } else {
84
- emit("storybook/highlight/remove", { id: HIGHLIGHT_ID });
85
- }
86
- }, [expandedIndex]);
87
74
  const handleKeyDown = useCallback((e, index) => {
88
75
  let next = null;
89
76
  switch (e.key) {
@@ -106,7 +93,8 @@ var Panel = ({ active, ...rest }) => {
106
93
  buttonRefs.current[next]?.focus();
107
94
  }, [sorted.length]);
108
95
  if (!active) return null;
109
- return /* @__PURE__ */ React.createElement(AddonPanel, { active, ...rest }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", height: "100%", fontFamily: "system-ui, sans-serif" } }, meta && /* @__PURE__ */ React.createElement("div", { style: {
96
+ const passed = ruleCount - new Set(violations.map((v) => v.ruleId)).size;
97
+ return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", height: "100%", fontFamily: "system-ui, sans-serif" } }, ruleCount > 0 && /* @__PURE__ */ React.createElement("div", { style: {
110
98
  display: "flex",
111
99
  gap: "12px",
112
100
  padding: "8px 12px",
@@ -114,7 +102,7 @@ var Panel = ({ active, ...rest }) => {
114
102
  color: colors.textMuted,
115
103
  borderBottom: `1px solid ${colors.border}`,
116
104
  flexShrink: 0
117
- } }, /* @__PURE__ */ React.createElement("span", null, meta.ruleCount, " rules"), /* @__PURE__ */ React.createElement("span", { style: { color: "#2e7d32" } }, meta.passed, " passed"), /* @__PURE__ */ React.createElement("span", { style: { color: meta.failed > 0 ? "#d32f2f" : colors.textMuted } }, meta.failed, " failed"), /* @__PURE__ */ React.createElement("span", null, meta.duration, "ms")), violations.length === 0 ? /* @__PURE__ */ React.createElement("p", { style: { padding: "12px", margin: 0, fontSize: "13px", color: colors.textMuted } }, "No accessibility violations found.") : /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", minHeight: 0 } }, /* @__PURE__ */ React.createElement(
105
+ } }, /* @__PURE__ */ React.createElement("span", null, ruleCount, " rules"), /* @__PURE__ */ React.createElement("span", { style: { color: "#2e7d32" } }, passed, " passed"), /* @__PURE__ */ React.createElement("span", { style: { color: violations.length > 0 ? "#d32f2f" : colors.textMuted } }, new Set(violations.map((v) => v.ruleId)).size, " failed")), violations.length === 0 ? /* @__PURE__ */ React.createElement("p", { style: { padding: "12px", margin: 0, fontSize: "13px", color: colors.textMuted } }, "No accessibility violations found.") : /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", minHeight: 0 } }, /* @__PURE__ */ React.createElement(
118
106
  "ul",
119
107
  {
120
108
  style: { listStyle: "none", padding: 0, margin: 0 },
@@ -201,13 +189,21 @@ var Panel = ({ active, ...rest }) => {
201
189
  v.html
202
190
  ))));
203
191
  })
204
- ))));
192
+ )));
205
193
  };
206
194
 
207
195
  // src/manager.tsx
196
+ var PANEL_ID = `${ADDON_ID}/panel`;
197
+ var TEST_PROVIDER_ID = `${ADDON_ID}/test-provider`;
208
198
  var Title = () => {
209
- const [violations] = useAddonState(ADDON_ID, []);
210
- const count = violations.length;
199
+ const [count, setCount] = React.useState(0);
200
+ useChannel({
201
+ [STORY_FINISHED]: ({ reporters }) => {
202
+ const report = reporters.find((r) => r.type === "accesslint");
203
+ const violations = report?.result?.violations;
204
+ setCount(violations?.length ?? 0);
205
+ }
206
+ });
211
207
  return /* @__PURE__ */ React.createElement(React.Fragment, null, "AccessLint", count > 0 && /* @__PURE__ */ React.createElement("span", { style: {
212
208
  display: "inline-block",
213
209
  marginLeft: "8px",
@@ -236,9 +232,6 @@ var StatusDot = styled.div(
236
232
  ({ status, theme }) => status === "positive" && {
237
233
  "--status-color": theme.color.positive
238
234
  },
239
- ({ status, theme }) => status === "warning" && {
240
- "--status-color": theme.color.gold
241
- },
242
235
  ({ status, theme }) => status === "negative" && {
243
236
  "--status-color": theme.color.negative
244
237
  },
@@ -247,15 +240,18 @@ var StatusDot = styled.div(
247
240
  }
248
241
  );
249
242
  var TestProviderWidget = () => {
250
- const [meta, setMeta] = useState(null);
243
+ const [violationCount, setViolationCount] = React.useState(null);
251
244
  const api = useStorybookApi();
252
245
  useChannel({
253
- [`${ADDON_ID}/meta`]: (data) => {
254
- setMeta(data);
246
+ [STORY_FINISHED]: ({ reporters }) => {
247
+ const report = reporters.find((r) => r.type === "accesslint");
248
+ if (!report) return;
249
+ const violations = report.result?.violations;
250
+ setViolationCount(violations?.length ?? 0);
255
251
  }
256
252
  });
257
- const hasViolations = meta !== null && meta.violations > 0;
258
- const status = meta === null ? "unknown" : hasViolations ? "negative" : "positive";
253
+ const hasViolations = violationCount !== null && violationCount > 0;
254
+ const status = violationCount === null ? "unknown" : hasViolations ? "negative" : "positive";
259
255
  const openPanel = () => {
260
256
  api.setSelectedPanel(PANEL_ID);
261
257
  api.togglePanel(true);
@@ -263,19 +259,20 @@ var TestProviderWidget = () => {
263
259
  return /* @__PURE__ */ React.createElement(StyledActionList, null, /* @__PURE__ */ React.createElement(ActionList.Item, null, /* @__PURE__ */ React.createElement(ActionList.Action, { as: "label", readOnly: true }, /* @__PURE__ */ React.createElement(ActionList.Icon, null, /* @__PURE__ */ React.createElement(Form.Checkbox, { name: "AccessLint", checked: true, disabled: true })), /* @__PURE__ */ React.createElement(ActionList.Text, null, "AccessLint")), /* @__PURE__ */ React.createElement(
264
260
  ActionList.Button,
265
261
  {
266
- ariaLabel: meta === null ? "AccessLint: not run yet" : hasViolations ? `AccessLint: ${meta.violations} violation${meta.violations === 1 ? "" : "s"}` : "AccessLint: no violations",
267
- disabled: meta === null,
262
+ ariaLabel: violationCount === null ? "AccessLint: not run yet" : hasViolations ? `AccessLint: ${violationCount} violation${violationCount === 1 ? "" : "s"}` : "AccessLint: no violations",
263
+ disabled: violationCount === null,
268
264
  onClick: openPanel
269
265
  },
270
- hasViolations ? meta.violations : null,
266
+ hasViolations ? violationCount : null,
271
267
  /* @__PURE__ */ React.createElement(StatusDot, { status })
272
268
  )));
273
269
  };
274
270
  addons.register(ADDON_ID, () => {
275
271
  addons.add(PANEL_ID, {
276
- type: types.PANEL,
277
272
  title: Title,
278
- render: Panel
273
+ type: types.PANEL,
274
+ render: Panel,
275
+ paramKey: PARAM_KEY
279
276
  });
280
277
  addons.add(TEST_PROVIDER_ID, {
281
278
  type: types.experimental_TEST_PROVIDER,
package/dist/preview.cjs CHANGED
@@ -1,62 +1,60 @@
1
1
  'use strict';
2
2
 
3
- var previewApi = require('storybook/internal/preview-api');
4
3
  var core = require('@accesslint/core');
5
4
 
6
- // src/preview.ts
7
-
8
- // src/constants.ts
9
- var ADDON_ID = "accesslint/a11y";
10
-
11
5
  // src/preview.ts
12
6
  core.configureRules({
13
7
  disabledRules: ["accesslint-045"]
14
8
  });
15
- var decorator = (storyFn) => {
16
- const story = storyFn();
17
- setTimeout(() => {
9
+ function scopeViolations(violations) {
10
+ const root = document.getElementById("storybook-root");
11
+ if (!root) return violations;
12
+ return violations.filter((v) => {
13
+ const local = v.selector.replace(/^.*>>>\s*iframe>\s*/, "");
18
14
  try {
19
- const start = performance.now();
20
- const results = core.runAudit(document);
21
- const duration = Math.round(performance.now() - start);
22
- const root = document.getElementById("storybook-root");
23
- const scoped = root ? results.violations.filter((v) => {
24
- const local = v.selector.replace(/^.*>>>\s*iframe>\s*/, "");
25
- try {
26
- const el = document.querySelector(local);
27
- return el && root.contains(el);
28
- } catch {
29
- return false;
30
- }
31
- }) : results.violations;
32
- const enriched = scoped.map((v) => {
33
- const rule = core.getRuleById(v.ruleId);
34
- return {
35
- ...v,
36
- element: void 0,
37
- // not serializable
38
- description: rule?.description,
39
- wcag: rule?.wcag,
40
- level: rule?.level,
41
- guidance: rule?.guidance
42
- };
43
- });
44
- const failedRuleIds = new Set(scoped.map((v) => v.ruleId));
45
- const channel = previewApi.addons.getChannel();
46
- channel.emit(`${ADDON_ID}/results`, enriched);
47
- channel.emit(`${ADDON_ID}/meta`, {
48
- duration,
49
- ruleCount: results.ruleCount,
50
- failed: failedRuleIds.size,
51
- passed: results.ruleCount - failedRuleIds.size,
52
- violations: scoped.length
53
- });
54
- } catch (err) {
55
- console.error("[AccessLint] decorator error:", err);
15
+ const el = document.querySelector(local);
16
+ return el && root.contains(el);
17
+ } catch {
18
+ return false;
56
19
  }
57
- }, 0);
58
- return story;
20
+ });
21
+ }
22
+ function enrichViolations(violations) {
23
+ return violations.map((v) => {
24
+ const rule = core.getRuleById(v.ruleId);
25
+ return {
26
+ ...v,
27
+ element: void 0,
28
+ // not serializable
29
+ description: rule?.description,
30
+ wcag: rule?.wcag,
31
+ level: rule?.level,
32
+ guidance: rule?.guidance
33
+ };
34
+ });
35
+ }
36
+ var afterEach = async ({
37
+ reporting,
38
+ parameters,
39
+ viewMode
40
+ }) => {
41
+ const accesslintParam = parameters?.accesslint;
42
+ if (accesslintParam?.disable === true || accesslintParam?.test === "off") return;
43
+ if (viewMode !== "story") return;
44
+ const result = core.runAudit(document);
45
+ const scoped = scopeViolations(result.violations);
46
+ const enriched = enrichViolations(scoped);
47
+ const hasViolations = enriched.length > 0;
48
+ const mode = accesslintParam?.test === "todo" ? "warning" : "failed";
49
+ reporting.addReport({
50
+ type: "accesslint",
51
+ version: 1,
52
+ result: {
53
+ violations: enriched,
54
+ ruleCount: result.ruleCount
55
+ },
56
+ status: hasViolations ? mode : "passed"
57
+ });
59
58
  };
60
- var decorators = [decorator];
61
59
 
62
- exports.decorators = decorators;
60
+ exports.afterEach = afterEach;
@@ -1,3 +1,9 @@
1
- declare const decorators: ((storyFn: () => unknown) => unknown)[];
1
+ declare const afterEach: ({ reporting, parameters, viewMode, }: {
2
+ reporting: {
3
+ addReport: (report: Record<string, unknown>) => void;
4
+ };
5
+ parameters: Record<string, unknown>;
6
+ viewMode: string;
7
+ }) => Promise<void>;
2
8
 
3
- export { decorators };
9
+ export { afterEach };
package/dist/preview.d.ts CHANGED
@@ -1,3 +1,9 @@
1
- declare const decorators: ((storyFn: () => unknown) => unknown)[];
1
+ declare const afterEach: ({ reporting, parameters, viewMode, }: {
2
+ reporting: {
3
+ addReport: (report: Record<string, unknown>) => void;
4
+ };
5
+ parameters: Record<string, unknown>;
6
+ viewMode: string;
7
+ }) => Promise<void>;
2
8
 
3
- export { decorators };
9
+ export { afterEach };
package/dist/preview.js CHANGED
@@ -1,60 +1,58 @@
1
- import { addons } from 'storybook/internal/preview-api';
2
1
  import { configureRules, runAudit, getRuleById } from '@accesslint/core';
3
2
 
4
- // src/preview.ts
5
-
6
- // src/constants.ts
7
- var ADDON_ID = "accesslint/a11y";
8
-
9
3
  // src/preview.ts
10
4
  configureRules({
11
5
  disabledRules: ["accesslint-045"]
12
6
  });
13
- var decorator = (storyFn) => {
14
- const story = storyFn();
15
- setTimeout(() => {
7
+ function scopeViolations(violations) {
8
+ const root = document.getElementById("storybook-root");
9
+ if (!root) return violations;
10
+ return violations.filter((v) => {
11
+ const local = v.selector.replace(/^.*>>>\s*iframe>\s*/, "");
16
12
  try {
17
- const start = performance.now();
18
- const results = runAudit(document);
19
- const duration = Math.round(performance.now() - start);
20
- const root = document.getElementById("storybook-root");
21
- const scoped = root ? results.violations.filter((v) => {
22
- const local = v.selector.replace(/^.*>>>\s*iframe>\s*/, "");
23
- try {
24
- const el = document.querySelector(local);
25
- return el && root.contains(el);
26
- } catch {
27
- return false;
28
- }
29
- }) : results.violations;
30
- const enriched = scoped.map((v) => {
31
- const rule = getRuleById(v.ruleId);
32
- return {
33
- ...v,
34
- element: void 0,
35
- // not serializable
36
- description: rule?.description,
37
- wcag: rule?.wcag,
38
- level: rule?.level,
39
- guidance: rule?.guidance
40
- };
41
- });
42
- const failedRuleIds = new Set(scoped.map((v) => v.ruleId));
43
- const channel = addons.getChannel();
44
- channel.emit(`${ADDON_ID}/results`, enriched);
45
- channel.emit(`${ADDON_ID}/meta`, {
46
- duration,
47
- ruleCount: results.ruleCount,
48
- failed: failedRuleIds.size,
49
- passed: results.ruleCount - failedRuleIds.size,
50
- violations: scoped.length
51
- });
52
- } catch (err) {
53
- console.error("[AccessLint] decorator error:", err);
13
+ const el = document.querySelector(local);
14
+ return el && root.contains(el);
15
+ } catch {
16
+ return false;
54
17
  }
55
- }, 0);
56
- return story;
18
+ });
19
+ }
20
+ function enrichViolations(violations) {
21
+ return violations.map((v) => {
22
+ const rule = getRuleById(v.ruleId);
23
+ return {
24
+ ...v,
25
+ element: void 0,
26
+ // not serializable
27
+ description: rule?.description,
28
+ wcag: rule?.wcag,
29
+ level: rule?.level,
30
+ guidance: rule?.guidance
31
+ };
32
+ });
33
+ }
34
+ var afterEach = async ({
35
+ reporting,
36
+ parameters,
37
+ viewMode
38
+ }) => {
39
+ const accesslintParam = parameters?.accesslint;
40
+ if (accesslintParam?.disable === true || accesslintParam?.test === "off") return;
41
+ if (viewMode !== "story") return;
42
+ const result = runAudit(document);
43
+ const scoped = scopeViolations(result.violations);
44
+ const enriched = enrichViolations(scoped);
45
+ const hasViolations = enriched.length > 0;
46
+ const mode = accesslintParam?.test === "todo" ? "warning" : "failed";
47
+ reporting.addReport({
48
+ type: "accesslint",
49
+ version: 1,
50
+ result: {
51
+ violations: enriched,
52
+ ruleCount: result.ruleCount
53
+ },
54
+ status: hasViolations ? mode : "passed"
55
+ });
57
56
  };
58
- var decorators = [decorator];
59
57
 
60
- export { decorators };
58
+ export { afterEach };
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
4
+ // src/vitest-plugin.ts
5
+ function accesslintTest() {
6
+ const distDir = new URL(".", (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('vitest-plugin.cjs', document.baseURI).href))).pathname;
7
+ return {
8
+ name: "@accesslint/storybook-addon",
9
+ config() {
10
+ return {
11
+ server: {
12
+ fs: {
13
+ allow: [distDir]
14
+ }
15
+ },
16
+ test: {
17
+ setupFiles: ["@accesslint/storybook-addon/vitest-setup"]
18
+ }
19
+ };
20
+ }
21
+ };
22
+ }
23
+
24
+ exports.accesslintTest = accesslintTest;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Vitest plugin that automatically registers AccessLint's afterEach annotation
3
+ * so that running component tests produces per-story accessibility badges.
4
+ *
5
+ * Usage in vitest.config.ts (or the storybook vitest workspace):
6
+ *
7
+ * import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
8
+ * import { accesslintTest } from "@accesslint/storybook-addon/vitest-plugin";
9
+ *
10
+ * export default defineConfig({
11
+ * plugins: [storybookTest(), accesslintTest()],
12
+ * });
13
+ */
14
+ declare function accesslintTest(): {
15
+ name: string;
16
+ config: () => Record<string, unknown>;
17
+ };
18
+
19
+ export { accesslintTest };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Vitest plugin that automatically registers AccessLint's afterEach annotation
3
+ * so that running component tests produces per-story accessibility badges.
4
+ *
5
+ * Usage in vitest.config.ts (or the storybook vitest workspace):
6
+ *
7
+ * import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
8
+ * import { accesslintTest } from "@accesslint/storybook-addon/vitest-plugin";
9
+ *
10
+ * export default defineConfig({
11
+ * plugins: [storybookTest(), accesslintTest()],
12
+ * });
13
+ */
14
+ declare function accesslintTest(): {
15
+ name: string;
16
+ config: () => Record<string, unknown>;
17
+ };
18
+
19
+ export { accesslintTest };
@@ -0,0 +1,21 @@
1
+ // src/vitest-plugin.ts
2
+ function accesslintTest() {
3
+ const distDir = new URL(".", import.meta.url).pathname;
4
+ return {
5
+ name: "@accesslint/storybook-addon",
6
+ config() {
7
+ return {
8
+ server: {
9
+ fs: {
10
+ allow: [distDir]
11
+ }
12
+ },
13
+ test: {
14
+ setupFiles: ["@accesslint/storybook-addon/vitest-setup"]
15
+ }
16
+ };
17
+ }
18
+ };
19
+ }
20
+
21
+ export { accesslintTest };
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ var previewApi = require('storybook/preview-api');
4
+ var core = require('@accesslint/core');
5
+
6
+ var __defProp = Object.defineProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/preview.ts
13
+ var preview_exports = {};
14
+ __export(preview_exports, {
15
+ afterEach: () => afterEach
16
+ });
17
+ core.configureRules({
18
+ disabledRules: ["accesslint-045"]
19
+ });
20
+ function scopeViolations(violations) {
21
+ const root = document.getElementById("storybook-root");
22
+ if (!root) return violations;
23
+ return violations.filter((v) => {
24
+ const local = v.selector.replace(/^.*>>>\s*iframe>\s*/, "");
25
+ try {
26
+ const el = document.querySelector(local);
27
+ return el && root.contains(el);
28
+ } catch {
29
+ return false;
30
+ }
31
+ });
32
+ }
33
+ function enrichViolations(violations) {
34
+ return violations.map((v) => {
35
+ const rule = core.getRuleById(v.ruleId);
36
+ return {
37
+ ...v,
38
+ element: void 0,
39
+ // not serializable
40
+ description: rule?.description,
41
+ wcag: rule?.wcag,
42
+ level: rule?.level,
43
+ guidance: rule?.guidance
44
+ };
45
+ });
46
+ }
47
+ var afterEach = async ({
48
+ reporting,
49
+ parameters,
50
+ viewMode
51
+ }) => {
52
+ const accesslintParam = parameters?.accesslint;
53
+ if (accesslintParam?.disable === true || accesslintParam?.test === "off") return;
54
+ if (viewMode !== "story") return;
55
+ const result = core.runAudit(document);
56
+ const scoped = scopeViolations(result.violations);
57
+ const enriched = enrichViolations(scoped);
58
+ const hasViolations = enriched.length > 0;
59
+ const mode = accesslintParam?.test === "todo" ? "warning" : "failed";
60
+ reporting.addReport({
61
+ type: "accesslint",
62
+ version: 1,
63
+ result: {
64
+ violations: enriched,
65
+ ruleCount: result.ruleCount
66
+ },
67
+ status: hasViolations ? mode : "passed"
68
+ });
69
+ };
70
+
71
+ // src/vitest-setup.ts
72
+ var g = globalThis;
73
+ var existing = g.globalProjectAnnotations;
74
+ g.globalProjectAnnotations = existing ? previewApi.composeConfigs([existing, preview_exports]) : previewApi.composeConfigs([preview_exports]);
@@ -0,0 +1,72 @@
1
+ import { composeConfigs } from 'storybook/preview-api';
2
+ import { configureRules, runAudit, getRuleById } from '@accesslint/core';
3
+
4
+ var __defProp = Object.defineProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+
10
+ // src/preview.ts
11
+ var preview_exports = {};
12
+ __export(preview_exports, {
13
+ afterEach: () => afterEach
14
+ });
15
+ configureRules({
16
+ disabledRules: ["accesslint-045"]
17
+ });
18
+ function scopeViolations(violations) {
19
+ const root = document.getElementById("storybook-root");
20
+ if (!root) return violations;
21
+ return violations.filter((v) => {
22
+ const local = v.selector.replace(/^.*>>>\s*iframe>\s*/, "");
23
+ try {
24
+ const el = document.querySelector(local);
25
+ return el && root.contains(el);
26
+ } catch {
27
+ return false;
28
+ }
29
+ });
30
+ }
31
+ function enrichViolations(violations) {
32
+ return violations.map((v) => {
33
+ const rule = getRuleById(v.ruleId);
34
+ return {
35
+ ...v,
36
+ element: void 0,
37
+ // not serializable
38
+ description: rule?.description,
39
+ wcag: rule?.wcag,
40
+ level: rule?.level,
41
+ guidance: rule?.guidance
42
+ };
43
+ });
44
+ }
45
+ var afterEach = async ({
46
+ reporting,
47
+ parameters,
48
+ viewMode
49
+ }) => {
50
+ const accesslintParam = parameters?.accesslint;
51
+ if (accesslintParam?.disable === true || accesslintParam?.test === "off") return;
52
+ if (viewMode !== "story") return;
53
+ const result = runAudit(document);
54
+ const scoped = scopeViolations(result.violations);
55
+ const enriched = enrichViolations(scoped);
56
+ const hasViolations = enriched.length > 0;
57
+ const mode = accesslintParam?.test === "todo" ? "warning" : "failed";
58
+ reporting.addReport({
59
+ type: "accesslint",
60
+ version: 1,
61
+ result: {
62
+ violations: enriched,
63
+ ruleCount: result.ruleCount
64
+ },
65
+ status: hasViolations ? mode : "passed"
66
+ });
67
+ };
68
+
69
+ // src/vitest-setup.ts
70
+ var g = globalThis;
71
+ var existing = g.globalProjectAnnotations;
72
+ g.globalProjectAnnotations = existing ? composeConfigs([existing, preview_exports]) : composeConfigs([preview_exports]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@accesslint/storybook-addon",
3
- "version": "0.6.7",
3
+ "version": "0.6.9",
4
4
  "description": "Catch accessibility violations in your Storybook stories as you develop",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -24,6 +24,14 @@
24
24
  "./preview": {
25
25
  "import": "./dist/preview.js",
26
26
  "require": "./dist/preview.cjs"
27
+ },
28
+ "./vitest-plugin": {
29
+ "import": "./dist/vitest-plugin.js",
30
+ "require": "./dist/vitest-plugin.cjs"
31
+ },
32
+ "./vitest-setup": {
33
+ "import": "./dist/vitest-setup.js",
34
+ "require": "./dist/vitest-setup.cjs"
27
35
  }
28
36
  },
29
37
  "main": "dist/index.cjs",