diff --git a/packages/stats/app/src/routes/index.css b/packages/stats/app/src/routes/index.css index 665f57af44..2be48d8393 100644 --- a/packages/stats/app/src/routes/index.css +++ b/packages/stats/app/src/routes/index.css @@ -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; diff --git a/packages/stats/app/src/routes/index.tsx b/packages/stats/app/src/routes/index.tsx index d283c5284c..4a5816f9d9 100644 --- a/packages/stats/app/src/routes/index.tsx +++ b/packages/stats/app/src/routes/index.tsx @@ -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 (
-

+

- {props.updatedAt ? `Updated ${formatUpdatedAt(props.updatedAt, timeZone())}` : "No rows yet"} + {props.updatedAt ? ( + <> + + + + ) : ( + No rows yet + )}