diff --git a/messages/de.json b/messages/de.json index 569854ee4..b49ed0581 100644 --- a/messages/de.json +++ b/messages/de.json @@ -3814,6 +3814,26 @@ "title": "Benachrichtigungen", "description": "Live-Übersicht aller konfigurierten Kanäle." }, + "layout": { + "title": "Layout & Personalisierung", + "description": "Lege fest, wie deine App aufgebaut ist — ein Ort für Dashboard, Insights, Medikamente und Stimmung.", + "dashboard": { + "title": "Dashboard", + "description": "Kachel-Layout und Reihenfolge." + }, + "insights": { + "title": "Insights", + "description": "Reihenfolge der Übersicht und der Navigations-Pills." + }, + "medications": { + "title": "Medikamente", + "description": "Listenansicht und Reihenfolge der Medikamente." + }, + "mood": { + "title": "Stimmungs-Tags", + "description": "Gruppen, Tags und Reihenfolge der Tag-Auswahl." + } + }, "dashboard": { "title": "Dashboard", "description": "Kachel-Layout und Reihenfolge." diff --git a/messages/en.json b/messages/en.json index adfa22579..8afabdd03 100644 --- a/messages/en.json +++ b/messages/en.json @@ -3814,6 +3814,26 @@ "title": "Notifications", "description": "Live overview of every configured channel." }, + "layout": { + "title": "Layout & Personalization", + "description": "Arrange how your app is laid out — one home for the dashboard, insights, medications, and mood.", + "dashboard": { + "title": "Dashboard", + "description": "Tile layout and order." + }, + "insights": { + "title": "Insights", + "description": "Overview sections and navigation-pill order." + }, + "medications": { + "title": "Medications", + "description": "List view and medication order." + }, + "mood": { + "title": "Mood tags", + "description": "Groups, tags, and order of the mood picker." + } + }, "dashboard": { "title": "Dashboard", "description": "Tile layout and order." diff --git a/messages/es.json b/messages/es.json index 2d284fe6d..9af3b04d0 100644 --- a/messages/es.json +++ b/messages/es.json @@ -3814,6 +3814,26 @@ "title": "Notificaciones", "description": "Vista en vivo de cada canal configurado." }, + "layout": { + "title": "Diseño y personalización", + "description": "Organiza cómo está dispuesta tu app — un único lugar para el panel, los análisis, los medicamentos y el ánimo.", + "dashboard": { + "title": "Panel", + "description": "Diseño y orden de los mosaicos." + }, + "insights": { + "title": "Análisis", + "description": "Orden de la vista general y de las pestañas de navegación." + }, + "medications": { + "title": "Medicamentos", + "description": "Vista de lista y orden de los medicamentos." + }, + "mood": { + "title": "Etiquetas de ánimo", + "description": "Grupos, etiquetas y orden del selector de ánimo." + } + }, "dashboard": { "title": "Panel", "description": "Diseño y orden de los mosaicos." diff --git a/messages/fr.json b/messages/fr.json index 2e480080f..aa345cfec 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -3814,6 +3814,26 @@ "title": "Notifications", "description": "Aperçu en direct de chaque canal configuré." }, + "layout": { + "title": "Disposition et personnalisation", + "description": "Organisez la disposition de votre application — un seul endroit pour le tableau de bord, les analyses, les médicaments et l’humeur.", + "dashboard": { + "title": "Tableau de bord", + "description": "Disposition et ordre des tuiles." + }, + "insights": { + "title": "Analyses", + "description": "Ordre de la vue d'ensemble et des onglets de navigation." + }, + "medications": { + "title": "Médicaments", + "description": "Vue de la liste et ordre des médicaments." + }, + "mood": { + "title": "Étiquettes d’humeur", + "description": "Groupes, étiquettes et ordre du sélecteur d’humeur." + } + }, "dashboard": { "title": "Tableau de bord", "description": "Disposition et ordre des tuiles." diff --git a/messages/it.json b/messages/it.json index b52b754a8..fc6312184 100644 --- a/messages/it.json +++ b/messages/it.json @@ -3814,6 +3814,26 @@ "title": "Notifiche", "description": "Panoramica in tempo reale di ogni canale configurato." }, + "layout": { + "title": "Layout e personalizzazione", + "description": "Organizza la disposizione della tua app — un unico posto per cruscotto, approfondimenti, farmaci e umore.", + "dashboard": { + "title": "Cruscotto", + "description": "Layout e ordine dei riquadri." + }, + "insights": { + "title": "Approfondimenti", + "description": "Ordine della panoramica e delle schede di navigazione." + }, + "medications": { + "title": "Farmaci", + "description": "Vista elenco e ordine dei farmaci." + }, + "mood": { + "title": "Tag dell’umore", + "description": "Gruppi, tag e ordine del selettore dell’umore." + } + }, "dashboard": { "title": "Cruscotto", "description": "Layout e ordine dei riquadri." diff --git a/messages/pl.json b/messages/pl.json index 02f4a3914..1bd18cc61 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -3814,6 +3814,26 @@ "title": "Powiadomienia", "description": "Podgląd na żywo każdego skonfigurowanego kanału." }, + "layout": { + "title": "Układ i personalizacja", + "description": "Ustal, jak rozłożona jest Twoja aplikacja — jedno miejsce dla pulpitu, analiz, leków i nastroju.", + "dashboard": { + "title": "Pulpit", + "description": "Układ i kolejność kafelków." + }, + "insights": { + "title": "Analizy", + "description": "Kolejność przeglądu i pigułek nawigacyjnych." + }, + "medications": { + "title": "Leki", + "description": "Widok listy i kolejność leków." + }, + "mood": { + "title": "Tagi nastroju", + "description": "Grupy, tagi i kolejność w wyborze nastroju." + } + }, "dashboard": { "title": "Pulpit", "description": "Układ i kolejność kafelków." diff --git a/src/app/settings/[section]/page.tsx b/src/app/settings/[section]/page.tsx index a0c522539..20f1d2d25 100644 --- a/src/app/settings/[section]/page.tsx +++ b/src/app/settings/[section]/page.tsx @@ -10,6 +10,7 @@ import { DashboardSection } from "@/components/settings/dashboard-section"; import { ExportSection } from "@/components/settings/export-section"; import { InsightsSection } from "@/components/settings/insights-section"; import { IntegrationsSection } from "@/components/settings/integrations-section"; +import { LayoutSection } from "@/components/settings/layout-section"; import { MedicationsSection } from "@/components/settings/medications-section"; import { MoodSection } from "@/components/settings/mood-section"; import { NotificationsSection } from "@/components/settings/notifications-section"; @@ -51,6 +52,7 @@ const SECTION_COMPONENTS: Record< ai: AiSection, integrations: IntegrationsSection, notifications: NotificationsSection, + layout: LayoutSection, dashboard: DashboardSection, insights: InsightsSection, medications: MedicationsSection, diff --git a/src/components/settings/__tests__/layout-section.test.tsx b/src/components/settings/__tests__/layout-section.test.tsx new file mode 100644 index 000000000..422fe8fcd --- /dev/null +++ b/src/components/settings/__tests__/layout-section.test.tsx @@ -0,0 +1,48 @@ +/** + * v1.17.1 (F-2) — the "Layout & Personalization" hub. + * + * The hub is the single front door for the four personalization editors. + * It must link to each of them with consistent framing so the concept + * reads as one home rather than four scattered settings sections. + */ +import { describe, expect, it } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; + +import { I18nProvider } from "@/lib/i18n/context"; +import { LayoutSection } from "../layout-section"; + +function render(locale: "en" | "de" = "en") { + return renderToStaticMarkup( + + + , + ); +} + +describe("", () => { + it("links to all four personalization editors", () => { + const html = render(); + for (const href of [ + "/settings/dashboard", + "/settings/insights", + "/settings/medications", + "/settings/mood", + ]) { + expect(html).toContain(`href="${href}"`); + } + }); + + it("renders the hub heading and a card per editor", () => { + const html = render(); + expect(html).toContain("Dashboard"); + expect(html).toContain("Insights"); + expect(html).toContain("Medications"); + expect(html).toContain("Mood tags"); + }); + + it("resolves its copy in German too", () => { + const html = render("de"); + expect(html).toContain("Medikamente"); + expect(html).toContain("Stimmungs-Tags"); + }); +}); diff --git a/src/components/settings/__tests__/settings-shell.test.tsx b/src/components/settings/__tests__/settings-shell.test.tsx index c613b9692..361c4a524 100644 --- a/src/components/settings/__tests__/settings-shell.test.tsx +++ b/src/components/settings/__tests__/settings-shell.test.tsx @@ -56,10 +56,16 @@ describe("SETTINGS_SECTION_SLUGS", () => { // v1.17 — `mood` sits between `medications` and `thresholds`: the // mood-tag management surface (groups, custom tags, hide/archive, // picker order), reached from the /mood page header's wrench glyph. + // v1.17.1 (F-2) — `layout` is the one "Layout & Personalization" home + // and sits after `notifications`. The four personalization editor + // slugs (`dashboard`, `insights`, `medications`, `mood`) keep their + // routes so deep links resolve, but they are reached through the + // Layout hub instead of four standalone nav entries. expect([...SETTINGS_SECTION_SLUGS]).toEqual([ "account", "integrations", "notifications", + "layout", "dashboard", "insights", "medications", @@ -140,6 +146,32 @@ describe("", () => { expect(html.match(activeLinkRegex)?.length ?? 0).toBe(2); }); + it("highlights the Layout hub when on a Layout child editor (v1.17.1 F-2)", () => { + // On `/settings/dashboard` (a Layout child reached through the hub) the + // Layout nav entry must read active even though Dashboard has no nav + // entry of its own. Both layouts emit the active link → two matches. + for (const child of [ + "/settings/dashboard", + "/settings/insights", + "/settings/medications", + "/settings/mood", + ]) { + const html = renderShell({ pathname: child }); + const layoutActive = + /]*\baria-current="page"[^>]*\bhref="\/settings\/layout"|]*\bhref="\/settings\/layout"[^>]*\baria-current="page"/g; + expect(html.match(layoutActive)?.length ?? 0).toBe(2); + } + }); + + it("highlights the Layout hub when the active prop is a Layout child", () => { + // The page passes `active={section}` explicitly; a child slug must + // still resolve onto the Layout hub for nav highlighting. + const html = renderShell({ active: "mood", pathname: "/settings/mood" }); + const layoutActive = + /]*\baria-current="page"[^>]*\bhref="\/settings\/layout"|]*\bhref="\/settings\/layout"[^>]*\baria-current="page"/g; + expect(html.match(layoutActive)?.length ?? 0).toBe(2); + }); + it("falls back to `account` when the pathname doesn't match a known slug", () => { const html = renderShell({ pathname: "/settings/totally-bogus" }); const activeLinkRegex = @@ -155,16 +187,14 @@ describe("", () => { // (single-line; the longer "Notification channels" wrapped). The // `/notifications` inbox now shares the "Notifications" label. expect(html).toContain("Notifications"); - expect(html).toContain("Dashboard"); - expect(html).toContain("Insights"); - // v1.16.10 — the /medications customise surface is its own nav entry - // between Insights and Targets. - expect(html).toContain('href="/settings/medications"'); - expect(html).toContain("Medications"); - // v1.17 — the mood-tag management surface sits between Medications - // and Targets. - expect(html).toContain('href="/settings/mood"'); - expect(html).toContain("Mood tags"); + // v1.17.1 (F-2) — the four personalization editors (Dashboard, + // Insights, Medications, Mood) are reached through one Layout hub + // entry; the hub itself is the single nav entry for the concept. + expect(html).toContain('href="/settings/layout"'); + expect(html).toContain("Layout & Personalization"); + // The four editors are no longer standalone nav entries. + expect(html).not.toContain('href="/settings/medications"'); + expect(html).not.toContain('href="/settings/mood"'); // The ampersand is HTML-escaped by React SSR — assert on the encoded // form so we don't accidentally match a parser that double-escapes. expect(html).toContain("API & Tokens"); @@ -192,17 +222,13 @@ describe("", () => { // compound "Benachrichtigungs-Kanäle" wrapped). The `/notifications` // inbox now shares the "Benachrichtigungen" label. expect(html).toContain("Benachrichtigungen"); - // v1.4.3: the Settings sub-section formerly labelled "Übersicht" is now - // "Dashboard" (matching the term users see in the main nav). The - // per-metric overrides moved out into their own "Persönliche Zielwerte" - // section, which is the new entry below the Dashboard one. - expect(html).toContain("Dashboard"); - // v1.16.10 — the /medications customise surface ("Medikamente"). - expect(html).toContain('href="/settings/medications"'); - expect(html).toContain("Medikamente"); - // v1.17 — the mood-tag management surface ("Stimmungs-Tags"). - expect(html).toContain('href="/settings/mood"'); - expect(html).toContain("Stimmungs-Tags"); + // v1.17.1 (F-2) — the four personalization editors are reached through + // one Layout hub entry ("Layout & Personalisierung"); they are no + // longer standalone German nav entries. + expect(html).toContain('href="/settings/layout"'); + expect(html).toContain("Layout & Personalisierung"); + expect(html).not.toContain('href="/settings/medications"'); + expect(html).not.toContain('href="/settings/mood"'); // v1.8.7.1 — Targets and Sources are two separate German nav // entries again: "Zielwerte" and "Quellen". expect(html).toContain("Zielwerte"); diff --git a/src/components/settings/layout-section.tsx b/src/components/settings/layout-section.tsx new file mode 100644 index 000000000..c9ed1814a --- /dev/null +++ b/src/components/settings/layout-section.tsx @@ -0,0 +1,110 @@ +"use client"; + +import Link from "next/link"; +import { + ChevronRight, + LayoutDashboard, + Pill, + Smile, + TrendingUp, + type LucideIcon, +} from "lucide-react"; + +import { cn } from "@/lib/utils"; +import { useTranslations } from "@/lib/i18n/context"; + +/** + * v1.17.1 (F-2) — the one "Layout & Personalization" home. + * + * Before this section, "how my app is laid out" was scattered across four + * unrelated settings sections — Dashboard, Insights, Medications, Mood — + * with no shared framing, so the same concept (arrange / reorder / show- + * hide) read as four disconnected places. This section is the single + * front door: it presents the four personalization surfaces as one + * consistent set of cards and links to each editor, which all keep their + * own routes and mutation flows intact. One concept, one home. + */ +interface LayoutLink { + href: string; + icon: LucideIcon; + titleKey: string; + descriptionKey: string; +} + +const LAYOUT_LINKS: ReadonlyArray = [ + { + href: "/settings/dashboard", + icon: LayoutDashboard, + titleKey: "settings.sections.layout.dashboard.title", + descriptionKey: "settings.sections.layout.dashboard.description", + }, + { + href: "/settings/insights", + icon: TrendingUp, + titleKey: "settings.sections.layout.insights.title", + descriptionKey: "settings.sections.layout.insights.description", + }, + { + href: "/settings/medications", + icon: Pill, + titleKey: "settings.sections.layout.medications.title", + descriptionKey: "settings.sections.layout.medications.description", + }, + { + href: "/settings/mood", + icon: Smile, + titleKey: "settings.sections.layout.mood.title", + descriptionKey: "settings.sections.layout.mood.description", + }, +]; + +export function LayoutSection() { + const { t } = useTranslations(); + + return ( + + + + {t("settings.sections.layout.title")} + + + {t("settings.sections.layout.description")} + + + + + {LAYOUT_LINKS.map((link) => { + const Icon = link.icon; + return ( + + + + + {t(link.titleKey)} + + {t(link.descriptionKey)} + + + + + + ); + })} + + + ); +} diff --git a/src/components/settings/section-placeholder.tsx b/src/components/settings/section-placeholder.tsx index 241654206..554c26cee 100644 --- a/src/components/settings/section-placeholder.tsx +++ b/src/components/settings/section-placeholder.tsx @@ -37,6 +37,7 @@ const SLUG_ICON: Record = { account: User, integrations: Link2, notifications: Bell, + layout: LayoutDashboard, dashboard: LayoutDashboard, insights: TrendingUp, medications: Pill, diff --git a/src/components/settings/section-slugs.ts b/src/components/settings/section-slugs.ts index 28b812cbf..fd886ef78 100644 --- a/src/components/settings/section-slugs.ts +++ b/src/components/settings/section-slugs.ts @@ -20,10 +20,18 @@ // v1.17 — `mood` sits after `medications`: the mood-tag management // surface (groups, custom tags, hide/archive, picker order), reached // from the /mood page header's wrench glyph. +// v1.17.1 (F-2) — `layout` is the one "Layout & Personalization" home. +// The dashboard / insights / medications / mood personalization editors +// each keep their own route (deep links, page-header cogs, and the hub's +// own links all resolve), but the settings NAV now surfaces a single +// `layout` entry instead of four scattered "arrange" entries, so the +// concept reads as one place. The four editor slugs stay in this list so +// their routes still resolve; they are simply not listed in the shell nav. export const SETTINGS_SECTION_SLUGS = [ "account", "integrations", "notifications", + "layout", "dashboard", "insights", "medications", diff --git a/src/components/settings/settings-shell.tsx b/src/components/settings/settings-shell.tsx index af5532ac2..530c48963 100644 --- a/src/components/settings/settings-shell.tsx +++ b/src/components/settings/settings-shell.tsx @@ -25,12 +25,9 @@ import { Layers, LayoutDashboard, Link2, - Pill, Settings2, SlidersHorizontal, - Smile, Sparkles, - TrendingUp, User, type LucideIcon, } from "lucide-react"; @@ -92,26 +89,16 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [ titleKey: "settings.sections.notifications.title", icon: Bell, }, + // v1.17.1 (F-2) — one "Layout & Personalization" nav entry replaces the + // four scattered Dashboard / Insights / Medications / Mood "arrange" + // entries. Those routes still resolve (deep links, page-header cogs, and + // the hub's own links all work); they are simply reached through the + // Layout hub now, so "how my app is laid out" reads as one home. { - slug: "dashboard", - titleKey: "settings.sections.dashboard.title", + slug: "layout", + titleKey: "settings.sections.layout.title", icon: LayoutDashboard, }, - { - slug: "insights", - titleKey: "settings.sections.insights.title", - icon: TrendingUp, - }, - { - slug: "medications", - titleKey: "settings.sections.medications.title", - icon: Pill, - }, - { - slug: "mood", - titleKey: "settings.sections.mood.title", - icon: Smile, - }, { slug: "thresholds", titleKey: "settings.sections.thresholds.title", @@ -151,16 +138,33 @@ export interface SettingsShellProps { children: React.ReactNode; } +// v1.17.1 (F-2) — the four personalization editors live under the Layout +// hub. They keep their own routes but are NOT standalone nav entries, so +// when the user is on one of them the Layout nav entry is the one that +// reads active. +const LAYOUT_CHILD_SLUGS: ReadonlySet = new Set([ + "dashboard", + "insights", + "medications", + "mood", +]); + +/** Map a Layout child editor onto the Layout hub for nav highlighting. */ +function navHighlightSlug(slug: SettingsSectionSlug): SettingsSectionSlug { + return LAYOUT_CHILD_SLUGS.has(slug) ? "layout" : slug; +} + function deriveActiveSlug( pathname: string | null, override?: SettingsSectionSlug, ): SettingsSectionSlug { - if (override) return override; + if (override) return navHighlightSlug(override); if (!pathname) return "account"; // Match `/settings/` and ignore any trailing segments (none today, // but cheap insurance for future nested routes). const match = pathname.match(/^\/settings\/([^/]+)/); const candidate = match?.[1] ?? ""; + if (LAYOUT_CHILD_SLUGS.has(candidate)) return "layout"; return isSettingsSectionSlug(candidate) ? candidate : "account"; }
+ {t("settings.sections.layout.description")} +
{t(link.titleKey)}
+ {t(link.descriptionKey)} +