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