Merge the separator sweep into the day and Coach polish

This commit is contained in:
Marc-André Bombeck committed 2026-10-09 13:44:58 +02:00
commit da92dabb55
39 files changed
+1027 -881

No files matched your search

@@ -188,8 +188,12 @@ describe("<DayView>", () => {
const html = await render();
expect(html).toContain("Running that day");
expect(html).toContain("Common cold");
expect(html).toContain("Day 4 · since");
expect(html).toContain("Winter course · Day 64 of 151");
// Name first, the quiet day count below; an open span names its start
// as a date, its year left out when it is the viewed day's year.
expect(html).toContain("Day 4, since December 31");
expect(html).toContain("Vitamin D 1,000 IU Winter course");
expect(html).toContain("Day 64 of 151");
expect(html).not.toContain("ongoing");
expect(html).toContain('href="/illness/ep1"');
});
@@ -228,10 +232,11 @@ describe("<DayView>", () => {
const html = await render();
const rows = html.split('data-slot="day-running-item"').slice(1);
const med = rows.find((row) => row.includes("Levothyroxine")) ?? "";
expect(med).toContain("50 µg");
expect(med).toContain("Levothyroxine 50 µg");
expect(med.split("</li>")[0]).not.toMatch(/Day \d|since/);
const leave = rows.find((row) => row.includes("Parental leave")) ?? "";
expect(leave).toContain("Family · Day 34 of 90");
expect(leave).toMatch(/data-slot="tag-chip"[^>]*>Family</);
expect(leave).toContain("Day 34 of 90");
expect(leave).not.toContain("FAMILY");
});
+58 -19
View File
@@ -23,8 +23,10 @@ import {
type LucideIcon,
} from "lucide-react";
import { TagChip } from "@/components/ui/tag-chip";
import {
LIFE_EVENT_CATEGORIES,
type DateKey,
type DayEvent,
type DayEventKind,
type DayNotable,
@@ -32,6 +34,7 @@ import {
type DayRunningKind,
type DayValue,
} from "@/lib/day/contract";
import { resolveIntlLocale } from "@/lib/format-locale";
import { useFormatters, useTranslations } from "@/lib/i18n/context";
import { cn } from "@/lib/utils";
@@ -138,7 +141,7 @@ export function DayNotableLines({
<Sparkles className="mt-px size-3.5 shrink-0" aria-hidden="true" />
<span>
{withMetric && notable.type
? `${labelFor(tileKeyOf(notable.type))} · `
? `${labelFor(tileKeyOf(notable.type))}: `
: null}
{text(notable)}
</span>
@@ -200,28 +203,59 @@ function dayMeta(
return meta;
}
export function DayRunning({ items }: { items: readonly DayRunningItem[] }) {
const { t } = useTranslations();
const fmt = useFormatters();
/** Running kinds whose `sub` is a dose and reads as part of the name. */
const DOSE_SUB_KINDS: ReadonlySet<DayRunningKind> = new Set([
"medication",
"medicationCourse",
]);
export function DayRunning({
items,
date,
}: {
items: readonly DayRunningItem[];
/** The viewed day; a start date in another year names its year. */
date?: DateKey;
}) {
const { t, locale } = useTranslations();
if (items.length === 0) return null;
const viewedYear = date
? date.slice(0, 4)
: String(new Date().getUTCFullYear());
const intlLocale = resolveIntlLocale(locale);
const sinceText = (since: string): string =>
new Intl.DateTimeFormat(intlLocale, {
day: "numeric",
month: "long",
...(since.slice(0, 4) === viewedYear ? {} : { year: "numeric" }),
timeZone: "UTC",
}).format(new Date(`${since}T12:00:00.000Z`));
return (
<section className="space-y-2" data-slot="day-running">
<h3 className={DAY_SECTION_LABEL}>{t("day.groups.running")}</h3>
<ul>
{items.map((item) => {
const parts = [
item.sub === null ? null : dayMeta(item.kind, item.sub, t),
const title = dayTitle(item.kind, item.title, t);
const doseInName = item.sub !== null && DOSE_SUB_KINDS.has(item.kind);
const name = doseInName ? `${title} ${item.sub}` : title;
const chip =
item.sub !== null && !doseInName
? dayMeta(item.kind, item.sub, t)
: null;
const since =
item.dayCount === null && item.since !== null
? sinceText(item.since)
: null;
const detail =
item.dayIndex !== null
? item.dayCount !== null
? t("day.dayOf", { n: item.dayIndex, m: item.dayCount })
: t("day.dayN", { n: item.dayIndex })
: null,
item.dayCount === null && item.since !== null
? t("day.since", {
date: fmt.dateShortSmartCalendar(item.since),
})
: null,
].filter((part): part is string => !!part);
: since !== null
? t("day.dayNSince", { n: item.dayIndex, date: since })
: t("day.dayN", { n: item.dayIndex })
: since !== null
? t("day.sinceDate", { date: since })
: null;
const body = (
<>
<span
@@ -230,12 +264,17 @@ export function DayRunning({ items }: { items: readonly DayRunningItem[] }) {
style={{ backgroundColor: RUNNING_COLOR[item.kind] }}
/>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium">
{dayTitle(item.kind, item.title, t)}
</span>
<span className="text-muted-foreground block truncate text-xs">
{parts.join(" · ")}
<span className="flex min-w-0 items-center gap-2">
<span className="min-w-0 truncate text-sm font-medium">
{name}
</span>
{chip ? <TagChip className="shrink-0">{chip}</TagChip> : null}
</span>
{detail ? (
<span className="text-muted-foreground block truncate text-xs">
{detail}
</span>
) : null}
</span>
{item.href ? (
<ChevronRight