@planningcenter/topbar 12.3.0 → 12.4.0-rc.1

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.
@@ -6,7 +6,7 @@ import * as general from '@planningcenter/icons/paths/general';
6
6
  import Jolt from '@planningcenter/jolt-client';
7
7
  import { useSyncExternalStore } from 'use-sync-external-store/shim';
8
8
  import 'react/jsx-runtime';
9
- import { ThemeProvider, Divider, Button as Button$1, StackView as StackView$2, Text, Icon, designSystem, Box, Heading, GridView, Avatar as Avatar$1, Link as Link$1, HeadingUppercase, List, Combobox, Logo, Dropdown, Spinner, Badge } from '@planningcenter/tapestry-react';
9
+ import { ThemeProvider, Divider, Button as Button$1, StackView as StackView$2, Text, Icon as Icon$1, designSystem, Box, Heading, GridView, Avatar as Avatar$1, Link as Link$1, HeadingUppercase, List, Combobox, Logo, Dropdown, Spinner, Badge } from '@planningcenter/tapestry-react';
10
10
  import { components, UiExtensionFirstPartyComponent, UiExtensionDataContext } from '@planningcenter/add-ons';
11
11
  import * as churchCenter from '@planningcenter/icons/paths/church-center';
12
12
  import * as people from '@planningcenter/icons/paths/people';
@@ -17,7 +17,7 @@ import { useSearchBox, useHits, InstantSearch, Configure } from 'react-instantse
17
17
  import { TransitionGroup, Transition } from 'react-transition-group';
18
18
  import datetimeFmt from '@planningcenter/datetime-fmt';
19
19
 
20
- var styles$l = {"accessibility-toolbar":"accessibility-toolbar-jgjUG"};
20
+ var styles$m = {"accessibility-toolbar":"accessibility-toolbar-jgjUG"};
21
21
 
22
22
  const AccessibilityToolbar = ({
23
23
  children
@@ -29,7 +29,7 @@ const AccessibilityToolbar = ({
29
29
  (_a = document.getElementById("main-content")) === null || _a === void 0 ? void 0 : _a.focus();
30
30
  };
31
31
  return /*#__PURE__*/React__default.createElement("div", {
32
- className: styles$l["accessibility-toolbar"]
32
+ className: styles$m["accessibility-toolbar"]
33
33
  }, /*#__PURE__*/React__default.createElement(Link, {
34
34
  href: "#main-content",
35
35
  kind: "primary",
@@ -156,12 +156,14 @@ function inferRailsCsrfToken() {
156
156
  }
157
157
  function apiRequest(fetch, url, {
158
158
  method = "GET",
159
- data = null
159
+ data = null,
160
+ headers = {}
160
161
  } = {}) {
161
162
  const options = {
162
163
  headers: {
163
164
  Accept: "application/json",
164
- "Content-Type": "application/json"
165
+ "Content-Type": "application/json",
166
+ ...headers
165
167
  },
166
168
  method,
167
169
  redirect: "follow"
@@ -346,11 +348,11 @@ const X = ({
346
348
  fill: fill
347
349
  })));
348
350
 
349
- var styles$k = {"announcement":"announcement-pI5tM","announcement--platform":"announcement--platform-EcUn2","announcement--promo":"announcement--promo-FarUk","announcement-icon":"announcement-icon-Ov39-","announcement-content":"announcement-content-q2ad6","dismiss":"dismiss-wUAoU","dismiss--promo":"dismiss--promo-7zvO4"};
351
+ var styles$l = {"announcement":"announcement-pI5tM","announcement--platform":"announcement--platform-EcUn2","announcement--promo":"announcement--promo-FarUk","announcement-icon":"announcement-icon-Ov39-","announcement-content":"announcement-content-q2ad6","dismiss":"dismiss-wUAoU","dismiss--promo":"dismiss--promo-7zvO4"};
350
352
 
351
353
  function AnnouncementIcon() {
352
354
  return /*#__PURE__*/React__default.createElement("svg", {
353
- className: styles$k["announcement-icon"],
355
+ className: styles$l["announcement-icon"],
354
356
  width: "16",
355
357
  height: "16",
356
358
  viewBox: "0 0 16 16",
@@ -475,7 +477,7 @@ let StyledAnnouncement$1 = class StyledAnnouncement extends React__default.Compo
475
477
  } = this.props;
476
478
  const isPromo = variant === "promo";
477
479
  return /*#__PURE__*/React__default.createElement("div", {
478
- className: classnames(styles$k["announcement"], styles$k[`announcement--${variant !== null && variant !== void 0 ? variant : "platform"}`]),
480
+ className: classnames(styles$l["announcement"], styles$l[`announcement--${variant !== null && variant !== void 0 ? variant : "platform"}`]),
479
481
  "data-announcement-variant": variant !== null && variant !== void 0 ? variant : "platform",
480
482
  style: isPromo ? {
481
483
  backgroundImage: promoBackgroundUrl,
@@ -484,7 +486,7 @@ let StyledAnnouncement$1 = class StyledAnnouncement extends React__default.Compo
484
486
  backgroundSize: "cover"
485
487
  } : undefined
486
488
  }, !isPromo && /*#__PURE__*/React__default.createElement(AnnouncementIcon, null), /*#__PURE__*/React__default.createElement("div", {
487
- className: styles$k["announcement-content"],
489
+ className: styles$l["announcement-content"],
488
490
  style: style,
489
491
  ...platformProps
490
492
  }));
@@ -533,7 +535,7 @@ function StyledDismissButton$1({
533
535
  const isPromo = variant === "promo";
534
536
  return /*#__PURE__*/React__default.createElement("button", {
535
537
  type: "button",
536
- className: classnames(styles$k.dismiss, isPromo && styles$k["dismiss--promo"]),
538
+ className: classnames(styles$l.dismiss, isPromo && styles$l["dismiss--promo"]),
537
539
  ...props
538
540
  }, /*#__PURE__*/React__default.createElement(X, {
539
541
  fill: isPromo ? "currentColor" : "white",
@@ -613,9 +615,9 @@ class StyledAnnouncement extends React__default.Component {
613
615
  ...platformProps
614
616
  } = this.props;
615
617
  return /*#__PURE__*/React__default.createElement("div", {
616
- className: styles$k["announcement"]
618
+ className: styles$l["announcement"]
617
619
  }, /*#__PURE__*/React__default.createElement(AnnouncementIcon, null), /*#__PURE__*/React__default.createElement("div", {
618
- className: styles$k["announcement-content"],
620
+ className: styles$l["announcement-content"],
619
621
  style: style,
620
622
  ...platformProps
621
623
  }));
@@ -626,7 +628,7 @@ function StyledDismissButton({
626
628
  }) {
627
629
  return /*#__PURE__*/React__default.createElement("button", {
628
630
  type: "button",
629
- className: styles$k.dismiss,
631
+ className: styles$l.dismiss,
630
632
  ...props
631
633
  }, /*#__PURE__*/React__default.createElement(X, {
632
634
  fill: "white",
@@ -781,7 +783,7 @@ function useMediaQuery(query) {
781
783
  return matches;
782
784
  }
783
785
 
784
- const capitalize = s => s.charAt(0).toUpperCase() + s.slice(1);
786
+ const capitalize$1 = s => s.charAt(0).toUpperCase() + s.slice(1);
785
787
  const RESOLVED_COLOR_MODE_ATTRIBUTE = "data-resolved-color-mode";
786
788
  function resolveColorMode(colorMode) {
787
789
  if (colorMode === "system") {
@@ -1748,7 +1750,7 @@ function MonoAppIcon({
1748
1750
  })) : (/*#__PURE__*/React__default.createElement("div", null, "appName not supported")));
1749
1751
  }
1750
1752
 
1751
- var styles$j = {"icon-slot":"icon-slot-m3ZIk","name":"name-UNqbW"};
1753
+ var styles$k = {"icon-slot":"icon-slot-m3ZIk","name":"name-UNqbW"};
1752
1754
 
1753
1755
  const AppRow = ({
1754
1756
  appName,
@@ -1757,24 +1759,24 @@ const AppRow = ({
1757
1759
  }) => {
1758
1760
  if (appName === "Accounts") {
1759
1761
  return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("span", {
1760
- className: styles$j["icon-slot"]
1762
+ className: styles$k["icon-slot"]
1761
1763
  }, /*#__PURE__*/React__default.createElement(MonoAppIcon, {
1762
1764
  appName: "Accounts",
1763
1765
  colors: colors,
1764
1766
  size: iconSize
1765
1767
  })), /*#__PURE__*/React__default.createElement("span", {
1766
- className: styles$j.name
1768
+ className: styles$k.name
1767
1769
  }, "Account settings"));
1768
1770
  }
1769
1771
  return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(ColorAppIcon, {
1770
1772
  appName: appName.replace(/[\s-]/, ""),
1771
1773
  size: iconSize
1772
1774
  }), /*#__PURE__*/React__default.createElement("span", {
1773
- className: styles$j.name
1775
+ className: styles$k.name
1774
1776
  }, appName));
1775
1777
  };
1776
1778
 
1777
- var styles$i = {"button":"button-XMbeQ","label":"label-884of","chevron":"chevron-RRg9k"};
1779
+ var styles$j = {"button":"button-XMbeQ","label":"label-884of","chevron":"chevron-RRg9k"};
1778
1780
 
1779
1781
  const AppsButton = /*#__PURE__*/React__default.forwardRef(({
1780
1782
  appName,
@@ -1786,10 +1788,10 @@ const AppsButton = /*#__PURE__*/React__default.forwardRef(({
1786
1788
  ...restProps,
1787
1789
  ref: ref,
1788
1790
  "aria-label": `App switcher: ${appName}`,
1789
- className: styles$i.button,
1791
+ className: styles$j.button,
1790
1792
  kind: "neutral-inline",
1791
1793
  label: /*#__PURE__*/React__default.createElement("span", {
1792
- className: styles$i.label
1794
+ className: styles$j.label
1793
1795
  }, isAccounts ? "Account settings" : appName),
1794
1796
  prefix: isAccounts ? (/*#__PURE__*/React__default.createElement(MonoAppIcon, {
1795
1797
  appName: "Accounts",
@@ -1800,13 +1802,13 @@ const AppsButton = /*#__PURE__*/React__default.forwardRef(({
1800
1802
  size: 22
1801
1803
  })),
1802
1804
  suffix: /*#__PURE__*/React__default.createElement(DisclosureChevronIcon, {
1803
- className: `${styles$i.chevron} symbol`
1805
+ className: `${styles$j.chevron} symbol`
1804
1806
  })
1805
1807
  });
1806
1808
  });
1807
1809
  AppsButton.displayName = "AppsButton";
1808
1810
 
1809
- var styles$h = {"nav":"nav-y-6ML","popover-content":"popover-content-jOAAY","list":"list-jUxvp","link":"link-LMp2h","current":"current-4U4VL","accounts-item":"accounts-item-erJW0","accounts-link":"accounts-link-4jwPW"};
1811
+ var styles$i = {"nav":"nav-y-6ML","popover-content":"popover-content-jOAAY","list":"list-jUxvp","link":"link-LMp2h","current":"current-4U4VL","accounts-item":"accounts-item-erJW0","accounts-link":"accounts-link-4jwPW"};
1810
1812
 
1811
1813
  const AppsMenu = ({
1812
1814
  appName,
@@ -1827,8 +1829,8 @@ const AppsMenu = ({
1827
1829
  }, /*#__PURE__*/React__default.createElement("a", {
1828
1830
  "aria-current": isCurrent ? "page" : undefined,
1829
1831
  "aria-label": name,
1830
- className: classnames(styles$h.link, {
1831
- [styles$h.current]: isCurrent
1832
+ className: classnames(styles$i.link, {
1833
+ [styles$i.current]: isCurrent
1832
1834
  }),
1833
1835
  "data-pendo": `app-switcher--change-app--${name}`,
1834
1836
  "data-turbolinks": false,
@@ -1844,22 +1846,22 @@ const AppsMenu = ({
1844
1846
  colors: colors
1845
1847
  })), /*#__PURE__*/React__default.createElement(PopoverContent, {
1846
1848
  "aria-label": "Switch app",
1847
- className: styles$h["popover-content"],
1849
+ className: styles$i["popover-content"],
1848
1850
  placement: "bottom start"
1849
1851
  }, /*#__PURE__*/React__default.createElement("nav", {
1850
- className: styles$h.nav,
1852
+ className: styles$i.nav,
1851
1853
  "data-pendo": "app-switcher"
1852
1854
  }, /*#__PURE__*/React__default.createElement("ul", {
1853
- className: styles$h.list,
1855
+ className: styles$i.list,
1854
1856
  role: "list"
1855
1857
  }, homeProduct && renderProductLink(homeProduct.name), restProducts.map(({
1856
1858
  name
1857
1859
  }) => renderProductLink(name)), accountsProduct && (/*#__PURE__*/React__default.createElement("li", {
1858
- className: styles$h["accounts-item"]
1860
+ className: styles$i["accounts-item"]
1859
1861
  }, /*#__PURE__*/React__default.createElement("a", {
1860
1862
  "aria-current": appName === accountsProduct.name ? "page" : undefined,
1861
1863
  "aria-label": "Account settings",
1862
- className: classnames(styles$h.link, styles$h["accounts-link"]),
1864
+ className: classnames(styles$i.link, styles$i["accounts-link"]),
1863
1865
  "data-pendo": "app-switcher--change-app--Accounts",
1864
1866
  "data-turbolinks": false,
1865
1867
  href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(accountsProduct.name)
@@ -2067,7 +2069,7 @@ function MonoAppName({
2067
2069
  })) : (/*#__PURE__*/React__default.createElement("div", null, "appName not supported"));
2068
2070
  }
2069
2071
 
2070
- var styles$g = {"unbutton":"unbutton-lEbTR"};
2072
+ var styles$h = {"unbutton":"unbutton-lEbTR"};
2071
2073
 
2072
2074
  function Unbutton({
2073
2075
  className,
@@ -2076,7 +2078,7 @@ function Unbutton({
2076
2078
  ...nativeProps
2077
2079
  }) {
2078
2080
  return /*#__PURE__*/React__default.createElement("button", {
2079
- className: classnames(styles$g.unbutton, className),
2081
+ className: classnames(styles$h.unbutton, className),
2080
2082
  style: {
2081
2083
  ...style
2082
2084
  },
@@ -2085,7 +2087,7 @@ function Unbutton({
2085
2087
  });
2086
2088
  }
2087
2089
 
2088
- var styles$f = {"button":"button-wsipc","button-accounts":"button-accounts-zQmBM","chevron":"chevron-obyBF","is-expanded":"is-expanded-PoCQ9"};
2090
+ var styles$g = {"button":"button-wsipc","button-accounts":"button-accounts-zQmBM","chevron":"chevron-obyBF","is-expanded":"is-expanded-PoCQ9"};
2089
2091
 
2090
2092
  const LegacyAppsButton = ({
2091
2093
  appName,
@@ -2100,8 +2102,8 @@ const LegacyAppsButton = ({
2100
2102
  "aria-haspopup": "true",
2101
2103
  "aria-label": `App switcher: ${appName}`,
2102
2104
  onClick: onClick,
2103
- className: classnames(styles$f.button, {
2104
- [styles$f["is-expanded"]]: expanded
2105
+ className: classnames(styles$g.button, {
2106
+ [styles$g["is-expanded"]]: expanded
2105
2107
  }),
2106
2108
  type: "button",
2107
2109
  style: style,
@@ -2110,7 +2112,7 @@ const LegacyAppsButton = ({
2110
2112
  appName: "Accounts",
2111
2113
  colors: colors
2112
2114
  }), /*#__PURE__*/React__default.createElement("div", {
2113
- className: styles$f["button-accounts"]
2115
+ className: styles$g["button-accounts"]
2114
2116
  }, "Account settings"))) : (/*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(ColorAppIcon, {
2115
2117
  appName: appName.replace(/[\s-]/, "")
2116
2118
  }), /*#__PURE__*/React__default.createElement(MonoAppName, {
@@ -2118,7 +2120,7 @@ const LegacyAppsButton = ({
2118
2120
  key: "mono-app-name",
2119
2121
  size: 24
2120
2122
  }))), /*#__PURE__*/React__default.createElement(DisclosureChevronIcon, {
2121
- className: styles$f.chevron,
2123
+ className: styles$g.chevron,
2122
2124
  key: "disclosure-chevron-icon"
2123
2125
  }));
2124
2126
  };
@@ -2251,7 +2253,7 @@ class Outsider extends React__default.Component {
2251
2253
  }
2252
2254
  }
2253
2255
 
2254
- var styles$e = {"popup-root":"popup-root-SeL5u","popup":"popup-nQXYW","is-visible":"is-visible-3DAUP"};
2256
+ var styles$f = {"popup-root":"popup-root-SeL5u","popup":"popup-nQXYW","is-visible":"is-visible-3DAUP"};
2255
2257
 
2256
2258
  const Popup = ({
2257
2259
  className,
@@ -2259,8 +2261,8 @@ const Popup = ({
2259
2261
  isVisible,
2260
2262
  ...nativeProps
2261
2263
  }) => (/*#__PURE__*/React__default.createElement(C, {
2262
- className: classnames(styles$e.popup, {
2263
- [styles$e["is-visible"]]: isVisible
2264
+ className: classnames(styles$f.popup, {
2265
+ [styles$f["is-visible"]]: isVisible
2264
2266
  }, className),
2265
2267
  ...nativeProps
2266
2268
  }));
@@ -2268,11 +2270,11 @@ const Popup = ({
2268
2270
  const PopupRoot = ({
2269
2271
  ...nativeProps
2270
2272
  }) => (/*#__PURE__*/React__default.createElement("div", {
2271
- className: styles$e["popup-root"],
2273
+ className: styles$f["popup-root"],
2272
2274
  ...nativeProps
2273
2275
  }));
2274
2276
 
2275
- var styles$d = {"popup":"popup-bVhSa"};
2277
+ var styles$e = {"popup":"popup-bVhSa"};
2276
2278
 
2277
2279
  const LegacyAppsMenu = ({
2278
2280
  appName,
@@ -2310,7 +2312,7 @@ const LegacyAppsMenu = ({
2310
2312
  cleanup: !visible,
2311
2313
  component: Outsider,
2312
2314
  onOutsideClick: toggle,
2313
- className: styles$d.popup,
2315
+ className: styles$e.popup,
2314
2316
  isVisible: visible
2315
2317
  }, /*#__PURE__*/React__default.createElement("menu", {
2316
2318
  ref: menuRef,
@@ -3223,27 +3225,34 @@ const useUpdateStore = () => useCallback(updates => {
3223
3225
  const isMac = () => /Mac|iPhone|iPad|iPod/.test(navigator.platform);
3224
3226
  const hasCommandKey = event => isMac() ? event.metaKey : event.ctrlKey;
3225
3227
  const commandKeyLabel = () => isMac() ? "⌘" : "Ctrl+";
3228
+ const isSearchShortcut = event => event.key.toLowerCase() === "k" && !event.altKey && !event.shiftKey && hasCommandKey(event);
3229
+ // Products opened their own search with `/` before this one replaced it.
3230
+ const isLegacySearchShortcut = event => event.key === "/" && !event.altKey && !event.ctrlKey && !event.metaKey && !isTypingIn(event.target);
3231
+ const isCopyShortcut = event => event.key.toLowerCase() === "c" && !event.altKey && !event.shiftKey && hasCommandKey(event);
3232
+ const isTypingIn = target => target instanceof Element && target.closest('input, select, textarea, [contenteditable]:not([contenteditable="false"])') !== null;
3233
+ const isTypingKey = event => event.key.length === 1 && event.key !== " " && !event.altKey && !event.ctrlKey && !event.metaKey;
3234
+ const verticalStep = key => ({
3235
+ ArrowDown: 1,
3236
+ ArrowUp: -1
3237
+ })[key];
3238
+ const horizontalStep = key => ({
3239
+ ArrowLeft: -1,
3240
+ ArrowRight: 1
3241
+ })[key];
3226
3242
 
3227
- const STORAGE_KEY = "topbar.product_search.last_product";
3228
- function readLastProduct() {
3229
- try {
3230
- return localStorage.getItem(STORAGE_KEY);
3231
- } catch {
3232
- return null;
3233
- }
3234
- }
3235
- function saveLastProduct(slug) {
3236
- try {
3237
- localStorage.setItem(STORAGE_KEY, slug);
3238
- } catch {
3239
- // Storage can be full or blocked; search still works without it.
3240
- }
3243
+ function SearchButton({
3244
+ onClick
3245
+ }) {
3246
+ return /*#__PURE__*/React__default.createElement("button", {
3247
+ "aria-keyshortcuts": `${isMac() ? "Meta+K" : "Control+K"} /`,
3248
+ "aria-label": "Search",
3249
+ onClick: onClick,
3250
+ type: "button"
3251
+ }, /*#__PURE__*/React__default.createElement(Spyglass, {
3252
+ fill: "currentColor"
3253
+ }));
3241
3254
  }
3242
3255
 
3243
- var styles$c = {"visuallyHidden":"visuallyHidden-C28ju"};
3244
-
3245
- var styles$b = {"tab":"tab-lQLDx"};
3246
-
3247
3256
  // useId isn't available on React 17, which our peer range still supports.
3248
3257
  let instanceCount$1 = 0;
3249
3258
  function useInstanceId(prefix) {
@@ -3251,173 +3260,33 @@ function useInstanceId(prefix) {
3251
3260
  return id;
3252
3261
  }
3253
3262
 
3254
- function ProductTabs({
3255
- children,
3256
- onKeyDown,
3257
- onSelect,
3258
- products,
3259
- selected
3260
- }) {
3261
- const id = useInstanceId("product-tabs");
3262
- const tabId = slug => `${id}-tab-${slug}`;
3263
- const tabRefs = React__default.useRef(new Map());
3264
- const handleKeyDown = event => {
3265
- var _a;
3266
- const offset = {
3267
- ArrowLeft: -1,
3268
- ArrowRight: 1
3269
- }[event.key];
3270
- if (offset) {
3271
- event.preventDefault();
3272
- const index = products.indexOf(selected);
3273
- const next = products[(index + offset + products.length) % products.length];
3274
- onSelect(next.slug);
3275
- (_a = tabRefs.current.get(next.slug)) === null || _a === void 0 ? void 0 : _a.focus();
3276
- } else {
3277
- onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
3278
- }
3279
- };
3280
- return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("div", {
3281
- "aria-label": "Product",
3282
- onKeyDown: handleKeyDown,
3283
- role: "tablist"
3284
- }, products.map(product => (/*#__PURE__*/React__default.createElement("button", {
3285
- "aria-controls": `${id}-panel`,
3286
- "aria-selected": product === selected,
3287
- className: styles$b.tab,
3288
- id: tabId(product.slug),
3289
- key: product.slug,
3290
- onClick: () => onSelect(product.slug),
3291
- ref: element => {
3292
- if (element) {
3293
- tabRefs.current.set(product.slug, element);
3294
- } else {
3295
- tabRefs.current.delete(product.slug);
3296
- }
3297
- },
3298
- role: "tab",
3299
- tabIndex: product === selected ? 0 : -1,
3300
- type: "button"
3301
- }, product.appName)))), /*#__PURE__*/React__default.createElement("div", {
3302
- "aria-labelledby": tabId(selected.slug),
3303
- id: `${id}-panel`,
3304
- role: "tabpanel"
3305
- }, children));
3306
- }
3307
-
3308
- var styles$a = {"result":"result-WCnxr"};
3309
-
3310
- function SearchResult({
3311
- highlighted,
3312
- id,
3313
- onHighlight,
3314
- result
3315
- }) {
3316
- const {
3317
- metadata,
3318
- title,
3319
- url
3320
- } = result.attributes;
3321
- const ref = React__default.useRef(null);
3322
- React__default.useEffect(() => {
3323
- var _a;
3324
- if (highlighted) (_a = ref.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({
3325
- block: "nearest"
3326
- });
3327
- }, [highlighted]);
3328
- // Focus stays in the search input, which points at the highlighted result
3329
- // with aria-activedescendant, so results are kept out of the tab order.
3330
- // Mouse move rather than enter: arrowing scrolls results under a resting
3331
- // pointer, and that shouldn't steal the highlight.
3332
- return /*#__PURE__*/React__default.createElement("a", {
3333
- "aria-selected": highlighted,
3334
- className: styles$a.result,
3335
- href: url,
3336
- id: id,
3337
- onMouseMove: onHighlight,
3338
- ref: ref,
3339
- role: "option",
3340
- tabIndex: -1
3341
- }, /*#__PURE__*/React__default.createElement("div", null, title), /*#__PURE__*/React__default.createElement("div", null, summarize(metadata)));
3342
- }
3343
- function summarize(metadata) {
3344
- return Object.values(metadata !== null && metadata !== void 0 ? metadata : {}).filter(value => typeof value === "string" && value !== "").join(" · ");
3345
- }
3346
-
3347
- const resultId = (listboxId, index) => `${listboxId}-result-${index}`;
3348
- function SearchResults({
3349
- search,
3350
- term,
3351
- ...navigation
3352
- }) {
3353
- var _a;
3354
- switch (search.status) {
3355
- case "idle":
3356
- return null;
3357
- case "loading":
3358
- if ((_a = search.groups) === null || _a === void 0 ? void 0 : _a.length) {
3359
- return /*#__PURE__*/React__default.createElement(ResultGroups, {
3360
- busy: true,
3361
- groups: search.groups,
3362
- ...navigation
3363
- });
3364
- }
3365
- return /*#__PURE__*/React__default.createElement("p", {
3366
- role: "status"
3367
- }, "Searching\u2026");
3368
- case "error":
3369
- return /*#__PURE__*/React__default.createElement("p", {
3370
- role: "alert"
3371
- }, "Search isn\u2019t working right now. Try again.");
3372
- case "done":
3373
- if (search.groups.length === 0) {
3374
- return /*#__PURE__*/React__default.createElement("p", {
3375
- role: "status"
3376
- }, "No results for \u201C", term.trim(), "\u201D");
3377
- }
3378
- return /*#__PURE__*/React__default.createElement(ResultGroups, {
3379
- groups: search.groups,
3380
- ...navigation
3381
- });
3263
+ // The versions home-mobile pins, so web and mobile read the same response
3264
+ // shapes. Some endpoints only exist from a version on: Registrations' me,
3265
+ // product_search_results, and pcrl_lookups need 2025-05-01.
3266
+ const API_VERSIONS = {
3267
+ calendar: "2022-07-07",
3268
+ "check-ins": "2025-05-28",
3269
+ giving: "2019-10-18",
3270
+ groups: "2023-07-10",
3271
+ home: "2018-08-01",
3272
+ people: "2023-03-21",
3273
+ publishing: "2024-03-25",
3274
+ registrations: "2025-05-01",
3275
+ services: "2018-11-01"
3276
+ };
3277
+ // For a `/~api/{product}/v2/...` URL, the header that pins its product's
3278
+ // version.
3279
+ function apiVersionHeaders(url) {
3280
+ const [,, product] = new URL(url).pathname.split("/");
3281
+ const version = API_VERSIONS[product];
3282
+ if (version) {
3283
+ return {
3284
+ "X-PCO-API-Version": version
3285
+ };
3286
+ } else {
3287
+ return {};
3382
3288
  }
3383
3289
  }
3384
- function ResultGroups({
3385
- busy = false,
3386
- groups,
3387
- highlightedIndex,
3388
- id,
3389
- label,
3390
- onHighlight
3391
- }) {
3392
- let index = 0;
3393
- return /*#__PURE__*/React__default.createElement("div", {
3394
- "aria-busy": busy,
3395
- "aria-label": label,
3396
- id: id,
3397
- role: "listbox"
3398
- }, groups.map(({
3399
- results,
3400
- type
3401
- }, groupIndex) => {
3402
- const headingId = `${id}-group-${groupIndex}`;
3403
- return /*#__PURE__*/React__default.createElement("div", {
3404
- "aria-labelledby": headingId,
3405
- key: type,
3406
- role: "group"
3407
- }, /*#__PURE__*/React__default.createElement("div", {
3408
- id: headingId
3409
- }, type), results.map(result => {
3410
- const resultIndex = index++;
3411
- return /*#__PURE__*/React__default.createElement(SearchResult, {
3412
- highlighted: resultIndex === highlightedIndex,
3413
- id: resultId(id, resultIndex),
3414
- key: result.id,
3415
- onHighlight: () => onHighlight(resultIndex),
3416
- result: result
3417
- });
3418
- }));
3419
- }));
3420
- }
3421
3290
 
3422
3291
  const CACHE_TTL_MS = 60000;
3423
3292
  const MAX_ENTRIES$1 = 50;
@@ -3434,7 +3303,9 @@ function cachedRequest(url) {
3434
3303
  }
3435
3304
  }
3436
3305
  function fetchIntoCache(url) {
3437
- const promise = apiRequest(defaultFetch, url).then(({
3306
+ const promise = apiRequest(defaultFetch, url, {
3307
+ headers: apiVersionHeaders(url)
3308
+ }).then(({
3438
3309
  json,
3439
3310
  ok
3440
3311
  }) => {
@@ -3490,44 +3361,52 @@ function useCachedRequest(url) {
3490
3361
  };
3491
3362
  }
3492
3363
  }
3493
-
3494
- const SEARCH_DEBOUNCE_MS = 300;
3495
- function useProductSearch(product, term) {
3496
- const query = term.trim();
3497
- const debouncedQuery = useDebouncedValue(query, SEARCH_DEBOUNCE_MS);
3498
- const request = useCachedRequest(debouncedQuery ? productSearchUrl(product, debouncedQuery) : null);
3499
- const response = "data" in request ? request.data : undefined;
3500
- const settledProduct = useSettledProduct(product, request.status);
3501
- // While a request loads, the previous response stays on screen, but only
3502
- // if it was for this product: People results must never show under the
3503
- // Services tab.
3504
- const visibleResponse = request.status === "done" || settledProduct === product ? response : undefined;
3505
- const groups = React__default.useMemo(() => visibleResponse && groupByType(visibleResponse), [visibleResponse]);
3506
- if (!query) {
3507
- return {
3508
- status: "idle"
3509
- };
3510
- } else if (query !== debouncedQuery) {
3511
- return {
3512
- groups,
3513
- status: "loading"
3514
- };
3515
- } else if (request.status === "done") {
3516
- return {
3517
- groups: groups !== null && groups !== void 0 ? groups : [],
3518
- status: "done"
3519
- };
3520
- } else if (request.status === "error") {
3521
- return {
3522
- status: "error"
3523
- };
3524
- } else {
3525
- return {
3526
- groups,
3527
- status: "loading"
3364
+ // One request per URL, for callers that fan out to a varying number of
3365
+ // endpoints. Unlike `useCachedRequest`, a URL that hasn't settled yet has no
3366
+ // earlier data to fall back on.
3367
+ function useCachedRequests(urls) {
3368
+ const [settled, setSettled] = React__default.useState({});
3369
+ // Keyed by value because each settle re-renders the caller, and a caller
3370
+ // that builds a fresh `urls` array on render would otherwise loop forever.
3371
+ const key = urls.join("\n");
3372
+ React__default.useEffect(() => {
3373
+ let current = true;
3374
+ const settle = result => current && setSettled(prev => ({
3375
+ ...prev,
3376
+ [result.url]: result
3377
+ }));
3378
+ for (const url of key.split("\n").filter(Boolean)) {
3379
+ cachedRequest(url).then(data => settle({
3380
+ data,
3381
+ url
3382
+ }), () => settle({
3383
+ failed: true,
3384
+ url
3385
+ }));
3386
+ }
3387
+ return () => {
3388
+ current = false;
3528
3389
  };
3529
- }
3390
+ }, [key]);
3391
+ return urls.map(url => {
3392
+ const result = settled[url];
3393
+ if (!result) {
3394
+ return {
3395
+ status: "loading"
3396
+ };
3397
+ } else if ("data" in result) {
3398
+ return {
3399
+ data: result.data,
3400
+ status: "done"
3401
+ };
3402
+ } else {
3403
+ return {
3404
+ status: "error"
3405
+ };
3406
+ }
3407
+ });
3530
3408
  }
3409
+
3531
3410
  function productSearchUrl(product, query) {
3532
3411
  const params = new URLSearchParams({
3533
3412
  per_page: "100",
@@ -3558,14 +3437,7 @@ function groupByType({
3558
3437
  };
3559
3438
  return [...groups.values()].sort((a, b) => rank(a.type) - rank(b.type));
3560
3439
  }
3561
- // The product the most recent finished request was for.
3562
- function useSettledProduct(product, status) {
3563
- const [settledProduct, setSettledProduct] = React__default.useState();
3564
- React__default.useEffect(() => {
3565
- if (status === "done") setSettledProduct(product);
3566
- }, [product, status]);
3567
- return settledProduct;
3568
- }
3440
+
3569
3441
  function useDebouncedValue(value, delay) {
3570
3442
  const [debounced, setDebounced] = React__default.useState(value);
3571
3443
  React__default.useEffect(() => {
@@ -3575,310 +3447,50 @@ function useDebouncedValue(value, delay) {
3575
3447
  return debounced;
3576
3448
  }
3577
3449
 
3578
- const COPIED_FEEDBACK_MS = 1500;
3579
- const verticalStep = key => ({
3580
- ArrowDown: 1,
3581
- ArrowUp: -1
3582
- })[key];
3583
- function useResultNavigation(results) {
3584
- // Remembering which results the index belongs to puts the highlight back on
3585
- // the first result whenever new ones arrive, without an effect to reset it.
3586
- const [highlight, setHighlight] = React__default.useState({
3587
- index: 0,
3588
- results
3589
- });
3590
- const highlightedIndex = highlight.results === results ? highlight.index : 0;
3591
- const highlighted = results[highlightedIndex];
3592
- const [copiedUrl, setCopiedUrl] = React__default.useState(null);
3593
- React__default.useEffect(() => {
3594
- if (copiedUrl) {
3595
- const timeout = setTimeout(() => setCopiedUrl(null), COPIED_FEEDBACK_MS);
3596
- return () => clearTimeout(timeout);
3597
- } else {
3598
- return undefined;
3599
- }
3600
- }, [copiedUrl]);
3601
- const highlightIndex = React__default.useCallback(index => setHighlight(current => current.results === results && current.index === index ? current : {
3602
- index,
3603
- results
3604
- }), [results]);
3605
- const handleKeyDown = event => {
3606
- const step = verticalStep(event.key);
3607
- if (step && results.length > 0) {
3608
- event.preventDefault();
3609
- highlightIndex((highlightedIndex + step + results.length) % results.length);
3610
- } else if (event.key === "Enter" && highlighted && !event.nativeEvent.isComposing) {
3611
- event.preventDefault();
3612
- open(highlighted.attributes.url, {
3613
- newTab: hasCommandKey(event)
3614
- });
3615
- } else if (isCopyShortcut(event) && highlighted && !hasSelectedText(event.currentTarget)) {
3616
- event.preventDefault();
3617
- const {
3618
- url
3619
- } = highlighted.attributes;
3620
- navigator.clipboard.writeText(url).then(() => setCopiedUrl(url));
3621
- }
3622
- };
3623
- return {
3624
- copied: copiedUrl !== null && copiedUrl === (highlighted === null || highlighted === void 0 ? void 0 : highlighted.attributes.url),
3625
- handleKeyDown,
3626
- highlightIndex,
3627
- highlighted,
3628
- highlightedIndex: highlighted ? highlightedIndex : null
3629
- };
3630
- }
3631
- function open(url, {
3632
- newTab
3633
- }) {
3634
- if (newTab) {
3635
- window.open(url, "_blank", "noopener");
3636
- } else {
3637
- window.location.assign(url);
3638
- }
3639
- }
3640
- const isCopyShortcut = event => event.key.toLowerCase() === "c" && !event.altKey && !event.shiftKey && hasCommandKey(event);
3641
- // With text selected, ⌘C should copy that text as usual, not the link.
3642
- function hasSelectedText(element) {
3643
- var _a, _b;
3644
- const selectedInField = element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
3645
- return selectedInField || ((_b = (_a = window.getSelection()) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : "") !== "";
3646
- }
3647
-
3648
- const SEARCHABLE_PRODUCTS = [{
3649
- appName: "Groups",
3650
- slug: "groups"
3651
- }, {
3652
- appName: "People",
3653
- slug: "people"
3654
- }, {
3655
- appName: "Services",
3656
- slug: "services"
3657
- }];
3658
- const PERMISSION_CHECKS = {
3659
- people: () => fetchMe("people", "people_permissions,site_administrator").then(({
3660
- people_permissions,
3661
- site_administrator
3662
- }) => site_administrator || people_permissions !== null),
3663
- services: () => fetchMe("services", "can_search").then(({
3664
- can_search
3665
- }) => can_search)
3666
- };
3667
- function fetchMe(product, fields) {
3668
- const params = new URLSearchParams({
3669
- "fields[Person]": fields
3670
- });
3671
- return apiRequest(defaultFetch, `${window.location.origin}/~api/${product}/v2/me?${params}`).then(({
3672
- json,
3673
- ok
3674
- }) => {
3675
- if (!ok || json === null) throw new Error("Permission check failed");
3676
- return json.data.attributes;
3677
- });
3678
- }
3679
- // Permissions rarely change mid-session, so a successful check is kept until
3680
- // the page reloads. A failed one is dropped so a remount tries again.
3681
- const permissionCache = new Map();
3682
- function canSearch(slug) {
3683
- const check = PERMISSION_CHECKS[slug];
3684
- const cached = permissionCache.get(slug);
3685
- if (!check) {
3686
- return Promise.resolve(true);
3687
- } else if (cached) {
3688
- return cached;
3689
- } else {
3690
- const promise = check().catch(() => {
3691
- permissionCache.delete(slug);
3692
- return false;
3693
- });
3694
- permissionCache.set(slug, promise);
3695
- return promise;
3696
- }
3697
- }
3698
- function useSearchableProducts(apps) {
3699
- // Keyed by name because providers hand over a fresh `apps` array on renders
3700
- // where nothing changed.
3701
- const accessibleKey = inSwitcherOrder(apps).map(({
3702
- slug
3703
- }) => slug).join(",");
3704
- const [settled, setSettled] = React__default.useState();
3705
- React__default.useEffect(() => {
3706
- let current = true;
3707
- const accessible = accessibleKey.split(",").flatMap(slug => SEARCHABLE_PRODUCTS.filter(product => product.slug === slug));
3708
- Promise.all(accessible.map(({
3709
- slug
3710
- }) => canSearch(slug))).then(allowed => {
3711
- if (current) {
3712
- setSettled({
3713
- accessibleKey,
3714
- products: accessible.filter((_, index) => allowed[index])
3715
- });
3716
- }
3717
- });
3718
- return () => {
3719
- current = false;
3450
+ const SEARCH_DEBOUNCE_MS = 300;
3451
+ function useProductSearch(product, term) {
3452
+ const query = term.trim();
3453
+ const debouncedQuery = useDebouncedValue(query, SEARCH_DEBOUNCE_MS);
3454
+ const request = useCachedRequest(debouncedQuery ? productSearchUrl(product, debouncedQuery) : null);
3455
+ const response = "data" in request ? request.data : undefined;
3456
+ const settledProduct = useSettledProduct(product, request.status);
3457
+ // While a request loads, the previous response stays on screen, but only
3458
+ // if it was for this product: People results must never show under the
3459
+ // Services tab.
3460
+ const visibleResponse = request.status === "done" || settledProduct === product ? response : undefined;
3461
+ const groups = React__default.useMemo(() => visibleResponse && groupByType(visibleResponse), [visibleResponse]);
3462
+ if (!query) {
3463
+ return {
3464
+ status: "idle"
3465
+ };
3466
+ } else if (query !== debouncedQuery) {
3467
+ return {
3468
+ groups,
3469
+ status: "loading"
3720
3470
  };
3721
- }, [accessibleKey]);
3722
- if ((settled === null || settled === void 0 ? void 0 : settled.accessibleKey) === accessibleKey) {
3471
+ } else if (request.status === "done") {
3723
3472
  return {
3724
- products: settled.products,
3473
+ groups: groups !== null && groups !== void 0 ? groups : [],
3725
3474
  status: "done"
3726
3475
  };
3476
+ } else if (request.status === "error") {
3477
+ return {
3478
+ status: "error"
3479
+ };
3727
3480
  } else {
3728
3481
  return {
3482
+ groups,
3729
3483
  status: "loading"
3730
3484
  };
3731
3485
  }
3732
3486
  }
3733
- function inSwitcherOrder(apps) {
3734
- return groupApps(apps).restProducts.flatMap(app => SEARCHABLE_PRODUCTS.filter(product => product.appName === app.name));
3735
- }
3736
-
3737
- const isSearchShortcut = event => event.key.toLowerCase() === "k" && !event.altKey && !event.shiftKey && hasCommandKey(event);
3738
- // Products opened their own search with `/` before this one replaced it.
3739
- const isLegacySearchShortcut = event => event.key === "/" && !event.altKey && !event.ctrlKey && !event.metaKey && !isTypingIn(event.target);
3740
- const isTypingKey = event => event.key.length === 1 && event.key !== " " && !event.altKey && !event.ctrlKey && !event.metaKey;
3741
- const isTypingIn = target => target instanceof Element && target.closest('input, select, textarea, [contenteditable]:not([contenteditable="false"])') !== null;
3742
- function ProductSearch({
3743
- apps
3744
- }) {
3745
- const searchable = useSearchableProducts(apps);
3746
- if (searchable.status === "done" && searchable.products.length > 0) {
3747
- return /*#__PURE__*/React__default.createElement(SearchButtonAndDialog, {
3748
- products: searchable.products
3749
- });
3750
- } else {
3751
- return null;
3752
- }
3753
- }
3754
- function SearchButtonAndDialog({
3755
- products
3756
- }) {
3757
- const [open, setOpen] = React__default.useState(false);
3758
- React__default.useEffect(() => {
3759
- // Rich text editors like Trix bind ⌘K to "insert link"; capturing at the
3760
- // window and stopping propagation keeps them from ever seeing it.
3761
- const handleKeyDown = event => {
3762
- if (isSearchShortcut(event)) {
3763
- event.preventDefault();
3764
- event.stopPropagation();
3765
- setOpen(wasOpen => !wasOpen);
3766
- } else if (isLegacySearchShortcut(event)) {
3767
- event.preventDefault();
3768
- event.stopPropagation();
3769
- setOpen(true);
3770
- }
3771
- };
3772
- window.addEventListener("keydown", handleKeyDown, {
3773
- capture: true
3774
- });
3775
- return () => window.removeEventListener("keydown", handleKeyDown, {
3776
- capture: true
3777
- });
3778
- }, []);
3779
- return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("button", {
3780
- "aria-keyshortcuts": `${isMac() ? "Meta+K" : "Control+K"} /`,
3781
- "aria-label": "Search",
3782
- onClick: () => setOpen(true),
3783
- type: "button"
3784
- }, /*#__PURE__*/React__default.createElement(Spyglass, {
3785
- fill: "currentColor"
3786
- })), /*#__PURE__*/React__default.createElement(Modal, {
3787
- "aria-label": "Search",
3788
- dismissible: true,
3789
- onClose: () => setOpen(false),
3790
- open: open
3791
- }, /*#__PURE__*/React__default.createElement(SearchPanel, {
3792
- products: products
3793
- })));
3794
- }
3795
- function SearchPanel({
3796
- products
3797
- }) {
3798
- var _a, _b;
3799
- const listboxId = useInstanceId("product-search-results");
3800
- const [term, setTerm] = React__default.useState("");
3801
- const [selectedSlug, setSelectedSlug] = React__default.useState(() => initialProduct(products).slug);
3802
- const product = (_a = products.find(({
3803
- slug
3804
- }) => slug === selectedSlug)) !== null && _a !== void 0 ? _a : products[0];
3805
- const hasTerm = term.trim() !== "";
3806
- const search = useProductSearch(product.slug, term);
3807
- const groups = "groups" in search ? search.groups : undefined;
3808
- const results = React__default.useMemo(() => {
3809
- var _a;
3810
- return (_a = groups === null || groups === void 0 ? void 0 : groups.flatMap(group => group.results)) !== null && _a !== void 0 ? _a : [];
3811
- }, [groups]);
3812
- const navigation = useResultNavigation(results);
3813
- const {
3814
- highlightedIndex
3815
- } = navigation;
3816
- const inputRef = React__default.useRef(null);
3817
- const [announceHighlight, setAnnounceHighlight] = React__default.useState(false);
3818
- // The tabs keep focus while arrowing through results, but only the focused
3819
- // input can point screen readers at the highlight, so from the tabs it's
3820
- // announced instead. Typing goes back to the input, keystroke and all.
3821
- const handleTabsKeyDown = event => {
3822
- var _a;
3823
- if (isTypingKey(event)) {
3824
- (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
3825
- } else if (verticalStep(event.key)) {
3826
- setAnnounceHighlight(true);
3827
- navigation.handleKeyDown(event);
3828
- } else {
3829
- navigation.handleKeyDown(event);
3830
- }
3831
- };
3487
+ // The product the most recent finished request was for.
3488
+ function useSettledProduct(product, status) {
3489
+ const [settledProduct, setSettledProduct] = React__default.useState();
3832
3490
  React__default.useEffect(() => {
3833
- if (hasTerm) saveLastProduct(product.slug);
3834
- }, [hasTerm, product.slug]);
3835
- return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("input", {
3836
- "aria-activedescendant": highlightedIndex === null ? undefined : resultId(listboxId, highlightedIndex),
3837
- "aria-autocomplete": "list",
3838
- "aria-controls": results.length > 0 ? listboxId : undefined,
3839
- "aria-expanded": results.length > 0,
3840
- "aria-label": "Search",
3841
- autoFocus: true,
3842
- onChange: event => setTerm(event.target.value),
3843
- onFocus: () => setAnnounceHighlight(false),
3844
- onKeyDown: navigation.handleKeyDown,
3845
- placeholder: "Search",
3846
- ref: inputRef,
3847
- role: "combobox",
3848
- type: "search",
3849
- value: term
3850
- }), /*#__PURE__*/React__default.createElement(ProductTabs, {
3851
- onKeyDown: handleTabsKeyDown,
3852
- onSelect: setSelectedSlug,
3853
- products: products,
3854
- selected: product
3855
- }, /*#__PURE__*/React__default.createElement(SearchResults, {
3856
- highlightedIndex: highlightedIndex,
3857
- id: listboxId,
3858
- label: `${product.appName} results`,
3859
- onHighlight: navigation.highlightIndex,
3860
- search: search,
3861
- term: term
3862
- })), /*#__PURE__*/React__default.createElement("div", {
3863
- "aria-live": "polite",
3864
- className: styles$c.visuallyHidden
3865
- }, announceHighlight ? (_b = navigation.highlighted) === null || _b === void 0 ? void 0 : _b.attributes.title : null), /*#__PURE__*/React__default.createElement(KeyboardHints, {
3866
- copied: navigation.copied
3867
- }));
3868
- }
3869
- function KeyboardHints({
3870
- copied
3871
- }) {
3872
- const command = commandKeyLabel();
3873
- return /*#__PURE__*/React__default.createElement("p", null, "\u2191\u2193 Select \u00B7 \u21B5 Open \u00B7 ", command, "\u21B5 Open in new tab \u00B7 ", command, "C", " ", /*#__PURE__*/React__default.createElement("span", {
3874
- role: "status"
3875
- }, copied ? "Copied!" : "Copy link"), " \u00B7 ", command, "K Open search");
3876
- }
3877
- function initialProduct(products) {
3878
- var _a, _b, _c;
3879
- const lastProduct = readLastProduct();
3880
- const bySlug = slug => products.find(product => product.slug === slug);
3881
- 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];
3491
+ if (status === "done") setSettledProduct(product);
3492
+ }, [product, status]);
3493
+ return settledProduct;
3882
3494
  }
3883
3495
 
3884
3496
  function parseValue(element) {
@@ -3947,10 +3559,14 @@ function useRecentlyVisitableMetadata() {
3947
3559
  return useSyncExternalStore(recentlyVisitableMetadataStore.subscribe, () => recentlyVisitableMetadataStore.getState().metadata);
3948
3560
  }
3949
3561
 
3950
- const MAX_ENTRIES = 20;
3951
- const FORMAT_VERSION = 1;
3562
+ // Entries match the ones home-mobile stores, so one list can sync across web
3563
+ // and mobile.
3564
+ const ENTRY_VERSION = 3;
3565
+ const MAX_ENTRIES = 50;
3952
3566
  const MAX_TEXT_LENGTH = 200;
3953
3567
  const PRODUCTS = ["accounts", "calendar", "check-ins", "giving", "groups", "home", "login", "people", "publishing", "registrations", "services"];
3568
+ // Login has no pages worth returning to.
3569
+ const APPS = PRODUCTS.filter(product => product !== "login");
3954
3570
  function isId(value) {
3955
3571
  return value !== "" && Array.from(value).every(character => character >= "0" && character <= "9");
3956
3572
  }
@@ -3959,169 +3575,1116 @@ function isPcrl(value) {
3959
3575
  const segments = value.split("/");
3960
3576
  return segments.length === 4 && PRODUCTS.some(product => product === segments[0]) && isId(segments[3]);
3961
3577
  }
3962
- function isNonBlankString(value) {
3963
- return typeof value === "string" && value.trim() !== "";
3578
+ function isApp(value) {
3579
+ return APPS.some(app => app === value);
3580
+ }
3581
+ // Links are built by resolving the path against its app's origin, so a stored
3582
+ // path must not be able to resolve anywhere else (`//evil.com`, `/\evil.com`).
3583
+ function isPath(value) {
3584
+ const origin = "https://app.invalid";
3585
+ try {
3586
+ return typeof value === "string" && value.startsWith("/") && new URL(value, origin).origin === origin;
3587
+ } catch {
3588
+ return false;
3589
+ }
3590
+ }
3591
+ function isNonBlankString(value) {
3592
+ return typeof value === "string" && value.trim() !== "";
3593
+ }
3594
+ function isOptionalString(value) {
3595
+ return value === undefined || typeof value === "string";
3596
+ }
3597
+ function isRecord(value) {
3598
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3599
+ }
3600
+ function isPageMetadata(value) {
3601
+ return value === undefined || isRecord(value) && isOptionalString(value.details) && isOptionalString(value.type);
3602
+ }
3603
+ // The query string is part of the link but not of the page's identity, so
3604
+ // revisiting with other filters moves the existing entry instead of adding one.
3605
+ function entryKey(entry) {
3606
+ if (entry.type === "resource") {
3607
+ return `resource:${entry.pcrl}`;
3608
+ } else {
3609
+ return `page:${entry.app}:${entry.path.split("?")[0]}`;
3610
+ }
3611
+ }
3612
+ function isEntry(value) {
3613
+ if (!isRecord(value) || value.version !== ENTRY_VERSION || typeof value.visitedAt !== "string") {
3614
+ return false;
3615
+ } else if (value.type === "resource") {
3616
+ return typeof value.pcrl === "string" && isPcrl(value.pcrl);
3617
+ } else if (value.type === "page") {
3618
+ return isApp(value.app) && isPath(value.path) && isNonBlankString(value.title) && isPageMetadata(value.metadata);
3619
+ } else {
3620
+ return false;
3621
+ }
3622
+ }
3623
+ // Cuts on code points so a surrogate pair is never split.
3624
+ function clampText(value) {
3625
+ return Array.from(value).slice(0, MAX_TEXT_LENGTH).join("");
3626
+ }
3627
+ function clampMetadata({
3628
+ details,
3629
+ type
3630
+ }) {
3631
+ return {
3632
+ ...(details && {
3633
+ details: clampText(details)
3634
+ }),
3635
+ ...(type && {
3636
+ type: clampText(type)
3637
+ })
3638
+ };
3639
+ }
3640
+ function clampVisit(visit) {
3641
+ if (visit.type === "resource") {
3642
+ return visit;
3643
+ } else {
3644
+ return {
3645
+ ...visit,
3646
+ ...(visit.metadata && {
3647
+ metadata: clampMetadata(visit.metadata)
3648
+ }),
3649
+ title: clampText(visit.title.trim())
3650
+ };
3651
+ }
3652
+ }
3653
+ function toEntry(visit, visitedAt) {
3654
+ return {
3655
+ ...clampVisit(visit),
3656
+ version: ENTRY_VERSION,
3657
+ visitedAt: visitedAt.toISOString()
3658
+ };
3659
+ }
3660
+ // Recents are disposable: anything unexpected reads as an empty list (or, for
3661
+ // a single bad entry, a list without it) rather than throwing.
3662
+ function parseRecentlyVisited(value) {
3663
+ let parsed = null;
3664
+ try {
3665
+ parsed = JSON.parse(value !== null && value !== void 0 ? value : "null");
3666
+ } catch {
3667
+ parsed = null;
3668
+ }
3669
+ if (Array.isArray(parsed)) {
3670
+ return parsed.filter(isEntry);
3671
+ } else {
3672
+ return [];
3673
+ }
3674
+ }
3675
+ function addVisit(entries, entry) {
3676
+ const key = entryKey(entry);
3677
+ return [entry, ...entries.filter(existing => entryKey(existing) !== key)].slice(0, MAX_ENTRIES);
3678
+ }
3679
+
3680
+ // The app whose URL, in this environment, has the current page's host.
3681
+ // Reversing pcoUrl handles every environment @planningcenter/url knows about,
3682
+ // and guarantees the stored app rebuilds a working link.
3683
+ function currentApp() {
3684
+ var _a;
3685
+ return (_a = APPS.find(app => new URL(pcoUrl$2(app)).hostname === window.location.hostname)) !== null && _a !== void 0 ? _a : null;
3686
+ }
3687
+ function isFilled(value) {
3688
+ return typeof value === "string" && value !== "";
3689
+ }
3690
+ // The tag's metadata is product-supplied JSON; only its string `type` and
3691
+ // `details` carry over, and an entry without either has no metadata at all.
3692
+ function metadataFromPage(metadata) {
3693
+ var _a;
3694
+ try {
3695
+ const {
3696
+ details,
3697
+ type
3698
+ } = (_a = JSON.parse(metadata !== null && metadata !== void 0 ? metadata : "{}")) !== null && _a !== void 0 ? _a : {};
3699
+ if (isFilled(details) || isFilled(type)) {
3700
+ return {
3701
+ ...(isFilled(details) && {
3702
+ details
3703
+ }),
3704
+ ...(isFilled(type) && {
3705
+ type
3706
+ })
3707
+ };
3708
+ } else {
3709
+ return undefined;
3710
+ }
3711
+ } catch {
3712
+ return undefined;
3713
+ }
3714
+ }
3715
+ // Staff impersonating someone browse under `/O<org id>/P<person id>/…`.
3716
+ function withoutImpersonation(segments) {
3717
+ const [org = "", person = ""] = segments;
3718
+ if (org.startsWith("O") && isId(org.slice(1)) && person.startsWith("P") && isId(person.slice(1))) {
3719
+ return segments.slice(2);
3720
+ } else {
3721
+ return segments;
3722
+ }
3723
+ }
3724
+ // Normalized the way home-mobile does it, so a page visited on web and on
3725
+ // mobile is one entry once the list syncs.
3726
+ function pathFromLocation({
3727
+ pathname,
3728
+ search
3729
+ }) {
3730
+ const segments = withoutImpersonation(pathname.split("/").filter(Boolean));
3731
+ if (segments.length === 0) {
3732
+ return "/";
3733
+ } else {
3734
+ return `/${segments.join("/")}${search.length > 1 ? search : ""}`;
3735
+ }
3736
+ }
3737
+ // pco:resource is the more specific signal, so it wins when both tags are
3738
+ // present. A resource that isn't a PCRL is ignored rather than recorded.
3739
+ function visitFromPage({
3740
+ metadata,
3741
+ resource,
3742
+ title
3743
+ }) {
3744
+ const app = currentApp();
3745
+ if (resource && isPcrl(resource)) {
3746
+ return {
3747
+ pcrl: resource,
3748
+ type: "resource"
3749
+ };
3750
+ } else if (title && title.trim() !== "" && app) {
3751
+ const pageMetadata = metadataFromPage(metadata);
3752
+ return {
3753
+ app,
3754
+ ...(pageMetadata && {
3755
+ metadata: pageMetadata
3756
+ }),
3757
+ path: pathFromLocation(window.location),
3758
+ title,
3759
+ type: "page"
3760
+ };
3761
+ } else {
3762
+ return null;
3763
+ }
3764
+ }
3765
+ // The entry Recently Visited would record for the current page, or null when
3766
+ // the page isn't eligible. Shared by the recorder and the debug panel so the
3767
+ // panel always shows what actually gets stored.
3768
+ function useCurrentVisit() {
3769
+ const {
3770
+ recently_visitable: title,
3771
+ resource
3772
+ } = usePcoMeta();
3773
+ const metadata = useRecentlyVisitableMetadata();
3774
+ return useMemo(() => visitFromPage({
3775
+ metadata,
3776
+ resource,
3777
+ title
3778
+ }), [metadata, resource, title]);
3779
+ }
3780
+ // Products hand the person's ID to AppsProvider, not to the topbar itself,
3781
+ // and several pass it as a number.
3782
+ function useCurrentPersonId() {
3783
+ const {
3784
+ userId
3785
+ } = useStore();
3786
+ return userId == null ? undefined : String(userId);
3787
+ }
3788
+
3789
+ // Per-origin localStorage is a stopgap: each product keeps its own list until
3790
+ // PCA-409 moves this to storage shared across product subdomains. Only this
3791
+ // file should need to change when it does.
3792
+ function storageKey({
3793
+ env,
3794
+ personId
3795
+ }) {
3796
+ return `topbar.recently_visited.${env}.${personId}`;
3797
+ }
3798
+ // localStorage doesn't notify the tab that wrote it, so readers in this tab
3799
+ // learn about new visits from this counter instead.
3800
+ const changes = createStore({
3801
+ version: 0
3802
+ });
3803
+ function readRecentlyVisited(scope) {
3804
+ try {
3805
+ return parseRecentlyVisited(localStorage.getItem(storageKey(scope)));
3806
+ } catch {
3807
+ return [];
3808
+ }
3809
+ }
3810
+ function recordVisit(visit, scope) {
3811
+ const entry = toEntry(visit, new Date());
3812
+ if (isEntry(entry)) {
3813
+ try {
3814
+ localStorage.setItem(storageKey(scope), JSON.stringify(addVisit(readRecentlyVisited(scope), entry)));
3815
+ changes.setState({
3816
+ version: changes.getState().version + 1
3817
+ });
3818
+ } catch {
3819
+ // Storage can be full or blocked; recents are disposable.
3820
+ }
3821
+ }
3822
+ }
3823
+ function useRecentlyVisited({
3824
+ env,
3825
+ personId
3826
+ }) {
3827
+ const version = useSyncExternalStore(changes.subscribe, () => changes.getState().version);
3828
+ // `version` isn't read, but listing it re-reads storage after each visit.
3829
+ return useMemo(() => personId ? readRecentlyVisited({
3830
+ env,
3831
+ personId
3832
+ }) : [], [env, personId, version]);
3833
+ }
3834
+
3835
+ const pcoUrl = pcoUrl$1;
3836
+
3837
+ // Resolves resource entries with one pcrl_lookups request per product.
3838
+ // pcrl_lookups reports forbidden and deleted resources the same way, so both
3839
+ // become `unresolved`, as does every entry in a batch whose request failed.
3840
+ function useHydratedRecentlyVisited(entries, env) {
3841
+ const batches = React__default.useMemo(() => batchByProduct(entries), [entries]);
3842
+ const urls = React__default.useMemo(() => batches.map(([product, pcrls]) => pcrlLookupsUrl(product, pcrls)), [batches]);
3843
+ const requests = useCachedRequests(urls);
3844
+ const lookups = new Map();
3845
+ batches.forEach(([, pcrls], index) => {
3846
+ const request = requests[index];
3847
+ for (const pcrl of pcrls) lookups.set(pcrl, lookupFor(pcrl, request));
3848
+ });
3849
+ return entries.map(entry => {
3850
+ var _a;
3851
+ if (entry.type === "page") {
3852
+ return {
3853
+ entry,
3854
+ status: "page",
3855
+ url: pageUrl(entry, env)
3856
+ };
3857
+ } else {
3858
+ const lookup = (_a = lookups.get(entry.pcrl)) !== null && _a !== void 0 ? _a : {
3859
+ status: "unresolved"
3860
+ };
3861
+ if (lookup.status === "resolved") {
3862
+ return {
3863
+ ...lookup,
3864
+ entry,
3865
+ url: lookup.result.attributes.url
3866
+ };
3867
+ } else {
3868
+ return {
3869
+ entry,
3870
+ status: lookup.status
3871
+ };
3872
+ }
3873
+ }
3874
+ });
3875
+ }
3876
+ function lookupFor(pcrl, request) {
3877
+ if (request.status === "done") {
3878
+ const result = request.data.data.find(({
3879
+ attributes
3880
+ }) => attributes.pcrl === pcrl);
3881
+ return result ? {
3882
+ result,
3883
+ status: "resolved"
3884
+ } : {
3885
+ status: "unresolved"
3886
+ };
3887
+ } else if (request.status === "error") {
3888
+ return {
3889
+ status: "unresolved"
3890
+ };
3891
+ } else {
3892
+ return {
3893
+ status: "loading"
3894
+ };
3895
+ }
3896
+ }
3897
+ const RECENTLY_VISITED_PAGE_SIZE = 10;
3898
+ // A PCRL can only be looked up through its own product's API, which may not be
3899
+ // the product it was visited in. Each page of entries is batched on its own,
3900
+ // so loading the next page leaves earlier pages' URLs, and so their cache
3901
+ // keys, alone. Sorting keeps them stable as a page reorders.
3902
+ function batchByProduct(entries) {
3903
+ var _a;
3904
+ const batches = [];
3905
+ for (let start = 0; start < entries.length; start += RECENTLY_VISITED_PAGE_SIZE) {
3906
+ const page = new Map();
3907
+ for (const entry of entries.slice(start, start + RECENTLY_VISITED_PAGE_SIZE)) {
3908
+ if (entry.type === "resource") {
3909
+ const [product] = entry.pcrl.split("/");
3910
+ page.set(product, [...((_a = page.get(product)) !== null && _a !== void 0 ? _a : []), entry.pcrl]);
3911
+ }
3912
+ }
3913
+ for (const [product, pcrls] of page) {
3914
+ batches.push([product, [...pcrls].sort()]);
3915
+ }
3916
+ }
3917
+ return batches;
3918
+ }
3919
+ function pcrlLookupsUrl(product, pcrls) {
3920
+ const params = new URLSearchParams({
3921
+ "where[pcrl]": pcrls.join(",")
3922
+ });
3923
+ return `${window.location.origin}/~api/${product}/v2/pcrl_lookups?${params}`;
3924
+ }
3925
+ // Built from the app rather than the current origin, so it stays right once
3926
+ // PCA-409 shares one list across product subdomains.
3927
+ function pageUrl(entry, env) {
3928
+ return new URL(entry.path, pcoUrl(env)(entry.app)).href;
3929
+ }
3930
+
3931
+ function useRecentVisits(env) {
3932
+ const personId = useCurrentPersonId();
3933
+ const entries = useRecentlyVisited({
3934
+ env,
3935
+ personId
3936
+ });
3937
+ const [count, setCount] = React__default.useState(RECENTLY_VISITED_PAGE_SIZE);
3938
+ const shown = React__default.useMemo(() => entries.slice(0, count), [entries, count]);
3939
+ const hydrated = useHydratedRecentlyVisited(shown, env);
3940
+ // Resources show up once they're found. Ones that can't be found anymore,
3941
+ // or that the person can no longer see, are left out.
3942
+ const visits = hydrated.filter(isShown);
3943
+ const loading = hydrated.some(({
3944
+ status
3945
+ }) => status === "loading");
3946
+ const showMore = React__default.useCallback(() => setCount(current => current + RECENTLY_VISITED_PAGE_SIZE), []);
3947
+ return {
3948
+ count,
3949
+ hasMore: entries.length > count,
3950
+ loading,
3951
+ showMore,
3952
+ visits
3953
+ };
3954
+ }
3955
+ function isShown(visit) {
3956
+ return visit.status === "page" || visit.status === "resolved";
3957
+ }
3958
+
3959
+ function open(url, {
3960
+ newTab
3961
+ }) {
3962
+ if (newTab) {
3963
+ window.open(url, "_blank", "noopener");
3964
+ } else {
3965
+ window.location.assign(url);
3966
+ }
3967
+ }
3968
+ // With text selected, ⌘C should copy that text as usual, not the link.
3969
+ function hasSelectedText(element) {
3970
+ var _a, _b;
3971
+ const selectedInField = element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
3972
+ return selectedInField || ((_b = (_a = window.getSelection()) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : "") !== "";
3973
+ }
3974
+
3975
+ const COPIED_FEEDBACK_MS = 1500;
3976
+ // `copiedUrl` stays set briefly after a copy so the UI can confirm it.
3977
+ function useCopyLink() {
3978
+ const [copiedUrl, setCopiedUrl] = React__default.useState(null);
3979
+ React__default.useEffect(() => {
3980
+ if (copiedUrl) {
3981
+ const timeout = setTimeout(() => setCopiedUrl(null), COPIED_FEEDBACK_MS);
3982
+ return () => clearTimeout(timeout);
3983
+ } else {
3984
+ return undefined;
3985
+ }
3986
+ }, [copiedUrl]);
3987
+ const copy = React__default.useCallback(url => {
3988
+ navigator.clipboard.writeText(url).then(() => setCopiedUrl(url));
3989
+ }, []);
3990
+ return {
3991
+ copiedUrl,
3992
+ copy
3993
+ };
3994
+ }
3995
+
3996
+ // Remembering which list the index belongs to puts the highlight back on the
3997
+ // first item whenever a new list replaces it, without an effect to reset it.
3998
+ // A list that grows or fills in under the same `list` keeps its highlight.
3999
+ function useResultNavigation(items, list) {
4000
+ const [highlight, setHighlight] = React__default.useState({
4001
+ index: 0,
4002
+ list
4003
+ });
4004
+ const highlightedIndex = highlight.list === list ? highlight.index : 0;
4005
+ const highlighted = items[highlightedIndex];
4006
+ const {
4007
+ copiedUrl,
4008
+ copy
4009
+ } = useCopyLink();
4010
+ const highlightIndex = React__default.useCallback(index => setHighlight(current => current.list === list && current.index === index ? current : {
4011
+ index,
4012
+ list
4013
+ }), [list]);
4014
+ const handleKeyDown = event => {
4015
+ const step = verticalStep(event.key);
4016
+ if (step && items.length > 0) {
4017
+ event.preventDefault();
4018
+ highlightIndex((highlightedIndex + step + items.length) % items.length);
4019
+ } else if (event.key === "Enter" && highlighted && !event.nativeEvent.isComposing) {
4020
+ event.preventDefault();
4021
+ open(highlighted.url, {
4022
+ newTab: hasCommandKey(event)
4023
+ });
4024
+ } else if (isCopyShortcut(event) && highlighted && !hasSelectedText(event.currentTarget)) {
4025
+ event.preventDefault();
4026
+ copy(highlighted.url);
4027
+ }
4028
+ };
4029
+ return {
4030
+ copied: copiedUrl !== null && copiedUrl === (highlighted === null || highlighted === void 0 ? void 0 : highlighted.url),
4031
+ handleKeyDown,
4032
+ highlightIndex,
4033
+ highlighted,
4034
+ highlightedIndex: highlighted ? highlightedIndex : null
4035
+ };
4036
+ }
4037
+
4038
+ function groupSections(groups = []) {
4039
+ return groups.map(({
4040
+ results,
4041
+ type
4042
+ }) => ({
4043
+ heading: type,
4044
+ items: results.map(resultItem),
4045
+ key: type
4046
+ }));
4047
+ }
4048
+ function resultItem({
4049
+ attributes,
4050
+ id
4051
+ }) {
4052
+ return {
4053
+ key: id,
4054
+ subtitle: summarize(attributes.metadata),
4055
+ title: attributes.title,
4056
+ url: attributes.url
4057
+ };
4058
+ }
4059
+ function visitItem(visit) {
4060
+ return {
4061
+ key: entryKey(visit.entry),
4062
+ ...describeVisit(visit)
4063
+ };
4064
+ }
4065
+ function summarize(metadata) {
4066
+ return Object.values(metadata !== null && metadata !== void 0 ? metadata : {}).filter(value => typeof value === "string" && value !== "").join(" · ");
4067
+ }
4068
+ // Unlike search results, recents come from every product, so each one names
4069
+ // its product and type.
4070
+ function describeVisit(visit) {
4071
+ var _a, _b;
4072
+ const subtitle = (...parts) => parts.filter(Boolean).join(" · ");
4073
+ if (visit.status === "page") {
4074
+ const {
4075
+ app,
4076
+ metadata,
4077
+ title
4078
+ } = visit.entry;
4079
+ return {
4080
+ subtitle: subtitle(productName(app), (_a = metadata === null || metadata === void 0 ? void 0 : metadata.type) !== null && _a !== void 0 ? _a : "", (_b = metadata === null || metadata === void 0 ? void 0 : metadata.details) !== null && _b !== void 0 ? _b : ""),
4081
+ title,
4082
+ url: visit.url
4083
+ };
4084
+ } else {
4085
+ const {
4086
+ metadata,
4087
+ product,
4088
+ title,
4089
+ type
4090
+ } = visit.result.attributes;
4091
+ return {
4092
+ subtitle: subtitle(productName(product), type, summarize(metadata)),
4093
+ title,
4094
+ url: visit.url
4095
+ };
4096
+ }
4097
+ }
4098
+ // check-ins → Check-Ins
4099
+ function productName(slug) {
4100
+ return slug.split("-").map(capitalize).join("-");
4101
+ }
4102
+ function capitalize(word) {
4103
+ return word.charAt(0).toUpperCase() + word.slice(1);
4104
+ }
4105
+
4106
+ const STORAGE_KEY = "topbar.product_search.last_product";
4107
+ function readLastProduct() {
4108
+ try {
4109
+ return localStorage.getItem(STORAGE_KEY);
4110
+ } catch {
4111
+ return null;
4112
+ }
4113
+ }
4114
+ function saveLastProduct(slug) {
4115
+ try {
4116
+ localStorage.setItem(STORAGE_KEY, slug);
4117
+ } catch {
4118
+ // Storage can be full or blocked; search still works without it.
4119
+ }
4120
+ }
4121
+ function initialProduct(products) {
4122
+ var _a, _b, _c;
4123
+ const lastProduct = readLastProduct();
4124
+ const bySlug = slug => products.find(product => product.slug === slug);
4125
+ return (_c = (_b = (_a = bySlug(lastProduct)) !== null && _a !== void 0 ? _a : bySlug("people")) !== null && _b !== void 0 ? _b : bySlug("services")) !== null && _c !== void 0 ? _c : products[0];
4126
+ }
4127
+
4128
+ function KeyboardHints({
4129
+ copied
4130
+ }) {
4131
+ const command = commandKeyLabel();
4132
+ return /*#__PURE__*/React__default.createElement("p", null, "\u2191\u2193 Select \u00B7 \u21B5 Open \u00B7 ", command, "\u21B5 Open in new tab \u00B7 ", command, "C", " ", /*#__PURE__*/React__default.createElement("span", {
4133
+ role: "status"
4134
+ }, copied ? "Copied!" : "Copy link"), " \u00B7 ", command, "K Open search");
4135
+ }
4136
+
4137
+ var styles$d = {"tab":"tab-dz8ki"};
4138
+
4139
+ function ProductTabs({
4140
+ children,
4141
+ onKeyDown,
4142
+ onSelect,
4143
+ products,
4144
+ selected
4145
+ }) {
4146
+ const id = useInstanceId("product-tabs");
4147
+ const tabId = slug => `${id}-tab-${slug}`;
4148
+ const tabRefs = React__default.useRef(new Map());
4149
+ const handleKeyDown = event => {
4150
+ var _a;
4151
+ const offset = horizontalStep(event.key);
4152
+ if (offset) {
4153
+ event.preventDefault();
4154
+ const index = products.indexOf(selected);
4155
+ const next = products[(index + offset + products.length) % products.length];
4156
+ onSelect(next.slug);
4157
+ (_a = tabRefs.current.get(next.slug)) === null || _a === void 0 ? void 0 : _a.focus();
4158
+ } else {
4159
+ onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
4160
+ }
4161
+ };
4162
+ return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("div", {
4163
+ "aria-label": "Product",
4164
+ onKeyDown: handleKeyDown,
4165
+ role: "tablist"
4166
+ }, products.map(product => (/*#__PURE__*/React__default.createElement("button", {
4167
+ "aria-controls": `${id}-panel`,
4168
+ "aria-selected": product === selected,
4169
+ className: styles$d.tab,
4170
+ id: tabId(product.slug),
4171
+ key: product.slug,
4172
+ onClick: () => onSelect(product.slug),
4173
+ ref: element => {
4174
+ if (element) {
4175
+ tabRefs.current.set(product.slug, element);
4176
+ } else {
4177
+ tabRefs.current.delete(product.slug);
4178
+ }
4179
+ },
4180
+ role: "tab",
4181
+ tabIndex: product === selected ? 0 : -1,
4182
+ type: "button"
4183
+ }, product.appName)))), /*#__PURE__*/React__default.createElement("div", {
4184
+ "aria-labelledby": tabId(selected.slug),
4185
+ id: `${id}-panel`,
4186
+ role: "tabpanel"
4187
+ }, children));
4188
+ }
4189
+
4190
+ function EndOfResultsObserver({
4191
+ onVisible
4192
+ }) {
4193
+ const ref = React__default.useRef(null);
4194
+ React__default.useEffect(() => {
4195
+ const observer = new IntersectionObserver(([entry]) => {
4196
+ if (entry.isIntersecting) onVisible();
4197
+ });
4198
+ if (ref.current) observer.observe(ref.current);
4199
+ return () => observer.disconnect();
4200
+ }, [onVisible]);
4201
+ return /*#__PURE__*/React__default.createElement("div", {
4202
+ ref: ref
4203
+ });
4204
+ }
4205
+
4206
+ const resultId = (listboxId, index) => `${listboxId}-result-${index}`;
4207
+
4208
+ var styles$c = {"result":"result-Thq2C"};
4209
+
4210
+ function SearchResult({
4211
+ highlighted,
4212
+ id,
4213
+ onHighlight,
4214
+ subtitle,
4215
+ title,
4216
+ url
4217
+ }) {
4218
+ const ref = React__default.useRef(null);
4219
+ React__default.useEffect(() => {
4220
+ var _a;
4221
+ if (highlighted) (_a = ref.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({
4222
+ block: "nearest"
4223
+ });
4224
+ }, [highlighted]);
4225
+ // Focus stays in the search input, which points at the highlighted result
4226
+ // with aria-activedescendant, so results are kept out of the tab order.
4227
+ // Mouse move rather than enter: arrowing scrolls results under a resting
4228
+ // pointer, and that shouldn't steal the highlight.
4229
+ return /*#__PURE__*/React__default.createElement("a", {
4230
+ "aria-selected": highlighted,
4231
+ className: styles$c.result,
4232
+ href: url,
4233
+ id: id,
4234
+ onMouseMove: onHighlight,
4235
+ ref: ref,
4236
+ role: "option",
4237
+ tabIndex: -1
4238
+ }, /*#__PURE__*/React__default.createElement("div", null, title), /*#__PURE__*/React__default.createElement("div", null, subtitle));
4239
+ }
4240
+
4241
+ // Sections with a heading render as labelled groups; ones without render
4242
+ // their items straight into the listbox. Indexes run across every section.
4243
+ function ResultListbox({
4244
+ busy,
4245
+ highlightedIndex,
4246
+ id,
4247
+ label,
4248
+ labelledBy,
4249
+ onHighlight,
4250
+ sections
4251
+ }) {
4252
+ let index = 0;
4253
+ const renderItem = item => {
4254
+ const itemIndex = index++;
4255
+ return /*#__PURE__*/React__default.createElement(SearchResult, {
4256
+ highlighted: itemIndex === highlightedIndex,
4257
+ id: resultId(id, itemIndex),
4258
+ key: item.key,
4259
+ onHighlight: () => onHighlight(itemIndex),
4260
+ subtitle: item.subtitle,
4261
+ title: item.title,
4262
+ url: item.url
4263
+ });
4264
+ };
4265
+ return /*#__PURE__*/React__default.createElement("div", {
4266
+ "aria-busy": busy,
4267
+ "aria-label": label,
4268
+ "aria-labelledby": labelledBy,
4269
+ id: id,
4270
+ role: "listbox"
4271
+ }, sections.map((section, sectionIndex) => {
4272
+ if (section.heading === undefined) {
4273
+ return /*#__PURE__*/React__default.createElement(React__default.Fragment, {
4274
+ key: section.key
4275
+ }, section.items.map(renderItem));
4276
+ }
4277
+ const headingId = `${id}-group-${sectionIndex}`;
4278
+ return /*#__PURE__*/React__default.createElement("div", {
4279
+ "aria-labelledby": headingId,
4280
+ key: section.key,
4281
+ role: "group"
4282
+ }, /*#__PURE__*/React__default.createElement("div", {
4283
+ id: headingId
4284
+ }, section.heading), section.items.map(renderItem));
4285
+ }));
4286
+ }
4287
+
4288
+ function RecentlyVisited({
4289
+ count,
4290
+ hasMore,
4291
+ items,
4292
+ loading,
4293
+ showMore,
4294
+ ...navigation
4295
+ }) {
4296
+ const headingId = `${navigation.id}-heading`;
4297
+ if (items.length === 0 && !hasMore && !loading) {
4298
+ return /*#__PURE__*/React__default.createElement("p", null, "Pages you visit will show up here.");
4299
+ } else {
4300
+ return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("div", {
4301
+ id: headingId
4302
+ }, "Recently visited"), /*#__PURE__*/React__default.createElement(ResultListbox, {
4303
+ labelledBy: headingId,
4304
+ sections: [{
4305
+ items,
4306
+ key: "recents"
4307
+ }],
4308
+ ...navigation
4309
+ }), hasMore && /*#__PURE__*/React__default.createElement(EndOfResultsObserver, {
4310
+ key: count,
4311
+ onVisible: showMore
4312
+ }));
4313
+ }
4314
+ }
4315
+
4316
+ // A combobox: focus stays here while aria-activedescendant points screen
4317
+ // readers at the highlighted result in the listbox it controls.
4318
+ function SearchInput({
4319
+ expanded,
4320
+ highlightedIndex,
4321
+ inputRef,
4322
+ listboxId,
4323
+ onChange,
4324
+ onFocus,
4325
+ onKeyDown,
4326
+ value
4327
+ }) {
4328
+ return /*#__PURE__*/React__default.createElement("input", {
4329
+ "aria-activedescendant": highlightedIndex === null ? undefined : resultId(listboxId, highlightedIndex),
4330
+ "aria-autocomplete": "list",
4331
+ "aria-controls": expanded ? listboxId : undefined,
4332
+ "aria-expanded": expanded,
4333
+ "aria-label": "Search",
4334
+ autoFocus: true,
4335
+ onChange: event => onChange(event.target.value),
4336
+ onFocus: onFocus,
4337
+ onKeyDown: onKeyDown,
4338
+ placeholder: "Search",
4339
+ ref: inputRef,
4340
+ role: "combobox",
4341
+ type: "search",
4342
+ value: value
4343
+ });
4344
+ }
4345
+
4346
+ var styles$b = {"visuallyHidden":"visuallyHidden-S9VG5"};
4347
+
4348
+ function SearchResults({
4349
+ label,
4350
+ sections,
4351
+ status,
4352
+ term,
4353
+ ...navigation
4354
+ }) {
4355
+ switch (status) {
4356
+ case "idle":
4357
+ return null;
4358
+ case "loading":
4359
+ if (sections.length > 0) {
4360
+ return /*#__PURE__*/React__default.createElement(ResultListbox, {
4361
+ busy: true,
4362
+ label: label,
4363
+ sections: sections,
4364
+ ...navigation
4365
+ });
4366
+ }
4367
+ return /*#__PURE__*/React__default.createElement("p", {
4368
+ role: "status"
4369
+ }, "Searching\u2026");
4370
+ case "error":
4371
+ return /*#__PURE__*/React__default.createElement("p", {
4372
+ role: "alert"
4373
+ }, "Search isn\u2019t working right now. Try again.");
4374
+ case "done":
4375
+ if (sections.length === 0) {
4376
+ return /*#__PURE__*/React__default.createElement("p", {
4377
+ role: "status"
4378
+ }, "No results for \u201C", term.trim(), "\u201D");
4379
+ }
4380
+ return /*#__PURE__*/React__default.createElement(ResultListbox, {
4381
+ busy: false,
4382
+ label: label,
4383
+ sections: sections,
4384
+ ...navigation
4385
+ });
4386
+ }
4387
+ }
4388
+
4389
+ // Keys the recents list for navigation, so the highlight
4390
+ // stays put as more recents load and as their lookups answer.
4391
+ const RECENTS = "recents";
4392
+ function SearchPanel({
4393
+ env,
4394
+ products
4395
+ }) {
4396
+ var _a, _b;
4397
+ const listboxId = useInstanceId("product-search-results");
4398
+ const [term, setTerm] = React__default.useState("");
4399
+ const [selectedSlug, setSelectedSlug] = React__default.useState(() => initialProduct(products).slug);
4400
+ const product = (_a = products.find(({
4401
+ slug
4402
+ }) => slug === selectedSlug)) !== null && _a !== void 0 ? _a : products[0];
4403
+ const hasTerm = term.trim() !== "";
4404
+ const search = useProductSearch(product.slug, term);
4405
+ const groups = "groups" in search ? search.groups : undefined;
4406
+ const sections = React__default.useMemo(() => groupSections(groups), [groups]);
4407
+ const {
4408
+ visits,
4409
+ ...recents
4410
+ } = useRecentVisits(env);
4411
+ const recentItems = visits.map(visitItem);
4412
+ const items = hasTerm ? sections.flatMap(section => section.items) : recentItems;
4413
+ const navigation = useResultNavigation(items, hasTerm ? sections : RECENTS);
4414
+ const {
4415
+ highlightedIndex
4416
+ } = navigation;
4417
+ const inputRef = React__default.useRef(null);
4418
+ const [announceHighlight, setAnnounceHighlight] = React__default.useState(false);
4419
+ // The tabs keep focus while arrowing through results, but only the focused
4420
+ // input can point screen readers at the highlight, so from the tabs it's
4421
+ // announced instead. Typing goes back to the input, keystroke and all.
4422
+ const handleTabsKeyDown = event => {
4423
+ var _a;
4424
+ if (isTypingKey(event)) {
4425
+ (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
4426
+ } else if (verticalStep(event.key)) {
4427
+ setAnnounceHighlight(true);
4428
+ navigation.handleKeyDown(event);
4429
+ } else {
4430
+ navigation.handleKeyDown(event);
4431
+ }
4432
+ };
4433
+ React__default.useEffect(() => {
4434
+ if (hasTerm) saveLastProduct(product.slug);
4435
+ }, [hasTerm, product.slug]);
4436
+ return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(SearchInput, {
4437
+ expanded: items.length > 0,
4438
+ highlightedIndex: highlightedIndex,
4439
+ inputRef: inputRef,
4440
+ listboxId: listboxId,
4441
+ onChange: setTerm,
4442
+ onFocus: () => setAnnounceHighlight(false),
4443
+ onKeyDown: navigation.handleKeyDown,
4444
+ value: term
4445
+ }), /*#__PURE__*/React__default.createElement(ProductTabs, {
4446
+ onKeyDown: handleTabsKeyDown,
4447
+ onSelect: setSelectedSlug,
4448
+ products: products,
4449
+ selected: product
4450
+ }, hasTerm ? (/*#__PURE__*/React__default.createElement(SearchResults, {
4451
+ highlightedIndex: highlightedIndex,
4452
+ id: listboxId,
4453
+ label: `${product.appName} results`,
4454
+ onHighlight: navigation.highlightIndex,
4455
+ sections: sections,
4456
+ status: search.status,
4457
+ term: term
4458
+ })) : (/*#__PURE__*/React__default.createElement(RecentlyVisited, {
4459
+ highlightedIndex: highlightedIndex,
4460
+ id: listboxId,
4461
+ items: recentItems,
4462
+ onHighlight: navigation.highlightIndex,
4463
+ ...recents
4464
+ }))), /*#__PURE__*/React__default.createElement("div", {
4465
+ "aria-live": "polite",
4466
+ className: styles$b.visuallyHidden
4467
+ }, announceHighlight ? (_b = navigation.highlighted) === null || _b === void 0 ? void 0 : _b.title : null), /*#__PURE__*/React__default.createElement(KeyboardHints, {
4468
+ copied: navigation.copied
4469
+ }));
3964
4470
  }
3965
- function isRecord(value) {
3966
- return typeof value === "object" && value !== null && !Array.isArray(value);
4471
+
4472
+ // Products open modals from many libraries, not all of which mark themselves
4473
+ // with roles, so a modal is recognized by what it does: cover the page.
4474
+ function isModalOpen() {
4475
+ const {
4476
+ clientHeight,
4477
+ clientWidth
4478
+ } = document.documentElement;
4479
+ const element = document.elementFromPoint(clientWidth / 2, clientHeight / 2);
4480
+ return element !== null && (isInNativeDialog(element) || hasBackdrop(element, clientWidth, clientHeight));
3967
4481
  }
3968
- function entryKey(entry) {
3969
- if (entry.type === "resource") {
3970
- return `resource:${entry.pcrl}`;
3971
- } else {
3972
- return `page:${entry.product}:${entry.path}`;
3973
- }
4482
+ // A native modal's backdrop is drawn by the browser rather than being an
4483
+ // element; hit-testing it lands on the dialog itself.
4484
+ function isInNativeDialog(element) {
4485
+ return element.closest("dialog[open]") !== null;
3974
4486
  }
3975
- function isEntry(value) {
3976
- if (!isRecord(value)) {
3977
- return false;
3978
- } else if (value.type === "resource") {
3979
- return typeof value.pcrl === "string" && isPcrl(value.pcrl);
3980
- } else if (value.type === "page") {
3981
- return isNonBlankString(value.product) && isNonBlankString(value.title) && isNonBlankString(value.path) && typeof value.details === "string";
3982
- } else {
3983
- return false;
4487
+ function hasBackdrop(element, width, height) {
4488
+ let current = element;
4489
+ while (current && !isBackdrop(current, width, height)) {
4490
+ current = current.parentElement;
3984
4491
  }
4492
+ return current !== null;
3985
4493
  }
3986
- // Cuts on code points so a surrogate pair is never split.
3987
- function clampText(value) {
3988
- return Array.from(value).slice(0, MAX_TEXT_LENGTH).join("");
4494
+ // Some backdrops start below the topbar rather than at the top of the screen,
4495
+ // so the layer only has to span the width and reach from the center to the
4496
+ // bottom. Its own box has to reach the center: a bar along the bottom can
4497
+ // hold a panel that pops up over the center, like Services' media player.
4498
+ // react-aria leaves a closed modal's backdrop up while it fades out, search's
4499
+ // own included, so one that's on its way out doesn't count.
4500
+ function isBackdrop(element, width, height) {
4501
+ const rect = element.getBoundingClientRect();
4502
+ return !element.matches("[data-exiting]") && getComputedStyle(element).position === "fixed" && rect.width >= width - SCROLLBAR_SLACK && rect.top <= height / 2 && rect.bottom >= height - ROUNDING_SLACK;
3989
4503
  }
3990
- function clampEntry(entry) {
3991
- if (entry.type === "resource") {
3992
- return entry;
3993
- } else {
3994
- return {
3995
- ...entry,
3996
- details: clampText(entry.details),
3997
- title: clampText(entry.title)
4504
+ // react-aria keeps a classic scrollbar's gutter reserved while its modals lock
4505
+ // scrolling, so their backdrops stop short of the right edge by its width.
4506
+ const SCROLLBAR_SLACK = 24;
4507
+ // Rects are fractional at non-100% zoom, but clientWidth and clientHeight are
4508
+ // rounded to whole pixels.
4509
+ const ROUNDING_SLACK = 1;
4510
+
4511
+ function useSearchShortcut(enabled, open, setOpen) {
4512
+ React__default.useEffect(() => {
4513
+ if (!enabled) return undefined;
4514
+ // Rich text editors like Trix bind ⌘K to "insert link"; capturing at the
4515
+ // window and stopping propagation keeps them from ever seeing it. Under a
4516
+ // product's modal, search stays shut rather than stacking a second modal,
4517
+ // and the keystroke goes on to the product.
4518
+ const handleKeyDown = event => {
4519
+ if (open && isSearchShortcut(event)) {
4520
+ event.preventDefault();
4521
+ event.stopPropagation();
4522
+ setOpen(false);
4523
+ } else if (!open && (isSearchShortcut(event) || isLegacySearchShortcut(event)) && !isModalOpen()) {
4524
+ event.preventDefault();
4525
+ event.stopPropagation();
4526
+ setOpen(true);
4527
+ }
3998
4528
  };
3999
- }
4000
- }
4001
- // Recents are disposable: anything unexpected reads as an empty list (or, for
4002
- // a single bad entry, a list without it) rather than throwing.
4003
- function parseRecentlyVisited(value, personId) {
4004
- let parsed = null;
4005
- try {
4006
- parsed = JSON.parse(value !== null && value !== void 0 ? value : "null");
4007
- } catch {
4008
- parsed = null;
4009
- }
4010
- if (isRecord(parsed) && parsed.v === FORMAT_VERSION && parsed.person === personId && Array.isArray(parsed.entries)) {
4011
- return parsed.entries.filter(isEntry);
4012
- } else {
4013
- return [];
4014
- }
4015
- }
4016
- function addVisit(entries, visit) {
4017
- const key = entryKey(visit);
4018
- return [clampEntry(visit), ...entries.filter(entry => entryKey(entry) !== key)].slice(0, MAX_ENTRIES);
4529
+ window.addEventListener("keydown", handleKeyDown, {
4530
+ capture: true
4531
+ });
4532
+ return () => window.removeEventListener("keydown", handleKeyDown, {
4533
+ capture: true
4534
+ });
4535
+ }, [enabled, open, setOpen]);
4019
4536
  }
4020
- function serializeRecentlyVisited(entries, personId) {
4021
- return JSON.stringify({
4022
- entries,
4023
- person: personId,
4024
- v: FORMAT_VERSION
4537
+
4538
+ const SEARCHABLE_PRODUCTS = [{
4539
+ appName: "Calendar",
4540
+ slug: "calendar"
4541
+ }, {
4542
+ appName: "Check-Ins",
4543
+ slug: "check-ins"
4544
+ }, {
4545
+ appName: "Giving",
4546
+ slug: "giving"
4547
+ }, {
4548
+ appName: "Groups",
4549
+ slug: "groups"
4550
+ }, {
4551
+ appName: "People",
4552
+ slug: "people"
4553
+ }, {
4554
+ appName: "Publishing",
4555
+ slug: "publishing"
4556
+ }, {
4557
+ appName: "Registrations",
4558
+ slug: "registrations"
4559
+ }, {
4560
+ appName: "Services",
4561
+ slug: "services"
4562
+ }];
4563
+
4564
+ const PERMISSION_CHECKS = {
4565
+ "check-ins": () => fetchMe("check-ins", "permission").then(({
4566
+ permission
4567
+ }) => permission !== null),
4568
+ giving: personId => {
4569
+ if (personId === undefined) {
4570
+ return Promise.reject(new Error("No person to check Giving access for"));
4571
+ } else {
4572
+ return fetchAttributes(`giving/v2/person_permissions/${personId}`, {
4573
+ "fields[PersonPermissions]": "can_search"
4574
+ }).then(({
4575
+ can_search
4576
+ }) => can_search);
4577
+ }
4578
+ },
4579
+ people: () => fetchMe("people", "people_permissions,site_administrator").then(({
4580
+ people_permissions,
4581
+ site_administrator
4582
+ }) => site_administrator || people_permissions !== null),
4583
+ registrations: () => fetchMe("registrations", "can_search").then(({
4584
+ can_search
4585
+ }) => can_search),
4586
+ services: () => fetchMe("services", "can_search").then(({
4587
+ can_search
4588
+ }) => can_search)
4589
+ };
4590
+ function fetchMe(product, fields) {
4591
+ return fetchAttributes(`${product}/v2/me`, {
4592
+ "fields[Person]": fields
4025
4593
  });
4026
4594
  }
4027
-
4028
- // The product whose URL, in this environment, has the current page's host.
4029
- // Reversing pcoUrl handles every environment @planningcenter/url knows about,
4030
- // and guarantees the stored product rebuilds a working link.
4031
- function currentProduct() {
4032
- var _a;
4033
- return (_a = PRODUCTS.find(product => new URL(pcoUrl$2(product)).hostname === window.location.hostname)) !== null && _a !== void 0 ? _a : null;
4595
+ function fetchAttributes(path, params) {
4596
+ const url = `${window.location.origin}/~api/${path}?${new URLSearchParams(params)}`;
4597
+ return apiRequest(defaultFetch, url, {
4598
+ headers: apiVersionHeaders(url)
4599
+ }).then(({
4600
+ json,
4601
+ ok
4602
+ }) => {
4603
+ if (!ok || json === null) throw new Error("Permission check failed");
4604
+ return json.data.attributes;
4605
+ });
4034
4606
  }
4035
- function detailsFromMetadata(metadata) {
4036
- var _a;
4037
- try {
4038
- const details = (_a = JSON.parse(metadata !== null && metadata !== void 0 ? metadata : "{}")) === null || _a === void 0 ? void 0 : _a.details;
4039
- return typeof details === "string" ? details : "";
4040
- } catch {
4041
- return "";
4607
+ // Permissions rarely change mid-session, so a successful check is kept until
4608
+ // the page reloads. A failed one is dropped so a remount tries again.
4609
+ const permissionCache = new Map();
4610
+ function canSearch(slug, personId) {
4611
+ const check = PERMISSION_CHECKS[slug];
4612
+ const cached = permissionCache.get(slug);
4613
+ if (!check) {
4614
+ return Promise.resolve(true);
4615
+ } else if (cached) {
4616
+ return cached;
4617
+ } else {
4618
+ const promise = check(personId).catch(() => {
4619
+ permissionCache.delete(slug);
4620
+ return false;
4621
+ });
4622
+ permissionCache.set(slug, promise);
4623
+ return promise;
4042
4624
  }
4043
4625
  }
4044
- // pco:resource is the more specific signal, so it wins when both tags are
4045
- // present. A resource that isn't a PCRL is ignored rather than recorded.
4046
- function visitFromPage({
4047
- metadata,
4048
- resource,
4049
- title
4050
- }) {
4051
- const product = currentProduct();
4052
- if (resource && isPcrl(resource)) {
4053
- return {
4054
- pcrl: resource,
4055
- type: "resource"
4626
+
4627
+ function useSearchableProducts(apps) {
4628
+ // Keyed by name because providers hand over a fresh `apps` array on renders
4629
+ // where nothing changed.
4630
+ const accessibleKey = inSwitcherOrder(apps).map(({
4631
+ slug
4632
+ }) => slug).join(",");
4633
+ const personId = useCurrentPersonId();
4634
+ const [settled, setSettled] = React__default.useState();
4635
+ React__default.useEffect(() => {
4636
+ let current = true;
4637
+ const accessible = accessibleKey.split(",").flatMap(slug => SEARCHABLE_PRODUCTS.filter(product => product.slug === slug));
4638
+ Promise.all(accessible.map(({
4639
+ slug
4640
+ }) => canSearch(slug, personId))).then(allowed => {
4641
+ if (current) {
4642
+ setSettled({
4643
+ accessibleKey,
4644
+ personId,
4645
+ products: accessible.filter((_, index) => allowed[index])
4646
+ });
4647
+ }
4648
+ });
4649
+ return () => {
4650
+ current = false;
4056
4651
  };
4057
- } else if (title && title.trim() !== "" && product) {
4652
+ }, [accessibleKey, personId]);
4653
+ if ((settled === null || settled === void 0 ? void 0 : settled.accessibleKey) === accessibleKey && settled.personId === personId) {
4058
4654
  return {
4059
- details: detailsFromMetadata(metadata),
4060
- path: window.location.pathname,
4061
- product,
4062
- title,
4063
- type: "page"
4655
+ products: settled.products,
4656
+ status: "done"
4064
4657
  };
4065
4658
  } else {
4066
- return null;
4659
+ return {
4660
+ status: "loading"
4661
+ };
4067
4662
  }
4068
4663
  }
4069
- // The entry Recently Visited would record for the current page, or null when
4070
- // the page isn't eligible. Shared by the recorder and the debug panel so the
4071
- // panel always shows what actually gets stored.
4072
- function useCurrentVisit() {
4073
- const {
4074
- recently_visitable: title,
4075
- resource
4076
- } = usePcoMeta();
4077
- const metadata = useRecentlyVisitableMetadata();
4078
- return useMemo(() => visitFromPage({
4079
- metadata,
4080
- resource,
4081
- title
4082
- }), [metadata, resource, title]);
4083
- }
4084
- // Products hand the person's ID to AppsProvider, not to the topbar itself,
4085
- // and several pass it as a number.
4086
- function useCurrentPersonId() {
4087
- const {
4088
- userId
4089
- } = useStore();
4090
- return userId == null ? undefined : String(userId);
4664
+ function inSwitcherOrder(apps) {
4665
+ return groupApps(apps).restProducts.flatMap(app => SEARCHABLE_PRODUCTS.filter(product => product.appName === app.name));
4091
4666
  }
4092
4667
 
4093
- // Per-origin localStorage is a stopgap: each product keeps its own list until
4094
- // PCA-409 moves this to storage shared across product subdomains. Only this
4095
- // file should need to change when it does.
4096
- function storageKey({
4097
- env,
4098
- personId
4668
+ function ProductSearch({
4669
+ apps,
4670
+ env
4099
4671
  }) {
4100
- return `topbar.recently_visited.${env}.${personId}`;
4101
- }
4102
- // localStorage doesn't notify the tab that wrote it, so readers in this tab
4103
- // learn about new visits from this counter instead.
4104
- const changes = createStore({
4105
- version: 0
4106
- });
4107
- function readRecentlyVisited(scope) {
4108
- try {
4109
- return parseRecentlyVisited(localStorage.getItem(storageKey(scope)), scope.personId);
4110
- } catch {
4111
- return [];
4112
- }
4113
- }
4114
- function recordVisit(visit, scope) {
4115
- if (isEntry(visit)) {
4116
- try {
4117
- localStorage.setItem(storageKey(scope), serializeRecentlyVisited(addVisit(readRecentlyVisited(scope), visit), scope.personId));
4118
- changes.setState({
4119
- version: changes.getState().version + 1
4120
- });
4121
- } catch {
4122
- // Storage can be full or blocked; recents are disposable.
4123
- }
4124
- }
4672
+ const searchable = useSearchableProducts(apps);
4673
+ const [open, setOpen] = React__default.useState(false);
4674
+ const available = searchable.status === "done" && searchable.products.length > 0;
4675
+ useSearchShortcut(available, open, setOpen);
4676
+ if (!available) return null;
4677
+ return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(SearchButton, {
4678
+ onClick: () => setOpen(true)
4679
+ }), /*#__PURE__*/React__default.createElement(Modal, {
4680
+ "aria-label": "Search",
4681
+ dismissible: true,
4682
+ onClose: () => setOpen(false),
4683
+ open: open
4684
+ }, /*#__PURE__*/React__default.createElement(SearchPanel, {
4685
+ env: env,
4686
+ products: searchable.products
4687
+ })));
4125
4688
  }
4126
4689
 
4127
4690
  function useRecordRecentlyVisited({
@@ -4166,9 +4729,9 @@ function StyledRoot({
4166
4729
  `), children);
4167
4730
  }
4168
4731
 
4169
- var styles$9 = {"route":"route--8BWe","is-active":"is-active-FsU9Y","apps-routes-separator":"apps-routes-separator-Nlm-N"};
4732
+ var styles$a = {"route":"route--8BWe","is-active":"is-active-FsU9Y","apps-routes-separator":"apps-routes-separator-Nlm-N"};
4170
4733
 
4171
- var styles$8 = {"popup":"popup-mDNKn","trigger-wrap":"trigger-wrap-Jd-KR","trigger":"trigger-WgRT5","is-visible":"is-visible-GZC0-","trigger-orgname":"trigger-orgname-TDHCc"};
4734
+ var styles$9 = {"popup":"popup-mDNKn","trigger-wrap":"trigger-wrap-Jd-KR","trigger":"trigger-WgRT5","is-visible":"is-visible-GZC0-","trigger-orgname":"trigger-orgname-TDHCc"};
4172
4735
 
4173
4736
  const UserMenu = ({
4174
4737
  appName,
@@ -4187,7 +4750,7 @@ const UserMenu = ({
4187
4750
  component: Outsider,
4188
4751
  onOutsideClick: () => setUserMenuVisible(false),
4189
4752
  cleanup: !userMenuVisible,
4190
- className: styles$8.popup,
4753
+ className: styles$9.popup,
4191
4754
  isVisible: userMenuVisible
4192
4755
  }, /*#__PURE__*/React__default.createElement("div", null, hasConnectedPeople ? (/*#__PURE__*/React__default.createElement("div", {
4193
4756
  style: {
@@ -4295,7 +4858,7 @@ const UserMenu = ({
4295
4858
  }, /*#__PURE__*/React__default.createElement(Text, {
4296
4859
  color: "var(--t-text-color-secondary)",
4297
4860
  lineHeight: "24px"
4298
- }, "Log out"), /*#__PURE__*/React__default.createElement(Icon, {
4861
+ }, "Log out"), /*#__PURE__*/React__default.createElement(Icon$1, {
4299
4862
  name: "general.intoBox",
4300
4863
  fill: "var(--t-icon-color-secondary)"
4301
4864
  })))))));
@@ -4331,11 +4894,11 @@ const UserMenuTrigger = ({
4331
4894
  userMenuVisible
4332
4895
  }) => {
4333
4896
  return /*#__PURE__*/React__default.createElement("div", {
4334
- className: styles$8["trigger-wrap"],
4897
+ className: styles$9["trigger-wrap"],
4335
4898
  "data-pendo": "topbar-org-switcher"
4336
4899
  }, /*#__PURE__*/React__default.createElement(Hoverable, {
4337
- className: classnames(styles$8["trigger"], {
4338
- [styles$8["is-visible"]]: userMenuVisible
4900
+ className: classnames(styles$9["trigger"], {
4901
+ [styles$9["is-visible"]]: userMenuVisible
4339
4902
  }),
4340
4903
  component: "button",
4341
4904
  type: "button",
@@ -4347,7 +4910,7 @@ const UserMenuTrigger = ({
4347
4910
  requestConnectedPeopleFetch();
4348
4911
  }
4349
4912
  }, showOrgName && (/*#__PURE__*/React__default.createElement("span", {
4350
- className: styles$8["trigger-orgname"]
4913
+ className: styles$9["trigger-orgname"]
4351
4914
  }, orgName)), /*#__PURE__*/React__default.createElement(DisclosureChevronIcon, null)));
4352
4915
  };
4353
4916
 
@@ -4407,13 +4970,14 @@ const NotSmallTopbar = ({
4407
4970
  colors: props.colors,
4408
4971
  requestAppsFetch: props.requestAppsFetch
4409
4972
  })), /*#__PURE__*/React__default.createElement("div", {
4410
- className: styles$9["apps-routes-separator"]
4973
+ className: styles$a["apps-routes-separator"]
4411
4974
  }), routesVisible && props.routes, /*#__PURE__*/React__default.createElement("div", {
4412
4975
  style: {
4413
4976
  margin: "auto"
4414
4977
  }
4415
4978
  }), !notificationsEnabled && notifications(), productSearchEnabled ? (/*#__PURE__*/React__default.createElement(ProductSearch, {
4416
- apps: props.apps
4979
+ apps: props.apps,
4980
+ env: props.env
4417
4981
  })) : search({
4418
4982
  hideRoutes: () => setRoutesVisible(false),
4419
4983
  showRoutes: () => setRoutesVisible(true)
@@ -4447,12 +5011,12 @@ const NotSmallRoute = ({
4447
5011
  ...nativeProps
4448
5012
  }) => {
4449
5013
  return /*#__PURE__*/React__default.createElement("a", {
4450
- className: classnames(styles$9["route"], className, active && styles$9["is-active"]),
5014
+ className: classnames(styles$a["route"], className, active && styles$a["is-active"]),
4451
5015
  ...nativeProps
4452
5016
  });
4453
5017
  };
4454
5018
 
4455
- var styles$7 = {"avatar":"avatar-tW-wn"};
5019
+ var styles$8 = {"avatar":"avatar-tW-wn"};
4456
5020
 
4457
5021
  function Avatar({
4458
5022
  env,
@@ -4462,7 +5026,7 @@ function Avatar({
4462
5026
  }) {
4463
5027
  const url = incomingUrl || `${pcoUrl$1(env)("people")}/static/no_photo_thumbnail_gray.svg`;
4464
5028
  return /*#__PURE__*/React__default.createElement("div", {
4465
- className: styles$7.avatar,
5029
+ className: styles$8.avatar,
4466
5030
  style: {
4467
5031
  backgroundImage: `url(${url})`,
4468
5032
  ...style
@@ -32221,7 +32785,7 @@ function requireReactDom () {
32221
32785
 
32222
32786
  var reactDomExports = requireReactDom();
32223
32787
 
32224
- var styles$6 = {"trigger":"trigger-Th-Up","layer":"layer-ck6BD","layer--visible":"layer--visible-4E2p1","panel":"panel-vobWE","header":"header-xndfZ","heading":"heading-uXyQf","close":"close-pK0Lk","list":"list-zWvrn","link":"link-Kf3qj","current":"current-g3Ey-","accounts-item":"accounts-item-eQqDD","accounts-link":"accounts-link-SeIbD"};
32788
+ var styles$7 = {"trigger":"trigger-Th-Up","layer":"layer-ck6BD","layer--visible":"layer--visible-4E2p1","panel":"panel-vobWE","header":"header-xndfZ","heading":"heading-uXyQf","close":"close-pK0Lk","list":"list-zWvrn","link":"link-Kf3qj","current":"current-g3Ey-","accounts-item":"accounts-item-eQqDD","accounts-link":"accounts-link-SeIbD"};
32225
32789
 
32226
32790
  const AppsSwitcher = ({
32227
32791
  appName,
@@ -32249,8 +32813,8 @@ const AppsSwitcher = ({
32249
32813
  }, /*#__PURE__*/React__default.createElement("a", {
32250
32814
  "aria-current": name === appName ? "page" : undefined,
32251
32815
  "aria-label": name,
32252
- className: classnames(styles$6.link, {
32253
- [styles$6.current]: name === appName
32816
+ className: classnames(styles$7.link, {
32817
+ [styles$7.current]: name === appName
32254
32818
  }),
32255
32819
  "data-pendo": `app-switcher--change-app--${name}`,
32256
32820
  "data-turbolinks": false,
@@ -32262,48 +32826,48 @@ const AppsSwitcher = ({
32262
32826
  "aria-expanded": visible,
32263
32827
  "aria-haspopup": "true",
32264
32828
  "aria-label": `App switcher: ${appName}`,
32265
- className: styles$6.trigger,
32829
+ className: styles$7.trigger,
32266
32830
  onClick: onTriggerClick,
32267
32831
  type: "button"
32268
32832
  }, /*#__PURE__*/React__default.createElement(ColorAppIcon, {
32269
32833
  appName: appName.replace(/[\s-]/, "")
32270
32834
  })), /*#__PURE__*/reactDomExports.createPortal(/*#__PURE__*/React__default.createElement("div", {
32271
32835
  ref: containerRef,
32272
- className: classnames(styles$6.layer, {
32273
- [styles$6["layer--visible"]]: visible
32836
+ className: classnames(styles$7.layer, {
32837
+ [styles$7["layer--visible"]]: visible
32274
32838
  }),
32275
32839
  onClick: toggle,
32276
32840
  onKeyDown: handleKeyDown
32277
32841
  }, /*#__PURE__*/React__default.createElement("nav", {
32278
32842
  "aria-label": "Switch app",
32279
32843
  "aria-modal": visible,
32280
- className: styles$6.panel,
32844
+ className: styles$7.panel,
32281
32845
  "data-pendo": "app-switcher",
32282
32846
  onClick: event => event.stopPropagation(),
32283
32847
  role: "dialog"
32284
32848
  }, /*#__PURE__*/React__default.createElement("div", {
32285
- className: styles$6.header
32849
+ className: styles$7.header
32286
32850
  }, /*#__PURE__*/React__default.createElement("span", {
32287
- className: styles$6.heading
32851
+ className: styles$7.heading
32288
32852
  }, "Switch to"), /*#__PURE__*/React__default.createElement("button", {
32289
32853
  "aria-label": "Close",
32290
- className: styles$6.close,
32854
+ className: styles$7.close,
32291
32855
  onClick: toggle,
32292
32856
  type: "button"
32293
32857
  }, /*#__PURE__*/React__default.createElement(X, {
32294
32858
  "aria-hidden": "true",
32295
32859
  fill: "var(--t-icon-color-dim)"
32296
32860
  }))), /*#__PURE__*/React__default.createElement("ul", {
32297
- className: styles$6.list,
32861
+ className: styles$7.list,
32298
32862
  role: "list"
32299
32863
  }, homeProduct && productLink(homeProduct.name), restProducts.map(({
32300
32864
  name
32301
32865
  }) => productLink(name)), accountsProduct && (/*#__PURE__*/React__default.createElement("li", {
32302
- className: styles$6["accounts-item"]
32866
+ className: styles$7["accounts-item"]
32303
32867
  }, /*#__PURE__*/React__default.createElement("a", {
32304
32868
  "aria-current": appName === "Accounts" ? "page" : undefined,
32305
32869
  "aria-label": "Account settings",
32306
- className: classnames(styles$6.link, styles$6["accounts-link"]),
32870
+ className: classnames(styles$7.link, styles$7["accounts-link"]),
32307
32871
  "data-pendo": "app-switcher--change-app--Accounts",
32308
32872
  "data-turbolinks": false,
32309
32873
  href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(accountsProduct.name)
@@ -32326,14 +32890,14 @@ ClientStorage.defaultProps = {
32326
32890
  method: "localStorage"
32327
32891
  };
32328
32892
 
32329
- var styles$5 = {"appmenu-button":"appmenu-button-b0UvT","appmenu-layer":"appmenu-layer-0IQpN","appmenu-container":"appmenu-container-gackd","appmenu-scroll":"appmenu-scroll-mPSx0","appmenu-list":"appmenu-list-h7clX","appmenu-list-a":"appmenu-list-a-rGIQN","appmenu-list-a--top":"appmenu-list-a--top-RT-2F","appmenu-list-a--bottom":"appmenu-list-a--bottom-YDeBC","appmenu-close-button":"appmenu-close-button-vv9bV","current-route":"current-route-wpJgz","is-active":"is-active-wy8Bg","routes":"routes-Nmt1S","route":"route-kkknQ","profile-button":"profile-button-a2WO2"};
32893
+ var styles$6 = {"appmenu-button":"appmenu-button-b0UvT","appmenu-layer":"appmenu-layer-0IQpN","appmenu-container":"appmenu-container-gackd","appmenu-scroll":"appmenu-scroll-mPSx0","appmenu-list":"appmenu-list-h7clX","appmenu-list-a":"appmenu-list-a-rGIQN","appmenu-list-a--top":"appmenu-list-a--top-RT-2F","appmenu-list-a--bottom":"appmenu-list-a--bottom-YDeBC","appmenu-close-button":"appmenu-close-button-vv9bV","current-route":"current-route-wpJgz","is-active":"is-active-wy8Bg","routes":"routes-Nmt1S","route":"route-kkknQ","profile-button":"profile-button-a2WO2"};
32330
32894
 
32331
32895
  function AccountSettingsLink({
32332
32896
  env
32333
32897
  }) {
32334
32898
  const accountsName = "accounts";
32335
32899
  return /*#__PURE__*/React__default.createElement("a", {
32336
- className: classnames(styles$5["appmenu-list-a"], styles$5["appmenu-list-a--bottom"]),
32900
+ className: classnames(styles$6["appmenu-list-a"], styles$6["appmenu-list-a--bottom"]),
32337
32901
  "data-pendo": `app-switcher--change-app--${accountsName}`,
32338
32902
  "data-turbolinks": false,
32339
32903
  href: pcoUrl$1(env)(accountsName) + getAppsSwitchProductPath(accountsName)
@@ -32356,7 +32920,7 @@ const CloseMenuButton = ({
32356
32920
  }) => (/*#__PURE__*/React__default.createElement("button", {
32357
32921
  "aria-label": "Close menu",
32358
32922
  type: "button",
32359
- className: styles$5["appmenu-close-button"],
32923
+ className: styles$6["appmenu-close-button"],
32360
32924
  style: style
32361
32925
  }, /*#__PURE__*/React__default.createElement(X, {
32362
32926
  fill: color,
@@ -32417,7 +32981,7 @@ const LegacyAppsSwitcher = ({
32417
32981
  "aria-haspopup": "true",
32418
32982
  "aria-label": `App switcher: ${appName}`,
32419
32983
  type: "button",
32420
- className: styles$5["appmenu-button"],
32984
+ className: styles$6["appmenu-button"],
32421
32985
  onClick: onTriggerClick
32422
32986
  }, /*#__PURE__*/React__default.createElement(ColorAppIcon, {
32423
32987
  appName: appName.replace(/[\s-]/, "")
@@ -32425,7 +32989,7 @@ const LegacyAppsSwitcher = ({
32425
32989
  item: "Topbar:Small:Menus:Products:visibility",
32426
32990
  render: () => (/*#__PURE__*/React__default.createElement("div", {
32427
32991
  ref: appMenuRef,
32428
- className: styles$5["appmenu-layer"],
32992
+ className: styles$6["appmenu-layer"],
32429
32993
  style: {
32430
32994
  ...(visible && {
32431
32995
  display: "block"
@@ -32442,37 +33006,37 @@ const LegacyAppsSwitcher = ({
32442
33006
  e.stopPropagation();
32443
33007
  }
32444
33008
  },
32445
- className: styles$5["appmenu-container"]
33009
+ className: styles$6["appmenu-container"]
32446
33010
  }, /*#__PURE__*/React__default.createElement("a", {
32447
33011
  "aria-current": appName === (homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name) ? "page" : undefined,
32448
33012
  "aria-label": homeProduct ? homeProduct.name : appName,
32449
33013
  "data-pendo": `app-switcher--change-app--${homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name}`,
32450
33014
  "data-turbolinks": false,
32451
- className: classnames(styles$5["appmenu-list-a"], styles$5["appmenu-list-a--top"]),
33015
+ className: classnames(styles$6["appmenu-list-a"], styles$6["appmenu-list-a--top"]),
32452
33016
  href: homeProduct ? pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name) : undefined
32453
33017
  }, /*#__PURE__*/React__default.createElement(UserMenuAppLockup, {
32454
33018
  appName: homeProduct ? homeProduct.name : appName
32455
33019
  })), /*#__PURE__*/React__default.createElement("div", {
32456
- className: styles$5["appmenu-scroll"]
33020
+ className: styles$6["appmenu-scroll"]
32457
33021
  }, /*#__PURE__*/React__default.createElement("ul", {
32458
33022
  "data-pendo": "app-switcher",
32459
- className: styles$5["appmenu-list"]
33023
+ className: styles$6["appmenu-list"]
32460
33024
  }, appsMenuFormatter(restProducts).map(({
32461
33025
  name
32462
33026
  }) => (/*#__PURE__*/React__default.createElement("li", {
32463
33027
  key: name,
32464
- className: styles$5["appmenu-list-li"]
33028
+ className: styles$6["appmenu-list-li"]
32465
33029
  }, /*#__PURE__*/React__default.createElement("a", {
32466
33030
  "aria-current": name === appName ? "page" : undefined,
32467
33031
  "aria-label": name,
32468
33032
  "data-pendo": `app-switcher--change-app--${name}`,
32469
33033
  "data-turbolinks": false,
32470
- className: styles$5["appmenu-list-a"],
33034
+ className: styles$6["appmenu-list-a"],
32471
33035
  href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(name)
32472
33036
  }, /*#__PURE__*/React__default.createElement(UserMenuAppLockup, {
32473
33037
  appName: name
32474
33038
  }))))), accountsProduct && (/*#__PURE__*/React__default.createElement("li", {
32475
- className: styles$5["appmenu-list-li"]
33039
+ className: styles$6["appmenu-list-li"]
32476
33040
  }, /*#__PURE__*/React__default.createElement(AccountSettingsLink, {
32477
33041
  env: env
32478
33042
  }))))))))
@@ -32536,19 +33100,19 @@ const SmallTopbar = ({
32536
33100
  toggle: toggleProductSwitcher,
32537
33101
  visible: productSwitcherVisible
32538
33102
  })), routesMenuVisible && (/*#__PURE__*/React__default.createElement("div", {
32539
- className: styles$5["routes"]
33103
+ className: styles$6["routes"]
32540
33104
  }, props.routes)), /*#__PURE__*/React__default.createElement("button", {
32541
33105
  type: "button",
32542
- className: classnames(styles$5["current-route"], {
32543
- [styles$5["is-active"]]: routesMenuVisible
33106
+ className: classnames(styles$6["current-route"], {
33107
+ [styles$6["is-active"]]: routesMenuVisible
32544
33108
  }),
32545
33109
  onClick: props.routes.length > 0 ? () => {
32546
33110
  setProductSwitcherVisible(false);
32547
33111
  setRoutesMenuVisible(visible => !visible);
32548
33112
  } : null
32549
- }, /*#__PURE__*/React__default.createElement(CurrentRouteComponent, null, capitalize(currentRouteText)), props.routes.length > 0 && /*#__PURE__*/React__default.createElement(DisclosureChevronIcon, null)), /*#__PURE__*/React__default.createElement("button", {
33113
+ }, /*#__PURE__*/React__default.createElement(CurrentRouteComponent, null, capitalize$1(currentRouteText)), props.routes.length > 0 && /*#__PURE__*/React__default.createElement(DisclosureChevronIcon, null)), /*#__PURE__*/React__default.createElement("button", {
32550
33114
  type: "button",
32551
- className: styles$5["profile-button"],
33115
+ className: styles$6["profile-button"],
32552
33116
  onClick: () => {
32553
33117
  var _a, _b;
32554
33118
  props.requestConnectedPeopleFetch();
@@ -32564,7 +33128,7 @@ const SmallRoute = ({
32564
33128
  className,
32565
33129
  ...props
32566
33130
  }) => (/*#__PURE__*/React__default.createElement("a", {
32567
- className: classnames(styles$5["route"], className, active && styles$5["is-active"]),
33131
+ className: classnames(styles$6["route"], className, active && styles$6["is-active"]),
32568
33132
  ...props
32569
33133
  }));
32570
33134
 
@@ -32703,7 +33267,7 @@ const topbarTheme = {
32703
33267
  id: "topbar"
32704
33268
  };
32705
33269
 
32706
- var styles$4 = {"debug-pco-resource":"debug-pco-resource-sLteM","debug-pco-resource-header":"debug-pco-resource-header-5fEbe","debug-pco-resource-badge":"debug-pco-resource-badge-J9v1F","debug-pco-resource-title":"debug-pco-resource-title-volWV","debug-pco-resource-field":"debug-pco-resource-field-7Gitp","debug-pco-resource-field-label":"debug-pco-resource-field-label--f1xX","debug-pco-resource-field-value":"debug-pco-resource-field-value-YoX-C","debug-pco-resource-note":"debug-pco-resource-note-N5xyf","debug-pco-resource-json":"debug-pco-resource-json-Rx-JI"};
33270
+ var styles$5 = {"debug-pco-resource":"debug-pco-resource-sLteM","debug-pco-resource-header":"debug-pco-resource-header-5fEbe","debug-pco-resource-badge":"debug-pco-resource-badge-J9v1F","debug-pco-resource-title":"debug-pco-resource-title-volWV","debug-pco-resource-field":"debug-pco-resource-field-7Gitp","debug-pco-resource-field-label":"debug-pco-resource-field-label--f1xX","debug-pco-resource-field-value":"debug-pco-resource-field-value-YoX-C","debug-pco-resource-note":"debug-pco-resource-note-N5xyf","debug-pco-resource-json":"debug-pco-resource-json-Rx-JI"};
32707
33271
 
32708
33272
  function isPresent(value) {
32709
33273
  return value !== undefined && value.trim() !== "";
@@ -32732,15 +33296,31 @@ function Field({
32732
33296
  value
32733
33297
  }) {
32734
33298
  return /*#__PURE__*/React__default.createElement("div", {
32735
- className: styles$4["debug-pco-resource-field"]
33299
+ className: styles$5["debug-pco-resource-field"]
32736
33300
  }, /*#__PURE__*/React__default.createElement("span", {
32737
- className: styles$4["debug-pco-resource-field-label"]
33301
+ className: styles$5["debug-pco-resource-field-label"]
32738
33302
  }, label), /*#__PURE__*/React__default.createElement("span", {
32739
- className: styles$4["debug-pco-resource-field-value"]
33303
+ className: styles$5["debug-pco-resource-field-value"]
32740
33304
  }, value));
32741
33305
  }
33306
+ // Only staff ghosting or impersonating a person get pco:staff-access-level,
33307
+ // which keeps the panel away from regular users on staging.
32742
33308
  function DebugPcoResource({
32743
33309
  env
33310
+ }) {
33311
+ const {
33312
+ "staff-access-level": staffAccessLevel
33313
+ } = usePcoMeta();
33314
+ if (isPresent(staffAccessLevel)) {
33315
+ return /*#__PURE__*/React__default.createElement(DebugPanel, {
33316
+ env: env
33317
+ });
33318
+ } else {
33319
+ return null;
33320
+ }
33321
+ }
33322
+ function DebugPanel({
33323
+ env
32744
33324
  }) {
32745
33325
  const {
32746
33326
  recently_visitable: recentlyVisitable,
@@ -32749,12 +33329,17 @@ function DebugPcoResource({
32749
33329
  const [lookup, setLookup] = useState({
32750
33330
  status: "idle"
32751
33331
  });
32752
- const hasResource = isPresent(resource);
33332
+ const visit = useCurrentVisit();
33333
+ const personId = useCurrentPersonId();
33334
+ const recentlyVisited = useRecentlyVisited({
33335
+ env,
33336
+ personId
33337
+ });
33338
+ const pcrl = (visit === null || visit === void 0 ? void 0 : visit.type) === "resource" ? visit.pcrl : null;
32753
33339
  const hasRecentlyVisitable = isPresent(recentlyVisitable);
33340
+ const hasInvalidResource = isPresent(resource) && !isPcrl(resource);
32754
33341
  useEffect(() => {
32755
- // pco:resource is the more specific signal, so it always wins when both
32756
- // tags are present.
32757
- const url = hasResource ? pcrlLookupUrl(resource, env) : null;
33342
+ const url = pcrl ? pcrlLookupUrl(pcrl, env) : null;
32758
33343
  if (!url) {
32759
33344
  setLookup({
32760
33345
  status: "idle"
@@ -32786,37 +33371,45 @@ function DebugPcoResource({
32786
33371
  return () => {
32787
33372
  cancelled = true;
32788
33373
  };
32789
- }, [resource, env, hasResource]);
33374
+ }, [pcrl, env]);
32790
33375
  return /*#__PURE__*/React__default.createElement("div", {
32791
- className: classnames("staff-only", styles$4["debug-pco-resource"])
33376
+ className: classnames("staff-only", styles$5["debug-pco-resource"])
32792
33377
  }, /*#__PURE__*/React__default.createElement("div", {
32793
- className: styles$4["debug-pco-resource-header"]
33378
+ className: styles$5["debug-pco-resource-header"]
32794
33379
  }, /*#__PURE__*/React__default.createElement("span", {
32795
- className: styles$4["debug-pco-resource-badge"]
33380
+ className: styles$5["debug-pco-resource-badge"]
32796
33381
  }, "Staff"), /*#__PURE__*/React__default.createElement("span", {
32797
- className: styles$4["debug-pco-resource-title"]
33382
+ className: styles$5["debug-pco-resource-title"]
32798
33383
  }, "Recently Visited debug")), /*#__PURE__*/React__default.createElement(Field, {
32799
33384
  label: "pco:resource",
32800
33385
  value: describeTag("resource", resource)
32801
33386
  }), /*#__PURE__*/React__default.createElement(Field, {
32802
33387
  label: "pco:recently_visitable",
32803
33388
  value: describeTag("recently_visitable", recentlyVisitable)
32804
- }), !hasResource && !hasRecentlyVisitable && (/*#__PURE__*/React__default.createElement("p", {
32805
- className: styles$4["debug-pco-resource-note"]
32806
- }, "This page is not eligible for Recently Visited")), hasResource && hasRecentlyVisitable && (/*#__PURE__*/React__default.createElement("p", {
32807
- className: styles$4["debug-pco-resource-note"]
32808
- }, "Both tags are present \u2014 pco:resource takes precedence for the Recently Visited entry.")), hasResource && lookup.status === "loading" && (/*#__PURE__*/React__default.createElement("p", {
32809
- className: styles$4["debug-pco-resource-note"]
32810
- }, "Looking up resource\u2026")), hasResource && lookup.status === "error" && (/*#__PURE__*/React__default.createElement("p", {
32811
- className: styles$4["debug-pco-resource-note"]
32812
- }, lookup.message)), hasResource && lookup.status === "loaded" && (/*#__PURE__*/React__default.createElement("pre", {
32813
- className: styles$4["debug-pco-resource-json"]
32814
- }, JSON.stringify(lookup.json, null, 2))), !hasResource && hasRecentlyVisitable && (/*#__PURE__*/React__default.createElement("p", {
32815
- className: styles$4["debug-pco-resource-note"]
32816
- }, "Recently Visited title: ", recentlyVisitable)));
32817
- }
32818
-
32819
- var styles$3 = {"drawer-wrap":"drawer-wrap-J0rSL","is-visible":"is-visible-KTME8","drawer":"drawer-j7f4l"};
33389
+ }), hasInvalidResource && (/*#__PURE__*/React__default.createElement("p", {
33390
+ className: styles$5["debug-pco-resource-note"]
33391
+ }, "pco:resource isn't a PCRL (product/v2/type/id), so Recently Visited ignores it.")), visit === null && (/*#__PURE__*/React__default.createElement("p", {
33392
+ className: styles$5["debug-pco-resource-note"]
33393
+ }, "This page is not eligible for Recently Visited")), pcrl && hasRecentlyVisitable && (/*#__PURE__*/React__default.createElement("p", {
33394
+ className: styles$5["debug-pco-resource-note"]
33395
+ }, "Both tags are present \u2014 pco:resource takes precedence for the Recently Visited entry.")), pcrl && lookup.status === "loading" && (/*#__PURE__*/React__default.createElement("p", {
33396
+ className: styles$5["debug-pco-resource-note"]
33397
+ }, "Looking up resource\u2026")), pcrl && lookup.status === "error" && (/*#__PURE__*/React__default.createElement("p", {
33398
+ className: styles$5["debug-pco-resource-note"]
33399
+ }, lookup.message)), pcrl && lookup.status === "loaded" && (/*#__PURE__*/React__default.createElement("pre", {
33400
+ className: styles$5["debug-pco-resource-json"]
33401
+ }, JSON.stringify(lookup.json, null, 2))), (visit === null || visit === void 0 ? void 0 : visit.type) === "page" && (/*#__PURE__*/React__default.createElement("p", {
33402
+ className: styles$5["debug-pco-resource-note"]
33403
+ }, "Recently Visited title: ", visit.title)), /*#__PURE__*/React__default.createElement("span", {
33404
+ className: styles$5["debug-pco-resource-field-label"]
33405
+ }, "Recently Visited"), personId ? (/*#__PURE__*/React__default.createElement("pre", {
33406
+ className: styles$5["debug-pco-resource-json"]
33407
+ }, JSON.stringify(recentlyVisited, null, 2))) : (/*#__PURE__*/React__default.createElement("p", {
33408
+ className: styles$5["debug-pco-resource-note"]
33409
+ }, "No current person, so nothing is recorded.")));
33410
+ }
33411
+
33412
+ var styles$4 = {"drawer-wrap":"drawer-wrap-J0rSL","is-visible":"is-visible-KTME8","drawer":"drawer-j7f4l"};
32820
33413
 
32821
33414
  function Drawer({
32822
33415
  children,
@@ -32824,7 +33417,7 @@ function Drawer({
32824
33417
  }) {
32825
33418
  return /*#__PURE__*/React__default.createElement("div", {
32826
33419
  id: id,
32827
- className: styles$3["drawer"]
33420
+ className: styles$4["drawer"]
32828
33421
  }, children);
32829
33422
  }
32830
33423
  function DrawerWrap({
@@ -32834,7 +33427,7 @@ function DrawerWrap({
32834
33427
  ...props
32835
33428
  }) {
32836
33429
  return /*#__PURE__*/React__default.createElement("div", {
32837
- className: classnames(styles$3["drawer-wrap"], className, isVisible && styles$3["is-visible"]),
33430
+ className: classnames(styles$4["drawer-wrap"], className, isVisible && styles$4["is-visible"]),
32838
33431
  ...props
32839
33432
  }, children);
32840
33433
  }
@@ -33018,15 +33611,50 @@ const SwitchView = ({
33018
33611
  }));
33019
33612
  };
33020
33613
 
33021
- 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"};
33614
+ const collections = {
33615
+ general,
33616
+ toolbar
33617
+ };
33618
+ const paths = collections;
33619
+ function Icon({
33620
+ name,
33621
+ size = 16,
33622
+ style,
33623
+ theme,
33624
+ title,
33625
+ ...props
33626
+ }) {
33627
+ const [collection, symbol] = name.split(".");
33628
+ return /*#__PURE__*/React__default.createElement("svg", {
33629
+ "aria-hidden": title ? undefined : true,
33630
+ fill: "currentColor",
33631
+ height: size,
33632
+ role: title ? "img" : undefined,
33633
+ style: {
33634
+ color: theme && `var(${theme})`,
33635
+ flexShrink: 0,
33636
+ ...style
33637
+ },
33638
+ viewBox: "0 0 16 16",
33639
+ width: size,
33640
+ ...props
33641
+ }, title && /*#__PURE__*/React__default.createElement("title", null, title), /*#__PURE__*/React__default.createElement("path", {
33642
+ d: paths[collection][symbol]
33643
+ }));
33644
+ }
33645
+
33646
+ var styles$3 = {"color-mode-switcher":"color-mode-switcher-JpczO","color-mode-switcher-label":"color-mode-switcher-label-3jPM2","color-mode-switcher-modes":"color-mode-switcher-modes-u-bic"};
33022
33647
 
33023
33648
  const COLOR_MODES = [{
33649
+ icon: "toolbar.sun",
33024
33650
  label: "Light",
33025
33651
  value: "light"
33026
33652
  }, {
33653
+ icon: "toolbar.moon",
33027
33654
  label: "Dark",
33028
33655
  value: "dark"
33029
33656
  }, {
33657
+ icon: "toolbar.display",
33030
33658
  label: "System",
33031
33659
  value: "system"
33032
33660
  }];
@@ -33038,14 +33666,15 @@ function ColorModeSwitcher() {
33038
33666
  const updateStore = useUpdateStore();
33039
33667
  const [counter] = useState(() => COLOR_MODE_INSTANCE_COUNTER++);
33040
33668
  return /*#__PURE__*/React__default.createElement("fieldset", {
33041
- className: styles$2["color-mode-switcher"]
33669
+ className: styles$3["color-mode-switcher"]
33042
33670
  }, /*#__PURE__*/React__default.createElement("legend", {
33043
- className: styles$2["color-mode-switcher-label"]
33671
+ className: styles$3["color-mode-switcher-label"]
33044
33672
  }, "Appearance", " "), /*#__PURE__*/React__default.createElement("div", {
33045
- className: styles$2["color-mode-switcher-modes"]
33673
+ className: styles$3["color-mode-switcher-modes"]
33046
33674
  }, COLOR_MODES.map(({
33047
- value,
33048
- label
33675
+ icon,
33676
+ label,
33677
+ value
33049
33678
  }) => (/*#__PURE__*/React__default.createElement("label", {
33050
33679
  key: value
33051
33680
  }, /*#__PURE__*/React__default.createElement("input", {
@@ -33062,8 +33691,9 @@ function ColorModeSwitcher() {
33062
33691
  document.documentElement.setAttribute(RESOLVED_COLOR_MODE_ATTRIBUTE, resolveColorMode(colorMode));
33063
33692
  updateColorMode(colorMode);
33064
33693
  }
33065
- }), /*#__PURE__*/React__default.createElement(ColorModeIcon, {
33066
- mode: value
33694
+ }), /*#__PURE__*/React__default.createElement(Icon, {
33695
+ name: icon,
33696
+ size: 15
33067
33697
  }), label)))));
33068
33698
  }
33069
33699
  function updateColorMode(mode) {
@@ -33078,28 +33708,6 @@ function updateColorMode(mode) {
33078
33708
  method: "POST"
33079
33709
  });
33080
33710
  }
33081
- const COLOR_MODE_ICON_PATHS = {
33082
- dark: "M12.66667,9.07579 C11.90047,9.42229 11.04997,9.61514 10.15457,9.61514 C6.78392,9.61514 4.05151,6.88275 4.05151,3.51215 C4.05151,2.61663 4.24438,1.76615 4.59086,1 C2.47336,1.95762 1,4.08858 1,6.56366 C1,9.93423 3.73241,12.66663 7.103,12.66663 C9.57807,12.66663 11.70907,11.19333 12.66667,9.07579 Z",
33083
- light: "M6.83333,1 L6.83333,2.16667 M6.83333,11.50003 L6.83333,12.66663 M2.16667,6.83334 L1,6.83334 M3.51657,3.51657 L2.69161,2.69161 M10.1501,3.51657 L10.9751,2.69161 M3.51657,10.15253 L2.69161,10.97743 M10.1501,10.15253 L10.9751,10.97743 M12.6667,6.83334 L11.5,6.83334 M9.75,6.83334 C9.75,8.44415 8.44415,9.75 6.83333,9.75 C5.2225,9.75 3.91667,8.44415 3.91667,6.83334 C3.91667,5.22251 5.2225,3.91667 6.83333,3.91667 C8.44415,3.91667 9.75,5.22251 9.75,6.83334 Z",
33084
- system: "M4.75,12.50003 L9.41667,12.50003 M7.08334,10.16667 L7.08334,12.50003 M4.05,10.16667 L10.11665,10.16667 C11.09675,10.16667 11.58675,10.16667 11.96115,9.97592 C12.29045,9.80815 12.55815,9.54046 12.72595,9.21117 C12.91665,8.83678 12.91665,8.34678 12.91665,7.36667 L12.91665,4.8 C12.91665,3.81991 12.91665,3.32986 12.72595,2.95552 C12.55815,2.62623 12.29045,2.35852 11.96115,2.19074 C11.58675,2 11.09675,2 10.11665,2 L4.05,2 C3.06991,2 2.57986,2 2.20552,2.19074 C1.87623,2.35852 1.60852,2.62623 1.44074,2.95552 C1.25,3.32986 1.25,3.81991 1.25,4.8 L1.25,7.36667 C1.25,8.34678 1.25,8.83678 1.44074,9.21117 C1.60852,9.54046 1.87623,9.80815 2.20552,9.97592 C2.57986,10.16667 3.06991,10.16667 4.05,10.16667 Z"
33085
- };
33086
- function ColorModeIcon({
33087
- mode
33088
- }) {
33089
- return /*#__PURE__*/React__default.createElement("svg", {
33090
- width: "16",
33091
- height: "16",
33092
- viewBox: "0 0 14 14",
33093
- xmlns: "http://www.w3.org/2000/svg",
33094
- "aria-hidden": "true"
33095
- }, /*#__PURE__*/React__default.createElement("path", {
33096
- d: COLOR_MODE_ICON_PATHS[mode],
33097
- strokeWidth: "1.5",
33098
- fill: "none",
33099
- strokeLinecap: "round",
33100
- strokeLinejoin: "round"
33101
- }));
33102
- }
33103
33711
 
33104
33712
  const requestClearConnectedPeopleCache = () => remove();
33105
33713
  function UserProfile({
@@ -33164,8 +33772,8 @@ function UserProfile({
33164
33772
  width: "100%"
33165
33773
  }, /*#__PURE__*/React__default.createElement(Icon, {
33166
33774
  name: mfaConfigured ? "general.lock" : "general.unlock",
33167
- size: "sm",
33168
- color: mfaConfigured ? "var(--t-icon-color-status-success)" : "var(--t-icon-color-status-error)"
33775
+ size: 12,
33776
+ theme: mfaConfigured ? "--t-icon-color-status-success" : "--t-icon-color-status-error"
33169
33777
  }), /*#__PURE__*/React__default.createElement(Text, {
33170
33778
  size: 4,
33171
33779
  weight: 400,
@@ -33215,7 +33823,8 @@ function UserProfile({
33215
33823
  size: 3
33216
33824
  }, "Log out"), /*#__PURE__*/React__default.createElement(Icon, {
33217
33825
  name: "general.intoBox",
33218
- fill: "var(--t-icon-color-secondary)"
33826
+ size: 14,
33827
+ theme: "--t-icon-color-secondary"
33219
33828
  })))), env !== "production" && /*#__PURE__*/React__default.createElement(DebugPcoResource, {
33220
33829
  env: env
33221
33830
  }), hasConnectedPeople && (/*#__PURE__*/React__default.createElement(StackView$2, {
@@ -33277,26 +33886,42 @@ function UserProfile({
33277
33886
 
33278
33887
  var ActionType;
33279
33888
  (function (ActionType) {
33889
+ ActionType["clearPending"] = "clearPending";
33280
33890
  ActionType["close"] = "close";
33281
33891
  ActionType["mount"] = "mount";
33282
33892
  ActionType["open"] = "open";
33893
+ ActionType["setPending"] = "setPending";
33283
33894
  ActionType["unmount"] = "unmount";
33284
33895
  })(ActionType || (ActionType = {}));
33285
33896
  const initialState = {
33286
33897
  currentContent: null,
33287
- open: false
33898
+ open: false,
33899
+ pending: null
33288
33900
  };
33289
33901
  const reducer = (state, action) => {
33902
+ var _a, _b;
33290
33903
  switch (action.type) {
33291
33904
  case ActionType.mount:
33292
33905
  return {
33293
33906
  ...state,
33294
- currentContent: action.payload.content
33907
+ currentContent: action.payload.content,
33908
+ pending: ((_a = state.pending) === null || _a === void 0 ? void 0 : _a.tool) === ((_b = action.payload.content) === null || _b === void 0 ? void 0 : _b.name) ? state.pending : null
33295
33909
  };
33296
33910
  case ActionType.unmount:
33297
33911
  return {
33298
33912
  ...state,
33299
- currentContent: null
33913
+ currentContent: null,
33914
+ pending: null
33915
+ };
33916
+ case ActionType.setPending:
33917
+ return {
33918
+ ...state,
33919
+ pending: action.payload
33920
+ };
33921
+ case ActionType.clearPending:
33922
+ return {
33923
+ ...state,
33924
+ pending: null
33300
33925
  };
33301
33926
  case ActionType.open:
33302
33927
  return {
@@ -33315,13 +33940,181 @@ const useToolbarReducer = overrides => React.useReducer(reducer, {
33315
33940
  ...overrides
33316
33941
  });
33317
33942
 
33318
- var styles$1 = {"toolbar":"toolbar-KHJR5","toolbar-button":"toolbar-button-Ye4h5","is-current":"is-current-DZK47","toolbar-button--avatar":"toolbar-button--avatar-5FUik","toolbar-button--home":"toolbar-button--home-Pthd0"};
33943
+ var styles$2 = {"toolbar":"toolbar-KHJR5","toolbar-button":"toolbar-button-Ye4h5","is-current":"is-current-DZK47","toolbar-button--avatar":"toolbar-button--avatar-5FUik","toolbar-button--home":"toolbar-button--home-Pthd0"};
33319
33944
 
33320
- const pcoUrl = pcoUrl$1;
33945
+ // PCO Box serves TLS by default, and a bare http:// frame inside an https
33946
+ // product page is blocked as mixed content, so follow the page's scheme.
33947
+ const iframeSrc = () => `${window.location.protocol}//pco.test/iframe`;
33948
+ const STAFF_PINK = "var(--t-fill-color-product-staff)";
33949
+ // The pco.test domain is only ever served by a local PCO Box, so the tool
33950
+ // stays hidden in staging and production — and on Topbar's own dev server,
33951
+ // which runs on localhost and has no pco.test to frame.
33952
+ function isPcoTestHost() {
33953
+ const {
33954
+ hostname
33955
+ } = window.location;
33956
+ return hostname === "pco.test" || hostname.endsWith(".pco.test");
33957
+ }
33958
+ // Flattened from the source artwork's two outline paths — its gradients,
33959
+ // filters and clip paths carry nothing at icon size. Those paths are authored
33960
+ // on a 128px canvas, hence the transform: it fits the pair into the same ~24px
33961
+ // optical box the sibling toolbar icons occupy. Filled explicitly rather than
33962
+ // with `currentColor` so the mark stays staff pink instead of inheriting the
33963
+ // toolbar's text color and its hover/active states.
33964
+ const WRENCH = "m22.42 17.2 12.96 12.95-2.5 8.78-8.78 2.5-12.96-12.96-5.86 5.86 14.89 21.46 17.56-3.34 61.63 65.62c9.44 9.51 22.17-4.75 13.34-12.87L46.85 42.92l4.01-17.76-22.14-14.27-6.3 6.31zm81.06 91.97c1.68-1.68 4.4-1.68 6.08 0 1.68 1.68 1.68 4.4 0 6.08-1.68 1.67-4.4 1.67-6.08 0-1.68-1.68-1.68-4.4 0-6.08z";
33965
+ const SCREWDRIVER = "m55.8 12 .76.16c8.84-2.94 28.68 8.64 24.26 18.96-1.12 1.62-.93 4.5.42 6.31L56.43 62.25c-1.39-1.39-4.48-1.36-5.9 0L9.5 101.63c-3.74 3.59-.8 6.64.78 8.25l7.36 7.36c1.61 1.6 5.86 3.76 8.88.56l38.72-40.81c1.35-1.42 1.36-4.47 0-5.87 0 0 .01-.02 0-.02l24.91-24.9c2.63 2.37 5.11 2.57 7.47 1.93 4.55-1.25 7.09 3.21 9.81 6.46l-1.23 1.21c-1.82 1.84-1.65 4.94.39 6.98s5.15 2.21 6.97.39l8.14-8.13c1.82-1.84 1.65-4.94-.4-6.97-1.84-1.85-4.51-2.09-6.37-.77-5.58-6.26-2.65-7.2-10.01-15.35L91.13 17.86C81.2 8.12 68.3 8.13 55.8 12z";
33966
+ function ToolsIcon() {
33967
+ return /*#__PURE__*/React__default.createElement("svg", {
33968
+ "aria-hidden": "true",
33969
+ width: "32",
33970
+ height: "32",
33971
+ viewBox: "0 0 32 32",
33972
+ xmlns: "http://www.w3.org/2000/svg"
33973
+ }, /*#__PURE__*/React__default.createElement("g", {
33974
+ fill: STAFF_PINK,
33975
+ transform: "translate(2.92 2.66) scale(.204)"
33976
+ }, /*#__PURE__*/React__default.createElement("path", {
33977
+ d: WRENCH
33978
+ }), /*#__PURE__*/React__default.createElement("path", {
33979
+ d: SCREWDRIVER
33980
+ })));
33981
+ }
33982
+ // A cross-origin frame's own history object is unreachable (touching it throws
33983
+ // a SecurityError), but the frame's navigations do land in the top-level joint
33984
+ // session history, so the *parent's* back/forward traverse them while leaving
33985
+ // the host page in place. Counting frame loads tracks how deep we are, which is
33986
+ // what keeps a click on Back from stepping past the frame's first page and
33987
+ // navigating the host app instead. Loads we caused are attributed to the
33988
+ // traversal that requested them rather than counted as new navigations.
33989
+ function useFrameHistory() {
33990
+ const [steps, setSteps] = useState({
33991
+ back: 0,
33992
+ forward: 0
33993
+ });
33994
+ // Traversal has to read the current counts outside of render, so the ref is
33995
+ // the source of truth and state exists to paint the buttons.
33996
+ const stepsRef = useRef(steps);
33997
+ const pendingTraversal = useRef(null);
33998
+ const frameIsLoaded = useRef(false);
33999
+ const moveTo = useCallback(next => {
34000
+ stepsRef.current = next;
34001
+ setSteps(next);
34002
+ }, []);
34003
+ const onFrameLoad = useCallback(() => {
34004
+ if (!frameIsLoaded.current) {
34005
+ // The frame's first page is the floor, not a step.
34006
+ frameIsLoaded.current = true;
34007
+ } else {
34008
+ const traversal = pendingTraversal.current;
34009
+ pendingTraversal.current = null;
34010
+ const {
34011
+ back,
34012
+ forward
34013
+ } = stepsRef.current;
34014
+ if (traversal === "back") {
34015
+ moveTo({
34016
+ back: back - 1,
34017
+ forward: forward + 1
34018
+ });
34019
+ } else if (traversal === "forward") {
34020
+ moveTo({
34021
+ back: back + 1,
34022
+ forward: forward - 1
34023
+ });
34024
+ } else {
34025
+ moveTo({
34026
+ back: back + 1,
34027
+ forward: 0
34028
+ });
34029
+ }
34030
+ }
34031
+ }, [moveTo]);
34032
+ // Guarded here rather than trusting the button's disabled state: traversing
34033
+ // with no frame step left would navigate the host app out from under us.
34034
+ const traverse = useCallback(direction => {
34035
+ if (stepsRef.current[direction] > 0) {
34036
+ pendingTraversal.current = direction;
34037
+ window.history[direction]();
34038
+ }
34039
+ }, []);
34040
+ return {
34041
+ onFrameLoad,
34042
+ steps,
34043
+ traverse
34044
+ };
34045
+ }
34046
+ function NavButton({
34047
+ direction,
34048
+ disabled,
34049
+ onClick
34050
+ }) {
34051
+ return /*#__PURE__*/React__default.createElement(Button$1
34052
+ // Tapestry's `disabled` suppresses the click but emits no ARIA state.
34053
+ , {
34054
+ "aria-disabled": disabled,
34055
+ disabled: disabled,
34056
+ hover: {
34057
+ backgroundColor: "var(--t-fill-color-neutral-070)"
34058
+ },
34059
+ icon: {
34060
+ fill: "var(--t-icon-color-dim)",
34061
+ name: direction === "back" ? "general.leftChevron" : "general.rightChevron",
34062
+ size: "md"
34063
+ },
34064
+ onClick: onClick,
34065
+ opacity: disabled ? 0.35 : 1,
34066
+ theme: false,
34067
+ title: direction === "back" ? "Back" : "Forward"
34068
+ });
34069
+ }
34070
+ function DeveloperToolsComponent({
34071
+ closeDrawer
34072
+ }) {
34073
+ const {
34074
+ onFrameLoad,
34075
+ steps,
34076
+ traverse
34077
+ } = useFrameHistory();
34078
+ return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(DrawerHeader, {
34079
+ closeDrawer: closeDrawer
34080
+ }, /*#__PURE__*/React__default.createElement(StackView$2, {
34081
+ alignment: "center",
34082
+ axis: "horizontal",
34083
+ spacing: 0.25
34084
+ }, /*#__PURE__*/React__default.createElement(NavButton, {
34085
+ direction: "back",
34086
+ disabled: steps.back === 0,
34087
+ onClick: () => traverse("back")
34088
+ }), /*#__PURE__*/React__default.createElement(NavButton, {
34089
+ direction: "forward",
34090
+ disabled: steps.forward === 0,
34091
+ onClick: () => traverse("forward")
34092
+ }), /*#__PURE__*/React__default.createElement(DrawerHeading, {
34093
+ paddingLeft: 0.5
34094
+ }, "Developer Tools"))), /*#__PURE__*/React__default.createElement(Box, {
34095
+ as: "iframe",
34096
+ border: "none",
34097
+ flex: "1",
34098
+ minHeight: 0,
34099
+ onLoad: onFrameLoad,
34100
+ src: iframeSrc(),
34101
+ title: "Developer Tools",
34102
+ width: "100%"
34103
+ }));
34104
+ }
34105
+ const DeveloperTools = {
34106
+ component: DeveloperToolsComponent,
34107
+ icons: {
34108
+ component: ToolsIcon
34109
+ },
34110
+ label: "Developer Tools",
34111
+ // No space: `name` becomes an element id and a pendo attribute.
34112
+ name: "DeveloperTools"
34113
+ };
33321
34114
 
33322
- var styles = {"banner":"banner-YfQ37","stacked":"stacked-EwLRh","content":"content-lNjWf","icon":"icon-abz7q","iconShape":"iconShape-6uyot","iconSparkle":"iconSparkle-t0OzK","text":"text--Vlhl","title":"title-zOEOR","subtitle":"subtitle-ufLnk","chevron":"chevron-oOUA-","inlineContent":"inlineContent-h5sVl","sparkle":"sparkle-uuMBp","inlineTitle":"inlineTitle-ru2Rk","action":"action-fSY3n","actionLabel":"actionLabel-MRBjb","actionChevron":"actionChevron-DghlN"};
34115
+ var styles$1 = {"banner":"banner-YfQ37","stacked":"stacked-EwLRh","content":"content-lNjWf","icon":"icon-abz7q","iconShape":"iconShape-6uyot","iconSparkle":"iconSparkle-t0OzK","text":"text--Vlhl","title":"title-zOEOR","subtitle":"subtitle-ufLnk","chevron":"chevron-oOUA-","inlineContent":"inlineContent-h5sVl","sparkle":"sparkle-uuMBp","inlineTitle":"inlineTitle-ru2Rk","action":"action-fSY3n","actionLabel":"actionLabel-MRBjb","actionChevron":"actionChevron-DghlN"};
33323
34116
 
33324
- const SPARKLE_PATH = "M3.809 10.1332C3.87511 9.95596 4.12579 9.95588 4.19181 10.1332L4.63712 11.3363C4.64169 11.3487 4.65116 11.359 4.66349 11.3637L5.86661 11.809C6.04452 11.8749 6.04455 12.126 5.86661 12.1918L4.66349 12.6371C4.65134 12.6417 4.64173 12.6513 4.63712 12.6635L4.19181 13.8666C4.12597 14.0445 3.87486 14.0445 3.809 13.8666L3.36368 12.6635C3.35905 12.6512 3.3487 12.6417 3.33634 12.6371L2.13321 12.1918C1.95588 12.1258 1.95596 11.8751 2.13321 11.809L3.33634 11.3637C3.34877 11.3591 3.35908 11.3488 3.36368 11.3363L3.809 10.1332ZM8.52091 3.33341C8.68571 2.88899 9.31518 2.88891 9.47989 3.33341L10.5922 6.34122C10.6037 6.37226 10.6285 6.39712 10.6596 6.4086L13.6664 7.52091C14.1113 7.68553 14.1113 8.31528 13.6664 8.47989L10.6596 9.5922C10.6285 9.6037 10.6037 9.6285 10.5922 9.65958L9.47989 12.6664C9.31528 13.1113 8.68553 13.1113 8.52091 12.6664L7.4086 9.65958C7.39712 9.62853 7.37226 9.60371 7.34122 9.5922L4.33341 8.47989C3.88891 8.31518 3.88899 7.68571 4.33341 7.52091L7.34122 6.4086C7.3723 6.3971 7.3971 6.3723 7.4086 6.34122L8.52091 3.33341ZM3.809 2.13321C3.87511 1.95596 4.12579 1.95588 4.19181 2.13321L4.63712 3.33634C4.64169 3.3487 4.65116 3.35905 4.66349 3.36368L5.86661 3.809C6.04452 3.87486 6.04455 4.12597 5.86661 4.19181L4.66349 4.63712C4.65134 4.64173 4.64173 4.65134 4.63712 4.66349L4.19181 5.86661C4.12597 6.04455 3.87486 6.04452 3.809 5.86661L3.36368 4.66349C3.35905 4.65116 3.3487 4.64169 3.33634 4.63712L2.13321 4.19181C1.95588 4.12579 1.95596 3.87511 2.13321 3.809L3.33634 3.36368C3.34877 3.35908 3.35908 3.34877 3.36368 3.33634L3.809 2.13321Z";
34117
+ const SPARKLE_PATH$1 = "M3.809 10.1332C3.87511 9.95596 4.12579 9.95588 4.19181 10.1332L4.63712 11.3363C4.64169 11.3487 4.65116 11.359 4.66349 11.3637L5.86661 11.809C6.04452 11.8749 6.04455 12.126 5.86661 12.1918L4.66349 12.6371C4.65134 12.6417 4.64173 12.6513 4.63712 12.6635L4.19181 13.8666C4.12597 14.0445 3.87486 14.0445 3.809 13.8666L3.36368 12.6635C3.35905 12.6512 3.3487 12.6417 3.33634 12.6371L2.13321 12.1918C1.95588 12.1258 1.95596 11.8751 2.13321 11.809L3.33634 11.3637C3.34877 11.3591 3.35908 11.3488 3.36368 11.3363L3.809 10.1332ZM8.52091 3.33341C8.68571 2.88899 9.31518 2.88891 9.47989 3.33341L10.5922 6.34122C10.6037 6.37226 10.6285 6.39712 10.6596 6.4086L13.6664 7.52091C14.1113 7.68553 14.1113 8.31528 13.6664 8.47989L10.6596 9.5922C10.6285 9.6037 10.6037 9.6285 10.5922 9.65958L9.47989 12.6664C9.31528 13.1113 8.68553 13.1113 8.52091 12.6664L7.4086 9.65958C7.39712 9.62853 7.37226 9.60371 7.34122 9.5922L4.33341 8.47989C3.88891 8.31518 3.88899 7.68571 4.33341 7.52091L7.34122 6.4086C7.3723 6.3971 7.3971 6.3723 7.4086 6.34122L8.52091 3.33341ZM3.809 2.13321C3.87511 1.95596 4.12579 1.95588 4.19181 2.13321L4.63712 3.33634C4.64169 3.3487 4.65116 3.35905 4.66349 3.36368L5.86661 3.809C6.04452 3.87486 6.04455 4.12597 5.86661 4.19181L4.66349 4.63712C4.65134 4.64173 4.64173 4.65134 4.63712 4.66349L4.19181 5.86661C4.12597 6.04455 3.87486 6.04452 3.809 5.86661L3.36368 4.66349C3.35905 4.65116 3.3487 4.64169 3.33634 4.63712L2.13321 4.19181C1.95588 4.12579 1.95596 3.87511 2.13321 3.809L3.33634 3.36368C3.34877 3.35908 3.35908 3.34877 3.36368 3.33634L3.809 2.13321Z";
33325
34118
  // Same stops as the AskAIBanner border gradient, so the two stay in sync.
33326
34119
  const GRADIENT_STOPS = [{
33327
34120
  color: "var(--t-fill-color-tag-teal-040)",
@@ -33370,7 +34163,7 @@ function SparkleIcon({
33370
34163
  stopColor: color
33371
34164
  }
33372
34165
  })))))), /*#__PURE__*/React__default.createElement("path", {
33373
- d: SPARKLE_PATH,
34166
+ d: SPARKLE_PATH$1,
33374
34167
  fill: isGradient ? `url(#${gradientId})` : "currentColor"
33375
34168
  }));
33376
34169
  }
@@ -33379,8 +34172,8 @@ function AskAIBanner(props) {
33379
34172
  var _a, _b;
33380
34173
  const isStacked = props.variant !== "inline";
33381
34174
  return /*#__PURE__*/React__default.createElement("button", {
33382
- className: classnames(styles.banner, {
33383
- [styles.stacked]: isStacked
34175
+ className: classnames(styles$1.banner, {
34176
+ [styles$1.stacked]: isStacked
33384
34177
  }),
33385
34178
  onClick: () => props.onClick(),
33386
34179
  type: "button"
@@ -33395,15 +34188,15 @@ function StackedContent({
33395
34188
  title
33396
34189
  }) {
33397
34190
  return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("span", {
33398
- className: styles.content
34191
+ className: styles$1.content
33399
34192
  }, /*#__PURE__*/React__default.createElement(AskAIIcon, null), /*#__PURE__*/React__default.createElement("span", {
33400
- className: styles.text
34193
+ className: styles$1.text
33401
34194
  }, /*#__PURE__*/React__default.createElement("span", {
33402
- className: styles.title
34195
+ className: styles$1.title
33403
34196
  }, title), /*#__PURE__*/React__default.createElement("span", {
33404
- className: styles.subtitle
33405
- }, "Support answers tailored to you"))), /*#__PURE__*/React__default.createElement(Icon, {
33406
- className: styles.chevron,
34197
+ className: styles$1.subtitle
34198
+ }, "Support answers tailored to you"))), /*#__PURE__*/React__default.createElement(Icon$1, {
34199
+ className: styles$1.chevron,
33407
34200
  name: "general.rightChevron",
33408
34201
  size: "16px"
33409
34202
  }));
@@ -33413,18 +34206,18 @@ function InlineContent({
33413
34206
  title
33414
34207
  }) {
33415
34208
  return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("span", {
33416
- className: styles.inlineContent
34209
+ className: styles$1.inlineContent
33417
34210
  }, /*#__PURE__*/React__default.createElement(SparkleIcon, {
33418
- className: styles.sparkle,
34211
+ className: styles$1.sparkle,
33419
34212
  fill: "currentColor"
33420
34213
  }), /*#__PURE__*/React__default.createElement("span", {
33421
- className: styles.inlineTitle
34214
+ className: styles$1.inlineTitle
33422
34215
  }, title)), /*#__PURE__*/React__default.createElement("span", {
33423
- className: styles.action
34216
+ className: styles$1.action
33424
34217
  }, /*#__PURE__*/React__default.createElement("span", {
33425
- className: styles.actionLabel
33426
- }, action), /*#__PURE__*/React__default.createElement(Icon, {
33427
- className: styles.actionChevron,
34218
+ className: styles$1.actionLabel
34219
+ }, action), /*#__PURE__*/React__default.createElement(Icon$1, {
34220
+ className: styles$1.actionChevron,
33428
34221
  name: "general.rightChevron",
33429
34222
  size: "14px"
33430
34223
  })));
@@ -33441,9 +34234,9 @@ function AskAIIcon() {
33441
34234
  const borderA = `${uid}-border-a`;
33442
34235
  const borderB = `${uid}-border-b`;
33443
34236
  return /*#__PURE__*/React__default.createElement("span", {
33444
- className: styles.icon
34237
+ className: styles$1.icon
33445
34238
  }, /*#__PURE__*/React__default.createElement("svg", {
33446
- className: styles.iconShape,
34239
+ className: styles$1.iconShape,
33447
34240
  viewBox: "0 0 32 32",
33448
34241
  fill: "none",
33449
34242
  "aria-hidden": "true"
@@ -33614,7 +34407,7 @@ function AskAIIcon() {
33614
34407
  stopColor: "#D48B91",
33615
34408
  stopOpacity: "0"
33616
34409
  })))), /*#__PURE__*/React__default.createElement("svg", {
33617
- className: styles.iconSparkle,
34410
+ className: styles$1.iconSparkle,
33618
34411
  viewBox: "0 0 19.4995 17.9999",
33619
34412
  "aria-hidden": "true"
33620
34413
  }, /*#__PURE__*/React__default.createElement("path", {
@@ -33623,6 +34416,23 @@ function AskAIIcon() {
33623
34416
  })));
33624
34417
  }
33625
34418
 
34419
+ var styles = {"button":"button-2NVI1","sparkle":"sparkle-ruJGP","label":"label-4NUGB"};
34420
+
34421
+ function AskAIButton({
34422
+ onClick,
34423
+ text = "Ask AI"
34424
+ }) {
34425
+ return /*#__PURE__*/React__default.createElement("button", {
34426
+ className: styles.button,
34427
+ onClick: () => onClick(),
34428
+ type: "button"
34429
+ }, /*#__PURE__*/React__default.createElement(SparkleIcon, {
34430
+ className: styles.sparkle
34431
+ }), /*#__PURE__*/React__default.createElement("span", {
34432
+ className: styles.label
34433
+ }, text));
34434
+ }
34435
+
33626
34436
  // The article heading lives in the deepest populated hierarchy level (lvl0 is
33627
34437
  // always "Planning Center").
33628
34438
  const hitTitle = hierarchy => (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl6) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl5) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl4) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl3) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl2) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl1) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl0) || "";
@@ -33646,7 +34456,7 @@ const HeadingIcon = ({
33646
34456
  const logoName = appName.toLowerCase().replace(/\s+/g, "-");
33647
34457
  // Accounts has no tapestry Logo, so use the cog icon
33648
34458
  if (logoName === "accounts") {
33649
- return /*#__PURE__*/React__default.createElement(Icon, {
34459
+ return /*#__PURE__*/React__default.createElement(Icon$1, {
33650
34460
  name: "general.cog",
33651
34461
  color: "foregroundSecondary"
33652
34462
  });
@@ -33710,7 +34520,7 @@ const CustomSearchBox = ({
33710
34520
  },
33711
34521
  placeholder: "Search our documentation",
33712
34522
  value: query,
33713
- renderRight: /*#__PURE__*/React__default.createElement(Icon, {
34523
+ renderRight: /*#__PURE__*/React__default.createElement(Icon$1, {
33714
34524
  name: "general.search"
33715
34525
  }),
33716
34526
  onChange: e => setQuery(e.target.value),
@@ -33778,16 +34588,24 @@ const DocumentationSearch = ({
33778
34588
  // not the path, so updating it is a same-document navigation that doesn't
33779
34589
  // reload the iframe, but also lets the route survive a forced reload (e.g.
33780
34590
  // Turbo re-parenting the topbar).
33781
- function buildPicobotSrc(origin, path) {
34591
+ //
34592
+ // `panel` loads picobot as its own toolbar panel rather than inside Helpdesk.
34593
+ function buildPicobotSrc(origin, path, {
34594
+ panel = false
34595
+ } = {}) {
33782
34596
  const [rawPath, queryPart] = path.replace(/^\/+/, "").split("?", 2);
33783
34597
  const safePath = rawPath.split("/").filter(segment => segment && segment !== "." && segment !== "..").join("/");
33784
34598
  const fragment = queryPart !== undefined ? `${safePath}?${queryPart}` : safePath;
33785
- const base = new URL("topbar", origin).toString();
34599
+ const url = new URL("topbar", origin);
34600
+ if (panel) url.searchParams.set("panel", "1");
34601
+ const base = url.toString();
33786
34602
  return fragment ? `${base}#${fragment}` : base;
33787
34603
  }
33788
34604
 
33789
34605
  const LOAD_TIMEOUT_MS = 3000;
33790
- function createPicobotIframe(picobotOrigin) {
34606
+ function createPicobotIframe(picobotOrigin, {
34607
+ panel = false
34608
+ } = {}) {
33791
34609
  return /*#__PURE__*/React__default.forwardRef(function PicobotIframe({
33792
34610
  currentPath: iframePath,
33793
34611
  onChangePath: onIframeChangePath,
@@ -33843,7 +34661,9 @@ function createPicobotIframe(picobotOrigin) {
33843
34661
  (_b = (_a = iframeRef.current) === null || _a === void 0 ? void 0 : _a.contentWindow) === null || _b === void 0 ? void 0 : _b.postMessage(message, picobotOrigin);
33844
34662
  }
33845
34663
  }), []);
33846
- const iframeSrc = buildPicobotSrc(picobotOrigin, loadedPath);
34664
+ const iframeSrc = buildPicobotSrc(picobotOrigin, loadedPath, {
34665
+ panel
34666
+ });
33847
34667
  return /*#__PURE__*/React__default.createElement("iframe", {
33848
34668
  ref: iframeRef,
33849
34669
  src: iframeSrc,
@@ -33882,22 +34702,28 @@ function HelpdeskComponent({
33882
34702
  currentPath,
33883
34703
  env,
33884
34704
  helpdeskConfig,
33885
- onChangePath
34705
+ isPicobotLoading,
34706
+ onChangePath,
34707
+ openPicobot
33886
34708
  }) {
33887
34709
  const PicobotIframe = React__default.useMemo(() => createPicobotIframe(pcoUrl(env)("picobot")), [env]);
34710
+ const loadingFallback = /*#__PURE__*/React__default.createElement(StackView$1, {
34711
+ flex: 1,
34712
+ axis: "vertical"
34713
+ }, /*#__PURE__*/React__default.createElement(DrawerHeader, {
34714
+ closeDrawer: closeDrawer
34715
+ }, /*#__PURE__*/React__default.createElement(StackView$1, {
34716
+ axis: "horizontal",
34717
+ spacing: 1
34718
+ }, /*#__PURE__*/React__default.createElement(DrawerHeading, null, "Help and support"), /*#__PURE__*/React__default.createElement(LoadingIndicator$1, null))));
34719
+ // The add-on reads its args once, so mounting before the gate answers
34720
+ // would leave out openPicobot for good.
34721
+ if (isPicobotLoading) return loadingFallback;
33888
34722
  return /*#__PURE__*/React__default.createElement(UiExtensionFirstPartyComponent, {
33889
34723
  grow: 1,
33890
34724
  display: "flex",
33891
34725
  extensionId: env === "staging" ? "42" : "11",
33892
- loadingFallback: /*#__PURE__*/React__default.createElement(StackView$1, {
33893
- flex: 1,
33894
- axis: "vertical"
33895
- }, /*#__PURE__*/React__default.createElement(DrawerHeader, {
33896
- closeDrawer: closeDrawer
33897
- }, /*#__PURE__*/React__default.createElement(StackView$1, {
33898
- axis: "horizontal",
33899
- spacing: 1
33900
- }, /*#__PURE__*/React__default.createElement(DrawerHeading, null, "Help and support"), /*#__PURE__*/React__default.createElement(LoadingIndicator$1, null)))),
34726
+ loadingFallback: loadingFallback,
33901
34727
  additionalComponents: {
33902
34728
  AskAIBanner: {
33903
34729
  component: AskAIBanner,
@@ -33917,6 +34743,18 @@ function HelpdeskComponent({
33917
34743
  }
33918
34744
  }
33919
34745
  },
34746
+ AskAIButton: {
34747
+ component: AskAIButton,
34748
+ properties: {
34749
+ onClick: {
34750
+ event: false,
34751
+ type: Function
34752
+ },
34753
+ text: {
34754
+ type: String
34755
+ }
34756
+ }
34757
+ },
33920
34758
  CalendlyWidget: {
33921
34759
  component: CalendlyWidget,
33922
34760
  properties: {
@@ -33995,7 +34833,11 @@ function HelpdeskComponent({
33995
34833
  currentPageParams: location.search,
33996
34834
  currentPath,
33997
34835
  helpdeskConfig,
33998
- onChangePath
34836
+ onChangePath,
34837
+ // Its presence tells helpdesk that Picobot has its own panel.
34838
+ ...(openPicobot && {
34839
+ openPicobot
34840
+ })
33999
34841
  }
34000
34842
  });
34001
34843
  }
@@ -34542,6 +35384,142 @@ const Notifications = {
34542
35384
  name: "Notifications"
34543
35385
  };
34544
35386
 
35387
+ const SPARKLE_PATH = "M8.7824 18.8791C8.88371 18.5973 9.26996 18.5973 9.37127 18.8791L9.94256 20.465C10.0222 20.6863 10.1914 20.8616 10.4064 20.9435L11.6779 21.4269L11.9484 21.5304L11.9963 21.5539C12.2205 21.6839 12.2047 22.039 11.9484 22.1369L11.6779 22.2394L10.4064 22.7248C10.1915 22.8068 10.0222 22.9812 9.94256 23.2023L9.37127 24.7893L9.34881 24.8381C9.23098 25.054 8.92273 25.0539 8.80486 24.8381L8.7824 24.7893L8.2111 23.2023C8.14141 23.0088 8.0029 22.8513 7.82536 22.76L7.74723 22.7248L6.47573 22.2394L6.20522 22.1369C5.93172 22.0325 5.93164 21.6347 6.20522 21.5304L6.47476 21.4269L7.74723 20.9435C7.96226 20.8616 8.13147 20.6863 8.2111 20.465L8.7824 18.8791ZM17.5706 8.11138C17.8241 7.40747 18.7928 7.40734 19.0462 8.11138L20.472 12.0782C20.6711 12.6318 21.0952 13.0687 21.6331 13.2735L25.4867 14.7413C26.1711 15.002 26.1711 15.9983 25.4867 16.2589L21.6331 17.7267C21.0952 17.9316 20.6711 18.3684 20.472 18.922L19.2649 22.2785L19.0462 22.8889L18.9905 23.0119C18.6956 23.5517 17.9212 23.5515 17.6262 23.0119L17.5706 22.8889L17.3508 22.2795L16.1448 18.922C15.9457 18.3684 15.5206 17.9316 14.9827 17.7267L11.1291 16.2589C10.4449 15.9982 10.4449 15.0021 11.1291 14.7413L14.9827 13.2735C15.4533 13.0943 15.8379 12.7376 16.0598 12.2804L16.1448 12.0782L17.5706 8.11138ZM18.0266 12.755C17.6351 13.8438 16.7916 14.7249 15.6946 15.1427L14.7551 15.5001L15.6946 15.8575C16.7916 16.2753 17.6351 17.1564 18.0266 18.2453L18.3079 19.0275L18.5891 18.2453C18.9806 17.1564 19.8242 16.2753 20.9212 15.8575L21.8597 15.5001L20.9212 15.1427C19.8242 14.7249 18.9806 13.8439 18.5891 12.755L18.3079 11.9718L18.0266 12.755ZM8.7824 6.21097C8.88388 5.9297 9.26982 5.92966 9.37127 6.21097L9.94256 7.7979C10.0222 8.01911 10.1915 8.19346 10.4064 8.27544L11.6779 8.75982L11.8322 9.16607L11.9484 8.86334C12.2218 8.96779 12.2218 9.36542 11.9484 9.46979L11.6779 9.57233L10.4064 10.0567C10.1914 10.1387 10.0222 10.3139 9.94256 10.5352L9.37127 12.1212C9.26996 12.4029 8.88371 12.4029 8.7824 12.1212L8.2111 10.5352C8.14139 10.3415 8.00304 10.1832 7.82536 10.0919L7.74723 10.0567L6.47573 9.57233L6.20522 9.46979C5.9486 9.37195 5.93265 9.01568 6.15737 8.8858L6.20522 8.86334L7.74723 8.27544C7.96214 8.19346 8.13149 8.0191 8.2111 7.7979L8.69451 6.45413L8.7824 6.21097ZM11.9484 8.86334L11.8322 9.16607L11.6779 8.75982L11.9484 8.86334Z";
35388
+ // Helpdesk reads this context off the email path to open its email form the
35389
+ // same way a handoff inside its own drawer does.
35390
+ function emailPathWithContext(context = {}) {
35391
+ const params = new URLSearchParams();
35392
+ for (const key of ["conversationId", "source", "turnIndex"]) {
35393
+ const value = context[key];
35394
+ if (value !== null && value !== undefined) params.set(key, String(value));
35395
+ }
35396
+ return `email?${params}`;
35397
+ }
35398
+ function PicobotComponent({
35399
+ clearPending,
35400
+ closeDrawer,
35401
+ currentPath,
35402
+ env,
35403
+ onChangePath,
35404
+ openTool,
35405
+ pending
35406
+ }) {
35407
+ const PicobotIframe = React__default.useMemo(() => createPicobotIframe(pcoUrl(env)("picobot"), {
35408
+ panel: true
35409
+ }), [env]);
35410
+ const picobotRef = React__default.useRef(null);
35411
+ const [isPicobotReady, setIsPicobotReady] = React__default.useState(false);
35412
+ // Picobot has no listener until it sends READY, so openPicobot's draft or
35413
+ // conversation waits for it.
35414
+ React__default.useEffect(function handOverPendingOncePicobotIsReady() {
35415
+ var _a, _b;
35416
+ if (!pending || !isPicobotReady) return;
35417
+ const {
35418
+ conversationId,
35419
+ draft,
35420
+ ticketExternalId
35421
+ } = pending;
35422
+ if (conversationId) {
35423
+ (_a = picobotRef.current) === null || _a === void 0 ? void 0 : _a.postMessage({
35424
+ payload: {
35425
+ conversation_id: conversationId,
35426
+ // Picobot asks whether this resolved the ticket, and closes it.
35427
+ ...(ticketExternalId && {
35428
+ external_id: ticketExternalId,
35429
+ source: "ticket_submitted"
35430
+ })
35431
+ },
35432
+ type: "LOAD_CONVERSATION"
35433
+ });
35434
+ } else if (draft) {
35435
+ // Not both: Picobot cancels a conversation load when a prefill
35436
+ // starts a new conversation.
35437
+ (_b = picobotRef.current) === null || _b === void 0 ? void 0 : _b.postMessage({
35438
+ payload: {
35439
+ source: "email_form",
35440
+ text: draft
35441
+ },
35442
+ type: "PREFILL_COMPOSER"
35443
+ });
35444
+ }
35445
+ clearPending();
35446
+ }, [pending, isPicobotReady, clearPending]);
35447
+ const [iframeErrored, setIframeErrored] = React__default.useState(false);
35448
+ const [mountKey, setMountKey] = React__default.useState(0);
35449
+ const handleRetry = React__default.useCallback(() => {
35450
+ setIframeErrored(false);
35451
+ setIsPicobotReady(false);
35452
+ setMountKey(key => key + 1);
35453
+ }, []);
35454
+ const handleMessage = React__default.useCallback(data => {
35455
+ var _a;
35456
+ const message = data;
35457
+ switch (message === null || message === void 0 ? void 0 : message.type) {
35458
+ case "CLOSE_DRAWER":
35459
+ return closeDrawer();
35460
+ case "READY":
35461
+ return setIsPicobotReady(true);
35462
+ case "PICOBOT_SUPPORT_HANDOFF":
35463
+ return openTool("helpdesk", ((_a = message.payload) === null || _a === void 0 ? void 0 : _a.method) === "zoom" ? "phone" : emailPathWithContext(message.payload));
35464
+ case "IFRAME_LOAD_ERROR":
35465
+ console.warn("topbar: Picobot iframe failed to load");
35466
+ return setIframeErrored(true);
35467
+ }
35468
+ }, [closeDrawer, openTool]);
35469
+ if (iframeErrored) {
35470
+ return /*#__PURE__*/React__default.createElement(LoadError, {
35471
+ onContactSupport: () => openTool("helpdesk", "email"),
35472
+ onRetry: handleRetry
35473
+ });
35474
+ }
35475
+ return /*#__PURE__*/React__default.createElement(PicobotIframe, {
35476
+ key: mountKey,
35477
+ ref: picobotRef,
35478
+ currentPath: currentPath,
35479
+ onChangePath: onChangePath,
35480
+ onMessage: handleMessage
35481
+ });
35482
+ }
35483
+ function LoadError({
35484
+ onContactSupport,
35485
+ onRetry
35486
+ }) {
35487
+ return /*#__PURE__*/React__default.createElement(StackView$2, {
35488
+ axis: "vertical",
35489
+ flex: 1,
35490
+ alignment: "center",
35491
+ distribution: "center",
35492
+ padding: 3,
35493
+ spacing: 1
35494
+ }, /*#__PURE__*/React__default.createElement(Heading, {
35495
+ level: 2
35496
+ }, "Picobot is temporarily unavailable"), /*#__PURE__*/React__default.createElement(Text, {
35497
+ size: 5,
35498
+ color: "var(--t-text-color-secondary)"
35499
+ }, "Try again or contact support."), /*#__PURE__*/React__default.createElement(Box, {
35500
+ paddingTop: 2
35501
+ }, /*#__PURE__*/React__default.createElement(Button$1, {
35502
+ onClick: onRetry,
35503
+ iconLeft: "general.refresh",
35504
+ theme: "primary",
35505
+ variant: "fill",
35506
+ title: "Retry"
35507
+ })), /*#__PURE__*/React__default.createElement(Button$1, {
35508
+ onClick: onContactSupport,
35509
+ variant: "naked",
35510
+ theme: "primary",
35511
+ title: "Contact support"
35512
+ }));
35513
+ }
35514
+ const Picobot = {
35515
+ component: PicobotComponent,
35516
+ icons: {
35517
+ staticDotPath: SPARKLE_PATH,
35518
+ staticPath: SPARKLE_PATH
35519
+ },
35520
+ name: "Picobot"
35521
+ };
35522
+
34545
35523
  const {
34546
35524
  LoadingIndicator,
34547
35525
  StackView
@@ -34937,6 +35915,41 @@ const useChatConfig = ({
34937
35915
  };
34938
35916
  };
34939
35917
 
35918
+ // Picobot's API rejects requests without a version header, and apiRequest
35919
+ // sets its own headers, so the version rides in on the fetch.
35920
+ const picobotFetch = (url, opts) => defaultFetch(url, {
35921
+ ...opts,
35922
+ headers: {
35923
+ ...(opts === null || opts === void 0 ? void 0 : opts.headers),
35924
+ "X-PCO-API-Version": "2026-08-11"
35925
+ }
35926
+ });
35927
+ // Routes wait on the gate, so a slow Picobot can't hold them up for long.
35928
+ const GATE_TIMEOUT_MS = 3000;
35929
+ const usePicobotConfig = ({
35930
+ isEnabled
35931
+ }) => {
35932
+ // Undefined until Picobot answers.
35933
+ const [panelEnabled, setPanelEnabled] = useState();
35934
+ useEffect(() => {
35935
+ if (!isEnabled) return undefined;
35936
+ // Whatever settles first wins, so a late answer can't reopen the gate.
35937
+ const settle = enabled => setPanelEnabled(current => current !== null && current !== void 0 ? current : enabled);
35938
+ const timeout = setTimeout(() => settle(false), GATE_TIMEOUT_MS);
35939
+ apiRequest(picobotFetch, `${window.location.origin}/~api/picobot/v2/me`).then(({
35940
+ json
35941
+ }) => {
35942
+ var _a, _b;
35943
+ return settle(((_b = (_a = json === null || json === void 0 ? void 0 : json.data) === null || _a === void 0 ? void 0 : _a.attributes) === null || _b === void 0 ? void 0 : _b.picobot_panel_enabled) === true);
35944
+ }).catch(() => settle(false)).finally(() => clearTimeout(timeout));
35945
+ return () => clearTimeout(timeout);
35946
+ }, [isEnabled]);
35947
+ return {
35948
+ displayPicobotIcon: panelEnabled === true,
35949
+ isLoading: isEnabled && panelEnabled === undefined
35950
+ };
35951
+ };
35952
+
34940
35953
  const Profile = {
34941
35954
  component: UserProfile,
34942
35955
  icons: {
@@ -34947,10 +35960,16 @@ const Profile = {
34947
35960
  };
34948
35961
 
34949
35962
  const DURATION = 100;
35963
+ // Exempt paths match regardless of query, so `email?conversationId=…` is
35964
+ // exempt along with `email`.
34950
35965
  function isToolPathExemptFromStorage(toolName, path) {
34951
35966
  var _a, _b;
34952
35967
  const tool = toolsRouteMap[toolName];
34953
- return (_b = (_a = tool === null || tool === void 0 ? void 0 : tool.storageExemptPaths) === null || _a === void 0 ? void 0 : _a.includes(path)) !== null && _b !== void 0 ? _b : false;
35968
+ const [pathWithoutQuery] = path.split("?");
35969
+ return (_b = (_a = tool === null || tool === void 0 ? void 0 : tool.storageExemptPaths) === null || _a === void 0 ? void 0 : _a.includes(pathWithoutQuery)) !== null && _b !== void 0 ? _b : false;
35970
+ }
35971
+ function isConversationPath(path) {
35972
+ return Boolean(path === null || path === void 0 ? void 0 : path.startsWith("conversations/"));
34954
35973
  }
34955
35974
  function scrollToolDrawerToTop(toolName) {
34956
35975
  var _a;
@@ -34963,6 +35982,7 @@ const TOOLS_MAP = {
34963
35982
  Chat,
34964
35983
  Helpdesk,
34965
35984
  Notifications,
35985
+ Picobot,
34966
35986
  Profile,
34967
35987
  Tasks
34968
35988
  };
@@ -34990,7 +36010,8 @@ function Toolbar({
34990
36010
  });
34991
36011
  const {
34992
36012
  currentContent,
34993
- open
36013
+ open,
36014
+ pending
34994
36015
  } = state;
34995
36016
  const mounted = !!currentContent;
34996
36017
  const storeData = useStore();
@@ -35003,6 +36024,15 @@ function Toolbar({
35003
36024
  ...(env === "development" ? _storeDataOverride !== null && _storeDataOverride !== void 0 ? _storeDataOverride : {} : {})
35004
36025
  };
35005
36026
  const updateStore = useUpdateStore();
36027
+ const {
36028
+ displayPicobotIcon,
36029
+ isLoading: isPicobotGateLoading
36030
+ } = usePicobotConfig({
36031
+ isEnabled: Boolean(userId)
36032
+ });
36033
+ // Picobot is a kill-switched tool: until its gate says the panel is
36034
+ // enabled, its routes are held, and if it never does, they're ignored.
36035
+ const routableTool = useCallback(name => name === Picobot.name.toLowerCase() && !displayPicobotIcon ? undefined : toolsRouteMap[name], [displayPicobotIcon]);
35006
36036
  React__default.useEffect(() => {
35007
36037
  if (open) {
35008
36038
  document.body.style.overflow = "hidden";
@@ -35090,7 +36120,9 @@ function Toolbar({
35090
36120
  });
35091
36121
  }, [clearRoute]);
35092
36122
  React__default.useEffect(() => {
35093
- if (!toolbarFragmentRoute) return;
36123
+ if (!toolbarFragmentRoute || !routableTool(toolbarFragmentRoute.tool)) {
36124
+ return;
36125
+ }
35094
36126
  const isExempt = isToolPathExemptFromStorage(toolbarFragmentRoute.tool, toolbarFragmentRoute.path);
35095
36127
  if (isExempt) return;
35096
36128
  const nextState = {
@@ -35100,12 +36132,32 @@ function Toolbar({
35100
36132
  if (!shallowCompare(nextState, toolStateCookie)) {
35101
36133
  setToolStateCookie(nextState);
35102
36134
  }
35103
- }, [toolbarFragmentRoute, toolStateCookie]);
36135
+ }, [toolbarFragmentRoute, toolStateCookie, routableTool]);
35104
36136
  React__default.useEffect(() => {
35105
- if ((toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && (currentContent !== toolsRouteMap[toolbarFragmentRoute.tool] || !open)) {
35106
- setToolAndOpenDrawer(toolsRouteMap[toolbarFragmentRoute.tool]);
36137
+ // Helpdesk conversations may belong to Picobot, so they wait for its gate
36138
+ // rather than flashing open in Helpdesk first.
36139
+ const mayRedirectToPicobot = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) === Helpdesk.name.toLowerCase() && isConversationPath(toolbarFragmentRoute.path) && (isPicobotGateLoading || displayPicobotIcon);
36140
+ const tool = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && !mayRedirectToPicobot && routableTool(toolbarFragmentRoute.tool);
36141
+ if (tool && (currentContent !== tool || !open)) {
36142
+ setToolAndOpenDrawer(tool);
35107
36143
  }
35108
- }, [setToolAndOpenDrawer, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool, currentContent, open]);
36144
+ }, [setToolAndOpenDrawer, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path, currentContent, open, routableTool, isPicobotGateLoading, displayPicobotIcon]);
36145
+ const openTool = useCallback((name, path, pendingValue) => {
36146
+ const tool = routableTool(name.toLowerCase());
36147
+ if (!tool) return;
36148
+ dispatch({
36149
+ payload: pendingValue === undefined ? null : {
36150
+ tool: tool.name,
36151
+ value: pendingValue
36152
+ },
36153
+ type: ActionType.setPending
36154
+ });
36155
+ onChangeToolbarFragmentRoute(tool.name.toLowerCase(), path);
36156
+ scrollToolDrawerToTop(tool.name);
36157
+ }, [onChangeToolbarFragmentRoute, routableTool]);
36158
+ const clearPending = useCallback(() => dispatch({
36159
+ type: ActionType.clearPending
36160
+ }), []);
35109
36161
  const toggleDrawer = content => {
35110
36162
  if (!open) {
35111
36163
  return openDrawer(content);
@@ -35122,12 +36174,52 @@ function Toolbar({
35122
36174
  joltPromise: storeData === null || storeData === void 0 ? void 0 : storeData.joltPromise,
35123
36175
  userId
35124
36176
  });
35125
- const TOOLS = [Notifications, Tasks, displayChatIcon && Chat].filter(Boolean);
36177
+ // Picobot conversations moved from Helpdesk to the Picobot panel, so
36178
+ // send old links and saved paths there once the panel is enabled.
36179
+ React__default.useEffect(() => {
36180
+ if (!displayPicobotIcon) return;
36181
+ if ((toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) === Helpdesk.name.toLowerCase() && isConversationPath(toolbarFragmentRoute.path)) {
36182
+ onChangeToolbarFragmentRoute(Picobot.name.toLowerCase(), toolbarFragmentRoute.path);
36183
+ }
36184
+ }, [displayPicobotIcon, toolbarFragmentRoute, onChangeToolbarFragmentRoute]);
36185
+ React__default.useEffect(() => {
36186
+ if (!displayPicobotIcon) return;
36187
+ const {
36188
+ helpdesk,
36189
+ picobot
36190
+ } = toolStateCookie;
36191
+ if (!isConversationPath(helpdesk)) return;
36192
+ setToolStateCookie({
36193
+ ...toolStateCookie,
36194
+ helpdesk: "",
36195
+ picobot: picobot || helpdesk
36196
+ });
36197
+ }, [displayPicobotIcon, toolStateCookie]);
36198
+ const openPicobot = useCallback(options => {
36199
+ var _a;
36200
+ // Moving an open Picobot off its path reloads or navigates the iframe,
36201
+ // which would race the options it's being handed.
36202
+ const path = currentContent === Picobot ? (_a = toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path) !== null && _a !== void 0 ? _a : "" : "";
36203
+ openTool(Picobot.name, path, options);
36204
+ }, [openTool, currentContent, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path]);
36205
+ React__default.useEffect(() => {
36206
+ if (!displayPicobotIcon) return undefined;
36207
+ window.topbar = window.topbar || {};
36208
+ window.topbar.openPicobot = openPicobot;
36209
+ return () => {
36210
+ delete window.topbar.openPicobot;
36211
+ };
36212
+ }, [displayPicobotIcon, openPicobot]);
36213
+ const TOOLS = [Notifications, Tasks, displayChatIcon && Chat, displayPicobotIcon && Picobot, isPcoTestHost() && DeveloperTools].filter(Boolean);
35126
36214
  const handleToolOnClick = tool => {
35127
36215
  toggleDrawer(tool);
35128
36216
  scrollToolDrawerToTop(tool.name);
35129
36217
  };
35130
36218
  const toolSpecificProps = {
36219
+ Helpdesk: {
36220
+ isPicobotLoading: isPicobotGateLoading,
36221
+ openPicobot: displayPicobotIcon ? openPicobot : undefined
36222
+ },
35131
36223
  Profile: {
35132
36224
  appName,
35133
36225
  closeDrawer,
@@ -35150,6 +36242,7 @@ function Toolbar({
35150
36242
  const routeIsForCurrentTool = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && toolsRouteMap[toolbarFragmentRoute.tool] === currentContent;
35151
36243
  const currentToolProps = {
35152
36244
  appName,
36245
+ clearPending,
35153
36246
  closeDrawer,
35154
36247
  currentPath: routeIsForCurrentTool ? toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path : null,
35155
36248
  env,
@@ -35158,6 +36251,8 @@ function Toolbar({
35158
36251
  joltPromise,
35159
36252
  onChangePath,
35160
36253
  openDrawer: () => currentContent && openDrawer(currentContent),
36254
+ openTool,
36255
+ pending: (pending === null || pending === void 0 ? void 0 : pending.tool) === (currentContent === null || currentContent === void 0 ? void 0 : currentContent.name) ? pending === null || pending === void 0 ? void 0 : pending.value : undefined,
35161
36256
  userId,
35162
36257
  ...(currentContent ? toolSpecificProps[currentContent.name] : {})
35163
36258
  };
@@ -35168,38 +36263,37 @@ function Toolbar({
35168
36263
  return /*#__PURE__*/React__default.createElement(ThemeProvider, {
35169
36264
  theme: topbarTheme
35170
36265
  }, /*#__PURE__*/React__default.createElement(UiExtensionDataContext, null, /*#__PURE__*/React__default.createElement("div", {
35171
- className: styles$1["toolbar"],
36266
+ className: styles$2["toolbar"],
35172
36267
  "data-pendo": "toolbar-profile-support-wrapper"
35173
36268
  }, /*#__PURE__*/React__default.createElement(ToolbarButton, {
35174
36269
  "aria-label": `Open ${userName}’s profile drawer`,
35175
36270
  onClick: () => toggleDrawer(Profile),
35176
36271
  "data-pendo": "toolbar-profile",
35177
- className: styles$1["toolbar-button--avatar"]
36272
+ className: styles$2["toolbar-button--avatar"]
35178
36273
  }, /*#__PURE__*/React__default.createElement(Avatar, {
35179
36274
  env: env,
35180
36275
  url: userAvatarPath
35181
36276
  })), /*#__PURE__*/React__default.createElement(ToolbarButton, {
35182
- className: styles$1["toolbar-button--home"],
36277
+ className: styles$2["toolbar-button--home"],
35183
36278
  onClick: () => closeDrawer()
35184
- }, /*#__PURE__*/React__default.createElement(Icon, {
36279
+ }, /*#__PURE__*/React__default.createElement(Icon$1, {
35185
36280
  name: "toolbar.planningCenter",
35186
36281
  size: "24px"
35187
36282
  })), [HelpDeskTool, ...TOOLS].map(tool => {
36283
+ var _a;
35188
36284
  const current = open && currentContent === tool;
35189
36285
  const indicator = indicatorState[tool.name];
35190
36286
  return /*#__PURE__*/React__default.createElement(ToolbarButton, {
35191
36287
  current: current,
35192
36288
  onClick: () => handleToolOnClick(tool),
35193
36289
  id: getDataPendoAttribute(tool.name),
35194
- "aria-label": `Open ${tool.name} drawer`,
36290
+ "aria-label": `Open ${(_a = tool.label) !== null && _a !== void 0 ? _a : tool.name} drawer`,
35195
36291
  "data-pendo": getDataPendoAttribute(tool.name),
35196
36292
  key: tool.name
35197
- }, !indicator && (/*#__PURE__*/React__default.createElement(ToolbarButtonIcon, {
35198
- path: tool.icons.staticPath
35199
- })), indicator && (/*#__PURE__*/React__default.createElement(ToolbarButtonIcon, {
35200
- path: tool.icons.staticDotPath,
35201
- dot: true
35202
- })));
36293
+ }, /*#__PURE__*/React__default.createElement(ToolbarToolIcon, {
36294
+ icons: tool.icons,
36295
+ dot: !!indicator
36296
+ }));
35203
36297
  })), /*#__PURE__*/React__default.createElement(DrawerWrap, {
35204
36298
  isVisible: open
35205
36299
  }, mounted && (/*#__PURE__*/React__default.createElement(Drawer, {
@@ -35217,12 +36311,25 @@ const ToolbarButton = ({
35217
36311
  }) => {
35218
36312
  return /*#__PURE__*/React__default.createElement("button", {
35219
36313
  type: "button",
35220
- className: classnames(styles$1["toolbar-button"], className, current && styles$1["is-current"]),
36314
+ className: classnames(styles$2["toolbar-button"], className, current && styles$2["is-current"]),
35221
36315
  onClick: onClick,
35222
36316
  "data-toolbar-link": "true",
35223
36317
  ...restProps
35224
36318
  }, children);
35225
36319
  };
36320
+ function ToolbarToolIcon({
36321
+ dot,
36322
+ icons
36323
+ }) {
36324
+ if ("component" in icons) {
36325
+ return /*#__PURE__*/React__default.createElement(icons.component, null);
36326
+ } else {
36327
+ return /*#__PURE__*/React__default.createElement(ToolbarButtonIcon, {
36328
+ path: dot ? icons.staticDotPath : icons.staticPath,
36329
+ dot: dot
36330
+ });
36331
+ }
36332
+ }
35226
36333
  function ToolbarButtonIcon({
35227
36334
  dot,
35228
36335
  path