@shipfox/client-shell 24.0.0 → 26.0.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 +1 @@
1
- {"version":3,"file":"main-layout.d.ts","sourceRoot":"","sources":["../../src/components/main-layout.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,cAAc,CAAC;AAG9C,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,yBAAyB,CAAC;AAOxD,OAAO,QAAQ,wBAAwB,CAAC;IACtC,UAAU,qBAAqB;QAC7B,KAAK,CAAC,EAAE,UAAU,CAAC;KACpB;CACF;AAQD,wBAAgB,UAAU,CAAC,EACzB,UAAU,EACV,qBAA6B,GAC9B,EAAE;IACD,UAAU,EAAE,SAAS,WAAW,EAAE,CAAC;IACnC,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,+BAoCA"}
1
+ {"version":3,"file":"main-layout.d.ts","sourceRoot":"","sources":["../../src/components/main-layout.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,cAAc,CAAC;AAK9C,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,yBAAyB,CAAC;AAOxD,OAAO,QAAQ,wBAAwB,CAAC;IACtC,UAAU,qBAAqB;QAC7B,KAAK,CAAC,EAAE,UAAU,CAAC;KACpB;CACF;AAgBD,wBAAgB,UAAU,CAAC,EACzB,UAAU,EACV,qBAA6B,GAC9B,EAAE;IACD,UAAU,EAAE,SAAS,WAAW,EAAE,CAAC;IACnC,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,+BAoFA"}
@@ -1,8 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { FullPageLoader } from '@shipfox/react-ui/loader';
3
3
  import { Navigate, Outlet, useLocation, useMatches } from '@tanstack/react-router';
4
+ import { useEffect, useMemo, useRef, useState } from 'react';
4
5
  import { useMaybeActiveWorkspace } from '#runtime/active-workspace.js';
5
6
  import { useAuthState } from '#runtime/auth.js';
7
+ import { useChrome } from '#runtime/chrome-context.js';
8
+ import { ReportErrorBoundary } from '#runtime/report-error-boundary.js';
6
9
  import { parseWorkspaceProjectParams, useRouteParams } from '#runtime/route-inputs.js';
7
10
  import { WorkspaceUnavailablePage } from '#runtime/workspace-setup.js';
8
11
  import { FOCUSED_FRAME_CONTENT_CLASS_NAME } from './focused-frame.js';
@@ -13,12 +16,47 @@ const frameClassNames = {
13
16
  data: 'flex min-h-0 w-full flex-1 flex-col px-frame py-frame',
14
17
  focused: `${FOCUSED_FRAME_CONTENT_CLASS_NAME} px-frame py-frame`
15
18
  };
19
+ /**
20
+ * Minimum height of the reserved SessionBanner strip, in pixels. The layout
21
+ * reserves this much for the slot and the app-content viewport arithmetic
22
+ * starts from it; the rendered strip height is measured and feeds the
23
+ * arithmetic so taller banners keep the content area consistent.
24
+ */ const SESSION_BANNER_HEIGHT_PX = 40;
16
25
  export function MainLayout({ navigation, hideProjectNavigation = false }) {
17
26
  const auth = useAuthState();
18
27
  const workspace = useMaybeActiveWorkspace();
19
28
  const location = useLocation();
20
29
  const { projectSlug } = useRouteParams(parseWorkspaceProjectParams);
21
30
  const matches = useMatches();
31
+ const { SessionBanner } = useChrome();
32
+ const sessionBannerStripRef = useRef(null);
33
+ const [bannerFailed, setBannerFailed] = useState(false);
34
+ const [bannerHeightPx, setBannerHeightPx] = useState(()=>SessionBanner ? SESSION_BANNER_HEIGHT_PX : 0);
35
+ // Retry the banner only when the route or the slot identity changes; the key
36
+ // stays referentially stable across the onError/onRecovered state toggles so
37
+ // a persistently failing slot latches instead of being retried in a loop.
38
+ const bannerRetryKey = useMemo(()=>({
39
+ href: location.href,
40
+ slot: SessionBanner
41
+ }), [
42
+ location.href,
43
+ SessionBanner
44
+ ]);
45
+ // Keep the app-content deduction aligned with the rendered strip height.
46
+ useEffect(()=>{
47
+ if (!SessionBanner || bannerFailed) return;
48
+ const strip = sessionBannerStripRef.current;
49
+ if (!strip) return;
50
+ const observer = new ResizeObserver((entries)=>{
51
+ const entry = entries[0];
52
+ if (entry) setBannerHeightPx(entry.contentRect.height);
53
+ });
54
+ observer.observe(strip);
55
+ return ()=>observer.disconnect();
56
+ }, [
57
+ SessionBanner,
58
+ bannerFailed
59
+ ]);
22
60
  if (auth.isLoading) return /*#__PURE__*/ _jsx(FullPageLoader, {});
23
61
  if (!auth.isAuthenticated) {
24
62
  return /*#__PURE__*/ _jsx(Navigate, {
@@ -31,11 +69,27 @@ export function MainLayout({ navigation, hideProjectNavigation = false }) {
31
69
  }
32
70
  if (!workspace) return /*#__PURE__*/ _jsx(WorkspaceUnavailablePage, {});
33
71
  const frame = matches.reduce((current, match)=>match.staticData.frame ?? current, 'content');
34
- const appContentHeight = hideProjectNavigation ? '[--app-content-h:calc(100dvh_-_56px)]' : '[--app-content-h:calc(100dvh_-_96px)]';
72
+ const reservedHeightPx = bannerFailed ? 0 : bannerHeightPx;
73
+ const appContentHeight = hideProjectNavigation ? `calc(100dvh - ${56 + reservedHeightPx}px)` : `calc(100dvh - ${96 + reservedHeightPx}px)`;
35
74
  const isFullBleedFrame = frame === 'data';
75
+ const mainClassName = isFullBleedFrame ? 'flex min-h-0 flex-1 flex-col overflow-hidden' : 'flex-1 overflow-auto';
36
76
  return /*#__PURE__*/ _jsxs("div", {
37
77
  className: "h-screen w-full flex flex-col bg-background-subtle-base",
38
78
  children: [
79
+ SessionBanner ? /*#__PURE__*/ _jsx(ReportErrorBoundary, {
80
+ label: "Failed to render session banner.",
81
+ retryKey: bannerRetryKey,
82
+ onError: ()=>setBannerFailed(true),
83
+ onRecovered: ()=>setBannerFailed(false),
84
+ children: /*#__PURE__*/ _jsx("div", {
85
+ ref: sessionBannerStripRef,
86
+ className: "flex shrink-0 items-center bg-background-subtle-base",
87
+ style: {
88
+ minHeight: SESSION_BANNER_HEIGHT_PX
89
+ },
90
+ children: /*#__PURE__*/ _jsx(SessionBanner, {})
91
+ })
92
+ }) : undefined,
39
93
  /*#__PURE__*/ _jsx(NavBar, {
40
94
  hideProjectNavigation: hideProjectNavigation
41
95
  }),
@@ -44,7 +98,10 @@ export function MainLayout({ navigation, hideProjectNavigation = false }) {
44
98
  scope: projectSlug ? 'project' : 'workspace'
45
99
  }),
46
100
  /*#__PURE__*/ _jsx("main", {
47
- className: `${isFullBleedFrame ? 'flex min-h-0 flex-1 flex-col overflow-hidden' : 'flex-1 overflow-auto'} ${appContentHeight}`,
101
+ className: mainClassName,
102
+ style: {
103
+ '--app-content-h': appContentHeight
104
+ },
48
105
  children: /*#__PURE__*/ _jsx("div", {
49
106
  className: frameClassNames[frame],
50
107
  children: /*#__PURE__*/ _jsx(Outlet, {})
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/main-layout.tsx"],"sourcesContent":["import {FullPageLoader} from '@shipfox/react-ui/loader';\nimport {Navigate, Outlet, useLocation, useMatches} from '@tanstack/react-router';\nimport type {NavTabEntry} from '#contract.js';\nimport {useMaybeActiveWorkspace} from '#runtime/active-workspace.js';\nimport {useAuthState} from '#runtime/auth.js';\nimport type {RouteFrame} from '#runtime/route-frame.js';\nimport {parseWorkspaceProjectParams, useRouteParams} from '#runtime/route-inputs.js';\nimport {WorkspaceUnavailablePage} from '#runtime/workspace-setup.js';\nimport {FOCUSED_FRAME_CONTENT_CLASS_NAME} from './focused-frame.js';\nimport {NavBar} from './nav-bar.js';\nimport {NavTabs} from './nav-tabs.js';\n\ndeclare module '@tanstack/react-router' {\n interface StaticDataRouteOption {\n frame?: RouteFrame;\n }\n}\n\nconst frameClassNames: Record<RouteFrame, string> = {\n content: 'mx-auto w-full max-w-[1120px] px-frame py-frame',\n data: 'flex min-h-0 w-full flex-1 flex-col px-frame py-frame',\n focused: `${FOCUSED_FRAME_CONTENT_CLASS_NAME} px-frame py-frame`,\n};\n\nexport function MainLayout({\n navigation,\n hideProjectNavigation = false,\n}: {\n navigation: readonly NavTabEntry[];\n hideProjectNavigation?: boolean;\n}) {\n const auth = useAuthState();\n const workspace = useMaybeActiveWorkspace();\n const location = useLocation();\n const {projectSlug} = useRouteParams(parseWorkspaceProjectParams);\n const matches = useMatches();\n if (auth.isLoading) return <FullPageLoader />;\n if (!auth.isAuthenticated) {\n return (\n <Navigate to={'/auth/login' as never} search={{redirect: location.href} as never} replace />\n );\n }\n if (!workspace) return <WorkspaceUnavailablePage />;\n const frame = matches.reduce<RouteFrame>(\n (current, match) => match.staticData.frame ?? current,\n 'content',\n );\n const appContentHeight = hideProjectNavigation\n ? '[--app-content-h:calc(100dvh_-_56px)]'\n : '[--app-content-h:calc(100dvh_-_96px)]';\n const isFullBleedFrame = frame === 'data';\n return (\n <div className=\"h-screen w-full flex flex-col bg-background-subtle-base\">\n <NavBar hideProjectNavigation={hideProjectNavigation} />\n {hideProjectNavigation ? undefined : (\n <NavTabs entries={navigation} scope={projectSlug ? 'project' : 'workspace'} />\n )}\n <main\n className={`${isFullBleedFrame ? 'flex min-h-0 flex-1 flex-col overflow-hidden' : 'flex-1 overflow-auto'} ${appContentHeight}`}\n >\n <div className={frameClassNames[frame]}>\n <Outlet />\n </div>\n </main>\n </div>\n );\n}\n"],"names":["FullPageLoader","Navigate","Outlet","useLocation","useMatches","useMaybeActiveWorkspace","useAuthState","parseWorkspaceProjectParams","useRouteParams","WorkspaceUnavailablePage","FOCUSED_FRAME_CONTENT_CLASS_NAME","NavBar","NavTabs","frameClassNames","content","data","focused","MainLayout","navigation","hideProjectNavigation","auth","workspace","location","projectSlug","matches","isLoading","isAuthenticated","to","search","redirect","href","replace","frame","reduce","current","match","staticData","appContentHeight","isFullBleedFrame","div","className","undefined","entries","scope","main"],"mappings":";AAAA,SAAQA,cAAc,QAAO,2BAA2B;AACxD,SAAQC,QAAQ,EAAEC,MAAM,EAAEC,WAAW,EAAEC,UAAU,QAAO,yBAAyB;AAEjF,SAAQC,uBAAuB,QAAO,+BAA+B;AACrE,SAAQC,YAAY,QAAO,mBAAmB;AAE9C,SAAQC,2BAA2B,EAAEC,cAAc,QAAO,2BAA2B;AACrF,SAAQC,wBAAwB,QAAO,8BAA8B;AACrE,SAAQC,gCAAgC,QAAO,qBAAqB;AACpE,SAAQC,MAAM,QAAO,eAAe;AACpC,SAAQC,OAAO,QAAO,gBAAgB;AAQtC,MAAMC,kBAA8C;IAClDC,SAAS;IACTC,MAAM;IACNC,SAAS,GAAGN,iCAAiC,kBAAkB,CAAC;AAClE;AAEA,OAAO,SAASO,WAAW,EACzBC,UAAU,EACVC,wBAAwB,KAAK,EAI9B;IACC,MAAMC,OAAOd;IACb,MAAMe,YAAYhB;IAClB,MAAMiB,WAAWnB;IACjB,MAAM,EAACoB,WAAW,EAAC,GAAGf,eAAeD;IACrC,MAAMiB,UAAUpB;IAChB,IAAIgB,KAAKK,SAAS,EAAE,qBAAO,KAACzB;IAC5B,IAAI,CAACoB,KAAKM,eAAe,EAAE;QACzB,qBACE,KAACzB;YAAS0B,IAAI;YAAwBC,QAAQ;gBAACC,UAAUP,SAASQ,IAAI;YAAA;YAAYC,OAAO;;IAE7F;IACA,IAAI,CAACV,WAAW,qBAAO,KAACZ;IACxB,MAAMuB,QAAQR,QAAQS,MAAM,CAC1B,CAACC,SAASC,QAAUA,MAAMC,UAAU,CAACJ,KAAK,IAAIE,SAC9C;IAEF,MAAMG,mBAAmBlB,wBACrB,0CACA;IACJ,MAAMmB,mBAAmBN,UAAU;IACnC,qBACE,MAACO;QAAIC,WAAU;;0BACb,KAAC7B;gBAAOQ,uBAAuBA;;YAC9BA,wBAAwBsB,0BACvB,KAAC7B;gBAAQ8B,SAASxB;gBAAYyB,OAAOpB,cAAc,YAAY;;0BAEjE,KAACqB;gBACCJ,WAAW,GAAGF,mBAAmB,iDAAiD,uBAAuB,CAAC,EAAED,kBAAkB;0BAE9H,cAAA,KAACE;oBAAIC,WAAW3B,eAAe,CAACmB,MAAM;8BACpC,cAAA,KAAC9B;;;;;AAKX"}
1
+ {"version":3,"sources":["../../src/components/main-layout.tsx"],"sourcesContent":["import {FullPageLoader} from '@shipfox/react-ui/loader';\nimport {Navigate, Outlet, useLocation, useMatches} from '@tanstack/react-router';\nimport {type CSSProperties, useEffect, useMemo, useRef, useState} from 'react';\nimport type {NavTabEntry} from '#contract.js';\nimport {useMaybeActiveWorkspace} from '#runtime/active-workspace.js';\nimport {useAuthState} from '#runtime/auth.js';\nimport {useChrome} from '#runtime/chrome-context.js';\nimport {ReportErrorBoundary} from '#runtime/report-error-boundary.js';\nimport type {RouteFrame} from '#runtime/route-frame.js';\nimport {parseWorkspaceProjectParams, useRouteParams} from '#runtime/route-inputs.js';\nimport {WorkspaceUnavailablePage} from '#runtime/workspace-setup.js';\nimport {FOCUSED_FRAME_CONTENT_CLASS_NAME} from './focused-frame.js';\nimport {NavBar} from './nav-bar.js';\nimport {NavTabs} from './nav-tabs.js';\n\ndeclare module '@tanstack/react-router' {\n interface StaticDataRouteOption {\n frame?: RouteFrame;\n }\n}\n\nconst frameClassNames: Record<RouteFrame, string> = {\n content: 'mx-auto w-full max-w-[1120px] px-frame py-frame',\n data: 'flex min-h-0 w-full flex-1 flex-col px-frame py-frame',\n focused: `${FOCUSED_FRAME_CONTENT_CLASS_NAME} px-frame py-frame`,\n};\n\n/**\n * Minimum height of the reserved SessionBanner strip, in pixels. The layout\n * reserves this much for the slot and the app-content viewport arithmetic\n * starts from it; the rendered strip height is measured and feeds the\n * arithmetic so taller banners keep the content area consistent.\n */\nconst SESSION_BANNER_HEIGHT_PX = 40;\n\nexport function MainLayout({\n navigation,\n hideProjectNavigation = false,\n}: {\n navigation: readonly NavTabEntry[];\n hideProjectNavigation?: boolean;\n}) {\n const auth = useAuthState();\n const workspace = useMaybeActiveWorkspace();\n const location = useLocation();\n const {projectSlug} = useRouteParams(parseWorkspaceProjectParams);\n const matches = useMatches();\n const {SessionBanner} = useChrome();\n const sessionBannerStripRef = useRef<HTMLDivElement | null>(null);\n const [bannerFailed, setBannerFailed] = useState(false);\n const [bannerHeightPx, setBannerHeightPx] = useState(() =>\n SessionBanner ? SESSION_BANNER_HEIGHT_PX : 0,\n );\n // Retry the banner only when the route or the slot identity changes; the key\n // stays referentially stable across the onError/onRecovered state toggles so\n // a persistently failing slot latches instead of being retried in a loop.\n const bannerRetryKey = useMemo(\n () => ({href: location.href, slot: SessionBanner}),\n [location.href, SessionBanner],\n );\n\n // Keep the app-content deduction aligned with the rendered strip height.\n useEffect(() => {\n if (!SessionBanner || bannerFailed) return;\n const strip = sessionBannerStripRef.current;\n if (!strip) return;\n const observer = new ResizeObserver((entries) => {\n const entry = entries[0];\n if (entry) setBannerHeightPx(entry.contentRect.height);\n });\n observer.observe(strip);\n return () => observer.disconnect();\n }, [SessionBanner, bannerFailed]);\n\n if (auth.isLoading) return <FullPageLoader />;\n if (!auth.isAuthenticated) {\n return (\n <Navigate to={'/auth/login' as never} search={{redirect: location.href} as never} replace />\n );\n }\n if (!workspace) return <WorkspaceUnavailablePage />;\n const frame = matches.reduce<RouteFrame>(\n (current, match) => match.staticData.frame ?? current,\n 'content',\n );\n const reservedHeightPx = bannerFailed ? 0 : bannerHeightPx;\n const appContentHeight = hideProjectNavigation\n ? `calc(100dvh - ${56 + reservedHeightPx}px)`\n : `calc(100dvh - ${96 + reservedHeightPx}px)`;\n const isFullBleedFrame = frame === 'data';\n const mainClassName = isFullBleedFrame\n ? 'flex min-h-0 flex-1 flex-col overflow-hidden'\n : 'flex-1 overflow-auto';\n return (\n <div className=\"h-screen w-full flex flex-col bg-background-subtle-base\">\n {SessionBanner ? (\n <ReportErrorBoundary\n label=\"Failed to render session banner.\"\n retryKey={bannerRetryKey}\n onError={() => setBannerFailed(true)}\n onRecovered={() => setBannerFailed(false)}\n >\n <div\n ref={sessionBannerStripRef}\n className=\"flex shrink-0 items-center bg-background-subtle-base\"\n style={{minHeight: SESSION_BANNER_HEIGHT_PX}}\n >\n <SessionBanner />\n </div>\n </ReportErrorBoundary>\n ) : undefined}\n <NavBar hideProjectNavigation={hideProjectNavigation} />\n {hideProjectNavigation ? undefined : (\n <NavTabs entries={navigation} scope={projectSlug ? 'project' : 'workspace'} />\n )}\n <main\n className={mainClassName}\n style={{'--app-content-h': appContentHeight} as CSSProperties}\n >\n <div className={frameClassNames[frame]}>\n <Outlet />\n </div>\n </main>\n </div>\n );\n}\n"],"names":["FullPageLoader","Navigate","Outlet","useLocation","useMatches","useEffect","useMemo","useRef","useState","useMaybeActiveWorkspace","useAuthState","useChrome","ReportErrorBoundary","parseWorkspaceProjectParams","useRouteParams","WorkspaceUnavailablePage","FOCUSED_FRAME_CONTENT_CLASS_NAME","NavBar","NavTabs","frameClassNames","content","data","focused","SESSION_BANNER_HEIGHT_PX","MainLayout","navigation","hideProjectNavigation","auth","workspace","location","projectSlug","matches","SessionBanner","sessionBannerStripRef","bannerFailed","setBannerFailed","bannerHeightPx","setBannerHeightPx","bannerRetryKey","href","slot","strip","current","observer","ResizeObserver","entries","entry","contentRect","height","observe","disconnect","isLoading","isAuthenticated","to","search","redirect","replace","frame","reduce","match","staticData","reservedHeightPx","appContentHeight","isFullBleedFrame","mainClassName","div","className","label","retryKey","onError","onRecovered","ref","style","minHeight","undefined","scope","main"],"mappings":"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"}
@@ -0,0 +1,231 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Text } from '@shipfox/react-ui/typography';
3
+ import { QueryClient } from '@tanstack/react-query';
4
+ import { createMemoryHistory, createRouter, RouterProvider } from '@tanstack/react-router';
5
+ import { createStore } from 'jotai';
6
+ import { useEffect, useMemo, useState } from 'react';
7
+ import { expect, waitFor, within } from 'storybook/test';
8
+ import { composeClientFeatures } from '#compose/compose-client-features.js';
9
+ import { defineClientFeature } from '#contract.js';
10
+ import { assembleRouteTree } from '#runtime/assemble-route-tree.js';
11
+ import { authStateAtom } from '#runtime/auth.js';
12
+ import { ChromeProvider } from '#runtime/chrome-context.js';
13
+ import { defineRoute } from '#runtime/define-route.js';
14
+ import { ShellProviders } from '../testing/index.js';
15
+ const WORKSPACE = {
16
+ id: '00000000-0000-4000-8000-000000000001',
17
+ name: 'Acme Workspace',
18
+ slug: 'acme',
19
+ membershipId: '10000000-0000-4000-8000-000000000001'
20
+ };
21
+ const AUTH = {
22
+ status: 'authenticated',
23
+ token: 'story-access-token',
24
+ user: {
25
+ id: '00000000-0000-4000-8000-00000000000a',
26
+ email: 'demo@shipfox.dev'
27
+ },
28
+ workspaces: [
29
+ WORKSPACE
30
+ ],
31
+ isLoading: false,
32
+ isAuthenticated: true,
33
+ hasWorkspace: true
34
+ };
35
+ const SHELL_STORY_FEATURE = defineClientFeature({
36
+ id: 'acme.shell-story',
37
+ routes: [
38
+ {
39
+ path: '/w/$workspaceSlug/overview',
40
+ parent: 'workspaceLayout',
41
+ impl: 'overview'
42
+ },
43
+ {
44
+ path: '/w/$workspaceSlug/projects',
45
+ parent: 'workspaceLayout',
46
+ impl: 'projects'
47
+ },
48
+ {
49
+ path: '/w/$workspaceSlug/runners',
50
+ parent: 'workspaceLayout',
51
+ impl: 'runners'
52
+ }
53
+ ],
54
+ navigation: [
55
+ {
56
+ id: 'overview',
57
+ label: 'Overview',
58
+ to: '/w/$workspaceSlug/overview',
59
+ scope: 'workspace'
60
+ },
61
+ {
62
+ id: 'projects',
63
+ label: 'Projects',
64
+ to: '/w/$workspaceSlug/projects',
65
+ scope: 'workspace'
66
+ },
67
+ {
68
+ id: 'runners',
69
+ label: 'Runners',
70
+ to: '/w/$workspaceSlug/runners',
71
+ scope: 'workspace'
72
+ }
73
+ ]
74
+ });
75
+ /** Generic filler for the session-banner slot; impersonation UI never ships here. */ function DemoSessionBanner() {
76
+ return /*#__PURE__*/ _jsxs("div", {
77
+ className: "flex h-full items-center justify-center gap-cluster bg-background-neutral-base px-row text-xs font-medium text-foreground-neutral-base",
78
+ children: [
79
+ /*#__PURE__*/ _jsx("span", {
80
+ className: "size-8 rounded-full bg-background-highlight-base",
81
+ "aria-hidden": "true"
82
+ }),
83
+ "Session banner slot"
84
+ ]
85
+ });
86
+ }
87
+ function OverviewPage() {
88
+ return /*#__PURE__*/ _jsxs("div", {
89
+ className: "flex flex-col gap-cluster",
90
+ children: [
91
+ /*#__PURE__*/ _jsx(Text, {
92
+ size: "md",
93
+ className: "text-foreground-neutral-base",
94
+ children: "Overview"
95
+ }),
96
+ /*#__PURE__*/ _jsx(Text, {
97
+ size: "sm",
98
+ className: "text-foreground-neutral-muted",
99
+ children: "Workspace content rendered inside the shell frame below the navigation chrome."
100
+ })
101
+ ]
102
+ });
103
+ }
104
+ function MainLayoutStory({ withSessionBanner, hideProjectNavigation }) {
105
+ const queryClient = useMemo(()=>new QueryClient({
106
+ defaultOptions: {
107
+ queries: {
108
+ retry: false
109
+ }
110
+ }
111
+ }), []);
112
+ const store = useMemo(()=>{
113
+ const nextStore = createStore();
114
+ nextStore.set(authStateAtom, AUTH);
115
+ return nextStore;
116
+ }, []);
117
+ const chrome = useMemo(()=>({
118
+ ProjectBreadcrumb: ()=>null,
119
+ projectSlugResolver: async ()=>'project',
120
+ ...withSessionBanner ? {
121
+ SessionBanner: DemoSessionBanner
122
+ } : {}
123
+ }), [
124
+ withSessionBanner
125
+ ]);
126
+ const [router, setRouter] = useState(null);
127
+ useEffect(()=>{
128
+ let cancelled = false;
129
+ void (async ()=>{
130
+ const composition = composeClientFeatures([
131
+ SHELL_STORY_FEATURE
132
+ ]);
133
+ const routeTree = await assembleRouteTree(composition.routes, {
134
+ layouts: composition.layouts,
135
+ resolveImpl: ()=>defineRoute({
136
+ staticData: {
137
+ frame: 'content'
138
+ },
139
+ component: OverviewPage
140
+ }),
141
+ navigation: composition.navigation,
142
+ settingsSections: composition.settingsSections
143
+ });
144
+ if (cancelled) return;
145
+ setRouter(createRouter({
146
+ routeTree,
147
+ history: createMemoryHistory({
148
+ initialEntries: [
149
+ '/w/acme/overview'
150
+ ]
151
+ }),
152
+ context: {
153
+ auth: AUTH,
154
+ queryClient,
155
+ workspaceSetup: async ()=>({
156
+ hideProjectNavigation
157
+ }),
158
+ projectSlugResolver: chrome.projectSlugResolver
159
+ }
160
+ }));
161
+ })();
162
+ return ()=>{
163
+ cancelled = true;
164
+ };
165
+ }, [
166
+ queryClient,
167
+ chrome,
168
+ hideProjectNavigation
169
+ ]);
170
+ // The provider stack mounts on the first render, before the async router
171
+ // assembly resolves, so its ThemeProvider commits together with the preview
172
+ // decorator's ThemeProvider. React flushes child effects before the parent's
173
+ // on mount, so the preview's theme (the Argos mode) is applied last and stays
174
+ // authoritative; if the stack mounted only after the router resolved, its
175
+ // system-default ThemeProvider would clobber the mode's dark class and every
176
+ // snapshot would render light. The router content is gated below instead.
177
+ return /*#__PURE__*/ _jsx(ChromeProvider, {
178
+ chrome: chrome,
179
+ children: /*#__PURE__*/ _jsx(ShellProviders, {
180
+ features: [
181
+ SHELL_STORY_FEATURE
182
+ ],
183
+ queryClient: queryClient,
184
+ store: store,
185
+ children: router ? /*#__PURE__*/ _jsx(RouterProvider, {
186
+ router: router
187
+ }) : null
188
+ })
189
+ });
190
+ }
191
+ const meta = {
192
+ title: 'Shell/MainLayout',
193
+ component: MainLayoutStory,
194
+ parameters: {
195
+ layout: 'fullscreen'
196
+ },
197
+ args: {
198
+ withSessionBanner: true,
199
+ hideProjectNavigation: false
200
+ }
201
+ };
202
+ export default meta;
203
+ export const Playground = {
204
+ play: async ({ canvasElement })=>{
205
+ const canvas = within(canvasElement);
206
+ const main = await canvas.findByRole('main');
207
+ // The Argos dark mode must stay authoritative on the preview document; the
208
+ // shell provider stack's system-default ThemeProvider would otherwise leave
209
+ // snapshots light (see the MainLayoutStory mount-order comment above).
210
+ expect(document.documentElement.classList.contains('dark')).toBe(true);
211
+ await waitFor(()=>{
212
+ const banner = canvas.getByText('Session banner slot');
213
+ const strip = banner.parentElement;
214
+ expect(strip).not.toBeNull();
215
+ const expectedHeight = `calc(100dvh - ${96 + Math.round(strip.getBoundingClientRect().height)}px)`;
216
+ expect(main.style.getPropertyValue('--app-content-h')).toBe(expectedHeight);
217
+ });
218
+ }
219
+ };
220
+ export const WithoutBanner = {
221
+ args: {
222
+ withSessionBanner: false
223
+ }
224
+ };
225
+ export const HideProjectNavigation = {
226
+ args: {
227
+ hideProjectNavigation: true
228
+ }
229
+ };
230
+
231
+ //# sourceMappingURL=main-layout.stories.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/main-layout.stories.tsx"],"sourcesContent":["import {Text} from '@shipfox/react-ui/typography';\nimport type {Meta, StoryObj} from '@storybook/react';\nimport {QueryClient} from '@tanstack/react-query';\nimport {\n type AnyRouter,\n createMemoryHistory,\n createRouter,\n RouterProvider,\n} from '@tanstack/react-router';\nimport {createStore} from 'jotai';\nimport {useEffect, useMemo, useState} from 'react';\nimport {expect, waitFor, within} from 'storybook/test';\nimport {composeClientFeatures} from '#compose/compose-client-features.js';\nimport {defineClientFeature} from '#contract.js';\nimport {assembleRouteTree} from '#runtime/assemble-route-tree.js';\nimport {type AuthStateValue, authStateAtom} from '#runtime/auth.js';\nimport {ChromeProvider, type ChromeSlots} from '#runtime/chrome-context.js';\nimport {defineRoute} from '#runtime/define-route.js';\nimport {ShellProviders} from '../testing/index.js';\n\nconst WORKSPACE = {\n id: '00000000-0000-4000-8000-000000000001',\n name: 'Acme Workspace',\n slug: 'acme',\n membershipId: '10000000-0000-4000-8000-000000000001',\n};\n\nconst AUTH: AuthStateValue = {\n status: 'authenticated',\n token: 'story-access-token',\n user: {id: '00000000-0000-4000-8000-00000000000a', email: 'demo@shipfox.dev'},\n workspaces: [WORKSPACE],\n isLoading: false,\n isAuthenticated: true,\n hasWorkspace: true,\n};\n\nconst SHELL_STORY_FEATURE = defineClientFeature({\n id: 'acme.shell-story',\n routes: [\n {path: '/w/$workspaceSlug/overview', parent: 'workspaceLayout', impl: 'overview'},\n {path: '/w/$workspaceSlug/projects', parent: 'workspaceLayout', impl: 'projects'},\n {path: '/w/$workspaceSlug/runners', parent: 'workspaceLayout', impl: 'runners'},\n ],\n navigation: [\n {id: 'overview', label: 'Overview', to: '/w/$workspaceSlug/overview', scope: 'workspace'},\n {id: 'projects', label: 'Projects', to: '/w/$workspaceSlug/projects', scope: 'workspace'},\n {id: 'runners', label: 'Runners', to: '/w/$workspaceSlug/runners', scope: 'workspace'},\n ],\n});\n\n/** Generic filler for the session-banner slot; impersonation UI never ships here. */\nfunction DemoSessionBanner() {\n return (\n <div className=\"flex h-full items-center justify-center gap-cluster bg-background-neutral-base px-row text-xs font-medium text-foreground-neutral-base\">\n <span className=\"size-8 rounded-full bg-background-highlight-base\" aria-hidden=\"true\" />\n Session banner slot\n </div>\n );\n}\n\nfunction OverviewPage() {\n return (\n <div className=\"flex flex-col gap-cluster\">\n <Text size=\"md\" className=\"text-foreground-neutral-base\">\n Overview\n </Text>\n <Text size=\"sm\" className=\"text-foreground-neutral-muted\">\n Workspace content rendered inside the shell frame below the navigation chrome.\n </Text>\n </div>\n );\n}\n\nfunction MainLayoutStory({\n withSessionBanner,\n hideProjectNavigation,\n}: {\n withSessionBanner: boolean;\n hideProjectNavigation: boolean;\n}) {\n const queryClient = useMemo(\n () => new QueryClient({defaultOptions: {queries: {retry: false}}}),\n [],\n );\n const store = useMemo(() => {\n const nextStore = createStore();\n nextStore.set(authStateAtom, AUTH);\n return nextStore;\n }, []);\n const chrome = useMemo<ChromeSlots>(\n () => ({\n ProjectBreadcrumb: () => null,\n projectSlugResolver: async () => 'project',\n ...(withSessionBanner ? {SessionBanner: DemoSessionBanner} : {}),\n }),\n [withSessionBanner],\n );\n const [router, setRouter] = useState<AnyRouter | null>(null);\n\n useEffect(() => {\n let cancelled = false;\n void (async () => {\n const composition = composeClientFeatures([SHELL_STORY_FEATURE]);\n const routeTree = await assembleRouteTree(composition.routes, {\n layouts: composition.layouts,\n resolveImpl: () => defineRoute({staticData: {frame: 'content'}, component: OverviewPage}),\n navigation: composition.navigation,\n settingsSections: composition.settingsSections,\n });\n if (cancelled) return;\n setRouter(\n createRouter({\n routeTree,\n history: createMemoryHistory({initialEntries: ['/w/acme/overview']}),\n context: {\n auth: AUTH,\n queryClient,\n workspaceSetup: async () => ({hideProjectNavigation}),\n projectSlugResolver: chrome.projectSlugResolver,\n },\n }),\n );\n })();\n return () => {\n cancelled = true;\n };\n }, [queryClient, chrome, hideProjectNavigation]);\n\n // The provider stack mounts on the first render, before the async router\n // assembly resolves, so its ThemeProvider commits together with the preview\n // decorator's ThemeProvider. React flushes child effects before the parent's\n // on mount, so the preview's theme (the Argos mode) is applied last and stays\n // authoritative; if the stack mounted only after the router resolved, its\n // system-default ThemeProvider would clobber the mode's dark class and every\n // snapshot would render light. The router content is gated below instead.\n return (\n <ChromeProvider chrome={chrome}>\n <ShellProviders features={[SHELL_STORY_FEATURE]} queryClient={queryClient} store={store}>\n {router ? <RouterProvider router={router} /> : null}\n </ShellProviders>\n </ChromeProvider>\n );\n}\n\nconst meta = {\n title: 'Shell/MainLayout',\n component: MainLayoutStory,\n parameters: {\n layout: 'fullscreen',\n },\n args: {\n withSessionBanner: true,\n hideProjectNavigation: false,\n },\n} satisfies Meta<typeof MainLayoutStory>;\n\nexport default meta;\ntype Story = StoryObj<typeof meta>;\n\nexport const Playground: Story = {\n play: async ({canvasElement}) => {\n const canvas = within(canvasElement);\n const main = await canvas.findByRole('main');\n // The Argos dark mode must stay authoritative on the preview document; the\n // shell provider stack's system-default ThemeProvider would otherwise leave\n // snapshots light (see the MainLayoutStory mount-order comment above).\n expect(document.documentElement.classList.contains('dark')).toBe(true);\n await waitFor(() => {\n const banner = canvas.getByText('Session banner slot');\n const strip = banner.parentElement;\n expect(strip).not.toBeNull();\n const expectedHeight = `calc(100dvh - ${96 + Math.round((strip as HTMLElement).getBoundingClientRect().height)}px)`;\n expect(main.style.getPropertyValue('--app-content-h')).toBe(expectedHeight);\n });\n },\n};\n\nexport const WithoutBanner: Story = {\n args: {\n withSessionBanner: false,\n },\n};\n\nexport const HideProjectNavigation: Story = {\n args: {\n hideProjectNavigation: true,\n },\n};\n"],"names":["Text","QueryClient","createMemoryHistory","createRouter","RouterProvider","createStore","useEffect","useMemo","useState","expect","waitFor","within","composeClientFeatures","defineClientFeature","assembleRouteTree","authStateAtom","ChromeProvider","defineRoute","ShellProviders","WORKSPACE","id","name","slug","membershipId","AUTH","status","token","user","email","workspaces","isLoading","isAuthenticated","hasWorkspace","SHELL_STORY_FEATURE","routes","path","parent","impl","navigation","label","to","scope","DemoSessionBanner","div","className","span","aria-hidden","OverviewPage","size","MainLayoutStory","withSessionBanner","hideProjectNavigation","queryClient","defaultOptions","queries","retry","store","nextStore","set","chrome","ProjectBreadcrumb","projectSlugResolver","SessionBanner","router","setRouter","cancelled","composition","routeTree","layouts","resolveImpl","staticData","frame","component","settingsSections","history","initialEntries","context","auth","workspaceSetup","features","meta","title","parameters","layout","args","Playground","play","canvasElement","canvas","main","findByRole","document","documentElement","classList","contains","toBe","banner","getByText","strip","parentElement","not","toBeNull","expectedHeight","Math","round","getBoundingClientRect","height","style","getPropertyValue","WithoutBanner","HideProjectNavigation"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"user-menu.d.ts","sourceRoot":"","sources":["../../src/components/user-menu.tsx"],"names":[],"mappings":"AA0CA,wBAAgB,QAAQ,gCA2CvB"}
1
+ {"version":3,"file":"user-menu.d.ts","sourceRoot":"","sources":["../../src/components/user-menu.tsx"],"names":[],"mappings":"AAyBA,wBAAgB,QAAQ,gCAuDvB"}
@@ -2,10 +2,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Avatar } from '@shipfox/react-ui/avatar';
3
3
  import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@shipfox/react-ui/dropdown-menu';
4
4
  import { useTheme } from '@shipfox/react-ui/hooks';
5
- import { Link } from '@tanstack/react-router';
6
- import { Component } from 'react';
5
+ import { Link, useLocation } from '@tanstack/react-router';
6
+ import { useMemo } from 'react';
7
7
  import { useAuthState } from '#runtime/auth.js';
8
8
  import { useChrome } from '#runtime/chrome-context.js';
9
+ import { ReportErrorBoundary } from '#runtime/report-error-boundary.js';
9
10
  const themeOptions = [
10
11
  {
11
12
  value: 'light',
@@ -20,31 +21,23 @@ const themeOptions = [
20
21
  label: 'System'
21
22
  }
22
23
  ];
23
- let AccountMenuEntryBoundary = class AccountMenuEntryBoundary extends Component {
24
- static getDerivedStateFromError() {
25
- return {
26
- hasError: true
27
- };
28
- }
29
- componentDidCatch(error) {
30
- globalThis.reportError?.(new Error('Failed to render account menu entry.', {
31
- cause: error
32
- }));
33
- }
34
- render() {
35
- return this.state.hasError ? null : this.props.children;
36
- }
37
- constructor(...args){
38
- super(...args), this.state = {
39
- hasError: false
40
- };
41
- }
42
- };
43
24
  export function UserMenu() {
44
25
  const { user } = useAuthState();
45
26
  const { AccountMenuEntry } = useChrome();
46
27
  const { theme, setTheme } = useTheme();
28
+ const location = useLocation();
47
29
  const email = user?.email ?? '';
30
+ // Retry the account-menu slot only when the route or the slot identity
31
+ // changes, matching the session-banner boundary: the key stays referentially
32
+ // stable across rerenders so a persistently failing slot latches instead of
33
+ // being retried in a loop.
34
+ const accountMenuRetryKey = useMemo(()=>({
35
+ href: location.href,
36
+ slot: AccountMenuEntry
37
+ }), [
38
+ location.href,
39
+ AccountMenuEntry
40
+ ]);
48
41
  return /*#__PURE__*/ _jsxs(DropdownMenu, {
49
42
  children: [
50
43
  /*#__PURE__*/ _jsx(DropdownMenuTrigger, {
@@ -82,7 +75,9 @@ export function UserMenu() {
82
75
  children: option.label
83
76
  }, option.value))
84
77
  }),
85
- AccountMenuEntry ? /*#__PURE__*/ _jsx(AccountMenuEntryBoundary, {
78
+ AccountMenuEntry ? /*#__PURE__*/ _jsx(ReportErrorBoundary, {
79
+ label: "Failed to render account menu entry.",
80
+ retryKey: accountMenuRetryKey,
86
81
  children: /*#__PURE__*/ _jsx(AccountMenuEntry, {})
87
82
  }) : undefined,
88
83
  /*#__PURE__*/ _jsx(DropdownMenuSeparator, {}),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/user-menu.tsx"],"sourcesContent":["import {Avatar} from '@shipfox/react-ui/avatar';\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '@shipfox/react-ui/dropdown-menu';\nimport {useTheme} from '@shipfox/react-ui/hooks';\nimport type {Theme} from '@shipfox/react-ui/theme';\nimport {Link} from '@tanstack/react-router';\nimport {Component, type PropsWithChildren} from 'react';\nimport {useAuthState} from '#runtime/auth.js';\nimport {useChrome} from '#runtime/chrome-context.js';\n\nconst themeOptions: Array<{value: Theme; label: string}> = [\n {value: 'light', label: 'Light'},\n {value: 'dark', label: 'Dark'},\n {value: 'system', label: 'System'},\n];\n\ntype AccountMenuEntryBoundaryState = {hasError: boolean};\n\nclass AccountMenuEntryBoundary extends Component<PropsWithChildren, AccountMenuEntryBoundaryState> {\n override state: AccountMenuEntryBoundaryState = {hasError: false};\n\n static getDerivedStateFromError(): AccountMenuEntryBoundaryState {\n return {hasError: true};\n }\n\n override componentDidCatch(error: unknown): void {\n globalThis.reportError?.(new Error('Failed to render account menu entry.', {cause: error}));\n }\n\n override render() {\n return this.state.hasError ? null : this.props.children;\n }\n}\n\nexport function UserMenu() {\n const {user} = useAuthState();\n const {AccountMenuEntry} = useChrome();\n const {theme, setTheme} = useTheme();\n const email = user?.email ?? '';\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n aria-label=\"User menu\"\n className=\"rounded-full focus-visible:outline-none focus-visible:shadow-button-neutral-focus\"\n >\n <Avatar size=\"sm\" content=\"letters\" fallback={email} />\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" sideOffset={8} className=\"min-w-[220px]\">\n <DropdownMenuLabel className=\"text-xs text-foreground-neutral-muted truncate\">\n {email}\n </DropdownMenuLabel>\n <DropdownMenuSeparator />\n <DropdownMenuLabel className=\"text-xs text-foreground-neutral-muted\">\n Theme\n </DropdownMenuLabel>\n <DropdownMenuRadioGroup value={theme} onValueChange={(value) => setTheme(value as Theme)}>\n {themeOptions.map((option) => (\n <DropdownMenuRadioItem key={option.value} value={option.value}>\n {option.label}\n </DropdownMenuRadioItem>\n ))}\n </DropdownMenuRadioGroup>\n {AccountMenuEntry ? (\n <AccountMenuEntryBoundary>\n <AccountMenuEntry />\n </AccountMenuEntryBoundary>\n ) : undefined}\n <DropdownMenuSeparator />\n <DropdownMenuItem asChild>\n <Link to={'/auth/logout' as never}>Logout</Link>\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n"],"names":["Avatar","DropdownMenu","DropdownMenuContent","DropdownMenuItem","DropdownMenuLabel","DropdownMenuRadioGroup","DropdownMenuRadioItem","DropdownMenuSeparator","DropdownMenuTrigger","useTheme","Link","Component","useAuthState","useChrome","themeOptions","value","label","AccountMenuEntryBoundary","getDerivedStateFromError","hasError","componentDidCatch","error","globalThis","reportError","Error","cause","render","state","props","children","UserMenu","user","AccountMenuEntry","theme","setTheme","email","asChild","button","type","aria-label","className","size","content","fallback","align","sideOffset","onValueChange","map","option","undefined","to"],"mappings":";AAAA,SAAQA,MAAM,QAAO,2BAA2B;AAChD,SACEC,YAAY,EACZC,mBAAmB,EACnBC,gBAAgB,EAChBC,iBAAiB,EACjBC,sBAAsB,EACtBC,qBAAqB,EACrBC,qBAAqB,EACrBC,mBAAmB,QACd,kCAAkC;AACzC,SAAQC,QAAQ,QAAO,0BAA0B;AAEjD,SAAQC,IAAI,QAAO,yBAAyB;AAC5C,SAAQC,SAAS,QAA+B,QAAQ;AACxD,SAAQC,YAAY,QAAO,mBAAmB;AAC9C,SAAQC,SAAS,QAAO,6BAA6B;AAErD,MAAMC,eAAqD;IACzD;QAACC,OAAO;QAASC,OAAO;IAAO;IAC/B;QAACD,OAAO;QAAQC,OAAO;IAAM;IAC7B;QAACD,OAAO;QAAUC,OAAO;IAAQ;CAClC;AAID,IAAA,AAAMC,2BAAN,MAAMA,iCAAiCN;IAGrC,OAAOO,2BAA0D;QAC/D,OAAO;YAACC,UAAU;QAAI;IACxB;IAESC,kBAAkBC,KAAc,EAAQ;QAC/CC,WAAWC,WAAW,GAAG,IAAIC,MAAM,wCAAwC;YAACC,OAAOJ;QAAK;IAC1F;IAESK,SAAS;QAChB,OAAO,IAAI,CAACC,KAAK,CAACR,QAAQ,GAAG,OAAO,IAAI,CAACS,KAAK,CAACC,QAAQ;IACzD;;QAbF,qBACWF,QAAuC;YAACR,UAAU;QAAK;;AAalE;AAEA,OAAO,SAASW;IACd,MAAM,EAACC,IAAI,EAAC,GAAGnB;IACf,MAAM,EAACoB,gBAAgB,EAAC,GAAGnB;IAC3B,MAAM,EAACoB,KAAK,EAAEC,QAAQ,EAAC,GAAGzB;IAC1B,MAAM0B,QAAQJ,MAAMI,SAAS;IAC7B,qBACE,MAAClC;;0BACC,KAACO;gBAAoB4B,OAAO;0BAC1B,cAAA,KAACC;oBACCC,MAAK;oBACLC,cAAW;oBACXC,WAAU;8BAEV,cAAA,KAACxC;wBAAOyC,MAAK;wBAAKC,SAAQ;wBAAUC,UAAUR;;;;0BAGlD,MAACjC;gBAAoB0C,OAAM;gBAAMC,YAAY;gBAAGL,WAAU;;kCACxD,KAACpC;wBAAkBoC,WAAU;kCAC1BL;;kCAEH,KAAC5B;kCACD,KAACH;wBAAkBoC,WAAU;kCAAwC;;kCAGrE,KAACnC;wBAAuBU,OAAOkB;wBAAOa,eAAe,CAAC/B,QAAUmB,SAASnB;kCACtED,aAAaiC,GAAG,CAAC,CAACC,uBACjB,KAAC1C;gCAAyCS,OAAOiC,OAAOjC,KAAK;0CAC1DiC,OAAOhC,KAAK;+BADagC,OAAOjC,KAAK;;oBAK3CiB,iCACC,KAACf;kCACC,cAAA,KAACe;yBAEDiB;kCACJ,KAAC1C;kCACD,KAACJ;wBAAiBiC,OAAO;kCACvB,cAAA,KAAC1B;4BAAKwC,IAAI;sCAAyB;;;;;;;AAK7C"}
1
+ {"version":3,"sources":["../../src/components/user-menu.tsx"],"sourcesContent":["import {Avatar} from '@shipfox/react-ui/avatar';\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '@shipfox/react-ui/dropdown-menu';\nimport {useTheme} from '@shipfox/react-ui/hooks';\nimport type {Theme} from '@shipfox/react-ui/theme';\nimport {Link, useLocation} from '@tanstack/react-router';\nimport {useMemo} from 'react';\nimport {useAuthState} from '#runtime/auth.js';\nimport {useChrome} from '#runtime/chrome-context.js';\nimport {ReportErrorBoundary} from '#runtime/report-error-boundary.js';\n\nconst themeOptions: Array<{value: Theme; label: string}> = [\n {value: 'light', label: 'Light'},\n {value: 'dark', label: 'Dark'},\n {value: 'system', label: 'System'},\n];\n\nexport function UserMenu() {\n const {user} = useAuthState();\n const {AccountMenuEntry} = useChrome();\n const {theme, setTheme} = useTheme();\n const location = useLocation();\n const email = user?.email ?? '';\n // Retry the account-menu slot only when the route or the slot identity\n // changes, matching the session-banner boundary: the key stays referentially\n // stable across rerenders so a persistently failing slot latches instead of\n // being retried in a loop.\n const accountMenuRetryKey = useMemo(\n () => ({href: location.href, slot: AccountMenuEntry}),\n [location.href, AccountMenuEntry],\n );\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n aria-label=\"User menu\"\n className=\"rounded-full focus-visible:outline-none focus-visible:shadow-button-neutral-focus\"\n >\n <Avatar size=\"sm\" content=\"letters\" fallback={email} />\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" sideOffset={8} className=\"min-w-[220px]\">\n <DropdownMenuLabel className=\"text-xs text-foreground-neutral-muted truncate\">\n {email}\n </DropdownMenuLabel>\n <DropdownMenuSeparator />\n <DropdownMenuLabel className=\"text-xs text-foreground-neutral-muted\">\n Theme\n </DropdownMenuLabel>\n <DropdownMenuRadioGroup value={theme} onValueChange={(value) => setTheme(value as Theme)}>\n {themeOptions.map((option) => (\n <DropdownMenuRadioItem key={option.value} value={option.value}>\n {option.label}\n </DropdownMenuRadioItem>\n ))}\n </DropdownMenuRadioGroup>\n {AccountMenuEntry ? (\n <ReportErrorBoundary\n label=\"Failed to render account menu entry.\"\n retryKey={accountMenuRetryKey}\n >\n <AccountMenuEntry />\n </ReportErrorBoundary>\n ) : undefined}\n <DropdownMenuSeparator />\n <DropdownMenuItem asChild>\n <Link to={'/auth/logout' as never}>Logout</Link>\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n"],"names":["Avatar","DropdownMenu","DropdownMenuContent","DropdownMenuItem","DropdownMenuLabel","DropdownMenuRadioGroup","DropdownMenuRadioItem","DropdownMenuSeparator","DropdownMenuTrigger","useTheme","Link","useLocation","useMemo","useAuthState","useChrome","ReportErrorBoundary","themeOptions","value","label","UserMenu","user","AccountMenuEntry","theme","setTheme","location","email","accountMenuRetryKey","href","slot","asChild","button","type","aria-label","className","size","content","fallback","align","sideOffset","onValueChange","map","option","retryKey","undefined","to"],"mappings":";AAAA,SAAQA,MAAM,QAAO,2BAA2B;AAChD,SACEC,YAAY,EACZC,mBAAmB,EACnBC,gBAAgB,EAChBC,iBAAiB,EACjBC,sBAAsB,EACtBC,qBAAqB,EACrBC,qBAAqB,EACrBC,mBAAmB,QACd,kCAAkC;AACzC,SAAQC,QAAQ,QAAO,0BAA0B;AAEjD,SAAQC,IAAI,EAAEC,WAAW,QAAO,yBAAyB;AACzD,SAAQC,OAAO,QAAO,QAAQ;AAC9B,SAAQC,YAAY,QAAO,mBAAmB;AAC9C,SAAQC,SAAS,QAAO,6BAA6B;AACrD,SAAQC,mBAAmB,QAAO,oCAAoC;AAEtE,MAAMC,eAAqD;IACzD;QAACC,OAAO;QAASC,OAAO;IAAO;IAC/B;QAACD,OAAO;QAAQC,OAAO;IAAM;IAC7B;QAACD,OAAO;QAAUC,OAAO;IAAQ;CAClC;AAED,OAAO,SAASC;IACd,MAAM,EAACC,IAAI,EAAC,GAAGP;IACf,MAAM,EAACQ,gBAAgB,EAAC,GAAGP;IAC3B,MAAM,EAACQ,KAAK,EAAEC,QAAQ,EAAC,GAAGd;IAC1B,MAAMe,WAAWb;IACjB,MAAMc,QAAQL,MAAMK,SAAS;IAC7B,uEAAuE;IACvE,6EAA6E;IAC7E,4EAA4E;IAC5E,2BAA2B;IAC3B,MAAMC,sBAAsBd,QAC1B,IAAO,CAAA;YAACe,MAAMH,SAASG,IAAI;YAAEC,MAAMP;QAAgB,CAAA,GACnD;QAACG,SAASG,IAAI;QAAEN;KAAiB;IAEnC,qBACE,MAACpB;;0BACC,KAACO;gBAAoBqB,OAAO;0BAC1B,cAAA,KAACC;oBACCC,MAAK;oBACLC,cAAW;oBACXC,WAAU;8BAEV,cAAA,KAACjC;wBAAOkC,MAAK;wBAAKC,SAAQ;wBAAUC,UAAUX;;;;0BAGlD,MAACvB;gBAAoBmC,OAAM;gBAAMC,YAAY;gBAAGL,WAAU;;kCACxD,KAAC7B;wBAAkB6B,WAAU;kCAC1BR;;kCAEH,KAAClB;kCACD,KAACH;wBAAkB6B,WAAU;kCAAwC;;kCAGrE,KAAC5B;wBAAuBY,OAAOK;wBAAOiB,eAAe,CAACtB,QAAUM,SAASN;kCACtED,aAAawB,GAAG,CAAC,CAACC,uBACjB,KAACnC;gCAAyCW,OAAOwB,OAAOxB,KAAK;0CAC1DwB,OAAOvB,KAAK;+BADauB,OAAOxB,KAAK;;oBAK3CI,iCACC,KAACN;wBACCG,OAAM;wBACNwB,UAAUhB;kCAEV,cAAA,KAACL;yBAEDsB;kCACJ,KAACpC;kCACD,KAACJ;wBAAiB0B,OAAO;kCACvB,cAAA,KAACnB;4BAAKkC,IAAI;sCAAyB;;;;;;;AAK7C"}
@@ -21,6 +21,16 @@ export interface ChromeSlots {
21
21
  * pages (while hideProjectNavigation is true).
22
22
  */
23
23
  WorkspaceSetupIndicator?: ComponentType;
24
+ /**
25
+ * Optional component the main layout renders above the navigation bar, inside
26
+ * an error boundary. The layout reserves a minimum-height strip for the slot,
27
+ * measures its rendered height, and accounts for it in the app-content
28
+ * viewport arithmetic, so a composing component taller than the minimum is
29
+ * still fully visible and the content area stays consistent. The layout
30
+ * renders nothing when the slot is absent, so a consumer that composes
31
+ * without the session banner is unaffected.
32
+ */
33
+ SessionBanner?: ComponentType;
24
34
  }
25
35
  export declare function ChromeProvider({ chrome, children, }: PropsWithChildren<{
26
36
  chrome: ChromeSlots | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"chrome-context.d.ts","sourceRoot":"","sources":["../../src/runtime/chrome-context.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,aAAa,EAAE,iBAAiB,EAAC,MAAM,OAAO,CAAC;AAE5D,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,qBAAqB,CAAC;AAE7D,MAAM,WAAW,WAAW;IAC1B,iBAAiB,EAAE,aAAa,CAAC;IACjC,mBAAmB,EAAE,mBAAmB,CAAC;IACzC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,aAAa,CAAC;IACjC;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,aAAa,CAAC;IACxC;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,aAAa,CAAC;CACzC;AAID,wBAAgB,cAAc,CAAC,EAC7B,MAAM,EACN,QAAQ,GACT,EAAE,iBAAiB,CAAC;IAAC,MAAM,EAAE,WAAW,GAAG,SAAS,CAAA;CAAC,CAAC,+BAEtD;AAED,wBAAgB,SAAS,IAAI,WAAW,CAIvC"}
1
+ {"version":3,"file":"chrome-context.d.ts","sourceRoot":"","sources":["../../src/runtime/chrome-context.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,aAAa,EAAE,iBAAiB,EAAC,MAAM,OAAO,CAAC;AAE5D,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,qBAAqB,CAAC;AAE7D,MAAM,WAAW,WAAW;IAC1B,iBAAiB,EAAE,aAAa,CAAC;IACjC,mBAAmB,EAAE,mBAAmB,CAAC;IACzC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,aAAa,CAAC;IACjC;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,aAAa,CAAC;IACxC;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,aAAa,CAAC;IACxC;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAID,wBAAgB,cAAc,CAAC,EAC7B,MAAM,EACN,QAAQ,GACT,EAAE,iBAAiB,CAAC;IAAC,MAAM,EAAE,WAAW,GAAG,SAAS,CAAA;CAAC,CAAC,+BAEtD;AAED,wBAAgB,SAAS,IAAI,WAAW,CAIvC"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/runtime/chrome-context.tsx"],"sourcesContent":["import type {ComponentType, PropsWithChildren} from 'react';\nimport {createContext, useContext} from 'react';\nimport type {ProjectSlugResolver} from './router-context.js';\n\nexport interface ChromeSlots {\n ProjectBreadcrumb: ComponentType;\n projectSlugResolver: ProjectSlugResolver;\n /**\n * Optional content rendered in the account menu before the shell-owned logout\n * action. The composing component must render one DropdownMenuItem or return\n * null, and owns whether this content renders from the current session.\n */\n AccountMenuEntry?: ComponentType;\n /**\n * Optional component the projects hub renders as its first panel. The hub\n * renders nothing when the slot is absent, so a consumer that composes\n * without the onboarding feature is unaffected.\n */\n WorkspaceSetupChecklist?: ComponentType;\n /**\n * Optional component the nav bar renders right of the breadcrumbs. The bar\n * renders nothing when the slot is absent, and never on the pre-project gate\n * pages (while hideProjectNavigation is true).\n */\n WorkspaceSetupIndicator?: ComponentType;\n}\n\nconst ChromeContext = createContext<ChromeSlots | undefined>(undefined);\n\nexport function ChromeProvider({\n chrome,\n children,\n}: PropsWithChildren<{chrome: ChromeSlots | undefined}>) {\n return <ChromeContext.Provider value={chrome}>{children}</ChromeContext.Provider>;\n}\n\nexport function useChrome(): ChromeSlots {\n const chrome = useContext(ChromeContext);\n if (!chrome) throw new Error('Client composition must provide browser chrome slots.');\n return chrome;\n}\n"],"names":["createContext","useContext","ChromeContext","undefined","ChromeProvider","chrome","children","Provider","value","useChrome","Error"],"mappings":";AACA,SAAQA,aAAa,EAAEC,UAAU,QAAO,QAAQ;AA0BhD,MAAMC,8BAAgBF,cAAuCG;AAE7D,OAAO,SAASC,eAAe,EAC7BC,MAAM,EACNC,QAAQ,EAC6C;IACrD,qBAAO,KAACJ,cAAcK,QAAQ;QAACC,OAAOH;kBAASC;;AACjD;AAEA,OAAO,SAASG;IACd,MAAMJ,SAASJ,WAAWC;IAC1B,IAAI,CAACG,QAAQ,MAAM,IAAIK,MAAM;IAC7B,OAAOL;AACT"}
1
+ {"version":3,"sources":["../../src/runtime/chrome-context.tsx"],"sourcesContent":["import type {ComponentType, PropsWithChildren} from 'react';\nimport {createContext, useContext} from 'react';\nimport type {ProjectSlugResolver} from './router-context.js';\n\nexport interface ChromeSlots {\n ProjectBreadcrumb: ComponentType;\n projectSlugResolver: ProjectSlugResolver;\n /**\n * Optional content rendered in the account menu before the shell-owned logout\n * action. The composing component must render one DropdownMenuItem or return\n * null, and owns whether this content renders from the current session.\n */\n AccountMenuEntry?: ComponentType;\n /**\n * Optional component the projects hub renders as its first panel. The hub\n * renders nothing when the slot is absent, so a consumer that composes\n * without the onboarding feature is unaffected.\n */\n WorkspaceSetupChecklist?: ComponentType;\n /**\n * Optional component the nav bar renders right of the breadcrumbs. The bar\n * renders nothing when the slot is absent, and never on the pre-project gate\n * pages (while hideProjectNavigation is true).\n */\n WorkspaceSetupIndicator?: ComponentType;\n /**\n * Optional component the main layout renders above the navigation bar, inside\n * an error boundary. The layout reserves a minimum-height strip for the slot,\n * measures its rendered height, and accounts for it in the app-content\n * viewport arithmetic, so a composing component taller than the minimum is\n * still fully visible and the content area stays consistent. The layout\n * renders nothing when the slot is absent, so a consumer that composes\n * without the session banner is unaffected.\n */\n SessionBanner?: ComponentType;\n}\n\nconst ChromeContext = createContext<ChromeSlots | undefined>(undefined);\n\nexport function ChromeProvider({\n chrome,\n children,\n}: PropsWithChildren<{chrome: ChromeSlots | undefined}>) {\n return <ChromeContext.Provider value={chrome}>{children}</ChromeContext.Provider>;\n}\n\nexport function useChrome(): ChromeSlots {\n const chrome = useContext(ChromeContext);\n if (!chrome) throw new Error('Client composition must provide browser chrome slots.');\n return chrome;\n}\n"],"names":["createContext","useContext","ChromeContext","undefined","ChromeProvider","chrome","children","Provider","value","useChrome","Error"],"mappings":";AACA,SAAQA,aAAa,EAAEC,UAAU,QAAO,QAAQ;AAoChD,MAAMC,8BAAgBF,cAAuCG;AAE7D,OAAO,SAASC,eAAe,EAC7BC,MAAM,EACNC,QAAQ,EAC6C;IACrD,qBAAO,KAACJ,cAAcK,QAAQ;QAACC,OAAOH;kBAASC;;AACjD;AAEA,OAAO,SAASG;IACd,MAAMJ,SAASJ,WAAWC;IAC1B,IAAI,CAACG,QAAQ,MAAM,IAAIK,MAAM;IAC7B,OAAOL;AACT"}
@@ -0,0 +1,34 @@
1
+ import { Component, type PropsWithChildren } from 'react';
2
+ export interface ReportErrorBoundaryProps extends PropsWithChildren {
3
+ /** Message reported when the guarded slot throws; names the slot in diagnostics. */
4
+ label: string;
5
+ /** Called after a render failure so an owner can collapse the reserved slot. */
6
+ onError?: () => void;
7
+ /** Called when the boundary clears the error to retry the slot. */
8
+ onRecovered?: () => void;
9
+ /**
10
+ * Stable value the owner changes only when the guarded slot should be
11
+ * retried. The boundary latches on failure and resets only when this value
12
+ * changes, so owner rerenders triggered by `onError`/`onRecovered` never
13
+ * retry the same failing slot in a loop.
14
+ */
15
+ retryKey?: unknown;
16
+ }
17
+ type ReportErrorBoundaryState = {
18
+ hasError: boolean;
19
+ };
20
+ /**
21
+ * Isolates an optional chrome slot from the rest of the shell. A render
22
+ * failure reports the error and renders nothing instead of unmounting the
23
+ * shell; the owner retries the slot by passing a new `retryKey`, so a
24
+ * transient failure recovers without a reload.
25
+ */
26
+ export declare class ReportErrorBoundary extends Component<ReportErrorBoundaryProps, ReportErrorBoundaryState> {
27
+ state: ReportErrorBoundaryState;
28
+ static getDerivedStateFromError(): ReportErrorBoundaryState;
29
+ componentDidCatch(error: unknown): void;
30
+ componentDidUpdate(prevProps: ReportErrorBoundaryProps): void;
31
+ render(): import("react").ReactNode;
32
+ }
33
+ export {};
34
+ //# sourceMappingURL=report-error-boundary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report-error-boundary.d.ts","sourceRoot":"","sources":["../../src/runtime/report-error-boundary.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAE,KAAK,iBAAiB,EAAC,MAAM,OAAO,CAAC;AAExD,MAAM,WAAW,wBAAyB,SAAQ,iBAAiB;IACjE,oFAAoF;IACpF,KAAK,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,mEAAmE;IACnE,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,KAAK,wBAAwB,GAAG;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAC,CAAC;AAYpD;;;;;GAKG;AACH,qBAAa,mBAAoB,SAAQ,SAAS,CAChD,wBAAwB,EACxB,wBAAwB,CACzB;IACU,KAAK,EAAE,wBAAwB,CAAqB;IAE7D,MAAM,CAAC,wBAAwB,IAAI,wBAAwB,CAE1D;IAEQ,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAG/C;IAEQ,kBAAkB,CAAC,SAAS,EAAE,wBAAwB,GAAG,IAAI,CAKrE;IAEQ,MAAM,8BAEd;CACF"}