@planningcenter/topbar 12.3.0-qa-937.2 → 12.3.0-qa-947.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/topbar.js CHANGED
@@ -3266,6 +3266,45 @@ function saveLastProduct(slug) {
3266
3266
  }
3267
3267
  }
3268
3268
 
3269
+ // Products open modals from many libraries, not all of which mark themselves
3270
+ // with roles, so a modal is recognized by what it does: cover the page.
3271
+ function isModalOpen() {
3272
+ const {
3273
+ clientHeight,
3274
+ clientWidth
3275
+ } = document.documentElement;
3276
+ const element = document.elementFromPoint(clientWidth / 2, clientHeight / 2);
3277
+ return element !== null && (isInNativeDialog(element) || hasBackdrop(element, clientWidth, clientHeight));
3278
+ }
3279
+ // A native modal's backdrop is drawn by the browser rather than being an
3280
+ // element; hit-testing it lands on the dialog itself.
3281
+ function isInNativeDialog(element) {
3282
+ return element.closest("dialog[open]") !== null;
3283
+ }
3284
+ function hasBackdrop(element, width, height) {
3285
+ let current = element;
3286
+ while (current && !isBackdrop(current, width, height)) {
3287
+ current = current.parentElement;
3288
+ }
3289
+ return current !== null;
3290
+ }
3291
+ // Some backdrops start below the topbar rather than at the top of the screen,
3292
+ // so the layer only has to span the width and reach from the center to the
3293
+ // bottom. Its own box has to reach the center: a bar along the bottom can
3294
+ // hold a panel that pops up over the center, like Services' media player.
3295
+ // react-aria leaves a closed modal's backdrop up while it fades out, search's
3296
+ // own included, so one that's on its way out doesn't count.
3297
+ function isBackdrop(element, width, height) {
3298
+ const rect = element.getBoundingClientRect();
3299
+ return !element.matches("[data-exiting]") && getComputedStyle(element).position === "fixed" && rect.width >= width - SCROLLBAR_SLACK && rect.top <= height / 2 && rect.bottom >= height - ROUNDING_SLACK;
3300
+ }
3301
+ // react-aria keeps a classic scrollbar's gutter reserved while its modals lock
3302
+ // scrolling, so their backdrops stop short of the right edge by its width.
3303
+ const SCROLLBAR_SLACK = 24;
3304
+ // Rects are fractional at non-100% zoom, but clientWidth and clientHeight are
3305
+ // rounded to whole pixels.
3306
+ const ROUNDING_SLACK = 1;
3307
+
3269
3308
  var styles$c = {"visuallyHidden":"visuallyHidden-C28ju"};
3270
3309
 
3271
3310
  var styles$b = {"tab":"tab-lQLDx"};
@@ -3397,10 +3436,11 @@ function useRecentlyVisitableMetadata() {
3397
3436
  return shim.useSyncExternalStore(recentlyVisitableMetadataStore.subscribe, () => recentlyVisitableMetadataStore.getState().metadata);
3398
3437
  }
3399
3438
 
3400
- const MAX_ENTRIES$1 = 20;
3401
- const FORMAT_VERSION = 1;
3439
+ const MAX_ENTRIES$1 = 50;
3402
3440
  const MAX_TEXT_LENGTH = 200;
3403
3441
  const PRODUCTS = ["accounts", "calendar", "check-ins", "giving", "groups", "home", "login", "people", "publishing", "registrations", "services"];
3442
+ // Login has no pages worth returning to.
3443
+ const APPS = PRODUCTS.filter(product => product !== "login");
3404
3444
  function isId(value) {
3405
3445
  return value !== "" && Array.from(value).every(character => character >= "0" && character <= "9");
3406
3446
  }
@@ -3409,26 +3449,47 @@ function isPcrl(value) {
3409
3449
  const segments = value.split("/");
3410
3450
  return segments.length === 4 && PRODUCTS.some(product => product === segments[0]) && isId(segments[3]);
3411
3451
  }
3452
+ function isApp(value) {
3453
+ return APPS.some(app => app === value);
3454
+ }
3455
+ // Links are built by resolving the path against its app's origin, so a stored
3456
+ // path must not be able to resolve anywhere else (`//evil.com`, `/\evil.com`).
3457
+ function isPath(value) {
3458
+ const origin = "https://app.invalid";
3459
+ try {
3460
+ return typeof value === "string" && value.startsWith("/") && new URL(value, origin).origin === origin;
3461
+ } catch {
3462
+ return false;
3463
+ }
3464
+ }
3412
3465
  function isNonBlankString(value) {
3413
3466
  return typeof value === "string" && value.trim() !== "";
3414
3467
  }
3468
+ function isOptionalString(value) {
3469
+ return value === undefined || typeof value === "string";
3470
+ }
3415
3471
  function isRecord(value) {
3416
3472
  return typeof value === "object" && value !== null && !Array.isArray(value);
3417
3473
  }
3474
+ function isPageMetadata(value) {
3475
+ return value === undefined || isRecord(value) && isOptionalString(value.details) && isOptionalString(value.type);
3476
+ }
3477
+ // The query string is part of the link but not of the page's identity, so
3478
+ // revisiting with other filters moves the existing entry instead of adding one.
3418
3479
  function entryKey(entry) {
3419
3480
  if (entry.type === "resource") {
3420
3481
  return `resource:${entry.pcrl}`;
3421
3482
  } else {
3422
- return `page:${entry.product}:${entry.path}`;
3483
+ return `page:${entry.app}:${entry.path.split("?")[0]}`;
3423
3484
  }
3424
3485
  }
3425
3486
  function isEntry(value) {
3426
- if (!isRecord(value)) {
3487
+ if (!isRecord(value) || typeof value.visitedAt !== "string") {
3427
3488
  return false;
3428
3489
  } else if (value.type === "resource") {
3429
3490
  return typeof value.pcrl === "string" && isPcrl(value.pcrl);
3430
3491
  } else if (value.type === "page") {
3431
- return isNonBlankString(value.product) && isNonBlankString(value.title) && isNonBlankString(value.path) && typeof value.details === "string";
3492
+ return isApp(value.app) && isPath(value.path) && isNonBlankString(value.title) && isPageMetadata(value.metadata);
3432
3493
  } else {
3433
3494
  return false;
3434
3495
  }
@@ -3437,58 +3498,175 @@ function isEntry(value) {
3437
3498
  function clampText(value) {
3438
3499
  return Array.from(value).slice(0, MAX_TEXT_LENGTH).join("");
3439
3500
  }
3440
- function clampEntry(entry) {
3501
+ function clampMetadata({
3502
+ details,
3503
+ type
3504
+ }) {
3505
+ return {
3506
+ ...(details && {
3507
+ details: clampText(details)
3508
+ }),
3509
+ ...(type && {
3510
+ type: clampText(type)
3511
+ })
3512
+ };
3513
+ }
3514
+ function clampVisit(visit) {
3515
+ if (visit.type === "resource") {
3516
+ return visit;
3517
+ } else {
3518
+ return {
3519
+ ...visit,
3520
+ ...(visit.metadata && {
3521
+ metadata: clampMetadata(visit.metadata)
3522
+ }),
3523
+ title: clampText(visit.title.trim())
3524
+ };
3525
+ }
3526
+ }
3527
+ function toEntry(visit, visitedAt) {
3528
+ return {
3529
+ ...clampVisit(visit),
3530
+ visitedAt: visitedAt.toISOString()
3531
+ };
3532
+ }
3533
+ function knownMetadata({
3534
+ details,
3535
+ type
3536
+ }) {
3537
+ if (details === undefined && type === undefined) {
3538
+ return undefined;
3539
+ } else {
3540
+ return {
3541
+ ...(details !== undefined && {
3542
+ details
3543
+ }),
3544
+ ...(type !== undefined && {
3545
+ type
3546
+ })
3547
+ };
3548
+ }
3549
+ }
3550
+ // user-store-sync rejects a whole write over one field it doesn't know, so
3551
+ // anything else an entry carries (like the version older entries stored) is
3552
+ // dropped on the way in.
3553
+ function withKnownFields(entry) {
3441
3554
  if (entry.type === "resource") {
3442
- return entry;
3555
+ return {
3556
+ pcrl: entry.pcrl,
3557
+ type: entry.type,
3558
+ visitedAt: entry.visitedAt
3559
+ };
3443
3560
  } else {
3561
+ const metadata = entry.metadata && knownMetadata(entry.metadata);
3444
3562
  return {
3445
- ...entry,
3446
- details: clampText(entry.details),
3447
- title: clampText(entry.title)
3563
+ app: entry.app,
3564
+ ...(metadata && {
3565
+ metadata
3566
+ }),
3567
+ path: entry.path,
3568
+ title: entry.title,
3569
+ type: entry.type,
3570
+ visitedAt: entry.visitedAt
3448
3571
  };
3449
3572
  }
3450
3573
  }
3451
3574
  // Recents are disposable: anything unexpected reads as an empty list (or, for
3452
3575
  // a single bad entry, a list without it) rather than throwing.
3453
- function parseRecentlyVisited(value, personId) {
3454
- let parsed = null;
3576
+ function entriesFrom(value) {
3577
+ if (Array.isArray(value)) {
3578
+ return value.filter(isEntry).map(withKnownFields);
3579
+ } else {
3580
+ return [];
3581
+ }
3582
+ }
3583
+ function parseRecentlyVisited(value) {
3455
3584
  try {
3456
- parsed = JSON.parse(value !== null && value !== void 0 ? value : "null");
3585
+ return entriesFrom(JSON.parse(value !== null && value !== void 0 ? value : "null"));
3457
3586
  } catch {
3458
- parsed = null;
3459
- }
3460
- if (isRecord(parsed) && parsed.v === FORMAT_VERSION && parsed.person === personId && Array.isArray(parsed.entries)) {
3461
- return parsed.entries.filter(isEntry);
3462
- } else {
3463
3587
  return [];
3464
3588
  }
3465
3589
  }
3466
- function addVisit(entries, visit) {
3467
- const key = entryKey(visit);
3468
- return [clampEntry(visit), ...entries.filter(entry => entryKey(entry) !== key)].slice(0, MAX_ENTRIES$1);
3590
+ function addVisit(entries, entry) {
3591
+ const key = entryKey(entry);
3592
+ return [entry, ...entries.filter(existing => entryKey(existing) !== key)].slice(0, MAX_ENTRIES$1);
3469
3593
  }
3470
- function serializeRecentlyVisited(entries, personId) {
3471
- return JSON.stringify({
3472
- entries,
3473
- person: personId,
3474
- v: FORMAT_VERSION
3475
- });
3594
+ // The service may write timestamps with fewer fractional digits than
3595
+ // toISOString, so they're compared as times rather than strings.
3596
+ function visitTime({
3597
+ visitedAt
3598
+ }) {
3599
+ return Date.parse(visitedAt) || 0;
3600
+ }
3601
+ // One list from several, newest visit first wherever it was recorded.
3602
+ function mergeEntries(...lists) {
3603
+ const seen = new Set();
3604
+ return lists.flat().sort((a, b) => visitTime(b) - visitTime(a)).filter(entry => {
3605
+ const key = entryKey(entry);
3606
+ const isFirst = !seen.has(key);
3607
+ seen.add(key);
3608
+ return isFirst;
3609
+ }).slice(0, MAX_ENTRIES$1);
3610
+ }
3611
+ function isSameList(a, b) {
3612
+ return a.length === b.length && a.every((entry, index) => entryKey(entry) === entryKey(b[index]) && visitTime(entry) === visitTime(b[index]));
3476
3613
  }
3477
3614
 
3478
- // The product whose URL, in this environment, has the current page's host.
3615
+ // The app whose URL, in this environment, has the current page's host.
3479
3616
  // Reversing pcoUrl handles every environment @planningcenter/url knows about,
3480
- // and guarantees the stored product rebuilds a working link.
3481
- function currentProduct() {
3617
+ // and guarantees the stored app rebuilds a working link.
3618
+ function currentApp() {
3482
3619
  var _a;
3483
- return (_a = PRODUCTS.find(product => new URL(pcoUrl$1.pcoUrl(product)).hostname === window.location.hostname)) !== null && _a !== void 0 ? _a : null;
3620
+ return (_a = APPS.find(app => new URL(pcoUrl$1.pcoUrl(app)).hostname === window.location.hostname)) !== null && _a !== void 0 ? _a : null;
3484
3621
  }
3485
- function detailsFromMetadata(metadata) {
3622
+ function isFilled(value) {
3623
+ return typeof value === "string" && value !== "";
3624
+ }
3625
+ // The tag's metadata is product-supplied JSON; only its string `type` and
3626
+ // `details` carry over, and an entry without either has no metadata at all.
3627
+ function metadataFromPage(metadata) {
3486
3628
  var _a;
3487
3629
  try {
3488
- const details = (_a = JSON.parse(metadata !== null && metadata !== void 0 ? metadata : "{}")) === null || _a === void 0 ? void 0 : _a.details;
3489
- return typeof details === "string" ? details : "";
3630
+ const {
3631
+ details,
3632
+ type
3633
+ } = (_a = JSON.parse(metadata !== null && metadata !== void 0 ? metadata : "{}")) !== null && _a !== void 0 ? _a : {};
3634
+ if (isFilled(details) || isFilled(type)) {
3635
+ return {
3636
+ ...(isFilled(details) && {
3637
+ details
3638
+ }),
3639
+ ...(isFilled(type) && {
3640
+ type
3641
+ })
3642
+ };
3643
+ } else {
3644
+ return undefined;
3645
+ }
3490
3646
  } catch {
3491
- return "";
3647
+ return undefined;
3648
+ }
3649
+ }
3650
+ // Staff impersonating someone browse under `/O<org id>/P<person id>/…`.
3651
+ function withoutImpersonation(segments) {
3652
+ const [org = "", person = ""] = segments;
3653
+ if (org.startsWith("O") && isId(org.slice(1)) && person.startsWith("P") && isId(person.slice(1))) {
3654
+ return segments.slice(2);
3655
+ } else {
3656
+ return segments;
3657
+ }
3658
+ }
3659
+ // Normalized the way home-mobile does it, so a page visited on web and on
3660
+ // mobile is one entry once the list syncs.
3661
+ function pathFromLocation({
3662
+ pathname,
3663
+ search
3664
+ }) {
3665
+ const segments = withoutImpersonation(pathname.split("/").filter(Boolean));
3666
+ if (segments.length === 0) {
3667
+ return "/";
3668
+ } else {
3669
+ return `/${segments.join("/")}${search.length > 1 ? search : ""}`;
3492
3670
  }
3493
3671
  }
3494
3672
  // pco:resource is the more specific signal, so it wins when both tags are
@@ -3498,17 +3676,20 @@ function visitFromPage({
3498
3676
  resource,
3499
3677
  title
3500
3678
  }) {
3501
- const product = currentProduct();
3679
+ const app = currentApp();
3502
3680
  if (resource && isPcrl(resource)) {
3503
3681
  return {
3504
3682
  pcrl: resource,
3505
3683
  type: "resource"
3506
3684
  };
3507
- } else if (title && title.trim() !== "" && product) {
3685
+ } else if (title && title.trim() !== "" && app) {
3686
+ const pageMetadata = metadataFromPage(metadata);
3508
3687
  return {
3509
- details: detailsFromMetadata(metadata),
3510
- path: window.location.pathname,
3511
- product,
3688
+ app,
3689
+ ...(pageMetadata && {
3690
+ metadata: pageMetadata
3691
+ }),
3692
+ path: pathFromLocation(window.location),
3512
3693
  title,
3513
3694
  type: "page"
3514
3695
  };
@@ -3540,9 +3721,136 @@ function useCurrentPersonId() {
3540
3721
  return userId == null ? undefined : String(userId);
3541
3722
  }
3542
3723
 
3543
- // Per-origin localStorage is a stopgap: each product keeps its own list until
3544
- // PCA-409 moves this to storage shared across product subdomains. Only this
3545
- // file should need to change when it does.
3724
+ // Tokens last 15 minutes. Refreshing a minute early keeps one from expiring
3725
+ // between reading a token and the service checking it.
3726
+ const REFRESH_MARGIN_MS = 60000;
3727
+ const MAX_ATTEMPTS = 3;
3728
+ // A product without the endpoint answers 404 for the rest of the session, so
3729
+ // it's asked once per tab rather than on every page.
3730
+ const UNAVAILABLE_KEY = "topbar.user_store_sync.unavailable";
3731
+ // Per person, so switching connected people never writes one person's visits
3732
+ // with another's token.
3733
+ function tokenKey(personId) {
3734
+ return `topbar.user_store_sync.token.${personId}`;
3735
+ }
3736
+ function readSession(key) {
3737
+ try {
3738
+ return sessionStorage.getItem(key);
3739
+ } catch {
3740
+ return null;
3741
+ }
3742
+ }
3743
+ function writeSession(key, value) {
3744
+ try {
3745
+ sessionStorage.setItem(key, value);
3746
+ } catch {
3747
+ // Without sessionStorage the next request just mints again.
3748
+ }
3749
+ }
3750
+ function isUserStoreSyncUnavailable() {
3751
+ return readSession(UNAVAILABLE_KEY) === "true";
3752
+ }
3753
+ function isToken(value) {
3754
+ return typeof value === "object" && value !== null && typeof value.token === "string" && typeof value.url === "string" && typeof value.expiresAt === "number";
3755
+ }
3756
+ function cachedToken(personId) {
3757
+ var _a;
3758
+ try {
3759
+ const token = JSON.parse((_a = readSession(tokenKey(personId))) !== null && _a !== void 0 ? _a : "null");
3760
+ if (isToken(token) && token.expiresAt - REFRESH_MARGIN_MS > Date.now()) {
3761
+ return token;
3762
+ } else {
3763
+ return null;
3764
+ }
3765
+ } catch {
3766
+ return null;
3767
+ }
3768
+ }
3769
+ async function mintToken(personId) {
3770
+ const {
3771
+ json,
3772
+ ok,
3773
+ response
3774
+ } = await apiRequest(railsCsrfFetch, `${window.location.origin}/user_store_sync/token`, {
3775
+ method: "POST"
3776
+ });
3777
+ if (ok && typeof (json === null || json === void 0 ? void 0 : json.token) === "string" && typeof (json === null || json === void 0 ? void 0 : json.url) === "string") {
3778
+ const token = {
3779
+ expiresAt: Date.parse(json.expires_at),
3780
+ token: json.token,
3781
+ url: json.url
3782
+ };
3783
+ writeSession(tokenKey(personId), JSON.stringify(token));
3784
+ return token;
3785
+ } else {
3786
+ if ((response === null || response === void 0 ? void 0 : response.status) === 404) {
3787
+ writeSession(UNAVAILABLE_KEY, "true");
3788
+ }
3789
+ return null;
3790
+ }
3791
+ }
3792
+ async function request(personId, key, init = {}, isRetry = false) {
3793
+ var _a;
3794
+ const token = isRetry ? await mintToken(personId) : (_a = cachedToken(personId)) !== null && _a !== void 0 ? _a : await mintToken(personId);
3795
+ if (token) {
3796
+ const response = await fetch(`${token.url}/v1/data/${key}`, {
3797
+ ...init,
3798
+ headers: {
3799
+ ...init.headers,
3800
+ Authorization: `Bearer ${token.token}`
3801
+ }
3802
+ });
3803
+ if (response.status === 401 && !isRetry) {
3804
+ return request(personId, key, init, true);
3805
+ } else {
3806
+ return response;
3807
+ }
3808
+ } else {
3809
+ return null;
3810
+ }
3811
+ }
3812
+ // Applies `change` to the stored value and writes the result, rereading and
3813
+ // retrying when another tab or device wrote in between. `change` returns null
3814
+ // when there's nothing to write. Resolves to the value now stored, or null
3815
+ // when the service couldn't be reached.
3816
+ async function updateStoredValue(personId, key, change, attemptsLeft = MAX_ATTEMPTS) {
3817
+ const read = await request(personId, key);
3818
+ if (read === null || read === void 0 ? void 0 : read.ok) {
3819
+ const {
3820
+ value,
3821
+ version
3822
+ } = await read.json();
3823
+ const next = change(value);
3824
+ if (next === null) {
3825
+ return Array.isArray(value) ? value : [];
3826
+ } else {
3827
+ const write = await request(personId, key, {
3828
+ body: JSON.stringify({
3829
+ value: next
3830
+ }),
3831
+ headers: {
3832
+ "Content-Type": "application/json",
3833
+ "If-Match": `"${version}"`
3834
+ },
3835
+ method: "PUT"
3836
+ });
3837
+ if (write === null || write === void 0 ? void 0 : write.ok) {
3838
+ return next;
3839
+ } else if ((write === null || write === void 0 ? void 0 : write.status) === 409 && attemptsLeft > 1) {
3840
+ return updateStoredValue(personId, key, change, attemptsLeft - 1);
3841
+ } else {
3842
+ return null;
3843
+ }
3844
+ }
3845
+ } else {
3846
+ return null;
3847
+ }
3848
+ }
3849
+
3850
+ // Each product keeps its own copy in localStorage, and user-store-sync joins
3851
+ // the copies up across products and devices wherever the product can mint a
3852
+ // token for it.
3853
+ const SYNC_KEY = "recently_viewed";
3546
3854
  function storageKey({
3547
3855
  env,
3548
3856
  personId
@@ -3556,21 +3864,62 @@ const changes = createStore({
3556
3864
  });
3557
3865
  function readRecentlyVisited(scope) {
3558
3866
  try {
3559
- return parseRecentlyVisited(localStorage.getItem(storageKey(scope)), scope.personId);
3867
+ return parseRecentlyVisited(localStorage.getItem(storageKey(scope)));
3560
3868
  } catch {
3561
3869
  return [];
3562
3870
  }
3563
3871
  }
3564
- function recordVisit(visit, scope) {
3565
- if (isEntry(visit)) {
3566
- try {
3567
- localStorage.setItem(storageKey(scope), serializeRecentlyVisited(addVisit(readRecentlyVisited(scope), visit), scope.personId));
3568
- changes.setState({
3569
- version: changes.getState().version + 1
3570
- });
3571
- } catch {
3572
- // Storage can be full or blocked; recents are disposable.
3872
+ function writeRecentlyVisited(scope, entries) {
3873
+ try {
3874
+ localStorage.setItem(storageKey(scope), JSON.stringify(entries));
3875
+ changes.setState({
3876
+ version: changes.getState().version + 1
3877
+ });
3878
+ } catch {
3879
+ // Storage can be full or blocked; recents are disposable.
3880
+ }
3881
+ }
3882
+ async function syncOnce(scope) {
3883
+ try {
3884
+ const synced = await updateStoredValue(scope.personId, SYNC_KEY, stored => {
3885
+ const shared = entriesFrom(stored);
3886
+ const merged = mergeEntries(shared, readRecentlyVisited(scope));
3887
+ const isUnchanged = Array.isArray(stored) && stored.length === shared.length && isSameList(merged, shared);
3888
+ return isUnchanged ? null : merged;
3889
+ });
3890
+ if (synced) {
3891
+ writeRecentlyVisited(scope, mergeEntries(entriesFrom(synced), readRecentlyVisited(scope)));
3573
3892
  }
3893
+ } catch {
3894
+ // The service is best-effort; the local copy stands on its own.
3895
+ }
3896
+ }
3897
+ const syncing = new Map();
3898
+ const syncAgain = new Set();
3899
+ // One sync per list at a time. Asking while one runs schedules a single
3900
+ // follow-up, so a visit recorded mid-sync still reaches the service.
3901
+ function syncRecentlyVisited(scope) {
3902
+ const key = storageKey(scope);
3903
+ const running = syncing.get(key);
3904
+ if (isUserStoreSyncUnavailable()) {
3905
+ return Promise.resolve();
3906
+ } else if (running) {
3907
+ syncAgain.add(key);
3908
+ return running;
3909
+ } else {
3910
+ const run = syncOnce(scope).then(() => {
3911
+ syncing.delete(key);
3912
+ return syncAgain.delete(key) ? syncRecentlyVisited(scope) : undefined;
3913
+ });
3914
+ syncing.set(key, run);
3915
+ return run;
3916
+ }
3917
+ }
3918
+ function recordVisit(visit, scope) {
3919
+ const entry = toEntry(visit, new Date());
3920
+ if (isEntry(entry)) {
3921
+ writeRecentlyVisited(scope, addVisit(readRecentlyVisited(scope), entry));
3922
+ syncRecentlyVisited(scope);
3574
3923
  }
3575
3924
  }
3576
3925
  function useRecentlyVisited({
@@ -3578,6 +3927,14 @@ function useRecentlyVisited({
3578
3927
  personId
3579
3928
  }) {
3580
3929
  const version = shim.useSyncExternalStore(changes.subscribe, () => changes.getState().version);
3930
+ React.useEffect(() => {
3931
+ if (personId) {
3932
+ syncRecentlyVisited({
3933
+ env,
3934
+ personId
3935
+ });
3936
+ }
3937
+ }, [env, personId]);
3581
3938
  // `version` isn't read, but listing it re-reads storage after each visit.
3582
3939
  return React.useMemo(() => personId ? readRecentlyVisited({
3583
3940
  env,
@@ -3822,10 +4179,10 @@ function pcrlLookupsUrl(product, pcrls) {
3822
4179
  });
3823
4180
  return `${window.location.origin}/~api/${product}/v2/pcrl_lookups?${params}`;
3824
4181
  }
3825
- // Built from the product rather than the current origin, so it stays right
3826
- // once PCA-409 shares one list across product subdomains.
4182
+ // Built from the app rather than the current origin, so it stays right once
4183
+ // PCA-409 shares one list across product subdomains.
3827
4184
  function pageUrl(entry, env) {
3828
- return `${pcoUrl(env)(entry.product)}${entry.path}`;
4185
+ return new URL(entry.path, pcoUrl(env)(entry.app)).href;
3829
4186
  }
3830
4187
 
3831
4188
  var styles$a = {"result":"result-WCnxr"};
@@ -4018,15 +4375,16 @@ function ShowMoreWhenVisible({
4018
4375
  // Unlike search results, recents come from every product, so each one names
4019
4376
  // its product and type.
4020
4377
  function describeVisit(visit) {
4378
+ var _a, _b;
4021
4379
  const subtitle = (...parts) => parts.filter(Boolean).join(" · ");
4022
4380
  if (visit.status === "page") {
4023
4381
  const {
4024
- details,
4025
- product,
4382
+ app,
4383
+ metadata,
4026
4384
  title
4027
4385
  } = visit.entry;
4028
4386
  return {
4029
- subtitle: subtitle(productName(product), details),
4387
+ 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 : ""),
4030
4388
  title,
4031
4389
  url: visit.url
4032
4390
  };
@@ -4361,13 +4719,15 @@ function SearchButtonAndDialog({
4361
4719
  const [open, setOpen] = React.useState(false);
4362
4720
  React.useEffect(() => {
4363
4721
  // Rich text editors like Trix bind ⌘K to "insert link"; capturing at the
4364
- // window and stopping propagation keeps them from ever seeing it.
4722
+ // window and stopping propagation keeps them from ever seeing it. Under a
4723
+ // product's modal, search stays shut rather than stacking a second modal,
4724
+ // and the keystroke goes on to the product.
4365
4725
  const handleKeyDown = event => {
4366
- if (isSearchShortcut(event)) {
4726
+ if (open && isSearchShortcut(event)) {
4367
4727
  event.preventDefault();
4368
4728
  event.stopPropagation();
4369
- setOpen(wasOpen => !wasOpen);
4370
- } else if (isLegacySearchShortcut(event)) {
4729
+ setOpen(false);
4730
+ } else if (!open && (isSearchShortcut(event) || isLegacySearchShortcut(event)) && !isModalOpen()) {
4371
4731
  event.preventDefault();
4372
4732
  event.stopPropagation();
4373
4733
  setOpen(true);
@@ -4379,7 +4739,7 @@ function SearchButtonAndDialog({
4379
4739
  return () => window.removeEventListener("keydown", handleKeyDown, {
4380
4740
  capture: true
4381
4741
  });
4382
- }, []);
4742
+ }, [open]);
4383
4743
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("button", {
4384
4744
  "aria-keyshortcuts": `${isMac() ? "Meta+K" : "Control+K"} /`,
4385
4745
  "aria-label": "Search",
@@ -33694,42 +34054,26 @@ function UserProfile({
33694
34054
 
33695
34055
  var ActionType;
33696
34056
  (function (ActionType) {
33697
- ActionType["clearPending"] = "clearPending";
33698
34057
  ActionType["close"] = "close";
33699
34058
  ActionType["mount"] = "mount";
33700
34059
  ActionType["open"] = "open";
33701
- ActionType["setPending"] = "setPending";
33702
34060
  ActionType["unmount"] = "unmount";
33703
34061
  })(ActionType || (ActionType = {}));
33704
34062
  const initialState = {
33705
34063
  currentContent: null,
33706
- open: false,
33707
- pending: null
34064
+ open: false
33708
34065
  };
33709
34066
  const reducer = (state, action) => {
33710
- var _a;
33711
34067
  switch (action.type) {
33712
34068
  case ActionType.mount:
33713
34069
  return {
33714
34070
  ...state,
33715
- currentContent: action.payload.content,
33716
- pending: ((_a = state.pending) === null || _a === void 0 ? void 0 : _a.tool) === action.payload.content.name ? state.pending : null
34071
+ currentContent: action.payload.content
33717
34072
  };
33718
34073
  case ActionType.unmount:
33719
34074
  return {
33720
34075
  ...state,
33721
- currentContent: null,
33722
- pending: null
33723
- };
33724
- case ActionType.setPending:
33725
- return {
33726
- ...state,
33727
- pending: action.payload
33728
- };
33729
- case ActionType.clearPending:
33730
- return {
33731
- ...state,
33732
- pending: null
34076
+ currentContent: null
33733
34077
  };
33734
34078
  case ActionType.open:
33735
34079
  return {
@@ -33752,7 +34096,7 @@ var styles$1 = {"toolbar":"toolbar-KHJR5","toolbar-button":"toolbar-button-Ye4h5
33752
34096
 
33753
34097
  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"};
33754
34098
 
33755
- 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";
34099
+ 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";
33756
34100
  // Same stops as the AskAIBanner border gradient, so the two stay in sync.
33757
34101
  const GRADIENT_STOPS = [{
33758
34102
  color: "var(--t-fill-color-tag-teal-040)",
@@ -33801,7 +34145,7 @@ function SparkleIcon({
33801
34145
  stopColor: color
33802
34146
  }
33803
34147
  })))))), /*#__PURE__*/React.createElement("path", {
33804
- d: SPARKLE_PATH$1,
34148
+ d: SPARKLE_PATH,
33805
34149
  fill: isGradient ? `url(#${gradientId})` : "currentColor"
33806
34150
  }));
33807
34151
  }
@@ -34209,24 +34553,16 @@ const DocumentationSearch = ({
34209
34553
  // not the path, so updating it is a same-document navigation that doesn't
34210
34554
  // reload the iframe, but also lets the route survive a forced reload (e.g.
34211
34555
  // Turbo re-parenting the topbar).
34212
- //
34213
- // `panel` loads picobot as its own toolbar panel rather than inside Helpdesk.
34214
- function buildPicobotSrc(origin, path, {
34215
- panel = false
34216
- } = {}) {
34556
+ function buildPicobotSrc(origin, path) {
34217
34557
  const [rawPath, queryPart] = path.replace(/^\/+/, "").split("?", 2);
34218
34558
  const safePath = rawPath.split("/").filter(segment => segment && segment !== "." && segment !== "..").join("/");
34219
34559
  const fragment = queryPart !== undefined ? `${safePath}?${queryPart}` : safePath;
34220
- const url = new URL("topbar", origin);
34221
- if (panel) url.searchParams.set("panel", "1");
34222
- const base = url.toString();
34560
+ const base = new URL("topbar", origin).toString();
34223
34561
  return fragment ? `${base}#${fragment}` : base;
34224
34562
  }
34225
34563
 
34226
34564
  const LOAD_TIMEOUT_MS = 3000;
34227
- function createPicobotIframe(picobotOrigin, {
34228
- panel = false
34229
- } = {}) {
34565
+ function createPicobotIframe(picobotOrigin) {
34230
34566
  return /*#__PURE__*/React.forwardRef(function PicobotIframe({
34231
34567
  currentPath: iframePath,
34232
34568
  onChangePath: onIframeChangePath,
@@ -34282,9 +34618,7 @@ function createPicobotIframe(picobotOrigin, {
34282
34618
  (_b = (_a = iframeRef.current) === null || _a === void 0 ? void 0 : _a.contentWindow) === null || _b === void 0 ? void 0 : _b.postMessage(message, picobotOrigin);
34283
34619
  }
34284
34620
  }), []);
34285
- const iframeSrc = buildPicobotSrc(picobotOrigin, loadedPath, {
34286
- panel
34287
- });
34621
+ const iframeSrc = buildPicobotSrc(picobotOrigin, loadedPath);
34288
34622
  return /*#__PURE__*/React.createElement("iframe", {
34289
34623
  ref: iframeRef,
34290
34624
  src: iframeSrc,
@@ -34323,28 +34657,22 @@ function HelpdeskComponent({
34323
34657
  currentPath,
34324
34658
  env,
34325
34659
  helpdeskConfig,
34326
- isPicobotLoading,
34327
- onChangePath,
34328
- openPicobot
34660
+ onChangePath
34329
34661
  }) {
34330
34662
  const PicobotIframe = React.useMemo(() => createPicobotIframe(pcoUrl(env)("picobot")), [env]);
34331
- const loadingFallback = /*#__PURE__*/React.createElement(StackView$1, {
34332
- flex: 1,
34333
- axis: "vertical"
34334
- }, /*#__PURE__*/React.createElement(DrawerHeader, {
34335
- closeDrawer: closeDrawer
34336
- }, /*#__PURE__*/React.createElement(StackView$1, {
34337
- axis: "horizontal",
34338
- spacing: 1
34339
- }, /*#__PURE__*/React.createElement(DrawerHeading, null, "Help and support"), /*#__PURE__*/React.createElement(LoadingIndicator$1, null))));
34340
- // The add-on reads its args once, so mounting before the gate answers
34341
- // would leave out openPicobot for good.
34342
- if (isPicobotLoading) return loadingFallback;
34343
34663
  return /*#__PURE__*/React.createElement(addOns.UiExtensionFirstPartyComponent, {
34344
34664
  grow: 1,
34345
34665
  display: "flex",
34346
34666
  extensionId: env === "staging" ? "42" : "11",
34347
- loadingFallback: loadingFallback,
34667
+ loadingFallback: /*#__PURE__*/React.createElement(StackView$1, {
34668
+ flex: 1,
34669
+ axis: "vertical"
34670
+ }, /*#__PURE__*/React.createElement(DrawerHeader, {
34671
+ closeDrawer: closeDrawer
34672
+ }, /*#__PURE__*/React.createElement(StackView$1, {
34673
+ axis: "horizontal",
34674
+ spacing: 1
34675
+ }, /*#__PURE__*/React.createElement(DrawerHeading, null, "Help and support"), /*#__PURE__*/React.createElement(LoadingIndicator$1, null)))),
34348
34676
  additionalComponents: {
34349
34677
  AskAIBanner: {
34350
34678
  component: AskAIBanner,
@@ -34442,11 +34770,7 @@ function HelpdeskComponent({
34442
34770
  currentPageParams: location.search,
34443
34771
  currentPath,
34444
34772
  helpdeskConfig,
34445
- onChangePath,
34446
- // Its presence tells helpdesk that Picobot has its own panel.
34447
- ...(openPicobot && {
34448
- openPicobot
34449
- })
34773
+ onChangePath
34450
34774
  }
34451
34775
  });
34452
34776
  }
@@ -34993,142 +35317,6 @@ const Notifications = {
34993
35317
  name: "Notifications"
34994
35318
  };
34995
35319
 
34996
- // The sparkle sits clear of the unread dot at (24, 8), so both variants match.
34997
- 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";
34998
- // Helpdesk reads this context off the email path to open its email form the
34999
- // same way a handoff inside its own drawer does.
35000
- function emailPathWithContext(context = {}) {
35001
- const params = new URLSearchParams();
35002
- for (const key of ["conversationId", "source", "turnIndex"]) {
35003
- const value = context[key];
35004
- if (value !== null && value !== undefined) params.set(key, String(value));
35005
- }
35006
- return `email?${params}`;
35007
- }
35008
- function PicobotComponent({
35009
- clearPending,
35010
- closeDrawer,
35011
- currentPath,
35012
- env,
35013
- onChangePath,
35014
- openTool,
35015
- pending
35016
- }) {
35017
- const PicobotIframe = React.useMemo(() => createPicobotIframe(pcoUrl(env)("picobot"), {
35018
- panel: true
35019
- }), [env]);
35020
- const picobotRef = React.useRef(null);
35021
- const [isPicobotReady, setIsPicobotReady] = React.useState(false);
35022
- // Picobot has no listener until it sends READY, so openPicobot's draft or
35023
- // conversation waits for it.
35024
- React.useEffect(function handOverPendingOncePicobotIsReady() {
35025
- var _a, _b;
35026
- if (!pending || !isPicobotReady) return;
35027
- const {
35028
- conversationId,
35029
- draft,
35030
- ticketExternalId
35031
- } = pending;
35032
- if (conversationId) {
35033
- (_a = picobotRef.current) === null || _a === void 0 ? void 0 : _a.postMessage({
35034
- payload: {
35035
- conversation_id: conversationId,
35036
- // Picobot asks whether this resolved the ticket, and closes it.
35037
- ...(ticketExternalId && {
35038
- external_id: ticketExternalId,
35039
- source: "ticket_submitted"
35040
- })
35041
- },
35042
- type: "LOAD_CONVERSATION"
35043
- });
35044
- }
35045
- if (draft) {
35046
- (_b = picobotRef.current) === null || _b === void 0 ? void 0 : _b.postMessage({
35047
- payload: {
35048
- source: "email_form",
35049
- text: draft
35050
- },
35051
- type: "PREFILL_COMPOSER"
35052
- });
35053
- }
35054
- clearPending();
35055
- }, [pending, isPicobotReady, clearPending]);
35056
- const [iframeErrored, setIframeErrored] = React.useState(false);
35057
- const [mountKey, setMountKey] = React.useState(0);
35058
- const handleRetry = React.useCallback(() => {
35059
- setIframeErrored(false);
35060
- setIsPicobotReady(false);
35061
- setMountKey(key => key + 1);
35062
- }, []);
35063
- const handleMessage = React.useCallback(data => {
35064
- var _a;
35065
- const message = data;
35066
- switch (message === null || message === void 0 ? void 0 : message.type) {
35067
- case "CLOSE_DRAWER":
35068
- return closeDrawer();
35069
- case "READY":
35070
- return setIsPicobotReady(true);
35071
- case "PICOBOT_SUPPORT_HANDOFF":
35072
- return openTool("helpdesk", ((_a = message.payload) === null || _a === void 0 ? void 0 : _a.method) === "zoom" ? "phone" : emailPathWithContext(message.payload));
35073
- case "IFRAME_LOAD_ERROR":
35074
- console.warn("topbar: Picobot iframe failed to load");
35075
- return setIframeErrored(true);
35076
- }
35077
- }, [closeDrawer, openTool]);
35078
- if (iframeErrored) {
35079
- return /*#__PURE__*/React.createElement(LoadError, {
35080
- onContactSupport: () => openTool("helpdesk", "email"),
35081
- onRetry: handleRetry
35082
- });
35083
- }
35084
- return /*#__PURE__*/React.createElement(PicobotIframe, {
35085
- key: mountKey,
35086
- ref: picobotRef,
35087
- currentPath: currentPath,
35088
- onChangePath: onChangePath,
35089
- onMessage: handleMessage
35090
- });
35091
- }
35092
- function LoadError({
35093
- onContactSupport,
35094
- onRetry
35095
- }) {
35096
- return /*#__PURE__*/React.createElement(tapestryReact.StackView, {
35097
- axis: "vertical",
35098
- flex: 1,
35099
- alignment: "center",
35100
- distribution: "center",
35101
- padding: 3,
35102
- spacing: 1
35103
- }, /*#__PURE__*/React.createElement(tapestryReact.Heading, {
35104
- level: 2
35105
- }, "Picobot is temporarily unavailable"), /*#__PURE__*/React.createElement(tapestryReact.Text, {
35106
- size: 5,
35107
- color: "var(--t-text-color-secondary)"
35108
- }, "Try again or contact support."), /*#__PURE__*/React.createElement(tapestryReact.Box, {
35109
- paddingTop: 2
35110
- }, /*#__PURE__*/React.createElement(tapestryReact.Button, {
35111
- onClick: onRetry,
35112
- iconLeft: "general.refresh",
35113
- theme: "primary",
35114
- variant: "fill",
35115
- title: "Retry"
35116
- })), /*#__PURE__*/React.createElement(tapestryReact.Button, {
35117
- onClick: onContactSupport,
35118
- variant: "naked",
35119
- theme: "primary",
35120
- title: "Contact support"
35121
- }));
35122
- }
35123
- const Picobot = {
35124
- component: PicobotComponent,
35125
- icons: {
35126
- staticDotPath: SPARKLE_PATH,
35127
- staticPath: SPARKLE_PATH
35128
- },
35129
- name: "Picobot"
35130
- };
35131
-
35132
35320
  const {
35133
35321
  LoadingIndicator,
35134
35322
  StackView
@@ -35524,35 +35712,6 @@ const useChatConfig = ({
35524
35712
  };
35525
35713
  };
35526
35714
 
35527
- // Picobot's API rejects requests without a version header, and apiRequest
35528
- // sets its own headers, so the version rides in on the fetch.
35529
- const picobotFetch = (url, opts) => defaultFetch(url, {
35530
- ...opts,
35531
- headers: {
35532
- ...(opts === null || opts === void 0 ? void 0 : opts.headers),
35533
- "X-PCO-API-Version": "2026-08-11"
35534
- }
35535
- });
35536
- const usePicobotConfig = ({
35537
- isEnabled
35538
- }) => {
35539
- // Undefined until Picobot answers.
35540
- const [panelEnabled, setPanelEnabled] = React.useState();
35541
- React.useEffect(() => {
35542
- if (!isEnabled) return;
35543
- apiRequest(picobotFetch, `${window.location.origin}/~api/picobot/v2/me`).then(({
35544
- json
35545
- }) => {
35546
- var _a, _b;
35547
- return setPanelEnabled(((_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);
35548
- }).catch(() => setPanelEnabled(false));
35549
- }, [isEnabled]);
35550
- return {
35551
- displayPicobotIcon: panelEnabled === true,
35552
- isLoading: isEnabled && panelEnabled === undefined
35553
- };
35554
- };
35555
-
35556
35715
  const Profile = {
35557
35716
  component: UserProfile,
35558
35717
  icons: {
@@ -35563,16 +35722,10 @@ const Profile = {
35563
35722
  };
35564
35723
 
35565
35724
  const DURATION = 100;
35566
- // Exempt paths match regardless of query, so `email?conversationId=…` is
35567
- // exempt along with `email`.
35568
35725
  function isToolPathExemptFromStorage(toolName, path) {
35569
35726
  var _a, _b;
35570
35727
  const tool = toolsRouteMap[toolName];
35571
- const [pathWithoutQuery] = path.split("?");
35572
- 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;
35573
- }
35574
- function isConversationPath(path) {
35575
- return Boolean(path === null || path === void 0 ? void 0 : path.startsWith("conversations/"));
35728
+ 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;
35576
35729
  }
35577
35730
  function scrollToolDrawerToTop(toolName) {
35578
35731
  var _a;
@@ -35585,7 +35738,6 @@ const TOOLS_MAP = {
35585
35738
  Chat,
35586
35739
  Helpdesk,
35587
35740
  Notifications,
35588
- Picobot,
35589
35741
  Profile,
35590
35742
  Tasks
35591
35743
  };
@@ -35613,8 +35765,7 @@ function Toolbar({
35613
35765
  });
35614
35766
  const {
35615
35767
  currentContent,
35616
- open,
35617
- pending
35768
+ open
35618
35769
  } = state;
35619
35770
  const mounted = !!currentContent;
35620
35771
  const storeData = useStore();
@@ -35627,15 +35778,6 @@ function Toolbar({
35627
35778
  ...(env === "development" ? _storeDataOverride !== null && _storeDataOverride !== void 0 ? _storeDataOverride : {} : {})
35628
35779
  };
35629
35780
  const updateStore = useUpdateStore();
35630
- const {
35631
- displayPicobotIcon,
35632
- isLoading: isPicobotGateLoading
35633
- } = usePicobotConfig({
35634
- isEnabled: Boolean(userId)
35635
- });
35636
- // Picobot is a kill-switched tool: until its gate says the panel is
35637
- // enabled, its routes are held, and if it never does, they're ignored.
35638
- const routableTool = React.useCallback(name => name === Picobot.name.toLowerCase() && !displayPicobotIcon ? undefined : toolsRouteMap[name], [displayPicobotIcon]);
35639
35781
  React.useEffect(() => {
35640
35782
  if (open) {
35641
35783
  document.body.style.overflow = "hidden";
@@ -35723,9 +35865,7 @@ function Toolbar({
35723
35865
  });
35724
35866
  }, [clearRoute]);
35725
35867
  React.useEffect(() => {
35726
- if (!toolbarFragmentRoute || !routableTool(toolbarFragmentRoute.tool)) {
35727
- return;
35728
- }
35868
+ if (!toolbarFragmentRoute) return;
35729
35869
  const isExempt = isToolPathExemptFromStorage(toolbarFragmentRoute.tool, toolbarFragmentRoute.path);
35730
35870
  if (isExempt) return;
35731
35871
  const nextState = {
@@ -35735,32 +35875,12 @@ function Toolbar({
35735
35875
  if (!shallowCompare(nextState, toolStateCookie)) {
35736
35876
  setToolStateCookie(nextState);
35737
35877
  }
35738
- }, [toolbarFragmentRoute, toolStateCookie, routableTool]);
35878
+ }, [toolbarFragmentRoute, toolStateCookie]);
35739
35879
  React.useEffect(() => {
35740
- // Helpdesk conversations may belong to Picobot, so they wait for its gate
35741
- // rather than flashing open in Helpdesk first.
35742
- const mayRedirectToPicobot = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) === Helpdesk.name.toLowerCase() && isConversationPath(toolbarFragmentRoute.path) && (isPicobotGateLoading || displayPicobotIcon);
35743
- const tool = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && !mayRedirectToPicobot && routableTool(toolbarFragmentRoute.tool);
35744
- if (tool && (currentContent !== tool || !open)) {
35745
- setToolAndOpenDrawer(tool);
35880
+ if ((toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && (currentContent !== toolsRouteMap[toolbarFragmentRoute.tool] || !open)) {
35881
+ setToolAndOpenDrawer(toolsRouteMap[toolbarFragmentRoute.tool]);
35746
35882
  }
35747
- }, [setToolAndOpenDrawer, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path, currentContent, open, routableTool, isPicobotGateLoading, displayPicobotIcon]);
35748
- const openTool = React.useCallback((name, path, pendingValue) => {
35749
- const tool = routableTool(name.toLowerCase());
35750
- if (!tool) return;
35751
- dispatch({
35752
- payload: pendingValue === undefined ? null : {
35753
- tool: tool.name,
35754
- value: pendingValue
35755
- },
35756
- type: ActionType.setPending
35757
- });
35758
- onChangeToolbarFragmentRoute(tool.name.toLowerCase(), path);
35759
- scrollToolDrawerToTop(tool.name);
35760
- }, [onChangeToolbarFragmentRoute, routableTool]);
35761
- const clearPending = React.useCallback(() => dispatch({
35762
- type: ActionType.clearPending
35763
- }), []);
35883
+ }, [setToolAndOpenDrawer, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool, currentContent, open]);
35764
35884
  const toggleDrawer = content => {
35765
35885
  if (!open) {
35766
35886
  return openDrawer(content);
@@ -35777,47 +35897,12 @@ function Toolbar({
35777
35897
  joltPromise: storeData === null || storeData === void 0 ? void 0 : storeData.joltPromise,
35778
35898
  userId
35779
35899
  });
35780
- // Picobot conversations moved from Helpdesk to the Picobot panel, so
35781
- // send old links and saved paths there once the panel is enabled.
35782
- React.useEffect(() => {
35783
- if (!displayPicobotIcon) return;
35784
- if ((toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) === Helpdesk.name.toLowerCase() && isConversationPath(toolbarFragmentRoute.path)) {
35785
- onChangeToolbarFragmentRoute(Picobot.name.toLowerCase(), toolbarFragmentRoute.path);
35786
- }
35787
- }, [displayPicobotIcon, toolbarFragmentRoute, onChangeToolbarFragmentRoute]);
35788
- React.useEffect(() => {
35789
- if (!displayPicobotIcon) return;
35790
- const {
35791
- helpdesk,
35792
- picobot
35793
- } = toolStateCookie;
35794
- if (!isConversationPath(helpdesk)) return;
35795
- setToolStateCookie({
35796
- ...toolStateCookie,
35797
- helpdesk: "",
35798
- picobot: picobot || helpdesk
35799
- });
35800
- }, [displayPicobotIcon, toolStateCookie]);
35801
- const openPicobot = React.useCallback(options => openTool(Picobot.name, "", options), [openTool]);
35802
- // Like Helpdesk's openPicobot arg, this only exists when the panel does.
35803
- React.useEffect(() => {
35804
- if (!displayPicobotIcon) return undefined;
35805
- window.topbar = window.topbar || {};
35806
- window.topbar.openPicobot = openPicobot;
35807
- return () => {
35808
- delete window.topbar.openPicobot;
35809
- };
35810
- }, [displayPicobotIcon, openPicobot]);
35811
- const TOOLS = [Notifications, Tasks, displayChatIcon && Chat, displayPicobotIcon && Picobot].filter(Boolean);
35900
+ const TOOLS = [Notifications, Tasks, displayChatIcon && Chat].filter(Boolean);
35812
35901
  const handleToolOnClick = tool => {
35813
35902
  toggleDrawer(tool);
35814
35903
  scrollToolDrawerToTop(tool.name);
35815
35904
  };
35816
35905
  const toolSpecificProps = {
35817
- Helpdesk: {
35818
- isPicobotLoading: isPicobotGateLoading,
35819
- openPicobot: displayPicobotIcon ? openPicobot : undefined
35820
- },
35821
35906
  Profile: {
35822
35907
  appName,
35823
35908
  closeDrawer,
@@ -35840,7 +35925,6 @@ function Toolbar({
35840
35925
  const routeIsForCurrentTool = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && toolsRouteMap[toolbarFragmentRoute.tool] === currentContent;
35841
35926
  const currentToolProps = {
35842
35927
  appName,
35843
- clearPending,
35844
35928
  closeDrawer,
35845
35929
  currentPath: routeIsForCurrentTool ? toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path : null,
35846
35930
  env,
@@ -35849,8 +35933,6 @@ function Toolbar({
35849
35933
  joltPromise,
35850
35934
  onChangePath,
35851
35935
  openDrawer: () => currentContent && openDrawer(currentContent),
35852
- openTool,
35853
- 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,
35854
35936
  userId,
35855
35937
  ...(currentContent ? toolSpecificProps[currentContent.name] : {})
35856
35938
  };