@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.
@@ -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;
@@ -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 !important;
53
- box-shadow: none !important;
54
- background-color: var(--color-surface-elevated) !important;
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 !important;
59
- padding: 0 !important;
60
- font-size: 13px !important;
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.0",
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": "bun src/index.ts"
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
- ".": "./src/index.ts"
28
+ ".": "./dist/server.js"
26
29
  },
30
+ "main": "./dist/server.js",
27
31
  "files": [
28
- "src",
29
- "bunfig.toml"
32
+ "dist"
30
33
  ],
34
+ "engines": {
35
+ "node": ">=18.0.0"
36
+ },
31
37
  "dependencies": {
32
- "@miketromba/issy-core": "^0.1.0",
38
+ "@miketromba/issy-core": "^0.1.4"
39
+ },
40
+ "devDependencies": {
33
41
  "@tailwindcss/typography": "^0.5.16",
34
- "bun-plugin-tailwind": "^0.1.2",
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.1.11"
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
@@ -1,2 +0,0 @@
1
- [serve.static]
2
- plugins = ["bun-plugin-tailwind"]
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
- }
@@ -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
- }