mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
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.
This commit is contained in:
1 parent
39505a5d35
commit
bfba92cb83
2 files changed
+66
-30
No files matched your search
@@ -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 (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-1">
|
||||
<SettingsHubBackLink href={backHref} labelKey={backLabelKey} />
|
||||
<h1 className="text-2xl font-bold tracking-tight">{title}</h1>
|
||||
<p className="text-muted-foreground text-sm">{description}</p>
|
||||
</div>
|
||||
<SettingsSectionFrame
|
||||
title={title}
|
||||
subtitle={description}
|
||||
topSlot={<SettingsHubBackLink href={backHref} labelKey={backLabelKey} />}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</SettingsSectionFrame>
|
||||
);
|
||||
}
|
||||
@@ -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.<slug>.{title,subtitle}` and the `<h1>` keeps the
|
||||
* historic `settings-section-<slug>-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 (
|
||||
<section aria-labelledby={titleId} className="space-y-6">
|
||||
@@ -58,11 +100,9 @@ export function SettingsSectionFrame({
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<h1 id={titleId} className="text-2xl font-bold tracking-tight">
|
||||
{t(`settings.sections.${slug}.title`)}
|
||||
{title}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t(`settings.sections.${slug}.subtitle`)}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm">{subtitle}</p>
|
||||
</div>
|
||||
{headingAccessory ? (
|
||||
<div className="shrink-0">{headingAccessory}</div>
|
||||
|
||||
Reference in new issue
Block a user