mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
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:
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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user