@the-portland-company/shell 0.73.2 → 0.75.1

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,31 @@
1
+ ## 0.75.1
2
+
3
+ Three visual-regression fixes found by cross-app comparison (every app diverged from the design the same three ways):
4
+
5
+ - **Font tokens.** `--font-sans`, `--font-mono` and `--font-plex-mono` were referenced across `loading-screen.tsx` and `native/index.tsx` (and `theme/index.ts` read `--type-mono-family`) but `chrome.css` never emitted any of them, so those surfaces silently fell back to a hardcoded system stack instead of the Brand `--font-family` token — the actual source of the Inter/Acumin mismatch. Every sans surface now consumes `--font-family` (the one token the Brand API emits); every mono surface now consumes one shared, shell-emitted `--font-mono` (also added to `scripts/gen-tokens.mjs`).
6
+ - **Card tokens.** `--color-panel` aliased `--color-surface` (`--surface-1`, #F0F4F7 — the PAGE background), so every card rendered page-tinted instead of the Brand Card white. `--color-border` aliased `--line` / `--sidebar-border`, a token scoped to the always-dark left rail, leaking a dark-rail-intended border onto light content. Fixed in `chrome.css` (and `gen-tokens.mjs`): `--color-panel` is now `--color-layer-3` (white); `--color-border` is now `--color-g3` (#D7DADF, the neutral Brand border step).
7
+ - **Mode eyebrow + tab icons.** `PageHeader` had no `mode`/`eyebrow` prop at all, so the 11px uppercase mode eyebrow (Reference/UI-Platform → "Page header") never had anywhere to render; it now renders by default whenever `mode` is passed (`showEyebrow`/`eyebrow` to override). `NavTab.icon` was marked `@deprecated` and explicitly not rendered as of 0.69.0 — reversed by the 2026-10-06 Tabs decision ("Icon + label. Each tab is a Lucide icon") — `NavTabStrip` now renders the icon again, 15px (13px on `size="sm"`), 1.75 stroke, 6px left of the label, following the label's color via `currentColor`.
8
+
9
+ ## 0.75.0
10
+
11
+ Built on top of 0.74.0 (`claude/retire-nav`). Two fixes:
12
+
13
+ - **Regression fix**: restored the `Polls` row to `standardPolitogyNav`'s default Collect section. Commit 03c4dc2 (retiring `app.politogyvrm.com`) dropped it entirely instead of repointing it — Polls (`polls.politogyvrm.com`) is a standalone app, independent of the retired core app, and belongs in the default tree. Apps that don't want it pass `hiddenItems: ['Polls']`.
14
+ - **Cross-app nav prerender**: `NavAppShell` now renders a `<script type="speculationrules">` (via the new `SpeculationRulesScript`, built on `buildNavSpeculationRules`/`collectSiblingAppUrls` in `src/speculation/emitRules.ts`) for every sibling `*.politogyvrm.com` link in the consumer's own sidebar tree, `eagerness: "moderate"` — the browser prerenders on hover/pointerdown of a matching link, no custom JS trigger needed. Sign-out and other destructive rows (by label or `/signout`-style path) are excluded. Browsers without Speculation Rules support fall back, once idle, to `<link rel="prefetch">` tags for the same URLs. Because this is wired into `NavAppShell` itself (used by both `PolitogyAppFrame` and direct `NavAppShell` consumers), every consumer gets it with no per-app opt-in.
15
+ - **Each sibling app must send `Supports-Loading-Mode: credentialed-prerender`** on its responses for cross-origin prerender to activate while the user is authenticated — without it the browser silently falls back to a normal navigation (other agents are adding this header per-app).
16
+ - `buildSpeculationRules`/`AppRegistryEntry` (the old, unused registry-based API) is kept for back-compat and now documented `@deprecated` in favor of `buildNavSpeculationRules`.
17
+
18
+ ## 0.74.0
19
+
20
+ Repoint `standardPolitogyNav` off `app.politogyvrm.com` (Reference/UI-Platform → "Retirement of app.politogyvrm.com", DECIDED 2026-10-07; District data target per v44).
21
+ - Dashboard, Battleground, District data → `dashboard.politogyvrm.com`.
22
+ - Organization settings (incl. the depth-3 Subscription & Billing branch) → `settings.politogyvrm.com`.
23
+ - My profile → `auth.politogyvrm.com` (Politogy ID account app).
24
+ - Calls to action → the standalone CTAs app (`ctas.politogyvrm.com`), replacing the legacy in-core feature.
25
+ - Removed the V1 Email Broadcast legacy child row (VRM core's email surface is retired).
26
+ - `coreBaseUrl` is now optional and unused — every row resolves against its V2 host directly. Kept on `StandardNavOptions` so existing callers compile unchanged.
27
+ - Added `PolitogyAppKey` registry entries `dashboard` (`dashboard.politogyvrm.com`) and `settings` (`settings.politogyvrm.com`) so those new V2 apps can drop their own hardcoded chrome config.
28
+
1
29
  ## 0.72.0
2
30
 
3
31
  Left-Navigation §5, §10 and §12.
@@ -187,6 +187,91 @@ function writeRailPreference(pref) {
187
187
  }
188
188
  }
189
189
 
190
+ // src/speculation/emitRules.ts
191
+ var DESTRUCTIVE_LABEL_PATTERNS = ["sign out", "signout", "log out", "logout", "delete"];
192
+ var DESTRUCTIVE_PATH_PATTERNS = ["/signout", "/sign-out", "/logout", "/log-out"];
193
+ function isDestructive(item) {
194
+ const label = item.label.toLowerCase();
195
+ if (DESTRUCTIVE_LABEL_PATTERNS.some((p) => label.includes(p))) return true;
196
+ const href = (item.href ?? "").toLowerCase();
197
+ return DESTRUCTIVE_PATH_PATTERNS.some((p) => href.includes(p));
198
+ }
199
+ function collectSiblingAppUrls(items, currentOrigin) {
200
+ const urls = /* @__PURE__ */ new Set();
201
+ const walk = (list) => {
202
+ for (const item of list) {
203
+ if (item.kind === "section") continue;
204
+ if (item.href && !isDestructive(item)) {
205
+ try {
206
+ const url = new URL(item.href, currentOrigin);
207
+ if (url.origin !== currentOrigin && /^https?:$/.test(url.protocol)) {
208
+ urls.add(url.href);
209
+ }
210
+ } catch {
211
+ }
212
+ }
213
+ if (item.children?.length) walk(item.children);
214
+ }
215
+ };
216
+ walk(items);
217
+ return Array.from(urls);
218
+ }
219
+ function buildNavSpeculationRules(items, currentOrigin = typeof window !== "undefined" ? window.location.origin : "") {
220
+ const urls = collectSiblingAppUrls(items, currentOrigin);
221
+ if (urls.length === 0) return null;
222
+ return {
223
+ prerender: [{ source: "list", urls, eagerness: "moderate" }]
224
+ };
225
+ }
226
+
227
+ // src/speculation/SpeculationRulesScript.tsx
228
+ var SCRIPT_ID = "politogy-speculation-rules";
229
+ var PREFETCH_ATTR = "data-politogy-speculation-prefetch";
230
+ function SpeculationRulesScript({ items }) {
231
+ const origin = typeof window !== "undefined" ? window.location.origin : "";
232
+ const rules = react.useMemo(() => buildNavSpeculationRules(items, origin), [items, origin]);
233
+ const json = rules ? JSON.stringify(rules) : null;
234
+ react.useEffect(() => {
235
+ if (!json || typeof document === "undefined") return;
236
+ let el = document.getElementById(SCRIPT_ID);
237
+ if (!el) {
238
+ el = document.createElement("script");
239
+ el.id = SCRIPT_ID;
240
+ el.type = "speculationrules";
241
+ document.head.appendChild(el);
242
+ }
243
+ el.textContent = json;
244
+ return () => {
245
+ };
246
+ }, [json]);
247
+ react.useEffect(() => {
248
+ if (typeof document === "undefined" || typeof window === "undefined") return;
249
+ const supportsSpecRules = typeof HTMLScriptElement !== "undefined" && typeof HTMLScriptElement.supports === "function" && HTMLScriptElement.supports(
250
+ "speculationrules"
251
+ );
252
+ if (supportsSpecRules) return;
253
+ const urls = collectSiblingAppUrls(items, origin);
254
+ if (urls.length === 0) return;
255
+ const schedule = typeof window.requestIdleCallback === "function" ? window.requestIdleCallback : (cb) => setTimeout(cb, 2e3);
256
+ const added = [];
257
+ schedule(() => {
258
+ for (const url of urls) {
259
+ if (document.querySelector(`link[${PREFETCH_ATTR}][href="${url}"]`)) continue;
260
+ const link = document.createElement("link");
261
+ link.rel = "prefetch";
262
+ link.href = url;
263
+ link.setAttribute(PREFETCH_ATTR, "");
264
+ document.head.appendChild(link);
265
+ added.push(link);
266
+ }
267
+ });
268
+ return () => {
269
+ added.forEach((l) => l.remove());
270
+ };
271
+ }, [items, origin]);
272
+ return null;
273
+ }
274
+
190
275
  // src/theme-preference.ts
191
276
  var THEME_STORAGE_KEY = "politogy_theme";
192
277
  var THEME_COOKIE = "politogy_theme";
@@ -1966,6 +2051,9 @@ var PHONEBANK_ORIGIN = "https://phonebank.politogyvrm.com/";
1966
2051
  var CANVASS_ORIGIN = "https://canvass.politogyvrm.com/";
1967
2052
  var SIGNS_ORIGIN = "https://signs.politogyvrm.com/";
1968
2053
  var POLLS_ORIGIN = "https://polls.politogyvrm.com/";
2054
+ var SETTINGS_ORIGIN = "https://settings.politogyvrm.com/";
2055
+ var DASHBOARD_ORIGIN = "https://dashboard.politogyvrm.com/";
2056
+ var AUTH_ORIGIN = "https://auth.politogyvrm.com/";
1969
2057
  var PETITION_MODE_ORIGIN = "https://petition-mode.politogyvrm.com/";
1970
2058
  function applyHiddenItems(items, hidden) {
1971
2059
  if (!hidden || hidden.length === 0) return items;
@@ -1997,29 +2085,23 @@ var APP_EMAIL_CHILDREN = [
1997
2085
  { label: "Statistics", icon: "barChart", href: `${EMAIL_APP}/statistics` },
1998
2086
  { label: "Deliverability", icon: "activity", href: `${EMAIL_APP}/deliverability` }
1999
2087
  ];
2000
- var EMAIL_V1_LEGACY = { label: "Email V1 (legacy)", icon: "clock", path: "/relationship/email/dashboard" };
2001
- var CTA_CHILDREN = [
2002
- { label: "Dashboard", icon: "grid", path: "/relationship/cta-database/dashboard" },
2003
- { label: "Create & Edit", icon: "edit", path: "/relationship/cta-creator/create-and-edit" },
2004
- { label: "Submissions", icon: "fileText", path: "/relationship/cta-submissions/submissions" }
2005
- ];
2006
2088
  var ORG_SETTINGS_CHILDREN = [
2007
- { label: "General", icon: "settings", path: "/organization-settings" },
2089
+ { label: "General", icon: "settings", path: "organization-settings" },
2008
2090
  {
2009
2091
  label: "Subscription & Billing",
2010
2092
  icon: "creditCard",
2011
- path: "/organization-settings/subscription-billing",
2093
+ path: "organization-settings/subscription-billing",
2012
2094
  children: [
2013
- { label: "Current Plan", icon: "fileText", path: "/organization-settings/subscription-billing/current-plan" },
2014
- { label: "SMS, MMS & Email Usage", icon: "activity", path: "/organization-settings/subscription-billing/text-email" },
2015
- { label: "Wallet", icon: "dollarSign", path: "/organization-settings/subscription-billing/wallet" },
2016
- { label: "10DLC", icon: "messageSquare", path: "/organization-settings/subscription-billing/10dlc" },
2017
- { label: "Billing History", icon: "clock", path: "/organization-settings/subscription-billing/billing-history" }
2095
+ { label: "Current Plan", icon: "fileText", path: "organization-settings/subscription-billing/current-plan" },
2096
+ { label: "SMS, MMS & Email Usage", icon: "activity", path: "organization-settings/subscription-billing/text-email" },
2097
+ { label: "Wallet", icon: "dollarSign", path: "organization-settings/subscription-billing/wallet" },
2098
+ { label: "10DLC", icon: "messageSquare", path: "organization-settings/subscription-billing/10dlc" },
2099
+ { label: "Billing History", icon: "clock", path: "organization-settings/subscription-billing/billing-history" }
2018
2100
  ]
2019
2101
  },
2020
- { label: "Email", icon: "mail", path: "/organization-settings/email-settings" },
2021
- { label: "Users", icon: "users", path: "/organization-settings/users" },
2022
- { label: "Shared Links", icon: "share", path: "/organization-settings/shared-links" }
2102
+ { label: "Email", icon: "mail", path: "organization-settings/email-settings" },
2103
+ { label: "Users", icon: "users", path: "organization-settings/users" },
2104
+ { label: "Shared Links", icon: "share", path: "organization-settings/shared-links" }
2023
2105
  ];
2024
2106
  var APP_FORMS_CHILDREN = [
2025
2107
  { label: "Forms", icon: "list", href: "https://forms.politogyvrm.com/?tab=forms" },
@@ -2035,6 +2117,10 @@ var APP_POLLS_CHILDREN = [
2035
2117
  { label: "Command Center", icon: "grid", href: "https://polls.politogyvrm.com/" },
2036
2118
  { label: "New Poll", icon: "edit", href: "https://polls.politogyvrm.com/polls/new" }
2037
2119
  ];
2120
+ var APP_CTAS_CHILDREN = [
2121
+ { label: "CTAs", icon: "list", href: "https://ctas.politogyvrm.com/" },
2122
+ { label: "Canvas", icon: "edit", href: "https://ctas.politogyvrm.com/?tab=canvas" }
2123
+ ];
2038
2124
  var APP_A2P_CHILDREN = [
2039
2125
  { label: "Dashboard", icon: "grid", path: "dashboard" },
2040
2126
  { label: "Compose", icon: "edit", path: "compose" },
@@ -2062,35 +2148,39 @@ var APP_SIGNS_CHILDREN = [
2062
2148
  { label: "Reports", icon: "barChart", path: "reports" }
2063
2149
  ];
2064
2150
  function standardPolitogyNav(opts) {
2065
- const base = opts.coreBaseUrl.replace(/\/+$/, "");
2066
- const abs = (path) => `${base}${path.startsWith("/") ? path : `/${path}`}`;
2067
2151
  const icon = (key) => opts.icons?.[key] ?? standardNavIcons[key];
2068
2152
  const orgParam = opts.activeOrgId ? `org=${opts.activeOrgId}` : "";
2069
2153
  const withOrg = (url) => orgParam ? `${url}${url.includes("?") ? "&" : "?"}${orgParam}` : url;
2070
- const leaves = (children) => children.map((c) => ({ label: c.label, icon: icon(c.icon), href: withOrg(abs(c.path)) }));
2071
- const branches = (children) => children.map((c) => ({
2072
- label: c.label,
2073
- icon: icon(c.icon),
2074
- href: withOrg(abs(c.path)),
2075
- children: c.children ? leaves(c.children) : void 0
2076
- }));
2154
+ const leaves = (origin, children) => appLeaves(origin, children);
2155
+ const branches = (origin, children) => {
2156
+ const o = origin.replace(/\/+$/, "");
2157
+ return children.map((c) => ({
2158
+ label: c.label,
2159
+ icon: icon(c.icon),
2160
+ href: withOrg(`${o}/${c.path}`),
2161
+ children: c.children ? leaves(origin, c.children) : void 0
2162
+ }));
2163
+ };
2077
2164
  const appLeaves = (origin, children, modeParam) => {
2078
- const base2 = origin.replace(/\/+$/, "");
2165
+ const base = origin.replace(/\/+$/, "");
2079
2166
  return children.map((c) => {
2080
- const url = c.path ? `${base2}/${c.path}` : `${base2}/`;
2167
+ const url = c.path ? `${base}/${c.path}` : `${base}/`;
2081
2168
  const withMode = modeParam ? `${url}${url.includes("?") ? "&" : "?"}mode=${modeParam}` : url;
2082
2169
  return { label: c.label, icon: icon(c.icon), href: withOrg(withMode) };
2083
2170
  });
2084
2171
  };
2085
2172
  const accountRows = () => [
2086
2173
  { kind: "section", label: "Account" },
2087
- // My Profile is global — NOT org-suffixed (main-app ShellChromeBridge).
2088
- { label: "My profile", icon: icon("circleUser"), href: withOrg(abs("/my-profile")) },
2174
+ // My Profile moved to the Politogy ID account app (auth.politogyvrm.com)
2175
+ // with the retirement of app.politogyvrm.com.
2176
+ { label: "My profile", icon: icon("circleUser"), href: withOrg(`${AUTH_ORIGIN}my-profile`) },
2089
2177
  {
2090
2178
  label: "Organization settings",
2091
2179
  icon: icon("settings"),
2092
- href: withOrg(abs("/organization-settings")),
2093
- children: branches(ORG_SETTINGS_CHILDREN)
2180
+ // Organization settings moved to the standalone Settings app
2181
+ // (settings.politogyvrm.com, Specs/Organization-Settings v12).
2182
+ href: withOrg(`${SETTINGS_ORIGIN}organization-settings`),
2183
+ children: branches(SETTINGS_ORIGIN, ORG_SETTINGS_CHILDREN)
2094
2184
  }
2095
2185
  ];
2096
2186
  const buildRelationship = () => {
@@ -2112,16 +2202,15 @@ function standardPolitogyNav(opts) {
2112
2202
  };
2113
2203
  return [
2114
2204
  { kind: "section", label: "Relationship" },
2115
- { label: "Dashboard", icon: icon("layoutDashboard"), href: withOrg(abs("/relationship/dashboard")) },
2205
+ // Dashboard moved to the standalone Dashboard app (dashboard.politogyvrm.com,
2206
+ // Specs/Relationship-Dashboard) with the retirement of app.politogyvrm.com.
2207
+ { label: "Dashboard", icon: icon("layoutDashboard"), href: withOrg(DASHBOARD_ORIGIN) },
2116
2208
  contacts,
2117
2209
  {
2118
2210
  label: "Email broadcast",
2119
2211
  icon: icon("mail"),
2120
2212
  href: withOrg(`${EMAIL_APP}/dashboard`),
2121
- children: [
2122
- ...APP_EMAIL_CHILDREN.map((c) => ({ label: c.label, icon: icon(c.icon), href: withOrg(c.href) })),
2123
- ...leaves([EMAIL_V1_LEGACY])
2124
- ]
2213
+ children: APP_EMAIL_CHILDREN.map((c) => ({ label: c.label, icon: icon(c.icon), href: withOrg(c.href) }))
2125
2214
  },
2126
2215
  {
2127
2216
  // SMS/MMS Blast is the a2p app entered in RELATIONSHIP mode (a2p is
@@ -2134,8 +2223,14 @@ function standardPolitogyNav(opts) {
2134
2223
  children: appLeaves(A2P_ORIGIN, APP_A2P_CHILDREN, "relationship")
2135
2224
  },
2136
2225
  { kind: "section", label: "Collect" },
2137
- // Carries no top-level href in VRM core (parent row only toggles).
2138
- { label: "Calls to action", icon: icon("mousePointerClick"), children: leaves(CTA_CHILDREN) },
2226
+ // Calls to action moved to the standalone CTAs app (ctas./runtime.
2227
+ // politogyvrm.com, Specs/CTAs) with the retirement of app.politogyvrm.com.
2228
+ {
2229
+ label: "Calls to action",
2230
+ icon: icon("mousePointerClick"),
2231
+ href: withOrg("https://ctas.politogyvrm.com/"),
2232
+ children: APP_CTAS_CHILDREN.map((c) => ({ label: c.label, icon: icon(c.icon), href: withOrg(c.href) }))
2233
+ },
2139
2234
  {
2140
2235
  label: "Forms",
2141
2236
  icon: icon("fileText"),
@@ -2148,6 +2243,16 @@ function standardPolitogyNav(opts) {
2148
2243
  href: withOrg("https://surveys.politogyvrm.com/surveys"),
2149
2244
  children: APP_SURVEYS_CHILDREN.map((c) => ({ label: c.label, icon: icon(c.icon), href: withOrg(c.href) }))
2150
2245
  },
2246
+ // Polls (polls.politogyvrm.com) — kept in the default Collect section;
2247
+ // apps that want it hidden pass `hiddenItems: ['Polls']`. Regression:
2248
+ // dropped entirely in 03c4dc2 when app.politogyvrm.com retired; restored
2249
+ // here (this row is independent of the retired app).
2250
+ {
2251
+ label: "Polls",
2252
+ icon: icon("chartNoAxes"),
2253
+ href: withOrg(POLLS_ORIGIN),
2254
+ children: APP_POLLS_CHILDREN.map((c) => ({ label: c.label, icon: icon(c.icon), href: withOrg(c.href) }))
2255
+ },
2151
2256
  ...accountRows()
2152
2257
  ];
2153
2258
  };
@@ -2161,8 +2266,12 @@ function standardPolitogyNav(opts) {
2161
2266
  // Battleground is the Campaign landing page; there is no Campaign
2162
2267
  // Dashboard row, and Voter Contact and Tribes have no row.
2163
2268
  { kind: "section", label: "Race data" },
2164
- { label: "Battleground", icon: icon("crosshair"), href: withOrg(abs("/campaign/battleground")) },
2165
- { label: "District data", icon: icon("landmark"), href: withOrg(abs("/campaign/districts")) },
2269
+ // Battleground moved to the Dashboard app (dashboard.politogyvrm.com)
2270
+ // until a dedicated Battleground host is decided (UI-Platform route table).
2271
+ { label: "Battleground", icon: icon("crosshair"), href: withOrg(`${DASHBOARD_ORIGIN}battleground`) },
2272
+ // District data moved to the Dashboard app (dashboard.politogyvrm.com)
2273
+ // per Reference/UI-Platform v44.
2274
+ { label: "District data", icon: icon("landmark"), href: withOrg(`${DASHBOARD_ORIGIN}districts`) },
2166
2275
  { kind: "section", label: "Research" },
2167
2276
  {
2168
2277
  label: "Polling",
@@ -2787,7 +2896,7 @@ function NavSidebar({
2787
2896
  "div",
2788
2897
  {
2789
2898
  "data-shell-meta": true,
2790
- className: `mt-1.5 flex flex-col items-center gap-0.5 font-[family-name:var(--font-plex-mono)] text-[11px] text-[color:var(--sidebar-meta-text,rgba(255,255,255,0.4))] ${rc("max-md:hidden")}`,
2899
+ className: `mt-1.5 flex flex-col items-center gap-0.5 font-[family-name:var(--font-mono,ui-monospace,monospace)] text-[11px] text-[color:var(--sidebar-meta-text,rgba(255,255,255,0.4))] ${rc("max-md:hidden")}`,
2791
2900
  children: [
2792
2901
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-1.5", children: [
2793
2902
  footer.envLabel ? shortEnvLabel(footer.envLabel) : null,
@@ -2937,7 +3046,7 @@ function NavActiveRail() {
2937
3046
  function NavRowBadge({ value }) {
2938
3047
  const rc = useRailCollapse();
2939
3048
  if (value === void 0 || value === null || value === "") return null;
2940
- return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `ml-auto shrink-0 font-[family-name:var(--font-plex-mono,ui-monospace,monospace)] text-[11px] font-semibold leading-none text-[color:var(--sidebar-caption,var(--sidebar-text-muted,var(--muted)))] ${rc("max-md:hidden")}`, children: value });
3049
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `ml-auto shrink-0 font-[family-name:var(--font-mono,ui-monospace,monospace)] text-[11px] font-semibold leading-none text-[color:var(--sidebar-caption,var(--sidebar-text-muted,var(--muted)))] ${rc("max-md:hidden")}`, children: value });
2941
3050
  }
2942
3051
  function NavSectionRow({
2943
3052
  label,
@@ -3051,7 +3160,7 @@ function OrgIdCopyButton({ id }) {
3051
3160
  setTimeout(() => setCopied(false), 1500);
3052
3161
  },
3053
3162
  title: "Copy organization id",
3054
- className: "-ml-1 rounded px-1 text-left font-[family-name:var(--font-plex-mono,ui-monospace,monospace)] text-[12px] leading-[1.2] text-[color:var(--sidebar-caption,var(--sidebar-text-muted,var(--muted)))] hover:bg-[color:var(--sidebar-row-hover-bg,var(--panel))]",
3163
+ className: "-ml-1 rounded px-1 text-left font-[family-name:var(--font-mono,ui-monospace,monospace)] text-[12px] leading-[1.2] text-[color:var(--sidebar-caption,var(--sidebar-text-muted,var(--muted)))] hover:bg-[color:var(--sidebar-row-hover-bg,var(--panel))]",
3055
3164
  children: copied ? "Copied" : `\u2026${id.slice(-8)}`
3056
3165
  }
3057
3166
  );
@@ -3111,7 +3220,7 @@ function NavTopBar({
3111
3220
  "kbd",
3112
3221
  {
3113
3222
  "aria-hidden": true,
3114
- className: "pointer-events-none absolute right-[10px] hidden md:block rounded-[3px] border border-[color:var(--line)] px-[5px] py-px font-[family-name:var(--font-plex-mono,ui-monospace,monospace)] text-[12px] leading-[1.2] text-[color:var(--muted)]",
3223
+ className: "pointer-events-none absolute right-[10px] hidden md:block rounded-[3px] border border-[color:var(--line)] px-[5px] py-px font-[family-name:var(--font-mono,ui-monospace,monospace)] text-[12px] leading-[1.2] text-[color:var(--muted)]",
3115
3224
  children: "/"
3116
3225
  }
3117
3226
  )
@@ -3655,6 +3764,7 @@ function NavAppShell({
3655
3764
  "data-politogy-mode": modeId ?? modeIdFromLabel(sidebar.mode?.label),
3656
3765
  className: "min-h-screen bg-[color:var(--background)] text-[color:var(--foreground)]",
3657
3766
  children: [
3767
+ /* @__PURE__ */ jsxRuntime.jsx(SpeculationRulesScript, { items: sidebar.nav.items }),
3658
3768
  /* @__PURE__ */ jsxRuntime.jsx(
3659
3769
  NavSidebar,
3660
3770
  {
@@ -4133,7 +4243,7 @@ function LoadingScreen({
4133
4243
  display: "flex",
4134
4244
  alignItems: "center",
4135
4245
  gap: "0.6rem",
4136
- fontFamily: "var(--font-sans, ui-sans-serif, system-ui, sans-serif)"
4246
+ fontFamily: "var(--font-family, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif)"
4137
4247
  },
4138
4248
  children: [
4139
4249
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4227,7 +4337,7 @@ function LoadingScreen({
4227
4337
  border: "none",
4228
4338
  background: "var(--color-race-red, #dc2626)",
4229
4339
  color: "#fff",
4230
- fontFamily: "var(--font-sans, ui-sans-serif, system-ui, sans-serif)",
4340
+ fontFamily: "var(--font-family, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif)",
4231
4341
  fontSize: "0.875rem",
4232
4342
  fontWeight: 600,
4233
4343
  cursor: "pointer"
@@ -4246,7 +4356,7 @@ function LoadingScreen({
4246
4356
  border: "none",
4247
4357
  background: "transparent",
4248
4358
  color: "rgba(255,255,255,0.38)",
4249
- fontFamily: "var(--font-sans, ui-sans-serif, system-ui, sans-serif)",
4359
+ fontFamily: "var(--font-family, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif)",
4250
4360
  fontSize: "12.5px",
4251
4361
  textDecoration: "underline",
4252
4362
  cursor: "pointer"
@@ -4270,7 +4380,7 @@ function LoadingScreen({
4270
4380
  alignItems: "center",
4271
4381
  justifyContent: "space-between",
4272
4382
  padding: "0 32px 30px",
4273
- fontFamily: "var(--font-sans, ui-sans-serif, system-ui, sans-serif)",
4383
+ fontFamily: "var(--font-family, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif)",
4274
4384
  fontSize: "12.5px",
4275
4385
  color: "rgba(255,255,255,0.38)"
4276
4386
  },
@@ -4345,6 +4455,13 @@ var POLITOGY_APPS = {
4345
4455
  // Specs/Left-Navigation §9.2: Polling is a Campaign row (Research).
4346
4456
  polls: { title: "Polls", origin: "https://polls.politogyvrm.com", storagePrefix: "polls", mode: "campaign" },
4347
4457
  ctas: { title: "CTAs", origin: "https://ctas.politogyvrm.com", storagePrefix: "ctas" },
4458
+ // V2 apps that replace retired app.politogyvrm.com pages (Reference/
4459
+ // UI-Platform v42 "Retirement of app.politogyvrm.com"). Dashboard hosts the
4460
+ // Relationship Dashboard + Campaign Battleground/District data/Campaigns
4461
+ // (Specs/Relationship-Dashboard); Settings hosts org settings & billing
4462
+ // (Specs/Organization-Settings).
4463
+ dashboard: { title: "Dashboard", origin: "https://dashboard.politogyvrm.com", storagePrefix: "dashboard" },
4464
+ settings: { title: "Settings", origin: "https://settings.politogyvrm.com", storagePrefix: "settings" },
4348
4465
  petitions: {
4349
4466
  title: "Petitions",
4350
4467
  origin: "https://petition-mode.politogyvrm.com",