initial commit
This commit is contained in:
100
src/components/TabBar.tsx
Normal file
100
src/components/TabBar.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user