mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-31 05:15:32 +00:00
feat(stats): split updated timestamp ticker
This commit is contained in:
parent
ffbf9df18e
commit
629d5593eb
2 changed files with 153 additions and 14 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"] }) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue