@the-portland-company/shell 0.84.0 → 0.85.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,13 @@
1
+ ## 0.85.0
2
+
3
+ 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").
4
+
5
+ - **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.
6
+ - **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.
7
+ - **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.
8
+ - **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).
9
+ - Tests: `__tests__/modeNavAgree.test.tsx`; `frame-grants.test.tsx` updated for the new pins.
10
+
1
11
  ## 0.84.0
2
12
 
3
13
  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).
@@ -4593,10 +4593,19 @@ var CORE_APP = "vrm";
4593
4593
  var DEFAULT_SIGN_OUT_URL = "/auth/signout";
4594
4594
  var POLITOGY_APPS = {
4595
4595
  vrm: { title: "Politogy VRM", origin: "https://app.politogyvrm.com", storagePrefix: "vrm" },
4596
+ // Contacts, Forms, Surveys and CTAs are Relationship rows only (Specs/
4597
+ // Left-Navigation §9.1; Campaign has none of them, §9.2), so they are
4598
+ // PINNED to Relationship like the Dashboard below. Left unscoped, the mode
4599
+ // switcher and rail colour took the user's last mode choice while the rows
4600
+ // stayed Relationship: after picking Campaign, Contacts drew the red
4601
+ // "Campaign mode" rail around Relationship rows, even after a reload
4602
+ // (measured 2026-10-16 on We need Steve; §11.1 "the rail never shows a mix
4603
+ // of two modes").
4596
4604
  contacts: {
4597
4605
  title: "Contacts",
4598
4606
  origin: "https://contacts.politogyvrm.com",
4599
4607
  storagePrefix: "contacts",
4608
+ mode: "relationship",
4600
4609
  pageTitle: false
4601
4610
  },
4602
4611
  // 'title' for forms/surveys/polls/ctas for the same reason as petitions
@@ -4605,11 +4614,11 @@ var POLITOGY_APPS = {
4605
4614
  // page header falls back to the static registry title forever, so the
4606
4615
  // designed "<mode> mode" eyebrow + H2 + subtitle block never gets the real
4607
4616
  // 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"] },
4617
+ forms: { title: "Forms", origin: "https://forms.politogyvrm.com", storagePrefix: "forms", mode: "relationship", grants: ["title"] },
4618
+ surveys: { title: "Surveys", origin: "https://surveys.politogyvrm.com", storagePrefix: "surveys", mode: "relationship", grants: ["title"] },
4610
4619
  // Specs/Left-Navigation §9.2: Polling is a Campaign row (Research).
4611
4620
  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"] },
4621
+ ctas: { title: "CTAs", origin: "https://ctas.politogyvrm.com", storagePrefix: "ctas", mode: "relationship", grants: ["title"] },
4613
4622
  // V2 apps that replace retired app.politogyvrm.com pages (Reference/
4614
4623
  // UI-Platform v42 "Retirement of app.politogyvrm.com"). Dashboard is the
4615
4624
  // Relationship Dashboard, the Relationship Mode landing page, and nothing
@@ -4833,7 +4842,7 @@ function FrameBody({
4833
4842
  const coreOrgs = chrome?.orgs ?? [];
4834
4843
  const activeCoreOrg = coreOrgs.find((o) => o.id === chrome?.orgId) ?? null;
4835
4844
  const navDisplay = chrome?.navDisplay ?? "both";
4836
- const navMode = appMode ?? "relationship";
4845
+ const navMode = appMode ?? (app !== CORE_APP && isStandardNavMode(mode.id) ? mode.id : "relationship");
4837
4846
  const activeOrgId = chrome?.orgId ?? null;
4838
4847
  const derivedNavItems = react.useMemo(
4839
4848
  () => standardPolitogyNav({
@@ -4876,7 +4885,7 @@ function FrameBody({
4876
4885
  {
4877
4886
  visible: bootVisible,
4878
4887
  bootProgress,
4879
- syntheticReady: (appReady ?? chromeResolved) && !chrome?.orgPending,
4888
+ syntheticReady: (appReady ?? chromeResolved) && !chrome?.orgPending && (appMode !== void 0 || mode.resolved !== false),
4880
4889
  onExitComplete: () => setBootVisible(false)
4881
4890
  },
4882
4891
  bootGeneration
@@ -5596,6 +5605,7 @@ function useChromeMode(arg = {}) {
5596
5605
  const orgId = ctx?.orgId ?? null;
5597
5606
  const orgPending = ctx?.orgPending ?? false;
5598
5607
  const [entitled, setEntitled] = react.useState(null);
5608
+ const [settledOrgId, setSettledOrgId] = react.useState(null);
5599
5609
  const [selectedModeId, setSelectedModeId] = react.useState(null);
5600
5610
  react.useEffect(() => {
5601
5611
  try {
@@ -5610,6 +5620,7 @@ function useChromeMode(arg = {}) {
5610
5620
  const bootMe = me;
5611
5621
  if (bootMe && bootMe.account === orgId && bootMe.modes !== void 0) {
5612
5622
  setEntitled(bootMe.modes ?? null);
5623
+ setSettledOrgId(orgId);
5613
5624
  return;
5614
5625
  }
5615
5626
  let cancelled = false;
@@ -5617,7 +5628,9 @@ function useChromeMode(arg = {}) {
5617
5628
  if (cancelled) return;
5618
5629
  const modes = data?.modes ?? null;
5619
5630
  setEntitled(modes);
5631
+ setSettledOrgId(orgId);
5620
5632
  }).catch(() => {
5633
+ if (!cancelled) setSettledOrgId(orgId);
5621
5634
  });
5622
5635
  return () => {
5623
5636
  cancelled = true;
@@ -5631,6 +5644,9 @@ function useChromeMode(arg = {}) {
5631
5644
  id: current.id,
5632
5645
  label: current.label,
5633
5646
  icon: current.icon,
5647
+ // A registry pin is final at once; otherwise the mode is final when the
5648
+ // active org's Modes are in (or nothing can be read: no chrome or no org).
5649
+ resolved: Boolean(pinned) || !dedupedFetch || !orgId || settledOrgId === orgId && !orgPending,
5634
5650
  // Left-Navigation §7 / Reference/UI-Platform: always the row of three icon
5635
5651
  // buttons. A mode the organization does not hold is shown dimmed and not
5636
5652
  // selectable, never hidden. Before entitlements land only the current mode