@planningcenter/topbar 12.3.0 → 12.4.0-qa-958.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  (function (global, factory) {
2
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@planningcenter/tapestry'), require('react'), require('@planningcenter/url'), require('@planningcenter/icons/paths/general'), require('@planningcenter/jolt-client'), require('use-sync-external-store/shim'), require('react/jsx-runtime'), require('@planningcenter/tapestry-react'), require('@planningcenter/add-ons'), require('@planningcenter/icons/paths/church-center'), require('@planningcenter/icons/paths/people'), require('@planningcenter/icons/paths/toolbar'), require('react-calendly'), require('algoliasearch'), require('react-instantsearch-core'), require('react-transition-group'), require('@planningcenter/datetime-fmt')) :
3
- typeof define === 'function' && define.amd ? define(['exports', '@planningcenter/tapestry', 'react', '@planningcenter/url', '@planningcenter/icons/paths/general', '@planningcenter/jolt-client', 'use-sync-external-store/shim', 'react/jsx-runtime', '@planningcenter/tapestry-react', '@planningcenter/add-ons', '@planningcenter/icons/paths/church-center', '@planningcenter/icons/paths/people', '@planningcenter/icons/paths/toolbar', 'react-calendly', 'algoliasearch', 'react-instantsearch-core', 'react-transition-group', '@planningcenter/datetime-fmt'], factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory((global.PLANNINGCENTER = global.PLANNINGCENTER || {}, global.PLANNINGCENTER.Topbar = {}), global.tapestry, global.react, global.pcoUrl, global.general, global.Jolt, global.shim, null, global.tapestryReact, global.addOns, global.churchCenter, global.people, global.toolbar, global.reactCalendly, global.algoliasearch, global.reactInstantsearchCore, global.reactTransitionGroup, global.datetimeFmt));
5
- })(this, (function (exports, tapestry, React, pcoUrl$1, general, Jolt, shim, jsxRuntime, tapestryReact, addOns, churchCenter, people, toolbar, reactCalendly, algoliasearch, reactInstantsearchCore, reactTransitionGroup, datetimeFmt) { 'use strict';
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@planningcenter/tapestry'), require('react'), require('@planningcenter/url'), require('@planningcenter/icons/paths/general'), require('@planningcenter/jolt-client'), require('use-sync-external-store/shim'), require('react/jsx-runtime'), require('@planningcenter/icons/paths/toolbar'), require('@planningcenter/icons/paths/logomark'), require('@planningcenter/tapestry-react'), require('@planningcenter/add-ons'), require('@planningcenter/icons/paths/church-center'), require('@planningcenter/icons/paths/people'), require('react-calendly'), require('algoliasearch'), require('react-instantsearch-core'), require('react-transition-group'), require('@planningcenter/datetime-fmt')) :
3
+ typeof define === 'function' && define.amd ? define(['exports', '@planningcenter/tapestry', 'react', '@planningcenter/url', '@planningcenter/icons/paths/general', '@planningcenter/jolt-client', 'use-sync-external-store/shim', 'react/jsx-runtime', '@planningcenter/icons/paths/toolbar', '@planningcenter/icons/paths/logomark', '@planningcenter/tapestry-react', '@planningcenter/add-ons', '@planningcenter/icons/paths/church-center', '@planningcenter/icons/paths/people', 'react-calendly', 'algoliasearch', 'react-instantsearch-core', 'react-transition-group', '@planningcenter/datetime-fmt'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory((global.PLANNINGCENTER = global.PLANNINGCENTER || {}, global.PLANNINGCENTER.Topbar = {}), global.tapestry, global.react, global.pcoUrl, global.general, global.Jolt, global.shim, null, global.toolbar, global.logomark, global.tapestryReact, global.addOns, global.churchCenter, global.people, global.reactCalendly, global.algoliasearch, global.reactInstantsearchCore, global.reactTransitionGroup, global.datetimeFmt));
5
+ })(this, (function (exports, tapestry, React, pcoUrl$1, general, Jolt, shim, jsxRuntime, toolbar, logomarks, tapestryReact, addOns, churchCenter, people, reactCalendly, algoliasearch, reactInstantsearchCore, reactTransitionGroup, datetimeFmt) { 'use strict';
6
6
 
7
7
  function _interopNamespaceDefault(e) {
8
8
  var n = Object.create(null);
@@ -23,11 +23,12 @@
23
23
 
24
24
  var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
25
25
  var general__namespace = /*#__PURE__*/_interopNamespaceDefault(general);
26
+ var toolbar__namespace = /*#__PURE__*/_interopNamespaceDefault(toolbar);
27
+ var logomarks__namespace = /*#__PURE__*/_interopNamespaceDefault(logomarks);
26
28
  var churchCenter__namespace = /*#__PURE__*/_interopNamespaceDefault(churchCenter);
27
29
  var people__namespace = /*#__PURE__*/_interopNamespaceDefault(people);
28
- var toolbar__namespace = /*#__PURE__*/_interopNamespaceDefault(toolbar);
29
30
 
30
- var styles$l = {"accessibility-toolbar":"accessibility-toolbar-jgjUG"};
31
+ var styles$s = {"accessibility-toolbar":"accessibility-toolbar-jgjUG"};
31
32
 
32
33
  const AccessibilityToolbar = ({
33
34
  children
@@ -39,7 +40,7 @@
39
40
  (_a = document.getElementById("main-content")) === null || _a === void 0 ? void 0 : _a.focus();
40
41
  };
41
42
  return /*#__PURE__*/React.createElement("div", {
42
- className: styles$l["accessibility-toolbar"]
43
+ className: styles$s["accessibility-toolbar"]
43
44
  }, /*#__PURE__*/React.createElement(tapestry.Link, {
44
45
  href: "#main-content",
45
46
  kind: "primary",
@@ -166,12 +167,14 @@
166
167
  }
167
168
  function apiRequest(fetch, url, {
168
169
  method = "GET",
169
- data = null
170
+ data = null,
171
+ headers = {}
170
172
  } = {}) {
171
173
  const options = {
172
174
  headers: {
173
175
  Accept: "application/json",
174
- "Content-Type": "application/json"
176
+ "Content-Type": "application/json",
177
+ ...headers
175
178
  },
176
179
  method,
177
180
  redirect: "follow"
@@ -356,11 +359,11 @@
356
359
  fill: fill
357
360
  })));
358
361
 
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"};
362
+ var styles$r = {"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
363
 
361
364
  function AnnouncementIcon() {
362
365
  return /*#__PURE__*/React.createElement("svg", {
363
- className: styles$k["announcement-icon"],
366
+ className: styles$r["announcement-icon"],
364
367
  width: "16",
365
368
  height: "16",
366
369
  viewBox: "0 0 16 16",
@@ -485,7 +488,7 @@
485
488
  } = this.props;
486
489
  const isPromo = variant === "promo";
487
490
  return /*#__PURE__*/React.createElement("div", {
488
- className: classnames(styles$k["announcement"], styles$k[`announcement--${variant !== null && variant !== void 0 ? variant : "platform"}`]),
491
+ className: classnames(styles$r["announcement"], styles$r[`announcement--${variant !== null && variant !== void 0 ? variant : "platform"}`]),
489
492
  "data-announcement-variant": variant !== null && variant !== void 0 ? variant : "platform",
490
493
  style: isPromo ? {
491
494
  backgroundImage: promoBackgroundUrl,
@@ -494,7 +497,7 @@
494
497
  backgroundSize: "cover"
495
498
  } : undefined
496
499
  }, !isPromo && /*#__PURE__*/React.createElement(AnnouncementIcon, null), /*#__PURE__*/React.createElement("div", {
497
- className: styles$k["announcement-content"],
500
+ className: styles$r["announcement-content"],
498
501
  style: style,
499
502
  ...platformProps
500
503
  }));
@@ -543,7 +546,7 @@
543
546
  const isPromo = variant === "promo";
544
547
  return /*#__PURE__*/React.createElement("button", {
545
548
  type: "button",
546
- className: classnames(styles$k.dismiss, isPromo && styles$k["dismiss--promo"]),
549
+ className: classnames(styles$r.dismiss, isPromo && styles$r["dismiss--promo"]),
547
550
  ...props
548
551
  }, /*#__PURE__*/React.createElement(X, {
549
552
  fill: isPromo ? "currentColor" : "white",
@@ -623,9 +626,9 @@
623
626
  ...platformProps
624
627
  } = this.props;
625
628
  return /*#__PURE__*/React.createElement("div", {
626
- className: styles$k["announcement"]
629
+ className: styles$r["announcement"]
627
630
  }, /*#__PURE__*/React.createElement(AnnouncementIcon, null), /*#__PURE__*/React.createElement("div", {
628
- className: styles$k["announcement-content"],
631
+ className: styles$r["announcement-content"],
629
632
  style: style,
630
633
  ...platformProps
631
634
  }));
@@ -636,7 +639,7 @@
636
639
  }) {
637
640
  return /*#__PURE__*/React.createElement("button", {
638
641
  type: "button",
639
- className: styles$k.dismiss,
642
+ className: styles$r.dismiss,
640
643
  ...props
641
644
  }, /*#__PURE__*/React.createElement(X, {
642
645
  fill: "white",
@@ -791,7 +794,7 @@
791
794
  return matches;
792
795
  }
793
796
 
794
- const capitalize = s => s.charAt(0).toUpperCase() + s.slice(1);
797
+ const capitalize$1 = s => s.charAt(0).toUpperCase() + s.slice(1);
795
798
  const RESOLVED_COLOR_MODE_ATTRIBUTE = "data-resolved-color-mode";
796
799
  function resolveColorMode(colorMode) {
797
800
  if (colorMode === "system") {
@@ -1758,7 +1761,7 @@
1758
1761
  })) : (/*#__PURE__*/React.createElement("div", null, "appName not supported")));
1759
1762
  }
1760
1763
 
1761
- var styles$j = {"icon-slot":"icon-slot-m3ZIk","name":"name-UNqbW"};
1764
+ var styles$q = {"icon-slot":"icon-slot-m3ZIk","name":"name-UNqbW"};
1762
1765
 
1763
1766
  const AppRow = ({
1764
1767
  appName,
@@ -1767,24 +1770,24 @@
1767
1770
  }) => {
1768
1771
  if (appName === "Accounts") {
1769
1772
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
1770
- className: styles$j["icon-slot"]
1773
+ className: styles$q["icon-slot"]
1771
1774
  }, /*#__PURE__*/React.createElement(MonoAppIcon, {
1772
1775
  appName: "Accounts",
1773
1776
  colors: colors,
1774
1777
  size: iconSize
1775
1778
  })), /*#__PURE__*/React.createElement("span", {
1776
- className: styles$j.name
1779
+ className: styles$q.name
1777
1780
  }, "Account settings"));
1778
1781
  }
1779
1782
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(ColorAppIcon, {
1780
1783
  appName: appName.replace(/[\s-]/, ""),
1781
1784
  size: iconSize
1782
1785
  }), /*#__PURE__*/React.createElement("span", {
1783
- className: styles$j.name
1786
+ className: styles$q.name
1784
1787
  }, appName));
1785
1788
  };
1786
1789
 
1787
- var styles$i = {"button":"button-XMbeQ","label":"label-884of","chevron":"chevron-RRg9k"};
1790
+ var styles$p = {"button":"button-XMbeQ","label":"label-884of","chevron":"chevron-RRg9k"};
1788
1791
 
1789
1792
  const AppsButton = /*#__PURE__*/React.forwardRef(({
1790
1793
  appName,
@@ -1796,10 +1799,10 @@
1796
1799
  ...restProps,
1797
1800
  ref: ref,
1798
1801
  "aria-label": `App switcher: ${appName}`,
1799
- className: styles$i.button,
1802
+ className: styles$p.button,
1800
1803
  kind: "neutral-inline",
1801
1804
  label: /*#__PURE__*/React.createElement("span", {
1802
- className: styles$i.label
1805
+ className: styles$p.label
1803
1806
  }, isAccounts ? "Account settings" : appName),
1804
1807
  prefix: isAccounts ? (/*#__PURE__*/React.createElement(MonoAppIcon, {
1805
1808
  appName: "Accounts",
@@ -1810,13 +1813,13 @@
1810
1813
  size: 22
1811
1814
  })),
1812
1815
  suffix: /*#__PURE__*/React.createElement(DisclosureChevronIcon, {
1813
- className: `${styles$i.chevron} symbol`
1816
+ className: `${styles$p.chevron} symbol`
1814
1817
  })
1815
1818
  });
1816
1819
  });
1817
1820
  AppsButton.displayName = "AppsButton";
1818
1821
 
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"};
1822
+ var styles$o = {"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
1823
 
1821
1824
  const AppsMenu = ({
1822
1825
  appName,
@@ -1837,8 +1840,8 @@
1837
1840
  }, /*#__PURE__*/React.createElement("a", {
1838
1841
  "aria-current": isCurrent ? "page" : undefined,
1839
1842
  "aria-label": name,
1840
- className: classnames(styles$h.link, {
1841
- [styles$h.current]: isCurrent
1843
+ className: classnames(styles$o.link, {
1844
+ [styles$o.current]: isCurrent
1842
1845
  }),
1843
1846
  "data-pendo": `app-switcher--change-app--${name}`,
1844
1847
  "data-turbolinks": false,
@@ -1854,22 +1857,22 @@
1854
1857
  colors: colors
1855
1858
  })), /*#__PURE__*/React.createElement(tapestry.PopoverContent, {
1856
1859
  "aria-label": "Switch app",
1857
- className: styles$h["popover-content"],
1860
+ className: styles$o["popover-content"],
1858
1861
  placement: "bottom start"
1859
1862
  }, /*#__PURE__*/React.createElement("nav", {
1860
- className: styles$h.nav,
1863
+ className: styles$o.nav,
1861
1864
  "data-pendo": "app-switcher"
1862
1865
  }, /*#__PURE__*/React.createElement("ul", {
1863
- className: styles$h.list,
1866
+ className: styles$o.list,
1864
1867
  role: "list"
1865
1868
  }, homeProduct && renderProductLink(homeProduct.name), restProducts.map(({
1866
1869
  name
1867
1870
  }) => renderProductLink(name)), accountsProduct && (/*#__PURE__*/React.createElement("li", {
1868
- className: styles$h["accounts-item"]
1871
+ className: styles$o["accounts-item"]
1869
1872
  }, /*#__PURE__*/React.createElement("a", {
1870
1873
  "aria-current": appName === accountsProduct.name ? "page" : undefined,
1871
1874
  "aria-label": "Account settings",
1872
- className: classnames(styles$h.link, styles$h["accounts-link"]),
1875
+ className: classnames(styles$o.link, styles$o["accounts-link"]),
1873
1876
  "data-pendo": "app-switcher--change-app--Accounts",
1874
1877
  "data-turbolinks": false,
1875
1878
  href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(accountsProduct.name)
@@ -2077,7 +2080,7 @@
2077
2080
  })) : (/*#__PURE__*/React.createElement("div", null, "appName not supported"));
2078
2081
  }
2079
2082
 
2080
- var styles$g = {"unbutton":"unbutton-lEbTR"};
2083
+ var styles$n = {"unbutton":"unbutton-lEbTR"};
2081
2084
 
2082
2085
  function Unbutton({
2083
2086
  className,
@@ -2086,7 +2089,7 @@
2086
2089
  ...nativeProps
2087
2090
  }) {
2088
2091
  return /*#__PURE__*/React.createElement("button", {
2089
- className: classnames(styles$g.unbutton, className),
2092
+ className: classnames(styles$n.unbutton, className),
2090
2093
  style: {
2091
2094
  ...style
2092
2095
  },
@@ -2095,7 +2098,7 @@
2095
2098
  });
2096
2099
  }
2097
2100
 
2098
- var styles$f = {"button":"button-wsipc","button-accounts":"button-accounts-zQmBM","chevron":"chevron-obyBF","is-expanded":"is-expanded-PoCQ9"};
2101
+ var styles$m = {"button":"button-wsipc","button-accounts":"button-accounts-zQmBM","chevron":"chevron-obyBF","is-expanded":"is-expanded-PoCQ9"};
2099
2102
 
2100
2103
  const LegacyAppsButton = ({
2101
2104
  appName,
@@ -2110,8 +2113,8 @@
2110
2113
  "aria-haspopup": "true",
2111
2114
  "aria-label": `App switcher: ${appName}`,
2112
2115
  onClick: onClick,
2113
- className: classnames(styles$f.button, {
2114
- [styles$f["is-expanded"]]: expanded
2116
+ className: classnames(styles$m.button, {
2117
+ [styles$m["is-expanded"]]: expanded
2115
2118
  }),
2116
2119
  type: "button",
2117
2120
  style: style,
@@ -2120,7 +2123,7 @@
2120
2123
  appName: "Accounts",
2121
2124
  colors: colors
2122
2125
  }), /*#__PURE__*/React.createElement("div", {
2123
- className: styles$f["button-accounts"]
2126
+ className: styles$m["button-accounts"]
2124
2127
  }, "Account settings"))) : (/*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(ColorAppIcon, {
2125
2128
  appName: appName.replace(/[\s-]/, "")
2126
2129
  }), /*#__PURE__*/React.createElement(MonoAppName, {
@@ -2128,7 +2131,7 @@
2128
2131
  key: "mono-app-name",
2129
2132
  size: 24
2130
2133
  }))), /*#__PURE__*/React.createElement(DisclosureChevronIcon, {
2131
- className: styles$f.chevron,
2134
+ className: styles$m.chevron,
2132
2135
  key: "disclosure-chevron-icon"
2133
2136
  }));
2134
2137
  };
@@ -2261,7 +2264,7 @@
2261
2264
  }
2262
2265
  }
2263
2266
 
2264
- var styles$e = {"popup-root":"popup-root-SeL5u","popup":"popup-nQXYW","is-visible":"is-visible-3DAUP"};
2267
+ var styles$l = {"popup-root":"popup-root-SeL5u","popup":"popup-nQXYW","is-visible":"is-visible-3DAUP"};
2265
2268
 
2266
2269
  const Popup = ({
2267
2270
  className,
@@ -2269,8 +2272,8 @@
2269
2272
  isVisible,
2270
2273
  ...nativeProps
2271
2274
  }) => (/*#__PURE__*/React.createElement(C, {
2272
- className: classnames(styles$e.popup, {
2273
- [styles$e["is-visible"]]: isVisible
2275
+ className: classnames(styles$l.popup, {
2276
+ [styles$l["is-visible"]]: isVisible
2274
2277
  }, className),
2275
2278
  ...nativeProps
2276
2279
  }));
@@ -2278,11 +2281,11 @@
2278
2281
  const PopupRoot = ({
2279
2282
  ...nativeProps
2280
2283
  }) => (/*#__PURE__*/React.createElement("div", {
2281
- className: styles$e["popup-root"],
2284
+ className: styles$l["popup-root"],
2282
2285
  ...nativeProps
2283
2286
  }));
2284
2287
 
2285
- var styles$d = {"popup":"popup-bVhSa"};
2288
+ var styles$k = {"popup":"popup-bVhSa"};
2286
2289
 
2287
2290
  const LegacyAppsMenu = ({
2288
2291
  appName,
@@ -2320,7 +2323,7 @@
2320
2323
  cleanup: !visible,
2321
2324
  component: Outsider,
2322
2325
  onOutsideClick: toggle,
2323
- className: styles$d.popup,
2326
+ className: styles$k.popup,
2324
2327
  isVisible: visible
2325
2328
  }, /*#__PURE__*/React.createElement("menu", {
2326
2329
  ref: menuRef,
@@ -3230,29 +3233,121 @@ true;
3230
3233
  });
3231
3234
  }, []);
3232
3235
 
3233
- const isMac = () => /Mac|iPhone|iPad|iPod/.test(navigator.platform);
3234
- const hasCommandKey = event => isMac() ? event.metaKey : event.ctrlKey;
3235
- const commandKeyLabel = () => isMac() ? "⌘" : "Ctrl+";
3236
-
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
- }
3236
+ const collections = {
3237
+ general: general__namespace,
3238
+ toolbar: toolbar__namespace
3239
+ };
3240
+ const paths = collections;
3241
+ function Icon({
3242
+ name,
3243
+ size = 16,
3244
+ style,
3245
+ theme,
3246
+ title,
3247
+ ...props
3248
+ }) {
3249
+ const [collection, symbol] = name.split(".");
3250
+ return /*#__PURE__*/React.createElement("svg", {
3251
+ "aria-hidden": title ? undefined : true,
3252
+ fill: "currentColor",
3253
+ height: size,
3254
+ role: title ? "img" : undefined,
3255
+ style: {
3256
+ color: theme && `var(${theme})`,
3257
+ flexShrink: 0,
3258
+ ...style
3259
+ },
3260
+ viewBox: "0 0 16 16",
3261
+ width: size,
3262
+ ...props
3263
+ }, title && /*#__PURE__*/React.createElement("title", null, title), /*#__PURE__*/React.createElement("path", {
3264
+ d: paths[collection][symbol]
3265
+ }));
3244
3266
  }
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.
3267
+
3268
+ const SEARCHABLE_PRODUCTS = [{
3269
+ appName: "Calendar",
3270
+ slug: "calendar"
3271
+ }, {
3272
+ appName: "Check-Ins",
3273
+ slug: "check-ins"
3274
+ }, {
3275
+ appName: "Giving",
3276
+ slug: "giving"
3277
+ }, {
3278
+ appName: "Groups",
3279
+ slug: "groups"
3280
+ }, {
3281
+ appName: "People",
3282
+ slug: "people"
3283
+ }, {
3284
+ appName: "Publishing",
3285
+ slug: "publishing"
3286
+ }, {
3287
+ appName: "Registrations",
3288
+ slug: "registrations"
3289
+ }, {
3290
+ appName: "Services",
3291
+ slug: "services"
3292
+ }];
3293
+ const KEYBOARD_SHORTCUTS = {
3294
+ copyLink: {
3295
+ key: "c",
3296
+ label: "Copy link",
3297
+ modifierKey: true
3298
+ },
3299
+ openInNewTab: {
3300
+ label: "Open in new tab"},
3301
+ openSearch: {
3302
+ key: "k",
3303
+ label: "Open search",
3304
+ modifierKey: true
3305
+ },
3306
+ select: {
3307
+ label: "Select",
3308
+ modifierKey: false,
3309
+ next: "ArrowDown",
3310
+ previous: "ArrowUp"
3250
3311
  }
3251
- }
3312
+ };
3313
+
3314
+ const isMac = () => /Mac|iPhone|iPad|iPod/.test(navigator.platform);
3315
+ const hasCommandKey = event => isMac() ? event.metaKey : event.ctrlKey;
3316
+ const matchesShortcut = (shortcut, event) => event.key.toLowerCase() === shortcut.key && !event.altKey && !event.shiftKey && hasCommandKey(event) === shortcut.modifierKey;
3317
+ const isSearchShortcut = event => matchesShortcut(KEYBOARD_SHORTCUTS.openSearch, event);
3318
+ // Products opened their own search with `/` before this one replaced it.
3319
+ const isLegacySearchShortcut = event => event.key === "/" && !event.altKey && !event.ctrlKey && !event.metaKey && !isTypingIn(event.target);
3320
+ const isCopyShortcut = event => matchesShortcut(KEYBOARD_SHORTCUTS.copyLink, event);
3321
+ const isTypingIn = target => target instanceof Element && target.closest('input, select, textarea, [contenteditable]:not([contenteditable="false"])') !== null;
3322
+ const isTypingKey = event => event.key.length === 1 && event.key !== " " && !event.altKey && !event.ctrlKey && !event.metaKey;
3323
+ const verticalStep = key => ({
3324
+ [KEYBOARD_SHORTCUTS.select.next]: 1,
3325
+ [KEYBOARD_SHORTCUTS.select.previous]: -1
3326
+ })[key];
3327
+ const horizontalStep = key => ({
3328
+ ArrowLeft: -1,
3329
+ ArrowRight: 1
3330
+ })[key];
3252
3331
 
3253
- var styles$c = {"visuallyHidden":"visuallyHidden-C28ju"};
3332
+ var styles$j = {"searchButton":"searchButton-fYa5-","mobile":"mobile-ksZyA"};
3254
3333
 
3255
- var styles$b = {"tab":"tab-lQLDx"};
3334
+ function SearchButton({
3335
+ mobile = false,
3336
+ onClick
3337
+ }) {
3338
+ return /*#__PURE__*/React.createElement("button", {
3339
+ "aria-keyshortcuts": mobile ? undefined : `${isMac() ? "Meta" : "Control"}+${KEYBOARD_SHORTCUTS.openSearch.key.toUpperCase()} /`,
3340
+ "aria-label": "Search",
3341
+ className: classnames(styles$j.searchButton, {
3342
+ [styles$j.mobile]: mobile
3343
+ }),
3344
+ onClick: onClick,
3345
+ type: "button"
3346
+ }, /*#__PURE__*/React.createElement(Icon, {
3347
+ name: "general.search",
3348
+ size: mobile ? 18 : 14
3349
+ }));
3350
+ }
3256
3351
 
3257
3352
  // useId isn't available on React 17, which our peer range still supports.
3258
3353
  let instanceCount$1 = 0;
@@ -3261,173 +3356,33 @@ true;
3261
3356
  return id;
3262
3357
  }
3263
3358
 
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
- });
3359
+ // The versions home-mobile pins, so web and mobile read the same response
3360
+ // shapes. Some endpoints only exist from a version on: Registrations' me,
3361
+ // product_search_results, and pcrl_lookups need 2025-05-01.
3362
+ const API_VERSIONS = {
3363
+ calendar: "2022-07-07",
3364
+ "check-ins": "2025-05-28",
3365
+ giving: "2019-10-18",
3366
+ groups: "2023-07-10",
3367
+ home: "2018-08-01",
3368
+ people: "2023-03-21",
3369
+ publishing: "2024-03-25",
3370
+ registrations: "2025-05-01",
3371
+ services: "2018-11-01"
3372
+ };
3373
+ // For a `/~api/{product}/v2/...` URL, the header that pins its product's
3374
+ // version.
3375
+ function apiVersionHeaders(url) {
3376
+ const [,, product] = new URL(url).pathname.split("/");
3377
+ const version = API_VERSIONS[product];
3378
+ if (version) {
3379
+ return {
3380
+ "X-PCO-API-Version": version
3381
+ };
3382
+ } else {
3383
+ return {};
3392
3384
  }
3393
3385
  }
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
3386
 
3432
3387
  const CACHE_TTL_MS = 60000;
3433
3388
  const MAX_ENTRIES$1 = 50;
@@ -3444,7 +3399,9 @@ true;
3444
3399
  }
3445
3400
  }
3446
3401
  function fetchIntoCache(url) {
3447
- const promise = apiRequest(defaultFetch, url).then(({
3402
+ const promise = apiRequest(defaultFetch, url, {
3403
+ headers: apiVersionHeaders(url)
3404
+ }).then(({
3448
3405
  json,
3449
3406
  ok
3450
3407
  }) => {
@@ -3500,44 +3457,52 @@ true;
3500
3457
  };
3501
3458
  }
3502
3459
  }
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"
3460
+ // One request per URL, for callers that fan out to a varying number of
3461
+ // endpoints. Unlike `useCachedRequest`, a URL that hasn't settled yet has no
3462
+ // earlier data to fall back on.
3463
+ function useCachedRequests(urls) {
3464
+ const [settled, setSettled] = React.useState({});
3465
+ // Keyed by value because each settle re-renders the caller, and a caller
3466
+ // that builds a fresh `urls` array on render would otherwise loop forever.
3467
+ const key = urls.join("\n");
3468
+ React.useEffect(() => {
3469
+ let current = true;
3470
+ const settle = result => current && setSettled(prev => ({
3471
+ ...prev,
3472
+ [result.url]: result
3473
+ }));
3474
+ for (const url of key.split("\n").filter(Boolean)) {
3475
+ cachedRequest(url).then(data => settle({
3476
+ data,
3477
+ url
3478
+ }), () => settle({
3479
+ failed: true,
3480
+ url
3481
+ }));
3482
+ }
3483
+ return () => {
3484
+ current = false;
3538
3485
  };
3539
- }
3486
+ }, [key]);
3487
+ return urls.map(url => {
3488
+ const result = settled[url];
3489
+ if (!result) {
3490
+ return {
3491
+ status: "loading"
3492
+ };
3493
+ } else if ("data" in result) {
3494
+ return {
3495
+ data: result.data,
3496
+ status: "done"
3497
+ };
3498
+ } else {
3499
+ return {
3500
+ status: "error"
3501
+ };
3502
+ }
3503
+ });
3540
3504
  }
3505
+
3541
3506
  function productSearchUrl(product, query) {
3542
3507
  const params = new URLSearchParams({
3543
3508
  per_page: "100",
@@ -3568,6 +3533,53 @@ true;
3568
3533
  };
3569
3534
  return [...groups.values()].sort((a, b) => rank(a.type) - rank(b.type));
3570
3535
  }
3536
+
3537
+ function useDebouncedValue(value, delay) {
3538
+ const [debounced, setDebounced] = React.useState(value);
3539
+ React.useEffect(() => {
3540
+ const timeout = setTimeout(() => setDebounced(value), delay);
3541
+ return () => clearTimeout(timeout);
3542
+ }, [value, delay]);
3543
+ return debounced;
3544
+ }
3545
+
3546
+ const SEARCH_DEBOUNCE_MS = 300;
3547
+ function useProductSearch(product, term) {
3548
+ const query = term.trim();
3549
+ const debouncedQuery = useDebouncedValue(query, SEARCH_DEBOUNCE_MS);
3550
+ const request = useCachedRequest(debouncedQuery ? productSearchUrl(product, debouncedQuery) : null);
3551
+ const response = "data" in request ? request.data : undefined;
3552
+ const settledProduct = useSettledProduct(product, request.status);
3553
+ // While a request loads, the previous response stays on screen, but only
3554
+ // if it was for this product: People results must never show under the
3555
+ // Services tab.
3556
+ const visibleResponse = request.status === "done" || settledProduct === product ? response : undefined;
3557
+ const groups = React.useMemo(() => visibleResponse && groupByType(visibleResponse), [visibleResponse]);
3558
+ if (!query) {
3559
+ return {
3560
+ status: "idle"
3561
+ };
3562
+ } else if (query !== debouncedQuery) {
3563
+ return {
3564
+ groups,
3565
+ status: "loading"
3566
+ };
3567
+ } else if (request.status === "done") {
3568
+ return {
3569
+ groups: groups !== null && groups !== void 0 ? groups : [],
3570
+ status: "done"
3571
+ };
3572
+ } else if (request.status === "error") {
3573
+ return {
3574
+ status: "error"
3575
+ };
3576
+ } else {
3577
+ return {
3578
+ groups,
3579
+ status: "loading"
3580
+ };
3581
+ }
3582
+ }
3571
3583
  // The product the most recent finished request was for.
3572
3584
  function useSettledProduct(product, status) {
3573
3585
  const [settledProduct, setSettledProduct] = React.useState();
@@ -3576,29 +3588,489 @@ true;
3576
3588
  }, [product, status]);
3577
3589
  return settledProduct;
3578
3590
  }
3579
- function useDebouncedValue(value, delay) {
3580
- const [debounced, setDebounced] = React.useState(value);
3581
- React.useEffect(() => {
3582
- const timeout = setTimeout(() => setDebounced(value), delay);
3583
- return () => clearTimeout(timeout);
3584
- }, [value, delay]);
3585
- return debounced;
3591
+
3592
+ function parseValue(element) {
3593
+ const datatype = element.getAttribute("datatype");
3594
+ switch (datatype) {
3595
+ case "boolean":
3596
+ return element.content === "true" || element.content === "1";
3597
+ default:
3598
+ return element.content;
3599
+ }
3600
+ }
3601
+ function getAllPcoMeta() {
3602
+ const elements = document.head.querySelectorAll(`meta[property^="pco:"]`);
3603
+ const meta = {};
3604
+ elements.forEach(e => {
3605
+ const property = e.getAttribute("property");
3606
+ meta[property.substring("pco:".length)] = parseValue(e);
3607
+ });
3608
+ return meta;
3609
+ }
3610
+ // usePcoMeta only reads each tag's content. Recently Visited also needs the
3611
+ // pco:recently_visitable tag's `metadata` attribute, which can change while
3612
+ // the content stays the same, so it is tracked in its own store.
3613
+ function getRecentlyVisitableMetadata() {
3614
+ var _a, _b;
3615
+ return (_b = (_a = document.head.querySelector(`meta[property="pco:recently_visitable"]`)) === null || _a === void 0 ? void 0 : _a.getAttribute("metadata")) !== null && _b !== void 0 ? _b : null;
3616
+ }
3617
+ const store = createStore(getAllPcoMeta());
3618
+ const recentlyVisitableMetadataStore = createStore({
3619
+ metadata: getRecentlyVisitableMetadata()
3620
+ });
3621
+ function sync() {
3622
+ const nextState = getAllPcoMeta();
3623
+ if (!shallowCompare(store.getState(), nextState)) store.setState(nextState);
3624
+ const metadata = getRecentlyVisitableMetadata();
3625
+ if (recentlyVisitableMetadataStore.getState().metadata !== metadata) {
3626
+ recentlyVisitableMetadataStore.setState({
3627
+ metadata
3628
+ });
3629
+ }
3630
+ }
3631
+ const observer = new MutationObserver(sync);
3632
+ let hasTriggeredObserve = false;
3633
+ function observe() {
3634
+ if (hasTriggeredObserve) return;
3635
+ hasTriggeredObserve = true;
3636
+ // The module-load snapshot above can predate meta tags that appear later
3637
+ // in <head> (e.g. pco:resource, yielded right before </head>) if this
3638
+ // module's own script tag isn't deferred and runs mid-parse. Those tags
3639
+ // never mutate afterward, so the observer alone would never catch them.
3640
+ // Re-read once now, since observe() is only called once a consumer is
3641
+ // actually mounting, which is always after the document has fully parsed.
3642
+ sync();
3643
+ observer.observe(document.head, {
3644
+ attributes: true,
3645
+ childList: true,
3646
+ subtree: true
3647
+ });
3648
+ }
3649
+ function usePcoMeta() {
3650
+ observe();
3651
+ return shim.useSyncExternalStore(store.subscribe, () => store.getState());
3652
+ }
3653
+ function useRecentlyVisitableMetadata() {
3654
+ observe();
3655
+ return shim.useSyncExternalStore(recentlyVisitableMetadataStore.subscribe, () => recentlyVisitableMetadataStore.getState().metadata);
3586
3656
  }
3587
3657
 
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
3658
+ // Entries match the ones home-mobile stores, so one list can sync across web
3659
+ // and mobile.
3660
+ const ENTRY_VERSION = 3;
3661
+ const MAX_ENTRIES = 50;
3662
+ const MAX_TEXT_LENGTH = 200;
3663
+ const PRODUCTS$1 = ["accounts", "calendar", "check-ins", "giving", "groups", "home", "login", "people", "publishing", "registrations", "services"];
3664
+ // Login has no pages worth returning to.
3665
+ const APPS = PRODUCTS$1.filter(product => product !== "login");
3666
+ function isId(value) {
3667
+ return value !== "" && Array.from(value).every(character => character >= "0" && character <= "9");
3668
+ }
3669
+ // A PCRL looks like `<product>/v2/<resource_type>/<id>`.
3670
+ function isPcrl(value) {
3671
+ const segments = value.split("/");
3672
+ return segments.length === 4 && PRODUCTS$1.some(product => product === segments[0]) && isId(segments[3]);
3673
+ }
3674
+ function isApp(value) {
3675
+ return APPS.some(app => app === value);
3676
+ }
3677
+ // Links are built by resolving the path against its app's origin, so a stored
3678
+ // path must not be able to resolve anywhere else (`//evil.com`, `/\evil.com`).
3679
+ function isPath(value) {
3680
+ const origin = "https://app.invalid";
3681
+ try {
3682
+ return typeof value === "string" && value.startsWith("/") && new URL(value, origin).origin === origin;
3683
+ } catch {
3684
+ return false;
3685
+ }
3686
+ }
3687
+ function isNonBlankString(value) {
3688
+ return typeof value === "string" && value.trim() !== "";
3689
+ }
3690
+ function isOptionalString(value) {
3691
+ return value === undefined || typeof value === "string";
3692
+ }
3693
+ function isRecord(value) {
3694
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3695
+ }
3696
+ function isPageMetadata(value) {
3697
+ return value === undefined || isRecord(value) && isOptionalString(value.details) && isOptionalString(value.type);
3698
+ }
3699
+ // The query string is part of the link but not of the page's identity, so
3700
+ // revisiting with other filters moves the existing entry instead of adding one.
3701
+ function entryKey(entry) {
3702
+ if (entry.type === "resource") {
3703
+ return `resource:${entry.pcrl}`;
3704
+ } else {
3705
+ return `page:${entry.app}:${entry.path.split("?")[0]}`;
3706
+ }
3707
+ }
3708
+ function isEntry(value) {
3709
+ if (!isRecord(value) || value.version !== ENTRY_VERSION || typeof value.visitedAt !== "string") {
3710
+ return false;
3711
+ } else if (value.type === "resource") {
3712
+ return typeof value.pcrl === "string" && isPcrl(value.pcrl);
3713
+ } else if (value.type === "page") {
3714
+ return isApp(value.app) && isPath(value.path) && isNonBlankString(value.title) && isPageMetadata(value.metadata);
3715
+ } else {
3716
+ return false;
3717
+ }
3718
+ }
3719
+ // Cuts on code points so a surrogate pair is never split.
3720
+ function clampText(value) {
3721
+ return Array.from(value).slice(0, MAX_TEXT_LENGTH).join("");
3722
+ }
3723
+ function clampMetadata({
3724
+ details,
3725
+ type
3726
+ }) {
3727
+ return {
3728
+ ...(details && {
3729
+ details: clampText(details)
3730
+ }),
3731
+ ...(type && {
3732
+ type: clampText(type)
3733
+ })
3734
+ };
3735
+ }
3736
+ function clampVisit(visit) {
3737
+ if (visit.type === "resource") {
3738
+ return visit;
3739
+ } else {
3740
+ return {
3741
+ ...visit,
3742
+ ...(visit.metadata && {
3743
+ metadata: clampMetadata(visit.metadata)
3744
+ }),
3745
+ title: clampText(visit.title.trim())
3746
+ };
3747
+ }
3748
+ }
3749
+ function toEntry(visit, visitedAt) {
3750
+ return {
3751
+ ...clampVisit(visit),
3752
+ version: ENTRY_VERSION,
3753
+ visitedAt: visitedAt.toISOString()
3754
+ };
3755
+ }
3756
+ // Recents are disposable: anything unexpected reads as an empty list (or, for
3757
+ // a single bad entry, a list without it) rather than throwing.
3758
+ function parseRecentlyVisited(value) {
3759
+ let parsed = null;
3760
+ try {
3761
+ parsed = JSON.parse(value !== null && value !== void 0 ? value : "null");
3762
+ } catch {
3763
+ parsed = null;
3764
+ }
3765
+ if (Array.isArray(parsed)) {
3766
+ return parsed.filter(isEntry);
3767
+ } else {
3768
+ return [];
3769
+ }
3770
+ }
3771
+ function addVisit(entries, entry) {
3772
+ const key = entryKey(entry);
3773
+ return [entry, ...entries.filter(existing => entryKey(existing) !== key)].slice(0, MAX_ENTRIES);
3774
+ }
3775
+
3776
+ // The app whose URL, in this environment, has the current page's host.
3777
+ // Reversing pcoUrl handles every environment @planningcenter/url knows about,
3778
+ // and guarantees the stored app rebuilds a working link.
3779
+ function currentApp() {
3780
+ var _a;
3781
+ return (_a = APPS.find(app => new URL(pcoUrl$1.pcoUrl(app)).hostname === window.location.hostname)) !== null && _a !== void 0 ? _a : null;
3782
+ }
3783
+ function isFilled(value) {
3784
+ return typeof value === "string" && value !== "";
3785
+ }
3786
+ // The tag's metadata is product-supplied JSON; only its string `type` and
3787
+ // `details` carry over, and an entry without either has no metadata at all.
3788
+ function metadataFromPage(metadata) {
3789
+ var _a;
3790
+ try {
3791
+ const {
3792
+ details,
3793
+ type
3794
+ } = (_a = JSON.parse(metadata !== null && metadata !== void 0 ? metadata : "{}")) !== null && _a !== void 0 ? _a : {};
3795
+ if (isFilled(details) || isFilled(type)) {
3796
+ return {
3797
+ ...(isFilled(details) && {
3798
+ details
3799
+ }),
3800
+ ...(isFilled(type) && {
3801
+ type
3802
+ })
3803
+ };
3804
+ } else {
3805
+ return undefined;
3806
+ }
3807
+ } catch {
3808
+ return undefined;
3809
+ }
3810
+ }
3811
+ // Staff impersonating someone browse under `/O<org id>/P<person id>/…`.
3812
+ function withoutImpersonation(segments) {
3813
+ const [org = "", person = ""] = segments;
3814
+ if (org.startsWith("O") && isId(org.slice(1)) && person.startsWith("P") && isId(person.slice(1))) {
3815
+ return segments.slice(2);
3816
+ } else {
3817
+ return segments;
3818
+ }
3819
+ }
3820
+ // Normalized the way home-mobile does it, so a page visited on web and on
3821
+ // mobile is one entry once the list syncs.
3822
+ function pathFromLocation({
3823
+ pathname,
3824
+ search
3825
+ }) {
3826
+ const segments = withoutImpersonation(pathname.split("/").filter(Boolean));
3827
+ if (segments.length === 0) {
3828
+ return "/";
3829
+ } else {
3830
+ return `/${segments.join("/")}${search.length > 1 ? search : ""}`;
3831
+ }
3832
+ }
3833
+ // pco:resource is the more specific signal, so it wins when both tags are
3834
+ // present. A resource that isn't a PCRL is ignored rather than recorded.
3835
+ function visitFromPage({
3836
+ metadata,
3837
+ resource,
3838
+ title
3839
+ }) {
3840
+ const app = currentApp();
3841
+ if (resource && isPcrl(resource)) {
3842
+ return {
3843
+ pcrl: resource,
3844
+ type: "resource"
3845
+ };
3846
+ } else if (title && title.trim() !== "" && app) {
3847
+ const pageMetadata = metadataFromPage(metadata);
3848
+ return {
3849
+ app,
3850
+ ...(pageMetadata && {
3851
+ metadata: pageMetadata
3852
+ }),
3853
+ path: pathFromLocation(window.location),
3854
+ title,
3855
+ type: "page"
3856
+ };
3857
+ } else {
3858
+ return null;
3859
+ }
3860
+ }
3861
+ // The entry Recently Visited would record for the current page, or null when
3862
+ // the page isn't eligible. Shared by the recorder and the debug panel so the
3863
+ // panel always shows what actually gets stored.
3864
+ function useCurrentVisit() {
3865
+ const {
3866
+ recently_visitable: title,
3867
+ resource
3868
+ } = usePcoMeta();
3869
+ const metadata = useRecentlyVisitableMetadata();
3870
+ return React.useMemo(() => visitFromPage({
3871
+ metadata,
3872
+ resource,
3873
+ title
3874
+ }), [metadata, resource, title]);
3875
+ }
3876
+ // Products hand the person's ID to AppsProvider, not to the topbar itself,
3877
+ // and several pass it as a number.
3878
+ function useCurrentPersonId() {
3879
+ const {
3880
+ userId
3881
+ } = useStore();
3882
+ return userId == null ? undefined : String(userId);
3883
+ }
3884
+
3885
+ // Per-origin localStorage is a stopgap: each product keeps its own list until
3886
+ // PCA-409 moves this to storage shared across product subdomains. Only this
3887
+ // file should need to change when it does.
3888
+ function storageKey({
3889
+ env,
3890
+ personId
3891
+ }) {
3892
+ return `topbar.recently_visited.${env}.${personId}`;
3893
+ }
3894
+ // localStorage doesn't notify the tab that wrote it, so readers in this tab
3895
+ // learn about new visits from this counter instead.
3896
+ const changes = createStore({
3897
+ version: 0
3898
+ });
3899
+ function readRecentlyVisited(scope) {
3900
+ try {
3901
+ return parseRecentlyVisited(localStorage.getItem(storageKey(scope)));
3902
+ } catch {
3903
+ return [];
3904
+ }
3905
+ }
3906
+ function recordVisit(visit, scope) {
3907
+ const entry = toEntry(visit, new Date());
3908
+ if (isEntry(entry)) {
3909
+ try {
3910
+ localStorage.setItem(storageKey(scope), JSON.stringify(addVisit(readRecentlyVisited(scope), entry)));
3911
+ changes.setState({
3912
+ version: changes.getState().version + 1
3913
+ });
3914
+ } catch {
3915
+ // Storage can be full or blocked; recents are disposable.
3916
+ }
3917
+ }
3918
+ }
3919
+ function useRecentlyVisited({
3920
+ env,
3921
+ personId
3922
+ }) {
3923
+ const version = shim.useSyncExternalStore(changes.subscribe, () => changes.getState().version);
3924
+ // `version` isn't read, but listing it re-reads storage after each visit.
3925
+ return React.useMemo(() => personId ? readRecentlyVisited({
3926
+ env,
3927
+ personId
3928
+ }) : [], [env, personId, version]);
3929
+ }
3930
+
3931
+ const pcoUrl = pcoUrl$1;
3932
+
3933
+ // Resolves resource entries with one pcrl_lookups request per product.
3934
+ // pcrl_lookups reports forbidden and deleted resources the same way, so both
3935
+ // become `unresolved`, as does every entry in a batch whose request failed.
3936
+ function useHydratedRecentlyVisited(entries, env) {
3937
+ const batches = React.useMemo(() => batchByProduct(entries), [entries]);
3938
+ const urls = React.useMemo(() => batches.map(([product, pcrls]) => pcrlLookupsUrl(product, pcrls)), [batches]);
3939
+ const requests = useCachedRequests(urls);
3940
+ const lookups = new Map();
3941
+ batches.forEach(([, pcrls], index) => {
3942
+ const request = requests[index];
3943
+ for (const pcrl of pcrls) lookups.set(pcrl, lookupFor(pcrl, request));
3599
3944
  });
3600
- const highlightedIndex = highlight.results === results ? highlight.index : 0;
3601
- const highlighted = results[highlightedIndex];
3945
+ return entries.map(entry => {
3946
+ var _a;
3947
+ if (entry.type === "page") {
3948
+ return {
3949
+ entry,
3950
+ status: "page",
3951
+ url: pageUrl(entry, env)
3952
+ };
3953
+ } else {
3954
+ const lookup = (_a = lookups.get(entry.pcrl)) !== null && _a !== void 0 ? _a : {
3955
+ status: "unresolved"
3956
+ };
3957
+ if (lookup.status === "resolved") {
3958
+ return {
3959
+ ...lookup,
3960
+ entry,
3961
+ url: lookup.result.attributes.url
3962
+ };
3963
+ } else {
3964
+ return {
3965
+ entry,
3966
+ status: lookup.status
3967
+ };
3968
+ }
3969
+ }
3970
+ });
3971
+ }
3972
+ function lookupFor(pcrl, request) {
3973
+ if (request.status === "done") {
3974
+ const result = request.data.data.find(({
3975
+ attributes
3976
+ }) => attributes.pcrl === pcrl);
3977
+ return result ? {
3978
+ result,
3979
+ status: "resolved"
3980
+ } : {
3981
+ status: "unresolved"
3982
+ };
3983
+ } else if (request.status === "error") {
3984
+ return {
3985
+ status: "unresolved"
3986
+ };
3987
+ } else {
3988
+ return {
3989
+ status: "loading"
3990
+ };
3991
+ }
3992
+ }
3993
+ const RECENTLY_VISITED_PAGE_SIZE = 10;
3994
+ // A PCRL can only be looked up through its own product's API, which may not be
3995
+ // the product it was visited in. Each page of entries is batched on its own,
3996
+ // so loading the next page leaves earlier pages' URLs, and so their cache
3997
+ // keys, alone. Sorting keeps them stable as a page reorders.
3998
+ function batchByProduct(entries) {
3999
+ var _a;
4000
+ const batches = [];
4001
+ for (let start = 0; start < entries.length; start += RECENTLY_VISITED_PAGE_SIZE) {
4002
+ const page = new Map();
4003
+ for (const entry of entries.slice(start, start + RECENTLY_VISITED_PAGE_SIZE)) {
4004
+ if (entry.type === "resource") {
4005
+ const [product] = entry.pcrl.split("/");
4006
+ page.set(product, [...((_a = page.get(product)) !== null && _a !== void 0 ? _a : []), entry.pcrl]);
4007
+ }
4008
+ }
4009
+ for (const [product, pcrls] of page) {
4010
+ batches.push([product, [...pcrls].sort()]);
4011
+ }
4012
+ }
4013
+ return batches;
4014
+ }
4015
+ function pcrlLookupsUrl(product, pcrls) {
4016
+ const params = new URLSearchParams({
4017
+ "where[pcrl]": pcrls.join(",")
4018
+ });
4019
+ return `${window.location.origin}/~api/${product}/v2/pcrl_lookups?${params}`;
4020
+ }
4021
+ // Built from the app rather than the current origin, so it stays right once
4022
+ // PCA-409 shares one list across product subdomains.
4023
+ function pageUrl(entry, env) {
4024
+ return new URL(entry.path, pcoUrl(env)(entry.app)).href;
4025
+ }
4026
+
4027
+ function useRecentVisits(env) {
4028
+ const personId = useCurrentPersonId();
4029
+ const entries = useRecentlyVisited({
4030
+ env,
4031
+ personId
4032
+ });
4033
+ const [count, setCount] = React.useState(RECENTLY_VISITED_PAGE_SIZE);
4034
+ const shown = React.useMemo(() => entries.slice(0, count), [entries, count]);
4035
+ const hydrated = useHydratedRecentlyVisited(shown, env);
4036
+ // Resources show up once they're found. Ones that can't be found anymore,
4037
+ // or that the person can no longer see, are left out.
4038
+ const visits = hydrated.filter(isShown);
4039
+ const loading = hydrated.some(({
4040
+ status
4041
+ }) => status === "loading");
4042
+ const showMore = React.useCallback(() => setCount(current => current + RECENTLY_VISITED_PAGE_SIZE), []);
4043
+ return {
4044
+ count,
4045
+ hasMore: entries.length > count,
4046
+ loading,
4047
+ showMore,
4048
+ visits
4049
+ };
4050
+ }
4051
+ function isShown(visit) {
4052
+ return visit.status === "page" || visit.status === "resolved";
4053
+ }
4054
+
4055
+ function open(url, {
4056
+ newTab
4057
+ }) {
4058
+ if (newTab) {
4059
+ window.open(url, "_blank", "noopener");
4060
+ } else {
4061
+ window.location.assign(url);
4062
+ }
4063
+ }
4064
+ // With text selected, ⌘C should copy that text as usual, not the link.
4065
+ function hasSelectedText(element) {
4066
+ var _a, _b;
4067
+ const selectedInField = element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
4068
+ return selectedInField || ((_b = (_a = window.getSelection()) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : "") !== "";
4069
+ }
4070
+
4071
+ const COPIED_FEEDBACK_MS = 1500;
4072
+ // `copiedUrl` stays set briefly after a copy so the UI can confirm it.
4073
+ function useCopyLink() {
3602
4074
  const [copiedUrl, setCopiedUrl] = React.useState(null);
3603
4075
  React.useEffect(() => {
3604
4076
  if (copiedUrl) {
@@ -3608,201 +4080,718 @@ true;
3608
4080
  return undefined;
3609
4081
  }
3610
4082
  }, [copiedUrl]);
3611
- const highlightIndex = React.useCallback(index => setHighlight(current => current.results === results && current.index === index ? current : {
3612
- index,
3613
- results
3614
- }), [results]);
4083
+ const copy = React.useCallback(url => {
4084
+ navigator.clipboard.writeText(url).then(() => setCopiedUrl(url));
4085
+ }, []);
4086
+ return {
4087
+ copiedUrl,
4088
+ copy
4089
+ };
4090
+ }
4091
+
4092
+ // Remembering which list the index belongs to puts the highlight back on the
4093
+ // first item whenever a new list replaces it, without an effect to reset it.
4094
+ // A list that grows or fills in under the same `list` keeps its highlight.
4095
+ // Disabled, nothing is highlighted and the keys and pointer leave it alone.
4096
+ function useResultNavigation(items, list, enabled = true) {
4097
+ const [highlight, setHighlight] = React.useState({
4098
+ index: 0,
4099
+ list
4100
+ });
4101
+ const highlightedIndex = highlight.list === list ? highlight.index : 0;
4102
+ const highlighted = enabled ? items[highlightedIndex] : undefined;
4103
+ const {
4104
+ copiedUrl,
4105
+ copy
4106
+ } = useCopyLink();
4107
+ const highlightIndex = React.useCallback(index => {
4108
+ if (!enabled) return;
4109
+ setHighlight(current => current.list === list && current.index === index ? current : {
4110
+ index,
4111
+ list
4112
+ });
4113
+ }, [enabled, list]);
3615
4114
  const handleKeyDown = event => {
4115
+ if (!enabled) return;
3616
4116
  const step = verticalStep(event.key);
3617
- if (step && results.length > 0) {
4117
+ if (step && items.length > 0) {
3618
4118
  event.preventDefault();
3619
- highlightIndex((highlightedIndex + step + results.length) % results.length);
4119
+ highlightIndex((highlightedIndex + step + items.length) % items.length);
3620
4120
  } else if (event.key === "Enter" && highlighted && !event.nativeEvent.isComposing) {
3621
4121
  event.preventDefault();
3622
- open(highlighted.attributes.url, {
4122
+ open(highlighted.url, {
3623
4123
  newTab: hasCommandKey(event)
3624
4124
  });
3625
4125
  } else if (isCopyShortcut(event) && highlighted && !hasSelectedText(event.currentTarget)) {
3626
4126
  event.preventDefault();
3627
- const {
3628
- url
3629
- } = highlighted.attributes;
3630
- navigator.clipboard.writeText(url).then(() => setCopiedUrl(url));
4127
+ copy(highlighted.url);
3631
4128
  }
3632
4129
  };
3633
4130
  return {
3634
- copied: copiedUrl !== null && copiedUrl === (highlighted === null || highlighted === void 0 ? void 0 : highlighted.attributes.url),
4131
+ copied: copiedUrl !== null && copiedUrl === (highlighted === null || highlighted === void 0 ? void 0 : highlighted.url),
3635
4132
  handleKeyDown,
3636
4133
  highlightIndex,
3637
4134
  highlighted,
3638
4135
  highlightedIndex: highlighted ? highlightedIndex : null
3639
4136
  };
3640
4137
  }
3641
- function open(url, {
3642
- newTab
4138
+
4139
+ var styles$i = {"modal":"modal-NR3TO","mobile":"mobile-4e5rC","visuallyHidden":"visuallyHidden-C28ju","panel":"panel-HlF7S"};
4140
+
4141
+ function groupSections(groups = []) {
4142
+ return groups.map(({
4143
+ results,
4144
+ type
4145
+ }) => ({
4146
+ heading: type,
4147
+ items: results.map(resultItem),
4148
+ key: type
4149
+ }));
4150
+ }
4151
+ function resultItem({
4152
+ attributes,
4153
+ id
3643
4154
  }) {
3644
- if (newTab) {
3645
- window.open(url, "_blank", "noopener");
4155
+ return {
4156
+ category: attributes.category,
4157
+ imageUrl: attributes.image_url,
4158
+ key: id,
4159
+ product: attributes.product,
4160
+ subtitle: summarize(attributes.metadata),
4161
+ title: attributes.title,
4162
+ url: attributes.url
4163
+ };
4164
+ }
4165
+ function visitItem(visit) {
4166
+ return {
4167
+ key: entryKey(visit.entry),
4168
+ ...describeVisit(visit)
4169
+ };
4170
+ }
4171
+ function summarize(metadata) {
4172
+ return Object.values(metadata !== null && metadata !== void 0 ? metadata : {}).filter(value => typeof value === "string" && value !== "").join(" · ");
4173
+ }
4174
+ // Unlike search results, recents come from every product, so each one names
4175
+ // its product and type.
4176
+ function describeVisit(visit) {
4177
+ var _a, _b;
4178
+ const subtitle = (...parts) => parts.filter(Boolean).join(" · ");
4179
+ if (visit.status === "page") {
4180
+ const {
4181
+ app,
4182
+ metadata,
4183
+ title
4184
+ } = visit.entry;
4185
+ return {
4186
+ category: "generic",
4187
+ imageUrl: null,
4188
+ product: app,
4189
+ 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 : ""),
4190
+ title,
4191
+ url: visit.url
4192
+ };
3646
4193
  } else {
3647
- window.location.assign(url);
4194
+ const {
4195
+ category,
4196
+ image_url,
4197
+ metadata,
4198
+ product,
4199
+ title,
4200
+ type
4201
+ } = visit.result.attributes;
4202
+ return {
4203
+ category,
4204
+ imageUrl: image_url,
4205
+ product,
4206
+ subtitle: subtitle(productName(product), type, summarize(metadata)),
4207
+ title,
4208
+ url: visit.url
4209
+ };
3648
4210
  }
3649
4211
  }
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 : "") !== "";
4212
+ // check-ins → Check-Ins
4213
+ function productName(slug) {
4214
+ return slug.split("-").map(capitalize).join("-");
4215
+ }
4216
+ function capitalize(word) {
4217
+ return word.charAt(0).toUpperCase() + word.slice(1);
3656
4218
  }
3657
4219
 
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)
4220
+ const STORAGE_KEY = "topbar.product_search.last_product";
4221
+ function readLastProduct() {
4222
+ try {
4223
+ return localStorage.getItem(STORAGE_KEY);
4224
+ } catch {
4225
+ return null;
4226
+ }
4227
+ }
4228
+ function saveLastProduct(slug) {
4229
+ try {
4230
+ localStorage.setItem(STORAGE_KEY, slug);
4231
+ } catch {
4232
+ // Storage can be full or blocked; search still works without it.
4233
+ }
4234
+ }
4235
+ function initialProduct(products) {
4236
+ var _a, _b, _c;
4237
+ const lastProduct = readLastProduct();
4238
+ const bySlug = slug => products.find(product => product.slug === slug);
4239
+ 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];
4240
+ }
4241
+
4242
+ var styles$h = {"container":"container-YDdM1","hint":"hint-017Rd","keycap":"keycap-ENJM2","copied":"copied-DUFa1"};
4243
+
4244
+ const SYMBOLS = {
4245
+ command: {
4246
+ label: "⌘",
4247
+ paths: ["M7 9a2 2 0 1 1 2 -2v10a2 2 0 1 1 -2 -2h10a2 2 0 1 1 -2 2v-10a2 2 0 1 1 2 2h-10"]
4248
+ },
4249
+ down: {
4250
+ label: "↓",
4251
+ paths: ["M12 6l0 12", "M16 14l-4 4", "M8 14l4 4"]
4252
+ },
4253
+ return: {
4254
+ label: "↵",
4255
+ paths: ["M18 6v6a3 3 0 0 1 -3 3h-10l4 -4m0 8l-4 -4"]
4256
+ },
4257
+ up: {
4258
+ label: "↑",
4259
+ paths: ["M12 6l0 12", "M16 10l-4 -4", "M8 10l4 -4"]
4260
+ }
3676
4261
  };
3677
- function fetchMe(product, fields) {
3678
- const params = new URLSearchParams({
3679
- "fields[Person]": fields
4262
+ function Keycap(props) {
4263
+ return /*#__PURE__*/React.createElement("kbd", {
4264
+ className: styles$h.keycap
4265
+ }, "symbol" in props ? (/*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("svg", {
4266
+ "aria-hidden": true,
4267
+ fill: "none",
4268
+ stroke: "currentColor",
4269
+ strokeLinecap: "round",
4270
+ strokeLinejoin: "round",
4271
+ viewBox: "0 0 24 24"
4272
+ }, SYMBOLS[props.symbol].paths.map(d => (/*#__PURE__*/React.createElement("path", {
4273
+ d: d,
4274
+ key: d
4275
+ })))), /*#__PURE__*/React.createElement("span", {
4276
+ className: styles$i.visuallyHidden
4277
+ }, SYMBOLS[props.symbol].label))) : props.label);
4278
+ }
4279
+ function CommandKey() {
4280
+ return isMac() ? /*#__PURE__*/React.createElement(Keycap, {
4281
+ symbol: "command"
4282
+ }) : /*#__PURE__*/React.createElement(Keycap, {
4283
+ label: "Ctrl"
3680
4284
  });
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;
4285
+ }
4286
+ function EnterKey() {
4287
+ return isMac() ? /*#__PURE__*/React.createElement(Keycap, {
4288
+ symbol: "return"
4289
+ }) : /*#__PURE__*/React.createElement(Keycap, {
4290
+ label: "Enter"
3687
4291
  });
3688
4292
  }
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;
4293
+ const {
4294
+ copyLink,
4295
+ openInNewTab,
4296
+ openSearch,
4297
+ select
4298
+ } = KEYBOARD_SHORTCUTS;
4299
+ function KeyboardHints({
4300
+ copied
4301
+ }) {
4302
+ return /*#__PURE__*/React.createElement("ul", {
4303
+ "aria-label": "Keyboard shortcuts",
4304
+ className: styles$h.container
4305
+ }, /*#__PURE__*/React.createElement("li", {
4306
+ className: styles$h.hint
4307
+ }, /*#__PURE__*/React.createElement(tapestry.Flex, {
4308
+ gap: 0.5
4309
+ }, select.modifierKey, /*#__PURE__*/React.createElement(Keycap, {
4310
+ symbol: "up"
4311
+ }), /*#__PURE__*/React.createElement(Keycap, {
4312
+ symbol: "down"
4313
+ })), select.label), /*#__PURE__*/React.createElement("li", {
4314
+ className: styles$h.hint
4315
+ }, /*#__PURE__*/React.createElement(tapestry.Flex, {
4316
+ gap: 0.5
4317
+ }, /*#__PURE__*/React.createElement(CommandKey, null), /*#__PURE__*/React.createElement(EnterKey, null)), openInNewTab.label), /*#__PURE__*/React.createElement("li", {
4318
+ className: classnames(styles$h.hint, copied && styles$h.copied)
4319
+ }, /*#__PURE__*/React.createElement(tapestry.Flex, {
4320
+ gap: 0.5
4321
+ }, /*#__PURE__*/React.createElement(CommandKey, null), /*#__PURE__*/React.createElement(Keycap, {
4322
+ label: copyLink.key.toUpperCase()
4323
+ })), /*#__PURE__*/React.createElement("span", {
4324
+ role: "status"
4325
+ }, copied ? "Copied!" : copyLink.label)), /*#__PURE__*/React.createElement("li", {
4326
+ className: styles$h.hint
4327
+ }, /*#__PURE__*/React.createElement(tapestry.Flex, {
4328
+ gap: 0.5
4329
+ }, /*#__PURE__*/React.createElement(CommandKey, null), /*#__PURE__*/React.createElement(Keycap, {
4330
+ label: openSearch.key.toUpperCase()
4331
+ })), openSearch.label));
4332
+ }
4333
+
4334
+ var styles$g = {"icon":"icon-XKLvN","squircle":"squircle-JCHr6"};
4335
+
4336
+ const PRODUCTS = {
4337
+ accounts: {
4338
+ gradient: "--t-fill-color-product-accounts-gradient-brand",
4339
+ path: logomarks__namespace.accounts
4340
+ },
4341
+ api: {
4342
+ gradient: "--t-fill-color-product-api-gradient-brand",
4343
+ path: logomarks__namespace.api
4344
+ },
4345
+ calendar: {
4346
+ gradient: "--t-fill-color-product-calendar-gradient-brand",
4347
+ path: logomarks__namespace.calendar
4348
+ },
4349
+ "check-ins": {
4350
+ gradient: "--t-fill-color-product-checkins-gradient-brand",
4351
+ path: logomarks__namespace.checkIns
4352
+ },
4353
+ "church-center": {
4354
+ gradient: "--t-fill-color-product-cc-gradient-brand",
4355
+ path: logomarks__namespace.churchCenter
4356
+ },
4357
+ giving: {
4358
+ gradient: "--t-fill-color-product-giving-gradient-brand",
4359
+ path: logomarks__namespace.giving
4360
+ },
4361
+ groups: {
4362
+ gradient: "--t-fill-color-product-groups-gradient-brand",
4363
+ path: logomarks__namespace.groups
4364
+ },
4365
+ home: {
4366
+ gradient: "--t-fill-color-product-home-gradient-brand",
4367
+ path: logomarks__namespace.home
4368
+ },
4369
+ "music-stand": {
4370
+ gradient: "--t-fill-color-product-musicstand-gradient-brand",
4371
+ path: logomarks__namespace.musicStand
4372
+ },
4373
+ people: {
4374
+ gradient: "--t-fill-color-product-people-gradient-brand",
4375
+ path: logomarks__namespace.people
4376
+ },
4377
+ publishing: {
4378
+ gradient: "--t-fill-color-product-publishing-gradient-brand",
4379
+ path: logomarks__namespace.publishing
4380
+ },
4381
+ registrations: {
4382
+ gradient: "--t-fill-color-product-registrations-gradient-brand",
4383
+ path: logomarks__namespace.registrations
4384
+ },
4385
+ services: {
4386
+ gradient: "--t-fill-color-product-services-gradient-brand",
4387
+ path: logomarks__namespace.services
3706
4388
  }
4389
+ };
4390
+ const isProductIconSlug = slug => Object.prototype.hasOwnProperty.call(PRODUCTS, slug);
4391
+ const maskFor = path => `url("data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="${path}"/></svg>`)}")`;
4392
+ function ProductIcon({
4393
+ size = 16,
4394
+ slug
4395
+ }) {
4396
+ const {
4397
+ gradient,
4398
+ path
4399
+ } = PRODUCTS[slug];
4400
+ const mask = maskFor(path);
4401
+ return /*#__PURE__*/React.createElement("span", {
4402
+ "aria-hidden": true,
4403
+ className: styles$g.icon,
4404
+ style: {
4405
+ height: size,
4406
+ width: size
4407
+ }
4408
+ }, /*#__PURE__*/React.createElement("span", {
4409
+ className: styles$g.squircle,
4410
+ style: {
4411
+ WebkitMaskImage: mask,
4412
+ background: `var(${gradient})`,
4413
+ maskImage: mask
4414
+ }
4415
+ }));
3707
4416
  }
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(({
4417
+
4418
+ var styles$f = {"picker":"picker-iuzzj","option":"option--7LXg"};
4419
+
4420
+ // Phones have no room for a tab per product, so they pick from a dropdown.
4421
+ // Complex mode renders each option's logomark, in the list and the trigger.
4422
+ function ProductSelect({
4423
+ children,
4424
+ onSelect,
4425
+ products,
4426
+ selected
4427
+ }) {
4428
+ const options = products.map(({
4429
+ appName,
3712
4430
  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
- });
4431
+ }) => ({
4432
+ label: (/*#__PURE__*/React.createElement("span", {
4433
+ className: styles$f.option
4434
+ }, /*#__PURE__*/React.createElement(ProductIcon, {
4435
+ slug: slug
4436
+ }), appName)),
4437
+ textValue: appName,
4438
+ value: slug
4439
+ }));
4440
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
4441
+ className: styles$f.picker
4442
+ }, /*#__PURE__*/React.createElement(tapestry.Select, {
4443
+ "aria-label": "Product",
4444
+ complex: true,
4445
+ onChange: event => onSelect(event.target.value),
4446
+ options: options,
4447
+ value: selected.slug
4448
+ })), /*#__PURE__*/React.createElement("div", {
4449
+ className: styles$i.panel
4450
+ }, children));
4451
+ }
4452
+
4453
+ var styles$e = {"tablist":"tablist-7ASvc","tab":"tab-dz8ki"};
4454
+
4455
+ function ProductTabs({
4456
+ children,
4457
+ onKeyDown,
4458
+ onSelect,
4459
+ products,
4460
+ selected
4461
+ }) {
4462
+ const id = useInstanceId("product-tabs");
4463
+ const tabId = slug => `${id}-tab-${slug}`;
4464
+ const tabRefs = React.useRef(new Map());
4465
+ const handleKeyDown = event => {
4466
+ var _a;
4467
+ const offset = horizontalStep(event.key);
4468
+ if (offset) {
4469
+ event.preventDefault();
4470
+ const index = products.indexOf(selected);
4471
+ const next = products[(index + offset + products.length) % products.length];
4472
+ onSelect(next.slug);
4473
+ (_a = tabRefs.current.get(next.slug)) === null || _a === void 0 ? void 0 : _a.focus();
4474
+ } else {
4475
+ onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
4476
+ }
4477
+ };
4478
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
4479
+ "aria-label": "Product",
4480
+ className: styles$e.tablist,
4481
+ onKeyDown: handleKeyDown,
4482
+ role: "tablist"
4483
+ }, products.map(product => (/*#__PURE__*/React.createElement("button", {
4484
+ "aria-controls": `${id}-panel`,
4485
+ "aria-selected": product === selected,
4486
+ className: styles$e.tab,
4487
+ id: tabId(product.slug),
4488
+ key: product.slug,
4489
+ onClick: () => onSelect(product.slug),
4490
+ ref: element => {
4491
+ if (element) {
4492
+ tabRefs.current.set(product.slug, element);
4493
+ } else {
4494
+ tabRefs.current.delete(product.slug);
3726
4495
  }
4496
+ },
4497
+ role: "tab",
4498
+ tabIndex: product === selected ? 0 : -1,
4499
+ type: "button"
4500
+ }, /*#__PURE__*/React.createElement(ProductIcon, {
4501
+ slug: product.slug
4502
+ }), product === selected ? product.appName : (/*#__PURE__*/React.createElement("span", {
4503
+ className: styles$i.visuallyHidden
4504
+ }, product.appName)))))), /*#__PURE__*/React.createElement("div", {
4505
+ "aria-labelledby": tabId(selected.slug),
4506
+ className: styles$i.panel,
4507
+ id: `${id}-panel`,
4508
+ role: "tabpanel"
4509
+ }, children));
4510
+ }
4511
+
4512
+ function EndOfResultsObserver({
4513
+ onVisible
4514
+ }) {
4515
+ const ref = React.useRef(null);
4516
+ React.useEffect(() => {
4517
+ const observer = new IntersectionObserver(([entry]) => {
4518
+ if (entry.isIntersecting) onVisible();
3727
4519
  });
3728
- return () => {
3729
- current = false;
3730
- };
3731
- }, [accessibleKey]);
3732
- if ((settled === null || settled === void 0 ? void 0 : settled.accessibleKey) === accessibleKey) {
3733
- return {
3734
- products: settled.products,
3735
- status: "done"
3736
- };
3737
- } else {
3738
- return {
3739
- status: "loading"
3740
- };
3741
- }
4520
+ if (ref.current) observer.observe(ref.current);
4521
+ return () => observer.disconnect();
4522
+ }, [onVisible]);
4523
+ return /*#__PURE__*/React.createElement("div", {
4524
+ ref: ref
4525
+ });
3742
4526
  }
3743
- function inSwitcherOrder(apps) {
3744
- return groupApps(apps).restProducts.flatMap(app => SEARCHABLE_PRODUCTS.filter(product => product.appName === app.name));
4527
+
4528
+ const resultId = (listboxId, index) => `${listboxId}-result-${index}`;
4529
+
4530
+ var styles$d = {"result":"result-Thq2C","title":"title-KYUEn","subtitle":"subtitle-R2haq","image":"image-KVDor","avatar":"avatar-6Gh62","photo":"photo-FZR3Q","badge":"badge-trUKt"};
4531
+
4532
+ function SearchResult({
4533
+ category,
4534
+ highlighted,
4535
+ id,
4536
+ imageUrl,
4537
+ onHighlight,
4538
+ product,
4539
+ subtitle,
4540
+ title,
4541
+ url
4542
+ }) {
4543
+ const ref = React.useRef(null);
4544
+ // A hovered result is already under the pointer, and scrolling it clear of
4545
+ // the panel's scroll padding would slide the list out from under the mouse.
4546
+ const highlightedByPointer = React.useRef(false);
4547
+ React.useEffect(() => {
4548
+ var _a;
4549
+ if (!highlighted) return;
4550
+ if (highlightedByPointer.current) {
4551
+ highlightedByPointer.current = false;
4552
+ } else {
4553
+ (_a = ref.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({
4554
+ block: "nearest"
4555
+ });
4556
+ }
4557
+ }, [highlighted]);
4558
+ const highlightFromPointer = () => {
4559
+ if (highlighted) return;
4560
+ highlightedByPointer.current = true;
4561
+ onHighlight();
4562
+ };
4563
+ // Focus stays in the search input, which points at the highlighted result
4564
+ // with aria-activedescendant, so results are kept out of the tab order.
4565
+ // Mouse move rather than enter: arrowing scrolls results under a resting
4566
+ // pointer, and that shouldn't steal the highlight.
4567
+ return /*#__PURE__*/React.createElement("a", {
4568
+ "aria-selected": highlighted,
4569
+ className: styles$d.result,
4570
+ href: url,
4571
+ id: id,
4572
+ onMouseMove: highlightFromPointer,
4573
+ ref: ref,
4574
+ role: "option",
4575
+ tabIndex: -1
4576
+ }, /*#__PURE__*/React.createElement(ResultImage, {
4577
+ category: category,
4578
+ imageUrl: imageUrl,
4579
+ product: product
4580
+ }), /*#__PURE__*/React.createElement("span", {
4581
+ className: styles$d.title
4582
+ }, title), /*#__PURE__*/React.createElement("span", {
4583
+ className: styles$d.subtitle
4584
+ }, subtitle));
4585
+ }
4586
+ // A person's photo or the result's own image, badged with its product, or
4587
+ // just the product's logomark when there's no image.
4588
+ function ResultImage({
4589
+ category,
4590
+ imageUrl,
4591
+ product
4592
+ }) {
4593
+ const icon = size => isProductIconSlug(product) ? (/*#__PURE__*/React.createElement(ProductIcon, {
4594
+ size: size,
4595
+ slug: product
4596
+ })) : null;
4597
+ return /*#__PURE__*/React.createElement("span", {
4598
+ "aria-hidden": true,
4599
+ className: styles$d.image
4600
+ }, imageUrl ? (/*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("img", {
4601
+ alt: "",
4602
+ className: category === "profile" ? styles$d.avatar : styles$d.photo,
4603
+ loading: "lazy",
4604
+ src: imageUrl
4605
+ }), /*#__PURE__*/React.createElement("span", {
4606
+ className: styles$d.badge
4607
+ }, icon(16)))) : icon("100%"));
3745
4608
  }
3746
4609
 
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
4610
+ var styles$c = {"message":"message-1-PP-","heading":"heading-LioUF"};
4611
+
4612
+ // Sections with a heading render as labelled groups; ones without render
4613
+ // their items straight into the listbox. Indexes run across every section.
4614
+ function ResultListbox({
4615
+ busy,
4616
+ highlightedIndex,
4617
+ id,
4618
+ label,
4619
+ labelledBy,
4620
+ onHighlight,
4621
+ sections
3754
4622
  }) {
3755
- const searchable = useSearchableProducts(apps);
3756
- if (searchable.status === "done" && searchable.products.length > 0) {
3757
- return /*#__PURE__*/React.createElement(SearchButtonAndDialog, {
3758
- products: searchable.products
4623
+ let index = 0;
4624
+ const renderItem = item => {
4625
+ const itemIndex = index++;
4626
+ return /*#__PURE__*/React.createElement(SearchResult, {
4627
+ category: item.category,
4628
+ highlighted: itemIndex === highlightedIndex,
4629
+ id: resultId(id, itemIndex),
4630
+ imageUrl: item.imageUrl,
4631
+ key: item.key,
4632
+ onHighlight: () => onHighlight(itemIndex),
4633
+ product: item.product,
4634
+ subtitle: item.subtitle,
4635
+ title: item.title,
4636
+ url: item.url
3759
4637
  });
4638
+ };
4639
+ return /*#__PURE__*/React.createElement("div", {
4640
+ "aria-busy": busy,
4641
+ "aria-label": label,
4642
+ "aria-labelledby": labelledBy,
4643
+ id: id,
4644
+ role: "listbox"
4645
+ }, sections.map((section, sectionIndex) => {
4646
+ if (section.heading === undefined) {
4647
+ return /*#__PURE__*/React.createElement(React.Fragment, {
4648
+ key: section.key
4649
+ }, section.items.map(renderItem));
4650
+ }
4651
+ const headingId = `${id}-group-${sectionIndex}`;
4652
+ return /*#__PURE__*/React.createElement("div", {
4653
+ "aria-labelledby": headingId,
4654
+ key: section.key,
4655
+ role: "group"
4656
+ }, /*#__PURE__*/React.createElement("div", {
4657
+ className: styles$c.heading,
4658
+ id: headingId
4659
+ }, section.heading), section.items.map(renderItem));
4660
+ }));
4661
+ }
4662
+
4663
+ function RecentlyVisited({
4664
+ count,
4665
+ hasMore,
4666
+ items,
4667
+ loading,
4668
+ showMore,
4669
+ ...navigation
4670
+ }) {
4671
+ const headingId = `${navigation.id}-heading`;
4672
+ if (items.length === 0 && !hasMore && !loading) {
4673
+ return /*#__PURE__*/React.createElement("p", {
4674
+ className: styles$c.message
4675
+ }, "Pages you visit will show up here.");
3760
4676
  } else {
3761
- return null;
4677
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
4678
+ className: styles$c.heading,
4679
+ id: headingId
4680
+ }, "Recently visited"), /*#__PURE__*/React.createElement(ResultListbox, {
4681
+ labelledBy: headingId,
4682
+ sections: [{
4683
+ items,
4684
+ key: "recents"
4685
+ }],
4686
+ ...navigation
4687
+ }), hasMore && /*#__PURE__*/React.createElement(EndOfResultsObserver, {
4688
+ key: count,
4689
+ onVisible: showMore
4690
+ }));
3762
4691
  }
3763
4692
  }
3764
- function SearchButtonAndDialog({
3765
- products
4693
+
4694
+ var styles$b = {"header":"header-sXEyG","input":"input-YQkTC","clearButton":"clearButton-9Yh9w"};
4695
+
4696
+ // A combobox: focus stays here while aria-activedescendant points screen
4697
+ // readers at the highlighted result in the listbox it controls.
4698
+ function SearchInput({
4699
+ expanded,
4700
+ highlightedIndex,
4701
+ inputRef,
4702
+ listboxId,
4703
+ onChange,
4704
+ onFocus,
4705
+ onKeyDown,
4706
+ value
3766
4707
  }) {
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"} /`,
4708
+ return /*#__PURE__*/React.createElement("label", {
4709
+ className: styles$b.header
4710
+ }, /*#__PURE__*/React.createElement(Icon, {
4711
+ name: "general.search",
4712
+ size: 20,
4713
+ theme: "--t-icon-color-dim"
4714
+ }), /*#__PURE__*/React.createElement("input", {
4715
+ "aria-activedescendant": highlightedIndex === null ? undefined : resultId(listboxId, highlightedIndex),
4716
+ "aria-autocomplete": "list",
4717
+ "aria-controls": expanded ? listboxId : undefined,
4718
+ "aria-expanded": expanded,
3791
4719
  "aria-label": "Search",
3792
- onClick: () => setOpen(true),
4720
+ autoFocus: true,
4721
+ className: styles$b.input,
4722
+ onChange: event => onChange(event.target.value),
4723
+ onFocus: onFocus,
4724
+ onKeyDown: onKeyDown,
4725
+ placeholder: "Search",
4726
+ ref: inputRef,
4727
+ role: "combobox",
4728
+ type: "search",
4729
+ value: value
4730
+ }), value !== "" && (/*#__PURE__*/React.createElement("button", {
4731
+ "aria-label": "Clear search",
4732
+ className: styles$b.clearButton,
4733
+ onClick: () => {
4734
+ var _a;
4735
+ onChange("");
4736
+ (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
4737
+ },
4738
+ tabIndex: -1,
3793
4739
  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
- })));
4740
+ }, /*#__PURE__*/React.createElement(Icon, {
4741
+ name: "general.smallX"
4742
+ }))));
4743
+ }
4744
+
4745
+ function SearchResults({
4746
+ label,
4747
+ sections,
4748
+ status,
4749
+ term,
4750
+ ...navigation
4751
+ }) {
4752
+ switch (status) {
4753
+ case "idle":
4754
+ return null;
4755
+ case "loading":
4756
+ if (sections.length > 0) {
4757
+ return /*#__PURE__*/React.createElement(ResultListbox, {
4758
+ busy: true,
4759
+ label: label,
4760
+ sections: sections,
4761
+ ...navigation
4762
+ });
4763
+ }
4764
+ return /*#__PURE__*/React.createElement("p", {
4765
+ className: styles$c.message,
4766
+ role: "status"
4767
+ }, "Searching\u2026");
4768
+ case "error":
4769
+ return /*#__PURE__*/React.createElement("p", {
4770
+ className: styles$c.message,
4771
+ role: "alert"
4772
+ }, "Search isn\u2019t working right now. Try again.");
4773
+ case "done":
4774
+ if (sections.length === 0) {
4775
+ return /*#__PURE__*/React.createElement("p", {
4776
+ className: styles$c.message,
4777
+ role: "status"
4778
+ }, "No results for \u201C", term.trim(), "\u201D");
4779
+ }
4780
+ return /*#__PURE__*/React.createElement(ResultListbox, {
4781
+ busy: false,
4782
+ label: label,
4783
+ sections: sections,
4784
+ ...navigation
4785
+ });
4786
+ }
3804
4787
  }
4788
+
4789
+ // Keys the recents list for navigation, so the highlight
4790
+ // stays put as more recents load and as their lookups answer.
4791
+ const RECENTS = "recents";
3805
4792
  function SearchPanel({
4793
+ mobile = false,
4794
+ env,
3806
4795
  products
3807
4796
  }) {
3808
4797
  var _a, _b;
@@ -3815,11 +4804,15 @@ true;
3815
4804
  const hasTerm = term.trim() !== "";
3816
4805
  const search = useProductSearch(product.slug, term);
3817
4806
  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);
4807
+ const sections = React.useMemo(() => groupSections(groups), [groups]);
4808
+ const {
4809
+ visits,
4810
+ ...recents
4811
+ } = useRecentVisits(env);
4812
+ const recentItems = visits.map(visitItem);
4813
+ const items = hasTerm ? sections.flatMap(section => section.items) : recentItems;
4814
+ // Phones open results by tapping them, with no highlight to arrow through.
4815
+ const navigation = useResultNavigation(items, hasTerm ? sections : RECENTS, !mobile);
3823
4816
  const {
3824
4817
  highlightedIndex
3825
4818
  } = navigation;
@@ -3842,296 +4835,269 @@ true;
3842
4835
  React.useEffect(() => {
3843
4836
  if (hasTerm) saveLastProduct(product.slug);
3844
4837
  }, [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),
4838
+ const results = hasTerm ? (/*#__PURE__*/React.createElement(SearchResults, {
4839
+ highlightedIndex: highlightedIndex,
4840
+ id: listboxId,
4841
+ label: `${product.appName} results`,
4842
+ onHighlight: navigation.highlightIndex,
4843
+ sections: sections,
4844
+ status: search.status,
4845
+ term: term
4846
+ })) : (/*#__PURE__*/React.createElement(RecentlyVisited, {
4847
+ highlightedIndex: highlightedIndex,
4848
+ id: listboxId,
4849
+ items: recentItems,
4850
+ onHighlight: navigation.highlightIndex,
4851
+ ...recents
4852
+ }));
4853
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SearchInput, {
4854
+ expanded: items.length > 0,
4855
+ highlightedIndex: highlightedIndex,
4856
+ inputRef: inputRef,
4857
+ listboxId: listboxId,
4858
+ onChange: setTerm,
3853
4859
  onFocus: () => setAnnounceHighlight(false),
3854
4860
  onKeyDown: navigation.handleKeyDown,
3855
- placeholder: "Search",
3856
- ref: inputRef,
3857
- role: "combobox",
3858
- type: "search",
3859
4861
  value: term
3860
- }), /*#__PURE__*/React.createElement(ProductTabs, {
3861
- onKeyDown: handleTabsKeyDown,
4862
+ }), mobile ? (/*#__PURE__*/React.createElement(ProductSelect, {
3862
4863
  onSelect: setSelectedSlug,
3863
4864
  products: products,
3864
4865
  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", {
4866
+ }, results)) : (/*#__PURE__*/React.createElement(ProductTabs, {
4867
+ onKeyDown: handleTabsKeyDown,
4868
+ onSelect: setSelectedSlug,
4869
+ products: products,
4870
+ selected: product
4871
+ }, results)), /*#__PURE__*/React.createElement("div", {
3873
4872
  "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, {
4873
+ className: styles$i.visuallyHidden
4874
+ }, announceHighlight ? (_b = navigation.highlighted) === null || _b === void 0 ? void 0 : _b.title : null), mobile ? null : /*#__PURE__*/React.createElement(KeyboardHints, {
3876
4875
  copied: navigation.copied
3877
4876
  }));
3878
4877
  }
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];
3892
- }
3893
4878
 
3894
- function parseValue(element) {
3895
- const datatype = element.getAttribute("datatype");
3896
- switch (datatype) {
3897
- case "boolean":
3898
- return element.content === "true" || element.content === "1";
3899
- default:
3900
- return element.content;
3901
- }
3902
- }
3903
- function getAllPcoMeta() {
3904
- const elements = document.head.querySelectorAll(`meta[property^="pco:"]`);
3905
- const meta = {};
3906
- elements.forEach(e => {
3907
- const property = e.getAttribute("property");
3908
- meta[property.substring("pco:".length)] = parseValue(e);
3909
- });
3910
- return meta;
4879
+ // Products open modals from many libraries, not all of which mark themselves
4880
+ // with roles, so a modal is recognized by what it does: cover the page.
4881
+ function isModalOpen() {
4882
+ const {
4883
+ clientHeight,
4884
+ clientWidth
4885
+ } = document.documentElement;
4886
+ const element = document.elementFromPoint(clientWidth / 2, clientHeight / 2);
4887
+ return element !== null && (isInNativeDialog(element) || hasBackdrop(element, clientWidth, clientHeight));
3911
4888
  }
3912
- // usePcoMeta only reads each tag's content. Recently Visited also needs the
3913
- // pco:recently_visitable tag's `metadata` attribute, which can change while
3914
- // the content stays the same, so it is tracked in its own store.
3915
- function getRecentlyVisitableMetadata() {
3916
- var _a, _b;
3917
- return (_b = (_a = document.head.querySelector(`meta[property="pco:recently_visitable"]`)) === null || _a === void 0 ? void 0 : _a.getAttribute("metadata")) !== null && _b !== void 0 ? _b : null;
4889
+ // A native modal's backdrop is drawn by the browser rather than being an
4890
+ // element; hit-testing it lands on the dialog itself.
4891
+ function isInNativeDialog(element) {
4892
+ return element.closest("dialog[open]") !== null;
3918
4893
  }
3919
- const store = createStore(getAllPcoMeta());
3920
- const recentlyVisitableMetadataStore = createStore({
3921
- metadata: getRecentlyVisitableMetadata()
3922
- });
3923
- function sync() {
3924
- const nextState = getAllPcoMeta();
3925
- if (!shallowCompare(store.getState(), nextState)) store.setState(nextState);
3926
- const metadata = getRecentlyVisitableMetadata();
3927
- if (recentlyVisitableMetadataStore.getState().metadata !== metadata) {
3928
- recentlyVisitableMetadataStore.setState({
3929
- metadata
3930
- });
4894
+ function hasBackdrop(element, width, height) {
4895
+ let current = element;
4896
+ while (current && !isBackdrop(current, width, height)) {
4897
+ current = current.parentElement;
3931
4898
  }
4899
+ return current !== null;
3932
4900
  }
3933
- const observer = new MutationObserver(sync);
3934
- let hasTriggeredObserve = false;
3935
- function observe() {
3936
- if (hasTriggeredObserve) return;
3937
- hasTriggeredObserve = true;
3938
- // The module-load snapshot above can predate meta tags that appear later
3939
- // in <head> (e.g. pco:resource, yielded right before </head>) if this
3940
- // module's own script tag isn't deferred and runs mid-parse. Those tags
3941
- // never mutate afterward, so the observer alone would never catch them.
3942
- // Re-read once now, since observe() is only called once a consumer is
3943
- // actually mounting, which is always after the document has fully parsed.
3944
- sync();
3945
- observer.observe(document.head, {
3946
- attributes: true,
3947
- childList: true,
3948
- subtree: true
3949
- });
3950
- }
3951
- function usePcoMeta() {
3952
- observe();
3953
- return shim.useSyncExternalStore(store.subscribe, () => store.getState());
4901
+ // Some backdrops start below the topbar rather than at the top of the screen,
4902
+ // so the layer only has to span the width and reach from the center to the
4903
+ // bottom. Its own box has to reach the center: a bar along the bottom can
4904
+ // hold a panel that pops up over the center, like Services' media player.
4905
+ // react-aria leaves a closed modal's backdrop up while it fades out, search's
4906
+ // own included, so one that's on its way out doesn't count.
4907
+ function isBackdrop(element, width, height) {
4908
+ const rect = element.getBoundingClientRect();
4909
+ return !element.matches("[data-exiting]") && getComputedStyle(element).position === "fixed" && rect.width >= width - SCROLLBAR_SLACK && rect.top <= height / 2 && rect.bottom >= height - ROUNDING_SLACK;
3954
4910
  }
3955
- function useRecentlyVisitableMetadata() {
3956
- observe();
3957
- return shim.useSyncExternalStore(recentlyVisitableMetadataStore.subscribe, () => recentlyVisitableMetadataStore.getState().metadata);
4911
+ // react-aria keeps a classic scrollbar's gutter reserved while its modals lock
4912
+ // scrolling, so their backdrops stop short of the right edge by its width.
4913
+ const SCROLLBAR_SLACK = 24;
4914
+ // Rects are fractional at non-100% zoom, but clientWidth and clientHeight are
4915
+ // rounded to whole pixels.
4916
+ const ROUNDING_SLACK = 1;
4917
+
4918
+ function useSearchShortcut(enabled, open, setOpen) {
4919
+ React.useEffect(() => {
4920
+ if (!enabled) return undefined;
4921
+ // Rich text editors like Trix bind ⌘K to "insert link"; capturing at the
4922
+ // window and stopping propagation keeps them from ever seeing it. Under a
4923
+ // product's modal, search stays shut rather than stacking a second modal,
4924
+ // and the keystroke goes on to the product.
4925
+ const handleKeyDown = event => {
4926
+ if (open && isSearchShortcut(event)) {
4927
+ event.preventDefault();
4928
+ event.stopPropagation();
4929
+ setOpen(false);
4930
+ } else if (!open && (isSearchShortcut(event) || isLegacySearchShortcut(event)) && !isModalOpen()) {
4931
+ event.preventDefault();
4932
+ event.stopPropagation();
4933
+ setOpen(true);
4934
+ }
4935
+ };
4936
+ window.addEventListener("keydown", handleKeyDown, {
4937
+ capture: true
4938
+ });
4939
+ return () => window.removeEventListener("keydown", handleKeyDown, {
4940
+ capture: true
4941
+ });
4942
+ }, [enabled, open, setOpen]);
3958
4943
  }
3959
4944
 
3960
- const MAX_ENTRIES = 20;
3961
- const FORMAT_VERSION = 1;
3962
- const MAX_TEXT_LENGTH = 200;
3963
- const PRODUCTS = ["accounts", "calendar", "check-ins", "giving", "groups", "home", "login", "people", "publishing", "registrations", "services"];
3964
- function isId(value) {
3965
- return value !== "" && Array.from(value).every(character => character >= "0" && character <= "9");
3966
- }
3967
- // A PCRL looks like `<product>/v2/<resource_type>/<id>`.
3968
- function isPcrl(value) {
3969
- const segments = value.split("/");
3970
- return segments.length === 4 && PRODUCTS.some(product => product === segments[0]) && isId(segments[3]);
3971
- }
3972
- function isNonBlankString(value) {
3973
- return typeof value === "string" && value.trim() !== "";
3974
- }
3975
- function isRecord(value) {
3976
- return typeof value === "object" && value !== null && !Array.isArray(value);
3977
- }
3978
- function entryKey(entry) {
3979
- if (entry.type === "resource") {
3980
- return `resource:${entry.pcrl}`;
3981
- } else {
3982
- return `page:${entry.product}:${entry.path}`;
3983
- }
3984
- }
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;
3994
- }
3995
- }
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("");
4945
+ const PERMISSION_CHECKS = {
4946
+ "check-ins": () => fetchMe("check-ins", "permission").then(({
4947
+ permission
4948
+ }) => permission !== null),
4949
+ giving: personId => {
4950
+ if (personId === undefined) {
4951
+ return Promise.reject(new Error("No person to check Giving access for"));
4952
+ } else {
4953
+ return fetchAttributes(`giving/v2/person_permissions/${personId}`, {
4954
+ "fields[PersonPermissions]": "can_search"
4955
+ }).then(({
4956
+ can_search
4957
+ }) => can_search);
4958
+ }
4959
+ },
4960
+ people: () => fetchMe("people", "people_permissions,site_administrator").then(({
4961
+ people_permissions,
4962
+ site_administrator
4963
+ }) => site_administrator || people_permissions !== null),
4964
+ registrations: () => fetchMe("registrations", "can_search").then(({
4965
+ can_search
4966
+ }) => can_search),
4967
+ services: () => fetchMe("services", "can_search").then(({
4968
+ can_search
4969
+ }) => can_search)
4970
+ };
4971
+ function fetchMe(product, fields) {
4972
+ return fetchAttributes(`${product}/v2/me`, {
4973
+ "fields[Person]": fields
4974
+ });
3999
4975
  }
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)
4008
- };
4009
- }
4976
+ function fetchAttributes(path, params) {
4977
+ const url = `${window.location.origin}/~api/${path}?${new URLSearchParams(params)}`;
4978
+ return apiRequest(defaultFetch, url, {
4979
+ headers: apiVersionHeaders(url)
4980
+ }).then(({
4981
+ json,
4982
+ ok
4983
+ }) => {
4984
+ if (!ok || json === null) throw new Error("Permission check failed");
4985
+ return json.data.attributes;
4986
+ });
4010
4987
  }
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);
4988
+ // Permissions rarely change mid-session, so a successful check is kept until
4989
+ // the page reloads. A failed one is dropped so a remount tries again.
4990
+ const permissionCache = new Map();
4991
+ function canSearch(slug, personId) {
4992
+ const check = PERMISSION_CHECKS[slug];
4993
+ const cached = permissionCache.get(slug);
4994
+ if (!check) {
4995
+ return Promise.resolve(true);
4996
+ } else if (cached) {
4997
+ return cached;
4022
4998
  } else {
4023
- return [];
4999
+ const promise = check(personId).catch(() => {
5000
+ permissionCache.delete(slug);
5001
+ return false;
5002
+ });
5003
+ permissionCache.set(slug, promise);
5004
+ return promise;
4024
5005
  }
4025
5006
  }
4026
- function addVisit(entries, visit) {
4027
- const key = entryKey(visit);
4028
- return [clampEntry(visit), ...entries.filter(entry => entryKey(entry) !== key)].slice(0, MAX_ENTRIES);
4029
- }
4030
- function serializeRecentlyVisited(entries, personId) {
4031
- return JSON.stringify({
4032
- entries,
4033
- person: personId,
4034
- v: FORMAT_VERSION
4035
- });
4036
- }
4037
5007
 
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;
4044
- }
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 "";
4052
- }
4053
- }
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"
5008
+ function useSearchableProducts(apps) {
5009
+ // Keyed by name because providers hand over a fresh `apps` array on renders
5010
+ // where nothing changed.
5011
+ const accessibleKey = inSwitcherOrder(apps).map(({
5012
+ slug
5013
+ }) => slug).join(",");
5014
+ const personId = useCurrentPersonId();
5015
+ const [settled, setSettled] = React.useState();
5016
+ React.useEffect(() => {
5017
+ let current = true;
5018
+ const accessible = accessibleKey.split(",").flatMap(slug => SEARCHABLE_PRODUCTS.filter(product => product.slug === slug));
5019
+ Promise.all(accessible.map(({
5020
+ slug
5021
+ }) => canSearch(slug, personId))).then(allowed => {
5022
+ if (current) {
5023
+ setSettled({
5024
+ accessibleKey,
5025
+ personId,
5026
+ products: accessible.filter((_, index) => allowed[index])
5027
+ });
5028
+ }
5029
+ });
5030
+ return () => {
5031
+ current = false;
4066
5032
  };
4067
- } else if (title && title.trim() !== "" && product) {
5033
+ }, [accessibleKey, personId]);
5034
+ if ((settled === null || settled === void 0 ? void 0 : settled.accessibleKey) === accessibleKey && settled.personId === personId) {
4068
5035
  return {
4069
- details: detailsFromMetadata(metadata),
4070
- path: window.location.pathname,
4071
- product,
4072
- title,
4073
- type: "page"
5036
+ products: settled.products,
5037
+ status: "done"
4074
5038
  };
4075
5039
  } else {
4076
- return null;
5040
+ return {
5041
+ status: "loading"
5042
+ };
4077
5043
  }
4078
5044
  }
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]);
5045
+ function inSwitcherOrder(apps) {
5046
+ return groupApps(apps).restProducts.flatMap(app => SEARCHABLE_PRODUCTS.filter(product => product.appName === app.name));
4093
5047
  }
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);
5048
+
5049
+ /**
5050
+ * Writes the height of the visible part of the page to the parent of `ref`'s
5051
+ * element as `--visual-viewport-height`. It shrinks when an on-screen keyboard
5052
+ * opens; `100dvh` doesn't, since mobile Safari draws the keyboard over it.
5053
+ */
5054
+ function useVisualViewportHeight(ref, enabled) {
5055
+ React.useEffect(() => {
5056
+ var _a;
5057
+ const viewport = window.visualViewport;
5058
+ const parent = (_a = ref.current) === null || _a === void 0 ? void 0 : _a.parentElement;
5059
+ if (!enabled || !viewport || !parent) return undefined;
5060
+ const update = () => parent.style.setProperty("--visual-viewport-height", `${viewport.height}px`);
5061
+ update();
5062
+ viewport.addEventListener("resize", update);
5063
+ return () => {
5064
+ viewport.removeEventListener("resize", update);
5065
+ parent.style.removeProperty("--visual-viewport-height");
5066
+ };
5067
+ }, [enabled, ref]);
4101
5068
  }
4102
5069
 
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
5070
+ function ProductSearch({
5071
+ apps,
5072
+ mobile = false,
5073
+ env
4109
5074
  }) {
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
- }
5075
+ const searchable = useSearchableProducts(apps);
5076
+ const [open, setOpen] = React.useState(false);
5077
+ const modalRef = React.useRef(null);
5078
+ // Sizes Tapestry's modal frame, the dialog's parent, to fit above the keyboard.
5079
+ useVisualViewportHeight(modalRef, mobile && open);
5080
+ const available = searchable.status === "done" && searchable.products.length > 0;
5081
+ useSearchShortcut(available, open, setOpen);
5082
+ if (!available) return null;
5083
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SearchButton, {
5084
+ mobile: mobile,
5085
+ onClick: () => setOpen(true)
5086
+ }), /*#__PURE__*/React.createElement(tapestry.Modal, {
5087
+ "aria-label": "Search",
5088
+ className: classnames(styles$i.modal, {
5089
+ [styles$i.mobile]: mobile
5090
+ }),
5091
+ dismissible: true,
5092
+ onClose: () => setOpen(false),
5093
+ open: open,
5094
+ ref: modalRef,
5095
+ size: "lg"
5096
+ }, /*#__PURE__*/React.createElement(SearchPanel, {
5097
+ mobile: mobile,
5098
+ env: env,
5099
+ products: searchable.products
5100
+ })));
4135
5101
  }
4136
5102
 
4137
5103
  function useRecordRecentlyVisited({
@@ -4176,9 +5142,9 @@ true;
4176
5142
  `), children);
4177
5143
  }
4178
5144
 
4179
- var styles$9 = {"route":"route--8BWe","is-active":"is-active-FsU9Y","apps-routes-separator":"apps-routes-separator-Nlm-N"};
5145
+ var styles$a = {"route":"route--8BWe","is-active":"is-active-FsU9Y","apps-routes-separator":"apps-routes-separator-Nlm-N"};
4180
5146
 
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"};
5147
+ 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
5148
 
4183
5149
  const UserMenu = ({
4184
5150
  appName,
@@ -4197,7 +5163,7 @@ true;
4197
5163
  component: Outsider,
4198
5164
  onOutsideClick: () => setUserMenuVisible(false),
4199
5165
  cleanup: !userMenuVisible,
4200
- className: styles$8.popup,
5166
+ className: styles$9.popup,
4201
5167
  isVisible: userMenuVisible
4202
5168
  }, /*#__PURE__*/React.createElement("div", null, hasConnectedPeople ? (/*#__PURE__*/React.createElement("div", {
4203
5169
  style: {
@@ -4341,11 +5307,11 @@ true;
4341
5307
  userMenuVisible
4342
5308
  }) => {
4343
5309
  return /*#__PURE__*/React.createElement("div", {
4344
- className: styles$8["trigger-wrap"],
5310
+ className: styles$9["trigger-wrap"],
4345
5311
  "data-pendo": "topbar-org-switcher"
4346
5312
  }, /*#__PURE__*/React.createElement(Hoverable, {
4347
- className: classnames(styles$8["trigger"], {
4348
- [styles$8["is-visible"]]: userMenuVisible
5313
+ className: classnames(styles$9["trigger"], {
5314
+ [styles$9["is-visible"]]: userMenuVisible
4349
5315
  }),
4350
5316
  component: "button",
4351
5317
  type: "button",
@@ -4357,7 +5323,7 @@ true;
4357
5323
  requestConnectedPeopleFetch();
4358
5324
  }
4359
5325
  }, showOrgName && (/*#__PURE__*/React.createElement("span", {
4360
- className: styles$8["trigger-orgname"]
5326
+ className: styles$9["trigger-orgname"]
4361
5327
  }, orgName)), /*#__PURE__*/React.createElement(DisclosureChevronIcon, null)));
4362
5328
  };
4363
5329
 
@@ -4417,13 +5383,14 @@ true;
4417
5383
  colors: props.colors,
4418
5384
  requestAppsFetch: props.requestAppsFetch
4419
5385
  })), /*#__PURE__*/React.createElement("div", {
4420
- className: styles$9["apps-routes-separator"]
5386
+ className: styles$a["apps-routes-separator"]
4421
5387
  }), routesVisible && props.routes, /*#__PURE__*/React.createElement("div", {
4422
5388
  style: {
4423
5389
  margin: "auto"
4424
5390
  }
4425
5391
  }), !notificationsEnabled && notifications(), productSearchEnabled ? (/*#__PURE__*/React.createElement(ProductSearch, {
4426
- apps: props.apps
5392
+ apps: props.apps,
5393
+ env: props.env
4427
5394
  })) : search({
4428
5395
  hideRoutes: () => setRoutesVisible(false),
4429
5396
  showRoutes: () => setRoutesVisible(true)
@@ -4457,12 +5424,12 @@ true;
4457
5424
  ...nativeProps
4458
5425
  }) => {
4459
5426
  return /*#__PURE__*/React.createElement("a", {
4460
- className: classnames(styles$9["route"], className, active && styles$9["is-active"]),
5427
+ className: classnames(styles$a["route"], className, active && styles$a["is-active"]),
4461
5428
  ...nativeProps
4462
5429
  });
4463
5430
  };
4464
5431
 
4465
- var styles$7 = {"avatar":"avatar-tW-wn"};
5432
+ var styles$8 = {"avatar":"avatar-tW-wn"};
4466
5433
 
4467
5434
  function Avatar({
4468
5435
  env,
@@ -4472,7 +5439,7 @@ true;
4472
5439
  }) {
4473
5440
  const url = incomingUrl || `${pcoUrl$1(env)("people")}/static/no_photo_thumbnail_gray.svg`;
4474
5441
  return /*#__PURE__*/React.createElement("div", {
4475
- className: styles$7.avatar,
5442
+ className: styles$8.avatar,
4476
5443
  style: {
4477
5444
  backgroundImage: `url(${url})`,
4478
5445
  ...style
@@ -32231,7 +33198,7 @@ true;
32231
33198
 
32232
33199
  var reactDomExports = requireReactDom();
32233
33200
 
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"};
33201
+ 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
33202
 
32236
33203
  const AppsSwitcher = ({
32237
33204
  appName,
@@ -32259,8 +33226,8 @@ true;
32259
33226
  }, /*#__PURE__*/React.createElement("a", {
32260
33227
  "aria-current": name === appName ? "page" : undefined,
32261
33228
  "aria-label": name,
32262
- className: classnames(styles$6.link, {
32263
- [styles$6.current]: name === appName
33229
+ className: classnames(styles$7.link, {
33230
+ [styles$7.current]: name === appName
32264
33231
  }),
32265
33232
  "data-pendo": `app-switcher--change-app--${name}`,
32266
33233
  "data-turbolinks": false,
@@ -32272,48 +33239,48 @@ true;
32272
33239
  "aria-expanded": visible,
32273
33240
  "aria-haspopup": "true",
32274
33241
  "aria-label": `App switcher: ${appName}`,
32275
- className: styles$6.trigger,
33242
+ className: styles$7.trigger,
32276
33243
  onClick: onTriggerClick,
32277
33244
  type: "button"
32278
33245
  }, /*#__PURE__*/React.createElement(ColorAppIcon, {
32279
33246
  appName: appName.replace(/[\s-]/, "")
32280
33247
  })), /*#__PURE__*/reactDomExports.createPortal(/*#__PURE__*/React.createElement("div", {
32281
33248
  ref: containerRef,
32282
- className: classnames(styles$6.layer, {
32283
- [styles$6["layer--visible"]]: visible
33249
+ className: classnames(styles$7.layer, {
33250
+ [styles$7["layer--visible"]]: visible
32284
33251
  }),
32285
33252
  onClick: toggle,
32286
33253
  onKeyDown: handleKeyDown
32287
33254
  }, /*#__PURE__*/React.createElement("nav", {
32288
33255
  "aria-label": "Switch app",
32289
33256
  "aria-modal": visible,
32290
- className: styles$6.panel,
33257
+ className: styles$7.panel,
32291
33258
  "data-pendo": "app-switcher",
32292
33259
  onClick: event => event.stopPropagation(),
32293
33260
  role: "dialog"
32294
33261
  }, /*#__PURE__*/React.createElement("div", {
32295
- className: styles$6.header
33262
+ className: styles$7.header
32296
33263
  }, /*#__PURE__*/React.createElement("span", {
32297
- className: styles$6.heading
33264
+ className: styles$7.heading
32298
33265
  }, "Switch to"), /*#__PURE__*/React.createElement("button", {
32299
33266
  "aria-label": "Close",
32300
- className: styles$6.close,
33267
+ className: styles$7.close,
32301
33268
  onClick: toggle,
32302
33269
  type: "button"
32303
33270
  }, /*#__PURE__*/React.createElement(X, {
32304
33271
  "aria-hidden": "true",
32305
33272
  fill: "var(--t-icon-color-dim)"
32306
33273
  }))), /*#__PURE__*/React.createElement("ul", {
32307
- className: styles$6.list,
33274
+ className: styles$7.list,
32308
33275
  role: "list"
32309
33276
  }, homeProduct && productLink(homeProduct.name), restProducts.map(({
32310
33277
  name
32311
33278
  }) => productLink(name)), accountsProduct && (/*#__PURE__*/React.createElement("li", {
32312
- className: styles$6["accounts-item"]
33279
+ className: styles$7["accounts-item"]
32313
33280
  }, /*#__PURE__*/React.createElement("a", {
32314
33281
  "aria-current": appName === "Accounts" ? "page" : undefined,
32315
33282
  "aria-label": "Account settings",
32316
- className: classnames(styles$6.link, styles$6["accounts-link"]),
33283
+ className: classnames(styles$7.link, styles$7["accounts-link"]),
32317
33284
  "data-pendo": "app-switcher--change-app--Accounts",
32318
33285
  "data-turbolinks": false,
32319
33286
  href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(accountsProduct.name)
@@ -32336,14 +33303,14 @@ true;
32336
33303
  method: "localStorage"
32337
33304
  };
32338
33305
 
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"};
33306
+ 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","actions":"actions-INQ7-","profile-button":"profile-button-a2WO2"};
32340
33307
 
32341
33308
  function AccountSettingsLink({
32342
33309
  env
32343
33310
  }) {
32344
33311
  const accountsName = "accounts";
32345
33312
  return /*#__PURE__*/React.createElement("a", {
32346
- className: classnames(styles$5["appmenu-list-a"], styles$5["appmenu-list-a--bottom"]),
33313
+ className: classnames(styles$6["appmenu-list-a"], styles$6["appmenu-list-a--bottom"]),
32347
33314
  "data-pendo": `app-switcher--change-app--${accountsName}`,
32348
33315
  "data-turbolinks": false,
32349
33316
  href: pcoUrl$1(env)(accountsName) + getAppsSwitchProductPath(accountsName)
@@ -32366,7 +33333,7 @@ true;
32366
33333
  }) => (/*#__PURE__*/React.createElement("button", {
32367
33334
  "aria-label": "Close menu",
32368
33335
  type: "button",
32369
- className: styles$5["appmenu-close-button"],
33336
+ className: styles$6["appmenu-close-button"],
32370
33337
  style: style
32371
33338
  }, /*#__PURE__*/React.createElement(X, {
32372
33339
  fill: color,
@@ -32427,7 +33394,7 @@ true;
32427
33394
  "aria-haspopup": "true",
32428
33395
  "aria-label": `App switcher: ${appName}`,
32429
33396
  type: "button",
32430
- className: styles$5["appmenu-button"],
33397
+ className: styles$6["appmenu-button"],
32431
33398
  onClick: onTriggerClick
32432
33399
  }, /*#__PURE__*/React.createElement(ColorAppIcon, {
32433
33400
  appName: appName.replace(/[\s-]/, "")
@@ -32435,7 +33402,7 @@ true;
32435
33402
  item: "Topbar:Small:Menus:Products:visibility",
32436
33403
  render: () => (/*#__PURE__*/React.createElement("div", {
32437
33404
  ref: appMenuRef,
32438
- className: styles$5["appmenu-layer"],
33405
+ className: styles$6["appmenu-layer"],
32439
33406
  style: {
32440
33407
  ...(visible && {
32441
33408
  display: "block"
@@ -32452,37 +33419,37 @@ true;
32452
33419
  e.stopPropagation();
32453
33420
  }
32454
33421
  },
32455
- className: styles$5["appmenu-container"]
33422
+ className: styles$6["appmenu-container"]
32456
33423
  }, /*#__PURE__*/React.createElement("a", {
32457
33424
  "aria-current": appName === (homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name) ? "page" : undefined,
32458
33425
  "aria-label": homeProduct ? homeProduct.name : appName,
32459
33426
  "data-pendo": `app-switcher--change-app--${homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name}`,
32460
33427
  "data-turbolinks": false,
32461
- className: classnames(styles$5["appmenu-list-a"], styles$5["appmenu-list-a--top"]),
33428
+ className: classnames(styles$6["appmenu-list-a"], styles$6["appmenu-list-a--top"]),
32462
33429
  href: homeProduct ? pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name) : undefined
32463
33430
  }, /*#__PURE__*/React.createElement(UserMenuAppLockup, {
32464
33431
  appName: homeProduct ? homeProduct.name : appName
32465
33432
  })), /*#__PURE__*/React.createElement("div", {
32466
- className: styles$5["appmenu-scroll"]
33433
+ className: styles$6["appmenu-scroll"]
32467
33434
  }, /*#__PURE__*/React.createElement("ul", {
32468
33435
  "data-pendo": "app-switcher",
32469
- className: styles$5["appmenu-list"]
33436
+ className: styles$6["appmenu-list"]
32470
33437
  }, appsMenuFormatter(restProducts).map(({
32471
33438
  name
32472
33439
  }) => (/*#__PURE__*/React.createElement("li", {
32473
33440
  key: name,
32474
- className: styles$5["appmenu-list-li"]
33441
+ className: styles$6["appmenu-list-li"]
32475
33442
  }, /*#__PURE__*/React.createElement("a", {
32476
33443
  "aria-current": name === appName ? "page" : undefined,
32477
33444
  "aria-label": name,
32478
33445
  "data-pendo": `app-switcher--change-app--${name}`,
32479
33446
  "data-turbolinks": false,
32480
- className: styles$5["appmenu-list-a"],
33447
+ className: styles$6["appmenu-list-a"],
32481
33448
  href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(name)
32482
33449
  }, /*#__PURE__*/React.createElement(UserMenuAppLockup, {
32483
33450
  appName: name
32484
33451
  }))))), accountsProduct && (/*#__PURE__*/React.createElement("li", {
32485
- className: styles$5["appmenu-list-li"]
33452
+ className: styles$6["appmenu-list-li"]
32486
33453
  }, /*#__PURE__*/React.createElement(AccountSettingsLink, {
32487
33454
  env: env
32488
33455
  }))))))))
@@ -32546,19 +33513,25 @@ true;
32546
33513
  toggle: toggleProductSwitcher,
32547
33514
  visible: productSwitcherVisible
32548
33515
  })), routesMenuVisible && (/*#__PURE__*/React.createElement("div", {
32549
- className: styles$5["routes"]
33516
+ className: styles$6["routes"]
32550
33517
  }, props.routes)), /*#__PURE__*/React.createElement("button", {
32551
33518
  type: "button",
32552
- className: classnames(styles$5["current-route"], {
32553
- [styles$5["is-active"]]: routesMenuVisible
33519
+ className: classnames(styles$6["current-route"], {
33520
+ [styles$6["is-active"]]: routesMenuVisible
32554
33521
  }),
32555
33522
  onClick: props.routes.length > 0 ? () => {
32556
33523
  setProductSwitcherVisible(false);
32557
33524
  setRoutesMenuVisible(visible => !visible);
32558
33525
  } : null
32559
- }, /*#__PURE__*/React.createElement(CurrentRouteComponent, null, capitalize(currentRouteText)), props.routes.length > 0 && /*#__PURE__*/React.createElement(DisclosureChevronIcon, null)), /*#__PURE__*/React.createElement("button", {
33526
+ }, /*#__PURE__*/React.createElement(CurrentRouteComponent, null, capitalize$1(currentRouteText)), props.routes.length > 0 && /*#__PURE__*/React.createElement(DisclosureChevronIcon, null)), /*#__PURE__*/React.createElement("div", {
33527
+ className: styles$6["actions"]
33528
+ }, productSearchEnabled && (/*#__PURE__*/React.createElement(ProductSearch, {
33529
+ apps: props.apps,
33530
+ mobile: true,
33531
+ env: props.env
33532
+ })), /*#__PURE__*/React.createElement("button", {
32560
33533
  type: "button",
32561
- className: styles$5["profile-button"],
33534
+ className: styles$6["profile-button"],
32562
33535
  onClick: () => {
32563
33536
  var _a, _b;
32564
33537
  props.requestConnectedPeopleFetch();
@@ -32567,14 +33540,14 @@ true;
32567
33540
  }, /*#__PURE__*/React.createElement(Avatar, {
32568
33541
  env: props.env,
32569
33542
  url: props.userAvatarPath
32570
- })));
33543
+ }))));
32571
33544
  };
32572
33545
  const SmallRoute = ({
32573
33546
  active,
32574
33547
  className,
32575
33548
  ...props
32576
33549
  }) => (/*#__PURE__*/React.createElement("a", {
32577
- className: classnames(styles$5["route"], className, active && styles$5["is-active"]),
33550
+ className: classnames(styles$6["route"], className, active && styles$6["is-active"]),
32578
33551
  ...props
32579
33552
  }));
32580
33553
 
@@ -32713,7 +33686,7 @@ true;
32713
33686
  id: "topbar"
32714
33687
  };
32715
33688
 
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"};
33689
+ 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
33690
 
32718
33691
  function isPresent(value) {
32719
33692
  return value !== undefined && value.trim() !== "";
@@ -32742,15 +33715,31 @@ true;
32742
33715
  value
32743
33716
  }) {
32744
33717
  return /*#__PURE__*/React.createElement("div", {
32745
- className: styles$4["debug-pco-resource-field"]
33718
+ className: styles$5["debug-pco-resource-field"]
32746
33719
  }, /*#__PURE__*/React.createElement("span", {
32747
- className: styles$4["debug-pco-resource-field-label"]
33720
+ className: styles$5["debug-pco-resource-field-label"]
32748
33721
  }, label), /*#__PURE__*/React.createElement("span", {
32749
- className: styles$4["debug-pco-resource-field-value"]
33722
+ className: styles$5["debug-pco-resource-field-value"]
32750
33723
  }, value));
32751
33724
  }
33725
+ // Only staff ghosting or impersonating a person get pco:staff-access-level,
33726
+ // which keeps the panel away from regular users on staging.
32752
33727
  function DebugPcoResource({
32753
33728
  env
33729
+ }) {
33730
+ const {
33731
+ "staff-access-level": staffAccessLevel
33732
+ } = usePcoMeta();
33733
+ if (isPresent(staffAccessLevel)) {
33734
+ return /*#__PURE__*/React.createElement(DebugPanel, {
33735
+ env: env
33736
+ });
33737
+ } else {
33738
+ return null;
33739
+ }
33740
+ }
33741
+ function DebugPanel({
33742
+ env
32754
33743
  }) {
32755
33744
  const {
32756
33745
  recently_visitable: recentlyVisitable,
@@ -32759,12 +33748,17 @@ true;
32759
33748
  const [lookup, setLookup] = React.useState({
32760
33749
  status: "idle"
32761
33750
  });
32762
- const hasResource = isPresent(resource);
33751
+ const visit = useCurrentVisit();
33752
+ const personId = useCurrentPersonId();
33753
+ const recentlyVisited = useRecentlyVisited({
33754
+ env,
33755
+ personId
33756
+ });
33757
+ const pcrl = (visit === null || visit === void 0 ? void 0 : visit.type) === "resource" ? visit.pcrl : null;
32763
33758
  const hasRecentlyVisitable = isPresent(recentlyVisitable);
33759
+ const hasInvalidResource = isPresent(resource) && !isPcrl(resource);
32764
33760
  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;
33761
+ const url = pcrl ? pcrlLookupUrl(pcrl, env) : null;
32768
33762
  if (!url) {
32769
33763
  setLookup({
32770
33764
  status: "idle"
@@ -32796,37 +33790,45 @@ true;
32796
33790
  return () => {
32797
33791
  cancelled = true;
32798
33792
  };
32799
- }, [resource, env, hasResource]);
33793
+ }, [pcrl, env]);
32800
33794
  return /*#__PURE__*/React.createElement("div", {
32801
- className: classnames("staff-only", styles$4["debug-pco-resource"])
33795
+ className: classnames("staff-only", styles$5["debug-pco-resource"])
32802
33796
  }, /*#__PURE__*/React.createElement("div", {
32803
- className: styles$4["debug-pco-resource-header"]
33797
+ className: styles$5["debug-pco-resource-header"]
32804
33798
  }, /*#__PURE__*/React.createElement("span", {
32805
- className: styles$4["debug-pco-resource-badge"]
33799
+ className: styles$5["debug-pco-resource-badge"]
32806
33800
  }, "Staff"), /*#__PURE__*/React.createElement("span", {
32807
- className: styles$4["debug-pco-resource-title"]
33801
+ className: styles$5["debug-pco-resource-title"]
32808
33802
  }, "Recently Visited debug")), /*#__PURE__*/React.createElement(Field, {
32809
33803
  label: "pco:resource",
32810
33804
  value: describeTag("resource", resource)
32811
33805
  }), /*#__PURE__*/React.createElement(Field, {
32812
33806
  label: "pco:recently_visitable",
32813
33807
  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"};
33808
+ }), hasInvalidResource && (/*#__PURE__*/React.createElement("p", {
33809
+ className: styles$5["debug-pco-resource-note"]
33810
+ }, "pco:resource isn't a PCRL (product/v2/type/id), so Recently Visited ignores it.")), visit === null && (/*#__PURE__*/React.createElement("p", {
33811
+ className: styles$5["debug-pco-resource-note"]
33812
+ }, "This page is not eligible for Recently Visited")), pcrl && hasRecentlyVisitable && (/*#__PURE__*/React.createElement("p", {
33813
+ className: styles$5["debug-pco-resource-note"]
33814
+ }, "Both tags are present \u2014 pco:resource takes precedence for the Recently Visited entry.")), pcrl && lookup.status === "loading" && (/*#__PURE__*/React.createElement("p", {
33815
+ className: styles$5["debug-pco-resource-note"]
33816
+ }, "Looking up resource\u2026")), pcrl && lookup.status === "error" && (/*#__PURE__*/React.createElement("p", {
33817
+ className: styles$5["debug-pco-resource-note"]
33818
+ }, lookup.message)), pcrl && lookup.status === "loaded" && (/*#__PURE__*/React.createElement("pre", {
33819
+ className: styles$5["debug-pco-resource-json"]
33820
+ }, JSON.stringify(lookup.json, null, 2))), (visit === null || visit === void 0 ? void 0 : visit.type) === "page" && (/*#__PURE__*/React.createElement("p", {
33821
+ className: styles$5["debug-pco-resource-note"]
33822
+ }, "Recently Visited title: ", visit.title)), /*#__PURE__*/React.createElement("span", {
33823
+ className: styles$5["debug-pco-resource-field-label"]
33824
+ }, "Recently Visited"), personId ? (/*#__PURE__*/React.createElement("pre", {
33825
+ className: styles$5["debug-pco-resource-json"]
33826
+ }, JSON.stringify(recentlyVisited, null, 2))) : (/*#__PURE__*/React.createElement("p", {
33827
+ className: styles$5["debug-pco-resource-note"]
33828
+ }, "No current person, so nothing is recorded.")));
33829
+ }
33830
+
33831
+ var styles$4 = {"drawer-wrap":"drawer-wrap-J0rSL","is-visible":"is-visible-KTME8","drawer":"drawer-j7f4l"};
32830
33832
 
32831
33833
  function Drawer({
32832
33834
  children,
@@ -32834,7 +33836,7 @@ true;
32834
33836
  }) {
32835
33837
  return /*#__PURE__*/React.createElement("div", {
32836
33838
  id: id,
32837
- className: styles$3["drawer"]
33839
+ className: styles$4["drawer"]
32838
33840
  }, children);
32839
33841
  }
32840
33842
  function DrawerWrap({
@@ -32844,7 +33846,7 @@ true;
32844
33846
  ...props
32845
33847
  }) {
32846
33848
  return /*#__PURE__*/React.createElement("div", {
32847
- className: classnames(styles$3["drawer-wrap"], className, isVisible && styles$3["is-visible"]),
33849
+ className: classnames(styles$4["drawer-wrap"], className, isVisible && styles$4["is-visible"]),
32848
33850
  ...props
32849
33851
  }, children);
32850
33852
  }
@@ -33028,15 +34030,18 @@ true;
33028
34030
  }));
33029
34031
  };
33030
34032
 
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"};
34033
+ 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
34034
 
33033
34035
  const COLOR_MODES = [{
34036
+ icon: "toolbar.sun",
33034
34037
  label: "Light",
33035
34038
  value: "light"
33036
34039
  }, {
34040
+ icon: "toolbar.moon",
33037
34041
  label: "Dark",
33038
34042
  value: "dark"
33039
34043
  }, {
34044
+ icon: "toolbar.display",
33040
34045
  label: "System",
33041
34046
  value: "system"
33042
34047
  }];
@@ -33048,14 +34053,15 @@ true;
33048
34053
  const updateStore = useUpdateStore();
33049
34054
  const [counter] = React.useState(() => COLOR_MODE_INSTANCE_COUNTER++);
33050
34055
  return /*#__PURE__*/React.createElement("fieldset", {
33051
- className: styles$2["color-mode-switcher"]
34056
+ className: styles$3["color-mode-switcher"]
33052
34057
  }, /*#__PURE__*/React.createElement("legend", {
33053
- className: styles$2["color-mode-switcher-label"]
34058
+ className: styles$3["color-mode-switcher-label"]
33054
34059
  }, "Appearance", " "), /*#__PURE__*/React.createElement("div", {
33055
- className: styles$2["color-mode-switcher-modes"]
34060
+ className: styles$3["color-mode-switcher-modes"]
33056
34061
  }, COLOR_MODES.map(({
33057
- value,
33058
- label
34062
+ icon,
34063
+ label,
34064
+ value
33059
34065
  }) => (/*#__PURE__*/React.createElement("label", {
33060
34066
  key: value
33061
34067
  }, /*#__PURE__*/React.createElement("input", {
@@ -33072,8 +34078,9 @@ true;
33072
34078
  document.documentElement.setAttribute(RESOLVED_COLOR_MODE_ATTRIBUTE, resolveColorMode(colorMode));
33073
34079
  updateColorMode(colorMode);
33074
34080
  }
33075
- }), /*#__PURE__*/React.createElement(ColorModeIcon, {
33076
- mode: value
34081
+ }), /*#__PURE__*/React.createElement(Icon, {
34082
+ name: icon,
34083
+ size: 15
33077
34084
  }), label)))));
33078
34085
  }
33079
34086
  function updateColorMode(mode) {
@@ -33088,28 +34095,6 @@ true;
33088
34095
  method: "POST"
33089
34096
  });
33090
34097
  }
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
34098
 
33114
34099
  const requestClearConnectedPeopleCache = () => remove();
33115
34100
  function UserProfile({
@@ -33172,10 +34157,10 @@ true;
33172
34157
  alignment: "center",
33173
34158
  spacing: 0.5,
33174
34159
  width: "100%"
33175
- }, /*#__PURE__*/React.createElement(tapestryReact.Icon, {
34160
+ }, /*#__PURE__*/React.createElement(Icon, {
33176
34161
  name: mfaConfigured ? "general.lock" : "general.unlock",
33177
- size: "sm",
33178
- color: mfaConfigured ? "var(--t-icon-color-status-success)" : "var(--t-icon-color-status-error)"
34162
+ size: 12,
34163
+ theme: mfaConfigured ? "--t-icon-color-status-success" : "--t-icon-color-status-error"
33179
34164
  }), /*#__PURE__*/React.createElement(tapestryReact.Text, {
33180
34165
  size: 4,
33181
34166
  weight: 400,
@@ -33223,9 +34208,10 @@ true;
33223
34208
  }, /*#__PURE__*/React.createElement(tapestryReact.Text, {
33224
34209
  color: "var(--text-color-default-secondary)",
33225
34210
  size: 3
33226
- }, "Log out"), /*#__PURE__*/React.createElement(tapestryReact.Icon, {
34211
+ }, "Log out"), /*#__PURE__*/React.createElement(Icon, {
33227
34212
  name: "general.intoBox",
33228
- fill: "var(--t-icon-color-secondary)"
34213
+ size: 14,
34214
+ theme: "--t-icon-color-secondary"
33229
34215
  })))), env !== "production" && /*#__PURE__*/React.createElement(DebugPcoResource, {
33230
34216
  env: env
33231
34217
  }), hasConnectedPeople && (/*#__PURE__*/React.createElement(tapestryReact.StackView, {
@@ -33287,26 +34273,42 @@ true;
33287
34273
 
33288
34274
  var ActionType;
33289
34275
  (function (ActionType) {
34276
+ ActionType["clearPending"] = "clearPending";
33290
34277
  ActionType["close"] = "close";
33291
34278
  ActionType["mount"] = "mount";
33292
34279
  ActionType["open"] = "open";
34280
+ ActionType["setPending"] = "setPending";
33293
34281
  ActionType["unmount"] = "unmount";
33294
34282
  })(ActionType || (ActionType = {}));
33295
34283
  const initialState = {
33296
34284
  currentContent: null,
33297
- open: false
34285
+ open: false,
34286
+ pending: null
33298
34287
  };
33299
34288
  const reducer = (state, action) => {
34289
+ var _a, _b;
33300
34290
  switch (action.type) {
33301
34291
  case ActionType.mount:
33302
34292
  return {
33303
34293
  ...state,
33304
- currentContent: action.payload.content
34294
+ currentContent: action.payload.content,
34295
+ 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
34296
  };
33306
34297
  case ActionType.unmount:
33307
34298
  return {
33308
34299
  ...state,
33309
- currentContent: null
34300
+ currentContent: null,
34301
+ pending: null
34302
+ };
34303
+ case ActionType.setPending:
34304
+ return {
34305
+ ...state,
34306
+ pending: action.payload
34307
+ };
34308
+ case ActionType.clearPending:
34309
+ return {
34310
+ ...state,
34311
+ pending: null
33310
34312
  };
33311
34313
  case ActionType.open:
33312
34314
  return {
@@ -33325,13 +34327,181 @@ true;
33325
34327
  ...overrides
33326
34328
  });
33327
34329
 
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"};
34330
+ 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
34331
 
33330
- const pcoUrl = pcoUrl$1;
34332
+ // PCO Box serves TLS by default, and a bare http:// frame inside an https
34333
+ // product page is blocked as mixed content, so follow the page's scheme.
34334
+ const iframeSrc = () => `${window.location.protocol}//pco.test/iframe`;
34335
+ const STAFF_PINK = "var(--t-fill-color-product-staff)";
34336
+ // The pco.test domain is only ever served by a local PCO Box, so the tool
34337
+ // stays hidden in staging and production — and on Topbar's own dev server,
34338
+ // which runs on localhost and has no pco.test to frame.
34339
+ function isPcoTestHost() {
34340
+ const {
34341
+ hostname
34342
+ } = window.location;
34343
+ return hostname === "pco.test" || hostname.endsWith(".pco.test");
34344
+ }
34345
+ // Flattened from the source artwork's two outline paths — its gradients,
34346
+ // filters and clip paths carry nothing at icon size. Those paths are authored
34347
+ // on a 128px canvas, hence the transform: it fits the pair into the same ~24px
34348
+ // optical box the sibling toolbar icons occupy. Filled explicitly rather than
34349
+ // with `currentColor` so the mark stays staff pink instead of inheriting the
34350
+ // toolbar's text color and its hover/active states.
34351
+ 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";
34352
+ 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";
34353
+ function ToolsIcon() {
34354
+ return /*#__PURE__*/React.createElement("svg", {
34355
+ "aria-hidden": "true",
34356
+ width: "32",
34357
+ height: "32",
34358
+ viewBox: "0 0 32 32",
34359
+ xmlns: "http://www.w3.org/2000/svg"
34360
+ }, /*#__PURE__*/React.createElement("g", {
34361
+ fill: STAFF_PINK,
34362
+ transform: "translate(2.92 2.66) scale(.204)"
34363
+ }, /*#__PURE__*/React.createElement("path", {
34364
+ d: WRENCH
34365
+ }), /*#__PURE__*/React.createElement("path", {
34366
+ d: SCREWDRIVER
34367
+ })));
34368
+ }
34369
+ // A cross-origin frame's own history object is unreachable (touching it throws
34370
+ // a SecurityError), but the frame's navigations do land in the top-level joint
34371
+ // session history, so the *parent's* back/forward traverse them while leaving
34372
+ // the host page in place. Counting frame loads tracks how deep we are, which is
34373
+ // what keeps a click on Back from stepping past the frame's first page and
34374
+ // navigating the host app instead. Loads we caused are attributed to the
34375
+ // traversal that requested them rather than counted as new navigations.
34376
+ function useFrameHistory() {
34377
+ const [steps, setSteps] = React.useState({
34378
+ back: 0,
34379
+ forward: 0
34380
+ });
34381
+ // Traversal has to read the current counts outside of render, so the ref is
34382
+ // the source of truth and state exists to paint the buttons.
34383
+ const stepsRef = React.useRef(steps);
34384
+ const pendingTraversal = React.useRef(null);
34385
+ const frameIsLoaded = React.useRef(false);
34386
+ const moveTo = React.useCallback(next => {
34387
+ stepsRef.current = next;
34388
+ setSteps(next);
34389
+ }, []);
34390
+ const onFrameLoad = React.useCallback(() => {
34391
+ if (!frameIsLoaded.current) {
34392
+ // The frame's first page is the floor, not a step.
34393
+ frameIsLoaded.current = true;
34394
+ } else {
34395
+ const traversal = pendingTraversal.current;
34396
+ pendingTraversal.current = null;
34397
+ const {
34398
+ back,
34399
+ forward
34400
+ } = stepsRef.current;
34401
+ if (traversal === "back") {
34402
+ moveTo({
34403
+ back: back - 1,
34404
+ forward: forward + 1
34405
+ });
34406
+ } else if (traversal === "forward") {
34407
+ moveTo({
34408
+ back: back + 1,
34409
+ forward: forward - 1
34410
+ });
34411
+ } else {
34412
+ moveTo({
34413
+ back: back + 1,
34414
+ forward: 0
34415
+ });
34416
+ }
34417
+ }
34418
+ }, [moveTo]);
34419
+ // Guarded here rather than trusting the button's disabled state: traversing
34420
+ // with no frame step left would navigate the host app out from under us.
34421
+ const traverse = React.useCallback(direction => {
34422
+ if (stepsRef.current[direction] > 0) {
34423
+ pendingTraversal.current = direction;
34424
+ window.history[direction]();
34425
+ }
34426
+ }, []);
34427
+ return {
34428
+ onFrameLoad,
34429
+ steps,
34430
+ traverse
34431
+ };
34432
+ }
34433
+ function NavButton({
34434
+ direction,
34435
+ disabled,
34436
+ onClick
34437
+ }) {
34438
+ return /*#__PURE__*/React.createElement(tapestryReact.Button
34439
+ // Tapestry's `disabled` suppresses the click but emits no ARIA state.
34440
+ , {
34441
+ "aria-disabled": disabled,
34442
+ disabled: disabled,
34443
+ hover: {
34444
+ backgroundColor: "var(--t-fill-color-neutral-070)"
34445
+ },
34446
+ icon: {
34447
+ fill: "var(--t-icon-color-dim)",
34448
+ name: direction === "back" ? "general.leftChevron" : "general.rightChevron",
34449
+ size: "md"
34450
+ },
34451
+ onClick: onClick,
34452
+ opacity: disabled ? 0.35 : 1,
34453
+ theme: false,
34454
+ title: direction === "back" ? "Back" : "Forward"
34455
+ });
34456
+ }
34457
+ function DeveloperToolsComponent({
34458
+ closeDrawer
34459
+ }) {
34460
+ const {
34461
+ onFrameLoad,
34462
+ steps,
34463
+ traverse
34464
+ } = useFrameHistory();
34465
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(DrawerHeader, {
34466
+ closeDrawer: closeDrawer
34467
+ }, /*#__PURE__*/React.createElement(tapestryReact.StackView, {
34468
+ alignment: "center",
34469
+ axis: "horizontal",
34470
+ spacing: 0.25
34471
+ }, /*#__PURE__*/React.createElement(NavButton, {
34472
+ direction: "back",
34473
+ disabled: steps.back === 0,
34474
+ onClick: () => traverse("back")
34475
+ }), /*#__PURE__*/React.createElement(NavButton, {
34476
+ direction: "forward",
34477
+ disabled: steps.forward === 0,
34478
+ onClick: () => traverse("forward")
34479
+ }), /*#__PURE__*/React.createElement(DrawerHeading, {
34480
+ paddingLeft: 0.5
34481
+ }, "Developer Tools"))), /*#__PURE__*/React.createElement(tapestryReact.Box, {
34482
+ as: "iframe",
34483
+ border: "none",
34484
+ flex: "1",
34485
+ minHeight: 0,
34486
+ onLoad: onFrameLoad,
34487
+ src: iframeSrc(),
34488
+ title: "Developer Tools",
34489
+ width: "100%"
34490
+ }));
34491
+ }
34492
+ const DeveloperTools = {
34493
+ component: DeveloperToolsComponent,
34494
+ icons: {
34495
+ component: ToolsIcon
34496
+ },
34497
+ label: "Developer Tools",
34498
+ // No space: `name` becomes an element id and a pendo attribute.
34499
+ name: "DeveloperTools"
34500
+ };
33331
34501
 
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"};
34502
+ 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
34503
 
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";
34504
+ 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
34505
  // Same stops as the AskAIBanner border gradient, so the two stay in sync.
33336
34506
  const GRADIENT_STOPS = [{
33337
34507
  color: "var(--t-fill-color-tag-teal-040)",
@@ -33380,7 +34550,7 @@ true;
33380
34550
  stopColor: color
33381
34551
  }
33382
34552
  })))))), /*#__PURE__*/React.createElement("path", {
33383
- d: SPARKLE_PATH,
34553
+ d: SPARKLE_PATH$1,
33384
34554
  fill: isGradient ? `url(#${gradientId})` : "currentColor"
33385
34555
  }));
33386
34556
  }
@@ -33389,8 +34559,8 @@ true;
33389
34559
  var _a, _b;
33390
34560
  const isStacked = props.variant !== "inline";
33391
34561
  return /*#__PURE__*/React.createElement("button", {
33392
- className: classnames(styles.banner, {
33393
- [styles.stacked]: isStacked
34562
+ className: classnames(styles$1.banner, {
34563
+ [styles$1.stacked]: isStacked
33394
34564
  }),
33395
34565
  onClick: () => props.onClick(),
33396
34566
  type: "button"
@@ -33405,15 +34575,15 @@ true;
33405
34575
  title
33406
34576
  }) {
33407
34577
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
33408
- className: styles.content
34578
+ className: styles$1.content
33409
34579
  }, /*#__PURE__*/React.createElement(AskAIIcon, null), /*#__PURE__*/React.createElement("span", {
33410
- className: styles.text
34580
+ className: styles$1.text
33411
34581
  }, /*#__PURE__*/React.createElement("span", {
33412
- className: styles.title
34582
+ className: styles$1.title
33413
34583
  }, title), /*#__PURE__*/React.createElement("span", {
33414
- className: styles.subtitle
34584
+ className: styles$1.subtitle
33415
34585
  }, "Support answers tailored to you"))), /*#__PURE__*/React.createElement(tapestryReact.Icon, {
33416
- className: styles.chevron,
34586
+ className: styles$1.chevron,
33417
34587
  name: "general.rightChevron",
33418
34588
  size: "16px"
33419
34589
  }));
@@ -33423,18 +34593,18 @@ true;
33423
34593
  title
33424
34594
  }) {
33425
34595
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
33426
- className: styles.inlineContent
34596
+ className: styles$1.inlineContent
33427
34597
  }, /*#__PURE__*/React.createElement(SparkleIcon, {
33428
- className: styles.sparkle,
34598
+ className: styles$1.sparkle,
33429
34599
  fill: "currentColor"
33430
34600
  }), /*#__PURE__*/React.createElement("span", {
33431
- className: styles.inlineTitle
34601
+ className: styles$1.inlineTitle
33432
34602
  }, title)), /*#__PURE__*/React.createElement("span", {
33433
- className: styles.action
34603
+ className: styles$1.action
33434
34604
  }, /*#__PURE__*/React.createElement("span", {
33435
- className: styles.actionLabel
34605
+ className: styles$1.actionLabel
33436
34606
  }, action), /*#__PURE__*/React.createElement(tapestryReact.Icon, {
33437
- className: styles.actionChevron,
34607
+ className: styles$1.actionChevron,
33438
34608
  name: "general.rightChevron",
33439
34609
  size: "14px"
33440
34610
  })));
@@ -33451,9 +34621,9 @@ true;
33451
34621
  const borderA = `${uid}-border-a`;
33452
34622
  const borderB = `${uid}-border-b`;
33453
34623
  return /*#__PURE__*/React.createElement("span", {
33454
- className: styles.icon
34624
+ className: styles$1.icon
33455
34625
  }, /*#__PURE__*/React.createElement("svg", {
33456
- className: styles.iconShape,
34626
+ className: styles$1.iconShape,
33457
34627
  viewBox: "0 0 32 32",
33458
34628
  fill: "none",
33459
34629
  "aria-hidden": "true"
@@ -33624,7 +34794,7 @@ true;
33624
34794
  stopColor: "#D48B91",
33625
34795
  stopOpacity: "0"
33626
34796
  })))), /*#__PURE__*/React.createElement("svg", {
33627
- className: styles.iconSparkle,
34797
+ className: styles$1.iconSparkle,
33628
34798
  viewBox: "0 0 19.4995 17.9999",
33629
34799
  "aria-hidden": "true"
33630
34800
  }, /*#__PURE__*/React.createElement("path", {
@@ -33633,6 +34803,23 @@ true;
33633
34803
  })));
33634
34804
  }
33635
34805
 
34806
+ var styles = {"button":"button-2NVI1","sparkle":"sparkle-ruJGP","label":"label-4NUGB"};
34807
+
34808
+ function AskAIButton({
34809
+ onClick,
34810
+ text = "Ask AI"
34811
+ }) {
34812
+ return /*#__PURE__*/React.createElement("button", {
34813
+ className: styles.button,
34814
+ onClick: () => onClick(),
34815
+ type: "button"
34816
+ }, /*#__PURE__*/React.createElement(SparkleIcon, {
34817
+ className: styles.sparkle
34818
+ }), /*#__PURE__*/React.createElement("span", {
34819
+ className: styles.label
34820
+ }, text));
34821
+ }
34822
+
33636
34823
  // The article heading lives in the deepest populated hierarchy level (lvl0 is
33637
34824
  // always "Planning Center").
33638
34825
  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 +34975,24 @@ true;
33788
34975
  // not the path, so updating it is a same-document navigation that doesn't
33789
34976
  // reload the iframe, but also lets the route survive a forced reload (e.g.
33790
34977
  // Turbo re-parenting the topbar).
33791
- function buildPicobotSrc(origin, path) {
34978
+ //
34979
+ // `panel` loads picobot as its own toolbar panel rather than inside Helpdesk.
34980
+ function buildPicobotSrc(origin, path, {
34981
+ panel = false
34982
+ } = {}) {
33792
34983
  const [rawPath, queryPart] = path.replace(/^\/+/, "").split("?", 2);
33793
34984
  const safePath = rawPath.split("/").filter(segment => segment && segment !== "." && segment !== "..").join("/");
33794
34985
  const fragment = queryPart !== undefined ? `${safePath}?${queryPart}` : safePath;
33795
- const base = new URL("topbar", origin).toString();
34986
+ const url = new URL("topbar", origin);
34987
+ if (panel) url.searchParams.set("panel", "1");
34988
+ const base = url.toString();
33796
34989
  return fragment ? `${base}#${fragment}` : base;
33797
34990
  }
33798
34991
 
33799
34992
  const LOAD_TIMEOUT_MS = 3000;
33800
- function createPicobotIframe(picobotOrigin) {
34993
+ function createPicobotIframe(picobotOrigin, {
34994
+ panel = false
34995
+ } = {}) {
33801
34996
  return /*#__PURE__*/React.forwardRef(function PicobotIframe({
33802
34997
  currentPath: iframePath,
33803
34998
  onChangePath: onIframeChangePath,
@@ -33853,7 +35048,9 @@ true;
33853
35048
  (_b = (_a = iframeRef.current) === null || _a === void 0 ? void 0 : _a.contentWindow) === null || _b === void 0 ? void 0 : _b.postMessage(message, picobotOrigin);
33854
35049
  }
33855
35050
  }), []);
33856
- const iframeSrc = buildPicobotSrc(picobotOrigin, loadedPath);
35051
+ const iframeSrc = buildPicobotSrc(picobotOrigin, loadedPath, {
35052
+ panel
35053
+ });
33857
35054
  return /*#__PURE__*/React.createElement("iframe", {
33858
35055
  ref: iframeRef,
33859
35056
  src: iframeSrc,
@@ -33892,22 +35089,28 @@ true;
33892
35089
  currentPath,
33893
35090
  env,
33894
35091
  helpdeskConfig,
33895
- onChangePath
35092
+ isPicobotLoading,
35093
+ onChangePath,
35094
+ openPicobot
33896
35095
  }) {
33897
35096
  const PicobotIframe = React.useMemo(() => createPicobotIframe(pcoUrl(env)("picobot")), [env]);
35097
+ const loadingFallback = /*#__PURE__*/React.createElement(StackView$1, {
35098
+ flex: 1,
35099
+ axis: "vertical"
35100
+ }, /*#__PURE__*/React.createElement(DrawerHeader, {
35101
+ closeDrawer: closeDrawer
35102
+ }, /*#__PURE__*/React.createElement(StackView$1, {
35103
+ axis: "horizontal",
35104
+ spacing: 1
35105
+ }, /*#__PURE__*/React.createElement(DrawerHeading, null, "Help and support"), /*#__PURE__*/React.createElement(LoadingIndicator$1, null))));
35106
+ // The add-on reads its args once, so mounting before the gate answers
35107
+ // would leave out openPicobot for good.
35108
+ if (isPicobotLoading) return loadingFallback;
33898
35109
  return /*#__PURE__*/React.createElement(addOns.UiExtensionFirstPartyComponent, {
33899
35110
  grow: 1,
33900
35111
  display: "flex",
33901
35112
  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)))),
35113
+ loadingFallback: loadingFallback,
33911
35114
  additionalComponents: {
33912
35115
  AskAIBanner: {
33913
35116
  component: AskAIBanner,
@@ -33927,6 +35130,18 @@ true;
33927
35130
  }
33928
35131
  }
33929
35132
  },
35133
+ AskAIButton: {
35134
+ component: AskAIButton,
35135
+ properties: {
35136
+ onClick: {
35137
+ event: false,
35138
+ type: Function
35139
+ },
35140
+ text: {
35141
+ type: String
35142
+ }
35143
+ }
35144
+ },
33930
35145
  CalendlyWidget: {
33931
35146
  component: CalendlyWidget,
33932
35147
  properties: {
@@ -34005,7 +35220,11 @@ true;
34005
35220
  currentPageParams: location.search,
34006
35221
  currentPath,
34007
35222
  helpdeskConfig,
34008
- onChangePath
35223
+ onChangePath,
35224
+ // Its presence tells helpdesk that Picobot has its own panel.
35225
+ ...(openPicobot && {
35226
+ openPicobot
35227
+ })
34009
35228
  }
34010
35229
  });
34011
35230
  }
@@ -34552,6 +35771,142 @@ true;
34552
35771
  name: "Notifications"
34553
35772
  };
34554
35773
 
35774
+ 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";
35775
+ // Helpdesk reads this context off the email path to open its email form the
35776
+ // same way a handoff inside its own drawer does.
35777
+ function emailPathWithContext(context = {}) {
35778
+ const params = new URLSearchParams();
35779
+ for (const key of ["conversationId", "source", "turnIndex"]) {
35780
+ const value = context[key];
35781
+ if (value !== null && value !== undefined) params.set(key, String(value));
35782
+ }
35783
+ return `email?${params}`;
35784
+ }
35785
+ function PicobotComponent({
35786
+ clearPending,
35787
+ closeDrawer,
35788
+ currentPath,
35789
+ env,
35790
+ onChangePath,
35791
+ openTool,
35792
+ pending
35793
+ }) {
35794
+ const PicobotIframe = React.useMemo(() => createPicobotIframe(pcoUrl(env)("picobot"), {
35795
+ panel: true
35796
+ }), [env]);
35797
+ const picobotRef = React.useRef(null);
35798
+ const [isPicobotReady, setIsPicobotReady] = React.useState(false);
35799
+ // Picobot has no listener until it sends READY, so openPicobot's draft or
35800
+ // conversation waits for it.
35801
+ React.useEffect(function handOverPendingOncePicobotIsReady() {
35802
+ var _a, _b;
35803
+ if (!pending || !isPicobotReady) return;
35804
+ const {
35805
+ conversationId,
35806
+ draft,
35807
+ ticketExternalId
35808
+ } = pending;
35809
+ if (conversationId) {
35810
+ (_a = picobotRef.current) === null || _a === void 0 ? void 0 : _a.postMessage({
35811
+ payload: {
35812
+ conversation_id: conversationId,
35813
+ // Picobot asks whether this resolved the ticket, and closes it.
35814
+ ...(ticketExternalId && {
35815
+ external_id: ticketExternalId,
35816
+ source: "ticket_submitted"
35817
+ })
35818
+ },
35819
+ type: "LOAD_CONVERSATION"
35820
+ });
35821
+ } else if (draft) {
35822
+ // Not both: Picobot cancels a conversation load when a prefill
35823
+ // starts a new conversation.
35824
+ (_b = picobotRef.current) === null || _b === void 0 ? void 0 : _b.postMessage({
35825
+ payload: {
35826
+ source: "email_form",
35827
+ text: draft
35828
+ },
35829
+ type: "PREFILL_COMPOSER"
35830
+ });
35831
+ }
35832
+ clearPending();
35833
+ }, [pending, isPicobotReady, clearPending]);
35834
+ const [iframeErrored, setIframeErrored] = React.useState(false);
35835
+ const [mountKey, setMountKey] = React.useState(0);
35836
+ const handleRetry = React.useCallback(() => {
35837
+ setIframeErrored(false);
35838
+ setIsPicobotReady(false);
35839
+ setMountKey(key => key + 1);
35840
+ }, []);
35841
+ const handleMessage = React.useCallback(data => {
35842
+ var _a;
35843
+ const message = data;
35844
+ switch (message === null || message === void 0 ? void 0 : message.type) {
35845
+ case "CLOSE_DRAWER":
35846
+ return closeDrawer();
35847
+ case "READY":
35848
+ return setIsPicobotReady(true);
35849
+ case "PICOBOT_SUPPORT_HANDOFF":
35850
+ return openTool("helpdesk", ((_a = message.payload) === null || _a === void 0 ? void 0 : _a.method) === "zoom" ? "phone" : emailPathWithContext(message.payload));
35851
+ case "IFRAME_LOAD_ERROR":
35852
+ console.warn("topbar: Picobot iframe failed to load");
35853
+ return setIframeErrored(true);
35854
+ }
35855
+ }, [closeDrawer, openTool]);
35856
+ if (iframeErrored) {
35857
+ return /*#__PURE__*/React.createElement(LoadError, {
35858
+ onContactSupport: () => openTool("helpdesk", "email"),
35859
+ onRetry: handleRetry
35860
+ });
35861
+ }
35862
+ return /*#__PURE__*/React.createElement(PicobotIframe, {
35863
+ key: mountKey,
35864
+ ref: picobotRef,
35865
+ currentPath: currentPath,
35866
+ onChangePath: onChangePath,
35867
+ onMessage: handleMessage
35868
+ });
35869
+ }
35870
+ function LoadError({
35871
+ onContactSupport,
35872
+ onRetry
35873
+ }) {
35874
+ return /*#__PURE__*/React.createElement(tapestryReact.StackView, {
35875
+ axis: "vertical",
35876
+ flex: 1,
35877
+ alignment: "center",
35878
+ distribution: "center",
35879
+ padding: 3,
35880
+ spacing: 1
35881
+ }, /*#__PURE__*/React.createElement(tapestryReact.Heading, {
35882
+ level: 2
35883
+ }, "Picobot is temporarily unavailable"), /*#__PURE__*/React.createElement(tapestryReact.Text, {
35884
+ size: 5,
35885
+ color: "var(--t-text-color-secondary)"
35886
+ }, "Try again or contact support."), /*#__PURE__*/React.createElement(tapestryReact.Box, {
35887
+ paddingTop: 2
35888
+ }, /*#__PURE__*/React.createElement(tapestryReact.Button, {
35889
+ onClick: onRetry,
35890
+ iconLeft: "general.refresh",
35891
+ theme: "primary",
35892
+ variant: "fill",
35893
+ title: "Retry"
35894
+ })), /*#__PURE__*/React.createElement(tapestryReact.Button, {
35895
+ onClick: onContactSupport,
35896
+ variant: "naked",
35897
+ theme: "primary",
35898
+ title: "Contact support"
35899
+ }));
35900
+ }
35901
+ const Picobot = {
35902
+ component: PicobotComponent,
35903
+ icons: {
35904
+ staticDotPath: SPARKLE_PATH,
35905
+ staticPath: SPARKLE_PATH
35906
+ },
35907
+ name: "Picobot"
35908
+ };
35909
+
34555
35910
  const {
34556
35911
  LoadingIndicator,
34557
35912
  StackView
@@ -34947,6 +36302,41 @@ true;
34947
36302
  };
34948
36303
  };
34949
36304
 
36305
+ // Picobot's API rejects requests without a version header, and apiRequest
36306
+ // sets its own headers, so the version rides in on the fetch.
36307
+ const picobotFetch = (url, opts) => defaultFetch(url, {
36308
+ ...opts,
36309
+ headers: {
36310
+ ...(opts === null || opts === void 0 ? void 0 : opts.headers),
36311
+ "X-PCO-API-Version": "2026-08-11"
36312
+ }
36313
+ });
36314
+ // Routes wait on the gate, so a slow Picobot can't hold them up for long.
36315
+ const GATE_TIMEOUT_MS = 3000;
36316
+ const usePicobotConfig = ({
36317
+ isEnabled
36318
+ }) => {
36319
+ // Undefined until Picobot answers.
36320
+ const [panelEnabled, setPanelEnabled] = React.useState();
36321
+ React.useEffect(() => {
36322
+ if (!isEnabled) return undefined;
36323
+ // Whatever settles first wins, so a late answer can't reopen the gate.
36324
+ const settle = enabled => setPanelEnabled(current => current !== null && current !== void 0 ? current : enabled);
36325
+ const timeout = setTimeout(() => settle(false), GATE_TIMEOUT_MS);
36326
+ apiRequest(picobotFetch, `${window.location.origin}/~api/picobot/v2/me`).then(({
36327
+ json
36328
+ }) => {
36329
+ var _a, _b;
36330
+ 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);
36331
+ }).catch(() => settle(false)).finally(() => clearTimeout(timeout));
36332
+ return () => clearTimeout(timeout);
36333
+ }, [isEnabled]);
36334
+ return {
36335
+ displayPicobotIcon: panelEnabled === true,
36336
+ isLoading: isEnabled && panelEnabled === undefined
36337
+ };
36338
+ };
36339
+
34950
36340
  const Profile = {
34951
36341
  component: UserProfile,
34952
36342
  icons: {
@@ -34957,10 +36347,16 @@ true;
34957
36347
  };
34958
36348
 
34959
36349
  const DURATION = 100;
36350
+ // Exempt paths match regardless of query, so `email?conversationId=…` is
36351
+ // exempt along with `email`.
34960
36352
  function isToolPathExemptFromStorage(toolName, path) {
34961
36353
  var _a, _b;
34962
36354
  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;
36355
+ const [pathWithoutQuery] = path.split("?");
36356
+ 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;
36357
+ }
36358
+ function isConversationPath(path) {
36359
+ return Boolean(path === null || path === void 0 ? void 0 : path.startsWith("conversations/"));
34964
36360
  }
34965
36361
  function scrollToolDrawerToTop(toolName) {
34966
36362
  var _a;
@@ -34973,6 +36369,7 @@ true;
34973
36369
  Chat,
34974
36370
  Helpdesk,
34975
36371
  Notifications,
36372
+ Picobot,
34976
36373
  Profile,
34977
36374
  Tasks
34978
36375
  };
@@ -35000,7 +36397,8 @@ true;
35000
36397
  });
35001
36398
  const {
35002
36399
  currentContent,
35003
- open
36400
+ open,
36401
+ pending
35004
36402
  } = state;
35005
36403
  const mounted = !!currentContent;
35006
36404
  const storeData = useStore();
@@ -35013,6 +36411,15 @@ true;
35013
36411
  ...(env === "development" ? _storeDataOverride !== null && _storeDataOverride !== void 0 ? _storeDataOverride : {} : {})
35014
36412
  };
35015
36413
  const updateStore = useUpdateStore();
36414
+ const {
36415
+ displayPicobotIcon,
36416
+ isLoading: isPicobotGateLoading
36417
+ } = usePicobotConfig({
36418
+ isEnabled: Boolean(userId)
36419
+ });
36420
+ // Picobot is a kill-switched tool: until its gate says the panel is
36421
+ // enabled, its routes are held, and if it never does, they're ignored.
36422
+ const routableTool = React.useCallback(name => name === Picobot.name.toLowerCase() && !displayPicobotIcon ? undefined : toolsRouteMap[name], [displayPicobotIcon]);
35016
36423
  React.useEffect(() => {
35017
36424
  if (open) {
35018
36425
  document.body.style.overflow = "hidden";
@@ -35100,7 +36507,9 @@ true;
35100
36507
  });
35101
36508
  }, [clearRoute]);
35102
36509
  React.useEffect(() => {
35103
- if (!toolbarFragmentRoute) return;
36510
+ if (!toolbarFragmentRoute || !routableTool(toolbarFragmentRoute.tool)) {
36511
+ return;
36512
+ }
35104
36513
  const isExempt = isToolPathExemptFromStorage(toolbarFragmentRoute.tool, toolbarFragmentRoute.path);
35105
36514
  if (isExempt) return;
35106
36515
  const nextState = {
@@ -35110,12 +36519,32 @@ true;
35110
36519
  if (!shallowCompare(nextState, toolStateCookie)) {
35111
36520
  setToolStateCookie(nextState);
35112
36521
  }
35113
- }, [toolbarFragmentRoute, toolStateCookie]);
36522
+ }, [toolbarFragmentRoute, toolStateCookie, routableTool]);
35114
36523
  React.useEffect(() => {
35115
- if ((toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && (currentContent !== toolsRouteMap[toolbarFragmentRoute.tool] || !open)) {
35116
- setToolAndOpenDrawer(toolsRouteMap[toolbarFragmentRoute.tool]);
36524
+ // Helpdesk conversations may belong to Picobot, so they wait for its gate
36525
+ // rather than flashing open in Helpdesk first.
36526
+ const mayRedirectToPicobot = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) === Helpdesk.name.toLowerCase() && isConversationPath(toolbarFragmentRoute.path) && (isPicobotGateLoading || displayPicobotIcon);
36527
+ const tool = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && !mayRedirectToPicobot && routableTool(toolbarFragmentRoute.tool);
36528
+ if (tool && (currentContent !== tool || !open)) {
36529
+ setToolAndOpenDrawer(tool);
35117
36530
  }
35118
- }, [setToolAndOpenDrawer, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool, currentContent, open]);
36531
+ }, [setToolAndOpenDrawer, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path, currentContent, open, routableTool, isPicobotGateLoading, displayPicobotIcon]);
36532
+ const openTool = React.useCallback((name, path, pendingValue) => {
36533
+ const tool = routableTool(name.toLowerCase());
36534
+ if (!tool) return;
36535
+ dispatch({
36536
+ payload: pendingValue === undefined ? null : {
36537
+ tool: tool.name,
36538
+ value: pendingValue
36539
+ },
36540
+ type: ActionType.setPending
36541
+ });
36542
+ onChangeToolbarFragmentRoute(tool.name.toLowerCase(), path);
36543
+ scrollToolDrawerToTop(tool.name);
36544
+ }, [onChangeToolbarFragmentRoute, routableTool]);
36545
+ const clearPending = React.useCallback(() => dispatch({
36546
+ type: ActionType.clearPending
36547
+ }), []);
35119
36548
  const toggleDrawer = content => {
35120
36549
  if (!open) {
35121
36550
  return openDrawer(content);
@@ -35132,12 +36561,52 @@ true;
35132
36561
  joltPromise: storeData === null || storeData === void 0 ? void 0 : storeData.joltPromise,
35133
36562
  userId
35134
36563
  });
35135
- const TOOLS = [Notifications, Tasks, displayChatIcon && Chat].filter(Boolean);
36564
+ // Picobot conversations moved from Helpdesk to the Picobot panel, so
36565
+ // send old links and saved paths there once the panel is enabled.
36566
+ React.useEffect(() => {
36567
+ if (!displayPicobotIcon) return;
36568
+ if ((toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) === Helpdesk.name.toLowerCase() && isConversationPath(toolbarFragmentRoute.path)) {
36569
+ onChangeToolbarFragmentRoute(Picobot.name.toLowerCase(), toolbarFragmentRoute.path);
36570
+ }
36571
+ }, [displayPicobotIcon, toolbarFragmentRoute, onChangeToolbarFragmentRoute]);
36572
+ React.useEffect(() => {
36573
+ if (!displayPicobotIcon) return;
36574
+ const {
36575
+ helpdesk,
36576
+ picobot
36577
+ } = toolStateCookie;
36578
+ if (!isConversationPath(helpdesk)) return;
36579
+ setToolStateCookie({
36580
+ ...toolStateCookie,
36581
+ helpdesk: "",
36582
+ picobot: picobot || helpdesk
36583
+ });
36584
+ }, [displayPicobotIcon, toolStateCookie]);
36585
+ const openPicobot = React.useCallback(options => {
36586
+ var _a;
36587
+ // Moving an open Picobot off its path reloads or navigates the iframe,
36588
+ // which would race the options it's being handed.
36589
+ const path = currentContent === Picobot ? (_a = toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path) !== null && _a !== void 0 ? _a : "" : "";
36590
+ openTool(Picobot.name, path, options);
36591
+ }, [openTool, currentContent, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path]);
36592
+ React.useEffect(() => {
36593
+ if (!displayPicobotIcon) return undefined;
36594
+ window.topbar = window.topbar || {};
36595
+ window.topbar.openPicobot = openPicobot;
36596
+ return () => {
36597
+ delete window.topbar.openPicobot;
36598
+ };
36599
+ }, [displayPicobotIcon, openPicobot]);
36600
+ const TOOLS = [Notifications, Tasks, displayChatIcon && Chat, displayPicobotIcon && Picobot, isPcoTestHost() && DeveloperTools].filter(Boolean);
35136
36601
  const handleToolOnClick = tool => {
35137
36602
  toggleDrawer(tool);
35138
36603
  scrollToolDrawerToTop(tool.name);
35139
36604
  };
35140
36605
  const toolSpecificProps = {
36606
+ Helpdesk: {
36607
+ isPicobotLoading: isPicobotGateLoading,
36608
+ openPicobot: displayPicobotIcon ? openPicobot : undefined
36609
+ },
35141
36610
  Profile: {
35142
36611
  appName,
35143
36612
  closeDrawer,
@@ -35160,6 +36629,7 @@ true;
35160
36629
  const routeIsForCurrentTool = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && toolsRouteMap[toolbarFragmentRoute.tool] === currentContent;
35161
36630
  const currentToolProps = {
35162
36631
  appName,
36632
+ clearPending,
35163
36633
  closeDrawer,
35164
36634
  currentPath: routeIsForCurrentTool ? toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path : null,
35165
36635
  env,
@@ -35168,6 +36638,8 @@ true;
35168
36638
  joltPromise,
35169
36639
  onChangePath,
35170
36640
  openDrawer: () => currentContent && openDrawer(currentContent),
36641
+ openTool,
36642
+ 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
36643
  userId,
35172
36644
  ...(currentContent ? toolSpecificProps[currentContent.name] : {})
35173
36645
  };
@@ -35178,38 +36650,37 @@ true;
35178
36650
  return /*#__PURE__*/React.createElement(tapestryReact.ThemeProvider, {
35179
36651
  theme: topbarTheme
35180
36652
  }, /*#__PURE__*/React.createElement(addOns.UiExtensionDataContext, null, /*#__PURE__*/React.createElement("div", {
35181
- className: styles$1["toolbar"],
36653
+ className: styles$2["toolbar"],
35182
36654
  "data-pendo": "toolbar-profile-support-wrapper"
35183
36655
  }, /*#__PURE__*/React.createElement(ToolbarButton, {
35184
36656
  "aria-label": `Open ${userName}’s profile drawer`,
35185
36657
  onClick: () => toggleDrawer(Profile),
35186
36658
  "data-pendo": "toolbar-profile",
35187
- className: styles$1["toolbar-button--avatar"]
36659
+ className: styles$2["toolbar-button--avatar"]
35188
36660
  }, /*#__PURE__*/React.createElement(Avatar, {
35189
36661
  env: env,
35190
36662
  url: userAvatarPath
35191
36663
  })), /*#__PURE__*/React.createElement(ToolbarButton, {
35192
- className: styles$1["toolbar-button--home"],
36664
+ className: styles$2["toolbar-button--home"],
35193
36665
  onClick: () => closeDrawer()
35194
36666
  }, /*#__PURE__*/React.createElement(tapestryReact.Icon, {
35195
36667
  name: "toolbar.planningCenter",
35196
36668
  size: "24px"
35197
36669
  })), [HelpDeskTool, ...TOOLS].map(tool => {
36670
+ var _a;
35198
36671
  const current = open && currentContent === tool;
35199
36672
  const indicator = indicatorState[tool.name];
35200
36673
  return /*#__PURE__*/React.createElement(ToolbarButton, {
35201
36674
  current: current,
35202
36675
  onClick: () => handleToolOnClick(tool),
35203
36676
  id: getDataPendoAttribute(tool.name),
35204
- "aria-label": `Open ${tool.name} drawer`,
36677
+ "aria-label": `Open ${(_a = tool.label) !== null && _a !== void 0 ? _a : tool.name} drawer`,
35205
36678
  "data-pendo": getDataPendoAttribute(tool.name),
35206
36679
  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
- })));
36680
+ }, /*#__PURE__*/React.createElement(ToolbarToolIcon, {
36681
+ icons: tool.icons,
36682
+ dot: !!indicator
36683
+ }));
35213
36684
  })), /*#__PURE__*/React.createElement(DrawerWrap, {
35214
36685
  isVisible: open
35215
36686
  }, mounted && (/*#__PURE__*/React.createElement(Drawer, {
@@ -35227,12 +36698,25 @@ true;
35227
36698
  }) => {
35228
36699
  return /*#__PURE__*/React.createElement("button", {
35229
36700
  type: "button",
35230
- className: classnames(styles$1["toolbar-button"], className, current && styles$1["is-current"]),
36701
+ className: classnames(styles$2["toolbar-button"], className, current && styles$2["is-current"]),
35231
36702
  onClick: onClick,
35232
36703
  "data-toolbar-link": "true",
35233
36704
  ...restProps
35234
36705
  }, children);
35235
36706
  };
36707
+ function ToolbarToolIcon({
36708
+ dot,
36709
+ icons
36710
+ }) {
36711
+ if ("component" in icons) {
36712
+ return /*#__PURE__*/React.createElement(icons.component, null);
36713
+ } else {
36714
+ return /*#__PURE__*/React.createElement(ToolbarButtonIcon, {
36715
+ path: dot ? icons.staticDotPath : icons.staticPath,
36716
+ dot: dot
36717
+ });
36718
+ }
36719
+ }
35236
36720
  function ToolbarButtonIcon({
35237
36721
  dot,
35238
36722
  path