mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
refactor(settings): drop the unused section heading frame
SettingsSectionFrame re-rendered the page heading and subtitle that SettingsShell already paints from the section slug. No route used it; only the per-section smoke tests rendered bodies through it to assert the visible heading and its id. Remove the component and move the heading wrapper those tests rely on into a test-only harness that reproduces the shell's heading markup, so the smoke tests keep asserting what the user actually sees.
This commit is contained in:
1 parent
d58116e3ff
commit
f4815fafe8
8 files changed
+51
-119
No files matched your search
@@ -31,7 +31,7 @@ vi.mock("@/hooks/use-auth", () => ({
|
||||
}));
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { SettingsSectionFrame } from "../settings-section-frame";
|
||||
import { SettingsSectionFrame } from "./section-frame-harness";
|
||||
import { CoachSection } from "../coach-section";
|
||||
|
||||
function render(locale: "en" | "de" = "en") {
|
||||
|
||||
@@ -44,7 +44,7 @@ vi.mock("@/hooks/use-auth", () => ({
|
||||
}));
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { SettingsSectionFrame } from "../settings-section-frame";
|
||||
import { SettingsSectionFrame } from "./section-frame-harness";
|
||||
import { ExportSection } from "../export-section";
|
||||
|
||||
function render(node: React.ReactElement, locale: "en" | "de" = "en") {
|
||||
|
||||
@@ -29,7 +29,7 @@ vi.mock("@/hooks/use-auth", () => ({
|
||||
}));
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { SettingsSectionFrame } from "../settings-section-frame";
|
||||
import { SettingsSectionFrame } from "./section-frame-harness";
|
||||
import { GesundheitsakteSection } from "../gesundheitsakte-section";
|
||||
|
||||
function render(node: React.ReactElement, locale: "en" | "de" = "en") {
|
||||
|
||||
@@ -65,7 +65,7 @@ vi.mock("@/hooks/use-auth", () => ({
|
||||
}));
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { SettingsSectionFrame } from "../settings-section-frame";
|
||||
import { SettingsSectionFrame } from "./section-frame-harness";
|
||||
import { MedicationsSection } from "../medications-section";
|
||||
|
||||
function render(locale: "en" | "de" = "en"): string {
|
||||
|
||||
@@ -131,7 +131,7 @@ vi.mock("@/hooks/use-auth", () => ({
|
||||
}));
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { SettingsSectionFrame } from "../settings-section-frame";
|
||||
import { SettingsSectionFrame } from "./section-frame-harness";
|
||||
import { MoodSection } from "../mood-section";
|
||||
|
||||
function render(locale: "en" | "de" = "en"): string {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Test-only harness that reproduces the page heading the live `SettingsShell`
|
||||
* paints for a given section slug (`settings-shell.tsx` headingBlock). The
|
||||
* per-section SSR smoke tests render a section body through this harness so
|
||||
* they can assert the visible heading + its historic `settings-section-<slug>-title`
|
||||
* id alongside the body — mirroring what a user sees on the route.
|
||||
*
|
||||
* Production no longer wraps section bodies in a standalone frame component:
|
||||
* the shell paints the heading from the slug and each section body is just its
|
||||
* cards. This harness lives in `__tests__` only and is never bundled.
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import type { SettingsSectionSlug } from "../section-slugs";
|
||||
|
||||
export function SettingsSectionFrame({
|
||||
slug,
|
||||
children,
|
||||
}: {
|
||||
slug: SettingsSectionSlug;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { t } = useTranslations();
|
||||
const titleId = `settings-section-${slug}-title`;
|
||||
|
||||
return (
|
||||
<section aria-labelledby={titleId} className="space-y-6">
|
||||
<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`)}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t(`settings.sections.${slug}.subtitle`)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -87,7 +87,7 @@ vi.mock("@/components/settings/thresholds-editor-section", () => ({
|
||||
ThresholdsEditorSection: () => <div data-testid="thresholds-editor" />,
|
||||
}));
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { SettingsSectionFrame } from "../settings-section-frame";
|
||||
import { SettingsSectionFrame } from "./section-frame-harness";
|
||||
import type { SettingsSectionSlug } from "../section-slugs";
|
||||
import { AboutSection } from "../about-section";
|
||||
import { AccountSection } from "../account-section";
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* `<SettingsSectionFrame>` — the canonical page-frame for every Settings
|
||||
* section. The settings twin of the admin renderer's `SectionFrame`
|
||||
* (`src/app/admin/[section]/renderer.tsx`): it emits the one visible
|
||||
* heading + subheading every page must show, in the reference style used by
|
||||
* the Admin Konsole → Übersicht landing.
|
||||
*
|
||||
* Before this primitive every settings section hand-rolled its own
|
||||
* `sr-only` `<h1>`, so no settings page showed a visible heading and the
|
||||
* left-menu-to-first-card alignment drifted page to page. Routing all 19
|
||||
* sections through one frame fixes both: a constant-height heading block
|
||||
* above the first card, and a real visible `h1 + subtitle`.
|
||||
*
|
||||
* <SettingsSectionFrame slug="account">
|
||||
* {cards}
|
||||
* </SettingsSectionFrame>
|
||||
*
|
||||
* Title comes from `settings.sections.<slug>.title`; subtitle from
|
||||
* `settings.sections.<slug>.subtitle`. The `<h1>` keeps the historic
|
||||
* `settings-section-<slug>-title` id so the section's own `aria-labelledby`
|
||||
* linkage — and the spotlight-tour anchors that reference it — stay intact.
|
||||
*
|
||||
* `topSlot` renders ABOVE the heading (used by the Layout-hub child
|
||||
* editors for their "← back to hub" link, which must sit above the title).
|
||||
* `headingAccessory` renders inline at the top-right of the heading row
|
||||
* (used by Gesundheitsakte for its guided-tour re-entry trigger).
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import type { SettingsSectionSlug } from "./section-slugs";
|
||||
|
||||
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
|
||||
* (e.g. a guided-tour re-entry trigger). */
|
||||
headingAccessory?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 { 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">
|
||||
{topSlot ? <div>{topSlot}</div> : null}
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<h1 id={titleId} className="text-2xl font-bold tracking-tight">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">{subtitle}</p>
|
||||
</div>
|
||||
{headingAccessory ? (
|
||||
<div className="shrink-0">{headingAccessory}</div>
|
||||
) : null}
|
||||
</div>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user