@swifttui/web 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +24 -10
  2. package/dist/index.d.ts +9 -0
  3. package/dist/index.js +9 -0
  4. package/dist/manifest.d.ts +2 -0
  5. package/dist/manifest.js +2 -0
  6. package/dist/src/AccessibilityTree.js +156 -0
  7. package/dist/src/AccessibilityTree.js.map +1 -0
  8. package/dist/src/BoxDrawingRenderer.js +1106 -0
  9. package/dist/src/BoxDrawingRenderer.js.map +1 -0
  10. package/dist/src/WebHostApp.d.ts +41 -0
  11. package/dist/src/WebHostApp.js +135 -0
  12. package/dist/src/WebHostApp.js.map +1 -0
  13. package/dist/src/WebHostSceneManifest.d.ts +18 -0
  14. package/dist/src/WebHostSceneManifest.js +70 -0
  15. package/dist/src/WebHostSceneManifest.js.map +1 -0
  16. package/dist/src/WebHostSceneRuntime.d.ts +112 -0
  17. package/dist/src/WebHostSceneRuntime.js +651 -0
  18. package/dist/src/WebHostSceneRuntime.js.map +1 -0
  19. package/dist/src/WebHostSurfaceTransport.d.ts +166 -0
  20. package/dist/src/WebHostSurfaceTransport.js +252 -0
  21. package/dist/src/WebHostSurfaceTransport.js.map +1 -0
  22. package/dist/src/WebHostTerminalStyle.d.ts +92 -0
  23. package/dist/src/WebHostTerminalStyle.js +277 -0
  24. package/dist/src/WebHostTerminalStyle.js.map +1 -0
  25. package/dist/src/WebHostTestFixtures.d.ts +5 -0
  26. package/dist/src/WebHostTestFixtures.js +9 -0
  27. package/dist/src/WebHostTestFixtures.js.map +1 -0
  28. package/dist/src/WebSocketSceneBridge.d.ts +53 -0
  29. package/dist/src/WebSocketSceneBridge.js +124 -0
  30. package/dist/src/WebSocketSceneBridge.js.map +1 -0
  31. package/dist/src/wasi/BrowserWASIBridge.d.ts +33 -0
  32. package/dist/src/wasi/BrowserWASIBridge.js +97 -0
  33. package/dist/src/wasi/BrowserWASIBridge.js.map +1 -0
  34. package/dist/src/wasi/SharedInputQueue.d.ts +31 -0
  35. package/dist/src/wasi/SharedInputQueue.js +102 -0
  36. package/dist/src/wasi/SharedInputQueue.js.map +1 -0
  37. package/dist/src/wasi/StdIOPipe.d.ts +15 -0
  38. package/dist/src/wasi/StdIOPipe.js +56 -0
  39. package/dist/src/wasi/StdIOPipe.js.map +1 -0
  40. package/dist/src/wasi/WasiPollScheduler.js +114 -0
  41. package/dist/src/wasi/WasiPollScheduler.js.map +1 -0
  42. package/dist/src/wasi/WasmSceneRuntime.d.ts +23 -0
  43. package/dist/src/wasi/WasmSceneRuntime.js +119 -0
  44. package/dist/src/wasi/WasmSceneRuntime.js.map +1 -0
  45. package/dist/src/wasi/WasmSceneWorker.d.ts +27 -0
  46. package/dist/src/wasi/WasmSceneWorker.js +109 -0
  47. package/dist/src/wasi/WasmSceneWorker.js.map +1 -0
  48. package/dist/testing.d.ts +2 -0
  49. package/dist/testing.js +2 -0
  50. package/dist/wasi-worker.d.ts +2 -0
  51. package/dist/wasi-worker.js +2 -0
  52. package/dist/wasi.d.ts +6 -0
  53. package/dist/wasi.js +6 -0
  54. package/dist/websocket.d.ts +2 -0
  55. package/dist/websocket.js +2 -0
  56. package/package.json +49 -18
  57. package/AGENTS.md +0 -52
  58. package/cli.ts +0 -168
  59. package/index.html +0 -50
  60. package/index.ts +0 -8
  61. package/manifest.ts +0 -1
  62. package/src/AccessibilityTree.ts +0 -262
  63. package/src/BoxDrawingRenderer.ts +0 -585
  64. package/src/PublicEntrypointBoundary.test.ts +0 -20
  65. package/src/WebHostApp.test.ts +0 -222
  66. package/src/WebHostApp.ts +0 -269
  67. package/src/WebHostSceneManifest.test.ts +0 -38
  68. package/src/WebHostSceneManifest.ts +0 -156
  69. package/src/WebHostSceneRuntime.test.ts +0 -1982
  70. package/src/WebHostSceneRuntime.ts +0 -1142
  71. package/src/WebHostSurfaceTransport.test.ts +0 -362
  72. package/src/WebHostSurfaceTransport.ts +0 -691
  73. package/src/WebHostTerminalStyle.test.ts +0 -123
  74. package/src/WebHostTerminalStyle.ts +0 -471
  75. package/src/WebHostTestFixtures.ts +0 -10
  76. package/src/WebSocketSceneBridge.test.ts +0 -198
  77. package/src/WebSocketSceneBridge.ts +0 -233
  78. package/src/browser.ts +0 -59
  79. package/src/wasi/BrowserWASIBridge.test.ts +0 -168
  80. package/src/wasi/BrowserWASIBridge.ts +0 -167
  81. package/src/wasi/SharedInputQueue.test.ts +0 -146
  82. package/src/wasi/SharedInputQueue.ts +0 -199
  83. package/src/wasi/StdIOPipe.ts +0 -72
  84. package/src/wasi/WasiPollScheduler.test.ts +0 -176
  85. package/src/wasi/WasiPollScheduler.ts +0 -305
  86. package/src/wasi/WasmSceneRuntime.ts +0 -205
  87. package/src/wasi/WasmSceneWorker.ts +0 -182
  88. package/testing.ts +0 -1
  89. package/tsconfig.json +0 -29
  90. package/wasi-worker.ts +0 -1
  91. package/wasi.ts +0 -4
  92. package/websocket.ts +0 -1
package/README.md CHANGED
@@ -6,9 +6,19 @@ This package owns browser-safe runtime APIs: scene manifest loading, canvas
6
6
  rendering, ARIA mounting, WebSocket scene bridges, and WASI scene bridges. Build
7
7
  tooling lives in the sibling [`@swifttui/build`](../build) workspace package.
8
8
 
9
- Publication status: the package name is reserved for the first public web
10
- release. Until it is published to npm or attached as a public release tarball,
11
- use the source checkout and the `swift-tui-examples/WebExample` template.
9
+ ## Installation
10
+
11
+ Published to npm as an ESM package with bundled TypeScript declarations — no
12
+ TypeScript toolchain required to consume it:
13
+
14
+ ```bash
15
+ npm install @swifttui/web
16
+ ```
17
+
18
+ The package ships compiled `dist/` JavaScript (`.js` + `.d.ts`); consuming it
19
+ does **not** require Bun or a TypeScript build step. Subpath entrypoints
20
+ (`./wasi`, `./wasi-worker`, `./manifest`, `./websocket`, `./testing`) and the
21
+ `./style.css` asset are declared in `package.json` `exports`.
12
22
 
13
23
  ## Toolchains
14
24
 
@@ -86,21 +96,25 @@ startWasmSceneWorker();
86
96
  ## Scripts
87
97
 
88
98
  - `bun test`
99
+ - `bun run build` — compile the publishable package to `dist/` with tsdown
100
+ (ESM `.js` + `.d.ts`). Run automatically on publish via `prepublishOnly`.
89
101
  - `bun run build:manifest -- --app <AppExecutable>`
90
102
  - `bun run build:wasm -- --app <AppExecutable>`
91
103
  - `bun run build:web`
92
- - `bun run build -- --app <AppExecutable>`
104
+ - `bun run build:app -- --app <AppExecutable>`
93
105
  - `bun run dev`
94
106
 
95
- `build:manifest`, `build:wasm`, and `build` delegate manifest/WASI packaging to
96
- `@swifttui/build`. `build:wasm` and `build` default to
97
- `--configuration release`; pass `--configuration debug` for local
98
- debug-oriented wasm builds.
107
+ `build` produces the published library. `build:manifest`, `build:wasm`, and
108
+ `build:app` delegate manifest/WASI packaging to `@swifttui/build`; `build:wasm`
109
+ and `build:app` default to `--configuration release` (pass
110
+ `--configuration debug` for local debug-oriented wasm builds). The demo/app
111
+ pipeline writes its artifacts to `dist-demo/` so they stay separate from the
112
+ published `dist/` library output.
99
113
 
100
- The build flow is intentionally small:
114
+ The demo/app build flow is intentionally small:
101
115
 
102
116
  1. `build:manifest` captures `TUIGUI_MODE=manifest` output from the Swift app by invoking `swiftly run swift`.
103
- 2. `build:wasm` copies the app's wasm artifact into `dist/assets/app.wasm`,
117
+ 2. `build:wasm` copies the app's wasm artifact into `dist-demo/assets/app.wasm`,
104
118
  validates it with the browser `WebAssembly` API, then keeps the stripped
105
119
  artifact only if stripping still produces browser-parseable wasm.
106
120
  3. `build:web` bundles `index.html` and the browser entrypoint with Bun.
@@ -0,0 +1,9 @@
1
+ import { ResolvedWebHostTerminalPalette, ResolvedWebHostTerminalStyle, WebHostANSIColors, WebHostTerminalAppearance, WebHostTerminalCursorStyle, WebHostTerminalPalette, WebHostTerminalRenderStyle, WebHostTerminalStyle, WebHostTerminalTheme, applyWebHostTerminalStyle, decodeWebHostTerminalRenderStyleBase64, encodeWebHostTerminalRenderStyleBase64, mergeWebHostTerminalStyle, normalizeWebHostTerminalStyle, resolveWebHostTerminalRenderStyle, webTUITerminalBackgroundColor } from "./src/WebHostTerminalStyle.js";
2
+ import { WebHostAccessibilityAnnouncement, WebHostAccessibilityLiveRegion, WebHostAccessibilityNode, WebHostAccessibilityPoint, WebHostFrameDiagnosticRecord, WebHostKeyInput, WebHostMouseInput, WebHostOutputDecoder, WebHostOutputRecord, WebHostOutputSink, WebHostRuntimeIssue, WebHostScrollRegion, WebHostSurfaceCell, WebHostSurfaceDamage, WebHostSurfaceDamageRange, WebHostSurfaceDamageTextRow, WebHostSurfaceDeltaFrame, WebHostSurfaceDeltaRow, WebHostSurfaceFrame, WebHostSurfaceImage, WebHostSurfaceImageFormat, WebHostSurfaceLineStyle, WebHostSurfaceRect, WebHostSurfaceSize, WebHostSurfaceStyle, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage } from "./src/WebHostSurfaceTransport.js";
3
+ import { WebHostSceneDescriptor, WebHostSceneManifest, WebHostSceneManifestSource, loadWebHostSceneManifest, normalizeWebHostSceneManifest, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON } from "./src/WebHostSceneManifest.js";
4
+ import { WebHostSceneBridge, WebHostSceneRuntime, WebHostSceneRuntimeOptions, WheelMode } from "./src/WebHostSceneRuntime.js";
5
+ import { WebSocketSceneBridge, WebSocketSceneBridgeFactory, WebSocketSceneBridgeOptions, WebSocketSceneSocket, webSocketSceneURL } from "./src/WebSocketSceneBridge.js";
6
+ import { WebHostAppController, WebHostAppOptions, WebHostBridgeFactory, WebHostBridgeFactoryOptions, WebHostEmbeddedHostConfig, createWebHostApp } from "./src/WebHostApp.js";
7
+ import { StdIOPipe } from "./src/wasi/StdIOPipe.js";
8
+ import { BrowserWASIBridge, BrowserWASIBridgeOptions, BrowserWASIOutputSink } from "./src/wasi/BrowserWASIBridge.js";
9
+ export { BrowserWASIBridge, BrowserWASIBridgeOptions, BrowserWASIOutputSink, ResolvedWebHostTerminalPalette, ResolvedWebHostTerminalStyle, StdIOPipe, WebHostANSIColors, WebHostAccessibilityAnnouncement, WebHostAccessibilityLiveRegion, WebHostAccessibilityNode, WebHostAccessibilityPoint, WebHostAppController, WebHostAppOptions, WebHostBridgeFactory, WebHostBridgeFactoryOptions, WebHostEmbeddedHostConfig, WebHostFrameDiagnosticRecord, WebHostKeyInput, WebHostMouseInput, WebHostOutputDecoder, WebHostOutputRecord, WebHostOutputSink, WebHostRuntimeIssue, WebHostSceneBridge, WebHostSceneDescriptor, WebHostSceneManifest, WebHostSceneManifestSource, WebHostSceneRuntime, WebHostSceneRuntimeOptions, WebHostScrollRegion, WebHostSurfaceCell, WebHostSurfaceDamage, WebHostSurfaceDamageRange, WebHostSurfaceDamageTextRow, WebHostSurfaceDeltaFrame, WebHostSurfaceDeltaRow, WebHostSurfaceFrame, WebHostSurfaceImage, WebHostSurfaceImageFormat, WebHostSurfaceLineStyle, WebHostSurfaceRect, WebHostSurfaceSize, WebHostSurfaceStyle, WebHostTerminalAppearance, WebHostTerminalCursorStyle, WebHostTerminalPalette, WebHostTerminalRenderStyle, WebHostTerminalStyle, WebHostTerminalTheme, WebSocketSceneBridge, WebSocketSceneBridgeFactory, WebSocketSceneBridgeOptions, WebSocketSceneSocket, WheelMode, applyWebHostTerminalStyle, createWebHostApp, decodeWebHostTerminalRenderStyleBase64, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage, encodeWebHostTerminalRenderStyleBase64, loadWebHostSceneManifest, mergeWebHostTerminalStyle, normalizeWebHostSceneManifest, normalizeWebHostTerminalStyle, resolveWebHostTerminalRenderStyle, webSocketSceneURL, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON, webTUITerminalBackgroundColor };
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ import { StdIOPipe } from "./src/wasi/StdIOPipe.js";
2
+ import { applyWebHostTerminalStyle, decodeWebHostTerminalRenderStyleBase64, encodeWebHostTerminalRenderStyleBase64, mergeWebHostTerminalStyle, normalizeWebHostTerminalStyle, resolveWebHostTerminalRenderStyle, webTUITerminalBackgroundColor } from "./src/WebHostTerminalStyle.js";
3
+ import { WebHostOutputDecoder, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage } from "./src/WebHostSurfaceTransport.js";
4
+ import { BrowserWASIBridge } from "./src/wasi/BrowserWASIBridge.js";
5
+ import { WebSocketSceneBridge, webSocketSceneURL } from "./src/WebSocketSceneBridge.js";
6
+ import { loadWebHostSceneManifest, normalizeWebHostSceneManifest, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON } from "./src/WebHostSceneManifest.js";
7
+ import { WebHostSceneRuntime } from "./src/WebHostSceneRuntime.js";
8
+ import { createWebHostApp } from "./src/WebHostApp.js";
9
+ export { BrowserWASIBridge, StdIOPipe, WebHostOutputDecoder, WebHostSceneRuntime, WebSocketSceneBridge, applyWebHostTerminalStyle, createWebHostApp, decodeWebHostTerminalRenderStyleBase64, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage, encodeWebHostTerminalRenderStyleBase64, loadWebHostSceneManifest, mergeWebHostTerminalStyle, normalizeWebHostSceneManifest, normalizeWebHostTerminalStyle, resolveWebHostTerminalRenderStyle, webSocketSceneURL, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON, webTUITerminalBackgroundColor };
@@ -0,0 +1,2 @@
1
+ import { WebHostSceneDescriptor, WebHostSceneManifest, WebHostSceneManifestSource, loadWebHostSceneManifest, normalizeWebHostSceneManifest, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON } from "./src/WebHostSceneManifest.js";
2
+ export { WebHostSceneDescriptor, WebHostSceneManifest, WebHostSceneManifestSource, loadWebHostSceneManifest, normalizeWebHostSceneManifest, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON };
@@ -0,0 +1,2 @@
1
+ import { loadWebHostSceneManifest, normalizeWebHostSceneManifest, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON } from "./src/WebHostSceneManifest.js";
2
+ export { loadWebHostSceneManifest, normalizeWebHostSceneManifest, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON };
@@ -0,0 +1,156 @@
1
+ //#region src/AccessibilityTree.ts
2
+ var AccessibilityTreeMounter = class {
3
+ element;
4
+ announcerElement;
5
+ nodesById = /* @__PURE__ */ new Map();
6
+ previousLabelsById = /* @__PURE__ */ new Map();
7
+ hasLiveRegionBaseline = false;
8
+ constructor() {
9
+ this.element = document.createElement("div");
10
+ this.element.className = "webhost-scene__accessibility-tree";
11
+ applyScreenReaderOnlyStyle(this.element);
12
+ this.announcerElement = document.createElement("div");
13
+ this.announcerElement.className = "webhost-scene__accessibility-announcer";
14
+ this.announcerElement.setAttribute("aria-atomic", "true");
15
+ applyScreenReaderOnlyStyle(this.announcerElement);
16
+ }
17
+ present(nodes, metrics, announcements = [], options = {}) {
18
+ this.element.replaceChildren();
19
+ this.nodesById.clear();
20
+ for (const node of nodes) {
21
+ const element = this.elementForNode(node, metrics);
22
+ this.nodesById.set(node.id, element);
23
+ }
24
+ for (const node of nodes) {
25
+ const element = this.nodesById.get(node.id);
26
+ if (!element) continue;
27
+ ((node.parentId ? this.nodesById.get(node.parentId) : void 0) ?? this.element).appendChild(element);
28
+ }
29
+ this.announceLiveRegionChanges(nodes, announcements);
30
+ const focused = nodes.find((node) => node.isFocused);
31
+ if ((options.synchronizeFocus ?? true) && focused) this.nodesById.get(focused.id)?.focus?.({ preventScroll: true });
32
+ }
33
+ elementForNode(node, metrics) {
34
+ const element = document.createElement("div");
35
+ element.id = `swifttui-a11y-${stableDOMId(node.id)}`;
36
+ element.dataset.accessibilityId = node.id;
37
+ element.tabIndex = node.isFocused ? 0 : -1;
38
+ const role = roleMapping(node.role);
39
+ if (role.role) element.setAttribute("role", role.role);
40
+ if (role.level !== void 0) element.setAttribute("aria-level", String(role.level));
41
+ if (node.label) element.setAttribute("aria-label", node.label);
42
+ if (node.hint) element.setAttribute("aria-description", node.hint);
43
+ if (node.liveRegion) element.setAttribute("aria-live", node.liveRegion);
44
+ if (node.isFocused) element.dataset.focused = "true";
45
+ const [x, y, width, height] = node.rect;
46
+ element.style.position = "absolute";
47
+ element.style.left = `${x * metrics.cellWidth}px`;
48
+ element.style.top = `${y * metrics.cellHeight}px`;
49
+ element.style.width = `${Math.max(1, width) * metrics.cellWidth}px`;
50
+ element.style.height = `${Math.max(1, height) * metrics.cellHeight}px`;
51
+ return element;
52
+ }
53
+ announceLiveRegionChanges(nodes, announcements) {
54
+ const candidates = nodes.filter((node) => node.liveRegion && node.liveRegion !== "off" && node.label);
55
+ const currentLabelsById = new Map(candidates.map((node) => [node.id, node.label ?? ""]));
56
+ const imperativeAssertive = announcements.filter((announcement) => announcement.politeness === "assertive");
57
+ const imperativePolite = announcements.filter((announcement) => announcement.politeness === "polite");
58
+ if (!this.hasLiveRegionBaseline) {
59
+ this.previousLabelsById = currentLabelsById;
60
+ this.hasLiveRegionBaseline = true;
61
+ this.publishAnnouncements([], imperativeAssertive, [], imperativePolite);
62
+ return;
63
+ }
64
+ const changed = candidates.filter((node) => {
65
+ const previous = this.previousLabelsById.get(node.id);
66
+ return previous !== void 0 && previous !== node.label;
67
+ });
68
+ this.previousLabelsById = currentLabelsById;
69
+ const assertive = changed.filter((node) => node.liveRegion === "assertive");
70
+ const polite = changed.filter((node) => node.liveRegion === "polite");
71
+ this.publishAnnouncements(assertive, imperativeAssertive, polite, imperativePolite);
72
+ }
73
+ publishAnnouncements(assertive, imperativeAssertive, polite, imperativePolite) {
74
+ const ordered = [
75
+ ...assertive,
76
+ ...imperativeAssertive,
77
+ ...polite,
78
+ ...imperativePolite
79
+ ];
80
+ if (ordered.length === 0) return;
81
+ const politeness = assertive.length > 0 || imperativeAssertive.length > 0 ? "assertive" : "polite";
82
+ this.announcerElement.setAttribute("aria-live", politeness);
83
+ this.announcerElement.textContent = ordered.map((entry) => {
84
+ if ("message" in entry) return entry.message;
85
+ return entry.label ?? "";
86
+ }).join("\n");
87
+ }
88
+ };
89
+ function applyScreenReaderOnlyStyle(element) {
90
+ element.style.position = "absolute";
91
+ element.style.left = "0";
92
+ element.style.top = "0";
93
+ element.style.width = "1px";
94
+ element.style.height = "1px";
95
+ element.style.overflow = "hidden";
96
+ element.style.clipPath = "inset(50%)";
97
+ element.style.whiteSpace = "nowrap";
98
+ }
99
+ function roleMapping(role) {
100
+ const heading = /^heading\(level: ([0-9]+)\)$/.exec(role);
101
+ if (heading) return {
102
+ role: "heading",
103
+ level: Math.max(1, Math.min(6, Number(heading[1])))
104
+ };
105
+ const custom = /^custom\((.+)\)$/.exec(role);
106
+ if (custom) return { role: custom[1] };
107
+ switch (role) {
108
+ case "alert":
109
+ case "button":
110
+ case "cell":
111
+ case "checkbox":
112
+ case "grid":
113
+ case "group":
114
+ case "link":
115
+ case "list":
116
+ case "menu":
117
+ case "region":
118
+ case "separator":
119
+ case "slider":
120
+ case "status":
121
+ case "tab":
122
+ case "table":
123
+ case "timer": return { role };
124
+ case "columnHeader": return { role: "columnheader" };
125
+ case "confirmationDialog":
126
+ case "sheet": return { role: "dialog" };
127
+ case "disclosureGroup":
128
+ case "scrollView":
129
+ case "scrollViewWithIndicators":
130
+ case "section": return { role: "region" };
131
+ case "image": return { role: "img" };
132
+ case "menuItem": return { role: "menuitem" };
133
+ case "picker": return { role: "combobox" };
134
+ case "progressBar": return { role: "progressbar" };
135
+ case "rowHeader": return { role: "rowheader" };
136
+ case "secureField":
137
+ case "textEditor":
138
+ case "textField": return { role: "textbox" };
139
+ case "stepper": return { role: "spinbutton" };
140
+ case "tabPanel": return { role: "tabpanel" };
141
+ case "tableRow": return { role: "row" };
142
+ case "tabView": return { role: "tablist" };
143
+ case "toggle": return { role: "checkbox" };
144
+ default: return { role: "group" };
145
+ }
146
+ }
147
+ function stableDOMId(id) {
148
+ return Array.from(id).map((character) => {
149
+ if (/^[a-zA-Z0-9_-]$/.test(character)) return character;
150
+ return `-${character.codePointAt(0)?.toString(16) ?? "0"}-`;
151
+ }).join("");
152
+ }
153
+ //#endregion
154
+ export { AccessibilityTreeMounter };
155
+
156
+ //# sourceMappingURL=AccessibilityTree.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AccessibilityTree.js","names":[],"sources":["../../src/AccessibilityTree.ts"],"sourcesContent":["import type {\n WebHostAccessibilityAnnouncement,\n WebHostAccessibilityNode,\n} from \"./WebHostSurfaceTransport.ts\";\n\ninterface AccessibilityTreeMetrics {\n cellWidth: number;\n cellHeight: number;\n}\n\ninterface AccessibilityTreePresentationOptions {\n synchronizeFocus?: boolean;\n}\n\ninterface RoleMapping {\n role?: string;\n level?: number;\n}\n\nexport class AccessibilityTreeMounter {\n readonly element: HTMLElement;\n readonly announcerElement: HTMLElement;\n\n private readonly nodesById = new Map<string, HTMLElement>();\n private previousLabelsById = new Map<string, string>();\n private hasLiveRegionBaseline = false;\n\n constructor() {\n this.element = document.createElement(\"div\");\n this.element.className = \"webhost-scene__accessibility-tree\";\n applyScreenReaderOnlyStyle(this.element);\n\n this.announcerElement = document.createElement(\"div\");\n this.announcerElement.className = \"webhost-scene__accessibility-announcer\";\n this.announcerElement.setAttribute(\"aria-atomic\", \"true\");\n applyScreenReaderOnlyStyle(this.announcerElement);\n }\n\n present(\n nodes: WebHostAccessibilityNode[],\n metrics: AccessibilityTreeMetrics,\n announcements: WebHostAccessibilityAnnouncement[] = [],\n options: AccessibilityTreePresentationOptions = {}\n ): void {\n this.element.replaceChildren();\n this.nodesById.clear();\n\n for (const node of nodes) {\n const element = this.elementForNode(node, metrics);\n this.nodesById.set(node.id, element);\n }\n\n for (const node of nodes) {\n const element = this.nodesById.get(node.id);\n if (!element) {\n continue;\n }\n\n const parent = node.parentId ? this.nodesById.get(node.parentId) : undefined;\n (parent ?? this.element).appendChild(element);\n }\n\n this.announceLiveRegionChanges(nodes, announcements);\n\n const focused = nodes.find((node) => node.isFocused);\n if ((options.synchronizeFocus ?? true) && focused) {\n this.nodesById.get(focused.id)?.focus?.({ preventScroll: true });\n }\n }\n\n private elementForNode(\n node: WebHostAccessibilityNode,\n metrics: AccessibilityTreeMetrics\n ): HTMLElement {\n const element = document.createElement(\"div\");\n element.id = `swifttui-a11y-${stableDOMId(node.id)}`;\n element.dataset.accessibilityId = node.id;\n element.tabIndex = node.isFocused ? 0 : -1;\n\n const role = roleMapping(node.role);\n if (role.role) {\n element.setAttribute(\"role\", role.role);\n }\n if (role.level !== undefined) {\n element.setAttribute(\"aria-level\", String(role.level));\n }\n if (node.label) {\n element.setAttribute(\"aria-label\", node.label);\n }\n if (node.hint) {\n element.setAttribute(\"aria-description\", node.hint);\n }\n if (node.liveRegion) {\n element.setAttribute(\"aria-live\", node.liveRegion);\n }\n if (node.isFocused) {\n element.dataset.focused = \"true\";\n }\n\n const [x, y, width, height] = node.rect;\n element.style.position = \"absolute\";\n element.style.left = `${x * metrics.cellWidth}px`;\n element.style.top = `${y * metrics.cellHeight}px`;\n element.style.width = `${Math.max(1, width) * metrics.cellWidth}px`;\n element.style.height = `${Math.max(1, height) * metrics.cellHeight}px`;\n\n return element;\n }\n\n private announceLiveRegionChanges(\n nodes: WebHostAccessibilityNode[],\n announcements: WebHostAccessibilityAnnouncement[]\n ): void {\n const candidates = nodes.filter(\n (node) => node.liveRegion && node.liveRegion !== \"off\" && node.label\n );\n const currentLabelsById = new Map(candidates.map((node) => [node.id, node.label ?? \"\"]));\n const imperativeAssertive = announcements.filter(\n (announcement) => announcement.politeness === \"assertive\"\n );\n const imperativePolite = announcements.filter(\n (announcement) => announcement.politeness === \"polite\"\n );\n\n if (!this.hasLiveRegionBaseline) {\n this.previousLabelsById = currentLabelsById;\n this.hasLiveRegionBaseline = true;\n this.publishAnnouncements([], imperativeAssertive, [], imperativePolite);\n return;\n }\n\n const changed = candidates.filter((node) => {\n const previous = this.previousLabelsById.get(node.id);\n return previous !== undefined && previous !== node.label;\n });\n this.previousLabelsById = currentLabelsById;\n\n const assertive = changed.filter((node) => node.liveRegion === \"assertive\");\n const polite = changed.filter((node) => node.liveRegion === \"polite\");\n this.publishAnnouncements(assertive, imperativeAssertive, polite, imperativePolite);\n }\n\n private publishAnnouncements(\n assertive: WebHostAccessibilityNode[],\n imperativeAssertive: WebHostAccessibilityAnnouncement[],\n polite: WebHostAccessibilityNode[],\n imperativePolite: WebHostAccessibilityAnnouncement[]\n ): void {\n const ordered = [...assertive, ...imperativeAssertive, ...polite, ...imperativePolite];\n if (ordered.length === 0) {\n return;\n }\n\n const politeness = assertive.length > 0 || imperativeAssertive.length > 0\n ? \"assertive\"\n : \"polite\";\n this.announcerElement.setAttribute(\"aria-live\", politeness);\n this.announcerElement.textContent = ordered.map((entry) => {\n if (\"message\" in entry) {\n return entry.message;\n }\n return entry.label ?? \"\";\n }).join(\"\\n\");\n }\n}\n\nfunction applyScreenReaderOnlyStyle(\n element: HTMLElement\n): void {\n element.style.position = \"absolute\";\n element.style.left = \"0\";\n element.style.top = \"0\";\n element.style.width = \"1px\";\n element.style.height = \"1px\";\n element.style.overflow = \"hidden\";\n element.style.clipPath = \"inset(50%)\";\n element.style.whiteSpace = \"nowrap\";\n}\n\nfunction roleMapping(\n role: string\n): RoleMapping {\n const heading = /^heading\\(level: ([0-9]+)\\)$/.exec(role);\n if (heading) {\n return {\n role: \"heading\",\n level: Math.max(1, Math.min(6, Number(heading[1]))),\n };\n }\n\n const custom = /^custom\\((.+)\\)$/.exec(role);\n if (custom) {\n return { role: custom[1] };\n }\n\n switch (role) {\n case \"alert\":\n case \"button\":\n case \"cell\":\n case \"checkbox\":\n case \"grid\":\n case \"group\":\n case \"link\":\n case \"list\":\n case \"menu\":\n case \"region\":\n case \"separator\":\n case \"slider\":\n case \"status\":\n case \"tab\":\n case \"table\":\n case \"timer\":\n return { role };\n case \"columnHeader\":\n return { role: \"columnheader\" };\n case \"confirmationDialog\":\n case \"sheet\":\n return { role: \"dialog\" };\n case \"disclosureGroup\":\n case \"scrollView\":\n case \"scrollViewWithIndicators\":\n case \"section\":\n return { role: \"region\" };\n case \"image\":\n return { role: \"img\" };\n case \"menuItem\":\n return { role: \"menuitem\" };\n case \"picker\":\n return { role: \"combobox\" };\n case \"progressBar\":\n return { role: \"progressbar\" };\n case \"rowHeader\":\n return { role: \"rowheader\" };\n case \"secureField\":\n case \"textEditor\":\n case \"textField\":\n return { role: \"textbox\" };\n case \"stepper\":\n return { role: \"spinbutton\" };\n case \"tabPanel\":\n return { role: \"tabpanel\" };\n case \"tableRow\":\n return { role: \"row\" };\n case \"tabView\":\n return { role: \"tablist\" };\n case \"toggle\":\n return { role: \"checkbox\" };\n default:\n return { role: \"group\" };\n }\n}\n\nfunction stableDOMId(\n id: string\n): string {\n return Array.from(id).map((character) => {\n if (/^[a-zA-Z0-9_-]$/.test(character)) {\n return character;\n }\n return `-${character.codePointAt(0)?.toString(16) ?? \"0\"}-`;\n }).join(\"\");\n}\n"],"mappings":"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"}