@guidelytech/guide-runtime 0.8.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
 
@@ -780,7 +904,15 @@ var STYLES = `
780
904
  .g-rc-item .g-rc-title{font-weight:500}
781
905
  .g-rc-item .g-rc-sum{font-size:12px;color:var(--g-muted);margin-top:2px}
782
906
  .g-rc-item .g-rc-icon{flex:none;width:20px;text-align:center;color:var(--g-primary)}
907
+ .g-rc-row{display:flex;align-items:center;gap:6px;padding-right:16px}
908
+ .g-rc-row>.g-rc-item{flex:1;min-width:0;padding-right:4px}
909
+ .g-rc-row:hover{background:var(--g-subtle)}
910
+ .g-rc-row:hover>.g-rc-item{background:transparent}
911
+ .g-rc-showme{flex:none;min-height:32px;padding:4px 10px;font-size:12px;white-space:nowrap}
912
+ .g-help-showme{margin:4px 0 12px}
783
913
  .g-rc-article{padding:16px 20px}
914
+ .g-resource-center .g-panel{transition:width .2s ease,max-height .2s ease}
915
+ .g-resource-center .g-panel.g-panel-reading{width:min(720px,calc(100vw - 32px));max-height:calc(100vh - 100px)}
784
916
  .g-rc-back{margin-bottom:8px}
785
917
  .g-rc-empty{padding:24px 20px;color:var(--g-muted);text-align:center}
786
918
  .g-faq-a{padding:0 20px 12px 50px;font-size:13px;color:var(--g-muted)}
@@ -940,12 +1072,6 @@ function button(doc, text, cls, attr) {
940
1072
  b.setAttribute(attr, "");
941
1073
  return b;
942
1074
  }
943
- function eventTarget(e) {
944
- const path = typeof e.composedPath === "function" ? e.composedPath() : [];
945
- const first = path[0] ?? e.target;
946
- if (!first) return null;
947
- return first.nodeType === 1 ? first : first.parentElement;
948
- }
949
1075
  function deepActiveElementIn(el) {
950
1076
  const root = el.getRootNode();
951
1077
  return root.activeElement ?? null;
@@ -1089,7 +1215,7 @@ function startCapture(options) {
1089
1215
  };
1090
1216
  const onPointerDown = (e) => {
1091
1217
  if (!recording() || e.button !== 0) return;
1092
- const target = eventTarget(e);
1218
+ const target = composedTarget(e);
1093
1219
  if (!target || isGuidelyElement(target)) return;
1094
1220
  const active = deepActiveElement(doc);
1095
1221
  if (active && active !== target && !active.contains(target) && isTextEntry(active)) flushField(active);
@@ -1113,7 +1239,7 @@ function startCapture(options) {
1113
1239
  };
1114
1240
  const onChange = (e) => {
1115
1241
  if (!recording()) return;
1116
- const el = eventTarget(e);
1242
+ const el = composedTarget(e);
1117
1243
  if (!el || isGuidelyElement(el)) return;
1118
1244
  if (el.tagName === "SELECT") {
1119
1245
  const sensitive = isSensitiveField(el, mask);
@@ -1130,19 +1256,19 @@ function startCapture(options) {
1130
1256
  if (isTextEntry(el)) flushField(el);
1131
1257
  };
1132
1258
  const onFocusIn = (e) => {
1133
- const el = eventTarget(e);
1259
+ const el = composedTarget(e);
1134
1260
  if (el && !isGuidelyElement(el) && isTextEntry(el)) {
1135
1261
  const host = el.tagName === "INPUT" || el.tagName === "TEXTAREA" ? el : actionTarget(el);
1136
1262
  baseline.set(host, fieldValue(host));
1137
1263
  }
1138
1264
  };
1139
1265
  const onFocusOut = (e) => {
1140
- const el = eventTarget(e);
1266
+ const el = composedTarget(e);
1141
1267
  if (el && el.tagName !== "INPUT" && el.tagName !== "TEXTAREA" && isTextEntry(el)) flushField(el);
1142
1268
  };
1143
1269
  const onKeyDown = (e) => {
1144
1270
  if (e.key !== "Enter" || e.isComposing) return;
1145
- const el = eventTarget(e);
1271
+ const el = composedTarget(e);
1146
1272
  if (el && (el.tagName === "INPUT" || (e.metaKey || e.ctrlKey) && isTextEntry(el))) flushField(el);
1147
1273
  };
1148
1274
  const checkUrl = () => {
@@ -1161,7 +1287,7 @@ function startCapture(options) {
1161
1287
  movedAt = performance.now();
1162
1288
  };
1163
1289
  const onInput = (e) => {
1164
- const el = eventTarget(e);
1290
+ const el = composedTarget(e);
1165
1291
  if (el && !isGuidelyElement(el)) typedAt = performance.now();
1166
1292
  };
1167
1293
  const expand = (open) => {