@the-portland-company/shell 0.81.2 → 0.84.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode } from 'react';
3
- import { S as StandardNavIconKey, f as NavIcon, N as NavItem, g as NavBreadcrumb, h as StandardNavMode, i as NavLinkComponent, j as NavChromeFetcher, k as NavMode, l as NavUserMenu, m as NavOrgOption, b as NotificationsAdapter, o as NavFooterLink, p as NavFooter, q as NavDisplayMode } from './index-_Y7jOKPN.cjs';
3
+ import { S as StandardNavIconKey, f as NavIcon, N as NavItem, g as NavBreadcrumb, h as StandardNavMode, i as NavLinkComponent, j as NavChromeFetcher, k as NavMode, l as NavUserMenu, m as NavOrgOption, b as NotificationsAdapter, o as NavFooterLink, p as NavFooter, q as NavDisplayMode } from './index--q40spnY.cjs';
4
4
  import './theme-preference.cjs';
5
5
 
6
6
  /**
@@ -181,6 +181,21 @@ type PolitogyAppSession = {
181
181
  */
182
182
  userName?: string | null;
183
183
  };
184
+ /**
185
+ * Optional DATA plumbing for apps whose session/auth differs — never styling.
186
+ *
187
+ * Everything here is about WHERE data comes from (which fetcher, which org
188
+ * state, which routes an app owns), not what the chrome looks like. Adding a
189
+ * visual option to this type would re-open the drift this component exists to
190
+ * close.
191
+ */
192
+ /**
193
+ * Universal-search placeholder, per Mode (Reference/UI-Platform v56 "Top bar —
194
+ * required elements", decided by Ben 2026-10-12). The Glossary term is Tribe,
195
+ * so the design frame's "segments" is not used. An app may still pass its own
196
+ * copy through `data.searchPlaceholder`.
197
+ */
198
+ declare const MODE_SEARCH_PLACEHOLDER: Record<string, string>;
184
199
  type PolitogyFrameData = {
185
200
  /** Chrome-API fetcher override for apps that authenticate client-side. */
186
201
  fetchJson?: NavChromeFetcher;
@@ -386,6 +401,21 @@ type ChromeState = {
386
401
  navDisplay: NavDisplayMode;
387
402
  setNavDisplay: (m: NavDisplayMode) => void;
388
403
  };
404
+ /**
405
+ * Where an organization switch goes (Specs/Left-Navigation §6, §11.2, decided
406
+ * 2026-10-15 by Ben): the new organization's Relationship Dashboard, from every
407
+ * app, Mode and page, behind the Loading Screen. The Relationship Dashboard is
408
+ * the one page every organization has, so nothing the user was looking at is
409
+ * carried into an organization that may not have it. (Measured before this: a
410
+ * switch from Campaign Mode to an organization without it kept the Campaign
411
+ * rail, then showed an access error that needed a sign-out.)
412
+ *
413
+ * An object so tests can replace `navigate` (jsdom cannot navigate).
414
+ */
415
+ declare const orgSwitchLanding: {
416
+ href(orgId: string): string | null;
417
+ navigate(href: string): void;
418
+ };
389
419
  declare function useChromeState(): ChromeState | null;
390
420
  type ControlledOrgState = {
391
421
  orgId: string | null;
@@ -565,4 +595,4 @@ type UseChromeModeArg = ModeId | {
565
595
  */
566
596
  declare function useChromeMode(arg?: UseChromeModeArg): NavMode;
567
597
 
568
- export { type BootPhase, type BootProgress, type BootStageId, type ChromeDevNotesConfig, type ChromeIdentity, ChromeTopBarActions, type ControlledOrgState, LoadingScreen, POLITOGY_APPS, PolitogyAppChrome, PolitogyAppFrame, type PolitogyAppKey, type PolitogyAppMode, type PolitogyAppSession, type PolitogyBuildMeta, type PolitogyFrameData, type PolitogyFrameGrant, type PolitogyGrantedFrame, type ResolvedIdentity, type TokenOrgState, deriveBreadcrumbs, footerLinks, looksLikeEmail, modeLandingHref, resolveIdentity, standardNavIcons, useChromeFooter, useChromeIdentity, useChromeMode, useChromeState, useChromeUserMenu, useIsSuperAdmin };
598
+ export { type BootPhase, type BootProgress, type BootStageId, type ChromeDevNotesConfig, type ChromeIdentity, ChromeTopBarActions, type ControlledOrgState, LoadingScreen, MODE_SEARCH_PLACEHOLDER, POLITOGY_APPS, PolitogyAppChrome, PolitogyAppFrame, type PolitogyAppKey, type PolitogyAppMode, type PolitogyAppSession, type PolitogyBuildMeta, type PolitogyFrameData, type PolitogyFrameGrant, type PolitogyGrantedFrame, type ResolvedIdentity, type TokenOrgState, deriveBreadcrumbs, footerLinks, looksLikeEmail, modeLandingHref, orgSwitchLanding, resolveIdentity, standardNavIcons, useChromeFooter, useChromeIdentity, useChromeMode, useChromeState, useChromeUserMenu, useIsSuperAdmin };
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode } from 'react';
3
- import { S as StandardNavIconKey, f as NavIcon, N as NavItem, g as NavBreadcrumb, h as StandardNavMode, i as NavLinkComponent, j as NavChromeFetcher, k as NavMode, l as NavUserMenu, m as NavOrgOption, b as NotificationsAdapter, o as NavFooterLink, p as NavFooter, q as NavDisplayMode } from './index-bYu6DxNt.js';
3
+ import { S as StandardNavIconKey, f as NavIcon, N as NavItem, g as NavBreadcrumb, h as StandardNavMode, i as NavLinkComponent, j as NavChromeFetcher, k as NavMode, l as NavUserMenu, m as NavOrgOption, b as NotificationsAdapter, o as NavFooterLink, p as NavFooter, q as NavDisplayMode } from './index-C4ob05Pc.js';
4
4
  import './theme-preference.js';
5
5
 
6
6
  /**
@@ -181,6 +181,21 @@ type PolitogyAppSession = {
181
181
  */
182
182
  userName?: string | null;
183
183
  };
184
+ /**
185
+ * Optional DATA plumbing for apps whose session/auth differs — never styling.
186
+ *
187
+ * Everything here is about WHERE data comes from (which fetcher, which org
188
+ * state, which routes an app owns), not what the chrome looks like. Adding a
189
+ * visual option to this type would re-open the drift this component exists to
190
+ * close.
191
+ */
192
+ /**
193
+ * Universal-search placeholder, per Mode (Reference/UI-Platform v56 "Top bar —
194
+ * required elements", decided by Ben 2026-10-12). The Glossary term is Tribe,
195
+ * so the design frame's "segments" is not used. An app may still pass its own
196
+ * copy through `data.searchPlaceholder`.
197
+ */
198
+ declare const MODE_SEARCH_PLACEHOLDER: Record<string, string>;
184
199
  type PolitogyFrameData = {
185
200
  /** Chrome-API fetcher override for apps that authenticate client-side. */
186
201
  fetchJson?: NavChromeFetcher;
@@ -386,6 +401,21 @@ type ChromeState = {
386
401
  navDisplay: NavDisplayMode;
387
402
  setNavDisplay: (m: NavDisplayMode) => void;
388
403
  };
404
+ /**
405
+ * Where an organization switch goes (Specs/Left-Navigation §6, §11.2, decided
406
+ * 2026-10-15 by Ben): the new organization's Relationship Dashboard, from every
407
+ * app, Mode and page, behind the Loading Screen. The Relationship Dashboard is
408
+ * the one page every organization has, so nothing the user was looking at is
409
+ * carried into an organization that may not have it. (Measured before this: a
410
+ * switch from Campaign Mode to an organization without it kept the Campaign
411
+ * rail, then showed an access error that needed a sign-out.)
412
+ *
413
+ * An object so tests can replace `navigate` (jsdom cannot navigate).
414
+ */
415
+ declare const orgSwitchLanding: {
416
+ href(orgId: string): string | null;
417
+ navigate(href: string): void;
418
+ };
389
419
  declare function useChromeState(): ChromeState | null;
390
420
  type ControlledOrgState = {
391
421
  orgId: string | null;
@@ -565,4 +595,4 @@ type UseChromeModeArg = ModeId | {
565
595
  */
566
596
  declare function useChromeMode(arg?: UseChromeModeArg): NavMode;
567
597
 
568
- export { type BootPhase, type BootProgress, type BootStageId, type ChromeDevNotesConfig, type ChromeIdentity, ChromeTopBarActions, type ControlledOrgState, LoadingScreen, POLITOGY_APPS, PolitogyAppChrome, PolitogyAppFrame, type PolitogyAppKey, type PolitogyAppMode, type PolitogyAppSession, type PolitogyBuildMeta, type PolitogyFrameData, type PolitogyFrameGrant, type PolitogyGrantedFrame, type ResolvedIdentity, type TokenOrgState, deriveBreadcrumbs, footerLinks, looksLikeEmail, modeLandingHref, resolveIdentity, standardNavIcons, useChromeFooter, useChromeIdentity, useChromeMode, useChromeState, useChromeUserMenu, useIsSuperAdmin };
598
+ export { type BootPhase, type BootProgress, type BootStageId, type ChromeDevNotesConfig, type ChromeIdentity, ChromeTopBarActions, type ControlledOrgState, LoadingScreen, MODE_SEARCH_PLACEHOLDER, POLITOGY_APPS, PolitogyAppChrome, PolitogyAppFrame, type PolitogyAppKey, type PolitogyAppMode, type PolitogyAppSession, type PolitogyBuildMeta, type PolitogyFrameData, type PolitogyFrameGrant, type PolitogyGrantedFrame, type ResolvedIdentity, type TokenOrgState, deriveBreadcrumbs, footerLinks, looksLikeEmail, modeLandingHref, orgSwitchLanding, resolveIdentity, standardNavIcons, useChromeFooter, useChromeIdentity, useChromeMode, useChromeState, useChromeUserMenu, useIsSuperAdmin };
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
  import { broadcastLogout, watchLogout } from './chunk-LL6LSJAH.js';
3
- import { clearBetaBannerDismissal, standardPolitogyNav, standardNavIcons, isNavItemActive, NavAppShell } from './chunk-MYY6N6D4.js';
4
- export { standardNavIcons } from './chunk-MYY6N6D4.js';
5
- import { warnDeprecated, NavChromeProvider, TopBarWidgetSlot, FuelAction, SpecOverlayAction, NotificationsAction, SupportAction, useChromeMe, useNavChromeDedupedFetch, useChromeOrgs, useNavChromeFetcher } from './chunk-L73FZKGE.js';
3
+ import { clearBetaBannerDismissal, standardPolitogyNav, standardNavIcons, isNavItemActive, NavAppShell } from './chunk-QS3Z4N73.js';
4
+ export { standardNavIcons } from './chunk-QS3Z4N73.js';
5
+ import { warnDeprecated, NavChromeProvider, TopBarWidgetSlot, FuelAction, SpecOverlayAction, NotificationsAction, SupportAction, useChromeMe, useNavChromeDedupedFetch, useChromeOrgs, useNavChromeFetcher } from './chunk-PRZP2SB6.js';
6
6
  import './chunk-BHRVZPZ7.js';
7
7
  import { NAV_DISPLAY_DEFAULT, resolveNavDisplay, writeNavDisplayLocal, writeNavDisplayCookie, isNavDisplayPreference, NAV_DISPLAY_STORAGE_KEY, readNavDisplayCookie } from './chunk-7YVJ7HQG.js';
8
8
  import { authBaseUrl, orgIdTail, settingsBaseUrl, ORG_ID_QUERY_PARAM, readOrgSelectorFromUrl, matchOrgSelector, syncOrgIdInUrl, isOrgId, switchActiveOrg, ACTIVE_ORG_QUERY_PARAM, writeActiveOrgCookie, resolveActiveOrg, readActiveOrgCookie } from './chunk-5LO5W7W5.js';
@@ -593,8 +593,10 @@ var POLITOGY_APPS = {
593
593
  canvass: { title: "Field Canvassing", origin: "https://canvass.politogyvrm.com", storagePrefix: "canvass", mode: "campaign", grants: ["title"] },
594
594
  signs: { title: "Sign Mapping", origin: "https://signs.politogyvrm.com", storagePrefix: "signs", mode: "campaign", grants: ["title"] }
595
595
  };
596
- var DEFAULT_SEARCH_PLACEHOLDER = {
597
- contacts: "Search contacts, segments, forms"
596
+ var MODE_SEARCH_PLACEHOLDER = {
597
+ relationship: "Search contacts, Tribes, forms",
598
+ campaign: "Search voters, walk lists, polls",
599
+ petition: "Search petitions, sheets, circulators"
598
600
  };
599
601
  function PolitogyAppFrame({
600
602
  app,
@@ -801,7 +803,7 @@ function FrameBody({
801
803
  {
802
804
  visible: bootVisible,
803
805
  bootProgress,
804
- syntheticReady: appReady ?? chromeResolved,
806
+ syntheticReady: (appReady ?? chromeResolved) && !chrome?.orgPending,
805
807
  onExitComplete: () => setBootVisible(false)
806
808
  },
807
809
  bootGeneration
@@ -872,7 +874,9 @@ function FrameBody({
872
874
  // search is the only thing on the left end of the bar. No app wires
873
875
  // search yet, so this stays decorative (`NavTopBarProps.search` — "an
874
876
  // app that has not wired search yet still gets the correct chrome").
875
- search: { placeholder: data?.searchPlaceholder ?? DEFAULT_SEARCH_PLACEHOLDER[app] ?? "Search" },
877
+ search: {
878
+ placeholder: data?.searchPlaceholder ?? MODE_SEARCH_PLACEHOLDER[appMode ?? mode.id ?? "relationship"] ?? "Search"
879
+ },
876
880
  // No Build notes control (Reference/UI-Platform, decided 2026-10-03).
877
881
  // The action cluster is IDENTICAL everywhere — core included. Page
878
882
  // actions sit BEFORE it, never inside it, so the Alerts bell keeps its
@@ -889,6 +893,18 @@ function FrameBody({
889
893
  }
890
894
  var ORG_STORAGE_KEY = "vrm.activeOrg";
891
895
  var MODE_STORAGE_KEY = "vrm.activeMode";
896
+ var orgSwitchLanding = {
897
+ href(orgId) {
898
+ return modeLandingHref("relationship", {
899
+ orgId,
900
+ hostname: typeof window === "undefined" ? null : window.location.hostname
901
+ });
902
+ },
903
+ navigate(href) {
904
+ window.location.assign(href);
905
+ }
906
+ };
907
+ var ORG_SWITCH_REEXCHANGE_BUDGET_MS = 4e3;
892
908
  var ChromeStateContext = createContext(null);
893
909
  function useChromeState() {
894
910
  return useContext(ChromeStateContext);
@@ -904,6 +920,7 @@ function TokenChromeStateProvider({
904
920
  const [pendingOrg, setPendingOrg] = useState(null);
905
921
  const tokenOrgRef = useRef(tokenOrg);
906
922
  tokenOrgRef.current = tokenOrg;
923
+ const leavingRef = useRef(false);
907
924
  useEffect(() => {
908
925
  setNavDisplayState(resolveNavDisplay());
909
926
  }, []);
@@ -944,6 +961,7 @@ function TokenChromeStateProvider({
944
961
  }, [overrideOrg, tokenOrg.orgId]);
945
962
  useEffect(() => {
946
963
  if (!pendingOrg || tokenOrg.orgId !== pendingOrg) return;
964
+ if (leavingRef.current) return;
947
965
  setPendingOrg(null);
948
966
  try {
949
967
  const next = syncOrgIdInUrl(window.location.href, pendingOrg);
@@ -966,8 +984,16 @@ function TokenChromeStateProvider({
966
984
  window.history.replaceState(window.history.state, "", url.toString());
967
985
  } catch {
968
986
  }
969
- await tokenOrgRef.current.onReexchange(claims.orgId, { perTab: false });
987
+ leavingRef.current = true;
988
+ await Promise.race([
989
+ Promise.resolve(tokenOrgRef.current.onReexchange(claims.orgId, { perTab: false })).catch(() => {
990
+ }),
991
+ new Promise((resolve) => setTimeout(resolve, ORG_SWITCH_REEXCHANGE_BUDGET_MS))
992
+ ]);
993
+ const href = orgSwitchLanding.href(claims.orgId);
994
+ if (href) orgSwitchLanding.navigate(href);
970
995
  } catch (err) {
996
+ leavingRef.current = false;
971
997
  setPendingOrg(null);
972
998
  tokenOrgRef.current.onSwitchError?.(err);
973
999
  }
@@ -1032,10 +1058,13 @@ function ChromeStateProvider({
1032
1058
  const navReconciled = useRef(false);
1033
1059
  const orgReconciled = useRef(false);
1034
1060
  const hadBootSelector = useRef(false);
1061
+ const bootUrlTail = useRef(null);
1035
1062
  useEffect(() => {
1036
1063
  try {
1064
+ const urlSelector = readOrgSelectorFromUrl(window.location.search);
1065
+ if (urlSelector && !isOrgId(urlSelector)) bootUrlTail.current = urlSelector;
1037
1066
  const selector = resolveActiveOrg();
1038
- if (selector) hadBootSelector.current = true;
1067
+ if (selector || urlSelector) hadBootSelector.current = true;
1039
1068
  const storedOrg = selector ?? localStorage.getItem(ORG_STORAGE_KEY);
1040
1069
  if (storedOrg) setOrgIdState(storedOrg);
1041
1070
  } catch {
@@ -1072,7 +1101,9 @@ function ChromeStateProvider({
1072
1101
  }, []);
1073
1102
  useEffect(() => {
1074
1103
  if (!orgs || orgs.length === 0) return;
1075
- setOrgIdState((prev) => prev && orgs.some((o) => o.id === prev) ? prev : orgs[0].id);
1104
+ const fromUrl = matchOrgSelector(bootUrlTail.current, orgs)?.id ?? null;
1105
+ bootUrlTail.current = null;
1106
+ setOrgIdState((prev) => fromUrl ?? (prev && orgs.some((o) => o.id === prev) ? prev : orgs[0].id));
1076
1107
  }, [orgs]);
1077
1108
  const activeOrgRef = useRef(null);
1078
1109
  const memberOrgsRef = useRef([]);
@@ -1130,6 +1161,10 @@ function ChromeStateProvider({
1130
1161
  } catch {
1131
1162
  }
1132
1163
  writeActiveOrgCookie(id);
1164
+ if (isOrgId(id)) {
1165
+ const href = orgSwitchLanding.href(id);
1166
+ if (href) orgSwitchLanding.navigate(href);
1167
+ }
1133
1168
  }, []);
1134
1169
  const setNavDisplay = useCallback(
1135
1170
  (m) => {
@@ -1196,8 +1231,10 @@ function ChromeStateProvider({
1196
1231
  }
1197
1232
  try {
1198
1233
  const url = new URL(window.location.href);
1199
- if (url.searchParams.get(ACTIVE_ORG_QUERY_PARAM) !== activeOrgId) {
1200
- url.searchParams.set(ACTIVE_ORG_QUERY_PARAM, activeOrgId);
1234
+ const tail = orgIdTail(activeOrgId);
1235
+ if (url.searchParams.has(ACTIVE_ORG_QUERY_PARAM) || url.searchParams.get(ORG_ID_QUERY_PARAM) !== tail) {
1236
+ url.searchParams.delete(ACTIVE_ORG_QUERY_PARAM);
1237
+ url.searchParams.set(ORG_ID_QUERY_PARAM, tail);
1201
1238
  window.history.replaceState(window.history.state, "", url.toString());
1202
1239
  }
1203
1240
  } catch {
@@ -1518,6 +1555,7 @@ function useChromeMode(arg = {}) {
1518
1555
  const selected = selectedModeId ? options.find((m) => m.id === selectedModeId) ?? null : null;
1519
1556
  const current = pinned ?? selected ?? options[0] ?? MODE_DEFS.find((m) => m.id === fallbackModeId) ?? MODE_DEFS[0];
1520
1557
  return {
1558
+ id: current.id,
1521
1559
  label: current.label,
1522
1560
  icon: current.icon,
1523
1561
  // Left-Navigation §7 / Reference/UI-Platform: always the row of three icon
@@ -1548,6 +1586,6 @@ function useChromeMode(arg = {}) {
1548
1586
  };
1549
1587
  }
1550
1588
 
1551
- export { ChromeTopBarActions, LoadingScreen, POLITOGY_APPS, PolitogyAppChrome, PolitogyAppFrame, deriveBreadcrumbs, footerLinks, looksLikeEmail, modeLandingHref, resolveIdentity, useChromeFooter, useChromeIdentity, useChromeMode, useChromeState, useChromeUserMenu, useIsSuperAdmin };
1589
+ export { ChromeTopBarActions, LoadingScreen, MODE_SEARCH_PLACEHOLDER, POLITOGY_APPS, PolitogyAppChrome, PolitogyAppFrame, deriveBreadcrumbs, footerLinks, looksLikeEmail, modeLandingHref, orgSwitchLanding, resolveIdentity, useChromeFooter, useChromeIdentity, useChromeMode, useChromeState, useChromeUserMenu, useIsSuperAdmin };
1552
1590
  //# sourceMappingURL=app-chrome.js.map
1553
1591
  //# sourceMappingURL=app-chrome.js.map