@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
|
@@ -1,61 +1 @@
|
|
|
1
|
-
|
|
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'
|
package/src/charts/sparkline.tsx
CHANGED
|
@@ -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
|
|
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 '
|
|
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 '
|
|
15
|
-
import { Input } from '
|
|
16
|
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '
|
|
17
|
-
import { Skeleton } from '
|
|
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/
|
|
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
|
-
|
|
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 '
|
|
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 '
|
|
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 '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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'
|
|
@@ -1,125 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
ColumnDef,
|
|
4
|
-
flexRender,
|
|
5
|
-
getCoreRowModel,
|
|
6
|
-
useReactTable,
|
|
7
|
-
RowSelectionState,
|
|
8
|
-
ColumnResizeMode,
|
|
9
|
-
} from '@tanstack/react-table'
|
|
10
|
-
import { cn } from '../utils/cn'
|
|
11
|
-
import { Checkbox } from '../primitives/checkbox'
|
|
12
|
-
|
|
13
|
-
interface DataGridProps<T extends object> {
|
|
14
|
-
columns: ColumnDef<T, any>[]
|
|
15
|
-
data: T[]
|
|
16
|
-
onDataChange?: (data: T[]) => void
|
|
17
|
-
editable?: boolean
|
|
18
|
-
className?: string
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function EditableCell<T>({ value: initial, onSave }: { value: unknown; onSave: (v: string) => void }) {
|
|
22
|
-
const [editing, setEditing] = React.useState(false)
|
|
23
|
-
const [val, setVal] = React.useState(String(initial ?? ''))
|
|
24
|
-
|
|
25
|
-
const commit = () => { onSave(val); setEditing(false) }
|
|
26
|
-
|
|
27
|
-
if (editing) {
|
|
28
|
-
return (
|
|
29
|
-
<input
|
|
30
|
-
autoFocus
|
|
31
|
-
className="w-full bg-transparent outline outline-1 outline-[hsl(var(--ring))] rounded px-1 text-sm"
|
|
32
|
-
value={val}
|
|
33
|
-
onChange={(e) => setVal(e.target.value)}
|
|
34
|
-
onBlur={commit}
|
|
35
|
-
onKeyDown={(e) => { if (e.key === 'Enter') commit(); if (e.key === 'Escape') setEditing(false) }}
|
|
36
|
-
/>
|
|
37
|
-
)
|
|
38
|
-
}
|
|
39
|
-
return (
|
|
40
|
-
<span className="cursor-pointer w-full block" onDoubleClick={() => setEditing(true)}>
|
|
41
|
-
{String(initial ?? '')}
|
|
42
|
-
</span>
|
|
43
|
-
)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const selectionCol = <T,>(): ColumnDef<T> => ({
|
|
47
|
-
id: '_select',
|
|
48
|
-
header: ({ table }) => (
|
|
49
|
-
<Checkbox checked={table.getIsAllRowsSelected()} onCheckedChange={(v) => table.toggleAllRowsSelected(!!v)} />
|
|
50
|
-
),
|
|
51
|
-
cell: ({ row }) => (
|
|
52
|
-
<Checkbox checked={row.getIsSelected()} onCheckedChange={(v) => row.toggleSelected(!!v)} />
|
|
53
|
-
),
|
|
54
|
-
size: 40,
|
|
55
|
-
enableResizing: false,
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
export function DataGrid<T extends object>({ columns, data: initialData, onDataChange, editable, className }: DataGridProps<T>) {
|
|
59
|
-
const [data, setData] = React.useState<T[]>(initialData)
|
|
60
|
-
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({})
|
|
61
|
-
const columnResizeMode: ColumnResizeMode = 'onChange'
|
|
62
|
-
|
|
63
|
-
const editableColumns = React.useMemo(() => {
|
|
64
|
-
if (!editable) return columns
|
|
65
|
-
return columns.map((col) => ({
|
|
66
|
-
...col,
|
|
67
|
-
cell: (info: any) => (
|
|
68
|
-
<EditableCell
|
|
69
|
-
value={info.getValue()}
|
|
70
|
-
onSave={(v) => {
|
|
71
|
-
const updated = data.map((row, i) =>
|
|
72
|
-
i === info.row.index ? { ...row, [info.column.id]: v } : row
|
|
73
|
-
) as T[]
|
|
74
|
-
setData(updated)
|
|
75
|
-
onDataChange?.(updated)
|
|
76
|
-
}}
|
|
77
|
-
/>
|
|
78
|
-
),
|
|
79
|
-
}))
|
|
80
|
-
}, [columns, data, editable, onDataChange])
|
|
81
|
-
|
|
82
|
-
const allColumns = React.useMemo(() => [selectionCol<T>(), ...editableColumns], [editableColumns])
|
|
83
|
-
|
|
84
|
-
const table = useReactTable({
|
|
85
|
-
data,
|
|
86
|
-
columns: allColumns,
|
|
87
|
-
columnResizeMode,
|
|
88
|
-
getCoreRowModel: getCoreRowModel(),
|
|
89
|
-
onRowSelectionChange: setRowSelection,
|
|
90
|
-
state: { rowSelection },
|
|
91
|
-
enableColumnResizing: true,
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
return (
|
|
95
|
-
<div className={cn('overflow-auto rounded-md border border-[hsl(var(--border))]', className)}>
|
|
96
|
-
<table className="w-full text-sm" style={{ width: table.getCenterTotalSize() }}>
|
|
97
|
-
<thead className="bg-[hsl(var(--muted)/0.4)]">
|
|
98
|
-
{table.getHeaderGroups().map((hg) => (
|
|
99
|
-
<tr key={hg.id}>
|
|
100
|
-
{hg.headers.map((h) => (
|
|
101
|
-
<th key={h.id} style={{ width: h.getSize() }} className="relative px-3 py-2 text-left font-medium text-[hsl(var(--muted-foreground))] border-r border-[hsl(var(--border))] last:border-r-0">
|
|
102
|
-
{h.isPlaceholder ? null : flexRender(h.column.columnDef.header, h.getContext())}
|
|
103
|
-
{h.column.getCanResize() && (
|
|
104
|
-
<div onMouseDown={h.getResizeHandler()} className="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-[hsl(var(--primary)/0.4)]" />
|
|
105
|
-
)}
|
|
106
|
-
</th>
|
|
107
|
-
))}
|
|
108
|
-
</tr>
|
|
109
|
-
))}
|
|
110
|
-
</thead>
|
|
111
|
-
<tbody>
|
|
112
|
-
{table.getRowModel().rows.map((row) => (
|
|
113
|
-
<tr key={row.id} className={cn('border-t border-[hsl(var(--border))] hover:bg-[hsl(var(--muted)/0.3)] transition-colors', row.getIsSelected() && 'bg-[hsl(var(--primary)/0.06)]')}>
|
|
114
|
-
{row.getVisibleCells().map((cell) => (
|
|
115
|
-
<td key={cell.id} style={{ width: cell.column.getSize() }} className="px-3 py-2 border-r border-[hsl(var(--border))] last:border-r-0">
|
|
116
|
-
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
117
|
-
</td>
|
|
118
|
-
))}
|
|
119
|
-
</tr>
|
|
120
|
-
))}
|
|
121
|
-
</tbody>
|
|
122
|
-
</table>
|
|
123
|
-
</div>
|
|
124
|
-
)
|
|
125
|
-
}
|
|
1
|
+
export * from './DataGrid'
|