-
Notifications
You must be signed in to change notification settings - Fork 914
fix(site): standardize headers for Admin Settings page #16911
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
de69ee3
a1da1c1
e5d581b
9b4928b
a35a9ae
9872c5d
1fe0022
7d0903a
a512bf5
734f59d
49b9dfe
840d6df
f169c4e
2325982
99c2dbc
50eddd0
2b77784
e2523cb
7f53666
daf6da9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import type { Meta, StoryObj } from "@storybook/react"; | ||
import { docs } from "utils/docs"; | ||
import { | ||
SettingsHeader, | ||
SettingsHeaderDescription, | ||
SettingsHeaderDocsLink, | ||
SettingsHeaderTitle, | ||
} from "./SettingsHeader"; | ||
|
||
const meta: Meta<typeof SettingsHeader> = { | ||
title: "components/SettingsHeader", | ||
component: SettingsHeader, | ||
}; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof SettingsHeader>; | ||
|
||
export const PrimaryHeaderOnly: Story = { | ||
args: { | ||
children: <SettingsHeaderTitle>This is a header</SettingsHeaderTitle>, | ||
}, | ||
}; | ||
|
||
export const PrimaryHeaderWithDescription: Story = { | ||
args: { | ||
children: ( | ||
<> | ||
<SettingsHeaderTitle>Another primary header</SettingsHeaderTitle> | ||
<SettingsHeaderDescription> | ||
This description can be any ReactNode. This provides more options for | ||
composition. | ||
</SettingsHeaderDescription> | ||
</> | ||
), | ||
}, | ||
}; | ||
|
||
export const PrimaryHeaderWithDescriptionAndDocsLink: Story = { | ||
args: { | ||
children: ( | ||
<> | ||
<SettingsHeaderTitle>Another primary header</SettingsHeaderTitle> | ||
<SettingsHeaderDescription> | ||
This description can be any ReactNode. This provides more options for | ||
composition. | ||
</SettingsHeaderDescription> | ||
</> | ||
), | ||
actions: <SettingsHeaderDocsLink href={docs("/admin/external-auth")} />, | ||
}, | ||
}; | ||
|
||
export const SecondaryHeaderWithDescription: Story = { | ||
args: { | ||
children: ( | ||
<> | ||
<SettingsHeaderTitle level="h6" hierarchy="secondary"> | ||
This is a secondary header. | ||
</SettingsHeaderTitle> | ||
<SettingsHeaderDescription> | ||
The header's styling is completely independent of its semantics. Both | ||
can be adjusted independently to help avoid invalid HTML. | ||
</SettingsHeaderDescription> | ||
</> | ||
), | ||
}, | ||
}; | ||
|
||
export const SecondaryHeaderWithDescriptionAndDocsLink: Story = { | ||
args: { | ||
children: ( | ||
<> | ||
<SettingsHeaderTitle level="h3" hierarchy="secondary"> | ||
Another secondary header | ||
</SettingsHeaderTitle> | ||
<SettingsHeaderDescription> | ||
Nothing to add, really. | ||
</SettingsHeaderDescription> | ||
</> | ||
), | ||
actions: <SettingsHeaderDocsLink href={docs("/admin/external-auth")} />, | ||
}, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,74 +1,107 @@ | ||
import { useTheme } from "@emotion/react"; | ||
import { type VariantProps, cva } from "class-variance-authority"; | ||
import { Button } from "components/Button/Button"; | ||
import { Stack } from "components/Stack/Stack"; | ||
import { SquareArrowOutUpRightIcon } from "lucide-react"; | ||
import type { FC, ReactNode } from "react"; | ||
import type { FC, PropsWithChildren, ReactNode } from "react"; | ||
import { cn } from "utils/cn"; | ||
|
||
interface HeaderProps { | ||
title: ReactNode; | ||
description?: ReactNode; | ||
secondary?: boolean; | ||
docsHref?: string; | ||
tooltip?: ReactNode; | ||
} | ||
|
||
export const SettingsHeader: FC<HeaderProps> = ({ | ||
title, | ||
description, | ||
docsHref, | ||
secondary, | ||
tooltip, | ||
type SettingsHeaderProps = Readonly< | ||
PropsWithChildren<{ | ||
actions?: ReactNode; | ||
className?: string; | ||
}> | ||
>; | ||
export const SettingsHeader: FC<SettingsHeaderProps> = ({ | ||
children, | ||
actions, | ||
className, | ||
}) => { | ||
const theme = useTheme(); | ||
return ( | ||
<hgroup className="flex flex-col justify-between items-start gap-2 pb-6 sm:flex-row"> | ||
{/* | ||
* The text-sm class is only meant to adjust the font size of | ||
* SettingsDescription, but we need to apply it here. That way, | ||
* text-sm combines with the max-w-prose class and makes sure | ||
* we have a predictable max width for the header + description by | ||
* default. | ||
*/} | ||
<div className={cn("text-sm max-w-prose", className)}>{children}</div> | ||
{actions} | ||
</hgroup> | ||
); | ||
}; | ||
|
||
type SettingsHeaderDocsLinkProps = Readonly< | ||
PropsWithChildren<{ href: string }> | ||
>; | ||
export const SettingsHeaderDocsLink: FC<SettingsHeaderDocsLinkProps> = ({ | ||
href, | ||
children = "Read the docs", | ||
}) => { | ||
return ( | ||
<Stack alignItems="baseline" direction="row" justifyContent="space-between"> | ||
<div css={{ maxWidth: 420, marginBottom: 24 }}> | ||
<Stack direction="row" spacing={1} alignItems="center"> | ||
<h1 | ||
css={[ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was a big accessibility/HTML validity issue. We were using this component multiple times on the same page, but only one There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is why I decoupled the |
||
{ | ||
fontSize: 32, | ||
fontWeight: 700, | ||
display: "flex", | ||
alignItems: "baseline", | ||
lineHeight: "initial", | ||
margin: 0, | ||
marginBottom: 4, | ||
gap: 8, | ||
}, | ||
secondary && { | ||
fontSize: 24, | ||
fontWeight: 500, | ||
}, | ||
]} | ||
> | ||
{title} | ||
</h1> | ||
{tooltip} | ||
</Stack> | ||
<Button asChild variant="outline"> | ||
<a href={href} target="_blank" rel="noreferrer"> | ||
<SquareArrowOutUpRightIcon /> | ||
{children} | ||
<span className="sr-only"> (link opens in new tab)</span> | ||
</a> | ||
</Button> | ||
); | ||
}; | ||
|
||
{description && ( | ||
<span | ||
css={{ | ||
fontSize: 14, | ||
color: theme.palette.text.secondary, | ||
lineHeight: "160%", | ||
}} | ||
> | ||
{description} | ||
</span> | ||
)} | ||
</div> | ||
const titleVariants = cva("m-0 pb-1 flex items-center gap-2 leading-tight", { | ||
variants: { | ||
hierarchy: { | ||
primary: "text-3xl font-bold", | ||
secondary: "text-2xl font-medium", | ||
}, | ||
}, | ||
defaultVariants: { | ||
hierarchy: "primary", | ||
}, | ||
}); | ||
type SettingsHeaderTitleProps = Readonly< | ||
PropsWithChildren< | ||
VariantProps<typeof titleVariants> & { | ||
level?: `h${1 | 2 | 3 | 4 | 5 | 6}`; | ||
tooltip?: ReactNode; | ||
className?: string; | ||
} | ||
> | ||
>; | ||
export const SettingsHeaderTitle: FC<SettingsHeaderTitleProps> = ({ | ||
children, | ||
tooltip, | ||
className, | ||
level = "h1", | ||
hierarchy = "primary", | ||
}) => { | ||
// Explicitly not using Radix's Slot component, because we don't want to | ||
// allow any arbitrary element to be composed into this. We specifically | ||
// only want to allow the six HTML headers. Anything else will likely result | ||
// in invalid markup | ||
const Title = level; | ||
Comment on lines
+78
to
+81
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a comment to call out why the code isn't using |
||
return ( | ||
<div className="flex flex-row gap-2 items-center"> | ||
<Title className={cn(titleVariants({ hierarchy }), className)}> | ||
{children} | ||
</Title> | ||
{tooltip} | ||
</div> | ||
); | ||
}; | ||
|
||
{docsHref && ( | ||
<Button asChild variant="outline"> | ||
<a href={docsHref} target="_blank" rel="noreferrer"> | ||
<SquareArrowOutUpRightIcon /> | ||
Read the docs | ||
</a> | ||
</Button> | ||
)} | ||
</Stack> | ||
type SettingsHeaderDescriptionProps = Readonly< | ||
PropsWithChildren<{ | ||
className?: string; | ||
}> | ||
>; | ||
export const SettingsHeaderDescription: FC<SettingsHeaderDescriptionProps> = ({ | ||
children, | ||
className, | ||
}) => { | ||
return ( | ||
<p className={cn("m-0 text-content-secondary leading-relaxed", className)}> | ||
{children} | ||
</p> | ||
); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add a storybook for it covering the new variants?