idea-manager 0.6.1 → 0.7.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "idea-manager",
3
- "version": "0.6.1",
3
+ "version": "0.7.1",
4
4
  "description": "AI 기반 브레인스토밍 → 구조화 → 프롬프트 생성 도구. MCP Server 내장.",
5
5
  "keywords": [
6
6
  "brainstorm",
Binary file
Binary file
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "IM - 아이디어 매니저",
3
+ "short_name": "IM",
4
+ "description": "아이디어에서 실행 가능한 프롬프트까지, 멀티 프로젝트 워크플로우 매니저",
5
+ "start_url": "/",
6
+ "display": "standalone",
7
+ "background_color": "#0f1117",
8
+ "theme_color": "#6366f1",
9
+ "icons": [
10
+ {
11
+ "src": "/icon-192.png",
12
+ "sizes": "192x192",
13
+ "type": "image/png"
14
+ },
15
+ {
16
+ "src": "/icon-512.png",
17
+ "sizes": "512x512",
18
+ "type": "image/png"
19
+ },
20
+ {
21
+ "src": "/icon-512.png",
22
+ "sizes": "512x512",
23
+ "type": "image/png",
24
+ "purpose": "maskable"
25
+ }
26
+ ]
27
+ }
package/public/sw.js ADDED
@@ -0,0 +1,27 @@
1
+ const CACHE_NAME = 'im-v1';
2
+
3
+ self.addEventListener('install', (event) => {
4
+ self.skipWaiting();
5
+ });
6
+
7
+ self.addEventListener('activate', (event) => {
8
+ event.waitUntil(clients.claim());
9
+ });
10
+
11
+ self.addEventListener('fetch', (event) => {
12
+ // Network-first strategy: always try network, fall back to cache
13
+ event.respondWith(
14
+ fetch(event.request)
15
+ .then((response) => {
16
+ // Cache successful GET responses
17
+ if (event.request.method === 'GET' && response.status === 200) {
18
+ const clone = response.clone();
19
+ caches.open(CACHE_NAME).then((cache) => {
20
+ cache.put(event.request, clone);
21
+ });
22
+ }
23
+ return response;
24
+ })
25
+ .catch(() => caches.match(event.request))
26
+ );
27
+ });
@@ -854,6 +854,64 @@ textarea:focus {
854
854
  animation: progressIndeterminate 1.5s ease-in-out infinite;
855
855
  }
856
856
 
857
+ /* Tab bar */
858
+ .tab-bar {
859
+ display: flex;
860
+ align-items: center;
861
+ gap: 0;
862
+ background: hsl(var(--background));
863
+ border-bottom: 1px solid hsl(var(--border));
864
+ padding: 0 4px;
865
+ height: 36px;
866
+ flex-shrink: 0;
867
+ overflow-x: auto;
868
+ -webkit-overflow-scrolling: touch;
869
+ }
870
+
871
+ .tab-bar::-webkit-scrollbar { height: 0; }
872
+
873
+ .tab-item {
874
+ display: flex;
875
+ align-items: center;
876
+ gap: 6px;
877
+ padding: 6px 12px;
878
+ font-size: 13px;
879
+ color: hsl(var(--muted-foreground));
880
+ cursor: pointer;
881
+ border-bottom: 2px solid transparent;
882
+ transition: all 0.15s;
883
+ white-space: nowrap;
884
+ flex-shrink: 0;
885
+ max-width: 180px;
886
+ user-select: none;
887
+ }
888
+
889
+ .tab-item:hover {
890
+ color: hsl(var(--foreground));
891
+ background: hsl(var(--muted));
892
+ }
893
+
894
+ .tab-item-active {
895
+ color: hsl(var(--foreground));
896
+ border-bottom-color: hsl(var(--primary));
897
+ }
898
+
899
+ .tab-close {
900
+ font-size: 15px;
901
+ line-height: 1;
902
+ color: hsl(var(--muted-foreground));
903
+ opacity: 0;
904
+ transition: opacity 0.1s, color 0.1s;
905
+ padding: 0 2px;
906
+ border-radius: 3px;
907
+ }
908
+
909
+ .tab-item:hover .tab-close { opacity: 1; }
910
+ .tab-close:hover {
911
+ color: hsl(var(--destructive));
912
+ background: hsl(var(--destructive) / 0.15);
913
+ }
914
+
857
915
  /* Panel resize handle (vertical) */
858
916
  .panel-resize-handle {
859
917
  flex-shrink: 0;
@@ -14,9 +14,17 @@ const geistMono = Geist_Mono({
14
14
 
15
15
  export const metadata: Metadata = {
16
16
  title: "IM - 아이디어 매니저",
17
- description: "자유롭게 아이디어를 쏟아내면, AI가 구조화해드립니다",
17
+ description: "아이디어에서 실행 가능한 프롬프트까지, 멀티 프로젝트 워크플로우 매니저",
18
18
  icons: {
19
19
  icon: '/favicon.svg',
20
+ apple: '/icon-192.png',
21
+ },
22
+ manifest: '/manifest.json',
23
+ themeColor: '#6366f1',
24
+ appleWebApp: {
25
+ capable: true,
26
+ statusBarStyle: 'black-translucent',
27
+ title: 'IM',
20
28
  },
21
29
  };
22
30
 
@@ -39,6 +47,15 @@ export default function RootLayout({
39
47
  className={`${geistSans.variable} ${geistMono.variable} antialiased`}
40
48
  >
41
49
  {children}
50
+ <script
51
+ dangerouslySetInnerHTML={{
52
+ __html: `
53
+ if ('serviceWorker' in navigator) {
54
+ navigator.serviceWorker.register('/sw.js');
55
+ }
56
+ `,
57
+ }}
58
+ />
42
59
  </body>
43
60
  </html>
44
61
  );
package/src/app/page.tsx CHANGED
@@ -1,347 +1,12 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect, useCallback } from 'react';
4
- import { useRouter } from 'next/navigation';
5
- import DirectoryPicker from '@/components/DirectoryPicker';
6
- import ConfirmDialog from '@/components/ui/ConfirmDialog';
7
- import TabBar, { type DashboardTab } from '@/components/dashboard/TabBar';
8
- import SubProjectCard from '@/components/dashboard/SubProjectCard';
9
- import type { ISubProjectWithStats, ITask } from '@/types';
10
-
11
- interface IProject {
12
- id: string;
13
- name: string;
14
- description: string;
15
- project_path: string | null;
16
- created_at: string;
17
- updated_at: string;
18
- }
19
-
20
- interface ProjectWithSubs extends IProject {
21
- subProjects: ISubProjectWithStats[];
22
- }
23
-
24
- export default function Dashboard() {
25
- const router = useRouter();
26
- const [projects, setProjects] = useState<ProjectWithSubs[]>([]);
27
- const [todayTasks, setTodayTasks] = useState<(ITask & { projectName: string; subProjectName: string })[]>([]);
28
- const [showForm, setShowForm] = useState(false);
29
- const [name, setName] = useState('');
30
- const [description, setDescription] = useState('');
31
- const [projectPath, setProjectPath] = useState('');
32
- const [loading, setLoading] = useState(true);
33
- const [showDirPicker, setShowDirPicker] = useState(false);
34
- const [deleteTarget, setDeleteTarget] = useState<string | null>(null);
35
- const [tab, setTab] = useState<DashboardTab>(() => {
36
- if (typeof window !== 'undefined') {
37
- return (localStorage.getItem('im-dashboard-tab') as DashboardTab) || 'active';
38
- }
39
- return 'active';
40
- });
41
-
42
- const fetchData = useCallback(async () => {
43
- const res = await fetch('/api/projects');
44
- const projectList: IProject[] = await res.json();
45
-
46
- const withSubs = await Promise.all(
47
- projectList.map(async (p) => {
48
- const subRes = await fetch(`/api/projects/${p.id}/sub-projects`);
49
- const subProjects: ISubProjectWithStats[] = await subRes.json();
50
- return { ...p, subProjects };
51
- })
52
- );
53
-
54
- setProjects(withSubs);
55
-
56
- // Gather today tasks
57
- const allToday: (ITask & { projectName: string; subProjectName: string })[] = [];
58
- for (const p of withSubs) {
59
- for (const sp of p.subProjects) {
60
- for (const pt of sp.preview_tasks) {
61
- // preview_tasks doesn't have full task data, so we need to check is_today from API
62
- }
63
- }
64
- }
65
- // Fetch today tasks from each project's tasks
66
- for (const p of withSubs) {
67
- for (const sp of p.subProjects) {
68
- if (sp.task_count > 0) {
69
- const tasksRes = await fetch(`/api/projects/${p.id}/sub-projects/${sp.id}/tasks`);
70
- const tasks: ITask[] = await tasksRes.json();
71
- for (const t of tasks) {
72
- if (t.is_today) {
73
- allToday.push({ ...t, projectName: p.name, subProjectName: sp.name });
74
- }
75
- }
76
- }
77
- }
78
- }
79
- setTodayTasks(allToday);
80
- setLoading(false);
81
- }, []);
82
-
83
- useEffect(() => {
84
- fetchData();
85
- }, [fetchData]);
86
-
87
- const handleTabChange = (newTab: DashboardTab) => {
88
- setTab(newTab);
89
- localStorage.setItem('im-dashboard-tab', newTab);
90
- };
91
-
92
- const handleCreate = async (e: React.FormEvent) => {
93
- e.preventDefault();
94
- if (!name.trim()) return;
95
-
96
- const res = await fetch('/api/projects', {
97
- method: 'POST',
98
- headers: { 'Content-Type': 'application/json' },
99
- body: JSON.stringify({ name: name.trim(), description: description.trim(), project_path: projectPath.trim() || undefined }),
100
- });
101
-
102
- if (res.ok) {
103
- const project = await res.json();
104
- setName('');
105
- setDescription('');
106
- setProjectPath('');
107
- setShowForm(false);
108
- router.push(`/projects/${project.id}`);
109
- }
110
- };
111
-
112
- const handleDeleteClick = (id: string, e: React.MouseEvent) => {
113
- e.stopPropagation();
114
- setDeleteTarget(id);
115
- };
116
-
117
- const handleDeleteConfirm = async () => {
118
- if (!deleteTarget) return;
119
- await fetch(`/api/projects/${deleteTarget}`, { method: 'DELETE' });
120
- setDeleteTarget(null);
121
- fetchData();
122
- };
123
-
124
- // Filter sub-projects based on tab
125
- const getVisibleCards = (): { sp: ISubProjectWithStats; projectName: string; projectId: string }[] => {
126
- const cards: { sp: ISubProjectWithStats; projectName: string; projectId: string }[] = [];
127
- for (const p of projects) {
128
- for (const sp of p.subProjects) {
129
- if (tab === 'active') {
130
- if (sp.active_count > 0 || sp.problem_count > 0) {
131
- cards.push({ sp, projectName: p.name, projectId: p.id });
132
- }
133
- } else if (tab === 'all') {
134
- cards.push({ sp, projectName: p.name, projectId: p.id });
135
- }
136
- }
137
- }
138
- // Sort: active cards first
139
- cards.sort((a, b) => (b.sp.active_count + b.sp.problem_count) - (a.sp.active_count + a.sp.problem_count));
140
- return cards;
141
- };
142
-
143
- const STATUS_ICONS: Record<string, string> = {
144
- idea: '\u{1F4A1}', writing: '\u{270F}\u{FE0F}', submitted: '\u{1F680}',
145
- testing: '\u{1F9EA}', done: '\u{2705}', problem: '\u{1F534}',
146
- };
3
+ import { TabProvider } from '@/components/tabs/TabContext';
4
+ import TabShell from '@/components/tabs/TabShell';
147
5
 
6
+ export default function App() {
148
7
  return (
149
- <div className="min-h-screen p-8 max-w-5xl mx-auto">
150
- <header className="flex items-center justify-between mb-6">
151
- <div>
152
- <h1 className="text-2xl font-bold tracking-tight">
153
- IM <span className="text-muted-foreground font-normal text-sm ml-2">Idea Manager v2</span>
154
- </h1>
155
- </div>
156
- <div className="flex items-center gap-3">
157
- <TabBar value={tab} onChange={handleTabChange} />
158
- <button
159
- onClick={() => setShowForm(!showForm)}
160
- className="px-4 py-2 bg-primary hover:bg-primary-hover text-white rounded-lg
161
- transition-colors font-medium text-sm"
162
- >
163
- + Project
164
- </button>
165
- </div>
166
- </header>
167
-
168
- {showForm && (
169
- <form onSubmit={handleCreate} className="mb-6 p-5 bg-card rounded-lg border border-border">
170
- <input
171
- type="text"
172
- placeholder="Project name"
173
- value={name}
174
- onChange={(e) => setName(e.target.value)}
175
- className="w-full bg-input border border-border rounded-lg px-4 py-2.5 mb-3
176
- focus:border-primary focus:outline-none text-foreground"
177
- autoFocus
178
- />
179
- <input
180
- type="text"
181
- placeholder="Description (optional)"
182
- value={description}
183
- onChange={(e) => setDescription(e.target.value)}
184
- className="w-full bg-input border border-border rounded-lg px-4 py-2.5 mb-3
185
- focus:border-primary focus:outline-none text-foreground"
186
- />
187
- <div className="mb-4">
188
- <button
189
- type="button"
190
- onClick={() => setShowDirPicker(true)}
191
- className="w-full bg-input border border-border rounded-lg px-4 py-2.5
192
- text-left text-sm hover:border-primary transition-colors"
193
- >
194
- {projectPath ? (
195
- <span className="font-mono text-foreground">{projectPath}</span>
196
- ) : (
197
- <span className="text-muted-foreground">Project folder (optional)</span>
198
- )}
199
- </button>
200
- </div>
201
- <div className="flex gap-2 justify-end">
202
- <button type="button" onClick={() => setShowForm(false)}
203
- className="px-4 py-2 text-muted-foreground hover:text-foreground transition-colors text-sm">
204
- Cancel
205
- </button>
206
- <button type="submit"
207
- className="px-4 py-2 bg-primary hover:bg-primary-hover text-white rounded-lg transition-colors text-sm">
208
- Create
209
- </button>
210
- </div>
211
- </form>
212
- )}
213
-
214
- {loading ? (
215
- <div className="text-center text-muted-foreground py-20">Loading...</div>
216
- ) : tab === 'today' ? (
217
- /* Today tab */
218
- todayTasks.length === 0 ? (
219
- <div className="text-center py-20 text-muted-foreground">
220
- <p className="text-lg mb-2">No tasks marked for today</p>
221
- <p className="text-sm">Mark tasks with the Today button in task detail</p>
222
- </div>
223
- ) : (
224
- <div className="space-y-2">
225
- {todayTasks.map((task) => (
226
- <div
227
- key={task.id}
228
- onClick={() => router.push(`/projects/${task.project_id}?sub=${task.sub_project_id}&task=${task.id}`)}
229
- className="flex items-center gap-3 p-3 bg-card hover:bg-card-hover border border-border
230
- rounded-lg cursor-pointer transition-colors"
231
- >
232
- <span className="text-sm">{STATUS_ICONS[task.status]}</span>
233
- <div className="flex-1 min-w-0">
234
- <span className="text-sm font-medium">{task.title}</span>
235
- <span className="text-xs text-muted-foreground ml-2">
236
- {task.projectName} / {task.subProjectName}
237
- </span>
238
- </div>
239
- </div>
240
- ))}
241
- </div>
242
- )
243
- ) : (
244
- /* Active / All tabs */
245
- <>
246
- {/* Project headers for All tab */}
247
- {tab === 'all' ? (
248
- projects.length === 0 ? (
249
- <div className="text-center py-20">
250
- <p className="text-muted-foreground text-lg mb-2">No projects yet</p>
251
- <p className="text-muted-foreground text-sm">Click + Project to get started</p>
252
- </div>
253
- ) : (
254
- <div className="space-y-6">
255
- {projects.map((project) => (
256
- <div key={project.id}>
257
- <div className="flex items-center justify-between mb-3">
258
- <div
259
- className="flex items-center gap-2 cursor-pointer hover:text-primary transition-colors"
260
- onClick={() => router.push(`/projects/${project.id}`)}
261
- >
262
- <h2 className="text-sm font-semibold">{project.name}</h2>
263
- {project.project_path && (
264
- <span className="text-xs text-muted-foreground font-mono truncate max-w-48">
265
- {project.project_path}
266
- </span>
267
- )}
268
- </div>
269
- <button
270
- onClick={(e) => handleDeleteClick(project.id, e)}
271
- className="text-xs text-muted-foreground hover:text-destructive transition-colors"
272
- >
273
- Delete
274
- </button>
275
- </div>
276
- {project.subProjects.length === 0 ? (
277
- <div className="text-xs text-muted-foreground py-4 text-center border border-dashed border-border rounded-lg">
278
- No sub-projects.{' '}
279
- <span
280
- className="text-primary cursor-pointer hover:underline"
281
- onClick={() => router.push(`/projects/${project.id}`)}
282
- >
283
- Open project
284
- </span>{' '}
285
- to add one.
286
- </div>
287
- ) : (
288
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
289
- {project.subProjects.map((sp) => (
290
- <SubProjectCard
291
- key={sp.id}
292
- subProject={sp}
293
- projectName={project.name}
294
- onClick={() => router.push(`/projects/${project.id}?sub=${sp.id}`)}
295
- />
296
- ))}
297
- </div>
298
- )}
299
- </div>
300
- ))}
301
- </div>
302
- )
303
- ) : (
304
- /* Active tab */
305
- (() => {
306
- const cards = getVisibleCards();
307
- return cards.length === 0 ? (
308
- <div className="text-center py-20 text-muted-foreground">
309
- <p className="text-lg mb-2">No active tasks</p>
310
- <p className="text-sm">Submit tasks to see them here</p>
311
- </div>
312
- ) : (
313
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
314
- {cards.map(({ sp, projectName, projectId }) => (
315
- <SubProjectCard
316
- key={sp.id}
317
- subProject={sp}
318
- projectName={projectName}
319
- onClick={() => router.push(`/projects/${projectId}?sub=${sp.id}`)}
320
- />
321
- ))}
322
- </div>
323
- );
324
- })()
325
- )}
326
- </>
327
- )}
328
-
329
- {showDirPicker && (
330
- <DirectoryPicker
331
- onSelect={(path) => { setProjectPath(path); setShowDirPicker(false); }}
332
- onCancel={() => setShowDirPicker(false)}
333
- />
334
- )}
335
-
336
- <ConfirmDialog
337
- open={!!deleteTarget}
338
- title="Delete project?"
339
- description="This will permanently delete the project and all its data."
340
- confirmLabel="Delete"
341
- variant="danger"
342
- onConfirm={handleDeleteConfirm}
343
- onCancel={() => setDeleteTarget(null)}
344
- />
345
- </div>
8
+ <TabProvider>
9
+ <TabShell />
10
+ </TabProvider>
346
11
  );
347
12
  }