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.
This commit is contained in:
Marc-André Bombeck committed 2026-10-20 13:03:15 +02:00
1 parent ce8074ad08
commit 60be63bcd1
33 files changed
+484 -222

No files matched your search

@@ -22,6 +22,7 @@ import {
useHandoverDecision,
type HandoverDecisionState,
} from "@/lib/queries/use-handover-decision";
import { InlineAction } from "@/components/ui/inline-action";
type PendingDecision = NonNullable<HandoverDecisionState["pending"]>;
@@ -191,13 +192,14 @@ export function HandoverNotificationHint() {
<p className="text-sm">
{t("recordSharing.handoverDecision.notificationsBody")}
</p>
<Link
href="/settings/notifications"
className="text-primary text-sm underline"
data-slot="handover-notification-link"
>
{t("recordSharing.handoverDecision.notificationsAction")}
</Link>
<InlineAction asChild size="sm" className="underline">
<Link
href="/settings/notifications"
data-slot="handover-notification-link"
>
{t("recordSharing.handoverDecision.notificationsAction")}
</Link>
</InlineAction>
</div>
</div>
);
@@ -18,6 +18,7 @@ import { useTranslations } from "@/lib/i18n/context";
import { aiInputDependentKeys, invalidateKeys } from "@/lib/query-keys";
import type { InsightsSettings } from "./shared";
import { InlineAction } from "@/components/ui/inline-action";
export function CodexProviderForm({
settings,
@@ -241,15 +242,15 @@ export function CodexProviderForm({
{t("settings.ai.deviceCodeStep2")}
<div className="bg-card border-border mt-2 inline-flex items-center gap-2 rounded border px-3 py-2 font-mono text-lg tracking-widest">
{deviceCode.userCode}
<button
type="button"
<InlineAction
onClick={() =>
navigator.clipboard?.writeText(deviceCode.userCode)
}
className="text-muted-foreground hover:text-foreground text-xs underline"
tone="muted"
className="underline"
>
{t("settings.ai.deviceCodeCopy")}
</button>
</InlineAction>
</div>
</li>
<li>{t("settings.ai.deviceCodeStep3")}</li>
@@ -27,6 +27,7 @@ import {
uiToLegacyProviderEnum,
type UserAIProvider,
} from "./shared";
import { InlineAction } from "@/components/ui/inline-action";
export function OpenAIProviderForm({
userProvider,
@@ -172,15 +173,15 @@ export function OpenAIProviderForm({
)}
<div>
<button
type="button"
<InlineAction
tone="muted"
className="underline"
onClick={() => setShowAdvanced((v) => !v)}
className="text-muted-foreground hover:text-foreground text-xs underline"
>
{showAdvanced
? t("settings.ai.openai.hideAdvanced")
: t("settings.ai.openai.showAdvanced")}
</button>
</InlineAction>
{showAdvanced && (
<div className="mt-2 space-y-1">
<Label htmlFor="ai-openai-base-url">
@@ -65,6 +65,7 @@ import {
type IntegrationDocsProvider,
} from "./setup-guide-link";
import { failureStreakFor, type IntegrationStatusViewModel } from "./shared";
import { InlineAction } from "@/components/ui/inline-action";
export type OAuthIntegrationKey =
| "withings"
@@ -489,16 +490,17 @@ export function OAuthIntegrationCardView({
details || planLimited || viewModel?.metricFreshness?.length ? (
<>
{planLimited ? (
<a
href={WITHINGS_API_PLANS_URL}
target="_blank"
rel="noopener noreferrer"
data-testid="withings-plan-link"
className="text-primary inline-flex min-h-11 items-center gap-1 text-sm underline-offset-2 hover:underline sm:min-h-0"
>
{t("settings.integrations.withings.plan.link")}
<ExternalLink className="size-3" aria-hidden="true" />
</a>
<InlineAction asChild size="sm">
<a
href={WITHINGS_API_PLANS_URL}
target="_blank"
rel="noopener noreferrer"
data-testid="withings-plan-link"
>
{t("settings.integrations.withings.plan.link")}
<ExternalLink className="size-3" aria-hidden="true" />
</a>
</InlineAction>
) : null}
{details}
<MetricFreshnessList entries={viewModel?.metricFreshness} />
+6 -7
View File
@@ -13,6 +13,7 @@ import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import { apiFetchRaw, apiGet } from "@/lib/api/api-fetch";
import { readUpdatedAtToken, withBaseToken } from "@/lib/api/optimistic-token";
import { InlineAction } from "@/components/ui/inline-action";
/**
* v1.16.11 — medication low-stock alert threshold (Settings →
@@ -320,13 +321,11 @@ export function LowStockCard({
{/* v1.16.11 — back-link to the medications list, the surface the
alert is about (the supply tab links here the same way). */}
<p>
<Link
href="/medications"
className="text-muted-foreground hover:text-foreground text-xs underline-offset-2 hover:underline"
data-slot="low-stock-medications-link"
>
{t("notifications.lowStock.medicationsLink")}
</Link>
<InlineAction asChild tone="muted">
<Link href="/medications" data-slot="low-stock-medications-link">
{t("notifications.lowStock.medicationsLink")}
</Link>
</InlineAction>
</p>
</SettingsCard>
);
@@ -36,6 +36,7 @@ import type { LucideIcon } from "lucide-react";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { InlineAction } from "@/components/ui/inline-action";
export interface ModuleToggleRowProps {
/** Stable key — used for the input id + the test query handle. */
@@ -112,13 +113,12 @@ export function ModuleToggleRow({
<p className="text-muted-foreground text-xs">{disabledReason}</p>
) : null}
{manageLink ? (
<Link
href={manageLink.href}
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1 text-xs underline-offset-2 hover:underline"
>
{manageLink.label}
<ChevronRight className="h-3.5 w-3.5" aria-hidden="true" />
</Link>
<InlineAction asChild tone="muted">
<Link href={manageLink.href}>
{manageLink.label}
<ChevronRight className="size-3.5" aria-hidden="true" />
</Link>
</InlineAction>
) : null}
</div>
</div>
@@ -74,6 +74,7 @@ import {
shiftDateKey,
validTimezoneOr,
} from "@/lib/tz/format";
import { InlineAction } from "@/components/ui/inline-action";
type ExportFormat = "pdf" | "fhir" | "package";
@@ -310,13 +311,12 @@ export function HealthRecordExportPanel() {
{/* Entry point into the document vault for the report period
(navigation only). Only rendered when the module is enabled. */}
{user?.modules?.inboundDocuments ? (
<Link
href={`/documents?year=${new Date().getFullYear()}`}
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 inline-flex items-center gap-1.5 rounded-md text-xs transition-colors focus-visible:ring-[3px] focus-visible:outline-none"
>
<FolderOpen className="size-3.5" aria-hidden />
{t("settings.healthRecord.documentsLink")}
</Link>
<InlineAction asChild tone="muted" className="gap-1.5">
<Link href={`/documents?year=${new Date().getFullYear()}`}>
<FolderOpen className="size-3.5" aria-hidden />
{t("settings.healthRecord.documentsLink")}
</Link>
</InlineAction>
) : null}
</div>
</div>
@@ -46,6 +46,7 @@ import {
type ExistingFactorProof,
type ReauthMethod,
} from "./existing-factor-reauth-dialog";
import { InlineAction } from "@/components/ui/inline-action";
interface SetupData {
otpauthUri: string;
@@ -430,13 +431,9 @@ export function TotpCard({
{t("settings.security.totp.verify")}
</Button>
</div>
<button
type="button"
onClick={resetWizard}
className="text-muted-foreground hover:text-foreground text-xs"
>
<InlineAction tone="muted" onClick={resetWizard}>
{t("common.cancel")}
</button>
</InlineAction>
</form>
</div>
)}
@@ -564,9 +561,8 @@ export function TotpCard({
}
className="font-mono"
/>
<button
type="button"
className="text-muted-foreground hover:text-foreground text-xs"
<InlineAction
tone="muted"
onClick={() =>
setDisableMethod((m) =>
m === "totp" ? "recovery" : "totp",
@@ -576,7 +572,7 @@ export function TotpCard({
{disableMethod === "totp"
? t("settings.security.useRecoveryCode")
: t("settings.security.useAuthenticator")}
</button>
</InlineAction>
</div>
<AlertDialogFooter>
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>