feat(settings): standardize settings and admin section framing

Introduce a shared SettingsSectionFrame (the settings twin of the admin
renderer's SectionFrame) that emits the canonical visible heading +
subheading, and route all 19 settings sections through it. Every settings
page now shows a real h1 + subtitle in the reference Übersicht style
instead of an invisible sr-only title; the section bodies drop their own
headings and off-style intro paragraphs (the explanatory copy folds into
the frame subtitle).

- Route the admin About page through SectionFrame so it carries the same
  visible heading as every other admin section.
- Rename the AI settings section from KI-Auswertung to KI-Anbieter across
  six locales: the page configures the provider and key, not an analysis.
- Add a heading-block spacer to the settings and admin sidebars so the
  first menu item lines up with the top of the first card.
- Remove the always-on core-domains card from Module: a tracking area that
  can't be turned off doesn't need listing.
- Move the proactive-coach control out of Benachrichtigungen into Coach.
- Move admin module availability out of the Coach section into its own
  page: it gates every module, not just the coach.
- Give the Layout Insights editor a proper Insights heading.

Adds settings.sections.<slug>.subtitle and the new admin keys across all
six locales; removes the now-unused module core-domain strings.
This commit is contained in:
Marc-André Bombeck committed 2026-06-18 21:28:33 +02:00
1 parent ae3091df9e
commit 33d7d103da
42 files changed
+822 -802

No files matched your search

+27 -7
View File
@@ -94,18 +94,32 @@ export function AdminSectionRenderer({
// v1.18.1 — the former ai-quality, assistant, and coach-feedback
// sections fold into one Coach area: surface toggles + operator
// config first, then the two feedback-quality tables.
// v1.18.6 (W9) — server-wide module availability moved out to its own
// `module-availability` section: it gates EVERY module, not just the
// coach, so it no longer belongs stacked under Coach.
return (
<SectionFrame
title={t("admin.section.coach.title")}
subtitle={t("admin.section.coach.subtitle")}
>
<AssistantSection />
<ModuleAvailabilitySection />
<AiServerKeySection />
<CoachFeedbackSection />
<AiQualitySection />
</SectionFrame>
);
case "module-availability":
// v1.18.6 (W9) — operator-side server-wide module on/off, its own
// admin section (was stacked under Coach). Reconciled name: the
// user-facing "Module" settings pick from what is available here.
return (
<SectionFrame
title={t("admin.section.module-availability.title")}
subtitle={t("admin.section.module-availability.subtitle")}
>
<ModuleAvailabilitySection />
</SectionFrame>
);
case "feedback":
return (
<SectionFrame
@@ -191,12 +205,18 @@ export function AdminSectionRenderer({
</SectionFrame>
);
case "about":
// v1.4.36 W4e — About section reused as-is from the settings
// surface. The component owns its own heading + cards layout
// so no SectionFrame wrapper.
// v1.18.6 — the About surface no longer carries the tour-replay card
// (consolidated onto Settings → Advanced), so there's nothing to hide.
return <AboutSection />;
// v1.4.36 W4e — About section reused as-is from the settings surface.
// v1.18.6 (W9) — route it through `SectionFrame` like every other admin
// section so it gets the same visible heading + subtitle (the one admin
// page that was bypassing the frame and showing no heading).
return (
<SectionFrame
title={t("admin.section.about.title")}
subtitle={t("admin.section.about.subtitle")}
>
<AboutSection />
</SectionFrame>
);
default:
slug satisfies never;
return null;
+58 -2
View File
@@ -1,5 +1,5 @@
import { notFound } from "next/navigation";
import type { JSX } from "react";
import type { JSX, ReactNode } from "react";
import { AccountSection } from "@/components/settings/account-section";
import { AboutSection } from "@/components/settings/about-section";
@@ -29,6 +29,9 @@ import {
type SettingsSectionSlug,
} from "@/components/settings/section-slugs";
import { SettingsShell } from "@/components/settings/settings-shell";
import { SettingsSectionFrame } from "@/components/settings/settings-section-frame";
import { SettingsHubBackLink } from "@/components/settings/settings-hub-back-link";
import { ModuleTourTrigger } from "@/components/onboarding/module-tour-trigger";
/**
* Dynamic settings section route. Each of the `SETTINGS_SECTION_SLUGS`
@@ -71,6 +74,50 @@ const SECTION_COMPONENTS: Record<
advanced: AdvancedSection,
};
// v1.18.6 (W9) — the two Layout-hub child editors render a "← back to hub"
// link above their heading. The shared `<SettingsSectionFrame>` exposes a
// `topSlot` for exactly this; the link lives here so the section bodies stay
// pure card content.
const HUB_CHILD_SLUGS: ReadonlySet<SettingsSectionSlug> = new Set([
"dashboard",
"insights",
]);
/** Optional content rendered above the canonical heading, per slug. */
function frameTopSlot(slug: SettingsSectionSlug): ReactNode {
if (HUB_CHILD_SLUGS.has(slug)) {
return (
<SettingsHubBackLink
href="/settings/layout"
labelKey="settings.sections.layout.backToHub"
/>
);
}
return undefined;
}
/** Optional inline content rendered at the top-right of the heading row. */
function frameHeadingAccessory(slug: SettingsSectionSlug): ReactNode {
// v1.18.6 (W9) — the module guided-tour re-entry triggers used to live in
// a `justify-end` header inside the section body; they move to the frame's
// heading row so the heading itself is the spotlight anchor.
if (slug === "gesundheitsakte") {
return (
<span data-tour-id="export-hero">
<ModuleTourTrigger stopId="export" />
</span>
);
}
if (slug === "integrations") {
return (
<span data-tour-id="integrations-hero">
<ModuleTourTrigger stopId="integrations" />
</span>
);
}
return undefined;
}
interface PageProps {
// Next.js 16 made route `params` an async Promise. We `await` it before use.
params: Promise<{ section: string }>;
@@ -91,8 +138,17 @@ export default async function SettingsSectionPage({ params }: PageProps) {
// guards the slug, but a future slug added to `SETTINGS_SECTION_SLUGS`
// without a wired component would otherwise crash silently — placeholder
// surfaces the gap visually instead.
// v1.18.6 (W9) — every section now renders inside the shared
// `<SettingsSectionFrame>`, which emits the one visible heading + subtitle
// each page must show (the section bodies no longer carry their own).
const body = SectionComponent ? (
<SectionComponent />
<SettingsSectionFrame
slug={section}
topSlot={frameTopSlot(section)}
headingAccessory={frameHeadingAccessory(section)}
>
<SectionComponent />
</SettingsSectionFrame>
) : (
<SectionPlaceholder slug={section} />
);