@wcag-checkr/ci 1.0.0-rc.31 → 1.0.0-rc.310

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 (104) hide show
  1. package/dist/assets/ErrorBoundary-C-kswn4E.js +594 -0
  2. package/dist/assets/ai-usage-log-BX3L6bKl.js +1 -0
  3. package/dist/assets/content-script.ts-FuMy_sE5.js +217 -0
  4. package/dist/assets/{content-script.ts-loader-Dfu1UEfD.js → content-script.ts-loader-CBHeu186.js} +1 -1
  5. package/dist/assets/copy-ai-fixer-prompt-DQYkHOv3.js +19 -0
  6. package/dist/assets/{crash-reporter-Dc5lvxvY.js → crash-reporter-Bu2p8K-p.js} +1 -1
  7. package/dist/assets/design-system-audit-DpxJrxnb.js +1 -0
  8. package/dist/assets/devtools-panel-DFQvqKKj.js +1 -0
  9. package/dist/assets/diff-DA41zYPc.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/options-BPhjrbGI.js +6 -0
  65. package/dist/assets/parallel-tab-flow-Xk9RSjay.js +1 -0
  66. package/dist/assets/scheduled-audit-runner-DyKpb3zg.js +2167 -0
  67. package/dist/assets/service-worker.ts-CMkltOzu.js +2 -0
  68. package/dist/assets/side-panel-Ctm2yXeo.css +1 -0
  69. package/dist/assets/side-panel-f_X4NOJt.js +4 -0
  70. package/dist/assets/site-report-renderer-DNgytqhZ.js +189 -0
  71. package/dist/assets/{styles-C4Kq0zOO.js → styles-Cn731SYD.js} +13 -13
  72. package/dist/assets/styles-d5msFsnl.css +1 -0
  73. package/dist/assets/zip-encoder-CtULHXx_.js +1 -0
  74. package/dist/axe.min.js +12 -0
  75. package/dist/devtools/panel.html +9 -8
  76. package/dist/manifest.json +11 -8
  77. package/dist/options/options.html +5 -6
  78. package/dist/service-worker-loader.js +1 -1
  79. package/dist/side-panel/App.tsx +129 -5
  80. package/dist/side-panel/audit-launcher.ts +21 -1
  81. package/dist/side-panel/azure-devops-issue.test.ts +68 -0
  82. package/dist/side-panel/azure-devops-issue.ts +89 -0
  83. package/dist/side-panel/gitlab-issue.test.ts +53 -0
  84. package/dist/side-panel/gitlab-issue.ts +78 -0
  85. package/dist/side-panel/main.tsx +39 -2
  86. package/dist/side-panel/side-panel.html +11 -8
  87. package/dist/side-panel/store.ts +149 -13
  88. package/dist/side-panel/styles.css +39 -0
  89. package/dist/side-panel/wire-messaging.ts +146 -9
  90. package/package.json +1 -1
  91. package/wcagcheckr-ci.mjs +193 -32
  92. package/dist/assets/ErrorBoundary-BLcMSVSr.js +0 -524
  93. package/dist/assets/ai-usage-log-Dj9Ub_DT.js +0 -1
  94. package/dist/assets/content-script.ts-CwcUMq3e.js +0 -181
  95. package/dist/assets/devtools-panel-DQ3Bbomf.js +0 -1
  96. package/dist/assets/diff-D4sCAdXf.js +0 -1
  97. package/dist/assets/forensic-log-B1UCXZ23.js +0 -129
  98. package/dist/assets/options-BG2i5vFf.js +0 -6
  99. package/dist/assets/preload-helper-D7HrI6pR.js +0 -1
  100. package/dist/assets/service-worker.ts-CO86CV_p.js +0 -715
  101. package/dist/assets/side-panel-XSB07vDa.js +0 -1
  102. package/dist/assets/site-report-renderer-CyHkM6hB.js +0 -147
  103. package/dist/assets/state-PELIq3oj.js +0 -1
  104. package/dist/assets/styles-Cevp58mS.css +0 -1
@@ -1,19 +1,56 @@
1
- import React from 'react';
1
+ import React, { useEffect, useState } from 'react';
2
2
  import { createRoot } from 'react-dom/client';
3
3
  import { App } from './App';
4
+ import { V2App } from './v2/App';
4
5
  import { ErrorBoundary } from './components/ErrorBoundary';
5
6
  import { installCrashReporter } from '../modules/crash-reporter';
6
7
  import './styles.css';
7
8
 
8
9
  installCrashReporter('side-panel');
9
10
 
11
+ const V2_STORAGE_KEY = 'v2UiEnabled';
12
+
13
+ /** rc.276 — Wrapper that picks the v1 or v2 UI based on a stored
14
+ * preference. Lives one layer above either App so a user can
15
+ * toggle without reloading the extension; toggling re-mounts. */
16
+ function Root() {
17
+ const [enabled, setEnabled] = useState<boolean | null>(null);
18
+ useEffect(() => {
19
+ let cancelled = false;
20
+ void chrome.storage.local
21
+ .get(V2_STORAGE_KEY)
22
+ .then((r) => {
23
+ if (cancelled) return;
24
+ setEnabled(Boolean(r[V2_STORAGE_KEY]));
25
+ })
26
+ .catch(() => setEnabled(false));
27
+ // Listen for live changes so toggling from Settings re-renders.
28
+ const onChange = (
29
+ changes: Record<string, chrome.storage.StorageChange>,
30
+ area: string,
31
+ ) => {
32
+ if (area !== 'local') return;
33
+ if (V2_STORAGE_KEY in changes) {
34
+ setEnabled(Boolean(changes[V2_STORAGE_KEY]?.newValue));
35
+ }
36
+ };
37
+ chrome.storage.onChanged.addListener(onChange);
38
+ return () => {
39
+ cancelled = true;
40
+ chrome.storage.onChanged.removeListener(onChange);
41
+ };
42
+ }, []);
43
+ if (enabled === null) return null; // brief tick before first paint
44
+ return enabled ? <V2App /> : <App />;
45
+ }
46
+
10
47
  const root = document.getElementById('root');
11
48
  if (!root) throw new Error('side-panel: #root not found');
12
49
 
13
50
  createRoot(root).render(
14
51
  <React.StrictMode>
15
52
  <ErrorBoundary>
16
- <App />
53
+ <Root />
17
54
  </ErrorBoundary>
18
55
  </React.StrictMode>
19
56
  );
@@ -4,16 +4,19 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>WCAG Component Auditor</title>
7
- <script type="module" crossorigin src="/assets/side-panel-XSB07vDa.js"></script>
7
+ <script type="module" crossorigin src="/assets/side-panel-f_X4NOJt.js"></script>
8
8
  <link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-B5Qt9EMX.js">
9
9
  <link rel="modulepreload" crossorigin href="/assets/_commonjsHelpers-Cpj98o6Y.js">
10
- <link rel="modulepreload" crossorigin href="/assets/crash-reporter-Dc5lvxvY.js">
11
- <link rel="modulepreload" crossorigin href="/assets/state-PELIq3oj.js">
12
- <link rel="modulepreload" crossorigin href="/assets/styles-C4Kq0zOO.js">
13
- <link rel="modulepreload" crossorigin href="/assets/preload-helper-D7HrI6pR.js">
14
- <link rel="modulepreload" crossorigin href="/assets/forensic-log-B1UCXZ23.js">
15
- <link rel="modulepreload" crossorigin href="/assets/ErrorBoundary-BLcMSVSr.js">
16
- <link rel="stylesheet" crossorigin href="/assets/styles-Cevp58mS.css">
10
+ <link rel="modulepreload" crossorigin href="/assets/crash-reporter-Bu2p8K-p.js">
11
+ <link rel="modulepreload" crossorigin href="/assets/ai-usage-log-BX3L6bKl.js">
12
+ <link rel="modulepreload" crossorigin href="/assets/styles-Cn731SYD.js">
13
+ <link rel="modulepreload" crossorigin href="/assets/diff-DA41zYPc.js">
14
+ <link rel="modulepreload" crossorigin href="/assets/scheduled-audit-runner-DyKpb3zg.js">
15
+ <link rel="modulepreload" crossorigin href="/assets/design-system-audit-DpxJrxnb.js">
16
+ <link rel="modulepreload" crossorigin href="/assets/ErrorBoundary-C-kswn4E.js">
17
+ <link rel="modulepreload" crossorigin href="/assets/copy-ai-fixer-prompt-DQYkHOv3.js">
18
+ <link rel="stylesheet" crossorigin href="/assets/styles-d5msFsnl.css">
19
+ <link rel="stylesheet" crossorigin href="/assets/side-panel-Ctm2yXeo.css">
17
20
  </head>
18
21
  <body class="m-0 p-0">
19
22
  <div id="root"></div>
@@ -7,9 +7,15 @@ import type { StateConfig } from '../types/state';
7
7
  import type { LicenseTier } from '../shared/messages';
8
8
  import type { InAppMessage } from '../modules/messages-client';
9
9
  import type { SiteCrawlReport } from '../shared/site-aggregator';
10
+ import { saveSiteCrawlReport } from '../shared/site-crawl-storage';
11
+ import type { IncompleteResolution } from '../shared/wcag-verdicts';
10
12
 
11
13
  export type AuditStatus = 'idle' | 'running' | 'complete' | 'failed' | 'interrupted';
12
- export type View = 'matrix' | 'delta' | 'activity' | 'guided' | 'flows' | 'scorecard' | 'crawl' | 'forensic' | 'compliance';
14
+ export type View = 'matrix' | 'report' | 'delta' | 'activity' | 'guided' | 'flows' | 'scorecard' | 'crawl' | 'forensic' | 'compliance' | 'schedules' | 'risk' | 'copilot' | 'ax-tree' | 'wcag3' | 'tokens';
15
+ // rc.159 — Findings sub-nav. The "Findings" (report) view is now a hub
16
+ // containing six lenses on the same audit data. The user picks which
17
+ // lens; the slug stays 'report' so existing tests/probes keep working.
18
+ export type FindingsLens = 'overview' | 'per-area' | 'violations' | 'activity' | 'delta' | 'wcag3';
13
19
  /** Two distinct personas: site owners (mom-and-pop, no code) vs developers
14
20
  * (full feature surface, CLI-friendly outputs, baselines, exports). */
15
21
  export type UserMode = 'owner' | 'dev';
@@ -64,6 +70,9 @@ type Store = {
64
70
 
65
71
  // ui
66
72
  view: View;
73
+ /** rc.159 — Which lens to show inside the Findings view. Defaults to
74
+ * 'overview' on every audit completion. */
75
+ findingsLens: FindingsLens;
67
76
  /** Persona-driven UI mode. `null` = first-launch (wizard not yet answered). */
68
77
  userMode: UserMode | null;
69
78
 
@@ -78,6 +87,15 @@ type Store = {
78
87
  pinnedMatchKey: string | null;
79
88
  pinnedFound: boolean;
80
89
 
90
+ /** rc.222 — Wall-clock audit timing. `startNewScan` writes Date.now()
91
+ * when the user kicks off an audit; `setResults` computes the delta
92
+ * on completion. Pre-rc.222 the GradeCard added per-state durations
93
+ * but missed the AI walkthroughs + pixel-contrast sampler that run
94
+ * AFTER the matrix — making the displayed "21.4s" significantly less
95
+ * than the user's real wait. Now we capture true elapsed time. */
96
+ auditStartedAtMs: number | null;
97
+ lastAuditWallClockMs: number | null;
98
+
81
99
  // AI augmentation failure surface — shown as a dismissible banner above the
82
100
  // results area when an audit's AI portion errored despite a configured key.
83
101
  // Cleared whenever a new audit starts or completes cleanly.
@@ -98,6 +116,12 @@ type Store = {
98
116
  currentCheckLabel: string;
99
117
  current: number;
100
118
  total: number;
119
+ /** Optional sub-progress within the current check: which candidate is
120
+ * being judged (e.g. image 3 of 10). When present the UI shows
121
+ * "AI: 7/9 — Reviewing link clarity… (3/10)" so the user can see
122
+ * movement inside a single check that judges many candidates. */
123
+ candidatesDone?: number;
124
+ candidatesTotal?: number;
101
125
  } | null;
102
126
 
103
127
  // actions
@@ -129,15 +153,76 @@ type Store = {
129
153
  }
130
154
  ) => void;
131
155
  setView: (v: View) => void;
156
+ setFindingsLens: (lens: FindingsLens) => void;
157
+ /** rc.205 — When the user clicks a Verification Area tile (or a
158
+ * criterion ID on the WCAG AA Status banner) we want the Per-area
159
+ * lens to render with THAT area's card already expanded. PerAreaCards
160
+ * reads this on mount, expands the matching area, then clears the
161
+ * request via `clearPendingAreaExpand`. Null when no jump is pending. */
162
+ pendingAreaExpand: string | null;
163
+ setPendingAreaExpand: (id: string | null) => void;
132
164
  setUserMode: (m: UserMode | null) => void;
133
165
  setSiteCrawlStatus: (s: Store['siteCrawlStatus']) => void;
134
166
  setSiteCrawlProgress: (p: Store['siteCrawlProgress']) => void;
135
167
  setSiteCrawlReport: (r: SiteCrawlReport | null) => void;
136
168
  setSiteCrawlError: (e: string | null) => void;
169
+ /** rc.234 — When the Findings tab is showing a slice of crawl data
170
+ * (either the site-wide aggregate or a specific page from the
171
+ * crawl), this carries the navigation context so the user can
172
+ * flip between them without losing the crawl. Null means "no
173
+ * active crawl context" (regular single-page audit).
174
+ * `mode: 'site-aggregate'` — Findings shows merged crawl data
175
+ * `mode: 'page', url: 'X'` — Findings shows one crawled page
176
+ * The ScopeBanner uses this to surface a page selector +
177
+ * "back to site-wide" affordance. The "Pages, worst first" list
178
+ * on the Crawl panel and the "← Back to site-wide" button on
179
+ * Findings both flip this state. */
180
+ crawlNavContext: { mode: 'site-aggregate' } | { mode: 'page'; url: string } | null;
181
+ setCrawlNavContext: (ctx: Store['crawlNavContext']) => void;
182
+ /** rc.235 — Synchronous cache of incomplete-resolution AI verdicts
183
+ * keyed by pageUrl. Pre-rc.235 the `useCurrentlyRelevantResolutions`
184
+ * hook always started with `[]` and async-loaded from chrome.storage,
185
+ * producing a TWO-RENDER FLICKER: first render counts violations
186
+ * WITHOUT AI fails, second render after async load includes them.
187
+ * Most visible when drilling into a crawled page (cached AI verdicts
188
+ * exist immediately), where the header would show "Fix 1 element"
189
+ * then jump to "Fix 5 elements" within milliseconds — Cliff: "things
190
+ * already do not make sense."
191
+ *
192
+ * The cache is in-memory only (no chrome.storage write). Callers
193
+ * pre-warm via `setResolutionsForUrl(url, resolutions)` BEFORE
194
+ * calling `setResults` so the hook's lazy initial value reads
195
+ * from cache instead of `[]`. The hook still runs its own async
196
+ * refresh to confirm + pick up any updates. */
197
+ resolutionsByUrl: Record<string, IncompleteResolution[]>;
198
+ setResolutionsForUrl: (url: string, resolutions: IncompleteResolution[]) => void;
137
199
  setPinned: (matchKey: string | null, found?: boolean) => void;
138
200
  setAiFailure: (failure: Store['aiFailure']) => void;
139
201
  clearAiFailure: () => void;
140
202
  setAiProgress: (progress: Store['aiProgress']) => void;
203
+ /** rc.37 — matchKeys the user has acknowledged as "not an issue." Loaded
204
+ * from chrome.storage at mount. Every ViolationCard reads this directly
205
+ * so the "Acknowledged" pill appears immediately on click, in every
206
+ * view (Matrix, Delta, WCAG-em, Owner — anywhere a card renders),
207
+ * before the SW round-trip to refresh the delta. */
208
+ acknowledgedKeys: Set<string>;
209
+ setAcknowledgedKeys: (keys: Set<string>) => void;
210
+ markAcknowledged: (matchKey: string) => void;
211
+ markUnacknowledged: (matchKey: string) => void;
212
+ /** rc.200 — Dismissal keys for axe violations (and AI-resolved fails).
213
+ * Distinct from `acknowledgedKeys`:
214
+ * - Acknowledged = "I verified the content; the rule was right
215
+ * that this looked worth reviewing, but it's actually fine."
216
+ * matchKey-based; survives re-audits as long as content matches.
217
+ * - Dismissed = "The rule itself is wrong about this page."
218
+ * URL-scoped; filtered from future audits + the AI-fix prompt.
219
+ * Loaded from `shared/dismissals` (chrome.storage.local) on audit-
220
+ * page change. ViolationCard renders the corresponding pill +
221
+ * un-dismiss affordance based on this set. */
222
+ dismissedKeys: Set<string>;
223
+ setDismissedKeys: (keys: Set<string>) => void;
224
+ markDismissed: (dismissalKey: string) => void;
225
+ markUndismissed: (dismissalKey: string) => void;
141
226
  /** Replace the in-app messages list + counters. Called by wire-messaging
142
227
  * after a refresh from the server. */
143
228
  setMessages: (messages: InAppMessage[], unreadCount: number, criticalUnacked: boolean) => void;
@@ -166,15 +251,22 @@ export const useStore = create<Store>((set) => ({
166
251
  unreadMessageCount: 0,
167
252
  criticalUnacked: false,
168
253
  view: 'matrix',
254
+ findingsLens: 'overview',
169
255
  userMode: null,
170
256
  siteCrawlStatus: 'idle',
171
257
  siteCrawlProgress: null,
172
258
  siteCrawlReport: null,
173
259
  siteCrawlError: null,
260
+ crawlNavContext: null,
261
+ resolutionsByUrl: {},
174
262
  pinnedMatchKey: null,
175
263
  pinnedFound: true,
176
264
  aiFailure: null,
177
265
  aiProgress: null,
266
+ acknowledgedKeys: new Set<string>(),
267
+ pendingAreaExpand: null,
268
+ auditStartedAtMs: null,
269
+ lastAuditWallClockMs: null,
178
270
 
179
271
  setStatus: (status) => set({ status }),
180
272
  startNewScan: () =>
@@ -189,21 +281,32 @@ export const useStore = create<Store>((set) => ({
189
281
  pinnedFound: true,
190
282
  aiFailure: null,
191
283
  aiProgress: null,
284
+ auditStartedAtMs: Date.now(),
285
+ // rc.234 — Single-page re-audit clears any crawl drill-in
286
+ // context so the user doesn't see a stale "Back to site-wide"
287
+ // affordance pointing at a no-longer-applicable crawl.
288
+ crawlNavContext: null,
192
289
  // freshThisSession stays false until setResults fires.
193
290
  }),
194
291
  setProgress: (progress) => set({ progress }),
195
292
  setResults: (results, delta, componentId) =>
196
- set({
197
- results,
198
- delta,
199
- componentId,
200
- status: 'complete',
201
- errorMessage: null,
202
- freshThisSession: true,
203
- aiProgress: null,
204
- // New audit invalidates any prior pin.
205
- pinnedMatchKey: null,
206
- pinnedFound: true,
293
+ set((state) => {
294
+ const wallClockMs = state.auditStartedAtMs
295
+ ? Date.now() - state.auditStartedAtMs
296
+ : null;
297
+ return {
298
+ results,
299
+ delta,
300
+ componentId,
301
+ status: 'complete',
302
+ errorMessage: null,
303
+ freshThisSession: true,
304
+ aiProgress: null,
305
+ pinnedMatchKey: null,
306
+ pinnedFound: true,
307
+ lastAuditWallClockMs: wallClockMs,
308
+ auditStartedAtMs: null,
309
+ };
207
310
  }),
208
311
  setDelta: (delta) => set({ delta }),
209
312
  setError: (errorMessage) => set({ errorMessage, status: 'failed' }),
@@ -233,15 +336,48 @@ export const useStore = create<Store>((set) => ({
233
336
  pastDue: extras?.pastDue ?? false,
234
337
  }),
235
338
  setView: (view) => set({ view }),
339
+ setFindingsLens: (findingsLens) => set({ findingsLens }),
340
+ setPendingAreaExpand: (pendingAreaExpand) => set({ pendingAreaExpand }),
236
341
  setUserMode: (userMode) => set({ userMode }),
237
342
  setSiteCrawlStatus: (siteCrawlStatus) => set({ siteCrawlStatus }),
343
+ setCrawlNavContext: (crawlNavContext) => set({ crawlNavContext }),
344
+ setResolutionsForUrl: (url, resolutions) =>
345
+ set((state) => ({
346
+ resolutionsByUrl: { ...state.resolutionsByUrl, [url]: resolutions },
347
+ })),
238
348
  setSiteCrawlProgress: (siteCrawlProgress) => set({ siteCrawlProgress }),
239
- setSiteCrawlReport: (siteCrawlReport) => set({ siteCrawlReport }),
349
+ setSiteCrawlReport: (siteCrawlReport) => {
350
+ set({ siteCrawlReport });
351
+ // rc.111 — persist so the AI fixer prompt's site-wide context section
352
+ // survives panel close / SW restart. Fire-and-forget; failure to write
353
+ // chrome.storage is non-fatal (the in-memory state still works for the
354
+ // current session).
355
+ void saveSiteCrawlReport(siteCrawlReport);
356
+ },
240
357
  setSiteCrawlError: (siteCrawlError) => set({ siteCrawlError }),
241
358
  setPinned: (matchKey, found = true) => set({ pinnedMatchKey: matchKey, pinnedFound: found }),
242
359
  setAiFailure: (aiFailure) => set({ aiFailure }),
243
360
  clearAiFailure: () => set({ aiFailure: null }),
244
361
  setAiProgress: (aiProgress) => set({ aiProgress }),
362
+ setAcknowledgedKeys: (acknowledgedKeys) => set({ acknowledgedKeys }),
363
+ markAcknowledged: (matchKey) =>
364
+ set((s) => ({ acknowledgedKeys: new Set(s.acknowledgedKeys).add(matchKey) })),
365
+ markUnacknowledged: (matchKey) =>
366
+ set((s) => {
367
+ const next = new Set(s.acknowledgedKeys);
368
+ next.delete(matchKey);
369
+ return { acknowledgedKeys: next };
370
+ }),
371
+ dismissedKeys: new Set<string>(),
372
+ setDismissedKeys: (dismissedKeys) => set({ dismissedKeys }),
373
+ markDismissed: (dismissalKey) =>
374
+ set((s) => ({ dismissedKeys: new Set(s.dismissedKeys).add(dismissalKey) })),
375
+ markUndismissed: (dismissalKey) =>
376
+ set((s) => {
377
+ const next = new Set(s.dismissedKeys);
378
+ next.delete(dismissalKey);
379
+ return { dismissedKeys: next };
380
+ }),
245
381
  setMessages: (messages, unreadMessageCount, criticalUnacked) =>
246
382
  set({ messages, unreadMessageCount, criticalUnacked }),
247
383
  applyMessageAck: (messageId, action) =>
@@ -1,3 +1,10 @@
1
+ /* rc.159 — Forensic-clarity type system. IBM Plex Sans for body + display
2
+ (humanist sans with strong character for a compliance-tool aesthetic);
3
+ IBM Plex Mono for selectors, matchKeys, JSON output, axe rule IDs.
4
+ Bundled via Google Fonts; Chrome caches woff2 across pages so subsequent
5
+ side-panel opens are offline-friendly. */
6
+ @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap');
7
+
1
8
  @tailwind base;
2
9
  @tailwind components;
3
10
  @tailwind utilities;
@@ -6,6 +13,38 @@ html, body, #root {
6
13
  height: 100%;
7
14
  }
8
15
 
16
+ html {
17
+ /* Default to the Plex stack; mono utilities switch to the mono stack. */
18
+ font-family:
19
+ 'IBM Plex Sans',
20
+ -apple-system,
21
+ BlinkMacSystemFont,
22
+ 'Segoe UI Variable Text',
23
+ 'Segoe UI',
24
+ system-ui,
25
+ sans-serif;
26
+ /* Slightly tighter feature defaults to match the compliance-doc feel:
27
+ stylistic alternates + tabular numerals so columns of counts and
28
+ percentages align visually inside tables and per-area cards. */
29
+ font-feature-settings: 'tnum' 1, 'ss01' 1, 'cv11' 1;
30
+ -webkit-font-smoothing: antialiased;
31
+ }
32
+
33
+ code,
34
+ kbd,
35
+ samp,
36
+ pre,
37
+ .font-mono {
38
+ font-family:
39
+ 'IBM Plex Mono',
40
+ ui-monospace,
41
+ 'JetBrains Mono',
42
+ 'SF Mono',
43
+ 'Consolas',
44
+ monospace;
45
+ font-feature-settings: 'tnum' 1;
46
+ }
47
+
9
48
  /* Tailwind's preflight resets the UA focus ring. Restore a high-contrast
10
49
  ring for keyboard users (only :focus-visible — pointer focus stays clean).
11
50
  Brand-700 #3730a3 over white = 9.6:1 contrast; same ring shows fine on
@@ -5,9 +5,16 @@ import { on, request } from '../shared/messaging';
5
5
  import { useStore } from './store';
6
6
  import { announcer } from '../shared/announcer';
7
7
  import { getAuditTargetTabId } from '../shared/active-tab';
8
- import type { BaselineListResponse, SettingsResponse, TierGetResponse } from '../shared/messages';
8
+ import type { BaselineListResponse, SettingsResponse, TierGetResponse, ExportResponse } from '../shared/messages';
9
9
  import type { UserMode } from './store';
10
10
  import type { AuditResult, DeltaResult } from '../types/audit';
11
+ import {
12
+ getAutoExportSettings,
13
+ recordLastAutoExport,
14
+ } from '../shared/auto-export-settings';
15
+ import { triggerDownload, bundleFilename } from '../shared/download-helper';
16
+ import { isFeatureAllowed } from '../shared/tier-rules';
17
+ import { isLockoutInEffectForTier } from '../shared/headless-build-lockdown';
11
18
 
12
19
  const LAST_AUDIT_KEY = 'sidePanel:lastAudit';
13
20
 
@@ -22,17 +29,27 @@ export function wireMessaging(): () => void {
22
29
 
23
30
  offs.push(
24
31
  on('AUDIT_PROGRESS_EVENT', (msg) => {
25
- const wasRunning = useStore.getState().status === 'running';
26
- useStore.getState().setProgress({
32
+ const s = useStore.getState();
33
+ const wasRunning = s.status === 'running';
34
+ const inCrawl = s.siteCrawlStatus === 'running';
35
+ s.setProgress({
27
36
  current: msg.current,
28
37
  total: msg.total,
29
38
  currentState: msg.currentState,
30
39
  });
31
- useStore.getState().setStatus('running');
32
- // Announce only at the start, not on every state otherwise SR users
33
- // get spammed once per matrix iteration.
34
- if (!wasRunning) {
35
- announcer.polite(`Audit running, scanning ${msg.total} state${msg.total === 1 ? '' : 's'}.`);
40
+ // rc.288 — During a crawl, AUDIT_PROGRESS_EVENT fires per page
41
+ // per state so the side panel can show inner matrix progress
42
+ // underneath the crawl's page counter. Don't flip s.status to
43
+ // 'running' in that case — the crawl's own siteCrawlStatus is
44
+ // the lifecycle signal, and we'd leave status='running' dangling
45
+ // after the crawl ends (AUDIT_COMPLETE is suppressed by
46
+ // asSubroutine). For genuine single-page runs, status='running'
47
+ // is still the right signal.
48
+ if (!inCrawl) {
49
+ s.setStatus('running');
50
+ if (!wasRunning) {
51
+ announcer.polite(`Audit running, scanning ${msg.total} state${msg.total === 1 ? '' : 's'}.`);
52
+ }
36
53
  }
37
54
  })
38
55
  );
@@ -46,11 +63,30 @@ export function wireMessaging(): () => void {
46
63
  ? `Audit complete. ${newCount} new violation${newCount === 1 ? '' : 's'} versus baseline.`
47
64
  : `Audit complete. ${violationCount} violation${violationCount === 1 ? '' : 's'} found across ${msg.results.length} state${msg.results.length === 1 ? '' : 's'}.`;
48
65
  announcer.polite(summary);
66
+ // rc.159 — Auto-navigate to Findings on audit completion. The user
67
+ // most likely wants to see the results, not stay on the Dashboard
68
+ // / specialized tool they kicked the audit off from. Exception: if
69
+ // they're mid-flow on Compliance (multi-step report builder),
70
+ // Guided (interactive workflow session), or Crawl (multi-page scan
71
+ // in progress), don't yank them out of context.
72
+ const currentView = useStore.getState().view;
73
+ const stickyViews: Array<typeof currentView> = ['compliance', 'guided', 'crawl'];
74
+ if (!stickyViews.includes(currentView)) {
75
+ useStore.getState().setView('report');
76
+ useStore.getState().setFindingsLens('overview');
77
+ }
49
78
  void persistLastAudit({
50
79
  results: msg.results,
51
80
  delta: msg.delta,
52
81
  componentId: msg.componentId,
53
82
  });
83
+ // rc.201 — Auto-export the audit report if the user has the
84
+ // "auto-export after each audit" setting on AND their tier
85
+ // allows it. Fires AFTER setResults so the rest of the UI has
86
+ // updated. Errors are best-effort + silent — the user already
87
+ // has the in-app result; failing to write a file is logged but
88
+ // doesn't disrupt the audit flow.
89
+ void maybeAutoExport(msg.results, msg.delta, msg.componentId);
54
90
  })
55
91
  );
56
92
 
@@ -70,9 +106,12 @@ export function wireMessaging(): () => void {
70
106
  currentCheckLabel: msg.currentCheckLabel,
71
107
  current: msg.current,
72
108
  total: msg.total,
109
+ candidatesDone: msg.candidatesDone,
110
+ candidatesTotal: msg.candidatesTotal,
73
111
  });
74
112
  // Announce only on the first AI check so SR users hear the phase
75
- // transition once, not 8 times.
113
+ // transition once, not 8 times. Sub-progress events fire often;
114
+ // don't speak those.
76
115
  if (!prev) {
77
116
  announcer.polite(`AI augmentation running ${msg.total} check${msg.total === 1 ? '' : 's'}.`);
78
117
  }
@@ -134,6 +173,39 @@ export function wireMessaging(): () => void {
134
173
  announcer.polite(
135
174
  `Site crawl complete. Grade ${msg.report.siteGrade}, ${msg.report.totalUniqueViolations} unique violation${msg.report.totalUniqueViolations === 1 ? '' : 's'}.`
136
175
  );
176
+ // rc.236 — Pre-warm resolutions cache for every crawled URL so
177
+ // that when the user clicks a page in the do-the-work report
178
+ // and drills into its Findings, the AI-resolved-fails count is
179
+ // correct on FIRST render (no two-render flicker like rc.235
180
+ // fixed for the now-removed aggregate view). Pre-rc.236 we also
181
+ // flattened all crawled results into the main `results` store
182
+ // to feed an aggregate "View site-wide Findings" button — that
183
+ // button is gone (the Crawl panel itself is the report now),
184
+ // and the aggregate-Findings view's broken math was what
185
+ // confused users. Drill-in setResults still happens, just only
186
+ // when the user clicks a specific page.
187
+ void (async () => {
188
+ try {
189
+ const { loadSiteCrawlPerUrlResults } = await import('../shared/site-crawl-storage');
190
+ const { getResolutionsForPage } = await import('../shared/incomplete-resolutions');
191
+ const perUrl = await loadSiteCrawlPerUrlResults();
192
+ if (!perUrl || perUrl.length === 0) return;
193
+ const setResolutionsForUrl = useStore.getState().setResolutionsForUrl;
194
+ await Promise.all(
195
+ perUrl.map(async (p) => {
196
+ try {
197
+ const r = await getResolutionsForPage(p.url);
198
+ setResolutionsForUrl(p.url, r);
199
+ } catch {
200
+ // best-effort
201
+ }
202
+ }),
203
+ );
204
+ } catch (err) {
205
+ // eslint-disable-next-line no-console
206
+ console.warn('[site-crawl] failed to pre-warm resolutions cache', err);
207
+ }
208
+ })();
137
209
  })
138
210
  );
139
211
 
@@ -150,6 +222,71 @@ export function wireMessaging(): () => void {
150
222
  return () => offs.forEach((off) => off());
151
223
  }
152
224
 
225
+ /** rc.201 — Auto-export the just-completed audit if the user opted in.
226
+ * Silent + best-effort. Errors are logged but never thrown — the audit
227
+ * itself completed successfully even if file-system write fails. */
228
+ async function maybeAutoExport(
229
+ results: AuditResult[],
230
+ delta: DeltaResult | null | undefined,
231
+ componentId: string | null | undefined,
232
+ ): Promise<void> {
233
+ if (results.length === 0) return;
234
+ try {
235
+ const settings = await getAutoExportSettings();
236
+ if (!settings.enabled) return;
237
+ // Tier-gate. Free tier won't get the toggle exposed post-launch;
238
+ // even if a tier change happens between toggle-on and audit-complete,
239
+ // the gate re-checks here.
240
+ const tier = useStore.getState().tier;
241
+ if (isLockoutInEffectForTier(tier)) return;
242
+ if (!isFeatureAllowed(tier, 'autoExportAuditReports')) return;
243
+
244
+ // rc.221 — Loop every enabled format. Each completed audit produces
245
+ // each requested artifact (defense-bundle for legal + ai-prompt for
246
+ // dev handoff + html-print for sharing — pick the set that matches
247
+ // the workflow).
248
+ const url = results[0]?.pageUrl ?? results[0]?.scope ?? '';
249
+ const exported: string[] = [];
250
+ // rc.228 — Snapshot wall-clock now so all formats in this auto-
251
+ // export round see the same value.
252
+ const wallClockMs = useStore.getState().lastAuditWallClockMs ?? undefined;
253
+ for (const format of settings.formats) {
254
+ try {
255
+ const res = await request<ExportResponse>({
256
+ type: 'EXPORT_REQUEST',
257
+ format,
258
+ results,
259
+ delta: delta ?? undefined,
260
+ wallClockMs,
261
+ });
262
+ // EXPORT_REQUEST returns a lockout error string when the SW-side
263
+ // gate refuses. Treat any very-short body as "refused."
264
+ if (typeof res.content !== 'string' || res.content.length < 200) continue;
265
+ const filename = bundleFilename(format, componentId ?? null);
266
+ triggerDownload(res.content, filename);
267
+ if (url) {
268
+ await recordLastAutoExport({
269
+ url,
270
+ exportedAt: new Date().toISOString(),
271
+ format,
272
+ filename,
273
+ });
274
+ }
275
+ exported.push(filename);
276
+ } catch (formatErr) {
277
+ console.warn(`[auto-export] format ${format} failed:`, formatErr);
278
+ }
279
+ }
280
+ if (exported.length > 0) {
281
+ announcer.polite(`Audit auto-exported ${exported.length} file${exported.length === 1 ? '' : 's'}.`);
282
+ }
283
+ } catch (err) {
284
+ // Silent failure — surface in console for debugging but don't
285
+ // interrupt the user's audit-review flow with a modal.
286
+ console.warn('[auto-export] failed:', err);
287
+ }
288
+ }
289
+
153
290
  async function persistLastAudit(payload: PersistedAudit): Promise<void> {
154
291
  // Strip screenshotDataUrl from each result before persisting. With a full
155
292
  // state matrix (up to 144 states), 144 base64 JPEGs at ~50-200KB each blow
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wcag-checkr/ci",
3
- "version": "1.0.0-rc.31",
3
+ "version": "1.0.0-rc.310",
4
4
  "private": false,
5
5
  "description": "Headless wcagcheckr accessibility audit runner for CI/CD pipelines. Drives the wcagcheckr Chrome extension via Playwright, runs full-page audits across the state matrix (108 combinations: hover, focus, dark mode, RTL, breakpoints), outputs JSON / SARIF / JUnit, exits with severity-aware codes.",
6
6
  "license": "UNLICENSED",