Typography round 5 (owner): drop the message count + active-row weight; ISO dates
Three declutter moves, all trivially reversible: - The meta line drops the message count (idle meta is now just how long ago). The count informed no decision — clutter. messageCount still rides the API payload, so this is a one-line flip in sessionMeta if it's ever wanted. - The active row is marked by the background alone: weight was a second channel doing the same job, and the flip made selected titles jump. - Dates beyond the week render ISO (YYYY-MM-DD, zero-padded) instead of M/D/YYYY — the US convention swapped for the international standard: unambiguous in any locale, sorts lexically. A user-locale setting can come later — it's a format-function swap. Also dropped the row's vestigial text-sm: both lines carry explicit sizes now (16px title, 11px meta). Live: active weight 400 + bg-accent, metas '17m ago' / 'Waiting for your answer · 1d ago', no counts. Gates: typecheck, lint, 469/469.
This commit is contained in:
+17
-8
@@ -86,7 +86,11 @@ function SessionStatus({ busy }: { busy: boolean }) {
|
||||
|
||||
/** Compact "how long ago": "just now", "5m ago", "3h ago", "2d ago",
|
||||
* then a plain date. How long a session has been WAITING matters more
|
||||
* than an absolute timestamp. Pure — exported for tests. */
|
||||
* than an absolute timestamp. Beyond a week the date renders ISO
|
||||
* (YYYY-MM-DD, zero-padded) — the international standard: unambiguous
|
||||
* in any locale and it sorts (owner, typography round 5; the old
|
||||
* M/D/YYYY was the US convention. A user-locale setting can come later
|
||||
* — trivial, a format function swap). Pure — exported for tests. */
|
||||
export function timeAgo(iso: string, now = new Date()): string {
|
||||
const ms = now.getTime() - new Date(iso).getTime();
|
||||
if (ms < 60_000) return "just now";
|
||||
@@ -96,16 +100,21 @@ export function timeAgo(iso: string, now = new Date()): string {
|
||||
if (h < 24) return `${h}h ago`;
|
||||
const d = Math.floor(h / 24);
|
||||
if (d < 7) return `${d}d ago`;
|
||||
return new Date(iso).toLocaleDateString();
|
||||
const date = new Date(iso);
|
||||
const mm = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const dd = String(date.getDate()).padStart(2, "0");
|
||||
return `${date.getFullYear()}-${mm}-${dd}`;
|
||||
}
|
||||
|
||||
/** The row's second line: state replaces the message count when the state
|
||||
* is the more useful fact (most resting sessions are awaiting an answer —
|
||||
* that's what the learner scans for). */
|
||||
/** The row's second line: the state when a state exists (waiting/working),
|
||||
* otherwise just how long ago. The message count is DROPPED (owner,
|
||||
* typography round 5): it informed no decision — clutter. messageCount
|
||||
* still rides the API payload, so this is a one-line flip back if it's
|
||||
* ever wanted. */
|
||||
function sessionMeta(chat: ChatListItem): string {
|
||||
if (chat.pendingQuestion) return `Waiting for your answer · ${timeAgo(chat.updatedAt)}`;
|
||||
if (chat.busy) return "Teacher is working…";
|
||||
return `${chat.messageCount} messages · ${new Date(chat.updatedAt).toLocaleDateString()}`;
|
||||
return timeAgo(chat.updatedAt);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,8 +136,8 @@ export function SessionRow({
|
||||
type="button"
|
||||
onClick={onOpen}
|
||||
className={cn(
|
||||
"relative mb-0.5 flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm transition-colors hover:bg-accent",
|
||||
active && "bg-accent font-medium",
|
||||
"relative mb-0.5 flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left transition-colors hover:bg-accent",
|
||||
active && "bg-accent",
|
||||
)}
|
||||
>
|
||||
<SessionStatus busy={chat.busy} />
|
||||
|
||||
@@ -86,26 +86,30 @@ describe("SessionRow", () => {
|
||||
const working = render(item({ busy: true }));
|
||||
assert.ok(working.includes("Teacher is working…"), "working meta");
|
||||
|
||||
// Owner, typography round 5: the message count is dropped — it
|
||||
// informed no decision. Idle meta is just how long ago.
|
||||
const idle = render(item({ title: "Partial derivatives", messageCount: 7 }));
|
||||
assert.ok(idle.includes("Partial derivatives"));
|
||||
assert.match(idle, /7 messages · \d{1,2}\/\d{1,2}\/\d{4}/, "idle keeps the count");
|
||||
assert.ok(!idle.includes("messages"), "the count is gone (flip-flop: one line in sessionMeta)");
|
||||
assert.match(idle, /\d{4}-\d{2}-\d{2}/, "idle shows the ISO date");
|
||||
});
|
||||
|
||||
it("active row is visually marked", () => {
|
||||
it("active row is visually marked by the background alone (owner, round 5: no weight change)", () => {
|
||||
const html = render(item({}), true);
|
||||
assert.ok(html.includes("font-medium"), "active emphasis");
|
||||
assert.ok(html.includes("bg-accent"), "active background");
|
||||
assert.ok(!html.includes("font-medium"), "weight is not a second channel doing the same job");
|
||||
});
|
||||
});
|
||||
|
||||
describe("timeAgo", () => {
|
||||
const now = new Date("2026-01-02T12:00:00.000Z");
|
||||
|
||||
it("just now / minutes / hours / days, then falls back to a date", () => {
|
||||
it("just now / minutes / hours / days, then the ISO date (owner, round 5)", () => {
|
||||
assert.equal(timeAgo("2026-01-02T11:59:30.000Z", now), "just now");
|
||||
assert.equal(timeAgo("2026-01-02T11:55:00.000Z", now), "5m ago");
|
||||
assert.equal(timeAgo("2026-01-02T09:00:00.000Z", now), "3h ago");
|
||||
assert.equal(timeAgo("2025-12-31T12:00:00.000Z", now), "2d ago");
|
||||
assert.match(timeAgo("2025-06-02T12:00:00.000Z", now), /\d/, "beyond a week: a date");
|
||||
assert.equal(timeAgo("2025-06-02T12:00:00.000Z", now), "2025-06-02", "beyond a week: ISO — unambiguous, sortable");
|
||||
});
|
||||
|
||||
it("negative drift (clock skew) reads as just now, never a future date", () => {
|
||||
|
||||
Reference in New Issue
Block a user