@the-portland-company/shell 0.67.0 → 0.68.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.
@@ -147,6 +147,14 @@ declare const POLITOGY_APPS: Record<PolitogyAppKey, {
147
147
  * product shows the platform's Beta notice is a platform decision.
148
148
  */
149
149
  beta?: boolean;
150
+ /**
151
+ * false: the chrome does NOT draw its own page-title line (the 56px
152
+ * `<h1>` above `main`); the app's page body owns its title. Specs/Contacts
153
+ * design: the page opens on its own "RELATIONSHIP MODE" eyebrow + H1, so
154
+ * a chrome-owned "Contacts" line above it was a second heading not in the
155
+ * design. Registry-level (platform decision), like `beta`.
156
+ */
157
+ pageTitle?: false;
150
158
  }>;
151
159
  type PolitogyAppSession = {
152
160
  /** Stable IdP subject (`sub`). */
@@ -163,14 +171,6 @@ type PolitogyAppSession = {
163
171
  */
164
172
  userName?: string | null;
165
173
  };
166
- /**
167
- * Optional DATA plumbing for apps whose session/auth differs — never styling.
168
- *
169
- * Everything here is about WHERE data comes from (which fetcher, which org
170
- * state, which routes an app owns), not what the chrome looks like. Adding a
171
- * visual option to this type would re-open the drift this component exists to
172
- * close.
173
- */
174
174
  type PolitogyFrameData = {
175
175
  /** Chrome-API fetcher override for apps that authenticate client-side. */
176
176
  fetchJson?: NavChromeFetcher;
@@ -178,11 +178,36 @@ type PolitogyFrameData = {
178
178
  devNotes?: ChromeDevNotesConfig;
179
179
  /** Controlled org state for apps that own active-org + switching. */
180
180
  org?: ControlledOrgState;
181
- /** App-owned children for a standard nav entry this app owns (Contacts). */
181
+ /**
182
+ * App-owned children for a standard nav entry this app owns (Contacts).
183
+ * `count` is the live contact total, pre-formatted ("31.9K"), drawn
184
+ * right-aligned on the Contacts row (Specs/Contacts design rail).
185
+ */
182
186
  contactsItem?: {
183
187
  href?: string;
184
188
  children?: NavItem[];
189
+ count?: string | number | null;
185
190
  };
191
+ /**
192
+ * Top-level standard-nav rows to omit, by label (e.g. `['Polls', 'CTAs']`).
193
+ * The Specs/Contacts design rail shows exactly Dashboard · Contacts · Email
194
+ * broadcast · SMS/MMS blast / Calls to action · Forms · Surveys / My profile
195
+ * · Organization settings. Default: nothing hidden.
196
+ */
197
+ hiddenNavItems?: string[];
198
+ /**
199
+ * Universal-search placeholder. Defaults per app (Contacts: "Search
200
+ * contacts, segments, forms"), else "Search".
201
+ */
202
+ searchPlaceholder?: string;
203
+ /**
204
+ * true: render the rail as flat rows — every standard entry's `children`
205
+ * are stripped before the sidebar draws, so no row carries an accordion
206
+ * chevron (Specs/Contacts design rail: nine plain rows). Routing data is
207
+ * untouched: active-row matching still uses the unstripped tree, exactly
208
+ * as the 'tabs only' preference does. Default: children kept.
209
+ */
210
+ flatNav?: boolean;
186
211
  /** Petitions app: nest its own routes under the Petition-mode entry. */
187
212
  petitionsItem?: {
188
213
  href?: string;
@@ -396,8 +421,11 @@ declare function PolitogyAppChrome({ storagePrefix, proxyBasePath, fetchJson: fe
396
421
  identity: ChromeIdentity;
397
422
  children: ReactNode;
398
423
  }): react_jsx_runtime.JSX.Element;
399
- /** The standard top-bar action cluster. The Alerts bell renders LAST so it
400
- * sits farthest right, immediately left of the My Account badge. */
424
+ /** The standard top-bar action cluster — Reference/UI-Platform v30 "Top bar":
425
+ * Search | Dev notes | Credits | Notifications | Help (farthest right). The
426
+ * Navigation Display switcher moved into the account menu (0.68.0); the Spec
427
+ * overlay slot still renders between Credits and Notifications when the
428
+ * signed-in user is allowed it (it draws nothing otherwise). */
401
429
  declare function ChromeTopBarActions(): react_jsx_runtime.JSX.Element | null;
402
430
  /** True when any core-org membership carries a super-admin tier. */
403
431
  declare function useIsSuperAdmin(): boolean;
@@ -147,6 +147,14 @@ declare const POLITOGY_APPS: Record<PolitogyAppKey, {
147
147
  * product shows the platform's Beta notice is a platform decision.
148
148
  */
149
149
  beta?: boolean;
150
+ /**
151
+ * false: the chrome does NOT draw its own page-title line (the 56px
152
+ * `<h1>` above `main`); the app's page body owns its title. Specs/Contacts
153
+ * design: the page opens on its own "RELATIONSHIP MODE" eyebrow + H1, so
154
+ * a chrome-owned "Contacts" line above it was a second heading not in the
155
+ * design. Registry-level (platform decision), like `beta`.
156
+ */
157
+ pageTitle?: false;
150
158
  }>;
151
159
  type PolitogyAppSession = {
152
160
  /** Stable IdP subject (`sub`). */
@@ -163,14 +171,6 @@ type PolitogyAppSession = {
163
171
  */
164
172
  userName?: string | null;
165
173
  };
166
- /**
167
- * Optional DATA plumbing for apps whose session/auth differs — never styling.
168
- *
169
- * Everything here is about WHERE data comes from (which fetcher, which org
170
- * state, which routes an app owns), not what the chrome looks like. Adding a
171
- * visual option to this type would re-open the drift this component exists to
172
- * close.
173
- */
174
174
  type PolitogyFrameData = {
175
175
  /** Chrome-API fetcher override for apps that authenticate client-side. */
176
176
  fetchJson?: NavChromeFetcher;
@@ -178,11 +178,36 @@ type PolitogyFrameData = {
178
178
  devNotes?: ChromeDevNotesConfig;
179
179
  /** Controlled org state for apps that own active-org + switching. */
180
180
  org?: ControlledOrgState;
181
- /** App-owned children for a standard nav entry this app owns (Contacts). */
181
+ /**
182
+ * App-owned children for a standard nav entry this app owns (Contacts).
183
+ * `count` is the live contact total, pre-formatted ("31.9K"), drawn
184
+ * right-aligned on the Contacts row (Specs/Contacts design rail).
185
+ */
182
186
  contactsItem?: {
183
187
  href?: string;
184
188
  children?: NavItem[];
189
+ count?: string | number | null;
185
190
  };
191
+ /**
192
+ * Top-level standard-nav rows to omit, by label (e.g. `['Polls', 'CTAs']`).
193
+ * The Specs/Contacts design rail shows exactly Dashboard · Contacts · Email
194
+ * broadcast · SMS/MMS blast / Calls to action · Forms · Surveys / My profile
195
+ * · Organization settings. Default: nothing hidden.
196
+ */
197
+ hiddenNavItems?: string[];
198
+ /**
199
+ * Universal-search placeholder. Defaults per app (Contacts: "Search
200
+ * contacts, segments, forms"), else "Search".
201
+ */
202
+ searchPlaceholder?: string;
203
+ /**
204
+ * true: render the rail as flat rows — every standard entry's `children`
205
+ * are stripped before the sidebar draws, so no row carries an accordion
206
+ * chevron (Specs/Contacts design rail: nine plain rows). Routing data is
207
+ * untouched: active-row matching still uses the unstripped tree, exactly
208
+ * as the 'tabs only' preference does. Default: children kept.
209
+ */
210
+ flatNav?: boolean;
186
211
  /** Petitions app: nest its own routes under the Petition-mode entry. */
187
212
  petitionsItem?: {
188
213
  href?: string;
@@ -396,8 +421,11 @@ declare function PolitogyAppChrome({ storagePrefix, proxyBasePath, fetchJson: fe
396
421
  identity: ChromeIdentity;
397
422
  children: ReactNode;
398
423
  }): react_jsx_runtime.JSX.Element;
399
- /** The standard top-bar action cluster. The Alerts bell renders LAST so it
400
- * sits farthest right, immediately left of the My Account badge. */
424
+ /** The standard top-bar action cluster — Reference/UI-Platform v30 "Top bar":
425
+ * Search | Dev notes | Credits | Notifications | Help (farthest right). The
426
+ * Navigation Display switcher moved into the account menu (0.68.0); the Spec
427
+ * overlay slot still renders between Credits and Notifications when the
428
+ * signed-in user is allowed it (it draws nothing otherwise). */
401
429
  declare function ChromeTopBarActions(): react_jsx_runtime.JSX.Element | null;
402
430
  /** True when any core-org membership carries a super-admin tier. */
403
431
  declare function useIsSuperAdmin(): boolean;
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { broadcastLogout, watchLogout } from './chunk-LL6LSJAH.js';
3
- import { clearBetaBannerDismissal, standardPolitogyNav, isNavItemActive, NavAppShell, NavChromeProvider, FuelAction, NavDisplayAction, TopBarWidgetSlot, SpecOverlayAction, SupportAction, NotificationsAction, useChromeMe, useThemePreference, useNavChromeFetcher, useChromeOrgs } from './chunk-T5ERHFNT.js';
4
- import './chunk-JHIFJZ73.js';
3
+ import { clearBetaBannerDismissal, standardPolitogyNav, isNavItemActive, NavAppShell, NavChromeProvider, TopBarWidgetSlot, FuelAction, SpecOverlayAction, NotificationsAction, SupportAction, useChromeMe, useThemePreference, useNavChromeFetcher, useChromeOrgs } from './chunk-ZKYREVCK.js';
4
+ import './chunk-NHBNM5P3.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';
7
7
  import { createContext, useMemo, useRef, useEffect, useState, useCallback, useContext } from 'react';
@@ -473,7 +473,12 @@ var MODE_BRAND_LOGOS = {
473
473
  var modeBrandLogo = (mode) => mode && MODE_BRAND_LOGOS[mode] || DEFAULT_BRAND_LOGO;
474
474
  var POLITOGY_APPS = {
475
475
  vrm: { title: "Politogy VRM", origin: "https://app.politogyvrm.com", storagePrefix: "vrm" },
476
- contacts: { title: "Contacts", origin: "https://contacts.politogyvrm.com", storagePrefix: "contacts" },
476
+ contacts: {
477
+ title: "Contacts",
478
+ origin: "https://contacts.politogyvrm.com",
479
+ storagePrefix: "contacts",
480
+ pageTitle: false
481
+ },
477
482
  forms: { title: "Forms", origin: "https://forms.politogyvrm.com", storagePrefix: "forms" },
478
483
  surveys: { title: "Surveys", origin: "https://surveys.politogyvrm.com", storagePrefix: "surveys" },
479
484
  polls: { title: "Polls", origin: "https://polls.politogyvrm.com", storagePrefix: "polls" },
@@ -514,6 +519,9 @@ var POLITOGY_APPS = {
514
519
  canvass: { title: "Field Canvassing", origin: "https://canvass.politogyvrm.com", storagePrefix: "canvass", mode: "campaign" },
515
520
  signs: { title: "Sign Mapping", origin: "https://signs.politogyvrm.com", storagePrefix: "signs", mode: "campaign" }
516
521
  };
522
+ var DEFAULT_SEARCH_PLACEHOLDER = {
523
+ contacts: "Search contacts, segments, forms"
524
+ };
517
525
  function PolitogyAppFrame({
518
526
  app,
519
527
  pathname,
@@ -672,10 +680,11 @@ function FrameBody({
672
680
  // under it. This is routing data, not chrome configuration.
673
681
  contactsItem: data?.contactsItem,
674
682
  petitionsItem: data?.petitionsItem,
683
+ hiddenItems: data?.hiddenNavItems,
675
684
  mode: navMode,
676
685
  activeOrgId
677
686
  }),
678
- [data?.contactsItem, data?.petitionsItem, navMode, activeOrgId]
687
+ [data?.contactsItem, data?.petitionsItem, data?.hiddenNavItems, navMode, activeOrgId]
679
688
  );
680
689
  const navItems = core?.nav ?? derivedNavItems;
681
690
  const navIsActive = useMemo(() => {
@@ -724,7 +733,7 @@ function FrameBody({
724
733
  // A flat rail's sub-menu IS the nav, so the 'tabs only' preference
725
734
  // (which strips children) would leave one childless parent row —
726
735
  // flat apps ignore the stripping.
727
- items: navDisplay === "tabs" && meta.navShape !== "flat" ? navItems.map(({ children: _sub, ...item }) => item) : navItems,
736
+ items: (navDisplay === "tabs" || data?.flatNav === true) && meta.navShape !== "flat" ? navItems.map(({ children: _sub, ...item }) => item) : navItems,
728
737
  isActive: navIsActive,
729
738
  flatten: meta.navShape === "flat"
730
739
  },
@@ -751,7 +760,7 @@ function FrameBody({
751
760
  topBar: {
752
761
  // Core's pages publish their own identity; every other app is one
753
762
  // product surface and takes its registry title.
754
- title: core?.page?.title ?? (grants?.includes("title") ? data?.frame?.title : void 0) ?? meta.title,
763
+ title: core?.page?.title ?? (grants?.includes("title") ? data?.frame?.title : void 0) ?? (meta.pageTitle === false ? void 0 : meta.title),
755
764
  breadcrumbs,
756
765
  user: {
757
766
  name: identity.userName,
@@ -767,7 +776,7 @@ function FrameBody({
767
776
  // search is the only thing on the left end of the bar. No app wires
768
777
  // search yet, so this stays decorative (`NavTopBarProps.search` — "an
769
778
  // app that has not wired search yet still gets the correct chrome").
770
- search: {},
779
+ search: { placeholder: data?.searchPlaceholder ?? DEFAULT_SEARCH_PLACEHOLDER[app] ?? "Search" },
771
780
  // "Build notes" — a quiet text button that starts the right-hand
772
781
  // group. The spec names the element and its position but not its
773
782
  // destination content; `onOpen` is left unset until that is defined.
@@ -1166,8 +1175,6 @@ function ChromeTopBarActions() {
1166
1175
  const state = useChromeState();
1167
1176
  if (!state) return null;
1168
1177
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1169
- /* @__PURE__ */ jsx(FuelAction, { orgId: state.orgId }),
1170
- /* @__PURE__ */ jsx(NavDisplayAction, { value: state.navDisplay, onChange: state.setNavDisplay }),
1171
1178
  /* @__PURE__ */ jsx(
1172
1179
  TopBarWidgetSlot,
1173
1180
  {
@@ -1176,9 +1183,10 @@ function ChromeTopBarActions() {
1176
1183
  children: /* @__PURE__ */ jsx(ChromeDevNotesMenu, {})
1177
1184
  }
1178
1185
  ),
1186
+ /* @__PURE__ */ jsx(FuelAction, { orgId: state.orgId }),
1179
1187
  /* @__PURE__ */ jsx(SpecOverlayAction, { orgId: state.orgId, orgName: state.orgs.find((o) => o.id === state.orgId)?.name ?? null }),
1180
- /* @__PURE__ */ jsx(SupportAction, { orgId: state.orgId }),
1181
- /* @__PURE__ */ jsx(NotificationsAction, { orgId: state.orgId })
1188
+ /* @__PURE__ */ jsx(NotificationsAction, { orgId: state.orgId }),
1189
+ /* @__PURE__ */ jsx(SupportAction, { orgId: state.orgId })
1182
1190
  ] });
1183
1191
  }
1184
1192
  function ChromeDevNotesMenu() {
@@ -1232,7 +1240,8 @@ function useChromeUserMenu({
1232
1240
  roles: (state?.orgs ?? []).filter((o) => o.rawRole || o.role).map((o) => ({ role: o.rawRole ?? o.role, orgName: o.name })),
1233
1241
  accounts: [{ id: userId, name: userName, email, avatarUrl: null, active: true }],
1234
1242
  addAccountHref: `${coreBase}/my-profile#accounts`,
1235
- theme: { value: theme.value, onChange: theme.setValue }
1243
+ theme: { value: theme.value, onChange: theme.setValue },
1244
+ navDisplay: state ? { value: state.navDisplay, onChange: state.setNavDisplay } : void 0
1236
1245
  };
1237
1246
  }
1238
1247
  var MODE_DEFS = [
@@ -1295,10 +1304,19 @@ function useChromeMode(arg = {}) {
1295
1304
  return {
1296
1305
  label: current.label,
1297
1306
  icon: current.icon,
1298
- options: options.length > 1 ? options.map((o) => ({ id: o.id, label: o.label, icon: o.icon })) : void 0,
1299
- onSelect: options.length > 1 ? (id) => {
1307
+ // Left-Navigation §7 / Reference/UI-Platform: always the row of three icon
1308
+ // buttons. A mode the organization does not hold is shown dimmed and not
1309
+ // selectable, never hidden. Before entitlements land only the current mode
1310
+ // reads as available (no layout shift, nothing clickable it shouldn't be).
1311
+ options: MODE_DEFS.map((o) => ({
1312
+ id: o.id,
1313
+ label: o.label,
1314
+ icon: o.icon,
1315
+ available: entitled ? Boolean(entitled[o.id]) || o.id === current.id : o.id === current.id
1316
+ })),
1317
+ onSelect: options.length > 0 ? (id) => {
1300
1318
  const def = MODE_DEFS.find((m) => m.id === id);
1301
- if (!def) return;
1319
+ if (!def || !entitled?.[def.id]) return;
1302
1320
  setSelectedModeId(id);
1303
1321
  try {
1304
1322
  localStorage.setItem(MODE_STORAGE_KEY, id);