From 93735e232a86f5e352927f9a212cd0b0f1112fa3 Mon Sep 17 00:00:00 2001 From: abhishek Date: Fri, 18 Sep 2026 19:19:34 +0530 Subject: [PATCH] ui/ux on catalogue --- src/components/PageHeader.tsx | 55 ++++++---- src/components/StickyRow.tsx | 59 ++++++++++ src/components/trailMap.css | 1 + src/features/catalogue/CatalogueBrowser.tsx | 46 +++++--- src/features/catalogue/CatalogueSidebar.tsx | 2 +- .../store-admin/pages/DispatchPage.tsx | 102 +++++++++--------- src/index.css | 88 +++++++++++++-- 7 files changed, 259 insertions(+), 94 deletions(-) create mode 100644 src/components/StickyRow.tsx diff --git a/src/components/PageHeader.tsx b/src/components/PageHeader.tsx index c8a73eb..9c6a08c 100644 --- a/src/components/PageHeader.tsx +++ b/src/components/PageHeader.tsx @@ -1,4 +1,5 @@ import type { ReactNode } from 'react'; +import { StickyRow } from './StickyRow'; export interface PageHeaderProps { title: string; @@ -39,9 +40,19 @@ export interface PageHeaderProps { * * The tabs' spacing lives here rather than at each call site, so the five pages * that have tabs cannot drift apart from each other again. + * + * ── Held under the nav bar ────────────────────────────────────────────────── + * + * This row does not scroll away. It used to: reaching the bottom of a long list + * put the tab you were in, the search and every action button off the top of + * the window, so the only way back to the controls over what you were reading + * was to scroll back through all of it. The page still scrolls normally — + * nothing here gets a scrollbar of its own — the row simply stays. The + * behaviour and the canvas it paints live in `StickyRow` and `.page-sticky`. */ export function PageHeader({ title, actions, tabs, isTabsInline }: PageHeaderProps) { const hasRow = Boolean(actions || (isTabsInline && tabs)); + const hasTabsRow = Boolean(tabs && !isTabsInline); return ( <> @@ -64,29 +75,33 @@ export function PageHeader({ title, actions, tabs, isTabsInline }: PageHeaderPro below the nav bar while Inventory sat at 24. The gap now comes from one place, the column's own padding, and every page matches. */} - {hasRow ? ( -
- {/* Left of the row when inline, so the tabs start at the page's left - edge and the actions stay on the right. */} - {isTabsInline && tabs ?
{tabs}
: } + {hasRow || hasTabsRow ? ( + + {hasRow ? ( +
+ {/* Left of the row when inline, so the tabs start at the page's + left edge and the actions stay on the right. */} + {isTabsInline && tabs ?
{tabs}
: } - {actions ? ( -
- {actions} -
+ {actions ? ( +
+ {actions} +
+ ) : null} +
) : null} -
- ) : null} - {tabs && !isTabsInline ?
{tabs}
: null} + {hasTabsRow ?
{tabs}
: null} + + ) : null} ); } diff --git a/src/components/StickyRow.tsx b/src/components/StickyRow.tsx new file mode 100644 index 0000000..392b723 --- /dev/null +++ b/src/components/StickyRow.tsx @@ -0,0 +1,59 @@ +import { useEffect, useRef, useState, type ReactNode } from 'react'; + +/** + * A row of page controls held under the nav bar while the page scrolls. + * + * Whatever sits above a page — its view tabs, its search, its action buttons — + * used to leave with the scroll. Reaching the bottom of a long list put every + * control over that list off the top of the window, and the only way back to + * them was to scroll back through everything just read. The further in somebody + * was, the further from the controls. + * + * The page still scrolls normally. Nothing here gets a scrollbar of its own — + * the row simply stays where it is. Positioning and the canvas it paints live + * in `.page-sticky`; this component exists for the one part CSS cannot answer, + * which is whether the row is currently pinned. + * + * `PageHeader` wraps itself in this, so every page built from it gets the + * behaviour without asking. Dispatch builds its own switcher rather than going + * through `PageHeader`, so it wraps itself — which is the reason this is a + * component and not four more lines inside the header. + */ +export function StickyRow({ children }: { children: ReactNode }) { + /** + * Pinned or not, so the hairline can be drawn only when it is. + * + * At rest the nav bar's own hairline sits just above this one, and two + * parallel lines across the page with nothing between them read as a mistake. + * The line is what separates the row from content passing UNDER it, so it + * belongs to the pinned state rather than to the row. + * + * Watched with an observer rather than a scroll handler: a scroll listener + * runs on every frame of every scroll to answer a question that changes + * twice. The negative top margin puts the observer's boundary a pixel above + * where the row comes to rest, so "no longer fully inside that box" means + * exactly "stuck". + */ + const rowRef = useRef(null); + const [isStuck, setIsStuck] = useState(false); + + useEffect(() => { + const element = rowRef.current; + /* Guarded for any renderer without it — the row still sticks, it just + never grows its line, which is a missing hairline rather than a crash. */ + if (!element || typeof IntersectionObserver === 'undefined') return; + + const observer = new IntersectionObserver( + ([entry]) => setIsStuck(Boolean(entry && !entry.isIntersecting)), + { rootMargin: '-57px 0px 0px 0px', threshold: 1 }, + ); + observer.observe(element); + return () => observer.disconnect(); + }, []); + + return ( +
+ {children} +
+ ); +} diff --git a/src/components/trailMap.css b/src/components/trailMap.css index 314b498..06cb857 100644 --- a/src/components/trailMap.css +++ b/src/components/trailMap.css @@ -8,6 +8,7 @@ .trail-map { position: relative; + z-index: 0; width: 100%; overflow: hidden; border: 1px solid var(--color-border); diff --git a/src/features/catalogue/CatalogueBrowser.tsx b/src/features/catalogue/CatalogueBrowser.tsx index 20595a9..af2ec5f 100644 --- a/src/features/catalogue/CatalogueBrowser.tsx +++ b/src/features/catalogue/CatalogueBrowser.tsx @@ -377,27 +377,43 @@ export function CatalogueBrowser({ It stays above both columns rather than inside the rail: the search narrows the whole catalogue, and the rail only lists brands. */} - + {/* Pinned to the right-hand corner, and stated inline so it cannot be + undone by a stylesheet. + + `justify="end"` was already here and already resolves to + `justify-content: flex-end` — HStack maps `justify` to the main axis, + and the rule is in the bundle — yet the row was reported rendering at + the top LEFT of the platform catalogue. A utility class landing after + the component's own is the only thing that explains that, and an + inline declaration is the one thing such a class cannot outrank. + `width` is explicit for the same reason rather than relying on the + flex default. */} + +
+ } + variant={isFiltersOpen ? 'secondary' : 'ghost'} + size="sm" + onClick={() => setIsFiltersOpen((open) => !open)} + /> +
+ +
+
+ {activeFilters.map((filter) => ( ))} {activeFilters.length > 1 ? (