@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.
Files changed (115) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +28 -41
  3. package/dist/index.js +206 -125
  4. package/dist/index.mjs +194 -124
  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 +6 -3
  111. package/src/primitives/Button/Button.tsx +18 -0
  112. package/src/primitives/code-block.tsx +1 -1
  113. package/src/primitives/comparison.tsx +1 -1
  114. package/src/primitives/qr-code.tsx +5 -1
  115. package/src/themes/base.css +18 -0
@@ -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'
@@ -1,113 +1 @@
1
- import React, { createContext, useContext, useState } from 'react'
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 * from './components/command-item'
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 * from './nav'
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?: boolean
23
+ excavate: boolean
24
+ x?: number
25
+ y?: number
26
+ opacity?: number
27
+ crossOrigin?: 'anonymous' | 'use-credentials' | ''
24
28
  }
25
29
  }
26
30
 
@@ -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
+ }