@guidelytech/guide-runtime 0.7.0 → 0.9.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.
@@ -0,0 +1,301 @@
1
+ import {
2
+ __publicField
3
+ } from "./chunk-PG7CMBHD.js";
4
+
5
+ // src/selectors.ts
6
+ import { matchUrlPattern } from "@guidelytech/shared/runtime";
7
+ function urlMatches(pattern, url) {
8
+ if (!pattern) return true;
9
+ if (matchUrlPattern(pattern, url)) return true;
10
+ try {
11
+ const u = new URL(url, "http://localhost");
12
+ if (!pattern.includes("?") && !pattern.includes("#")) return matchUrlPattern(pattern, u.pathname) || matchUrlPattern(pattern, u.origin + u.pathname);
13
+ } catch {
14
+ }
15
+ return false;
16
+ }
17
+ var TEXT_CANDIDATES = "button, a, [role='button'], [role='link'], [role='tab'], [role='menuitem'], h1, h2, h3, h4, h5, h6, label, summary";
18
+ var HOST_ID = "guidely-root";
19
+ function docOf(root) {
20
+ return root.nodeType === 9 ? root : root.ownerDocument;
21
+ }
22
+ function hasLayout(doc) {
23
+ const de = doc.documentElement;
24
+ if (!de) return false;
25
+ const r = de.getBoundingClientRect();
26
+ return r.width > 0 || r.height > 0;
27
+ }
28
+ function normalizeText(text) {
29
+ return (text || "").replace(/\s+/g, " ").trim();
30
+ }
31
+ function isVisible(el) {
32
+ if (!el.isConnected) return false;
33
+ const doc = el.ownerDocument;
34
+ const win = doc.defaultView;
35
+ let cur = el;
36
+ while (cur && cur !== doc.documentElement) {
37
+ if (cur.hidden) return false;
38
+ if (win) {
39
+ const cs = win.getComputedStyle(cur);
40
+ if (cs.display === "none" || cs.visibility === "hidden") return false;
41
+ }
42
+ cur = cur.parentElement ?? cur.getRootNode().host ?? null;
43
+ }
44
+ if (hasLayout(doc)) {
45
+ const r = el.getBoundingClientRect();
46
+ if (r.width === 0 && r.height === 0) return false;
47
+ }
48
+ return true;
49
+ }
50
+ function isGuidelyElement(el) {
51
+ if (!el) return false;
52
+ const root = el.getRootNode();
53
+ if (root instanceof ShadowRoot && root.host.id === HOST_ID) return true;
54
+ return el.id === HOST_ID || !!el.closest?.(`#${HOST_ID}`);
55
+ }
56
+ function safeQueryAll(root, selector) {
57
+ try {
58
+ return Array.from(root.querySelectorAll(selector));
59
+ } catch {
60
+ return [];
61
+ }
62
+ }
63
+ function escapeCss(value) {
64
+ const g = globalThis;
65
+ if (g.CSS && typeof g.CSS.escape === "function") return g.CSS.escape(value);
66
+ return value.replace(/["\\]/g, "\\$&");
67
+ }
68
+ function queryXPath(root, expr) {
69
+ const doc = docOf(root);
70
+ const d = doc;
71
+ if (typeof d.evaluate !== "function") return [];
72
+ try {
73
+ const result = d.evaluate(expr, root, null, 7, null);
74
+ const out = [];
75
+ for (let i = 0; i < result.snapshotLength; i++) {
76
+ const n = result.snapshotItem(i);
77
+ if (n && n.nodeType === 1) out.push(n);
78
+ }
79
+ return out;
80
+ } catch {
81
+ return [];
82
+ }
83
+ }
84
+ function queryText(root, text) {
85
+ const needle = normalizeText(text).toLowerCase();
86
+ if (!needle) return [];
87
+ const candidates = safeQueryAll(root, TEXT_CANDIDATES);
88
+ const exact = candidates.filter((c) => normalizeText(c.textContent).toLowerCase() === needle);
89
+ if (exact.length) return exact;
90
+ return candidates.filter((c) => normalizeText(c.textContent).toLowerCase().includes(needle));
91
+ }
92
+ function querySelectorStrategy(strategy, root) {
93
+ const value = strategy.value.trim();
94
+ if (!value) return [];
95
+ switch (strategy.kind) {
96
+ case "id": {
97
+ const id = value.startsWith("#") ? value.slice(1) : value;
98
+ return safeQueryAll(root, `#${escapeCss(id)}`);
99
+ }
100
+ case "guidely-id": {
101
+ const id = value.startsWith("#") ? value.slice(1) : value;
102
+ return safeQueryAll(root, attrSelector("data-guidely-id", id));
103
+ }
104
+ case "data-attr":
105
+ case "aria":
106
+ case "css":
107
+ return safeQueryAll(root, value.startsWith("[") || /^[a-z*#.[]/i.test(value) ? value : `[${value}]`);
108
+ case "text":
109
+ return queryText(root, value);
110
+ case "xpath":
111
+ return queryXPath(root, value);
112
+ default:
113
+ return [];
114
+ }
115
+ }
116
+ function resolveTarget(target, root = document) {
117
+ const selectors = Array.isArray(target) ? target : target.selectors;
118
+ for (const strategy of selectors) {
119
+ const matches = querySelectorStrategy(strategy, root).filter((el) => !isGuidelyElement(el) && isVisible(el));
120
+ if (matches.length === 1) return matches[0];
121
+ }
122
+ return null;
123
+ }
124
+ var TargetTimeoutError = class extends Error {
125
+ constructor(message = "Target element not found before timeout") {
126
+ super(message);
127
+ __publicField(this, "name", "TargetTimeoutError");
128
+ }
129
+ };
130
+ function waitForTarget(target, options = {}) {
131
+ const timeoutMs = options.timeoutMs ?? 5e3;
132
+ const root = options.root ?? document;
133
+ const pollMs = Math.min(250, Math.max(16, options.pollMs ?? 250));
134
+ return new Promise((resolve, reject) => {
135
+ const immediate = resolveTarget(target, root);
136
+ if (immediate) return resolve(immediate);
137
+ if (options.signal?.aborted) return reject(new DOMException("Aborted", "AbortError"));
138
+ let done = false;
139
+ let observer = null;
140
+ let timer = null;
141
+ let timeout = null;
142
+ const cleanup = () => {
143
+ done = true;
144
+ observer?.disconnect();
145
+ if (timer) clearInterval(timer);
146
+ if (timeout) clearTimeout(timeout);
147
+ options.signal?.removeEventListener("abort", onAbort);
148
+ };
149
+ const check = () => {
150
+ if (done) return;
151
+ const el = resolveTarget(target, root);
152
+ if (el) {
153
+ cleanup();
154
+ resolve(el);
155
+ }
156
+ };
157
+ const onAbort = () => {
158
+ cleanup();
159
+ reject(new DOMException("Aborted", "AbortError"));
160
+ };
161
+ options.signal?.addEventListener("abort", onAbort);
162
+ const observeRoot = root.nodeType === 9 ? root.documentElement : root;
163
+ if (typeof MutationObserver !== "undefined" && observeRoot) {
164
+ observer = new MutationObserver(check);
165
+ observer.observe(observeRoot, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "style", "hidden", "id", "data-tour", "data-guidely-id", "aria-label"] });
166
+ }
167
+ timer = setInterval(check, pollMs);
168
+ if (timeoutMs > 0) {
169
+ timeout = setTimeout(() => {
170
+ cleanup();
171
+ reject(new TargetTimeoutError());
172
+ }, timeoutMs);
173
+ }
174
+ });
175
+ }
176
+ var GENERATED_ID = /(\d{4,}|:r[0-9a-z]+:|^radix-|^headlessui-|^mui-|^ember\d|^react-|^\w*-\d+$|__|^[a-f0-9]{8,}$|^svelte-)/i;
177
+ var GENERATED_CLASS = /(^css-|^sc-|^jss\d|^svelte-|^emotion-|^chakra-|^Mui|^_|[0-9a-f]{6,}|^\w{1,2}$|^[a-z]+-\d+$|[:[\]/])/;
178
+ var PREFERRED_DATA_ATTRS = ["data-testid", "data-tour", "data-test", "data-test-id", "data-cy", "data-qa", "data-id", "data-name", "data-guidely"];
179
+ var IGNORED_DATA_ATTRS = /^data-(guidely-id|state|v-|reactid|react|radix|headlessui|svelte|hydrated|orientation|highlighted|focus|key|index|rbd|dnd|rk-|sveltekit)/i;
180
+ function isStableId(id) {
181
+ return !!id && /^[A-Za-z][\w-]*$/.test(id) && !GENERATED_ID.test(id);
182
+ }
183
+ function isStableClass(cls) {
184
+ return !!cls && !GENERATED_CLASS.test(cls) && cls.length <= 40;
185
+ }
186
+ function uniqueIn(doc, selector, el) {
187
+ const all = safeQueryAll(doc, selector);
188
+ return all.length === 1 && all[0] === el;
189
+ }
190
+ function attrSelector(name, value) {
191
+ return `[${name}='${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}']`;
192
+ }
193
+ function cssPath(el, doc) {
194
+ const segments = [];
195
+ let cur = el;
196
+ while (cur && cur.nodeType === 1 && cur !== doc.body && cur !== doc.documentElement) {
197
+ const tag = cur.tagName.toLowerCase();
198
+ if (isStableId(cur.id)) {
199
+ segments.unshift(`#${cur.id}`);
200
+ break;
201
+ }
202
+ let seg = tag;
203
+ const parent = cur.parentElement;
204
+ if (parent) {
205
+ const sameTag = Array.from(parent.children).filter((c) => c.tagName === cur.tagName);
206
+ if (sameTag.length > 1) {
207
+ const stableCls = Array.from(cur.classList).find((c) => isStableClass(c) && sameTag.filter((s) => s.classList.contains(c)).length === 1);
208
+ seg += stableCls ? `.${escapeCss(stableCls)}` : `:nth-of-type(${sameTag.indexOf(cur) + 1})`;
209
+ }
210
+ }
211
+ segments.unshift(seg);
212
+ const candidate = segments.join(" > ");
213
+ if (uniqueIn(doc, candidate, el)) return candidate;
214
+ cur = parent;
215
+ }
216
+ const full = segments.join(" > ");
217
+ return uniqueIn(doc, full, el) ? full : null;
218
+ }
219
+ function textSnapshotOf(el) {
220
+ const he = el;
221
+ const own = normalizeText(he.innerText ?? el.textContent);
222
+ const alt = normalizeText(el.getAttribute("aria-label") || el.getAttribute("title") || el.getAttribute("placeholder") || el.getAttribute("alt") || el.value);
223
+ return (own || alt).slice(0, 200);
224
+ }
225
+ function generateSelectors(el) {
226
+ const doc = el.ownerDocument;
227
+ const out = [];
228
+ const seen = /* @__PURE__ */ new Set();
229
+ const push = (s) => {
230
+ const k = `${s.kind}:${s.value}`;
231
+ if (!seen.has(k)) {
232
+ seen.add(k);
233
+ out.push(s);
234
+ }
235
+ };
236
+ const tag = el.tagName.toLowerCase();
237
+ const guidelyId = el.getAttribute("data-guidely-id");
238
+ if (guidelyId && uniqueIn(doc, attrSelector("data-guidely-id", guidelyId), el)) push({ kind: "guidely-id", value: guidelyId, label: `Guidely id ${guidelyId}` });
239
+ if (isStableId(el.id) && uniqueIn(doc, `#${escapeCss(el.id)}`, el)) push({ kind: "id", value: `#${el.id}`, label: `id ${el.id}` });
240
+ 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));
241
+ dataAttrs.sort((a, b) => {
242
+ const ia = PREFERRED_DATA_ATTRS.indexOf(a.name);
243
+ const ib = PREFERRED_DATA_ATTRS.indexOf(b.name);
244
+ return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib);
245
+ });
246
+ for (const a of dataAttrs) {
247
+ const sel = attrSelector(a.name, a.value);
248
+ if (uniqueIn(doc, sel, el)) push({ kind: "data-attr", value: sel, label: `${a.name}=${a.value}` });
249
+ else if (uniqueIn(doc, tag + sel, el)) push({ kind: "data-attr", value: tag + sel, label: `${a.name}=${a.value}` });
250
+ }
251
+ const aria = el.getAttribute("aria-label");
252
+ if (aria && aria.length <= 120) {
253
+ const sel = attrSelector("aria-label", aria);
254
+ if (uniqueIn(doc, sel, el)) push({ kind: "aria", value: sel, label: aria });
255
+ else if (uniqueIn(doc, tag + sel, el)) push({ kind: "aria", value: tag + sel, label: aria });
256
+ }
257
+ const name = el.getAttribute("name");
258
+ if (name && uniqueIn(doc, tag + attrSelector("name", name), el)) push({ kind: "css", value: tag + attrSelector("name", name), label: `${tag} name=${name}` });
259
+ const role = el.getAttribute("role");
260
+ if (role) {
261
+ const title = el.getAttribute("title") || el.getAttribute("aria-labelledby");
262
+ const sel = title ? `[role='${role}']${attrSelector(el.getAttribute("title") ? "title" : "aria-labelledby", title)}` : `[role='${role}']`;
263
+ if (uniqueIn(doc, sel, el)) push({ kind: "css", value: sel, label: `role ${role}` });
264
+ }
265
+ if (tag === "a") {
266
+ const href = el.getAttribute("href");
267
+ if (href && href.length <= 200 && uniqueIn(doc, `a${attrSelector("href", href)}`, el)) push({ kind: "css", value: `a${attrSelector("href", href)}`, label: `link ${href}` });
268
+ }
269
+ const path = cssPath(el, doc);
270
+ if (path) push({ kind: "css", value: path, label: "css path" });
271
+ if (/^(button|a|h[1-6]|label|summary)$/.test(tag) || role === "button" || role === "link" || role === "tab" || role === "menuitem") {
272
+ const text = normalizeText(el.innerText ?? el.textContent);
273
+ if (text && text.length <= 80) push({ kind: "text", value: text, label: `text "${text}"` });
274
+ }
275
+ return out.slice(0, 10);
276
+ }
277
+ function describeElement(el) {
278
+ const r = el.getBoundingClientRect();
279
+ return {
280
+ selectors: generateSelectors(el),
281
+ textSnapshot: textSnapshotOf(el),
282
+ rect: { x: r.x, y: r.y, width: r.width, height: r.height },
283
+ tag: el.tagName.toLowerCase()
284
+ };
285
+ }
286
+
287
+ export {
288
+ urlMatches,
289
+ normalizeText,
290
+ isVisible,
291
+ isGuidelyElement,
292
+ querySelectorStrategy,
293
+ resolveTarget,
294
+ TargetTimeoutError,
295
+ waitForTarget,
296
+ isStableId,
297
+ textSnapshotOf,
298
+ generateSelectors,
299
+ describeElement
300
+ };
301
+ //# sourceMappingURL=chunk-VNJPNGZ4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/selectors.ts"],"sourcesContent":["import { matchUrlPattern } from \"@guidelytech/shared/runtime\";\nimport type { SelectorStrategy, StepTarget } from \"@guidelytech/shared\";\n\n/**\n * URL matching used for `step.url`, `urlPatterns` and `trigger.urlPattern`: a pattern matches the\n * full URL, the path+query+hash, or the bare path (so `/deals` also matches `/deals?x=1`).\n */\nexport function urlMatches(pattern: string | undefined | null, url: string): boolean {\n if (!pattern) return true;\n if (matchUrlPattern(pattern, url)) return true;\n try {\n const u = new URL(url, \"http://localhost\");\n if (!pattern.includes(\"?\") && !pattern.includes(\"#\")) return matchUrlPattern(pattern, u.pathname) || matchUrlPattern(pattern, u.origin + u.pathname);\n } catch {\n /* ignore */\n }\n return false;\n}\n\nexport type QueryRoot = Document | DocumentFragment | Element;\n\nconst TEXT_CANDIDATES =\n \"button, a, [role='button'], [role='link'], [role='tab'], [role='menuitem'], h1, h2, h3, h4, h5, h6, label, summary\";\n\nconst HOST_ID = \"guidely-root\";\n\nfunction docOf(root: QueryRoot): Document {\n return root.nodeType === 9 ? (root as Document) : (root.ownerDocument as Document);\n}\n\n/** True when the environment has a layout engine (real browsers). happy-dom/jsdom report zero sizes. */\nfunction hasLayout(doc: Document): boolean {\n const de = doc.documentElement;\n if (!de) return false;\n const r = de.getBoundingClientRect();\n return r.width > 0 || r.height > 0;\n}\n\n/** Normalises whitespace in a text snippet. */\nexport function normalizeText(text: string | null | undefined): string {\n return (text || \"\").replace(/\\s+/g, \" \").trim();\n}\n\n/** Is the element rendered (not hidden by `hidden`, `display:none`, `visibility:hidden`, or zero size)? */\nexport function isVisible(el: Element): boolean {\n if (!el.isConnected) return false;\n const doc = el.ownerDocument;\n const win = doc.defaultView;\n let cur: Element | null = el;\n while (cur && cur !== doc.documentElement) {\n if ((cur as HTMLElement).hidden) return false;\n if (win) {\n const cs = win.getComputedStyle(cur);\n if (cs.display === \"none\" || cs.visibility === \"hidden\") return false;\n }\n cur = cur.parentElement ?? ((cur.getRootNode() as ShadowRoot).host as Element | undefined) ?? null;\n }\n if (hasLayout(doc)) {\n const r = el.getBoundingClientRect();\n if (r.width === 0 && r.height === 0) return false;\n }\n return true;\n}\n\n/** Elements that belong to Guidely's own UI are never valid targets. */\nexport function isGuidelyElement(el: Element | null): boolean {\n if (!el) return false;\n const root = el.getRootNode();\n if (root instanceof ShadowRoot && root.host.id === HOST_ID) return true;\n return el.id === HOST_ID || !!el.closest?.(`#${HOST_ID}`);\n}\n\nfunction safeQueryAll(root: QueryRoot, selector: string): Element[] {\n try {\n return Array.from(root.querySelectorAll(selector));\n } catch {\n return [];\n }\n}\n\nfunction escapeCss(value: string): string {\n const g = globalThis as { CSS?: { escape?: (v: string) => string } };\n if (g.CSS && typeof g.CSS.escape === \"function\") return g.CSS.escape(value);\n return value.replace(/[\"\\\\]/g, \"\\\\$&\");\n}\n\nfunction queryXPath(root: QueryRoot, expr: string): Element[] {\n const doc = docOf(root);\n const d = doc as Document & { evaluate?: Document[\"evaluate\"] };\n if (typeof d.evaluate !== \"function\") return [];\n try {\n const result = d.evaluate(expr, root, null, 7 /* ORDERED_NODE_SNAPSHOT_TYPE */, null);\n const out: Element[] = [];\n for (let i = 0; i < result.snapshotLength; i++) {\n const n = result.snapshotItem(i);\n if (n && n.nodeType === 1) out.push(n as Element);\n }\n return out;\n } catch {\n return [];\n }\n}\n\nfunction queryText(root: QueryRoot, text: string): Element[] {\n const needle = normalizeText(text).toLowerCase();\n if (!needle) return [];\n const candidates = safeQueryAll(root, TEXT_CANDIDATES);\n const exact = candidates.filter((c) => normalizeText(c.textContent).toLowerCase() === needle);\n if (exact.length) return exact;\n return candidates.filter((c) => normalizeText(c.textContent).toLowerCase().includes(needle));\n}\n\n/** Resolves every element matching one strategy (visible or not). */\nexport function querySelectorStrategy(strategy: SelectorStrategy, root: QueryRoot): Element[] {\n const value = strategy.value.trim();\n if (!value) return [];\n switch (strategy.kind) {\n case \"id\": {\n const id = value.startsWith(\"#\") ? value.slice(1) : value;\n return safeQueryAll(root, `#${escapeCss(id)}`);\n }\n case \"guidely-id\": {\n const id = value.startsWith(\"#\") ? value.slice(1) : value;\n return safeQueryAll(root, attrSelector(\"data-guidely-id\", id));\n }\n case \"data-attr\":\n case \"aria\":\n case \"css\":\n return safeQueryAll(root, value.startsWith(\"[\") || /^[a-z*#.[]/i.test(value) ? value : `[${value}]`);\n case \"text\":\n return queryText(root, value);\n case \"xpath\":\n return queryXPath(root, value);\n default:\n return [];\n }\n}\n\n/**\n * Resolves a step target by trying each strategy in order. A strategy wins only when it\n * resolves to exactly one *visible* element; otherwise the next strategy is tried.\n */\nexport function resolveTarget(target: Pick<StepTarget, \"selectors\"> | SelectorStrategy[], root: QueryRoot = document): Element | null {\n const selectors = Array.isArray(target) ? target : target.selectors;\n for (const strategy of selectors) {\n const matches = querySelectorStrategy(strategy, root).filter((el) => !isGuidelyElement(el) && isVisible(el));\n if (matches.length === 1) return matches[0] as Element;\n }\n return null;\n}\n\nexport interface WaitForTargetOptions {\n /** Give up after this many milliseconds (default 5000). */\n timeoutMs?: number;\n /** Abort early. */\n signal?: AbortSignal;\n root?: QueryRoot;\n /** Polling fallback interval (default 250ms, which is also the maximum). */\n pollMs?: number;\n}\n\nexport class TargetTimeoutError extends Error {\n override name = \"TargetTimeoutError\";\n constructor(message = \"Target element not found before timeout\") {\n super(message);\n }\n}\n\n/**\n * Waits until `resolveTarget` finds an element. Uses a MutationObserver plus a polling fallback\n * (at most every 250ms) so visibility changes without DOM mutations are also detected.\n */\nexport function waitForTarget(target: Pick<StepTarget, \"selectors\"> | SelectorStrategy[], options: WaitForTargetOptions = {}): Promise<Element> {\n const timeoutMs = options.timeoutMs ?? 5000;\n const root = options.root ?? document;\n const pollMs = Math.min(250, Math.max(16, options.pollMs ?? 250));\n return new Promise<Element>((resolve, reject) => {\n const immediate = resolveTarget(target, root);\n if (immediate) return resolve(immediate);\n if (options.signal?.aborted) return reject(new DOMException(\"Aborted\", \"AbortError\"));\n let done = false;\n let observer: MutationObserver | null = null;\n let timer: ReturnType<typeof setInterval> | null = null;\n let timeout: ReturnType<typeof setTimeout> | null = null;\n const cleanup = () => {\n done = true;\n observer?.disconnect();\n if (timer) clearInterval(timer);\n if (timeout) clearTimeout(timeout);\n options.signal?.removeEventListener(\"abort\", onAbort);\n };\n const check = () => {\n if (done) return;\n const el = resolveTarget(target, root);\n if (el) {\n cleanup();\n resolve(el);\n }\n };\n const onAbort = () => {\n cleanup();\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n };\n options.signal?.addEventListener(\"abort\", onAbort);\n const observeRoot = root.nodeType === 9 ? (root as Document).documentElement : (root as Element);\n if (typeof MutationObserver !== \"undefined\" && observeRoot) {\n observer = new MutationObserver(check);\n observer.observe(observeRoot, { childList: true, subtree: true, attributes: true, attributeFilter: [\"class\", \"style\", \"hidden\", \"id\", \"data-tour\", \"data-guidely-id\", \"aria-label\"] });\n }\n timer = setInterval(check, pollMs);\n if (timeoutMs > 0) {\n timeout = setTimeout(() => {\n cleanup();\n reject(new TargetTimeoutError());\n }, timeoutMs);\n }\n });\n}\n\n// ---------------------------------------------------------------------------\n// Selector generation (used by the element picker)\n// ---------------------------------------------------------------------------\n\nconst GENERATED_ID = /(\\d{4,}|:r[0-9a-z]+:|^radix-|^headlessui-|^mui-|^ember\\d|^react-|^\\w*-\\d+$|__|^[a-f0-9]{8,}$|^svelte-)/i;\nconst GENERATED_CLASS = /(^css-|^sc-|^jss\\d|^svelte-|^emotion-|^chakra-|^Mui|^_|[0-9a-f]{6,}|^\\w{1,2}$|^[a-z]+-\\d+$|[:[\\]/])/;\nconst PREFERRED_DATA_ATTRS = [\"data-testid\", \"data-tour\", \"data-test\", \"data-test-id\", \"data-cy\", \"data-qa\", \"data-id\", \"data-name\", \"data-guidely\"];\nconst IGNORED_DATA_ATTRS = /^data-(guidely-id|state|v-|reactid|react|radix|headlessui|svelte|hydrated|orientation|highlighted|focus|key|index|rbd|dnd|rk-|sveltekit)/i;\n\n/** Is this id stable enough to be used as a selector (not generated by a framework)? */\nexport function isStableId(id: string): boolean {\n return !!id && /^[A-Za-z][\\w-]*$/.test(id) && !GENERATED_ID.test(id);\n}\n\nfunction isStableClass(cls: string): boolean {\n return !!cls && !GENERATED_CLASS.test(cls) && cls.length <= 40;\n}\n\nfunction uniqueIn(doc: Document, selector: string, el: Element): boolean {\n const all = safeQueryAll(doc, selector);\n return all.length === 1 && all[0] === el;\n}\n\nfunction attrSelector(name: string, value: string): string {\n return `[${name}='${value.replace(/\\\\/g, \"\\\\\\\\\").replace(/'/g, \"\\\\'\")}']`;\n}\n\nfunction cssPath(el: Element, doc: Document): string | null {\n const segments: string[] = [];\n let cur: Element | null = el;\n while (cur && cur.nodeType === 1 && cur !== doc.body && cur !== doc.documentElement) {\n const tag = cur.tagName.toLowerCase();\n if (isStableId(cur.id)) {\n segments.unshift(`#${cur.id}`);\n break;\n }\n let seg = tag;\n const parent: Element | null = cur.parentElement;\n if (parent) {\n const sameTag = Array.from(parent.children).filter((c) => c.tagName === cur!.tagName);\n if (sameTag.length > 1) {\n const stableCls = Array.from(cur.classList).find((c) => isStableClass(c) && sameTag.filter((s) => s.classList.contains(c)).length === 1);\n seg += stableCls ? `.${escapeCss(stableCls)}` : `:nth-of-type(${sameTag.indexOf(cur) + 1})`;\n }\n }\n segments.unshift(seg);\n const candidate = segments.join(\" > \");\n if (uniqueIn(doc, candidate, el)) return candidate;\n cur = parent;\n }\n const full = segments.join(\" > \");\n return uniqueIn(doc, full, el) ? full : null;\n}\n\n/** Short human-readable text for an element, trimmed to 200 characters. */\nexport function textSnapshotOf(el: Element): string {\n const he = el as HTMLElement;\n const own = normalizeText(he.innerText ?? el.textContent);\n const alt = normalizeText(el.getAttribute(\"aria-label\") || el.getAttribute(\"title\") || el.getAttribute(\"placeholder\") || el.getAttribute(\"alt\") || (el as HTMLInputElement).value);\n return (own || alt).slice(0, 200);\n}\n\n/**\n * Generates an ordered list of selector strategies for an element, most robust first:\n * stable id → data-* attributes → aria-label → role/name → short unique CSS path → visible text.\n */\nexport function generateSelectors(el: Element): SelectorStrategy[] {\n const doc = el.ownerDocument;\n const out: SelectorStrategy[] = [];\n const seen = new Set<string>();\n const push = (s: SelectorStrategy) => {\n const k = `${s.kind}:${s.value}`;\n if (!seen.has(k)) {\n seen.add(k);\n out.push(s);\n }\n };\n const tag = el.tagName.toLowerCase();\n\n // A Guidely id is shared with the mobile apps, so it is the most portable target there is.\n const guidelyId = el.getAttribute(\"data-guidely-id\");\n if (guidelyId && uniqueIn(doc, attrSelector(\"data-guidely-id\", guidelyId), el)) push({ kind: \"guidely-id\", value: guidelyId, label: `Guidely id ${guidelyId}` });\n if (isStableId(el.id) && uniqueIn(doc, `#${escapeCss(el.id)}`, el)) push({ kind: \"id\", value: `#${el.id}`, label: `id ${el.id}` });\n\n 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));\n dataAttrs.sort((a, b) => {\n const ia = PREFERRED_DATA_ATTRS.indexOf(a.name);\n const ib = PREFERRED_DATA_ATTRS.indexOf(b.name);\n return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib);\n });\n for (const a of dataAttrs) {\n const sel = attrSelector(a.name, a.value);\n if (uniqueIn(doc, sel, el)) push({ kind: \"data-attr\", value: sel, label: `${a.name}=${a.value}` });\n else if (uniqueIn(doc, tag + sel, el)) push({ kind: \"data-attr\", value: tag + sel, label: `${a.name}=${a.value}` });\n }\n\n const aria = el.getAttribute(\"aria-label\");\n if (aria && aria.length <= 120) {\n const sel = attrSelector(\"aria-label\", aria);\n if (uniqueIn(doc, sel, el)) push({ kind: \"aria\", value: sel, label: aria });\n else if (uniqueIn(doc, tag + sel, el)) push({ kind: \"aria\", value: tag + sel, label: aria });\n }\n\n const name = el.getAttribute(\"name\");\n if (name && uniqueIn(doc, tag + attrSelector(\"name\", name), el)) push({ kind: \"css\", value: tag + attrSelector(\"name\", name), label: `${tag} name=${name}` });\n const role = el.getAttribute(\"role\");\n if (role) {\n const title = el.getAttribute(\"title\") || el.getAttribute(\"aria-labelledby\");\n const sel = title ? `[role='${role}']${attrSelector(el.getAttribute(\"title\") ? \"title\" : \"aria-labelledby\", title)}` : `[role='${role}']`;\n if (uniqueIn(doc, sel, el)) push({ kind: \"css\", value: sel, label: `role ${role}` });\n }\n if (tag === \"a\") {\n const href = el.getAttribute(\"href\");\n if (href && href.length <= 200 && uniqueIn(doc, `a${attrSelector(\"href\", href)}`, el)) push({ kind: \"css\", value: `a${attrSelector(\"href\", href)}`, label: `link ${href}` });\n }\n\n const path = cssPath(el, doc);\n if (path) push({ kind: \"css\", value: path, label: \"css path\" });\n\n if (/^(button|a|h[1-6]|label|summary)$/.test(tag) || role === \"button\" || role === \"link\" || role === \"tab\" || role === \"menuitem\") {\n const text = normalizeText((el as HTMLElement).innerText ?? el.textContent);\n if (text && text.length <= 80) push({ kind: \"text\", value: text, label: `text \"${text}\"` });\n }\n return out.slice(0, 10);\n}\n\nexport interface ElementDescription {\n selectors: SelectorStrategy[];\n textSnapshot: string;\n rect: { x: number; y: number; width: number; height: number };\n tag: string;\n}\n\n/** Everything the authoring picker needs for one element. */\nexport function describeElement(el: Element): ElementDescription {\n const r = el.getBoundingClientRect();\n return {\n selectors: generateSelectors(el),\n textSnapshot: textSnapshotOf(el),\n rect: { x: r.x, y: r.y, width: r.width, height: r.height },\n tag: el.tagName.toLowerCase(),\n };\n}\n"],"mappings":"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","names":[]}
package/dist/index.cjs CHANGED
@@ -618,7 +618,15 @@ var STYLES = `
618
618
  .g-rc-item .g-rc-title{font-weight:500}
619
619
  .g-rc-item .g-rc-sum{font-size:12px;color:var(--g-muted);margin-top:2px}
620
620
  .g-rc-item .g-rc-icon{flex:none;width:20px;text-align:center;color:var(--g-primary)}
621
+ .g-rc-row{display:flex;align-items:center;gap:6px;padding-right:16px}
622
+ .g-rc-row>.g-rc-item{flex:1;min-width:0;padding-right:4px}
623
+ .g-rc-row:hover{background:var(--g-subtle)}
624
+ .g-rc-row:hover>.g-rc-item{background:transparent}
625
+ .g-rc-showme{flex:none;min-height:32px;padding:4px 10px;font-size:12px;white-space:nowrap}
626
+ .g-help-showme{margin:4px 0 12px}
621
627
  .g-rc-article{padding:16px 20px}
628
+ .g-resource-center .g-panel{transition:width .2s ease,max-height .2s ease}
629
+ .g-resource-center .g-panel.g-panel-reading{width:min(720px,calc(100vw - 32px));max-height:calc(100vh - 100px)}
622
630
  .g-rc-back{margin-bottom:8px}
623
631
  .g-rc-empty{padding:24px 20px;color:var(--g-muted);text-align:center}
624
632
  .g-faq-a{padding:0 20px 12px 50px;font-size:13px;color:var(--g-muted)}
@@ -1363,6 +1371,17 @@ function isAnchoredStep(step) {
1363
1371
  var import_runtime5 = require("@guidelytech/shared/runtime");
1364
1372
  function noop() {
1365
1373
  }
1374
+ var escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
1375
+ function navigableUrl(pattern) {
1376
+ if (!pattern || /[*?#\s]/.test(pattern) || pattern.includes("/:")) return null;
1377
+ if (pattern.startsWith("/") && !pattern.startsWith("//")) return pattern;
1378
+ try {
1379
+ const u = new URL(pattern);
1380
+ if (typeof location !== "undefined" && u.origin === location.origin) return u.pathname;
1381
+ } catch {
1382
+ }
1383
+ return null;
1384
+ }
1366
1385
  function runGuide(guide, options = {}) {
1367
1386
  const doc = options.doc ?? (options.container?.ownerDocument || document);
1368
1387
  const steps = guide.config.steps;
@@ -1507,15 +1526,15 @@ function runGuide(guide, options = {}) {
1507
1526
  onError(err);
1508
1527
  }
1509
1528
  };
1510
- const setupAdvance = (step) => {
1529
+ const setupAdvance = (step, target) => {
1511
1530
  const modes = step.advanceOn === "any" ? ["element_click", "element_visible", "event", "delay"] : [step.advanceOn];
1512
1531
  for (const mode of modes) {
1513
- if (mode === "element_click" && step.advanceValue) {
1532
+ if (mode === "element_click" && (step.advanceValue || target)) {
1514
1533
  const selector = step.advanceValue;
1515
1534
  const handler = (e) => {
1516
1535
  const t = e.target;
1517
1536
  try {
1518
- if (t && typeof t.closest === "function" && t.closest(selector)) advance();
1537
+ if (selector ? t && typeof t.closest === "function" && t.closest(selector) : t && target?.contains(t)) advance();
1519
1538
  } catch {
1520
1539
  }
1521
1540
  };
@@ -1551,10 +1570,11 @@ function runGuide(guide, options = {}) {
1551
1570
  const render = (step, target, asModal = false) => {
1552
1571
  if (ended) return;
1553
1572
  status = "running";
1573
+ const shown = !target && step.advanceOn === "element_click" && !step.advanceValue && !step.buttons.length ? { ...step, buttons: [{ id: "g-next", label: index + 1 >= total ? "Done" : "Next", action: index + 1 >= total ? "complete" : "next", variant: "primary" }] } : step;
1554
1574
  try {
1555
1575
  view = renderStep({
1556
1576
  scope: getScope(),
1557
- step,
1577
+ step: shown,
1558
1578
  index,
1559
1579
  total,
1560
1580
  canGoBack: history.length > 0 || index > 0,
@@ -1576,7 +1596,43 @@ function runGuide(guide, options = {}) {
1576
1596
  }
1577
1597
  emit("step_viewed", step, index, asModal ? { fallback: "show_as_modal" } : void 0);
1578
1598
  announce(step.title ? `${step.title}. Step ${index + 1} of ${total}` : `Step ${index + 1} of ${total}`);
1579
- setupAdvance(step);
1599
+ setupAdvance(step, target);
1600
+ };
1601
+ const showUrlPrompt = (step) => {
1602
+ if (ended || status !== "waiting_url" || view) return;
1603
+ const href = navigableUrl(step.url);
1604
+ const where = (step.url ?? "").replace(/^https?:\/\/[^/]+/i, "") || "another page";
1605
+ const prompt = {
1606
+ ...step,
1607
+ type: "modal",
1608
+ title: "Continue on another page",
1609
+ body: `<p>The next step${step.title ? `, <strong>${escapeHtml(step.title)}</strong>,` : ""} is on <code>${escapeHtml(where)}</code>.</p>`,
1610
+ imageUrl: "",
1611
+ buttons: [
1612
+ ...href ? [{ id: "g-go", label: "Take me there", action: "url", value: href, variant: "primary" }] : [],
1613
+ { id: "g-exit", label: "Exit", action: "dismiss", variant: href ? "secondary" : "primary" }
1614
+ ]
1615
+ };
1616
+ try {
1617
+ view = renderStep({
1618
+ scope: getScope(),
1619
+ step: prompt,
1620
+ index,
1621
+ total,
1622
+ canGoBack: false,
1623
+ target: null,
1624
+ asModal: true,
1625
+ onButton: (b) => {
1626
+ if (b.action !== "url" || !b.value) return end("dismissed");
1627
+ if (options.onNavigate) options.onNavigate(b.value);
1628
+ else location.assign(b.value);
1629
+ },
1630
+ onClose: () => end("dismissed"),
1631
+ announce
1632
+ });
1633
+ } catch (err) {
1634
+ onError(err);
1635
+ }
1580
1636
  };
1581
1637
  const applyFallback = (step, direction) => {
1582
1638
  const fallback = step.target?.fallback ?? "skip";
@@ -1602,9 +1658,18 @@ function runGuide(guide, options = {}) {
1602
1658
  if (nextIndex < 0) return;
1603
1659
  return showStep(nextIndex, direction);
1604
1660
  }
1661
+ try {
1662
+ options.onPosition?.(step.id);
1663
+ } catch (err) {
1664
+ onError(err);
1665
+ }
1605
1666
  if (step.url && !urlMatches(step.url, currentUrl())) {
1606
1667
  status = "waiting_url";
1607
1668
  pendingIndex = i;
1669
+ if (options.promptForUrl) {
1670
+ const t = setTimeout(() => showUrlPrompt(step), options.urlPromptDelayMs ?? 1200);
1671
+ stepCleanups.push(() => clearTimeout(t));
1672
+ }
1608
1673
  return;
1609
1674
  }
1610
1675
  pendingIndex = -1;
@@ -2102,6 +2167,38 @@ function renderSurvey(options) {
2102
2167
 
2103
2168
  // src/help-view.ts
2104
2169
  var import_runtime8 = require("@guidelytech/shared/runtime");
2170
+ var SHOW_ME_KINDS = /* @__PURE__ */ new Set(["flow", "modal", "tooltip", "announcement"]);
2171
+ function startableGuide(guides, keyOrId) {
2172
+ if (!keyOrId || !guides) return null;
2173
+ return guides.find((g) => (g.key === keyOrId || g.id === keyOrId) && SHOW_ME_KINDS.has(g.kind)) ?? null;
2174
+ }
2175
+ function resultRow(doc, icon, title, summary, onOpen, showMe) {
2176
+ const item = listItem(doc, icon, title, summary);
2177
+ item.addEventListener("click", onOpen);
2178
+ if (!showMe) return item;
2179
+ const row = el(doc, "div", "g-rc-row");
2180
+ const b = el(doc, "button", "g-btn g-btn-primary g-rc-showme", "\u25B6 Show me");
2181
+ b.type = "button";
2182
+ b.setAttribute("aria-label", `Show me: ${title}`);
2183
+ b.addEventListener("click", showMe);
2184
+ row.append(item, b);
2185
+ return row;
2186
+ }
2187
+ function enhanceShowMe(root, doc, canStart, start) {
2188
+ for (const a of Array.from(root.querySelectorAll(`a[href^="${import_runtime8.SHOW_ME_HREF_PREFIX}"]`))) {
2189
+ const key = (0, import_runtime8.showMeKeyFromHref)(a.getAttribute("href"));
2190
+ const holder = a.closest("p.gd-show-me") ?? a;
2191
+ if (!key || !canStart(key)) {
2192
+ if (holder !== a) holder.remove();
2193
+ else a.replaceWith(doc.createTextNode(a.textContent ?? ""));
2194
+ continue;
2195
+ }
2196
+ const b = el(doc, "button", "g-btn g-btn-primary g-help-showme", `\u25B6 ${(a.textContent ?? "").trim() || "Show me"}`);
2197
+ b.type = "button";
2198
+ b.addEventListener("click", () => start(key));
2199
+ a.replaceWith(b);
2200
+ }
2201
+ }
2105
2202
  function el(doc, tag, cls, text) {
2106
2203
  const e = doc.createElement(tag);
2107
2204
  if (cls) e.className = cls;
@@ -2126,6 +2223,11 @@ function createHelpView(options) {
2126
2223
  let searchSeq = 0;
2127
2224
  let openSeq = 0;
2128
2225
  let active = null;
2226
+ const setActive = (next) => {
2227
+ const was = !!active;
2228
+ active = next;
2229
+ if (was !== !!next) options.onArticleChange?.(!!next);
2230
+ };
2129
2231
  const root = el(doc, "div", "g-help");
2130
2232
  const searchWrap = el(doc, "div", "g-rc-search");
2131
2233
  const input = el(doc, "input", "g-input");
@@ -2136,6 +2238,8 @@ function createHelpView(options) {
2136
2238
  const body = el(doc, "div", "g-panel-body");
2137
2239
  root.append(searchWrap, body);
2138
2240
  const hasBody = (a) => typeof a.body === "string" && !a.bodyOmitted;
2241
+ const canShowMe = (key) => !!options.onShowMe && !!startableGuide(options.guides?.(), key);
2242
+ const showMe = (key) => options.onShowMe?.(startableGuide(options.guides?.(), key)?.key ?? key);
2139
2243
  const openArticle = (article) => {
2140
2244
  if (article.externalUrl) {
2141
2245
  window.open(article.externalUrl, "_blank", "noopener,noreferrer");
@@ -2144,20 +2248,20 @@ function createHelpView(options) {
2144
2248
  }
2145
2249
  const seq = ++openSeq;
2146
2250
  if (hasBody(article) || !options.loadArticle) {
2147
- active = { article, loading: false };
2251
+ setActive({ article, loading: false });
2148
2252
  if (hasBody(article)) options.onArticleView?.(article);
2149
2253
  return render();
2150
2254
  }
2151
- active = { article, loading: true };
2255
+ setActive({ article, loading: true });
2152
2256
  render();
2153
2257
  options.loadArticle(article.id).then((full) => {
2154
2258
  if (seq !== openSeq || !active) return;
2155
- active = { article: full ?? { ...article, body: "" }, loading: false };
2259
+ setActive({ article: full ?? { ...article, body: "" }, loading: false });
2156
2260
  if (full) options.onArticleView?.(full);
2157
2261
  render();
2158
2262
  }).catch(() => {
2159
2263
  if (seq !== openSeq || !active) return;
2160
- active = { article: { ...article, body: "" }, loading: false };
2264
+ setActive({ article: { ...article, body: "" }, loading: false });
2161
2265
  render();
2162
2266
  });
2163
2267
  };
@@ -2167,7 +2271,7 @@ function createHelpView(options) {
2167
2271
  back.type = "button";
2168
2272
  back.addEventListener("click", () => {
2169
2273
  openSeq++;
2170
- active = null;
2274
+ setActive(null);
2171
2275
  render();
2172
2276
  });
2173
2277
  const title = el(doc, "h3", "g-title", article.title);
@@ -2176,6 +2280,7 @@ function createHelpView(options) {
2176
2280
  else if (article.body) {
2177
2281
  setSanitizedHtml(content, article.body);
2178
2282
  enhancePdfViewers(content, doc);
2283
+ enhanceShowMe(content, doc, canShowMe, showMe);
2179
2284
  } else content.appendChild(el(doc, "div", "g-rc-empty", "This article couldn't be loaded."));
2180
2285
  wrap.append(back, title, content);
2181
2286
  body.appendChild(wrap);
@@ -2187,9 +2292,9 @@ function createHelpView(options) {
2187
2292
  return s;
2188
2293
  };
2189
2294
  const articleButton = (a) => {
2190
- const b = listItem(doc, "\u{1F4C4}", a.title, a.summary);
2191
- b.addEventListener("click", () => openArticle(a));
2192
- body.appendChild(b);
2295
+ const key = a.walkthroughs?.find(canShowMe);
2296
+ const summary = a.hasVideo ? a.summary ? `Video \xB7 ${a.summary}` : "Video" : a.summary;
2297
+ body.appendChild(resultRow(doc, a.hasVideo ? "\u25B6" : "\u{1F4C4}", a.title, summary, () => openArticle(a), key ? () => showMe(key) : void 0));
2193
2298
  };
2194
2299
  function render() {
2195
2300
  body.textContent = "";
@@ -2198,10 +2303,17 @@ function createHelpView(options) {
2198
2303
  const member = options.audienceIds?.() ?? [];
2199
2304
  const sections = (0, import_runtime8.resourceCenterSections)(data.collections, data.articles, member);
2200
2305
  const q = query.trim().toLowerCase();
2306
+ if (q) {
2307
+ const walkthroughs = (data.recommended ?? []).filter((g) => (0, import_runtime8.matchesHelpQuery)(g.name, q) && startableGuide(options.guides?.() ?? data.recommended, g.id));
2308
+ if (walkthroughs.length) {
2309
+ section(`Walkthroughs (${walkthroughs.length})`);
2310
+ for (const g of walkthroughs) body.appendChild(resultRow(doc, "\u25B6", g.name, "Show me, step by step", () => options.onStartGuide?.(g.id)));
2311
+ }
2312
+ }
2201
2313
  body.append(...options.extra?.(q) ?? []);
2202
2314
  if (q) {
2203
2315
  const visible = sections.flatMap((s) => s.articles);
2204
- const local = visible.filter((a) => a.title.toLowerCase().includes(q) || a.summary.toLowerCase().includes(q));
2316
+ const local = visible.filter((a) => (0, import_runtime8.matchesHelpQuery)(`${a.title} ${a.summary}`, q));
2205
2317
  const seen = new Set(local.map((a) => a.id));
2206
2318
  const hidden = new Set(data.articles.map((a) => a.id).filter((id) => !visible.some((v) => v.id === id)));
2207
2319
  const merged = [...local, ...serverResults.filter((a) => !seen.has(a.id) && !hidden.has(a.id) && (0, import_runtime8.isArticleListed)(data.collections, a, member))];
@@ -2273,7 +2385,7 @@ function createHelpView(options) {
2273
2385
  const onInput = () => {
2274
2386
  query = input.value;
2275
2387
  openSeq++;
2276
- active = null;
2388
+ setActive(null);
2277
2389
  render();
2278
2390
  if (debounce) clearTimeout(debounce);
2279
2391
  if (!options.search || !query.trim()) {
@@ -2296,7 +2408,7 @@ function createHelpView(options) {
2296
2408
  render,
2297
2409
  reset() {
2298
2410
  openSeq++;
2299
- active = null;
2411
+ setActive(null);
2300
2412
  query = "";
2301
2413
  input.value = "";
2302
2414
  serverResults = [];
@@ -2305,6 +2417,7 @@ function createHelpView(options) {
2305
2417
  const listed = options.data().articles.find((a) => a.id === id) ?? { id, slug: id, title: "Article", summary: "", updatedAt: "", bodyOmitted: true };
2306
2418
  openArticle(listed);
2307
2419
  },
2420
+ isArticleOpen: () => !!active,
2308
2421
  focusSearch: () => input.focus?.({ preventScroll: true }),
2309
2422
  setQuery(q) {
2310
2423
  input.value = q;
@@ -2356,7 +2469,14 @@ function renderResourceCenter(options) {
2356
2469
  loadArticle: options.loadArticle,
2357
2470
  onStartGuide: options.onStartGuide,
2358
2471
  onOpenChecklist: options.onOpenChecklist,
2359
- onArticleView: options.onArticleView
2472
+ onArticleView: options.onArticleView,
2473
+ guides: () => guides,
2474
+ onShowMe: (key) => {
2475
+ setOpen(false);
2476
+ options.onStartGuide?.(key);
2477
+ },
2478
+ // Articles read in a larger panel.
2479
+ onArticleChange: (reading) => panel.classList.toggle("g-panel-reading", reading)
2360
2480
  });
2361
2481
  panel.append(head, help.el);
2362
2482
  root.append(launcher, panel);
@@ -2542,7 +2662,7 @@ function previewStep(step, theme, options = {}) {
2542
2662
  name: "Preview",
2543
2663
  kind: "flow",
2544
2664
  version: 0,
2545
- config: { steps: [previewStepConfig], targeting: { id: "root", op: "and", rules: [] }, trigger: { type: "manual", delayMs: 0 }, frequency: { mode: "every_time", cooldownHours: 0, priority: 0, mutuallyExclusiveWith: [], reshowAfterDismiss: true }, appearance: options.appearance ?? { theme: "auto", animation: "fade" }, urlPatterns: [], platforms: ["web"] }
2665
+ config: { steps: [previewStepConfig], visibility: "everyone", targeting: { id: "root", op: "and", rules: [] }, trigger: { type: "manual", delayMs: 0 }, frequency: { mode: "every_time", cooldownHours: 0, priority: 0, mutuallyExclusiveWith: [], reshowAfterDismiss: true }, appearance: options.appearance ?? { theme: "auto", animation: "fade" }, urlPatterns: [], platforms: ["web"] }
2546
2666
  };
2547
2667
  const controller = runGuide(guide, { theme, doc: options.doc, onEnd: () => options.onEnd?.(), onNavigate: () => {
2548
2668
  }, onAction: () => {