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

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,45 +3266,6 @@ 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
-
3308
3269
  var styles$c = {"visuallyHidden":"visuallyHidden-C28ju"};
3309
3270
 
3310
3271
  var styles$b = {"tab":"tab-lQLDx"};
@@ -3436,11 +3397,10 @@ function useRecentlyVisitableMetadata() {
3436
3397
  return shim.useSyncExternalStore(recentlyVisitableMetadataStore.subscribe, () => recentlyVisitableMetadataStore.getState().metadata);
3437
3398
  }
3438
3399
 
3439
- const MAX_ENTRIES$1 = 50;
3400
+ const MAX_ENTRIES$1 = 20;
3401
+ const FORMAT_VERSION = 1;
3440
3402
  const MAX_TEXT_LENGTH = 200;
3441
3403
  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");
3444
3404
  function isId(value) {
3445
3405
  return value !== "" && Array.from(value).every(character => character >= "0" && character <= "9");
3446
3406
  }
@@ -3449,47 +3409,26 @@ function isPcrl(value) {
3449
3409
  const segments = value.split("/");
3450
3410
  return segments.length === 4 && PRODUCTS.some(product => product === segments[0]) && isId(segments[3]);
3451
3411
  }
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
- }
3465
3412
  function isNonBlankString(value) {
3466
3413
  return typeof value === "string" && value.trim() !== "";
3467
3414
  }
3468
- function isOptionalString(value) {
3469
- return value === undefined || typeof value === "string";
3470
- }
3471
3415
  function isRecord(value) {
3472
3416
  return typeof value === "object" && value !== null && !Array.isArray(value);
3473
3417
  }
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.
3479
3418
  function entryKey(entry) {
3480
3419
  if (entry.type === "resource") {
3481
3420
  return `resource:${entry.pcrl}`;
3482
3421
  } else {
3483
- return `page:${entry.app}:${entry.path.split("?")[0]}`;
3422
+ return `page:${entry.product}:${entry.path}`;
3484
3423
  }
3485
3424
  }
3486
3425
  function isEntry(value) {
3487
- if (!isRecord(value) || typeof value.visitedAt !== "string") {
3426
+ if (!isRecord(value)) {
3488
3427
  return false;
3489
3428
  } else if (value.type === "resource") {
3490
3429
  return typeof value.pcrl === "string" && isPcrl(value.pcrl);
3491
3430
  } else if (value.type === "page") {
3492
- return isApp(value.app) && isPath(value.path) && isNonBlankString(value.title) && isPageMetadata(value.metadata);
3431
+ return isNonBlankString(value.product) && isNonBlankString(value.title) && isNonBlankString(value.path) && typeof value.details === "string";
3493
3432
  } else {
3494
3433
  return false;
3495
3434
  }
@@ -3498,175 +3437,58 @@ function isEntry(value) {
3498
3437
  function clampText(value) {
3499
3438
  return Array.from(value).slice(0, MAX_TEXT_LENGTH).join("");
3500
3439
  }
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) {
3440
+ function clampEntry(entry) {
3554
3441
  if (entry.type === "resource") {
3555
- return {
3556
- pcrl: entry.pcrl,
3557
- type: entry.type,
3558
- visitedAt: entry.visitedAt
3559
- };
3442
+ return entry;
3560
3443
  } else {
3561
- const metadata = entry.metadata && knownMetadata(entry.metadata);
3562
3444
  return {
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
3445
+ ...entry,
3446
+ details: clampText(entry.details),
3447
+ title: clampText(entry.title)
3571
3448
  };
3572
3449
  }
3573
3450
  }
3574
3451
  // Recents are disposable: anything unexpected reads as an empty list (or, for
3575
3452
  // a single bad entry, a list without it) rather than throwing.
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) {
3453
+ function parseRecentlyVisited(value, personId) {
3454
+ let parsed = null;
3584
3455
  try {
3585
- return entriesFrom(JSON.parse(value !== null && value !== void 0 ? value : "null"));
3456
+ parsed = JSON.parse(value !== null && value !== void 0 ? value : "null");
3586
3457
  } 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 {
3587
3463
  return [];
3588
3464
  }
3589
3465
  }
3590
- function addVisit(entries, entry) {
3591
- const key = entryKey(entry);
3592
- return [entry, ...entries.filter(existing => entryKey(existing) !== key)].slice(0, MAX_ENTRIES$1);
3593
- }
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);
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);
3610
3469
  }
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]));
3470
+ function serializeRecentlyVisited(entries, personId) {
3471
+ return JSON.stringify({
3472
+ entries,
3473
+ person: personId,
3474
+ v: FORMAT_VERSION
3475
+ });
3613
3476
  }
3614
3477
 
3615
- // The app whose URL, in this environment, has the current page's host.
3478
+ // The product whose URL, in this environment, has the current page's host.
3616
3479
  // Reversing pcoUrl handles every environment @planningcenter/url knows about,
3617
- // and guarantees the stored app rebuilds a working link.
3618
- function currentApp() {
3480
+ // and guarantees the stored product rebuilds a working link.
3481
+ function currentProduct() {
3619
3482
  var _a;
3620
- return (_a = APPS.find(app => new URL(pcoUrl$1.pcoUrl(app)).hostname === window.location.hostname)) !== null && _a !== void 0 ? _a : null;
3483
+ return (_a = PRODUCTS.find(product => new URL(pcoUrl$1.pcoUrl(product)).hostname === window.location.hostname)) !== null && _a !== void 0 ? _a : null;
3621
3484
  }
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) {
3485
+ function detailsFromMetadata(metadata) {
3628
3486
  var _a;
3629
3487
  try {
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
- }
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 : "";
3646
3490
  } catch {
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 : ""}`;
3491
+ return "";
3670
3492
  }
3671
3493
  }
3672
3494
  // pco:resource is the more specific signal, so it wins when both tags are
@@ -3676,20 +3498,17 @@ function visitFromPage({
3676
3498
  resource,
3677
3499
  title
3678
3500
  }) {
3679
- const app = currentApp();
3501
+ const product = currentProduct();
3680
3502
  if (resource && isPcrl(resource)) {
3681
3503
  return {
3682
3504
  pcrl: resource,
3683
3505
  type: "resource"
3684
3506
  };
3685
- } else if (title && title.trim() !== "" && app) {
3686
- const pageMetadata = metadataFromPage(metadata);
3507
+ } else if (title && title.trim() !== "" && product) {
3687
3508
  return {
3688
- app,
3689
- ...(pageMetadata && {
3690
- metadata: pageMetadata
3691
- }),
3692
- path: pathFromLocation(window.location),
3509
+ details: detailsFromMetadata(metadata),
3510
+ path: window.location.pathname,
3511
+ product,
3693
3512
  title,
3694
3513
  type: "page"
3695
3514
  };
@@ -3721,136 +3540,9 @@ function useCurrentPersonId() {
3721
3540
  return userId == null ? undefined : String(userId);
3722
3541
  }
3723
3542
 
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";
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.
3854
3546
  function storageKey({
3855
3547
  env,
3856
3548
  personId
@@ -3864,62 +3556,21 @@ const changes = createStore({
3864
3556
  });
3865
3557
  function readRecentlyVisited(scope) {
3866
3558
  try {
3867
- return parseRecentlyVisited(localStorage.getItem(storageKey(scope)));
3559
+ return parseRecentlyVisited(localStorage.getItem(storageKey(scope)), scope.personId);
3868
3560
  } catch {
3869
3561
  return [];
3870
3562
  }
3871
3563
  }
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)));
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
3564
  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);
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.
3573
+ }
3923
3574
  }
3924
3575
  }
3925
3576
  function useRecentlyVisited({
@@ -3927,14 +3578,6 @@ function useRecentlyVisited({
3927
3578
  personId
3928
3579
  }) {
3929
3580
  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]);
3938
3581
  // `version` isn't read, but listing it re-reads storage after each visit.
3939
3582
  return React.useMemo(() => personId ? readRecentlyVisited({
3940
3583
  env,
@@ -4179,10 +3822,10 @@ function pcrlLookupsUrl(product, pcrls) {
4179
3822
  });
4180
3823
  return `${window.location.origin}/~api/${product}/v2/pcrl_lookups?${params}`;
4181
3824
  }
4182
- // Built from the app rather than the current origin, so it stays right once
4183
- // PCA-409 shares one list across product subdomains.
3825
+ // Built from the product rather than the current origin, so it stays right
3826
+ // once PCA-409 shares one list across product subdomains.
4184
3827
  function pageUrl(entry, env) {
4185
- return new URL(entry.path, pcoUrl(env)(entry.app)).href;
3828
+ return `${pcoUrl(env)(entry.product)}${entry.path}`;
4186
3829
  }
4187
3830
 
4188
3831
  var styles$a = {"result":"result-WCnxr"};
@@ -4375,16 +4018,15 @@ function ShowMoreWhenVisible({
4375
4018
  // Unlike search results, recents come from every product, so each one names
4376
4019
  // its product and type.
4377
4020
  function describeVisit(visit) {
4378
- var _a, _b;
4379
4021
  const subtitle = (...parts) => parts.filter(Boolean).join(" · ");
4380
4022
  if (visit.status === "page") {
4381
4023
  const {
4382
- app,
4383
- metadata,
4024
+ details,
4025
+ product,
4384
4026
  title
4385
4027
  } = visit.entry;
4386
4028
  return {
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 : ""),
4029
+ subtitle: subtitle(productName(product), details),
4388
4030
  title,
4389
4031
  url: visit.url
4390
4032
  };
@@ -4719,15 +4361,13 @@ function SearchButtonAndDialog({
4719
4361
  const [open, setOpen] = React.useState(false);
4720
4362
  React.useEffect(() => {
4721
4363
  // Rich text editors like Trix bind ⌘K to "insert link"; capturing at the
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.
4364
+ // window and stopping propagation keeps them from ever seeing it.
4725
4365
  const handleKeyDown = event => {
4726
- if (open && isSearchShortcut(event)) {
4366
+ if (isSearchShortcut(event)) {
4727
4367
  event.preventDefault();
4728
4368
  event.stopPropagation();
4729
- setOpen(false);
4730
- } else if (!open && (isSearchShortcut(event) || isLegacySearchShortcut(event)) && !isModalOpen()) {
4369
+ setOpen(wasOpen => !wasOpen);
4370
+ } else if (isLegacySearchShortcut(event)) {
4731
4371
  event.preventDefault();
4732
4372
  event.stopPropagation();
4733
4373
  setOpen(true);
@@ -4739,7 +4379,7 @@ function SearchButtonAndDialog({
4739
4379
  return () => window.removeEventListener("keydown", handleKeyDown, {
4740
4380
  capture: true
4741
4381
  });
4742
- }, [open]);
4382
+ }, []);
4743
4383
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("button", {
4744
4384
  "aria-keyshortcuts": `${isMac() ? "Meta+K" : "Control+K"} /`,
4745
4385
  "aria-label": "Search",
@@ -34054,26 +33694,42 @@ function UserProfile({
34054
33694
 
34055
33695
  var ActionType;
34056
33696
  (function (ActionType) {
33697
+ ActionType["clearPending"] = "clearPending";
34057
33698
  ActionType["close"] = "close";
34058
33699
  ActionType["mount"] = "mount";
34059
33700
  ActionType["open"] = "open";
33701
+ ActionType["setPending"] = "setPending";
34060
33702
  ActionType["unmount"] = "unmount";
34061
33703
  })(ActionType || (ActionType = {}));
34062
33704
  const initialState = {
34063
33705
  currentContent: null,
34064
- open: false
33706
+ open: false,
33707
+ pending: null
34065
33708
  };
34066
33709
  const reducer = (state, action) => {
33710
+ var _a, _b;
34067
33711
  switch (action.type) {
34068
33712
  case ActionType.mount:
34069
33713
  return {
34070
33714
  ...state,
34071
- currentContent: action.payload.content
33715
+ currentContent: action.payload.content,
33716
+ 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
34072
33717
  };
34073
33718
  case ActionType.unmount:
34074
33719
  return {
34075
33720
  ...state,
34076
- currentContent: null
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
34077
33733
  };
34078
33734
  case ActionType.open:
34079
33735
  return {
@@ -34094,9 +33750,179 @@ const useToolbarReducer = overrides => React__namespace.useReducer(reducer, {
34094
33750
 
34095
33751
  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"};
34096
33752
 
33753
+ // PCO Box serves TLS by default, and a bare http:// frame inside an https
33754
+ // product page is blocked as mixed content, so follow the page's scheme.
33755
+ const iframeSrc = () => `${window.location.protocol}//pco.test/iframe`;
33756
+ const STAFF_PINK = "var(--t-fill-color-product-staff)";
33757
+ // The pco.test domain is only ever served by a local PCO Box, so the tool
33758
+ // stays hidden in staging and production — and on Topbar's own dev server,
33759
+ // which runs on localhost and has no pco.test to frame.
33760
+ function isPcoTestHost() {
33761
+ const {
33762
+ hostname
33763
+ } = window.location;
33764
+ return hostname === "pco.test" || hostname.endsWith(".pco.test");
33765
+ }
33766
+ // Flattened from the source artwork's two outline paths — its gradients,
33767
+ // filters and clip paths carry nothing at icon size. Those paths are authored
33768
+ // on a 128px canvas, hence the transform: it fits the pair into the same ~24px
33769
+ // optical box the sibling toolbar icons occupy. Filled explicitly rather than
33770
+ // with `currentColor` so the mark stays staff pink instead of inheriting the
33771
+ // toolbar's text color and its hover/active states.
33772
+ 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";
33773
+ 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";
33774
+ function ToolsIcon() {
33775
+ return /*#__PURE__*/React.createElement("svg", {
33776
+ "aria-hidden": "true",
33777
+ width: "32",
33778
+ height: "32",
33779
+ viewBox: "0 0 32 32",
33780
+ xmlns: "http://www.w3.org/2000/svg"
33781
+ }, /*#__PURE__*/React.createElement("g", {
33782
+ fill: STAFF_PINK,
33783
+ transform: "translate(2.92 2.66) scale(.204)"
33784
+ }, /*#__PURE__*/React.createElement("path", {
33785
+ d: WRENCH
33786
+ }), /*#__PURE__*/React.createElement("path", {
33787
+ d: SCREWDRIVER
33788
+ })));
33789
+ }
33790
+ // A cross-origin frame's own history object is unreachable (touching it throws
33791
+ // a SecurityError), but the frame's navigations do land in the top-level joint
33792
+ // session history, so the *parent's* back/forward traverse them while leaving
33793
+ // the host page in place. Counting frame loads tracks how deep we are, which is
33794
+ // what keeps a click on Back from stepping past the frame's first page and
33795
+ // navigating the host app instead. Loads we caused are attributed to the
33796
+ // traversal that requested them rather than counted as new navigations.
33797
+ function useFrameHistory() {
33798
+ const [steps, setSteps] = React.useState({
33799
+ back: 0,
33800
+ forward: 0
33801
+ });
33802
+ // Traversal has to read the current counts outside of render, so the ref is
33803
+ // the source of truth and state exists to paint the buttons.
33804
+ const stepsRef = React.useRef(steps);
33805
+ const pendingTraversal = React.useRef(null);
33806
+ const frameIsLoaded = React.useRef(false);
33807
+ const moveTo = React.useCallback(next => {
33808
+ stepsRef.current = next;
33809
+ setSteps(next);
33810
+ }, []);
33811
+ const onFrameLoad = React.useCallback(() => {
33812
+ if (!frameIsLoaded.current) {
33813
+ // The frame's first page is the floor, not a step.
33814
+ frameIsLoaded.current = true;
33815
+ } else {
33816
+ const traversal = pendingTraversal.current;
33817
+ pendingTraversal.current = null;
33818
+ const {
33819
+ back,
33820
+ forward
33821
+ } = stepsRef.current;
33822
+ if (traversal === "back") {
33823
+ moveTo({
33824
+ back: back - 1,
33825
+ forward: forward + 1
33826
+ });
33827
+ } else if (traversal === "forward") {
33828
+ moveTo({
33829
+ back: back + 1,
33830
+ forward: forward - 1
33831
+ });
33832
+ } else {
33833
+ moveTo({
33834
+ back: back + 1,
33835
+ forward: 0
33836
+ });
33837
+ }
33838
+ }
33839
+ }, [moveTo]);
33840
+ // Guarded here rather than trusting the button's disabled state: traversing
33841
+ // with no frame step left would navigate the host app out from under us.
33842
+ const traverse = React.useCallback(direction => {
33843
+ if (stepsRef.current[direction] > 0) {
33844
+ pendingTraversal.current = direction;
33845
+ window.history[direction]();
33846
+ }
33847
+ }, []);
33848
+ return {
33849
+ onFrameLoad,
33850
+ steps,
33851
+ traverse
33852
+ };
33853
+ }
33854
+ function NavButton({
33855
+ direction,
33856
+ disabled,
33857
+ onClick
33858
+ }) {
33859
+ return /*#__PURE__*/React.createElement(tapestryReact.Button
33860
+ // Tapestry's `disabled` suppresses the click but emits no ARIA state.
33861
+ , {
33862
+ "aria-disabled": disabled,
33863
+ disabled: disabled,
33864
+ hover: {
33865
+ backgroundColor: "var(--t-fill-color-neutral-070)"
33866
+ },
33867
+ icon: {
33868
+ fill: "var(--t-icon-color-dim)",
33869
+ name: direction === "back" ? "general.leftChevron" : "general.rightChevron",
33870
+ size: "md"
33871
+ },
33872
+ onClick: onClick,
33873
+ opacity: disabled ? 0.35 : 1,
33874
+ theme: false,
33875
+ title: direction === "back" ? "Back" : "Forward"
33876
+ });
33877
+ }
33878
+ function DeveloperToolsComponent({
33879
+ closeDrawer
33880
+ }) {
33881
+ const {
33882
+ onFrameLoad,
33883
+ steps,
33884
+ traverse
33885
+ } = useFrameHistory();
33886
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(DrawerHeader, {
33887
+ closeDrawer: closeDrawer
33888
+ }, /*#__PURE__*/React.createElement(tapestryReact.StackView, {
33889
+ alignment: "center",
33890
+ axis: "horizontal",
33891
+ spacing: 0.25
33892
+ }, /*#__PURE__*/React.createElement(NavButton, {
33893
+ direction: "back",
33894
+ disabled: steps.back === 0,
33895
+ onClick: () => traverse("back")
33896
+ }), /*#__PURE__*/React.createElement(NavButton, {
33897
+ direction: "forward",
33898
+ disabled: steps.forward === 0,
33899
+ onClick: () => traverse("forward")
33900
+ }), /*#__PURE__*/React.createElement(DrawerHeading, {
33901
+ paddingLeft: 0.5
33902
+ }, "Developer Tools"))), /*#__PURE__*/React.createElement(tapestryReact.Box, {
33903
+ as: "iframe",
33904
+ border: "none",
33905
+ flex: "1",
33906
+ minHeight: 0,
33907
+ onLoad: onFrameLoad,
33908
+ src: iframeSrc(),
33909
+ title: "Developer Tools",
33910
+ width: "100%"
33911
+ }));
33912
+ }
33913
+ const DeveloperTools = {
33914
+ component: DeveloperToolsComponent,
33915
+ icons: {
33916
+ component: ToolsIcon
33917
+ },
33918
+ label: "Developer Tools",
33919
+ // No space: `name` becomes an element id and a pendo attribute.
33920
+ name: "DeveloperTools"
33921
+ };
33922
+
34097
33923
  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"};
34098
33924
 
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";
33925
+ 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";
34100
33926
  // Same stops as the AskAIBanner border gradient, so the two stay in sync.
34101
33927
  const GRADIENT_STOPS = [{
34102
33928
  color: "var(--t-fill-color-tag-teal-040)",
@@ -34145,7 +33971,7 @@ function SparkleIcon({
34145
33971
  stopColor: color
34146
33972
  }
34147
33973
  })))))), /*#__PURE__*/React.createElement("path", {
34148
- d: SPARKLE_PATH,
33974
+ d: SPARKLE_PATH$1,
34149
33975
  fill: isGradient ? `url(#${gradientId})` : "currentColor"
34150
33976
  }));
34151
33977
  }
@@ -34553,16 +34379,24 @@ const DocumentationSearch = ({
34553
34379
  // not the path, so updating it is a same-document navigation that doesn't
34554
34380
  // reload the iframe, but also lets the route survive a forced reload (e.g.
34555
34381
  // Turbo re-parenting the topbar).
34556
- function buildPicobotSrc(origin, path) {
34382
+ //
34383
+ // `panel` loads picobot as its own toolbar panel rather than inside Helpdesk.
34384
+ function buildPicobotSrc(origin, path, {
34385
+ panel = false
34386
+ } = {}) {
34557
34387
  const [rawPath, queryPart] = path.replace(/^\/+/, "").split("?", 2);
34558
34388
  const safePath = rawPath.split("/").filter(segment => segment && segment !== "." && segment !== "..").join("/");
34559
34389
  const fragment = queryPart !== undefined ? `${safePath}?${queryPart}` : safePath;
34560
- const base = new URL("topbar", origin).toString();
34390
+ const url = new URL("topbar", origin);
34391
+ if (panel) url.searchParams.set("panel", "1");
34392
+ const base = url.toString();
34561
34393
  return fragment ? `${base}#${fragment}` : base;
34562
34394
  }
34563
34395
 
34564
34396
  const LOAD_TIMEOUT_MS = 3000;
34565
- function createPicobotIframe(picobotOrigin) {
34397
+ function createPicobotIframe(picobotOrigin, {
34398
+ panel = false
34399
+ } = {}) {
34566
34400
  return /*#__PURE__*/React.forwardRef(function PicobotIframe({
34567
34401
  currentPath: iframePath,
34568
34402
  onChangePath: onIframeChangePath,
@@ -34618,7 +34452,9 @@ function createPicobotIframe(picobotOrigin) {
34618
34452
  (_b = (_a = iframeRef.current) === null || _a === void 0 ? void 0 : _a.contentWindow) === null || _b === void 0 ? void 0 : _b.postMessage(message, picobotOrigin);
34619
34453
  }
34620
34454
  }), []);
34621
- const iframeSrc = buildPicobotSrc(picobotOrigin, loadedPath);
34455
+ const iframeSrc = buildPicobotSrc(picobotOrigin, loadedPath, {
34456
+ panel
34457
+ });
34622
34458
  return /*#__PURE__*/React.createElement("iframe", {
34623
34459
  ref: iframeRef,
34624
34460
  src: iframeSrc,
@@ -34657,22 +34493,28 @@ function HelpdeskComponent({
34657
34493
  currentPath,
34658
34494
  env,
34659
34495
  helpdeskConfig,
34660
- onChangePath
34496
+ isPicobotLoading,
34497
+ onChangePath,
34498
+ openPicobot
34661
34499
  }) {
34662
34500
  const PicobotIframe = React.useMemo(() => createPicobotIframe(pcoUrl(env)("picobot")), [env]);
34501
+ const loadingFallback = /*#__PURE__*/React.createElement(StackView$1, {
34502
+ flex: 1,
34503
+ axis: "vertical"
34504
+ }, /*#__PURE__*/React.createElement(DrawerHeader, {
34505
+ closeDrawer: closeDrawer
34506
+ }, /*#__PURE__*/React.createElement(StackView$1, {
34507
+ axis: "horizontal",
34508
+ spacing: 1
34509
+ }, /*#__PURE__*/React.createElement(DrawerHeading, null, "Help and support"), /*#__PURE__*/React.createElement(LoadingIndicator$1, null))));
34510
+ // The add-on reads its args once, so mounting before the gate answers
34511
+ // would leave out openPicobot for good.
34512
+ if (isPicobotLoading) return loadingFallback;
34663
34513
  return /*#__PURE__*/React.createElement(addOns.UiExtensionFirstPartyComponent, {
34664
34514
  grow: 1,
34665
34515
  display: "flex",
34666
34516
  extensionId: env === "staging" ? "42" : "11",
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)))),
34517
+ loadingFallback: loadingFallback,
34676
34518
  additionalComponents: {
34677
34519
  AskAIBanner: {
34678
34520
  component: AskAIBanner,
@@ -34770,7 +34612,11 @@ function HelpdeskComponent({
34770
34612
  currentPageParams: location.search,
34771
34613
  currentPath,
34772
34614
  helpdeskConfig,
34773
- onChangePath
34615
+ onChangePath,
34616
+ // Its presence tells helpdesk that Picobot has its own panel.
34617
+ ...(openPicobot && {
34618
+ openPicobot
34619
+ })
34774
34620
  }
34775
34621
  });
34776
34622
  }
@@ -35317,6 +35163,142 @@ const Notifications = {
35317
35163
  name: "Notifications"
35318
35164
  };
35319
35165
 
35166
+ 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";
35167
+ // Helpdesk reads this context off the email path to open its email form the
35168
+ // same way a handoff inside its own drawer does.
35169
+ function emailPathWithContext(context = {}) {
35170
+ const params = new URLSearchParams();
35171
+ for (const key of ["conversationId", "source", "turnIndex"]) {
35172
+ const value = context[key];
35173
+ if (value !== null && value !== undefined) params.set(key, String(value));
35174
+ }
35175
+ return `email?${params}`;
35176
+ }
35177
+ function PicobotComponent({
35178
+ clearPending,
35179
+ closeDrawer,
35180
+ currentPath,
35181
+ env,
35182
+ onChangePath,
35183
+ openTool,
35184
+ pending
35185
+ }) {
35186
+ const PicobotIframe = React.useMemo(() => createPicobotIframe(pcoUrl(env)("picobot"), {
35187
+ panel: true
35188
+ }), [env]);
35189
+ const picobotRef = React.useRef(null);
35190
+ const [isPicobotReady, setIsPicobotReady] = React.useState(false);
35191
+ // Picobot has no listener until it sends READY, so openPicobot's draft or
35192
+ // conversation waits for it.
35193
+ React.useEffect(function handOverPendingOncePicobotIsReady() {
35194
+ var _a, _b;
35195
+ if (!pending || !isPicobotReady) return;
35196
+ const {
35197
+ conversationId,
35198
+ draft,
35199
+ ticketExternalId
35200
+ } = pending;
35201
+ if (conversationId) {
35202
+ (_a = picobotRef.current) === null || _a === void 0 ? void 0 : _a.postMessage({
35203
+ payload: {
35204
+ conversation_id: conversationId,
35205
+ // Picobot asks whether this resolved the ticket, and closes it.
35206
+ ...(ticketExternalId && {
35207
+ external_id: ticketExternalId,
35208
+ source: "ticket_submitted"
35209
+ })
35210
+ },
35211
+ type: "LOAD_CONVERSATION"
35212
+ });
35213
+ } else if (draft) {
35214
+ // Not both: Picobot cancels a conversation load when a prefill
35215
+ // starts a new conversation.
35216
+ (_b = picobotRef.current) === null || _b === void 0 ? void 0 : _b.postMessage({
35217
+ payload: {
35218
+ source: "email_form",
35219
+ text: draft
35220
+ },
35221
+ type: "PREFILL_COMPOSER"
35222
+ });
35223
+ }
35224
+ clearPending();
35225
+ }, [pending, isPicobotReady, clearPending]);
35226
+ const [iframeErrored, setIframeErrored] = React.useState(false);
35227
+ const [mountKey, setMountKey] = React.useState(0);
35228
+ const handleRetry = React.useCallback(() => {
35229
+ setIframeErrored(false);
35230
+ setIsPicobotReady(false);
35231
+ setMountKey(key => key + 1);
35232
+ }, []);
35233
+ const handleMessage = React.useCallback(data => {
35234
+ var _a;
35235
+ const message = data;
35236
+ switch (message === null || message === void 0 ? void 0 : message.type) {
35237
+ case "CLOSE_DRAWER":
35238
+ return closeDrawer();
35239
+ case "READY":
35240
+ return setIsPicobotReady(true);
35241
+ case "PICOBOT_SUPPORT_HANDOFF":
35242
+ return openTool("helpdesk", ((_a = message.payload) === null || _a === void 0 ? void 0 : _a.method) === "zoom" ? "phone" : emailPathWithContext(message.payload));
35243
+ case "IFRAME_LOAD_ERROR":
35244
+ console.warn("topbar: Picobot iframe failed to load");
35245
+ return setIframeErrored(true);
35246
+ }
35247
+ }, [closeDrawer, openTool]);
35248
+ if (iframeErrored) {
35249
+ return /*#__PURE__*/React.createElement(LoadError, {
35250
+ onContactSupport: () => openTool("helpdesk", "email"),
35251
+ onRetry: handleRetry
35252
+ });
35253
+ }
35254
+ return /*#__PURE__*/React.createElement(PicobotIframe, {
35255
+ key: mountKey,
35256
+ ref: picobotRef,
35257
+ currentPath: currentPath,
35258
+ onChangePath: onChangePath,
35259
+ onMessage: handleMessage
35260
+ });
35261
+ }
35262
+ function LoadError({
35263
+ onContactSupport,
35264
+ onRetry
35265
+ }) {
35266
+ return /*#__PURE__*/React.createElement(tapestryReact.StackView, {
35267
+ axis: "vertical",
35268
+ flex: 1,
35269
+ alignment: "center",
35270
+ distribution: "center",
35271
+ padding: 3,
35272
+ spacing: 1
35273
+ }, /*#__PURE__*/React.createElement(tapestryReact.Heading, {
35274
+ level: 2
35275
+ }, "Picobot is temporarily unavailable"), /*#__PURE__*/React.createElement(tapestryReact.Text, {
35276
+ size: 5,
35277
+ color: "var(--t-text-color-secondary)"
35278
+ }, "Try again or contact support."), /*#__PURE__*/React.createElement(tapestryReact.Box, {
35279
+ paddingTop: 2
35280
+ }, /*#__PURE__*/React.createElement(tapestryReact.Button, {
35281
+ onClick: onRetry,
35282
+ iconLeft: "general.refresh",
35283
+ theme: "primary",
35284
+ variant: "fill",
35285
+ title: "Retry"
35286
+ })), /*#__PURE__*/React.createElement(tapestryReact.Button, {
35287
+ onClick: onContactSupport,
35288
+ variant: "naked",
35289
+ theme: "primary",
35290
+ title: "Contact support"
35291
+ }));
35292
+ }
35293
+ const Picobot = {
35294
+ component: PicobotComponent,
35295
+ icons: {
35296
+ staticDotPath: SPARKLE_PATH,
35297
+ staticPath: SPARKLE_PATH
35298
+ },
35299
+ name: "Picobot"
35300
+ };
35301
+
35320
35302
  const {
35321
35303
  LoadingIndicator,
35322
35304
  StackView
@@ -35712,6 +35694,41 @@ const useChatConfig = ({
35712
35694
  };
35713
35695
  };
35714
35696
 
35697
+ // Picobot's API rejects requests without a version header, and apiRequest
35698
+ // sets its own headers, so the version rides in on the fetch.
35699
+ const picobotFetch = (url, opts) => defaultFetch(url, {
35700
+ ...opts,
35701
+ headers: {
35702
+ ...(opts === null || opts === void 0 ? void 0 : opts.headers),
35703
+ "X-PCO-API-Version": "2026-08-11"
35704
+ }
35705
+ });
35706
+ // Routes wait on the gate, so a slow Picobot can't hold them up for long.
35707
+ const GATE_TIMEOUT_MS = 3000;
35708
+ const usePicobotConfig = ({
35709
+ isEnabled
35710
+ }) => {
35711
+ // Undefined until Picobot answers.
35712
+ const [panelEnabled, setPanelEnabled] = React.useState();
35713
+ React.useEffect(() => {
35714
+ if (!isEnabled) return undefined;
35715
+ // Whatever settles first wins, so a late answer can't reopen the gate.
35716
+ const settle = enabled => setPanelEnabled(current => current !== null && current !== void 0 ? current : enabled);
35717
+ const timeout = setTimeout(() => settle(false), GATE_TIMEOUT_MS);
35718
+ apiRequest(picobotFetch, `${window.location.origin}/~api/picobot/v2/me`).then(({
35719
+ json
35720
+ }) => {
35721
+ var _a, _b;
35722
+ 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);
35723
+ }).catch(() => settle(false)).finally(() => clearTimeout(timeout));
35724
+ return () => clearTimeout(timeout);
35725
+ }, [isEnabled]);
35726
+ return {
35727
+ displayPicobotIcon: panelEnabled === true,
35728
+ isLoading: isEnabled && panelEnabled === undefined
35729
+ };
35730
+ };
35731
+
35715
35732
  const Profile = {
35716
35733
  component: UserProfile,
35717
35734
  icons: {
@@ -35722,10 +35739,16 @@ const Profile = {
35722
35739
  };
35723
35740
 
35724
35741
  const DURATION = 100;
35742
+ // Exempt paths match regardless of query, so `email?conversationId=…` is
35743
+ // exempt along with `email`.
35725
35744
  function isToolPathExemptFromStorage(toolName, path) {
35726
35745
  var _a, _b;
35727
35746
  const tool = toolsRouteMap[toolName];
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;
35747
+ const [pathWithoutQuery] = path.split("?");
35748
+ 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;
35749
+ }
35750
+ function isConversationPath(path) {
35751
+ return Boolean(path === null || path === void 0 ? void 0 : path.startsWith("conversations/"));
35729
35752
  }
35730
35753
  function scrollToolDrawerToTop(toolName) {
35731
35754
  var _a;
@@ -35738,6 +35761,7 @@ const TOOLS_MAP = {
35738
35761
  Chat,
35739
35762
  Helpdesk,
35740
35763
  Notifications,
35764
+ Picobot,
35741
35765
  Profile,
35742
35766
  Tasks
35743
35767
  };
@@ -35765,7 +35789,8 @@ function Toolbar({
35765
35789
  });
35766
35790
  const {
35767
35791
  currentContent,
35768
- open
35792
+ open,
35793
+ pending
35769
35794
  } = state;
35770
35795
  const mounted = !!currentContent;
35771
35796
  const storeData = useStore();
@@ -35778,6 +35803,15 @@ function Toolbar({
35778
35803
  ...(env === "development" ? _storeDataOverride !== null && _storeDataOverride !== void 0 ? _storeDataOverride : {} : {})
35779
35804
  };
35780
35805
  const updateStore = useUpdateStore();
35806
+ const {
35807
+ displayPicobotIcon,
35808
+ isLoading: isPicobotGateLoading
35809
+ } = usePicobotConfig({
35810
+ isEnabled: Boolean(userId)
35811
+ });
35812
+ // Picobot is a kill-switched tool: until its gate says the panel is
35813
+ // enabled, its routes are held, and if it never does, they're ignored.
35814
+ const routableTool = React.useCallback(name => name === Picobot.name.toLowerCase() && !displayPicobotIcon ? undefined : toolsRouteMap[name], [displayPicobotIcon]);
35781
35815
  React.useEffect(() => {
35782
35816
  if (open) {
35783
35817
  document.body.style.overflow = "hidden";
@@ -35865,7 +35899,9 @@ function Toolbar({
35865
35899
  });
35866
35900
  }, [clearRoute]);
35867
35901
  React.useEffect(() => {
35868
- if (!toolbarFragmentRoute) return;
35902
+ if (!toolbarFragmentRoute || !routableTool(toolbarFragmentRoute.tool)) {
35903
+ return;
35904
+ }
35869
35905
  const isExempt = isToolPathExemptFromStorage(toolbarFragmentRoute.tool, toolbarFragmentRoute.path);
35870
35906
  if (isExempt) return;
35871
35907
  const nextState = {
@@ -35875,12 +35911,32 @@ function Toolbar({
35875
35911
  if (!shallowCompare(nextState, toolStateCookie)) {
35876
35912
  setToolStateCookie(nextState);
35877
35913
  }
35878
- }, [toolbarFragmentRoute, toolStateCookie]);
35914
+ }, [toolbarFragmentRoute, toolStateCookie, routableTool]);
35879
35915
  React.useEffect(() => {
35880
- if ((toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && (currentContent !== toolsRouteMap[toolbarFragmentRoute.tool] || !open)) {
35881
- setToolAndOpenDrawer(toolsRouteMap[toolbarFragmentRoute.tool]);
35916
+ // Helpdesk conversations may belong to Picobot, so they wait for its gate
35917
+ // rather than flashing open in Helpdesk first.
35918
+ const mayRedirectToPicobot = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) === Helpdesk.name.toLowerCase() && isConversationPath(toolbarFragmentRoute.path) && (isPicobotGateLoading || displayPicobotIcon);
35919
+ const tool = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && !mayRedirectToPicobot && routableTool(toolbarFragmentRoute.tool);
35920
+ if (tool && (currentContent !== tool || !open)) {
35921
+ setToolAndOpenDrawer(tool);
35882
35922
  }
35883
- }, [setToolAndOpenDrawer, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool, currentContent, open]);
35923
+ }, [setToolAndOpenDrawer, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path, currentContent, open, routableTool, isPicobotGateLoading, displayPicobotIcon]);
35924
+ const openTool = React.useCallback((name, path, pendingValue) => {
35925
+ const tool = routableTool(name.toLowerCase());
35926
+ if (!tool) return;
35927
+ dispatch({
35928
+ payload: pendingValue === undefined ? null : {
35929
+ tool: tool.name,
35930
+ value: pendingValue
35931
+ },
35932
+ type: ActionType.setPending
35933
+ });
35934
+ onChangeToolbarFragmentRoute(tool.name.toLowerCase(), path);
35935
+ scrollToolDrawerToTop(tool.name);
35936
+ }, [onChangeToolbarFragmentRoute, routableTool]);
35937
+ const clearPending = React.useCallback(() => dispatch({
35938
+ type: ActionType.clearPending
35939
+ }), []);
35884
35940
  const toggleDrawer = content => {
35885
35941
  if (!open) {
35886
35942
  return openDrawer(content);
@@ -35897,12 +35953,52 @@ function Toolbar({
35897
35953
  joltPromise: storeData === null || storeData === void 0 ? void 0 : storeData.joltPromise,
35898
35954
  userId
35899
35955
  });
35900
- const TOOLS = [Notifications, Tasks, displayChatIcon && Chat].filter(Boolean);
35956
+ // Picobot conversations moved from Helpdesk to the Picobot panel, so
35957
+ // send old links and saved paths there once the panel is enabled.
35958
+ React.useEffect(() => {
35959
+ if (!displayPicobotIcon) return;
35960
+ if ((toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) === Helpdesk.name.toLowerCase() && isConversationPath(toolbarFragmentRoute.path)) {
35961
+ onChangeToolbarFragmentRoute(Picobot.name.toLowerCase(), toolbarFragmentRoute.path);
35962
+ }
35963
+ }, [displayPicobotIcon, toolbarFragmentRoute, onChangeToolbarFragmentRoute]);
35964
+ React.useEffect(() => {
35965
+ if (!displayPicobotIcon) return;
35966
+ const {
35967
+ helpdesk,
35968
+ picobot
35969
+ } = toolStateCookie;
35970
+ if (!isConversationPath(helpdesk)) return;
35971
+ setToolStateCookie({
35972
+ ...toolStateCookie,
35973
+ helpdesk: "",
35974
+ picobot: picobot || helpdesk
35975
+ });
35976
+ }, [displayPicobotIcon, toolStateCookie]);
35977
+ const openPicobot = React.useCallback(options => {
35978
+ var _a;
35979
+ // Moving an open Picobot off its path reloads or navigates the iframe,
35980
+ // which would race the options it's being handed.
35981
+ const path = currentContent === Picobot ? (_a = toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path) !== null && _a !== void 0 ? _a : "" : "";
35982
+ openTool(Picobot.name, path, options);
35983
+ }, [openTool, currentContent, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path]);
35984
+ React.useEffect(() => {
35985
+ if (!displayPicobotIcon) return undefined;
35986
+ window.topbar = window.topbar || {};
35987
+ window.topbar.openPicobot = openPicobot;
35988
+ return () => {
35989
+ delete window.topbar.openPicobot;
35990
+ };
35991
+ }, [displayPicobotIcon, openPicobot]);
35992
+ const TOOLS = [Notifications, Tasks, displayChatIcon && Chat, displayPicobotIcon && Picobot, isPcoTestHost() && DeveloperTools].filter(Boolean);
35901
35993
  const handleToolOnClick = tool => {
35902
35994
  toggleDrawer(tool);
35903
35995
  scrollToolDrawerToTop(tool.name);
35904
35996
  };
35905
35997
  const toolSpecificProps = {
35998
+ Helpdesk: {
35999
+ isPicobotLoading: isPicobotGateLoading,
36000
+ openPicobot: displayPicobotIcon ? openPicobot : undefined
36001
+ },
35906
36002
  Profile: {
35907
36003
  appName,
35908
36004
  closeDrawer,
@@ -35925,6 +36021,7 @@ function Toolbar({
35925
36021
  const routeIsForCurrentTool = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && toolsRouteMap[toolbarFragmentRoute.tool] === currentContent;
35926
36022
  const currentToolProps = {
35927
36023
  appName,
36024
+ clearPending,
35928
36025
  closeDrawer,
35929
36026
  currentPath: routeIsForCurrentTool ? toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path : null,
35930
36027
  env,
@@ -35933,6 +36030,8 @@ function Toolbar({
35933
36030
  joltPromise,
35934
36031
  onChangePath,
35935
36032
  openDrawer: () => currentContent && openDrawer(currentContent),
36033
+ openTool,
36034
+ 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,
35936
36035
  userId,
35937
36036
  ...(currentContent ? toolSpecificProps[currentContent.name] : {})
35938
36037
  };
@@ -35960,21 +36059,20 @@ function Toolbar({
35960
36059
  name: "toolbar.planningCenter",
35961
36060
  size: "24px"
35962
36061
  })), [HelpDeskTool, ...TOOLS].map(tool => {
36062
+ var _a;
35963
36063
  const current = open && currentContent === tool;
35964
36064
  const indicator = indicatorState[tool.name];
35965
36065
  return /*#__PURE__*/React.createElement(ToolbarButton, {
35966
36066
  current: current,
35967
36067
  onClick: () => handleToolOnClick(tool),
35968
36068
  id: getDataPendoAttribute(tool.name),
35969
- "aria-label": `Open ${tool.name} drawer`,
36069
+ "aria-label": `Open ${(_a = tool.label) !== null && _a !== void 0 ? _a : tool.name} drawer`,
35970
36070
  "data-pendo": getDataPendoAttribute(tool.name),
35971
36071
  key: tool.name
35972
- }, !indicator && (/*#__PURE__*/React.createElement(ToolbarButtonIcon, {
35973
- path: tool.icons.staticPath
35974
- })), indicator && (/*#__PURE__*/React.createElement(ToolbarButtonIcon, {
35975
- path: tool.icons.staticDotPath,
35976
- dot: true
35977
- })));
36072
+ }, /*#__PURE__*/React.createElement(ToolbarToolIcon, {
36073
+ icons: tool.icons,
36074
+ dot: !!indicator
36075
+ }));
35978
36076
  })), /*#__PURE__*/React.createElement(DrawerWrap, {
35979
36077
  isVisible: open
35980
36078
  }, mounted && (/*#__PURE__*/React.createElement(Drawer, {
@@ -35998,6 +36096,19 @@ const ToolbarButton = ({
35998
36096
  ...restProps
35999
36097
  }, children);
36000
36098
  };
36099
+ function ToolbarToolIcon({
36100
+ dot,
36101
+ icons
36102
+ }) {
36103
+ if ("component" in icons) {
36104
+ return /*#__PURE__*/React.createElement(icons.component, null);
36105
+ } else {
36106
+ return /*#__PURE__*/React.createElement(ToolbarButtonIcon, {
36107
+ path: dot ? icons.staticDotPath : icons.staticPath,
36108
+ dot: dot
36109
+ });
36110
+ }
36111
+ }
36001
36112
  function ToolbarButtonIcon({
36002
36113
  dot,
36003
36114
  path