@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 +152 -34
- package/dist/capture.cjs.map +1 -1
- package/dist/capture.js +8 -13
- package/dist/capture.js.map +1 -1
- package/dist/chunk-GUH2PQJ7.js +462 -0
- package/dist/chunk-GUH2PQJ7.js.map +1 -0
- package/dist/index.cjs +219 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +52 -7
- package/dist/index.d.ts +52 -7
- package/dist/index.js +35 -15
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
111
|
-
const all = safeQueryAll(
|
|
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,
|
|
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 (
|
|
194
|
+
if (uniqueWithin(root, candidate, el)) return candidate;
|
|
138
195
|
cur = parent;
|
|
139
196
|
}
|
|
140
197
|
const full = segments.join(" > ");
|
|
141
|
-
return
|
|
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
|
|
163
|
-
if (isStableId(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 (
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
194
|
-
if (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
|
|
325
|
+
const target = componentAnchor(el);
|
|
326
|
+
const r = visualRect(target);
|
|
203
327
|
return {
|
|
204
|
-
selectors: generateSelectors(
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1290
|
+
const el = composedTarget(e);
|
|
1173
1291
|
if (el && !isGuidelyElement(el)) typedAt = performance.now();
|
|
1174
1292
|
};
|
|
1175
1293
|
const expand = (open) => {
|