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} />;
|
||||
}
|
||||
@@ -6,27 +6,39 @@ import {
|
||||
Content,
|
||||
Title,
|
||||
Close,
|
||||
Description,
|
||||
} from "@radix-ui/react-dialog";
|
||||
import { IconCrossLargeX } from "../../Atoms/Icons";
|
||||
import type { ReactNode } from "react";
|
||||
import type { HTMLAttributes, ReactNode } from "react";
|
||||
import { Button } from "../../Atoms/Button/Button";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import clsx from "clsx";
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
trigger: ReactNode;
|
||||
title: ReactNode;
|
||||
children?: ReactNode;
|
||||
open?: boolean;
|
||||
};
|
||||
|
||||
export function Dialog({ onClose, trigger, title, children }: Props) {
|
||||
export function Dialog({ onClose, trigger, title, children, open }: Props) {
|
||||
const onOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Root>
|
||||
<Root open={open} onOpenChange={onOpenChange}>
|
||||
<Trigger asChild>{trigger}</Trigger>
|
||||
<Portal>
|
||||
<Overlay className="fixed inset-0 z-50 bg-dialog/40 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
|
||||
<div className="fixed grid place-items-center inset-0 z-50">
|
||||
<Content className="bg-level-2 rounded-2xl max-w-5xl w-[95%]">
|
||||
<Content
|
||||
aria-describedby={undefined}
|
||||
className="bg-level-2 rounded-2xl max-w-5xl w-[95%]"
|
||||
>
|
||||
<div className="flex justify-between items-center p-3 border-b border-b-border-low">
|
||||
<Title className="text-sm font-medium">
|
||||
{title}
|
||||
@@ -58,15 +70,14 @@ export function DialogFooter({ children }: { children?: ReactNode }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function DialogContent({ children }: { children: ReactNode }) {
|
||||
export function DialogContent(props: HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className="overflow-y-auto"
|
||||
{...props}
|
||||
className={clsx("overflow-y-auto", props.className)}
|
||||
style={{
|
||||
maxHeight: "min(640px, calc(100vh - 140px))",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
18
packages/ui/src/Molecules/Field/Field.stories.tsx
Normal file
18
packages/ui/src/Molecules/Field/Field.stories.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Field } from "./Field.tsx";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
export default {
|
||||
title: "Molecules/Field",
|
||||
component: Field,
|
||||
argTypes: {},
|
||||
} satisfies Meta<typeof Field>;
|
||||
|
||||
type Story = StoryObj<typeof Field>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
label: "Label",
|
||||
name: "id",
|
||||
help: "e.g. This is a hint",
|
||||
},
|
||||
};
|
||||
51
packages/ui/src/Molecules/Field/Field.tsx
Normal file
51
packages/ui/src/Molecules/Field/Field.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { tv } from "tailwind-variants";
|
||||
import { Label } from "../../Atoms/Label/Label";
|
||||
import { Input } from "../../Atoms/Input/Input";
|
||||
import { Textarea } from "../../Atoms/Textarea/Textarea";
|
||||
|
||||
type Props = {
|
||||
label?: string;
|
||||
help?: string;
|
||||
name?: string;
|
||||
placeholder?: string;
|
||||
type?: string;
|
||||
required?: boolean;
|
||||
};
|
||||
|
||||
const field = tv({
|
||||
slots: {
|
||||
base: "flex flex-col",
|
||||
label: "mb-[6px]",
|
||||
help: "text-xs font-semibold text-txt-tertiary mt-1",
|
||||
},
|
||||
});
|
||||
|
||||
const { base: baseClass, label: labelClass, help: helpClass } = field();
|
||||
|
||||
export function Field({
|
||||
label,
|
||||
help,
|
||||
name,
|
||||
placeholder,
|
||||
type,
|
||||
required,
|
||||
}: Props) {
|
||||
const Comp = type === "textarea" ? Textarea : Input;
|
||||
return (
|
||||
<div className={baseClass()}>
|
||||
{label && (
|
||||
<Label htmlFor={name} className={labelClass()}>
|
||||
{label}
|
||||
</Label>
|
||||
)}
|
||||
<Comp
|
||||
name={name}
|
||||
id={name}
|
||||
placeholder={placeholder}
|
||||
type={type}
|
||||
required={required}
|
||||
/>
|
||||
{help && <span className={helpClass()}>{help}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -18,14 +18,14 @@ export function UserDropdown({ fullName, children, email }: Props) {
|
||||
className="w-60"
|
||||
toggle={
|
||||
<Button variant="tertiary">
|
||||
<Avatar fullName={fullName} />
|
||||
<Avatar name={fullName} />
|
||||
<span>{fullName}</span>
|
||||
<IconChevronDown size={16} />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Avatar fullName={fullName} size="l" />
|
||||
<Avatar name={fullName} size="l" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-txt-primary">
|
||||
{fullName}
|
||||
|
||||
@@ -20,7 +20,10 @@ export {
|
||||
DropdownItem,
|
||||
} from "./Atoms/Dropdown/Dropdown";
|
||||
export { Avatar } from "./Atoms/Avatar/Avatar";
|
||||
export { Input } from "./Atoms/Input/Input";
|
||||
export { Field } from "./Molecules/Field/Field.tsx";
|
||||
export { Input } from "./Atoms/Input/Input.tsx";
|
||||
export { Select, Option } from "./Atoms/Select/Select.tsx";
|
||||
export { Label } from "./Atoms/Label/Label";
|
||||
|
||||
// Molecules
|
||||
export {
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./theme.css";
|
||||
import App from "./App.tsx";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
Reference in New Issue
Block a user