fix(web): крошки деталей, контраст warning и тёмный сайдбар на телефоне
Детальные VPS и проекта показывают «раздел / запись» в крошках, warning-значения KPI читаемы в светлой теме, мобильный сайдбар в светлой теме остаётся тёмным, заголовки групп не попадают в Tab-порядок свёрнутого меню. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9483e1122d
commit
166abb03b1
6 files changed
+38
-8
No files matched your search
@@ -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',
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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%);
|
||||
|
||||
Reference in new issue
Block a user