@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,88 +1 @@
1
- import React from 'react'
2
- import { Calendar } from 'lucide-react'
3
- import { cn } from '../utils/cn'
4
- import { Card, CardContent } from '../primitives/card'
5
- import { Avatar, AvatarFallback } from '../primitives/avatar'
6
- import { Badge } from '../primitives/badge'
7
-
8
- const PRIORITY_COLOR: Record<string, string> = {
9
- High: 'bg-red-500/10 text-red-600 border-red-200',
10
- Medium: 'bg-yellow-500/10 text-yellow-600 border-yellow-200',
11
- Low: 'bg-green-500/10 text-green-600 border-green-200',
12
- }
13
-
14
- export interface TaskCardProps {
15
- title?: string
16
- description?: string
17
- assigneeInitials?: string
18
- dueDate?: string
19
- priority?: 'High' | 'Medium' | 'Low'
20
- className?: string
21
- }
22
-
23
- export function TaskCard({
24
- title = 'Redesign onboarding flow',
25
- description = 'Update the user onboarding experience with the new design language.',
26
- assigneeInitials = 'AC',
27
- dueDate = 'Mar 20',
28
- priority = 'High',
29
- className,
30
- }: TaskCardProps) {
31
- return (
32
- <Card className={cn('cursor-pointer hover:shadow-md transition-shadow', className)}>
33
- <CardContent className="p-4 space-y-3">
34
- <div className="flex items-start justify-between gap-2">
35
- <p className="text-sm font-semibold leading-tight">{title}</p>
36
- <span className={cn('text-[10px] px-1.5 py-0.5 rounded-full border font-medium shrink-0', PRIORITY_COLOR[priority])}>
37
- {priority}
38
- </span>
39
- </div>
40
- <p className="text-xs text-[hsl(var(--muted-foreground))] leading-relaxed line-clamp-2">{description}</p>
41
- <div className="flex items-center justify-between">
42
- <Avatar className="h-6 w-6"><AvatarFallback className="text-[10px]">{assigneeInitials}</AvatarFallback></Avatar>
43
- <div className="flex items-center gap-1 text-xs text-[hsl(var(--muted-foreground))]">
44
- <Calendar className="h-3 w-3" />{dueDate}
45
- </div>
46
- </div>
47
- </CardContent>
48
- </Card>
49
- )
50
- }
51
-
52
- const LABEL_COLORS = ['bg-blue-100 text-blue-700', 'bg-purple-100 text-purple-700', 'bg-green-100 text-green-700']
53
-
54
- export interface TaskCardWithLabelsProps extends TaskCardProps {
55
- labels?: string[]
56
- }
57
-
58
- export function TaskCardWithLabels({
59
- labels = ['Design', 'Frontend'],
60
- ...props
61
- }: TaskCardWithLabelsProps) {
62
- return (
63
- <Card className={cn('cursor-pointer hover:shadow-md transition-shadow', props.className)}>
64
- <CardContent className="p-4 space-y-3">
65
- <div className="flex items-start justify-between gap-2">
66
- <p className="text-sm font-semibold leading-tight">{props.title ?? 'Redesign onboarding flow'}</p>
67
- <span className={cn('text-[10px] px-1.5 py-0.5 rounded-full border font-medium shrink-0', PRIORITY_COLOR[props.priority ?? 'High'])}>
68
- {props.priority ?? 'High'}
69
- </span>
70
- </div>
71
- <p className="text-xs text-[hsl(var(--muted-foreground))] leading-relaxed line-clamp-2">
72
- {props.description ?? 'Update the user onboarding experience with the new design language.'}
73
- </p>
74
- <div className="flex flex-wrap gap-1">
75
- {labels.map((l, i) => (
76
- <span key={l} className={cn('text-[10px] px-1.5 py-0.5 rounded-full font-medium', LABEL_COLORS[i % LABEL_COLORS.length])}>{l}</span>
77
- ))}
78
- </div>
79
- <div className="flex items-center justify-between">
80
- <Avatar className="h-6 w-6"><AvatarFallback className="text-[10px]">{props.assigneeInitials ?? 'AC'}</AvatarFallback></Avatar>
81
- <div className="flex items-center gap-1 text-xs text-[hsl(var(--muted-foreground))]">
82
- <Calendar className="h-3 w-3" />{props.dueDate ?? 'Mar 20'}
83
- </div>
84
- </div>
85
- </CardContent>
86
- </Card>
87
- )
88
- }
1
+ export * from './TaskCard'
@@ -1,62 +1 @@
1
- import React from 'react'
2
- import { ChevronDown, User, Settings, CreditCard, LogOut } from 'lucide-react'
3
- import { cn } from '../utils/cn'
4
- import { Avatar, AvatarFallback } from '../primitives/avatar'
5
- import {
6
- DropdownMenu,
7
- DropdownMenuContent,
8
- DropdownMenuItem,
9
- DropdownMenuLabel,
10
- DropdownMenuSeparator,
11
- DropdownMenuTrigger,
12
- } from '../primitives/dropdown-menu'
13
-
14
- export interface UserMenuProps {
15
- name?: string
16
- email?: string
17
- initials?: string
18
- onProfile?: () => void
19
- onSettings?: () => void
20
- onBilling?: () => void
21
- onSignOut?: () => void
22
- className?: string
23
- }
24
-
25
- export function UserMenu({
26
- name = 'Jane Doe',
27
- email = 'jane@acme.com',
28
- initials = 'JD',
29
- onProfile,
30
- onSettings,
31
- onBilling,
32
- onSignOut,
33
- className,
34
- }: UserMenuProps) {
35
- return (
36
- <DropdownMenu>
37
- <DropdownMenuTrigger asChild>
38
- <button className={cn('flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors hover:bg-[hsl(var(--accent))] focus:outline-none', className)}>
39
- <Avatar className="h-7 w-7">
40
- <AvatarFallback className="text-xs">{initials}</AvatarFallback>
41
- </Avatar>
42
- <span className="font-medium max-w-[120px] truncate">{name}</span>
43
- <ChevronDown className="h-3.5 w-3.5 text-[hsl(var(--muted-foreground))]" />
44
- </button>
45
- </DropdownMenuTrigger>
46
- <DropdownMenuContent align="end" className="w-52">
47
- <DropdownMenuLabel className="font-normal">
48
- <p className="text-sm font-medium">{name}</p>
49
- <p className="text-xs text-[hsl(var(--muted-foreground))]">{email}</p>
50
- </DropdownMenuLabel>
51
- <DropdownMenuSeparator />
52
- <DropdownMenuItem onClick={onProfile}><User className="h-4 w-4" />Profile</DropdownMenuItem>
53
- <DropdownMenuItem onClick={onSettings}><Settings className="h-4 w-4" />Account Settings</DropdownMenuItem>
54
- <DropdownMenuItem onClick={onBilling}><CreditCard className="h-4 w-4" />Billing</DropdownMenuItem>
55
- <DropdownMenuSeparator />
56
- <DropdownMenuItem onClick={onSignOut} className="text-[hsl(var(--destructive))] focus:text-[hsl(var(--destructive))]">
57
- <LogOut className="h-4 w-4" />Sign out
58
- </DropdownMenuItem>
59
- </DropdownMenuContent>
60
- </DropdownMenu>
61
- )
62
- }
1
+ export * from './UserMenu'
@@ -1,61 +1 @@
1
- import React, { useState } from 'react'
2
- import { Search, X } from 'lucide-react'
3
- import { cn } from '../utils/cn'
4
- import { Input } from '../primitives/input'
5
- import { Avatar, AvatarFallback } from '../primitives/avatar'
6
- import { Badge } from '../primitives/badge'
7
- import { Button } from '../primitives/button'
8
-
9
- const MEMBERS = [
10
- { id: '1', name: 'Jane Doe', email: 'jane@acme.com', role: 'Owner', initials: 'JD' },
11
- { id: '2', name: 'Bob Smith', email: 'bob@acme.com', role: 'Admin', initials: 'BS' },
12
- { id: '3', name: 'Alice Chen', email: 'alice@acme.com', role: 'Member', initials: 'AC' },
13
- { id: '4', name: 'Tom Ray', email: 'tom@acme.com', role: 'Member', initials: 'TR' },
14
- { id: '5', name: 'Sara Kim', email: 'sara@acme.com', role: 'Viewer', initials: 'SK' },
15
- ]
16
-
17
- const roleVariant = (role: string): 'default' | 'secondary' | 'outline' =>
18
- role === 'Owner' ? 'default' : role === 'Admin' ? 'secondary' : 'outline'
19
-
20
- export interface WorkspaceMembersProps { className?: string }
21
-
22
- export function WorkspaceMembers({ className }: WorkspaceMembersProps) {
23
- const [query, setQuery] = useState('')
24
- const [members, setMembers] = useState(MEMBERS)
25
- const filtered = members.filter(m =>
26
- m.name.toLowerCase().includes(query.toLowerCase()) ||
27
- m.email.toLowerCase().includes(query.toLowerCase())
28
- )
29
-
30
- return (
31
- <div className={cn('rounded-md border border-[hsl(var(--border))] bg-[hsl(var(--card))]', className)}>
32
- <div className="flex items-center justify-between p-4 border-b border-[hsl(var(--border))]">
33
- <div>
34
- <h2 className="text-sm font-semibold">Members</h2>
35
- <p className="text-xs text-[hsl(var(--muted-foreground))]">{members.length} members</p>
36
- </div>
37
- <div className="relative">
38
- <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-[hsl(var(--muted-foreground))]" />
39
- <Input className="h-8 pl-8 text-xs w-48" placeholder="Search members..." value={query} onChange={e => setQuery(e.target.value)} />
40
- </div>
41
- </div>
42
- <div className="divide-y divide-[hsl(var(--border))]">
43
- {filtered.map(m => (
44
- <div key={m.id} className="flex items-center gap-3 px-4 py-3">
45
- <Avatar className="h-8 w-8"><AvatarFallback className="text-xs">{m.initials}</AvatarFallback></Avatar>
46
- <div className="flex-1 min-w-0">
47
- <p className="text-sm font-medium truncate">{m.name}</p>
48
- <p className="text-xs text-[hsl(var(--muted-foreground))] truncate">{m.email}</p>
49
- </div>
50
- <Badge variant={roleVariant(m.role)} className="text-[10px] px-1.5 py-0.5 shrink-0">{m.role}</Badge>
51
- {m.role !== 'Owner' && (
52
- <Button size="icon" variant="ghost" className="h-7 w-7 shrink-0" onClick={() => setMembers(ms => ms.filter(x => x.id !== m.id))}>
53
- <X className="h-3.5 w-3.5" />
54
- </Button>
55
- )}
56
- </div>
57
- ))}
58
- </div>
59
- </div>
60
- )
61
- }
1
+ export * from './WorkspaceMembers'
@@ -38,7 +38,7 @@ export function Sparkline({
38
38
  const max = Math.max(...data)
39
39
  const range = max - min || 1
40
40
  const pad = 2
41
- const lastY = pad + (1 - (data[data.length - 1] - min) / range) * (height - pad * 2)
41
+ const lastY = pad + (1 - ((data.at(-1) ?? 0) - min) / range) * (height - pad * 2)
42
42
 
43
43
  const areaPath = type === 'area'
44
44
  ? `${path} L ${width - pad},${height - pad} L ${pad},${height - pad} Z`
@@ -8,7 +8,7 @@ import {
8
8
  ColumnResizeMode,
9
9
  } from '@tanstack/react-table'
10
10
  import { cn } from '../utils/cn'
11
- import { Checkbox } from '../primitives/checkbox'
11
+ import { Checkbox } from '../../primitives/checkbox'
12
12
 
13
13
  interface DataGridProps<T extends object> {
14
14
  columns: ColumnDef<T, any>[]
@@ -11,10 +11,10 @@ import {
11
11
  } from '@tanstack/react-table'
12
12
  import { ArrowUpDown, ArrowUp, ArrowDown, ChevronLeft, ChevronRight } from 'lucide-react'
13
13
  import { cn } from '../utils/cn'
14
- import { Button } from '../primitives/button'
15
- import { Input } from '../primitives/input'
16
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../primitives/select'
17
- import { Skeleton } from '../primitives/skeleton'
14
+ import { Button } from '../../primitives/button'
15
+ import { Input } from '../../primitives/input'
16
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../primitives/select'
17
+ import { Skeleton } from '../../primitives/skeleton'
18
18
 
19
19
  interface DataTableProps<T> {
20
20
  columns: ColumnDef<T>[]
@@ -1,24 +1,15 @@
1
1
  import React from 'react'
2
2
  import { cn } from '../../utils/cn'
3
- import { Button } from '../../primitives/Button/Button'
3
+ import { Button } from '../../primitives/button'
4
4
 
5
5
  export const EmptyStateIcon = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
6
6
  ({ className, ...props }, ref) => (
7
- <div
8
- ref={ref}
9
- data-slot="empty-state-icon"
10
- className={cn(
11
- 'flex h-12 w-12 items-center justify-center rounded-full',
12
- 'bg-muted text-muted-foreground',
13
- className
14
- )}
15
- {...props}
16
- />
7
+ <div ref={ref} className={cn('flex h-12 w-12 items-center justify-center rounded-full bg-[hsl(var(--muted))] text-[hsl(var(--muted-foreground))]', className)} {...props} />
17
8
  )
18
9
  )
19
10
  EmptyStateIcon.displayName = 'EmptyStateIcon'
20
11
 
21
- export interface EmptyStateProps {
12
+ interface EmptyStateProps {
22
13
  icon?: React.ReactNode
23
14
  title: string
24
15
  description?: string
@@ -62,23 +53,16 @@ export function EmptyState({
62
53
  className,
63
54
  }: EmptyStateProps) {
64
55
  return (
65
- <div
66
- data-slot="empty-state"
67
- className={cn(
68
- 'flex flex-col items-center justify-center text-center',
69
- sizeMap[size],
70
- className
71
- )}
72
- >
56
+ <div className={cn('flex flex-col items-center justify-center text-center', sizeMap[size], className)}>
73
57
  {icon && (
74
58
  <EmptyStateIcon className={iconSizeMap[size]}>
75
59
  {icon}
76
60
  </EmptyStateIcon>
77
61
  )}
78
62
  <div className="space-y-1 max-w-sm">
79
- <h3 className={cn(titleSizeMap[size], 'text-foreground')}>{title}</h3>
63
+ <h3 className={cn(titleSizeMap[size], 'text-[hsl(var(--foreground))]')}>{title}</h3>
80
64
  {description && (
81
- <p className={cn(descSizeMap[size], 'text-muted-foreground')}>
65
+ <p className={cn(descSizeMap[size], 'text-[hsl(var(--muted-foreground))]')}>
82
66
  {description}
83
67
  </p>
84
68
  )}
@@ -1,6 +1,6 @@
1
1
  import React from 'react'
2
2
  import { cn } from '../utils/cn'
3
- import { Avatar, AvatarFallback, AvatarImage } from '../primitives/avatar'
3
+ import { Avatar, AvatarFallback, AvatarImage } from '../../primitives/avatar'
4
4
 
5
5
  const sizeMap = {
6
6
  sm: { avatar: 'h-7 w-7', name: 'text-sm', subtitle: 'text-xs' },
@@ -1,7 +1,7 @@
1
1
  import React from 'react'
2
2
  import { TrendingUp, TrendingDown } from 'lucide-react'
3
3
  import { cn } from '../utils/cn'
4
- import { Card, CardContent } from '../primitives/card'
4
+ import { Card, CardContent } from '../../primitives/card'
5
5
 
6
6
  interface StatProps {
7
7
  label: string
@@ -1,6 +1,6 @@
1
1
  import React from 'react'
2
2
  import { Check } from 'lucide-react'
3
- import { cn } from '../utils/cn'
3
+ import { cn } from '../../utils/cn'
4
4
 
5
5
  interface StepDef { title: string; description?: string }
6
6
 
@@ -40,9 +40,20 @@ interface StepperProps {
40
40
  currentStep: number
41
41
  steps: StepDef[]
42
42
  className?: string
43
+ /** 'horizontal' (default) | 'vertical' — vertical stacks steps as rows */
44
+ direction?: 'horizontal' | 'vertical'
43
45
  }
44
46
 
45
- export function Stepper({ currentStep, steps, className }: StepperProps) {
47
+ export function Stepper({ currentStep, steps, className, direction = 'horizontal' }: StepperProps) {
48
+ if (direction === 'vertical' || (className && className.includes('flex-col'))) {
49
+ return (
50
+ <div className={cn('flex flex-col gap-0', className?.replace('flex-col', ''))}>
51
+ {steps.map((step, i) => (
52
+ <StepperStep key={i} {...step} index={i} current={currentStep} total={steps.length} />
53
+ ))}
54
+ </div>
55
+ )
56
+ }
46
57
  return (
47
58
  <div className={cn('flex gap-0', className)}>
48
59
  {steps.map((step, i) => (
@@ -1,203 +1 @@
1
- import React from 'react'
2
- import { Info, CheckCircle2, AlertTriangle, XCircle, X, ArrowRight } from 'lucide-react'
3
- import { cva, type VariantProps } from 'class-variance-authority'
4
- import { cn } from '../utils/cn'
5
-
6
- // ─── Variant config ──────────────────────────────────────────────────────────
7
-
8
- const bannerVariants = cva(
9
- 'relative flex items-start gap-3 rounded-lg border px-4 py-3.5 text-sm transition-all',
10
- {
11
- variants: {
12
- variant: {
13
- info: 'border-blue-200/80 bg-blue-50/60 dark:border-blue-800/60 dark:bg-blue-950/30',
14
- success: 'border-green-200/80 bg-green-50/60 dark:border-green-800/60 dark:bg-green-950/30',
15
- warning: 'border-yellow-200/80 bg-yellow-50/60 dark:border-yellow-800/60 dark:bg-yellow-950/30',
16
- error: 'border-red-200/80 bg-red-50/60 dark:border-red-800/60 dark:bg-red-950/30',
17
- neutral: 'border-[hsl(var(--border))] bg-[hsl(var(--muted)/0.5)]',
18
- },
19
- },
20
- defaultVariants: { variant: 'info' },
21
- }
22
- )
23
-
24
- const accentVariants = cva('absolute left-0 top-[12%] bottom-[12%] w-[3px] rounded-full', {
25
- variants: {
26
- variant: {
27
- info: 'bg-blue-500',
28
- success: 'bg-green-500',
29
- warning: 'bg-yellow-500',
30
- error: 'bg-red-500',
31
- neutral: 'bg-[hsl(var(--foreground)/0.3)]',
32
- },
33
- },
34
- defaultVariants: { variant: 'info' },
35
- })
36
-
37
- const iconVariants = cva('mt-0.5 h-4 w-4 shrink-0', {
38
- variants: {
39
- variant: {
40
- info: 'text-blue-500 dark:text-blue-400',
41
- success: 'text-green-500 dark:text-green-400',
42
- warning: 'text-yellow-500 dark:text-yellow-400',
43
- error: 'text-red-500 dark:text-red-400',
44
- neutral: 'text-[hsl(var(--muted-foreground))]',
45
- },
46
- },
47
- defaultVariants: { variant: 'info' },
48
- })
49
-
50
- const titleVariants = cva('font-semibold leading-snug', {
51
- variants: {
52
- variant: {
53
- info: 'text-blue-900 dark:text-blue-100',
54
- success: 'text-green-900 dark:text-green-100',
55
- warning: 'text-yellow-900 dark:text-yellow-100',
56
- error: 'text-red-900 dark:text-red-100',
57
- neutral: 'text-[hsl(var(--foreground))]',
58
- },
59
- },
60
- defaultVariants: { variant: 'info' },
61
- })
62
-
63
- const descriptionVariants = cva('mt-0.5 leading-snug opacity-80', {
64
- variants: {
65
- variant: {
66
- info: 'text-blue-800 dark:text-blue-200',
67
- success: 'text-green-800 dark:text-green-200',
68
- warning: 'text-yellow-800 dark:text-yellow-200',
69
- error: 'text-red-800 dark:text-red-200',
70
- neutral: 'text-[hsl(var(--muted-foreground))]',
71
- },
72
- },
73
- defaultVariants: { variant: 'info' },
74
- })
75
-
76
- const actionVariants = cva(
77
- 'inline-flex items-center gap-1 text-xs font-medium underline-offset-2 hover:underline transition-opacity hover:opacity-80',
78
- {
79
- variants: {
80
- variant: {
81
- info: 'text-blue-700 dark:text-blue-300',
82
- success: 'text-green-700 dark:text-green-300',
83
- warning: 'text-yellow-700 dark:text-yellow-300',
84
- error: 'text-red-700 dark:text-red-300',
85
- neutral: 'text-[hsl(var(--foreground))]',
86
- },
87
- },
88
- defaultVariants: { variant: 'info' },
89
- }
90
- )
91
-
92
- // ─── Icons ───────────────────────────────────────────────────────────────────
93
-
94
- const ICONS: Record<NonNullable<BannerVariant>, React.ReactNode> = {
95
- info: <Info className="h-4 w-4" />,
96
- success: <CheckCircle2 className="h-4 w-4" />,
97
- warning: <AlertTriangle className="h-4 w-4" />,
98
- error: <XCircle className="h-4 w-4" />,
99
- neutral: <Info className="h-4 w-4" />,
100
- }
101
-
102
- // ─── Types ───────────────────────────────────────────────────────────────────
103
-
104
- type BannerVariant = VariantProps<typeof bannerVariants>['variant']
105
-
106
- export interface BannerAction {
107
- label: string
108
- onClick: () => void
109
- icon?: boolean
110
- }
111
-
112
- export interface BannerProps extends VariantProps<typeof bannerVariants> {
113
- title?: string
114
- description?: string
115
- dismissible?: boolean
116
- onDismiss?: () => void
117
- action?: BannerAction
118
- icon?: boolean
119
- className?: string
120
- children?: React.ReactNode
121
- }
122
-
123
- // ─── Component ───────────────────────────────────────────────────────────────
124
-
125
- export function Banner({
126
- variant = 'info',
127
- title,
128
- description,
129
- dismissible,
130
- onDismiss,
131
- action,
132
- icon = true,
133
- className,
134
- children,
135
- }: BannerProps) {
136
- const [dismissed, setDismissed] = React.useState(false)
137
- const [exiting, setExiting] = React.useState(false)
138
-
139
- const handleDismiss = () => {
140
- setExiting(true)
141
- setTimeout(() => {
142
- setDismissed(true)
143
- onDismiss?.()
144
- }, 200)
145
- }
146
-
147
- if (dismissed) return null
148
-
149
- return (
150
- <div
151
- className={cn(
152
- bannerVariants({ variant }),
153
- 'pl-[18px]',
154
- exiting && 'opacity-0 translate-x-1 scale-[0.98]',
155
- 'transition-all duration-200',
156
- className
157
- )}
158
- >
159
- {/* left accent bar */}
160
- <span className={accentVariants({ variant })} aria-hidden />
161
-
162
- {/* icon */}
163
- {icon && (
164
- <span className={iconVariants({ variant })}>
165
- {ICONS[variant as NonNullable<BannerVariant>]}
166
- </span>
167
- )}
168
-
169
- {/* body */}
170
- <div className="min-w-0 flex-1 space-y-1">
171
- {title && (
172
- <p className={cn(titleVariants({ variant }), 'text-sm')}>{title}</p>
173
- )}
174
- {description && (
175
- <p className={cn(descriptionVariants({ variant }), 'text-sm')}>{description}</p>
176
- )}
177
- {children && (
178
- <div className={cn(descriptionVariants({ variant }), 'text-sm')}>{children}</div>
179
- )}
180
- {action && (
181
- <button
182
- onClick={action.onClick}
183
- className={cn(actionVariants({ variant }), 'mt-1.5')}
184
- >
185
- {action.label}
186
- {action.icon !== false && <ArrowRight className="h-3 w-3" />}
187
- </button>
188
- )}
189
- </div>
190
-
191
- {/* dismiss */}
192
- {dismissible && (
193
- <button
194
- onClick={handleDismiss}
195
- aria-label="Dismiss"
196
- className="shrink-0 rounded-md p-0.5 text-[hsl(var(--muted-foreground))] opacity-60 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--ring))]"
197
- >
198
- <X className="h-4 w-4" />
199
- </button>
200
- )}
201
- </div>
202
- )
203
- }
1
+ export * from './Banner/index'
@@ -1,58 +1 @@
1
- import React from 'react'
2
- import { ChevronRight } from 'lucide-react'
3
- import { cn } from '../utils/cn'
4
-
5
- interface BreadcrumbItemProps {
6
- href?: string
7
- label: string
8
- active?: boolean
9
- className?: string
10
- }
11
-
12
- export function BreadcrumbItem({ href, label, active, className }: BreadcrumbItemProps) {
13
- return (
14
- <li className={cn('flex items-center', className)}>
15
- {href && !active ? (
16
- <a href={href} className="text-sm text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--foreground))] transition-colors">{label}</a>
17
- ) : (
18
- <span className="text-sm text-[hsl(var(--foreground))] font-medium">{label}</span>
19
- )}
20
- </li>
21
- )
22
- }
23
-
24
- export function BreadcrumbPage({ children, className }: { children: React.ReactNode; className?: string }) {
25
- return (
26
- <li aria-current="page" className={cn('flex items-center text-sm font-medium text-[hsl(var(--foreground))]', className)}>
27
- {children}
28
- </li>
29
- )
30
- }
31
-
32
- export function BreadcrumbSeparator({ className }: { className?: string }) {
33
- return (
34
- <li role="presentation" aria-hidden="true" className={cn('flex items-center text-[hsl(var(--muted-foreground))]', className)}>
35
- <ChevronRight className="h-3.5 w-3.5" />
36
- </li>
37
- )
38
- }
39
-
40
- interface BreadcrumbProps {
41
- children: React.ReactNode
42
- className?: string
43
- }
44
-
45
- export function Breadcrumb({ children, className }: BreadcrumbProps) {
46
- const items = React.Children.toArray(children)
47
- const withSeparators: React.ReactNode[] = []
48
- items.forEach((item, i) => {
49
- withSeparators.push(item)
50
- if (i < items.length - 1) withSeparators.push(<BreadcrumbSeparator key={`sep-${i}`} />)
51
- })
52
-
53
- return (
54
- <nav aria-label="breadcrumb">
55
- <ol className={cn('flex flex-wrap items-center gap-1', className)}>{withSeparators}</ol>
56
- </nav>
57
- )
58
- }
1
+ export * from './Breadcrumb/index'
@@ -1,36 +1 @@
1
- import React from 'react'
2
- import { cn } from '../utils/cn'
3
-
4
- interface CommandItemProps {
5
- icon?: React.ReactNode
6
- label: string
7
- description?: string
8
- shortcut?: string
9
- active?: boolean
10
- onClick?: () => void
11
- className?: string
12
- }
13
-
14
- export function CommandItem({ icon, label, description, shortcut, active, onClick, className }: CommandItemProps) {
15
- return (
16
- <div
17
- onClick={onClick}
18
- className={cn(
19
- 'flex items-center gap-3 rounded-md px-3 py-2 cursor-pointer transition-colors',
20
- active ? 'bg-[hsl(var(--accent))] text-[hsl(var(--accent-foreground))]' : 'hover:bg-[hsl(var(--accent)/0.6)]',
21
- className
22
- )}
23
- >
24
- {icon && <span className="shrink-0 text-[hsl(var(--muted-foreground))] h-4 w-4 flex items-center justify-center">{icon}</span>}
25
- <div className="min-w-0 flex-1">
26
- <p className="text-sm font-medium truncate">{label}</p>
27
- {description && <p className="text-xs text-[hsl(var(--muted-foreground))] truncate">{description}</p>}
28
- </div>
29
- {shortcut && (
30
- <kbd className="shrink-0 pointer-events-none inline-flex h-5 items-center gap-1 rounded border border-[hsl(var(--border))] bg-[hsl(var(--muted))] px-1.5 text-[10px] font-medium text-[hsl(var(--muted-foreground))]">
31
- {shortcut}
32
- </kbd>
33
- )}
34
- </div>
35
- )
36
- }
1
+ export { CommandItem as CommandListItem } from './CommandItem'