feat: add modal and icon button

main
Juan Olvera 11 months ago
parent a9f052f488
commit 91d26fb0af

@ -695,6 +695,10 @@ select {
pointer-events: none; pointer-events: none;
} }
.fixed {
position: fixed;
}
.absolute { .absolute {
position: absolute; position: absolute;
} }
@ -703,6 +707,13 @@ select {
position: relative; position: relative;
} }
.inset-0 {
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
}
.inset-y-0 { .inset-y-0 {
top: 0px; top: 0px;
bottom: 0px; bottom: 0px;
@ -724,10 +735,54 @@ select {
right: 0px; right: 0px;
} }
.right-4 {
right: 1rem;
}
.top-4 {
top: 1rem;
}
.top-1\/2 {
top: 50%;
}
.left-1\/2 {
left: 50%;
}
.top-1 {
top: 0.25rem;
}
.right-2 {
right: 0.5rem;
}
.right-1 {
right: 0.25rem;
}
.top-2 {
top: 0.5rem;
}
.right-3 {
right: 0.75rem;
}
.top-3 {
top: 0.75rem;
}
.isolate { .isolate {
isolation: isolate; isolation: isolate;
} }
.z-50 {
z-index: 50;
}
.m-0 { .m-0 {
margin: 0px; margin: 0px;
} }
@ -756,6 +811,10 @@ select {
display: inline-flex; display: inline-flex;
} }
.grid {
display: grid;
}
.h-full { .h-full {
height: 100%; height: 100%;
} }
@ -772,10 +831,18 @@ select {
width: 24rem; width: 24rem;
} }
.w-56 {
width: 14rem;
}
.min-w-0 { .min-w-0 {
min-width: 0px; min-width: 0px;
} }
.max-w-lg {
max-width: 32rem;
}
.-translate-x-2\/4 { .-translate-x-2\/4 {
--tw-translate-x: -50%; --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)); 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));
@ -786,6 +853,30 @@ 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)); 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));
} }
.translate-x-\[-50\%\] {
--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));
}
.translate-y-\[-50\%\] {
--tw-translate-y: -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));
}
.-translate-x-1\/2 {
--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));
}
.-translate-y-1\/2 {
--tw-translate-y: -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));
}
.transform {
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));
}
.items-center { .items-center {
align-items: center; align-items: center;
} }
@ -798,6 +889,10 @@ select {
justify-content: space-between; justify-content: space-between;
} }
.gap-4 {
gap: 1rem;
}
.gap-x-2 { .gap-x-2 {
-moz-column-gap: 0.5rem; -moz-column-gap: 0.5rem;
column-gap: 0.5rem; column-gap: 0.5rem;
@ -839,6 +934,18 @@ select {
border-radius: 0.5rem; border-radius: 0.5rem;
} }
.rounded-sm {
border-radius: 0.125rem;
}
.rounded-md {
border-radius: 0.375rem;
}
.rounded-full {
border-radius: 9999px;
}
.border-2 { .border-2 {
border-width: 2px; border-width: 2px;
} }
@ -847,6 +954,10 @@ select {
border-width: 1px; border-width: 1px;
} }
.border-0 {
border-width: 0px;
}
.border-mono-border { .border-mono-border {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-border-opacity)); border-color: rgb(209 213 219 / var(--tw-border-opacity));
@ -877,6 +988,23 @@ select {
background-color: rgb(209 213 219 / var(--tw-bg-opacity)); background-color: rgb(209 213 219 / var(--tw-bg-opacity));
} }
.bg-black\/80 {
background-color: rgb(0 0 0 / 0.8);
}
.bg-black {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
.bg-transparent {
background-color: transparent;
}
.bg-opacity-50 {
--tw-bg-opacity: 0.5;
}
.p-0 { .p-0 {
padding: 0px; padding: 0px;
} }
@ -997,6 +1125,11 @@ select {
line-height: 1.75rem; line-height: 1.75rem;
} }
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
}
.font-semibold { .font-semibold {
font-weight: 600; font-weight: 600;
} }
@ -1029,11 +1162,31 @@ select {
opacity: 0; opacity: 0;
} }
.opacity-70 {
opacity: 0.7;
}
.shadow-lg {
--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.outline-none { .outline-none {
outline: 2px solid transparent; outline: 2px solid transparent;
outline-offset: 2px; outline-offset: 2px;
} }
.transition-opacity {
transition-property: opacity;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.duration-200 {
transition-duration: 200ms;
}
html { html {
font-size: 14px; font-size: 14px;
} }
@ -1058,6 +1211,10 @@ html {
background-color: rgb(63 63 70 / var(--tw-bg-opacity)); background-color: rgb(63 63 70 / var(--tw-bg-opacity));
} }
.hover\:opacity-100:hover {
opacity: 1;
}
.focus\:border-mono-primary:focus { .focus\:border-mono-primary:focus {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(39 39 42 / var(--tw-border-opacity)); border-color: rgb(39 39 42 / var(--tw-border-opacity));
@ -1074,6 +1231,16 @@ html {
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
} }
.focus\:ring-2: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(2px + 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\:ring-offset-2:focus {
--tw-ring-offset-width: 2px;
}
.focus-visible\:ring-2:focus-visible { .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-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); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
@ -1089,6 +1256,10 @@ html {
--tw-ring-offset-width: 2px; --tw-ring-offset-width: 2px;
} }
.disabled\:pointer-events-none:disabled {
pointer-events: none;
}
.data-\[disabled\]\:opacity-50[data-disabled] { .data-\[disabled\]\:opacity-50[data-disabled] {
opacity: 0.5; opacity: 0.5;
} }
@ -1122,6 +1293,10 @@ html {
} }
@media (min-width: 640px) { @media (min-width: 640px) {
.sm\:rounded-lg {
border-radius: 0.5rem;
}
.sm\:px-6 { .sm\:px-6 {
padding-left: 1.5rem; padding-left: 1.5rem;
padding-right: 1.5rem; padding-right: 1.5rem;

1745
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -77,15 +77,17 @@
"webpack": "^5.77.0" "webpack": "^5.77.0"
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.4", "@radix-ui/react-dropdown-menu": "^2.0.4",
"@radix-ui/react-navigation-menu": "^1.1.4", "@radix-ui/react-navigation-menu": "^1.1.4",
"@radix-ui/react-primitive": "^1.0.3", "@radix-ui/react-primitive": "^1.0.3",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toggle": "^1.0.3", "@radix-ui/react-toggle": "^1.0.3",
"@react-spring/web": "^9.7.1", "@react-spring/web": "^9.7.1",
"@tanstack/react-table": "^8.8.5", "@tanstack/react-table": "^8.8.5",
"dashify": "^2.0.0", "dashify": "^2.0.0",
"react-icons": "^4.7.1", "react-icons": "^4.12.0",
"react-number-format": "^5.1.4", "react-number-format": "^5.1.4",
"ts-pattern": "^5.0.6", "ts-pattern": "^5.0.6",
"zustand": "^4.3.7" "zustand": "^4.3.7"

@ -1,6 +1,10 @@
import React from "react"; import React from "react";
import type { Meta, StoryObj } from "@storybook/react"; import type { Meta, StoryObj } from "@storybook/react";
import { Box } from "./Box"; import { Box } from "./Box";
import { Input } from "../Input";
import { Button } from "../Button";
import { Divider } from "../Divider";
import { cx } from "../../utils";
const meta: Meta<typeof Box> = { const meta: Meta<typeof Box> = {
title: "UI/Box", title: "UI/Box",
@ -16,10 +20,18 @@ export const Default: Story = {
export const AsForm: Story = { export const AsForm: Story = {
render: () => ( render: () => (
<Box asChild> <Box asChild className={cx(["w-56"])}>
<form> <form
<input type="text" /> onSubmit={(event) => {
<button type="submit">Submit</button> event.preventDefault();
console.log("event", event);
}}
>
<Input label="Username" />
<Divider />
<Button type="submit" intent="primary">
Submit
</Button>
</form> </form>
</Box> </Box>
), ),

@ -49,6 +49,20 @@ export const WithIcon: Story = {
), ),
}; };
export const OnlyIcon: Story = {
render: () => (
<Box className={cx(["space-x-2"])}>
<Button onlyIcon aria-label="Click me">
<FaBeer />
</Button>
<Button intent="primary" onlyIcon aria-label="Click me">
<FaApple />
</Button>
</Box>
),
};
export const Loading: Story = { export const Loading: Story = {
render: () => ( render: () => (
<Box className={cx(["space-x-2"])}> <Box className={cx(["space-x-2"])}>

@ -5,7 +5,6 @@ const base = [
"relative isolate inline-flex items-center justify-center gap-x-2 rounded-lg border-2 text-base/6 font-semibold", "relative isolate inline-flex items-center justify-center gap-x-2 rounded-lg border-2 text-base/6 font-semibold",
// Focus // Focus
// "focus:outline-none data-[focus]:outline data-[focus]:outline-2 data-[focus]:outline-offset-2 data-[focus]:outline-blue-500",
"focus:outline-none", "focus:outline-none",
"data-[focus]:outline", "data-[focus]:outline",
"focus-visible:ring-2", "focus-visible:ring-2",
@ -36,11 +35,14 @@ const primary = [
"text-white", "text-white",
]; ];
const onlyIcon = ["rounded-full", "p-1"];
const buttonStyles = cva(base, { const buttonStyles = cva(base, {
variants: { variants: {
primary: { primary: {
true: primary, true: primary,
}, },
intent: { intent: {
primary, primary,
DEFAULT, DEFAULT,
@ -52,6 +54,9 @@ const buttonStyles = cva(base, {
disabled: { disabled: {
true: ["data-[disabled]:opacity-50"], true: ["data-[disabled]:opacity-50"],
}, },
onlyIcon: {
true: onlyIcon,
},
}, },
defaultVariants: { defaultVariants: {
size: "md", size: "md",

@ -2,23 +2,23 @@ import React, { forwardRef } from "react";
import { match, P } from "ts-pattern"; import { match, P } from "ts-pattern";
import { Primitive } from "@radix-ui/react-primitive"; import { Primitive } from "@radix-ui/react-primitive";
import { CgSpinner } from "react-icons/cg"; import { CgSpinner } from "react-icons/cg";
import { buttonStyles } from "./Button.styles"; import * as styles from "./Button.styles";
import type * as T from "./Button.types"; import type * as T from "./Button.types";
import { cx } from "../../utils"; import { cx } from "../../utils";
const Button = forwardRef<T.ButtonElement, T.ButtonProps>( const Button = forwardRef<T.ButtonElement, T.ButtonProps>(
( (
{ size, intent, primary, loading = false, children, ...props }, { size, onlyIcon, intent, primary, loading = false, children, ...props },
forwardedRef forwardedRef
) => { ) => {
const classes = cx( const classes = cx(
buttonStyles({ size, intent, primary }), styles.buttonStyles({ size, intent, primary, onlyIcon }),
props.className props.className
); );
return ( return (
<Primitive.button {...props} className={classes} ref={forwardedRef}> <Primitive.button {...props} className={classes} ref={forwardedRef}>
{match({ loading }) {match({ loading, onlyIcon })
.with({ loading: true }, () => ( .with({ loading: true }, () => (
<> <>
<span <span
@ -35,6 +35,7 @@ const Button = forwardRef<T.ButtonElement, T.ButtonProps>(
<span className={cx(["opacity-0"])}>{children}</span> <span className={cx(["opacity-0"])}>{children}</span>
</> </>
)) ))
// .with({ onlyIcon: true }, () => <span>{children}</span>)
.otherwise(() => children)} .otherwise(() => children)}
</Primitive.button> </Primitive.button>
); );

@ -26,15 +26,15 @@ export const Default: Story = {
</Card.Header> </Card.Header>
<Card.Body> <Card.Body>
<form onSubmit={(event) => event.preventDefault()}> <form onSubmit={(event) => event.preventDefault()}>
<Input label="Name" fullWidth /> <Input label="Name" />
<Box className={cx(["py-2"])} /> <Box className={cx(["py-2"])} />
<Input label="Type" fullWidth /> <Input label="Type" />
<Box className={cx(["py-2"])} /> <Box className={cx(["py-2"])} />
<Input label="Balance" money fullWidth /> <Input label="Balance" money />
</form> </form>
</Card.Body> </Card.Body>
<Card.Footer className={cx(["flex", "justify-between"])}> <Card.Footer className={cx(["flex", "justify-between"])}>

@ -4,7 +4,6 @@ const base = [
"overflow-hidden", "overflow-hidden",
"rounded-lg", "rounded-lg",
"bg-white", "bg-white",
"border-2",
"divide-y", "divide-y",
"divide-y-2", "divide-y-2",
"text-mono-text", "text-mono-text",
@ -16,9 +15,14 @@ const cardStyles = cva(base, {
true: ["p-0"], true: ["p-0"],
false: ["px-4", "py-4", "sm:px-6", "divide-y-0"], false: ["px-4", "py-4", "sm:px-6", "divide-y-0"],
}, },
borderless: {
true: ["border-0"],
false: ["border-2"],
},
}, },
defaultVariants: { defaultVariants: {
paddless: false, paddless: false,
borderless: false,
}, },
}); });

@ -7,8 +7,8 @@ import { Header, Title, Description, Body, Footer } from "./components";
import { cx } from "../../utils"; import { cx } from "../../utils";
const Card = React.forwardRef<T.CardElement, T.CardProps>( const Card = React.forwardRef<T.CardElement, T.CardProps>(
({ children, paddless, ...props }, forwardedRef) => { ({ children, paddless, borderless = false, ...props }, forwardedRef) => {
const classes = cx(cardStyles({ paddless }), props.className); const classes = cx(cardStyles({ paddless, borderless }), props.className);
return ( return (
<Primitive.div {...props} className={classes} ref={forwardedRef}> <Primitive.div {...props} className={classes} ref={forwardedRef}>

@ -1,7 +1,7 @@
import * as React from "react"; import * as React from "react";
import { match, P } from "ts-pattern"; import { match, P } from "ts-pattern";
import * as T from "./Divider.types"; import * as T from "./Divider.types";
import { Box } from "../Box"; import { Modal } from "../Box";
import { cx } from "../../utils"; import { cx } from "../../utils";
import { dividerStyles } from "./Divider.styles"; import { dividerStyles } from "./Divider.styles";
@ -16,7 +16,7 @@ const Divider = React.forwardRef<
return match({ border }) return match({ border })
.with({ border: true }, () => ( .with({ border: true }, () => (
<div {...props} className={classes}> <div {...props} className={classes}>
<Box className={cx(["bg-mono-border", "h-[2px]"])} /> <Modal className={cx(["bg-mono-border", "h-[2px]"])} />
</div> </div>
)) ))
.otherwise(() => <div {...props} className={classes} ref={forwardedRef} />); .otherwise(() => <div {...props} className={classes} ref={forwardedRef} />);

@ -0,0 +1 @@
export * from "./Divider";

@ -0,0 +1,48 @@
import * as React from "react";
import type { Meta, StoryObj } from "@storybook/react";
import { Box } from "../Box";
import { Button } from "../Button";
import * as Card from "../Card";
import { Modal } from "./Modal";
import { cx } from "../../utils";
import { Input } from "../Input";
const meta: Meta<typeof Modal> = {
title: "UI/Modal",
component: Modal,
};
export default meta;
type Story = StoryObj<typeof Modal>;
export const Default: Story = {
render: () => (
<Modal>
<Card.$ paddless borderless>
<Card.Header>
<Card.Title>Hello, world.</Card.Title>
<Card.Description>This is a modal example.</Card.Description>
</Card.Header>
<Card.Body>
<form onSubmit={(event) => event.preventDefault()}>
<Input label="Name" />
<Box className={cx(["py-2"])} />
<Input label="Type" />
<Box className={cx(["py-2"])} />
<Input label="Balance" money />
</form>
</Card.Body>
<Card.Footer className={cx(["flex", "justify-between"])}>
<Button>Cancel</Button>
<Button intent="primary">Save</Button>
</Card.Footer>
</Card.$>
</Modal>
),
};

@ -0,0 +1,27 @@
import { cva } from "class-variance-authority";
const base = [""];
const title = ["text-lg", "font-semibold"];
const description = ["text-mono-text"];
const overlay = ["fixed", "inset-0", "bg-black", "bg-opacity-50"];
const content = [
"fixed",
"top-1/2",
"left-1/2",
"transform",
"-translate-x-1/2",
"-translate-y-1/2",
"bg-transparent",
"w-96",
"rounded-md",
];
const close = ["absolute", "top-3", "right-3", "text-xl"];
const modalStyles = cva(base);
export { modalStyles, title, description, overlay, content, close };

@ -0,0 +1,45 @@
import * as React from "react";
import { FaCircleXmark } from "react-icons/fa6";
import { match, P } from "ts-pattern";
import * as $ from "@radix-ui/react-dialog";
import * as T from "./Modal.types";
import * as styles from "./Modal.styles";
import { Button } from "../Button";
import { cx } from "../../utils";
import { Box } from "../Box";
const Modal = React.forwardRef<T.ModalElement, T.ModalProps>(
({ trigger, children, ...props }, forwardedRef) => {
return (
<$.Root>
<$.Trigger asChild>
{match(trigger)
.with(P.not(P.nullish), (t) => <>{t}</>)
.otherwise(() => (
<Button>Open</Button>
))}
</$.Trigger>
<$.Portal>
<$.Overlay className={cx([styles.overlay])} />
<$.Content
className={cx([styles.content, props.className])}
{...props}
>
<Box>{children}</Box>
<$.Close asChild>
<Button
aria-label="Close"
onlyIcon
className={cx([styles.close, "border-0", "p-0"])}
>
<FaCircleXmark />
</Button>
</$.Close>
</$.Content>
</$.Portal>
</$.Root>
);
}
);
export { Modal };

@ -0,0 +1,13 @@
import React from "react";
import * as $ from "@radix-ui/react-dialog";
import { VariantProps } from "class-variance-authority";
import { modalStyles } from "./Modal.styles";
type ModalElement = React.ElementRef<typeof $.Content>;
type ModalProps = React.ComponentProps<typeof $.Content> &
VariantProps<typeof modalStyles> & {
trigger?: React.ReactNode;
};
export type { ModalElement, ModalProps };

@ -0,0 +1 @@
export * from "./Modal";

@ -0,0 +1,8 @@
import * as React from "react";
import * as $ from "@radix-ui/react-select";
const Select = () => {
return <div>Select</div>;
};
export { Select };

@ -0,0 +1 @@
export * from "./Select";
Loading…
Cancel
Save