fix(symptoms): size the icon pickers for touch and name each icon in the reader's language

This commit is contained in:
Marc-André Bombeck committed 2026-10-03 23:10:56 +02:00
1 parent 1eab6145a9
commit 367a1bb4b4
10 files changed
+279 -3

No files matched your search

+22
View File
@@ -11529,6 +11529,28 @@
"nameLabel": "Name",
"namePlaceholder": "z. B. Aura",
"iconLabel": "Symbol",
"icons": {
"Tag": "Etikett",
"Activity": "Aktivität",
"Heart": "Herz",
"HeartPulse": "Herzschlag",
"Brain": "Gehirn",
"Zap": "Blitz",
"Flame": "Flamme",
"Snowflake": "Schneeflocke",
"Droplet": "Tropfen",
"CircleDot": "Punkt",
"BatteryLow": "Leerer Akku",
"MoonStar": "Mond und Stern",
"PersonStanding": "Person",
"Drama": "Masken",
"Frown": "Trauriges Gesicht",
"Cookie": "Keks",
"Soup": "Suppe",
"Pill": "Tablette",
"Thermometer": "Thermometer",
"Stethoscope": "Stethoskop"
},
"limitReached": "Du hast 8 Symptome. Blende eins aus, um ein neues anzulegen.",
"saveError": "Das Symptom konnte nicht gespeichert werden. Bitte versuch es noch einmal.",
"nameRequired": "Gib dem Symptom einen Namen.",
+22
View File
@@ -11529,6 +11529,28 @@
"nameLabel": "Name",
"namePlaceholder": "e.g. Aura",
"iconLabel": "Icon",
"icons": {
"Tag": "Tag",
"Activity": "Activity",
"Heart": "Heart",
"HeartPulse": "Heartbeat",
"Brain": "Brain",
"Zap": "Lightning",
"Flame": "Flame",
"Snowflake": "Snowflake",
"Droplet": "Drop",
"CircleDot": "Dot",
"BatteryLow": "Low battery",
"MoonStar": "Moon and star",
"PersonStanding": "Person",
"Drama": "Masks",
"Frown": "Frowning face",
"Cookie": "Cookie",
"Soup": "Soup",
"Pill": "Pill",
"Thermometer": "Thermometer",
"Stethoscope": "Stethoscope"
},
"limitReached": "You have 8 symptoms. Hide one to add another.",
"saveError": "Couldn't save the symptom. Please try again.",
"nameRequired": "Give the symptom a name.",
+22
View File
@@ -11529,6 +11529,28 @@
"nameLabel": "Nombre",
"namePlaceholder": "p. ej. Aura",
"iconLabel": "Icono",
"icons": {
"Tag": "Etiqueta",
"Activity": "Actividad",
"Heart": "Corazón",
"HeartPulse": "Latido",
"Brain": "Cerebro",
"Zap": "Rayo",
"Flame": "Llama",
"Snowflake": "Copo de nieve",
"Droplet": "Gota",
"CircleDot": "Punto",
"BatteryLow": "Batería baja",
"MoonStar": "Luna y estrella",
"PersonStanding": "Persona",
"Drama": "Máscaras",
"Frown": "Cara triste",
"Cookie": "Galleta",
"Soup": "Sopa",
"Pill": "Pastilla",
"Thermometer": "Termómetro",
"Stethoscope": "Estetoscopio"
},
"limitReached": "Tienes 8 síntomas. Oculta uno para añadir otro.",
"saveError": "No se pudo guardar el síntoma. Inténtalo de nuevo.",
"nameRequired": "Ponle un nombre al síntoma.",
+22
View File
@@ -11529,6 +11529,28 @@
"nameLabel": "Nom",
"namePlaceholder": "p. ex. Aura",
"iconLabel": "Icône",
"icons": {
"Tag": "Étiquette",
"Activity": "Activité",
"Heart": "Cœur",
"HeartPulse": "Battement de cœur",
"Brain": "Cerveau",
"Zap": "Éclair",
"Flame": "Flamme",
"Snowflake": "Flocon de neige",
"Droplet": "Goutte",
"CircleDot": "Point",
"BatteryLow": "Batterie faible",
"MoonStar": "Lune et étoile",
"PersonStanding": "Personne",
"Drama": "Masques",
"Frown": "Visage triste",
"Cookie": "Biscuit",
"Soup": "Soupe",
"Pill": "Comprimé",
"Thermometer": "Thermomètre",
"Stethoscope": "Stéthoscope"
},
"limitReached": "Tu as 8 symptômes. Masques-en un pour en ajouter un autre.",
"saveError": "Impossible d'enregistrer le symptôme. Réessaie.",
"nameRequired": "Donne un nom au symptôme.",
+22
View File
@@ -11529,6 +11529,28 @@
"nameLabel": "Nome",
"namePlaceholder": "es. Aura",
"iconLabel": "Icona",
"icons": {
"Tag": "Etichetta",
"Activity": "Attività",
"Heart": "Cuore",
"HeartPulse": "Battito",
"Brain": "Cervello",
"Zap": "Fulmine",
"Flame": "Fiamma",
"Snowflake": "Fiocco di neve",
"Droplet": "Goccia",
"CircleDot": "Punto",
"BatteryLow": "Batteria scarica",
"MoonStar": "Luna e stella",
"PersonStanding": "Persona",
"Drama": "Maschere",
"Frown": "Viso triste",
"Cookie": "Biscotto",
"Soup": "Zuppa",
"Pill": "Pillola",
"Thermometer": "Termometro",
"Stethoscope": "Stetoscopio"
},
"limitReached": "Hai 8 sintomi. Nascondine uno per aggiungerne un altro.",
"saveError": "Impossibile salvare il sintomo. Riprova.",
"nameRequired": "Dai un nome al sintomo.",
+22
View File
@@ -11529,6 +11529,28 @@
"nameLabel": "이름",
"namePlaceholder": "예: 전조",
"iconLabel": "아이콘",
"icons": {
"Tag": "태그",
"Activity": "활동",
"Heart": "하트",
"HeartPulse": "심장 박동",
"Brain": "뇌",
"Zap": "번개",
"Flame": "불꽃",
"Snowflake": "눈송이",
"Droplet": "물방울",
"CircleDot": "점",
"BatteryLow": "배터리 부족",
"MoonStar": "달과 별",
"PersonStanding": "사람",
"Drama": "가면",
"Frown": "찡그린 얼굴",
"Cookie": "쿠키",
"Soup": "수프",
"Pill": "알약",
"Thermometer": "체온계",
"Stethoscope": "청진기"
},
"limitReached": "증상이 8개입니다. 하나를 숨기면 새로 추가할 수 있습니다.",
"saveError": "증상을 저장하지 못했습니다. 다시 시도하세요.",
"nameRequired": "증상 이름을 입력하세요.",
+22
View File
@@ -11529,6 +11529,28 @@
"nameLabel": "Nazwa",
"namePlaceholder": "np. Aura",
"iconLabel": "Ikona",
"icons": {
"Tag": "Etykieta",
"Activity": "Aktywność",
"Heart": "Serce",
"HeartPulse": "Bicie serca",
"Brain": "Mózg",
"Zap": "Błyskawica",
"Flame": "Płomień",
"Snowflake": "Płatek śniegu",
"Droplet": "Kropla",
"CircleDot": "Kropka",
"BatteryLow": "Słaba bateria",
"MoonStar": "Księżyc i gwiazda",
"PersonStanding": "Osoba",
"Drama": "Maski",
"Frown": "Smutna twarz",
"Cookie": "Ciastko",
"Soup": "Zupa",
"Pill": "Tabletka",
"Thermometer": "Termometr",
"Stethoscope": "Stetoskop"
},
"limitReached": "Masz 8 objawów. Ukryj jeden, aby dodać kolejny.",
"saveError": "Nie udało się zapisać objawu. Spróbuj ponownie.",
"nameRequired": "Nadaj objawowi nazwę.",
@@ -0,0 +1,122 @@
/**
* Both symptom icon pickers (the add chip's popover and the edit sheet) share
* one shape: 44 px targets on a phone, 36 px from `sm`, and a translated
* accessible name per icon rather than the raw Lucide component name.
*
* The popover and the sheet portal, and portals do not render server-side,
* so both are replaced by pass-through wrappers that render their children
* inline; what is measured is the picker inside them.
*
* Mutation checks (each run, each seen red):
* - put `size-7` back on the add chip's icon buttons → "the add chip's
* icon targets" goes red;
* - put `aria-label={name}` back on either picker → the label assertions
* go red ("HeartPulse" is a raw name, "Heartbeat" its English label).
*/
import { describe, expect, it, vi } from "vitest";
import type { ReactNode } from "react";
import { renderToStaticMarkup } from "react-dom/server";
vi.mock("@/components/ui/popover", () => ({
Popover: ({ children }: { children: ReactNode }) => <>{children}</>,
PopoverTrigger: ({ children }: { children: ReactNode }) => <>{children}</>,
PopoverContent: ({ children }: { children: ReactNode }) => (
<div>{children}</div>
),
}));
vi.mock("@/components/ui/responsive-sheet", () => ({
ResponsiveSheet: ({
children,
footer,
}: {
children: ReactNode;
footer?: ReactNode;
}) => (
<div>
{children}
{footer}
</div>
),
}));
vi.mock("../use-symptoms", () => {
const mutation = {
mutateAsync: vi.fn(),
reset: vi.fn(),
isPending: false,
isError: false,
error: null,
};
return {
useCreateSymptomDefinition: () => mutation,
useUpdateSymptomDefinition: () => mutation,
useUpdateSymptomEvent: () => mutation,
};
});
import { I18nProvider } from "@/lib/i18n/context";
import { SYMPTOM_ICON_ALLOWLIST } from "@/lib/symptoms/shared";
import { AddSymptomChip } from "../add-symptom-chip";
import { EditSymptomDefinitionSheet } from "../symptom-edit-sheets";
function iconButtons(html: string): string[] {
return html.match(/<button[^>]*role="radio"[^>]*>/g) ?? [];
}
function assertPicker(html: string) {
const buttons = iconButtons(html);
expect(buttons).toHaveLength(SYMPTOM_ICON_ALLOWLIST.length);
for (const button of buttons) {
expect(button).toContain("size-11");
expect(button).toContain("sm:size-9");
expect(button).not.toMatch(/\bsize-7\b/);
}
expect(html).toContain('aria-label="Heartbeat"');
expect(html).toContain('aria-label="Low battery"');
expect(html).not.toContain('aria-label="HeartPulse"');
expect(html).not.toContain('aria-label="BatteryLow"');
}
describe("symptom icon pickers", () => {
it("the add chip's icon targets are 44 px on a phone, with translated names", () => {
const html = renderToStaticMarkup(
<I18nProvider initialLocale="en">
<AddSymptomChip onCreated={() => {}} />
</I18nProvider>,
);
assertPicker(html);
});
it("the edit sheet's picker matches, with translated names", () => {
const html = renderToStaticMarkup(
<I18nProvider initialLocale="en">
<EditSymptomDefinitionSheet
definition={{
id: "d1",
label: "Aura",
icon: "Zap",
sortOrder: 0,
isActive: true,
recent: {
count30d: 0,
maxIntensity30d: null,
lastOccurredAt: null,
},
}}
onClose={() => {}}
/>
</I18nProvider>,
);
assertPicker(html);
});
it("every allowlisted icon has a label in the English bundle", async () => {
const en = (await import("../../../../messages/en.json")).default as {
symptoms: { icons: Record<string, string> };
};
for (const name of SYMPTOM_ICON_ALLOWLIST) {
expect(en.symptoms.icons[name], name).toBeTruthy();
}
});
});
+2 -2
View File
@@ -115,10 +115,10 @@ export function AddSymptomChip({
type="button"
role="radio"
aria-checked={selected}
aria-label={name}
aria-label={t(`symptoms.icons.${name}`)}
onClick={() => setIcon(name)}
className={cn(
"focus-visible:ring-ring/50 grid size-7 place-items-center rounded-md border transition-colors focus-visible:ring-2 focus-visible:outline-none",
"focus-visible:ring-ring/50 grid size-11 place-items-center rounded-md border transition-colors focus-visible:ring-2 focus-visible:outline-none sm:size-9",
selected
? "border-primary bg-primary/10 text-primary"
: "border-border text-muted-foreground hover:bg-accent",
@@ -241,7 +241,7 @@ export function EditSymptomDefinitionSheet({
type="button"
role="radio"
aria-checked={selected}
aria-label={name}
aria-label={t(`symptoms.icons.${name}`)}
onClick={() => setIcon(name)}
className={cn(
"focus-visible:ring-ring/50 grid size-11 place-items-center rounded-md border transition-colors focus-visible:ring-2 focus-visible:outline-none sm:size-9",