@wcag-checkr/ci 1.0.0-rc.98 → 1.0.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.
Files changed (117) hide show
  1. package/dist/assets/ErrorBoundary-C_dGDDCo.js +594 -0
  2. package/dist/assets/ai-usage-log-CYyG5MT2.js +1 -0
  3. package/dist/assets/content-script.ts-DUFtdkRn.js +217 -0
  4. package/dist/assets/{content-script.ts-loader-BjJqq5Xq.js → content-script.ts-loader-BhZWdOHh.js} +1 -1
  5. package/dist/assets/copy-ai-fixer-prompt-B-MMXqXA.js +19 -0
  6. package/dist/assets/crash-reporter-KD5f4utB.js +4 -0
  7. package/dist/assets/design-system-audit-DpxJrxnb.js +1 -0
  8. package/dist/assets/devtools-panel-BmQF2Qi9.js +1 -0
  9. package/dist/assets/diff-RnwNJg4F.js +1 -0
  10. package/dist/assets/dom-criterion-analyzers-DoUaJV5C.js +8 -0
  11. package/dist/assets/fraunces-latin-400-normal-6IfK1voy.woff2 +0 -0
  12. package/dist/assets/fraunces-latin-400-normal-NUPT2cO8.woff +0 -0
  13. package/dist/assets/fraunces-latin-500-normal-BTR4KCeb.woff +0 -0
  14. package/dist/assets/fraunces-latin-500-normal-DnGCNyPD.woff2 +0 -0
  15. package/dist/assets/fraunces-latin-600-normal-BFCDtZfi.woff2 +0 -0
  16. package/dist/assets/fraunces-latin-600-normal-DL5QCzvS.woff +0 -0
  17. package/dist/assets/fraunces-latin-ext-400-normal-D8gbi3Gu.woff2 +0 -0
  18. package/dist/assets/fraunces-latin-ext-400-normal-UihxqfOe.woff +0 -0
  19. package/dist/assets/fraunces-latin-ext-500-normal-BMcFk1Xs.woff +0 -0
  20. package/dist/assets/fraunces-latin-ext-500-normal-Z5DV8IzT.woff2 +0 -0
  21. package/dist/assets/fraunces-latin-ext-600-normal-B0Dy4lqi.woff +0 -0
  22. package/dist/assets/fraunces-latin-ext-600-normal-BtzmzP0X.woff2 +0 -0
  23. package/dist/assets/fraunces-vietnamese-400-normal-B65MOf9T.woff +0 -0
  24. package/dist/assets/fraunces-vietnamese-400-normal-CvGt0Ybw.woff2 +0 -0
  25. package/dist/assets/fraunces-vietnamese-500-normal-B-KbxExq.woff +0 -0
  26. package/dist/assets/fraunces-vietnamese-500-normal-GOH_-EGq.woff2 +0 -0
  27. package/dist/assets/fraunces-vietnamese-600-normal-BjlAJixd.woff2 +0 -0
  28. package/dist/assets/fraunces-vietnamese-600-normal-DlAl5EAR.woff +0 -0
  29. package/dist/assets/geist-sans-latin-400-normal-BOaIZNA2.woff +0 -0
  30. package/dist/assets/geist-sans-latin-400-normal-gapTbOY8.woff2 +0 -0
  31. package/dist/assets/geist-sans-latin-500-normal-CN2lyvyL.woff +0 -0
  32. package/dist/assets/geist-sans-latin-500-normal-uokXdC-Q.woff2 +0 -0
  33. package/dist/assets/geist-sans-latin-600-normal-CA1yjETN.woff +0 -0
  34. package/dist/assets/geist-sans-latin-600-normal-DFOURf8L.woff2 +0 -0
  35. package/dist/assets/geist-sans-latin-700-normal-BmN9tIp5.woff2 +0 -0
  36. package/dist/assets/geist-sans-latin-700-normal-CjScfYeH.woff +0 -0
  37. package/dist/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  38. package/dist/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  39. package/dist/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
  40. package/dist/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
  41. package/dist/assets/jetbrains-mono-cyrillic-600-normal-8K4wrrwR.woff +0 -0
  42. package/dist/assets/jetbrains-mono-cyrillic-600-normal-EVf6-Yzo.woff2 +0 -0
  43. package/dist/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  44. package/dist/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  45. package/dist/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
  46. package/dist/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
  47. package/dist/assets/jetbrains-mono-greek-600-normal-H7WoG9Et.woff2 +0 -0
  48. package/dist/assets/jetbrains-mono-greek-600-normal-mc2nkWzM.woff +0 -0
  49. package/dist/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  50. package/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  51. package/dist/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  52. package/dist/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
  53. package/dist/assets/jetbrains-mono-latin-600-normal-BfsvjouI.woff +0 -0
  54. package/dist/assets/jetbrains-mono-latin-600-normal-C8RAYTDA.woff2 +0 -0
  55. package/dist/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  56. package/dist/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  57. package/dist/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
  58. package/dist/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
  59. package/dist/assets/jetbrains-mono-latin-ext-600-normal-BfB_LPfz.woff2 +0 -0
  60. package/dist/assets/jetbrains-mono-latin-ext-600-normal-DObL3zCW.woff +0 -0
  61. package/dist/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  62. package/dist/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
  63. package/dist/assets/jetbrains-mono-vietnamese-600-normal-OWROknRo.woff +0 -0
  64. package/dist/assets/offscreen-BAvM-6sh.js +1 -0
  65. package/dist/assets/options-D_6nRFjj.js +6 -0
  66. package/dist/assets/parallel-tab-flow-DjD6Nlor.js +1 -0
  67. package/dist/assets/pdf-DfaD4CCm.js +13 -0
  68. package/dist/assets/pdf.worker-B4IDxXWI.js +1 -0
  69. package/dist/assets/pdf.worker-ByF8NTMy.mjs +62201 -0
  70. package/dist/assets/rubric-config-client-DvjbA4nG.js +1 -0
  71. package/dist/assets/scheduled-audit-runner-ZCuDk8-r.js +2019 -0
  72. package/dist/assets/service-worker.ts-CvO1EqpM.js +2 -0
  73. package/dist/assets/side-panel-Cq-w2yTn.css +1 -0
  74. package/dist/assets/side-panel-DaVp_XgU.js +4 -0
  75. package/dist/assets/site-report-renderer-MWuVmKOh.js +248 -0
  76. package/dist/assets/styles-BJICDw6s.css +1 -0
  77. package/dist/assets/{styles-DhnON7zr.js → styles-B_kTykZT.js} +12 -12
  78. package/dist/assets/tier-config-client-85XOCE5d.js +1 -0
  79. package/dist/assets/zip-encoder-CtULHXx_.js +1 -0
  80. package/dist/devtools/panel.html +9 -7
  81. package/dist/manifest.json +10 -11
  82. package/dist/offscreen/offscreen.html +12 -0
  83. package/dist/options/options.html +6 -6
  84. package/dist/service-worker-loader.js +1 -1
  85. package/dist/side-panel/side-panel.html +13 -7
  86. package/package.json +2 -1
  87. package/wcagcheckr-ci.mjs +238 -39
  88. package/dist/assets/ErrorBoundary-C2WBadbU.js +0 -544
  89. package/dist/assets/ai-usage-log-Dj9Ub_DT.js +0 -1
  90. package/dist/assets/content-script.ts-vGsvD70K.js +0 -181
  91. package/dist/assets/copy-ai-fixer-prompt-DDYcXAe7.js +0 -19
  92. package/dist/assets/crash-reporter-Dc5lvxvY.js +0 -4
  93. package/dist/assets/devtools-panel-CSy65hq-.js +0 -1
  94. package/dist/assets/diff-DIBMr3fQ.js +0 -1
  95. package/dist/assets/dom-criterion-analyzers-C2gMp3xS.js +0 -1
  96. package/dist/assets/forensic-log-D-KXUwm8.js +0 -129
  97. package/dist/assets/options-Byz5A0JD.js +0 -6
  98. package/dist/assets/parallel-tab-flow-BUjkjlHH.js +0 -1
  99. package/dist/assets/service-worker.ts-DwSYhM1S.js +0 -954
  100. package/dist/assets/side-panel-DIo5JGlH.js +0 -1
  101. package/dist/assets/site-report-renderer-D8y25osg.js +0 -147
  102. package/dist/assets/state-BIsozXtc.js +0 -1
  103. package/dist/assets/styles-Bh3rgVbO.css +0 -1
  104. package/dist/side-panel/App.tsx +0 -193
  105. package/dist/side-panel/README.md +0 -57
  106. package/dist/side-panel/audit-launcher.test.ts +0 -56
  107. package/dist/side-panel/audit-launcher.ts +0 -65
  108. package/dist/side-panel/format-component-id.test.ts +0 -89
  109. package/dist/side-panel/format-component-id.ts +0 -40
  110. package/dist/side-panel/github-issue.test.ts +0 -102
  111. package/dist/side-panel/github-issue.ts +0 -66
  112. package/dist/side-panel/jira-issue.ts +0 -64
  113. package/dist/side-panel/main.tsx +0 -19
  114. package/dist/side-panel/store.ts +0 -289
  115. package/dist/side-panel/styles.css +0 -16
  116. package/dist/side-panel/wire-messaging.test.ts +0 -202
  117. package/dist/side-panel/wire-messaging.ts +0 -288
@@ -1,89 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { displayComponentId } from './format-component-id';
3
-
4
- describe('displayComponentId', () => {
5
- it('formats Storybook story id', () => {
6
- const r = displayComponentId('::story:atoms-button--primary');
7
- expect(r.primary).toBe('Atoms Button / Primary');
8
- expect(r.secondary).toBe('storybook');
9
- });
10
-
11
- it('formats testid with URL scope', () => {
12
- const r = displayComponentId('https://example.com/login::testid:email-input');
13
- expect(r.primary).toBe('email-input');
14
- expect(r.secondary).toBe('https://example.com/login');
15
- });
16
-
17
- it('formats explicit data-componentid', () => {
18
- const r = displayComponentId('https://example.com::explicit:my-card');
19
- expect(r.primary).toBe('my-card');
20
- expect(r.secondary).toBe('https://example.com');
21
- });
22
-
23
- it('formats custom element', () => {
24
- const r = displayComponentId('https://example.com::customelement:my-button');
25
- expect(r.primary).toBe('my-button');
26
- expect(r.secondary).toBe('https://example.com');
27
- });
28
-
29
- it('formats aria id with role:name', () => {
30
- const r = displayComponentId('https://example.com::aria:button:Submit form');
31
- // value contains a colon; we keep everything after the first colon as the value
32
- expect(r.primary).toBe('button:Submit form');
33
- expect(r.secondary).toBe('https://example.com');
34
- });
35
-
36
- it('falls back gracefully when no separator', () => {
37
- const r = displayComponentId('weird-id');
38
- expect(r.primary).toBe('weird-id');
39
- });
40
-
41
- it('handles aria value with multiple colons (kept verbatim after first colon)', () => {
42
- const r = displayComponentId('https://example.com::aria:button:Foo: bar: baz');
43
- // Strategy is "aria"; value is "button:Foo: bar: baz" (preserves colons after the first).
44
- expect(r.primary).toBe('button:Foo: bar: baz');
45
- expect(r.secondary).toBe('https://example.com');
46
- });
47
-
48
- it('handles unicode story names', () => {
49
- const r = displayComponentId('::story:atoms-bütton--primary');
50
- expect(r.primary).toBe('Atoms Bütton / Primary');
51
- });
52
-
53
- it('handles emoji in story names', () => {
54
- const r = displayComponentId('::story:atoms-button--🚀-primary');
55
- expect(r.primary).toContain('🚀');
56
- });
57
-
58
- it('handles very long story id without crashing', () => {
59
- const longSegment = 'a'.repeat(200);
60
- const r = displayComponentId(`::story:${longSegment}--variant`);
61
- expect(r.primary.length).toBeGreaterThan(0);
62
- expect(r.primary).toContain('Variant');
63
- });
64
-
65
- it('handles single-segment story id without "--" separator', () => {
66
- const r = displayComponentId('::story:single-segment-only');
67
- // No "--" separator → entire value is one segment, gets capitalized.
68
- expect(r.primary).toBe('Single Segment Only');
69
- });
70
-
71
- it('handles testid with special characters', () => {
72
- const r = displayComponentId('https://example.com::testid:btn-#$@-special');
73
- expect(r.primary).toBe('btn-#$@-special');
74
- expect(r.secondary).toBe('https://example.com');
75
- });
76
-
77
- it('handles empty url scope (storybook-style separator with no URL)', () => {
78
- const r = displayComponentId('::testid:button-x');
79
- expect(r.primary).toBe('button-x');
80
- // Empty URL scope falls back to strategy as the secondary label.
81
- expect(r.secondary).toBe('testid');
82
- });
83
-
84
- it('handles trailing :: with empty rest', () => {
85
- const r = displayComponentId('https://example.com::');
86
- // No strategy:value content after the separator; should not crash.
87
- expect(r.primary).toBe('');
88
- });
89
- });
@@ -1,40 +0,0 @@
1
- // Pretty-print a canonical componentId for display in the UI.
2
- // Spec: phase-1-specs.md §2 (componentId format).
3
-
4
- export type ComponentIdDisplay = {
5
- primary: string; // Headline label
6
- secondary?: string; // Smaller subtext (URL or strategy)
7
- };
8
-
9
- export function displayComponentId(id: string): ComponentIdDisplay {
10
- // Format: {urlScope}::{strategy}:{value} (urlScope may be empty for storybook)
11
- const sepIdx = id.indexOf('::');
12
- if (sepIdx === -1) {
13
- return { primary: id };
14
- }
15
- const url = id.slice(0, sepIdx);
16
- const rest = id.slice(sepIdx + 2);
17
-
18
- // rest is "{strategy}:{value}". Strategy is the first segment up to the FIRST colon.
19
- const colonIdx = rest.indexOf(':');
20
- if (colonIdx === -1) {
21
- return { primary: rest };
22
- }
23
- const strategy = rest.slice(0, colonIdx);
24
- const value = rest.slice(colonIdx + 1);
25
-
26
- if (strategy === 'story') {
27
- // storyId like "atoms-button--primary" → "Atoms / Button / Primary"
28
- const niceStory = value
29
- .split('--')
30
- .map((seg) => seg.split('-').map(cap).join(' '))
31
- .join(' / ');
32
- return { primary: niceStory, secondary: 'storybook' };
33
- }
34
-
35
- return { primary: value, secondary: url || strategy };
36
- }
37
-
38
- function cap(word: string): string {
39
- return word.length === 0 ? word : word[0]!.toUpperCase() + word.slice(1);
40
- }
@@ -1,102 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { buildGithubIssueUrl } from './github-issue';
3
- import { DEFAULT_BREAKPOINT_PRESETS } from '../types/state';
4
- import type { Violation } from '../types/audit';
5
-
6
- const desktop = DEFAULT_BREAKPOINT_PRESETS.find((p) => p.id === 'desktop')!;
7
-
8
- function v(overrides: Partial<Violation> = {}): Violation {
9
- return {
10
- ruleId: 'color-contrast',
11
- wcagCriterion: 'wcag143',
12
- wcagLevel: 'AA',
13
- impact: 'serious',
14
- description: 'Insufficient contrast',
15
- helpUrl: 'https://example.com/r',
16
- target: {
17
- selector: '#btn',
18
- outerHTML: '<button id="btn">Go</button>',
19
- failureSummary: '',
20
- tagName: 'BUTTON',
21
- role: null,
22
- accessibleName: 'Go',
23
- textSnippet: 'Go',
24
- attrId: 'btn',
25
- attrTestid: null,
26
- },
27
- componentId: '::cmp::btn',
28
- currentState: {
29
- pseudoState: 'default',
30
- ariaVariation: null,
31
- theme: 'light',
32
- direction: 'ltr',
33
- breakpoint: desktop,
34
- },
35
- axeVersion: '4.11.4',
36
- detectedAt: '2026-05-04T00:00:00Z',
37
- matchKey: 'mk-1',
38
- ...overrides,
39
- };
40
- }
41
-
42
- describe('buildGithubIssueUrl', () => {
43
- it('produces a /issues/new URL pointing at the configured repo', () => {
44
- const r = buildGithubIssueUrl('https://github.com/foo/bar', '::cmp::x', [v()]);
45
- expect(r.url.startsWith('https://github.com/foo/bar/issues/new?')).toBe(true);
46
- });
47
-
48
- it('strips trailing slash from repo URL', () => {
49
- const r = buildGithubIssueUrl('https://github.com/foo/bar/', '::x', [v()]);
50
- expect(r.url.startsWith('https://github.com/foo/bar/issues/new?')).toBe(true);
51
- });
52
-
53
- it('puts violation count in the title', () => {
54
- const r = buildGithubIssueUrl('https://github.com/foo/bar', '::x', [
55
- v({ matchKey: 'm1' }),
56
- v({ matchKey: 'm2', ruleId: 'image-alt' }),
57
- ]);
58
- expect(r.title).toMatch(/2 new violations/);
59
- });
60
-
61
- it('singular vs plural in title', () => {
62
- const r = buildGithubIssueUrl('https://github.com/foo/bar', '::x', [v()]);
63
- expect(r.title).toMatch(/1 new violation in/);
64
- expect(r.title).not.toMatch(/violations/);
65
- });
66
-
67
- it('dedupes violations sharing the same logical key (rule + selector)', () => {
68
- const a = v({ matchKey: 'm1', currentState: { ...v().currentState, pseudoState: 'hover' } });
69
- const b = v({ matchKey: 'm2', currentState: { ...v().currentState, pseudoState: 'focus' } });
70
- const r = buildGithubIssueUrl('https://github.com/foo/bar', '::x', [a, b]);
71
- // Title says 1 violation (deduped), body lists both states.
72
- expect(r.title).toMatch(/1 new violation/);
73
- expect(r.body).toMatch(/:hover/);
74
- expect(r.body).toMatch(/:focus/);
75
- });
76
-
77
- it('includes selector, helpUrl, and outerHTML in the body', () => {
78
- const r = buildGithubIssueUrl('https://github.com/foo/bar', '::x', [
79
- v({
80
- target: {
81
- ...v().target,
82
- selector: '.my-thing',
83
- outerHTML: '<button class="my-thing">x</button>',
84
- },
85
- helpUrl: 'https://example.com/help-link',
86
- }),
87
- ]);
88
- expect(r.body).toContain('.my-thing');
89
- expect(r.body).toContain('https://example.com/help-link');
90
- expect(r.body).toContain('<button class="my-thing">x</button>');
91
- });
92
-
93
- it('truncates the URL when it gets absurdly long', () => {
94
- const huge = 'x'.repeat(20000);
95
- const r = buildGithubIssueUrl('https://github.com/foo/bar', '::x', [
96
- v({
97
- target: { ...v().target, outerHTML: huge },
98
- }),
99
- ]);
100
- expect(r.url.length).toBeLessThanOrEqual(7800);
101
- });
102
- });
@@ -1,66 +0,0 @@
1
- // Builds a pre-filled GitHub new-issue URL from a set of NEW violations.
2
- // Pure function — extracted from DeltaView so the URL/body shape is testable.
3
-
4
- import type { Violation } from '../types/audit';
5
-
6
- export type IssueBuild = { url: string; title: string; body: string };
7
-
8
- const URL_LIMIT = 7800;
9
-
10
- export function buildGithubIssueUrl(
11
- repoUrl: string,
12
- componentId: string | null,
13
- newViolations: Violation[]
14
- ): IssueBuild {
15
- const cleaned = repoUrl.replace(/\/$/, '');
16
-
17
- // Dedup violations by logical key (rule + selector); aggregate states.
18
- const groups: Array<Violation & { _states: string[] }> = [];
19
- for (const v of newViolations) {
20
- const key = `${v.ruleId}::${v.target.selector}`;
21
- const stateLabel = `:${v.currentState.pseudoState} · ${v.currentState.theme} · ${v.currentState.direction}`;
22
- const existing = groups.find((g) => `${g.ruleId}::${g.target.selector}` === key);
23
- if (existing) {
24
- if (!existing._states.includes(stateLabel)) existing._states.push(stateLabel);
25
- continue;
26
- }
27
- groups.push({ ...v, _states: [stateLabel] });
28
- }
29
-
30
- const title = `a11y: ${groups.length} new violation${groups.length === 1 ? '' : 's'} in ${componentId ?? 'audited component'}`;
31
-
32
- const lines: string[] = [];
33
- lines.push(`**Component:** \`${componentId ?? 'unknown'}\``);
34
- lines.push('');
35
- lines.push(
36
- `Detected by WCAG Component Auditor as **new** vs the saved baseline — these are violations introduced since the last accepted baseline.`
37
- );
38
- lines.push('');
39
- lines.push('---');
40
- lines.push('');
41
- for (const v of groups) {
42
- lines.push(`### \`${v.ruleId}\` — ${v.impact}`);
43
- lines.push('');
44
- lines.push(v.description);
45
- lines.push('');
46
- lines.push(`- **WCAG:** ${v.wcagCriterion} (${v.wcagLevel})`);
47
- lines.push(`- **Selector:** \`${v.target.selector}\``);
48
- lines.push(`- **Found in state(s):** ${v._states.join(', ')}`);
49
- if (v.helpUrl) lines.push(`- **More info:** ${v.helpUrl}`);
50
- lines.push('');
51
- lines.push('```html');
52
- lines.push(v.target.outerHTML);
53
- lines.push('```');
54
- lines.push('');
55
- }
56
- lines.push('---');
57
- lines.push('');
58
- lines.push('_Filed via WCAG Component Auditor (delta-only — inherited debt not included)._');
59
-
60
- const body = lines.join('\n');
61
- const params = new URLSearchParams({ title, body });
62
- let url = `${cleaned}/issues/new?${params.toString()}`;
63
- if (url.length > URL_LIMIT) url = url.slice(0, URL_LIMIT);
64
-
65
- return { url, title, body };
66
- }
@@ -1,64 +0,0 @@
1
- // Builds a pre-filled Jira create-issue URL from a set of NEW violations.
2
- // Mirrors the GitHub flow but targets Jira Cloud's CreateIssue URL params.
3
- // Description field is plain text; we keep formatting minimal so it reads
4
- // cleanly in Jira's default text view (markdown won't render natively).
5
-
6
- import type { Violation } from '../types/audit';
7
-
8
- export type JiraIssueBuild = { url: string; summary: string; description: string };
9
-
10
- const URL_LIMIT = 7800;
11
-
12
- export function buildJiraIssueUrl(
13
- instanceUrl: string,
14
- componentId: string | null,
15
- newViolations: Violation[]
16
- ): JiraIssueBuild {
17
- const cleaned = instanceUrl.replace(/\/$/, '');
18
-
19
- // Dedup violations by logical key (rule + selector); aggregate states.
20
- const groups: Array<Violation & { _states: string[] }> = [];
21
- for (const v of newViolations) {
22
- const key = `${v.ruleId}::${v.target.selector}`;
23
- const stateLabel = `:${v.currentState.pseudoState} · ${v.currentState.theme} · ${v.currentState.direction}`;
24
- const existing = groups.find((g) => `${g.ruleId}::${g.target.selector}` === key);
25
- if (existing) {
26
- if (!existing._states.includes(stateLabel)) existing._states.push(stateLabel);
27
- continue;
28
- }
29
- groups.push({ ...v, _states: [stateLabel] });
30
- }
31
-
32
- const summary = `a11y: ${groups.length} new violation${groups.length === 1 ? '' : 's'} in ${componentId ?? 'audited component'}`;
33
-
34
- const lines: string[] = [];
35
- lines.push(`Component: ${componentId ?? 'unknown'}`);
36
- lines.push('');
37
- lines.push(
38
- `Detected by WCAG Component Auditor as NEW vs the saved baseline — these are violations introduced since the last accepted baseline.`
39
- );
40
- lines.push('');
41
- lines.push('---');
42
- lines.push('');
43
- for (const v of groups) {
44
- lines.push(`Rule: ${v.ruleId} (${v.impact})`);
45
- lines.push(`WCAG: ${v.wcagCriterion} ${v.wcagLevel}`);
46
- lines.push(`Description: ${v.description}`);
47
- lines.push(`Selector: ${v.target.selector}`);
48
- lines.push(`Found in state(s): ${v._states.join(', ')}`);
49
- if (v.helpUrl) lines.push(`More info: ${v.helpUrl}`);
50
- lines.push('');
51
- lines.push(`HTML: ${v.target.outerHTML}`);
52
- lines.push('');
53
- lines.push('---');
54
- lines.push('');
55
- }
56
- lines.push('Filed via WCAG Component Auditor (delta-only — inherited debt not included).');
57
-
58
- const description = lines.join('\n');
59
- const params = new URLSearchParams({ summary, description });
60
- let url = `${cleaned}/secure/CreateIssue!default.jspa?${params.toString()}`;
61
- if (url.length > URL_LIMIT) url = url.slice(0, URL_LIMIT);
62
-
63
- return { url, summary, description };
64
- }
@@ -1,19 +0,0 @@
1
- import React from 'react';
2
- import { createRoot } from 'react-dom/client';
3
- import { App } from './App';
4
- import { ErrorBoundary } from './components/ErrorBoundary';
5
- import { installCrashReporter } from '../modules/crash-reporter';
6
- import './styles.css';
7
-
8
- installCrashReporter('side-panel');
9
-
10
- const root = document.getElementById('root');
11
- if (!root) throw new Error('side-panel: #root not found');
12
-
13
- createRoot(root).render(
14
- <React.StrictMode>
15
- <ErrorBoundary>
16
- <App />
17
- </ErrorBoundary>
18
- </React.StrictMode>
19
- );
@@ -1,289 +0,0 @@
1
- // Zustand store for side panel state.
2
- // Spec: phase-1-specs.md §9.
3
-
4
- import { create } from 'zustand';
5
- import type { AuditResult, DeltaResult } from '../types/audit';
6
- import type { StateConfig } from '../types/state';
7
- import type { LicenseTier } from '../shared/messages';
8
- import type { InAppMessage } from '../modules/messages-client';
9
- import type { SiteCrawlReport } from '../shared/site-aggregator';
10
-
11
- export type AuditStatus = 'idle' | 'running' | 'complete' | 'failed' | 'interrupted';
12
- export type View = 'matrix' | 'report' | 'delta' | 'activity' | 'guided' | 'flows' | 'scorecard' | 'crawl' | 'forensic' | 'compliance';
13
- /** Two distinct personas: site owners (mom-and-pop, no code) vs developers
14
- * (full feature surface, CLI-friendly outputs, baselines, exports). */
15
- export type UserMode = 'owner' | 'dev';
16
-
17
- type Store = {
18
- // audit
19
- status: AuditStatus;
20
- progress: { current: number; total: number; currentState?: StateConfig } | null;
21
- results: AuditResult[];
22
- delta: DeltaResult | null;
23
- componentId: string | null;
24
- errorMessage: string | null;
25
- /** True only when results came from a scan that completed during this side-panel
26
- * session. False when results were rehydrated from `loadLastAudit` — in that case
27
- * the score UI should mark the grade as "stale, re-scan to refresh" rather than
28
- * imply it reflects the page's current state. */
29
- freshThisSession: boolean;
30
-
31
- // baselines
32
- baselineList: Array<{
33
- componentId: string;
34
- violationCount: number;
35
- lastUpdated: string;
36
- seenOnUrlsCount?: number;
37
- metrics?: import('../shared/messages').BaselineSummaryMetrics;
38
- }>;
39
-
40
- // license
41
- tier: LicenseTier;
42
- /** Trial-tier countdown. null = unknown / not on trial. */
43
- trialDaysRemaining: number | null;
44
- /** Team-tier seat consumption. null = server hasn't emitted seat info. */
45
- seatsUsed: number | null;
46
- seatsTotal: number | null;
47
- /** Server's actual plan code for the active license — `solo`, `solo-yearly`,
48
- * `solo-single-month`, `team`, `team-15`. Lets the UpgradeCard tell apart
49
- * the recurring SKUs from the one-time single-month pass even though they
50
- * all map to tier='solo' or tier='team' for capability gating. */
51
- planCode: string | null;
52
- /** Days until the active one-time license (e.g. solo-single-month) expires.
53
- * null for recurring subscriptions. */
54
- licenseDaysRemaining: number | null;
55
- /** True when Stripe reported the most recent invoice payment as failed.
56
- * License stays valid during the grace period but UI surfaces the
57
- * failure prominently so the user updates their card before access cuts. */
58
- pastDue: boolean;
59
-
60
- // in-app messages
61
- messages: InAppMessage[];
62
- unreadMessageCount: number;
63
- criticalUnacked: boolean;
64
-
65
- // ui
66
- view: View;
67
- /** Persona-driven UI mode. `null` = first-launch (wizard not yet answered). */
68
- userMode: UserMode | null;
69
-
70
- // site crawl
71
- siteCrawlStatus: 'idle' | 'running' | 'complete' | 'failed';
72
- siteCrawlProgress: { current: number; total: number; url: string; lastViolations?: number } | null;
73
- siteCrawlReport: SiteCrawlReport | null;
74
- siteCrawlError: string | null;
75
-
76
- // highlight pin: which violation row is currently showing its overlay on the page,
77
- // and whether its selector resolved (false → render "not in current state" hint).
78
- pinnedMatchKey: string | null;
79
- pinnedFound: boolean;
80
-
81
- // AI augmentation failure surface — shown as a dismissible banner above the
82
- // results area when an audit's AI portion errored despite a configured key.
83
- // Cleared whenever a new audit starts or completes cleanly.
84
- aiFailure: {
85
- severity: 'total' | 'partial';
86
- reason: string;
87
- checksAttempted: number;
88
- checksSucceeded: number;
89
- checksErrored: number;
90
- errorDetails: string[];
91
- } | null;
92
-
93
- // AI augmentation in-flight progress. Drives the post-matrix progress line:
94
- // matrix loop owns 1..N states, then AI augmentation takes over and the bar
95
- // shows e.g. "AI: 3/8 — Verifying alt text…". Cleared when AUDIT_COMPLETE
96
- // fires or a new scan starts.
97
- aiProgress: {
98
- currentCheckLabel: string;
99
- current: number;
100
- total: number;
101
- /** Optional sub-progress within the current check: which candidate is
102
- * being judged (e.g. image 3 of 10). When present the UI shows
103
- * "AI: 7/9 — Reviewing link clarity… (3/10)" so the user can see
104
- * movement inside a single check that judges many candidates. */
105
- candidatesDone?: number;
106
- candidatesTotal?: number;
107
- } | null;
108
-
109
- // actions
110
- setStatus: (s: AuditStatus) => void;
111
- /** Begin a new scan: flips status to 'running' AND clears prior results,
112
- * delta, error, and progress so the panel doesn't show stale data from the
113
- * previous site under the new "Scanning…" indicator. Use this from every
114
- * START_AUDIT dispatch site instead of setStatus('running') alone. */
115
- startNewScan: () => void;
116
- setProgress: (p: Store['progress']) => void;
117
- setResults: (results: AuditResult[], delta: DeltaResult | null, componentId: string) => void;
118
- setDelta: (delta: DeltaResult | null) => void;
119
- setError: (message: string) => void;
120
- setBaselineList: (list: Store['baselineList']) => void;
121
- /** Wipe the current audit (results, delta, componentId, error, progress).
122
- * Used by the user-triggered Clear button and by auto-clear when the user
123
- * navigates the audit target to a different URL. Baselines + forensic
124
- * history are NOT touched — those are persistent records. */
125
- clearResults: () => void;
126
- setTier: (
127
- tier: LicenseTier,
128
- extras?: {
129
- trialDaysRemaining?: number;
130
- seatsUsed?: number;
131
- seatsTotal?: number;
132
- planCode?: string | null;
133
- licenseDaysRemaining?: number;
134
- pastDue?: boolean;
135
- }
136
- ) => void;
137
- setView: (v: View) => void;
138
- setUserMode: (m: UserMode | null) => void;
139
- setSiteCrawlStatus: (s: Store['siteCrawlStatus']) => void;
140
- setSiteCrawlProgress: (p: Store['siteCrawlProgress']) => void;
141
- setSiteCrawlReport: (r: SiteCrawlReport | null) => void;
142
- setSiteCrawlError: (e: string | null) => void;
143
- setPinned: (matchKey: string | null, found?: boolean) => void;
144
- setAiFailure: (failure: Store['aiFailure']) => void;
145
- clearAiFailure: () => void;
146
- setAiProgress: (progress: Store['aiProgress']) => void;
147
- /** rc.37 — matchKeys the user has acknowledged as "not an issue." Loaded
148
- * from chrome.storage at mount. Every ViolationCard reads this directly
149
- * so the "Acknowledged" pill appears immediately on click, in every
150
- * view (Matrix, Delta, WCAG-em, Owner — anywhere a card renders),
151
- * before the SW round-trip to refresh the delta. */
152
- acknowledgedKeys: Set<string>;
153
- setAcknowledgedKeys: (keys: Set<string>) => void;
154
- markAcknowledged: (matchKey: string) => void;
155
- markUnacknowledged: (matchKey: string) => void;
156
- /** Replace the in-app messages list + counters. Called by wire-messaging
157
- * after a refresh from the server. */
158
- setMessages: (messages: InAppMessage[], unreadCount: number, criticalUnacked: boolean) => void;
159
- /** Mark a message as locally seen/dismissed/acknowledged so the UI updates
160
- * immediately without waiting for the next server refresh. */
161
- applyMessageAck: (messageId: number, action: 'seen' | 'dismissed' | 'acknowledged' | 'clicked') => void;
162
- };
163
-
164
- export const useStore = create<Store>((set) => ({
165
- status: 'idle',
166
- progress: null,
167
- results: [],
168
- delta: null,
169
- componentId: null,
170
- errorMessage: null,
171
- freshThisSession: false,
172
- baselineList: [],
173
- tier: 'trial',
174
- trialDaysRemaining: null,
175
- seatsUsed: null,
176
- seatsTotal: null,
177
- planCode: null,
178
- licenseDaysRemaining: null,
179
- pastDue: false,
180
- messages: [],
181
- unreadMessageCount: 0,
182
- criticalUnacked: false,
183
- view: 'matrix',
184
- userMode: null,
185
- siteCrawlStatus: 'idle',
186
- siteCrawlProgress: null,
187
- siteCrawlReport: null,
188
- siteCrawlError: null,
189
- pinnedMatchKey: null,
190
- pinnedFound: true,
191
- aiFailure: null,
192
- aiProgress: null,
193
- acknowledgedKeys: new Set<string>(),
194
-
195
- setStatus: (status) => set({ status }),
196
- startNewScan: () =>
197
- set({
198
- status: 'running',
199
- results: [],
200
- delta: null,
201
- errorMessage: null,
202
- progress: null,
203
- componentId: null,
204
- pinnedMatchKey: null,
205
- pinnedFound: true,
206
- aiFailure: null,
207
- aiProgress: null,
208
- // freshThisSession stays false until setResults fires.
209
- }),
210
- setProgress: (progress) => set({ progress }),
211
- setResults: (results, delta, componentId) =>
212
- set({
213
- results,
214
- delta,
215
- componentId,
216
- status: 'complete',
217
- errorMessage: null,
218
- freshThisSession: true,
219
- aiProgress: null,
220
- // New audit invalidates any prior pin.
221
- pinnedMatchKey: null,
222
- pinnedFound: true,
223
- }),
224
- setDelta: (delta) => set({ delta }),
225
- setError: (errorMessage) => set({ errorMessage, status: 'failed' }),
226
- setBaselineList: (baselineList) => set({ baselineList }),
227
- clearResults: () =>
228
- set({
229
- results: [],
230
- delta: null,
231
- componentId: null,
232
- errorMessage: null,
233
- progress: null,
234
- status: 'idle',
235
- freshThisSession: false,
236
- pinnedMatchKey: null,
237
- pinnedFound: true,
238
- aiFailure: null,
239
- aiProgress: null,
240
- }),
241
- setTier: (tier, extras) =>
242
- set({
243
- tier,
244
- trialDaysRemaining: extras?.trialDaysRemaining ?? null,
245
- seatsUsed: extras?.seatsUsed ?? null,
246
- seatsTotal: extras?.seatsTotal ?? null,
247
- planCode: extras?.planCode ?? null,
248
- licenseDaysRemaining: extras?.licenseDaysRemaining ?? null,
249
- pastDue: extras?.pastDue ?? false,
250
- }),
251
- setView: (view) => set({ view }),
252
- setUserMode: (userMode) => set({ userMode }),
253
- setSiteCrawlStatus: (siteCrawlStatus) => set({ siteCrawlStatus }),
254
- setSiteCrawlProgress: (siteCrawlProgress) => set({ siteCrawlProgress }),
255
- setSiteCrawlReport: (siteCrawlReport) => set({ siteCrawlReport }),
256
- setSiteCrawlError: (siteCrawlError) => set({ siteCrawlError }),
257
- setPinned: (matchKey, found = true) => set({ pinnedMatchKey: matchKey, pinnedFound: found }),
258
- setAiFailure: (aiFailure) => set({ aiFailure }),
259
- clearAiFailure: () => set({ aiFailure: null }),
260
- setAiProgress: (aiProgress) => set({ aiProgress }),
261
- setAcknowledgedKeys: (acknowledgedKeys) => set({ acknowledgedKeys }),
262
- markAcknowledged: (matchKey) =>
263
- set((s) => ({ acknowledgedKeys: new Set(s.acknowledgedKeys).add(matchKey) })),
264
- markUnacknowledged: (matchKey) =>
265
- set((s) => {
266
- const next = new Set(s.acknowledgedKeys);
267
- next.delete(matchKey);
268
- return { acknowledgedKeys: next };
269
- }),
270
- setMessages: (messages, unreadMessageCount, criticalUnacked) =>
271
- set({ messages, unreadMessageCount, criticalUnacked }),
272
- applyMessageAck: (messageId, action) =>
273
- set((s) => {
274
- const next = s.messages
275
- .map((m) => {
276
- if (m.id !== messageId) return m;
277
- if (action === 'seen') return { ...m, seen: true };
278
- if (action === 'dismissed') return { ...m, dismissed: true };
279
- if (action === 'acknowledged') return { ...m, acknowledged: true };
280
- if (action === 'clicked') return { ...m, clicked: true };
281
- return m;
282
- })
283
- // Drop fully-handled non-critical messages (mirrors server visibility).
284
- .filter((m) => (m.severity === 'critical' ? !m.acknowledged : !m.dismissed));
285
- const unread = next.filter((m) => !m.seen).length;
286
- const critUnacked = next.some((m) => m.severity === 'critical' && !m.acknowledged);
287
- return { messages: next, unreadMessageCount: unread, criticalUnacked: critUnacked };
288
- }),
289
- }));