@the-portland-company/shell 0.84.0 → 0.86.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 CHANGED
@@ -1,3 +1,22 @@
1
+ ## 0.86.0
2
+
3
+ Campaign Mode gets a Tribes row. Spec cited: Specs/Tribes §2.3–§2.4 and Specs/Left-Navigation §9.2 (decided 2026-10-16 by Ben: Campaign and Relationship Tribes are separate; Campaign Tribes live in Campaign Mode).
4
+
5
+ - **Campaign tree.** Race data now lists Battleground · District data · **Tribes** (`users-round`), linking to `https://battleground.politogyvrm.com/tribes` (org carried as before). It is a page of the Battleground app at `/tribes`, so it is the only active row there.
6
+ - **Relationship tree unchanged.** Relationship Tribes stay inside Contacts → Tribes; there is still no Relationship Tribes row.
7
+ - **Release order.** Publish after the Battleground app serves `/tribes`, so the row never links to a missing page.
8
+ - Tests: `__tests__/standardNav.test.ts`.
9
+
10
+ ## 0.85.0
11
+
12
+ The mode switcher, the rail and the rows always show one mode. Spec cited: Specs/Left-Navigation §9.1, §9.2, §9.4 and §11.1 ("the rail never shows a mix of two modes").
13
+
14
+ - **Why.** Reported by Ben and measured on production 2026-10-16 on "We need Steve": after picking Campaign, Contacts (and Forms, Surveys, CTAs) drew the red "Campaign mode" rail and the Campaign search placeholder around Relationship rows, even after a reload. These apps had no registry mode, so the switcher took the user's last choice (`vrm.activeMode`) while the rows stayed Relationship.
15
+ - **Registry.** `contacts`, `forms`, `surveys` and `ctas` are pinned to `mode: 'relationship'`: they are Relationship rows only (§9.1) and Campaign has none of them (§9.2). Same fix as the Dashboard's earlier pin. Clicking another mode still goes to that mode's landing.
16
+ - **Unpinned apps** (Settings: the Account section is in every mode, §9.4) draw the tree of the mode their switcher shows, so rows, rail colour and label agree. Core (`vrm`) is unchanged.
17
+ - **Loading Screen.** For an unpinned app it stays up until the organization's Modes are read, so the rail never changes mode under the user. `NavMode` gains optional `resolved` (false while the id is a stand-in).
18
+ - Tests: `__tests__/modeNavAgree.test.tsx`; `frame-grants.test.tsx` updated for the new pins.
19
+
1
20
  ## 0.84.0
2
21
 
3
22
  An organization switch lands on the new organization's Relationship Dashboard. Spec cited: Specs/Left-Navigation §6 and §11.2 (v21, decided 2026-10-15 by Ben; supersedes the 2026-10-06 rule that kept mode, page and tab).
@@ -2443,13 +2443,19 @@ function standardPolitogyNav(opts) {
2443
2443
  const rows = [
2444
2444
  // Left-Navigation §9.2: Race data -> Research -> GOTV -> Account.
2445
2445
  // Battleground is the Campaign landing page; there is no Campaign
2446
- // Dashboard row, and Voter Contact and Tribes have no row.
2446
+ // Dashboard row, and Voter Contact has no row. Campaign Tribes has its
2447
+ // own row after District data (decided 2026-10-16).
2447
2448
  { kind: "section", label: "Race data" },
2448
2449
  // Battleground is the Campaign Mode landing page, its own app on its own
2449
2450
  // host (registry key `battleground`; Specs/Left-Navigation §9.2, §13, §14).
2450
2451
  { label: "Battleground", icon: icon("crosshair"), href: withOrg(`${BATTLEGROUND_ORIGIN}battleground`) },
2451
2452
  // District Data is its own Campaign Mode app on its own host (registry key `districts`).
2452
2453
  { label: "District data", icon: icon("landmark"), href: withOrg(`${DISTRICTS_ORIGIN}districts`) },
2454
+ // Campaign Tribes (Specs/Tribes §2.3–§2.4, Left-Navigation §9.2; decided
2455
+ // 2026-10-16, Ben): a page of the Battleground app at /tribes, so it is
2456
+ // the only active row there (Battleground's /battleground prefix does
2457
+ // not match it). Relationship Tribes stay inside Contacts.
2458
+ { label: "Tribes", icon: icon("usersRound"), href: withOrg(`${BATTLEGROUND_ORIGIN}tribes`) },
2453
2459
  { kind: "section", label: "Research" },
2454
2460
  {
2455
2461
  label: "Polling",
@@ -4593,10 +4599,19 @@ var CORE_APP = "vrm";
4593
4599
  var DEFAULT_SIGN_OUT_URL = "/auth/signout";
4594
4600
  var POLITOGY_APPS = {
4595
4601
  vrm: { title: "Politogy VRM", origin: "https://app.politogyvrm.com", storagePrefix: "vrm" },
4602
+ // Contacts, Forms, Surveys and CTAs are Relationship rows only (Specs/
4603
+ // Left-Navigation §9.1; Campaign has none of them, §9.2), so they are
4604
+ // PINNED to Relationship like the Dashboard below. Left unscoped, the mode
4605
+ // switcher and rail colour took the user's last mode choice while the rows
4606
+ // stayed Relationship: after picking Campaign, Contacts drew the red
4607
+ // "Campaign mode" rail around Relationship rows, even after a reload
4608
+ // (measured 2026-10-16 on We need Steve; §11.1 "the rail never shows a mix
4609
+ // of two modes").
4596
4610
  contacts: {
4597
4611
  title: "Contacts",
4598
4612
  origin: "https://contacts.politogyvrm.com",
4599
4613
  storagePrefix: "contacts",
4614
+ mode: "relationship",
4600
4615
  pageTitle: false
4601
4616
  },
4602
4617
  // 'title' for forms/surveys/polls/ctas for the same reason as petitions
@@ -4605,11 +4620,11 @@ var POLITOGY_APPS = {
4605
4620
  // page header falls back to the static registry title forever, so the
4606
4621
  // designed "<mode> mode" eyebrow + H2 + subtitle block never gets the real
4607
4622
  // per-page title an app passes it (UI-Platform → Page header).
4608
- forms: { title: "Forms", origin: "https://forms.politogyvrm.com", storagePrefix: "forms", grants: ["title"] },
4609
- surveys: { title: "Surveys", origin: "https://surveys.politogyvrm.com", storagePrefix: "surveys", grants: ["title"] },
4623
+ forms: { title: "Forms", origin: "https://forms.politogyvrm.com", storagePrefix: "forms", mode: "relationship", grants: ["title"] },
4624
+ surveys: { title: "Surveys", origin: "https://surveys.politogyvrm.com", storagePrefix: "surveys", mode: "relationship", grants: ["title"] },
4610
4625
  // Specs/Left-Navigation §9.2: Polling is a Campaign row (Research).
4611
4626
  polls: { title: "Polls", origin: "https://polls.politogyvrm.com", storagePrefix: "polls", mode: "campaign", grants: ["title"] },
4612
- ctas: { title: "CTAs", origin: "https://ctas.politogyvrm.com", storagePrefix: "ctas", grants: ["title"] },
4627
+ ctas: { title: "CTAs", origin: "https://ctas.politogyvrm.com", storagePrefix: "ctas", mode: "relationship", grants: ["title"] },
4613
4628
  // V2 apps that replace retired app.politogyvrm.com pages (Reference/
4614
4629
  // UI-Platform v42 "Retirement of app.politogyvrm.com"). Dashboard is the
4615
4630
  // Relationship Dashboard, the Relationship Mode landing page, and nothing
@@ -4833,7 +4848,7 @@ function FrameBody({
4833
4848
  const coreOrgs = chrome?.orgs ?? [];
4834
4849
  const activeCoreOrg = coreOrgs.find((o) => o.id === chrome?.orgId) ?? null;
4835
4850
  const navDisplay = chrome?.navDisplay ?? "both";
4836
- const navMode = appMode ?? "relationship";
4851
+ const navMode = appMode ?? (app !== CORE_APP && isStandardNavMode(mode.id) ? mode.id : "relationship");
4837
4852
  const activeOrgId = chrome?.orgId ?? null;
4838
4853
  const derivedNavItems = react.useMemo(
4839
4854
  () => standardPolitogyNav({
@@ -4876,7 +4891,7 @@ function FrameBody({
4876
4891
  {
4877
4892
  visible: bootVisible,
4878
4893
  bootProgress,
4879
- syntheticReady: (appReady ?? chromeResolved) && !chrome?.orgPending,
4894
+ syntheticReady: (appReady ?? chromeResolved) && !chrome?.orgPending && (appMode !== void 0 || mode.resolved !== false),
4880
4895
  onExitComplete: () => setBootVisible(false)
4881
4896
  },
4882
4897
  bootGeneration
@@ -5596,6 +5611,7 @@ function useChromeMode(arg = {}) {
5596
5611
  const orgId = ctx?.orgId ?? null;
5597
5612
  const orgPending = ctx?.orgPending ?? false;
5598
5613
  const [entitled, setEntitled] = react.useState(null);
5614
+ const [settledOrgId, setSettledOrgId] = react.useState(null);
5599
5615
  const [selectedModeId, setSelectedModeId] = react.useState(null);
5600
5616
  react.useEffect(() => {
5601
5617
  try {
@@ -5610,6 +5626,7 @@ function useChromeMode(arg = {}) {
5610
5626
  const bootMe = me;
5611
5627
  if (bootMe && bootMe.account === orgId && bootMe.modes !== void 0) {
5612
5628
  setEntitled(bootMe.modes ?? null);
5629
+ setSettledOrgId(orgId);
5613
5630
  return;
5614
5631
  }
5615
5632
  let cancelled = false;
@@ -5617,7 +5634,9 @@ function useChromeMode(arg = {}) {
5617
5634
  if (cancelled) return;
5618
5635
  const modes = data?.modes ?? null;
5619
5636
  setEntitled(modes);
5637
+ setSettledOrgId(orgId);
5620
5638
  }).catch(() => {
5639
+ if (!cancelled) setSettledOrgId(orgId);
5621
5640
  });
5622
5641
  return () => {
5623
5642
  cancelled = true;
@@ -5631,6 +5650,9 @@ function useChromeMode(arg = {}) {
5631
5650
  id: current.id,
5632
5651
  label: current.label,
5633
5652
  icon: current.icon,
5653
+ // A registry pin is final at once; otherwise the mode is final when the
5654
+ // active org's Modes are in (or nothing can be read: no chrome or no org).
5655
+ resolved: Boolean(pinned) || !dedupedFetch || !orgId || settledOrgId === orgId && !orgPending,
5634
5656
  // Left-Navigation §7 / Reference/UI-Platform: always the row of three icon
5635
5657
  // buttons. A mode the organization does not hold is shown dimmed and not
5636
5658
  // selectable, never hidden. Before entitlements land only the current mode