From bfba92cb833cdb94101917a0cf8500f308a3f0f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 18 Jun 2026 22:06:40 +0200 Subject: [PATCH] refactor(settings): share one heading frame across all settings pages Teach SettingsSectionFrame an explicit title/subtitle mode alongside the slug-driven mode, and have the per-module settings pages (Vorsorge, Illness, Labs) delegate to it so every settings surface renders the same heading frame. --- .../module-list/module-settings-frame.tsx | 30 ++++----- .../settings/settings-section-frame.tsx | 66 +++++++++++++++---- 2 files changed, 66 insertions(+), 30 deletions(-) diff --git a/src/components/module-list/module-settings-frame.tsx b/src/components/module-list/module-settings-frame.tsx index 12c6887d1..4d2959b19 100644 --- a/src/components/module-list/module-settings-frame.tsx +++ b/src/components/module-list/module-settings-frame.tsx @@ -3,21 +3,18 @@ import type { ReactNode } from "react"; import { SettingsHubBackLink } from "@/components/settings/settings-hub-back-link"; +import { SettingsSectionFrame } from "@/components/settings/settings-section-frame"; /** * v1.18.6 (W8 / MOD-03) — heading frame for the per-module settings pages * (Vorsorge / Illness / Labs). * - * FRAME-ADOPTION SEAM: a parallel phase is introducing a shared - * `SettingsSectionFrame` primitive under `src/components/settings/*` (which - * this phase must not edit). Until that lands, this renders the canonical - * settings heading markup verbatim — `h1.text-2xl.font-bold.tracking-tight` - * + `p.text-muted-foreground.text-sm` — so the three new pages already - * conform to the standard and can be swapped to the shared frame at merge by - * replacing this single wrapper. The back-link reuses the existing - * `SettingsHubBackLink` (read-only import) so the pages have a consistent - * "return to the hub" affordance even though they are not listed in the - * settings-shell sidebar. + * Delegates to the shared `SettingsSectionFrame` so every settings page — the + * 19 shell sections and these three per-module pages — renders one identical + * heading frame. These pages stay out of the slug registry (their static + * routes win over `/settings/[section]`), so they drive the frame's explicit + * title/subtitle mode and supply the "return to the module" back-link through + * the frame's `topSlot`, which sits above the heading. */ export function ModuleSettingsFrame({ title, @@ -34,13 +31,12 @@ export function ModuleSettingsFrame({ children: ReactNode; }) { return ( -
-
- -

{title}

-

{description}

-
+ } + > {children} -
+ ); } diff --git a/src/components/settings/settings-section-frame.tsx b/src/components/settings/settings-section-frame.tsx index 1191d1cb0..067bd3b17 100644 --- a/src/components/settings/settings-section-frame.tsx +++ b/src/components/settings/settings-section-frame.tsx @@ -33,8 +33,7 @@ import * as React from "react"; import { useTranslations } from "@/lib/i18n/context"; import type { SettingsSectionSlug } from "./section-slugs"; -export interface SettingsSectionFrameProps { - slug: SettingsSectionSlug; +interface SettingsSectionFrameBaseProps { /** Optional content rendered above the heading (e.g. a back-to-hub link). */ topSlot?: React.ReactNode; /** Optional content rendered inline at the top-right of the heading row @@ -43,14 +42,57 @@ export interface SettingsSectionFrameProps { children: React.ReactNode; } -export function SettingsSectionFrame({ - slug, - topSlot, - headingAccessory, - children, -}: SettingsSectionFrameProps) { +/** + * Slug mode (the 19 shell sections): title + subtitle come from + * `settings.sections..{title,subtitle}` and the `

` keeps the + * historic `settings-section--title` id for `aria-labelledby` and + * spotlight-tour anchors. + */ +interface SettingsSectionFrameSlugProps extends SettingsSectionFrameBaseProps { + slug: SettingsSectionSlug; +} + +/** + * Explicit mode (the per-module settings pages — Vorsorge / Illness / Labs — + * which deliberately stay out of the slug registry because their static + * routes win over `/settings/[section]`): caller passes resolved strings and + * an optional heading id. This lets `ModuleSettingsFrame` delegate here so + * every settings page shares one heading frame. + */ +interface SettingsSectionFrameExplicitProps + extends SettingsSectionFrameBaseProps { + title: string; + subtitle: string; + /** Heading element id (defaults to a stable slug-less id). */ + titleId?: string; +} + +export type SettingsSectionFrameProps = + | SettingsSectionFrameSlugProps + | SettingsSectionFrameExplicitProps; + +function isSlugProps( + props: SettingsSectionFrameProps, +): props is SettingsSectionFrameSlugProps { + return "slug" in props; +} + +export function SettingsSectionFrame(props: SettingsSectionFrameProps) { const { t } = useTranslations(); - const titleId = `settings-section-${slug}-title`; + const { topSlot, headingAccessory, children } = props; + + let title: string; + let subtitle: string; + let titleId: string; + if (isSlugProps(props)) { + titleId = `settings-section-${props.slug}-title`; + title = t(`settings.sections.${props.slug}.title`); + subtitle = t(`settings.sections.${props.slug}.subtitle`); + } else { + titleId = props.titleId ?? "settings-section-title"; + title = props.title; + subtitle = props.subtitle; + } return (
@@ -58,11 +100,9 @@ export function SettingsSectionFrame({

- {t(`settings.sections.${slug}.title`)} + {title}

-

- {t(`settings.sections.${slug}.subtitle`)} -

+

{subtitle}

{headingAccessory ? (
{headingAccessory}