ui/ux on catalogue

This commit is contained in:
2026-09-18 19:19:34 +05:30
parent 041dc37861
commit 93735e232a
7 changed files with 259 additions and 94 deletions

View File

@@ -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. */}
<HStack gap={1} align="center" wrap="wrap" justify="end">
{/* 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. */}
<HStack gap={1} align="center" wrap="wrap" justify="start" width="100%">
<div style={{ width: 216, display: 'flex', gap: 8, alignItems: 'center' }}>
<IconButton
label={isFiltersOpen ? 'Hide filters' : 'Show filters'}
icon={<Funnel size={15} />}
variant={isFiltersOpen ? 'secondary' : 'ghost'}
size="sm"
onClick={() => setIsFiltersOpen((open) => !open)}
/>
<div style={{ flex: 1, minWidth: 0 }}>
<SearchInput
label="Search the catalogue"
value={keyword}
onChange={setKeyword}
placeholder="Search products…"
width="full"
/>
</div>
</div>
{activeFilters.map((filter) => (
<Token key={filter.key} label={filter.label} size="sm" onRemove={filter.clear} />
))}
{activeFilters.length > 1 ? (
<Button label="Clear all" variant="ghost" size="sm" onClick={clearFilters} />
) : null}
<IconButton
label={isFiltersOpen ? 'Hide filters' : 'Show filters'}
icon={<Funnel size={15} />}
variant={isFiltersOpen ? 'secondary' : 'ghost'}
size="sm"
onClick={() => setIsFiltersOpen((open) => !open)}
/>
<SearchInput
label="Search the catalogue"
value={keyword}
onChange={setKeyword}
placeholder="Search products…"
width={220}
/>
</HStack>
<div className="catalogue-layout" data-rail={isFiltersOpen ? 'open' : 'closed'}>

View File

@@ -4,7 +4,7 @@ import { VStack } from '@astryxdesign/core/VStack';
import { ChevronDown } from 'lucide-react';
import type { CatalogueBrand } from '@/api/types';
const COLLAPSED_COUNT = 8;
const COLLAPSED_COUNT = 25;
/**
* The brand rail.

View File

@@ -22,6 +22,7 @@ import {
import type { DeliveryRow, OrderRow, TenantLocation } from '@/api/types';
import { KpiCard } from '@/components/KpiCard';
import { Tab, TabBar } from '@/components/TabBar';
import { StickyRow } from '@/components/StickyRow';
import { useSelection, type Selection } from '@/components/useSelection';
import { Panel } from '@/components/Panel';
import { TablePager } from '@/components/TablePager';
@@ -244,57 +245,62 @@ export function DispatchBoard({ scope }: { scope: DispatchScope }) {
operations cluster beside it was removed earlier — and a card holding
nothing but a tab track is a box drawn round a box. Every other page
stands its switcher directly on the page. */}
<TabBar aria-label="Dispatch views">
<Tab
label="By rider"
icon={<Bike size={14} />}
count={totals.riders || undefined}
isActive={mode === 'riders'}
onClick={() => changeMode('riders')}
/>
<Tab
label="By store"
icon={<Store size={14} />}
count={branches.length || undefined}
isActive={mode === 'stores'}
onClick={() => changeMode('stores')}
/>
{/* A shop's customer book is its own. Grouping a partner's rounds by
customer would mean reading every merchant's book to put a name to
a phone number, so the tab is not offered on a partner board. */}
{scope.kind === 'tenant' ? (
{/* Held under the nav bar while the board scrolls. Dispatch builds its
own switcher instead of going through `PageHeader`, so it asks for
the behaviour directly — same component, same row, same canvas. */}
<StickyRow>
<TabBar aria-label="Dispatch views">
<Tab
label="By customer"
icon={<Users size={14} />}
isActive={mode === 'customers'}
onClick={() => changeMode('customers')}
label="By rider"
icon={<Bike size={14} />}
count={totals.riders || undefined}
isActive={mode === 'riders'}
onClick={() => changeMode('riders')}
/>
) : null}
<Tab
label="By store"
icon={<Store size={14} />}
count={branches.length || undefined}
isActive={mode === 'stores'}
onClick={() => changeMode('stores')}
/>
{/* A shop's customer book is its own. Grouping a partner's rounds by
customer would mean reading every merchant's book to put a name to
a phone number, so the tab is not offered on a partner board. */}
{scope.kind === 'tenant' ? (
<Tab
label="By customer"
icon={<Users size={14} />}
isActive={mode === 'customers'}
onClick={() => changeMode('customers')}
/>
) : null}
{/* The three below group the day differently from the three above:
those slice the same stops by whose they are, these report on how
the day went. The rule in the track says so. */}
<Tab
label="Active fleet"
icon={<Radio size={14} />}
isActive={mode === 'active'}
onClick={() => changeMode('active')}
isGroupStart
isLive
/>
<Tab
label="Plan vs actual"
icon={<Timer size={14} />}
isActive={mode === 'timing'}
onClick={() => changeMode('timing')}
/>
<Tab
label="Unit economics"
icon={<IndianRupee size={14} />}
isActive={mode === 'profit'}
onClick={() => changeMode('profit')}
/>
</TabBar>
{/* The three below group the day differently from the three above:
those slice the same stops by whose they are, these report on how
the day went. The rule in the track says so. */}
<Tab
label="Active fleet"
icon={<Radio size={14} />}
isActive={mode === 'active'}
onClick={() => changeMode('active')}
isGroupStart
isLive
/>
<Tab
label="Plan vs actual"
icon={<Timer size={14} />}
isActive={mode === 'timing'}
onClick={() => changeMode('timing')}
/>
<Tab
label="Unit economics"
icon={<IndianRupee size={14} />}
isActive={mode === 'profit'}
onClick={() => changeMode('profit')}
/>
</TabBar>
</StickyRow>
{/* The same four-card row every other page uses — same `kpi-grid`, same