@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.
Files changed (120) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +28 -41
  3. package/dist/index.js +972 -655
  4. package/dist/index.mjs +826 -525
  5. package/package.json +5 -4
  6. package/src/advanced/BulkActions/BulkActions.tsx +2 -2
  7. package/src/advanced/CommandBar/CommandBar.tsx +2 -2
  8. package/src/advanced/Filters/Filters.tsx +6 -6
  9. package/src/advanced/Kanban/Kanban.tsx +1 -1
  10. package/src/advanced/ToggleButton/ToggleButton.tsx +1 -1
  11. package/src/advanced/bulk-actions.tsx +1 -46
  12. package/src/advanced/command-bar.tsx +1 -115
  13. package/src/advanced/filters.tsx +1 -103
  14. package/src/advanced/kanban.tsx +1 -95
  15. package/src/advanced/toggle-button.tsx +1 -78
  16. package/src/blocks/ChatDetail/ChatDetail.tsx +4 -4
  17. package/src/blocks/FeedbackModal/FeedbackModal.tsx +3 -3
  18. package/src/blocks/FileCards/FileCards.tsx +2 -2
  19. package/src/blocks/FilesList/FilesList.tsx +3 -3
  20. package/src/blocks/IntegrationCard/IntegrationCard.tsx +3 -3
  21. package/src/blocks/InviteModal/InviteModal.tsx +7 -7
  22. package/src/blocks/ManageTagsModal/ManageTagsModal.tsx +4 -4
  23. package/src/blocks/MessagesCard/MessagesCard.tsx +3 -3
  24. package/src/blocks/MetricCard/MetricCard.tsx +3 -3
  25. package/src/blocks/NotificationSettings/NotificationSettings.tsx +4 -4
  26. package/src/blocks/OrganizationMenu/OrganizationMenu.tsx +30 -11
  27. package/src/blocks/RolesMenu/RolesMenu.tsx +4 -4
  28. package/src/blocks/SelectUsersModal/SelectUsersModal.tsx +5 -5
  29. package/src/blocks/SidebarLayoutDashboard/SidebarLayoutDashboard.tsx +5 -5
  30. package/src/blocks/SidebarLayoutGroups/SidebarLayoutGroups.tsx +2 -2
  31. package/src/blocks/SidebarLayoutMinimal/SidebarLayoutMinimal.tsx +3 -3
  32. package/src/blocks/SidebarLayoutSearch/SidebarLayoutSearch.tsx +3 -3
  33. package/src/blocks/SidebarLayoutUser/SidebarLayoutUser.tsx +4 -4
  34. package/src/blocks/SortableTaskList/SortableTaskList.tsx +3 -3
  35. package/src/blocks/StackedLayoutBranded/StackedLayoutBranded.tsx +4 -4
  36. package/src/blocks/StackedLayoutTabs/StackedLayoutTabs.tsx +4 -4
  37. package/src/blocks/TaskCard/TaskCard.tsx +4 -4
  38. package/src/blocks/UserMenu/UserMenu.tsx +3 -3
  39. package/src/blocks/WorkspaceMembers/WorkspaceMembers.tsx +5 -5
  40. package/src/blocks/chat-detail.tsx +1 -73
  41. package/src/blocks/feedback-modal.tsx +1 -68
  42. package/src/blocks/file-cards.tsx +1 -56
  43. package/src/blocks/files-list.tsx +1 -72
  44. package/src/blocks/integration-card.tsx +1 -50
  45. package/src/blocks/invite-modal.tsx +1 -69
  46. package/src/blocks/manage-tags-modal.tsx +1 -63
  47. package/src/blocks/messages-card.tsx +1 -59
  48. package/src/blocks/metric-card.tsx +1 -79
  49. package/src/blocks/notification-settings.tsx +1 -61
  50. package/src/blocks/organization-menu.tsx +1 -85
  51. package/src/blocks/roles-menu.tsx +1 -61
  52. package/src/blocks/select-users-modal.tsx +1 -64
  53. package/src/blocks/sidebar-layout-dashboard.tsx +1 -97
  54. package/src/blocks/sidebar-layout-groups.tsx +1 -79
  55. package/src/blocks/sidebar-layout-minimal.tsx +1 -73
  56. package/src/blocks/sidebar-layout-search.tsx +1 -79
  57. package/src/blocks/sidebar-layout-user.tsx +1 -82
  58. package/src/blocks/sortable-task-list.tsx +1 -70
  59. package/src/blocks/stacked-layout-branded.tsx +1 -64
  60. package/src/blocks/stacked-layout-tabs.tsx +1 -80
  61. package/src/blocks/task-card.tsx +1 -88
  62. package/src/blocks/user-menu.tsx +1 -62
  63. package/src/blocks/workspace-members.tsx +1 -61
  64. package/src/charts/sparkline.tsx +1 -1
  65. package/src/components/DataGrid/DataGrid.tsx +1 -1
  66. package/src/components/DataTable/DataTable.tsx +4 -4
  67. package/src/components/EmptyState/EmptyState.tsx +6 -22
  68. package/src/components/Persona/Persona.tsx +1 -1
  69. package/src/components/Stat/Stat.tsx +1 -1
  70. package/src/components/Stepper/Stepper.tsx +13 -2
  71. package/src/components/banner.tsx +1 -203
  72. package/src/components/breadcrumb.tsx +1 -58
  73. package/src/components/command-item.tsx +1 -36
  74. package/src/components/data-grid.tsx +1 -125
  75. package/src/components/data-table.tsx +1 -139
  76. package/src/components/empty-state.tsx +1 -31
  77. package/src/components/hotkeys.tsx +1 -31
  78. package/src/components/icon-badge.tsx +1 -35
  79. package/src/components/loading-overlay.tsx +1 -38
  80. package/src/components/persona.tsx +1 -38
  81. package/src/components/property-list.tsx +1 -29
  82. package/src/components/stat.tsx +1 -51
  83. package/src/components/stepper.tsx +1 -69
  84. package/src/components/structured-list.tsx +1 -51
  85. package/src/components/timeline.tsx +1 -69
  86. package/src/components/toaster.tsx +1 -42
  87. package/src/data/DataList.tsx +1 -1
  88. package/src/engagement/Tour/Tour.tsx +1 -1
  89. package/src/engagement/beacon.tsx +1 -50
  90. package/src/engagement/tour.tsx +1 -106
  91. package/src/forms/ArrayField/ArrayField.tsx +2 -2
  92. package/src/forms/AutoForm/AutoForm.tsx +9 -8
  93. package/src/forms/DatePicker/DatePicker.tsx +3 -3
  94. package/src/forms/FloatingLabelInput/FloatingLabelInput.tsx +104 -0
  95. package/src/forms/FloatingLabelInput/index.ts +1 -0
  96. package/src/forms/PasswordInput/PasswordInput.tsx +2 -2
  97. package/src/forms/SearchInput/SearchInput.tsx +2 -2
  98. package/src/forms/StepForm/StepForm.tsx +2 -2
  99. package/src/forms/array-field.tsx +1 -61
  100. package/src/forms/auto-form.tsx +1 -133
  101. package/src/forms/date-picker.tsx +1 -109
  102. package/src/forms/field.tsx +1 -62
  103. package/src/forms/file-upload.tsx +1 -101
  104. package/src/forms/floating-label-input.tsx +1 -0
  105. package/src/forms/form.tsx +1 -57
  106. package/src/forms/object-field.tsx +1 -53
  107. package/src/forms/password-input.tsx +1 -54
  108. package/src/forms/search-input.tsx +1 -64
  109. package/src/forms/step-form.tsx +1 -113
  110. package/src/index.ts +11 -3
  111. package/src/primitives/Button/Button.tsx +18 -0
  112. package/src/primitives/cart-line-item.tsx +81 -0
  113. package/src/primitives/code-block.tsx +1 -1
  114. package/src/primitives/comparison.tsx +1 -1
  115. package/src/primitives/empty-cart.tsx +52 -0
  116. package/src/primitives/price-tag.tsx +62 -0
  117. package/src/primitives/qr-code.tsx +5 -1
  118. package/src/primitives/quantity-stepper.tsx +92 -0
  119. package/src/primitives/variant-picker.tsx +74 -0
  120. package/src/themes/base.css +18 -0
@@ -1,133 +1 @@
1
- import React from 'react'
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
- import React, { useState } from 'react'
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'
@@ -1,62 +1 @@
1
- import React from 'react'
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
- import React, { useRef, useState } from 'react'
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'
@@ -1,57 +1 @@
1
- import React, { useId } from 'react'
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
- import React, { useState } from 'react'
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
- import React, { useState } from 'react'
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
- import React, { useState } from 'react'
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'