The DataTable's pagination carries the only interpolated strings in this area
('Showing page {{current}} of {{total}}', '{{size}} / page'). No plural forms:
nothing here is a count-varying noun, so inventing one would only add a key
nobody uses.
DataTable.test.tsx now renders inside an I18nProvider with the real en-GB
catalog, so it still asserts the English a user sees rather than a dot-path.
Left/right utilities became logical (ms-/me-/ps-/pe-/start-) per D3.
77 lines
2.7 KiB
TypeScript
77 lines
2.7 KiB
TypeScript
import type { LucideIcon } from 'lucide-react';
|
|
import type { ReactNode } from 'react';
|
|
import { Loader2, TriangleAlert } from 'lucide-react';
|
|
import { cn } from 'helpers/cn';
|
|
import { useTranslation } from '@/components/locales';
|
|
|
|
// Loading, failed, empty. See docs/design-language-interface.md § 6.
|
|
//
|
|
// These are the states a new user meets FIRST — before a single record exists, before a token is
|
|
// pasted, when something is misconfigured. Treating them as filler is how an app ends up saying "No
|
|
// results" to someone who cannot tell whether that means "nothing here" or "it is broken".
|
|
//
|
|
// One implementation, so all three agree on size, spacing and centring across every app. They fill
|
|
// their container rather than sitting at the top, because a half-empty pane with a line of grey text
|
|
// stuck to the ceiling reads as a rendering bug.
|
|
|
|
const Shell = ({ children, className }: { children: ReactNode; className?: string }) => (
|
|
<div className={cn('flex h-full flex-col items-center justify-center gap-2 p-6 text-center', className)}>
|
|
{children}
|
|
</div>
|
|
);
|
|
|
|
export const LoadingBlock = ({ label }: { label?: string }) => {
|
|
const { t } = useTranslation();
|
|
return (
|
|
<Shell>
|
|
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
|
<p className="text-sm text-muted-foreground">{label ?? t('state.loading')}</p>
|
|
</Shell>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* The real message is shown, not a friendly substitute for it. On a self-hosted platform the person
|
|
* reading an error is almost always the person who can fix it, and "Something went wrong" costs them
|
|
* the one clue they needed.
|
|
*/
|
|
export const ErrorBlock = ({ title, message, action }: { title: string; message?: ReactNode; action?: ReactNode }) => (
|
|
<Shell>
|
|
<TriangleAlert className="h-5 w-5 text-warning" />
|
|
<div className="flex flex-col gap-1">
|
|
<p className="text-base font-semibold">{title}</p>
|
|
{message && <p className="max-w-sm text-sm text-muted-foreground">{message}</p>}
|
|
</div>
|
|
{action}
|
|
</Shell>
|
|
);
|
|
|
|
/**
|
|
* `hint` is not decoration — it is where the state says what WOULD be here and why it is not, which
|
|
* is the difference between an empty state and a shrug.
|
|
*/
|
|
export const EmptyBlock = ({
|
|
icon: Icon,
|
|
title,
|
|
hint,
|
|
action,
|
|
}: {
|
|
icon?: LucideIcon;
|
|
title: string;
|
|
hint?: ReactNode;
|
|
action?: ReactNode;
|
|
}) => (
|
|
<Shell>
|
|
{Icon && (
|
|
<div className="mb-1 flex h-10 w-10 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
|
<Icon className="h-5 w-5" />
|
|
</div>
|
|
)}
|
|
<div className="flex flex-col gap-1">
|
|
<p className="text-base font-semibold">{title}</p>
|
|
{hint && <p className="max-w-sm text-sm text-muted-foreground">{hint}</p>}
|
|
</div>
|
|
{action}
|
|
</Shell>
|
|
);
|