@planningcenter/topbar 12.3.0 → 12.4.0-rc.1

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.
@@ -27,7 +27,7 @@
27
27
  var people__namespace = /*#__PURE__*/_interopNamespaceDefault(people);
28
28
  var toolbar__namespace = /*#__PURE__*/_interopNamespaceDefault(toolbar);
29
29
 
30
- var styles$l = {"accessibility-toolbar":"accessibility-toolbar-jgjUG"};
30
+ var styles$m = {"accessibility-toolbar":"accessibility-toolbar-jgjUG"};
31
31
 
32
32
  const AccessibilityToolbar = ({
33
33
  children
@@ -39,7 +39,7 @@
39
39
  (_a = document.getElementById("main-content")) === null || _a === void 0 ? void 0 : _a.focus();
40
40
  };
41
41
  return /*#__PURE__*/React.createElement("div", {
42
- className: styles$l["accessibility-toolbar"]
42
+ className: styles$m["accessibility-toolbar"]
43
43
  }, /*#__PURE__*/React.createElement(tapestry.Link, {
44
44
  href: "#main-content",
45
45
  kind: "primary",
@@ -166,12 +166,14 @@
166
166
  }
167
167
  function apiRequest(fetch, url, {
168
168
  method = "GET",
169
- data = null
169
+ data = null,
170
+ headers = {}
170
171
  } = {}) {
171
172
  const options = {
172
173
  headers: {
173
174
  Accept: "application/json",
174
- "Content-Type": "application/json"
175
+ "Content-Type": "application/json",
176
+ ...headers
175
177
  },
176
178
  method,
177
179
  redirect: "follow"
@@ -356,11 +358,11 @@
356
358
  fill: fill
357
359
  })));
358
360
 
359
- var styles$k = {"announcement":"announcement-pI5tM","announcement--platform":"announcement--platform-EcUn2","announcement--promo":"announcement--promo-FarUk","announcement-icon":"announcement-icon-Ov39-","announcement-content":"announcement-content-q2ad6","dismiss":"dismiss-wUAoU","dismiss--promo":"dismiss--promo-7zvO4"};
361
+ var styles$l = {"announcement":"announcement-pI5tM","announcement--platform":"announcement--platform-EcUn2","announcement--promo":"announcement--promo-FarUk","announcement-icon":"announcement-icon-Ov39-","announcement-content":"announcement-content-q2ad6","dismiss":"dismiss-wUAoU","dismiss--promo":"dismiss--promo-7zvO4"};
360
362
 
361
363
  function AnnouncementIcon() {
362
364
  return /*#__PURE__*/React.createElement("svg", {
363
- className: styles$k["announcement-icon"],
365
+ className: styles$l["announcement-icon"],
364
366
  width: "16",
365
367
  height: "16",
366
368
  viewBox: "0 0 16 16",
@@ -485,7 +487,7 @@
485
487
  } = this.props;
486
488
  const isPromo = variant === "promo";
487
489
  return /*#__PURE__*/React.createElement("div", {
488
- className: classnames(styles$k["announcement"], styles$k[`announcement--${variant !== null && variant !== void 0 ? variant : "platform"}`]),
490
+ className: classnames(styles$l["announcement"], styles$l[`announcement--${variant !== null && variant !== void 0 ? variant : "platform"}`]),
489
491
  "data-announcement-variant": variant !== null && variant !== void 0 ? variant : "platform",
490
492
  style: isPromo ? {
491
493
  backgroundImage: promoBackgroundUrl,
@@ -494,7 +496,7 @@
494
496
  backgroundSize: "cover"
495
497
  } : undefined
496
498
  }, !isPromo && /*#__PURE__*/React.createElement(AnnouncementIcon, null), /*#__PURE__*/React.createElement("div", {
497
- className: styles$k["announcement-content"],
499
+ className: styles$l["announcement-content"],
498
500
  style: style,
499
501
  ...platformProps
500
502
  }));
@@ -543,7 +545,7 @@
543
545
  const isPromo = variant === "promo";
544
546
  return /*#__PURE__*/React.createElement("button", {
545
547
  type: "button",
546
- className: classnames(styles$k.dismiss, isPromo && styles$k["dismiss--promo"]),
548
+ className: classnames(styles$l.dismiss, isPromo && styles$l["dismiss--promo"]),
547
549
  ...props
548
550
  }, /*#__PURE__*/React.createElement(X, {
549
551
  fill: isPromo ? "currentColor" : "white",
@@ -623,9 +625,9 @@
623
625
  ...platformProps
624
626
  } = this.props;
625
627
  return /*#__PURE__*/React.createElement("div", {
626
- className: styles$k["announcement"]
628
+ className: styles$l["announcement"]
627
629
  }, /*#__PURE__*/React.createElement(AnnouncementIcon, null), /*#__PURE__*/React.createElement("div", {
628
- className: styles$k["announcement-content"],
630
+ className: styles$l["announcement-content"],
629
631
  style: style,
630
632
  ...platformProps
631
633
  }));
@@ -636,7 +638,7 @@
636
638
  }) {
637
639
  return /*#__PURE__*/React.createElement("button", {
638
640
  type: "button",
639
- className: styles$k.dismiss,
641
+ className: styles$l.dismiss,
640
642
  ...props
641
643
  }, /*#__PURE__*/React.createElement(X, {
642
644
  fill: "white",
@@ -791,7 +793,7 @@
791
793
  return matches;
792
794
  }
793
795
 
794
- const capitalize = s => s.charAt(0).toUpperCase() + s.slice(1);
796
+ const capitalize$1 = s => s.charAt(0).toUpperCase() + s.slice(1);
795
797
  const RESOLVED_COLOR_MODE_ATTRIBUTE = "data-resolved-color-mode";
796
798
  function resolveColorMode(colorMode) {
797
799
  if (colorMode === "system") {
@@ -1758,7 +1760,7 @@
1758
1760
  })) : (/*#__PURE__*/React.createElement("div", null, "appName not supported")));
1759
1761
  }
1760
1762
 
1761
- var styles$j = {"icon-slot":"icon-slot-m3ZIk","name":"name-UNqbW"};
1763
+ var styles$k = {"icon-slot":"icon-slot-m3ZIk","name":"name-UNqbW"};
1762
1764
 
1763
1765
  const AppRow = ({
1764
1766
  appName,
@@ -1767,24 +1769,24 @@
1767
1769
  }) => {
1768
1770
  if (appName === "Accounts") {
1769
1771
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
1770
- className: styles$j["icon-slot"]
1772
+ className: styles$k["icon-slot"]
1771
1773
  }, /*#__PURE__*/React.createElement(MonoAppIcon, {
1772
1774
  appName: "Accounts",
1773
1775
  colors: colors,
1774
1776
  size: iconSize
1775
1777
  })), /*#__PURE__*/React.createElement("span", {
1776
- className: styles$j.name
1778
+ className: styles$k.name
1777
1779
  }, "Account settings"));
1778
1780
  }
1779
1781
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(ColorAppIcon, {
1780
1782
  appName: appName.replace(/[\s-]/, ""),
1781
1783
  size: iconSize
1782
1784
  }), /*#__PURE__*/React.createElement("span", {
1783
- className: styles$j.name
1785
+ className: styles$k.name
1784
1786
  }, appName));
1785
1787
  };
1786
1788
 
1787
- var styles$i = {"button":"button-XMbeQ","label":"label-884of","chevron":"chevron-RRg9k"};
1789
+ var styles$j = {"button":"button-XMbeQ","label":"label-884of","chevron":"chevron-RRg9k"};
1788
1790
 
1789
1791
  const AppsButton = /*#__PURE__*/React.forwardRef(({
1790
1792
  appName,
@@ -1796,10 +1798,10 @@
1796
1798
  ...restProps,
1797
1799
  ref: ref,
1798
1800
  "aria-label": `App switcher: ${appName}`,
1799
- className: styles$i.button,
1801
+ className: styles$j.button,
1800
1802
  kind: "neutral-inline",
1801
1803
  label: /*#__PURE__*/React.createElement("span", {
1802
- className: styles$i.label
1804
+ className: styles$j.label
1803
1805
  }, isAccounts ? "Account settings" : appName),
1804
1806
  prefix: isAccounts ? (/*#__PURE__*/React.createElement(MonoAppIcon, {
1805
1807
  appName: "Accounts",
@@ -1810,13 +1812,13 @@
1810
1812
  size: 22
1811
1813
  })),
1812
1814
  suffix: /*#__PURE__*/React.createElement(DisclosureChevronIcon, {
1813
- className: `${styles$i.chevron} symbol`
1815
+ className: `${styles$j.chevron} symbol`
1814
1816
  })
1815
1817
  });
1816
1818
  });
1817
1819
  AppsButton.displayName = "AppsButton";
1818
1820
 
1819
- var styles$h = {"nav":"nav-y-6ML","popover-content":"popover-content-jOAAY","list":"list-jUxvp","link":"link-LMp2h","current":"current-4U4VL","accounts-item":"accounts-item-erJW0","accounts-link":"accounts-link-4jwPW"};
1821
+ var styles$i = {"nav":"nav-y-6ML","popover-content":"popover-content-jOAAY","list":"list-jUxvp","link":"link-LMp2h","current":"current-4U4VL","accounts-item":"accounts-item-erJW0","accounts-link":"accounts-link-4jwPW"};
1820
1822
 
1821
1823
  const AppsMenu = ({
1822
1824
  appName,
@@ -1837,8 +1839,8 @@
1837
1839
  }, /*#__PURE__*/React.createElement("a", {
1838
1840
  "aria-current": isCurrent ? "page" : undefined,
1839
1841
  "aria-label": name,
1840
- className: classnames(styles$h.link, {
1841
- [styles$h.current]: isCurrent
1842
+ className: classnames(styles$i.link, {
1843
+ [styles$i.current]: isCurrent
1842
1844
  }),
1843
1845
  "data-pendo": `app-switcher--change-app--${name}`,
1844
1846
  "data-turbolinks": false,
@@ -1854,22 +1856,22 @@
1854
1856
  colors: colors
1855
1857
  })), /*#__PURE__*/React.createElement(tapestry.PopoverContent, {
1856
1858
  "aria-label": "Switch app",
1857
- className: styles$h["popover-content"],
1859
+ className: styles$i["popover-content"],
1858
1860
  placement: "bottom start"
1859
1861
  }, /*#__PURE__*/React.createElement("nav", {
1860
- className: styles$h.nav,
1862
+ className: styles$i.nav,
1861
1863
  "data-pendo": "app-switcher"
1862
1864
  }, /*#__PURE__*/React.createElement("ul", {
1863
- className: styles$h.list,
1865
+ className: styles$i.list,
1864
1866
  role: "list"
1865
1867
  }, homeProduct && renderProductLink(homeProduct.name), restProducts.map(({
1866
1868
  name
1867
1869
  }) => renderProductLink(name)), accountsProduct && (/*#__PURE__*/React.createElement("li", {
1868
- className: styles$h["accounts-item"]
1870
+ className: styles$i["accounts-item"]
1869
1871
  }, /*#__PURE__*/React.createElement("a", {
1870
1872
  "aria-current": appName === accountsProduct.name ? "page" : undefined,
1871
1873
  "aria-label": "Account settings",
1872
- className: classnames(styles$h.link, styles$h["accounts-link"]),
1874
+ className: classnames(styles$i.link, styles$i["accounts-link"]),
1873
1875
  "data-pendo": "app-switcher--change-app--Accounts",
1874
1876
  "data-turbolinks": false,
1875
1877
  href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(accountsProduct.name)
@@ -2077,7 +2079,7 @@
2077
2079
  })) : (/*#__PURE__*/React.createElement("div", null, "appName not supported"));
2078
2080
  }
2079
2081
 
2080
- var styles$g = {"unbutton":"unbutton-lEbTR"};
2082
+ var styles$h = {"unbutton":"unbutton-lEbTR"};
2081
2083
 
2082
2084
  function Unbutton({
2083
2085
  className,
@@ -2086,7 +2088,7 @@
2086
2088
  ...nativeProps
2087
2089
  }) {
2088
2090
  return /*#__PURE__*/React.createElement("button", {
2089
- className: classnames(styles$g.unbutton, className),
2091
+ className: classnames(styles$h.unbutton, className),
2090
2092
  style: {
2091
2093
  ...style
2092
2094
  },
@@ -2095,7 +2097,7 @@
2095
2097
  });
2096
2098
  }
2097
2099
 
2098
- var styles$f = {"button":"button-wsipc","button-accounts":"button-accounts-zQmBM","chevron":"chevron-obyBF","is-expanded":"is-expanded-PoCQ9"};
2100
+ var styles$g = {"button":"button-wsipc","button-accounts":"button-accounts-zQmBM","chevron":"chevron-obyBF","is-expanded":"is-expanded-PoCQ9"};
2099
2101
 
2100
2102
  const LegacyAppsButton = ({
2101
2103
  appName,
@@ -2110,8 +2112,8 @@
2110
2112
  "aria-haspopup": "true",
2111
2113
  "aria-label": `App switcher: ${appName}`,
2112
2114
  onClick: onClick,
2113
- className: classnames(styles$f.button, {
2114
- [styles$f["is-expanded"]]: expanded
2115
+ className: classnames(styles$g.button, {
2116
+ [styles$g["is-expanded"]]: expanded
2115
2117
  }),
2116
2118
  type: "button",
2117
2119
  style: style,
@@ -2120,7 +2122,7 @@
2120
2122
  appName: "Accounts",
2121
2123
  colors: colors
2122
2124
  }), /*#__PURE__*/React.createElement("div", {
2123
- className: styles$f["button-accounts"]
2125
+ className: styles$g["button-accounts"]
2124
2126
  }, "Account settings"))) : (/*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(ColorAppIcon, {
2125
2127
  appName: appName.replace(/[\s-]/, "")
2126
2128
  }), /*#__PURE__*/React.createElement(MonoAppName, {
@@ -2128,7 +2130,7 @@
2128
2130
  key: "mono-app-name",
2129
2131
  size: 24
2130
2132
  }))), /*#__PURE__*/React.createElement(DisclosureChevronIcon, {
2131
- className: styles$f.chevron,
2133
+ className: styles$g.chevron,
2132
2134
  key: "disclosure-chevron-icon"
2133
2135
  }));
2134
2136
  };
@@ -2261,7 +2263,7 @@
2261
2263
  }
2262
2264
  }
2263
2265
 
2264
- var styles$e = {"popup-root":"popup-root-SeL5u","popup":"popup-nQXYW","is-visible":"is-visible-3DAUP"};
2266
+ var styles$f = {"popup-root":"popup-root-SeL5u","popup":"popup-nQXYW","is-visible":"is-visible-3DAUP"};
2265
2267
 
2266
2268
  const Popup = ({
2267
2269
  className,
@@ -2269,8 +2271,8 @@
2269
2271
  isVisible,
2270
2272
  ...nativeProps
2271
2273
  }) => (/*#__PURE__*/React.createElement(C, {
2272
- className: classnames(styles$e.popup, {
2273
- [styles$e["is-visible"]]: isVisible
2274
+ className: classnames(styles$f.popup, {
2275
+ [styles$f["is-visible"]]: isVisible
2274
2276
  }, className),
2275
2277
  ...nativeProps
2276
2278
  }));
@@ -2278,11 +2280,11 @@
2278
2280
  const PopupRoot = ({
2279
2281
  ...nativeProps
2280
2282
  }) => (/*#__PURE__*/React.createElement("div", {
2281
- className: styles$e["popup-root"],
2283
+ className: styles$f["popup-root"],
2282
2284
  ...nativeProps
2283
2285
  }));
2284
2286
 
2285
- var styles$d = {"popup":"popup-bVhSa"};
2287
+ var styles$e = {"popup":"popup-bVhSa"};
2286
2288
 
2287
2289
  const LegacyAppsMenu = ({
2288
2290
  appName,
@@ -2320,7 +2322,7 @@
2320
2322
  cleanup: !visible,
2321
2323
  component: Outsider,
2322
2324
  onOutsideClick: toggle,
2323
- className: styles$d.popup,
2325
+ className: styles$e.popup,
2324
2326
  isVisible: visible
2325
2327
  }, /*#__PURE__*/React.createElement("menu", {
2326
2328
  ref: menuRef,
@@ -3233,27 +3235,34 @@ true;
3233
3235
  const isMac = () => /Mac|iPhone|iPad|iPod/.test(navigator.platform);
3234
3236
  const hasCommandKey = event => isMac() ? event.metaKey : event.ctrlKey;
3235
3237
  const commandKeyLabel = () => isMac() ? "⌘" : "Ctrl+";
3238
+ const isSearchShortcut = event => event.key.toLowerCase() === "k" && !event.altKey && !event.shiftKey && hasCommandKey(event);
3239
+ // Products opened their own search with `/` before this one replaced it.
3240
+ const isLegacySearchShortcut = event => event.key === "/" && !event.altKey && !event.ctrlKey && !event.metaKey && !isTypingIn(event.target);
3241
+ const isCopyShortcut = event => event.key.toLowerCase() === "c" && !event.altKey && !event.shiftKey && hasCommandKey(event);
3242
+ const isTypingIn = target => target instanceof Element && target.closest('input, select, textarea, [contenteditable]:not([contenteditable="false"])') !== null;
3243
+ const isTypingKey = event => event.key.length === 1 && event.key !== " " && !event.altKey && !event.ctrlKey && !event.metaKey;
3244
+ const verticalStep = key => ({
3245
+ ArrowDown: 1,
3246
+ ArrowUp: -1
3247
+ })[key];
3248
+ const horizontalStep = key => ({
3249
+ ArrowLeft: -1,
3250
+ ArrowRight: 1
3251
+ })[key];
3236
3252
 
3237
- const STORAGE_KEY = "topbar.product_search.last_product";
3238
- function readLastProduct() {
3239
- try {
3240
- return localStorage.getItem(STORAGE_KEY);
3241
- } catch {
3242
- return null;
3243
- }
3244
- }
3245
- function saveLastProduct(slug) {
3246
- try {
3247
- localStorage.setItem(STORAGE_KEY, slug);
3248
- } catch {
3249
- // Storage can be full or blocked; search still works without it.
3250
- }
3253
+ function SearchButton({
3254
+ onClick
3255
+ }) {
3256
+ return /*#__PURE__*/React.createElement("button", {
3257
+ "aria-keyshortcuts": `${isMac() ? "Meta+K" : "Control+K"} /`,
3258
+ "aria-label": "Search",
3259
+ onClick: onClick,
3260
+ type: "button"
3261
+ }, /*#__PURE__*/React.createElement(Spyglass, {
3262
+ fill: "currentColor"
3263
+ }));
3251
3264
  }
3252
3265
 
3253
- var styles$c = {"visuallyHidden":"visuallyHidden-C28ju"};
3254
-
3255
- var styles$b = {"tab":"tab-lQLDx"};
3256
-
3257
3266
  // useId isn't available on React 17, which our peer range still supports.
3258
3267
  let instanceCount$1 = 0;
3259
3268
  function useInstanceId(prefix) {
@@ -3261,173 +3270,33 @@ true;
3261
3270
  return id;
3262
3271
  }
3263
3272
 
3264
- function ProductTabs({
3265
- children,
3266
- onKeyDown,
3267
- onSelect,
3268
- products,
3269
- selected
3270
- }) {
3271
- const id = useInstanceId("product-tabs");
3272
- const tabId = slug => `${id}-tab-${slug}`;
3273
- const tabRefs = React.useRef(new Map());
3274
- const handleKeyDown = event => {
3275
- var _a;
3276
- const offset = {
3277
- ArrowLeft: -1,
3278
- ArrowRight: 1
3279
- }[event.key];
3280
- if (offset) {
3281
- event.preventDefault();
3282
- const index = products.indexOf(selected);
3283
- const next = products[(index + offset + products.length) % products.length];
3284
- onSelect(next.slug);
3285
- (_a = tabRefs.current.get(next.slug)) === null || _a === void 0 ? void 0 : _a.focus();
3286
- } else {
3287
- onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
3288
- }
3289
- };
3290
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
3291
- "aria-label": "Product",
3292
- onKeyDown: handleKeyDown,
3293
- role: "tablist"
3294
- }, products.map(product => (/*#__PURE__*/React.createElement("button", {
3295
- "aria-controls": `${id}-panel`,
3296
- "aria-selected": product === selected,
3297
- className: styles$b.tab,
3298
- id: tabId(product.slug),
3299
- key: product.slug,
3300
- onClick: () => onSelect(product.slug),
3301
- ref: element => {
3302
- if (element) {
3303
- tabRefs.current.set(product.slug, element);
3304
- } else {
3305
- tabRefs.current.delete(product.slug);
3306
- }
3307
- },
3308
- role: "tab",
3309
- tabIndex: product === selected ? 0 : -1,
3310
- type: "button"
3311
- }, product.appName)))), /*#__PURE__*/React.createElement("div", {
3312
- "aria-labelledby": tabId(selected.slug),
3313
- id: `${id}-panel`,
3314
- role: "tabpanel"
3315
- }, children));
3316
- }
3317
-
3318
- var styles$a = {"result":"result-WCnxr"};
3319
-
3320
- function SearchResult({
3321
- highlighted,
3322
- id,
3323
- onHighlight,
3324
- result
3325
- }) {
3326
- const {
3327
- metadata,
3328
- title,
3329
- url
3330
- } = result.attributes;
3331
- const ref = React.useRef(null);
3332
- React.useEffect(() => {
3333
- var _a;
3334
- if (highlighted) (_a = ref.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({
3335
- block: "nearest"
3336
- });
3337
- }, [highlighted]);
3338
- // Focus stays in the search input, which points at the highlighted result
3339
- // with aria-activedescendant, so results are kept out of the tab order.
3340
- // Mouse move rather than enter: arrowing scrolls results under a resting
3341
- // pointer, and that shouldn't steal the highlight.
3342
- return /*#__PURE__*/React.createElement("a", {
3343
- "aria-selected": highlighted,
3344
- className: styles$a.result,
3345
- href: url,
3346
- id: id,
3347
- onMouseMove: onHighlight,
3348
- ref: ref,
3349
- role: "option",
3350
- tabIndex: -1
3351
- }, /*#__PURE__*/React.createElement("div", null, title), /*#__PURE__*/React.createElement("div", null, summarize(metadata)));
3352
- }
3353
- function summarize(metadata) {
3354
- return Object.values(metadata !== null && metadata !== void 0 ? metadata : {}).filter(value => typeof value === "string" && value !== "").join(" · ");
3355
- }
3356
-
3357
- const resultId = (listboxId, index) => `${listboxId}-result-${index}`;
3358
- function SearchResults({
3359
- search,
3360
- term,
3361
- ...navigation
3362
- }) {
3363
- var _a;
3364
- switch (search.status) {
3365
- case "idle":
3366
- return null;
3367
- case "loading":
3368
- if ((_a = search.groups) === null || _a === void 0 ? void 0 : _a.length) {
3369
- return /*#__PURE__*/React.createElement(ResultGroups, {
3370
- busy: true,
3371
- groups: search.groups,
3372
- ...navigation
3373
- });
3374
- }
3375
- return /*#__PURE__*/React.createElement("p", {
3376
- role: "status"
3377
- }, "Searching\u2026");
3378
- case "error":
3379
- return /*#__PURE__*/React.createElement("p", {
3380
- role: "alert"
3381
- }, "Search isn\u2019t working right now. Try again.");
3382
- case "done":
3383
- if (search.groups.length === 0) {
3384
- return /*#__PURE__*/React.createElement("p", {
3385
- role: "status"
3386
- }, "No results for \u201C", term.trim(), "\u201D");
3387
- }
3388
- return /*#__PURE__*/React.createElement(ResultGroups, {
3389
- groups: search.groups,
3390
- ...navigation
3391
- });
3273
+ // The versions home-mobile pins, so web and mobile read the same response
3274
+ // shapes. Some endpoints only exist from a version on: Registrations' me,
3275
+ // product_search_results, and pcrl_lookups need 2025-05-01.
3276
+ const API_VERSIONS = {
3277
+ calendar: "2022-07-07",
3278
+ "check-ins": "2025-05-28",
3279
+ giving: "2019-10-18",
3280
+ groups: "2023-07-10",
3281
+ home: "2018-08-01",
3282
+ people: "2023-03-21",
3283
+ publishing: "2024-03-25",
3284
+ registrations: "2025-05-01",
3285
+ services: "2018-11-01"
3286
+ };
3287
+ // For a `/~api/{product}/v2/...` URL, the header that pins its product's
3288
+ // version.
3289
+ function apiVersionHeaders(url) {
3290
+ const [,, product] = new URL(url).pathname.split("/");
3291
+ const version = API_VERSIONS[product];
3292
+ if (version) {
3293
+ return {
3294
+ "X-PCO-API-Version": version
3295
+ };
3296
+ } else {
3297
+ return {};
3392
3298
  }
3393
3299
  }
3394
- function ResultGroups({
3395
- busy = false,
3396
- groups,
3397
- highlightedIndex,
3398
- id,
3399
- label,
3400
- onHighlight
3401
- }) {
3402
- let index = 0;
3403
- return /*#__PURE__*/React.createElement("div", {
3404
- "aria-busy": busy,
3405
- "aria-label": label,
3406
- id: id,
3407
- role: "listbox"
3408
- }, groups.map(({
3409
- results,
3410
- type
3411
- }, groupIndex) => {
3412
- const headingId = `${id}-group-${groupIndex}`;
3413
- return /*#__PURE__*/React.createElement("div", {
3414
- "aria-labelledby": headingId,
3415
- key: type,
3416
- role: "group"
3417
- }, /*#__PURE__*/React.createElement("div", {
3418
- id: headingId
3419
- }, type), results.map(result => {
3420
- const resultIndex = index++;
3421
- return /*#__PURE__*/React.createElement(SearchResult, {
3422
- highlighted: resultIndex === highlightedIndex,
3423
- id: resultId(id, resultIndex),
3424
- key: result.id,
3425
- onHighlight: () => onHighlight(resultIndex),
3426
- result: result
3427
- });
3428
- }));
3429
- }));
3430
- }
3431
3300
 
3432
3301
  const CACHE_TTL_MS = 60000;
3433
3302
  const MAX_ENTRIES$1 = 50;
@@ -3444,7 +3313,9 @@ true;
3444
3313
  }
3445
3314
  }
3446
3315
  function fetchIntoCache(url) {
3447
- const promise = apiRequest(defaultFetch, url).then(({
3316
+ const promise = apiRequest(defaultFetch, url, {
3317
+ headers: apiVersionHeaders(url)
3318
+ }).then(({
3448
3319
  json,
3449
3320
  ok
3450
3321
  }) => {
@@ -3500,44 +3371,52 @@ true;
3500
3371
  };
3501
3372
  }
3502
3373
  }
3503
-
3504
- const SEARCH_DEBOUNCE_MS = 300;
3505
- function useProductSearch(product, term) {
3506
- const query = term.trim();
3507
- const debouncedQuery = useDebouncedValue(query, SEARCH_DEBOUNCE_MS);
3508
- const request = useCachedRequest(debouncedQuery ? productSearchUrl(product, debouncedQuery) : null);
3509
- const response = "data" in request ? request.data : undefined;
3510
- const settledProduct = useSettledProduct(product, request.status);
3511
- // While a request loads, the previous response stays on screen, but only
3512
- // if it was for this product: People results must never show under the
3513
- // Services tab.
3514
- const visibleResponse = request.status === "done" || settledProduct === product ? response : undefined;
3515
- const groups = React.useMemo(() => visibleResponse && groupByType(visibleResponse), [visibleResponse]);
3516
- if (!query) {
3517
- return {
3518
- status: "idle"
3519
- };
3520
- } else if (query !== debouncedQuery) {
3521
- return {
3522
- groups,
3523
- status: "loading"
3524
- };
3525
- } else if (request.status === "done") {
3526
- return {
3527
- groups: groups !== null && groups !== void 0 ? groups : [],
3528
- status: "done"
3529
- };
3530
- } else if (request.status === "error") {
3531
- return {
3532
- status: "error"
3533
- };
3534
- } else {
3535
- return {
3536
- groups,
3537
- status: "loading"
3374
+ // One request per URL, for callers that fan out to a varying number of
3375
+ // endpoints. Unlike `useCachedRequest`, a URL that hasn't settled yet has no
3376
+ // earlier data to fall back on.
3377
+ function useCachedRequests(urls) {
3378
+ const [settled, setSettled] = React.useState({});
3379
+ // Keyed by value because each settle re-renders the caller, and a caller
3380
+ // that builds a fresh `urls` array on render would otherwise loop forever.
3381
+ const key = urls.join("\n");
3382
+ React.useEffect(() => {
3383
+ let current = true;
3384
+ const settle = result => current && setSettled(prev => ({
3385
+ ...prev,
3386
+ [result.url]: result
3387
+ }));
3388
+ for (const url of key.split("\n").filter(Boolean)) {
3389
+ cachedRequest(url).then(data => settle({
3390
+ data,
3391
+ url
3392
+ }), () => settle({
3393
+ failed: true,
3394
+ url
3395
+ }));
3396
+ }
3397
+ return () => {
3398
+ current = false;
3538
3399
  };
3539
- }
3400
+ }, [key]);
3401
+ return urls.map(url => {
3402
+ const result = settled[url];
3403
+ if (!result) {
3404
+ return {
3405
+ status: "loading"
3406
+ };
3407
+ } else if ("data" in result) {
3408
+ return {
3409
+ data: result.data,
3410
+ status: "done"
3411
+ };
3412
+ } else {
3413
+ return {
3414
+ status: "error"
3415
+ };
3416
+ }
3417
+ });
3540
3418
  }
3419
+
3541
3420
  function productSearchUrl(product, query) {
3542
3421
  const params = new URLSearchParams({
3543
3422
  per_page: "100",
@@ -3568,14 +3447,7 @@ true;
3568
3447
  };
3569
3448
  return [...groups.values()].sort((a, b) => rank(a.type) - rank(b.type));
3570
3449
  }
3571
- // The product the most recent finished request was for.
3572
- function useSettledProduct(product, status) {
3573
- const [settledProduct, setSettledProduct] = React.useState();
3574
- React.useEffect(() => {
3575
- if (status === "done") setSettledProduct(product);
3576
- }, [product, status]);
3577
- return settledProduct;
3578
- }
3450
+
3579
3451
  function useDebouncedValue(value, delay) {
3580
3452
  const [debounced, setDebounced] = React.useState(value);
3581
3453
  React.useEffect(() => {
@@ -3585,310 +3457,50 @@ true;
3585
3457
  return debounced;
3586
3458
  }
3587
3459
 
3588
- const COPIED_FEEDBACK_MS = 1500;
3589
- const verticalStep = key => ({
3590
- ArrowDown: 1,
3591
- ArrowUp: -1
3592
- })[key];
3593
- function useResultNavigation(results) {
3594
- // Remembering which results the index belongs to puts the highlight back on
3595
- // the first result whenever new ones arrive, without an effect to reset it.
3596
- const [highlight, setHighlight] = React.useState({
3597
- index: 0,
3598
- results
3599
- });
3600
- const highlightedIndex = highlight.results === results ? highlight.index : 0;
3601
- const highlighted = results[highlightedIndex];
3602
- const [copiedUrl, setCopiedUrl] = React.useState(null);
3603
- React.useEffect(() => {
3604
- if (copiedUrl) {
3605
- const timeout = setTimeout(() => setCopiedUrl(null), COPIED_FEEDBACK_MS);
3606
- return () => clearTimeout(timeout);
3607
- } else {
3608
- return undefined;
3609
- }
3610
- }, [copiedUrl]);
3611
- const highlightIndex = React.useCallback(index => setHighlight(current => current.results === results && current.index === index ? current : {
3612
- index,
3613
- results
3614
- }), [results]);
3615
- const handleKeyDown = event => {
3616
- const step = verticalStep(event.key);
3617
- if (step && results.length > 0) {
3618
- event.preventDefault();
3619
- highlightIndex((highlightedIndex + step + results.length) % results.length);
3620
- } else if (event.key === "Enter" && highlighted && !event.nativeEvent.isComposing) {
3621
- event.preventDefault();
3622
- open(highlighted.attributes.url, {
3623
- newTab: hasCommandKey(event)
3624
- });
3625
- } else if (isCopyShortcut(event) && highlighted && !hasSelectedText(event.currentTarget)) {
3626
- event.preventDefault();
3627
- const {
3628
- url
3629
- } = highlighted.attributes;
3630
- navigator.clipboard.writeText(url).then(() => setCopiedUrl(url));
3631
- }
3632
- };
3633
- return {
3634
- copied: copiedUrl !== null && copiedUrl === (highlighted === null || highlighted === void 0 ? void 0 : highlighted.attributes.url),
3635
- handleKeyDown,
3636
- highlightIndex,
3637
- highlighted,
3638
- highlightedIndex: highlighted ? highlightedIndex : null
3639
- };
3640
- }
3641
- function open(url, {
3642
- newTab
3643
- }) {
3644
- if (newTab) {
3645
- window.open(url, "_blank", "noopener");
3646
- } else {
3647
- window.location.assign(url);
3648
- }
3649
- }
3650
- const isCopyShortcut = event => event.key.toLowerCase() === "c" && !event.altKey && !event.shiftKey && hasCommandKey(event);
3651
- // With text selected, ⌘C should copy that text as usual, not the link.
3652
- function hasSelectedText(element) {
3653
- var _a, _b;
3654
- const selectedInField = element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
3655
- return selectedInField || ((_b = (_a = window.getSelection()) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : "") !== "";
3656
- }
3657
-
3658
- const SEARCHABLE_PRODUCTS = [{
3659
- appName: "Groups",
3660
- slug: "groups"
3661
- }, {
3662
- appName: "People",
3663
- slug: "people"
3664
- }, {
3665
- appName: "Services",
3666
- slug: "services"
3667
- }];
3668
- const PERMISSION_CHECKS = {
3669
- people: () => fetchMe("people", "people_permissions,site_administrator").then(({
3670
- people_permissions,
3671
- site_administrator
3672
- }) => site_administrator || people_permissions !== null),
3673
- services: () => fetchMe("services", "can_search").then(({
3674
- can_search
3675
- }) => can_search)
3676
- };
3677
- function fetchMe(product, fields) {
3678
- const params = new URLSearchParams({
3679
- "fields[Person]": fields
3680
- });
3681
- return apiRequest(defaultFetch, `${window.location.origin}/~api/${product}/v2/me?${params}`).then(({
3682
- json,
3683
- ok
3684
- }) => {
3685
- if (!ok || json === null) throw new Error("Permission check failed");
3686
- return json.data.attributes;
3687
- });
3688
- }
3689
- // Permissions rarely change mid-session, so a successful check is kept until
3690
- // the page reloads. A failed one is dropped so a remount tries again.
3691
- const permissionCache = new Map();
3692
- function canSearch(slug) {
3693
- const check = PERMISSION_CHECKS[slug];
3694
- const cached = permissionCache.get(slug);
3695
- if (!check) {
3696
- return Promise.resolve(true);
3697
- } else if (cached) {
3698
- return cached;
3699
- } else {
3700
- const promise = check().catch(() => {
3701
- permissionCache.delete(slug);
3702
- return false;
3703
- });
3704
- permissionCache.set(slug, promise);
3705
- return promise;
3706
- }
3707
- }
3708
- function useSearchableProducts(apps) {
3709
- // Keyed by name because providers hand over a fresh `apps` array on renders
3710
- // where nothing changed.
3711
- const accessibleKey = inSwitcherOrder(apps).map(({
3712
- slug
3713
- }) => slug).join(",");
3714
- const [settled, setSettled] = React.useState();
3715
- React.useEffect(() => {
3716
- let current = true;
3717
- const accessible = accessibleKey.split(",").flatMap(slug => SEARCHABLE_PRODUCTS.filter(product => product.slug === slug));
3718
- Promise.all(accessible.map(({
3719
- slug
3720
- }) => canSearch(slug))).then(allowed => {
3721
- if (current) {
3722
- setSettled({
3723
- accessibleKey,
3724
- products: accessible.filter((_, index) => allowed[index])
3725
- });
3726
- }
3727
- });
3728
- return () => {
3729
- current = false;
3460
+ const SEARCH_DEBOUNCE_MS = 300;
3461
+ function useProductSearch(product, term) {
3462
+ const query = term.trim();
3463
+ const debouncedQuery = useDebouncedValue(query, SEARCH_DEBOUNCE_MS);
3464
+ const request = useCachedRequest(debouncedQuery ? productSearchUrl(product, debouncedQuery) : null);
3465
+ const response = "data" in request ? request.data : undefined;
3466
+ const settledProduct = useSettledProduct(product, request.status);
3467
+ // While a request loads, the previous response stays on screen, but only
3468
+ // if it was for this product: People results must never show under the
3469
+ // Services tab.
3470
+ const visibleResponse = request.status === "done" || settledProduct === product ? response : undefined;
3471
+ const groups = React.useMemo(() => visibleResponse && groupByType(visibleResponse), [visibleResponse]);
3472
+ if (!query) {
3473
+ return {
3474
+ status: "idle"
3475
+ };
3476
+ } else if (query !== debouncedQuery) {
3477
+ return {
3478
+ groups,
3479
+ status: "loading"
3730
3480
  };
3731
- }, [accessibleKey]);
3732
- if ((settled === null || settled === void 0 ? void 0 : settled.accessibleKey) === accessibleKey) {
3481
+ } else if (request.status === "done") {
3733
3482
  return {
3734
- products: settled.products,
3483
+ groups: groups !== null && groups !== void 0 ? groups : [],
3735
3484
  status: "done"
3736
3485
  };
3486
+ } else if (request.status === "error") {
3487
+ return {
3488
+ status: "error"
3489
+ };
3737
3490
  } else {
3738
3491
  return {
3492
+ groups,
3739
3493
  status: "loading"
3740
3494
  };
3741
3495
  }
3742
3496
  }
3743
- function inSwitcherOrder(apps) {
3744
- return groupApps(apps).restProducts.flatMap(app => SEARCHABLE_PRODUCTS.filter(product => product.appName === app.name));
3745
- }
3746
-
3747
- const isSearchShortcut = event => event.key.toLowerCase() === "k" && !event.altKey && !event.shiftKey && hasCommandKey(event);
3748
- // Products opened their own search with `/` before this one replaced it.
3749
- const isLegacySearchShortcut = event => event.key === "/" && !event.altKey && !event.ctrlKey && !event.metaKey && !isTypingIn(event.target);
3750
- const isTypingKey = event => event.key.length === 1 && event.key !== " " && !event.altKey && !event.ctrlKey && !event.metaKey;
3751
- const isTypingIn = target => target instanceof Element && target.closest('input, select, textarea, [contenteditable]:not([contenteditable="false"])') !== null;
3752
- function ProductSearch({
3753
- apps
3754
- }) {
3755
- const searchable = useSearchableProducts(apps);
3756
- if (searchable.status === "done" && searchable.products.length > 0) {
3757
- return /*#__PURE__*/React.createElement(SearchButtonAndDialog, {
3758
- products: searchable.products
3759
- });
3760
- } else {
3761
- return null;
3762
- }
3763
- }
3764
- function SearchButtonAndDialog({
3765
- products
3766
- }) {
3767
- const [open, setOpen] = React.useState(false);
3768
- React.useEffect(() => {
3769
- // Rich text editors like Trix bind ⌘K to "insert link"; capturing at the
3770
- // window and stopping propagation keeps them from ever seeing it.
3771
- const handleKeyDown = event => {
3772
- if (isSearchShortcut(event)) {
3773
- event.preventDefault();
3774
- event.stopPropagation();
3775
- setOpen(wasOpen => !wasOpen);
3776
- } else if (isLegacySearchShortcut(event)) {
3777
- event.preventDefault();
3778
- event.stopPropagation();
3779
- setOpen(true);
3780
- }
3781
- };
3782
- window.addEventListener("keydown", handleKeyDown, {
3783
- capture: true
3784
- });
3785
- return () => window.removeEventListener("keydown", handleKeyDown, {
3786
- capture: true
3787
- });
3788
- }, []);
3789
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("button", {
3790
- "aria-keyshortcuts": `${isMac() ? "Meta+K" : "Control+K"} /`,
3791
- "aria-label": "Search",
3792
- onClick: () => setOpen(true),
3793
- type: "button"
3794
- }, /*#__PURE__*/React.createElement(Spyglass, {
3795
- fill: "currentColor"
3796
- })), /*#__PURE__*/React.createElement(tapestry.Modal, {
3797
- "aria-label": "Search",
3798
- dismissible: true,
3799
- onClose: () => setOpen(false),
3800
- open: open
3801
- }, /*#__PURE__*/React.createElement(SearchPanel, {
3802
- products: products
3803
- })));
3804
- }
3805
- function SearchPanel({
3806
- products
3807
- }) {
3808
- var _a, _b;
3809
- const listboxId = useInstanceId("product-search-results");
3810
- const [term, setTerm] = React.useState("");
3811
- const [selectedSlug, setSelectedSlug] = React.useState(() => initialProduct(products).slug);
3812
- const product = (_a = products.find(({
3813
- slug
3814
- }) => slug === selectedSlug)) !== null && _a !== void 0 ? _a : products[0];
3815
- const hasTerm = term.trim() !== "";
3816
- const search = useProductSearch(product.slug, term);
3817
- const groups = "groups" in search ? search.groups : undefined;
3818
- const results = React.useMemo(() => {
3819
- var _a;
3820
- return (_a = groups === null || groups === void 0 ? void 0 : groups.flatMap(group => group.results)) !== null && _a !== void 0 ? _a : [];
3821
- }, [groups]);
3822
- const navigation = useResultNavigation(results);
3823
- const {
3824
- highlightedIndex
3825
- } = navigation;
3826
- const inputRef = React.useRef(null);
3827
- const [announceHighlight, setAnnounceHighlight] = React.useState(false);
3828
- // The tabs keep focus while arrowing through results, but only the focused
3829
- // input can point screen readers at the highlight, so from the tabs it's
3830
- // announced instead. Typing goes back to the input, keystroke and all.
3831
- const handleTabsKeyDown = event => {
3832
- var _a;
3833
- if (isTypingKey(event)) {
3834
- (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
3835
- } else if (verticalStep(event.key)) {
3836
- setAnnounceHighlight(true);
3837
- navigation.handleKeyDown(event);
3838
- } else {
3839
- navigation.handleKeyDown(event);
3840
- }
3841
- };
3497
+ // The product the most recent finished request was for.
3498
+ function useSettledProduct(product, status) {
3499
+ const [settledProduct, setSettledProduct] = React.useState();
3842
3500
  React.useEffect(() => {
3843
- if (hasTerm) saveLastProduct(product.slug);
3844
- }, [hasTerm, product.slug]);
3845
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("input", {
3846
- "aria-activedescendant": highlightedIndex === null ? undefined : resultId(listboxId, highlightedIndex),
3847
- "aria-autocomplete": "list",
3848
- "aria-controls": results.length > 0 ? listboxId : undefined,
3849
- "aria-expanded": results.length > 0,
3850
- "aria-label": "Search",
3851
- autoFocus: true,
3852
- onChange: event => setTerm(event.target.value),
3853
- onFocus: () => setAnnounceHighlight(false),
3854
- onKeyDown: navigation.handleKeyDown,
3855
- placeholder: "Search",
3856
- ref: inputRef,
3857
- role: "combobox",
3858
- type: "search",
3859
- value: term
3860
- }), /*#__PURE__*/React.createElement(ProductTabs, {
3861
- onKeyDown: handleTabsKeyDown,
3862
- onSelect: setSelectedSlug,
3863
- products: products,
3864
- selected: product
3865
- }, /*#__PURE__*/React.createElement(SearchResults, {
3866
- highlightedIndex: highlightedIndex,
3867
- id: listboxId,
3868
- label: `${product.appName} results`,
3869
- onHighlight: navigation.highlightIndex,
3870
- search: search,
3871
- term: term
3872
- })), /*#__PURE__*/React.createElement("div", {
3873
- "aria-live": "polite",
3874
- className: styles$c.visuallyHidden
3875
- }, announceHighlight ? (_b = navigation.highlighted) === null || _b === void 0 ? void 0 : _b.attributes.title : null), /*#__PURE__*/React.createElement(KeyboardHints, {
3876
- copied: navigation.copied
3877
- }));
3878
- }
3879
- function KeyboardHints({
3880
- copied
3881
- }) {
3882
- const command = commandKeyLabel();
3883
- return /*#__PURE__*/React.createElement("p", null, "\u2191\u2193 Select \u00B7 \u21B5 Open \u00B7 ", command, "\u21B5 Open in new tab \u00B7 ", command, "C", " ", /*#__PURE__*/React.createElement("span", {
3884
- role: "status"
3885
- }, copied ? "Copied!" : "Copy link"), " \u00B7 ", command, "K Open search");
3886
- }
3887
- function initialProduct(products) {
3888
- var _a, _b, _c;
3889
- const lastProduct = readLastProduct();
3890
- const bySlug = slug => products.find(product => product.slug === slug);
3891
- return (_c = (_b = (_a = bySlug(lastProduct)) !== null && _a !== void 0 ? _a : bySlug("people")) !== null && _b !== void 0 ? _b : bySlug("services")) !== null && _c !== void 0 ? _c : products[0];
3501
+ if (status === "done") setSettledProduct(product);
3502
+ }, [product, status]);
3503
+ return settledProduct;
3892
3504
  }
3893
3505
 
3894
3506
  function parseValue(element) {
@@ -3957,10 +3569,14 @@ true;
3957
3569
  return shim.useSyncExternalStore(recentlyVisitableMetadataStore.subscribe, () => recentlyVisitableMetadataStore.getState().metadata);
3958
3570
  }
3959
3571
 
3960
- const MAX_ENTRIES = 20;
3961
- const FORMAT_VERSION = 1;
3572
+ // Entries match the ones home-mobile stores, so one list can sync across web
3573
+ // and mobile.
3574
+ const ENTRY_VERSION = 3;
3575
+ const MAX_ENTRIES = 50;
3962
3576
  const MAX_TEXT_LENGTH = 200;
3963
3577
  const PRODUCTS = ["accounts", "calendar", "check-ins", "giving", "groups", "home", "login", "people", "publishing", "registrations", "services"];
3578
+ // Login has no pages worth returning to.
3579
+ const APPS = PRODUCTS.filter(product => product !== "login");
3964
3580
  function isId(value) {
3965
3581
  return value !== "" && Array.from(value).every(character => character >= "0" && character <= "9");
3966
3582
  }
@@ -3969,169 +3585,1116 @@ true;
3969
3585
  const segments = value.split("/");
3970
3586
  return segments.length === 4 && PRODUCTS.some(product => product === segments[0]) && isId(segments[3]);
3971
3587
  }
3972
- function isNonBlankString(value) {
3973
- return typeof value === "string" && value.trim() !== "";
3588
+ function isApp(value) {
3589
+ return APPS.some(app => app === value);
3590
+ }
3591
+ // Links are built by resolving the path against its app's origin, so a stored
3592
+ // path must not be able to resolve anywhere else (`//evil.com`, `/\evil.com`).
3593
+ function isPath(value) {
3594
+ const origin = "https://app.invalid";
3595
+ try {
3596
+ return typeof value === "string" && value.startsWith("/") && new URL(value, origin).origin === origin;
3597
+ } catch {
3598
+ return false;
3599
+ }
3600
+ }
3601
+ function isNonBlankString(value) {
3602
+ return typeof value === "string" && value.trim() !== "";
3603
+ }
3604
+ function isOptionalString(value) {
3605
+ return value === undefined || typeof value === "string";
3606
+ }
3607
+ function isRecord(value) {
3608
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3609
+ }
3610
+ function isPageMetadata(value) {
3611
+ return value === undefined || isRecord(value) && isOptionalString(value.details) && isOptionalString(value.type);
3612
+ }
3613
+ // The query string is part of the link but not of the page's identity, so
3614
+ // revisiting with other filters moves the existing entry instead of adding one.
3615
+ function entryKey(entry) {
3616
+ if (entry.type === "resource") {
3617
+ return `resource:${entry.pcrl}`;
3618
+ } else {
3619
+ return `page:${entry.app}:${entry.path.split("?")[0]}`;
3620
+ }
3621
+ }
3622
+ function isEntry(value) {
3623
+ if (!isRecord(value) || value.version !== ENTRY_VERSION || typeof value.visitedAt !== "string") {
3624
+ return false;
3625
+ } else if (value.type === "resource") {
3626
+ return typeof value.pcrl === "string" && isPcrl(value.pcrl);
3627
+ } else if (value.type === "page") {
3628
+ return isApp(value.app) && isPath(value.path) && isNonBlankString(value.title) && isPageMetadata(value.metadata);
3629
+ } else {
3630
+ return false;
3631
+ }
3632
+ }
3633
+ // Cuts on code points so a surrogate pair is never split.
3634
+ function clampText(value) {
3635
+ return Array.from(value).slice(0, MAX_TEXT_LENGTH).join("");
3636
+ }
3637
+ function clampMetadata({
3638
+ details,
3639
+ type
3640
+ }) {
3641
+ return {
3642
+ ...(details && {
3643
+ details: clampText(details)
3644
+ }),
3645
+ ...(type && {
3646
+ type: clampText(type)
3647
+ })
3648
+ };
3649
+ }
3650
+ function clampVisit(visit) {
3651
+ if (visit.type === "resource") {
3652
+ return visit;
3653
+ } else {
3654
+ return {
3655
+ ...visit,
3656
+ ...(visit.metadata && {
3657
+ metadata: clampMetadata(visit.metadata)
3658
+ }),
3659
+ title: clampText(visit.title.trim())
3660
+ };
3661
+ }
3662
+ }
3663
+ function toEntry(visit, visitedAt) {
3664
+ return {
3665
+ ...clampVisit(visit),
3666
+ version: ENTRY_VERSION,
3667
+ visitedAt: visitedAt.toISOString()
3668
+ };
3669
+ }
3670
+ // Recents are disposable: anything unexpected reads as an empty list (or, for
3671
+ // a single bad entry, a list without it) rather than throwing.
3672
+ function parseRecentlyVisited(value) {
3673
+ let parsed = null;
3674
+ try {
3675
+ parsed = JSON.parse(value !== null && value !== void 0 ? value : "null");
3676
+ } catch {
3677
+ parsed = null;
3678
+ }
3679
+ if (Array.isArray(parsed)) {
3680
+ return parsed.filter(isEntry);
3681
+ } else {
3682
+ return [];
3683
+ }
3684
+ }
3685
+ function addVisit(entries, entry) {
3686
+ const key = entryKey(entry);
3687
+ return [entry, ...entries.filter(existing => entryKey(existing) !== key)].slice(0, MAX_ENTRIES);
3688
+ }
3689
+
3690
+ // The app whose URL, in this environment, has the current page's host.
3691
+ // Reversing pcoUrl handles every environment @planningcenter/url knows about,
3692
+ // and guarantees the stored app rebuilds a working link.
3693
+ function currentApp() {
3694
+ var _a;
3695
+ return (_a = APPS.find(app => new URL(pcoUrl$1.pcoUrl(app)).hostname === window.location.hostname)) !== null && _a !== void 0 ? _a : null;
3696
+ }
3697
+ function isFilled(value) {
3698
+ return typeof value === "string" && value !== "";
3699
+ }
3700
+ // The tag's metadata is product-supplied JSON; only its string `type` and
3701
+ // `details` carry over, and an entry without either has no metadata at all.
3702
+ function metadataFromPage(metadata) {
3703
+ var _a;
3704
+ try {
3705
+ const {
3706
+ details,
3707
+ type
3708
+ } = (_a = JSON.parse(metadata !== null && metadata !== void 0 ? metadata : "{}")) !== null && _a !== void 0 ? _a : {};
3709
+ if (isFilled(details) || isFilled(type)) {
3710
+ return {
3711
+ ...(isFilled(details) && {
3712
+ details
3713
+ }),
3714
+ ...(isFilled(type) && {
3715
+ type
3716
+ })
3717
+ };
3718
+ } else {
3719
+ return undefined;
3720
+ }
3721
+ } catch {
3722
+ return undefined;
3723
+ }
3724
+ }
3725
+ // Staff impersonating someone browse under `/O<org id>/P<person id>/…`.
3726
+ function withoutImpersonation(segments) {
3727
+ const [org = "", person = ""] = segments;
3728
+ if (org.startsWith("O") && isId(org.slice(1)) && person.startsWith("P") && isId(person.slice(1))) {
3729
+ return segments.slice(2);
3730
+ } else {
3731
+ return segments;
3732
+ }
3733
+ }
3734
+ // Normalized the way home-mobile does it, so a page visited on web and on
3735
+ // mobile is one entry once the list syncs.
3736
+ function pathFromLocation({
3737
+ pathname,
3738
+ search
3739
+ }) {
3740
+ const segments = withoutImpersonation(pathname.split("/").filter(Boolean));
3741
+ if (segments.length === 0) {
3742
+ return "/";
3743
+ } else {
3744
+ return `/${segments.join("/")}${search.length > 1 ? search : ""}`;
3745
+ }
3746
+ }
3747
+ // pco:resource is the more specific signal, so it wins when both tags are
3748
+ // present. A resource that isn't a PCRL is ignored rather than recorded.
3749
+ function visitFromPage({
3750
+ metadata,
3751
+ resource,
3752
+ title
3753
+ }) {
3754
+ const app = currentApp();
3755
+ if (resource && isPcrl(resource)) {
3756
+ return {
3757
+ pcrl: resource,
3758
+ type: "resource"
3759
+ };
3760
+ } else if (title && title.trim() !== "" && app) {
3761
+ const pageMetadata = metadataFromPage(metadata);
3762
+ return {
3763
+ app,
3764
+ ...(pageMetadata && {
3765
+ metadata: pageMetadata
3766
+ }),
3767
+ path: pathFromLocation(window.location),
3768
+ title,
3769
+ type: "page"
3770
+ };
3771
+ } else {
3772
+ return null;
3773
+ }
3774
+ }
3775
+ // The entry Recently Visited would record for the current page, or null when
3776
+ // the page isn't eligible. Shared by the recorder and the debug panel so the
3777
+ // panel always shows what actually gets stored.
3778
+ function useCurrentVisit() {
3779
+ const {
3780
+ recently_visitable: title,
3781
+ resource
3782
+ } = usePcoMeta();
3783
+ const metadata = useRecentlyVisitableMetadata();
3784
+ return React.useMemo(() => visitFromPage({
3785
+ metadata,
3786
+ resource,
3787
+ title
3788
+ }), [metadata, resource, title]);
3789
+ }
3790
+ // Products hand the person's ID to AppsProvider, not to the topbar itself,
3791
+ // and several pass it as a number.
3792
+ function useCurrentPersonId() {
3793
+ const {
3794
+ userId
3795
+ } = useStore();
3796
+ return userId == null ? undefined : String(userId);
3797
+ }
3798
+
3799
+ // Per-origin localStorage is a stopgap: each product keeps its own list until
3800
+ // PCA-409 moves this to storage shared across product subdomains. Only this
3801
+ // file should need to change when it does.
3802
+ function storageKey({
3803
+ env,
3804
+ personId
3805
+ }) {
3806
+ return `topbar.recently_visited.${env}.${personId}`;
3807
+ }
3808
+ // localStorage doesn't notify the tab that wrote it, so readers in this tab
3809
+ // learn about new visits from this counter instead.
3810
+ const changes = createStore({
3811
+ version: 0
3812
+ });
3813
+ function readRecentlyVisited(scope) {
3814
+ try {
3815
+ return parseRecentlyVisited(localStorage.getItem(storageKey(scope)));
3816
+ } catch {
3817
+ return [];
3818
+ }
3819
+ }
3820
+ function recordVisit(visit, scope) {
3821
+ const entry = toEntry(visit, new Date());
3822
+ if (isEntry(entry)) {
3823
+ try {
3824
+ localStorage.setItem(storageKey(scope), JSON.stringify(addVisit(readRecentlyVisited(scope), entry)));
3825
+ changes.setState({
3826
+ version: changes.getState().version + 1
3827
+ });
3828
+ } catch {
3829
+ // Storage can be full or blocked; recents are disposable.
3830
+ }
3831
+ }
3832
+ }
3833
+ function useRecentlyVisited({
3834
+ env,
3835
+ personId
3836
+ }) {
3837
+ const version = shim.useSyncExternalStore(changes.subscribe, () => changes.getState().version);
3838
+ // `version` isn't read, but listing it re-reads storage after each visit.
3839
+ return React.useMemo(() => personId ? readRecentlyVisited({
3840
+ env,
3841
+ personId
3842
+ }) : [], [env, personId, version]);
3843
+ }
3844
+
3845
+ const pcoUrl = pcoUrl$1;
3846
+
3847
+ // Resolves resource entries with one pcrl_lookups request per product.
3848
+ // pcrl_lookups reports forbidden and deleted resources the same way, so both
3849
+ // become `unresolved`, as does every entry in a batch whose request failed.
3850
+ function useHydratedRecentlyVisited(entries, env) {
3851
+ const batches = React.useMemo(() => batchByProduct(entries), [entries]);
3852
+ const urls = React.useMemo(() => batches.map(([product, pcrls]) => pcrlLookupsUrl(product, pcrls)), [batches]);
3853
+ const requests = useCachedRequests(urls);
3854
+ const lookups = new Map();
3855
+ batches.forEach(([, pcrls], index) => {
3856
+ const request = requests[index];
3857
+ for (const pcrl of pcrls) lookups.set(pcrl, lookupFor(pcrl, request));
3858
+ });
3859
+ return entries.map(entry => {
3860
+ var _a;
3861
+ if (entry.type === "page") {
3862
+ return {
3863
+ entry,
3864
+ status: "page",
3865
+ url: pageUrl(entry, env)
3866
+ };
3867
+ } else {
3868
+ const lookup = (_a = lookups.get(entry.pcrl)) !== null && _a !== void 0 ? _a : {
3869
+ status: "unresolved"
3870
+ };
3871
+ if (lookup.status === "resolved") {
3872
+ return {
3873
+ ...lookup,
3874
+ entry,
3875
+ url: lookup.result.attributes.url
3876
+ };
3877
+ } else {
3878
+ return {
3879
+ entry,
3880
+ status: lookup.status
3881
+ };
3882
+ }
3883
+ }
3884
+ });
3885
+ }
3886
+ function lookupFor(pcrl, request) {
3887
+ if (request.status === "done") {
3888
+ const result = request.data.data.find(({
3889
+ attributes
3890
+ }) => attributes.pcrl === pcrl);
3891
+ return result ? {
3892
+ result,
3893
+ status: "resolved"
3894
+ } : {
3895
+ status: "unresolved"
3896
+ };
3897
+ } else if (request.status === "error") {
3898
+ return {
3899
+ status: "unresolved"
3900
+ };
3901
+ } else {
3902
+ return {
3903
+ status: "loading"
3904
+ };
3905
+ }
3906
+ }
3907
+ const RECENTLY_VISITED_PAGE_SIZE = 10;
3908
+ // A PCRL can only be looked up through its own product's API, which may not be
3909
+ // the product it was visited in. Each page of entries is batched on its own,
3910
+ // so loading the next page leaves earlier pages' URLs, and so their cache
3911
+ // keys, alone. Sorting keeps them stable as a page reorders.
3912
+ function batchByProduct(entries) {
3913
+ var _a;
3914
+ const batches = [];
3915
+ for (let start = 0; start < entries.length; start += RECENTLY_VISITED_PAGE_SIZE) {
3916
+ const page = new Map();
3917
+ for (const entry of entries.slice(start, start + RECENTLY_VISITED_PAGE_SIZE)) {
3918
+ if (entry.type === "resource") {
3919
+ const [product] = entry.pcrl.split("/");
3920
+ page.set(product, [...((_a = page.get(product)) !== null && _a !== void 0 ? _a : []), entry.pcrl]);
3921
+ }
3922
+ }
3923
+ for (const [product, pcrls] of page) {
3924
+ batches.push([product, [...pcrls].sort()]);
3925
+ }
3926
+ }
3927
+ return batches;
3928
+ }
3929
+ function pcrlLookupsUrl(product, pcrls) {
3930
+ const params = new URLSearchParams({
3931
+ "where[pcrl]": pcrls.join(",")
3932
+ });
3933
+ return `${window.location.origin}/~api/${product}/v2/pcrl_lookups?${params}`;
3934
+ }
3935
+ // Built from the app rather than the current origin, so it stays right once
3936
+ // PCA-409 shares one list across product subdomains.
3937
+ function pageUrl(entry, env) {
3938
+ return new URL(entry.path, pcoUrl(env)(entry.app)).href;
3939
+ }
3940
+
3941
+ function useRecentVisits(env) {
3942
+ const personId = useCurrentPersonId();
3943
+ const entries = useRecentlyVisited({
3944
+ env,
3945
+ personId
3946
+ });
3947
+ const [count, setCount] = React.useState(RECENTLY_VISITED_PAGE_SIZE);
3948
+ const shown = React.useMemo(() => entries.slice(0, count), [entries, count]);
3949
+ const hydrated = useHydratedRecentlyVisited(shown, env);
3950
+ // Resources show up once they're found. Ones that can't be found anymore,
3951
+ // or that the person can no longer see, are left out.
3952
+ const visits = hydrated.filter(isShown);
3953
+ const loading = hydrated.some(({
3954
+ status
3955
+ }) => status === "loading");
3956
+ const showMore = React.useCallback(() => setCount(current => current + RECENTLY_VISITED_PAGE_SIZE), []);
3957
+ return {
3958
+ count,
3959
+ hasMore: entries.length > count,
3960
+ loading,
3961
+ showMore,
3962
+ visits
3963
+ };
3964
+ }
3965
+ function isShown(visit) {
3966
+ return visit.status === "page" || visit.status === "resolved";
3967
+ }
3968
+
3969
+ function open(url, {
3970
+ newTab
3971
+ }) {
3972
+ if (newTab) {
3973
+ window.open(url, "_blank", "noopener");
3974
+ } else {
3975
+ window.location.assign(url);
3976
+ }
3977
+ }
3978
+ // With text selected, ⌘C should copy that text as usual, not the link.
3979
+ function hasSelectedText(element) {
3980
+ var _a, _b;
3981
+ const selectedInField = element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
3982
+ return selectedInField || ((_b = (_a = window.getSelection()) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : "") !== "";
3983
+ }
3984
+
3985
+ const COPIED_FEEDBACK_MS = 1500;
3986
+ // `copiedUrl` stays set briefly after a copy so the UI can confirm it.
3987
+ function useCopyLink() {
3988
+ const [copiedUrl, setCopiedUrl] = React.useState(null);
3989
+ React.useEffect(() => {
3990
+ if (copiedUrl) {
3991
+ const timeout = setTimeout(() => setCopiedUrl(null), COPIED_FEEDBACK_MS);
3992
+ return () => clearTimeout(timeout);
3993
+ } else {
3994
+ return undefined;
3995
+ }
3996
+ }, [copiedUrl]);
3997
+ const copy = React.useCallback(url => {
3998
+ navigator.clipboard.writeText(url).then(() => setCopiedUrl(url));
3999
+ }, []);
4000
+ return {
4001
+ copiedUrl,
4002
+ copy
4003
+ };
4004
+ }
4005
+
4006
+ // Remembering which list the index belongs to puts the highlight back on the
4007
+ // first item whenever a new list replaces it, without an effect to reset it.
4008
+ // A list that grows or fills in under the same `list` keeps its highlight.
4009
+ function useResultNavigation(items, list) {
4010
+ const [highlight, setHighlight] = React.useState({
4011
+ index: 0,
4012
+ list
4013
+ });
4014
+ const highlightedIndex = highlight.list === list ? highlight.index : 0;
4015
+ const highlighted = items[highlightedIndex];
4016
+ const {
4017
+ copiedUrl,
4018
+ copy
4019
+ } = useCopyLink();
4020
+ const highlightIndex = React.useCallback(index => setHighlight(current => current.list === list && current.index === index ? current : {
4021
+ index,
4022
+ list
4023
+ }), [list]);
4024
+ const handleKeyDown = event => {
4025
+ const step = verticalStep(event.key);
4026
+ if (step && items.length > 0) {
4027
+ event.preventDefault();
4028
+ highlightIndex((highlightedIndex + step + items.length) % items.length);
4029
+ } else if (event.key === "Enter" && highlighted && !event.nativeEvent.isComposing) {
4030
+ event.preventDefault();
4031
+ open(highlighted.url, {
4032
+ newTab: hasCommandKey(event)
4033
+ });
4034
+ } else if (isCopyShortcut(event) && highlighted && !hasSelectedText(event.currentTarget)) {
4035
+ event.preventDefault();
4036
+ copy(highlighted.url);
4037
+ }
4038
+ };
4039
+ return {
4040
+ copied: copiedUrl !== null && copiedUrl === (highlighted === null || highlighted === void 0 ? void 0 : highlighted.url),
4041
+ handleKeyDown,
4042
+ highlightIndex,
4043
+ highlighted,
4044
+ highlightedIndex: highlighted ? highlightedIndex : null
4045
+ };
4046
+ }
4047
+
4048
+ function groupSections(groups = []) {
4049
+ return groups.map(({
4050
+ results,
4051
+ type
4052
+ }) => ({
4053
+ heading: type,
4054
+ items: results.map(resultItem),
4055
+ key: type
4056
+ }));
4057
+ }
4058
+ function resultItem({
4059
+ attributes,
4060
+ id
4061
+ }) {
4062
+ return {
4063
+ key: id,
4064
+ subtitle: summarize(attributes.metadata),
4065
+ title: attributes.title,
4066
+ url: attributes.url
4067
+ };
4068
+ }
4069
+ function visitItem(visit) {
4070
+ return {
4071
+ key: entryKey(visit.entry),
4072
+ ...describeVisit(visit)
4073
+ };
4074
+ }
4075
+ function summarize(metadata) {
4076
+ return Object.values(metadata !== null && metadata !== void 0 ? metadata : {}).filter(value => typeof value === "string" && value !== "").join(" · ");
4077
+ }
4078
+ // Unlike search results, recents come from every product, so each one names
4079
+ // its product and type.
4080
+ function describeVisit(visit) {
4081
+ var _a, _b;
4082
+ const subtitle = (...parts) => parts.filter(Boolean).join(" · ");
4083
+ if (visit.status === "page") {
4084
+ const {
4085
+ app,
4086
+ metadata,
4087
+ title
4088
+ } = visit.entry;
4089
+ return {
4090
+ subtitle: subtitle(productName(app), (_a = metadata === null || metadata === void 0 ? void 0 : metadata.type) !== null && _a !== void 0 ? _a : "", (_b = metadata === null || metadata === void 0 ? void 0 : metadata.details) !== null && _b !== void 0 ? _b : ""),
4091
+ title,
4092
+ url: visit.url
4093
+ };
4094
+ } else {
4095
+ const {
4096
+ metadata,
4097
+ product,
4098
+ title,
4099
+ type
4100
+ } = visit.result.attributes;
4101
+ return {
4102
+ subtitle: subtitle(productName(product), type, summarize(metadata)),
4103
+ title,
4104
+ url: visit.url
4105
+ };
4106
+ }
4107
+ }
4108
+ // check-ins → Check-Ins
4109
+ function productName(slug) {
4110
+ return slug.split("-").map(capitalize).join("-");
4111
+ }
4112
+ function capitalize(word) {
4113
+ return word.charAt(0).toUpperCase() + word.slice(1);
4114
+ }
4115
+
4116
+ const STORAGE_KEY = "topbar.product_search.last_product";
4117
+ function readLastProduct() {
4118
+ try {
4119
+ return localStorage.getItem(STORAGE_KEY);
4120
+ } catch {
4121
+ return null;
4122
+ }
4123
+ }
4124
+ function saveLastProduct(slug) {
4125
+ try {
4126
+ localStorage.setItem(STORAGE_KEY, slug);
4127
+ } catch {
4128
+ // Storage can be full or blocked; search still works without it.
4129
+ }
4130
+ }
4131
+ function initialProduct(products) {
4132
+ var _a, _b, _c;
4133
+ const lastProduct = readLastProduct();
4134
+ const bySlug = slug => products.find(product => product.slug === slug);
4135
+ return (_c = (_b = (_a = bySlug(lastProduct)) !== null && _a !== void 0 ? _a : bySlug("people")) !== null && _b !== void 0 ? _b : bySlug("services")) !== null && _c !== void 0 ? _c : products[0];
4136
+ }
4137
+
4138
+ function KeyboardHints({
4139
+ copied
4140
+ }) {
4141
+ const command = commandKeyLabel();
4142
+ return /*#__PURE__*/React.createElement("p", null, "\u2191\u2193 Select \u00B7 \u21B5 Open \u00B7 ", command, "\u21B5 Open in new tab \u00B7 ", command, "C", " ", /*#__PURE__*/React.createElement("span", {
4143
+ role: "status"
4144
+ }, copied ? "Copied!" : "Copy link"), " \u00B7 ", command, "K Open search");
4145
+ }
4146
+
4147
+ var styles$d = {"tab":"tab-dz8ki"};
4148
+
4149
+ function ProductTabs({
4150
+ children,
4151
+ onKeyDown,
4152
+ onSelect,
4153
+ products,
4154
+ selected
4155
+ }) {
4156
+ const id = useInstanceId("product-tabs");
4157
+ const tabId = slug => `${id}-tab-${slug}`;
4158
+ const tabRefs = React.useRef(new Map());
4159
+ const handleKeyDown = event => {
4160
+ var _a;
4161
+ const offset = horizontalStep(event.key);
4162
+ if (offset) {
4163
+ event.preventDefault();
4164
+ const index = products.indexOf(selected);
4165
+ const next = products[(index + offset + products.length) % products.length];
4166
+ onSelect(next.slug);
4167
+ (_a = tabRefs.current.get(next.slug)) === null || _a === void 0 ? void 0 : _a.focus();
4168
+ } else {
4169
+ onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
4170
+ }
4171
+ };
4172
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
4173
+ "aria-label": "Product",
4174
+ onKeyDown: handleKeyDown,
4175
+ role: "tablist"
4176
+ }, products.map(product => (/*#__PURE__*/React.createElement("button", {
4177
+ "aria-controls": `${id}-panel`,
4178
+ "aria-selected": product === selected,
4179
+ className: styles$d.tab,
4180
+ id: tabId(product.slug),
4181
+ key: product.slug,
4182
+ onClick: () => onSelect(product.slug),
4183
+ ref: element => {
4184
+ if (element) {
4185
+ tabRefs.current.set(product.slug, element);
4186
+ } else {
4187
+ tabRefs.current.delete(product.slug);
4188
+ }
4189
+ },
4190
+ role: "tab",
4191
+ tabIndex: product === selected ? 0 : -1,
4192
+ type: "button"
4193
+ }, product.appName)))), /*#__PURE__*/React.createElement("div", {
4194
+ "aria-labelledby": tabId(selected.slug),
4195
+ id: `${id}-panel`,
4196
+ role: "tabpanel"
4197
+ }, children));
4198
+ }
4199
+
4200
+ function EndOfResultsObserver({
4201
+ onVisible
4202
+ }) {
4203
+ const ref = React.useRef(null);
4204
+ React.useEffect(() => {
4205
+ const observer = new IntersectionObserver(([entry]) => {
4206
+ if (entry.isIntersecting) onVisible();
4207
+ });
4208
+ if (ref.current) observer.observe(ref.current);
4209
+ return () => observer.disconnect();
4210
+ }, [onVisible]);
4211
+ return /*#__PURE__*/React.createElement("div", {
4212
+ ref: ref
4213
+ });
4214
+ }
4215
+
4216
+ const resultId = (listboxId, index) => `${listboxId}-result-${index}`;
4217
+
4218
+ var styles$c = {"result":"result-Thq2C"};
4219
+
4220
+ function SearchResult({
4221
+ highlighted,
4222
+ id,
4223
+ onHighlight,
4224
+ subtitle,
4225
+ title,
4226
+ url
4227
+ }) {
4228
+ const ref = React.useRef(null);
4229
+ React.useEffect(() => {
4230
+ var _a;
4231
+ if (highlighted) (_a = ref.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({
4232
+ block: "nearest"
4233
+ });
4234
+ }, [highlighted]);
4235
+ // Focus stays in the search input, which points at the highlighted result
4236
+ // with aria-activedescendant, so results are kept out of the tab order.
4237
+ // Mouse move rather than enter: arrowing scrolls results under a resting
4238
+ // pointer, and that shouldn't steal the highlight.
4239
+ return /*#__PURE__*/React.createElement("a", {
4240
+ "aria-selected": highlighted,
4241
+ className: styles$c.result,
4242
+ href: url,
4243
+ id: id,
4244
+ onMouseMove: onHighlight,
4245
+ ref: ref,
4246
+ role: "option",
4247
+ tabIndex: -1
4248
+ }, /*#__PURE__*/React.createElement("div", null, title), /*#__PURE__*/React.createElement("div", null, subtitle));
4249
+ }
4250
+
4251
+ // Sections with a heading render as labelled groups; ones without render
4252
+ // their items straight into the listbox. Indexes run across every section.
4253
+ function ResultListbox({
4254
+ busy,
4255
+ highlightedIndex,
4256
+ id,
4257
+ label,
4258
+ labelledBy,
4259
+ onHighlight,
4260
+ sections
4261
+ }) {
4262
+ let index = 0;
4263
+ const renderItem = item => {
4264
+ const itemIndex = index++;
4265
+ return /*#__PURE__*/React.createElement(SearchResult, {
4266
+ highlighted: itemIndex === highlightedIndex,
4267
+ id: resultId(id, itemIndex),
4268
+ key: item.key,
4269
+ onHighlight: () => onHighlight(itemIndex),
4270
+ subtitle: item.subtitle,
4271
+ title: item.title,
4272
+ url: item.url
4273
+ });
4274
+ };
4275
+ return /*#__PURE__*/React.createElement("div", {
4276
+ "aria-busy": busy,
4277
+ "aria-label": label,
4278
+ "aria-labelledby": labelledBy,
4279
+ id: id,
4280
+ role: "listbox"
4281
+ }, sections.map((section, sectionIndex) => {
4282
+ if (section.heading === undefined) {
4283
+ return /*#__PURE__*/React.createElement(React.Fragment, {
4284
+ key: section.key
4285
+ }, section.items.map(renderItem));
4286
+ }
4287
+ const headingId = `${id}-group-${sectionIndex}`;
4288
+ return /*#__PURE__*/React.createElement("div", {
4289
+ "aria-labelledby": headingId,
4290
+ key: section.key,
4291
+ role: "group"
4292
+ }, /*#__PURE__*/React.createElement("div", {
4293
+ id: headingId
4294
+ }, section.heading), section.items.map(renderItem));
4295
+ }));
4296
+ }
4297
+
4298
+ function RecentlyVisited({
4299
+ count,
4300
+ hasMore,
4301
+ items,
4302
+ loading,
4303
+ showMore,
4304
+ ...navigation
4305
+ }) {
4306
+ const headingId = `${navigation.id}-heading`;
4307
+ if (items.length === 0 && !hasMore && !loading) {
4308
+ return /*#__PURE__*/React.createElement("p", null, "Pages you visit will show up here.");
4309
+ } else {
4310
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
4311
+ id: headingId
4312
+ }, "Recently visited"), /*#__PURE__*/React.createElement(ResultListbox, {
4313
+ labelledBy: headingId,
4314
+ sections: [{
4315
+ items,
4316
+ key: "recents"
4317
+ }],
4318
+ ...navigation
4319
+ }), hasMore && /*#__PURE__*/React.createElement(EndOfResultsObserver, {
4320
+ key: count,
4321
+ onVisible: showMore
4322
+ }));
4323
+ }
4324
+ }
4325
+
4326
+ // A combobox: focus stays here while aria-activedescendant points screen
4327
+ // readers at the highlighted result in the listbox it controls.
4328
+ function SearchInput({
4329
+ expanded,
4330
+ highlightedIndex,
4331
+ inputRef,
4332
+ listboxId,
4333
+ onChange,
4334
+ onFocus,
4335
+ onKeyDown,
4336
+ value
4337
+ }) {
4338
+ return /*#__PURE__*/React.createElement("input", {
4339
+ "aria-activedescendant": highlightedIndex === null ? undefined : resultId(listboxId, highlightedIndex),
4340
+ "aria-autocomplete": "list",
4341
+ "aria-controls": expanded ? listboxId : undefined,
4342
+ "aria-expanded": expanded,
4343
+ "aria-label": "Search",
4344
+ autoFocus: true,
4345
+ onChange: event => onChange(event.target.value),
4346
+ onFocus: onFocus,
4347
+ onKeyDown: onKeyDown,
4348
+ placeholder: "Search",
4349
+ ref: inputRef,
4350
+ role: "combobox",
4351
+ type: "search",
4352
+ value: value
4353
+ });
4354
+ }
4355
+
4356
+ var styles$b = {"visuallyHidden":"visuallyHidden-S9VG5"};
4357
+
4358
+ function SearchResults({
4359
+ label,
4360
+ sections,
4361
+ status,
4362
+ term,
4363
+ ...navigation
4364
+ }) {
4365
+ switch (status) {
4366
+ case "idle":
4367
+ return null;
4368
+ case "loading":
4369
+ if (sections.length > 0) {
4370
+ return /*#__PURE__*/React.createElement(ResultListbox, {
4371
+ busy: true,
4372
+ label: label,
4373
+ sections: sections,
4374
+ ...navigation
4375
+ });
4376
+ }
4377
+ return /*#__PURE__*/React.createElement("p", {
4378
+ role: "status"
4379
+ }, "Searching\u2026");
4380
+ case "error":
4381
+ return /*#__PURE__*/React.createElement("p", {
4382
+ role: "alert"
4383
+ }, "Search isn\u2019t working right now. Try again.");
4384
+ case "done":
4385
+ if (sections.length === 0) {
4386
+ return /*#__PURE__*/React.createElement("p", {
4387
+ role: "status"
4388
+ }, "No results for \u201C", term.trim(), "\u201D");
4389
+ }
4390
+ return /*#__PURE__*/React.createElement(ResultListbox, {
4391
+ busy: false,
4392
+ label: label,
4393
+ sections: sections,
4394
+ ...navigation
4395
+ });
4396
+ }
4397
+ }
4398
+
4399
+ // Keys the recents list for navigation, so the highlight
4400
+ // stays put as more recents load and as their lookups answer.
4401
+ const RECENTS = "recents";
4402
+ function SearchPanel({
4403
+ env,
4404
+ products
4405
+ }) {
4406
+ var _a, _b;
4407
+ const listboxId = useInstanceId("product-search-results");
4408
+ const [term, setTerm] = React.useState("");
4409
+ const [selectedSlug, setSelectedSlug] = React.useState(() => initialProduct(products).slug);
4410
+ const product = (_a = products.find(({
4411
+ slug
4412
+ }) => slug === selectedSlug)) !== null && _a !== void 0 ? _a : products[0];
4413
+ const hasTerm = term.trim() !== "";
4414
+ const search = useProductSearch(product.slug, term);
4415
+ const groups = "groups" in search ? search.groups : undefined;
4416
+ const sections = React.useMemo(() => groupSections(groups), [groups]);
4417
+ const {
4418
+ visits,
4419
+ ...recents
4420
+ } = useRecentVisits(env);
4421
+ const recentItems = visits.map(visitItem);
4422
+ const items = hasTerm ? sections.flatMap(section => section.items) : recentItems;
4423
+ const navigation = useResultNavigation(items, hasTerm ? sections : RECENTS);
4424
+ const {
4425
+ highlightedIndex
4426
+ } = navigation;
4427
+ const inputRef = React.useRef(null);
4428
+ const [announceHighlight, setAnnounceHighlight] = React.useState(false);
4429
+ // The tabs keep focus while arrowing through results, but only the focused
4430
+ // input can point screen readers at the highlight, so from the tabs it's
4431
+ // announced instead. Typing goes back to the input, keystroke and all.
4432
+ const handleTabsKeyDown = event => {
4433
+ var _a;
4434
+ if (isTypingKey(event)) {
4435
+ (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
4436
+ } else if (verticalStep(event.key)) {
4437
+ setAnnounceHighlight(true);
4438
+ navigation.handleKeyDown(event);
4439
+ } else {
4440
+ navigation.handleKeyDown(event);
4441
+ }
4442
+ };
4443
+ React.useEffect(() => {
4444
+ if (hasTerm) saveLastProduct(product.slug);
4445
+ }, [hasTerm, product.slug]);
4446
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SearchInput, {
4447
+ expanded: items.length > 0,
4448
+ highlightedIndex: highlightedIndex,
4449
+ inputRef: inputRef,
4450
+ listboxId: listboxId,
4451
+ onChange: setTerm,
4452
+ onFocus: () => setAnnounceHighlight(false),
4453
+ onKeyDown: navigation.handleKeyDown,
4454
+ value: term
4455
+ }), /*#__PURE__*/React.createElement(ProductTabs, {
4456
+ onKeyDown: handleTabsKeyDown,
4457
+ onSelect: setSelectedSlug,
4458
+ products: products,
4459
+ selected: product
4460
+ }, hasTerm ? (/*#__PURE__*/React.createElement(SearchResults, {
4461
+ highlightedIndex: highlightedIndex,
4462
+ id: listboxId,
4463
+ label: `${product.appName} results`,
4464
+ onHighlight: navigation.highlightIndex,
4465
+ sections: sections,
4466
+ status: search.status,
4467
+ term: term
4468
+ })) : (/*#__PURE__*/React.createElement(RecentlyVisited, {
4469
+ highlightedIndex: highlightedIndex,
4470
+ id: listboxId,
4471
+ items: recentItems,
4472
+ onHighlight: navigation.highlightIndex,
4473
+ ...recents
4474
+ }))), /*#__PURE__*/React.createElement("div", {
4475
+ "aria-live": "polite",
4476
+ className: styles$b.visuallyHidden
4477
+ }, announceHighlight ? (_b = navigation.highlighted) === null || _b === void 0 ? void 0 : _b.title : null), /*#__PURE__*/React.createElement(KeyboardHints, {
4478
+ copied: navigation.copied
4479
+ }));
3974
4480
  }
3975
- function isRecord(value) {
3976
- return typeof value === "object" && value !== null && !Array.isArray(value);
4481
+
4482
+ // Products open modals from many libraries, not all of which mark themselves
4483
+ // with roles, so a modal is recognized by what it does: cover the page.
4484
+ function isModalOpen() {
4485
+ const {
4486
+ clientHeight,
4487
+ clientWidth
4488
+ } = document.documentElement;
4489
+ const element = document.elementFromPoint(clientWidth / 2, clientHeight / 2);
4490
+ return element !== null && (isInNativeDialog(element) || hasBackdrop(element, clientWidth, clientHeight));
3977
4491
  }
3978
- function entryKey(entry) {
3979
- if (entry.type === "resource") {
3980
- return `resource:${entry.pcrl}`;
3981
- } else {
3982
- return `page:${entry.product}:${entry.path}`;
3983
- }
4492
+ // A native modal's backdrop is drawn by the browser rather than being an
4493
+ // element; hit-testing it lands on the dialog itself.
4494
+ function isInNativeDialog(element) {
4495
+ return element.closest("dialog[open]") !== null;
3984
4496
  }
3985
- function isEntry(value) {
3986
- if (!isRecord(value)) {
3987
- return false;
3988
- } else if (value.type === "resource") {
3989
- return typeof value.pcrl === "string" && isPcrl(value.pcrl);
3990
- } else if (value.type === "page") {
3991
- return isNonBlankString(value.product) && isNonBlankString(value.title) && isNonBlankString(value.path) && typeof value.details === "string";
3992
- } else {
3993
- return false;
4497
+ function hasBackdrop(element, width, height) {
4498
+ let current = element;
4499
+ while (current && !isBackdrop(current, width, height)) {
4500
+ current = current.parentElement;
3994
4501
  }
4502
+ return current !== null;
3995
4503
  }
3996
- // Cuts on code points so a surrogate pair is never split.
3997
- function clampText(value) {
3998
- return Array.from(value).slice(0, MAX_TEXT_LENGTH).join("");
4504
+ // Some backdrops start below the topbar rather than at the top of the screen,
4505
+ // so the layer only has to span the width and reach from the center to the
4506
+ // bottom. Its own box has to reach the center: a bar along the bottom can
4507
+ // hold a panel that pops up over the center, like Services' media player.
4508
+ // react-aria leaves a closed modal's backdrop up while it fades out, search's
4509
+ // own included, so one that's on its way out doesn't count.
4510
+ function isBackdrop(element, width, height) {
4511
+ const rect = element.getBoundingClientRect();
4512
+ return !element.matches("[data-exiting]") && getComputedStyle(element).position === "fixed" && rect.width >= width - SCROLLBAR_SLACK && rect.top <= height / 2 && rect.bottom >= height - ROUNDING_SLACK;
3999
4513
  }
4000
- function clampEntry(entry) {
4001
- if (entry.type === "resource") {
4002
- return entry;
4003
- } else {
4004
- return {
4005
- ...entry,
4006
- details: clampText(entry.details),
4007
- title: clampText(entry.title)
4514
+ // react-aria keeps a classic scrollbar's gutter reserved while its modals lock
4515
+ // scrolling, so their backdrops stop short of the right edge by its width.
4516
+ const SCROLLBAR_SLACK = 24;
4517
+ // Rects are fractional at non-100% zoom, but clientWidth and clientHeight are
4518
+ // rounded to whole pixels.
4519
+ const ROUNDING_SLACK = 1;
4520
+
4521
+ function useSearchShortcut(enabled, open, setOpen) {
4522
+ React.useEffect(() => {
4523
+ if (!enabled) return undefined;
4524
+ // Rich text editors like Trix bind ⌘K to "insert link"; capturing at the
4525
+ // window and stopping propagation keeps them from ever seeing it. Under a
4526
+ // product's modal, search stays shut rather than stacking a second modal,
4527
+ // and the keystroke goes on to the product.
4528
+ const handleKeyDown = event => {
4529
+ if (open && isSearchShortcut(event)) {
4530
+ event.preventDefault();
4531
+ event.stopPropagation();
4532
+ setOpen(false);
4533
+ } else if (!open && (isSearchShortcut(event) || isLegacySearchShortcut(event)) && !isModalOpen()) {
4534
+ event.preventDefault();
4535
+ event.stopPropagation();
4536
+ setOpen(true);
4537
+ }
4008
4538
  };
4009
- }
4010
- }
4011
- // Recents are disposable: anything unexpected reads as an empty list (or, for
4012
- // a single bad entry, a list without it) rather than throwing.
4013
- function parseRecentlyVisited(value, personId) {
4014
- let parsed = null;
4015
- try {
4016
- parsed = JSON.parse(value !== null && value !== void 0 ? value : "null");
4017
- } catch {
4018
- parsed = null;
4019
- }
4020
- if (isRecord(parsed) && parsed.v === FORMAT_VERSION && parsed.person === personId && Array.isArray(parsed.entries)) {
4021
- return parsed.entries.filter(isEntry);
4022
- } else {
4023
- return [];
4024
- }
4025
- }
4026
- function addVisit(entries, visit) {
4027
- const key = entryKey(visit);
4028
- return [clampEntry(visit), ...entries.filter(entry => entryKey(entry) !== key)].slice(0, MAX_ENTRIES);
4539
+ window.addEventListener("keydown", handleKeyDown, {
4540
+ capture: true
4541
+ });
4542
+ return () => window.removeEventListener("keydown", handleKeyDown, {
4543
+ capture: true
4544
+ });
4545
+ }, [enabled, open, setOpen]);
4029
4546
  }
4030
- function serializeRecentlyVisited(entries, personId) {
4031
- return JSON.stringify({
4032
- entries,
4033
- person: personId,
4034
- v: FORMAT_VERSION
4547
+
4548
+ const SEARCHABLE_PRODUCTS = [{
4549
+ appName: "Calendar",
4550
+ slug: "calendar"
4551
+ }, {
4552
+ appName: "Check-Ins",
4553
+ slug: "check-ins"
4554
+ }, {
4555
+ appName: "Giving",
4556
+ slug: "giving"
4557
+ }, {
4558
+ appName: "Groups",
4559
+ slug: "groups"
4560
+ }, {
4561
+ appName: "People",
4562
+ slug: "people"
4563
+ }, {
4564
+ appName: "Publishing",
4565
+ slug: "publishing"
4566
+ }, {
4567
+ appName: "Registrations",
4568
+ slug: "registrations"
4569
+ }, {
4570
+ appName: "Services",
4571
+ slug: "services"
4572
+ }];
4573
+
4574
+ const PERMISSION_CHECKS = {
4575
+ "check-ins": () => fetchMe("check-ins", "permission").then(({
4576
+ permission
4577
+ }) => permission !== null),
4578
+ giving: personId => {
4579
+ if (personId === undefined) {
4580
+ return Promise.reject(new Error("No person to check Giving access for"));
4581
+ } else {
4582
+ return fetchAttributes(`giving/v2/person_permissions/${personId}`, {
4583
+ "fields[PersonPermissions]": "can_search"
4584
+ }).then(({
4585
+ can_search
4586
+ }) => can_search);
4587
+ }
4588
+ },
4589
+ people: () => fetchMe("people", "people_permissions,site_administrator").then(({
4590
+ people_permissions,
4591
+ site_administrator
4592
+ }) => site_administrator || people_permissions !== null),
4593
+ registrations: () => fetchMe("registrations", "can_search").then(({
4594
+ can_search
4595
+ }) => can_search),
4596
+ services: () => fetchMe("services", "can_search").then(({
4597
+ can_search
4598
+ }) => can_search)
4599
+ };
4600
+ function fetchMe(product, fields) {
4601
+ return fetchAttributes(`${product}/v2/me`, {
4602
+ "fields[Person]": fields
4035
4603
  });
4036
4604
  }
4037
-
4038
- // The product whose URL, in this environment, has the current page's host.
4039
- // Reversing pcoUrl handles every environment @planningcenter/url knows about,
4040
- // and guarantees the stored product rebuilds a working link.
4041
- function currentProduct() {
4042
- var _a;
4043
- return (_a = PRODUCTS.find(product => new URL(pcoUrl$1.pcoUrl(product)).hostname === window.location.hostname)) !== null && _a !== void 0 ? _a : null;
4605
+ function fetchAttributes(path, params) {
4606
+ const url = `${window.location.origin}/~api/${path}?${new URLSearchParams(params)}`;
4607
+ return apiRequest(defaultFetch, url, {
4608
+ headers: apiVersionHeaders(url)
4609
+ }).then(({
4610
+ json,
4611
+ ok
4612
+ }) => {
4613
+ if (!ok || json === null) throw new Error("Permission check failed");
4614
+ return json.data.attributes;
4615
+ });
4044
4616
  }
4045
- function detailsFromMetadata(metadata) {
4046
- var _a;
4047
- try {
4048
- const details = (_a = JSON.parse(metadata !== null && metadata !== void 0 ? metadata : "{}")) === null || _a === void 0 ? void 0 : _a.details;
4049
- return typeof details === "string" ? details : "";
4050
- } catch {
4051
- return "";
4617
+ // Permissions rarely change mid-session, so a successful check is kept until
4618
+ // the page reloads. A failed one is dropped so a remount tries again.
4619
+ const permissionCache = new Map();
4620
+ function canSearch(slug, personId) {
4621
+ const check = PERMISSION_CHECKS[slug];
4622
+ const cached = permissionCache.get(slug);
4623
+ if (!check) {
4624
+ return Promise.resolve(true);
4625
+ } else if (cached) {
4626
+ return cached;
4627
+ } else {
4628
+ const promise = check(personId).catch(() => {
4629
+ permissionCache.delete(slug);
4630
+ return false;
4631
+ });
4632
+ permissionCache.set(slug, promise);
4633
+ return promise;
4052
4634
  }
4053
4635
  }
4054
- // pco:resource is the more specific signal, so it wins when both tags are
4055
- // present. A resource that isn't a PCRL is ignored rather than recorded.
4056
- function visitFromPage({
4057
- metadata,
4058
- resource,
4059
- title
4060
- }) {
4061
- const product = currentProduct();
4062
- if (resource && isPcrl(resource)) {
4063
- return {
4064
- pcrl: resource,
4065
- type: "resource"
4636
+
4637
+ function useSearchableProducts(apps) {
4638
+ // Keyed by name because providers hand over a fresh `apps` array on renders
4639
+ // where nothing changed.
4640
+ const accessibleKey = inSwitcherOrder(apps).map(({
4641
+ slug
4642
+ }) => slug).join(",");
4643
+ const personId = useCurrentPersonId();
4644
+ const [settled, setSettled] = React.useState();
4645
+ React.useEffect(() => {
4646
+ let current = true;
4647
+ const accessible = accessibleKey.split(",").flatMap(slug => SEARCHABLE_PRODUCTS.filter(product => product.slug === slug));
4648
+ Promise.all(accessible.map(({
4649
+ slug
4650
+ }) => canSearch(slug, personId))).then(allowed => {
4651
+ if (current) {
4652
+ setSettled({
4653
+ accessibleKey,
4654
+ personId,
4655
+ products: accessible.filter((_, index) => allowed[index])
4656
+ });
4657
+ }
4658
+ });
4659
+ return () => {
4660
+ current = false;
4066
4661
  };
4067
- } else if (title && title.trim() !== "" && product) {
4662
+ }, [accessibleKey, personId]);
4663
+ if ((settled === null || settled === void 0 ? void 0 : settled.accessibleKey) === accessibleKey && settled.personId === personId) {
4068
4664
  return {
4069
- details: detailsFromMetadata(metadata),
4070
- path: window.location.pathname,
4071
- product,
4072
- title,
4073
- type: "page"
4665
+ products: settled.products,
4666
+ status: "done"
4074
4667
  };
4075
4668
  } else {
4076
- return null;
4669
+ return {
4670
+ status: "loading"
4671
+ };
4077
4672
  }
4078
4673
  }
4079
- // The entry Recently Visited would record for the current page, or null when
4080
- // the page isn't eligible. Shared by the recorder and the debug panel so the
4081
- // panel always shows what actually gets stored.
4082
- function useCurrentVisit() {
4083
- const {
4084
- recently_visitable: title,
4085
- resource
4086
- } = usePcoMeta();
4087
- const metadata = useRecentlyVisitableMetadata();
4088
- return React.useMemo(() => visitFromPage({
4089
- metadata,
4090
- resource,
4091
- title
4092
- }), [metadata, resource, title]);
4093
- }
4094
- // Products hand the person's ID to AppsProvider, not to the topbar itself,
4095
- // and several pass it as a number.
4096
- function useCurrentPersonId() {
4097
- const {
4098
- userId
4099
- } = useStore();
4100
- return userId == null ? undefined : String(userId);
4674
+ function inSwitcherOrder(apps) {
4675
+ return groupApps(apps).restProducts.flatMap(app => SEARCHABLE_PRODUCTS.filter(product => product.appName === app.name));
4101
4676
  }
4102
4677
 
4103
- // Per-origin localStorage is a stopgap: each product keeps its own list until
4104
- // PCA-409 moves this to storage shared across product subdomains. Only this
4105
- // file should need to change when it does.
4106
- function storageKey({
4107
- env,
4108
- personId
4678
+ function ProductSearch({
4679
+ apps,
4680
+ env
4109
4681
  }) {
4110
- return `topbar.recently_visited.${env}.${personId}`;
4111
- }
4112
- // localStorage doesn't notify the tab that wrote it, so readers in this tab
4113
- // learn about new visits from this counter instead.
4114
- const changes = createStore({
4115
- version: 0
4116
- });
4117
- function readRecentlyVisited(scope) {
4118
- try {
4119
- return parseRecentlyVisited(localStorage.getItem(storageKey(scope)), scope.personId);
4120
- } catch {
4121
- return [];
4122
- }
4123
- }
4124
- function recordVisit(visit, scope) {
4125
- if (isEntry(visit)) {
4126
- try {
4127
- localStorage.setItem(storageKey(scope), serializeRecentlyVisited(addVisit(readRecentlyVisited(scope), visit), scope.personId));
4128
- changes.setState({
4129
- version: changes.getState().version + 1
4130
- });
4131
- } catch {
4132
- // Storage can be full or blocked; recents are disposable.
4133
- }
4134
- }
4682
+ const searchable = useSearchableProducts(apps);
4683
+ const [open, setOpen] = React.useState(false);
4684
+ const available = searchable.status === "done" && searchable.products.length > 0;
4685
+ useSearchShortcut(available, open, setOpen);
4686
+ if (!available) return null;
4687
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SearchButton, {
4688
+ onClick: () => setOpen(true)
4689
+ }), /*#__PURE__*/React.createElement(tapestry.Modal, {
4690
+ "aria-label": "Search",
4691
+ dismissible: true,
4692
+ onClose: () => setOpen(false),
4693
+ open: open
4694
+ }, /*#__PURE__*/React.createElement(SearchPanel, {
4695
+ env: env,
4696
+ products: searchable.products
4697
+ })));
4135
4698
  }
4136
4699
 
4137
4700
  function useRecordRecentlyVisited({
@@ -4176,9 +4739,9 @@ true;
4176
4739
  `), children);
4177
4740
  }
4178
4741
 
4179
- var styles$9 = {"route":"route--8BWe","is-active":"is-active-FsU9Y","apps-routes-separator":"apps-routes-separator-Nlm-N"};
4742
+ var styles$a = {"route":"route--8BWe","is-active":"is-active-FsU9Y","apps-routes-separator":"apps-routes-separator-Nlm-N"};
4180
4743
 
4181
- var styles$8 = {"popup":"popup-mDNKn","trigger-wrap":"trigger-wrap-Jd-KR","trigger":"trigger-WgRT5","is-visible":"is-visible-GZC0-","trigger-orgname":"trigger-orgname-TDHCc"};
4744
+ var styles$9 = {"popup":"popup-mDNKn","trigger-wrap":"trigger-wrap-Jd-KR","trigger":"trigger-WgRT5","is-visible":"is-visible-GZC0-","trigger-orgname":"trigger-orgname-TDHCc"};
4182
4745
 
4183
4746
  const UserMenu = ({
4184
4747
  appName,
@@ -4197,7 +4760,7 @@ true;
4197
4760
  component: Outsider,
4198
4761
  onOutsideClick: () => setUserMenuVisible(false),
4199
4762
  cleanup: !userMenuVisible,
4200
- className: styles$8.popup,
4763
+ className: styles$9.popup,
4201
4764
  isVisible: userMenuVisible
4202
4765
  }, /*#__PURE__*/React.createElement("div", null, hasConnectedPeople ? (/*#__PURE__*/React.createElement("div", {
4203
4766
  style: {
@@ -4341,11 +4904,11 @@ true;
4341
4904
  userMenuVisible
4342
4905
  }) => {
4343
4906
  return /*#__PURE__*/React.createElement("div", {
4344
- className: styles$8["trigger-wrap"],
4907
+ className: styles$9["trigger-wrap"],
4345
4908
  "data-pendo": "topbar-org-switcher"
4346
4909
  }, /*#__PURE__*/React.createElement(Hoverable, {
4347
- className: classnames(styles$8["trigger"], {
4348
- [styles$8["is-visible"]]: userMenuVisible
4910
+ className: classnames(styles$9["trigger"], {
4911
+ [styles$9["is-visible"]]: userMenuVisible
4349
4912
  }),
4350
4913
  component: "button",
4351
4914
  type: "button",
@@ -4357,7 +4920,7 @@ true;
4357
4920
  requestConnectedPeopleFetch();
4358
4921
  }
4359
4922
  }, showOrgName && (/*#__PURE__*/React.createElement("span", {
4360
- className: styles$8["trigger-orgname"]
4923
+ className: styles$9["trigger-orgname"]
4361
4924
  }, orgName)), /*#__PURE__*/React.createElement(DisclosureChevronIcon, null)));
4362
4925
  };
4363
4926
 
@@ -4417,13 +4980,14 @@ true;
4417
4980
  colors: props.colors,
4418
4981
  requestAppsFetch: props.requestAppsFetch
4419
4982
  })), /*#__PURE__*/React.createElement("div", {
4420
- className: styles$9["apps-routes-separator"]
4983
+ className: styles$a["apps-routes-separator"]
4421
4984
  }), routesVisible && props.routes, /*#__PURE__*/React.createElement("div", {
4422
4985
  style: {
4423
4986
  margin: "auto"
4424
4987
  }
4425
4988
  }), !notificationsEnabled && notifications(), productSearchEnabled ? (/*#__PURE__*/React.createElement(ProductSearch, {
4426
- apps: props.apps
4989
+ apps: props.apps,
4990
+ env: props.env
4427
4991
  })) : search({
4428
4992
  hideRoutes: () => setRoutesVisible(false),
4429
4993
  showRoutes: () => setRoutesVisible(true)
@@ -4457,12 +5021,12 @@ true;
4457
5021
  ...nativeProps
4458
5022
  }) => {
4459
5023
  return /*#__PURE__*/React.createElement("a", {
4460
- className: classnames(styles$9["route"], className, active && styles$9["is-active"]),
5024
+ className: classnames(styles$a["route"], className, active && styles$a["is-active"]),
4461
5025
  ...nativeProps
4462
5026
  });
4463
5027
  };
4464
5028
 
4465
- var styles$7 = {"avatar":"avatar-tW-wn"};
5029
+ var styles$8 = {"avatar":"avatar-tW-wn"};
4466
5030
 
4467
5031
  function Avatar({
4468
5032
  env,
@@ -4472,7 +5036,7 @@ true;
4472
5036
  }) {
4473
5037
  const url = incomingUrl || `${pcoUrl$1(env)("people")}/static/no_photo_thumbnail_gray.svg`;
4474
5038
  return /*#__PURE__*/React.createElement("div", {
4475
- className: styles$7.avatar,
5039
+ className: styles$8.avatar,
4476
5040
  style: {
4477
5041
  backgroundImage: `url(${url})`,
4478
5042
  ...style
@@ -32231,7 +32795,7 @@ true;
32231
32795
 
32232
32796
  var reactDomExports = requireReactDom();
32233
32797
 
32234
- var styles$6 = {"trigger":"trigger-Th-Up","layer":"layer-ck6BD","layer--visible":"layer--visible-4E2p1","panel":"panel-vobWE","header":"header-xndfZ","heading":"heading-uXyQf","close":"close-pK0Lk","list":"list-zWvrn","link":"link-Kf3qj","current":"current-g3Ey-","accounts-item":"accounts-item-eQqDD","accounts-link":"accounts-link-SeIbD"};
32798
+ var styles$7 = {"trigger":"trigger-Th-Up","layer":"layer-ck6BD","layer--visible":"layer--visible-4E2p1","panel":"panel-vobWE","header":"header-xndfZ","heading":"heading-uXyQf","close":"close-pK0Lk","list":"list-zWvrn","link":"link-Kf3qj","current":"current-g3Ey-","accounts-item":"accounts-item-eQqDD","accounts-link":"accounts-link-SeIbD"};
32235
32799
 
32236
32800
  const AppsSwitcher = ({
32237
32801
  appName,
@@ -32259,8 +32823,8 @@ true;
32259
32823
  }, /*#__PURE__*/React.createElement("a", {
32260
32824
  "aria-current": name === appName ? "page" : undefined,
32261
32825
  "aria-label": name,
32262
- className: classnames(styles$6.link, {
32263
- [styles$6.current]: name === appName
32826
+ className: classnames(styles$7.link, {
32827
+ [styles$7.current]: name === appName
32264
32828
  }),
32265
32829
  "data-pendo": `app-switcher--change-app--${name}`,
32266
32830
  "data-turbolinks": false,
@@ -32272,48 +32836,48 @@ true;
32272
32836
  "aria-expanded": visible,
32273
32837
  "aria-haspopup": "true",
32274
32838
  "aria-label": `App switcher: ${appName}`,
32275
- className: styles$6.trigger,
32839
+ className: styles$7.trigger,
32276
32840
  onClick: onTriggerClick,
32277
32841
  type: "button"
32278
32842
  }, /*#__PURE__*/React.createElement(ColorAppIcon, {
32279
32843
  appName: appName.replace(/[\s-]/, "")
32280
32844
  })), /*#__PURE__*/reactDomExports.createPortal(/*#__PURE__*/React.createElement("div", {
32281
32845
  ref: containerRef,
32282
- className: classnames(styles$6.layer, {
32283
- [styles$6["layer--visible"]]: visible
32846
+ className: classnames(styles$7.layer, {
32847
+ [styles$7["layer--visible"]]: visible
32284
32848
  }),
32285
32849
  onClick: toggle,
32286
32850
  onKeyDown: handleKeyDown
32287
32851
  }, /*#__PURE__*/React.createElement("nav", {
32288
32852
  "aria-label": "Switch app",
32289
32853
  "aria-modal": visible,
32290
- className: styles$6.panel,
32854
+ className: styles$7.panel,
32291
32855
  "data-pendo": "app-switcher",
32292
32856
  onClick: event => event.stopPropagation(),
32293
32857
  role: "dialog"
32294
32858
  }, /*#__PURE__*/React.createElement("div", {
32295
- className: styles$6.header
32859
+ className: styles$7.header
32296
32860
  }, /*#__PURE__*/React.createElement("span", {
32297
- className: styles$6.heading
32861
+ className: styles$7.heading
32298
32862
  }, "Switch to"), /*#__PURE__*/React.createElement("button", {
32299
32863
  "aria-label": "Close",
32300
- className: styles$6.close,
32864
+ className: styles$7.close,
32301
32865
  onClick: toggle,
32302
32866
  type: "button"
32303
32867
  }, /*#__PURE__*/React.createElement(X, {
32304
32868
  "aria-hidden": "true",
32305
32869
  fill: "var(--t-icon-color-dim)"
32306
32870
  }))), /*#__PURE__*/React.createElement("ul", {
32307
- className: styles$6.list,
32871
+ className: styles$7.list,
32308
32872
  role: "list"
32309
32873
  }, homeProduct && productLink(homeProduct.name), restProducts.map(({
32310
32874
  name
32311
32875
  }) => productLink(name)), accountsProduct && (/*#__PURE__*/React.createElement("li", {
32312
- className: styles$6["accounts-item"]
32876
+ className: styles$7["accounts-item"]
32313
32877
  }, /*#__PURE__*/React.createElement("a", {
32314
32878
  "aria-current": appName === "Accounts" ? "page" : undefined,
32315
32879
  "aria-label": "Account settings",
32316
- className: classnames(styles$6.link, styles$6["accounts-link"]),
32880
+ className: classnames(styles$7.link, styles$7["accounts-link"]),
32317
32881
  "data-pendo": "app-switcher--change-app--Accounts",
32318
32882
  "data-turbolinks": false,
32319
32883
  href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(accountsProduct.name)
@@ -32336,14 +32900,14 @@ true;
32336
32900
  method: "localStorage"
32337
32901
  };
32338
32902
 
32339
- var styles$5 = {"appmenu-button":"appmenu-button-b0UvT","appmenu-layer":"appmenu-layer-0IQpN","appmenu-container":"appmenu-container-gackd","appmenu-scroll":"appmenu-scroll-mPSx0","appmenu-list":"appmenu-list-h7clX","appmenu-list-a":"appmenu-list-a-rGIQN","appmenu-list-a--top":"appmenu-list-a--top-RT-2F","appmenu-list-a--bottom":"appmenu-list-a--bottom-YDeBC","appmenu-close-button":"appmenu-close-button-vv9bV","current-route":"current-route-wpJgz","is-active":"is-active-wy8Bg","routes":"routes-Nmt1S","route":"route-kkknQ","profile-button":"profile-button-a2WO2"};
32903
+ var styles$6 = {"appmenu-button":"appmenu-button-b0UvT","appmenu-layer":"appmenu-layer-0IQpN","appmenu-container":"appmenu-container-gackd","appmenu-scroll":"appmenu-scroll-mPSx0","appmenu-list":"appmenu-list-h7clX","appmenu-list-a":"appmenu-list-a-rGIQN","appmenu-list-a--top":"appmenu-list-a--top-RT-2F","appmenu-list-a--bottom":"appmenu-list-a--bottom-YDeBC","appmenu-close-button":"appmenu-close-button-vv9bV","current-route":"current-route-wpJgz","is-active":"is-active-wy8Bg","routes":"routes-Nmt1S","route":"route-kkknQ","profile-button":"profile-button-a2WO2"};
32340
32904
 
32341
32905
  function AccountSettingsLink({
32342
32906
  env
32343
32907
  }) {
32344
32908
  const accountsName = "accounts";
32345
32909
  return /*#__PURE__*/React.createElement("a", {
32346
- className: classnames(styles$5["appmenu-list-a"], styles$5["appmenu-list-a--bottom"]),
32910
+ className: classnames(styles$6["appmenu-list-a"], styles$6["appmenu-list-a--bottom"]),
32347
32911
  "data-pendo": `app-switcher--change-app--${accountsName}`,
32348
32912
  "data-turbolinks": false,
32349
32913
  href: pcoUrl$1(env)(accountsName) + getAppsSwitchProductPath(accountsName)
@@ -32366,7 +32930,7 @@ true;
32366
32930
  }) => (/*#__PURE__*/React.createElement("button", {
32367
32931
  "aria-label": "Close menu",
32368
32932
  type: "button",
32369
- className: styles$5["appmenu-close-button"],
32933
+ className: styles$6["appmenu-close-button"],
32370
32934
  style: style
32371
32935
  }, /*#__PURE__*/React.createElement(X, {
32372
32936
  fill: color,
@@ -32427,7 +32991,7 @@ true;
32427
32991
  "aria-haspopup": "true",
32428
32992
  "aria-label": `App switcher: ${appName}`,
32429
32993
  type: "button",
32430
- className: styles$5["appmenu-button"],
32994
+ className: styles$6["appmenu-button"],
32431
32995
  onClick: onTriggerClick
32432
32996
  }, /*#__PURE__*/React.createElement(ColorAppIcon, {
32433
32997
  appName: appName.replace(/[\s-]/, "")
@@ -32435,7 +32999,7 @@ true;
32435
32999
  item: "Topbar:Small:Menus:Products:visibility",
32436
33000
  render: () => (/*#__PURE__*/React.createElement("div", {
32437
33001
  ref: appMenuRef,
32438
- className: styles$5["appmenu-layer"],
33002
+ className: styles$6["appmenu-layer"],
32439
33003
  style: {
32440
33004
  ...(visible && {
32441
33005
  display: "block"
@@ -32452,37 +33016,37 @@ true;
32452
33016
  e.stopPropagation();
32453
33017
  }
32454
33018
  },
32455
- className: styles$5["appmenu-container"]
33019
+ className: styles$6["appmenu-container"]
32456
33020
  }, /*#__PURE__*/React.createElement("a", {
32457
33021
  "aria-current": appName === (homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name) ? "page" : undefined,
32458
33022
  "aria-label": homeProduct ? homeProduct.name : appName,
32459
33023
  "data-pendo": `app-switcher--change-app--${homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name}`,
32460
33024
  "data-turbolinks": false,
32461
- className: classnames(styles$5["appmenu-list-a"], styles$5["appmenu-list-a--top"]),
33025
+ className: classnames(styles$6["appmenu-list-a"], styles$6["appmenu-list-a--top"]),
32462
33026
  href: homeProduct ? pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name) : undefined
32463
33027
  }, /*#__PURE__*/React.createElement(UserMenuAppLockup, {
32464
33028
  appName: homeProduct ? homeProduct.name : appName
32465
33029
  })), /*#__PURE__*/React.createElement("div", {
32466
- className: styles$5["appmenu-scroll"]
33030
+ className: styles$6["appmenu-scroll"]
32467
33031
  }, /*#__PURE__*/React.createElement("ul", {
32468
33032
  "data-pendo": "app-switcher",
32469
- className: styles$5["appmenu-list"]
33033
+ className: styles$6["appmenu-list"]
32470
33034
  }, appsMenuFormatter(restProducts).map(({
32471
33035
  name
32472
33036
  }) => (/*#__PURE__*/React.createElement("li", {
32473
33037
  key: name,
32474
- className: styles$5["appmenu-list-li"]
33038
+ className: styles$6["appmenu-list-li"]
32475
33039
  }, /*#__PURE__*/React.createElement("a", {
32476
33040
  "aria-current": name === appName ? "page" : undefined,
32477
33041
  "aria-label": name,
32478
33042
  "data-pendo": `app-switcher--change-app--${name}`,
32479
33043
  "data-turbolinks": false,
32480
- className: styles$5["appmenu-list-a"],
33044
+ className: styles$6["appmenu-list-a"],
32481
33045
  href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(name)
32482
33046
  }, /*#__PURE__*/React.createElement(UserMenuAppLockup, {
32483
33047
  appName: name
32484
33048
  }))))), accountsProduct && (/*#__PURE__*/React.createElement("li", {
32485
- className: styles$5["appmenu-list-li"]
33049
+ className: styles$6["appmenu-list-li"]
32486
33050
  }, /*#__PURE__*/React.createElement(AccountSettingsLink, {
32487
33051
  env: env
32488
33052
  }))))))))
@@ -32546,19 +33110,19 @@ true;
32546
33110
  toggle: toggleProductSwitcher,
32547
33111
  visible: productSwitcherVisible
32548
33112
  })), routesMenuVisible && (/*#__PURE__*/React.createElement("div", {
32549
- className: styles$5["routes"]
33113
+ className: styles$6["routes"]
32550
33114
  }, props.routes)), /*#__PURE__*/React.createElement("button", {
32551
33115
  type: "button",
32552
- className: classnames(styles$5["current-route"], {
32553
- [styles$5["is-active"]]: routesMenuVisible
33116
+ className: classnames(styles$6["current-route"], {
33117
+ [styles$6["is-active"]]: routesMenuVisible
32554
33118
  }),
32555
33119
  onClick: props.routes.length > 0 ? () => {
32556
33120
  setProductSwitcherVisible(false);
32557
33121
  setRoutesMenuVisible(visible => !visible);
32558
33122
  } : null
32559
- }, /*#__PURE__*/React.createElement(CurrentRouteComponent, null, capitalize(currentRouteText)), props.routes.length > 0 && /*#__PURE__*/React.createElement(DisclosureChevronIcon, null)), /*#__PURE__*/React.createElement("button", {
33123
+ }, /*#__PURE__*/React.createElement(CurrentRouteComponent, null, capitalize$1(currentRouteText)), props.routes.length > 0 && /*#__PURE__*/React.createElement(DisclosureChevronIcon, null)), /*#__PURE__*/React.createElement("button", {
32560
33124
  type: "button",
32561
- className: styles$5["profile-button"],
33125
+ className: styles$6["profile-button"],
32562
33126
  onClick: () => {
32563
33127
  var _a, _b;
32564
33128
  props.requestConnectedPeopleFetch();
@@ -32574,7 +33138,7 @@ true;
32574
33138
  className,
32575
33139
  ...props
32576
33140
  }) => (/*#__PURE__*/React.createElement("a", {
32577
- className: classnames(styles$5["route"], className, active && styles$5["is-active"]),
33141
+ className: classnames(styles$6["route"], className, active && styles$6["is-active"]),
32578
33142
  ...props
32579
33143
  }));
32580
33144
 
@@ -32713,7 +33277,7 @@ true;
32713
33277
  id: "topbar"
32714
33278
  };
32715
33279
 
32716
- var styles$4 = {"debug-pco-resource":"debug-pco-resource-sLteM","debug-pco-resource-header":"debug-pco-resource-header-5fEbe","debug-pco-resource-badge":"debug-pco-resource-badge-J9v1F","debug-pco-resource-title":"debug-pco-resource-title-volWV","debug-pco-resource-field":"debug-pco-resource-field-7Gitp","debug-pco-resource-field-label":"debug-pco-resource-field-label--f1xX","debug-pco-resource-field-value":"debug-pco-resource-field-value-YoX-C","debug-pco-resource-note":"debug-pco-resource-note-N5xyf","debug-pco-resource-json":"debug-pco-resource-json-Rx-JI"};
33280
+ var styles$5 = {"debug-pco-resource":"debug-pco-resource-sLteM","debug-pco-resource-header":"debug-pco-resource-header-5fEbe","debug-pco-resource-badge":"debug-pco-resource-badge-J9v1F","debug-pco-resource-title":"debug-pco-resource-title-volWV","debug-pco-resource-field":"debug-pco-resource-field-7Gitp","debug-pco-resource-field-label":"debug-pco-resource-field-label--f1xX","debug-pco-resource-field-value":"debug-pco-resource-field-value-YoX-C","debug-pco-resource-note":"debug-pco-resource-note-N5xyf","debug-pco-resource-json":"debug-pco-resource-json-Rx-JI"};
32717
33281
 
32718
33282
  function isPresent(value) {
32719
33283
  return value !== undefined && value.trim() !== "";
@@ -32742,15 +33306,31 @@ true;
32742
33306
  value
32743
33307
  }) {
32744
33308
  return /*#__PURE__*/React.createElement("div", {
32745
- className: styles$4["debug-pco-resource-field"]
33309
+ className: styles$5["debug-pco-resource-field"]
32746
33310
  }, /*#__PURE__*/React.createElement("span", {
32747
- className: styles$4["debug-pco-resource-field-label"]
33311
+ className: styles$5["debug-pco-resource-field-label"]
32748
33312
  }, label), /*#__PURE__*/React.createElement("span", {
32749
- className: styles$4["debug-pco-resource-field-value"]
33313
+ className: styles$5["debug-pco-resource-field-value"]
32750
33314
  }, value));
32751
33315
  }
33316
+ // Only staff ghosting or impersonating a person get pco:staff-access-level,
33317
+ // which keeps the panel away from regular users on staging.
32752
33318
  function DebugPcoResource({
32753
33319
  env
33320
+ }) {
33321
+ const {
33322
+ "staff-access-level": staffAccessLevel
33323
+ } = usePcoMeta();
33324
+ if (isPresent(staffAccessLevel)) {
33325
+ return /*#__PURE__*/React.createElement(DebugPanel, {
33326
+ env: env
33327
+ });
33328
+ } else {
33329
+ return null;
33330
+ }
33331
+ }
33332
+ function DebugPanel({
33333
+ env
32754
33334
  }) {
32755
33335
  const {
32756
33336
  recently_visitable: recentlyVisitable,
@@ -32759,12 +33339,17 @@ true;
32759
33339
  const [lookup, setLookup] = React.useState({
32760
33340
  status: "idle"
32761
33341
  });
32762
- const hasResource = isPresent(resource);
33342
+ const visit = useCurrentVisit();
33343
+ const personId = useCurrentPersonId();
33344
+ const recentlyVisited = useRecentlyVisited({
33345
+ env,
33346
+ personId
33347
+ });
33348
+ const pcrl = (visit === null || visit === void 0 ? void 0 : visit.type) === "resource" ? visit.pcrl : null;
32763
33349
  const hasRecentlyVisitable = isPresent(recentlyVisitable);
33350
+ const hasInvalidResource = isPresent(resource) && !isPcrl(resource);
32764
33351
  React.useEffect(() => {
32765
- // pco:resource is the more specific signal, so it always wins when both
32766
- // tags are present.
32767
- const url = hasResource ? pcrlLookupUrl(resource, env) : null;
33352
+ const url = pcrl ? pcrlLookupUrl(pcrl, env) : null;
32768
33353
  if (!url) {
32769
33354
  setLookup({
32770
33355
  status: "idle"
@@ -32796,37 +33381,45 @@ true;
32796
33381
  return () => {
32797
33382
  cancelled = true;
32798
33383
  };
32799
- }, [resource, env, hasResource]);
33384
+ }, [pcrl, env]);
32800
33385
  return /*#__PURE__*/React.createElement("div", {
32801
- className: classnames("staff-only", styles$4["debug-pco-resource"])
33386
+ className: classnames("staff-only", styles$5["debug-pco-resource"])
32802
33387
  }, /*#__PURE__*/React.createElement("div", {
32803
- className: styles$4["debug-pco-resource-header"]
33388
+ className: styles$5["debug-pco-resource-header"]
32804
33389
  }, /*#__PURE__*/React.createElement("span", {
32805
- className: styles$4["debug-pco-resource-badge"]
33390
+ className: styles$5["debug-pco-resource-badge"]
32806
33391
  }, "Staff"), /*#__PURE__*/React.createElement("span", {
32807
- className: styles$4["debug-pco-resource-title"]
33392
+ className: styles$5["debug-pco-resource-title"]
32808
33393
  }, "Recently Visited debug")), /*#__PURE__*/React.createElement(Field, {
32809
33394
  label: "pco:resource",
32810
33395
  value: describeTag("resource", resource)
32811
33396
  }), /*#__PURE__*/React.createElement(Field, {
32812
33397
  label: "pco:recently_visitable",
32813
33398
  value: describeTag("recently_visitable", recentlyVisitable)
32814
- }), !hasResource && !hasRecentlyVisitable && (/*#__PURE__*/React.createElement("p", {
32815
- className: styles$4["debug-pco-resource-note"]
32816
- }, "This page is not eligible for Recently Visited")), hasResource && hasRecentlyVisitable && (/*#__PURE__*/React.createElement("p", {
32817
- className: styles$4["debug-pco-resource-note"]
32818
- }, "Both tags are present \u2014 pco:resource takes precedence for the Recently Visited entry.")), hasResource && lookup.status === "loading" && (/*#__PURE__*/React.createElement("p", {
32819
- className: styles$4["debug-pco-resource-note"]
32820
- }, "Looking up resource\u2026")), hasResource && lookup.status === "error" && (/*#__PURE__*/React.createElement("p", {
32821
- className: styles$4["debug-pco-resource-note"]
32822
- }, lookup.message)), hasResource && lookup.status === "loaded" && (/*#__PURE__*/React.createElement("pre", {
32823
- className: styles$4["debug-pco-resource-json"]
32824
- }, JSON.stringify(lookup.json, null, 2))), !hasResource && hasRecentlyVisitable && (/*#__PURE__*/React.createElement("p", {
32825
- className: styles$4["debug-pco-resource-note"]
32826
- }, "Recently Visited title: ", recentlyVisitable)));
32827
- }
32828
-
32829
- var styles$3 = {"drawer-wrap":"drawer-wrap-J0rSL","is-visible":"is-visible-KTME8","drawer":"drawer-j7f4l"};
33399
+ }), hasInvalidResource && (/*#__PURE__*/React.createElement("p", {
33400
+ className: styles$5["debug-pco-resource-note"]
33401
+ }, "pco:resource isn't a PCRL (product/v2/type/id), so Recently Visited ignores it.")), visit === null && (/*#__PURE__*/React.createElement("p", {
33402
+ className: styles$5["debug-pco-resource-note"]
33403
+ }, "This page is not eligible for Recently Visited")), pcrl && hasRecentlyVisitable && (/*#__PURE__*/React.createElement("p", {
33404
+ className: styles$5["debug-pco-resource-note"]
33405
+ }, "Both tags are present \u2014 pco:resource takes precedence for the Recently Visited entry.")), pcrl && lookup.status === "loading" && (/*#__PURE__*/React.createElement("p", {
33406
+ className: styles$5["debug-pco-resource-note"]
33407
+ }, "Looking up resource\u2026")), pcrl && lookup.status === "error" && (/*#__PURE__*/React.createElement("p", {
33408
+ className: styles$5["debug-pco-resource-note"]
33409
+ }, lookup.message)), pcrl && lookup.status === "loaded" && (/*#__PURE__*/React.createElement("pre", {
33410
+ className: styles$5["debug-pco-resource-json"]
33411
+ }, JSON.stringify(lookup.json, null, 2))), (visit === null || visit === void 0 ? void 0 : visit.type) === "page" && (/*#__PURE__*/React.createElement("p", {
33412
+ className: styles$5["debug-pco-resource-note"]
33413
+ }, "Recently Visited title: ", visit.title)), /*#__PURE__*/React.createElement("span", {
33414
+ className: styles$5["debug-pco-resource-field-label"]
33415
+ }, "Recently Visited"), personId ? (/*#__PURE__*/React.createElement("pre", {
33416
+ className: styles$5["debug-pco-resource-json"]
33417
+ }, JSON.stringify(recentlyVisited, null, 2))) : (/*#__PURE__*/React.createElement("p", {
33418
+ className: styles$5["debug-pco-resource-note"]
33419
+ }, "No current person, so nothing is recorded.")));
33420
+ }
33421
+
33422
+ var styles$4 = {"drawer-wrap":"drawer-wrap-J0rSL","is-visible":"is-visible-KTME8","drawer":"drawer-j7f4l"};
32830
33423
 
32831
33424
  function Drawer({
32832
33425
  children,
@@ -32834,7 +33427,7 @@ true;
32834
33427
  }) {
32835
33428
  return /*#__PURE__*/React.createElement("div", {
32836
33429
  id: id,
32837
- className: styles$3["drawer"]
33430
+ className: styles$4["drawer"]
32838
33431
  }, children);
32839
33432
  }
32840
33433
  function DrawerWrap({
@@ -32844,7 +33437,7 @@ true;
32844
33437
  ...props
32845
33438
  }) {
32846
33439
  return /*#__PURE__*/React.createElement("div", {
32847
- className: classnames(styles$3["drawer-wrap"], className, isVisible && styles$3["is-visible"]),
33440
+ className: classnames(styles$4["drawer-wrap"], className, isVisible && styles$4["is-visible"]),
32848
33441
  ...props
32849
33442
  }, children);
32850
33443
  }
@@ -33028,15 +33621,50 @@ true;
33028
33621
  }));
33029
33622
  };
33030
33623
 
33031
- var styles$2 = {"color-mode-switcher":"color-mode-switcher-JpczO","color-mode-switcher-label":"color-mode-switcher-label-3jPM2","color-mode-switcher-modes":"color-mode-switcher-modes-u-bic"};
33624
+ const collections = {
33625
+ general: general__namespace,
33626
+ toolbar: toolbar__namespace
33627
+ };
33628
+ const paths = collections;
33629
+ function Icon({
33630
+ name,
33631
+ size = 16,
33632
+ style,
33633
+ theme,
33634
+ title,
33635
+ ...props
33636
+ }) {
33637
+ const [collection, symbol] = name.split(".");
33638
+ return /*#__PURE__*/React.createElement("svg", {
33639
+ "aria-hidden": title ? undefined : true,
33640
+ fill: "currentColor",
33641
+ height: size,
33642
+ role: title ? "img" : undefined,
33643
+ style: {
33644
+ color: theme && `var(${theme})`,
33645
+ flexShrink: 0,
33646
+ ...style
33647
+ },
33648
+ viewBox: "0 0 16 16",
33649
+ width: size,
33650
+ ...props
33651
+ }, title && /*#__PURE__*/React.createElement("title", null, title), /*#__PURE__*/React.createElement("path", {
33652
+ d: paths[collection][symbol]
33653
+ }));
33654
+ }
33655
+
33656
+ var styles$3 = {"color-mode-switcher":"color-mode-switcher-JpczO","color-mode-switcher-label":"color-mode-switcher-label-3jPM2","color-mode-switcher-modes":"color-mode-switcher-modes-u-bic"};
33032
33657
 
33033
33658
  const COLOR_MODES = [{
33659
+ icon: "toolbar.sun",
33034
33660
  label: "Light",
33035
33661
  value: "light"
33036
33662
  }, {
33663
+ icon: "toolbar.moon",
33037
33664
  label: "Dark",
33038
33665
  value: "dark"
33039
33666
  }, {
33667
+ icon: "toolbar.display",
33040
33668
  label: "System",
33041
33669
  value: "system"
33042
33670
  }];
@@ -33048,14 +33676,15 @@ true;
33048
33676
  const updateStore = useUpdateStore();
33049
33677
  const [counter] = React.useState(() => COLOR_MODE_INSTANCE_COUNTER++);
33050
33678
  return /*#__PURE__*/React.createElement("fieldset", {
33051
- className: styles$2["color-mode-switcher"]
33679
+ className: styles$3["color-mode-switcher"]
33052
33680
  }, /*#__PURE__*/React.createElement("legend", {
33053
- className: styles$2["color-mode-switcher-label"]
33681
+ className: styles$3["color-mode-switcher-label"]
33054
33682
  }, "Appearance", " "), /*#__PURE__*/React.createElement("div", {
33055
- className: styles$2["color-mode-switcher-modes"]
33683
+ className: styles$3["color-mode-switcher-modes"]
33056
33684
  }, COLOR_MODES.map(({
33057
- value,
33058
- label
33685
+ icon,
33686
+ label,
33687
+ value
33059
33688
  }) => (/*#__PURE__*/React.createElement("label", {
33060
33689
  key: value
33061
33690
  }, /*#__PURE__*/React.createElement("input", {
@@ -33072,8 +33701,9 @@ true;
33072
33701
  document.documentElement.setAttribute(RESOLVED_COLOR_MODE_ATTRIBUTE, resolveColorMode(colorMode));
33073
33702
  updateColorMode(colorMode);
33074
33703
  }
33075
- }), /*#__PURE__*/React.createElement(ColorModeIcon, {
33076
- mode: value
33704
+ }), /*#__PURE__*/React.createElement(Icon, {
33705
+ name: icon,
33706
+ size: 15
33077
33707
  }), label)))));
33078
33708
  }
33079
33709
  function updateColorMode(mode) {
@@ -33088,28 +33718,6 @@ true;
33088
33718
  method: "POST"
33089
33719
  });
33090
33720
  }
33091
- const COLOR_MODE_ICON_PATHS = {
33092
- dark: "M12.66667,9.07579 C11.90047,9.42229 11.04997,9.61514 10.15457,9.61514 C6.78392,9.61514 4.05151,6.88275 4.05151,3.51215 C4.05151,2.61663 4.24438,1.76615 4.59086,1 C2.47336,1.95762 1,4.08858 1,6.56366 C1,9.93423 3.73241,12.66663 7.103,12.66663 C9.57807,12.66663 11.70907,11.19333 12.66667,9.07579 Z",
33093
- light: "M6.83333,1 L6.83333,2.16667 M6.83333,11.50003 L6.83333,12.66663 M2.16667,6.83334 L1,6.83334 M3.51657,3.51657 L2.69161,2.69161 M10.1501,3.51657 L10.9751,2.69161 M3.51657,10.15253 L2.69161,10.97743 M10.1501,10.15253 L10.9751,10.97743 M12.6667,6.83334 L11.5,6.83334 M9.75,6.83334 C9.75,8.44415 8.44415,9.75 6.83333,9.75 C5.2225,9.75 3.91667,8.44415 3.91667,6.83334 C3.91667,5.22251 5.2225,3.91667 6.83333,3.91667 C8.44415,3.91667 9.75,5.22251 9.75,6.83334 Z",
33094
- system: "M4.75,12.50003 L9.41667,12.50003 M7.08334,10.16667 L7.08334,12.50003 M4.05,10.16667 L10.11665,10.16667 C11.09675,10.16667 11.58675,10.16667 11.96115,9.97592 C12.29045,9.80815 12.55815,9.54046 12.72595,9.21117 C12.91665,8.83678 12.91665,8.34678 12.91665,7.36667 L12.91665,4.8 C12.91665,3.81991 12.91665,3.32986 12.72595,2.95552 C12.55815,2.62623 12.29045,2.35852 11.96115,2.19074 C11.58675,2 11.09675,2 10.11665,2 L4.05,2 C3.06991,2 2.57986,2 2.20552,2.19074 C1.87623,2.35852 1.60852,2.62623 1.44074,2.95552 C1.25,3.32986 1.25,3.81991 1.25,4.8 L1.25,7.36667 C1.25,8.34678 1.25,8.83678 1.44074,9.21117 C1.60852,9.54046 1.87623,9.80815 2.20552,9.97592 C2.57986,10.16667 3.06991,10.16667 4.05,10.16667 Z"
33095
- };
33096
- function ColorModeIcon({
33097
- mode
33098
- }) {
33099
- return /*#__PURE__*/React.createElement("svg", {
33100
- width: "16",
33101
- height: "16",
33102
- viewBox: "0 0 14 14",
33103
- xmlns: "http://www.w3.org/2000/svg",
33104
- "aria-hidden": "true"
33105
- }, /*#__PURE__*/React.createElement("path", {
33106
- d: COLOR_MODE_ICON_PATHS[mode],
33107
- strokeWidth: "1.5",
33108
- fill: "none",
33109
- strokeLinecap: "round",
33110
- strokeLinejoin: "round"
33111
- }));
33112
- }
33113
33721
 
33114
33722
  const requestClearConnectedPeopleCache = () => remove();
33115
33723
  function UserProfile({
@@ -33172,10 +33780,10 @@ true;
33172
33780
  alignment: "center",
33173
33781
  spacing: 0.5,
33174
33782
  width: "100%"
33175
- }, /*#__PURE__*/React.createElement(tapestryReact.Icon, {
33783
+ }, /*#__PURE__*/React.createElement(Icon, {
33176
33784
  name: mfaConfigured ? "general.lock" : "general.unlock",
33177
- size: "sm",
33178
- color: mfaConfigured ? "var(--t-icon-color-status-success)" : "var(--t-icon-color-status-error)"
33785
+ size: 12,
33786
+ theme: mfaConfigured ? "--t-icon-color-status-success" : "--t-icon-color-status-error"
33179
33787
  }), /*#__PURE__*/React.createElement(tapestryReact.Text, {
33180
33788
  size: 4,
33181
33789
  weight: 400,
@@ -33223,9 +33831,10 @@ true;
33223
33831
  }, /*#__PURE__*/React.createElement(tapestryReact.Text, {
33224
33832
  color: "var(--text-color-default-secondary)",
33225
33833
  size: 3
33226
- }, "Log out"), /*#__PURE__*/React.createElement(tapestryReact.Icon, {
33834
+ }, "Log out"), /*#__PURE__*/React.createElement(Icon, {
33227
33835
  name: "general.intoBox",
33228
- fill: "var(--t-icon-color-secondary)"
33836
+ size: 14,
33837
+ theme: "--t-icon-color-secondary"
33229
33838
  })))), env !== "production" && /*#__PURE__*/React.createElement(DebugPcoResource, {
33230
33839
  env: env
33231
33840
  }), hasConnectedPeople && (/*#__PURE__*/React.createElement(tapestryReact.StackView, {
@@ -33287,26 +33896,42 @@ true;
33287
33896
 
33288
33897
  var ActionType;
33289
33898
  (function (ActionType) {
33899
+ ActionType["clearPending"] = "clearPending";
33290
33900
  ActionType["close"] = "close";
33291
33901
  ActionType["mount"] = "mount";
33292
33902
  ActionType["open"] = "open";
33903
+ ActionType["setPending"] = "setPending";
33293
33904
  ActionType["unmount"] = "unmount";
33294
33905
  })(ActionType || (ActionType = {}));
33295
33906
  const initialState = {
33296
33907
  currentContent: null,
33297
- open: false
33908
+ open: false,
33909
+ pending: null
33298
33910
  };
33299
33911
  const reducer = (state, action) => {
33912
+ var _a, _b;
33300
33913
  switch (action.type) {
33301
33914
  case ActionType.mount:
33302
33915
  return {
33303
33916
  ...state,
33304
- currentContent: action.payload.content
33917
+ currentContent: action.payload.content,
33918
+ pending: ((_a = state.pending) === null || _a === void 0 ? void 0 : _a.tool) === ((_b = action.payload.content) === null || _b === void 0 ? void 0 : _b.name) ? state.pending : null
33305
33919
  };
33306
33920
  case ActionType.unmount:
33307
33921
  return {
33308
33922
  ...state,
33309
- currentContent: null
33923
+ currentContent: null,
33924
+ pending: null
33925
+ };
33926
+ case ActionType.setPending:
33927
+ return {
33928
+ ...state,
33929
+ pending: action.payload
33930
+ };
33931
+ case ActionType.clearPending:
33932
+ return {
33933
+ ...state,
33934
+ pending: null
33310
33935
  };
33311
33936
  case ActionType.open:
33312
33937
  return {
@@ -33325,13 +33950,181 @@ true;
33325
33950
  ...overrides
33326
33951
  });
33327
33952
 
33328
- var styles$1 = {"toolbar":"toolbar-KHJR5","toolbar-button":"toolbar-button-Ye4h5","is-current":"is-current-DZK47","toolbar-button--avatar":"toolbar-button--avatar-5FUik","toolbar-button--home":"toolbar-button--home-Pthd0"};
33953
+ var styles$2 = {"toolbar":"toolbar-KHJR5","toolbar-button":"toolbar-button-Ye4h5","is-current":"is-current-DZK47","toolbar-button--avatar":"toolbar-button--avatar-5FUik","toolbar-button--home":"toolbar-button--home-Pthd0"};
33329
33954
 
33330
- const pcoUrl = pcoUrl$1;
33955
+ // PCO Box serves TLS by default, and a bare http:// frame inside an https
33956
+ // product page is blocked as mixed content, so follow the page's scheme.
33957
+ const iframeSrc = () => `${window.location.protocol}//pco.test/iframe`;
33958
+ const STAFF_PINK = "var(--t-fill-color-product-staff)";
33959
+ // The pco.test domain is only ever served by a local PCO Box, so the tool
33960
+ // stays hidden in staging and production — and on Topbar's own dev server,
33961
+ // which runs on localhost and has no pco.test to frame.
33962
+ function isPcoTestHost() {
33963
+ const {
33964
+ hostname
33965
+ } = window.location;
33966
+ return hostname === "pco.test" || hostname.endsWith(".pco.test");
33967
+ }
33968
+ // Flattened from the source artwork's two outline paths — its gradients,
33969
+ // filters and clip paths carry nothing at icon size. Those paths are authored
33970
+ // on a 128px canvas, hence the transform: it fits the pair into the same ~24px
33971
+ // optical box the sibling toolbar icons occupy. Filled explicitly rather than
33972
+ // with `currentColor` so the mark stays staff pink instead of inheriting the
33973
+ // toolbar's text color and its hover/active states.
33974
+ const WRENCH = "m22.42 17.2 12.96 12.95-2.5 8.78-8.78 2.5-12.96-12.96-5.86 5.86 14.89 21.46 17.56-3.34 61.63 65.62c9.44 9.51 22.17-4.75 13.34-12.87L46.85 42.92l4.01-17.76-22.14-14.27-6.3 6.31zm81.06 91.97c1.68-1.68 4.4-1.68 6.08 0 1.68 1.68 1.68 4.4 0 6.08-1.68 1.67-4.4 1.67-6.08 0-1.68-1.68-1.68-4.4 0-6.08z";
33975
+ const SCREWDRIVER = "m55.8 12 .76.16c8.84-2.94 28.68 8.64 24.26 18.96-1.12 1.62-.93 4.5.42 6.31L56.43 62.25c-1.39-1.39-4.48-1.36-5.9 0L9.5 101.63c-3.74 3.59-.8 6.64.78 8.25l7.36 7.36c1.61 1.6 5.86 3.76 8.88.56l38.72-40.81c1.35-1.42 1.36-4.47 0-5.87 0 0 .01-.02 0-.02l24.91-24.9c2.63 2.37 5.11 2.57 7.47 1.93 4.55-1.25 7.09 3.21 9.81 6.46l-1.23 1.21c-1.82 1.84-1.65 4.94.39 6.98s5.15 2.21 6.97.39l8.14-8.13c1.82-1.84 1.65-4.94-.4-6.97-1.84-1.85-4.51-2.09-6.37-.77-5.58-6.26-2.65-7.2-10.01-15.35L91.13 17.86C81.2 8.12 68.3 8.13 55.8 12z";
33976
+ function ToolsIcon() {
33977
+ return /*#__PURE__*/React.createElement("svg", {
33978
+ "aria-hidden": "true",
33979
+ width: "32",
33980
+ height: "32",
33981
+ viewBox: "0 0 32 32",
33982
+ xmlns: "http://www.w3.org/2000/svg"
33983
+ }, /*#__PURE__*/React.createElement("g", {
33984
+ fill: STAFF_PINK,
33985
+ transform: "translate(2.92 2.66) scale(.204)"
33986
+ }, /*#__PURE__*/React.createElement("path", {
33987
+ d: WRENCH
33988
+ }), /*#__PURE__*/React.createElement("path", {
33989
+ d: SCREWDRIVER
33990
+ })));
33991
+ }
33992
+ // A cross-origin frame's own history object is unreachable (touching it throws
33993
+ // a SecurityError), but the frame's navigations do land in the top-level joint
33994
+ // session history, so the *parent's* back/forward traverse them while leaving
33995
+ // the host page in place. Counting frame loads tracks how deep we are, which is
33996
+ // what keeps a click on Back from stepping past the frame's first page and
33997
+ // navigating the host app instead. Loads we caused are attributed to the
33998
+ // traversal that requested them rather than counted as new navigations.
33999
+ function useFrameHistory() {
34000
+ const [steps, setSteps] = React.useState({
34001
+ back: 0,
34002
+ forward: 0
34003
+ });
34004
+ // Traversal has to read the current counts outside of render, so the ref is
34005
+ // the source of truth and state exists to paint the buttons.
34006
+ const stepsRef = React.useRef(steps);
34007
+ const pendingTraversal = React.useRef(null);
34008
+ const frameIsLoaded = React.useRef(false);
34009
+ const moveTo = React.useCallback(next => {
34010
+ stepsRef.current = next;
34011
+ setSteps(next);
34012
+ }, []);
34013
+ const onFrameLoad = React.useCallback(() => {
34014
+ if (!frameIsLoaded.current) {
34015
+ // The frame's first page is the floor, not a step.
34016
+ frameIsLoaded.current = true;
34017
+ } else {
34018
+ const traversal = pendingTraversal.current;
34019
+ pendingTraversal.current = null;
34020
+ const {
34021
+ back,
34022
+ forward
34023
+ } = stepsRef.current;
34024
+ if (traversal === "back") {
34025
+ moveTo({
34026
+ back: back - 1,
34027
+ forward: forward + 1
34028
+ });
34029
+ } else if (traversal === "forward") {
34030
+ moveTo({
34031
+ back: back + 1,
34032
+ forward: forward - 1
34033
+ });
34034
+ } else {
34035
+ moveTo({
34036
+ back: back + 1,
34037
+ forward: 0
34038
+ });
34039
+ }
34040
+ }
34041
+ }, [moveTo]);
34042
+ // Guarded here rather than trusting the button's disabled state: traversing
34043
+ // with no frame step left would navigate the host app out from under us.
34044
+ const traverse = React.useCallback(direction => {
34045
+ if (stepsRef.current[direction] > 0) {
34046
+ pendingTraversal.current = direction;
34047
+ window.history[direction]();
34048
+ }
34049
+ }, []);
34050
+ return {
34051
+ onFrameLoad,
34052
+ steps,
34053
+ traverse
34054
+ };
34055
+ }
34056
+ function NavButton({
34057
+ direction,
34058
+ disabled,
34059
+ onClick
34060
+ }) {
34061
+ return /*#__PURE__*/React.createElement(tapestryReact.Button
34062
+ // Tapestry's `disabled` suppresses the click but emits no ARIA state.
34063
+ , {
34064
+ "aria-disabled": disabled,
34065
+ disabled: disabled,
34066
+ hover: {
34067
+ backgroundColor: "var(--t-fill-color-neutral-070)"
34068
+ },
34069
+ icon: {
34070
+ fill: "var(--t-icon-color-dim)",
34071
+ name: direction === "back" ? "general.leftChevron" : "general.rightChevron",
34072
+ size: "md"
34073
+ },
34074
+ onClick: onClick,
34075
+ opacity: disabled ? 0.35 : 1,
34076
+ theme: false,
34077
+ title: direction === "back" ? "Back" : "Forward"
34078
+ });
34079
+ }
34080
+ function DeveloperToolsComponent({
34081
+ closeDrawer
34082
+ }) {
34083
+ const {
34084
+ onFrameLoad,
34085
+ steps,
34086
+ traverse
34087
+ } = useFrameHistory();
34088
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(DrawerHeader, {
34089
+ closeDrawer: closeDrawer
34090
+ }, /*#__PURE__*/React.createElement(tapestryReact.StackView, {
34091
+ alignment: "center",
34092
+ axis: "horizontal",
34093
+ spacing: 0.25
34094
+ }, /*#__PURE__*/React.createElement(NavButton, {
34095
+ direction: "back",
34096
+ disabled: steps.back === 0,
34097
+ onClick: () => traverse("back")
34098
+ }), /*#__PURE__*/React.createElement(NavButton, {
34099
+ direction: "forward",
34100
+ disabled: steps.forward === 0,
34101
+ onClick: () => traverse("forward")
34102
+ }), /*#__PURE__*/React.createElement(DrawerHeading, {
34103
+ paddingLeft: 0.5
34104
+ }, "Developer Tools"))), /*#__PURE__*/React.createElement(tapestryReact.Box, {
34105
+ as: "iframe",
34106
+ border: "none",
34107
+ flex: "1",
34108
+ minHeight: 0,
34109
+ onLoad: onFrameLoad,
34110
+ src: iframeSrc(),
34111
+ title: "Developer Tools",
34112
+ width: "100%"
34113
+ }));
34114
+ }
34115
+ const DeveloperTools = {
34116
+ component: DeveloperToolsComponent,
34117
+ icons: {
34118
+ component: ToolsIcon
34119
+ },
34120
+ label: "Developer Tools",
34121
+ // No space: `name` becomes an element id and a pendo attribute.
34122
+ name: "DeveloperTools"
34123
+ };
33331
34124
 
33332
- var styles = {"banner":"banner-YfQ37","stacked":"stacked-EwLRh","content":"content-lNjWf","icon":"icon-abz7q","iconShape":"iconShape-6uyot","iconSparkle":"iconSparkle-t0OzK","text":"text--Vlhl","title":"title-zOEOR","subtitle":"subtitle-ufLnk","chevron":"chevron-oOUA-","inlineContent":"inlineContent-h5sVl","sparkle":"sparkle-uuMBp","inlineTitle":"inlineTitle-ru2Rk","action":"action-fSY3n","actionLabel":"actionLabel-MRBjb","actionChevron":"actionChevron-DghlN"};
34125
+ var styles$1 = {"banner":"banner-YfQ37","stacked":"stacked-EwLRh","content":"content-lNjWf","icon":"icon-abz7q","iconShape":"iconShape-6uyot","iconSparkle":"iconSparkle-t0OzK","text":"text--Vlhl","title":"title-zOEOR","subtitle":"subtitle-ufLnk","chevron":"chevron-oOUA-","inlineContent":"inlineContent-h5sVl","sparkle":"sparkle-uuMBp","inlineTitle":"inlineTitle-ru2Rk","action":"action-fSY3n","actionLabel":"actionLabel-MRBjb","actionChevron":"actionChevron-DghlN"};
33333
34126
 
33334
- const SPARKLE_PATH = "M3.809 10.1332C3.87511 9.95596 4.12579 9.95588 4.19181 10.1332L4.63712 11.3363C4.64169 11.3487 4.65116 11.359 4.66349 11.3637L5.86661 11.809C6.04452 11.8749 6.04455 12.126 5.86661 12.1918L4.66349 12.6371C4.65134 12.6417 4.64173 12.6513 4.63712 12.6635L4.19181 13.8666C4.12597 14.0445 3.87486 14.0445 3.809 13.8666L3.36368 12.6635C3.35905 12.6512 3.3487 12.6417 3.33634 12.6371L2.13321 12.1918C1.95588 12.1258 1.95596 11.8751 2.13321 11.809L3.33634 11.3637C3.34877 11.3591 3.35908 11.3488 3.36368 11.3363L3.809 10.1332ZM8.52091 3.33341C8.68571 2.88899 9.31518 2.88891 9.47989 3.33341L10.5922 6.34122C10.6037 6.37226 10.6285 6.39712 10.6596 6.4086L13.6664 7.52091C14.1113 7.68553 14.1113 8.31528 13.6664 8.47989L10.6596 9.5922C10.6285 9.6037 10.6037 9.6285 10.5922 9.65958L9.47989 12.6664C9.31528 13.1113 8.68553 13.1113 8.52091 12.6664L7.4086 9.65958C7.39712 9.62853 7.37226 9.60371 7.34122 9.5922L4.33341 8.47989C3.88891 8.31518 3.88899 7.68571 4.33341 7.52091L7.34122 6.4086C7.3723 6.3971 7.3971 6.3723 7.4086 6.34122L8.52091 3.33341ZM3.809 2.13321C3.87511 1.95596 4.12579 1.95588 4.19181 2.13321L4.63712 3.33634C4.64169 3.3487 4.65116 3.35905 4.66349 3.36368L5.86661 3.809C6.04452 3.87486 6.04455 4.12597 5.86661 4.19181L4.66349 4.63712C4.65134 4.64173 4.64173 4.65134 4.63712 4.66349L4.19181 5.86661C4.12597 6.04455 3.87486 6.04452 3.809 5.86661L3.36368 4.66349C3.35905 4.65116 3.3487 4.64169 3.33634 4.63712L2.13321 4.19181C1.95588 4.12579 1.95596 3.87511 2.13321 3.809L3.33634 3.36368C3.34877 3.35908 3.35908 3.34877 3.36368 3.33634L3.809 2.13321Z";
34127
+ const SPARKLE_PATH$1 = "M3.809 10.1332C3.87511 9.95596 4.12579 9.95588 4.19181 10.1332L4.63712 11.3363C4.64169 11.3487 4.65116 11.359 4.66349 11.3637L5.86661 11.809C6.04452 11.8749 6.04455 12.126 5.86661 12.1918L4.66349 12.6371C4.65134 12.6417 4.64173 12.6513 4.63712 12.6635L4.19181 13.8666C4.12597 14.0445 3.87486 14.0445 3.809 13.8666L3.36368 12.6635C3.35905 12.6512 3.3487 12.6417 3.33634 12.6371L2.13321 12.1918C1.95588 12.1258 1.95596 11.8751 2.13321 11.809L3.33634 11.3637C3.34877 11.3591 3.35908 11.3488 3.36368 11.3363L3.809 10.1332ZM8.52091 3.33341C8.68571 2.88899 9.31518 2.88891 9.47989 3.33341L10.5922 6.34122C10.6037 6.37226 10.6285 6.39712 10.6596 6.4086L13.6664 7.52091C14.1113 7.68553 14.1113 8.31528 13.6664 8.47989L10.6596 9.5922C10.6285 9.6037 10.6037 9.6285 10.5922 9.65958L9.47989 12.6664C9.31528 13.1113 8.68553 13.1113 8.52091 12.6664L7.4086 9.65958C7.39712 9.62853 7.37226 9.60371 7.34122 9.5922L4.33341 8.47989C3.88891 8.31518 3.88899 7.68571 4.33341 7.52091L7.34122 6.4086C7.3723 6.3971 7.3971 6.3723 7.4086 6.34122L8.52091 3.33341ZM3.809 2.13321C3.87511 1.95596 4.12579 1.95588 4.19181 2.13321L4.63712 3.33634C4.64169 3.3487 4.65116 3.35905 4.66349 3.36368L5.86661 3.809C6.04452 3.87486 6.04455 4.12597 5.86661 4.19181L4.66349 4.63712C4.65134 4.64173 4.64173 4.65134 4.63712 4.66349L4.19181 5.86661C4.12597 6.04455 3.87486 6.04452 3.809 5.86661L3.36368 4.66349C3.35905 4.65116 3.3487 4.64169 3.33634 4.63712L2.13321 4.19181C1.95588 4.12579 1.95596 3.87511 2.13321 3.809L3.33634 3.36368C3.34877 3.35908 3.35908 3.34877 3.36368 3.33634L3.809 2.13321Z";
33335
34128
  // Same stops as the AskAIBanner border gradient, so the two stay in sync.
33336
34129
  const GRADIENT_STOPS = [{
33337
34130
  color: "var(--t-fill-color-tag-teal-040)",
@@ -33380,7 +34173,7 @@ true;
33380
34173
  stopColor: color
33381
34174
  }
33382
34175
  })))))), /*#__PURE__*/React.createElement("path", {
33383
- d: SPARKLE_PATH,
34176
+ d: SPARKLE_PATH$1,
33384
34177
  fill: isGradient ? `url(#${gradientId})` : "currentColor"
33385
34178
  }));
33386
34179
  }
@@ -33389,8 +34182,8 @@ true;
33389
34182
  var _a, _b;
33390
34183
  const isStacked = props.variant !== "inline";
33391
34184
  return /*#__PURE__*/React.createElement("button", {
33392
- className: classnames(styles.banner, {
33393
- [styles.stacked]: isStacked
34185
+ className: classnames(styles$1.banner, {
34186
+ [styles$1.stacked]: isStacked
33394
34187
  }),
33395
34188
  onClick: () => props.onClick(),
33396
34189
  type: "button"
@@ -33405,15 +34198,15 @@ true;
33405
34198
  title
33406
34199
  }) {
33407
34200
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
33408
- className: styles.content
34201
+ className: styles$1.content
33409
34202
  }, /*#__PURE__*/React.createElement(AskAIIcon, null), /*#__PURE__*/React.createElement("span", {
33410
- className: styles.text
34203
+ className: styles$1.text
33411
34204
  }, /*#__PURE__*/React.createElement("span", {
33412
- className: styles.title
34205
+ className: styles$1.title
33413
34206
  }, title), /*#__PURE__*/React.createElement("span", {
33414
- className: styles.subtitle
34207
+ className: styles$1.subtitle
33415
34208
  }, "Support answers tailored to you"))), /*#__PURE__*/React.createElement(tapestryReact.Icon, {
33416
- className: styles.chevron,
34209
+ className: styles$1.chevron,
33417
34210
  name: "general.rightChevron",
33418
34211
  size: "16px"
33419
34212
  }));
@@ -33423,18 +34216,18 @@ true;
33423
34216
  title
33424
34217
  }) {
33425
34218
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
33426
- className: styles.inlineContent
34219
+ className: styles$1.inlineContent
33427
34220
  }, /*#__PURE__*/React.createElement(SparkleIcon, {
33428
- className: styles.sparkle,
34221
+ className: styles$1.sparkle,
33429
34222
  fill: "currentColor"
33430
34223
  }), /*#__PURE__*/React.createElement("span", {
33431
- className: styles.inlineTitle
34224
+ className: styles$1.inlineTitle
33432
34225
  }, title)), /*#__PURE__*/React.createElement("span", {
33433
- className: styles.action
34226
+ className: styles$1.action
33434
34227
  }, /*#__PURE__*/React.createElement("span", {
33435
- className: styles.actionLabel
34228
+ className: styles$1.actionLabel
33436
34229
  }, action), /*#__PURE__*/React.createElement(tapestryReact.Icon, {
33437
- className: styles.actionChevron,
34230
+ className: styles$1.actionChevron,
33438
34231
  name: "general.rightChevron",
33439
34232
  size: "14px"
33440
34233
  })));
@@ -33451,9 +34244,9 @@ true;
33451
34244
  const borderA = `${uid}-border-a`;
33452
34245
  const borderB = `${uid}-border-b`;
33453
34246
  return /*#__PURE__*/React.createElement("span", {
33454
- className: styles.icon
34247
+ className: styles$1.icon
33455
34248
  }, /*#__PURE__*/React.createElement("svg", {
33456
- className: styles.iconShape,
34249
+ className: styles$1.iconShape,
33457
34250
  viewBox: "0 0 32 32",
33458
34251
  fill: "none",
33459
34252
  "aria-hidden": "true"
@@ -33624,7 +34417,7 @@ true;
33624
34417
  stopColor: "#D48B91",
33625
34418
  stopOpacity: "0"
33626
34419
  })))), /*#__PURE__*/React.createElement("svg", {
33627
- className: styles.iconSparkle,
34420
+ className: styles$1.iconSparkle,
33628
34421
  viewBox: "0 0 19.4995 17.9999",
33629
34422
  "aria-hidden": "true"
33630
34423
  }, /*#__PURE__*/React.createElement("path", {
@@ -33633,6 +34426,23 @@ true;
33633
34426
  })));
33634
34427
  }
33635
34428
 
34429
+ var styles = {"button":"button-2NVI1","sparkle":"sparkle-ruJGP","label":"label-4NUGB"};
34430
+
34431
+ function AskAIButton({
34432
+ onClick,
34433
+ text = "Ask AI"
34434
+ }) {
34435
+ return /*#__PURE__*/React.createElement("button", {
34436
+ className: styles.button,
34437
+ onClick: () => onClick(),
34438
+ type: "button"
34439
+ }, /*#__PURE__*/React.createElement(SparkleIcon, {
34440
+ className: styles.sparkle
34441
+ }), /*#__PURE__*/React.createElement("span", {
34442
+ className: styles.label
34443
+ }, text));
34444
+ }
34445
+
33636
34446
  // The article heading lives in the deepest populated hierarchy level (lvl0 is
33637
34447
  // always "Planning Center").
33638
34448
  const hitTitle = hierarchy => (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl6) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl5) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl4) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl3) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl2) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl1) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl0) || "";
@@ -33788,16 +34598,24 @@ true;
33788
34598
  // not the path, so updating it is a same-document navigation that doesn't
33789
34599
  // reload the iframe, but also lets the route survive a forced reload (e.g.
33790
34600
  // Turbo re-parenting the topbar).
33791
- function buildPicobotSrc(origin, path) {
34601
+ //
34602
+ // `panel` loads picobot as its own toolbar panel rather than inside Helpdesk.
34603
+ function buildPicobotSrc(origin, path, {
34604
+ panel = false
34605
+ } = {}) {
33792
34606
  const [rawPath, queryPart] = path.replace(/^\/+/, "").split("?", 2);
33793
34607
  const safePath = rawPath.split("/").filter(segment => segment && segment !== "." && segment !== "..").join("/");
33794
34608
  const fragment = queryPart !== undefined ? `${safePath}?${queryPart}` : safePath;
33795
- const base = new URL("topbar", origin).toString();
34609
+ const url = new URL("topbar", origin);
34610
+ if (panel) url.searchParams.set("panel", "1");
34611
+ const base = url.toString();
33796
34612
  return fragment ? `${base}#${fragment}` : base;
33797
34613
  }
33798
34614
 
33799
34615
  const LOAD_TIMEOUT_MS = 3000;
33800
- function createPicobotIframe(picobotOrigin) {
34616
+ function createPicobotIframe(picobotOrigin, {
34617
+ panel = false
34618
+ } = {}) {
33801
34619
  return /*#__PURE__*/React.forwardRef(function PicobotIframe({
33802
34620
  currentPath: iframePath,
33803
34621
  onChangePath: onIframeChangePath,
@@ -33853,7 +34671,9 @@ true;
33853
34671
  (_b = (_a = iframeRef.current) === null || _a === void 0 ? void 0 : _a.contentWindow) === null || _b === void 0 ? void 0 : _b.postMessage(message, picobotOrigin);
33854
34672
  }
33855
34673
  }), []);
33856
- const iframeSrc = buildPicobotSrc(picobotOrigin, loadedPath);
34674
+ const iframeSrc = buildPicobotSrc(picobotOrigin, loadedPath, {
34675
+ panel
34676
+ });
33857
34677
  return /*#__PURE__*/React.createElement("iframe", {
33858
34678
  ref: iframeRef,
33859
34679
  src: iframeSrc,
@@ -33892,22 +34712,28 @@ true;
33892
34712
  currentPath,
33893
34713
  env,
33894
34714
  helpdeskConfig,
33895
- onChangePath
34715
+ isPicobotLoading,
34716
+ onChangePath,
34717
+ openPicobot
33896
34718
  }) {
33897
34719
  const PicobotIframe = React.useMemo(() => createPicobotIframe(pcoUrl(env)("picobot")), [env]);
34720
+ const loadingFallback = /*#__PURE__*/React.createElement(StackView$1, {
34721
+ flex: 1,
34722
+ axis: "vertical"
34723
+ }, /*#__PURE__*/React.createElement(DrawerHeader, {
34724
+ closeDrawer: closeDrawer
34725
+ }, /*#__PURE__*/React.createElement(StackView$1, {
34726
+ axis: "horizontal",
34727
+ spacing: 1
34728
+ }, /*#__PURE__*/React.createElement(DrawerHeading, null, "Help and support"), /*#__PURE__*/React.createElement(LoadingIndicator$1, null))));
34729
+ // The add-on reads its args once, so mounting before the gate answers
34730
+ // would leave out openPicobot for good.
34731
+ if (isPicobotLoading) return loadingFallback;
33898
34732
  return /*#__PURE__*/React.createElement(addOns.UiExtensionFirstPartyComponent, {
33899
34733
  grow: 1,
33900
34734
  display: "flex",
33901
34735
  extensionId: env === "staging" ? "42" : "11",
33902
- loadingFallback: /*#__PURE__*/React.createElement(StackView$1, {
33903
- flex: 1,
33904
- axis: "vertical"
33905
- }, /*#__PURE__*/React.createElement(DrawerHeader, {
33906
- closeDrawer: closeDrawer
33907
- }, /*#__PURE__*/React.createElement(StackView$1, {
33908
- axis: "horizontal",
33909
- spacing: 1
33910
- }, /*#__PURE__*/React.createElement(DrawerHeading, null, "Help and support"), /*#__PURE__*/React.createElement(LoadingIndicator$1, null)))),
34736
+ loadingFallback: loadingFallback,
33911
34737
  additionalComponents: {
33912
34738
  AskAIBanner: {
33913
34739
  component: AskAIBanner,
@@ -33927,6 +34753,18 @@ true;
33927
34753
  }
33928
34754
  }
33929
34755
  },
34756
+ AskAIButton: {
34757
+ component: AskAIButton,
34758
+ properties: {
34759
+ onClick: {
34760
+ event: false,
34761
+ type: Function
34762
+ },
34763
+ text: {
34764
+ type: String
34765
+ }
34766
+ }
34767
+ },
33930
34768
  CalendlyWidget: {
33931
34769
  component: CalendlyWidget,
33932
34770
  properties: {
@@ -34005,7 +34843,11 @@ true;
34005
34843
  currentPageParams: location.search,
34006
34844
  currentPath,
34007
34845
  helpdeskConfig,
34008
- onChangePath
34846
+ onChangePath,
34847
+ // Its presence tells helpdesk that Picobot has its own panel.
34848
+ ...(openPicobot && {
34849
+ openPicobot
34850
+ })
34009
34851
  }
34010
34852
  });
34011
34853
  }
@@ -34552,6 +35394,142 @@ true;
34552
35394
  name: "Notifications"
34553
35395
  };
34554
35396
 
35397
+ const SPARKLE_PATH = "M8.7824 18.8791C8.88371 18.5973 9.26996 18.5973 9.37127 18.8791L9.94256 20.465C10.0222 20.6863 10.1914 20.8616 10.4064 20.9435L11.6779 21.4269L11.9484 21.5304L11.9963 21.5539C12.2205 21.6839 12.2047 22.039 11.9484 22.1369L11.6779 22.2394L10.4064 22.7248C10.1915 22.8068 10.0222 22.9812 9.94256 23.2023L9.37127 24.7893L9.34881 24.8381C9.23098 25.054 8.92273 25.0539 8.80486 24.8381L8.7824 24.7893L8.2111 23.2023C8.14141 23.0088 8.0029 22.8513 7.82536 22.76L7.74723 22.7248L6.47573 22.2394L6.20522 22.1369C5.93172 22.0325 5.93164 21.6347 6.20522 21.5304L6.47476 21.4269L7.74723 20.9435C7.96226 20.8616 8.13147 20.6863 8.2111 20.465L8.7824 18.8791ZM17.5706 8.11138C17.8241 7.40747 18.7928 7.40734 19.0462 8.11138L20.472 12.0782C20.6711 12.6318 21.0952 13.0687 21.6331 13.2735L25.4867 14.7413C26.1711 15.002 26.1711 15.9983 25.4867 16.2589L21.6331 17.7267C21.0952 17.9316 20.6711 18.3684 20.472 18.922L19.2649 22.2785L19.0462 22.8889L18.9905 23.0119C18.6956 23.5517 17.9212 23.5515 17.6262 23.0119L17.5706 22.8889L17.3508 22.2795L16.1448 18.922C15.9457 18.3684 15.5206 17.9316 14.9827 17.7267L11.1291 16.2589C10.4449 15.9982 10.4449 15.0021 11.1291 14.7413L14.9827 13.2735C15.4533 13.0943 15.8379 12.7376 16.0598 12.2804L16.1448 12.0782L17.5706 8.11138ZM18.0266 12.755C17.6351 13.8438 16.7916 14.7249 15.6946 15.1427L14.7551 15.5001L15.6946 15.8575C16.7916 16.2753 17.6351 17.1564 18.0266 18.2453L18.3079 19.0275L18.5891 18.2453C18.9806 17.1564 19.8242 16.2753 20.9212 15.8575L21.8597 15.5001L20.9212 15.1427C19.8242 14.7249 18.9806 13.8439 18.5891 12.755L18.3079 11.9718L18.0266 12.755ZM8.7824 6.21097C8.88388 5.9297 9.26982 5.92966 9.37127 6.21097L9.94256 7.7979C10.0222 8.01911 10.1915 8.19346 10.4064 8.27544L11.6779 8.75982L11.8322 9.16607L11.9484 8.86334C12.2218 8.96779 12.2218 9.36542 11.9484 9.46979L11.6779 9.57233L10.4064 10.0567C10.1914 10.1387 10.0222 10.3139 9.94256 10.5352L9.37127 12.1212C9.26996 12.4029 8.88371 12.4029 8.7824 12.1212L8.2111 10.5352C8.14139 10.3415 8.00304 10.1832 7.82536 10.0919L7.74723 10.0567L6.47573 9.57233L6.20522 9.46979C5.9486 9.37195 5.93265 9.01568 6.15737 8.8858L6.20522 8.86334L7.74723 8.27544C7.96214 8.19346 8.13149 8.0191 8.2111 7.7979L8.69451 6.45413L8.7824 6.21097ZM11.9484 8.86334L11.8322 9.16607L11.6779 8.75982L11.9484 8.86334Z";
35398
+ // Helpdesk reads this context off the email path to open its email form the
35399
+ // same way a handoff inside its own drawer does.
35400
+ function emailPathWithContext(context = {}) {
35401
+ const params = new URLSearchParams();
35402
+ for (const key of ["conversationId", "source", "turnIndex"]) {
35403
+ const value = context[key];
35404
+ if (value !== null && value !== undefined) params.set(key, String(value));
35405
+ }
35406
+ return `email?${params}`;
35407
+ }
35408
+ function PicobotComponent({
35409
+ clearPending,
35410
+ closeDrawer,
35411
+ currentPath,
35412
+ env,
35413
+ onChangePath,
35414
+ openTool,
35415
+ pending
35416
+ }) {
35417
+ const PicobotIframe = React.useMemo(() => createPicobotIframe(pcoUrl(env)("picobot"), {
35418
+ panel: true
35419
+ }), [env]);
35420
+ const picobotRef = React.useRef(null);
35421
+ const [isPicobotReady, setIsPicobotReady] = React.useState(false);
35422
+ // Picobot has no listener until it sends READY, so openPicobot's draft or
35423
+ // conversation waits for it.
35424
+ React.useEffect(function handOverPendingOncePicobotIsReady() {
35425
+ var _a, _b;
35426
+ if (!pending || !isPicobotReady) return;
35427
+ const {
35428
+ conversationId,
35429
+ draft,
35430
+ ticketExternalId
35431
+ } = pending;
35432
+ if (conversationId) {
35433
+ (_a = picobotRef.current) === null || _a === void 0 ? void 0 : _a.postMessage({
35434
+ payload: {
35435
+ conversation_id: conversationId,
35436
+ // Picobot asks whether this resolved the ticket, and closes it.
35437
+ ...(ticketExternalId && {
35438
+ external_id: ticketExternalId,
35439
+ source: "ticket_submitted"
35440
+ })
35441
+ },
35442
+ type: "LOAD_CONVERSATION"
35443
+ });
35444
+ } else if (draft) {
35445
+ // Not both: Picobot cancels a conversation load when a prefill
35446
+ // starts a new conversation.
35447
+ (_b = picobotRef.current) === null || _b === void 0 ? void 0 : _b.postMessage({
35448
+ payload: {
35449
+ source: "email_form",
35450
+ text: draft
35451
+ },
35452
+ type: "PREFILL_COMPOSER"
35453
+ });
35454
+ }
35455
+ clearPending();
35456
+ }, [pending, isPicobotReady, clearPending]);
35457
+ const [iframeErrored, setIframeErrored] = React.useState(false);
35458
+ const [mountKey, setMountKey] = React.useState(0);
35459
+ const handleRetry = React.useCallback(() => {
35460
+ setIframeErrored(false);
35461
+ setIsPicobotReady(false);
35462
+ setMountKey(key => key + 1);
35463
+ }, []);
35464
+ const handleMessage = React.useCallback(data => {
35465
+ var _a;
35466
+ const message = data;
35467
+ switch (message === null || message === void 0 ? void 0 : message.type) {
35468
+ case "CLOSE_DRAWER":
35469
+ return closeDrawer();
35470
+ case "READY":
35471
+ return setIsPicobotReady(true);
35472
+ case "PICOBOT_SUPPORT_HANDOFF":
35473
+ return openTool("helpdesk", ((_a = message.payload) === null || _a === void 0 ? void 0 : _a.method) === "zoom" ? "phone" : emailPathWithContext(message.payload));
35474
+ case "IFRAME_LOAD_ERROR":
35475
+ console.warn("topbar: Picobot iframe failed to load");
35476
+ return setIframeErrored(true);
35477
+ }
35478
+ }, [closeDrawer, openTool]);
35479
+ if (iframeErrored) {
35480
+ return /*#__PURE__*/React.createElement(LoadError, {
35481
+ onContactSupport: () => openTool("helpdesk", "email"),
35482
+ onRetry: handleRetry
35483
+ });
35484
+ }
35485
+ return /*#__PURE__*/React.createElement(PicobotIframe, {
35486
+ key: mountKey,
35487
+ ref: picobotRef,
35488
+ currentPath: currentPath,
35489
+ onChangePath: onChangePath,
35490
+ onMessage: handleMessage
35491
+ });
35492
+ }
35493
+ function LoadError({
35494
+ onContactSupport,
35495
+ onRetry
35496
+ }) {
35497
+ return /*#__PURE__*/React.createElement(tapestryReact.StackView, {
35498
+ axis: "vertical",
35499
+ flex: 1,
35500
+ alignment: "center",
35501
+ distribution: "center",
35502
+ padding: 3,
35503
+ spacing: 1
35504
+ }, /*#__PURE__*/React.createElement(tapestryReact.Heading, {
35505
+ level: 2
35506
+ }, "Picobot is temporarily unavailable"), /*#__PURE__*/React.createElement(tapestryReact.Text, {
35507
+ size: 5,
35508
+ color: "var(--t-text-color-secondary)"
35509
+ }, "Try again or contact support."), /*#__PURE__*/React.createElement(tapestryReact.Box, {
35510
+ paddingTop: 2
35511
+ }, /*#__PURE__*/React.createElement(tapestryReact.Button, {
35512
+ onClick: onRetry,
35513
+ iconLeft: "general.refresh",
35514
+ theme: "primary",
35515
+ variant: "fill",
35516
+ title: "Retry"
35517
+ })), /*#__PURE__*/React.createElement(tapestryReact.Button, {
35518
+ onClick: onContactSupport,
35519
+ variant: "naked",
35520
+ theme: "primary",
35521
+ title: "Contact support"
35522
+ }));
35523
+ }
35524
+ const Picobot = {
35525
+ component: PicobotComponent,
35526
+ icons: {
35527
+ staticDotPath: SPARKLE_PATH,
35528
+ staticPath: SPARKLE_PATH
35529
+ },
35530
+ name: "Picobot"
35531
+ };
35532
+
34555
35533
  const {
34556
35534
  LoadingIndicator,
34557
35535
  StackView
@@ -34947,6 +35925,41 @@ true;
34947
35925
  };
34948
35926
  };
34949
35927
 
35928
+ // Picobot's API rejects requests without a version header, and apiRequest
35929
+ // sets its own headers, so the version rides in on the fetch.
35930
+ const picobotFetch = (url, opts) => defaultFetch(url, {
35931
+ ...opts,
35932
+ headers: {
35933
+ ...(opts === null || opts === void 0 ? void 0 : opts.headers),
35934
+ "X-PCO-API-Version": "2026-08-11"
35935
+ }
35936
+ });
35937
+ // Routes wait on the gate, so a slow Picobot can't hold them up for long.
35938
+ const GATE_TIMEOUT_MS = 3000;
35939
+ const usePicobotConfig = ({
35940
+ isEnabled
35941
+ }) => {
35942
+ // Undefined until Picobot answers.
35943
+ const [panelEnabled, setPanelEnabled] = React.useState();
35944
+ React.useEffect(() => {
35945
+ if (!isEnabled) return undefined;
35946
+ // Whatever settles first wins, so a late answer can't reopen the gate.
35947
+ const settle = enabled => setPanelEnabled(current => current !== null && current !== void 0 ? current : enabled);
35948
+ const timeout = setTimeout(() => settle(false), GATE_TIMEOUT_MS);
35949
+ apiRequest(picobotFetch, `${window.location.origin}/~api/picobot/v2/me`).then(({
35950
+ json
35951
+ }) => {
35952
+ var _a, _b;
35953
+ return settle(((_b = (_a = json === null || json === void 0 ? void 0 : json.data) === null || _a === void 0 ? void 0 : _a.attributes) === null || _b === void 0 ? void 0 : _b.picobot_panel_enabled) === true);
35954
+ }).catch(() => settle(false)).finally(() => clearTimeout(timeout));
35955
+ return () => clearTimeout(timeout);
35956
+ }, [isEnabled]);
35957
+ return {
35958
+ displayPicobotIcon: panelEnabled === true,
35959
+ isLoading: isEnabled && panelEnabled === undefined
35960
+ };
35961
+ };
35962
+
34950
35963
  const Profile = {
34951
35964
  component: UserProfile,
34952
35965
  icons: {
@@ -34957,10 +35970,16 @@ true;
34957
35970
  };
34958
35971
 
34959
35972
  const DURATION = 100;
35973
+ // Exempt paths match regardless of query, so `email?conversationId=…` is
35974
+ // exempt along with `email`.
34960
35975
  function isToolPathExemptFromStorage(toolName, path) {
34961
35976
  var _a, _b;
34962
35977
  const tool = toolsRouteMap[toolName];
34963
- return (_b = (_a = tool === null || tool === void 0 ? void 0 : tool.storageExemptPaths) === null || _a === void 0 ? void 0 : _a.includes(path)) !== null && _b !== void 0 ? _b : false;
35978
+ const [pathWithoutQuery] = path.split("?");
35979
+ return (_b = (_a = tool === null || tool === void 0 ? void 0 : tool.storageExemptPaths) === null || _a === void 0 ? void 0 : _a.includes(pathWithoutQuery)) !== null && _b !== void 0 ? _b : false;
35980
+ }
35981
+ function isConversationPath(path) {
35982
+ return Boolean(path === null || path === void 0 ? void 0 : path.startsWith("conversations/"));
34964
35983
  }
34965
35984
  function scrollToolDrawerToTop(toolName) {
34966
35985
  var _a;
@@ -34973,6 +35992,7 @@ true;
34973
35992
  Chat,
34974
35993
  Helpdesk,
34975
35994
  Notifications,
35995
+ Picobot,
34976
35996
  Profile,
34977
35997
  Tasks
34978
35998
  };
@@ -35000,7 +36020,8 @@ true;
35000
36020
  });
35001
36021
  const {
35002
36022
  currentContent,
35003
- open
36023
+ open,
36024
+ pending
35004
36025
  } = state;
35005
36026
  const mounted = !!currentContent;
35006
36027
  const storeData = useStore();
@@ -35013,6 +36034,15 @@ true;
35013
36034
  ...(env === "development" ? _storeDataOverride !== null && _storeDataOverride !== void 0 ? _storeDataOverride : {} : {})
35014
36035
  };
35015
36036
  const updateStore = useUpdateStore();
36037
+ const {
36038
+ displayPicobotIcon,
36039
+ isLoading: isPicobotGateLoading
36040
+ } = usePicobotConfig({
36041
+ isEnabled: Boolean(userId)
36042
+ });
36043
+ // Picobot is a kill-switched tool: until its gate says the panel is
36044
+ // enabled, its routes are held, and if it never does, they're ignored.
36045
+ const routableTool = React.useCallback(name => name === Picobot.name.toLowerCase() && !displayPicobotIcon ? undefined : toolsRouteMap[name], [displayPicobotIcon]);
35016
36046
  React.useEffect(() => {
35017
36047
  if (open) {
35018
36048
  document.body.style.overflow = "hidden";
@@ -35100,7 +36130,9 @@ true;
35100
36130
  });
35101
36131
  }, [clearRoute]);
35102
36132
  React.useEffect(() => {
35103
- if (!toolbarFragmentRoute) return;
36133
+ if (!toolbarFragmentRoute || !routableTool(toolbarFragmentRoute.tool)) {
36134
+ return;
36135
+ }
35104
36136
  const isExempt = isToolPathExemptFromStorage(toolbarFragmentRoute.tool, toolbarFragmentRoute.path);
35105
36137
  if (isExempt) return;
35106
36138
  const nextState = {
@@ -35110,12 +36142,32 @@ true;
35110
36142
  if (!shallowCompare(nextState, toolStateCookie)) {
35111
36143
  setToolStateCookie(nextState);
35112
36144
  }
35113
- }, [toolbarFragmentRoute, toolStateCookie]);
36145
+ }, [toolbarFragmentRoute, toolStateCookie, routableTool]);
35114
36146
  React.useEffect(() => {
35115
- if ((toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && (currentContent !== toolsRouteMap[toolbarFragmentRoute.tool] || !open)) {
35116
- setToolAndOpenDrawer(toolsRouteMap[toolbarFragmentRoute.tool]);
36147
+ // Helpdesk conversations may belong to Picobot, so they wait for its gate
36148
+ // rather than flashing open in Helpdesk first.
36149
+ const mayRedirectToPicobot = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) === Helpdesk.name.toLowerCase() && isConversationPath(toolbarFragmentRoute.path) && (isPicobotGateLoading || displayPicobotIcon);
36150
+ const tool = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && !mayRedirectToPicobot && routableTool(toolbarFragmentRoute.tool);
36151
+ if (tool && (currentContent !== tool || !open)) {
36152
+ setToolAndOpenDrawer(tool);
35117
36153
  }
35118
- }, [setToolAndOpenDrawer, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool, currentContent, open]);
36154
+ }, [setToolAndOpenDrawer, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path, currentContent, open, routableTool, isPicobotGateLoading, displayPicobotIcon]);
36155
+ const openTool = React.useCallback((name, path, pendingValue) => {
36156
+ const tool = routableTool(name.toLowerCase());
36157
+ if (!tool) return;
36158
+ dispatch({
36159
+ payload: pendingValue === undefined ? null : {
36160
+ tool: tool.name,
36161
+ value: pendingValue
36162
+ },
36163
+ type: ActionType.setPending
36164
+ });
36165
+ onChangeToolbarFragmentRoute(tool.name.toLowerCase(), path);
36166
+ scrollToolDrawerToTop(tool.name);
36167
+ }, [onChangeToolbarFragmentRoute, routableTool]);
36168
+ const clearPending = React.useCallback(() => dispatch({
36169
+ type: ActionType.clearPending
36170
+ }), []);
35119
36171
  const toggleDrawer = content => {
35120
36172
  if (!open) {
35121
36173
  return openDrawer(content);
@@ -35132,12 +36184,52 @@ true;
35132
36184
  joltPromise: storeData === null || storeData === void 0 ? void 0 : storeData.joltPromise,
35133
36185
  userId
35134
36186
  });
35135
- const TOOLS = [Notifications, Tasks, displayChatIcon && Chat].filter(Boolean);
36187
+ // Picobot conversations moved from Helpdesk to the Picobot panel, so
36188
+ // send old links and saved paths there once the panel is enabled.
36189
+ React.useEffect(() => {
36190
+ if (!displayPicobotIcon) return;
36191
+ if ((toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) === Helpdesk.name.toLowerCase() && isConversationPath(toolbarFragmentRoute.path)) {
36192
+ onChangeToolbarFragmentRoute(Picobot.name.toLowerCase(), toolbarFragmentRoute.path);
36193
+ }
36194
+ }, [displayPicobotIcon, toolbarFragmentRoute, onChangeToolbarFragmentRoute]);
36195
+ React.useEffect(() => {
36196
+ if (!displayPicobotIcon) return;
36197
+ const {
36198
+ helpdesk,
36199
+ picobot
36200
+ } = toolStateCookie;
36201
+ if (!isConversationPath(helpdesk)) return;
36202
+ setToolStateCookie({
36203
+ ...toolStateCookie,
36204
+ helpdesk: "",
36205
+ picobot: picobot || helpdesk
36206
+ });
36207
+ }, [displayPicobotIcon, toolStateCookie]);
36208
+ const openPicobot = React.useCallback(options => {
36209
+ var _a;
36210
+ // Moving an open Picobot off its path reloads or navigates the iframe,
36211
+ // which would race the options it's being handed.
36212
+ const path = currentContent === Picobot ? (_a = toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path) !== null && _a !== void 0 ? _a : "" : "";
36213
+ openTool(Picobot.name, path, options);
36214
+ }, [openTool, currentContent, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path]);
36215
+ React.useEffect(() => {
36216
+ if (!displayPicobotIcon) return undefined;
36217
+ window.topbar = window.topbar || {};
36218
+ window.topbar.openPicobot = openPicobot;
36219
+ return () => {
36220
+ delete window.topbar.openPicobot;
36221
+ };
36222
+ }, [displayPicobotIcon, openPicobot]);
36223
+ const TOOLS = [Notifications, Tasks, displayChatIcon && Chat, displayPicobotIcon && Picobot, isPcoTestHost() && DeveloperTools].filter(Boolean);
35136
36224
  const handleToolOnClick = tool => {
35137
36225
  toggleDrawer(tool);
35138
36226
  scrollToolDrawerToTop(tool.name);
35139
36227
  };
35140
36228
  const toolSpecificProps = {
36229
+ Helpdesk: {
36230
+ isPicobotLoading: isPicobotGateLoading,
36231
+ openPicobot: displayPicobotIcon ? openPicobot : undefined
36232
+ },
35141
36233
  Profile: {
35142
36234
  appName,
35143
36235
  closeDrawer,
@@ -35160,6 +36252,7 @@ true;
35160
36252
  const routeIsForCurrentTool = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && toolsRouteMap[toolbarFragmentRoute.tool] === currentContent;
35161
36253
  const currentToolProps = {
35162
36254
  appName,
36255
+ clearPending,
35163
36256
  closeDrawer,
35164
36257
  currentPath: routeIsForCurrentTool ? toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path : null,
35165
36258
  env,
@@ -35168,6 +36261,8 @@ true;
35168
36261
  joltPromise,
35169
36262
  onChangePath,
35170
36263
  openDrawer: () => currentContent && openDrawer(currentContent),
36264
+ openTool,
36265
+ pending: (pending === null || pending === void 0 ? void 0 : pending.tool) === (currentContent === null || currentContent === void 0 ? void 0 : currentContent.name) ? pending === null || pending === void 0 ? void 0 : pending.value : undefined,
35171
36266
  userId,
35172
36267
  ...(currentContent ? toolSpecificProps[currentContent.name] : {})
35173
36268
  };
@@ -35178,38 +36273,37 @@ true;
35178
36273
  return /*#__PURE__*/React.createElement(tapestryReact.ThemeProvider, {
35179
36274
  theme: topbarTheme
35180
36275
  }, /*#__PURE__*/React.createElement(addOns.UiExtensionDataContext, null, /*#__PURE__*/React.createElement("div", {
35181
- className: styles$1["toolbar"],
36276
+ className: styles$2["toolbar"],
35182
36277
  "data-pendo": "toolbar-profile-support-wrapper"
35183
36278
  }, /*#__PURE__*/React.createElement(ToolbarButton, {
35184
36279
  "aria-label": `Open ${userName}’s profile drawer`,
35185
36280
  onClick: () => toggleDrawer(Profile),
35186
36281
  "data-pendo": "toolbar-profile",
35187
- className: styles$1["toolbar-button--avatar"]
36282
+ className: styles$2["toolbar-button--avatar"]
35188
36283
  }, /*#__PURE__*/React.createElement(Avatar, {
35189
36284
  env: env,
35190
36285
  url: userAvatarPath
35191
36286
  })), /*#__PURE__*/React.createElement(ToolbarButton, {
35192
- className: styles$1["toolbar-button--home"],
36287
+ className: styles$2["toolbar-button--home"],
35193
36288
  onClick: () => closeDrawer()
35194
36289
  }, /*#__PURE__*/React.createElement(tapestryReact.Icon, {
35195
36290
  name: "toolbar.planningCenter",
35196
36291
  size: "24px"
35197
36292
  })), [HelpDeskTool, ...TOOLS].map(tool => {
36293
+ var _a;
35198
36294
  const current = open && currentContent === tool;
35199
36295
  const indicator = indicatorState[tool.name];
35200
36296
  return /*#__PURE__*/React.createElement(ToolbarButton, {
35201
36297
  current: current,
35202
36298
  onClick: () => handleToolOnClick(tool),
35203
36299
  id: getDataPendoAttribute(tool.name),
35204
- "aria-label": `Open ${tool.name} drawer`,
36300
+ "aria-label": `Open ${(_a = tool.label) !== null && _a !== void 0 ? _a : tool.name} drawer`,
35205
36301
  "data-pendo": getDataPendoAttribute(tool.name),
35206
36302
  key: tool.name
35207
- }, !indicator && (/*#__PURE__*/React.createElement(ToolbarButtonIcon, {
35208
- path: tool.icons.staticPath
35209
- })), indicator && (/*#__PURE__*/React.createElement(ToolbarButtonIcon, {
35210
- path: tool.icons.staticDotPath,
35211
- dot: true
35212
- })));
36303
+ }, /*#__PURE__*/React.createElement(ToolbarToolIcon, {
36304
+ icons: tool.icons,
36305
+ dot: !!indicator
36306
+ }));
35213
36307
  })), /*#__PURE__*/React.createElement(DrawerWrap, {
35214
36308
  isVisible: open
35215
36309
  }, mounted && (/*#__PURE__*/React.createElement(Drawer, {
@@ -35227,12 +36321,25 @@ true;
35227
36321
  }) => {
35228
36322
  return /*#__PURE__*/React.createElement("button", {
35229
36323
  type: "button",
35230
- className: classnames(styles$1["toolbar-button"], className, current && styles$1["is-current"]),
36324
+ className: classnames(styles$2["toolbar-button"], className, current && styles$2["is-current"]),
35231
36325
  onClick: onClick,
35232
36326
  "data-toolbar-link": "true",
35233
36327
  ...restProps
35234
36328
  }, children);
35235
36329
  };
36330
+ function ToolbarToolIcon({
36331
+ dot,
36332
+ icons
36333
+ }) {
36334
+ if ("component" in icons) {
36335
+ return /*#__PURE__*/React.createElement(icons.component, null);
36336
+ } else {
36337
+ return /*#__PURE__*/React.createElement(ToolbarButtonIcon, {
36338
+ path: dot ? icons.staticDotPath : icons.staticPath,
36339
+ dot: dot
36340
+ });
36341
+ }
36342
+ }
35236
36343
  function ToolbarButtonIcon({
35237
36344
  dot,
35238
36345
  path