Files
route-commerce/src/components/admin/EmptyState.tsx
T
Claude 8e937344ff feat(admin): extract KPIStat, EmptyState, LoadingState primitives; tighten AdminBadge palette
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.
2026-06-17 00:15:44 -06:00

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