coder/site/src/components/Form/Form.tsx

220 lines
4.6 KiB
TypeScript

import { type Interpolation, type Theme, useTheme } from "@emotion/react";
import {
type ComponentProps,
createContext,
type FC,
forwardRef,
type HTMLProps,
type ReactNode,
useContext,
} from "react";
import { AlphaBadge, DeprecatedBadge } from "components/Badges/Badges";
import { Stack } from "components/Stack/Stack";
import {
FormFooter as BaseFormFooter,
type FormFooterProps,
type FormFooterStyles,
} from "../FormFooter/FormFooter";
type FormContextValue = { direction?: "horizontal" | "vertical" };
const FormContext = createContext<FormContextValue>({
direction: "horizontal",
});
type FormProps = HTMLProps<HTMLFormElement> & {
direction?: FormContextValue["direction"];
};
export const Form: FC<FormProps> = ({ direction, ...formProps }) => {
const theme = useTheme();
return (
<FormContext.Provider value={{ direction }}>
<form
{...formProps}
css={{
display: "flex",
flexDirection: "column",
gap: direction === "horizontal" ? 80 : 40,
[theme.breakpoints.down("md")]: {
gap: 64,
},
}}
/>
</FormContext.Provider>
);
};
export const HorizontalForm: FC<HTMLProps<HTMLFormElement>> = ({
children,
...formProps
}) => {
return (
<Form direction="horizontal" {...formProps}>
{children}
</Form>
);
};
export const VerticalForm: FC<HTMLProps<HTMLFormElement>> = ({
children,
...formProps
}) => {
return (
<Form direction="vertical" {...formProps}>
{children}
</Form>
);
};
interface FormSectionProps {
children?: ReactNode;
title: ReactNode;
description: ReactNode;
classes?: {
root?: string;
sectionInfo?: string;
infoTitle?: string;
};
alpha?: boolean;
deprecated?: boolean;
}
export const FormSection = forwardRef<HTMLDivElement, FormSectionProps>(
(
{
children,
title,
description,
classes = {},
alpha = false,
deprecated = false,
},
ref,
) => {
const { direction } = useContext(FormContext);
return (
<section
ref={ref}
css={[
styles.formSection,
direction === "horizontal" && styles.formSectionHorizontal,
]}
className={classes.root}
>
<div
css={[
styles.formSectionInfo,
direction === "horizontal" && styles.formSectionInfoHorizontal,
]}
className={classes.sectionInfo}
>
<h2 css={styles.formSectionInfoTitle} className={classes.infoTitle}>
{title}
{alpha && <AlphaBadge />}
{deprecated && <DeprecatedBadge />}
</h2>
<div css={styles.formSectionInfoDescription}>{description}</div>
</div>
{children}
</section>
);
},
);
export const FormFields: FC<ComponentProps<typeof Stack>> = (props) => {
return (
<Stack
direction="column"
spacing={3}
{...props}
css={styles.formSectionFields}
/>
);
};
const styles = {
formSection: (theme) => ({
display: "flex",
alignItems: "flex-start",
flexDirection: "column",
gap: 24,
[theme.breakpoints.down("lg")]: {
flexDirection: "column",
gap: 16,
},
}),
formSectionHorizontal: {
flexDirection: "row",
gap: 120,
},
formSectionInfo: (theme) => ({
width: "100%",
flexShrink: 0,
top: 24,
[theme.breakpoints.down("md")]: {
width: "100%",
position: "initial" as const,
},
}),
formSectionInfoHorizontal: {
maxWidth: 312,
position: "sticky",
},
formSectionInfoTitle: (theme) => ({
fontSize: 20,
color: theme.palette.text.primary,
fontWeight: 400,
margin: 0,
marginBottom: 8,
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: 12,
}),
formSectionInfoDescription: (theme) => ({
fontSize: 14,
color: theme.palette.text.secondary,
lineHeight: "160%",
margin: 0,
}),
formSectionFields: {
width: "100%",
},
} satisfies Record<string, Interpolation<Theme>>;
export const FormFooter: FC<Exclude<FormFooterProps, "styles">> = (props) => (
<BaseFormFooter {...props} styles={footerStyles} />
);
const footerStyles = {
button: (theme) => ({
minWidth: 184,
[theme.breakpoints.down("md")]: {
width: "100%",
},
}),
footer: (theme) => ({
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
flexDirection: "row-reverse",
gap: 16,
[theme.breakpoints.down("md")]: {
flexDirection: "column",
gap: 8,
},
}),
} satisfies FormFooterStyles;