8e937344ff
Phase 2 pattern extraction from the design/ui-revamp-2026-06 spec. - KPIStat: extract the stat card pattern from DashboardClient (used four times inline). Supports tone (default/primary/accent/warning/danger) and an optional trend indicator. Reuses .admin-stat-card* CSS classes. - EmptyState: generic centered icon + title + description + single primary CTA. Action renders as <Link> when href is provided, else a <button>; both use .ha-btn-primary. - LoadingState: skeleton list driven by .ha-skeleton. Renders an optional ha-eyebrow label above N rows (icon tile + label + value placeholders). Each row uses the new admin design tokens. - AdminBadge: tighten variants to the new design tokens. Legacy variant= prop kept for back-compat (default/success/warning/danger/ info); new tone= prop adds neutral/primary/accent/success. All colors now reference --admin-* tokens; no hardcoded hex values. Added a 1px tone-aware border for stronger definition on the cream canvas. AdminStatusBadge + AdminCountBadge updated to use the new tone prop directly. - design-system/index.tsx: re-export KPIStat, EmptyState, LoadingState from the design-system barrel alongside AdminBadge. npx tsc --noEmit clean.
108 lines
2.6 KiB
TypeScript
108 lines
2.6 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import type { ReactNode } from "react";
|
|
import { Inbox } from "lucide-react";
|
|
|
|
type EmptyStateAction = {
|
|
/** Visible label for the call-to-action. */
|
|
label: string;
|
|
/** If provided, renders a Next.js Link. Mutually exclusive with onClick. */
|
|
href?: string;
|
|
/** If provided, renders a button. */
|
|
onClick?: () => void;
|
|
};
|
|
|
|
type EmptyStateProps = {
|
|
/** Optional icon element (typically a lucide-react icon). */
|
|
icon?: ReactNode;
|
|
/** Required heading explaining the empty state. */
|
|
title: string;
|
|
/** Optional supporting copy shown below the title. */
|
|
description?: string;
|
|
/** Optional single primary action. */
|
|
action?: EmptyStateAction;
|
|
/** Extra class names appended to the root. */
|
|
className?: string;
|
|
};
|
|
|
|
/**
|
|
* EmptyState — generic empty state for admin pages.
|
|
*
|
|
* Centers an icon + title + description and (optionally) a single primary CTA
|
|
* styled with `.ha-btn-primary`. Uses the new admin design tokens for
|
|
* colors and Fraunces for the title.
|
|
*/
|
|
export default function EmptyState({
|
|
icon,
|
|
title,
|
|
description,
|
|
action,
|
|
className = "",
|
|
}: EmptyStateProps) {
|
|
return (
|
|
<div
|
|
className={`flex flex-col items-center justify-center text-center ${className}`.trim()}
|
|
style={{ padding: "3rem 1.5rem" }}
|
|
>
|
|
<div
|
|
className="mb-4"
|
|
style={{ color: "var(--admin-text-muted)" }}
|
|
aria-hidden="true"
|
|
>
|
|
{icon ?? <DefaultEmptyIcon />}
|
|
</div>
|
|
|
|
<h3
|
|
className="ha-display"
|
|
style={{
|
|
fontSize: "1.125rem",
|
|
color: "var(--admin-text-primary)",
|
|
}}
|
|
>
|
|
{title}
|
|
</h3>
|
|
|
|
{description && (
|
|
<p
|
|
className="mt-2 max-w-sm text-sm leading-relaxed"
|
|
style={{ color: "var(--admin-text-muted)" }}
|
|
>
|
|
{description}
|
|
</p>
|
|
)}
|
|
|
|
{action && (
|
|
<div className="mt-6">
|
|
<EmptyStateActionButton action={action} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function DefaultEmptyIcon() {
|
|
return <Inbox className="h-9 w-9" strokeWidth={1.25} />;
|
|
}
|
|
|
|
function EmptyStateActionButton({ action }: { action: EmptyStateAction }) {
|
|
// Pick Link vs button based on whether an href is provided. Both render
|
|
// with the same .ha-btn-primary class so the CTA is visually consistent.
|
|
if (action.href) {
|
|
return (
|
|
<Link href={action.href} className="ha-btn-primary">
|
|
{action.label}
|
|
</Link>
|
|
);
|
|
}
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={action.onClick}
|
|
className="ha-btn-primary"
|
|
>
|
|
{action.label}
|
|
</button>
|
|
);
|
|
}
|