@guidelytech/guide-runtime 0.9.0 → 0.11.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
 
@@ -622,6 +746,9 @@ function screenshotDataUrl(canvas) {
622
746
  }
623
747
  }
624
748
 
749
+ // src/host.ts
750
+ var import_runtime3 = require("@guidelytech/shared/runtime");
751
+
625
752
  // src/styles.ts
626
753
  var STYLES = `
627
754
  :host{all:initial;position:static;display:block;contain:none}
@@ -805,6 +932,21 @@ var STYLES = `
805
932
  .g-auth-outline.g-picked .g-auth-label{background:#16a34a}
806
933
  .g-highlight{position:fixed;z-index:calc(var(--g-z) + 8);pointer-events:none;border:2px dashed #f59e0b;background:rgba(245,158,11,.12);border-radius:4px}
807
934
  @media (max-width:480px){.g-card.g-modal,.g-panel{width:calc(100vw - 32px)}.g-inner{padding:16px}}
935
+ /* resource center: unread badge + "What's new" */
936
+ .g-launcher-badge{position:absolute;top:-4px;right:-4px;min-width:20px;height:20px;padding:0 6px;border-radius:999px;background:#dc2626;color:#fff;font-size:11px;font-weight:700;line-height:20px;text-align:center;box-shadow:0 0 0 2px var(--g-bg)}
937
+ .g-rc-news{padding:10px 20px 12px;border-bottom:1px solid var(--g-border)}
938
+ .g-rc-news:last-of-type{border-bottom:0}
939
+ .g-rc-news-head{display:flex;align-items:baseline;gap:8px}
940
+ .g-rc-news-title{margin:0;font-size:14px;font-weight:600;line-height:1.35;flex:1}
941
+ .g-rc-news-new{flex:none;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--g-primary-text);background:var(--g-primary);border-radius:999px;padding:2px 7px}
942
+ .g-rc-news-date{display:block;font-size:12px;color:var(--g-muted);margin:2px 0 6px}
943
+ .g-rc-news-body{font-size:13px}
944
+ .g-rc-news-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}
945
+ .g-rc-news-actions .g-btn{min-height:32px;padding:6px 12px;font-size:13px}
946
+ /* accessibility */
947
+ .g-sr-only{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
948
+ .g-hotspot:focus-visible,.g-custom:focus-visible,.g-card:focus-visible{outline:2px solid var(--g-primary);outline-offset:2px}
949
+ @media (forced-colors:active){.g-card,.g-panel,.g-banner{border:1px solid CanvasText}.g-btn{border:1px solid ButtonText}}
808
950
  `;
809
951
 
810
952
  // src/host.ts
@@ -893,6 +1035,7 @@ function resolveTheme(theme, appearance) {
893
1035
  }
894
1036
  function applyTheme(el, vars) {
895
1037
  el.style.setProperty("--g-primary", vars.primary);
1038
+ if (/^#[0-9a-f]{3}([0-9a-f]{3})?$/i.test(vars.primary)) el.style.setProperty("--g-primary-text", (0, import_runtime3.readableTextOn)(vars.primary));
896
1039
  el.style.setProperty("--g-bg", vars.bg);
897
1040
  el.style.setProperty("--g-text", vars.text);
898
1041
  el.style.setProperty("--g-radius", `${vars.radius}px`);
@@ -909,6 +1052,16 @@ function createScope(theme, appearance, doc = document) {
909
1052
  const vars = resolveTheme(theme, appearance);
910
1053
  applyTheme(el, vars);
911
1054
  handle.shadow.appendChild(el);
1055
+ const css = [appearance ? theme?.customCss : void 0, appearance?.customCss].filter((c) => !!c && !!c.trim());
1056
+ let style = null;
1057
+ if (css.length) {
1058
+ const id = `c${++cssScopeSeq}`;
1059
+ el.setAttribute("data-g-css", id);
1060
+ style = doc.createElement("style");
1061
+ style.setAttribute("data-g-custom", id);
1062
+ style.textContent = css.map((c) => (0, import_runtime3.scopeCustomCss)(c, `.g-scope[data-g-css="${id}"]`)).join("\n");
1063
+ handle.shadow.insertBefore(style, el);
1064
+ }
912
1065
  let mq = null;
913
1066
  const onChange = () => applyTheme(el, resolveTheme(theme, appearance));
914
1067
  if ((appearance?.theme ?? "auto") === "auto" && typeof window !== "undefined" && typeof window.matchMedia === "function") {
@@ -924,10 +1077,12 @@ function createScope(theme, appearance, doc = document) {
924
1077
  vars,
925
1078
  destroy() {
926
1079
  mq?.removeEventListener?.("change", onChange);
1080
+ style?.remove();
927
1081
  el.remove();
928
1082
  }
929
1083
  };
930
1084
  }
1085
+ var cssScopeSeq = 0;
931
1086
 
932
1087
  // src/capture.ts
933
1088
  var DOUBLE_CLICK_MS = 400;
@@ -948,12 +1103,6 @@ function button(doc, text, cls, attr) {
948
1103
  b.setAttribute(attr, "");
949
1104
  return b;
950
1105
  }
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
1106
  function deepActiveElementIn(el) {
958
1107
  const root = el.getRootNode();
959
1108
  return root.activeElement ?? null;
@@ -1097,7 +1246,7 @@ function startCapture(options) {
1097
1246
  };
1098
1247
  const onPointerDown = (e) => {
1099
1248
  if (!recording() || e.button !== 0) return;
1100
- const target = eventTarget(e);
1249
+ const target = composedTarget(e);
1101
1250
  if (!target || isGuidelyElement(target)) return;
1102
1251
  const active = deepActiveElement(doc);
1103
1252
  if (active && active !== target && !active.contains(target) && isTextEntry(active)) flushField(active);
@@ -1121,7 +1270,7 @@ function startCapture(options) {
1121
1270
  };
1122
1271
  const onChange = (e) => {
1123
1272
  if (!recording()) return;
1124
- const el = eventTarget(e);
1273
+ const el = composedTarget(e);
1125
1274
  if (!el || isGuidelyElement(el)) return;
1126
1275
  if (el.tagName === "SELECT") {
1127
1276
  const sensitive = isSensitiveField(el, mask);
@@ -1138,19 +1287,19 @@ function startCapture(options) {
1138
1287
  if (isTextEntry(el)) flushField(el);
1139
1288
  };
1140
1289
  const onFocusIn = (e) => {
1141
- const el = eventTarget(e);
1290
+ const el = composedTarget(e);
1142
1291
  if (el && !isGuidelyElement(el) && isTextEntry(el)) {
1143
1292
  const host = el.tagName === "INPUT" || el.tagName === "TEXTAREA" ? el : actionTarget(el);
1144
1293
  baseline.set(host, fieldValue(host));
1145
1294
  }
1146
1295
  };
1147
1296
  const onFocusOut = (e) => {
1148
- const el = eventTarget(e);
1297
+ const el = composedTarget(e);
1149
1298
  if (el && el.tagName !== "INPUT" && el.tagName !== "TEXTAREA" && isTextEntry(el)) flushField(el);
1150
1299
  };
1151
1300
  const onKeyDown = (e) => {
1152
1301
  if (e.key !== "Enter" || e.isComposing) return;
1153
- const el = eventTarget(e);
1302
+ const el = composedTarget(e);
1154
1303
  if (el && (el.tagName === "INPUT" || (e.metaKey || e.ctrlKey) && isTextEntry(el))) flushField(el);
1155
1304
  };
1156
1305
  const checkUrl = () => {
@@ -1169,7 +1318,7 @@ function startCapture(options) {
1169
1318
  movedAt = performance.now();
1170
1319
  };
1171
1320
  const onInput = (e) => {
1172
- const el = eventTarget(e);
1321
+ const el = composedTarget(e);
1173
1322
  if (el && !isGuidelyElement(el)) typedAt = performance.now();
1174
1323
  };
1175
1324
  const expand = (open) => {