diff --git a/packages/ui/package.json b/packages/ui/package.json index 7e6c7c00..84b30bd2 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -9,7 +9,13 @@ "./modal": "./src/components/Modal.tsx", "./confirm-dialog": "./src/components/ConfirmDialog.tsx", "./badge": "./src/components/Badge.tsx", - "./empty-state": "./src/components/EmptyState.tsx" + "./empty-state": "./src/components/EmptyState.tsx", + "./input": "./src/components/Input.tsx", + "./textarea": "./src/components/Textarea.tsx", + "./card": "./src/components/Card.tsx", + "./label": "./src/components/Label.tsx", + "./select": "./src/components/Select.tsx", + "./separator": "./src/components/Separator.tsx" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", diff --git a/packages/ui/src/components/Card.tsx b/packages/ui/src/components/Card.tsx new file mode 100644 index 00000000..bf20f525 --- /dev/null +++ b/packages/ui/src/components/Card.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import { clsx } from 'clsx'; + +export interface CardProps extends React.HTMLAttributes { + padding?: 'none' | 'sm' | 'md' | 'lg'; + hover?: boolean; +} + +const paddings: Record = { + none: '', + sm: 'p-3', + md: 'p-4', + lg: 'p-6', +}; + +export function Card({ padding = 'md', hover, className, children, ...props }: CardProps) { + return ( +
+ {children} +
+ ); +} + +export interface CardHeaderProps extends React.HTMLAttributes {} + +export function CardHeader({ className, children, ...props }: CardHeaderProps) { + return ( +
+ {children} +
+ ); +} + +export type CardTitleProps = React.ComponentPropsWithoutRef<'h3'>; + +export function CardTitle({ className, children, ...props }: CardTitleProps) { + return ( +

+ {children} +

+ ); +} + +export type CardDescriptionProps = React.ComponentPropsWithoutRef<'p'>; + +export function CardDescription({ className, children, ...props }: CardDescriptionProps) { + return ( +

+ {children} +

+ ); +} diff --git a/packages/ui/src/components/Input.tsx b/packages/ui/src/components/Input.tsx new file mode 100644 index 00000000..a2d80615 --- /dev/null +++ b/packages/ui/src/components/Input.tsx @@ -0,0 +1,54 @@ +import * as React from 'react'; +import { clsx } from 'clsx'; + +export interface InputProps extends React.InputHTMLAttributes { + label?: string; + error?: string; + hint?: string; +} + +export const Input = React.forwardRef( + ({ label, error, hint, className, id, ...props }, ref) => { + const inputId = id ?? (label ? `input-${label.toLowerCase().replace(/\s+/g, '-')}` : undefined); + + return ( +
+ {label && ( + + )} + + {error && ( + + )} + {hint && !error && ( +

+ {hint} +

+ )} +
+ ); + } +); + +Input.displayName = 'Input'; diff --git a/packages/ui/src/components/Label.tsx b/packages/ui/src/components/Label.tsx new file mode 100644 index 00000000..08f1aac4 --- /dev/null +++ b/packages/ui/src/components/Label.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import { clsx } from 'clsx'; + +export interface LabelProps extends React.LabelHTMLAttributes { + required?: boolean; +} + +export function Label({ required, className, children, ...props }: LabelProps) { + return ( + + ); +} diff --git a/packages/ui/src/components/Select.tsx b/packages/ui/src/components/Select.tsx new file mode 100644 index 00000000..4f0124e6 --- /dev/null +++ b/packages/ui/src/components/Select.tsx @@ -0,0 +1,67 @@ +import * as React from 'react'; +import { clsx } from 'clsx'; +import { ChevronDown } from 'lucide-react'; + +export interface SelectProps extends React.SelectHTMLAttributes { + label?: string; + error?: string; + options: Array<{ value: string; label: string; disabled?: boolean }>; + placeholder?: string; +} + +export const Select = React.forwardRef( + ({ label, error, options, placeholder, className, id, ...props }, ref) => { + const selectId = + id ?? (label ? `select-${label.toLowerCase().replace(/\s+/g, '-')}` : undefined); + + return ( +
+ {label && ( + + )} +
+ +
+ {error && ( +

+ {error} +

+ )} +
+ ); + } +); + +Select.displayName = 'Select'; diff --git a/packages/ui/src/components/Separator.tsx b/packages/ui/src/components/Separator.tsx new file mode 100644 index 00000000..06ab92fb --- /dev/null +++ b/packages/ui/src/components/Separator.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { clsx } from 'clsx'; + +export interface SeparatorProps extends React.ComponentPropsWithoutRef<'hr'> { + orientation?: 'horizontal' | 'vertical'; +} + +export function Separator({ orientation = 'horizontal', className, ...props }: SeparatorProps) { + if (orientation === 'vertical') { + return ( +
)} + /> + ); + } + + return ( +
+ ); +} diff --git a/packages/ui/src/components/Textarea.tsx b/packages/ui/src/components/Textarea.tsx new file mode 100644 index 00000000..da0d198c --- /dev/null +++ b/packages/ui/src/components/Textarea.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import { clsx } from 'clsx'; + +export interface TextareaProps extends React.TextareaHTMLAttributes { + label?: string; + error?: string; + hint?: string; +} + +export const Textarea = React.forwardRef( + ({ label, error, hint, className, id, ...props }, ref) => { + const textareaId = + id ?? (label ? `textarea-${label.toLowerCase().replace(/\s+/g, '-')}` : undefined); + + return ( +
+ {label && ( + + )} +