feat: add card, divider and input
parent
d16d06da3b
commit
fa840257d3
@ -0,0 +1,59 @@
|
|||||||
|
import React from "react";
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
import * as Card from "./Card";
|
||||||
|
import { Box } from "../Box";
|
||||||
|
import { Input } from "../Input";
|
||||||
|
import { Button } from "../Button";
|
||||||
|
import { cx } from "../../utils";
|
||||||
|
|
||||||
|
const meta: Meta<typeof Card.$> = {
|
||||||
|
title: "UI/Card",
|
||||||
|
component: Card.$,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof Card>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => (
|
||||||
|
<Box className={cx(["w-96"])}>
|
||||||
|
<Card.$ paddless>
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title>Create account</Card.Title>
|
||||||
|
<Card.Description>
|
||||||
|
Manage all your transactions in one place.
|
||||||
|
</Card.Description>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Body>
|
||||||
|
<form onSubmit={(event) => event.preventDefault()}>
|
||||||
|
<Input label="Name" fullWidth />
|
||||||
|
|
||||||
|
<Box className={cx(["py-2"])} />
|
||||||
|
|
||||||
|
<Input label="Type" fullWidth />
|
||||||
|
|
||||||
|
<Box className={cx(["py-2"])} />
|
||||||
|
|
||||||
|
<Input label="Balance" money fullWidth />
|
||||||
|
</form>
|
||||||
|
</Card.Body>
|
||||||
|
<Card.Footer className={cx(["flex", "justify-between"])}>
|
||||||
|
<Button>Cancel</Button>
|
||||||
|
<Button intent="primary">Save</Button>
|
||||||
|
</Card.Footer>
|
||||||
|
</Card.$>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Another way to use this component alone is to import the named export
|
||||||
|
// import { Card } from './Card';
|
||||||
|
export const Paddless: Story = {
|
||||||
|
render: () => (
|
||||||
|
<Box className={cx(["w-96"])}>
|
||||||
|
<Card.$>
|
||||||
|
<p>Dummy content while we create the input components.</p>
|
||||||
|
</Card.$>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
@ -0,0 +1,25 @@
|
|||||||
|
import { cva } from "class-variance-authority";
|
||||||
|
|
||||||
|
const base = [
|
||||||
|
"overflow-hidden",
|
||||||
|
"rounded-lg",
|
||||||
|
"bg-white",
|
||||||
|
"border-2",
|
||||||
|
"divide-y",
|
||||||
|
"divide-y-2",
|
||||||
|
"text-mono-text",
|
||||||
|
];
|
||||||
|
|
||||||
|
const cardStyles = cva(base, {
|
||||||
|
variants: {
|
||||||
|
paddless: {
|
||||||
|
true: ["p-0"],
|
||||||
|
false: ["px-4", "py-4", "sm:px-6", "divide-y-0"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
paddless: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export { cardStyles };
|
@ -0,0 +1,24 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Primitive } from "@radix-ui/react-primitive";
|
||||||
|
|
||||||
|
import * as T from "./Card.types";
|
||||||
|
import { cardStyles } from "./Card.styles";
|
||||||
|
import { Header, Title, Description, Body, Footer } from "./components";
|
||||||
|
import { cx } from "../../utils";
|
||||||
|
|
||||||
|
const Card = React.forwardRef<T.CardElement, T.CardProps>(
|
||||||
|
({ children, paddless, ...props }, forwardedRef) => {
|
||||||
|
const classes = cx(cardStyles({ paddless }), props.className);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Primitive.div {...props} className={classes} ref={forwardedRef}>
|
||||||
|
{children}
|
||||||
|
</Primitive.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Root element
|
||||||
|
const $ = Card;
|
||||||
|
|
||||||
|
export { $, Card, Header, Title, Body, Description, Footer };
|
@ -0,0 +1,13 @@
|
|||||||
|
import { ComponentPropsWithRef, ElementRef } from "react";
|
||||||
|
import { Primitive } from "@radix-ui/react-primitive";
|
||||||
|
import { VariantProps } from "class-variance-authority";
|
||||||
|
import { cardStyles } from "./Card.styles";
|
||||||
|
|
||||||
|
type CardElement = ElementRef<typeof Primitive.div>;
|
||||||
|
|
||||||
|
type CardProps = Omit<ComponentPropsWithRef<typeof Primitive.div>, "asChild"> &
|
||||||
|
VariantProps<typeof cardStyles> & {
|
||||||
|
paddless?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type { CardElement, CardProps };
|
@ -0,0 +1,20 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Box } from "../../Box";
|
||||||
|
import { cx } from "../../../utils";
|
||||||
|
import { styles } from "./styles";
|
||||||
|
import { HTMLAttributes } from "react";
|
||||||
|
|
||||||
|
type BodyProps = HTMLAttributes<HTMLDivElement> & React.PropsWithChildren<{}>;
|
||||||
|
|
||||||
|
type Body = (props: BodyProps) => React.ReactElement | null;
|
||||||
|
|
||||||
|
const Body: Body = ({ children, ...props }) => {
|
||||||
|
const classes = cx(styles, props.className);
|
||||||
|
return (
|
||||||
|
<Box {...props} className={classes}>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Body };
|
@ -0,0 +1,13 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Box } from "../../Box";
|
||||||
|
import { cx } from "../../../utils";
|
||||||
|
|
||||||
|
type Description = (
|
||||||
|
props: React.PropsWithChildren<{}>
|
||||||
|
) => React.ReactElement | null;
|
||||||
|
|
||||||
|
const Description: Description = ({ children }) => {
|
||||||
|
return <Box className={cx(["mt-1"])}>{children}</Box>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Description };
|
@ -0,0 +1,21 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Box } from "../../Box";
|
||||||
|
import { cx } from "../../../utils";
|
||||||
|
import { styles } from "./styles";
|
||||||
|
import { HTMLAttributes } from "react";
|
||||||
|
import { cva } from "class-variance-authority";
|
||||||
|
|
||||||
|
type FooterProps = HTMLAttributes<HTMLDivElement> & React.PropsWithChildren<{}>;
|
||||||
|
|
||||||
|
type Footer = (props: FooterProps) => React.ReactElement | null;
|
||||||
|
|
||||||
|
const Footer: Footer = ({ children, ...props }) => {
|
||||||
|
const classes = cx(styles, props.className);
|
||||||
|
return (
|
||||||
|
<Box {...props} className={classes}>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Footer };
|
@ -0,0 +1,21 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Primitive } from "@radix-ui/react-primitive";
|
||||||
|
import { styles } from "./styles";
|
||||||
|
import { cx } from "../../../utils";
|
||||||
|
|
||||||
|
type HTMLTableHeader = React.ElementRef<typeof Primitive.div>;
|
||||||
|
|
||||||
|
type HeaderProps = React.ComponentPropsWithoutRef<typeof Primitive.div> & {};
|
||||||
|
|
||||||
|
const Header = React.forwardRef<HTMLTableHeader, HeaderProps>(
|
||||||
|
({ children, ...props }, forwardRef) => {
|
||||||
|
const classes = cx(styles, props.className);
|
||||||
|
return (
|
||||||
|
<Primitive.div {...props} className={classes} ref={forwardRef}>
|
||||||
|
{children}
|
||||||
|
</Primitive.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export { Header };
|
@ -0,0 +1,26 @@
|
|||||||
|
import { Primitive } from "@radix-ui/react-primitive";
|
||||||
|
import * as React from "react";
|
||||||
|
import { cva, VariantProps } from "class-variance-authority";
|
||||||
|
import { cx } from "../../../utils";
|
||||||
|
|
||||||
|
const base = ["text-lg", "font-semibold", "leading-6", "text-mono-primary"];
|
||||||
|
|
||||||
|
const titleStyles = cva(base);
|
||||||
|
|
||||||
|
type TitleElement = React.ElementRef<typeof Primitive.h3>;
|
||||||
|
|
||||||
|
type TitleProps = React.ComponentPropsWithoutRef<typeof Primitive.h3> &
|
||||||
|
VariantProps<typeof titleStyles> & {};
|
||||||
|
|
||||||
|
const Title = React.forwardRef<TitleElement, TitleProps>(
|
||||||
|
({ children, ...props }, forwardRef) => {
|
||||||
|
const classes = cx(titleStyles(), props.className);
|
||||||
|
return (
|
||||||
|
<Primitive.h3 {...props} className={classes} ref={forwardRef}>
|
||||||
|
{children}
|
||||||
|
</Primitive.h3>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export { Title };
|
@ -0,0 +1,5 @@
|
|||||||
|
export * from "./Header";
|
||||||
|
export * from "./Title";
|
||||||
|
export * from "./Body";
|
||||||
|
export * from "./Description";
|
||||||
|
export * from "./Footer";
|
@ -0,0 +1,3 @@
|
|||||||
|
const styles = ["px-4", "py-4", "sm:px-6"];
|
||||||
|
|
||||||
|
export { styles };
|
@ -0,0 +1 @@
|
|||||||
|
export * from "./Card";
|
@ -0,0 +1,48 @@
|
|||||||
|
import React from "react";
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
import { Box } from "../Box";
|
||||||
|
import { Divider } from "./Divider";
|
||||||
|
import * as Card from "../Card";
|
||||||
|
import { cx } from "../../utils";
|
||||||
|
|
||||||
|
const meta: Meta<typeof Divider> = {
|
||||||
|
title: "UI/Divider",
|
||||||
|
component: Divider,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof Divider>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => (
|
||||||
|
<Box>
|
||||||
|
Hello world
|
||||||
|
<Divider />
|
||||||
|
Hello world
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithBorder: Story = {
|
||||||
|
render: () => (
|
||||||
|
<Box>
|
||||||
|
Hello world
|
||||||
|
<Divider border />
|
||||||
|
Hello world
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OnCard: Story = {
|
||||||
|
render: () => (
|
||||||
|
<Box className={cx(["lead"])}>
|
||||||
|
<Card.$>
|
||||||
|
<p>
|
||||||
|
The first rule of Fight Club is: You do not talk about Fight Club.
|
||||||
|
</p>
|
||||||
|
<Divider border />
|
||||||
|
<p>The second rule of Fight Club is: Always bring cupcakes.</p>
|
||||||
|
</Card.$>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
@ -0,0 +1,30 @@
|
|||||||
|
import { cva } from "class-variance-authority";
|
||||||
|
|
||||||
|
const base = [""];
|
||||||
|
|
||||||
|
const horizontal = ["px-0", "w-full"];
|
||||||
|
const vertical = ["py-0", "h-full"];
|
||||||
|
|
||||||
|
const dividerStyles = cva(base, {
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
sm: ["p-1"],
|
||||||
|
md: ["p-2"],
|
||||||
|
lg: ["p-4"],
|
||||||
|
xl: ["p-6"],
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
true: [],
|
||||||
|
},
|
||||||
|
orientation: {
|
||||||
|
horizontal,
|
||||||
|
vertical,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
size: "md",
|
||||||
|
orientation: "horizontal",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export { dividerStyles };
|
@ -0,0 +1,25 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { match, P } from "ts-pattern";
|
||||||
|
import * as T from "./Divider.types";
|
||||||
|
import { Box } from "../Box";
|
||||||
|
import { cx } from "../../utils";
|
||||||
|
import { dividerStyles } from "./Divider.styles";
|
||||||
|
|
||||||
|
const Divider = React.forwardRef<
|
||||||
|
HTMLDivElement | HTMLHRElement,
|
||||||
|
T.DividerProps
|
||||||
|
>(({ children, border = false, orientation, size, ...props }, forwardedRef) => {
|
||||||
|
const classes = cx(
|
||||||
|
dividerStyles({ orientation, size, border }),
|
||||||
|
props.className
|
||||||
|
);
|
||||||
|
return match({ border })
|
||||||
|
.with({ border: true }, () => (
|
||||||
|
<div {...props} className={classes}>
|
||||||
|
<Box className={cx(["bg-mono-border", "h-[2px]"])} />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
.otherwise(() => <div {...props} className={classes} ref={forwardedRef} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
export { Divider };
|
@ -0,0 +1,10 @@
|
|||||||
|
import { VariantProps } from "class-variance-authority";
|
||||||
|
import { dividerStyles } from "./Divider.styles";
|
||||||
|
import { HTMLAttributes } from "react";
|
||||||
|
|
||||||
|
type DividerProps = VariantProps<typeof dividerStyles> &
|
||||||
|
HTMLAttributes<HTMLDivElement> & {
|
||||||
|
border?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { DividerProps };
|
@ -0,0 +1,21 @@
|
|||||||
|
import React from "react";
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
import { Box } from "../Box";
|
||||||
|
import { Input } from "./Input";
|
||||||
|
import { cx } from "../../utils";
|
||||||
|
|
||||||
|
const meta: Meta<typeof Input> = {
|
||||||
|
title: "UI/Input",
|
||||||
|
component: Input,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof Input>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => (
|
||||||
|
<Box className={cx(["w-96"])}>
|
||||||
|
<Input label="name" />
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
@ -0,0 +1,29 @@
|
|||||||
|
import { cva } from "class-variance-authority";
|
||||||
|
|
||||||
|
const base = [
|
||||||
|
"block",
|
||||||
|
"rounded-lg",
|
||||||
|
"border-2",
|
||||||
|
"border-mono-border",
|
||||||
|
"py-1.5",
|
||||||
|
"px-2",
|
||||||
|
"text-mono-text",
|
||||||
|
"text-ellipsis",
|
||||||
|
"placeholder:text-mono-border",
|
||||||
|
"outline-none",
|
||||||
|
"focus:ring-0",
|
||||||
|
"focus:border-mono-primary",
|
||||||
|
];
|
||||||
|
|
||||||
|
const inputStyles = cva(base, {
|
||||||
|
variants: {
|
||||||
|
fullWidth: {
|
||||||
|
true: ["w-full"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
fullWidth: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export { inputStyles };
|
@ -0,0 +1,106 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import dashify from "dashify";
|
||||||
|
import { Primitive } from "@radix-ui/react-primitive";
|
||||||
|
import { NumericFormat } from "react-number-format";
|
||||||
|
import type * as T from "./Input.types";
|
||||||
|
import { Box } from "../Box";
|
||||||
|
import { cx } from "../../utils";
|
||||||
|
import { inputStyles } from "./Input.styles";
|
||||||
|
import { match } from "ts-pattern";
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, T.InputProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
label,
|
||||||
|
fullWidth,
|
||||||
|
money,
|
||||||
|
numericFormatOptions,
|
||||||
|
hideLabel = false,
|
||||||
|
symbol = "$",
|
||||||
|
currency = "USD",
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
forwardedRef
|
||||||
|
) => {
|
||||||
|
const htmlId = dashify(label);
|
||||||
|
const classes = cx(
|
||||||
|
inputStyles({ fullWidth }),
|
||||||
|
{
|
||||||
|
"pr-12": currency,
|
||||||
|
"text-right": money,
|
||||||
|
},
|
||||||
|
props.className
|
||||||
|
);
|
||||||
|
|
||||||
|
const moneyDecoratorsClasses = [
|
||||||
|
"pointer-events-none",
|
||||||
|
"absolute",
|
||||||
|
"inset-y-0",
|
||||||
|
"flex",
|
||||||
|
"items-center",
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
className={cx(["relative"], {
|
||||||
|
"w-full": fullWidth,
|
||||||
|
"max-w-xs": !fullWidth,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className={cx(
|
||||||
|
["text-sm", "font-semibold", "leading-6", "text-mono-primary"],
|
||||||
|
{
|
||||||
|
"sr-only": hideLabel,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
htmlFor={htmlId}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
<Box
|
||||||
|
className={cx(["relative"], {
|
||||||
|
"mt-1": !hideLabel,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{match({ money })
|
||||||
|
.with({ money: true }, () => (
|
||||||
|
<>
|
||||||
|
<Box className={cx(["left-0", "pl-3"], moneyDecoratorsClasses)}>
|
||||||
|
<span className={cx(["text-mono-primary", "sm:text-sm"])}>
|
||||||
|
{symbol}
|
||||||
|
</span>
|
||||||
|
</Box>
|
||||||
|
<NumericFormat
|
||||||
|
id={htmlId}
|
||||||
|
className={classes}
|
||||||
|
valueIsNumericString
|
||||||
|
{...numericFormatOptions}
|
||||||
|
{...props}
|
||||||
|
// prefix={"$"}
|
||||||
|
getInputRef={forwardedRef}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
className={cx(["right-0", "pr-3"], moneyDecoratorsClasses)}
|
||||||
|
>
|
||||||
|
<span className={cx(["text-mono-primary", "sm:text-sm"])}>
|
||||||
|
{currency}
|
||||||
|
</span>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
))
|
||||||
|
.otherwise(() => (
|
||||||
|
<input
|
||||||
|
{...props}
|
||||||
|
id={htmlId}
|
||||||
|
className={classes}
|
||||||
|
ref={forwardedRef}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export { Input };
|
@ -0,0 +1,24 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Primitive } from "@radix-ui/react-primitive";
|
||||||
|
import { VariantProps } from "class-variance-authority";
|
||||||
|
import { inputStyles } from "./Input.styles";
|
||||||
|
import { NumericFormatProps } from "react-number-format";
|
||||||
|
import { InputHTMLAttributes } from "react";
|
||||||
|
|
||||||
|
type InputProps = Omit<InputHTMLAttributes<HTMLInputElement>, "size"> &
|
||||||
|
VariantProps<typeof inputStyles> & {
|
||||||
|
label: string;
|
||||||
|
money?: boolean;
|
||||||
|
hideLabel?: boolean;
|
||||||
|
type?: NumericFormatProps["type"];
|
||||||
|
value?: HTMLInputElement["value"] | NumericFormatProps["value"];
|
||||||
|
defaultValue?:
|
||||||
|
| HTMLInputElement["defaultValue"]
|
||||||
|
| NumericFormatProps["defaultValue"];
|
||||||
|
onValueChange?: NumericFormatProps["onValueChange"];
|
||||||
|
numericFormatOptions?: NumericFormatProps;
|
||||||
|
symbol?: string;
|
||||||
|
currency?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { InputProps };
|
@ -0,0 +1 @@
|
|||||||
|
export * from "./Input";
|
Loading…
Reference in New Issue