@planningcenter/topbar 12.4.0-rc.0 → 12.4.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.
@@ -17,7 +17,7 @@ import { useSearchBox, useHits, InstantSearch, Configure } from 'react-instantse
17
17
  import { TransitionGroup, Transition } from 'react-transition-group';
18
18
  import datetimeFmt from '@planningcenter/datetime-fmt';
19
19
 
20
- var styles$l = {"accessibility-toolbar":"accessibility-toolbar-jgjUG"};
20
+ var styles$m = {"accessibility-toolbar":"accessibility-toolbar-jgjUG"};
21
21
 
22
22
  const AccessibilityToolbar = ({
23
23
  children
@@ -29,7 +29,7 @@ const AccessibilityToolbar = ({
29
29
  (_a = document.getElementById("main-content")) === null || _a === void 0 ? void 0 : _a.focus();
30
30
  };
31
31
  return /*#__PURE__*/React__default.createElement("div", {
32
- className: styles$l["accessibility-toolbar"]
32
+ className: styles$m["accessibility-toolbar"]
33
33
  }, /*#__PURE__*/React__default.createElement(Link, {
34
34
  href: "#main-content",
35
35
  kind: "primary",
@@ -348,11 +348,11 @@ const X = ({
348
348
  fill: fill
349
349
  })));
350
350
 
351
- 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"};
351
+ var styles$l = {"announcement":"announcement-pI5tM","announcement--platform":"announcement--platform-EcUn2","announcement--promo":"announcement--promo-FarUk","announcement-icon":"announcement-icon-Ov39-","announcement-content":"announcement-content-q2ad6","dismiss":"dismiss-wUAoU","dismiss--promo":"dismiss--promo-7zvO4"};
352
352
 
353
353
  function AnnouncementIcon() {
354
354
  return /*#__PURE__*/React__default.createElement("svg", {
355
- className: styles$k["announcement-icon"],
355
+ className: styles$l["announcement-icon"],
356
356
  width: "16",
357
357
  height: "16",
358
358
  viewBox: "0 0 16 16",
@@ -477,7 +477,7 @@ let StyledAnnouncement$1 = class StyledAnnouncement extends React__default.Compo
477
477
  } = this.props;
478
478
  const isPromo = variant === "promo";
479
479
  return /*#__PURE__*/React__default.createElement("div", {
480
- className: classnames(styles$k["announcement"], styles$k[`announcement--${variant !== null && variant !== void 0 ? variant : "platform"}`]),
480
+ className: classnames(styles$l["announcement"], styles$l[`announcement--${variant !== null && variant !== void 0 ? variant : "platform"}`]),
481
481
  "data-announcement-variant": variant !== null && variant !== void 0 ? variant : "platform",
482
482
  style: isPromo ? {
483
483
  backgroundImage: promoBackgroundUrl,
@@ -486,7 +486,7 @@ let StyledAnnouncement$1 = class StyledAnnouncement extends React__default.Compo
486
486
  backgroundSize: "cover"
487
487
  } : undefined
488
488
  }, !isPromo && /*#__PURE__*/React__default.createElement(AnnouncementIcon, null), /*#__PURE__*/React__default.createElement("div", {
489
- className: styles$k["announcement-content"],
489
+ className: styles$l["announcement-content"],
490
490
  style: style,
491
491
  ...platformProps
492
492
  }));
@@ -535,7 +535,7 @@ function StyledDismissButton$1({
535
535
  const isPromo = variant === "promo";
536
536
  return /*#__PURE__*/React__default.createElement("button", {
537
537
  type: "button",
538
- className: classnames(styles$k.dismiss, isPromo && styles$k["dismiss--promo"]),
538
+ className: classnames(styles$l.dismiss, isPromo && styles$l["dismiss--promo"]),
539
539
  ...props
540
540
  }, /*#__PURE__*/React__default.createElement(X, {
541
541
  fill: isPromo ? "currentColor" : "white",
@@ -615,9 +615,9 @@ class StyledAnnouncement extends React__default.Component {
615
615
  ...platformProps
616
616
  } = this.props;
617
617
  return /*#__PURE__*/React__default.createElement("div", {
618
- className: styles$k["announcement"]
618
+ className: styles$l["announcement"]
619
619
  }, /*#__PURE__*/React__default.createElement(AnnouncementIcon, null), /*#__PURE__*/React__default.createElement("div", {
620
- className: styles$k["announcement-content"],
620
+ className: styles$l["announcement-content"],
621
621
  style: style,
622
622
  ...platformProps
623
623
  }));
@@ -628,7 +628,7 @@ function StyledDismissButton({
628
628
  }) {
629
629
  return /*#__PURE__*/React__default.createElement("button", {
630
630
  type: "button",
631
- className: styles$k.dismiss,
631
+ className: styles$l.dismiss,
632
632
  ...props
633
633
  }, /*#__PURE__*/React__default.createElement(X, {
634
634
  fill: "white",
@@ -1750,7 +1750,7 @@ function MonoAppIcon({
1750
1750
  })) : (/*#__PURE__*/React__default.createElement("div", null, "appName not supported")));
1751
1751
  }
1752
1752
 
1753
- var styles$j = {"icon-slot":"icon-slot-m3ZIk","name":"name-UNqbW"};
1753
+ var styles$k = {"icon-slot":"icon-slot-m3ZIk","name":"name-UNqbW"};
1754
1754
 
1755
1755
  const AppRow = ({
1756
1756
  appName,
@@ -1759,24 +1759,24 @@ const AppRow = ({
1759
1759
  }) => {
1760
1760
  if (appName === "Accounts") {
1761
1761
  return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("span", {
1762
- className: styles$j["icon-slot"]
1762
+ className: styles$k["icon-slot"]
1763
1763
  }, /*#__PURE__*/React__default.createElement(MonoAppIcon, {
1764
1764
  appName: "Accounts",
1765
1765
  colors: colors,
1766
1766
  size: iconSize
1767
1767
  })), /*#__PURE__*/React__default.createElement("span", {
1768
- className: styles$j.name
1768
+ className: styles$k.name
1769
1769
  }, "Account settings"));
1770
1770
  }
1771
1771
  return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(ColorAppIcon, {
1772
1772
  appName: appName.replace(/[\s-]/, ""),
1773
1773
  size: iconSize
1774
1774
  }), /*#__PURE__*/React__default.createElement("span", {
1775
- className: styles$j.name
1775
+ className: styles$k.name
1776
1776
  }, appName));
1777
1777
  };
1778
1778
 
1779
- var styles$i = {"button":"button-XMbeQ","label":"label-884of","chevron":"chevron-RRg9k"};
1779
+ var styles$j = {"button":"button-XMbeQ","label":"label-884of","chevron":"chevron-RRg9k"};
1780
1780
 
1781
1781
  const AppsButton = /*#__PURE__*/React__default.forwardRef(({
1782
1782
  appName,
@@ -1788,10 +1788,10 @@ const AppsButton = /*#__PURE__*/React__default.forwardRef(({
1788
1788
  ...restProps,
1789
1789
  ref: ref,
1790
1790
  "aria-label": `App switcher: ${appName}`,
1791
- className: styles$i.button,
1791
+ className: styles$j.button,
1792
1792
  kind: "neutral-inline",
1793
1793
  label: /*#__PURE__*/React__default.createElement("span", {
1794
- className: styles$i.label
1794
+ className: styles$j.label
1795
1795
  }, isAccounts ? "Account settings" : appName),
1796
1796
  prefix: isAccounts ? (/*#__PURE__*/React__default.createElement(MonoAppIcon, {
1797
1797
  appName: "Accounts",
@@ -1802,13 +1802,13 @@ const AppsButton = /*#__PURE__*/React__default.forwardRef(({
1802
1802
  size: 22
1803
1803
  })),
1804
1804
  suffix: /*#__PURE__*/React__default.createElement(DisclosureChevronIcon, {
1805
- className: `${styles$i.chevron} symbol`
1805
+ className: `${styles$j.chevron} symbol`
1806
1806
  })
1807
1807
  });
1808
1808
  });
1809
1809
  AppsButton.displayName = "AppsButton";
1810
1810
 
1811
- 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"};
1811
+ var styles$i = {"nav":"nav-y-6ML","popover-content":"popover-content-jOAAY","list":"list-jUxvp","link":"link-LMp2h","current":"current-4U4VL","accounts-item":"accounts-item-erJW0","accounts-link":"accounts-link-4jwPW"};
1812
1812
 
1813
1813
  const AppsMenu = ({
1814
1814
  appName,
@@ -1829,8 +1829,8 @@ const AppsMenu = ({
1829
1829
  }, /*#__PURE__*/React__default.createElement("a", {
1830
1830
  "aria-current": isCurrent ? "page" : undefined,
1831
1831
  "aria-label": name,
1832
- className: classnames(styles$h.link, {
1833
- [styles$h.current]: isCurrent
1832
+ className: classnames(styles$i.link, {
1833
+ [styles$i.current]: isCurrent
1834
1834
  }),
1835
1835
  "data-pendo": `app-switcher--change-app--${name}`,
1836
1836
  "data-turbolinks": false,
@@ -1846,22 +1846,22 @@ const AppsMenu = ({
1846
1846
  colors: colors
1847
1847
  })), /*#__PURE__*/React__default.createElement(PopoverContent, {
1848
1848
  "aria-label": "Switch app",
1849
- className: styles$h["popover-content"],
1849
+ className: styles$i["popover-content"],
1850
1850
  placement: "bottom start"
1851
1851
  }, /*#__PURE__*/React__default.createElement("nav", {
1852
- className: styles$h.nav,
1852
+ className: styles$i.nav,
1853
1853
  "data-pendo": "app-switcher"
1854
1854
  }, /*#__PURE__*/React__default.createElement("ul", {
1855
- className: styles$h.list,
1855
+ className: styles$i.list,
1856
1856
  role: "list"
1857
1857
  }, homeProduct && renderProductLink(homeProduct.name), restProducts.map(({
1858
1858
  name
1859
1859
  }) => renderProductLink(name)), accountsProduct && (/*#__PURE__*/React__default.createElement("li", {
1860
- className: styles$h["accounts-item"]
1860
+ className: styles$i["accounts-item"]
1861
1861
  }, /*#__PURE__*/React__default.createElement("a", {
1862
1862
  "aria-current": appName === accountsProduct.name ? "page" : undefined,
1863
1863
  "aria-label": "Account settings",
1864
- className: classnames(styles$h.link, styles$h["accounts-link"]),
1864
+ className: classnames(styles$i.link, styles$i["accounts-link"]),
1865
1865
  "data-pendo": "app-switcher--change-app--Accounts",
1866
1866
  "data-turbolinks": false,
1867
1867
  href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(accountsProduct.name)
@@ -2069,7 +2069,7 @@ function MonoAppName({
2069
2069
  })) : (/*#__PURE__*/React__default.createElement("div", null, "appName not supported"));
2070
2070
  }
2071
2071
 
2072
- var styles$g = {"unbutton":"unbutton-lEbTR"};
2072
+ var styles$h = {"unbutton":"unbutton-lEbTR"};
2073
2073
 
2074
2074
  function Unbutton({
2075
2075
  className,
@@ -2078,7 +2078,7 @@ function Unbutton({
2078
2078
  ...nativeProps
2079
2079
  }) {
2080
2080
  return /*#__PURE__*/React__default.createElement("button", {
2081
- className: classnames(styles$g.unbutton, className),
2081
+ className: classnames(styles$h.unbutton, className),
2082
2082
  style: {
2083
2083
  ...style
2084
2084
  },
@@ -2087,7 +2087,7 @@ function Unbutton({
2087
2087
  });
2088
2088
  }
2089
2089
 
2090
- var styles$f = {"button":"button-wsipc","button-accounts":"button-accounts-zQmBM","chevron":"chevron-obyBF","is-expanded":"is-expanded-PoCQ9"};
2090
+ var styles$g = {"button":"button-wsipc","button-accounts":"button-accounts-zQmBM","chevron":"chevron-obyBF","is-expanded":"is-expanded-PoCQ9"};
2091
2091
 
2092
2092
  const LegacyAppsButton = ({
2093
2093
  appName,
@@ -2102,8 +2102,8 @@ const LegacyAppsButton = ({
2102
2102
  "aria-haspopup": "true",
2103
2103
  "aria-label": `App switcher: ${appName}`,
2104
2104
  onClick: onClick,
2105
- className: classnames(styles$f.button, {
2106
- [styles$f["is-expanded"]]: expanded
2105
+ className: classnames(styles$g.button, {
2106
+ [styles$g["is-expanded"]]: expanded
2107
2107
  }),
2108
2108
  type: "button",
2109
2109
  style: style,
@@ -2112,7 +2112,7 @@ const LegacyAppsButton = ({
2112
2112
  appName: "Accounts",
2113
2113
  colors: colors
2114
2114
  }), /*#__PURE__*/React__default.createElement("div", {
2115
- className: styles$f["button-accounts"]
2115
+ className: styles$g["button-accounts"]
2116
2116
  }, "Account settings"))) : (/*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(ColorAppIcon, {
2117
2117
  appName: appName.replace(/[\s-]/, "")
2118
2118
  }), /*#__PURE__*/React__default.createElement(MonoAppName, {
@@ -2120,7 +2120,7 @@ const LegacyAppsButton = ({
2120
2120
  key: "mono-app-name",
2121
2121
  size: 24
2122
2122
  }))), /*#__PURE__*/React__default.createElement(DisclosureChevronIcon, {
2123
- className: styles$f.chevron,
2123
+ className: styles$g.chevron,
2124
2124
  key: "disclosure-chevron-icon"
2125
2125
  }));
2126
2126
  };
@@ -2253,7 +2253,7 @@ class Outsider extends React__default.Component {
2253
2253
  }
2254
2254
  }
2255
2255
 
2256
- var styles$e = {"popup-root":"popup-root-SeL5u","popup":"popup-nQXYW","is-visible":"is-visible-3DAUP"};
2256
+ var styles$f = {"popup-root":"popup-root-SeL5u","popup":"popup-nQXYW","is-visible":"is-visible-3DAUP"};
2257
2257
 
2258
2258
  const Popup = ({
2259
2259
  className,
@@ -2261,8 +2261,8 @@ const Popup = ({
2261
2261
  isVisible,
2262
2262
  ...nativeProps
2263
2263
  }) => (/*#__PURE__*/React__default.createElement(C, {
2264
- className: classnames(styles$e.popup, {
2265
- [styles$e["is-visible"]]: isVisible
2264
+ className: classnames(styles$f.popup, {
2265
+ [styles$f["is-visible"]]: isVisible
2266
2266
  }, className),
2267
2267
  ...nativeProps
2268
2268
  }));
@@ -2270,11 +2270,11 @@ const Popup = ({
2270
2270
  const PopupRoot = ({
2271
2271
  ...nativeProps
2272
2272
  }) => (/*#__PURE__*/React__default.createElement("div", {
2273
- className: styles$e["popup-root"],
2273
+ className: styles$f["popup-root"],
2274
2274
  ...nativeProps
2275
2275
  }));
2276
2276
 
2277
- var styles$d = {"popup":"popup-bVhSa"};
2277
+ var styles$e = {"popup":"popup-bVhSa"};
2278
2278
 
2279
2279
  const LegacyAppsMenu = ({
2280
2280
  appName,
@@ -2312,7 +2312,7 @@ const LegacyAppsMenu = ({
2312
2312
  cleanup: !visible,
2313
2313
  component: Outsider,
2314
2314
  onOutsideClick: toggle,
2315
- className: styles$d.popup,
2315
+ className: styles$e.popup,
2316
2316
  isVisible: visible
2317
2317
  }, /*#__PURE__*/React__default.createElement("menu", {
2318
2318
  ref: menuRef,
@@ -3225,27 +3225,34 @@ const useUpdateStore = () => useCallback(updates => {
3225
3225
  const isMac = () => /Mac|iPhone|iPad|iPod/.test(navigator.platform);
3226
3226
  const hasCommandKey = event => isMac() ? event.metaKey : event.ctrlKey;
3227
3227
  const commandKeyLabel = () => isMac() ? "⌘" : "Ctrl+";
3228
+ const isSearchShortcut = event => event.key.toLowerCase() === "k" && !event.altKey && !event.shiftKey && hasCommandKey(event);
3229
+ // Products opened their own search with `/` before this one replaced it.
3230
+ const isLegacySearchShortcut = event => event.key === "/" && !event.altKey && !event.ctrlKey && !event.metaKey && !isTypingIn(event.target);
3231
+ const isCopyShortcut = event => event.key.toLowerCase() === "c" && !event.altKey && !event.shiftKey && hasCommandKey(event);
3232
+ const isTypingIn = target => target instanceof Element && target.closest('input, select, textarea, [contenteditable]:not([contenteditable="false"])') !== null;
3233
+ const isTypingKey = event => event.key.length === 1 && event.key !== " " && !event.altKey && !event.ctrlKey && !event.metaKey;
3234
+ const verticalStep = key => ({
3235
+ ArrowDown: 1,
3236
+ ArrowUp: -1
3237
+ })[key];
3238
+ const horizontalStep = key => ({
3239
+ ArrowLeft: -1,
3240
+ ArrowRight: 1
3241
+ })[key];
3228
3242
 
3229
- const STORAGE_KEY = "topbar.product_search.last_product";
3230
- function readLastProduct() {
3231
- try {
3232
- return localStorage.getItem(STORAGE_KEY);
3233
- } catch {
3234
- return null;
3235
- }
3236
- }
3237
- function saveLastProduct(slug) {
3238
- try {
3239
- localStorage.setItem(STORAGE_KEY, slug);
3240
- } catch {
3241
- // Storage can be full or blocked; search still works without it.
3242
- }
3243
+ function SearchButton({
3244
+ onClick
3245
+ }) {
3246
+ return /*#__PURE__*/React__default.createElement("button", {
3247
+ "aria-keyshortcuts": `${isMac() ? "Meta+K" : "Control+K"} /`,
3248
+ "aria-label": "Search",
3249
+ onClick: onClick,
3250
+ type: "button"
3251
+ }, /*#__PURE__*/React__default.createElement(Spyglass, {
3252
+ fill: "currentColor"
3253
+ }));
3243
3254
  }
3244
3255
 
3245
- var styles$c = {"visuallyHidden":"visuallyHidden-C28ju"};
3246
-
3247
- var styles$b = {"tab":"tab-lQLDx"};
3248
-
3249
3256
  // useId isn't available on React 17, which our peer range still supports.
3250
3257
  let instanceCount$1 = 0;
3251
3258
  function useInstanceId(prefix) {
@@ -3253,58 +3260,237 @@ function useInstanceId(prefix) {
3253
3260
  return id;
3254
3261
  }
3255
3262
 
3256
- function ProductTabs({
3257
- children,
3258
- onKeyDown,
3259
- onSelect,
3260
- products,
3261
- selected
3262
- }) {
3263
- const id = useInstanceId("product-tabs");
3264
- const tabId = slug => `${id}-tab-${slug}`;
3265
- const tabRefs = React__default.useRef(new Map());
3266
- const handleKeyDown = event => {
3267
- var _a;
3268
- const offset = {
3269
- ArrowLeft: -1,
3270
- ArrowRight: 1
3271
- }[event.key];
3272
- if (offset) {
3273
- event.preventDefault();
3274
- const index = products.indexOf(selected);
3275
- const next = products[(index + offset + products.length) % products.length];
3276
- onSelect(next.slug);
3277
- (_a = tabRefs.current.get(next.slug)) === null || _a === void 0 ? void 0 : _a.focus();
3263
+ // The versions home-mobile pins, so web and mobile read the same response
3264
+ // shapes. Some endpoints only exist from a version on: Registrations' me,
3265
+ // product_search_results, and pcrl_lookups need 2025-05-01.
3266
+ const API_VERSIONS = {
3267
+ calendar: "2022-07-07",
3268
+ "check-ins": "2025-05-28",
3269
+ giving: "2019-10-18",
3270
+ groups: "2023-07-10",
3271
+ home: "2018-08-01",
3272
+ people: "2023-03-21",
3273
+ publishing: "2024-03-25",
3274
+ registrations: "2025-05-01",
3275
+ services: "2018-11-01"
3276
+ };
3277
+ // For a `/~api/{product}/v2/...` URL, the header that pins its product's
3278
+ // version.
3279
+ function apiVersionHeaders(url) {
3280
+ const [,, product] = new URL(url).pathname.split("/");
3281
+ const version = API_VERSIONS[product];
3282
+ if (version) {
3283
+ return {
3284
+ "X-PCO-API-Version": version
3285
+ };
3286
+ } else {
3287
+ return {};
3288
+ }
3289
+ }
3290
+
3291
+ const CACHE_TTL_MS = 60000;
3292
+ const MAX_ENTRIES$1 = 50;
3293
+ const cache = new Map();
3294
+ function cachedRequest(url) {
3295
+ const cached = cache.get(url);
3296
+ if (cached && cached.expiresAt > Date.now()) {
3297
+ // Re-inserting keeps the Map in recently-used order for eviction.
3298
+ cache.delete(url);
3299
+ cache.set(url, cached);
3300
+ return cached.promise;
3301
+ } else {
3302
+ return fetchIntoCache(url);
3303
+ }
3304
+ }
3305
+ function fetchIntoCache(url) {
3306
+ const promise = apiRequest(defaultFetch, url, {
3307
+ headers: apiVersionHeaders(url)
3308
+ }).then(({
3309
+ json,
3310
+ ok
3311
+ }) => {
3312
+ if (!ok || json === null) throw new Error(`Request failed: ${url}`);
3313
+ return json;
3314
+ });
3315
+ // In-flight entries never expire, so every caller shares the one fetch.
3316
+ const entry = {
3317
+ expiresAt: Infinity,
3318
+ promise
3319
+ };
3320
+ cache.set(url, entry);
3321
+ promise.then(() => entry.expiresAt = Date.now() + CACHE_TTL_MS, () => cache.get(url) === entry && cache.delete(url));
3322
+ if (cache.size > MAX_ENTRIES$1) cache.delete(cache.keys().next().value);
3323
+ return promise;
3324
+ }
3325
+ function useCachedRequest(url) {
3326
+ const [settled, setSettled] = React__default.useState();
3327
+ React__default.useEffect(() => {
3328
+ if (url) {
3329
+ let current = true;
3330
+ cachedRequest(url).then(data => current && setSettled({
3331
+ data,
3332
+ url
3333
+ }), () => current && setSettled({
3334
+ failed: true,
3335
+ url
3336
+ }));
3337
+ return () => {
3338
+ current = false;
3339
+ };
3278
3340
  } else {
3279
- onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
3341
+ return undefined;
3342
+ }
3343
+ }, [url]);
3344
+ if (!url) {
3345
+ return {
3346
+ status: "idle"
3347
+ };
3348
+ } else if ((settled === null || settled === void 0 ? void 0 : settled.url) !== url) {
3349
+ return {
3350
+ data: settled && "data" in settled ? settled.data : undefined,
3351
+ status: "loading"
3352
+ };
3353
+ } else if ("data" in settled) {
3354
+ return {
3355
+ data: settled.data,
3356
+ status: "done"
3357
+ };
3358
+ } else {
3359
+ return {
3360
+ status: "error"
3361
+ };
3362
+ }
3363
+ }
3364
+ // One request per URL, for callers that fan out to a varying number of
3365
+ // endpoints. Unlike `useCachedRequest`, a URL that hasn't settled yet has no
3366
+ // earlier data to fall back on.
3367
+ function useCachedRequests(urls) {
3368
+ const [settled, setSettled] = React__default.useState({});
3369
+ // Keyed by value because each settle re-renders the caller, and a caller
3370
+ // that builds a fresh `urls` array on render would otherwise loop forever.
3371
+ const key = urls.join("\n");
3372
+ React__default.useEffect(() => {
3373
+ let current = true;
3374
+ const settle = result => current && setSettled(prev => ({
3375
+ ...prev,
3376
+ [result.url]: result
3377
+ }));
3378
+ for (const url of key.split("\n").filter(Boolean)) {
3379
+ cachedRequest(url).then(data => settle({
3380
+ data,
3381
+ url
3382
+ }), () => settle({
3383
+ failed: true,
3384
+ url
3385
+ }));
3280
3386
  }
3387
+ return () => {
3388
+ current = false;
3389
+ };
3390
+ }, [key]);
3391
+ return urls.map(url => {
3392
+ const result = settled[url];
3393
+ if (!result) {
3394
+ return {
3395
+ status: "loading"
3396
+ };
3397
+ } else if ("data" in result) {
3398
+ return {
3399
+ data: result.data,
3400
+ status: "done"
3401
+ };
3402
+ } else {
3403
+ return {
3404
+ status: "error"
3405
+ };
3406
+ }
3407
+ });
3408
+ }
3409
+
3410
+ function productSearchUrl(product, query) {
3411
+ const params = new URLSearchParams({
3412
+ per_page: "100",
3413
+ "where[term]": query
3414
+ });
3415
+ return `${window.location.origin}/~api/${product}/v2/product_search_results?${params}`;
3416
+ }
3417
+ function groupByType({
3418
+ data,
3419
+ meta
3420
+ }) {
3421
+ var _a, _b;
3422
+ const typeOrder = (_a = meta === null || meta === void 0 ? void 0 : meta.type_order) !== null && _a !== void 0 ? _a : [];
3423
+ const groups = new Map();
3424
+ for (const result of data) {
3425
+ const {
3426
+ type
3427
+ } = result.attributes;
3428
+ if (!groups.has(type)) groups.set(type, {
3429
+ results: [],
3430
+ type
3431
+ });
3432
+ (_b = groups.get(type)) === null || _b === void 0 ? void 0 : _b.results.push(result);
3433
+ }
3434
+ const rank = type => {
3435
+ const index = typeOrder.indexOf(type);
3436
+ return index === -1 ? typeOrder.length : index;
3281
3437
  };
3282
- return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("div", {
3283
- "aria-label": "Product",
3284
- onKeyDown: handleKeyDown,
3285
- role: "tablist"
3286
- }, products.map(product => (/*#__PURE__*/React__default.createElement("button", {
3287
- "aria-controls": `${id}-panel`,
3288
- "aria-selected": product === selected,
3289
- className: styles$b.tab,
3290
- id: tabId(product.slug),
3291
- key: product.slug,
3292
- onClick: () => onSelect(product.slug),
3293
- ref: element => {
3294
- if (element) {
3295
- tabRefs.current.set(product.slug, element);
3296
- } else {
3297
- tabRefs.current.delete(product.slug);
3298
- }
3299
- },
3300
- role: "tab",
3301
- tabIndex: product === selected ? 0 : -1,
3302
- type: "button"
3303
- }, product.appName)))), /*#__PURE__*/React__default.createElement("div", {
3304
- "aria-labelledby": tabId(selected.slug),
3305
- id: `${id}-panel`,
3306
- role: "tabpanel"
3307
- }, children));
3438
+ return [...groups.values()].sort((a, b) => rank(a.type) - rank(b.type));
3439
+ }
3440
+
3441
+ function useDebouncedValue(value, delay) {
3442
+ const [debounced, setDebounced] = React__default.useState(value);
3443
+ React__default.useEffect(() => {
3444
+ const timeout = setTimeout(() => setDebounced(value), delay);
3445
+ return () => clearTimeout(timeout);
3446
+ }, [value, delay]);
3447
+ return debounced;
3448
+ }
3449
+
3450
+ const SEARCH_DEBOUNCE_MS = 300;
3451
+ function useProductSearch(product, term) {
3452
+ const query = term.trim();
3453
+ const debouncedQuery = useDebouncedValue(query, SEARCH_DEBOUNCE_MS);
3454
+ const request = useCachedRequest(debouncedQuery ? productSearchUrl(product, debouncedQuery) : null);
3455
+ const response = "data" in request ? request.data : undefined;
3456
+ const settledProduct = useSettledProduct(product, request.status);
3457
+ // While a request loads, the previous response stays on screen, but only
3458
+ // if it was for this product: People results must never show under the
3459
+ // Services tab.
3460
+ const visibleResponse = request.status === "done" || settledProduct === product ? response : undefined;
3461
+ const groups = React__default.useMemo(() => visibleResponse && groupByType(visibleResponse), [visibleResponse]);
3462
+ if (!query) {
3463
+ return {
3464
+ status: "idle"
3465
+ };
3466
+ } else if (query !== debouncedQuery) {
3467
+ return {
3468
+ groups,
3469
+ status: "loading"
3470
+ };
3471
+ } else if (request.status === "done") {
3472
+ return {
3473
+ groups: groups !== null && groups !== void 0 ? groups : [],
3474
+ status: "done"
3475
+ };
3476
+ } else if (request.status === "error") {
3477
+ return {
3478
+ status: "error"
3479
+ };
3480
+ } else {
3481
+ return {
3482
+ groups,
3483
+ status: "loading"
3484
+ };
3485
+ }
3486
+ }
3487
+ // The product the most recent finished request was for.
3488
+ function useSettledProduct(product, status) {
3489
+ const [settledProduct, setSettledProduct] = React__default.useState();
3490
+ React__default.useEffect(() => {
3491
+ if (status === "done") setSettledProduct(product);
3492
+ }, [product, status]);
3493
+ return settledProduct;
3308
3494
  }
3309
3495
 
3310
3496
  function parseValue(element) {
@@ -3373,10 +3559,14 @@ function useRecentlyVisitableMetadata() {
3373
3559
  return useSyncExternalStore(recentlyVisitableMetadataStore.subscribe, () => recentlyVisitableMetadataStore.getState().metadata);
3374
3560
  }
3375
3561
 
3376
- const MAX_ENTRIES$1 = 20;
3377
- const FORMAT_VERSION = 1;
3562
+ // Entries match the ones home-mobile stores, so one list can sync across web
3563
+ // and mobile.
3564
+ const ENTRY_VERSION = 3;
3565
+ const MAX_ENTRIES = 50;
3378
3566
  const MAX_TEXT_LENGTH = 200;
3379
3567
  const PRODUCTS = ["accounts", "calendar", "check-ins", "giving", "groups", "home", "login", "people", "publishing", "registrations", "services"];
3568
+ // Login has no pages worth returning to.
3569
+ const APPS = PRODUCTS.filter(product => product !== "login");
3380
3570
  function isId(value) {
3381
3571
  return value !== "" && Array.from(value).every(character => character >= "0" && character <= "9");
3382
3572
  }
@@ -3385,26 +3575,47 @@ function isPcrl(value) {
3385
3575
  const segments = value.split("/");
3386
3576
  return segments.length === 4 && PRODUCTS.some(product => product === segments[0]) && isId(segments[3]);
3387
3577
  }
3578
+ function isApp(value) {
3579
+ return APPS.some(app => app === value);
3580
+ }
3581
+ // Links are built by resolving the path against its app's origin, so a stored
3582
+ // path must not be able to resolve anywhere else (`//evil.com`, `/\evil.com`).
3583
+ function isPath(value) {
3584
+ const origin = "https://app.invalid";
3585
+ try {
3586
+ return typeof value === "string" && value.startsWith("/") && new URL(value, origin).origin === origin;
3587
+ } catch {
3588
+ return false;
3589
+ }
3590
+ }
3388
3591
  function isNonBlankString(value) {
3389
3592
  return typeof value === "string" && value.trim() !== "";
3390
3593
  }
3594
+ function isOptionalString(value) {
3595
+ return value === undefined || typeof value === "string";
3596
+ }
3391
3597
  function isRecord(value) {
3392
3598
  return typeof value === "object" && value !== null && !Array.isArray(value);
3393
3599
  }
3600
+ function isPageMetadata(value) {
3601
+ return value === undefined || isRecord(value) && isOptionalString(value.details) && isOptionalString(value.type);
3602
+ }
3603
+ // The query string is part of the link but not of the page's identity, so
3604
+ // revisiting with other filters moves the existing entry instead of adding one.
3394
3605
  function entryKey(entry) {
3395
3606
  if (entry.type === "resource") {
3396
3607
  return `resource:${entry.pcrl}`;
3397
3608
  } else {
3398
- return `page:${entry.product}:${entry.path}`;
3609
+ return `page:${entry.app}:${entry.path.split("?")[0]}`;
3399
3610
  }
3400
3611
  }
3401
3612
  function isEntry(value) {
3402
- if (!isRecord(value)) {
3613
+ if (!isRecord(value) || value.version !== ENTRY_VERSION || typeof value.visitedAt !== "string") {
3403
3614
  return false;
3404
3615
  } else if (value.type === "resource") {
3405
3616
  return typeof value.pcrl === "string" && isPcrl(value.pcrl);
3406
3617
  } else if (value.type === "page") {
3407
- return isNonBlankString(value.product) && isNonBlankString(value.title) && isNonBlankString(value.path) && typeof value.details === "string";
3618
+ return isApp(value.app) && isPath(value.path) && isNonBlankString(value.title) && isPageMetadata(value.metadata);
3408
3619
  } else {
3409
3620
  return false;
3410
3621
  }
@@ -3413,58 +3624,114 @@ function isEntry(value) {
3413
3624
  function clampText(value) {
3414
3625
  return Array.from(value).slice(0, MAX_TEXT_LENGTH).join("");
3415
3626
  }
3416
- function clampEntry(entry) {
3417
- if (entry.type === "resource") {
3418
- return entry;
3627
+ function clampMetadata({
3628
+ details,
3629
+ type
3630
+ }) {
3631
+ return {
3632
+ ...(details && {
3633
+ details: clampText(details)
3634
+ }),
3635
+ ...(type && {
3636
+ type: clampText(type)
3637
+ })
3638
+ };
3639
+ }
3640
+ function clampVisit(visit) {
3641
+ if (visit.type === "resource") {
3642
+ return visit;
3419
3643
  } else {
3420
3644
  return {
3421
- ...entry,
3422
- details: clampText(entry.details),
3423
- title: clampText(entry.title)
3645
+ ...visit,
3646
+ ...(visit.metadata && {
3647
+ metadata: clampMetadata(visit.metadata)
3648
+ }),
3649
+ title: clampText(visit.title.trim())
3424
3650
  };
3425
3651
  }
3426
3652
  }
3653
+ function toEntry(visit, visitedAt) {
3654
+ return {
3655
+ ...clampVisit(visit),
3656
+ version: ENTRY_VERSION,
3657
+ visitedAt: visitedAt.toISOString()
3658
+ };
3659
+ }
3427
3660
  // Recents are disposable: anything unexpected reads as an empty list (or, for
3428
3661
  // a single bad entry, a list without it) rather than throwing.
3429
- function parseRecentlyVisited(value, personId) {
3662
+ function parseRecentlyVisited(value) {
3430
3663
  let parsed = null;
3431
3664
  try {
3432
3665
  parsed = JSON.parse(value !== null && value !== void 0 ? value : "null");
3433
3666
  } catch {
3434
3667
  parsed = null;
3435
3668
  }
3436
- if (isRecord(parsed) && parsed.v === FORMAT_VERSION && parsed.person === personId && Array.isArray(parsed.entries)) {
3437
- return parsed.entries.filter(isEntry);
3669
+ if (Array.isArray(parsed)) {
3670
+ return parsed.filter(isEntry);
3438
3671
  } else {
3439
3672
  return [];
3440
3673
  }
3441
3674
  }
3442
- function addVisit(entries, visit) {
3443
- const key = entryKey(visit);
3444
- return [clampEntry(visit), ...entries.filter(entry => entryKey(entry) !== key)].slice(0, MAX_ENTRIES$1);
3445
- }
3446
- function serializeRecentlyVisited(entries, personId) {
3447
- return JSON.stringify({
3448
- entries,
3449
- person: personId,
3450
- v: FORMAT_VERSION
3451
- });
3675
+ function addVisit(entries, entry) {
3676
+ const key = entryKey(entry);
3677
+ return [entry, ...entries.filter(existing => entryKey(existing) !== key)].slice(0, MAX_ENTRIES);
3452
3678
  }
3453
3679
 
3454
- // The product whose URL, in this environment, has the current page's host.
3680
+ // The app whose URL, in this environment, has the current page's host.
3455
3681
  // Reversing pcoUrl handles every environment @planningcenter/url knows about,
3456
- // and guarantees the stored product rebuilds a working link.
3457
- function currentProduct() {
3682
+ // and guarantees the stored app rebuilds a working link.
3683
+ function currentApp() {
3458
3684
  var _a;
3459
- return (_a = PRODUCTS.find(product => new URL(pcoUrl$2(product)).hostname === window.location.hostname)) !== null && _a !== void 0 ? _a : null;
3685
+ return (_a = APPS.find(app => new URL(pcoUrl$2(app)).hostname === window.location.hostname)) !== null && _a !== void 0 ? _a : null;
3460
3686
  }
3461
- function detailsFromMetadata(metadata) {
3687
+ function isFilled(value) {
3688
+ return typeof value === "string" && value !== "";
3689
+ }
3690
+ // The tag's metadata is product-supplied JSON; only its string `type` and
3691
+ // `details` carry over, and an entry without either has no metadata at all.
3692
+ function metadataFromPage(metadata) {
3462
3693
  var _a;
3463
3694
  try {
3464
- const details = (_a = JSON.parse(metadata !== null && metadata !== void 0 ? metadata : "{}")) === null || _a === void 0 ? void 0 : _a.details;
3465
- return typeof details === "string" ? details : "";
3695
+ const {
3696
+ details,
3697
+ type
3698
+ } = (_a = JSON.parse(metadata !== null && metadata !== void 0 ? metadata : "{}")) !== null && _a !== void 0 ? _a : {};
3699
+ if (isFilled(details) || isFilled(type)) {
3700
+ return {
3701
+ ...(isFilled(details) && {
3702
+ details
3703
+ }),
3704
+ ...(isFilled(type) && {
3705
+ type
3706
+ })
3707
+ };
3708
+ } else {
3709
+ return undefined;
3710
+ }
3466
3711
  } catch {
3467
- return "";
3712
+ return undefined;
3713
+ }
3714
+ }
3715
+ // Staff impersonating someone browse under `/O<org id>/P<person id>/…`.
3716
+ function withoutImpersonation(segments) {
3717
+ const [org = "", person = ""] = segments;
3718
+ if (org.startsWith("O") && isId(org.slice(1)) && person.startsWith("P") && isId(person.slice(1))) {
3719
+ return segments.slice(2);
3720
+ } else {
3721
+ return segments;
3722
+ }
3723
+ }
3724
+ // Normalized the way home-mobile does it, so a page visited on web and on
3725
+ // mobile is one entry once the list syncs.
3726
+ function pathFromLocation({
3727
+ pathname,
3728
+ search
3729
+ }) {
3730
+ const segments = withoutImpersonation(pathname.split("/").filter(Boolean));
3731
+ if (segments.length === 0) {
3732
+ return "/";
3733
+ } else {
3734
+ return `/${segments.join("/")}${search.length > 1 ? search : ""}`;
3468
3735
  }
3469
3736
  }
3470
3737
  // pco:resource is the more specific signal, so it wins when both tags are
@@ -3474,17 +3741,20 @@ function visitFromPage({
3474
3741
  resource,
3475
3742
  title
3476
3743
  }) {
3477
- const product = currentProduct();
3744
+ const app = currentApp();
3478
3745
  if (resource && isPcrl(resource)) {
3479
3746
  return {
3480
3747
  pcrl: resource,
3481
3748
  type: "resource"
3482
3749
  };
3483
- } else if (title && title.trim() !== "" && product) {
3750
+ } else if (title && title.trim() !== "" && app) {
3751
+ const pageMetadata = metadataFromPage(metadata);
3484
3752
  return {
3485
- details: detailsFromMetadata(metadata),
3486
- path: window.location.pathname,
3487
- product,
3753
+ app,
3754
+ ...(pageMetadata && {
3755
+ metadata: pageMetadata
3756
+ }),
3757
+ path: pathFromLocation(window.location),
3488
3758
  title,
3489
3759
  type: "page"
3490
3760
  };
@@ -3532,15 +3802,16 @@ const changes = createStore({
3532
3802
  });
3533
3803
  function readRecentlyVisited(scope) {
3534
3804
  try {
3535
- return parseRecentlyVisited(localStorage.getItem(storageKey(scope)), scope.personId);
3805
+ return parseRecentlyVisited(localStorage.getItem(storageKey(scope)));
3536
3806
  } catch {
3537
3807
  return [];
3538
3808
  }
3539
3809
  }
3540
3810
  function recordVisit(visit, scope) {
3541
- if (isEntry(visit)) {
3811
+ const entry = toEntry(visit, new Date());
3812
+ if (isEntry(entry)) {
3542
3813
  try {
3543
- localStorage.setItem(storageKey(scope), serializeRecentlyVisited(addVisit(readRecentlyVisited(scope), visit), scope.personId));
3814
+ localStorage.setItem(storageKey(scope), JSON.stringify(addVisit(readRecentlyVisited(scope), entry)));
3544
3815
  changes.setState({
3545
3816
  version: changes.getState().version + 1
3546
3817
  });
@@ -3561,153 +3832,6 @@ function useRecentlyVisited({
3561
3832
  }) : [], [env, personId, version]);
3562
3833
  }
3563
3834
 
3564
- // The versions home-mobile pins, so web and mobile read the same response
3565
- // shapes. Some endpoints only exist from a version on: Registrations' me,
3566
- // product_search_results, and pcrl_lookups need 2025-05-01.
3567
- const API_VERSIONS = {
3568
- calendar: "2022-07-07",
3569
- "check-ins": "2025-05-28",
3570
- giving: "2019-10-18",
3571
- groups: "2023-07-10",
3572
- home: "2018-08-01",
3573
- people: "2023-03-21",
3574
- publishing: "2024-03-25",
3575
- registrations: "2025-05-01",
3576
- services: "2018-11-01"
3577
- };
3578
- // For a `/~api/{product}/v2/...` URL, the header that pins its product's
3579
- // version.
3580
- function apiVersionHeaders(url) {
3581
- const [,, product] = new URL(url).pathname.split("/");
3582
- const version = API_VERSIONS[product];
3583
- if (version) {
3584
- return {
3585
- "X-PCO-API-Version": version
3586
- };
3587
- } else {
3588
- return {};
3589
- }
3590
- }
3591
-
3592
- const CACHE_TTL_MS = 60000;
3593
- const MAX_ENTRIES = 50;
3594
- const cache = new Map();
3595
- function cachedRequest(url) {
3596
- const cached = cache.get(url);
3597
- if (cached && cached.expiresAt > Date.now()) {
3598
- // Re-inserting keeps the Map in recently-used order for eviction.
3599
- cache.delete(url);
3600
- cache.set(url, cached);
3601
- return cached.promise;
3602
- } else {
3603
- return fetchIntoCache(url);
3604
- }
3605
- }
3606
- function fetchIntoCache(url) {
3607
- const promise = apiRequest(defaultFetch, url, {
3608
- headers: apiVersionHeaders(url)
3609
- }).then(({
3610
- json,
3611
- ok
3612
- }) => {
3613
- if (!ok || json === null) throw new Error(`Request failed: ${url}`);
3614
- return json;
3615
- });
3616
- // In-flight entries never expire, so every caller shares the one fetch.
3617
- const entry = {
3618
- expiresAt: Infinity,
3619
- promise
3620
- };
3621
- cache.set(url, entry);
3622
- promise.then(() => entry.expiresAt = Date.now() + CACHE_TTL_MS, () => cache.get(url) === entry && cache.delete(url));
3623
- if (cache.size > MAX_ENTRIES) cache.delete(cache.keys().next().value);
3624
- return promise;
3625
- }
3626
- function useCachedRequest(url) {
3627
- const [settled, setSettled] = React__default.useState();
3628
- React__default.useEffect(() => {
3629
- if (url) {
3630
- let current = true;
3631
- cachedRequest(url).then(data => current && setSettled({
3632
- data,
3633
- url
3634
- }), () => current && setSettled({
3635
- failed: true,
3636
- url
3637
- }));
3638
- return () => {
3639
- current = false;
3640
- };
3641
- } else {
3642
- return undefined;
3643
- }
3644
- }, [url]);
3645
- if (!url) {
3646
- return {
3647
- status: "idle"
3648
- };
3649
- } else if ((settled === null || settled === void 0 ? void 0 : settled.url) !== url) {
3650
- return {
3651
- data: settled && "data" in settled ? settled.data : undefined,
3652
- status: "loading"
3653
- };
3654
- } else if ("data" in settled) {
3655
- return {
3656
- data: settled.data,
3657
- status: "done"
3658
- };
3659
- } else {
3660
- return {
3661
- status: "error"
3662
- };
3663
- }
3664
- }
3665
- // One request per URL, for callers that fan out to a varying number of
3666
- // endpoints. Unlike `useCachedRequest`, a URL that hasn't settled yet has no
3667
- // earlier data to fall back on.
3668
- function useCachedRequests(urls) {
3669
- const [settled, setSettled] = React__default.useState({});
3670
- // Keyed by value because each settle re-renders the caller, and a caller
3671
- // that builds a fresh `urls` array on render would otherwise loop forever.
3672
- const key = urls.join("\n");
3673
- React__default.useEffect(() => {
3674
- let current = true;
3675
- const settle = result => current && setSettled(prev => ({
3676
- ...prev,
3677
- [result.url]: result
3678
- }));
3679
- for (const url of key.split("\n").filter(Boolean)) {
3680
- cachedRequest(url).then(data => settle({
3681
- data,
3682
- url
3683
- }), () => settle({
3684
- failed: true,
3685
- url
3686
- }));
3687
- }
3688
- return () => {
3689
- current = false;
3690
- };
3691
- }, [key]);
3692
- return urls.map(url => {
3693
- const result = settled[url];
3694
- if (!result) {
3695
- return {
3696
- status: "loading"
3697
- };
3698
- } else if ("data" in result) {
3699
- return {
3700
- data: result.data,
3701
- status: "done"
3702
- };
3703
- } else {
3704
- return {
3705
- status: "error"
3706
- };
3707
- }
3708
- });
3709
- }
3710
-
3711
3835
  const pcoUrl = pcoUrl$1;
3712
3836
 
3713
3837
  // Resolves resource entries with one pcrl_lookups request per product.
@@ -3798,13 +3922,290 @@ function pcrlLookupsUrl(product, pcrls) {
3798
3922
  });
3799
3923
  return `${window.location.origin}/~api/${product}/v2/pcrl_lookups?${params}`;
3800
3924
  }
3801
- // Built from the product rather than the current origin, so it stays right
3802
- // once PCA-409 shares one list across product subdomains.
3925
+ // Built from the app rather than the current origin, so it stays right once
3926
+ // PCA-409 shares one list across product subdomains.
3803
3927
  function pageUrl(entry, env) {
3804
- return `${pcoUrl(env)(entry.product)}${entry.path}`;
3928
+ return new URL(entry.path, pcoUrl(env)(entry.app)).href;
3929
+ }
3930
+
3931
+ function useRecentVisits(env) {
3932
+ const personId = useCurrentPersonId();
3933
+ const entries = useRecentlyVisited({
3934
+ env,
3935
+ personId
3936
+ });
3937
+ const [count, setCount] = React__default.useState(RECENTLY_VISITED_PAGE_SIZE);
3938
+ const shown = React__default.useMemo(() => entries.slice(0, count), [entries, count]);
3939
+ const hydrated = useHydratedRecentlyVisited(shown, env);
3940
+ // Resources show up once they're found. Ones that can't be found anymore,
3941
+ // or that the person can no longer see, are left out.
3942
+ const visits = hydrated.filter(isShown);
3943
+ const loading = hydrated.some(({
3944
+ status
3945
+ }) => status === "loading");
3946
+ const showMore = React__default.useCallback(() => setCount(current => current + RECENTLY_VISITED_PAGE_SIZE), []);
3947
+ return {
3948
+ count,
3949
+ hasMore: entries.length > count,
3950
+ loading,
3951
+ showMore,
3952
+ visits
3953
+ };
3954
+ }
3955
+ function isShown(visit) {
3956
+ return visit.status === "page" || visit.status === "resolved";
3957
+ }
3958
+
3959
+ function open(url, {
3960
+ newTab
3961
+ }) {
3962
+ if (newTab) {
3963
+ window.open(url, "_blank", "noopener");
3964
+ } else {
3965
+ window.location.assign(url);
3966
+ }
3967
+ }
3968
+ // With text selected, ⌘C should copy that text as usual, not the link.
3969
+ function hasSelectedText(element) {
3970
+ var _a, _b;
3971
+ const selectedInField = element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
3972
+ return selectedInField || ((_b = (_a = window.getSelection()) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : "") !== "";
3973
+ }
3974
+
3975
+ const COPIED_FEEDBACK_MS = 1500;
3976
+ // `copiedUrl` stays set briefly after a copy so the UI can confirm it.
3977
+ function useCopyLink() {
3978
+ const [copiedUrl, setCopiedUrl] = React__default.useState(null);
3979
+ React__default.useEffect(() => {
3980
+ if (copiedUrl) {
3981
+ const timeout = setTimeout(() => setCopiedUrl(null), COPIED_FEEDBACK_MS);
3982
+ return () => clearTimeout(timeout);
3983
+ } else {
3984
+ return undefined;
3985
+ }
3986
+ }, [copiedUrl]);
3987
+ const copy = React__default.useCallback(url => {
3988
+ navigator.clipboard.writeText(url).then(() => setCopiedUrl(url));
3989
+ }, []);
3990
+ return {
3991
+ copiedUrl,
3992
+ copy
3993
+ };
3994
+ }
3995
+
3996
+ // Remembering which list the index belongs to puts the highlight back on the
3997
+ // first item whenever a new list replaces it, without an effect to reset it.
3998
+ // A list that grows or fills in under the same `list` keeps its highlight.
3999
+ function useResultNavigation(items, list) {
4000
+ const [highlight, setHighlight] = React__default.useState({
4001
+ index: 0,
4002
+ list
4003
+ });
4004
+ const highlightedIndex = highlight.list === list ? highlight.index : 0;
4005
+ const highlighted = items[highlightedIndex];
4006
+ const {
4007
+ copiedUrl,
4008
+ copy
4009
+ } = useCopyLink();
4010
+ const highlightIndex = React__default.useCallback(index => setHighlight(current => current.list === list && current.index === index ? current : {
4011
+ index,
4012
+ list
4013
+ }), [list]);
4014
+ const handleKeyDown = event => {
4015
+ const step = verticalStep(event.key);
4016
+ if (step && items.length > 0) {
4017
+ event.preventDefault();
4018
+ highlightIndex((highlightedIndex + step + items.length) % items.length);
4019
+ } else if (event.key === "Enter" && highlighted && !event.nativeEvent.isComposing) {
4020
+ event.preventDefault();
4021
+ open(highlighted.url, {
4022
+ newTab: hasCommandKey(event)
4023
+ });
4024
+ } else if (isCopyShortcut(event) && highlighted && !hasSelectedText(event.currentTarget)) {
4025
+ event.preventDefault();
4026
+ copy(highlighted.url);
4027
+ }
4028
+ };
4029
+ return {
4030
+ copied: copiedUrl !== null && copiedUrl === (highlighted === null || highlighted === void 0 ? void 0 : highlighted.url),
4031
+ handleKeyDown,
4032
+ highlightIndex,
4033
+ highlighted,
4034
+ highlightedIndex: highlighted ? highlightedIndex : null
4035
+ };
4036
+ }
4037
+
4038
+ function groupSections(groups = []) {
4039
+ return groups.map(({
4040
+ results,
4041
+ type
4042
+ }) => ({
4043
+ heading: type,
4044
+ items: results.map(resultItem),
4045
+ key: type
4046
+ }));
4047
+ }
4048
+ function resultItem({
4049
+ attributes,
4050
+ id
4051
+ }) {
4052
+ return {
4053
+ key: id,
4054
+ subtitle: summarize(attributes.metadata),
4055
+ title: attributes.title,
4056
+ url: attributes.url
4057
+ };
4058
+ }
4059
+ function visitItem(visit) {
4060
+ return {
4061
+ key: entryKey(visit.entry),
4062
+ ...describeVisit(visit)
4063
+ };
4064
+ }
4065
+ function summarize(metadata) {
4066
+ return Object.values(metadata !== null && metadata !== void 0 ? metadata : {}).filter(value => typeof value === "string" && value !== "").join(" · ");
4067
+ }
4068
+ // Unlike search results, recents come from every product, so each one names
4069
+ // its product and type.
4070
+ function describeVisit(visit) {
4071
+ var _a, _b;
4072
+ const subtitle = (...parts) => parts.filter(Boolean).join(" · ");
4073
+ if (visit.status === "page") {
4074
+ const {
4075
+ app,
4076
+ metadata,
4077
+ title
4078
+ } = visit.entry;
4079
+ return {
4080
+ 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 : ""),
4081
+ title,
4082
+ url: visit.url
4083
+ };
4084
+ } else {
4085
+ const {
4086
+ metadata,
4087
+ product,
4088
+ title,
4089
+ type
4090
+ } = visit.result.attributes;
4091
+ return {
4092
+ subtitle: subtitle(productName(product), type, summarize(metadata)),
4093
+ title,
4094
+ url: visit.url
4095
+ };
4096
+ }
4097
+ }
4098
+ // check-ins → Check-Ins
4099
+ function productName(slug) {
4100
+ return slug.split("-").map(capitalize).join("-");
4101
+ }
4102
+ function capitalize(word) {
4103
+ return word.charAt(0).toUpperCase() + word.slice(1);
4104
+ }
4105
+
4106
+ const STORAGE_KEY = "topbar.product_search.last_product";
4107
+ function readLastProduct() {
4108
+ try {
4109
+ return localStorage.getItem(STORAGE_KEY);
4110
+ } catch {
4111
+ return null;
4112
+ }
4113
+ }
4114
+ function saveLastProduct(slug) {
4115
+ try {
4116
+ localStorage.setItem(STORAGE_KEY, slug);
4117
+ } catch {
4118
+ // Storage can be full or blocked; search still works without it.
4119
+ }
4120
+ }
4121
+ function initialProduct(products) {
4122
+ var _a, _b, _c;
4123
+ const lastProduct = readLastProduct();
4124
+ const bySlug = slug => products.find(product => product.slug === slug);
4125
+ 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];
3805
4126
  }
3806
4127
 
3807
- var styles$a = {"result":"result-WCnxr"};
4128
+ function KeyboardHints({
4129
+ copied
4130
+ }) {
4131
+ const command = commandKeyLabel();
4132
+ return /*#__PURE__*/React__default.createElement("p", null, "\u2191\u2193 Select \u00B7 \u21B5 Open \u00B7 ", command, "\u21B5 Open in new tab \u00B7 ", command, "C", " ", /*#__PURE__*/React__default.createElement("span", {
4133
+ role: "status"
4134
+ }, copied ? "Copied!" : "Copy link"), " \u00B7 ", command, "K Open search");
4135
+ }
4136
+
4137
+ var styles$d = {"tab":"tab-dz8ki"};
4138
+
4139
+ function ProductTabs({
4140
+ children,
4141
+ onKeyDown,
4142
+ onSelect,
4143
+ products,
4144
+ selected
4145
+ }) {
4146
+ const id = useInstanceId("product-tabs");
4147
+ const tabId = slug => `${id}-tab-${slug}`;
4148
+ const tabRefs = React__default.useRef(new Map());
4149
+ const handleKeyDown = event => {
4150
+ var _a;
4151
+ const offset = horizontalStep(event.key);
4152
+ if (offset) {
4153
+ event.preventDefault();
4154
+ const index = products.indexOf(selected);
4155
+ const next = products[(index + offset + products.length) % products.length];
4156
+ onSelect(next.slug);
4157
+ (_a = tabRefs.current.get(next.slug)) === null || _a === void 0 ? void 0 : _a.focus();
4158
+ } else {
4159
+ onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
4160
+ }
4161
+ };
4162
+ return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("div", {
4163
+ "aria-label": "Product",
4164
+ onKeyDown: handleKeyDown,
4165
+ role: "tablist"
4166
+ }, products.map(product => (/*#__PURE__*/React__default.createElement("button", {
4167
+ "aria-controls": `${id}-panel`,
4168
+ "aria-selected": product === selected,
4169
+ className: styles$d.tab,
4170
+ id: tabId(product.slug),
4171
+ key: product.slug,
4172
+ onClick: () => onSelect(product.slug),
4173
+ ref: element => {
4174
+ if (element) {
4175
+ tabRefs.current.set(product.slug, element);
4176
+ } else {
4177
+ tabRefs.current.delete(product.slug);
4178
+ }
4179
+ },
4180
+ role: "tab",
4181
+ tabIndex: product === selected ? 0 : -1,
4182
+ type: "button"
4183
+ }, product.appName)))), /*#__PURE__*/React__default.createElement("div", {
4184
+ "aria-labelledby": tabId(selected.slug),
4185
+ id: `${id}-panel`,
4186
+ role: "tabpanel"
4187
+ }, children));
4188
+ }
4189
+
4190
+ function EndOfResultsObserver({
4191
+ onVisible
4192
+ }) {
4193
+ const ref = React__default.useRef(null);
4194
+ React__default.useEffect(() => {
4195
+ const observer = new IntersectionObserver(([entry]) => {
4196
+ if (entry.isIntersecting) onVisible();
4197
+ });
4198
+ if (ref.current) observer.observe(ref.current);
4199
+ return () => observer.disconnect();
4200
+ }, [onVisible]);
4201
+ return /*#__PURE__*/React__default.createElement("div", {
4202
+ ref: ref
4203
+ });
4204
+ }
4205
+
4206
+ const resultId = (listboxId, index) => `${listboxId}-result-${index}`;
4207
+
4208
+ var styles$c = {"result":"result-Thq2C"};
3808
4209
 
3809
4210
  function SearchResult({
3810
4211
  highlighted,
@@ -3827,7 +4228,7 @@ function SearchResult({
3827
4228
  // pointer, and that shouldn't steal the highlight.
3828
4229
  return /*#__PURE__*/React__default.createElement("a", {
3829
4230
  "aria-selected": highlighted,
3830
- className: styles$a.result,
4231
+ className: styles$c.result,
3831
4232
  href: url,
3832
4233
  id: id,
3833
4234
  onMouseMove: onHighlight,
@@ -3837,21 +4238,129 @@ function SearchResult({
3837
4238
  }, /*#__PURE__*/React__default.createElement("div", null, title), /*#__PURE__*/React__default.createElement("div", null, subtitle));
3838
4239
  }
3839
4240
 
3840
- const resultId = (listboxId, index) => `${listboxId}-result-${index}`;
4241
+ // Sections with a heading render as labelled groups; ones without render
4242
+ // their items straight into the listbox. Indexes run across every section.
4243
+ function ResultListbox({
4244
+ busy,
4245
+ highlightedIndex,
4246
+ id,
4247
+ label,
4248
+ labelledBy,
4249
+ onHighlight,
4250
+ sections
4251
+ }) {
4252
+ let index = 0;
4253
+ const renderItem = item => {
4254
+ const itemIndex = index++;
4255
+ return /*#__PURE__*/React__default.createElement(SearchResult, {
4256
+ highlighted: itemIndex === highlightedIndex,
4257
+ id: resultId(id, itemIndex),
4258
+ key: item.key,
4259
+ onHighlight: () => onHighlight(itemIndex),
4260
+ subtitle: item.subtitle,
4261
+ title: item.title,
4262
+ url: item.url
4263
+ });
4264
+ };
4265
+ return /*#__PURE__*/React__default.createElement("div", {
4266
+ "aria-busy": busy,
4267
+ "aria-label": label,
4268
+ "aria-labelledby": labelledBy,
4269
+ id: id,
4270
+ role: "listbox"
4271
+ }, sections.map((section, sectionIndex) => {
4272
+ if (section.heading === undefined) {
4273
+ return /*#__PURE__*/React__default.createElement(React__default.Fragment, {
4274
+ key: section.key
4275
+ }, section.items.map(renderItem));
4276
+ }
4277
+ const headingId = `${id}-group-${sectionIndex}`;
4278
+ return /*#__PURE__*/React__default.createElement("div", {
4279
+ "aria-labelledby": headingId,
4280
+ key: section.key,
4281
+ role: "group"
4282
+ }, /*#__PURE__*/React__default.createElement("div", {
4283
+ id: headingId
4284
+ }, section.heading), section.items.map(renderItem));
4285
+ }));
4286
+ }
4287
+
4288
+ function RecentlyVisited({
4289
+ count,
4290
+ hasMore,
4291
+ items,
4292
+ loading,
4293
+ showMore,
4294
+ ...navigation
4295
+ }) {
4296
+ const headingId = `${navigation.id}-heading`;
4297
+ if (items.length === 0 && !hasMore && !loading) {
4298
+ return /*#__PURE__*/React__default.createElement("p", null, "Pages you visit will show up here.");
4299
+ } else {
4300
+ return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("div", {
4301
+ id: headingId
4302
+ }, "Recently visited"), /*#__PURE__*/React__default.createElement(ResultListbox, {
4303
+ labelledBy: headingId,
4304
+ sections: [{
4305
+ items,
4306
+ key: "recents"
4307
+ }],
4308
+ ...navigation
4309
+ }), hasMore && /*#__PURE__*/React__default.createElement(EndOfResultsObserver, {
4310
+ key: count,
4311
+ onVisible: showMore
4312
+ }));
4313
+ }
4314
+ }
4315
+
4316
+ // A combobox: focus stays here while aria-activedescendant points screen
4317
+ // readers at the highlighted result in the listbox it controls.
4318
+ function SearchInput({
4319
+ expanded,
4320
+ highlightedIndex,
4321
+ inputRef,
4322
+ listboxId,
4323
+ onChange,
4324
+ onFocus,
4325
+ onKeyDown,
4326
+ value
4327
+ }) {
4328
+ return /*#__PURE__*/React__default.createElement("input", {
4329
+ "aria-activedescendant": highlightedIndex === null ? undefined : resultId(listboxId, highlightedIndex),
4330
+ "aria-autocomplete": "list",
4331
+ "aria-controls": expanded ? listboxId : undefined,
4332
+ "aria-expanded": expanded,
4333
+ "aria-label": "Search",
4334
+ autoFocus: true,
4335
+ onChange: event => onChange(event.target.value),
4336
+ onFocus: onFocus,
4337
+ onKeyDown: onKeyDown,
4338
+ placeholder: "Search",
4339
+ ref: inputRef,
4340
+ role: "combobox",
4341
+ type: "search",
4342
+ value: value
4343
+ });
4344
+ }
4345
+
4346
+ var styles$b = {"visuallyHidden":"visuallyHidden-S9VG5"};
4347
+
3841
4348
  function SearchResults({
3842
- search,
4349
+ label,
4350
+ sections,
4351
+ status,
3843
4352
  term,
3844
4353
  ...navigation
3845
4354
  }) {
3846
- var _a;
3847
- switch (search.status) {
4355
+ switch (status) {
3848
4356
  case "idle":
3849
4357
  return null;
3850
4358
  case "loading":
3851
- if ((_a = search.groups) === null || _a === void 0 ? void 0 : _a.length) {
3852
- return /*#__PURE__*/React__default.createElement(ResultGroups, {
4359
+ if (sections.length > 0) {
4360
+ return /*#__PURE__*/React__default.createElement(ResultListbox, {
3853
4361
  busy: true,
3854
- groups: search.groups,
4362
+ label: label,
4363
+ sections: sections,
3855
4364
  ...navigation
3856
4365
  });
3857
4366
  }
@@ -3863,324 +4372,167 @@ function SearchResults({
3863
4372
  role: "alert"
3864
4373
  }, "Search isn\u2019t working right now. Try again.");
3865
4374
  case "done":
3866
- if (search.groups.length === 0) {
4375
+ if (sections.length === 0) {
3867
4376
  return /*#__PURE__*/React__default.createElement("p", {
3868
4377
  role: "status"
3869
4378
  }, "No results for \u201C", term.trim(), "\u201D");
3870
4379
  }
3871
- return /*#__PURE__*/React__default.createElement(ResultGroups, {
3872
- groups: search.groups,
4380
+ return /*#__PURE__*/React__default.createElement(ResultListbox, {
4381
+ busy: false,
4382
+ label: label,
4383
+ sections: sections,
3873
4384
  ...navigation
3874
4385
  });
3875
4386
  }
3876
4387
  }
3877
- function ResultGroups({
3878
- busy = false,
3879
- groups,
3880
- highlightedIndex,
3881
- id,
3882
- label,
3883
- onHighlight
3884
- }) {
3885
- let index = 0;
3886
- return /*#__PURE__*/React__default.createElement("div", {
3887
- "aria-busy": busy,
3888
- "aria-label": label,
3889
- id: id,
3890
- role: "listbox"
3891
- }, groups.map(({
3892
- results,
3893
- type
3894
- }, groupIndex) => {
3895
- const headingId = `${id}-group-${groupIndex}`;
3896
- return /*#__PURE__*/React__default.createElement("div", {
3897
- "aria-labelledby": headingId,
3898
- key: type,
3899
- role: "group"
3900
- }, /*#__PURE__*/React__default.createElement("div", {
3901
- id: headingId
3902
- }, type), results.map(result => {
3903
- const resultIndex = index++;
3904
- return /*#__PURE__*/React__default.createElement(SearchResult, {
3905
- highlighted: resultIndex === highlightedIndex,
3906
- id: resultId(id, resultIndex),
3907
- key: result.id,
3908
- onHighlight: () => onHighlight(resultIndex),
3909
- subtitle: summarize(result.attributes.metadata),
3910
- title: result.attributes.title,
3911
- url: result.attributes.url
3912
- });
3913
- }));
3914
- }));
3915
- }
3916
- function summarize(metadata) {
3917
- return Object.values(metadata !== null && metadata !== void 0 ? metadata : {}).filter(value => typeof value === "string" && value !== "").join(" · ");
3918
- }
3919
4388
 
3920
- function useRecentVisits(env) {
3921
- const personId = useCurrentPersonId();
3922
- const entries = useRecentlyVisited({
3923
- env,
3924
- personId
3925
- });
3926
- const [count, setCount] = React__default.useState(RECENTLY_VISITED_PAGE_SIZE);
3927
- const shown = React__default.useMemo(() => entries.slice(0, count), [entries, count]);
3928
- const hydrated = useHydratedRecentlyVisited(shown, env);
3929
- // Resources show up once they're found. Ones that can't be found anymore,
3930
- // or that the person can no longer see, are left out.
3931
- const visits = hydrated.filter(isShown);
3932
- const loading = hydrated.some(({
3933
- status
3934
- }) => status === "loading");
3935
- const showMore = React__default.useCallback(() => setCount(current => current + RECENTLY_VISITED_PAGE_SIZE), []);
3936
- return {
3937
- count,
3938
- hasMore: entries.length > count,
3939
- loading,
3940
- showMore,
3941
- visits
3942
- };
3943
- }
3944
- function isShown(visit) {
3945
- return visit.status === "page" || visit.status === "resolved";
3946
- }
3947
- function RecentlyVisited({
3948
- count,
3949
- hasMore,
3950
- highlightedIndex,
3951
- id,
3952
- loading,
3953
- onHighlight,
3954
- showMore,
3955
- visits
3956
- }) {
3957
- const headingId = `${id}-heading`;
3958
- if (visits.length === 0 && !hasMore && !loading) {
3959
- return /*#__PURE__*/React__default.createElement("p", null, "Pages you visit will show up here.");
3960
- } else {
3961
- return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("div", {
3962
- id: headingId
3963
- }, "Recently visited"), /*#__PURE__*/React__default.createElement("div", {
3964
- "aria-labelledby": headingId,
3965
- id: id,
3966
- role: "listbox"
3967
- }, visits.map((visit, index) => (/*#__PURE__*/React__default.createElement(SearchResult, {
3968
- highlighted: index === highlightedIndex,
3969
- id: resultId(id, index),
3970
- key: entryKey(visit.entry),
3971
- onHighlight: () => onHighlight(index),
3972
- ...describeVisit(visit)
3973
- })))), hasMore && /*#__PURE__*/React__default.createElement(ShowMoreWhenVisible, {
3974
- key: count,
3975
- onVisible: showMore
3976
- }));
3977
- }
3978
- }
3979
- function ShowMoreWhenVisible({
3980
- onVisible
4389
+ // Keys the recents list for navigation, so the highlight
4390
+ // stays put as more recents load and as their lookups answer.
4391
+ const RECENTS = "recents";
4392
+ function SearchPanel({
4393
+ env,
4394
+ products
3981
4395
  }) {
3982
- const ref = React__default.useRef(null);
4396
+ var _a, _b;
4397
+ const listboxId = useInstanceId("product-search-results");
4398
+ const [term, setTerm] = React__default.useState("");
4399
+ const [selectedSlug, setSelectedSlug] = React__default.useState(() => initialProduct(products).slug);
4400
+ const product = (_a = products.find(({
4401
+ slug
4402
+ }) => slug === selectedSlug)) !== null && _a !== void 0 ? _a : products[0];
4403
+ const hasTerm = term.trim() !== "";
4404
+ const search = useProductSearch(product.slug, term);
4405
+ const groups = "groups" in search ? search.groups : undefined;
4406
+ const sections = React__default.useMemo(() => groupSections(groups), [groups]);
4407
+ const {
4408
+ visits,
4409
+ ...recents
4410
+ } = useRecentVisits(env);
4411
+ const recentItems = visits.map(visitItem);
4412
+ const items = hasTerm ? sections.flatMap(section => section.items) : recentItems;
4413
+ const navigation = useResultNavigation(items, hasTerm ? sections : RECENTS);
4414
+ const {
4415
+ highlightedIndex
4416
+ } = navigation;
4417
+ const inputRef = React__default.useRef(null);
4418
+ const [announceHighlight, setAnnounceHighlight] = React__default.useState(false);
4419
+ // The tabs keep focus while arrowing through results, but only the focused
4420
+ // input can point screen readers at the highlight, so from the tabs it's
4421
+ // announced instead. Typing goes back to the input, keystroke and all.
4422
+ const handleTabsKeyDown = event => {
4423
+ var _a;
4424
+ if (isTypingKey(event)) {
4425
+ (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
4426
+ } else if (verticalStep(event.key)) {
4427
+ setAnnounceHighlight(true);
4428
+ navigation.handleKeyDown(event);
4429
+ } else {
4430
+ navigation.handleKeyDown(event);
4431
+ }
4432
+ };
3983
4433
  React__default.useEffect(() => {
3984
- const observer = new IntersectionObserver(([entry]) => {
3985
- if (entry.isIntersecting) onVisible();
3986
- });
3987
- if (ref.current) observer.observe(ref.current);
3988
- return () => observer.disconnect();
3989
- }, [onVisible]);
3990
- return /*#__PURE__*/React__default.createElement("div", {
3991
- ref: ref
3992
- });
3993
- }
3994
- // Unlike search results, recents come from every product, so each one names
3995
- // its product and type.
3996
- function describeVisit(visit) {
3997
- const subtitle = (...parts) => parts.filter(Boolean).join(" · ");
3998
- if (visit.status === "page") {
3999
- const {
4000
- details,
4001
- product,
4002
- title
4003
- } = visit.entry;
4004
- return {
4005
- subtitle: subtitle(productName(product), details),
4006
- title,
4007
- url: visit.url
4008
- };
4009
- } else {
4010
- const {
4011
- metadata,
4012
- product,
4013
- title,
4014
- type
4015
- } = visit.result.attributes;
4016
- return {
4017
- subtitle: subtitle(productName(product), type, summarize(metadata)),
4018
- title,
4019
- url: visit.url
4020
- };
4021
- }
4022
- }
4023
- // check-ins → Check-Ins
4024
- function productName(slug) {
4025
- return slug.split("-").map(capitalize).join("-");
4026
- }
4027
- function capitalize(word) {
4028
- return word.charAt(0).toUpperCase() + word.slice(1);
4434
+ if (hasTerm) saveLastProduct(product.slug);
4435
+ }, [hasTerm, product.slug]);
4436
+ return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(SearchInput, {
4437
+ expanded: items.length > 0,
4438
+ highlightedIndex: highlightedIndex,
4439
+ inputRef: inputRef,
4440
+ listboxId: listboxId,
4441
+ onChange: setTerm,
4442
+ onFocus: () => setAnnounceHighlight(false),
4443
+ onKeyDown: navigation.handleKeyDown,
4444
+ value: term
4445
+ }), /*#__PURE__*/React__default.createElement(ProductTabs, {
4446
+ onKeyDown: handleTabsKeyDown,
4447
+ onSelect: setSelectedSlug,
4448
+ products: products,
4449
+ selected: product
4450
+ }, hasTerm ? (/*#__PURE__*/React__default.createElement(SearchResults, {
4451
+ highlightedIndex: highlightedIndex,
4452
+ id: listboxId,
4453
+ label: `${product.appName} results`,
4454
+ onHighlight: navigation.highlightIndex,
4455
+ sections: sections,
4456
+ status: search.status,
4457
+ term: term
4458
+ })) : (/*#__PURE__*/React__default.createElement(RecentlyVisited, {
4459
+ highlightedIndex: highlightedIndex,
4460
+ id: listboxId,
4461
+ items: recentItems,
4462
+ onHighlight: navigation.highlightIndex,
4463
+ ...recents
4464
+ }))), /*#__PURE__*/React__default.createElement("div", {
4465
+ "aria-live": "polite",
4466
+ className: styles$b.visuallyHidden
4467
+ }, announceHighlight ? (_b = navigation.highlighted) === null || _b === void 0 ? void 0 : _b.title : null), /*#__PURE__*/React__default.createElement(KeyboardHints, {
4468
+ copied: navigation.copied
4469
+ }));
4029
4470
  }
4030
4471
 
4031
- const SEARCH_DEBOUNCE_MS = 300;
4032
- function useProductSearch(product, term) {
4033
- const query = term.trim();
4034
- const debouncedQuery = useDebouncedValue(query, SEARCH_DEBOUNCE_MS);
4035
- const request = useCachedRequest(debouncedQuery ? productSearchUrl(product, debouncedQuery) : null);
4036
- const response = "data" in request ? request.data : undefined;
4037
- const settledProduct = useSettledProduct(product, request.status);
4038
- // While a request loads, the previous response stays on screen, but only
4039
- // if it was for this product: People results must never show under the
4040
- // Services tab.
4041
- const visibleResponse = request.status === "done" || settledProduct === product ? response : undefined;
4042
- const groups = React__default.useMemo(() => visibleResponse && groupByType(visibleResponse), [visibleResponse]);
4043
- if (!query) {
4044
- return {
4045
- status: "idle"
4046
- };
4047
- } else if (query !== debouncedQuery) {
4048
- return {
4049
- groups,
4050
- status: "loading"
4051
- };
4052
- } else if (request.status === "done") {
4053
- return {
4054
- groups: groups !== null && groups !== void 0 ? groups : [],
4055
- status: "done"
4056
- };
4057
- } else if (request.status === "error") {
4058
- return {
4059
- status: "error"
4060
- };
4061
- } else {
4062
- return {
4063
- groups,
4064
- status: "loading"
4065
- };
4066
- }
4472
+ // Products open modals from many libraries, not all of which mark themselves
4473
+ // with roles, so a modal is recognized by what it does: cover the page.
4474
+ function isModalOpen() {
4475
+ const {
4476
+ clientHeight,
4477
+ clientWidth
4478
+ } = document.documentElement;
4479
+ const element = document.elementFromPoint(clientWidth / 2, clientHeight / 2);
4480
+ return element !== null && (isInNativeDialog(element) || hasBackdrop(element, clientWidth, clientHeight));
4067
4481
  }
4068
- function productSearchUrl(product, query) {
4069
- const params = new URLSearchParams({
4070
- per_page: "100",
4071
- "where[term]": query
4072
- });
4073
- return `${window.location.origin}/~api/${product}/v2/product_search_results?${params}`;
4482
+ // A native modal's backdrop is drawn by the browser rather than being an
4483
+ // element; hit-testing it lands on the dialog itself.
4484
+ function isInNativeDialog(element) {
4485
+ return element.closest("dialog[open]") !== null;
4074
4486
  }
4075
- function groupByType({
4076
- data,
4077
- meta
4078
- }) {
4079
- var _a, _b;
4080
- const typeOrder = (_a = meta === null || meta === void 0 ? void 0 : meta.type_order) !== null && _a !== void 0 ? _a : [];
4081
- const groups = new Map();
4082
- for (const result of data) {
4083
- const {
4084
- type
4085
- } = result.attributes;
4086
- if (!groups.has(type)) groups.set(type, {
4087
- results: [],
4088
- type
4089
- });
4090
- (_b = groups.get(type)) === null || _b === void 0 ? void 0 : _b.results.push(result);
4487
+ function hasBackdrop(element, width, height) {
4488
+ let current = element;
4489
+ while (current && !isBackdrop(current, width, height)) {
4490
+ current = current.parentElement;
4091
4491
  }
4092
- const rank = type => {
4093
- const index = typeOrder.indexOf(type);
4094
- return index === -1 ? typeOrder.length : index;
4095
- };
4096
- return [...groups.values()].sort((a, b) => rank(a.type) - rank(b.type));
4492
+ return current !== null;
4097
4493
  }
4098
- // The product the most recent finished request was for.
4099
- function useSettledProduct(product, status) {
4100
- const [settledProduct, setSettledProduct] = React__default.useState();
4101
- React__default.useEffect(() => {
4102
- if (status === "done") setSettledProduct(product);
4103
- }, [product, status]);
4104
- return settledProduct;
4105
- }
4106
- function useDebouncedValue(value, delay) {
4107
- const [debounced, setDebounced] = React__default.useState(value);
4108
- React__default.useEffect(() => {
4109
- const timeout = setTimeout(() => setDebounced(value), delay);
4110
- return () => clearTimeout(timeout);
4111
- }, [value, delay]);
4112
- return debounced;
4494
+ // Some backdrops start below the topbar rather than at the top of the screen,
4495
+ // so the layer only has to span the width and reach from the center to the
4496
+ // bottom. Its own box has to reach the center: a bar along the bottom can
4497
+ // hold a panel that pops up over the center, like Services' media player.
4498
+ // react-aria leaves a closed modal's backdrop up while it fades out, search's
4499
+ // own included, so one that's on its way out doesn't count.
4500
+ function isBackdrop(element, width, height) {
4501
+ const rect = element.getBoundingClientRect();
4502
+ return !element.matches("[data-exiting]") && getComputedStyle(element).position === "fixed" && rect.width >= width - SCROLLBAR_SLACK && rect.top <= height / 2 && rect.bottom >= height - ROUNDING_SLACK;
4113
4503
  }
4114
-
4115
- const COPIED_FEEDBACK_MS = 1500;
4116
- const verticalStep = key => ({
4117
- ArrowDown: 1,
4118
- ArrowUp: -1
4119
- })[key];
4120
- // Remembering which list the index belongs to puts the highlight back on the
4121
- // first option whenever a new list replaces it, without an effect to reset it.
4122
- // A list that grows or fills in under the same `list` keeps its highlight.
4123
- function useResultNavigation(options, list) {
4124
- const [highlight, setHighlight] = React__default.useState({
4125
- index: 0,
4126
- list
4127
- });
4128
- const highlightedIndex = highlight.list === list ? highlight.index : 0;
4129
- const highlighted = options[highlightedIndex];
4130
- const [copiedUrl, setCopiedUrl] = React__default.useState(null);
4504
+ // react-aria keeps a classic scrollbar's gutter reserved while its modals lock
4505
+ // scrolling, so their backdrops stop short of the right edge by its width.
4506
+ const SCROLLBAR_SLACK = 24;
4507
+ // Rects are fractional at non-100% zoom, but clientWidth and clientHeight are
4508
+ // rounded to whole pixels.
4509
+ const ROUNDING_SLACK = 1;
4510
+
4511
+ function useSearchShortcut(enabled, open, setOpen) {
4131
4512
  React__default.useEffect(() => {
4132
- if (copiedUrl) {
4133
- const timeout = setTimeout(() => setCopiedUrl(null), COPIED_FEEDBACK_MS);
4134
- return () => clearTimeout(timeout);
4135
- } else {
4136
- return undefined;
4137
- }
4138
- }, [copiedUrl]);
4139
- const highlightIndex = React__default.useCallback(index => setHighlight(current => current.list === list && current.index === index ? current : {
4140
- index,
4141
- list
4142
- }), [list]);
4143
- const handleKeyDown = event => {
4144
- const step = verticalStep(event.key);
4145
- if (step && options.length > 0) {
4146
- event.preventDefault();
4147
- highlightIndex((highlightedIndex + step + options.length) % options.length);
4148
- } else if (event.key === "Enter" && highlighted && !event.nativeEvent.isComposing) {
4149
- event.preventDefault();
4150
- open(highlighted.url, {
4151
- newTab: hasCommandKey(event)
4152
- });
4153
- } else if (isCopyShortcut(event) && highlighted && !hasSelectedText(event.currentTarget)) {
4154
- event.preventDefault();
4155
- const {
4156
- url
4157
- } = highlighted;
4158
- navigator.clipboard.writeText(url).then(() => setCopiedUrl(url));
4159
- }
4160
- };
4161
- return {
4162
- copied: copiedUrl !== null && copiedUrl === (highlighted === null || highlighted === void 0 ? void 0 : highlighted.url),
4163
- handleKeyDown,
4164
- highlightIndex,
4165
- highlighted,
4166
- highlightedIndex: highlighted ? highlightedIndex : null
4167
- };
4168
- }
4169
- function open(url, {
4170
- newTab
4171
- }) {
4172
- if (newTab) {
4173
- window.open(url, "_blank", "noopener");
4174
- } else {
4175
- window.location.assign(url);
4176
- }
4177
- }
4178
- const isCopyShortcut = event => event.key.toLowerCase() === "c" && !event.altKey && !event.shiftKey && hasCommandKey(event);
4179
- // With text selected, ⌘C should copy that text as usual, not the link.
4180
- function hasSelectedText(element) {
4181
- var _a, _b;
4182
- const selectedInField = element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
4183
- return selectedInField || ((_b = (_a = window.getSelection()) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : "") !== "";
4513
+ if (!enabled) return undefined;
4514
+ // Rich text editors like Trix bind ⌘K to "insert link"; capturing at the
4515
+ // window and stopping propagation keeps them from ever seeing it. Under a
4516
+ // product's modal, search stays shut rather than stacking a second modal,
4517
+ // and the keystroke goes on to the product.
4518
+ const handleKeyDown = event => {
4519
+ if (open && isSearchShortcut(event)) {
4520
+ event.preventDefault();
4521
+ event.stopPropagation();
4522
+ setOpen(false);
4523
+ } else if (!open && (isSearchShortcut(event) || isLegacySearchShortcut(event)) && !isModalOpen()) {
4524
+ event.preventDefault();
4525
+ event.stopPropagation();
4526
+ setOpen(true);
4527
+ }
4528
+ };
4529
+ window.addEventListener("keydown", handleKeyDown, {
4530
+ capture: true
4531
+ });
4532
+ return () => window.removeEventListener("keydown", handleKeyDown, {
4533
+ capture: true
4534
+ });
4535
+ }, [enabled, open, setOpen]);
4184
4536
  }
4185
4537
 
4186
4538
  const SEARCHABLE_PRODUCTS = [{
@@ -4208,6 +4560,7 @@ const SEARCHABLE_PRODUCTS = [{
4208
4560
  appName: "Services",
4209
4561
  slug: "services"
4210
4562
  }];
4563
+
4211
4564
  const PERMISSION_CHECKS = {
4212
4565
  "check-ins": () => fetchMe("check-ins", "permission").then(({
4213
4566
  permission
@@ -4270,6 +4623,7 @@ function canSearch(slug, personId) {
4270
4623
  return promise;
4271
4624
  }
4272
4625
  }
4626
+
4273
4627
  function useSearchableProducts(apps) {
4274
4628
  // Keyed by name because providers hand over a fresh `apps` array on renders
4275
4629
  // where nothing changed.
@@ -4311,181 +4665,27 @@ function inSwitcherOrder(apps) {
4311
4665
  return groupApps(apps).restProducts.flatMap(app => SEARCHABLE_PRODUCTS.filter(product => product.appName === app.name));
4312
4666
  }
4313
4667
 
4314
- const isSearchShortcut = event => event.key.toLowerCase() === "k" && !event.altKey && !event.shiftKey && hasCommandKey(event);
4315
- // Products opened their own search with `/` before this one replaced it.
4316
- const isLegacySearchShortcut = event => event.key === "/" && !event.altKey && !event.ctrlKey && !event.metaKey && !isTypingIn(event.target);
4317
- const isTypingKey = event => event.key.length === 1 && event.key !== " " && !event.altKey && !event.ctrlKey && !event.metaKey;
4318
- const isTypingIn = target => target instanceof Element && target.closest('input, select, textarea, [contenteditable]:not([contenteditable="false"])') !== null;
4319
4668
  function ProductSearch({
4320
4669
  apps,
4321
4670
  env
4322
4671
  }) {
4323
4672
  const searchable = useSearchableProducts(apps);
4324
- if (searchable.status === "done" && searchable.products.length > 0) {
4325
- return /*#__PURE__*/React__default.createElement(SearchButtonAndDialog, {
4326
- env: env,
4327
- products: searchable.products
4328
- });
4329
- } else {
4330
- return null;
4331
- }
4332
- }
4333
- function SearchButtonAndDialog({
4334
- env,
4335
- products
4336
- }) {
4337
4673
  const [open, setOpen] = React__default.useState(false);
4338
- React__default.useEffect(() => {
4339
- // Rich text editors like Trix bind ⌘K to "insert link"; capturing at the
4340
- // window and stopping propagation keeps them from ever seeing it.
4341
- const handleKeyDown = event => {
4342
- if (isSearchShortcut(event)) {
4343
- event.preventDefault();
4344
- event.stopPropagation();
4345
- setOpen(wasOpen => !wasOpen);
4346
- } else if (isLegacySearchShortcut(event)) {
4347
- event.preventDefault();
4348
- event.stopPropagation();
4349
- setOpen(true);
4350
- }
4351
- };
4352
- window.addEventListener("keydown", handleKeyDown, {
4353
- capture: true
4354
- });
4355
- return () => window.removeEventListener("keydown", handleKeyDown, {
4356
- capture: true
4357
- });
4358
- }, []);
4359
- return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("button", {
4360
- "aria-keyshortcuts": `${isMac() ? "Meta+K" : "Control+K"} /`,
4361
- "aria-label": "Search",
4362
- onClick: () => setOpen(true),
4363
- type: "button"
4364
- }, /*#__PURE__*/React__default.createElement(Spyglass, {
4365
- fill: "currentColor"
4366
- })), /*#__PURE__*/React__default.createElement(Modal, {
4674
+ const available = searchable.status === "done" && searchable.products.length > 0;
4675
+ useSearchShortcut(available, open, setOpen);
4676
+ if (!available) return null;
4677
+ return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(SearchButton, {
4678
+ onClick: () => setOpen(true)
4679
+ }), /*#__PURE__*/React__default.createElement(Modal, {
4367
4680
  "aria-label": "Search",
4368
4681
  dismissible: true,
4369
4682
  onClose: () => setOpen(false),
4370
4683
  open: open
4371
4684
  }, /*#__PURE__*/React__default.createElement(SearchPanel, {
4372
4685
  env: env,
4373
- products: products
4686
+ products: searchable.products
4374
4687
  })));
4375
4688
  }
4376
- // Keys the recents list for navigation, so the highlight stays put as more
4377
- // recents load and as their lookups answer.
4378
- const RECENTS = "recents";
4379
- function SearchPanel({
4380
- env,
4381
- products
4382
- }) {
4383
- var _a, _b;
4384
- const listboxId = useInstanceId("product-search-results");
4385
- const [term, setTerm] = React__default.useState("");
4386
- const [selectedSlug, setSelectedSlug] = React__default.useState(() => initialProduct(products).slug);
4387
- const product = (_a = products.find(({
4388
- slug
4389
- }) => slug === selectedSlug)) !== null && _a !== void 0 ? _a : products[0];
4390
- const hasTerm = term.trim() !== "";
4391
- const search = useProductSearch(product.slug, term);
4392
- const groups = "groups" in search ? search.groups : undefined;
4393
- const results = React__default.useMemo(() => {
4394
- var _a;
4395
- return (_a = groups === null || groups === void 0 ? void 0 : groups.flatMap(group => group.results)) !== null && _a !== void 0 ? _a : [];
4396
- }, [groups]);
4397
- const recents = useRecentVisits(env);
4398
- const options = hasTerm ? results.map(({
4399
- attributes
4400
- }) => ({
4401
- label: attributes.title,
4402
- url: attributes.url
4403
- })) : recents.visits.map(visit => {
4404
- const {
4405
- title,
4406
- url
4407
- } = describeVisit(visit);
4408
- return {
4409
- label: title,
4410
- url
4411
- };
4412
- });
4413
- const navigation = useResultNavigation(options, hasTerm ? results : RECENTS);
4414
- const {
4415
- highlightedIndex
4416
- } = navigation;
4417
- const inputRef = React__default.useRef(null);
4418
- const [announceHighlight, setAnnounceHighlight] = React__default.useState(false);
4419
- // The tabs keep focus while arrowing through results, but only the focused
4420
- // input can point screen readers at the highlight, so from the tabs it's
4421
- // announced instead. Typing goes back to the input, keystroke and all.
4422
- const handleTabsKeyDown = event => {
4423
- var _a;
4424
- if (isTypingKey(event)) {
4425
- (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
4426
- } else if (verticalStep(event.key)) {
4427
- setAnnounceHighlight(true);
4428
- navigation.handleKeyDown(event);
4429
- } else {
4430
- navigation.handleKeyDown(event);
4431
- }
4432
- };
4433
- React__default.useEffect(() => {
4434
- if (hasTerm) saveLastProduct(product.slug);
4435
- }, [hasTerm, product.slug]);
4436
- return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("input", {
4437
- "aria-activedescendant": highlightedIndex === null ? undefined : resultId(listboxId, highlightedIndex),
4438
- "aria-autocomplete": "list",
4439
- "aria-controls": options.length > 0 ? listboxId : undefined,
4440
- "aria-expanded": options.length > 0,
4441
- "aria-label": "Search",
4442
- autoFocus: true,
4443
- onChange: event => setTerm(event.target.value),
4444
- onFocus: () => setAnnounceHighlight(false),
4445
- onKeyDown: navigation.handleKeyDown,
4446
- placeholder: "Search",
4447
- ref: inputRef,
4448
- role: "combobox",
4449
- type: "search",
4450
- value: term
4451
- }), /*#__PURE__*/React__default.createElement(ProductTabs, {
4452
- onKeyDown: handleTabsKeyDown,
4453
- onSelect: setSelectedSlug,
4454
- products: products,
4455
- selected: product
4456
- }, hasTerm ? (/*#__PURE__*/React__default.createElement(SearchResults, {
4457
- highlightedIndex: highlightedIndex,
4458
- id: listboxId,
4459
- label: `${product.appName} results`,
4460
- onHighlight: navigation.highlightIndex,
4461
- search: search,
4462
- term: term
4463
- })) : (/*#__PURE__*/React__default.createElement(RecentlyVisited, {
4464
- highlightedIndex: highlightedIndex,
4465
- id: listboxId,
4466
- onHighlight: navigation.highlightIndex,
4467
- ...recents
4468
- }))), /*#__PURE__*/React__default.createElement("div", {
4469
- "aria-live": "polite",
4470
- className: styles$c.visuallyHidden
4471
- }, announceHighlight ? (_b = navigation.highlighted) === null || _b === void 0 ? void 0 : _b.label : null), /*#__PURE__*/React__default.createElement(KeyboardHints, {
4472
- copied: navigation.copied
4473
- }));
4474
- }
4475
- function KeyboardHints({
4476
- copied
4477
- }) {
4478
- const command = commandKeyLabel();
4479
- return /*#__PURE__*/React__default.createElement("p", null, "\u2191\u2193 Select \u00B7 \u21B5 Open \u00B7 ", command, "\u21B5 Open in new tab \u00B7 ", command, "C", " ", /*#__PURE__*/React__default.createElement("span", {
4480
- role: "status"
4481
- }, copied ? "Copied!" : "Copy link"), " \u00B7 ", command, "K Open search");
4482
- }
4483
- function initialProduct(products) {
4484
- var _a, _b, _c;
4485
- const lastProduct = readLastProduct();
4486
- const bySlug = slug => products.find(product => product.slug === slug);
4487
- 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];
4488
- }
4489
4689
 
4490
4690
  function useRecordRecentlyVisited({
4491
4691
  enabled,
@@ -4529,9 +4729,9 @@ function StyledRoot({
4529
4729
  `), children);
4530
4730
  }
4531
4731
 
4532
- var styles$9 = {"route":"route--8BWe","is-active":"is-active-FsU9Y","apps-routes-separator":"apps-routes-separator-Nlm-N"};
4732
+ var styles$a = {"route":"route--8BWe","is-active":"is-active-FsU9Y","apps-routes-separator":"apps-routes-separator-Nlm-N"};
4533
4733
 
4534
- 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"};
4734
+ 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"};
4535
4735
 
4536
4736
  const UserMenu = ({
4537
4737
  appName,
@@ -4550,7 +4750,7 @@ const UserMenu = ({
4550
4750
  component: Outsider,
4551
4751
  onOutsideClick: () => setUserMenuVisible(false),
4552
4752
  cleanup: !userMenuVisible,
4553
- className: styles$8.popup,
4753
+ className: styles$9.popup,
4554
4754
  isVisible: userMenuVisible
4555
4755
  }, /*#__PURE__*/React__default.createElement("div", null, hasConnectedPeople ? (/*#__PURE__*/React__default.createElement("div", {
4556
4756
  style: {
@@ -4694,11 +4894,11 @@ const UserMenuTrigger = ({
4694
4894
  userMenuVisible
4695
4895
  }) => {
4696
4896
  return /*#__PURE__*/React__default.createElement("div", {
4697
- className: styles$8["trigger-wrap"],
4897
+ className: styles$9["trigger-wrap"],
4698
4898
  "data-pendo": "topbar-org-switcher"
4699
4899
  }, /*#__PURE__*/React__default.createElement(Hoverable, {
4700
- className: classnames(styles$8["trigger"], {
4701
- [styles$8["is-visible"]]: userMenuVisible
4900
+ className: classnames(styles$9["trigger"], {
4901
+ [styles$9["is-visible"]]: userMenuVisible
4702
4902
  }),
4703
4903
  component: "button",
4704
4904
  type: "button",
@@ -4710,7 +4910,7 @@ const UserMenuTrigger = ({
4710
4910
  requestConnectedPeopleFetch();
4711
4911
  }
4712
4912
  }, showOrgName && (/*#__PURE__*/React__default.createElement("span", {
4713
- className: styles$8["trigger-orgname"]
4913
+ className: styles$9["trigger-orgname"]
4714
4914
  }, orgName)), /*#__PURE__*/React__default.createElement(DisclosureChevronIcon, null)));
4715
4915
  };
4716
4916
 
@@ -4770,7 +4970,7 @@ const NotSmallTopbar = ({
4770
4970
  colors: props.colors,
4771
4971
  requestAppsFetch: props.requestAppsFetch
4772
4972
  })), /*#__PURE__*/React__default.createElement("div", {
4773
- className: styles$9["apps-routes-separator"]
4973
+ className: styles$a["apps-routes-separator"]
4774
4974
  }), routesVisible && props.routes, /*#__PURE__*/React__default.createElement("div", {
4775
4975
  style: {
4776
4976
  margin: "auto"
@@ -4811,12 +5011,12 @@ const NotSmallRoute = ({
4811
5011
  ...nativeProps
4812
5012
  }) => {
4813
5013
  return /*#__PURE__*/React__default.createElement("a", {
4814
- className: classnames(styles$9["route"], className, active && styles$9["is-active"]),
5014
+ className: classnames(styles$a["route"], className, active && styles$a["is-active"]),
4815
5015
  ...nativeProps
4816
5016
  });
4817
5017
  };
4818
5018
 
4819
- var styles$7 = {"avatar":"avatar-tW-wn"};
5019
+ var styles$8 = {"avatar":"avatar-tW-wn"};
4820
5020
 
4821
5021
  function Avatar({
4822
5022
  env,
@@ -4826,7 +5026,7 @@ function Avatar({
4826
5026
  }) {
4827
5027
  const url = incomingUrl || `${pcoUrl$1(env)("people")}/static/no_photo_thumbnail_gray.svg`;
4828
5028
  return /*#__PURE__*/React__default.createElement("div", {
4829
- className: styles$7.avatar,
5029
+ className: styles$8.avatar,
4830
5030
  style: {
4831
5031
  backgroundImage: `url(${url})`,
4832
5032
  ...style
@@ -32585,7 +32785,7 @@ function requireReactDom () {
32585
32785
 
32586
32786
  var reactDomExports = requireReactDom();
32587
32787
 
32588
- 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"};
32788
+ 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"};
32589
32789
 
32590
32790
  const AppsSwitcher = ({
32591
32791
  appName,
@@ -32613,8 +32813,8 @@ const AppsSwitcher = ({
32613
32813
  }, /*#__PURE__*/React__default.createElement("a", {
32614
32814
  "aria-current": name === appName ? "page" : undefined,
32615
32815
  "aria-label": name,
32616
- className: classnames(styles$6.link, {
32617
- [styles$6.current]: name === appName
32816
+ className: classnames(styles$7.link, {
32817
+ [styles$7.current]: name === appName
32618
32818
  }),
32619
32819
  "data-pendo": `app-switcher--change-app--${name}`,
32620
32820
  "data-turbolinks": false,
@@ -32626,48 +32826,48 @@ const AppsSwitcher = ({
32626
32826
  "aria-expanded": visible,
32627
32827
  "aria-haspopup": "true",
32628
32828
  "aria-label": `App switcher: ${appName}`,
32629
- className: styles$6.trigger,
32829
+ className: styles$7.trigger,
32630
32830
  onClick: onTriggerClick,
32631
32831
  type: "button"
32632
32832
  }, /*#__PURE__*/React__default.createElement(ColorAppIcon, {
32633
32833
  appName: appName.replace(/[\s-]/, "")
32634
32834
  })), /*#__PURE__*/reactDomExports.createPortal(/*#__PURE__*/React__default.createElement("div", {
32635
32835
  ref: containerRef,
32636
- className: classnames(styles$6.layer, {
32637
- [styles$6["layer--visible"]]: visible
32836
+ className: classnames(styles$7.layer, {
32837
+ [styles$7["layer--visible"]]: visible
32638
32838
  }),
32639
32839
  onClick: toggle,
32640
32840
  onKeyDown: handleKeyDown
32641
32841
  }, /*#__PURE__*/React__default.createElement("nav", {
32642
32842
  "aria-label": "Switch app",
32643
32843
  "aria-modal": visible,
32644
- className: styles$6.panel,
32844
+ className: styles$7.panel,
32645
32845
  "data-pendo": "app-switcher",
32646
32846
  onClick: event => event.stopPropagation(),
32647
32847
  role: "dialog"
32648
32848
  }, /*#__PURE__*/React__default.createElement("div", {
32649
- className: styles$6.header
32849
+ className: styles$7.header
32650
32850
  }, /*#__PURE__*/React__default.createElement("span", {
32651
- className: styles$6.heading
32851
+ className: styles$7.heading
32652
32852
  }, "Switch to"), /*#__PURE__*/React__default.createElement("button", {
32653
32853
  "aria-label": "Close",
32654
- className: styles$6.close,
32854
+ className: styles$7.close,
32655
32855
  onClick: toggle,
32656
32856
  type: "button"
32657
32857
  }, /*#__PURE__*/React__default.createElement(X, {
32658
32858
  "aria-hidden": "true",
32659
32859
  fill: "var(--t-icon-color-dim)"
32660
32860
  }))), /*#__PURE__*/React__default.createElement("ul", {
32661
- className: styles$6.list,
32861
+ className: styles$7.list,
32662
32862
  role: "list"
32663
32863
  }, homeProduct && productLink(homeProduct.name), restProducts.map(({
32664
32864
  name
32665
32865
  }) => productLink(name)), accountsProduct && (/*#__PURE__*/React__default.createElement("li", {
32666
- className: styles$6["accounts-item"]
32866
+ className: styles$7["accounts-item"]
32667
32867
  }, /*#__PURE__*/React__default.createElement("a", {
32668
32868
  "aria-current": appName === "Accounts" ? "page" : undefined,
32669
32869
  "aria-label": "Account settings",
32670
- className: classnames(styles$6.link, styles$6["accounts-link"]),
32870
+ className: classnames(styles$7.link, styles$7["accounts-link"]),
32671
32871
  "data-pendo": "app-switcher--change-app--Accounts",
32672
32872
  "data-turbolinks": false,
32673
32873
  href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(accountsProduct.name)
@@ -32690,14 +32890,14 @@ ClientStorage.defaultProps = {
32690
32890
  method: "localStorage"
32691
32891
  };
32692
32892
 
32693
- 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"};
32893
+ var styles$6 = {"appmenu-button":"appmenu-button-b0UvT","appmenu-layer":"appmenu-layer-0IQpN","appmenu-container":"appmenu-container-gackd","appmenu-scroll":"appmenu-scroll-mPSx0","appmenu-list":"appmenu-list-h7clX","appmenu-list-a":"appmenu-list-a-rGIQN","appmenu-list-a--top":"appmenu-list-a--top-RT-2F","appmenu-list-a--bottom":"appmenu-list-a--bottom-YDeBC","appmenu-close-button":"appmenu-close-button-vv9bV","current-route":"current-route-wpJgz","is-active":"is-active-wy8Bg","routes":"routes-Nmt1S","route":"route-kkknQ","profile-button":"profile-button-a2WO2"};
32694
32894
 
32695
32895
  function AccountSettingsLink({
32696
32896
  env
32697
32897
  }) {
32698
32898
  const accountsName = "accounts";
32699
32899
  return /*#__PURE__*/React__default.createElement("a", {
32700
- className: classnames(styles$5["appmenu-list-a"], styles$5["appmenu-list-a--bottom"]),
32900
+ className: classnames(styles$6["appmenu-list-a"], styles$6["appmenu-list-a--bottom"]),
32701
32901
  "data-pendo": `app-switcher--change-app--${accountsName}`,
32702
32902
  "data-turbolinks": false,
32703
32903
  href: pcoUrl$1(env)(accountsName) + getAppsSwitchProductPath(accountsName)
@@ -32720,7 +32920,7 @@ const CloseMenuButton = ({
32720
32920
  }) => (/*#__PURE__*/React__default.createElement("button", {
32721
32921
  "aria-label": "Close menu",
32722
32922
  type: "button",
32723
- className: styles$5["appmenu-close-button"],
32923
+ className: styles$6["appmenu-close-button"],
32724
32924
  style: style
32725
32925
  }, /*#__PURE__*/React__default.createElement(X, {
32726
32926
  fill: color,
@@ -32781,7 +32981,7 @@ const LegacyAppsSwitcher = ({
32781
32981
  "aria-haspopup": "true",
32782
32982
  "aria-label": `App switcher: ${appName}`,
32783
32983
  type: "button",
32784
- className: styles$5["appmenu-button"],
32984
+ className: styles$6["appmenu-button"],
32785
32985
  onClick: onTriggerClick
32786
32986
  }, /*#__PURE__*/React__default.createElement(ColorAppIcon, {
32787
32987
  appName: appName.replace(/[\s-]/, "")
@@ -32789,7 +32989,7 @@ const LegacyAppsSwitcher = ({
32789
32989
  item: "Topbar:Small:Menus:Products:visibility",
32790
32990
  render: () => (/*#__PURE__*/React__default.createElement("div", {
32791
32991
  ref: appMenuRef,
32792
- className: styles$5["appmenu-layer"],
32992
+ className: styles$6["appmenu-layer"],
32793
32993
  style: {
32794
32994
  ...(visible && {
32795
32995
  display: "block"
@@ -32806,37 +33006,37 @@ const LegacyAppsSwitcher = ({
32806
33006
  e.stopPropagation();
32807
33007
  }
32808
33008
  },
32809
- className: styles$5["appmenu-container"]
33009
+ className: styles$6["appmenu-container"]
32810
33010
  }, /*#__PURE__*/React__default.createElement("a", {
32811
33011
  "aria-current": appName === (homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name) ? "page" : undefined,
32812
33012
  "aria-label": homeProduct ? homeProduct.name : appName,
32813
33013
  "data-pendo": `app-switcher--change-app--${homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name}`,
32814
33014
  "data-turbolinks": false,
32815
- className: classnames(styles$5["appmenu-list-a"], styles$5["appmenu-list-a--top"]),
33015
+ className: classnames(styles$6["appmenu-list-a"], styles$6["appmenu-list-a--top"]),
32816
33016
  href: homeProduct ? pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name) : undefined
32817
33017
  }, /*#__PURE__*/React__default.createElement(UserMenuAppLockup, {
32818
33018
  appName: homeProduct ? homeProduct.name : appName
32819
33019
  })), /*#__PURE__*/React__default.createElement("div", {
32820
- className: styles$5["appmenu-scroll"]
33020
+ className: styles$6["appmenu-scroll"]
32821
33021
  }, /*#__PURE__*/React__default.createElement("ul", {
32822
33022
  "data-pendo": "app-switcher",
32823
- className: styles$5["appmenu-list"]
33023
+ className: styles$6["appmenu-list"]
32824
33024
  }, appsMenuFormatter(restProducts).map(({
32825
33025
  name
32826
33026
  }) => (/*#__PURE__*/React__default.createElement("li", {
32827
33027
  key: name,
32828
- className: styles$5["appmenu-list-li"]
33028
+ className: styles$6["appmenu-list-li"]
32829
33029
  }, /*#__PURE__*/React__default.createElement("a", {
32830
33030
  "aria-current": name === appName ? "page" : undefined,
32831
33031
  "aria-label": name,
32832
33032
  "data-pendo": `app-switcher--change-app--${name}`,
32833
33033
  "data-turbolinks": false,
32834
- className: styles$5["appmenu-list-a"],
33034
+ className: styles$6["appmenu-list-a"],
32835
33035
  href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(name)
32836
33036
  }, /*#__PURE__*/React__default.createElement(UserMenuAppLockup, {
32837
33037
  appName: name
32838
33038
  }))))), accountsProduct && (/*#__PURE__*/React__default.createElement("li", {
32839
- className: styles$5["appmenu-list-li"]
33039
+ className: styles$6["appmenu-list-li"]
32840
33040
  }, /*#__PURE__*/React__default.createElement(AccountSettingsLink, {
32841
33041
  env: env
32842
33042
  }))))))))
@@ -32900,11 +33100,11 @@ const SmallTopbar = ({
32900
33100
  toggle: toggleProductSwitcher,
32901
33101
  visible: productSwitcherVisible
32902
33102
  })), routesMenuVisible && (/*#__PURE__*/React__default.createElement("div", {
32903
- className: styles$5["routes"]
33103
+ className: styles$6["routes"]
32904
33104
  }, props.routes)), /*#__PURE__*/React__default.createElement("button", {
32905
33105
  type: "button",
32906
- className: classnames(styles$5["current-route"], {
32907
- [styles$5["is-active"]]: routesMenuVisible
33106
+ className: classnames(styles$6["current-route"], {
33107
+ [styles$6["is-active"]]: routesMenuVisible
32908
33108
  }),
32909
33109
  onClick: props.routes.length > 0 ? () => {
32910
33110
  setProductSwitcherVisible(false);
@@ -32912,7 +33112,7 @@ const SmallTopbar = ({
32912
33112
  } : null
32913
33113
  }, /*#__PURE__*/React__default.createElement(CurrentRouteComponent, null, capitalize$1(currentRouteText)), props.routes.length > 0 && /*#__PURE__*/React__default.createElement(DisclosureChevronIcon, null)), /*#__PURE__*/React__default.createElement("button", {
32914
33114
  type: "button",
32915
- className: styles$5["profile-button"],
33115
+ className: styles$6["profile-button"],
32916
33116
  onClick: () => {
32917
33117
  var _a, _b;
32918
33118
  props.requestConnectedPeopleFetch();
@@ -32928,7 +33128,7 @@ const SmallRoute = ({
32928
33128
  className,
32929
33129
  ...props
32930
33130
  }) => (/*#__PURE__*/React__default.createElement("a", {
32931
- className: classnames(styles$5["route"], className, active && styles$5["is-active"]),
33131
+ className: classnames(styles$6["route"], className, active && styles$6["is-active"]),
32932
33132
  ...props
32933
33133
  }));
32934
33134
 
@@ -33067,7 +33267,7 @@ const topbarTheme = {
33067
33267
  id: "topbar"
33068
33268
  };
33069
33269
 
33070
- 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"};
33270
+ 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"};
33071
33271
 
33072
33272
  function isPresent(value) {
33073
33273
  return value !== undefined && value.trim() !== "";
@@ -33096,11 +33296,11 @@ function Field({
33096
33296
  value
33097
33297
  }) {
33098
33298
  return /*#__PURE__*/React__default.createElement("div", {
33099
- className: styles$4["debug-pco-resource-field"]
33299
+ className: styles$5["debug-pco-resource-field"]
33100
33300
  }, /*#__PURE__*/React__default.createElement("span", {
33101
- className: styles$4["debug-pco-resource-field-label"]
33301
+ className: styles$5["debug-pco-resource-field-label"]
33102
33302
  }, label), /*#__PURE__*/React__default.createElement("span", {
33103
- className: styles$4["debug-pco-resource-field-value"]
33303
+ className: styles$5["debug-pco-resource-field-value"]
33104
33304
  }, value));
33105
33305
  }
33106
33306
  // Only staff ghosting or impersonating a person get pco:staff-access-level,
@@ -33173,13 +33373,13 @@ function DebugPanel({
33173
33373
  };
33174
33374
  }, [pcrl, env]);
33175
33375
  return /*#__PURE__*/React__default.createElement("div", {
33176
- className: classnames("staff-only", styles$4["debug-pco-resource"])
33376
+ className: classnames("staff-only", styles$5["debug-pco-resource"])
33177
33377
  }, /*#__PURE__*/React__default.createElement("div", {
33178
- className: styles$4["debug-pco-resource-header"]
33378
+ className: styles$5["debug-pco-resource-header"]
33179
33379
  }, /*#__PURE__*/React__default.createElement("span", {
33180
- className: styles$4["debug-pco-resource-badge"]
33380
+ className: styles$5["debug-pco-resource-badge"]
33181
33381
  }, "Staff"), /*#__PURE__*/React__default.createElement("span", {
33182
- className: styles$4["debug-pco-resource-title"]
33382
+ className: styles$5["debug-pco-resource-title"]
33183
33383
  }, "Recently Visited debug")), /*#__PURE__*/React__default.createElement(Field, {
33184
33384
  label: "pco:resource",
33185
33385
  value: describeTag("resource", resource)
@@ -33187,29 +33387,29 @@ function DebugPanel({
33187
33387
  label: "pco:recently_visitable",
33188
33388
  value: describeTag("recently_visitable", recentlyVisitable)
33189
33389
  }), hasInvalidResource && (/*#__PURE__*/React__default.createElement("p", {
33190
- className: styles$4["debug-pco-resource-note"]
33390
+ className: styles$5["debug-pco-resource-note"]
33191
33391
  }, "pco:resource isn't a PCRL (product/v2/type/id), so Recently Visited ignores it.")), visit === null && (/*#__PURE__*/React__default.createElement("p", {
33192
- className: styles$4["debug-pco-resource-note"]
33392
+ className: styles$5["debug-pco-resource-note"]
33193
33393
  }, "This page is not eligible for Recently Visited")), pcrl && hasRecentlyVisitable && (/*#__PURE__*/React__default.createElement("p", {
33194
- className: styles$4["debug-pco-resource-note"]
33394
+ className: styles$5["debug-pco-resource-note"]
33195
33395
  }, "Both tags are present \u2014 pco:resource takes precedence for the Recently Visited entry.")), pcrl && lookup.status === "loading" && (/*#__PURE__*/React__default.createElement("p", {
33196
- className: styles$4["debug-pco-resource-note"]
33396
+ className: styles$5["debug-pco-resource-note"]
33197
33397
  }, "Looking up resource\u2026")), pcrl && lookup.status === "error" && (/*#__PURE__*/React__default.createElement("p", {
33198
- className: styles$4["debug-pco-resource-note"]
33398
+ className: styles$5["debug-pco-resource-note"]
33199
33399
  }, lookup.message)), pcrl && lookup.status === "loaded" && (/*#__PURE__*/React__default.createElement("pre", {
33200
- className: styles$4["debug-pco-resource-json"]
33400
+ className: styles$5["debug-pco-resource-json"]
33201
33401
  }, JSON.stringify(lookup.json, null, 2))), (visit === null || visit === void 0 ? void 0 : visit.type) === "page" && (/*#__PURE__*/React__default.createElement("p", {
33202
- className: styles$4["debug-pco-resource-note"]
33402
+ className: styles$5["debug-pco-resource-note"]
33203
33403
  }, "Recently Visited title: ", visit.title)), /*#__PURE__*/React__default.createElement("span", {
33204
- className: styles$4["debug-pco-resource-field-label"]
33404
+ className: styles$5["debug-pco-resource-field-label"]
33205
33405
  }, "Recently Visited"), personId ? (/*#__PURE__*/React__default.createElement("pre", {
33206
- className: styles$4["debug-pco-resource-json"]
33406
+ className: styles$5["debug-pco-resource-json"]
33207
33407
  }, JSON.stringify(recentlyVisited, null, 2))) : (/*#__PURE__*/React__default.createElement("p", {
33208
- className: styles$4["debug-pco-resource-note"]
33408
+ className: styles$5["debug-pco-resource-note"]
33209
33409
  }, "No current person, so nothing is recorded.")));
33210
33410
  }
33211
33411
 
33212
- var styles$3 = {"drawer-wrap":"drawer-wrap-J0rSL","is-visible":"is-visible-KTME8","drawer":"drawer-j7f4l"};
33412
+ var styles$4 = {"drawer-wrap":"drawer-wrap-J0rSL","is-visible":"is-visible-KTME8","drawer":"drawer-j7f4l"};
33213
33413
 
33214
33414
  function Drawer({
33215
33415
  children,
@@ -33217,7 +33417,7 @@ function Drawer({
33217
33417
  }) {
33218
33418
  return /*#__PURE__*/React__default.createElement("div", {
33219
33419
  id: id,
33220
- className: styles$3["drawer"]
33420
+ className: styles$4["drawer"]
33221
33421
  }, children);
33222
33422
  }
33223
33423
  function DrawerWrap({
@@ -33227,7 +33427,7 @@ function DrawerWrap({
33227
33427
  ...props
33228
33428
  }) {
33229
33429
  return /*#__PURE__*/React__default.createElement("div", {
33230
- className: classnames(styles$3["drawer-wrap"], className, isVisible && styles$3["is-visible"]),
33430
+ className: classnames(styles$4["drawer-wrap"], className, isVisible && styles$4["is-visible"]),
33231
33431
  ...props
33232
33432
  }, children);
33233
33433
  }
@@ -33443,7 +33643,7 @@ function Icon({
33443
33643
  }));
33444
33644
  }
33445
33645
 
33446
- 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"};
33646
+ 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"};
33447
33647
 
33448
33648
  const COLOR_MODES = [{
33449
33649
  icon: "toolbar.sun",
@@ -33466,11 +33666,11 @@ function ColorModeSwitcher() {
33466
33666
  const updateStore = useUpdateStore();
33467
33667
  const [counter] = useState(() => COLOR_MODE_INSTANCE_COUNTER++);
33468
33668
  return /*#__PURE__*/React__default.createElement("fieldset", {
33469
- className: styles$2["color-mode-switcher"]
33669
+ className: styles$3["color-mode-switcher"]
33470
33670
  }, /*#__PURE__*/React__default.createElement("legend", {
33471
- className: styles$2["color-mode-switcher-label"]
33671
+ className: styles$3["color-mode-switcher-label"]
33472
33672
  }, "Appearance", " "), /*#__PURE__*/React__default.createElement("div", {
33473
- className: styles$2["color-mode-switcher-modes"]
33673
+ className: styles$3["color-mode-switcher-modes"]
33474
33674
  }, COLOR_MODES.map(({
33475
33675
  icon,
33476
33676
  label,
@@ -33740,7 +33940,7 @@ const useToolbarReducer = overrides => React.useReducer(reducer, {
33740
33940
  ...overrides
33741
33941
  });
33742
33942
 
33743
- 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"};
33943
+ 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"};
33744
33944
 
33745
33945
  // PCO Box serves TLS by default, and a bare http:// frame inside an https
33746
33946
  // product page is blocked as mixed content, so follow the page's scheme.
@@ -33912,7 +34112,7 @@ const DeveloperTools = {
33912
34112
  name: "DeveloperTools"
33913
34113
  };
33914
34114
 
33915
- 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"};
34115
+ 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"};
33916
34116
 
33917
34117
  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";
33918
34118
  // Same stops as the AskAIBanner border gradient, so the two stay in sync.
@@ -33972,8 +34172,8 @@ function AskAIBanner(props) {
33972
34172
  var _a, _b;
33973
34173
  const isStacked = props.variant !== "inline";
33974
34174
  return /*#__PURE__*/React__default.createElement("button", {
33975
- className: classnames(styles.banner, {
33976
- [styles.stacked]: isStacked
34175
+ className: classnames(styles$1.banner, {
34176
+ [styles$1.stacked]: isStacked
33977
34177
  }),
33978
34178
  onClick: () => props.onClick(),
33979
34179
  type: "button"
@@ -33988,15 +34188,15 @@ function StackedContent({
33988
34188
  title
33989
34189
  }) {
33990
34190
  return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("span", {
33991
- className: styles.content
34191
+ className: styles$1.content
33992
34192
  }, /*#__PURE__*/React__default.createElement(AskAIIcon, null), /*#__PURE__*/React__default.createElement("span", {
33993
- className: styles.text
34193
+ className: styles$1.text
33994
34194
  }, /*#__PURE__*/React__default.createElement("span", {
33995
- className: styles.title
34195
+ className: styles$1.title
33996
34196
  }, title), /*#__PURE__*/React__default.createElement("span", {
33997
- className: styles.subtitle
34197
+ className: styles$1.subtitle
33998
34198
  }, "Support answers tailored to you"))), /*#__PURE__*/React__default.createElement(Icon$1, {
33999
- className: styles.chevron,
34199
+ className: styles$1.chevron,
34000
34200
  name: "general.rightChevron",
34001
34201
  size: "16px"
34002
34202
  }));
@@ -34006,18 +34206,18 @@ function InlineContent({
34006
34206
  title
34007
34207
  }) {
34008
34208
  return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("span", {
34009
- className: styles.inlineContent
34209
+ className: styles$1.inlineContent
34010
34210
  }, /*#__PURE__*/React__default.createElement(SparkleIcon, {
34011
- className: styles.sparkle,
34211
+ className: styles$1.sparkle,
34012
34212
  fill: "currentColor"
34013
34213
  }), /*#__PURE__*/React__default.createElement("span", {
34014
- className: styles.inlineTitle
34214
+ className: styles$1.inlineTitle
34015
34215
  }, title)), /*#__PURE__*/React__default.createElement("span", {
34016
- className: styles.action
34216
+ className: styles$1.action
34017
34217
  }, /*#__PURE__*/React__default.createElement("span", {
34018
- className: styles.actionLabel
34218
+ className: styles$1.actionLabel
34019
34219
  }, action), /*#__PURE__*/React__default.createElement(Icon$1, {
34020
- className: styles.actionChevron,
34220
+ className: styles$1.actionChevron,
34021
34221
  name: "general.rightChevron",
34022
34222
  size: "14px"
34023
34223
  })));
@@ -34034,9 +34234,9 @@ function AskAIIcon() {
34034
34234
  const borderA = `${uid}-border-a`;
34035
34235
  const borderB = `${uid}-border-b`;
34036
34236
  return /*#__PURE__*/React__default.createElement("span", {
34037
- className: styles.icon
34237
+ className: styles$1.icon
34038
34238
  }, /*#__PURE__*/React__default.createElement("svg", {
34039
- className: styles.iconShape,
34239
+ className: styles$1.iconShape,
34040
34240
  viewBox: "0 0 32 32",
34041
34241
  fill: "none",
34042
34242
  "aria-hidden": "true"
@@ -34207,7 +34407,7 @@ function AskAIIcon() {
34207
34407
  stopColor: "#D48B91",
34208
34408
  stopOpacity: "0"
34209
34409
  })))), /*#__PURE__*/React__default.createElement("svg", {
34210
- className: styles.iconSparkle,
34410
+ className: styles$1.iconSparkle,
34211
34411
  viewBox: "0 0 19.4995 17.9999",
34212
34412
  "aria-hidden": "true"
34213
34413
  }, /*#__PURE__*/React__default.createElement("path", {
@@ -34216,6 +34416,23 @@ function AskAIIcon() {
34216
34416
  })));
34217
34417
  }
34218
34418
 
34419
+ var styles = {"button":"button-2NVI1","sparkle":"sparkle-ruJGP","label":"label-4NUGB"};
34420
+
34421
+ function AskAIButton({
34422
+ onClick,
34423
+ text = "Ask AI"
34424
+ }) {
34425
+ return /*#__PURE__*/React__default.createElement("button", {
34426
+ className: styles.button,
34427
+ onClick: () => onClick(),
34428
+ type: "button"
34429
+ }, /*#__PURE__*/React__default.createElement(SparkleIcon, {
34430
+ className: styles.sparkle
34431
+ }), /*#__PURE__*/React__default.createElement("span", {
34432
+ className: styles.label
34433
+ }, text));
34434
+ }
34435
+
34219
34436
  // The article heading lives in the deepest populated hierarchy level (lvl0 is
34220
34437
  // always "Planning Center").
34221
34438
  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) || "";
@@ -34526,6 +34743,18 @@ function HelpdeskComponent({
34526
34743
  }
34527
34744
  }
34528
34745
  },
34746
+ AskAIButton: {
34747
+ component: AskAIButton,
34748
+ properties: {
34749
+ onClick: {
34750
+ event: false,
34751
+ type: Function
34752
+ },
34753
+ text: {
34754
+ type: String
34755
+ }
34756
+ }
34757
+ },
34529
34758
  CalendlyWidget: {
34530
34759
  component: CalendlyWidget,
34531
34760
  properties: {
@@ -36034,18 +36263,18 @@ function Toolbar({
36034
36263
  return /*#__PURE__*/React__default.createElement(ThemeProvider, {
36035
36264
  theme: topbarTheme
36036
36265
  }, /*#__PURE__*/React__default.createElement(UiExtensionDataContext, null, /*#__PURE__*/React__default.createElement("div", {
36037
- className: styles$1["toolbar"],
36266
+ className: styles$2["toolbar"],
36038
36267
  "data-pendo": "toolbar-profile-support-wrapper"
36039
36268
  }, /*#__PURE__*/React__default.createElement(ToolbarButton, {
36040
36269
  "aria-label": `Open ${userName}’s profile drawer`,
36041
36270
  onClick: () => toggleDrawer(Profile),
36042
36271
  "data-pendo": "toolbar-profile",
36043
- className: styles$1["toolbar-button--avatar"]
36272
+ className: styles$2["toolbar-button--avatar"]
36044
36273
  }, /*#__PURE__*/React__default.createElement(Avatar, {
36045
36274
  env: env,
36046
36275
  url: userAvatarPath
36047
36276
  })), /*#__PURE__*/React__default.createElement(ToolbarButton, {
36048
- className: styles$1["toolbar-button--home"],
36277
+ className: styles$2["toolbar-button--home"],
36049
36278
  onClick: () => closeDrawer()
36050
36279
  }, /*#__PURE__*/React__default.createElement(Icon$1, {
36051
36280
  name: "toolbar.planningCenter",
@@ -36082,7 +36311,7 @@ const ToolbarButton = ({
36082
36311
  }) => {
36083
36312
  return /*#__PURE__*/React__default.createElement("button", {
36084
36313
  type: "button",
36085
- className: classnames(styles$1["toolbar-button"], className, current && styles$1["is-current"]),
36314
+ className: classnames(styles$2["toolbar-button"], className, current && styles$2["is-current"]),
36086
36315
  onClick: onClick,
36087
36316
  "data-toolbar-link": "true",
36088
36317
  ...restProps