@the-portland-company/shell 0.52.0 → 0.53.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.
@@ -24,6 +24,46 @@ declare function resolveIdentity(input: {
24
24
  loaded?: boolean;
25
25
  }): ResolvedIdentity;
26
26
 
27
+ /** One stage in the boot pipeline (spec §7). */
28
+ type BootStageId = 's0_session' | 's1_org' | 's2_uvp_connect' | 's3_district' | 's4_universe' | 's5_layers' | 's6_verified' | 's7_tribes' | 's8_intelligence' | 's9_shell';
29
+ type BootPhase = 'booting' | 'slow' | 'error' | 'done';
30
+ /**
31
+ * The progress-reporting contract (spec §14, Shell package integration
32
+ * point). An app with a real pipeline drives this; the shape is intentionally
33
+ * small so a future per-app pipeline can report into it without a shell
34
+ * release.
35
+ */
36
+ type BootProgress = {
37
+ stageId: BootStageId;
38
+ /** 0–100, real, monotonic — never supplied as a guess. */
39
+ pct: number;
40
+ /** The account's own status line for this stage (spec §8; tokens already
41
+ * filled, fallback already applied by the caller). */
42
+ label: string;
43
+ phase: BootPhase;
44
+ /** Critical-stage failure: Retry / Sign out (spec §9.1). */
45
+ onRetry?: () => void;
46
+ onSignOut?: () => void;
47
+ };
48
+ type LoadingScreenProps = {
49
+ /** Real pipeline progress, when the host app has one wired. */
50
+ bootProgress?: BootProgress;
51
+ /**
52
+ * Fallback completion signal used when `bootProgress` is absent: the
53
+ * screen dismisses once this flips true. Callers pass the same "org list
54
+ * resolved" signal FrameBody already computes, so a consumer that never
55
+ * wires the full contract still gets a truthful (if coarse) boot report
56
+ * instead of a spinner.
57
+ */
58
+ syntheticReady?: boolean;
59
+ /** True while the screen should be mounted at all (trigger gate lives in
60
+ * frame.tsx; this component only renders its own exit transition). */
61
+ visible: boolean;
62
+ /** Called after the fade-out completes and the DOM node should unmount. */
63
+ onExitComplete: () => void;
64
+ };
65
+ declare function LoadingScreen({ bootProgress, syntheticReady, visible, onExitComplete, }: LoadingScreenProps): react_jsx_runtime.JSX.Element | null;
66
+
27
67
  /** Every app that renders the shared chrome. Keyed by its subdomain. */
28
68
  type PolitogyAppKey = 'vrm' | 'contacts' | 'forms' | 'surveys' | 'polls' | 'petitions' | 'email' | 'a2p' | 'phonebank' | 'canvass' | 'signs' | 'ctas';
29
69
  /**
@@ -205,7 +245,7 @@ type PolitogyBuildMeta = {
205
245
  commitDate?: string;
206
246
  commitAuthor?: string;
207
247
  };
208
- declare function PolitogyAppFrame({ app, pathname, search, linkComponent, session, onSignOut, signOutUrl, buildMeta, data, children, }: {
248
+ declare function PolitogyAppFrame({ app, pathname, search, linkComponent, session, onSignOut, signOutUrl, buildMeta, data, bootProgress, appReady, children, }: {
209
249
  app: PolitogyAppKey;
210
250
  /** Current path from the app's router (Next: usePathname()). */
211
251
  pathname: string;
@@ -225,6 +265,19 @@ declare function PolitogyAppFrame({ app, pathname, search, linkComponent, sessio
225
265
  buildMeta?: PolitogyBuildMeta;
226
266
  /** Optional DATA plumbing (never styling) — see PolitogyFrameData. */
227
267
  data?: PolitogyFrameData;
268
+ /**
269
+ * Real boot-pipeline progress (Specs/Loading-Screen §14 contract). Absent
270
+ * for apps that have not wired their own pipeline yet — the frame then
271
+ * reports a truthful but coarse two-stage boot instead of a spinner.
272
+ */
273
+ bootProgress?: BootProgress;
274
+ /**
275
+ * Explicit "the host app is ready to be shown" signal. When omitted (every
276
+ * app today), the Loading Screen dismisses itself once the chrome's own
277
+ * org/nav state resolves, so none of the 14 consumer apps has to opt in to
278
+ * keep working.
279
+ */
280
+ appReady?: boolean;
228
281
  children: ReactNode;
229
282
  }): react_jsx_runtime.JSX.Element;
230
283
 
@@ -375,4 +428,4 @@ type UseChromeModeArg = ModeId | {
375
428
  */
376
429
  declare function useChromeMode(arg?: UseChromeModeArg): NavMode;
377
430
 
378
- export { type ChromeDevNotesConfig, type ChromeIdentity, ChromeTopBarActions, type ControlledOrgState, POLITOGY_APPS, PolitogyAppChrome, PolitogyAppFrame, type PolitogyAppKey, type PolitogyAppMode, type PolitogyAppSession, type PolitogyBuildMeta, type PolitogyFrameData, type PolitogyFrameGrant, type PolitogyGrantedFrame, type ResolvedIdentity, deriveBreadcrumbs, looksLikeEmail, resolveIdentity, standardNavIcons, useChromeFooter, useChromeIdentity, useChromeMode, useChromeState, useChromeUserMenu, useIsSuperAdmin };
431
+ export { type BootPhase, type BootProgress, type BootStageId, type ChromeDevNotesConfig, type ChromeIdentity, ChromeTopBarActions, type ControlledOrgState, LoadingScreen, POLITOGY_APPS, PolitogyAppChrome, PolitogyAppFrame, type PolitogyAppKey, type PolitogyAppMode, type PolitogyAppSession, type PolitogyBuildMeta, type PolitogyFrameData, type PolitogyFrameGrant, type PolitogyGrantedFrame, type ResolvedIdentity, deriveBreadcrumbs, looksLikeEmail, resolveIdentity, standardNavIcons, useChromeFooter, useChromeIdentity, useChromeMode, useChromeState, useChromeUserMenu, useIsSuperAdmin };
@@ -24,6 +24,46 @@ declare function resolveIdentity(input: {
24
24
  loaded?: boolean;
25
25
  }): ResolvedIdentity;
26
26
 
27
+ /** One stage in the boot pipeline (spec §7). */
28
+ type BootStageId = 's0_session' | 's1_org' | 's2_uvp_connect' | 's3_district' | 's4_universe' | 's5_layers' | 's6_verified' | 's7_tribes' | 's8_intelligence' | 's9_shell';
29
+ type BootPhase = 'booting' | 'slow' | 'error' | 'done';
30
+ /**
31
+ * The progress-reporting contract (spec §14, Shell package integration
32
+ * point). An app with a real pipeline drives this; the shape is intentionally
33
+ * small so a future per-app pipeline can report into it without a shell
34
+ * release.
35
+ */
36
+ type BootProgress = {
37
+ stageId: BootStageId;
38
+ /** 0–100, real, monotonic — never supplied as a guess. */
39
+ pct: number;
40
+ /** The account's own status line for this stage (spec §8; tokens already
41
+ * filled, fallback already applied by the caller). */
42
+ label: string;
43
+ phase: BootPhase;
44
+ /** Critical-stage failure: Retry / Sign out (spec §9.1). */
45
+ onRetry?: () => void;
46
+ onSignOut?: () => void;
47
+ };
48
+ type LoadingScreenProps = {
49
+ /** Real pipeline progress, when the host app has one wired. */
50
+ bootProgress?: BootProgress;
51
+ /**
52
+ * Fallback completion signal used when `bootProgress` is absent: the
53
+ * screen dismisses once this flips true. Callers pass the same "org list
54
+ * resolved" signal FrameBody already computes, so a consumer that never
55
+ * wires the full contract still gets a truthful (if coarse) boot report
56
+ * instead of a spinner.
57
+ */
58
+ syntheticReady?: boolean;
59
+ /** True while the screen should be mounted at all (trigger gate lives in
60
+ * frame.tsx; this component only renders its own exit transition). */
61
+ visible: boolean;
62
+ /** Called after the fade-out completes and the DOM node should unmount. */
63
+ onExitComplete: () => void;
64
+ };
65
+ declare function LoadingScreen({ bootProgress, syntheticReady, visible, onExitComplete, }: LoadingScreenProps): react_jsx_runtime.JSX.Element | null;
66
+
27
67
  /** Every app that renders the shared chrome. Keyed by its subdomain. */
28
68
  type PolitogyAppKey = 'vrm' | 'contacts' | 'forms' | 'surveys' | 'polls' | 'petitions' | 'email' | 'a2p' | 'phonebank' | 'canvass' | 'signs' | 'ctas';
29
69
  /**
@@ -205,7 +245,7 @@ type PolitogyBuildMeta = {
205
245
  commitDate?: string;
206
246
  commitAuthor?: string;
207
247
  };
208
- declare function PolitogyAppFrame({ app, pathname, search, linkComponent, session, onSignOut, signOutUrl, buildMeta, data, children, }: {
248
+ declare function PolitogyAppFrame({ app, pathname, search, linkComponent, session, onSignOut, signOutUrl, buildMeta, data, bootProgress, appReady, children, }: {
209
249
  app: PolitogyAppKey;
210
250
  /** Current path from the app's router (Next: usePathname()). */
211
251
  pathname: string;
@@ -225,6 +265,19 @@ declare function PolitogyAppFrame({ app, pathname, search, linkComponent, sessio
225
265
  buildMeta?: PolitogyBuildMeta;
226
266
  /** Optional DATA plumbing (never styling) — see PolitogyFrameData. */
227
267
  data?: PolitogyFrameData;
268
+ /**
269
+ * Real boot-pipeline progress (Specs/Loading-Screen §14 contract). Absent
270
+ * for apps that have not wired their own pipeline yet — the frame then
271
+ * reports a truthful but coarse two-stage boot instead of a spinner.
272
+ */
273
+ bootProgress?: BootProgress;
274
+ /**
275
+ * Explicit "the host app is ready to be shown" signal. When omitted (every
276
+ * app today), the Loading Screen dismisses itself once the chrome's own
277
+ * org/nav state resolves, so none of the 14 consumer apps has to opt in to
278
+ * keep working.
279
+ */
280
+ appReady?: boolean;
228
281
  children: ReactNode;
229
282
  }): react_jsx_runtime.JSX.Element;
230
283
 
@@ -375,4 +428,4 @@ type UseChromeModeArg = ModeId | {
375
428
  */
376
429
  declare function useChromeMode(arg?: UseChromeModeArg): NavMode;
377
430
 
378
- export { type ChromeDevNotesConfig, type ChromeIdentity, ChromeTopBarActions, type ControlledOrgState, POLITOGY_APPS, PolitogyAppChrome, PolitogyAppFrame, type PolitogyAppKey, type PolitogyAppMode, type PolitogyAppSession, type PolitogyBuildMeta, type PolitogyFrameData, type PolitogyFrameGrant, type PolitogyGrantedFrame, type ResolvedIdentity, deriveBreadcrumbs, looksLikeEmail, resolveIdentity, standardNavIcons, useChromeFooter, useChromeIdentity, useChromeMode, useChromeState, useChromeUserMenu, useIsSuperAdmin };
431
+ export { type BootPhase, type BootProgress, type BootStageId, type ChromeDevNotesConfig, type ChromeIdentity, ChromeTopBarActions, type ControlledOrgState, LoadingScreen, POLITOGY_APPS, PolitogyAppChrome, PolitogyAppFrame, type PolitogyAppKey, type PolitogyAppMode, type PolitogyAppSession, type PolitogyBuildMeta, type PolitogyFrameData, type PolitogyFrameGrant, type PolitogyGrantedFrame, type ResolvedIdentity, deriveBreadcrumbs, looksLikeEmail, resolveIdentity, standardNavIcons, useChromeFooter, useChromeIdentity, useChromeMode, useChromeState, useChromeUserMenu, useIsSuperAdmin };
@@ -4,10 +4,10 @@ import { clearBetaBannerDismissal, standardPolitogyNav, NavAppShell, NavChromePr
4
4
  import './chunk-U4RZPZET.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
- import { createContext, useRef, useCallback, useEffect, useMemo, useContext, useState } from 'react';
7
+ import { createContext, useMemo, useRef, useEffect, useState, useCallback, useContext } from 'react';
8
8
  import { DevNotesMenu, createDevNotesClient, DevNotesProvider, DevNotesTaskListModal, DevNotesOverlay } from '@the-portland-company/devnotes';
9
9
  import { FiFlag, FiTarget, FiMapPin, FiMap, FiPhone, FiShare2, FiDollarSign, FiCreditCard, FiSettings, FiBriefcase, FiBarChart2, FiUser, FiClock, FiSend, FiFileText, FiEdit3, FiUpload, FiList, FiGrid, FiLifeBuoy, FiActivity, FiLayout, FiMessageSquare, FiMail, FiUsers, FiHome } from 'react-icons/fi';
10
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
10
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
11
11
 
12
12
  // src/lib/breadcrumbs.ts
13
13
  function deriveBreadcrumbs(items, pathname, opts) {
@@ -78,6 +78,182 @@ function resolveIdentity(input) {
78
78
  loaded: input.loaded ?? false
79
79
  };
80
80
  }
81
+ var SYNTHETIC_STAGES = [
82
+ { pct: 20, label: "Connecting to your organization\u2026" },
83
+ { pct: 100, label: "Ready." }
84
+ ];
85
+ var ANNOUNCE_THROTTLE_MS = 1500;
86
+ var MIN_DWELL_MS = 450;
87
+ var FILL_EASE_MS = 380;
88
+ var HOLD_MS = 240;
89
+ var FADE_MS = 300;
90
+ function usePrefersReducedMotion() {
91
+ const [reduced, setReduced] = useState(false);
92
+ useEffect(() => {
93
+ if (typeof window === "undefined" || !window.matchMedia) return;
94
+ const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
95
+ setReduced(mq.matches);
96
+ const onChange = () => setReduced(mq.matches);
97
+ mq.addEventListener("change", onChange);
98
+ return () => mq.removeEventListener("change", onChange);
99
+ }, []);
100
+ return reduced;
101
+ }
102
+ function useThrottledAnnouncement(label, isFinal) {
103
+ const [announced, setAnnounced] = useState(label);
104
+ const lastAtRef = useRef(0);
105
+ useEffect(() => {
106
+ const now = Date.now();
107
+ if (isFinal || now - lastAtRef.current >= ANNOUNCE_THROTTLE_MS) {
108
+ lastAtRef.current = now;
109
+ setAnnounced(label);
110
+ return;
111
+ }
112
+ const wait = ANNOUNCE_THROTTLE_MS - (now - lastAtRef.current);
113
+ const t = setTimeout(() => {
114
+ lastAtRef.current = Date.now();
115
+ setAnnounced(label);
116
+ }, wait);
117
+ return () => clearTimeout(t);
118
+ }, [label, isFinal]);
119
+ return announced;
120
+ }
121
+ function useDwelledLabel(label) {
122
+ const [displayed, setDisplayed] = useState(label);
123
+ const shownAtRef = useRef(0);
124
+ const pendingRef = useRef(null);
125
+ useEffect(() => {
126
+ if (label === displayed) return;
127
+ const elapsed = Date.now() - shownAtRef.current;
128
+ if (pendingRef.current) clearTimeout(pendingRef.current);
129
+ if (elapsed >= MIN_DWELL_MS) {
130
+ shownAtRef.current = Date.now();
131
+ setDisplayed(label);
132
+ } else {
133
+ pendingRef.current = setTimeout(() => {
134
+ shownAtRef.current = Date.now();
135
+ setDisplayed(label);
136
+ }, MIN_DWELL_MS - elapsed);
137
+ }
138
+ return () => {
139
+ if (pendingRef.current) clearTimeout(pendingRef.current);
140
+ };
141
+ }, [label]);
142
+ return displayed;
143
+ }
144
+ function LoadingScreen({
145
+ bootProgress,
146
+ syntheticReady,
147
+ visible,
148
+ onExitComplete
149
+ }) {
150
+ const reducedMotion = usePrefersReducedMotion();
151
+ const synthetic = useMemo(() => {
152
+ const stage = syntheticReady ? SYNTHETIC_STAGES[1] : SYNTHETIC_STAGES[0];
153
+ return {
154
+ pct: stage.pct,
155
+ label: stage.label,
156
+ phase: syntheticReady ? "done" : "booting"
157
+ };
158
+ }, [syntheticReady]);
159
+ const pct = bootProgress?.pct ?? synthetic.pct;
160
+ const rawLabel = bootProgress?.label ?? synthetic.label;
161
+ const phase = bootProgress?.phase ?? synthetic.phase;
162
+ const isFinal = phase === "done" && pct >= 100;
163
+ const highWaterRef = useRef(0);
164
+ const clampedPct = Math.max(pct, highWaterRef.current);
165
+ useEffect(() => {
166
+ highWaterRef.current = clampedPct;
167
+ }, [clampedPct]);
168
+ const displayedLabel = useDwelledLabel(rawLabel);
169
+ const announced = useThrottledAnnouncement(displayedLabel, isFinal);
170
+ const [fading, setFading] = useState(false);
171
+ useEffect(() => {
172
+ if (!isFinal) return;
173
+ const holdMs = reducedMotion ? 0 : HOLD_MS;
174
+ const fadeMs = reducedMotion ? 0 : FADE_MS;
175
+ const holdTimer = setTimeout(() => {
176
+ setFading(true);
177
+ const fadeTimer = setTimeout(onExitComplete, fadeMs);
178
+ return () => clearTimeout(fadeTimer);
179
+ }, holdMs);
180
+ return () => clearTimeout(holdTimer);
181
+ }, [isFinal, reducedMotion]);
182
+ if (!visible) return null;
183
+ const isError = phase === "error";
184
+ return /* @__PURE__ */ jsxs(
185
+ "div",
186
+ {
187
+ "data-testid": "politogy-loading-screen",
188
+ style: {
189
+ position: "fixed",
190
+ inset: 0,
191
+ zIndex: 2147483e3,
192
+ display: "flex",
193
+ flexDirection: "column",
194
+ alignItems: "center",
195
+ justifyContent: "center",
196
+ gap: "2rem",
197
+ background: "var(--color-ink-900, #0a0a0c)",
198
+ color: "#fff",
199
+ opacity: fading ? 0 : 1,
200
+ transition: reducedMotion ? void 0 : `opacity ${FADE_MS}ms ease-out`
201
+ },
202
+ children: [
203
+ /* @__PURE__ */ jsx("img", { src: "/politogy-lockup-white.svg", alt: "Politogy VRM", style: { width: 220, height: "auto" } }),
204
+ /* @__PURE__ */ jsxs("div", { style: { width: 320, maxWidth: "80vw" }, children: [
205
+ /* @__PURE__ */ jsx(
206
+ "div",
207
+ {
208
+ role: "progressbar",
209
+ "aria-valuemin": 0,
210
+ "aria-valuemax": 100,
211
+ "aria-valuenow": Math.round(clampedPct),
212
+ "aria-label": "Loading your workspace",
213
+ style: {
214
+ height: 3,
215
+ width: "100%",
216
+ borderRadius: 2,
217
+ background: "rgba(255,255,255,0.12)",
218
+ overflow: "hidden"
219
+ },
220
+ children: /* @__PURE__ */ jsx(
221
+ "div",
222
+ {
223
+ style: {
224
+ height: "100%",
225
+ width: `${clampedPct}%`,
226
+ background: "linear-gradient(90deg, var(--color-race-blue, #1d4ed8), var(--color-race-red, #dc2626))",
227
+ transition: reducedMotion ? void 0 : `width ${FILL_EASE_MS}ms ease-out`
228
+ }
229
+ }
230
+ )
231
+ }
232
+ ),
233
+ /* @__PURE__ */ jsx(
234
+ "div",
235
+ {
236
+ role: "status",
237
+ "aria-live": "polite",
238
+ style: {
239
+ marginTop: "0.75rem",
240
+ fontFamily: "var(--font-mono, ui-monospace, monospace)",
241
+ fontSize: "0.8125rem",
242
+ color: isError ? "var(--color-race-red, #dc2626)" : "rgba(255,255,255,0.72)",
243
+ textAlign: "center"
244
+ },
245
+ children: announced
246
+ }
247
+ ),
248
+ isError && (bootProgress?.onRetry || bootProgress?.onSignOut) && /* @__PURE__ */ jsxs("div", { style: { marginTop: "1rem", display: "flex", gap: "0.75rem", justifyContent: "center" }, children: [
249
+ bootProgress?.onRetry && /* @__PURE__ */ jsx("button", { type: "button", onClick: bootProgress.onRetry, children: "Retry" }),
250
+ bootProgress?.onSignOut && /* @__PURE__ */ jsx("button", { type: "button", onClick: bootProgress.onSignOut, children: "Sign out" })
251
+ ] })
252
+ ] })
253
+ ]
254
+ }
255
+ );
256
+ }
81
257
  var CORE_APP = "vrm";
82
258
  var DEFAULT_SIGN_OUT_URL = "/auth/signout";
83
259
  var DEFAULT_BRAND_LOGO = "/brand/logos/relationship-mode-logo.png";
@@ -128,6 +304,8 @@ function PolitogyAppFrame({
128
304
  signOutUrl = DEFAULT_SIGN_OUT_URL,
129
305
  buildMeta,
130
306
  data,
307
+ bootProgress,
308
+ appReady,
131
309
  children
132
310
  }) {
133
311
  const meta = POLITOGY_APPS[app];
@@ -156,6 +334,8 @@ function PolitogyAppFrame({
156
334
  signOutUrl,
157
335
  buildMeta,
158
336
  data,
337
+ bootProgress,
338
+ appReady,
159
339
  children
160
340
  }
161
341
  )
@@ -172,10 +352,33 @@ function FrameBody({
172
352
  signOutUrl,
173
353
  buildMeta,
174
354
  data,
355
+ bootProgress,
356
+ appReady,
175
357
  children
176
358
  }) {
177
359
  const meta = POLITOGY_APPS[app];
178
360
  const chrome = useChromeState();
361
+ const bootAppMode = meta.mode;
362
+ const bootOrgId = chrome?.orgId ?? null;
363
+ const [bootVisible, setBootVisible] = useState(true);
364
+ const [bootGeneration, setBootGeneration] = useState(0);
365
+ const seenOrgRef = useRef(bootOrgId);
366
+ const seenModeRef = useRef(bootAppMode);
367
+ useEffect(() => {
368
+ if (seenOrgRef.current !== null && seenOrgRef.current !== bootOrgId) {
369
+ setBootVisible(true);
370
+ setBootGeneration((g) => g + 1);
371
+ }
372
+ seenOrgRef.current = bootOrgId;
373
+ }, [bootOrgId]);
374
+ useEffect(() => {
375
+ if (seenModeRef.current !== void 0 && seenModeRef.current !== bootAppMode) {
376
+ setBootVisible(true);
377
+ setBootGeneration((g) => g + 1);
378
+ }
379
+ seenModeRef.current = bootAppMode;
380
+ }, [bootAppMode]);
381
+ const chromeResolved = chrome == null || !chrome.orgsLoading;
179
382
  const signOutRef = useRef(onSignOut);
180
383
  signOutRef.current = onSignOut;
181
384
  const signingOutRef = useRef(false);
@@ -250,70 +453,82 @@ function FrameBody({
250
453
  [navItems, pathname, search, meta.origin]
251
454
  );
252
455
  const breadcrumbs = core ? core.page?.breadcrumbs : grants?.includes("breadcrumbs") ? granted("breadcrumbs") : derivedBreadcrumbs;
253
- return /* @__PURE__ */ jsx(
254
- NavAppShell,
255
- {
256
- sidebar: {
257
- pathname,
258
- brand: {
259
- logoSrc: core?.brandLogoSrc ?? modeBrandLogo(appMode),
260
- logoAlt: "Politogy",
261
- badgeLabel: "P",
262
- title: "Politogy"
263
- },
264
- nav: {
265
- // A flat rail's sub-menu IS the nav, so the 'tabs only' preference
266
- // (which strips children) would leave one childless parent row —
267
- // flat apps ignore the stripping.
268
- items: navDisplay === "tabs" && meta.navShape !== "flat" ? navItems.map(({ children: _sub, ...item }) => item) : navItems,
269
- flatten: meta.navShape === "flat"
270
- },
271
- // The app's own workspace is NOT the organization — it is a per-app
272
- // bootstrap account with a different id in every app. Showing it while
273
- // the chrome API resolves flashed the wrong org on every load ("not
274
- // connected to the account"). Shimmer while in flight; only fall back
275
- // to the app label once the chrome lookup has actually failed.
276
- org: activeCoreOrg ? {
277
- name: activeCoreOrg.name,
278
- id: activeCoreOrg.id,
279
- logoUrl: activeCoreOrg.logoUrl,
280
- brandColor: activeCoreOrg.brandColor,
281
- organizations: coreOrgs,
282
- onSwitch: chrome?.setOrgId
283
- } : chrome?.orgsLoading ? { name: null, id: null, loading: true } : { name: session.accountName, id: session.accountId },
284
- mode,
285
- footer,
286
- linkComponent
456
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
457
+ /* @__PURE__ */ jsx(
458
+ LoadingScreen,
459
+ {
460
+ visible: bootVisible,
461
+ bootProgress,
462
+ syntheticReady: appReady ?? chromeResolved,
463
+ onExitComplete: () => setBootVisible(false)
287
464
  },
288
- betaBanner: meta.beta === false ? void 0 : {},
289
- modeId: appMode,
290
- contentWrapper: core?.contentWrapper ?? granted("contentWrapper"),
291
- topBar: {
292
- // Core's pages publish their own identity; every other app is one
293
- // product surface and takes its registry title.
294
- title: core?.page?.title ?? (grants?.includes("title") ? data?.frame?.title : void 0) ?? meta.title,
295
- breadcrumbs,
296
- user: {
297
- name: identity.userName,
298
- email: identity.email,
299
- // The chrome API's avatar wins; the app may supply a fallback chain
300
- // (e.g. a stored memoji) for users with no uploaded photo.
301
- avatarUrl: identity.avatarUrl ?? data?.fallbackAvatarUrl ?? null,
302
- role: session.role
465
+ bootGeneration
466
+ ),
467
+ /* @__PURE__ */ jsx(
468
+ NavAppShell,
469
+ {
470
+ sidebar: {
471
+ pathname,
472
+ brand: {
473
+ logoSrc: core?.brandLogoSrc ?? modeBrandLogo(appMode),
474
+ logoAlt: "Politogy",
475
+ badgeLabel: "P",
476
+ title: "Politogy"
477
+ },
478
+ nav: {
479
+ // A flat rail's sub-menu IS the nav, so the 'tabs only' preference
480
+ // (which strips children) would leave one childless parent row —
481
+ // flat apps ignore the stripping.
482
+ items: navDisplay === "tabs" && meta.navShape !== "flat" ? navItems.map(({ children: _sub, ...item }) => item) : navItems,
483
+ flatten: meta.navShape === "flat"
484
+ },
485
+ // The app's own workspace is NOT the organization — it is a per-app
486
+ // bootstrap account with a different id in every app. Showing it while
487
+ // the chrome API resolves flashed the wrong org on every load ("not
488
+ // connected to the account"). Shimmer while in flight; only fall back
489
+ // to the app label once the chrome lookup has actually failed.
490
+ org: activeCoreOrg ? {
491
+ name: activeCoreOrg.name,
492
+ id: activeCoreOrg.id,
493
+ logoUrl: activeCoreOrg.logoUrl,
494
+ brandColor: activeCoreOrg.brandColor,
495
+ organizations: coreOrgs,
496
+ onSwitch: chrome?.setOrgId
497
+ } : chrome?.orgsLoading ? { name: null, id: null, loading: true } : { name: session.accountName, id: session.accountId },
498
+ mode,
499
+ footer,
500
+ linkComponent
303
501
  },
304
- userMenu,
305
- onSignOut: handleSignOut,
306
- // The action cluster is IDENTICAL everywhere — core included. Page
307
- // actions sit BEFORE it, never inside it, so the Alerts bell keeps its
308
- // required position: farthest right, immediately before My Account.
309
- actions: /* @__PURE__ */ jsxs(Fragment, { children: [
310
- core?.page?.actions,
311
- /* @__PURE__ */ jsx(ChromeTopBarActions, {})
312
- ] })
313
- },
314
- children
315
- }
316
- );
502
+ betaBanner: meta.beta === false ? void 0 : {},
503
+ modeId: appMode,
504
+ contentWrapper: core?.contentWrapper ?? granted("contentWrapper"),
505
+ topBar: {
506
+ // Core's pages publish their own identity; every other app is one
507
+ // product surface and takes its registry title.
508
+ title: core?.page?.title ?? (grants?.includes("title") ? data?.frame?.title : void 0) ?? meta.title,
509
+ breadcrumbs,
510
+ user: {
511
+ name: identity.userName,
512
+ email: identity.email,
513
+ // The chrome API's avatar wins; the app may supply a fallback chain
514
+ // (e.g. a stored memoji) for users with no uploaded photo.
515
+ avatarUrl: identity.avatarUrl ?? data?.fallbackAvatarUrl ?? null,
516
+ role: session.role
517
+ },
518
+ userMenu,
519
+ onSignOut: handleSignOut,
520
+ // The action cluster is IDENTICAL everywhere — core included. Page
521
+ // actions sit BEFORE it, never inside it, so the Alerts bell keeps its
522
+ // required position: farthest right, immediately before My Account.
523
+ actions: /* @__PURE__ */ jsxs(Fragment, { children: [
524
+ core?.page?.actions,
525
+ /* @__PURE__ */ jsx(ChromeTopBarActions, {})
526
+ ] })
527
+ },
528
+ children
529
+ }
530
+ )
531
+ ] });
317
532
  }
318
533
  var standardNavIcons = {
319
534
  home: FiHome,
@@ -790,6 +1005,6 @@ function useChromeMode(arg = {}) {
790
1005
  };
791
1006
  }
792
1007
 
793
- export { ChromeTopBarActions, POLITOGY_APPS, PolitogyAppChrome, PolitogyAppFrame, deriveBreadcrumbs, looksLikeEmail, resolveIdentity, standardNavIcons, useChromeFooter, useChromeIdentity, useChromeMode, useChromeState, useChromeUserMenu, useIsSuperAdmin };
1008
+ export { ChromeTopBarActions, LoadingScreen, POLITOGY_APPS, PolitogyAppChrome, PolitogyAppFrame, deriveBreadcrumbs, looksLikeEmail, resolveIdentity, standardNavIcons, useChromeFooter, useChromeIdentity, useChromeMode, useChromeState, useChromeUserMenu, useIsSuperAdmin };
794
1009
  //# sourceMappingURL=app-chrome.js.map
795
1010
  //# sourceMappingURL=app-chrome.js.map