feat(settings): turn Appearance into a hub of per-module subpages

Appearance ("Darstellung") stacked every module's view and ordering
settings on one long page. Split it into a hub that lists the modules as
navigable rows, each opening its own /settings/layout/<module> subpage
with a back link — the per-module settings move verbatim, no restyling.
The module routes now redirect to those subpages instead of an in-page
anchor, and the in-app cogs link straight to them.
This commit is contained in:
Marc-André Bombeck committed 2026-06-30 18:53:50 +02:00
1 parent fa2e2c6ba2
commit a01c9d0435
20 files changed
+451 -205

No files matched your search

+3 -1
View File
@@ -570,7 +570,9 @@ export default function InsightsPage() {
description={t("insights.editMode.emptyDescription")}
action={
<Button size="sm" asChild>
<Link href="/settings/insights">{t("insights.customize")}</Link>
<Link href="/settings/layout/insights">
{t("insights.customize")}
</Link>
</Button>
}
/>
+1 -1
View File
@@ -90,7 +90,7 @@ export default function LabsPage() {
className="min-h-11 min-w-11 sm:min-h-9 sm:min-w-9"
>
<Link
href="/settings/labs"
href="/settings/layout/labs"
aria-label={t("labs.customize")}
title={t("labs.customize")}
>
@@ -123,12 +123,14 @@ describe("/medications — view renders from the persisted preference", () => {
expect(html).not.toContain('data-slot="medications-view-table"');
});
it("links the header customize glyph to /settings/medications", () => {
it("links the header customize glyph to /settings/layout/medications", () => {
// v1.16.11 — same entry-point logic as the dashboard and insights
// headers: one wrench ghost icon linking to the page's settings
// section (view preference + manual order live there).
// v1.25.11 (#148) — the per-module settings now live on the Appearance
// subpage `/settings/layout/medications`.
const html = renderPage({ version: 1, view: "cards", order: [] });
expect(html).toContain('href="/settings/medications"');
expect(html).toContain('href="/settings/layout/medications"');
expect(html).toContain('aria-label="Customize medications"');
// The in-page order dialog is gone — no dialog trigger remains.
expect(html).not.toContain('aria-label="Adjust order"');
+1 -1
View File
@@ -370,7 +370,7 @@ export default function MedicationsPage() {
className="min-h-11 min-w-11 sm:min-h-9 sm:min-w-9"
>
<Link
href="/settings/medications"
href="/settings/layout/medications"
aria-label={t("medications.customize")}
title={t("medications.customize")}
>
+1 -1
View File
@@ -90,7 +90,7 @@ export default function MoodPage() {
className="min-h-11 min-w-11 sm:min-h-9 sm:min-w-9"
>
<Link
href="/settings/mood"
href="/settings/layout/mood"
aria-label={t("mood.customize")}
title={t("mood.customize")}
>
+85
View File
@@ -0,0 +1,85 @@
import { notFound } from "next/navigation";
import { LayoutModuleGate } from "@/components/settings/layout-module-gate";
import {
LAYOUT_GROUPS,
LAYOUT_GROUP_IDS,
isLayoutGroupId,
} from "@/components/settings/layout-groups";
import { SettingsHubBackLink } from "@/components/settings/settings-hub-back-link";
import { SettingsShell } from "@/components/settings/settings-shell";
import { resolveServerLocale } from "@/lib/i18n/server-locale";
import { getServerTranslator } from "@/lib/i18n/server-translator";
/**
* v1.25.11 (#148) — per-module Appearance subpage.
*
* `/settings/layout/<module>` renders ONLY that module's section (the existing
* `*Section` component, verbatim) under the Appearance hub, with a
* "← Appearance" back-link. The hub at `/settings/layout` lists the modules and
* links here; the legacy `/settings/<module>` routes 301-redirect here
* (next.config.ts).
*
* Each subpage is pre-rendered for the known module ids via
* `generateStaticParams()`; `dynamicParams = false` 404s any other segment at
* routing time (re-checked with `notFound()` for defence-in-depth). The slug
* list is imported from the SERVER-safe `layout-groups` module so this server
* component never pulls runtime values across a client boundary.
*/
export const dynamicParams = false;
export function generateStaticParams() {
return LAYOUT_GROUP_IDS.map((module) => ({ module }));
}
interface PageProps {
// Next.js 16 made route `params` an async Promise. We `await` it before use.
params: Promise<{ module: string }>;
}
export default async function SettingsLayoutModulePage({ params }: PageProps) {
const { module } = await params;
if (!isLayoutGroupId(module)) {
notFound();
}
const group = LAYOUT_GROUPS.find((entry) => entry.id === module);
if (!group) {
notFound();
}
// Heading lives in the shell (its own grid row spanning only the content
// column). The title is the MODULE's title, resolved server-side from the
// cookie locale (mirroring the onboarding pages) so the shell receives a
// ready string — no client re-translation, so server HTML and the hydration
// render carry the identical heading text.
const locale = await resolveServerLocale();
const { t } = getServerTranslator(locale);
const Body = group.Body;
const headingId = `settings-section-${module}-title`;
return (
<SettingsShell
active="layout"
heading={{
title: t(group.titleKey),
subtitle: t(group.descriptionKey),
headingId,
topSlot: (
<SettingsHubBackLink
href="/settings/layout"
labelKey="settings.sections.layout.backToHub"
/>
),
}}
>
<section aria-labelledby={headingId} className="space-y-6">
<LayoutModuleGate moduleGate={group.moduleGate}>
<Body />
</LayoutModuleGate>
</section>
</SettingsShell>
);
}