@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.
- package/dist/components/main-layout.d.ts.map +1 -1
- package/dist/components/main-layout.js +59 -2
- package/dist/components/main-layout.js.map +1 -1
- package/dist/components/main-layout.stories.js +231 -0
- package/dist/components/main-layout.stories.js.map +1 -0
- package/dist/components/user-menu.d.ts.map +1 -1
- package/dist/components/user-menu.js +18 -23
- package/dist/components/user-menu.js.map +1 -1
- package/dist/runtime/chrome-context.d.ts +10 -0
- package/dist/runtime/chrome-context.d.ts.map +1 -1
- package/dist/runtime/chrome-context.js.map +1 -1
- package/dist/runtime/report-error-boundary.d.ts +34 -0
- package/dist/runtime/report-error-boundary.d.ts.map +1 -0
- package/dist/runtime/report-error-boundary.js +46 -0
- package/dist/runtime/report-error-boundary.js.map +1 -0
- package/dist/runtime/workspace-setup-dismissal.d.ts +1 -0
- package/dist/runtime/workspace-setup-dismissal.d.ts.map +1 -1
- package/dist/runtime/workspace-setup-dismissal.js +7 -0
- package/dist/runtime/workspace-setup-dismissal.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +6 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main-layout.d.ts","sourceRoot":"","sources":["../../src/components/main-layout.tsx"],"names":[],"mappings":"
|
|
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
|
|
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:
|
|
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 ?
|
|
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":"
|
|
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 {
|
|
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(
|
|
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 {
|
|
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;
|
|
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;
|
|
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"}
|