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

@ -707,6 +707,10 @@ select {
position: relative; position: relative;
} }
.sticky {
position: sticky;
}
.inset-0 { .inset-0 {
top: 0px; top: 0px;
right: 0px; right: 0px;
@ -727,6 +731,10 @@ select {
left: 50%; left: 50%;
} }
.right-2 {
right: 0.5rem;
}
.left-0 { .left-0 {
left: 0px; left: 0px;
} }
@ -751,22 +759,47 @@ select {
right: 0.75rem; right: 0.75rem;
} }
.right-2 { .top-0 {
right: 0.5rem; top: 0px;
}
.-right-5 {
right: -1.25rem;
}
.-right-10 {
right: -2.5rem;
} }
.isolate { .isolate {
isolation: isolate; isolation: isolate;
} }
.z-40 {
z-index: 40;
}
.z-50 {
z-index: 50;
}
.m-0 { .m-0 {
margin: 0px; margin: 0px;
} }
.-mx-2 {
margin-left: -0.5rem;
margin-right: -0.5rem;
}
.mt-2 { .mt-2 {
margin-top: 0.5rem; margin-top: 0.5rem;
} }
.ml-auto {
margin-left: auto;
}
.mt-1 { .mt-1 {
margin-top: 0.25rem; margin-top: 0.25rem;
} }
@ -775,6 +808,14 @@ select {
margin-left: 0.5rem; margin-left: 0.5rem;
} }
.mr-2 {
margin-right: 0.5rem;
}
.mt-auto {
margin-top: auto;
}
.box-border { .box-border {
box-sizing: border-box; box-sizing: border-box;
} }
@ -791,6 +832,10 @@ select {
display: inline-flex; display: inline-flex;
} }
.hidden {
display: none;
}
.h-full { .h-full {
height: 100%; height: 100%;
} }
@ -799,8 +844,24 @@ select {
height: 1rem; height: 1rem;
} }
.h-\[1\.5px\] { .h-px {
height: 1.5px; height: 1px;
}
.h-6 {
height: 1.5rem;
}
.h-16 {
height: 4rem;
}
.h-8 {
height: 2rem;
}
.h-5 {
height: 1.25rem;
} }
.max-h-96 { .max-h-96 {
@ -823,6 +884,26 @@ select {
width: 1rem; 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-w-0 {
min-width: 0px; min-width: 0px;
} }
@ -831,10 +912,18 @@ select {
min-width: 8rem; min-width: 8rem;
} }
.flex-1 {
flex: 1 1 0%;
}
.shrink-0 { .shrink-0 {
flex-shrink: 0; flex-shrink: 0;
} }
.grow {
flex-grow: 1;
}
.-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));
@ -885,17 +974,45 @@ 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;
} }
.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]) { .space-x-2 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0; --tw-space-x-reverse: 0;
margin-right: calc(0.5rem * var(--tw-space-x-reverse)); margin-right: calc(0.5rem * var(--tw-space-x-reverse));
margin-left: calc(0.5rem * calc(1 - 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]) { .divide-y > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0; --tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
@ -918,6 +1035,10 @@ select {
overflow: hidden; overflow: hidden;
} }
.overflow-y-auto {
overflow-y: auto;
}
.text-ellipsis { .text-ellipsis {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
@ -950,6 +1071,22 @@ select {
border-width: 1px; 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 { .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));
@ -960,6 +1097,10 @@ select {
border-color: rgb(39 39 42 / var(--tw-border-opacity)); border-color: rgb(39 39 42 / var(--tw-border-opacity));
} }
.border-transparent {
border-color: transparent;
}
.border-mono-error { .border-mono-error {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(239 68 68 / var(--tw-border-opacity)); 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)); 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 { .bg-transparent {
background-color: transparent; background-color: transparent;
} }
.bg-mono-border { .bg-black {
--tw-bg-opacity: 1; --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; --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 { .bg-opacity-50 {
@ -1005,6 +1160,10 @@ select {
padding: 0.5rem; padding: 0.5rem;
} }
.p-3 {
padding: 0.75rem;
}
.p-4 { .p-4 {
padding: 1rem; padding: 1rem;
} }
@ -1078,6 +1237,16 @@ select {
padding-right: 0px; 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 { .pr-12 {
padding-right: 3rem; padding-right: 3rem;
} }
@ -1090,6 +1259,14 @@ select {
padding-right: 0.75rem; padding-right: 0.75rem;
} }
.pl-72 {
padding-left: 18rem;
}
.pb-4 {
padding-bottom: 1rem;
}
.text-left { .text-left {
text-align: left; text-align: left;
} }
@ -1122,6 +1299,10 @@ select {
font-weight: 600; font-weight: 600;
} }
.font-normal {
font-weight: 400;
}
.font-bold { .font-bold {
font-weight: 700; font-weight: 700;
} }
@ -1150,15 +1331,59 @@ select {
color: rgb(239 68 68 / var(--tw-text-opacity)); 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: 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-none {
outline: 2px solid transparent; outline: 2px solid transparent;
outline-offset: 2px; 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 { @keyframes enter {
from { from {
opacity: var(--tw-enter-opacity, 1); 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 { html {
font-size: 14px; font-size: 14px;
} }
@ -1197,6 +1466,40 @@ html {
background-color: rgb(63 63 70 / var(--tw-bg-opacity)); 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 { .focus\:rounded-md:focus {
border-radius: 0.375rem; border-radius: 0.375rem;
} }
@ -1276,6 +1579,14 @@ html {
outline-style: solid; 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] { .data-\[state\=open\]\:animate-in[data-state=open] {
animation-name: enter; animation-name: enter;
animation-duration: 150ms; animation-duration: 150ms;
@ -1328,6 +1639,66 @@ html {
--tw-enter-translate-y: 0.5rem; --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) { @media (prefers-reduced-motion: no-preference) {
@keyframes spin { @keyframes spin {
to { to {
@ -1341,6 +1712,15 @@ html {
} }
@media (min-width: 640px) { @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 { .sm\:px-6 {
padding-left: 1.5rem; padding-left: 1.5rem;
padding-right: 1.5rem; padding-right: 1.5rem;
@ -1376,3 +1756,43 @@ html {
margin-bottom: 0.25rem; 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": { "dependencies": {
"@radix-ui/react-dialog": "^1.0.5", "@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-label": "^2.0.2",
"@radix-ui/react-navigation-menu": "^1.1.4", "@radix-ui/react-navigation-menu": "^1.1.4",
"@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-popover": "^1.0.7",

@ -23,6 +23,14 @@ export const Primary: Story = {
render: () => <Button intent="primary">Click me</Button>, 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 = { export const Sizes: Story = {
render: () => ( render: () => (
<Box className={cx(["space-x-2"])}> <Box className={cx(["space-x-2"])}>

@ -35,6 +35,10 @@ const primary = [
"text-white", "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 onlyIcon = ["rounded-full", "p-1"];
const buttonStyles = cva(base, { const buttonStyles = cva(base, {
@ -45,7 +49,9 @@ const buttonStyles = cva(base, {
intent: { intent: {
primary, primary,
secondary,
DEFAULT, DEFAULT,
plain,
}, },
size, size,
fullWidth: { fullWidth: {

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

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

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

@ -1,20 +1,20 @@
import { cva } from "class-variance-authority"; import { cva } from "class-variance-authority";
const base = [""]; const base = ["block"];
const horizontal = ["px-0", "w-full"]; const horizontal = [""];
const vertical = ["py-0", "h-full"]; const vertical = [""];
const dividerStyles = cva(base, { const dividerStyles = cva(base, {
variants: { variants: {
size: { size: {
sm: ["p-1"], sm: ["p-1"],
md: ["p-2"], md: ["p-2"],
lg: ["p-4"], lg: ["p-3"],
xl: ["p-6"], xl: ["p-4"],
}, },
border: { border: {
true: [], true: [""],
}, },
orientation: { orientation: {
horizontal, 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 * as T from "./Divider.types";
import { Box } from "../Box"; import { Box } from "../Box";
import { cx } from "../../utils"; import { cx } from "../../utils";
import { dividerStyles } from "./Divider.styles"; import * as styles from "./Divider.styles";
const Divider = React.forwardRef< const Divider = React.forwardRef<
HTMLDivElement | HTMLHRElement, HTMLDivElement | HTMLHRElement,
T.DividerProps T.DividerProps
>(({ children, border = false, orientation, size, ...props }, forwardedRef) => { >(
const classes = cx( (
dividerStyles({ orientation, size, border }), { children, border = false, orientation = "horizontal", size, ...props },
props.className forwardedRef
); ) => {
return match({ border }) const classes = cx(
.with({ border: true }, () => ( styles.dividerStyles({ orientation, size, border }),
<div {...props} className={classes}> props.className
<Box className={cx(["bg-mono-border", "h-[1.5px]"])} /> );
</div> return match({ border })
)) .with({ border: true }, () => (
.otherwise(() => <div {...props} className={classes} ref={forwardedRef} />); <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 }; 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 { Input } from "./components/Input";
export { Modal } from "./components/Modal"; export { Modal } from "./components/Modal";
export { Select } from "./components/Select"; export { Select } from "./components/Select";
export { Dropdown } from "./components/Dropdown";
export { cx } from "./utils"; export { cx } from "./utils";

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

Loading…
Cancel
Save