@reticular/speakable 1.6.0 → 1.8.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.
- package/README.md +86 -2
- package/dist/browser/index.d.ts +58 -0
- package/dist/browser/index.js +4100 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/chunk-WYFXGXYU.js +87 -0
- package/dist/chunk-WYFXGXYU.js.map +1 -0
- package/dist/cli.js +164 -21
- package/dist/cli.js.map +1 -1
- package/dist/harness/index.d.ts +57 -0
- package/dist/harness/index.js +176 -0
- package/dist/harness/index.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +168 -24
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +164 -21
- package/dist/mcp.js.map +1 -1
- package/dist/speakable-browser.global.js +10 -0
- package/dist/speakable-browser.global.js.map +1 -0
- package/dist/storybook/dist/chunk-3SVI6IQE.js +86 -0
- package/dist/storybook/dist/{chunk-CL3DJBD7.js → chunk-JSBRDJBE.js} +1 -11
- package/dist/storybook/dist/chunk-QVJRQZT5.js +16 -0
- package/dist/storybook/dist/index.js +6 -1
- package/dist/storybook/dist/manager.js +404 -5
- package/dist/storybook/dist/preview.js +2241 -49
- package/dist/storybook/dist/upgrade-5GL42JH4.js +7 -0
- package/dist/types-Blh0EnZF.d.ts +213 -0
- package/dist/upgrade-53M5VYGH.js +7 -0
- package/dist/upgrade-53M5VYGH.js.map +1 -0
- package/package.json +13 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// ../src/browser/upgrade.ts
|
|
2
|
+
var DEFAULT_UPGRADE_TIMEOUT_MS = 2e3;
|
|
3
|
+
function withTimeout(promise, timeoutMs) {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
let settled = false;
|
|
6
|
+
const timer = setTimeout(() => {
|
|
7
|
+
if (!settled) {
|
|
8
|
+
settled = true;
|
|
9
|
+
resolve(false);
|
|
10
|
+
}
|
|
11
|
+
}, timeoutMs);
|
|
12
|
+
promise.then(
|
|
13
|
+
() => {
|
|
14
|
+
if (!settled) {
|
|
15
|
+
settled = true;
|
|
16
|
+
clearTimeout(timer);
|
|
17
|
+
resolve(true);
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
() => {
|
|
21
|
+
if (!settled) {
|
|
22
|
+
settled = true;
|
|
23
|
+
clearTimeout(timer);
|
|
24
|
+
resolve(true);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function collectCustomElementTags(root) {
|
|
31
|
+
const tags = /* @__PURE__ */ new Set();
|
|
32
|
+
const rootEl = "documentElement" in root ? root.documentElement : root;
|
|
33
|
+
if (!rootEl) return [];
|
|
34
|
+
const consider = (el) => {
|
|
35
|
+
if (el.localName && el.localName.includes("-")) {
|
|
36
|
+
tags.add(el.localName);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
consider(rootEl);
|
|
40
|
+
const all = rootEl.querySelectorAll("*");
|
|
41
|
+
all.forEach(consider);
|
|
42
|
+
return Array.from(tags);
|
|
43
|
+
}
|
|
44
|
+
function collectUpdateCompletables(root) {
|
|
45
|
+
const rootEl = "documentElement" in root ? root.documentElement : root;
|
|
46
|
+
if (!rootEl) return [];
|
|
47
|
+
const promises = [];
|
|
48
|
+
const consider = (el) => {
|
|
49
|
+
const uc = el.updateComplete;
|
|
50
|
+
if (uc && typeof uc.then === "function") {
|
|
51
|
+
promises.push(uc);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
consider(rootEl);
|
|
55
|
+
rootEl.querySelectorAll("*").forEach(consider);
|
|
56
|
+
return promises;
|
|
57
|
+
}
|
|
58
|
+
async function awaitCustomElementsReady(root, timeoutMs = DEFAULT_UPGRADE_TIMEOUT_MS) {
|
|
59
|
+
const warnings = [];
|
|
60
|
+
const hasCustomElements = typeof customElements !== "undefined" && customElements !== null;
|
|
61
|
+
if (!hasCustomElements) {
|
|
62
|
+
return warnings;
|
|
63
|
+
}
|
|
64
|
+
const tags = collectCustomElementTags(root);
|
|
65
|
+
await Promise.all(
|
|
66
|
+
tags.map(async (tag) => {
|
|
67
|
+
if (customElements.get(tag)) return;
|
|
68
|
+
const defined = await withTimeout(customElements.whenDefined(tag), timeoutMs);
|
|
69
|
+
if (!defined) {
|
|
70
|
+
warnings.push({
|
|
71
|
+
tag,
|
|
72
|
+
message: `Custom element "${tag}" did not upgrade within ${timeoutMs}ms; analysis may reflect a pre-hydration state.`
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
const updateCompletables = collectUpdateCompletables(root);
|
|
78
|
+
if (updateCompletables.length > 0) {
|
|
79
|
+
await withTimeout(Promise.all(updateCompletables), timeoutMs);
|
|
80
|
+
}
|
|
81
|
+
return warnings;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export {
|
|
85
|
+
awaitCustomElementsReady
|
|
86
|
+
};
|
|
@@ -24,17 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
mod
|
|
25
25
|
));
|
|
26
26
|
|
|
27
|
-
// src/index.ts
|
|
28
|
-
var ADDON_ID = "speakable";
|
|
29
|
-
var PANEL_ID = `${ADDON_ID}/panel`;
|
|
30
|
-
var EVENT_RENDER = `${ADDON_ID}/render`;
|
|
31
|
-
var EVENT_RESULT = `${ADDON_ID}/result`;
|
|
32
|
-
|
|
33
27
|
export {
|
|
34
28
|
__commonJS,
|
|
35
|
-
__toESM
|
|
36
|
-
ADDON_ID,
|
|
37
|
-
PANEL_ID,
|
|
38
|
-
EVENT_RENDER,
|
|
39
|
-
EVENT_RESULT
|
|
29
|
+
__toESM
|
|
40
30
|
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var ADDON_ID = "speakable";
|
|
3
|
+
var PANEL_ID = `${ADDON_ID}/panel`;
|
|
4
|
+
var EVENT_RENDER = `${ADDON_ID}/render`;
|
|
5
|
+
var EVENT_RESULT = `${ADDON_ID}/result`;
|
|
6
|
+
var EVENT_TIMELINE = `${ADDON_ID}/timeline`;
|
|
7
|
+
var EVENT_DIFF = `${ADDON_ID}/diff`;
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
ADDON_ID,
|
|
11
|
+
PANEL_ID,
|
|
12
|
+
EVENT_RENDER,
|
|
13
|
+
EVENT_RESULT,
|
|
14
|
+
EVENT_TIMELINE,
|
|
15
|
+
EVENT_DIFF
|
|
16
|
+
};
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ADDON_ID,
|
|
3
|
+
EVENT_DIFF,
|
|
3
4
|
EVENT_RENDER,
|
|
4
5
|
EVENT_RESULT,
|
|
6
|
+
EVENT_TIMELINE,
|
|
5
7
|
PANEL_ID
|
|
6
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-QVJRQZT5.js";
|
|
9
|
+
import "./chunk-JSBRDJBE.js";
|
|
7
10
|
export {
|
|
8
11
|
ADDON_ID,
|
|
12
|
+
EVENT_DIFF,
|
|
9
13
|
EVENT_RENDER,
|
|
10
14
|
EVENT_RESULT,
|
|
15
|
+
EVENT_TIMELINE,
|
|
11
16
|
PANEL_ID
|
|
12
17
|
};
|
|
@@ -1,31 +1,325 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ADDON_ID,
|
|
3
3
|
EVENT_RESULT,
|
|
4
|
+
EVENT_TIMELINE,
|
|
4
5
|
PANEL_ID
|
|
5
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-QVJRQZT5.js";
|
|
7
|
+
import "./chunk-JSBRDJBE.js";
|
|
6
8
|
|
|
7
9
|
// src/manager.tsx
|
|
8
10
|
import { useState, useEffect } from "react";
|
|
9
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
|
|
10
244
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
245
|
var READER_TABS = [
|
|
12
246
|
{ key: "nvda", label: "NVDA", color: "#3b82f6" },
|
|
13
247
|
{ key: "jaws", label: "JAWS", color: "#8b5cf6" },
|
|
14
248
|
{ key: "voiceover", label: "VoiceOver", color: "#06b6d4" },
|
|
15
249
|
{ key: "narrator", label: "Narrator", color: "#f59e0b" },
|
|
16
|
-
{ key: "audit", label: "Audit", color: "#ef4444" }
|
|
250
|
+
{ key: "audit", label: "Audit", color: "#ef4444" },
|
|
251
|
+
{ key: "timeline", label: "Timeline", color: "#10b981" },
|
|
252
|
+
{ key: "diff", label: "Diff", color: "#ec4899" }
|
|
17
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
|
+
}
|
|
18
286
|
function SpeakablePanel() {
|
|
19
287
|
const [result, setResult] = useState(null);
|
|
288
|
+
const [timeline, setTimeline] = useState(null);
|
|
289
|
+
const [diff, setDiff] = useState(null);
|
|
290
|
+
const [hasBaseline, setHasBaseline] = useState(false);
|
|
20
291
|
const [activeTab, setActiveTab] = useState("nvda");
|
|
21
292
|
useEffect(() => {
|
|
22
293
|
const channel = addons.getChannel();
|
|
23
294
|
const handleResult = (data) => {
|
|
24
295
|
setResult(data);
|
|
25
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
|
+
};
|
|
26
309
|
channel.on(EVENT_RESULT, handleResult);
|
|
27
|
-
|
|
310
|
+
channel.on(EVENT_TIMELINE, handleTimeline);
|
|
311
|
+
return () => {
|
|
312
|
+
channel.off(EVENT_RESULT, handleResult);
|
|
313
|
+
channel.off(EVENT_TIMELINE, handleTimeline);
|
|
314
|
+
};
|
|
28
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
|
+
};
|
|
29
323
|
if (!result) {
|
|
30
324
|
return /* @__PURE__ */ jsxs("div", { style: styles.emptyState, children: [
|
|
31
325
|
/* @__PURE__ */ jsx("div", { style: styles.emptyIcon, children: "\u{1F50A}" }),
|
|
@@ -33,7 +327,9 @@ function SpeakablePanel() {
|
|
|
33
327
|
/* @__PURE__ */ jsx("p", { style: styles.emptySubtext, children: "Powered by Speakable" })
|
|
34
328
|
] });
|
|
35
329
|
}
|
|
36
|
-
const
|
|
330
|
+
const READER_KEYS = ["nvda", "jaws", "voiceover", "narrator"];
|
|
331
|
+
const isReaderTab = READER_KEYS.includes(activeTab);
|
|
332
|
+
const activeLines = isReaderTab ? result[activeTab] : [];
|
|
37
333
|
const auditFindings = result.audit;
|
|
38
334
|
return /* @__PURE__ */ jsxs("div", { style: styles.container, children: [
|
|
39
335
|
/* @__PURE__ */ jsx("div", { style: styles.tabBar, children: READER_TABS.map((tab) => /* @__PURE__ */ jsxs(
|
|
@@ -69,7 +365,7 @@ function SpeakablePanel() {
|
|
|
69
365
|
" headings"
|
|
70
366
|
] })
|
|
71
367
|
] }),
|
|
72
|
-
/* @__PURE__ */ jsx("div", { style: styles.content, children: activeTab === "audit" ? /* @__PURE__ */ jsx(AuditView, { findings: auditFindings }) : /* @__PURE__ */ jsx(AnnouncementView, { lines: activeLines, readerKey: activeTab }) }),
|
|
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 }) }),
|
|
73
369
|
/* @__PURE__ */ jsxs("div", { style: styles.footer, children: [
|
|
74
370
|
/* @__PURE__ */ jsx("span", { style: styles.footerText, children: "Predictions are heuristic-based. Verify with real screen readers before release." }),
|
|
75
371
|
/* @__PURE__ */ jsx("a", { href: "https://getspeakable.dev", target: "_blank", rel: "noopener noreferrer", style: styles.footerLink, children: "getspeakable.dev" })
|
|
@@ -101,6 +397,86 @@ function AuditView({ findings }) {
|
|
|
101
397
|
] })
|
|
102
398
|
] }, i)) });
|
|
103
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
|
+
}
|
|
104
480
|
var styles = {
|
|
105
481
|
container: {
|
|
106
482
|
display: "flex",
|
|
@@ -157,6 +533,29 @@ var styles = {
|
|
|
157
533
|
borderBottom: "1px solid #f1f5f9",
|
|
158
534
|
background: "#f8fafc"
|
|
159
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
|
+
},
|
|
160
559
|
stat: {
|
|
161
560
|
fontSize: 11,
|
|
162
561
|
color: "#64748b",
|