@the-portland-company/shell 0.52.0 → 0.53.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.
@@ -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,216 @@ 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__ */ jsxs(
204
+ "div",
205
+ {
206
+ "aria-label": "Politogy",
207
+ style: {
208
+ display: "flex",
209
+ alignItems: "center",
210
+ gap: "0.6rem",
211
+ fontFamily: "var(--font-sans, ui-sans-serif, system-ui, sans-serif)"
212
+ },
213
+ children: [
214
+ /* @__PURE__ */ jsx(
215
+ "span",
216
+ {
217
+ "aria-hidden": "true",
218
+ style: {
219
+ display: "inline-flex",
220
+ alignItems: "center",
221
+ justifyContent: "center",
222
+ width: 34,
223
+ height: 26,
224
+ borderRadius: 6,
225
+ background: "#fff",
226
+ color: "var(--color-ink-900, #0a0a0c)",
227
+ fontSize: "0.8125rem",
228
+ fontWeight: 800,
229
+ letterSpacing: "-0.02em"
230
+ },
231
+ children: "vrm"
232
+ }
233
+ ),
234
+ /* @__PURE__ */ jsx("span", { style: { fontSize: "1.375rem", fontWeight: 700, letterSpacing: "-0.02em" }, children: "Politogy" })
235
+ ]
236
+ }
237
+ ),
238
+ /* @__PURE__ */ jsxs("div", { style: { width: 320, maxWidth: "80vw" }, children: [
239
+ /* @__PURE__ */ jsx(
240
+ "div",
241
+ {
242
+ role: "progressbar",
243
+ "aria-valuemin": 0,
244
+ "aria-valuemax": 100,
245
+ "aria-valuenow": Math.round(clampedPct),
246
+ "aria-label": "Loading your workspace",
247
+ style: {
248
+ height: 3,
249
+ width: "100%",
250
+ borderRadius: 2,
251
+ background: "rgba(255,255,255,0.12)",
252
+ overflow: "hidden"
253
+ },
254
+ children: /* @__PURE__ */ jsx(
255
+ "div",
256
+ {
257
+ style: {
258
+ height: "100%",
259
+ width: `${clampedPct}%`,
260
+ background: "linear-gradient(90deg, var(--color-race-blue, #1d4ed8), var(--color-race-red, #dc2626))",
261
+ transition: reducedMotion ? void 0 : `width ${FILL_EASE_MS}ms ease-out`
262
+ }
263
+ }
264
+ )
265
+ }
266
+ ),
267
+ /* @__PURE__ */ jsx(
268
+ "div",
269
+ {
270
+ role: "status",
271
+ "aria-live": "polite",
272
+ style: {
273
+ marginTop: "0.75rem",
274
+ fontFamily: "var(--font-mono, ui-monospace, monospace)",
275
+ fontSize: "0.8125rem",
276
+ color: isError ? "var(--color-race-red, #dc2626)" : "rgba(255,255,255,0.72)",
277
+ textAlign: "center"
278
+ },
279
+ children: announced
280
+ }
281
+ ),
282
+ isError && (bootProgress?.onRetry || bootProgress?.onSignOut) && /* @__PURE__ */ jsxs("div", { style: { marginTop: "1rem", display: "flex", gap: "0.75rem", justifyContent: "center" }, children: [
283
+ bootProgress?.onRetry && /* @__PURE__ */ jsx("button", { type: "button", onClick: bootProgress.onRetry, children: "Retry" }),
284
+ bootProgress?.onSignOut && /* @__PURE__ */ jsx("button", { type: "button", onClick: bootProgress.onSignOut, children: "Sign out" })
285
+ ] })
286
+ ] })
287
+ ]
288
+ }
289
+ );
290
+ }
81
291
  var CORE_APP = "vrm";
82
292
  var DEFAULT_SIGN_OUT_URL = "/auth/signout";
83
293
  var DEFAULT_BRAND_LOGO = "/brand/logos/relationship-mode-logo.png";
@@ -128,6 +338,8 @@ function PolitogyAppFrame({
128
338
  signOutUrl = DEFAULT_SIGN_OUT_URL,
129
339
  buildMeta,
130
340
  data,
341
+ bootProgress,
342
+ appReady,
131
343
  children
132
344
  }) {
133
345
  const meta = POLITOGY_APPS[app];
@@ -156,6 +368,8 @@ function PolitogyAppFrame({
156
368
  signOutUrl,
157
369
  buildMeta,
158
370
  data,
371
+ bootProgress,
372
+ appReady,
159
373
  children
160
374
  }
161
375
  )
@@ -172,10 +386,33 @@ function FrameBody({
172
386
  signOutUrl,
173
387
  buildMeta,
174
388
  data,
389
+ bootProgress,
390
+ appReady,
175
391
  children
176
392
  }) {
177
393
  const meta = POLITOGY_APPS[app];
178
394
  const chrome = useChromeState();
395
+ const bootAppMode = meta.mode;
396
+ const bootOrgId = chrome?.orgId ?? null;
397
+ const [bootVisible, setBootVisible] = useState(true);
398
+ const [bootGeneration, setBootGeneration] = useState(0);
399
+ const seenOrgRef = useRef(bootOrgId);
400
+ const seenModeRef = useRef(bootAppMode);
401
+ useEffect(() => {
402
+ if (seenOrgRef.current !== null && seenOrgRef.current !== bootOrgId) {
403
+ setBootVisible(true);
404
+ setBootGeneration((g) => g + 1);
405
+ }
406
+ seenOrgRef.current = bootOrgId;
407
+ }, [bootOrgId]);
408
+ useEffect(() => {
409
+ if (seenModeRef.current !== void 0 && seenModeRef.current !== bootAppMode) {
410
+ setBootVisible(true);
411
+ setBootGeneration((g) => g + 1);
412
+ }
413
+ seenModeRef.current = bootAppMode;
414
+ }, [bootAppMode]);
415
+ const chromeResolved = chrome == null || !chrome.orgsLoading;
179
416
  const signOutRef = useRef(onSignOut);
180
417
  signOutRef.current = onSignOut;
181
418
  const signingOutRef = useRef(false);
@@ -250,70 +487,82 @@ function FrameBody({
250
487
  [navItems, pathname, search, meta.origin]
251
488
  );
252
489
  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
490
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
491
+ /* @__PURE__ */ jsx(
492
+ LoadingScreen,
493
+ {
494
+ visible: bootVisible,
495
+ bootProgress,
496
+ syntheticReady: appReady ?? chromeResolved,
497
+ onExitComplete: () => setBootVisible(false)
287
498
  },
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
499
+ bootGeneration
500
+ ),
501
+ /* @__PURE__ */ jsx(
502
+ NavAppShell,
503
+ {
504
+ sidebar: {
505
+ pathname,
506
+ brand: {
507
+ logoSrc: core?.brandLogoSrc ?? modeBrandLogo(appMode),
508
+ logoAlt: "Politogy",
509
+ badgeLabel: "P",
510
+ title: "Politogy"
511
+ },
512
+ nav: {
513
+ // A flat rail's sub-menu IS the nav, so the 'tabs only' preference
514
+ // (which strips children) would leave one childless parent row —
515
+ // flat apps ignore the stripping.
516
+ items: navDisplay === "tabs" && meta.navShape !== "flat" ? navItems.map(({ children: _sub, ...item }) => item) : navItems,
517
+ flatten: meta.navShape === "flat"
518
+ },
519
+ // The app's own workspace is NOT the organization — it is a per-app
520
+ // bootstrap account with a different id in every app. Showing it while
521
+ // the chrome API resolves flashed the wrong org on every load ("not
522
+ // connected to the account"). Shimmer while in flight; only fall back
523
+ // to the app label once the chrome lookup has actually failed.
524
+ org: activeCoreOrg ? {
525
+ name: activeCoreOrg.name,
526
+ id: activeCoreOrg.id,
527
+ logoUrl: activeCoreOrg.logoUrl,
528
+ brandColor: activeCoreOrg.brandColor,
529
+ organizations: coreOrgs,
530
+ onSwitch: chrome?.setOrgId
531
+ } : chrome?.orgsLoading ? { name: null, id: null, loading: true } : { name: session.accountName, id: session.accountId },
532
+ mode,
533
+ footer,
534
+ linkComponent
303
535
  },
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
- );
536
+ betaBanner: meta.beta === false ? void 0 : {},
537
+ modeId: appMode,
538
+ contentWrapper: core?.contentWrapper ?? granted("contentWrapper"),
539
+ topBar: {
540
+ // Core's pages publish their own identity; every other app is one
541
+ // product surface and takes its registry title.
542
+ title: core?.page?.title ?? (grants?.includes("title") ? data?.frame?.title : void 0) ?? meta.title,
543
+ breadcrumbs,
544
+ user: {
545
+ name: identity.userName,
546
+ email: identity.email,
547
+ // The chrome API's avatar wins; the app may supply a fallback chain
548
+ // (e.g. a stored memoji) for users with no uploaded photo.
549
+ avatarUrl: identity.avatarUrl ?? data?.fallbackAvatarUrl ?? null,
550
+ role: session.role
551
+ },
552
+ userMenu,
553
+ onSignOut: handleSignOut,
554
+ // The action cluster is IDENTICAL everywhere — core included. Page
555
+ // actions sit BEFORE it, never inside it, so the Alerts bell keeps its
556
+ // required position: farthest right, immediately before My Account.
557
+ actions: /* @__PURE__ */ jsxs(Fragment, { children: [
558
+ core?.page?.actions,
559
+ /* @__PURE__ */ jsx(ChromeTopBarActions, {})
560
+ ] })
561
+ },
562
+ children
563
+ }
564
+ )
565
+ ] });
317
566
  }
318
567
  var standardNavIcons = {
319
568
  home: FiHome,
@@ -790,6 +1039,6 @@ function useChromeMode(arg = {}) {
790
1039
  };
791
1040
  }
792
1041
 
793
- export { ChromeTopBarActions, POLITOGY_APPS, PolitogyAppChrome, PolitogyAppFrame, deriveBreadcrumbs, looksLikeEmail, resolveIdentity, standardNavIcons, useChromeFooter, useChromeIdentity, useChromeMode, useChromeState, useChromeUserMenu, useIsSuperAdmin };
1042
+ export { ChromeTopBarActions, LoadingScreen, POLITOGY_APPS, PolitogyAppChrome, PolitogyAppFrame, deriveBreadcrumbs, looksLikeEmail, resolveIdentity, standardNavIcons, useChromeFooter, useChromeIdentity, useChromeMode, useChromeState, useChromeUserMenu, useIsSuperAdmin };
794
1043
  //# sourceMappingURL=app-chrome.js.map
795
1044
  //# sourceMappingURL=app-chrome.js.map