diff --git a/apps/web/src/components/layout/app-shell.tsx b/apps/web/src/components/layout/app-shell.tsx index bef1cfa..369eed2 100644 --- a/apps/web/src/components/layout/app-shell.tsx +++ b/apps/web/src/components/layout/app-shell.tsx @@ -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(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} - {ROUTE_LABELS[activeItem.to] ?? ''} + {isDetail ? pageCrumb : (ROUTE_LABELS[activeItem.to] ?? '')} @@ -242,7 +246,9 @@ export function AppShell({ children }: { children: ReactNode }) {
- {children} + + {children} +
@@ -262,11 +268,17 @@ function NavGroupSection({ group, pathname }: { group: NavGroup; pathname: strin } - className="group/label hover:text-sidebar-foreground cursor-pointer uppercase tracking-wider transition-colors" + render={} + className="hover:text-sidebar-foreground cursor-pointer uppercase tracking-wider transition-colors" > {group.label} - + diff --git a/apps/web/src/components/reui-kit/kpi-stat-grid.tsx b/apps/web/src/components/reui-kit/kpi-stat-grid.tsx index 56e83fb..5b62100 100644 --- a/apps/web/src/components/reui-kit/kpi-stat-grid.tsx +++ b/apps/web/src/components/reui-kit/kpi-stat-grid.tsx @@ -44,7 +44,8 @@ const DEFAULT_ICON_CLASS = 'text-muted-foreground [&_svg]:text-current' const VALUE_VARIANT_CLASS: Record = { 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', } diff --git a/apps/web/src/lib/page-crumb.ts b/apps/web/src/lib/page-crumb.ts new file mode 100644 index 0000000..a732ced --- /dev/null +++ b/apps/web/src/lib/page-crumb.ts @@ -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]) +} diff --git a/apps/web/src/routes/_auth/projects_.$projectId.tsx b/apps/web/src/routes/_auth/projects_.$projectId.tsx index 3a28854..4e783bf 100644 --- a/apps/web/src/routes/_auth/projects_.$projectId.tsx +++ b/apps/web/src/routes/_auth/projects_.$projectId.tsx @@ -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], diff --git a/apps/web/src/routes/_auth/vps_.$vpsId.tsx b/apps/web/src/routes/_auth/vps_.$vpsId.tsx index 50de0e4..701ee8f 100644 --- a/apps/web/src/routes/_auth/vps_.$vpsId.tsx +++ b/apps/web/src/routes/_auth/vps_.$vpsId.tsx @@ -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 diff --git a/apps/web/src/styles/theme.css b/apps/web/src/styles/theme.css index 0e50890..c140b63 100644 --- a/apps/web/src/styles/theme.css +++ b/apps/web/src/styles/theme.css @@ -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%);