From 60be63bcd1b8a900ec496a7b173c4dcca049254b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Tue, 20 Oct 2026 13:03:15 +0200 Subject: [PATCH] feat(ui): InlineAction for small text actions, with a 44 px touch floor Thirty-three hand-rolled underlined text links and buttons (Open schedule, Select all, View all, Retry, Learn more, the device-code copy action, ...) carried three different focus rings, none on half of them, and a 16 px target on a phone. They now render through one InlineAction primitive: tone primary / muted / foreground, size xs / sm, a focus ring, and min-h-11 on a coarse pointer so the desktop layout does not move. A structural test refuses a new hand-rolled copy. --- src/__tests__/inline-action-guard.test.ts | 121 ++++++++++++++++++ src/app/mood/page-client.tsx | 15 +-- .../admin/central-codex-section.tsx | 9 +- .../admin/module-availability-section.tsx | 12 +- src/components/admin/recent-audit-preview.tsx | 14 +- .../dashboard/key-backup-banner.tsx | 12 +- .../sources/document-source-picker.tsx | 9 +- .../insights/coach-panel/chat-bubble.tsx | 13 +- src/components/insights/health-score-card.tsx | 18 +-- .../insights/medication-efficacy-summary.tsx | 16 ++- .../insights/metric-target-summary.tsx | 24 ++-- .../insights/sleep/chronotype-card.tsx | 9 +- src/components/insights/trends-row.tsx | 22 ++-- .../insights/workout-detail/stats.tsx | 18 +-- src/components/links/entity-link-picker.tsx | 8 +- .../card-parts/medication-compliance-bars.tsx | 11 +- .../detail/medication-detail-tabs.tsx | 8 +- .../sections/inventory-section.tsx | 24 ++-- src/components/mood/mood-context-fields.tsx | 22 ++-- src/components/mood/mood-list.tsx | 8 +- src/components/onboarding/done-screen.tsx | 42 +++--- .../settings/access/handover-decision.tsx | 16 ++- .../settings/ai/codex-provider-form.tsx | 9 +- .../settings/ai/openai-provider-form.tsx | 9 +- .../integrations/oauth-integration-card.tsx | 22 ++-- src/components/settings/low-stock-card.tsx | 13 +- src/components/settings/module-toggle-row.tsx | 14 +- .../report-selection/selection-panel.tsx | 14 +- .../settings/security-section/totp-card.tsx | 16 +-- .../ui/__tests__/inline-action.test.tsx | 50 ++++++++ src/components/ui/inline-action.tsx | 72 +++++++++++ src/components/ui/learn-more-link.tsx | 27 ++-- .../vaccinations/vaccination-form.tsx | 9 +- 33 files changed, 484 insertions(+), 222 deletions(-) create mode 100644 src/__tests__/inline-action-guard.test.ts create mode 100644 src/components/ui/__tests__/inline-action.test.tsx create mode 100644 src/components/ui/inline-action.tsx diff --git a/src/__tests__/inline-action-guard.test.ts b/src/__tests__/inline-action-guard.test.ts new file mode 100644 index 000000000..82b6fccba --- /dev/null +++ b/src/__tests__/inline-action-guard.test.ts @@ -0,0 +1,121 @@ +/** + * A small text action inside a line of content goes through `InlineAction` + * (`src/components/ui/inline-action.tsx`). + * + * Thirty-odd hand-rolled copies of the same "underlined small text link" + * carried three different focus rings, none at all on half of them, and a + * 16 px touch target on a phone. The primitive owns the 44 px floor on a + * coarse pointer and the ring; this guard keeps a new copy from appearing + * beside it. + * + * Matched: a `', + }, + { + path: "components/z.tsx", + source: 'Go', + }, + ]; + expect(findHandRolledInlineActions(planted)).toEqual([ + "components/x.tsx:Link", + "components/y.tsx:button", + ]); + }); + + it("is used where the copies were", () => { + const users = files.filter((f) => f.source.includes(" - {t("mood.mentalWellbeingLink")} -
{sourceLink ? ( - - - {t("targets.sourceLabel", { - source: sourceName(target.source, t), - })} - - + + + + {t("targets.sourceLabel", { + source: sourceName(target.source, t), + })} + + + ) : ( {t("targets.sourceLabel", { diff --git a/src/components/insights/sleep/chronotype-card.tsx b/src/components/insights/sleep/chronotype-card.tsx index 14c0e66be..3220acd6c 100644 --- a/src/components/insights/sleep/chronotype-card.tsx +++ b/src/components/insights/sleep/chronotype-card.tsx @@ -8,6 +8,7 @@ import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { LearningGate } from "@/components/ui/learning-gate"; import { TileHeader } from "@/components/insights/tile-header"; import type { ChronotypeDto } from "./use-sleep-rhythm"; +import { InlineAction } from "@/components/ui/inline-action"; /** Minutes-of-day → "HH:MM" wall-clock label (24 h, zero-padded). */ function clockLabel(minutesOfDay: number): string { @@ -90,12 +91,12 @@ export function ChronotypeCard({ chronotype }: { chronotype: ChronotypeDto }) { )}
- + {showAdvanced && (
diff --git a/src/components/insights/trends-row.tsx b/src/components/insights/trends-row.tsx index b62e8e06b..eda495290 100644 --- a/src/components/insights/trends-row.tsx +++ b/src/components/insights/trends-row.tsx @@ -23,6 +23,7 @@ import { type TrendAnnotationStatus, } from "./trend-annotation"; import { useUnitDisplay } from "@/hooks/use-unit-display"; +import { InlineAction } from "@/components/ui/inline-action"; /** * Trends row — a small chart per metric above a one-sentence @@ -339,15 +340,20 @@ export function TrendsRow({ is used but was unreachable from Insights. The link sits below the caption so the chart keeps its own interactions. */} {config.detailHref ? ( - - {t("insights.trendsRow.viewDetail", { metric: title })} -
); diff --git a/src/components/insights/workout-detail/stats.tsx b/src/components/insights/workout-detail/stats.tsx index 36002d3bd..4aa93b56c 100644 --- a/src/components/insights/workout-detail/stats.tsx +++ b/src/components/insights/workout-detail/stats.tsx @@ -28,6 +28,7 @@ import { formatPace, formatDurationMinutes, } from "./format"; +import { InlineAction } from "@/components/ui/inline-action"; interface StatTileProps { icon: ReactNode; @@ -199,14 +200,15 @@ export function WorkoutDetailStats({ workout }: WorkoutDetailStatsProps) { )} {workout.previousWorkoutId ? ( - - {t("insights.workouts.detail.previousSessionLink")} - - + ) : null}