@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/index.js CHANGED
@@ -5,21 +5,31 @@ import {
5
5
  sanitizeHtml,
6
6
  sanitizeToFragment,
7
7
  setSanitizedHtml
8
- } from "./chunk-KJRXCITD.js";
8
+ } from "./chunk-H3CROHPR.js";
9
9
  import {
10
+ SHADOW_SEPARATOR,
10
11
  TargetTimeoutError,
12
+ closestComposed,
13
+ componentAnchor,
14
+ composedParent,
15
+ composedTarget,
16
+ containsComposed,
17
+ deepElementFromPoint,
11
18
  describeElement,
12
19
  generateSelectors,
13
20
  isGuidelyElement,
21
+ isStableClass,
14
22
  isStableId,
15
23
  isVisible,
16
24
  normalizeText,
17
25
  querySelectorStrategy,
18
26
  resolveTarget,
27
+ shadowHostChain,
19
28
  textSnapshotOf,
20
29
  urlMatches,
30
+ visualRect,
21
31
  waitForTarget
22
- } from "./chunk-VNJPNGZ4.js";
32
+ } from "./chunk-VQJ7MZ2K.js";
23
33
  import {
24
34
  DEFAULT_THEME,
25
35
  HOST_ID,
@@ -30,11 +40,12 @@ import {
30
40
  ensureHost,
31
41
  prefersReducedMotion,
32
42
  resolveTheme
33
- } from "./chunk-PG7CMBHD.js";
43
+ } from "./chunk-VJSQM23R.js";
34
44
 
35
45
  // src/position.ts
36
46
  import { arrow, autoPlacement, autoUpdate, computePosition, flip, offset, shift } from "@floating-ui/dom";
37
- function anchor(reference, floating, options = {}) {
47
+ function anchor(target, floating, options = {}) {
48
+ const reference = target.shadowRoot ? { getBoundingClientRect: () => visualRect(target), contextElement: target } : target;
38
49
  const placement = options.placement ?? "auto";
39
50
  const gap = options.offset ?? 10;
40
51
  const middleware = [offset(gap + (options.arrowEl ? 4 : 0))];
@@ -87,7 +98,7 @@ function trackRect(el2, cb) {
87
98
  let stopped = false;
88
99
  const run = () => {
89
100
  if (stopped) return;
90
- cb(el2.getBoundingClientRect());
101
+ cb(visualRect(el2));
91
102
  };
92
103
  const schedule = () => {
93
104
  if (stopped) return;
@@ -100,6 +111,9 @@ function trackRect(el2, cb) {
100
111
  const win = el2.ownerDocument.defaultView;
101
112
  win?.addEventListener("scroll", schedule, true);
102
113
  win?.addEventListener("resize", schedule);
114
+ const shadowRoots = [];
115
+ for (let root = el2.getRootNode(); root.nodeType === 11 && root.host; root = root.host.getRootNode()) shadowRoots.push(root);
116
+ for (const root of shadowRoots) root.addEventListener("scroll", schedule, true);
103
117
  let ro = null;
104
118
  if (typeof ResizeObserver !== "undefined") {
105
119
  try {
@@ -115,6 +129,7 @@ function trackRect(el2, cb) {
115
129
  stopped = true;
116
130
  win?.removeEventListener("scroll", schedule, true);
117
131
  win?.removeEventListener("resize", schedule);
132
+ for (const root of shadowRoots) root.removeEventListener("scroll", schedule, true);
118
133
  ro?.disconnect();
119
134
  clearInterval(interval);
120
135
  if (typeof cancelAnimationFrame === "function") cancelAnimationFrame(raf);
@@ -155,7 +170,7 @@ function buildCard(doc, opts, extraClass) {
155
170
  if (step.imageUrl) {
156
171
  const img = h(doc, "img", "g-image");
157
172
  img.src = step.imageUrl;
158
- img.alt = "";
173
+ img.alt = step.imageAlt ?? "";
159
174
  card.appendChild(img);
160
175
  }
161
176
  const inner = h(doc, "div", "g-inner");
@@ -183,18 +198,29 @@ function buildCard(doc, opts, extraClass) {
183
198
  const buttons = buildButtons(doc, step, opts.canGoBack ?? opts.index > 0, opts.onButton);
184
199
  if (buttons.length || step.showProgress && opts.total > 1) {
185
200
  const footer = h(doc, "div", "g-footer");
186
- if (step.showProgress && opts.total > 1) footer.appendChild(h(doc, "span", "g-progress", `${opts.index + 1} of ${opts.total}`));
201
+ if (step.showProgress && opts.total > 1) {
202
+ const progress = h(doc, "span", "g-progress", `${opts.index + 1} of ${opts.total}`);
203
+ progress.setAttribute("aria-label", `Step ${opts.index + 1} of ${opts.total}`);
204
+ footer.appendChild(progress);
205
+ }
187
206
  buttons.forEach((b) => footer.appendChild(b));
188
207
  inner.appendChild(footer);
189
208
  }
190
209
  card.appendChild(inner);
191
210
  if (step.title) card.setAttribute("aria-labelledby", titleId);
211
+ else card.setAttribute("aria-label", opts.total > 1 ? `Step ${opts.index + 1} of ${opts.total}` : "Guide");
192
212
  if (step.body) card.setAttribute("aria-describedby", bodyId);
193
213
  return { card, inner, titleId, bodyId };
194
214
  }
195
215
  function focusables(root) {
196
216
  return Array.from(root.querySelectorAll(FOCUSABLE));
197
217
  }
218
+ function isTextEntry(el2) {
219
+ const t = el2;
220
+ if (!t || !t.tagName) return false;
221
+ const tag = t.tagName.toLowerCase();
222
+ return tag === "input" || tag === "textarea" || tag === "select" || t.isContentEditable === true;
223
+ }
198
224
  function installKeyboard(card, step, trap, onClose) {
199
225
  const onKey = (e) => {
200
226
  if (e.key === "Escape" && step.showClose) {
@@ -202,6 +228,16 @@ function installKeyboard(card, step, trap, onClose) {
202
228
  onClose();
203
229
  return;
204
230
  }
231
+ if ((e.key === "ArrowRight" || e.key === "ArrowLeft") && !e.altKey && !e.ctrlKey && !e.metaKey && !isTextEntry(e.target)) {
232
+ const actions = e.key === "ArrowRight" ? ["next", "complete"] : ["back"];
233
+ const btn = actions.map((a) => card.querySelector(`button[data-action="${a}"]`)).find(Boolean);
234
+ if (btn) {
235
+ e.preventDefault();
236
+ e.stopPropagation();
237
+ btn.click();
238
+ }
239
+ return;
240
+ }
205
241
  if (trap && e.key === "Tab") {
206
242
  const items = focusables(card);
207
243
  if (!items.length) {
@@ -226,7 +262,8 @@ function installKeyboard(card, step, trap, onClose) {
226
262
  }
227
263
  function focusInto(card) {
228
264
  const doc = card.ownerDocument;
229
- const previous = doc.activeElement;
265
+ const active = doc.activeElement;
266
+ const previous = active && active.id !== "guidely-root" ? active : null;
230
267
  const primary = card.querySelector(".g-btn-primary") || focusables(card)[0] || card;
231
268
  try {
232
269
  primary.focus({ preventScroll: true });
@@ -374,23 +411,32 @@ function renderHotspot(doc, opts) {
374
411
  let card = null;
375
412
  let stopAnchor = null;
376
413
  let stopKeys = null;
377
- const close = () => {
414
+ const close = (returnFocus = false) => {
415
+ const hadFocus = !!card && card.getRootNode().activeElement !== null && card.contains(card.getRootNode().activeElement);
378
416
  stopAnchor?.();
379
417
  stopKeys?.();
380
418
  card?.remove();
381
419
  card = null;
382
420
  dot.setAttribute("aria-expanded", "false");
421
+ if (returnFocus || hadFocus) {
422
+ try {
423
+ dot.focus({ preventScroll: true });
424
+ } catch {
425
+ }
426
+ }
383
427
  };
384
428
  const open = () => {
385
429
  if (card) return;
386
430
  const built = buildCard(doc, { ...opts, onClose: () => opts.onClose() }, "g-hotspot-card");
387
431
  card = built.card;
388
432
  card.setAttribute("role", "dialog");
433
+ card.id = nextId("hotspot");
434
+ dot.setAttribute("aria-controls", card.id);
389
435
  const arrowEl = h(doc, "div", "g-arrow");
390
436
  card.appendChild(arrowEl);
391
437
  wrapper.appendChild(card);
392
438
  stopAnchor = anchor(target, card, { placement: opts.step.placement, offset: opts.step.offset, arrowEl });
393
- stopKeys = installKeyboard(card, opts.step, false, close);
439
+ stopKeys = installKeyboard(card, opts.step, false, () => close(true));
394
440
  dot.setAttribute("aria-expanded", "true");
395
441
  opts.onOpen?.();
396
442
  if (cfg.openOn === "click") focusInto(card);
@@ -412,7 +458,8 @@ function renderBanner(doc, opts) {
412
458
  const cleanups = [];
413
459
  const pos = step.position === "top" || step.placement === "top" || step.position.startsWith("top") ? "top" : "bottom";
414
460
  const banner = h(doc, "div", `g-banner g-pos-${pos}`);
415
- banner.setAttribute("role", "status");
461
+ banner.setAttribute("role", "region");
462
+ banner.setAttribute("aria-label", step.title || "Announcement");
416
463
  banner.setAttribute("data-step-id", step.id);
417
464
  const body = h(doc, "div", "g-body");
418
465
  if (step.title) body.appendChild(h(doc, "strong", "", `${step.title} `));
@@ -445,6 +492,27 @@ function renderSlideout(doc, opts) {
445
492
  cleanups.push(focusInto(card));
446
493
  return { el: wrapper, card, destroy: () => cleanupAll(cleanups, wrapper) };
447
494
  }
495
+ function runCodeBlock(code, container, opts) {
496
+ const act = (action) => () => opts.onButton({ id: `code-${action}`, label: action, action, variant: "primary" });
497
+ const api = Object.freeze({ next: act("next"), back: act("back"), complete: act("complete"), skip: act("skip"), dismiss: () => opts.onClose(), stepId: opts.step.id });
498
+ try {
499
+ const fn = new Function("guidely", "container", `"use strict";
500
+ ${code}`);
501
+ const result = fn(api, container);
502
+ return typeof result === "function" ? () => {
503
+ try {
504
+ result();
505
+ } catch (err) {
506
+ opts.onError?.(err);
507
+ }
508
+ } : () => {
509
+ };
510
+ } catch (err) {
511
+ opts.onError?.(err);
512
+ return () => {
513
+ };
514
+ }
515
+ }
448
516
  function renderCustom(doc, opts) {
449
517
  const cleanups = [];
450
518
  const wrapper = h(doc, "div", "g-step");
@@ -453,6 +521,9 @@ function renderCustom(doc, opts) {
453
521
  el2.tabIndex = -1;
454
522
  el2.setAttribute("data-step-id", opts.step.id);
455
523
  setSanitizedHtml(el2, opts.step.customHtml || opts.step.body || "");
524
+ el2.setAttribute("role", opts.step.showOverlay ? "dialog" : "group");
525
+ el2.setAttribute("aria-label", opts.step.title || "Guide");
526
+ if (opts.step.showOverlay) el2.setAttribute("aria-modal", "true");
456
527
  const onClick = (e) => {
457
528
  const t = e.target?.closest?.("[data-g-action]");
458
529
  const action = t?.getAttribute("data-g-action");
@@ -466,6 +537,7 @@ function renderCustom(doc, opts) {
466
537
  opts.scope.appendChild(wrapper);
467
538
  cleanups.push(installKeyboard(el2, opts.step, false, opts.onClose));
468
539
  cleanups.push(focusInto(el2));
540
+ if (opts.runCode && opts.step.code) cleanups.push(runCodeBlock(opts.step.code, el2, opts));
469
541
  return { el: wrapper, card: el2, destroy: () => cleanupAll(cleanups, wrapper) };
470
542
  }
471
543
  function renderStep(opts) {
@@ -563,6 +635,17 @@ function runGuide(guide, options = {}) {
563
635
  view = null;
564
636
  stepDone = false;
565
637
  };
638
+ let returnFocusTo = null;
639
+ const restoreFocus = () => {
640
+ const el2 = returnFocusTo;
641
+ returnFocusTo = null;
642
+ try {
643
+ const now = doc.activeElement;
644
+ const lost = !now || now === doc.body || now.id === "guidely-root";
645
+ if (el2 && lost && el2.isConnected && typeof el2.focus === "function") el2.focus({ preventScroll: true });
646
+ } catch {
647
+ }
648
+ };
566
649
  const end = (reason) => {
567
650
  if (ended) return;
568
651
  ended = true;
@@ -574,6 +657,7 @@ function runGuide(guide, options = {}) {
574
657
  else if (reason === "skipped") emit("guide_skipped", step, index);
575
658
  scope?.destroy();
576
659
  scope = null;
660
+ restoreFocus();
577
661
  try {
578
662
  options.onEnd?.(reason);
579
663
  } catch (err) {
@@ -662,9 +746,9 @@ function runGuide(guide, options = {}) {
662
746
  if (mode === "element_click" && (step.advanceValue || target)) {
663
747
  const selector = step.advanceValue;
664
748
  const handler = (e) => {
665
- const t = e.target;
749
+ const t = composedTarget(e);
666
750
  try {
667
- if (selector ? t && typeof t.closest === "function" && t.closest(selector) : t && target?.contains(t)) advance();
751
+ if (selector ? t && closestComposed(t, selector) : t && target && containsComposed(target, t)) advance();
668
752
  } catch {
669
753
  }
670
754
  };
@@ -712,7 +796,9 @@ function runGuide(guide, options = {}) {
712
796
  asModal,
713
797
  onButton: handleButton,
714
798
  onClose: () => end("dismissed"),
715
- announce
799
+ announce,
800
+ runCode: options.runCode,
801
+ onError
716
802
  });
717
803
  } catch (err) {
718
804
  onError(err);
@@ -824,6 +910,8 @@ function runGuide(guide, options = {}) {
824
910
  guide,
825
911
  start() {
826
912
  if (status !== "idle") return;
913
+ const focused = doc.activeElement;
914
+ if (focused && focused !== doc.body && focused.id !== "guidely-root") returnFocusTo = focused;
827
915
  emit("guide_viewed");
828
916
  if (total === 0) return end("completed");
829
917
  let startIndex = options.startAtStep ? steps.findIndex((s) => s.id === options.startAtStep) : 0;
@@ -1296,6 +1384,7 @@ function renderSurvey(options) {
1296
1384
  }
1297
1385
 
1298
1386
  // src/resource-center.ts
1387
+ import { orderedModules, unreadAnnouncements, visibleAnnouncements } from "@guidelytech/shared/runtime";
1299
1388
  function renderResourceCenter(options) {
1300
1389
  const { scope, config } = options;
1301
1390
  const doc = scope.ownerDocument;
@@ -1315,6 +1404,19 @@ function renderResourceCenter(options) {
1315
1404
  launcher.appendChild(el(doc, "span", "", config.launcher.label || config.title));
1316
1405
  launcher.setAttribute("aria-label", config.launcher.label || config.title);
1317
1406
  if (options.hideLauncher) launcher.hidden = true;
1407
+ const badge = el(doc, "span", "g-launcher-badge");
1408
+ badge.hidden = true;
1409
+ launcher.appendChild(badge);
1410
+ const modules = () => orderedModules(config.modules).filter((m) => m.enabled && (!("targeting" in m) || !m.targeting || !options.evaluate || options.evaluate(m.targeting))).map((m) => m.id);
1411
+ const announcements = () => modules().includes("announcements") ? visibleAnnouncements(config.announcements, options.audienceIds?.() ?? []) : [];
1412
+ const updateBadge = () => {
1413
+ const unread = unreadAnnouncements(announcements(), options.announcementsSeenAt?.());
1414
+ badge.hidden = unread === 0;
1415
+ badge.textContent = unread > 9 ? "9+" : String(unread);
1416
+ const label = config.launcher.label || config.title;
1417
+ launcher.setAttribute("aria-label", unread ? `${label} (${unread} new update${unread === 1 ? "" : "s"})` : label);
1418
+ };
1419
+ updateBadge();
1318
1420
  const panel = el(doc, "div", `g-panel ${pos}`);
1319
1421
  panel.setAttribute("role", "dialog");
1320
1422
  panel.setAttribute("aria-label", config.title);
@@ -1325,9 +1427,20 @@ function renderResourceCenter(options) {
1325
1427
  close.type = "button";
1326
1428
  close.setAttribute("aria-label", "Close");
1327
1429
  head.appendChild(close);
1430
+ let seenAtOpen = options.announcementsSeenAt?.() ?? null;
1328
1431
  const help = createHelpView({
1329
1432
  doc,
1330
- data: () => ({ collections: config.collections, articles: config.articles, recommended: byId(config.recommendedGuideIds), checklists: byId(config.checklistGuideIds), faqs: config.faqs, links: config.links }),
1433
+ data: () => ({
1434
+ collections: config.collections,
1435
+ articles: config.articles,
1436
+ recommended: byId(config.recommendedGuideIds),
1437
+ checklists: byId(config.checklistGuideIds),
1438
+ faqs: config.faqs,
1439
+ links: config.links,
1440
+ announcements: announcements(),
1441
+ announcementsSeenAt: seenAtOpen,
1442
+ modules: modules()
1443
+ }),
1331
1444
  search: options.search,
1332
1445
  audienceIds: options.audienceIds,
1333
1446
  loadArticle: options.loadArticle,
@@ -1351,8 +1464,13 @@ function renderResourceCenter(options) {
1351
1464
  panel.hidden = !v;
1352
1465
  launcher.setAttribute("aria-expanded", String(v));
1353
1466
  if (v) {
1467
+ seenAtOpen = options.announcementsSeenAt?.() ?? null;
1354
1468
  help.render();
1355
1469
  help.focusSearch();
1470
+ if (announcements().length) {
1471
+ options.onAnnouncementsSeen?.();
1472
+ updateBadge();
1473
+ }
1356
1474
  options.onOpen?.();
1357
1475
  } else {
1358
1476
  help.reset();
@@ -1429,7 +1547,7 @@ function startAuthoring(options) {
1429
1547
  outline.hidden = true;
1430
1548
  return;
1431
1549
  }
1432
- const r = el2.getBoundingClientRect();
1550
+ const r = visualRect(el2);
1433
1551
  outline.hidden = false;
1434
1552
  outline.classList.toggle("g-picked", picked);
1435
1553
  Object.assign(outline.style, { left: `${r.left}px`, top: `${r.top}px`, width: `${r.width}px`, height: `${r.height}px` });
@@ -1437,9 +1555,9 @@ function startAuthoring(options) {
1437
1555
  label.style.top = r.top < 30 ? "100%" : "-24px";
1438
1556
  };
1439
1557
  const elementAt = (x, y) => {
1440
- const el2 = doc.elementFromPoint(x, y);
1558
+ const el2 = deepElementFromPoint(doc, x, y);
1441
1559
  if (!el2 || isGuidelyElement(el2) || el2 === doc.documentElement || el2 === doc.body) return null;
1442
- return el2;
1560
+ return componentAnchor(el2);
1443
1561
  };
1444
1562
  const onMove = (e) => {
1445
1563
  if (!picking || stopped) return;
@@ -1454,7 +1572,7 @@ function startAuthoring(options) {
1454
1572
  };
1455
1573
  const onClick = (e) => {
1456
1574
  if (!picking || stopped) return;
1457
- if (e.target && isGuidelyElement(e.target)) return;
1575
+ if (isGuidelyElement(composedTarget(e))) return;
1458
1576
  const el2 = elementAt(e.clientX, e.clientY) ?? hovered;
1459
1577
  if (!el2) return;
1460
1578
  e.preventDefault();
@@ -1467,8 +1585,7 @@ function startAuthoring(options) {
1467
1585
  };
1468
1586
  const swallow = (e) => {
1469
1587
  if (!picking || stopped) return;
1470
- const t = e.target;
1471
- if (t && isGuidelyElement(t)) return;
1588
+ if (isGuidelyElement(composedTarget(e))) return;
1472
1589
  e.preventDefault();
1473
1590
  e.stopPropagation();
1474
1591
  };
@@ -1539,17 +1656,15 @@ function highlightSelectors(selectors, options = {}) {
1539
1656
  const scope = createScope(null, { theme: "light", animation: "none" }, doc);
1540
1657
  const boxes = [];
1541
1658
  const counts = [];
1542
- let resolved = null;
1659
+ const resolved = resolveTarget(selectors, doc);
1543
1660
  const matched = /* @__PURE__ */ new Set();
1544
1661
  selectors.forEach((s) => {
1545
1662
  const els = querySelectorStrategy(s, doc).filter((el2) => !isGuidelyElement(el2));
1546
- const visible = els.filter(isVisible);
1547
1663
  counts.push(els.length);
1548
- if (!resolved && visible.length === 1) resolved = visible[0];
1549
1664
  els.forEach((el2) => matched.add(el2));
1550
1665
  });
1551
1666
  matched.forEach((el2) => {
1552
- const r = el2.getBoundingClientRect();
1667
+ const r = visualRect(el2);
1553
1668
  const box = doc.createElement("div");
1554
1669
  box.className = "g-highlight";
1555
1670
  Object.assign(box.style, { left: `${r.left}px`, top: `${r.top}px`, width: `${r.width}px`, height: `${r.height}px` });
@@ -1569,12 +1684,19 @@ function highlightSelectors(selectors, options = {}) {
1569
1684
  export {
1570
1685
  DEFAULT_THEME,
1571
1686
  HOST_ID,
1687
+ SHADOW_SEPARATOR,
1572
1688
  STYLES,
1573
1689
  TargetTimeoutError,
1574
1690
  anchor,
1575
1691
  applyTheme,
1576
1692
  buildCard,
1693
+ closestComposed,
1694
+ componentAnchor,
1695
+ composedParent,
1696
+ composedTarget,
1697
+ containsComposed,
1577
1698
  createScope,
1699
+ deepElementFromPoint,
1578
1700
  describeElement,
1579
1701
  destroyHost,
1580
1702
  enhancePdfViewers,
@@ -1585,6 +1707,7 @@ export {
1585
1707
  isChecklistComplete,
1586
1708
  isGuidelyElement,
1587
1709
  isQuestionVisible,
1710
+ isStableClass,
1588
1711
  isStableId,
1589
1712
  isVisible,
1590
1713
  normalizeText,
@@ -1608,10 +1731,12 @@ export {
1608
1731
  sanitizeHtml,
1609
1732
  sanitizeToFragment,
1610
1733
  setSanitizedHtml,
1734
+ shadowHostChain,
1611
1735
  startAuthoring,
1612
1736
  textSnapshotOf,
1613
1737
  trackRect,
1614
1738
  urlMatches,
1739
+ visualRect,
1615
1740
  waitForTarget
1616
1741
  };
1617
1742
  //# sourceMappingURL=index.js.map