Add UI input & textarea
Signed-off-by: Bryan Frimin <bryan@getprobo.com> Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
committed by
Sacha Al Himdani
parent
a86181c83d
commit
15cf471454
@@ -11,6 +11,6 @@ type Story = StoryObj<typeof Avatar>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
fullName: "John Doe",
|
||||
name: "John Doe",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import clsx from "clsx";
|
||||
import { Link } from "react-router";
|
||||
import { IconChevronRight } from "../Icons";
|
||||
import { Fragment } from "react/jsx-runtime";
|
||||
|
||||
type Props = {
|
||||
items: (ItemProps | string)[];
|
||||
@@ -15,15 +16,14 @@ type ItemProps = {
|
||||
export function Breadcrumb({ items }: Props) {
|
||||
return (
|
||||
<div className="flex items-center gap-[6px] text-txt-tertiary">
|
||||
{items.map((item, index) => (
|
||||
<>
|
||||
{index > 0 && <IconChevronRight size={12} />}
|
||||
{items.map((item, k) => (
|
||||
<Fragment key={k}>
|
||||
{k > 0 && <IconChevronRight size={12} />}
|
||||
<BreadcrumbItem
|
||||
key={index}
|
||||
{...(typeof item === "string" ? { label: item } : item)}
|
||||
active={index === items.length - 1}
|
||||
active={k === items.length - 1}
|
||||
/>
|
||||
</>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,6 @@ import type {
|
||||
FC,
|
||||
PropsWithChildren,
|
||||
} from "react";
|
||||
import { forwardRef } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { tv, type VariantProps } from "tailwind-variants";
|
||||
|
||||
@@ -56,28 +55,25 @@ type Props = PropsWithChildren<
|
||||
| AnchorHTMLAttributes<HTMLAnchorElement>
|
||||
);
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, Props>(
|
||||
(props, ref) => {
|
||||
const Component = props.to ? Link : "button";
|
||||
const {
|
||||
icon: IconComponent,
|
||||
iconAfter: IconAfterComponent,
|
||||
children,
|
||||
onClick,
|
||||
...componentProps
|
||||
} = props;
|
||||
return (
|
||||
<Component
|
||||
{...componentProps}
|
||||
onClick={onClick}
|
||||
// @ts-expect-error Component is too dynamic
|
||||
ref={ref}
|
||||
className={button({ ...props, empty: !children })}
|
||||
>
|
||||
{IconComponent && <IconComponent size={16} />}
|
||||
{children}
|
||||
{IconAfterComponent && <IconAfterComponent size={16} />}
|
||||
</Component>
|
||||
);
|
||||
},
|
||||
);
|
||||
export const Button = (props: Props) => {
|
||||
const Component = props.to ? Link : "button";
|
||||
const {
|
||||
icon: IconComponent,
|
||||
iconAfter: IconAfterComponent,
|
||||
children,
|
||||
onClick,
|
||||
...componentProps
|
||||
} = props;
|
||||
return (
|
||||
// @ts-expect-error Component is too dynamic
|
||||
<Component
|
||||
{...componentProps}
|
||||
onClick={onClick}
|
||||
className={button({ ...props, empty: !children })}
|
||||
>
|
||||
{IconComponent && <IconComponent size={16} />}
|
||||
{children}
|
||||
{IconAfterComponent && <IconAfterComponent size={16} />}
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,10 +9,4 @@ export default {
|
||||
|
||||
type Story = StoryObj<typeof Input>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
label: "Label",
|
||||
name: "id",
|
||||
help: "e.g. This is a hint",
|
||||
},
|
||||
};
|
||||
export const Default: Story = {};
|
||||
|
||||
@@ -1,54 +1,12 @@
|
||||
import type { InputHTMLAttributes } from "react";
|
||||
import { tv } from "tailwind-variants";
|
||||
|
||||
type Props = {
|
||||
label?: string;
|
||||
help?: string;
|
||||
name?: string;
|
||||
placeholder?: string;
|
||||
type?: string;
|
||||
required?: boolean;
|
||||
};
|
||||
type Props = InputHTMLAttributes<HTMLInputElement>;
|
||||
|
||||
const input = tv({
|
||||
slots: {
|
||||
base: "flex flex-col",
|
||||
label: "text-sm font-medium text-txt-primary mb-[6px]",
|
||||
input: "py-[6px] bg-secondary border border-border-mid rounded-[10px] hover:border-border-strong focus:shadow-focus text-sm px-3",
|
||||
help: "text-xs font-semibold text-txt-tertiary mt-1",
|
||||
},
|
||||
export const input = tv({
|
||||
base: "py-[6px] bg-secondary border border-border-mid rounded-[10px] hover:border-border-strong focus:shadow-focus text-sm px-3",
|
||||
});
|
||||
|
||||
const {
|
||||
base: baseClass,
|
||||
label: labelClass,
|
||||
input: inputClass,
|
||||
help: helpClass,
|
||||
} = input();
|
||||
|
||||
export function Input({
|
||||
label,
|
||||
help,
|
||||
name,
|
||||
placeholder,
|
||||
type,
|
||||
required,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className={baseClass()}>
|
||||
{label && (
|
||||
<label htmlFor={name} className={labelClass()}>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
name={name}
|
||||
id={name}
|
||||
className={inputClass()}
|
||||
placeholder={placeholder}
|
||||
type={type}
|
||||
required={required}
|
||||
/>
|
||||
{help && <span className={helpClass()}>{help}</span>}
|
||||
</div>
|
||||
);
|
||||
export function Input(props: Props) {
|
||||
return <input className={input(props)} {...props} />;
|
||||
}
|
||||
|
||||
12
packages/ui/src/Atoms/Label/Label.stories.tsx
Normal file
12
packages/ui/src/Atoms/Label/Label.stories.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Label } from "./Label";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
export default {
|
||||
title: "Atoms/Form/Label",
|
||||
component: Label,
|
||||
argTypes: {},
|
||||
} satisfies Meta<typeof Label>;
|
||||
|
||||
type Story = StoryObj<typeof Label>;
|
||||
|
||||
export const Default: Story = {};
|
||||
14
packages/ui/src/Atoms/Label/Label.tsx
Normal file
14
packages/ui/src/Atoms/Label/Label.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { HTMLAttributes } from "react";
|
||||
import { tv } from "tailwind-variants";
|
||||
|
||||
type Props = HTMLAttributes<HTMLLabelElement> & {
|
||||
htmlFor?: string;
|
||||
};
|
||||
|
||||
const label = tv({
|
||||
base: "text-sm font-medium text-txt-primary",
|
||||
});
|
||||
|
||||
export function Label({ ...props }: Props) {
|
||||
return <label {...props} className={label(props)} />;
|
||||
}
|
||||
31
packages/ui/src/Atoms/Select/Select.stories.tsx
Normal file
31
packages/ui/src/Atoms/Select/Select.stories.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Select, Option } from "./Select";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
export default {
|
||||
title: "Atoms/Form/Select",
|
||||
component: Select,
|
||||
argTypes: {},
|
||||
} satisfies Meta<typeof Select>;
|
||||
|
||||
type Story = StoryObj<typeof Select>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
placeholder: "Select an option",
|
||||
children: (
|
||||
<>
|
||||
<Option value="apple">Apple</Option>
|
||||
<Option value="banana">Banana</Option>
|
||||
<Option value="blueberry">Blueberry</Option>
|
||||
<Option value="grapes">Grapes</Option>
|
||||
<Option value="pineapple">Pineapple</Option>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const NoChildren: Story = {
|
||||
args: {
|
||||
placeholder: "Select an option",
|
||||
},
|
||||
};
|
||||
94
packages/ui/src/Atoms/Select/Select.tsx
Normal file
94
packages/ui/src/Atoms/Select/Select.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
Root,
|
||||
Trigger,
|
||||
Value,
|
||||
Icon,
|
||||
Portal,
|
||||
Content,
|
||||
Viewport,
|
||||
Item,
|
||||
ItemText,
|
||||
} from "@radix-ui/react-select";
|
||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
import { input } from "../Input/Input.tsx";
|
||||
import { IconChevronGrabberVertical } from "../Icons/IconChevronGrabberVertical.tsx";
|
||||
import { tv } from "tailwind-variants";
|
||||
import type { ComponentProps, PropsWithChildren } from "react";
|
||||
|
||||
type Props = PropsWithChildren<
|
||||
{
|
||||
placeholder?: string;
|
||||
onChange?: (s: string) => void;
|
||||
empty?: boolean;
|
||||
} & Omit<ComponentProps<typeof Trigger>, "onChange">
|
||||
>;
|
||||
|
||||
const select = tv({
|
||||
slots: {
|
||||
trigger: input({
|
||||
className:
|
||||
"flex justify-between items-center gap-4 data-placeholder:text-txt-tertiary w-full whitespace-nowrap",
|
||||
}),
|
||||
content:
|
||||
"z-100 shadow-mid rounded-[10px] bg-level-1 p-1 animate-in fade-in slide-in-from-top-2 overflow-y-auto overflow-y-auto",
|
||||
option: "flex items-center gap-2 h-8 text-sm font-medium text-txt-primary hover:bg-tertiary-hover active:bg-tertiary-pressed cursor-pointer px-[10px]",
|
||||
},
|
||||
variants: {
|
||||
empty: {
|
||||
true: {
|
||||
trigger: "border-dashed pointer-events-none",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { trigger, option, content } = select();
|
||||
|
||||
export function Select({
|
||||
placeholder,
|
||||
children,
|
||||
onChange,
|
||||
empty,
|
||||
...props
|
||||
}: Props) {
|
||||
return (
|
||||
<Root onValueChange={onChange}>
|
||||
<Trigger className={trigger({ ...props, empty })}>
|
||||
<div className="text-ellipsis overflow-hidden">
|
||||
<Value placeholder={placeholder} />
|
||||
</div>
|
||||
<Icon className="SelectIcon">
|
||||
<IconChevronGrabberVertical size={16} />
|
||||
</Icon>
|
||||
</Trigger>
|
||||
<Portal>
|
||||
<Content
|
||||
className={content()}
|
||||
position="popper"
|
||||
sideOffset={5}
|
||||
style={{
|
||||
minWidth: "var(--radix-select-trigger-width)",
|
||||
maxHeight:
|
||||
"var(--radix-select-content-available-height)",
|
||||
}}
|
||||
>
|
||||
<ScrollArea.Root className="ScrollAreaRoot" type="auto">
|
||||
<Viewport asChild>
|
||||
<ScrollArea.Viewport className="ScrollAreaViewport">
|
||||
{children}
|
||||
</ScrollArea.Viewport>
|
||||
</Viewport>
|
||||
</ScrollArea.Root>
|
||||
</Content>
|
||||
</Portal>
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
|
||||
export function Option({ children, ...props }: ComponentProps<typeof Item>) {
|
||||
return (
|
||||
<Item {...props} className={option(props)}>
|
||||
<ItemText>{children}</ItemText>
|
||||
</Item>
|
||||
);
|
||||
}
|
||||
12
packages/ui/src/Atoms/Textarea/Textarea.stories.tsx
Normal file
12
packages/ui/src/Atoms/Textarea/Textarea.stories.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Textarea } from "./Textarea";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
export default {
|
||||
title: "Atoms/Form/Textarea",
|
||||
component: Textarea,
|
||||
argTypes: {},
|
||||
} satisfies Meta<typeof Textarea>;
|
||||
|
||||
type Story = StoryObj<typeof Textarea>;
|
||||
|
||||
export const Default: Story = {};
|
||||
15
packages/ui/src/Atoms/Textarea/Textarea.tsx
Normal file
15
packages/ui/src/Atoms/Textarea/Textarea.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { InputHTMLAttributes } from "react";
|
||||
import { tv } from "tailwind-variants";
|
||||
import { input } from "../Input/Input";
|
||||
|
||||
type Props = InputHTMLAttributes<HTMLTextAreaElement>;
|
||||
|
||||
export const textarea = tv({
|
||||
base: input({ class: "min-h-20" }),
|
||||
});
|
||||
|
||||
console.log(input({ class: "min-h-20" }));
|
||||
|
||||
export function Textarea(props: Props) {
|
||||
return <textarea className={textarea(props)} {...props} />;
|
||||
}
|
||||
Reference in New Issue
Block a user