mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
363 lines
12 KiB
TypeScript
363 lines
12 KiB
TypeScript
import {
|
||
SimpleGrid,
|
||
Alert,
|
||
Anchor,
|
||
Badge,
|
||
Box,
|
||
Button,
|
||
Card,
|
||
Checkbox,
|
||
Divider,
|
||
Group,
|
||
Paper,
|
||
Radio,
|
||
Select,
|
||
Stack,
|
||
Switch,
|
||
Table,
|
||
Tabs,
|
||
Text,
|
||
Textarea,
|
||
TextInput,
|
||
ThemeIcon,
|
||
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.
|
||
*
|
||
* This is the visual-regression surface for theme work. A real feature page
|
||
* renders a handful of primitives and only their happy states; a theme change
|
||
* that breaks `Button disabled` or `TextInput error` would sail past it. This
|
||
* renders all of them, including the states nothing else shows, so a screenshot
|
||
* diff says precisely what a theme edit did.
|
||
*
|
||
* Deliberately free of data, auth and network so it cannot flake: mounted at
|
||
* `/__gallery` outside the protected routes, it needs no API and no database.
|
||
*
|
||
* Not part of the product. Excluded from the nav on purpose.
|
||
*/
|
||
|
||
const SWATCH_SHADES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] as const;
|
||
|
||
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
||
return (
|
||
<Stack gap="sm">
|
||
<Title order={3}>{title}</Title>
|
||
<Divider />
|
||
{children}
|
||
</Stack>
|
||
);
|
||
}
|
||
|
||
/** A full 0–9 ramp, so a palette swap is visible shade by shade. */
|
||
function ColorRamp({ name }: { name: string }) {
|
||
return (
|
||
<Stack gap={4}>
|
||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||
{name}
|
||
</Text>
|
||
<Group gap={0} wrap="nowrap">
|
||
{SWATCH_SHADES.map((shade) => (
|
||
<Box
|
||
key={shade}
|
||
style={{
|
||
background: `var(--mantine-color-${name}-${shade})`,
|
||
width: 48,
|
||
height: 40,
|
||
display: 'flex',
|
||
alignItems: 'flex-end',
|
||
justifyContent: 'center',
|
||
fontSize: 10,
|
||
// Shades 0-4 are light enough to need dark text; 5+ need light.
|
||
color: shade < 5 ? 'var(--mantine-color-black)' : 'var(--mantine-color-white)',
|
||
}}
|
||
>
|
||
{shade}
|
||
</Box>
|
||
))}
|
||
</Group>
|
||
</Stack>
|
||
);
|
||
}
|
||
|
||
export function ThemeGallery() {
|
||
const theme = useMantineTheme();
|
||
const paletteNames = Object.keys(theme.colors).filter((c) =>
|
||
// The stock Mantine ramps are noise here; show the ones this app defines,
|
||
// plus gray because overriding it is the riskiest single theme change.
|
||
c.startsWith('ema') || c === 'gray',
|
||
);
|
||
|
||
return (
|
||
<>
|
||
{/* Mirrors the real app shells, so the skip-link contract is testable
|
||
without a session. */}
|
||
<SkipLink />
|
||
<Box id={MAIN_CONTENT_ID} p="xl" style={{ maxWidth: 1100, margin: '0 auto' }}>
|
||
<Stack gap="xl">
|
||
<Stack gap={4}>
|
||
<Title order={1}>Theme Gallery</Title>
|
||
<Text c="dimmed">
|
||
Visual-regression surface for theme changes. Not a product page.
|
||
</Text>
|
||
</Stack>
|
||
|
||
<Section title="Typography">
|
||
<Stack gap="xs">
|
||
<Title order={1}>Heading 1 — Maritime licensing</Title>
|
||
<Title order={2}>Heading 2 — Seafarer registry</Title>
|
||
<Title order={3}>Heading 3 — Vessel registration</Title>
|
||
<Title order={4}>Heading 4 — Certificate of competency</Title>
|
||
<Title order={5}>Heading 5 — Sea service record</Title>
|
||
<Text>
|
||
Body text. The quick brown fox jumps over the lazy dog.
|
||
</Text>
|
||
<Text size="sm" c="dimmed">
|
||
Small dimmed text, used for subtitles and helper copy.
|
||
</Text>
|
||
<Text size="xs" c="dimmed">
|
||
Extra-small text, used for metadata.
|
||
</Text>
|
||
{/* Mixed-script line: the case a [lang] font swap would break. */}
|
||
<Text>Amharic: አበበ ግርማ — SF-2024-0001 — የአዲስ አበባ ወደብ</Text>
|
||
{/* Ge'ez at heading size, where its taller ascenders clip first. */}
|
||
<Title order={2}>የኢትዮጵያ ማሪታይም ባለስልጣን</Title>
|
||
<Title order={4}>የመርከበኞች መዝገብ — Seafarer Registry</Title>
|
||
<Text ff="monospace">Monospace: ET-IMO-0231 · 28,450 GT</Text>
|
||
</Stack>
|
||
</Section>
|
||
|
||
<Section title="Color ramps">
|
||
<Stack gap="md">
|
||
{paletteNames.map((name) => (
|
||
<ColorRamp key={name} name={name} />
|
||
))}
|
||
</Stack>
|
||
</Section>
|
||
|
||
<Section title="Buttons">
|
||
<Stack gap="sm">
|
||
<Group>
|
||
<Button>Filled</Button>
|
||
<Button variant="light">Light</Button>
|
||
<Button variant="outline">Outline</Button>
|
||
<Button variant="subtle">Subtle</Button>
|
||
<Button variant="default">Default</Button>
|
||
<Button variant="white">White</Button>
|
||
</Group>
|
||
<Group>
|
||
<Button size="xs">xs</Button>
|
||
<Button size="sm">sm</Button>
|
||
<Button size="md">md</Button>
|
||
<Button size="lg">lg</Button>
|
||
</Group>
|
||
<Group>
|
||
<Button color="red">Destructive</Button>
|
||
<Button disabled>Disabled</Button>
|
||
<Button loading>Loading</Button>
|
||
<Button fullWidth>Full width</Button>
|
||
</Group>
|
||
</Stack>
|
||
</Section>
|
||
|
||
<Section title="Surfaces">
|
||
<Group align="stretch" grow>
|
||
<Card withBorder>
|
||
<Text fw={600}>Card withBorder</Text>
|
||
<Text size="sm" c="dimmed">
|
||
Radius and shadow come from the theme.
|
||
</Text>
|
||
</Card>
|
||
<Paper p="md" withBorder>
|
||
<Text fw={600}>Paper withBorder</Text>
|
||
<Text size="sm" c="dimmed">
|
||
Default radius applies here too.
|
||
</Text>
|
||
</Paper>
|
||
<Paper p="md" shadow="md">
|
||
<Text fw={600}>Paper shadow=md</Text>
|
||
<Text size="sm" c="dimmed">
|
||
Shadow ramp check.
|
||
</Text>
|
||
</Paper>
|
||
</Group>
|
||
</Section>
|
||
|
||
<Section title="Badges">
|
||
<Group>
|
||
<Badge>Default</Badge>
|
||
<Badge color="green">Active</Badge>
|
||
<Badge color="yellow">Pending</Badge>
|
||
<Badge color="red">Suspended</Badge>
|
||
<Badge color="orange">Expiring</Badge>
|
||
<Badge color="gray">Draft</Badge>
|
||
<Badge variant="filled">Filled</Badge>
|
||
<Badge variant="outline">Outline</Badge>
|
||
<Badge variant="dot">Dot</Badge>
|
||
</Group>
|
||
</Section>
|
||
|
||
<Section title="Form controls">
|
||
<Stack gap="md" style={{ maxWidth: 520 }}>
|
||
<TextInput label="Text input" placeholder="you@example.com" />
|
||
<TextInput
|
||
label="With description"
|
||
description="Helper text that persists, unlike a placeholder."
|
||
placeholder="ET-000000"
|
||
/>
|
||
<TextInput
|
||
label="Error state"
|
||
error="This field is required."
|
||
placeholder="Required"
|
||
/>
|
||
<TextInput label="Disabled" disabled value="Cannot edit" />
|
||
<TextInput label="Read-only" readOnly value="Read-only value" />
|
||
<Select
|
||
label="Select"
|
||
data={['Ethiopian', 'Djiboutian', 'Kenyan']}
|
||
placeholder="Pick one"
|
||
/>
|
||
<Textarea label="Textarea" placeholder="Full permanent address" />
|
||
<Group>
|
||
<Checkbox label="Checkbox" defaultChecked />
|
||
<Radio label="Radio" defaultChecked />
|
||
<Switch label="Switch" defaultChecked />
|
||
</Group>
|
||
</Stack>
|
||
</Section>
|
||
|
||
<Section title="Feedback">
|
||
<Stack gap="sm">
|
||
<Alert color="blue" title="Information">
|
||
A unique Seafarer ID is generated on approval.
|
||
</Alert>
|
||
<Alert color="yellow" title="Warning">
|
||
Upload clear copies of all required documents.
|
||
</Alert>
|
||
<Alert color="red" title="Error">
|
||
The certificate number could not be verified.
|
||
</Alert>
|
||
<Group>
|
||
<ThemeIcon size="lg">Ic</ThemeIcon>
|
||
<ThemeIcon size="lg" variant="light">
|
||
Lt
|
||
</ThemeIcon>
|
||
<ThemeIcon size="lg" color="green" variant="light">
|
||
Ok
|
||
</ThemeIcon>
|
||
</Group>
|
||
</Stack>
|
||
</Section>
|
||
|
||
<Section title="Tabs">
|
||
<Tabs defaultValue="overview">
|
||
<Tabs.List>
|
||
<Tabs.Tab value="overview">Overview</Tabs.Tab>
|
||
<Tabs.Tab value="training">Training</Tabs.Tab>
|
||
<Tabs.Tab value="medical">Medical</Tabs.Tab>
|
||
</Tabs.List>
|
||
<Tabs.Panel value="overview" pt="md">
|
||
<Text size="sm">Overview panel content.</Text>
|
||
</Tabs.Panel>
|
||
</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>
|
||
<Table.Thead>
|
||
<Table.Tr>
|
||
<Table.Th>Seafarer ID</Table.Th>
|
||
<Table.Th>Name</Table.Th>
|
||
<Table.Th>Region</Table.Th>
|
||
<Table.Th>Status</Table.Th>
|
||
</Table.Tr>
|
||
</Table.Thead>
|
||
<Table.Tbody>
|
||
<Table.Tr>
|
||
<Table.Td>
|
||
<Anchor size="sm">SF-2024-0001</Anchor>
|
||
</Table.Td>
|
||
<Table.Td>Abebe Girma</Table.Td>
|
||
<Table.Td>Addis Ababa</Table.Td>
|
||
<Table.Td>
|
||
<Badge color="green">Active</Badge>
|
||
</Table.Td>
|
||
</Table.Tr>
|
||
<Table.Tr>
|
||
<Table.Td>
|
||
<Anchor size="sm">SF-2024-0002</Anchor>
|
||
</Table.Td>
|
||
<Table.Td>ሳራ ተስፋዬ</Table.Td>
|
||
<Table.Td>Dire Dawa</Table.Td>
|
||
<Table.Td>
|
||
<Badge color="yellow">Pending</Badge>
|
||
</Table.Td>
|
||
</Table.Tr>
|
||
</Table.Tbody>
|
||
</Table>
|
||
</Section>
|
||
|
||
<Section title="Focus states">
|
||
<Text size="sm" c="dimmed">
|
||
Tab through these to check the focus ring. Every interactive element
|
||
must show a visible indicator.
|
||
</Text>
|
||
<Group mt="sm">
|
||
<Button>Button</Button>
|
||
<Anchor href="#gallery-focus">Link</Anchor>
|
||
<TextInput placeholder="Input" />
|
||
<Checkbox label="Checkbox" />
|
||
</Group>
|
||
</Section>
|
||
</Stack>
|
||
</Box>
|
||
</>
|
||
);
|
||
}
|