Browse Source

RHL-038 fix(layout): improve TopNav and SearchForm responsiveness for better UX

Code_Uwe 3 weeks ago
parent
commit
911702cf35

+ 34 - 22
components/app-shell/TopNav.jsx

@@ -9,32 +9,44 @@ import QuickNav from "@/components/app-shell/QuickNav";
 export default function TopNav() {
 	return (
 		<header className="sticky top-0 z-50 w-full border-b bg-background/80 backdrop-blur">
-			<div className="mx-auto flex h-14 max-w-7xl items-center justify-between px-4">
-				<div className="flex items-center gap-3">
-					<Link href="/" className="font-semibold tracking-tight">
-						RHL Lieferscheine
-					</Link>
-					<span className="text-xs text-muted-foreground">
-						Lieferschein-Explorer
-					</span>
-				</div>
+			{/* 
+				TopNav alignment strategy (2xl+):
+				- Use the same 60% center column as the AppShell content grid.
+				- This keeps header content perfectly aligned with Explorer/Search.
+
+				Below 2xl:
+				- Full width for usability.
+			*/}
+			<div className="px-4">
+				<div className="mx-auto grid h-14 w-full items-center 2xl:grid-cols-[1fr_minmax(0,50%)_1fr]">
+					<div className="flex items-center justify-between gap-3 2xl:col-start-2">
+						<div className="flex items-center gap-3">
+							<Link href="/" className="font-semibold tracking-tight">
+								RHL Lieferscheine
+							</Link>
+							<span className="text-xs text-muted-foreground">
+								Lieferschein-Explorer
+							</span>
+						</div>
 
-				<div className="flex items-center gap-2">
-					<UserStatus />
+						<div className="flex items-center gap-2">
+							<UserStatus />
 
-					<QuickNav />
+							<QuickNav />
 
-					<Button
-						variant="outline"
-						size="sm"
-						disabled
-						aria-disabled="true"
-						title="Design-Umschaltung kommt später"
-					>
-						Design
-					</Button>
+							<Button
+								variant="outline"
+								size="sm"
+								disabled
+								aria-disabled="true"
+								title="Design-Umschaltung kommt später"
+							>
+								Design
+							</Button>
 
-					<LogoutButton />
+							<LogoutButton />
+						</div>
+					</div>
 				</div>
 			</div>
 		</header>

+ 44 - 50
components/search/SearchForm.jsx

@@ -31,61 +31,70 @@ export default function SearchForm({
 }) {
 	const canSearch = typeof qDraft === "string" && qDraft.trim().length > 0;
 
+	// RHL-038 UI behavior:
+	// - Single scope shows an extra combobox (branch picker).
+	// - Multi/All does not.
+	// - We animate the combobox container (max-width/max-height/opacity) so the
+	//   query block (input + search button) grows/shrinks smoothly.
+	const showSingleBranchCombobox = Boolean(
+		isAdminDev && scope === SEARCH_SCOPE.SINGLE
+	);
+
 	return (
 		<div className="space-y-4">
 			<form
 				onSubmit={(e) => {
 					e.preventDefault();
-
-					// UX rule:
-					// - Only submit when the query is non-empty.
-					// - This keeps the Search API safe and avoids accidental “match everything”.
 					if (!canSearch) return;
-
 					onSubmit();
 				}}
 				className="space-y-4"
 			>
 				{/* 
-					Layout goal (RHL-038):
-					- Desktop: one row with (1) scope, (2) query, (3) limit
-					- Mobile: natural stacking
-					Implementation:
-					- Use a responsive grid that becomes 3 columns on lg+
-					- Align items to the bottom so labels line up nicely.
+					Layout goal:
+					- Desktop: everything on one line (scope + optional single combobox | query | limit)
+					- Mobile: stack to avoid horizontal overflow
 				*/}
-				<div className="flex items-start justify-between gap-3 flex-nowrap">
-					<div className="flex justify-start gap-2 items-center">
-						{/* 1) Scope (admin/dev only) */}
-						{isAdminDev ? (
+				<div className="flex flex-col gap-3 lg:flex-row items-start lg:flex-nowrap">
+					{/* Left block: scope + (animated) single-branch combobox */}
+					{isAdminDev ? (
+						<div className="flex items-end shrink-0">
 							<SearchScopeSelect
 								branch={branch}
 								scope={scope}
 								onScopeChange={onScopeChange}
 								isSubmitting={isSubmitting}
 							/>
-						) : null}
 
-						{/* 
-					Admin/dev: SINGLE branch selection.
-					We keep this *below* the main row so the top row stays exactly:
-					Scope | Query | Limit
-				*/}
-						{isAdminDev && scope === SEARCH_SCOPE.SINGLE ? (
-							<div>
+							{/* 
+								Animated presence (no mount/unmount):
+								- When hidden: max-w-0/max-h-0 + opacity-0 so it takes no space.
+								- When shown: expands to a reasonable max size.
+								This animation drives the smooth resize of the query input block.
+							*/}
+							<div
+								className={[
+									"overflow-hidden transition-all duration-200 ease-in-out",
+									showSingleBranchCombobox
+										? "ml-2 max-w-44 max-h-24 opacity-100"
+										: "ml-0 max-w-0 max-h-0 opacity-0 pointer-events-none",
+								].join(" ")}
+								aria-hidden={showSingleBranchCombobox ? "false" : "true"}
+							>
 								<SearchSingleBranchCombobox
 									branch={branch}
 									branchesStatus={branchesStatus}
 									availableBranches={availableBranches}
 									onSingleBranchChange={onSingleBranchChange}
-									isSubmitting={isSubmitting}
+									// Disable when hidden so it can't be focused/clicked.
+									isSubmitting={isSubmitting || !showSingleBranchCombobox}
 								/>
 							</div>
-						) : null}
-					</div>
+						</div>
+					) : null}
 
-					<div className="flex flex-1">
-						{/* 2) Query (always) */}
+					{/* Middle block: query must take the most space */}
+					<div className="flex-1 min-w-0">
 						<SearchQueryBox
 							qDraft={qDraft}
 							onQDraftChange={onQDraftChange}
@@ -96,33 +105,18 @@ export default function SearchForm({
 						/>
 					</div>
 
-					{/* 3) Limit (always) */}
-					<SearchLimitSelect
-						limit={limit}
-						onLimitChange={onLimitChange}
-						isSubmitting={isSubmitting}
-					/>
-				</div>
-
-				{/* 
-					Admin/dev: SINGLE branch selection.
-					We keep this *below* the main row so the top row stays exactly:
-					Scope | Query | Limit
-				*/}
-				{/* {isAdminDev && scope === SEARCH_SCOPE.SINGLE ? (
-					<div>
-						<SearchSingleBranchCombobox
-							branch={branch}
-							branchesStatus={branchesStatus}
-							availableBranches={availableBranches}
-							onSingleBranchChange={onSingleBranchChange}
+					{/* Right block: limit stays “content-sized” */}
+					<div className="shrink-0">
+						<SearchLimitSelect
+							limit={limit}
+							onLimitChange={onLimitChange}
 							isSubmitting={isSubmitting}
 						/>
 					</div>
-				) : null} */}
+				</div>
 			</form>
 
-			{/* Multi scope branch picker remains below the form row (as requested). */}
+			{/* Multi scope branch picker remains below */}
 			{isAdminDev && scope === SEARCH_SCOPE.MULTI ? (
 				<SearchMultiBranchPicker
 					branchesStatus={branchesStatus}

+ 12 - 4
components/search/form/SearchQueryBox.jsx

@@ -16,10 +16,13 @@ export default function SearchQueryBox({
 	canSearch,
 }) {
 	return (
-		<div className="grid gap-2">
+		// Important for flex layouts:
+		// - w-full ensures the box fills its container width.
+		// - min-w-0 allows the input row to shrink without overflow.
+		<div className="grid w-full min-w-0 gap-2">
 			<Label htmlFor="q">Suchbegriff</Label>
 
-			<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
+			<div className="flex w-full min-w-0 flex-col gap-2 sm:flex-row sm:items-center">
 				<Input
 					id="q"
 					name="q"
@@ -27,10 +30,15 @@ export default function SearchQueryBox({
 					onChange={(e) => onQDraftChange(e.target.value)}
 					placeholder="z. B. Bridgestone, Rechnung, Kundennummer…"
 					disabled={isSubmitting}
-					className="flex-1"
+					// Make the input take the remaining space next to the button.
+					className="flex-1 min-w-0"
 				/>
 
-				<Button type="submit" disabled={!canSearch || isSubmitting}>
+				<Button
+					type="submit"
+					disabled={!canSearch || isSubmitting}
+					className="shrink-0"
+				>
 					<Search className="h-4 w-4" />
 					Suchen
 				</Button>