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:
Maofeng
2026-07-29 21:23:17 +08:00
parent 9e3f1d6b59
commit a9480ad9aa
32 changed files with 1555 additions and 309 deletions
+146
View File
@@ -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>
)
}
+7
View File
@@ -0,0 +1,7 @@
import { createFileRoute } from "@tanstack/react-router"
import { DashboardPage } from "@/pages/dashboard-page"
export const Route = createFileRoute("/_app/$")({
component: DashboardPage,
})
+7
View File
@@ -0,0 +1,7 @@
import { createFileRoute } from "@tanstack/react-router"
import { DashboardPage } from "@/pages/dashboard-page"
export const Route = createFileRoute("/_app/")({
component: DashboardPage,
})
+29
View File
@@ -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>
)
}