@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.
- package/LICENSE +1 -1
- package/README.md +28 -41
- package/dist/index.js +972 -655
- package/dist/index.mjs +826 -525
- 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 +11 -3
- package/src/primitives/Button/Button.tsx +18 -0
- package/src/primitives/cart-line-item.tsx +81 -0
- package/src/primitives/code-block.tsx +1 -1
- package/src/primitives/comparison.tsx +1 -1
- package/src/primitives/empty-cart.tsx +52 -0
- package/src/primitives/price-tag.tsx +62 -0
- package/src/primitives/qr-code.tsx +5 -1
- package/src/primitives/quantity-stepper.tsx +92 -0
- package/src/primitives/variant-picker.tsx +74 -0
- package/src/themes/base.css +18 -0
|
@@ -1,50 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { cn } from '../utils/cn'
|
|
3
|
-
import { Badge } from '../primitives/badge'
|
|
4
|
-
import { Button } from '../primitives/button'
|
|
5
|
-
|
|
6
|
-
export interface IntegrationCardProps {
|
|
7
|
-
icon?: React.ReactNode
|
|
8
|
-
name?: string
|
|
9
|
-
description?: string
|
|
10
|
-
connected?: boolean
|
|
11
|
-
className?: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function IntegrationCard({
|
|
15
|
-
icon,
|
|
16
|
-
name = 'Slack',
|
|
17
|
-
description = 'Send notifications and updates to your Slack workspace.',
|
|
18
|
-
connected = false,
|
|
19
|
-
className,
|
|
20
|
-
}: IntegrationCardProps) {
|
|
21
|
-
const [isConnected, setIsConnected] = useState(connected)
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<div className={cn('flex items-center gap-4 rounded-md border border-[hsl(var(--border))] bg-[hsl(var(--card))] p-4', className)}>
|
|
25
|
-
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-[hsl(var(--muted))] text-[hsl(var(--muted-foreground))]">
|
|
26
|
-
{icon ?? <span className="text-base font-bold">{name[0]}</span>}
|
|
27
|
-
</div>
|
|
28
|
-
<div className="flex-1 min-w-0">
|
|
29
|
-
<div className="flex items-center gap-2">
|
|
30
|
-
<p className="text-sm font-semibold">{name}</p>
|
|
31
|
-
<Badge
|
|
32
|
-
variant={isConnected ? 'default' : 'outline'}
|
|
33
|
-
className="text-[10px] px-1.5 py-0.5 shrink-0"
|
|
34
|
-
>
|
|
35
|
-
{isConnected ? 'Connected' : 'Not connected'}
|
|
36
|
-
</Badge>
|
|
37
|
-
</div>
|
|
38
|
-
<p className="text-xs text-[hsl(var(--muted-foreground))] mt-0.5 truncate">{description}</p>
|
|
39
|
-
</div>
|
|
40
|
-
<Button
|
|
41
|
-
size="sm"
|
|
42
|
-
variant={isConnected ? 'outline' : 'default'}
|
|
43
|
-
className="shrink-0"
|
|
44
|
-
onClick={() => setIsConnected(v => !v)}
|
|
45
|
-
>
|
|
46
|
-
{isConnected ? 'Disconnect' : 'Connect'}
|
|
47
|
-
</Button>
|
|
48
|
-
</div>
|
|
49
|
-
)
|
|
50
|
-
}
|
|
1
|
+
export * from './IntegrationCard'
|
|
@@ -1,69 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { X } from 'lucide-react'
|
|
3
|
-
import { cn } from '../utils/cn'
|
|
4
|
-
import { Input } from '../primitives/input'
|
|
5
|
-
import { Button } from '../primitives/button'
|
|
6
|
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../primitives/select'
|
|
7
|
-
import { Avatar, AvatarFallback } from '../primitives/avatar'
|
|
8
|
-
import { Badge } from '../primitives/badge'
|
|
9
|
-
|
|
10
|
-
const PENDING = [
|
|
11
|
-
{ email: 'alex@startup.io', role: 'Member' },
|
|
12
|
-
{ email: 'sam@startup.io', role: 'Viewer' },
|
|
13
|
-
]
|
|
14
|
-
|
|
15
|
-
export interface InviteModalProps { className?: string }
|
|
16
|
-
|
|
17
|
-
export function InviteModal({ className }: InviteModalProps) {
|
|
18
|
-
const [email, setEmail] = useState('')
|
|
19
|
-
const [role, setRole] = useState('Member')
|
|
20
|
-
const [pending, setPending] = useState(PENDING)
|
|
21
|
-
|
|
22
|
-
const invite = () => {
|
|
23
|
-
if (!email.trim()) return
|
|
24
|
-
setPending(p => [...p, { email: email.trim(), role }])
|
|
25
|
-
setEmail('')
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return (
|
|
29
|
-
<div className={cn('flex flex-col gap-4 p-6', className)}>
|
|
30
|
-
<div>
|
|
31
|
-
<h2 className="text-base font-semibold">Invite People</h2>
|
|
32
|
-
<p className="text-sm text-[hsl(var(--muted-foreground))] mt-0.5">Invite team members to collaborate.</p>
|
|
33
|
-
</div>
|
|
34
|
-
<div className="flex gap-2">
|
|
35
|
-
<Input
|
|
36
|
-
className="flex-1 h-9 text-sm"
|
|
37
|
-
placeholder="colleague@company.com"
|
|
38
|
-
value={email}
|
|
39
|
-
onChange={e => setEmail(e.target.value)}
|
|
40
|
-
onKeyDown={e => e.key === 'Enter' && invite()}
|
|
41
|
-
/>
|
|
42
|
-
<Select value={role} onValueChange={setRole}>
|
|
43
|
-
<SelectTrigger className="h-9 w-28 text-sm"><SelectValue /></SelectTrigger>
|
|
44
|
-
<SelectContent>
|
|
45
|
-
{['Admin', 'Member', 'Viewer'].map(r => <SelectItem key={r} value={r}>{r}</SelectItem>)}
|
|
46
|
-
</SelectContent>
|
|
47
|
-
</Select>
|
|
48
|
-
<Button size="sm" className="h-9" onClick={invite}>Send</Button>
|
|
49
|
-
</div>
|
|
50
|
-
{pending.length > 0 && (
|
|
51
|
-
<div className="space-y-1">
|
|
52
|
-
<p className="text-xs font-medium text-[hsl(var(--muted-foreground))]">Pending invites</p>
|
|
53
|
-
<div className="divide-y divide-[hsl(var(--border))] rounded-md border border-[hsl(var(--border))]">
|
|
54
|
-
{pending.map((p, i) => (
|
|
55
|
-
<div key={i} className="flex items-center gap-2 px-3 py-2">
|
|
56
|
-
<Avatar className="h-6 w-6"><AvatarFallback className="text-[10px]">{p.email[0].toUpperCase()}</AvatarFallback></Avatar>
|
|
57
|
-
<span className="flex-1 text-sm truncate">{p.email}</span>
|
|
58
|
-
<Badge variant="outline" className="text-[10px] px-1.5 py-0.5 shrink-0">{p.role}</Badge>
|
|
59
|
-
<button onClick={() => setPending(ps => ps.filter((_, j) => j !== i))}>
|
|
60
|
-
<X className="h-3.5 w-3.5 text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--foreground))]" />
|
|
61
|
-
</button>
|
|
62
|
-
</div>
|
|
63
|
-
))}
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
66
|
-
)}
|
|
67
|
-
</div>
|
|
68
|
-
)
|
|
69
|
-
}
|
|
1
|
+
export * from './InviteModal'
|
|
@@ -1,63 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { X, Pencil } from 'lucide-react'
|
|
3
|
-
import { cn } from '../utils/cn'
|
|
4
|
-
import { Input } from '../primitives/input'
|
|
5
|
-
import { Button } from '../primitives/button'
|
|
6
|
-
|
|
7
|
-
const COLORS = ['bg-blue-500', 'bg-green-500', 'bg-yellow-500', 'bg-purple-500', 'bg-red-500', 'bg-pink-500']
|
|
8
|
-
|
|
9
|
-
interface Tag { id: string; name: string; color: string }
|
|
10
|
-
|
|
11
|
-
const INITIAL_TAGS: Tag[] = [
|
|
12
|
-
{ id: '1', name: 'Design', color: 'bg-blue-500' },
|
|
13
|
-
{ id: '2', name: 'Engineering', color: 'bg-green-500' },
|
|
14
|
-
{ id: '3', name: 'Marketing', color: 'bg-yellow-500' },
|
|
15
|
-
{ id: '4', name: 'Research', color: 'bg-purple-500' },
|
|
16
|
-
]
|
|
17
|
-
|
|
18
|
-
export interface ManageTagsModalProps { className?: string }
|
|
19
|
-
|
|
20
|
-
export function ManageTagsModal({ className }: ManageTagsModalProps) {
|
|
21
|
-
const [tags, setTags] = useState<Tag[]>(INITIAL_TAGS)
|
|
22
|
-
const [newTag, setNewTag] = useState('')
|
|
23
|
-
const [colorIdx, setColorIdx] = useState(0)
|
|
24
|
-
|
|
25
|
-
const addTag = () => {
|
|
26
|
-
if (!newTag.trim()) return
|
|
27
|
-
const id = `tag-${Date.now()}`
|
|
28
|
-
setTags(t => [...t, { id, name: newTag.trim(), color: COLORS[colorIdx % COLORS.length] }])
|
|
29
|
-
setNewTag('')
|
|
30
|
-
setColorIdx(i => i + 1)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<div className={cn('flex flex-col gap-4 p-6', className)}>
|
|
35
|
-
<div>
|
|
36
|
-
<h2 className="text-base font-semibold">Manage Tags</h2>
|
|
37
|
-
<p className="text-sm text-[hsl(var(--muted-foreground))] mt-0.5">Create and organize your tags.</p>
|
|
38
|
-
</div>
|
|
39
|
-
<div className="flex gap-2">
|
|
40
|
-
<div className={cn('h-9 w-9 shrink-0 rounded-md cursor-pointer transition-opacity', COLORS[colorIdx % COLORS.length])}
|
|
41
|
-
onClick={() => setColorIdx(i => i + 1)} title="Click to change color" />
|
|
42
|
-
<Input className="flex-1 h-9 text-sm" placeholder="New tag name..." value={newTag}
|
|
43
|
-
onChange={e => setNewTag(e.target.value)} onKeyDown={e => e.key === 'Enter' && addTag()} />
|
|
44
|
-
<Button size="sm" className="h-9" onClick={addTag}>Add</Button>
|
|
45
|
-
</div>
|
|
46
|
-
<div className="space-y-1.5">
|
|
47
|
-
{tags.map(tag => (
|
|
48
|
-
<div key={tag.id} className="flex items-center gap-2 rounded-md border border-[hsl(var(--border))] px-3 py-2">
|
|
49
|
-
<span className={cn('h-3 w-3 rounded-full shrink-0', tag.color)} />
|
|
50
|
-
<span className="flex-1 text-sm">{tag.name}</span>
|
|
51
|
-
<button className="text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--foreground))] transition-colors">
|
|
52
|
-
<Pencil className="h-3.5 w-3.5" />
|
|
53
|
-
</button>
|
|
54
|
-
<button onClick={() => setTags(ts => ts.filter(t => t.id !== tag.id))}
|
|
55
|
-
className="text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--foreground))] transition-colors">
|
|
56
|
-
<X className="h-3.5 w-3.5" />
|
|
57
|
-
</button>
|
|
58
|
-
</div>
|
|
59
|
-
))}
|
|
60
|
-
</div>
|
|
61
|
-
</div>
|
|
62
|
-
)
|
|
63
|
-
}
|
|
1
|
+
export * from './ManageTagsModal'
|
|
@@ -1,59 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { cn } from '../utils/cn'
|
|
3
|
-
import { Avatar, AvatarFallback } from '../primitives/avatar'
|
|
4
|
-
import { Card, CardContent, CardHeader, CardTitle } from '../primitives/card'
|
|
5
|
-
|
|
6
|
-
interface Message {
|
|
7
|
-
id: string
|
|
8
|
-
name: string
|
|
9
|
-
initials: string
|
|
10
|
-
preview: string
|
|
11
|
-
time: string
|
|
12
|
-
unread?: boolean
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const MESSAGES: Message[] = [
|
|
16
|
-
{ id: '1', name: 'Alice Chen', initials: 'AC', preview: 'Just pushed the new design files to Figma.', time: '9:41 AM', unread: true },
|
|
17
|
-
{ id: '2', name: 'Bob Smith', initials: 'BS', preview: "I'll take a look and share feedback by EOD.", time: '9:43 AM', unread: true },
|
|
18
|
-
{ id: '3', name: 'Tom Ray', initials: 'TR', preview: 'Can we reschedule the standup to 10am?', time: 'Yesterday' },
|
|
19
|
-
{ id: '4', name: 'Sara Kim', initials: 'SK', preview: 'Shipped the new auth flow. Ready for QA.', time: 'Yesterday' },
|
|
20
|
-
{ id: '5', name: 'Mike Liu', initials: 'ML', preview: 'The latest metrics look promising 📈', time: 'Mon' },
|
|
21
|
-
]
|
|
22
|
-
|
|
23
|
-
function MessageRow({ msg }: { msg: Message }) {
|
|
24
|
-
return (
|
|
25
|
-
<div className="flex items-center gap-3 px-4 py-3 hover:bg-[hsl(var(--accent))] cursor-pointer transition-colors">
|
|
26
|
-
<div className="relative shrink-0">
|
|
27
|
-
<Avatar className="h-9 w-9"><AvatarFallback className="text-xs">{msg.initials}</AvatarFallback></Avatar>
|
|
28
|
-
{msg.unread && <span className="absolute -top-0.5 -right-0.5 h-2.5 w-2.5 rounded-full bg-[hsl(var(--primary))] ring-2 ring-[hsl(var(--card))]" />}
|
|
29
|
-
</div>
|
|
30
|
-
<div className="flex-1 min-w-0">
|
|
31
|
-
<div className="flex items-center justify-between gap-2">
|
|
32
|
-
<p className={cn('text-sm truncate', msg.unread ? 'font-semibold' : 'font-medium')}>{msg.name}</p>
|
|
33
|
-
<span className="text-xs text-[hsl(var(--muted-foreground))] shrink-0">{msg.time}</span>
|
|
34
|
-
</div>
|
|
35
|
-
<p className={cn('text-xs mt-0.5 truncate', msg.unread ? 'text-[hsl(var(--foreground))]' : 'text-[hsl(var(--muted-foreground))]')}>
|
|
36
|
-
{msg.preview}
|
|
37
|
-
</p>
|
|
38
|
-
</div>
|
|
39
|
-
</div>
|
|
40
|
-
)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface MessagesCardProps { className?: string }
|
|
44
|
-
|
|
45
|
-
export function MessagesCard({ className }: MessagesCardProps) {
|
|
46
|
-
return (
|
|
47
|
-
<Card className={cn('overflow-hidden', className)}>
|
|
48
|
-
<CardHeader className="px-4 py-3 border-b border-[hsl(var(--border))]">
|
|
49
|
-
<CardTitle className="text-sm font-semibold flex items-center justify-between">
|
|
50
|
-
Messages
|
|
51
|
-
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-[hsl(var(--primary)/0.1)] text-[hsl(var(--primary))] font-medium">2 new</span>
|
|
52
|
-
</CardTitle>
|
|
53
|
-
</CardHeader>
|
|
54
|
-
<div className="divide-y divide-[hsl(var(--border))]">
|
|
55
|
-
{MESSAGES.map(m => <MessageRow key={m.id} msg={m} />)}
|
|
56
|
-
</div>
|
|
57
|
-
</Card>
|
|
58
|
-
)
|
|
59
|
-
}
|
|
1
|
+
export * from './MessagesCard'
|
|
@@ -1,79 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { TrendingUp, TrendingDown } from 'lucide-react'
|
|
3
|
-
import { cn } from '../utils/cn'
|
|
4
|
-
import { Card, CardContent } from '../primitives/card'
|
|
5
|
-
import { Button } from '../primitives/button'
|
|
6
|
-
|
|
7
|
-
export interface MetricCardProps {
|
|
8
|
-
label?: string
|
|
9
|
-
value?: string
|
|
10
|
-
change?: string
|
|
11
|
-
trend?: 'up' | 'down'
|
|
12
|
-
className?: string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function MetricCard({ label = 'Total Revenue', value = '$48,295', change = '+12.5%', trend = 'up', className }: MetricCardProps) {
|
|
16
|
-
const TrendIcon = trend === 'up' ? TrendingUp : TrendingDown
|
|
17
|
-
const trendColor = trend === 'up' ? 'text-green-600' : 'text-red-500'
|
|
18
|
-
return (
|
|
19
|
-
<Card className={cn('', className)}>
|
|
20
|
-
<CardContent className="p-4">
|
|
21
|
-
<p className="text-sm text-[hsl(var(--muted-foreground))]">{label}</p>
|
|
22
|
-
<p className="text-2xl font-bold mt-1">{value}</p>
|
|
23
|
-
<p className={cn('flex items-center gap-1 text-sm mt-1', trendColor)}>
|
|
24
|
-
<TrendIcon className="h-4 w-4" />{change} from last month
|
|
25
|
-
</p>
|
|
26
|
-
</CardContent>
|
|
27
|
-
</Card>
|
|
28
|
-
)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface MetricCardWithIconProps extends MetricCardProps {
|
|
32
|
-
icon?: React.ReactNode
|
|
33
|
-
iconColor?: string
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function MetricCardWithIcon({ icon, iconColor = 'bg-[hsl(var(--primary)/0.1)] text-[hsl(var(--primary))]', ...props }: MetricCardWithIconProps) {
|
|
37
|
-
const TrendIcon = (props.trend ?? 'up') === 'up' ? TrendingUp : TrendingDown
|
|
38
|
-
const trendColor = (props.trend ?? 'up') === 'up' ? 'text-green-600' : 'text-red-500'
|
|
39
|
-
return (
|
|
40
|
-
<Card className={cn('', props.className)}>
|
|
41
|
-
<CardContent className="p-4">
|
|
42
|
-
<div className="flex items-start justify-between">
|
|
43
|
-
<div>
|
|
44
|
-
<p className="text-sm text-[hsl(var(--muted-foreground))]">{props.label ?? 'Total Revenue'}</p>
|
|
45
|
-
<p className="text-2xl font-bold mt-1">{props.value ?? '$48,295'}</p>
|
|
46
|
-
<p className={cn('flex items-center gap-1 text-sm mt-1', trendColor)}>
|
|
47
|
-
<TrendIcon className="h-4 w-4" />{props.change ?? '+12.5%'}
|
|
48
|
-
</p>
|
|
49
|
-
</div>
|
|
50
|
-
<div className={cn('flex h-10 w-10 items-center justify-center rounded-md', iconColor)}>
|
|
51
|
-
{icon ?? <TrendingUp className="h-5 w-5" />}
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
</CardContent>
|
|
55
|
-
</Card>
|
|
56
|
-
)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export interface MetricCardWithButtonProps extends MetricCardProps {
|
|
60
|
-
actionLabel?: string
|
|
61
|
-
onAction?: () => void
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function MetricCardWithButton({ actionLabel = 'View details', onAction, ...props }: MetricCardWithButtonProps) {
|
|
65
|
-
const TrendIcon = (props.trend ?? 'up') === 'up' ? TrendingUp : TrendingDown
|
|
66
|
-
const trendColor = (props.trend ?? 'up') === 'up' ? 'text-green-600' : 'text-red-500'
|
|
67
|
-
return (
|
|
68
|
-
<Card className={cn('', props.className)}>
|
|
69
|
-
<CardContent className="p-4">
|
|
70
|
-
<p className="text-sm text-[hsl(var(--muted-foreground))]">{props.label ?? 'Total Revenue'}</p>
|
|
71
|
-
<p className="text-2xl font-bold mt-1">{props.value ?? '$48,295'}</p>
|
|
72
|
-
<p className={cn('flex items-center gap-1 text-sm mt-1 mb-3', trendColor)}>
|
|
73
|
-
<TrendIcon className="h-4 w-4" />{props.change ?? '+12.5%'}
|
|
74
|
-
</p>
|
|
75
|
-
<Button size="sm" variant="outline" className="w-full" onClick={onAction}>{actionLabel}</Button>
|
|
76
|
-
</CardContent>
|
|
77
|
-
</Card>
|
|
78
|
-
)
|
|
79
|
-
}
|
|
1
|
+
export * from './MetricCard'
|
|
@@ -1,61 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { cn } from '../utils/cn'
|
|
3
|
-
import { Switch } from '../primitives/switch'
|
|
4
|
-
import { Separator } from '../primitives/separator'
|
|
5
|
-
|
|
6
|
-
const NOTIFICATIONS = [
|
|
7
|
-
{ id: 'comments', label: 'Comments', description: 'Get notified when someone comments on your post', default: true },
|
|
8
|
-
{ id: 'mentions', label: 'Mentions', description: 'Get notified when you are mentioned', default: true },
|
|
9
|
-
{ id: 'followers', label: 'New followers', description: 'Get notified when someone follows you', default: false },
|
|
10
|
-
{ id: 'updates', label: 'Product updates', description: 'Receive emails about new features and improvements', default: true },
|
|
11
|
-
{ id: 'digest', label: 'Weekly digest', description: 'A summary of activity from the past week', default: false },
|
|
12
|
-
{ id: 'security', label: 'Security alerts', description: 'Important alerts about your account security', default: true },
|
|
13
|
-
]
|
|
14
|
-
|
|
15
|
-
interface NotifRow {
|
|
16
|
-
id: string
|
|
17
|
-
label: string
|
|
18
|
-
description: string
|
|
19
|
-
checked: boolean
|
|
20
|
-
onChange: (checked: boolean) => void
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function NotifRow({ label, description, checked, onChange }: NotifRow) {
|
|
24
|
-
return (
|
|
25
|
-
<div className="flex items-center justify-between gap-4 py-4">
|
|
26
|
-
<div>
|
|
27
|
-
<p className="text-sm font-medium">{label}</p>
|
|
28
|
-
<p className="text-xs text-[hsl(var(--muted-foreground))] mt-0.5">{description}</p>
|
|
29
|
-
</div>
|
|
30
|
-
<Switch checked={checked} onCheckedChange={onChange} />
|
|
31
|
-
</div>
|
|
32
|
-
)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface NotificationSettingsProps { className?: string }
|
|
36
|
-
|
|
37
|
-
export function NotificationSettings({ className }: NotificationSettingsProps) {
|
|
38
|
-
const [states, setStates] = useState<Record<string, boolean>>(
|
|
39
|
-
Object.fromEntries(NOTIFICATIONS.map(n => [n.id, n.default]))
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
<div className={cn('rounded-md border border-[hsl(var(--border))] bg-[hsl(var(--card))] p-6', className)}>
|
|
44
|
-
<div className="mb-4">
|
|
45
|
-
<h2 className="text-base font-semibold">Email Notifications</h2>
|
|
46
|
-
<p className="text-sm text-[hsl(var(--muted-foreground))] mt-0.5">Choose what you want to be notified about.</p>
|
|
47
|
-
</div>
|
|
48
|
-
<Separator />
|
|
49
|
-
<div className="divide-y divide-[hsl(var(--border))]">
|
|
50
|
-
{NOTIFICATIONS.map(n => (
|
|
51
|
-
<NotifRow
|
|
52
|
-
key={n.id}
|
|
53
|
-
{...n}
|
|
54
|
-
checked={states[n.id]}
|
|
55
|
-
onChange={val => setStates(s => ({ ...s, [n.id]: val }))}
|
|
56
|
-
/>
|
|
57
|
-
))}
|
|
58
|
-
</div>
|
|
59
|
-
</div>
|
|
60
|
-
)
|
|
61
|
-
}
|
|
1
|
+
export * from './NotificationSettings'
|
|
@@ -1,85 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { ChevronDown, Check, Plus } from 'lucide-react'
|
|
3
|
-
import { cn } from '../utils/cn'
|
|
4
|
-
import {
|
|
5
|
-
DropdownMenu,
|
|
6
|
-
DropdownMenuContent,
|
|
7
|
-
DropdownMenuItem,
|
|
8
|
-
DropdownMenuLabel,
|
|
9
|
-
DropdownMenuSeparator,
|
|
10
|
-
DropdownMenuTrigger,
|
|
11
|
-
} from '../primitives/dropdown-menu'
|
|
12
|
-
import { useSidebar } from '../app-shell/Sidebar'
|
|
13
|
-
|
|
14
|
-
const ORGS = [
|
|
15
|
-
{ id: '1', name: 'Acme Corp', slug: 'acme', plan: 'Pro' },
|
|
16
|
-
{ id: '2', name: 'Beta Labs', slug: 'beta-labs', plan: 'Starter' },
|
|
17
|
-
{ id: '3', name: 'Gamma Inc', slug: 'gamma', plan: 'Free' },
|
|
18
|
-
]
|
|
19
|
-
|
|
20
|
-
export interface OrgMenuProps {
|
|
21
|
-
/** Controlled mode — pass to manage selection externally. If omitted, the menu manages its own state. */
|
|
22
|
-
currentOrgId?: string
|
|
23
|
-
/** Default selection in uncontrolled mode. Defaults to the first org. */
|
|
24
|
-
defaultOrgId?: string
|
|
25
|
-
onSwitch?: (orgId: string) => void
|
|
26
|
-
onCreateOrg?: () => void
|
|
27
|
-
className?: string
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function OrgMenu({
|
|
31
|
-
currentOrgId,
|
|
32
|
-
defaultOrgId,
|
|
33
|
-
onSwitch,
|
|
34
|
-
onCreateOrg,
|
|
35
|
-
className,
|
|
36
|
-
}: OrgMenuProps) {
|
|
37
|
-
const [internalId, setInternalId] = useState(defaultOrgId ?? ORGS[0]?.id ?? '1')
|
|
38
|
-
const activeId = currentOrgId ?? internalId
|
|
39
|
-
const current = ORGS.find(o => o.id === activeId) ?? ORGS[0]
|
|
40
|
-
const { collapsed } = useSidebar()
|
|
41
|
-
|
|
42
|
-
function handleSwitch(orgId: string) {
|
|
43
|
-
if (currentOrgId === undefined) setInternalId(orgId)
|
|
44
|
-
onSwitch?.(orgId)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return (
|
|
48
|
-
<DropdownMenu>
|
|
49
|
-
<DropdownMenuTrigger asChild>
|
|
50
|
-
<button
|
|
51
|
-
className={cn(
|
|
52
|
-
'flex items-center rounded-md py-1.5 text-sm transition-colors hover:bg-[hsl(var(--accent))] focus:outline-none',
|
|
53
|
-
collapsed ? 'justify-center px-0' : 'gap-2 px-2',
|
|
54
|
-
className,
|
|
55
|
-
)}
|
|
56
|
-
>
|
|
57
|
-
<div className="flex h-6 w-6 items-center justify-center rounded bg-[hsl(var(--primary))] text-[10px] font-bold text-[hsl(var(--primary-foreground))] shrink-0">
|
|
58
|
-
{current.name[0]}
|
|
59
|
-
</div>
|
|
60
|
-
{!collapsed && <span className="font-medium max-w-[120px] truncate">{current.name}</span>}
|
|
61
|
-
{!collapsed && <ChevronDown className="h-3.5 w-3.5 text-[hsl(var(--muted-foreground))]" />}
|
|
62
|
-
</button>
|
|
63
|
-
</DropdownMenuTrigger>
|
|
64
|
-
<DropdownMenuContent align="start" className="w-52">
|
|
65
|
-
<DropdownMenuLabel>Organizations</DropdownMenuLabel>
|
|
66
|
-
<DropdownMenuSeparator />
|
|
67
|
-
{ORGS.map(org => (
|
|
68
|
-
<DropdownMenuItem key={org.id} onClick={() => handleSwitch(org.id)} className="justify-between">
|
|
69
|
-
<div className="flex items-center gap-2">
|
|
70
|
-
<div className="flex h-5 w-5 items-center justify-center rounded bg-[hsl(var(--muted))] text-[10px] font-bold">
|
|
71
|
-
{org.name[0]}
|
|
72
|
-
</div>
|
|
73
|
-
<span>{org.name}</span>
|
|
74
|
-
</div>
|
|
75
|
-
{org.id === activeId && <Check className="h-3.5 w-3.5 text-[hsl(var(--primary))]" />}
|
|
76
|
-
</DropdownMenuItem>
|
|
77
|
-
))}
|
|
78
|
-
<DropdownMenuSeparator />
|
|
79
|
-
<DropdownMenuItem onClick={onCreateOrg}>
|
|
80
|
-
<Plus className="h-4 w-4" />Create organization
|
|
81
|
-
</DropdownMenuItem>
|
|
82
|
-
</DropdownMenuContent>
|
|
83
|
-
</DropdownMenu>
|
|
84
|
-
)
|
|
85
|
-
}
|
|
1
|
+
export * from './OrganizationMenu'
|
|
@@ -1,61 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { ChevronDown, Check } from 'lucide-react'
|
|
3
|
-
import { cn } from '../utils/cn'
|
|
4
|
-
import {
|
|
5
|
-
DropdownMenu,
|
|
6
|
-
DropdownMenuContent,
|
|
7
|
-
DropdownMenuRadioGroup,
|
|
8
|
-
DropdownMenuRadioItem,
|
|
9
|
-
DropdownMenuLabel,
|
|
10
|
-
DropdownMenuSeparator,
|
|
11
|
-
DropdownMenuTrigger,
|
|
12
|
-
} from '../primitives/dropdown-menu'
|
|
13
|
-
import { Button } from '../primitives/button'
|
|
14
|
-
|
|
15
|
-
const ROLES = [
|
|
16
|
-
{ value: 'owner', label: 'Owner', description: 'Full control over the workspace' },
|
|
17
|
-
{ value: 'admin', label: 'Admin', description: 'Manage members and settings' },
|
|
18
|
-
{ value: 'member', label: 'Member', description: 'Can create and edit content' },
|
|
19
|
-
{ value: 'viewer', label: 'Viewer', description: 'Read-only access' },
|
|
20
|
-
]
|
|
21
|
-
|
|
22
|
-
export interface RolesMenuProps {
|
|
23
|
-
value?: string
|
|
24
|
-
onChange?: (role: string) => void
|
|
25
|
-
className?: string
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function RolesMenu({ value = 'member', onChange, className }: RolesMenuProps) {
|
|
29
|
-
const [role, setRole] = useState(value)
|
|
30
|
-
const current = ROLES.find(r => r.value === role) ?? ROLES[2]
|
|
31
|
-
|
|
32
|
-
const handleChange = (v: string) => {
|
|
33
|
-
setRole(v)
|
|
34
|
-
onChange?.(v)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<DropdownMenu>
|
|
39
|
-
<DropdownMenuTrigger asChild>
|
|
40
|
-
<Button variant="outline" size="sm" className={cn('gap-1.5', className)}>
|
|
41
|
-
{current.label}
|
|
42
|
-
<ChevronDown className="h-3.5 w-3.5 opacity-60" />
|
|
43
|
-
</Button>
|
|
44
|
-
</DropdownMenuTrigger>
|
|
45
|
-
<DropdownMenuContent align="end" className="w-56">
|
|
46
|
-
<DropdownMenuLabel>Change role</DropdownMenuLabel>
|
|
47
|
-
<DropdownMenuSeparator />
|
|
48
|
-
<DropdownMenuRadioGroup value={role} onValueChange={handleChange}>
|
|
49
|
-
{ROLES.map(r => (
|
|
50
|
-
<DropdownMenuRadioItem key={r.value} value={r.value} className="items-start py-2.5">
|
|
51
|
-
<div className="ml-1">
|
|
52
|
-
<p className="text-sm font-medium leading-none">{r.label}</p>
|
|
53
|
-
<p className="text-xs text-[hsl(var(--muted-foreground))] mt-0.5">{r.description}</p>
|
|
54
|
-
</div>
|
|
55
|
-
</DropdownMenuRadioItem>
|
|
56
|
-
))}
|
|
57
|
-
</DropdownMenuRadioGroup>
|
|
58
|
-
</DropdownMenuContent>
|
|
59
|
-
</DropdownMenu>
|
|
60
|
-
)
|
|
61
|
-
}
|
|
1
|
+
export * from './RolesMenu'
|
|
@@ -1,64 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { Search } from 'lucide-react'
|
|
3
|
-
import { cn } from '../utils/cn'
|
|
4
|
-
import { Input } from '../primitives/input'
|
|
5
|
-
import { Button } from '../primitives/button'
|
|
6
|
-
import { Checkbox } from '../primitives/checkbox'
|
|
7
|
-
import { Avatar, AvatarFallback } from '../primitives/avatar'
|
|
8
|
-
|
|
9
|
-
const USERS = [
|
|
10
|
-
{ id: '1', name: 'Jane Doe', email: 'jane@acme.com', initials: 'JD' },
|
|
11
|
-
{ id: '2', name: 'Bob Smith', email: 'bob@acme.com', initials: 'BS' },
|
|
12
|
-
{ id: '3', name: 'Alice Chen', email: 'alice@acme.com', initials: 'AC' },
|
|
13
|
-
{ id: '4', name: 'Tom Ray', email: 'tom@acme.com', initials: 'TR' },
|
|
14
|
-
{ id: '5', name: 'Sara Kim', email: 'sara@acme.com', initials: 'SK' },
|
|
15
|
-
{ id: '6', name: 'Mike Liu', email: 'mike@acme.com', initials: 'ML' },
|
|
16
|
-
]
|
|
17
|
-
|
|
18
|
-
export interface SelectUsersModalProps {
|
|
19
|
-
onConfirm?: (ids: string[]) => void
|
|
20
|
-
className?: string
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function SelectUsersModal({ onConfirm, className }: SelectUsersModalProps) {
|
|
24
|
-
const [query, setQuery] = useState('')
|
|
25
|
-
const [selected, setSelected] = useState<Set<string>>(new Set())
|
|
26
|
-
|
|
27
|
-
const filtered = USERS.filter(u =>
|
|
28
|
-
u.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
29
|
-
u.email.toLowerCase().includes(query.toLowerCase())
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
const toggle = (id: string) =>
|
|
33
|
-
setSelected(s => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n })
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
<div className={cn('flex flex-col', className)}>
|
|
37
|
-
<div className="p-4 border-b border-[hsl(var(--border))]">
|
|
38
|
-
<h2 className="text-sm font-semibold mb-3">Select Users</h2>
|
|
39
|
-
<div className="relative">
|
|
40
|
-
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-[hsl(var(--muted-foreground))]" />
|
|
41
|
-
<Input className="pl-8 h-8 text-sm" placeholder="Search users..." value={query} onChange={e => setQuery(e.target.value)} />
|
|
42
|
-
</div>
|
|
43
|
-
</div>
|
|
44
|
-
<div className="overflow-auto max-h-64 divide-y divide-[hsl(var(--border))]">
|
|
45
|
-
{filtered.map(u => (
|
|
46
|
-
<label key={u.id} className="flex cursor-pointer items-center gap-3 px-4 py-3 hover:bg-[hsl(var(--accent))] transition-colors">
|
|
47
|
-
<Checkbox checked={selected.has(u.id)} onCheckedChange={() => toggle(u.id)} />
|
|
48
|
-
<Avatar className="h-7 w-7"><AvatarFallback className="text-xs">{u.initials}</AvatarFallback></Avatar>
|
|
49
|
-
<div className="flex-1 min-w-0">
|
|
50
|
-
<p className="text-sm font-medium truncate">{u.name}</p>
|
|
51
|
-
<p className="text-xs text-[hsl(var(--muted-foreground))] truncate">{u.email}</p>
|
|
52
|
-
</div>
|
|
53
|
-
</label>
|
|
54
|
-
))}
|
|
55
|
-
</div>
|
|
56
|
-
<div className="flex items-center justify-between p-4 border-t border-[hsl(var(--border))]">
|
|
57
|
-
<span className="text-sm text-[hsl(var(--muted-foreground))]">{selected.size} selected</span>
|
|
58
|
-
<Button size="sm" disabled={selected.size === 0} onClick={() => onConfirm?.(Array.from(selected))}>
|
|
59
|
-
Confirm
|
|
60
|
-
</Button>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
)
|
|
64
|
-
}
|
|
1
|
+
export * from './SelectUsersModal'
|