@planningcenter/topbar 12.3.0 → 12.4.0-qa-958.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { Link, Button, Popover, PopoverTrigger, PopoverContent, Modal } from '@planningcenter/tapestry';
1
+ import { Link, Button, Popover, PopoverTrigger, PopoverContent, Flex, Select, Modal } from '@planningcenter/tapestry';
2
2
  import * as React from 'react';
3
3
  import React__default, { useState, useEffect, useMemo, useLayoutEffect, useContext, createContext, useCallback, useRef } from 'react';
4
4
  import pcoUrl$1, { pcoUrl as pcoUrl$2, pcoApiUrl } from '@planningcenter/url';
@@ -6,18 +6,19 @@ 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 * as toolbar from '@planningcenter/icons/paths/toolbar';
10
+ import * as logomarks from '@planningcenter/icons/paths/logomark';
11
+ 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
12
  import { components, UiExtensionFirstPartyComponent, UiExtensionDataContext } from '@planningcenter/add-ons';
11
13
  import * as churchCenter from '@planningcenter/icons/paths/church-center';
12
14
  import * as people from '@planningcenter/icons/paths/people';
13
- import * as toolbar from '@planningcenter/icons/paths/toolbar';
14
15
  import { InlineWidget } from 'react-calendly';
15
16
  import algoliasearch from 'algoliasearch';
16
17
  import { useSearchBox, useHits, InstantSearch, Configure } from 'react-instantsearch-core';
17
18
  import { TransitionGroup, Transition } from 'react-transition-group';
18
19
  import datetimeFmt from '@planningcenter/datetime-fmt';
19
20
 
20
- var styles$l = {"accessibility-toolbar":"accessibility-toolbar-jgjUG"};
21
+ var styles$s = {"accessibility-toolbar":"accessibility-toolbar-jgjUG"};
21
22
 
22
23
  const AccessibilityToolbar = ({
23
24
  children
@@ -29,7 +30,7 @@ const AccessibilityToolbar = ({
29
30
  (_a = document.getElementById("main-content")) === null || _a === void 0 ? void 0 : _a.focus();
30
31
  };
31
32
  return /*#__PURE__*/React__default.createElement("div", {
32
- className: styles$l["accessibility-toolbar"]
33
+ className: styles$s["accessibility-toolbar"]
33
34
  }, /*#__PURE__*/React__default.createElement(Link, {
34
35
  href: "#main-content",
35
36
  kind: "primary",
@@ -156,12 +157,14 @@ function inferRailsCsrfToken() {
156
157
  }
157
158
  function apiRequest(fetch, url, {
158
159
  method = "GET",
159
- data = null
160
+ data = null,
161
+ headers = {}
160
162
  } = {}) {
161
163
  const options = {
162
164
  headers: {
163
165
  Accept: "application/json",
164
- "Content-Type": "application/json"
166
+ "Content-Type": "application/json",
167
+ ...headers
165
168
  },
166
169
  method,
167
170
  redirect: "follow"
@@ -346,11 +349,11 @@ const X = ({
346
349
  fill: fill
347
350
  })));
348
351
 
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"};
352
+ var styles$r = {"announcement":"announcement-pI5tM","announcement--platform":"announcement--platform-EcUn2","announcement--promo":"announcement--promo-FarUk","announcement-icon":"announcement-icon-Ov39-","announcement-content":"announcement-content-q2ad6","dismiss":"dismiss-wUAoU","dismiss--promo":"dismiss--promo-7zvO4"};
350
353
 
351
354
  function AnnouncementIcon() {
352
355
  return /*#__PURE__*/React__default.createElement("svg", {
353
- className: styles$k["announcement-icon"],
356
+ className: styles$r["announcement-icon"],
354
357
  width: "16",
355
358
  height: "16",
356
359
  viewBox: "0 0 16 16",
@@ -475,7 +478,7 @@ let StyledAnnouncement$1 = class StyledAnnouncement extends React__default.Compo
475
478
  } = this.props;
476
479
  const isPromo = variant === "promo";
477
480
  return /*#__PURE__*/React__default.createElement("div", {
478
- className: classnames(styles$k["announcement"], styles$k[`announcement--${variant !== null && variant !== void 0 ? variant : "platform"}`]),
481
+ className: classnames(styles$r["announcement"], styles$r[`announcement--${variant !== null && variant !== void 0 ? variant : "platform"}`]),
479
482
  "data-announcement-variant": variant !== null && variant !== void 0 ? variant : "platform",
480
483
  style: isPromo ? {
481
484
  backgroundImage: promoBackgroundUrl,
@@ -484,7 +487,7 @@ let StyledAnnouncement$1 = class StyledAnnouncement extends React__default.Compo
484
487
  backgroundSize: "cover"
485
488
  } : undefined
486
489
  }, !isPromo && /*#__PURE__*/React__default.createElement(AnnouncementIcon, null), /*#__PURE__*/React__default.createElement("div", {
487
- className: styles$k["announcement-content"],
490
+ className: styles$r["announcement-content"],
488
491
  style: style,
489
492
  ...platformProps
490
493
  }));
@@ -533,7 +536,7 @@ function StyledDismissButton$1({
533
536
  const isPromo = variant === "promo";
534
537
  return /*#__PURE__*/React__default.createElement("button", {
535
538
  type: "button",
536
- className: classnames(styles$k.dismiss, isPromo && styles$k["dismiss--promo"]),
539
+ className: classnames(styles$r.dismiss, isPromo && styles$r["dismiss--promo"]),
537
540
  ...props
538
541
  }, /*#__PURE__*/React__default.createElement(X, {
539
542
  fill: isPromo ? "currentColor" : "white",
@@ -613,9 +616,9 @@ class StyledAnnouncement extends React__default.Component {
613
616
  ...platformProps
614
617
  } = this.props;
615
618
  return /*#__PURE__*/React__default.createElement("div", {
616
- className: styles$k["announcement"]
619
+ className: styles$r["announcement"]
617
620
  }, /*#__PURE__*/React__default.createElement(AnnouncementIcon, null), /*#__PURE__*/React__default.createElement("div", {
618
- className: styles$k["announcement-content"],
621
+ className: styles$r["announcement-content"],
619
622
  style: style,
620
623
  ...platformProps
621
624
  }));
@@ -626,7 +629,7 @@ function StyledDismissButton({
626
629
  }) {
627
630
  return /*#__PURE__*/React__default.createElement("button", {
628
631
  type: "button",
629
- className: styles$k.dismiss,
632
+ className: styles$r.dismiss,
630
633
  ...props
631
634
  }, /*#__PURE__*/React__default.createElement(X, {
632
635
  fill: "white",
@@ -781,7 +784,7 @@ function useMediaQuery(query) {
781
784
  return matches;
782
785
  }
783
786
 
784
- const capitalize = s => s.charAt(0).toUpperCase() + s.slice(1);
787
+ const capitalize$1 = s => s.charAt(0).toUpperCase() + s.slice(1);
785
788
  const RESOLVED_COLOR_MODE_ATTRIBUTE = "data-resolved-color-mode";
786
789
  function resolveColorMode(colorMode) {
787
790
  if (colorMode === "system") {
@@ -1748,7 +1751,7 @@ function MonoAppIcon({
1748
1751
  })) : (/*#__PURE__*/React__default.createElement("div", null, "appName not supported")));
1749
1752
  }
1750
1753
 
1751
- var styles$j = {"icon-slot":"icon-slot-m3ZIk","name":"name-UNqbW"};
1754
+ var styles$q = {"icon-slot":"icon-slot-m3ZIk","name":"name-UNqbW"};
1752
1755
 
1753
1756
  const AppRow = ({
1754
1757
  appName,
@@ -1757,24 +1760,24 @@ const AppRow = ({
1757
1760
  }) => {
1758
1761
  if (appName === "Accounts") {
1759
1762
  return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("span", {
1760
- className: styles$j["icon-slot"]
1763
+ className: styles$q["icon-slot"]
1761
1764
  }, /*#__PURE__*/React__default.createElement(MonoAppIcon, {
1762
1765
  appName: "Accounts",
1763
1766
  colors: colors,
1764
1767
  size: iconSize
1765
1768
  })), /*#__PURE__*/React__default.createElement("span", {
1766
- className: styles$j.name
1769
+ className: styles$q.name
1767
1770
  }, "Account settings"));
1768
1771
  }
1769
1772
  return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(ColorAppIcon, {
1770
1773
  appName: appName.replace(/[\s-]/, ""),
1771
1774
  size: iconSize
1772
1775
  }), /*#__PURE__*/React__default.createElement("span", {
1773
- className: styles$j.name
1776
+ className: styles$q.name
1774
1777
  }, appName));
1775
1778
  };
1776
1779
 
1777
- var styles$i = {"button":"button-XMbeQ","label":"label-884of","chevron":"chevron-RRg9k"};
1780
+ var styles$p = {"button":"button-XMbeQ","label":"label-884of","chevron":"chevron-RRg9k"};
1778
1781
 
1779
1782
  const AppsButton = /*#__PURE__*/React__default.forwardRef(({
1780
1783
  appName,
@@ -1786,10 +1789,10 @@ const AppsButton = /*#__PURE__*/React__default.forwardRef(({
1786
1789
  ...restProps,
1787
1790
  ref: ref,
1788
1791
  "aria-label": `App switcher: ${appName}`,
1789
- className: styles$i.button,
1792
+ className: styles$p.button,
1790
1793
  kind: "neutral-inline",
1791
1794
  label: /*#__PURE__*/React__default.createElement("span", {
1792
- className: styles$i.label
1795
+ className: styles$p.label
1793
1796
  }, isAccounts ? "Account settings" : appName),
1794
1797
  prefix: isAccounts ? (/*#__PURE__*/React__default.createElement(MonoAppIcon, {
1795
1798
  appName: "Accounts",
@@ -1800,13 +1803,13 @@ const AppsButton = /*#__PURE__*/React__default.forwardRef(({
1800
1803
  size: 22
1801
1804
  })),
1802
1805
  suffix: /*#__PURE__*/React__default.createElement(DisclosureChevronIcon, {
1803
- className: `${styles$i.chevron} symbol`
1806
+ className: `${styles$p.chevron} symbol`
1804
1807
  })
1805
1808
  });
1806
1809
  });
1807
1810
  AppsButton.displayName = "AppsButton";
1808
1811
 
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"};
1812
+ var styles$o = {"nav":"nav-y-6ML","popover-content":"popover-content-jOAAY","list":"list-jUxvp","link":"link-LMp2h","current":"current-4U4VL","accounts-item":"accounts-item-erJW0","accounts-link":"accounts-link-4jwPW"};
1810
1813
 
1811
1814
  const AppsMenu = ({
1812
1815
  appName,
@@ -1827,8 +1830,8 @@ const AppsMenu = ({
1827
1830
  }, /*#__PURE__*/React__default.createElement("a", {
1828
1831
  "aria-current": isCurrent ? "page" : undefined,
1829
1832
  "aria-label": name,
1830
- className: classnames(styles$h.link, {
1831
- [styles$h.current]: isCurrent
1833
+ className: classnames(styles$o.link, {
1834
+ [styles$o.current]: isCurrent
1832
1835
  }),
1833
1836
  "data-pendo": `app-switcher--change-app--${name}`,
1834
1837
  "data-turbolinks": false,
@@ -1844,22 +1847,22 @@ const AppsMenu = ({
1844
1847
  colors: colors
1845
1848
  })), /*#__PURE__*/React__default.createElement(PopoverContent, {
1846
1849
  "aria-label": "Switch app",
1847
- className: styles$h["popover-content"],
1850
+ className: styles$o["popover-content"],
1848
1851
  placement: "bottom start"
1849
1852
  }, /*#__PURE__*/React__default.createElement("nav", {
1850
- className: styles$h.nav,
1853
+ className: styles$o.nav,
1851
1854
  "data-pendo": "app-switcher"
1852
1855
  }, /*#__PURE__*/React__default.createElement("ul", {
1853
- className: styles$h.list,
1856
+ className: styles$o.list,
1854
1857
  role: "list"
1855
1858
  }, homeProduct && renderProductLink(homeProduct.name), restProducts.map(({
1856
1859
  name
1857
1860
  }) => renderProductLink(name)), accountsProduct && (/*#__PURE__*/React__default.createElement("li", {
1858
- className: styles$h["accounts-item"]
1861
+ className: styles$o["accounts-item"]
1859
1862
  }, /*#__PURE__*/React__default.createElement("a", {
1860
1863
  "aria-current": appName === accountsProduct.name ? "page" : undefined,
1861
1864
  "aria-label": "Account settings",
1862
- className: classnames(styles$h.link, styles$h["accounts-link"]),
1865
+ className: classnames(styles$o.link, styles$o["accounts-link"]),
1863
1866
  "data-pendo": "app-switcher--change-app--Accounts",
1864
1867
  "data-turbolinks": false,
1865
1868
  href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(accountsProduct.name)
@@ -2067,7 +2070,7 @@ function MonoAppName({
2067
2070
  })) : (/*#__PURE__*/React__default.createElement("div", null, "appName not supported"));
2068
2071
  }
2069
2072
 
2070
- var styles$g = {"unbutton":"unbutton-lEbTR"};
2073
+ var styles$n = {"unbutton":"unbutton-lEbTR"};
2071
2074
 
2072
2075
  function Unbutton({
2073
2076
  className,
@@ -2076,7 +2079,7 @@ function Unbutton({
2076
2079
  ...nativeProps
2077
2080
  }) {
2078
2081
  return /*#__PURE__*/React__default.createElement("button", {
2079
- className: classnames(styles$g.unbutton, className),
2082
+ className: classnames(styles$n.unbutton, className),
2080
2083
  style: {
2081
2084
  ...style
2082
2085
  },
@@ -2085,7 +2088,7 @@ function Unbutton({
2085
2088
  });
2086
2089
  }
2087
2090
 
2088
- var styles$f = {"button":"button-wsipc","button-accounts":"button-accounts-zQmBM","chevron":"chevron-obyBF","is-expanded":"is-expanded-PoCQ9"};
2091
+ var styles$m = {"button":"button-wsipc","button-accounts":"button-accounts-zQmBM","chevron":"chevron-obyBF","is-expanded":"is-expanded-PoCQ9"};
2089
2092
 
2090
2093
  const LegacyAppsButton = ({
2091
2094
  appName,
@@ -2100,8 +2103,8 @@ const LegacyAppsButton = ({
2100
2103
  "aria-haspopup": "true",
2101
2104
  "aria-label": `App switcher: ${appName}`,
2102
2105
  onClick: onClick,
2103
- className: classnames(styles$f.button, {
2104
- [styles$f["is-expanded"]]: expanded
2106
+ className: classnames(styles$m.button, {
2107
+ [styles$m["is-expanded"]]: expanded
2105
2108
  }),
2106
2109
  type: "button",
2107
2110
  style: style,
@@ -2110,7 +2113,7 @@ const LegacyAppsButton = ({
2110
2113
  appName: "Accounts",
2111
2114
  colors: colors
2112
2115
  }), /*#__PURE__*/React__default.createElement("div", {
2113
- className: styles$f["button-accounts"]
2116
+ className: styles$m["button-accounts"]
2114
2117
  }, "Account settings"))) : (/*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(ColorAppIcon, {
2115
2118
  appName: appName.replace(/[\s-]/, "")
2116
2119
  }), /*#__PURE__*/React__default.createElement(MonoAppName, {
@@ -2118,7 +2121,7 @@ const LegacyAppsButton = ({
2118
2121
  key: "mono-app-name",
2119
2122
  size: 24
2120
2123
  }))), /*#__PURE__*/React__default.createElement(DisclosureChevronIcon, {
2121
- className: styles$f.chevron,
2124
+ className: styles$m.chevron,
2122
2125
  key: "disclosure-chevron-icon"
2123
2126
  }));
2124
2127
  };
@@ -2251,7 +2254,7 @@ class Outsider extends React__default.Component {
2251
2254
  }
2252
2255
  }
2253
2256
 
2254
- var styles$e = {"popup-root":"popup-root-SeL5u","popup":"popup-nQXYW","is-visible":"is-visible-3DAUP"};
2257
+ var styles$l = {"popup-root":"popup-root-SeL5u","popup":"popup-nQXYW","is-visible":"is-visible-3DAUP"};
2255
2258
 
2256
2259
  const Popup = ({
2257
2260
  className,
@@ -2259,8 +2262,8 @@ const Popup = ({
2259
2262
  isVisible,
2260
2263
  ...nativeProps
2261
2264
  }) => (/*#__PURE__*/React__default.createElement(C, {
2262
- className: classnames(styles$e.popup, {
2263
- [styles$e["is-visible"]]: isVisible
2265
+ className: classnames(styles$l.popup, {
2266
+ [styles$l["is-visible"]]: isVisible
2264
2267
  }, className),
2265
2268
  ...nativeProps
2266
2269
  }));
@@ -2268,11 +2271,11 @@ const Popup = ({
2268
2271
  const PopupRoot = ({
2269
2272
  ...nativeProps
2270
2273
  }) => (/*#__PURE__*/React__default.createElement("div", {
2271
- className: styles$e["popup-root"],
2274
+ className: styles$l["popup-root"],
2272
2275
  ...nativeProps
2273
2276
  }));
2274
2277
 
2275
- var styles$d = {"popup":"popup-bVhSa"};
2278
+ var styles$k = {"popup":"popup-bVhSa"};
2276
2279
 
2277
2280
  const LegacyAppsMenu = ({
2278
2281
  appName,
@@ -2310,7 +2313,7 @@ const LegacyAppsMenu = ({
2310
2313
  cleanup: !visible,
2311
2314
  component: Outsider,
2312
2315
  onOutsideClick: toggle,
2313
- className: styles$d.popup,
2316
+ className: styles$k.popup,
2314
2317
  isVisible: visible
2315
2318
  }, /*#__PURE__*/React__default.createElement("menu", {
2316
2319
  ref: menuRef,
@@ -3220,29 +3223,121 @@ const useUpdateStore = () => useCallback(updates => {
3220
3223
  });
3221
3224
  }, []);
3222
3225
 
3223
- const isMac = () => /Mac|iPhone|iPad|iPod/.test(navigator.platform);
3224
- const hasCommandKey = event => isMac() ? event.metaKey : event.ctrlKey;
3225
- const commandKeyLabel = () => isMac() ? "⌘" : "Ctrl+";
3226
-
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
- }
3226
+ const collections = {
3227
+ general,
3228
+ toolbar
3229
+ };
3230
+ const paths = collections;
3231
+ function Icon({
3232
+ name,
3233
+ size = 16,
3234
+ style,
3235
+ theme,
3236
+ title,
3237
+ ...props
3238
+ }) {
3239
+ const [collection, symbol] = name.split(".");
3240
+ return /*#__PURE__*/React__default.createElement("svg", {
3241
+ "aria-hidden": title ? undefined : true,
3242
+ fill: "currentColor",
3243
+ height: size,
3244
+ role: title ? "img" : undefined,
3245
+ style: {
3246
+ color: theme && `var(${theme})`,
3247
+ flexShrink: 0,
3248
+ ...style
3249
+ },
3250
+ viewBox: "0 0 16 16",
3251
+ width: size,
3252
+ ...props
3253
+ }, title && /*#__PURE__*/React__default.createElement("title", null, title), /*#__PURE__*/React__default.createElement("path", {
3254
+ d: paths[collection][symbol]
3255
+ }));
3234
3256
  }
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.
3257
+
3258
+ const SEARCHABLE_PRODUCTS = [{
3259
+ appName: "Calendar",
3260
+ slug: "calendar"
3261
+ }, {
3262
+ appName: "Check-Ins",
3263
+ slug: "check-ins"
3264
+ }, {
3265
+ appName: "Giving",
3266
+ slug: "giving"
3267
+ }, {
3268
+ appName: "Groups",
3269
+ slug: "groups"
3270
+ }, {
3271
+ appName: "People",
3272
+ slug: "people"
3273
+ }, {
3274
+ appName: "Publishing",
3275
+ slug: "publishing"
3276
+ }, {
3277
+ appName: "Registrations",
3278
+ slug: "registrations"
3279
+ }, {
3280
+ appName: "Services",
3281
+ slug: "services"
3282
+ }];
3283
+ const KEYBOARD_SHORTCUTS = {
3284
+ copyLink: {
3285
+ key: "c",
3286
+ label: "Copy link",
3287
+ modifierKey: true
3288
+ },
3289
+ openInNewTab: {
3290
+ label: "Open in new tab"},
3291
+ openSearch: {
3292
+ key: "k",
3293
+ label: "Open search",
3294
+ modifierKey: true
3295
+ },
3296
+ select: {
3297
+ label: "Select",
3298
+ modifierKey: false,
3299
+ next: "ArrowDown",
3300
+ previous: "ArrowUp"
3240
3301
  }
3241
- }
3302
+ };
3303
+
3304
+ const isMac = () => /Mac|iPhone|iPad|iPod/.test(navigator.platform);
3305
+ const hasCommandKey = event => isMac() ? event.metaKey : event.ctrlKey;
3306
+ const matchesShortcut = (shortcut, event) => event.key.toLowerCase() === shortcut.key && !event.altKey && !event.shiftKey && hasCommandKey(event) === shortcut.modifierKey;
3307
+ const isSearchShortcut = event => matchesShortcut(KEYBOARD_SHORTCUTS.openSearch, event);
3308
+ // Products opened their own search with `/` before this one replaced it.
3309
+ const isLegacySearchShortcut = event => event.key === "/" && !event.altKey && !event.ctrlKey && !event.metaKey && !isTypingIn(event.target);
3310
+ const isCopyShortcut = event => matchesShortcut(KEYBOARD_SHORTCUTS.copyLink, event);
3311
+ const isTypingIn = target => target instanceof Element && target.closest('input, select, textarea, [contenteditable]:not([contenteditable="false"])') !== null;
3312
+ const isTypingKey = event => event.key.length === 1 && event.key !== " " && !event.altKey && !event.ctrlKey && !event.metaKey;
3313
+ const verticalStep = key => ({
3314
+ [KEYBOARD_SHORTCUTS.select.next]: 1,
3315
+ [KEYBOARD_SHORTCUTS.select.previous]: -1
3316
+ })[key];
3317
+ const horizontalStep = key => ({
3318
+ ArrowLeft: -1,
3319
+ ArrowRight: 1
3320
+ })[key];
3242
3321
 
3243
- var styles$c = {"visuallyHidden":"visuallyHidden-C28ju"};
3322
+ var styles$j = {"searchButton":"searchButton-fYa5-","mobile":"mobile-ksZyA"};
3244
3323
 
3245
- var styles$b = {"tab":"tab-lQLDx"};
3324
+ function SearchButton({
3325
+ mobile = false,
3326
+ onClick
3327
+ }) {
3328
+ return /*#__PURE__*/React__default.createElement("button", {
3329
+ "aria-keyshortcuts": mobile ? undefined : `${isMac() ? "Meta" : "Control"}+${KEYBOARD_SHORTCUTS.openSearch.key.toUpperCase()} /`,
3330
+ "aria-label": "Search",
3331
+ className: classnames(styles$j.searchButton, {
3332
+ [styles$j.mobile]: mobile
3333
+ }),
3334
+ onClick: onClick,
3335
+ type: "button"
3336
+ }, /*#__PURE__*/React__default.createElement(Icon, {
3337
+ name: "general.search",
3338
+ size: mobile ? 18 : 14
3339
+ }));
3340
+ }
3246
3341
 
3247
3342
  // useId isn't available on React 17, which our peer range still supports.
3248
3343
  let instanceCount$1 = 0;
@@ -3251,173 +3346,33 @@ function useInstanceId(prefix) {
3251
3346
  return id;
3252
3347
  }
3253
3348
 
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
- });
3349
+ // The versions home-mobile pins, so web and mobile read the same response
3350
+ // shapes. Some endpoints only exist from a version on: Registrations' me,
3351
+ // product_search_results, and pcrl_lookups need 2025-05-01.
3352
+ const API_VERSIONS = {
3353
+ calendar: "2022-07-07",
3354
+ "check-ins": "2025-05-28",
3355
+ giving: "2019-10-18",
3356
+ groups: "2023-07-10",
3357
+ home: "2018-08-01",
3358
+ people: "2023-03-21",
3359
+ publishing: "2024-03-25",
3360
+ registrations: "2025-05-01",
3361
+ services: "2018-11-01"
3362
+ };
3363
+ // For a `/~api/{product}/v2/...` URL, the header that pins its product's
3364
+ // version.
3365
+ function apiVersionHeaders(url) {
3366
+ const [,, product] = new URL(url).pathname.split("/");
3367
+ const version = API_VERSIONS[product];
3368
+ if (version) {
3369
+ return {
3370
+ "X-PCO-API-Version": version
3371
+ };
3372
+ } else {
3373
+ return {};
3382
3374
  }
3383
3375
  }
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
3376
 
3422
3377
  const CACHE_TTL_MS = 60000;
3423
3378
  const MAX_ENTRIES$1 = 50;
@@ -3434,7 +3389,9 @@ function cachedRequest(url) {
3434
3389
  }
3435
3390
  }
3436
3391
  function fetchIntoCache(url) {
3437
- const promise = apiRequest(defaultFetch, url).then(({
3392
+ const promise = apiRequest(defaultFetch, url, {
3393
+ headers: apiVersionHeaders(url)
3394
+ }).then(({
3438
3395
  json,
3439
3396
  ok
3440
3397
  }) => {
@@ -3490,44 +3447,52 @@ function useCachedRequest(url) {
3490
3447
  };
3491
3448
  }
3492
3449
  }
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"
3450
+ // One request per URL, for callers that fan out to a varying number of
3451
+ // endpoints. Unlike `useCachedRequest`, a URL that hasn't settled yet has no
3452
+ // earlier data to fall back on.
3453
+ function useCachedRequests(urls) {
3454
+ const [settled, setSettled] = React__default.useState({});
3455
+ // Keyed by value because each settle re-renders the caller, and a caller
3456
+ // that builds a fresh `urls` array on render would otherwise loop forever.
3457
+ const key = urls.join("\n");
3458
+ React__default.useEffect(() => {
3459
+ let current = true;
3460
+ const settle = result => current && setSettled(prev => ({
3461
+ ...prev,
3462
+ [result.url]: result
3463
+ }));
3464
+ for (const url of key.split("\n").filter(Boolean)) {
3465
+ cachedRequest(url).then(data => settle({
3466
+ data,
3467
+ url
3468
+ }), () => settle({
3469
+ failed: true,
3470
+ url
3471
+ }));
3472
+ }
3473
+ return () => {
3474
+ current = false;
3528
3475
  };
3529
- }
3476
+ }, [key]);
3477
+ return urls.map(url => {
3478
+ const result = settled[url];
3479
+ if (!result) {
3480
+ return {
3481
+ status: "loading"
3482
+ };
3483
+ } else if ("data" in result) {
3484
+ return {
3485
+ data: result.data,
3486
+ status: "done"
3487
+ };
3488
+ } else {
3489
+ return {
3490
+ status: "error"
3491
+ };
3492
+ }
3493
+ });
3530
3494
  }
3495
+
3531
3496
  function productSearchUrl(product, query) {
3532
3497
  const params = new URLSearchParams({
3533
3498
  per_page: "100",
@@ -3558,6 +3523,53 @@ function groupByType({
3558
3523
  };
3559
3524
  return [...groups.values()].sort((a, b) => rank(a.type) - rank(b.type));
3560
3525
  }
3526
+
3527
+ function useDebouncedValue(value, delay) {
3528
+ const [debounced, setDebounced] = React__default.useState(value);
3529
+ React__default.useEffect(() => {
3530
+ const timeout = setTimeout(() => setDebounced(value), delay);
3531
+ return () => clearTimeout(timeout);
3532
+ }, [value, delay]);
3533
+ return debounced;
3534
+ }
3535
+
3536
+ const SEARCH_DEBOUNCE_MS = 300;
3537
+ function useProductSearch(product, term) {
3538
+ const query = term.trim();
3539
+ const debouncedQuery = useDebouncedValue(query, SEARCH_DEBOUNCE_MS);
3540
+ const request = useCachedRequest(debouncedQuery ? productSearchUrl(product, debouncedQuery) : null);
3541
+ const response = "data" in request ? request.data : undefined;
3542
+ const settledProduct = useSettledProduct(product, request.status);
3543
+ // While a request loads, the previous response stays on screen, but only
3544
+ // if it was for this product: People results must never show under the
3545
+ // Services tab.
3546
+ const visibleResponse = request.status === "done" || settledProduct === product ? response : undefined;
3547
+ const groups = React__default.useMemo(() => visibleResponse && groupByType(visibleResponse), [visibleResponse]);
3548
+ if (!query) {
3549
+ return {
3550
+ status: "idle"
3551
+ };
3552
+ } else if (query !== debouncedQuery) {
3553
+ return {
3554
+ groups,
3555
+ status: "loading"
3556
+ };
3557
+ } else if (request.status === "done") {
3558
+ return {
3559
+ groups: groups !== null && groups !== void 0 ? groups : [],
3560
+ status: "done"
3561
+ };
3562
+ } else if (request.status === "error") {
3563
+ return {
3564
+ status: "error"
3565
+ };
3566
+ } else {
3567
+ return {
3568
+ groups,
3569
+ status: "loading"
3570
+ };
3571
+ }
3572
+ }
3561
3573
  // The product the most recent finished request was for.
3562
3574
  function useSettledProduct(product, status) {
3563
3575
  const [settledProduct, setSettledProduct] = React__default.useState();
@@ -3566,29 +3578,489 @@ function useSettledProduct(product, status) {
3566
3578
  }, [product, status]);
3567
3579
  return settledProduct;
3568
3580
  }
3569
- function useDebouncedValue(value, delay) {
3570
- const [debounced, setDebounced] = React__default.useState(value);
3571
- React__default.useEffect(() => {
3572
- const timeout = setTimeout(() => setDebounced(value), delay);
3573
- return () => clearTimeout(timeout);
3574
- }, [value, delay]);
3575
- return debounced;
3581
+
3582
+ function parseValue(element) {
3583
+ const datatype = element.getAttribute("datatype");
3584
+ switch (datatype) {
3585
+ case "boolean":
3586
+ return element.content === "true" || element.content === "1";
3587
+ default:
3588
+ return element.content;
3589
+ }
3590
+ }
3591
+ function getAllPcoMeta() {
3592
+ const elements = document.head.querySelectorAll(`meta[property^="pco:"]`);
3593
+ const meta = {};
3594
+ elements.forEach(e => {
3595
+ const property = e.getAttribute("property");
3596
+ meta[property.substring("pco:".length)] = parseValue(e);
3597
+ });
3598
+ return meta;
3599
+ }
3600
+ // usePcoMeta only reads each tag's content. Recently Visited also needs the
3601
+ // pco:recently_visitable tag's `metadata` attribute, which can change while
3602
+ // the content stays the same, so it is tracked in its own store.
3603
+ function getRecentlyVisitableMetadata() {
3604
+ var _a, _b;
3605
+ return (_b = (_a = document.head.querySelector(`meta[property="pco:recently_visitable"]`)) === null || _a === void 0 ? void 0 : _a.getAttribute("metadata")) !== null && _b !== void 0 ? _b : null;
3606
+ }
3607
+ const store = createStore(getAllPcoMeta());
3608
+ const recentlyVisitableMetadataStore = createStore({
3609
+ metadata: getRecentlyVisitableMetadata()
3610
+ });
3611
+ function sync() {
3612
+ const nextState = getAllPcoMeta();
3613
+ if (!shallowCompare(store.getState(), nextState)) store.setState(nextState);
3614
+ const metadata = getRecentlyVisitableMetadata();
3615
+ if (recentlyVisitableMetadataStore.getState().metadata !== metadata) {
3616
+ recentlyVisitableMetadataStore.setState({
3617
+ metadata
3618
+ });
3619
+ }
3620
+ }
3621
+ const observer = new MutationObserver(sync);
3622
+ let hasTriggeredObserve = false;
3623
+ function observe() {
3624
+ if (hasTriggeredObserve) return;
3625
+ hasTriggeredObserve = true;
3626
+ // The module-load snapshot above can predate meta tags that appear later
3627
+ // in <head> (e.g. pco:resource, yielded right before </head>) if this
3628
+ // module's own script tag isn't deferred and runs mid-parse. Those tags
3629
+ // never mutate afterward, so the observer alone would never catch them.
3630
+ // Re-read once now, since observe() is only called once a consumer is
3631
+ // actually mounting, which is always after the document has fully parsed.
3632
+ sync();
3633
+ observer.observe(document.head, {
3634
+ attributes: true,
3635
+ childList: true,
3636
+ subtree: true
3637
+ });
3638
+ }
3639
+ function usePcoMeta() {
3640
+ observe();
3641
+ return useSyncExternalStore(store.subscribe, () => store.getState());
3642
+ }
3643
+ function useRecentlyVisitableMetadata() {
3644
+ observe();
3645
+ return useSyncExternalStore(recentlyVisitableMetadataStore.subscribe, () => recentlyVisitableMetadataStore.getState().metadata);
3576
3646
  }
3577
3647
 
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
3648
+ // Entries match the ones home-mobile stores, so one list can sync across web
3649
+ // and mobile.
3650
+ const ENTRY_VERSION = 3;
3651
+ const MAX_ENTRIES = 50;
3652
+ const MAX_TEXT_LENGTH = 200;
3653
+ const PRODUCTS$1 = ["accounts", "calendar", "check-ins", "giving", "groups", "home", "login", "people", "publishing", "registrations", "services"];
3654
+ // Login has no pages worth returning to.
3655
+ const APPS = PRODUCTS$1.filter(product => product !== "login");
3656
+ function isId(value) {
3657
+ return value !== "" && Array.from(value).every(character => character >= "0" && character <= "9");
3658
+ }
3659
+ // A PCRL looks like `<product>/v2/<resource_type>/<id>`.
3660
+ function isPcrl(value) {
3661
+ const segments = value.split("/");
3662
+ return segments.length === 4 && PRODUCTS$1.some(product => product === segments[0]) && isId(segments[3]);
3663
+ }
3664
+ function isApp(value) {
3665
+ return APPS.some(app => app === value);
3666
+ }
3667
+ // Links are built by resolving the path against its app's origin, so a stored
3668
+ // path must not be able to resolve anywhere else (`//evil.com`, `/\evil.com`).
3669
+ function isPath(value) {
3670
+ const origin = "https://app.invalid";
3671
+ try {
3672
+ return typeof value === "string" && value.startsWith("/") && new URL(value, origin).origin === origin;
3673
+ } catch {
3674
+ return false;
3675
+ }
3676
+ }
3677
+ function isNonBlankString(value) {
3678
+ return typeof value === "string" && value.trim() !== "";
3679
+ }
3680
+ function isOptionalString(value) {
3681
+ return value === undefined || typeof value === "string";
3682
+ }
3683
+ function isRecord(value) {
3684
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3685
+ }
3686
+ function isPageMetadata(value) {
3687
+ return value === undefined || isRecord(value) && isOptionalString(value.details) && isOptionalString(value.type);
3688
+ }
3689
+ // The query string is part of the link but not of the page's identity, so
3690
+ // revisiting with other filters moves the existing entry instead of adding one.
3691
+ function entryKey(entry) {
3692
+ if (entry.type === "resource") {
3693
+ return `resource:${entry.pcrl}`;
3694
+ } else {
3695
+ return `page:${entry.app}:${entry.path.split("?")[0]}`;
3696
+ }
3697
+ }
3698
+ function isEntry(value) {
3699
+ if (!isRecord(value) || value.version !== ENTRY_VERSION || typeof value.visitedAt !== "string") {
3700
+ return false;
3701
+ } else if (value.type === "resource") {
3702
+ return typeof value.pcrl === "string" && isPcrl(value.pcrl);
3703
+ } else if (value.type === "page") {
3704
+ return isApp(value.app) && isPath(value.path) && isNonBlankString(value.title) && isPageMetadata(value.metadata);
3705
+ } else {
3706
+ return false;
3707
+ }
3708
+ }
3709
+ // Cuts on code points so a surrogate pair is never split.
3710
+ function clampText(value) {
3711
+ return Array.from(value).slice(0, MAX_TEXT_LENGTH).join("");
3712
+ }
3713
+ function clampMetadata({
3714
+ details,
3715
+ type
3716
+ }) {
3717
+ return {
3718
+ ...(details && {
3719
+ details: clampText(details)
3720
+ }),
3721
+ ...(type && {
3722
+ type: clampText(type)
3723
+ })
3724
+ };
3725
+ }
3726
+ function clampVisit(visit) {
3727
+ if (visit.type === "resource") {
3728
+ return visit;
3729
+ } else {
3730
+ return {
3731
+ ...visit,
3732
+ ...(visit.metadata && {
3733
+ metadata: clampMetadata(visit.metadata)
3734
+ }),
3735
+ title: clampText(visit.title.trim())
3736
+ };
3737
+ }
3738
+ }
3739
+ function toEntry(visit, visitedAt) {
3740
+ return {
3741
+ ...clampVisit(visit),
3742
+ version: ENTRY_VERSION,
3743
+ visitedAt: visitedAt.toISOString()
3744
+ };
3745
+ }
3746
+ // Recents are disposable: anything unexpected reads as an empty list (or, for
3747
+ // a single bad entry, a list without it) rather than throwing.
3748
+ function parseRecentlyVisited(value) {
3749
+ let parsed = null;
3750
+ try {
3751
+ parsed = JSON.parse(value !== null && value !== void 0 ? value : "null");
3752
+ } catch {
3753
+ parsed = null;
3754
+ }
3755
+ if (Array.isArray(parsed)) {
3756
+ return parsed.filter(isEntry);
3757
+ } else {
3758
+ return [];
3759
+ }
3760
+ }
3761
+ function addVisit(entries, entry) {
3762
+ const key = entryKey(entry);
3763
+ return [entry, ...entries.filter(existing => entryKey(existing) !== key)].slice(0, MAX_ENTRIES);
3764
+ }
3765
+
3766
+ // The app whose URL, in this environment, has the current page's host.
3767
+ // Reversing pcoUrl handles every environment @planningcenter/url knows about,
3768
+ // and guarantees the stored app rebuilds a working link.
3769
+ function currentApp() {
3770
+ var _a;
3771
+ return (_a = APPS.find(app => new URL(pcoUrl$2(app)).hostname === window.location.hostname)) !== null && _a !== void 0 ? _a : null;
3772
+ }
3773
+ function isFilled(value) {
3774
+ return typeof value === "string" && value !== "";
3775
+ }
3776
+ // The tag's metadata is product-supplied JSON; only its string `type` and
3777
+ // `details` carry over, and an entry without either has no metadata at all.
3778
+ function metadataFromPage(metadata) {
3779
+ var _a;
3780
+ try {
3781
+ const {
3782
+ details,
3783
+ type
3784
+ } = (_a = JSON.parse(metadata !== null && metadata !== void 0 ? metadata : "{}")) !== null && _a !== void 0 ? _a : {};
3785
+ if (isFilled(details) || isFilled(type)) {
3786
+ return {
3787
+ ...(isFilled(details) && {
3788
+ details
3789
+ }),
3790
+ ...(isFilled(type) && {
3791
+ type
3792
+ })
3793
+ };
3794
+ } else {
3795
+ return undefined;
3796
+ }
3797
+ } catch {
3798
+ return undefined;
3799
+ }
3800
+ }
3801
+ // Staff impersonating someone browse under `/O<org id>/P<person id>/…`.
3802
+ function withoutImpersonation(segments) {
3803
+ const [org = "", person = ""] = segments;
3804
+ if (org.startsWith("O") && isId(org.slice(1)) && person.startsWith("P") && isId(person.slice(1))) {
3805
+ return segments.slice(2);
3806
+ } else {
3807
+ return segments;
3808
+ }
3809
+ }
3810
+ // Normalized the way home-mobile does it, so a page visited on web and on
3811
+ // mobile is one entry once the list syncs.
3812
+ function pathFromLocation({
3813
+ pathname,
3814
+ search
3815
+ }) {
3816
+ const segments = withoutImpersonation(pathname.split("/").filter(Boolean));
3817
+ if (segments.length === 0) {
3818
+ return "/";
3819
+ } else {
3820
+ return `/${segments.join("/")}${search.length > 1 ? search : ""}`;
3821
+ }
3822
+ }
3823
+ // pco:resource is the more specific signal, so it wins when both tags are
3824
+ // present. A resource that isn't a PCRL is ignored rather than recorded.
3825
+ function visitFromPage({
3826
+ metadata,
3827
+ resource,
3828
+ title
3829
+ }) {
3830
+ const app = currentApp();
3831
+ if (resource && isPcrl(resource)) {
3832
+ return {
3833
+ pcrl: resource,
3834
+ type: "resource"
3835
+ };
3836
+ } else if (title && title.trim() !== "" && app) {
3837
+ const pageMetadata = metadataFromPage(metadata);
3838
+ return {
3839
+ app,
3840
+ ...(pageMetadata && {
3841
+ metadata: pageMetadata
3842
+ }),
3843
+ path: pathFromLocation(window.location),
3844
+ title,
3845
+ type: "page"
3846
+ };
3847
+ } else {
3848
+ return null;
3849
+ }
3850
+ }
3851
+ // The entry Recently Visited would record for the current page, or null when
3852
+ // the page isn't eligible. Shared by the recorder and the debug panel so the
3853
+ // panel always shows what actually gets stored.
3854
+ function useCurrentVisit() {
3855
+ const {
3856
+ recently_visitable: title,
3857
+ resource
3858
+ } = usePcoMeta();
3859
+ const metadata = useRecentlyVisitableMetadata();
3860
+ return useMemo(() => visitFromPage({
3861
+ metadata,
3862
+ resource,
3863
+ title
3864
+ }), [metadata, resource, title]);
3865
+ }
3866
+ // Products hand the person's ID to AppsProvider, not to the topbar itself,
3867
+ // and several pass it as a number.
3868
+ function useCurrentPersonId() {
3869
+ const {
3870
+ userId
3871
+ } = useStore();
3872
+ return userId == null ? undefined : String(userId);
3873
+ }
3874
+
3875
+ // Per-origin localStorage is a stopgap: each product keeps its own list until
3876
+ // PCA-409 moves this to storage shared across product subdomains. Only this
3877
+ // file should need to change when it does.
3878
+ function storageKey({
3879
+ env,
3880
+ personId
3881
+ }) {
3882
+ return `topbar.recently_visited.${env}.${personId}`;
3883
+ }
3884
+ // localStorage doesn't notify the tab that wrote it, so readers in this tab
3885
+ // learn about new visits from this counter instead.
3886
+ const changes = createStore({
3887
+ version: 0
3888
+ });
3889
+ function readRecentlyVisited(scope) {
3890
+ try {
3891
+ return parseRecentlyVisited(localStorage.getItem(storageKey(scope)));
3892
+ } catch {
3893
+ return [];
3894
+ }
3895
+ }
3896
+ function recordVisit(visit, scope) {
3897
+ const entry = toEntry(visit, new Date());
3898
+ if (isEntry(entry)) {
3899
+ try {
3900
+ localStorage.setItem(storageKey(scope), JSON.stringify(addVisit(readRecentlyVisited(scope), entry)));
3901
+ changes.setState({
3902
+ version: changes.getState().version + 1
3903
+ });
3904
+ } catch {
3905
+ // Storage can be full or blocked; recents are disposable.
3906
+ }
3907
+ }
3908
+ }
3909
+ function useRecentlyVisited({
3910
+ env,
3911
+ personId
3912
+ }) {
3913
+ const version = useSyncExternalStore(changes.subscribe, () => changes.getState().version);
3914
+ // `version` isn't read, but listing it re-reads storage after each visit.
3915
+ return useMemo(() => personId ? readRecentlyVisited({
3916
+ env,
3917
+ personId
3918
+ }) : [], [env, personId, version]);
3919
+ }
3920
+
3921
+ const pcoUrl = pcoUrl$1;
3922
+
3923
+ // Resolves resource entries with one pcrl_lookups request per product.
3924
+ // pcrl_lookups reports forbidden and deleted resources the same way, so both
3925
+ // become `unresolved`, as does every entry in a batch whose request failed.
3926
+ function useHydratedRecentlyVisited(entries, env) {
3927
+ const batches = React__default.useMemo(() => batchByProduct(entries), [entries]);
3928
+ const urls = React__default.useMemo(() => batches.map(([product, pcrls]) => pcrlLookupsUrl(product, pcrls)), [batches]);
3929
+ const requests = useCachedRequests(urls);
3930
+ const lookups = new Map();
3931
+ batches.forEach(([, pcrls], index) => {
3932
+ const request = requests[index];
3933
+ for (const pcrl of pcrls) lookups.set(pcrl, lookupFor(pcrl, request));
3589
3934
  });
3590
- const highlightedIndex = highlight.results === results ? highlight.index : 0;
3591
- const highlighted = results[highlightedIndex];
3935
+ return entries.map(entry => {
3936
+ var _a;
3937
+ if (entry.type === "page") {
3938
+ return {
3939
+ entry,
3940
+ status: "page",
3941
+ url: pageUrl(entry, env)
3942
+ };
3943
+ } else {
3944
+ const lookup = (_a = lookups.get(entry.pcrl)) !== null && _a !== void 0 ? _a : {
3945
+ status: "unresolved"
3946
+ };
3947
+ if (lookup.status === "resolved") {
3948
+ return {
3949
+ ...lookup,
3950
+ entry,
3951
+ url: lookup.result.attributes.url
3952
+ };
3953
+ } else {
3954
+ return {
3955
+ entry,
3956
+ status: lookup.status
3957
+ };
3958
+ }
3959
+ }
3960
+ });
3961
+ }
3962
+ function lookupFor(pcrl, request) {
3963
+ if (request.status === "done") {
3964
+ const result = request.data.data.find(({
3965
+ attributes
3966
+ }) => attributes.pcrl === pcrl);
3967
+ return result ? {
3968
+ result,
3969
+ status: "resolved"
3970
+ } : {
3971
+ status: "unresolved"
3972
+ };
3973
+ } else if (request.status === "error") {
3974
+ return {
3975
+ status: "unresolved"
3976
+ };
3977
+ } else {
3978
+ return {
3979
+ status: "loading"
3980
+ };
3981
+ }
3982
+ }
3983
+ const RECENTLY_VISITED_PAGE_SIZE = 10;
3984
+ // A PCRL can only be looked up through its own product's API, which may not be
3985
+ // the product it was visited in. Each page of entries is batched on its own,
3986
+ // so loading the next page leaves earlier pages' URLs, and so their cache
3987
+ // keys, alone. Sorting keeps them stable as a page reorders.
3988
+ function batchByProduct(entries) {
3989
+ var _a;
3990
+ const batches = [];
3991
+ for (let start = 0; start < entries.length; start += RECENTLY_VISITED_PAGE_SIZE) {
3992
+ const page = new Map();
3993
+ for (const entry of entries.slice(start, start + RECENTLY_VISITED_PAGE_SIZE)) {
3994
+ if (entry.type === "resource") {
3995
+ const [product] = entry.pcrl.split("/");
3996
+ page.set(product, [...((_a = page.get(product)) !== null && _a !== void 0 ? _a : []), entry.pcrl]);
3997
+ }
3998
+ }
3999
+ for (const [product, pcrls] of page) {
4000
+ batches.push([product, [...pcrls].sort()]);
4001
+ }
4002
+ }
4003
+ return batches;
4004
+ }
4005
+ function pcrlLookupsUrl(product, pcrls) {
4006
+ const params = new URLSearchParams({
4007
+ "where[pcrl]": pcrls.join(",")
4008
+ });
4009
+ return `${window.location.origin}/~api/${product}/v2/pcrl_lookups?${params}`;
4010
+ }
4011
+ // Built from the app rather than the current origin, so it stays right once
4012
+ // PCA-409 shares one list across product subdomains.
4013
+ function pageUrl(entry, env) {
4014
+ return new URL(entry.path, pcoUrl(env)(entry.app)).href;
4015
+ }
4016
+
4017
+ function useRecentVisits(env) {
4018
+ const personId = useCurrentPersonId();
4019
+ const entries = useRecentlyVisited({
4020
+ env,
4021
+ personId
4022
+ });
4023
+ const [count, setCount] = React__default.useState(RECENTLY_VISITED_PAGE_SIZE);
4024
+ const shown = React__default.useMemo(() => entries.slice(0, count), [entries, count]);
4025
+ const hydrated = useHydratedRecentlyVisited(shown, env);
4026
+ // Resources show up once they're found. Ones that can't be found anymore,
4027
+ // or that the person can no longer see, are left out.
4028
+ const visits = hydrated.filter(isShown);
4029
+ const loading = hydrated.some(({
4030
+ status
4031
+ }) => status === "loading");
4032
+ const showMore = React__default.useCallback(() => setCount(current => current + RECENTLY_VISITED_PAGE_SIZE), []);
4033
+ return {
4034
+ count,
4035
+ hasMore: entries.length > count,
4036
+ loading,
4037
+ showMore,
4038
+ visits
4039
+ };
4040
+ }
4041
+ function isShown(visit) {
4042
+ return visit.status === "page" || visit.status === "resolved";
4043
+ }
4044
+
4045
+ function open(url, {
4046
+ newTab
4047
+ }) {
4048
+ if (newTab) {
4049
+ window.open(url, "_blank", "noopener");
4050
+ } else {
4051
+ window.location.assign(url);
4052
+ }
4053
+ }
4054
+ // With text selected, ⌘C should copy that text as usual, not the link.
4055
+ function hasSelectedText(element) {
4056
+ var _a, _b;
4057
+ const selectedInField = element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
4058
+ return selectedInField || ((_b = (_a = window.getSelection()) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : "") !== "";
4059
+ }
4060
+
4061
+ const COPIED_FEEDBACK_MS = 1500;
4062
+ // `copiedUrl` stays set briefly after a copy so the UI can confirm it.
4063
+ function useCopyLink() {
3592
4064
  const [copiedUrl, setCopiedUrl] = React__default.useState(null);
3593
4065
  React__default.useEffect(() => {
3594
4066
  if (copiedUrl) {
@@ -3598,201 +4070,718 @@ function useResultNavigation(results) {
3598
4070
  return undefined;
3599
4071
  }
3600
4072
  }, [copiedUrl]);
3601
- const highlightIndex = React__default.useCallback(index => setHighlight(current => current.results === results && current.index === index ? current : {
3602
- index,
3603
- results
3604
- }), [results]);
4073
+ const copy = React__default.useCallback(url => {
4074
+ navigator.clipboard.writeText(url).then(() => setCopiedUrl(url));
4075
+ }, []);
4076
+ return {
4077
+ copiedUrl,
4078
+ copy
4079
+ };
4080
+ }
4081
+
4082
+ // Remembering which list the index belongs to puts the highlight back on the
4083
+ // first item whenever a new list replaces it, without an effect to reset it.
4084
+ // A list that grows or fills in under the same `list` keeps its highlight.
4085
+ // Disabled, nothing is highlighted and the keys and pointer leave it alone.
4086
+ function useResultNavigation(items, list, enabled = true) {
4087
+ const [highlight, setHighlight] = React__default.useState({
4088
+ index: 0,
4089
+ list
4090
+ });
4091
+ const highlightedIndex = highlight.list === list ? highlight.index : 0;
4092
+ const highlighted = enabled ? items[highlightedIndex] : undefined;
4093
+ const {
4094
+ copiedUrl,
4095
+ copy
4096
+ } = useCopyLink();
4097
+ const highlightIndex = React__default.useCallback(index => {
4098
+ if (!enabled) return;
4099
+ setHighlight(current => current.list === list && current.index === index ? current : {
4100
+ index,
4101
+ list
4102
+ });
4103
+ }, [enabled, list]);
3605
4104
  const handleKeyDown = event => {
4105
+ if (!enabled) return;
3606
4106
  const step = verticalStep(event.key);
3607
- if (step && results.length > 0) {
4107
+ if (step && items.length > 0) {
3608
4108
  event.preventDefault();
3609
- highlightIndex((highlightedIndex + step + results.length) % results.length);
4109
+ highlightIndex((highlightedIndex + step + items.length) % items.length);
3610
4110
  } else if (event.key === "Enter" && highlighted && !event.nativeEvent.isComposing) {
3611
4111
  event.preventDefault();
3612
- open(highlighted.attributes.url, {
4112
+ open(highlighted.url, {
3613
4113
  newTab: hasCommandKey(event)
3614
4114
  });
3615
4115
  } else if (isCopyShortcut(event) && highlighted && !hasSelectedText(event.currentTarget)) {
3616
4116
  event.preventDefault();
3617
- const {
3618
- url
3619
- } = highlighted.attributes;
3620
- navigator.clipboard.writeText(url).then(() => setCopiedUrl(url));
4117
+ copy(highlighted.url);
3621
4118
  }
3622
4119
  };
3623
4120
  return {
3624
- copied: copiedUrl !== null && copiedUrl === (highlighted === null || highlighted === void 0 ? void 0 : highlighted.attributes.url),
4121
+ copied: copiedUrl !== null && copiedUrl === (highlighted === null || highlighted === void 0 ? void 0 : highlighted.url),
3625
4122
  handleKeyDown,
3626
4123
  highlightIndex,
3627
4124
  highlighted,
3628
4125
  highlightedIndex: highlighted ? highlightedIndex : null
3629
4126
  };
3630
4127
  }
3631
- function open(url, {
3632
- newTab
4128
+
4129
+ var styles$i = {"modal":"modal-NR3TO","mobile":"mobile-4e5rC","visuallyHidden":"visuallyHidden-C28ju","panel":"panel-HlF7S"};
4130
+
4131
+ function groupSections(groups = []) {
4132
+ return groups.map(({
4133
+ results,
4134
+ type
4135
+ }) => ({
4136
+ heading: type,
4137
+ items: results.map(resultItem),
4138
+ key: type
4139
+ }));
4140
+ }
4141
+ function resultItem({
4142
+ attributes,
4143
+ id
3633
4144
  }) {
3634
- if (newTab) {
3635
- window.open(url, "_blank", "noopener");
4145
+ return {
4146
+ category: attributes.category,
4147
+ imageUrl: attributes.image_url,
4148
+ key: id,
4149
+ product: attributes.product,
4150
+ subtitle: summarize(attributes.metadata),
4151
+ title: attributes.title,
4152
+ url: attributes.url
4153
+ };
4154
+ }
4155
+ function visitItem(visit) {
4156
+ return {
4157
+ key: entryKey(visit.entry),
4158
+ ...describeVisit(visit)
4159
+ };
4160
+ }
4161
+ function summarize(metadata) {
4162
+ return Object.values(metadata !== null && metadata !== void 0 ? metadata : {}).filter(value => typeof value === "string" && value !== "").join(" · ");
4163
+ }
4164
+ // Unlike search results, recents come from every product, so each one names
4165
+ // its product and type.
4166
+ function describeVisit(visit) {
4167
+ var _a, _b;
4168
+ const subtitle = (...parts) => parts.filter(Boolean).join(" · ");
4169
+ if (visit.status === "page") {
4170
+ const {
4171
+ app,
4172
+ metadata,
4173
+ title
4174
+ } = visit.entry;
4175
+ return {
4176
+ category: "generic",
4177
+ imageUrl: null,
4178
+ product: app,
4179
+ 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 : ""),
4180
+ title,
4181
+ url: visit.url
4182
+ };
3636
4183
  } else {
3637
- window.location.assign(url);
4184
+ const {
4185
+ category,
4186
+ image_url,
4187
+ metadata,
4188
+ product,
4189
+ title,
4190
+ type
4191
+ } = visit.result.attributes;
4192
+ return {
4193
+ category,
4194
+ imageUrl: image_url,
4195
+ product,
4196
+ subtitle: subtitle(productName(product), type, summarize(metadata)),
4197
+ title,
4198
+ url: visit.url
4199
+ };
3638
4200
  }
3639
4201
  }
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 : "") !== "";
4202
+ // check-ins → Check-Ins
4203
+ function productName(slug) {
4204
+ return slug.split("-").map(capitalize).join("-");
4205
+ }
4206
+ function capitalize(word) {
4207
+ return word.charAt(0).toUpperCase() + word.slice(1);
3646
4208
  }
3647
4209
 
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)
4210
+ const STORAGE_KEY = "topbar.product_search.last_product";
4211
+ function readLastProduct() {
4212
+ try {
4213
+ return localStorage.getItem(STORAGE_KEY);
4214
+ } catch {
4215
+ return null;
4216
+ }
4217
+ }
4218
+ function saveLastProduct(slug) {
4219
+ try {
4220
+ localStorage.setItem(STORAGE_KEY, slug);
4221
+ } catch {
4222
+ // Storage can be full or blocked; search still works without it.
4223
+ }
4224
+ }
4225
+ function initialProduct(products) {
4226
+ var _a, _b, _c;
4227
+ const lastProduct = readLastProduct();
4228
+ const bySlug = slug => products.find(product => product.slug === slug);
4229
+ 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];
4230
+ }
4231
+
4232
+ var styles$h = {"container":"container-YDdM1","hint":"hint-017Rd","keycap":"keycap-ENJM2","copied":"copied-DUFa1"};
4233
+
4234
+ const SYMBOLS = {
4235
+ command: {
4236
+ label: "⌘",
4237
+ paths: ["M7 9a2 2 0 1 1 2 -2v10a2 2 0 1 1 -2 -2h10a2 2 0 1 1 -2 2v-10a2 2 0 1 1 2 2h-10"]
4238
+ },
4239
+ down: {
4240
+ label: "↓",
4241
+ paths: ["M12 6l0 12", "M16 14l-4 4", "M8 14l4 4"]
4242
+ },
4243
+ return: {
4244
+ label: "↵",
4245
+ paths: ["M18 6v6a3 3 0 0 1 -3 3h-10l4 -4m0 8l-4 -4"]
4246
+ },
4247
+ up: {
4248
+ label: "↑",
4249
+ paths: ["M12 6l0 12", "M16 10l-4 -4", "M8 10l4 -4"]
4250
+ }
3666
4251
  };
3667
- function fetchMe(product, fields) {
3668
- const params = new URLSearchParams({
3669
- "fields[Person]": fields
4252
+ function Keycap(props) {
4253
+ return /*#__PURE__*/React__default.createElement("kbd", {
4254
+ className: styles$h.keycap
4255
+ }, "symbol" in props ? (/*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("svg", {
4256
+ "aria-hidden": true,
4257
+ fill: "none",
4258
+ stroke: "currentColor",
4259
+ strokeLinecap: "round",
4260
+ strokeLinejoin: "round",
4261
+ viewBox: "0 0 24 24"
4262
+ }, SYMBOLS[props.symbol].paths.map(d => (/*#__PURE__*/React__default.createElement("path", {
4263
+ d: d,
4264
+ key: d
4265
+ })))), /*#__PURE__*/React__default.createElement("span", {
4266
+ className: styles$i.visuallyHidden
4267
+ }, SYMBOLS[props.symbol].label))) : props.label);
4268
+ }
4269
+ function CommandKey() {
4270
+ return isMac() ? /*#__PURE__*/React__default.createElement(Keycap, {
4271
+ symbol: "command"
4272
+ }) : /*#__PURE__*/React__default.createElement(Keycap, {
4273
+ label: "Ctrl"
3670
4274
  });
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;
4275
+ }
4276
+ function EnterKey() {
4277
+ return isMac() ? /*#__PURE__*/React__default.createElement(Keycap, {
4278
+ symbol: "return"
4279
+ }) : /*#__PURE__*/React__default.createElement(Keycap, {
4280
+ label: "Enter"
3677
4281
  });
3678
4282
  }
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;
4283
+ const {
4284
+ copyLink,
4285
+ openInNewTab,
4286
+ openSearch,
4287
+ select
4288
+ } = KEYBOARD_SHORTCUTS;
4289
+ function KeyboardHints({
4290
+ copied
4291
+ }) {
4292
+ return /*#__PURE__*/React__default.createElement("ul", {
4293
+ "aria-label": "Keyboard shortcuts",
4294
+ className: styles$h.container
4295
+ }, /*#__PURE__*/React__default.createElement("li", {
4296
+ className: styles$h.hint
4297
+ }, /*#__PURE__*/React__default.createElement(Flex, {
4298
+ gap: 0.5
4299
+ }, select.modifierKey, /*#__PURE__*/React__default.createElement(Keycap, {
4300
+ symbol: "up"
4301
+ }), /*#__PURE__*/React__default.createElement(Keycap, {
4302
+ symbol: "down"
4303
+ })), select.label), /*#__PURE__*/React__default.createElement("li", {
4304
+ className: styles$h.hint
4305
+ }, /*#__PURE__*/React__default.createElement(Flex, {
4306
+ gap: 0.5
4307
+ }, /*#__PURE__*/React__default.createElement(CommandKey, null), /*#__PURE__*/React__default.createElement(EnterKey, null)), openInNewTab.label), /*#__PURE__*/React__default.createElement("li", {
4308
+ className: classnames(styles$h.hint, copied && styles$h.copied)
4309
+ }, /*#__PURE__*/React__default.createElement(Flex, {
4310
+ gap: 0.5
4311
+ }, /*#__PURE__*/React__default.createElement(CommandKey, null), /*#__PURE__*/React__default.createElement(Keycap, {
4312
+ label: copyLink.key.toUpperCase()
4313
+ })), /*#__PURE__*/React__default.createElement("span", {
4314
+ role: "status"
4315
+ }, copied ? "Copied!" : copyLink.label)), /*#__PURE__*/React__default.createElement("li", {
4316
+ className: styles$h.hint
4317
+ }, /*#__PURE__*/React__default.createElement(Flex, {
4318
+ gap: 0.5
4319
+ }, /*#__PURE__*/React__default.createElement(CommandKey, null), /*#__PURE__*/React__default.createElement(Keycap, {
4320
+ label: openSearch.key.toUpperCase()
4321
+ })), openSearch.label));
4322
+ }
4323
+
4324
+ var styles$g = {"icon":"icon-XKLvN","squircle":"squircle-JCHr6"};
4325
+
4326
+ const PRODUCTS = {
4327
+ accounts: {
4328
+ gradient: "--t-fill-color-product-accounts-gradient-brand",
4329
+ path: logomarks.accounts
4330
+ },
4331
+ api: {
4332
+ gradient: "--t-fill-color-product-api-gradient-brand",
4333
+ path: logomarks.api
4334
+ },
4335
+ calendar: {
4336
+ gradient: "--t-fill-color-product-calendar-gradient-brand",
4337
+ path: logomarks.calendar
4338
+ },
4339
+ "check-ins": {
4340
+ gradient: "--t-fill-color-product-checkins-gradient-brand",
4341
+ path: logomarks.checkIns
4342
+ },
4343
+ "church-center": {
4344
+ gradient: "--t-fill-color-product-cc-gradient-brand",
4345
+ path: logomarks.churchCenter
4346
+ },
4347
+ giving: {
4348
+ gradient: "--t-fill-color-product-giving-gradient-brand",
4349
+ path: logomarks.giving
4350
+ },
4351
+ groups: {
4352
+ gradient: "--t-fill-color-product-groups-gradient-brand",
4353
+ path: logomarks.groups
4354
+ },
4355
+ home: {
4356
+ gradient: "--t-fill-color-product-home-gradient-brand",
4357
+ path: logomarks.home
4358
+ },
4359
+ "music-stand": {
4360
+ gradient: "--t-fill-color-product-musicstand-gradient-brand",
4361
+ path: logomarks.musicStand
4362
+ },
4363
+ people: {
4364
+ gradient: "--t-fill-color-product-people-gradient-brand",
4365
+ path: logomarks.people
4366
+ },
4367
+ publishing: {
4368
+ gradient: "--t-fill-color-product-publishing-gradient-brand",
4369
+ path: logomarks.publishing
4370
+ },
4371
+ registrations: {
4372
+ gradient: "--t-fill-color-product-registrations-gradient-brand",
4373
+ path: logomarks.registrations
4374
+ },
4375
+ services: {
4376
+ gradient: "--t-fill-color-product-services-gradient-brand",
4377
+ path: logomarks.services
3696
4378
  }
4379
+ };
4380
+ const isProductIconSlug = slug => Object.prototype.hasOwnProperty.call(PRODUCTS, slug);
4381
+ const maskFor = path => `url("data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="${path}"/></svg>`)}")`;
4382
+ function ProductIcon({
4383
+ size = 16,
4384
+ slug
4385
+ }) {
4386
+ const {
4387
+ gradient,
4388
+ path
4389
+ } = PRODUCTS[slug];
4390
+ const mask = maskFor(path);
4391
+ return /*#__PURE__*/React__default.createElement("span", {
4392
+ "aria-hidden": true,
4393
+ className: styles$g.icon,
4394
+ style: {
4395
+ height: size,
4396
+ width: size
4397
+ }
4398
+ }, /*#__PURE__*/React__default.createElement("span", {
4399
+ className: styles$g.squircle,
4400
+ style: {
4401
+ WebkitMaskImage: mask,
4402
+ background: `var(${gradient})`,
4403
+ maskImage: mask
4404
+ }
4405
+ }));
3697
4406
  }
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(({
4407
+
4408
+ var styles$f = {"picker":"picker-iuzzj","option":"option--7LXg"};
4409
+
4410
+ // Phones have no room for a tab per product, so they pick from a dropdown.
4411
+ // Complex mode renders each option's logomark, in the list and the trigger.
4412
+ function ProductSelect({
4413
+ children,
4414
+ onSelect,
4415
+ products,
4416
+ selected
4417
+ }) {
4418
+ const options = products.map(({
4419
+ appName,
3702
4420
  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
- });
4421
+ }) => ({
4422
+ label: (/*#__PURE__*/React__default.createElement("span", {
4423
+ className: styles$f.option
4424
+ }, /*#__PURE__*/React__default.createElement(ProductIcon, {
4425
+ slug: slug
4426
+ }), appName)),
4427
+ textValue: appName,
4428
+ value: slug
4429
+ }));
4430
+ return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("div", {
4431
+ className: styles$f.picker
4432
+ }, /*#__PURE__*/React__default.createElement(Select, {
4433
+ "aria-label": "Product",
4434
+ complex: true,
4435
+ onChange: event => onSelect(event.target.value),
4436
+ options: options,
4437
+ value: selected.slug
4438
+ })), /*#__PURE__*/React__default.createElement("div", {
4439
+ className: styles$i.panel
4440
+ }, children));
4441
+ }
4442
+
4443
+ var styles$e = {"tablist":"tablist-7ASvc","tab":"tab-dz8ki"};
4444
+
4445
+ function ProductTabs({
4446
+ children,
4447
+ onKeyDown,
4448
+ onSelect,
4449
+ products,
4450
+ selected
4451
+ }) {
4452
+ const id = useInstanceId("product-tabs");
4453
+ const tabId = slug => `${id}-tab-${slug}`;
4454
+ const tabRefs = React__default.useRef(new Map());
4455
+ const handleKeyDown = event => {
4456
+ var _a;
4457
+ const offset = horizontalStep(event.key);
4458
+ if (offset) {
4459
+ event.preventDefault();
4460
+ const index = products.indexOf(selected);
4461
+ const next = products[(index + offset + products.length) % products.length];
4462
+ onSelect(next.slug);
4463
+ (_a = tabRefs.current.get(next.slug)) === null || _a === void 0 ? void 0 : _a.focus();
4464
+ } else {
4465
+ onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
4466
+ }
4467
+ };
4468
+ return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("div", {
4469
+ "aria-label": "Product",
4470
+ className: styles$e.tablist,
4471
+ onKeyDown: handleKeyDown,
4472
+ role: "tablist"
4473
+ }, products.map(product => (/*#__PURE__*/React__default.createElement("button", {
4474
+ "aria-controls": `${id}-panel`,
4475
+ "aria-selected": product === selected,
4476
+ className: styles$e.tab,
4477
+ id: tabId(product.slug),
4478
+ key: product.slug,
4479
+ onClick: () => onSelect(product.slug),
4480
+ ref: element => {
4481
+ if (element) {
4482
+ tabRefs.current.set(product.slug, element);
4483
+ } else {
4484
+ tabRefs.current.delete(product.slug);
3716
4485
  }
4486
+ },
4487
+ role: "tab",
4488
+ tabIndex: product === selected ? 0 : -1,
4489
+ type: "button"
4490
+ }, /*#__PURE__*/React__default.createElement(ProductIcon, {
4491
+ slug: product.slug
4492
+ }), product === selected ? product.appName : (/*#__PURE__*/React__default.createElement("span", {
4493
+ className: styles$i.visuallyHidden
4494
+ }, product.appName)))))), /*#__PURE__*/React__default.createElement("div", {
4495
+ "aria-labelledby": tabId(selected.slug),
4496
+ className: styles$i.panel,
4497
+ id: `${id}-panel`,
4498
+ role: "tabpanel"
4499
+ }, children));
4500
+ }
4501
+
4502
+ function EndOfResultsObserver({
4503
+ onVisible
4504
+ }) {
4505
+ const ref = React__default.useRef(null);
4506
+ React__default.useEffect(() => {
4507
+ const observer = new IntersectionObserver(([entry]) => {
4508
+ if (entry.isIntersecting) onVisible();
3717
4509
  });
3718
- return () => {
3719
- current = false;
3720
- };
3721
- }, [accessibleKey]);
3722
- if ((settled === null || settled === void 0 ? void 0 : settled.accessibleKey) === accessibleKey) {
3723
- return {
3724
- products: settled.products,
3725
- status: "done"
3726
- };
3727
- } else {
3728
- return {
3729
- status: "loading"
3730
- };
3731
- }
4510
+ if (ref.current) observer.observe(ref.current);
4511
+ return () => observer.disconnect();
4512
+ }, [onVisible]);
4513
+ return /*#__PURE__*/React__default.createElement("div", {
4514
+ ref: ref
4515
+ });
3732
4516
  }
3733
- function inSwitcherOrder(apps) {
3734
- return groupApps(apps).restProducts.flatMap(app => SEARCHABLE_PRODUCTS.filter(product => product.appName === app.name));
4517
+
4518
+ const resultId = (listboxId, index) => `${listboxId}-result-${index}`;
4519
+
4520
+ var styles$d = {"result":"result-Thq2C","title":"title-KYUEn","subtitle":"subtitle-R2haq","image":"image-KVDor","avatar":"avatar-6Gh62","photo":"photo-FZR3Q","badge":"badge-trUKt"};
4521
+
4522
+ function SearchResult({
4523
+ category,
4524
+ highlighted,
4525
+ id,
4526
+ imageUrl,
4527
+ onHighlight,
4528
+ product,
4529
+ subtitle,
4530
+ title,
4531
+ url
4532
+ }) {
4533
+ const ref = React__default.useRef(null);
4534
+ // A hovered result is already under the pointer, and scrolling it clear of
4535
+ // the panel's scroll padding would slide the list out from under the mouse.
4536
+ const highlightedByPointer = React__default.useRef(false);
4537
+ React__default.useEffect(() => {
4538
+ var _a;
4539
+ if (!highlighted) return;
4540
+ if (highlightedByPointer.current) {
4541
+ highlightedByPointer.current = false;
4542
+ } else {
4543
+ (_a = ref.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({
4544
+ block: "nearest"
4545
+ });
4546
+ }
4547
+ }, [highlighted]);
4548
+ const highlightFromPointer = () => {
4549
+ if (highlighted) return;
4550
+ highlightedByPointer.current = true;
4551
+ onHighlight();
4552
+ };
4553
+ // Focus stays in the search input, which points at the highlighted result
4554
+ // with aria-activedescendant, so results are kept out of the tab order.
4555
+ // Mouse move rather than enter: arrowing scrolls results under a resting
4556
+ // pointer, and that shouldn't steal the highlight.
4557
+ return /*#__PURE__*/React__default.createElement("a", {
4558
+ "aria-selected": highlighted,
4559
+ className: styles$d.result,
4560
+ href: url,
4561
+ id: id,
4562
+ onMouseMove: highlightFromPointer,
4563
+ ref: ref,
4564
+ role: "option",
4565
+ tabIndex: -1
4566
+ }, /*#__PURE__*/React__default.createElement(ResultImage, {
4567
+ category: category,
4568
+ imageUrl: imageUrl,
4569
+ product: product
4570
+ }), /*#__PURE__*/React__default.createElement("span", {
4571
+ className: styles$d.title
4572
+ }, title), /*#__PURE__*/React__default.createElement("span", {
4573
+ className: styles$d.subtitle
4574
+ }, subtitle));
4575
+ }
4576
+ // A person's photo or the result's own image, badged with its product, or
4577
+ // just the product's logomark when there's no image.
4578
+ function ResultImage({
4579
+ category,
4580
+ imageUrl,
4581
+ product
4582
+ }) {
4583
+ const icon = size => isProductIconSlug(product) ? (/*#__PURE__*/React__default.createElement(ProductIcon, {
4584
+ size: size,
4585
+ slug: product
4586
+ })) : null;
4587
+ return /*#__PURE__*/React__default.createElement("span", {
4588
+ "aria-hidden": true,
4589
+ className: styles$d.image
4590
+ }, imageUrl ? (/*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("img", {
4591
+ alt: "",
4592
+ className: category === "profile" ? styles$d.avatar : styles$d.photo,
4593
+ loading: "lazy",
4594
+ src: imageUrl
4595
+ }), /*#__PURE__*/React__default.createElement("span", {
4596
+ className: styles$d.badge
4597
+ }, icon(16)))) : icon("100%"));
3735
4598
  }
3736
4599
 
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
4600
+ var styles$c = {"message":"message-1-PP-","heading":"heading-LioUF"};
4601
+
4602
+ // Sections with a heading render as labelled groups; ones without render
4603
+ // their items straight into the listbox. Indexes run across every section.
4604
+ function ResultListbox({
4605
+ busy,
4606
+ highlightedIndex,
4607
+ id,
4608
+ label,
4609
+ labelledBy,
4610
+ onHighlight,
4611
+ sections
3744
4612
  }) {
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
4613
+ let index = 0;
4614
+ const renderItem = item => {
4615
+ const itemIndex = index++;
4616
+ return /*#__PURE__*/React__default.createElement(SearchResult, {
4617
+ category: item.category,
4618
+ highlighted: itemIndex === highlightedIndex,
4619
+ id: resultId(id, itemIndex),
4620
+ imageUrl: item.imageUrl,
4621
+ key: item.key,
4622
+ onHighlight: () => onHighlight(itemIndex),
4623
+ product: item.product,
4624
+ subtitle: item.subtitle,
4625
+ title: item.title,
4626
+ url: item.url
3749
4627
  });
4628
+ };
4629
+ return /*#__PURE__*/React__default.createElement("div", {
4630
+ "aria-busy": busy,
4631
+ "aria-label": label,
4632
+ "aria-labelledby": labelledBy,
4633
+ id: id,
4634
+ role: "listbox"
4635
+ }, sections.map((section, sectionIndex) => {
4636
+ if (section.heading === undefined) {
4637
+ return /*#__PURE__*/React__default.createElement(React__default.Fragment, {
4638
+ key: section.key
4639
+ }, section.items.map(renderItem));
4640
+ }
4641
+ const headingId = `${id}-group-${sectionIndex}`;
4642
+ return /*#__PURE__*/React__default.createElement("div", {
4643
+ "aria-labelledby": headingId,
4644
+ key: section.key,
4645
+ role: "group"
4646
+ }, /*#__PURE__*/React__default.createElement("div", {
4647
+ className: styles$c.heading,
4648
+ id: headingId
4649
+ }, section.heading), section.items.map(renderItem));
4650
+ }));
4651
+ }
4652
+
4653
+ function RecentlyVisited({
4654
+ count,
4655
+ hasMore,
4656
+ items,
4657
+ loading,
4658
+ showMore,
4659
+ ...navigation
4660
+ }) {
4661
+ const headingId = `${navigation.id}-heading`;
4662
+ if (items.length === 0 && !hasMore && !loading) {
4663
+ return /*#__PURE__*/React__default.createElement("p", {
4664
+ className: styles$c.message
4665
+ }, "Pages you visit will show up here.");
3750
4666
  } else {
3751
- return null;
4667
+ return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("div", {
4668
+ className: styles$c.heading,
4669
+ id: headingId
4670
+ }, "Recently visited"), /*#__PURE__*/React__default.createElement(ResultListbox, {
4671
+ labelledBy: headingId,
4672
+ sections: [{
4673
+ items,
4674
+ key: "recents"
4675
+ }],
4676
+ ...navigation
4677
+ }), hasMore && /*#__PURE__*/React__default.createElement(EndOfResultsObserver, {
4678
+ key: count,
4679
+ onVisible: showMore
4680
+ }));
3752
4681
  }
3753
4682
  }
3754
- function SearchButtonAndDialog({
3755
- products
4683
+
4684
+ var styles$b = {"header":"header-sXEyG","input":"input-YQkTC","clearButton":"clearButton-9Yh9w"};
4685
+
4686
+ // A combobox: focus stays here while aria-activedescendant points screen
4687
+ // readers at the highlighted result in the listbox it controls.
4688
+ function SearchInput({
4689
+ expanded,
4690
+ highlightedIndex,
4691
+ inputRef,
4692
+ listboxId,
4693
+ onChange,
4694
+ onFocus,
4695
+ onKeyDown,
4696
+ value
3756
4697
  }) {
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"} /`,
4698
+ return /*#__PURE__*/React__default.createElement("label", {
4699
+ className: styles$b.header
4700
+ }, /*#__PURE__*/React__default.createElement(Icon, {
4701
+ name: "general.search",
4702
+ size: 20,
4703
+ theme: "--t-icon-color-dim"
4704
+ }), /*#__PURE__*/React__default.createElement("input", {
4705
+ "aria-activedescendant": highlightedIndex === null ? undefined : resultId(listboxId, highlightedIndex),
4706
+ "aria-autocomplete": "list",
4707
+ "aria-controls": expanded ? listboxId : undefined,
4708
+ "aria-expanded": expanded,
3781
4709
  "aria-label": "Search",
3782
- onClick: () => setOpen(true),
4710
+ autoFocus: true,
4711
+ className: styles$b.input,
4712
+ onChange: event => onChange(event.target.value),
4713
+ onFocus: onFocus,
4714
+ onKeyDown: onKeyDown,
4715
+ placeholder: "Search",
4716
+ ref: inputRef,
4717
+ role: "combobox",
4718
+ type: "search",
4719
+ value: value
4720
+ }), value !== "" && (/*#__PURE__*/React__default.createElement("button", {
4721
+ "aria-label": "Clear search",
4722
+ className: styles$b.clearButton,
4723
+ onClick: () => {
4724
+ var _a;
4725
+ onChange("");
4726
+ (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
4727
+ },
4728
+ tabIndex: -1,
3783
4729
  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
- })));
4730
+ }, /*#__PURE__*/React__default.createElement(Icon, {
4731
+ name: "general.smallX"
4732
+ }))));
4733
+ }
4734
+
4735
+ function SearchResults({
4736
+ label,
4737
+ sections,
4738
+ status,
4739
+ term,
4740
+ ...navigation
4741
+ }) {
4742
+ switch (status) {
4743
+ case "idle":
4744
+ return null;
4745
+ case "loading":
4746
+ if (sections.length > 0) {
4747
+ return /*#__PURE__*/React__default.createElement(ResultListbox, {
4748
+ busy: true,
4749
+ label: label,
4750
+ sections: sections,
4751
+ ...navigation
4752
+ });
4753
+ }
4754
+ return /*#__PURE__*/React__default.createElement("p", {
4755
+ className: styles$c.message,
4756
+ role: "status"
4757
+ }, "Searching\u2026");
4758
+ case "error":
4759
+ return /*#__PURE__*/React__default.createElement("p", {
4760
+ className: styles$c.message,
4761
+ role: "alert"
4762
+ }, "Search isn\u2019t working right now. Try again.");
4763
+ case "done":
4764
+ if (sections.length === 0) {
4765
+ return /*#__PURE__*/React__default.createElement("p", {
4766
+ className: styles$c.message,
4767
+ role: "status"
4768
+ }, "No results for \u201C", term.trim(), "\u201D");
4769
+ }
4770
+ return /*#__PURE__*/React__default.createElement(ResultListbox, {
4771
+ busy: false,
4772
+ label: label,
4773
+ sections: sections,
4774
+ ...navigation
4775
+ });
4776
+ }
3794
4777
  }
4778
+
4779
+ // Keys the recents list for navigation, so the highlight
4780
+ // stays put as more recents load and as their lookups answer.
4781
+ const RECENTS = "recents";
3795
4782
  function SearchPanel({
4783
+ mobile = false,
4784
+ env,
3796
4785
  products
3797
4786
  }) {
3798
4787
  var _a, _b;
@@ -3805,11 +4794,15 @@ function SearchPanel({
3805
4794
  const hasTerm = term.trim() !== "";
3806
4795
  const search = useProductSearch(product.slug, term);
3807
4796
  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);
4797
+ const sections = React__default.useMemo(() => groupSections(groups), [groups]);
4798
+ const {
4799
+ visits,
4800
+ ...recents
4801
+ } = useRecentVisits(env);
4802
+ const recentItems = visits.map(visitItem);
4803
+ const items = hasTerm ? sections.flatMap(section => section.items) : recentItems;
4804
+ // Phones open results by tapping them, with no highlight to arrow through.
4805
+ const navigation = useResultNavigation(items, hasTerm ? sections : RECENTS, !mobile);
3813
4806
  const {
3814
4807
  highlightedIndex
3815
4808
  } = navigation;
@@ -3832,296 +4825,269 @@ function SearchPanel({
3832
4825
  React__default.useEffect(() => {
3833
4826
  if (hasTerm) saveLastProduct(product.slug);
3834
4827
  }, [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),
4828
+ const results = hasTerm ? (/*#__PURE__*/React__default.createElement(SearchResults, {
4829
+ highlightedIndex: highlightedIndex,
4830
+ id: listboxId,
4831
+ label: `${product.appName} results`,
4832
+ onHighlight: navigation.highlightIndex,
4833
+ sections: sections,
4834
+ status: search.status,
4835
+ term: term
4836
+ })) : (/*#__PURE__*/React__default.createElement(RecentlyVisited, {
4837
+ highlightedIndex: highlightedIndex,
4838
+ id: listboxId,
4839
+ items: recentItems,
4840
+ onHighlight: navigation.highlightIndex,
4841
+ ...recents
4842
+ }));
4843
+ return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(SearchInput, {
4844
+ expanded: items.length > 0,
4845
+ highlightedIndex: highlightedIndex,
4846
+ inputRef: inputRef,
4847
+ listboxId: listboxId,
4848
+ onChange: setTerm,
3843
4849
  onFocus: () => setAnnounceHighlight(false),
3844
4850
  onKeyDown: navigation.handleKeyDown,
3845
- placeholder: "Search",
3846
- ref: inputRef,
3847
- role: "combobox",
3848
- type: "search",
3849
4851
  value: term
3850
- }), /*#__PURE__*/React__default.createElement(ProductTabs, {
3851
- onKeyDown: handleTabsKeyDown,
4852
+ }), mobile ? (/*#__PURE__*/React__default.createElement(ProductSelect, {
3852
4853
  onSelect: setSelectedSlug,
3853
4854
  products: products,
3854
4855
  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", {
4856
+ }, results)) : (/*#__PURE__*/React__default.createElement(ProductTabs, {
4857
+ onKeyDown: handleTabsKeyDown,
4858
+ onSelect: setSelectedSlug,
4859
+ products: products,
4860
+ selected: product
4861
+ }, results)), /*#__PURE__*/React__default.createElement("div", {
3863
4862
  "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, {
4863
+ className: styles$i.visuallyHidden
4864
+ }, announceHighlight ? (_b = navigation.highlighted) === null || _b === void 0 ? void 0 : _b.title : null), mobile ? null : /*#__PURE__*/React__default.createElement(KeyboardHints, {
3866
4865
  copied: navigation.copied
3867
4866
  }));
3868
4867
  }
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];
3882
- }
3883
4868
 
3884
- function parseValue(element) {
3885
- const datatype = element.getAttribute("datatype");
3886
- switch (datatype) {
3887
- case "boolean":
3888
- return element.content === "true" || element.content === "1";
3889
- default:
3890
- return element.content;
3891
- }
3892
- }
3893
- function getAllPcoMeta() {
3894
- const elements = document.head.querySelectorAll(`meta[property^="pco:"]`);
3895
- const meta = {};
3896
- elements.forEach(e => {
3897
- const property = e.getAttribute("property");
3898
- meta[property.substring("pco:".length)] = parseValue(e);
3899
- });
3900
- return meta;
4869
+ // Products open modals from many libraries, not all of which mark themselves
4870
+ // with roles, so a modal is recognized by what it does: cover the page.
4871
+ function isModalOpen() {
4872
+ const {
4873
+ clientHeight,
4874
+ clientWidth
4875
+ } = document.documentElement;
4876
+ const element = document.elementFromPoint(clientWidth / 2, clientHeight / 2);
4877
+ return element !== null && (isInNativeDialog(element) || hasBackdrop(element, clientWidth, clientHeight));
3901
4878
  }
3902
- // usePcoMeta only reads each tag's content. Recently Visited also needs the
3903
- // pco:recently_visitable tag's `metadata` attribute, which can change while
3904
- // the content stays the same, so it is tracked in its own store.
3905
- function getRecentlyVisitableMetadata() {
3906
- var _a, _b;
3907
- return (_b = (_a = document.head.querySelector(`meta[property="pco:recently_visitable"]`)) === null || _a === void 0 ? void 0 : _a.getAttribute("metadata")) !== null && _b !== void 0 ? _b : null;
4879
+ // A native modal's backdrop is drawn by the browser rather than being an
4880
+ // element; hit-testing it lands on the dialog itself.
4881
+ function isInNativeDialog(element) {
4882
+ return element.closest("dialog[open]") !== null;
3908
4883
  }
3909
- const store = createStore(getAllPcoMeta());
3910
- const recentlyVisitableMetadataStore = createStore({
3911
- metadata: getRecentlyVisitableMetadata()
3912
- });
3913
- function sync() {
3914
- const nextState = getAllPcoMeta();
3915
- if (!shallowCompare(store.getState(), nextState)) store.setState(nextState);
3916
- const metadata = getRecentlyVisitableMetadata();
3917
- if (recentlyVisitableMetadataStore.getState().metadata !== metadata) {
3918
- recentlyVisitableMetadataStore.setState({
3919
- metadata
3920
- });
4884
+ function hasBackdrop(element, width, height) {
4885
+ let current = element;
4886
+ while (current && !isBackdrop(current, width, height)) {
4887
+ current = current.parentElement;
3921
4888
  }
4889
+ return current !== null;
3922
4890
  }
3923
- const observer = new MutationObserver(sync);
3924
- let hasTriggeredObserve = false;
3925
- function observe() {
3926
- if (hasTriggeredObserve) return;
3927
- hasTriggeredObserve = true;
3928
- // The module-load snapshot above can predate meta tags that appear later
3929
- // in <head> (e.g. pco:resource, yielded right before </head>) if this
3930
- // module's own script tag isn't deferred and runs mid-parse. Those tags
3931
- // never mutate afterward, so the observer alone would never catch them.
3932
- // Re-read once now, since observe() is only called once a consumer is
3933
- // actually mounting, which is always after the document has fully parsed.
3934
- sync();
3935
- observer.observe(document.head, {
3936
- attributes: true,
3937
- childList: true,
3938
- subtree: true
3939
- });
3940
- }
3941
- function usePcoMeta() {
3942
- observe();
3943
- return useSyncExternalStore(store.subscribe, () => store.getState());
4891
+ // Some backdrops start below the topbar rather than at the top of the screen,
4892
+ // so the layer only has to span the width and reach from the center to the
4893
+ // bottom. Its own box has to reach the center: a bar along the bottom can
4894
+ // hold a panel that pops up over the center, like Services' media player.
4895
+ // react-aria leaves a closed modal's backdrop up while it fades out, search's
4896
+ // own included, so one that's on its way out doesn't count.
4897
+ function isBackdrop(element, width, height) {
4898
+ const rect = element.getBoundingClientRect();
4899
+ return !element.matches("[data-exiting]") && getComputedStyle(element).position === "fixed" && rect.width >= width - SCROLLBAR_SLACK && rect.top <= height / 2 && rect.bottom >= height - ROUNDING_SLACK;
3944
4900
  }
3945
- function useRecentlyVisitableMetadata() {
3946
- observe();
3947
- return useSyncExternalStore(recentlyVisitableMetadataStore.subscribe, () => recentlyVisitableMetadataStore.getState().metadata);
4901
+ // react-aria keeps a classic scrollbar's gutter reserved while its modals lock
4902
+ // scrolling, so their backdrops stop short of the right edge by its width.
4903
+ const SCROLLBAR_SLACK = 24;
4904
+ // Rects are fractional at non-100% zoom, but clientWidth and clientHeight are
4905
+ // rounded to whole pixels.
4906
+ const ROUNDING_SLACK = 1;
4907
+
4908
+ function useSearchShortcut(enabled, open, setOpen) {
4909
+ React__default.useEffect(() => {
4910
+ if (!enabled) return undefined;
4911
+ // Rich text editors like Trix bind ⌘K to "insert link"; capturing at the
4912
+ // window and stopping propagation keeps them from ever seeing it. Under a
4913
+ // product's modal, search stays shut rather than stacking a second modal,
4914
+ // and the keystroke goes on to the product.
4915
+ const handleKeyDown = event => {
4916
+ if (open && isSearchShortcut(event)) {
4917
+ event.preventDefault();
4918
+ event.stopPropagation();
4919
+ setOpen(false);
4920
+ } else if (!open && (isSearchShortcut(event) || isLegacySearchShortcut(event)) && !isModalOpen()) {
4921
+ event.preventDefault();
4922
+ event.stopPropagation();
4923
+ setOpen(true);
4924
+ }
4925
+ };
4926
+ window.addEventListener("keydown", handleKeyDown, {
4927
+ capture: true
4928
+ });
4929
+ return () => window.removeEventListener("keydown", handleKeyDown, {
4930
+ capture: true
4931
+ });
4932
+ }, [enabled, open, setOpen]);
3948
4933
  }
3949
4934
 
3950
- const MAX_ENTRIES = 20;
3951
- const FORMAT_VERSION = 1;
3952
- const MAX_TEXT_LENGTH = 200;
3953
- const PRODUCTS = ["accounts", "calendar", "check-ins", "giving", "groups", "home", "login", "people", "publishing", "registrations", "services"];
3954
- function isId(value) {
3955
- return value !== "" && Array.from(value).every(character => character >= "0" && character <= "9");
3956
- }
3957
- // A PCRL looks like `<product>/v2/<resource_type>/<id>`.
3958
- function isPcrl(value) {
3959
- const segments = value.split("/");
3960
- return segments.length === 4 && PRODUCTS.some(product => product === segments[0]) && isId(segments[3]);
3961
- }
3962
- function isNonBlankString(value) {
3963
- return typeof value === "string" && value.trim() !== "";
3964
- }
3965
- function isRecord(value) {
3966
- return typeof value === "object" && value !== null && !Array.isArray(value);
3967
- }
3968
- function entryKey(entry) {
3969
- if (entry.type === "resource") {
3970
- return `resource:${entry.pcrl}`;
3971
- } else {
3972
- return `page:${entry.product}:${entry.path}`;
3973
- }
3974
- }
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;
3984
- }
3985
- }
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("");
4935
+ const PERMISSION_CHECKS = {
4936
+ "check-ins": () => fetchMe("check-ins", "permission").then(({
4937
+ permission
4938
+ }) => permission !== null),
4939
+ giving: personId => {
4940
+ if (personId === undefined) {
4941
+ return Promise.reject(new Error("No person to check Giving access for"));
4942
+ } else {
4943
+ return fetchAttributes(`giving/v2/person_permissions/${personId}`, {
4944
+ "fields[PersonPermissions]": "can_search"
4945
+ }).then(({
4946
+ can_search
4947
+ }) => can_search);
4948
+ }
4949
+ },
4950
+ people: () => fetchMe("people", "people_permissions,site_administrator").then(({
4951
+ people_permissions,
4952
+ site_administrator
4953
+ }) => site_administrator || people_permissions !== null),
4954
+ registrations: () => fetchMe("registrations", "can_search").then(({
4955
+ can_search
4956
+ }) => can_search),
4957
+ services: () => fetchMe("services", "can_search").then(({
4958
+ can_search
4959
+ }) => can_search)
4960
+ };
4961
+ function fetchMe(product, fields) {
4962
+ return fetchAttributes(`${product}/v2/me`, {
4963
+ "fields[Person]": fields
4964
+ });
3989
4965
  }
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)
3998
- };
3999
- }
4966
+ function fetchAttributes(path, params) {
4967
+ const url = `${window.location.origin}/~api/${path}?${new URLSearchParams(params)}`;
4968
+ return apiRequest(defaultFetch, url, {
4969
+ headers: apiVersionHeaders(url)
4970
+ }).then(({
4971
+ json,
4972
+ ok
4973
+ }) => {
4974
+ if (!ok || json === null) throw new Error("Permission check failed");
4975
+ return json.data.attributes;
4976
+ });
4000
4977
  }
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);
4978
+ // Permissions rarely change mid-session, so a successful check is kept until
4979
+ // the page reloads. A failed one is dropped so a remount tries again.
4980
+ const permissionCache = new Map();
4981
+ function canSearch(slug, personId) {
4982
+ const check = PERMISSION_CHECKS[slug];
4983
+ const cached = permissionCache.get(slug);
4984
+ if (!check) {
4985
+ return Promise.resolve(true);
4986
+ } else if (cached) {
4987
+ return cached;
4012
4988
  } else {
4013
- return [];
4989
+ const promise = check(personId).catch(() => {
4990
+ permissionCache.delete(slug);
4991
+ return false;
4992
+ });
4993
+ permissionCache.set(slug, promise);
4994
+ return promise;
4014
4995
  }
4015
4996
  }
4016
- function addVisit(entries, visit) {
4017
- const key = entryKey(visit);
4018
- return [clampEntry(visit), ...entries.filter(entry => entryKey(entry) !== key)].slice(0, MAX_ENTRIES);
4019
- }
4020
- function serializeRecentlyVisited(entries, personId) {
4021
- return JSON.stringify({
4022
- entries,
4023
- person: personId,
4024
- v: FORMAT_VERSION
4025
- });
4026
- }
4027
4997
 
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;
4034
- }
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 "";
4042
- }
4043
- }
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"
4998
+ function useSearchableProducts(apps) {
4999
+ // Keyed by name because providers hand over a fresh `apps` array on renders
5000
+ // where nothing changed.
5001
+ const accessibleKey = inSwitcherOrder(apps).map(({
5002
+ slug
5003
+ }) => slug).join(",");
5004
+ const personId = useCurrentPersonId();
5005
+ const [settled, setSettled] = React__default.useState();
5006
+ React__default.useEffect(() => {
5007
+ let current = true;
5008
+ const accessible = accessibleKey.split(",").flatMap(slug => SEARCHABLE_PRODUCTS.filter(product => product.slug === slug));
5009
+ Promise.all(accessible.map(({
5010
+ slug
5011
+ }) => canSearch(slug, personId))).then(allowed => {
5012
+ if (current) {
5013
+ setSettled({
5014
+ accessibleKey,
5015
+ personId,
5016
+ products: accessible.filter((_, index) => allowed[index])
5017
+ });
5018
+ }
5019
+ });
5020
+ return () => {
5021
+ current = false;
4056
5022
  };
4057
- } else if (title && title.trim() !== "" && product) {
5023
+ }, [accessibleKey, personId]);
5024
+ if ((settled === null || settled === void 0 ? void 0 : settled.accessibleKey) === accessibleKey && settled.personId === personId) {
4058
5025
  return {
4059
- details: detailsFromMetadata(metadata),
4060
- path: window.location.pathname,
4061
- product,
4062
- title,
4063
- type: "page"
5026
+ products: settled.products,
5027
+ status: "done"
4064
5028
  };
4065
5029
  } else {
4066
- return null;
5030
+ return {
5031
+ status: "loading"
5032
+ };
4067
5033
  }
4068
5034
  }
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]);
5035
+ function inSwitcherOrder(apps) {
5036
+ return groupApps(apps).restProducts.flatMap(app => SEARCHABLE_PRODUCTS.filter(product => product.appName === app.name));
4083
5037
  }
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);
5038
+
5039
+ /**
5040
+ * Writes the height of the visible part of the page to the parent of `ref`'s
5041
+ * element as `--visual-viewport-height`. It shrinks when an on-screen keyboard
5042
+ * opens; `100dvh` doesn't, since mobile Safari draws the keyboard over it.
5043
+ */
5044
+ function useVisualViewportHeight(ref, enabled) {
5045
+ React__default.useEffect(() => {
5046
+ var _a;
5047
+ const viewport = window.visualViewport;
5048
+ const parent = (_a = ref.current) === null || _a === void 0 ? void 0 : _a.parentElement;
5049
+ if (!enabled || !viewport || !parent) return undefined;
5050
+ const update = () => parent.style.setProperty("--visual-viewport-height", `${viewport.height}px`);
5051
+ update();
5052
+ viewport.addEventListener("resize", update);
5053
+ return () => {
5054
+ viewport.removeEventListener("resize", update);
5055
+ parent.style.removeProperty("--visual-viewport-height");
5056
+ };
5057
+ }, [enabled, ref]);
4091
5058
  }
4092
5059
 
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
5060
+ function ProductSearch({
5061
+ apps,
5062
+ mobile = false,
5063
+ env
4099
5064
  }) {
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
- }
5065
+ const searchable = useSearchableProducts(apps);
5066
+ const [open, setOpen] = React__default.useState(false);
5067
+ const modalRef = React__default.useRef(null);
5068
+ // Sizes Tapestry's modal frame, the dialog's parent, to fit above the keyboard.
5069
+ useVisualViewportHeight(modalRef, mobile && open);
5070
+ const available = searchable.status === "done" && searchable.products.length > 0;
5071
+ useSearchShortcut(available, open, setOpen);
5072
+ if (!available) return null;
5073
+ return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(SearchButton, {
5074
+ mobile: mobile,
5075
+ onClick: () => setOpen(true)
5076
+ }), /*#__PURE__*/React__default.createElement(Modal, {
5077
+ "aria-label": "Search",
5078
+ className: classnames(styles$i.modal, {
5079
+ [styles$i.mobile]: mobile
5080
+ }),
5081
+ dismissible: true,
5082
+ onClose: () => setOpen(false),
5083
+ open: open,
5084
+ ref: modalRef,
5085
+ size: "lg"
5086
+ }, /*#__PURE__*/React__default.createElement(SearchPanel, {
5087
+ mobile: mobile,
5088
+ env: env,
5089
+ products: searchable.products
5090
+ })));
4125
5091
  }
4126
5092
 
4127
5093
  function useRecordRecentlyVisited({
@@ -4166,9 +5132,9 @@ function StyledRoot({
4166
5132
  `), children);
4167
5133
  }
4168
5134
 
4169
- var styles$9 = {"route":"route--8BWe","is-active":"is-active-FsU9Y","apps-routes-separator":"apps-routes-separator-Nlm-N"};
5135
+ var styles$a = {"route":"route--8BWe","is-active":"is-active-FsU9Y","apps-routes-separator":"apps-routes-separator-Nlm-N"};
4170
5136
 
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"};
5137
+ 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
5138
 
4173
5139
  const UserMenu = ({
4174
5140
  appName,
@@ -4187,7 +5153,7 @@ const UserMenu = ({
4187
5153
  component: Outsider,
4188
5154
  onOutsideClick: () => setUserMenuVisible(false),
4189
5155
  cleanup: !userMenuVisible,
4190
- className: styles$8.popup,
5156
+ className: styles$9.popup,
4191
5157
  isVisible: userMenuVisible
4192
5158
  }, /*#__PURE__*/React__default.createElement("div", null, hasConnectedPeople ? (/*#__PURE__*/React__default.createElement("div", {
4193
5159
  style: {
@@ -4295,7 +5261,7 @@ const UserMenu = ({
4295
5261
  }, /*#__PURE__*/React__default.createElement(Text, {
4296
5262
  color: "var(--t-text-color-secondary)",
4297
5263
  lineHeight: "24px"
4298
- }, "Log out"), /*#__PURE__*/React__default.createElement(Icon, {
5264
+ }, "Log out"), /*#__PURE__*/React__default.createElement(Icon$1, {
4299
5265
  name: "general.intoBox",
4300
5266
  fill: "var(--t-icon-color-secondary)"
4301
5267
  })))))));
@@ -4331,11 +5297,11 @@ const UserMenuTrigger = ({
4331
5297
  userMenuVisible
4332
5298
  }) => {
4333
5299
  return /*#__PURE__*/React__default.createElement("div", {
4334
- className: styles$8["trigger-wrap"],
5300
+ className: styles$9["trigger-wrap"],
4335
5301
  "data-pendo": "topbar-org-switcher"
4336
5302
  }, /*#__PURE__*/React__default.createElement(Hoverable, {
4337
- className: classnames(styles$8["trigger"], {
4338
- [styles$8["is-visible"]]: userMenuVisible
5303
+ className: classnames(styles$9["trigger"], {
5304
+ [styles$9["is-visible"]]: userMenuVisible
4339
5305
  }),
4340
5306
  component: "button",
4341
5307
  type: "button",
@@ -4347,7 +5313,7 @@ const UserMenuTrigger = ({
4347
5313
  requestConnectedPeopleFetch();
4348
5314
  }
4349
5315
  }, showOrgName && (/*#__PURE__*/React__default.createElement("span", {
4350
- className: styles$8["trigger-orgname"]
5316
+ className: styles$9["trigger-orgname"]
4351
5317
  }, orgName)), /*#__PURE__*/React__default.createElement(DisclosureChevronIcon, null)));
4352
5318
  };
4353
5319
 
@@ -4407,13 +5373,14 @@ const NotSmallTopbar = ({
4407
5373
  colors: props.colors,
4408
5374
  requestAppsFetch: props.requestAppsFetch
4409
5375
  })), /*#__PURE__*/React__default.createElement("div", {
4410
- className: styles$9["apps-routes-separator"]
5376
+ className: styles$a["apps-routes-separator"]
4411
5377
  }), routesVisible && props.routes, /*#__PURE__*/React__default.createElement("div", {
4412
5378
  style: {
4413
5379
  margin: "auto"
4414
5380
  }
4415
5381
  }), !notificationsEnabled && notifications(), productSearchEnabled ? (/*#__PURE__*/React__default.createElement(ProductSearch, {
4416
- apps: props.apps
5382
+ apps: props.apps,
5383
+ env: props.env
4417
5384
  })) : search({
4418
5385
  hideRoutes: () => setRoutesVisible(false),
4419
5386
  showRoutes: () => setRoutesVisible(true)
@@ -4447,12 +5414,12 @@ const NotSmallRoute = ({
4447
5414
  ...nativeProps
4448
5415
  }) => {
4449
5416
  return /*#__PURE__*/React__default.createElement("a", {
4450
- className: classnames(styles$9["route"], className, active && styles$9["is-active"]),
5417
+ className: classnames(styles$a["route"], className, active && styles$a["is-active"]),
4451
5418
  ...nativeProps
4452
5419
  });
4453
5420
  };
4454
5421
 
4455
- var styles$7 = {"avatar":"avatar-tW-wn"};
5422
+ var styles$8 = {"avatar":"avatar-tW-wn"};
4456
5423
 
4457
5424
  function Avatar({
4458
5425
  env,
@@ -4462,7 +5429,7 @@ function Avatar({
4462
5429
  }) {
4463
5430
  const url = incomingUrl || `${pcoUrl$1(env)("people")}/static/no_photo_thumbnail_gray.svg`;
4464
5431
  return /*#__PURE__*/React__default.createElement("div", {
4465
- className: styles$7.avatar,
5432
+ className: styles$8.avatar,
4466
5433
  style: {
4467
5434
  backgroundImage: `url(${url})`,
4468
5435
  ...style
@@ -32221,7 +33188,7 @@ function requireReactDom () {
32221
33188
 
32222
33189
  var reactDomExports = requireReactDom();
32223
33190
 
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"};
33191
+ 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
33192
 
32226
33193
  const AppsSwitcher = ({
32227
33194
  appName,
@@ -32249,8 +33216,8 @@ const AppsSwitcher = ({
32249
33216
  }, /*#__PURE__*/React__default.createElement("a", {
32250
33217
  "aria-current": name === appName ? "page" : undefined,
32251
33218
  "aria-label": name,
32252
- className: classnames(styles$6.link, {
32253
- [styles$6.current]: name === appName
33219
+ className: classnames(styles$7.link, {
33220
+ [styles$7.current]: name === appName
32254
33221
  }),
32255
33222
  "data-pendo": `app-switcher--change-app--${name}`,
32256
33223
  "data-turbolinks": false,
@@ -32262,48 +33229,48 @@ const AppsSwitcher = ({
32262
33229
  "aria-expanded": visible,
32263
33230
  "aria-haspopup": "true",
32264
33231
  "aria-label": `App switcher: ${appName}`,
32265
- className: styles$6.trigger,
33232
+ className: styles$7.trigger,
32266
33233
  onClick: onTriggerClick,
32267
33234
  type: "button"
32268
33235
  }, /*#__PURE__*/React__default.createElement(ColorAppIcon, {
32269
33236
  appName: appName.replace(/[\s-]/, "")
32270
33237
  })), /*#__PURE__*/reactDomExports.createPortal(/*#__PURE__*/React__default.createElement("div", {
32271
33238
  ref: containerRef,
32272
- className: classnames(styles$6.layer, {
32273
- [styles$6["layer--visible"]]: visible
33239
+ className: classnames(styles$7.layer, {
33240
+ [styles$7["layer--visible"]]: visible
32274
33241
  }),
32275
33242
  onClick: toggle,
32276
33243
  onKeyDown: handleKeyDown
32277
33244
  }, /*#__PURE__*/React__default.createElement("nav", {
32278
33245
  "aria-label": "Switch app",
32279
33246
  "aria-modal": visible,
32280
- className: styles$6.panel,
33247
+ className: styles$7.panel,
32281
33248
  "data-pendo": "app-switcher",
32282
33249
  onClick: event => event.stopPropagation(),
32283
33250
  role: "dialog"
32284
33251
  }, /*#__PURE__*/React__default.createElement("div", {
32285
- className: styles$6.header
33252
+ className: styles$7.header
32286
33253
  }, /*#__PURE__*/React__default.createElement("span", {
32287
- className: styles$6.heading
33254
+ className: styles$7.heading
32288
33255
  }, "Switch to"), /*#__PURE__*/React__default.createElement("button", {
32289
33256
  "aria-label": "Close",
32290
- className: styles$6.close,
33257
+ className: styles$7.close,
32291
33258
  onClick: toggle,
32292
33259
  type: "button"
32293
33260
  }, /*#__PURE__*/React__default.createElement(X, {
32294
33261
  "aria-hidden": "true",
32295
33262
  fill: "var(--t-icon-color-dim)"
32296
33263
  }))), /*#__PURE__*/React__default.createElement("ul", {
32297
- className: styles$6.list,
33264
+ className: styles$7.list,
32298
33265
  role: "list"
32299
33266
  }, homeProduct && productLink(homeProduct.name), restProducts.map(({
32300
33267
  name
32301
33268
  }) => productLink(name)), accountsProduct && (/*#__PURE__*/React__default.createElement("li", {
32302
- className: styles$6["accounts-item"]
33269
+ className: styles$7["accounts-item"]
32303
33270
  }, /*#__PURE__*/React__default.createElement("a", {
32304
33271
  "aria-current": appName === "Accounts" ? "page" : undefined,
32305
33272
  "aria-label": "Account settings",
32306
- className: classnames(styles$6.link, styles$6["accounts-link"]),
33273
+ className: classnames(styles$7.link, styles$7["accounts-link"]),
32307
33274
  "data-pendo": "app-switcher--change-app--Accounts",
32308
33275
  "data-turbolinks": false,
32309
33276
  href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(accountsProduct.name)
@@ -32326,14 +33293,14 @@ ClientStorage.defaultProps = {
32326
33293
  method: "localStorage"
32327
33294
  };
32328
33295
 
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"};
33296
+ var styles$6 = {"appmenu-button":"appmenu-button-b0UvT","appmenu-layer":"appmenu-layer-0IQpN","appmenu-container":"appmenu-container-gackd","appmenu-scroll":"appmenu-scroll-mPSx0","appmenu-list":"appmenu-list-h7clX","appmenu-list-a":"appmenu-list-a-rGIQN","appmenu-list-a--top":"appmenu-list-a--top-RT-2F","appmenu-list-a--bottom":"appmenu-list-a--bottom-YDeBC","appmenu-close-button":"appmenu-close-button-vv9bV","current-route":"current-route-wpJgz","is-active":"is-active-wy8Bg","routes":"routes-Nmt1S","route":"route-kkknQ","actions":"actions-INQ7-","profile-button":"profile-button-a2WO2"};
32330
33297
 
32331
33298
  function AccountSettingsLink({
32332
33299
  env
32333
33300
  }) {
32334
33301
  const accountsName = "accounts";
32335
33302
  return /*#__PURE__*/React__default.createElement("a", {
32336
- className: classnames(styles$5["appmenu-list-a"], styles$5["appmenu-list-a--bottom"]),
33303
+ className: classnames(styles$6["appmenu-list-a"], styles$6["appmenu-list-a--bottom"]),
32337
33304
  "data-pendo": `app-switcher--change-app--${accountsName}`,
32338
33305
  "data-turbolinks": false,
32339
33306
  href: pcoUrl$1(env)(accountsName) + getAppsSwitchProductPath(accountsName)
@@ -32356,7 +33323,7 @@ const CloseMenuButton = ({
32356
33323
  }) => (/*#__PURE__*/React__default.createElement("button", {
32357
33324
  "aria-label": "Close menu",
32358
33325
  type: "button",
32359
- className: styles$5["appmenu-close-button"],
33326
+ className: styles$6["appmenu-close-button"],
32360
33327
  style: style
32361
33328
  }, /*#__PURE__*/React__default.createElement(X, {
32362
33329
  fill: color,
@@ -32417,7 +33384,7 @@ const LegacyAppsSwitcher = ({
32417
33384
  "aria-haspopup": "true",
32418
33385
  "aria-label": `App switcher: ${appName}`,
32419
33386
  type: "button",
32420
- className: styles$5["appmenu-button"],
33387
+ className: styles$6["appmenu-button"],
32421
33388
  onClick: onTriggerClick
32422
33389
  }, /*#__PURE__*/React__default.createElement(ColorAppIcon, {
32423
33390
  appName: appName.replace(/[\s-]/, "")
@@ -32425,7 +33392,7 @@ const LegacyAppsSwitcher = ({
32425
33392
  item: "Topbar:Small:Menus:Products:visibility",
32426
33393
  render: () => (/*#__PURE__*/React__default.createElement("div", {
32427
33394
  ref: appMenuRef,
32428
- className: styles$5["appmenu-layer"],
33395
+ className: styles$6["appmenu-layer"],
32429
33396
  style: {
32430
33397
  ...(visible && {
32431
33398
  display: "block"
@@ -32442,37 +33409,37 @@ const LegacyAppsSwitcher = ({
32442
33409
  e.stopPropagation();
32443
33410
  }
32444
33411
  },
32445
- className: styles$5["appmenu-container"]
33412
+ className: styles$6["appmenu-container"]
32446
33413
  }, /*#__PURE__*/React__default.createElement("a", {
32447
33414
  "aria-current": appName === (homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name) ? "page" : undefined,
32448
33415
  "aria-label": homeProduct ? homeProduct.name : appName,
32449
33416
  "data-pendo": `app-switcher--change-app--${homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name}`,
32450
33417
  "data-turbolinks": false,
32451
- className: classnames(styles$5["appmenu-list-a"], styles$5["appmenu-list-a--top"]),
33418
+ className: classnames(styles$6["appmenu-list-a"], styles$6["appmenu-list-a--top"]),
32452
33419
  href: homeProduct ? pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name) : undefined
32453
33420
  }, /*#__PURE__*/React__default.createElement(UserMenuAppLockup, {
32454
33421
  appName: homeProduct ? homeProduct.name : appName
32455
33422
  })), /*#__PURE__*/React__default.createElement("div", {
32456
- className: styles$5["appmenu-scroll"]
33423
+ className: styles$6["appmenu-scroll"]
32457
33424
  }, /*#__PURE__*/React__default.createElement("ul", {
32458
33425
  "data-pendo": "app-switcher",
32459
- className: styles$5["appmenu-list"]
33426
+ className: styles$6["appmenu-list"]
32460
33427
  }, appsMenuFormatter(restProducts).map(({
32461
33428
  name
32462
33429
  }) => (/*#__PURE__*/React__default.createElement("li", {
32463
33430
  key: name,
32464
- className: styles$5["appmenu-list-li"]
33431
+ className: styles$6["appmenu-list-li"]
32465
33432
  }, /*#__PURE__*/React__default.createElement("a", {
32466
33433
  "aria-current": name === appName ? "page" : undefined,
32467
33434
  "aria-label": name,
32468
33435
  "data-pendo": `app-switcher--change-app--${name}`,
32469
33436
  "data-turbolinks": false,
32470
- className: styles$5["appmenu-list-a"],
33437
+ className: styles$6["appmenu-list-a"],
32471
33438
  href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(name)
32472
33439
  }, /*#__PURE__*/React__default.createElement(UserMenuAppLockup, {
32473
33440
  appName: name
32474
33441
  }))))), accountsProduct && (/*#__PURE__*/React__default.createElement("li", {
32475
- className: styles$5["appmenu-list-li"]
33442
+ className: styles$6["appmenu-list-li"]
32476
33443
  }, /*#__PURE__*/React__default.createElement(AccountSettingsLink, {
32477
33444
  env: env
32478
33445
  }))))))))
@@ -32536,19 +33503,25 @@ const SmallTopbar = ({
32536
33503
  toggle: toggleProductSwitcher,
32537
33504
  visible: productSwitcherVisible
32538
33505
  })), routesMenuVisible && (/*#__PURE__*/React__default.createElement("div", {
32539
- className: styles$5["routes"]
33506
+ className: styles$6["routes"]
32540
33507
  }, props.routes)), /*#__PURE__*/React__default.createElement("button", {
32541
33508
  type: "button",
32542
- className: classnames(styles$5["current-route"], {
32543
- [styles$5["is-active"]]: routesMenuVisible
33509
+ className: classnames(styles$6["current-route"], {
33510
+ [styles$6["is-active"]]: routesMenuVisible
32544
33511
  }),
32545
33512
  onClick: props.routes.length > 0 ? () => {
32546
33513
  setProductSwitcherVisible(false);
32547
33514
  setRoutesMenuVisible(visible => !visible);
32548
33515
  } : 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", {
33516
+ }, /*#__PURE__*/React__default.createElement(CurrentRouteComponent, null, capitalize$1(currentRouteText)), props.routes.length > 0 && /*#__PURE__*/React__default.createElement(DisclosureChevronIcon, null)), /*#__PURE__*/React__default.createElement("div", {
33517
+ className: styles$6["actions"]
33518
+ }, productSearchEnabled && (/*#__PURE__*/React__default.createElement(ProductSearch, {
33519
+ apps: props.apps,
33520
+ mobile: true,
33521
+ env: props.env
33522
+ })), /*#__PURE__*/React__default.createElement("button", {
32550
33523
  type: "button",
32551
- className: styles$5["profile-button"],
33524
+ className: styles$6["profile-button"],
32552
33525
  onClick: () => {
32553
33526
  var _a, _b;
32554
33527
  props.requestConnectedPeopleFetch();
@@ -32557,14 +33530,14 @@ const SmallTopbar = ({
32557
33530
  }, /*#__PURE__*/React__default.createElement(Avatar, {
32558
33531
  env: props.env,
32559
33532
  url: props.userAvatarPath
32560
- })));
33533
+ }))));
32561
33534
  };
32562
33535
  const SmallRoute = ({
32563
33536
  active,
32564
33537
  className,
32565
33538
  ...props
32566
33539
  }) => (/*#__PURE__*/React__default.createElement("a", {
32567
- className: classnames(styles$5["route"], className, active && styles$5["is-active"]),
33540
+ className: classnames(styles$6["route"], className, active && styles$6["is-active"]),
32568
33541
  ...props
32569
33542
  }));
32570
33543
 
@@ -32703,7 +33676,7 @@ const topbarTheme = {
32703
33676
  id: "topbar"
32704
33677
  };
32705
33678
 
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"};
33679
+ 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
33680
 
32708
33681
  function isPresent(value) {
32709
33682
  return value !== undefined && value.trim() !== "";
@@ -32732,15 +33705,31 @@ function Field({
32732
33705
  value
32733
33706
  }) {
32734
33707
  return /*#__PURE__*/React__default.createElement("div", {
32735
- className: styles$4["debug-pco-resource-field"]
33708
+ className: styles$5["debug-pco-resource-field"]
32736
33709
  }, /*#__PURE__*/React__default.createElement("span", {
32737
- className: styles$4["debug-pco-resource-field-label"]
33710
+ className: styles$5["debug-pco-resource-field-label"]
32738
33711
  }, label), /*#__PURE__*/React__default.createElement("span", {
32739
- className: styles$4["debug-pco-resource-field-value"]
33712
+ className: styles$5["debug-pco-resource-field-value"]
32740
33713
  }, value));
32741
33714
  }
33715
+ // Only staff ghosting or impersonating a person get pco:staff-access-level,
33716
+ // which keeps the panel away from regular users on staging.
32742
33717
  function DebugPcoResource({
32743
33718
  env
33719
+ }) {
33720
+ const {
33721
+ "staff-access-level": staffAccessLevel
33722
+ } = usePcoMeta();
33723
+ if (isPresent(staffAccessLevel)) {
33724
+ return /*#__PURE__*/React__default.createElement(DebugPanel, {
33725
+ env: env
33726
+ });
33727
+ } else {
33728
+ return null;
33729
+ }
33730
+ }
33731
+ function DebugPanel({
33732
+ env
32744
33733
  }) {
32745
33734
  const {
32746
33735
  recently_visitable: recentlyVisitable,
@@ -32749,12 +33738,17 @@ function DebugPcoResource({
32749
33738
  const [lookup, setLookup] = useState({
32750
33739
  status: "idle"
32751
33740
  });
32752
- const hasResource = isPresent(resource);
33741
+ const visit = useCurrentVisit();
33742
+ const personId = useCurrentPersonId();
33743
+ const recentlyVisited = useRecentlyVisited({
33744
+ env,
33745
+ personId
33746
+ });
33747
+ const pcrl = (visit === null || visit === void 0 ? void 0 : visit.type) === "resource" ? visit.pcrl : null;
32753
33748
  const hasRecentlyVisitable = isPresent(recentlyVisitable);
33749
+ const hasInvalidResource = isPresent(resource) && !isPcrl(resource);
32754
33750
  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;
33751
+ const url = pcrl ? pcrlLookupUrl(pcrl, env) : null;
32758
33752
  if (!url) {
32759
33753
  setLookup({
32760
33754
  status: "idle"
@@ -32786,37 +33780,45 @@ function DebugPcoResource({
32786
33780
  return () => {
32787
33781
  cancelled = true;
32788
33782
  };
32789
- }, [resource, env, hasResource]);
33783
+ }, [pcrl, env]);
32790
33784
  return /*#__PURE__*/React__default.createElement("div", {
32791
- className: classnames("staff-only", styles$4["debug-pco-resource"])
33785
+ className: classnames("staff-only", styles$5["debug-pco-resource"])
32792
33786
  }, /*#__PURE__*/React__default.createElement("div", {
32793
- className: styles$4["debug-pco-resource-header"]
33787
+ className: styles$5["debug-pco-resource-header"]
32794
33788
  }, /*#__PURE__*/React__default.createElement("span", {
32795
- className: styles$4["debug-pco-resource-badge"]
33789
+ className: styles$5["debug-pco-resource-badge"]
32796
33790
  }, "Staff"), /*#__PURE__*/React__default.createElement("span", {
32797
- className: styles$4["debug-pco-resource-title"]
33791
+ className: styles$5["debug-pco-resource-title"]
32798
33792
  }, "Recently Visited debug")), /*#__PURE__*/React__default.createElement(Field, {
32799
33793
  label: "pco:resource",
32800
33794
  value: describeTag("resource", resource)
32801
33795
  }), /*#__PURE__*/React__default.createElement(Field, {
32802
33796
  label: "pco:recently_visitable",
32803
33797
  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"};
33798
+ }), hasInvalidResource && (/*#__PURE__*/React__default.createElement("p", {
33799
+ className: styles$5["debug-pco-resource-note"]
33800
+ }, "pco:resource isn't a PCRL (product/v2/type/id), so Recently Visited ignores it.")), visit === null && (/*#__PURE__*/React__default.createElement("p", {
33801
+ className: styles$5["debug-pco-resource-note"]
33802
+ }, "This page is not eligible for Recently Visited")), pcrl && hasRecentlyVisitable && (/*#__PURE__*/React__default.createElement("p", {
33803
+ className: styles$5["debug-pco-resource-note"]
33804
+ }, "Both tags are present \u2014 pco:resource takes precedence for the Recently Visited entry.")), pcrl && lookup.status === "loading" && (/*#__PURE__*/React__default.createElement("p", {
33805
+ className: styles$5["debug-pco-resource-note"]
33806
+ }, "Looking up resource\u2026")), pcrl && lookup.status === "error" && (/*#__PURE__*/React__default.createElement("p", {
33807
+ className: styles$5["debug-pco-resource-note"]
33808
+ }, lookup.message)), pcrl && lookup.status === "loaded" && (/*#__PURE__*/React__default.createElement("pre", {
33809
+ className: styles$5["debug-pco-resource-json"]
33810
+ }, JSON.stringify(lookup.json, null, 2))), (visit === null || visit === void 0 ? void 0 : visit.type) === "page" && (/*#__PURE__*/React__default.createElement("p", {
33811
+ className: styles$5["debug-pco-resource-note"]
33812
+ }, "Recently Visited title: ", visit.title)), /*#__PURE__*/React__default.createElement("span", {
33813
+ className: styles$5["debug-pco-resource-field-label"]
33814
+ }, "Recently Visited"), personId ? (/*#__PURE__*/React__default.createElement("pre", {
33815
+ className: styles$5["debug-pco-resource-json"]
33816
+ }, JSON.stringify(recentlyVisited, null, 2))) : (/*#__PURE__*/React__default.createElement("p", {
33817
+ className: styles$5["debug-pco-resource-note"]
33818
+ }, "No current person, so nothing is recorded.")));
33819
+ }
33820
+
33821
+ var styles$4 = {"drawer-wrap":"drawer-wrap-J0rSL","is-visible":"is-visible-KTME8","drawer":"drawer-j7f4l"};
32820
33822
 
32821
33823
  function Drawer({
32822
33824
  children,
@@ -32824,7 +33826,7 @@ function Drawer({
32824
33826
  }) {
32825
33827
  return /*#__PURE__*/React__default.createElement("div", {
32826
33828
  id: id,
32827
- className: styles$3["drawer"]
33829
+ className: styles$4["drawer"]
32828
33830
  }, children);
32829
33831
  }
32830
33832
  function DrawerWrap({
@@ -32834,7 +33836,7 @@ function DrawerWrap({
32834
33836
  ...props
32835
33837
  }) {
32836
33838
  return /*#__PURE__*/React__default.createElement("div", {
32837
- className: classnames(styles$3["drawer-wrap"], className, isVisible && styles$3["is-visible"]),
33839
+ className: classnames(styles$4["drawer-wrap"], className, isVisible && styles$4["is-visible"]),
32838
33840
  ...props
32839
33841
  }, children);
32840
33842
  }
@@ -33018,15 +34020,18 @@ const SwitchView = ({
33018
34020
  }));
33019
34021
  };
33020
34022
 
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"};
34023
+ 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
34024
 
33023
34025
  const COLOR_MODES = [{
34026
+ icon: "toolbar.sun",
33024
34027
  label: "Light",
33025
34028
  value: "light"
33026
34029
  }, {
34030
+ icon: "toolbar.moon",
33027
34031
  label: "Dark",
33028
34032
  value: "dark"
33029
34033
  }, {
34034
+ icon: "toolbar.display",
33030
34035
  label: "System",
33031
34036
  value: "system"
33032
34037
  }];
@@ -33038,14 +34043,15 @@ function ColorModeSwitcher() {
33038
34043
  const updateStore = useUpdateStore();
33039
34044
  const [counter] = useState(() => COLOR_MODE_INSTANCE_COUNTER++);
33040
34045
  return /*#__PURE__*/React__default.createElement("fieldset", {
33041
- className: styles$2["color-mode-switcher"]
34046
+ className: styles$3["color-mode-switcher"]
33042
34047
  }, /*#__PURE__*/React__default.createElement("legend", {
33043
- className: styles$2["color-mode-switcher-label"]
34048
+ className: styles$3["color-mode-switcher-label"]
33044
34049
  }, "Appearance", " "), /*#__PURE__*/React__default.createElement("div", {
33045
- className: styles$2["color-mode-switcher-modes"]
34050
+ className: styles$3["color-mode-switcher-modes"]
33046
34051
  }, COLOR_MODES.map(({
33047
- value,
33048
- label
34052
+ icon,
34053
+ label,
34054
+ value
33049
34055
  }) => (/*#__PURE__*/React__default.createElement("label", {
33050
34056
  key: value
33051
34057
  }, /*#__PURE__*/React__default.createElement("input", {
@@ -33062,8 +34068,9 @@ function ColorModeSwitcher() {
33062
34068
  document.documentElement.setAttribute(RESOLVED_COLOR_MODE_ATTRIBUTE, resolveColorMode(colorMode));
33063
34069
  updateColorMode(colorMode);
33064
34070
  }
33065
- }), /*#__PURE__*/React__default.createElement(ColorModeIcon, {
33066
- mode: value
34071
+ }), /*#__PURE__*/React__default.createElement(Icon, {
34072
+ name: icon,
34073
+ size: 15
33067
34074
  }), label)))));
33068
34075
  }
33069
34076
  function updateColorMode(mode) {
@@ -33078,28 +34085,6 @@ function updateColorMode(mode) {
33078
34085
  method: "POST"
33079
34086
  });
33080
34087
  }
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
34088
 
33104
34089
  const requestClearConnectedPeopleCache = () => remove();
33105
34090
  function UserProfile({
@@ -33164,8 +34149,8 @@ function UserProfile({
33164
34149
  width: "100%"
33165
34150
  }, /*#__PURE__*/React__default.createElement(Icon, {
33166
34151
  name: mfaConfigured ? "general.lock" : "general.unlock",
33167
- size: "sm",
33168
- color: mfaConfigured ? "var(--t-icon-color-status-success)" : "var(--t-icon-color-status-error)"
34152
+ size: 12,
34153
+ theme: mfaConfigured ? "--t-icon-color-status-success" : "--t-icon-color-status-error"
33169
34154
  }), /*#__PURE__*/React__default.createElement(Text, {
33170
34155
  size: 4,
33171
34156
  weight: 400,
@@ -33215,7 +34200,8 @@ function UserProfile({
33215
34200
  size: 3
33216
34201
  }, "Log out"), /*#__PURE__*/React__default.createElement(Icon, {
33217
34202
  name: "general.intoBox",
33218
- fill: "var(--t-icon-color-secondary)"
34203
+ size: 14,
34204
+ theme: "--t-icon-color-secondary"
33219
34205
  })))), env !== "production" && /*#__PURE__*/React__default.createElement(DebugPcoResource, {
33220
34206
  env: env
33221
34207
  }), hasConnectedPeople && (/*#__PURE__*/React__default.createElement(StackView$2, {
@@ -33277,26 +34263,42 @@ function UserProfile({
33277
34263
 
33278
34264
  var ActionType;
33279
34265
  (function (ActionType) {
34266
+ ActionType["clearPending"] = "clearPending";
33280
34267
  ActionType["close"] = "close";
33281
34268
  ActionType["mount"] = "mount";
33282
34269
  ActionType["open"] = "open";
34270
+ ActionType["setPending"] = "setPending";
33283
34271
  ActionType["unmount"] = "unmount";
33284
34272
  })(ActionType || (ActionType = {}));
33285
34273
  const initialState = {
33286
34274
  currentContent: null,
33287
- open: false
34275
+ open: false,
34276
+ pending: null
33288
34277
  };
33289
34278
  const reducer = (state, action) => {
34279
+ var _a, _b;
33290
34280
  switch (action.type) {
33291
34281
  case ActionType.mount:
33292
34282
  return {
33293
34283
  ...state,
33294
- currentContent: action.payload.content
34284
+ currentContent: action.payload.content,
34285
+ 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
34286
  };
33296
34287
  case ActionType.unmount:
33297
34288
  return {
33298
34289
  ...state,
33299
- currentContent: null
34290
+ currentContent: null,
34291
+ pending: null
34292
+ };
34293
+ case ActionType.setPending:
34294
+ return {
34295
+ ...state,
34296
+ pending: action.payload
34297
+ };
34298
+ case ActionType.clearPending:
34299
+ return {
34300
+ ...state,
34301
+ pending: null
33300
34302
  };
33301
34303
  case ActionType.open:
33302
34304
  return {
@@ -33315,13 +34317,181 @@ const useToolbarReducer = overrides => React.useReducer(reducer, {
33315
34317
  ...overrides
33316
34318
  });
33317
34319
 
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"};
34320
+ 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
34321
 
33320
- const pcoUrl = pcoUrl$1;
34322
+ // PCO Box serves TLS by default, and a bare http:// frame inside an https
34323
+ // product page is blocked as mixed content, so follow the page's scheme.
34324
+ const iframeSrc = () => `${window.location.protocol}//pco.test/iframe`;
34325
+ const STAFF_PINK = "var(--t-fill-color-product-staff)";
34326
+ // The pco.test domain is only ever served by a local PCO Box, so the tool
34327
+ // stays hidden in staging and production — and on Topbar's own dev server,
34328
+ // which runs on localhost and has no pco.test to frame.
34329
+ function isPcoTestHost() {
34330
+ const {
34331
+ hostname
34332
+ } = window.location;
34333
+ return hostname === "pco.test" || hostname.endsWith(".pco.test");
34334
+ }
34335
+ // Flattened from the source artwork's two outline paths — its gradients,
34336
+ // filters and clip paths carry nothing at icon size. Those paths are authored
34337
+ // on a 128px canvas, hence the transform: it fits the pair into the same ~24px
34338
+ // optical box the sibling toolbar icons occupy. Filled explicitly rather than
34339
+ // with `currentColor` so the mark stays staff pink instead of inheriting the
34340
+ // toolbar's text color and its hover/active states.
34341
+ 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";
34342
+ 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";
34343
+ function ToolsIcon() {
34344
+ return /*#__PURE__*/React__default.createElement("svg", {
34345
+ "aria-hidden": "true",
34346
+ width: "32",
34347
+ height: "32",
34348
+ viewBox: "0 0 32 32",
34349
+ xmlns: "http://www.w3.org/2000/svg"
34350
+ }, /*#__PURE__*/React__default.createElement("g", {
34351
+ fill: STAFF_PINK,
34352
+ transform: "translate(2.92 2.66) scale(.204)"
34353
+ }, /*#__PURE__*/React__default.createElement("path", {
34354
+ d: WRENCH
34355
+ }), /*#__PURE__*/React__default.createElement("path", {
34356
+ d: SCREWDRIVER
34357
+ })));
34358
+ }
34359
+ // A cross-origin frame's own history object is unreachable (touching it throws
34360
+ // a SecurityError), but the frame's navigations do land in the top-level joint
34361
+ // session history, so the *parent's* back/forward traverse them while leaving
34362
+ // the host page in place. Counting frame loads tracks how deep we are, which is
34363
+ // what keeps a click on Back from stepping past the frame's first page and
34364
+ // navigating the host app instead. Loads we caused are attributed to the
34365
+ // traversal that requested them rather than counted as new navigations.
34366
+ function useFrameHistory() {
34367
+ const [steps, setSteps] = useState({
34368
+ back: 0,
34369
+ forward: 0
34370
+ });
34371
+ // Traversal has to read the current counts outside of render, so the ref is
34372
+ // the source of truth and state exists to paint the buttons.
34373
+ const stepsRef = useRef(steps);
34374
+ const pendingTraversal = useRef(null);
34375
+ const frameIsLoaded = useRef(false);
34376
+ const moveTo = useCallback(next => {
34377
+ stepsRef.current = next;
34378
+ setSteps(next);
34379
+ }, []);
34380
+ const onFrameLoad = useCallback(() => {
34381
+ if (!frameIsLoaded.current) {
34382
+ // The frame's first page is the floor, not a step.
34383
+ frameIsLoaded.current = true;
34384
+ } else {
34385
+ const traversal = pendingTraversal.current;
34386
+ pendingTraversal.current = null;
34387
+ const {
34388
+ back,
34389
+ forward
34390
+ } = stepsRef.current;
34391
+ if (traversal === "back") {
34392
+ moveTo({
34393
+ back: back - 1,
34394
+ forward: forward + 1
34395
+ });
34396
+ } else if (traversal === "forward") {
34397
+ moveTo({
34398
+ back: back + 1,
34399
+ forward: forward - 1
34400
+ });
34401
+ } else {
34402
+ moveTo({
34403
+ back: back + 1,
34404
+ forward: 0
34405
+ });
34406
+ }
34407
+ }
34408
+ }, [moveTo]);
34409
+ // Guarded here rather than trusting the button's disabled state: traversing
34410
+ // with no frame step left would navigate the host app out from under us.
34411
+ const traverse = useCallback(direction => {
34412
+ if (stepsRef.current[direction] > 0) {
34413
+ pendingTraversal.current = direction;
34414
+ window.history[direction]();
34415
+ }
34416
+ }, []);
34417
+ return {
34418
+ onFrameLoad,
34419
+ steps,
34420
+ traverse
34421
+ };
34422
+ }
34423
+ function NavButton({
34424
+ direction,
34425
+ disabled,
34426
+ onClick
34427
+ }) {
34428
+ return /*#__PURE__*/React__default.createElement(Button$1
34429
+ // Tapestry's `disabled` suppresses the click but emits no ARIA state.
34430
+ , {
34431
+ "aria-disabled": disabled,
34432
+ disabled: disabled,
34433
+ hover: {
34434
+ backgroundColor: "var(--t-fill-color-neutral-070)"
34435
+ },
34436
+ icon: {
34437
+ fill: "var(--t-icon-color-dim)",
34438
+ name: direction === "back" ? "general.leftChevron" : "general.rightChevron",
34439
+ size: "md"
34440
+ },
34441
+ onClick: onClick,
34442
+ opacity: disabled ? 0.35 : 1,
34443
+ theme: false,
34444
+ title: direction === "back" ? "Back" : "Forward"
34445
+ });
34446
+ }
34447
+ function DeveloperToolsComponent({
34448
+ closeDrawer
34449
+ }) {
34450
+ const {
34451
+ onFrameLoad,
34452
+ steps,
34453
+ traverse
34454
+ } = useFrameHistory();
34455
+ return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(DrawerHeader, {
34456
+ closeDrawer: closeDrawer
34457
+ }, /*#__PURE__*/React__default.createElement(StackView$2, {
34458
+ alignment: "center",
34459
+ axis: "horizontal",
34460
+ spacing: 0.25
34461
+ }, /*#__PURE__*/React__default.createElement(NavButton, {
34462
+ direction: "back",
34463
+ disabled: steps.back === 0,
34464
+ onClick: () => traverse("back")
34465
+ }), /*#__PURE__*/React__default.createElement(NavButton, {
34466
+ direction: "forward",
34467
+ disabled: steps.forward === 0,
34468
+ onClick: () => traverse("forward")
34469
+ }), /*#__PURE__*/React__default.createElement(DrawerHeading, {
34470
+ paddingLeft: 0.5
34471
+ }, "Developer Tools"))), /*#__PURE__*/React__default.createElement(Box, {
34472
+ as: "iframe",
34473
+ border: "none",
34474
+ flex: "1",
34475
+ minHeight: 0,
34476
+ onLoad: onFrameLoad,
34477
+ src: iframeSrc(),
34478
+ title: "Developer Tools",
34479
+ width: "100%"
34480
+ }));
34481
+ }
34482
+ const DeveloperTools = {
34483
+ component: DeveloperToolsComponent,
34484
+ icons: {
34485
+ component: ToolsIcon
34486
+ },
34487
+ label: "Developer Tools",
34488
+ // No space: `name` becomes an element id and a pendo attribute.
34489
+ name: "DeveloperTools"
34490
+ };
33321
34491
 
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"};
34492
+ 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
34493
 
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";
34494
+ 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
34495
  // Same stops as the AskAIBanner border gradient, so the two stay in sync.
33326
34496
  const GRADIENT_STOPS = [{
33327
34497
  color: "var(--t-fill-color-tag-teal-040)",
@@ -33370,7 +34540,7 @@ function SparkleIcon({
33370
34540
  stopColor: color
33371
34541
  }
33372
34542
  })))))), /*#__PURE__*/React__default.createElement("path", {
33373
- d: SPARKLE_PATH,
34543
+ d: SPARKLE_PATH$1,
33374
34544
  fill: isGradient ? `url(#${gradientId})` : "currentColor"
33375
34545
  }));
33376
34546
  }
@@ -33379,8 +34549,8 @@ function AskAIBanner(props) {
33379
34549
  var _a, _b;
33380
34550
  const isStacked = props.variant !== "inline";
33381
34551
  return /*#__PURE__*/React__default.createElement("button", {
33382
- className: classnames(styles.banner, {
33383
- [styles.stacked]: isStacked
34552
+ className: classnames(styles$1.banner, {
34553
+ [styles$1.stacked]: isStacked
33384
34554
  }),
33385
34555
  onClick: () => props.onClick(),
33386
34556
  type: "button"
@@ -33395,15 +34565,15 @@ function StackedContent({
33395
34565
  title
33396
34566
  }) {
33397
34567
  return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("span", {
33398
- className: styles.content
34568
+ className: styles$1.content
33399
34569
  }, /*#__PURE__*/React__default.createElement(AskAIIcon, null), /*#__PURE__*/React__default.createElement("span", {
33400
- className: styles.text
34570
+ className: styles$1.text
33401
34571
  }, /*#__PURE__*/React__default.createElement("span", {
33402
- className: styles.title
34572
+ className: styles$1.title
33403
34573
  }, 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,
34574
+ className: styles$1.subtitle
34575
+ }, "Support answers tailored to you"))), /*#__PURE__*/React__default.createElement(Icon$1, {
34576
+ className: styles$1.chevron,
33407
34577
  name: "general.rightChevron",
33408
34578
  size: "16px"
33409
34579
  }));
@@ -33413,18 +34583,18 @@ function InlineContent({
33413
34583
  title
33414
34584
  }) {
33415
34585
  return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("span", {
33416
- className: styles.inlineContent
34586
+ className: styles$1.inlineContent
33417
34587
  }, /*#__PURE__*/React__default.createElement(SparkleIcon, {
33418
- className: styles.sparkle,
34588
+ className: styles$1.sparkle,
33419
34589
  fill: "currentColor"
33420
34590
  }), /*#__PURE__*/React__default.createElement("span", {
33421
- className: styles.inlineTitle
34591
+ className: styles$1.inlineTitle
33422
34592
  }, title)), /*#__PURE__*/React__default.createElement("span", {
33423
- className: styles.action
34593
+ className: styles$1.action
33424
34594
  }, /*#__PURE__*/React__default.createElement("span", {
33425
- className: styles.actionLabel
33426
- }, action), /*#__PURE__*/React__default.createElement(Icon, {
33427
- className: styles.actionChevron,
34595
+ className: styles$1.actionLabel
34596
+ }, action), /*#__PURE__*/React__default.createElement(Icon$1, {
34597
+ className: styles$1.actionChevron,
33428
34598
  name: "general.rightChevron",
33429
34599
  size: "14px"
33430
34600
  })));
@@ -33441,9 +34611,9 @@ function AskAIIcon() {
33441
34611
  const borderA = `${uid}-border-a`;
33442
34612
  const borderB = `${uid}-border-b`;
33443
34613
  return /*#__PURE__*/React__default.createElement("span", {
33444
- className: styles.icon
34614
+ className: styles$1.icon
33445
34615
  }, /*#__PURE__*/React__default.createElement("svg", {
33446
- className: styles.iconShape,
34616
+ className: styles$1.iconShape,
33447
34617
  viewBox: "0 0 32 32",
33448
34618
  fill: "none",
33449
34619
  "aria-hidden": "true"
@@ -33614,7 +34784,7 @@ function AskAIIcon() {
33614
34784
  stopColor: "#D48B91",
33615
34785
  stopOpacity: "0"
33616
34786
  })))), /*#__PURE__*/React__default.createElement("svg", {
33617
- className: styles.iconSparkle,
34787
+ className: styles$1.iconSparkle,
33618
34788
  viewBox: "0 0 19.4995 17.9999",
33619
34789
  "aria-hidden": "true"
33620
34790
  }, /*#__PURE__*/React__default.createElement("path", {
@@ -33623,6 +34793,23 @@ function AskAIIcon() {
33623
34793
  })));
33624
34794
  }
33625
34795
 
34796
+ var styles = {"button":"button-2NVI1","sparkle":"sparkle-ruJGP","label":"label-4NUGB"};
34797
+
34798
+ function AskAIButton({
34799
+ onClick,
34800
+ text = "Ask AI"
34801
+ }) {
34802
+ return /*#__PURE__*/React__default.createElement("button", {
34803
+ className: styles.button,
34804
+ onClick: () => onClick(),
34805
+ type: "button"
34806
+ }, /*#__PURE__*/React__default.createElement(SparkleIcon, {
34807
+ className: styles.sparkle
34808
+ }), /*#__PURE__*/React__default.createElement("span", {
34809
+ className: styles.label
34810
+ }, text));
34811
+ }
34812
+
33626
34813
  // The article heading lives in the deepest populated hierarchy level (lvl0 is
33627
34814
  // always "Planning Center").
33628
34815
  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 +34833,7 @@ const HeadingIcon = ({
33646
34833
  const logoName = appName.toLowerCase().replace(/\s+/g, "-");
33647
34834
  // Accounts has no tapestry Logo, so use the cog icon
33648
34835
  if (logoName === "accounts") {
33649
- return /*#__PURE__*/React__default.createElement(Icon, {
34836
+ return /*#__PURE__*/React__default.createElement(Icon$1, {
33650
34837
  name: "general.cog",
33651
34838
  color: "foregroundSecondary"
33652
34839
  });
@@ -33710,7 +34897,7 @@ const CustomSearchBox = ({
33710
34897
  },
33711
34898
  placeholder: "Search our documentation",
33712
34899
  value: query,
33713
- renderRight: /*#__PURE__*/React__default.createElement(Icon, {
34900
+ renderRight: /*#__PURE__*/React__default.createElement(Icon$1, {
33714
34901
  name: "general.search"
33715
34902
  }),
33716
34903
  onChange: e => setQuery(e.target.value),
@@ -33778,16 +34965,24 @@ const DocumentationSearch = ({
33778
34965
  // not the path, so updating it is a same-document navigation that doesn't
33779
34966
  // reload the iframe, but also lets the route survive a forced reload (e.g.
33780
34967
  // Turbo re-parenting the topbar).
33781
- function buildPicobotSrc(origin, path) {
34968
+ //
34969
+ // `panel` loads picobot as its own toolbar panel rather than inside Helpdesk.
34970
+ function buildPicobotSrc(origin, path, {
34971
+ panel = false
34972
+ } = {}) {
33782
34973
  const [rawPath, queryPart] = path.replace(/^\/+/, "").split("?", 2);
33783
34974
  const safePath = rawPath.split("/").filter(segment => segment && segment !== "." && segment !== "..").join("/");
33784
34975
  const fragment = queryPart !== undefined ? `${safePath}?${queryPart}` : safePath;
33785
- const base = new URL("topbar", origin).toString();
34976
+ const url = new URL("topbar", origin);
34977
+ if (panel) url.searchParams.set("panel", "1");
34978
+ const base = url.toString();
33786
34979
  return fragment ? `${base}#${fragment}` : base;
33787
34980
  }
33788
34981
 
33789
34982
  const LOAD_TIMEOUT_MS = 3000;
33790
- function createPicobotIframe(picobotOrigin) {
34983
+ function createPicobotIframe(picobotOrigin, {
34984
+ panel = false
34985
+ } = {}) {
33791
34986
  return /*#__PURE__*/React__default.forwardRef(function PicobotIframe({
33792
34987
  currentPath: iframePath,
33793
34988
  onChangePath: onIframeChangePath,
@@ -33843,7 +35038,9 @@ function createPicobotIframe(picobotOrigin) {
33843
35038
  (_b = (_a = iframeRef.current) === null || _a === void 0 ? void 0 : _a.contentWindow) === null || _b === void 0 ? void 0 : _b.postMessage(message, picobotOrigin);
33844
35039
  }
33845
35040
  }), []);
33846
- const iframeSrc = buildPicobotSrc(picobotOrigin, loadedPath);
35041
+ const iframeSrc = buildPicobotSrc(picobotOrigin, loadedPath, {
35042
+ panel
35043
+ });
33847
35044
  return /*#__PURE__*/React__default.createElement("iframe", {
33848
35045
  ref: iframeRef,
33849
35046
  src: iframeSrc,
@@ -33882,22 +35079,28 @@ function HelpdeskComponent({
33882
35079
  currentPath,
33883
35080
  env,
33884
35081
  helpdeskConfig,
33885
- onChangePath
35082
+ isPicobotLoading,
35083
+ onChangePath,
35084
+ openPicobot
33886
35085
  }) {
33887
35086
  const PicobotIframe = React__default.useMemo(() => createPicobotIframe(pcoUrl(env)("picobot")), [env]);
35087
+ const loadingFallback = /*#__PURE__*/React__default.createElement(StackView$1, {
35088
+ flex: 1,
35089
+ axis: "vertical"
35090
+ }, /*#__PURE__*/React__default.createElement(DrawerHeader, {
35091
+ closeDrawer: closeDrawer
35092
+ }, /*#__PURE__*/React__default.createElement(StackView$1, {
35093
+ axis: "horizontal",
35094
+ spacing: 1
35095
+ }, /*#__PURE__*/React__default.createElement(DrawerHeading, null, "Help and support"), /*#__PURE__*/React__default.createElement(LoadingIndicator$1, null))));
35096
+ // The add-on reads its args once, so mounting before the gate answers
35097
+ // would leave out openPicobot for good.
35098
+ if (isPicobotLoading) return loadingFallback;
33888
35099
  return /*#__PURE__*/React__default.createElement(UiExtensionFirstPartyComponent, {
33889
35100
  grow: 1,
33890
35101
  display: "flex",
33891
35102
  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)))),
35103
+ loadingFallback: loadingFallback,
33901
35104
  additionalComponents: {
33902
35105
  AskAIBanner: {
33903
35106
  component: AskAIBanner,
@@ -33917,6 +35120,18 @@ function HelpdeskComponent({
33917
35120
  }
33918
35121
  }
33919
35122
  },
35123
+ AskAIButton: {
35124
+ component: AskAIButton,
35125
+ properties: {
35126
+ onClick: {
35127
+ event: false,
35128
+ type: Function
35129
+ },
35130
+ text: {
35131
+ type: String
35132
+ }
35133
+ }
35134
+ },
33920
35135
  CalendlyWidget: {
33921
35136
  component: CalendlyWidget,
33922
35137
  properties: {
@@ -33995,7 +35210,11 @@ function HelpdeskComponent({
33995
35210
  currentPageParams: location.search,
33996
35211
  currentPath,
33997
35212
  helpdeskConfig,
33998
- onChangePath
35213
+ onChangePath,
35214
+ // Its presence tells helpdesk that Picobot has its own panel.
35215
+ ...(openPicobot && {
35216
+ openPicobot
35217
+ })
33999
35218
  }
34000
35219
  });
34001
35220
  }
@@ -34542,6 +35761,142 @@ const Notifications = {
34542
35761
  name: "Notifications"
34543
35762
  };
34544
35763
 
35764
+ 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";
35765
+ // Helpdesk reads this context off the email path to open its email form the
35766
+ // same way a handoff inside its own drawer does.
35767
+ function emailPathWithContext(context = {}) {
35768
+ const params = new URLSearchParams();
35769
+ for (const key of ["conversationId", "source", "turnIndex"]) {
35770
+ const value = context[key];
35771
+ if (value !== null && value !== undefined) params.set(key, String(value));
35772
+ }
35773
+ return `email?${params}`;
35774
+ }
35775
+ function PicobotComponent({
35776
+ clearPending,
35777
+ closeDrawer,
35778
+ currentPath,
35779
+ env,
35780
+ onChangePath,
35781
+ openTool,
35782
+ pending
35783
+ }) {
35784
+ const PicobotIframe = React__default.useMemo(() => createPicobotIframe(pcoUrl(env)("picobot"), {
35785
+ panel: true
35786
+ }), [env]);
35787
+ const picobotRef = React__default.useRef(null);
35788
+ const [isPicobotReady, setIsPicobotReady] = React__default.useState(false);
35789
+ // Picobot has no listener until it sends READY, so openPicobot's draft or
35790
+ // conversation waits for it.
35791
+ React__default.useEffect(function handOverPendingOncePicobotIsReady() {
35792
+ var _a, _b;
35793
+ if (!pending || !isPicobotReady) return;
35794
+ const {
35795
+ conversationId,
35796
+ draft,
35797
+ ticketExternalId
35798
+ } = pending;
35799
+ if (conversationId) {
35800
+ (_a = picobotRef.current) === null || _a === void 0 ? void 0 : _a.postMessage({
35801
+ payload: {
35802
+ conversation_id: conversationId,
35803
+ // Picobot asks whether this resolved the ticket, and closes it.
35804
+ ...(ticketExternalId && {
35805
+ external_id: ticketExternalId,
35806
+ source: "ticket_submitted"
35807
+ })
35808
+ },
35809
+ type: "LOAD_CONVERSATION"
35810
+ });
35811
+ } else if (draft) {
35812
+ // Not both: Picobot cancels a conversation load when a prefill
35813
+ // starts a new conversation.
35814
+ (_b = picobotRef.current) === null || _b === void 0 ? void 0 : _b.postMessage({
35815
+ payload: {
35816
+ source: "email_form",
35817
+ text: draft
35818
+ },
35819
+ type: "PREFILL_COMPOSER"
35820
+ });
35821
+ }
35822
+ clearPending();
35823
+ }, [pending, isPicobotReady, clearPending]);
35824
+ const [iframeErrored, setIframeErrored] = React__default.useState(false);
35825
+ const [mountKey, setMountKey] = React__default.useState(0);
35826
+ const handleRetry = React__default.useCallback(() => {
35827
+ setIframeErrored(false);
35828
+ setIsPicobotReady(false);
35829
+ setMountKey(key => key + 1);
35830
+ }, []);
35831
+ const handleMessage = React__default.useCallback(data => {
35832
+ var _a;
35833
+ const message = data;
35834
+ switch (message === null || message === void 0 ? void 0 : message.type) {
35835
+ case "CLOSE_DRAWER":
35836
+ return closeDrawer();
35837
+ case "READY":
35838
+ return setIsPicobotReady(true);
35839
+ case "PICOBOT_SUPPORT_HANDOFF":
35840
+ return openTool("helpdesk", ((_a = message.payload) === null || _a === void 0 ? void 0 : _a.method) === "zoom" ? "phone" : emailPathWithContext(message.payload));
35841
+ case "IFRAME_LOAD_ERROR":
35842
+ console.warn("topbar: Picobot iframe failed to load");
35843
+ return setIframeErrored(true);
35844
+ }
35845
+ }, [closeDrawer, openTool]);
35846
+ if (iframeErrored) {
35847
+ return /*#__PURE__*/React__default.createElement(LoadError, {
35848
+ onContactSupport: () => openTool("helpdesk", "email"),
35849
+ onRetry: handleRetry
35850
+ });
35851
+ }
35852
+ return /*#__PURE__*/React__default.createElement(PicobotIframe, {
35853
+ key: mountKey,
35854
+ ref: picobotRef,
35855
+ currentPath: currentPath,
35856
+ onChangePath: onChangePath,
35857
+ onMessage: handleMessage
35858
+ });
35859
+ }
35860
+ function LoadError({
35861
+ onContactSupport,
35862
+ onRetry
35863
+ }) {
35864
+ return /*#__PURE__*/React__default.createElement(StackView$2, {
35865
+ axis: "vertical",
35866
+ flex: 1,
35867
+ alignment: "center",
35868
+ distribution: "center",
35869
+ padding: 3,
35870
+ spacing: 1
35871
+ }, /*#__PURE__*/React__default.createElement(Heading, {
35872
+ level: 2
35873
+ }, "Picobot is temporarily unavailable"), /*#__PURE__*/React__default.createElement(Text, {
35874
+ size: 5,
35875
+ color: "var(--t-text-color-secondary)"
35876
+ }, "Try again or contact support."), /*#__PURE__*/React__default.createElement(Box, {
35877
+ paddingTop: 2
35878
+ }, /*#__PURE__*/React__default.createElement(Button$1, {
35879
+ onClick: onRetry,
35880
+ iconLeft: "general.refresh",
35881
+ theme: "primary",
35882
+ variant: "fill",
35883
+ title: "Retry"
35884
+ })), /*#__PURE__*/React__default.createElement(Button$1, {
35885
+ onClick: onContactSupport,
35886
+ variant: "naked",
35887
+ theme: "primary",
35888
+ title: "Contact support"
35889
+ }));
35890
+ }
35891
+ const Picobot = {
35892
+ component: PicobotComponent,
35893
+ icons: {
35894
+ staticDotPath: SPARKLE_PATH,
35895
+ staticPath: SPARKLE_PATH
35896
+ },
35897
+ name: "Picobot"
35898
+ };
35899
+
34545
35900
  const {
34546
35901
  LoadingIndicator,
34547
35902
  StackView
@@ -34937,6 +36292,41 @@ const useChatConfig = ({
34937
36292
  };
34938
36293
  };
34939
36294
 
36295
+ // Picobot's API rejects requests without a version header, and apiRequest
36296
+ // sets its own headers, so the version rides in on the fetch.
36297
+ const picobotFetch = (url, opts) => defaultFetch(url, {
36298
+ ...opts,
36299
+ headers: {
36300
+ ...(opts === null || opts === void 0 ? void 0 : opts.headers),
36301
+ "X-PCO-API-Version": "2026-08-11"
36302
+ }
36303
+ });
36304
+ // Routes wait on the gate, so a slow Picobot can't hold them up for long.
36305
+ const GATE_TIMEOUT_MS = 3000;
36306
+ const usePicobotConfig = ({
36307
+ isEnabled
36308
+ }) => {
36309
+ // Undefined until Picobot answers.
36310
+ const [panelEnabled, setPanelEnabled] = useState();
36311
+ useEffect(() => {
36312
+ if (!isEnabled) return undefined;
36313
+ // Whatever settles first wins, so a late answer can't reopen the gate.
36314
+ const settle = enabled => setPanelEnabled(current => current !== null && current !== void 0 ? current : enabled);
36315
+ const timeout = setTimeout(() => settle(false), GATE_TIMEOUT_MS);
36316
+ apiRequest(picobotFetch, `${window.location.origin}/~api/picobot/v2/me`).then(({
36317
+ json
36318
+ }) => {
36319
+ var _a, _b;
36320
+ 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);
36321
+ }).catch(() => settle(false)).finally(() => clearTimeout(timeout));
36322
+ return () => clearTimeout(timeout);
36323
+ }, [isEnabled]);
36324
+ return {
36325
+ displayPicobotIcon: panelEnabled === true,
36326
+ isLoading: isEnabled && panelEnabled === undefined
36327
+ };
36328
+ };
36329
+
34940
36330
  const Profile = {
34941
36331
  component: UserProfile,
34942
36332
  icons: {
@@ -34947,10 +36337,16 @@ const Profile = {
34947
36337
  };
34948
36338
 
34949
36339
  const DURATION = 100;
36340
+ // Exempt paths match regardless of query, so `email?conversationId=…` is
36341
+ // exempt along with `email`.
34950
36342
  function isToolPathExemptFromStorage(toolName, path) {
34951
36343
  var _a, _b;
34952
36344
  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;
36345
+ const [pathWithoutQuery] = path.split("?");
36346
+ 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;
36347
+ }
36348
+ function isConversationPath(path) {
36349
+ return Boolean(path === null || path === void 0 ? void 0 : path.startsWith("conversations/"));
34954
36350
  }
34955
36351
  function scrollToolDrawerToTop(toolName) {
34956
36352
  var _a;
@@ -34963,6 +36359,7 @@ const TOOLS_MAP = {
34963
36359
  Chat,
34964
36360
  Helpdesk,
34965
36361
  Notifications,
36362
+ Picobot,
34966
36363
  Profile,
34967
36364
  Tasks
34968
36365
  };
@@ -34990,7 +36387,8 @@ function Toolbar({
34990
36387
  });
34991
36388
  const {
34992
36389
  currentContent,
34993
- open
36390
+ open,
36391
+ pending
34994
36392
  } = state;
34995
36393
  const mounted = !!currentContent;
34996
36394
  const storeData = useStore();
@@ -35003,6 +36401,15 @@ function Toolbar({
35003
36401
  ...(env === "development" ? _storeDataOverride !== null && _storeDataOverride !== void 0 ? _storeDataOverride : {} : {})
35004
36402
  };
35005
36403
  const updateStore = useUpdateStore();
36404
+ const {
36405
+ displayPicobotIcon,
36406
+ isLoading: isPicobotGateLoading
36407
+ } = usePicobotConfig({
36408
+ isEnabled: Boolean(userId)
36409
+ });
36410
+ // Picobot is a kill-switched tool: until its gate says the panel is
36411
+ // enabled, its routes are held, and if it never does, they're ignored.
36412
+ const routableTool = useCallback(name => name === Picobot.name.toLowerCase() && !displayPicobotIcon ? undefined : toolsRouteMap[name], [displayPicobotIcon]);
35006
36413
  React__default.useEffect(() => {
35007
36414
  if (open) {
35008
36415
  document.body.style.overflow = "hidden";
@@ -35090,7 +36497,9 @@ function Toolbar({
35090
36497
  });
35091
36498
  }, [clearRoute]);
35092
36499
  React__default.useEffect(() => {
35093
- if (!toolbarFragmentRoute) return;
36500
+ if (!toolbarFragmentRoute || !routableTool(toolbarFragmentRoute.tool)) {
36501
+ return;
36502
+ }
35094
36503
  const isExempt = isToolPathExemptFromStorage(toolbarFragmentRoute.tool, toolbarFragmentRoute.path);
35095
36504
  if (isExempt) return;
35096
36505
  const nextState = {
@@ -35100,12 +36509,32 @@ function Toolbar({
35100
36509
  if (!shallowCompare(nextState, toolStateCookie)) {
35101
36510
  setToolStateCookie(nextState);
35102
36511
  }
35103
- }, [toolbarFragmentRoute, toolStateCookie]);
36512
+ }, [toolbarFragmentRoute, toolStateCookie, routableTool]);
35104
36513
  React__default.useEffect(() => {
35105
- if ((toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && (currentContent !== toolsRouteMap[toolbarFragmentRoute.tool] || !open)) {
35106
- setToolAndOpenDrawer(toolsRouteMap[toolbarFragmentRoute.tool]);
36514
+ // Helpdesk conversations may belong to Picobot, so they wait for its gate
36515
+ // rather than flashing open in Helpdesk first.
36516
+ const mayRedirectToPicobot = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) === Helpdesk.name.toLowerCase() && isConversationPath(toolbarFragmentRoute.path) && (isPicobotGateLoading || displayPicobotIcon);
36517
+ const tool = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && !mayRedirectToPicobot && routableTool(toolbarFragmentRoute.tool);
36518
+ if (tool && (currentContent !== tool || !open)) {
36519
+ setToolAndOpenDrawer(tool);
35107
36520
  }
35108
- }, [setToolAndOpenDrawer, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool, currentContent, open]);
36521
+ }, [setToolAndOpenDrawer, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path, currentContent, open, routableTool, isPicobotGateLoading, displayPicobotIcon]);
36522
+ const openTool = useCallback((name, path, pendingValue) => {
36523
+ const tool = routableTool(name.toLowerCase());
36524
+ if (!tool) return;
36525
+ dispatch({
36526
+ payload: pendingValue === undefined ? null : {
36527
+ tool: tool.name,
36528
+ value: pendingValue
36529
+ },
36530
+ type: ActionType.setPending
36531
+ });
36532
+ onChangeToolbarFragmentRoute(tool.name.toLowerCase(), path);
36533
+ scrollToolDrawerToTop(tool.name);
36534
+ }, [onChangeToolbarFragmentRoute, routableTool]);
36535
+ const clearPending = useCallback(() => dispatch({
36536
+ type: ActionType.clearPending
36537
+ }), []);
35109
36538
  const toggleDrawer = content => {
35110
36539
  if (!open) {
35111
36540
  return openDrawer(content);
@@ -35122,12 +36551,52 @@ function Toolbar({
35122
36551
  joltPromise: storeData === null || storeData === void 0 ? void 0 : storeData.joltPromise,
35123
36552
  userId
35124
36553
  });
35125
- const TOOLS = [Notifications, Tasks, displayChatIcon && Chat].filter(Boolean);
36554
+ // Picobot conversations moved from Helpdesk to the Picobot panel, so
36555
+ // send old links and saved paths there once the panel is enabled.
36556
+ React__default.useEffect(() => {
36557
+ if (!displayPicobotIcon) return;
36558
+ if ((toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) === Helpdesk.name.toLowerCase() && isConversationPath(toolbarFragmentRoute.path)) {
36559
+ onChangeToolbarFragmentRoute(Picobot.name.toLowerCase(), toolbarFragmentRoute.path);
36560
+ }
36561
+ }, [displayPicobotIcon, toolbarFragmentRoute, onChangeToolbarFragmentRoute]);
36562
+ React__default.useEffect(() => {
36563
+ if (!displayPicobotIcon) return;
36564
+ const {
36565
+ helpdesk,
36566
+ picobot
36567
+ } = toolStateCookie;
36568
+ if (!isConversationPath(helpdesk)) return;
36569
+ setToolStateCookie({
36570
+ ...toolStateCookie,
36571
+ helpdesk: "",
36572
+ picobot: picobot || helpdesk
36573
+ });
36574
+ }, [displayPicobotIcon, toolStateCookie]);
36575
+ const openPicobot = useCallback(options => {
36576
+ var _a;
36577
+ // Moving an open Picobot off its path reloads or navigates the iframe,
36578
+ // which would race the options it's being handed.
36579
+ const path = currentContent === Picobot ? (_a = toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path) !== null && _a !== void 0 ? _a : "" : "";
36580
+ openTool(Picobot.name, path, options);
36581
+ }, [openTool, currentContent, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path]);
36582
+ React__default.useEffect(() => {
36583
+ if (!displayPicobotIcon) return undefined;
36584
+ window.topbar = window.topbar || {};
36585
+ window.topbar.openPicobot = openPicobot;
36586
+ return () => {
36587
+ delete window.topbar.openPicobot;
36588
+ };
36589
+ }, [displayPicobotIcon, openPicobot]);
36590
+ const TOOLS = [Notifications, Tasks, displayChatIcon && Chat, displayPicobotIcon && Picobot, isPcoTestHost() && DeveloperTools].filter(Boolean);
35126
36591
  const handleToolOnClick = tool => {
35127
36592
  toggleDrawer(tool);
35128
36593
  scrollToolDrawerToTop(tool.name);
35129
36594
  };
35130
36595
  const toolSpecificProps = {
36596
+ Helpdesk: {
36597
+ isPicobotLoading: isPicobotGateLoading,
36598
+ openPicobot: displayPicobotIcon ? openPicobot : undefined
36599
+ },
35131
36600
  Profile: {
35132
36601
  appName,
35133
36602
  closeDrawer,
@@ -35150,6 +36619,7 @@ function Toolbar({
35150
36619
  const routeIsForCurrentTool = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && toolsRouteMap[toolbarFragmentRoute.tool] === currentContent;
35151
36620
  const currentToolProps = {
35152
36621
  appName,
36622
+ clearPending,
35153
36623
  closeDrawer,
35154
36624
  currentPath: routeIsForCurrentTool ? toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path : null,
35155
36625
  env,
@@ -35158,6 +36628,8 @@ function Toolbar({
35158
36628
  joltPromise,
35159
36629
  onChangePath,
35160
36630
  openDrawer: () => currentContent && openDrawer(currentContent),
36631
+ openTool,
36632
+ 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
36633
  userId,
35162
36634
  ...(currentContent ? toolSpecificProps[currentContent.name] : {})
35163
36635
  };
@@ -35168,38 +36640,37 @@ function Toolbar({
35168
36640
  return /*#__PURE__*/React__default.createElement(ThemeProvider, {
35169
36641
  theme: topbarTheme
35170
36642
  }, /*#__PURE__*/React__default.createElement(UiExtensionDataContext, null, /*#__PURE__*/React__default.createElement("div", {
35171
- className: styles$1["toolbar"],
36643
+ className: styles$2["toolbar"],
35172
36644
  "data-pendo": "toolbar-profile-support-wrapper"
35173
36645
  }, /*#__PURE__*/React__default.createElement(ToolbarButton, {
35174
36646
  "aria-label": `Open ${userName}’s profile drawer`,
35175
36647
  onClick: () => toggleDrawer(Profile),
35176
36648
  "data-pendo": "toolbar-profile",
35177
- className: styles$1["toolbar-button--avatar"]
36649
+ className: styles$2["toolbar-button--avatar"]
35178
36650
  }, /*#__PURE__*/React__default.createElement(Avatar, {
35179
36651
  env: env,
35180
36652
  url: userAvatarPath
35181
36653
  })), /*#__PURE__*/React__default.createElement(ToolbarButton, {
35182
- className: styles$1["toolbar-button--home"],
36654
+ className: styles$2["toolbar-button--home"],
35183
36655
  onClick: () => closeDrawer()
35184
- }, /*#__PURE__*/React__default.createElement(Icon, {
36656
+ }, /*#__PURE__*/React__default.createElement(Icon$1, {
35185
36657
  name: "toolbar.planningCenter",
35186
36658
  size: "24px"
35187
36659
  })), [HelpDeskTool, ...TOOLS].map(tool => {
36660
+ var _a;
35188
36661
  const current = open && currentContent === tool;
35189
36662
  const indicator = indicatorState[tool.name];
35190
36663
  return /*#__PURE__*/React__default.createElement(ToolbarButton, {
35191
36664
  current: current,
35192
36665
  onClick: () => handleToolOnClick(tool),
35193
36666
  id: getDataPendoAttribute(tool.name),
35194
- "aria-label": `Open ${tool.name} drawer`,
36667
+ "aria-label": `Open ${(_a = tool.label) !== null && _a !== void 0 ? _a : tool.name} drawer`,
35195
36668
  "data-pendo": getDataPendoAttribute(tool.name),
35196
36669
  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
- })));
36670
+ }, /*#__PURE__*/React__default.createElement(ToolbarToolIcon, {
36671
+ icons: tool.icons,
36672
+ dot: !!indicator
36673
+ }));
35203
36674
  })), /*#__PURE__*/React__default.createElement(DrawerWrap, {
35204
36675
  isVisible: open
35205
36676
  }, mounted && (/*#__PURE__*/React__default.createElement(Drawer, {
@@ -35217,12 +36688,25 @@ const ToolbarButton = ({
35217
36688
  }) => {
35218
36689
  return /*#__PURE__*/React__default.createElement("button", {
35219
36690
  type: "button",
35220
- className: classnames(styles$1["toolbar-button"], className, current && styles$1["is-current"]),
36691
+ className: classnames(styles$2["toolbar-button"], className, current && styles$2["is-current"]),
35221
36692
  onClick: onClick,
35222
36693
  "data-toolbar-link": "true",
35223
36694
  ...restProps
35224
36695
  }, children);
35225
36696
  };
36697
+ function ToolbarToolIcon({
36698
+ dot,
36699
+ icons
36700
+ }) {
36701
+ if ("component" in icons) {
36702
+ return /*#__PURE__*/React__default.createElement(icons.component, null);
36703
+ } else {
36704
+ return /*#__PURE__*/React__default.createElement(ToolbarButtonIcon, {
36705
+ path: dot ? icons.staticDotPath : icons.staticPath,
36706
+ dot: dot
36707
+ });
36708
+ }
36709
+ }
35226
36710
  function ToolbarButtonIcon({
35227
36711
  dot,
35228
36712
  path