A compact semantic date for tables and lists — calendar-page tile with a primary today band, two-line stack, and inline variants with Today/Yesterday wording and current-year elision.
npx shadcn@latest add @paragon/date-cell"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
/* ------------------------------------------------------------------
* Hand-rolled date formatting — no date libraries. Day comparisons go
* through Date.UTC on local calendar components so DST can never skew
* a "Today" badge.
* ------------------------------------------------------------------ */
const MONTHS = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
] as const;
const WEEKDAYS = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
] as const;
const mon3 = (d: Date) => MONTHS[d.getMonth()].slice(0, 3);
const wd3 = (d: Date) => WEEKDAYS[d.getDay()].slice(0, 3);
/** Whole calendar days from `b` to `a` (positive when `a` is later). */
function calendarDayDiff(a: Date, b: Date): number {
return Math.round(
(Date.UTC(a.getFullYear(), a.getMonth(), a.getDate()) -
Date.UTC(b.getFullYear(), b.getMonth(), b.getDate())) /
86_400_000,
);
}
/** "9:41 AM" — deterministic, locale-independent. */
function formatTime(d: Date): string {
const h12 = d.getHours() % 12 || 12;
return `${h12}:${String(d.getMinutes()).padStart(2, "0")} ${d.getHours() < 12 ? "AM" : "PM"}`;
}
const pad2 = (n: number) => String(n).padStart(2, "0");
function isoDate(d: Date, withTime: boolean): string {
const day = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
return withTime ? `${day}T${pad2(d.getHours())}:${pad2(d.getMinutes())}` : day;
}
export interface DateCellProps
extends Omit<React.ComponentProps<"time">, "children" | "dateTime"> {
date?: Date;
variant?: "tile" | "stack" | "inline";
/** Reference "today". Enables Today/Yesterday/Tomorrow wording, the
* today tile accent, and current-year elision. Pass a fixed date for
* deterministic renders. */
now?: Date;
showWeekday?: boolean;
showTime?: boolean;
/** Say "Today" / "Yesterday" / "Tomorrow" when within a day of `now`. */
relativeDays?: boolean;
}
// Fixed default so the zero-props render never touches the wall clock.
const DEFAULT_DATE = new Date(2026, 6, 14, 9, 41);
/**
* A compact date for tables and lists in three shapes: a calendar-page
* tile whose month band turns primary on today, a two-line stack, and a
* single inline run. Today/Yesterday/Tomorrow wording and current-year
* elision keep the copy product-real; everything is a semantic <time>.
*/
export function DateCell({
date = DEFAULT_DATE,
variant = "tile",
now,
showWeekday = false,
showTime = false,
relativeDays = true,
className,
...props
}: DateCellProps) {
const dayDiff = now ? calendarDayDiff(date, now) : null;
const relativeWord =
relativeDays && dayDiff !== null && Math.abs(dayDiff) <= 1
? dayDiff === 0
? "Today"
: dayDiff === 1
? "Tomorrow"
: "Yesterday"
: null;
const isToday = dayDiff === 0;
// Elide the year only when a reference `now` proves it is the current one.
const withYear = !now || date.getFullYear() !== now.getFullYear();
const dateLine = (weekday: boolean) =>
`${weekday ? `${wd3(date)}, ` : ""}${mon3(date)} ${date.getDate()}${withYear ? `, ${date.getFullYear()}` : ""}`;
const absolute = `${WEEKDAYS[date.getDay()]}, ${MONTHS[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}${showTime ? ` at ${formatTime(date)}` : ""}`;
const dateTime = isoDate(date, showTime);
if (variant === "tile") {
return (
<time
dateTime={dateTime}
className={cn(
"flex w-12 shrink-0 flex-col overflow-hidden rounded-lg bg-card text-center shadow-border",
className,
)}
{...props}
>
<span className="sr-only">{absolute}</span>
<span
aria-hidden
className={cn(
"py-[3px] text-[10px] leading-none font-semibold tracking-widest uppercase",
isToday
? "bg-primary text-primary-foreground"
: "bg-secondary/80 text-muted-foreground dark:bg-secondary/50",
)}
>
{mon3(date)}
</span>
<span
aria-hidden
className={cn(
"pt-1 text-xl leading-6 font-semibold text-foreground tabular-nums",
!showWeekday && "pb-1.5",
)}
>
{date.getDate()}
</span>
{showWeekday && (
<span
aria-hidden
className="pb-1 text-[10px] leading-3 text-muted-foreground"
>
{wd3(date)}
</span>
)}
</time>
);
}
if (variant === "stack") {
const secondary = [
...(showWeekday && !relativeWord ? [wd3(date)] : []),
...(showTime ? [formatTime(date)] : []),
].join(" · ");
return (
<time
dateTime={dateTime}
className={cn("flex flex-col gap-px", className)}
{...props}
>
<span className="text-sm leading-5 font-medium text-foreground tabular-nums">
{relativeWord ?? dateLine(false)}
</span>
{secondary && (
<span className="text-xs leading-4 text-muted-foreground tabular-nums">
{secondary}
</span>
)}
</time>
);
}
return (
<time
dateTime={dateTime}
className={cn("inline-flex items-baseline gap-1.5 whitespace-nowrap", className)}
{...props}
>
<span className="text-sm font-medium text-foreground tabular-nums">
{relativeWord ?? dateLine(showWeekday)}
</span>
{showTime && (
<span className="text-xs text-muted-foreground tabular-nums">
· {formatTime(date)}
</span>
)}
</time>
);
}