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