kanban-lite 1.0.4
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/.editorconfig +9 -0
- package/.github/workflows/ci.yml +59 -0
- package/.github/workflows/release.yml +75 -0
- package/.prettierignore +6 -0
- package/.prettierrc.yaml +4 -0
- package/.vscode/extensions.json +3 -0
- package/.vscode/launch.json +17 -0
- package/.vscode/settings.json +21 -0
- package/.vscode/tasks.json +22 -0
- package/.vscodeignore +11 -0
- package/CHANGELOG.md +184 -0
- package/CLAUDE.md +58 -0
- package/CONTRIBUTING.md +114 -0
- package/LICENSE +22 -0
- package/README.md +482 -0
- package/SKILL.md +237 -0
- package/dist/cli.js +8716 -0
- package/dist/extension.js +8463 -0
- package/dist/mcp-server.js +1327 -0
- package/dist/standalone-webview/icons-Dx9MGYqN.js +180 -0
- package/dist/standalone-webview/icons-Dx9MGYqN.js.map +1 -0
- package/dist/standalone-webview/index.js +85 -0
- package/dist/standalone-webview/index.js.map +1 -0
- package/dist/standalone-webview/react-vendor-DkYdDBET.js +25 -0
- package/dist/standalone-webview/react-vendor-DkYdDBET.js.map +1 -0
- package/dist/standalone-webview/style.css +1 -0
- package/dist/standalone.js +7513 -0
- package/dist/webview/icons-Dx9MGYqN.js +180 -0
- package/dist/webview/icons-Dx9MGYqN.js.map +1 -0
- package/dist/webview/index.js +85 -0
- package/dist/webview/index.js.map +1 -0
- package/dist/webview/react-vendor-DkYdDBET.js +25 -0
- package/dist/webview/react-vendor-DkYdDBET.js.map +1 -0
- package/dist/webview/style.css +1 -0
- package/docs/images/board-overview.png +0 -0
- package/docs/images/editor-view.png +0 -0
- package/docs/plans/2026-02-20-kanban-json-config-design.md +74 -0
- package/docs/plans/2026-02-20-kanban-json-config.md +690 -0
- package/eslint.config.mjs +31 -0
- package/package.json +161 -0
- package/postcss.config.js +6 -0
- package/resources/icon-light.png +0 -0
- package/resources/icon-light.svg +105 -0
- package/resources/icon.png +0 -0
- package/resources/icon.svg +105 -0
- package/resources/kanban-dark.svg +21 -0
- package/resources/kanban-light.svg +21 -0
- package/resources/kanban.svg +21 -0
- package/src/cli/index.ts +846 -0
- package/src/extension/FeatureHeaderProvider.ts +370 -0
- package/src/extension/KanbanPanel.ts +973 -0
- package/src/extension/SidebarViewProvider.ts +507 -0
- package/src/extension/featureFileUtils.ts +82 -0
- package/src/extension/index.ts +234 -0
- package/src/mcp-server/index.ts +632 -0
- package/src/sdk/KanbanSDK.ts +349 -0
- package/src/sdk/__tests__/KanbanSDK.test.ts +468 -0
- package/src/sdk/__tests__/parser.test.ts +170 -0
- package/src/sdk/fileUtils.ts +76 -0
- package/src/sdk/index.ts +6 -0
- package/src/sdk/parser.ts +70 -0
- package/src/sdk/types.ts +15 -0
- package/src/shared/config.ts +113 -0
- package/src/shared/editorTypes.ts +14 -0
- package/src/shared/types.ts +120 -0
- package/src/standalone/__tests__/server.integration.test.ts +1916 -0
- package/src/standalone/__tests__/webhooks.test.ts +357 -0
- package/src/standalone/fileUtils.ts +70 -0
- package/src/standalone/index.ts +71 -0
- package/src/standalone/server.ts +1046 -0
- package/src/standalone/webhooks.ts +135 -0
- package/src/webview/App.tsx +469 -0
- package/src/webview/assets/main.css +329 -0
- package/src/webview/assets/standalone-theme.css +130 -0
- package/src/webview/components/ColumnDialog.tsx +119 -0
- package/src/webview/components/CreateFeatureDialog.tsx +524 -0
- package/src/webview/components/DatePicker.tsx +185 -0
- package/src/webview/components/FeatureCard.tsx +186 -0
- package/src/webview/components/FeatureEditor.tsx +623 -0
- package/src/webview/components/KanbanBoard.tsx +144 -0
- package/src/webview/components/KanbanColumn.tsx +159 -0
- package/src/webview/components/MarkdownEditor.tsx +291 -0
- package/src/webview/components/PrioritySelect.tsx +39 -0
- package/src/webview/components/QuickAddInput.tsx +72 -0
- package/src/webview/components/SettingsPanel.tsx +284 -0
- package/src/webview/components/Toolbar.tsx +175 -0
- package/src/webview/components/UndoToast.tsx +70 -0
- package/src/webview/index.html +12 -0
- package/src/webview/lib/utils.ts +6 -0
- package/src/webview/main.tsx +11 -0
- package/src/webview/standalone-main.tsx +13 -0
- package/src/webview/standalone-shim.ts +132 -0
- package/src/webview/standalone.html +12 -0
- package/src/webview/store/index.ts +241 -0
- package/tailwind.config.js +53 -0
- package/tsconfig.json +22 -0
- package/vite.config.ts +36 -0
- package/vite.standalone.config.ts +62 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { X, ChevronLeft, ChevronRight } from 'lucide-react'
|
|
3
|
+
import { cn } from '../lib/utils'
|
|
4
|
+
|
|
5
|
+
const DAYS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
|
|
6
|
+
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
|
|
7
|
+
|
|
8
|
+
interface DatePickerProps {
|
|
9
|
+
value: string
|
|
10
|
+
onChange: (date: string) => void
|
|
11
|
+
placeholder?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function DatePicker({ value, onChange, placeholder = 'Due date' }: DatePickerProps) {
|
|
15
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
16
|
+
const today = new Date()
|
|
17
|
+
const selected = value ? new Date(value + 'T00:00:00') : null
|
|
18
|
+
const [viewYear, setViewYear] = useState(selected?.getFullYear() ?? today.getFullYear())
|
|
19
|
+
const [viewMonth, setViewMonth] = useState(selected?.getMonth() ?? today.getMonth())
|
|
20
|
+
|
|
21
|
+
const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate()
|
|
22
|
+
// Monday = 0
|
|
23
|
+
const firstDay = (new Date(viewYear, viewMonth, 1).getDay() + 6) % 7
|
|
24
|
+
const cells: (number | null)[] = Array(firstDay).fill(null)
|
|
25
|
+
for (let d = 1; d <= daysInMonth; d++) cells.push(d)
|
|
26
|
+
|
|
27
|
+
const prevMonth = () => {
|
|
28
|
+
if (viewMonth === 0) { setViewMonth(11); setViewYear(y => y - 1) }
|
|
29
|
+
else setViewMonth(m => m - 1)
|
|
30
|
+
}
|
|
31
|
+
const nextMonth = () => {
|
|
32
|
+
if (viewMonth === 11) { setViewMonth(0); setViewYear(y => y + 1) }
|
|
33
|
+
else setViewMonth(m => m + 1)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const selectDay = (day: number) => {
|
|
37
|
+
const m = String(viewMonth + 1).padStart(2, '0')
|
|
38
|
+
const d = String(day).padStart(2, '0')
|
|
39
|
+
onChange(`${viewYear}-${m}-${d}`)
|
|
40
|
+
setIsOpen(false)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const isToday = (day: number) =>
|
|
44
|
+
day === today.getDate() && viewMonth === today.getMonth() && viewYear === today.getFullYear()
|
|
45
|
+
const isSelected = (day: number) =>
|
|
46
|
+
selected !== null && day === selected.getDate() && viewMonth === selected.getMonth() && viewYear === selected.getFullYear()
|
|
47
|
+
|
|
48
|
+
const formatDisplay = (dateStr: string) => {
|
|
49
|
+
const d = new Date(dateStr + 'T00:00:00')
|
|
50
|
+
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="relative">
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
58
|
+
className="flex items-center gap-2 px-2 py-1 text-xs font-medium rounded transition-colors"
|
|
59
|
+
style={{
|
|
60
|
+
color: value ? 'var(--vscode-foreground)' : 'var(--vscode-descriptionForeground)',
|
|
61
|
+
}}
|
|
62
|
+
onMouseEnter={e => e.currentTarget.style.background = 'var(--vscode-list-hoverBackground)'}
|
|
63
|
+
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
|
|
64
|
+
>
|
|
65
|
+
<span>{value ? formatDisplay(value) : placeholder}</span>
|
|
66
|
+
{value && (
|
|
67
|
+
<span
|
|
68
|
+
role="button"
|
|
69
|
+
onClick={(e) => { e.stopPropagation(); onChange(''); setIsOpen(false) }}
|
|
70
|
+
className="ml-0.5 hover:text-red-500 transition-colors"
|
|
71
|
+
>
|
|
72
|
+
<X size={12} />
|
|
73
|
+
</span>
|
|
74
|
+
)}
|
|
75
|
+
</button>
|
|
76
|
+
{isOpen && (
|
|
77
|
+
<>
|
|
78
|
+
<div className="fixed inset-0 z-10" onClick={() => setIsOpen(false)} />
|
|
79
|
+
<div
|
|
80
|
+
className="absolute top-full left-0 mt-1 z-20 rounded-lg shadow-lg p-3 w-[252px]"
|
|
81
|
+
style={{
|
|
82
|
+
background: 'var(--vscode-dropdown-background)',
|
|
83
|
+
border: '1px solid var(--vscode-dropdown-border, var(--vscode-panel-border))',
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
{/* Month/year nav */}
|
|
87
|
+
<div className="flex items-center justify-between mb-2">
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
onClick={prevMonth}
|
|
91
|
+
className="p-1 rounded transition-colors"
|
|
92
|
+
style={{ color: 'var(--vscode-descriptionForeground)' }}
|
|
93
|
+
onMouseEnter={e => e.currentTarget.style.background = 'var(--vscode-list-hoverBackground)'}
|
|
94
|
+
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
|
|
95
|
+
>
|
|
96
|
+
<ChevronLeft size={14} />
|
|
97
|
+
</button>
|
|
98
|
+
<span className="text-xs font-medium" style={{ color: 'var(--vscode-foreground)' }}>
|
|
99
|
+
{MONTHS[viewMonth]} {viewYear}
|
|
100
|
+
</span>
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
onClick={nextMonth}
|
|
104
|
+
className="p-1 rounded transition-colors"
|
|
105
|
+
style={{ color: 'var(--vscode-descriptionForeground)' }}
|
|
106
|
+
onMouseEnter={e => e.currentTarget.style.background = 'var(--vscode-list-hoverBackground)'}
|
|
107
|
+
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
|
|
108
|
+
>
|
|
109
|
+
<ChevronRight size={14} />
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
112
|
+
{/* Day headers */}
|
|
113
|
+
<div className="grid grid-cols-7 mb-1">
|
|
114
|
+
{DAYS.map(d => (
|
|
115
|
+
<div
|
|
116
|
+
key={d}
|
|
117
|
+
className="text-center text-[10px] font-medium py-1"
|
|
118
|
+
style={{ color: 'var(--vscode-descriptionForeground)' }}
|
|
119
|
+
>
|
|
120
|
+
{d}
|
|
121
|
+
</div>
|
|
122
|
+
))}
|
|
123
|
+
</div>
|
|
124
|
+
{/* Day grid */}
|
|
125
|
+
<div className="grid grid-cols-7">
|
|
126
|
+
{cells.map((day, i) => (
|
|
127
|
+
<div key={i} className="flex items-center justify-center">
|
|
128
|
+
{day ? (
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
onClick={() => selectDay(day)}
|
|
132
|
+
className={cn('w-7 h-7 rounded-md text-[11px] transition-colors font-medium')}
|
|
133
|
+
style={{
|
|
134
|
+
background: isSelected(day)
|
|
135
|
+
? 'var(--vscode-focusBorder)'
|
|
136
|
+
: isToday(day)
|
|
137
|
+
? 'var(--vscode-editor-selectionBackground)'
|
|
138
|
+
: undefined,
|
|
139
|
+
color: isSelected(day)
|
|
140
|
+
? 'var(--vscode-editor-background)'
|
|
141
|
+
: isToday(day)
|
|
142
|
+
? 'var(--vscode-foreground)'
|
|
143
|
+
: 'var(--vscode-foreground)',
|
|
144
|
+
}}
|
|
145
|
+
onMouseEnter={e => {
|
|
146
|
+
if (!isSelected(day)) e.currentTarget.style.background = 'var(--vscode-list-hoverBackground)'
|
|
147
|
+
}}
|
|
148
|
+
onMouseLeave={e => {
|
|
149
|
+
if (!isSelected(day) && !isToday(day)) e.currentTarget.style.background = 'transparent'
|
|
150
|
+
else if (isToday(day) && !isSelected(day)) e.currentTarget.style.background = 'var(--vscode-editor-selectionBackground)'
|
|
151
|
+
}}
|
|
152
|
+
>
|
|
153
|
+
{day}
|
|
154
|
+
</button>
|
|
155
|
+
) : <div className="w-7 h-7" />}
|
|
156
|
+
</div>
|
|
157
|
+
))}
|
|
158
|
+
</div>
|
|
159
|
+
{/* Today shortcut */}
|
|
160
|
+
<div
|
|
161
|
+
className="mt-2 pt-2 flex justify-center"
|
|
162
|
+
style={{ borderTop: '1px solid var(--vscode-panel-border)' }}
|
|
163
|
+
>
|
|
164
|
+
<button
|
|
165
|
+
type="button"
|
|
166
|
+
onClick={() => {
|
|
167
|
+
const m = String(today.getMonth() + 1).padStart(2, '0')
|
|
168
|
+
const d = String(today.getDate()).padStart(2, '0')
|
|
169
|
+
onChange(`${today.getFullYear()}-${m}-${d}`)
|
|
170
|
+
setIsOpen(false)
|
|
171
|
+
}}
|
|
172
|
+
className="text-[11px] font-medium transition-colors"
|
|
173
|
+
style={{ color: 'var(--vscode-textLink-foreground)' }}
|
|
174
|
+
onMouseEnter={e => e.currentTarget.style.color = 'var(--vscode-textLink-activeForeground)'}
|
|
175
|
+
onMouseLeave={e => e.currentTarget.style.color = 'var(--vscode-textLink-foreground)'}
|
|
176
|
+
>
|
|
177
|
+
Today
|
|
178
|
+
</button>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
</>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { marked } from 'marked'
|
|
2
|
+
import { Calendar, Check, FileText, Paperclip } from 'lucide-react'
|
|
3
|
+
import { getTitleFromContent } from '../../shared/types'
|
|
4
|
+
import type { Feature, Priority } from '../../shared/types'
|
|
5
|
+
import { useStore } from '../store'
|
|
6
|
+
|
|
7
|
+
interface FeatureCardProps {
|
|
8
|
+
feature: Feature
|
|
9
|
+
onClick: () => void
|
|
10
|
+
isDragging?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const priorityColors: Record<Priority, string> = {
|
|
14
|
+
critical: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400',
|
|
15
|
+
high: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400',
|
|
16
|
+
medium: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400',
|
|
17
|
+
low: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const priorityLabels: Record<Priority, string> = {
|
|
21
|
+
critical: 'Critical',
|
|
22
|
+
high: 'High',
|
|
23
|
+
medium: 'Med',
|
|
24
|
+
low: 'Low'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getDescriptionFromContent(content: string): string {
|
|
28
|
+
// Remove the first # heading line, then grab the remaining content
|
|
29
|
+
const lines = content.split('\n')
|
|
30
|
+
const headingIndex = lines.findIndex(l => /^#\s+/.test(l))
|
|
31
|
+
const afterHeading = headingIndex >= 0 ? lines.slice(headingIndex + 1) : lines
|
|
32
|
+
// Trim leading/trailing blank lines but preserve internal structure (lists, etc.)
|
|
33
|
+
const trimmed = afterHeading.join('\n').trim()
|
|
34
|
+
return trimmed
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function renderDescriptionHtml(text: string): string {
|
|
38
|
+
return marked.parse(text, { async: false, gfm: true, breaks: true }) as string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function FeatureCard({ feature, onClick, isDragging }: FeatureCardProps) {
|
|
42
|
+
const { cardSettings } = useStore()
|
|
43
|
+
const title = getTitleFromContent(feature.content)
|
|
44
|
+
const description = getDescriptionFromContent(feature.content)
|
|
45
|
+
const fileName = feature.filePath ? feature.filePath.split('/').pop() || '' : ''
|
|
46
|
+
|
|
47
|
+
const formatDueDate = (dateStr: string | null) => {
|
|
48
|
+
if (!dateStr) return null
|
|
49
|
+
const date = new Date(dateStr)
|
|
50
|
+
const now = new Date()
|
|
51
|
+
const diff = date.getTime() - now.getTime()
|
|
52
|
+
const days = Math.ceil(diff / (1000 * 60 * 60 * 24))
|
|
53
|
+
|
|
54
|
+
if (days < 0) return { text: 'Overdue', className: 'text-red-500' }
|
|
55
|
+
if (days === 0) return { text: 'Today', className: 'text-orange-500' }
|
|
56
|
+
if (days === 1) return { text: 'Tomorrow', className: 'text-yellow-600 dark:text-yellow-400' }
|
|
57
|
+
if (days <= 7) return { text: `${days}d`, className: 'text-zinc-500 dark:text-zinc-400' }
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
text: date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
|
|
61
|
+
className: 'text-zinc-500 dark:text-zinc-400'
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const dueInfo = feature.status === 'done' ? null : formatDueDate(feature.dueDate)
|
|
66
|
+
|
|
67
|
+
const formatCompletedAt = (dateStr: string | null) => {
|
|
68
|
+
if (!dateStr) return null
|
|
69
|
+
const completed = new Date(dateStr)
|
|
70
|
+
const now = new Date()
|
|
71
|
+
const diffMs = now.getTime() - completed.getTime()
|
|
72
|
+
const diffMins = Math.floor(diffMs / (1000 * 60))
|
|
73
|
+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
|
|
74
|
+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
|
|
75
|
+
|
|
76
|
+
if (diffMins < 1) return 'Just now'
|
|
77
|
+
if (diffMins < 60) return `${diffMins}m ago`
|
|
78
|
+
if (diffHours < 24) return `${diffHours}h ago`
|
|
79
|
+
if (diffDays === 1) return '1d ago'
|
|
80
|
+
if (diffDays < 30) return `${diffDays}d ago`
|
|
81
|
+
if (diffDays < 365) return `${Math.floor(diffDays / 30)}mo ago`
|
|
82
|
+
return `${Math.floor(diffDays / 365)}y ago`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const completedText = feature.status === 'done' ? formatCompletedAt(feature.completedAt) : null
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div
|
|
89
|
+
onClick={onClick}
|
|
90
|
+
className={`group relative flex flex-col bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 ${cardSettings.compactMode ? 'p-2 min-h-[4.5rem]' : 'p-3 min-h-[7rem]'} cursor-pointer hover:shadow-md transition-shadow ${
|
|
91
|
+
isDragging ? 'shadow-lg opacity-90' : ''
|
|
92
|
+
}`}
|
|
93
|
+
>
|
|
94
|
+
{/* Title & Content */}
|
|
95
|
+
<div className="flex-1">
|
|
96
|
+
{/* File Name + Priority badge row (when fileName enabled) */}
|
|
97
|
+
{cardSettings.showFileName && fileName && (
|
|
98
|
+
<div className="flex items-center gap-1.5 mb-1">
|
|
99
|
+
<FileText size={10} className="shrink-0 text-zinc-400 dark:text-zinc-500" />
|
|
100
|
+
<span className="text-[10px] font-mono text-zinc-400 dark:text-zinc-500 truncate flex-1">
|
|
101
|
+
{fileName}
|
|
102
|
+
</span>
|
|
103
|
+
{cardSettings.showPriorityBadges && (
|
|
104
|
+
<span
|
|
105
|
+
className={`text-[10px] font-medium px-1.5 py-0.5 rounded shrink-0 ${priorityColors[feature.priority]}`}
|
|
106
|
+
>
|
|
107
|
+
{priorityLabels[feature.priority]}
|
|
108
|
+
</span>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
<div className={`flex items-start gap-2 ${description ? 'mb-1' : cardSettings.compactMode ? 'mb-1' : 'mb-2'}`}>
|
|
114
|
+
<h3 className="text-sm font-medium text-zinc-900 dark:text-zinc-100 line-clamp-2 flex-1">
|
|
115
|
+
{title}
|
|
116
|
+
</h3>
|
|
117
|
+
{cardSettings.showPriorityBadges && !(cardSettings.showFileName && fileName) && (
|
|
118
|
+
<span
|
|
119
|
+
className={`text-xs font-medium px-1.5 py-0.5 rounded shrink-0 ${priorityColors[feature.priority]}`}
|
|
120
|
+
>
|
|
121
|
+
{priorityLabels[feature.priority]}
|
|
122
|
+
</span>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{/* Description */}
|
|
127
|
+
{/* eslint-disable-next-line react/no-danger */}
|
|
128
|
+
{description && !cardSettings.compactMode && (
|
|
129
|
+
<div
|
|
130
|
+
className="text-xs text-zinc-500 dark:text-zinc-400 line-clamp-2 mb-2 card-inline-markdown"
|
|
131
|
+
dangerouslySetInnerHTML={{ __html: renderDescriptionHtml(description) }}
|
|
132
|
+
/>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
{/* Labels */}
|
|
136
|
+
{cardSettings.showLabels && feature.labels.length > 0 && (
|
|
137
|
+
<div className="flex flex-wrap gap-1 mb-2">
|
|
138
|
+
{feature.labels.slice(0, 3).map((label) => (
|
|
139
|
+
<span
|
|
140
|
+
key={label}
|
|
141
|
+
className="text-xs px-1.5 py-0.5 rounded bg-zinc-100 text-zinc-600 dark:bg-zinc-700 dark:text-zinc-300"
|
|
142
|
+
>
|
|
143
|
+
{label}
|
|
144
|
+
</span>
|
|
145
|
+
))}
|
|
146
|
+
{feature.labels.length > 3 && (
|
|
147
|
+
<span className="text-xs text-zinc-400">+{feature.labels.length - 3}</span>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
{/* Footer */}
|
|
154
|
+
<div className="flex items-center justify-between text-xs mt-auto">
|
|
155
|
+
<div className="flex items-center gap-1">
|
|
156
|
+
{cardSettings.showAssignee && feature.assignee && feature.assignee !== 'null' && (
|
|
157
|
+
<div className="flex items-center gap-1.5 text-zinc-500 dark:text-zinc-400">
|
|
158
|
+
<span className="shrink-0 w-4 h-4 rounded-full flex items-center justify-center text-[8px] font-bold bg-zinc-200 dark:bg-zinc-600 text-zinc-700 dark:text-zinc-300">
|
|
159
|
+
{feature.assignee.split(/\s+/).map((w: string) => w[0]).join('').toUpperCase().slice(0, 2)}
|
|
160
|
+
</span>
|
|
161
|
+
<span>{feature.assignee}</span>
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
{feature.attachments.length > 0 && (
|
|
166
|
+
<div className="flex items-center gap-1 text-zinc-400 dark:text-zinc-500">
|
|
167
|
+
<Paperclip size={12} />
|
|
168
|
+
<span>{feature.attachments.length}</span>
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
171
|
+
{cardSettings.showDueDate && dueInfo && (
|
|
172
|
+
<div className={`flex items-center gap-1 ${dueInfo.className}`}>
|
|
173
|
+
<Calendar size={12} />
|
|
174
|
+
<span>{dueInfo.text}</span>
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
{completedText && (
|
|
178
|
+
<div className="flex items-center gap-1" style={{ color: 'var(--vscode-descriptionForeground)' }}>
|
|
179
|
+
<Check size={12} />
|
|
180
|
+
<span>{completedText}</span>
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
)
|
|
186
|
+
}
|