@guidelytech/guide-runtime 0.10.0 → 0.12.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.cjs CHANGED
@@ -44,9 +44,10 @@ __export(src_exports, {
44
44
  generateSelectors: () => generateSelectors,
45
45
  highlightSelectors: () => highlightSelectors,
46
46
  isAnchoredStep: () => isAnchoredStep,
47
- isChecklistComplete: () => import_runtime6.isChecklistComplete,
47
+ isChecklistComplete: () => import_runtime7.isChecklistComplete,
48
48
  isGuidelyElement: () => isGuidelyElement,
49
- isQuestionVisible: () => import_runtime7.isQuestionVisible,
49
+ isQuestionVisible: () => import_runtime8.isQuestionVisible,
50
+ isStableClass: () => isStableClass,
50
51
  isStableId: () => isStableId,
51
52
  isVisible: () => isVisible,
52
53
  normalizeText: () => normalizeText,
@@ -619,6 +620,9 @@ function setSanitizedHtml(el2, html) {
619
620
  }
620
621
 
621
622
  // src/pdf-viewer.ts
623
+ var import_runtime5 = require("@guidelytech/shared/runtime");
624
+
625
+ // src/host.ts
622
626
  var import_runtime4 = require("@guidelytech/shared/runtime");
623
627
 
624
628
  // src/styles.ts
@@ -804,6 +808,21 @@ var STYLES = `
804
808
  .g-auth-outline.g-picked .g-auth-label{background:#16a34a}
805
809
  .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}
806
810
  @media (max-width:480px){.g-card.g-modal,.g-panel{width:calc(100vw - 32px)}.g-inner{padding:16px}}
811
+ /* resource center: unread badge + "What's new" */
812
+ .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)}
813
+ .g-rc-news{padding:10px 20px 12px;border-bottom:1px solid var(--g-border)}
814
+ .g-rc-news:last-of-type{border-bottom:0}
815
+ .g-rc-news-head{display:flex;align-items:baseline;gap:8px}
816
+ .g-rc-news-title{margin:0;font-size:14px;font-weight:600;line-height:1.35;flex:1}
817
+ .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}
818
+ .g-rc-news-date{display:block;font-size:12px;color:var(--g-muted);margin:2px 0 6px}
819
+ .g-rc-news-body{font-size:13px}
820
+ .g-rc-news-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}
821
+ .g-rc-news-actions .g-btn{min-height:32px;padding:6px 12px;font-size:13px}
822
+ /* accessibility */
823
+ .g-sr-only{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
824
+ .g-hotspot:focus-visible,.g-custom:focus-visible,.g-card:focus-visible{outline:2px solid var(--g-primary);outline-offset:2px}
825
+ @media (forced-colors:active){.g-card,.g-panel,.g-banner{border:1px solid CanvasText}.g-btn{border:1px solid ButtonText}}
807
826
  `;
808
827
 
809
828
  // src/host.ts
@@ -898,6 +917,7 @@ function resolveTheme(theme, appearance) {
898
917
  }
899
918
  function applyTheme(el2, vars) {
900
919
  el2.style.setProperty("--g-primary", vars.primary);
920
+ if (/^#[0-9a-f]{3}([0-9a-f]{3})?$/i.test(vars.primary)) el2.style.setProperty("--g-primary-text", (0, import_runtime4.readableTextOn)(vars.primary));
901
921
  el2.style.setProperty("--g-bg", vars.bg);
902
922
  el2.style.setProperty("--g-text", vars.text);
903
923
  el2.style.setProperty("--g-radius", `${vars.radius}px`);
@@ -914,6 +934,16 @@ function createScope(theme, appearance, doc = document) {
914
934
  const vars = resolveTheme(theme, appearance);
915
935
  applyTheme(el2, vars);
916
936
  handle.shadow.appendChild(el2);
937
+ const css = [appearance ? theme?.customCss : void 0, appearance?.customCss].filter((c) => !!c && !!c.trim());
938
+ let style = null;
939
+ if (css.length) {
940
+ const id = `c${++cssScopeSeq}`;
941
+ el2.setAttribute("data-g-css", id);
942
+ style = doc.createElement("style");
943
+ style.setAttribute("data-g-custom", id);
944
+ style.textContent = css.map((c) => (0, import_runtime4.scopeCustomCss)(c, `.g-scope[data-g-css="${id}"]`)).join("\n");
945
+ handle.shadow.insertBefore(style, el2);
946
+ }
917
947
  let mq = null;
918
948
  const onChange = () => applyTheme(el2, resolveTheme(theme, appearance));
919
949
  if ((appearance?.theme ?? "auto") === "auto" && typeof window !== "undefined" && typeof window.matchMedia === "function") {
@@ -929,16 +959,18 @@ function createScope(theme, appearance, doc = document) {
929
959
  vars,
930
960
  destroy() {
931
961
  mq?.removeEventListener?.("change", onChange);
962
+ style?.remove();
932
963
  el2.remove();
933
964
  }
934
965
  };
935
966
  }
967
+ var cssScopeSeq = 0;
936
968
 
937
969
  // src/pdf-viewer.ts
938
970
  function enhancePdfViewers(root, doc = root.ownerDocument) {
939
- for (const block of Array.from(root.querySelectorAll(`div.${import_runtime4.PDF_BLOCK_CLASS}`))) {
940
- const link = block.querySelector(`a.${import_runtime4.FILE_LINK_CLASS}`);
941
- const strip = block.querySelector(`.${import_runtime4.PDF_PAGES_CLASS}`);
971
+ for (const block of Array.from(root.querySelectorAll(`div.${import_runtime5.PDF_BLOCK_CLASS}`))) {
972
+ const link = block.querySelector(`a.${import_runtime5.FILE_LINK_CLASS}`);
973
+ const strip = block.querySelector(`.${import_runtime5.PDF_PAGES_CLASS}`);
942
974
  if (!link || !strip || block.hasAttribute("data-g-ready")) continue;
943
975
  for (const img of Array.from(strip.querySelectorAll("img:not([src])"))) img.remove();
944
976
  const pages = Array.from(strip.querySelectorAll("img"));
@@ -1191,7 +1223,7 @@ function buildCard(doc, opts, extraClass) {
1191
1223
  if (step.imageUrl) {
1192
1224
  const img = h(doc, "img", "g-image");
1193
1225
  img.src = step.imageUrl;
1194
- img.alt = "";
1226
+ img.alt = step.imageAlt ?? "";
1195
1227
  card.appendChild(img);
1196
1228
  }
1197
1229
  const inner = h(doc, "div", "g-inner");
@@ -1219,18 +1251,29 @@ function buildCard(doc, opts, extraClass) {
1219
1251
  const buttons = buildButtons(doc, step, opts.canGoBack ?? opts.index > 0, opts.onButton);
1220
1252
  if (buttons.length || step.showProgress && opts.total > 1) {
1221
1253
  const footer = h(doc, "div", "g-footer");
1222
- if (step.showProgress && opts.total > 1) footer.appendChild(h(doc, "span", "g-progress", `${opts.index + 1} of ${opts.total}`));
1254
+ if (step.showProgress && opts.total > 1) {
1255
+ const progress = h(doc, "span", "g-progress", `${opts.index + 1} of ${opts.total}`);
1256
+ progress.setAttribute("aria-label", `Step ${opts.index + 1} of ${opts.total}`);
1257
+ footer.appendChild(progress);
1258
+ }
1223
1259
  buttons.forEach((b) => footer.appendChild(b));
1224
1260
  inner.appendChild(footer);
1225
1261
  }
1226
1262
  card.appendChild(inner);
1227
1263
  if (step.title) card.setAttribute("aria-labelledby", titleId);
1264
+ else card.setAttribute("aria-label", opts.total > 1 ? `Step ${opts.index + 1} of ${opts.total}` : "Guide");
1228
1265
  if (step.body) card.setAttribute("aria-describedby", bodyId);
1229
1266
  return { card, inner, titleId, bodyId };
1230
1267
  }
1231
1268
  function focusables(root) {
1232
1269
  return Array.from(root.querySelectorAll(FOCUSABLE));
1233
1270
  }
1271
+ function isTextEntry(el2) {
1272
+ const t = el2;
1273
+ if (!t || !t.tagName) return false;
1274
+ const tag = t.tagName.toLowerCase();
1275
+ return tag === "input" || tag === "textarea" || tag === "select" || t.isContentEditable === true;
1276
+ }
1234
1277
  function installKeyboard(card, step, trap, onClose) {
1235
1278
  const onKey = (e) => {
1236
1279
  if (e.key === "Escape" && step.showClose) {
@@ -1238,6 +1281,16 @@ function installKeyboard(card, step, trap, onClose) {
1238
1281
  onClose();
1239
1282
  return;
1240
1283
  }
1284
+ if ((e.key === "ArrowRight" || e.key === "ArrowLeft") && !e.altKey && !e.ctrlKey && !e.metaKey && !isTextEntry(e.target)) {
1285
+ const actions = e.key === "ArrowRight" ? ["next", "complete"] : ["back"];
1286
+ const btn = actions.map((a) => card.querySelector(`button[data-action="${a}"]`)).find(Boolean);
1287
+ if (btn) {
1288
+ e.preventDefault();
1289
+ e.stopPropagation();
1290
+ btn.click();
1291
+ }
1292
+ return;
1293
+ }
1241
1294
  if (trap && e.key === "Tab") {
1242
1295
  const items = focusables(card);
1243
1296
  if (!items.length) {
@@ -1262,7 +1315,8 @@ function installKeyboard(card, step, trap, onClose) {
1262
1315
  }
1263
1316
  function focusInto(card) {
1264
1317
  const doc = card.ownerDocument;
1265
- const previous = doc.activeElement;
1318
+ const active = doc.activeElement;
1319
+ const previous = active && active.id !== "guidely-root" ? active : null;
1266
1320
  const primary = card.querySelector(".g-btn-primary") || focusables(card)[0] || card;
1267
1321
  try {
1268
1322
  primary.focus({ preventScroll: true });
@@ -1410,23 +1464,32 @@ function renderHotspot(doc, opts) {
1410
1464
  let card = null;
1411
1465
  let stopAnchor = null;
1412
1466
  let stopKeys = null;
1413
- const close = () => {
1467
+ const close = (returnFocus = false) => {
1468
+ const hadFocus = !!card && card.getRootNode().activeElement !== null && card.contains(card.getRootNode().activeElement);
1414
1469
  stopAnchor?.();
1415
1470
  stopKeys?.();
1416
1471
  card?.remove();
1417
1472
  card = null;
1418
1473
  dot.setAttribute("aria-expanded", "false");
1474
+ if (returnFocus || hadFocus) {
1475
+ try {
1476
+ dot.focus({ preventScroll: true });
1477
+ } catch {
1478
+ }
1479
+ }
1419
1480
  };
1420
1481
  const open = () => {
1421
1482
  if (card) return;
1422
1483
  const built = buildCard(doc, { ...opts, onClose: () => opts.onClose() }, "g-hotspot-card");
1423
1484
  card = built.card;
1424
1485
  card.setAttribute("role", "dialog");
1486
+ card.id = nextId("hotspot");
1487
+ dot.setAttribute("aria-controls", card.id);
1425
1488
  const arrowEl = h(doc, "div", "g-arrow");
1426
1489
  card.appendChild(arrowEl);
1427
1490
  wrapper.appendChild(card);
1428
1491
  stopAnchor = anchor(target, card, { placement: opts.step.placement, offset: opts.step.offset, arrowEl });
1429
- stopKeys = installKeyboard(card, opts.step, false, close);
1492
+ stopKeys = installKeyboard(card, opts.step, false, () => close(true));
1430
1493
  dot.setAttribute("aria-expanded", "true");
1431
1494
  opts.onOpen?.();
1432
1495
  if (cfg.openOn === "click") focusInto(card);
@@ -1448,7 +1511,8 @@ function renderBanner(doc, opts) {
1448
1511
  const cleanups = [];
1449
1512
  const pos = step.position === "top" || step.placement === "top" || step.position.startsWith("top") ? "top" : "bottom";
1450
1513
  const banner = h(doc, "div", `g-banner g-pos-${pos}`);
1451
- banner.setAttribute("role", "status");
1514
+ banner.setAttribute("role", "region");
1515
+ banner.setAttribute("aria-label", step.title || "Announcement");
1452
1516
  banner.setAttribute("data-step-id", step.id);
1453
1517
  const body = h(doc, "div", "g-body");
1454
1518
  if (step.title) body.appendChild(h(doc, "strong", "", `${step.title} `));
@@ -1481,6 +1545,27 @@ function renderSlideout(doc, opts) {
1481
1545
  cleanups.push(focusInto(card));
1482
1546
  return { el: wrapper, card, destroy: () => cleanupAll(cleanups, wrapper) };
1483
1547
  }
1548
+ function runCodeBlock(code, container, opts) {
1549
+ const act = (action) => () => opts.onButton({ id: `code-${action}`, label: action, action, variant: "primary" });
1550
+ const api = Object.freeze({ next: act("next"), back: act("back"), complete: act("complete"), skip: act("skip"), dismiss: () => opts.onClose(), stepId: opts.step.id });
1551
+ try {
1552
+ const fn = new Function("guidely", "container", `"use strict";
1553
+ ${code}`);
1554
+ const result = fn(api, container);
1555
+ return typeof result === "function" ? () => {
1556
+ try {
1557
+ result();
1558
+ } catch (err) {
1559
+ opts.onError?.(err);
1560
+ }
1561
+ } : () => {
1562
+ };
1563
+ } catch (err) {
1564
+ opts.onError?.(err);
1565
+ return () => {
1566
+ };
1567
+ }
1568
+ }
1484
1569
  function renderCustom(doc, opts) {
1485
1570
  const cleanups = [];
1486
1571
  const wrapper = h(doc, "div", "g-step");
@@ -1489,6 +1574,9 @@ function renderCustom(doc, opts) {
1489
1574
  el2.tabIndex = -1;
1490
1575
  el2.setAttribute("data-step-id", opts.step.id);
1491
1576
  setSanitizedHtml(el2, opts.step.customHtml || opts.step.body || "");
1577
+ el2.setAttribute("role", opts.step.showOverlay ? "dialog" : "group");
1578
+ el2.setAttribute("aria-label", opts.step.title || "Guide");
1579
+ if (opts.step.showOverlay) el2.setAttribute("aria-modal", "true");
1492
1580
  const onClick = (e) => {
1493
1581
  const t = e.target?.closest?.("[data-g-action]");
1494
1582
  const action = t?.getAttribute("data-g-action");
@@ -1502,6 +1590,7 @@ function renderCustom(doc, opts) {
1502
1590
  opts.scope.appendChild(wrapper);
1503
1591
  cleanups.push(installKeyboard(el2, opts.step, false, opts.onClose));
1504
1592
  cleanups.push(focusInto(el2));
1593
+ if (opts.runCode && opts.step.code) cleanups.push(runCodeBlock(opts.step.code, el2, opts));
1505
1594
  return { el: wrapper, card: el2, destroy: () => cleanupAll(cleanups, wrapper) };
1506
1595
  }
1507
1596
  function renderStep(opts) {
@@ -1534,7 +1623,7 @@ function isAnchoredStep(step) {
1534
1623
  }
1535
1624
 
1536
1625
  // src/controller.ts
1537
- var import_runtime5 = require("@guidelytech/shared/runtime");
1626
+ var import_runtime6 = require("@guidelytech/shared/runtime");
1538
1627
  function noop() {
1539
1628
  }
1540
1629
  var escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
@@ -1599,6 +1688,17 @@ function runGuide(guide, options = {}) {
1599
1688
  view = null;
1600
1689
  stepDone = false;
1601
1690
  };
1691
+ let returnFocusTo = null;
1692
+ const restoreFocus = () => {
1693
+ const el2 = returnFocusTo;
1694
+ returnFocusTo = null;
1695
+ try {
1696
+ const now = doc.activeElement;
1697
+ const lost = !now || now === doc.body || now.id === "guidely-root";
1698
+ if (el2 && lost && el2.isConnected && typeof el2.focus === "function") el2.focus({ preventScroll: true });
1699
+ } catch {
1700
+ }
1701
+ };
1602
1702
  const end = (reason) => {
1603
1703
  if (ended) return;
1604
1704
  ended = true;
@@ -1610,6 +1710,7 @@ function runGuide(guide, options = {}) {
1610
1710
  else if (reason === "skipped") emit("guide_skipped", step, index);
1611
1711
  scope?.destroy();
1612
1712
  scope = null;
1713
+ restoreFocus();
1613
1714
  try {
1614
1715
  options.onEnd?.(reason);
1615
1716
  } catch (err) {
@@ -1627,7 +1728,7 @@ function runGuide(guide, options = {}) {
1627
1728
  const ctx = getCtx();
1628
1729
  if (step && ctx) {
1629
1730
  for (const branch of step.branches) {
1630
- if ((0, import_runtime5.evaluateRules)(branch.when, ctx)) {
1731
+ if ((0, import_runtime6.evaluateRules)(branch.when, ctx)) {
1631
1732
  target = steps.findIndex((s) => s.id === branch.goTo);
1632
1733
  if (target >= 0) break;
1633
1734
  }
@@ -1748,7 +1849,9 @@ function runGuide(guide, options = {}) {
1748
1849
  asModal,
1749
1850
  onButton: handleButton,
1750
1851
  onClose: () => end("dismissed"),
1751
- announce
1852
+ announce,
1853
+ runCode: options.runCode,
1854
+ onError
1752
1855
  });
1753
1856
  } catch (err) {
1754
1857
  onError(err);
@@ -1818,7 +1921,7 @@ function runGuide(guide, options = {}) {
1818
1921
  index = i;
1819
1922
  const step = steps[i];
1820
1923
  const ctx = getCtx();
1821
- if (step.targeting && step.targeting.rules.length && ctx && !(0, import_runtime5.evaluateRules)(step.targeting, ctx)) {
1924
+ if (step.targeting && step.targeting.rules.length && ctx && !(0, import_runtime6.evaluateRules)(step.targeting, ctx)) {
1822
1925
  const nextIndex = i + direction;
1823
1926
  if (nextIndex >= total) return end("completed");
1824
1927
  if (nextIndex < 0) return;
@@ -1860,6 +1963,8 @@ function runGuide(guide, options = {}) {
1860
1963
  guide,
1861
1964
  start() {
1862
1965
  if (status !== "idle") return;
1966
+ const focused = doc.activeElement;
1967
+ if (focused && focused !== doc.body && focused.id !== "guidely-root") returnFocusTo = focused;
1863
1968
  emit("guide_viewed");
1864
1969
  if (total === 0) return end("completed");
1865
1970
  let startIndex = options.startAtStep ? steps.findIndex((s) => s.id === options.startAtStep) : 0;
@@ -1921,7 +2026,7 @@ function runGuide(guide, options = {}) {
1921
2026
  }
1922
2027
 
1923
2028
  // src/checklist.ts
1924
- var import_runtime6 = require("@guidelytech/shared/runtime");
2029
+ var import_runtime7 = require("@guidelytech/shared/runtime");
1925
2030
  function renderChecklist(options) {
1926
2031
  const { scope, config } = options;
1927
2032
  const doc = scope.ownerDocument;
@@ -1996,7 +2101,7 @@ function renderChecklist(options) {
1996
2101
  bar.setAttribute("aria-valuemax", "100");
1997
2102
  body.textContent = "";
1998
2103
  foot.textContent = "";
1999
- const complete = (0, import_runtime6.isChecklistComplete)(config, progress);
2104
+ const complete = (0, import_runtime7.isChecklistComplete)(config, progress);
2000
2105
  if (complete) {
2001
2106
  const wrap = doc.createElement("div");
2002
2107
  wrap.className = "g-complete";
@@ -2084,7 +2189,7 @@ function renderChecklist(options) {
2084
2189
  }
2085
2190
 
2086
2191
  // src/survey.ts
2087
- var import_runtime7 = require("@guidelytech/shared/runtime");
2192
+ var import_runtime8 = require("@guidelytech/shared/runtime");
2088
2193
  function renderSurvey(options) {
2089
2194
  const { scope, config } = options;
2090
2195
  const doc = scope.ownerDocument;
@@ -2276,14 +2381,14 @@ function renderSurvey(options) {
2276
2381
  config.questions.forEach((q) => {
2277
2382
  const el2 = questionEls.get(q.id);
2278
2383
  if (!el2) return;
2279
- const visible = (0, import_runtime7.isQuestionVisible)(q, answers);
2384
+ const visible = (0, import_runtime8.isQuestionVisible)(q, answers);
2280
2385
  el2.hidden = !visible;
2281
2386
  if (!visible && answers[q.id] !== void 0) delete answers[q.id];
2282
2387
  });
2283
2388
  }
2284
2389
  form.addEventListener("submit", (e) => {
2285
2390
  e.preventDefault();
2286
- const missing = config.questions.filter((q) => q.required && (0, import_runtime7.isQuestionVisible)(q, answers) && answers[q.id] === void 0);
2391
+ const missing = config.questions.filter((q) => q.required && (0, import_runtime8.isQuestionVisible)(q, answers) && answers[q.id] === void 0);
2287
2392
  if (missing.length) {
2288
2393
  errorEl.textContent = `Please answer: ${missing.map((q) => q.title).join(", ")}`;
2289
2394
  questionEls.get(missing[0].id)?.querySelector("button,textarea")?.focus();
@@ -2331,8 +2436,11 @@ function renderSurvey(options) {
2331
2436
  };
2332
2437
  }
2333
2438
 
2439
+ // src/resource-center.ts
2440
+ var import_runtime10 = require("@guidelytech/shared/runtime");
2441
+
2334
2442
  // src/help-view.ts
2335
- var import_runtime8 = require("@guidelytech/shared/runtime");
2443
+ var import_runtime9 = require("@guidelytech/shared/runtime");
2336
2444
  var SHOW_ME_KINDS = /* @__PURE__ */ new Set(["flow", "modal", "tooltip", "announcement"]);
2337
2445
  function startableGuide(guides, keyOrId) {
2338
2446
  if (!keyOrId || !guides) return null;
@@ -2351,8 +2459,8 @@ function resultRow(doc, icon, title, summary, onOpen, showMe) {
2351
2459
  return row;
2352
2460
  }
2353
2461
  function enhanceShowMe(root, doc, canStart, start) {
2354
- for (const a of Array.from(root.querySelectorAll(`a[href^="${import_runtime8.SHOW_ME_HREF_PREFIX}"]`))) {
2355
- const key = (0, import_runtime8.showMeKeyFromHref)(a.getAttribute("href"));
2462
+ for (const a of Array.from(root.querySelectorAll(`a[href^="${import_runtime9.SHOW_ME_HREF_PREFIX}"]`))) {
2463
+ const key = (0, import_runtime9.showMeKeyFromHref)(a.getAttribute("href"));
2356
2464
  const holder = a.closest("p.gd-show-me") ?? a;
2357
2465
  if (!key || !canStart(key)) {
2358
2466
  if (holder !== a) holder.remove();
@@ -2467,10 +2575,11 @@ function createHelpView(options) {
2467
2575
  if (active) return renderArticle(active);
2468
2576
  const data = options.data();
2469
2577
  const member = options.audienceIds?.() ?? [];
2470
- const sections = (0, import_runtime8.resourceCenterSections)(data.collections, data.articles, member);
2578
+ searchWrap.hidden = !!data.modules && !data.modules.includes("search");
2579
+ const sections = (0, import_runtime9.resourceCenterSections)(data.collections, data.articles, member);
2471
2580
  const q = query.trim().toLowerCase();
2472
2581
  if (q) {
2473
- const walkthroughs = (data.recommended ?? []).filter((g) => (0, import_runtime8.matchesHelpQuery)(g.name, q) && startableGuide(options.guides?.() ?? data.recommended, g.id));
2582
+ const walkthroughs = (data.recommended ?? []).filter((g) => (0, import_runtime9.matchesHelpQuery)(g.name, q) && startableGuide(options.guides?.() ?? data.recommended, g.id));
2474
2583
  if (walkthroughs.length) {
2475
2584
  section(`Walkthroughs (${walkthroughs.length})`);
2476
2585
  for (const g of walkthroughs) body.appendChild(resultRow(doc, "\u25B6", g.name, "Show me, step by step", () => options.onStartGuide?.(g.id)));
@@ -2479,10 +2588,10 @@ function createHelpView(options) {
2479
2588
  body.append(...options.extra?.(q) ?? []);
2480
2589
  if (q) {
2481
2590
  const visible = sections.flatMap((s) => s.articles);
2482
- const local = visible.filter((a) => (0, import_runtime8.matchesHelpQuery)(`${a.title} ${a.summary}`, q));
2591
+ const local = visible.filter((a) => (0, import_runtime9.matchesHelpQuery)(`${a.title} ${a.summary}`, q));
2483
2592
  const seen = new Set(local.map((a) => a.id));
2484
2593
  const hidden = new Set(data.articles.map((a) => a.id).filter((id) => !visible.some((v) => v.id === id)));
2485
- const merged = [...local, ...serverResults.filter((a) => !seen.has(a.id) && !hidden.has(a.id) && (0, import_runtime8.isArticleListed)(data.collections, a, member))];
2594
+ const merged = [...local, ...serverResults.filter((a) => !seen.has(a.id) && !hidden.has(a.id) && (0, import_runtime9.isArticleListed)(data.collections, a, member))];
2486
2595
  section(`Results (${merged.length})`);
2487
2596
  if (!merged.length) body.appendChild(el(doc, "div", "g-rc-empty", "No articles match your search."));
2488
2597
  merged.forEach(articleButton);
@@ -2497,55 +2606,124 @@ function createHelpView(options) {
2497
2606
  body.appendChild(b);
2498
2607
  });
2499
2608
  }
2500
- if (data.recommended?.length) {
2501
- section("Recommended");
2502
- data.recommended.forEach((g) => {
2503
- const b = listItem(doc, "\u25B6", g.name, "Start guide");
2504
- b.addEventListener("click", () => options.onStartGuide?.(g.id));
2505
- body.appendChild(b);
2506
- });
2507
- }
2508
- if (data.checklists?.length) {
2509
- section("Checklists");
2510
- data.checklists.forEach((g) => {
2511
- const b = listItem(doc, "\u2611", g.name, "Open checklist");
2512
- b.addEventListener("click", () => options.onOpenChecklist?.(g.id));
2513
- body.appendChild(b);
2514
- });
2515
- }
2516
- for (const s of sections) {
2517
- section(s.collection ? s.path.join(" \u203A ") : "Articles", s.depth);
2518
- s.articles.forEach(articleButton);
2519
- }
2520
- if (data.faqs?.length) {
2521
- section("FAQ");
2522
- data.faqs.forEach((f) => {
2523
- const b = listItem(doc, "?", f.question);
2524
- b.setAttribute("aria-expanded", "false");
2525
- const a = el(doc, "div", "g-faq-a");
2526
- setSanitizedHtml(a, f.answer);
2527
- a.hidden = true;
2528
- b.addEventListener("click", () => {
2529
- a.hidden = !a.hidden;
2530
- b.setAttribute("aria-expanded", String(!a.hidden));
2609
+ const renderers = {
2610
+ search: () => {
2611
+ },
2612
+ announcements: () => {
2613
+ if (!data.announcements?.length) return;
2614
+ section(import_runtime9.RC_MODULE_LABELS.announcements);
2615
+ const seen = data.announcementsSeenAt ? Date.parse(data.announcementsSeenAt) : NaN;
2616
+ data.announcements.slice(0, 20).forEach((n) => body.appendChild(announcementItem(n, Number.isNaN(seen) || Date.parse(n.publishedAt) > seen)));
2617
+ },
2618
+ guides: () => {
2619
+ if (!data.recommended?.length) return;
2620
+ section("Recommended");
2621
+ data.recommended.forEach((g) => {
2622
+ const b = listItem(doc, "\u25B6", g.name, "Start guide");
2623
+ b.addEventListener("click", () => options.onStartGuide?.(g.id));
2624
+ body.appendChild(b);
2531
2625
  });
2532
- body.append(b, a);
2533
- });
2626
+ },
2627
+ checklists: () => {
2628
+ if (!data.checklists?.length) return;
2629
+ section("Checklists");
2630
+ data.checklists.forEach((g) => {
2631
+ const b = listItem(doc, "\u2611", g.name, "Open checklist");
2632
+ b.addEventListener("click", () => options.onOpenChecklist?.(g.id));
2633
+ body.appendChild(b);
2634
+ });
2635
+ },
2636
+ articles: () => {
2637
+ for (const s of sections) {
2638
+ section(s.collection ? s.path.join(" \u203A ") : "Articles", s.depth);
2639
+ s.articles.forEach(articleButton);
2640
+ }
2641
+ },
2642
+ faqs: () => {
2643
+ if (!data.faqs?.length) return;
2644
+ section("FAQ");
2645
+ data.faqs.forEach((f) => {
2646
+ const b = listItem(doc, "?", f.question);
2647
+ b.setAttribute("aria-expanded", "false");
2648
+ const a = el(doc, "div", "g-faq-a");
2649
+ a.id = `g-faq-${f.id}`;
2650
+ b.setAttribute("aria-controls", a.id);
2651
+ setSanitizedHtml(a, f.answer);
2652
+ a.hidden = true;
2653
+ b.addEventListener("click", () => {
2654
+ a.hidden = !a.hidden;
2655
+ b.setAttribute("aria-expanded", String(!a.hidden));
2656
+ });
2657
+ body.append(b, a);
2658
+ });
2659
+ },
2660
+ links: () => {
2661
+ if (!data.links?.length) return;
2662
+ section("Links");
2663
+ data.links.forEach((l) => {
2664
+ const a = el(doc, "a", "g-rc-item");
2665
+ a.href = l.url;
2666
+ a.target = "_blank";
2667
+ a.rel = "noopener noreferrer";
2668
+ const i = el(doc, "span", "g-rc-icon", "\u2197");
2669
+ i.setAttribute("aria-hidden", "true");
2670
+ a.append(i, el(doc, "span", "g-rc-title", l.label));
2671
+ a.setAttribute("aria-label", `${l.label} (opens in a new tab)`);
2672
+ body.appendChild(a);
2673
+ });
2674
+ }
2675
+ };
2676
+ for (const id of data.modules ?? import_runtime9.RC_MODULE_IDS) renderers[id]?.();
2677
+ if (!body.childNodes.length) body.appendChild(el(doc, "div", "g-rc-empty", "Nothing here yet."));
2678
+ }
2679
+ function announcementItem(n, unread) {
2680
+ const item = el(doc, "article", `g-rc-news${unread ? " g-rc-news-unread" : ""}`);
2681
+ const head = el(doc, "div", "g-rc-news-head");
2682
+ const title = el(doc, "h3", "g-rc-news-title", n.title);
2683
+ head.appendChild(title);
2684
+ if (unread) {
2685
+ const dot = el(doc, "span", "g-rc-news-new", "New");
2686
+ head.appendChild(dot);
2687
+ }
2688
+ const date = el(doc, "time", "g-rc-news-date");
2689
+ date.dateTime = n.publishedAt;
2690
+ try {
2691
+ date.textContent = new Date(n.publishedAt).toLocaleDateString(void 0, { year: "numeric", month: "short", day: "numeric" });
2692
+ } catch {
2693
+ date.textContent = n.publishedAt.slice(0, 10);
2694
+ }
2695
+ item.append(head, date);
2696
+ if (n.body) {
2697
+ const b = el(doc, "div", "g-body g-rc-news-body");
2698
+ setSanitizedHtml(b, n.body);
2699
+ item.appendChild(b);
2700
+ }
2701
+ const actions = el(doc, "div", "g-rc-news-actions");
2702
+ const guide = n.guideId ? startableGuide(options.guides?.(), n.guideId) : null;
2703
+ if (guide) {
2704
+ const b = el(doc, "button", "g-btn g-btn-primary", n.linkLabel || "Show me");
2705
+ b.type = "button";
2706
+ b.addEventListener("click", () => options.onShowMe ? options.onShowMe(guide.key) : options.onStartGuide?.(guide.id));
2707
+ actions.appendChild(b);
2708
+ }
2709
+ if (n.articleId) {
2710
+ const listed = options.data().articles.find((a) => a.id === n.articleId);
2711
+ if (listed) {
2712
+ const b = el(doc, "button", "g-btn g-btn-secondary", guide ? "Read more" : n.linkLabel || "Read more");
2713
+ b.type = "button";
2714
+ b.addEventListener("click", () => openArticle(listed));
2715
+ actions.appendChild(b);
2716
+ }
2534
2717
  }
2535
- if (data.links?.length) {
2536
- section("Links");
2537
- data.links.forEach((l) => {
2538
- const a = el(doc, "a", "g-rc-item");
2539
- a.href = l.url;
2540
- a.target = "_blank";
2541
- a.rel = "noopener noreferrer";
2542
- const i = el(doc, "span", "g-rc-icon", "\u2197");
2543
- i.setAttribute("aria-hidden", "true");
2544
- a.append(i, el(doc, "span", "g-rc-title", l.label));
2545
- body.appendChild(a);
2546
- });
2718
+ if (n.url && /^https?:\/\//i.test(n.url)) {
2719
+ const a = el(doc, "a", "g-btn g-btn-link", !guide && !n.articleId && n.linkLabel ? n.linkLabel : "Learn more");
2720
+ a.href = n.url;
2721
+ a.target = "_blank";
2722
+ a.rel = "noopener noreferrer";
2723
+ actions.appendChild(a);
2547
2724
  }
2548
- if (!body.childNodes.length) body.appendChild(el(doc, "div", "g-rc-empty", "Nothing here yet."));
2725
+ if (actions.childNodes.length) item.appendChild(actions);
2726
+ return item;
2549
2727
  }
2550
2728
  let debounce = null;
2551
2729
  const onInput = () => {
@@ -2617,6 +2795,19 @@ function renderResourceCenter(options) {
2617
2795
  launcher.appendChild(el(doc, "span", "", config.launcher.label || config.title));
2618
2796
  launcher.setAttribute("aria-label", config.launcher.label || config.title);
2619
2797
  if (options.hideLauncher) launcher.hidden = true;
2798
+ const badge = el(doc, "span", "g-launcher-badge");
2799
+ badge.hidden = true;
2800
+ launcher.appendChild(badge);
2801
+ const modules = () => (0, import_runtime10.orderedModules)(config.modules).filter((m) => m.enabled && (!("targeting" in m) || !m.targeting || !options.evaluate || options.evaluate(m.targeting))).map((m) => m.id);
2802
+ const announcements = () => modules().includes("announcements") ? (0, import_runtime10.visibleAnnouncements)(config.announcements, options.audienceIds?.() ?? []) : [];
2803
+ const updateBadge = () => {
2804
+ const unread = (0, import_runtime10.unreadAnnouncements)(announcements(), options.announcementsSeenAt?.());
2805
+ badge.hidden = unread === 0;
2806
+ badge.textContent = unread > 9 ? "9+" : String(unread);
2807
+ const label = config.launcher.label || config.title;
2808
+ launcher.setAttribute("aria-label", unread ? `${label} (${unread} new update${unread === 1 ? "" : "s"})` : label);
2809
+ };
2810
+ updateBadge();
2620
2811
  const panel = el(doc, "div", `g-panel ${pos}`);
2621
2812
  panel.setAttribute("role", "dialog");
2622
2813
  panel.setAttribute("aria-label", config.title);
@@ -2627,9 +2818,20 @@ function renderResourceCenter(options) {
2627
2818
  close.type = "button";
2628
2819
  close.setAttribute("aria-label", "Close");
2629
2820
  head.appendChild(close);
2821
+ let seenAtOpen = options.announcementsSeenAt?.() ?? null;
2630
2822
  const help = createHelpView({
2631
2823
  doc,
2632
- data: () => ({ collections: config.collections, articles: config.articles, recommended: byId(config.recommendedGuideIds), checklists: byId(config.checklistGuideIds), faqs: config.faqs, links: config.links }),
2824
+ data: () => ({
2825
+ collections: config.collections,
2826
+ articles: config.articles,
2827
+ recommended: byId(config.recommendedGuideIds),
2828
+ checklists: byId(config.checklistGuideIds),
2829
+ faqs: config.faqs,
2830
+ links: config.links,
2831
+ announcements: announcements(),
2832
+ announcementsSeenAt: seenAtOpen,
2833
+ modules: modules()
2834
+ }),
2633
2835
  search: options.search,
2634
2836
  audienceIds: options.audienceIds,
2635
2837
  loadArticle: options.loadArticle,
@@ -2653,8 +2855,13 @@ function renderResourceCenter(options) {
2653
2855
  panel.hidden = !v;
2654
2856
  launcher.setAttribute("aria-expanded", String(v));
2655
2857
  if (v) {
2858
+ seenAtOpen = options.announcementsSeenAt?.() ?? null;
2656
2859
  help.render();
2657
2860
  help.focusSearch();
2861
+ if (announcements().length) {
2862
+ options.onAnnouncementsSeen?.();
2863
+ updateBadge();
2864
+ }
2658
2865
  options.onOpen?.();
2659
2866
  } else {
2660
2867
  help.reset();
@@ -2892,6 +3099,7 @@ function highlightSelectors(selectors, options = {}) {
2892
3099
  isChecklistComplete,
2893
3100
  isGuidelyElement,
2894
3101
  isQuestionVisible,
3102
+ isStableClass,
2895
3103
  isStableId,
2896
3104
  isVisible,
2897
3105
  normalizeText,