create-arete-workspace 0.2.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.
Files changed (180) hide show
  1. package/README.md +77 -0
  2. package/bin/arete.js +156 -0
  3. package/bin/create.js +111 -0
  4. package/lib/install-openclaw.js +50 -0
  5. package/lib/scaffold.js +213 -0
  6. package/lib/setup-wizard.js +88 -0
  7. package/lib/updater.js +130 -0
  8. package/package.json +34 -0
  9. package/packages/gatsaeng-os/README.md +36 -0
  10. package/packages/gatsaeng-os/components.json +23 -0
  11. package/packages/gatsaeng-os/eslint.config.mjs +18 -0
  12. package/packages/gatsaeng-os/next.config.ts +7 -0
  13. package/packages/gatsaeng-os/package.json +59 -0
  14. package/packages/gatsaeng-os/postcss.config.mjs +7 -0
  15. package/packages/gatsaeng-os/public/file.svg +1 -0
  16. package/packages/gatsaeng-os/public/globe.svg +1 -0
  17. package/packages/gatsaeng-os/public/next.svg +1 -0
  18. package/packages/gatsaeng-os/public/vercel.svg +1 -0
  19. package/packages/gatsaeng-os/public/window.svg +1 -0
  20. package/packages/gatsaeng-os/python/api_server.py +248 -0
  21. package/packages/gatsaeng-os/python/briefing.py +145 -0
  22. package/packages/gatsaeng-os/python/config.py +55 -0
  23. package/packages/gatsaeng-os/python/goal_context_agent.py +193 -0
  24. package/packages/gatsaeng-os/python/gyeokguk.py +171 -0
  25. package/packages/gatsaeng-os/python/proactive.py +158 -0
  26. package/packages/gatsaeng-os/python/requirements.txt +11 -0
  27. package/packages/gatsaeng-os/python/run.py +28 -0
  28. package/packages/gatsaeng-os/python/scoring.py +44 -0
  29. package/packages/gatsaeng-os/python/streak.py +70 -0
  30. package/packages/gatsaeng-os/python/telegram_bot.py +331 -0
  31. package/packages/gatsaeng-os/python/timing_engine.py +117 -0
  32. package/packages/gatsaeng-os/python/vault_io.py +423 -0
  33. package/packages/gatsaeng-os/src/app/(dashboard)/areas/[id]/page.tsx +215 -0
  34. package/packages/gatsaeng-os/src/app/(dashboard)/areas/page.tsx +161 -0
  35. package/packages/gatsaeng-os/src/app/(dashboard)/books/[id]/page.tsx +215 -0
  36. package/packages/gatsaeng-os/src/app/(dashboard)/books/page.tsx +268 -0
  37. package/packages/gatsaeng-os/src/app/(dashboard)/calendar/page.tsx +379 -0
  38. package/packages/gatsaeng-os/src/app/(dashboard)/error.tsx +30 -0
  39. package/packages/gatsaeng-os/src/app/(dashboard)/focus/page.tsx +293 -0
  40. package/packages/gatsaeng-os/src/app/(dashboard)/goals/[id]/page.tsx +426 -0
  41. package/packages/gatsaeng-os/src/app/(dashboard)/goals/page.tsx +178 -0
  42. package/packages/gatsaeng-os/src/app/(dashboard)/layout.tsx +29 -0
  43. package/packages/gatsaeng-os/src/app/(dashboard)/notes/[id]/page.tsx +147 -0
  44. package/packages/gatsaeng-os/src/app/(dashboard)/notes/page.tsx +254 -0
  45. package/packages/gatsaeng-os/src/app/(dashboard)/page.tsx +26 -0
  46. package/packages/gatsaeng-os/src/app/(dashboard)/projects/[id]/page.tsx +86 -0
  47. package/packages/gatsaeng-os/src/app/(dashboard)/projects/page.tsx +215 -0
  48. package/packages/gatsaeng-os/src/app/(dashboard)/review/page.tsx +475 -0
  49. package/packages/gatsaeng-os/src/app/(dashboard)/routines/page.tsx +436 -0
  50. package/packages/gatsaeng-os/src/app/(dashboard)/tasks/[id]/page.tsx +210 -0
  51. package/packages/gatsaeng-os/src/app/(dashboard)/tasks/page.tsx +307 -0
  52. package/packages/gatsaeng-os/src/app/(dashboard)/voice/page.tsx +212 -0
  53. package/packages/gatsaeng-os/src/app/api/areas/[id]/route.ts +26 -0
  54. package/packages/gatsaeng-os/src/app/api/areas/route.ts +22 -0
  55. package/packages/gatsaeng-os/src/app/api/auth/login/route.ts +52 -0
  56. package/packages/gatsaeng-os/src/app/api/auth/logout/route.ts +8 -0
  57. package/packages/gatsaeng-os/src/app/api/books/[id]/route.ts +27 -0
  58. package/packages/gatsaeng-os/src/app/api/books/route.ts +20 -0
  59. package/packages/gatsaeng-os/src/app/api/calendar/[id]/route.ts +24 -0
  60. package/packages/gatsaeng-os/src/app/api/calendar/import/route.ts +52 -0
  61. package/packages/gatsaeng-os/src/app/api/calendar/route.ts +37 -0
  62. package/packages/gatsaeng-os/src/app/api/daily/route.ts +51 -0
  63. package/packages/gatsaeng-os/src/app/api/goals/[id]/route.ts +34 -0
  64. package/packages/gatsaeng-os/src/app/api/goals/route.ts +30 -0
  65. package/packages/gatsaeng-os/src/app/api/logs/energy/route.ts +40 -0
  66. package/packages/gatsaeng-os/src/app/api/logs/focus/route.ts +22 -0
  67. package/packages/gatsaeng-os/src/app/api/logs/routine/route.ts +54 -0
  68. package/packages/gatsaeng-os/src/app/api/milestones/[id]/route.ts +26 -0
  69. package/packages/gatsaeng-os/src/app/api/milestones/route.ts +47 -0
  70. package/packages/gatsaeng-os/src/app/api/notes/[id]/route.ts +29 -0
  71. package/packages/gatsaeng-os/src/app/api/notes/route.ts +37 -0
  72. package/packages/gatsaeng-os/src/app/api/profile/route.ts +17 -0
  73. package/packages/gatsaeng-os/src/app/api/projects/[id]/route.ts +27 -0
  74. package/packages/gatsaeng-os/src/app/api/projects/route.ts +25 -0
  75. package/packages/gatsaeng-os/src/app/api/reviews/[id]/route.ts +26 -0
  76. package/packages/gatsaeng-os/src/app/api/reviews/route.ts +29 -0
  77. package/packages/gatsaeng-os/src/app/api/routines/[id]/route.ts +26 -0
  78. package/packages/gatsaeng-os/src/app/api/routines/route.ts +28 -0
  79. package/packages/gatsaeng-os/src/app/api/tasks/[id]/route.ts +28 -0
  80. package/packages/gatsaeng-os/src/app/api/tasks/route.ts +66 -0
  81. package/packages/gatsaeng-os/src/app/api/timing/current/route.ts +63 -0
  82. package/packages/gatsaeng-os/src/app/api/voice/chat/route.ts +50 -0
  83. package/packages/gatsaeng-os/src/app/api/voice/transcribe/route.ts +25 -0
  84. package/packages/gatsaeng-os/src/app/api/voice/tts/route.ts +36 -0
  85. package/packages/gatsaeng-os/src/app/error.tsx +30 -0
  86. package/packages/gatsaeng-os/src/app/favicon.ico +0 -0
  87. package/packages/gatsaeng-os/src/app/globals.css +208 -0
  88. package/packages/gatsaeng-os/src/app/layout.tsx +33 -0
  89. package/packages/gatsaeng-os/src/app/login/page.tsx +87 -0
  90. package/packages/gatsaeng-os/src/app/providers.tsx +27 -0
  91. package/packages/gatsaeng-os/src/components/ErrorBoundary.tsx +46 -0
  92. package/packages/gatsaeng-os/src/components/dashboard/DashboardGrid.tsx +86 -0
  93. package/packages/gatsaeng-os/src/components/dashboard/DdayWidget.tsx +88 -0
  94. package/packages/gatsaeng-os/src/components/dashboard/EnergyTracker.tsx +87 -0
  95. package/packages/gatsaeng-os/src/components/dashboard/FocusTimer.tsx +139 -0
  96. package/packages/gatsaeng-os/src/components/dashboard/GatsaengScore.tsx +30 -0
  97. package/packages/gatsaeng-os/src/components/dashboard/GoalRings.tsx +107 -0
  98. package/packages/gatsaeng-os/src/components/dashboard/ProactiveBar.tsx +98 -0
  99. package/packages/gatsaeng-os/src/components/dashboard/RoutineChecklist.tsx +81 -0
  100. package/packages/gatsaeng-os/src/components/dashboard/TimingWidget.tsx +86 -0
  101. package/packages/gatsaeng-os/src/components/dashboard/WidgetCustomizer.tsx +95 -0
  102. package/packages/gatsaeng-os/src/components/dashboard/WidgetWrapper.tsx +33 -0
  103. package/packages/gatsaeng-os/src/components/dashboard/ZeigarnikPanel.tsx +43 -0
  104. package/packages/gatsaeng-os/src/components/editor/EditorToolbar.tsx +186 -0
  105. package/packages/gatsaeng-os/src/components/editor/TiptapEditor.tsx +114 -0
  106. package/packages/gatsaeng-os/src/components/layout/Header.tsx +47 -0
  107. package/packages/gatsaeng-os/src/components/layout/MobileBottomNav.tsx +122 -0
  108. package/packages/gatsaeng-os/src/components/layout/MobileSidebar.tsx +29 -0
  109. package/packages/gatsaeng-os/src/components/layout/Sidebar.tsx +142 -0
  110. package/packages/gatsaeng-os/src/components/onboarding/OnboardingFlow.tsx +229 -0
  111. package/packages/gatsaeng-os/src/components/onboarding/OnboardingGate.tsx +78 -0
  112. package/packages/gatsaeng-os/src/components/projects/CalendarView.tsx +152 -0
  113. package/packages/gatsaeng-os/src/components/projects/KanbanView.tsx +180 -0
  114. package/packages/gatsaeng-os/src/components/projects/ListView.tsx +82 -0
  115. package/packages/gatsaeng-os/src/components/projects/TableView.tsx +206 -0
  116. package/packages/gatsaeng-os/src/components/projects/TaskCard.tsx +154 -0
  117. package/packages/gatsaeng-os/src/components/projects/TaskForm.tsx +128 -0
  118. package/packages/gatsaeng-os/src/components/projects/ViewSwitcher.tsx +40 -0
  119. package/packages/gatsaeng-os/src/components/search/GlobalSearch.tsx +179 -0
  120. package/packages/gatsaeng-os/src/components/shared/InlineEdit.tsx +77 -0
  121. package/packages/gatsaeng-os/src/components/shared/PinButton.tsx +42 -0
  122. package/packages/gatsaeng-os/src/components/tasks/DDayBadge.tsx +34 -0
  123. package/packages/gatsaeng-os/src/components/ui/badge.tsx +48 -0
  124. package/packages/gatsaeng-os/src/components/ui/button.tsx +64 -0
  125. package/packages/gatsaeng-os/src/components/ui/card.tsx +92 -0
  126. package/packages/gatsaeng-os/src/components/ui/checkbox.tsx +32 -0
  127. package/packages/gatsaeng-os/src/components/ui/command.tsx +184 -0
  128. package/packages/gatsaeng-os/src/components/ui/dialog.tsx +158 -0
  129. package/packages/gatsaeng-os/src/components/ui/input.tsx +21 -0
  130. package/packages/gatsaeng-os/src/components/ui/label.tsx +24 -0
  131. package/packages/gatsaeng-os/src/components/ui/popover.tsx +89 -0
  132. package/packages/gatsaeng-os/src/components/ui/progress.tsx +31 -0
  133. package/packages/gatsaeng-os/src/components/ui/select.tsx +190 -0
  134. package/packages/gatsaeng-os/src/components/ui/sheet.tsx +143 -0
  135. package/packages/gatsaeng-os/src/components/ui/tabs.tsx +91 -0
  136. package/packages/gatsaeng-os/src/components/ui/toggle-group.tsx +83 -0
  137. package/packages/gatsaeng-os/src/components/ui/toggle.tsx +47 -0
  138. package/packages/gatsaeng-os/src/components/ui/tooltip.tsx +57 -0
  139. package/packages/gatsaeng-os/src/hooks/useAreas.ts +53 -0
  140. package/packages/gatsaeng-os/src/hooks/useBooks.ts +62 -0
  141. package/packages/gatsaeng-os/src/hooks/useCalendar.ts +59 -0
  142. package/packages/gatsaeng-os/src/hooks/useDaily.ts +15 -0
  143. package/packages/gatsaeng-os/src/hooks/useGlobalTasks.ts +45 -0
  144. package/packages/gatsaeng-os/src/hooks/useGoals.ts +53 -0
  145. package/packages/gatsaeng-os/src/hooks/useMilestones.ts +75 -0
  146. package/packages/gatsaeng-os/src/hooks/useNotes.ts +65 -0
  147. package/packages/gatsaeng-os/src/hooks/useProjects.ts +102 -0
  148. package/packages/gatsaeng-os/src/hooks/useRoutines.ts +76 -0
  149. package/packages/gatsaeng-os/src/hooks/useTiming.ts +27 -0
  150. package/packages/gatsaeng-os/src/lib/apiFetch.ts +14 -0
  151. package/packages/gatsaeng-os/src/lib/auth.ts +32 -0
  152. package/packages/gatsaeng-os/src/lib/date.ts +7 -0
  153. package/packages/gatsaeng-os/src/lib/editor/markdown.ts +35 -0
  154. package/packages/gatsaeng-os/src/lib/llm-governor.ts +167 -0
  155. package/packages/gatsaeng-os/src/lib/neuroscience/energyCycle.ts +35 -0
  156. package/packages/gatsaeng-os/src/lib/neuroscience/habitStack.ts +22 -0
  157. package/packages/gatsaeng-os/src/lib/neuroscience/scoring.ts +32 -0
  158. package/packages/gatsaeng-os/src/lib/routes.ts +15 -0
  159. package/packages/gatsaeng-os/src/lib/utils.ts +6 -0
  160. package/packages/gatsaeng-os/src/lib/vault/config.ts +29 -0
  161. package/packages/gatsaeng-os/src/lib/vault/frontmatter.ts +84 -0
  162. package/packages/gatsaeng-os/src/lib/vault/index.ts +180 -0
  163. package/packages/gatsaeng-os/src/lib/vault/schemas.ts +274 -0
  164. package/packages/gatsaeng-os/src/middleware.ts +34 -0
  165. package/packages/gatsaeng-os/src/stores/dashboardStore.ts +26 -0
  166. package/packages/gatsaeng-os/src/stores/favoritesStore.ts +47 -0
  167. package/packages/gatsaeng-os/src/stores/timerStore.ts +65 -0
  168. package/packages/gatsaeng-os/src/types/index.ts +320 -0
  169. package/packages/gatsaeng-os/tsconfig.json +34 -0
  170. package/templates/scripts/forge_qa.sh.tmpl +237 -0
  171. package/templates/scripts/forge_ship.sh.tmpl +183 -0
  172. package/templates/scripts/session_indexer.py.tmpl +420 -0
  173. package/templates/scripts/tracer.py.tmpl +266 -0
  174. package/templates/workspace/AGENTS.md.tmpl +190 -0
  175. package/templates/workspace/BOOTSTRAP.md.tmpl +27 -0
  176. package/templates/workspace/HEARTBEAT.md.tmpl +23 -0
  177. package/templates/workspace/MEMORY.md.tmpl +35 -0
  178. package/templates/workspace/SOUL.md.tmpl +258 -0
  179. package/templates/workspace/TOOLS.md.tmpl +28 -0
  180. package/templates/workspace/USER.md.tmpl +43 -0
@@ -0,0 +1,81 @@
1
+ 'use client'
2
+
3
+ import { WidgetWrapper } from './WidgetWrapper'
4
+ import { Checkbox } from '@/components/ui/checkbox'
5
+ import { Badge } from '@/components/ui/badge'
6
+ import { RotateCcw } from 'lucide-react'
7
+ import { useRoutines, useToggleRoutine } from '@/hooks/useRoutines'
8
+ import { cn } from '@/lib/utils'
9
+ import type { RoutineWithStatus } from '@/types'
10
+
11
+ export function RoutineChecklist() {
12
+ const { chains, isLoading } = useRoutines()
13
+ const toggleMutation = useToggleRoutine()
14
+
15
+ const handleToggle = (routine: RoutineWithStatus) => {
16
+ toggleMutation.mutate({
17
+ routineId: routine.id,
18
+ completed: routine.completed_today,
19
+ })
20
+ }
21
+
22
+ if (isLoading) {
23
+ return (
24
+ <WidgetWrapper title="오늘의 루틴" icon={<RotateCcw className="w-4 h-4" />}>
25
+ <div className="space-y-3">
26
+ {[1, 2, 3].map(i => (
27
+ <div key={i} className="h-8 bg-muted rounded animate-pulse" />
28
+ ))}
29
+ </div>
30
+ </WidgetWrapper>
31
+ )
32
+ }
33
+
34
+ return (
35
+ <WidgetWrapper title="오늘의 루틴" icon={<RotateCcw className="w-4 h-4" />}>
36
+ {chains.length === 0 ? (
37
+ <p className="text-sm text-muted-foreground">루틴을 추가해보세요</p>
38
+ ) : (
39
+ <div className="space-y-4">
40
+ {chains.map((chain) => (
41
+ <div key={chain.map(r => r.id).join('-')} className="space-y-1">
42
+ {chain.map((routine, i) => (
43
+ <div
44
+ key={routine.id}
45
+ className={cn(
46
+ 'flex items-center gap-3 px-3 py-2 rounded-md transition-colors',
47
+ routine.completed_today ? 'bg-gatsaeng-teal/10' : 'hover:bg-card'
48
+ )}
49
+ >
50
+ {i > 0 && (
51
+ <div className="w-4 flex justify-center">
52
+ <div className="w-px h-3 bg-border -mt-4" />
53
+ </div>
54
+ )}
55
+ <Checkbox
56
+ checked={routine.completed_today}
57
+ onCheckedChange={() => handleToggle(routine)}
58
+ />
59
+ <span className={cn(
60
+ 'text-sm flex-1',
61
+ routine.completed_today && 'line-through text-muted-foreground'
62
+ )}>
63
+ {routine.title}
64
+ </span>
65
+ {routine.trigger_cue && (
66
+ <span className="text-xs text-muted-foreground">{routine.trigger_cue}</span>
67
+ )}
68
+ {routine.streak > 0 && (
69
+ <Badge variant="outline" className="text-gatsaeng-amber border-gatsaeng-amber/30 text-xs">
70
+ {routine.streak}일
71
+ </Badge>
72
+ )}
73
+ </div>
74
+ ))}
75
+ </div>
76
+ ))}
77
+ </div>
78
+ )}
79
+ </WidgetWrapper>
80
+ )
81
+ }
@@ -0,0 +1,86 @@
1
+ 'use client'
2
+
3
+ import { useCurrentTiming } from '@/hooks/useTiming'
4
+ import { WidgetWrapper } from './WidgetWrapper'
5
+ import { Compass } from 'lucide-react'
6
+ import { Badge } from '@/components/ui/badge'
7
+
8
+ const RATING_LABELS = ['', '주의', '신중', '보통', '유리', '최적']
9
+ const RATING_COLORS = ['', 'text-gatsaeng-red', 'text-gatsaeng-amber', 'text-muted-foreground', 'text-gatsaeng-teal', 'text-primary']
10
+
11
+ export function TimingWidget() {
12
+ const { data: timing, isLoading } = useCurrentTiming()
13
+
14
+ if (isLoading) {
15
+ return (
16
+ <WidgetWrapper title="이달의 운기" icon={<Compass className="w-4 h-4 text-gatsaeng-purple" />}>
17
+ <div className="h-24 bg-muted/30 rounded animate-pulse" />
18
+ </WidgetWrapper>
19
+ )
20
+ }
21
+
22
+ if (!timing) {
23
+ return (
24
+ <WidgetWrapper title="이달의 운기" icon={<Compass className="w-4 h-4 text-gatsaeng-purple" />}>
25
+ <p className="text-sm text-muted-foreground">타이밍 데이터가 없습니다.</p>
26
+ </WidgetWrapper>
27
+ )
28
+ }
29
+
30
+ return (
31
+ <WidgetWrapper title="이달의 운기" icon={<Compass className="w-4 h-4 text-gatsaeng-purple" />}>
32
+ <div className="space-y-3">
33
+ {/* Header: pillar + rating */}
34
+ <div className="flex items-center justify-between">
35
+ <div className="flex items-center gap-2">
36
+ <span className="text-lg font-bold">{timing.pillar}</span>
37
+ <span className="text-xs text-muted-foreground">
38
+ {timing.heavenly_stem} {timing.earthly_branch}
39
+ </span>
40
+ </div>
41
+ <Badge
42
+ variant="outline"
43
+ className={`${RATING_COLORS[timing.rating] ?? ''} border-current`}
44
+ >
45
+ {RATING_LABELS[timing.rating] ?? '?'} ({timing.rating}/5)
46
+ </Badge>
47
+ </div>
48
+
49
+ {/* Theme */}
50
+ <div className="text-sm">
51
+ <span className="text-muted-foreground">테마:</span>{' '}
52
+ <span className="text-foreground font-medium">{timing.theme}</span>
53
+ </div>
54
+
55
+ {/* Insight */}
56
+ <p className="text-xs text-muted-foreground leading-relaxed">{timing.insight}</p>
57
+
58
+ {/* Action guide */}
59
+ {timing.action_guide?.length > 0 && (
60
+ <div className="space-y-1">
61
+ <div className="text-[10px] uppercase tracking-wider text-muted-foreground">실행 가이드</div>
62
+ {timing.action_guide.map((a, i) => (
63
+ <div key={i} className="text-xs text-foreground flex items-start gap-1.5">
64
+ <span className="text-gatsaeng-teal mt-0.5">•</span>
65
+ {a}
66
+ </div>
67
+ ))}
68
+ </div>
69
+ )}
70
+
71
+ {/* Caution */}
72
+ {(timing.caution?.length ?? 0) > 0 && (
73
+ <div className="space-y-1">
74
+ <div className="text-[10px] uppercase tracking-wider text-gatsaeng-amber">주의</div>
75
+ {timing.caution!.map((c, i) => (
76
+ <div key={i} className="text-xs text-muted-foreground flex items-start gap-1.5">
77
+ <span className="text-gatsaeng-amber mt-0.5">!</span>
78
+ {c}
79
+ </div>
80
+ ))}
81
+ </div>
82
+ )}
83
+ </div>
84
+ </WidgetWrapper>
85
+ )
86
+ }
@@ -0,0 +1,95 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { useDashboardStore } from '@/stores/dashboardStore'
5
+ import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'
6
+ import { Button } from '@/components/ui/button'
7
+ import { Checkbox } from '@/components/ui/checkbox'
8
+ import { Settings2, GripVertical } from 'lucide-react'
9
+ import type { WidgetId } from '@/types'
10
+
11
+ const WIDGET_INFO: Partial<Record<WidgetId, { label: string; description: string }>> = {
12
+ routine: { label: '루틴 체크리스트', description: '오늘의 습관 체인' },
13
+ goals: { label: '목표 링', description: '목표별 진행률 링 차트' },
14
+ heatmap: { label: '활동 히트맵', description: '일간 활동 기록' },
15
+ timer: { label: '포커스 타이머', description: '집중 세션 타이머' },
16
+ zeigarnik: { label: 'Zeigarnik 패널', description: '미완료 항목 알림 바' },
17
+ energy: { label: '에너지 트래커', description: '시간대별 에너지 기록' },
18
+ kanban: { label: '퀵 칸반', description: '태스크 칸반 미니뷰' },
19
+ dday: { label: 'D-day 현황', description: '마일스톤 D-day 카운터' },
20
+ proactive: { label: '코칭 알림', description: '사주 + 갓생 패턴 감지 알림' },
21
+ }
22
+
23
+ export function WidgetCustomizer() {
24
+ const { activeWidgets, toggleWidget, reorderWidgets } = useDashboardStore()
25
+ const [open, setOpen] = useState(false)
26
+ const [dragItem, setDragItem] = useState<number | null>(null)
27
+
28
+ const allWidgets: WidgetId[] = ['proactive', 'zeigarnik', 'routine', 'goals', 'timer', 'energy', 'heatmap', 'kanban', 'dday']
29
+
30
+ const handleDragStart = (index: number) => {
31
+ setDragItem(index)
32
+ }
33
+
34
+ const handleDragOver = (e: React.DragEvent, index: number) => {
35
+ e.preventDefault()
36
+ if (dragItem === null || dragItem === index) return
37
+
38
+ const newOrder = [...activeWidgets]
39
+ const [removed] = newOrder.splice(dragItem, 1)
40
+ newOrder.splice(index, 0, removed)
41
+ reorderWidgets(newOrder)
42
+ setDragItem(index)
43
+ }
44
+
45
+ return (
46
+ <Sheet open={open} onOpenChange={setOpen}>
47
+ <SheetTrigger asChild>
48
+ <Button variant="outline" size="sm" className="gap-1.5">
49
+ <Settings2 className="w-3.5 h-3.5" />
50
+ <span className="hidden sm:inline">커스터마이즈</span>
51
+ </Button>
52
+ </SheetTrigger>
53
+ <SheetContent>
54
+ <SheetHeader>
55
+ <SheetTitle>위젯 설정</SheetTitle>
56
+ </SheetHeader>
57
+ <div className="mt-6 space-y-1">
58
+ <p className="text-xs text-muted-foreground mb-3">위젯을 켜고/끄거나 드래그하여 순서를 변경하세요</p>
59
+
60
+ {allWidgets.map((widgetId, index) => {
61
+ const info = WIDGET_INFO[widgetId]
62
+ const isActive = activeWidgets.includes(widgetId)
63
+ const activeIndex = activeWidgets.indexOf(widgetId)
64
+
65
+ return (
66
+ <div
67
+ key={widgetId}
68
+ draggable={isActive}
69
+ onDragStart={() => isActive && handleDragStart(activeIndex)}
70
+ onDragOver={(e) => isActive && handleDragOver(e, activeIndex)}
71
+ onDragEnd={() => setDragItem(null)}
72
+ className={`flex items-center gap-3 p-3 rounded-md transition-colors ${
73
+ isActive ? 'bg-card' : 'opacity-60'
74
+ } ${dragItem === activeIndex ? 'opacity-50' : ''}`}
75
+ >
76
+ {isActive && (
77
+ <GripVertical className="w-3.5 h-3.5 text-muted-foreground cursor-grab flex-shrink-0" />
78
+ )}
79
+ {!isActive && <div className="w-3.5" />}
80
+ <Checkbox
81
+ checked={isActive}
82
+ onCheckedChange={() => toggleWidget(widgetId)}
83
+ />
84
+ <div className="flex-1 min-w-0">
85
+ <div className="text-sm font-medium">{info?.label}</div>
86
+ <div className="text-[10px] text-muted-foreground">{info?.description}</div>
87
+ </div>
88
+ </div>
89
+ )
90
+ })}
91
+ </div>
92
+ </SheetContent>
93
+ </Sheet>
94
+ )
95
+ }
@@ -0,0 +1,33 @@
1
+ 'use client'
2
+
3
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
4
+
5
+ interface WidgetWrapperProps {
6
+ title: string
7
+ icon?: React.ReactNode
8
+ children: React.ReactNode
9
+ className?: string
10
+ widgetId?: string
11
+ /** When true, renders without Card wrapper (used inside cockpit grid which has its own frame) */
12
+ compact?: boolean
13
+ }
14
+
15
+ export function WidgetWrapper({ title, icon, children, className, widgetId, compact }: WidgetWrapperProps) {
16
+ if (compact) {
17
+ return <div className={className}>{children}</div>
18
+ }
19
+
20
+ return (
21
+ <Card className={className} id={widgetId ? `widget-${widgetId}` : undefined}>
22
+ <CardHeader className="pb-3">
23
+ <CardTitle className="text-sm font-medium flex items-center gap-2">
24
+ {icon}
25
+ {title}
26
+ </CardTitle>
27
+ </CardHeader>
28
+ <CardContent>
29
+ {children}
30
+ </CardContent>
31
+ </Card>
32
+ )
33
+ }
@@ -0,0 +1,43 @@
1
+ 'use client'
2
+
3
+ import { AlertCircle } from 'lucide-react'
4
+ import { useRoutines } from '@/hooks/useRoutines'
5
+ import { useTasks } from '@/hooks/useProjects'
6
+
7
+ export function ZeigarnikPanel() {
8
+ const { routines } = useRoutines()
9
+ const { data: allTasks } = useTasks()
10
+
11
+ const incompleteRoutines = routines.filter(r => !r.completed_today)
12
+ const urgentTasks = (allTasks ?? []).filter(
13
+ t => t.status !== 'done' && (t.priority === 'urgent' || t.priority === 'high')
14
+ )
15
+
16
+ const totalIncomplete = incompleteRoutines.length + urgentTasks.length
17
+
18
+ if (totalIncomplete === 0) {
19
+ return (
20
+ <div className="w-full flex items-center justify-center p-4 text-center">
21
+ <div>
22
+ <div className="text-2xl mb-1">&#10003;</div>
23
+ <p className="text-xs text-muted-foreground">모두 완료!</p>
24
+ </div>
25
+ </div>
26
+ )
27
+ }
28
+
29
+ return (
30
+ <div className="w-full bg-gatsaeng-red/10 border border-gatsaeng-red/30 rounded-lg px-4 py-3 flex items-center gap-3">
31
+ <AlertCircle className="w-4 h-4 text-gatsaeng-red shrink-0" />
32
+ <span className="text-sm text-foreground">
33
+ <strong className="text-gatsaeng-red">{totalIncomplete}개</strong>의 미완성 항목이 기다리고 있습니다
34
+ {incompleteRoutines.length > 0 && (
35
+ <span className="text-muted-foreground"> — 루틴 {incompleteRoutines.length}개</span>
36
+ )}
37
+ {urgentTasks.length > 0 && (
38
+ <span className="text-muted-foreground"> — 긴급 태스크 {urgentTasks.length}개</span>
39
+ )}
40
+ </span>
41
+ </div>
42
+ )
43
+ }
@@ -0,0 +1,186 @@
1
+ 'use client'
2
+
3
+ import type { Editor } from '@tiptap/react'
4
+ import {
5
+ Bold,
6
+ Italic,
7
+ Strikethrough,
8
+ Heading1,
9
+ Heading2,
10
+ Heading3,
11
+ List,
12
+ ListOrdered,
13
+ ListChecks,
14
+ Code,
15
+ Link,
16
+ Minus,
17
+ Undo,
18
+ Redo,
19
+ } from 'lucide-react'
20
+ import { cn } from '@/lib/utils'
21
+ import { useCallback } from 'react'
22
+
23
+ interface EditorToolbarProps {
24
+ editor: Editor | null
25
+ }
26
+
27
+ function ToolbarButton({
28
+ onClick,
29
+ isActive,
30
+ children,
31
+ title,
32
+ }: {
33
+ onClick: () => void
34
+ isActive?: boolean
35
+ children: React.ReactNode
36
+ title: string
37
+ }) {
38
+ return (
39
+ <button
40
+ type="button"
41
+ onClick={onClick}
42
+ title={title}
43
+ className={cn(
44
+ 'p-1.5 rounded-md transition-colors',
45
+ isActive
46
+ ? 'bg-primary/10 text-primary'
47
+ : 'text-muted-foreground hover:text-foreground hover:bg-muted'
48
+ )}
49
+ >
50
+ {children}
51
+ </button>
52
+ )
53
+ }
54
+
55
+ function Separator() {
56
+ return <div className="w-px h-5 bg-border mx-1" />
57
+ }
58
+
59
+ export function EditorToolbar({ editor }: EditorToolbarProps) {
60
+ const setLink = useCallback(() => {
61
+ if (!editor) return
62
+ const prev = editor.getAttributes('link').href
63
+ const url = window.prompt('URL', prev ?? '')
64
+ if (url === null) return
65
+ if (url === '') {
66
+ editor.chain().focus().extendMarkRange('link').unsetLink().run()
67
+ return
68
+ }
69
+ editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
70
+ }, [editor])
71
+
72
+ if (!editor) return null
73
+
74
+ const iconSize = 'w-4 h-4'
75
+
76
+ return (
77
+ <div className="flex items-center gap-0.5 flex-wrap px-3 py-2 border-b border-border bg-muted/30">
78
+ {/* Undo/Redo */}
79
+ <ToolbarButton onClick={() => editor.chain().focus().undo().run()} title="Undo">
80
+ <Undo className={iconSize} />
81
+ </ToolbarButton>
82
+ <ToolbarButton onClick={() => editor.chain().focus().redo().run()} title="Redo">
83
+ <Redo className={iconSize} />
84
+ </ToolbarButton>
85
+
86
+ <Separator />
87
+
88
+ {/* Text formatting */}
89
+ <ToolbarButton
90
+ onClick={() => editor.chain().focus().toggleBold().run()}
91
+ isActive={editor.isActive('bold')}
92
+ title="Bold"
93
+ >
94
+ <Bold className={iconSize} />
95
+ </ToolbarButton>
96
+ <ToolbarButton
97
+ onClick={() => editor.chain().focus().toggleItalic().run()}
98
+ isActive={editor.isActive('italic')}
99
+ title="Italic"
100
+ >
101
+ <Italic className={iconSize} />
102
+ </ToolbarButton>
103
+ <ToolbarButton
104
+ onClick={() => editor.chain().focus().toggleStrike().run()}
105
+ isActive={editor.isActive('strike')}
106
+ title="Strikethrough"
107
+ >
108
+ <Strikethrough className={iconSize} />
109
+ </ToolbarButton>
110
+ <ToolbarButton
111
+ onClick={() => editor.chain().focus().toggleCode().run()}
112
+ isActive={editor.isActive('code')}
113
+ title="Inline Code"
114
+ >
115
+ <Code className={iconSize} />
116
+ </ToolbarButton>
117
+
118
+ <Separator />
119
+
120
+ {/* Headings */}
121
+ <ToolbarButton
122
+ onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
123
+ isActive={editor.isActive('heading', { level: 1 })}
124
+ title="Heading 1"
125
+ >
126
+ <Heading1 className={iconSize} />
127
+ </ToolbarButton>
128
+ <ToolbarButton
129
+ onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
130
+ isActive={editor.isActive('heading', { level: 2 })}
131
+ title="Heading 2"
132
+ >
133
+ <Heading2 className={iconSize} />
134
+ </ToolbarButton>
135
+ <ToolbarButton
136
+ onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
137
+ isActive={editor.isActive('heading', { level: 3 })}
138
+ title="Heading 3"
139
+ >
140
+ <Heading3 className={iconSize} />
141
+ </ToolbarButton>
142
+
143
+ <Separator />
144
+
145
+ {/* Lists */}
146
+ <ToolbarButton
147
+ onClick={() => editor.chain().focus().toggleBulletList().run()}
148
+ isActive={editor.isActive('bulletList')}
149
+ title="Bullet List"
150
+ >
151
+ <List className={iconSize} />
152
+ </ToolbarButton>
153
+ <ToolbarButton
154
+ onClick={() => editor.chain().focus().toggleOrderedList().run()}
155
+ isActive={editor.isActive('orderedList')}
156
+ title="Ordered List"
157
+ >
158
+ <ListOrdered className={iconSize} />
159
+ </ToolbarButton>
160
+ <ToolbarButton
161
+ onClick={() => editor.chain().focus().toggleTaskList().run()}
162
+ isActive={editor.isActive('taskList')}
163
+ title="Task List"
164
+ >
165
+ <ListChecks className={iconSize} />
166
+ </ToolbarButton>
167
+
168
+ <Separator />
169
+
170
+ {/* Link & HR */}
171
+ <ToolbarButton
172
+ onClick={setLink}
173
+ isActive={editor.isActive('link')}
174
+ title="Link"
175
+ >
176
+ <Link className={iconSize} />
177
+ </ToolbarButton>
178
+ <ToolbarButton
179
+ onClick={() => editor.chain().focus().setHorizontalRule().run()}
180
+ title="Horizontal Rule"
181
+ >
182
+ <Minus className={iconSize} />
183
+ </ToolbarButton>
184
+ </div>
185
+ )
186
+ }
@@ -0,0 +1,114 @@
1
+ 'use client'
2
+
3
+ import { useEditor, EditorContent } from '@tiptap/react'
4
+ import StarterKit from '@tiptap/starter-kit'
5
+ import Placeholder from '@tiptap/extension-placeholder'
6
+ import LinkExtension from '@tiptap/extension-link'
7
+ import TaskList from '@tiptap/extension-task-list'
8
+ import TaskItem from '@tiptap/extension-task-item'
9
+ import { useEffect, useRef, useCallback } from 'react'
10
+ import { EditorToolbar } from './EditorToolbar'
11
+ import { markdownToHtml, htmlToMarkdown } from '@/lib/editor/markdown'
12
+
13
+ interface TiptapEditorProps {
14
+ content: string // markdown string from vault
15
+ onSave: (markdown: string) => void
16
+ placeholder?: string
17
+ autoSaveMs?: number // debounce ms, 0 = manual only
18
+ className?: string
19
+ readOnly?: boolean
20
+ }
21
+
22
+ export function TiptapEditor({
23
+ content,
24
+ onSave,
25
+ placeholder = '내용을 입력하세요...',
26
+ autoSaveMs = 3000,
27
+ className,
28
+ readOnly = false,
29
+ }: TiptapEditorProps) {
30
+ const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
31
+ const lastSavedRef = useRef(content)
32
+ const isInitialRef = useRef(true)
33
+
34
+ const handleSave = useCallback(
35
+ (html: string) => {
36
+ const md = htmlToMarkdown(html)
37
+ if (md !== lastSavedRef.current) {
38
+ lastSavedRef.current = md
39
+ onSave(md)
40
+ }
41
+ },
42
+ [onSave]
43
+ )
44
+
45
+ const editor = useEditor({
46
+ extensions: [
47
+ StarterKit.configure({
48
+ heading: { levels: [1, 2, 3] },
49
+ }),
50
+ Placeholder.configure({ placeholder }),
51
+ LinkExtension.configure({
52
+ openOnClick: false,
53
+ HTMLAttributes: { class: 'text-primary underline cursor-pointer' },
54
+ }),
55
+ TaskList,
56
+ TaskItem.configure({ nested: true }),
57
+ ],
58
+ content: markdownToHtml(content),
59
+ editable: !readOnly,
60
+ editorProps: {
61
+ attributes: {
62
+ class:
63
+ 'prose prose-sm dark:prose-invert max-w-none focus:outline-none min-h-[200px] px-4 py-3',
64
+ },
65
+ },
66
+ onUpdate: ({ editor: ed }) => {
67
+ if (isInitialRef.current) {
68
+ isInitialRef.current = false
69
+ return
70
+ }
71
+ if (autoSaveMs > 0) {
72
+ if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current)
73
+ saveTimeoutRef.current = setTimeout(() => {
74
+ handleSave(ed.getHTML())
75
+ }, autoSaveMs)
76
+ }
77
+ },
78
+ onBlur: ({ editor: ed }) => {
79
+ // save immediately on blur
80
+ if (saveTimeoutRef.current) {
81
+ clearTimeout(saveTimeoutRef.current)
82
+ saveTimeoutRef.current = null
83
+ }
84
+ handleSave(ed.getHTML())
85
+ },
86
+ })
87
+
88
+ // Update content when prop changes externally
89
+ useEffect(() => {
90
+ if (editor && content !== lastSavedRef.current) {
91
+ const html = markdownToHtml(content)
92
+ const currentHtml = editor.getHTML()
93
+ if (html !== currentHtml) {
94
+ isInitialRef.current = true
95
+ editor.commands.setContent(html)
96
+ lastSavedRef.current = content
97
+ }
98
+ }
99
+ }, [content, editor])
100
+
101
+ // Cleanup timeout on unmount
102
+ useEffect(() => {
103
+ return () => {
104
+ if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current)
105
+ }
106
+ }, [])
107
+
108
+ return (
109
+ <div className={className}>
110
+ {!readOnly && <EditorToolbar editor={editor} />}
111
+ <EditorContent editor={editor} />
112
+ </div>
113
+ )
114
+ }
@@ -0,0 +1,47 @@
1
+ 'use client'
2
+
3
+ import Link from 'next/link'
4
+ import { usePathname } from 'next/navigation'
5
+ import { cn } from '@/lib/utils'
6
+ import { MobileSidebar } from './MobileSidebar'
7
+
8
+ const NAV_ITEMS = [
9
+ { href: '/', label: '대시보드' },
10
+ { href: '/goals', label: '목표' },
11
+ { href: '/projects', label: '프로젝트' },
12
+ { href: '/routines', label: '루틴' },
13
+ { href: '/review', label: '회고' },
14
+ ]
15
+
16
+ export function Header() {
17
+ const pathname = usePathname()
18
+
19
+ return (
20
+ <header className="flex items-center justify-between px-4 md:px-6 py-3 border-b border-border bg-card dark:bg-[#010409]">
21
+ <div className="flex items-center gap-3">
22
+ <MobileSidebar />
23
+ <div className="font-mono font-bold text-lg flex items-center tracking-wider">
24
+ <span className="text-primary mr-1.5">GS</span>
25
+ <span className="text-foreground text-[11px] font-semibold border-l border-border pl-3 uppercase tracking-[0.2em] hidden sm:inline">
26
+ GATSAENG OS
27
+ </span>
28
+ </div>
29
+ </div>
30
+ <nav className="hidden md:flex gap-8 text-sm font-medium">
31
+ {NAV_ITEMS.map(item => (
32
+ <Link
33
+ key={item.href}
34
+ href={item.href}
35
+ className={cn(
36
+ 'text-muted-foreground hover:text-primary transition-colors py-1',
37
+ pathname === item.href && 'text-primary border-b-2 border-primary'
38
+ )}
39
+ >
40
+ {item.label}
41
+ </Link>
42
+ ))}
43
+ </nav>
44
+ <div className="w-8 md:w-48" />
45
+ </header>
46
+ )
47
+ }