diff --git a/.storybook/output.css b/.storybook/output.css index 7436d7b..3eed179 100644 --- a/.storybook/output.css +++ b/.storybook/output.css @@ -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; diff --git a/src/components/Card/Card.stories.tsx b/src/components/Card/Card.stories.tsx new file mode 100644 index 0000000..9325e3a --- /dev/null +++ b/src/components/Card/Card.stories.tsx @@ -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 = { + title: "UI/Card", + component: Card.$, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => ( + + + + Create account + + Manage all your transactions in one place. + + + +
event.preventDefault()}> + + + + + + + + + + +
+ + + + +
+
+ ), +}; + +// Another way to use this component alone is to import the named export +// import { Card } from './Card'; +export const Paddless: Story = { + render: () => ( + + +

Dummy content while we create the input components.

+
+
+ ), +}; diff --git a/src/components/Card/Card.styles.ts b/src/components/Card/Card.styles.ts new file mode 100644 index 0000000..4e5bca8 --- /dev/null +++ b/src/components/Card/Card.styles.ts @@ -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 }; diff --git a/src/components/Card/Card.tsx b/src/components/Card/Card.tsx new file mode 100644 index 0000000..f21b9af --- /dev/null +++ b/src/components/Card/Card.tsx @@ -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( + ({ children, paddless, ...props }, forwardedRef) => { + const classes = cx(cardStyles({ paddless }), props.className); + + return ( + + {children} + + ); + } +); + +// Root element +const $ = Card; + +export { $, Card, Header, Title, Body, Description, Footer }; diff --git a/src/components/Card/Card.types.ts b/src/components/Card/Card.types.ts new file mode 100644 index 0000000..3f44c72 --- /dev/null +++ b/src/components/Card/Card.types.ts @@ -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; + +type CardProps = Omit, "asChild"> & + VariantProps & { + paddless?: boolean; + }; + +export type { CardElement, CardProps }; diff --git a/src/components/Card/components/Body.tsx b/src/components/Card/components/Body.tsx new file mode 100644 index 0000000..bfc1065 --- /dev/null +++ b/src/components/Card/components/Body.tsx @@ -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 & React.PropsWithChildren<{}>; + +type Body = (props: BodyProps) => React.ReactElement | null; + +const Body: Body = ({ children, ...props }) => { + const classes = cx(styles, props.className); + return ( + + {children} + + ); +}; + +export { Body }; diff --git a/src/components/Card/components/Description.tsx b/src/components/Card/components/Description.tsx new file mode 100644 index 0000000..e882767 --- /dev/null +++ b/src/components/Card/components/Description.tsx @@ -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 {children}; +}; + +export { Description }; diff --git a/src/components/Card/components/Footer.tsx b/src/components/Card/components/Footer.tsx new file mode 100644 index 0000000..55016c9 --- /dev/null +++ b/src/components/Card/components/Footer.tsx @@ -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 & React.PropsWithChildren<{}>; + +type Footer = (props: FooterProps) => React.ReactElement | null; + +const Footer: Footer = ({ children, ...props }) => { + const classes = cx(styles, props.className); + return ( + + {children} + + ); +}; + +export { Footer }; diff --git a/src/components/Card/components/Header.tsx b/src/components/Card/components/Header.tsx new file mode 100644 index 0000000..b6a82fd --- /dev/null +++ b/src/components/Card/components/Header.tsx @@ -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; + +type HeaderProps = React.ComponentPropsWithoutRef & {}; + +const Header = React.forwardRef( + ({ children, ...props }, forwardRef) => { + const classes = cx(styles, props.className); + return ( + + {children} + + ); + } +); + +export { Header }; diff --git a/src/components/Card/components/Title.tsx b/src/components/Card/components/Title.tsx new file mode 100644 index 0000000..8034b77 --- /dev/null +++ b/src/components/Card/components/Title.tsx @@ -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; + +type TitleProps = React.ComponentPropsWithoutRef & + VariantProps & {}; + +const Title = React.forwardRef( + ({ children, ...props }, forwardRef) => { + const classes = cx(titleStyles(), props.className); + return ( + + {children} + + ); + } +); + +export { Title }; diff --git a/src/components/Card/components/index.ts b/src/components/Card/components/index.ts new file mode 100644 index 0000000..439216a --- /dev/null +++ b/src/components/Card/components/index.ts @@ -0,0 +1,5 @@ +export * from "./Header"; +export * from "./Title"; +export * from "./Body"; +export * from "./Description"; +export * from "./Footer"; diff --git a/src/components/Card/components/styles.ts b/src/components/Card/components/styles.ts new file mode 100644 index 0000000..e21b9bd --- /dev/null +++ b/src/components/Card/components/styles.ts @@ -0,0 +1,3 @@ +const styles = ["px-4", "py-4", "sm:px-6"]; + +export { styles }; diff --git a/src/components/Card/index.ts b/src/components/Card/index.ts new file mode 100644 index 0000000..24d3212 --- /dev/null +++ b/src/components/Card/index.ts @@ -0,0 +1 @@ +export * from "./Card"; diff --git a/src/components/Divider/Divider.stories.tsx b/src/components/Divider/Divider.stories.tsx new file mode 100644 index 0000000..139095f --- /dev/null +++ b/src/components/Divider/Divider.stories.tsx @@ -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 = { + title: "UI/Divider", + component: Divider, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => ( + + Hello world + + Hello world + + ), +}; + +export const WithBorder: Story = { + render: () => ( + + Hello world + + Hello world + + ), +}; + +export const OnCard: Story = { + render: () => ( + + +

+ The first rule of Fight Club is: You do not talk about Fight Club. +

+ +

The second rule of Fight Club is: Always bring cupcakes.

+
+
+ ), +}; diff --git a/src/components/Divider/Divider.styles.ts b/src/components/Divider/Divider.styles.ts new file mode 100644 index 0000000..d8b23f9 --- /dev/null +++ b/src/components/Divider/Divider.styles.ts @@ -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 }; diff --git a/src/components/Divider/Divider.tsx b/src/components/Divider/Divider.tsx new file mode 100644 index 0000000..e7eeea7 --- /dev/null +++ b/src/components/Divider/Divider.tsx @@ -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 }, () => ( +
+ +
+ )) + .otherwise(() =>
); +}); + +export { Divider }; diff --git a/src/components/Divider/Divider.types.ts b/src/components/Divider/Divider.types.ts new file mode 100644 index 0000000..fdc711a --- /dev/null +++ b/src/components/Divider/Divider.types.ts @@ -0,0 +1,10 @@ +import { VariantProps } from "class-variance-authority"; +import { dividerStyles } from "./Divider.styles"; +import { HTMLAttributes } from "react"; + +type DividerProps = VariantProps & + HTMLAttributes & { + border?: boolean; + }; + +export { DividerProps }; diff --git a/src/components/Divider/index.ts b/src/components/Divider/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/components/Input/Input.stories.tsx b/src/components/Input/Input.stories.tsx new file mode 100644 index 0000000..1f5f230 --- /dev/null +++ b/src/components/Input/Input.stories.tsx @@ -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 = { + title: "UI/Input", + component: Input, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => ( + + + + ), +}; diff --git a/src/components/Input/Input.styles.ts b/src/components/Input/Input.styles.ts new file mode 100644 index 0000000..96feaf4 --- /dev/null +++ b/src/components/Input/Input.styles.ts @@ -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 }; diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx new file mode 100644 index 0000000..638a9dd --- /dev/null +++ b/src/components/Input/Input.tsx @@ -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( + ( + { + 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 ( + + + + {match({ money }) + .with({ money: true }, () => ( + <> + + + {symbol} + + + + + + {currency} + + + + )) + .otherwise(() => ( + + ))} + + + ); + } +); + +export { Input }; diff --git a/src/components/Input/Input.types.ts b/src/components/Input/Input.types.ts new file mode 100644 index 0000000..4f50ce5 --- /dev/null +++ b/src/components/Input/Input.types.ts @@ -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, "size"> & + VariantProps & { + 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 }; diff --git a/src/components/Input/index.ts b/src/components/Input/index.ts new file mode 100644 index 0000000..be66d76 --- /dev/null +++ b/src/components/Input/index.ts @@ -0,0 +1 @@ +export * from "./Input"; diff --git a/tailwind.config.js b/tailwind.config.js index c453c60..35bccb6 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -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"], }, }, },