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.
- package/README.md +77 -0
- package/bin/arete.js +156 -0
- package/bin/create.js +111 -0
- package/lib/install-openclaw.js +50 -0
- package/lib/scaffold.js +213 -0
- package/lib/setup-wizard.js +88 -0
- package/lib/updater.js +130 -0
- package/package.json +34 -0
- package/packages/gatsaeng-os/README.md +36 -0
- package/packages/gatsaeng-os/components.json +23 -0
- package/packages/gatsaeng-os/eslint.config.mjs +18 -0
- package/packages/gatsaeng-os/next.config.ts +7 -0
- package/packages/gatsaeng-os/package.json +59 -0
- package/packages/gatsaeng-os/postcss.config.mjs +7 -0
- package/packages/gatsaeng-os/public/file.svg +1 -0
- package/packages/gatsaeng-os/public/globe.svg +1 -0
- package/packages/gatsaeng-os/public/next.svg +1 -0
- package/packages/gatsaeng-os/public/vercel.svg +1 -0
- package/packages/gatsaeng-os/public/window.svg +1 -0
- package/packages/gatsaeng-os/python/api_server.py +248 -0
- package/packages/gatsaeng-os/python/briefing.py +145 -0
- package/packages/gatsaeng-os/python/config.py +55 -0
- package/packages/gatsaeng-os/python/goal_context_agent.py +193 -0
- package/packages/gatsaeng-os/python/gyeokguk.py +171 -0
- package/packages/gatsaeng-os/python/proactive.py +158 -0
- package/packages/gatsaeng-os/python/requirements.txt +11 -0
- package/packages/gatsaeng-os/python/run.py +28 -0
- package/packages/gatsaeng-os/python/scoring.py +44 -0
- package/packages/gatsaeng-os/python/streak.py +70 -0
- package/packages/gatsaeng-os/python/telegram_bot.py +331 -0
- package/packages/gatsaeng-os/python/timing_engine.py +117 -0
- package/packages/gatsaeng-os/python/vault_io.py +423 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/areas/[id]/page.tsx +215 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/areas/page.tsx +161 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/books/[id]/page.tsx +215 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/books/page.tsx +268 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/calendar/page.tsx +379 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/error.tsx +30 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/focus/page.tsx +293 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/goals/[id]/page.tsx +426 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/goals/page.tsx +178 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/layout.tsx +29 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/notes/[id]/page.tsx +147 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/notes/page.tsx +254 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/page.tsx +26 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/projects/[id]/page.tsx +86 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/projects/page.tsx +215 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/review/page.tsx +475 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/routines/page.tsx +436 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/tasks/[id]/page.tsx +210 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/tasks/page.tsx +307 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/voice/page.tsx +212 -0
- package/packages/gatsaeng-os/src/app/api/areas/[id]/route.ts +26 -0
- package/packages/gatsaeng-os/src/app/api/areas/route.ts +22 -0
- package/packages/gatsaeng-os/src/app/api/auth/login/route.ts +52 -0
- package/packages/gatsaeng-os/src/app/api/auth/logout/route.ts +8 -0
- package/packages/gatsaeng-os/src/app/api/books/[id]/route.ts +27 -0
- package/packages/gatsaeng-os/src/app/api/books/route.ts +20 -0
- package/packages/gatsaeng-os/src/app/api/calendar/[id]/route.ts +24 -0
- package/packages/gatsaeng-os/src/app/api/calendar/import/route.ts +52 -0
- package/packages/gatsaeng-os/src/app/api/calendar/route.ts +37 -0
- package/packages/gatsaeng-os/src/app/api/daily/route.ts +51 -0
- package/packages/gatsaeng-os/src/app/api/goals/[id]/route.ts +34 -0
- package/packages/gatsaeng-os/src/app/api/goals/route.ts +30 -0
- package/packages/gatsaeng-os/src/app/api/logs/energy/route.ts +40 -0
- package/packages/gatsaeng-os/src/app/api/logs/focus/route.ts +22 -0
- package/packages/gatsaeng-os/src/app/api/logs/routine/route.ts +54 -0
- package/packages/gatsaeng-os/src/app/api/milestones/[id]/route.ts +26 -0
- package/packages/gatsaeng-os/src/app/api/milestones/route.ts +47 -0
- package/packages/gatsaeng-os/src/app/api/notes/[id]/route.ts +29 -0
- package/packages/gatsaeng-os/src/app/api/notes/route.ts +37 -0
- package/packages/gatsaeng-os/src/app/api/profile/route.ts +17 -0
- package/packages/gatsaeng-os/src/app/api/projects/[id]/route.ts +27 -0
- package/packages/gatsaeng-os/src/app/api/projects/route.ts +25 -0
- package/packages/gatsaeng-os/src/app/api/reviews/[id]/route.ts +26 -0
- package/packages/gatsaeng-os/src/app/api/reviews/route.ts +29 -0
- package/packages/gatsaeng-os/src/app/api/routines/[id]/route.ts +26 -0
- package/packages/gatsaeng-os/src/app/api/routines/route.ts +28 -0
- package/packages/gatsaeng-os/src/app/api/tasks/[id]/route.ts +28 -0
- package/packages/gatsaeng-os/src/app/api/tasks/route.ts +66 -0
- package/packages/gatsaeng-os/src/app/api/timing/current/route.ts +63 -0
- package/packages/gatsaeng-os/src/app/api/voice/chat/route.ts +50 -0
- package/packages/gatsaeng-os/src/app/api/voice/transcribe/route.ts +25 -0
- package/packages/gatsaeng-os/src/app/api/voice/tts/route.ts +36 -0
- package/packages/gatsaeng-os/src/app/error.tsx +30 -0
- package/packages/gatsaeng-os/src/app/favicon.ico +0 -0
- package/packages/gatsaeng-os/src/app/globals.css +208 -0
- package/packages/gatsaeng-os/src/app/layout.tsx +33 -0
- package/packages/gatsaeng-os/src/app/login/page.tsx +87 -0
- package/packages/gatsaeng-os/src/app/providers.tsx +27 -0
- package/packages/gatsaeng-os/src/components/ErrorBoundary.tsx +46 -0
- package/packages/gatsaeng-os/src/components/dashboard/DashboardGrid.tsx +86 -0
- package/packages/gatsaeng-os/src/components/dashboard/DdayWidget.tsx +88 -0
- package/packages/gatsaeng-os/src/components/dashboard/EnergyTracker.tsx +87 -0
- package/packages/gatsaeng-os/src/components/dashboard/FocusTimer.tsx +139 -0
- package/packages/gatsaeng-os/src/components/dashboard/GatsaengScore.tsx +30 -0
- package/packages/gatsaeng-os/src/components/dashboard/GoalRings.tsx +107 -0
- package/packages/gatsaeng-os/src/components/dashboard/ProactiveBar.tsx +98 -0
- package/packages/gatsaeng-os/src/components/dashboard/RoutineChecklist.tsx +81 -0
- package/packages/gatsaeng-os/src/components/dashboard/TimingWidget.tsx +86 -0
- package/packages/gatsaeng-os/src/components/dashboard/WidgetCustomizer.tsx +95 -0
- package/packages/gatsaeng-os/src/components/dashboard/WidgetWrapper.tsx +33 -0
- package/packages/gatsaeng-os/src/components/dashboard/ZeigarnikPanel.tsx +43 -0
- package/packages/gatsaeng-os/src/components/editor/EditorToolbar.tsx +186 -0
- package/packages/gatsaeng-os/src/components/editor/TiptapEditor.tsx +114 -0
- package/packages/gatsaeng-os/src/components/layout/Header.tsx +47 -0
- package/packages/gatsaeng-os/src/components/layout/MobileBottomNav.tsx +122 -0
- package/packages/gatsaeng-os/src/components/layout/MobileSidebar.tsx +29 -0
- package/packages/gatsaeng-os/src/components/layout/Sidebar.tsx +142 -0
- package/packages/gatsaeng-os/src/components/onboarding/OnboardingFlow.tsx +229 -0
- package/packages/gatsaeng-os/src/components/onboarding/OnboardingGate.tsx +78 -0
- package/packages/gatsaeng-os/src/components/projects/CalendarView.tsx +152 -0
- package/packages/gatsaeng-os/src/components/projects/KanbanView.tsx +180 -0
- package/packages/gatsaeng-os/src/components/projects/ListView.tsx +82 -0
- package/packages/gatsaeng-os/src/components/projects/TableView.tsx +206 -0
- package/packages/gatsaeng-os/src/components/projects/TaskCard.tsx +154 -0
- package/packages/gatsaeng-os/src/components/projects/TaskForm.tsx +128 -0
- package/packages/gatsaeng-os/src/components/projects/ViewSwitcher.tsx +40 -0
- package/packages/gatsaeng-os/src/components/search/GlobalSearch.tsx +179 -0
- package/packages/gatsaeng-os/src/components/shared/InlineEdit.tsx +77 -0
- package/packages/gatsaeng-os/src/components/shared/PinButton.tsx +42 -0
- package/packages/gatsaeng-os/src/components/tasks/DDayBadge.tsx +34 -0
- package/packages/gatsaeng-os/src/components/ui/badge.tsx +48 -0
- package/packages/gatsaeng-os/src/components/ui/button.tsx +64 -0
- package/packages/gatsaeng-os/src/components/ui/card.tsx +92 -0
- package/packages/gatsaeng-os/src/components/ui/checkbox.tsx +32 -0
- package/packages/gatsaeng-os/src/components/ui/command.tsx +184 -0
- package/packages/gatsaeng-os/src/components/ui/dialog.tsx +158 -0
- package/packages/gatsaeng-os/src/components/ui/input.tsx +21 -0
- package/packages/gatsaeng-os/src/components/ui/label.tsx +24 -0
- package/packages/gatsaeng-os/src/components/ui/popover.tsx +89 -0
- package/packages/gatsaeng-os/src/components/ui/progress.tsx +31 -0
- package/packages/gatsaeng-os/src/components/ui/select.tsx +190 -0
- package/packages/gatsaeng-os/src/components/ui/sheet.tsx +143 -0
- package/packages/gatsaeng-os/src/components/ui/tabs.tsx +91 -0
- package/packages/gatsaeng-os/src/components/ui/toggle-group.tsx +83 -0
- package/packages/gatsaeng-os/src/components/ui/toggle.tsx +47 -0
- package/packages/gatsaeng-os/src/components/ui/tooltip.tsx +57 -0
- package/packages/gatsaeng-os/src/hooks/useAreas.ts +53 -0
- package/packages/gatsaeng-os/src/hooks/useBooks.ts +62 -0
- package/packages/gatsaeng-os/src/hooks/useCalendar.ts +59 -0
- package/packages/gatsaeng-os/src/hooks/useDaily.ts +15 -0
- package/packages/gatsaeng-os/src/hooks/useGlobalTasks.ts +45 -0
- package/packages/gatsaeng-os/src/hooks/useGoals.ts +53 -0
- package/packages/gatsaeng-os/src/hooks/useMilestones.ts +75 -0
- package/packages/gatsaeng-os/src/hooks/useNotes.ts +65 -0
- package/packages/gatsaeng-os/src/hooks/useProjects.ts +102 -0
- package/packages/gatsaeng-os/src/hooks/useRoutines.ts +76 -0
- package/packages/gatsaeng-os/src/hooks/useTiming.ts +27 -0
- package/packages/gatsaeng-os/src/lib/apiFetch.ts +14 -0
- package/packages/gatsaeng-os/src/lib/auth.ts +32 -0
- package/packages/gatsaeng-os/src/lib/date.ts +7 -0
- package/packages/gatsaeng-os/src/lib/editor/markdown.ts +35 -0
- package/packages/gatsaeng-os/src/lib/llm-governor.ts +167 -0
- package/packages/gatsaeng-os/src/lib/neuroscience/energyCycle.ts +35 -0
- package/packages/gatsaeng-os/src/lib/neuroscience/habitStack.ts +22 -0
- package/packages/gatsaeng-os/src/lib/neuroscience/scoring.ts +32 -0
- package/packages/gatsaeng-os/src/lib/routes.ts +15 -0
- package/packages/gatsaeng-os/src/lib/utils.ts +6 -0
- package/packages/gatsaeng-os/src/lib/vault/config.ts +29 -0
- package/packages/gatsaeng-os/src/lib/vault/frontmatter.ts +84 -0
- package/packages/gatsaeng-os/src/lib/vault/index.ts +180 -0
- package/packages/gatsaeng-os/src/lib/vault/schemas.ts +274 -0
- package/packages/gatsaeng-os/src/middleware.ts +34 -0
- package/packages/gatsaeng-os/src/stores/dashboardStore.ts +26 -0
- package/packages/gatsaeng-os/src/stores/favoritesStore.ts +47 -0
- package/packages/gatsaeng-os/src/stores/timerStore.ts +65 -0
- package/packages/gatsaeng-os/src/types/index.ts +320 -0
- package/packages/gatsaeng-os/tsconfig.json +34 -0
- package/templates/scripts/forge_qa.sh.tmpl +237 -0
- package/templates/scripts/forge_ship.sh.tmpl +183 -0
- package/templates/scripts/session_indexer.py.tmpl +420 -0
- package/templates/scripts/tracer.py.tmpl +266 -0
- package/templates/workspace/AGENTS.md.tmpl +190 -0
- package/templates/workspace/BOOTSTRAP.md.tmpl +27 -0
- package/templates/workspace/HEARTBEAT.md.tmpl +23 -0
- package/templates/workspace/MEMORY.md.tmpl +35 -0
- package/templates/workspace/SOUL.md.tmpl +258 -0
- package/templates/workspace/TOOLS.md.tmpl +28 -0
- 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">✓</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
|
+
}
|