Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | 7x 377x 377x 377x 32x 345x 94x | import { Link } from "@tanstack/react-router";
import type { ReactNode } from "react";
import { Skeleton } from "../ui/skeleton";
import { cn } from "../../lib/utils";
/**
* `.metric` — the Job Book's figure tile. **Figures and rows lead.**
*
* The spec is `.metric` / `.metrics` in `.context/mockups/_base.css` §7.3:
*
* padding: 0.5rem 0 0.4375rem;
* border-bottom: 2px solid var(--border-strong);
* > .label { display:block; color: var(--foreground-muted) }
* > .num { display:block; margin-top: 0.1875rem }
*
* A figure is **a label, a rule and a number** — not a card. The admin
* dashboard, the search console and the platform analytics screen all drew
* their figures as `Card > CardHeader > CardTitle + CardContent`: four boxes
* with shadows, a 14px title and the number buried in the third element. Boxes
* are the one thing the Job Book forbids, so the box is gone and the 2px rule
* under each figure is what groups them.
*
* `value` is a `ReactNode`, not a number: an admin figure is routinely `"-"`
* (not computed yet) or a rupee string, and `.num` is about alignment and
* tabular figures rather than about the type of the thing printed.
*
* **State is a printed code, never a colour.** The dashboard's "pros pending
* publish" tile used to go warning-coloured when it was non-zero, which is
* colour travelling alone. Pass `code` and the state is a word.
*
* NOTE ON DUPLICATION: `TrafficFigure` in
* `components/analytics/MarketplaceTraffic.tsx` is this same `.metric` spec
* with a narrower prop set (`value: number`, a fixed window string, no link,
* no code). That file belongs to another group this phase; folding it into
* this component is a one-line swap and is reported rather than made here.
*/
export interface AdminMetricProps {
/** `.label` — caps, 620 weight, muted. */
label: ReactNode;
/** The figure. `.num-xl`, tabular, right-aligned. */
value: ReactNode;
/** Printed under the figure: the window, the breakdown, the next action. */
note?: ReactNode;
/** A printed status code beside the label — how state is carried. */
code?: ReactNode;
/** Makes the whole tile the link target. */
href?: string;
/** Swaps the figure for a skeleton of the right height. */
isLoading?: boolean;
className?: string;
}
const LABEL = "text-xs font-semibold uppercase tracking-wider";
export function AdminMetric({
label,
value,
note,
code,
href,
isLoading,
className,
}: AdminMetricProps) {
const body = (
<>
<span className="flex items-center gap-1.5">
<span className={cn(LABEL, "text-foreground-muted")}>{label}</span>
{code}
</span>
{isLoading ? (
<Skeleton className="mt-1 h-7 w-16" />
) : (
// `.num-xl`: 1.5rem, tabular, right-aligned.
<span className="mt-0.5 block text-right text-2xl font-bold tabular-nums text-foreground-default">
{value}
</span>
)}
{note && (
<span className="block text-xs text-foreground-subtle">{note}</span>
)}
</>
);
// The 2px strong rule under the figure is what groups a row of them. No
// box, no shadow — and no hover lift, which means "pick-up-able".
const tile = "block border-b-2 border-border-strong pt-2 pb-1.5";
if (href) {
return (
<Link
to={href}
className={cn(
tile,
"transition-colors hover:bg-foreground-default/7 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500",
className,
)}
>
{body}
</Link>
);
}
return <div className={cn(tile, className)}>{body}</div>;
}
/**
* `.metrics` — the auto-fitting grid a row of figures sits in
* (`repeat(auto-fit, minmax(6.5rem, 1fr))`, `gap: 0 1rem`).
*/
export function AdminMetrics({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return (
<div
className={cn(
"grid grid-cols-2 gap-x-4 sm:grid-cols-3 lg:grid-cols-4",
className,
)}
>
{children}
</div>
);
}
|