|
1 |
| -import { useTheme } from "@emotion/react"; |
| 1 | +import { type VariantProps, cva } from "class-variance-authority"; |
2 | 2 | import { Button } from "components/Button/Button";
|
3 |
| -import { Stack } from "components/Stack/Stack"; |
4 | 3 | 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"; |
6 | 6 |
|
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, |
21 | 17 | }) => {
|
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 | +}; |
23 | 32 |
|
| 33 | +type SettingsHeaderDocsLinkProps = Readonly< |
| 34 | + PropsWithChildren<{ href: string }> |
| 35 | +>; |
| 36 | +export const SettingsHeaderDocsLink: FC<SettingsHeaderDocsLinkProps> = ({ |
| 37 | + href, |
| 38 | + children = "Read the docs", |
| 39 | +}) => { |
24 | 40 | 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 | +}; |
50 | 50 |
|
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 | +}; |
63 | 92 |
|
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> |
73 | 106 | );
|
74 | 107 | };
|
0 commit comments