ui chnages

This commit is contained in:
Fistum
2026-08-21 11:16:22 +00:00
parent 8f6ebdafb1
commit 6b5b99f42d
31 changed files with 1173 additions and 1642 deletions

View File

@@ -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";

View File

@@ -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>
);
}

View 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>
);
}

View File

@@ -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 },