@reticular/speakable 1.5.0 → 1.7.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.
@@ -0,0 +1,701 @@
1
+ import {
2
+ ADDON_ID,
3
+ EVENT_RESULT,
4
+ EVENT_TIMELINE,
5
+ PANEL_ID
6
+ } from "./chunk-QVJRQZT5.js";
7
+ import "./chunk-JSBRDJBE.js";
8
+
9
+ // src/manager.tsx
10
+ import { useState, useEffect } from "react";
11
+ import { addons, types } from "@storybook/manager-api";
12
+
13
+ // ../src/runtime/diff-engine.ts
14
+ function matchingKey(event) {
15
+ return `${event.type}::${event.target.selector}`;
16
+ }
17
+ function payloadsEqual(a, b) {
18
+ if (a.type !== b.type) return false;
19
+ if (!targetsEqual(a.target, b.target)) return false;
20
+ return deepEqual(a.payload, b.payload);
21
+ }
22
+ function targetsEqual(a, b) {
23
+ return a.role === b.role && a.accessibleName === b.accessibleName && a.selector === b.selector;
24
+ }
25
+ function deepEqual(a, b) {
26
+ if (a === b) return true;
27
+ if (a === null || b === null) return a === b;
28
+ if (typeof a !== typeof b) return false;
29
+ if (typeof a !== "object") return a === b;
30
+ const aObj = a;
31
+ const bObj = b;
32
+ const aKeys = Object.keys(aObj).sort();
33
+ const bKeys = Object.keys(bObj).sort();
34
+ if (aKeys.length !== bKeys.length) return false;
35
+ for (let i = 0; i < aKeys.length; i++) {
36
+ if (aKeys[i] !== bKeys[i]) return false;
37
+ if (!deepEqual(aObj[aKeys[i]], bObj[bKeys[i]])) return false;
38
+ }
39
+ return true;
40
+ }
41
+ function buildModifiedMessage(baseline, current) {
42
+ const type = current.type;
43
+ const selector = current.target.selector;
44
+ if (baseline.target.accessibleName !== current.target.accessibleName) {
45
+ return `${type} on "${selector}": accessible name changed from "${baseline.target.accessibleName}" to "${current.target.accessibleName}"`;
46
+ }
47
+ if (baseline.target.role !== current.target.role) {
48
+ return `${type} on "${selector}": role changed from "${baseline.target.role}" to "${current.target.role}"`;
49
+ }
50
+ return `${type} on "${selector}": payload values differ`;
51
+ }
52
+ function diffTimelines(baseline, current) {
53
+ const added = [];
54
+ const removed = [];
55
+ const modified = [];
56
+ const currentByKey = /* @__PURE__ */ new Map();
57
+ for (const event of current.events) {
58
+ const key = matchingKey(event);
59
+ const existing = currentByKey.get(key);
60
+ if (existing) {
61
+ existing.push(event);
62
+ } else {
63
+ currentByKey.set(key, [event]);
64
+ }
65
+ }
66
+ const matchedCurrentIndices = /* @__PURE__ */ new Set();
67
+ for (const baselineEvent of baseline.events) {
68
+ const key = matchingKey(baselineEvent);
69
+ const candidates = currentByKey.get(key);
70
+ if (!candidates || candidates.length === 0) {
71
+ removed.push({
72
+ event: baselineEvent,
73
+ message: `${baselineEvent.type} on "${baselineEvent.target.selector}" was removed`
74
+ });
75
+ continue;
76
+ }
77
+ let matched = false;
78
+ for (let i = 0; i < candidates.length; i++) {
79
+ const candidateId = `${key}::${i}`;
80
+ if (matchedCurrentIndices.has(candidateId)) continue;
81
+ const currentEvent = candidates[i];
82
+ matchedCurrentIndices.add(candidateId);
83
+ if (!payloadsEqual(baselineEvent, currentEvent)) {
84
+ modified.push({
85
+ event: currentEvent,
86
+ previousEvent: baselineEvent,
87
+ message: buildModifiedMessage(baselineEvent, currentEvent)
88
+ });
89
+ }
90
+ matched = true;
91
+ break;
92
+ }
93
+ if (!matched) {
94
+ removed.push({
95
+ event: baselineEvent,
96
+ message: `${baselineEvent.type} on "${baselineEvent.target.selector}" was removed`
97
+ });
98
+ }
99
+ }
100
+ for (const [key, candidates] of currentByKey.entries()) {
101
+ for (let i = 0; i < candidates.length; i++) {
102
+ const candidateId = `${key}::${i}`;
103
+ if (!matchedCurrentIndices.has(candidateId)) {
104
+ const event = candidates[i];
105
+ added.push({
106
+ event,
107
+ message: `${event.type} on "${event.target.selector}" was added`
108
+ });
109
+ }
110
+ }
111
+ }
112
+ return {
113
+ baseline: { totalEvents: baseline.events.length },
114
+ current: { totalEvents: current.events.length },
115
+ added,
116
+ removed,
117
+ modified,
118
+ summary: {
119
+ totalBaseline: baseline.events.length,
120
+ totalCurrent: current.events.length,
121
+ added: added.length,
122
+ removed: removed.length,
123
+ modified: modified.length
124
+ }
125
+ };
126
+ }
127
+
128
+ // ../src/runtime/severity.ts
129
+ function classifyDiff(report) {
130
+ const entries = [];
131
+ for (const entry of report.removed) {
132
+ const severity = classifyRemoved(entry);
133
+ entries.push({ ...entry, severity });
134
+ }
135
+ for (const entry of report.added) {
136
+ const severity = classifyAdded(entry);
137
+ entries.push({ ...entry, severity });
138
+ }
139
+ for (const entry of report.modified) {
140
+ const severity = classifyModified(entry);
141
+ entries.push({ ...entry, severity });
142
+ }
143
+ const criticalCount = entries.filter((e) => e.severity === "critical").length;
144
+ const highCount = entries.filter((e) => e.severity === "high").length;
145
+ const mediumCount = entries.filter((e) => e.severity === "medium").length;
146
+ const lowCount = entries.filter((e) => e.severity === "low").length;
147
+ let highestSeverity = null;
148
+ if (criticalCount > 0) highestSeverity = "critical";
149
+ else if (highCount > 0) highestSeverity = "high";
150
+ else if (mediumCount > 0) highestSeverity = "medium";
151
+ else if (lowCount > 0) highestSeverity = "low";
152
+ return { entries, highestSeverity, criticalCount, highCount, mediumCount, lowCount };
153
+ }
154
+ function classifyRemoved(entry) {
155
+ switch (entry.event.type) {
156
+ case "FOCUS_CHANGED":
157
+ return "critical";
158
+ case "ANNOUNCEMENT":
159
+ return "critical";
160
+ case "DIALOG_OPENED":
161
+ return "high";
162
+ case "DIALOG_CLOSED":
163
+ return "high";
164
+ case "STATE_CHANGED":
165
+ return "high";
166
+ case "KEYBOARD_ACTION":
167
+ return "medium";
168
+ default:
169
+ return "medium";
170
+ }
171
+ }
172
+ function classifyAdded(entry) {
173
+ switch (entry.event.type) {
174
+ case "WARNING":
175
+ return "high";
176
+ case "REGRESSION":
177
+ return "critical";
178
+ case "ANNOUNCEMENT":
179
+ return "medium";
180
+ case "FOCUS_CHANGED":
181
+ return "medium";
182
+ default:
183
+ return "low";
184
+ }
185
+ }
186
+ function classifyModified(entry) {
187
+ switch (entry.event.type) {
188
+ case "FOCUS_CHANGED":
189
+ return "high";
190
+ case "STATE_CHANGED":
191
+ return "high";
192
+ case "ACCESSIBLE_NAME_CHANGED":
193
+ return "medium";
194
+ case "ANNOUNCEMENT":
195
+ return "medium";
196
+ default:
197
+ return "medium";
198
+ }
199
+ }
200
+
201
+ // src/baseline-store.ts
202
+ var STORAGE_PREFIX = "speakable:baseline:";
203
+ function getStorage() {
204
+ try {
205
+ if (typeof localStorage !== "undefined") return localStorage;
206
+ } catch {
207
+ }
208
+ return null;
209
+ }
210
+ function createPanelBaselineStore() {
211
+ const memory = /* @__PURE__ */ new Map();
212
+ function storageKey(component, story) {
213
+ return `${STORAGE_PREFIX}${component}::${story}`;
214
+ }
215
+ return {
216
+ key(component, story) {
217
+ return storageKey(component, story);
218
+ },
219
+ async save(component, story, timeline) {
220
+ const k = storageKey(component, story);
221
+ const json = JSON.stringify(timeline);
222
+ const storage = getStorage();
223
+ if (storage) {
224
+ storage.setItem(k, json);
225
+ } else {
226
+ memory.set(k, json);
227
+ }
228
+ },
229
+ async load(component, story) {
230
+ const k = storageKey(component, story);
231
+ const storage = getStorage();
232
+ const json = storage ? storage.getItem(k) : memory.get(k) ?? null;
233
+ if (!json) return null;
234
+ try {
235
+ return JSON.parse(json);
236
+ } catch {
237
+ return null;
238
+ }
239
+ }
240
+ };
241
+ }
242
+
243
+ // src/manager.tsx
244
+ import { jsx, jsxs } from "react/jsx-runtime";
245
+ var READER_TABS = [
246
+ { key: "nvda", label: "NVDA", color: "#3b82f6" },
247
+ { key: "jaws", label: "JAWS", color: "#8b5cf6" },
248
+ { key: "voiceover", label: "VoiceOver", color: "#06b6d4" },
249
+ { key: "narrator", label: "Narrator", color: "#f59e0b" },
250
+ { key: "audit", label: "Audit", color: "#ef4444" },
251
+ { key: "timeline", label: "Timeline", color: "#10b981" },
252
+ { key: "diff", label: "Diff", color: "#ec4899" }
253
+ ];
254
+ var baselineStore = createPanelBaselineStore();
255
+ var SEVERITY_COLORS = {
256
+ critical: "#ef4444",
257
+ high: "#f97316",
258
+ medium: "#f59e0b",
259
+ low: "#6b7280"
260
+ };
261
+ function summarizePayload(event) {
262
+ const p = event.payload;
263
+ switch (p.kind) {
264
+ case "focus_changed":
265
+ return "focus moved here";
266
+ case "announcement":
267
+ return `announce (${p.politeness}): "${p.text}"`;
268
+ case "role_changed":
269
+ return `role ${p.previousRole} \u2192 ${p.newRole}`;
270
+ case "accessible_name_changed":
271
+ return `name "${p.previousName}" \u2192 "${p.newName}"`;
272
+ case "state_changed":
273
+ return `${p.attribute}: ${String(p.previousValue)} \u2192 ${String(p.newValue)}`;
274
+ case "dialog_opened":
275
+ return `dialog opened: ${p.dialogName}${p.isModal ? " (modal)" : ""}`;
276
+ case "dialog_closed":
277
+ return `dialog closed: ${p.dialogName}`;
278
+ case "keyboard_action":
279
+ return `key ${[...p.modifiers || [], p.key].join("+")}`;
280
+ case "warning":
281
+ return `\u26A0 ${p.message}`;
282
+ default:
283
+ return "";
284
+ }
285
+ }
286
+ function SpeakablePanel() {
287
+ const [result, setResult] = useState(null);
288
+ const [timeline, setTimeline] = useState(null);
289
+ const [diff, setDiff] = useState(null);
290
+ const [hasBaseline, setHasBaseline] = useState(false);
291
+ const [activeTab, setActiveTab] = useState("nvda");
292
+ useEffect(() => {
293
+ const channel = addons.getChannel();
294
+ const handleResult = (data) => {
295
+ setResult(data);
296
+ };
297
+ const handleTimeline = async (data) => {
298
+ setTimeline(data);
299
+ const baseline = await baselineStore.load(data.component, data.story || "Story");
300
+ if (baseline) {
301
+ setHasBaseline(true);
302
+ const report = diffTimelines(baseline, data);
303
+ setDiff(classifyDiff(report));
304
+ } else {
305
+ setHasBaseline(false);
306
+ setDiff(null);
307
+ }
308
+ };
309
+ channel.on(EVENT_RESULT, handleResult);
310
+ channel.on(EVENT_TIMELINE, handleTimeline);
311
+ return () => {
312
+ channel.off(EVENT_RESULT, handleResult);
313
+ channel.off(EVENT_TIMELINE, handleTimeline);
314
+ };
315
+ }, []);
316
+ const handleSetBaseline = async () => {
317
+ if (!timeline) return;
318
+ await baselineStore.save(timeline.component, timeline.story || "Story", timeline);
319
+ setHasBaseline(true);
320
+ const report = diffTimelines(timeline, timeline);
321
+ setDiff(classifyDiff(report));
322
+ };
323
+ if (!result) {
324
+ return /* @__PURE__ */ jsxs("div", { style: styles.emptyState, children: [
325
+ /* @__PURE__ */ jsx("div", { style: styles.emptyIcon, children: "\u{1F50A}" }),
326
+ /* @__PURE__ */ jsx("p", { style: styles.emptyText, children: "Render a story to see predicted screen reader output" }),
327
+ /* @__PURE__ */ jsx("p", { style: styles.emptySubtext, children: "Powered by Speakable" })
328
+ ] });
329
+ }
330
+ const READER_KEYS = ["nvda", "jaws", "voiceover", "narrator"];
331
+ const isReaderTab = READER_KEYS.includes(activeTab);
332
+ const activeLines = isReaderTab ? result[activeTab] : [];
333
+ const auditFindings = result.audit;
334
+ return /* @__PURE__ */ jsxs("div", { style: styles.container, children: [
335
+ /* @__PURE__ */ jsx("div", { style: styles.tabBar, children: READER_TABS.map((tab) => /* @__PURE__ */ jsxs(
336
+ "button",
337
+ {
338
+ onClick: () => setActiveTab(tab.key),
339
+ style: {
340
+ ...styles.tab,
341
+ ...activeTab === tab.key ? { ...styles.tabActive, borderBottomColor: tab.color } : { borderBottomColor: "transparent" }
342
+ },
343
+ children: [
344
+ tab.label,
345
+ tab.key === "audit" && auditFindings.length > 0 && /* @__PURE__ */ jsx("span", { style: styles.badge, children: auditFindings.length })
346
+ ]
347
+ },
348
+ tab.key
349
+ )) }),
350
+ /* @__PURE__ */ jsxs("div", { style: styles.statsBar, children: [
351
+ /* @__PURE__ */ jsxs("span", { style: styles.stat, children: [
352
+ result.stats.totalElements,
353
+ " elements"
354
+ ] }),
355
+ /* @__PURE__ */ jsxs("span", { style: styles.stat, children: [
356
+ result.stats.interactiveElements,
357
+ " interactive"
358
+ ] }),
359
+ /* @__PURE__ */ jsxs("span", { style: styles.stat, children: [
360
+ result.stats.landmarks,
361
+ " landmarks"
362
+ ] }),
363
+ /* @__PURE__ */ jsxs("span", { style: styles.stat, children: [
364
+ result.stats.headings,
365
+ " headings"
366
+ ] })
367
+ ] }),
368
+ /* @__PURE__ */ jsx("div", { style: styles.content, children: activeTab === "audit" ? /* @__PURE__ */ jsx(AuditView, { findings: auditFindings }) : activeTab === "timeline" ? /* @__PURE__ */ jsx(TimelineView, { timeline, hasBaseline, onSetBaseline: handleSetBaseline }) : activeTab === "diff" ? /* @__PURE__ */ jsx(DiffView, { diff, hasBaseline, hasTimeline: !!timeline, onSetBaseline: handleSetBaseline }) : /* @__PURE__ */ jsx(AnnouncementView, { lines: activeLines, readerKey: activeTab }) }),
369
+ /* @__PURE__ */ jsxs("div", { style: styles.footer, children: [
370
+ /* @__PURE__ */ jsx("span", { style: styles.footerText, children: "Predictions are heuristic-based. Verify with real screen readers before release." }),
371
+ /* @__PURE__ */ jsx("a", { href: "https://getspeakable.dev", target: "_blank", rel: "noopener noreferrer", style: styles.footerLink, children: "getspeakable.dev" })
372
+ ] })
373
+ ] });
374
+ }
375
+ function AnnouncementView({ lines, readerKey }) {
376
+ if (lines.length === 0) {
377
+ return /* @__PURE__ */ jsx("p", { style: styles.emptyContent, children: "No announcements for this story." });
378
+ }
379
+ return /* @__PURE__ */ jsx("div", { style: styles.lineList, children: lines.map((line, i) => /* @__PURE__ */ jsxs("div", { style: styles.line, children: [
380
+ /* @__PURE__ */ jsx("span", { style: styles.lineNumber, children: i + 1 }),
381
+ /* @__PURE__ */ jsx("span", { style: styles.lineText, children: line })
382
+ ] }, `${readerKey}-${i}`)) });
383
+ }
384
+ function AuditView({ findings }) {
385
+ if (findings.length === 0) {
386
+ return /* @__PURE__ */ jsxs("div", { style: styles.auditPass, children: [
387
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 24 }, children: "\u2713" }),
388
+ /* @__PURE__ */ jsx("p", { style: { margin: 0, fontWeight: 600, color: "#059669" }, children: "No issues found" }),
389
+ /* @__PURE__ */ jsx("p", { style: { margin: 0, fontSize: 12, color: "#6b7280" }, children: "All interactive elements have accessible names and heading hierarchy is correct." })
390
+ ] });
391
+ }
392
+ return /* @__PURE__ */ jsx("div", { style: styles.lineList, children: findings.map((finding, i) => /* @__PURE__ */ jsxs("div", { style: { ...styles.finding, borderLeftColor: finding.severity === "error" ? "#ef4444" : finding.severity === "warning" ? "#f59e0b" : "#6b7280" }, children: [
393
+ /* @__PURE__ */ jsx("div", { style: styles.findingSeverity, children: finding.severity === "error" ? "\u2717" : finding.severity === "warning" ? "\u26A0" : "\u2139" }),
394
+ /* @__PURE__ */ jsxs("div", { children: [
395
+ /* @__PURE__ */ jsx("p", { style: styles.findingMessage, children: finding.message }),
396
+ /* @__PURE__ */ jsx("p", { style: styles.findingSelector, children: finding.selector })
397
+ ] })
398
+ ] }, i)) });
399
+ }
400
+ function TimelineView({
401
+ timeline,
402
+ hasBaseline,
403
+ onSetBaseline
404
+ }) {
405
+ if (!timeline) {
406
+ return /* @__PURE__ */ jsxs("p", { style: styles.emptyContent, children: [
407
+ "No interaction timeline for this story. Add",
408
+ " ",
409
+ /* @__PURE__ */ jsx("code", { children: "parameters.speakable.sequence" }),
410
+ " or",
411
+ " ",
412
+ /* @__PURE__ */ jsx("code", { children: "parameters.speakable.pattern" }),
413
+ " to capture one."
414
+ ] });
415
+ }
416
+ return /* @__PURE__ */ jsxs("div", { children: [
417
+ /* @__PURE__ */ jsxs("div", { style: styles.baselineBar, children: [
418
+ /* @__PURE__ */ jsxs("span", { style: styles.stat, children: [
419
+ timeline.events.length,
420
+ " events \xB7 ",
421
+ timeline.duration,
422
+ "ms"
423
+ ] }),
424
+ /* @__PURE__ */ jsx("span", { style: styles.baselineHint, children: hasBaseline ? "\u2713 baseline set" : "No baseline yet" }),
425
+ /* @__PURE__ */ jsx("button", { style: styles.baselineButton, onClick: onSetBaseline, children: "Set baseline" })
426
+ ] }),
427
+ /* @__PURE__ */ jsx("div", { style: styles.lineList, children: timeline.events.map((event, i) => /* @__PURE__ */ jsxs("div", { style: styles.line, children: [
428
+ /* @__PURE__ */ jsxs("span", { style: styles.lineNumber, children: [
429
+ event.timestamp,
430
+ "ms"
431
+ ] }),
432
+ /* @__PURE__ */ jsxs("span", { style: styles.lineText, children: [
433
+ /* @__PURE__ */ jsx("strong", { children: event.type }),
434
+ event.target.role ? ` \xB7 ${event.target.role}` : "",
435
+ event.target.accessibleName ? ` "${event.target.accessibleName}"` : "",
436
+ summarizePayload(event) ? ` \u2014 ${summarizePayload(event)}` : ""
437
+ ] })
438
+ ] }, i)) })
439
+ ] });
440
+ }
441
+ function DiffView({
442
+ diff,
443
+ hasBaseline,
444
+ hasTimeline,
445
+ onSetBaseline
446
+ }) {
447
+ if (!hasBaseline) {
448
+ return /* @__PURE__ */ jsxs("div", { style: styles.emptyState, children: [
449
+ /* @__PURE__ */ jsx("p", { style: styles.emptyText, children: "No baseline to compare against" }),
450
+ /* @__PURE__ */ jsx("p", { style: styles.emptySubtext, children: "Capture a timeline, then set it as the baseline to detect regressions." }),
451
+ hasTimeline && /* @__PURE__ */ jsx("button", { style: styles.baselineButton, onClick: onSetBaseline, children: "Set baseline" })
452
+ ] });
453
+ }
454
+ if (!diff || diff.entries.length === 0) {
455
+ return /* @__PURE__ */ jsxs("div", { style: styles.auditPass, children: [
456
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 24 }, children: "\u2713" }),
457
+ /* @__PURE__ */ jsx("p", { style: { margin: 0, fontWeight: 600, color: "#059669" }, children: "No behavioral changes" }),
458
+ /* @__PURE__ */ jsx("p", { style: { margin: 0, fontSize: 12, color: "#6b7280" }, children: "The current timeline matches the baseline." })
459
+ ] });
460
+ }
461
+ return /* @__PURE__ */ jsx("div", { style: styles.lineList, children: diff.entries.map((entry, i) => /* @__PURE__ */ jsxs(
462
+ "div",
463
+ {
464
+ style: { ...styles.finding, background: "#fff7fb", borderLeftWidth: 3, borderLeftStyle: "solid", borderLeftColor: SEVERITY_COLORS[entry.severity] },
465
+ children: [
466
+ /* @__PURE__ */ jsx("div", { style: { ...styles.findingSeverity, color: SEVERITY_COLORS[entry.severity] }, children: entry.severity.toUpperCase() }),
467
+ /* @__PURE__ */ jsxs("div", { children: [
468
+ /* @__PURE__ */ jsx("p", { style: styles.findingMessage, children: entry.message }),
469
+ /* @__PURE__ */ jsxs("p", { style: styles.findingSelector, children: [
470
+ entry.event.type,
471
+ " \xB7 ",
472
+ entry.event.target.selector
473
+ ] })
474
+ ] })
475
+ ]
476
+ },
477
+ i
478
+ )) });
479
+ }
480
+ var styles = {
481
+ container: {
482
+ display: "flex",
483
+ flexDirection: "column",
484
+ height: "100%",
485
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
486
+ fontSize: 13,
487
+ color: "#1e293b",
488
+ background: "#ffffff"
489
+ },
490
+ tabBar: {
491
+ display: "flex",
492
+ borderBottom: "1px solid #e2e8f0",
493
+ padding: "0 12px",
494
+ gap: 0,
495
+ background: "#f8fafc"
496
+ },
497
+ tab: {
498
+ padding: "10px 14px",
499
+ fontSize: 12,
500
+ fontWeight: 600,
501
+ color: "#64748b",
502
+ background: "none",
503
+ borderTop: "none",
504
+ borderLeft: "none",
505
+ borderRight: "none",
506
+ borderBottomWidth: 2,
507
+ borderBottomStyle: "solid",
508
+ borderBottomColor: "transparent",
509
+ cursor: "pointer",
510
+ transition: "color 0.15s, border-color 0.15s",
511
+ display: "flex",
512
+ alignItems: "center",
513
+ gap: 6,
514
+ outline: "none"
515
+ },
516
+ tabActive: {
517
+ color: "#1e293b"
518
+ },
519
+ badge: {
520
+ background: "#ef4444",
521
+ color: "#fff",
522
+ fontSize: 10,
523
+ fontWeight: 700,
524
+ borderRadius: 10,
525
+ padding: "1px 6px",
526
+ minWidth: 18,
527
+ textAlign: "center"
528
+ },
529
+ statsBar: {
530
+ display: "flex",
531
+ gap: 16,
532
+ padding: "8px 16px",
533
+ borderBottom: "1px solid #f1f5f9",
534
+ background: "#f8fafc"
535
+ },
536
+ baselineBar: {
537
+ display: "flex",
538
+ alignItems: "center",
539
+ gap: 12,
540
+ padding: "8px 16px",
541
+ borderBottom: "1px solid #f1f5f9",
542
+ background: "#f8fafc"
543
+ },
544
+ baselineHint: {
545
+ fontSize: 11,
546
+ color: "#64748b",
547
+ marginLeft: "auto"
548
+ },
549
+ baselineButton: {
550
+ fontSize: 11,
551
+ fontWeight: 600,
552
+ color: "#fff",
553
+ background: "#10b981",
554
+ border: "none",
555
+ borderRadius: 4,
556
+ padding: "4px 10px",
557
+ cursor: "pointer"
558
+ },
559
+ stat: {
560
+ fontSize: 11,
561
+ color: "#64748b",
562
+ fontWeight: 500
563
+ },
564
+ content: {
565
+ flex: 1,
566
+ overflow: "auto",
567
+ padding: 0
568
+ },
569
+ lineList: {
570
+ padding: "8px 0"
571
+ },
572
+ line: {
573
+ display: "flex",
574
+ alignItems: "flex-start",
575
+ gap: 12,
576
+ padding: "6px 16px",
577
+ borderBottom: "1px solid #f8fafc",
578
+ transition: "background 0.1s"
579
+ },
580
+ lineNumber: {
581
+ fontSize: 10,
582
+ color: "#94a3b8",
583
+ fontFamily: "monospace",
584
+ minWidth: 20,
585
+ textAlign: "right",
586
+ paddingTop: 2
587
+ },
588
+ lineText: {
589
+ fontSize: 13,
590
+ color: "#1e293b",
591
+ fontFamily: "monospace",
592
+ lineHeight: 1.5
593
+ },
594
+ emptyContent: {
595
+ padding: 24,
596
+ textAlign: "center",
597
+ color: "#94a3b8"
598
+ },
599
+ footer: {
600
+ display: "flex",
601
+ justifyContent: "space-between",
602
+ alignItems: "center",
603
+ padding: "8px 16px",
604
+ borderTop: "1px solid #e2e8f0",
605
+ background: "#f8fafc"
606
+ },
607
+ footerText: {
608
+ fontSize: 11,
609
+ color: "#94a3b8",
610
+ fontStyle: "italic"
611
+ },
612
+ footerLink: {
613
+ fontSize: 11,
614
+ color: "#3b82f6",
615
+ textDecoration: "none",
616
+ fontWeight: 600
617
+ },
618
+ emptyState: {
619
+ display: "flex",
620
+ flexDirection: "column",
621
+ alignItems: "center",
622
+ justifyContent: "center",
623
+ height: "100%",
624
+ gap: 8,
625
+ padding: 32
626
+ },
627
+ emptyIcon: {
628
+ fontSize: 32
629
+ },
630
+ emptyText: {
631
+ fontSize: 14,
632
+ fontWeight: 600,
633
+ color: "#475569"
634
+ },
635
+ emptySubtext: {
636
+ fontSize: 12,
637
+ color: "#94a3b8"
638
+ },
639
+ auditPass: {
640
+ display: "flex",
641
+ flexDirection: "column",
642
+ alignItems: "center",
643
+ gap: 8,
644
+ padding: 32,
645
+ color: "#059669"
646
+ },
647
+ finding: {
648
+ display: "flex",
649
+ alignItems: "flex-start",
650
+ gap: 10,
651
+ padding: "10px 16px",
652
+ margin: "4px 12px",
653
+ borderRadius: 4,
654
+ background: "#fefce8"
655
+ },
656
+ findingSeverity: {
657
+ fontSize: 14,
658
+ fontWeight: 700,
659
+ paddingTop: 1
660
+ },
661
+ findingMessage: {
662
+ margin: 0,
663
+ fontSize: 13,
664
+ fontWeight: 500,
665
+ color: "#1e293b"
666
+ },
667
+ findingSelector: {
668
+ margin: "2px 0 0",
669
+ fontSize: 11,
670
+ color: "#64748b",
671
+ fontFamily: "monospace"
672
+ }
673
+ };
674
+ function SpeakableIcon() {
675
+ return /* @__PURE__ */ jsxs(
676
+ "svg",
677
+ {
678
+ xmlns: "http://www.w3.org/2000/svg",
679
+ viewBox: "0 0 32 32",
680
+ fill: "none",
681
+ style: { width: 14, height: 14, marginRight: 4, verticalAlign: "middle" },
682
+ children: [
683
+ /* @__PURE__ */ jsx("rect", { x: "2", y: "4", width: "28", height: "20", rx: "4", fill: "#0D9488" }),
684
+ /* @__PURE__ */ jsx("polygon", { points: "8,24 14,24 10,30", fill: "#0D9488" }),
685
+ /* @__PURE__ */ jsx("path", { d: "M12 11 L12 17", stroke: "white", strokeWidth: "2.5", strokeLinecap: "round" }),
686
+ /* @__PURE__ */ jsx("path", { d: "M16 9 L16 19", stroke: "white", strokeWidth: "2.5", strokeLinecap: "round" }),
687
+ /* @__PURE__ */ jsx("path", { d: "M20 11 L20 17", stroke: "white", strokeWidth: "2.5", strokeLinecap: "round" })
688
+ ]
689
+ }
690
+ );
691
+ }
692
+ addons.register(ADDON_ID, () => {
693
+ addons.add(PANEL_ID, {
694
+ type: types.PANEL,
695
+ title: /* @__PURE__ */ jsxs("span", { style: { display: "inline-flex", alignItems: "center" }, children: [
696
+ /* @__PURE__ */ jsx(SpeakableIcon, {}),
697
+ "Screen Readers"
698
+ ] }),
699
+ render: ({ active }) => active ? /* @__PURE__ */ jsx(SpeakablePanel, {}) : null
700
+ });
701
+ });