feat(web): migrate application shell to TanStack Start
- replace the legacy client-only Vite bootstrap with TanStack Router routes, Start SSR, and Nitro output - compose the reusable layout, navigation, appearance, chats, and notifications blocks with application-owned data - persist UI preferences through server functions and initialize theme, sidebar, and breakpoint state before hydration - add React Query, hotkey, toaster, and responsive dashboard integration - add application-owned Lingui configuration and catalogs, mount the runtime provider, and lazily enable I18nDevtool in development - remove superseded HTML, main entry, theme provider, and application ESLint configuration
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
import type { ReactNode } from "react"
|
||||
import {
|
||||
HeadContent,
|
||||
Outlet,
|
||||
Scripts,
|
||||
createRootRoute,
|
||||
} from "@tanstack/react-router"
|
||||
import {
|
||||
BreakpointReactor,
|
||||
getBreakpointInitializationScript,
|
||||
} from "@workspace/ui/hooks/use-breakpoint"
|
||||
|
||||
import appCss from "@/styles/globals.css?url"
|
||||
import {
|
||||
AppearanceController,
|
||||
buildThemeStyleSheet,
|
||||
THEME_STYLE_ELEMENT_ID,
|
||||
type UiState,
|
||||
UiStateProvider,
|
||||
} from "@workspace/blocks/appearance"
|
||||
import { SidebarStateProvider } from "@workspace/blocks/layout"
|
||||
import { getUiState, setUiState } from "@/server/ui-state.functions"
|
||||
import { HotkeysProvider } from "@tanstack/react-hotkeys"
|
||||
import { TooltipProvider } from "@workspace/ui/components/tooltip"
|
||||
import { QueryClientProvider } from "@tanstack/react-query"
|
||||
import { AppI18nProvider } from "@/i18n/app-i18n-provider"
|
||||
import { queryClient } from "@/lib/query-client"
|
||||
|
||||
export const Route = createRootRoute({
|
||||
loader: () => getUiState(),
|
||||
head: () => ({
|
||||
meta: [
|
||||
{ charSet: "utf-8" },
|
||||
{
|
||||
name: "viewport",
|
||||
content: "width=device-width, initial-scale=1",
|
||||
},
|
||||
{ title: "Simple React App Kit" },
|
||||
],
|
||||
links: [
|
||||
{
|
||||
rel: "stylesheet",
|
||||
href: appCss,
|
||||
},
|
||||
],
|
||||
}),
|
||||
component: RootComponent,
|
||||
})
|
||||
|
||||
function RootComponent() {
|
||||
const initialState = Route.useLoaderData()
|
||||
|
||||
return (
|
||||
<RootDocument initialState={initialState}>
|
||||
<AppI18nProvider>
|
||||
<BreakpointReactor />
|
||||
<UiStateProvider
|
||||
initialState={initialState}
|
||||
onStateChange={(update) => setUiState({ data: update })}
|
||||
>
|
||||
<SidebarStateProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<HotkeysProvider>
|
||||
<TooltipProvider>
|
||||
<AppearanceController />
|
||||
<Outlet />
|
||||
</TooltipProvider>
|
||||
</HotkeysProvider>
|
||||
</QueryClientProvider>
|
||||
</SidebarStateProvider>
|
||||
</UiStateProvider>
|
||||
</AppI18nProvider>
|
||||
</RootDocument>
|
||||
)
|
||||
}
|
||||
|
||||
function getUiInitializationScript(state: UiState) {
|
||||
return `(() => {
|
||||
const theme = ${JSON.stringify(state["theme-mode"])};
|
||||
const resolvedTheme =
|
||||
theme === "system"
|
||||
? window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: theme;
|
||||
const root = document.documentElement;
|
||||
root.classList.remove("ui:light", "ui:dark");
|
||||
root.classList.add("ui:" + resolvedTheme);
|
||||
root.classList.toggle("ui:compacted", ${state["main-compacted"]});
|
||||
root.classList.toggle("ui:collapsed", ${state["sidebar-collapsed"]});
|
||||
root.style.colorScheme = resolvedTheme;
|
||||
})();`
|
||||
}
|
||||
|
||||
function RootDocument({
|
||||
children,
|
||||
initialState,
|
||||
}: Readonly<{
|
||||
children: ReactNode
|
||||
initialState: UiState
|
||||
}>) {
|
||||
const themeMode = initialState["theme-mode"]
|
||||
const className = [
|
||||
themeMode === "system" ? null : `ui:${themeMode}`,
|
||||
initialState["main-compacted"] ? "ui:compacted" : null,
|
||||
initialState["sidebar-collapsed"] ? "ui:collapsed" : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
|
||||
return (
|
||||
<html
|
||||
lang="zh-CN"
|
||||
suppressHydrationWarning
|
||||
className={className || undefined}
|
||||
style={themeMode === "system" ? undefined : { colorScheme: themeMode }}
|
||||
>
|
||||
<head>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: getBreakpointInitializationScript(),
|
||||
}}
|
||||
/>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: getUiInitializationScript(initialState),
|
||||
}}
|
||||
/>
|
||||
<HeadContent />
|
||||
<style
|
||||
id={THEME_STYLE_ELEMENT_ID}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: buildThemeStyleSheet(
|
||||
initialState["theme-light"],
|
||||
initialState["theme-dark"]
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
|
||||
import { DashboardPage } from "@/pages/dashboard-page"
|
||||
|
||||
export const Route = createFileRoute("/_app/$")({
|
||||
component: DashboardPage,
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
|
||||
import { DashboardPage } from "@/pages/dashboard-page"
|
||||
|
||||
export const Route = createFileRoute("/_app/")({
|
||||
component: DashboardPage,
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Outlet, createFileRoute } from "@tanstack/react-router"
|
||||
|
||||
import { AppLayout } from "@workspace/blocks/layout"
|
||||
|
||||
import { chatThreads } from "@/data/chats"
|
||||
import { primaryNavigationGroups } from "@/data/navigation"
|
||||
import {
|
||||
fetchMockNotifications,
|
||||
notificationsQueryKey,
|
||||
} from "@/data/notifications"
|
||||
|
||||
export const Route = createFileRoute("/_app")({
|
||||
component: ApplicationLayoutRoute,
|
||||
})
|
||||
|
||||
function ApplicationLayoutRoute() {
|
||||
return (
|
||||
<AppLayout
|
||||
chatThreads={chatThreads}
|
||||
navigationGroups={primaryNavigationGroups}
|
||||
notifications={{
|
||||
queryFn: fetchMockNotifications,
|
||||
queryKey: notificationsQueryKey,
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user