fix(ui): tidy the new record and settings surfaces at phone width

- Composition amounts show only the decimals they carry, so 100 µg no
  longer reads as 100.000 µg; the remove button stays on the amount row.
- AUDIT-C remove controls are icon-only, so they no longer spill out of
  the card or cover the earlier answer.
- Life-circumstance labels keep their colon when they wrap, checkbox
  groups sit in two columns and the weekly-minutes sentence ends.
- Link chips get a 44 px hit area for their remove control.
- The AI usage level reports a failed save with a sentence and the
  error id instead of a bare "Error".
- The Dawarich home-country and admin refresh-gap labels sit above
  their fields.
This commit is contained in:
Marc-André Bombeck committed 2026-10-16 08:03:29 +02:00
1 parent a806676a71
commit 69e2f18788
14 files changed
+50 -34

No files matched your search

+1 -1
View File
@@ -188,7 +188,7 @@ function AiUsageForm({ bounds }: { bounds: AiUsageBounds }) {
</p>
) : null}
<div className="space-y-1.5">
<Label htmlFor="admin-ai-usage-gap">
<Label htmlFor="admin-ai-usage-gap" className="block leading-snug">
{t("admin.aiUsage.gapLabel")}
</Label>
<Input
+1 -1
View File
@@ -423,7 +423,7 @@ export function EntityLinkPicker({
name: chipName(option),
})}
onClick={() => unlink(option)}
className="text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:ring-ring/50 mr-0.5 flex size-7 shrink-0 items-center justify-center rounded-full focus-visible:ring-[3px] focus-visible:outline-none"
className="text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:ring-ring/50 relative mr-0.5 flex size-7 shrink-0 items-center justify-center rounded-full after:absolute after:-inset-2 after:content-[''] focus-visible:ring-[3px] focus-visible:outline-none"
>
<X className="size-3.5" aria-hidden />
</button>
@@ -64,7 +64,13 @@ function formatAmount(
value: number,
): string {
// Up to three decimals: 0.025 mg of a trace element must not read as 0.
return fmt.number(Math.round(value * 1000) / 1000, 3);
// Only as many as the value has, so 100 µg never reads "100.000" (which a
// German reader takes for a hundred thousand).
const rounded = Math.round(value * 1000) / 1000;
const digits = [0, 1, 2, 3].find(
(d) => Math.abs(rounded * 10 ** d - Math.round(rounded * 10 ** d)) < 1e-9,
);
return fmt.number(rounded, digits ?? 3);
}
/** One composition line: "25 µg (1,000 IU)". */
@@ -343,10 +349,10 @@ function CompositionEditor({
return (
<li
key={row.key}
className="grid grid-cols-[1fr_auto] items-end gap-x-3 gap-y-2 sm:grid-cols-[2fr_1fr_1fr_auto]"
className="grid grid-cols-[1fr_auto_auto] items-end gap-x-3 gap-y-2 sm:grid-cols-[2fr_1fr_1fr_auto]"
data-slot="medication-composition-row"
>
<div className="col-span-2 space-y-2 sm:col-span-1">
<div className="col-span-3 space-y-2 sm:col-span-1">
<Label htmlFor={`${formId}-nutrient-${row.key}`}>
{t("medications.composition.nutrientLabel")}
</Label>
+5 -5
View File
@@ -163,7 +163,7 @@ export function AuditCPanel({
})}
</span>
<ConfirmButton
label={t("records.life.auditC.remove")}
label=""
ariaLabel={t("records.life.auditC.remove")}
title={t("records.life.auditC.removeConfirmTitle")}
body={t("records.life.auditC.removeConfirmBody")}
@@ -173,7 +173,7 @@ export function AuditCPanel({
disabled={remove.isPending}
variant="ghost"
size="icon"
className="text-destructive min-h-11 sm:min-h-9"
className="text-destructive size-11 shrink-0 sm:size-9"
icon={<Trash2 className="size-4" aria-hidden />}
slot={`audit-c-remove-${entry.id}`}
/>
@@ -188,7 +188,7 @@ export function AuditCPanel({
className="flex items-start justify-between gap-2"
data-slot="audit-c-legacy-pattern"
>
<div className="space-y-1">
<div className="min-w-0 flex-1 space-y-1">
<p className="text-sm">
{t("records.life.auditC.legacyLabel", {
value: t(
@@ -201,7 +201,7 @@ export function AuditCPanel({
</p>
</div>
<ConfirmButton
label={t("records.life.auditC.legacyRemove")}
label=""
ariaLabel={t("records.life.auditC.legacyRemove")}
title={t("records.life.auditC.legacyRemoveConfirmTitle")}
body={t("records.life.auditC.legacyRemoveConfirmBody")}
@@ -211,7 +211,7 @@ export function AuditCPanel({
disabled={removeLegacy.isPending}
variant="ghost"
size="icon"
className="text-destructive min-h-11 sm:min-h-9"
className="text-destructive size-11 shrink-0 sm:size-9"
icon={<Trash2 className="size-4" aria-hidden />}
slot="audit-c-legacy-remove"
/>
@@ -185,7 +185,11 @@ function Field({
}) {
return (
<div className="space-y-1.5">
<Label htmlFor={id}>{label}</Label>
{/* Block, so the colon follows a label that wraps instead of standing
alone at the far edge as a flex item. */}
<Label htmlFor={id} className="block leading-snug">
{label}
</Label>
{children}
</div>
);
@@ -324,16 +328,17 @@ function MultiField<T extends string>({
<fieldset className="space-y-2" data-slot={id}>
<legend className="text-sm font-medium">{label}</legend>
{hint ? <p className="text-muted-foreground text-xs">{hint}</p> : null}
<div className="grid gap-x-3 gap-y-2 sm:grid-cols-2">
<div className="grid grid-cols-2 gap-x-3 gap-y-2">
{options.map((option) => {
const checked = value.includes(option.value);
const full = max !== undefined && value.length >= max && !checked;
return (
<label
key={option.value}
className="flex min-h-11 items-center gap-2 text-sm sm:min-h-9"
className="flex min-h-11 min-w-0 items-center gap-2 text-sm sm:min-h-9"
>
<Checkbox
className="shrink-0"
checked={checked}
disabled={disabled || full}
onCheckedChange={(next) =>
+4 -6
View File
@@ -26,6 +26,7 @@ import { SettingsCardActions } from "@/components/settings/_card-actions";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useAiProviderState, useAiUsage } from "@/hooks/use-ai-capability";
import { apiPut } from "@/lib/api/api-fetch";
import { showErrorToast } from "@/lib/errors/error-toast";
import type { AiUsageLevel, AiUsageState } from "@/lib/ai/usage-level";
import { useTranslations } from "@/lib/i18n/context";
import { aiInputDependentKeys, invalidateKeys } from "@/lib/query-keys";
@@ -59,6 +60,9 @@ export function AiUsageCard() {
onSuccess: () => {
void invalidateKeys(queryClient, aiInputDependentKeys);
},
// The toast carries the error id a 5xx needs for a support report.
onError: (error) =>
showErrorToast(t("settings.ai.usage.saveError"), { cause: error }),
});
// An older server publishes no level: there is nothing to set.
@@ -215,12 +219,6 @@ export function AiUsageCard() {
</p>
) : null}
{save.isError ? (
<p role="alert" className="text-destructive text-xs">
{t("settings.ai.errorGeneric")}
</p>
) : null}
{usage.preference !== null ? (
<SettingsCardActions>
<Button
@@ -319,7 +319,7 @@ function DawarichHome() {
</div>
) : null}
<div className="space-y-1.5">
<Label htmlFor="dawarich-home-country">
<Label htmlFor="dawarich-home-country" className="flex">
{t("settings.dawarichHomeCountry")}
</Label>
<Input