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