feat(ui): establish Settings page layout primitives
This commit is contained in:
@@ -31,6 +31,7 @@ import NotificationsNoneIcon from "@mui/icons-material/NotificationsNone";
|
||||
import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
|
||||
|
||||
import JobbjaktMark from "../assets/JobbjaktMark";
|
||||
import { PageHeader, type PageWidth } from "./PageLayout";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
|
||||
export type NavItem = {
|
||||
@@ -78,6 +79,7 @@ const SIDEBAR_SELECTED_ICON = "var(--palette-navigation-selectedIcon)";
|
||||
export default function AppShell({
|
||||
pageTitle,
|
||||
pageSubtitle,
|
||||
pageLayout,
|
||||
breadcrumbs,
|
||||
pathname,
|
||||
nav,
|
||||
@@ -97,6 +99,7 @@ export default function AppShell({
|
||||
}: {
|
||||
pageTitle: string;
|
||||
pageSubtitle?: string;
|
||||
pageLayout?: PageWidth;
|
||||
breadcrumbs: string[];
|
||||
pathname: string;
|
||||
nav: NavItem[];
|
||||
@@ -527,7 +530,7 @@ export default function AppShell({
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ mx: "auto", maxWidth: 1320, width: "100%", minWidth: 0 }}>
|
||||
<Box sx={{ mx: "auto", maxWidth: pageLayout ? `var(--app-page-width-${pageLayout})` : 1320, width: "100%", minWidth: 0 }}>
|
||||
<Toolbar sx={{ minHeight: { xs: isMobile ? 124 : 68, md: 76 } }} />
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 0.75, mb: "var(--app-section-gap)", minWidth: 0 }}>
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
@@ -538,14 +541,18 @@ export default function AppShell({
|
||||
</Typography>
|
||||
))}
|
||||
</Breadcrumbs>
|
||||
<Typography variant="h5" sx={{ fontWeight: 600, overflowWrap: "anywhere" }}>
|
||||
{pageTitle}
|
||||
</Typography>
|
||||
{pageSubtitle ? (
|
||||
<Typography variant="body2" sx={{ color: "text.secondary", mt: 0.25, overflowWrap: "anywhere" }}>
|
||||
{pageSubtitle}
|
||||
</Typography>
|
||||
) : null}
|
||||
{pageLayout ? <PageHeader title={pageTitle} description={pageSubtitle} /> : (
|
||||
<>
|
||||
<Typography variant="h5" sx={{ fontWeight: 600, overflowWrap: "anywhere" }}>
|
||||
{pageTitle}
|
||||
</Typography>
|
||||
{pageSubtitle ? (
|
||||
<Typography variant="body2" sx={{ color: "text.secondary", mt: 0.25, overflowWrap: "anywhere" }}>
|
||||
{pageSubtitle}
|
||||
</Typography>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import React, { useId } from "react";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
|
||||
// Opt-in widths: the shell owns gutters; consumers must not add a second page gutter.
|
||||
export type PageWidth = "reading" | "data" | "editor";
|
||||
|
||||
type HeaderProps = {
|
||||
title: string;
|
||||
description?: string;
|
||||
actions?: React.ReactNode;
|
||||
level?: "h1" | "h2" | "h3";
|
||||
titleId?: string;
|
||||
};
|
||||
|
||||
export function PageHeader({ title, description, actions, level = "h1", titleId }: HeaderProps) {
|
||||
return (
|
||||
<Box component="header" sx={{ display: "flex", flexWrap: "wrap", alignItems: "flex-start", gap: 2, minWidth: 0 }}>
|
||||
<Box sx={{ flex: "1 1 16rem", minWidth: 0 }}>
|
||||
<Typography id={titleId} component={level} variant={level === "h1" ? "pageTitle" : "sectionTitle"} sx={{ overflowWrap: "anywhere" }}>
|
||||
{title}
|
||||
</Typography>
|
||||
{description ? <Typography variant="pageDescription" component="p" color="text.secondary" sx={{ mt: 1, maxWidth: "75ch", overflowWrap: "anywhere" }}>
|
||||
{description}
|
||||
</Typography> : null}
|
||||
</Box>
|
||||
{actions ? <Box sx={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: 1, minWidth: 0, maxWidth: "100%", "& > *": { maxWidth: "100%", whiteSpace: "normal", overflowWrap: "anywhere" } }}>
|
||||
{actions}
|
||||
</Box> : null}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Presentation only: domain components keep their loading, draft and mutation owners.
|
||||
export function PageSection({ title, description, actions, children, level = "h2", surface = false }: Omit<HeaderProps, "titleId" | "level"> & {
|
||||
children?: React.ReactNode;
|
||||
level?: "h2" | "h3";
|
||||
surface?: boolean;
|
||||
}) {
|
||||
const titleId = useId();
|
||||
return (
|
||||
<Box component="section" aria-labelledby={titleId} sx={{
|
||||
display: "grid", gap: 2, minWidth: 0, alignContent: "start",
|
||||
...(surface ? { p: "var(--app-card-padding)", bgcolor: "background.paper", border: "1px solid", borderColor: "divider", borderRadius: 1 } : {}),
|
||||
}}>
|
||||
<PageHeader title={title} titleId={titleId} description={description} actions={actions} level={level} />
|
||||
{children ? <Box sx={{ minWidth: 0 }}>{children}</Box> : null}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user