@otfdashkit/ui 0.1.1 → 0.1.2
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 +206 -125
- package/dist/index.mjs +194 -124
- 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 +6 -3
- package/src/primitives/Button/Button.tsx +18 -0
- package/src/primitives/code-block.tsx +1 -1
- package/src/primitives/comparison.tsx +1 -1
- package/src/primitives/qr-code.tsx +5 -1
- package/src/themes/base.css +18 -0
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'
|
package/src/forms/step-form.tsx
CHANGED
|
@@ -1,113 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { Button } from '../primitives/button'
|
|
3
|
-
import { cn } from '../utils/cn'
|
|
4
|
-
|
|
5
|
-
interface StepFormContextValue {
|
|
6
|
-
currentStep: number
|
|
7
|
-
totalSteps: number
|
|
8
|
-
steps: string[]
|
|
9
|
-
next: () => void
|
|
10
|
-
prev: () => void
|
|
11
|
-
isFirst: boolean
|
|
12
|
-
isLast: boolean
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const StepFormContext = createContext<StepFormContextValue | null>(null)
|
|
16
|
-
|
|
17
|
-
function useStepForm() {
|
|
18
|
-
const ctx = useContext(StepFormContext)
|
|
19
|
-
if (!ctx) throw new Error('useStepForm must be used within StepForm')
|
|
20
|
-
return ctx
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface StepFormProps {
|
|
24
|
-
steps: string[]
|
|
25
|
-
onSubmit: () => void
|
|
26
|
-
children: React.ReactNode
|
|
27
|
-
className?: string
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function StepForm({ steps, onSubmit, children, className }: StepFormProps) {
|
|
31
|
-
const [currentStep, setCurrentStep] = useState(0)
|
|
32
|
-
const totalSteps = steps.length
|
|
33
|
-
|
|
34
|
-
const ctx: StepFormContextValue = {
|
|
35
|
-
currentStep,
|
|
36
|
-
totalSteps,
|
|
37
|
-
steps,
|
|
38
|
-
next: () => setCurrentStep(s => Math.min(s + 1, totalSteps - 1)),
|
|
39
|
-
prev: () => setCurrentStep(s => Math.max(s - 1, 0)),
|
|
40
|
-
isFirst: currentStep === 0,
|
|
41
|
-
isLast: currentStep === totalSteps - 1,
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const childArray = React.Children.toArray(children)
|
|
45
|
-
const currentChild = childArray[currentStep]
|
|
46
|
-
|
|
47
|
-
return (
|
|
48
|
-
<StepFormContext.Provider value={ctx}>
|
|
49
|
-
<div className={cn('flex flex-col gap-6', className)}>
|
|
50
|
-
<StepIndicator steps={steps} currentStep={currentStep} />
|
|
51
|
-
<div>{currentChild}</div>
|
|
52
|
-
<StepFormNavigation onSubmit={onSubmit} />
|
|
53
|
-
</div>
|
|
54
|
-
</StepFormContext.Provider>
|
|
55
|
-
)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function StepIndicator({ steps, currentStep }: { steps: string[]; currentStep: number }) {
|
|
59
|
-
return (
|
|
60
|
-
<div className="flex items-center gap-2">
|
|
61
|
-
{steps.map((label, i) => (
|
|
62
|
-
<React.Fragment key={i}>
|
|
63
|
-
{i > 0 && <div className={cn('h-px flex-1 bg-[hsl(var(--border))]', i <= currentStep && 'bg-[hsl(var(--primary))]')} />}
|
|
64
|
-
<div className="flex flex-col items-center gap-1">
|
|
65
|
-
<div className={cn(
|
|
66
|
-
'flex h-7 w-7 items-center justify-center rounded-full text-xs font-medium border transition-colors',
|
|
67
|
-
i < currentStep && 'bg-[hsl(var(--primary))] border-[hsl(var(--primary))] text-[hsl(var(--primary-foreground))]',
|
|
68
|
-
i === currentStep && 'border-[hsl(var(--primary))] text-[hsl(var(--primary))]',
|
|
69
|
-
i > currentStep && 'border-[hsl(var(--border))] text-[hsl(var(--muted-foreground))]'
|
|
70
|
-
)}>{i + 1}</div>
|
|
71
|
-
<span className="hidden text-[10px] text-[hsl(var(--muted-foreground))] sm:block">{label}</span>
|
|
72
|
-
</div>
|
|
73
|
-
</React.Fragment>
|
|
74
|
-
))}
|
|
75
|
-
</div>
|
|
76
|
-
)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export interface StepFormStepProps {
|
|
80
|
-
title: string
|
|
81
|
-
description?: string
|
|
82
|
-
children: React.ReactNode
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function StepFormStep({ title, description, children }: StepFormStepProps) {
|
|
86
|
-
return (
|
|
87
|
-
<div className="flex flex-col gap-4">
|
|
88
|
-
<div>
|
|
89
|
-
<h3 className="text-base font-semibold text-[hsl(var(--foreground))]">{title}</h3>
|
|
90
|
-
{description && <p className="text-sm text-[hsl(var(--muted-foreground))]">{description}</p>}
|
|
91
|
-
</div>
|
|
92
|
-
{children}
|
|
93
|
-
</div>
|
|
94
|
-
)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export interface StepFormNavigationProps {
|
|
98
|
-
onSubmit?: () => void
|
|
99
|
-
submitLabel?: string
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function StepFormNavigation({ onSubmit, submitLabel = 'Submit' }: StepFormNavigationProps) {
|
|
103
|
-
const { prev, next, isFirst, isLast } = useStepForm()
|
|
104
|
-
return (
|
|
105
|
-
<div className="flex justify-between">
|
|
106
|
-
<Button type="button" variant="outline" onClick={prev} disabled={isFirst}>Previous</Button>
|
|
107
|
-
{isLast
|
|
108
|
-
? <Button type="button" onClick={onSubmit}>{submitLabel}</Button>
|
|
109
|
-
: <Button type="button" onClick={next}>Next</Button>
|
|
110
|
-
}
|
|
111
|
-
</div>
|
|
112
|
-
)
|
|
113
|
-
}
|
|
1
|
+
export * from './StepForm'
|
package/src/index.ts
CHANGED
|
@@ -23,7 +23,7 @@ export * from './primitives/context-menu'
|
|
|
23
23
|
export * from './primitives/dialog'
|
|
24
24
|
export * from './primitives/drawer'
|
|
25
25
|
export * from './primitives/dropdown-menu'
|
|
26
|
-
export * from './primitives/form'
|
|
26
|
+
// export * from './primitives/form' // conflicts with forms/form.tsx (Form, FormField, etc.)
|
|
27
27
|
export * from './primitives/hover-card'
|
|
28
28
|
export * from './primitives/input-otp'
|
|
29
29
|
export * from './primitives/input'
|
|
@@ -59,7 +59,7 @@ export * from './primitives/tooltip'
|
|
|
59
59
|
// components
|
|
60
60
|
export * from './components/banner'
|
|
61
61
|
export * from './components/breadcrumb'
|
|
62
|
-
export
|
|
62
|
+
export { CommandListItem } from './components/command-item'
|
|
63
63
|
export * from './components/data-grid'
|
|
64
64
|
export * from './components/data-table'
|
|
65
65
|
export * from './components/empty-state'
|
|
@@ -132,6 +132,7 @@ export * from './forms/color-picker'
|
|
|
132
132
|
export * from './forms/date-picker'
|
|
133
133
|
export * from './forms/field'
|
|
134
134
|
export * from './forms/file-upload'
|
|
135
|
+
export * from './forms/floating-label-input'
|
|
135
136
|
export * from './forms/form'
|
|
136
137
|
export * from './forms/object-field'
|
|
137
138
|
export * from './forms/password-input'
|
|
@@ -153,7 +154,9 @@ export * from './layouts/stack'
|
|
|
153
154
|
export { PageLayout } from './layout/PageLayout'
|
|
154
155
|
export type { PageHeaderProps } from './layout/PageLayout'
|
|
155
156
|
export { PageContainer, PageMain } from './layout/PageContainer'
|
|
156
|
-
export
|
|
157
|
+
export type { CommandPaletteProps } from './nav/CommandPalette'
|
|
158
|
+
export { CommandPaletteProvider, useCommandPalette, CommandPalette } from './nav/CommandPalette'
|
|
159
|
+
export * from './nav/HotkeysProvider'
|
|
157
160
|
export * from './feedback'
|
|
158
161
|
export * from './text'
|
|
159
162
|
export * from './backgrounds'
|
|
@@ -43,6 +43,24 @@ const buttonVariants = cva(
|
|
|
43
43
|
'hover:border-border/60 hover:bg-accent',
|
|
44
44
|
'active:shadow-none',
|
|
45
45
|
].join(' '),
|
|
46
|
+
/** Shimmer — animated conic gradient sweep with a hover highlight.
|
|
47
|
+
* Loud + premium; reserve for primary CTAs and pricing pages, not
|
|
48
|
+
* for in-product UI (gets tiring fast). Cross-platform parity with
|
|
49
|
+
* @otfdashkit/ui-native via the same `shimmer` variant key. */
|
|
50
|
+
shimmer: [
|
|
51
|
+
'relative isolate overflow-hidden',
|
|
52
|
+
'bg-primary text-primary-foreground',
|
|
53
|
+
'shadow-[0_1px_0_rgba(255,255,255,0.18)_inset,0_-1px_0_rgba(0,0,0,0.25)_inset]',
|
|
54
|
+
'hover:bg-primary/95',
|
|
55
|
+
// Continuous conic-gradient sweep behind the surface.
|
|
56
|
+
'before:absolute before:inset-[-1px] before:rounded-[inherit] before:-z-10',
|
|
57
|
+
'before:bg-[conic-gradient(from_0deg,transparent_0deg,transparent_70deg,rgba(255,255,255,0.55)_90deg,transparent_110deg,transparent_360deg)]',
|
|
58
|
+
'before:animate-[shimmer-rotate_2.6s_linear_infinite]',
|
|
59
|
+
// Diagonal gloss that sweeps on hover.
|
|
60
|
+
'after:absolute after:inset-0 after:rounded-[inherit] after:pointer-events-none',
|
|
61
|
+
'after:bg-[linear-gradient(115deg,transparent_30%,rgba(255,255,255,0.22)_50%,transparent_70%)]',
|
|
62
|
+
'after:translate-x-[-120%] hover:after:translate-x-[120%] after:transition-transform after:duration-[900ms] after:ease-out',
|
|
63
|
+
].join(' '),
|
|
46
64
|
},
|
|
47
65
|
size: {
|
|
48
66
|
sm: 'h-8 px-3 text-xs rounded-md',
|
|
@@ -106,7 +106,7 @@ export const CodeBlock = React.forwardRef<HTMLDivElement, CodeBlockProps>(
|
|
|
106
106
|
return (
|
|
107
107
|
<div
|
|
108
108
|
ref={(node) => {
|
|
109
|
-
containerRef.current = node
|
|
109
|
+
(containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node
|
|
110
110
|
if (typeof ref === 'function') ref(node)
|
|
111
111
|
else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node
|
|
112
112
|
}}
|
|
@@ -100,7 +100,7 @@ export const Comparison = React.forwardRef<HTMLDivElement, ComparisonProps>(
|
|
|
100
100
|
return (
|
|
101
101
|
<div
|
|
102
102
|
ref={(node) => {
|
|
103
|
-
containerRef.current = node
|
|
103
|
+
(containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node
|
|
104
104
|
if (typeof ref === 'function') ref(node)
|
|
105
105
|
else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node
|
|
106
106
|
}}
|
|
@@ -20,7 +20,11 @@ export type QrCodeProps = Omit<React.HTMLAttributes<HTMLDivElement>, 'children'>
|
|
|
20
20
|
src: string
|
|
21
21
|
height: number
|
|
22
22
|
width: number
|
|
23
|
-
excavate
|
|
23
|
+
excavate: boolean
|
|
24
|
+
x?: number
|
|
25
|
+
y?: number
|
|
26
|
+
opacity?: number
|
|
27
|
+
crossOrigin?: 'anonymous' | 'use-credentials' | ''
|
|
24
28
|
}
|
|
25
29
|
}
|
|
26
30
|
|
package/src/themes/base.css
CHANGED
|
@@ -23,3 +23,21 @@ h1, h2, h3, h4, h5, h6 {
|
|
|
23
23
|
line-height: var(--line-height-heading, 1.2);
|
|
24
24
|
font-weight: var(--font-weight-heading, 600);
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
/* ── Animations shipped with the SDK ────────────────────────────────────────
|
|
28
|
+
Consumers get these automatically by importing '@otfdashkit/ui/styles'.
|
|
29
|
+
Keep keyframe names unique to avoid collision with consumer-defined ones. */
|
|
30
|
+
|
|
31
|
+
/* Conic-gradient shimmer rotation — used by Button variant="shimmer".
|
|
32
|
+
Animation: 360deg sweep, 2.6s linear, infinite. */
|
|
33
|
+
@keyframes shimmer-rotate {
|
|
34
|
+
0% { --shimmer-angle: 0deg; transform: rotate(0deg); }
|
|
35
|
+
100% { --shimmer-angle: 360deg; transform: rotate(360deg); }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Concentric ripple expand — reserved for future use (RippleLoader, ping).
|
|
39
|
+
The existing motion/Ripple is click-triggered; this is for ambient pulses. */
|
|
40
|
+
@keyframes pulse-ring {
|
|
41
|
+
0% { transform: scale(0.4); opacity: 0.8; }
|
|
42
|
+
100% { transform: scale(1.6); opacity: 0; }
|
|
43
|
+
}
|