fix(web): крошки деталей, контраст warning и тёмный сайдбар на телефоне

Детальные VPS и проекта показывают «раздел / запись» в крошках, warning-значения
KPI читаемы в светлой теме, мобильный сайдбар в светлой теме остаётся тёмным,
заголовки групп не попадают в Tab-порядок свёрнутого меню.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DenozordecandClaude Opus 5.5 committed 2026-11-06 22:06:39 +07:00
1 parent 9483e1122d
commit 166abb03b1
6 files changed
+38 -8

No files matched your search

+18 -6
View File
@@ -65,6 +65,7 @@ import {
permissionForPath,
} from '@/lib/auth'
import { SpaceProvider, useSpaceId } from '@/lib/space'
import { PageCrumbContext } from '@/lib/page-crumb'
import { SKIP_TO_CONTENT_CLASS } from '@/lib/ui-surface'
import { cn } from '@cfdm/ui/lib/utils'
@@ -156,7 +157,10 @@ export function AppShell({ children }: { children: ReactNode }) {
const activeItem =
ALL_NAV_ITEMS.find((i) => pathname === i.to || pathname.startsWith(`${i.to}/`)) ??
ALL_NAV_ITEMS[0]
const parentTo = PARENT_ROUTE[activeItem.to]
const [pageCrumb, setPageCrumb] = useState<string | null>(null)
// Detail routes (design: «VPS / 203.0.113.21»): section links back, page shows the record title.
const isDetail = pageCrumb != null && pathname !== activeItem.to
const parentTo = isDetail ? activeItem.to : PARENT_ROUTE[activeItem.to]
const parentLabel = parentTo ? ROUTE_LABELS[parentTo] : null
const { data: stats } = useQuery(dashboardStatsQueryOptions())
@@ -232,7 +236,7 @@ export function AppShell({ children }: { children: ReactNode }) {
) : null}
<BreadcrumbItem>
<BreadcrumbPage>
{ROUTE_LABELS[activeItem.to] ?? ''}
{isDetail ? pageCrumb : (ROUTE_LABELS[activeItem.to] ?? '')}
</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
@@ -242,7 +246,9 @@ export function AppShell({ children }: { children: ReactNode }) {
</div>
</header>
<div className="flex min-w-0 flex-1 flex-col gap-4 px-4 py-4 md:gap-6 md:px-6 md:py-5">
<SpaceScopedMain>{children}</SpaceScopedMain>
<PageCrumbContext.Provider value={setPageCrumb}>
<SpaceScopedMain>{children}</SpaceScopedMain>
</PageCrumbContext.Provider>
</div>
</SidebarInset>
<GlobalSearch open={searchOpen} onOpenChange={setSearchOpen} />
@@ -262,11 +268,17 @@ function NavGroupSection({ group, pathname }: { group: NavGroup; pathname: strin
<Collapsible open={open || iconRail} onOpenChange={setOpen}>
<SidebarGroup>
<SidebarGroupLabel
render={<CollapsibleTrigger />}
className="group/label hover:text-sidebar-foreground cursor-pointer uppercase tracking-wider transition-colors"
render={<CollapsibleTrigger tabIndex={iconRail ? -1 : undefined} />}
className="hover:text-sidebar-foreground cursor-pointer uppercase tracking-wider transition-colors"
>
{group.label}
<ChevronDownIcon className="ml-auto transition-transform motion-reduce:transition-none group-not-data-panel-open/label:-rotate-90" />
<ChevronDownIcon
aria-hidden
className={cn(
'ml-auto transition-transform motion-reduce:transition-none',
!open && '-rotate-90',
)}
/>
</SidebarGroupLabel>
<CollapsibleContent>
<SidebarGroupContent>
@@ -44,7 +44,8 @@ const DEFAULT_ICON_CLASS = 'text-muted-foreground [&_svg]:text-current'
const VALUE_VARIANT_CLASS: Record<KpiStatVariant, string> = {
default: 'text-foreground',
warning: 'text-warning',
// text-warning on white fails contrast; readable *-foreground in light theme.
warning: 'text-warning-foreground dark:text-warning',
destructive: 'text-destructive',
}
+13
View File
@@ -0,0 +1,13 @@
import { createContext, useContext, useEffect } from 'react'
/** Setter for the current page breadcrumb (detail routes: «VPS / 203.0.113.21»). */
export const PageCrumbContext = createContext<(title: string | null) => void>(() => {})
/** Detail pages call this; AppShell shows `section / title` while the page is mounted. */
export function usePageCrumb(title: string | null | undefined) {
const setTitle = useContext(PageCrumbContext)
useEffect(() => {
setTitle(title || null)
return () => setTitle(null)
}, [setTitle, title])
}
@@ -1,4 +1,5 @@
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'
import { usePageCrumb } from '@/lib/page-crumb'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useMemo, useState } from 'react'
import {
@@ -73,6 +74,7 @@ function ProjectDetailPage() {
const [editOpen, setEditOpen] = useState(false)
const project = snapshot ? findProject(snapshot, projectId) : undefined
usePageCrumb(project?.name)
const projectVps = useMemo(
() => (snapshot ? projectVpsList(snapshot, projectId) : []),
[snapshot, projectId],
@@ -1,4 +1,5 @@
import { createFileRoute, Link } from '@tanstack/react-router'
import { usePageCrumb } from '@/lib/page-crumb'
import { useQuery } from '@tanstack/react-query'
import { useMemo } from 'react'
import {
@@ -95,6 +96,7 @@ function VpsDetailPage() {
const ratesData = normalizeRatesPayload(rawRates) ?? rawRates ?? null
const vps = snapshot?.vps.find((v) => v.id === vpsId)
usePageCrumb(vps ? vps.ip || vps.dns || 'VPS' : null)
const providerById = snapshot ? providerByIdMap(snapshot.providers) : new Map()
const account = snapshot?.providerAccounts.find((a) => a.id === vps?.providerAccountId)
const provider = vps ? providerById.get(vps.providerId) : undefined
+1 -1
View File
@@ -53,7 +53,7 @@
}
/* Dark-sidebar scope in light theme: inner text/border/muted tokens follow the sidebar. */
:root:not(.dark) [data-slot='sidebar-inner'] {
:root:not(.dark) :is([data-slot='sidebar-inner'], [data-slot='sidebar'][data-mobile='true']) {
--foreground: oklch(0.92 0 0);
--muted-foreground: oklch(0.648 0.008 264.376);
--border: oklch(1 0 0 / 8%);