@miketromba/issy-app 0.1.0

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.
@@ -0,0 +1,160 @@
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
34
+ search by text.
35
+ </p>
36
+
37
+ <section className="mb-6">
38
+ <h3 className="text-[13px] font-semibold text-text-muted uppercase tracking-wide mb-3">
39
+ Qualifiers
40
+ </h3>
41
+ <div className="flex flex-col gap-2">
42
+ <QualifierRow
43
+ qualifier="is:"
44
+ description="Filter by status"
45
+ values="open, closed"
46
+ />
47
+ <QualifierRow
48
+ qualifier="priority:"
49
+ description="Filter by priority"
50
+ values="high, medium, low"
51
+ />
52
+ <QualifierRow
53
+ qualifier="type:"
54
+ description="Filter by issue type"
55
+ values="bug, feature, task, etc."
56
+ />
57
+ <QualifierRow
58
+ qualifier="label:"
59
+ description="Filter by label"
60
+ values="any label name"
61
+ />
62
+ <QualifierRow
63
+ qualifier="sort:"
64
+ description="Sort results"
65
+ values="created, priority, title"
66
+ />
67
+ </div>
68
+ </section>
69
+
70
+ <section className="mb-6">
71
+ <h3 className="text-[13px] font-semibold text-text-muted uppercase tracking-wide mb-3">
72
+ Examples
73
+ </h3>
74
+ <div className="flex flex-col gap-2">
75
+ <ExampleRow
76
+ code="is:open priority:high"
77
+ description="High priority open issues"
78
+ />
79
+ <ExampleRow
80
+ code="type:bug dashboard"
81
+ description='Bugs mentioning "dashboard"'
82
+ />
83
+ <ExampleRow
84
+ code="is:open sort:priority"
85
+ description="Open issues sorted by priority"
86
+ />
87
+ <ExampleRow
88
+ code="kubernetes cluster"
89
+ description='Issues matching "kubernetes cluster"'
90
+ />
91
+ </div>
92
+ </section>
93
+
94
+ <section>
95
+ <h3 className="text-[13px] font-semibold text-text-muted uppercase tracking-wide mb-3">
96
+ Tips
97
+ </h3>
98
+ <ul className="list-none p-0 m-0 flex flex-col gap-2">
99
+ <li className="text-text-secondary text-[13px] pl-4 relative before:content-['•'] before:absolute before:left-0 before:text-text-muted">
100
+ Combine multiple qualifiers to narrow results
101
+ </li>
102
+ <li className="text-text-secondary text-[13px] pl-4 relative before:content-['•'] before:absolute before:left-0 before:text-text-muted">
103
+ Free text searches titles, descriptions, and
104
+ content
105
+ </li>
106
+ <li className="text-text-secondary text-[13px] pl-4 relative before:content-['•'] before:absolute before:left-0 before:text-text-muted">
107
+ Use quotes for multi-word searches:{' '}
108
+ <code className="bg-surface-elevated px-1.5 py-0.5 rounded text-xs font-mono">
109
+ "api error"
110
+ </code>
111
+ </li>
112
+ <li className="text-text-secondary text-[13px] pl-4 relative before:content-['•'] before:absolute before:left-0 before:text-text-muted">
113
+ Qualifiers are case-insensitive
114
+ </li>
115
+ </ul>
116
+ </section>
117
+ </div>
118
+ </div>
119
+ </div>
120
+ )
121
+ }
122
+
123
+ function QualifierRow({
124
+ qualifier,
125
+ description,
126
+ values
127
+ }: {
128
+ qualifier: string
129
+ description: string
130
+ values: string
131
+ }) {
132
+ return (
133
+ <div className="flex gap-3 px-3 py-2.5 bg-surface-elevated rounded-md">
134
+ <code className="font-mono text-[13px] text-accent shrink-0 min-w-[80px]">
135
+ {qualifier}
136
+ </code>
137
+ <span className="text-text-secondary text-[13px] flex flex-col gap-0.5">
138
+ {description}
139
+ <span className="text-text-muted text-xs">{values}</span>
140
+ </span>
141
+ </div>
142
+ )
143
+ }
144
+
145
+ function ExampleRow({
146
+ code,
147
+ description
148
+ }: {
149
+ code: string
150
+ description: string
151
+ }) {
152
+ return (
153
+ <div className="flex flex-col gap-1 px-3 py-2.5 bg-surface-elevated rounded-md">
154
+ <code className="font-mono text-[13px] text-text-primary">
155
+ {code}
156
+ </code>
157
+ <span className="text-text-muted text-xs">{description}</span>
158
+ </div>
159
+ )
160
+ }
@@ -0,0 +1,18 @@
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 root = createRoot(document.getElementById('root')!)
11
+ root.render(<App />)
12
+ }
13
+
14
+ if (document.readyState === 'loading') {
15
+ document.addEventListener('DOMContentLoaded', start)
16
+ } else {
17
+ start()
18
+ }
package/src/index.css ADDED
@@ -0,0 +1,99 @@
1
+ @import "tailwindcss";
2
+ @plugin "@tailwindcss/typography";
3
+
4
+ @theme {
5
+ /* Core palette - dark theme */
6
+ --color-background: #0a0b0f;
7
+ --color-surface: #12141a;
8
+ --color-surface-elevated: #1a1c24;
9
+ --color-border: #2a2d38;
10
+ --color-border-subtle: #1e2028;
11
+
12
+ /* Text hierarchy */
13
+ --color-text-primary: #f4f4f5;
14
+ --color-text-secondary: #a1a1aa;
15
+ --color-text-muted: #71717a;
16
+
17
+ /* Accent - warm coral */
18
+ --color-accent: #ff6b5b;
19
+ --color-accent-hover: #ff8577;
20
+
21
+ /* Status colors */
22
+ --color-status-open: #22c55e;
23
+ --color-status-closed: #6b7280;
24
+
25
+ /* Priority colors */
26
+ --color-priority-high: #ef4444;
27
+ --color-priority-medium: #f59e0b;
28
+ --color-priority-low: #22c55e;
29
+
30
+ /* Type colors */
31
+ --color-type-default: #a78bfa;
32
+ --color-type-bug: #f87171;
33
+ --color-type-feature: #60a5fa;
34
+ }
35
+
36
+ /* Custom scrollbar */
37
+ .custom-scrollbar::-webkit-scrollbar {
38
+ width: 6px;
39
+ }
40
+
41
+ .custom-scrollbar::-webkit-scrollbar-track {
42
+ background: var(--color-background);
43
+ }
44
+
45
+ .custom-scrollbar::-webkit-scrollbar-thumb {
46
+ background: var(--color-border);
47
+ border-radius: 3px;
48
+ }
49
+
50
+ /* Code block styling - remove borders, unify background, add dark scrollbar */
51
+ .prose pre {
52
+ border: none !important;
53
+ box-shadow: none !important;
54
+ background-color: var(--color-surface-elevated) !important;
55
+ }
56
+
57
+ .prose pre code.hljs {
58
+ background: transparent !important;
59
+ padding: 0 !important;
60
+ font-size: 13px !important;
61
+ }
62
+
63
+ .prose pre::-webkit-scrollbar {
64
+ height: 6px;
65
+ }
66
+
67
+ .prose pre::-webkit-scrollbar-track {
68
+ background: transparent;
69
+ }
70
+
71
+ .prose pre::-webkit-scrollbar-thumb {
72
+ background: var(--color-border);
73
+ border-radius: 3px;
74
+ }
75
+
76
+ .prose pre::-webkit-scrollbar-thumb:hover {
77
+ background: var(--color-text-muted);
78
+ }
79
+
80
+ /* Firefox scrollbar */
81
+ .prose pre {
82
+ scrollbar-width: thin;
83
+ scrollbar-color: var(--color-border) transparent;
84
+ }
85
+
86
+ /* Line clamp utility for title */
87
+ .line-clamp-2 {
88
+ display: -webkit-box;
89
+ -webkit-line-clamp: 2;
90
+ -webkit-box-orient: vertical;
91
+ overflow: hidden;
92
+ }
93
+
94
+ .line-clamp-1 {
95
+ display: -webkit-box;
96
+ -webkit-line-clamp: 1;
97
+ -webkit-box-orient: vertical;
98
+ overflow: hidden;
99
+ }
package/src/index.html ADDED
@@ -0,0 +1,12 @@
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 ADDED
@@ -0,0 +1,164 @@
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 { serve } from "bun";
9
+ import { resolve } from "node:path";
10
+ import index from "./index.html";
11
+
12
+ // Import shared library
13
+ import {
14
+ setIssuesDir,
15
+ getAllIssues,
16
+ getIssue,
17
+ createIssue,
18
+ updateIssue,
19
+ closeIssue,
20
+ reopenIssue,
21
+ deleteIssue,
22
+ filterAndSearchIssues,
23
+ filterByQuery,
24
+ type CreateIssueInput,
25
+ type UpdateIssueInput,
26
+ } from "@miketromba/issy-core";
27
+
28
+ // Initialize issues directory from env or current working directory
29
+ const DEFAULT_ROOT = process.env.ISSUES_ROOT || process.cwd();
30
+ const ISSUES_DIR = process.env.ISSUES_DIR || resolve(DEFAULT_ROOT, ".issues");
31
+ setIssuesDir(ISSUES_DIR);
32
+
33
+ const PORT = Number(process.env.ISSUES_PORT || process.env.PORT || 1554);
34
+
35
+ const server = serve({
36
+ port: PORT,
37
+ routes: {
38
+ // API: List all issues with optional filtering and search
39
+ // Supports both legacy filters (status, priority, type, search) and
40
+ // new query language via 'q' parameter (e.g., q=is:open priority:high)
41
+ "/api/issues": {
42
+ GET: async (req) => {
43
+ const url = new URL(req.url);
44
+ const allIssues = await getAllIssues();
45
+
46
+ // New query language support via 'q' parameter
47
+ const query = url.searchParams.get("q");
48
+ if (query) {
49
+ return Response.json(filterByQuery(allIssues, query));
50
+ }
51
+
52
+ // Legacy filter parameters
53
+ const status = url.searchParams.get("status") || undefined;
54
+ const priority = url.searchParams.get("priority") || undefined;
55
+ const type = url.searchParams.get("type") || undefined;
56
+ const search = url.searchParams.get("search") || undefined;
57
+
58
+ // If any legacy filters are provided, apply them
59
+ if (status || priority || type || search) {
60
+ const filtered = filterAndSearchIssues(allIssues, {
61
+ status,
62
+ priority,
63
+ type,
64
+ search,
65
+ });
66
+ return Response.json(filtered);
67
+ }
68
+
69
+ return Response.json(allIssues);
70
+ },
71
+ },
72
+
73
+ // API: Get single issue by ID
74
+ "/api/issues/:id": {
75
+ GET: async (req) => {
76
+ const issue = await getIssue(req.params.id);
77
+ if (!issue) {
78
+ return Response.json({ error: "Issue not found" }, { status: 404 });
79
+ }
80
+ return Response.json(issue);
81
+ },
82
+
83
+ // Update an issue
84
+ PATCH: async (req) => {
85
+ try {
86
+ const input: UpdateIssueInput = await req.json();
87
+ const issue = await updateIssue(req.params.id, input);
88
+ return Response.json(issue);
89
+ } catch (e) {
90
+ const message = e instanceof Error ? e.message : "Unknown error";
91
+ return Response.json({ error: message }, { status: 400 });
92
+ }
93
+ },
94
+ },
95
+
96
+ // API: Create a new issue
97
+ "/api/issues/create": {
98
+ POST: async (req) => {
99
+ try {
100
+ const input: CreateIssueInput = await req.json();
101
+ const issue = await createIssue(input);
102
+ return Response.json(issue, { status: 201 });
103
+ } catch (e) {
104
+ const message = e instanceof Error ? e.message : "Unknown error";
105
+ return Response.json({ error: message }, { status: 400 });
106
+ }
107
+ },
108
+ },
109
+
110
+ // API: Close an issue
111
+ "/api/issues/:id/close": {
112
+ POST: async (req) => {
113
+ try {
114
+ const issue = await closeIssue(req.params.id);
115
+ return Response.json(issue);
116
+ } catch (e) {
117
+ const message = e instanceof Error ? e.message : "Unknown error";
118
+ return Response.json({ error: message }, { status: 400 });
119
+ }
120
+ },
121
+ },
122
+
123
+ // API: Reopen an issue
124
+ "/api/issues/:id/reopen": {
125
+ POST: async (req) => {
126
+ try {
127
+ const issue = await reopenIssue(req.params.id);
128
+ return Response.json(issue);
129
+ } catch (e) {
130
+ const message = e instanceof Error ? e.message : "Unknown error";
131
+ return Response.json({ error: message }, { status: 400 });
132
+ }
133
+ },
134
+ },
135
+
136
+ // API: Delete an issue
137
+ "/api/issues/:id/delete": {
138
+ DELETE: async (req) => {
139
+ try {
140
+ await deleteIssue(req.params.id);
141
+ return Response.json({ success: true });
142
+ } catch (e) {
143
+ const message = e instanceof Error ? e.message : "Unknown error";
144
+ return Response.json({ error: message }, { status: 400 });
145
+ }
146
+ },
147
+ },
148
+
149
+ // API: Health check
150
+ "/api/health": {
151
+ GET: () => Response.json({ status: "ok", service: "issy" }),
152
+ },
153
+
154
+ // Serve frontend for everything else
155
+ "/*": index,
156
+ },
157
+
158
+ development: process.env.NODE_ENV !== "production" && {
159
+ hmr: true,
160
+ console: true,
161
+ },
162
+ });
163
+
164
+ console.log(`📋 issy running at ${server.url}`);