@devaccel-labs/app-comments-toolbar 1.1.0 → 1.3.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.
Files changed (214) hide show
  1. package/README.md +148 -30
  2. package/dist/react.js +417 -64
  3. package/dist/server.js +342 -14
  4. package/dist/types/capture/api.d.ts +30 -0
  5. package/dist/types/capture/editor.d.ts +10 -0
  6. package/dist/types/capture/ignore.d.ts +19 -0
  7. package/dist/types/capture/index.d.ts +1 -0
  8. package/dist/types/capture/picker.d.ts +9 -0
  9. package/dist/types/capture/picture.d.ts +29 -0
  10. package/dist/types/capture/surface.d.ts +39 -0
  11. package/dist/types/core/index.d.ts +2 -2
  12. package/dist/types/core/resolve.d.ts +2 -1
  13. package/dist/types/core/types.d.ts +11 -0
  14. package/dist/types/debug-context.d.ts +7 -0
  15. package/dist/types/dictation.d.ts +9 -0
  16. package/dist/types/embed/controller.d.ts +61 -0
  17. package/dist/types/embed/index.d.ts +1 -0
  18. package/dist/types/embed/options.d.ts +35 -0
  19. package/dist/types/help/index.d.ts +1 -0
  20. package/dist/types/help/panel-index.d.ts +1 -0
  21. package/dist/types/help/panel-styles.d.ts +1 -0
  22. package/dist/types/help/panel.d.ts +4 -0
  23. package/dist/types/help/pill-styles.d.ts +11 -0
  24. package/dist/types/help/pill.d.ts +44 -0
  25. package/dist/types/help/react.d.ts +12 -0
  26. package/dist/types/help/requests.d.ts +2 -0
  27. package/dist/types/help/types.d.ts +119 -0
  28. package/dist/types/help/wire.d.ts +61 -0
  29. package/dist/types/native-wire.d.ts +63 -0
  30. package/dist/types/native.d.ts +43 -0
  31. package/dist/types/react.d.ts +7 -0
  32. package/dist/types/replay.d.ts +6 -0
  33. package/dist/types/runtime/api.d.ts +250 -0
  34. package/dist/types/runtime/attachment-model.d.ts +16 -0
  35. package/dist/types/runtime/attachments.d.ts +43 -7
  36. package/dist/types/runtime/boot.d.ts +39 -0
  37. package/dist/types/runtime/browser-label.d.ts +5 -0
  38. package/dist/types/runtime/build-id.d.ts +1 -0
  39. package/dist/types/runtime/capture-ui.d.ts +45 -21
  40. package/dist/types/runtime/clarity-model.d.ts +260 -0
  41. package/dist/types/runtime/clarity-ui.d.ts +62 -0
  42. package/dist/types/runtime/client.d.ts +6 -197
  43. package/dist/types/runtime/composer-size.d.ts +30 -0
  44. package/dist/types/runtime/dashboard.d.ts +17 -2
  45. package/dist/types/runtime/debug-context-core.d.ts +88 -0
  46. package/dist/types/runtime/debug-context-host.d.ts +39 -0
  47. package/dist/types/runtime/debug-context-model.d.ts +352 -0
  48. package/dist/types/runtime/debug-context-redact.d.ts +48 -0
  49. package/dist/types/runtime/debug-context.d.ts +36 -0
  50. package/dist/types/runtime/details-model.d.ts +7 -0
  51. package/dist/types/runtime/details.d.ts +7 -5
  52. package/dist/types/runtime/dictation-engine.d.ts +98 -0
  53. package/dist/types/runtime/dictation-model.d.ts +28 -0
  54. package/dist/types/runtime/dictation-ui.d.ts +56 -0
  55. package/dist/types/runtime/dictation.d.ts +66 -0
  56. package/dist/types/runtime/draft-dock.d.ts +32 -0
  57. package/dist/types/runtime/draft-model.d.ts +104 -0
  58. package/dist/types/runtime/draft-store.d.ts +15 -0
  59. package/dist/types/runtime/edits.d.ts +1 -1
  60. package/dist/types/runtime/font.d.ts +16 -0
  61. package/dist/types/runtime/format.d.ts +30 -1
  62. package/dist/types/runtime/host-watch.d.ts +20 -0
  63. package/dist/types/runtime/icons.d.ts +16 -2
  64. package/dist/types/runtime/index.d.ts +2 -0
  65. package/dist/types/runtime/ink.d.ts +8 -0
  66. package/dist/types/runtime/kind-model.d.ts +14 -0
  67. package/dist/types/runtime/live-model.d.ts +25 -0
  68. package/dist/types/runtime/loader.d.ts +5 -3
  69. package/dist/types/runtime/markup-editor.d.ts +54 -0
  70. package/dist/types/runtime/markup-model.d.ts +126 -0
  71. package/dist/types/runtime/markup.d.ts +69 -0
  72. package/dist/types/runtime/mention-parse.d.ts +18 -0
  73. package/dist/types/runtime/mentions.d.ts +4 -4
  74. package/dist/types/runtime/page-background.d.ts +9 -0
  75. package/dist/types/runtime/page-image.d.ts +64 -11
  76. package/dist/types/runtime/placement.d.ts +6 -3
  77. package/dist/types/runtime/record.d.ts +18 -2
  78. package/dist/types/runtime/related.d.ts +1 -1
  79. package/dist/types/runtime/replay-model.d.ts +80 -0
  80. package/dist/types/runtime/replay.d.ts +5 -38
  81. package/dist/types/runtime/screen.d.ts +47 -0
  82. package/dist/types/runtime/screenshot.d.ts +60 -8
  83. package/dist/types/runtime/session-source.d.ts +45 -0
  84. package/dist/types/runtime/session.d.ts +20 -18
  85. package/dist/types/runtime/shield.d.ts +9 -0
  86. package/dist/types/runtime/shot.d.ts +45 -0
  87. package/dist/types/runtime/sketch.d.ts +26 -0
  88. package/dist/types/runtime/styles.d.ts +1 -1
  89. package/dist/types/runtime/sync.d.ts +1 -0
  90. package/dist/types/runtime/thread-model.d.ts +18 -0
  91. package/dist/types/runtime/ticket-form-model.d.ts +57 -0
  92. package/dist/types/runtime/ticket-form.d.ts +9 -0
  93. package/dist/types/runtime/toolbar.d.ts +37 -1
  94. package/dist/types/runtime/ui-scale.d.ts +17 -0
  95. package/dist/types/runtime/update.d.ts +32 -8
  96. package/dist/types/runtime/use-debug-context.d.ts +27 -0
  97. package/dist/types/server/index.d.ts +22 -0
  98. package/dist/types/server/support.d.ts +20 -0
  99. package/dist/types/server.d.ts +2 -0
  100. package/dist/types/vite/plugin.d.ts +15 -0
  101. package/dist/vite.js +121 -41
  102. package/package.json +24 -11
  103. package/src/capture/api.ts +128 -0
  104. package/src/capture/editor.tsx +178 -0
  105. package/src/capture/ignore.ts +38 -0
  106. package/src/capture/index.ts +27 -0
  107. package/src/capture/picker.tsx +171 -0
  108. package/src/capture/picture.ts +90 -0
  109. package/src/capture/surface.tsx +185 -0
  110. package/src/compiler/annotate.ts +202 -0
  111. package/src/compiler/babel.d.ts +11 -0
  112. package/src/compiler/enabled.ts +55 -0
  113. package/src/compiler/index.ts +2 -0
  114. package/src/core/index.ts +8 -0
  115. package/src/core/resolve.ts +307 -0
  116. package/src/core/types.ts +58 -0
  117. package/src/debug-context.ts +47 -0
  118. package/src/dictation.ts +17 -0
  119. package/src/embed/controller.ts +215 -0
  120. package/src/embed/index.ts +68 -0
  121. package/src/embed/options.ts +100 -0
  122. package/src/help/index.ts +38 -0
  123. package/src/help/panel-index.ts +17 -0
  124. package/src/help/panel-styles.ts +123 -0
  125. package/src/help/panel.ts +840 -0
  126. package/src/help/pill-styles.ts +75 -0
  127. package/src/help/pill.ts +338 -0
  128. package/src/help/react.tsx +143 -0
  129. package/src/help/requests.ts +111 -0
  130. package/src/help/types.ts +124 -0
  131. package/src/help/wire.ts +55 -0
  132. package/src/native-wire.ts +124 -0
  133. package/src/native.ts +282 -0
  134. package/src/react.ts +27 -0
  135. package/src/replay.ts +13 -0
  136. package/src/runtime/api.ts +531 -0
  137. package/src/runtime/attachment-model.ts +42 -0
  138. package/src/runtime/attachments.tsx +1260 -0
  139. package/src/runtime/boot.ts +95 -0
  140. package/src/runtime/browser-label.ts +31 -0
  141. package/src/runtime/build-id.ts +10 -0
  142. package/src/runtime/capture-ui.tsx +306 -0
  143. package/src/runtime/capture.ts +508 -0
  144. package/src/runtime/clarity-model.ts +634 -0
  145. package/src/runtime/clarity-ui.tsx +454 -0
  146. package/src/runtime/client.ts +151 -0
  147. package/src/runtime/composer-size.tsx +146 -0
  148. package/src/runtime/dashboard.tsx +1130 -0
  149. package/src/runtime/debug-context-core.ts +681 -0
  150. package/src/runtime/debug-context-host.ts +56 -0
  151. package/src/runtime/debug-context-model.ts +1319 -0
  152. package/src/runtime/debug-context-redact.ts +259 -0
  153. package/src/runtime/debug-context.ts +613 -0
  154. package/src/runtime/details-model.ts +30 -0
  155. package/src/runtime/details.tsx +395 -0
  156. package/src/runtime/dictation-engine.ts +616 -0
  157. package/src/runtime/dictation-model.ts +79 -0
  158. package/src/runtime/dictation-ui.tsx +363 -0
  159. package/src/runtime/dictation.ts +271 -0
  160. package/src/runtime/dock.ts +108 -0
  161. package/src/runtime/draft-dock.tsx +124 -0
  162. package/src/runtime/draft-model.ts +251 -0
  163. package/src/runtime/draft-store.ts +149 -0
  164. package/src/runtime/edits.tsx +165 -0
  165. package/src/runtime/font.ts +42 -0
  166. package/src/runtime/format.ts +129 -0
  167. package/src/runtime/freshness.ts +23 -0
  168. package/src/runtime/handoff.ts +76 -0
  169. package/src/runtime/host-watch.ts +64 -0
  170. package/src/runtime/icons.tsx +295 -0
  171. package/src/runtime/idle.ts +20 -0
  172. package/src/runtime/index.ts +11 -0
  173. package/src/runtime/ink.ts +6 -0
  174. package/src/runtime/kind-model.ts +20 -0
  175. package/src/runtime/live-model.ts +32 -0
  176. package/src/runtime/loader.tsx +87 -0
  177. package/src/runtime/markup-editor.tsx +1333 -0
  178. package/src/runtime/markup-model.ts +485 -0
  179. package/src/runtime/markup.ts +382 -0
  180. package/src/runtime/members.ts +58 -0
  181. package/src/runtime/mention-parse.ts +56 -0
  182. package/src/runtime/mentions.tsx +205 -0
  183. package/src/runtime/page-background.ts +51 -0
  184. package/src/runtime/page-image.ts +1592 -0
  185. package/src/runtime/placement.ts +336 -0
  186. package/src/runtime/record.ts +402 -0
  187. package/src/runtime/redact.ts +30 -0
  188. package/src/runtime/related.ts +34 -0
  189. package/src/runtime/render-page.ts +2 -0
  190. package/src/runtime/replay-model.ts +232 -0
  191. package/src/runtime/replay.ts +173 -0
  192. package/src/runtime/runtime-version.ts +7 -0
  193. package/src/runtime/runtime.tsx +51 -0
  194. package/src/runtime/screen.ts +182 -0
  195. package/src/runtime/screenshot.ts +260 -0
  196. package/src/runtime/session-source.ts +112 -0
  197. package/src/runtime/session.ts +108 -0
  198. package/src/runtime/shield.ts +34 -0
  199. package/src/runtime/shot.tsx +422 -0
  200. package/src/runtime/sketch.ts +139 -0
  201. package/src/runtime/styles.ts +1541 -0
  202. package/src/runtime/sync.ts +300 -0
  203. package/src/runtime/thread-model.ts +71 -0
  204. package/src/runtime/ticket-form-model.ts +128 -0
  205. package/src/runtime/ticket-form.tsx +342 -0
  206. package/src/runtime/toolbar.tsx +4427 -0
  207. package/src/runtime/ui-scale.ts +75 -0
  208. package/src/runtime/update.ts +98 -0
  209. package/src/runtime/use-debug-context.ts +81 -0
  210. package/src/server/index.ts +133 -0
  211. package/src/server/support.ts +499 -0
  212. package/src/server.ts +4 -0
  213. package/src/vite/plugin.ts +102 -0
  214. package/src/vite.ts +1 -0
@@ -0,0 +1,508 @@
1
+ import type { Anchor, Candidate, Resolution } from "../core/index.js";
2
+ import { resolveAnchor } from "../core/index.js";
3
+
4
+ const EXPLICIT = ["data-cs-anchor", "data-testid", "data-test", "id"];
5
+ const SKIPPED = new Set(["SCRIPT", "STYLE", "LINK", "META"]);
6
+ /** Most elements one lookup builds candidates for, so a huge page can't stall a resolve. */
7
+ const MAX_ELEMENTS = 8000;
8
+
9
+ function clip(value: string | null | undefined, max = 160) {
10
+ const text = (value ?? "").replace(/\s+/g, " ").trim();
11
+ return text ? text.slice(0, max) : null;
12
+ }
13
+
14
+ function usableExplicit(attr: string, value: string) {
15
+ if (!value || value.length > 200) return false;
16
+ if (attr === "id" || attr === "data-testid" || attr === "data-test") {
17
+ if (value.startsWith(":") || value.startsWith("radix-") || value.startsWith("headlessui-"))
18
+ return false;
19
+ }
20
+ return true;
21
+ }
22
+
23
+ function domPath(element: Element) {
24
+ const parts: number[] = [];
25
+ let current: Element | null = element;
26
+ while (current && current !== current.ownerDocument.body) {
27
+ const parent: Element | null = current.parentElement;
28
+ if (!parent) break;
29
+ parts.push([...parent.children].indexOf(current));
30
+ current = parent;
31
+ }
32
+ return parts.reverse().join("/");
33
+ }
34
+
35
+ function intraPath(element: Element, owner: Element | null) {
36
+ if (!owner || owner === element) return element === owner ? element.tagName.toLowerCase() : null;
37
+ const parts: string[] = [];
38
+ let current: Element | null = element;
39
+ while (current && current !== owner) {
40
+ const parent: Element | null = current.parentElement;
41
+ if (!parent) break;
42
+ const same = [...parent.children].filter((child) => child.tagName === current!.tagName);
43
+ const index = same.indexOf(current);
44
+ const tag = current.tagName.toLowerCase();
45
+ parts.push(same.length > 1 ? `${tag}[${index}]` : tag);
46
+ current = parent;
47
+ }
48
+ return parts.reverse().join("/") || null;
49
+ }
50
+
51
+ /**
52
+ * `clip(element.textContent, max)` without reading the whole subtree: text nodes are visited in order and the walk
53
+ * stops once `max` normalized characters are in hand, so a page-wide wrapper costs the same as a button.
54
+ */
55
+ function clippedText(element: Element, max: number) {
56
+ const walker = element.ownerDocument.createTreeWalker(element, NodeFilter.SHOW_TEXT);
57
+ let text = "";
58
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
59
+ text = `${text}${node.textContent ?? ""}`.replace(/\s+/g, " ");
60
+ if (text.trimStart().length >= max) break;
61
+ }
62
+ return clip(text, max);
63
+ }
64
+
65
+ function elementText(element: Element) {
66
+ let direct = "";
67
+ for (const node of element.childNodes) {
68
+ if (node.nodeType === Node.TEXT_NODE) direct += node.textContent ?? "";
69
+ }
70
+ const own = clip(direct);
71
+ if (own) return own;
72
+ if (element.children.length > 3) return null;
73
+ return clippedText(element, 80);
74
+ }
75
+
76
+ function labelFor(element: Element) {
77
+ const aria = element.getAttribute("aria-label");
78
+ if (aria) return clip(aria);
79
+ const labelledby = element.getAttribute("aria-labelledby");
80
+ if (labelledby) {
81
+ const text = labelledby
82
+ .split(/\s+/)
83
+ .map((id) => element.ownerDocument.getElementById(id)?.textContent ?? "")
84
+ .join(" ");
85
+ const clipped = clip(text);
86
+ if (clipped) return clipped;
87
+ }
88
+ if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
89
+ if (element.labels?.[0]?.textContent) return clip(element.labels[0].textContent);
90
+ if (element.placeholder) return clip(element.placeholder);
91
+ }
92
+ if (element instanceof HTMLImageElement && element.alt) return clip(element.alt);
93
+ return elementText(element);
94
+ }
95
+
96
+ function roleFor(element: Element) {
97
+ const explicit = element.getAttribute("role");
98
+ if (explicit) return explicit;
99
+ const tag = element.tagName.toLowerCase();
100
+ if (tag === "button") return "button";
101
+ if (tag === "a" && element.hasAttribute("href")) return "link";
102
+ if (tag === "textarea") return "textbox";
103
+ if (tag === "select") return "combobox";
104
+ if (tag === "dialog") return "dialog";
105
+ if (/^h[1-6]$/.test(tag)) return "heading";
106
+ if (element instanceof HTMLInputElement)
107
+ return element.type === "checkbox" ? "checkbox" : "textbox";
108
+ return null;
109
+ }
110
+
111
+ function overlayFor(element: Element) {
112
+ const node = element.closest("dialog,[role='dialog'],[role='alertdialog']");
113
+ if (!node) return null;
114
+ const role = node.getAttribute("role") || (node.tagName === "DIALOG" ? "dialog" : "dialog");
115
+ return { role, name: labelFor(node) || "dialog" };
116
+ }
117
+
118
+ type Fiber = { return?: Fiber | null; type?: unknown; memoizedProps?: unknown };
119
+
120
+ function fiberOf(element: Element) {
121
+ const key = Object.keys(element).find(
122
+ (name) => name.startsWith("__reactFiber$") || name.startsWith("__reactInternalInstance$"),
123
+ );
124
+ return key ? (element as unknown as Record<string, Fiber | undefined>)[key] : undefined;
125
+ }
126
+
127
+ function fiberNames(element: Element) {
128
+ const names: string[] = [];
129
+ let fiber = fiberOf(element);
130
+ for (let guard = 0; fiber && guard < 40; guard++) {
131
+ const type = fiber.type;
132
+ if (typeof type === "function") {
133
+ const fn = type as { displayName?: string; name?: string };
134
+ const name = fn.displayName || fn.name;
135
+ if (name && !name.startsWith("_")) names.push(name);
136
+ }
137
+ fiber = fiber.return ?? undefined;
138
+ }
139
+ return names.reverse();
140
+ }
141
+
142
+ /** Stamps along React's tree, nearest first. Unlike the DOM, this path runs through portals. */
143
+ function fiberStamps(element: Element) {
144
+ const stamps: Array<{ name: string; file: string | null }> = [];
145
+ let fiber = fiberOf(element);
146
+ for (let guard = 0; fiber && guard < 2000; guard++) {
147
+ // allow-banned-record-unknown: React fiber props are an open bag; only two data-cs-* keys are read
148
+ const props = fiber.memoizedProps as Record<string, unknown> | null | undefined;
149
+ const name = props && typeof props === "object" ? props["data-cs-cmp"] : undefined;
150
+ if (typeof name === "string" && name) {
151
+ const file = props?.["data-cs-file"];
152
+ stamps.push({ name, file: typeof file === "string" ? file : null });
153
+ }
154
+ fiber = fiber.return ?? undefined;
155
+ }
156
+ return stamps;
157
+ }
158
+
159
+ /** The element's ancestor that sits directly under <body>: a portal root for popups and modals. */
160
+ function bodyChild(element: Element) {
161
+ let current: Element | null = element;
162
+ while (current?.parentElement && current.parentElement !== current.ownerDocument.body)
163
+ current = current.parentElement;
164
+ return current?.parentElement ? current : null;
165
+ }
166
+
167
+ function obviouslyHidden(element: Element) {
168
+ const dialog = element.closest("dialog");
169
+ if (dialog instanceof HTMLDialogElement && !dialog.open) return true;
170
+ if (element.closest("[hidden]")) return true;
171
+ let current: Element | null = element;
172
+ while (current) {
173
+ if (current.getAttribute("aria-hidden") === "true") return true;
174
+ current = current.parentElement;
175
+ }
176
+ return false;
177
+ }
178
+
179
+ type Owned = {
180
+ component: { name: string; file: string | null } | null;
181
+ stack: string[];
182
+ componentRoot: boolean;
183
+ owner: Element | null;
184
+ };
185
+
186
+ function ownership(element: Element): Owned {
187
+ const stack: string[] = [];
188
+ let owner: Element | null = null;
189
+ let current: Element | null = element;
190
+ while (current) {
191
+ const name = current.getAttribute("data-cs-cmp");
192
+ if (name) {
193
+ if (!owner) owner = current;
194
+ stack.push(name);
195
+ }
196
+ current = current.parentElement;
197
+ }
198
+ const top = bodyChild(element);
199
+ if (top && top !== element && (!owner || !top.contains(owner))) {
200
+ const stamps = fiberStamps(element);
201
+ if (stamps.length && (!owner || stamps[0].name !== owner.getAttribute("data-cs-cmp"))) {
202
+ return {
203
+ component: stamps[0],
204
+ stack: stamps.map((stamp) => stamp.name).reverse(),
205
+ componentRoot: false,
206
+ owner: top,
207
+ };
208
+ }
209
+ }
210
+ if (owner) {
211
+ return {
212
+ component: {
213
+ name: owner.getAttribute("data-cs-cmp") || "component",
214
+ file: owner.getAttribute("data-cs-file"),
215
+ },
216
+ stack: stack.reverse(),
217
+ componentRoot: owner === element,
218
+ owner,
219
+ };
220
+ }
221
+ const names = fiberNames(element);
222
+ const name = names.at(-1) ?? null;
223
+ return {
224
+ component: name ? { name, file: null } : null,
225
+ stack: names,
226
+ componentRoot: false,
227
+ owner: null,
228
+ };
229
+ }
230
+
231
+ function once<T>(compute: () => T): () => T {
232
+ let done = false;
233
+ let value: T;
234
+ return () => {
235
+ if (!done) {
236
+ value = compute();
237
+ done = true;
238
+ }
239
+ return value;
240
+ };
241
+ }
242
+
243
+ /** Each field is computed when first read, so a resolve only pays for what the match looks at. */
244
+ export function candidateFromElement(
245
+ element: Element,
246
+ route: string,
247
+ key: string,
248
+ owned = ownership(element),
249
+ pathOf: (element: Element) => string = domPath,
250
+ ): Candidate {
251
+ const label = once(() => labelFor(element));
252
+ const visible = once(() => !obviouslyHidden(element));
253
+ const path = once(() => pathOf(element));
254
+ const overlay = once(() => overlayFor(element));
255
+ const role = once(() => roleFor(element));
256
+ const intra = once(() => intraPath(element, owned.owner));
257
+ const explicit = once(() =>
258
+ EXPLICIT.flatMap((attr) => {
259
+ const value = element.getAttribute(attr);
260
+ return value && usableExplicit(attr, value) ? [{ attr, value }] : [];
261
+ }),
262
+ );
263
+ return {
264
+ key,
265
+ route,
266
+ get visible() {
267
+ return visible();
268
+ },
269
+ get path() {
270
+ return path();
271
+ },
272
+ componentRoot: owned.componentRoot,
273
+ get overlay() {
274
+ return overlay();
275
+ },
276
+ get explicit() {
277
+ return explicit();
278
+ },
279
+ component: owned.component,
280
+ get stack() {
281
+ return owned.stack;
282
+ },
283
+ get role() {
284
+ return role();
285
+ },
286
+ get accName() {
287
+ return label();
288
+ },
289
+ get text() {
290
+ return label();
291
+ },
292
+ tag: element.tagName.toLowerCase(),
293
+ get intra() {
294
+ return intra();
295
+ },
296
+ };
297
+ }
298
+
299
+ export function captureAnchor(element: Element, route = window.location.pathname): Anchor {
300
+ const candidate = candidateFromElement(element, route, "target");
301
+ return {
302
+ v: 1,
303
+ route,
304
+ overlay: candidate.overlay,
305
+ explicit: candidate.explicit,
306
+ component: candidate.component,
307
+ stack: candidate.stack,
308
+ role: candidate.role,
309
+ accName: candidate.accName,
310
+ text: candidate.text,
311
+ tag: candidate.tag,
312
+ intra: candidate.intra,
313
+ };
314
+ }
315
+
316
+ export type Resolved = { resolution: Resolution; element: Element | null };
317
+
318
+ /**
319
+ * Resolves anchors against the page as it is now, building candidates only for the elements that can decide the
320
+ * result: those carrying one of the anchor's explicit ids, those owned by a component of the anchor's name, and,
321
+ * when the label changed, those inside that component's root. Never walks the whole page for a stamped anchor.
322
+ * Reuse one resolver for anchors resolved together; make a new one once the page may have changed.
323
+ */
324
+ export function createResolver(route = window.location.pathname) {
325
+ const body = document.body;
326
+ const toolbar = document.getElementById("cs-root");
327
+ const owners = new Map<Element, Owned>();
328
+ /** Elements reached by walking down from their stamped owner, which is therefore their nearest stamp. */
329
+ const walkedFrom = new Map<Element, Element>();
330
+ const candidates = new Map<Element, Candidate>();
331
+ const elements = new Map<string, Element>();
332
+ const pools = new Map<string, Element[]>();
333
+ /** Index paths, shared across the elements of one resolve: a path is its parent's path plus one lookup. */
334
+ const paths = new Map<Element, string>();
335
+ /** Each child's index under its parent, read once per parent instead of once per child. */
336
+ const indexes = new Map<Element, Map<Element, number>>();
337
+ let unowned: Element[] | null = null;
338
+
339
+ const indexUnder = (parent: Element, child: Element) => {
340
+ let table = indexes.get(parent);
341
+ if (!table) {
342
+ table = new Map();
343
+ let index = 0;
344
+ for (const node of parent.children) table.set(node, index++);
345
+ indexes.set(parent, table);
346
+ }
347
+ return table.get(child) ?? -1;
348
+ };
349
+ /** Same result as `domPath`, built from the parent's cached path. */
350
+ const pathOf = (element: Element): string => {
351
+ const known = paths.get(element);
352
+ if (known !== undefined) return known;
353
+ const parent = element.parentElement;
354
+ let path = "";
355
+ if (element !== body && parent) {
356
+ const above = pathOf(parent);
357
+ const index = String(indexUnder(parent, element));
358
+ path = above ? `${above}/${index}` : index;
359
+ }
360
+ paths.set(element, path);
361
+ return path;
362
+ };
363
+
364
+ const counted = (element: Element) =>
365
+ !SKIPPED.has(element.tagName) && !(toolbar && toolbar.contains(element));
366
+ const ownerOf = (element: Element) => {
367
+ let owned = owners.get(element);
368
+ if (!owned) {
369
+ const stamped = walkedFrom.get(element);
370
+ owned = stamped
371
+ ? {
372
+ component: {
373
+ name: stamped.getAttribute("data-cs-cmp") || "component",
374
+ file: stamped.getAttribute("data-cs-file"),
375
+ },
376
+ componentRoot: stamped === element,
377
+ owner: stamped,
378
+ get stack() {
379
+ return ownership(element).stack;
380
+ },
381
+ }
382
+ : ownership(element);
383
+ owners.set(element, owned);
384
+ }
385
+ return owned;
386
+ };
387
+ const candidateOf = (element: Element) => {
388
+ let candidate = candidates.get(element);
389
+ if (!candidate) {
390
+ candidate = candidateFromElement(
391
+ element,
392
+ route,
393
+ `e${candidates.size}`,
394
+ ownerOf(element),
395
+ pathOf,
396
+ );
397
+ candidates.set(element, candidate);
398
+ elements.set(candidate.key, element);
399
+ }
400
+ return candidate;
401
+ };
402
+ const take = (into: Set<Element>, element: Element) => {
403
+ if (into.size < MAX_ELEMENTS && counted(element)) into.add(element);
404
+ };
405
+ /** `start` and its descendants, skipping the subtrees of other stamped components. */
406
+ const ownedBy = (start: Element, into: Set<Element>) => {
407
+ const walker = document.createTreeWalker(start, NodeFilter.SHOW_ELEMENT, {
408
+ acceptNode: (node) =>
409
+ node === toolbar || (node !== start && (node as Element).hasAttribute("data-cs-cmp"))
410
+ ? NodeFilter.FILTER_REJECT
411
+ : NodeFilter.FILTER_ACCEPT,
412
+ });
413
+ for (
414
+ let node: Node | null = start;
415
+ node && into.size < MAX_ELEMENTS;
416
+ node = walker.nextNode()
417
+ ) {
418
+ take(into, node as Element);
419
+ walkedFrom.set(node as Element, start);
420
+ }
421
+ };
422
+ /** Elements with no stamped ancestor inside <body>: wrappers and portaled content, credited through React's tree. */
423
+ const unownedElements = () => {
424
+ if (unowned) return unowned;
425
+ const found = new Set<Element>();
426
+ const walker = document.createTreeWalker(body, NodeFilter.SHOW_ELEMENT, {
427
+ acceptNode: (node) =>
428
+ (node as Element).hasAttribute("data-cs-cmp") || node === toolbar
429
+ ? NodeFilter.FILTER_REJECT
430
+ : NodeFilter.FILTER_ACCEPT,
431
+ });
432
+ for (let node = walker.nextNode(); node && found.size < MAX_ELEMENTS; node = walker.nextNode())
433
+ take(found, node as Element);
434
+ unowned = [...found];
435
+ return unowned;
436
+ };
437
+ const explicitHits = (anchor: Anchor, into: Set<Element>) => {
438
+ for (const { attr, value } of anchor.explicit) {
439
+ if (!EXPLICIT.includes(attr) || !usableExplicit(attr, value)) continue;
440
+ for (const element of body.querySelectorAll(`[${attr}="${CSS.escape(value)}"]`))
441
+ take(into, element);
442
+ }
443
+ };
444
+ /** Every element owned by a component named `name`, found once per resolver. */
445
+ const ownedByName = (name: string) => {
446
+ let pool = pools.get(name);
447
+ if (!pool) {
448
+ const found = new Set<Element>();
449
+ for (const root of body.querySelectorAll(`[data-cs-cmp="${CSS.escape(name)}"]`))
450
+ ownedBy(root, found);
451
+ for (const element of unownedElements()) {
452
+ if (ownerOf(element).component?.name === name) take(found, element);
453
+ }
454
+ pool = [...found];
455
+ pools.set(name, pool);
456
+ }
457
+ return pool;
458
+ };
459
+
460
+ return {
461
+ resolve(anchor: Anchor): Resolved {
462
+ const pool = new Set<Element>();
463
+ explicitHits(anchor, pool);
464
+ for (const element of anchor.component?.name ? ownedByName(anchor.component.name) : []) {
465
+ if (pool.size >= MAX_ELEMENTS) break;
466
+ pool.add(element);
467
+ }
468
+ const inside = (root: Candidate) => {
469
+ const start = elements.get(root.key);
470
+ if (!start) return [];
471
+ const found = new Set<Element>([start]);
472
+ for (const element of start.querySelectorAll("*")) {
473
+ if (found.size >= MAX_ELEMENTS) break;
474
+ if (!anchor.role || roleFor(element) === anchor.role) take(found, element);
475
+ }
476
+ return [...found].map(candidateOf);
477
+ };
478
+ const resolution = resolveAnchor(anchor, [...pool].map(candidateOf), inside);
479
+ return {
480
+ resolution,
481
+ element: resolution.key ? (elements.get(resolution.key) ?? null) : null,
482
+ };
483
+ },
484
+ };
485
+ }
486
+
487
+ export function resolveInDocument(anchor: Anchor, route = window.location.pathname): Resolved {
488
+ return createResolver(route).resolve(anchor);
489
+ }
490
+
491
+ export function elementAt(x: number, y: number) {
492
+ const root = document.getElementById("cs-root");
493
+ for (const element of document.elementsFromPoint(x, y)) {
494
+ if (element === document.body || element === document.documentElement) continue;
495
+ if (root && (element === root || root.contains(element))) continue;
496
+ const tag = element.tagName;
497
+ if (tag === "SCRIPT" || tag === "STYLE") continue;
498
+ return element;
499
+ }
500
+ return null;
501
+ }
502
+
503
+ export function describeElement(element: Element) {
504
+ const name = ownership(element).component?.name;
505
+ const label = labelFor(element);
506
+ if (name && label) return `${name} — ${label}`;
507
+ return name || label || element.tagName.toLowerCase();
508
+ }