@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.
package/dist/topbar.js CHANGED
@@ -7,11 +7,12 @@ var general = require('@planningcenter/icons/paths/general');
7
7
  var Jolt = require('@planningcenter/jolt-client');
8
8
  var shim = require('use-sync-external-store/shim');
9
9
  require('react/jsx-runtime');
10
+ var toolbar = require('@planningcenter/icons/paths/toolbar');
11
+ var logomarks = require('@planningcenter/icons/paths/logomark');
10
12
  var tapestryReact = require('@planningcenter/tapestry-react');
11
13
  var addOns = require('@planningcenter/add-ons');
12
14
  var churchCenter = require('@planningcenter/icons/paths/church-center');
13
15
  var people = require('@planningcenter/icons/paths/people');
14
- var toolbar = require('@planningcenter/icons/paths/toolbar');
15
16
  var reactCalendly = require('react-calendly');
16
17
  var algoliasearch = require('algoliasearch');
17
18
  var reactInstantsearchCore = require('react-instantsearch-core');
@@ -37,11 +38,12 @@ function _interopNamespaceDefault(e) {
37
38
 
38
39
  var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
39
40
  var general__namespace = /*#__PURE__*/_interopNamespaceDefault(general);
41
+ var toolbar__namespace = /*#__PURE__*/_interopNamespaceDefault(toolbar);
42
+ var logomarks__namespace = /*#__PURE__*/_interopNamespaceDefault(logomarks);
40
43
  var churchCenter__namespace = /*#__PURE__*/_interopNamespaceDefault(churchCenter);
41
44
  var people__namespace = /*#__PURE__*/_interopNamespaceDefault(people);
42
- var toolbar__namespace = /*#__PURE__*/_interopNamespaceDefault(toolbar);
43
45
 
44
- var styles$l = {"accessibility-toolbar":"accessibility-toolbar-jgjUG"};
46
+ var styles$s = {"accessibility-toolbar":"accessibility-toolbar-jgjUG"};
45
47
 
46
48
  const AccessibilityToolbar = ({
47
49
  children
@@ -53,7 +55,7 @@ const AccessibilityToolbar = ({
53
55
  (_a = document.getElementById("main-content")) === null || _a === void 0 ? void 0 : _a.focus();
54
56
  };
55
57
  return /*#__PURE__*/React.createElement("div", {
56
- className: styles$l["accessibility-toolbar"]
58
+ className: styles$s["accessibility-toolbar"]
57
59
  }, /*#__PURE__*/React.createElement(tapestry.Link, {
58
60
  href: "#main-content",
59
61
  kind: "primary",
@@ -180,12 +182,14 @@ function inferRailsCsrfToken() {
180
182
  }
181
183
  function apiRequest(fetch, url, {
182
184
  method = "GET",
183
- data = null
185
+ data = null,
186
+ headers = {}
184
187
  } = {}) {
185
188
  const options = {
186
189
  headers: {
187
190
  Accept: "application/json",
188
- "Content-Type": "application/json"
191
+ "Content-Type": "application/json",
192
+ ...headers
189
193
  },
190
194
  method,
191
195
  redirect: "follow"
@@ -370,11 +374,11 @@ const X = ({
370
374
  fill: fill
371
375
  })));
372
376
 
373
- 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"};
377
+ 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"};
374
378
 
375
379
  function AnnouncementIcon() {
376
380
  return /*#__PURE__*/React.createElement("svg", {
377
- className: styles$k["announcement-icon"],
381
+ className: styles$r["announcement-icon"],
378
382
  width: "16",
379
383
  height: "16",
380
384
  viewBox: "0 0 16 16",
@@ -499,7 +503,7 @@ let StyledAnnouncement$1 = class StyledAnnouncement extends React.Component {
499
503
  } = this.props;
500
504
  const isPromo = variant === "promo";
501
505
  return /*#__PURE__*/React.createElement("div", {
502
- className: classnames(styles$k["announcement"], styles$k[`announcement--${variant !== null && variant !== void 0 ? variant : "platform"}`]),
506
+ className: classnames(styles$r["announcement"], styles$r[`announcement--${variant !== null && variant !== void 0 ? variant : "platform"}`]),
503
507
  "data-announcement-variant": variant !== null && variant !== void 0 ? variant : "platform",
504
508
  style: isPromo ? {
505
509
  backgroundImage: promoBackgroundUrl,
@@ -508,7 +512,7 @@ let StyledAnnouncement$1 = class StyledAnnouncement extends React.Component {
508
512
  backgroundSize: "cover"
509
513
  } : undefined
510
514
  }, !isPromo && /*#__PURE__*/React.createElement(AnnouncementIcon, null), /*#__PURE__*/React.createElement("div", {
511
- className: styles$k["announcement-content"],
515
+ className: styles$r["announcement-content"],
512
516
  style: style,
513
517
  ...platformProps
514
518
  }));
@@ -557,7 +561,7 @@ function StyledDismissButton$1({
557
561
  const isPromo = variant === "promo";
558
562
  return /*#__PURE__*/React.createElement("button", {
559
563
  type: "button",
560
- className: classnames(styles$k.dismiss, isPromo && styles$k["dismiss--promo"]),
564
+ className: classnames(styles$r.dismiss, isPromo && styles$r["dismiss--promo"]),
561
565
  ...props
562
566
  }, /*#__PURE__*/React.createElement(X, {
563
567
  fill: isPromo ? "currentColor" : "white",
@@ -637,9 +641,9 @@ class StyledAnnouncement extends React.Component {
637
641
  ...platformProps
638
642
  } = this.props;
639
643
  return /*#__PURE__*/React.createElement("div", {
640
- className: styles$k["announcement"]
644
+ className: styles$r["announcement"]
641
645
  }, /*#__PURE__*/React.createElement(AnnouncementIcon, null), /*#__PURE__*/React.createElement("div", {
642
- className: styles$k["announcement-content"],
646
+ className: styles$r["announcement-content"],
643
647
  style: style,
644
648
  ...platformProps
645
649
  }));
@@ -650,7 +654,7 @@ function StyledDismissButton({
650
654
  }) {
651
655
  return /*#__PURE__*/React.createElement("button", {
652
656
  type: "button",
653
- className: styles$k.dismiss,
657
+ className: styles$r.dismiss,
654
658
  ...props
655
659
  }, /*#__PURE__*/React.createElement(X, {
656
660
  fill: "white",
@@ -805,7 +809,7 @@ function useMediaQuery(query) {
805
809
  return matches;
806
810
  }
807
811
 
808
- const capitalize = s => s.charAt(0).toUpperCase() + s.slice(1);
812
+ const capitalize$1 = s => s.charAt(0).toUpperCase() + s.slice(1);
809
813
  const RESOLVED_COLOR_MODE_ATTRIBUTE = "data-resolved-color-mode";
810
814
  function resolveColorMode(colorMode) {
811
815
  if (colorMode === "system") {
@@ -1772,7 +1776,7 @@ function MonoAppIcon({
1772
1776
  })) : (/*#__PURE__*/React.createElement("div", null, "appName not supported")));
1773
1777
  }
1774
1778
 
1775
- var styles$j = {"icon-slot":"icon-slot-m3ZIk","name":"name-UNqbW"};
1779
+ var styles$q = {"icon-slot":"icon-slot-m3ZIk","name":"name-UNqbW"};
1776
1780
 
1777
1781
  const AppRow = ({
1778
1782
  appName,
@@ -1781,24 +1785,24 @@ const AppRow = ({
1781
1785
  }) => {
1782
1786
  if (appName === "Accounts") {
1783
1787
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
1784
- className: styles$j["icon-slot"]
1788
+ className: styles$q["icon-slot"]
1785
1789
  }, /*#__PURE__*/React.createElement(MonoAppIcon, {
1786
1790
  appName: "Accounts",
1787
1791
  colors: colors,
1788
1792
  size: iconSize
1789
1793
  })), /*#__PURE__*/React.createElement("span", {
1790
- className: styles$j.name
1794
+ className: styles$q.name
1791
1795
  }, "Account settings"));
1792
1796
  }
1793
1797
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(ColorAppIcon, {
1794
1798
  appName: appName.replace(/[\s-]/, ""),
1795
1799
  size: iconSize
1796
1800
  }), /*#__PURE__*/React.createElement("span", {
1797
- className: styles$j.name
1801
+ className: styles$q.name
1798
1802
  }, appName));
1799
1803
  };
1800
1804
 
1801
- var styles$i = {"button":"button-XMbeQ","label":"label-884of","chevron":"chevron-RRg9k"};
1805
+ var styles$p = {"button":"button-XMbeQ","label":"label-884of","chevron":"chevron-RRg9k"};
1802
1806
 
1803
1807
  const AppsButton = /*#__PURE__*/React.forwardRef(({
1804
1808
  appName,
@@ -1810,10 +1814,10 @@ const AppsButton = /*#__PURE__*/React.forwardRef(({
1810
1814
  ...restProps,
1811
1815
  ref: ref,
1812
1816
  "aria-label": `App switcher: ${appName}`,
1813
- className: styles$i.button,
1817
+ className: styles$p.button,
1814
1818
  kind: "neutral-inline",
1815
1819
  label: /*#__PURE__*/React.createElement("span", {
1816
- className: styles$i.label
1820
+ className: styles$p.label
1817
1821
  }, isAccounts ? "Account settings" : appName),
1818
1822
  prefix: isAccounts ? (/*#__PURE__*/React.createElement(MonoAppIcon, {
1819
1823
  appName: "Accounts",
@@ -1824,13 +1828,13 @@ const AppsButton = /*#__PURE__*/React.forwardRef(({
1824
1828
  size: 22
1825
1829
  })),
1826
1830
  suffix: /*#__PURE__*/React.createElement(DisclosureChevronIcon, {
1827
- className: `${styles$i.chevron} symbol`
1831
+ className: `${styles$p.chevron} symbol`
1828
1832
  })
1829
1833
  });
1830
1834
  });
1831
1835
  AppsButton.displayName = "AppsButton";
1832
1836
 
1833
- 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"};
1837
+ 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"};
1834
1838
 
1835
1839
  const AppsMenu = ({
1836
1840
  appName,
@@ -1851,8 +1855,8 @@ const AppsMenu = ({
1851
1855
  }, /*#__PURE__*/React.createElement("a", {
1852
1856
  "aria-current": isCurrent ? "page" : undefined,
1853
1857
  "aria-label": name,
1854
- className: classnames(styles$h.link, {
1855
- [styles$h.current]: isCurrent
1858
+ className: classnames(styles$o.link, {
1859
+ [styles$o.current]: isCurrent
1856
1860
  }),
1857
1861
  "data-pendo": `app-switcher--change-app--${name}`,
1858
1862
  "data-turbolinks": false,
@@ -1868,22 +1872,22 @@ const AppsMenu = ({
1868
1872
  colors: colors
1869
1873
  })), /*#__PURE__*/React.createElement(tapestry.PopoverContent, {
1870
1874
  "aria-label": "Switch app",
1871
- className: styles$h["popover-content"],
1875
+ className: styles$o["popover-content"],
1872
1876
  placement: "bottom start"
1873
1877
  }, /*#__PURE__*/React.createElement("nav", {
1874
- className: styles$h.nav,
1878
+ className: styles$o.nav,
1875
1879
  "data-pendo": "app-switcher"
1876
1880
  }, /*#__PURE__*/React.createElement("ul", {
1877
- className: styles$h.list,
1881
+ className: styles$o.list,
1878
1882
  role: "list"
1879
1883
  }, homeProduct && renderProductLink(homeProduct.name), restProducts.map(({
1880
1884
  name
1881
1885
  }) => renderProductLink(name)), accountsProduct && (/*#__PURE__*/React.createElement("li", {
1882
- className: styles$h["accounts-item"]
1886
+ className: styles$o["accounts-item"]
1883
1887
  }, /*#__PURE__*/React.createElement("a", {
1884
1888
  "aria-current": appName === accountsProduct.name ? "page" : undefined,
1885
1889
  "aria-label": "Account settings",
1886
- className: classnames(styles$h.link, styles$h["accounts-link"]),
1890
+ className: classnames(styles$o.link, styles$o["accounts-link"]),
1887
1891
  "data-pendo": "app-switcher--change-app--Accounts",
1888
1892
  "data-turbolinks": false,
1889
1893
  href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(accountsProduct.name)
@@ -2091,7 +2095,7 @@ function MonoAppName({
2091
2095
  })) : (/*#__PURE__*/React.createElement("div", null, "appName not supported"));
2092
2096
  }
2093
2097
 
2094
- var styles$g = {"unbutton":"unbutton-lEbTR"};
2098
+ var styles$n = {"unbutton":"unbutton-lEbTR"};
2095
2099
 
2096
2100
  function Unbutton({
2097
2101
  className,
@@ -2100,7 +2104,7 @@ function Unbutton({
2100
2104
  ...nativeProps
2101
2105
  }) {
2102
2106
  return /*#__PURE__*/React.createElement("button", {
2103
- className: classnames(styles$g.unbutton, className),
2107
+ className: classnames(styles$n.unbutton, className),
2104
2108
  style: {
2105
2109
  ...style
2106
2110
  },
@@ -2109,7 +2113,7 @@ function Unbutton({
2109
2113
  });
2110
2114
  }
2111
2115
 
2112
- var styles$f = {"button":"button-wsipc","button-accounts":"button-accounts-zQmBM","chevron":"chevron-obyBF","is-expanded":"is-expanded-PoCQ9"};
2116
+ var styles$m = {"button":"button-wsipc","button-accounts":"button-accounts-zQmBM","chevron":"chevron-obyBF","is-expanded":"is-expanded-PoCQ9"};
2113
2117
 
2114
2118
  const LegacyAppsButton = ({
2115
2119
  appName,
@@ -2124,8 +2128,8 @@ const LegacyAppsButton = ({
2124
2128
  "aria-haspopup": "true",
2125
2129
  "aria-label": `App switcher: ${appName}`,
2126
2130
  onClick: onClick,
2127
- className: classnames(styles$f.button, {
2128
- [styles$f["is-expanded"]]: expanded
2131
+ className: classnames(styles$m.button, {
2132
+ [styles$m["is-expanded"]]: expanded
2129
2133
  }),
2130
2134
  type: "button",
2131
2135
  style: style,
@@ -2134,7 +2138,7 @@ const LegacyAppsButton = ({
2134
2138
  appName: "Accounts",
2135
2139
  colors: colors
2136
2140
  }), /*#__PURE__*/React.createElement("div", {
2137
- className: styles$f["button-accounts"]
2141
+ className: styles$m["button-accounts"]
2138
2142
  }, "Account settings"))) : (/*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(ColorAppIcon, {
2139
2143
  appName: appName.replace(/[\s-]/, "")
2140
2144
  }), /*#__PURE__*/React.createElement(MonoAppName, {
@@ -2142,7 +2146,7 @@ const LegacyAppsButton = ({
2142
2146
  key: "mono-app-name",
2143
2147
  size: 24
2144
2148
  }))), /*#__PURE__*/React.createElement(DisclosureChevronIcon, {
2145
- className: styles$f.chevron,
2149
+ className: styles$m.chevron,
2146
2150
  key: "disclosure-chevron-icon"
2147
2151
  }));
2148
2152
  };
@@ -2275,7 +2279,7 @@ class Outsider extends React.Component {
2275
2279
  }
2276
2280
  }
2277
2281
 
2278
- var styles$e = {"popup-root":"popup-root-SeL5u","popup":"popup-nQXYW","is-visible":"is-visible-3DAUP"};
2282
+ var styles$l = {"popup-root":"popup-root-SeL5u","popup":"popup-nQXYW","is-visible":"is-visible-3DAUP"};
2279
2283
 
2280
2284
  const Popup = ({
2281
2285
  className,
@@ -2283,8 +2287,8 @@ const Popup = ({
2283
2287
  isVisible,
2284
2288
  ...nativeProps
2285
2289
  }) => (/*#__PURE__*/React.createElement(C, {
2286
- className: classnames(styles$e.popup, {
2287
- [styles$e["is-visible"]]: isVisible
2290
+ className: classnames(styles$l.popup, {
2291
+ [styles$l["is-visible"]]: isVisible
2288
2292
  }, className),
2289
2293
  ...nativeProps
2290
2294
  }));
@@ -2292,11 +2296,11 @@ const Popup = ({
2292
2296
  const PopupRoot = ({
2293
2297
  ...nativeProps
2294
2298
  }) => (/*#__PURE__*/React.createElement("div", {
2295
- className: styles$e["popup-root"],
2299
+ className: styles$l["popup-root"],
2296
2300
  ...nativeProps
2297
2301
  }));
2298
2302
 
2299
- var styles$d = {"popup":"popup-bVhSa"};
2303
+ var styles$k = {"popup":"popup-bVhSa"};
2300
2304
 
2301
2305
  const LegacyAppsMenu = ({
2302
2306
  appName,
@@ -2334,7 +2338,7 @@ const LegacyAppsMenu = ({
2334
2338
  cleanup: !visible,
2335
2339
  component: Outsider,
2336
2340
  onOutsideClick: toggle,
2337
- className: styles$d.popup,
2341
+ className: styles$k.popup,
2338
2342
  isVisible: visible
2339
2343
  }, /*#__PURE__*/React.createElement("menu", {
2340
2344
  ref: menuRef,
@@ -3244,29 +3248,121 @@ const useUpdateStore = () => React.useCallback(updates => {
3244
3248
  });
3245
3249
  }, []);
3246
3250
 
3247
- const isMac = () => /Mac|iPhone|iPad|iPod/.test(navigator.platform);
3248
- const hasCommandKey = event => isMac() ? event.metaKey : event.ctrlKey;
3249
- const commandKeyLabel = () => isMac() ? "⌘" : "Ctrl+";
3250
-
3251
- const STORAGE_KEY = "topbar.product_search.last_product";
3252
- function readLastProduct() {
3253
- try {
3254
- return localStorage.getItem(STORAGE_KEY);
3255
- } catch {
3256
- return null;
3257
- }
3251
+ const collections = {
3252
+ general: general__namespace,
3253
+ toolbar: toolbar__namespace
3254
+ };
3255
+ const paths = collections;
3256
+ function Icon({
3257
+ name,
3258
+ size = 16,
3259
+ style,
3260
+ theme,
3261
+ title,
3262
+ ...props
3263
+ }) {
3264
+ const [collection, symbol] = name.split(".");
3265
+ return /*#__PURE__*/React.createElement("svg", {
3266
+ "aria-hidden": title ? undefined : true,
3267
+ fill: "currentColor",
3268
+ height: size,
3269
+ role: title ? "img" : undefined,
3270
+ style: {
3271
+ color: theme && `var(${theme})`,
3272
+ flexShrink: 0,
3273
+ ...style
3274
+ },
3275
+ viewBox: "0 0 16 16",
3276
+ width: size,
3277
+ ...props
3278
+ }, title && /*#__PURE__*/React.createElement("title", null, title), /*#__PURE__*/React.createElement("path", {
3279
+ d: paths[collection][symbol]
3280
+ }));
3258
3281
  }
3259
- function saveLastProduct(slug) {
3260
- try {
3261
- localStorage.setItem(STORAGE_KEY, slug);
3262
- } catch {
3263
- // Storage can be full or blocked; search still works without it.
3282
+
3283
+ const SEARCHABLE_PRODUCTS = [{
3284
+ appName: "Calendar",
3285
+ slug: "calendar"
3286
+ }, {
3287
+ appName: "Check-Ins",
3288
+ slug: "check-ins"
3289
+ }, {
3290
+ appName: "Giving",
3291
+ slug: "giving"
3292
+ }, {
3293
+ appName: "Groups",
3294
+ slug: "groups"
3295
+ }, {
3296
+ appName: "People",
3297
+ slug: "people"
3298
+ }, {
3299
+ appName: "Publishing",
3300
+ slug: "publishing"
3301
+ }, {
3302
+ appName: "Registrations",
3303
+ slug: "registrations"
3304
+ }, {
3305
+ appName: "Services",
3306
+ slug: "services"
3307
+ }];
3308
+ const KEYBOARD_SHORTCUTS = {
3309
+ copyLink: {
3310
+ key: "c",
3311
+ label: "Copy link",
3312
+ modifierKey: true
3313
+ },
3314
+ openInNewTab: {
3315
+ label: "Open in new tab"},
3316
+ openSearch: {
3317
+ key: "k",
3318
+ label: "Open search",
3319
+ modifierKey: true
3320
+ },
3321
+ select: {
3322
+ label: "Select",
3323
+ modifierKey: false,
3324
+ next: "ArrowDown",
3325
+ previous: "ArrowUp"
3264
3326
  }
3265
- }
3327
+ };
3266
3328
 
3267
- var styles$c = {"visuallyHidden":"visuallyHidden-C28ju"};
3329
+ const isMac = () => /Mac|iPhone|iPad|iPod/.test(navigator.platform);
3330
+ const hasCommandKey = event => isMac() ? event.metaKey : event.ctrlKey;
3331
+ const matchesShortcut = (shortcut, event) => event.key.toLowerCase() === shortcut.key && !event.altKey && !event.shiftKey && hasCommandKey(event) === shortcut.modifierKey;
3332
+ const isSearchShortcut = event => matchesShortcut(KEYBOARD_SHORTCUTS.openSearch, event);
3333
+ // Products opened their own search with `/` before this one replaced it.
3334
+ const isLegacySearchShortcut = event => event.key === "/" && !event.altKey && !event.ctrlKey && !event.metaKey && !isTypingIn(event.target);
3335
+ const isCopyShortcut = event => matchesShortcut(KEYBOARD_SHORTCUTS.copyLink, event);
3336
+ const isTypingIn = target => target instanceof Element && target.closest('input, select, textarea, [contenteditable]:not([contenteditable="false"])') !== null;
3337
+ const isTypingKey = event => event.key.length === 1 && event.key !== " " && !event.altKey && !event.ctrlKey && !event.metaKey;
3338
+ const verticalStep = key => ({
3339
+ [KEYBOARD_SHORTCUTS.select.next]: 1,
3340
+ [KEYBOARD_SHORTCUTS.select.previous]: -1
3341
+ })[key];
3342
+ const horizontalStep = key => ({
3343
+ ArrowLeft: -1,
3344
+ ArrowRight: 1
3345
+ })[key];
3268
3346
 
3269
- var styles$b = {"tab":"tab-lQLDx"};
3347
+ var styles$j = {"searchButton":"searchButton-fYa5-","mobile":"mobile-ksZyA"};
3348
+
3349
+ function SearchButton({
3350
+ mobile = false,
3351
+ onClick
3352
+ }) {
3353
+ return /*#__PURE__*/React.createElement("button", {
3354
+ "aria-keyshortcuts": mobile ? undefined : `${isMac() ? "Meta" : "Control"}+${KEYBOARD_SHORTCUTS.openSearch.key.toUpperCase()} /`,
3355
+ "aria-label": "Search",
3356
+ className: classnames(styles$j.searchButton, {
3357
+ [styles$j.mobile]: mobile
3358
+ }),
3359
+ onClick: onClick,
3360
+ type: "button"
3361
+ }, /*#__PURE__*/React.createElement(Icon, {
3362
+ name: "general.search",
3363
+ size: mobile ? 18 : 14
3364
+ }));
3365
+ }
3270
3366
 
3271
3367
  // useId isn't available on React 17, which our peer range still supports.
3272
3368
  let instanceCount$1 = 0;
@@ -3275,173 +3371,33 @@ function useInstanceId(prefix) {
3275
3371
  return id;
3276
3372
  }
3277
3373
 
3278
- function ProductTabs({
3279
- children,
3280
- onKeyDown,
3281
- onSelect,
3282
- products,
3283
- selected
3284
- }) {
3285
- const id = useInstanceId("product-tabs");
3286
- const tabId = slug => `${id}-tab-${slug}`;
3287
- const tabRefs = React.useRef(new Map());
3288
- const handleKeyDown = event => {
3289
- var _a;
3290
- const offset = {
3291
- ArrowLeft: -1,
3292
- ArrowRight: 1
3293
- }[event.key];
3294
- if (offset) {
3295
- event.preventDefault();
3296
- const index = products.indexOf(selected);
3297
- const next = products[(index + offset + products.length) % products.length];
3298
- onSelect(next.slug);
3299
- (_a = tabRefs.current.get(next.slug)) === null || _a === void 0 ? void 0 : _a.focus();
3300
- } else {
3301
- onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
3302
- }
3303
- };
3304
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
3305
- "aria-label": "Product",
3306
- onKeyDown: handleKeyDown,
3307
- role: "tablist"
3308
- }, products.map(product => (/*#__PURE__*/React.createElement("button", {
3309
- "aria-controls": `${id}-panel`,
3310
- "aria-selected": product === selected,
3311
- className: styles$b.tab,
3312
- id: tabId(product.slug),
3313
- key: product.slug,
3314
- onClick: () => onSelect(product.slug),
3315
- ref: element => {
3316
- if (element) {
3317
- tabRefs.current.set(product.slug, element);
3318
- } else {
3319
- tabRefs.current.delete(product.slug);
3320
- }
3321
- },
3322
- role: "tab",
3323
- tabIndex: product === selected ? 0 : -1,
3324
- type: "button"
3325
- }, product.appName)))), /*#__PURE__*/React.createElement("div", {
3326
- "aria-labelledby": tabId(selected.slug),
3327
- id: `${id}-panel`,
3328
- role: "tabpanel"
3329
- }, children));
3330
- }
3331
-
3332
- var styles$a = {"result":"result-WCnxr"};
3333
-
3334
- function SearchResult({
3335
- highlighted,
3336
- id,
3337
- onHighlight,
3338
- result
3339
- }) {
3340
- const {
3341
- metadata,
3342
- title,
3343
- url
3344
- } = result.attributes;
3345
- const ref = React.useRef(null);
3346
- React.useEffect(() => {
3347
- var _a;
3348
- if (highlighted) (_a = ref.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({
3349
- block: "nearest"
3350
- });
3351
- }, [highlighted]);
3352
- // Focus stays in the search input, which points at the highlighted result
3353
- // with aria-activedescendant, so results are kept out of the tab order.
3354
- // Mouse move rather than enter: arrowing scrolls results under a resting
3355
- // pointer, and that shouldn't steal the highlight.
3356
- return /*#__PURE__*/React.createElement("a", {
3357
- "aria-selected": highlighted,
3358
- className: styles$a.result,
3359
- href: url,
3360
- id: id,
3361
- onMouseMove: onHighlight,
3362
- ref: ref,
3363
- role: "option",
3364
- tabIndex: -1
3365
- }, /*#__PURE__*/React.createElement("div", null, title), /*#__PURE__*/React.createElement("div", null, summarize(metadata)));
3366
- }
3367
- function summarize(metadata) {
3368
- return Object.values(metadata !== null && metadata !== void 0 ? metadata : {}).filter(value => typeof value === "string" && value !== "").join(" · ");
3369
- }
3370
-
3371
- const resultId = (listboxId, index) => `${listboxId}-result-${index}`;
3372
- function SearchResults({
3373
- search,
3374
- term,
3375
- ...navigation
3376
- }) {
3377
- var _a;
3378
- switch (search.status) {
3379
- case "idle":
3380
- return null;
3381
- case "loading":
3382
- if ((_a = search.groups) === null || _a === void 0 ? void 0 : _a.length) {
3383
- return /*#__PURE__*/React.createElement(ResultGroups, {
3384
- busy: true,
3385
- groups: search.groups,
3386
- ...navigation
3387
- });
3388
- }
3389
- return /*#__PURE__*/React.createElement("p", {
3390
- role: "status"
3391
- }, "Searching\u2026");
3392
- case "error":
3393
- return /*#__PURE__*/React.createElement("p", {
3394
- role: "alert"
3395
- }, "Search isn\u2019t working right now. Try again.");
3396
- case "done":
3397
- if (search.groups.length === 0) {
3398
- return /*#__PURE__*/React.createElement("p", {
3399
- role: "status"
3400
- }, "No results for \u201C", term.trim(), "\u201D");
3401
- }
3402
- return /*#__PURE__*/React.createElement(ResultGroups, {
3403
- groups: search.groups,
3404
- ...navigation
3405
- });
3374
+ // The versions home-mobile pins, so web and mobile read the same response
3375
+ // shapes. Some endpoints only exist from a version on: Registrations' me,
3376
+ // product_search_results, and pcrl_lookups need 2025-05-01.
3377
+ const API_VERSIONS = {
3378
+ calendar: "2022-07-07",
3379
+ "check-ins": "2025-05-28",
3380
+ giving: "2019-10-18",
3381
+ groups: "2023-07-10",
3382
+ home: "2018-08-01",
3383
+ people: "2023-03-21",
3384
+ publishing: "2024-03-25",
3385
+ registrations: "2025-05-01",
3386
+ services: "2018-11-01"
3387
+ };
3388
+ // For a `/~api/{product}/v2/...` URL, the header that pins its product's
3389
+ // version.
3390
+ function apiVersionHeaders(url) {
3391
+ const [,, product] = new URL(url).pathname.split("/");
3392
+ const version = API_VERSIONS[product];
3393
+ if (version) {
3394
+ return {
3395
+ "X-PCO-API-Version": version
3396
+ };
3397
+ } else {
3398
+ return {};
3406
3399
  }
3407
3400
  }
3408
- function ResultGroups({
3409
- busy = false,
3410
- groups,
3411
- highlightedIndex,
3412
- id,
3413
- label,
3414
- onHighlight
3415
- }) {
3416
- let index = 0;
3417
- return /*#__PURE__*/React.createElement("div", {
3418
- "aria-busy": busy,
3419
- "aria-label": label,
3420
- id: id,
3421
- role: "listbox"
3422
- }, groups.map(({
3423
- results,
3424
- type
3425
- }, groupIndex) => {
3426
- const headingId = `${id}-group-${groupIndex}`;
3427
- return /*#__PURE__*/React.createElement("div", {
3428
- "aria-labelledby": headingId,
3429
- key: type,
3430
- role: "group"
3431
- }, /*#__PURE__*/React.createElement("div", {
3432
- id: headingId
3433
- }, type), results.map(result => {
3434
- const resultIndex = index++;
3435
- return /*#__PURE__*/React.createElement(SearchResult, {
3436
- highlighted: resultIndex === highlightedIndex,
3437
- id: resultId(id, resultIndex),
3438
- key: result.id,
3439
- onHighlight: () => onHighlight(resultIndex),
3440
- result: result
3441
- });
3442
- }));
3443
- }));
3444
- }
3445
3401
 
3446
3402
  const CACHE_TTL_MS = 60000;
3447
3403
  const MAX_ENTRIES$1 = 50;
@@ -3458,7 +3414,9 @@ function cachedRequest(url) {
3458
3414
  }
3459
3415
  }
3460
3416
  function fetchIntoCache(url) {
3461
- const promise = apiRequest(defaultFetch, url).then(({
3417
+ const promise = apiRequest(defaultFetch, url, {
3418
+ headers: apiVersionHeaders(url)
3419
+ }).then(({
3462
3420
  json,
3463
3421
  ok
3464
3422
  }) => {
@@ -3514,44 +3472,52 @@ function useCachedRequest(url) {
3514
3472
  };
3515
3473
  }
3516
3474
  }
3517
-
3518
- const SEARCH_DEBOUNCE_MS = 300;
3519
- function useProductSearch(product, term) {
3520
- const query = term.trim();
3521
- const debouncedQuery = useDebouncedValue(query, SEARCH_DEBOUNCE_MS);
3522
- const request = useCachedRequest(debouncedQuery ? productSearchUrl(product, debouncedQuery) : null);
3523
- const response = "data" in request ? request.data : undefined;
3524
- const settledProduct = useSettledProduct(product, request.status);
3525
- // While a request loads, the previous response stays on screen, but only
3526
- // if it was for this product: People results must never show under the
3527
- // Services tab.
3528
- const visibleResponse = request.status === "done" || settledProduct === product ? response : undefined;
3529
- const groups = React.useMemo(() => visibleResponse && groupByType(visibleResponse), [visibleResponse]);
3530
- if (!query) {
3531
- return {
3532
- status: "idle"
3533
- };
3534
- } else if (query !== debouncedQuery) {
3535
- return {
3536
- groups,
3537
- status: "loading"
3538
- };
3539
- } else if (request.status === "done") {
3540
- return {
3541
- groups: groups !== null && groups !== void 0 ? groups : [],
3542
- status: "done"
3543
- };
3544
- } else if (request.status === "error") {
3545
- return {
3546
- status: "error"
3547
- };
3548
- } else {
3549
- return {
3550
- groups,
3551
- status: "loading"
3475
+ // One request per URL, for callers that fan out to a varying number of
3476
+ // endpoints. Unlike `useCachedRequest`, a URL that hasn't settled yet has no
3477
+ // earlier data to fall back on.
3478
+ function useCachedRequests(urls) {
3479
+ const [settled, setSettled] = React.useState({});
3480
+ // Keyed by value because each settle re-renders the caller, and a caller
3481
+ // that builds a fresh `urls` array on render would otherwise loop forever.
3482
+ const key = urls.join("\n");
3483
+ React.useEffect(() => {
3484
+ let current = true;
3485
+ const settle = result => current && setSettled(prev => ({
3486
+ ...prev,
3487
+ [result.url]: result
3488
+ }));
3489
+ for (const url of key.split("\n").filter(Boolean)) {
3490
+ cachedRequest(url).then(data => settle({
3491
+ data,
3492
+ url
3493
+ }), () => settle({
3494
+ failed: true,
3495
+ url
3496
+ }));
3497
+ }
3498
+ return () => {
3499
+ current = false;
3552
3500
  };
3553
- }
3501
+ }, [key]);
3502
+ return urls.map(url => {
3503
+ const result = settled[url];
3504
+ if (!result) {
3505
+ return {
3506
+ status: "loading"
3507
+ };
3508
+ } else if ("data" in result) {
3509
+ return {
3510
+ data: result.data,
3511
+ status: "done"
3512
+ };
3513
+ } else {
3514
+ return {
3515
+ status: "error"
3516
+ };
3517
+ }
3518
+ });
3554
3519
  }
3520
+
3555
3521
  function productSearchUrl(product, query) {
3556
3522
  const params = new URLSearchParams({
3557
3523
  per_page: "100",
@@ -3582,6 +3548,53 @@ function groupByType({
3582
3548
  };
3583
3549
  return [...groups.values()].sort((a, b) => rank(a.type) - rank(b.type));
3584
3550
  }
3551
+
3552
+ function useDebouncedValue(value, delay) {
3553
+ const [debounced, setDebounced] = React.useState(value);
3554
+ React.useEffect(() => {
3555
+ const timeout = setTimeout(() => setDebounced(value), delay);
3556
+ return () => clearTimeout(timeout);
3557
+ }, [value, delay]);
3558
+ return debounced;
3559
+ }
3560
+
3561
+ const SEARCH_DEBOUNCE_MS = 300;
3562
+ function useProductSearch(product, term) {
3563
+ const query = term.trim();
3564
+ const debouncedQuery = useDebouncedValue(query, SEARCH_DEBOUNCE_MS);
3565
+ const request = useCachedRequest(debouncedQuery ? productSearchUrl(product, debouncedQuery) : null);
3566
+ const response = "data" in request ? request.data : undefined;
3567
+ const settledProduct = useSettledProduct(product, request.status);
3568
+ // While a request loads, the previous response stays on screen, but only
3569
+ // if it was for this product: People results must never show under the
3570
+ // Services tab.
3571
+ const visibleResponse = request.status === "done" || settledProduct === product ? response : undefined;
3572
+ const groups = React.useMemo(() => visibleResponse && groupByType(visibleResponse), [visibleResponse]);
3573
+ if (!query) {
3574
+ return {
3575
+ status: "idle"
3576
+ };
3577
+ } else if (query !== debouncedQuery) {
3578
+ return {
3579
+ groups,
3580
+ status: "loading"
3581
+ };
3582
+ } else if (request.status === "done") {
3583
+ return {
3584
+ groups: groups !== null && groups !== void 0 ? groups : [],
3585
+ status: "done"
3586
+ };
3587
+ } else if (request.status === "error") {
3588
+ return {
3589
+ status: "error"
3590
+ };
3591
+ } else {
3592
+ return {
3593
+ groups,
3594
+ status: "loading"
3595
+ };
3596
+ }
3597
+ }
3585
3598
  // The product the most recent finished request was for.
3586
3599
  function useSettledProduct(product, status) {
3587
3600
  const [settledProduct, setSettledProduct] = React.useState();
@@ -3590,29 +3603,489 @@ function useSettledProduct(product, status) {
3590
3603
  }, [product, status]);
3591
3604
  return settledProduct;
3592
3605
  }
3593
- function useDebouncedValue(value, delay) {
3594
- const [debounced, setDebounced] = React.useState(value);
3595
- React.useEffect(() => {
3596
- const timeout = setTimeout(() => setDebounced(value), delay);
3597
- return () => clearTimeout(timeout);
3598
- }, [value, delay]);
3599
- return debounced;
3606
+
3607
+ function parseValue(element) {
3608
+ const datatype = element.getAttribute("datatype");
3609
+ switch (datatype) {
3610
+ case "boolean":
3611
+ return element.content === "true" || element.content === "1";
3612
+ default:
3613
+ return element.content;
3614
+ }
3615
+ }
3616
+ function getAllPcoMeta() {
3617
+ const elements = document.head.querySelectorAll(`meta[property^="pco:"]`);
3618
+ const meta = {};
3619
+ elements.forEach(e => {
3620
+ const property = e.getAttribute("property");
3621
+ meta[property.substring("pco:".length)] = parseValue(e);
3622
+ });
3623
+ return meta;
3624
+ }
3625
+ // usePcoMeta only reads each tag's content. Recently Visited also needs the
3626
+ // pco:recently_visitable tag's `metadata` attribute, which can change while
3627
+ // the content stays the same, so it is tracked in its own store.
3628
+ function getRecentlyVisitableMetadata() {
3629
+ var _a, _b;
3630
+ 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;
3631
+ }
3632
+ const store = createStore(getAllPcoMeta());
3633
+ const recentlyVisitableMetadataStore = createStore({
3634
+ metadata: getRecentlyVisitableMetadata()
3635
+ });
3636
+ function sync() {
3637
+ const nextState = getAllPcoMeta();
3638
+ if (!shallowCompare(store.getState(), nextState)) store.setState(nextState);
3639
+ const metadata = getRecentlyVisitableMetadata();
3640
+ if (recentlyVisitableMetadataStore.getState().metadata !== metadata) {
3641
+ recentlyVisitableMetadataStore.setState({
3642
+ metadata
3643
+ });
3644
+ }
3645
+ }
3646
+ const observer = new MutationObserver(sync);
3647
+ let hasTriggeredObserve = false;
3648
+ function observe() {
3649
+ if (hasTriggeredObserve) return;
3650
+ hasTriggeredObserve = true;
3651
+ // The module-load snapshot above can predate meta tags that appear later
3652
+ // in <head> (e.g. pco:resource, yielded right before </head>) if this
3653
+ // module's own script tag isn't deferred and runs mid-parse. Those tags
3654
+ // never mutate afterward, so the observer alone would never catch them.
3655
+ // Re-read once now, since observe() is only called once a consumer is
3656
+ // actually mounting, which is always after the document has fully parsed.
3657
+ sync();
3658
+ observer.observe(document.head, {
3659
+ attributes: true,
3660
+ childList: true,
3661
+ subtree: true
3662
+ });
3663
+ }
3664
+ function usePcoMeta() {
3665
+ observe();
3666
+ return shim.useSyncExternalStore(store.subscribe, () => store.getState());
3667
+ }
3668
+ function useRecentlyVisitableMetadata() {
3669
+ observe();
3670
+ return shim.useSyncExternalStore(recentlyVisitableMetadataStore.subscribe, () => recentlyVisitableMetadataStore.getState().metadata);
3600
3671
  }
3601
3672
 
3602
- const COPIED_FEEDBACK_MS = 1500;
3603
- const verticalStep = key => ({
3604
- ArrowDown: 1,
3605
- ArrowUp: -1
3606
- })[key];
3607
- function useResultNavigation(results) {
3608
- // Remembering which results the index belongs to puts the highlight back on
3609
- // the first result whenever new ones arrive, without an effect to reset it.
3610
- const [highlight, setHighlight] = React.useState({
3611
- index: 0,
3612
- results
3673
+ // Entries match the ones home-mobile stores, so one list can sync across web
3674
+ // and mobile.
3675
+ const ENTRY_VERSION = 3;
3676
+ const MAX_ENTRIES = 50;
3677
+ const MAX_TEXT_LENGTH = 200;
3678
+ const PRODUCTS$1 = ["accounts", "calendar", "check-ins", "giving", "groups", "home", "login", "people", "publishing", "registrations", "services"];
3679
+ // Login has no pages worth returning to.
3680
+ const APPS = PRODUCTS$1.filter(product => product !== "login");
3681
+ function isId(value) {
3682
+ return value !== "" && Array.from(value).every(character => character >= "0" && character <= "9");
3683
+ }
3684
+ // A PCRL looks like `<product>/v2/<resource_type>/<id>`.
3685
+ function isPcrl(value) {
3686
+ const segments = value.split("/");
3687
+ return segments.length === 4 && PRODUCTS$1.some(product => product === segments[0]) && isId(segments[3]);
3688
+ }
3689
+ function isApp(value) {
3690
+ return APPS.some(app => app === value);
3691
+ }
3692
+ // Links are built by resolving the path against its app's origin, so a stored
3693
+ // path must not be able to resolve anywhere else (`//evil.com`, `/\evil.com`).
3694
+ function isPath(value) {
3695
+ const origin = "https://app.invalid";
3696
+ try {
3697
+ return typeof value === "string" && value.startsWith("/") && new URL(value, origin).origin === origin;
3698
+ } catch {
3699
+ return false;
3700
+ }
3701
+ }
3702
+ function isNonBlankString(value) {
3703
+ return typeof value === "string" && value.trim() !== "";
3704
+ }
3705
+ function isOptionalString(value) {
3706
+ return value === undefined || typeof value === "string";
3707
+ }
3708
+ function isRecord(value) {
3709
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3710
+ }
3711
+ function isPageMetadata(value) {
3712
+ return value === undefined || isRecord(value) && isOptionalString(value.details) && isOptionalString(value.type);
3713
+ }
3714
+ // The query string is part of the link but not of the page's identity, so
3715
+ // revisiting with other filters moves the existing entry instead of adding one.
3716
+ function entryKey(entry) {
3717
+ if (entry.type === "resource") {
3718
+ return `resource:${entry.pcrl}`;
3719
+ } else {
3720
+ return `page:${entry.app}:${entry.path.split("?")[0]}`;
3721
+ }
3722
+ }
3723
+ function isEntry(value) {
3724
+ if (!isRecord(value) || value.version !== ENTRY_VERSION || typeof value.visitedAt !== "string") {
3725
+ return false;
3726
+ } else if (value.type === "resource") {
3727
+ return typeof value.pcrl === "string" && isPcrl(value.pcrl);
3728
+ } else if (value.type === "page") {
3729
+ return isApp(value.app) && isPath(value.path) && isNonBlankString(value.title) && isPageMetadata(value.metadata);
3730
+ } else {
3731
+ return false;
3732
+ }
3733
+ }
3734
+ // Cuts on code points so a surrogate pair is never split.
3735
+ function clampText(value) {
3736
+ return Array.from(value).slice(0, MAX_TEXT_LENGTH).join("");
3737
+ }
3738
+ function clampMetadata({
3739
+ details,
3740
+ type
3741
+ }) {
3742
+ return {
3743
+ ...(details && {
3744
+ details: clampText(details)
3745
+ }),
3746
+ ...(type && {
3747
+ type: clampText(type)
3748
+ })
3749
+ };
3750
+ }
3751
+ function clampVisit(visit) {
3752
+ if (visit.type === "resource") {
3753
+ return visit;
3754
+ } else {
3755
+ return {
3756
+ ...visit,
3757
+ ...(visit.metadata && {
3758
+ metadata: clampMetadata(visit.metadata)
3759
+ }),
3760
+ title: clampText(visit.title.trim())
3761
+ };
3762
+ }
3763
+ }
3764
+ function toEntry(visit, visitedAt) {
3765
+ return {
3766
+ ...clampVisit(visit),
3767
+ version: ENTRY_VERSION,
3768
+ visitedAt: visitedAt.toISOString()
3769
+ };
3770
+ }
3771
+ // Recents are disposable: anything unexpected reads as an empty list (or, for
3772
+ // a single bad entry, a list without it) rather than throwing.
3773
+ function parseRecentlyVisited(value) {
3774
+ let parsed = null;
3775
+ try {
3776
+ parsed = JSON.parse(value !== null && value !== void 0 ? value : "null");
3777
+ } catch {
3778
+ parsed = null;
3779
+ }
3780
+ if (Array.isArray(parsed)) {
3781
+ return parsed.filter(isEntry);
3782
+ } else {
3783
+ return [];
3784
+ }
3785
+ }
3786
+ function addVisit(entries, entry) {
3787
+ const key = entryKey(entry);
3788
+ return [entry, ...entries.filter(existing => entryKey(existing) !== key)].slice(0, MAX_ENTRIES);
3789
+ }
3790
+
3791
+ // The app whose URL, in this environment, has the current page's host.
3792
+ // Reversing pcoUrl handles every environment @planningcenter/url knows about,
3793
+ // and guarantees the stored app rebuilds a working link.
3794
+ function currentApp() {
3795
+ var _a;
3796
+ return (_a = APPS.find(app => new URL(pcoUrl$1.pcoUrl(app)).hostname === window.location.hostname)) !== null && _a !== void 0 ? _a : null;
3797
+ }
3798
+ function isFilled(value) {
3799
+ return typeof value === "string" && value !== "";
3800
+ }
3801
+ // The tag's metadata is product-supplied JSON; only its string `type` and
3802
+ // `details` carry over, and an entry without either has no metadata at all.
3803
+ function metadataFromPage(metadata) {
3804
+ var _a;
3805
+ try {
3806
+ const {
3807
+ details,
3808
+ type
3809
+ } = (_a = JSON.parse(metadata !== null && metadata !== void 0 ? metadata : "{}")) !== null && _a !== void 0 ? _a : {};
3810
+ if (isFilled(details) || isFilled(type)) {
3811
+ return {
3812
+ ...(isFilled(details) && {
3813
+ details
3814
+ }),
3815
+ ...(isFilled(type) && {
3816
+ type
3817
+ })
3818
+ };
3819
+ } else {
3820
+ return undefined;
3821
+ }
3822
+ } catch {
3823
+ return undefined;
3824
+ }
3825
+ }
3826
+ // Staff impersonating someone browse under `/O<org id>/P<person id>/…`.
3827
+ function withoutImpersonation(segments) {
3828
+ const [org = "", person = ""] = segments;
3829
+ if (org.startsWith("O") && isId(org.slice(1)) && person.startsWith("P") && isId(person.slice(1))) {
3830
+ return segments.slice(2);
3831
+ } else {
3832
+ return segments;
3833
+ }
3834
+ }
3835
+ // Normalized the way home-mobile does it, so a page visited on web and on
3836
+ // mobile is one entry once the list syncs.
3837
+ function pathFromLocation({
3838
+ pathname,
3839
+ search
3840
+ }) {
3841
+ const segments = withoutImpersonation(pathname.split("/").filter(Boolean));
3842
+ if (segments.length === 0) {
3843
+ return "/";
3844
+ } else {
3845
+ return `/${segments.join("/")}${search.length > 1 ? search : ""}`;
3846
+ }
3847
+ }
3848
+ // pco:resource is the more specific signal, so it wins when both tags are
3849
+ // present. A resource that isn't a PCRL is ignored rather than recorded.
3850
+ function visitFromPage({
3851
+ metadata,
3852
+ resource,
3853
+ title
3854
+ }) {
3855
+ const app = currentApp();
3856
+ if (resource && isPcrl(resource)) {
3857
+ return {
3858
+ pcrl: resource,
3859
+ type: "resource"
3860
+ };
3861
+ } else if (title && title.trim() !== "" && app) {
3862
+ const pageMetadata = metadataFromPage(metadata);
3863
+ return {
3864
+ app,
3865
+ ...(pageMetadata && {
3866
+ metadata: pageMetadata
3867
+ }),
3868
+ path: pathFromLocation(window.location),
3869
+ title,
3870
+ type: "page"
3871
+ };
3872
+ } else {
3873
+ return null;
3874
+ }
3875
+ }
3876
+ // The entry Recently Visited would record for the current page, or null when
3877
+ // the page isn't eligible. Shared by the recorder and the debug panel so the
3878
+ // panel always shows what actually gets stored.
3879
+ function useCurrentVisit() {
3880
+ const {
3881
+ recently_visitable: title,
3882
+ resource
3883
+ } = usePcoMeta();
3884
+ const metadata = useRecentlyVisitableMetadata();
3885
+ return React.useMemo(() => visitFromPage({
3886
+ metadata,
3887
+ resource,
3888
+ title
3889
+ }), [metadata, resource, title]);
3890
+ }
3891
+ // Products hand the person's ID to AppsProvider, not to the topbar itself,
3892
+ // and several pass it as a number.
3893
+ function useCurrentPersonId() {
3894
+ const {
3895
+ userId
3896
+ } = useStore();
3897
+ return userId == null ? undefined : String(userId);
3898
+ }
3899
+
3900
+ // Per-origin localStorage is a stopgap: each product keeps its own list until
3901
+ // PCA-409 moves this to storage shared across product subdomains. Only this
3902
+ // file should need to change when it does.
3903
+ function storageKey({
3904
+ env,
3905
+ personId
3906
+ }) {
3907
+ return `topbar.recently_visited.${env}.${personId}`;
3908
+ }
3909
+ // localStorage doesn't notify the tab that wrote it, so readers in this tab
3910
+ // learn about new visits from this counter instead.
3911
+ const changes = createStore({
3912
+ version: 0
3913
+ });
3914
+ function readRecentlyVisited(scope) {
3915
+ try {
3916
+ return parseRecentlyVisited(localStorage.getItem(storageKey(scope)));
3917
+ } catch {
3918
+ return [];
3919
+ }
3920
+ }
3921
+ function recordVisit(visit, scope) {
3922
+ const entry = toEntry(visit, new Date());
3923
+ if (isEntry(entry)) {
3924
+ try {
3925
+ localStorage.setItem(storageKey(scope), JSON.stringify(addVisit(readRecentlyVisited(scope), entry)));
3926
+ changes.setState({
3927
+ version: changes.getState().version + 1
3928
+ });
3929
+ } catch {
3930
+ // Storage can be full or blocked; recents are disposable.
3931
+ }
3932
+ }
3933
+ }
3934
+ function useRecentlyVisited({
3935
+ env,
3936
+ personId
3937
+ }) {
3938
+ const version = shim.useSyncExternalStore(changes.subscribe, () => changes.getState().version);
3939
+ // `version` isn't read, but listing it re-reads storage after each visit.
3940
+ return React.useMemo(() => personId ? readRecentlyVisited({
3941
+ env,
3942
+ personId
3943
+ }) : [], [env, personId, version]);
3944
+ }
3945
+
3946
+ const pcoUrl = pcoUrl$1;
3947
+
3948
+ // Resolves resource entries with one pcrl_lookups request per product.
3949
+ // pcrl_lookups reports forbidden and deleted resources the same way, so both
3950
+ // become `unresolved`, as does every entry in a batch whose request failed.
3951
+ function useHydratedRecentlyVisited(entries, env) {
3952
+ const batches = React.useMemo(() => batchByProduct(entries), [entries]);
3953
+ const urls = React.useMemo(() => batches.map(([product, pcrls]) => pcrlLookupsUrl(product, pcrls)), [batches]);
3954
+ const requests = useCachedRequests(urls);
3955
+ const lookups = new Map();
3956
+ batches.forEach(([, pcrls], index) => {
3957
+ const request = requests[index];
3958
+ for (const pcrl of pcrls) lookups.set(pcrl, lookupFor(pcrl, request));
3613
3959
  });
3614
- const highlightedIndex = highlight.results === results ? highlight.index : 0;
3615
- const highlighted = results[highlightedIndex];
3960
+ return entries.map(entry => {
3961
+ var _a;
3962
+ if (entry.type === "page") {
3963
+ return {
3964
+ entry,
3965
+ status: "page",
3966
+ url: pageUrl(entry, env)
3967
+ };
3968
+ } else {
3969
+ const lookup = (_a = lookups.get(entry.pcrl)) !== null && _a !== void 0 ? _a : {
3970
+ status: "unresolved"
3971
+ };
3972
+ if (lookup.status === "resolved") {
3973
+ return {
3974
+ ...lookup,
3975
+ entry,
3976
+ url: lookup.result.attributes.url
3977
+ };
3978
+ } else {
3979
+ return {
3980
+ entry,
3981
+ status: lookup.status
3982
+ };
3983
+ }
3984
+ }
3985
+ });
3986
+ }
3987
+ function lookupFor(pcrl, request) {
3988
+ if (request.status === "done") {
3989
+ const result = request.data.data.find(({
3990
+ attributes
3991
+ }) => attributes.pcrl === pcrl);
3992
+ return result ? {
3993
+ result,
3994
+ status: "resolved"
3995
+ } : {
3996
+ status: "unresolved"
3997
+ };
3998
+ } else if (request.status === "error") {
3999
+ return {
4000
+ status: "unresolved"
4001
+ };
4002
+ } else {
4003
+ return {
4004
+ status: "loading"
4005
+ };
4006
+ }
4007
+ }
4008
+ const RECENTLY_VISITED_PAGE_SIZE = 10;
4009
+ // A PCRL can only be looked up through its own product's API, which may not be
4010
+ // the product it was visited in. Each page of entries is batched on its own,
4011
+ // so loading the next page leaves earlier pages' URLs, and so their cache
4012
+ // keys, alone. Sorting keeps them stable as a page reorders.
4013
+ function batchByProduct(entries) {
4014
+ var _a;
4015
+ const batches = [];
4016
+ for (let start = 0; start < entries.length; start += RECENTLY_VISITED_PAGE_SIZE) {
4017
+ const page = new Map();
4018
+ for (const entry of entries.slice(start, start + RECENTLY_VISITED_PAGE_SIZE)) {
4019
+ if (entry.type === "resource") {
4020
+ const [product] = entry.pcrl.split("/");
4021
+ page.set(product, [...((_a = page.get(product)) !== null && _a !== void 0 ? _a : []), entry.pcrl]);
4022
+ }
4023
+ }
4024
+ for (const [product, pcrls] of page) {
4025
+ batches.push([product, [...pcrls].sort()]);
4026
+ }
4027
+ }
4028
+ return batches;
4029
+ }
4030
+ function pcrlLookupsUrl(product, pcrls) {
4031
+ const params = new URLSearchParams({
4032
+ "where[pcrl]": pcrls.join(",")
4033
+ });
4034
+ return `${window.location.origin}/~api/${product}/v2/pcrl_lookups?${params}`;
4035
+ }
4036
+ // Built from the app rather than the current origin, so it stays right once
4037
+ // PCA-409 shares one list across product subdomains.
4038
+ function pageUrl(entry, env) {
4039
+ return new URL(entry.path, pcoUrl(env)(entry.app)).href;
4040
+ }
4041
+
4042
+ function useRecentVisits(env) {
4043
+ const personId = useCurrentPersonId();
4044
+ const entries = useRecentlyVisited({
4045
+ env,
4046
+ personId
4047
+ });
4048
+ const [count, setCount] = React.useState(RECENTLY_VISITED_PAGE_SIZE);
4049
+ const shown = React.useMemo(() => entries.slice(0, count), [entries, count]);
4050
+ const hydrated = useHydratedRecentlyVisited(shown, env);
4051
+ // Resources show up once they're found. Ones that can't be found anymore,
4052
+ // or that the person can no longer see, are left out.
4053
+ const visits = hydrated.filter(isShown);
4054
+ const loading = hydrated.some(({
4055
+ status
4056
+ }) => status === "loading");
4057
+ const showMore = React.useCallback(() => setCount(current => current + RECENTLY_VISITED_PAGE_SIZE), []);
4058
+ return {
4059
+ count,
4060
+ hasMore: entries.length > count,
4061
+ loading,
4062
+ showMore,
4063
+ visits
4064
+ };
4065
+ }
4066
+ function isShown(visit) {
4067
+ return visit.status === "page" || visit.status === "resolved";
4068
+ }
4069
+
4070
+ function open(url, {
4071
+ newTab
4072
+ }) {
4073
+ if (newTab) {
4074
+ window.open(url, "_blank", "noopener");
4075
+ } else {
4076
+ window.location.assign(url);
4077
+ }
4078
+ }
4079
+ // With text selected, ⌘C should copy that text as usual, not the link.
4080
+ function hasSelectedText(element) {
4081
+ var _a, _b;
4082
+ const selectedInField = element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
4083
+ return selectedInField || ((_b = (_a = window.getSelection()) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : "") !== "";
4084
+ }
4085
+
4086
+ const COPIED_FEEDBACK_MS = 1500;
4087
+ // `copiedUrl` stays set briefly after a copy so the UI can confirm it.
4088
+ function useCopyLink() {
3616
4089
  const [copiedUrl, setCopiedUrl] = React.useState(null);
3617
4090
  React.useEffect(() => {
3618
4091
  if (copiedUrl) {
@@ -3622,201 +4095,718 @@ function useResultNavigation(results) {
3622
4095
  return undefined;
3623
4096
  }
3624
4097
  }, [copiedUrl]);
3625
- const highlightIndex = React.useCallback(index => setHighlight(current => current.results === results && current.index === index ? current : {
3626
- index,
3627
- results
3628
- }), [results]);
4098
+ const copy = React.useCallback(url => {
4099
+ navigator.clipboard.writeText(url).then(() => setCopiedUrl(url));
4100
+ }, []);
4101
+ return {
4102
+ copiedUrl,
4103
+ copy
4104
+ };
4105
+ }
4106
+
4107
+ // Remembering which list the index belongs to puts the highlight back on the
4108
+ // first item whenever a new list replaces it, without an effect to reset it.
4109
+ // A list that grows or fills in under the same `list` keeps its highlight.
4110
+ // Disabled, nothing is highlighted and the keys and pointer leave it alone.
4111
+ function useResultNavigation(items, list, enabled = true) {
4112
+ const [highlight, setHighlight] = React.useState({
4113
+ index: 0,
4114
+ list
4115
+ });
4116
+ const highlightedIndex = highlight.list === list ? highlight.index : 0;
4117
+ const highlighted = enabled ? items[highlightedIndex] : undefined;
4118
+ const {
4119
+ copiedUrl,
4120
+ copy
4121
+ } = useCopyLink();
4122
+ const highlightIndex = React.useCallback(index => {
4123
+ if (!enabled) return;
4124
+ setHighlight(current => current.list === list && current.index === index ? current : {
4125
+ index,
4126
+ list
4127
+ });
4128
+ }, [enabled, list]);
3629
4129
  const handleKeyDown = event => {
4130
+ if (!enabled) return;
3630
4131
  const step = verticalStep(event.key);
3631
- if (step && results.length > 0) {
4132
+ if (step && items.length > 0) {
3632
4133
  event.preventDefault();
3633
- highlightIndex((highlightedIndex + step + results.length) % results.length);
4134
+ highlightIndex((highlightedIndex + step + items.length) % items.length);
3634
4135
  } else if (event.key === "Enter" && highlighted && !event.nativeEvent.isComposing) {
3635
4136
  event.preventDefault();
3636
- open(highlighted.attributes.url, {
4137
+ open(highlighted.url, {
3637
4138
  newTab: hasCommandKey(event)
3638
4139
  });
3639
4140
  } else if (isCopyShortcut(event) && highlighted && !hasSelectedText(event.currentTarget)) {
3640
4141
  event.preventDefault();
3641
- const {
3642
- url
3643
- } = highlighted.attributes;
3644
- navigator.clipboard.writeText(url).then(() => setCopiedUrl(url));
4142
+ copy(highlighted.url);
3645
4143
  }
3646
4144
  };
3647
4145
  return {
3648
- copied: copiedUrl !== null && copiedUrl === (highlighted === null || highlighted === void 0 ? void 0 : highlighted.attributes.url),
4146
+ copied: copiedUrl !== null && copiedUrl === (highlighted === null || highlighted === void 0 ? void 0 : highlighted.url),
3649
4147
  handleKeyDown,
3650
4148
  highlightIndex,
3651
4149
  highlighted,
3652
4150
  highlightedIndex: highlighted ? highlightedIndex : null
3653
4151
  };
3654
4152
  }
3655
- function open(url, {
3656
- newTab
4153
+
4154
+ var styles$i = {"modal":"modal-NR3TO","mobile":"mobile-4e5rC","visuallyHidden":"visuallyHidden-C28ju","panel":"panel-HlF7S"};
4155
+
4156
+ function groupSections(groups = []) {
4157
+ return groups.map(({
4158
+ results,
4159
+ type
4160
+ }) => ({
4161
+ heading: type,
4162
+ items: results.map(resultItem),
4163
+ key: type
4164
+ }));
4165
+ }
4166
+ function resultItem({
4167
+ attributes,
4168
+ id
3657
4169
  }) {
3658
- if (newTab) {
3659
- window.open(url, "_blank", "noopener");
4170
+ return {
4171
+ category: attributes.category,
4172
+ imageUrl: attributes.image_url,
4173
+ key: id,
4174
+ product: attributes.product,
4175
+ subtitle: summarize(attributes.metadata),
4176
+ title: attributes.title,
4177
+ url: attributes.url
4178
+ };
4179
+ }
4180
+ function visitItem(visit) {
4181
+ return {
4182
+ key: entryKey(visit.entry),
4183
+ ...describeVisit(visit)
4184
+ };
4185
+ }
4186
+ function summarize(metadata) {
4187
+ return Object.values(metadata !== null && metadata !== void 0 ? metadata : {}).filter(value => typeof value === "string" && value !== "").join(" · ");
4188
+ }
4189
+ // Unlike search results, recents come from every product, so each one names
4190
+ // its product and type.
4191
+ function describeVisit(visit) {
4192
+ var _a, _b;
4193
+ const subtitle = (...parts) => parts.filter(Boolean).join(" · ");
4194
+ if (visit.status === "page") {
4195
+ const {
4196
+ app,
4197
+ metadata,
4198
+ title
4199
+ } = visit.entry;
4200
+ return {
4201
+ category: "generic",
4202
+ imageUrl: null,
4203
+ product: app,
4204
+ 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 : ""),
4205
+ title,
4206
+ url: visit.url
4207
+ };
3660
4208
  } else {
3661
- window.location.assign(url);
4209
+ const {
4210
+ category,
4211
+ image_url,
4212
+ metadata,
4213
+ product,
4214
+ title,
4215
+ type
4216
+ } = visit.result.attributes;
4217
+ return {
4218
+ category,
4219
+ imageUrl: image_url,
4220
+ product,
4221
+ subtitle: subtitle(productName(product), type, summarize(metadata)),
4222
+ title,
4223
+ url: visit.url
4224
+ };
3662
4225
  }
3663
4226
  }
3664
- const isCopyShortcut = event => event.key.toLowerCase() === "c" && !event.altKey && !event.shiftKey && hasCommandKey(event);
3665
- // With text selected, ⌘C should copy that text as usual, not the link.
3666
- function hasSelectedText(element) {
3667
- var _a, _b;
3668
- const selectedInField = element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
3669
- return selectedInField || ((_b = (_a = window.getSelection()) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : "") !== "";
4227
+ // check-ins → Check-Ins
4228
+ function productName(slug) {
4229
+ return slug.split("-").map(capitalize).join("-");
4230
+ }
4231
+ function capitalize(word) {
4232
+ return word.charAt(0).toUpperCase() + word.slice(1);
3670
4233
  }
3671
4234
 
3672
- const SEARCHABLE_PRODUCTS = [{
3673
- appName: "Groups",
3674
- slug: "groups"
3675
- }, {
3676
- appName: "People",
3677
- slug: "people"
3678
- }, {
3679
- appName: "Services",
3680
- slug: "services"
3681
- }];
3682
- const PERMISSION_CHECKS = {
3683
- people: () => fetchMe("people", "people_permissions,site_administrator").then(({
3684
- people_permissions,
3685
- site_administrator
3686
- }) => site_administrator || people_permissions !== null),
3687
- services: () => fetchMe("services", "can_search").then(({
3688
- can_search
3689
- }) => can_search)
4235
+ const STORAGE_KEY = "topbar.product_search.last_product";
4236
+ function readLastProduct() {
4237
+ try {
4238
+ return localStorage.getItem(STORAGE_KEY);
4239
+ } catch {
4240
+ return null;
4241
+ }
4242
+ }
4243
+ function saveLastProduct(slug) {
4244
+ try {
4245
+ localStorage.setItem(STORAGE_KEY, slug);
4246
+ } catch {
4247
+ // Storage can be full or blocked; search still works without it.
4248
+ }
4249
+ }
4250
+ function initialProduct(products) {
4251
+ var _a, _b, _c;
4252
+ const lastProduct = readLastProduct();
4253
+ const bySlug = slug => products.find(product => product.slug === slug);
4254
+ 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];
4255
+ }
4256
+
4257
+ var styles$h = {"container":"container-YDdM1","hint":"hint-017Rd","keycap":"keycap-ENJM2","copied":"copied-DUFa1"};
4258
+
4259
+ const SYMBOLS = {
4260
+ command: {
4261
+ label: "⌘",
4262
+ 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"]
4263
+ },
4264
+ down: {
4265
+ label: "↓",
4266
+ paths: ["M12 6l0 12", "M16 14l-4 4", "M8 14l4 4"]
4267
+ },
4268
+ return: {
4269
+ label: "↵",
4270
+ paths: ["M18 6v6a3 3 0 0 1 -3 3h-10l4 -4m0 8l-4 -4"]
4271
+ },
4272
+ up: {
4273
+ label: "↑",
4274
+ paths: ["M12 6l0 12", "M16 10l-4 -4", "M8 10l4 -4"]
4275
+ }
3690
4276
  };
3691
- function fetchMe(product, fields) {
3692
- const params = new URLSearchParams({
3693
- "fields[Person]": fields
4277
+ function Keycap(props) {
4278
+ return /*#__PURE__*/React.createElement("kbd", {
4279
+ className: styles$h.keycap
4280
+ }, "symbol" in props ? (/*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("svg", {
4281
+ "aria-hidden": true,
4282
+ fill: "none",
4283
+ stroke: "currentColor",
4284
+ strokeLinecap: "round",
4285
+ strokeLinejoin: "round",
4286
+ viewBox: "0 0 24 24"
4287
+ }, SYMBOLS[props.symbol].paths.map(d => (/*#__PURE__*/React.createElement("path", {
4288
+ d: d,
4289
+ key: d
4290
+ })))), /*#__PURE__*/React.createElement("span", {
4291
+ className: styles$i.visuallyHidden
4292
+ }, SYMBOLS[props.symbol].label))) : props.label);
4293
+ }
4294
+ function CommandKey() {
4295
+ return isMac() ? /*#__PURE__*/React.createElement(Keycap, {
4296
+ symbol: "command"
4297
+ }) : /*#__PURE__*/React.createElement(Keycap, {
4298
+ label: "Ctrl"
3694
4299
  });
3695
- return apiRequest(defaultFetch, `${window.location.origin}/~api/${product}/v2/me?${params}`).then(({
3696
- json,
3697
- ok
3698
- }) => {
3699
- if (!ok || json === null) throw new Error("Permission check failed");
3700
- return json.data.attributes;
4300
+ }
4301
+ function EnterKey() {
4302
+ return isMac() ? /*#__PURE__*/React.createElement(Keycap, {
4303
+ symbol: "return"
4304
+ }) : /*#__PURE__*/React.createElement(Keycap, {
4305
+ label: "Enter"
3701
4306
  });
3702
4307
  }
3703
- // Permissions rarely change mid-session, so a successful check is kept until
3704
- // the page reloads. A failed one is dropped so a remount tries again.
3705
- const permissionCache = new Map();
3706
- function canSearch(slug) {
3707
- const check = PERMISSION_CHECKS[slug];
3708
- const cached = permissionCache.get(slug);
3709
- if (!check) {
3710
- return Promise.resolve(true);
3711
- } else if (cached) {
3712
- return cached;
3713
- } else {
3714
- const promise = check().catch(() => {
3715
- permissionCache.delete(slug);
3716
- return false;
3717
- });
3718
- permissionCache.set(slug, promise);
3719
- return promise;
4308
+ const {
4309
+ copyLink,
4310
+ openInNewTab,
4311
+ openSearch,
4312
+ select
4313
+ } = KEYBOARD_SHORTCUTS;
4314
+ function KeyboardHints({
4315
+ copied
4316
+ }) {
4317
+ return /*#__PURE__*/React.createElement("ul", {
4318
+ "aria-label": "Keyboard shortcuts",
4319
+ className: styles$h.container
4320
+ }, /*#__PURE__*/React.createElement("li", {
4321
+ className: styles$h.hint
4322
+ }, /*#__PURE__*/React.createElement(tapestry.Flex, {
4323
+ gap: 0.5
4324
+ }, select.modifierKey, /*#__PURE__*/React.createElement(Keycap, {
4325
+ symbol: "up"
4326
+ }), /*#__PURE__*/React.createElement(Keycap, {
4327
+ symbol: "down"
4328
+ })), select.label), /*#__PURE__*/React.createElement("li", {
4329
+ className: styles$h.hint
4330
+ }, /*#__PURE__*/React.createElement(tapestry.Flex, {
4331
+ gap: 0.5
4332
+ }, /*#__PURE__*/React.createElement(CommandKey, null), /*#__PURE__*/React.createElement(EnterKey, null)), openInNewTab.label), /*#__PURE__*/React.createElement("li", {
4333
+ className: classnames(styles$h.hint, copied && styles$h.copied)
4334
+ }, /*#__PURE__*/React.createElement(tapestry.Flex, {
4335
+ gap: 0.5
4336
+ }, /*#__PURE__*/React.createElement(CommandKey, null), /*#__PURE__*/React.createElement(Keycap, {
4337
+ label: copyLink.key.toUpperCase()
4338
+ })), /*#__PURE__*/React.createElement("span", {
4339
+ role: "status"
4340
+ }, copied ? "Copied!" : copyLink.label)), /*#__PURE__*/React.createElement("li", {
4341
+ className: styles$h.hint
4342
+ }, /*#__PURE__*/React.createElement(tapestry.Flex, {
4343
+ gap: 0.5
4344
+ }, /*#__PURE__*/React.createElement(CommandKey, null), /*#__PURE__*/React.createElement(Keycap, {
4345
+ label: openSearch.key.toUpperCase()
4346
+ })), openSearch.label));
4347
+ }
4348
+
4349
+ var styles$g = {"icon":"icon-XKLvN","squircle":"squircle-JCHr6"};
4350
+
4351
+ const PRODUCTS = {
4352
+ accounts: {
4353
+ gradient: "--t-fill-color-product-accounts-gradient-brand",
4354
+ path: logomarks__namespace.accounts
4355
+ },
4356
+ api: {
4357
+ gradient: "--t-fill-color-product-api-gradient-brand",
4358
+ path: logomarks__namespace.api
4359
+ },
4360
+ calendar: {
4361
+ gradient: "--t-fill-color-product-calendar-gradient-brand",
4362
+ path: logomarks__namespace.calendar
4363
+ },
4364
+ "check-ins": {
4365
+ gradient: "--t-fill-color-product-checkins-gradient-brand",
4366
+ path: logomarks__namespace.checkIns
4367
+ },
4368
+ "church-center": {
4369
+ gradient: "--t-fill-color-product-cc-gradient-brand",
4370
+ path: logomarks__namespace.churchCenter
4371
+ },
4372
+ giving: {
4373
+ gradient: "--t-fill-color-product-giving-gradient-brand",
4374
+ path: logomarks__namespace.giving
4375
+ },
4376
+ groups: {
4377
+ gradient: "--t-fill-color-product-groups-gradient-brand",
4378
+ path: logomarks__namespace.groups
4379
+ },
4380
+ home: {
4381
+ gradient: "--t-fill-color-product-home-gradient-brand",
4382
+ path: logomarks__namespace.home
4383
+ },
4384
+ "music-stand": {
4385
+ gradient: "--t-fill-color-product-musicstand-gradient-brand",
4386
+ path: logomarks__namespace.musicStand
4387
+ },
4388
+ people: {
4389
+ gradient: "--t-fill-color-product-people-gradient-brand",
4390
+ path: logomarks__namespace.people
4391
+ },
4392
+ publishing: {
4393
+ gradient: "--t-fill-color-product-publishing-gradient-brand",
4394
+ path: logomarks__namespace.publishing
4395
+ },
4396
+ registrations: {
4397
+ gradient: "--t-fill-color-product-registrations-gradient-brand",
4398
+ path: logomarks__namespace.registrations
4399
+ },
4400
+ services: {
4401
+ gradient: "--t-fill-color-product-services-gradient-brand",
4402
+ path: logomarks__namespace.services
3720
4403
  }
4404
+ };
4405
+ const isProductIconSlug = slug => Object.prototype.hasOwnProperty.call(PRODUCTS, slug);
4406
+ 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>`)}")`;
4407
+ function ProductIcon({
4408
+ size = 16,
4409
+ slug
4410
+ }) {
4411
+ const {
4412
+ gradient,
4413
+ path
4414
+ } = PRODUCTS[slug];
4415
+ const mask = maskFor(path);
4416
+ return /*#__PURE__*/React.createElement("span", {
4417
+ "aria-hidden": true,
4418
+ className: styles$g.icon,
4419
+ style: {
4420
+ height: size,
4421
+ width: size
4422
+ }
4423
+ }, /*#__PURE__*/React.createElement("span", {
4424
+ className: styles$g.squircle,
4425
+ style: {
4426
+ WebkitMaskImage: mask,
4427
+ background: `var(${gradient})`,
4428
+ maskImage: mask
4429
+ }
4430
+ }));
3721
4431
  }
3722
- function useSearchableProducts(apps) {
3723
- // Keyed by name because providers hand over a fresh `apps` array on renders
3724
- // where nothing changed.
3725
- const accessibleKey = inSwitcherOrder(apps).map(({
4432
+
4433
+ var styles$f = {"picker":"picker-iuzzj","option":"option--7LXg"};
4434
+
4435
+ // Phones have no room for a tab per product, so they pick from a dropdown.
4436
+ // Complex mode renders each option's logomark, in the list and the trigger.
4437
+ function ProductSelect({
4438
+ children,
4439
+ onSelect,
4440
+ products,
4441
+ selected
4442
+ }) {
4443
+ const options = products.map(({
4444
+ appName,
3726
4445
  slug
3727
- }) => slug).join(",");
3728
- const [settled, setSettled] = React.useState();
3729
- React.useEffect(() => {
3730
- let current = true;
3731
- const accessible = accessibleKey.split(",").flatMap(slug => SEARCHABLE_PRODUCTS.filter(product => product.slug === slug));
3732
- Promise.all(accessible.map(({
3733
- slug
3734
- }) => canSearch(slug))).then(allowed => {
3735
- if (current) {
3736
- setSettled({
3737
- accessibleKey,
3738
- products: accessible.filter((_, index) => allowed[index])
3739
- });
4446
+ }) => ({
4447
+ label: (/*#__PURE__*/React.createElement("span", {
4448
+ className: styles$f.option
4449
+ }, /*#__PURE__*/React.createElement(ProductIcon, {
4450
+ slug: slug
4451
+ }), appName)),
4452
+ textValue: appName,
4453
+ value: slug
4454
+ }));
4455
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
4456
+ className: styles$f.picker
4457
+ }, /*#__PURE__*/React.createElement(tapestry.Select, {
4458
+ "aria-label": "Product",
4459
+ complex: true,
4460
+ onChange: event => onSelect(event.target.value),
4461
+ options: options,
4462
+ value: selected.slug
4463
+ })), /*#__PURE__*/React.createElement("div", {
4464
+ className: styles$i.panel
4465
+ }, children));
4466
+ }
4467
+
4468
+ var styles$e = {"tablist":"tablist-7ASvc","tab":"tab-dz8ki"};
4469
+
4470
+ function ProductTabs({
4471
+ children,
4472
+ onKeyDown,
4473
+ onSelect,
4474
+ products,
4475
+ selected
4476
+ }) {
4477
+ const id = useInstanceId("product-tabs");
4478
+ const tabId = slug => `${id}-tab-${slug}`;
4479
+ const tabRefs = React.useRef(new Map());
4480
+ const handleKeyDown = event => {
4481
+ var _a;
4482
+ const offset = horizontalStep(event.key);
4483
+ if (offset) {
4484
+ event.preventDefault();
4485
+ const index = products.indexOf(selected);
4486
+ const next = products[(index + offset + products.length) % products.length];
4487
+ onSelect(next.slug);
4488
+ (_a = tabRefs.current.get(next.slug)) === null || _a === void 0 ? void 0 : _a.focus();
4489
+ } else {
4490
+ onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
4491
+ }
4492
+ };
4493
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
4494
+ "aria-label": "Product",
4495
+ className: styles$e.tablist,
4496
+ onKeyDown: handleKeyDown,
4497
+ role: "tablist"
4498
+ }, products.map(product => (/*#__PURE__*/React.createElement("button", {
4499
+ "aria-controls": `${id}-panel`,
4500
+ "aria-selected": product === selected,
4501
+ className: styles$e.tab,
4502
+ id: tabId(product.slug),
4503
+ key: product.slug,
4504
+ onClick: () => onSelect(product.slug),
4505
+ ref: element => {
4506
+ if (element) {
4507
+ tabRefs.current.set(product.slug, element);
4508
+ } else {
4509
+ tabRefs.current.delete(product.slug);
3740
4510
  }
4511
+ },
4512
+ role: "tab",
4513
+ tabIndex: product === selected ? 0 : -1,
4514
+ type: "button"
4515
+ }, /*#__PURE__*/React.createElement(ProductIcon, {
4516
+ slug: product.slug
4517
+ }), product === selected ? product.appName : (/*#__PURE__*/React.createElement("span", {
4518
+ className: styles$i.visuallyHidden
4519
+ }, product.appName)))))), /*#__PURE__*/React.createElement("div", {
4520
+ "aria-labelledby": tabId(selected.slug),
4521
+ className: styles$i.panel,
4522
+ id: `${id}-panel`,
4523
+ role: "tabpanel"
4524
+ }, children));
4525
+ }
4526
+
4527
+ function EndOfResultsObserver({
4528
+ onVisible
4529
+ }) {
4530
+ const ref = React.useRef(null);
4531
+ React.useEffect(() => {
4532
+ const observer = new IntersectionObserver(([entry]) => {
4533
+ if (entry.isIntersecting) onVisible();
3741
4534
  });
3742
- return () => {
3743
- current = false;
3744
- };
3745
- }, [accessibleKey]);
3746
- if ((settled === null || settled === void 0 ? void 0 : settled.accessibleKey) === accessibleKey) {
3747
- return {
3748
- products: settled.products,
3749
- status: "done"
3750
- };
3751
- } else {
3752
- return {
3753
- status: "loading"
3754
- };
3755
- }
4535
+ if (ref.current) observer.observe(ref.current);
4536
+ return () => observer.disconnect();
4537
+ }, [onVisible]);
4538
+ return /*#__PURE__*/React.createElement("div", {
4539
+ ref: ref
4540
+ });
3756
4541
  }
3757
- function inSwitcherOrder(apps) {
3758
- return groupApps(apps).restProducts.flatMap(app => SEARCHABLE_PRODUCTS.filter(product => product.appName === app.name));
4542
+
4543
+ const resultId = (listboxId, index) => `${listboxId}-result-${index}`;
4544
+
4545
+ var styles$d = {"result":"result-Thq2C","title":"title-KYUEn","subtitle":"subtitle-R2haq","image":"image-KVDor","avatar":"avatar-6Gh62","photo":"photo-FZR3Q","badge":"badge-trUKt"};
4546
+
4547
+ function SearchResult({
4548
+ category,
4549
+ highlighted,
4550
+ id,
4551
+ imageUrl,
4552
+ onHighlight,
4553
+ product,
4554
+ subtitle,
4555
+ title,
4556
+ url
4557
+ }) {
4558
+ const ref = React.useRef(null);
4559
+ // A hovered result is already under the pointer, and scrolling it clear of
4560
+ // the panel's scroll padding would slide the list out from under the mouse.
4561
+ const highlightedByPointer = React.useRef(false);
4562
+ React.useEffect(() => {
4563
+ var _a;
4564
+ if (!highlighted) return;
4565
+ if (highlightedByPointer.current) {
4566
+ highlightedByPointer.current = false;
4567
+ } else {
4568
+ (_a = ref.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({
4569
+ block: "nearest"
4570
+ });
4571
+ }
4572
+ }, [highlighted]);
4573
+ const highlightFromPointer = () => {
4574
+ if (highlighted) return;
4575
+ highlightedByPointer.current = true;
4576
+ onHighlight();
4577
+ };
4578
+ // Focus stays in the search input, which points at the highlighted result
4579
+ // with aria-activedescendant, so results are kept out of the tab order.
4580
+ // Mouse move rather than enter: arrowing scrolls results under a resting
4581
+ // pointer, and that shouldn't steal the highlight.
4582
+ return /*#__PURE__*/React.createElement("a", {
4583
+ "aria-selected": highlighted,
4584
+ className: styles$d.result,
4585
+ href: url,
4586
+ id: id,
4587
+ onMouseMove: highlightFromPointer,
4588
+ ref: ref,
4589
+ role: "option",
4590
+ tabIndex: -1
4591
+ }, /*#__PURE__*/React.createElement(ResultImage, {
4592
+ category: category,
4593
+ imageUrl: imageUrl,
4594
+ product: product
4595
+ }), /*#__PURE__*/React.createElement("span", {
4596
+ className: styles$d.title
4597
+ }, title), /*#__PURE__*/React.createElement("span", {
4598
+ className: styles$d.subtitle
4599
+ }, subtitle));
4600
+ }
4601
+ // A person's photo or the result's own image, badged with its product, or
4602
+ // just the product's logomark when there's no image.
4603
+ function ResultImage({
4604
+ category,
4605
+ imageUrl,
4606
+ product
4607
+ }) {
4608
+ const icon = size => isProductIconSlug(product) ? (/*#__PURE__*/React.createElement(ProductIcon, {
4609
+ size: size,
4610
+ slug: product
4611
+ })) : null;
4612
+ return /*#__PURE__*/React.createElement("span", {
4613
+ "aria-hidden": true,
4614
+ className: styles$d.image
4615
+ }, imageUrl ? (/*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("img", {
4616
+ alt: "",
4617
+ className: category === "profile" ? styles$d.avatar : styles$d.photo,
4618
+ loading: "lazy",
4619
+ src: imageUrl
4620
+ }), /*#__PURE__*/React.createElement("span", {
4621
+ className: styles$d.badge
4622
+ }, icon(16)))) : icon("100%"));
3759
4623
  }
3760
4624
 
3761
- const isSearchShortcut = event => event.key.toLowerCase() === "k" && !event.altKey && !event.shiftKey && hasCommandKey(event);
3762
- // Products opened their own search with `/` before this one replaced it.
3763
- const isLegacySearchShortcut = event => event.key === "/" && !event.altKey && !event.ctrlKey && !event.metaKey && !isTypingIn(event.target);
3764
- const isTypingKey = event => event.key.length === 1 && event.key !== " " && !event.altKey && !event.ctrlKey && !event.metaKey;
3765
- const isTypingIn = target => target instanceof Element && target.closest('input, select, textarea, [contenteditable]:not([contenteditable="false"])') !== null;
3766
- function ProductSearch({
3767
- apps
4625
+ var styles$c = {"message":"message-1-PP-","heading":"heading-LioUF"};
4626
+
4627
+ // Sections with a heading render as labelled groups; ones without render
4628
+ // their items straight into the listbox. Indexes run across every section.
4629
+ function ResultListbox({
4630
+ busy,
4631
+ highlightedIndex,
4632
+ id,
4633
+ label,
4634
+ labelledBy,
4635
+ onHighlight,
4636
+ sections
3768
4637
  }) {
3769
- const searchable = useSearchableProducts(apps);
3770
- if (searchable.status === "done" && searchable.products.length > 0) {
3771
- return /*#__PURE__*/React.createElement(SearchButtonAndDialog, {
3772
- products: searchable.products
4638
+ let index = 0;
4639
+ const renderItem = item => {
4640
+ const itemIndex = index++;
4641
+ return /*#__PURE__*/React.createElement(SearchResult, {
4642
+ category: item.category,
4643
+ highlighted: itemIndex === highlightedIndex,
4644
+ id: resultId(id, itemIndex),
4645
+ imageUrl: item.imageUrl,
4646
+ key: item.key,
4647
+ onHighlight: () => onHighlight(itemIndex),
4648
+ product: item.product,
4649
+ subtitle: item.subtitle,
4650
+ title: item.title,
4651
+ url: item.url
3773
4652
  });
4653
+ };
4654
+ return /*#__PURE__*/React.createElement("div", {
4655
+ "aria-busy": busy,
4656
+ "aria-label": label,
4657
+ "aria-labelledby": labelledBy,
4658
+ id: id,
4659
+ role: "listbox"
4660
+ }, sections.map((section, sectionIndex) => {
4661
+ if (section.heading === undefined) {
4662
+ return /*#__PURE__*/React.createElement(React.Fragment, {
4663
+ key: section.key
4664
+ }, section.items.map(renderItem));
4665
+ }
4666
+ const headingId = `${id}-group-${sectionIndex}`;
4667
+ return /*#__PURE__*/React.createElement("div", {
4668
+ "aria-labelledby": headingId,
4669
+ key: section.key,
4670
+ role: "group"
4671
+ }, /*#__PURE__*/React.createElement("div", {
4672
+ className: styles$c.heading,
4673
+ id: headingId
4674
+ }, section.heading), section.items.map(renderItem));
4675
+ }));
4676
+ }
4677
+
4678
+ function RecentlyVisited({
4679
+ count,
4680
+ hasMore,
4681
+ items,
4682
+ loading,
4683
+ showMore,
4684
+ ...navigation
4685
+ }) {
4686
+ const headingId = `${navigation.id}-heading`;
4687
+ if (items.length === 0 && !hasMore && !loading) {
4688
+ return /*#__PURE__*/React.createElement("p", {
4689
+ className: styles$c.message
4690
+ }, "Pages you visit will show up here.");
3774
4691
  } else {
3775
- return null;
4692
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
4693
+ className: styles$c.heading,
4694
+ id: headingId
4695
+ }, "Recently visited"), /*#__PURE__*/React.createElement(ResultListbox, {
4696
+ labelledBy: headingId,
4697
+ sections: [{
4698
+ items,
4699
+ key: "recents"
4700
+ }],
4701
+ ...navigation
4702
+ }), hasMore && /*#__PURE__*/React.createElement(EndOfResultsObserver, {
4703
+ key: count,
4704
+ onVisible: showMore
4705
+ }));
3776
4706
  }
3777
4707
  }
3778
- function SearchButtonAndDialog({
3779
- products
4708
+
4709
+ var styles$b = {"header":"header-sXEyG","input":"input-YQkTC","clearButton":"clearButton-9Yh9w"};
4710
+
4711
+ // A combobox: focus stays here while aria-activedescendant points screen
4712
+ // readers at the highlighted result in the listbox it controls.
4713
+ function SearchInput({
4714
+ expanded,
4715
+ highlightedIndex,
4716
+ inputRef,
4717
+ listboxId,
4718
+ onChange,
4719
+ onFocus,
4720
+ onKeyDown,
4721
+ value
3780
4722
  }) {
3781
- const [open, setOpen] = React.useState(false);
3782
- React.useEffect(() => {
3783
- // Rich text editors like Trix bind ⌘K to "insert link"; capturing at the
3784
- // window and stopping propagation keeps them from ever seeing it.
3785
- const handleKeyDown = event => {
3786
- if (isSearchShortcut(event)) {
3787
- event.preventDefault();
3788
- event.stopPropagation();
3789
- setOpen(wasOpen => !wasOpen);
3790
- } else if (isLegacySearchShortcut(event)) {
3791
- event.preventDefault();
3792
- event.stopPropagation();
3793
- setOpen(true);
3794
- }
3795
- };
3796
- window.addEventListener("keydown", handleKeyDown, {
3797
- capture: true
3798
- });
3799
- return () => window.removeEventListener("keydown", handleKeyDown, {
3800
- capture: true
3801
- });
3802
- }, []);
3803
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("button", {
3804
- "aria-keyshortcuts": `${isMac() ? "Meta+K" : "Control+K"} /`,
4723
+ return /*#__PURE__*/React.createElement("label", {
4724
+ className: styles$b.header
4725
+ }, /*#__PURE__*/React.createElement(Icon, {
4726
+ name: "general.search",
4727
+ size: 20,
4728
+ theme: "--t-icon-color-dim"
4729
+ }), /*#__PURE__*/React.createElement("input", {
4730
+ "aria-activedescendant": highlightedIndex === null ? undefined : resultId(listboxId, highlightedIndex),
4731
+ "aria-autocomplete": "list",
4732
+ "aria-controls": expanded ? listboxId : undefined,
4733
+ "aria-expanded": expanded,
3805
4734
  "aria-label": "Search",
3806
- onClick: () => setOpen(true),
4735
+ autoFocus: true,
4736
+ className: styles$b.input,
4737
+ onChange: event => onChange(event.target.value),
4738
+ onFocus: onFocus,
4739
+ onKeyDown: onKeyDown,
4740
+ placeholder: "Search",
4741
+ ref: inputRef,
4742
+ role: "combobox",
4743
+ type: "search",
4744
+ value: value
4745
+ }), value !== "" && (/*#__PURE__*/React.createElement("button", {
4746
+ "aria-label": "Clear search",
4747
+ className: styles$b.clearButton,
4748
+ onClick: () => {
4749
+ var _a;
4750
+ onChange("");
4751
+ (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
4752
+ },
4753
+ tabIndex: -1,
3807
4754
  type: "button"
3808
- }, /*#__PURE__*/React.createElement(Spyglass, {
3809
- fill: "currentColor"
3810
- })), /*#__PURE__*/React.createElement(tapestry.Modal, {
3811
- "aria-label": "Search",
3812
- dismissible: true,
3813
- onClose: () => setOpen(false),
3814
- open: open
3815
- }, /*#__PURE__*/React.createElement(SearchPanel, {
3816
- products: products
3817
- })));
4755
+ }, /*#__PURE__*/React.createElement(Icon, {
4756
+ name: "general.smallX"
4757
+ }))));
4758
+ }
4759
+
4760
+ function SearchResults({
4761
+ label,
4762
+ sections,
4763
+ status,
4764
+ term,
4765
+ ...navigation
4766
+ }) {
4767
+ switch (status) {
4768
+ case "idle":
4769
+ return null;
4770
+ case "loading":
4771
+ if (sections.length > 0) {
4772
+ return /*#__PURE__*/React.createElement(ResultListbox, {
4773
+ busy: true,
4774
+ label: label,
4775
+ sections: sections,
4776
+ ...navigation
4777
+ });
4778
+ }
4779
+ return /*#__PURE__*/React.createElement("p", {
4780
+ className: styles$c.message,
4781
+ role: "status"
4782
+ }, "Searching\u2026");
4783
+ case "error":
4784
+ return /*#__PURE__*/React.createElement("p", {
4785
+ className: styles$c.message,
4786
+ role: "alert"
4787
+ }, "Search isn\u2019t working right now. Try again.");
4788
+ case "done":
4789
+ if (sections.length === 0) {
4790
+ return /*#__PURE__*/React.createElement("p", {
4791
+ className: styles$c.message,
4792
+ role: "status"
4793
+ }, "No results for \u201C", term.trim(), "\u201D");
4794
+ }
4795
+ return /*#__PURE__*/React.createElement(ResultListbox, {
4796
+ busy: false,
4797
+ label: label,
4798
+ sections: sections,
4799
+ ...navigation
4800
+ });
4801
+ }
3818
4802
  }
4803
+
4804
+ // Keys the recents list for navigation, so the highlight
4805
+ // stays put as more recents load and as their lookups answer.
4806
+ const RECENTS = "recents";
3819
4807
  function SearchPanel({
4808
+ mobile = false,
4809
+ env,
3820
4810
  products
3821
4811
  }) {
3822
4812
  var _a, _b;
@@ -3829,11 +4819,15 @@ function SearchPanel({
3829
4819
  const hasTerm = term.trim() !== "";
3830
4820
  const search = useProductSearch(product.slug, term);
3831
4821
  const groups = "groups" in search ? search.groups : undefined;
3832
- const results = React.useMemo(() => {
3833
- var _a;
3834
- return (_a = groups === null || groups === void 0 ? void 0 : groups.flatMap(group => group.results)) !== null && _a !== void 0 ? _a : [];
3835
- }, [groups]);
3836
- const navigation = useResultNavigation(results);
4822
+ const sections = React.useMemo(() => groupSections(groups), [groups]);
4823
+ const {
4824
+ visits,
4825
+ ...recents
4826
+ } = useRecentVisits(env);
4827
+ const recentItems = visits.map(visitItem);
4828
+ const items = hasTerm ? sections.flatMap(section => section.items) : recentItems;
4829
+ // Phones open results by tapping them, with no highlight to arrow through.
4830
+ const navigation = useResultNavigation(items, hasTerm ? sections : RECENTS, !mobile);
3837
4831
  const {
3838
4832
  highlightedIndex
3839
4833
  } = navigation;
@@ -3856,296 +4850,269 @@ function SearchPanel({
3856
4850
  React.useEffect(() => {
3857
4851
  if (hasTerm) saveLastProduct(product.slug);
3858
4852
  }, [hasTerm, product.slug]);
3859
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("input", {
3860
- "aria-activedescendant": highlightedIndex === null ? undefined : resultId(listboxId, highlightedIndex),
3861
- "aria-autocomplete": "list",
3862
- "aria-controls": results.length > 0 ? listboxId : undefined,
3863
- "aria-expanded": results.length > 0,
3864
- "aria-label": "Search",
3865
- autoFocus: true,
3866
- onChange: event => setTerm(event.target.value),
4853
+ const results = hasTerm ? (/*#__PURE__*/React.createElement(SearchResults, {
4854
+ highlightedIndex: highlightedIndex,
4855
+ id: listboxId,
4856
+ label: `${product.appName} results`,
4857
+ onHighlight: navigation.highlightIndex,
4858
+ sections: sections,
4859
+ status: search.status,
4860
+ term: term
4861
+ })) : (/*#__PURE__*/React.createElement(RecentlyVisited, {
4862
+ highlightedIndex: highlightedIndex,
4863
+ id: listboxId,
4864
+ items: recentItems,
4865
+ onHighlight: navigation.highlightIndex,
4866
+ ...recents
4867
+ }));
4868
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SearchInput, {
4869
+ expanded: items.length > 0,
4870
+ highlightedIndex: highlightedIndex,
4871
+ inputRef: inputRef,
4872
+ listboxId: listboxId,
4873
+ onChange: setTerm,
3867
4874
  onFocus: () => setAnnounceHighlight(false),
3868
4875
  onKeyDown: navigation.handleKeyDown,
3869
- placeholder: "Search",
3870
- ref: inputRef,
3871
- role: "combobox",
3872
- type: "search",
3873
4876
  value: term
3874
- }), /*#__PURE__*/React.createElement(ProductTabs, {
3875
- onKeyDown: handleTabsKeyDown,
4877
+ }), mobile ? (/*#__PURE__*/React.createElement(ProductSelect, {
3876
4878
  onSelect: setSelectedSlug,
3877
4879
  products: products,
3878
4880
  selected: product
3879
- }, /*#__PURE__*/React.createElement(SearchResults, {
3880
- highlightedIndex: highlightedIndex,
3881
- id: listboxId,
3882
- label: `${product.appName} results`,
3883
- onHighlight: navigation.highlightIndex,
3884
- search: search,
3885
- term: term
3886
- })), /*#__PURE__*/React.createElement("div", {
4881
+ }, results)) : (/*#__PURE__*/React.createElement(ProductTabs, {
4882
+ onKeyDown: handleTabsKeyDown,
4883
+ onSelect: setSelectedSlug,
4884
+ products: products,
4885
+ selected: product
4886
+ }, results)), /*#__PURE__*/React.createElement("div", {
3887
4887
  "aria-live": "polite",
3888
- className: styles$c.visuallyHidden
3889
- }, announceHighlight ? (_b = navigation.highlighted) === null || _b === void 0 ? void 0 : _b.attributes.title : null), /*#__PURE__*/React.createElement(KeyboardHints, {
4888
+ className: styles$i.visuallyHidden
4889
+ }, announceHighlight ? (_b = navigation.highlighted) === null || _b === void 0 ? void 0 : _b.title : null), mobile ? null : /*#__PURE__*/React.createElement(KeyboardHints, {
3890
4890
  copied: navigation.copied
3891
4891
  }));
3892
4892
  }
3893
- function KeyboardHints({
3894
- copied
3895
- }) {
3896
- const command = commandKeyLabel();
3897
- 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", {
3898
- role: "status"
3899
- }, copied ? "Copied!" : "Copy link"), " \u00B7 ", command, "K Open search");
3900
- }
3901
- function initialProduct(products) {
3902
- var _a, _b, _c;
3903
- const lastProduct = readLastProduct();
3904
- const bySlug = slug => products.find(product => product.slug === slug);
3905
- 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];
3906
- }
3907
4893
 
3908
- function parseValue(element) {
3909
- const datatype = element.getAttribute("datatype");
3910
- switch (datatype) {
3911
- case "boolean":
3912
- return element.content === "true" || element.content === "1";
3913
- default:
3914
- return element.content;
3915
- }
3916
- }
3917
- function getAllPcoMeta() {
3918
- const elements = document.head.querySelectorAll(`meta[property^="pco:"]`);
3919
- const meta = {};
3920
- elements.forEach(e => {
3921
- const property = e.getAttribute("property");
3922
- meta[property.substring("pco:".length)] = parseValue(e);
3923
- });
3924
- return meta;
4894
+ // Products open modals from many libraries, not all of which mark themselves
4895
+ // with roles, so a modal is recognized by what it does: cover the page.
4896
+ function isModalOpen() {
4897
+ const {
4898
+ clientHeight,
4899
+ clientWidth
4900
+ } = document.documentElement;
4901
+ const element = document.elementFromPoint(clientWidth / 2, clientHeight / 2);
4902
+ return element !== null && (isInNativeDialog(element) || hasBackdrop(element, clientWidth, clientHeight));
3925
4903
  }
3926
- // usePcoMeta only reads each tag's content. Recently Visited also needs the
3927
- // pco:recently_visitable tag's `metadata` attribute, which can change while
3928
- // the content stays the same, so it is tracked in its own store.
3929
- function getRecentlyVisitableMetadata() {
3930
- var _a, _b;
3931
- 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;
4904
+ // A native modal's backdrop is drawn by the browser rather than being an
4905
+ // element; hit-testing it lands on the dialog itself.
4906
+ function isInNativeDialog(element) {
4907
+ return element.closest("dialog[open]") !== null;
3932
4908
  }
3933
- const store = createStore(getAllPcoMeta());
3934
- const recentlyVisitableMetadataStore = createStore({
3935
- metadata: getRecentlyVisitableMetadata()
3936
- });
3937
- function sync() {
3938
- const nextState = getAllPcoMeta();
3939
- if (!shallowCompare(store.getState(), nextState)) store.setState(nextState);
3940
- const metadata = getRecentlyVisitableMetadata();
3941
- if (recentlyVisitableMetadataStore.getState().metadata !== metadata) {
3942
- recentlyVisitableMetadataStore.setState({
3943
- metadata
3944
- });
4909
+ function hasBackdrop(element, width, height) {
4910
+ let current = element;
4911
+ while (current && !isBackdrop(current, width, height)) {
4912
+ current = current.parentElement;
3945
4913
  }
4914
+ return current !== null;
3946
4915
  }
3947
- const observer = new MutationObserver(sync);
3948
- let hasTriggeredObserve = false;
3949
- function observe() {
3950
- if (hasTriggeredObserve) return;
3951
- hasTriggeredObserve = true;
3952
- // The module-load snapshot above can predate meta tags that appear later
3953
- // in <head> (e.g. pco:resource, yielded right before </head>) if this
3954
- // module's own script tag isn't deferred and runs mid-parse. Those tags
3955
- // never mutate afterward, so the observer alone would never catch them.
3956
- // Re-read once now, since observe() is only called once a consumer is
3957
- // actually mounting, which is always after the document has fully parsed.
3958
- sync();
3959
- observer.observe(document.head, {
3960
- attributes: true,
3961
- childList: true,
3962
- subtree: true
3963
- });
3964
- }
3965
- function usePcoMeta() {
3966
- observe();
3967
- return shim.useSyncExternalStore(store.subscribe, () => store.getState());
4916
+ // Some backdrops start below the topbar rather than at the top of the screen,
4917
+ // so the layer only has to span the width and reach from the center to the
4918
+ // bottom. Its own box has to reach the center: a bar along the bottom can
4919
+ // hold a panel that pops up over the center, like Services' media player.
4920
+ // react-aria leaves a closed modal's backdrop up while it fades out, search's
4921
+ // own included, so one that's on its way out doesn't count.
4922
+ function isBackdrop(element, width, height) {
4923
+ const rect = element.getBoundingClientRect();
4924
+ return !element.matches("[data-exiting]") && getComputedStyle(element).position === "fixed" && rect.width >= width - SCROLLBAR_SLACK && rect.top <= height / 2 && rect.bottom >= height - ROUNDING_SLACK;
3968
4925
  }
3969
- function useRecentlyVisitableMetadata() {
3970
- observe();
3971
- return shim.useSyncExternalStore(recentlyVisitableMetadataStore.subscribe, () => recentlyVisitableMetadataStore.getState().metadata);
4926
+ // react-aria keeps a classic scrollbar's gutter reserved while its modals lock
4927
+ // scrolling, so their backdrops stop short of the right edge by its width.
4928
+ const SCROLLBAR_SLACK = 24;
4929
+ // Rects are fractional at non-100% zoom, but clientWidth and clientHeight are
4930
+ // rounded to whole pixels.
4931
+ const ROUNDING_SLACK = 1;
4932
+
4933
+ function useSearchShortcut(enabled, open, setOpen) {
4934
+ React.useEffect(() => {
4935
+ if (!enabled) return undefined;
4936
+ // Rich text editors like Trix bind ⌘K to "insert link"; capturing at the
4937
+ // window and stopping propagation keeps them from ever seeing it. Under a
4938
+ // product's modal, search stays shut rather than stacking a second modal,
4939
+ // and the keystroke goes on to the product.
4940
+ const handleKeyDown = event => {
4941
+ if (open && isSearchShortcut(event)) {
4942
+ event.preventDefault();
4943
+ event.stopPropagation();
4944
+ setOpen(false);
4945
+ } else if (!open && (isSearchShortcut(event) || isLegacySearchShortcut(event)) && !isModalOpen()) {
4946
+ event.preventDefault();
4947
+ event.stopPropagation();
4948
+ setOpen(true);
4949
+ }
4950
+ };
4951
+ window.addEventListener("keydown", handleKeyDown, {
4952
+ capture: true
4953
+ });
4954
+ return () => window.removeEventListener("keydown", handleKeyDown, {
4955
+ capture: true
4956
+ });
4957
+ }, [enabled, open, setOpen]);
3972
4958
  }
3973
4959
 
3974
- const MAX_ENTRIES = 20;
3975
- const FORMAT_VERSION = 1;
3976
- const MAX_TEXT_LENGTH = 200;
3977
- const PRODUCTS = ["accounts", "calendar", "check-ins", "giving", "groups", "home", "login", "people", "publishing", "registrations", "services"];
3978
- function isId(value) {
3979
- return value !== "" && Array.from(value).every(character => character >= "0" && character <= "9");
3980
- }
3981
- // A PCRL looks like `<product>/v2/<resource_type>/<id>`.
3982
- function isPcrl(value) {
3983
- const segments = value.split("/");
3984
- return segments.length === 4 && PRODUCTS.some(product => product === segments[0]) && isId(segments[3]);
3985
- }
3986
- function isNonBlankString(value) {
3987
- return typeof value === "string" && value.trim() !== "";
3988
- }
3989
- function isRecord(value) {
3990
- return typeof value === "object" && value !== null && !Array.isArray(value);
3991
- }
3992
- function entryKey(entry) {
3993
- if (entry.type === "resource") {
3994
- return `resource:${entry.pcrl}`;
3995
- } else {
3996
- return `page:${entry.product}:${entry.path}`;
3997
- }
3998
- }
3999
- function isEntry(value) {
4000
- if (!isRecord(value)) {
4001
- return false;
4002
- } else if (value.type === "resource") {
4003
- return typeof value.pcrl === "string" && isPcrl(value.pcrl);
4004
- } else if (value.type === "page") {
4005
- return isNonBlankString(value.product) && isNonBlankString(value.title) && isNonBlankString(value.path) && typeof value.details === "string";
4006
- } else {
4007
- return false;
4008
- }
4009
- }
4010
- // Cuts on code points so a surrogate pair is never split.
4011
- function clampText(value) {
4012
- return Array.from(value).slice(0, MAX_TEXT_LENGTH).join("");
4960
+ const PERMISSION_CHECKS = {
4961
+ "check-ins": () => fetchMe("check-ins", "permission").then(({
4962
+ permission
4963
+ }) => permission !== null),
4964
+ giving: personId => {
4965
+ if (personId === undefined) {
4966
+ return Promise.reject(new Error("No person to check Giving access for"));
4967
+ } else {
4968
+ return fetchAttributes(`giving/v2/person_permissions/${personId}`, {
4969
+ "fields[PersonPermissions]": "can_search"
4970
+ }).then(({
4971
+ can_search
4972
+ }) => can_search);
4973
+ }
4974
+ },
4975
+ people: () => fetchMe("people", "people_permissions,site_administrator").then(({
4976
+ people_permissions,
4977
+ site_administrator
4978
+ }) => site_administrator || people_permissions !== null),
4979
+ registrations: () => fetchMe("registrations", "can_search").then(({
4980
+ can_search
4981
+ }) => can_search),
4982
+ services: () => fetchMe("services", "can_search").then(({
4983
+ can_search
4984
+ }) => can_search)
4985
+ };
4986
+ function fetchMe(product, fields) {
4987
+ return fetchAttributes(`${product}/v2/me`, {
4988
+ "fields[Person]": fields
4989
+ });
4013
4990
  }
4014
- function clampEntry(entry) {
4015
- if (entry.type === "resource") {
4016
- return entry;
4017
- } else {
4018
- return {
4019
- ...entry,
4020
- details: clampText(entry.details),
4021
- title: clampText(entry.title)
4022
- };
4023
- }
4991
+ function fetchAttributes(path, params) {
4992
+ const url = `${window.location.origin}/~api/${path}?${new URLSearchParams(params)}`;
4993
+ return apiRequest(defaultFetch, url, {
4994
+ headers: apiVersionHeaders(url)
4995
+ }).then(({
4996
+ json,
4997
+ ok
4998
+ }) => {
4999
+ if (!ok || json === null) throw new Error("Permission check failed");
5000
+ return json.data.attributes;
5001
+ });
4024
5002
  }
4025
- // Recents are disposable: anything unexpected reads as an empty list (or, for
4026
- // a single bad entry, a list without it) rather than throwing.
4027
- function parseRecentlyVisited(value, personId) {
4028
- let parsed = null;
4029
- try {
4030
- parsed = JSON.parse(value !== null && value !== void 0 ? value : "null");
4031
- } catch {
4032
- parsed = null;
4033
- }
4034
- if (isRecord(parsed) && parsed.v === FORMAT_VERSION && parsed.person === personId && Array.isArray(parsed.entries)) {
4035
- return parsed.entries.filter(isEntry);
5003
+ // Permissions rarely change mid-session, so a successful check is kept until
5004
+ // the page reloads. A failed one is dropped so a remount tries again.
5005
+ const permissionCache = new Map();
5006
+ function canSearch(slug, personId) {
5007
+ const check = PERMISSION_CHECKS[slug];
5008
+ const cached = permissionCache.get(slug);
5009
+ if (!check) {
5010
+ return Promise.resolve(true);
5011
+ } else if (cached) {
5012
+ return cached;
4036
5013
  } else {
4037
- return [];
5014
+ const promise = check(personId).catch(() => {
5015
+ permissionCache.delete(slug);
5016
+ return false;
5017
+ });
5018
+ permissionCache.set(slug, promise);
5019
+ return promise;
4038
5020
  }
4039
5021
  }
4040
- function addVisit(entries, visit) {
4041
- const key = entryKey(visit);
4042
- return [clampEntry(visit), ...entries.filter(entry => entryKey(entry) !== key)].slice(0, MAX_ENTRIES);
4043
- }
4044
- function serializeRecentlyVisited(entries, personId) {
4045
- return JSON.stringify({
4046
- entries,
4047
- person: personId,
4048
- v: FORMAT_VERSION
4049
- });
4050
- }
4051
5022
 
4052
- // The product whose URL, in this environment, has the current page's host.
4053
- // Reversing pcoUrl handles every environment @planningcenter/url knows about,
4054
- // and guarantees the stored product rebuilds a working link.
4055
- function currentProduct() {
4056
- var _a;
4057
- return (_a = PRODUCTS.find(product => new URL(pcoUrl$1.pcoUrl(product)).hostname === window.location.hostname)) !== null && _a !== void 0 ? _a : null;
4058
- }
4059
- function detailsFromMetadata(metadata) {
4060
- var _a;
4061
- try {
4062
- const details = (_a = JSON.parse(metadata !== null && metadata !== void 0 ? metadata : "{}")) === null || _a === void 0 ? void 0 : _a.details;
4063
- return typeof details === "string" ? details : "";
4064
- } catch {
4065
- return "";
4066
- }
4067
- }
4068
- // pco:resource is the more specific signal, so it wins when both tags are
4069
- // present. A resource that isn't a PCRL is ignored rather than recorded.
4070
- function visitFromPage({
4071
- metadata,
4072
- resource,
4073
- title
4074
- }) {
4075
- const product = currentProduct();
4076
- if (resource && isPcrl(resource)) {
4077
- return {
4078
- pcrl: resource,
4079
- type: "resource"
5023
+ function useSearchableProducts(apps) {
5024
+ // Keyed by name because providers hand over a fresh `apps` array on renders
5025
+ // where nothing changed.
5026
+ const accessibleKey = inSwitcherOrder(apps).map(({
5027
+ slug
5028
+ }) => slug).join(",");
5029
+ const personId = useCurrentPersonId();
5030
+ const [settled, setSettled] = React.useState();
5031
+ React.useEffect(() => {
5032
+ let current = true;
5033
+ const accessible = accessibleKey.split(",").flatMap(slug => SEARCHABLE_PRODUCTS.filter(product => product.slug === slug));
5034
+ Promise.all(accessible.map(({
5035
+ slug
5036
+ }) => canSearch(slug, personId))).then(allowed => {
5037
+ if (current) {
5038
+ setSettled({
5039
+ accessibleKey,
5040
+ personId,
5041
+ products: accessible.filter((_, index) => allowed[index])
5042
+ });
5043
+ }
5044
+ });
5045
+ return () => {
5046
+ current = false;
4080
5047
  };
4081
- } else if (title && title.trim() !== "" && product) {
5048
+ }, [accessibleKey, personId]);
5049
+ if ((settled === null || settled === void 0 ? void 0 : settled.accessibleKey) === accessibleKey && settled.personId === personId) {
4082
5050
  return {
4083
- details: detailsFromMetadata(metadata),
4084
- path: window.location.pathname,
4085
- product,
4086
- title,
4087
- type: "page"
5051
+ products: settled.products,
5052
+ status: "done"
4088
5053
  };
4089
5054
  } else {
4090
- return null;
5055
+ return {
5056
+ status: "loading"
5057
+ };
4091
5058
  }
4092
5059
  }
4093
- // The entry Recently Visited would record for the current page, or null when
4094
- // the page isn't eligible. Shared by the recorder and the debug panel so the
4095
- // panel always shows what actually gets stored.
4096
- function useCurrentVisit() {
4097
- const {
4098
- recently_visitable: title,
4099
- resource
4100
- } = usePcoMeta();
4101
- const metadata = useRecentlyVisitableMetadata();
4102
- return React.useMemo(() => visitFromPage({
4103
- metadata,
4104
- resource,
4105
- title
4106
- }), [metadata, resource, title]);
5060
+ function inSwitcherOrder(apps) {
5061
+ return groupApps(apps).restProducts.flatMap(app => SEARCHABLE_PRODUCTS.filter(product => product.appName === app.name));
4107
5062
  }
4108
- // Products hand the person's ID to AppsProvider, not to the topbar itself,
4109
- // and several pass it as a number.
4110
- function useCurrentPersonId() {
4111
- const {
4112
- userId
4113
- } = useStore();
4114
- return userId == null ? undefined : String(userId);
5063
+
5064
+ /**
5065
+ * Writes the height of the visible part of the page to the parent of `ref`'s
5066
+ * element as `--visual-viewport-height`. It shrinks when an on-screen keyboard
5067
+ * opens; `100dvh` doesn't, since mobile Safari draws the keyboard over it.
5068
+ */
5069
+ function useVisualViewportHeight(ref, enabled) {
5070
+ React.useEffect(() => {
5071
+ var _a;
5072
+ const viewport = window.visualViewport;
5073
+ const parent = (_a = ref.current) === null || _a === void 0 ? void 0 : _a.parentElement;
5074
+ if (!enabled || !viewport || !parent) return undefined;
5075
+ const update = () => parent.style.setProperty("--visual-viewport-height", `${viewport.height}px`);
5076
+ update();
5077
+ viewport.addEventListener("resize", update);
5078
+ return () => {
5079
+ viewport.removeEventListener("resize", update);
5080
+ parent.style.removeProperty("--visual-viewport-height");
5081
+ };
5082
+ }, [enabled, ref]);
4115
5083
  }
4116
5084
 
4117
- // Per-origin localStorage is a stopgap: each product keeps its own list until
4118
- // PCA-409 moves this to storage shared across product subdomains. Only this
4119
- // file should need to change when it does.
4120
- function storageKey({
4121
- env,
4122
- personId
5085
+ function ProductSearch({
5086
+ apps,
5087
+ mobile = false,
5088
+ env
4123
5089
  }) {
4124
- return `topbar.recently_visited.${env}.${personId}`;
4125
- }
4126
- // localStorage doesn't notify the tab that wrote it, so readers in this tab
4127
- // learn about new visits from this counter instead.
4128
- const changes = createStore({
4129
- version: 0
4130
- });
4131
- function readRecentlyVisited(scope) {
4132
- try {
4133
- return parseRecentlyVisited(localStorage.getItem(storageKey(scope)), scope.personId);
4134
- } catch {
4135
- return [];
4136
- }
4137
- }
4138
- function recordVisit(visit, scope) {
4139
- if (isEntry(visit)) {
4140
- try {
4141
- localStorage.setItem(storageKey(scope), serializeRecentlyVisited(addVisit(readRecentlyVisited(scope), visit), scope.personId));
4142
- changes.setState({
4143
- version: changes.getState().version + 1
4144
- });
4145
- } catch {
4146
- // Storage can be full or blocked; recents are disposable.
4147
- }
4148
- }
5090
+ const searchable = useSearchableProducts(apps);
5091
+ const [open, setOpen] = React.useState(false);
5092
+ const modalRef = React.useRef(null);
5093
+ // Sizes Tapestry's modal frame, the dialog's parent, to fit above the keyboard.
5094
+ useVisualViewportHeight(modalRef, mobile && open);
5095
+ const available = searchable.status === "done" && searchable.products.length > 0;
5096
+ useSearchShortcut(available, open, setOpen);
5097
+ if (!available) return null;
5098
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SearchButton, {
5099
+ mobile: mobile,
5100
+ onClick: () => setOpen(true)
5101
+ }), /*#__PURE__*/React.createElement(tapestry.Modal, {
5102
+ "aria-label": "Search",
5103
+ className: classnames(styles$i.modal, {
5104
+ [styles$i.mobile]: mobile
5105
+ }),
5106
+ dismissible: true,
5107
+ onClose: () => setOpen(false),
5108
+ open: open,
5109
+ ref: modalRef,
5110
+ size: "lg"
5111
+ }, /*#__PURE__*/React.createElement(SearchPanel, {
5112
+ mobile: mobile,
5113
+ env: env,
5114
+ products: searchable.products
5115
+ })));
4149
5116
  }
4150
5117
 
4151
5118
  function useRecordRecentlyVisited({
@@ -4190,9 +5157,9 @@ function StyledRoot({
4190
5157
  `), children);
4191
5158
  }
4192
5159
 
4193
- var styles$9 = {"route":"route--8BWe","is-active":"is-active-FsU9Y","apps-routes-separator":"apps-routes-separator-Nlm-N"};
5160
+ var styles$a = {"route":"route--8BWe","is-active":"is-active-FsU9Y","apps-routes-separator":"apps-routes-separator-Nlm-N"};
4194
5161
 
4195
- 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"};
5162
+ 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"};
4196
5163
 
4197
5164
  const UserMenu = ({
4198
5165
  appName,
@@ -4211,7 +5178,7 @@ const UserMenu = ({
4211
5178
  component: Outsider,
4212
5179
  onOutsideClick: () => setUserMenuVisible(false),
4213
5180
  cleanup: !userMenuVisible,
4214
- className: styles$8.popup,
5181
+ className: styles$9.popup,
4215
5182
  isVisible: userMenuVisible
4216
5183
  }, /*#__PURE__*/React.createElement("div", null, hasConnectedPeople ? (/*#__PURE__*/React.createElement("div", {
4217
5184
  style: {
@@ -4355,11 +5322,11 @@ const UserMenuTrigger = ({
4355
5322
  userMenuVisible
4356
5323
  }) => {
4357
5324
  return /*#__PURE__*/React.createElement("div", {
4358
- className: styles$8["trigger-wrap"],
5325
+ className: styles$9["trigger-wrap"],
4359
5326
  "data-pendo": "topbar-org-switcher"
4360
5327
  }, /*#__PURE__*/React.createElement(Hoverable, {
4361
- className: classnames(styles$8["trigger"], {
4362
- [styles$8["is-visible"]]: userMenuVisible
5328
+ className: classnames(styles$9["trigger"], {
5329
+ [styles$9["is-visible"]]: userMenuVisible
4363
5330
  }),
4364
5331
  component: "button",
4365
5332
  type: "button",
@@ -4371,7 +5338,7 @@ const UserMenuTrigger = ({
4371
5338
  requestConnectedPeopleFetch();
4372
5339
  }
4373
5340
  }, showOrgName && (/*#__PURE__*/React.createElement("span", {
4374
- className: styles$8["trigger-orgname"]
5341
+ className: styles$9["trigger-orgname"]
4375
5342
  }, orgName)), /*#__PURE__*/React.createElement(DisclosureChevronIcon, null)));
4376
5343
  };
4377
5344
 
@@ -4431,13 +5398,14 @@ const NotSmallTopbar = ({
4431
5398
  colors: props.colors,
4432
5399
  requestAppsFetch: props.requestAppsFetch
4433
5400
  })), /*#__PURE__*/React.createElement("div", {
4434
- className: styles$9["apps-routes-separator"]
5401
+ className: styles$a["apps-routes-separator"]
4435
5402
  }), routesVisible && props.routes, /*#__PURE__*/React.createElement("div", {
4436
5403
  style: {
4437
5404
  margin: "auto"
4438
5405
  }
4439
5406
  }), !notificationsEnabled && notifications(), productSearchEnabled ? (/*#__PURE__*/React.createElement(ProductSearch, {
4440
- apps: props.apps
5407
+ apps: props.apps,
5408
+ env: props.env
4441
5409
  })) : search({
4442
5410
  hideRoutes: () => setRoutesVisible(false),
4443
5411
  showRoutes: () => setRoutesVisible(true)
@@ -4471,12 +5439,12 @@ const NotSmallRoute = ({
4471
5439
  ...nativeProps
4472
5440
  }) => {
4473
5441
  return /*#__PURE__*/React.createElement("a", {
4474
- className: classnames(styles$9["route"], className, active && styles$9["is-active"]),
5442
+ className: classnames(styles$a["route"], className, active && styles$a["is-active"]),
4475
5443
  ...nativeProps
4476
5444
  });
4477
5445
  };
4478
5446
 
4479
- var styles$7 = {"avatar":"avatar-tW-wn"};
5447
+ var styles$8 = {"avatar":"avatar-tW-wn"};
4480
5448
 
4481
5449
  function Avatar({
4482
5450
  env,
@@ -4486,7 +5454,7 @@ function Avatar({
4486
5454
  }) {
4487
5455
  const url = incomingUrl || `${pcoUrl$1(env)("people")}/static/no_photo_thumbnail_gray.svg`;
4488
5456
  return /*#__PURE__*/React.createElement("div", {
4489
- className: styles$7.avatar,
5457
+ className: styles$8.avatar,
4490
5458
  style: {
4491
5459
  backgroundImage: `url(${url})`,
4492
5460
  ...style
@@ -32245,7 +33213,7 @@ function requireReactDom () {
32245
33213
 
32246
33214
  var reactDomExports = requireReactDom();
32247
33215
 
32248
- 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"};
33216
+ 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"};
32249
33217
 
32250
33218
  const AppsSwitcher = ({
32251
33219
  appName,
@@ -32273,8 +33241,8 @@ const AppsSwitcher = ({
32273
33241
  }, /*#__PURE__*/React.createElement("a", {
32274
33242
  "aria-current": name === appName ? "page" : undefined,
32275
33243
  "aria-label": name,
32276
- className: classnames(styles$6.link, {
32277
- [styles$6.current]: name === appName
33244
+ className: classnames(styles$7.link, {
33245
+ [styles$7.current]: name === appName
32278
33246
  }),
32279
33247
  "data-pendo": `app-switcher--change-app--${name}`,
32280
33248
  "data-turbolinks": false,
@@ -32286,48 +33254,48 @@ const AppsSwitcher = ({
32286
33254
  "aria-expanded": visible,
32287
33255
  "aria-haspopup": "true",
32288
33256
  "aria-label": `App switcher: ${appName}`,
32289
- className: styles$6.trigger,
33257
+ className: styles$7.trigger,
32290
33258
  onClick: onTriggerClick,
32291
33259
  type: "button"
32292
33260
  }, /*#__PURE__*/React.createElement(ColorAppIcon, {
32293
33261
  appName: appName.replace(/[\s-]/, "")
32294
33262
  })), /*#__PURE__*/reactDomExports.createPortal(/*#__PURE__*/React.createElement("div", {
32295
33263
  ref: containerRef,
32296
- className: classnames(styles$6.layer, {
32297
- [styles$6["layer--visible"]]: visible
33264
+ className: classnames(styles$7.layer, {
33265
+ [styles$7["layer--visible"]]: visible
32298
33266
  }),
32299
33267
  onClick: toggle,
32300
33268
  onKeyDown: handleKeyDown
32301
33269
  }, /*#__PURE__*/React.createElement("nav", {
32302
33270
  "aria-label": "Switch app",
32303
33271
  "aria-modal": visible,
32304
- className: styles$6.panel,
33272
+ className: styles$7.panel,
32305
33273
  "data-pendo": "app-switcher",
32306
33274
  onClick: event => event.stopPropagation(),
32307
33275
  role: "dialog"
32308
33276
  }, /*#__PURE__*/React.createElement("div", {
32309
- className: styles$6.header
33277
+ className: styles$7.header
32310
33278
  }, /*#__PURE__*/React.createElement("span", {
32311
- className: styles$6.heading
33279
+ className: styles$7.heading
32312
33280
  }, "Switch to"), /*#__PURE__*/React.createElement("button", {
32313
33281
  "aria-label": "Close",
32314
- className: styles$6.close,
33282
+ className: styles$7.close,
32315
33283
  onClick: toggle,
32316
33284
  type: "button"
32317
33285
  }, /*#__PURE__*/React.createElement(X, {
32318
33286
  "aria-hidden": "true",
32319
33287
  fill: "var(--t-icon-color-dim)"
32320
33288
  }))), /*#__PURE__*/React.createElement("ul", {
32321
- className: styles$6.list,
33289
+ className: styles$7.list,
32322
33290
  role: "list"
32323
33291
  }, homeProduct && productLink(homeProduct.name), restProducts.map(({
32324
33292
  name
32325
33293
  }) => productLink(name)), accountsProduct && (/*#__PURE__*/React.createElement("li", {
32326
- className: styles$6["accounts-item"]
33294
+ className: styles$7["accounts-item"]
32327
33295
  }, /*#__PURE__*/React.createElement("a", {
32328
33296
  "aria-current": appName === "Accounts" ? "page" : undefined,
32329
33297
  "aria-label": "Account settings",
32330
- className: classnames(styles$6.link, styles$6["accounts-link"]),
33298
+ className: classnames(styles$7.link, styles$7["accounts-link"]),
32331
33299
  "data-pendo": "app-switcher--change-app--Accounts",
32332
33300
  "data-turbolinks": false,
32333
33301
  href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(accountsProduct.name)
@@ -32350,14 +33318,14 @@ ClientStorage.defaultProps = {
32350
33318
  method: "localStorage"
32351
33319
  };
32352
33320
 
32353
- 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"};
33321
+ 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"};
32354
33322
 
32355
33323
  function AccountSettingsLink({
32356
33324
  env
32357
33325
  }) {
32358
33326
  const accountsName = "accounts";
32359
33327
  return /*#__PURE__*/React.createElement("a", {
32360
- className: classnames(styles$5["appmenu-list-a"], styles$5["appmenu-list-a--bottom"]),
33328
+ className: classnames(styles$6["appmenu-list-a"], styles$6["appmenu-list-a--bottom"]),
32361
33329
  "data-pendo": `app-switcher--change-app--${accountsName}`,
32362
33330
  "data-turbolinks": false,
32363
33331
  href: pcoUrl$1(env)(accountsName) + getAppsSwitchProductPath(accountsName)
@@ -32380,7 +33348,7 @@ const CloseMenuButton = ({
32380
33348
  }) => (/*#__PURE__*/React.createElement("button", {
32381
33349
  "aria-label": "Close menu",
32382
33350
  type: "button",
32383
- className: styles$5["appmenu-close-button"],
33351
+ className: styles$6["appmenu-close-button"],
32384
33352
  style: style
32385
33353
  }, /*#__PURE__*/React.createElement(X, {
32386
33354
  fill: color,
@@ -32441,7 +33409,7 @@ const LegacyAppsSwitcher = ({
32441
33409
  "aria-haspopup": "true",
32442
33410
  "aria-label": `App switcher: ${appName}`,
32443
33411
  type: "button",
32444
- className: styles$5["appmenu-button"],
33412
+ className: styles$6["appmenu-button"],
32445
33413
  onClick: onTriggerClick
32446
33414
  }, /*#__PURE__*/React.createElement(ColorAppIcon, {
32447
33415
  appName: appName.replace(/[\s-]/, "")
@@ -32449,7 +33417,7 @@ const LegacyAppsSwitcher = ({
32449
33417
  item: "Topbar:Small:Menus:Products:visibility",
32450
33418
  render: () => (/*#__PURE__*/React.createElement("div", {
32451
33419
  ref: appMenuRef,
32452
- className: styles$5["appmenu-layer"],
33420
+ className: styles$6["appmenu-layer"],
32453
33421
  style: {
32454
33422
  ...(visible && {
32455
33423
  display: "block"
@@ -32466,37 +33434,37 @@ const LegacyAppsSwitcher = ({
32466
33434
  e.stopPropagation();
32467
33435
  }
32468
33436
  },
32469
- className: styles$5["appmenu-container"]
33437
+ className: styles$6["appmenu-container"]
32470
33438
  }, /*#__PURE__*/React.createElement("a", {
32471
33439
  "aria-current": appName === (homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name) ? "page" : undefined,
32472
33440
  "aria-label": homeProduct ? homeProduct.name : appName,
32473
33441
  "data-pendo": `app-switcher--change-app--${homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name}`,
32474
33442
  "data-turbolinks": false,
32475
- className: classnames(styles$5["appmenu-list-a"], styles$5["appmenu-list-a--top"]),
33443
+ className: classnames(styles$6["appmenu-list-a"], styles$6["appmenu-list-a--top"]),
32476
33444
  href: homeProduct ? pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name) : undefined
32477
33445
  }, /*#__PURE__*/React.createElement(UserMenuAppLockup, {
32478
33446
  appName: homeProduct ? homeProduct.name : appName
32479
33447
  })), /*#__PURE__*/React.createElement("div", {
32480
- className: styles$5["appmenu-scroll"]
33448
+ className: styles$6["appmenu-scroll"]
32481
33449
  }, /*#__PURE__*/React.createElement("ul", {
32482
33450
  "data-pendo": "app-switcher",
32483
- className: styles$5["appmenu-list"]
33451
+ className: styles$6["appmenu-list"]
32484
33452
  }, appsMenuFormatter(restProducts).map(({
32485
33453
  name
32486
33454
  }) => (/*#__PURE__*/React.createElement("li", {
32487
33455
  key: name,
32488
- className: styles$5["appmenu-list-li"]
33456
+ className: styles$6["appmenu-list-li"]
32489
33457
  }, /*#__PURE__*/React.createElement("a", {
32490
33458
  "aria-current": name === appName ? "page" : undefined,
32491
33459
  "aria-label": name,
32492
33460
  "data-pendo": `app-switcher--change-app--${name}`,
32493
33461
  "data-turbolinks": false,
32494
- className: styles$5["appmenu-list-a"],
33462
+ className: styles$6["appmenu-list-a"],
32495
33463
  href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(name)
32496
33464
  }, /*#__PURE__*/React.createElement(UserMenuAppLockup, {
32497
33465
  appName: name
32498
33466
  }))))), accountsProduct && (/*#__PURE__*/React.createElement("li", {
32499
- className: styles$5["appmenu-list-li"]
33467
+ className: styles$6["appmenu-list-li"]
32500
33468
  }, /*#__PURE__*/React.createElement(AccountSettingsLink, {
32501
33469
  env: env
32502
33470
  }))))))))
@@ -32560,19 +33528,25 @@ const SmallTopbar = ({
32560
33528
  toggle: toggleProductSwitcher,
32561
33529
  visible: productSwitcherVisible
32562
33530
  })), routesMenuVisible && (/*#__PURE__*/React.createElement("div", {
32563
- className: styles$5["routes"]
33531
+ className: styles$6["routes"]
32564
33532
  }, props.routes)), /*#__PURE__*/React.createElement("button", {
32565
33533
  type: "button",
32566
- className: classnames(styles$5["current-route"], {
32567
- [styles$5["is-active"]]: routesMenuVisible
33534
+ className: classnames(styles$6["current-route"], {
33535
+ [styles$6["is-active"]]: routesMenuVisible
32568
33536
  }),
32569
33537
  onClick: props.routes.length > 0 ? () => {
32570
33538
  setProductSwitcherVisible(false);
32571
33539
  setRoutesMenuVisible(visible => !visible);
32572
33540
  } : null
32573
- }, /*#__PURE__*/React.createElement(CurrentRouteComponent, null, capitalize(currentRouteText)), props.routes.length > 0 && /*#__PURE__*/React.createElement(DisclosureChevronIcon, null)), /*#__PURE__*/React.createElement("button", {
33541
+ }, /*#__PURE__*/React.createElement(CurrentRouteComponent, null, capitalize$1(currentRouteText)), props.routes.length > 0 && /*#__PURE__*/React.createElement(DisclosureChevronIcon, null)), /*#__PURE__*/React.createElement("div", {
33542
+ className: styles$6["actions"]
33543
+ }, productSearchEnabled && (/*#__PURE__*/React.createElement(ProductSearch, {
33544
+ apps: props.apps,
33545
+ mobile: true,
33546
+ env: props.env
33547
+ })), /*#__PURE__*/React.createElement("button", {
32574
33548
  type: "button",
32575
- className: styles$5["profile-button"],
33549
+ className: styles$6["profile-button"],
32576
33550
  onClick: () => {
32577
33551
  var _a, _b;
32578
33552
  props.requestConnectedPeopleFetch();
@@ -32581,14 +33555,14 @@ const SmallTopbar = ({
32581
33555
  }, /*#__PURE__*/React.createElement(Avatar, {
32582
33556
  env: props.env,
32583
33557
  url: props.userAvatarPath
32584
- })));
33558
+ }))));
32585
33559
  };
32586
33560
  const SmallRoute = ({
32587
33561
  active,
32588
33562
  className,
32589
33563
  ...props
32590
33564
  }) => (/*#__PURE__*/React.createElement("a", {
32591
- className: classnames(styles$5["route"], className, active && styles$5["is-active"]),
33565
+ className: classnames(styles$6["route"], className, active && styles$6["is-active"]),
32592
33566
  ...props
32593
33567
  }));
32594
33568
 
@@ -32727,7 +33701,7 @@ const topbarTheme = {
32727
33701
  id: "topbar"
32728
33702
  };
32729
33703
 
32730
- 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"};
33704
+ 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"};
32731
33705
 
32732
33706
  function isPresent(value) {
32733
33707
  return value !== undefined && value.trim() !== "";
@@ -32756,15 +33730,31 @@ function Field({
32756
33730
  value
32757
33731
  }) {
32758
33732
  return /*#__PURE__*/React.createElement("div", {
32759
- className: styles$4["debug-pco-resource-field"]
33733
+ className: styles$5["debug-pco-resource-field"]
32760
33734
  }, /*#__PURE__*/React.createElement("span", {
32761
- className: styles$4["debug-pco-resource-field-label"]
33735
+ className: styles$5["debug-pco-resource-field-label"]
32762
33736
  }, label), /*#__PURE__*/React.createElement("span", {
32763
- className: styles$4["debug-pco-resource-field-value"]
33737
+ className: styles$5["debug-pco-resource-field-value"]
32764
33738
  }, value));
32765
33739
  }
33740
+ // Only staff ghosting or impersonating a person get pco:staff-access-level,
33741
+ // which keeps the panel away from regular users on staging.
32766
33742
  function DebugPcoResource({
32767
33743
  env
33744
+ }) {
33745
+ const {
33746
+ "staff-access-level": staffAccessLevel
33747
+ } = usePcoMeta();
33748
+ if (isPresent(staffAccessLevel)) {
33749
+ return /*#__PURE__*/React.createElement(DebugPanel, {
33750
+ env: env
33751
+ });
33752
+ } else {
33753
+ return null;
33754
+ }
33755
+ }
33756
+ function DebugPanel({
33757
+ env
32768
33758
  }) {
32769
33759
  const {
32770
33760
  recently_visitable: recentlyVisitable,
@@ -32773,12 +33763,17 @@ function DebugPcoResource({
32773
33763
  const [lookup, setLookup] = React.useState({
32774
33764
  status: "idle"
32775
33765
  });
32776
- const hasResource = isPresent(resource);
33766
+ const visit = useCurrentVisit();
33767
+ const personId = useCurrentPersonId();
33768
+ const recentlyVisited = useRecentlyVisited({
33769
+ env,
33770
+ personId
33771
+ });
33772
+ const pcrl = (visit === null || visit === void 0 ? void 0 : visit.type) === "resource" ? visit.pcrl : null;
32777
33773
  const hasRecentlyVisitable = isPresent(recentlyVisitable);
33774
+ const hasInvalidResource = isPresent(resource) && !isPcrl(resource);
32778
33775
  React.useEffect(() => {
32779
- // pco:resource is the more specific signal, so it always wins when both
32780
- // tags are present.
32781
- const url = hasResource ? pcrlLookupUrl(resource, env) : null;
33776
+ const url = pcrl ? pcrlLookupUrl(pcrl, env) : null;
32782
33777
  if (!url) {
32783
33778
  setLookup({
32784
33779
  status: "idle"
@@ -32810,37 +33805,45 @@ function DebugPcoResource({
32810
33805
  return () => {
32811
33806
  cancelled = true;
32812
33807
  };
32813
- }, [resource, env, hasResource]);
33808
+ }, [pcrl, env]);
32814
33809
  return /*#__PURE__*/React.createElement("div", {
32815
- className: classnames("staff-only", styles$4["debug-pco-resource"])
33810
+ className: classnames("staff-only", styles$5["debug-pco-resource"])
32816
33811
  }, /*#__PURE__*/React.createElement("div", {
32817
- className: styles$4["debug-pco-resource-header"]
33812
+ className: styles$5["debug-pco-resource-header"]
32818
33813
  }, /*#__PURE__*/React.createElement("span", {
32819
- className: styles$4["debug-pco-resource-badge"]
33814
+ className: styles$5["debug-pco-resource-badge"]
32820
33815
  }, "Staff"), /*#__PURE__*/React.createElement("span", {
32821
- className: styles$4["debug-pco-resource-title"]
33816
+ className: styles$5["debug-pco-resource-title"]
32822
33817
  }, "Recently Visited debug")), /*#__PURE__*/React.createElement(Field, {
32823
33818
  label: "pco:resource",
32824
33819
  value: describeTag("resource", resource)
32825
33820
  }), /*#__PURE__*/React.createElement(Field, {
32826
33821
  label: "pco:recently_visitable",
32827
33822
  value: describeTag("recently_visitable", recentlyVisitable)
32828
- }), !hasResource && !hasRecentlyVisitable && (/*#__PURE__*/React.createElement("p", {
32829
- className: styles$4["debug-pco-resource-note"]
32830
- }, "This page is not eligible for Recently Visited")), hasResource && hasRecentlyVisitable && (/*#__PURE__*/React.createElement("p", {
32831
- className: styles$4["debug-pco-resource-note"]
32832
- }, "Both tags are present \u2014 pco:resource takes precedence for the Recently Visited entry.")), hasResource && lookup.status === "loading" && (/*#__PURE__*/React.createElement("p", {
32833
- className: styles$4["debug-pco-resource-note"]
32834
- }, "Looking up resource\u2026")), hasResource && lookup.status === "error" && (/*#__PURE__*/React.createElement("p", {
32835
- className: styles$4["debug-pco-resource-note"]
32836
- }, lookup.message)), hasResource && lookup.status === "loaded" && (/*#__PURE__*/React.createElement("pre", {
32837
- className: styles$4["debug-pco-resource-json"]
32838
- }, JSON.stringify(lookup.json, null, 2))), !hasResource && hasRecentlyVisitable && (/*#__PURE__*/React.createElement("p", {
32839
- className: styles$4["debug-pco-resource-note"]
32840
- }, "Recently Visited title: ", recentlyVisitable)));
32841
- }
32842
-
32843
- var styles$3 = {"drawer-wrap":"drawer-wrap-J0rSL","is-visible":"is-visible-KTME8","drawer":"drawer-j7f4l"};
33823
+ }), hasInvalidResource && (/*#__PURE__*/React.createElement("p", {
33824
+ className: styles$5["debug-pco-resource-note"]
33825
+ }, "pco:resource isn't a PCRL (product/v2/type/id), so Recently Visited ignores it.")), visit === null && (/*#__PURE__*/React.createElement("p", {
33826
+ className: styles$5["debug-pco-resource-note"]
33827
+ }, "This page is not eligible for Recently Visited")), pcrl && hasRecentlyVisitable && (/*#__PURE__*/React.createElement("p", {
33828
+ className: styles$5["debug-pco-resource-note"]
33829
+ }, "Both tags are present \u2014 pco:resource takes precedence for the Recently Visited entry.")), pcrl && lookup.status === "loading" && (/*#__PURE__*/React.createElement("p", {
33830
+ className: styles$5["debug-pco-resource-note"]
33831
+ }, "Looking up resource\u2026")), pcrl && lookup.status === "error" && (/*#__PURE__*/React.createElement("p", {
33832
+ className: styles$5["debug-pco-resource-note"]
33833
+ }, lookup.message)), pcrl && lookup.status === "loaded" && (/*#__PURE__*/React.createElement("pre", {
33834
+ className: styles$5["debug-pco-resource-json"]
33835
+ }, JSON.stringify(lookup.json, null, 2))), (visit === null || visit === void 0 ? void 0 : visit.type) === "page" && (/*#__PURE__*/React.createElement("p", {
33836
+ className: styles$5["debug-pco-resource-note"]
33837
+ }, "Recently Visited title: ", visit.title)), /*#__PURE__*/React.createElement("span", {
33838
+ className: styles$5["debug-pco-resource-field-label"]
33839
+ }, "Recently Visited"), personId ? (/*#__PURE__*/React.createElement("pre", {
33840
+ className: styles$5["debug-pco-resource-json"]
33841
+ }, JSON.stringify(recentlyVisited, null, 2))) : (/*#__PURE__*/React.createElement("p", {
33842
+ className: styles$5["debug-pco-resource-note"]
33843
+ }, "No current person, so nothing is recorded.")));
33844
+ }
33845
+
33846
+ var styles$4 = {"drawer-wrap":"drawer-wrap-J0rSL","is-visible":"is-visible-KTME8","drawer":"drawer-j7f4l"};
32844
33847
 
32845
33848
  function Drawer({
32846
33849
  children,
@@ -32848,7 +33851,7 @@ function Drawer({
32848
33851
  }) {
32849
33852
  return /*#__PURE__*/React.createElement("div", {
32850
33853
  id: id,
32851
- className: styles$3["drawer"]
33854
+ className: styles$4["drawer"]
32852
33855
  }, children);
32853
33856
  }
32854
33857
  function DrawerWrap({
@@ -32858,7 +33861,7 @@ function DrawerWrap({
32858
33861
  ...props
32859
33862
  }) {
32860
33863
  return /*#__PURE__*/React.createElement("div", {
32861
- className: classnames(styles$3["drawer-wrap"], className, isVisible && styles$3["is-visible"]),
33864
+ className: classnames(styles$4["drawer-wrap"], className, isVisible && styles$4["is-visible"]),
32862
33865
  ...props
32863
33866
  }, children);
32864
33867
  }
@@ -33042,15 +34045,18 @@ const SwitchView = ({
33042
34045
  }));
33043
34046
  };
33044
34047
 
33045
- 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"};
34048
+ 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"};
33046
34049
 
33047
34050
  const COLOR_MODES = [{
34051
+ icon: "toolbar.sun",
33048
34052
  label: "Light",
33049
34053
  value: "light"
33050
34054
  }, {
34055
+ icon: "toolbar.moon",
33051
34056
  label: "Dark",
33052
34057
  value: "dark"
33053
34058
  }, {
34059
+ icon: "toolbar.display",
33054
34060
  label: "System",
33055
34061
  value: "system"
33056
34062
  }];
@@ -33062,14 +34068,15 @@ function ColorModeSwitcher() {
33062
34068
  const updateStore = useUpdateStore();
33063
34069
  const [counter] = React.useState(() => COLOR_MODE_INSTANCE_COUNTER++);
33064
34070
  return /*#__PURE__*/React.createElement("fieldset", {
33065
- className: styles$2["color-mode-switcher"]
34071
+ className: styles$3["color-mode-switcher"]
33066
34072
  }, /*#__PURE__*/React.createElement("legend", {
33067
- className: styles$2["color-mode-switcher-label"]
34073
+ className: styles$3["color-mode-switcher-label"]
33068
34074
  }, "Appearance", " "), /*#__PURE__*/React.createElement("div", {
33069
- className: styles$2["color-mode-switcher-modes"]
34075
+ className: styles$3["color-mode-switcher-modes"]
33070
34076
  }, COLOR_MODES.map(({
33071
- value,
33072
- label
34077
+ icon,
34078
+ label,
34079
+ value
33073
34080
  }) => (/*#__PURE__*/React.createElement("label", {
33074
34081
  key: value
33075
34082
  }, /*#__PURE__*/React.createElement("input", {
@@ -33086,8 +34093,9 @@ function ColorModeSwitcher() {
33086
34093
  document.documentElement.setAttribute(RESOLVED_COLOR_MODE_ATTRIBUTE, resolveColorMode(colorMode));
33087
34094
  updateColorMode(colorMode);
33088
34095
  }
33089
- }), /*#__PURE__*/React.createElement(ColorModeIcon, {
33090
- mode: value
34096
+ }), /*#__PURE__*/React.createElement(Icon, {
34097
+ name: icon,
34098
+ size: 15
33091
34099
  }), label)))));
33092
34100
  }
33093
34101
  function updateColorMode(mode) {
@@ -33102,28 +34110,6 @@ function updateColorMode(mode) {
33102
34110
  method: "POST"
33103
34111
  });
33104
34112
  }
33105
- const COLOR_MODE_ICON_PATHS = {
33106
- 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",
33107
- 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",
33108
- 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"
33109
- };
33110
- function ColorModeIcon({
33111
- mode
33112
- }) {
33113
- return /*#__PURE__*/React.createElement("svg", {
33114
- width: "16",
33115
- height: "16",
33116
- viewBox: "0 0 14 14",
33117
- xmlns: "http://www.w3.org/2000/svg",
33118
- "aria-hidden": "true"
33119
- }, /*#__PURE__*/React.createElement("path", {
33120
- d: COLOR_MODE_ICON_PATHS[mode],
33121
- strokeWidth: "1.5",
33122
- fill: "none",
33123
- strokeLinecap: "round",
33124
- strokeLinejoin: "round"
33125
- }));
33126
- }
33127
34113
 
33128
34114
  const requestClearConnectedPeopleCache = () => remove();
33129
34115
  function UserProfile({
@@ -33186,10 +34172,10 @@ function UserProfile({
33186
34172
  alignment: "center",
33187
34173
  spacing: 0.5,
33188
34174
  width: "100%"
33189
- }, /*#__PURE__*/React.createElement(tapestryReact.Icon, {
34175
+ }, /*#__PURE__*/React.createElement(Icon, {
33190
34176
  name: mfaConfigured ? "general.lock" : "general.unlock",
33191
- size: "sm",
33192
- color: mfaConfigured ? "var(--t-icon-color-status-success)" : "var(--t-icon-color-status-error)"
34177
+ size: 12,
34178
+ theme: mfaConfigured ? "--t-icon-color-status-success" : "--t-icon-color-status-error"
33193
34179
  }), /*#__PURE__*/React.createElement(tapestryReact.Text, {
33194
34180
  size: 4,
33195
34181
  weight: 400,
@@ -33237,9 +34223,10 @@ function UserProfile({
33237
34223
  }, /*#__PURE__*/React.createElement(tapestryReact.Text, {
33238
34224
  color: "var(--text-color-default-secondary)",
33239
34225
  size: 3
33240
- }, "Log out"), /*#__PURE__*/React.createElement(tapestryReact.Icon, {
34226
+ }, "Log out"), /*#__PURE__*/React.createElement(Icon, {
33241
34227
  name: "general.intoBox",
33242
- fill: "var(--t-icon-color-secondary)"
34228
+ size: 14,
34229
+ theme: "--t-icon-color-secondary"
33243
34230
  })))), env !== "production" && /*#__PURE__*/React.createElement(DebugPcoResource, {
33244
34231
  env: env
33245
34232
  }), hasConnectedPeople && (/*#__PURE__*/React.createElement(tapestryReact.StackView, {
@@ -33301,26 +34288,42 @@ function UserProfile({
33301
34288
 
33302
34289
  var ActionType;
33303
34290
  (function (ActionType) {
34291
+ ActionType["clearPending"] = "clearPending";
33304
34292
  ActionType["close"] = "close";
33305
34293
  ActionType["mount"] = "mount";
33306
34294
  ActionType["open"] = "open";
34295
+ ActionType["setPending"] = "setPending";
33307
34296
  ActionType["unmount"] = "unmount";
33308
34297
  })(ActionType || (ActionType = {}));
33309
34298
  const initialState = {
33310
34299
  currentContent: null,
33311
- open: false
34300
+ open: false,
34301
+ pending: null
33312
34302
  };
33313
34303
  const reducer = (state, action) => {
34304
+ var _a, _b;
33314
34305
  switch (action.type) {
33315
34306
  case ActionType.mount:
33316
34307
  return {
33317
34308
  ...state,
33318
- currentContent: action.payload.content
34309
+ currentContent: action.payload.content,
34310
+ 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
33319
34311
  };
33320
34312
  case ActionType.unmount:
33321
34313
  return {
33322
34314
  ...state,
33323
- currentContent: null
34315
+ currentContent: null,
34316
+ pending: null
34317
+ };
34318
+ case ActionType.setPending:
34319
+ return {
34320
+ ...state,
34321
+ pending: action.payload
34322
+ };
34323
+ case ActionType.clearPending:
34324
+ return {
34325
+ ...state,
34326
+ pending: null
33324
34327
  };
33325
34328
  case ActionType.open:
33326
34329
  return {
@@ -33339,13 +34342,181 @@ const useToolbarReducer = overrides => React__namespace.useReducer(reducer, {
33339
34342
  ...overrides
33340
34343
  });
33341
34344
 
33342
- 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"};
34345
+ 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"};
33343
34346
 
33344
- const pcoUrl = pcoUrl$1;
34347
+ // PCO Box serves TLS by default, and a bare http:// frame inside an https
34348
+ // product page is blocked as mixed content, so follow the page's scheme.
34349
+ const iframeSrc = () => `${window.location.protocol}//pco.test/iframe`;
34350
+ const STAFF_PINK = "var(--t-fill-color-product-staff)";
34351
+ // The pco.test domain is only ever served by a local PCO Box, so the tool
34352
+ // stays hidden in staging and production — and on Topbar's own dev server,
34353
+ // which runs on localhost and has no pco.test to frame.
34354
+ function isPcoTestHost() {
34355
+ const {
34356
+ hostname
34357
+ } = window.location;
34358
+ return hostname === "pco.test" || hostname.endsWith(".pco.test");
34359
+ }
34360
+ // Flattened from the source artwork's two outline paths — its gradients,
34361
+ // filters and clip paths carry nothing at icon size. Those paths are authored
34362
+ // on a 128px canvas, hence the transform: it fits the pair into the same ~24px
34363
+ // optical box the sibling toolbar icons occupy. Filled explicitly rather than
34364
+ // with `currentColor` so the mark stays staff pink instead of inheriting the
34365
+ // toolbar's text color and its hover/active states.
34366
+ 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";
34367
+ 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";
34368
+ function ToolsIcon() {
34369
+ return /*#__PURE__*/React.createElement("svg", {
34370
+ "aria-hidden": "true",
34371
+ width: "32",
34372
+ height: "32",
34373
+ viewBox: "0 0 32 32",
34374
+ xmlns: "http://www.w3.org/2000/svg"
34375
+ }, /*#__PURE__*/React.createElement("g", {
34376
+ fill: STAFF_PINK,
34377
+ transform: "translate(2.92 2.66) scale(.204)"
34378
+ }, /*#__PURE__*/React.createElement("path", {
34379
+ d: WRENCH
34380
+ }), /*#__PURE__*/React.createElement("path", {
34381
+ d: SCREWDRIVER
34382
+ })));
34383
+ }
34384
+ // A cross-origin frame's own history object is unreachable (touching it throws
34385
+ // a SecurityError), but the frame's navigations do land in the top-level joint
34386
+ // session history, so the *parent's* back/forward traverse them while leaving
34387
+ // the host page in place. Counting frame loads tracks how deep we are, which is
34388
+ // what keeps a click on Back from stepping past the frame's first page and
34389
+ // navigating the host app instead. Loads we caused are attributed to the
34390
+ // traversal that requested them rather than counted as new navigations.
34391
+ function useFrameHistory() {
34392
+ const [steps, setSteps] = React.useState({
34393
+ back: 0,
34394
+ forward: 0
34395
+ });
34396
+ // Traversal has to read the current counts outside of render, so the ref is
34397
+ // the source of truth and state exists to paint the buttons.
34398
+ const stepsRef = React.useRef(steps);
34399
+ const pendingTraversal = React.useRef(null);
34400
+ const frameIsLoaded = React.useRef(false);
34401
+ const moveTo = React.useCallback(next => {
34402
+ stepsRef.current = next;
34403
+ setSteps(next);
34404
+ }, []);
34405
+ const onFrameLoad = React.useCallback(() => {
34406
+ if (!frameIsLoaded.current) {
34407
+ // The frame's first page is the floor, not a step.
34408
+ frameIsLoaded.current = true;
34409
+ } else {
34410
+ const traversal = pendingTraversal.current;
34411
+ pendingTraversal.current = null;
34412
+ const {
34413
+ back,
34414
+ forward
34415
+ } = stepsRef.current;
34416
+ if (traversal === "back") {
34417
+ moveTo({
34418
+ back: back - 1,
34419
+ forward: forward + 1
34420
+ });
34421
+ } else if (traversal === "forward") {
34422
+ moveTo({
34423
+ back: back + 1,
34424
+ forward: forward - 1
34425
+ });
34426
+ } else {
34427
+ moveTo({
34428
+ back: back + 1,
34429
+ forward: 0
34430
+ });
34431
+ }
34432
+ }
34433
+ }, [moveTo]);
34434
+ // Guarded here rather than trusting the button's disabled state: traversing
34435
+ // with no frame step left would navigate the host app out from under us.
34436
+ const traverse = React.useCallback(direction => {
34437
+ if (stepsRef.current[direction] > 0) {
34438
+ pendingTraversal.current = direction;
34439
+ window.history[direction]();
34440
+ }
34441
+ }, []);
34442
+ return {
34443
+ onFrameLoad,
34444
+ steps,
34445
+ traverse
34446
+ };
34447
+ }
34448
+ function NavButton({
34449
+ direction,
34450
+ disabled,
34451
+ onClick
34452
+ }) {
34453
+ return /*#__PURE__*/React.createElement(tapestryReact.Button
34454
+ // Tapestry's `disabled` suppresses the click but emits no ARIA state.
34455
+ , {
34456
+ "aria-disabled": disabled,
34457
+ disabled: disabled,
34458
+ hover: {
34459
+ backgroundColor: "var(--t-fill-color-neutral-070)"
34460
+ },
34461
+ icon: {
34462
+ fill: "var(--t-icon-color-dim)",
34463
+ name: direction === "back" ? "general.leftChevron" : "general.rightChevron",
34464
+ size: "md"
34465
+ },
34466
+ onClick: onClick,
34467
+ opacity: disabled ? 0.35 : 1,
34468
+ theme: false,
34469
+ title: direction === "back" ? "Back" : "Forward"
34470
+ });
34471
+ }
34472
+ function DeveloperToolsComponent({
34473
+ closeDrawer
34474
+ }) {
34475
+ const {
34476
+ onFrameLoad,
34477
+ steps,
34478
+ traverse
34479
+ } = useFrameHistory();
34480
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(DrawerHeader, {
34481
+ closeDrawer: closeDrawer
34482
+ }, /*#__PURE__*/React.createElement(tapestryReact.StackView, {
34483
+ alignment: "center",
34484
+ axis: "horizontal",
34485
+ spacing: 0.25
34486
+ }, /*#__PURE__*/React.createElement(NavButton, {
34487
+ direction: "back",
34488
+ disabled: steps.back === 0,
34489
+ onClick: () => traverse("back")
34490
+ }), /*#__PURE__*/React.createElement(NavButton, {
34491
+ direction: "forward",
34492
+ disabled: steps.forward === 0,
34493
+ onClick: () => traverse("forward")
34494
+ }), /*#__PURE__*/React.createElement(DrawerHeading, {
34495
+ paddingLeft: 0.5
34496
+ }, "Developer Tools"))), /*#__PURE__*/React.createElement(tapestryReact.Box, {
34497
+ as: "iframe",
34498
+ border: "none",
34499
+ flex: "1",
34500
+ minHeight: 0,
34501
+ onLoad: onFrameLoad,
34502
+ src: iframeSrc(),
34503
+ title: "Developer Tools",
34504
+ width: "100%"
34505
+ }));
34506
+ }
34507
+ const DeveloperTools = {
34508
+ component: DeveloperToolsComponent,
34509
+ icons: {
34510
+ component: ToolsIcon
34511
+ },
34512
+ label: "Developer Tools",
34513
+ // No space: `name` becomes an element id and a pendo attribute.
34514
+ name: "DeveloperTools"
34515
+ };
33345
34516
 
33346
- 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"};
34517
+ 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"};
33347
34518
 
33348
- 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";
34519
+ 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";
33349
34520
  // Same stops as the AskAIBanner border gradient, so the two stay in sync.
33350
34521
  const GRADIENT_STOPS = [{
33351
34522
  color: "var(--t-fill-color-tag-teal-040)",
@@ -33394,7 +34565,7 @@ function SparkleIcon({
33394
34565
  stopColor: color
33395
34566
  }
33396
34567
  })))))), /*#__PURE__*/React.createElement("path", {
33397
- d: SPARKLE_PATH,
34568
+ d: SPARKLE_PATH$1,
33398
34569
  fill: isGradient ? `url(#${gradientId})` : "currentColor"
33399
34570
  }));
33400
34571
  }
@@ -33403,8 +34574,8 @@ function AskAIBanner(props) {
33403
34574
  var _a, _b;
33404
34575
  const isStacked = props.variant !== "inline";
33405
34576
  return /*#__PURE__*/React.createElement("button", {
33406
- className: classnames(styles.banner, {
33407
- [styles.stacked]: isStacked
34577
+ className: classnames(styles$1.banner, {
34578
+ [styles$1.stacked]: isStacked
33408
34579
  }),
33409
34580
  onClick: () => props.onClick(),
33410
34581
  type: "button"
@@ -33419,15 +34590,15 @@ function StackedContent({
33419
34590
  title
33420
34591
  }) {
33421
34592
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
33422
- className: styles.content
34593
+ className: styles$1.content
33423
34594
  }, /*#__PURE__*/React.createElement(AskAIIcon, null), /*#__PURE__*/React.createElement("span", {
33424
- className: styles.text
34595
+ className: styles$1.text
33425
34596
  }, /*#__PURE__*/React.createElement("span", {
33426
- className: styles.title
34597
+ className: styles$1.title
33427
34598
  }, title), /*#__PURE__*/React.createElement("span", {
33428
- className: styles.subtitle
34599
+ className: styles$1.subtitle
33429
34600
  }, "Support answers tailored to you"))), /*#__PURE__*/React.createElement(tapestryReact.Icon, {
33430
- className: styles.chevron,
34601
+ className: styles$1.chevron,
33431
34602
  name: "general.rightChevron",
33432
34603
  size: "16px"
33433
34604
  }));
@@ -33437,18 +34608,18 @@ function InlineContent({
33437
34608
  title
33438
34609
  }) {
33439
34610
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
33440
- className: styles.inlineContent
34611
+ className: styles$1.inlineContent
33441
34612
  }, /*#__PURE__*/React.createElement(SparkleIcon, {
33442
- className: styles.sparkle,
34613
+ className: styles$1.sparkle,
33443
34614
  fill: "currentColor"
33444
34615
  }), /*#__PURE__*/React.createElement("span", {
33445
- className: styles.inlineTitle
34616
+ className: styles$1.inlineTitle
33446
34617
  }, title)), /*#__PURE__*/React.createElement("span", {
33447
- className: styles.action
34618
+ className: styles$1.action
33448
34619
  }, /*#__PURE__*/React.createElement("span", {
33449
- className: styles.actionLabel
34620
+ className: styles$1.actionLabel
33450
34621
  }, action), /*#__PURE__*/React.createElement(tapestryReact.Icon, {
33451
- className: styles.actionChevron,
34622
+ className: styles$1.actionChevron,
33452
34623
  name: "general.rightChevron",
33453
34624
  size: "14px"
33454
34625
  })));
@@ -33465,9 +34636,9 @@ function AskAIIcon() {
33465
34636
  const borderA = `${uid}-border-a`;
33466
34637
  const borderB = `${uid}-border-b`;
33467
34638
  return /*#__PURE__*/React.createElement("span", {
33468
- className: styles.icon
34639
+ className: styles$1.icon
33469
34640
  }, /*#__PURE__*/React.createElement("svg", {
33470
- className: styles.iconShape,
34641
+ className: styles$1.iconShape,
33471
34642
  viewBox: "0 0 32 32",
33472
34643
  fill: "none",
33473
34644
  "aria-hidden": "true"
@@ -33638,7 +34809,7 @@ function AskAIIcon() {
33638
34809
  stopColor: "#D48B91",
33639
34810
  stopOpacity: "0"
33640
34811
  })))), /*#__PURE__*/React.createElement("svg", {
33641
- className: styles.iconSparkle,
34812
+ className: styles$1.iconSparkle,
33642
34813
  viewBox: "0 0 19.4995 17.9999",
33643
34814
  "aria-hidden": "true"
33644
34815
  }, /*#__PURE__*/React.createElement("path", {
@@ -33647,6 +34818,23 @@ function AskAIIcon() {
33647
34818
  })));
33648
34819
  }
33649
34820
 
34821
+ var styles = {"button":"button-2NVI1","sparkle":"sparkle-ruJGP","label":"label-4NUGB"};
34822
+
34823
+ function AskAIButton({
34824
+ onClick,
34825
+ text = "Ask AI"
34826
+ }) {
34827
+ return /*#__PURE__*/React.createElement("button", {
34828
+ className: styles.button,
34829
+ onClick: () => onClick(),
34830
+ type: "button"
34831
+ }, /*#__PURE__*/React.createElement(SparkleIcon, {
34832
+ className: styles.sparkle
34833
+ }), /*#__PURE__*/React.createElement("span", {
34834
+ className: styles.label
34835
+ }, text));
34836
+ }
34837
+
33650
34838
  // The article heading lives in the deepest populated hierarchy level (lvl0 is
33651
34839
  // always "Planning Center").
33652
34840
  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) || "";
@@ -33802,16 +34990,24 @@ const DocumentationSearch = ({
33802
34990
  // not the path, so updating it is a same-document navigation that doesn't
33803
34991
  // reload the iframe, but also lets the route survive a forced reload (e.g.
33804
34992
  // Turbo re-parenting the topbar).
33805
- function buildPicobotSrc(origin, path) {
34993
+ //
34994
+ // `panel` loads picobot as its own toolbar panel rather than inside Helpdesk.
34995
+ function buildPicobotSrc(origin, path, {
34996
+ panel = false
34997
+ } = {}) {
33806
34998
  const [rawPath, queryPart] = path.replace(/^\/+/, "").split("?", 2);
33807
34999
  const safePath = rawPath.split("/").filter(segment => segment && segment !== "." && segment !== "..").join("/");
33808
35000
  const fragment = queryPart !== undefined ? `${safePath}?${queryPart}` : safePath;
33809
- const base = new URL("topbar", origin).toString();
35001
+ const url = new URL("topbar", origin);
35002
+ if (panel) url.searchParams.set("panel", "1");
35003
+ const base = url.toString();
33810
35004
  return fragment ? `${base}#${fragment}` : base;
33811
35005
  }
33812
35006
 
33813
35007
  const LOAD_TIMEOUT_MS = 3000;
33814
- function createPicobotIframe(picobotOrigin) {
35008
+ function createPicobotIframe(picobotOrigin, {
35009
+ panel = false
35010
+ } = {}) {
33815
35011
  return /*#__PURE__*/React.forwardRef(function PicobotIframe({
33816
35012
  currentPath: iframePath,
33817
35013
  onChangePath: onIframeChangePath,
@@ -33867,7 +35063,9 @@ function createPicobotIframe(picobotOrigin) {
33867
35063
  (_b = (_a = iframeRef.current) === null || _a === void 0 ? void 0 : _a.contentWindow) === null || _b === void 0 ? void 0 : _b.postMessage(message, picobotOrigin);
33868
35064
  }
33869
35065
  }), []);
33870
- const iframeSrc = buildPicobotSrc(picobotOrigin, loadedPath);
35066
+ const iframeSrc = buildPicobotSrc(picobotOrigin, loadedPath, {
35067
+ panel
35068
+ });
33871
35069
  return /*#__PURE__*/React.createElement("iframe", {
33872
35070
  ref: iframeRef,
33873
35071
  src: iframeSrc,
@@ -33906,22 +35104,28 @@ function HelpdeskComponent({
33906
35104
  currentPath,
33907
35105
  env,
33908
35106
  helpdeskConfig,
33909
- onChangePath
35107
+ isPicobotLoading,
35108
+ onChangePath,
35109
+ openPicobot
33910
35110
  }) {
33911
35111
  const PicobotIframe = React.useMemo(() => createPicobotIframe(pcoUrl(env)("picobot")), [env]);
35112
+ const loadingFallback = /*#__PURE__*/React.createElement(StackView$1, {
35113
+ flex: 1,
35114
+ axis: "vertical"
35115
+ }, /*#__PURE__*/React.createElement(DrawerHeader, {
35116
+ closeDrawer: closeDrawer
35117
+ }, /*#__PURE__*/React.createElement(StackView$1, {
35118
+ axis: "horizontal",
35119
+ spacing: 1
35120
+ }, /*#__PURE__*/React.createElement(DrawerHeading, null, "Help and support"), /*#__PURE__*/React.createElement(LoadingIndicator$1, null))));
35121
+ // The add-on reads its args once, so mounting before the gate answers
35122
+ // would leave out openPicobot for good.
35123
+ if (isPicobotLoading) return loadingFallback;
33912
35124
  return /*#__PURE__*/React.createElement(addOns.UiExtensionFirstPartyComponent, {
33913
35125
  grow: 1,
33914
35126
  display: "flex",
33915
35127
  extensionId: env === "staging" ? "42" : "11",
33916
- loadingFallback: /*#__PURE__*/React.createElement(StackView$1, {
33917
- flex: 1,
33918
- axis: "vertical"
33919
- }, /*#__PURE__*/React.createElement(DrawerHeader, {
33920
- closeDrawer: closeDrawer
33921
- }, /*#__PURE__*/React.createElement(StackView$1, {
33922
- axis: "horizontal",
33923
- spacing: 1
33924
- }, /*#__PURE__*/React.createElement(DrawerHeading, null, "Help and support"), /*#__PURE__*/React.createElement(LoadingIndicator$1, null)))),
35128
+ loadingFallback: loadingFallback,
33925
35129
  additionalComponents: {
33926
35130
  AskAIBanner: {
33927
35131
  component: AskAIBanner,
@@ -33941,6 +35145,18 @@ function HelpdeskComponent({
33941
35145
  }
33942
35146
  }
33943
35147
  },
35148
+ AskAIButton: {
35149
+ component: AskAIButton,
35150
+ properties: {
35151
+ onClick: {
35152
+ event: false,
35153
+ type: Function
35154
+ },
35155
+ text: {
35156
+ type: String
35157
+ }
35158
+ }
35159
+ },
33944
35160
  CalendlyWidget: {
33945
35161
  component: CalendlyWidget,
33946
35162
  properties: {
@@ -34019,7 +35235,11 @@ function HelpdeskComponent({
34019
35235
  currentPageParams: location.search,
34020
35236
  currentPath,
34021
35237
  helpdeskConfig,
34022
- onChangePath
35238
+ onChangePath,
35239
+ // Its presence tells helpdesk that Picobot has its own panel.
35240
+ ...(openPicobot && {
35241
+ openPicobot
35242
+ })
34023
35243
  }
34024
35244
  });
34025
35245
  }
@@ -34566,6 +35786,142 @@ const Notifications = {
34566
35786
  name: "Notifications"
34567
35787
  };
34568
35788
 
35789
+ 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";
35790
+ // Helpdesk reads this context off the email path to open its email form the
35791
+ // same way a handoff inside its own drawer does.
35792
+ function emailPathWithContext(context = {}) {
35793
+ const params = new URLSearchParams();
35794
+ for (const key of ["conversationId", "source", "turnIndex"]) {
35795
+ const value = context[key];
35796
+ if (value !== null && value !== undefined) params.set(key, String(value));
35797
+ }
35798
+ return `email?${params}`;
35799
+ }
35800
+ function PicobotComponent({
35801
+ clearPending,
35802
+ closeDrawer,
35803
+ currentPath,
35804
+ env,
35805
+ onChangePath,
35806
+ openTool,
35807
+ pending
35808
+ }) {
35809
+ const PicobotIframe = React.useMemo(() => createPicobotIframe(pcoUrl(env)("picobot"), {
35810
+ panel: true
35811
+ }), [env]);
35812
+ const picobotRef = React.useRef(null);
35813
+ const [isPicobotReady, setIsPicobotReady] = React.useState(false);
35814
+ // Picobot has no listener until it sends READY, so openPicobot's draft or
35815
+ // conversation waits for it.
35816
+ React.useEffect(function handOverPendingOncePicobotIsReady() {
35817
+ var _a, _b;
35818
+ if (!pending || !isPicobotReady) return;
35819
+ const {
35820
+ conversationId,
35821
+ draft,
35822
+ ticketExternalId
35823
+ } = pending;
35824
+ if (conversationId) {
35825
+ (_a = picobotRef.current) === null || _a === void 0 ? void 0 : _a.postMessage({
35826
+ payload: {
35827
+ conversation_id: conversationId,
35828
+ // Picobot asks whether this resolved the ticket, and closes it.
35829
+ ...(ticketExternalId && {
35830
+ external_id: ticketExternalId,
35831
+ source: "ticket_submitted"
35832
+ })
35833
+ },
35834
+ type: "LOAD_CONVERSATION"
35835
+ });
35836
+ } else if (draft) {
35837
+ // Not both: Picobot cancels a conversation load when a prefill
35838
+ // starts a new conversation.
35839
+ (_b = picobotRef.current) === null || _b === void 0 ? void 0 : _b.postMessage({
35840
+ payload: {
35841
+ source: "email_form",
35842
+ text: draft
35843
+ },
35844
+ type: "PREFILL_COMPOSER"
35845
+ });
35846
+ }
35847
+ clearPending();
35848
+ }, [pending, isPicobotReady, clearPending]);
35849
+ const [iframeErrored, setIframeErrored] = React.useState(false);
35850
+ const [mountKey, setMountKey] = React.useState(0);
35851
+ const handleRetry = React.useCallback(() => {
35852
+ setIframeErrored(false);
35853
+ setIsPicobotReady(false);
35854
+ setMountKey(key => key + 1);
35855
+ }, []);
35856
+ const handleMessage = React.useCallback(data => {
35857
+ var _a;
35858
+ const message = data;
35859
+ switch (message === null || message === void 0 ? void 0 : message.type) {
35860
+ case "CLOSE_DRAWER":
35861
+ return closeDrawer();
35862
+ case "READY":
35863
+ return setIsPicobotReady(true);
35864
+ case "PICOBOT_SUPPORT_HANDOFF":
35865
+ return openTool("helpdesk", ((_a = message.payload) === null || _a === void 0 ? void 0 : _a.method) === "zoom" ? "phone" : emailPathWithContext(message.payload));
35866
+ case "IFRAME_LOAD_ERROR":
35867
+ console.warn("topbar: Picobot iframe failed to load");
35868
+ return setIframeErrored(true);
35869
+ }
35870
+ }, [closeDrawer, openTool]);
35871
+ if (iframeErrored) {
35872
+ return /*#__PURE__*/React.createElement(LoadError, {
35873
+ onContactSupport: () => openTool("helpdesk", "email"),
35874
+ onRetry: handleRetry
35875
+ });
35876
+ }
35877
+ return /*#__PURE__*/React.createElement(PicobotIframe, {
35878
+ key: mountKey,
35879
+ ref: picobotRef,
35880
+ currentPath: currentPath,
35881
+ onChangePath: onChangePath,
35882
+ onMessage: handleMessage
35883
+ });
35884
+ }
35885
+ function LoadError({
35886
+ onContactSupport,
35887
+ onRetry
35888
+ }) {
35889
+ return /*#__PURE__*/React.createElement(tapestryReact.StackView, {
35890
+ axis: "vertical",
35891
+ flex: 1,
35892
+ alignment: "center",
35893
+ distribution: "center",
35894
+ padding: 3,
35895
+ spacing: 1
35896
+ }, /*#__PURE__*/React.createElement(tapestryReact.Heading, {
35897
+ level: 2
35898
+ }, "Picobot is temporarily unavailable"), /*#__PURE__*/React.createElement(tapestryReact.Text, {
35899
+ size: 5,
35900
+ color: "var(--t-text-color-secondary)"
35901
+ }, "Try again or contact support."), /*#__PURE__*/React.createElement(tapestryReact.Box, {
35902
+ paddingTop: 2
35903
+ }, /*#__PURE__*/React.createElement(tapestryReact.Button, {
35904
+ onClick: onRetry,
35905
+ iconLeft: "general.refresh",
35906
+ theme: "primary",
35907
+ variant: "fill",
35908
+ title: "Retry"
35909
+ })), /*#__PURE__*/React.createElement(tapestryReact.Button, {
35910
+ onClick: onContactSupport,
35911
+ variant: "naked",
35912
+ theme: "primary",
35913
+ title: "Contact support"
35914
+ }));
35915
+ }
35916
+ const Picobot = {
35917
+ component: PicobotComponent,
35918
+ icons: {
35919
+ staticDotPath: SPARKLE_PATH,
35920
+ staticPath: SPARKLE_PATH
35921
+ },
35922
+ name: "Picobot"
35923
+ };
35924
+
34569
35925
  const {
34570
35926
  LoadingIndicator,
34571
35927
  StackView
@@ -34961,6 +36317,41 @@ const useChatConfig = ({
34961
36317
  };
34962
36318
  };
34963
36319
 
36320
+ // Picobot's API rejects requests without a version header, and apiRequest
36321
+ // sets its own headers, so the version rides in on the fetch.
36322
+ const picobotFetch = (url, opts) => defaultFetch(url, {
36323
+ ...opts,
36324
+ headers: {
36325
+ ...(opts === null || opts === void 0 ? void 0 : opts.headers),
36326
+ "X-PCO-API-Version": "2026-08-11"
36327
+ }
36328
+ });
36329
+ // Routes wait on the gate, so a slow Picobot can't hold them up for long.
36330
+ const GATE_TIMEOUT_MS = 3000;
36331
+ const usePicobotConfig = ({
36332
+ isEnabled
36333
+ }) => {
36334
+ // Undefined until Picobot answers.
36335
+ const [panelEnabled, setPanelEnabled] = React.useState();
36336
+ React.useEffect(() => {
36337
+ if (!isEnabled) return undefined;
36338
+ // Whatever settles first wins, so a late answer can't reopen the gate.
36339
+ const settle = enabled => setPanelEnabled(current => current !== null && current !== void 0 ? current : enabled);
36340
+ const timeout = setTimeout(() => settle(false), GATE_TIMEOUT_MS);
36341
+ apiRequest(picobotFetch, `${window.location.origin}/~api/picobot/v2/me`).then(({
36342
+ json
36343
+ }) => {
36344
+ var _a, _b;
36345
+ 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);
36346
+ }).catch(() => settle(false)).finally(() => clearTimeout(timeout));
36347
+ return () => clearTimeout(timeout);
36348
+ }, [isEnabled]);
36349
+ return {
36350
+ displayPicobotIcon: panelEnabled === true,
36351
+ isLoading: isEnabled && panelEnabled === undefined
36352
+ };
36353
+ };
36354
+
34964
36355
  const Profile = {
34965
36356
  component: UserProfile,
34966
36357
  icons: {
@@ -34971,10 +36362,16 @@ const Profile = {
34971
36362
  };
34972
36363
 
34973
36364
  const DURATION = 100;
36365
+ // Exempt paths match regardless of query, so `email?conversationId=…` is
36366
+ // exempt along with `email`.
34974
36367
  function isToolPathExemptFromStorage(toolName, path) {
34975
36368
  var _a, _b;
34976
36369
  const tool = toolsRouteMap[toolName];
34977
- 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;
36370
+ const [pathWithoutQuery] = path.split("?");
36371
+ 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;
36372
+ }
36373
+ function isConversationPath(path) {
36374
+ return Boolean(path === null || path === void 0 ? void 0 : path.startsWith("conversations/"));
34978
36375
  }
34979
36376
  function scrollToolDrawerToTop(toolName) {
34980
36377
  var _a;
@@ -34987,6 +36384,7 @@ const TOOLS_MAP = {
34987
36384
  Chat,
34988
36385
  Helpdesk,
34989
36386
  Notifications,
36387
+ Picobot,
34990
36388
  Profile,
34991
36389
  Tasks
34992
36390
  };
@@ -35014,7 +36412,8 @@ function Toolbar({
35014
36412
  });
35015
36413
  const {
35016
36414
  currentContent,
35017
- open
36415
+ open,
36416
+ pending
35018
36417
  } = state;
35019
36418
  const mounted = !!currentContent;
35020
36419
  const storeData = useStore();
@@ -35027,6 +36426,15 @@ function Toolbar({
35027
36426
  ...(env === "development" ? _storeDataOverride !== null && _storeDataOverride !== void 0 ? _storeDataOverride : {} : {})
35028
36427
  };
35029
36428
  const updateStore = useUpdateStore();
36429
+ const {
36430
+ displayPicobotIcon,
36431
+ isLoading: isPicobotGateLoading
36432
+ } = usePicobotConfig({
36433
+ isEnabled: Boolean(userId)
36434
+ });
36435
+ // Picobot is a kill-switched tool: until its gate says the panel is
36436
+ // enabled, its routes are held, and if it never does, they're ignored.
36437
+ const routableTool = React.useCallback(name => name === Picobot.name.toLowerCase() && !displayPicobotIcon ? undefined : toolsRouteMap[name], [displayPicobotIcon]);
35030
36438
  React.useEffect(() => {
35031
36439
  if (open) {
35032
36440
  document.body.style.overflow = "hidden";
@@ -35114,7 +36522,9 @@ function Toolbar({
35114
36522
  });
35115
36523
  }, [clearRoute]);
35116
36524
  React.useEffect(() => {
35117
- if (!toolbarFragmentRoute) return;
36525
+ if (!toolbarFragmentRoute || !routableTool(toolbarFragmentRoute.tool)) {
36526
+ return;
36527
+ }
35118
36528
  const isExempt = isToolPathExemptFromStorage(toolbarFragmentRoute.tool, toolbarFragmentRoute.path);
35119
36529
  if (isExempt) return;
35120
36530
  const nextState = {
@@ -35124,12 +36534,32 @@ function Toolbar({
35124
36534
  if (!shallowCompare(nextState, toolStateCookie)) {
35125
36535
  setToolStateCookie(nextState);
35126
36536
  }
35127
- }, [toolbarFragmentRoute, toolStateCookie]);
36537
+ }, [toolbarFragmentRoute, toolStateCookie, routableTool]);
35128
36538
  React.useEffect(() => {
35129
- if ((toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && (currentContent !== toolsRouteMap[toolbarFragmentRoute.tool] || !open)) {
35130
- setToolAndOpenDrawer(toolsRouteMap[toolbarFragmentRoute.tool]);
36539
+ // Helpdesk conversations may belong to Picobot, so they wait for its gate
36540
+ // rather than flashing open in Helpdesk first.
36541
+ const mayRedirectToPicobot = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) === Helpdesk.name.toLowerCase() && isConversationPath(toolbarFragmentRoute.path) && (isPicobotGateLoading || displayPicobotIcon);
36542
+ const tool = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && !mayRedirectToPicobot && routableTool(toolbarFragmentRoute.tool);
36543
+ if (tool && (currentContent !== tool || !open)) {
36544
+ setToolAndOpenDrawer(tool);
35131
36545
  }
35132
- }, [setToolAndOpenDrawer, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool, currentContent, open]);
36546
+ }, [setToolAndOpenDrawer, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path, currentContent, open, routableTool, isPicobotGateLoading, displayPicobotIcon]);
36547
+ const openTool = React.useCallback((name, path, pendingValue) => {
36548
+ const tool = routableTool(name.toLowerCase());
36549
+ if (!tool) return;
36550
+ dispatch({
36551
+ payload: pendingValue === undefined ? null : {
36552
+ tool: tool.name,
36553
+ value: pendingValue
36554
+ },
36555
+ type: ActionType.setPending
36556
+ });
36557
+ onChangeToolbarFragmentRoute(tool.name.toLowerCase(), path);
36558
+ scrollToolDrawerToTop(tool.name);
36559
+ }, [onChangeToolbarFragmentRoute, routableTool]);
36560
+ const clearPending = React.useCallback(() => dispatch({
36561
+ type: ActionType.clearPending
36562
+ }), []);
35133
36563
  const toggleDrawer = content => {
35134
36564
  if (!open) {
35135
36565
  return openDrawer(content);
@@ -35146,12 +36576,52 @@ function Toolbar({
35146
36576
  joltPromise: storeData === null || storeData === void 0 ? void 0 : storeData.joltPromise,
35147
36577
  userId
35148
36578
  });
35149
- const TOOLS = [Notifications, Tasks, displayChatIcon && Chat].filter(Boolean);
36579
+ // Picobot conversations moved from Helpdesk to the Picobot panel, so
36580
+ // send old links and saved paths there once the panel is enabled.
36581
+ React.useEffect(() => {
36582
+ if (!displayPicobotIcon) return;
36583
+ if ((toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) === Helpdesk.name.toLowerCase() && isConversationPath(toolbarFragmentRoute.path)) {
36584
+ onChangeToolbarFragmentRoute(Picobot.name.toLowerCase(), toolbarFragmentRoute.path);
36585
+ }
36586
+ }, [displayPicobotIcon, toolbarFragmentRoute, onChangeToolbarFragmentRoute]);
36587
+ React.useEffect(() => {
36588
+ if (!displayPicobotIcon) return;
36589
+ const {
36590
+ helpdesk,
36591
+ picobot
36592
+ } = toolStateCookie;
36593
+ if (!isConversationPath(helpdesk)) return;
36594
+ setToolStateCookie({
36595
+ ...toolStateCookie,
36596
+ helpdesk: "",
36597
+ picobot: picobot || helpdesk
36598
+ });
36599
+ }, [displayPicobotIcon, toolStateCookie]);
36600
+ const openPicobot = React.useCallback(options => {
36601
+ var _a;
36602
+ // Moving an open Picobot off its path reloads or navigates the iframe,
36603
+ // which would race the options it's being handed.
36604
+ const path = currentContent === Picobot ? (_a = toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path) !== null && _a !== void 0 ? _a : "" : "";
36605
+ openTool(Picobot.name, path, options);
36606
+ }, [openTool, currentContent, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path]);
36607
+ React.useEffect(() => {
36608
+ if (!displayPicobotIcon) return undefined;
36609
+ window.topbar = window.topbar || {};
36610
+ window.topbar.openPicobot = openPicobot;
36611
+ return () => {
36612
+ delete window.topbar.openPicobot;
36613
+ };
36614
+ }, [displayPicobotIcon, openPicobot]);
36615
+ const TOOLS = [Notifications, Tasks, displayChatIcon && Chat, displayPicobotIcon && Picobot, isPcoTestHost() && DeveloperTools].filter(Boolean);
35150
36616
  const handleToolOnClick = tool => {
35151
36617
  toggleDrawer(tool);
35152
36618
  scrollToolDrawerToTop(tool.name);
35153
36619
  };
35154
36620
  const toolSpecificProps = {
36621
+ Helpdesk: {
36622
+ isPicobotLoading: isPicobotGateLoading,
36623
+ openPicobot: displayPicobotIcon ? openPicobot : undefined
36624
+ },
35155
36625
  Profile: {
35156
36626
  appName,
35157
36627
  closeDrawer,
@@ -35174,6 +36644,7 @@ function Toolbar({
35174
36644
  const routeIsForCurrentTool = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && toolsRouteMap[toolbarFragmentRoute.tool] === currentContent;
35175
36645
  const currentToolProps = {
35176
36646
  appName,
36647
+ clearPending,
35177
36648
  closeDrawer,
35178
36649
  currentPath: routeIsForCurrentTool ? toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path : null,
35179
36650
  env,
@@ -35182,6 +36653,8 @@ function Toolbar({
35182
36653
  joltPromise,
35183
36654
  onChangePath,
35184
36655
  openDrawer: () => currentContent && openDrawer(currentContent),
36656
+ openTool,
36657
+ 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,
35185
36658
  userId,
35186
36659
  ...(currentContent ? toolSpecificProps[currentContent.name] : {})
35187
36660
  };
@@ -35192,38 +36665,37 @@ function Toolbar({
35192
36665
  return /*#__PURE__*/React.createElement(tapestryReact.ThemeProvider, {
35193
36666
  theme: topbarTheme
35194
36667
  }, /*#__PURE__*/React.createElement(addOns.UiExtensionDataContext, null, /*#__PURE__*/React.createElement("div", {
35195
- className: styles$1["toolbar"],
36668
+ className: styles$2["toolbar"],
35196
36669
  "data-pendo": "toolbar-profile-support-wrapper"
35197
36670
  }, /*#__PURE__*/React.createElement(ToolbarButton, {
35198
36671
  "aria-label": `Open ${userName}’s profile drawer`,
35199
36672
  onClick: () => toggleDrawer(Profile),
35200
36673
  "data-pendo": "toolbar-profile",
35201
- className: styles$1["toolbar-button--avatar"]
36674
+ className: styles$2["toolbar-button--avatar"]
35202
36675
  }, /*#__PURE__*/React.createElement(Avatar, {
35203
36676
  env: env,
35204
36677
  url: userAvatarPath
35205
36678
  })), /*#__PURE__*/React.createElement(ToolbarButton, {
35206
- className: styles$1["toolbar-button--home"],
36679
+ className: styles$2["toolbar-button--home"],
35207
36680
  onClick: () => closeDrawer()
35208
36681
  }, /*#__PURE__*/React.createElement(tapestryReact.Icon, {
35209
36682
  name: "toolbar.planningCenter",
35210
36683
  size: "24px"
35211
36684
  })), [HelpDeskTool, ...TOOLS].map(tool => {
36685
+ var _a;
35212
36686
  const current = open && currentContent === tool;
35213
36687
  const indicator = indicatorState[tool.name];
35214
36688
  return /*#__PURE__*/React.createElement(ToolbarButton, {
35215
36689
  current: current,
35216
36690
  onClick: () => handleToolOnClick(tool),
35217
36691
  id: getDataPendoAttribute(tool.name),
35218
- "aria-label": `Open ${tool.name} drawer`,
36692
+ "aria-label": `Open ${(_a = tool.label) !== null && _a !== void 0 ? _a : tool.name} drawer`,
35219
36693
  "data-pendo": getDataPendoAttribute(tool.name),
35220
36694
  key: tool.name
35221
- }, !indicator && (/*#__PURE__*/React.createElement(ToolbarButtonIcon, {
35222
- path: tool.icons.staticPath
35223
- })), indicator && (/*#__PURE__*/React.createElement(ToolbarButtonIcon, {
35224
- path: tool.icons.staticDotPath,
35225
- dot: true
35226
- })));
36695
+ }, /*#__PURE__*/React.createElement(ToolbarToolIcon, {
36696
+ icons: tool.icons,
36697
+ dot: !!indicator
36698
+ }));
35227
36699
  })), /*#__PURE__*/React.createElement(DrawerWrap, {
35228
36700
  isVisible: open
35229
36701
  }, mounted && (/*#__PURE__*/React.createElement(Drawer, {
@@ -35241,12 +36713,25 @@ const ToolbarButton = ({
35241
36713
  }) => {
35242
36714
  return /*#__PURE__*/React.createElement("button", {
35243
36715
  type: "button",
35244
- className: classnames(styles$1["toolbar-button"], className, current && styles$1["is-current"]),
36716
+ className: classnames(styles$2["toolbar-button"], className, current && styles$2["is-current"]),
35245
36717
  onClick: onClick,
35246
36718
  "data-toolbar-link": "true",
35247
36719
  ...restProps
35248
36720
  }, children);
35249
36721
  };
36722
+ function ToolbarToolIcon({
36723
+ dot,
36724
+ icons
36725
+ }) {
36726
+ if ("component" in icons) {
36727
+ return /*#__PURE__*/React.createElement(icons.component, null);
36728
+ } else {
36729
+ return /*#__PURE__*/React.createElement(ToolbarButtonIcon, {
36730
+ path: dot ? icons.staticDotPath : icons.staticPath,
36731
+ dot: dot
36732
+ });
36733
+ }
36734
+ }
35250
36735
  function ToolbarButtonIcon({
35251
36736
  dot,
35252
36737
  path