@startsimpli/ui 0.4.6 → 0.4.7
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/package.json +2 -1
- package/src/components/ActivityTimeline.tsx +173 -0
- package/src/components/LogActivityDialog.tsx +303 -0
- package/src/components/QuickLogButtons.tsx +32 -0
- package/src/components/badge/StageBadge.tsx +31 -0
- package/src/components/badge/index.ts +3 -0
- package/src/components/command-palette/CommandPalette.tsx +344 -0
- package/src/components/command-palette/command-palette-context.tsx +51 -0
- package/src/components/command-palette/index.ts +3 -0
- package/src/components/compose/compose-header.tsx +72 -0
- package/src/components/compose/compose-loading.tsx +13 -0
- package/src/components/compose/index.ts +6 -0
- package/src/components/compose/save-status-indicator.tsx +57 -0
- package/src/components/compose/send-confirmation-dialog.tsx +87 -0
- package/src/components/compose/subject-input.tsx +25 -0
- package/src/components/compose/useAutoSave.ts +93 -0
- package/src/components/dashboard/DashboardGrid.tsx +32 -0
- package/src/components/dashboard/DashboardSection.tsx +32 -0
- package/src/components/dashboard/MetricCard.tsx +129 -0
- package/src/components/dashboard/PeriodSelector.tsx +55 -0
- package/src/components/dashboard/SparklineTrend.tsx +102 -0
- package/src/components/dashboard/index.ts +14 -0
- package/src/components/email-dialogs/index.ts +14 -0
- package/src/components/email-dialogs/merge-fields.tsx +196 -0
- package/src/components/email-dialogs/preview-dialog.tsx +194 -0
- package/src/components/email-dialogs/schedule-dialog.tsx +297 -0
- package/src/components/email-dialogs/template-picker.tsx +225 -0
- package/src/components/email-dialogs/test-send-dialog.tsx +188 -0
- package/src/components/email-editor/add-block-menu.tsx +151 -0
- package/src/components/email-editor/block-toolbar.tsx +73 -0
- package/src/components/email-editor/blocks/button-block.tsx +44 -0
- package/src/components/email-editor/blocks/divider-block.tsx +43 -0
- package/src/components/email-editor/blocks/footer-block.tsx +39 -0
- package/src/components/email-editor/blocks/header-block.tsx +39 -0
- package/src/components/email-editor/blocks/image-block.tsx +61 -0
- package/src/components/email-editor/blocks/index.ts +9 -0
- package/src/components/email-editor/blocks/metrics-block.tsx +198 -0
- package/src/components/email-editor/blocks/social-block.tsx +75 -0
- package/src/components/email-editor/blocks/spacer-block.tsx +26 -0
- package/src/components/email-editor/blocks/text-block.tsx +75 -0
- package/src/components/email-editor/editor-sidebar.tsx +791 -0
- package/src/components/email-editor/email-editor.tsx +886 -0
- package/src/components/email-editor/index.ts +50 -0
- package/src/components/email-editor/renderer/block-renderers.ts +209 -0
- package/src/components/email-editor/renderer/email-html-renderer.ts +128 -0
- package/src/components/email-editor/types.ts +413 -0
- package/src/components/email-editor/utils/defaults.ts +116 -0
- package/src/components/email-editor/utils/undo-redo.ts +59 -0
- package/src/components/enrichment/EnrichButton.tsx +33 -0
- package/src/components/enrichment/EnrichmentProgress.tsx +66 -0
- package/src/components/enrichment/QualityBadge.tsx +43 -0
- package/src/components/enrichment/index.ts +8 -0
- package/src/components/gantt/GanttChart.tsx +25 -25
- package/src/components/gantt/types.ts +5 -5
- package/src/components/index.ts +46 -0
- package/src/components/integrations/ConnectionStatus.tsx +77 -0
- package/src/components/integrations/IntegrationCard.tsx +92 -0
- package/src/components/integrations/index.ts +5 -0
- package/src/components/kanban/KanbanBoard.tsx +103 -0
- package/src/components/kanban/index.ts +2 -0
- package/src/components/lists/CreateListDialog.tsx +158 -0
- package/src/components/lists/ListCard.tsx +77 -0
- package/src/components/lists/index.ts +5 -0
- package/src/components/pipeline/StageTransitionModal.tsx +146 -0
- package/src/components/pipeline/index.ts +2 -0
- package/src/components/settings/SettingsCard.tsx +33 -0
- package/src/components/settings/SettingsLayout.tsx +28 -0
- package/src/components/settings/SettingsNav.tsx +42 -0
- package/src/components/settings/index.ts +6 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
|
|
3
|
+
export interface QualityScore {
|
|
4
|
+
score: number
|
|
5
|
+
grade: 'A' | 'B' | 'C' | 'D' | 'F'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface QualityBadgeProps {
|
|
9
|
+
quality: QualityScore
|
|
10
|
+
showScore?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const GRADE_STYLES: Record<QualityScore['grade'], string> = {
|
|
14
|
+
A: 'bg-green-100 text-green-800 border-green-200',
|
|
15
|
+
B: 'bg-blue-100 text-blue-800 border-blue-200',
|
|
16
|
+
C: 'bg-yellow-100 text-yellow-800 border-yellow-200',
|
|
17
|
+
D: 'bg-orange-100 text-orange-800 border-orange-200',
|
|
18
|
+
F: 'bg-red-100 text-red-800 border-red-200',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const GRADE_LABELS: Record<QualityScore['grade'], string> = {
|
|
22
|
+
A: 'Excellent',
|
|
23
|
+
B: 'Good',
|
|
24
|
+
C: 'Fair',
|
|
25
|
+
D: 'Poor',
|
|
26
|
+
F: 'Incomplete',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function QualityBadge({ quality, showScore = false }: QualityBadgeProps) {
|
|
30
|
+
const { grade, score } = quality
|
|
31
|
+
const styleClass = GRADE_STYLES[grade]
|
|
32
|
+
const label = GRADE_LABELS[grade]
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<span
|
|
36
|
+
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold border ${styleClass}`}
|
|
37
|
+
title={`Data quality: ${label} (${score}/100)`}
|
|
38
|
+
>
|
|
39
|
+
<span>{grade}</span>
|
|
40
|
+
{showScore && <span className="font-normal opacity-75">({score})</span>}
|
|
41
|
+
</span>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { QualityBadge } from './QualityBadge'
|
|
2
|
+
export type { QualityBadgeProps, QualityScore } from './QualityBadge'
|
|
3
|
+
|
|
4
|
+
export { EnrichButton } from './EnrichButton'
|
|
5
|
+
export type { EnrichButtonProps } from './EnrichButton'
|
|
6
|
+
|
|
7
|
+
export { EnrichmentProgress } from './EnrichmentProgress'
|
|
8
|
+
export type { EnrichmentProgressProps, QueueStatus as EnrichmentQueueStatus } from './EnrichmentProgress'
|
|
@@ -173,8 +173,8 @@ export function GanttChart({
|
|
|
173
173
|
if (statuses.length > 0 && !statuses.includes(item.status)) return false
|
|
174
174
|
if (categories.length > 0 && (!item.category || !categories.includes(item.category))) return false
|
|
175
175
|
if (dateRange.start || dateRange.end) {
|
|
176
|
-
const itemEnd = item.
|
|
177
|
-
const itemStart = item.
|
|
176
|
+
const itemEnd = item.endDate ? new Date(item.endDate) : null
|
|
177
|
+
const itemStart = item.startDate ? new Date(item.startDate) : null
|
|
178
178
|
if (dateRange.start && itemEnd && isBefore(itemEnd, dateRange.start)) return false
|
|
179
179
|
if (dateRange.end && itemStart && isAfter(itemStart, dateRange.end)) return false
|
|
180
180
|
}
|
|
@@ -229,16 +229,16 @@ export function GanttChart({
|
|
|
229
229
|
|
|
230
230
|
for (const dep of dependencies) {
|
|
231
231
|
if (dep.type === 'blocks') {
|
|
232
|
-
const parentItem = itemMap.get(dep.
|
|
233
|
-
const childItem = itemMap.get(dep.
|
|
232
|
+
const parentItem = itemMap.get(dep.fromId)
|
|
233
|
+
const childItem = itemMap.get(dep.toId)
|
|
234
234
|
if (!parentItem || !childItem) continue
|
|
235
235
|
const parentLevel = getHierarchyLevel(parentItem.title)
|
|
236
236
|
const childLevel = getHierarchyLevel(childItem.title)
|
|
237
|
-
if (parentLevel > childLevel && !parentMap.has(dep.
|
|
238
|
-
const children = childrenMap.get(dep.
|
|
239
|
-
children.push(dep.
|
|
240
|
-
childrenMap.set(dep.
|
|
241
|
-
parentMap.set(dep.
|
|
237
|
+
if (parentLevel > childLevel && !parentMap.has(dep.toId)) {
|
|
238
|
+
const children = childrenMap.get(dep.fromId) || []
|
|
239
|
+
children.push(dep.toId)
|
|
240
|
+
childrenMap.set(dep.fromId, children)
|
|
241
|
+
parentMap.set(dep.toId, dep.fromId)
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
244
|
}
|
|
@@ -257,8 +257,8 @@ export function GanttChart({
|
|
|
257
257
|
const parsedRange = parseDateRangeFromTitle(item.title, currentYear)
|
|
258
258
|
if (parsedRange) { dates.set(item.id, parsedRange); continue }
|
|
259
259
|
|
|
260
|
-
const startStr = item.
|
|
261
|
-
const endStr = item.
|
|
260
|
+
const startStr = item.startDate
|
|
261
|
+
const endStr = item.endDate
|
|
262
262
|
|
|
263
263
|
if (startStr && endStr) {
|
|
264
264
|
const s = new Date(startStr)
|
|
@@ -271,8 +271,8 @@ export function GanttChart({
|
|
|
271
271
|
if (endStr) {
|
|
272
272
|
const e = new Date(endStr)
|
|
273
273
|
if (!isNaN(e.getTime())) {
|
|
274
|
-
const s = item.
|
|
275
|
-
item.
|
|
274
|
+
const s = item.startDate ? new Date(item.startDate) :
|
|
275
|
+
item.createdAt ? new Date(item.createdAt) : addDays(e, -14)
|
|
276
276
|
dates.set(item.id, { start: startOfDay(s), end: endOfDay(e) }); continue
|
|
277
277
|
}
|
|
278
278
|
}
|
|
@@ -284,7 +284,7 @@ export function GanttChart({
|
|
|
284
284
|
}
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
-
const fallbackStart = item.
|
|
287
|
+
const fallbackStart = item.createdAt ? new Date(item.createdAt) : now
|
|
288
288
|
dates.set(item.id, { start: startOfDay(fallbackStart), end: endOfDay(addDays(fallbackStart, 14)) })
|
|
289
289
|
}
|
|
290
290
|
|
|
@@ -546,15 +546,15 @@ export function GanttChart({
|
|
|
546
546
|
tasks.forEach((task, index) => taskIndexMap.set(task.item.id, index))
|
|
547
547
|
|
|
548
548
|
for (const dep of dependencies) {
|
|
549
|
-
const fromIdx = taskIndexMap.get(dep.
|
|
550
|
-
const toIdx = taskIndexMap.get(dep.
|
|
549
|
+
const fromIdx = taskIndexMap.get(dep.fromId)
|
|
550
|
+
const toIdx = taskIndexMap.get(dep.toId)
|
|
551
551
|
if (fromIdx !== undefined && toIdx !== undefined) {
|
|
552
552
|
const fromPos = getBarPosition(tasks[fromIdx], fromIdx)
|
|
553
553
|
const toPos = getBarPosition(tasks[toIdx], toIdx)
|
|
554
554
|
const x1 = fromPos.left + fromPos.width, y1 = fromPos.top + fromPos.height / 2
|
|
555
555
|
const x2 = toPos.left, y2 = toPos.top + toPos.height / 2
|
|
556
556
|
const midX = (x1 + x2) / 2
|
|
557
|
-
paths.push({ path: `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`, fromId: dep.
|
|
557
|
+
paths.push({ path: `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`, fromId: dep.fromId, toId: dep.toId })
|
|
558
558
|
}
|
|
559
559
|
}
|
|
560
560
|
return paths
|
|
@@ -856,8 +856,8 @@ export function GanttChart({
|
|
|
856
856
|
{item.category}
|
|
857
857
|
</span>
|
|
858
858
|
)}
|
|
859
|
-
{item.
|
|
860
|
-
<span className="gantt-board-card-date">{format(new Date(item.
|
|
859
|
+
{item.endDate && (
|
|
860
|
+
<span className="gantt-board-card-date">{format(new Date(item.endDate), 'MMM d')}</span>
|
|
861
861
|
)}
|
|
862
862
|
</div>
|
|
863
863
|
))}
|
|
@@ -969,11 +969,11 @@ export function GanttChart({
|
|
|
969
969
|
<input
|
|
970
970
|
type="date"
|
|
971
971
|
className="gantt-detail-input"
|
|
972
|
-
value={detailItem.
|
|
972
|
+
value={detailItem.startDate ? format(new Date(detailItem.startDate), 'yyyy-MM-dd') : ''}
|
|
973
973
|
onChange={(e) => {
|
|
974
|
-
const updated = { ...detailItem,
|
|
974
|
+
const updated = { ...detailItem, startDate: e.target.value || null }
|
|
975
975
|
setDetailItem(updated)
|
|
976
|
-
onItemEdit(detailItem.id, {
|
|
976
|
+
onItemEdit(detailItem.id, { startDate: e.target.value || null })
|
|
977
977
|
}}
|
|
978
978
|
/>
|
|
979
979
|
</div>
|
|
@@ -982,11 +982,11 @@ export function GanttChart({
|
|
|
982
982
|
<input
|
|
983
983
|
type="date"
|
|
984
984
|
className="gantt-detail-input"
|
|
985
|
-
value={detailItem.
|
|
985
|
+
value={detailItem.endDate ? format(new Date(detailItem.endDate), 'yyyy-MM-dd') : ''}
|
|
986
986
|
onChange={(e) => {
|
|
987
|
-
const updated = { ...detailItem,
|
|
987
|
+
const updated = { ...detailItem, endDate: e.target.value || null }
|
|
988
988
|
setDetailItem(updated)
|
|
989
|
-
onItemEdit(detailItem.id, {
|
|
989
|
+
onItemEdit(detailItem.id, { endDate: e.target.value || null })
|
|
990
990
|
}}
|
|
991
991
|
/>
|
|
992
992
|
</div>
|
|
@@ -9,9 +9,9 @@ export interface TimelineItem {
|
|
|
9
9
|
status: string
|
|
10
10
|
category?: string
|
|
11
11
|
progress?: number // 0-100
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
startDate?: string | null
|
|
13
|
+
endDate?: string | null
|
|
14
|
+
createdAt?: string
|
|
15
15
|
children?: TimelineItem[]
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -19,8 +19,8 @@ export interface TimelineItem {
|
|
|
19
19
|
* Dependency between two timeline items.
|
|
20
20
|
*/
|
|
21
21
|
export interface TimelineDependency {
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
fromId: string
|
|
23
|
+
toId: string
|
|
24
24
|
type?: string
|
|
25
25
|
}
|
|
26
26
|
|
package/src/components/index.ts
CHANGED
|
@@ -76,3 +76,49 @@ export * from './wizard'
|
|
|
76
76
|
|
|
77
77
|
// Account (profile + password forms)
|
|
78
78
|
export * from './account'
|
|
79
|
+
|
|
80
|
+
// Compose flow (auto-save, header, status, confirmation dialog)
|
|
81
|
+
export * from './compose'
|
|
82
|
+
|
|
83
|
+
// Email Editor (block-based email builder with drag-drop, undo/redo, HTML renderer)
|
|
84
|
+
export * from './email-editor'
|
|
85
|
+
|
|
86
|
+
// Email dialogs (schedule, test send, preview, template picker, merge fields)
|
|
87
|
+
export * from './email-dialogs'
|
|
88
|
+
|
|
89
|
+
// Dashboard components (MetricCard, PeriodSelector, SparklineTrend, DashboardGrid, DashboardSection)
|
|
90
|
+
export * from './dashboard'
|
|
91
|
+
|
|
92
|
+
// Enrichment components (QualityBadge, EnrichButton, EnrichmentProgress)
|
|
93
|
+
export * from './enrichment'
|
|
94
|
+
|
|
95
|
+
// Integrations (IntegrationCard, ConnectionStatus)
|
|
96
|
+
export * from './integrations'
|
|
97
|
+
|
|
98
|
+
// Command Palette
|
|
99
|
+
export * from './command-palette'
|
|
100
|
+
|
|
101
|
+
// Settings components (SettingsLayout, SettingsNav, SettingsCard)
|
|
102
|
+
export * from './settings'
|
|
103
|
+
|
|
104
|
+
// Kanban board layout
|
|
105
|
+
export * from './kanban'
|
|
106
|
+
|
|
107
|
+
// List components (ListCard, CreateListDialog)
|
|
108
|
+
export * from './lists'
|
|
109
|
+
|
|
110
|
+
// Pipeline components (StageTransitionModal)
|
|
111
|
+
export * from './pipeline'
|
|
112
|
+
|
|
113
|
+
// Activity components (timeline, quick log, log dialog)
|
|
114
|
+
export { ActivityTimeline } from './ActivityTimeline'
|
|
115
|
+
export type { ActivityTimelineProps, ActivityTimelineItem } from './ActivityTimeline'
|
|
116
|
+
export { QuickLogButtons } from './QuickLogButtons'
|
|
117
|
+
export type { QuickLogButtonsProps, QuickLogAction } from './QuickLogButtons'
|
|
118
|
+
export { LogActivityDialog } from './LogActivityDialog'
|
|
119
|
+
export type {
|
|
120
|
+
LogActivityDialogProps,
|
|
121
|
+
LogActivityFormData,
|
|
122
|
+
ActivityTypeOption,
|
|
123
|
+
OutcomeOption,
|
|
124
|
+
} from './LogActivityDialog'
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { Loader2 } from 'lucide-react'
|
|
5
|
+
import { cn } from '../../lib/utils'
|
|
6
|
+
|
|
7
|
+
export interface ConnectionStatusProps {
|
|
8
|
+
providerName: string
|
|
9
|
+
providerIcon: React.ElementType
|
|
10
|
+
connected: boolean
|
|
11
|
+
accountLabel?: string
|
|
12
|
+
onConnect?: () => void
|
|
13
|
+
onDisconnect?: () => void
|
|
14
|
+
isLoading?: boolean
|
|
15
|
+
className?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ConnectionStatus({
|
|
19
|
+
providerName,
|
|
20
|
+
providerIcon: Icon,
|
|
21
|
+
connected,
|
|
22
|
+
accountLabel,
|
|
23
|
+
onConnect,
|
|
24
|
+
onDisconnect,
|
|
25
|
+
isLoading = false,
|
|
26
|
+
className,
|
|
27
|
+
}: ConnectionStatusProps) {
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
className={cn(
|
|
31
|
+
'flex items-center justify-between rounded-lg border p-4',
|
|
32
|
+
className
|
|
33
|
+
)}
|
|
34
|
+
>
|
|
35
|
+
<div className="flex items-center gap-3">
|
|
36
|
+
<div className="flex-shrink-0 w-9 h-9 rounded-lg bg-gray-100 flex items-center justify-center">
|
|
37
|
+
<Icon className="w-5 h-5 text-gray-600" />
|
|
38
|
+
</div>
|
|
39
|
+
<div>
|
|
40
|
+
<p className="text-sm font-medium">{providerName}</p>
|
|
41
|
+
{connected && accountLabel && (
|
|
42
|
+
<p className="text-xs text-muted-foreground">{accountLabel}</p>
|
|
43
|
+
)}
|
|
44
|
+
{!connected && (
|
|
45
|
+
<p className="text-xs text-muted-foreground">Not connected</p>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div>
|
|
51
|
+
{isLoading ? (
|
|
52
|
+
<button
|
|
53
|
+
disabled
|
|
54
|
+
className="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium bg-gray-100 text-gray-400 cursor-not-allowed"
|
|
55
|
+
>
|
|
56
|
+
<Loader2 className="w-3 h-3 animate-spin" />
|
|
57
|
+
Loading...
|
|
58
|
+
</button>
|
|
59
|
+
) : connected ? (
|
|
60
|
+
<button
|
|
61
|
+
onClick={onDisconnect}
|
|
62
|
+
className="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium border border-red-200 text-red-600 hover:bg-red-50 transition-colors"
|
|
63
|
+
>
|
|
64
|
+
Disconnect
|
|
65
|
+
</button>
|
|
66
|
+
) : (
|
|
67
|
+
<button
|
|
68
|
+
onClick={onConnect}
|
|
69
|
+
className="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
|
|
70
|
+
>
|
|
71
|
+
Connect
|
|
72
|
+
</button>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { ChevronRight } from 'lucide-react'
|
|
5
|
+
import { cn } from '../../lib/utils'
|
|
6
|
+
|
|
7
|
+
export type IntegrationStatus = 'connected' | 'available' | 'coming-soon'
|
|
8
|
+
|
|
9
|
+
export interface IntegrationCardProps {
|
|
10
|
+
name: string
|
|
11
|
+
description: string
|
|
12
|
+
icon: React.ElementType
|
|
13
|
+
status: IntegrationStatus
|
|
14
|
+
onClick?: () => void
|
|
15
|
+
lastSync?: string
|
|
16
|
+
className?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const STATUS_CONFIG: Record<IntegrationStatus, { label: string; className: string }> = {
|
|
20
|
+
connected: { label: 'Connected', className: 'bg-green-100 text-green-700' },
|
|
21
|
+
available: { label: 'Available', className: 'bg-blue-100 text-blue-700' },
|
|
22
|
+
'coming-soon': { label: 'Coming Soon', className: 'bg-gray-100 text-gray-500' },
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function IntegrationCard({
|
|
26
|
+
name,
|
|
27
|
+
description,
|
|
28
|
+
icon: Icon,
|
|
29
|
+
status,
|
|
30
|
+
onClick,
|
|
31
|
+
lastSync,
|
|
32
|
+
className,
|
|
33
|
+
}: IntegrationCardProps) {
|
|
34
|
+
const isClickable = status !== 'coming-soon'
|
|
35
|
+
const statusEntry = STATUS_CONFIG[status]
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
className={cn(
|
|
40
|
+
'rounded-xl border bg-card text-card-foreground shadow p-5 flex items-start gap-4',
|
|
41
|
+
isClickable && 'cursor-pointer hover:shadow-md hover:border-primary/30 transition-shadow',
|
|
42
|
+
!isClickable && 'opacity-75',
|
|
43
|
+
className
|
|
44
|
+
)}
|
|
45
|
+
onClick={isClickable ? onClick : undefined}
|
|
46
|
+
role={isClickable ? 'button' : undefined}
|
|
47
|
+
tabIndex={isClickable ? 0 : undefined}
|
|
48
|
+
onKeyDown={
|
|
49
|
+
isClickable
|
|
50
|
+
? (e: React.KeyboardEvent) => {
|
|
51
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
52
|
+
e.preventDefault()
|
|
53
|
+
onClick?.()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
: undefined
|
|
57
|
+
}
|
|
58
|
+
>
|
|
59
|
+
<div className="flex-shrink-0 w-10 h-10 rounded-lg bg-gray-100 flex items-center justify-center">
|
|
60
|
+
<Icon className="w-5 h-5 text-gray-600" />
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div className="flex-1 min-w-0">
|
|
64
|
+
<div className="flex items-center gap-2 mb-1">
|
|
65
|
+
<h3 className="font-medium text-sm truncate">{name}</h3>
|
|
66
|
+
<span
|
|
67
|
+
className={cn(
|
|
68
|
+
'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium',
|
|
69
|
+
statusEntry.className
|
|
70
|
+
)}
|
|
71
|
+
>
|
|
72
|
+
{statusEntry.label}
|
|
73
|
+
</span>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<p className="text-xs text-muted-foreground line-clamp-2">{description}</p>
|
|
77
|
+
|
|
78
|
+
{lastSync && (
|
|
79
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
80
|
+
Last synced: {lastSync}
|
|
81
|
+
</p>
|
|
82
|
+
)}
|
|
83
|
+
|
|
84
|
+
{isClickable && (
|
|
85
|
+
<span className="inline-flex items-center text-xs text-primary mt-2 font-medium">
|
|
86
|
+
Configure <ChevronRight className="w-3 h-3 ml-0.5" />
|
|
87
|
+
</span>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { cn } from '../../lib/utils'
|
|
5
|
+
|
|
6
|
+
export interface KanbanColumnConfig {
|
|
7
|
+
id: string
|
|
8
|
+
label: string
|
|
9
|
+
color?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface KanbanBoardProps<T> {
|
|
13
|
+
columns: KanbanColumnConfig[]
|
|
14
|
+
items: Record<string, T[]>
|
|
15
|
+
renderCard: (item: T) => React.ReactNode
|
|
16
|
+
renderColumnHeader?: (column: KanbanColumnConfig, items: T[]) => React.ReactNode
|
|
17
|
+
renderColumnFooter?: (column: KanbanColumnConfig) => React.ReactNode
|
|
18
|
+
emptyColumnMessage?: string
|
|
19
|
+
columnWidth?: number
|
|
20
|
+
columnRef?: (columnId: string) => React.Ref<HTMLDivElement> | undefined
|
|
21
|
+
isColumnOver?: (columnId: string) => boolean
|
|
22
|
+
className?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function KanbanBoard<T>({
|
|
26
|
+
columns,
|
|
27
|
+
items,
|
|
28
|
+
renderCard,
|
|
29
|
+
renderColumnHeader,
|
|
30
|
+
renderColumnFooter,
|
|
31
|
+
emptyColumnMessage = 'No items',
|
|
32
|
+
columnWidth = 320,
|
|
33
|
+
columnRef,
|
|
34
|
+
isColumnOver,
|
|
35
|
+
className,
|
|
36
|
+
}: KanbanBoardProps<T>) {
|
|
37
|
+
return (
|
|
38
|
+
<div className={cn('flex gap-4 overflow-x-auto pb-4', className)}>
|
|
39
|
+
{columns.map((column) => {
|
|
40
|
+
const columnItems = items[column.id] ?? []
|
|
41
|
+
const ref = columnRef?.(column.id)
|
|
42
|
+
const isOver = isColumnOver?.(column.id) ?? false
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
key={column.id}
|
|
47
|
+
ref={ref}
|
|
48
|
+
style={{ minWidth: columnWidth, maxWidth: columnWidth }}
|
|
49
|
+
className={cn(
|
|
50
|
+
'flex flex-col rounded-lg border bg-muted/50 transition-colors',
|
|
51
|
+
isOver && 'border-primary ring-2 ring-primary/20',
|
|
52
|
+
)}
|
|
53
|
+
>
|
|
54
|
+
{/* Column Header */}
|
|
55
|
+
{renderColumnHeader ? (
|
|
56
|
+
renderColumnHeader(column, columnItems)
|
|
57
|
+
) : (
|
|
58
|
+
<div className="flex items-center justify-between px-3 py-2 border-b">
|
|
59
|
+
<div className="flex items-center gap-2">
|
|
60
|
+
{column.color && (
|
|
61
|
+
<span
|
|
62
|
+
className="w-2.5 h-2.5 rounded-full shrink-0"
|
|
63
|
+
style={{ backgroundColor: column.color }}
|
|
64
|
+
aria-hidden="true"
|
|
65
|
+
/>
|
|
66
|
+
)}
|
|
67
|
+
<h3 className="text-sm font-semibold text-foreground truncate">
|
|
68
|
+
{column.label}
|
|
69
|
+
</h3>
|
|
70
|
+
</div>
|
|
71
|
+
<span className="inline-flex items-center justify-center rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground">
|
|
72
|
+
{columnItems.length}
|
|
73
|
+
</span>
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
|
|
77
|
+
{/* Column Body */}
|
|
78
|
+
<div className="flex-1 overflow-y-auto p-2 space-y-2">
|
|
79
|
+
{columnItems.length === 0 ? (
|
|
80
|
+
<p className="text-xs text-muted-foreground text-center py-8">
|
|
81
|
+
{emptyColumnMessage}
|
|
82
|
+
</p>
|
|
83
|
+
) : (
|
|
84
|
+
columnItems.map((item, index) => (
|
|
85
|
+
<React.Fragment key={index}>
|
|
86
|
+
{renderCard(item)}
|
|
87
|
+
</React.Fragment>
|
|
88
|
+
))
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
{/* Column Footer */}
|
|
93
|
+
{renderColumnFooter && (
|
|
94
|
+
<div className="border-t px-3 py-2">
|
|
95
|
+
{renderColumnFooter(column)}
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
)
|
|
100
|
+
})}
|
|
101
|
+
</div>
|
|
102
|
+
)
|
|
103
|
+
}
|