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.
This commit is contained in:
@@ -0,0 +1,107 @@
|
|||||||
|
"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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { ArrowDown, ArrowRight, ArrowUp } from "lucide-react";
|
||||||
|
|
||||||
|
type KPITone = "default" | "primary" | "accent" | "warning" | "danger";
|
||||||
|
|
||||||
|
type KPIStatProps = {
|
||||||
|
/** Eyebrow label above the value (e.g. "Today's Orders") */
|
||||||
|
label: string;
|
||||||
|
/** The primary value to display. Accepts a node so callers can pass formatted strings, JSX, etc. */
|
||||||
|
value: ReactNode;
|
||||||
|
/** Optional icon element (typically a lucide-react icon) shown in the leading tile. */
|
||||||
|
icon?: ReactNode;
|
||||||
|
/** Controls the icon tile's background + icon color. Defaults to "default". */
|
||||||
|
tone?: KPITone;
|
||||||
|
/** Optional trend indicator rendered next to the value. */
|
||||||
|
trend?: { value: string; direction: "up" | "down" | "flat" };
|
||||||
|
/** Extra class names appended to the card. */
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tone -> icon-tile background / icon color. Pulled from --admin-* tokens
|
||||||
|
// introduced in the Phase 1 palette unification. No hardcoded hex values.
|
||||||
|
const toneStyles: Record<KPITone, { bg: string; fg: string }> = {
|
||||||
|
default: { bg: "var(--admin-bg-subtle)", fg: "var(--admin-text-secondary)" },
|
||||||
|
primary: { bg: "var(--admin-primary-soft)", fg: "var(--admin-primary)" },
|
||||||
|
accent: { bg: "var(--admin-accent-soft)", fg: "var(--admin-accent)" },
|
||||||
|
warning: { bg: "var(--admin-warning-soft)", fg: "var(--admin-warning)" },
|
||||||
|
danger: { bg: "var(--admin-danger-soft)", fg: "var(--admin-danger)" },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Trend direction -> icon + color. "up" reads as positive (primary),
|
||||||
|
// "down" as negative (danger), "flat" as neutral.
|
||||||
|
const trendStyles: Record<"up" | "down" | "flat", { Icon: typeof ArrowUp; color: string }> = {
|
||||||
|
up: { Icon: ArrowUp, color: "var(--admin-primary)" },
|
||||||
|
down: { Icon: ArrowDown, color: "var(--admin-danger)" },
|
||||||
|
flat: { Icon: ArrowRight, color: "var(--admin-text-muted)" },
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KPIStat — compact metric card for admin surfaces.
|
||||||
|
*
|
||||||
|
* Extracted from the inline pattern previously duplicated four times in
|
||||||
|
* `DashboardClient.tsx`. Reuses the `.admin-stat-card*` classes from
|
||||||
|
* `admin-design-system.css` for the shell, then applies tone-aware
|
||||||
|
* background/foreground via inline `var(--admin-*)` styles.
|
||||||
|
*/
|
||||||
|
export default function KPIStat({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
icon,
|
||||||
|
tone = "default",
|
||||||
|
trend,
|
||||||
|
className = "",
|
||||||
|
}: KPIStatProps) {
|
||||||
|
const { bg, fg } = toneStyles[tone];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`admin-stat-card ${className}`.trim()}>
|
||||||
|
<div className="admin-stat-card-inner">
|
||||||
|
{icon && (
|
||||||
|
<div
|
||||||
|
className="admin-stat-icon"
|
||||||
|
style={{ backgroundColor: bg, color: fg }}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="admin-stat-body">
|
||||||
|
<span className="admin-stat-label">{label}</span>
|
||||||
|
<div className="flex items-baseline gap-2">
|
||||||
|
<span className="admin-stat-value">{value}</span>
|
||||||
|
{trend && <TrendIndicator trend={trend} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TrendIndicator({
|
||||||
|
trend,
|
||||||
|
}: {
|
||||||
|
trend: { value: string; direction: "up" | "down" | "flat" };
|
||||||
|
}) {
|
||||||
|
const { Icon, color } = trendStyles[trend.direction];
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center gap-0.5 text-[0.6875rem] font-semibold"
|
||||||
|
style={{ color }}
|
||||||
|
>
|
||||||
|
<Icon className="h-3 w-3" strokeWidth={2.25} aria-hidden="true" />
|
||||||
|
{trend.value}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
type LoadingStateProps = {
|
||||||
|
/** Optional eyebrow text rendered above the skeleton (e.g. "Loading orders..."). */
|
||||||
|
label?: string;
|
||||||
|
/** Number of skeleton rows to show. Defaults to 3. */
|
||||||
|
rows?: number;
|
||||||
|
/** Extra class names appended to the wrapper. */
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LoadingState — generic skeleton for admin list/card surfaces.
|
||||||
|
*
|
||||||
|
* Renders an optional centered "Loading…" eyebrow above `rows` skeleton
|
||||||
|
* blocks, each composed of a label-sized bar and a value-sized bar. The
|
||||||
|
* shimmer comes from the shared `.ha-skeleton` class in
|
||||||
|
* `admin-design-system.css` so motion / reduced-motion stay consistent
|
||||||
|
* with the rest of the admin shell.
|
||||||
|
*/
|
||||||
|
export default function LoadingState({
|
||||||
|
label,
|
||||||
|
rows = 3,
|
||||||
|
className = "",
|
||||||
|
}: LoadingStateProps) {
|
||||||
|
const safeRows = Math.max(1, Math.floor(rows));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex flex-col items-stretch ${className}`.trim()}
|
||||||
|
style={{ padding: "1.5rem 1rem" }}
|
||||||
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-busy="true"
|
||||||
|
>
|
||||||
|
{label && (
|
||||||
|
<span
|
||||||
|
className="ha-eyebrow mx-auto mb-4"
|
||||||
|
aria-label={label}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{Array.from({ length: safeRows }).map((_, i) => (
|
||||||
|
// Index is fine here — the list shape is stable per render.
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
<SkeletonRow key={i} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SkeletonRow() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-3 rounded-md border p-3"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--admin-card-bg)",
|
||||||
|
borderColor: "var(--admin-border)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Icon-tile placeholder */}
|
||||||
|
<span
|
||||||
|
className="ha-skeleton block flex-shrink-0"
|
||||||
|
style={{ width: "2.25rem", height: "2.25rem", borderRadius: "0.5rem" }}
|
||||||
|
/>
|
||||||
|
{/* Label + value placeholders */}
|
||||||
|
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
|
||||||
|
<span
|
||||||
|
className="ha-skeleton block"
|
||||||
|
style={{ width: "40%", height: "0.5rem", borderRadius: "0.25rem" }}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="ha-skeleton block"
|
||||||
|
style={{ width: "65%", height: "0.75rem", borderRadius: "0.25rem" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,33 +1,139 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import type { CSSProperties, ReactNode } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visual variants for `AdminBadge`. Kept for backward compatibility —
|
||||||
|
* existing callers (status pills, add-on flags, etc.) continue to pass
|
||||||
|
* one of these values and are routed onto the new tone palette below.
|
||||||
|
*/
|
||||||
type BadgeVariant = "default" | "success" | "warning" | "danger" | "info";
|
type BadgeVariant = "default" | "success" | "warning" | "danger" | "info";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* New tone palette. Matches the spec in
|
||||||
|
* `docs/superpowers/specs/2026-06-17-admin-redesign.md` §3. The previous
|
||||||
|
* `--admin-warning-light` / `--admin-danger-light` references have been
|
||||||
|
* replaced with the new `--admin-warning-soft` / `--admin-danger-soft`
|
||||||
|
* tokens. No hardcoded hex values remain in this file.
|
||||||
|
*/
|
||||||
|
type BadgeTone =
|
||||||
|
| "neutral"
|
||||||
|
| "primary"
|
||||||
|
| "accent"
|
||||||
|
| "warning"
|
||||||
|
| "danger"
|
||||||
|
| "success"
|
||||||
|
| "info";
|
||||||
|
|
||||||
type AdminBadgeProps = {
|
type AdminBadgeProps = {
|
||||||
children: React.ReactNode;
|
children: ReactNode;
|
||||||
|
/** Legacy variant name. Falls back to "default". Mapped onto `tone` internally. */
|
||||||
variant?: BadgeVariant;
|
variant?: BadgeVariant;
|
||||||
|
/** New tone name. Takes precedence over `variant` when both are provided. */
|
||||||
|
tone?: BadgeTone;
|
||||||
|
/** Optional leading dot indicator. */
|
||||||
dot?: boolean;
|
dot?: boolean;
|
||||||
|
/** Extra classes — typically size overrides like `"text-xs px-2 py-0.5"`. */
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const variantClasses: Record<BadgeVariant, { bg: string; text: string; dot: string }> = {
|
type ToneStyle = {
|
||||||
default: { bg: "bg-[var(--admin-border-light)]", text: "text-[var(--admin-text-secondary)]", dot: "bg-[var(--admin-text-muted)]" },
|
bg: string;
|
||||||
success: { bg: "bg-[var(--admin-accent-light)]", text: "text-[var(--admin-accent-text)]", dot: "bg-[var(--admin-accent)]" },
|
text: string;
|
||||||
warning: { bg: "bg-[var(--admin-warning-light)]", text: "text-[var(--admin-warning)]", dot: "bg-[var(--admin-warning)]" },
|
/** Optional border color; falls back to the background when unset. */
|
||||||
danger: { bg: "bg-[var(--admin-danger-light)]", text: "text-[var(--admin-danger)]", dot: "bg-[var(--admin-danger)]" },
|
border?: string;
|
||||||
info: { bg: "bg-[var(--admin-border)]", text: "text-[var(--admin-text-secondary)]", dot: "bg-[var(--admin-info)]" },
|
dot: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// New tone palette. Every value references a `--admin-*` design token
|
||||||
|
// declared in `admin-design-system.css`.
|
||||||
|
const toneStyles: Record<BadgeTone, ToneStyle> = {
|
||||||
|
neutral: {
|
||||||
|
bg: "var(--admin-bg-subtle)",
|
||||||
|
text: "var(--admin-text-secondary)",
|
||||||
|
border: "var(--admin-border)",
|
||||||
|
dot: "var(--admin-text-muted)",
|
||||||
|
},
|
||||||
|
primary: {
|
||||||
|
bg: "var(--admin-primary-soft)",
|
||||||
|
text: "var(--admin-primary)",
|
||||||
|
border: "var(--admin-primary)",
|
||||||
|
dot: "var(--admin-primary)",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
bg: "var(--admin-accent-soft)",
|
||||||
|
text: "var(--admin-accent)",
|
||||||
|
border: "var(--admin-accent)",
|
||||||
|
dot: "var(--admin-accent)",
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
// "success" shares the primary botanical green per the unified palette.
|
||||||
|
bg: "var(--admin-primary-soft)",
|
||||||
|
text: "var(--admin-primary)",
|
||||||
|
border: "var(--admin-primary)",
|
||||||
|
dot: "var(--admin-primary)",
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
bg: "var(--admin-warning-soft)",
|
||||||
|
text: "var(--admin-warning)",
|
||||||
|
border: "var(--admin-warning)",
|
||||||
|
dot: "var(--admin-warning)",
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
bg: "var(--admin-danger-soft)",
|
||||||
|
text: "var(--admin-danger)",
|
||||||
|
border: "var(--admin-danger)",
|
||||||
|
dot: "var(--admin-danger)",
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
bg: "var(--admin-bg-subtle)",
|
||||||
|
text: "var(--admin-text-secondary)",
|
||||||
|
border: "var(--admin-border)",
|
||||||
|
dot: "var(--admin-text-secondary)",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Map the legacy `variant` prop onto the new `tone` palette. The mapping
|
||||||
|
// preserves the visual intent of pre-refactor call sites.
|
||||||
|
const variantToTone: Record<BadgeVariant, BadgeTone> = {
|
||||||
|
default: "neutral",
|
||||||
|
success: "success",
|
||||||
|
warning: "warning",
|
||||||
|
danger: "danger",
|
||||||
|
info: "info",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AdminBadge({
|
export default function AdminBadge({
|
||||||
children,
|
children,
|
||||||
variant = "default",
|
variant = "default",
|
||||||
|
tone,
|
||||||
dot = false,
|
dot = false,
|
||||||
className = ""
|
className = "",
|
||||||
}: AdminBadgeProps) {
|
}: AdminBadgeProps) {
|
||||||
const { bg, text, dot: dotColor } = variantClasses[variant];
|
// `tone` wins when provided; otherwise translate the legacy variant.
|
||||||
|
const resolvedTone: BadgeTone = tone ?? variantToTone[variant];
|
||||||
|
const { bg, text, border, dot: dotColor } = toneStyles[resolvedTone];
|
||||||
|
|
||||||
|
// Border color tracks tone when the tone is one of the chromatic ones;
|
||||||
|
// otherwise it falls back to the neutral hairline so the pill keeps a
|
||||||
|
// subtle outline against the cream canvas.
|
||||||
|
const style: CSSProperties = {
|
||||||
|
backgroundColor: bg,
|
||||||
|
color: text,
|
||||||
|
borderColor: border ?? "var(--admin-border)",
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-[10px] font-bold ${bg} ${text} ${className}`}>
|
<span
|
||||||
{dot && <span className={`h-1.5 w-1.5 rounded-full ${dotColor}`} />}
|
className={`inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-[10px] font-bold ${className}`}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{dot && (
|
||||||
|
<span
|
||||||
|
className="h-1.5 w-1.5 rounded-full"
|
||||||
|
style={{ backgroundColor: dotColor }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@@ -39,19 +145,21 @@ type AdminStatusBadgeProps = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusConfig: Record<string, { variant: BadgeVariant; dot: boolean; label: string }> = {
|
// Reuse the new `tone` prop directly so the status pill text style always
|
||||||
active: { variant: "success", dot: true, label: "Active" },
|
// matches the active AdminBadge palette.
|
||||||
inactive: { variant: "default", dot: true, label: "Inactive" },
|
const statusConfig: Record<string, { tone: BadgeTone; dot: boolean; label: string }> = {
|
||||||
pending: { variant: "warning", dot: true, label: "Pending" },
|
active: { tone: "success", dot: true, label: "Active" },
|
||||||
draft: { variant: "default", dot: true, label: "Draft" },
|
inactive: { tone: "neutral", dot: true, label: "Inactive" },
|
||||||
completed: { variant: "success", dot: true, label: "Completed" },
|
pending: { tone: "warning", dot: true, label: "Pending" },
|
||||||
cancelled: { variant: "danger", dot: true, label: "Cancelled" },
|
draft: { tone: "neutral", dot: true, label: "Draft" },
|
||||||
|
completed: { tone: "success", dot: true, label: "Completed" },
|
||||||
|
cancelled: { tone: "danger", dot: true, label: "Cancelled" },
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AdminStatusBadge({ status, className = "" }: AdminStatusBadgeProps) {
|
export function AdminStatusBadge({ status, className = "" }: AdminStatusBadgeProps) {
|
||||||
const config = statusConfig[status] || statusConfig.inactive;
|
const config = statusConfig[status] || statusConfig.inactive;
|
||||||
return (
|
return (
|
||||||
<AdminBadge variant={config.variant} dot={config.dot} className={className}>
|
<AdminBadge tone={config.tone} dot={config.dot} className={className}>
|
||||||
{config.label}
|
{config.label}
|
||||||
</AdminBadge>
|
</AdminBadge>
|
||||||
);
|
);
|
||||||
@@ -60,14 +168,25 @@ export function AdminStatusBadge({ status, className = "" }: AdminStatusBadgePro
|
|||||||
// Count badge (circular)
|
// Count badge (circular)
|
||||||
type AdminCountBadgeProps = {
|
type AdminCountBadgeProps = {
|
||||||
count: number;
|
count: number;
|
||||||
|
tone?: BadgeTone;
|
||||||
|
/** @deprecated Use `tone` instead. */
|
||||||
variant?: BadgeVariant;
|
variant?: BadgeVariant;
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AdminCountBadge({ count, variant = "default", className = "" }: AdminCountBadgeProps) {
|
export function AdminCountBadge({
|
||||||
const { bg, text } = variantClasses[variant];
|
count,
|
||||||
|
tone,
|
||||||
|
variant = "default",
|
||||||
|
className = "",
|
||||||
|
}: AdminCountBadgeProps) {
|
||||||
|
const resolvedTone: BadgeTone = tone ?? variantToTone[variant];
|
||||||
|
const { bg, text } = toneStyles[resolvedTone];
|
||||||
return (
|
return (
|
||||||
<span className={`inline-flex h-5 min-w-[1.25rem] items-center justify-center rounded-full px-1.5 text-xs font-bold ${bg} ${text} ${className}`}>
|
<span
|
||||||
|
className={`inline-flex h-5 min-w-[1.25rem] items-center justify-center rounded-full px-1.5 text-xs font-bold ${className}`}
|
||||||
|
style={{ backgroundColor: bg, color: text }}
|
||||||
|
>
|
||||||
{count}
|
{count}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ export { default as AdminActionMenu, AdminActionButton } from "./AdminActionMenu
|
|||||||
export { default as AdminPagination, AdminSimplePagination } from "./AdminPagination";
|
export { default as AdminPagination, AdminSimplePagination } from "./AdminPagination";
|
||||||
export { default as AdminBadge, AdminStatusBadge, AdminCountBadge } from "./AdminBadge";
|
export { default as AdminBadge, AdminStatusBadge, AdminCountBadge } from "./AdminBadge";
|
||||||
|
|
||||||
|
// Phase 2 pattern primitives (admin-level, not in design-system/ folder)
|
||||||
|
// Re-exported here so callers can `import { KPIStat, EmptyState, LoadingState } from "@/components/admin/design-system"`.
|
||||||
|
export { default as KPIStat } from "../KPIStat";
|
||||||
|
export { default as EmptyState } from "../EmptyState";
|
||||||
|
export { default as LoadingState } from "../LoadingState";
|
||||||
|
|
||||||
// Design system components
|
// Design system components
|
||||||
export { default as AdminButton, AdminIconButton } from "./AdminButton";
|
export { default as AdminButton, AdminIconButton } from "./AdminButton";
|
||||||
export { default as PageHeader } from "./PageHeader";
|
export { default as PageHeader } from "./PageHeader";
|
||||||
|
|||||||
Reference in New Issue
Block a user