@the-portland-company/shell 0.55.0 → 0.57.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.
@@ -119,6 +119,19 @@ declare const POLITOGY_APPS: Record<PolitogyAppKey, {
119
119
  origin: string;
120
120
  storagePrefix: string;
121
121
  mode?: PolitogyAppMode;
122
+ /**
123
+ * DUAL-MODE apps (today: a2p — Specs/A2P: "the SMS/MMS delivery engine
124
+ * for Relationship Mode and Campaign Mode") have no single fixed mode.
125
+ * `mode` above stays the fallback/default identity for any code that
126
+ * still reads `meta.mode` directly; FrameBody instead resolves the
127
+ * EFFECTIVE mode via `resolveActiveMode` (?mode= query → the
128
+ * `politogy_active_mode` cookie → `defaultMode`) and uses that for the
129
+ * nav tree, the Mode pill, and the brand lockup.
130
+ */
131
+ dual?: {
132
+ modes: readonly StandardNavMode[];
133
+ defaultMode: StandardNavMode;
134
+ };
122
135
  /** See PolitogyFrameGrant. Omitted => the app may pass no frame input. */
123
136
  grants?: readonly PolitogyFrameGrant[];
124
137
  /**
@@ -119,6 +119,19 @@ declare const POLITOGY_APPS: Record<PolitogyAppKey, {
119
119
  origin: string;
120
120
  storagePrefix: string;
121
121
  mode?: PolitogyAppMode;
122
+ /**
123
+ * DUAL-MODE apps (today: a2p — Specs/A2P: "the SMS/MMS delivery engine
124
+ * for Relationship Mode and Campaign Mode") have no single fixed mode.
125
+ * `mode` above stays the fallback/default identity for any code that
126
+ * still reads `meta.mode` directly; FrameBody instead resolves the
127
+ * EFFECTIVE mode via `resolveActiveMode` (?mode= query → the
128
+ * `politogy_active_mode` cookie → `defaultMode`) and uses that for the
129
+ * nav tree, the Mode pill, and the brand lockup.
130
+ */
131
+ dual?: {
132
+ modes: readonly StandardNavMode[];
133
+ defaultMode: StandardNavMode;
134
+ };
122
135
  /** See PolitogyFrameGrant. Omitted => the app may pass no frame input. */
123
136
  grants?: readonly PolitogyFrameGrant[];
124
137
  /**
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { broadcastLogout, watchLogout } from './chunk-LL6LSJAH.js';
3
- import { clearBetaBannerDismissal, standardPolitogyNav, NavAppShell, NavChromeProvider, FuelAction, NavDisplayAction, TopBarWidgetSlot, SupportAction, NotificationsAction, useChromeMe, useThemePreference, useNavChromeFetcher, useChromeOrgs } from './chunk-K5GWJSHE.js';
3
+ import { clearBetaBannerDismissal, standardPolitogyNav, NavAppShell, NavChromeProvider, FuelAction, NavDisplayAction, TopBarWidgetSlot, SpecOverlayAction, SupportAction, NotificationsAction, useChromeMe, useThemePreference, useNavChromeFetcher, useChromeOrgs } from './chunk-TMOZTLUZ.js';
4
4
  import './chunk-U4RZPZET.js';
5
5
  import { NAV_DISPLAY_DEFAULT, resolveNavDisplay, isNavDisplayPreference, writeNavDisplayCookie, writeNavDisplayLocal, NAV_DISPLAY_STORAGE_KEY, readNavDisplayCookie } from './chunk-6UHJV2FH.js';
6
6
  import { resolveActiveOrg, isOrgId, writeActiveOrgCookie, ACTIVE_ORG_QUERY_PARAM, readActiveOrgCookie } from './chunk-W2HITMV5.js';
@@ -78,6 +78,58 @@ function resolveIdentity(input) {
78
78
  loaded: input.loaded ?? false
79
79
  };
80
80
  }
81
+
82
+ // src/mode-preference.ts
83
+ var ACTIVE_MODE_COOKIE = "politogy_active_mode";
84
+ var VALID_MODES = ["relationship", "campaign", "petition"];
85
+ function isStandardNavMode(v) {
86
+ return typeof v === "string" && VALID_MODES.includes(v);
87
+ }
88
+ function readActiveModeCookie() {
89
+ if (typeof document === "undefined") return null;
90
+ try {
91
+ const m = document.cookie.match(
92
+ new RegExp(`(?:^|;\\s*)${ACTIVE_MODE_COOKIE}=([^;]+)`)
93
+ );
94
+ const v = m ? decodeURIComponent(m[1]) : null;
95
+ return isStandardNavMode(v) ? v : null;
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
100
+ function writeActiveModeCookie(mode) {
101
+ if (typeof document === "undefined" || typeof window === "undefined") return;
102
+ const host = window.location.hostname;
103
+ const domainAttr = /(^|\.)politogyvrm\.com$/i.test(host) ? "; Domain=.politogyvrm.com" : "";
104
+ const secure = window.location.protocol === "https:" ? "; Secure" : "";
105
+ document.cookie = `${ACTIVE_MODE_COOKIE}=${encodeURIComponent(
106
+ mode
107
+ )}; Path=/; Max-Age=31536000; SameSite=Lax${domainAttr}${secure}`;
108
+ }
109
+ function modeFromSearch(search) {
110
+ if (!search) return null;
111
+ try {
112
+ const params = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
113
+ return params.get("mode");
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+ function resolveActiveMode({
119
+ search,
120
+ modes,
121
+ defaultMode
122
+ }) {
123
+ const fromQuery = modeFromSearch(search);
124
+ if (fromQuery && isStandardNavMode(fromQuery) && modes.includes(fromQuery)) {
125
+ return fromQuery;
126
+ }
127
+ const fromCookie = readActiveModeCookie();
128
+ if (fromCookie && modes.includes(fromCookie)) {
129
+ return fromCookie;
130
+ }
131
+ return defaultMode;
132
+ }
81
133
  var BOOT_STAGE_TABLE = [
82
134
  { id: "s0_session", weight: 5, critical: true, waitsOn: "Politogy ID session resolution" },
83
135
  { id: "s1_org", weight: 10, critical: true, waitsOn: "Chrome API /api/chrome/me + orgs" },
@@ -447,7 +499,17 @@ var POLITOGY_APPS = {
447
499
  // Campaign Mode apps — each scoped to Campaign, so they render the Campaign
448
500
  // tree and a static Campaign pill. Not yet deployed; hidden from the Campaign
449
501
  // nav by CAMPAIGN_FEATURE_AVAILABILITY until their launch flips the flag.
450
- a2p: { title: "A2P Messaging", origin: "https://a2p.politogyvrm.com", storagePrefix: "a2p", mode: "campaign" },
502
+ // Dual-mode: a2p is the SMS/MMS delivery engine for BOTH Relationship Mode
503
+ // and Campaign Mode (Specs/A2P). `mode: 'campaign'` stays the fallback for
504
+ // anything reading `meta.mode` directly; the effective mode a user sees is
505
+ // resolved per-visit (see `dual`, resolveActiveMode).
506
+ a2p: {
507
+ title: "A2P Messaging",
508
+ origin: "https://a2p.politogyvrm.com",
509
+ storagePrefix: "a2p",
510
+ mode: "campaign",
511
+ dual: { modes: ["relationship", "campaign"], defaultMode: "campaign" }
512
+ },
451
513
  phonebank: { title: "Phone Banking", origin: "https://phonebank.politogyvrm.com", storagePrefix: "phonebank", mode: "campaign" },
452
514
  canvass: { title: "Field Canvassing", origin: "https://canvass.politogyvrm.com", storagePrefix: "canvass", mode: "campaign" },
453
515
  signs: { title: "Sign Mapping", origin: "https://signs.politogyvrm.com", storagePrefix: "signs", mode: "campaign" }
@@ -584,9 +646,19 @@ function FrameBody({
584
646
  });
585
647
  const userMenu = core?.userMenu ?? derivedUserMenu;
586
648
  const footer = useChromeFooter(buildMeta ?? {});
587
- const appMode = meta.mode;
649
+ const resolvedDualMode = meta.dual ? resolveActiveMode({
650
+ search: search ?? (typeof window !== "undefined" ? window.location.search : void 0),
651
+ modes: meta.dual.modes,
652
+ defaultMode: meta.dual.defaultMode
653
+ }) : void 0;
654
+ const appMode = resolvedDualMode ?? meta.mode;
588
655
  const derivedMode = useChromeMode(appMode ? { pinnedModeId: appMode } : void 0);
589
656
  const mode = core?.mode ?? derivedMode;
657
+ const coreLiveMode = app === CORE_APP ? pathname.startsWith("/campaign") ? "campaign" : pathname.startsWith("/relationship") ? "relationship" : void 0 : void 0;
658
+ const cookieMode = coreLiveMode ?? (meta.dual ? void 0 : meta.mode);
659
+ useEffect(() => {
660
+ if (cookieMode) writeActiveModeCookie(cookieMode);
661
+ }, [cookieMode]);
590
662
  const coreOrgs = chrome?.orgs ?? [];
591
663
  const activeCoreOrg = coreOrgs.find((o) => o.id === chrome?.orgId) ?? null;
592
664
  const navDisplay = chrome?.navDisplay ?? "both";
@@ -959,18 +1031,71 @@ function AppDevNotes({
959
1031
  }),
960
1032
  [storagePrefix, identity.role]
961
1033
  );
962
- if (devNotes?.providedByApp) return /* @__PURE__ */ jsx(DevNotesSurface, { children });
963
- return /* @__PURE__ */ jsx(DevNotesProvider, { adapter, user, config, children: /* @__PURE__ */ jsx(DevNotesSurface, { children }) });
1034
+ const activatedRef = useRef(false);
1035
+ const activationWaitersRef = useRef([]);
1036
+ const activate = useCallback(() => {
1037
+ if (activatedRef.current) return;
1038
+ activatedRef.current = true;
1039
+ const waiters = activationWaitersRef.current;
1040
+ activationWaitersRef.current = [];
1041
+ waiters.forEach((resolve) => resolve());
1042
+ }, []);
1043
+ const waitForActivation = useCallback(() => {
1044
+ if (activatedRef.current) return Promise.resolve();
1045
+ return new Promise((resolve) => {
1046
+ activationWaitersRef.current.push(resolve);
1047
+ });
1048
+ }, []);
1049
+ const lazyAdapter = useMemo(() => {
1050
+ const GATED_METHODS = [
1051
+ "fetchTasks",
1052
+ "fetchTaskTypes",
1053
+ "fetchTaskLists",
1054
+ "fetchCollaborators",
1055
+ "fetchCapabilities",
1056
+ "getAppLinkStatus",
1057
+ "fetchBugReports",
1058
+ "fetchBugReportTypes"
1059
+ ];
1060
+ const wrapped = { ...adapter };
1061
+ for (const method of GATED_METHODS) {
1062
+ const original = adapter[method].bind(adapter);
1063
+ wrapped[method] = (...args) => waitForActivation().then(() => original(...args));
1064
+ }
1065
+ let idleScheduled = false;
1066
+ const originalUnreadCounts = adapter.fetchUnreadCounts.bind(adapter);
1067
+ wrapped.fetchUnreadCounts = () => {
1068
+ if (idleScheduled || typeof window === "undefined") return originalUnreadCounts();
1069
+ idleScheduled = true;
1070
+ return new Promise((resolve, reject) => {
1071
+ const run = () => originalUnreadCounts().then(resolve, reject);
1072
+ const ric = window.requestIdleCallback;
1073
+ if (ric) ric(run);
1074
+ else setTimeout(run, 0);
1075
+ });
1076
+ };
1077
+ return wrapped;
1078
+ }, [adapter, waitForActivation]);
1079
+ if (devNotes?.providedByApp) return /* @__PURE__ */ jsx(DevNotesSurface, { activate, children });
1080
+ return /* @__PURE__ */ jsx(DevNotesProvider, { adapter: lazyAdapter, user, config, children: /* @__PURE__ */ jsx(DevNotesSurface, { activate, children }) });
964
1081
  }
965
1082
  var DevNotesTasksContext = createContext({
966
1083
  ready: false,
967
1084
  openTasks: () => {
1085
+ },
1086
+ activate: () => {
968
1087
  }
969
1088
  });
970
- function DevNotesSurface({ children }) {
1089
+ function DevNotesSurface({
1090
+ activate,
1091
+ children
1092
+ }) {
971
1093
  const [tasksOpen, setTasksOpen] = useState(false);
972
- const openTasks = useCallback(() => setTasksOpen(true), []);
973
- const tasksCtx = useMemo(() => ({ ready: true, openTasks }), [openTasks]);
1094
+ const openTasks = useCallback(() => {
1095
+ activate();
1096
+ setTasksOpen(true);
1097
+ }, [activate]);
1098
+ const tasksCtx = useMemo(() => ({ ready: true, openTasks, activate }), [openTasks, activate]);
974
1099
  return /* @__PURE__ */ jsxs(DevNotesTasksContext.Provider, { value: tasksCtx, children: [
975
1100
  children,
976
1101
  tasksOpen ? /* @__PURE__ */ jsx(DevNotesTaskListModal, { open: true, onClose: () => setTasksOpen(false) }) : null,
@@ -1026,14 +1151,15 @@ function ChromeTopBarActions() {
1026
1151
  children: /* @__PURE__ */ jsx(ChromeDevNotesMenu, {})
1027
1152
  }
1028
1153
  ),
1154
+ /* @__PURE__ */ jsx(SpecOverlayAction, { orgId: state.orgId }),
1029
1155
  /* @__PURE__ */ jsx(SupportAction, { orgId: state.orgId }),
1030
1156
  /* @__PURE__ */ jsx(NotificationsAction, { orgId: state.orgId })
1031
1157
  ] });
1032
1158
  }
1033
1159
  function ChromeDevNotesMenu() {
1034
- const { ready, openTasks } = useContext(DevNotesTasksContext);
1160
+ const { ready, openTasks, activate } = useContext(DevNotesTasksContext);
1035
1161
  if (!ready) return null;
1036
- return /* @__PURE__ */ jsx(DevNotesMenu, { position: "top-right", onViewTasks: openTasks });
1162
+ return /* @__PURE__ */ jsx("span", { style: { display: "contents" }, onPointerDownCapture: activate, onFocusCapture: activate, onKeyDownCapture: activate, children: /* @__PURE__ */ jsx(DevNotesMenu, { position: "top-right", onViewTasks: openTasks }) });
1037
1163
  }
1038
1164
  function useIsSuperAdmin() {
1039
1165
  const state = useChromeState();