@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.
package/dist/topbar.js CHANGED
@@ -41,7 +41,7 @@ var churchCenter__namespace = /*#__PURE__*/_interopNamespaceDefault(churchCenter
41
41
  var people__namespace = /*#__PURE__*/_interopNamespaceDefault(people);
42
42
  var toolbar__namespace = /*#__PURE__*/_interopNamespaceDefault(toolbar);
43
43
 
44
- var styles$l = {"accessibility-toolbar":"accessibility-toolbar-jgjUG"};
44
+ var styles$m = {"accessibility-toolbar":"accessibility-toolbar-jgjUG"};
45
45
 
46
46
  const AccessibilityToolbar = ({
47
47
  children
@@ -53,7 +53,7 @@ const AccessibilityToolbar = ({
53
53
  (_a = document.getElementById("main-content")) === null || _a === void 0 ? void 0 : _a.focus();
54
54
  };
55
55
  return /*#__PURE__*/React.createElement("div", {
56
- className: styles$l["accessibility-toolbar"]
56
+ className: styles$m["accessibility-toolbar"]
57
57
  }, /*#__PURE__*/React.createElement(tapestry.Link, {
58
58
  href: "#main-content",
59
59
  kind: "primary",
@@ -372,11 +372,11 @@ const X = ({
372
372
  fill: fill
373
373
  })));
374
374
 
375
- 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"};
375
+ 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"};
376
376
 
377
377
  function AnnouncementIcon() {
378
378
  return /*#__PURE__*/React.createElement("svg", {
379
- className: styles$k["announcement-icon"],
379
+ className: styles$l["announcement-icon"],
380
380
  width: "16",
381
381
  height: "16",
382
382
  viewBox: "0 0 16 16",
@@ -501,7 +501,7 @@ let StyledAnnouncement$1 = class StyledAnnouncement extends React.Component {
501
501
  } = this.props;
502
502
  const isPromo = variant === "promo";
503
503
  return /*#__PURE__*/React.createElement("div", {
504
- className: classnames(styles$k["announcement"], styles$k[`announcement--${variant !== null && variant !== void 0 ? variant : "platform"}`]),
504
+ className: classnames(styles$l["announcement"], styles$l[`announcement--${variant !== null && variant !== void 0 ? variant : "platform"}`]),
505
505
  "data-announcement-variant": variant !== null && variant !== void 0 ? variant : "platform",
506
506
  style: isPromo ? {
507
507
  backgroundImage: promoBackgroundUrl,
@@ -510,7 +510,7 @@ let StyledAnnouncement$1 = class StyledAnnouncement extends React.Component {
510
510
  backgroundSize: "cover"
511
511
  } : undefined
512
512
  }, !isPromo && /*#__PURE__*/React.createElement(AnnouncementIcon, null), /*#__PURE__*/React.createElement("div", {
513
- className: styles$k["announcement-content"],
513
+ className: styles$l["announcement-content"],
514
514
  style: style,
515
515
  ...platformProps
516
516
  }));
@@ -559,7 +559,7 @@ function StyledDismissButton$1({
559
559
  const isPromo = variant === "promo";
560
560
  return /*#__PURE__*/React.createElement("button", {
561
561
  type: "button",
562
- className: classnames(styles$k.dismiss, isPromo && styles$k["dismiss--promo"]),
562
+ className: classnames(styles$l.dismiss, isPromo && styles$l["dismiss--promo"]),
563
563
  ...props
564
564
  }, /*#__PURE__*/React.createElement(X, {
565
565
  fill: isPromo ? "currentColor" : "white",
@@ -639,9 +639,9 @@ class StyledAnnouncement extends React.Component {
639
639
  ...platformProps
640
640
  } = this.props;
641
641
  return /*#__PURE__*/React.createElement("div", {
642
- className: styles$k["announcement"]
642
+ className: styles$l["announcement"]
643
643
  }, /*#__PURE__*/React.createElement(AnnouncementIcon, null), /*#__PURE__*/React.createElement("div", {
644
- className: styles$k["announcement-content"],
644
+ className: styles$l["announcement-content"],
645
645
  style: style,
646
646
  ...platformProps
647
647
  }));
@@ -652,7 +652,7 @@ function StyledDismissButton({
652
652
  }) {
653
653
  return /*#__PURE__*/React.createElement("button", {
654
654
  type: "button",
655
- className: styles$k.dismiss,
655
+ className: styles$l.dismiss,
656
656
  ...props
657
657
  }, /*#__PURE__*/React.createElement(X, {
658
658
  fill: "white",
@@ -1774,7 +1774,7 @@ function MonoAppIcon({
1774
1774
  })) : (/*#__PURE__*/React.createElement("div", null, "appName not supported")));
1775
1775
  }
1776
1776
 
1777
- var styles$j = {"icon-slot":"icon-slot-m3ZIk","name":"name-UNqbW"};
1777
+ var styles$k = {"icon-slot":"icon-slot-m3ZIk","name":"name-UNqbW"};
1778
1778
 
1779
1779
  const AppRow = ({
1780
1780
  appName,
@@ -1783,24 +1783,24 @@ const AppRow = ({
1783
1783
  }) => {
1784
1784
  if (appName === "Accounts") {
1785
1785
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
1786
- className: styles$j["icon-slot"]
1786
+ className: styles$k["icon-slot"]
1787
1787
  }, /*#__PURE__*/React.createElement(MonoAppIcon, {
1788
1788
  appName: "Accounts",
1789
1789
  colors: colors,
1790
1790
  size: iconSize
1791
1791
  })), /*#__PURE__*/React.createElement("span", {
1792
- className: styles$j.name
1792
+ className: styles$k.name
1793
1793
  }, "Account settings"));
1794
1794
  }
1795
1795
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(ColorAppIcon, {
1796
1796
  appName: appName.replace(/[\s-]/, ""),
1797
1797
  size: iconSize
1798
1798
  }), /*#__PURE__*/React.createElement("span", {
1799
- className: styles$j.name
1799
+ className: styles$k.name
1800
1800
  }, appName));
1801
1801
  };
1802
1802
 
1803
- var styles$i = {"button":"button-XMbeQ","label":"label-884of","chevron":"chevron-RRg9k"};
1803
+ var styles$j = {"button":"button-XMbeQ","label":"label-884of","chevron":"chevron-RRg9k"};
1804
1804
 
1805
1805
  const AppsButton = /*#__PURE__*/React.forwardRef(({
1806
1806
  appName,
@@ -1812,10 +1812,10 @@ const AppsButton = /*#__PURE__*/React.forwardRef(({
1812
1812
  ...restProps,
1813
1813
  ref: ref,
1814
1814
  "aria-label": `App switcher: ${appName}`,
1815
- className: styles$i.button,
1815
+ className: styles$j.button,
1816
1816
  kind: "neutral-inline",
1817
1817
  label: /*#__PURE__*/React.createElement("span", {
1818
- className: styles$i.label
1818
+ className: styles$j.label
1819
1819
  }, isAccounts ? "Account settings" : appName),
1820
1820
  prefix: isAccounts ? (/*#__PURE__*/React.createElement(MonoAppIcon, {
1821
1821
  appName: "Accounts",
@@ -1826,13 +1826,13 @@ const AppsButton = /*#__PURE__*/React.forwardRef(({
1826
1826
  size: 22
1827
1827
  })),
1828
1828
  suffix: /*#__PURE__*/React.createElement(DisclosureChevronIcon, {
1829
- className: `${styles$i.chevron} symbol`
1829
+ className: `${styles$j.chevron} symbol`
1830
1830
  })
1831
1831
  });
1832
1832
  });
1833
1833
  AppsButton.displayName = "AppsButton";
1834
1834
 
1835
- 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"};
1835
+ 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"};
1836
1836
 
1837
1837
  const AppsMenu = ({
1838
1838
  appName,
@@ -1853,8 +1853,8 @@ const AppsMenu = ({
1853
1853
  }, /*#__PURE__*/React.createElement("a", {
1854
1854
  "aria-current": isCurrent ? "page" : undefined,
1855
1855
  "aria-label": name,
1856
- className: classnames(styles$h.link, {
1857
- [styles$h.current]: isCurrent
1856
+ className: classnames(styles$i.link, {
1857
+ [styles$i.current]: isCurrent
1858
1858
  }),
1859
1859
  "data-pendo": `app-switcher--change-app--${name}`,
1860
1860
  "data-turbolinks": false,
@@ -1870,22 +1870,22 @@ const AppsMenu = ({
1870
1870
  colors: colors
1871
1871
  })), /*#__PURE__*/React.createElement(tapestry.PopoverContent, {
1872
1872
  "aria-label": "Switch app",
1873
- className: styles$h["popover-content"],
1873
+ className: styles$i["popover-content"],
1874
1874
  placement: "bottom start"
1875
1875
  }, /*#__PURE__*/React.createElement("nav", {
1876
- className: styles$h.nav,
1876
+ className: styles$i.nav,
1877
1877
  "data-pendo": "app-switcher"
1878
1878
  }, /*#__PURE__*/React.createElement("ul", {
1879
- className: styles$h.list,
1879
+ className: styles$i.list,
1880
1880
  role: "list"
1881
1881
  }, homeProduct && renderProductLink(homeProduct.name), restProducts.map(({
1882
1882
  name
1883
1883
  }) => renderProductLink(name)), accountsProduct && (/*#__PURE__*/React.createElement("li", {
1884
- className: styles$h["accounts-item"]
1884
+ className: styles$i["accounts-item"]
1885
1885
  }, /*#__PURE__*/React.createElement("a", {
1886
1886
  "aria-current": appName === accountsProduct.name ? "page" : undefined,
1887
1887
  "aria-label": "Account settings",
1888
- className: classnames(styles$h.link, styles$h["accounts-link"]),
1888
+ className: classnames(styles$i.link, styles$i["accounts-link"]),
1889
1889
  "data-pendo": "app-switcher--change-app--Accounts",
1890
1890
  "data-turbolinks": false,
1891
1891
  href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(accountsProduct.name)
@@ -2093,7 +2093,7 @@ function MonoAppName({
2093
2093
  })) : (/*#__PURE__*/React.createElement("div", null, "appName not supported"));
2094
2094
  }
2095
2095
 
2096
- var styles$g = {"unbutton":"unbutton-lEbTR"};
2096
+ var styles$h = {"unbutton":"unbutton-lEbTR"};
2097
2097
 
2098
2098
  function Unbutton({
2099
2099
  className,
@@ -2102,7 +2102,7 @@ function Unbutton({
2102
2102
  ...nativeProps
2103
2103
  }) {
2104
2104
  return /*#__PURE__*/React.createElement("button", {
2105
- className: classnames(styles$g.unbutton, className),
2105
+ className: classnames(styles$h.unbutton, className),
2106
2106
  style: {
2107
2107
  ...style
2108
2108
  },
@@ -2111,7 +2111,7 @@ function Unbutton({
2111
2111
  });
2112
2112
  }
2113
2113
 
2114
- var styles$f = {"button":"button-wsipc","button-accounts":"button-accounts-zQmBM","chevron":"chevron-obyBF","is-expanded":"is-expanded-PoCQ9"};
2114
+ var styles$g = {"button":"button-wsipc","button-accounts":"button-accounts-zQmBM","chevron":"chevron-obyBF","is-expanded":"is-expanded-PoCQ9"};
2115
2115
 
2116
2116
  const LegacyAppsButton = ({
2117
2117
  appName,
@@ -2126,8 +2126,8 @@ const LegacyAppsButton = ({
2126
2126
  "aria-haspopup": "true",
2127
2127
  "aria-label": `App switcher: ${appName}`,
2128
2128
  onClick: onClick,
2129
- className: classnames(styles$f.button, {
2130
- [styles$f["is-expanded"]]: expanded
2129
+ className: classnames(styles$g.button, {
2130
+ [styles$g["is-expanded"]]: expanded
2131
2131
  }),
2132
2132
  type: "button",
2133
2133
  style: style,
@@ -2136,7 +2136,7 @@ const LegacyAppsButton = ({
2136
2136
  appName: "Accounts",
2137
2137
  colors: colors
2138
2138
  }), /*#__PURE__*/React.createElement("div", {
2139
- className: styles$f["button-accounts"]
2139
+ className: styles$g["button-accounts"]
2140
2140
  }, "Account settings"))) : (/*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(ColorAppIcon, {
2141
2141
  appName: appName.replace(/[\s-]/, "")
2142
2142
  }), /*#__PURE__*/React.createElement(MonoAppName, {
@@ -2144,7 +2144,7 @@ const LegacyAppsButton = ({
2144
2144
  key: "mono-app-name",
2145
2145
  size: 24
2146
2146
  }))), /*#__PURE__*/React.createElement(DisclosureChevronIcon, {
2147
- className: styles$f.chevron,
2147
+ className: styles$g.chevron,
2148
2148
  key: "disclosure-chevron-icon"
2149
2149
  }));
2150
2150
  };
@@ -2277,7 +2277,7 @@ class Outsider extends React.Component {
2277
2277
  }
2278
2278
  }
2279
2279
 
2280
- var styles$e = {"popup-root":"popup-root-SeL5u","popup":"popup-nQXYW","is-visible":"is-visible-3DAUP"};
2280
+ var styles$f = {"popup-root":"popup-root-SeL5u","popup":"popup-nQXYW","is-visible":"is-visible-3DAUP"};
2281
2281
 
2282
2282
  const Popup = ({
2283
2283
  className,
@@ -2285,8 +2285,8 @@ const Popup = ({
2285
2285
  isVisible,
2286
2286
  ...nativeProps
2287
2287
  }) => (/*#__PURE__*/React.createElement(C, {
2288
- className: classnames(styles$e.popup, {
2289
- [styles$e["is-visible"]]: isVisible
2288
+ className: classnames(styles$f.popup, {
2289
+ [styles$f["is-visible"]]: isVisible
2290
2290
  }, className),
2291
2291
  ...nativeProps
2292
2292
  }));
@@ -2294,11 +2294,11 @@ const Popup = ({
2294
2294
  const PopupRoot = ({
2295
2295
  ...nativeProps
2296
2296
  }) => (/*#__PURE__*/React.createElement("div", {
2297
- className: styles$e["popup-root"],
2297
+ className: styles$f["popup-root"],
2298
2298
  ...nativeProps
2299
2299
  }));
2300
2300
 
2301
- var styles$d = {"popup":"popup-bVhSa"};
2301
+ var styles$e = {"popup":"popup-bVhSa"};
2302
2302
 
2303
2303
  const LegacyAppsMenu = ({
2304
2304
  appName,
@@ -2336,7 +2336,7 @@ const LegacyAppsMenu = ({
2336
2336
  cleanup: !visible,
2337
2337
  component: Outsider,
2338
2338
  onOutsideClick: toggle,
2339
- className: styles$d.popup,
2339
+ className: styles$e.popup,
2340
2340
  isVisible: visible
2341
2341
  }, /*#__PURE__*/React.createElement("menu", {
2342
2342
  ref: menuRef,
@@ -3249,27 +3249,34 @@ const useUpdateStore = () => React.useCallback(updates => {
3249
3249
  const isMac = () => /Mac|iPhone|iPad|iPod/.test(navigator.platform);
3250
3250
  const hasCommandKey = event => isMac() ? event.metaKey : event.ctrlKey;
3251
3251
  const commandKeyLabel = () => isMac() ? "⌘" : "Ctrl+";
3252
+ const isSearchShortcut = event => event.key.toLowerCase() === "k" && !event.altKey && !event.shiftKey && hasCommandKey(event);
3253
+ // Products opened their own search with `/` before this one replaced it.
3254
+ const isLegacySearchShortcut = event => event.key === "/" && !event.altKey && !event.ctrlKey && !event.metaKey && !isTypingIn(event.target);
3255
+ const isCopyShortcut = event => event.key.toLowerCase() === "c" && !event.altKey && !event.shiftKey && hasCommandKey(event);
3256
+ const isTypingIn = target => target instanceof Element && target.closest('input, select, textarea, [contenteditable]:not([contenteditable="false"])') !== null;
3257
+ const isTypingKey = event => event.key.length === 1 && event.key !== " " && !event.altKey && !event.ctrlKey && !event.metaKey;
3258
+ const verticalStep = key => ({
3259
+ ArrowDown: 1,
3260
+ ArrowUp: -1
3261
+ })[key];
3262
+ const horizontalStep = key => ({
3263
+ ArrowLeft: -1,
3264
+ ArrowRight: 1
3265
+ })[key];
3252
3266
 
3253
- const STORAGE_KEY = "topbar.product_search.last_product";
3254
- function readLastProduct() {
3255
- try {
3256
- return localStorage.getItem(STORAGE_KEY);
3257
- } catch {
3258
- return null;
3259
- }
3260
- }
3261
- function saveLastProduct(slug) {
3262
- try {
3263
- localStorage.setItem(STORAGE_KEY, slug);
3264
- } catch {
3265
- // Storage can be full or blocked; search still works without it.
3266
- }
3267
+ function SearchButton({
3268
+ onClick
3269
+ }) {
3270
+ return /*#__PURE__*/React.createElement("button", {
3271
+ "aria-keyshortcuts": `${isMac() ? "Meta+K" : "Control+K"} /`,
3272
+ "aria-label": "Search",
3273
+ onClick: onClick,
3274
+ type: "button"
3275
+ }, /*#__PURE__*/React.createElement(Spyglass, {
3276
+ fill: "currentColor"
3277
+ }));
3267
3278
  }
3268
3279
 
3269
- var styles$c = {"visuallyHidden":"visuallyHidden-C28ju"};
3270
-
3271
- var styles$b = {"tab":"tab-lQLDx"};
3272
-
3273
3280
  // useId isn't available on React 17, which our peer range still supports.
3274
3281
  let instanceCount$1 = 0;
3275
3282
  function useInstanceId(prefix) {
@@ -3277,58 +3284,237 @@ function useInstanceId(prefix) {
3277
3284
  return id;
3278
3285
  }
3279
3286
 
3280
- function ProductTabs({
3281
- children,
3282
- onKeyDown,
3283
- onSelect,
3284
- products,
3285
- selected
3286
- }) {
3287
- const id = useInstanceId("product-tabs");
3288
- const tabId = slug => `${id}-tab-${slug}`;
3289
- const tabRefs = React.useRef(new Map());
3290
- const handleKeyDown = event => {
3291
- var _a;
3292
- const offset = {
3293
- ArrowLeft: -1,
3294
- ArrowRight: 1
3295
- }[event.key];
3296
- if (offset) {
3297
- event.preventDefault();
3298
- const index = products.indexOf(selected);
3299
- const next = products[(index + offset + products.length) % products.length];
3300
- onSelect(next.slug);
3301
- (_a = tabRefs.current.get(next.slug)) === null || _a === void 0 ? void 0 : _a.focus();
3287
+ // The versions home-mobile pins, so web and mobile read the same response
3288
+ // shapes. Some endpoints only exist from a version on: Registrations' me,
3289
+ // product_search_results, and pcrl_lookups need 2025-05-01.
3290
+ const API_VERSIONS = {
3291
+ calendar: "2022-07-07",
3292
+ "check-ins": "2025-05-28",
3293
+ giving: "2019-10-18",
3294
+ groups: "2023-07-10",
3295
+ home: "2018-08-01",
3296
+ people: "2023-03-21",
3297
+ publishing: "2024-03-25",
3298
+ registrations: "2025-05-01",
3299
+ services: "2018-11-01"
3300
+ };
3301
+ // For a `/~api/{product}/v2/...` URL, the header that pins its product's
3302
+ // version.
3303
+ function apiVersionHeaders(url) {
3304
+ const [,, product] = new URL(url).pathname.split("/");
3305
+ const version = API_VERSIONS[product];
3306
+ if (version) {
3307
+ return {
3308
+ "X-PCO-API-Version": version
3309
+ };
3310
+ } else {
3311
+ return {};
3312
+ }
3313
+ }
3314
+
3315
+ const CACHE_TTL_MS = 60000;
3316
+ const MAX_ENTRIES$1 = 50;
3317
+ const cache = new Map();
3318
+ function cachedRequest(url) {
3319
+ const cached = cache.get(url);
3320
+ if (cached && cached.expiresAt > Date.now()) {
3321
+ // Re-inserting keeps the Map in recently-used order for eviction.
3322
+ cache.delete(url);
3323
+ cache.set(url, cached);
3324
+ return cached.promise;
3325
+ } else {
3326
+ return fetchIntoCache(url);
3327
+ }
3328
+ }
3329
+ function fetchIntoCache(url) {
3330
+ const promise = apiRequest(defaultFetch, url, {
3331
+ headers: apiVersionHeaders(url)
3332
+ }).then(({
3333
+ json,
3334
+ ok
3335
+ }) => {
3336
+ if (!ok || json === null) throw new Error(`Request failed: ${url}`);
3337
+ return json;
3338
+ });
3339
+ // In-flight entries never expire, so every caller shares the one fetch.
3340
+ const entry = {
3341
+ expiresAt: Infinity,
3342
+ promise
3343
+ };
3344
+ cache.set(url, entry);
3345
+ promise.then(() => entry.expiresAt = Date.now() + CACHE_TTL_MS, () => cache.get(url) === entry && cache.delete(url));
3346
+ if (cache.size > MAX_ENTRIES$1) cache.delete(cache.keys().next().value);
3347
+ return promise;
3348
+ }
3349
+ function useCachedRequest(url) {
3350
+ const [settled, setSettled] = React.useState();
3351
+ React.useEffect(() => {
3352
+ if (url) {
3353
+ let current = true;
3354
+ cachedRequest(url).then(data => current && setSettled({
3355
+ data,
3356
+ url
3357
+ }), () => current && setSettled({
3358
+ failed: true,
3359
+ url
3360
+ }));
3361
+ return () => {
3362
+ current = false;
3363
+ };
3302
3364
  } else {
3303
- onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
3365
+ return undefined;
3366
+ }
3367
+ }, [url]);
3368
+ if (!url) {
3369
+ return {
3370
+ status: "idle"
3371
+ };
3372
+ } else if ((settled === null || settled === void 0 ? void 0 : settled.url) !== url) {
3373
+ return {
3374
+ data: settled && "data" in settled ? settled.data : undefined,
3375
+ status: "loading"
3376
+ };
3377
+ } else if ("data" in settled) {
3378
+ return {
3379
+ data: settled.data,
3380
+ status: "done"
3381
+ };
3382
+ } else {
3383
+ return {
3384
+ status: "error"
3385
+ };
3386
+ }
3387
+ }
3388
+ // One request per URL, for callers that fan out to a varying number of
3389
+ // endpoints. Unlike `useCachedRequest`, a URL that hasn't settled yet has no
3390
+ // earlier data to fall back on.
3391
+ function useCachedRequests(urls) {
3392
+ const [settled, setSettled] = React.useState({});
3393
+ // Keyed by value because each settle re-renders the caller, and a caller
3394
+ // that builds a fresh `urls` array on render would otherwise loop forever.
3395
+ const key = urls.join("\n");
3396
+ React.useEffect(() => {
3397
+ let current = true;
3398
+ const settle = result => current && setSettled(prev => ({
3399
+ ...prev,
3400
+ [result.url]: result
3401
+ }));
3402
+ for (const url of key.split("\n").filter(Boolean)) {
3403
+ cachedRequest(url).then(data => settle({
3404
+ data,
3405
+ url
3406
+ }), () => settle({
3407
+ failed: true,
3408
+ url
3409
+ }));
3304
3410
  }
3411
+ return () => {
3412
+ current = false;
3413
+ };
3414
+ }, [key]);
3415
+ return urls.map(url => {
3416
+ const result = settled[url];
3417
+ if (!result) {
3418
+ return {
3419
+ status: "loading"
3420
+ };
3421
+ } else if ("data" in result) {
3422
+ return {
3423
+ data: result.data,
3424
+ status: "done"
3425
+ };
3426
+ } else {
3427
+ return {
3428
+ status: "error"
3429
+ };
3430
+ }
3431
+ });
3432
+ }
3433
+
3434
+ function productSearchUrl(product, query) {
3435
+ const params = new URLSearchParams({
3436
+ per_page: "100",
3437
+ "where[term]": query
3438
+ });
3439
+ return `${window.location.origin}/~api/${product}/v2/product_search_results?${params}`;
3440
+ }
3441
+ function groupByType({
3442
+ data,
3443
+ meta
3444
+ }) {
3445
+ var _a, _b;
3446
+ const typeOrder = (_a = meta === null || meta === void 0 ? void 0 : meta.type_order) !== null && _a !== void 0 ? _a : [];
3447
+ const groups = new Map();
3448
+ for (const result of data) {
3449
+ const {
3450
+ type
3451
+ } = result.attributes;
3452
+ if (!groups.has(type)) groups.set(type, {
3453
+ results: [],
3454
+ type
3455
+ });
3456
+ (_b = groups.get(type)) === null || _b === void 0 ? void 0 : _b.results.push(result);
3457
+ }
3458
+ const rank = type => {
3459
+ const index = typeOrder.indexOf(type);
3460
+ return index === -1 ? typeOrder.length : index;
3305
3461
  };
3306
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
3307
- "aria-label": "Product",
3308
- onKeyDown: handleKeyDown,
3309
- role: "tablist"
3310
- }, products.map(product => (/*#__PURE__*/React.createElement("button", {
3311
- "aria-controls": `${id}-panel`,
3312
- "aria-selected": product === selected,
3313
- className: styles$b.tab,
3314
- id: tabId(product.slug),
3315
- key: product.slug,
3316
- onClick: () => onSelect(product.slug),
3317
- ref: element => {
3318
- if (element) {
3319
- tabRefs.current.set(product.slug, element);
3320
- } else {
3321
- tabRefs.current.delete(product.slug);
3322
- }
3323
- },
3324
- role: "tab",
3325
- tabIndex: product === selected ? 0 : -1,
3326
- type: "button"
3327
- }, product.appName)))), /*#__PURE__*/React.createElement("div", {
3328
- "aria-labelledby": tabId(selected.slug),
3329
- id: `${id}-panel`,
3330
- role: "tabpanel"
3331
- }, children));
3462
+ return [...groups.values()].sort((a, b) => rank(a.type) - rank(b.type));
3463
+ }
3464
+
3465
+ function useDebouncedValue(value, delay) {
3466
+ const [debounced, setDebounced] = React.useState(value);
3467
+ React.useEffect(() => {
3468
+ const timeout = setTimeout(() => setDebounced(value), delay);
3469
+ return () => clearTimeout(timeout);
3470
+ }, [value, delay]);
3471
+ return debounced;
3472
+ }
3473
+
3474
+ const SEARCH_DEBOUNCE_MS = 300;
3475
+ function useProductSearch(product, term) {
3476
+ const query = term.trim();
3477
+ const debouncedQuery = useDebouncedValue(query, SEARCH_DEBOUNCE_MS);
3478
+ const request = useCachedRequest(debouncedQuery ? productSearchUrl(product, debouncedQuery) : null);
3479
+ const response = "data" in request ? request.data : undefined;
3480
+ const settledProduct = useSettledProduct(product, request.status);
3481
+ // While a request loads, the previous response stays on screen, but only
3482
+ // if it was for this product: People results must never show under the
3483
+ // Services tab.
3484
+ const visibleResponse = request.status === "done" || settledProduct === product ? response : undefined;
3485
+ const groups = React.useMemo(() => visibleResponse && groupByType(visibleResponse), [visibleResponse]);
3486
+ if (!query) {
3487
+ return {
3488
+ status: "idle"
3489
+ };
3490
+ } else if (query !== debouncedQuery) {
3491
+ return {
3492
+ groups,
3493
+ status: "loading"
3494
+ };
3495
+ } else if (request.status === "done") {
3496
+ return {
3497
+ groups: groups !== null && groups !== void 0 ? groups : [],
3498
+ status: "done"
3499
+ };
3500
+ } else if (request.status === "error") {
3501
+ return {
3502
+ status: "error"
3503
+ };
3504
+ } else {
3505
+ return {
3506
+ groups,
3507
+ status: "loading"
3508
+ };
3509
+ }
3510
+ }
3511
+ // The product the most recent finished request was for.
3512
+ function useSettledProduct(product, status) {
3513
+ const [settledProduct, setSettledProduct] = React.useState();
3514
+ React.useEffect(() => {
3515
+ if (status === "done") setSettledProduct(product);
3516
+ }, [product, status]);
3517
+ return settledProduct;
3332
3518
  }
3333
3519
 
3334
3520
  function parseValue(element) {
@@ -3397,10 +3583,14 @@ function useRecentlyVisitableMetadata() {
3397
3583
  return shim.useSyncExternalStore(recentlyVisitableMetadataStore.subscribe, () => recentlyVisitableMetadataStore.getState().metadata);
3398
3584
  }
3399
3585
 
3400
- const MAX_ENTRIES$1 = 20;
3401
- const FORMAT_VERSION = 1;
3586
+ // Entries match the ones home-mobile stores, so one list can sync across web
3587
+ // and mobile.
3588
+ const ENTRY_VERSION = 3;
3589
+ const MAX_ENTRIES = 50;
3402
3590
  const MAX_TEXT_LENGTH = 200;
3403
3591
  const PRODUCTS = ["accounts", "calendar", "check-ins", "giving", "groups", "home", "login", "people", "publishing", "registrations", "services"];
3592
+ // Login has no pages worth returning to.
3593
+ const APPS = PRODUCTS.filter(product => product !== "login");
3404
3594
  function isId(value) {
3405
3595
  return value !== "" && Array.from(value).every(character => character >= "0" && character <= "9");
3406
3596
  }
@@ -3409,26 +3599,47 @@ function isPcrl(value) {
3409
3599
  const segments = value.split("/");
3410
3600
  return segments.length === 4 && PRODUCTS.some(product => product === segments[0]) && isId(segments[3]);
3411
3601
  }
3602
+ function isApp(value) {
3603
+ return APPS.some(app => app === value);
3604
+ }
3605
+ // Links are built by resolving the path against its app's origin, so a stored
3606
+ // path must not be able to resolve anywhere else (`//evil.com`, `/\evil.com`).
3607
+ function isPath(value) {
3608
+ const origin = "https://app.invalid";
3609
+ try {
3610
+ return typeof value === "string" && value.startsWith("/") && new URL(value, origin).origin === origin;
3611
+ } catch {
3612
+ return false;
3613
+ }
3614
+ }
3412
3615
  function isNonBlankString(value) {
3413
3616
  return typeof value === "string" && value.trim() !== "";
3414
3617
  }
3618
+ function isOptionalString(value) {
3619
+ return value === undefined || typeof value === "string";
3620
+ }
3415
3621
  function isRecord(value) {
3416
3622
  return typeof value === "object" && value !== null && !Array.isArray(value);
3417
3623
  }
3624
+ function isPageMetadata(value) {
3625
+ return value === undefined || isRecord(value) && isOptionalString(value.details) && isOptionalString(value.type);
3626
+ }
3627
+ // The query string is part of the link but not of the page's identity, so
3628
+ // revisiting with other filters moves the existing entry instead of adding one.
3418
3629
  function entryKey(entry) {
3419
3630
  if (entry.type === "resource") {
3420
3631
  return `resource:${entry.pcrl}`;
3421
3632
  } else {
3422
- return `page:${entry.product}:${entry.path}`;
3633
+ return `page:${entry.app}:${entry.path.split("?")[0]}`;
3423
3634
  }
3424
3635
  }
3425
3636
  function isEntry(value) {
3426
- if (!isRecord(value)) {
3637
+ if (!isRecord(value) || value.version !== ENTRY_VERSION || typeof value.visitedAt !== "string") {
3427
3638
  return false;
3428
3639
  } else if (value.type === "resource") {
3429
3640
  return typeof value.pcrl === "string" && isPcrl(value.pcrl);
3430
3641
  } else if (value.type === "page") {
3431
- return isNonBlankString(value.product) && isNonBlankString(value.title) && isNonBlankString(value.path) && typeof value.details === "string";
3642
+ return isApp(value.app) && isPath(value.path) && isNonBlankString(value.title) && isPageMetadata(value.metadata);
3432
3643
  } else {
3433
3644
  return false;
3434
3645
  }
@@ -3437,58 +3648,114 @@ function isEntry(value) {
3437
3648
  function clampText(value) {
3438
3649
  return Array.from(value).slice(0, MAX_TEXT_LENGTH).join("");
3439
3650
  }
3440
- function clampEntry(entry) {
3441
- if (entry.type === "resource") {
3442
- return entry;
3651
+ function clampMetadata({
3652
+ details,
3653
+ type
3654
+ }) {
3655
+ return {
3656
+ ...(details && {
3657
+ details: clampText(details)
3658
+ }),
3659
+ ...(type && {
3660
+ type: clampText(type)
3661
+ })
3662
+ };
3663
+ }
3664
+ function clampVisit(visit) {
3665
+ if (visit.type === "resource") {
3666
+ return visit;
3443
3667
  } else {
3444
3668
  return {
3445
- ...entry,
3446
- details: clampText(entry.details),
3447
- title: clampText(entry.title)
3669
+ ...visit,
3670
+ ...(visit.metadata && {
3671
+ metadata: clampMetadata(visit.metadata)
3672
+ }),
3673
+ title: clampText(visit.title.trim())
3448
3674
  };
3449
3675
  }
3450
3676
  }
3677
+ function toEntry(visit, visitedAt) {
3678
+ return {
3679
+ ...clampVisit(visit),
3680
+ version: ENTRY_VERSION,
3681
+ visitedAt: visitedAt.toISOString()
3682
+ };
3683
+ }
3451
3684
  // Recents are disposable: anything unexpected reads as an empty list (or, for
3452
3685
  // a single bad entry, a list without it) rather than throwing.
3453
- function parseRecentlyVisited(value, personId) {
3686
+ function parseRecentlyVisited(value) {
3454
3687
  let parsed = null;
3455
3688
  try {
3456
3689
  parsed = JSON.parse(value !== null && value !== void 0 ? value : "null");
3457
3690
  } catch {
3458
3691
  parsed = null;
3459
3692
  }
3460
- if (isRecord(parsed) && parsed.v === FORMAT_VERSION && parsed.person === personId && Array.isArray(parsed.entries)) {
3461
- return parsed.entries.filter(isEntry);
3693
+ if (Array.isArray(parsed)) {
3694
+ return parsed.filter(isEntry);
3462
3695
  } else {
3463
3696
  return [];
3464
3697
  }
3465
3698
  }
3466
- function addVisit(entries, visit) {
3467
- const key = entryKey(visit);
3468
- return [clampEntry(visit), ...entries.filter(entry => entryKey(entry) !== key)].slice(0, MAX_ENTRIES$1);
3469
- }
3470
- function serializeRecentlyVisited(entries, personId) {
3471
- return JSON.stringify({
3472
- entries,
3473
- person: personId,
3474
- v: FORMAT_VERSION
3475
- });
3699
+ function addVisit(entries, entry) {
3700
+ const key = entryKey(entry);
3701
+ return [entry, ...entries.filter(existing => entryKey(existing) !== key)].slice(0, MAX_ENTRIES);
3476
3702
  }
3477
3703
 
3478
- // The product whose URL, in this environment, has the current page's host.
3704
+ // The app whose URL, in this environment, has the current page's host.
3479
3705
  // Reversing pcoUrl handles every environment @planningcenter/url knows about,
3480
- // and guarantees the stored product rebuilds a working link.
3481
- function currentProduct() {
3706
+ // and guarantees the stored app rebuilds a working link.
3707
+ function currentApp() {
3482
3708
  var _a;
3483
- return (_a = PRODUCTS.find(product => new URL(pcoUrl$1.pcoUrl(product)).hostname === window.location.hostname)) !== null && _a !== void 0 ? _a : null;
3709
+ return (_a = APPS.find(app => new URL(pcoUrl$1.pcoUrl(app)).hostname === window.location.hostname)) !== null && _a !== void 0 ? _a : null;
3484
3710
  }
3485
- function detailsFromMetadata(metadata) {
3711
+ function isFilled(value) {
3712
+ return typeof value === "string" && value !== "";
3713
+ }
3714
+ // The tag's metadata is product-supplied JSON; only its string `type` and
3715
+ // `details` carry over, and an entry without either has no metadata at all.
3716
+ function metadataFromPage(metadata) {
3486
3717
  var _a;
3487
3718
  try {
3488
- const details = (_a = JSON.parse(metadata !== null && metadata !== void 0 ? metadata : "{}")) === null || _a === void 0 ? void 0 : _a.details;
3489
- return typeof details === "string" ? details : "";
3719
+ const {
3720
+ details,
3721
+ type
3722
+ } = (_a = JSON.parse(metadata !== null && metadata !== void 0 ? metadata : "{}")) !== null && _a !== void 0 ? _a : {};
3723
+ if (isFilled(details) || isFilled(type)) {
3724
+ return {
3725
+ ...(isFilled(details) && {
3726
+ details
3727
+ }),
3728
+ ...(isFilled(type) && {
3729
+ type
3730
+ })
3731
+ };
3732
+ } else {
3733
+ return undefined;
3734
+ }
3490
3735
  } catch {
3491
- return "";
3736
+ return undefined;
3737
+ }
3738
+ }
3739
+ // Staff impersonating someone browse under `/O<org id>/P<person id>/…`.
3740
+ function withoutImpersonation(segments) {
3741
+ const [org = "", person = ""] = segments;
3742
+ if (org.startsWith("O") && isId(org.slice(1)) && person.startsWith("P") && isId(person.slice(1))) {
3743
+ return segments.slice(2);
3744
+ } else {
3745
+ return segments;
3746
+ }
3747
+ }
3748
+ // Normalized the way home-mobile does it, so a page visited on web and on
3749
+ // mobile is one entry once the list syncs.
3750
+ function pathFromLocation({
3751
+ pathname,
3752
+ search
3753
+ }) {
3754
+ const segments = withoutImpersonation(pathname.split("/").filter(Boolean));
3755
+ if (segments.length === 0) {
3756
+ return "/";
3757
+ } else {
3758
+ return `/${segments.join("/")}${search.length > 1 ? search : ""}`;
3492
3759
  }
3493
3760
  }
3494
3761
  // pco:resource is the more specific signal, so it wins when both tags are
@@ -3498,17 +3765,20 @@ function visitFromPage({
3498
3765
  resource,
3499
3766
  title
3500
3767
  }) {
3501
- const product = currentProduct();
3768
+ const app = currentApp();
3502
3769
  if (resource && isPcrl(resource)) {
3503
3770
  return {
3504
3771
  pcrl: resource,
3505
3772
  type: "resource"
3506
3773
  };
3507
- } else if (title && title.trim() !== "" && product) {
3774
+ } else if (title && title.trim() !== "" && app) {
3775
+ const pageMetadata = metadataFromPage(metadata);
3508
3776
  return {
3509
- details: detailsFromMetadata(metadata),
3510
- path: window.location.pathname,
3511
- product,
3777
+ app,
3778
+ ...(pageMetadata && {
3779
+ metadata: pageMetadata
3780
+ }),
3781
+ path: pathFromLocation(window.location),
3512
3782
  title,
3513
3783
  type: "page"
3514
3784
  };
@@ -3556,15 +3826,16 @@ const changes = createStore({
3556
3826
  });
3557
3827
  function readRecentlyVisited(scope) {
3558
3828
  try {
3559
- return parseRecentlyVisited(localStorage.getItem(storageKey(scope)), scope.personId);
3829
+ return parseRecentlyVisited(localStorage.getItem(storageKey(scope)));
3560
3830
  } catch {
3561
3831
  return [];
3562
3832
  }
3563
3833
  }
3564
3834
  function recordVisit(visit, scope) {
3565
- if (isEntry(visit)) {
3835
+ const entry = toEntry(visit, new Date());
3836
+ if (isEntry(entry)) {
3566
3837
  try {
3567
- localStorage.setItem(storageKey(scope), serializeRecentlyVisited(addVisit(readRecentlyVisited(scope), visit), scope.personId));
3838
+ localStorage.setItem(storageKey(scope), JSON.stringify(addVisit(readRecentlyVisited(scope), entry)));
3568
3839
  changes.setState({
3569
3840
  version: changes.getState().version + 1
3570
3841
  });
@@ -3585,153 +3856,6 @@ function useRecentlyVisited({
3585
3856
  }) : [], [env, personId, version]);
3586
3857
  }
3587
3858
 
3588
- // The versions home-mobile pins, so web and mobile read the same response
3589
- // shapes. Some endpoints only exist from a version on: Registrations' me,
3590
- // product_search_results, and pcrl_lookups need 2025-05-01.
3591
- const API_VERSIONS = {
3592
- calendar: "2022-07-07",
3593
- "check-ins": "2025-05-28",
3594
- giving: "2019-10-18",
3595
- groups: "2023-07-10",
3596
- home: "2018-08-01",
3597
- people: "2023-03-21",
3598
- publishing: "2024-03-25",
3599
- registrations: "2025-05-01",
3600
- services: "2018-11-01"
3601
- };
3602
- // For a `/~api/{product}/v2/...` URL, the header that pins its product's
3603
- // version.
3604
- function apiVersionHeaders(url) {
3605
- const [,, product] = new URL(url).pathname.split("/");
3606
- const version = API_VERSIONS[product];
3607
- if (version) {
3608
- return {
3609
- "X-PCO-API-Version": version
3610
- };
3611
- } else {
3612
- return {};
3613
- }
3614
- }
3615
-
3616
- const CACHE_TTL_MS = 60000;
3617
- const MAX_ENTRIES = 50;
3618
- const cache = new Map();
3619
- function cachedRequest(url) {
3620
- const cached = cache.get(url);
3621
- if (cached && cached.expiresAt > Date.now()) {
3622
- // Re-inserting keeps the Map in recently-used order for eviction.
3623
- cache.delete(url);
3624
- cache.set(url, cached);
3625
- return cached.promise;
3626
- } else {
3627
- return fetchIntoCache(url);
3628
- }
3629
- }
3630
- function fetchIntoCache(url) {
3631
- const promise = apiRequest(defaultFetch, url, {
3632
- headers: apiVersionHeaders(url)
3633
- }).then(({
3634
- json,
3635
- ok
3636
- }) => {
3637
- if (!ok || json === null) throw new Error(`Request failed: ${url}`);
3638
- return json;
3639
- });
3640
- // In-flight entries never expire, so every caller shares the one fetch.
3641
- const entry = {
3642
- expiresAt: Infinity,
3643
- promise
3644
- };
3645
- cache.set(url, entry);
3646
- promise.then(() => entry.expiresAt = Date.now() + CACHE_TTL_MS, () => cache.get(url) === entry && cache.delete(url));
3647
- if (cache.size > MAX_ENTRIES) cache.delete(cache.keys().next().value);
3648
- return promise;
3649
- }
3650
- function useCachedRequest(url) {
3651
- const [settled, setSettled] = React.useState();
3652
- React.useEffect(() => {
3653
- if (url) {
3654
- let current = true;
3655
- cachedRequest(url).then(data => current && setSettled({
3656
- data,
3657
- url
3658
- }), () => current && setSettled({
3659
- failed: true,
3660
- url
3661
- }));
3662
- return () => {
3663
- current = false;
3664
- };
3665
- } else {
3666
- return undefined;
3667
- }
3668
- }, [url]);
3669
- if (!url) {
3670
- return {
3671
- status: "idle"
3672
- };
3673
- } else if ((settled === null || settled === void 0 ? void 0 : settled.url) !== url) {
3674
- return {
3675
- data: settled && "data" in settled ? settled.data : undefined,
3676
- status: "loading"
3677
- };
3678
- } else if ("data" in settled) {
3679
- return {
3680
- data: settled.data,
3681
- status: "done"
3682
- };
3683
- } else {
3684
- return {
3685
- status: "error"
3686
- };
3687
- }
3688
- }
3689
- // One request per URL, for callers that fan out to a varying number of
3690
- // endpoints. Unlike `useCachedRequest`, a URL that hasn't settled yet has no
3691
- // earlier data to fall back on.
3692
- function useCachedRequests(urls) {
3693
- const [settled, setSettled] = React.useState({});
3694
- // Keyed by value because each settle re-renders the caller, and a caller
3695
- // that builds a fresh `urls` array on render would otherwise loop forever.
3696
- const key = urls.join("\n");
3697
- React.useEffect(() => {
3698
- let current = true;
3699
- const settle = result => current && setSettled(prev => ({
3700
- ...prev,
3701
- [result.url]: result
3702
- }));
3703
- for (const url of key.split("\n").filter(Boolean)) {
3704
- cachedRequest(url).then(data => settle({
3705
- data,
3706
- url
3707
- }), () => settle({
3708
- failed: true,
3709
- url
3710
- }));
3711
- }
3712
- return () => {
3713
- current = false;
3714
- };
3715
- }, [key]);
3716
- return urls.map(url => {
3717
- const result = settled[url];
3718
- if (!result) {
3719
- return {
3720
- status: "loading"
3721
- };
3722
- } else if ("data" in result) {
3723
- return {
3724
- data: result.data,
3725
- status: "done"
3726
- };
3727
- } else {
3728
- return {
3729
- status: "error"
3730
- };
3731
- }
3732
- });
3733
- }
3734
-
3735
3859
  const pcoUrl = pcoUrl$1;
3736
3860
 
3737
3861
  // Resolves resource entries with one pcrl_lookups request per product.
@@ -3822,13 +3946,290 @@ function pcrlLookupsUrl(product, pcrls) {
3822
3946
  });
3823
3947
  return `${window.location.origin}/~api/${product}/v2/pcrl_lookups?${params}`;
3824
3948
  }
3825
- // Built from the product rather than the current origin, so it stays right
3826
- // once PCA-409 shares one list across product subdomains.
3949
+ // Built from the app rather than the current origin, so it stays right once
3950
+ // PCA-409 shares one list across product subdomains.
3827
3951
  function pageUrl(entry, env) {
3828
- return `${pcoUrl(env)(entry.product)}${entry.path}`;
3952
+ return new URL(entry.path, pcoUrl(env)(entry.app)).href;
3953
+ }
3954
+
3955
+ function useRecentVisits(env) {
3956
+ const personId = useCurrentPersonId();
3957
+ const entries = useRecentlyVisited({
3958
+ env,
3959
+ personId
3960
+ });
3961
+ const [count, setCount] = React.useState(RECENTLY_VISITED_PAGE_SIZE);
3962
+ const shown = React.useMemo(() => entries.slice(0, count), [entries, count]);
3963
+ const hydrated = useHydratedRecentlyVisited(shown, env);
3964
+ // Resources show up once they're found. Ones that can't be found anymore,
3965
+ // or that the person can no longer see, are left out.
3966
+ const visits = hydrated.filter(isShown);
3967
+ const loading = hydrated.some(({
3968
+ status
3969
+ }) => status === "loading");
3970
+ const showMore = React.useCallback(() => setCount(current => current + RECENTLY_VISITED_PAGE_SIZE), []);
3971
+ return {
3972
+ count,
3973
+ hasMore: entries.length > count,
3974
+ loading,
3975
+ showMore,
3976
+ visits
3977
+ };
3978
+ }
3979
+ function isShown(visit) {
3980
+ return visit.status === "page" || visit.status === "resolved";
3981
+ }
3982
+
3983
+ function open(url, {
3984
+ newTab
3985
+ }) {
3986
+ if (newTab) {
3987
+ window.open(url, "_blank", "noopener");
3988
+ } else {
3989
+ window.location.assign(url);
3990
+ }
3991
+ }
3992
+ // With text selected, ⌘C should copy that text as usual, not the link.
3993
+ function hasSelectedText(element) {
3994
+ var _a, _b;
3995
+ const selectedInField = element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
3996
+ return selectedInField || ((_b = (_a = window.getSelection()) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : "") !== "";
3997
+ }
3998
+
3999
+ const COPIED_FEEDBACK_MS = 1500;
4000
+ // `copiedUrl` stays set briefly after a copy so the UI can confirm it.
4001
+ function useCopyLink() {
4002
+ const [copiedUrl, setCopiedUrl] = React.useState(null);
4003
+ React.useEffect(() => {
4004
+ if (copiedUrl) {
4005
+ const timeout = setTimeout(() => setCopiedUrl(null), COPIED_FEEDBACK_MS);
4006
+ return () => clearTimeout(timeout);
4007
+ } else {
4008
+ return undefined;
4009
+ }
4010
+ }, [copiedUrl]);
4011
+ const copy = React.useCallback(url => {
4012
+ navigator.clipboard.writeText(url).then(() => setCopiedUrl(url));
4013
+ }, []);
4014
+ return {
4015
+ copiedUrl,
4016
+ copy
4017
+ };
4018
+ }
4019
+
4020
+ // Remembering which list the index belongs to puts the highlight back on the
4021
+ // first item whenever a new list replaces it, without an effect to reset it.
4022
+ // A list that grows or fills in under the same `list` keeps its highlight.
4023
+ function useResultNavigation(items, list) {
4024
+ const [highlight, setHighlight] = React.useState({
4025
+ index: 0,
4026
+ list
4027
+ });
4028
+ const highlightedIndex = highlight.list === list ? highlight.index : 0;
4029
+ const highlighted = items[highlightedIndex];
4030
+ const {
4031
+ copiedUrl,
4032
+ copy
4033
+ } = useCopyLink();
4034
+ const highlightIndex = React.useCallback(index => setHighlight(current => current.list === list && current.index === index ? current : {
4035
+ index,
4036
+ list
4037
+ }), [list]);
4038
+ const handleKeyDown = event => {
4039
+ const step = verticalStep(event.key);
4040
+ if (step && items.length > 0) {
4041
+ event.preventDefault();
4042
+ highlightIndex((highlightedIndex + step + items.length) % items.length);
4043
+ } else if (event.key === "Enter" && highlighted && !event.nativeEvent.isComposing) {
4044
+ event.preventDefault();
4045
+ open(highlighted.url, {
4046
+ newTab: hasCommandKey(event)
4047
+ });
4048
+ } else if (isCopyShortcut(event) && highlighted && !hasSelectedText(event.currentTarget)) {
4049
+ event.preventDefault();
4050
+ copy(highlighted.url);
4051
+ }
4052
+ };
4053
+ return {
4054
+ copied: copiedUrl !== null && copiedUrl === (highlighted === null || highlighted === void 0 ? void 0 : highlighted.url),
4055
+ handleKeyDown,
4056
+ highlightIndex,
4057
+ highlighted,
4058
+ highlightedIndex: highlighted ? highlightedIndex : null
4059
+ };
4060
+ }
4061
+
4062
+ function groupSections(groups = []) {
4063
+ return groups.map(({
4064
+ results,
4065
+ type
4066
+ }) => ({
4067
+ heading: type,
4068
+ items: results.map(resultItem),
4069
+ key: type
4070
+ }));
4071
+ }
4072
+ function resultItem({
4073
+ attributes,
4074
+ id
4075
+ }) {
4076
+ return {
4077
+ key: id,
4078
+ subtitle: summarize(attributes.metadata),
4079
+ title: attributes.title,
4080
+ url: attributes.url
4081
+ };
4082
+ }
4083
+ function visitItem(visit) {
4084
+ return {
4085
+ key: entryKey(visit.entry),
4086
+ ...describeVisit(visit)
4087
+ };
4088
+ }
4089
+ function summarize(metadata) {
4090
+ return Object.values(metadata !== null && metadata !== void 0 ? metadata : {}).filter(value => typeof value === "string" && value !== "").join(" · ");
4091
+ }
4092
+ // Unlike search results, recents come from every product, so each one names
4093
+ // its product and type.
4094
+ function describeVisit(visit) {
4095
+ var _a, _b;
4096
+ const subtitle = (...parts) => parts.filter(Boolean).join(" · ");
4097
+ if (visit.status === "page") {
4098
+ const {
4099
+ app,
4100
+ metadata,
4101
+ title
4102
+ } = visit.entry;
4103
+ return {
4104
+ 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 : ""),
4105
+ title,
4106
+ url: visit.url
4107
+ };
4108
+ } else {
4109
+ const {
4110
+ metadata,
4111
+ product,
4112
+ title,
4113
+ type
4114
+ } = visit.result.attributes;
4115
+ return {
4116
+ subtitle: subtitle(productName(product), type, summarize(metadata)),
4117
+ title,
4118
+ url: visit.url
4119
+ };
4120
+ }
4121
+ }
4122
+ // check-ins → Check-Ins
4123
+ function productName(slug) {
4124
+ return slug.split("-").map(capitalize).join("-");
4125
+ }
4126
+ function capitalize(word) {
4127
+ return word.charAt(0).toUpperCase() + word.slice(1);
4128
+ }
4129
+
4130
+ const STORAGE_KEY = "topbar.product_search.last_product";
4131
+ function readLastProduct() {
4132
+ try {
4133
+ return localStorage.getItem(STORAGE_KEY);
4134
+ } catch {
4135
+ return null;
4136
+ }
4137
+ }
4138
+ function saveLastProduct(slug) {
4139
+ try {
4140
+ localStorage.setItem(STORAGE_KEY, slug);
4141
+ } catch {
4142
+ // Storage can be full or blocked; search still works without it.
4143
+ }
4144
+ }
4145
+ function initialProduct(products) {
4146
+ var _a, _b, _c;
4147
+ const lastProduct = readLastProduct();
4148
+ const bySlug = slug => products.find(product => product.slug === slug);
4149
+ 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];
3829
4150
  }
3830
4151
 
3831
- var styles$a = {"result":"result-WCnxr"};
4152
+ function KeyboardHints({
4153
+ copied
4154
+ }) {
4155
+ const command = commandKeyLabel();
4156
+ return /*#__PURE__*/React.createElement("p", null, "\u2191\u2193 Select \u00B7 \u21B5 Open \u00B7 ", command, "\u21B5 Open in new tab \u00B7 ", command, "C", " ", /*#__PURE__*/React.createElement("span", {
4157
+ role: "status"
4158
+ }, copied ? "Copied!" : "Copy link"), " \u00B7 ", command, "K Open search");
4159
+ }
4160
+
4161
+ var styles$d = {"tab":"tab-dz8ki"};
4162
+
4163
+ function ProductTabs({
4164
+ children,
4165
+ onKeyDown,
4166
+ onSelect,
4167
+ products,
4168
+ selected
4169
+ }) {
4170
+ const id = useInstanceId("product-tabs");
4171
+ const tabId = slug => `${id}-tab-${slug}`;
4172
+ const tabRefs = React.useRef(new Map());
4173
+ const handleKeyDown = event => {
4174
+ var _a;
4175
+ const offset = horizontalStep(event.key);
4176
+ if (offset) {
4177
+ event.preventDefault();
4178
+ const index = products.indexOf(selected);
4179
+ const next = products[(index + offset + products.length) % products.length];
4180
+ onSelect(next.slug);
4181
+ (_a = tabRefs.current.get(next.slug)) === null || _a === void 0 ? void 0 : _a.focus();
4182
+ } else {
4183
+ onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
4184
+ }
4185
+ };
4186
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
4187
+ "aria-label": "Product",
4188
+ onKeyDown: handleKeyDown,
4189
+ role: "tablist"
4190
+ }, products.map(product => (/*#__PURE__*/React.createElement("button", {
4191
+ "aria-controls": `${id}-panel`,
4192
+ "aria-selected": product === selected,
4193
+ className: styles$d.tab,
4194
+ id: tabId(product.slug),
4195
+ key: product.slug,
4196
+ onClick: () => onSelect(product.slug),
4197
+ ref: element => {
4198
+ if (element) {
4199
+ tabRefs.current.set(product.slug, element);
4200
+ } else {
4201
+ tabRefs.current.delete(product.slug);
4202
+ }
4203
+ },
4204
+ role: "tab",
4205
+ tabIndex: product === selected ? 0 : -1,
4206
+ type: "button"
4207
+ }, product.appName)))), /*#__PURE__*/React.createElement("div", {
4208
+ "aria-labelledby": tabId(selected.slug),
4209
+ id: `${id}-panel`,
4210
+ role: "tabpanel"
4211
+ }, children));
4212
+ }
4213
+
4214
+ function EndOfResultsObserver({
4215
+ onVisible
4216
+ }) {
4217
+ const ref = React.useRef(null);
4218
+ React.useEffect(() => {
4219
+ const observer = new IntersectionObserver(([entry]) => {
4220
+ if (entry.isIntersecting) onVisible();
4221
+ });
4222
+ if (ref.current) observer.observe(ref.current);
4223
+ return () => observer.disconnect();
4224
+ }, [onVisible]);
4225
+ return /*#__PURE__*/React.createElement("div", {
4226
+ ref: ref
4227
+ });
4228
+ }
4229
+
4230
+ const resultId = (listboxId, index) => `${listboxId}-result-${index}`;
4231
+
4232
+ var styles$c = {"result":"result-Thq2C"};
3832
4233
 
3833
4234
  function SearchResult({
3834
4235
  highlighted,
@@ -3851,7 +4252,7 @@ function SearchResult({
3851
4252
  // pointer, and that shouldn't steal the highlight.
3852
4253
  return /*#__PURE__*/React.createElement("a", {
3853
4254
  "aria-selected": highlighted,
3854
- className: styles$a.result,
4255
+ className: styles$c.result,
3855
4256
  href: url,
3856
4257
  id: id,
3857
4258
  onMouseMove: onHighlight,
@@ -3861,21 +4262,129 @@ function SearchResult({
3861
4262
  }, /*#__PURE__*/React.createElement("div", null, title), /*#__PURE__*/React.createElement("div", null, subtitle));
3862
4263
  }
3863
4264
 
3864
- const resultId = (listboxId, index) => `${listboxId}-result-${index}`;
4265
+ // Sections with a heading render as labelled groups; ones without render
4266
+ // their items straight into the listbox. Indexes run across every section.
4267
+ function ResultListbox({
4268
+ busy,
4269
+ highlightedIndex,
4270
+ id,
4271
+ label,
4272
+ labelledBy,
4273
+ onHighlight,
4274
+ sections
4275
+ }) {
4276
+ let index = 0;
4277
+ const renderItem = item => {
4278
+ const itemIndex = index++;
4279
+ return /*#__PURE__*/React.createElement(SearchResult, {
4280
+ highlighted: itemIndex === highlightedIndex,
4281
+ id: resultId(id, itemIndex),
4282
+ key: item.key,
4283
+ onHighlight: () => onHighlight(itemIndex),
4284
+ subtitle: item.subtitle,
4285
+ title: item.title,
4286
+ url: item.url
4287
+ });
4288
+ };
4289
+ return /*#__PURE__*/React.createElement("div", {
4290
+ "aria-busy": busy,
4291
+ "aria-label": label,
4292
+ "aria-labelledby": labelledBy,
4293
+ id: id,
4294
+ role: "listbox"
4295
+ }, sections.map((section, sectionIndex) => {
4296
+ if (section.heading === undefined) {
4297
+ return /*#__PURE__*/React.createElement(React.Fragment, {
4298
+ key: section.key
4299
+ }, section.items.map(renderItem));
4300
+ }
4301
+ const headingId = `${id}-group-${sectionIndex}`;
4302
+ return /*#__PURE__*/React.createElement("div", {
4303
+ "aria-labelledby": headingId,
4304
+ key: section.key,
4305
+ role: "group"
4306
+ }, /*#__PURE__*/React.createElement("div", {
4307
+ id: headingId
4308
+ }, section.heading), section.items.map(renderItem));
4309
+ }));
4310
+ }
4311
+
4312
+ function RecentlyVisited({
4313
+ count,
4314
+ hasMore,
4315
+ items,
4316
+ loading,
4317
+ showMore,
4318
+ ...navigation
4319
+ }) {
4320
+ const headingId = `${navigation.id}-heading`;
4321
+ if (items.length === 0 && !hasMore && !loading) {
4322
+ return /*#__PURE__*/React.createElement("p", null, "Pages you visit will show up here.");
4323
+ } else {
4324
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
4325
+ id: headingId
4326
+ }, "Recently visited"), /*#__PURE__*/React.createElement(ResultListbox, {
4327
+ labelledBy: headingId,
4328
+ sections: [{
4329
+ items,
4330
+ key: "recents"
4331
+ }],
4332
+ ...navigation
4333
+ }), hasMore && /*#__PURE__*/React.createElement(EndOfResultsObserver, {
4334
+ key: count,
4335
+ onVisible: showMore
4336
+ }));
4337
+ }
4338
+ }
4339
+
4340
+ // A combobox: focus stays here while aria-activedescendant points screen
4341
+ // readers at the highlighted result in the listbox it controls.
4342
+ function SearchInput({
4343
+ expanded,
4344
+ highlightedIndex,
4345
+ inputRef,
4346
+ listboxId,
4347
+ onChange,
4348
+ onFocus,
4349
+ onKeyDown,
4350
+ value
4351
+ }) {
4352
+ return /*#__PURE__*/React.createElement("input", {
4353
+ "aria-activedescendant": highlightedIndex === null ? undefined : resultId(listboxId, highlightedIndex),
4354
+ "aria-autocomplete": "list",
4355
+ "aria-controls": expanded ? listboxId : undefined,
4356
+ "aria-expanded": expanded,
4357
+ "aria-label": "Search",
4358
+ autoFocus: true,
4359
+ onChange: event => onChange(event.target.value),
4360
+ onFocus: onFocus,
4361
+ onKeyDown: onKeyDown,
4362
+ placeholder: "Search",
4363
+ ref: inputRef,
4364
+ role: "combobox",
4365
+ type: "search",
4366
+ value: value
4367
+ });
4368
+ }
4369
+
4370
+ var styles$b = {"visuallyHidden":"visuallyHidden-S9VG5"};
4371
+
3865
4372
  function SearchResults({
3866
- search,
4373
+ label,
4374
+ sections,
4375
+ status,
3867
4376
  term,
3868
4377
  ...navigation
3869
4378
  }) {
3870
- var _a;
3871
- switch (search.status) {
4379
+ switch (status) {
3872
4380
  case "idle":
3873
4381
  return null;
3874
4382
  case "loading":
3875
- if ((_a = search.groups) === null || _a === void 0 ? void 0 : _a.length) {
3876
- return /*#__PURE__*/React.createElement(ResultGroups, {
4383
+ if (sections.length > 0) {
4384
+ return /*#__PURE__*/React.createElement(ResultListbox, {
3877
4385
  busy: true,
3878
- groups: search.groups,
4386
+ label: label,
4387
+ sections: sections,
3879
4388
  ...navigation
3880
4389
  });
3881
4390
  }
@@ -3887,324 +4396,167 @@ function SearchResults({
3887
4396
  role: "alert"
3888
4397
  }, "Search isn\u2019t working right now. Try again.");
3889
4398
  case "done":
3890
- if (search.groups.length === 0) {
4399
+ if (sections.length === 0) {
3891
4400
  return /*#__PURE__*/React.createElement("p", {
3892
4401
  role: "status"
3893
4402
  }, "No results for \u201C", term.trim(), "\u201D");
3894
4403
  }
3895
- return /*#__PURE__*/React.createElement(ResultGroups, {
3896
- groups: search.groups,
4404
+ return /*#__PURE__*/React.createElement(ResultListbox, {
4405
+ busy: false,
4406
+ label: label,
4407
+ sections: sections,
3897
4408
  ...navigation
3898
4409
  });
3899
4410
  }
3900
4411
  }
3901
- function ResultGroups({
3902
- busy = false,
3903
- groups,
3904
- highlightedIndex,
3905
- id,
3906
- label,
3907
- onHighlight
3908
- }) {
3909
- let index = 0;
3910
- return /*#__PURE__*/React.createElement("div", {
3911
- "aria-busy": busy,
3912
- "aria-label": label,
3913
- id: id,
3914
- role: "listbox"
3915
- }, groups.map(({
3916
- results,
3917
- type
3918
- }, groupIndex) => {
3919
- const headingId = `${id}-group-${groupIndex}`;
3920
- return /*#__PURE__*/React.createElement("div", {
3921
- "aria-labelledby": headingId,
3922
- key: type,
3923
- role: "group"
3924
- }, /*#__PURE__*/React.createElement("div", {
3925
- id: headingId
3926
- }, type), results.map(result => {
3927
- const resultIndex = index++;
3928
- return /*#__PURE__*/React.createElement(SearchResult, {
3929
- highlighted: resultIndex === highlightedIndex,
3930
- id: resultId(id, resultIndex),
3931
- key: result.id,
3932
- onHighlight: () => onHighlight(resultIndex),
3933
- subtitle: summarize(result.attributes.metadata),
3934
- title: result.attributes.title,
3935
- url: result.attributes.url
3936
- });
3937
- }));
3938
- }));
3939
- }
3940
- function summarize(metadata) {
3941
- return Object.values(metadata !== null && metadata !== void 0 ? metadata : {}).filter(value => typeof value === "string" && value !== "").join(" · ");
3942
- }
3943
4412
 
3944
- function useRecentVisits(env) {
3945
- const personId = useCurrentPersonId();
3946
- const entries = useRecentlyVisited({
3947
- env,
3948
- personId
3949
- });
3950
- const [count, setCount] = React.useState(RECENTLY_VISITED_PAGE_SIZE);
3951
- const shown = React.useMemo(() => entries.slice(0, count), [entries, count]);
3952
- const hydrated = useHydratedRecentlyVisited(shown, env);
3953
- // Resources show up once they're found. Ones that can't be found anymore,
3954
- // or that the person can no longer see, are left out.
3955
- const visits = hydrated.filter(isShown);
3956
- const loading = hydrated.some(({
3957
- status
3958
- }) => status === "loading");
3959
- const showMore = React.useCallback(() => setCount(current => current + RECENTLY_VISITED_PAGE_SIZE), []);
3960
- return {
3961
- count,
3962
- hasMore: entries.length > count,
3963
- loading,
3964
- showMore,
3965
- visits
3966
- };
3967
- }
3968
- function isShown(visit) {
3969
- return visit.status === "page" || visit.status === "resolved";
3970
- }
3971
- function RecentlyVisited({
3972
- count,
3973
- hasMore,
3974
- highlightedIndex,
3975
- id,
3976
- loading,
3977
- onHighlight,
3978
- showMore,
3979
- visits
3980
- }) {
3981
- const headingId = `${id}-heading`;
3982
- if (visits.length === 0 && !hasMore && !loading) {
3983
- return /*#__PURE__*/React.createElement("p", null, "Pages you visit will show up here.");
3984
- } else {
3985
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
3986
- id: headingId
3987
- }, "Recently visited"), /*#__PURE__*/React.createElement("div", {
3988
- "aria-labelledby": headingId,
3989
- id: id,
3990
- role: "listbox"
3991
- }, visits.map((visit, index) => (/*#__PURE__*/React.createElement(SearchResult, {
3992
- highlighted: index === highlightedIndex,
3993
- id: resultId(id, index),
3994
- key: entryKey(visit.entry),
3995
- onHighlight: () => onHighlight(index),
3996
- ...describeVisit(visit)
3997
- })))), hasMore && /*#__PURE__*/React.createElement(ShowMoreWhenVisible, {
3998
- key: count,
3999
- onVisible: showMore
4000
- }));
4001
- }
4002
- }
4003
- function ShowMoreWhenVisible({
4004
- onVisible
4413
+ // Keys the recents list for navigation, so the highlight
4414
+ // stays put as more recents load and as their lookups answer.
4415
+ const RECENTS = "recents";
4416
+ function SearchPanel({
4417
+ env,
4418
+ products
4005
4419
  }) {
4006
- const ref = React.useRef(null);
4420
+ var _a, _b;
4421
+ const listboxId = useInstanceId("product-search-results");
4422
+ const [term, setTerm] = React.useState("");
4423
+ const [selectedSlug, setSelectedSlug] = React.useState(() => initialProduct(products).slug);
4424
+ const product = (_a = products.find(({
4425
+ slug
4426
+ }) => slug === selectedSlug)) !== null && _a !== void 0 ? _a : products[0];
4427
+ const hasTerm = term.trim() !== "";
4428
+ const search = useProductSearch(product.slug, term);
4429
+ const groups = "groups" in search ? search.groups : undefined;
4430
+ const sections = React.useMemo(() => groupSections(groups), [groups]);
4431
+ const {
4432
+ visits,
4433
+ ...recents
4434
+ } = useRecentVisits(env);
4435
+ const recentItems = visits.map(visitItem);
4436
+ const items = hasTerm ? sections.flatMap(section => section.items) : recentItems;
4437
+ const navigation = useResultNavigation(items, hasTerm ? sections : RECENTS);
4438
+ const {
4439
+ highlightedIndex
4440
+ } = navigation;
4441
+ const inputRef = React.useRef(null);
4442
+ const [announceHighlight, setAnnounceHighlight] = React.useState(false);
4443
+ // The tabs keep focus while arrowing through results, but only the focused
4444
+ // input can point screen readers at the highlight, so from the tabs it's
4445
+ // announced instead. Typing goes back to the input, keystroke and all.
4446
+ const handleTabsKeyDown = event => {
4447
+ var _a;
4448
+ if (isTypingKey(event)) {
4449
+ (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
4450
+ } else if (verticalStep(event.key)) {
4451
+ setAnnounceHighlight(true);
4452
+ navigation.handleKeyDown(event);
4453
+ } else {
4454
+ navigation.handleKeyDown(event);
4455
+ }
4456
+ };
4007
4457
  React.useEffect(() => {
4008
- const observer = new IntersectionObserver(([entry]) => {
4009
- if (entry.isIntersecting) onVisible();
4010
- });
4011
- if (ref.current) observer.observe(ref.current);
4012
- return () => observer.disconnect();
4013
- }, [onVisible]);
4014
- return /*#__PURE__*/React.createElement("div", {
4015
- ref: ref
4016
- });
4017
- }
4018
- // Unlike search results, recents come from every product, so each one names
4019
- // its product and type.
4020
- function describeVisit(visit) {
4021
- const subtitle = (...parts) => parts.filter(Boolean).join(" · ");
4022
- if (visit.status === "page") {
4023
- const {
4024
- details,
4025
- product,
4026
- title
4027
- } = visit.entry;
4028
- return {
4029
- subtitle: subtitle(productName(product), details),
4030
- title,
4031
- url: visit.url
4032
- };
4033
- } else {
4034
- const {
4035
- metadata,
4036
- product,
4037
- title,
4038
- type
4039
- } = visit.result.attributes;
4040
- return {
4041
- subtitle: subtitle(productName(product), type, summarize(metadata)),
4042
- title,
4043
- url: visit.url
4044
- };
4045
- }
4046
- }
4047
- // check-ins → Check-Ins
4048
- function productName(slug) {
4049
- return slug.split("-").map(capitalize).join("-");
4050
- }
4051
- function capitalize(word) {
4052
- return word.charAt(0).toUpperCase() + word.slice(1);
4458
+ if (hasTerm) saveLastProduct(product.slug);
4459
+ }, [hasTerm, product.slug]);
4460
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SearchInput, {
4461
+ expanded: items.length > 0,
4462
+ highlightedIndex: highlightedIndex,
4463
+ inputRef: inputRef,
4464
+ listboxId: listboxId,
4465
+ onChange: setTerm,
4466
+ onFocus: () => setAnnounceHighlight(false),
4467
+ onKeyDown: navigation.handleKeyDown,
4468
+ value: term
4469
+ }), /*#__PURE__*/React.createElement(ProductTabs, {
4470
+ onKeyDown: handleTabsKeyDown,
4471
+ onSelect: setSelectedSlug,
4472
+ products: products,
4473
+ selected: product
4474
+ }, hasTerm ? (/*#__PURE__*/React.createElement(SearchResults, {
4475
+ highlightedIndex: highlightedIndex,
4476
+ id: listboxId,
4477
+ label: `${product.appName} results`,
4478
+ onHighlight: navigation.highlightIndex,
4479
+ sections: sections,
4480
+ status: search.status,
4481
+ term: term
4482
+ })) : (/*#__PURE__*/React.createElement(RecentlyVisited, {
4483
+ highlightedIndex: highlightedIndex,
4484
+ id: listboxId,
4485
+ items: recentItems,
4486
+ onHighlight: navigation.highlightIndex,
4487
+ ...recents
4488
+ }))), /*#__PURE__*/React.createElement("div", {
4489
+ "aria-live": "polite",
4490
+ className: styles$b.visuallyHidden
4491
+ }, announceHighlight ? (_b = navigation.highlighted) === null || _b === void 0 ? void 0 : _b.title : null), /*#__PURE__*/React.createElement(KeyboardHints, {
4492
+ copied: navigation.copied
4493
+ }));
4053
4494
  }
4054
4495
 
4055
- const SEARCH_DEBOUNCE_MS = 300;
4056
- function useProductSearch(product, term) {
4057
- const query = term.trim();
4058
- const debouncedQuery = useDebouncedValue(query, SEARCH_DEBOUNCE_MS);
4059
- const request = useCachedRequest(debouncedQuery ? productSearchUrl(product, debouncedQuery) : null);
4060
- const response = "data" in request ? request.data : undefined;
4061
- const settledProduct = useSettledProduct(product, request.status);
4062
- // While a request loads, the previous response stays on screen, but only
4063
- // if it was for this product: People results must never show under the
4064
- // Services tab.
4065
- const visibleResponse = request.status === "done" || settledProduct === product ? response : undefined;
4066
- const groups = React.useMemo(() => visibleResponse && groupByType(visibleResponse), [visibleResponse]);
4067
- if (!query) {
4068
- return {
4069
- status: "idle"
4070
- };
4071
- } else if (query !== debouncedQuery) {
4072
- return {
4073
- groups,
4074
- status: "loading"
4075
- };
4076
- } else if (request.status === "done") {
4077
- return {
4078
- groups: groups !== null && groups !== void 0 ? groups : [],
4079
- status: "done"
4080
- };
4081
- } else if (request.status === "error") {
4082
- return {
4083
- status: "error"
4084
- };
4085
- } else {
4086
- return {
4087
- groups,
4088
- status: "loading"
4089
- };
4090
- }
4496
+ // Products open modals from many libraries, not all of which mark themselves
4497
+ // with roles, so a modal is recognized by what it does: cover the page.
4498
+ function isModalOpen() {
4499
+ const {
4500
+ clientHeight,
4501
+ clientWidth
4502
+ } = document.documentElement;
4503
+ const element = document.elementFromPoint(clientWidth / 2, clientHeight / 2);
4504
+ return element !== null && (isInNativeDialog(element) || hasBackdrop(element, clientWidth, clientHeight));
4091
4505
  }
4092
- function productSearchUrl(product, query) {
4093
- const params = new URLSearchParams({
4094
- per_page: "100",
4095
- "where[term]": query
4096
- });
4097
- return `${window.location.origin}/~api/${product}/v2/product_search_results?${params}`;
4506
+ // A native modal's backdrop is drawn by the browser rather than being an
4507
+ // element; hit-testing it lands on the dialog itself.
4508
+ function isInNativeDialog(element) {
4509
+ return element.closest("dialog[open]") !== null;
4098
4510
  }
4099
- function groupByType({
4100
- data,
4101
- meta
4102
- }) {
4103
- var _a, _b;
4104
- const typeOrder = (_a = meta === null || meta === void 0 ? void 0 : meta.type_order) !== null && _a !== void 0 ? _a : [];
4105
- const groups = new Map();
4106
- for (const result of data) {
4107
- const {
4108
- type
4109
- } = result.attributes;
4110
- if (!groups.has(type)) groups.set(type, {
4111
- results: [],
4112
- type
4113
- });
4114
- (_b = groups.get(type)) === null || _b === void 0 ? void 0 : _b.results.push(result);
4511
+ function hasBackdrop(element, width, height) {
4512
+ let current = element;
4513
+ while (current && !isBackdrop(current, width, height)) {
4514
+ current = current.parentElement;
4115
4515
  }
4116
- const rank = type => {
4117
- const index = typeOrder.indexOf(type);
4118
- return index === -1 ? typeOrder.length : index;
4119
- };
4120
- return [...groups.values()].sort((a, b) => rank(a.type) - rank(b.type));
4516
+ return current !== null;
4121
4517
  }
4122
- // The product the most recent finished request was for.
4123
- function useSettledProduct(product, status) {
4124
- const [settledProduct, setSettledProduct] = React.useState();
4125
- React.useEffect(() => {
4126
- if (status === "done") setSettledProduct(product);
4127
- }, [product, status]);
4128
- return settledProduct;
4129
- }
4130
- function useDebouncedValue(value, delay) {
4131
- const [debounced, setDebounced] = React.useState(value);
4132
- React.useEffect(() => {
4133
- const timeout = setTimeout(() => setDebounced(value), delay);
4134
- return () => clearTimeout(timeout);
4135
- }, [value, delay]);
4136
- return debounced;
4518
+ // Some backdrops start below the topbar rather than at the top of the screen,
4519
+ // so the layer only has to span the width and reach from the center to the
4520
+ // bottom. Its own box has to reach the center: a bar along the bottom can
4521
+ // hold a panel that pops up over the center, like Services' media player.
4522
+ // react-aria leaves a closed modal's backdrop up while it fades out, search's
4523
+ // own included, so one that's on its way out doesn't count.
4524
+ function isBackdrop(element, width, height) {
4525
+ const rect = element.getBoundingClientRect();
4526
+ return !element.matches("[data-exiting]") && getComputedStyle(element).position === "fixed" && rect.width >= width - SCROLLBAR_SLACK && rect.top <= height / 2 && rect.bottom >= height - ROUNDING_SLACK;
4137
4527
  }
4138
-
4139
- const COPIED_FEEDBACK_MS = 1500;
4140
- const verticalStep = key => ({
4141
- ArrowDown: 1,
4142
- ArrowUp: -1
4143
- })[key];
4144
- // Remembering which list the index belongs to puts the highlight back on the
4145
- // first option whenever a new list replaces it, without an effect to reset it.
4146
- // A list that grows or fills in under the same `list` keeps its highlight.
4147
- function useResultNavigation(options, list) {
4148
- const [highlight, setHighlight] = React.useState({
4149
- index: 0,
4150
- list
4151
- });
4152
- const highlightedIndex = highlight.list === list ? highlight.index : 0;
4153
- const highlighted = options[highlightedIndex];
4154
- const [copiedUrl, setCopiedUrl] = React.useState(null);
4528
+ // react-aria keeps a classic scrollbar's gutter reserved while its modals lock
4529
+ // scrolling, so their backdrops stop short of the right edge by its width.
4530
+ const SCROLLBAR_SLACK = 24;
4531
+ // Rects are fractional at non-100% zoom, but clientWidth and clientHeight are
4532
+ // rounded to whole pixels.
4533
+ const ROUNDING_SLACK = 1;
4534
+
4535
+ function useSearchShortcut(enabled, open, setOpen) {
4155
4536
  React.useEffect(() => {
4156
- if (copiedUrl) {
4157
- const timeout = setTimeout(() => setCopiedUrl(null), COPIED_FEEDBACK_MS);
4158
- return () => clearTimeout(timeout);
4159
- } else {
4160
- return undefined;
4161
- }
4162
- }, [copiedUrl]);
4163
- const highlightIndex = React.useCallback(index => setHighlight(current => current.list === list && current.index === index ? current : {
4164
- index,
4165
- list
4166
- }), [list]);
4167
- const handleKeyDown = event => {
4168
- const step = verticalStep(event.key);
4169
- if (step && options.length > 0) {
4170
- event.preventDefault();
4171
- highlightIndex((highlightedIndex + step + options.length) % options.length);
4172
- } else if (event.key === "Enter" && highlighted && !event.nativeEvent.isComposing) {
4173
- event.preventDefault();
4174
- open(highlighted.url, {
4175
- newTab: hasCommandKey(event)
4176
- });
4177
- } else if (isCopyShortcut(event) && highlighted && !hasSelectedText(event.currentTarget)) {
4178
- event.preventDefault();
4179
- const {
4180
- url
4181
- } = highlighted;
4182
- navigator.clipboard.writeText(url).then(() => setCopiedUrl(url));
4183
- }
4184
- };
4185
- return {
4186
- copied: copiedUrl !== null && copiedUrl === (highlighted === null || highlighted === void 0 ? void 0 : highlighted.url),
4187
- handleKeyDown,
4188
- highlightIndex,
4189
- highlighted,
4190
- highlightedIndex: highlighted ? highlightedIndex : null
4191
- };
4192
- }
4193
- function open(url, {
4194
- newTab
4195
- }) {
4196
- if (newTab) {
4197
- window.open(url, "_blank", "noopener");
4198
- } else {
4199
- window.location.assign(url);
4200
- }
4201
- }
4202
- const isCopyShortcut = event => event.key.toLowerCase() === "c" && !event.altKey && !event.shiftKey && hasCommandKey(event);
4203
- // With text selected, ⌘C should copy that text as usual, not the link.
4204
- function hasSelectedText(element) {
4205
- var _a, _b;
4206
- const selectedInField = element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
4207
- return selectedInField || ((_b = (_a = window.getSelection()) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : "") !== "";
4537
+ if (!enabled) return undefined;
4538
+ // Rich text editors like Trix bind ⌘K to "insert link"; capturing at the
4539
+ // window and stopping propagation keeps them from ever seeing it. Under a
4540
+ // product's modal, search stays shut rather than stacking a second modal,
4541
+ // and the keystroke goes on to the product.
4542
+ const handleKeyDown = event => {
4543
+ if (open && isSearchShortcut(event)) {
4544
+ event.preventDefault();
4545
+ event.stopPropagation();
4546
+ setOpen(false);
4547
+ } else if (!open && (isSearchShortcut(event) || isLegacySearchShortcut(event)) && !isModalOpen()) {
4548
+ event.preventDefault();
4549
+ event.stopPropagation();
4550
+ setOpen(true);
4551
+ }
4552
+ };
4553
+ window.addEventListener("keydown", handleKeyDown, {
4554
+ capture: true
4555
+ });
4556
+ return () => window.removeEventListener("keydown", handleKeyDown, {
4557
+ capture: true
4558
+ });
4559
+ }, [enabled, open, setOpen]);
4208
4560
  }
4209
4561
 
4210
4562
  const SEARCHABLE_PRODUCTS = [{
@@ -4232,6 +4584,7 @@ const SEARCHABLE_PRODUCTS = [{
4232
4584
  appName: "Services",
4233
4585
  slug: "services"
4234
4586
  }];
4587
+
4235
4588
  const PERMISSION_CHECKS = {
4236
4589
  "check-ins": () => fetchMe("check-ins", "permission").then(({
4237
4590
  permission
@@ -4294,6 +4647,7 @@ function canSearch(slug, personId) {
4294
4647
  return promise;
4295
4648
  }
4296
4649
  }
4650
+
4297
4651
  function useSearchableProducts(apps) {
4298
4652
  // Keyed by name because providers hand over a fresh `apps` array on renders
4299
4653
  // where nothing changed.
@@ -4335,181 +4689,27 @@ function inSwitcherOrder(apps) {
4335
4689
  return groupApps(apps).restProducts.flatMap(app => SEARCHABLE_PRODUCTS.filter(product => product.appName === app.name));
4336
4690
  }
4337
4691
 
4338
- const isSearchShortcut = event => event.key.toLowerCase() === "k" && !event.altKey && !event.shiftKey && hasCommandKey(event);
4339
- // Products opened their own search with `/` before this one replaced it.
4340
- const isLegacySearchShortcut = event => event.key === "/" && !event.altKey && !event.ctrlKey && !event.metaKey && !isTypingIn(event.target);
4341
- const isTypingKey = event => event.key.length === 1 && event.key !== " " && !event.altKey && !event.ctrlKey && !event.metaKey;
4342
- const isTypingIn = target => target instanceof Element && target.closest('input, select, textarea, [contenteditable]:not([contenteditable="false"])') !== null;
4343
4692
  function ProductSearch({
4344
4693
  apps,
4345
4694
  env
4346
4695
  }) {
4347
4696
  const searchable = useSearchableProducts(apps);
4348
- if (searchable.status === "done" && searchable.products.length > 0) {
4349
- return /*#__PURE__*/React.createElement(SearchButtonAndDialog, {
4350
- env: env,
4351
- products: searchable.products
4352
- });
4353
- } else {
4354
- return null;
4355
- }
4356
- }
4357
- function SearchButtonAndDialog({
4358
- env,
4359
- products
4360
- }) {
4361
4697
  const [open, setOpen] = React.useState(false);
4362
- React.useEffect(() => {
4363
- // Rich text editors like Trix bind ⌘K to "insert link"; capturing at the
4364
- // window and stopping propagation keeps them from ever seeing it.
4365
- const handleKeyDown = event => {
4366
- if (isSearchShortcut(event)) {
4367
- event.preventDefault();
4368
- event.stopPropagation();
4369
- setOpen(wasOpen => !wasOpen);
4370
- } else if (isLegacySearchShortcut(event)) {
4371
- event.preventDefault();
4372
- event.stopPropagation();
4373
- setOpen(true);
4374
- }
4375
- };
4376
- window.addEventListener("keydown", handleKeyDown, {
4377
- capture: true
4378
- });
4379
- return () => window.removeEventListener("keydown", handleKeyDown, {
4380
- capture: true
4381
- });
4382
- }, []);
4383
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("button", {
4384
- "aria-keyshortcuts": `${isMac() ? "Meta+K" : "Control+K"} /`,
4385
- "aria-label": "Search",
4386
- onClick: () => setOpen(true),
4387
- type: "button"
4388
- }, /*#__PURE__*/React.createElement(Spyglass, {
4389
- fill: "currentColor"
4390
- })), /*#__PURE__*/React.createElement(tapestry.Modal, {
4698
+ const available = searchable.status === "done" && searchable.products.length > 0;
4699
+ useSearchShortcut(available, open, setOpen);
4700
+ if (!available) return null;
4701
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SearchButton, {
4702
+ onClick: () => setOpen(true)
4703
+ }), /*#__PURE__*/React.createElement(tapestry.Modal, {
4391
4704
  "aria-label": "Search",
4392
4705
  dismissible: true,
4393
4706
  onClose: () => setOpen(false),
4394
4707
  open: open
4395
4708
  }, /*#__PURE__*/React.createElement(SearchPanel, {
4396
4709
  env: env,
4397
- products: products
4710
+ products: searchable.products
4398
4711
  })));
4399
4712
  }
4400
- // Keys the recents list for navigation, so the highlight stays put as more
4401
- // recents load and as their lookups answer.
4402
- const RECENTS = "recents";
4403
- function SearchPanel({
4404
- env,
4405
- products
4406
- }) {
4407
- var _a, _b;
4408
- const listboxId = useInstanceId("product-search-results");
4409
- const [term, setTerm] = React.useState("");
4410
- const [selectedSlug, setSelectedSlug] = React.useState(() => initialProduct(products).slug);
4411
- const product = (_a = products.find(({
4412
- slug
4413
- }) => slug === selectedSlug)) !== null && _a !== void 0 ? _a : products[0];
4414
- const hasTerm = term.trim() !== "";
4415
- const search = useProductSearch(product.slug, term);
4416
- const groups = "groups" in search ? search.groups : undefined;
4417
- const results = React.useMemo(() => {
4418
- var _a;
4419
- return (_a = groups === null || groups === void 0 ? void 0 : groups.flatMap(group => group.results)) !== null && _a !== void 0 ? _a : [];
4420
- }, [groups]);
4421
- const recents = useRecentVisits(env);
4422
- const options = hasTerm ? results.map(({
4423
- attributes
4424
- }) => ({
4425
- label: attributes.title,
4426
- url: attributes.url
4427
- })) : recents.visits.map(visit => {
4428
- const {
4429
- title,
4430
- url
4431
- } = describeVisit(visit);
4432
- return {
4433
- label: title,
4434
- url
4435
- };
4436
- });
4437
- const navigation = useResultNavigation(options, hasTerm ? results : RECENTS);
4438
- const {
4439
- highlightedIndex
4440
- } = navigation;
4441
- const inputRef = React.useRef(null);
4442
- const [announceHighlight, setAnnounceHighlight] = React.useState(false);
4443
- // The tabs keep focus while arrowing through results, but only the focused
4444
- // input can point screen readers at the highlight, so from the tabs it's
4445
- // announced instead. Typing goes back to the input, keystroke and all.
4446
- const handleTabsKeyDown = event => {
4447
- var _a;
4448
- if (isTypingKey(event)) {
4449
- (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
4450
- } else if (verticalStep(event.key)) {
4451
- setAnnounceHighlight(true);
4452
- navigation.handleKeyDown(event);
4453
- } else {
4454
- navigation.handleKeyDown(event);
4455
- }
4456
- };
4457
- React.useEffect(() => {
4458
- if (hasTerm) saveLastProduct(product.slug);
4459
- }, [hasTerm, product.slug]);
4460
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("input", {
4461
- "aria-activedescendant": highlightedIndex === null ? undefined : resultId(listboxId, highlightedIndex),
4462
- "aria-autocomplete": "list",
4463
- "aria-controls": options.length > 0 ? listboxId : undefined,
4464
- "aria-expanded": options.length > 0,
4465
- "aria-label": "Search",
4466
- autoFocus: true,
4467
- onChange: event => setTerm(event.target.value),
4468
- onFocus: () => setAnnounceHighlight(false),
4469
- onKeyDown: navigation.handleKeyDown,
4470
- placeholder: "Search",
4471
- ref: inputRef,
4472
- role: "combobox",
4473
- type: "search",
4474
- value: term
4475
- }), /*#__PURE__*/React.createElement(ProductTabs, {
4476
- onKeyDown: handleTabsKeyDown,
4477
- onSelect: setSelectedSlug,
4478
- products: products,
4479
- selected: product
4480
- }, hasTerm ? (/*#__PURE__*/React.createElement(SearchResults, {
4481
- highlightedIndex: highlightedIndex,
4482
- id: listboxId,
4483
- label: `${product.appName} results`,
4484
- onHighlight: navigation.highlightIndex,
4485
- search: search,
4486
- term: term
4487
- })) : (/*#__PURE__*/React.createElement(RecentlyVisited, {
4488
- highlightedIndex: highlightedIndex,
4489
- id: listboxId,
4490
- onHighlight: navigation.highlightIndex,
4491
- ...recents
4492
- }))), /*#__PURE__*/React.createElement("div", {
4493
- "aria-live": "polite",
4494
- className: styles$c.visuallyHidden
4495
- }, announceHighlight ? (_b = navigation.highlighted) === null || _b === void 0 ? void 0 : _b.label : null), /*#__PURE__*/React.createElement(KeyboardHints, {
4496
- copied: navigation.copied
4497
- }));
4498
- }
4499
- function KeyboardHints({
4500
- copied
4501
- }) {
4502
- const command = commandKeyLabel();
4503
- return /*#__PURE__*/React.createElement("p", null, "\u2191\u2193 Select \u00B7 \u21B5 Open \u00B7 ", command, "\u21B5 Open in new tab \u00B7 ", command, "C", " ", /*#__PURE__*/React.createElement("span", {
4504
- role: "status"
4505
- }, copied ? "Copied!" : "Copy link"), " \u00B7 ", command, "K Open search");
4506
- }
4507
- function initialProduct(products) {
4508
- var _a, _b, _c;
4509
- const lastProduct = readLastProduct();
4510
- const bySlug = slug => products.find(product => product.slug === slug);
4511
- 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];
4512
- }
4513
4713
 
4514
4714
  function useRecordRecentlyVisited({
4515
4715
  enabled,
@@ -4553,9 +4753,9 @@ function StyledRoot({
4553
4753
  `), children);
4554
4754
  }
4555
4755
 
4556
- var styles$9 = {"route":"route--8BWe","is-active":"is-active-FsU9Y","apps-routes-separator":"apps-routes-separator-Nlm-N"};
4756
+ var styles$a = {"route":"route--8BWe","is-active":"is-active-FsU9Y","apps-routes-separator":"apps-routes-separator-Nlm-N"};
4557
4757
 
4558
- 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"};
4758
+ 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"};
4559
4759
 
4560
4760
  const UserMenu = ({
4561
4761
  appName,
@@ -4574,7 +4774,7 @@ const UserMenu = ({
4574
4774
  component: Outsider,
4575
4775
  onOutsideClick: () => setUserMenuVisible(false),
4576
4776
  cleanup: !userMenuVisible,
4577
- className: styles$8.popup,
4777
+ className: styles$9.popup,
4578
4778
  isVisible: userMenuVisible
4579
4779
  }, /*#__PURE__*/React.createElement("div", null, hasConnectedPeople ? (/*#__PURE__*/React.createElement("div", {
4580
4780
  style: {
@@ -4718,11 +4918,11 @@ const UserMenuTrigger = ({
4718
4918
  userMenuVisible
4719
4919
  }) => {
4720
4920
  return /*#__PURE__*/React.createElement("div", {
4721
- className: styles$8["trigger-wrap"],
4921
+ className: styles$9["trigger-wrap"],
4722
4922
  "data-pendo": "topbar-org-switcher"
4723
4923
  }, /*#__PURE__*/React.createElement(Hoverable, {
4724
- className: classnames(styles$8["trigger"], {
4725
- [styles$8["is-visible"]]: userMenuVisible
4924
+ className: classnames(styles$9["trigger"], {
4925
+ [styles$9["is-visible"]]: userMenuVisible
4726
4926
  }),
4727
4927
  component: "button",
4728
4928
  type: "button",
@@ -4734,7 +4934,7 @@ const UserMenuTrigger = ({
4734
4934
  requestConnectedPeopleFetch();
4735
4935
  }
4736
4936
  }, showOrgName && (/*#__PURE__*/React.createElement("span", {
4737
- className: styles$8["trigger-orgname"]
4937
+ className: styles$9["trigger-orgname"]
4738
4938
  }, orgName)), /*#__PURE__*/React.createElement(DisclosureChevronIcon, null)));
4739
4939
  };
4740
4940
 
@@ -4794,7 +4994,7 @@ const NotSmallTopbar = ({
4794
4994
  colors: props.colors,
4795
4995
  requestAppsFetch: props.requestAppsFetch
4796
4996
  })), /*#__PURE__*/React.createElement("div", {
4797
- className: styles$9["apps-routes-separator"]
4997
+ className: styles$a["apps-routes-separator"]
4798
4998
  }), routesVisible && props.routes, /*#__PURE__*/React.createElement("div", {
4799
4999
  style: {
4800
5000
  margin: "auto"
@@ -4835,12 +5035,12 @@ const NotSmallRoute = ({
4835
5035
  ...nativeProps
4836
5036
  }) => {
4837
5037
  return /*#__PURE__*/React.createElement("a", {
4838
- className: classnames(styles$9["route"], className, active && styles$9["is-active"]),
5038
+ className: classnames(styles$a["route"], className, active && styles$a["is-active"]),
4839
5039
  ...nativeProps
4840
5040
  });
4841
5041
  };
4842
5042
 
4843
- var styles$7 = {"avatar":"avatar-tW-wn"};
5043
+ var styles$8 = {"avatar":"avatar-tW-wn"};
4844
5044
 
4845
5045
  function Avatar({
4846
5046
  env,
@@ -4850,7 +5050,7 @@ function Avatar({
4850
5050
  }) {
4851
5051
  const url = incomingUrl || `${pcoUrl$1(env)("people")}/static/no_photo_thumbnail_gray.svg`;
4852
5052
  return /*#__PURE__*/React.createElement("div", {
4853
- className: styles$7.avatar,
5053
+ className: styles$8.avatar,
4854
5054
  style: {
4855
5055
  backgroundImage: `url(${url})`,
4856
5056
  ...style
@@ -32609,7 +32809,7 @@ function requireReactDom () {
32609
32809
 
32610
32810
  var reactDomExports = requireReactDom();
32611
32811
 
32612
- 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"};
32812
+ 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"};
32613
32813
 
32614
32814
  const AppsSwitcher = ({
32615
32815
  appName,
@@ -32637,8 +32837,8 @@ const AppsSwitcher = ({
32637
32837
  }, /*#__PURE__*/React.createElement("a", {
32638
32838
  "aria-current": name === appName ? "page" : undefined,
32639
32839
  "aria-label": name,
32640
- className: classnames(styles$6.link, {
32641
- [styles$6.current]: name === appName
32840
+ className: classnames(styles$7.link, {
32841
+ [styles$7.current]: name === appName
32642
32842
  }),
32643
32843
  "data-pendo": `app-switcher--change-app--${name}`,
32644
32844
  "data-turbolinks": false,
@@ -32650,48 +32850,48 @@ const AppsSwitcher = ({
32650
32850
  "aria-expanded": visible,
32651
32851
  "aria-haspopup": "true",
32652
32852
  "aria-label": `App switcher: ${appName}`,
32653
- className: styles$6.trigger,
32853
+ className: styles$7.trigger,
32654
32854
  onClick: onTriggerClick,
32655
32855
  type: "button"
32656
32856
  }, /*#__PURE__*/React.createElement(ColorAppIcon, {
32657
32857
  appName: appName.replace(/[\s-]/, "")
32658
32858
  })), /*#__PURE__*/reactDomExports.createPortal(/*#__PURE__*/React.createElement("div", {
32659
32859
  ref: containerRef,
32660
- className: classnames(styles$6.layer, {
32661
- [styles$6["layer--visible"]]: visible
32860
+ className: classnames(styles$7.layer, {
32861
+ [styles$7["layer--visible"]]: visible
32662
32862
  }),
32663
32863
  onClick: toggle,
32664
32864
  onKeyDown: handleKeyDown
32665
32865
  }, /*#__PURE__*/React.createElement("nav", {
32666
32866
  "aria-label": "Switch app",
32667
32867
  "aria-modal": visible,
32668
- className: styles$6.panel,
32868
+ className: styles$7.panel,
32669
32869
  "data-pendo": "app-switcher",
32670
32870
  onClick: event => event.stopPropagation(),
32671
32871
  role: "dialog"
32672
32872
  }, /*#__PURE__*/React.createElement("div", {
32673
- className: styles$6.header
32873
+ className: styles$7.header
32674
32874
  }, /*#__PURE__*/React.createElement("span", {
32675
- className: styles$6.heading
32875
+ className: styles$7.heading
32676
32876
  }, "Switch to"), /*#__PURE__*/React.createElement("button", {
32677
32877
  "aria-label": "Close",
32678
- className: styles$6.close,
32878
+ className: styles$7.close,
32679
32879
  onClick: toggle,
32680
32880
  type: "button"
32681
32881
  }, /*#__PURE__*/React.createElement(X, {
32682
32882
  "aria-hidden": "true",
32683
32883
  fill: "var(--t-icon-color-dim)"
32684
32884
  }))), /*#__PURE__*/React.createElement("ul", {
32685
- className: styles$6.list,
32885
+ className: styles$7.list,
32686
32886
  role: "list"
32687
32887
  }, homeProduct && productLink(homeProduct.name), restProducts.map(({
32688
32888
  name
32689
32889
  }) => productLink(name)), accountsProduct && (/*#__PURE__*/React.createElement("li", {
32690
- className: styles$6["accounts-item"]
32890
+ className: styles$7["accounts-item"]
32691
32891
  }, /*#__PURE__*/React.createElement("a", {
32692
32892
  "aria-current": appName === "Accounts" ? "page" : undefined,
32693
32893
  "aria-label": "Account settings",
32694
- className: classnames(styles$6.link, styles$6["accounts-link"]),
32894
+ className: classnames(styles$7.link, styles$7["accounts-link"]),
32695
32895
  "data-pendo": "app-switcher--change-app--Accounts",
32696
32896
  "data-turbolinks": false,
32697
32897
  href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(accountsProduct.name)
@@ -32714,14 +32914,14 @@ ClientStorage.defaultProps = {
32714
32914
  method: "localStorage"
32715
32915
  };
32716
32916
 
32717
- 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"};
32917
+ 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"};
32718
32918
 
32719
32919
  function AccountSettingsLink({
32720
32920
  env
32721
32921
  }) {
32722
32922
  const accountsName = "accounts";
32723
32923
  return /*#__PURE__*/React.createElement("a", {
32724
- className: classnames(styles$5["appmenu-list-a"], styles$5["appmenu-list-a--bottom"]),
32924
+ className: classnames(styles$6["appmenu-list-a"], styles$6["appmenu-list-a--bottom"]),
32725
32925
  "data-pendo": `app-switcher--change-app--${accountsName}`,
32726
32926
  "data-turbolinks": false,
32727
32927
  href: pcoUrl$1(env)(accountsName) + getAppsSwitchProductPath(accountsName)
@@ -32744,7 +32944,7 @@ const CloseMenuButton = ({
32744
32944
  }) => (/*#__PURE__*/React.createElement("button", {
32745
32945
  "aria-label": "Close menu",
32746
32946
  type: "button",
32747
- className: styles$5["appmenu-close-button"],
32947
+ className: styles$6["appmenu-close-button"],
32748
32948
  style: style
32749
32949
  }, /*#__PURE__*/React.createElement(X, {
32750
32950
  fill: color,
@@ -32805,7 +33005,7 @@ const LegacyAppsSwitcher = ({
32805
33005
  "aria-haspopup": "true",
32806
33006
  "aria-label": `App switcher: ${appName}`,
32807
33007
  type: "button",
32808
- className: styles$5["appmenu-button"],
33008
+ className: styles$6["appmenu-button"],
32809
33009
  onClick: onTriggerClick
32810
33010
  }, /*#__PURE__*/React.createElement(ColorAppIcon, {
32811
33011
  appName: appName.replace(/[\s-]/, "")
@@ -32813,7 +33013,7 @@ const LegacyAppsSwitcher = ({
32813
33013
  item: "Topbar:Small:Menus:Products:visibility",
32814
33014
  render: () => (/*#__PURE__*/React.createElement("div", {
32815
33015
  ref: appMenuRef,
32816
- className: styles$5["appmenu-layer"],
33016
+ className: styles$6["appmenu-layer"],
32817
33017
  style: {
32818
33018
  ...(visible && {
32819
33019
  display: "block"
@@ -32830,37 +33030,37 @@ const LegacyAppsSwitcher = ({
32830
33030
  e.stopPropagation();
32831
33031
  }
32832
33032
  },
32833
- className: styles$5["appmenu-container"]
33033
+ className: styles$6["appmenu-container"]
32834
33034
  }, /*#__PURE__*/React.createElement("a", {
32835
33035
  "aria-current": appName === (homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name) ? "page" : undefined,
32836
33036
  "aria-label": homeProduct ? homeProduct.name : appName,
32837
33037
  "data-pendo": `app-switcher--change-app--${homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name}`,
32838
33038
  "data-turbolinks": false,
32839
- className: classnames(styles$5["appmenu-list-a"], styles$5["appmenu-list-a--top"]),
33039
+ className: classnames(styles$6["appmenu-list-a"], styles$6["appmenu-list-a--top"]),
32840
33040
  href: homeProduct ? pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name) : undefined
32841
33041
  }, /*#__PURE__*/React.createElement(UserMenuAppLockup, {
32842
33042
  appName: homeProduct ? homeProduct.name : appName
32843
33043
  })), /*#__PURE__*/React.createElement("div", {
32844
- className: styles$5["appmenu-scroll"]
33044
+ className: styles$6["appmenu-scroll"]
32845
33045
  }, /*#__PURE__*/React.createElement("ul", {
32846
33046
  "data-pendo": "app-switcher",
32847
- className: styles$5["appmenu-list"]
33047
+ className: styles$6["appmenu-list"]
32848
33048
  }, appsMenuFormatter(restProducts).map(({
32849
33049
  name
32850
33050
  }) => (/*#__PURE__*/React.createElement("li", {
32851
33051
  key: name,
32852
- className: styles$5["appmenu-list-li"]
33052
+ className: styles$6["appmenu-list-li"]
32853
33053
  }, /*#__PURE__*/React.createElement("a", {
32854
33054
  "aria-current": name === appName ? "page" : undefined,
32855
33055
  "aria-label": name,
32856
33056
  "data-pendo": `app-switcher--change-app--${name}`,
32857
33057
  "data-turbolinks": false,
32858
- className: styles$5["appmenu-list-a"],
33058
+ className: styles$6["appmenu-list-a"],
32859
33059
  href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(name)
32860
33060
  }, /*#__PURE__*/React.createElement(UserMenuAppLockup, {
32861
33061
  appName: name
32862
33062
  }))))), accountsProduct && (/*#__PURE__*/React.createElement("li", {
32863
- className: styles$5["appmenu-list-li"]
33063
+ className: styles$6["appmenu-list-li"]
32864
33064
  }, /*#__PURE__*/React.createElement(AccountSettingsLink, {
32865
33065
  env: env
32866
33066
  }))))))))
@@ -32924,11 +33124,11 @@ const SmallTopbar = ({
32924
33124
  toggle: toggleProductSwitcher,
32925
33125
  visible: productSwitcherVisible
32926
33126
  })), routesMenuVisible && (/*#__PURE__*/React.createElement("div", {
32927
- className: styles$5["routes"]
33127
+ className: styles$6["routes"]
32928
33128
  }, props.routes)), /*#__PURE__*/React.createElement("button", {
32929
33129
  type: "button",
32930
- className: classnames(styles$5["current-route"], {
32931
- [styles$5["is-active"]]: routesMenuVisible
33130
+ className: classnames(styles$6["current-route"], {
33131
+ [styles$6["is-active"]]: routesMenuVisible
32932
33132
  }),
32933
33133
  onClick: props.routes.length > 0 ? () => {
32934
33134
  setProductSwitcherVisible(false);
@@ -32936,7 +33136,7 @@ const SmallTopbar = ({
32936
33136
  } : null
32937
33137
  }, /*#__PURE__*/React.createElement(CurrentRouteComponent, null, capitalize$1(currentRouteText)), props.routes.length > 0 && /*#__PURE__*/React.createElement(DisclosureChevronIcon, null)), /*#__PURE__*/React.createElement("button", {
32938
33138
  type: "button",
32939
- className: styles$5["profile-button"],
33139
+ className: styles$6["profile-button"],
32940
33140
  onClick: () => {
32941
33141
  var _a, _b;
32942
33142
  props.requestConnectedPeopleFetch();
@@ -32952,7 +33152,7 @@ const SmallRoute = ({
32952
33152
  className,
32953
33153
  ...props
32954
33154
  }) => (/*#__PURE__*/React.createElement("a", {
32955
- className: classnames(styles$5["route"], className, active && styles$5["is-active"]),
33155
+ className: classnames(styles$6["route"], className, active && styles$6["is-active"]),
32956
33156
  ...props
32957
33157
  }));
32958
33158
 
@@ -33091,7 +33291,7 @@ const topbarTheme = {
33091
33291
  id: "topbar"
33092
33292
  };
33093
33293
 
33094
- 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"};
33294
+ 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"};
33095
33295
 
33096
33296
  function isPresent(value) {
33097
33297
  return value !== undefined && value.trim() !== "";
@@ -33120,11 +33320,11 @@ function Field({
33120
33320
  value
33121
33321
  }) {
33122
33322
  return /*#__PURE__*/React.createElement("div", {
33123
- className: styles$4["debug-pco-resource-field"]
33323
+ className: styles$5["debug-pco-resource-field"]
33124
33324
  }, /*#__PURE__*/React.createElement("span", {
33125
- className: styles$4["debug-pco-resource-field-label"]
33325
+ className: styles$5["debug-pco-resource-field-label"]
33126
33326
  }, label), /*#__PURE__*/React.createElement("span", {
33127
- className: styles$4["debug-pco-resource-field-value"]
33327
+ className: styles$5["debug-pco-resource-field-value"]
33128
33328
  }, value));
33129
33329
  }
33130
33330
  // Only staff ghosting or impersonating a person get pco:staff-access-level,
@@ -33197,13 +33397,13 @@ function DebugPanel({
33197
33397
  };
33198
33398
  }, [pcrl, env]);
33199
33399
  return /*#__PURE__*/React.createElement("div", {
33200
- className: classnames("staff-only", styles$4["debug-pco-resource"])
33400
+ className: classnames("staff-only", styles$5["debug-pco-resource"])
33201
33401
  }, /*#__PURE__*/React.createElement("div", {
33202
- className: styles$4["debug-pco-resource-header"]
33402
+ className: styles$5["debug-pco-resource-header"]
33203
33403
  }, /*#__PURE__*/React.createElement("span", {
33204
- className: styles$4["debug-pco-resource-badge"]
33404
+ className: styles$5["debug-pco-resource-badge"]
33205
33405
  }, "Staff"), /*#__PURE__*/React.createElement("span", {
33206
- className: styles$4["debug-pco-resource-title"]
33406
+ className: styles$5["debug-pco-resource-title"]
33207
33407
  }, "Recently Visited debug")), /*#__PURE__*/React.createElement(Field, {
33208
33408
  label: "pco:resource",
33209
33409
  value: describeTag("resource", resource)
@@ -33211,29 +33411,29 @@ function DebugPanel({
33211
33411
  label: "pco:recently_visitable",
33212
33412
  value: describeTag("recently_visitable", recentlyVisitable)
33213
33413
  }), hasInvalidResource && (/*#__PURE__*/React.createElement("p", {
33214
- className: styles$4["debug-pco-resource-note"]
33414
+ className: styles$5["debug-pco-resource-note"]
33215
33415
  }, "pco:resource isn't a PCRL (product/v2/type/id), so Recently Visited ignores it.")), visit === null && (/*#__PURE__*/React.createElement("p", {
33216
- className: styles$4["debug-pco-resource-note"]
33416
+ className: styles$5["debug-pco-resource-note"]
33217
33417
  }, "This page is not eligible for Recently Visited")), pcrl && hasRecentlyVisitable && (/*#__PURE__*/React.createElement("p", {
33218
- className: styles$4["debug-pco-resource-note"]
33418
+ className: styles$5["debug-pco-resource-note"]
33219
33419
  }, "Both tags are present \u2014 pco:resource takes precedence for the Recently Visited entry.")), pcrl && lookup.status === "loading" && (/*#__PURE__*/React.createElement("p", {
33220
- className: styles$4["debug-pco-resource-note"]
33420
+ className: styles$5["debug-pco-resource-note"]
33221
33421
  }, "Looking up resource\u2026")), pcrl && lookup.status === "error" && (/*#__PURE__*/React.createElement("p", {
33222
- className: styles$4["debug-pco-resource-note"]
33422
+ className: styles$5["debug-pco-resource-note"]
33223
33423
  }, lookup.message)), pcrl && lookup.status === "loaded" && (/*#__PURE__*/React.createElement("pre", {
33224
- className: styles$4["debug-pco-resource-json"]
33424
+ className: styles$5["debug-pco-resource-json"]
33225
33425
  }, JSON.stringify(lookup.json, null, 2))), (visit === null || visit === void 0 ? void 0 : visit.type) === "page" && (/*#__PURE__*/React.createElement("p", {
33226
- className: styles$4["debug-pco-resource-note"]
33426
+ className: styles$5["debug-pco-resource-note"]
33227
33427
  }, "Recently Visited title: ", visit.title)), /*#__PURE__*/React.createElement("span", {
33228
- className: styles$4["debug-pco-resource-field-label"]
33428
+ className: styles$5["debug-pco-resource-field-label"]
33229
33429
  }, "Recently Visited"), personId ? (/*#__PURE__*/React.createElement("pre", {
33230
- className: styles$4["debug-pco-resource-json"]
33430
+ className: styles$5["debug-pco-resource-json"]
33231
33431
  }, JSON.stringify(recentlyVisited, null, 2))) : (/*#__PURE__*/React.createElement("p", {
33232
- className: styles$4["debug-pco-resource-note"]
33432
+ className: styles$5["debug-pco-resource-note"]
33233
33433
  }, "No current person, so nothing is recorded.")));
33234
33434
  }
33235
33435
 
33236
- var styles$3 = {"drawer-wrap":"drawer-wrap-J0rSL","is-visible":"is-visible-KTME8","drawer":"drawer-j7f4l"};
33436
+ var styles$4 = {"drawer-wrap":"drawer-wrap-J0rSL","is-visible":"is-visible-KTME8","drawer":"drawer-j7f4l"};
33237
33437
 
33238
33438
  function Drawer({
33239
33439
  children,
@@ -33241,7 +33441,7 @@ function Drawer({
33241
33441
  }) {
33242
33442
  return /*#__PURE__*/React.createElement("div", {
33243
33443
  id: id,
33244
- className: styles$3["drawer"]
33444
+ className: styles$4["drawer"]
33245
33445
  }, children);
33246
33446
  }
33247
33447
  function DrawerWrap({
@@ -33251,7 +33451,7 @@ function DrawerWrap({
33251
33451
  ...props
33252
33452
  }) {
33253
33453
  return /*#__PURE__*/React.createElement("div", {
33254
- className: classnames(styles$3["drawer-wrap"], className, isVisible && styles$3["is-visible"]),
33454
+ className: classnames(styles$4["drawer-wrap"], className, isVisible && styles$4["is-visible"]),
33255
33455
  ...props
33256
33456
  }, children);
33257
33457
  }
@@ -33467,7 +33667,7 @@ function Icon({
33467
33667
  }));
33468
33668
  }
33469
33669
 
33470
- 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"};
33670
+ 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"};
33471
33671
 
33472
33672
  const COLOR_MODES = [{
33473
33673
  icon: "toolbar.sun",
@@ -33490,11 +33690,11 @@ function ColorModeSwitcher() {
33490
33690
  const updateStore = useUpdateStore();
33491
33691
  const [counter] = React.useState(() => COLOR_MODE_INSTANCE_COUNTER++);
33492
33692
  return /*#__PURE__*/React.createElement("fieldset", {
33493
- className: styles$2["color-mode-switcher"]
33693
+ className: styles$3["color-mode-switcher"]
33494
33694
  }, /*#__PURE__*/React.createElement("legend", {
33495
- className: styles$2["color-mode-switcher-label"]
33695
+ className: styles$3["color-mode-switcher-label"]
33496
33696
  }, "Appearance", " "), /*#__PURE__*/React.createElement("div", {
33497
- className: styles$2["color-mode-switcher-modes"]
33697
+ className: styles$3["color-mode-switcher-modes"]
33498
33698
  }, COLOR_MODES.map(({
33499
33699
  icon,
33500
33700
  label,
@@ -33764,7 +33964,7 @@ const useToolbarReducer = overrides => React__namespace.useReducer(reducer, {
33764
33964
  ...overrides
33765
33965
  });
33766
33966
 
33767
- 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"};
33967
+ 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"};
33768
33968
 
33769
33969
  // PCO Box serves TLS by default, and a bare http:// frame inside an https
33770
33970
  // product page is blocked as mixed content, so follow the page's scheme.
@@ -33936,7 +34136,7 @@ const DeveloperTools = {
33936
34136
  name: "DeveloperTools"
33937
34137
  };
33938
34138
 
33939
- 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"};
34139
+ 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"};
33940
34140
 
33941
34141
  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";
33942
34142
  // Same stops as the AskAIBanner border gradient, so the two stay in sync.
@@ -33996,8 +34196,8 @@ function AskAIBanner(props) {
33996
34196
  var _a, _b;
33997
34197
  const isStacked = props.variant !== "inline";
33998
34198
  return /*#__PURE__*/React.createElement("button", {
33999
- className: classnames(styles.banner, {
34000
- [styles.stacked]: isStacked
34199
+ className: classnames(styles$1.banner, {
34200
+ [styles$1.stacked]: isStacked
34001
34201
  }),
34002
34202
  onClick: () => props.onClick(),
34003
34203
  type: "button"
@@ -34012,15 +34212,15 @@ function StackedContent({
34012
34212
  title
34013
34213
  }) {
34014
34214
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
34015
- className: styles.content
34215
+ className: styles$1.content
34016
34216
  }, /*#__PURE__*/React.createElement(AskAIIcon, null), /*#__PURE__*/React.createElement("span", {
34017
- className: styles.text
34217
+ className: styles$1.text
34018
34218
  }, /*#__PURE__*/React.createElement("span", {
34019
- className: styles.title
34219
+ className: styles$1.title
34020
34220
  }, title), /*#__PURE__*/React.createElement("span", {
34021
- className: styles.subtitle
34221
+ className: styles$1.subtitle
34022
34222
  }, "Support answers tailored to you"))), /*#__PURE__*/React.createElement(tapestryReact.Icon, {
34023
- className: styles.chevron,
34223
+ className: styles$1.chevron,
34024
34224
  name: "general.rightChevron",
34025
34225
  size: "16px"
34026
34226
  }));
@@ -34030,18 +34230,18 @@ function InlineContent({
34030
34230
  title
34031
34231
  }) {
34032
34232
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
34033
- className: styles.inlineContent
34233
+ className: styles$1.inlineContent
34034
34234
  }, /*#__PURE__*/React.createElement(SparkleIcon, {
34035
- className: styles.sparkle,
34235
+ className: styles$1.sparkle,
34036
34236
  fill: "currentColor"
34037
34237
  }), /*#__PURE__*/React.createElement("span", {
34038
- className: styles.inlineTitle
34238
+ className: styles$1.inlineTitle
34039
34239
  }, title)), /*#__PURE__*/React.createElement("span", {
34040
- className: styles.action
34240
+ className: styles$1.action
34041
34241
  }, /*#__PURE__*/React.createElement("span", {
34042
- className: styles.actionLabel
34242
+ className: styles$1.actionLabel
34043
34243
  }, action), /*#__PURE__*/React.createElement(tapestryReact.Icon, {
34044
- className: styles.actionChevron,
34244
+ className: styles$1.actionChevron,
34045
34245
  name: "general.rightChevron",
34046
34246
  size: "14px"
34047
34247
  })));
@@ -34058,9 +34258,9 @@ function AskAIIcon() {
34058
34258
  const borderA = `${uid}-border-a`;
34059
34259
  const borderB = `${uid}-border-b`;
34060
34260
  return /*#__PURE__*/React.createElement("span", {
34061
- className: styles.icon
34261
+ className: styles$1.icon
34062
34262
  }, /*#__PURE__*/React.createElement("svg", {
34063
- className: styles.iconShape,
34263
+ className: styles$1.iconShape,
34064
34264
  viewBox: "0 0 32 32",
34065
34265
  fill: "none",
34066
34266
  "aria-hidden": "true"
@@ -34231,7 +34431,7 @@ function AskAIIcon() {
34231
34431
  stopColor: "#D48B91",
34232
34432
  stopOpacity: "0"
34233
34433
  })))), /*#__PURE__*/React.createElement("svg", {
34234
- className: styles.iconSparkle,
34434
+ className: styles$1.iconSparkle,
34235
34435
  viewBox: "0 0 19.4995 17.9999",
34236
34436
  "aria-hidden": "true"
34237
34437
  }, /*#__PURE__*/React.createElement("path", {
@@ -34240,6 +34440,23 @@ function AskAIIcon() {
34240
34440
  })));
34241
34441
  }
34242
34442
 
34443
+ var styles = {"button":"button-2NVI1","sparkle":"sparkle-ruJGP","label":"label-4NUGB"};
34444
+
34445
+ function AskAIButton({
34446
+ onClick,
34447
+ text = "Ask AI"
34448
+ }) {
34449
+ return /*#__PURE__*/React.createElement("button", {
34450
+ className: styles.button,
34451
+ onClick: () => onClick(),
34452
+ type: "button"
34453
+ }, /*#__PURE__*/React.createElement(SparkleIcon, {
34454
+ className: styles.sparkle
34455
+ }), /*#__PURE__*/React.createElement("span", {
34456
+ className: styles.label
34457
+ }, text));
34458
+ }
34459
+
34243
34460
  // The article heading lives in the deepest populated hierarchy level (lvl0 is
34244
34461
  // always "Planning Center").
34245
34462
  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) || "";
@@ -34550,6 +34767,18 @@ function HelpdeskComponent({
34550
34767
  }
34551
34768
  }
34552
34769
  },
34770
+ AskAIButton: {
34771
+ component: AskAIButton,
34772
+ properties: {
34773
+ onClick: {
34774
+ event: false,
34775
+ type: Function
34776
+ },
34777
+ text: {
34778
+ type: String
34779
+ }
34780
+ }
34781
+ },
34553
34782
  CalendlyWidget: {
34554
34783
  component: CalendlyWidget,
34555
34784
  properties: {
@@ -36058,18 +36287,18 @@ function Toolbar({
36058
36287
  return /*#__PURE__*/React.createElement(tapestryReact.ThemeProvider, {
36059
36288
  theme: topbarTheme
36060
36289
  }, /*#__PURE__*/React.createElement(addOns.UiExtensionDataContext, null, /*#__PURE__*/React.createElement("div", {
36061
- className: styles$1["toolbar"],
36290
+ className: styles$2["toolbar"],
36062
36291
  "data-pendo": "toolbar-profile-support-wrapper"
36063
36292
  }, /*#__PURE__*/React.createElement(ToolbarButton, {
36064
36293
  "aria-label": `Open ${userName}’s profile drawer`,
36065
36294
  onClick: () => toggleDrawer(Profile),
36066
36295
  "data-pendo": "toolbar-profile",
36067
- className: styles$1["toolbar-button--avatar"]
36296
+ className: styles$2["toolbar-button--avatar"]
36068
36297
  }, /*#__PURE__*/React.createElement(Avatar, {
36069
36298
  env: env,
36070
36299
  url: userAvatarPath
36071
36300
  })), /*#__PURE__*/React.createElement(ToolbarButton, {
36072
- className: styles$1["toolbar-button--home"],
36301
+ className: styles$2["toolbar-button--home"],
36073
36302
  onClick: () => closeDrawer()
36074
36303
  }, /*#__PURE__*/React.createElement(tapestryReact.Icon, {
36075
36304
  name: "toolbar.planningCenter",
@@ -36106,7 +36335,7 @@ const ToolbarButton = ({
36106
36335
  }) => {
36107
36336
  return /*#__PURE__*/React.createElement("button", {
36108
36337
  type: "button",
36109
- className: classnames(styles$1["toolbar-button"], className, current && styles$1["is-current"]),
36338
+ className: classnames(styles$2["toolbar-button"], className, current && styles$2["is-current"]),
36110
36339
  onClick: onClick,
36111
36340
  "data-toolbar-link": "true",
36112
36341
  ...restProps