mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
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:
1 parent
a806676a71
commit
69e2f18788
14 files changed
+50
-34
No files matched your search
+3
-2
@@ -6427,7 +6427,8 @@
|
||||
"noEstimate": "Eine Schätzung je Stufe erscheint nach drei Tagen KI-Nutzung.",
|
||||
"capped": "Du hast {choice} gewählt, das der Server gerade nicht erlaubt, deshalb gilt {level}. Deine Wahl bleibt gespeichert.",
|
||||
"followsDefault": "Du folgst dem Standard des Servers ({level}).",
|
||||
"useDefault": "Standard des Servers verwenden ({level})"
|
||||
"useDefault": "Standard des Servers verwenden ({level})",
|
||||
"saveError": "Die Stufe konnte nicht gespeichert werden. Bitte versuche es erneut."
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
@@ -11261,7 +11262,7 @@
|
||||
"derived": {
|
||||
"packYears": "{value} Packungsjahre",
|
||||
"packYearsMissing": "Für Packungsjahre fehlen Zigaretten pro Tag oder das Jahr des Rauchbeginns.",
|
||||
"activityMinutes": "{value} Minuten pro Woche",
|
||||
"activityMinutes": "{value} Minuten pro Woche.",
|
||||
"meetsWho": "Damit erreichst du die WHO-Empfehlung von 150 Minuten pro Woche.",
|
||||
"belowWho": "Das liegt unter der WHO-Empfehlung von 150 Minuten pro Woche.",
|
||||
"measuredSteps": "Deine gemessenen Schritte haben Vorrang, diese Angabe ergänzt sie.",
|
||||
|
||||
+3
-2
@@ -6427,7 +6427,8 @@
|
||||
"noEstimate": "An estimate per level appears after three days of AI use.",
|
||||
"capped": "You chose {choice}, which the server does not allow at the moment, so {level} applies. Your choice is kept.",
|
||||
"followsDefault": "You follow the server default ({level}).",
|
||||
"useDefault": "Use the server default ({level})"
|
||||
"useDefault": "Use the server default ({level})",
|
||||
"saveError": "The level could not be saved. Please try again."
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
@@ -11261,7 +11262,7 @@
|
||||
"derived": {
|
||||
"packYears": "{value} pack-years",
|
||||
"packYearsMissing": "Pack-years need cigarettes per day and the year you started.",
|
||||
"activityMinutes": "{value} minutes a week",
|
||||
"activityMinutes": "{value} minutes a week.",
|
||||
"meetsWho": "That meets the WHO recommendation of 150 minutes a week.",
|
||||
"belowWho": "That is below the WHO recommendation of 150 minutes a week.",
|
||||
"measuredSteps": "Your measured steps come first; this answer adds context.",
|
||||
|
||||
+3
-2
@@ -6427,7 +6427,8 @@
|
||||
"noEstimate": "La estimación por nivel aparece tras tres días de uso de la IA.",
|
||||
"capped": "Elegiste {choice}, que el servidor no permite ahora mismo, así que se aplica {level}. Tu elección se conserva.",
|
||||
"followsDefault": "Sigues el valor predeterminado del servidor ({level}).",
|
||||
"useDefault": "Usar el valor predeterminado del servidor ({level})"
|
||||
"useDefault": "Usar el valor predeterminado del servidor ({level})",
|
||||
"saveError": "No se pudo guardar el nivel. Inténtalo de nuevo."
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
@@ -11261,7 +11262,7 @@
|
||||
"derived": {
|
||||
"packYears": "{value} paquetes-año",
|
||||
"packYearsMissing": "Para los paquetes-año faltan los cigarrillos al día o el año en que empezaste.",
|
||||
"activityMinutes": "{value} minutos a la semana",
|
||||
"activityMinutes": "{value} minutos a la semana.",
|
||||
"meetsWho": "Cumple la recomendación de la OMS de 150 minutos semanales.",
|
||||
"belowWho": "Está por debajo de la recomendación de la OMS de 150 minutos semanales.",
|
||||
"measuredSteps": "Tus pasos medidos tienen prioridad; esta respuesta aporta contexto.",
|
||||
|
||||
+3
-2
@@ -6427,7 +6427,8 @@
|
||||
"noEstimate": "Une estimation par niveau apparaît après trois jours d'utilisation de l'IA.",
|
||||
"capped": "Vous avez choisi {choice}, que le serveur n'autorise pas pour le moment, donc {level} s'applique. Votre choix est conservé.",
|
||||
"followsDefault": "Vous suivez le réglage par défaut du serveur ({level}).",
|
||||
"useDefault": "Utiliser le réglage par défaut du serveur ({level})"
|
||||
"useDefault": "Utiliser le réglage par défaut du serveur ({level})",
|
||||
"saveError": "Le niveau n'a pas pu être enregistré. Réessaie."
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
@@ -11261,7 +11262,7 @@
|
||||
"derived": {
|
||||
"packYears": "{value} paquets-années",
|
||||
"packYearsMissing": "Pour les paquets-années, il manque les cigarettes par jour ou l'année de début.",
|
||||
"activityMinutes": "{value} minutes par semaine",
|
||||
"activityMinutes": "{value} minutes par semaine.",
|
||||
"meetsWho": "Cela atteint la recommandation de l'OMS de 150 minutes par semaine.",
|
||||
"belowWho": "C'est en dessous de la recommandation de l'OMS de 150 minutes par semaine.",
|
||||
"measuredSteps": "Vos pas mesurés passent en premier ; cette réponse apporte du contexte.",
|
||||
|
||||
+3
-2
@@ -6427,7 +6427,8 @@
|
||||
"noEstimate": "La stima per livello compare dopo tre giorni di uso dell'IA.",
|
||||
"capped": "Hai scelto {choice}, che il server al momento non consente, quindi vale {level}. La tua scelta resta salvata.",
|
||||
"followsDefault": "Segui l'impostazione predefinita del server ({level}).",
|
||||
"useDefault": "Usa l'impostazione predefinita del server ({level})"
|
||||
"useDefault": "Usa l'impostazione predefinita del server ({level})",
|
||||
"saveError": "Non è stato possibile salvare il livello. Riprova."
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
@@ -11261,7 +11262,7 @@
|
||||
"derived": {
|
||||
"packYears": "{value} pacchetti-anno",
|
||||
"packYearsMissing": "Per i pacchetti-anno mancano le sigarette al giorno o l'anno di inizio.",
|
||||
"activityMinutes": "{value} minuti a settimana",
|
||||
"activityMinutes": "{value} minuti a settimana.",
|
||||
"meetsWho": "Raggiunge la raccomandazione dell'OMS di 150 minuti a settimana.",
|
||||
"belowWho": "È sotto la raccomandazione dell'OMS di 150 minuti a settimana.",
|
||||
"measuredSteps": "I tuoi passi misurati hanno la precedenza; questa risposta aggiunge contesto.",
|
||||
|
||||
+3
-2
@@ -6427,7 +6427,8 @@
|
||||
"noEstimate": "AI를 3일 사용하면 단계별 추정치가 표시돼요.",
|
||||
"capped": "{choice} 단계를 선택했지만 지금은 서버에서 허용하지 않아 {level} 단계가 적용돼요. 선택은 그대로 저장돼요.",
|
||||
"followsDefault": "서버 기본값({level})을 따르고 있어요.",
|
||||
"useDefault": "서버 기본값 사용({level})"
|
||||
"useDefault": "서버 기본값 사용({level})",
|
||||
"saveError": "단계를 저장하지 못했어요. 다시 시도해 주세요."
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
@@ -11261,7 +11262,7 @@
|
||||
"derived": {
|
||||
"packYears": "{value}갑년",
|
||||
"packYearsMissing": "갑년을 계산하려면 하루 개비 수와 흡연 시작 연도가 필요해요.",
|
||||
"activityMinutes": "주당 {value}분",
|
||||
"activityMinutes": "주당 {value}분.",
|
||||
"meetsWho": "WHO 권장량인 주당 150분을 채워요.",
|
||||
"belowWho": "WHO 권장량인 주당 150분보다 적어요.",
|
||||
"measuredSteps": "측정된 걸음 수가 우선이고, 이 응답은 맥락을 더해요.",
|
||||
|
||||
+3
-2
@@ -6427,7 +6427,8 @@
|
||||
"noEstimate": "Szacunek dla każdego poziomu pojawi się po trzech dniach korzystania z AI.",
|
||||
"capped": "Wybrano poziom {choice}, na który serwer obecnie nie pozwala, więc obowiązuje {level}. Twój wybór zostaje zapisany.",
|
||||
"followsDefault": "Korzystasz z ustawienia domyślnego serwera ({level}).",
|
||||
"useDefault": "Użyj ustawienia domyślnego serwera ({level})"
|
||||
"useDefault": "Użyj ustawienia domyślnego serwera ({level})",
|
||||
"saveError": "Nie udało się zapisać poziomu. Spróbuj ponownie."
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
@@ -11261,7 +11262,7 @@
|
||||
"derived": {
|
||||
"packYears": "{value} paczkolat",
|
||||
"packYearsMissing": "Do paczkolat brakuje liczby papierosów dziennie albo roku rozpoczęcia.",
|
||||
"activityMinutes": "{value} minut w tygodniu",
|
||||
"activityMinutes": "{value} minut w tygodniu.",
|
||||
"meetsWho": "To spełnia zalecenie WHO 150 minut tygodniowo.",
|
||||
"belowWho": "To poniżej zalecenia WHO 150 minut tygodniowo.",
|
||||
"measuredSteps": "Zmierzone kroki mają pierwszeństwo, ta odpowiedź dodaje kontekst.",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user