Fix copy, colour and frame details across the record pages

- Achievements: '1 Tag', not '1 Tage'; point totals grouped; whole
  descriptions instead of a clamp that hid the target at the end.
- Illness: the episode note label is translated, day notes read in the
  content colour.
- Coach history: conversation titles read in the content colour.
- Anamnesis and mental wellbeing: drop a second page gutter the shell
  already provides, and the nested main landmark.
- Privacy: repair words broken by a stray hyphen and space.
- Cycle: the ring caption reads 'Tag 9 deines Zyklus' as one phrase.
- Practitioners: the add button says 'Hinzufügen' like every other page;
  a shorter search placeholder fits a phone.
- Empty state cards inset their content from their border, so a phone's
  full-width action no longer runs into the frame.
This commit is contained in:
Marc-André Bombeck committed 2026-10-08 16:34:54 +02:00
1 parent 9fd43623dd
commit 3e1f630fa1
17 files changed
+133 -82

No files matched your search

+5 -4
View File
@@ -8015,7 +8015,9 @@
"noneUnlockedYet": "Noch keine Erfolge freigeschaltet.",
"pointsValue": "{points} Punkte",
"metricCount": "{count}",
"metricDays": "{count} Tage",
"metricDaysOne": "{count} Tag",
"metricDaysFew": "{count} Tage",
"metricDaysOther": "{count} Tage",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -9929,7 +9931,7 @@
"loadError": "Diese Episode konnte nicht geladen werden.",
"backToList": "Zurück zum Journal",
"detail": {
"note": "Note"
"note": "Notiz"
},
"edit": {
"title": "Episode bearbeiten",
@@ -12019,8 +12021,7 @@
"title": "Praxen und Ärzte",
"description": "Dein eigenes Adressbuch — einmal tippen, überall auswählen.",
"linkLabel": "Adressbuch",
"add": "Eintrag",
"searchPlaceholder": "Name, Praxis oder Fachrichtung suchen",
"searchPlaceholder": "Name, Praxis oder Fach suchen",
"loadError": "Adressbuch konnte nicht geladen werden.",
"emptyTitle": "Noch kein Eintrag",
"emptyDescription": "Lege eine Praxis an, damit du sie beim Termin auswählen kannst.",
+3 -2
View File
@@ -8015,7 +8015,9 @@
"noneUnlockedYet": "No achievements unlocked yet.",
"pointsValue": "{points} points",
"metricCount": "{count}",
"metricDays": "{count} days",
"metricDaysOne": "{count} day",
"metricDaysFew": "{count} days",
"metricDaysOther": "{count} days",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -12019,7 +12021,6 @@
"title": "Practices and doctors",
"description": "Your own address book — type a practice once, pick it everywhere.",
"linkLabel": "Address book",
"add": "Entry",
"searchPlaceholder": "Search name, practice or specialty",
"loadError": "The address book could not be loaded.",
"emptyTitle": "No entry yet",
+4 -3
View File
@@ -8015,7 +8015,9 @@
"noneUnlockedYet": "Aún no has desbloqueado ningún logro.",
"pointsValue": "{points} puntos",
"metricCount": "{count}",
"metricDays": "{count} días",
"metricDaysOne": "{count} día",
"metricDaysFew": "{count} días",
"metricDaysOther": "{count} días",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -9929,7 +9931,7 @@
"loadError": "No se pudo cargar este episodio.",
"backToList": "Volver al diario",
"detail": {
"note": "Note"
"note": "Nota"
},
"edit": {
"title": "Editar episodio",
@@ -12019,7 +12021,6 @@
"title": "Consultas y médicos",
"description": "Tu propia agenda: escribe una consulta una vez y elígela en todas partes.",
"linkLabel": "Agenda",
"add": "Entrada",
"searchPlaceholder": "Buscar nombre, consulta o especialidad",
"loadError": "No se pudo cargar la agenda.",
"emptyTitle": "Todavía no hay entradas",
+3 -2
View File
@@ -8015,7 +8015,9 @@
"noneUnlockedYet": "Aucune réussite débloquée pour l’instant.",
"pointsValue": "{points} points",
"metricCount": "{count}",
"metricDays": "{count} jours",
"metricDaysOne": "{count} jour",
"metricDaysFew": "{count} jours",
"metricDaysOther": "{count} jours",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -12019,7 +12021,6 @@
"title": "Cabinets et médecins",
"description": "Votre carnet d'adresses : saisissez un cabinet une fois, choisissez-le partout.",
"linkLabel": "Carnet d'adresses",
"add": "Entrée",
"searchPlaceholder": "Rechercher un nom, un cabinet ou une spécialité",
"loadError": "Impossible de charger le carnet d'adresses.",
"emptyTitle": "Aucune entrée",
+4 -3
View File
@@ -8015,7 +8015,9 @@
"noneUnlockedYet": "Nessun obiettivo ancora sbloccato.",
"pointsValue": "{points} punti",
"metricCount": "{count}",
"metricDays": "{count} giorni",
"metricDaysOne": "{count} giorno",
"metricDaysFew": "{count} giorni",
"metricDaysOther": "{count} giorni",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -9929,7 +9931,7 @@
"loadError": "Impossibile caricare questo episodio.",
"backToList": "Torna al diario",
"detail": {
"note": "Note"
"note": "Nota"
},
"edit": {
"title": "Modifica episodio",
@@ -12019,7 +12021,6 @@
"title": "Studi e medici",
"description": "La tua rubrica: scrivi uno studio una volta e selezionalo ovunque.",
"linkLabel": "Rubrica",
"add": "Voce",
"searchPlaceholder": "Cerca nome, studio o specializzazione",
"loadError": "Impossibile caricare la rubrica.",
"emptyTitle": "Nessuna voce",
+3 -2
View File
@@ -8015,7 +8015,9 @@
"noneUnlockedYet": "아직 달성한 업적이 없어요.",
"pointsValue": "{points}포인트",
"metricCount": "{count}",
"metricDays": "{count}일",
"metricDaysOne": "{count}일",
"metricDaysFew": "{count}일",
"metricDaysOther": "{count}일",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -12019,7 +12021,6 @@
"title": "병원과 의사",
"description": "나만의 주소록이에요 — 병원을 한 번만 입력하면 어디서든 골라 쓸 수 있어요.",
"linkLabel": "주소록",
"add": "항목",
"searchPlaceholder": "이름, 병원, 진료과 검색",
"loadError": "주소록을 불러오지 못했어요.",
"emptyTitle": "아직 항목이 없어요",
+4 -3
View File
@@ -8015,7 +8015,9 @@
"noneUnlockedYet": "Nie odblokowano jeszcze żadnego osiągnięcia.",
"pointsValue": "{points} punktów",
"metricCount": "{count}",
"metricDays": "{count} dni",
"metricDaysOne": "{count} dzień",
"metricDaysFew": "{count} dni",
"metricDaysOther": "{count} dni",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -9929,7 +9931,7 @@
"loadError": "Nie udało się wczytać tego epizodu.",
"backToList": "Powrót do dziennika",
"detail": {
"note": "Note"
"note": "Notatka"
},
"edit": {
"title": "Edytuj epizod",
@@ -12019,7 +12021,6 @@
"title": "Gabinety i lekarze",
"description": "Twoja własna książka adresowa — wpisz gabinet raz, wybieraj go wszędzie.",
"linkLabel": "Książka adresowa",
"add": "Wpis",
"searchPlaceholder": "Szukaj nazwiska, gabinetu lub specjalizacji",
"loadError": "Nie udało się wczytać książki adresowej.",
"emptyTitle": "Brak wpisów",
+39 -17
View File
@@ -35,7 +35,7 @@ import { PageHeader } from "@/components/ui/page-header";
import { PageAuthGate } from "@/components/ui/page-auth-gate";
import { QueryErrorCard } from "@/components/ui/query-error-card";
import { useAuth } from "@/hooks/use-auth";
import { useTranslations } from "@/lib/i18n/context";
import { useFormatters, useTranslations } from "@/lib/i18n/context";
import { formatDate } from "@/lib/format";
import { useAchievementsQuery } from "@/lib/queries/use-achievements-query";
import {
@@ -81,12 +81,13 @@ const CATEGORY_LABEL_KEY: Record<AchievementCategory, string> = {
function formatMetric(
format: AchievementProgress["format"],
value: number,
t: (key: string, params?: Record<string, string | number>) => string,
i18n: Pick<ReturnType<typeof useTranslations>, "t" | "tCount">,
) {
if (format === "percent")
return t("achievements.metricPercent", { count: value });
if (format === "days") return t("achievements.metricDays", { count: value });
return t("achievements.metricCount", { count: value });
return i18n.t("achievements.metricPercent", { count: value });
// "1 Tag", not "1 Tage": the day count takes the plural form.
if (format === "days") return i18n.tCount("achievements.metricDays", value);
return i18n.t("achievements.metricCount", { count: value });
}
/**
@@ -127,9 +128,11 @@ export function groupByCategory(
interface AchievementCardProps {
achievement: AchievementProgress;
t: ReturnType<typeof useTranslations>["t"];
tCount: ReturnType<typeof useTranslations>["tCount"];
}
function AchievementCard({ achievement, t }: AchievementCardProps) {
function AchievementCard({ achievement, t, tCount }: AchievementCardProps) {
const i18n = { t, tCount };
const Icon = iconMap[achievement.icon] ?? Star;
const unlocked = achievement.unlocked;
@@ -191,7 +194,7 @@ function AchievementCard({ achievement, t }: AchievementCardProps) {
</div>
<div className="min-w-0">
<h3 className="text-sm font-semibold">{t(achievement.titleKey)}</h3>
<p className="text-muted-foreground line-clamp-2 text-xs">
<p className="text-muted-foreground text-xs">
{t(achievement.descriptionKey)}
</p>
</div>
@@ -215,8 +218,16 @@ function AchievementCard({ achievement, t }: AchievementCardProps) {
<div className="mt-2 space-y-1.5">
<p className="text-muted-foreground text-xs">
{t("achievements.criterionHint", {
current: formatMetric(achievement.format, achievement.current, t),
target: formatMetric(achievement.format, achievement.target, t),
current: formatMetric(
achievement.format,
achievement.current,
i18n,
),
target: formatMetric(
achievement.format,
achievement.target,
i18n,
),
})}
</p>
{/* v1.4.33 IW9 — aria-label so the bar carries an accessible name. */}
@@ -224,8 +235,16 @@ function AchievementCard({ achievement, t }: AchievementCardProps) {
value={achievement.progressPercent}
className="h-1.5"
aria-label={t("achievements.criterionHint", {
current: formatMetric(achievement.format, achievement.current, t),
target: formatMetric(achievement.format, achievement.target, t),
current: formatMetric(
achievement.format,
achievement.current,
i18n,
),
target: formatMetric(
achievement.format,
achievement.target,
i18n,
),
})}
/>
</div>
@@ -259,7 +278,9 @@ function AchievementCard({ achievement, t }: AchievementCardProps) {
export default function AchievementsPage() {
const { user, isAuthenticated, isLoading: authLoading } = useAuth();
const { t } = useTranslations();
const { t, tCount } = useTranslations();
const fmt = useFormatters();
const i18n = { t, tCount };
// v1.18.0 — the achievements module gate. When the account has the
// module turned off the whole page disappears: an empty state stands in
@@ -335,9 +356,9 @@ export default function AchievementsPage() {
{t("achievements.points")}
</p>
<p className="text-4xl leading-none font-bold md:text-5xl">
{summary?.earnedPoints ?? 0}
{fmt.integer(summary?.earnedPoints ?? 0)}
<span className="text-muted-foreground ml-2 text-base font-medium">
/ {summary?.totalPoints ?? 0}
/ {fmt.integer(summary?.totalPoints ?? 0)}
</span>
</p>
<Progress
@@ -373,7 +394,7 @@ export default function AchievementsPage() {
<p className="text-base font-semibold">
{t(summary.nextAchievement.titleKey)}
</p>
<p className="text-muted-foreground line-clamp-2 text-xs">
<p className="text-muted-foreground text-xs">
{t(summary.nextAchievement.descriptionKey)}
</p>
<p className="text-muted-foreground text-xs leading-relaxed">
@@ -381,13 +402,13 @@ export default function AchievementsPage() {
{formatMetric(
summary.nextAchievement.format,
summary.nextAchievement.current,
t,
i18n,
)}{" "}
/{" "}
{formatMetric(
summary.nextAchievement.format,
summary.nextAchievement.target,
t,
i18n,
)}{" "}
({summary.nextAchievement.progressPercent}%)
</p>
@@ -449,6 +470,7 @@ export default function AchievementsPage() {
key={achievement.id}
achievement={achievement}
t={t}
tCount={tCount}
/>
))}
</div>
+3 -1
View File
@@ -219,7 +219,9 @@ function CoachConversationsBody({ readOnly }: { readOnly: boolean }) {
className={cn(
"group relative flex flex-wrap items-center gap-1.5 rounded-xl px-3 py-2.5",
"text-sm transition-colors",
"text-muted-foreground hover:bg-muted/40 hover:text-foreground",
// The title is the reader's own content: foreground,
// with the timestamp below as the muted meta line.
"text-foreground hover:bg-muted/40",
)}
>
<button
+2 -3
View File
@@ -44,8 +44,7 @@ export default function MentalWellbeingPage() {
}
return (
<div className="p-4 sm:p-6">
<MentalWellbeing />
</div>
// The shell owns the page gutter; a second one here doubled it.
<MentalWellbeing />
);
}
+36 -37
View File
@@ -386,8 +386,9 @@ export default function PrivacyPage() {
bodyEn={
<>
<p>
HealthLog is an open-source, self-hostable personal-health-
tracking application. Source code is published at{" "}
HealthLog is an open-source, self-hostable
personal-health-tracking application. Source code is published
at{" "}
<a
href="https://github.com/MBombeck/HealthLog"
target="_blank"
@@ -406,7 +407,7 @@ export default function PrivacyPage() {
</p>
<p>
The controller under Art. 4 (7) GDPR is the operator of this
instance (an individual based in Germany). Self- hosted
instance (an individual based in Germany). Self-hosted
deployments controlled by a different operator are governed by
that operator&apos;s own privacy policy; this document applies
only to this instance.
@@ -454,7 +455,7 @@ export default function PrivacyPage() {
</li>
<li>
Profil-Metadaten: Sprache, Zeitzone, Geburtsdatum (optional),
biologisches Geschlecht (optional), Körper- größe (optional) —
biologisches Geschlecht (optional), Körpergröße (optional) —
zur Berechnung altersgerechter Zielbereiche und des BMI.
</li>
</ul>
@@ -462,7 +463,7 @@ export default function PrivacyPage() {
2.2 Manuell erfasste Gesundheitsdaten
</h3>
<p>
Jeder in den App-Formularen eingegebene Wert, einschließ- lich
Jeder in den App-Formularen eingegebene Wert, einschließlich
Zeitstempel, optionaler Notiz und Metrik-Typ. Die vollständige
Aufzählung der unterstützten Metriken steht im OpenAPI-Schema;
die nutzerrelevante Teilmenge umfasst Körpergewicht, Körperfett,
@@ -587,9 +588,9 @@ export default function PrivacyPage() {
</ul>
<p>
Schreibzugriff wird für eine Teilmenge angefordert (Körpermasse,
Blutdruck systolisch / diastolisch, Blut- zucker), damit
manuelle Einträge in der iOS-App zurück nach HealthKit fließen
können. Der Nutzer steuert beide Richtungen über die
Blutdruck systolisch / diastolisch, Blutzucker), damit manuelle
Einträge in der iOS-App zurück nach HealthKit fließen können.
Der Nutzer steuert beide Richtungen über die
Berechtigungsoberfläche der iOS- Health-App und kann jederzeit
widerrufen.
</p>
@@ -598,25 +599,24 @@ export default function PrivacyPage() {
</h3>
<p>
Verbindet der Nutzer ein Withings-Konto, speichert HealthLog die
OAuth-Refresh- und -Access-Token (spalten- weise verschlüsselt)
OAuth-Refresh- und -Access-Token (spaltenweise verschlüsselt)
sowie die Withings-Nutzerkennung. Webhook-getriebene
Folgesynchronisierungen ziehen Körper- gewicht, Fettanteil,
Folgesynchronisierungen ziehen Körpergewicht, Fettanteil,
fettfreie Masse, Muskelmasse, Knochenmasse, Wassergehalt,
Magermasse, Hydration, Blutdruck (systolisch / diastolisch) und
Puls, Blut- zucker, Körpertemperatur (Basal- und
Hauttemperatur), Aktivitätssummen (Schritte, Distanz, aktive
Kalorien), Schlafsitzungen mit Pro-Stadien-Segmenten (Awake /
Light / Deep / REM) sowie SpO₂ und Herzfrequenzvariabilität,
soweit vorhanden.
Puls, Blutzucker, Körpertemperatur (Basal- und Hauttemperatur),
Aktivitätssummen (Schritte, Distanz, aktive Kalorien),
Schlafsitzungen mit Pro-Stadien-Segmenten (Awake / Light / Deep
/ REM) sowie SpO₂ und Herzfrequenzvariabilität, soweit
vorhanden.
</p>
<h3 className="text-foreground pt-2 text-base font-semibold">
2.5 Medikamente
</h3>
<ul className="list-disc space-y-1 pl-5">
<li>
Aktive Verordnungen: Wirkstoffname, Stärke, Verabreich-
ungsweg, Einnahmeplan, Behandlungsklasse (Standard oder
GLP-1).
Aktive Verordnungen: Wirkstoffname, Stärke, Verabreichungsweg,
Einnahmeplan, Behandlungsklasse (Standard oder GLP-1).
</li>
<li>
Einnahme-Ereignisse: geplante Zeit, tatsächliche Zeit, Status
@@ -716,8 +716,8 @@ export default function PrivacyPage() {
<code className="bg-muted rounded px-1 py-0.5 text-xs">
X-Device-Id
</code>{" "}
-Header — für Mehrgeräte-Synchronisation und Miss-
brauchsschutz.
-Header — für Mehrgeräte-Synchronisation und
Missbrauchsschutz.
</li>
<li>
Apple-Push-Notification-Service-(APNs)-Geräte-Token und
@@ -759,8 +759,8 @@ export default function PrivacyPage() {
</h3>
<ul className="list-disc space-y-1 pl-5">
<li>
Keine Werbe-Identifier, Fingerprints oder anwendungs-
übergreifenden Tracker.
Keine Werbe-Identifier, Fingerprints oder
anwendungsübergreifenden Tracker.
</li>
<li>Keine Zahlungsdaten (es gibt keinen Bezahltarif).</li>
<li>
@@ -938,11 +938,11 @@ export default function PrivacyPage() {
</li>
</ul>
<p>
Write access is requested for a subset (body mass, blood-
pressure systolic / diastolic, blood glucose) so that manual
entries made inside the iOS app can flow back into HealthKit.
The user controls both directions in the iOS Health app&apos;s
permission surface and may revoke at any time.
Write access is requested for a subset (body mass,
blood-pressure systolic / diastolic, blood glucose) so that
manual entries made inside the iOS app can flow back into
HealthKit. The user controls both directions in the iOS Health
app&apos;s permission surface and may revoke at any time.
</p>
<h3 className="text-foreground pt-2 text-base font-semibold">
2.4 Withings sync (optional)
@@ -1057,8 +1057,8 @@ export default function PrivacyPage() {
</h3>
<ul className="list-disc space-y-1 pl-5">
<li>
A device identifier (random UUID generated client- side,
stored in the iOS Keychain) sent as the{" "}
A device identifier (random UUID generated client-side, stored
in the iOS Keychain) sent as the{" "}
<code className="bg-muted rounded px-1 py-0.5 text-xs">
X-Device-Id
</code>{" "}
@@ -1135,16 +1135,15 @@ export default function PrivacyPage() {
Authentifizierungsdaten
</span>{" "}
— Identifikation des Nutzers und Sicherung der Sitzung.
Rechtsgrundlage: Art. 6 Abs. 1 lit. b DSGVO (Vertrags-
erfüllung).
Rechtsgrundlage: Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung).
</li>
<li>
<span className="text-foreground font-medium">
Gesundheitsdaten
</span>{" "}
— Darstellung von Verläufen, Berechnung der Zielwerte- Treue,
Erzeugung des Coach-Snapshots auf Abruf. Rechts- grundlage: Art.
9 Abs. 2 lit. a DSGVO (ausdrückliche Einwilligung für Daten
Erzeugung des Coach-Snapshots auf Abruf. Rechtsgrundlage: Art. 9
Abs. 2 lit. a DSGVO (ausdrückliche Einwilligung für Daten
besonderer Kategorien).
</li>
<li>
@@ -1420,8 +1419,8 @@ export default function PrivacyPage() {
<span className="text-foreground font-medium">
Kein HealthLog-iCloud-Speicher
</span>{" "}
— HealthLog speichert Server-Datenbank, lokale Gesundheits-
caches, Offline-Outbox, ECG- und Workout-Payloads,
— HealthLog speichert Server-Datenbank, lokale
Gesundheits-Caches, Offline-Outbox, ECG- und Workout-Payloads,
KI-Transkript-Cache, Medikamentenplaner-Datenbank sowie Widget-
und Watch-Snapshots weder in iCloud noch in CloudKit. Lokale
gesundheitsbezogene Speicher liegen in geschütztem App- oder
@@ -1587,10 +1586,10 @@ export default function PrivacyPage() {
<span className="text-foreground font-medium">
Widerspruch und automatisierte Entscheidungen (Art. 21–22)
</span>{" "}
— der KI-Coach trifft keine automatisierten Entscheid- ungen im
— der KI-Coach trifft keine automatisierten Entscheidungen im
Sinne von Art. 22 DSGVO. Er erzeugt Texte zum Nachlesen; ohne
ausdrückliche Bestätigung wird keine Aktion ausgeführt. Siehe
Abschnitt 7 zur Medizinpro- dukte-Grenze.
Abschnitt 7 zur Medizinprodukte-Grenze.
</li>
<li>
<span className="text-foreground font-medium">
+8 -1
View File
@@ -271,7 +271,14 @@ export function CycleView() {
days: verdict.overdueDays,
})
: t("cycle.ring.periodOverdue")
: t("cycle.ring.caption")}
: // The ring shows the bare number; the caption under it
// reads as a whole phrase ("Tag 9 deines Zyklus"), not
// a fragment hanging off the ring.
verdict?.dayOfCycle != null
? t("cycle.insightsSummary.currentDay", {
day: verdict.dayOfCycle,
})
: t("cycle.ring.caption")}
</p>
) : null}
{/* Log CTA — first period when no cycle exists yet, next period
@@ -86,7 +86,8 @@ function DayLogRow({ log }: { log: IllnessDayLogDTO }) {
) : null}
{log.note ? (
<p className="text-muted-foreground text-sm whitespace-pre-wrap">
// User-written text reads in the content colour, never muted.
<p className="text-foreground text-sm whitespace-pre-wrap">
{log.note}
</p>
) : null}
@@ -98,7 +98,7 @@ export function PractitionerList({ enabled = true }: { enabled?: boolean }) {
onClick={() => openSheet("new")}
>
<Plus className="size-4" aria-hidden />
{t("practitioners.add")}
{t("common.add")}
</Button>
) : null;
+4 -2
View File
@@ -130,7 +130,9 @@ export function ProfileSummaryPage() {
});
return (
<main className="container mx-auto max-w-3xl space-y-6 px-4 py-6">
// The shell owns the page frame (gutter, width, top space) and the
// <main> landmark; this page only sets its rhythm.
<div className="space-y-6">
<PageHeader title={t("settings.sections.anamnesis.title")} />
{summary.isPending ? (
<p className="text-muted-foreground text-sm" aria-live="polite">
@@ -141,6 +143,6 @@ export function ProfileSummaryPage() {
<QueryErrorCard onRetry={() => void summary.refetch()} />
) : null}
{summary.data ? <ProfileSummary summary={summary.data} /> : null}
</main>
</div>
);
}
@@ -46,4 +46,13 @@ describe("<EmptyState>", () => {
expect(card).toContain("border-dashed");
expect(plain).not.toContain("border-dashed");
});
it("insets the card variant's content from its border, not the plain one", () => {
const card = renderToStaticMarkup(<EmptyState title="leer" />);
const plain = renderToStaticMarkup(
<EmptyState title="leer" variant="plain" />,
);
expect(card).toMatch(/class="[^"]*\bpx-4\b/);
expect(plain).not.toMatch(/\bpx-4\b/);
});
});
+3
View File
@@ -64,6 +64,9 @@ function EmptyState({
className={cn(
"flex flex-col items-center justify-center text-center",
size === "compact" ? "gap-2 py-4" : "gap-3 py-8",
// The card variant paints a border, so its content insets from it:
// a full-width phone CTA otherwise ran edge to edge into the frame.
variant === "card" && "px-4",
)}
>
{icon ? (