@the-portland-company/shell 0.55.0 → 0.56.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, SupportAction, NotificationsAction, useChromeMe, useThemePreference, useNavChromeFetcher, useChromeOrgs } from './chunk-MR6TLHEE.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";