Skip to content

Commit fcac4ab

Browse files
authored
fix(site): standardize headers for Admin Settings page (#16911)
## Changes made - Switched almost all headers to use the `SettingHeader` component - Redesigned component to be more composition-based, to stay in line with the patterns we're starting to use more throughout the codebase - Refactored `SettingHeader` to be based on Radix and Tailwind, rather than Emotion/MUI - Added additional props to `SettingHeader` to help resolve issues with the component creating invalid HTML - Beefed up `SettingHeader` to have better out-of-the-box accessibility - Addressed some typographic problems in `SettingHeader` - Addressed some responsive layout problems for `SettingsHeader` - Added first-ever stories for `SettingsHeader` ## Notes - There are still a few headers that aren't using `SettingHeader` yet. There were some UI edge cases that meant I couldn't reliably bring it in without consulting the Design team first. I'm a little less worried about them, because they at least *look* like the other headers, but it'd be nice if we could centralize everything in a followup PR
1 parent 1e11e82 commit fcac4ab

File tree

27 files changed

+556
-246
lines changed

27 files changed

+556
-246
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { docs } from "utils/docs";
3+
import {
4+
SettingsHeader,
5+
SettingsHeaderDescription,
6+
SettingsHeaderDocsLink,
7+
SettingsHeaderTitle,
8+
} from "./SettingsHeader";
9+
10+
const meta: Meta<typeof SettingsHeader> = {
11+
title: "components/SettingsHeader",
12+
component: SettingsHeader,
13+
};
14+
15+
export default meta;
16+
type Story = StoryObj<typeof SettingsHeader>;
17+
18+
export const PrimaryHeaderOnly: Story = {
19+
args: {
20+
children: <SettingsHeaderTitle>This is a header</SettingsHeaderTitle>,
21+
},
22+
};
23+
24+
export const PrimaryHeaderWithDescription: Story = {
25+
args: {
26+
children: (
27+
<>
28+
<SettingsHeaderTitle>Another primary header</SettingsHeaderTitle>
29+
<SettingsHeaderDescription>
30+
This description can be any ReactNode. This provides more options for
31+
composition.
32+
</SettingsHeaderDescription>
33+
</>
34+
),
35+
},
36+
};
37+
38+
export const PrimaryHeaderWithDescriptionAndDocsLink: Story = {
39+
args: {
40+
children: (
41+
<>
42+
<SettingsHeaderTitle>Another primary header</SettingsHeaderTitle>
43+
<SettingsHeaderDescription>
44+
This description can be any ReactNode. This provides more options for
45+
composition.
46+
</SettingsHeaderDescription>
47+
</>
48+
),
49+
actions: <SettingsHeaderDocsLink href={docs("/admin/external-auth")} />,
50+
},
51+
};
52+
53+
export const SecondaryHeaderWithDescription: Story = {
54+
args: {
55+
children: (
56+
<>
57+
<SettingsHeaderTitle level="h6" hierarchy="secondary">
58+
This is a secondary header.
59+
</SettingsHeaderTitle>
60+
<SettingsHeaderDescription>
61+
The header's styling is completely independent of its semantics. Both
62+
can be adjusted independently to help avoid invalid HTML.
63+
</SettingsHeaderDescription>
64+
</>
65+
),
66+
},
67+
};
68+
69+
export const SecondaryHeaderWithDescriptionAndDocsLink: Story = {
70+
args: {
71+
children: (
72+
<>
73+
<SettingsHeaderTitle level="h3" hierarchy="secondary">
74+
Another secondary header
75+
</SettingsHeaderTitle>
76+
<SettingsHeaderDescription>
77+
Nothing to add, really.
78+
</SettingsHeaderDescription>
79+
</>
80+
),
81+
actions: <SettingsHeaderDocsLink href={docs("/admin/external-auth")} />,
82+
},
83+
};
Lines changed: 97 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,107 @@
1-
import { useTheme } from "@emotion/react";
1+
import { type VariantProps, cva } from "class-variance-authority";
22
import { Button } from "components/Button/Button";
3-
import { Stack } from "components/Stack/Stack";
43
import { SquareArrowOutUpRightIcon } from "lucide-react";
5-
import type { FC, ReactNode } from "react";
4+
import type { FC, PropsWithChildren, ReactNode } from "react";
5+
import { cn } from "utils/cn";
66

7-
interface HeaderProps {
8-
title: ReactNode;
9-
description?: ReactNode;
10-
secondary?: boolean;
11-
docsHref?: string;
12-
tooltip?: ReactNode;
13-
}
14-
15-
export const SettingsHeader: FC<HeaderProps> = ({
16-
title,
17-
description,
18-
docsHref,
19-
secondary,
20-
tooltip,
7+
type SettingsHeaderProps = Readonly<
8+
PropsWithChildren<{
9+
actions?: ReactNode;
10+
className?: string;
11+
}>
12+
>;
13+
export const SettingsHeader: FC<SettingsHeaderProps> = ({
14+
children,
15+
actions,
16+
className,
2117
}) => {
22-
const theme = useTheme();
18+
return (
19+
<hgroup className="flex flex-col justify-between items-start gap-2 pb-6 sm:flex-row">
20+
{/*
21+
* The text-sm class is only meant to adjust the font size of
22+
* SettingsDescription, but we need to apply it here. That way,
23+
* text-sm combines with the max-w-prose class and makes sure
24+
* we have a predictable max width for the header + description by
25+
* default.
26+
*/}
27+
<div className={cn("text-sm max-w-prose", className)}>{children}</div>
28+
{actions}
29+
</hgroup>
30+
);
31+
};
2332

33+
type SettingsHeaderDocsLinkProps = Readonly<
34+
PropsWithChildren<{ href: string }>
35+
>;
36+
export const SettingsHeaderDocsLink: FC<SettingsHeaderDocsLinkProps> = ({
37+
href,
38+
children = "Read the docs",
39+
}) => {
2440
return (
25-
<Stack alignItems="baseline" direction="row" justifyContent="space-between">
26-
<div css={{ maxWidth: 420, marginBottom: 24 }}>
27-
<Stack direction="row" spacing={1} alignItems="center">
28-
<h1
29-
css={[
30-
{
31-
fontSize: 32,
32-
fontWeight: 700,
33-
display: "flex",
34-
alignItems: "baseline",
35-
lineHeight: "initial",
36-
margin: 0,
37-
marginBottom: 4,
38-
gap: 8,
39-
},
40-
secondary && {
41-
fontSize: 24,
42-
fontWeight: 500,
43-
},
44-
]}
45-
>
46-
{title}
47-
</h1>
48-
{tooltip}
49-
</Stack>
41+
<Button asChild variant="outline">
42+
<a href={href} target="_blank" rel="noreferrer">
43+
<SquareArrowOutUpRightIcon />
44+
{children}
45+
<span className="sr-only"> (link opens in new tab)</span>
46+
</a>
47+
</Button>
48+
);
49+
};
5050

51-
{description && (
52-
<span
53-
css={{
54-
fontSize: 14,
55-
color: theme.palette.text.secondary,
56-
lineHeight: "160%",
57-
}}
58-
>
59-
{description}
60-
</span>
61-
)}
62-
</div>
51+
const titleVariants = cva("m-0 pb-1 flex items-center gap-2 leading-tight", {
52+
variants: {
53+
hierarchy: {
54+
primary: "text-3xl font-bold",
55+
secondary: "text-2xl font-medium",
56+
},
57+
},
58+
defaultVariants: {
59+
hierarchy: "primary",
60+
},
61+
});
62+
type SettingsHeaderTitleProps = Readonly<
63+
PropsWithChildren<
64+
VariantProps<typeof titleVariants> & {
65+
level?: `h${1 | 2 | 3 | 4 | 5 | 6}`;
66+
tooltip?: ReactNode;
67+
className?: string;
68+
}
69+
>
70+
>;
71+
export const SettingsHeaderTitle: FC<SettingsHeaderTitleProps> = ({
72+
children,
73+
tooltip,
74+
className,
75+
level = "h1",
76+
hierarchy = "primary",
77+
}) => {
78+
// Explicitly not using Radix's Slot component, because we don't want to
79+
// allow any arbitrary element to be composed into this. We specifically
80+
// only want to allow the six HTML headers. Anything else will likely result
81+
// in invalid markup
82+
const Title = level;
83+
return (
84+
<div className="flex flex-row gap-2 items-center">
85+
<Title className={cn(titleVariants({ hierarchy }), className)}>
86+
{children}
87+
</Title>
88+
{tooltip}
89+
</div>
90+
);
91+
};
6392

64-
{docsHref && (
65-
<Button asChild variant="outline">
66-
<a href={docsHref} target="_blank" rel="noreferrer">
67-
<SquareArrowOutUpRightIcon />
68-
Read the docs
69-
</a>
70-
</Button>
71-
)}
72-
</Stack>
93+
type SettingsHeaderDescriptionProps = Readonly<
94+
PropsWithChildren<{
95+
className?: string;
96+
}>
97+
>;
98+
export const SettingsHeaderDescription: FC<SettingsHeaderDescriptionProps> = ({
99+
children,
100+
className,
101+
}) => {
102+
return (
103+
<p className={cn("m-0 text-content-secondary leading-relaxed", className)}>
104+
{children}
105+
</p>
73106
);
74107
};

site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import {
88
} from "components/Badges/Badges";
99
import { Button } from "components/Button/Button";
1010
import { PopoverPaywall } from "components/Paywall/PopoverPaywall";
11-
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
11+
import {
12+
SettingsHeader,
13+
SettingsHeaderDescription,
14+
SettingsHeaderTitle,
15+
} from "components/SettingsHeader/SettingsHeader";
1216
import {
1317
Popover,
1418
PopoverContent,
@@ -54,10 +58,12 @@ export const AppearanceSettingsPageView: FC<
5458

5559
return (
5660
<>
57-
<SettingsHeader
58-
title="Appearance"
59-
description="Customize the look and feel of your Coder deployment."
60-
/>
61+
<SettingsHeader>
62+
<SettingsHeaderTitle>Appearance</SettingsHeaderTitle>
63+
<SettingsHeaderDescription>
64+
Customize the look and feel of your Coder deployment.
65+
</SettingsHeaderDescription>
66+
</SettingsHeader>
6167

6268
<Badges>
6369
<Popover mode="hover">

site/src/pages/DeploymentSettingsPage/ExternalAuthSettingsPage/ExternalAuthSettingsPageView.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import TableRow from "@mui/material/TableRow";
88
import type { DeploymentValues, ExternalAuthConfig } from "api/typesGenerated";
99
import { Alert } from "components/Alert/Alert";
1010
import { PremiumBadge } from "components/Badges/Badges";
11-
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
11+
import {
12+
SettingsHeader,
13+
SettingsHeaderDescription,
14+
SettingsHeaderDocsLink,
15+
SettingsHeaderTitle,
16+
} from "components/SettingsHeader/SettingsHeader";
1217
import type { FC } from "react";
1318
import { docs } from "utils/docs";
1419

@@ -22,10 +27,14 @@ export const ExternalAuthSettingsPageView: FC<
2227
return (
2328
<>
2429
<SettingsHeader
25-
title="External Authentication"
26-
description="Coder integrates with GitHub, GitLab, BitBucket, Azure Repos, and OpenID Connect to authenticate developers with external services."
27-
docsHref={docs("/admin/external-auth")}
28-
/>
30+
actions={<SettingsHeaderDocsLink href={docs("/admin/external-auth")} />}
31+
>
32+
<SettingsHeaderTitle>External Authentication</SettingsHeaderTitle>
33+
<SettingsHeaderDescription>
34+
Coder integrates with GitHub, GitLab, BitBucket, Azure Repos, and
35+
OpenID Connect to authenticate developers with external services.
36+
</SettingsHeaderDescription>
37+
</SettingsHeader>
2938

3039
<video
3140
autoPlay

site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/AddNewLicensePageView.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { ErrorAlert } from "components/Alert/ErrorAlert";
33
import { Button } from "components/Button/Button";
44
import { FileUpload } from "components/FileUpload/FileUpload";
55
import { displayError } from "components/GlobalSnackbar/utils";
6-
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
6+
import {
7+
SettingsHeader,
8+
SettingsHeaderDescription,
9+
SettingsHeaderTitle,
10+
} from "components/SettingsHeader/SettingsHeader";
711
import { Stack } from "components/Stack/Stack";
812
import { ChevronLeftIcon } from "lucide-react";
913
import type { FC } from "react";
@@ -50,10 +54,13 @@ export const AddNewLicensePageView: FC<AddNewLicenseProps> = ({
5054
direction="row"
5155
justifyContent="space-between"
5256
>
53-
<SettingsHeader
54-
title="Add a license"
55-
description="Get access to high availability, RBAC, quotas, and more."
56-
/>
57+
<SettingsHeader>
58+
<SettingsHeaderTitle>Add a license</SettingsHeaderTitle>
59+
<SettingsHeaderDescription>
60+
Get access to high availability, RBAC, quotas, and more.
61+
</SettingsHeaderDescription>
62+
</SettingsHeader>
63+
5764
<Button asChild variant="outline">
5865
<RouterLink to="/deployment/licenses">
5966
<ChevronLeftIcon />

site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import Skeleton from "@mui/material/Skeleton";
88
import Tooltip from "@mui/material/Tooltip";
99
import type { GetLicensesResponse } from "api/api";
1010
import type { UserStatusChangeCount } from "api/typesGenerated";
11-
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
11+
import {
12+
SettingsHeader,
13+
SettingsHeaderDescription,
14+
SettingsHeaderTitle,
15+
} from "components/SettingsHeader/SettingsHeader";
1216
import { Stack } from "components/Stack/Stack";
1317
import { useWindowSize } from "hooks/useWindowSize";
1418
import type { FC } from "react";
@@ -60,10 +64,12 @@ const LicensesSettingsPageView: FC<Props> = ({
6064
direction="row"
6165
justifyContent="space-between"
6266
>
63-
<SettingsHeader
64-
title="Licenses"
65-
description="Manage licenses to unlock Premium features."
66-
/>
67+
<SettingsHeader>
68+
<SettingsHeaderTitle>Licenses</SettingsHeaderTitle>
69+
<SettingsHeaderDescription>
70+
Manage licenses to unlock Premium features.
71+
</SettingsHeaderDescription>
72+
</SettingsHeader>
6773

6874
<Stack direction="row" spacing={2}>
6975
<Button

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy