From e3403bcb56741ae76b19fd69869860fc3edf7152 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Fri, 6 Nov 2026 22:02:04 +0700 Subject: [PATCH] =?UTF-8?q?feat(web):=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B2?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=B8=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=B9=D0=BA=D0=B8,=20VPS-=D0=B4=D0=B5=D1=82=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=20=D0=B8=20=D0=B6=D1=83=D1=80=D0=BD=D0=B0=D0=BB=20=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=B2=D0=BA=D0=BB=D0=B0=D0=B4=D0=BA=D0=B8=20=D0=B8=D0=B7=20?= =?UTF-8?q?=D0=B4=D0=B8=D0=B7=D0=B0=D0=B9=D0=BD=D0=B0=20VPS-Tracker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Настройки — line-вкладки сверху вместо боковой навигации, детали VPS — line-вкладки и действия в шапке, журнал — общие CountedLineTabs, шапка страницы 20px. Детальные маршруты VPS и проекта вынесены из вложенности (vps_.$vpsId, projects_.$projectId): списки не рендерят Outlet, и детали не открывались. Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/page-header.tsx | 8 +- .../components/reui-kit/settings-shell.tsx | 85 ++++++------------ apps/web/src/routeTree.gen.ts | 87 ++++++++----------- apps/web/src/routes/_auth/audit.tsx | 27 +++--- ...projectId.tsx => projects_.$projectId.tsx} | 2 +- .../_auth/{vps.$vpsId.tsx => vps_.$vpsId.tsx} | 41 +++++---- 6 files changed, 102 insertions(+), 148 deletions(-) rename apps/web/src/routes/_auth/{projects.$projectId.tsx => projects_.$projectId.tsx} (99%) rename apps/web/src/routes/_auth/{vps.$vpsId.tsx => vps_.$vpsId.tsx} (91%) diff --git a/apps/web/src/components/page-header.tsx b/apps/web/src/components/page-header.tsx index 7a50dbc..bdd9fc4 100644 --- a/apps/web/src/components/page-header.tsx +++ b/apps/web/src/components/page-header.tsx @@ -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 ( -
-
-

{title}

+
+
+

{title}

{description ? (

{description}

) : null} diff --git a/apps/web/src/components/reui-kit/settings-shell.tsx b/apps/web/src/components/reui-kit/settings-shell.tsx index ca95b67..16735b6 100644 --- a/apps/web/src/components/reui-kit/settings-shell.tsx +++ b/apps/web/src/components/reui-kit/settings-shell.tsx @@ -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 ( -
-
- - -
- -
- {tabs.length > 1 ? ( - - ) : null} - -
- -
-
+ + {/* Design «VPS-Tracker»: settings sections as page line tabs. */} + {tabs.length > 1 ? ( + + + {tabs.map((tab) => ( + } + className="text-muted-foreground hover:text-foreground h-auto gap-2 px-0 pb-3 after:bottom-0" + > + {tab.icon} + {tab.label} + + ))} + + + ) : null} +
+
) diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index 46dad30..6d6694f 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -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) diff --git a/apps/web/src/routes/_auth/audit.tsx b/apps/web/src/routes/_auth/audit.tsx index 91802ff..e1156ed 100644 --- a/apps/web/src/routes/_auth/audit.tsx +++ b/apps/web/src/routes/_auth/audit.tsx @@ -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={} /> - - - {ACTION_FILTERS.map((option) => ( - - {option.label} - - {actionCounts[option.value]} - - - ))} - - + ({ + id: option.value, + label: option.label, + count: actionCounts[option.value], + }))} + value={actionFilter} + onValueChange={setActionFilter} + /> queryClient.ensureQueryData(snapshotQueryOptions()), component: ProjectDetailPage, diff --git a/apps/web/src/routes/_auth/vps.$vpsId.tsx b/apps/web/src/routes/_auth/vps_.$vpsId.tsx similarity index 91% rename from apps/web/src/routes/_auth/vps.$vpsId.tsx rename to apps/web/src/routes/_auth/vps_.$vpsId.tsx index b6b9f27..50de0e4 100644 --- a/apps/web/src/routes/_auth/vps.$vpsId.tsx +++ b/apps/web/src/routes/_auth/vps_.$vpsId.tsx @@ -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 ? ( - - ) : undefined + {vps ? ( + + ) : null} + } /> - - {(row) => ( - - - Обзор - Финансы - Заметки - Синк + + + {[ + ['overview', 'Обзор'], + ['finance', 'Финансы'], + ['notes', 'Заметки'], + ['sync', 'Синк'], + ].map(([value, label]) => ( + + {label} + + ))}