@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,184 +0,0 @@
1
- import hljs from 'highlight.js'
2
- import { marked, Renderer } from 'marked'
3
- import 'highlight.js/styles/github-dark.css'
4
- import { formatDisplayDate, formatFullDate } from '@miketromba/issy-core'
5
- import type { Issue } from '../App'
6
- import { Badge } from './Badge'
7
-
8
- interface IssueDetailProps {
9
- issue: Issue
10
- onBack?: () => void
11
- onEdit?: () => void
12
- onClose?: () => void
13
- onReopen?: () => void
14
- onDelete?: () => void
15
- }
16
-
17
- // Custom renderer with syntax highlighting
18
- const renderer = new Renderer()
19
- renderer.code = ({ text, lang }) => {
20
- const language = lang && hljs.getLanguage(lang) ? lang : 'plaintext'
21
- const highlighted = hljs.highlight(text, { language }).value
22
- return `<pre><code class="hljs language-${language}">${highlighted}</code></pre>`
23
- }
24
-
25
- marked.setOptions({
26
- gfm: true,
27
- breaks: true,
28
- renderer,
29
- })
30
-
31
- export function IssueDetail({
32
- issue,
33
- onBack,
34
- onEdit,
35
- onClose,
36
- onReopen,
37
- onDelete,
38
- }: IssueDetailProps) {
39
- const labels =
40
- issue.frontmatter.labels
41
- ?.split(',')
42
- .map((l) => l.trim())
43
- .filter(Boolean) || []
44
- const isOpen = issue.frontmatter.status === 'open'
45
-
46
- return (
47
- <div className="max-w-[800px] mx-auto px-4 md:px-10 py-6 md:py-8">
48
- {/* Mobile back button */}
49
- {onBack && (
50
- <button
51
- onClick={onBack}
52
- className="md:hidden inline-flex items-center gap-1.5 mb-4 text-text-secondary text-sm"
53
- >
54
- <svg
55
- width="16"
56
- height="16"
57
- viewBox="0 0 24 24"
58
- fill="none"
59
- stroke="currentColor"
60
- strokeWidth="2"
61
- >
62
- <path d="M15 19l-7-7 7-7" />
63
- </svg>
64
- Back to issues
65
- </button>
66
- )}
67
-
68
- <div className="mb-6">
69
- <div className="flex items-start justify-between gap-3 mb-3">
70
- <h1 className="text-xl md:text-2xl font-semibold text-text-primary leading-tight">
71
- {issue.frontmatter.title || 'Untitled Issue'}
72
- <span className="font-normal text-text-muted ml-2">
73
- #{issue.id}
74
- </span>
75
- </h1>
76
-
77
- {/* Action buttons */}
78
- <div className="flex items-center gap-1 shrink-0">
79
- {onEdit && (
80
- <button
81
- onClick={onEdit}
82
- title="Edit issue"
83
- className="p-2 text-text-muted hover:text-text-primary hover:bg-surface rounded-lg transition-colors"
84
- >
85
- <svg
86
- width="16"
87
- height="16"
88
- viewBox="0 0 24 24"
89
- fill="none"
90
- stroke="currentColor"
91
- strokeWidth="2"
92
- >
93
- <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
94
- <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
95
- </svg>
96
- </button>
97
- )}
98
-
99
- {onDelete && (
100
- <button
101
- onClick={onDelete}
102
- title="Delete issue"
103
- className="p-2 text-text-muted hover:text-red-400 hover:bg-red-500/10 rounded-lg transition-colors"
104
- >
105
- <svg
106
- width="16"
107
- height="16"
108
- viewBox="0 0 24 24"
109
- fill="none"
110
- stroke="currentColor"
111
- strokeWidth="2"
112
- >
113
- <path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
114
- </svg>
115
- </button>
116
- )}
117
-
118
- {isOpen && onClose && (
119
- <button
120
- onClick={onClose}
121
- title="Close issue"
122
- className="ml-1 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"
123
- >
124
- Close
125
- </button>
126
- )}
127
-
128
- {!isOpen && onReopen && (
129
- <button
130
- onClick={onReopen}
131
- title="Reopen issue"
132
- className="ml-1 px-3 py-1.5 text-xs font-medium text-green-400 hover:text-green-300 bg-green-500/10 hover:bg-green-500/20 border border-green-500/30 rounded-lg transition-colors"
133
- >
134
- Reopen
135
- </button>
136
- )}
137
- </div>
138
- </div>
139
-
140
- {issue.frontmatter.description && (
141
- <p className="text-[15px] text-text-secondary leading-relaxed mb-4">
142
- {issue.frontmatter.description}
143
- </p>
144
- )}
145
-
146
- <div className="flex flex-wrap items-center gap-2 text-sm">
147
- {issue.frontmatter.priority && (
148
- <Badge variant="priority" value={issue.frontmatter.priority} />
149
- )}
150
-
151
- {issue.frontmatter.status && (
152
- <Badge variant="status" value={issue.frontmatter.status} />
153
- )}
154
-
155
- {issue.frontmatter.type && (
156
- <Badge variant="type" value={issue.frontmatter.type} />
157
- )}
158
-
159
- {labels.map((label) => (
160
- <Badge key={label} variant="label" value={label} />
161
- ))}
162
-
163
- {issue.frontmatter.created && (
164
- <span
165
- className="text-xs text-text-muted"
166
- title={formatFullDate(issue.frontmatter.created)}
167
- >
168
- {formatDisplayDate(issue.frontmatter.created)}
169
- </span>
170
- )}
171
- </div>
172
- </div>
173
-
174
- <hr className="border-0 border-t border-border my-6" />
175
-
176
- <div
177
- className="prose prose-invert prose-sm max-w-none prose-a:text-accent prose-pre:border-0 prose-pre:shadow-none prose-code:bg-surface-elevated prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none"
178
- dangerouslySetInnerHTML={{
179
- __html: marked.parse(issue.content) as string,
180
- }}
181
- />
182
- </div>
183
- )
184
- }
@@ -1,73 +0,0 @@
1
- import { formatDisplayDate, formatFullDate } from '@miketromba/issy-core'
2
- import type { Issue } from '../App'
3
- import { Badge } from './Badge'
4
-
5
- interface IssueListProps {
6
- issues: Issue[]
7
- selectedId: string | null
8
- onSelect: (id: string) => void
9
- }
10
-
11
- export function IssueList({ issues, selectedId, onSelect }: IssueListProps) {
12
- if (issues.length === 0) {
13
- return (
14
- <div className="px-5 py-6 text-text-muted text-sm">No issues found</div>
15
- )
16
- }
17
-
18
- return (
19
- <>
20
- {issues.map((issue) => {
21
- const isSelected = issue.id === selectedId
22
-
23
- return (
24
- <button
25
- key={issue.id}
26
- onClick={() => onSelect(issue.id)}
27
- className={`block w-full px-5 py-4 border-0 border-b border-border-subtle bg-transparent text-left cursor-pointer transition-colors hover:bg-surface ${
28
- isSelected ? 'bg-surface-elevated' : ''
29
- }`}
30
- >
31
- <div className="flex items-baseline gap-2 mb-1.5">
32
- <span className="text-sm font-medium text-text-primary leading-snug flex-1 min-w-0 line-clamp-2">
33
- {issue.frontmatter.title || 'Untitled'}
34
- </span>
35
- <span className="font-mono text-xs text-text-muted shrink-0 ml-1">
36
- #{issue.id}
37
- </span>
38
- </div>
39
-
40
- {issue.frontmatter.description && (
41
- <div className="text-[13px] text-text-muted mb-2.5 line-clamp-1">
42
- {issue.frontmatter.description}
43
- </div>
44
- )}
45
-
46
- <div className="flex items-center gap-2 flex-wrap">
47
- {issue.frontmatter.priority && (
48
- <Badge variant="priority" value={issue.frontmatter.priority} />
49
- )}
50
-
51
- {issue.frontmatter.status && (
52
- <Badge variant="status" value={issue.frontmatter.status} />
53
- )}
54
-
55
- {issue.frontmatter.type && (
56
- <Badge variant="type" value={issue.frontmatter.type} />
57
- )}
58
-
59
- {issue.frontmatter.created && (
60
- <span
61
- className="text-xs text-text-muted"
62
- title={formatFullDate(issue.frontmatter.created)}
63
- >
64
- {formatDisplayDate(issue.frontmatter.created)}
65
- </span>
66
- )}
67
- </div>
68
- </button>
69
- )
70
- })}
71
- </>
72
- )
73
- }
@@ -1,156 +0,0 @@
1
- interface QueryHelpModalProps {
2
- isOpen: boolean
3
- onClose: () => void
4
- }
5
-
6
- export function QueryHelpModal({ isOpen, onClose }: QueryHelpModalProps) {
7
- if (!isOpen) return null
8
-
9
- return (
10
- <div
11
- className="fixed inset-0 bg-black/70 flex items-center justify-center z-[1000] p-5"
12
- onClick={onClose}
13
- >
14
- <div
15
- className="bg-surface border border-border rounded-xl max-w-[600px] w-full max-h-[65vh] overflow-y-auto shadow-2xl custom-scrollbar"
16
- onClick={(e) => e.stopPropagation()}
17
- >
18
- <div className="flex items-center justify-between px-6 py-5 border-b border-border">
19
- <h2 className="text-lg font-semibold text-text-primary">
20
- Query Syntax Help
21
- </h2>
22
- <button
23
- onClick={onClose}
24
- aria-label="Close"
25
- className="w-8 h-8 flex items-center justify-center bg-transparent border-0 rounded-md text-text-muted text-2xl cursor-pointer transition-all hover:bg-surface-elevated hover:text-text-primary"
26
- >
27
- ×
28
- </button>
29
- </div>
30
-
31
- <div className="p-6">
32
- <p className="text-text-secondary mb-6 leading-relaxed">
33
- Use qualifiers to filter issues, or type freely to search by text.
34
- </p>
35
-
36
- <section className="mb-6">
37
- <h3 className="text-[13px] font-semibold text-text-muted uppercase tracking-wide mb-3">
38
- Qualifiers
39
- </h3>
40
- <div className="flex flex-col gap-2">
41
- <QualifierRow
42
- qualifier="is:"
43
- description="Filter by status"
44
- values="open, closed"
45
- />
46
- <QualifierRow
47
- qualifier="priority:"
48
- description="Filter by priority"
49
- values="high, medium, low"
50
- />
51
- <QualifierRow
52
- qualifier="type:"
53
- description="Filter by issue type"
54
- values="bug, feature, task, etc."
55
- />
56
- <QualifierRow
57
- qualifier="label:"
58
- description="Filter by label"
59
- values="any label name"
60
- />
61
- <QualifierRow
62
- qualifier="sort:"
63
- description="Sort results"
64
- values="created, priority, title"
65
- />
66
- </div>
67
- </section>
68
-
69
- <section className="mb-6">
70
- <h3 className="text-[13px] font-semibold text-text-muted uppercase tracking-wide mb-3">
71
- Examples
72
- </h3>
73
- <div className="flex flex-col gap-2">
74
- <ExampleRow
75
- code="is:open priority:high"
76
- description="High priority open issues"
77
- />
78
- <ExampleRow
79
- code="type:bug dashboard"
80
- description='Bugs mentioning "dashboard"'
81
- />
82
- <ExampleRow
83
- code="is:open sort:priority"
84
- description="Open issues sorted by priority"
85
- />
86
- <ExampleRow
87
- code="kubernetes cluster"
88
- description='Issues matching "kubernetes cluster"'
89
- />
90
- </div>
91
- </section>
92
-
93
- <section>
94
- <h3 className="text-[13px] font-semibold text-text-muted uppercase tracking-wide mb-3">
95
- Tips
96
- </h3>
97
- <ul className="list-none p-0 m-0 flex flex-col gap-2">
98
- <li className="text-text-secondary text-[13px] pl-4 relative before:content-['•'] before:absolute before:left-0 before:text-text-muted">
99
- Combine multiple qualifiers to narrow results
100
- </li>
101
- <li className="text-text-secondary text-[13px] pl-4 relative before:content-['•'] before:absolute before:left-0 before:text-text-muted">
102
- Free text searches titles, descriptions, and content
103
- </li>
104
- <li className="text-text-secondary text-[13px] pl-4 relative before:content-['•'] before:absolute before:left-0 before:text-text-muted">
105
- Use quotes for multi-word searches:{' '}
106
- <code className="bg-surface-elevated px-1.5 py-0.5 rounded text-xs font-mono">
107
- "api error"
108
- </code>
109
- </li>
110
- <li className="text-text-secondary text-[13px] pl-4 relative before:content-['•'] before:absolute before:left-0 before:text-text-muted">
111
- Qualifiers are case-insensitive
112
- </li>
113
- </ul>
114
- </section>
115
- </div>
116
- </div>
117
- </div>
118
- )
119
- }
120
-
121
- function QualifierRow({
122
- qualifier,
123
- description,
124
- values,
125
- }: {
126
- qualifier: string
127
- description: string
128
- values: string
129
- }) {
130
- return (
131
- <div className="flex gap-3 px-3 py-2.5 bg-surface-elevated rounded-md">
132
- <code className="font-mono text-[13px] text-accent shrink-0 min-w-[80px]">
133
- {qualifier}
134
- </code>
135
- <span className="text-text-secondary text-[13px] flex flex-col gap-0.5">
136
- {description}
137
- <span className="text-text-muted text-xs">{values}</span>
138
- </span>
139
- </div>
140
- )
141
- }
142
-
143
- function ExampleRow({
144
- code,
145
- description,
146
- }: {
147
- code: string
148
- description: string
149
- }) {
150
- return (
151
- <div className="flex flex-col gap-1 px-3 py-2.5 bg-surface-elevated rounded-md">
152
- <code className="font-mono text-[13px] text-text-primary">{code}</code>
153
- <span className="text-text-muted text-xs">{description}</span>
154
- </div>
155
- )
156
- }
package/src/frontend.tsx DELETED
@@ -1,20 +0,0 @@
1
- /**
2
- * issy Frontend Entry Point
3
- */
4
-
5
- import { createRoot } from 'react-dom/client'
6
- import { App } from './App'
7
- import './index.css'
8
-
9
- function start() {
10
- const container = document.getElementById('root')
11
- if (!container) throw new Error('Root element not found')
12
- const root = createRoot(container)
13
- root.render(<App />)
14
- }
15
-
16
- if (document.readyState === 'loading') {
17
- document.addEventListener('DOMContentLoaded', start)
18
- } else {
19
- start()
20
- }
package/src/index.html DELETED
@@ -1,12 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>issy</title>
7
- </head>
8
- <body>
9
- <div id="root"></div>
10
- <script type="module" src="./frontend.tsx"></script>
11
- </body>
12
- </html>
package/src/index.ts DELETED
@@ -1,163 +0,0 @@
1
- /**
2
- * issy API Server
3
- *
4
- * Provides REST API endpoints for the issue tracking system.
5
- * Uses the shared library for all issue operations.
6
- */
7
-
8
- import { resolve } from 'node:path'
9
- // Import shared library
10
- import {
11
- type CreateIssueInput,
12
- closeIssue,
13
- createIssue,
14
- deleteIssue,
15
- filterAndSearchIssues,
16
- filterByQuery,
17
- getAllIssues,
18
- getIssue,
19
- reopenIssue,
20
- setIssuesDir,
21
- type UpdateIssueInput,
22
- updateIssue,
23
- } from '@miketromba/issy-core'
24
- import { serve } from 'bun'
25
- import index from './index.html'
26
-
27
- // Initialize issues directory from env or current working directory
28
- const DEFAULT_ROOT = process.env.ISSUES_ROOT || process.cwd()
29
- const ISSUES_DIR = process.env.ISSUES_DIR || resolve(DEFAULT_ROOT, '.issues')
30
- setIssuesDir(ISSUES_DIR)
31
-
32
- const PORT = Number(process.env.ISSUES_PORT || process.env.PORT || 1554)
33
-
34
- const server = serve({
35
- port: PORT,
36
- routes: {
37
- // API: List all issues with optional filtering and search
38
- // Supports both legacy filters (status, priority, type, search) and
39
- // new query language via 'q' parameter (e.g., q=is:open priority:high)
40
- '/api/issues': {
41
- GET: async (req) => {
42
- const url = new URL(req.url)
43
- const allIssues = await getAllIssues()
44
-
45
- // New query language support via 'q' parameter
46
- const query = url.searchParams.get('q')
47
- if (query) {
48
- return Response.json(filterByQuery(allIssues, query))
49
- }
50
-
51
- // Legacy filter parameters
52
- const status = url.searchParams.get('status') || undefined
53
- const priority = url.searchParams.get('priority') || undefined
54
- const type = url.searchParams.get('type') || undefined
55
- const search = url.searchParams.get('search') || undefined
56
-
57
- // If any legacy filters are provided, apply them
58
- if (status || priority || type || search) {
59
- const filtered = filterAndSearchIssues(allIssues, {
60
- status,
61
- priority,
62
- type,
63
- search,
64
- })
65
- return Response.json(filtered)
66
- }
67
-
68
- return Response.json(allIssues)
69
- },
70
- },
71
-
72
- // API: Get single issue by ID
73
- '/api/issues/:id': {
74
- GET: async (req) => {
75
- const issue = await getIssue(req.params.id)
76
- if (!issue) {
77
- return Response.json({ error: 'Issue not found' }, { status: 404 })
78
- }
79
- return Response.json(issue)
80
- },
81
-
82
- // Update an issue
83
- PATCH: async (req) => {
84
- try {
85
- const input: UpdateIssueInput = await req.json()
86
- const issue = await updateIssue(req.params.id, input)
87
- return Response.json(issue)
88
- } catch (e) {
89
- const message = e instanceof Error ? e.message : 'Unknown error'
90
- return Response.json({ error: message }, { status: 400 })
91
- }
92
- },
93
- },
94
-
95
- // API: Create a new issue
96
- '/api/issues/create': {
97
- POST: async (req) => {
98
- try {
99
- const input: CreateIssueInput = await req.json()
100
- const issue = await createIssue(input)
101
- return Response.json(issue, { status: 201 })
102
- } catch (e) {
103
- const message = e instanceof Error ? e.message : 'Unknown error'
104
- return Response.json({ error: message }, { status: 400 })
105
- }
106
- },
107
- },
108
-
109
- // API: Close an issue
110
- '/api/issues/:id/close': {
111
- POST: async (req) => {
112
- try {
113
- const issue = await closeIssue(req.params.id)
114
- return Response.json(issue)
115
- } catch (e) {
116
- const message = e instanceof Error ? e.message : 'Unknown error'
117
- return Response.json({ error: message }, { status: 400 })
118
- }
119
- },
120
- },
121
-
122
- // API: Reopen an issue
123
- '/api/issues/:id/reopen': {
124
- POST: async (req) => {
125
- try {
126
- const issue = await reopenIssue(req.params.id)
127
- return Response.json(issue)
128
- } catch (e) {
129
- const message = e instanceof Error ? e.message : 'Unknown error'
130
- return Response.json({ error: message }, { status: 400 })
131
- }
132
- },
133
- },
134
-
135
- // API: Delete an issue
136
- '/api/issues/:id/delete': {
137
- DELETE: async (req) => {
138
- try {
139
- await deleteIssue(req.params.id)
140
- return Response.json({ success: true })
141
- } catch (e) {
142
- const message = e instanceof Error ? e.message : 'Unknown error'
143
- return Response.json({ error: message }, { status: 400 })
144
- }
145
- },
146
- },
147
-
148
- // API: Health check
149
- '/api/health': {
150
- GET: () => Response.json({ status: 'ok', service: 'issy' }),
151
- },
152
-
153
- // Serve frontend for everything else
154
- '/*': index,
155
- },
156
-
157
- development: process.env.NODE_ENV !== 'production' && {
158
- hmr: true,
159
- console: true,
160
- },
161
- })
162
-
163
- console.log(`📋 issy running at ${server.url}`)