diff --git a/messages/de.json b/messages/de.json index 430d15ebc..4026a4aa4 100644 --- a/messages/de.json +++ b/messages/de.json @@ -834,7 +834,7 @@ "title": "Medikament beschreiben", "description": "Beschreibe das Medikament in eigenen Worten — das Formular wird automatisch vorausgefüllt.", "placeholder": "z. B. Mounjaro 5 mg wöchentlich am Mittwochmorgen ab nächstem Montag", - "submit": "Übernehmen", + "submit": "Extrahieren", "submitBusy": "Wird verarbeitet …", "cancel": "Abbrechen", "examples": { @@ -2099,6 +2099,8 @@ "navSteps": "Schritte", "navActiveEnergy": "Aktive Energie", "navAriaLabel": "Zu Abschnitt springen", + "pillScrollLeft": "Tabs nach links scrollen", + "pillScrollRight": "Tabs nach rechts scrollen", "tabStrip": { "vitalsParent": { "label": "Vitalwerte", diff --git a/messages/en.json b/messages/en.json index aac9080a9..ac85b895c 100644 --- a/messages/en.json +++ b/messages/en.json @@ -2099,6 +2099,8 @@ "navSteps": "Steps", "navActiveEnergy": "Active energy", "navAriaLabel": "Skip to section", + "pillScrollLeft": "Scroll tabs left", + "pillScrollRight": "Scroll tabs right", "tabStrip": { "vitalsParent": { "label": "Vitals", diff --git a/messages/es.json b/messages/es.json index b37cd76c0..07da460d5 100644 --- a/messages/es.json +++ b/messages/es.json @@ -2099,6 +2099,8 @@ "navSteps": "Pasos", "navActiveEnergy": "Energía activa", "navAriaLabel": "Saltar a sección", + "pillScrollLeft": "Desplazar las pestañas a la izquierda", + "pillScrollRight": "Desplazar las pestañas a la derecha", "tabStrip": { "vitalsParent": { "label": "Vitales", diff --git a/messages/fr.json b/messages/fr.json index 3399e0725..d6020b9c1 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -2099,6 +2099,8 @@ "navSteps": "Pas", "navActiveEnergy": "Énergie active", "navAriaLabel": "Aller à la section", + "pillScrollLeft": "Faire défiler les onglets vers la gauche", + "pillScrollRight": "Faire défiler les onglets vers la droite", "tabStrip": { "vitalsParent": { "label": "Constantes", diff --git a/messages/it.json b/messages/it.json index b56733170..72394ccb9 100644 --- a/messages/it.json +++ b/messages/it.json @@ -2099,6 +2099,8 @@ "navSteps": "Passi", "navActiveEnergy": "Energia attiva", "navAriaLabel": "Vai alla sezione", + "pillScrollLeft": "Scorri le schede a sinistra", + "pillScrollRight": "Scorri le schede a destra", "tabStrip": { "vitalsParent": { "label": "Vitali", diff --git a/messages/pl.json b/messages/pl.json index 71c5b8d23..632887aa3 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -2099,6 +2099,8 @@ "navSteps": "Kroki", "navActiveEnergy": "Energia czynna", "navAriaLabel": "Przejdź do sekcji", + "pillScrollLeft": "Przewiń karty w lewo", + "pillScrollRight": "Przewiń karty w prawo", "tabStrip": { "vitalsParent": { "label": "Witalne", diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 000000000..b71aacf83 Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/logo-maskable-512.png b/public/logo-maskable-512.png new file mode 100644 index 000000000..f192bd598 Binary files /dev/null and b/public/logo-maskable-512.png differ diff --git a/public/manifest.json b/public/manifest.json index 8c3882279..d351c6fbb 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -13,12 +13,20 @@ { "src": "/logo-192.png", "sizes": "192x192", - "type": "image/png" + "type": "image/png", + "purpose": "any" }, { "src": "/logo-512.png", "sizes": "512x512", - "type": "image/png" + "type": "image/png", + "purpose": "any" + }, + { + "src": "/logo-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" } ] } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 422457f1e..32757cd95 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -39,7 +39,10 @@ export const metadata: Metadata = { manifest: "/manifest.json", icons: { icon: "/favicon.svg", - apple: "/logo-192.png", + // Pre-flattened 180×180 on the app background — Safari ignores PNG + // alpha in favourites/home-screen tiles and would render the + // transparent logo on a white slab otherwise. + apple: "/apple-touch-icon.png", }, appleWebApp: { capable: true, diff --git a/src/components/admin/backups-section.tsx b/src/components/admin/backups-section.tsx index 9c82204e9..d60abcf4e 100644 --- a/src/components/admin/backups-section.tsx +++ b/src/components/admin/backups-section.tsx @@ -127,7 +127,7 @@ function RestoreRowDialog({ {t("common.cancel")} { if (!matched) return; setOpen(false); diff --git a/src/components/admin/danger-zone-section.tsx b/src/components/admin/danger-zone-section.tsx index fcb45722b..4f42d3047 100644 --- a/src/components/admin/danger-zone-section.tsx +++ b/src/components/admin/danger-zone-section.tsx @@ -104,9 +104,14 @@ export function DangerZoneSection() { {t("common.cancel")} wipeAllData.mutate()} > + {wipeAllData.isPending && ( + + )} {t("admin.finalDelete")} diff --git a/src/components/admin/feedback-inbox-section.tsx b/src/components/admin/feedback-inbox-section.tsx index ebf92acf3..31180ed6c 100644 --- a/src/components/admin/feedback-inbox-section.tsx +++ b/src/components/admin/feedback-inbox-section.tsx @@ -398,8 +398,13 @@ function FeedbackDetailDialog({ {t("common.cancel")} archive.mutate()} - className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + variant="destructive" + disabled={archive.isPending} + aria-busy={archive.isPending || undefined} > + {archive.isPending && ( + + )} {t("admin.feedback.actionArchive")} diff --git a/src/components/admin/user-management-section.tsx b/src/components/admin/user-management-section.tsx index cc4f33dd9..38fa8a431 100644 --- a/src/components/admin/user-management-section.tsx +++ b/src/components/admin/user-management-section.tsx @@ -560,7 +560,9 @@ export function UserManagementSection() { forceLogout.mutate({ id: logoutTarget.id }); } }} - className="bg-destructive hover:bg-destructive/90" + variant="destructive" + disabled={forceLogout.isPending} + aria-busy={forceLogout.isPending || undefined} > {forceLogout.isPending ? ( diff --git a/src/components/data-list/delete-button.tsx b/src/components/data-list/delete-button.tsx index dd53016e3..3252d381e 100644 --- a/src/components/data-list/delete-button.tsx +++ b/src/components/data-list/delete-button.tsx @@ -64,7 +64,7 @@ export function DeleteButton({ {cancelLabel ?? t("common.cancel")} {confirmLabel ?? t("common.delete")} diff --git a/src/components/data-list/selection-action-bar.tsx b/src/components/data-list/selection-action-bar.tsx index f7a48ea36..02cb5c5c9 100644 --- a/src/components/data-list/selection-action-bar.tsx +++ b/src/components/data-list/selection-action-bar.tsx @@ -105,7 +105,7 @@ export function SelectionActionBar({ {t("common.cancel")} diff --git a/src/components/insights/derived/score-ring.tsx b/src/components/insights/derived/score-ring.tsx index 7cbd46b4a..83fa5f717 100644 --- a/src/components/insights/derived/score-ring.tsx +++ b/src/components/insights/derived/score-ring.tsx @@ -122,8 +122,7 @@ export function ScoreRing({ const shouldAnimate = animate && hasScore && !reduced; const dash = (clamped / 100) * C; - const hasBaseline = - baseline != null && Number.isFinite(baseline) && hasScore; + const hasBaseline = baseline != null && Number.isFinite(baseline) && hasScore; const baselineDash = hasBaseline ? (clampScore(baseline) / 100) * C : 0; // Arc starts empty (offset C) and sweeps to its fill on the next frame after @@ -170,9 +169,14 @@ export function ScoreRing({ } as React.CSSProperties } > + {/* `overflow-visible`: the arc's drop-shadow bloom (`.wellness-ring-arc`) + paints outside the viewBox — the arc fills the 0 0 100 100 box, so the + default `overflow: hidden` on SVG clipped the glow flat at the circle's + edge. The -90° rotation is unaffected (the bloom is radially symmetric) + and the parent div never clips, so the glow now feathers out fully. */}
-
+ {pillsOverflow && ( + + )} +
{tabs.map((tab) => { if (tab.kind === "link") { // The overview pill matches the mother page exactly; the @@ -631,6 +720,23 @@ function InsightsTabStripImpl({ ); })}
+ {pillsOverflow && ( + + )} {/* v1.4.27 MB7 / CF-72 — right-edge fade. The gradient absolute-positions over the rightmost ~24 px of the strip so the last visible pill softly fades into the background diff --git a/src/components/insights/mood/mood-time-of-day-chart.tsx b/src/components/insights/mood/mood-time-of-day-chart.tsx index 7c2d43d19..e1e8f61bc 100644 --- a/src/components/insights/mood/mood-time-of-day-chart.tsx +++ b/src/components/insights/mood/mood-time-of-day-chart.tsx @@ -25,11 +25,7 @@ import { useTranslations } from "@/lib/i18n/context"; * a muted zero-height bar so the four-column rhythm stays intact. */ -export type MoodTimeOfDayBucket = - | "morning" - | "afternoon" - | "evening" - | "night"; +export type MoodTimeOfDayBucket = "morning" | "afternoon" | "evening" | "night"; export interface MoodTimeOfDayRow { bucket: MoodTimeOfDayBucket; @@ -88,23 +84,28 @@ export function MoodTimeOfDayChart({ data={data} margin={{ top: 8, right: 12, bottom: 8, left: 0 }} > + {/* Muted chart language — same treatment as the weekday/distribution + siblings: `--muted-foreground` axis ticks, a quiet comment-grey + target guide, and 0.55 bar fill so the level hues inform without + shouting. */} - + {data.map((row) => ( diff --git a/src/components/mood/mood-form.tsx b/src/components/mood/mood-form.tsx index ed63cf8f2..e964e3cf1 100644 --- a/src/components/mood/mood-form.tsx +++ b/src/components/mood/mood-form.tsx @@ -498,7 +498,7 @@ export function MoodForm({ onSuccess, onCancel, footerSlot }: MoodFormProps) { {t("common.cancel")} {t("mood.formResetConfirmAction")} diff --git a/src/components/mood/mood-list.tsx b/src/components/mood/mood-list.tsx index cfcb170cd..d62a6e97b 100644 --- a/src/components/mood/mood-list.tsx +++ b/src/components/mood/mood-list.tsx @@ -1018,7 +1018,7 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) { {t("common.cancel")} diff --git a/src/components/settings/account-section.tsx b/src/components/settings/account-section.tsx index 8e546b13d..117dfbc23 100644 --- a/src/components/settings/account-section.tsx +++ b/src/components/settings/account-section.tsx @@ -940,9 +940,14 @@ function PasskeyListSection({ isAuthenticated }: { isAuthenticated: boolean }) { {t("common.cancel")} deletePasskey.mutate(pk.id)} > + {deletePasskey.isPending && ( + + )} {t("common.delete")} @@ -1022,9 +1027,14 @@ function PasskeyListSection({ isAuthenticated }: { isAuthenticated: boolean }) { {t("common.cancel")} deletePasskey.mutate(pk.id)} > + {deletePasskey.isPending && ( + + )} {t("common.delete")} diff --git a/src/components/settings/advanced-section.tsx b/src/components/settings/advanced-section.tsx index e1802e895..8d7340cab 100644 --- a/src/components/settings/advanced-section.tsx +++ b/src/components/settings/advanced-section.tsx @@ -327,7 +327,7 @@ function DataResetCard() { {t("common.cancel")} { e.preventDefault(); void handleDeleteAllData(); @@ -471,7 +471,7 @@ function AccountDeleteCard() { {t("common.cancel")} { e.preventDefault(); void handleDeleteAccount(); diff --git a/src/components/settings/api-section.tsx b/src/components/settings/api-section.tsx index bc7f1835d..fad61ed8d 100644 --- a/src/components/settings/api-section.tsx +++ b/src/components/settings/api-section.tsx @@ -134,13 +134,13 @@ function ApiEndpointsCard() { {endpoint.path}
-

+

{t("settings.apiEndpointAuth")}

{endpoint.auth}
-

+

{t("settings.apiEndpointExample")}

@@ -263,7 +263,10 @@ function ApiTokensCard() { />
-
+ {/* The button mirrors the Input's responsive height (h-11 / sm:h-10) + so the row reads as one aligned control group instead of a tall + input next to a shorter `sm` button. */} + setNewName(e.target.value)} @@ -274,10 +277,12 @@ function ApiTokensCard() {
@@ -416,7 +421,7 @@ function ApiTokensCard() { {t("common.cancel")} handleRevoke(tok.id)} > {t("settings.tokenRevoked")} @@ -515,7 +520,7 @@ function ApiTokensCard() { {t("common.cancel")} handleRevoke(tok.id)} > {t("settings.tokenRevoked")} diff --git a/src/components/settings/coach-memory-section.tsx b/src/components/settings/coach-memory-section.tsx index dde6912bf..77fd4aad8 100644 --- a/src/components/settings/coach-memory-section.tsx +++ b/src/components/settings/coach-memory-section.tsx @@ -197,8 +197,13 @@ export function CoachMemorySection({ forgetAll.mutate()} > + {forgetAll.isPending && ( + + )} {t("settings.ai.coachMemory.forgetAllConfirmAction")} @@ -208,7 +213,11 @@ export function CoachMemorySection({
{query.isError && ( -

+

{t("settings.ai.coachMemory.loadError")}

)} diff --git a/src/components/settings/integrations-section.tsx b/src/components/settings/integrations-section.tsx index afdc9d224..91c9dcf03 100644 --- a/src/components/settings/integrations-section.tsx +++ b/src/components/settings/integrations-section.tsx @@ -622,7 +622,7 @@ function WithingsCard({ {t("common.cancel")} disconnect.mutate()} > {t("settings.withingsDisconnect")} @@ -1035,7 +1035,7 @@ function WhoopCard({ {t("common.cancel")} disconnect.mutate()} > {t("settings.whoopDisconnect")} @@ -1481,7 +1481,7 @@ function FitbitCard({ {t("common.cancel")} disconnect.mutate()} > {t("settings.fitbitDisconnect")} diff --git a/src/components/settings/sources-section.tsx b/src/components/settings/sources-section.tsx index 232162404..35ece6b40 100644 --- a/src/components/settings/sources-section.tsx +++ b/src/components/settings/sources-section.tsx @@ -387,7 +387,8 @@ export function SourcesSection() { className="h-11 w-11" onClick={() => moveSource(metric, index, 1)} disabled={ - index === list.length - 1 || saveMutation.isPending + index === list.length - 1 || + saveMutation.isPending } aria-label={t("settings.sections.sources.moveDown")} > @@ -536,9 +537,7 @@ export function SourcesSection() { variant="ghost" size="icon" className="h-11 w-11" - onClick={() => - moveDeviceType(null, index, -1) - } + onClick={() => moveDeviceType(null, index, -1)} disabled={index === 0 || saveMutation.isPending} aria-label={t("settings.sections.sources.moveUp")} > @@ -566,8 +565,11 @@ export function SourcesSection() {
)} + {/* `flex-col-reverse` mirrors the DialogFooter idiom: the primary + action stacks on top on mobile, row order stays Cancel → Save + on `sm+`. */} {dirty && priority && ( -
+