@otfdashkit/ui 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +28 -41
- package/dist/index.js +972 -655
- package/dist/index.mjs +826 -525
- package/package.json +5 -4
- package/src/advanced/BulkActions/BulkActions.tsx +2 -2
- package/src/advanced/CommandBar/CommandBar.tsx +2 -2
- package/src/advanced/Filters/Filters.tsx +6 -6
- package/src/advanced/Kanban/Kanban.tsx +1 -1
- package/src/advanced/ToggleButton/ToggleButton.tsx +1 -1
- package/src/advanced/bulk-actions.tsx +1 -46
- package/src/advanced/command-bar.tsx +1 -115
- package/src/advanced/filters.tsx +1 -103
- package/src/advanced/kanban.tsx +1 -95
- package/src/advanced/toggle-button.tsx +1 -78
- package/src/blocks/ChatDetail/ChatDetail.tsx +4 -4
- package/src/blocks/FeedbackModal/FeedbackModal.tsx +3 -3
- package/src/blocks/FileCards/FileCards.tsx +2 -2
- package/src/blocks/FilesList/FilesList.tsx +3 -3
- package/src/blocks/IntegrationCard/IntegrationCard.tsx +3 -3
- package/src/blocks/InviteModal/InviteModal.tsx +7 -7
- package/src/blocks/ManageTagsModal/ManageTagsModal.tsx +4 -4
- package/src/blocks/MessagesCard/MessagesCard.tsx +3 -3
- package/src/blocks/MetricCard/MetricCard.tsx +3 -3
- package/src/blocks/NotificationSettings/NotificationSettings.tsx +4 -4
- package/src/blocks/OrganizationMenu/OrganizationMenu.tsx +30 -11
- package/src/blocks/RolesMenu/RolesMenu.tsx +4 -4
- package/src/blocks/SelectUsersModal/SelectUsersModal.tsx +5 -5
- package/src/blocks/SidebarLayoutDashboard/SidebarLayoutDashboard.tsx +5 -5
- package/src/blocks/SidebarLayoutGroups/SidebarLayoutGroups.tsx +2 -2
- package/src/blocks/SidebarLayoutMinimal/SidebarLayoutMinimal.tsx +3 -3
- package/src/blocks/SidebarLayoutSearch/SidebarLayoutSearch.tsx +3 -3
- package/src/blocks/SidebarLayoutUser/SidebarLayoutUser.tsx +4 -4
- package/src/blocks/SortableTaskList/SortableTaskList.tsx +3 -3
- package/src/blocks/StackedLayoutBranded/StackedLayoutBranded.tsx +4 -4
- package/src/blocks/StackedLayoutTabs/StackedLayoutTabs.tsx +4 -4
- package/src/blocks/TaskCard/TaskCard.tsx +4 -4
- package/src/blocks/UserMenu/UserMenu.tsx +3 -3
- package/src/blocks/WorkspaceMembers/WorkspaceMembers.tsx +5 -5
- package/src/blocks/chat-detail.tsx +1 -73
- package/src/blocks/feedback-modal.tsx +1 -68
- package/src/blocks/file-cards.tsx +1 -56
- package/src/blocks/files-list.tsx +1 -72
- package/src/blocks/integration-card.tsx +1 -50
- package/src/blocks/invite-modal.tsx +1 -69
- package/src/blocks/manage-tags-modal.tsx +1 -63
- package/src/blocks/messages-card.tsx +1 -59
- package/src/blocks/metric-card.tsx +1 -79
- package/src/blocks/notification-settings.tsx +1 -61
- package/src/blocks/organization-menu.tsx +1 -85
- package/src/blocks/roles-menu.tsx +1 -61
- package/src/blocks/select-users-modal.tsx +1 -64
- package/src/blocks/sidebar-layout-dashboard.tsx +1 -97
- package/src/blocks/sidebar-layout-groups.tsx +1 -79
- package/src/blocks/sidebar-layout-minimal.tsx +1 -73
- package/src/blocks/sidebar-layout-search.tsx +1 -79
- package/src/blocks/sidebar-layout-user.tsx +1 -82
- package/src/blocks/sortable-task-list.tsx +1 -70
- package/src/blocks/stacked-layout-branded.tsx +1 -64
- package/src/blocks/stacked-layout-tabs.tsx +1 -80
- package/src/blocks/task-card.tsx +1 -88
- package/src/blocks/user-menu.tsx +1 -62
- package/src/blocks/workspace-members.tsx +1 -61
- package/src/charts/sparkline.tsx +1 -1
- package/src/components/DataGrid/DataGrid.tsx +1 -1
- package/src/components/DataTable/DataTable.tsx +4 -4
- package/src/components/EmptyState/EmptyState.tsx +6 -22
- package/src/components/Persona/Persona.tsx +1 -1
- package/src/components/Stat/Stat.tsx +1 -1
- package/src/components/Stepper/Stepper.tsx +13 -2
- package/src/components/banner.tsx +1 -203
- package/src/components/breadcrumb.tsx +1 -58
- package/src/components/command-item.tsx +1 -36
- package/src/components/data-grid.tsx +1 -125
- package/src/components/data-table.tsx +1 -139
- package/src/components/empty-state.tsx +1 -31
- package/src/components/hotkeys.tsx +1 -31
- package/src/components/icon-badge.tsx +1 -35
- package/src/components/loading-overlay.tsx +1 -38
- package/src/components/persona.tsx +1 -38
- package/src/components/property-list.tsx +1 -29
- package/src/components/stat.tsx +1 -51
- package/src/components/stepper.tsx +1 -69
- package/src/components/structured-list.tsx +1 -51
- package/src/components/timeline.tsx +1 -69
- package/src/components/toaster.tsx +1 -42
- package/src/data/DataList.tsx +1 -1
- package/src/engagement/Tour/Tour.tsx +1 -1
- package/src/engagement/beacon.tsx +1 -50
- package/src/engagement/tour.tsx +1 -106
- package/src/forms/ArrayField/ArrayField.tsx +2 -2
- package/src/forms/AutoForm/AutoForm.tsx +9 -8
- package/src/forms/DatePicker/DatePicker.tsx +3 -3
- package/src/forms/FloatingLabelInput/FloatingLabelInput.tsx +104 -0
- package/src/forms/FloatingLabelInput/index.ts +1 -0
- package/src/forms/PasswordInput/PasswordInput.tsx +2 -2
- package/src/forms/SearchInput/SearchInput.tsx +2 -2
- package/src/forms/StepForm/StepForm.tsx +2 -2
- package/src/forms/array-field.tsx +1 -61
- package/src/forms/auto-form.tsx +1 -133
- package/src/forms/date-picker.tsx +1 -109
- package/src/forms/field.tsx +1 -62
- package/src/forms/file-upload.tsx +1 -101
- package/src/forms/floating-label-input.tsx +1 -0
- package/src/forms/form.tsx +1 -57
- package/src/forms/object-field.tsx +1 -53
- package/src/forms/password-input.tsx +1 -54
- package/src/forms/search-input.tsx +1 -64
- package/src/forms/step-form.tsx +1 -113
- package/src/index.ts +11 -3
- package/src/primitives/Button/Button.tsx +18 -0
- package/src/primitives/cart-line-item.tsx +81 -0
- package/src/primitives/code-block.tsx +1 -1
- package/src/primitives/comparison.tsx +1 -1
- package/src/primitives/empty-cart.tsx +52 -0
- package/src/primitives/price-tag.tsx +62 -0
- package/src/primitives/qr-code.tsx +5 -1
- package/src/primitives/quantity-stepper.tsx +92 -0
- package/src/primitives/variant-picker.tsx +74 -0
- package/src/themes/base.css +18 -0
package/src/forms/auto-form.tsx
CHANGED
|
@@ -1,133 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { useForm } from 'react-hook-form'
|
|
3
|
-
import { zodResolver } from '@hookform/resolvers/zod'
|
|
4
|
-
import * as z from 'zod'
|
|
5
|
-
import { Input } from '../primitives/input'
|
|
6
|
-
import { Checkbox } from '../primitives/checkbox'
|
|
7
|
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../primitives/select'
|
|
8
|
-
import { Button } from '../primitives/button'
|
|
9
|
-
import { DatePicker } from './date-picker'
|
|
10
|
-
import { Field } from './field'
|
|
11
|
-
|
|
12
|
-
export interface AutoFormProps {
|
|
13
|
-
schema: z.ZodObject<z.ZodRawShape>
|
|
14
|
-
onSubmit: (data: Record<string, unknown>) => void
|
|
15
|
-
defaultValues?: Record<string, unknown>
|
|
16
|
-
submitLabel?: string
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function toLabel(key: string) {
|
|
20
|
-
return key.replace(/([A-Z])/g, ' $1').replace(/^./, s => s.toUpperCase())
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function getInputType(field: z.ZodTypeAny): string {
|
|
24
|
-
if (field instanceof z.ZodEmail) return 'email'
|
|
25
|
-
if (field instanceof z.ZodURL) return 'url'
|
|
26
|
-
return 'text'
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
|
-
function unwrapOptional(field: z.ZodTypeAny): any {
|
|
31
|
-
if (field instanceof z.ZodOptional || field instanceof z.ZodNullable) {
|
|
32
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
|
-
return unwrapOptional((field as any).unwrap())
|
|
34
|
-
}
|
|
35
|
-
return field
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
interface FieldRendererProps {
|
|
39
|
-
name: string
|
|
40
|
-
field: z.ZodTypeAny
|
|
41
|
-
register: ReturnType<typeof useForm>['register']
|
|
42
|
-
setValue: ReturnType<typeof useForm>['setValue']
|
|
43
|
-
watch: ReturnType<typeof useForm>['watch']
|
|
44
|
-
error?: string
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function FieldRenderer({ name, field, register, setValue, watch, error }: FieldRendererProps) {
|
|
48
|
-
const inner = unwrapOptional(field)
|
|
49
|
-
const label = toLabel(name)
|
|
50
|
-
const isOptional = field instanceof z.ZodOptional
|
|
51
|
-
|
|
52
|
-
if (inner instanceof z.ZodBoolean) {
|
|
53
|
-
const checked = Boolean(watch(name))
|
|
54
|
-
return (
|
|
55
|
-
<Field label={label} error={error}>
|
|
56
|
-
<div className="flex items-center gap-2">
|
|
57
|
-
<Checkbox
|
|
58
|
-
id={name}
|
|
59
|
-
checked={checked}
|
|
60
|
-
onCheckedChange={v => setValue(name, v)}
|
|
61
|
-
/>
|
|
62
|
-
<label htmlFor={name} className="text-sm text-[hsl(var(--foreground))]">{label}</label>
|
|
63
|
-
</div>
|
|
64
|
-
</Field>
|
|
65
|
-
)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (inner instanceof z.ZodEnum) {
|
|
69
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
70
|
-
const options = ((inner as any).options ?? []) as string[]
|
|
71
|
-
const value = watch(name) as string | undefined
|
|
72
|
-
return (
|
|
73
|
-
<Field label={label} required={!isOptional} error={error}>
|
|
74
|
-
<Select value={value} onValueChange={v => setValue(name, v)}>
|
|
75
|
-
<SelectTrigger><SelectValue placeholder={`Select ${label.toLowerCase()}`} /></SelectTrigger>
|
|
76
|
-
<SelectContent>
|
|
77
|
-
{options.map(opt => <SelectItem key={opt} value={opt}>{opt}</SelectItem>)}
|
|
78
|
-
</SelectContent>
|
|
79
|
-
</Select>
|
|
80
|
-
</Field>
|
|
81
|
-
)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (inner instanceof z.ZodDate) {
|
|
85
|
-
const value = watch(name) as Date | undefined
|
|
86
|
-
return (
|
|
87
|
-
<Field label={label} required={!isOptional} error={error}>
|
|
88
|
-
<DatePicker value={value} onChange={d => setValue(name, d)} placeholder={`Select ${label.toLowerCase()}`} />
|
|
89
|
-
</Field>
|
|
90
|
-
)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (inner instanceof z.ZodNumber) {
|
|
94
|
-
return (
|
|
95
|
-
<Field label={label} required={!isOptional} error={error}>
|
|
96
|
-
<Input type="number" {...register(name, { valueAsNumber: true })} />
|
|
97
|
-
</Field>
|
|
98
|
-
)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const inputType = getInputType(inner)
|
|
102
|
-
return (
|
|
103
|
-
<Field label={label} required={!isOptional} error={error}>
|
|
104
|
-
<Input type={inputType} {...register(name)} />
|
|
105
|
-
</Field>
|
|
106
|
-
)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function AutoForm({ schema, onSubmit, defaultValues, submitLabel = 'Submit' }: AutoFormProps) {
|
|
110
|
-
const { register, handleSubmit, setValue, watch, formState } = useForm({
|
|
111
|
-
resolver: zodResolver(schema),
|
|
112
|
-
defaultValues: defaultValues as Record<string, unknown>,
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
const shape = schema.shape
|
|
116
|
-
|
|
117
|
-
return (
|
|
118
|
-
<form onSubmit={handleSubmit(d => onSubmit(d as Record<string, unknown>))} className="flex flex-col gap-4">
|
|
119
|
-
{Object.entries(shape).map(([name, field]) => (
|
|
120
|
-
<FieldRenderer
|
|
121
|
-
key={name}
|
|
122
|
-
name={name}
|
|
123
|
-
field={field as z.ZodTypeAny}
|
|
124
|
-
register={register}
|
|
125
|
-
setValue={setValue}
|
|
126
|
-
watch={watch}
|
|
127
|
-
error={formState.errors[name]?.message as string | undefined}
|
|
128
|
-
/>
|
|
129
|
-
))}
|
|
130
|
-
<Button type="submit">{submitLabel}</Button>
|
|
131
|
-
</form>
|
|
132
|
-
)
|
|
133
|
-
}
|
|
1
|
+
export * from './AutoForm'
|
|
@@ -1,109 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { ChevronLeft, ChevronRight, Calendar } from 'lucide-react'
|
|
3
|
-
import { Button } from '../primitives/button'
|
|
4
|
-
import { Popover, PopoverContent, PopoverTrigger } from '../primitives/popover'
|
|
5
|
-
import { cn } from '../utils/cn'
|
|
6
|
-
|
|
7
|
-
const DAYS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']
|
|
8
|
-
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
|
|
9
|
-
|
|
10
|
-
export interface DatePickerProps {
|
|
11
|
-
value?: Date
|
|
12
|
-
onChange?: (date: Date) => void
|
|
13
|
-
placeholder?: string
|
|
14
|
-
disabled?: boolean
|
|
15
|
-
className?: string
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function isSameDay(a: Date, b: Date) {
|
|
19
|
-
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function getDaysInMonth(year: number, month: number) {
|
|
23
|
-
return new Date(year, month + 1, 0).getDate()
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function getFirstDayOfWeek(year: number, month: number) {
|
|
27
|
-
return new Date(year, month, 1).getDay()
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function CalendarGrid({ year, month, selected, onSelect }: {
|
|
31
|
-
year: number; month: number; selected?: Date; onSelect: (d: Date) => void
|
|
32
|
-
}) {
|
|
33
|
-
const today = new Date()
|
|
34
|
-
const daysInMonth = getDaysInMonth(year, month)
|
|
35
|
-
const firstDay = getFirstDayOfWeek(year, month)
|
|
36
|
-
const cells = Array.from({ length: firstDay + daysInMonth }, (_, i) => i < firstDay ? null : i - firstDay + 1)
|
|
37
|
-
|
|
38
|
-
return (
|
|
39
|
-
<div className="grid grid-cols-7 gap-0.5">
|
|
40
|
-
{DAYS.map(d => <div key={d} className="py-1 text-center text-xs font-medium text-[hsl(var(--muted-foreground))]">{d}</div>)}
|
|
41
|
-
{cells.map((day, i) => {
|
|
42
|
-
if (!day) return <div key={i} />
|
|
43
|
-
const date = new Date(year, month, day)
|
|
44
|
-
const isToday = isSameDay(date, today)
|
|
45
|
-
const isSelected = selected && isSameDay(date, selected)
|
|
46
|
-
return (
|
|
47
|
-
<button
|
|
48
|
-
key={i}
|
|
49
|
-
type="button"
|
|
50
|
-
onClick={() => onSelect(date)}
|
|
51
|
-
className={cn(
|
|
52
|
-
'flex h-8 w-8 items-center justify-center rounded-md text-sm transition-colors',
|
|
53
|
-
isSelected && 'bg-[hsl(var(--primary))] text-[hsl(var(--primary-foreground))]',
|
|
54
|
-
!isSelected && isToday && 'border border-[hsl(var(--primary))] text-[hsl(var(--primary))]',
|
|
55
|
-
!isSelected && !isToday && 'hover:bg-[hsl(var(--accent))]'
|
|
56
|
-
)}
|
|
57
|
-
>
|
|
58
|
-
{day}
|
|
59
|
-
</button>
|
|
60
|
-
)
|
|
61
|
-
})}
|
|
62
|
-
</div>
|
|
63
|
-
)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function DatePicker({ value, onChange, placeholder = 'Pick a date', disabled, className }: DatePickerProps) {
|
|
67
|
-
const [open, setOpen] = useState(false)
|
|
68
|
-
const now = value ?? new Date()
|
|
69
|
-
const [view, setView] = useState({ year: now.getFullYear(), month: now.getMonth() })
|
|
70
|
-
|
|
71
|
-
function navigate(delta: number) {
|
|
72
|
-
setView(v => {
|
|
73
|
-
const date = new Date(v.year, v.month + delta)
|
|
74
|
-
return { year: date.getFullYear(), month: date.getMonth() }
|
|
75
|
-
})
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function handleSelect(date: Date) {
|
|
79
|
-
onChange?.(date)
|
|
80
|
-
setOpen(false)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return (
|
|
84
|
-
<Popover open={open} onOpenChange={setOpen}>
|
|
85
|
-
<PopoverTrigger asChild>
|
|
86
|
-
<Button
|
|
87
|
-
variant="outline"
|
|
88
|
-
disabled={disabled}
|
|
89
|
-
className={cn('w-full justify-start font-normal', !value && 'text-[hsl(var(--muted-foreground))]', className)}
|
|
90
|
-
>
|
|
91
|
-
<Calendar className="mr-2 h-4 w-4 shrink-0" />
|
|
92
|
-
{value ? value.toLocaleDateString() : placeholder}
|
|
93
|
-
</Button>
|
|
94
|
-
</PopoverTrigger>
|
|
95
|
-
<PopoverContent className="w-auto p-3" align="start">
|
|
96
|
-
<div className="flex items-center justify-between mb-2">
|
|
97
|
-
<button type="button" onClick={() => navigate(-1)} className="rounded p-1 hover:bg-[hsl(var(--accent))]">
|
|
98
|
-
<ChevronLeft className="h-4 w-4" />
|
|
99
|
-
</button>
|
|
100
|
-
<span className="text-sm font-medium">{MONTHS[view.month]} {view.year}</span>
|
|
101
|
-
<button type="button" onClick={() => navigate(1)} className="rounded p-1 hover:bg-[hsl(var(--accent))]">
|
|
102
|
-
<ChevronRight className="h-4 w-4" />
|
|
103
|
-
</button>
|
|
104
|
-
</div>
|
|
105
|
-
<CalendarGrid year={view.year} month={view.month} selected={value} onSelect={handleSelect} />
|
|
106
|
-
</PopoverContent>
|
|
107
|
-
</Popover>
|
|
108
|
-
)
|
|
109
|
-
}
|
|
1
|
+
export * from './DatePicker'
|
package/src/forms/field.tsx
CHANGED
|
@@ -1,62 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { cn } from '../utils/cn'
|
|
3
|
-
|
|
4
|
-
export interface FieldProps {
|
|
5
|
-
label?: string
|
|
6
|
-
description?: string
|
|
7
|
-
error?: string
|
|
8
|
-
required?: boolean
|
|
9
|
-
children: React.ReactNode
|
|
10
|
-
className?: string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function Field({ label, description, error, required, children, className }: FieldProps) {
|
|
14
|
-
return (
|
|
15
|
-
<div className={cn('flex flex-col gap-1.5', className)}>
|
|
16
|
-
{label && <FieldLabel required={required}>{label}</FieldLabel>}
|
|
17
|
-
{children}
|
|
18
|
-
{description && !error && <FieldDescription>{description}</FieldDescription>}
|
|
19
|
-
{error && <FieldError>{error}</FieldError>}
|
|
20
|
-
</div>
|
|
21
|
-
)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface FieldLabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
|
|
25
|
-
required?: boolean
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export const FieldLabel = React.forwardRef<HTMLLabelElement, FieldLabelProps>(
|
|
29
|
-
({ className, required, children, ...props }, ref) => (
|
|
30
|
-
<label
|
|
31
|
-
ref={ref}
|
|
32
|
-
className={cn('text-sm font-medium leading-none text-[hsl(var(--foreground))]', className)}
|
|
33
|
-
{...props}
|
|
34
|
-
>
|
|
35
|
-
{children}
|
|
36
|
-
{required && <span className="ml-1 text-[hsl(var(--destructive))]">*</span>}
|
|
37
|
-
</label>
|
|
38
|
-
)
|
|
39
|
-
)
|
|
40
|
-
FieldLabel.displayName = 'FieldLabel'
|
|
41
|
-
|
|
42
|
-
export const FieldDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
|
43
|
-
({ className, ...props }, ref) => (
|
|
44
|
-
<p
|
|
45
|
-
ref={ref}
|
|
46
|
-
className={cn('text-xs text-[hsl(var(--muted-foreground))]', className)}
|
|
47
|
-
{...props}
|
|
48
|
-
/>
|
|
49
|
-
)
|
|
50
|
-
)
|
|
51
|
-
FieldDescription.displayName = 'FieldDescription'
|
|
52
|
-
|
|
53
|
-
export const FieldError = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
|
54
|
-
({ className, ...props }, ref) => (
|
|
55
|
-
<p
|
|
56
|
-
ref={ref}
|
|
57
|
-
className={cn('text-xs text-[hsl(var(--destructive))]', className)}
|
|
58
|
-
{...props}
|
|
59
|
-
/>
|
|
60
|
-
)
|
|
61
|
-
)
|
|
62
|
-
FieldError.displayName = 'FieldError'
|
|
1
|
+
export * from './Field/index'
|
|
@@ -1,101 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { Upload, X } from 'lucide-react'
|
|
3
|
-
import { cn } from '../utils/cn'
|
|
4
|
-
|
|
5
|
-
export interface FileUploadProps {
|
|
6
|
-
accept?: string
|
|
7
|
-
multiple?: boolean
|
|
8
|
-
maxSize?: number
|
|
9
|
-
onFiles?: (files: File[]) => void
|
|
10
|
-
className?: string
|
|
11
|
-
disabled?: boolean
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function formatBytes(bytes: number) {
|
|
15
|
-
if (bytes < 1024) return `${bytes} B`
|
|
16
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
17
|
-
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function filterBySize(files: File[], maxSize?: number) {
|
|
21
|
-
return maxSize ? files.filter(f => f.size <= maxSize) : files
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function FileUpload({ accept, multiple, maxSize, onFiles, className, disabled }: FileUploadProps) {
|
|
25
|
-
const inputRef = useRef<HTMLInputElement>(null)
|
|
26
|
-
const [dragging, setDragging] = useState(false)
|
|
27
|
-
|
|
28
|
-
function handleFiles(raw: FileList | null) {
|
|
29
|
-
if (!raw) return
|
|
30
|
-
const files = filterBySize(Array.from(raw), maxSize)
|
|
31
|
-
onFiles?.(files)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function handleDrop(e: React.DragEvent) {
|
|
35
|
-
e.preventDefault()
|
|
36
|
-
setDragging(false)
|
|
37
|
-
if (!disabled) handleFiles(e.dataTransfer.files)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<div
|
|
42
|
-
role="button"
|
|
43
|
-
tabIndex={disabled ? -1 : 0}
|
|
44
|
-
onClick={() => !disabled && inputRef.current?.click()}
|
|
45
|
-
onKeyDown={e => e.key === 'Enter' && !disabled && inputRef.current?.click()}
|
|
46
|
-
onDragOver={e => { e.preventDefault(); if (!disabled) setDragging(true) }}
|
|
47
|
-
onDragLeave={() => setDragging(false)}
|
|
48
|
-
onDrop={handleDrop}
|
|
49
|
-
className={cn(
|
|
50
|
-
'flex flex-col items-center justify-center gap-2 rounded-md border-2 border-dashed border-[hsl(var(--border))] p-8 text-center transition-colors cursor-pointer',
|
|
51
|
-
dragging && 'border-[hsl(var(--primary))] bg-[hsl(var(--accent))]',
|
|
52
|
-
disabled && 'cursor-not-allowed opacity-50',
|
|
53
|
-
className
|
|
54
|
-
)}
|
|
55
|
-
>
|
|
56
|
-
<Upload className="h-8 w-8 text-[hsl(var(--muted-foreground))]" />
|
|
57
|
-
<div>
|
|
58
|
-
<p className="text-sm font-medium">Drag files here or click to upload</p>
|
|
59
|
-
{maxSize && <p className="text-xs text-[hsl(var(--muted-foreground))]">Max size: {formatBytes(maxSize)}</p>}
|
|
60
|
-
</div>
|
|
61
|
-
<input
|
|
62
|
-
ref={inputRef}
|
|
63
|
-
type="file"
|
|
64
|
-
accept={accept}
|
|
65
|
-
multiple={multiple}
|
|
66
|
-
className="hidden"
|
|
67
|
-
onChange={e => handleFiles(e.target.files)}
|
|
68
|
-
disabled={disabled}
|
|
69
|
-
/>
|
|
70
|
-
</div>
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface FileUploadPreviewProps {
|
|
75
|
-
files: File[]
|
|
76
|
-
onRemove: (index: number) => void
|
|
77
|
-
className?: string
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function FileUploadPreview({ files, onRemove, className }: FileUploadPreviewProps) {
|
|
81
|
-
if (!files.length) return null
|
|
82
|
-
return (
|
|
83
|
-
<ul className={cn('flex flex-col gap-1', className)}>
|
|
84
|
-
{files.map((file, i) => (
|
|
85
|
-
<li key={i} className="flex items-center justify-between rounded-md border border-[hsl(var(--border))] px-3 py-2 text-sm">
|
|
86
|
-
<span className="min-w-0 truncate text-[hsl(var(--foreground))]">{file.name}</span>
|
|
87
|
-
<div className="ml-2 flex shrink-0 items-center gap-2">
|
|
88
|
-
<span className="text-xs text-[hsl(var(--muted-foreground))]">{formatBytes(file.size)}</span>
|
|
89
|
-
<button
|
|
90
|
-
type="button"
|
|
91
|
-
onClick={() => onRemove(i)}
|
|
92
|
-
className="text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--destructive))] transition-colors"
|
|
93
|
-
>
|
|
94
|
-
<X className="h-4 w-4" />
|
|
95
|
-
</button>
|
|
96
|
-
</div>
|
|
97
|
-
</li>
|
|
98
|
-
))}
|
|
99
|
-
</ul>
|
|
100
|
-
)
|
|
101
|
-
}
|
|
1
|
+
export * from './FileUpload'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './FloatingLabelInput'
|
package/src/forms/form.tsx
CHANGED
|
@@ -1,57 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { FormProvider, Controller, useFormContext, type FieldValues, type ControllerProps, type FieldPath } from 'react-hook-form'
|
|
3
|
-
import { cn } from '../utils/cn'
|
|
4
|
-
|
|
5
|
-
export { FormProvider as Form }
|
|
6
|
-
|
|
7
|
-
export function FormField<T extends FieldValues, N extends FieldPath<T>>(
|
|
8
|
-
props: ControllerProps<T, N>
|
|
9
|
-
) {
|
|
10
|
-
return <Controller {...props} />
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function FormItem({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
14
|
-
return <div className={cn('flex flex-col gap-1.5', className)} {...props} />
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface FormLabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
|
|
18
|
-
error?: boolean
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export const FormLabel = React.forwardRef<HTMLLabelElement, FormLabelProps>(
|
|
22
|
-
({ className, error, ...props }, ref) => (
|
|
23
|
-
<label
|
|
24
|
-
ref={ref}
|
|
25
|
-
className={cn(
|
|
26
|
-
'text-sm font-medium leading-none',
|
|
27
|
-
error ? 'text-[hsl(var(--destructive))]' : 'text-[hsl(var(--foreground))]',
|
|
28
|
-
className
|
|
29
|
-
)}
|
|
30
|
-
{...props}
|
|
31
|
-
/>
|
|
32
|
-
)
|
|
33
|
-
)
|
|
34
|
-
FormLabel.displayName = 'FormLabel'
|
|
35
|
-
|
|
36
|
-
export const FormControl = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
37
|
-
({ ...props }, ref) => {
|
|
38
|
-
const id = useId()
|
|
39
|
-
return <div ref={ref} id={id} {...props} />
|
|
40
|
-
}
|
|
41
|
-
)
|
|
42
|
-
FormControl.displayName = 'FormControl'
|
|
43
|
-
|
|
44
|
-
export function FormDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
|
|
45
|
-
return <p className={cn('text-xs text-[hsl(var(--muted-foreground))]', className)} {...props} />
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function FormMessage({ className, children, name }: React.HTMLAttributes<HTMLParagraphElement> & { name?: string }) {
|
|
49
|
-
const { formState } = useFormContext()
|
|
50
|
-
const error = name ? formState.errors[name] : undefined
|
|
51
|
-
const message = error?.message ? String(error.message) : children
|
|
52
|
-
|
|
53
|
-
if (!message) return null
|
|
54
|
-
return (
|
|
55
|
-
<p className={cn('text-xs text-[hsl(var(--destructive))]', className)}>{message}</p>
|
|
56
|
-
)
|
|
57
|
-
}
|
|
1
|
+
export * from './Form/index'
|
|
@@ -1,53 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { ChevronDown, ChevronRight } from 'lucide-react'
|
|
3
|
-
import { cn } from '../utils/cn'
|
|
4
|
-
|
|
5
|
-
export interface ObjectFieldProps {
|
|
6
|
-
title: string
|
|
7
|
-
description?: string
|
|
8
|
-
collapsible?: boolean
|
|
9
|
-
defaultOpen?: boolean
|
|
10
|
-
children: React.ReactNode
|
|
11
|
-
className?: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function ObjectField({
|
|
15
|
-
title,
|
|
16
|
-
description,
|
|
17
|
-
collapsible = false,
|
|
18
|
-
defaultOpen = true,
|
|
19
|
-
children,
|
|
20
|
-
className,
|
|
21
|
-
}: ObjectFieldProps) {
|
|
22
|
-
const [open, setOpen] = useState(defaultOpen)
|
|
23
|
-
|
|
24
|
-
return (
|
|
25
|
-
<div className={cn('rounded-md border border-[hsl(var(--border))] overflow-hidden', className)}>
|
|
26
|
-
<div
|
|
27
|
-
className={cn(
|
|
28
|
-
'flex items-center justify-between px-4 py-3 bg-[hsl(var(--muted)/0.4)]',
|
|
29
|
-
collapsible && 'cursor-pointer hover:bg-[hsl(var(--accent))] transition-colors'
|
|
30
|
-
)}
|
|
31
|
-
onClick={() => collapsible && setOpen(o => !o)}
|
|
32
|
-
role={collapsible ? 'button' : undefined}
|
|
33
|
-
tabIndex={collapsible ? 0 : undefined}
|
|
34
|
-
onKeyDown={e => collapsible && e.key === 'Enter' && setOpen(o => !o)}
|
|
35
|
-
>
|
|
36
|
-
<div>
|
|
37
|
-
<h4 className="text-sm font-semibold text-[hsl(var(--foreground))]">{title}</h4>
|
|
38
|
-
{description && <p className="text-xs text-[hsl(var(--muted-foreground))]">{description}</p>}
|
|
39
|
-
</div>
|
|
40
|
-
{collapsible && (
|
|
41
|
-
open
|
|
42
|
-
? <ChevronDown className="h-4 w-4 text-[hsl(var(--muted-foreground))]" />
|
|
43
|
-
: <ChevronRight className="h-4 w-4 text-[hsl(var(--muted-foreground))]" />
|
|
44
|
-
)}
|
|
45
|
-
</div>
|
|
46
|
-
{(!collapsible || open) && (
|
|
47
|
-
<div className="p-4 flex flex-col gap-4">
|
|
48
|
-
{children}
|
|
49
|
-
</div>
|
|
50
|
-
)}
|
|
51
|
-
</div>
|
|
52
|
-
)
|
|
53
|
-
}
|
|
1
|
+
export * from './ObjectField'
|
|
@@ -1,54 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { Eye, EyeOff } from 'lucide-react'
|
|
3
|
-
import { Input, type InputProps } from '../primitives/input'
|
|
4
|
-
import { cn } from '../utils/cn'
|
|
5
|
-
|
|
6
|
-
export interface PasswordInputProps extends InputProps {
|
|
7
|
-
strength?: boolean
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function getStrength(value: string): { level: 'weak' | 'medium' | 'strong'; width: string; color: string } {
|
|
11
|
-
const has = (re: RegExp) => re.test(value)
|
|
12
|
-
const score = [value.length >= 8, has(/[A-Z]/), has(/[0-9]/), has(/[^A-Za-z0-9]/)].filter(Boolean).length
|
|
13
|
-
if (score <= 1) return { level: 'weak', width: 'w-1/3', color: 'bg-[hsl(var(--destructive))]' }
|
|
14
|
-
if (score <= 3) return { level: 'medium', width: 'w-2/3', color: 'bg-yellow-500' }
|
|
15
|
-
return { level: 'strong', width: 'w-full', color: 'bg-green-500' }
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
|
|
19
|
-
({ className, strength, ...props }, ref) => {
|
|
20
|
-
const [show, setShow] = useState(false)
|
|
21
|
-
const value = String(props.value ?? '')
|
|
22
|
-
const str = getStrength(value)
|
|
23
|
-
|
|
24
|
-
return (
|
|
25
|
-
<div className="flex flex-col gap-1.5">
|
|
26
|
-
<div className="relative">
|
|
27
|
-
<Input
|
|
28
|
-
ref={ref}
|
|
29
|
-
type={show ? 'text' : 'password'}
|
|
30
|
-
className={cn('pr-9', className)}
|
|
31
|
-
{...props}
|
|
32
|
-
/>
|
|
33
|
-
<button
|
|
34
|
-
type="button"
|
|
35
|
-
onClick={() => setShow(s => !s)}
|
|
36
|
-
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--foreground))] transition-colors"
|
|
37
|
-
tabIndex={-1}
|
|
38
|
-
>
|
|
39
|
-
{show ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
40
|
-
</button>
|
|
41
|
-
</div>
|
|
42
|
-
{strength && value.length > 0 && (
|
|
43
|
-
<div className="space-y-1">
|
|
44
|
-
<div className="h-1 w-full rounded-full bg-[hsl(var(--muted))]">
|
|
45
|
-
<div className={cn('h-full rounded-full transition-all', str.width, str.color)} />
|
|
46
|
-
</div>
|
|
47
|
-
<p className="text-xs text-[hsl(var(--muted-foreground))] capitalize">{str.level}</p>
|
|
48
|
-
</div>
|
|
49
|
-
)}
|
|
50
|
-
</div>
|
|
51
|
-
)
|
|
52
|
-
}
|
|
53
|
-
)
|
|
54
|
-
PasswordInput.displayName = 'PasswordInput'
|
|
1
|
+
export * from './PasswordInput'
|
|
@@ -1,64 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { Search, X } from 'lucide-react'
|
|
3
|
-
import { cn } from '../utils/cn'
|
|
4
|
-
import { type InputProps } from '../primitives/input'
|
|
5
|
-
|
|
6
|
-
export interface SearchInputProps extends Omit<InputProps, 'onChange'> {
|
|
7
|
-
shortcut?: string
|
|
8
|
-
onClear?: () => void
|
|
9
|
-
onChange?: (value: string) => void
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export const SearchInput = React.forwardRef<HTMLInputElement, SearchInputProps>(
|
|
13
|
-
({ className, shortcut, onClear, onChange, value: valueProp, defaultValue, ...props }, ref) => {
|
|
14
|
-
const [internalValue, setInternalValue] = useState(defaultValue ?? '')
|
|
15
|
-
const isControlled = valueProp !== undefined
|
|
16
|
-
const value = isControlled ? String(valueProp) : String(internalValue)
|
|
17
|
-
|
|
18
|
-
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
19
|
-
if (!isControlled) setInternalValue(e.target.value)
|
|
20
|
-
onChange?.(e.target.value)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function handleClear() {
|
|
24
|
-
if (!isControlled) setInternalValue('')
|
|
25
|
-
onChange?.('')
|
|
26
|
-
onClear?.()
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return (
|
|
30
|
-
<div className="relative flex items-center">
|
|
31
|
-
<Search className="pointer-events-none absolute left-2.5 h-4 w-4 text-[hsl(var(--muted-foreground))]" />
|
|
32
|
-
<input
|
|
33
|
-
ref={ref}
|
|
34
|
-
value={value}
|
|
35
|
-
onChange={handleChange}
|
|
36
|
-
className={cn(
|
|
37
|
-
'flex h-9 w-full rounded-md border border-[hsl(var(--input))] bg-transparent pl-8 pr-8 text-sm shadow-sm transition-colors',
|
|
38
|
-
'placeholder:text-[hsl(var(--muted-foreground))]',
|
|
39
|
-
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--ring))]',
|
|
40
|
-
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
41
|
-
className
|
|
42
|
-
)}
|
|
43
|
-
{...props}
|
|
44
|
-
/>
|
|
45
|
-
<div className="absolute right-2.5 flex items-center">
|
|
46
|
-
{value ? (
|
|
47
|
-
<button
|
|
48
|
-
type="button"
|
|
49
|
-
onClick={handleClear}
|
|
50
|
-
className="text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--foreground))] transition-colors"
|
|
51
|
-
>
|
|
52
|
-
<X className="h-4 w-4" />
|
|
53
|
-
</button>
|
|
54
|
-
) : shortcut ? (
|
|
55
|
-
<kbd className="pointer-events-none rounded border border-[hsl(var(--border))] px-1.5 py-0.5 text-[10px] text-[hsl(var(--muted-foreground))]">
|
|
56
|
-
{shortcut}
|
|
57
|
-
</kbd>
|
|
58
|
-
) : null}
|
|
59
|
-
</div>
|
|
60
|
-
</div>
|
|
61
|
-
)
|
|
62
|
-
}
|
|
63
|
-
)
|
|
64
|
-
SearchInput.displayName = 'SearchInput'
|
|
1
|
+
export * from './SearchInput'
|