refactor(sidebar): split nav views and add router decorator
Some checks failed
Client CMS Check Build (NixCN CMS) TeamCity build failed
Backend Check Build (NixCN CMS) TeamCity build finished

Signed-off-by: Noa Virellia <noa@requiem.garden>
This commit is contained in:
2026-02-01 09:03:12 +08:00
parent 65d493b91b
commit ba29eb00d1
14 changed files with 119 additions and 82 deletions

View File

@@ -1,9 +1,21 @@
import type { Preview } from '@storybook/react-vite';
import type { Decorator, Preview } from '@storybook/react-vite';
import { ThemeProvider } from '../src/components/theme-provider';
import '../src/index.css';
import { createRootRoute, createRouter, RouterProvider } from '@tanstack/react-router';
const RouterDecorator: Decorator = (Story) => {
const rootRoute = createRootRoute({ component: () => <Story /> });
const routeTree = rootRoute;
const router = createRouter({ routeTree });
return <RouterProvider router={router} />;
};
const ThemeDecorator: Decorator = (Story) => {
return <ThemeProvider defaultTheme="dark"><Story /></ThemeProvider>;
};
const preview: Preview = {
decorators: [(Story) => <ThemeProvider defaultTheme="dark"><Story /></ThemeProvider >],
decorators: [RouterDecorator, ThemeDecorator],
parameters: {
controls: {
matchers: {

View File

@@ -1,20 +0,0 @@
import type { ReactNode } from 'react';
import React, { Suspense } from 'react';
export function withFallback<P extends object>(
Component: React.ComponentType<P>,
fallback: ReactNode,
) {
const Wrapped: React.FC<P> = (props) => {
return (
<Suspense fallback={fallback}>
<Component {...props} />
</Suspense>
);
};
Wrapped.displayName = `withFallback(${Component.displayName! || Component.name || 'Component'
})`;
return Wrapped;
}

View File

@@ -1,8 +1,8 @@
import type { NavData } from '@/lib/navData';
import * as React from 'react';
import NixOSLogo from '@/assets/nixos.svg?react';
import { NavMain } from '@/components/sidebar/nav-main';
import { NavSecondary } from '@/components/sidebar/nav-secondary';
import { NavMain } from '@/components/sidebar/nav-main.view';
import { NavSecondary } from '@/components/sidebar/nav-secondary.view';
import {
Sidebar,
SidebarContent,
@@ -12,9 +12,8 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from '@/components/ui/sidebar';
import { NavUser } from './nav-user';
export function AppSidebar({ navData, ...props }: React.ComponentProps<typeof Sidebar> & { navData: NavData }) {
export function AppSidebar({ navData, footerWidget, ...props }: React.ComponentProps<typeof Sidebar> & { navData: NavData; footerWidget: React.ReactNode }) {
return (
<Sidebar collapsible="offcanvas" {...props}>
<SidebarHeader>
@@ -37,7 +36,7 @@ export function AppSidebar({ navData, ...props }: React.ComponentProps<typeof Si
<NavSecondary items={navData.navSecondary} className="mt-auto" />
</SidebarContent>
<SidebarFooter>
<NavUser />
{footerWidget}
</SidebarFooter>
</Sidebar>
);

View File

@@ -0,0 +1,11 @@
import { useUserInfo } from '@/hooks/data/useUserInfo';
import { NavUserView } from './nav-user.view';
export function NavUserContainer() {
const { data } = useUserInfo();
return (
<NavUserView
user={data.data!}
/>
);
}

View File

@@ -0,0 +1,18 @@
import { IconDotsVertical } from '@tabler/icons-react';
import { SidebarMenuButton } from '../ui/sidebar';
import { Skeleton } from '../ui/skeleton';
export function NavUserSkeleton() {
return (
<SidebarMenuButton
size="lg"
>
<Skeleton className="h-8 w-8 rounded-lg" />
<div className="flex flex-col flex-1 gap-1">
<Skeleton className="h-3 w-16" />
<Skeleton className="h-3 w-24" />
</div>
<IconDotsVertical className="ml-auto size-4" />
</SidebarMenuButton>
);
}

View File

@@ -1,5 +1,6 @@
import { identicon } from '@dicebear/collection';
import type { ServiceUserUserInfoData } from '@/client';
import { identicon } from '@dicebear/collection';
import { createAvatar } from '@dicebear/core';
import {
IconDotsVertical,
@@ -25,15 +26,10 @@ import {
SidebarMenuItem,
useSidebar,
} from '@/components/ui/sidebar';
import { useUserInfo } from '@/hooks/data/useUserInfo';
import { logout } from '@/lib/token';
import { withFallback } from '../hoc/with-fallback';
import { Skeleton } from '../ui/skeleton';
export function NavUser_() {
export function NavUserView({ user }: { user: ServiceUserUserInfoData }) {
const { isMobile } = useSidebar();
const { data } = useUserInfo();
const user = data.data!;
const IdentIcon = useMemo(() => {
const avatar = createAvatar(identicon, {
@@ -94,20 +90,3 @@ export function NavUser_() {
</SidebarMenu>
);
}
function NavUserSkeleton() {
return (
<SidebarMenuButton
size="lg"
>
<Skeleton className="h-8 w-8 rounded-lg" />
<div className="flex flex-col flex-1 gap-1">
<Skeleton className="h-3 w-16" />
<Skeleton className="h-3 w-24" />
</div>
<IconDotsVertical className="ml-auto size-4" />
</SidebarMenuButton>
);
}
export const NavUser = withFallback(NavUser_, <NavUserSkeleton />);

View File

@@ -1,5 +1,8 @@
import { createFileRoute, Outlet, useRouterState } from '@tanstack/react-router';
import { AppSidebar } from '@/components/sidebar/app-sidebar';
import { Suspense } from 'react';
import { AppSidebar } from '@/components/sidebar/app-sidebar.view';
import { NavUserContainer } from '@/components/sidebar/nav-user.container';
import { NavUserSkeleton } from '@/components/sidebar/nav-user.skeletion';
import { SiteHeader } from '@/components/site-header';
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
import { navData } from '@/lib/navData';
@@ -27,7 +30,15 @@ function RouteComponent() {
} as React.CSSProperties
}
>
<AppSidebar navData={navData} variant="inset" />
<AppSidebar
navData={navData}
footerWidget={(
<Suspense fallback={<NavUserSkeleton />}>
<NavUserContainer />
</Suspense>
)}
variant="inset"
/>
<SidebarInset>
<SiteHeader title={title} />
<div className="flex flex-1 flex-col">

View File

@@ -0,0 +1,8 @@
export const user = {
username: 'nvirellia',
nickname: 'Noa Virellia',
subtitle: '天生骄傲',
email: 'noa@requiem.garden',
bio: '',
avatar: 'https://avatars.githubusercontent.com/u/54884471?v=4',
};

View File

@@ -1,12 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { NavUser_ } from '@/components/sidebar/nav-user';
const meta = {
title: 'NavUser',
component: NavUser_,
} satisfies Meta<typeof NavUser_>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {};

View File

@@ -1,5 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { EditProfileDialogView } from '@/components/profile/edit-profile.dialog.view';
import { user } from '../exampleUser';
const meta = {
title: 'Profile/EditDialog',
@@ -27,14 +28,7 @@ type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
user: {
username: 'nvirellia',
nickname: 'Noa Virellia',
subtitle: '天生骄傲',
email: 'noa@requiem.garden',
bio: '',
avatar: 'https://avatars.githubusercontent.com/u/54884471?v=4',
},
user,
updateProfile: async () => { },
},
parameters: {

View File

@@ -3,6 +3,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ProfileError } from '@/components/profile/profile.error';
import { ProfileSkeleton } from '@/components/profile/profile.skeleton';
import { ProfileView } from '@/components/profile/profile.view';
import { user } from '../exampleUser';
const queryClient = new QueryClient();
@@ -23,20 +24,13 @@ type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
user: {
username: 'nvirellia',
nickname: 'Noa Virellia',
subtitle: '天生骄傲',
email: 'noa@requiem.garden',
bio: '',
avatar: 'https://avatars.githubusercontent.com/u/54884471?v=4',
},
user,
onSaveBio: async () => Promise.resolve(),
},
};
export const Skeleton: Story = {
export const Loading: Story = {
render: () => <ProfileSkeleton />,
args: {
user: {},

View File

@@ -0,0 +1,43 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { AppSidebar } from '@/components/sidebar/app-sidebar.view';
import { NavUserSkeleton } from '@/components/sidebar/nav-user.skeletion';
import { NavUserView } from '@/components/sidebar/nav-user.view';
import { SidebarProvider } from '@/components/ui/sidebar';
import { navData } from '@/lib/navData';
import { user } from './exampleUser';
const meta = {
title: 'Navigation/Sidebar',
component: AppSidebar,
decorators: [
Story => (
<SidebarProvider
style={
{
'--sidebar-width': 'calc(var(--spacing) * 72)',
'--header-height': 'calc(var(--spacing) * 12)',
} as React.CSSProperties
}
>
<Story />
</SidebarProvider>
),
],
} satisfies Meta<typeof AppSidebar>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
navData,
footerWidget: <NavUserView user={user} />,
},
};
export const Loading: Story = {
args: {
navData,
footerWidget: <NavUserSkeleton />,
},
};