101 lines
4.2 KiB
TypeScript
101 lines
4.2 KiB
TypeScript
import type { ReactNode } from 'react';
|
|
import './tabBar.css';
|
|
|
|
/**
|
|
* The console's view switcher — Orders / Deliveries / Counter sales, Catalogue
|
|
* / My stock / My requests, Store users / Terminal accounts / Riders.
|
|
*
|
|
* ── There were six of these ─────────────────────────────────────────────────
|
|
*
|
|
* `TabButton` was defined privately in `StoresPage`, `InventoryPage`,
|
|
* `ReportsPage`, `SalesPage`, `UsersPage` and `store-user/ui.tsx` — six copies
|
|
* of the same button, each styled inline, and they had already drifted: three
|
|
* of them filled the count badge with `--color-warning`, so "85" under Orders
|
|
* was drawn in the colour this console reserves for something being wrong. A
|
|
* count is not an alarm. Sales had the sensible version. Nobody could see the
|
|
* difference because no two of them are ever on screen together.
|
|
*
|
|
* ── Why it is a track and not loose pills ───────────────────────────────────
|
|
*
|
|
* The old buttons were transparent until selected, so an unselected set read as
|
|
* four pieces of text floating above the page with no indication they were one
|
|
* control or that picking one deselects the others. They sit in a recessed
|
|
* track now, with the selected tab raised out of it on the card surface — the
|
|
* standard segmented control, and the one shape that says "these are the views,
|
|
* you are in this one" without needing a label to say so.
|
|
*/
|
|
|
|
export interface TabBarProps {
|
|
children: ReactNode;
|
|
/** `sm` for a secondary strip — a filter under a primary switcher. */
|
|
size?: 'md' | 'sm';
|
|
'aria-label'?: string;
|
|
}
|
|
|
|
export function TabBar({ children, size = 'md', ...rest }: TabBarProps) {
|
|
return (
|
|
<div className="tabbar" data-size={size} role="tablist" aria-label={rest['aria-label']}>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export interface TabProps {
|
|
label: string;
|
|
icon?: ReactNode;
|
|
isActive: boolean;
|
|
onClick: () => void;
|
|
/**
|
|
* The count beside the label. Omit it and no count is drawn.
|
|
*
|
|
* Whether ZERO is worth drawing depends on what the tab is, so the caller
|
|
* decides rather than this component:
|
|
*
|
|
* - A VIEW passes `x.length || undefined`. "Deliveries 0" beside "Orders
|
|
* 85" reads as a broken tab; "Deliveries" alone says nothing and lets the
|
|
* empty state explain.
|
|
* - A STATUS in a ladder passes the raw count. "Cancelled 0" is the answer
|
|
* to a question the reader is asking — nothing was cancelled — and a rung
|
|
* that loses its number when it empties makes the ladder jump about as
|
|
* the day goes on.
|
|
*/
|
|
count?: number | undefined;
|
|
/**
|
|
* Draws a hairline before this tab, dividing the track into groups.
|
|
*
|
|
* Sales needs it: Orders and Deliveries are two halves of the app's own
|
|
* trade, and Counter sales is a different channel entirely. The rule says so
|
|
* without spending a tab on a heading.
|
|
*/
|
|
isGroupStart?: boolean;
|
|
/**
|
|
* A pulsing dot after the label, for a view that updates by itself.
|
|
*
|
|
* Dispatch's Active Fleet is the one: it reads rider positions live, and the
|
|
* dot is how the board says so. A named prop rather than a general-purpose
|
|
* slot — a `trailing` that took any node would be the seam this component
|
|
* exists to close, and there is exactly one live view in the console.
|
|
*/
|
|
isLive?: boolean;
|
|
}
|
|
|
|
export function Tab({ label, icon, isActive, onClick, count, isGroupStart, isLive }: TabProps) {
|
|
return (
|
|
<button
|
|
type="button"
|
|
role="tab"
|
|
className="tabbar-tab"
|
|
aria-selected={isActive}
|
|
{...(isGroupStart ? { 'data-group-start': 'true' } : {})}
|
|
onClick={onClick}
|
|
>
|
|
{icon ? <span className="tabbar-icon">{icon}</span> : null}
|
|
<span className="tabbar-label">{label}</span>
|
|
{count === undefined ? null : <span className="tabbar-count">{count}</span>}
|
|
{/* Labelled, not decorative: a green dot with no accessible name is a
|
|
colour that means something to sighted readers only. */}
|
|
{isLive ? <span className="tabbar-live" role="img" aria-label="updating live" /> : null}
|
|
</button>
|
|
);
|
|
}
|