feat(stats): split updated timestamp ticker

This commit is contained in:
Adam 2026-05-28 15:07:53 -05:00
parent ffbf9df18e
commit 629d5593eb
No known key found for this signature in database
GPG key ID: 9CB48779AF150E75
2 changed files with 153 additions and 14 deletions

View file

@ -520,12 +520,73 @@
flex: 0 0 auto;
}
[data-page="stats"] [data-slot="hero-meta"] span {
[data-page="stats"] [data-slot="hero-meta-label"],
[data-page="stats"] [data-slot="hero-meta-empty"] {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
[data-page="stats"] [data-slot="hero-meta-time"] {
display: inline-flex;
align-items: center;
min-width: 0;
overflow: hidden;
}
[data-page="stats"] [data-slot="hero-meta-separator"] {
flex: 0 0 auto;
margin-right: 0.35em;
}
[data-page="stats"] [data-slot="hero-meta-ticker"] {
position: relative;
flex: 0 1 auto;
min-width: 0;
height: 1.1em;
overflow: hidden;
line-height: 1.1;
}
[data-page="stats"] [data-slot="hero-meta-ticker-track"] {
display: flex;
min-width: 0;
flex-direction: column;
transform: translateY(0);
will-change: transform;
}
[data-page="stats"] [data-slot="hero-meta-ticker"][data-ticking="true"] [data-slot="hero-meta-ticker-track"] {
animation: stats-hero-meta-ticker 680ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
[data-page="stats"] [data-slot="hero-meta-ticker-item"] {
display: block;
min-width: 0;
overflow: hidden;
line-height: 1.1;
text-overflow: ellipsis;
}
@keyframes stats-hero-meta-ticker {
0%,
18% {
transform: translateY(0);
}
82%,
100% {
transform: translateY(-50%);
}
}
@media (prefers-reduced-motion: reduce) {
[data-page="stats"] [data-slot="hero-meta-ticker"][data-ticking="true"] [data-slot="hero-meta-ticker-track"] {
animation: none;
transform: translateY(-50%);
}
}
@media (min-width: 48rem) {
[data-page="stats"] [data-section="hero"] {
gap: 16px;

View file

@ -117,11 +117,47 @@ export default function StatsHome() {
function Hero(props: { updatedAt: string | null }) {
const [timeZone, setTimeZone] = createSignal("UTC")
onMount(() => setTimeZone(Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"))
const [previousTimeZone, setPreviousTimeZone] = createSignal("UTC")
const [isTicking, setIsTicking] = createSignal(false)
const updatedAtParts = (timeZone: string) =>
props.updatedAt ? formatUpdatedAtParts(props.updatedAt, timeZone) : { date: "No rows yet", time: "" }
const previousUpdatedAt = createMemo(() => updatedAtParts(previousTimeZone()))
const currentUpdatedAt = createMemo(() => updatedAtParts(timeZone()))
const currentUpdatedLabel = createMemo(() =>
props.updatedAt ? `Updated ${formatUpdatedAtLabel(currentUpdatedAt())}` : "No rows yet",
)
const isDateTicking = createMemo(() => isTicking() && previousUpdatedAt().date !== currentUpdatedAt().date)
const isTimeTicking = createMemo(() => isTicking() && previousUpdatedAt().time !== currentUpdatedAt().time)
onMount(() => {
if (!props.updatedAt) return
const nextTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"
if (nextTimeZone === "UTC") return
if (
formatUpdatedAtLabel(formatUpdatedAtParts(props.updatedAt, nextTimeZone)) ===
formatUpdatedAtLabel(updatedAtParts("UTC"))
)
return
const timeouts: number[] = []
timeouts.push(
window.setTimeout(() => {
setPreviousTimeZone(timeZone())
setTimeZone(nextTimeZone)
setIsTicking(true)
timeouts.push(
window.setTimeout(() => {
setPreviousTimeZone(nextTimeZone)
setIsTicking(false)
}, 720),
)
}, 480),
)
onCleanup(() => timeouts.forEach((timeout) => window.clearTimeout(timeout)))
})
return (
<section data-section="hero">
<p data-slot="hero-meta">
<p data-slot="hero-meta" aria-live="polite" aria-atomic="true" aria-label={currentUpdatedLabel()}>
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16">
<path
fill-rule="evenodd"
@ -130,7 +166,28 @@ function Hero(props: { updatedAt: string | null }) {
fill="currentColor"
/>
</svg>
<span>{props.updatedAt ? `Updated ${formatUpdatedAt(props.updatedAt, timeZone())}` : "No rows yet"}</span>
{props.updatedAt ? (
<>
<span data-slot="hero-meta-label" aria-hidden="true">
Updated
</span>
<span data-slot="hero-meta-time" aria-hidden="true">
<HeroMetaTickerPart
previous={previousUpdatedAt().date}
current={currentUpdatedAt().date}
ticking={isDateTicking()}
/>
<span data-slot="hero-meta-separator">,</span>
<HeroMetaTickerPart
previous={previousUpdatedAt().time}
current={currentUpdatedAt().time}
ticking={isTimeTicking()}
/>
</span>
</>
) : (
<span data-slot="hero-meta-empty">No rows yet</span>
)}
</p>
<div data-slot="hero-canvas">
<div data-slot="hero-pattern" aria-hidden="true" />
@ -144,6 +201,17 @@ function Hero(props: { updatedAt: string | null }) {
)
}
function HeroMetaTickerPart(props: { previous: string; current: string; ticking: boolean }) {
return (
<span data-slot="hero-meta-ticker" data-ticking={props.ticking}>
<span data-slot="hero-meta-ticker-track">
<span data-slot="hero-meta-ticker-item">{props.previous}</span>
<span data-slot="hero-meta-ticker-item">{props.current}</span>
</span>
</span>
)
}
function StatsLoading() {
return (
<>
@ -185,17 +253,27 @@ function EmptyState(props: { title: string; description: string }) {
)
}
function formatUpdatedAt(value: string, timeZone: string) {
function formatUpdatedAtParts(value: string, timeZone: string) {
const date = new Date(value)
if (Number.isNaN(date.getTime())) return "just now"
return new Intl.DateTimeFormat("en", {
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
timeZone,
timeZoneName: "short",
}).format(date)
if (Number.isNaN(date.getTime())) return { date: "just now", time: "" }
return {
date: new Intl.DateTimeFormat("en", {
month: "short",
day: "numeric",
timeZone,
}).format(date),
time: new Intl.DateTimeFormat("en", {
hour: "numeric",
minute: "2-digit",
timeZone,
timeZoneName: "short",
}).format(date),
}
}
function formatUpdatedAtLabel(value: { date: string; time: string }) {
if (!value.time) return value.date
return `${value.date}, ${value.time}`
}
function TopModelsSection(props: { data: StatsHomeData["usage"] }) {