@guidelytech/guide-runtime 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/capture.cjs CHANGED
@@ -76,6 +76,12 @@ var CAPTURE_STYLES = `
76
76
  // src/selectors.ts
77
77
  var import_runtime = require("@guidelytech/shared/runtime");
78
78
  var HOST_ID = "guidely-root";
79
+ function hasLayout(doc) {
80
+ const de = doc.documentElement;
81
+ if (!de) return false;
82
+ const r = de.getBoundingClientRect();
83
+ return r.width > 0 || r.height > 0;
84
+ }
79
85
  function normalizeText(text) {
80
86
  return (text || "").replace(/\s+/g, " ").trim();
81
87
  }
@@ -92,6 +98,56 @@ function safeQueryAll(root, selector) {
92
98
  return [];
93
99
  }
94
100
  }
101
+ var SHADOW_SEPARATOR = " >>> ";
102
+ var SHADOW_SPLIT = /\s*>>>\s*/;
103
+ function isShadowRoot(node) {
104
+ return !!node && node.nodeType === 11 && !!node.host;
105
+ }
106
+ function shadowRootOf(el) {
107
+ const root = el.getRootNode?.();
108
+ return isShadowRoot(root) ? root : null;
109
+ }
110
+ function composedTarget(e) {
111
+ const path = typeof e.composedPath === "function" ? e.composedPath() : [];
112
+ const first = path[0] ?? e.target;
113
+ if (!first) return null;
114
+ return first.nodeType === 1 ? first : first.parentElement;
115
+ }
116
+ var shadowRootCache = null;
117
+ function shadowRootsIn(root) {
118
+ const cached = shadowRootCache?.get(root);
119
+ if (cached) return cached;
120
+ const out = [];
121
+ const visit = (scope) => {
122
+ const own = scope.nodeType === 1 ? scope.shadowRoot : null;
123
+ const hosts = own ? [scope] : [];
124
+ for (const el of safeQueryAll(scope, "*")) if (el.shadowRoot) hosts.push(el);
125
+ for (const host of hosts) {
126
+ if (host.id === HOST_ID || !host.shadowRoot || out.includes(host.shadowRoot)) continue;
127
+ out.push(host.shadowRoot);
128
+ visit(host.shadowRoot);
129
+ }
130
+ };
131
+ visit(root);
132
+ shadowRootCache?.set(root, out);
133
+ return out;
134
+ }
135
+ function queryAllDeep(root, selector) {
136
+ const out = safeQueryAll(root, selector);
137
+ for (const sr of shadowRootsIn(root)) out.push(...safeQueryAll(sr, selector));
138
+ return out;
139
+ }
140
+ function queryCss(root, selector, deep) {
141
+ if (!selector.includes(">>>")) return deep ? queryAllDeep(root, selector) : safeQueryAll(root, selector);
142
+ const steps = selector.split(SHADOW_SPLIT).filter(Boolean);
143
+ let scopes = [root];
144
+ let matches = [];
145
+ steps.forEach((step, i) => {
146
+ matches = Array.from(new Set(scopes.flatMap((scope) => queryAllDeep(scope, step))));
147
+ if (i < steps.length - 1) scopes = matches.map((el) => el.shadowRoot).filter((sr) => !!sr);
148
+ });
149
+ return matches;
150
+ }
95
151
  function escapeCss(value) {
96
152
  const g = globalThis;
97
153
  if (g.CSS && typeof g.CSS.escape === "function") return g.CSS.escape(value);
@@ -107,14 +163,15 @@ function isStableId(id) {
107
163
  function isStableClass(cls) {
108
164
  return !!cls && !GENERATED_CLASS.test(cls) && cls.length <= 40;
109
165
  }
110
- function uniqueIn(doc, selector, el) {
111
- const all = safeQueryAll(doc, selector);
166
+ function uniqueWithin(root, selector, el) {
167
+ const all = safeQueryAll(root, selector);
112
168
  return all.length === 1 && all[0] === el;
113
169
  }
114
170
  function attrSelector(name, value) {
115
171
  return `[${name}='${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}']`;
116
172
  }
117
- function cssPath(el, doc) {
173
+ function cssPath(el, root) {
174
+ const doc = el.ownerDocument;
118
175
  const segments = [];
119
176
  let cur = el;
120
177
  while (cur && cur.nodeType === 1 && cur !== doc.body && cur !== doc.documentElement) {
@@ -134,20 +191,48 @@ function cssPath(el, doc) {
134
191
  }
135
192
  segments.unshift(seg);
136
193
  const candidate = segments.join(" > ");
137
- if (uniqueIn(doc, candidate, el)) return candidate;
194
+ if (uniqueWithin(root, candidate, el)) return candidate;
138
195
  cur = parent;
139
196
  }
140
197
  const full = segments.join(" > ");
141
- return uniqueIn(doc, full, el) ? full : null;
198
+ return uniqueWithin(root, full, el) ? full : null;
199
+ }
200
+ function shadowHostChain(el) {
201
+ const steps = [];
202
+ for (let root = shadowRootOf(el); root; root = shadowRootOf(root.host)) {
203
+ const host = root.host;
204
+ const step = cssPath(host, shadowRootOf(host) ?? host.ownerDocument);
205
+ if (!step) return null;
206
+ steps.unshift(step);
207
+ }
208
+ return steps.join(SHADOW_SEPARATOR);
209
+ }
210
+ function selectorFor(el, selector) {
211
+ const root = shadowRootOf(el);
212
+ if (!root) return uniqueWithin(el.ownerDocument, selector, el) ? selector : null;
213
+ if (!uniqueWithin(root, selector, el)) return null;
214
+ const doc = el.ownerDocument;
215
+ const only = (sel) => {
216
+ const all = queryCss(doc, sel, true);
217
+ return all.length === 1 && all[0] === el;
218
+ };
219
+ if (only(selector)) return selector;
220
+ const chain = shadowHostChain(el);
221
+ const scoped = chain ? chain + SHADOW_SEPARATOR + selector : null;
222
+ return scoped && only(scoped) ? scoped : null;
223
+ }
224
+ function shadowText(el) {
225
+ if (!el.shadowRoot) return "";
226
+ const parts = Array.from(el.shadowRoot.children).filter((c) => !/^(STYLE|SCRIPT|TEMPLATE)$/.test(c.tagName));
227
+ return normalizeText(parts.map((c) => c.innerText ?? c.textContent).join(" "));
142
228
  }
143
229
  function textSnapshotOf(el) {
144
230
  const he = el;
145
- const own = normalizeText(he.innerText ?? el.textContent);
231
+ const own = normalizeText(he.innerText ?? el.textContent) || shadowText(el);
146
232
  const alt = normalizeText(el.getAttribute("aria-label") || el.getAttribute("title") || el.getAttribute("placeholder") || el.getAttribute("alt") || el.value);
147
233
  return (own || alt).slice(0, 200);
148
234
  }
149
235
  function generateSelectors(el) {
150
- const doc = el.ownerDocument;
151
236
  const out = [];
152
237
  const seen = /* @__PURE__ */ new Set();
153
238
  const push = (s) => {
@@ -158,9 +243,15 @@ function generateSelectors(el) {
158
243
  }
159
244
  };
160
245
  const tag = el.tagName.toLowerCase();
246
+ const pushFor = (kind, selector, value, label) => {
247
+ const resolved = selectorFor(el, selector);
248
+ if (resolved === selector) push({ kind, value, label });
249
+ else if (resolved) push({ kind: "css", value: resolved, label });
250
+ return !!resolved;
251
+ };
161
252
  const guidelyId = el.getAttribute("data-guidely-id");
162
- if (guidelyId && uniqueIn(doc, attrSelector("data-guidely-id", guidelyId), el)) push({ kind: "guidely-id", value: guidelyId, label: `Guidely id ${guidelyId}` });
163
- if (isStableId(el.id) && uniqueIn(doc, `#${escapeCss(el.id)}`, el)) push({ kind: "id", value: `#${el.id}`, label: `id ${el.id}` });
253
+ if (guidelyId) pushFor("guidely-id", attrSelector("data-guidely-id", guidelyId), guidelyId, `Guidely id ${guidelyId}`);
254
+ if (isStableId(el.id)) pushFor("id", `#${escapeCss(el.id)}`, `#${el.id}`, `id ${el.id}`);
164
255
  const dataAttrs = Array.from(el.attributes).filter((a) => a.name.startsWith("data-") && !IGNORED_DATA_ATTRS.test(a.name) && a.value && a.value.length <= 120 && !/\d{5,}/.test(a.value));
165
256
  dataAttrs.sort((a, b) => {
166
257
  const ia = PREFERRED_DATA_ATTRS.indexOf(a.name);
@@ -169,42 +260,75 @@ function generateSelectors(el) {
169
260
  });
170
261
  for (const a of dataAttrs) {
171
262
  const sel = attrSelector(a.name, a.value);
172
- if (uniqueIn(doc, sel, el)) push({ kind: "data-attr", value: sel, label: `${a.name}=${a.value}` });
173
- else if (uniqueIn(doc, tag + sel, el)) push({ kind: "data-attr", value: tag + sel, label: `${a.name}=${a.value}` });
263
+ if (!pushFor("data-attr", sel, sel, `${a.name}=${a.value}`)) pushFor("data-attr", tag + sel, tag + sel, `${a.name}=${a.value}`);
174
264
  }
175
265
  const aria = el.getAttribute("aria-label");
176
266
  if (aria && aria.length <= 120) {
177
267
  const sel = attrSelector("aria-label", aria);
178
- if (uniqueIn(doc, sel, el)) push({ kind: "aria", value: sel, label: aria });
179
- else if (uniqueIn(doc, tag + sel, el)) push({ kind: "aria", value: tag + sel, label: aria });
268
+ if (!pushFor("aria", sel, sel, aria)) pushFor("aria", tag + sel, tag + sel, aria);
180
269
  }
181
270
  const name = el.getAttribute("name");
182
- if (name && uniqueIn(doc, tag + attrSelector("name", name), el)) push({ kind: "css", value: tag + attrSelector("name", name), label: `${tag} name=${name}` });
271
+ if (name) pushFor("css", tag + attrSelector("name", name), tag + attrSelector("name", name), `${tag} name=${name}`);
183
272
  const role = el.getAttribute("role");
184
273
  if (role) {
185
274
  const title = el.getAttribute("title") || el.getAttribute("aria-labelledby");
186
275
  const sel = title ? `[role='${role}']${attrSelector(el.getAttribute("title") ? "title" : "aria-labelledby", title)}` : `[role='${role}']`;
187
- if (uniqueIn(doc, sel, el)) push({ kind: "css", value: sel, label: `role ${role}` });
276
+ pushFor("css", sel, sel, `role ${role}`);
188
277
  }
189
278
  if (tag === "a") {
190
279
  const href = el.getAttribute("href");
191
- if (href && href.length <= 200 && uniqueIn(doc, `a${attrSelector("href", href)}`, el)) push({ kind: "css", value: `a${attrSelector("href", href)}`, label: `link ${href}` });
280
+ if (href && href.length <= 200) pushFor("css", `a${attrSelector("href", href)}`, `a${attrSelector("href", href)}`, `link ${href}`);
192
281
  }
193
- const path = cssPath(el, doc);
194
- if (path) push({ kind: "css", value: path, label: "css path" });
282
+ const path = cssPath(el, shadowRootOf(el) ?? el.ownerDocument);
283
+ if (path) pushFor("css", path, path, "css path");
195
284
  if (/^(button|a|h[1-6]|label|summary)$/.test(tag) || role === "button" || role === "link" || role === "tab" || role === "menuitem") {
196
285
  const text = normalizeText(el.innerText ?? el.textContent);
197
286
  if (text && text.length <= 80) push({ kind: "text", value: text, label: `text "${text}"` });
198
287
  }
199
288
  return out.slice(0, 10);
200
289
  }
290
+ function visualRect(el) {
291
+ const own = el.getBoundingClientRect();
292
+ if (!el.shadowRoot) return own;
293
+ const boxes = [own, ...Array.from(el.shadowRoot.children).map((c) => c.getBoundingClientRect())].filter((r) => r.width > 0 && r.height > 0);
294
+ if (boxes.length < 2) return boxes[0] ?? own;
295
+ const left = Math.min(...boxes.map((r) => r.left));
296
+ const top = Math.min(...boxes.map((r) => r.top));
297
+ const right = Math.max(...boxes.map((r) => r.right));
298
+ const bottom = Math.max(...boxes.map((r) => r.bottom));
299
+ return new DOMRect(left, top, right - left, bottom - top);
300
+ }
301
+ var ANCHOR_MAX_AREA_RATIO = 4;
302
+ var CONTROL_SELECTOR = "button, a[href], input, select, textarea, [role='button'], [role='link'], [role='checkbox'], [role='radio'], [role='switch'], [role='tab'], [role='menuitem'], [role='combobox'], [role='option']";
303
+ function componentAnchor(el) {
304
+ if (!hasLayout(el.ownerDocument)) return el;
305
+ let cur = el;
306
+ for (let root = shadowRootOf(cur); root; root = shadowRootOf(cur)) {
307
+ const host = root.host;
308
+ if (host.id === HOST_ID) break;
309
+ let control = null;
310
+ try {
311
+ control = cur.closest(CONTROL_SELECTOR);
312
+ } catch {
313
+ }
314
+ const r = visualRect(control ?? cur);
315
+ const area = r.width * r.height;
316
+ const h = visualRect(host);
317
+ const hostArea = h.width * h.height;
318
+ const inside = r.left >= h.left - 1 && r.top >= h.top - 1 && r.right <= h.right + 1 && r.bottom <= h.bottom + 1;
319
+ if (!area || !inside || hostArea > area * ANCHOR_MAX_AREA_RATIO) break;
320
+ cur = host;
321
+ }
322
+ return cur;
323
+ }
201
324
  function describeElement(el) {
202
- const r = el.getBoundingClientRect();
325
+ const target = componentAnchor(el);
326
+ const r = visualRect(target);
203
327
  return {
204
- selectors: generateSelectors(el),
205
- textSnapshot: textSnapshotOf(el),
328
+ selectors: generateSelectors(target),
329
+ textSnapshot: textSnapshotOf(target) || textSnapshotOf(el),
206
330
  rect: { x: r.x, y: r.y, width: r.width, height: r.height },
207
- tag: el.tagName.toLowerCase()
331
+ tag: target.tagName.toLowerCase()
208
332
  };
209
333
  }
210
334
 
@@ -948,12 +1072,6 @@ function button(doc, text, cls, attr) {
948
1072
  b.setAttribute(attr, "");
949
1073
  return b;
950
1074
  }
951
- function eventTarget(e) {
952
- const path = typeof e.composedPath === "function" ? e.composedPath() : [];
953
- const first = path[0] ?? e.target;
954
- if (!first) return null;
955
- return first.nodeType === 1 ? first : first.parentElement;
956
- }
957
1075
  function deepActiveElementIn(el) {
958
1076
  const root = el.getRootNode();
959
1077
  return root.activeElement ?? null;
@@ -1097,7 +1215,7 @@ function startCapture(options) {
1097
1215
  };
1098
1216
  const onPointerDown = (e) => {
1099
1217
  if (!recording() || e.button !== 0) return;
1100
- const target = eventTarget(e);
1218
+ const target = composedTarget(e);
1101
1219
  if (!target || isGuidelyElement(target)) return;
1102
1220
  const active = deepActiveElement(doc);
1103
1221
  if (active && active !== target && !active.contains(target) && isTextEntry(active)) flushField(active);
@@ -1121,7 +1239,7 @@ function startCapture(options) {
1121
1239
  };
1122
1240
  const onChange = (e) => {
1123
1241
  if (!recording()) return;
1124
- const el = eventTarget(e);
1242
+ const el = composedTarget(e);
1125
1243
  if (!el || isGuidelyElement(el)) return;
1126
1244
  if (el.tagName === "SELECT") {
1127
1245
  const sensitive = isSensitiveField(el, mask);
@@ -1138,19 +1256,19 @@ function startCapture(options) {
1138
1256
  if (isTextEntry(el)) flushField(el);
1139
1257
  };
1140
1258
  const onFocusIn = (e) => {
1141
- const el = eventTarget(e);
1259
+ const el = composedTarget(e);
1142
1260
  if (el && !isGuidelyElement(el) && isTextEntry(el)) {
1143
1261
  const host = el.tagName === "INPUT" || el.tagName === "TEXTAREA" ? el : actionTarget(el);
1144
1262
  baseline.set(host, fieldValue(host));
1145
1263
  }
1146
1264
  };
1147
1265
  const onFocusOut = (e) => {
1148
- const el = eventTarget(e);
1266
+ const el = composedTarget(e);
1149
1267
  if (el && el.tagName !== "INPUT" && el.tagName !== "TEXTAREA" && isTextEntry(el)) flushField(el);
1150
1268
  };
1151
1269
  const onKeyDown = (e) => {
1152
1270
  if (e.key !== "Enter" || e.isComposing) return;
1153
- const el = eventTarget(e);
1271
+ const el = composedTarget(e);
1154
1272
  if (el && (el.tagName === "INPUT" || (e.metaKey || e.ctrlKey) && isTextEntry(el))) flushField(el);
1155
1273
  };
1156
1274
  const checkUrl = () => {
@@ -1169,7 +1287,7 @@ function startCapture(options) {
1169
1287
  movedAt = performance.now();
1170
1288
  };
1171
1289
  const onInput = (e) => {
1172
- const el = eventTarget(e);
1290
+ const el = composedTarget(e);
1173
1291
  if (el && !isGuidelyElement(el)) typedAt = performance.now();
1174
1292
  };
1175
1293
  const expand = (open) => {