@the-portland-company/shell 0.24.0 → 0.26.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.
@@ -1,9 +1,102 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode } from 'react';
3
- import { NavOrgOption, NavChromeFetcher, StandardNavIconKey, NavIcon, NavFooter, NavMode, NavDisplayMode, NavUserMenu } from './native.cjs';
3
+ import { NavItem, NavBreadcrumb, NavLinkComponent, NavChromeFetcher, NavOrgOption, StandardNavIconKey, NavIcon, NavFooter, NavMode, NavDisplayMode, NavUserMenu } from './native.cjs';
4
4
  import './topbarIconSlot-FunwEKO9.cjs';
5
5
  import './theme-preference.cjs';
6
6
 
7
+ declare function deriveBreadcrumbs(items: NavItem[], pathname: string, opts?: {
8
+ search?: string;
9
+ origin?: string;
10
+ }): NavBreadcrumb[];
11
+
12
+ type ResolvedIdentity = {
13
+ userName: string | null;
14
+ email: string | null;
15
+ avatarUrl: string | null;
16
+ loaded: boolean;
17
+ };
18
+ /** True when a "name" is really just an email address. */
19
+ declare function looksLikeEmail(value: string | null | undefined): boolean;
20
+ declare function resolveIdentity(input: {
21
+ apiName?: string | null;
22
+ fallbackName?: string | null;
23
+ email?: string | null;
24
+ avatarUrl?: string | null;
25
+ loaded?: boolean;
26
+ }): ResolvedIdentity;
27
+
28
+ /** Every app that renders the shared chrome. Keyed by its subdomain. */
29
+ type PolitogyAppKey = 'vrm' | 'contacts' | 'forms' | 'surveys' | 'polls' | 'petitions';
30
+ /**
31
+ * The per-app registry — the ONLY place app identity lives. A new app adds a
32
+ * row here and inherits every chrome behavior; it never re-derives one.
33
+ */
34
+ declare const POLITOGY_APPS: Record<PolitogyAppKey, {
35
+ title: string;
36
+ origin: string;
37
+ storagePrefix: string;
38
+ }>;
39
+ type PolitogyAppSession = {
40
+ /** Stable IdP subject (`sub`). */
41
+ userId: string;
42
+ email: string | null;
43
+ /** The app's own role string — feeds DevNotes admin gating. */
44
+ role: string;
45
+ /** The app's own workspace label, used only until core orgs resolve. */
46
+ accountName: string;
47
+ accountId: string;
48
+ /**
49
+ * Pre-hydration display name ONLY. The chrome API is the authority; an email
50
+ * passed here is discarded rather than rendered as a name.
51
+ */
52
+ userName?: string | null;
53
+ };
54
+ /**
55
+ * Optional DATA plumbing for apps whose session/auth differs — never styling.
56
+ *
57
+ * Everything here is about WHERE data comes from (which fetcher, which org
58
+ * state, which routes an app owns), not what the chrome looks like. Adding a
59
+ * visual option to this type would re-open the drift this component exists to
60
+ * close.
61
+ */
62
+ type PolitogyFrameData = {
63
+ /** Chrome-API fetcher override for apps that authenticate client-side. */
64
+ fetchJson?: NavChromeFetcher;
65
+ /** DevNotes wiring override for those same direct-auth apps. */
66
+ devNotes?: ChromeDevNotesConfig;
67
+ /** Controlled org state for apps that own active-org + switching. */
68
+ org?: ControlledOrgState;
69
+ /** App-owned children for a standard nav entry this app owns (Contacts). */
70
+ contactsItem?: {
71
+ href?: string;
72
+ children?: NavItem[];
73
+ };
74
+ /** Avatar fallback when the profile has no uploaded photo (memoji, default). */
75
+ fallbackAvatarUrl?: string | null;
76
+ };
77
+ type PolitogyBuildMeta = {
78
+ envLabel?: string;
79
+ version?: string;
80
+ commit?: string;
81
+ commitDate?: string;
82
+ commitAuthor?: string;
83
+ };
84
+ declare function PolitogyAppFrame({ app, pathname, search, linkComponent, session, onSignOut, buildMeta, data, children, }: {
85
+ app: PolitogyAppKey;
86
+ /** Current path from the app's router (Next: usePathname()). */
87
+ pathname: string;
88
+ /** Current query string, when the app routes by query (forms' ?tab=). */
89
+ search?: string;
90
+ /** The app's routing primitive (Next: <Link>). */
91
+ linkComponent: NavLinkComponent;
92
+ session: PolitogyAppSession;
93
+ onSignOut: () => void;
94
+ buildMeta?: PolitogyBuildMeta;
95
+ /** Optional DATA plumbing (never styling) — see PolitogyFrameData. */
96
+ data?: PolitogyFrameData;
97
+ children: ReactNode;
98
+ }): react_jsx_runtime.JSX.Element;
99
+
7
100
  /**
8
101
  * The canonical icon map for standardPolitogyNav — ONE definition so every
9
102
  * app renders identical glyphs (including the Organization Settings branch:
@@ -83,6 +176,11 @@ declare function useChromeFooter(meta: {
83
176
  commitDate?: string;
84
177
  commitAuthor?: string;
85
178
  }): NavFooter;
179
+ declare function useChromeIdentity(fallback?: {
180
+ userName?: string | null;
181
+ email?: string | null;
182
+ }): ResolvedIdentity;
183
+
86
184
  /**
87
185
  * The My Account menu config — profile/org links into VRM core, real roles
88
186
  * from the user's core organizations, the signed-in account, and the shared
@@ -93,6 +191,16 @@ declare function useChromeUserMenu({ userId, userName, email, }: {
93
191
  userName: string | null;
94
192
  email: string | null;
95
193
  }): NavUserMenu;
96
- declare function useChromeMode(currentModeId?: 'relationship' | 'campaign' | 'petition'): NavMode;
194
+ /**
195
+ * The active mode pill / selector.
196
+ *
197
+ * The current mode is the USER'S OWN SELECTION (persisted cross-app in
198
+ * localStorage under `vrm.activeMode`), narrowed to what the active org is
199
+ * entitled to — never a literal an app hardcodes. Apps used to call
200
+ * `useChromeMode('relationship')`, which pinned the pill to Relationship even
201
+ * for an org entitled to Campaign or Petition (HITL 2026-07-23). The optional
202
+ * argument now only supplies the pre-entitlement fallback for first paint.
203
+ */
204
+ declare function useChromeMode(fallbackModeId?: 'relationship' | 'campaign' | 'petition'): NavMode;
97
205
 
98
- export { type ChromeDevNotesConfig, type ChromeIdentity, ChromeTopBarActions, type ControlledOrgState, PolitogyAppChrome, standardNavIcons, useChromeFooter, useChromeMode, useChromeState, useChromeUserMenu, useIsSuperAdmin };
206
+ export { type ChromeDevNotesConfig, type ChromeIdentity, ChromeTopBarActions, type ControlledOrgState, POLITOGY_APPS, PolitogyAppChrome, PolitogyAppFrame, type PolitogyAppKey, type PolitogyAppSession, type PolitogyBuildMeta, type PolitogyFrameData, type ResolvedIdentity, deriveBreadcrumbs, looksLikeEmail, resolveIdentity, standardNavIcons, useChromeFooter, useChromeIdentity, useChromeMode, useChromeState, useChromeUserMenu, useIsSuperAdmin };
@@ -1,9 +1,102 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode } from 'react';
3
- import { NavOrgOption, NavChromeFetcher, StandardNavIconKey, NavIcon, NavFooter, NavMode, NavDisplayMode, NavUserMenu } from './native.js';
3
+ import { NavItem, NavBreadcrumb, NavLinkComponent, NavChromeFetcher, NavOrgOption, StandardNavIconKey, NavIcon, NavFooter, NavMode, NavDisplayMode, NavUserMenu } from './native.js';
4
4
  import './topbarIconSlot-FunwEKO9.js';
5
5
  import './theme-preference.js';
6
6
 
7
+ declare function deriveBreadcrumbs(items: NavItem[], pathname: string, opts?: {
8
+ search?: string;
9
+ origin?: string;
10
+ }): NavBreadcrumb[];
11
+
12
+ type ResolvedIdentity = {
13
+ userName: string | null;
14
+ email: string | null;
15
+ avatarUrl: string | null;
16
+ loaded: boolean;
17
+ };
18
+ /** True when a "name" is really just an email address. */
19
+ declare function looksLikeEmail(value: string | null | undefined): boolean;
20
+ declare function resolveIdentity(input: {
21
+ apiName?: string | null;
22
+ fallbackName?: string | null;
23
+ email?: string | null;
24
+ avatarUrl?: string | null;
25
+ loaded?: boolean;
26
+ }): ResolvedIdentity;
27
+
28
+ /** Every app that renders the shared chrome. Keyed by its subdomain. */
29
+ type PolitogyAppKey = 'vrm' | 'contacts' | 'forms' | 'surveys' | 'polls' | 'petitions';
30
+ /**
31
+ * The per-app registry — the ONLY place app identity lives. A new app adds a
32
+ * row here and inherits every chrome behavior; it never re-derives one.
33
+ */
34
+ declare const POLITOGY_APPS: Record<PolitogyAppKey, {
35
+ title: string;
36
+ origin: string;
37
+ storagePrefix: string;
38
+ }>;
39
+ type PolitogyAppSession = {
40
+ /** Stable IdP subject (`sub`). */
41
+ userId: string;
42
+ email: string | null;
43
+ /** The app's own role string — feeds DevNotes admin gating. */
44
+ role: string;
45
+ /** The app's own workspace label, used only until core orgs resolve. */
46
+ accountName: string;
47
+ accountId: string;
48
+ /**
49
+ * Pre-hydration display name ONLY. The chrome API is the authority; an email
50
+ * passed here is discarded rather than rendered as a name.
51
+ */
52
+ userName?: string | null;
53
+ };
54
+ /**
55
+ * Optional DATA plumbing for apps whose session/auth differs — never styling.
56
+ *
57
+ * Everything here is about WHERE data comes from (which fetcher, which org
58
+ * state, which routes an app owns), not what the chrome looks like. Adding a
59
+ * visual option to this type would re-open the drift this component exists to
60
+ * close.
61
+ */
62
+ type PolitogyFrameData = {
63
+ /** Chrome-API fetcher override for apps that authenticate client-side. */
64
+ fetchJson?: NavChromeFetcher;
65
+ /** DevNotes wiring override for those same direct-auth apps. */
66
+ devNotes?: ChromeDevNotesConfig;
67
+ /** Controlled org state for apps that own active-org + switching. */
68
+ org?: ControlledOrgState;
69
+ /** App-owned children for a standard nav entry this app owns (Contacts). */
70
+ contactsItem?: {
71
+ href?: string;
72
+ children?: NavItem[];
73
+ };
74
+ /** Avatar fallback when the profile has no uploaded photo (memoji, default). */
75
+ fallbackAvatarUrl?: string | null;
76
+ };
77
+ type PolitogyBuildMeta = {
78
+ envLabel?: string;
79
+ version?: string;
80
+ commit?: string;
81
+ commitDate?: string;
82
+ commitAuthor?: string;
83
+ };
84
+ declare function PolitogyAppFrame({ app, pathname, search, linkComponent, session, onSignOut, buildMeta, data, children, }: {
85
+ app: PolitogyAppKey;
86
+ /** Current path from the app's router (Next: usePathname()). */
87
+ pathname: string;
88
+ /** Current query string, when the app routes by query (forms' ?tab=). */
89
+ search?: string;
90
+ /** The app's routing primitive (Next: <Link>). */
91
+ linkComponent: NavLinkComponent;
92
+ session: PolitogyAppSession;
93
+ onSignOut: () => void;
94
+ buildMeta?: PolitogyBuildMeta;
95
+ /** Optional DATA plumbing (never styling) — see PolitogyFrameData. */
96
+ data?: PolitogyFrameData;
97
+ children: ReactNode;
98
+ }): react_jsx_runtime.JSX.Element;
99
+
7
100
  /**
8
101
  * The canonical icon map for standardPolitogyNav — ONE definition so every
9
102
  * app renders identical glyphs (including the Organization Settings branch:
@@ -83,6 +176,11 @@ declare function useChromeFooter(meta: {
83
176
  commitDate?: string;
84
177
  commitAuthor?: string;
85
178
  }): NavFooter;
179
+ declare function useChromeIdentity(fallback?: {
180
+ userName?: string | null;
181
+ email?: string | null;
182
+ }): ResolvedIdentity;
183
+
86
184
  /**
87
185
  * The My Account menu config — profile/org links into VRM core, real roles
88
186
  * from the user's core organizations, the signed-in account, and the shared
@@ -93,6 +191,16 @@ declare function useChromeUserMenu({ userId, userName, email, }: {
93
191
  userName: string | null;
94
192
  email: string | null;
95
193
  }): NavUserMenu;
96
- declare function useChromeMode(currentModeId?: 'relationship' | 'campaign' | 'petition'): NavMode;
194
+ /**
195
+ * The active mode pill / selector.
196
+ *
197
+ * The current mode is the USER'S OWN SELECTION (persisted cross-app in
198
+ * localStorage under `vrm.activeMode`), narrowed to what the active org is
199
+ * entitled to — never a literal an app hardcodes. Apps used to call
200
+ * `useChromeMode('relationship')`, which pinned the pill to Relationship even
201
+ * for an org entitled to Campaign or Petition (HITL 2026-07-23). The optional
202
+ * argument now only supplies the pre-entitlement fallback for first paint.
203
+ */
204
+ declare function useChromeMode(fallbackModeId?: 'relationship' | 'campaign' | 'petition'): NavMode;
97
205
 
98
- export { type ChromeDevNotesConfig, type ChromeIdentity, ChromeTopBarActions, type ControlledOrgState, PolitogyAppChrome, standardNavIcons, useChromeFooter, useChromeMode, useChromeState, useChromeUserMenu, useIsSuperAdmin };
206
+ export { type ChromeDevNotesConfig, type ChromeIdentity, ChromeTopBarActions, type ControlledOrgState, POLITOGY_APPS, PolitogyAppChrome, PolitogyAppFrame, type PolitogyAppKey, type PolitogyAppSession, type PolitogyBuildMeta, type PolitogyFrameData, type ResolvedIdentity, deriveBreadcrumbs, looksLikeEmail, resolveIdentity, standardNavIcons, useChromeFooter, useChromeIdentity, useChromeMode, useChromeState, useChromeUserMenu, useIsSuperAdmin };
@@ -1,12 +1,215 @@
1
1
  "use client";
2
- import { NavChromeProvider, FuelAction, NavDisplayAction, TopBarWidgetSlot, SupportAction, NotificationsAction, useThemePreference, useNavChromeFetcher, useChromeOrgs } from './chunk-YWFWSAVT.js';
2
+ import { standardPolitogyNav, NavAppShell, NavChromeProvider, FuelAction, NavDisplayAction, TopBarWidgetSlot, SupportAction, NotificationsAction, useChromeMe, useThemePreference, useNavChromeFetcher, useChromeOrgs } from './chunk-QCA2PGKA.js';
3
3
  import './chunk-NV6WHTEZ.js';
4
- import './chunk-ZZWM7BZZ.js';
5
- import { createContext, useContext, useMemo, useState, useEffect, useCallback } from 'react';
4
+ import './chunk-LL7RCJQN.js';
5
+ import { createContext, useMemo, useContext, useState, useEffect, useCallback } from 'react';
6
6
  import { DevNotesMenu, createDevNotesClient, DevNotesProvider } from '@the-portland-company/devnotes';
7
7
  import { FiShare2, FiDollarSign, FiCreditCard, FiSettings, FiBriefcase, FiBarChart2, FiUser, FiClock, FiSend, FiFileText, FiEdit3, FiUpload, FiList, FiGrid, FiLifeBuoy, FiActivity, FiLayout, FiMessageSquare, FiMail, FiUsers, FiHome, FiFlag } from 'react-icons/fi';
8
8
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
9
9
 
10
+ // src/lib/breadcrumbs.ts
11
+ function deriveBreadcrumbs(items, pathname, opts) {
12
+ const search = opts?.search ?? (typeof window !== "undefined" ? window.location.search : "");
13
+ const origin = opts?.origin ?? (typeof window !== "undefined" ? window.location.origin : "");
14
+ const matches = (href) => {
15
+ if (!href) return false;
16
+ let path = href;
17
+ let query = "";
18
+ if (/^https?:\/\//i.test(href)) {
19
+ try {
20
+ const u = new URL(href);
21
+ if (origin && u.origin !== origin) return false;
22
+ path = u.pathname;
23
+ query = u.search;
24
+ } catch {
25
+ return false;
26
+ }
27
+ } else {
28
+ const qi = href.indexOf("?");
29
+ if (qi >= 0) {
30
+ path = href.slice(0, qi) || "/";
31
+ query = href.slice(qi);
32
+ }
33
+ }
34
+ if (path !== pathname) return false;
35
+ if (!query) return true;
36
+ const want = new URLSearchParams(query);
37
+ const have = new URLSearchParams(search);
38
+ for (const [k, v] of want) if (have.get(k) !== v) return false;
39
+ return true;
40
+ };
41
+ const walk = (nodes, trail2) => {
42
+ for (const node of nodes) {
43
+ const here = [...trail2, node];
44
+ if (node.children?.length) {
45
+ const deeper = walk(node.children, here);
46
+ if (deeper) return deeper;
47
+ }
48
+ if (matches(node.href)) return here;
49
+ }
50
+ return null;
51
+ };
52
+ const trail = walk(items, []);
53
+ const dashboard = items.find((i) => i.label === "Dashboard");
54
+ const crumbs = [{ label: "Dashboard", href: dashboard?.href }];
55
+ if (!trail) return crumbs;
56
+ for (const node of trail) {
57
+ if (node.label === "Dashboard") continue;
58
+ crumbs.push({ label: node.label, href: node.href });
59
+ }
60
+ const last = crumbs[crumbs.length - 1];
61
+ if (last) delete last.href;
62
+ return crumbs;
63
+ }
64
+
65
+ // src/lib/identity.ts
66
+ function looksLikeEmail(value) {
67
+ return typeof value === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim());
68
+ }
69
+ function resolveIdentity(input) {
70
+ const apiName = looksLikeEmail(input.apiName) ? null : input.apiName ?? null;
71
+ const fallbackName = looksLikeEmail(input.fallbackName) ? null : input.fallbackName ?? null;
72
+ return {
73
+ userName: apiName ?? fallbackName,
74
+ email: input.email ?? null,
75
+ avatarUrl: input.avatarUrl ?? null,
76
+ loaded: input.loaded ?? false
77
+ };
78
+ }
79
+ var POLITOGY_APPS = {
80
+ vrm: { title: "Politogy VRM", origin: "https://app.politogyvrm.com", storagePrefix: "vrm" },
81
+ contacts: { title: "Contacts", origin: "https://contacts.politogyvrm.com", storagePrefix: "contacts" },
82
+ forms: { title: "Forms", origin: "https://forms.politogyvrm.com", storagePrefix: "forms" },
83
+ surveys: { title: "Surveys", origin: "https://surveys.politogyvrm.com", storagePrefix: "surveys" },
84
+ polls: { title: "Polls", origin: "https://polls.politogyvrm.com", storagePrefix: "polls" },
85
+ petitions: { title: "Petitions", origin: "https://petitions.politogyvrm.com", storagePrefix: "petitions" }
86
+ };
87
+ function PolitogyAppFrame({
88
+ app,
89
+ pathname,
90
+ search,
91
+ linkComponent,
92
+ session,
93
+ onSignOut,
94
+ buildMeta,
95
+ data,
96
+ children
97
+ }) {
98
+ const meta = POLITOGY_APPS[app];
99
+ return /* @__PURE__ */ jsx(
100
+ PolitogyAppChrome,
101
+ {
102
+ storagePrefix: meta.storagePrefix,
103
+ fetchJson: data?.fetchJson,
104
+ devNotes: data?.devNotes,
105
+ org: data?.org,
106
+ identity: {
107
+ userId: session.userId,
108
+ userName: session.userName ?? null,
109
+ email: session.email,
110
+ role: session.role
111
+ },
112
+ children: /* @__PURE__ */ jsx(
113
+ FrameBody,
114
+ {
115
+ app,
116
+ pathname,
117
+ search,
118
+ linkComponent,
119
+ session,
120
+ onSignOut,
121
+ buildMeta,
122
+ data,
123
+ children
124
+ }
125
+ )
126
+ }
127
+ );
128
+ }
129
+ function FrameBody({
130
+ app,
131
+ pathname,
132
+ search,
133
+ linkComponent,
134
+ session,
135
+ onSignOut,
136
+ buildMeta,
137
+ data,
138
+ children
139
+ }) {
140
+ const meta = POLITOGY_APPS[app];
141
+ const chrome = useChromeState();
142
+ const identity = useChromeIdentity({ userName: session.userName, email: session.email });
143
+ const userMenu = useChromeUserMenu({
144
+ userId: session.userId,
145
+ userName: identity.userName,
146
+ email: identity.email
147
+ });
148
+ const footer = useChromeFooter(buildMeta ?? {});
149
+ const mode = useChromeMode();
150
+ const coreOrgs = chrome?.orgs ?? [];
151
+ const activeCoreOrg = coreOrgs.find((o) => o.id === chrome?.orgId) ?? null;
152
+ const navDisplay = chrome?.navDisplay ?? "both";
153
+ const navItems = useMemo(
154
+ () => standardPolitogyNav({
155
+ coreBaseUrl: POLITOGY_APPS.vrm.origin,
156
+ icons: standardNavIcons,
157
+ // An app that OWNS a standard nav entry (Contacts) nests its own routes
158
+ // under it. This is routing data, not chrome configuration.
159
+ contactsItem: data?.contactsItem
160
+ }),
161
+ [data?.contactsItem]
162
+ );
163
+ const breadcrumbs = useMemo(
164
+ () => deriveBreadcrumbs(navItems, pathname, { search, origin: meta.origin }),
165
+ [navItems, pathname, search, meta.origin]
166
+ );
167
+ return /* @__PURE__ */ jsx(
168
+ NavAppShell,
169
+ {
170
+ sidebar: {
171
+ pathname,
172
+ brand: {
173
+ logoSrc: "/brand/logos/relationship-mode-logo.png",
174
+ logoAlt: "Politogy",
175
+ badgeLabel: "P",
176
+ title: "Politogy"
177
+ },
178
+ nav: {
179
+ items: navDisplay === "tabs" ? navItems.map(({ children: _sub, ...item }) => item) : navItems
180
+ },
181
+ org: activeCoreOrg ? {
182
+ name: activeCoreOrg.name,
183
+ id: activeCoreOrg.id,
184
+ logoUrl: activeCoreOrg.logoUrl,
185
+ brandColor: activeCoreOrg.brandColor,
186
+ organizations: coreOrgs,
187
+ onSwitch: chrome?.setOrgId
188
+ } : { name: session.accountName, id: session.accountId },
189
+ mode,
190
+ footer,
191
+ linkComponent
192
+ },
193
+ betaBanner: {},
194
+ topBar: {
195
+ title: meta.title,
196
+ breadcrumbs,
197
+ user: {
198
+ name: identity.userName,
199
+ email: identity.email,
200
+ // The chrome API's avatar wins; the app may supply a fallback chain
201
+ // (e.g. a stored memoji) for users with no uploaded photo.
202
+ avatarUrl: identity.avatarUrl ?? data?.fallbackAvatarUrl ?? null,
203
+ role: session.role
204
+ },
205
+ userMenu,
206
+ onSignOut,
207
+ actions: /* @__PURE__ */ jsx(ChromeTopBarActions, {})
208
+ },
209
+ children
210
+ }
211
+ );
212
+ }
10
213
  var standardNavIcons = {
11
214
  home: FiHome,
12
215
  users: FiUsers,
@@ -31,6 +234,7 @@ var standardNavIcons = {
31
234
  share: FiShare2
32
235
  };
33
236
  var ORG_STORAGE_KEY = "vrm.activeOrg";
237
+ var MODE_STORAGE_KEY = "vrm.activeMode";
34
238
  var NAV_DISPLAY_KEY = "vrm.navDisplay";
35
239
  var CORE_BASE_URL = "https://app.politogyvrm.com";
36
240
  var ChromeStateContext = createContext(null);
@@ -215,6 +419,16 @@ function useChromeFooter(meta) {
215
419
  [isSuperAdmin, meta.envLabel, meta.version, meta.commit, meta.commitDate, meta.commitAuthor]
216
420
  );
217
421
  }
422
+ function useChromeIdentity(fallback) {
423
+ const { me, loaded } = useChromeMe();
424
+ return resolveIdentity({
425
+ apiName: me?.userName,
426
+ fallbackName: fallback?.userName,
427
+ email: fallback?.email,
428
+ avatarUrl: me?.avatarUrl,
429
+ loaded
430
+ });
431
+ }
218
432
  function useChromeUserMenu({
219
433
  userId,
220
434
  userName,
@@ -258,11 +472,19 @@ var MODE_DEFS = [
258
472
  orgQuery: true
259
473
  }
260
474
  ];
261
- function useChromeMode(currentModeId = "relationship") {
475
+ function useChromeMode(fallbackModeId = "relationship") {
262
476
  const ctx = useContext(ChromeStateContext);
263
477
  const nav = useNavChromeFetcher();
264
478
  const orgId = ctx?.orgId ?? null;
265
479
  const [entitled, setEntitled] = useState(null);
480
+ const [selectedModeId, setSelectedModeId] = useState(null);
481
+ useEffect(() => {
482
+ try {
483
+ const stored = localStorage.getItem(MODE_STORAGE_KEY);
484
+ if (stored && MODE_DEFS.some((m) => m.id === stored)) setSelectedModeId(stored);
485
+ } catch {
486
+ }
487
+ }, []);
266
488
  useEffect(() => {
267
489
  if (!nav || !orgId) return;
268
490
  let cancelled = false;
@@ -276,8 +498,9 @@ function useChromeMode(currentModeId = "relationship") {
276
498
  cancelled = true;
277
499
  };
278
500
  }, [nav, orgId]);
279
- const current = MODE_DEFS.find((m) => m.id === currentModeId) ?? MODE_DEFS[0];
280
501
  const options = MODE_DEFS.filter((m) => entitled?.[m.id]);
502
+ const selected = selectedModeId ? options.find((m) => m.id === selectedModeId) ?? null : null;
503
+ const current = selected ?? options[0] ?? MODE_DEFS.find((m) => m.id === fallbackModeId) ?? MODE_DEFS[0];
281
504
  const suffix = orgId ? `?org=${orgId.slice(-6)}` : "";
282
505
  return {
283
506
  label: current.label,
@@ -286,6 +509,11 @@ function useChromeMode(currentModeId = "relationship") {
286
509
  onSelect: options.length > 1 ? (id) => {
287
510
  const def = MODE_DEFS.find((m) => m.id === id);
288
511
  if (!def) return;
512
+ setSelectedModeId(id);
513
+ try {
514
+ localStorage.setItem(MODE_STORAGE_KEY, id);
515
+ } catch {
516
+ }
289
517
  const sep = def.href.includes("?") ? "&" : "?";
290
518
  window.location.assign(
291
519
  def.orgQuery && suffix ? `${def.href}${sep}${suffix.slice(1)}` : def.href
@@ -294,6 +522,6 @@ function useChromeMode(currentModeId = "relationship") {
294
522
  };
295
523
  }
296
524
 
297
- export { ChromeTopBarActions, PolitogyAppChrome, standardNavIcons, useChromeFooter, useChromeMode, useChromeState, useChromeUserMenu, useIsSuperAdmin };
525
+ export { ChromeTopBarActions, POLITOGY_APPS, PolitogyAppChrome, PolitogyAppFrame, deriveBreadcrumbs, looksLikeEmail, resolveIdentity, standardNavIcons, useChromeFooter, useChromeIdentity, useChromeMode, useChromeState, useChromeUserMenu, useIsSuperAdmin };
298
526
  //# sourceMappingURL=app-chrome.js.map
299
527
  //# sourceMappingURL=app-chrome.js.map