feat: new components and a example
* add sidebar * add dashboard example * fix combobox selectionmain
parent
6314afe148
commit
f8a39cdb4e
@ -0,0 +1,6 @@
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
File diff suppressed because it is too large
Load Diff
@ -1,22 +1,59 @@
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
const base = [
|
||||
"block",
|
||||
const base = [""];
|
||||
|
||||
const command = [
|
||||
"bg-white",
|
||||
"flex",
|
||||
"h-full",
|
||||
"w-full",
|
||||
"flex-col",
|
||||
"overflow-hidden",
|
||||
"rounded-lg",
|
||||
"border-2",
|
||||
"border-mono-border",
|
||||
"py-1.5",
|
||||
"px-2",
|
||||
"text-left",
|
||||
"text-mono-text",
|
||||
"text-ellipsis",
|
||||
"placeholder:text-mono-border",
|
||||
];
|
||||
|
||||
const input = [
|
||||
"flex",
|
||||
"w-full",
|
||||
"rounded-md",
|
||||
"bg-transparent",
|
||||
"outline-none",
|
||||
"border-0",
|
||||
"placeholder:text-muted-foreground",
|
||||
"focus:outline-none",
|
||||
"focus:ring-0",
|
||||
"focus:border-mono-primary",
|
||||
"disabled:cursor-not-allowed",
|
||||
"disabled:opacity-50",
|
||||
];
|
||||
|
||||
const item = [
|
||||
"relative",
|
||||
"flex",
|
||||
"cursor-default",
|
||||
"select-none",
|
||||
"items-center",
|
||||
"px-2",
|
||||
"py-1",
|
||||
"outline-none",
|
||||
"rounded-sm",
|
||||
"aria-selected:bg-mono-primary",
|
||||
"aria-selected:text-white",
|
||||
"data-[disabled]:pointer-events-none",
|
||||
"data-[disabled]:opacity-50",
|
||||
];
|
||||
|
||||
const comboboxStyles = cva(base);
|
||||
const checkedIcon = ["opacity-0", "absolute", "right-2"];
|
||||
const comboboxStyles = cva(base, {
|
||||
variants: {
|
||||
hasError: {
|
||||
true: ["border-mono-error"],
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
hasError: false,
|
||||
},
|
||||
});
|
||||
|
||||
export { comboboxStyles };
|
||||
export { comboboxStyles, command, input, item, checkedIcon };
|
||||
|
@ -0,0 +1,30 @@
|
||||
import React from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Dropdown } from "./Dropdown";
|
||||
import { Button } from "../Button";
|
||||
import { cx } from "../../utils";
|
||||
|
||||
const meta: Meta<typeof Dropdown> = {
|
||||
title: "UI / Dropdown",
|
||||
component: Dropdown,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Dropdown>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<Dropdown
|
||||
options={[
|
||||
{ label: "Profile", value: "/profile" },
|
||||
{
|
||||
label: "Sign out",
|
||||
value: "/sign-out",
|
||||
onSelect: (value) => console.log("sign out!", value),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Button>Open</Button>
|
||||
</Dropdown>
|
||||
),
|
||||
};
|
@ -0,0 +1,43 @@
|
||||
const content = [
|
||||
"relative",
|
||||
"max-h-96",
|
||||
"min-w-[8rem]",
|
||||
"p-1",
|
||||
"border-2",
|
||||
"border-mono-border",
|
||||
"overflow-hidden",
|
||||
"rounded-lg",
|
||||
"bg-white",
|
||||
|
||||
// Animation
|
||||
"data-[state=open]:animate-in",
|
||||
"data-[state=closed]:animate-out",
|
||||
"data-[state=closed]:fade-out-0",
|
||||
"data-[state=open]:fade-in-0",
|
||||
"data-[state=closed]:zoom-out-95",
|
||||
"data-[state=open]:zoom-in-95",
|
||||
"data-[side=bottom]:slide-in-from-top-2",
|
||||
"data-[side=left]:slide-in-from-right-2",
|
||||
"data-[side=right]:slide-in-from-left-2",
|
||||
"data-[side=top]:slide-in-from-bottom-2",
|
||||
];
|
||||
const item = [
|
||||
"relative",
|
||||
"flex",
|
||||
"w-full",
|
||||
"cursor-default",
|
||||
"select-none",
|
||||
"items-center",
|
||||
"py-0.5",
|
||||
"px-2",
|
||||
"rounded-sm",
|
||||
"data-[disabled]:pointer-events-none",
|
||||
"data-[disabled]:text-mono-text/50",
|
||||
"outline-none",
|
||||
"focus:rounded-md",
|
||||
"focus:bg-mono-primary",
|
||||
"focus:text-white",
|
||||
"focus:border-mono-primary",
|
||||
];
|
||||
|
||||
export { item, content };
|
@ -0,0 +1,33 @@
|
||||
import * as React from "react";
|
||||
import * as $ from "@radix-ui/react-dropdown-menu";
|
||||
import * as T from "./Dropdown.types";
|
||||
import * as styles from "./Dropdown.styles";
|
||||
import { cx } from "../../utils";
|
||||
|
||||
const Dropdown = React.forwardRef<T.DropdownElement, T.DropdownProps>(
|
||||
({ children, options, ...props }, forwardedRef) => {
|
||||
return (
|
||||
<$.Root>
|
||||
<$.Trigger asChild>{children}</$.Trigger>
|
||||
<$.Content
|
||||
sideOffset={4}
|
||||
{...props}
|
||||
className={cx([styles.content, props.className])}
|
||||
ref={forwardedRef}
|
||||
>
|
||||
{options?.map(({ label, value, icon: Icon, href, onSelect }) => (
|
||||
<$.Item
|
||||
className={cx([styles.item])}
|
||||
onSelect={() => onSelect?.(href ?? value)}
|
||||
>
|
||||
{Icon && <Icon className={cx(["mr-2"])} />}
|
||||
{label}
|
||||
</$.Item>
|
||||
))}
|
||||
</$.Content>
|
||||
</$.Root>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export { Dropdown };
|
@ -0,0 +1,18 @@
|
||||
import * as React from "react";
|
||||
import { Primitive } from "@radix-ui/react-primitive";
|
||||
import { IconType } from "react-icons";
|
||||
|
||||
type DropdownOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
icon?: IconType;
|
||||
onSelect?: (value: string) => void;
|
||||
};
|
||||
|
||||
type DropdownElement = React.ElementRef<typeof Primitive.div>;
|
||||
|
||||
type DropdownProps = React.ComponentPropsWithoutRef<typeof Primitive.div> & {
|
||||
options: DropdownOption[];
|
||||
};
|
||||
|
||||
export type { DropdownElement, DropdownProps };
|
@ -0,0 +1 @@
|
||||
export * from "./Dropdown";
|
@ -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 { Flyout } from "./Flyout";
|
||||
import { cx } from "../../utils";
|
||||
import { Input } from "../Input";
|
||||
|
||||
const meta: Meta<typeof Flyout> = {
|
||||
title: "UI/Flyout",
|
||||
component: Flyout,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Flyout>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<Flyout>
|
||||
<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", "mt-auto"])}>
|
||||
<Button>Cancel</Button>
|
||||
<Button intent="primary">Save</Button>
|
||||
</Card.Footer>
|
||||
</Card.$>
|
||||
</Flyout>
|
||||
),
|
||||
};
|
@ -0,0 +1,71 @@
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
const description = ["text-mono-text"];
|
||||
|
||||
const overlay = [
|
||||
"fixed",
|
||||
"inset-0",
|
||||
"z-50",
|
||||
"bg-black/80",
|
||||
"data-[state=open]:animate-in",
|
||||
"data-[state=closed]:animate-out",
|
||||
"data-[state=closed]:fade-out-0",
|
||||
"data-[state=open]:fade-in-0",
|
||||
];
|
||||
|
||||
const base = [
|
||||
"fixed",
|
||||
"z-50",
|
||||
"gap-4",
|
||||
"bg-white",
|
||||
"shadow-lg",
|
||||
"transition",
|
||||
"ease-in-out",
|
||||
"group",
|
||||
"data-[state=open]:animate-in",
|
||||
"data-[state=closed]:animate-out",
|
||||
"data-[state=closed]:duration-300",
|
||||
"data-[state=open]:duration-500",
|
||||
];
|
||||
|
||||
const close = [
|
||||
"absolute",
|
||||
"top-3",
|
||||
"-right-10",
|
||||
"bg-transparent",
|
||||
"hover:bg-transparent",
|
||||
"text-xl",
|
||||
"text-gray-200",
|
||||
];
|
||||
|
||||
const flyoutStyles = cva(base, {
|
||||
variants: {
|
||||
side: {
|
||||
left: [
|
||||
"inset-y-0",
|
||||
"left-0",
|
||||
"h-full",
|
||||
"w-3/4",
|
||||
"border-r",
|
||||
"data-[state=closed]:slide-out-to-left",
|
||||
"data-[state=open]:slide-in-from-left",
|
||||
"sm:max-w-sm",
|
||||
],
|
||||
right: [
|
||||
"inset-y-0",
|
||||
"right-0",
|
||||
"h-full",
|
||||
"w-3/4",
|
||||
"border-l",
|
||||
"data-[state=closed]:slide-out-to-right",
|
||||
"data-[state=open]:slide-in-from-right",
|
||||
"sm:max-w-sm",
|
||||
],
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
});
|
||||
|
||||
export { flyoutStyles, description, overlay, close };
|
@ -0,0 +1,53 @@
|
||||
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 "./Flyout.types";
|
||||
import * as styles from "./Flyout.styles";
|
||||
import { Button } from "../Button";
|
||||
import { cx } from "../../utils";
|
||||
import { Box } from "../Box";
|
||||
import { FaTimes } from "react-icons/fa";
|
||||
|
||||
const Flyout = React.forwardRef<T.FlyoutElement, T.FlyoutProps>(
|
||||
({ trigger, side, children, ...props }, forwardedRef) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const classes = cx(styles.flyoutStyles({ side }), props.className);
|
||||
return (
|
||||
<$.Root open={open} onOpenChange={setOpen}>
|
||||
<$.Trigger asChild>
|
||||
{match(trigger)
|
||||
.with(P.not(P.nullish), (t) => t)
|
||||
.otherwise(() => (
|
||||
<Button>Open</Button>
|
||||
))}
|
||||
</$.Trigger>
|
||||
<$.Portal>
|
||||
<$.Overlay className={cx([styles.overlay])} />
|
||||
<$.Content {...props} className={classes}>
|
||||
<Box>{children}</Box>
|
||||
<$.Close asChild>
|
||||
<Button
|
||||
aria-label="Close"
|
||||
className={cx([
|
||||
styles.close,
|
||||
"border-0",
|
||||
"p-0",
|
||||
"animate",
|
||||
{
|
||||
"animate-in fade-in duration-300": open,
|
||||
"animate-out fade-out duration-300": !open,
|
||||
},
|
||||
])}
|
||||
>
|
||||
<FaTimes />
|
||||
</Button>
|
||||
</$.Close>
|
||||
</$.Content>
|
||||
</$.Portal>
|
||||
</$.Root>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export { Flyout };
|
@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import * as $ from "@radix-ui/react-dialog";
|
||||
import { VariantProps } from "class-variance-authority";
|
||||
import * as styles from "./Flyout.styles";
|
||||
|
||||
type FlyoutElement = React.ElementRef<typeof $.Content>;
|
||||
|
||||
type FlyoutProps = React.ComponentProps<typeof $.Content> &
|
||||
VariantProps<typeof styles.flyoutStyles> & {
|
||||
trigger?: React.ReactNode;
|
||||
};
|
||||
|
||||
export type { FlyoutElement, FlyoutProps };
|
@ -0,0 +1 @@
|
||||
export * from "./Flyout";
|
@ -0,0 +1,21 @@
|
||||
import * as React from "react";
|
||||
import { Box } from "../../components/Box";
|
||||
import { cx } from "../../utils";
|
||||
import { Header } from "./Header";
|
||||
|
||||
const Content = () => {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Box className={cx(["py-10"])} asChild>
|
||||
<main>
|
||||
<Box className={cx(["px-4", "sm:px-6", "lg:px-8"])}>
|
||||
<p className={cx(["text-mono-text"])}>Dashboard content</p>
|
||||
</Box>
|
||||
</main>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { Content };
|
@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Dashboard } from "./Dashboard";
|
||||
|
||||
const meta: Meta<typeof Dashboard> = {
|
||||
title: "Examples / Dashboard",
|
||||
component: Dashboard,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Dashboard>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <Dashboard />,
|
||||
};
|
||||
|
||||
Default.parameters = {
|
||||
layout: "fullscreen",
|
||||
};
|
@ -0,0 +1,32 @@
|
||||
import * as React from "react";
|
||||
import { Box } from "../../components/Box";
|
||||
import { cx } from "../../utils";
|
||||
import { Header } from "./Header";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
import { Content } from "./Content";
|
||||
import { Flyout } from "../../components/Flyout";
|
||||
|
||||
const Dashboard = () => {
|
||||
return (
|
||||
<Box className={cx(["flex", "flex-col"])}>
|
||||
<Box
|
||||
className={cx([
|
||||
"hidden",
|
||||
"lg:fixed",
|
||||
"lg:inset-y-0",
|
||||
"lg:z-50",
|
||||
"lg:flex",
|
||||
"lg:w-72",
|
||||
"lg:flex-col",
|
||||
])}
|
||||
>
|
||||
<Sidebar />
|
||||
</Box>
|
||||
<Box className={cx(["lg:pl-72"])}>
|
||||
<Content />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export { Dashboard };
|
@ -0,0 +1,83 @@
|
||||
import * as React from "react";
|
||||
import { FaCog } from "react-icons/fa";
|
||||
import { FaBars, FaDoorOpen, FaUserLarge } from "react-icons/fa6";
|
||||
import { Box } from "../../components/Box";
|
||||
import { cx } from "../../utils";
|
||||
import { Button } from "../../components/Button";
|
||||
import { Divider } from "../../components/Divider";
|
||||
import { Dropdown } from "../../components/Dropdown";
|
||||
import { Flyout } from "../../components/Flyout";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
|
||||
const userNav = [
|
||||
{
|
||||
label: "Your Profile",
|
||||
value: "/profile",
|
||||
icon: FaUserLarge,
|
||||
},
|
||||
{
|
||||
label: "Settings",
|
||||
value: "/settings",
|
||||
icon: FaCog,
|
||||
},
|
||||
{
|
||||
label: "Sign out",
|
||||
value: "/sign-out",
|
||||
icon: FaDoorOpen,
|
||||
},
|
||||
];
|
||||
|
||||
const Header = () => {
|
||||
return (
|
||||
<Box
|
||||
className={cx([
|
||||
"sticky",
|
||||
"top-0",
|
||||
"z-40",
|
||||
"flex",
|
||||
"h-16",
|
||||
"shrink-0",
|
||||
"items-center",
|
||||
"gap-x-4",
|
||||
"border-b-2",
|
||||
"border-mono-border",
|
||||
"bg-white",
|
||||
"px-4",
|
||||
"sm:gap-x-6",
|
||||
"sm:px-6",
|
||||
"lg:px-8",
|
||||
])}
|
||||
>
|
||||
<Box className={cx(["flex", "w-full"])}>
|
||||
<Flyout
|
||||
side="left"
|
||||
className={cx(["border-0"])}
|
||||
trigger={
|
||||
<Button intent="plain" aria-label="Open sidebar">
|
||||
<FaBars />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Sidebar />
|
||||
</Flyout>
|
||||
|
||||
<Divider
|
||||
className={cx(["ml-auto"])}
|
||||
border
|
||||
orientation="vertical"
|
||||
size="xl"
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Dropdown options={userNav}>
|
||||
<Button intent="plain">
|
||||
<FaUserLarge />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export { Header };
|
@ -0,0 +1,48 @@
|
||||
const base = [
|
||||
"flex",
|
||||
"grow",
|
||||
"flex-col",
|
||||
"gap-y-5",
|
||||
"overflow-y-auto",
|
||||
"border-r-2",
|
||||
"border-mono-border",
|
||||
"px-6",
|
||||
"pb-4",
|
||||
];
|
||||
|
||||
const logoContainer = ["flex", "h-16", "shrink-0", "items-center"];
|
||||
|
||||
const logo = ["h-8", "w-auto"];
|
||||
|
||||
const navItem = [
|
||||
"text-mono-primary",
|
||||
"hover:bg-mono-hover",
|
||||
"group",
|
||||
"flex",
|
||||
"gap-x-3",
|
||||
"rounded-md",
|
||||
"p-2",
|
||||
"leading-6",
|
||||
"font-semibold",
|
||||
"flex",
|
||||
"items-center",
|
||||
];
|
||||
|
||||
const listContainer = ["flex", "flex-1", "flex-col"];
|
||||
|
||||
const list = ["flex", "flex-col", "flex-1", "gap-y-1"];
|
||||
|
||||
const subList = ["-mx-2", "space-y-1"];
|
||||
|
||||
const icon = ["w-5", "h-5"];
|
||||
|
||||
export {
|
||||
base,
|
||||
logoContainer,
|
||||
logo,
|
||||
listContainer,
|
||||
list,
|
||||
subList,
|
||||
navItem,
|
||||
icon,
|
||||
};
|
@ -0,0 +1,52 @@
|
||||
import * as React from "react";
|
||||
import { FaHome, FaUsers, FaCog } from "react-icons/fa";
|
||||
import { Box } from "../../components/Box";
|
||||
import { cx } from "../../utils";
|
||||
import * as styles from "./Sidebar.styles";
|
||||
|
||||
const Sidebar = () => {
|
||||
return (
|
||||
<Box className={cx([styles.base])}>
|
||||
<Box className={cx([styles.logoContainer])}>
|
||||
<img
|
||||
className={cx([styles.logo])}
|
||||
src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=500"
|
||||
alt="Your Company"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box className={cx([styles.listContainer])} asChild>
|
||||
<nav>
|
||||
<ul role="list" className={cx([styles.list])}>
|
||||
<li>
|
||||
<ul role="list" className={cx([styles.subList])}>
|
||||
<li>
|
||||
<a className={cx([styles.navItem])} href="#">
|
||||
<FaHome className={cx([styles.icon])} />
|
||||
Dashboard
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a className={cx([styles.navItem])} href="#">
|
||||
<FaUsers className={cx([styles.icon])} />
|
||||
Team
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a className={cx([styles.navItem])} href="#">
|
||||
<FaCog className={cx([styles.icon])} />
|
||||
Projects
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export { Sidebar };
|
Loading…
Reference in New Issue