feat(admin): add PageHeader, StatusPill, EmptyState, CardList, StickyActionBar
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface CardListProps {
|
||||
children: ReactNode;
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
export function CardList({ children, ariaLabel }: CardListProps) {
|
||||
return (
|
||||
<ul role="list" aria-label={ariaLabel} className="space-y-3 px-4 pb-24">
|
||||
{children}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
interface CardListItemProps {
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
children: ReactNode;
|
||||
as?: "li" | "div";
|
||||
}
|
||||
|
||||
export function CardListItem({ onClick, href, children, as = "li" }: CardListItemProps) {
|
||||
const content = (
|
||||
<div
|
||||
className="rounded-2xl p-4 transition-colors"
|
||||
style={{
|
||||
backgroundColor: "var(--color-surface-2)",
|
||||
minHeight: "72px",
|
||||
boxShadow: "0 1px 2px rgba(0,0,0,0.04)",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<li>
|
||||
<a href={href} className="block active:scale-[0.99] transition-transform" style={{ minHeight: "72px" }}>
|
||||
{content}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
if (onClick) {
|
||||
return (
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="block w-full text-left active:scale-[0.99] transition-transform"
|
||||
style={{ minHeight: "72px" }}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
return <li>{content}</li>;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface PageHeaderProps {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
actions?: ReactNode;
|
||||
}
|
||||
|
||||
export function PageHeader({ title, subtitle, actions }: PageHeaderProps) {
|
||||
return (
|
||||
<header className="px-4 pt-6 pb-4 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-display font-display" style={{ fontWeight: 600, lineHeight: 1.15, letterSpacing: "-0.01em" }}>
|
||||
{title}
|
||||
</h1>
|
||||
{subtitle && (
|
||||
<p className="text-mono mt-1" style={{ color: "var(--color-text-muted)" }}>{subtitle}</p>
|
||||
)}
|
||||
</div>
|
||||
{actions && <div className="flex items-center gap-2 shrink-0">{actions}</div>}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export default PageHeader;
|
||||
@@ -0,0 +1,35 @@
|
||||
export type StatusKind = "placed" | "ready" | "picked-up" | "cancelled" | "scheduled" | "in-transit" | "completed" | "low" | "out" | "in-stock" | "hidden";
|
||||
|
||||
const STATUS_MAP: Record<StatusKind, { bg: string; fg: string; label: string }> = {
|
||||
placed: { bg: "var(--color-info-soft)", fg: "var(--color-info)", label: "Placed" },
|
||||
ready: { bg: "var(--color-warning-soft)", fg: "var(--color-warning)", label: "Ready" },
|
||||
"picked-up": { bg: "var(--color-success-soft)", fg: "var(--color-success)", label: "Picked up" },
|
||||
cancelled: { bg: "var(--color-surface-2)", fg: "var(--color-text-muted)", label: "Cancelled" },
|
||||
scheduled: { bg: "var(--color-info-soft)", fg: "var(--color-info)", label: "Scheduled" },
|
||||
"in-transit": { bg: "var(--color-warning-soft)", fg: "var(--color-warning)", label: "In transit" },
|
||||
completed: { bg: "var(--color-success-soft)", fg: "var(--color-success)", label: "Completed" },
|
||||
low: { bg: "var(--color-warning-soft)", fg: "var(--color-warning)", label: "Low stock" },
|
||||
out: { bg: "var(--color-danger-soft)", fg: "var(--color-danger)", label: "Out" },
|
||||
"in-stock": { bg: "var(--color-success-soft)", fg: "var(--color-success)", label: "In stock" },
|
||||
hidden: { bg: "var(--color-surface-2)", fg: "var(--color-text-muted)", label: "Hidden" },
|
||||
};
|
||||
|
||||
interface StatusPillProps {
|
||||
status: StatusKind;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export function StatusPill({ status, label }: StatusPillProps) {
|
||||
const cfg = STATUS_MAP[status];
|
||||
return (
|
||||
<span
|
||||
className="inline-flex items-center px-2.5 py-1 rounded-full text-label font-semibold"
|
||||
style={{ backgroundColor: cfg.bg, color: cfg.fg, letterSpacing: "0.02em", minHeight: "24px" }}
|
||||
aria-label={cfg.label}
|
||||
>
|
||||
{label ?? cfg.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default StatusPill;
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface StickyActionBarProps {
|
||||
children: ReactNode;
|
||||
pendingSync?: boolean;
|
||||
}
|
||||
|
||||
export function StickyActionBar({ children, pendingSync = false }: StickyActionBarProps) {
|
||||
return (
|
||||
<div
|
||||
className="fixed left-0 right-0 z-20 px-4 py-3 border-t"
|
||||
style={{
|
||||
bottom: "60px", // sits above the mobile tab bar
|
||||
backgroundColor: "var(--color-bg)",
|
||||
borderColor: "var(--color-surface-3)",
|
||||
boxShadow: "0 -4px 12px rgba(0,0,0,0.04)",
|
||||
}}
|
||||
>
|
||||
{pendingSync && (
|
||||
<div className="text-label font-semibold mb-2 flex items-center gap-2" style={{ color: "var(--color-text-muted)" }}>
|
||||
<span className="inline-block w-3 h-3 rounded-full animate-pulse" style={{ backgroundColor: "var(--color-warning)" }} />
|
||||
Pending sync
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default StickyActionBar;
|
||||
Reference in New Issue
Block a user