Files
platform/src/workspaces/components/Data/StateBlock.tsx
T
pastilhas 5ce1a2928e inputs and data views keyed: search, select, tabs, state blocks, data table
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.
2026-08-23 04:10:33 +00:00

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