feat(web): перевести настройки, VPS-детали и журнал на вкладки из дизайна VPS-Tracker
Настройки — line-вкладки сверху вместо боковой навигации, детали VPS — line-вкладки и действия в шапке, журнал — общие CountedLineTabs, шапка страницы 20px. Детальные маршруты VPS и проекта вынесены из вложенности (vps_.$vpsId, projects_.$projectId): списки не рендерят Outlet, и детали не открывались. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b5300b9082
commit
e3403bcb56
6 files changed
+102
-148
No files matched your search
@@ -6,12 +6,12 @@ interface PageHeaderProps {
|
||||
actions?: ReactNode
|
||||
}
|
||||
|
||||
/** Page-level section header — etalon EvoBGP. */
|
||||
/** Page header — design «VPS-Tracker»: 20px title + muted sub, actions bottom-aligned right. */
|
||||
export function PageHeader({ title, description, actions }: PageHeaderProps) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-end md:justify-between">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<h1 className="text-xl font-semibold tracking-tight">{title}</h1>
|
||||
{description ? (
|
||||
<p className="text-muted-foreground text-sm">{description}</p>
|
||||
) : null}
|
||||
|
||||
@@ -7,11 +7,9 @@ import {
|
||||
SettingsIcon,
|
||||
} from 'lucide-react'
|
||||
|
||||
import { useIsMobile } from '@cfdm/ui/hooks/use-mobile'
|
||||
import { cn } from '@cfdm/ui/lib/utils'
|
||||
import { PageShell } from '@/components/page-shell'
|
||||
import { PageHeader } from '@/components/page-header'
|
||||
import { Separator } from '@cfdm/ui/components/separator'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@cfdm/ui/components/tabs'
|
||||
|
||||
export interface SettingsTabConfig {
|
||||
id: string
|
||||
@@ -55,69 +53,42 @@ interface SettingsShellProps {
|
||||
tabs?: SettingsTabConfig[]
|
||||
}
|
||||
|
||||
/** Settings shell — preview https://reui.io/preview/base/settings-16 */
|
||||
/** Settings shell — line tabs on top (design «VPS-Tracker»); sections: https://reui.io/preview/base/settings-16 */
|
||||
export function SettingsShell({
|
||||
title = 'Настройки',
|
||||
description = 'Параметры приложения и связи с другими сервисами',
|
||||
tabs = DEFAULT_TABS,
|
||||
}: SettingsShellProps) {
|
||||
const isMobile = useIsMobile()
|
||||
const pathname = useRouterState({ select: (s) => s.location.pathname })
|
||||
const active =
|
||||
tabs.find((tab) =>
|
||||
tab.exact ? pathname === tab.to || pathname === `${tab.to}/` : pathname.startsWith(tab.to),
|
||||
) ?? tabs[0]
|
||||
|
||||
return (
|
||||
<PageShell>
|
||||
<div className="mx-auto flex w-full max-w-4xl flex-col gap-5">
|
||||
<div className="flex flex-col gap-3">
|
||||
<PageHeader title={title} description={description} />
|
||||
<Separator />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'flex gap-5',
|
||||
isMobile ? 'flex-col' : 'flex-row items-start',
|
||||
)}
|
||||
>
|
||||
{tabs.length > 1 ? (
|
||||
<nav
|
||||
aria-label="Разделы настроек"
|
||||
className={cn(
|
||||
'flex gap-1',
|
||||
isMobile
|
||||
? 'scrollbar-none -mx-1 overflow-x-auto overflow-y-hidden pb-1'
|
||||
: 'w-44 shrink-0 flex-col',
|
||||
)}
|
||||
>
|
||||
{tabs.map((tab) => {
|
||||
const isActive = tab.exact
|
||||
? pathname === tab.to || pathname === `${tab.to}/`
|
||||
: pathname.startsWith(tab.to)
|
||||
return (
|
||||
<Link
|
||||
key={tab.id}
|
||||
to={tab.to}
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-lg px-3 py-2 text-sm transition-colors',
|
||||
isMobile && 'shrink-0',
|
||||
!isMobile && 'w-full',
|
||||
isActive
|
||||
? 'bg-muted text-foreground font-medium shadow-sm ring-1 ring-border/60'
|
||||
: 'text-muted-foreground hover:bg-muted/60 hover:text-foreground',
|
||||
)}
|
||||
>
|
||||
{tab.icon}
|
||||
{tab.label}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
) : null}
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
<PageHeader title={title} description={description} />
|
||||
{/* Design «VPS-Tracker»: settings sections as page line tabs. */}
|
||||
{tabs.length > 1 ? (
|
||||
<Tabs value={active?.id}>
|
||||
<TabsList variant="line" aria-label="Разделы настроек" className="gap-5">
|
||||
{tabs.map((tab) => (
|
||||
<TabsTrigger
|
||||
key={tab.id}
|
||||
value={tab.id}
|
||||
nativeButton={false}
|
||||
render={<Link to={tab.to} />}
|
||||
className="text-muted-foreground hover:text-foreground h-auto gap-2 px-0 pb-3 after:bottom-0"
|
||||
>
|
||||
{tab.icon}
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
) : null}
|
||||
<div className="min-w-0">
|
||||
<Outlet />
|
||||
</div>
|
||||
</PageShell>
|
||||
)
|
||||
|
||||
@@ -31,11 +31,11 @@ import { Route as AuthAuditRouteImport } from './routes/_auth/audit'
|
||||
import { Route as AuthAccountsRouteImport } from './routes/_auth/accounts'
|
||||
import { Route as AuthSettingsRouteRouteImport } from './routes/_auth/settings/route'
|
||||
import { Route as AuthSettingsIndexRouteImport } from './routes/_auth/settings/index'
|
||||
import { Route as AuthVpsVpsIdRouteImport } from './routes/_auth/vps.$vpsId'
|
||||
import { Route as AuthVpsVpsIdRouteImport } from './routes/_auth/vps_.$vpsId'
|
||||
import { Route as AuthSettingsSyncRouteImport } from './routes/_auth/settings/sync'
|
||||
import { Route as AuthSettingsNotificationsRouteImport } from './routes/_auth/settings/notifications'
|
||||
import { Route as AuthSettingsIntegrationsRouteImport } from './routes/_auth/settings/integrations'
|
||||
import { Route as AuthProjectsProjectIdRouteImport } from './routes/_auth/projects.$projectId'
|
||||
import { Route as AuthProjectsProjectIdRouteImport } from './routes/_auth/projects_.$projectId'
|
||||
|
||||
const AuthRoute = AuthRouteImport.update({
|
||||
id: '/_auth',
|
||||
@@ -147,9 +147,9 @@ const AuthSettingsIndexRoute = AuthSettingsIndexRouteImport.update({
|
||||
getParentRoute: () => AuthSettingsRouteRoute,
|
||||
} as any)
|
||||
const AuthVpsVpsIdRoute = AuthVpsVpsIdRouteImport.update({
|
||||
id: '/$vpsId',
|
||||
path: '/$vpsId',
|
||||
getParentRoute: () => AuthVpsRoute,
|
||||
id: '/vps_/$vpsId',
|
||||
path: '/vps/$vpsId',
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any)
|
||||
const AuthSettingsSyncRoute = AuthSettingsSyncRouteImport.update({
|
||||
id: '/sync',
|
||||
@@ -169,9 +169,9 @@ const AuthSettingsIntegrationsRoute =
|
||||
getParentRoute: () => AuthSettingsRouteRoute,
|
||||
} as any)
|
||||
const AuthProjectsProjectIdRoute = AuthProjectsProjectIdRouteImport.update({
|
||||
id: '/$projectId',
|
||||
path: '/$projectId',
|
||||
getParentRoute: () => AuthProjectsRoute,
|
||||
id: '/projects_/$projectId',
|
||||
path: '/projects/$projectId',
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
@@ -184,7 +184,7 @@ export interface FileRoutesByFullPath {
|
||||
'/dashboard': typeof AuthDashboardRoute
|
||||
'/geo': typeof AuthGeoRoute
|
||||
'/payments': typeof AuthPaymentsRoute
|
||||
'/projects': typeof AuthProjectsRouteWithChildren
|
||||
'/projects': typeof AuthProjectsRoute
|
||||
'/providers': typeof AuthProvidersRoute
|
||||
'/renewals': typeof AuthRenewalsRoute
|
||||
'/reports': typeof AuthReportsRoute
|
||||
@@ -193,7 +193,7 @@ export interface FileRoutesByFullPath {
|
||||
'/sync-journal': typeof AuthSyncJournalRoute
|
||||
'/tariffs': typeof AuthTariffsRoute
|
||||
'/topology': typeof AuthTopologyRoute
|
||||
'/vps': typeof AuthVpsRouteWithChildren
|
||||
'/vps': typeof AuthVpsRoute
|
||||
'/auth/callback': typeof AuthCallbackRoute
|
||||
'/projects/$projectId': typeof AuthProjectsProjectIdRoute
|
||||
'/settings/integrations': typeof AuthSettingsIntegrationsRoute
|
||||
@@ -211,7 +211,7 @@ export interface FileRoutesByTo {
|
||||
'/dashboard': typeof AuthDashboardRoute
|
||||
'/geo': typeof AuthGeoRoute
|
||||
'/payments': typeof AuthPaymentsRoute
|
||||
'/projects': typeof AuthProjectsRouteWithChildren
|
||||
'/projects': typeof AuthProjectsRoute
|
||||
'/providers': typeof AuthProvidersRoute
|
||||
'/renewals': typeof AuthRenewalsRoute
|
||||
'/reports': typeof AuthReportsRoute
|
||||
@@ -220,7 +220,7 @@ export interface FileRoutesByTo {
|
||||
'/sync-journal': typeof AuthSyncJournalRoute
|
||||
'/tariffs': typeof AuthTariffsRoute
|
||||
'/topology': typeof AuthTopologyRoute
|
||||
'/vps': typeof AuthVpsRouteWithChildren
|
||||
'/vps': typeof AuthVpsRoute
|
||||
'/auth/callback': typeof AuthCallbackRoute
|
||||
'/projects/$projectId': typeof AuthProjectsProjectIdRoute
|
||||
'/settings/integrations': typeof AuthSettingsIntegrationsRoute
|
||||
@@ -241,7 +241,7 @@ export interface FileRoutesById {
|
||||
'/_auth/dashboard': typeof AuthDashboardRoute
|
||||
'/_auth/geo': typeof AuthGeoRoute
|
||||
'/_auth/payments': typeof AuthPaymentsRoute
|
||||
'/_auth/projects': typeof AuthProjectsRouteWithChildren
|
||||
'/_auth/projects': typeof AuthProjectsRoute
|
||||
'/_auth/providers': typeof AuthProvidersRoute
|
||||
'/_auth/renewals': typeof AuthRenewalsRoute
|
||||
'/_auth/reports': typeof AuthReportsRoute
|
||||
@@ -250,13 +250,13 @@ export interface FileRoutesById {
|
||||
'/_auth/sync-journal': typeof AuthSyncJournalRoute
|
||||
'/_auth/tariffs': typeof AuthTariffsRoute
|
||||
'/_auth/topology': typeof AuthTopologyRoute
|
||||
'/_auth/vps': typeof AuthVpsRouteWithChildren
|
||||
'/_auth/vps': typeof AuthVpsRoute
|
||||
'/auth/callback': typeof AuthCallbackRoute
|
||||
'/_auth/projects/$projectId': typeof AuthProjectsProjectIdRoute
|
||||
'/_auth/projects_/$projectId': typeof AuthProjectsProjectIdRoute
|
||||
'/_auth/settings/integrations': typeof AuthSettingsIntegrationsRoute
|
||||
'/_auth/settings/notifications': typeof AuthSettingsNotificationsRoute
|
||||
'/_auth/settings/sync': typeof AuthSettingsSyncRoute
|
||||
'/_auth/vps/$vpsId': typeof AuthVpsVpsIdRoute
|
||||
'/_auth/vps_/$vpsId': typeof AuthVpsVpsIdRoute
|
||||
'/_auth/settings/': typeof AuthSettingsIndexRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
@@ -338,11 +338,11 @@ export interface FileRouteTypes {
|
||||
| '/_auth/topology'
|
||||
| '/_auth/vps'
|
||||
| '/auth/callback'
|
||||
| '/_auth/projects/$projectId'
|
||||
| '/_auth/projects_/$projectId'
|
||||
| '/_auth/settings/integrations'
|
||||
| '/_auth/settings/notifications'
|
||||
| '/_auth/settings/sync'
|
||||
| '/_auth/vps/$vpsId'
|
||||
| '/_auth/vps_/$vpsId'
|
||||
| '/_auth/settings/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
@@ -508,12 +508,12 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthSettingsIndexRouteImport
|
||||
parentRoute: typeof AuthSettingsRouteRoute
|
||||
}
|
||||
'/_auth/vps/$vpsId': {
|
||||
id: '/_auth/vps/$vpsId'
|
||||
path: '/$vpsId'
|
||||
'/_auth/vps_/$vpsId': {
|
||||
id: '/_auth/vps_/$vpsId'
|
||||
path: '/vps/$vpsId'
|
||||
fullPath: '/vps/$vpsId'
|
||||
preLoaderRoute: typeof AuthVpsVpsIdRouteImport
|
||||
parentRoute: typeof AuthVpsRoute
|
||||
parentRoute: typeof AuthRoute
|
||||
}
|
||||
'/_auth/settings/sync': {
|
||||
id: '/_auth/settings/sync'
|
||||
@@ -536,12 +536,12 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthSettingsIntegrationsRouteImport
|
||||
parentRoute: typeof AuthSettingsRouteRoute
|
||||
}
|
||||
'/_auth/projects/$projectId': {
|
||||
id: '/_auth/projects/$projectId'
|
||||
path: '/$projectId'
|
||||
'/_auth/projects_/$projectId': {
|
||||
id: '/_auth/projects_/$projectId'
|
||||
path: '/projects/$projectId'
|
||||
fullPath: '/projects/$projectId'
|
||||
preLoaderRoute: typeof AuthProjectsProjectIdRouteImport
|
||||
parentRoute: typeof AuthProjectsRoute
|
||||
parentRoute: typeof AuthRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -563,29 +563,6 @@ const AuthSettingsRouteRouteChildren: AuthSettingsRouteRouteChildren = {
|
||||
const AuthSettingsRouteRouteWithChildren =
|
||||
AuthSettingsRouteRoute._addFileChildren(AuthSettingsRouteRouteChildren)
|
||||
|
||||
interface AuthProjectsRouteChildren {
|
||||
AuthProjectsProjectIdRoute: typeof AuthProjectsProjectIdRoute
|
||||
}
|
||||
|
||||
const AuthProjectsRouteChildren: AuthProjectsRouteChildren = {
|
||||
AuthProjectsProjectIdRoute: AuthProjectsProjectIdRoute,
|
||||
}
|
||||
|
||||
const AuthProjectsRouteWithChildren = AuthProjectsRoute._addFileChildren(
|
||||
AuthProjectsRouteChildren,
|
||||
)
|
||||
|
||||
interface AuthVpsRouteChildren {
|
||||
AuthVpsVpsIdRoute: typeof AuthVpsVpsIdRoute
|
||||
}
|
||||
|
||||
const AuthVpsRouteChildren: AuthVpsRouteChildren = {
|
||||
AuthVpsVpsIdRoute: AuthVpsVpsIdRoute,
|
||||
}
|
||||
|
||||
const AuthVpsRouteWithChildren =
|
||||
AuthVpsRoute._addFileChildren(AuthVpsRouteChildren)
|
||||
|
||||
interface AuthRouteChildren {
|
||||
AuthSettingsRouteRoute: typeof AuthSettingsRouteRouteWithChildren
|
||||
AuthAccountsRoute: typeof AuthAccountsRoute
|
||||
@@ -595,7 +572,7 @@ interface AuthRouteChildren {
|
||||
AuthDashboardRoute: typeof AuthDashboardRoute
|
||||
AuthGeoRoute: typeof AuthGeoRoute
|
||||
AuthPaymentsRoute: typeof AuthPaymentsRoute
|
||||
AuthProjectsRoute: typeof AuthProjectsRouteWithChildren
|
||||
AuthProjectsRoute: typeof AuthProjectsRoute
|
||||
AuthProvidersRoute: typeof AuthProvidersRoute
|
||||
AuthRenewalsRoute: typeof AuthRenewalsRoute
|
||||
AuthReportsRoute: typeof AuthReportsRoute
|
||||
@@ -604,7 +581,9 @@ interface AuthRouteChildren {
|
||||
AuthSyncJournalRoute: typeof AuthSyncJournalRoute
|
||||
AuthTariffsRoute: typeof AuthTariffsRoute
|
||||
AuthTopologyRoute: typeof AuthTopologyRoute
|
||||
AuthVpsRoute: typeof AuthVpsRouteWithChildren
|
||||
AuthVpsRoute: typeof AuthVpsRoute
|
||||
AuthProjectsProjectIdRoute: typeof AuthProjectsProjectIdRoute
|
||||
AuthVpsVpsIdRoute: typeof AuthVpsVpsIdRoute
|
||||
}
|
||||
|
||||
const AuthRouteChildren: AuthRouteChildren = {
|
||||
@@ -616,7 +595,7 @@ const AuthRouteChildren: AuthRouteChildren = {
|
||||
AuthDashboardRoute: AuthDashboardRoute,
|
||||
AuthGeoRoute: AuthGeoRoute,
|
||||
AuthPaymentsRoute: AuthPaymentsRoute,
|
||||
AuthProjectsRoute: AuthProjectsRouteWithChildren,
|
||||
AuthProjectsRoute: AuthProjectsRoute,
|
||||
AuthProvidersRoute: AuthProvidersRoute,
|
||||
AuthRenewalsRoute: AuthRenewalsRoute,
|
||||
AuthReportsRoute: AuthReportsRoute,
|
||||
@@ -625,7 +604,9 @@ const AuthRouteChildren: AuthRouteChildren = {
|
||||
AuthSyncJournalRoute: AuthSyncJournalRoute,
|
||||
AuthTariffsRoute: AuthTariffsRoute,
|
||||
AuthTopologyRoute: AuthTopologyRoute,
|
||||
AuthVpsRoute: AuthVpsRouteWithChildren,
|
||||
AuthVpsRoute: AuthVpsRoute,
|
||||
AuthProjectsProjectIdRoute: AuthProjectsProjectIdRoute,
|
||||
AuthVpsVpsIdRoute: AuthVpsVpsIdRoute,
|
||||
}
|
||||
|
||||
const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
} from '@/components/domain/audit-labels'
|
||||
import { Button } from '@cfdm/ui/components/button'
|
||||
import { Badge } from '@/components/reui/badge'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@cfdm/ui/components/tabs'
|
||||
import { CountedLineTabs } from '@/components/counted-line-tabs'
|
||||
import { TableSkeleton } from '@/components/skeletons'
|
||||
|
||||
const auditSearchSchema = z.object({
|
||||
@@ -197,22 +197,15 @@ function AuditPage() {
|
||||
actions={<AuditViewToggle view={view} onViewChange={setView} />}
|
||||
/>
|
||||
|
||||
<Tabs value={actionFilter} onValueChange={setActionFilter}>
|
||||
<TabsList variant="line" aria-label="Фильтр по действию" className="gap-5">
|
||||
{ACTION_FILTERS.map((option) => (
|
||||
<TabsTrigger
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className="text-muted-foreground hover:text-foreground h-auto gap-2 px-0 pb-3"
|
||||
>
|
||||
<span>{option.label}</span>
|
||||
<span className="bg-muted text-muted-foreground rounded-md px-1.5 py-0.5 text-[10px] tabular-nums">
|
||||
{actionCounts[option.value]}
|
||||
</span>
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<CountedLineTabs
|
||||
tabs={ACTION_FILTERS.map((option) => ({
|
||||
id: option.value,
|
||||
label: option.label,
|
||||
count: actionCounts[option.value],
|
||||
}))}
|
||||
value={actionFilter}
|
||||
onValueChange={setActionFilter}
|
||||
/>
|
||||
|
||||
<QueryState
|
||||
data={data}
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ import {
|
||||
} from '@/lib/project-analytics'
|
||||
import type { Vps } from '@/types/entities'
|
||||
|
||||
export const Route = createFileRoute('/_auth/projects/$projectId')({
|
||||
export const Route = createFileRoute('/_auth/projects_/$projectId')({
|
||||
loader: ({ context: { queryClient } }) =>
|
||||
queryClient.ensureQueryData(snapshotQueryOptions()),
|
||||
component: ProjectDetailPage,
|
||||
+25
-16
@@ -48,7 +48,7 @@ import {
|
||||
import type { Payment, Vps } from '@/types/entities'
|
||||
import { VpsDomainsCell } from '@/components/integrations/vps-domains-cell'
|
||||
|
||||
export const Route = createFileRoute('/_auth/vps/$vpsId')({
|
||||
export const Route = createFileRoute('/_auth/vps_/$vpsId')({
|
||||
loader: ({ context: { queryClient } }) =>
|
||||
queryClient.ensureQueryData(snapshotQueryOptions()),
|
||||
component: VpsDetailPage,
|
||||
@@ -153,19 +153,18 @@ function VpsDetailPage() {
|
||||
title={vps ? (vps.ip || vps.dns || 'VPS') : 'VPS'}
|
||||
description={account ? accountSelectLabel(account, providerById) : undefined}
|
||||
actions={
|
||||
vps ? (
|
||||
<Button variant="outline" render={<Link to="/vps" search={{ edit: vpsId }} />}>
|
||||
Редактировать
|
||||
<>
|
||||
<Button variant="outline" render={<Link to="/vps" />}>
|
||||
<ArrowLeftIcon data-icon="inline-start" />
|
||||
К списку VPS
|
||||
</Button>
|
||||
) : undefined
|
||||
{vps ? (
|
||||
<Button render={<Link to="/vps" search={{ edit: vpsId }} />}>Редактировать</Button>
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Button variant="ghost" size="sm" className="w-fit" render={<Link to="/vps" />}>
|
||||
<ArrowLeftIcon data-icon="inline-start" />
|
||||
К списку VPS
|
||||
</Button>
|
||||
|
||||
<QueryState
|
||||
data={vps}
|
||||
isLoading={isLoading}
|
||||
@@ -189,12 +188,22 @@ function VpsDetailPage() {
|
||||
}
|
||||
>
|
||||
{(row) => (
|
||||
<Tabs defaultValue="overview">
|
||||
<TabsList>
|
||||
<TabsTrigger value="overview">Обзор</TabsTrigger>
|
||||
<TabsTrigger value="finance">Финансы</TabsTrigger>
|
||||
<TabsTrigger value="notes">Заметки</TabsTrigger>
|
||||
<TabsTrigger value="sync">Синк</TabsTrigger>
|
||||
<Tabs defaultValue="overview" className="flex flex-col gap-4 md:gap-6">
|
||||
<TabsList variant="line" className="gap-5">
|
||||
{[
|
||||
['overview', 'Обзор'],
|
||||
['finance', 'Финансы'],
|
||||
['notes', 'Заметки'],
|
||||
['sync', 'Синк'],
|
||||
].map(([value, label]) => (
|
||||
<TabsTrigger
|
||||
key={value}
|
||||
value={value}
|
||||
className="text-muted-foreground hover:text-foreground h-auto px-0 pb-3 after:bottom-0"
|
||||
>
|
||||
{label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="flex flex-col gap-4">
|
||||
Reference in new issue
Block a user