@the-portland-company/shell 0.54.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.
- package/CHANGELOG.md +54 -0
- package/dist/app-chrome.cjs +94 -21
- package/dist/app-chrome.cjs.map +1 -1
- package/dist/app-chrome.d.cts +13 -0
- package/dist/app-chrome.d.ts +13 -0
- package/dist/app-chrome.js +75 -3
- package/dist/app-chrome.js.map +1 -1
- package/dist/{chunk-XEFBZ3PR.js → chunk-MR6TLHEE.js} +22 -21
- package/dist/chunk-MR6TLHEE.js.map +1 -0
- package/dist/native.cjs +20 -19
- package/dist/native.cjs.map +1 -1
- package/dist/native.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-XEFBZ3PR.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,57 @@
|
|
|
1
|
+
## 0.56.0 — 2026-09-30
|
|
2
|
+
|
|
3
|
+
### Fixed — mode continuity for dual-mode apps (a2p)
|
|
4
|
+
|
|
5
|
+
a2p (Specs/A2P) is the SMS/MMS delivery engine for BOTH Relationship Mode and
|
|
6
|
+
Campaign Mode, but it was registered `mode: 'campaign'` only, so the
|
|
7
|
+
Relationship tree's "SMS/MMS Blast" row sent users into a2p and the whole left
|
|
8
|
+
nav flipped to the Campaign tree + "Campaign Mode" pill — a jarring identity
|
|
9
|
+
switch for a user who never left Relationship Mode.
|
|
10
|
+
|
|
11
|
+
`POLITOGY_APPS[app]` now supports an optional `dual: { modes, defaultMode }`.
|
|
12
|
+
a2p declares `dual: { modes: ['relationship', 'campaign'], defaultMode:
|
|
13
|
+
'campaign' }`; its EFFECTIVE mode is resolved per-visit by the new
|
|
14
|
+
`resolveActiveMode` (`src/mode-preference.ts`): `?mode=` query param → the new
|
|
15
|
+
cross-subdomain `politogy_active_mode` cookie → the app's own default. Every
|
|
16
|
+
pinned app and core writes that cookie with its own mode, so arriving at a2p
|
|
17
|
+
after using Relationship Mode elsewhere resolves to Relationship even without
|
|
18
|
+
an explicit `?mode=` link. The Relationship tree's "SMS/MMS Blast" row now
|
|
19
|
+
links straight to a2p with `?mode=relationship` (the dead
|
|
20
|
+
`/relationship/sms-mms-blast/*` core routes are removed); the Campaign tree's
|
|
21
|
+
existing A2P row is unchanged.
|
|
22
|
+
|
|
23
|
+
## 0.55.0 — 2026-09-30
|
|
24
|
+
|
|
25
|
+
### Fixed — org pill body no longer takes the Mode accent (`./native`)
|
|
26
|
+
|
|
27
|
+
Reference/UI-Platform ("Left bar" / "Per-mode branding") is explicit: the org
|
|
28
|
+
pill sits on a **neutral surface** — only the circular org badge carries a
|
|
29
|
+
brand or Mode color, never the pill body. The `./native` `NavSidebar`'s org
|
|
30
|
+
pill (both the disabled single-org button and the multi-org `<details>`
|
|
31
|
+
selector) resolved `var(--mode-accent, var(--panel-elevated))` as its own
|
|
32
|
+
background and `var(--mode-on-accent, var(--foreground))` as its text color —
|
|
33
|
+
so a Mode with its own accent (Campaign's `#8F1F2A`) painted the **whole
|
|
34
|
+
pill** solid red instead of just the badge. Reported live as "V1 styling" on
|
|
35
|
+
a2p.politogyvrm.com (shell 0.46.0, still present through 0.54.0). Both spots
|
|
36
|
+
now render the neutral `--panel-elevated` / `--line` / `--foreground` chain
|
|
37
|
+
unconditionally; the badge (unchanged) still resolves the org's brand color
|
|
38
|
+
or, absent one, the chrome/Mode accent.
|
|
39
|
+
|
|
40
|
+
### Fixed — mode pill resolves the semantic Mode accent, not the flat global one
|
|
41
|
+
|
|
42
|
+
The mode pill (both the disabled single-mode button and the multi-mode
|
|
43
|
+
selector) was hardcoded to `var(--color-accent)` / `text-white`, so a Mode
|
|
44
|
+
that declares its own `chrome.accent` (Reference/UI-Platform → "Per-mode
|
|
45
|
+
branding") never reached this pill — only the rail background did. It now
|
|
46
|
+
resolves `var(--mode-accent, var(--color-accent))` with
|
|
47
|
+
`var(--mode-on-accent, white)`, matching the org pill's badge and the active
|
|
48
|
+
nav row's existing pattern.
|
|
49
|
+
|
|
50
|
+
### Why
|
|
51
|
+
|
|
52
|
+
Live audit against Reference/UI-Platform + the shared Relationship Mode UI
|
|
53
|
+
design template (orchestrator rail-parity sweep, 2026-09-30).
|
|
54
|
+
|
|
1
55
|
## 0.53.0 — 2026-09-29
|
|
2
56
|
|
|
3
57
|
### Added — Loading Screen boot surface (Specs/Loading-Screen)
|
package/dist/app-chrome.cjs
CHANGED
|
@@ -1494,11 +1494,6 @@ var APP_EMAIL_CHILDREN = [
|
|
|
1494
1494
|
{ label: "Deliverability", icon: "activity", href: `${EMAIL_APP}/deliverability` }
|
|
1495
1495
|
];
|
|
1496
1496
|
var EMAIL_V1_LEGACY = { label: "Email V1 (legacy)", icon: "clock", path: "/relationship/email/dashboard" };
|
|
1497
|
-
var SMS_CHILDREN = [
|
|
1498
|
-
{ label: "Dashboard", icon: "grid", path: "/relationship/sms-mms-blast/dashboard" },
|
|
1499
|
-
{ label: "Compose & Send", icon: "send", path: "/relationship/sms-mms-blast/compose-and-send" },
|
|
1500
|
-
{ label: "Conversations", icon: "messageSquare", path: "/relationship/sms-mms-blast/conversations" }
|
|
1501
|
-
];
|
|
1502
1497
|
var CTA_CHILDREN = [
|
|
1503
1498
|
{ label: "Dashboard", icon: "grid", path: "/relationship/cta-database/dashboard" },
|
|
1504
1499
|
{ label: "Create & Edit", icon: "edit", path: "/relationship/cta-creator/create-and-edit" },
|
|
@@ -1579,6 +1574,14 @@ function standardPolitogyNav(opts) {
|
|
|
1579
1574
|
href: withOrg(abs(c.path)),
|
|
1580
1575
|
children: c.children ? leaves(c.children) : void 0
|
|
1581
1576
|
}));
|
|
1577
|
+
const appLeaves = (origin, children, modeParam) => {
|
|
1578
|
+
const base2 = origin.replace(/\/+$/, "");
|
|
1579
|
+
return children.map((c) => {
|
|
1580
|
+
const url = c.path ? `${base2}/${c.path}` : `${base2}/`;
|
|
1581
|
+
const withMode = modeParam ? `${url}${url.includes("?") ? "&" : "?"}mode=${modeParam}` : url;
|
|
1582
|
+
return { label: c.label, icon: icon(c.icon), href: withOrg(withMode) };
|
|
1583
|
+
});
|
|
1584
|
+
};
|
|
1582
1585
|
const buildRelationship = () => {
|
|
1583
1586
|
const contacts = opts.contactsItem ? {
|
|
1584
1587
|
label: "Contacts",
|
|
@@ -1607,11 +1610,17 @@ function standardPolitogyNav(opts) {
|
|
|
1607
1610
|
...leaves([EMAIL_V1_LEGACY])
|
|
1608
1611
|
]
|
|
1609
1612
|
},
|
|
1613
|
+
// SMS/MMS Blast IS a2p (Specs/A2P) — the standalone delivery engine
|
|
1614
|
+
// shared with Campaign Mode, not a core-hosted surface (the old
|
|
1615
|
+
// `/relationship/sms-mms-blast/*` core routes are dead; core no longer
|
|
1616
|
+
// hosts them). Every link carries `mode=relationship` so a2p's nav
|
|
1617
|
+
// resolves the Relationship tree instead of its own Campaign default
|
|
1618
|
+
// (resolveActiveMode, mode-preference.ts).
|
|
1610
1619
|
{
|
|
1611
1620
|
label: "SMS/MMS Blast",
|
|
1612
1621
|
icon: icon("messageSquare"),
|
|
1613
|
-
href: withOrg(
|
|
1614
|
-
children:
|
|
1622
|
+
href: withOrg(`${A2P_ORIGIN}?mode=relationship`),
|
|
1623
|
+
children: appLeaves(A2P_ORIGIN, APP_A2P_CHILDREN, "relationship")
|
|
1615
1624
|
},
|
|
1616
1625
|
// These two carry no top-level href in VRM core (parent row only toggles).
|
|
1617
1626
|
{ label: "Calls to Action", icon: icon("layout"), children: leaves(CTA_CHILDREN) },
|
|
@@ -1652,14 +1661,6 @@ function standardPolitogyNav(opts) {
|
|
|
1652
1661
|
...CAMPAIGN_FEATURE_AVAILABILITY,
|
|
1653
1662
|
...opts.campaignAvailability
|
|
1654
1663
|
};
|
|
1655
|
-
const appLeaves = (origin, children) => {
|
|
1656
|
-
const base2 = origin.replace(/\/+$/, "");
|
|
1657
|
-
return children.map((c) => ({
|
|
1658
|
-
label: c.label,
|
|
1659
|
-
icon: icon(c.icon),
|
|
1660
|
-
href: withOrg(c.path ? `${base2}/${c.path}` : `${base2}/`)
|
|
1661
|
-
}));
|
|
1662
|
-
};
|
|
1663
1664
|
const rows = [
|
|
1664
1665
|
{ label: "Dashboard", icon: icon("home"), href: withOrg(abs("/campaign/dashboard")) },
|
|
1665
1666
|
{ label: "Voter Contact", icon: icon("target"), href: withOrg(abs("/campaign/campaigns")) },
|
|
@@ -1932,7 +1933,7 @@ function NavSidebar({
|
|
|
1932
1933
|
),
|
|
1933
1934
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sr-only", children: "Loading organization" })
|
|
1934
1935
|
] }) : org.organizations && org.organizations.length > 0 && org.onSwitch ? /* @__PURE__ */ jsxRuntime.jsxs("details", { "data-nav-details": true, className: "group relative", children: [
|
|
1935
|
-
/* @__PURE__ */ jsxRuntime.jsxs("summary", { className: "flex w-full cursor-pointer list-none items-center justify-between gap-2 rounded-md border border-[color:var(--
|
|
1936
|
+
/* @__PURE__ */ jsxRuntime.jsxs("summary", { className: "flex w-full cursor-pointer list-none items-center justify-between gap-2 rounded-md border border-[color:var(--line)] bg-[color:var(--panel-elevated)] px-2.5 py-2 text-left text-[color:var(--foreground)] shadow-sm transition hover:bg-[color:var(--panel)] [&::-webkit-details-marker]:hidden", children: [
|
|
1936
1937
|
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 items-center gap-2.5", children: [
|
|
1937
1938
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1938
1939
|
"span",
|
|
@@ -2021,7 +2022,7 @@ function NavSidebar({
|
|
|
2021
2022
|
{
|
|
2022
2023
|
type: "button",
|
|
2023
2024
|
onClick: org.onClick,
|
|
2024
|
-
className: "flex w-full items-center justify-between gap-2 rounded-md border border-[color:var(--
|
|
2025
|
+
className: "flex w-full items-center justify-between gap-2 rounded-md border border-[color:var(--line)] bg-[color:var(--panel-elevated)] px-2.5 py-2 text-left text-[color:var(--foreground)] shadow-sm transition hover:bg-[color:var(--panel)]",
|
|
2025
2026
|
children: [
|
|
2026
2027
|
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 items-center gap-2.5", children: [
|
|
2027
2028
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -2040,7 +2041,7 @@ function NavSidebar({
|
|
|
2040
2041
|
)
|
|
2041
2042
|
] }) : null,
|
|
2042
2043
|
mode ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-4", children: mode.options && mode.options.length > 1 && mode.onSelect ? /* @__PURE__ */ jsxRuntime.jsxs("details", { "data-nav-details": true, className: "group relative", children: [
|
|
2043
|
-
/* @__PURE__ */ jsxRuntime.jsxs("summary", { className: "flex w-full cursor-pointer list-none items-center justify-between gap-2 rounded-md bg-[color:var(--color-accent)] px-2.5 py-2 text-white shadow-sm transition hover:opacity-95 [&::-webkit-details-marker]:hidden", children: [
|
|
2044
|
+
/* @__PURE__ */ jsxRuntime.jsxs("summary", { className: "flex w-full cursor-pointer list-none items-center justify-between gap-2 rounded-md bg-[color:var(--mode-accent,var(--color-accent))] px-2.5 py-2 text-[color:var(--mode-on-accent,white)] shadow-sm transition hover:bg-[color:var(--mode-accent-strong,var(--color-accent))] hover:opacity-95 [&::-webkit-details-marker]:hidden", children: [
|
|
2044
2045
|
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-2.5", children: [
|
|
2045
2046
|
ModeIcon ? /* @__PURE__ */ jsxRuntime.jsx(ModeIcon, { size: 16, className: "shrink-0" }) : null,
|
|
2046
2047
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-semibold tracking-[-0.01em]", children: mode.label })
|
|
@@ -2075,7 +2076,7 @@ function NavSidebar({
|
|
|
2075
2076
|
"button",
|
|
2076
2077
|
{
|
|
2077
2078
|
type: "button",
|
|
2078
|
-
className: "flex w-full items-center justify-between gap-2 rounded-md bg-[color:var(--color-accent)] px-2.5 py-2 text-white shadow-sm",
|
|
2079
|
+
className: "flex w-full items-center justify-between gap-2 rounded-md bg-[color:var(--mode-accent,var(--color-accent))] px-2.5 py-2 text-[color:var(--mode-on-accent,white)] shadow-sm",
|
|
2079
2080
|
children: [
|
|
2080
2081
|
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-2.5", children: [
|
|
2081
2082
|
ModeIcon ? /* @__PURE__ */ jsxRuntime.jsx(ModeIcon, { size: 16, className: "shrink-0" }) : null,
|
|
@@ -3256,6 +3257,58 @@ function watchLogout(onLogout, options = {}) {
|
|
|
3256
3257
|
}
|
|
3257
3258
|
return cleanup;
|
|
3258
3259
|
}
|
|
3260
|
+
|
|
3261
|
+
// src/mode-preference.ts
|
|
3262
|
+
var ACTIVE_MODE_COOKIE = "politogy_active_mode";
|
|
3263
|
+
var VALID_MODES = ["relationship", "campaign", "petition"];
|
|
3264
|
+
function isStandardNavMode(v) {
|
|
3265
|
+
return typeof v === "string" && VALID_MODES.includes(v);
|
|
3266
|
+
}
|
|
3267
|
+
function readActiveModeCookie() {
|
|
3268
|
+
if (typeof document === "undefined") return null;
|
|
3269
|
+
try {
|
|
3270
|
+
const m = document.cookie.match(
|
|
3271
|
+
new RegExp(`(?:^|;\\s*)${ACTIVE_MODE_COOKIE}=([^;]+)`)
|
|
3272
|
+
);
|
|
3273
|
+
const v = m ? decodeURIComponent(m[1]) : null;
|
|
3274
|
+
return isStandardNavMode(v) ? v : null;
|
|
3275
|
+
} catch {
|
|
3276
|
+
return null;
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
function writeActiveModeCookie(mode) {
|
|
3280
|
+
if (typeof document === "undefined" || typeof window === "undefined") return;
|
|
3281
|
+
const host = window.location.hostname;
|
|
3282
|
+
const domainAttr = /(^|\.)politogyvrm\.com$/i.test(host) ? "; Domain=.politogyvrm.com" : "";
|
|
3283
|
+
const secure = window.location.protocol === "https:" ? "; Secure" : "";
|
|
3284
|
+
document.cookie = `${ACTIVE_MODE_COOKIE}=${encodeURIComponent(
|
|
3285
|
+
mode
|
|
3286
|
+
)}; Path=/; Max-Age=31536000; SameSite=Lax${domainAttr}${secure}`;
|
|
3287
|
+
}
|
|
3288
|
+
function modeFromSearch(search) {
|
|
3289
|
+
if (!search) return null;
|
|
3290
|
+
try {
|
|
3291
|
+
const params = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
|
|
3292
|
+
return params.get("mode");
|
|
3293
|
+
} catch {
|
|
3294
|
+
return null;
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
function resolveActiveMode({
|
|
3298
|
+
search,
|
|
3299
|
+
modes,
|
|
3300
|
+
defaultMode
|
|
3301
|
+
}) {
|
|
3302
|
+
const fromQuery = modeFromSearch(search);
|
|
3303
|
+
if (fromQuery && isStandardNavMode(fromQuery) && modes.includes(fromQuery)) {
|
|
3304
|
+
return fromQuery;
|
|
3305
|
+
}
|
|
3306
|
+
const fromCookie = readActiveModeCookie();
|
|
3307
|
+
if (fromCookie && modes.includes(fromCookie)) {
|
|
3308
|
+
return fromCookie;
|
|
3309
|
+
}
|
|
3310
|
+
return defaultMode;
|
|
3311
|
+
}
|
|
3259
3312
|
var BOOT_STAGE_TABLE = [
|
|
3260
3313
|
{ id: "s0_session", weight: 5, critical: true, waitsOn: "Politogy ID session resolution" },
|
|
3261
3314
|
{ id: "s1_org", weight: 10, critical: true, waitsOn: "Chrome API /api/chrome/me + orgs" },
|
|
@@ -3625,7 +3678,17 @@ var POLITOGY_APPS = {
|
|
|
3625
3678
|
// Campaign Mode apps — each scoped to Campaign, so they render the Campaign
|
|
3626
3679
|
// tree and a static Campaign pill. Not yet deployed; hidden from the Campaign
|
|
3627
3680
|
// nav by CAMPAIGN_FEATURE_AVAILABILITY until their launch flips the flag.
|
|
3628
|
-
|
|
3681
|
+
// Dual-mode: a2p is the SMS/MMS delivery engine for BOTH Relationship Mode
|
|
3682
|
+
// and Campaign Mode (Specs/A2P). `mode: 'campaign'` stays the fallback for
|
|
3683
|
+
// anything reading `meta.mode` directly; the effective mode a user sees is
|
|
3684
|
+
// resolved per-visit (see `dual`, resolveActiveMode).
|
|
3685
|
+
a2p: {
|
|
3686
|
+
title: "A2P Messaging",
|
|
3687
|
+
origin: "https://a2p.politogyvrm.com",
|
|
3688
|
+
storagePrefix: "a2p",
|
|
3689
|
+
mode: "campaign",
|
|
3690
|
+
dual: { modes: ["relationship", "campaign"], defaultMode: "campaign" }
|
|
3691
|
+
},
|
|
3629
3692
|
phonebank: { title: "Phone Banking", origin: "https://phonebank.politogyvrm.com", storagePrefix: "phonebank", mode: "campaign" },
|
|
3630
3693
|
canvass: { title: "Field Canvassing", origin: "https://canvass.politogyvrm.com", storagePrefix: "canvass", mode: "campaign" },
|
|
3631
3694
|
signs: { title: "Sign Mapping", origin: "https://signs.politogyvrm.com", storagePrefix: "signs", mode: "campaign" }
|
|
@@ -3762,9 +3825,19 @@ function FrameBody({
|
|
|
3762
3825
|
});
|
|
3763
3826
|
const userMenu = core?.userMenu ?? derivedUserMenu;
|
|
3764
3827
|
const footer = useChromeFooter(buildMeta ?? {});
|
|
3765
|
-
const
|
|
3828
|
+
const resolvedDualMode = meta.dual ? resolveActiveMode({
|
|
3829
|
+
search: search ?? (typeof window !== "undefined" ? window.location.search : void 0),
|
|
3830
|
+
modes: meta.dual.modes,
|
|
3831
|
+
defaultMode: meta.dual.defaultMode
|
|
3832
|
+
}) : void 0;
|
|
3833
|
+
const appMode = resolvedDualMode ?? meta.mode;
|
|
3766
3834
|
const derivedMode = useChromeMode(appMode ? { pinnedModeId: appMode } : void 0);
|
|
3767
3835
|
const mode = core?.mode ?? derivedMode;
|
|
3836
|
+
const coreLiveMode = app === CORE_APP ? pathname.startsWith("/campaign") ? "campaign" : pathname.startsWith("/relationship") ? "relationship" : void 0 : void 0;
|
|
3837
|
+
const cookieMode = coreLiveMode ?? (meta.dual ? void 0 : meta.mode);
|
|
3838
|
+
react.useEffect(() => {
|
|
3839
|
+
if (cookieMode) writeActiveModeCookie(cookieMode);
|
|
3840
|
+
}, [cookieMode]);
|
|
3768
3841
|
const coreOrgs = chrome?.orgs ?? [];
|
|
3769
3842
|
const activeCoreOrg = coreOrgs.find((o) => o.id === chrome?.orgId) ?? null;
|
|
3770
3843
|
const navDisplay = chrome?.navDisplay ?? "both";
|