feat: new components and a example

* add sidebar
* add dashboard example
* fix combobox selection
main
Juan Olvera 9 months ago
parent 6314afe148
commit f8a39cdb4e

@ -2,6 +2,7 @@ module.exports = {
stories: [
"../src/components/**/*.stories.mdx",
"../src/components/**/*.stories.@(js|jsx|ts|tsx)",
"../src/examples/**/*.stories.@(js|jsx|ts|tsx)",
// "../src/examples/**/*.stories.mdx",
// "../src/examples/**/*.stories.@(js|jsx|ts|tsx)",
],
@ -19,6 +20,6 @@ module.exports = {
framework: {
name: "@storybook/react-webpack5",
options: {}
}
options: {},
},
};

@ -707,6 +707,10 @@ select {
position: relative;
}
.sticky {
position: sticky;
}
.inset-0 {
top: 0px;
right: 0px;
@ -727,6 +731,10 @@ select {
left: 50%;
}
.right-2 {
right: 0.5rem;
}
.left-0 {
left: 0px;
}
@ -751,22 +759,47 @@ select {
right: 0.75rem;
}
.right-2 {
right: 0.5rem;
.top-0 {
top: 0px;
}
.-right-5 {
right: -1.25rem;
}
.-right-10 {
right: -2.5rem;
}
.isolate {
isolation: isolate;
}
.z-40 {
z-index: 40;
}
.z-50 {
z-index: 50;
}
.m-0 {
margin: 0px;
}
.-mx-2 {
margin-left: -0.5rem;
margin-right: -0.5rem;
}
.mt-2 {
margin-top: 0.5rem;
}
.ml-auto {
margin-left: auto;
}
.mt-1 {
margin-top: 0.25rem;
}
@ -775,6 +808,14 @@ select {
margin-left: 0.5rem;
}
.mr-2 {
margin-right: 0.5rem;
}
.mt-auto {
margin-top: auto;
}
.box-border {
box-sizing: border-box;
}
@ -791,6 +832,10 @@ select {
display: inline-flex;
}
.hidden {
display: none;
}
.h-full {
height: 100%;
}
@ -799,8 +844,24 @@ select {
height: 1rem;
}
.h-\[1\.5px\] {
height: 1.5px;
.h-px {
height: 1px;
}
.h-6 {
height: 1.5rem;
}
.h-16 {
height: 4rem;
}
.h-8 {
height: 2rem;
}
.h-5 {
height: 1.25rem;
}
.max-h-96 {
@ -823,6 +884,26 @@ select {
width: 1rem;
}
.w-px {
width: 1px;
}
.w-6 {
width: 1.5rem;
}
.w-auto {
width: auto;
}
.w-5 {
width: 1.25rem;
}
.w-3\/4 {
width: 75%;
}
.min-w-0 {
min-width: 0px;
}
@ -831,10 +912,18 @@ select {
min-width: 8rem;
}
.flex-1 {
flex: 1 1 0%;
}
.shrink-0 {
flex-shrink: 0;
}
.grow {
flex-grow: 1;
}
.-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));
@ -885,17 +974,45 @@ select {
justify-content: space-between;
}
.gap-4 {
gap: 1rem;
}
.gap-x-2 {
-moz-column-gap: 0.5rem;
column-gap: 0.5rem;
}
.gap-x-4 {
-moz-column-gap: 1rem;
column-gap: 1rem;
}
.gap-y-5 {
row-gap: 1.25rem;
}
.gap-x-3 {
-moz-column-gap: 0.75rem;
column-gap: 0.75rem;
}
.gap-y-1 {
row-gap: 0.25rem;
}
.space-x-2 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-y-1 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0.25rem * var(--tw-space-y-reverse));
}
.divide-y > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
@ -918,6 +1035,10 @@ select {
overflow: hidden;
}
.overflow-y-auto {
overflow-y: auto;
}
.text-ellipsis {
text-overflow: ellipsis;
}
@ -950,6 +1071,22 @@ select {
border-width: 1px;
}
.border-b-2 {
border-bottom-width: 2px;
}
.border-r {
border-right-width: 1px;
}
.border-l {
border-left-width: 1px;
}
.border-r-2 {
border-right-width: 2px;
}
.border-mono-border {
--tw-border-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-border-opacity));
@ -960,6 +1097,10 @@ select {
border-color: rgb(39 39 42 / var(--tw-border-opacity));
}
.border-transparent {
border-color: transparent;
}
.border-mono-error {
--tw-border-opacity: 1;
border-color: rgb(239 68 68 / var(--tw-border-opacity));
@ -975,18 +1116,32 @@ select {
background-color: rgb(39 39 42 / var(--tw-bg-opacity));
}
.bg-mono-hover {
--tw-bg-opacity: 1;
background-color: rgb(229 231 235 / var(--tw-bg-opacity));
}
.bg-transparent {
background-color: transparent;
}
.bg-mono-border {
.bg-black {
--tw-bg-opacity: 1;
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
.bg-black {
.bg-gray-900 {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
}
.bg-black\/80 {
background-color: rgb(0 0 0 / 0.8);
}
.bg-gray-800 {
--tw-bg-opacity: 1;
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
}
.bg-opacity-50 {
@ -1005,6 +1160,10 @@ select {
padding: 0.5rem;
}
.p-3 {
padding: 0.75rem;
}
.p-4 {
padding: 1rem;
}
@ -1078,6 +1237,16 @@ select {
padding-right: 0px;
}
.py-10 {
padding-top: 2.5rem;
padding-bottom: 2.5rem;
}
.px-6 {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.pr-12 {
padding-right: 3rem;
}
@ -1090,6 +1259,14 @@ select {
padding-right: 0.75rem;
}
.pl-72 {
padding-left: 18rem;
}
.pb-4 {
padding-bottom: 1rem;
}
.text-left {
text-align: left;
}
@ -1122,6 +1299,10 @@ select {
font-weight: 600;
}
.font-normal {
font-weight: 400;
}
.font-bold {
font-weight: 700;
}
@ -1150,15 +1331,59 @@ select {
color: rgb(239 68 68 / var(--tw-text-opacity));
}
.text-gray-400 {
--tw-text-opacity: 1;
color: rgb(156 163 175 / var(--tw-text-opacity));
}
.text-gray-200 {
--tw-text-opacity: 1;
color: rgb(229 231 235 / var(--tw-text-opacity));
}
.opacity-0 {
opacity: 0;
}
.opacity-100 {
opacity: 1;
}
.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: 2px solid transparent;
outline-offset: 2px;
}
.transition {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.duration-300 {
transition-duration: 300ms;
}
.duration-500 {
transition-duration: 500ms;
}
.duration-200 {
transition-duration: 200ms;
}
.ease-in-out {
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes enter {
from {
opacity: var(--tw-enter-opacity, 1);
@ -1173,6 +1398,50 @@ select {
}
}
.animate-in {
animation-name: enter;
animation-duration: 150ms;
--tw-enter-opacity: initial;
--tw-enter-scale: initial;
--tw-enter-rotate: initial;
--tw-enter-translate-x: initial;
--tw-enter-translate-y: initial;
}
.animate-out {
animation-name: exit;
animation-duration: 150ms;
--tw-exit-opacity: initial;
--tw-exit-scale: initial;
--tw-exit-rotate: initial;
--tw-exit-translate-x: initial;
--tw-exit-translate-y: initial;
}
.fade-in {
--tw-enter-opacity: 0;
}
.fade-out {
--tw-exit-opacity: 0;
}
.duration-300 {
animation-duration: 300ms;
}
.duration-500 {
animation-duration: 500ms;
}
.duration-200 {
animation-duration: 200ms;
}
.ease-in-out {
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
html {
font-size: 14px;
}
@ -1197,6 +1466,40 @@ html {
background-color: rgb(63 63 70 / var(--tw-bg-opacity));
}
.hover\:bg-transparent:hover {
background-color: transparent;
}
.hover\:bg-gray-800:hover {
--tw-bg-opacity: 1;
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
}
.hover\:bg-gray-300:hover {
--tw-bg-opacity: 1;
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
}
.hover\:bg-mono-hover:hover {
--tw-bg-opacity: 1;
background-color: rgb(229 231 235 / var(--tw-bg-opacity));
}
.hover\:text-white:hover {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.hover\:text-mono-primary:hover {
--tw-text-opacity: 1;
color: rgb(39 39 42 / var(--tw-text-opacity));
}
.hover\:text-mono-text:hover {
--tw-text-opacity: 1;
color: rgb(107 114 128 / var(--tw-text-opacity));
}
.focus\:rounded-md:focus {
border-radius: 0.375rem;
}
@ -1276,6 +1579,14 @@ html {
outline-style: solid;
}
.data-\[state\=closed\]\:duration-300[data-state=closed] {
transition-duration: 300ms;
}
.data-\[state\=open\]\:duration-500[data-state=open] {
transition-duration: 500ms;
}
.data-\[state\=open\]\:animate-in[data-state=open] {
animation-name: enter;
animation-duration: 150ms;
@ -1328,6 +1639,66 @@ html {
--tw-enter-translate-y: 0.5rem;
}
.data-\[state\=closed\]\:slide-out-to-left[data-state=closed] {
--tw-exit-translate-x: -100%;
}
.data-\[state\=open\]\:slide-in-from-left[data-state=open] {
--tw-enter-translate-x: -100%;
}
.data-\[state\=closed\]\:slide-out-to-right[data-state=closed] {
--tw-exit-translate-x: 100%;
}
.data-\[state\=open\]\:slide-in-from-right[data-state=open] {
--tw-enter-translate-x: 100%;
}
.data-\[state\=closed\]\:duration-300[data-state=closed] {
animation-duration: 300ms;
}
.data-\[state\=open\]\:duration-500[data-state=open] {
animation-duration: 500ms;
}
.group[data-state=closed] .group-data-\[state\=closed\]\:duration-300 {
transition-duration: 300ms;
}
.group[data-state=open] .group-data-\[state\=open\]\:duration-500 {
transition-duration: 500ms;
}
.group[data-state=open] .group-data-\[state\=open\]\:animate-in {
animation-name: enter;
animation-duration: 150ms;
--tw-enter-opacity: initial;
--tw-enter-scale: initial;
--tw-enter-rotate: initial;
--tw-enter-translate-x: initial;
--tw-enter-translate-y: initial;
}
.group[data-state=closed] .group-data-\[state\=closed\]\:animate-out {
animation-name: exit;
animation-duration: 150ms;
--tw-exit-opacity: initial;
--tw-exit-scale: initial;
--tw-exit-rotate: initial;
--tw-exit-translate-x: initial;
--tw-exit-translate-y: initial;
}
.group[data-state=closed] .group-data-\[state\=closed\]\:duration-300 {
animation-duration: 300ms;
}
.group[data-state=open] .group-data-\[state\=open\]\:duration-500 {
animation-duration: 500ms;
}
@media (prefers-reduced-motion: no-preference) {
@keyframes spin {
to {
@ -1341,6 +1712,15 @@ html {
}
@media (min-width: 640px) {
.sm\:max-w-sm {
max-width: 24rem;
}
.sm\:gap-x-6 {
-moz-column-gap: 1.5rem;
column-gap: 1.5rem;
}
.sm\:px-6 {
padding-left: 1.5rem;
padding-right: 1.5rem;
@ -1376,3 +1756,43 @@ html {
margin-bottom: 0.25rem;
}
}
@media (min-width: 1024px) {
.lg\:fixed {
position: fixed;
}
.lg\:inset-y-0 {
top: 0px;
bottom: 0px;
}
.lg\:z-50 {
z-index: 50;
}
.lg\:flex {
display: flex;
}
.lg\:hidden {
display: none;
}
.lg\:w-72 {
width: 18rem;
}
.lg\:flex-col {
flex-direction: column;
}
.lg\:px-8 {
padding-left: 2rem;
padding-right: 2rem;
}
.lg\:pl-72 {
padding-left: 18rem;
}
}

@ -0,0 +1,6 @@
<style>
html,
body {
height: 100%;
}
</style>

2213
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -81,7 +81,7 @@
},
"dependencies": {
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-navigation-menu": "^1.1.4",
"@radix-ui/react-popover": "^1.0.7",

@ -23,6 +23,14 @@ export const Primary: Story = {
render: () => <Button intent="primary">Click me</Button>,
};
export const Secondary: Story = {
render: () => <Button intent="secondary">Click me</Button>,
};
export const Plain: Story = {
render: () => <Button intent="plain">Click me</Button>,
};
export const Sizes: Story = {
render: () => (
<Box className={cx(["space-x-2"])}>

@ -35,6 +35,10 @@ const primary = [
"text-white",
];
const secondary = ["bg-mono-hover", "hover:bg-transparent"];
const plain = ["bg-transparent", "border-transparent", "hover:bg-gray-100"];
const onlyIcon = ["rounded-full", "p-1"];
const buttonStyles = cva(base, {
@ -45,7 +49,9 @@ const buttonStyles = cva(base, {
intent: {
primary,
secondary,
DEFAULT,
plain,
},
size,
fullWidth: {

@ -4,13 +4,13 @@ import { Box } from "../Box";
import { Combobox } from "./Combobox";
import { cx } from "../../utils";
const meta: Meta<typeof Box> = {
const meta: Meta<typeof Combobox> = {
title: "UI/Combobox",
component: Box,
component: Combobox,
};
export default meta;
type Story = StoryObj<typeof Box>;
type Story = StoryObj<typeof Combobox>;
const animals = [
{ label: "Cat", value: "cat" },
@ -38,3 +38,35 @@ const animals = [
export const Default: Story = {
render: () => <Combobox label="Animals" options={animals} />,
};
export const WithHelpText: Story = {
render: () => (
<Combobox
label="Animals"
options={animals}
helpText="Please select an option."
/>
),
};
export const Loading: Story = {
render: () => (
<Combobox
label="Animals"
options={animals}
helpText="Please select an option."
isLoading
/>
),
};
export const WithError: Story = {
render: () => (
<Combobox
label="Animals"
options={animals}
hasError
helpText="Please select an option."
/>
),
};

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

@ -1,6 +1,7 @@
import * as _ from "lodash-es";
import * as React from "react";
import * as Popover from "@radix-ui/react-popover";
import { match, P } from "ts-pattern";
import {
Command,
CommandEmpty,
@ -9,35 +10,54 @@ import {
CommandItem,
CommandList,
} from "cmdk";
import * as T from "./Combobox.types";
import type * as T from "./Combobox.types";
import * as styles from "./Combobox.styles";
import { Divider } from "../Divider";
import { Label } from "../Common";
import { HelpText, Label } from "../Common";
import { Button } from "../Button";
import { Box } from "../Box";
import { Input } from "../Input";
import { cx } from "../../utils";
import { FaCheck, FaChevronDown, FaSistrix } from "react-icons/fa";
import { CgSpinner } from "react-icons/cg";
const Combobox = React.forwardRef<T.ComboboxElement, T.ComboboxProps>(
({ label, hideLabel, options, onSelect, defaultValue }, forwardedRef) => {
(
{
isLoading = false,
label,
hideLabel,
helpText,
options,
onSelect,
onUnselect,
hasError,
defaultValue,
...props
},
forwardedRef
) => {
const [open, setOpen] = React.useState(false);
const [selected, setSelected] = React.useState(defaultValue);
const handleOnSelect = (selectedValue: string) => {
console.log("selected value", selectedValue);
const classes = cx(styles.comboboxStyles({ hasError }), props.className);
const handleOnSelect = (selectedValue: string) => {
const option = options.find((option) => option.value === selectedValue);
console.log("selected option", option);
if (!_.isNil(option)) {
// If same option then unselect it.
if (option?.label === selected) {
// Unselect logic
setSelected(undefined);
onUnselect?.(option);
} else {
// Select logic
setSelected(option.label);
onSelect?.(option);
}
}
setOpen(false);
};
return (
@ -46,26 +66,37 @@ const Combobox = React.forwardRef<T.ComboboxElement, T.ComboboxProps>(
<Label className={cx([], { "sr-only": hideLabel })}>{label}</Label>
</Box>
<Popover.Trigger asChild>
<Button role="combobox" className={cx([""], { "mt-2": !hideLabel })}>
{label}
<FaChevronDown />
<Button
role="combobox"
className={cx(classes, ["font-normal"], { "mt-2": !hideLabel })}
>
{match([selected, props.placeholder])
// Use selected value if available
.with([P.not(P.nullish), P._], () => <span>{selected}</span>)
// Use placeholder if value is not available and placeholder is
.with([P.nullish, P.not(P.nullish)], () => (
<span>{props.placeholder}</span>
))
// Use default placeholder if value and placeholder are not available
.with(P.array(P.nullish), () => <span>Select option...</span>)
.exhaustive()}
{isLoading ? (
<CgSpinner
className={cx(["motion-safe:animate-spin", "ml-auto"])}
/>
) : (
<FaChevronDown />
)}
</Button>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content sideOffset={4} align="start">
<Command
className={cx([
"bg-white",
"flex",
"h-full",
"w-full",
"flex-col",
"overflow-hidden",
"rounded-lg",
"border-2",
"border-mono-border",
])}
>
<Popover.Content
sideOffset={4}
align="start"
{...props}
ref={forwardedRef}
>
<Command className={cx([styles.command])}>
<Box
className={cx(["flex", "items-center", "px-2"])}
cmdk-input-wrapper=""
@ -80,22 +111,8 @@ const Combobox = React.forwardRef<T.ComboboxElement, T.ComboboxProps>(
])}
/>
<CommandInput
// className={cx([styles.comboboxStyles()])}
placeholder="Search Option..."
ref={forwardedRef}
className={cx([
"flex",
"w-full",
"rounded-md",
"bg-transparent",
"outline-none",
"border-0",
"placeholder:text-muted-foreground",
"focus:outline-none",
"focus:ring-0",
"disabled:cursor-not-allowed",
"disabled:opacity-50",
])}
className={cx([styles.input])}
/>
</Box>
<Divider border size="sm" className={cx(["py-0"])} />
@ -114,9 +131,15 @@ const Combobox = React.forwardRef<T.ComboboxElement, T.ComboboxProps>(
{nestedOptions.map((nestedOption, nestedIndex) => (
<CommandItem
key={String(nestedOption.value)}
className={cx([styles.item])}
onSelect={handleOnSelect}
>
{nestedOption.label}
<FaCheck
className={cx([styles.checkedIcon], {
"opacity-100": selected === option.label,
})}
/>
</CommandItem>
))}
</CommandGroup>
@ -125,25 +148,16 @@ const Combobox = React.forwardRef<T.ComboboxElement, T.ComboboxProps>(
return (
<CommandGroup>
<CommandItem
className={cx([
"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",
])}
key={option.value}
className={cx([styles.item])}
onSelect={handleOnSelect}
>
{option.label}
<FaCheck
className={cx([styles.checkedIcon], {
"opacity-100": selected === option.label,
})}
/>
</CommandItem>
</CommandGroup>
);
@ -152,6 +166,11 @@ const Combobox = React.forwardRef<T.ComboboxElement, T.ComboboxProps>(
</Command>
</Popover.Content>
</Popover.Portal>
{match(helpText)
.with(P.not(P.nullish), (helpText) => (
<HelpText hasError={hasError}>{helpText}</HelpText>
))
.otherwise(() => null)}
</Popover.Root>
);
}

@ -1,5 +1,4 @@
import * as React from "react";
import { Input } from "../Input";
import { InputProps } from "../Common/Common.types";
type ComboboxOption = {
@ -7,26 +6,18 @@ type ComboboxOption = {
value: string | ComboboxOption[];
};
type ComboboxElement = React.ElementRef<typeof Input>;
type ComboboxElement = React.ElementRef<"div">;
type ComboboxProps = React.ComponentPropsWithoutRef<typeof Input> &
type ComboboxProps = React.ComponentPropsWithoutRef<"div"> &
InputProps & {
isLoading?: boolean;
options: ComboboxOption[];
fullWidth?: boolean;
allowCreate?: boolean;
onSelect?: (option: ComboboxOption) => void;
onUnselect?: (option: ComboboxOption) => void;
onCreate?: (option: ComboboxOption) => void;
// Updates the selected option when optimistic updates happen.
onSync?: (option: ComboboxOption) => void;
};
// type ComboboxItemElement = React.ElementRef<typeof ListBoxItem>;
//
// type ComboboxItemProps = React.ComponentPropsWithoutRef<typeof ListBoxItem>;
export {
ComboboxElement,
ComboboxProps,
// ComboboxItemElement,
// ComboboxItemProps,
};
export type { ComboboxElement, ComboboxProps, ComboboxOption };

@ -26,9 +26,19 @@ export const Default: Story = {
export const WithBorder: Story = {
render: () => (
<Box>
Hello world
<Divider border />
Hello world
<Box>
Hello world
<Divider border />
Hello world
</Box>
<Divider />
<Box className={cx(["flex"])}>
Hello world
<Divider border orientation="vertical" size="xl" />
Hello world
</Box>
</Box>
),
};

@ -1,20 +1,20 @@
import { cva } from "class-variance-authority";
const base = [""];
const base = ["block"];
const horizontal = ["px-0", "w-full"];
const vertical = ["py-0", "h-full"];
const horizontal = [""];
const vertical = [""];
const dividerStyles = cva(base, {
variants: {
size: {
sm: ["p-1"],
md: ["p-2"],
lg: ["p-4"],
xl: ["p-6"],
lg: ["p-3"],
xl: ["p-4"],
},
border: {
true: [],
true: [""],
},
orientation: {
horizontal,
@ -27,4 +27,4 @@ const dividerStyles = cva(base, {
},
});
export { dividerStyles };
export { dividerStyles, horizontal, vertical };

@ -3,23 +3,43 @@ 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";
import * as styles 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-[1.5px]"])} />
</div>
))
.otherwise(() => <div {...props} className={classes} ref={forwardedRef} />);
});
>(
(
{ children, border = false, orientation = "horizontal", size, ...props },
forwardedRef
) => {
const classes = cx(
styles.dividerStyles({ orientation, size, border }),
props.className
);
return match({ border })
.with({ border: true }, () => (
<Box
{...props}
className={cx([classes], {
"py-0": orientation === "vertical",
"px-0": orientation === "horizontal",
})}
>
<Box
className={cx(["border", "border-mono-border"], {
"w-full h-px": orientation === "horizontal",
"h-full w-px": orientation === "vertical",
})}
/>
</Box>
))
.otherwise(() => (
<div {...props} className={classes} ref={forwardedRef} />
));
}
);
// const horizontal = ["w-6", "h-px"];
// const vertical = ["h-6", "w-px"];
export { Divider };

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

@ -5,4 +5,5 @@ export { Divider } from "./components/Divider";
export { Input } from "./components/Input";
export { Modal } from "./components/Modal";
export { Select } from "./components/Select";
export { Dropdown } from "./components/Dropdown";
export { cx } from "./utils";

@ -12,7 +12,7 @@ module.exports = {
darkMode: "class",
content: [
"./src/components/**/*.{ts,tsx}",
// "./src/examples/**/*.{ts,tsx}",
"./src/examples/**/*.{ts,tsx}",
"./src/index.ts",
],
theme: {

Loading…
Cancel
Save