feat: add card, divider and input

main
Juan Olvera 10 months ago
parent d16d06da3b
commit fa840257d3

@ -679,6 +679,22 @@ select {
--tw-backdrop-sepia: ;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.pointer-events-none {
pointer-events: none;
}
.absolute {
position: absolute;
}
@ -687,6 +703,11 @@ select {
position: relative;
}
.inset-y-0 {
top: 0px;
bottom: 0px;
}
.top-\[50\%\] {
top: 50%;
}
@ -695,6 +716,14 @@ select {
left: 50%;
}
.left-0 {
left: 0px;
}
.right-0 {
right: 0px;
}
.isolate {
isolation: isolate;
}
@ -703,22 +732,58 @@ select {
margin: 0px;
}
.mt-1 {
margin-top: 0.25rem;
}
.box-border {
box-sizing: border-box;
}
.block {
display: block;
}
.flex {
display: flex;
}
.inline-flex {
display: inline-flex;
}
.h-full {
height: 100%;
}
.h-1 {
height: 0.25rem;
}
.h-0 {
height: 0px;
}
.h-\[2px\] {
height: 2px;
}
.w-full {
width: 100%;
}
.w-96 {
width: 24rem;
}
.min-w-0 {
min-width: 0px;
}
.max-w-xs {
max-width: 20rem;
}
.-translate-x-2\/4 {
--tw-translate-x: -50%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
@ -729,16 +794,6 @@ select {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
.items-center {
align-items: center;
}
@ -747,6 +802,10 @@ select {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.gap-x-2 {
-moz-column-gap: 0.5rem;
column-gap: 0.5rem;
@ -758,6 +817,32 @@ select {
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
}
.divide-y > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
}
.divide-y-2 > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(2px * calc(1 - var(--tw-divide-y-reverse)));
border-bottom-width: calc(2px * var(--tw-divide-y-reverse));
}
.divide-y-0 > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(0px * calc(1 - var(--tw-divide-y-reverse)));
border-bottom-width: calc(0px * var(--tw-divide-y-reverse));
}
.overflow-hidden {
overflow: hidden;
}
.text-ellipsis {
text-overflow: ellipsis;
}
.rounded-lg {
border-radius: 0.5rem;
}
@ -766,6 +851,22 @@ select {
border-width: 2px;
}
.border {
border-width: 1px;
}
.border-0 {
border-width: 0px;
}
.border-t {
border-top-width: 1px;
}
.border-b {
border-bottom-width: 1px;
}
.border-mono-border {
--tw-border-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-border-opacity));
@ -786,6 +887,41 @@ select {
background-color: rgb(39 39 42 / var(--tw-bg-opacity));
}
.bg-gray-400 {
--tw-bg-opacity: 1;
background-color: rgb(156 163 175 / var(--tw-bg-opacity));
}
.bg-gray-300 {
--tw-bg-opacity: 1;
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
}
.bg-mono-border {
--tw-bg-opacity: 1;
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
}
.p-0 {
padding: 0px;
}
.p-1 {
padding: 0.25rem;
}
.p-2 {
padding: 0.5rem;
}
.p-4 {
padding: 1rem;
}
.p-6 {
padding: 1.5rem;
}
.py-0\.5 {
padding-top: 0.125rem;
padding-bottom: 0.125rem;
@ -831,6 +967,42 @@ select {
padding-right: 0.75rem;
}
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.py-4 {
padding-top: 1rem;
padding-bottom: 1rem;
}
.px-0 {
padding-left: 0px;
padding-right: 0px;
}
.pr-12 {
padding-right: 3rem;
}
.pl-3 {
padding-left: 0.75rem;
}
.pr-3 {
padding-right: 0.75rem;
}
.text-right {
text-align: right;
}
.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
@ -841,10 +1013,19 @@ select {
line-height: 1.5rem;
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.font-semibold {
font-weight: 600;
}
.leading-6 {
line-height: 1.5rem;
}
.text-mono-primary {
--tw-text-opacity: 1;
color: rgb(39 39 42 / var(--tw-text-opacity));
@ -855,14 +1036,49 @@ select {
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.text-mono-text {
--tw-text-opacity: 1;
color: rgb(107 114 128 / var(--tw-text-opacity));
}
.opacity-0 {
opacity: 0;
}
.outline-none {
outline: 2px solid transparent;
outline-offset: 2px;
}
html {
font-size: 14px;
}
.placeholder\:text-mono-border::-moz-placeholder {
--tw-text-opacity: 1;
color: rgb(209 213 219 / var(--tw-text-opacity));
}
.placeholder\:text-mono-border::placeholder {
--tw-text-opacity: 1;
color: rgb(209 213 219 / var(--tw-text-opacity));
}
.after\:content-none::after {
--tw-content: none;
content: var(--tw-content);
}
.after\:content-\[\'\'\]::after {
--tw-content: '';
content: var(--tw-content);
}
.after\:content-\[\'\$\'\]::after {
--tw-content: '$';
content: var(--tw-content);
}
.hover\:bg-gray-100:hover {
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
@ -873,11 +1089,22 @@ html {
background-color: rgb(63 63 70 / var(--tw-bg-opacity));
}
.focus\:border-mono-primary:focus {
--tw-border-opacity: 1;
border-color: rgb(39 39 42 / var(--tw-border-opacity));
}
.focus\:outline-none:focus {
outline: 2px solid transparent;
outline-offset: 2px;
}
.focus\:ring-0:focus {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.focus-visible\:ring-2:focus-visible {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
@ -925,6 +1152,22 @@ html {
}
}
@media (min-width: 640px) {
.sm\:p-6 {
padding: 1.5rem;
}
.sm\:px-6 {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.sm\:text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
}
.\[\&\>\[data-slot\=icon\]\]\:-mx-0\.5>[data-slot=icon] {
margin-left: -0.125rem;
margin-right: -0.125rem;

@ -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";

@ -1,4 +1,5 @@
const colors = require("tailwindcss/colors");
const defaultTheme = require("tailwindcss/defaultTheme");
const Color = require("color");
// https://github.com/tailwindlabs/discuss/issues/392#issuecomment-559305633
@ -25,7 +26,10 @@ module.exports = {
DEFAULT: colors.gray["300"],
},
"mono-text": {
DEFAULT: colors.zinc["800"],
DEFAULT: colors.gray["500"],
},
"mono-rounded": {
DEFAULT: defaultTheme.borderRadius["lg"],
},
},
},

Loading…
Cancel
Save