mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
feat(dashboard): enhance dashboard with stat tiles for better data visualization
This commit is contained in:
@@ -27,6 +27,7 @@ export * from "./lib/input/PhoneInput";
|
||||
export * from "./lib/input/phone";
|
||||
export * from "./lib/data/AdvancedTable";
|
||||
export * from "./lib/data/WaitingFor";
|
||||
export * from "./lib/data/StatTile";
|
||||
export * from "./lib/feedback/use-error-handler";
|
||||
export * from "./lib/data/useServerTable";
|
||||
export * from "./lib/landing/LandingPage";
|
||||
|
||||
83
libs/ui/src/lib/data/StatTile.tsx
Normal file
83
libs/ui/src/lib/data/StatTile.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Group, Paper, Text, ThemeIcon, UnstyledButton } from '@mantine/core';
|
||||
import type { Icon } from '@tabler/icons-react';
|
||||
import { STATUS_TONE_COLOR, type StatusTone } from '@ema-platform/shared';
|
||||
import './stat-tile.css';
|
||||
|
||||
export interface StatTileProps {
|
||||
label: string;
|
||||
/** The number itself. A string so callers can pass "—" while loading. */
|
||||
value: ReactNode;
|
||||
/** One line under the value: what the number means, or how it is trending. */
|
||||
hint?: ReactNode;
|
||||
icon?: Icon;
|
||||
/**
|
||||
* Which tone the icon carries. Tone rather than colour so a tile counting
|
||||
* overdue work is the same red as an overdue badge.
|
||||
*/
|
||||
tone?: StatusTone;
|
||||
/** Makes the whole tile a button — use when the number has somewhere to go. */
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* One figure on a dashboard.
|
||||
*
|
||||
* The four stat cards on the backoffice home were `<Card>` + two `<Text>`,
|
||||
* re-declared inline on every dashboard that wanted them — so the logistics
|
||||
* overview and the backoffice home showed the same kind of number at different
|
||||
* sizes. The number leads, the label sits above it small and quiet, and the
|
||||
* icon is decoration that carries the tone.
|
||||
*
|
||||
* A tile with `onClick` becomes a real button: dashboards exist to be a
|
||||
* jumping-off point, and a count you cannot click is a dead end.
|
||||
*/
|
||||
export function StatTile({ label, value, hint, icon: TileIcon, tone, onClick }: StatTileProps) {
|
||||
const body = (
|
||||
<>
|
||||
<Group justify="space-between" align="flex-start" wrap="nowrap">
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700} lh={1.4}>
|
||||
{label}
|
||||
</Text>
|
||||
{TileIcon && (
|
||||
<ThemeIcon
|
||||
size={38}
|
||||
radius="md"
|
||||
variant="light"
|
||||
color={tone ? STATUS_TONE_COLOR[tone] : undefined}
|
||||
>
|
||||
<TileIcon size={20} stroke={1.7} />
|
||||
</ThemeIcon>
|
||||
)}
|
||||
</Group>
|
||||
<Text fz={30} fw={800} lh={1.15} mt="xs">
|
||||
{value}
|
||||
</Text>
|
||||
{hint && (
|
||||
<Text size="xs" c="dimmed" mt={2}>
|
||||
{hint}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
if (!onClick) {
|
||||
return (
|
||||
<Paper withBorder radius="lg" p="lg">
|
||||
{body}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<UnstyledButton
|
||||
onClick={onClick}
|
||||
className="ema-stat-tile-clickable"
|
||||
style={{ display: 'block', width: '100%', height: '100%' }}
|
||||
>
|
||||
<Paper withBorder radius="lg" p="lg" h="100%">
|
||||
{body}
|
||||
</Paper>
|
||||
</UnstyledButton>
|
||||
);
|
||||
}
|
||||
16
libs/ui/src/lib/data/stat-tile.css
Normal file
16
libs/ui/src/lib/data/stat-tile.css
Normal file
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* A clickable StatTile. The hover cue lives here rather than in inline style
|
||||
* handlers so it can use a real `:hover` — a JS mouseenter/leave pair misses
|
||||
* keyboard focus, and `:focus-visible` needs the app's focus ring anyway.
|
||||
*/
|
||||
.ema-stat-tile-clickable > * {
|
||||
transition: border-color 120ms ease, box-shadow 120ms ease;
|
||||
}
|
||||
|
||||
.ema-stat-tile-clickable:hover > * {
|
||||
border-color: var(--mantine-primary-color-filled);
|
||||
}
|
||||
|
||||
.ema-stat-tile-clickable:focus-visible > * {
|
||||
border-color: var(--mantine-primary-color-filled);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
SimpleGrid,
|
||||
Alert,
|
||||
Anchor,
|
||||
Badge,
|
||||
@@ -22,7 +23,20 @@ import {
|
||||
Title,
|
||||
useMantineTheme,
|
||||
} from '@mantine/core';
|
||||
import { IconAlertTriangle, IconInbox } from '@tabler/icons-react';
|
||||
import { SkipLink, MAIN_CONTENT_ID } from '../layout/SkipLink';
|
||||
import { StatusBadge } from '../feedback/StatusBadge';
|
||||
import { WaitingFor } from '../data/WaitingFor';
|
||||
import { StatTile } from '../data/StatTile';
|
||||
|
||||
/**
|
||||
* Fixed clock. The gallery is screenshotted by the visual suite, so a tile
|
||||
* reading "3d" must read "3d" tomorrow too — `Date.now()` would rewrite the
|
||||
* baseline every day.
|
||||
*/
|
||||
const GALLERY_NOW = '2026-08-21T00:00:00.000Z';
|
||||
const daysAgo = (n: number) =>
|
||||
new Date(Date.parse(GALLERY_NOW) - n * 86_400_000).toISOString();
|
||||
|
||||
/**
|
||||
* Every primitive the theme controls, on one page.
|
||||
@@ -264,8 +278,38 @@ export function ThemeGallery() {
|
||||
</Tabs>
|
||||
</Section>
|
||||
|
||||
<Section title="Status badges">
|
||||
<Group>
|
||||
<StatusBadge tone="success" label="Approved" />
|
||||
<StatusBadge tone="warning" label="Expiring" />
|
||||
<StatusBadge tone="danger" label="Rejected" />
|
||||
<StatusBadge tone="info" label="Submitted" />
|
||||
<StatusBadge tone="pending" label="Corrections requested" />
|
||||
<StatusBadge tone="neutral" label="Draft" />
|
||||
</Group>
|
||||
</Section>
|
||||
|
||||
<Section title="Waiting for">
|
||||
<Group>
|
||||
<WaitingFor since={GALLERY_NOW} />
|
||||
<WaitingFor since={daysAgo(9)} />
|
||||
<WaitingFor since={daysAgo(16)} />
|
||||
<WaitingFor since={daysAgo(30)} done />
|
||||
<WaitingFor since={null} />
|
||||
</Group>
|
||||
</Section>
|
||||
|
||||
<Section title="Stat tiles">
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }}>
|
||||
<StatTile label="Awaiting claim" value={12} hint="Nobody has picked these up" icon={IconInbox} tone="info" />
|
||||
<StatTile label="Needs applicant action" value={3} hint="Returned for corrections" icon={IconAlertTriangle} tone="pending" />
|
||||
<StatTile label="Overdue" value={2} hint="Past the turnaround target" icon={IconAlertTriangle} tone="danger" />
|
||||
<StatTile label="Not permitted" value="—" hint="No access to this queue" icon={IconInbox} tone="neutral" />
|
||||
</SimpleGrid>
|
||||
</Section>
|
||||
|
||||
<Section title="Table">
|
||||
<Table striped highlightOnHover withTableBorder>
|
||||
<Table>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Seafarer ID</Table.Th>
|
||||
|
||||
Reference in New Issue
Block a user