Agent docs

Timestamp

A relative time that reveals the exact clock on hover, or the reverse with absolute; carries its own tooltip, never wrap it in another. Includes the Refresh control that carries it.

A relative time ("2h ago") that reveals the exact clock, down to the second, on hover. absolute flips the reading: a compact absolute date in the text, the relative reading in the tooltip.

Toolkit sibling. packages/toolkit/src/date.ts (getHowLongAgo, getRelativeTime) is the same relative-time ladder this component solves in timestamp.tsx:17; the toolkit README's module table names this doc as where date belongs (packages/toolkit/README.md, "Belongs with"); the two ladders diverge only on falsy input, where the kit's renders the epoch and the toolkit's returns an empty string (docs/plan/43-toolkit-rca.md:128-137).

When to reach for it#

Reach for Timestamp anywhere a raw ISO string would otherwise be formatted by hand: table cells, activity feeds, provenance lines. It is the default reading for "when did this happen" across both apps: speedway/app/components/JobsTable.tsx:771, walmart-mvp/frontend/src/pages/Jobs.tsx:227-234, 240-247, walmart-mvp/frontend/src/pages/ActiveParts.tsx:100, and the activity rails in both apps all render it as a table or list cell.

Timestamp carries its own tooltip. Do not wrap it in another Tooltip; it stacks two. This is not a style guess, it is a documented, load-bearing comment at the one real call site that sits beside a sibling Tooltip in the same row: walmart-mvp/frontend/src/pages/Settings.tsx:519, "Timestamp carries its own tooltip; wrapping it stacks two." The mechanism is timestamp.tsx:61-66: the component's own render already wraps its <time> element in a Tooltip.

Contract#

  • iso (required): an ISO date string. Default reading: formatRelativeTime(iso) in the visible text, terser the older it gets: "just now" under a minute, "5m ago" under an hour, "3h ago" under 12 hours, and from 12 hours on a date ("Oct 22") with no clock, because the clock is in the tooltip. The year appears only when it differs from the current one ("Oct 22, 2025"). formatDateTime(iso) is the tooltip: weekday, date and time to the second ("Thursday, October 22 at 9:15:04 AM"), again with the year only when it differs. A future time reads "in 3d", never "just now": the console once rendered seven expiries as "just now" while each was 3.8 days away (gcp row #492 K1). Both directions share the floor: 3.8 days ahead is "in 3d". A future time more than 7 days out reads as a date.
  • locale: a BCP 47 tag, default DEFAULT_LOCALE (en-US). Every formatter here pins the locale rather than reading the runtime's, because an undefined locale is the server's on the first render and the browser's on hydration, and React reports the mismatch; speedway pinned en-US in its own app/lib/datetime.ts for this reason before the kit did. Change it on purpose per app, never per call.
  • absolute: flips which formatter owns which slot. Visible text becomes formatCompactDateTime(iso) ("Jul 19, 12:17 AM", the year appearing only once it differs from the current one), and the tooltip becomes formatRelativeTime(iso). The doc comment on the prop calls this out as "for logs and ledgers, where the date is the datum." No shipped usage of absolute found in either app. Speedway's log page wants exactly this reading, an absolute date up front with the relative reading on hover, but builds it by hand with a local compactWhen/relativeWhen pair and a bare Tooltip, with the comment "Interim local formatter until the kit pin carries Timestamp's absolute mode" (speedway/app/routes/workspaces/review/log.tsx:459-461). Speedway's pin has since moved to ^0.2.2 (its main, measured 2026-09-03), which carries absolute; the local formatter is a fold-back candidate at its next deliberate kit pass. The current pins for every consumer are stated once, in the repo's CLAUDE.md.
  • className: applied to the <time> element.

Two formatter functions are also exported from the package for callers that need the raw text without the component itself: formatDateTime and formatRelativeTime (packages/ui/src/index.ts:68). A third, formatCompactDateTime, is exported too since 2026-08-18, with the pinned DEFAULT_LOCALE beside it (packages/ui/src/index.ts:68). Real usage pairs one of these with the component in the same table column: walmart-mvp/frontend/src/pages/Jobs.tsx:220-234 uses copyValue: v => formatDateTime(v.created_at) for the column's clipboard text while render shows the live <Timestamp iso={r.created_at} />, so the copied value and the displayed value stay independently correct for their two different jobs (a stable full timestamp to paste, a compact live label to read).

The Refresh control#

RefreshControl is the one shape for "reload this page's data and tell me how fresh it is". It is a round, icon-only ghost button (variant="ghost" shade circle, Icon="Refresh") with a two-line tooltip: the heading "Refresh" and the line "Updated 3m ago, 11:15:04 PM", built from the same relative reading and clock as Timestamp. No clock or "Updated" text is ever printed beside the button; the header stays quiet until someone asks.

  • updatedAt: ISO string, epoch ms or Date; omit before the first fetch and the tooltip reads "Not loaded yet". The relative part re-renders every 30 seconds while the page stays open.
  • onRefresh: return the fetch promise and the button shows its spinner and disables itself until it settles; pass loading instead when a data hook (SWR, a loader) owns the fetch.
  • label: the tooltip heading and the accessible name, default "Refresh".

Put it in PageTitle actions or a toolbar, once per page. Do not render Updated <clock> text next to it.

Laws it enforces#

  • Show only what has something to say: the clock is not printed inline, it appears on hover; a date appears only once a time is 12 hours old.
  • One signal, said once: relative text and the exact clock are two readings of one time, so one sits in the text and the other in the tooltip, never both inline.

Sanctioned combinations#

CombinationProducesWhere usedWhy
<RefreshControl updatedAt={fetchedAt} onRefresh={reload} /> inside PageTitle actionsA page's refresh with its freshness in the tooltipthe playground /components/timestamp, "Refresh control" exampleThe ruled replacement for an inline "Updated 23:15:04" clock plus a text Refresh button
<Timestamp iso={...} className="..." /> as a table cell's render, paired with copyValue: v => formatDateTime(v.field) on the same columnA relative-time cell that copies as a full absolute timestampwalmart-mvp/frontend/src/pages/Jobs.tsx:220-234The clipboard value and the displayed value serve different readers; pairing the exported formatter with the component keeps both correct without duplicating date logic
Inline in a sentence: Last changed <Timestamp iso={...} /> by <ActorName .../>The kit's provenance-footer preset (canon doc 5 §B)speedway/app/routes/admin/usage.tsx:340-344The exact preset canon doc 5 §B names ("Provenance footer: compact Card + 'Last changed {Timestamp} by {ActorName}'")
<Timestamp iso={...} className="..." /> rendered as a plain sibling beside another element that itself carries a Tooltip, with a comment noting the deliberate non-wrapTwo independent hovers in the same row: one on the sibling, one (built in) on the Timestampwalmart-mvp/frontend/src/pages/Settings.tsx:519-533The concrete, commented case for the banned combination below: this is what doing it correctly looks like

Banned combinations#

Do not print the refresh time as inline text (Updated 23:15:04) beside a Refresh button; use RefreshControl.

Do not wrap Timestamp in a second Tooltip. It already renders one internally (timestamp.tsx:62-66); nesting stacks two tooltip triggers on the same element, a live defect the Settings.tsx:519 comment exists specifically to prevent from recurring.

Do not treat walmart-mvp/frontend/src/pages/Admin.tsx:751-761's local function Timestamp({ value }) as the kit component. It shares a name only: it takes a value prop (not iso), has no tooltip, and formats with a plain toLocaleString. It is defined and used only inside Admin.tsx itself; the kit's Timestamp (imported from @versable-git/ui) is used everywhere else in the same file for iso-keyed values. Passing value to the kit component, or iso to the local one, is a silent prop mismatch, the same same-name trap empty-state.md documents for walmart's local EmptyState.

Before you adopt this#

Five questions to answer before reaching for Timestamp.

  1. Does the shell, a parent layout, or a global provider already render this? Not applicable, Timestamp is inline content; no shell already renders it.
  2. Does this app already ship a local implementation of the same thing? Admin.tsx's own local Timestamp takes value, not iso; check which one is imported.
  3. Does this app's kit pin reach the version this component or prop landed in? absolute mode landed before 0.2.x; walmart's ^0.0.24 does not reach it, speedway's ^0.2.2 does (pins in CLAUDE.md).
  4. Does the component derive its own accessible name and keyboard path, or must the call site supply them? Not applicable, Timestamp is a <time> element with no keyboard path of its own.
  5. Which canon §A rules bind this surface, and which does the composition break? Not applicable, no canon §A rule is cited for Timestamp specifically.

Travels with#

Tooltip, internally, never externally (see above). Button (via RefreshControl) and PageTitle, whose actions slot hosts the refresh control.

Table columns' copyValue, paired with the exported formatDateTime helper rather than a hand-written date format, whenever a Timestamp cell's copy behavior needs to differ from its display.

Snippet#

import { Timestamp } from "@versable-git/ui";
// walmart-mvp/frontend/src/pages/Jobs.tsx:220-234
{
key: "created_at",
header: "Started",
sortable: true,
width: "105px",
copyValue: v => formatDateTime(v.created_at),
render: (r) => (
<Timestamp
iso={r.created_at}
className="text-base-content/55 text-xs whitespace-nowrap"
/>
),
},
@versable-git/ui · reference, canon, and method, read in place