@reticular/speakable 1.6.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.
@@ -1,8 +1,14 @@
1
1
  import {
2
2
  EVENT_RESULT,
3
+ EVENT_TIMELINE
4
+ } from "./chunk-QVJRQZT5.js";
5
+ import {
6
+ awaitCustomElementsReady
7
+ } from "./chunk-3SVI6IQE.js";
8
+ import {
3
9
  __commonJS,
4
10
  __toESM
5
- } from "./chunk-CL3DJBD7.js";
11
+ } from "./chunk-JSBRDJBE.js";
6
12
 
7
13
  // ../node_modules/picocolors/picocolors.browser.js
8
14
  var require_picocolors_browser = __commonJS({
@@ -806,17 +812,54 @@ function processChildNode(node, children, warnings) {
806
812
  }
807
813
  }
808
814
  function collectChildrenFromRolelessElement(element, warnings) {
815
+ return collectAccessibleChildren(element, warnings);
816
+ }
817
+ function collectAccessibleChildren(element, warnings) {
809
818
  const children = [];
810
819
  if (element.shadowRoot) {
811
820
  for (const child of Array.from(element.shadowRoot.childNodes)) {
812
- processChildNode(child, children, warnings);
821
+ processShadowNode(child, children, warnings);
813
822
  }
823
+ return children;
824
+ }
825
+ if (element.localName && element.localName.includes("-") && element.childNodes.length === 0) {
826
+ warnings.push({
827
+ message: `Custom element <${element.localName}> has no accessible children and may have a closed shadow root; its content could not be analyzed.`,
828
+ element
829
+ });
814
830
  }
815
831
  for (const child of Array.from(element.childNodes)) {
816
832
  processChildNode(child, children, warnings);
817
833
  }
818
834
  return children;
819
835
  }
836
+ function processShadowNode(node, children, warnings) {
837
+ if (node.nodeType === 1) {
838
+ const element = node;
839
+ if (element.localName === "slot") {
840
+ const slot = element;
841
+ let assigned = [];
842
+ if (typeof slot.assignedNodes === "function") {
843
+ try {
844
+ assigned = Array.from(slot.assignedNodes({ flatten: true }));
845
+ } catch {
846
+ assigned = [];
847
+ }
848
+ }
849
+ if (assigned.length > 0) {
850
+ for (const assignedNode of assigned) {
851
+ processChildNode(assignedNode, children, warnings);
852
+ }
853
+ } else {
854
+ for (const child of Array.from(slot.childNodes)) {
855
+ processShadowNode(child, children, warnings);
856
+ }
857
+ }
858
+ return;
859
+ }
860
+ }
861
+ processChildNode(node, children, warnings);
862
+ }
820
863
  function buildAccessibilityTree(rootElement, sourceHash) {
821
864
  const warnings = [];
822
865
  if (isHidden(rootElement)) {
@@ -884,15 +927,7 @@ function buildNodeRecursive(element, warnings) {
884
927
  element: w.element
885
928
  })));
886
929
  const focusInfo = extractFocusInfo(element);
887
- const children = [];
888
- if (element.shadowRoot) {
889
- for (const child of Array.from(element.shadowRoot.childNodes)) {
890
- processChildNode(child, children, warnings);
891
- }
892
- }
893
- for (const child of Array.from(element.childNodes)) {
894
- processChildNode(child, children, warnings);
895
- }
930
+ const children = collectAccessibleChildren(element, warnings);
896
931
  const node = {
897
932
  role: roleResult.role,
898
933
  name: nameResult.name,
@@ -905,15 +940,7 @@ function buildNodeRecursive(element, warnings) {
905
940
  return node;
906
941
  }
907
942
  function createGenericContainer(rootElement, warnings) {
908
- const children = [];
909
- if (rootElement.shadowRoot) {
910
- for (const child of Array.from(rootElement.shadowRoot.childNodes)) {
911
- processChildNode(child, children, warnings);
912
- }
913
- }
914
- for (const child of Array.from(rootElement.childNodes)) {
915
- processChildNode(child, children, warnings);
916
- }
943
+ const children = collectAccessibleChildren(rootElement, warnings);
917
944
  return {
918
945
  role: "generic",
919
946
  name: "",
@@ -1999,7 +2026,7 @@ function detectInteractiveIssues(elements, issues) {
1999
2026
  });
2000
2027
  }
2001
2028
 
2002
- // src/analyzer.ts
2029
+ // ../src/browser/analyze.ts
2003
2030
  var LANDMARK_ROLES = /* @__PURE__ */ new Set([
2004
2031
  "navigation",
2005
2032
  "main",
@@ -2042,37 +2069,2078 @@ function convertAuditFindings(model) {
2042
2069
  selector: issue.element ? `${issue.element.role}${issue.element.name ? ` "${issue.element.name}"` : ""}` : issue.suggestion || ""
2043
2070
  }));
2044
2071
  }
2045
- function analyzeDOM(root) {
2046
- const { model } = buildAccessibilityTree(root);
2047
- const nvdaText = renderNVDA(model, false);
2048
- const jawsText = renderJAWS(model, false);
2049
- const voiceoverText = renderVoiceOver(model, false);
2050
- const narratorText = renderNarrator(model, false);
2051
- const nvda = nvdaText.split("\n").filter(Boolean);
2052
- const jaws = jawsText.split("\n").filter(Boolean);
2053
- const voiceover = voiceoverText.split("\n").filter(Boolean);
2054
- const narrator = narratorText.split("\n").filter(Boolean);
2072
+ function emptyResult(extraWarnings = []) {
2073
+ return {
2074
+ nvda: [],
2075
+ jaws: [],
2076
+ voiceover: [],
2077
+ narrator: [],
2078
+ audit: [],
2079
+ stats: { totalElements: 0, interactiveElements: 0, landmarks: 0, headings: 0 },
2080
+ warnings: [...extraWarnings]
2081
+ };
2082
+ }
2083
+ function analyzeElement(root, extraWarnings = []) {
2084
+ if (!root) {
2085
+ return emptyResult(extraWarnings);
2086
+ }
2087
+ let built;
2088
+ try {
2089
+ built = buildAccessibilityTree(root);
2090
+ } catch (err) {
2091
+ return emptyResult([
2092
+ ...extraWarnings,
2093
+ `Analysis failed: ${err instanceof Error ? err.message : String(err)}`
2094
+ ]);
2095
+ }
2096
+ const { model, warnings: treeWarnings } = built;
2097
+ const nvda = renderNVDA(model, false).split("\n").filter(Boolean);
2098
+ const jaws = renderJAWS(model, false).split("\n").filter(Boolean);
2099
+ const voiceover = renderVoiceOver(model, false).split("\n").filter(Boolean);
2100
+ const narrator = renderNarrator(model, false).split("\n").filter(Boolean);
2055
2101
  const audit = convertAuditFindings(model);
2056
2102
  const stats = countStats(model.root);
2057
- return { nvda, jaws, voiceover, narrator, audit, stats };
2103
+ const warnings = [
2104
+ ...extraWarnings,
2105
+ ...treeWarnings.map((w) => w.message)
2106
+ ];
2107
+ return { nvda, jaws, voiceover, narrator, audit, stats, warnings };
2058
2108
  }
2059
2109
 
2060
- // src/preview.ts
2061
- var currentObserver = null;
2062
- var debounceTimer = null;
2063
- function getChannel() {
2064
- return window.__STORYBOOK_ADDONS_CHANNEL__;
2110
+ // src/analyzer.ts
2111
+ function analyzeDOM(root) {
2112
+ return analyzeElement(root);
2065
2113
  }
2066
- function findStoryRoot() {
2067
- return document.getElementById("storybook-root") || document.getElementById("root") || document.body;
2114
+
2115
+ // ../src/runtime/selector.ts
2116
+ function generateSelector(element) {
2117
+ if (element.id) {
2118
+ return `#${escapeCSSIdentifier(element.id)}`;
2119
+ }
2120
+ const document2 = element.ownerDocument;
2121
+ if (!document2) {
2122
+ return getTagName(element);
2123
+ }
2124
+ const segments = [];
2125
+ let current = element;
2126
+ while (current && current !== document2.documentElement) {
2127
+ const segment = buildSegment(current, document2);
2128
+ segments.unshift(segment);
2129
+ const partialSelector = segments.join(" > ");
2130
+ try {
2131
+ const matches = document2.querySelectorAll(partialSelector);
2132
+ if (matches.length === 1 && matches[0] === element) {
2133
+ return partialSelector;
2134
+ }
2135
+ } catch {
2136
+ }
2137
+ current = current.parentElement;
2138
+ }
2139
+ if (segments.length === 0) {
2140
+ return getTagName(element);
2141
+ }
2142
+ const fullSelector = segments.join(" > ");
2143
+ return fullSelector;
2068
2144
  }
2069
- function runAnalysis() {
2070
- const root = findStoryRoot();
2071
- if (!root) return;
2072
- const analysis = analyzeDOM(root);
2073
- const channel = getChannel();
2074
- if (channel) {
2075
- channel.emit(EVENT_RESULT, analysis);
2145
+ function buildSegment(element, _document) {
2146
+ const tag = getTagName(element);
2147
+ const parent = element.parentElement;
2148
+ if (parent) {
2149
+ const siblingsWithSameTag = Array.from(parent.children).filter(
2150
+ (child) => getTagName(child) === tag
2151
+ );
2152
+ if (siblingsWithSameTag.length === 1) {
2153
+ return tag;
2154
+ }
2155
+ } else {
2156
+ return tag;
2157
+ }
2158
+ const classes = getStableClasses(element);
2159
+ if (classes.length > 0) {
2160
+ const tagWithClasses = `${tag}.${classes.join(".")}`;
2161
+ if (parent) {
2162
+ const siblingsMatchingClasses = Array.from(parent.children).filter(
2163
+ (child) => {
2164
+ if (getTagName(child) !== tag) return false;
2165
+ return classes.every((cls) => child.classList.contains(cls));
2166
+ }
2167
+ );
2168
+ if (siblingsMatchingClasses.length === 1) {
2169
+ return tagWithClasses;
2170
+ }
2171
+ }
2172
+ }
2173
+ const index = getNthChildIndex(element);
2174
+ return `${tag}:nth-child(${index})`;
2175
+ }
2176
+ function getTagName(element) {
2177
+ return element.tagName.toLowerCase();
2178
+ }
2179
+ function getStableClasses(element) {
2180
+ const classes = Array.from(element.classList);
2181
+ if (classes.length === 0) return [];
2182
+ const stableClasses = classes.filter((cls) => !looksLikeDynamicClass(cls));
2183
+ return stableClasses.sort().map(escapeCSSIdentifier);
2184
+ }
2185
+ function looksLikeDynamicClass(className) {
2186
+ if (/^[a-f0-9]{5,}$/i.test(className)) return true;
2187
+ if (/[_-][a-f0-9]{5,}$/i.test(className)) return true;
2188
+ if (/_{2,}[A-Za-z0-9]{4,}$/.test(className)) return true;
2189
+ return false;
2190
+ }
2191
+ function getNthChildIndex(element) {
2192
+ const parent = element.parentElement;
2193
+ if (!parent) return 1;
2194
+ const children = Array.from(parent.children);
2195
+ const index = children.indexOf(element);
2196
+ return index + 1;
2197
+ }
2198
+ function escapeCSSIdentifier(value) {
2199
+ if (!value) return value;
2200
+ let result = "";
2201
+ for (let i = 0; i < value.length; i++) {
2202
+ const char = value[i];
2203
+ const code = char.charCodeAt(0);
2204
+ if (i === 0 && code >= 48 && code <= 57) {
2205
+ result += `\\3${char} `;
2206
+ continue;
2207
+ }
2208
+ if (char === "!" || char === '"' || char === "#" || char === "$" || char === "%" || char === "&" || char === "'" || char === "(" || char === ")" || char === "*" || char === "+" || char === "," || char === "." || char === "/" || char === ":" || char === ";" || char === "<" || char === "=" || char === ">" || char === "?" || char === "@" || char === "[" || char === "\\" || char === "]" || char === "^" || char === "`" || char === "{" || char === "|" || char === "}" || char === "~" || char === " ") {
2209
+ result += `\\${char}`;
2210
+ continue;
2211
+ }
2212
+ result += char;
2213
+ }
2214
+ return result;
2215
+ }
2216
+
2217
+ // ../src/runtime/observers/focus-observer.ts
2218
+ function computeAccessibleName2(element, document2) {
2219
+ const ariaLabel = element.getAttribute("aria-label");
2220
+ if (ariaLabel && ariaLabel.trim()) {
2221
+ return ariaLabel.trim();
2222
+ }
2223
+ const labelledBy = element.getAttribute("aria-labelledby");
2224
+ if (labelledBy && labelledBy.trim()) {
2225
+ const ids = labelledBy.trim().split(/\s+/);
2226
+ const parts = [];
2227
+ for (const id of ids) {
2228
+ const referenced = document2.getElementById(id);
2229
+ if (referenced && referenced.textContent) {
2230
+ parts.push(referenced.textContent.trim());
2231
+ }
2232
+ }
2233
+ const combined = parts.join(" ");
2234
+ if (combined) {
2235
+ return combined;
2236
+ }
2237
+ }
2238
+ const textContent = element.textContent;
2239
+ if (textContent && textContent.trim()) {
2240
+ return textContent.trim();
2241
+ }
2242
+ return "";
2243
+ }
2244
+ function getElementRole(element) {
2245
+ const explicitRole = element.getAttribute("role");
2246
+ if (explicitRole && explicitRole.trim()) {
2247
+ return explicitRole.trim();
2248
+ }
2249
+ const tagName = element.tagName.toLowerCase();
2250
+ switch (tagName) {
2251
+ case "button":
2252
+ return "button";
2253
+ case "a":
2254
+ return element.hasAttribute("href") ? "link" : "generic";
2255
+ case "input": {
2256
+ const type = (element.getAttribute("type") || "text").toLowerCase();
2257
+ switch (type) {
2258
+ case "button":
2259
+ case "submit":
2260
+ case "reset":
2261
+ case "image":
2262
+ return "button";
2263
+ case "checkbox":
2264
+ return "checkbox";
2265
+ case "radio":
2266
+ return "radio";
2267
+ case "range":
2268
+ return "slider";
2269
+ case "search":
2270
+ return "searchbox";
2271
+ default:
2272
+ return "textbox";
2273
+ }
2274
+ }
2275
+ case "textarea":
2276
+ return "textbox";
2277
+ case "select":
2278
+ return element.hasAttribute("multiple") ? "listbox" : "combobox";
2279
+ case "nav":
2280
+ return "navigation";
2281
+ case "main":
2282
+ return "main";
2283
+ case "header":
2284
+ return "banner";
2285
+ case "footer":
2286
+ return "contentinfo";
2287
+ case "aside":
2288
+ return "complementary";
2289
+ case "form":
2290
+ return "form";
2291
+ case "section":
2292
+ return element.hasAttribute("aria-label") || element.hasAttribute("aria-labelledby") ? "region" : "generic";
2293
+ case "article":
2294
+ return "article";
2295
+ case "dialog":
2296
+ return "dialog";
2297
+ case "table":
2298
+ return "table";
2299
+ case "ul":
2300
+ case "ol":
2301
+ return "list";
2302
+ case "li":
2303
+ return "listitem";
2304
+ case "img":
2305
+ return element.getAttribute("alt") === "" ? "presentation" : "img";
2306
+ case "h1":
2307
+ case "h2":
2308
+ case "h3":
2309
+ case "h4":
2310
+ case "h5":
2311
+ case "h6":
2312
+ return "heading";
2313
+ case "option":
2314
+ return "option";
2315
+ case "progress":
2316
+ return "progressbar";
2317
+ case "output":
2318
+ return "status";
2319
+ case "menu":
2320
+ return "menu";
2321
+ default:
2322
+ return "generic";
2323
+ }
2324
+ }
2325
+ function createFocusObserver(options) {
2326
+ const { document: doc, onEvent, getTimestamp } = options;
2327
+ let active = false;
2328
+ let previousTarget = null;
2329
+ function buildEventTarget2(element) {
2330
+ return {
2331
+ role: getElementRole(element),
2332
+ accessibleName: computeAccessibleName2(element, doc),
2333
+ selector: generateSelector(element)
2334
+ };
2335
+ }
2336
+ function handleFocusIn(event) {
2337
+ if (!active) return;
2338
+ const target = event.target;
2339
+ if (!target || !(target instanceof doc.defaultView.Element)) {
2340
+ return;
2341
+ }
2342
+ const currentTarget = buildEventTarget2(target);
2343
+ const a11yEvent = {
2344
+ type: "FOCUS_CHANGED",
2345
+ timestamp: getTimestamp(),
2346
+ target: currentTarget,
2347
+ payload: {
2348
+ kind: "focus_changed",
2349
+ previousTarget
2350
+ }
2351
+ };
2352
+ onEvent(a11yEvent);
2353
+ previousTarget = currentTarget;
2354
+ }
2355
+ return {
2356
+ start() {
2357
+ if (active) return;
2358
+ active = true;
2359
+ previousTarget = null;
2360
+ doc.addEventListener("focusin", handleFocusIn);
2361
+ },
2362
+ stop() {
2363
+ if (!active) return;
2364
+ active = false;
2365
+ doc.removeEventListener("focusin", handleFocusIn);
2366
+ },
2367
+ get isActive() {
2368
+ return active;
2369
+ }
2370
+ };
2371
+ }
2372
+
2373
+ // ../src/runtime/observers/mutation-observer.ts
2374
+ var ROLE_ATTRIBUTE = "role";
2375
+ var NAME_ATTRIBUTES = /* @__PURE__ */ new Set(["aria-label", "aria-labelledby"]);
2376
+ var STATE_ATTRIBUTES = /* @__PURE__ */ new Set([
2377
+ "aria-expanded",
2378
+ "aria-selected",
2379
+ "aria-checked",
2380
+ "aria-disabled",
2381
+ "aria-hidden"
2382
+ ]);
2383
+ var OBSERVED_ATTRIBUTES = [
2384
+ ROLE_ATTRIBUTE,
2385
+ "aria-label",
2386
+ "aria-labelledby",
2387
+ "aria-expanded",
2388
+ "aria-selected",
2389
+ "aria-checked",
2390
+ "aria-disabled",
2391
+ "aria-hidden"
2392
+ ];
2393
+ function computeAccessibleName3(element, document2) {
2394
+ const ariaLabel = element.getAttribute("aria-label");
2395
+ if (ariaLabel && ariaLabel.trim()) {
2396
+ return ariaLabel.trim();
2397
+ }
2398
+ const labelledBy = element.getAttribute("aria-labelledby");
2399
+ if (labelledBy && labelledBy.trim()) {
2400
+ const ids = labelledBy.trim().split(/\s+/);
2401
+ const parts = [];
2402
+ for (const id of ids) {
2403
+ const referenced = document2.getElementById(id);
2404
+ if (referenced && referenced.textContent) {
2405
+ parts.push(referenced.textContent.trim());
2406
+ }
2407
+ }
2408
+ const combined = parts.join(" ");
2409
+ if (combined) {
2410
+ return combined;
2411
+ }
2412
+ }
2413
+ const textContent = element.textContent;
2414
+ if (textContent && textContent.trim()) {
2415
+ return textContent.trim();
2416
+ }
2417
+ return "";
2418
+ }
2419
+ function getElementRole2(element) {
2420
+ const explicitRole = element.getAttribute("role");
2421
+ if (explicitRole && explicitRole.trim()) {
2422
+ return explicitRole.trim();
2423
+ }
2424
+ const tagName = element.tagName.toLowerCase();
2425
+ switch (tagName) {
2426
+ case "button":
2427
+ return "button";
2428
+ case "a":
2429
+ return element.hasAttribute("href") ? "link" : "generic";
2430
+ case "input": {
2431
+ const type = (element.getAttribute("type") || "text").toLowerCase();
2432
+ switch (type) {
2433
+ case "button":
2434
+ case "submit":
2435
+ case "reset":
2436
+ case "image":
2437
+ return "button";
2438
+ case "checkbox":
2439
+ return "checkbox";
2440
+ case "radio":
2441
+ return "radio";
2442
+ case "range":
2443
+ return "slider";
2444
+ case "search":
2445
+ return "searchbox";
2446
+ default:
2447
+ return "textbox";
2448
+ }
2449
+ }
2450
+ case "textarea":
2451
+ return "textbox";
2452
+ case "select":
2453
+ return element.hasAttribute("multiple") ? "listbox" : "combobox";
2454
+ case "nav":
2455
+ return "navigation";
2456
+ case "main":
2457
+ return "main";
2458
+ case "header":
2459
+ return "banner";
2460
+ case "footer":
2461
+ return "contentinfo";
2462
+ case "aside":
2463
+ return "complementary";
2464
+ case "form":
2465
+ return "form";
2466
+ case "section":
2467
+ return element.hasAttribute("aria-label") || element.hasAttribute("aria-labelledby") ? "region" : "generic";
2468
+ case "article":
2469
+ return "article";
2470
+ case "dialog":
2471
+ return "dialog";
2472
+ case "table":
2473
+ return "table";
2474
+ case "ul":
2475
+ case "ol":
2476
+ return "list";
2477
+ case "li":
2478
+ return "listitem";
2479
+ case "img":
2480
+ return element.getAttribute("alt") === "" ? "presentation" : "img";
2481
+ case "h1":
2482
+ case "h2":
2483
+ case "h3":
2484
+ case "h4":
2485
+ case "h5":
2486
+ case "h6":
2487
+ return "heading";
2488
+ case "option":
2489
+ return "option";
2490
+ case "progress":
2491
+ return "progressbar";
2492
+ case "output":
2493
+ return "status";
2494
+ case "menu":
2495
+ return "menu";
2496
+ default:
2497
+ return "generic";
2498
+ }
2499
+ }
2500
+ function createMutationObserver(options) {
2501
+ const { document: doc, onEvent, getTimestamp } = options;
2502
+ let observer = null;
2503
+ let active = false;
2504
+ function handleMutations(mutations) {
2505
+ if (!active) return;
2506
+ for (const mutation of mutations) {
2507
+ if (mutation.type !== "attributes") {
2508
+ continue;
2509
+ }
2510
+ const attributeName = mutation.attributeName;
2511
+ if (!attributeName) {
2512
+ continue;
2513
+ }
2514
+ const target = mutation.target;
2515
+ if (!(target instanceof Element)) {
2516
+ continue;
2517
+ }
2518
+ if (attributeName === ROLE_ATTRIBUTE) {
2519
+ handleRoleChange(target, mutation.oldValue);
2520
+ } else if (NAME_ATTRIBUTES.has(attributeName)) {
2521
+ handleNameChange(target, attributeName, mutation.oldValue);
2522
+ } else if (STATE_ATTRIBUTES.has(attributeName)) {
2523
+ handleStateChange(target, attributeName, mutation.oldValue);
2524
+ }
2525
+ }
2526
+ }
2527
+ function handleRoleChange(element, oldValue) {
2528
+ const previousRole = oldValue?.trim() || getImplicitRole(element);
2529
+ const newRole = element.getAttribute("role")?.trim() || getImplicitRole(element);
2530
+ if (previousRole === newRole) {
2531
+ return;
2532
+ }
2533
+ const eventTarget = buildEventTarget2(element);
2534
+ const payload = {
2535
+ kind: "role_changed",
2536
+ previousRole,
2537
+ newRole
2538
+ };
2539
+ onEvent({
2540
+ type: "ROLE_CHANGED",
2541
+ timestamp: getTimestamp(),
2542
+ target: eventTarget,
2543
+ payload
2544
+ });
2545
+ }
2546
+ function handleNameChange(element, attributeName, oldValue) {
2547
+ let previousName;
2548
+ let newName;
2549
+ if (attributeName === "aria-label") {
2550
+ previousName = oldValue?.trim() || "";
2551
+ newName = computeAccessibleName3(element, doc);
2552
+ if (!previousName && oldValue === null) {
2553
+ const labelledBy = element.getAttribute("aria-labelledby");
2554
+ if (labelledBy) {
2555
+ previousName = resolveAriaLabelledBy(element, labelledBy);
2556
+ } else {
2557
+ previousName = element.textContent?.trim() || "";
2558
+ }
2559
+ }
2560
+ } else {
2561
+ previousName = oldValue ? resolveAriaLabelledBy(element, oldValue) : "";
2562
+ if (!previousName && oldValue === null) {
2563
+ const ariaLabel = element.getAttribute("aria-label");
2564
+ if (ariaLabel) {
2565
+ previousName = ariaLabel.trim();
2566
+ } else {
2567
+ previousName = element.textContent?.trim() || "";
2568
+ }
2569
+ }
2570
+ newName = computeAccessibleName3(element, doc);
2571
+ }
2572
+ if (previousName === newName) {
2573
+ return;
2574
+ }
2575
+ const eventTarget = buildEventTarget2(element);
2576
+ const payload = {
2577
+ kind: "accessible_name_changed",
2578
+ previousName,
2579
+ newName
2580
+ };
2581
+ onEvent({
2582
+ type: "ACCESSIBLE_NAME_CHANGED",
2583
+ timestamp: getTimestamp(),
2584
+ target: eventTarget,
2585
+ payload
2586
+ });
2587
+ }
2588
+ function handleStateChange(element, attributeName, oldValue) {
2589
+ const currentValue = element.getAttribute(attributeName);
2590
+ const normalizedOld = normalizeStateValue(oldValue);
2591
+ const normalizedNew = normalizeStateValue(currentValue);
2592
+ if (normalizedOld === normalizedNew) {
2593
+ return;
2594
+ }
2595
+ const eventTarget = buildEventTarget2(element);
2596
+ const payload = {
2597
+ kind: "state_changed",
2598
+ attribute: attributeName,
2599
+ previousValue: normalizedOld,
2600
+ newValue: normalizedNew
2601
+ };
2602
+ onEvent({
2603
+ type: "STATE_CHANGED",
2604
+ timestamp: getTimestamp(),
2605
+ target: eventTarget,
2606
+ payload
2607
+ });
2608
+ }
2609
+ function buildEventTarget2(element) {
2610
+ return {
2611
+ role: getElementRole2(element),
2612
+ accessibleName: computeAccessibleName3(element, doc),
2613
+ selector: generateSelector(element)
2614
+ };
2615
+ }
2616
+ function getImplicitRole(element) {
2617
+ const tagName = element.tagName.toLowerCase();
2618
+ switch (tagName) {
2619
+ case "button":
2620
+ return "button";
2621
+ case "a":
2622
+ return element.hasAttribute("href") ? "link" : "generic";
2623
+ case "input": {
2624
+ const type = (element.getAttribute("type") || "text").toLowerCase();
2625
+ switch (type) {
2626
+ case "button":
2627
+ case "submit":
2628
+ case "reset":
2629
+ case "image":
2630
+ return "button";
2631
+ case "checkbox":
2632
+ return "checkbox";
2633
+ case "radio":
2634
+ return "radio";
2635
+ default:
2636
+ return "textbox";
2637
+ }
2638
+ }
2639
+ default:
2640
+ return "generic";
2641
+ }
2642
+ }
2643
+ function resolveAriaLabelledBy(element, idRefs) {
2644
+ const ownerDoc = element.ownerDocument;
2645
+ if (!ownerDoc) {
2646
+ return "";
2647
+ }
2648
+ const ids = idRefs.trim().split(/\s+/).filter(Boolean);
2649
+ const parts = [];
2650
+ for (const id of ids) {
2651
+ const referenced = ownerDoc.getElementById(id);
2652
+ if (referenced && referenced.textContent) {
2653
+ parts.push(referenced.textContent.trim());
2654
+ }
2655
+ }
2656
+ return parts.join(" ");
2657
+ }
2658
+ function normalizeStateValue(value) {
2659
+ if (value === null) {
2660
+ return null;
2661
+ }
2662
+ if (value === "true") {
2663
+ return true;
2664
+ }
2665
+ if (value === "false") {
2666
+ return false;
2667
+ }
2668
+ return value;
2669
+ }
2670
+ return {
2671
+ start() {
2672
+ if (active) return;
2673
+ active = true;
2674
+ observer = new MutationObserver(handleMutations);
2675
+ observer.observe(doc.body ?? doc.documentElement, {
2676
+ attributes: true,
2677
+ attributeOldValue: true,
2678
+ attributeFilter: OBSERVED_ATTRIBUTES,
2679
+ subtree: true
2680
+ });
2681
+ },
2682
+ stop() {
2683
+ if (!active) return;
2684
+ active = false;
2685
+ if (observer) {
2686
+ observer.disconnect();
2687
+ observer = null;
2688
+ }
2689
+ },
2690
+ get isActive() {
2691
+ return active;
2692
+ }
2693
+ };
2694
+ }
2695
+
2696
+ // ../src/runtime/observers/live-region-observer.ts
2697
+ var IMPLICIT_LIVE_ROLES = /* @__PURE__ */ new Set(["alert", "status", "log", "marquee", "timer"]);
2698
+ var ROLE_POLITENESS_MAP = {
2699
+ alert: "assertive",
2700
+ status: "polite",
2701
+ log: "polite",
2702
+ marquee: "polite",
2703
+ timer: "polite"
2704
+ };
2705
+ function createLiveRegionObserver(document2, callback) {
2706
+ let contentObserver = null;
2707
+ let documentObserver = null;
2708
+ const trackedRegions = /* @__PURE__ */ new Set();
2709
+ function getLiveRegionPoliteness(element) {
2710
+ const ariaLive = element.getAttribute("aria-live");
2711
+ if (ariaLive === "assertive") return "assertive";
2712
+ if (ariaLive === "polite") return "polite";
2713
+ if (ariaLive === "off") return null;
2714
+ const role = element.getAttribute("role");
2715
+ if (role && IMPLICIT_LIVE_ROLES.has(role)) {
2716
+ return ROLE_POLITENESS_MAP[role] || "polite";
2717
+ }
2718
+ if (ariaLive && ariaLive !== "off") return "polite";
2719
+ return null;
2720
+ }
2721
+ function isAtomic(element) {
2722
+ return element.getAttribute("aria-atomic") === "true";
2723
+ }
2724
+ function buildEventTarget2(element) {
2725
+ const role = element.getAttribute("role") || "region";
2726
+ const accessibleName = element.getAttribute("aria-label") || element.getAttribute("aria-labelledby") || "";
2727
+ const selector = generateSelector(element);
2728
+ return { role, accessibleName, selector };
2729
+ }
2730
+ function getTextFromNodes(nodes) {
2731
+ const parts = [];
2732
+ nodes.forEach((node) => {
2733
+ if (node.nodeType === Node.TEXT_NODE) {
2734
+ const text = node.textContent?.trim();
2735
+ if (text) parts.push(text);
2736
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
2737
+ const text = node.textContent?.trim();
2738
+ if (text) parts.push(text);
2739
+ }
2740
+ });
2741
+ return parts.join(" ").trim();
2742
+ }
2743
+ function emitAnnouncement(liveRegion, text, politeness) {
2744
+ if (!text) return;
2745
+ const target = buildEventTarget2(liveRegion);
2746
+ callback({
2747
+ type: "ANNOUNCEMENT",
2748
+ target,
2749
+ payload: {
2750
+ kind: "announcement",
2751
+ politeness,
2752
+ text
2753
+ }
2754
+ });
2755
+ }
2756
+ function findLiveRegionAncestor(node) {
2757
+ let current = node;
2758
+ while (current) {
2759
+ if (current.nodeType === Node.ELEMENT_NODE && trackedRegions.has(current)) {
2760
+ return current;
2761
+ }
2762
+ current = current.parentNode;
2763
+ }
2764
+ return null;
2765
+ }
2766
+ function handleContentMutations(mutations) {
2767
+ const regionChanges = /* @__PURE__ */ new Map();
2768
+ for (const mutation of mutations) {
2769
+ const liveRegion = findLiveRegionAncestor(mutation.target);
2770
+ if (!liveRegion) continue;
2771
+ if (!regionChanges.has(liveRegion)) {
2772
+ regionChanges.set(liveRegion, { addedText: [], charDataChanged: false });
2773
+ }
2774
+ const changes = regionChanges.get(liveRegion);
2775
+ if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
2776
+ const text = getTextFromNodes(mutation.addedNodes);
2777
+ if (text) {
2778
+ changes.addedText.push(text);
2779
+ }
2780
+ } else if (mutation.type === "characterData") {
2781
+ changes.charDataChanged = true;
2782
+ }
2783
+ }
2784
+ for (const [region, changes] of regionChanges) {
2785
+ const politeness = getLiveRegionPoliteness(region);
2786
+ if (!politeness) continue;
2787
+ let text;
2788
+ if (isAtomic(region)) {
2789
+ text = region.textContent?.trim() || "";
2790
+ } else {
2791
+ if (changes.addedText.length > 0) {
2792
+ text = changes.addedText.join(" ").trim();
2793
+ } else if (changes.charDataChanged) {
2794
+ text = region.textContent?.trim() || "";
2795
+ } else {
2796
+ text = "";
2797
+ }
2798
+ }
2799
+ emitAnnouncement(region, text, politeness);
2800
+ }
2801
+ }
2802
+ function handleDocumentMutations(mutations) {
2803
+ for (const mutation of mutations) {
2804
+ if (mutation.type !== "childList") continue;
2805
+ mutation.addedNodes.forEach((node) => {
2806
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
2807
+ const element = node;
2808
+ if (getLiveRegionPoliteness(element) !== null && !trackedRegions.has(element)) {
2809
+ trackRegion(element);
2810
+ }
2811
+ const descendants = element.querySelectorAll("[aria-live], [role]");
2812
+ descendants.forEach((desc) => {
2813
+ if (getLiveRegionPoliteness(desc) !== null && !trackedRegions.has(desc)) {
2814
+ trackRegion(desc);
2815
+ }
2816
+ });
2817
+ });
2818
+ mutation.removedNodes.forEach((node) => {
2819
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
2820
+ const element = node;
2821
+ if (trackedRegions.has(element)) {
2822
+ trackedRegions.delete(element);
2823
+ }
2824
+ const descendants = element.querySelectorAll("[aria-live], [role]");
2825
+ descendants.forEach((desc) => {
2826
+ trackedRegions.delete(desc);
2827
+ });
2828
+ });
2829
+ }
2830
+ }
2831
+ function trackRegion(element) {
2832
+ trackedRegions.add(element);
2833
+ }
2834
+ function discoverExistingRegions() {
2835
+ const ariaLiveElements = document2.querySelectorAll("[aria-live]");
2836
+ ariaLiveElements.forEach((el) => {
2837
+ if (getLiveRegionPoliteness(el) !== null) {
2838
+ trackRegion(el);
2839
+ }
2840
+ });
2841
+ IMPLICIT_LIVE_ROLES.forEach((role) => {
2842
+ const roleElements = document2.querySelectorAll(`[role="${role}"]`);
2843
+ roleElements.forEach((el) => {
2844
+ if (!trackedRegions.has(el)) {
2845
+ trackRegion(el);
2846
+ }
2847
+ });
2848
+ });
2849
+ }
2850
+ function start() {
2851
+ discoverExistingRegions();
2852
+ contentObserver = new MutationObserver(handleContentMutations);
2853
+ const body = document2.body;
2854
+ if (body) {
2855
+ contentObserver.observe(body, {
2856
+ childList: true,
2857
+ characterData: true,
2858
+ subtree: true,
2859
+ characterDataOldValue: true
2860
+ });
2861
+ }
2862
+ documentObserver = new MutationObserver(handleDocumentMutations);
2863
+ if (body) {
2864
+ documentObserver.observe(body, {
2865
+ childList: true,
2866
+ subtree: true
2867
+ });
2868
+ }
2869
+ }
2870
+ function stop() {
2871
+ if (contentObserver) {
2872
+ contentObserver.disconnect();
2873
+ contentObserver = null;
2874
+ }
2875
+ if (documentObserver) {
2876
+ documentObserver.disconnect();
2877
+ documentObserver = null;
2878
+ }
2879
+ trackedRegions.clear();
2880
+ }
2881
+ return { start, stop };
2882
+ }
2883
+
2884
+ // ../src/runtime/observers/dialog-observer.ts
2885
+ function computeDialogAccessibleName(element, document2) {
2886
+ const ariaLabel = element.getAttribute("aria-label");
2887
+ if (ariaLabel && ariaLabel.trim()) {
2888
+ return ariaLabel.trim();
2889
+ }
2890
+ const labelledBy = element.getAttribute("aria-labelledby");
2891
+ if (labelledBy && labelledBy.trim()) {
2892
+ const ids = labelledBy.trim().split(/\s+/);
2893
+ const parts = [];
2894
+ for (const id of ids) {
2895
+ const referenced = document2.getElementById(id);
2896
+ if (referenced && referenced.textContent) {
2897
+ parts.push(referenced.textContent.trim());
2898
+ }
2899
+ }
2900
+ const combined = parts.join(" ");
2901
+ if (combined) {
2902
+ return combined;
2903
+ }
2904
+ }
2905
+ const title = element.getAttribute("title");
2906
+ if (title && title.trim()) {
2907
+ return title.trim();
2908
+ }
2909
+ const heading = element.querySelector("h1, h2, h3, h4, h5, h6");
2910
+ if (heading && heading.textContent && heading.textContent.trim()) {
2911
+ return heading.textContent.trim();
2912
+ }
2913
+ return "";
2914
+ }
2915
+ function getDialogRole(element) {
2916
+ const explicitRole = element.getAttribute("role");
2917
+ if (explicitRole && explicitRole.trim()) {
2918
+ return explicitRole.trim();
2919
+ }
2920
+ if (element.tagName.toLowerCase() === "dialog") {
2921
+ return "dialog";
2922
+ }
2923
+ return "generic";
2924
+ }
2925
+ function isDialogElement(element) {
2926
+ if (element.tagName.toLowerCase() === "dialog") {
2927
+ return true;
2928
+ }
2929
+ const role = element.getAttribute("role");
2930
+ return role === "dialog" || role === "alertdialog";
2931
+ }
2932
+ function isDialogModal(element) {
2933
+ const ariaModal = element.getAttribute("aria-modal");
2934
+ if (ariaModal === "true") {
2935
+ return true;
2936
+ }
2937
+ return false;
2938
+ }
2939
+ function createDialogObserver(options) {
2940
+ const { document: doc, onEvent, getTimestamp } = options;
2941
+ let active = false;
2942
+ let mutationObserver = null;
2943
+ const openDialogStack = [];
2944
+ function buildEventTarget2(element) {
2945
+ return {
2946
+ role: getDialogRole(element),
2947
+ accessibleName: computeDialogAccessibleName(element, doc),
2948
+ selector: generateSelector(element)
2949
+ };
2950
+ }
2951
+ function emitDialogOpened(element) {
2952
+ const name = computeDialogAccessibleName(element, doc);
2953
+ const isModal = isDialogModal(element);
2954
+ const target = buildEventTarget2(element);
2955
+ openDialogStack.push({
2956
+ element,
2957
+ name,
2958
+ selector: target.selector
2959
+ });
2960
+ const a11yEvent = {
2961
+ type: "DIALOG_OPENED",
2962
+ timestamp: getTimestamp(),
2963
+ target,
2964
+ payload: {
2965
+ kind: "dialog_opened",
2966
+ dialogName: name,
2967
+ isModal
2968
+ }
2969
+ };
2970
+ onEvent(a11yEvent);
2971
+ }
2972
+ function emitDialogClosed(element) {
2973
+ const name = computeDialogAccessibleName(element, doc);
2974
+ const target = buildEventTarget2(element);
2975
+ const index = openDialogStack.findIndex((d) => d.element === element);
2976
+ if (index !== -1) {
2977
+ openDialogStack.splice(index, 1);
2978
+ }
2979
+ const a11yEvent = {
2980
+ type: "DIALOG_CLOSED",
2981
+ timestamp: getTimestamp(),
2982
+ target,
2983
+ payload: {
2984
+ kind: "dialog_closed",
2985
+ dialogName: name
2986
+ }
2987
+ };
2988
+ onEvent(a11yEvent);
2989
+ }
2990
+ function isTrackedAsOpen(element) {
2991
+ return openDialogStack.some((d) => d.element === element);
2992
+ }
2993
+ function handleMutations(mutations) {
2994
+ if (!active) return;
2995
+ for (const mutation of mutations) {
2996
+ if (mutation.type === "attributes") {
2997
+ const target = mutation.target;
2998
+ if (!(target instanceof doc.defaultView.Element)) continue;
2999
+ if (mutation.attributeName === "open" && target.tagName.toLowerCase() === "dialog") {
3000
+ if (target.hasAttribute("open")) {
3001
+ if (!isTrackedAsOpen(target)) {
3002
+ emitDialogOpened(target);
3003
+ }
3004
+ } else {
3005
+ if (isTrackedAsOpen(target)) {
3006
+ emitDialogClosed(target);
3007
+ }
3008
+ }
3009
+ }
3010
+ if (mutation.attributeName === "aria-modal" && isDialogElement(target)) {
3011
+ if (!isTrackedAsOpen(target) && isVisibleDialog(target)) {
3012
+ emitDialogOpened(target);
3013
+ }
3014
+ }
3015
+ if (mutation.attributeName === "role") {
3016
+ const oldRole = mutation.oldValue;
3017
+ const newRole = target.getAttribute("role");
3018
+ const wasDialog = oldRole === "dialog" || oldRole === "alertdialog";
3019
+ const isNowDialog = newRole === "dialog" || newRole === "alertdialog";
3020
+ if (!wasDialog && isNowDialog && isVisibleDialog(target)) {
3021
+ emitDialogOpened(target);
3022
+ } else if (wasDialog && !isNowDialog && isTrackedAsOpen(target)) {
3023
+ emitDialogClosed(target);
3024
+ }
3025
+ }
3026
+ }
3027
+ if (mutation.type === "childList") {
3028
+ for (let i = 0; i < mutation.addedNodes.length; i++) {
3029
+ const node = mutation.addedNodes[i];
3030
+ if (!(node instanceof doc.defaultView.Element)) continue;
3031
+ if (isDialogElement(node) && isVisibleDialog(node)) {
3032
+ if (!isTrackedAsOpen(node)) {
3033
+ emitDialogOpened(node);
3034
+ }
3035
+ }
3036
+ const dialogDescendants = node.querySelectorAll(
3037
+ 'dialog[open], [role="dialog"], [role="alertdialog"]'
3038
+ );
3039
+ for (let j = 0; j < dialogDescendants.length; j++) {
3040
+ const dialog = dialogDescendants[j];
3041
+ if (isVisibleDialog(dialog) && !isTrackedAsOpen(dialog)) {
3042
+ emitDialogOpened(dialog);
3043
+ }
3044
+ }
3045
+ }
3046
+ for (let i = 0; i < mutation.removedNodes.length; i++) {
3047
+ const node = mutation.removedNodes[i];
3048
+ if (!(node instanceof doc.defaultView.Element)) continue;
3049
+ if (isTrackedAsOpen(node)) {
3050
+ emitDialogClosed(node);
3051
+ }
3052
+ const trackedInRemoved = openDialogStack.filter(
3053
+ (d) => node.contains(d.element)
3054
+ );
3055
+ for (const tracked of trackedInRemoved) {
3056
+ emitDialogClosed(tracked.element);
3057
+ }
3058
+ }
3059
+ }
3060
+ }
3061
+ }
3062
+ function isVisibleDialog(element) {
3063
+ if (element.tagName.toLowerCase() === "dialog") {
3064
+ return element.hasAttribute("open");
3065
+ }
3066
+ return true;
3067
+ }
3068
+ return {
3069
+ start() {
3070
+ if (active) return;
3071
+ active = true;
3072
+ openDialogStack.length = 0;
3073
+ const existingDialogs = doc.querySelectorAll(
3074
+ 'dialog[open], [role="dialog"], [role="alertdialog"]'
3075
+ );
3076
+ for (let i = 0; i < existingDialogs.length; i++) {
3077
+ const dialog = existingDialogs[i];
3078
+ if (isVisibleDialog(dialog)) {
3079
+ openDialogStack.push({
3080
+ element: dialog,
3081
+ name: computeDialogAccessibleName(dialog, doc),
3082
+ selector: generateSelector(dialog)
3083
+ });
3084
+ }
3085
+ }
3086
+ mutationObserver = new MutationObserver(handleMutations);
3087
+ mutationObserver.observe(doc.body || doc.documentElement, {
3088
+ attributes: true,
3089
+ attributeFilter: ["open", "role", "aria-modal"],
3090
+ attributeOldValue: true,
3091
+ childList: true,
3092
+ subtree: true
3093
+ });
3094
+ },
3095
+ stop() {
3096
+ if (!active) return;
3097
+ active = false;
3098
+ if (mutationObserver) {
3099
+ mutationObserver.disconnect();
3100
+ mutationObserver = null;
3101
+ }
3102
+ openDialogStack.length = 0;
3103
+ },
3104
+ get isActive() {
3105
+ return active;
3106
+ },
3107
+ getOpenDialogs() {
3108
+ return openDialogStack.map((d) => d.name);
3109
+ }
3110
+ };
3111
+ }
3112
+
3113
+ // ../src/runtime/engine.ts
3114
+ function createEngine(options) {
3115
+ const { document: doc } = options;
3116
+ let attached = false;
3117
+ let sessionStart = 0;
3118
+ let events = [];
3119
+ function getTimestamp() {
3120
+ return Date.now() - sessionStart;
3121
+ }
3122
+ function collectEvent(event) {
3123
+ events.push(event);
3124
+ }
3125
+ function collectLiveRegionEvent(event) {
3126
+ const fullEvent = {
3127
+ ...event,
3128
+ timestamp: getTimestamp()
3129
+ };
3130
+ events.push(fullEvent);
3131
+ }
3132
+ const focusObs = createFocusObserver({
3133
+ document: doc,
3134
+ onEvent: collectEvent,
3135
+ getTimestamp
3136
+ });
3137
+ const mutationObs = createMutationObserver({
3138
+ document: doc,
3139
+ onEvent: collectEvent,
3140
+ getTimestamp
3141
+ });
3142
+ const liveRegionObs = createLiveRegionObserver(
3143
+ doc,
3144
+ collectLiveRegionEvent
3145
+ );
3146
+ const dialogObs = createDialogObserver({
3147
+ document: doc,
3148
+ onEvent: collectEvent,
3149
+ getTimestamp
3150
+ });
3151
+ return {
3152
+ attach() {
3153
+ if (attached) return;
3154
+ attached = true;
3155
+ sessionStart = Date.now();
3156
+ events = [];
3157
+ focusObs.start();
3158
+ mutationObs.start();
3159
+ liveRegionObs.start();
3160
+ dialogObs.start();
3161
+ },
3162
+ detach() {
3163
+ if (!attached) return;
3164
+ attached = false;
3165
+ focusObs.stop();
3166
+ mutationObs.stop();
3167
+ liveRegionObs.stop();
3168
+ dialogObs.stop();
3169
+ },
3170
+ getEvents() {
3171
+ return [...events];
3172
+ },
3173
+ reset() {
3174
+ events = [];
3175
+ },
3176
+ get isAttached() {
3177
+ return attached;
3178
+ },
3179
+ get dialogObserver() {
3180
+ return {
3181
+ getOpenDialogs() {
3182
+ return dialogObs.getOpenDialogs();
3183
+ }
3184
+ };
3185
+ }
3186
+ };
3187
+ }
3188
+
3189
+ // ../src/runtime/interactions.ts
3190
+ var KEY_MAP = {
3191
+ Tab: { key: "Tab", code: "Tab", keyCode: 9 },
3192
+ Escape: { key: "Escape", code: "Escape", keyCode: 27 },
3193
+ Enter: { key: "Enter", code: "Enter", keyCode: 13 },
3194
+ " ": { key: " ", code: "Space", keyCode: 32 },
3195
+ ArrowUp: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
3196
+ ArrowDown: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
3197
+ ArrowLeft: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
3198
+ ArrowRight: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 }
3199
+ };
3200
+ var FOCUSABLE_SELECTOR = [
3201
+ "button:not([disabled])",
3202
+ "a[href]",
3203
+ "input:not([disabled])",
3204
+ "select:not([disabled])",
3205
+ "textarea:not([disabled])",
3206
+ '[tabindex]:not([tabindex="-1"])'
3207
+ ].join(", ");
3208
+ function getFocusableElements(document2) {
3209
+ return Array.from(document2.querySelectorAll(FOCUSABLE_SELECTOR));
3210
+ }
3211
+ function computeAccessibleName4(element, document2) {
3212
+ const ariaLabel = element.getAttribute("aria-label");
3213
+ if (ariaLabel && ariaLabel.trim()) {
3214
+ return ariaLabel.trim();
3215
+ }
3216
+ const labelledBy = element.getAttribute("aria-labelledby");
3217
+ if (labelledBy && labelledBy.trim()) {
3218
+ const ids = labelledBy.trim().split(/\s+/);
3219
+ const parts = [];
3220
+ for (const id of ids) {
3221
+ const referenced = document2.getElementById(id);
3222
+ if (referenced && referenced.textContent) {
3223
+ parts.push(referenced.textContent.trim());
3224
+ }
3225
+ }
3226
+ const combined = parts.join(" ");
3227
+ if (combined) return combined;
3228
+ }
3229
+ const textContent = element.textContent;
3230
+ if (textContent && textContent.trim()) {
3231
+ return textContent.trim();
3232
+ }
3233
+ return "";
3234
+ }
3235
+ function getElementRole3(element) {
3236
+ const explicitRole = element.getAttribute("role");
3237
+ if (explicitRole && explicitRole.trim()) {
3238
+ return explicitRole.trim();
3239
+ }
3240
+ const tagName = element.tagName.toLowerCase();
3241
+ switch (tagName) {
3242
+ case "button":
3243
+ return "button";
3244
+ case "a":
3245
+ return element.hasAttribute("href") ? "link" : "generic";
3246
+ case "input": {
3247
+ const type = (element.getAttribute("type") || "text").toLowerCase();
3248
+ switch (type) {
3249
+ case "button":
3250
+ case "submit":
3251
+ case "reset":
3252
+ case "image":
3253
+ return "button";
3254
+ case "checkbox":
3255
+ return "checkbox";
3256
+ case "radio":
3257
+ return "radio";
3258
+ case "range":
3259
+ return "slider";
3260
+ case "search":
3261
+ return "searchbox";
3262
+ default:
3263
+ return "textbox";
3264
+ }
3265
+ }
3266
+ case "textarea":
3267
+ return "textbox";
3268
+ case "select":
3269
+ return element.hasAttribute("multiple") ? "listbox" : "combobox";
3270
+ case "dialog":
3271
+ return "dialog";
3272
+ default:
3273
+ return "generic";
3274
+ }
3275
+ }
3276
+ function buildEventTarget(element, document2) {
3277
+ return {
3278
+ role: getElementRole3(element),
3279
+ accessibleName: computeAccessibleName4(element, document2),
3280
+ selector: generateSelector(element)
3281
+ };
3282
+ }
3283
+ function dispatchKeyboardEvent(document2, options) {
3284
+ const target = document2.activeElement || document2.body;
3285
+ const commonProps = {
3286
+ key: options.key,
3287
+ code: options.code,
3288
+ keyCode: options.keyCode,
3289
+ which: options.keyCode,
3290
+ shiftKey: options.shiftKey || false,
3291
+ ctrlKey: options.ctrlKey || false,
3292
+ altKey: options.altKey || false,
3293
+ metaKey: options.metaKey || false,
3294
+ bubbles: true,
3295
+ cancelable: true
3296
+ };
3297
+ target.dispatchEvent(new KeyboardEvent("keydown", commonProps));
3298
+ if (options.key.length === 1) {
3299
+ target.dispatchEvent(new KeyboardEvent("keypress", commonProps));
3300
+ }
3301
+ target.dispatchEvent(new KeyboardEvent("keyup", commonProps));
3302
+ }
3303
+ function parseKeyCombo(combo) {
3304
+ const parts = combo.split("+");
3305
+ const modifiers = [];
3306
+ let shiftKey = false;
3307
+ let ctrlKey = false;
3308
+ let altKey = false;
3309
+ let metaKey = false;
3310
+ const key = parts[parts.length - 1];
3311
+ for (let i = 0; i < parts.length - 1; i++) {
3312
+ const mod = parts[i].toLowerCase();
3313
+ switch (mod) {
3314
+ case "shift":
3315
+ shiftKey = true;
3316
+ modifiers.push("Shift");
3317
+ break;
3318
+ case "control":
3319
+ case "ctrl":
3320
+ ctrlKey = true;
3321
+ modifiers.push("Control");
3322
+ break;
3323
+ case "alt":
3324
+ altKey = true;
3325
+ modifiers.push("Alt");
3326
+ break;
3327
+ case "meta":
3328
+ case "cmd":
3329
+ case "command":
3330
+ metaKey = true;
3331
+ modifiers.push("Meta");
3332
+ break;
3333
+ }
3334
+ }
3335
+ return { key, modifiers, shiftKey, ctrlKey, altKey, metaKey };
3336
+ }
3337
+ function getKeyInfo(key) {
3338
+ if (KEY_MAP[key]) {
3339
+ return KEY_MAP[key];
3340
+ }
3341
+ if (key.length === 1) {
3342
+ return {
3343
+ key,
3344
+ code: `Key${key.toUpperCase()}`,
3345
+ keyCode: key.toUpperCase().charCodeAt(0)
3346
+ };
3347
+ }
3348
+ return {
3349
+ key,
3350
+ code: key,
3351
+ keyCode: 0
3352
+ };
3353
+ }
3354
+ function executeClick(selector, document2, onEvent, getTimestamp) {
3355
+ const element = document2.querySelector(selector);
3356
+ if (!element) {
3357
+ const warningEvent = {
3358
+ type: "WARNING",
3359
+ timestamp: getTimestamp(),
3360
+ target: {
3361
+ role: "generic",
3362
+ accessibleName: "",
3363
+ selector
3364
+ },
3365
+ payload: {
3366
+ kind: "warning",
3367
+ message: `Element not found: ${selector}`
3368
+ }
3369
+ };
3370
+ onEvent(warningEvent);
3371
+ return;
3372
+ }
3373
+ const eventProps = { bubbles: true, cancelable: true };
3374
+ element.dispatchEvent(new MouseEvent("mousedown", eventProps));
3375
+ element.dispatchEvent(new MouseEvent("mouseup", eventProps));
3376
+ element.dispatchEvent(new MouseEvent("click", eventProps));
3377
+ if ("focus" in element && typeof element.focus === "function") {
3378
+ element.focus();
3379
+ }
3380
+ }
3381
+ function executeTab(document2, onEvent, getTimestamp) {
3382
+ const focusableElements = getFocusableElements(document2);
3383
+ if (focusableElements.length === 0) return;
3384
+ const activeElement = document2.activeElement;
3385
+ let currentIndex = -1;
3386
+ if (activeElement) {
3387
+ currentIndex = focusableElements.indexOf(activeElement);
3388
+ }
3389
+ const nextIndex = (currentIndex + 1) % focusableElements.length;
3390
+ const nextElement = focusableElements[nextIndex];
3391
+ dispatchKeyboardEvent(document2, {
3392
+ ...KEY_MAP["Tab"],
3393
+ shiftKey: false
3394
+ });
3395
+ if (nextElement && typeof nextElement.focus === "function") {
3396
+ nextElement.focus();
3397
+ }
3398
+ const target = document2.activeElement || document2.body;
3399
+ const keyboardEvent = {
3400
+ type: "KEYBOARD_ACTION",
3401
+ timestamp: getTimestamp(),
3402
+ target: buildEventTarget(target, document2),
3403
+ payload: {
3404
+ kind: "keyboard_action",
3405
+ key: "Tab",
3406
+ modifiers: []
3407
+ }
3408
+ };
3409
+ onEvent(keyboardEvent);
3410
+ }
3411
+ function executeShiftTab(document2, onEvent, getTimestamp) {
3412
+ const focusableElements = getFocusableElements(document2);
3413
+ if (focusableElements.length === 0) return;
3414
+ const activeElement = document2.activeElement;
3415
+ let currentIndex = -1;
3416
+ if (activeElement) {
3417
+ currentIndex = focusableElements.indexOf(activeElement);
3418
+ }
3419
+ const prevIndex = currentIndex <= 0 ? focusableElements.length - 1 : currentIndex - 1;
3420
+ const prevElement = focusableElements[prevIndex];
3421
+ dispatchKeyboardEvent(document2, {
3422
+ ...KEY_MAP["Tab"],
3423
+ shiftKey: true
3424
+ });
3425
+ if (prevElement && typeof prevElement.focus === "function") {
3426
+ prevElement.focus();
3427
+ }
3428
+ const target = document2.activeElement || document2.body;
3429
+ const keyboardEvent = {
3430
+ type: "KEYBOARD_ACTION",
3431
+ timestamp: getTimestamp(),
3432
+ target: buildEventTarget(target, document2),
3433
+ payload: {
3434
+ kind: "keyboard_action",
3435
+ key: "Tab",
3436
+ modifiers: ["Shift"]
3437
+ }
3438
+ };
3439
+ onEvent(keyboardEvent);
3440
+ }
3441
+ function executeKeyAction(keyName, document2, onEvent, getTimestamp) {
3442
+ const keyInfo = KEY_MAP[keyName];
3443
+ if (!keyInfo) return;
3444
+ dispatchKeyboardEvent(document2, {
3445
+ ...keyInfo
3446
+ });
3447
+ const target = document2.activeElement || document2.body;
3448
+ const keyboardEvent = {
3449
+ type: "KEYBOARD_ACTION",
3450
+ timestamp: getTimestamp(),
3451
+ target: buildEventTarget(target, document2),
3452
+ payload: {
3453
+ kind: "keyboard_action",
3454
+ key: keyName,
3455
+ modifiers: []
3456
+ }
3457
+ };
3458
+ onEvent(keyboardEvent);
3459
+ }
3460
+ function executeKeyCombo(combo, document2, onEvent, getTimestamp) {
3461
+ const parsed = parseKeyCombo(combo);
3462
+ const keyInfo = getKeyInfo(parsed.key);
3463
+ dispatchKeyboardEvent(document2, {
3464
+ ...keyInfo,
3465
+ shiftKey: parsed.shiftKey,
3466
+ ctrlKey: parsed.ctrlKey,
3467
+ altKey: parsed.altKey,
3468
+ metaKey: parsed.metaKey
3469
+ });
3470
+ const target = document2.activeElement || document2.body;
3471
+ const keyboardEvent = {
3472
+ type: "KEYBOARD_ACTION",
3473
+ timestamp: getTimestamp(),
3474
+ target: buildEventTarget(target, document2),
3475
+ payload: {
3476
+ kind: "keyboard_action",
3477
+ key: parsed.key,
3478
+ modifiers: parsed.modifiers
3479
+ }
3480
+ };
3481
+ onEvent(keyboardEvent);
3482
+ }
3483
+ async function executeAction(action, document2, onEvent, getTimestamp) {
3484
+ switch (action.type) {
3485
+ case "click":
3486
+ executeClick(action.selector, document2, onEvent, getTimestamp);
3487
+ break;
3488
+ case "tab":
3489
+ executeTab(document2, onEvent, getTimestamp);
3490
+ break;
3491
+ case "shiftTab":
3492
+ executeShiftTab(document2, onEvent, getTimestamp);
3493
+ break;
3494
+ case "escape":
3495
+ executeKeyAction("Escape", document2, onEvent, getTimestamp);
3496
+ break;
3497
+ case "enter":
3498
+ executeKeyAction("Enter", document2, onEvent, getTimestamp);
3499
+ break;
3500
+ case "space":
3501
+ executeKeyAction(" ", document2, onEvent, getTimestamp);
3502
+ break;
3503
+ case "arrowUp":
3504
+ executeKeyAction("ArrowUp", document2, onEvent, getTimestamp);
3505
+ break;
3506
+ case "arrowDown":
3507
+ executeKeyAction("ArrowDown", document2, onEvent, getTimestamp);
3508
+ break;
3509
+ case "arrowLeft":
3510
+ executeKeyAction("ArrowLeft", document2, onEvent, getTimestamp);
3511
+ break;
3512
+ case "arrowRight":
3513
+ executeKeyAction("ArrowRight", document2, onEvent, getTimestamp);
3514
+ break;
3515
+ case "key":
3516
+ executeKeyCombo(action.combo, document2, onEvent, getTimestamp);
3517
+ break;
3518
+ case "wait":
3519
+ await new Promise((resolve) => setTimeout(resolve, action.ms));
3520
+ break;
3521
+ }
3522
+ }
3523
+ async function executeSequence(sequence, document2, onEvent, getTimestamp, settleTime = 100) {
3524
+ for (let i = 0; i < sequence.actions.length; i++) {
3525
+ const action = sequence.actions[i];
3526
+ await executeAction(action, document2, onEvent, getTimestamp);
3527
+ if (i < sequence.actions.length - 1) {
3528
+ await new Promise((resolve) => setTimeout(resolve, settleTime));
3529
+ }
3530
+ }
3531
+ }
3532
+
3533
+ // ../src/runtime/heuristics.ts
3534
+ var DEFAULT_CONFIG = {
3535
+ dialogFocusTimeout: 100,
3536
+ rapidAnnouncementWindow: 500,
3537
+ rapidAnnouncementThreshold: 3,
3538
+ keyboardResponseTimeout: 200
3539
+ };
3540
+ function createHeuristicAnalyzer(config) {
3541
+ const cfg = { ...DEFAULT_CONFIG, ...config };
3542
+ const trackedDialogs = [];
3543
+ const pendingDialogFocusChecks = [];
3544
+ const pendingKeyboardResponses = [];
3545
+ const announcementTimestamps = [];
3546
+ let lastFocusedSelector = null;
3547
+ function createWarning(timestamp, message, target, relatedEvent) {
3548
+ const payload = {
3549
+ kind: "warning",
3550
+ message,
3551
+ relatedEvent
3552
+ };
3553
+ return {
3554
+ type: "WARNING",
3555
+ timestamp,
3556
+ target,
3557
+ payload
3558
+ };
3559
+ }
3560
+ function checkExpiredDialogFocusTimeouts(currentTimestamp) {
3561
+ const warnings = [];
3562
+ for (let i = pendingDialogFocusChecks.length - 1; i >= 0; i--) {
3563
+ const pending = pendingDialogFocusChecks[i];
3564
+ if (pending.resolved) continue;
3565
+ const elapsed = currentTimestamp - pending.openedAt;
3566
+ if (elapsed > cfg.dialogFocusTimeout) {
3567
+ if (pending.isModal) {
3568
+ warnings.push(
3569
+ createWarning(
3570
+ currentTimestamp,
3571
+ "Focus was not moved into modal dialog",
3572
+ {
3573
+ role: "dialog",
3574
+ accessibleName: pending.dialogName,
3575
+ selector: pending.dialogSelector
3576
+ }
3577
+ )
3578
+ );
3579
+ }
3580
+ pending.resolved = true;
3581
+ }
3582
+ }
3583
+ return warnings;
3584
+ }
3585
+ function checkExpiredKeyboardResponses(currentTimestamp) {
3586
+ const warnings = [];
3587
+ for (let i = pendingKeyboardResponses.length - 1; i >= 0; i--) {
3588
+ const pending = pendingKeyboardResponses[i];
3589
+ if (pending.resolved) continue;
3590
+ const elapsed = currentTimestamp - pending.timestamp;
3591
+ if (elapsed > cfg.keyboardResponseTimeout) {
3592
+ warnings.push(
3593
+ createWarning(
3594
+ currentTimestamp,
3595
+ "Keyboard action produced no accessibility response",
3596
+ pending.event.target,
3597
+ pending.event
3598
+ )
3599
+ );
3600
+ pending.resolved = true;
3601
+ }
3602
+ }
3603
+ return warnings;
3604
+ }
3605
+ function checkRapidAnnouncements(currentTimestamp) {
3606
+ const warnings = [];
3607
+ const windowStart = currentTimestamp - cfg.rapidAnnouncementWindow;
3608
+ while (announcementTimestamps.length > 0 && announcementTimestamps[0] < windowStart) {
3609
+ announcementTimestamps.shift();
3610
+ }
3611
+ if (announcementTimestamps.length > cfg.rapidAnnouncementThreshold) {
3612
+ warnings.push(
3613
+ createWarning(
3614
+ currentTimestamp,
3615
+ "Rapid announcements detected: assistive technology users may miss content",
3616
+ {
3617
+ role: "region",
3618
+ accessibleName: "",
3619
+ selector: "body"
3620
+ }
3621
+ )
3622
+ );
3623
+ announcementTimestamps.length = 0;
3624
+ }
3625
+ return warnings;
3626
+ }
3627
+ function checkFocusEscape(event) {
3628
+ const warnings = [];
3629
+ if (event.type !== "FOCUS_CHANGED") return warnings;
3630
+ const modalDialogs = trackedDialogs.filter((d) => d.isModal);
3631
+ if (modalDialogs.length === 0) return warnings;
3632
+ const focusedSelector = event.target.selector;
3633
+ for (const dialog of modalDialogs) {
3634
+ if (!isSelectorInside(focusedSelector, dialog.selector)) {
3635
+ warnings.push(
3636
+ createWarning(
3637
+ event.timestamp,
3638
+ "Focus escaped modal dialog",
3639
+ event.target,
3640
+ event
3641
+ )
3642
+ );
3643
+ break;
3644
+ }
3645
+ }
3646
+ return warnings;
3647
+ }
3648
+ function checkFocusedElementRemoved(event) {
3649
+ const warnings = [];
3650
+ if (event.type === "DOM_MUTATION" && lastFocusedSelector !== null) {
3651
+ if (event.target.selector === lastFocusedSelector) {
3652
+ warnings.push(
3653
+ createWarning(
3654
+ event.timestamp,
3655
+ "Focused element removed without focus management",
3656
+ event.target,
3657
+ event
3658
+ )
3659
+ );
3660
+ }
3661
+ }
3662
+ return warnings;
3663
+ }
3664
+ function isSelectorInside(childSelector, containerSelector) {
3665
+ if (childSelector === containerSelector) return true;
3666
+ if (childSelector.startsWith(containerSelector + " ")) return true;
3667
+ if (childSelector.startsWith(containerSelector + " > ")) return true;
3668
+ if (childSelector.startsWith(containerSelector + ">")) return true;
3669
+ return false;
3670
+ }
3671
+ function processEvent(event) {
3672
+ const warnings = [];
3673
+ warnings.push(...checkExpiredDialogFocusTimeouts(event.timestamp));
3674
+ warnings.push(...checkExpiredKeyboardResponses(event.timestamp));
3675
+ switch (event.type) {
3676
+ case "DIALOG_OPENED": {
3677
+ const payload = event.payload;
3678
+ const dialog = {
3679
+ selector: event.target.selector,
3680
+ name: payload.dialogName,
3681
+ isModal: payload.isModal
3682
+ };
3683
+ trackedDialogs.push(dialog);
3684
+ if (payload.isModal) {
3685
+ pendingDialogFocusChecks.push({
3686
+ dialogSelector: event.target.selector,
3687
+ dialogName: payload.dialogName,
3688
+ openedAt: event.timestamp,
3689
+ isModal: true,
3690
+ resolved: false
3691
+ });
3692
+ }
3693
+ break;
3694
+ }
3695
+ case "DIALOG_CLOSED": {
3696
+ const closedSelector = event.target.selector;
3697
+ const idx = trackedDialogs.findIndex(
3698
+ (d) => d.selector === closedSelector
3699
+ );
3700
+ if (idx !== -1) {
3701
+ trackedDialogs.splice(idx, 1);
3702
+ }
3703
+ for (const pending of pendingDialogFocusChecks) {
3704
+ if (pending.dialogSelector === closedSelector && !pending.resolved) {
3705
+ pending.resolved = true;
3706
+ }
3707
+ }
3708
+ break;
3709
+ }
3710
+ case "FOCUS_CHANGED": {
3711
+ for (const pending of pendingDialogFocusChecks) {
3712
+ if (pending.resolved) continue;
3713
+ if (isSelectorInside(event.target.selector, pending.dialogSelector)) {
3714
+ pending.resolved = true;
3715
+ }
3716
+ }
3717
+ for (const pending of pendingKeyboardResponses) {
3718
+ if (!pending.resolved) {
3719
+ pending.resolved = true;
3720
+ }
3721
+ }
3722
+ warnings.push(...checkFocusEscape(event));
3723
+ lastFocusedSelector = event.target.selector;
3724
+ break;
3725
+ }
3726
+ case "ANNOUNCEMENT": {
3727
+ announcementTimestamps.push(event.timestamp);
3728
+ for (const pending of pendingKeyboardResponses) {
3729
+ if (!pending.resolved) {
3730
+ pending.resolved = true;
3731
+ }
3732
+ }
3733
+ warnings.push(...checkRapidAnnouncements(event.timestamp));
3734
+ break;
3735
+ }
3736
+ case "KEYBOARD_ACTION": {
3737
+ pendingKeyboardResponses.push({
3738
+ event,
3739
+ timestamp: event.timestamp,
3740
+ resolved: false
3741
+ });
3742
+ break;
3743
+ }
3744
+ case "DOM_MUTATION": {
3745
+ warnings.push(...checkFocusedElementRemoved(event));
3746
+ for (const pending of pendingKeyboardResponses) {
3747
+ if (!pending.resolved) {
3748
+ pending.resolved = true;
3749
+ }
3750
+ }
3751
+ break;
3752
+ }
3753
+ case "STATE_CHANGED":
3754
+ case "ROLE_CHANGED":
3755
+ case "ACCESSIBLE_NAME_CHANGED": {
3756
+ for (const pending of pendingKeyboardResponses) {
3757
+ if (!pending.resolved) {
3758
+ pending.resolved = true;
3759
+ }
3760
+ }
3761
+ break;
3762
+ }
3763
+ default:
3764
+ break;
3765
+ }
3766
+ cleanupResolvedChecks();
3767
+ return warnings;
3768
+ }
3769
+ function cleanupResolvedChecks() {
3770
+ for (let i = pendingDialogFocusChecks.length - 1; i >= 0; i--) {
3771
+ if (pendingDialogFocusChecks[i].resolved) {
3772
+ pendingDialogFocusChecks.splice(i, 1);
3773
+ }
3774
+ }
3775
+ for (let i = pendingKeyboardResponses.length - 1; i >= 0; i--) {
3776
+ if (pendingKeyboardResponses[i].resolved) {
3777
+ pendingKeyboardResponses.splice(i, 1);
3778
+ }
3779
+ }
3780
+ }
3781
+ return {
3782
+ process(event) {
3783
+ return processEvent(event);
3784
+ },
3785
+ get openDialogs() {
3786
+ return trackedDialogs.map((d) => d.selector);
3787
+ }
3788
+ };
3789
+ }
3790
+
3791
+ // ../src/version.ts
3792
+ var SPEAKABLE_VERSION = typeof __SPEAKABLE_VERSION__ !== "undefined" && __SPEAKABLE_VERSION__ ? __SPEAKABLE_VERSION__ : "dev";
3793
+
3794
+ // ../src/runtime/timeline-generator.ts
3795
+ var TIMELINE_FORMAT_VERSION = "1.0";
3796
+ var DEFAULT_SETTLE_PERIOD = 500;
3797
+ var DEFAULT_LOAD_TIMEOUT = 1e4;
3798
+ var DOM_STABILITY_WAIT = 100;
3799
+ function createTimelineGenerator(options) {
3800
+ const {
3801
+ document: doc,
3802
+ componentName,
3803
+ storyName,
3804
+ settlePeriod = DEFAULT_SETTLE_PERIOD,
3805
+ loadTimeout = DEFAULT_LOAD_TIMEOUT,
3806
+ heuristics: enableHeuristics = true
3807
+ } = options;
3808
+ let aborted = false;
3809
+ let currentEngine = null;
3810
+ return {
3811
+ async capture(sequence) {
3812
+ aborted = false;
3813
+ await waitForDocumentReady(doc, loadTimeout);
3814
+ if (aborted) {
3815
+ return buildAbortedTimeline(componentName, storyName, sequence);
3816
+ }
3817
+ const engine = createEngine({ document: doc });
3818
+ currentEngine = engine;
3819
+ const heuristicAnalyzer = enableHeuristics ? createHeuristicAnalyzer() : null;
3820
+ const interactionEvents = [];
3821
+ const sessionStart = Date.now();
3822
+ engine.attach();
3823
+ if (aborted) {
3824
+ engine.detach();
3825
+ currentEngine = null;
3826
+ return buildAbortedTimeline(componentName, storyName, sequence);
3827
+ }
3828
+ function getTimestamp() {
3829
+ return Date.now() - sessionStart;
3830
+ }
3831
+ await executeSequence(
3832
+ sequence,
3833
+ doc,
3834
+ (event) => {
3835
+ interactionEvents.push(event);
3836
+ },
3837
+ getTimestamp,
3838
+ DOM_STABILITY_WAIT
3839
+ );
3840
+ if (aborted) {
3841
+ engine.detach();
3842
+ currentEngine = null;
3843
+ return buildAbortedTimeline(componentName, storyName, sequence);
3844
+ }
3845
+ await delay(settlePeriod);
3846
+ engine.detach();
3847
+ currentEngine = null;
3848
+ const duration = Date.now() - sessionStart;
3849
+ const engineEvents = engine.getEvents();
3850
+ const allEvents = mergeEvents(engineEvents, interactionEvents);
3851
+ if (heuristicAnalyzer) {
3852
+ const heuristicWarnings = [];
3853
+ for (const event of allEvents) {
3854
+ const warnings = heuristicAnalyzer.process(event);
3855
+ heuristicWarnings.push(...warnings);
3856
+ }
3857
+ for (const warning of heuristicWarnings) {
3858
+ allEvents.push(warning);
3859
+ }
3860
+ allEvents.sort((a, b) => a.timestamp - b.timestamp);
3861
+ }
3862
+ const allWarnings = allEvents.filter((e) => e.type === "WARNING");
3863
+ const metadata = {
3864
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
3865
+ speakableVersion: SPEAKABLE_VERSION,
3866
+ sourceUrl: getSourceUrl(doc),
3867
+ userAgent: getUserAgent()
3868
+ };
3869
+ const timeline = {
3870
+ version: TIMELINE_FORMAT_VERSION,
3871
+ component: componentName,
3872
+ story: storyName || null,
3873
+ interactionSequence: sequence.description,
3874
+ duration,
3875
+ events: allEvents,
3876
+ warnings: allWarnings,
3877
+ metadata
3878
+ };
3879
+ return timeline;
3880
+ },
3881
+ abort() {
3882
+ aborted = true;
3883
+ if (currentEngine && currentEngine.isAttached) {
3884
+ currentEngine.detach();
3885
+ currentEngine = null;
3886
+ }
3887
+ }
3888
+ };
3889
+ }
3890
+ async function waitForDocumentReady(doc, timeout) {
3891
+ if (doc.body) return;
3892
+ return new Promise((resolve, reject) => {
3893
+ const timeoutId = setTimeout(() => {
3894
+ reject(
3895
+ new Error(
3896
+ `Document load timeout (${timeout}ms): ${getSourceUrl(doc)}`
3897
+ )
3898
+ );
3899
+ }, timeout);
3900
+ const interval = setInterval(() => {
3901
+ if (doc.body) {
3902
+ clearTimeout(timeoutId);
3903
+ clearInterval(interval);
3904
+ resolve();
3905
+ }
3906
+ }, 50);
3907
+ });
3908
+ }
3909
+ function mergeEvents(engineEvents, interactionEvents) {
3910
+ const all = [...engineEvents, ...interactionEvents];
3911
+ all.sort((a, b) => a.timestamp - b.timestamp);
3912
+ return all;
3913
+ }
3914
+ function getSourceUrl(doc) {
3915
+ try {
3916
+ const url = doc.URL;
3917
+ if (url && url !== "about:blank" && url !== "") {
3918
+ return url;
3919
+ }
3920
+ } catch {
3921
+ }
3922
+ return "local";
3923
+ }
3924
+ function getUserAgent() {
3925
+ try {
3926
+ if (typeof navigator !== "undefined" && navigator.userAgent) {
3927
+ return navigator.userAgent;
3928
+ }
3929
+ } catch {
3930
+ }
3931
+ return "jsdom";
3932
+ }
3933
+ function delay(ms) {
3934
+ return new Promise((resolve) => setTimeout(resolve, ms));
3935
+ }
3936
+ function buildAbortedTimeline(componentName, storyName, sequence) {
3937
+ return {
3938
+ version: TIMELINE_FORMAT_VERSION,
3939
+ component: componentName,
3940
+ story: storyName || null,
3941
+ interactionSequence: sequence.description,
3942
+ duration: 0,
3943
+ events: [],
3944
+ warnings: [],
3945
+ metadata: {
3946
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
3947
+ speakableVersion: SPEAKABLE_VERSION,
3948
+ sourceUrl: "local",
3949
+ userAgent: getUserAgent()
3950
+ }
3951
+ };
3952
+ }
3953
+
3954
+ // ../src/browser/capture.ts
3955
+ async function captureTimeline(document2, options) {
3956
+ const {
3957
+ componentName,
3958
+ storyName,
3959
+ sequence,
3960
+ settlePeriod,
3961
+ awaitUpgrade = true,
3962
+ upgradeTimeoutMs
3963
+ } = options;
3964
+ let upgradeWarnings = [];
3965
+ if (awaitUpgrade) {
3966
+ const warnings = await awaitCustomElementsReady(document2, upgradeTimeoutMs);
3967
+ upgradeWarnings = warnings.map((w) => w.message);
3968
+ }
3969
+ const generator = createTimelineGenerator({
3970
+ document: document2,
3971
+ componentName,
3972
+ storyName,
3973
+ ...settlePeriod !== void 0 && { settlePeriod }
3974
+ });
3975
+ const timeline = await generator.capture(sequence);
3976
+ if (upgradeWarnings.length > 0) {
3977
+ const upgradeEvents = upgradeWarnings.map((message) => ({
3978
+ type: "WARNING",
3979
+ timestamp: 0,
3980
+ target: { role: "generic", accessibleName: "", selector: ":root" },
3981
+ payload: { kind: "warning", message }
3982
+ }));
3983
+ return {
3984
+ ...timeline,
3985
+ events: [...upgradeEvents, ...timeline.events],
3986
+ warnings: [...upgradeEvents, ...timeline.warnings]
3987
+ };
3988
+ }
3989
+ return timeline;
3990
+ }
3991
+
3992
+ // ../src/runtime/patterns.ts
3993
+ var MODAL_DIALOG_DEFAULTS = {
3994
+ trigger: "button",
3995
+ container: '[role="dialog"], dialog'
3996
+ };
3997
+ var COMBOBOX_DEFAULTS = {
3998
+ input: '[role="combobox"], input[type="text"]',
3999
+ items: '[role="option"]'
4000
+ };
4001
+ var TABS_DEFAULTS = {
4002
+ container: '[role="tablist"]',
4003
+ items: '[role="tab"]'
4004
+ };
4005
+ var ACCORDION_DEFAULTS = {
4006
+ trigger: "button[aria-expanded]"
4007
+ };
4008
+ function buildModalDialogPattern(selectors) {
4009
+ const trigger = selectors?.trigger ?? MODAL_DIALOG_DEFAULTS.trigger;
4010
+ const actions = [
4011
+ { type: "click", selector: trigger },
4012
+ { type: "tab" },
4013
+ { type: "tab" },
4014
+ { type: "tab" },
4015
+ { type: "escape" }
4016
+ ];
4017
+ return {
4018
+ description: "Modal dialog: click trigger to open, tab through dialog content, press Escape to close, verify focus returns to trigger",
4019
+ actions
4020
+ };
4021
+ }
4022
+ function buildComboboxPattern(selectors) {
4023
+ const input = selectors?.input ?? COMBOBOX_DEFAULTS.input;
4024
+ const actions = [
4025
+ { type: "click", selector: input },
4026
+ { type: "key", combo: "a" },
4027
+ { type: "arrowDown" },
4028
+ { type: "arrowDown" },
4029
+ { type: "enter" }
4030
+ ];
4031
+ return {
4032
+ description: "Combobox: focus input, type to filter, navigate options with arrow keys, select with Enter",
4033
+ actions
4034
+ };
4035
+ }
4036
+ function buildTabsPattern(selectors) {
4037
+ const container = selectors?.container ?? TABS_DEFAULTS.container;
4038
+ const actions = [
4039
+ { type: "click", selector: container },
4040
+ { type: "arrowRight" },
4041
+ { type: "arrowRight" },
4042
+ { type: "enter" },
4043
+ { type: "arrowLeft" },
4044
+ { type: "space" }
4045
+ ];
4046
+ return {
4047
+ description: "Tabs: focus tablist, navigate between tabs with arrow keys, activate with Enter/Space",
4048
+ actions
4049
+ };
4050
+ }
4051
+ function buildAccordionPattern(selectors) {
4052
+ const trigger = selectors?.trigger ?? ACCORDION_DEFAULTS.trigger;
4053
+ const actions = [
4054
+ { type: "click", selector: trigger },
4055
+ { type: "enter" },
4056
+ { type: "arrowDown" },
4057
+ { type: "space" },
4058
+ { type: "arrowDown" },
4059
+ { type: "enter" }
4060
+ ];
4061
+ return {
4062
+ description: "Accordion: focus header, toggle with Enter/Space, navigate between headers with arrow keys",
4063
+ actions
4064
+ };
4065
+ }
4066
+ function getBuiltinPattern(name, selectors) {
4067
+ switch (name) {
4068
+ case "modal-dialog":
4069
+ return buildModalDialogPattern(selectors);
4070
+ case "combobox":
4071
+ return buildComboboxPattern(selectors);
4072
+ case "tabs":
4073
+ return buildTabsPattern(selectors);
4074
+ case "accordion":
4075
+ return buildAccordionPattern(selectors);
4076
+ }
4077
+ }
4078
+
4079
+ // src/sequence.ts
4080
+ var PLAY_PROBE_SEQUENCE = {
4081
+ description: "Keyboard walk: tab through focusable elements",
4082
+ actions: [{ type: "tab" }, { type: "tab" }, { type: "tab" }]
4083
+ };
4084
+ function resolveSequence(context) {
4085
+ const params = context?.parameters?.speakable;
4086
+ if (params && params.timeline === false) {
4087
+ return null;
4088
+ }
4089
+ if (params?.sequence && Array.isArray(params.sequence.actions)) {
4090
+ return params.sequence;
4091
+ }
4092
+ if (params?.pattern) {
4093
+ try {
4094
+ return getBuiltinPattern(params.pattern, params.selectors);
4095
+ } catch {
4096
+ return null;
4097
+ }
4098
+ }
4099
+ if (typeof context?.playFunction === "function" || typeof context?.play === "function") {
4100
+ return PLAY_PROBE_SEQUENCE;
4101
+ }
4102
+ return null;
4103
+ }
4104
+
4105
+ // src/preview.ts
4106
+ var currentObserver = null;
4107
+ var debounceTimer = null;
4108
+ var captureToken = 0;
4109
+ function getChannel() {
4110
+ return window.__STORYBOOK_ADDONS_CHANNEL__;
4111
+ }
4112
+ function findStoryRoot() {
4113
+ return document.getElementById("storybook-root") || document.getElementById("root") || document.body;
4114
+ }
4115
+ function runAnalysis() {
4116
+ const root = findStoryRoot();
4117
+ if (!root) return;
4118
+ const analysis = analyzeDOM(root);
4119
+ const channel = getChannel();
4120
+ if (channel) {
4121
+ channel.emit(EVENT_RESULT, analysis);
4122
+ }
4123
+ }
4124
+ async function runTimelineCapture(context) {
4125
+ const sequence = resolveSequence(context || {});
4126
+ if (!sequence) return;
4127
+ const token = ++captureToken;
4128
+ const componentName = context?.title || context?.kind || context?.componentId || "Component";
4129
+ const storyName = context?.name || context?.story || "Story";
4130
+ let timeline;
4131
+ try {
4132
+ timeline = await captureTimeline(document, {
4133
+ componentName,
4134
+ storyName,
4135
+ sequence
4136
+ });
4137
+ } catch {
4138
+ return;
4139
+ }
4140
+ if (token !== captureToken) return;
4141
+ const channel = getChannel();
4142
+ if (channel) {
4143
+ channel.emit(EVENT_TIMELINE, timeline);
2076
4144
  }
2077
4145
  }
2078
4146
  function debouncedAnalysis() {
@@ -2111,13 +4179,15 @@ function startObserving() {
2111
4179
  ]
2112
4180
  });
2113
4181
  }
2114
- var withSpeakable = (storyFn, _context) => {
4182
+ var withSpeakable = (storyFn, context) => {
2115
4183
  const result = storyFn();
4184
+ captureToken++;
2116
4185
  if (typeof window !== "undefined") {
2117
4186
  requestAnimationFrame(() => {
2118
4187
  setTimeout(() => {
2119
4188
  runAnalysis();
2120
4189
  startObserving();
4190
+ void runTimelineCapture(context);
2121
4191
  }, 100);
2122
4192
  });
2123
4193
  }