mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
Fix settings field rows, defaults and narrow layouts
Module rows drop the field-label colon and inset, the reminder number fields align, target defaults print at their own precision, the appointment offer wraps instead of squeezing to one word per line, the export format legend matches its neighbour label, and the profile-photo explainer reads as body text.
This commit is contained in:
1 parent
2dda933d77
commit
37f922af97
10 files changed
+41
-19
No files matched your search
@@ -113,7 +113,7 @@ describe("change-password card placement", () => {
|
||||
"utf8",
|
||||
);
|
||||
for (const marker of [
|
||||
"settings.passwordReset",
|
||||
"settings.passwordTitle",
|
||||
"settings.changePassword",
|
||||
"/api/auth/password",
|
||||
// A change-password form is the pair current + new. The Account form
|
||||
|
||||
@@ -73,4 +73,13 @@ describe("<ThresholdsEditorSection> glucose unit", () => {
|
||||
expect(html).toContain('value="72"');
|
||||
expect(html).toContain('min="40"');
|
||||
});
|
||||
|
||||
it("prints a whole-number default without a trailing .0", () => {
|
||||
// A bound reads at its own precision: the fixed one decimal printed
|
||||
// "70.0–99.0 mg/dL" and "8,000.0–15,000.0 steps".
|
||||
authUser.current = { glucoseUnit: "mg/dL", unitPreference: "metric" };
|
||||
const html = render();
|
||||
expect(html).toMatch(/70–99 mg\/dL/);
|
||||
expect(html).not.toMatch(/70\.0–99\.0/);
|
||||
});
|
||||
});
|
||||
@@ -114,7 +114,7 @@ export function AvatarSection() {
|
||||
Two strings rather than one merged key: the same wording is the
|
||||
upload's own constraint copy, and a translator keeps the shorter unit
|
||||
either way. */}
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<p className="text-sm">
|
||||
{t("settings.avatar.description")} {t("settings.avatar.hint")}
|
||||
</p>
|
||||
<div className="flex items-center gap-4">
|
||||
@@ -171,7 +171,7 @@ export function AvatarSection() {
|
||||
{msg && (
|
||||
<p
|
||||
role="alert"
|
||||
className={`mt-3 text-sm ${
|
||||
className={`text-sm ${
|
||||
msgType === "success" ? "text-success" : "text-destructive"
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -244,7 +244,7 @@ export function LowStockCard({
|
||||
/>
|
||||
<p className="text-sm">{t("notifications.lowStock.detail")}</p>
|
||||
{enabled && (
|
||||
<div className="flex min-h-11 items-center gap-3">
|
||||
<div className="flex min-h-11 items-center justify-between gap-3">
|
||||
<label htmlFor="low-stock-days" className="text-sm font-medium">
|
||||
{t("notifications.lowStock.daysLabel")}
|
||||
</label>
|
||||
@@ -254,7 +254,7 @@ export function LowStockCard({
|
||||
inputMode="numeric"
|
||||
min={MIN_DAYS}
|
||||
max={MAX_DAYS}
|
||||
className="w-auto"
|
||||
className="w-24 text-right tabular-nums"
|
||||
value={draft ?? String(days)}
|
||||
disabled={!isAuthenticated || saving}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
@@ -275,7 +275,7 @@ export function LowStockCard({
|
||||
own supply tab. */}
|
||||
{enabled && (
|
||||
<div>
|
||||
<div className="flex min-h-11 items-center gap-3">
|
||||
<div className="flex min-h-11 items-center justify-between gap-3">
|
||||
<label htmlFor="low-stock-lead" className="text-sm font-medium">
|
||||
{t("notifications.lowStock.leadLabel")}
|
||||
</label>
|
||||
@@ -285,7 +285,7 @@ export function LowStockCard({
|
||||
inputMode="numeric"
|
||||
min={MIN_LEAD}
|
||||
max={MAX_LEAD}
|
||||
className="w-auto"
|
||||
className="w-24 text-right tabular-nums"
|
||||
value={leadDraft ?? String(leadDays)}
|
||||
disabled={!isAuthenticated || saving}
|
||||
onChange={(e) => setLeadDraft(e.target.value)}
|
||||
|
||||
@@ -98,7 +98,10 @@ export function ModuleToggleRow({
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="min-w-0 space-y-0.5">
|
||||
<Label htmlFor={inputId} className="text-sm font-medium">
|
||||
{/* A row title beside a switch, not a field label: no colon, and
|
||||
on the description's left edge (the primitive's `pl-1` would
|
||||
inset it), matching the admin `SettingsToggle` rows. */}
|
||||
<Label htmlFor={inputId} noColon className="pl-0 text-sm font-medium">
|
||||
{label}
|
||||
</Label>
|
||||
<p className="text-muted-foreground text-xs">{description}</p>
|
||||
|
||||
@@ -203,7 +203,7 @@ export function MoodReminderCard({
|
||||
/>
|
||||
<p className="text-sm">{t("notifications.moodReminder.detail")}</p>
|
||||
{enabled && (
|
||||
<div className="flex min-h-11 items-center gap-3">
|
||||
<div className="flex min-h-11 items-center justify-between gap-3">
|
||||
<label htmlFor="mood-reminder-hour" className="text-sm font-medium">
|
||||
{t("notifications.moodReminder.hourLabel")}
|
||||
</label>
|
||||
@@ -229,8 +229,8 @@ export function MoodReminderCard({
|
||||
aria-live="polite"
|
||||
className={
|
||||
msgType === "error"
|
||||
? "text-destructive mt-3 text-sm"
|
||||
: "text-muted-foreground mt-3 text-sm"
|
||||
? "text-destructive text-sm"
|
||||
: "text-muted-foreground text-sm"
|
||||
}
|
||||
>
|
||||
{msg}
|
||||
|
||||
@@ -96,7 +96,7 @@ export function PasswordCard() {
|
||||
<SettingsCard data-slot="settings-password-card">
|
||||
<SettingsCardHeader
|
||||
icon={Shield}
|
||||
title={t("settings.passwordReset")}
|
||||
title={t("settings.passwordTitle")}
|
||||
description={t("settings.changePasswordDescription")}
|
||||
/>
|
||||
<SettingsCardActions>
|
||||
@@ -122,7 +122,7 @@ export function PasswordCard() {
|
||||
setMsgType(null);
|
||||
}
|
||||
}}
|
||||
title={t("settings.passwordReset")}
|
||||
title={t("settings.passwordTitle")}
|
||||
description={t("settings.changePasswordDescription")}
|
||||
className="sm:max-w-xl"
|
||||
>
|
||||
|
||||
@@ -74,8 +74,11 @@ export function ReportVisitOffer({
|
||||
data-slot="report-visit-offer"
|
||||
className="border-border flex flex-wrap items-center gap-2 rounded-lg border p-3"
|
||||
>
|
||||
{/* Content, not meta: it is the offer, and it names its own source. */}
|
||||
<p className="text-foreground flex min-w-0 flex-1 items-center gap-2 text-sm">
|
||||
{/* Content, not meta: it is the offer, and it names its own source. On
|
||||
a phone the sentence takes the full row and the two buttons wrap
|
||||
below it; with a zero basis it was squeezed into a one-word column
|
||||
beside them. */}
|
||||
<p className="text-foreground flex min-w-0 flex-1 basis-full items-center gap-2 text-sm sm:basis-0">
|
||||
<CalendarClock className="size-4 shrink-0" aria-hidden />
|
||||
<span>
|
||||
{t("settings.healthRecord.visitOffer", {
|
||||
@@ -88,7 +91,7 @@ export function ReportVisitOffer({
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
className="ml-auto min-h-11 sm:ml-0 sm:min-h-9"
|
||||
data-slot="report-visit-offer-apply"
|
||||
onClick={() => onUseVisitDate(localDay(next.occurredAt))}
|
||||
>
|
||||
|
||||
@@ -261,7 +261,7 @@ export function HealthRecordExportPanel() {
|
||||
{/* Format + range share a row on desktop; on mobile they stack. */}
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<fieldset className="space-y-1.5">
|
||||
<legend className="mb-1 text-sm font-medium">
|
||||
<legend className="pl-1 text-sm leading-none font-medium after:content-[':']">
|
||||
{t("settings.healthRecord.format")}
|
||||
</legend>
|
||||
<div className="flex flex-wrap gap-2" role="radiogroup">
|
||||
|
||||
@@ -142,7 +142,7 @@ export function ThresholdsEditorSection({ id }: { id: string }) {
|
||||
at all. The page subtitle is the one that goes. */}
|
||||
<SettingsCardHeader
|
||||
icon={SlidersHorizontal}
|
||||
title={t("settings.sections.thresholds.title")}
|
||||
title={t("thresholds.cardTitle")}
|
||||
/>
|
||||
|
||||
{isLoading || !data ? (
|
||||
@@ -307,6 +307,13 @@ function MetricRow({
|
||||
minNum < maxNum;
|
||||
|
||||
const defaultRange = effective?.default;
|
||||
// A bound reads at its own precision: "120–129 mmHg", "8,000–15,000 steps",
|
||||
// "61.3–82.5 kg". A fixed one decimal printed "120.0–129.0" and
|
||||
// "8,000.0–15,000.0" for metrics that are whole numbers by nature.
|
||||
const formatBound = (value: number) => {
|
||||
const rounded = Math.round(value * 10) / 10;
|
||||
return fmt.number(rounded, Number.isInteger(rounded) ? 0 : 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border-border space-y-3 rounded-lg border p-4">
|
||||
@@ -315,7 +322,7 @@ function MetricRow({
|
||||
<p className="text-sm font-medium">{t(METRIC_LABEL_KEYS[metric])}</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{defaultRange
|
||||
? `${t("thresholds.defaultLabel")}: ${fmt.number(units.toDisplay(defaultRange.greenMin), 1)}–${fmt.number(units.toDisplay(defaultRange.greenMax), 1)} ${bounds.unit}`
|
||||
? `${t("thresholds.defaultLabel")}: ${formatBound(units.toDisplay(defaultRange.greenMin))}–${formatBound(units.toDisplay(defaultRange.greenMax))} ${bounds.unit}`
|
||||
: t("thresholds.unsetExplanation")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user