@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.
@@ -27,7 +27,7 @@
27
27
  var people__namespace = /*#__PURE__*/_interopNamespaceDefault(people);
28
28
  var toolbar__namespace = /*#__PURE__*/_interopNamespaceDefault(toolbar);
29
29
 
30
- var styles$l = {"accessibility-toolbar":"accessibility-toolbar-jgjUG"};
30
+ var styles$m = {"accessibility-toolbar":"accessibility-toolbar-jgjUG"};
31
31
 
32
32
  const AccessibilityToolbar = ({
33
33
  children
@@ -39,7 +39,7 @@
39
39
  (_a = document.getElementById("main-content")) === null || _a === void 0 ? void 0 : _a.focus();
40
40
  };
41
41
  return /*#__PURE__*/React.createElement("div", {
42
- className: styles$l["accessibility-toolbar"]
42
+ className: styles$m["accessibility-toolbar"]
43
43
  }, /*#__PURE__*/React.createElement(tapestry.Link, {
44
44
  href: "#main-content",
45
45
  kind: "primary",
@@ -358,11 +358,11 @@
358
358
  fill: fill
359
359
  })));
360
360
 
361
- var styles$k = {"announcement":"announcement-pI5tM","announcement--platform":"announcement--platform-EcUn2","announcement--promo":"announcement--promo-FarUk","announcement-icon":"announcement-icon-Ov39-","announcement-content":"announcement-content-q2ad6","dismiss":"dismiss-wUAoU","dismiss--promo":"dismiss--promo-7zvO4"};
361
+ var styles$l = {"announcement":"announcement-pI5tM","announcement--platform":"announcement--platform-EcUn2","announcement--promo":"announcement--promo-FarUk","announcement-icon":"announcement-icon-Ov39-","announcement-content":"announcement-content-q2ad6","dismiss":"dismiss-wUAoU","dismiss--promo":"dismiss--promo-7zvO4"};
362
362
 
363
363
  function AnnouncementIcon() {
364
364
  return /*#__PURE__*/React.createElement("svg", {
365
- className: styles$k["announcement-icon"],
365
+ className: styles$l["announcement-icon"],
366
366
  width: "16",
367
367
  height: "16",
368
368
  viewBox: "0 0 16 16",
@@ -487,7 +487,7 @@
487
487
  } = this.props;
488
488
  const isPromo = variant === "promo";
489
489
  return /*#__PURE__*/React.createElement("div", {
490
- className: classnames(styles$k["announcement"], styles$k[`announcement--${variant !== null && variant !== void 0 ? variant : "platform"}`]),
490
+ className: classnames(styles$l["announcement"], styles$l[`announcement--${variant !== null && variant !== void 0 ? variant : "platform"}`]),
491
491
  "data-announcement-variant": variant !== null && variant !== void 0 ? variant : "platform",
492
492
  style: isPromo ? {
493
493
  backgroundImage: promoBackgroundUrl,
@@ -496,7 +496,7 @@
496
496
  backgroundSize: "cover"
497
497
  } : undefined
498
498
  }, !isPromo && /*#__PURE__*/React.createElement(AnnouncementIcon, null), /*#__PURE__*/React.createElement("div", {
499
- className: styles$k["announcement-content"],
499
+ className: styles$l["announcement-content"],
500
500
  style: style,
501
501
  ...platformProps
502
502
  }));
@@ -545,7 +545,7 @@
545
545
  const isPromo = variant === "promo";
546
546
  return /*#__PURE__*/React.createElement("button", {
547
547
  type: "button",
548
- className: classnames(styles$k.dismiss, isPromo && styles$k["dismiss--promo"]),
548
+ className: classnames(styles$l.dismiss, isPromo && styles$l["dismiss--promo"]),
549
549
  ...props
550
550
  }, /*#__PURE__*/React.createElement(X, {
551
551
  fill: isPromo ? "currentColor" : "white",
@@ -625,9 +625,9 @@
625
625
  ...platformProps
626
626
  } = this.props;
627
627
  return /*#__PURE__*/React.createElement("div", {
628
- className: styles$k["announcement"]
628
+ className: styles$l["announcement"]
629
629
  }, /*#__PURE__*/React.createElement(AnnouncementIcon, null), /*#__PURE__*/React.createElement("div", {
630
- className: styles$k["announcement-content"],
630
+ className: styles$l["announcement-content"],
631
631
  style: style,
632
632
  ...platformProps
633
633
  }));
@@ -638,7 +638,7 @@
638
638
  }) {
639
639
  return /*#__PURE__*/React.createElement("button", {
640
640
  type: "button",
641
- className: styles$k.dismiss,
641
+ className: styles$l.dismiss,
642
642
  ...props
643
643
  }, /*#__PURE__*/React.createElement(X, {
644
644
  fill: "white",
@@ -1760,7 +1760,7 @@
1760
1760
  })) : (/*#__PURE__*/React.createElement("div", null, "appName not supported")));
1761
1761
  }
1762
1762
 
1763
- var styles$j = {"icon-slot":"icon-slot-m3ZIk","name":"name-UNqbW"};
1763
+ var styles$k = {"icon-slot":"icon-slot-m3ZIk","name":"name-UNqbW"};
1764
1764
 
1765
1765
  const AppRow = ({
1766
1766
  appName,
@@ -1769,24 +1769,24 @@
1769
1769
  }) => {
1770
1770
  if (appName === "Accounts") {
1771
1771
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
1772
- className: styles$j["icon-slot"]
1772
+ className: styles$k["icon-slot"]
1773
1773
  }, /*#__PURE__*/React.createElement(MonoAppIcon, {
1774
1774
  appName: "Accounts",
1775
1775
  colors: colors,
1776
1776
  size: iconSize
1777
1777
  })), /*#__PURE__*/React.createElement("span", {
1778
- className: styles$j.name
1778
+ className: styles$k.name
1779
1779
  }, "Account settings"));
1780
1780
  }
1781
1781
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(ColorAppIcon, {
1782
1782
  appName: appName.replace(/[\s-]/, ""),
1783
1783
  size: iconSize
1784
1784
  }), /*#__PURE__*/React.createElement("span", {
1785
- className: styles$j.name
1785
+ className: styles$k.name
1786
1786
  }, appName));
1787
1787
  };
1788
1788
 
1789
- var styles$i = {"button":"button-XMbeQ","label":"label-884of","chevron":"chevron-RRg9k"};
1789
+ var styles$j = {"button":"button-XMbeQ","label":"label-884of","chevron":"chevron-RRg9k"};
1790
1790
 
1791
1791
  const AppsButton = /*#__PURE__*/React.forwardRef(({
1792
1792
  appName,
@@ -1798,10 +1798,10 @@
1798
1798
  ...restProps,
1799
1799
  ref: ref,
1800
1800
  "aria-label": `App switcher: ${appName}`,
1801
- className: styles$i.button,
1801
+ className: styles$j.button,
1802
1802
  kind: "neutral-inline",
1803
1803
  label: /*#__PURE__*/React.createElement("span", {
1804
- className: styles$i.label
1804
+ className: styles$j.label
1805
1805
  }, isAccounts ? "Account settings" : appName),
1806
1806
  prefix: isAccounts ? (/*#__PURE__*/React.createElement(MonoAppIcon, {
1807
1807
  appName: "Accounts",
@@ -1812,13 +1812,13 @@
1812
1812
  size: 22
1813
1813
  })),
1814
1814
  suffix: /*#__PURE__*/React.createElement(DisclosureChevronIcon, {
1815
- className: `${styles$i.chevron} symbol`
1815
+ className: `${styles$j.chevron} symbol`
1816
1816
  })
1817
1817
  });
1818
1818
  });
1819
1819
  AppsButton.displayName = "AppsButton";
1820
1820
 
1821
- var styles$h = {"nav":"nav-y-6ML","popover-content":"popover-content-jOAAY","list":"list-jUxvp","link":"link-LMp2h","current":"current-4U4VL","accounts-item":"accounts-item-erJW0","accounts-link":"accounts-link-4jwPW"};
1821
+ var styles$i = {"nav":"nav-y-6ML","popover-content":"popover-content-jOAAY","list":"list-jUxvp","link":"link-LMp2h","current":"current-4U4VL","accounts-item":"accounts-item-erJW0","accounts-link":"accounts-link-4jwPW"};
1822
1822
 
1823
1823
  const AppsMenu = ({
1824
1824
  appName,
@@ -1839,8 +1839,8 @@
1839
1839
  }, /*#__PURE__*/React.createElement("a", {
1840
1840
  "aria-current": isCurrent ? "page" : undefined,
1841
1841
  "aria-label": name,
1842
- className: classnames(styles$h.link, {
1843
- [styles$h.current]: isCurrent
1842
+ className: classnames(styles$i.link, {
1843
+ [styles$i.current]: isCurrent
1844
1844
  }),
1845
1845
  "data-pendo": `app-switcher--change-app--${name}`,
1846
1846
  "data-turbolinks": false,
@@ -1856,22 +1856,22 @@
1856
1856
  colors: colors
1857
1857
  })), /*#__PURE__*/React.createElement(tapestry.PopoverContent, {
1858
1858
  "aria-label": "Switch app",
1859
- className: styles$h["popover-content"],
1859
+ className: styles$i["popover-content"],
1860
1860
  placement: "bottom start"
1861
1861
  }, /*#__PURE__*/React.createElement("nav", {
1862
- className: styles$h.nav,
1862
+ className: styles$i.nav,
1863
1863
  "data-pendo": "app-switcher"
1864
1864
  }, /*#__PURE__*/React.createElement("ul", {
1865
- className: styles$h.list,
1865
+ className: styles$i.list,
1866
1866
  role: "list"
1867
1867
  }, homeProduct && renderProductLink(homeProduct.name), restProducts.map(({
1868
1868
  name
1869
1869
  }) => renderProductLink(name)), accountsProduct && (/*#__PURE__*/React.createElement("li", {
1870
- className: styles$h["accounts-item"]
1870
+ className: styles$i["accounts-item"]
1871
1871
  }, /*#__PURE__*/React.createElement("a", {
1872
1872
  "aria-current": appName === accountsProduct.name ? "page" : undefined,
1873
1873
  "aria-label": "Account settings",
1874
- className: classnames(styles$h.link, styles$h["accounts-link"]),
1874
+ className: classnames(styles$i.link, styles$i["accounts-link"]),
1875
1875
  "data-pendo": "app-switcher--change-app--Accounts",
1876
1876
  "data-turbolinks": false,
1877
1877
  href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(accountsProduct.name)
@@ -2079,7 +2079,7 @@
2079
2079
  })) : (/*#__PURE__*/React.createElement("div", null, "appName not supported"));
2080
2080
  }
2081
2081
 
2082
- var styles$g = {"unbutton":"unbutton-lEbTR"};
2082
+ var styles$h = {"unbutton":"unbutton-lEbTR"};
2083
2083
 
2084
2084
  function Unbutton({
2085
2085
  className,
@@ -2088,7 +2088,7 @@
2088
2088
  ...nativeProps
2089
2089
  }) {
2090
2090
  return /*#__PURE__*/React.createElement("button", {
2091
- className: classnames(styles$g.unbutton, className),
2091
+ className: classnames(styles$h.unbutton, className),
2092
2092
  style: {
2093
2093
  ...style
2094
2094
  },
@@ -2097,7 +2097,7 @@
2097
2097
  });
2098
2098
  }
2099
2099
 
2100
- var styles$f = {"button":"button-wsipc","button-accounts":"button-accounts-zQmBM","chevron":"chevron-obyBF","is-expanded":"is-expanded-PoCQ9"};
2100
+ var styles$g = {"button":"button-wsipc","button-accounts":"button-accounts-zQmBM","chevron":"chevron-obyBF","is-expanded":"is-expanded-PoCQ9"};
2101
2101
 
2102
2102
  const LegacyAppsButton = ({
2103
2103
  appName,
@@ -2112,8 +2112,8 @@
2112
2112
  "aria-haspopup": "true",
2113
2113
  "aria-label": `App switcher: ${appName}`,
2114
2114
  onClick: onClick,
2115
- className: classnames(styles$f.button, {
2116
- [styles$f["is-expanded"]]: expanded
2115
+ className: classnames(styles$g.button, {
2116
+ [styles$g["is-expanded"]]: expanded
2117
2117
  }),
2118
2118
  type: "button",
2119
2119
  style: style,
@@ -2122,7 +2122,7 @@
2122
2122
  appName: "Accounts",
2123
2123
  colors: colors
2124
2124
  }), /*#__PURE__*/React.createElement("div", {
2125
- className: styles$f["button-accounts"]
2125
+ className: styles$g["button-accounts"]
2126
2126
  }, "Account settings"))) : (/*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(ColorAppIcon, {
2127
2127
  appName: appName.replace(/[\s-]/, "")
2128
2128
  }), /*#__PURE__*/React.createElement(MonoAppName, {
@@ -2130,7 +2130,7 @@
2130
2130
  key: "mono-app-name",
2131
2131
  size: 24
2132
2132
  }))), /*#__PURE__*/React.createElement(DisclosureChevronIcon, {
2133
- className: styles$f.chevron,
2133
+ className: styles$g.chevron,
2134
2134
  key: "disclosure-chevron-icon"
2135
2135
  }));
2136
2136
  };
@@ -2263,7 +2263,7 @@
2263
2263
  }
2264
2264
  }
2265
2265
 
2266
- var styles$e = {"popup-root":"popup-root-SeL5u","popup":"popup-nQXYW","is-visible":"is-visible-3DAUP"};
2266
+ var styles$f = {"popup-root":"popup-root-SeL5u","popup":"popup-nQXYW","is-visible":"is-visible-3DAUP"};
2267
2267
 
2268
2268
  const Popup = ({
2269
2269
  className,
@@ -2271,8 +2271,8 @@
2271
2271
  isVisible,
2272
2272
  ...nativeProps
2273
2273
  }) => (/*#__PURE__*/React.createElement(C, {
2274
- className: classnames(styles$e.popup, {
2275
- [styles$e["is-visible"]]: isVisible
2274
+ className: classnames(styles$f.popup, {
2275
+ [styles$f["is-visible"]]: isVisible
2276
2276
  }, className),
2277
2277
  ...nativeProps
2278
2278
  }));
@@ -2280,11 +2280,11 @@
2280
2280
  const PopupRoot = ({
2281
2281
  ...nativeProps
2282
2282
  }) => (/*#__PURE__*/React.createElement("div", {
2283
- className: styles$e["popup-root"],
2283
+ className: styles$f["popup-root"],
2284
2284
  ...nativeProps
2285
2285
  }));
2286
2286
 
2287
- var styles$d = {"popup":"popup-bVhSa"};
2287
+ var styles$e = {"popup":"popup-bVhSa"};
2288
2288
 
2289
2289
  const LegacyAppsMenu = ({
2290
2290
  appName,
@@ -2322,7 +2322,7 @@
2322
2322
  cleanup: !visible,
2323
2323
  component: Outsider,
2324
2324
  onOutsideClick: toggle,
2325
- className: styles$d.popup,
2325
+ className: styles$e.popup,
2326
2326
  isVisible: visible
2327
2327
  }, /*#__PURE__*/React.createElement("menu", {
2328
2328
  ref: menuRef,
@@ -3235,27 +3235,34 @@ true;
3235
3235
  const isMac = () => /Mac|iPhone|iPad|iPod/.test(navigator.platform);
3236
3236
  const hasCommandKey = event => isMac() ? event.metaKey : event.ctrlKey;
3237
3237
  const commandKeyLabel = () => isMac() ? "⌘" : "Ctrl+";
3238
+ const isSearchShortcut = event => event.key.toLowerCase() === "k" && !event.altKey && !event.shiftKey && hasCommandKey(event);
3239
+ // Products opened their own search with `/` before this one replaced it.
3240
+ const isLegacySearchShortcut = event => event.key === "/" && !event.altKey && !event.ctrlKey && !event.metaKey && !isTypingIn(event.target);
3241
+ const isCopyShortcut = event => event.key.toLowerCase() === "c" && !event.altKey && !event.shiftKey && hasCommandKey(event);
3242
+ const isTypingIn = target => target instanceof Element && target.closest('input, select, textarea, [contenteditable]:not([contenteditable="false"])') !== null;
3243
+ const isTypingKey = event => event.key.length === 1 && event.key !== " " && !event.altKey && !event.ctrlKey && !event.metaKey;
3244
+ const verticalStep = key => ({
3245
+ ArrowDown: 1,
3246
+ ArrowUp: -1
3247
+ })[key];
3248
+ const horizontalStep = key => ({
3249
+ ArrowLeft: -1,
3250
+ ArrowRight: 1
3251
+ })[key];
3238
3252
 
3239
- const STORAGE_KEY = "topbar.product_search.last_product";
3240
- function readLastProduct() {
3241
- try {
3242
- return localStorage.getItem(STORAGE_KEY);
3243
- } catch {
3244
- return null;
3245
- }
3246
- }
3247
- function saveLastProduct(slug) {
3248
- try {
3249
- localStorage.setItem(STORAGE_KEY, slug);
3250
- } catch {
3251
- // Storage can be full or blocked; search still works without it.
3252
- }
3253
+ function SearchButton({
3254
+ onClick
3255
+ }) {
3256
+ return /*#__PURE__*/React.createElement("button", {
3257
+ "aria-keyshortcuts": `${isMac() ? "Meta+K" : "Control+K"} /`,
3258
+ "aria-label": "Search",
3259
+ onClick: onClick,
3260
+ type: "button"
3261
+ }, /*#__PURE__*/React.createElement(Spyglass, {
3262
+ fill: "currentColor"
3263
+ }));
3253
3264
  }
3254
3265
 
3255
- var styles$c = {"visuallyHidden":"visuallyHidden-C28ju"};
3256
-
3257
- var styles$b = {"tab":"tab-lQLDx"};
3258
-
3259
3266
  // useId isn't available on React 17, which our peer range still supports.
3260
3267
  let instanceCount$1 = 0;
3261
3268
  function useInstanceId(prefix) {
@@ -3263,58 +3270,237 @@ true;
3263
3270
  return id;
3264
3271
  }
3265
3272
 
3266
- function ProductTabs({
3267
- children,
3268
- onKeyDown,
3269
- onSelect,
3270
- products,
3271
- selected
3272
- }) {
3273
- const id = useInstanceId("product-tabs");
3274
- const tabId = slug => `${id}-tab-${slug}`;
3275
- const tabRefs = React.useRef(new Map());
3276
- const handleKeyDown = event => {
3277
- var _a;
3278
- const offset = {
3279
- ArrowLeft: -1,
3280
- ArrowRight: 1
3281
- }[event.key];
3282
- if (offset) {
3283
- event.preventDefault();
3284
- const index = products.indexOf(selected);
3285
- const next = products[(index + offset + products.length) % products.length];
3286
- onSelect(next.slug);
3287
- (_a = tabRefs.current.get(next.slug)) === null || _a === void 0 ? void 0 : _a.focus();
3273
+ // The versions home-mobile pins, so web and mobile read the same response
3274
+ // shapes. Some endpoints only exist from a version on: Registrations' me,
3275
+ // product_search_results, and pcrl_lookups need 2025-05-01.
3276
+ const API_VERSIONS = {
3277
+ calendar: "2022-07-07",
3278
+ "check-ins": "2025-05-28",
3279
+ giving: "2019-10-18",
3280
+ groups: "2023-07-10",
3281
+ home: "2018-08-01",
3282
+ people: "2023-03-21",
3283
+ publishing: "2024-03-25",
3284
+ registrations: "2025-05-01",
3285
+ services: "2018-11-01"
3286
+ };
3287
+ // For a `/~api/{product}/v2/...` URL, the header that pins its product's
3288
+ // version.
3289
+ function apiVersionHeaders(url) {
3290
+ const [,, product] = new URL(url).pathname.split("/");
3291
+ const version = API_VERSIONS[product];
3292
+ if (version) {
3293
+ return {
3294
+ "X-PCO-API-Version": version
3295
+ };
3296
+ } else {
3297
+ return {};
3298
+ }
3299
+ }
3300
+
3301
+ const CACHE_TTL_MS = 60000;
3302
+ const MAX_ENTRIES$1 = 50;
3303
+ const cache = new Map();
3304
+ function cachedRequest(url) {
3305
+ const cached = cache.get(url);
3306
+ if (cached && cached.expiresAt > Date.now()) {
3307
+ // Re-inserting keeps the Map in recently-used order for eviction.
3308
+ cache.delete(url);
3309
+ cache.set(url, cached);
3310
+ return cached.promise;
3311
+ } else {
3312
+ return fetchIntoCache(url);
3313
+ }
3314
+ }
3315
+ function fetchIntoCache(url) {
3316
+ const promise = apiRequest(defaultFetch, url, {
3317
+ headers: apiVersionHeaders(url)
3318
+ }).then(({
3319
+ json,
3320
+ ok
3321
+ }) => {
3322
+ if (!ok || json === null) throw new Error(`Request failed: ${url}`);
3323
+ return json;
3324
+ });
3325
+ // In-flight entries never expire, so every caller shares the one fetch.
3326
+ const entry = {
3327
+ expiresAt: Infinity,
3328
+ promise
3329
+ };
3330
+ cache.set(url, entry);
3331
+ promise.then(() => entry.expiresAt = Date.now() + CACHE_TTL_MS, () => cache.get(url) === entry && cache.delete(url));
3332
+ if (cache.size > MAX_ENTRIES$1) cache.delete(cache.keys().next().value);
3333
+ return promise;
3334
+ }
3335
+ function useCachedRequest(url) {
3336
+ const [settled, setSettled] = React.useState();
3337
+ React.useEffect(() => {
3338
+ if (url) {
3339
+ let current = true;
3340
+ cachedRequest(url).then(data => current && setSettled({
3341
+ data,
3342
+ url
3343
+ }), () => current && setSettled({
3344
+ failed: true,
3345
+ url
3346
+ }));
3347
+ return () => {
3348
+ current = false;
3349
+ };
3288
3350
  } else {
3289
- onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
3351
+ return undefined;
3352
+ }
3353
+ }, [url]);
3354
+ if (!url) {
3355
+ return {
3356
+ status: "idle"
3357
+ };
3358
+ } else if ((settled === null || settled === void 0 ? void 0 : settled.url) !== url) {
3359
+ return {
3360
+ data: settled && "data" in settled ? settled.data : undefined,
3361
+ status: "loading"
3362
+ };
3363
+ } else if ("data" in settled) {
3364
+ return {
3365
+ data: settled.data,
3366
+ status: "done"
3367
+ };
3368
+ } else {
3369
+ return {
3370
+ status: "error"
3371
+ };
3372
+ }
3373
+ }
3374
+ // One request per URL, for callers that fan out to a varying number of
3375
+ // endpoints. Unlike `useCachedRequest`, a URL that hasn't settled yet has no
3376
+ // earlier data to fall back on.
3377
+ function useCachedRequests(urls) {
3378
+ const [settled, setSettled] = React.useState({});
3379
+ // Keyed by value because each settle re-renders the caller, and a caller
3380
+ // that builds a fresh `urls` array on render would otherwise loop forever.
3381
+ const key = urls.join("\n");
3382
+ React.useEffect(() => {
3383
+ let current = true;
3384
+ const settle = result => current && setSettled(prev => ({
3385
+ ...prev,
3386
+ [result.url]: result
3387
+ }));
3388
+ for (const url of key.split("\n").filter(Boolean)) {
3389
+ cachedRequest(url).then(data => settle({
3390
+ data,
3391
+ url
3392
+ }), () => settle({
3393
+ failed: true,
3394
+ url
3395
+ }));
3290
3396
  }
3397
+ return () => {
3398
+ current = false;
3399
+ };
3400
+ }, [key]);
3401
+ return urls.map(url => {
3402
+ const result = settled[url];
3403
+ if (!result) {
3404
+ return {
3405
+ status: "loading"
3406
+ };
3407
+ } else if ("data" in result) {
3408
+ return {
3409
+ data: result.data,
3410
+ status: "done"
3411
+ };
3412
+ } else {
3413
+ return {
3414
+ status: "error"
3415
+ };
3416
+ }
3417
+ });
3418
+ }
3419
+
3420
+ function productSearchUrl(product, query) {
3421
+ const params = new URLSearchParams({
3422
+ per_page: "100",
3423
+ "where[term]": query
3424
+ });
3425
+ return `${window.location.origin}/~api/${product}/v2/product_search_results?${params}`;
3426
+ }
3427
+ function groupByType({
3428
+ data,
3429
+ meta
3430
+ }) {
3431
+ var _a, _b;
3432
+ const typeOrder = (_a = meta === null || meta === void 0 ? void 0 : meta.type_order) !== null && _a !== void 0 ? _a : [];
3433
+ const groups = new Map();
3434
+ for (const result of data) {
3435
+ const {
3436
+ type
3437
+ } = result.attributes;
3438
+ if (!groups.has(type)) groups.set(type, {
3439
+ results: [],
3440
+ type
3441
+ });
3442
+ (_b = groups.get(type)) === null || _b === void 0 ? void 0 : _b.results.push(result);
3443
+ }
3444
+ const rank = type => {
3445
+ const index = typeOrder.indexOf(type);
3446
+ return index === -1 ? typeOrder.length : index;
3291
3447
  };
3292
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
3293
- "aria-label": "Product",
3294
- onKeyDown: handleKeyDown,
3295
- role: "tablist"
3296
- }, products.map(product => (/*#__PURE__*/React.createElement("button", {
3297
- "aria-controls": `${id}-panel`,
3298
- "aria-selected": product === selected,
3299
- className: styles$b.tab,
3300
- id: tabId(product.slug),
3301
- key: product.slug,
3302
- onClick: () => onSelect(product.slug),
3303
- ref: element => {
3304
- if (element) {
3305
- tabRefs.current.set(product.slug, element);
3306
- } else {
3307
- tabRefs.current.delete(product.slug);
3308
- }
3309
- },
3310
- role: "tab",
3311
- tabIndex: product === selected ? 0 : -1,
3312
- type: "button"
3313
- }, product.appName)))), /*#__PURE__*/React.createElement("div", {
3314
- "aria-labelledby": tabId(selected.slug),
3315
- id: `${id}-panel`,
3316
- role: "tabpanel"
3317
- }, children));
3448
+ return [...groups.values()].sort((a, b) => rank(a.type) - rank(b.type));
3449
+ }
3450
+
3451
+ function useDebouncedValue(value, delay) {
3452
+ const [debounced, setDebounced] = React.useState(value);
3453
+ React.useEffect(() => {
3454
+ const timeout = setTimeout(() => setDebounced(value), delay);
3455
+ return () => clearTimeout(timeout);
3456
+ }, [value, delay]);
3457
+ return debounced;
3458
+ }
3459
+
3460
+ const SEARCH_DEBOUNCE_MS = 300;
3461
+ function useProductSearch(product, term) {
3462
+ const query = term.trim();
3463
+ const debouncedQuery = useDebouncedValue(query, SEARCH_DEBOUNCE_MS);
3464
+ const request = useCachedRequest(debouncedQuery ? productSearchUrl(product, debouncedQuery) : null);
3465
+ const response = "data" in request ? request.data : undefined;
3466
+ const settledProduct = useSettledProduct(product, request.status);
3467
+ // While a request loads, the previous response stays on screen, but only
3468
+ // if it was for this product: People results must never show under the
3469
+ // Services tab.
3470
+ const visibleResponse = request.status === "done" || settledProduct === product ? response : undefined;
3471
+ const groups = React.useMemo(() => visibleResponse && groupByType(visibleResponse), [visibleResponse]);
3472
+ if (!query) {
3473
+ return {
3474
+ status: "idle"
3475
+ };
3476
+ } else if (query !== debouncedQuery) {
3477
+ return {
3478
+ groups,
3479
+ status: "loading"
3480
+ };
3481
+ } else if (request.status === "done") {
3482
+ return {
3483
+ groups: groups !== null && groups !== void 0 ? groups : [],
3484
+ status: "done"
3485
+ };
3486
+ } else if (request.status === "error") {
3487
+ return {
3488
+ status: "error"
3489
+ };
3490
+ } else {
3491
+ return {
3492
+ groups,
3493
+ status: "loading"
3494
+ };
3495
+ }
3496
+ }
3497
+ // The product the most recent finished request was for.
3498
+ function useSettledProduct(product, status) {
3499
+ const [settledProduct, setSettledProduct] = React.useState();
3500
+ React.useEffect(() => {
3501
+ if (status === "done") setSettledProduct(product);
3502
+ }, [product, status]);
3503
+ return settledProduct;
3318
3504
  }
3319
3505
 
3320
3506
  function parseValue(element) {
@@ -3383,10 +3569,14 @@ true;
3383
3569
  return shim.useSyncExternalStore(recentlyVisitableMetadataStore.subscribe, () => recentlyVisitableMetadataStore.getState().metadata);
3384
3570
  }
3385
3571
 
3386
- const MAX_ENTRIES$1 = 20;
3387
- const FORMAT_VERSION = 1;
3572
+ // Entries match the ones home-mobile stores, so one list can sync across web
3573
+ // and mobile.
3574
+ const ENTRY_VERSION = 3;
3575
+ const MAX_ENTRIES = 50;
3388
3576
  const MAX_TEXT_LENGTH = 200;
3389
3577
  const PRODUCTS = ["accounts", "calendar", "check-ins", "giving", "groups", "home", "login", "people", "publishing", "registrations", "services"];
3578
+ // Login has no pages worth returning to.
3579
+ const APPS = PRODUCTS.filter(product => product !== "login");
3390
3580
  function isId(value) {
3391
3581
  return value !== "" && Array.from(value).every(character => character >= "0" && character <= "9");
3392
3582
  }
@@ -3395,26 +3585,47 @@ true;
3395
3585
  const segments = value.split("/");
3396
3586
  return segments.length === 4 && PRODUCTS.some(product => product === segments[0]) && isId(segments[3]);
3397
3587
  }
3588
+ function isApp(value) {
3589
+ return APPS.some(app => app === value);
3590
+ }
3591
+ // Links are built by resolving the path against its app's origin, so a stored
3592
+ // path must not be able to resolve anywhere else (`//evil.com`, `/\evil.com`).
3593
+ function isPath(value) {
3594
+ const origin = "https://app.invalid";
3595
+ try {
3596
+ return typeof value === "string" && value.startsWith("/") && new URL(value, origin).origin === origin;
3597
+ } catch {
3598
+ return false;
3599
+ }
3600
+ }
3398
3601
  function isNonBlankString(value) {
3399
3602
  return typeof value === "string" && value.trim() !== "";
3400
3603
  }
3604
+ function isOptionalString(value) {
3605
+ return value === undefined || typeof value === "string";
3606
+ }
3401
3607
  function isRecord(value) {
3402
3608
  return typeof value === "object" && value !== null && !Array.isArray(value);
3403
3609
  }
3610
+ function isPageMetadata(value) {
3611
+ return value === undefined || isRecord(value) && isOptionalString(value.details) && isOptionalString(value.type);
3612
+ }
3613
+ // The query string is part of the link but not of the page's identity, so
3614
+ // revisiting with other filters moves the existing entry instead of adding one.
3404
3615
  function entryKey(entry) {
3405
3616
  if (entry.type === "resource") {
3406
3617
  return `resource:${entry.pcrl}`;
3407
3618
  } else {
3408
- return `page:${entry.product}:${entry.path}`;
3619
+ return `page:${entry.app}:${entry.path.split("?")[0]}`;
3409
3620
  }
3410
3621
  }
3411
3622
  function isEntry(value) {
3412
- if (!isRecord(value)) {
3623
+ if (!isRecord(value) || value.version !== ENTRY_VERSION || typeof value.visitedAt !== "string") {
3413
3624
  return false;
3414
3625
  } else if (value.type === "resource") {
3415
3626
  return typeof value.pcrl === "string" && isPcrl(value.pcrl);
3416
3627
  } else if (value.type === "page") {
3417
- return isNonBlankString(value.product) && isNonBlankString(value.title) && isNonBlankString(value.path) && typeof value.details === "string";
3628
+ return isApp(value.app) && isPath(value.path) && isNonBlankString(value.title) && isPageMetadata(value.metadata);
3418
3629
  } else {
3419
3630
  return false;
3420
3631
  }
@@ -3423,58 +3634,114 @@ true;
3423
3634
  function clampText(value) {
3424
3635
  return Array.from(value).slice(0, MAX_TEXT_LENGTH).join("");
3425
3636
  }
3426
- function clampEntry(entry) {
3427
- if (entry.type === "resource") {
3428
- return entry;
3637
+ function clampMetadata({
3638
+ details,
3639
+ type
3640
+ }) {
3641
+ return {
3642
+ ...(details && {
3643
+ details: clampText(details)
3644
+ }),
3645
+ ...(type && {
3646
+ type: clampText(type)
3647
+ })
3648
+ };
3649
+ }
3650
+ function clampVisit(visit) {
3651
+ if (visit.type === "resource") {
3652
+ return visit;
3429
3653
  } else {
3430
3654
  return {
3431
- ...entry,
3432
- details: clampText(entry.details),
3433
- title: clampText(entry.title)
3655
+ ...visit,
3656
+ ...(visit.metadata && {
3657
+ metadata: clampMetadata(visit.metadata)
3658
+ }),
3659
+ title: clampText(visit.title.trim())
3434
3660
  };
3435
3661
  }
3436
3662
  }
3663
+ function toEntry(visit, visitedAt) {
3664
+ return {
3665
+ ...clampVisit(visit),
3666
+ version: ENTRY_VERSION,
3667
+ visitedAt: visitedAt.toISOString()
3668
+ };
3669
+ }
3437
3670
  // Recents are disposable: anything unexpected reads as an empty list (or, for
3438
3671
  // a single bad entry, a list without it) rather than throwing.
3439
- function parseRecentlyVisited(value, personId) {
3672
+ function parseRecentlyVisited(value) {
3440
3673
  let parsed = null;
3441
3674
  try {
3442
3675
  parsed = JSON.parse(value !== null && value !== void 0 ? value : "null");
3443
3676
  } catch {
3444
3677
  parsed = null;
3445
3678
  }
3446
- if (isRecord(parsed) && parsed.v === FORMAT_VERSION && parsed.person === personId && Array.isArray(parsed.entries)) {
3447
- return parsed.entries.filter(isEntry);
3679
+ if (Array.isArray(parsed)) {
3680
+ return parsed.filter(isEntry);
3448
3681
  } else {
3449
3682
  return [];
3450
3683
  }
3451
3684
  }
3452
- function addVisit(entries, visit) {
3453
- const key = entryKey(visit);
3454
- return [clampEntry(visit), ...entries.filter(entry => entryKey(entry) !== key)].slice(0, MAX_ENTRIES$1);
3455
- }
3456
- function serializeRecentlyVisited(entries, personId) {
3457
- return JSON.stringify({
3458
- entries,
3459
- person: personId,
3460
- v: FORMAT_VERSION
3461
- });
3685
+ function addVisit(entries, entry) {
3686
+ const key = entryKey(entry);
3687
+ return [entry, ...entries.filter(existing => entryKey(existing) !== key)].slice(0, MAX_ENTRIES);
3462
3688
  }
3463
3689
 
3464
- // The product whose URL, in this environment, has the current page's host.
3690
+ // The app whose URL, in this environment, has the current page's host.
3465
3691
  // Reversing pcoUrl handles every environment @planningcenter/url knows about,
3466
- // and guarantees the stored product rebuilds a working link.
3467
- function currentProduct() {
3692
+ // and guarantees the stored app rebuilds a working link.
3693
+ function currentApp() {
3468
3694
  var _a;
3469
- return (_a = PRODUCTS.find(product => new URL(pcoUrl$1.pcoUrl(product)).hostname === window.location.hostname)) !== null && _a !== void 0 ? _a : null;
3695
+ return (_a = APPS.find(app => new URL(pcoUrl$1.pcoUrl(app)).hostname === window.location.hostname)) !== null && _a !== void 0 ? _a : null;
3470
3696
  }
3471
- function detailsFromMetadata(metadata) {
3697
+ function isFilled(value) {
3698
+ return typeof value === "string" && value !== "";
3699
+ }
3700
+ // The tag's metadata is product-supplied JSON; only its string `type` and
3701
+ // `details` carry over, and an entry without either has no metadata at all.
3702
+ function metadataFromPage(metadata) {
3472
3703
  var _a;
3473
3704
  try {
3474
- const details = (_a = JSON.parse(metadata !== null && metadata !== void 0 ? metadata : "{}")) === null || _a === void 0 ? void 0 : _a.details;
3475
- return typeof details === "string" ? details : "";
3705
+ const {
3706
+ details,
3707
+ type
3708
+ } = (_a = JSON.parse(metadata !== null && metadata !== void 0 ? metadata : "{}")) !== null && _a !== void 0 ? _a : {};
3709
+ if (isFilled(details) || isFilled(type)) {
3710
+ return {
3711
+ ...(isFilled(details) && {
3712
+ details
3713
+ }),
3714
+ ...(isFilled(type) && {
3715
+ type
3716
+ })
3717
+ };
3718
+ } else {
3719
+ return undefined;
3720
+ }
3476
3721
  } catch {
3477
- return "";
3722
+ return undefined;
3723
+ }
3724
+ }
3725
+ // Staff impersonating someone browse under `/O<org id>/P<person id>/…`.
3726
+ function withoutImpersonation(segments) {
3727
+ const [org = "", person = ""] = segments;
3728
+ if (org.startsWith("O") && isId(org.slice(1)) && person.startsWith("P") && isId(person.slice(1))) {
3729
+ return segments.slice(2);
3730
+ } else {
3731
+ return segments;
3732
+ }
3733
+ }
3734
+ // Normalized the way home-mobile does it, so a page visited on web and on
3735
+ // mobile is one entry once the list syncs.
3736
+ function pathFromLocation({
3737
+ pathname,
3738
+ search
3739
+ }) {
3740
+ const segments = withoutImpersonation(pathname.split("/").filter(Boolean));
3741
+ if (segments.length === 0) {
3742
+ return "/";
3743
+ } else {
3744
+ return `/${segments.join("/")}${search.length > 1 ? search : ""}`;
3478
3745
  }
3479
3746
  }
3480
3747
  // pco:resource is the more specific signal, so it wins when both tags are
@@ -3484,17 +3751,20 @@ true;
3484
3751
  resource,
3485
3752
  title
3486
3753
  }) {
3487
- const product = currentProduct();
3754
+ const app = currentApp();
3488
3755
  if (resource && isPcrl(resource)) {
3489
3756
  return {
3490
3757
  pcrl: resource,
3491
3758
  type: "resource"
3492
3759
  };
3493
- } else if (title && title.trim() !== "" && product) {
3760
+ } else if (title && title.trim() !== "" && app) {
3761
+ const pageMetadata = metadataFromPage(metadata);
3494
3762
  return {
3495
- details: detailsFromMetadata(metadata),
3496
- path: window.location.pathname,
3497
- product,
3763
+ app,
3764
+ ...(pageMetadata && {
3765
+ metadata: pageMetadata
3766
+ }),
3767
+ path: pathFromLocation(window.location),
3498
3768
  title,
3499
3769
  type: "page"
3500
3770
  };
@@ -3542,15 +3812,16 @@ true;
3542
3812
  });
3543
3813
  function readRecentlyVisited(scope) {
3544
3814
  try {
3545
- return parseRecentlyVisited(localStorage.getItem(storageKey(scope)), scope.personId);
3815
+ return parseRecentlyVisited(localStorage.getItem(storageKey(scope)));
3546
3816
  } catch {
3547
3817
  return [];
3548
3818
  }
3549
3819
  }
3550
3820
  function recordVisit(visit, scope) {
3551
- if (isEntry(visit)) {
3821
+ const entry = toEntry(visit, new Date());
3822
+ if (isEntry(entry)) {
3552
3823
  try {
3553
- localStorage.setItem(storageKey(scope), serializeRecentlyVisited(addVisit(readRecentlyVisited(scope), visit), scope.personId));
3824
+ localStorage.setItem(storageKey(scope), JSON.stringify(addVisit(readRecentlyVisited(scope), entry)));
3554
3825
  changes.setState({
3555
3826
  version: changes.getState().version + 1
3556
3827
  });
@@ -3571,153 +3842,6 @@ true;
3571
3842
  }) : [], [env, personId, version]);
3572
3843
  }
3573
3844
 
3574
- // The versions home-mobile pins, so web and mobile read the same response
3575
- // shapes. Some endpoints only exist from a version on: Registrations' me,
3576
- // product_search_results, and pcrl_lookups need 2025-05-01.
3577
- const API_VERSIONS = {
3578
- calendar: "2022-07-07",
3579
- "check-ins": "2025-05-28",
3580
- giving: "2019-10-18",
3581
- groups: "2023-07-10",
3582
- home: "2018-08-01",
3583
- people: "2023-03-21",
3584
- publishing: "2024-03-25",
3585
- registrations: "2025-05-01",
3586
- services: "2018-11-01"
3587
- };
3588
- // For a `/~api/{product}/v2/...` URL, the header that pins its product's
3589
- // version.
3590
- function apiVersionHeaders(url) {
3591
- const [,, product] = new URL(url).pathname.split("/");
3592
- const version = API_VERSIONS[product];
3593
- if (version) {
3594
- return {
3595
- "X-PCO-API-Version": version
3596
- };
3597
- } else {
3598
- return {};
3599
- }
3600
- }
3601
-
3602
- const CACHE_TTL_MS = 60000;
3603
- const MAX_ENTRIES = 50;
3604
- const cache = new Map();
3605
- function cachedRequest(url) {
3606
- const cached = cache.get(url);
3607
- if (cached && cached.expiresAt > Date.now()) {
3608
- // Re-inserting keeps the Map in recently-used order for eviction.
3609
- cache.delete(url);
3610
- cache.set(url, cached);
3611
- return cached.promise;
3612
- } else {
3613
- return fetchIntoCache(url);
3614
- }
3615
- }
3616
- function fetchIntoCache(url) {
3617
- const promise = apiRequest(defaultFetch, url, {
3618
- headers: apiVersionHeaders(url)
3619
- }).then(({
3620
- json,
3621
- ok
3622
- }) => {
3623
- if (!ok || json === null) throw new Error(`Request failed: ${url}`);
3624
- return json;
3625
- });
3626
- // In-flight entries never expire, so every caller shares the one fetch.
3627
- const entry = {
3628
- expiresAt: Infinity,
3629
- promise
3630
- };
3631
- cache.set(url, entry);
3632
- promise.then(() => entry.expiresAt = Date.now() + CACHE_TTL_MS, () => cache.get(url) === entry && cache.delete(url));
3633
- if (cache.size > MAX_ENTRIES) cache.delete(cache.keys().next().value);
3634
- return promise;
3635
- }
3636
- function useCachedRequest(url) {
3637
- const [settled, setSettled] = React.useState();
3638
- React.useEffect(() => {
3639
- if (url) {
3640
- let current = true;
3641
- cachedRequest(url).then(data => current && setSettled({
3642
- data,
3643
- url
3644
- }), () => current && setSettled({
3645
- failed: true,
3646
- url
3647
- }));
3648
- return () => {
3649
- current = false;
3650
- };
3651
- } else {
3652
- return undefined;
3653
- }
3654
- }, [url]);
3655
- if (!url) {
3656
- return {
3657
- status: "idle"
3658
- };
3659
- } else if ((settled === null || settled === void 0 ? void 0 : settled.url) !== url) {
3660
- return {
3661
- data: settled && "data" in settled ? settled.data : undefined,
3662
- status: "loading"
3663
- };
3664
- } else if ("data" in settled) {
3665
- return {
3666
- data: settled.data,
3667
- status: "done"
3668
- };
3669
- } else {
3670
- return {
3671
- status: "error"
3672
- };
3673
- }
3674
- }
3675
- // One request per URL, for callers that fan out to a varying number of
3676
- // endpoints. Unlike `useCachedRequest`, a URL that hasn't settled yet has no
3677
- // earlier data to fall back on.
3678
- function useCachedRequests(urls) {
3679
- const [settled, setSettled] = React.useState({});
3680
- // Keyed by value because each settle re-renders the caller, and a caller
3681
- // that builds a fresh `urls` array on render would otherwise loop forever.
3682
- const key = urls.join("\n");
3683
- React.useEffect(() => {
3684
- let current = true;
3685
- const settle = result => current && setSettled(prev => ({
3686
- ...prev,
3687
- [result.url]: result
3688
- }));
3689
- for (const url of key.split("\n").filter(Boolean)) {
3690
- cachedRequest(url).then(data => settle({
3691
- data,
3692
- url
3693
- }), () => settle({
3694
- failed: true,
3695
- url
3696
- }));
3697
- }
3698
- return () => {
3699
- current = false;
3700
- };
3701
- }, [key]);
3702
- return urls.map(url => {
3703
- const result = settled[url];
3704
- if (!result) {
3705
- return {
3706
- status: "loading"
3707
- };
3708
- } else if ("data" in result) {
3709
- return {
3710
- data: result.data,
3711
- status: "done"
3712
- };
3713
- } else {
3714
- return {
3715
- status: "error"
3716
- };
3717
- }
3718
- });
3719
- }
3720
-
3721
3845
  const pcoUrl = pcoUrl$1;
3722
3846
 
3723
3847
  // Resolves resource entries with one pcrl_lookups request per product.
@@ -3808,13 +3932,290 @@ true;
3808
3932
  });
3809
3933
  return `${window.location.origin}/~api/${product}/v2/pcrl_lookups?${params}`;
3810
3934
  }
3811
- // Built from the product rather than the current origin, so it stays right
3812
- // once PCA-409 shares one list across product subdomains.
3935
+ // Built from the app rather than the current origin, so it stays right once
3936
+ // PCA-409 shares one list across product subdomains.
3813
3937
  function pageUrl(entry, env) {
3814
- return `${pcoUrl(env)(entry.product)}${entry.path}`;
3938
+ return new URL(entry.path, pcoUrl(env)(entry.app)).href;
3939
+ }
3940
+
3941
+ function useRecentVisits(env) {
3942
+ const personId = useCurrentPersonId();
3943
+ const entries = useRecentlyVisited({
3944
+ env,
3945
+ personId
3946
+ });
3947
+ const [count, setCount] = React.useState(RECENTLY_VISITED_PAGE_SIZE);
3948
+ const shown = React.useMemo(() => entries.slice(0, count), [entries, count]);
3949
+ const hydrated = useHydratedRecentlyVisited(shown, env);
3950
+ // Resources show up once they're found. Ones that can't be found anymore,
3951
+ // or that the person can no longer see, are left out.
3952
+ const visits = hydrated.filter(isShown);
3953
+ const loading = hydrated.some(({
3954
+ status
3955
+ }) => status === "loading");
3956
+ const showMore = React.useCallback(() => setCount(current => current + RECENTLY_VISITED_PAGE_SIZE), []);
3957
+ return {
3958
+ count,
3959
+ hasMore: entries.length > count,
3960
+ loading,
3961
+ showMore,
3962
+ visits
3963
+ };
3964
+ }
3965
+ function isShown(visit) {
3966
+ return visit.status === "page" || visit.status === "resolved";
3967
+ }
3968
+
3969
+ function open(url, {
3970
+ newTab
3971
+ }) {
3972
+ if (newTab) {
3973
+ window.open(url, "_blank", "noopener");
3974
+ } else {
3975
+ window.location.assign(url);
3976
+ }
3977
+ }
3978
+ // With text selected, ⌘C should copy that text as usual, not the link.
3979
+ function hasSelectedText(element) {
3980
+ var _a, _b;
3981
+ const selectedInField = element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
3982
+ return selectedInField || ((_b = (_a = window.getSelection()) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : "") !== "";
3983
+ }
3984
+
3985
+ const COPIED_FEEDBACK_MS = 1500;
3986
+ // `copiedUrl` stays set briefly after a copy so the UI can confirm it.
3987
+ function useCopyLink() {
3988
+ const [copiedUrl, setCopiedUrl] = React.useState(null);
3989
+ React.useEffect(() => {
3990
+ if (copiedUrl) {
3991
+ const timeout = setTimeout(() => setCopiedUrl(null), COPIED_FEEDBACK_MS);
3992
+ return () => clearTimeout(timeout);
3993
+ } else {
3994
+ return undefined;
3995
+ }
3996
+ }, [copiedUrl]);
3997
+ const copy = React.useCallback(url => {
3998
+ navigator.clipboard.writeText(url).then(() => setCopiedUrl(url));
3999
+ }, []);
4000
+ return {
4001
+ copiedUrl,
4002
+ copy
4003
+ };
4004
+ }
4005
+
4006
+ // Remembering which list the index belongs to puts the highlight back on the
4007
+ // first item whenever a new list replaces it, without an effect to reset it.
4008
+ // A list that grows or fills in under the same `list` keeps its highlight.
4009
+ function useResultNavigation(items, list) {
4010
+ const [highlight, setHighlight] = React.useState({
4011
+ index: 0,
4012
+ list
4013
+ });
4014
+ const highlightedIndex = highlight.list === list ? highlight.index : 0;
4015
+ const highlighted = items[highlightedIndex];
4016
+ const {
4017
+ copiedUrl,
4018
+ copy
4019
+ } = useCopyLink();
4020
+ const highlightIndex = React.useCallback(index => setHighlight(current => current.list === list && current.index === index ? current : {
4021
+ index,
4022
+ list
4023
+ }), [list]);
4024
+ const handleKeyDown = event => {
4025
+ const step = verticalStep(event.key);
4026
+ if (step && items.length > 0) {
4027
+ event.preventDefault();
4028
+ highlightIndex((highlightedIndex + step + items.length) % items.length);
4029
+ } else if (event.key === "Enter" && highlighted && !event.nativeEvent.isComposing) {
4030
+ event.preventDefault();
4031
+ open(highlighted.url, {
4032
+ newTab: hasCommandKey(event)
4033
+ });
4034
+ } else if (isCopyShortcut(event) && highlighted && !hasSelectedText(event.currentTarget)) {
4035
+ event.preventDefault();
4036
+ copy(highlighted.url);
4037
+ }
4038
+ };
4039
+ return {
4040
+ copied: copiedUrl !== null && copiedUrl === (highlighted === null || highlighted === void 0 ? void 0 : highlighted.url),
4041
+ handleKeyDown,
4042
+ highlightIndex,
4043
+ highlighted,
4044
+ highlightedIndex: highlighted ? highlightedIndex : null
4045
+ };
4046
+ }
4047
+
4048
+ function groupSections(groups = []) {
4049
+ return groups.map(({
4050
+ results,
4051
+ type
4052
+ }) => ({
4053
+ heading: type,
4054
+ items: results.map(resultItem),
4055
+ key: type
4056
+ }));
4057
+ }
4058
+ function resultItem({
4059
+ attributes,
4060
+ id
4061
+ }) {
4062
+ return {
4063
+ key: id,
4064
+ subtitle: summarize(attributes.metadata),
4065
+ title: attributes.title,
4066
+ url: attributes.url
4067
+ };
4068
+ }
4069
+ function visitItem(visit) {
4070
+ return {
4071
+ key: entryKey(visit.entry),
4072
+ ...describeVisit(visit)
4073
+ };
4074
+ }
4075
+ function summarize(metadata) {
4076
+ return Object.values(metadata !== null && metadata !== void 0 ? metadata : {}).filter(value => typeof value === "string" && value !== "").join(" · ");
4077
+ }
4078
+ // Unlike search results, recents come from every product, so each one names
4079
+ // its product and type.
4080
+ function describeVisit(visit) {
4081
+ var _a, _b;
4082
+ const subtitle = (...parts) => parts.filter(Boolean).join(" · ");
4083
+ if (visit.status === "page") {
4084
+ const {
4085
+ app,
4086
+ metadata,
4087
+ title
4088
+ } = visit.entry;
4089
+ return {
4090
+ subtitle: subtitle(productName(app), (_a = metadata === null || metadata === void 0 ? void 0 : metadata.type) !== null && _a !== void 0 ? _a : "", (_b = metadata === null || metadata === void 0 ? void 0 : metadata.details) !== null && _b !== void 0 ? _b : ""),
4091
+ title,
4092
+ url: visit.url
4093
+ };
4094
+ } else {
4095
+ const {
4096
+ metadata,
4097
+ product,
4098
+ title,
4099
+ type
4100
+ } = visit.result.attributes;
4101
+ return {
4102
+ subtitle: subtitle(productName(product), type, summarize(metadata)),
4103
+ title,
4104
+ url: visit.url
4105
+ };
4106
+ }
4107
+ }
4108
+ // check-ins → Check-Ins
4109
+ function productName(slug) {
4110
+ return slug.split("-").map(capitalize).join("-");
4111
+ }
4112
+ function capitalize(word) {
4113
+ return word.charAt(0).toUpperCase() + word.slice(1);
4114
+ }
4115
+
4116
+ const STORAGE_KEY = "topbar.product_search.last_product";
4117
+ function readLastProduct() {
4118
+ try {
4119
+ return localStorage.getItem(STORAGE_KEY);
4120
+ } catch {
4121
+ return null;
4122
+ }
4123
+ }
4124
+ function saveLastProduct(slug) {
4125
+ try {
4126
+ localStorage.setItem(STORAGE_KEY, slug);
4127
+ } catch {
4128
+ // Storage can be full or blocked; search still works without it.
4129
+ }
4130
+ }
4131
+ function initialProduct(products) {
4132
+ var _a, _b, _c;
4133
+ const lastProduct = readLastProduct();
4134
+ const bySlug = slug => products.find(product => product.slug === slug);
4135
+ return (_c = (_b = (_a = bySlug(lastProduct)) !== null && _a !== void 0 ? _a : bySlug("people")) !== null && _b !== void 0 ? _b : bySlug("services")) !== null && _c !== void 0 ? _c : products[0];
3815
4136
  }
3816
4137
 
3817
- var styles$a = {"result":"result-WCnxr"};
4138
+ function KeyboardHints({
4139
+ copied
4140
+ }) {
4141
+ const command = commandKeyLabel();
4142
+ return /*#__PURE__*/React.createElement("p", null, "\u2191\u2193 Select \u00B7 \u21B5 Open \u00B7 ", command, "\u21B5 Open in new tab \u00B7 ", command, "C", " ", /*#__PURE__*/React.createElement("span", {
4143
+ role: "status"
4144
+ }, copied ? "Copied!" : "Copy link"), " \u00B7 ", command, "K Open search");
4145
+ }
4146
+
4147
+ var styles$d = {"tab":"tab-dz8ki"};
4148
+
4149
+ function ProductTabs({
4150
+ children,
4151
+ onKeyDown,
4152
+ onSelect,
4153
+ products,
4154
+ selected
4155
+ }) {
4156
+ const id = useInstanceId("product-tabs");
4157
+ const tabId = slug => `${id}-tab-${slug}`;
4158
+ const tabRefs = React.useRef(new Map());
4159
+ const handleKeyDown = event => {
4160
+ var _a;
4161
+ const offset = horizontalStep(event.key);
4162
+ if (offset) {
4163
+ event.preventDefault();
4164
+ const index = products.indexOf(selected);
4165
+ const next = products[(index + offset + products.length) % products.length];
4166
+ onSelect(next.slug);
4167
+ (_a = tabRefs.current.get(next.slug)) === null || _a === void 0 ? void 0 : _a.focus();
4168
+ } else {
4169
+ onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
4170
+ }
4171
+ };
4172
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
4173
+ "aria-label": "Product",
4174
+ onKeyDown: handleKeyDown,
4175
+ role: "tablist"
4176
+ }, products.map(product => (/*#__PURE__*/React.createElement("button", {
4177
+ "aria-controls": `${id}-panel`,
4178
+ "aria-selected": product === selected,
4179
+ className: styles$d.tab,
4180
+ id: tabId(product.slug),
4181
+ key: product.slug,
4182
+ onClick: () => onSelect(product.slug),
4183
+ ref: element => {
4184
+ if (element) {
4185
+ tabRefs.current.set(product.slug, element);
4186
+ } else {
4187
+ tabRefs.current.delete(product.slug);
4188
+ }
4189
+ },
4190
+ role: "tab",
4191
+ tabIndex: product === selected ? 0 : -1,
4192
+ type: "button"
4193
+ }, product.appName)))), /*#__PURE__*/React.createElement("div", {
4194
+ "aria-labelledby": tabId(selected.slug),
4195
+ id: `${id}-panel`,
4196
+ role: "tabpanel"
4197
+ }, children));
4198
+ }
4199
+
4200
+ function EndOfResultsObserver({
4201
+ onVisible
4202
+ }) {
4203
+ const ref = React.useRef(null);
4204
+ React.useEffect(() => {
4205
+ const observer = new IntersectionObserver(([entry]) => {
4206
+ if (entry.isIntersecting) onVisible();
4207
+ });
4208
+ if (ref.current) observer.observe(ref.current);
4209
+ return () => observer.disconnect();
4210
+ }, [onVisible]);
4211
+ return /*#__PURE__*/React.createElement("div", {
4212
+ ref: ref
4213
+ });
4214
+ }
4215
+
4216
+ const resultId = (listboxId, index) => `${listboxId}-result-${index}`;
4217
+
4218
+ var styles$c = {"result":"result-Thq2C"};
3818
4219
 
3819
4220
  function SearchResult({
3820
4221
  highlighted,
@@ -3837,7 +4238,7 @@ true;
3837
4238
  // pointer, and that shouldn't steal the highlight.
3838
4239
  return /*#__PURE__*/React.createElement("a", {
3839
4240
  "aria-selected": highlighted,
3840
- className: styles$a.result,
4241
+ className: styles$c.result,
3841
4242
  href: url,
3842
4243
  id: id,
3843
4244
  onMouseMove: onHighlight,
@@ -3847,21 +4248,129 @@ true;
3847
4248
  }, /*#__PURE__*/React.createElement("div", null, title), /*#__PURE__*/React.createElement("div", null, subtitle));
3848
4249
  }
3849
4250
 
3850
- const resultId = (listboxId, index) => `${listboxId}-result-${index}`;
4251
+ // Sections with a heading render as labelled groups; ones without render
4252
+ // their items straight into the listbox. Indexes run across every section.
4253
+ function ResultListbox({
4254
+ busy,
4255
+ highlightedIndex,
4256
+ id,
4257
+ label,
4258
+ labelledBy,
4259
+ onHighlight,
4260
+ sections
4261
+ }) {
4262
+ let index = 0;
4263
+ const renderItem = item => {
4264
+ const itemIndex = index++;
4265
+ return /*#__PURE__*/React.createElement(SearchResult, {
4266
+ highlighted: itemIndex === highlightedIndex,
4267
+ id: resultId(id, itemIndex),
4268
+ key: item.key,
4269
+ onHighlight: () => onHighlight(itemIndex),
4270
+ subtitle: item.subtitle,
4271
+ title: item.title,
4272
+ url: item.url
4273
+ });
4274
+ };
4275
+ return /*#__PURE__*/React.createElement("div", {
4276
+ "aria-busy": busy,
4277
+ "aria-label": label,
4278
+ "aria-labelledby": labelledBy,
4279
+ id: id,
4280
+ role: "listbox"
4281
+ }, sections.map((section, sectionIndex) => {
4282
+ if (section.heading === undefined) {
4283
+ return /*#__PURE__*/React.createElement(React.Fragment, {
4284
+ key: section.key
4285
+ }, section.items.map(renderItem));
4286
+ }
4287
+ const headingId = `${id}-group-${sectionIndex}`;
4288
+ return /*#__PURE__*/React.createElement("div", {
4289
+ "aria-labelledby": headingId,
4290
+ key: section.key,
4291
+ role: "group"
4292
+ }, /*#__PURE__*/React.createElement("div", {
4293
+ id: headingId
4294
+ }, section.heading), section.items.map(renderItem));
4295
+ }));
4296
+ }
4297
+
4298
+ function RecentlyVisited({
4299
+ count,
4300
+ hasMore,
4301
+ items,
4302
+ loading,
4303
+ showMore,
4304
+ ...navigation
4305
+ }) {
4306
+ const headingId = `${navigation.id}-heading`;
4307
+ if (items.length === 0 && !hasMore && !loading) {
4308
+ return /*#__PURE__*/React.createElement("p", null, "Pages you visit will show up here.");
4309
+ } else {
4310
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
4311
+ id: headingId
4312
+ }, "Recently visited"), /*#__PURE__*/React.createElement(ResultListbox, {
4313
+ labelledBy: headingId,
4314
+ sections: [{
4315
+ items,
4316
+ key: "recents"
4317
+ }],
4318
+ ...navigation
4319
+ }), hasMore && /*#__PURE__*/React.createElement(EndOfResultsObserver, {
4320
+ key: count,
4321
+ onVisible: showMore
4322
+ }));
4323
+ }
4324
+ }
4325
+
4326
+ // A combobox: focus stays here while aria-activedescendant points screen
4327
+ // readers at the highlighted result in the listbox it controls.
4328
+ function SearchInput({
4329
+ expanded,
4330
+ highlightedIndex,
4331
+ inputRef,
4332
+ listboxId,
4333
+ onChange,
4334
+ onFocus,
4335
+ onKeyDown,
4336
+ value
4337
+ }) {
4338
+ return /*#__PURE__*/React.createElement("input", {
4339
+ "aria-activedescendant": highlightedIndex === null ? undefined : resultId(listboxId, highlightedIndex),
4340
+ "aria-autocomplete": "list",
4341
+ "aria-controls": expanded ? listboxId : undefined,
4342
+ "aria-expanded": expanded,
4343
+ "aria-label": "Search",
4344
+ autoFocus: true,
4345
+ onChange: event => onChange(event.target.value),
4346
+ onFocus: onFocus,
4347
+ onKeyDown: onKeyDown,
4348
+ placeholder: "Search",
4349
+ ref: inputRef,
4350
+ role: "combobox",
4351
+ type: "search",
4352
+ value: value
4353
+ });
4354
+ }
4355
+
4356
+ var styles$b = {"visuallyHidden":"visuallyHidden-S9VG5"};
4357
+
3851
4358
  function SearchResults({
3852
- search,
4359
+ label,
4360
+ sections,
4361
+ status,
3853
4362
  term,
3854
4363
  ...navigation
3855
4364
  }) {
3856
- var _a;
3857
- switch (search.status) {
4365
+ switch (status) {
3858
4366
  case "idle":
3859
4367
  return null;
3860
4368
  case "loading":
3861
- if ((_a = search.groups) === null || _a === void 0 ? void 0 : _a.length) {
3862
- return /*#__PURE__*/React.createElement(ResultGroups, {
4369
+ if (sections.length > 0) {
4370
+ return /*#__PURE__*/React.createElement(ResultListbox, {
3863
4371
  busy: true,
3864
- groups: search.groups,
4372
+ label: label,
4373
+ sections: sections,
3865
4374
  ...navigation
3866
4375
  });
3867
4376
  }
@@ -3873,324 +4382,167 @@ true;
3873
4382
  role: "alert"
3874
4383
  }, "Search isn\u2019t working right now. Try again.");
3875
4384
  case "done":
3876
- if (search.groups.length === 0) {
4385
+ if (sections.length === 0) {
3877
4386
  return /*#__PURE__*/React.createElement("p", {
3878
4387
  role: "status"
3879
4388
  }, "No results for \u201C", term.trim(), "\u201D");
3880
4389
  }
3881
- return /*#__PURE__*/React.createElement(ResultGroups, {
3882
- groups: search.groups,
4390
+ return /*#__PURE__*/React.createElement(ResultListbox, {
4391
+ busy: false,
4392
+ label: label,
4393
+ sections: sections,
3883
4394
  ...navigation
3884
4395
  });
3885
4396
  }
3886
4397
  }
3887
- function ResultGroups({
3888
- busy = false,
3889
- groups,
3890
- highlightedIndex,
3891
- id,
3892
- label,
3893
- onHighlight
3894
- }) {
3895
- let index = 0;
3896
- return /*#__PURE__*/React.createElement("div", {
3897
- "aria-busy": busy,
3898
- "aria-label": label,
3899
- id: id,
3900
- role: "listbox"
3901
- }, groups.map(({
3902
- results,
3903
- type
3904
- }, groupIndex) => {
3905
- const headingId = `${id}-group-${groupIndex}`;
3906
- return /*#__PURE__*/React.createElement("div", {
3907
- "aria-labelledby": headingId,
3908
- key: type,
3909
- role: "group"
3910
- }, /*#__PURE__*/React.createElement("div", {
3911
- id: headingId
3912
- }, type), results.map(result => {
3913
- const resultIndex = index++;
3914
- return /*#__PURE__*/React.createElement(SearchResult, {
3915
- highlighted: resultIndex === highlightedIndex,
3916
- id: resultId(id, resultIndex),
3917
- key: result.id,
3918
- onHighlight: () => onHighlight(resultIndex),
3919
- subtitle: summarize(result.attributes.metadata),
3920
- title: result.attributes.title,
3921
- url: result.attributes.url
3922
- });
3923
- }));
3924
- }));
3925
- }
3926
- function summarize(metadata) {
3927
- return Object.values(metadata !== null && metadata !== void 0 ? metadata : {}).filter(value => typeof value === "string" && value !== "").join(" · ");
3928
- }
3929
4398
 
3930
- function useRecentVisits(env) {
3931
- const personId = useCurrentPersonId();
3932
- const entries = useRecentlyVisited({
3933
- env,
3934
- personId
3935
- });
3936
- const [count, setCount] = React.useState(RECENTLY_VISITED_PAGE_SIZE);
3937
- const shown = React.useMemo(() => entries.slice(0, count), [entries, count]);
3938
- const hydrated = useHydratedRecentlyVisited(shown, env);
3939
- // Resources show up once they're found. Ones that can't be found anymore,
3940
- // or that the person can no longer see, are left out.
3941
- const visits = hydrated.filter(isShown);
3942
- const loading = hydrated.some(({
3943
- status
3944
- }) => status === "loading");
3945
- const showMore = React.useCallback(() => setCount(current => current + RECENTLY_VISITED_PAGE_SIZE), []);
3946
- return {
3947
- count,
3948
- hasMore: entries.length > count,
3949
- loading,
3950
- showMore,
3951
- visits
3952
- };
3953
- }
3954
- function isShown(visit) {
3955
- return visit.status === "page" || visit.status === "resolved";
3956
- }
3957
- function RecentlyVisited({
3958
- count,
3959
- hasMore,
3960
- highlightedIndex,
3961
- id,
3962
- loading,
3963
- onHighlight,
3964
- showMore,
3965
- visits
3966
- }) {
3967
- const headingId = `${id}-heading`;
3968
- if (visits.length === 0 && !hasMore && !loading) {
3969
- return /*#__PURE__*/React.createElement("p", null, "Pages you visit will show up here.");
3970
- } else {
3971
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
3972
- id: headingId
3973
- }, "Recently visited"), /*#__PURE__*/React.createElement("div", {
3974
- "aria-labelledby": headingId,
3975
- id: id,
3976
- role: "listbox"
3977
- }, visits.map((visit, index) => (/*#__PURE__*/React.createElement(SearchResult, {
3978
- highlighted: index === highlightedIndex,
3979
- id: resultId(id, index),
3980
- key: entryKey(visit.entry),
3981
- onHighlight: () => onHighlight(index),
3982
- ...describeVisit(visit)
3983
- })))), hasMore && /*#__PURE__*/React.createElement(ShowMoreWhenVisible, {
3984
- key: count,
3985
- onVisible: showMore
3986
- }));
3987
- }
3988
- }
3989
- function ShowMoreWhenVisible({
3990
- onVisible
4399
+ // Keys the recents list for navigation, so the highlight
4400
+ // stays put as more recents load and as their lookups answer.
4401
+ const RECENTS = "recents";
4402
+ function SearchPanel({
4403
+ env,
4404
+ products
3991
4405
  }) {
3992
- const ref = React.useRef(null);
4406
+ var _a, _b;
4407
+ const listboxId = useInstanceId("product-search-results");
4408
+ const [term, setTerm] = React.useState("");
4409
+ const [selectedSlug, setSelectedSlug] = React.useState(() => initialProduct(products).slug);
4410
+ const product = (_a = products.find(({
4411
+ slug
4412
+ }) => slug === selectedSlug)) !== null && _a !== void 0 ? _a : products[0];
4413
+ const hasTerm = term.trim() !== "";
4414
+ const search = useProductSearch(product.slug, term);
4415
+ const groups = "groups" in search ? search.groups : undefined;
4416
+ const sections = React.useMemo(() => groupSections(groups), [groups]);
4417
+ const {
4418
+ visits,
4419
+ ...recents
4420
+ } = useRecentVisits(env);
4421
+ const recentItems = visits.map(visitItem);
4422
+ const items = hasTerm ? sections.flatMap(section => section.items) : recentItems;
4423
+ const navigation = useResultNavigation(items, hasTerm ? sections : RECENTS);
4424
+ const {
4425
+ highlightedIndex
4426
+ } = navigation;
4427
+ const inputRef = React.useRef(null);
4428
+ const [announceHighlight, setAnnounceHighlight] = React.useState(false);
4429
+ // The tabs keep focus while arrowing through results, but only the focused
4430
+ // input can point screen readers at the highlight, so from the tabs it's
4431
+ // announced instead. Typing goes back to the input, keystroke and all.
4432
+ const handleTabsKeyDown = event => {
4433
+ var _a;
4434
+ if (isTypingKey(event)) {
4435
+ (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
4436
+ } else if (verticalStep(event.key)) {
4437
+ setAnnounceHighlight(true);
4438
+ navigation.handleKeyDown(event);
4439
+ } else {
4440
+ navigation.handleKeyDown(event);
4441
+ }
4442
+ };
3993
4443
  React.useEffect(() => {
3994
- const observer = new IntersectionObserver(([entry]) => {
3995
- if (entry.isIntersecting) onVisible();
3996
- });
3997
- if (ref.current) observer.observe(ref.current);
3998
- return () => observer.disconnect();
3999
- }, [onVisible]);
4000
- return /*#__PURE__*/React.createElement("div", {
4001
- ref: ref
4002
- });
4003
- }
4004
- // Unlike search results, recents come from every product, so each one names
4005
- // its product and type.
4006
- function describeVisit(visit) {
4007
- const subtitle = (...parts) => parts.filter(Boolean).join(" · ");
4008
- if (visit.status === "page") {
4009
- const {
4010
- details,
4011
- product,
4012
- title
4013
- } = visit.entry;
4014
- return {
4015
- subtitle: subtitle(productName(product), details),
4016
- title,
4017
- url: visit.url
4018
- };
4019
- } else {
4020
- const {
4021
- metadata,
4022
- product,
4023
- title,
4024
- type
4025
- } = visit.result.attributes;
4026
- return {
4027
- subtitle: subtitle(productName(product), type, summarize(metadata)),
4028
- title,
4029
- url: visit.url
4030
- };
4031
- }
4032
- }
4033
- // check-ins → Check-Ins
4034
- function productName(slug) {
4035
- return slug.split("-").map(capitalize).join("-");
4036
- }
4037
- function capitalize(word) {
4038
- return word.charAt(0).toUpperCase() + word.slice(1);
4444
+ if (hasTerm) saveLastProduct(product.slug);
4445
+ }, [hasTerm, product.slug]);
4446
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SearchInput, {
4447
+ expanded: items.length > 0,
4448
+ highlightedIndex: highlightedIndex,
4449
+ inputRef: inputRef,
4450
+ listboxId: listboxId,
4451
+ onChange: setTerm,
4452
+ onFocus: () => setAnnounceHighlight(false),
4453
+ onKeyDown: navigation.handleKeyDown,
4454
+ value: term
4455
+ }), /*#__PURE__*/React.createElement(ProductTabs, {
4456
+ onKeyDown: handleTabsKeyDown,
4457
+ onSelect: setSelectedSlug,
4458
+ products: products,
4459
+ selected: product
4460
+ }, hasTerm ? (/*#__PURE__*/React.createElement(SearchResults, {
4461
+ highlightedIndex: highlightedIndex,
4462
+ id: listboxId,
4463
+ label: `${product.appName} results`,
4464
+ onHighlight: navigation.highlightIndex,
4465
+ sections: sections,
4466
+ status: search.status,
4467
+ term: term
4468
+ })) : (/*#__PURE__*/React.createElement(RecentlyVisited, {
4469
+ highlightedIndex: highlightedIndex,
4470
+ id: listboxId,
4471
+ items: recentItems,
4472
+ onHighlight: navigation.highlightIndex,
4473
+ ...recents
4474
+ }))), /*#__PURE__*/React.createElement("div", {
4475
+ "aria-live": "polite",
4476
+ className: styles$b.visuallyHidden
4477
+ }, announceHighlight ? (_b = navigation.highlighted) === null || _b === void 0 ? void 0 : _b.title : null), /*#__PURE__*/React.createElement(KeyboardHints, {
4478
+ copied: navigation.copied
4479
+ }));
4039
4480
  }
4040
4481
 
4041
- const SEARCH_DEBOUNCE_MS = 300;
4042
- function useProductSearch(product, term) {
4043
- const query = term.trim();
4044
- const debouncedQuery = useDebouncedValue(query, SEARCH_DEBOUNCE_MS);
4045
- const request = useCachedRequest(debouncedQuery ? productSearchUrl(product, debouncedQuery) : null);
4046
- const response = "data" in request ? request.data : undefined;
4047
- const settledProduct = useSettledProduct(product, request.status);
4048
- // While a request loads, the previous response stays on screen, but only
4049
- // if it was for this product: People results must never show under the
4050
- // Services tab.
4051
- const visibleResponse = request.status === "done" || settledProduct === product ? response : undefined;
4052
- const groups = React.useMemo(() => visibleResponse && groupByType(visibleResponse), [visibleResponse]);
4053
- if (!query) {
4054
- return {
4055
- status: "idle"
4056
- };
4057
- } else if (query !== debouncedQuery) {
4058
- return {
4059
- groups,
4060
- status: "loading"
4061
- };
4062
- } else if (request.status === "done") {
4063
- return {
4064
- groups: groups !== null && groups !== void 0 ? groups : [],
4065
- status: "done"
4066
- };
4067
- } else if (request.status === "error") {
4068
- return {
4069
- status: "error"
4070
- };
4071
- } else {
4072
- return {
4073
- groups,
4074
- status: "loading"
4075
- };
4076
- }
4482
+ // Products open modals from many libraries, not all of which mark themselves
4483
+ // with roles, so a modal is recognized by what it does: cover the page.
4484
+ function isModalOpen() {
4485
+ const {
4486
+ clientHeight,
4487
+ clientWidth
4488
+ } = document.documentElement;
4489
+ const element = document.elementFromPoint(clientWidth / 2, clientHeight / 2);
4490
+ return element !== null && (isInNativeDialog(element) || hasBackdrop(element, clientWidth, clientHeight));
4077
4491
  }
4078
- function productSearchUrl(product, query) {
4079
- const params = new URLSearchParams({
4080
- per_page: "100",
4081
- "where[term]": query
4082
- });
4083
- return `${window.location.origin}/~api/${product}/v2/product_search_results?${params}`;
4492
+ // A native modal's backdrop is drawn by the browser rather than being an
4493
+ // element; hit-testing it lands on the dialog itself.
4494
+ function isInNativeDialog(element) {
4495
+ return element.closest("dialog[open]") !== null;
4084
4496
  }
4085
- function groupByType({
4086
- data,
4087
- meta
4088
- }) {
4089
- var _a, _b;
4090
- const typeOrder = (_a = meta === null || meta === void 0 ? void 0 : meta.type_order) !== null && _a !== void 0 ? _a : [];
4091
- const groups = new Map();
4092
- for (const result of data) {
4093
- const {
4094
- type
4095
- } = result.attributes;
4096
- if (!groups.has(type)) groups.set(type, {
4097
- results: [],
4098
- type
4099
- });
4100
- (_b = groups.get(type)) === null || _b === void 0 ? void 0 : _b.results.push(result);
4497
+ function hasBackdrop(element, width, height) {
4498
+ let current = element;
4499
+ while (current && !isBackdrop(current, width, height)) {
4500
+ current = current.parentElement;
4101
4501
  }
4102
- const rank = type => {
4103
- const index = typeOrder.indexOf(type);
4104
- return index === -1 ? typeOrder.length : index;
4105
- };
4106
- return [...groups.values()].sort((a, b) => rank(a.type) - rank(b.type));
4502
+ return current !== null;
4107
4503
  }
4108
- // The product the most recent finished request was for.
4109
- function useSettledProduct(product, status) {
4110
- const [settledProduct, setSettledProduct] = React.useState();
4111
- React.useEffect(() => {
4112
- if (status === "done") setSettledProduct(product);
4113
- }, [product, status]);
4114
- return settledProduct;
4115
- }
4116
- function useDebouncedValue(value, delay) {
4117
- const [debounced, setDebounced] = React.useState(value);
4118
- React.useEffect(() => {
4119
- const timeout = setTimeout(() => setDebounced(value), delay);
4120
- return () => clearTimeout(timeout);
4121
- }, [value, delay]);
4122
- return debounced;
4504
+ // Some backdrops start below the topbar rather than at the top of the screen,
4505
+ // so the layer only has to span the width and reach from the center to the
4506
+ // bottom. Its own box has to reach the center: a bar along the bottom can
4507
+ // hold a panel that pops up over the center, like Services' media player.
4508
+ // react-aria leaves a closed modal's backdrop up while it fades out, search's
4509
+ // own included, so one that's on its way out doesn't count.
4510
+ function isBackdrop(element, width, height) {
4511
+ const rect = element.getBoundingClientRect();
4512
+ return !element.matches("[data-exiting]") && getComputedStyle(element).position === "fixed" && rect.width >= width - SCROLLBAR_SLACK && rect.top <= height / 2 && rect.bottom >= height - ROUNDING_SLACK;
4123
4513
  }
4124
-
4125
- const COPIED_FEEDBACK_MS = 1500;
4126
- const verticalStep = key => ({
4127
- ArrowDown: 1,
4128
- ArrowUp: -1
4129
- })[key];
4130
- // Remembering which list the index belongs to puts the highlight back on the
4131
- // first option whenever a new list replaces it, without an effect to reset it.
4132
- // A list that grows or fills in under the same `list` keeps its highlight.
4133
- function useResultNavigation(options, list) {
4134
- const [highlight, setHighlight] = React.useState({
4135
- index: 0,
4136
- list
4137
- });
4138
- const highlightedIndex = highlight.list === list ? highlight.index : 0;
4139
- const highlighted = options[highlightedIndex];
4140
- const [copiedUrl, setCopiedUrl] = React.useState(null);
4514
+ // react-aria keeps a classic scrollbar's gutter reserved while its modals lock
4515
+ // scrolling, so their backdrops stop short of the right edge by its width.
4516
+ const SCROLLBAR_SLACK = 24;
4517
+ // Rects are fractional at non-100% zoom, but clientWidth and clientHeight are
4518
+ // rounded to whole pixels.
4519
+ const ROUNDING_SLACK = 1;
4520
+
4521
+ function useSearchShortcut(enabled, open, setOpen) {
4141
4522
  React.useEffect(() => {
4142
- if (copiedUrl) {
4143
- const timeout = setTimeout(() => setCopiedUrl(null), COPIED_FEEDBACK_MS);
4144
- return () => clearTimeout(timeout);
4145
- } else {
4146
- return undefined;
4147
- }
4148
- }, [copiedUrl]);
4149
- const highlightIndex = React.useCallback(index => setHighlight(current => current.list === list && current.index === index ? current : {
4150
- index,
4151
- list
4152
- }), [list]);
4153
- const handleKeyDown = event => {
4154
- const step = verticalStep(event.key);
4155
- if (step && options.length > 0) {
4156
- event.preventDefault();
4157
- highlightIndex((highlightedIndex + step + options.length) % options.length);
4158
- } else if (event.key === "Enter" && highlighted && !event.nativeEvent.isComposing) {
4159
- event.preventDefault();
4160
- open(highlighted.url, {
4161
- newTab: hasCommandKey(event)
4162
- });
4163
- } else if (isCopyShortcut(event) && highlighted && !hasSelectedText(event.currentTarget)) {
4164
- event.preventDefault();
4165
- const {
4166
- url
4167
- } = highlighted;
4168
- navigator.clipboard.writeText(url).then(() => setCopiedUrl(url));
4169
- }
4170
- };
4171
- return {
4172
- copied: copiedUrl !== null && copiedUrl === (highlighted === null || highlighted === void 0 ? void 0 : highlighted.url),
4173
- handleKeyDown,
4174
- highlightIndex,
4175
- highlighted,
4176
- highlightedIndex: highlighted ? highlightedIndex : null
4177
- };
4178
- }
4179
- function open(url, {
4180
- newTab
4181
- }) {
4182
- if (newTab) {
4183
- window.open(url, "_blank", "noopener");
4184
- } else {
4185
- window.location.assign(url);
4186
- }
4187
- }
4188
- const isCopyShortcut = event => event.key.toLowerCase() === "c" && !event.altKey && !event.shiftKey && hasCommandKey(event);
4189
- // With text selected, ⌘C should copy that text as usual, not the link.
4190
- function hasSelectedText(element) {
4191
- var _a, _b;
4192
- const selectedInField = element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
4193
- return selectedInField || ((_b = (_a = window.getSelection()) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : "") !== "";
4523
+ if (!enabled) return undefined;
4524
+ // Rich text editors like Trix bind ⌘K to "insert link"; capturing at the
4525
+ // window and stopping propagation keeps them from ever seeing it. Under a
4526
+ // product's modal, search stays shut rather than stacking a second modal,
4527
+ // and the keystroke goes on to the product.
4528
+ const handleKeyDown = event => {
4529
+ if (open && isSearchShortcut(event)) {
4530
+ event.preventDefault();
4531
+ event.stopPropagation();
4532
+ setOpen(false);
4533
+ } else if (!open && (isSearchShortcut(event) || isLegacySearchShortcut(event)) && !isModalOpen()) {
4534
+ event.preventDefault();
4535
+ event.stopPropagation();
4536
+ setOpen(true);
4537
+ }
4538
+ };
4539
+ window.addEventListener("keydown", handleKeyDown, {
4540
+ capture: true
4541
+ });
4542
+ return () => window.removeEventListener("keydown", handleKeyDown, {
4543
+ capture: true
4544
+ });
4545
+ }, [enabled, open, setOpen]);
4194
4546
  }
4195
4547
 
4196
4548
  const SEARCHABLE_PRODUCTS = [{
@@ -4218,6 +4570,7 @@ true;
4218
4570
  appName: "Services",
4219
4571
  slug: "services"
4220
4572
  }];
4573
+
4221
4574
  const PERMISSION_CHECKS = {
4222
4575
  "check-ins": () => fetchMe("check-ins", "permission").then(({
4223
4576
  permission
@@ -4280,6 +4633,7 @@ true;
4280
4633
  return promise;
4281
4634
  }
4282
4635
  }
4636
+
4283
4637
  function useSearchableProducts(apps) {
4284
4638
  // Keyed by name because providers hand over a fresh `apps` array on renders
4285
4639
  // where nothing changed.
@@ -4321,181 +4675,27 @@ true;
4321
4675
  return groupApps(apps).restProducts.flatMap(app => SEARCHABLE_PRODUCTS.filter(product => product.appName === app.name));
4322
4676
  }
4323
4677
 
4324
- const isSearchShortcut = event => event.key.toLowerCase() === "k" && !event.altKey && !event.shiftKey && hasCommandKey(event);
4325
- // Products opened their own search with `/` before this one replaced it.
4326
- const isLegacySearchShortcut = event => event.key === "/" && !event.altKey && !event.ctrlKey && !event.metaKey && !isTypingIn(event.target);
4327
- const isTypingKey = event => event.key.length === 1 && event.key !== " " && !event.altKey && !event.ctrlKey && !event.metaKey;
4328
- const isTypingIn = target => target instanceof Element && target.closest('input, select, textarea, [contenteditable]:not([contenteditable="false"])') !== null;
4329
4678
  function ProductSearch({
4330
4679
  apps,
4331
4680
  env
4332
4681
  }) {
4333
4682
  const searchable = useSearchableProducts(apps);
4334
- if (searchable.status === "done" && searchable.products.length > 0) {
4335
- return /*#__PURE__*/React.createElement(SearchButtonAndDialog, {
4336
- env: env,
4337
- products: searchable.products
4338
- });
4339
- } else {
4340
- return null;
4341
- }
4342
- }
4343
- function SearchButtonAndDialog({
4344
- env,
4345
- products
4346
- }) {
4347
4683
  const [open, setOpen] = React.useState(false);
4348
- React.useEffect(() => {
4349
- // Rich text editors like Trix bind ⌘K to "insert link"; capturing at the
4350
- // window and stopping propagation keeps them from ever seeing it.
4351
- const handleKeyDown = event => {
4352
- if (isSearchShortcut(event)) {
4353
- event.preventDefault();
4354
- event.stopPropagation();
4355
- setOpen(wasOpen => !wasOpen);
4356
- } else if (isLegacySearchShortcut(event)) {
4357
- event.preventDefault();
4358
- event.stopPropagation();
4359
- setOpen(true);
4360
- }
4361
- };
4362
- window.addEventListener("keydown", handleKeyDown, {
4363
- capture: true
4364
- });
4365
- return () => window.removeEventListener("keydown", handleKeyDown, {
4366
- capture: true
4367
- });
4368
- }, []);
4369
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("button", {
4370
- "aria-keyshortcuts": `${isMac() ? "Meta+K" : "Control+K"} /`,
4371
- "aria-label": "Search",
4372
- onClick: () => setOpen(true),
4373
- type: "button"
4374
- }, /*#__PURE__*/React.createElement(Spyglass, {
4375
- fill: "currentColor"
4376
- })), /*#__PURE__*/React.createElement(tapestry.Modal, {
4684
+ const available = searchable.status === "done" && searchable.products.length > 0;
4685
+ useSearchShortcut(available, open, setOpen);
4686
+ if (!available) return null;
4687
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SearchButton, {
4688
+ onClick: () => setOpen(true)
4689
+ }), /*#__PURE__*/React.createElement(tapestry.Modal, {
4377
4690
  "aria-label": "Search",
4378
4691
  dismissible: true,
4379
4692
  onClose: () => setOpen(false),
4380
4693
  open: open
4381
4694
  }, /*#__PURE__*/React.createElement(SearchPanel, {
4382
4695
  env: env,
4383
- products: products
4696
+ products: searchable.products
4384
4697
  })));
4385
4698
  }
4386
- // Keys the recents list for navigation, so the highlight stays put as more
4387
- // recents load and as their lookups answer.
4388
- const RECENTS = "recents";
4389
- function SearchPanel({
4390
- env,
4391
- products
4392
- }) {
4393
- var _a, _b;
4394
- const listboxId = useInstanceId("product-search-results");
4395
- const [term, setTerm] = React.useState("");
4396
- const [selectedSlug, setSelectedSlug] = React.useState(() => initialProduct(products).slug);
4397
- const product = (_a = products.find(({
4398
- slug
4399
- }) => slug === selectedSlug)) !== null && _a !== void 0 ? _a : products[0];
4400
- const hasTerm = term.trim() !== "";
4401
- const search = useProductSearch(product.slug, term);
4402
- const groups = "groups" in search ? search.groups : undefined;
4403
- const results = React.useMemo(() => {
4404
- var _a;
4405
- return (_a = groups === null || groups === void 0 ? void 0 : groups.flatMap(group => group.results)) !== null && _a !== void 0 ? _a : [];
4406
- }, [groups]);
4407
- const recents = useRecentVisits(env);
4408
- const options = hasTerm ? results.map(({
4409
- attributes
4410
- }) => ({
4411
- label: attributes.title,
4412
- url: attributes.url
4413
- })) : recents.visits.map(visit => {
4414
- const {
4415
- title,
4416
- url
4417
- } = describeVisit(visit);
4418
- return {
4419
- label: title,
4420
- url
4421
- };
4422
- });
4423
- const navigation = useResultNavigation(options, hasTerm ? results : RECENTS);
4424
- const {
4425
- highlightedIndex
4426
- } = navigation;
4427
- const inputRef = React.useRef(null);
4428
- const [announceHighlight, setAnnounceHighlight] = React.useState(false);
4429
- // The tabs keep focus while arrowing through results, but only the focused
4430
- // input can point screen readers at the highlight, so from the tabs it's
4431
- // announced instead. Typing goes back to the input, keystroke and all.
4432
- const handleTabsKeyDown = event => {
4433
- var _a;
4434
- if (isTypingKey(event)) {
4435
- (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
4436
- } else if (verticalStep(event.key)) {
4437
- setAnnounceHighlight(true);
4438
- navigation.handleKeyDown(event);
4439
- } else {
4440
- navigation.handleKeyDown(event);
4441
- }
4442
- };
4443
- React.useEffect(() => {
4444
- if (hasTerm) saveLastProduct(product.slug);
4445
- }, [hasTerm, product.slug]);
4446
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("input", {
4447
- "aria-activedescendant": highlightedIndex === null ? undefined : resultId(listboxId, highlightedIndex),
4448
- "aria-autocomplete": "list",
4449
- "aria-controls": options.length > 0 ? listboxId : undefined,
4450
- "aria-expanded": options.length > 0,
4451
- "aria-label": "Search",
4452
- autoFocus: true,
4453
- onChange: event => setTerm(event.target.value),
4454
- onFocus: () => setAnnounceHighlight(false),
4455
- onKeyDown: navigation.handleKeyDown,
4456
- placeholder: "Search",
4457
- ref: inputRef,
4458
- role: "combobox",
4459
- type: "search",
4460
- value: term
4461
- }), /*#__PURE__*/React.createElement(ProductTabs, {
4462
- onKeyDown: handleTabsKeyDown,
4463
- onSelect: setSelectedSlug,
4464
- products: products,
4465
- selected: product
4466
- }, hasTerm ? (/*#__PURE__*/React.createElement(SearchResults, {
4467
- highlightedIndex: highlightedIndex,
4468
- id: listboxId,
4469
- label: `${product.appName} results`,
4470
- onHighlight: navigation.highlightIndex,
4471
- search: search,
4472
- term: term
4473
- })) : (/*#__PURE__*/React.createElement(RecentlyVisited, {
4474
- highlightedIndex: highlightedIndex,
4475
- id: listboxId,
4476
- onHighlight: navigation.highlightIndex,
4477
- ...recents
4478
- }))), /*#__PURE__*/React.createElement("div", {
4479
- "aria-live": "polite",
4480
- className: styles$c.visuallyHidden
4481
- }, announceHighlight ? (_b = navigation.highlighted) === null || _b === void 0 ? void 0 : _b.label : null), /*#__PURE__*/React.createElement(KeyboardHints, {
4482
- copied: navigation.copied
4483
- }));
4484
- }
4485
- function KeyboardHints({
4486
- copied
4487
- }) {
4488
- const command = commandKeyLabel();
4489
- 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", {
4490
- role: "status"
4491
- }, copied ? "Copied!" : "Copy link"), " \u00B7 ", command, "K Open search");
4492
- }
4493
- function initialProduct(products) {
4494
- var _a, _b, _c;
4495
- const lastProduct = readLastProduct();
4496
- const bySlug = slug => products.find(product => product.slug === slug);
4497
- 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];
4498
- }
4499
4699
 
4500
4700
  function useRecordRecentlyVisited({
4501
4701
  enabled,
@@ -4539,9 +4739,9 @@ true;
4539
4739
  `), children);
4540
4740
  }
4541
4741
 
4542
- var styles$9 = {"route":"route--8BWe","is-active":"is-active-FsU9Y","apps-routes-separator":"apps-routes-separator-Nlm-N"};
4742
+ var styles$a = {"route":"route--8BWe","is-active":"is-active-FsU9Y","apps-routes-separator":"apps-routes-separator-Nlm-N"};
4543
4743
 
4544
- var styles$8 = {"popup":"popup-mDNKn","trigger-wrap":"trigger-wrap-Jd-KR","trigger":"trigger-WgRT5","is-visible":"is-visible-GZC0-","trigger-orgname":"trigger-orgname-TDHCc"};
4744
+ var styles$9 = {"popup":"popup-mDNKn","trigger-wrap":"trigger-wrap-Jd-KR","trigger":"trigger-WgRT5","is-visible":"is-visible-GZC0-","trigger-orgname":"trigger-orgname-TDHCc"};
4545
4745
 
4546
4746
  const UserMenu = ({
4547
4747
  appName,
@@ -4560,7 +4760,7 @@ true;
4560
4760
  component: Outsider,
4561
4761
  onOutsideClick: () => setUserMenuVisible(false),
4562
4762
  cleanup: !userMenuVisible,
4563
- className: styles$8.popup,
4763
+ className: styles$9.popup,
4564
4764
  isVisible: userMenuVisible
4565
4765
  }, /*#__PURE__*/React.createElement("div", null, hasConnectedPeople ? (/*#__PURE__*/React.createElement("div", {
4566
4766
  style: {
@@ -4704,11 +4904,11 @@ true;
4704
4904
  userMenuVisible
4705
4905
  }) => {
4706
4906
  return /*#__PURE__*/React.createElement("div", {
4707
- className: styles$8["trigger-wrap"],
4907
+ className: styles$9["trigger-wrap"],
4708
4908
  "data-pendo": "topbar-org-switcher"
4709
4909
  }, /*#__PURE__*/React.createElement(Hoverable, {
4710
- className: classnames(styles$8["trigger"], {
4711
- [styles$8["is-visible"]]: userMenuVisible
4910
+ className: classnames(styles$9["trigger"], {
4911
+ [styles$9["is-visible"]]: userMenuVisible
4712
4912
  }),
4713
4913
  component: "button",
4714
4914
  type: "button",
@@ -4720,7 +4920,7 @@ true;
4720
4920
  requestConnectedPeopleFetch();
4721
4921
  }
4722
4922
  }, showOrgName && (/*#__PURE__*/React.createElement("span", {
4723
- className: styles$8["trigger-orgname"]
4923
+ className: styles$9["trigger-orgname"]
4724
4924
  }, orgName)), /*#__PURE__*/React.createElement(DisclosureChevronIcon, null)));
4725
4925
  };
4726
4926
 
@@ -4780,7 +4980,7 @@ true;
4780
4980
  colors: props.colors,
4781
4981
  requestAppsFetch: props.requestAppsFetch
4782
4982
  })), /*#__PURE__*/React.createElement("div", {
4783
- className: styles$9["apps-routes-separator"]
4983
+ className: styles$a["apps-routes-separator"]
4784
4984
  }), routesVisible && props.routes, /*#__PURE__*/React.createElement("div", {
4785
4985
  style: {
4786
4986
  margin: "auto"
@@ -4821,12 +5021,12 @@ true;
4821
5021
  ...nativeProps
4822
5022
  }) => {
4823
5023
  return /*#__PURE__*/React.createElement("a", {
4824
- className: classnames(styles$9["route"], className, active && styles$9["is-active"]),
5024
+ className: classnames(styles$a["route"], className, active && styles$a["is-active"]),
4825
5025
  ...nativeProps
4826
5026
  });
4827
5027
  };
4828
5028
 
4829
- var styles$7 = {"avatar":"avatar-tW-wn"};
5029
+ var styles$8 = {"avatar":"avatar-tW-wn"};
4830
5030
 
4831
5031
  function Avatar({
4832
5032
  env,
@@ -4836,7 +5036,7 @@ true;
4836
5036
  }) {
4837
5037
  const url = incomingUrl || `${pcoUrl$1(env)("people")}/static/no_photo_thumbnail_gray.svg`;
4838
5038
  return /*#__PURE__*/React.createElement("div", {
4839
- className: styles$7.avatar,
5039
+ className: styles$8.avatar,
4840
5040
  style: {
4841
5041
  backgroundImage: `url(${url})`,
4842
5042
  ...style
@@ -32595,7 +32795,7 @@ true;
32595
32795
 
32596
32796
  var reactDomExports = requireReactDom();
32597
32797
 
32598
- var styles$6 = {"trigger":"trigger-Th-Up","layer":"layer-ck6BD","layer--visible":"layer--visible-4E2p1","panel":"panel-vobWE","header":"header-xndfZ","heading":"heading-uXyQf","close":"close-pK0Lk","list":"list-zWvrn","link":"link-Kf3qj","current":"current-g3Ey-","accounts-item":"accounts-item-eQqDD","accounts-link":"accounts-link-SeIbD"};
32798
+ var styles$7 = {"trigger":"trigger-Th-Up","layer":"layer-ck6BD","layer--visible":"layer--visible-4E2p1","panel":"panel-vobWE","header":"header-xndfZ","heading":"heading-uXyQf","close":"close-pK0Lk","list":"list-zWvrn","link":"link-Kf3qj","current":"current-g3Ey-","accounts-item":"accounts-item-eQqDD","accounts-link":"accounts-link-SeIbD"};
32599
32799
 
32600
32800
  const AppsSwitcher = ({
32601
32801
  appName,
@@ -32623,8 +32823,8 @@ true;
32623
32823
  }, /*#__PURE__*/React.createElement("a", {
32624
32824
  "aria-current": name === appName ? "page" : undefined,
32625
32825
  "aria-label": name,
32626
- className: classnames(styles$6.link, {
32627
- [styles$6.current]: name === appName
32826
+ className: classnames(styles$7.link, {
32827
+ [styles$7.current]: name === appName
32628
32828
  }),
32629
32829
  "data-pendo": `app-switcher--change-app--${name}`,
32630
32830
  "data-turbolinks": false,
@@ -32636,48 +32836,48 @@ true;
32636
32836
  "aria-expanded": visible,
32637
32837
  "aria-haspopup": "true",
32638
32838
  "aria-label": `App switcher: ${appName}`,
32639
- className: styles$6.trigger,
32839
+ className: styles$7.trigger,
32640
32840
  onClick: onTriggerClick,
32641
32841
  type: "button"
32642
32842
  }, /*#__PURE__*/React.createElement(ColorAppIcon, {
32643
32843
  appName: appName.replace(/[\s-]/, "")
32644
32844
  })), /*#__PURE__*/reactDomExports.createPortal(/*#__PURE__*/React.createElement("div", {
32645
32845
  ref: containerRef,
32646
- className: classnames(styles$6.layer, {
32647
- [styles$6["layer--visible"]]: visible
32846
+ className: classnames(styles$7.layer, {
32847
+ [styles$7["layer--visible"]]: visible
32648
32848
  }),
32649
32849
  onClick: toggle,
32650
32850
  onKeyDown: handleKeyDown
32651
32851
  }, /*#__PURE__*/React.createElement("nav", {
32652
32852
  "aria-label": "Switch app",
32653
32853
  "aria-modal": visible,
32654
- className: styles$6.panel,
32854
+ className: styles$7.panel,
32655
32855
  "data-pendo": "app-switcher",
32656
32856
  onClick: event => event.stopPropagation(),
32657
32857
  role: "dialog"
32658
32858
  }, /*#__PURE__*/React.createElement("div", {
32659
- className: styles$6.header
32859
+ className: styles$7.header
32660
32860
  }, /*#__PURE__*/React.createElement("span", {
32661
- className: styles$6.heading
32861
+ className: styles$7.heading
32662
32862
  }, "Switch to"), /*#__PURE__*/React.createElement("button", {
32663
32863
  "aria-label": "Close",
32664
- className: styles$6.close,
32864
+ className: styles$7.close,
32665
32865
  onClick: toggle,
32666
32866
  type: "button"
32667
32867
  }, /*#__PURE__*/React.createElement(X, {
32668
32868
  "aria-hidden": "true",
32669
32869
  fill: "var(--t-icon-color-dim)"
32670
32870
  }))), /*#__PURE__*/React.createElement("ul", {
32671
- className: styles$6.list,
32871
+ className: styles$7.list,
32672
32872
  role: "list"
32673
32873
  }, homeProduct && productLink(homeProduct.name), restProducts.map(({
32674
32874
  name
32675
32875
  }) => productLink(name)), accountsProduct && (/*#__PURE__*/React.createElement("li", {
32676
- className: styles$6["accounts-item"]
32876
+ className: styles$7["accounts-item"]
32677
32877
  }, /*#__PURE__*/React.createElement("a", {
32678
32878
  "aria-current": appName === "Accounts" ? "page" : undefined,
32679
32879
  "aria-label": "Account settings",
32680
- className: classnames(styles$6.link, styles$6["accounts-link"]),
32880
+ className: classnames(styles$7.link, styles$7["accounts-link"]),
32681
32881
  "data-pendo": "app-switcher--change-app--Accounts",
32682
32882
  "data-turbolinks": false,
32683
32883
  href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(accountsProduct.name)
@@ -32700,14 +32900,14 @@ true;
32700
32900
  method: "localStorage"
32701
32901
  };
32702
32902
 
32703
- var styles$5 = {"appmenu-button":"appmenu-button-b0UvT","appmenu-layer":"appmenu-layer-0IQpN","appmenu-container":"appmenu-container-gackd","appmenu-scroll":"appmenu-scroll-mPSx0","appmenu-list":"appmenu-list-h7clX","appmenu-list-a":"appmenu-list-a-rGIQN","appmenu-list-a--top":"appmenu-list-a--top-RT-2F","appmenu-list-a--bottom":"appmenu-list-a--bottom-YDeBC","appmenu-close-button":"appmenu-close-button-vv9bV","current-route":"current-route-wpJgz","is-active":"is-active-wy8Bg","routes":"routes-Nmt1S","route":"route-kkknQ","profile-button":"profile-button-a2WO2"};
32903
+ var styles$6 = {"appmenu-button":"appmenu-button-b0UvT","appmenu-layer":"appmenu-layer-0IQpN","appmenu-container":"appmenu-container-gackd","appmenu-scroll":"appmenu-scroll-mPSx0","appmenu-list":"appmenu-list-h7clX","appmenu-list-a":"appmenu-list-a-rGIQN","appmenu-list-a--top":"appmenu-list-a--top-RT-2F","appmenu-list-a--bottom":"appmenu-list-a--bottom-YDeBC","appmenu-close-button":"appmenu-close-button-vv9bV","current-route":"current-route-wpJgz","is-active":"is-active-wy8Bg","routes":"routes-Nmt1S","route":"route-kkknQ","profile-button":"profile-button-a2WO2"};
32704
32904
 
32705
32905
  function AccountSettingsLink({
32706
32906
  env
32707
32907
  }) {
32708
32908
  const accountsName = "accounts";
32709
32909
  return /*#__PURE__*/React.createElement("a", {
32710
- className: classnames(styles$5["appmenu-list-a"], styles$5["appmenu-list-a--bottom"]),
32910
+ className: classnames(styles$6["appmenu-list-a"], styles$6["appmenu-list-a--bottom"]),
32711
32911
  "data-pendo": `app-switcher--change-app--${accountsName}`,
32712
32912
  "data-turbolinks": false,
32713
32913
  href: pcoUrl$1(env)(accountsName) + getAppsSwitchProductPath(accountsName)
@@ -32730,7 +32930,7 @@ true;
32730
32930
  }) => (/*#__PURE__*/React.createElement("button", {
32731
32931
  "aria-label": "Close menu",
32732
32932
  type: "button",
32733
- className: styles$5["appmenu-close-button"],
32933
+ className: styles$6["appmenu-close-button"],
32734
32934
  style: style
32735
32935
  }, /*#__PURE__*/React.createElement(X, {
32736
32936
  fill: color,
@@ -32791,7 +32991,7 @@ true;
32791
32991
  "aria-haspopup": "true",
32792
32992
  "aria-label": `App switcher: ${appName}`,
32793
32993
  type: "button",
32794
- className: styles$5["appmenu-button"],
32994
+ className: styles$6["appmenu-button"],
32795
32995
  onClick: onTriggerClick
32796
32996
  }, /*#__PURE__*/React.createElement(ColorAppIcon, {
32797
32997
  appName: appName.replace(/[\s-]/, "")
@@ -32799,7 +32999,7 @@ true;
32799
32999
  item: "Topbar:Small:Menus:Products:visibility",
32800
33000
  render: () => (/*#__PURE__*/React.createElement("div", {
32801
33001
  ref: appMenuRef,
32802
- className: styles$5["appmenu-layer"],
33002
+ className: styles$6["appmenu-layer"],
32803
33003
  style: {
32804
33004
  ...(visible && {
32805
33005
  display: "block"
@@ -32816,37 +33016,37 @@ true;
32816
33016
  e.stopPropagation();
32817
33017
  }
32818
33018
  },
32819
- className: styles$5["appmenu-container"]
33019
+ className: styles$6["appmenu-container"]
32820
33020
  }, /*#__PURE__*/React.createElement("a", {
32821
33021
  "aria-current": appName === (homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name) ? "page" : undefined,
32822
33022
  "aria-label": homeProduct ? homeProduct.name : appName,
32823
33023
  "data-pendo": `app-switcher--change-app--${homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name}`,
32824
33024
  "data-turbolinks": false,
32825
- className: classnames(styles$5["appmenu-list-a"], styles$5["appmenu-list-a--top"]),
33025
+ className: classnames(styles$6["appmenu-list-a"], styles$6["appmenu-list-a--top"]),
32826
33026
  href: homeProduct ? pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name) : undefined
32827
33027
  }, /*#__PURE__*/React.createElement(UserMenuAppLockup, {
32828
33028
  appName: homeProduct ? homeProduct.name : appName
32829
33029
  })), /*#__PURE__*/React.createElement("div", {
32830
- className: styles$5["appmenu-scroll"]
33030
+ className: styles$6["appmenu-scroll"]
32831
33031
  }, /*#__PURE__*/React.createElement("ul", {
32832
33032
  "data-pendo": "app-switcher",
32833
- className: styles$5["appmenu-list"]
33033
+ className: styles$6["appmenu-list"]
32834
33034
  }, appsMenuFormatter(restProducts).map(({
32835
33035
  name
32836
33036
  }) => (/*#__PURE__*/React.createElement("li", {
32837
33037
  key: name,
32838
- className: styles$5["appmenu-list-li"]
33038
+ className: styles$6["appmenu-list-li"]
32839
33039
  }, /*#__PURE__*/React.createElement("a", {
32840
33040
  "aria-current": name === appName ? "page" : undefined,
32841
33041
  "aria-label": name,
32842
33042
  "data-pendo": `app-switcher--change-app--${name}`,
32843
33043
  "data-turbolinks": false,
32844
- className: styles$5["appmenu-list-a"],
33044
+ className: styles$6["appmenu-list-a"],
32845
33045
  href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(name)
32846
33046
  }, /*#__PURE__*/React.createElement(UserMenuAppLockup, {
32847
33047
  appName: name
32848
33048
  }))))), accountsProduct && (/*#__PURE__*/React.createElement("li", {
32849
- className: styles$5["appmenu-list-li"]
33049
+ className: styles$6["appmenu-list-li"]
32850
33050
  }, /*#__PURE__*/React.createElement(AccountSettingsLink, {
32851
33051
  env: env
32852
33052
  }))))))))
@@ -32910,11 +33110,11 @@ true;
32910
33110
  toggle: toggleProductSwitcher,
32911
33111
  visible: productSwitcherVisible
32912
33112
  })), routesMenuVisible && (/*#__PURE__*/React.createElement("div", {
32913
- className: styles$5["routes"]
33113
+ className: styles$6["routes"]
32914
33114
  }, props.routes)), /*#__PURE__*/React.createElement("button", {
32915
33115
  type: "button",
32916
- className: classnames(styles$5["current-route"], {
32917
- [styles$5["is-active"]]: routesMenuVisible
33116
+ className: classnames(styles$6["current-route"], {
33117
+ [styles$6["is-active"]]: routesMenuVisible
32918
33118
  }),
32919
33119
  onClick: props.routes.length > 0 ? () => {
32920
33120
  setProductSwitcherVisible(false);
@@ -32922,7 +33122,7 @@ true;
32922
33122
  } : null
32923
33123
  }, /*#__PURE__*/React.createElement(CurrentRouteComponent, null, capitalize$1(currentRouteText)), props.routes.length > 0 && /*#__PURE__*/React.createElement(DisclosureChevronIcon, null)), /*#__PURE__*/React.createElement("button", {
32924
33124
  type: "button",
32925
- className: styles$5["profile-button"],
33125
+ className: styles$6["profile-button"],
32926
33126
  onClick: () => {
32927
33127
  var _a, _b;
32928
33128
  props.requestConnectedPeopleFetch();
@@ -32938,7 +33138,7 @@ true;
32938
33138
  className,
32939
33139
  ...props
32940
33140
  }) => (/*#__PURE__*/React.createElement("a", {
32941
- className: classnames(styles$5["route"], className, active && styles$5["is-active"]),
33141
+ className: classnames(styles$6["route"], className, active && styles$6["is-active"]),
32942
33142
  ...props
32943
33143
  }));
32944
33144
 
@@ -33077,7 +33277,7 @@ true;
33077
33277
  id: "topbar"
33078
33278
  };
33079
33279
 
33080
- var styles$4 = {"debug-pco-resource":"debug-pco-resource-sLteM","debug-pco-resource-header":"debug-pco-resource-header-5fEbe","debug-pco-resource-badge":"debug-pco-resource-badge-J9v1F","debug-pco-resource-title":"debug-pco-resource-title-volWV","debug-pco-resource-field":"debug-pco-resource-field-7Gitp","debug-pco-resource-field-label":"debug-pco-resource-field-label--f1xX","debug-pco-resource-field-value":"debug-pco-resource-field-value-YoX-C","debug-pco-resource-note":"debug-pco-resource-note-N5xyf","debug-pco-resource-json":"debug-pco-resource-json-Rx-JI"};
33280
+ var styles$5 = {"debug-pco-resource":"debug-pco-resource-sLteM","debug-pco-resource-header":"debug-pco-resource-header-5fEbe","debug-pco-resource-badge":"debug-pco-resource-badge-J9v1F","debug-pco-resource-title":"debug-pco-resource-title-volWV","debug-pco-resource-field":"debug-pco-resource-field-7Gitp","debug-pco-resource-field-label":"debug-pco-resource-field-label--f1xX","debug-pco-resource-field-value":"debug-pco-resource-field-value-YoX-C","debug-pco-resource-note":"debug-pco-resource-note-N5xyf","debug-pco-resource-json":"debug-pco-resource-json-Rx-JI"};
33081
33281
 
33082
33282
  function isPresent(value) {
33083
33283
  return value !== undefined && value.trim() !== "";
@@ -33106,11 +33306,11 @@ true;
33106
33306
  value
33107
33307
  }) {
33108
33308
  return /*#__PURE__*/React.createElement("div", {
33109
- className: styles$4["debug-pco-resource-field"]
33309
+ className: styles$5["debug-pco-resource-field"]
33110
33310
  }, /*#__PURE__*/React.createElement("span", {
33111
- className: styles$4["debug-pco-resource-field-label"]
33311
+ className: styles$5["debug-pco-resource-field-label"]
33112
33312
  }, label), /*#__PURE__*/React.createElement("span", {
33113
- className: styles$4["debug-pco-resource-field-value"]
33313
+ className: styles$5["debug-pco-resource-field-value"]
33114
33314
  }, value));
33115
33315
  }
33116
33316
  // Only staff ghosting or impersonating a person get pco:staff-access-level,
@@ -33183,13 +33383,13 @@ true;
33183
33383
  };
33184
33384
  }, [pcrl, env]);
33185
33385
  return /*#__PURE__*/React.createElement("div", {
33186
- className: classnames("staff-only", styles$4["debug-pco-resource"])
33386
+ className: classnames("staff-only", styles$5["debug-pco-resource"])
33187
33387
  }, /*#__PURE__*/React.createElement("div", {
33188
- className: styles$4["debug-pco-resource-header"]
33388
+ className: styles$5["debug-pco-resource-header"]
33189
33389
  }, /*#__PURE__*/React.createElement("span", {
33190
- className: styles$4["debug-pco-resource-badge"]
33390
+ className: styles$5["debug-pco-resource-badge"]
33191
33391
  }, "Staff"), /*#__PURE__*/React.createElement("span", {
33192
- className: styles$4["debug-pco-resource-title"]
33392
+ className: styles$5["debug-pco-resource-title"]
33193
33393
  }, "Recently Visited debug")), /*#__PURE__*/React.createElement(Field, {
33194
33394
  label: "pco:resource",
33195
33395
  value: describeTag("resource", resource)
@@ -33197,29 +33397,29 @@ true;
33197
33397
  label: "pco:recently_visitable",
33198
33398
  value: describeTag("recently_visitable", recentlyVisitable)
33199
33399
  }), hasInvalidResource && (/*#__PURE__*/React.createElement("p", {
33200
- className: styles$4["debug-pco-resource-note"]
33400
+ className: styles$5["debug-pco-resource-note"]
33201
33401
  }, "pco:resource isn't a PCRL (product/v2/type/id), so Recently Visited ignores it.")), visit === null && (/*#__PURE__*/React.createElement("p", {
33202
- className: styles$4["debug-pco-resource-note"]
33402
+ className: styles$5["debug-pco-resource-note"]
33203
33403
  }, "This page is not eligible for Recently Visited")), pcrl && hasRecentlyVisitable && (/*#__PURE__*/React.createElement("p", {
33204
- className: styles$4["debug-pco-resource-note"]
33404
+ className: styles$5["debug-pco-resource-note"]
33205
33405
  }, "Both tags are present \u2014 pco:resource takes precedence for the Recently Visited entry.")), pcrl && lookup.status === "loading" && (/*#__PURE__*/React.createElement("p", {
33206
- className: styles$4["debug-pco-resource-note"]
33406
+ className: styles$5["debug-pco-resource-note"]
33207
33407
  }, "Looking up resource\u2026")), pcrl && lookup.status === "error" && (/*#__PURE__*/React.createElement("p", {
33208
- className: styles$4["debug-pco-resource-note"]
33408
+ className: styles$5["debug-pco-resource-note"]
33209
33409
  }, lookup.message)), pcrl && lookup.status === "loaded" && (/*#__PURE__*/React.createElement("pre", {
33210
- className: styles$4["debug-pco-resource-json"]
33410
+ className: styles$5["debug-pco-resource-json"]
33211
33411
  }, JSON.stringify(lookup.json, null, 2))), (visit === null || visit === void 0 ? void 0 : visit.type) === "page" && (/*#__PURE__*/React.createElement("p", {
33212
- className: styles$4["debug-pco-resource-note"]
33412
+ className: styles$5["debug-pco-resource-note"]
33213
33413
  }, "Recently Visited title: ", visit.title)), /*#__PURE__*/React.createElement("span", {
33214
- className: styles$4["debug-pco-resource-field-label"]
33414
+ className: styles$5["debug-pco-resource-field-label"]
33215
33415
  }, "Recently Visited"), personId ? (/*#__PURE__*/React.createElement("pre", {
33216
- className: styles$4["debug-pco-resource-json"]
33416
+ className: styles$5["debug-pco-resource-json"]
33217
33417
  }, JSON.stringify(recentlyVisited, null, 2))) : (/*#__PURE__*/React.createElement("p", {
33218
- className: styles$4["debug-pco-resource-note"]
33418
+ className: styles$5["debug-pco-resource-note"]
33219
33419
  }, "No current person, so nothing is recorded.")));
33220
33420
  }
33221
33421
 
33222
- var styles$3 = {"drawer-wrap":"drawer-wrap-J0rSL","is-visible":"is-visible-KTME8","drawer":"drawer-j7f4l"};
33422
+ var styles$4 = {"drawer-wrap":"drawer-wrap-J0rSL","is-visible":"is-visible-KTME8","drawer":"drawer-j7f4l"};
33223
33423
 
33224
33424
  function Drawer({
33225
33425
  children,
@@ -33227,7 +33427,7 @@ true;
33227
33427
  }) {
33228
33428
  return /*#__PURE__*/React.createElement("div", {
33229
33429
  id: id,
33230
- className: styles$3["drawer"]
33430
+ className: styles$4["drawer"]
33231
33431
  }, children);
33232
33432
  }
33233
33433
  function DrawerWrap({
@@ -33237,7 +33437,7 @@ true;
33237
33437
  ...props
33238
33438
  }) {
33239
33439
  return /*#__PURE__*/React.createElement("div", {
33240
- className: classnames(styles$3["drawer-wrap"], className, isVisible && styles$3["is-visible"]),
33440
+ className: classnames(styles$4["drawer-wrap"], className, isVisible && styles$4["is-visible"]),
33241
33441
  ...props
33242
33442
  }, children);
33243
33443
  }
@@ -33453,7 +33653,7 @@ true;
33453
33653
  }));
33454
33654
  }
33455
33655
 
33456
- 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"};
33656
+ var styles$3 = {"color-mode-switcher":"color-mode-switcher-JpczO","color-mode-switcher-label":"color-mode-switcher-label-3jPM2","color-mode-switcher-modes":"color-mode-switcher-modes-u-bic"};
33457
33657
 
33458
33658
  const COLOR_MODES = [{
33459
33659
  icon: "toolbar.sun",
@@ -33476,11 +33676,11 @@ true;
33476
33676
  const updateStore = useUpdateStore();
33477
33677
  const [counter] = React.useState(() => COLOR_MODE_INSTANCE_COUNTER++);
33478
33678
  return /*#__PURE__*/React.createElement("fieldset", {
33479
- className: styles$2["color-mode-switcher"]
33679
+ className: styles$3["color-mode-switcher"]
33480
33680
  }, /*#__PURE__*/React.createElement("legend", {
33481
- className: styles$2["color-mode-switcher-label"]
33681
+ className: styles$3["color-mode-switcher-label"]
33482
33682
  }, "Appearance", " "), /*#__PURE__*/React.createElement("div", {
33483
- className: styles$2["color-mode-switcher-modes"]
33683
+ className: styles$3["color-mode-switcher-modes"]
33484
33684
  }, COLOR_MODES.map(({
33485
33685
  icon,
33486
33686
  label,
@@ -33750,7 +33950,7 @@ true;
33750
33950
  ...overrides
33751
33951
  });
33752
33952
 
33753
- var styles$1 = {"toolbar":"toolbar-KHJR5","toolbar-button":"toolbar-button-Ye4h5","is-current":"is-current-DZK47","toolbar-button--avatar":"toolbar-button--avatar-5FUik","toolbar-button--home":"toolbar-button--home-Pthd0"};
33953
+ var styles$2 = {"toolbar":"toolbar-KHJR5","toolbar-button":"toolbar-button-Ye4h5","is-current":"is-current-DZK47","toolbar-button--avatar":"toolbar-button--avatar-5FUik","toolbar-button--home":"toolbar-button--home-Pthd0"};
33754
33954
 
33755
33955
  // PCO Box serves TLS by default, and a bare http:// frame inside an https
33756
33956
  // product page is blocked as mixed content, so follow the page's scheme.
@@ -33922,7 +34122,7 @@ true;
33922
34122
  name: "DeveloperTools"
33923
34123
  };
33924
34124
 
33925
- var styles = {"banner":"banner-YfQ37","stacked":"stacked-EwLRh","content":"content-lNjWf","icon":"icon-abz7q","iconShape":"iconShape-6uyot","iconSparkle":"iconSparkle-t0OzK","text":"text--Vlhl","title":"title-zOEOR","subtitle":"subtitle-ufLnk","chevron":"chevron-oOUA-","inlineContent":"inlineContent-h5sVl","sparkle":"sparkle-uuMBp","inlineTitle":"inlineTitle-ru2Rk","action":"action-fSY3n","actionLabel":"actionLabel-MRBjb","actionChevron":"actionChevron-DghlN"};
34125
+ var styles$1 = {"banner":"banner-YfQ37","stacked":"stacked-EwLRh","content":"content-lNjWf","icon":"icon-abz7q","iconShape":"iconShape-6uyot","iconSparkle":"iconSparkle-t0OzK","text":"text--Vlhl","title":"title-zOEOR","subtitle":"subtitle-ufLnk","chevron":"chevron-oOUA-","inlineContent":"inlineContent-h5sVl","sparkle":"sparkle-uuMBp","inlineTitle":"inlineTitle-ru2Rk","action":"action-fSY3n","actionLabel":"actionLabel-MRBjb","actionChevron":"actionChevron-DghlN"};
33926
34126
 
33927
34127
  const SPARKLE_PATH$1 = "M3.809 10.1332C3.87511 9.95596 4.12579 9.95588 4.19181 10.1332L4.63712 11.3363C4.64169 11.3487 4.65116 11.359 4.66349 11.3637L5.86661 11.809C6.04452 11.8749 6.04455 12.126 5.86661 12.1918L4.66349 12.6371C4.65134 12.6417 4.64173 12.6513 4.63712 12.6635L4.19181 13.8666C4.12597 14.0445 3.87486 14.0445 3.809 13.8666L3.36368 12.6635C3.35905 12.6512 3.3487 12.6417 3.33634 12.6371L2.13321 12.1918C1.95588 12.1258 1.95596 11.8751 2.13321 11.809L3.33634 11.3637C3.34877 11.3591 3.35908 11.3488 3.36368 11.3363L3.809 10.1332ZM8.52091 3.33341C8.68571 2.88899 9.31518 2.88891 9.47989 3.33341L10.5922 6.34122C10.6037 6.37226 10.6285 6.39712 10.6596 6.4086L13.6664 7.52091C14.1113 7.68553 14.1113 8.31528 13.6664 8.47989L10.6596 9.5922C10.6285 9.6037 10.6037 9.6285 10.5922 9.65958L9.47989 12.6664C9.31528 13.1113 8.68553 13.1113 8.52091 12.6664L7.4086 9.65958C7.39712 9.62853 7.37226 9.60371 7.34122 9.5922L4.33341 8.47989C3.88891 8.31518 3.88899 7.68571 4.33341 7.52091L7.34122 6.4086C7.3723 6.3971 7.3971 6.3723 7.4086 6.34122L8.52091 3.33341ZM3.809 2.13321C3.87511 1.95596 4.12579 1.95588 4.19181 2.13321L4.63712 3.33634C4.64169 3.3487 4.65116 3.35905 4.66349 3.36368L5.86661 3.809C6.04452 3.87486 6.04455 4.12597 5.86661 4.19181L4.66349 4.63712C4.65134 4.64173 4.64173 4.65134 4.63712 4.66349L4.19181 5.86661C4.12597 6.04455 3.87486 6.04452 3.809 5.86661L3.36368 4.66349C3.35905 4.65116 3.3487 4.64169 3.33634 4.63712L2.13321 4.19181C1.95588 4.12579 1.95596 3.87511 2.13321 3.809L3.33634 3.36368C3.34877 3.35908 3.35908 3.34877 3.36368 3.33634L3.809 2.13321Z";
33928
34128
  // Same stops as the AskAIBanner border gradient, so the two stay in sync.
@@ -33982,8 +34182,8 @@ true;
33982
34182
  var _a, _b;
33983
34183
  const isStacked = props.variant !== "inline";
33984
34184
  return /*#__PURE__*/React.createElement("button", {
33985
- className: classnames(styles.banner, {
33986
- [styles.stacked]: isStacked
34185
+ className: classnames(styles$1.banner, {
34186
+ [styles$1.stacked]: isStacked
33987
34187
  }),
33988
34188
  onClick: () => props.onClick(),
33989
34189
  type: "button"
@@ -33998,15 +34198,15 @@ true;
33998
34198
  title
33999
34199
  }) {
34000
34200
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
34001
- className: styles.content
34201
+ className: styles$1.content
34002
34202
  }, /*#__PURE__*/React.createElement(AskAIIcon, null), /*#__PURE__*/React.createElement("span", {
34003
- className: styles.text
34203
+ className: styles$1.text
34004
34204
  }, /*#__PURE__*/React.createElement("span", {
34005
- className: styles.title
34205
+ className: styles$1.title
34006
34206
  }, title), /*#__PURE__*/React.createElement("span", {
34007
- className: styles.subtitle
34207
+ className: styles$1.subtitle
34008
34208
  }, "Support answers tailored to you"))), /*#__PURE__*/React.createElement(tapestryReact.Icon, {
34009
- className: styles.chevron,
34209
+ className: styles$1.chevron,
34010
34210
  name: "general.rightChevron",
34011
34211
  size: "16px"
34012
34212
  }));
@@ -34016,18 +34216,18 @@ true;
34016
34216
  title
34017
34217
  }) {
34018
34218
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
34019
- className: styles.inlineContent
34219
+ className: styles$1.inlineContent
34020
34220
  }, /*#__PURE__*/React.createElement(SparkleIcon, {
34021
- className: styles.sparkle,
34221
+ className: styles$1.sparkle,
34022
34222
  fill: "currentColor"
34023
34223
  }), /*#__PURE__*/React.createElement("span", {
34024
- className: styles.inlineTitle
34224
+ className: styles$1.inlineTitle
34025
34225
  }, title)), /*#__PURE__*/React.createElement("span", {
34026
- className: styles.action
34226
+ className: styles$1.action
34027
34227
  }, /*#__PURE__*/React.createElement("span", {
34028
- className: styles.actionLabel
34228
+ className: styles$1.actionLabel
34029
34229
  }, action), /*#__PURE__*/React.createElement(tapestryReact.Icon, {
34030
- className: styles.actionChevron,
34230
+ className: styles$1.actionChevron,
34031
34231
  name: "general.rightChevron",
34032
34232
  size: "14px"
34033
34233
  })));
@@ -34044,9 +34244,9 @@ true;
34044
34244
  const borderA = `${uid}-border-a`;
34045
34245
  const borderB = `${uid}-border-b`;
34046
34246
  return /*#__PURE__*/React.createElement("span", {
34047
- className: styles.icon
34247
+ className: styles$1.icon
34048
34248
  }, /*#__PURE__*/React.createElement("svg", {
34049
- className: styles.iconShape,
34249
+ className: styles$1.iconShape,
34050
34250
  viewBox: "0 0 32 32",
34051
34251
  fill: "none",
34052
34252
  "aria-hidden": "true"
@@ -34217,7 +34417,7 @@ true;
34217
34417
  stopColor: "#D48B91",
34218
34418
  stopOpacity: "0"
34219
34419
  })))), /*#__PURE__*/React.createElement("svg", {
34220
- className: styles.iconSparkle,
34420
+ className: styles$1.iconSparkle,
34221
34421
  viewBox: "0 0 19.4995 17.9999",
34222
34422
  "aria-hidden": "true"
34223
34423
  }, /*#__PURE__*/React.createElement("path", {
@@ -34226,6 +34426,23 @@ true;
34226
34426
  })));
34227
34427
  }
34228
34428
 
34429
+ var styles = {"button":"button-2NVI1","sparkle":"sparkle-ruJGP","label":"label-4NUGB"};
34430
+
34431
+ function AskAIButton({
34432
+ onClick,
34433
+ text = "Ask AI"
34434
+ }) {
34435
+ return /*#__PURE__*/React.createElement("button", {
34436
+ className: styles.button,
34437
+ onClick: () => onClick(),
34438
+ type: "button"
34439
+ }, /*#__PURE__*/React.createElement(SparkleIcon, {
34440
+ className: styles.sparkle
34441
+ }), /*#__PURE__*/React.createElement("span", {
34442
+ className: styles.label
34443
+ }, text));
34444
+ }
34445
+
34229
34446
  // The article heading lives in the deepest populated hierarchy level (lvl0 is
34230
34447
  // always "Planning Center").
34231
34448
  const hitTitle = hierarchy => (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl6) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl5) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl4) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl3) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl2) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl1) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl0) || "";
@@ -34536,6 +34753,18 @@ true;
34536
34753
  }
34537
34754
  }
34538
34755
  },
34756
+ AskAIButton: {
34757
+ component: AskAIButton,
34758
+ properties: {
34759
+ onClick: {
34760
+ event: false,
34761
+ type: Function
34762
+ },
34763
+ text: {
34764
+ type: String
34765
+ }
34766
+ }
34767
+ },
34539
34768
  CalendlyWidget: {
34540
34769
  component: CalendlyWidget,
34541
34770
  properties: {
@@ -36044,18 +36273,18 @@ true;
36044
36273
  return /*#__PURE__*/React.createElement(tapestryReact.ThemeProvider, {
36045
36274
  theme: topbarTheme
36046
36275
  }, /*#__PURE__*/React.createElement(addOns.UiExtensionDataContext, null, /*#__PURE__*/React.createElement("div", {
36047
- className: styles$1["toolbar"],
36276
+ className: styles$2["toolbar"],
36048
36277
  "data-pendo": "toolbar-profile-support-wrapper"
36049
36278
  }, /*#__PURE__*/React.createElement(ToolbarButton, {
36050
36279
  "aria-label": `Open ${userName}’s profile drawer`,
36051
36280
  onClick: () => toggleDrawer(Profile),
36052
36281
  "data-pendo": "toolbar-profile",
36053
- className: styles$1["toolbar-button--avatar"]
36282
+ className: styles$2["toolbar-button--avatar"]
36054
36283
  }, /*#__PURE__*/React.createElement(Avatar, {
36055
36284
  env: env,
36056
36285
  url: userAvatarPath
36057
36286
  })), /*#__PURE__*/React.createElement(ToolbarButton, {
36058
- className: styles$1["toolbar-button--home"],
36287
+ className: styles$2["toolbar-button--home"],
36059
36288
  onClick: () => closeDrawer()
36060
36289
  }, /*#__PURE__*/React.createElement(tapestryReact.Icon, {
36061
36290
  name: "toolbar.planningCenter",
@@ -36092,7 +36321,7 @@ true;
36092
36321
  }) => {
36093
36322
  return /*#__PURE__*/React.createElement("button", {
36094
36323
  type: "button",
36095
- className: classnames(styles$1["toolbar-button"], className, current && styles$1["is-current"]),
36324
+ className: classnames(styles$2["toolbar-button"], className, current && styles$2["is-current"]),
36096
36325
  onClick: onClick,
36097
36326
  "data-toolbar-link": "true",
36098
36327
  ...restProps