@miketromba/issy-app 0.1.0 → 0.1.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/dist/app.js +304 -0
- package/dist/index.html +13 -0
- package/dist/server.js +1899 -0
- package/{src/index.css → dist/styles.css} +7 -8
- package/package.json +19 -13
- package/bunfig.toml +0 -2
- package/src/App.tsx +0 -332
- package/src/components/Badge.tsx +0 -49
- package/src/components/ConfirmModal.tsx +0 -71
- package/src/components/CreateIssueModal.tsx +0 -176
- package/src/components/EditIssueModal.tsx +0 -183
- package/src/components/FilterBar.tsx +0 -182
- package/src/components/IssueDetail.tsx +0 -148
- package/src/components/IssueList.tsx +0 -75
- package/src/components/QueryHelpModal.tsx +0 -160
- package/src/frontend.tsx +0 -18
- package/src/index.html +0 -12
- package/src/index.ts +0 -164
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
@import "tailwindcss";
|
|
2
|
-
@plugin "@tailwindcss/typography";
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
:root {
|
|
5
4
|
/* Core palette - dark theme */
|
|
6
5
|
--color-background: #0a0b0f;
|
|
7
6
|
--color-surface: #12141a;
|
|
@@ -49,15 +48,15 @@
|
|
|
49
48
|
|
|
50
49
|
/* Code block styling - remove borders, unify background, add dark scrollbar */
|
|
51
50
|
.prose pre {
|
|
52
|
-
border: none
|
|
53
|
-
box-shadow: none
|
|
54
|
-
background-color: var(--color-surface-elevated)
|
|
51
|
+
border: none;
|
|
52
|
+
box-shadow: none;
|
|
53
|
+
background-color: var(--color-surface-elevated);
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
.prose pre code.hljs {
|
|
58
|
-
background: transparent
|
|
59
|
-
padding: 0
|
|
60
|
-
font-size: 13px
|
|
57
|
+
background: transparent;
|
|
58
|
+
padding: 0;
|
|
59
|
+
font-size: 13px;
|
|
61
60
|
}
|
|
62
61
|
|
|
63
62
|
.prose pre::-webkit-scrollbar {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miketromba/issy-app",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Local web UI and API server for issy",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,28 +19,34 @@
|
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
21
|
"dev": "bun --hot src/index.ts",
|
|
22
|
-
"start": "
|
|
22
|
+
"start": "node dist/server.js",
|
|
23
|
+
"build": "bun scripts/build.js",
|
|
24
|
+
"prepublishOnly": "bun run build",
|
|
25
|
+
"lint": "biome check src"
|
|
23
26
|
},
|
|
24
27
|
"exports": {
|
|
25
|
-
".": "./
|
|
28
|
+
".": "./dist/server.js"
|
|
26
29
|
},
|
|
30
|
+
"main": "./dist/server.js",
|
|
27
31
|
"files": [
|
|
28
|
-
"
|
|
29
|
-
"bunfig.toml"
|
|
32
|
+
"dist"
|
|
30
33
|
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18.0.0"
|
|
36
|
+
},
|
|
31
37
|
"dependencies": {
|
|
32
|
-
"@miketromba/issy-core": "^0.1.
|
|
38
|
+
"@miketromba/issy-core": "^0.1.4"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
33
41
|
"@tailwindcss/typography": "^0.5.16",
|
|
34
|
-
"
|
|
42
|
+
"@types/react": "^19",
|
|
43
|
+
"@types/react-dom": "^19",
|
|
44
|
+
"@types/bun": "latest",
|
|
45
|
+
"@types/node": "^22",
|
|
35
46
|
"highlight.js": "^11.11.1",
|
|
36
47
|
"marked": "^17.0.1",
|
|
37
48
|
"react": "^19",
|
|
38
49
|
"react-dom": "^19",
|
|
39
|
-
"tailwindcss": "^4.
|
|
40
|
-
},
|
|
41
|
-
"devDependencies": {
|
|
42
|
-
"@types/react": "^19",
|
|
43
|
-
"@types/react-dom": "^19",
|
|
44
|
-
"@types/bun": "latest"
|
|
50
|
+
"tailwindcss": "^3.4.0"
|
|
45
51
|
}
|
|
46
52
|
}
|
package/bunfig.toml
DELETED
package/src/App.tsx
DELETED
|
@@ -1,332 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect, useCallback, useMemo } from 'react'
|
|
2
|
-
import { IssueList } from './components/IssueList'
|
|
3
|
-
import { IssueDetail } from './components/IssueDetail'
|
|
4
|
-
import { QueryHelpModal } from './components/QueryHelpModal'
|
|
5
|
-
import { FilterBar } from './components/FilterBar'
|
|
6
|
-
import { CreateIssueModal } from './components/CreateIssueModal'
|
|
7
|
-
import { EditIssueModal } from './components/EditIssueModal'
|
|
8
|
-
import { ConfirmModal } from './components/ConfirmModal'
|
|
9
|
-
import type { Issue, IssueFrontmatter } from '@miketromba/issy-core'
|
|
10
|
-
import { filterByQuery } from '@miketromba/issy-core'
|
|
11
|
-
|
|
12
|
-
// Re-export types for components
|
|
13
|
-
export type { Issue, IssueFrontmatter }
|
|
14
|
-
|
|
15
|
-
interface FilterState {
|
|
16
|
-
query: string
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const STORAGE_KEY = 'issy-state'
|
|
20
|
-
|
|
21
|
-
function loadState(): { filters: FilterState; selectedIssueId: string | null } {
|
|
22
|
-
try {
|
|
23
|
-
const stored = localStorage.getItem(STORAGE_KEY)
|
|
24
|
-
if (stored) {
|
|
25
|
-
const parsed = JSON.parse(stored)
|
|
26
|
-
// Migrate old filter format to new query format
|
|
27
|
-
if (parsed.filters && !parsed.filters.query) {
|
|
28
|
-
const oldFilters = parsed.filters
|
|
29
|
-
const queryParts: string[] = []
|
|
30
|
-
if (oldFilters.status)
|
|
31
|
-
queryParts.push(`is:${oldFilters.status}`)
|
|
32
|
-
if (oldFilters.priority)
|
|
33
|
-
queryParts.push(`priority:${oldFilters.priority}`)
|
|
34
|
-
if (oldFilters.type) queryParts.push(`type:${oldFilters.type}`)
|
|
35
|
-
if (oldFilters.search) queryParts.push(oldFilters.search)
|
|
36
|
-
return {
|
|
37
|
-
filters: { query: queryParts.join(' ') || 'is:open' },
|
|
38
|
-
selectedIssueId: parsed.selectedIssueId || null
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return parsed
|
|
42
|
-
}
|
|
43
|
-
} catch (e) {
|
|
44
|
-
console.error('Failed to load state from localStorage:', e)
|
|
45
|
-
}
|
|
46
|
-
return {
|
|
47
|
-
filters: { query: 'is:open' }, // Default to showing open issues
|
|
48
|
-
selectedIssueId: null
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function saveState(filters: FilterState, selectedIssueId: string | null) {
|
|
53
|
-
try {
|
|
54
|
-
localStorage.setItem(
|
|
55
|
-
STORAGE_KEY,
|
|
56
|
-
JSON.stringify({ filters, selectedIssueId })
|
|
57
|
-
)
|
|
58
|
-
} catch (e) {
|
|
59
|
-
console.error('Failed to save state to localStorage:', e)
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function App() {
|
|
64
|
-
const [issues, setIssues] = useState<Issue[]>([])
|
|
65
|
-
const [loading, setLoading] = useState(true)
|
|
66
|
-
const [error, setError] = useState<string | null>(null)
|
|
67
|
-
|
|
68
|
-
const initialState = loadState()
|
|
69
|
-
const [filters, setFilters] = useState<FilterState>(initialState.filters)
|
|
70
|
-
const [selectedIssueId, setSelectedIssueId] = useState<string | null>(
|
|
71
|
-
initialState.selectedIssueId
|
|
72
|
-
)
|
|
73
|
-
const [showHelp, setShowHelp] = useState(false)
|
|
74
|
-
const [showCreate, setShowCreate] = useState(false)
|
|
75
|
-
const [showEdit, setShowEdit] = useState(false)
|
|
76
|
-
const [showCloseConfirm, setShowCloseConfirm] = useState(false)
|
|
77
|
-
const [showReopenConfirm, setShowReopenConfirm] = useState(false)
|
|
78
|
-
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
|
79
|
-
const [isDeleting, setIsDeleting] = useState(false)
|
|
80
|
-
const [actionLoading, setActionLoading] = useState(false)
|
|
81
|
-
|
|
82
|
-
// Fetch issues function - reusable for refresh
|
|
83
|
-
const fetchIssues = useCallback(async (showLoading = false) => {
|
|
84
|
-
try {
|
|
85
|
-
if (showLoading) setLoading(true)
|
|
86
|
-
const response = await fetch('/api/issues')
|
|
87
|
-
if (!response.ok) throw new Error('Failed to fetch issues')
|
|
88
|
-
const data = await response.json()
|
|
89
|
-
setIssues(data)
|
|
90
|
-
setError(null)
|
|
91
|
-
} catch (e) {
|
|
92
|
-
setError(e instanceof Error ? e.message : 'Unknown error')
|
|
93
|
-
} finally {
|
|
94
|
-
setLoading(false)
|
|
95
|
-
}
|
|
96
|
-
}, [])
|
|
97
|
-
|
|
98
|
-
// Initial fetch
|
|
99
|
-
useEffect(() => {
|
|
100
|
-
fetchIssues(true)
|
|
101
|
-
}, [fetchIssues])
|
|
102
|
-
|
|
103
|
-
// Auto-refresh when window regains focus or tab becomes visible
|
|
104
|
-
useEffect(() => {
|
|
105
|
-
const handleVisibilityChange = () => {
|
|
106
|
-
if (document.visibilityState === 'visible') {
|
|
107
|
-
fetchIssues()
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const handleFocus = () => {
|
|
112
|
-
fetchIssues()
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
document.addEventListener('visibilitychange', handleVisibilityChange)
|
|
116
|
-
window.addEventListener('focus', handleFocus)
|
|
117
|
-
|
|
118
|
-
return () => {
|
|
119
|
-
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
|
120
|
-
window.removeEventListener('focus', handleFocus)
|
|
121
|
-
}
|
|
122
|
-
}, [fetchIssues])
|
|
123
|
-
|
|
124
|
-
useEffect(() => {
|
|
125
|
-
saveState(filters, selectedIssueId)
|
|
126
|
-
}, [filters, selectedIssueId])
|
|
127
|
-
|
|
128
|
-
const handleSelectIssue = useCallback((id: string | null) => {
|
|
129
|
-
setSelectedIssueId(id)
|
|
130
|
-
}, [])
|
|
131
|
-
|
|
132
|
-
const handleCloseIssue = useCallback(async () => {
|
|
133
|
-
if (!selectedIssueId || actionLoading) return
|
|
134
|
-
setActionLoading(true)
|
|
135
|
-
try {
|
|
136
|
-
const response = await fetch(`/api/issues/${selectedIssueId}/close`, { method: 'POST' })
|
|
137
|
-
if (!response.ok) throw new Error('Failed to close issue')
|
|
138
|
-
setShowCloseConfirm(false)
|
|
139
|
-
await fetchIssues()
|
|
140
|
-
} catch (e) {
|
|
141
|
-
console.error('Failed to close issue:', e)
|
|
142
|
-
} finally {
|
|
143
|
-
setActionLoading(false)
|
|
144
|
-
}
|
|
145
|
-
}, [selectedIssueId, actionLoading, fetchIssues])
|
|
146
|
-
|
|
147
|
-
const handleReopenIssue = useCallback(async () => {
|
|
148
|
-
if (!selectedIssueId || actionLoading) return
|
|
149
|
-
setActionLoading(true)
|
|
150
|
-
try {
|
|
151
|
-
const response = await fetch(`/api/issues/${selectedIssueId}/reopen`, { method: 'POST' })
|
|
152
|
-
if (!response.ok) throw new Error('Failed to reopen issue')
|
|
153
|
-
setShowReopenConfirm(false)
|
|
154
|
-
await fetchIssues()
|
|
155
|
-
} catch (e) {
|
|
156
|
-
console.error('Failed to reopen issue:', e)
|
|
157
|
-
} finally {
|
|
158
|
-
setActionLoading(false)
|
|
159
|
-
}
|
|
160
|
-
}, [selectedIssueId, actionLoading, fetchIssues])
|
|
161
|
-
|
|
162
|
-
const handleDeleteIssue = useCallback(async () => {
|
|
163
|
-
if (!selectedIssueId || isDeleting) return
|
|
164
|
-
setIsDeleting(true)
|
|
165
|
-
try {
|
|
166
|
-
const response = await fetch(`/api/issues/${selectedIssueId}/delete`, { method: 'DELETE' })
|
|
167
|
-
if (!response.ok) throw new Error('Failed to delete issue')
|
|
168
|
-
setShowDeleteConfirm(false)
|
|
169
|
-
setSelectedIssueId(null)
|
|
170
|
-
await fetchIssues()
|
|
171
|
-
} catch (e) {
|
|
172
|
-
console.error('Failed to delete issue:', e)
|
|
173
|
-
} finally {
|
|
174
|
-
setIsDeleting(false)
|
|
175
|
-
}
|
|
176
|
-
}, [selectedIssueId, isDeleting, fetchIssues])
|
|
177
|
-
|
|
178
|
-
const filteredIssues = useMemo(() => {
|
|
179
|
-
return filterByQuery(issues, filters.query)
|
|
180
|
-
}, [issues, filters.query])
|
|
181
|
-
|
|
182
|
-
const selectedIssue = selectedIssueId
|
|
183
|
-
? issues.find(i => i.id === selectedIssueId) || null
|
|
184
|
-
: null
|
|
185
|
-
|
|
186
|
-
if (loading) {
|
|
187
|
-
return (
|
|
188
|
-
<div className="flex items-center justify-center h-screen bg-background text-text-muted">
|
|
189
|
-
Loading issues...
|
|
190
|
-
</div>
|
|
191
|
-
)
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (error) {
|
|
195
|
-
return (
|
|
196
|
-
<div className="flex items-center justify-center h-screen bg-background text-red-400">
|
|
197
|
-
Error: {error}
|
|
198
|
-
</div>
|
|
199
|
-
)
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return (
|
|
203
|
-
<div className="flex h-screen bg-background text-text-primary font-sans text-sm leading-relaxed">
|
|
204
|
-
{/* Sidebar - hidden on mobile when issue is selected */}
|
|
205
|
-
<aside className={`w-full md:w-[380px] md:min-w-[380px] border-r border-border flex flex-col h-screen bg-background ${
|
|
206
|
-
selectedIssue ? 'hidden md:flex' : 'flex'
|
|
207
|
-
}`}>
|
|
208
|
-
<div className="p-4 md:p-5 border-b border-border">
|
|
209
|
-
<div className="flex items-center justify-between mb-4">
|
|
210
|
-
<h1 className="text-lg font-semibold text-text-primary">
|
|
211
|
-
issy
|
|
212
|
-
</h1>
|
|
213
|
-
<button
|
|
214
|
-
onClick={() => setShowCreate(true)}
|
|
215
|
-
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-text-secondary hover:text-text-primary bg-surface hover:bg-surface-elevated border border-border rounded-lg transition-colors"
|
|
216
|
-
>
|
|
217
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
218
|
-
<path d="M12 5v14M5 12h14" />
|
|
219
|
-
</svg>
|
|
220
|
-
New
|
|
221
|
-
</button>
|
|
222
|
-
</div>
|
|
223
|
-
|
|
224
|
-
<div className="flex items-center bg-surface border border-border rounded-lg focus-within:border-accent transition-colors">
|
|
225
|
-
<input
|
|
226
|
-
type="text"
|
|
227
|
-
placeholder="Filter: is:open priority:high..."
|
|
228
|
-
value={filters.query}
|
|
229
|
-
onChange={e => setFilters({ query: e.target.value })}
|
|
230
|
-
className="flex-1 px-3.5 py-2.5 bg-transparent text-text-primary text-sm placeholder:text-text-muted focus:outline-none"
|
|
231
|
-
/>
|
|
232
|
-
<button
|
|
233
|
-
onClick={() => setShowHelp(true)}
|
|
234
|
-
aria-label="Query syntax help"
|
|
235
|
-
title="Query syntax help"
|
|
236
|
-
className="w-10 h-[38px] shrink-0 flex items-center justify-center border-l border-border rounded-r-lg text-text-muted text-sm font-medium cursor-pointer transition-colors hover:bg-surface-elevated hover:text-text-primary"
|
|
237
|
-
>
|
|
238
|
-
?
|
|
239
|
-
</button>
|
|
240
|
-
</div>
|
|
241
|
-
|
|
242
|
-
<div className="mt-3">
|
|
243
|
-
<FilterBar
|
|
244
|
-
query={filters.query}
|
|
245
|
-
onQueryChange={(query) => setFilters({ query })}
|
|
246
|
-
issues={issues}
|
|
247
|
-
/>
|
|
248
|
-
</div>
|
|
249
|
-
</div>
|
|
250
|
-
|
|
251
|
-
<div className="flex-1 overflow-y-auto custom-scrollbar">
|
|
252
|
-
<IssueList
|
|
253
|
-
issues={filteredIssues}
|
|
254
|
-
selectedId={selectedIssueId}
|
|
255
|
-
onSelect={handleSelectIssue}
|
|
256
|
-
/>
|
|
257
|
-
</div>
|
|
258
|
-
|
|
259
|
-
<div className="px-4 md:px-5 py-3 border-t border-border text-xs text-text-muted">
|
|
260
|
-
{filteredIssues.length} of {issues.length} issues
|
|
261
|
-
</div>
|
|
262
|
-
</aside>
|
|
263
|
-
|
|
264
|
-
{/* Main content - full width on mobile when issue selected, hidden when no issue on mobile */}
|
|
265
|
-
<main className={`flex-1 h-screen overflow-y-auto bg-background custom-scrollbar ${
|
|
266
|
-
selectedIssue ? 'block' : 'hidden md:block'
|
|
267
|
-
}`}>
|
|
268
|
-
{selectedIssue ? (
|
|
269
|
-
<IssueDetail
|
|
270
|
-
issue={selectedIssue}
|
|
271
|
-
onBack={() => handleSelectIssue(null)}
|
|
272
|
-
onEdit={() => setShowEdit(true)}
|
|
273
|
-
onClose={() => setShowCloseConfirm(true)}
|
|
274
|
-
onReopen={() => setShowReopenConfirm(true)}
|
|
275
|
-
onDelete={() => setShowDeleteConfirm(true)}
|
|
276
|
-
/>
|
|
277
|
-
) : (
|
|
278
|
-
<div className="flex items-center justify-center h-full text-text-muted text-[15px]">
|
|
279
|
-
Select an issue to view details
|
|
280
|
-
</div>
|
|
281
|
-
)}
|
|
282
|
-
</main>
|
|
283
|
-
|
|
284
|
-
{/* Modals */}
|
|
285
|
-
<QueryHelpModal isOpen={showHelp} onClose={() => setShowHelp(false)} />
|
|
286
|
-
|
|
287
|
-
<CreateIssueModal
|
|
288
|
-
isOpen={showCreate}
|
|
289
|
-
onClose={() => setShowCreate(false)}
|
|
290
|
-
onCreated={fetchIssues}
|
|
291
|
-
/>
|
|
292
|
-
|
|
293
|
-
<EditIssueModal
|
|
294
|
-
issue={selectedIssue}
|
|
295
|
-
isOpen={showEdit}
|
|
296
|
-
onClose={() => setShowEdit(false)}
|
|
297
|
-
onUpdated={fetchIssues}
|
|
298
|
-
/>
|
|
299
|
-
|
|
300
|
-
<ConfirmModal
|
|
301
|
-
isOpen={showCloseConfirm}
|
|
302
|
-
title="Close Issue"
|
|
303
|
-
message={`Are you sure you want to close issue #${selectedIssue?.id}?`}
|
|
304
|
-
confirmText="Close Issue"
|
|
305
|
-
onConfirm={handleCloseIssue}
|
|
306
|
-
onCancel={() => setShowCloseConfirm(false)}
|
|
307
|
-
isLoading={actionLoading}
|
|
308
|
-
/>
|
|
309
|
-
|
|
310
|
-
<ConfirmModal
|
|
311
|
-
isOpen={showReopenConfirm}
|
|
312
|
-
title="Reopen Issue"
|
|
313
|
-
message={`Are you sure you want to reopen issue #${selectedIssue?.id}?`}
|
|
314
|
-
confirmText="Reopen Issue"
|
|
315
|
-
onConfirm={handleReopenIssue}
|
|
316
|
-
onCancel={() => setShowReopenConfirm(false)}
|
|
317
|
-
isLoading={actionLoading}
|
|
318
|
-
/>
|
|
319
|
-
|
|
320
|
-
<ConfirmModal
|
|
321
|
-
isOpen={showDeleteConfirm}
|
|
322
|
-
title="Delete Issue"
|
|
323
|
-
message={`Are you sure you want to permanently delete issue #${selectedIssue?.id}? This action cannot be undone.`}
|
|
324
|
-
confirmText="Delete"
|
|
325
|
-
confirmVariant="danger"
|
|
326
|
-
onConfirm={handleDeleteIssue}
|
|
327
|
-
onCancel={() => setShowDeleteConfirm(false)}
|
|
328
|
-
isLoading={isDeleting}
|
|
329
|
-
/>
|
|
330
|
-
</div>
|
|
331
|
-
)
|
|
332
|
-
}
|
package/src/components/Badge.tsx
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
interface BadgeProps {
|
|
2
|
-
variant: 'status' | 'priority' | 'type' | 'label';
|
|
3
|
-
value: string;
|
|
4
|
-
className?: string;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
const statusStyles: Record<string, string> = {
|
|
8
|
-
open: "bg-green-500/15 text-green-500",
|
|
9
|
-
closed: "bg-gray-500/15 text-gray-400",
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
const priorityStyles: Record<string, string> = {
|
|
13
|
-
high: "bg-priority-high/15 text-red-400",
|
|
14
|
-
medium: "bg-priority-medium/15 text-amber-400",
|
|
15
|
-
low: "bg-priority-low/15 text-green-400",
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const typeStyles: Record<string, string> = {
|
|
19
|
-
bug: "bg-red-500/15 text-red-400",
|
|
20
|
-
feature: "bg-blue-500/15 text-blue-400",
|
|
21
|
-
enhancement: "bg-purple-500/15 text-purple-400",
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export function Badge({ variant, value, className = '' }: BadgeProps) {
|
|
25
|
-
const normalizedValue = value.toLowerCase();
|
|
26
|
-
|
|
27
|
-
let style = '';
|
|
28
|
-
|
|
29
|
-
switch (variant) {
|
|
30
|
-
case 'status':
|
|
31
|
-
style = statusStyles[normalizedValue] || statusStyles.open;
|
|
32
|
-
break;
|
|
33
|
-
case 'priority':
|
|
34
|
-
style = priorityStyles[normalizedValue] || '';
|
|
35
|
-
break;
|
|
36
|
-
case 'type':
|
|
37
|
-
style = typeStyles[normalizedValue] || "bg-purple-500/15 text-purple-400";
|
|
38
|
-
break;
|
|
39
|
-
case 'label':
|
|
40
|
-
style = "bg-surface-elevated text-text-secondary";
|
|
41
|
-
break;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<span className={`inline-block px-2 py-0.5 rounded text-[11px] font-medium ${style} ${className}`}>
|
|
46
|
-
{normalizedValue}
|
|
47
|
-
</span>
|
|
48
|
-
);
|
|
49
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
|
|
3
|
-
interface ConfirmModalProps {
|
|
4
|
-
isOpen: boolean
|
|
5
|
-
title: string
|
|
6
|
-
message: string
|
|
7
|
-
confirmText?: string
|
|
8
|
-
confirmVariant?: 'danger' | 'default'
|
|
9
|
-
onConfirm: () => void
|
|
10
|
-
onCancel: () => void
|
|
11
|
-
isLoading?: boolean
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function ConfirmModal({
|
|
15
|
-
isOpen,
|
|
16
|
-
title,
|
|
17
|
-
message,
|
|
18
|
-
confirmText = 'Confirm',
|
|
19
|
-
confirmVariant = 'default',
|
|
20
|
-
onConfirm,
|
|
21
|
-
onCancel,
|
|
22
|
-
isLoading = false,
|
|
23
|
-
}: ConfirmModalProps) {
|
|
24
|
-
if (!isOpen) return null
|
|
25
|
-
|
|
26
|
-
const handleBackdropClick = (e: React.MouseEvent) => {
|
|
27
|
-
if (e.target === e.currentTarget && !isLoading) {
|
|
28
|
-
onCancel()
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const confirmStyles = confirmVariant === 'danger'
|
|
33
|
-
? 'bg-red-500 hover:bg-red-600 text-white'
|
|
34
|
-
: 'bg-accent hover:bg-accent-hover text-white'
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<div
|
|
38
|
-
className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4"
|
|
39
|
-
onClick={handleBackdropClick}
|
|
40
|
-
>
|
|
41
|
-
<div className="bg-surface-elevated border border-border rounded-xl w-full max-w-sm shadow-2xl">
|
|
42
|
-
<div className="px-5 py-4 border-b border-border">
|
|
43
|
-
<h2 className="text-lg font-semibold text-text-primary">{title}</h2>
|
|
44
|
-
</div>
|
|
45
|
-
|
|
46
|
-
<div className="p-5">
|
|
47
|
-
<p className="text-sm text-text-secondary mb-6">{message}</p>
|
|
48
|
-
|
|
49
|
-
<div className="flex justify-end gap-3">
|
|
50
|
-
<button
|
|
51
|
-
type="button"
|
|
52
|
-
onClick={onCancel}
|
|
53
|
-
disabled={isLoading}
|
|
54
|
-
className="px-4 py-2 text-sm text-text-secondary hover:text-text-primary transition-colors disabled:opacity-50"
|
|
55
|
-
>
|
|
56
|
-
Cancel
|
|
57
|
-
</button>
|
|
58
|
-
<button
|
|
59
|
-
type="button"
|
|
60
|
-
onClick={onConfirm}
|
|
61
|
-
disabled={isLoading}
|
|
62
|
-
className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors disabled:opacity-50 ${confirmStyles}`}
|
|
63
|
-
>
|
|
64
|
-
{isLoading ? 'Processing...' : confirmText}
|
|
65
|
-
</button>
|
|
66
|
-
</div>
|
|
67
|
-
</div>
|
|
68
|
-
</div>
|
|
69
|
-
</div>
|
|
70
|
-
)
|
|
71
|
-
}
|