feat(uat-batch-17): layout polish — DocumentsHub flush-left, breadcrumb wrap fix, viewport-centered topbar search

- DocumentsHub root container gains `sm:-mx-6 sm:-mt-3 sm:-mb-6` to
  escape the AppShell main padding (`px-6 pt-3 pb-6`). The folder
  column now sits flush against the global app sidebar, reading as an
  extension of navigation rather than a card-inside-a-page. Mobile
  layout retains the AppShell padding.
- Breadcrumbs: each crumb + its trailing separator now share a single
  `<BreadcrumbItem>` instead of being separate `<li>`s. Flex-wrap can
  no longer strand an orphan separator at end-of-line above a wrapped
  child crumb. Drops the standalone `<BreadcrumbSeparator>` usage from
  the consumer; the primitive is still exported for backcompat.
- Topbar search visually centered against the full viewport via a
  `translate-x:calc(-var(--width-sidebar)/2)` shift. Grid middle slot
  bumped from `minmax(360px, 640px)` → `minmax(420px, 800px)` and the
  search wrapper from `max-w-md` → `max-w-2xl` so reps actually have
  room to read long results.

tsc clean. 1419/1419 vitest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 18:31:32 +02:00
parent 9adb80ada4
commit 8fcbe45d36
3 changed files with 32 additions and 23 deletions

View File

@@ -243,7 +243,12 @@ export function DocumentsHub({ portSlug }: DocumentsHubProps) {
); );
return ( return (
<div className="h-full"> // Escape the AppShell's desktop main padding (px-6 pt-3 pb-6) so the
// folder column sits flush against the global app sidebar — reads
// as an extension of navigation rather than a card-inside-a-page.
// Inner content keeps its own padding so the right pane doesn't
// run flush with the viewport edge.
<div className="h-full sm:-mx-6 sm:-mt-3 sm:-mb-6">
{/* Mobile: stacked, with a Sheet drawer for folders. */} {/* Mobile: stacked, with a Sheet drawer for folders. */}
<div className="flex flex-col h-full sm:hidden"> <div className="flex flex-col h-full sm:hidden">
<FolderTreeSidebar <FolderTreeSidebar

View File

@@ -2,7 +2,6 @@
import Link from 'next/link'; import Link from 'next/link';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { Fragment } from 'react';
import { ChevronRight } from 'lucide-react'; import { ChevronRight } from 'lucide-react';
import { import {
@@ -11,7 +10,6 @@ import {
BreadcrumbLink, BreadcrumbLink,
BreadcrumbList, BreadcrumbList,
BreadcrumbPage, BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb'; } from '@/components/ui/breadcrumb';
import { useUIStore } from '@/stores/ui-store'; import { useUIStore } from '@/stores/ui-store';
import { useBreadcrumbStore } from '@/stores/breadcrumb-store'; import { useBreadcrumbStore } from '@/stores/breadcrumb-store';
@@ -102,14 +100,17 @@ export function Breadcrumbs() {
return ( return (
<Breadcrumb> <Breadcrumb>
<BreadcrumbList className="text-sm gap-1.5"> <BreadcrumbList className="text-sm gap-1.5">
{crumbs.map((crumb, _index) => ( {crumbs.map((crumb) => (
<Fragment key={crumb.href}> // Each crumb + its trailing separator share a single
<BreadcrumbItem> // inline-flex `<li>` so flex-wrap can't strand the
// separator at end-of-line above the wrapped child crumb.
<BreadcrumbItem key={crumb.href}>
{crumb.isLast ? ( {crumb.isLast ? (
<BreadcrumbPage className="font-medium text-foreground truncate max-w-[160px]"> <BreadcrumbPage className="font-medium text-foreground truncate max-w-[160px]">
{crumb.label} {crumb.label}
</BreadcrumbPage> </BreadcrumbPage>
) : ( ) : (
<>
<BreadcrumbLink asChild> <BreadcrumbLink asChild>
<Link <Link
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -119,14 +120,14 @@ export function Breadcrumbs() {
{crumb.label} {crumb.label}
</Link> </Link>
</BreadcrumbLink> </BreadcrumbLink>
<ChevronRight
className="w-3 h-3 text-muted-foreground/40"
aria-hidden
role="presentation"
/>
</>
)} )}
</BreadcrumbItem> </BreadcrumbItem>
{!crumb.isLast && (
<BreadcrumbSeparator className="text-muted-foreground/40">
<ChevronRight className="w-3 h-3" aria-hidden />
</BreadcrumbSeparator>
)}
</Fragment>
))} ))}
</BreadcrumbList> </BreadcrumbList>
</Breadcrumb> </Breadcrumb>

View File

@@ -54,7 +54,7 @@ export function Topbar({ ports, user }: TopbarProps) {
// Three-column grid: breadcrumbs left, search center, actions right. // Three-column grid: breadcrumbs left, search center, actions right.
// The brand logo lives in the sidebar header (per design feedback) so the // The brand logo lives in the sidebar header (per design feedback) so the
// topbar center is dedicated to the global search bar. // topbar center is dedicated to the global search bar.
<header className="grid h-14 grid-cols-[minmax(0,1fr)_minmax(360px,640px)_minmax(0,1fr)] items-center border-b border-border bg-background gap-3 px-4 shrink-0"> <header className="grid h-14 grid-cols-[minmax(0,1fr)_minmax(420px,800px)_minmax(0,1fr)] items-center border-b border-border bg-background gap-3 px-4 shrink-0">
{/* LEFT: optional back button + breadcrumbs / page title */} {/* LEFT: optional back button + breadcrumbs / page title */}
<div className="min-w-0 flex items-center gap-1.5"> <div className="min-w-0 flex items-center gap-1.5">
{showBackButton && ( {showBackButton && (
@@ -74,11 +74,14 @@ export function Topbar({ ports, user }: TopbarProps) {
<Breadcrumbs /> <Breadcrumbs />
</div> </div>
{/* CENTER: global search - capped width and horizontally centered {/* CENTER: global search - capped width and visually centered
inside its grid slot so it sits visually in the middle of the against the FULL viewport (not the post-sidebar area) via a
topbar regardless of breadcrumb / action-row width. */} translate-X that shifts left by half the sidebar width.
Without the translate the topbar's grid centers inside the
area-after-the-sidebar, so the search visually drifts right
by half the sidebar width. */}
<div className="flex items-center justify-center min-w-0"> <div className="flex items-center justify-center min-w-0">
<div className="w-full max-w-md mx-auto min-w-0"> <div className="w-full max-w-2xl mx-auto min-w-0 sm:-translate-x-[calc(var(--width-sidebar)/2)]">
<CommandSearch /> <CommandSearch />
</div> </div>
</div> </div>