@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,115 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { Search, X } from 'lucide-react'
|
|
3
|
-
import { cn } from '../utils/cn'
|
|
4
|
-
import { Dialog, DialogContent, DialogTitle } from '../primitives/dialog'
|
|
5
|
-
|
|
6
|
-
interface UseCommandBarReturn { open: boolean; setOpen: (v: boolean) => void }
|
|
7
|
-
|
|
8
|
-
export function useCommandBar(): UseCommandBarReturn {
|
|
9
|
-
const [open, setOpen] = React.useState(false)
|
|
10
|
-
|
|
11
|
-
React.useEffect(() => {
|
|
12
|
-
const handler = (e: KeyboardEvent) => {
|
|
13
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); setOpen((o) => !o) }
|
|
14
|
-
}
|
|
15
|
-
window.addEventListener('keydown', handler)
|
|
16
|
-
return () => window.removeEventListener('keydown', handler)
|
|
17
|
-
}, [])
|
|
18
|
-
|
|
19
|
-
return { open, setOpen }
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function CommandBarEmpty({ children = 'No results found.' }: { children?: React.ReactNode }) {
|
|
23
|
-
return <div className="py-6 text-center text-sm text-[hsl(var(--muted-foreground))]">{children}</div>
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function CommandBarGroup({ heading, children }: { heading: string; children: React.ReactNode }) {
|
|
27
|
-
return (
|
|
28
|
-
<div className="px-2 py-1">
|
|
29
|
-
<p className="mb-1 px-2 text-xs font-semibold text-[hsl(var(--muted-foreground))]">{heading}</p>
|
|
30
|
-
{children}
|
|
31
|
-
</div>
|
|
32
|
-
)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface CommandBarItemProps {
|
|
36
|
-
icon?: React.ReactNode
|
|
37
|
-
label: string
|
|
38
|
-
shortcut?: string
|
|
39
|
-
onSelect?: () => void
|
|
40
|
-
active?: boolean
|
|
41
|
-
className?: string
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function CommandBarItem({ icon, label, shortcut, onSelect, active, className }: CommandBarItemProps) {
|
|
45
|
-
return (
|
|
46
|
-
<button
|
|
47
|
-
onClick={onSelect}
|
|
48
|
-
className={cn(
|
|
49
|
-
'flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm transition-colors',
|
|
50
|
-
active ? 'bg-[hsl(var(--accent))] text-[hsl(var(--accent-foreground))]' : 'hover:bg-[hsl(var(--accent)/0.6)]',
|
|
51
|
-
className
|
|
52
|
-
)}
|
|
53
|
-
>
|
|
54
|
-
{icon && <span className="h-4 w-4 shrink-0 text-[hsl(var(--muted-foreground))]">{icon}</span>}
|
|
55
|
-
<span className="flex-1 text-left truncate">{label}</span>
|
|
56
|
-
{shortcut && (
|
|
57
|
-
<kbd className="pointer-events-none shrink-0 inline-flex h-5 items-center rounded border border-[hsl(var(--border))] bg-[hsl(var(--muted))] px-1.5 text-[10px] font-medium text-[hsl(var(--muted-foreground))]">
|
|
58
|
-
{shortcut}
|
|
59
|
-
</kbd>
|
|
60
|
-
)}
|
|
61
|
-
</button>
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
interface CommandBarProps {
|
|
66
|
-
open: boolean
|
|
67
|
-
onOpenChange: (v: boolean) => void
|
|
68
|
-
children: React.ReactNode
|
|
69
|
-
placeholder?: string
|
|
70
|
-
onSearch?: (q: string) => void
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function CommandBar({ open, onOpenChange, children, placeholder = 'Search commands...', onSearch }: CommandBarProps) {
|
|
74
|
-
const [query, setQuery] = React.useState('')
|
|
75
|
-
|
|
76
|
-
const handleSearch = (v: string) => { setQuery(v); onSearch?.(v) }
|
|
77
|
-
|
|
78
|
-
const items = React.useRef<HTMLDivElement>(null)
|
|
79
|
-
|
|
80
|
-
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
81
|
-
if (e.key === 'Escape') { onOpenChange(false); return }
|
|
82
|
-
const focusable = items.current?.querySelectorAll<HTMLButtonElement>('button')
|
|
83
|
-
if (!focusable?.length) return
|
|
84
|
-
const arr = Array.from(focusable)
|
|
85
|
-
const idx = arr.indexOf(document.activeElement as HTMLButtonElement)
|
|
86
|
-
if (e.key === 'ArrowDown') { e.preventDefault(); arr[(idx + 1) % arr.length]?.focus() }
|
|
87
|
-
if (e.key === 'ArrowUp') { e.preventDefault(); arr[(idx - 1 + arr.length) % arr.length]?.focus() }
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return (
|
|
91
|
-
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
92
|
-
<DialogContent className="p-0 gap-0 max-w-lg overflow-hidden" onKeyDown={handleKeyDown}>
|
|
93
|
-
<DialogTitle className="sr-only">Command Bar</DialogTitle>
|
|
94
|
-
<div className="flex items-center gap-2 border-b border-[hsl(var(--border))] px-4 py-3">
|
|
95
|
-
<Search className="h-4 w-4 shrink-0 text-[hsl(var(--muted-foreground))]" />
|
|
96
|
-
<input
|
|
97
|
-
autoFocus
|
|
98
|
-
className="flex-1 bg-transparent text-sm outline-none placeholder:text-[hsl(var(--muted-foreground))]"
|
|
99
|
-
placeholder={placeholder}
|
|
100
|
-
value={query}
|
|
101
|
-
onChange={(e) => handleSearch(e.target.value)}
|
|
102
|
-
/>
|
|
103
|
-
{query && (
|
|
104
|
-
<button onClick={() => handleSearch('')} className="text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--foreground))]">
|
|
105
|
-
<X className="h-4 w-4" />
|
|
106
|
-
</button>
|
|
107
|
-
)}
|
|
108
|
-
</div>
|
|
109
|
-
<div ref={items} className="max-h-80 overflow-y-auto py-2">
|
|
110
|
-
{children}
|
|
111
|
-
</div>
|
|
112
|
-
</DialogContent>
|
|
113
|
-
</Dialog>
|
|
114
|
-
)
|
|
115
|
-
}
|
|
1
|
+
export * from './CommandBar'
|
package/src/advanced/filters.tsx
CHANGED
|
@@ -1,103 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { Plus, X } from 'lucide-react'
|
|
3
|
-
import { cn } from '../utils/cn'
|
|
4
|
-
import { Button } from '../primitives/button'
|
|
5
|
-
import { Popover, PopoverContent, PopoverTrigger } from '../primitives/popover'
|
|
6
|
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../primitives/select'
|
|
7
|
-
import { Input } from '../primitives/input'
|
|
8
|
-
|
|
9
|
-
export interface FilterDef {
|
|
10
|
-
key: string
|
|
11
|
-
label: string
|
|
12
|
-
type: 'text' | 'select' | 'date' | 'number'
|
|
13
|
-
options?: { label: string; value: string }[]
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface FilterValue {
|
|
17
|
-
key: string
|
|
18
|
-
operator: string
|
|
19
|
-
value: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const textOps = ['contains', 'equals', 'starts with']
|
|
23
|
-
const numOps = ['=', '>', '<', '>=', '<=']
|
|
24
|
-
|
|
25
|
-
function AddFilterPopover({ filters, onAdd }: { filters: FilterDef[]; onAdd: (v: FilterValue) => void }) {
|
|
26
|
-
const [open, setOpen] = React.useState(false)
|
|
27
|
-
const [key, setKey] = React.useState('')
|
|
28
|
-
const [op, setOp] = React.useState('')
|
|
29
|
-
const [val, setVal] = React.useState('')
|
|
30
|
-
|
|
31
|
-
const def = filters.find((f) => f.key === key)
|
|
32
|
-
const ops = def?.type === 'number' ? numOps : textOps
|
|
33
|
-
|
|
34
|
-
const handleAdd = () => {
|
|
35
|
-
if (!key || !val) return
|
|
36
|
-
onAdd({ key, operator: op || ops[0], value: val })
|
|
37
|
-
setKey(''); setOp(''); setVal('')
|
|
38
|
-
setOpen(false)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<Popover open={open} onOpenChange={setOpen}>
|
|
43
|
-
<PopoverTrigger asChild>
|
|
44
|
-
<Button variant="outline" size="sm" className="h-7 gap-1 text-xs"><Plus className="h-3.5 w-3.5" />Add Filter</Button>
|
|
45
|
-
</PopoverTrigger>
|
|
46
|
-
<PopoverContent className="w-72 p-3 space-y-2">
|
|
47
|
-
<Select value={key} onValueChange={setKey}>
|
|
48
|
-
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="Field" /></SelectTrigger>
|
|
49
|
-
<SelectContent>{filters.map((f) => <SelectItem key={f.key} value={f.key}>{f.label}</SelectItem>)}</SelectContent>
|
|
50
|
-
</Select>
|
|
51
|
-
{key && (
|
|
52
|
-
<Select value={op || ops[0]} onValueChange={setOp}>
|
|
53
|
-
<SelectTrigger className="h-8 text-xs"><SelectValue /></SelectTrigger>
|
|
54
|
-
<SelectContent>{ops.map((o) => <SelectItem key={o} value={o}>{o}</SelectItem>)}</SelectContent>
|
|
55
|
-
</Select>
|
|
56
|
-
)}
|
|
57
|
-
{key && def?.type === 'select' ? (
|
|
58
|
-
<Select value={val} onValueChange={setVal}>
|
|
59
|
-
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="Value" /></SelectTrigger>
|
|
60
|
-
<SelectContent>{def.options?.map((o) => <SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>)}</SelectContent>
|
|
61
|
-
</Select>
|
|
62
|
-
) : key && (
|
|
63
|
-
<Input type={def?.type === 'number' ? 'number' : def?.type === 'date' ? 'date' : 'text'} className="h-8 text-xs" value={val} onChange={(e) => setVal(e.target.value)} placeholder="Value" />
|
|
64
|
-
)}
|
|
65
|
-
<Button size="sm" className="w-full h-7 text-xs" onClick={handleAdd} disabled={!key || !val}>Apply</Button>
|
|
66
|
-
</PopoverContent>
|
|
67
|
-
</Popover>
|
|
68
|
-
)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function FilterBadge({ filter, def, onRemove }: { filter: FilterValue; def?: FilterDef; onRemove: () => void }) {
|
|
72
|
-
return (
|
|
73
|
-
<span className="inline-flex items-center gap-1 rounded-full border border-[hsl(var(--border))] bg-[hsl(var(--muted))] px-2.5 py-0.5 text-xs font-medium text-[hsl(var(--foreground))]">
|
|
74
|
-
<span className="text-[hsl(var(--muted-foreground))]">{def?.label ?? filter.key}</span>
|
|
75
|
-
<span>{filter.operator}</span>
|
|
76
|
-
<span className="font-semibold">{filter.value}</span>
|
|
77
|
-
<button onClick={onRemove} className="ml-0.5 rounded-full hover:text-[hsl(var(--destructive))]"><X className="h-3 w-3" /></button>
|
|
78
|
-
</span>
|
|
79
|
-
)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function FilterItem({ children }: { children: React.ReactNode }) {
|
|
83
|
-
return <div className="flex items-center gap-1">{children}</div>
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
interface FiltersProps {
|
|
87
|
-
filters: FilterDef[]
|
|
88
|
-
value: FilterValue[]
|
|
89
|
-
onChange: (v: FilterValue[]) => void
|
|
90
|
-
className?: string
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function Filters({ filters, value, onChange, className }: FiltersProps) {
|
|
94
|
-
const remove = (i: number) => onChange(value.filter((_, idx) => idx !== i))
|
|
95
|
-
return (
|
|
96
|
-
<div className={cn('flex flex-wrap items-center gap-2', className)}>
|
|
97
|
-
{value.map((fv, i) => (
|
|
98
|
-
<FilterBadge key={i} filter={fv} def={filters.find((f) => f.key === fv.key)} onRemove={() => remove(i)} />
|
|
99
|
-
))}
|
|
100
|
-
<AddFilterPopover filters={filters} onAdd={(fv) => onChange([...value, fv])} />
|
|
101
|
-
</div>
|
|
102
|
-
)
|
|
103
|
-
}
|
|
1
|
+
export * from './Filters/index'
|
package/src/advanced/kanban.tsx
CHANGED
|
@@ -1,95 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { DndContext, DragEndEvent, DragOverEvent, DragOverlay, DragStartEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'
|
|
3
|
-
import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable'
|
|
4
|
-
import { CSS } from '@dnd-kit/utilities'
|
|
5
|
-
import { cn } from '../utils/cn'
|
|
6
|
-
|
|
7
|
-
export interface KanbanCardDef { id: string; title: string; description?: string; badge?: string }
|
|
8
|
-
export interface KanbanColumnDef { id: string; title: string; cards: KanbanCardDef[] }
|
|
9
|
-
|
|
10
|
-
export function KanbanCard({ card, isDragging }: { card: KanbanCardDef; isDragging?: boolean }) {
|
|
11
|
-
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: card.id })
|
|
12
|
-
const style = { transform: CSS.Transform.toString(transform), transition }
|
|
13
|
-
return (
|
|
14
|
-
<div ref={setNodeRef} style={style} {...attributes} {...listeners}
|
|
15
|
-
className={cn('rounded-md border border-[hsl(var(--border))] bg-[hsl(var(--card))] p-3 cursor-grab active:cursor-grabbing select-none', isDragging && 'shadow-lg opacity-80')}>
|
|
16
|
-
<div className="flex items-start justify-between gap-2">
|
|
17
|
-
<p className="text-sm font-medium text-[hsl(var(--foreground))] leading-tight">{card.title}</p>
|
|
18
|
-
{card.badge && (
|
|
19
|
-
<span className="shrink-0 text-[10px] px-1.5 py-0.5 rounded-full bg-[hsl(var(--primary)/0.1)] text-[hsl(var(--primary))] font-medium">{card.badge}</span>
|
|
20
|
-
)}
|
|
21
|
-
</div>
|
|
22
|
-
{card.description && <p className="mt-1 text-xs text-[hsl(var(--muted-foreground))] line-clamp-2">{card.description}</p>}
|
|
23
|
-
</div>
|
|
24
|
-
)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function KanbanColumn({ column, isOver }: { column: KanbanColumnDef; isOver?: boolean }) {
|
|
28
|
-
return (
|
|
29
|
-
<div className={cn('flex w-64 shrink-0 flex-col gap-2 rounded-md bg-[hsl(var(--muted)/0.4)] p-3 transition-colors', isOver && 'bg-[hsl(var(--muted)/0.7)]')}>
|
|
30
|
-
<div className="flex items-center justify-between">
|
|
31
|
-
<h3 className="text-sm font-semibold text-[hsl(var(--foreground))]">{column.title}</h3>
|
|
32
|
-
<span className="text-xs text-[hsl(var(--muted-foreground))] bg-[hsl(var(--muted))] rounded-full px-2 py-0.5">{column.cards.length}</span>
|
|
33
|
-
</div>
|
|
34
|
-
<SortableContext items={column.cards.map((c) => c.id)} strategy={verticalListSortingStrategy}>
|
|
35
|
-
<div className="flex flex-col gap-2 min-h-[60px]">
|
|
36
|
-
{column.cards.map((card) => <KanbanCard key={card.id} card={card} />)}
|
|
37
|
-
</div>
|
|
38
|
-
</SortableContext>
|
|
39
|
-
</div>
|
|
40
|
-
)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
interface KanbanProps {
|
|
44
|
-
columns: KanbanColumnDef[]
|
|
45
|
-
onMoveCard?: (cardId: string, fromCol: string, toCol: string) => void
|
|
46
|
-
className?: string
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function Kanban({ columns: initialColumns, onMoveCard, className }: KanbanProps) {
|
|
50
|
-
const [cols, setCols] = React.useState<KanbanColumnDef[]>(initialColumns)
|
|
51
|
-
const [activeCard, setActiveCard] = React.useState<KanbanCardDef | null>(null)
|
|
52
|
-
const [overColId, setOverColId] = React.useState<string | null>(null)
|
|
53
|
-
|
|
54
|
-
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } }))
|
|
55
|
-
|
|
56
|
-
const findColByCard = (cardId: string) => cols.find((c) => c.cards.some((card) => card.id === cardId))
|
|
57
|
-
|
|
58
|
-
const onDragStart = ({ active }: DragStartEvent) => {
|
|
59
|
-
const col = findColByCard(active.id as string)
|
|
60
|
-
setActiveCard(col?.cards.find((c) => c.id === active.id) ?? null)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const onDragOver = ({ over }: DragOverEvent) => {
|
|
64
|
-
if (!over) { setOverColId(null); return }
|
|
65
|
-
const colId = cols.find((c) => c.id === over.id)?.id ?? findColByCard(over.id as string)?.id ?? null
|
|
66
|
-
setOverColId(colId)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const onDragEnd = ({ active, over }: DragEndEvent) => {
|
|
70
|
-
setActiveCard(null); setOverColId(null)
|
|
71
|
-
if (!over || active.id === over.id) return
|
|
72
|
-
const fromCol = findColByCard(active.id as string)
|
|
73
|
-
const toCol = cols.find((c) => c.id === over.id) ?? findColByCard(over.id as string)
|
|
74
|
-
if (!fromCol || !toCol || fromCol.id === toCol.id) return
|
|
75
|
-
|
|
76
|
-
const card = fromCol.cards.find((c) => c.id === active.id)!
|
|
77
|
-
setCols((prev) => prev.map((c) => {
|
|
78
|
-
if (c.id === fromCol.id) return { ...c, cards: c.cards.filter((card) => card.id !== active.id) }
|
|
79
|
-
if (c.id === toCol.id) return { ...c, cards: [...c.cards, card] }
|
|
80
|
-
return c
|
|
81
|
-
}))
|
|
82
|
-
onMoveCard?.(active.id as string, fromCol.id, toCol.id)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return (
|
|
86
|
-
<DndContext sensors={sensors} onDragStart={onDragStart} onDragOver={onDragOver} onDragEnd={onDragEnd}>
|
|
87
|
-
<div className={cn('flex gap-4 overflow-x-auto pb-4', className)}>
|
|
88
|
-
{cols.map((col) => <KanbanColumn key={col.id} column={col} isOver={overColId === col.id} />)}
|
|
89
|
-
</div>
|
|
90
|
-
<DragOverlay>
|
|
91
|
-
{activeCard && <KanbanCard card={activeCard} isDragging />}
|
|
92
|
-
</DragOverlay>
|
|
93
|
-
</DndContext>
|
|
94
|
-
)
|
|
95
|
-
}
|
|
1
|
+
export * from './Kanban/index'
|
|
@@ -1,78 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { cn } from '../utils/cn'
|
|
3
|
-
|
|
4
|
-
interface ToggleButtonProps {
|
|
5
|
-
value: string
|
|
6
|
-
pressed?: boolean
|
|
7
|
-
onClick?: () => void
|
|
8
|
-
disabled?: boolean
|
|
9
|
-
children: React.ReactNode
|
|
10
|
-
className?: string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function ToggleButton({ pressed, onClick, disabled, children, className }: ToggleButtonProps) {
|
|
14
|
-
return (
|
|
15
|
-
<button
|
|
16
|
-
type="button"
|
|
17
|
-
aria-pressed={pressed}
|
|
18
|
-
disabled={disabled}
|
|
19
|
-
onClick={onClick}
|
|
20
|
-
className={cn(
|
|
21
|
-
'inline-flex items-center justify-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors border',
|
|
22
|
-
'disabled:pointer-events-none disabled:opacity-50',
|
|
23
|
-
pressed
|
|
24
|
-
? 'bg-[hsl(var(--primary))] text-[hsl(var(--primary-foreground))] border-[hsl(var(--primary))]'
|
|
25
|
-
: 'bg-transparent text-[hsl(var(--foreground))] border-[hsl(var(--border))] hover:bg-[hsl(var(--accent))] hover:text-[hsl(var(--accent-foreground))]',
|
|
26
|
-
className
|
|
27
|
-
)}
|
|
28
|
-
>
|
|
29
|
-
{children}
|
|
30
|
-
</button>
|
|
31
|
-
)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
interface ToggleButtonGroupSingle {
|
|
35
|
-
type?: 'single'
|
|
36
|
-
value: string
|
|
37
|
-
onChange: (v: string) => void
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
interface ToggleButtonGroupMultiple {
|
|
41
|
-
type: 'multiple'
|
|
42
|
-
value: string[]
|
|
43
|
-
onChange: (v: string[]) => void
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
type ToggleButtonGroupProps = (ToggleButtonGroupSingle | ToggleButtonGroupMultiple) & {
|
|
47
|
-
children: React.ReactNode
|
|
48
|
-
className?: string
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function ToggleButtonGroup(props: ToggleButtonGroupProps) {
|
|
52
|
-
const { children, className } = props
|
|
53
|
-
|
|
54
|
-
const isPressed = (val: string) =>
|
|
55
|
-
props.type === 'multiple' ? (props.value as string[]).includes(val) : props.value === val
|
|
56
|
-
|
|
57
|
-
const handleClick = (val: string) => {
|
|
58
|
-
if (props.type === 'multiple') {
|
|
59
|
-
const current = props.value as string[]
|
|
60
|
-
props.onChange(current.includes(val) ? current.filter((v) => v !== val) : [...current, val])
|
|
61
|
-
} else {
|
|
62
|
-
;(props as ToggleButtonGroupSingle).onChange(val)
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return (
|
|
67
|
-
<div className={cn('inline-flex items-center gap-1 rounded-md border border-[hsl(var(--border))] bg-[hsl(var(--muted)/0.4)] p-0.5', className)}>
|
|
68
|
-
{React.Children.map(children, (child) => {
|
|
69
|
-
if (!React.isValidElement(child)) return child
|
|
70
|
-
const tb = child as React.ReactElement<ToggleButtonProps>
|
|
71
|
-
return React.cloneElement(tb, {
|
|
72
|
-
pressed: isPressed(tb.props.value),
|
|
73
|
-
onClick: () => handleClick(tb.props.value),
|
|
74
|
-
})
|
|
75
|
-
})}
|
|
76
|
-
</div>
|
|
77
|
-
)
|
|
78
|
-
}
|
|
1
|
+
export * from './ToggleButton'
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
2
|
import { Send } from 'lucide-react'
|
|
3
|
-
import { cn } from '
|
|
4
|
-
import { Avatar, AvatarFallback } from '
|
|
5
|
-
import { Input } from '
|
|
6
|
-
import { Button } from '
|
|
3
|
+
import { cn } from '../../utils/cn'
|
|
4
|
+
import { Avatar, AvatarFallback } from '../../primitives/avatar'
|
|
5
|
+
import { Input } from '../../primitives/input'
|
|
6
|
+
import { Button } from '../../primitives/button'
|
|
7
7
|
|
|
8
8
|
interface Message { id: string; author: string; initials: string; text: string; time: string; self?: boolean }
|
|
9
9
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
|
-
import { cn } from '
|
|
3
|
-
import { Button } from '
|
|
4
|
-
import { Textarea } from '
|
|
2
|
+
import { cn } from '../../utils/cn'
|
|
3
|
+
import { Button } from '../../primitives/button'
|
|
4
|
+
import { Textarea } from '../../primitives/textarea'
|
|
5
5
|
|
|
6
6
|
const REACTIONS = ['😊', '😐', '😕', '😡', '🤩']
|
|
7
7
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
import React from 'react'
|
|
3
3
|
import { FileText, Image, FileCode, Film, Archive, File } from 'lucide-react'
|
|
4
|
-
import { cn } from '
|
|
5
|
-
import { Card, CardContent } from '
|
|
4
|
+
import { cn } from '../../utils/cn'
|
|
5
|
+
import { Card, CardContent } from '../../primitives/card'
|
|
6
6
|
|
|
7
7
|
interface FileItem { id: string; name: string; type: string; size: string; modified: string }
|
|
8
8
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
import React from 'react'
|
|
3
3
|
import { FileText, Image, FileCode, Film, Archive, MoreHorizontal, Download, Trash2 } from 'lucide-react'
|
|
4
|
-
import { cn } from '
|
|
5
|
-
import { Button } from '
|
|
4
|
+
import { cn } from '../../utils/cn'
|
|
5
|
+
import { Button } from '../../primitives/button'
|
|
6
6
|
import {
|
|
7
7
|
DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,
|
|
8
|
-
} from '
|
|
8
|
+
} from '../../primitives/dropdown-menu'
|
|
9
9
|
|
|
10
10
|
interface FileItem { id: string; name: string; type: string; size: string; modified: string }
|
|
11
11
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
|
-
import { cn } from '
|
|
3
|
-
import { Badge } from '
|
|
4
|
-
import { Button } from '
|
|
2
|
+
import { cn } from '../../utils/cn'
|
|
3
|
+
import { Badge } from '../../primitives/badge'
|
|
4
|
+
import { Button } from '../../primitives/button'
|
|
5
5
|
|
|
6
6
|
export interface IntegrationCardProps {
|
|
7
7
|
icon?: React.ReactNode
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
2
|
import { X } from 'lucide-react'
|
|
3
|
-
import { cn } from '
|
|
4
|
-
import { Input } from '
|
|
5
|
-
import { Button } from '
|
|
6
|
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '
|
|
7
|
-
import { Avatar, AvatarFallback } from '
|
|
8
|
-
import { Badge } from '
|
|
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
9
|
|
|
10
10
|
const PENDING = [
|
|
11
11
|
{ email: 'alex@startup.io', role: 'Member' },
|
|
@@ -53,7 +53,7 @@ export function InviteModal({ className }: InviteModalProps) {
|
|
|
53
53
|
<div className="divide-y divide-[hsl(var(--border))] rounded-md border border-[hsl(var(--border))]">
|
|
54
54
|
{pending.map((p, i) => (
|
|
55
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
|
|
56
|
+
<Avatar className="h-6 w-6"><AvatarFallback className="text-[10px]">{p.email.charAt(0).toUpperCase()}</AvatarFallback></Avatar>
|
|
57
57
|
<span className="flex-1 text-sm truncate">{p.email}</span>
|
|
58
58
|
<Badge variant="outline" className="text-[10px] px-1.5 py-0.5 shrink-0">{p.role}</Badge>
|
|
59
59
|
<button onClick={() => setPending(ps => ps.filter((_, j) => j !== i))}>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
2
|
import { X, Pencil } from 'lucide-react'
|
|
3
|
-
import { cn } from '
|
|
4
|
-
import { Input } from '
|
|
5
|
-
import { Button } from '
|
|
3
|
+
import { cn } from '../../utils/cn'
|
|
4
|
+
import { Input } from '../../primitives/input'
|
|
5
|
+
import { Button } from '../../primitives/button'
|
|
6
6
|
|
|
7
7
|
const COLORS = ['bg-blue-500', 'bg-green-500', 'bg-yellow-500', 'bg-purple-500', 'bg-red-500', 'bg-pink-500']
|
|
8
8
|
|
|
@@ -25,7 +25,7 @@ export function ManageTagsModal({ className }: ManageTagsModalProps) {
|
|
|
25
25
|
const addTag = () => {
|
|
26
26
|
if (!newTag.trim()) return
|
|
27
27
|
const id = `tag-${Date.now()}`
|
|
28
|
-
setTags(t => [...t, { id, name: newTag.trim(), color: COLORS[colorIdx % COLORS.length] }])
|
|
28
|
+
setTags(t => [...t, { id, name: newTag.trim(), color: COLORS[colorIdx % COLORS.length]! }])
|
|
29
29
|
setNewTag('')
|
|
30
30
|
setColorIdx(i => i + 1)
|
|
31
31
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { cn } from '
|
|
3
|
-
import { Avatar, AvatarFallback } from '
|
|
4
|
-
import { Card, CardContent, CardHeader, CardTitle } from '
|
|
2
|
+
import { cn } from '../../utils/cn'
|
|
3
|
+
import { Avatar, AvatarFallback } from '../../primitives/avatar'
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from '../../primitives/card'
|
|
5
5
|
|
|
6
6
|
interface Message {
|
|
7
7
|
id: string
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { TrendingUp, TrendingDown } from 'lucide-react'
|
|
3
|
-
import { cn } from '
|
|
4
|
-
import { Card, CardContent } from '
|
|
5
|
-
import { Button } from '
|
|
3
|
+
import { cn } from '../../utils/cn'
|
|
4
|
+
import { Card, CardContent } from '../../primitives/card'
|
|
5
|
+
import { Button } from '../../primitives/button'
|
|
6
6
|
|
|
7
7
|
export interface MetricCardProps {
|
|
8
8
|
label?: string
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
|
-
import { cn } from '
|
|
3
|
-
import { Switch } from '
|
|
4
|
-
import { Separator } from '
|
|
2
|
+
import { cn } from '../../utils/cn'
|
|
3
|
+
import { Switch } from '../../primitives/switch'
|
|
4
|
+
import { Separator } from '../../primitives/separator'
|
|
5
5
|
|
|
6
6
|
const NOTIFICATIONS = [
|
|
7
7
|
{ id: 'comments', label: 'Comments', description: 'Get notified when someone comments on your post', default: true },
|
|
@@ -51,7 +51,7 @@ export function NotificationSettings({ className }: NotificationSettingsProps) {
|
|
|
51
51
|
<NotifRow
|
|
52
52
|
key={n.id}
|
|
53
53
|
{...n}
|
|
54
|
-
checked={states[n.id]}
|
|
54
|
+
checked={states[n.id] ?? false}
|
|
55
55
|
onChange={val => setStates(s => ({ ...s, [n.id]: val }))}
|
|
56
56
|
/>
|
|
57
57
|
))}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React from 'react'
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
2
|
import { ChevronDown, Check, Plus } from 'lucide-react'
|
|
3
|
-
import { cn } from '
|
|
3
|
+
import { cn } from '../../utils/cn'
|
|
4
4
|
import {
|
|
5
5
|
DropdownMenu,
|
|
6
6
|
DropdownMenuContent,
|
|
@@ -8,7 +8,8 @@ import {
|
|
|
8
8
|
DropdownMenuLabel,
|
|
9
9
|
DropdownMenuSeparator,
|
|
10
10
|
DropdownMenuTrigger,
|
|
11
|
-
} from '
|
|
11
|
+
} from '../../primitives/dropdown-menu'
|
|
12
|
+
import { useSidebar } from '../../app-shell/Sidebar'
|
|
12
13
|
|
|
13
14
|
const ORGS = [
|
|
14
15
|
{ id: '1', name: 'Acme Corp', slug: 'acme', plan: 'Pro' },
|
|
@@ -17,43 +18,61 @@ const ORGS = [
|
|
|
17
18
|
]
|
|
18
19
|
|
|
19
20
|
export interface OrgMenuProps {
|
|
21
|
+
/** Controlled mode — pass to manage selection externally. If omitted, the menu manages its own state. */
|
|
20
22
|
currentOrgId?: string
|
|
23
|
+
/** Default selection in uncontrolled mode. Defaults to the first org. */
|
|
24
|
+
defaultOrgId?: string
|
|
21
25
|
onSwitch?: (orgId: string) => void
|
|
22
26
|
onCreateOrg?: () => void
|
|
23
27
|
className?: string
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
export function OrgMenu({
|
|
27
|
-
currentOrgId
|
|
31
|
+
currentOrgId,
|
|
32
|
+
defaultOrgId,
|
|
28
33
|
onSwitch,
|
|
29
34
|
onCreateOrg,
|
|
30
35
|
className,
|
|
31
36
|
}: OrgMenuProps) {
|
|
32
|
-
const
|
|
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
|
+
}
|
|
33
46
|
|
|
34
47
|
return (
|
|
35
48
|
<DropdownMenu>
|
|
36
49
|
<DropdownMenuTrigger asChild>
|
|
37
|
-
<button
|
|
38
|
-
|
|
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">
|
|
39
58
|
{current.name[0]}
|
|
40
59
|
</div>
|
|
41
|
-
<span className="font-medium max-w-[120px] truncate">{current.name}</span>
|
|
42
|
-
<ChevronDown className="h-3.5 w-3.5 text-[hsl(var(--muted-foreground))]" />
|
|
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))]" />}
|
|
43
62
|
</button>
|
|
44
63
|
</DropdownMenuTrigger>
|
|
45
64
|
<DropdownMenuContent align="start" className="w-52">
|
|
46
65
|
<DropdownMenuLabel>Organizations</DropdownMenuLabel>
|
|
47
66
|
<DropdownMenuSeparator />
|
|
48
67
|
{ORGS.map(org => (
|
|
49
|
-
<DropdownMenuItem key={org.id} onClick={() =>
|
|
68
|
+
<DropdownMenuItem key={org.id} onClick={() => handleSwitch(org.id)} className="justify-between">
|
|
50
69
|
<div className="flex items-center gap-2">
|
|
51
70
|
<div className="flex h-5 w-5 items-center justify-center rounded bg-[hsl(var(--muted))] text-[10px] font-bold">
|
|
52
71
|
{org.name[0]}
|
|
53
72
|
</div>
|
|
54
73
|
<span>{org.name}</span>
|
|
55
74
|
</div>
|
|
56
|
-
{org.id ===
|
|
75
|
+
{org.id === activeId && <Check className="h-3.5 w-3.5 text-[hsl(var(--primary))]" />}
|
|
57
76
|
</DropdownMenuItem>
|
|
58
77
|
))}
|
|
59
78
|
<DropdownMenuSeparator />
|