mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
ui chnages
This commit is contained in:
@@ -107,12 +107,26 @@ export const SEAFARER_REGISTRATION_STATUS_LABELS: Record<SeafarerRegistrationSta
|
||||
REJECTED: 'Rejected',
|
||||
};
|
||||
|
||||
export const SEAFARER_REGISTRATION_STATUS_COLORS: Record<SeafarerRegistrationStatus, string> = {
|
||||
DRAFT: 'gray',
|
||||
SUBMITTED: 'blue',
|
||||
RESUBMIT_REQUIRED: 'orange',
|
||||
APPROVED: 'teal',
|
||||
REJECTED: 'red',
|
||||
/**
|
||||
* Registration status → platform tone.
|
||||
*
|
||||
* Tones, not colours. `StatusTone` is the platform's status vocabulary and the
|
||||
* one place a tone becomes a colour (`STATUS_TONE_COLOR` in
|
||||
* @ema-platform/shared), so this map cannot drift the way `APPROVED: 'teal'`
|
||||
* had already drifted from every other feature's green success.
|
||||
*
|
||||
* The union is repeated rather than imported because @ema-platform/api does not
|
||||
* depend on the theme layer, and should not start to for five string literals.
|
||||
*/
|
||||
export const SEAFARER_REGISTRATION_STATUS_TONES: Record<
|
||||
SeafarerRegistrationStatus,
|
||||
'success' | 'warning' | 'danger' | 'info' | 'pending' | 'neutral'
|
||||
> = {
|
||||
DRAFT: 'neutral',
|
||||
SUBMITTED: 'info',
|
||||
RESUBMIT_REQUIRED: 'pending',
|
||||
APPROVED: 'success',
|
||||
REJECTED: 'danger',
|
||||
};
|
||||
|
||||
/** Human label for each answer — the review table and the summary both use it. */
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { CSSProperties } from 'react';
|
||||
import { createTheme, rem } from '@mantine/core';
|
||||
|
||||
/**
|
||||
@@ -97,6 +98,40 @@ export const baseTheme = createTheme({
|
||||
Select: { defaultProps: { radius: 'md' } },
|
||||
PasswordInput: { defaultProps: { radius: 'md' } },
|
||||
|
||||
// The page sits on a tinted surface and cards float on white. Without this
|
||||
// the main area is the same white as every Paper on it, and the card
|
||||
// borders are the only thing separating content from chrome.
|
||||
AppShell: {
|
||||
styles: { main: { background: 'var(--ema-surface-page)' } },
|
||||
},
|
||||
|
||||
// Tables — the registry look: quiet uppercase headers, hairline row
|
||||
// borders, a tint on hover, no zebra striping and no column rules. Set
|
||||
// once here so the 14 pages rendering a raw <Table> match the 28 that go
|
||||
// through AdvancedTable instead of each picking their own density.
|
||||
//
|
||||
// Header text is `text-secondary` rather than the lighter dimmed gray the
|
||||
// mockup used: at 11px uppercase, gray-5 on white fails 4.5:1.
|
||||
Table: {
|
||||
defaultProps: { highlightOnHover: true, verticalSpacing: 'sm', horizontalSpacing: 'md' },
|
||||
styles: {
|
||||
table: {
|
||||
'--table-border-color': 'var(--ema-border-subtle)',
|
||||
'--table-hover-color': 'var(--ema-surface-page)',
|
||||
'--table-striped-color': 'var(--ema-surface-sunken)',
|
||||
} as CSSProperties,
|
||||
th: {
|
||||
fontSize: rem(11),
|
||||
fontWeight: 700,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
color: 'var(--ema-text-secondary)',
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
td: { fontSize: rem(13) },
|
||||
},
|
||||
},
|
||||
|
||||
// Mantine's stock scroll wrapper (NativeScrollArea) discards the
|
||||
// max-height it's handed unless scrollAreaComponent is set, so a modal
|
||||
// taller than the viewport just gets clipped with no way to scroll it.
|
||||
|
||||
@@ -9,6 +9,7 @@ export * from "./lib/feedback/FeatureUnavailable";
|
||||
export * from "./lib/feedback/EmptyState";
|
||||
export * from "./lib/feedback/ErrorState";
|
||||
export * from "./lib/feedback/PageLoader";
|
||||
export * from "./lib/feedback/StatusBadge";
|
||||
export * from "./lib/components/MaritimeLoader";
|
||||
export * from "./lib/theme/maritime-loader-theme";
|
||||
export * from "./lib/layout/AppHeader";
|
||||
|
||||
@@ -51,6 +51,10 @@ interface AdvancedTableProps<T> {
|
||||
rowStyle?: (row: T, index: number) => CSSProperties | undefined;
|
||||
/** Makes rows clickable (adds pointer cursor). */
|
||||
onRowClick?: (row: T) => void;
|
||||
/** Card title, top-left. Defaults to `tableName`, which every caller already passes. */
|
||||
title?: ReactNode;
|
||||
/** Search box, filters, export — rendered top-right before Refresh/View. */
|
||||
toolbar?: ReactNode;
|
||||
}
|
||||
|
||||
function getByPath(obj: unknown, path?: string): unknown {
|
||||
@@ -82,6 +86,8 @@ export function AdvancedTable<T extends { id?: string | number }>({
|
||||
verticalSpacing = "sm",
|
||||
rowStyle,
|
||||
onRowClick,
|
||||
title,
|
||||
toolbar,
|
||||
}: AdvancedTableProps<T>) {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState<boolean[]>(
|
||||
@@ -94,13 +100,22 @@ export function AdvancedTable<T extends { id?: string | number }>({
|
||||
});
|
||||
const shownColumns = columns.filter((_, i) => visible[i] ?? true);
|
||||
|
||||
const heading = title ?? tableName;
|
||||
const from = itemCount === 0 ? 0 : pageIndex * pageSize + 1;
|
||||
const to = Math.min(itemCount, pageIndex * pageSize + data.length);
|
||||
|
||||
return (
|
||||
<Paper withBorder radius="md" p="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Paper withBorder radius="lg" p={0}>
|
||||
<Group justify="space-between" px="md" py="sm" wrap="wrap" gap="sm">
|
||||
<Group gap="xs">
|
||||
<Text fw={600}>{""}</Text>
|
||||
{heading && (
|
||||
<Text fw={600} size="sm">
|
||||
{heading}
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<Group gap="xs" wrap="wrap">
|
||||
{toolbar}
|
||||
{refresh && (
|
||||
<Button
|
||||
variant="default"
|
||||
@@ -162,13 +177,7 @@ export function AdvancedTable<T extends { id?: string | number }>({
|
||||
</Group>
|
||||
|
||||
<Table.ScrollContainer minWidth={480}>
|
||||
<Table
|
||||
striped
|
||||
highlightOnHover
|
||||
withTableBorder
|
||||
withColumnBorders
|
||||
verticalSpacing={verticalSpacing}
|
||||
>
|
||||
<Table verticalSpacing={verticalSpacing}>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
{shownColumns.map((col, i) => (
|
||||
@@ -230,8 +239,21 @@ export function AdvancedTable<T extends { id?: string | number }>({
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
|
||||
{(itemCount > pageSize || onPageSizeChange) && (
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Group
|
||||
justify="space-between"
|
||||
px="md"
|
||||
py="sm"
|
||||
style={{ borderTop: "1px solid var(--ema-border-subtle)" }}
|
||||
>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t("common.showingRange", {
|
||||
from,
|
||||
to,
|
||||
total: itemCount,
|
||||
defaultValue: "Showing {{from}}–{{to}} of {{total}}",
|
||||
})}
|
||||
</Text>
|
||||
<Group gap="sm">
|
||||
{onPageSizeChange && (
|
||||
<Select
|
||||
size="sm"
|
||||
@@ -253,7 +275,7 @@ export function AdvancedTable<T extends { id?: string | number }>({
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
30
libs/ui/src/lib/feedback/StatusBadge.tsx
Normal file
30
libs/ui/src/lib/feedback/StatusBadge.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Badge, type BadgeProps } from '@mantine/core';
|
||||
import { STATUS_TONE_COLOR, type StatusTone } from '@ema-platform/shared';
|
||||
|
||||
export interface StatusBadgeProps extends Omit<BadgeProps, 'color' | 'children'> {
|
||||
/** The platform tone this status maps onto. */
|
||||
tone: StatusTone;
|
||||
/** Human label. Already translated by the caller. */
|
||||
label: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* One badge for every status in the platform.
|
||||
*
|
||||
* There were 29 files rendering `<Badge variant="light" color={MAP[status]}>`,
|
||||
* each re-deciding size, variant and radius alongside the colour — which is why
|
||||
* a "pending" badge in one queue did not match "pending" in the next. Callers
|
||||
* now supply a tone and a label; how a status *looks* is decided once, here.
|
||||
*
|
||||
* Tone rather than colour on purpose: `STATUS_TONE_COLOR` is the single place a
|
||||
* tone becomes a Mantine colour, so restyling the platform's idea of "danger"
|
||||
* stays one edit rather than a sweep.
|
||||
*/
|
||||
export function StatusBadge({ tone, label, ...props }: StatusBadgeProps) {
|
||||
return (
|
||||
<Badge variant="light" size="sm" radius="sm" {...props} color={STATUS_TONE_COLOR[tone]}>
|
||||
{label}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
@@ -157,8 +157,8 @@ function SidebarItem({ item, collapsed, activePath, onNavigate, opened, onToggle
|
||||
height: rem(40),
|
||||
borderRadius: rem(10),
|
||||
opacity: item.soon ? 0.55 : 1,
|
||||
color: branchActive ? 'var(--mantine-color-blue-6)' : undefined,
|
||||
backgroundColor: branchActive ? 'var(--mantine-color-blue-light)' : undefined,
|
||||
color: branchActive ? 'var(--mantine-primary-color-filled)' : undefined,
|
||||
backgroundColor: branchActive ? 'var(--mantine-primary-color-light)' : undefined,
|
||||
}}
|
||||
>
|
||||
<ItemIcon size={20} stroke={1.6} />
|
||||
@@ -222,7 +222,10 @@ function SidebarItem({ item, collapsed, activePath, onNavigate, opened, onToggle
|
||||
opened={hasChildren ? opened : undefined}
|
||||
onChange={hasChildren ? onToggle : undefined}
|
||||
onClick={() => !hasChildren && onNavigate(item)}
|
||||
variant="light"
|
||||
// A leaf that is the current page is filled in the app's primary; a
|
||||
// branch whose child is current stays a tint, so the filled item is
|
||||
// always exactly the page you are on.
|
||||
variant={hasChildren ? 'light' : 'filled'}
|
||||
styles={{
|
||||
root: { borderRadius: rem(10), opacity: item.soon ? 0.7 : 1 },
|
||||
label: { fontWeight: 500 },
|
||||
|
||||
Reference in New Issue
Block a user