prjct-cli 0.10.14 → 0.11.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/CHANGELOG.md +19 -0
- package/bin/dev.js +217 -0
- package/bin/prjct +10 -0
- package/bin/serve.js +78 -0
- package/core/bus/index.js +322 -0
- package/core/command-registry.js +65 -0
- package/core/domain/snapshot-manager.js +375 -0
- package/core/plugin/hooks.js +313 -0
- package/core/plugin/index.js +52 -0
- package/core/plugin/loader.js +331 -0
- package/core/plugin/registry.js +325 -0
- package/core/plugins/webhook.js +143 -0
- package/core/session/index.js +449 -0
- package/core/session/metrics.js +293 -0
- package/package.json +28 -4
- package/packages/shared/dist/index.d.ts +615 -0
- package/packages/shared/dist/index.js +204 -0
- package/packages/shared/package.json +29 -0
- package/packages/shared/src/index.ts +9 -0
- package/packages/shared/src/schemas.ts +124 -0
- package/packages/shared/src/types.ts +187 -0
- package/packages/shared/src/utils.ts +148 -0
- package/packages/shared/tsconfig.json +18 -0
- package/packages/web/README.md +36 -0
- package/packages/web/app/api/claude/sessions/route.ts +44 -0
- package/packages/web/app/api/claude/status/route.ts +34 -0
- package/packages/web/app/api/projects/[id]/delete/route.ts +21 -0
- package/packages/web/app/api/projects/[id]/icon/route.ts +33 -0
- package/packages/web/app/api/projects/[id]/route.ts +29 -0
- package/packages/web/app/api/projects/[id]/stats/route.ts +36 -0
- package/packages/web/app/api/projects/[id]/status/route.ts +21 -0
- package/packages/web/app/api/projects/route.ts +16 -0
- package/packages/web/app/api/sessions/history/route.ts +122 -0
- package/packages/web/app/api/stats/route.ts +38 -0
- package/packages/web/app/error.tsx +34 -0
- package/packages/web/app/favicon.ico +0 -0
- package/packages/web/app/globals.css +155 -0
- package/packages/web/app/layout.tsx +43 -0
- package/packages/web/app/loading.tsx +7 -0
- package/packages/web/app/not-found.tsx +25 -0
- package/packages/web/app/page.tsx +227 -0
- package/packages/web/app/project/[id]/error.tsx +41 -0
- package/packages/web/app/project/[id]/loading.tsx +9 -0
- package/packages/web/app/project/[id]/not-found.tsx +27 -0
- package/packages/web/app/project/[id]/page.tsx +253 -0
- package/packages/web/app/project/[id]/stats/page.tsx +447 -0
- package/packages/web/app/sessions/page.tsx +165 -0
- package/packages/web/app/settings/page.tsx +150 -0
- package/packages/web/components/AppSidebar.tsx +113 -0
- package/packages/web/components/CommandButton.tsx +39 -0
- package/packages/web/components/ConnectionStatus.tsx +29 -0
- package/packages/web/components/Logo.tsx +65 -0
- package/packages/web/components/MarkdownContent.tsx +123 -0
- package/packages/web/components/ProjectAvatar.tsx +54 -0
- package/packages/web/components/TechStackBadges.tsx +20 -0
- package/packages/web/components/TerminalTab.tsx +84 -0
- package/packages/web/components/TerminalTabs.tsx +210 -0
- package/packages/web/components/charts/SessionsChart.tsx +172 -0
- package/packages/web/components/providers.tsx +45 -0
- package/packages/web/components/ui/alert-dialog.tsx +157 -0
- package/packages/web/components/ui/badge.tsx +46 -0
- package/packages/web/components/ui/button.tsx +60 -0
- package/packages/web/components/ui/card.tsx +92 -0
- package/packages/web/components/ui/chart.tsx +385 -0
- package/packages/web/components/ui/dropdown-menu.tsx +257 -0
- package/packages/web/components/ui/scroll-area.tsx +58 -0
- package/packages/web/components/ui/sheet.tsx +139 -0
- package/packages/web/components/ui/tabs.tsx +66 -0
- package/packages/web/components/ui/tooltip.tsx +61 -0
- package/packages/web/components.json +22 -0
- package/packages/web/context/TerminalContext.tsx +45 -0
- package/packages/web/context/TerminalTabsContext.tsx +136 -0
- package/packages/web/eslint.config.mjs +18 -0
- package/packages/web/hooks/useClaudeTerminal.ts +375 -0
- package/packages/web/hooks/useProjectStats.ts +38 -0
- package/packages/web/hooks/useProjects.ts +73 -0
- package/packages/web/hooks/useStats.ts +28 -0
- package/packages/web/lib/format.ts +23 -0
- package/packages/web/lib/parse-prjct-files.ts +1122 -0
- package/packages/web/lib/projects.ts +452 -0
- package/packages/web/lib/pty.ts +101 -0
- package/packages/web/lib/query-config.ts +44 -0
- package/packages/web/lib/utils.ts +6 -0
- package/packages/web/next-env.d.ts +6 -0
- package/packages/web/next.config.ts +7 -0
- package/packages/web/package.json +53 -0
- package/packages/web/postcss.config.mjs +7 -0
- package/packages/web/public/file.svg +1 -0
- package/packages/web/public/globe.svg +1 -0
- package/packages/web/public/next.svg +1 -0
- package/packages/web/public/vercel.svg +1 -0
- package/packages/web/public/window.svg +1 -0
- package/packages/web/server.ts +262 -0
- package/packages/web/tsconfig.json +34 -0
- package/templates/commands/done.md +176 -54
- package/templates/commands/history.md +176 -0
- package/templates/commands/init.md +28 -1
- package/templates/commands/now.md +191 -9
- package/templates/commands/pause.md +176 -12
- package/templates/commands/redo.md +142 -0
- package/templates/commands/resume.md +166 -62
- package/templates/commands/serve.md +121 -0
- package/templates/commands/ship.md +45 -1
- package/templates/commands/sync.md +34 -1
- package/templates/commands/undo.md +152 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState, useRef, useEffect } from 'react'
|
|
4
|
+
import { useTerminalTabs } from '@/context/TerminalTabsContext'
|
|
5
|
+
import { TerminalTab } from './TerminalTab'
|
|
6
|
+
import { Button } from '@/components/ui/button'
|
|
7
|
+
import {
|
|
8
|
+
AlertDialog,
|
|
9
|
+
AlertDialogAction,
|
|
10
|
+
AlertDialogCancel,
|
|
11
|
+
AlertDialogContent,
|
|
12
|
+
AlertDialogDescription,
|
|
13
|
+
AlertDialogFooter,
|
|
14
|
+
AlertDialogHeader,
|
|
15
|
+
AlertDialogTitle,
|
|
16
|
+
} from '@/components/ui/alert-dialog'
|
|
17
|
+
import { X, Terminal as TerminalIcon, Plus, Loader2, AlertTriangle } from 'lucide-react'
|
|
18
|
+
import { cn } from '@/lib/utils'
|
|
19
|
+
|
|
20
|
+
interface TerminalTabsProps {
|
|
21
|
+
projectDir: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function TerminalTabs({ projectDir }: TerminalTabsProps) {
|
|
25
|
+
const {
|
|
26
|
+
sessions,
|
|
27
|
+
activeSessionId,
|
|
28
|
+
createSession,
|
|
29
|
+
closeSession,
|
|
30
|
+
setActiveSession,
|
|
31
|
+
updateSession
|
|
32
|
+
} = useTerminalTabs()
|
|
33
|
+
|
|
34
|
+
const [sessionToClose, setSessionToClose] = useState<string | null>(null)
|
|
35
|
+
const [editingSessionId, setEditingSessionId] = useState<string | null>(null)
|
|
36
|
+
const [editValue, setEditValue] = useState('')
|
|
37
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
38
|
+
|
|
39
|
+
const handleCloseTab = useCallback((sessionId: string, e: React.MouseEvent) => {
|
|
40
|
+
e.stopPropagation()
|
|
41
|
+
const session = sessions.find(s => s.id === sessionId)
|
|
42
|
+
if (session?.isConnected) {
|
|
43
|
+
setSessionToClose(sessionId)
|
|
44
|
+
} else {
|
|
45
|
+
closeSession(sessionId)
|
|
46
|
+
}
|
|
47
|
+
}, [sessions, closeSession])
|
|
48
|
+
|
|
49
|
+
const handleConfirmClose = useCallback(() => {
|
|
50
|
+
if (sessionToClose) {
|
|
51
|
+
// Call disconnect on the terminal
|
|
52
|
+
const disconnectFn = (window as unknown as Record<string, () => void>)[`terminal_disconnect_${sessionToClose}`]
|
|
53
|
+
if (disconnectFn) disconnectFn()
|
|
54
|
+
closeSession(sessionToClose)
|
|
55
|
+
setSessionToClose(null)
|
|
56
|
+
}
|
|
57
|
+
}, [sessionToClose, closeSession])
|
|
58
|
+
|
|
59
|
+
const startEditing = useCallback((sessionId: string, currentLabel: string) => {
|
|
60
|
+
setEditingSessionId(sessionId)
|
|
61
|
+
setEditValue(currentLabel)
|
|
62
|
+
}, [])
|
|
63
|
+
|
|
64
|
+
const finishEditing = useCallback(() => {
|
|
65
|
+
if (editingSessionId && editValue.trim()) {
|
|
66
|
+
updateSession(editingSessionId, { label: editValue.trim() })
|
|
67
|
+
}
|
|
68
|
+
setEditingSessionId(null)
|
|
69
|
+
setEditValue('')
|
|
70
|
+
}, [editingSessionId, editValue, updateSession])
|
|
71
|
+
|
|
72
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
73
|
+
if (e.key === 'Enter') {
|
|
74
|
+
finishEditing()
|
|
75
|
+
} else if (e.key === 'Escape') {
|
|
76
|
+
setEditingSessionId(null)
|
|
77
|
+
setEditValue('')
|
|
78
|
+
}
|
|
79
|
+
}, [finishEditing])
|
|
80
|
+
|
|
81
|
+
// Focus input when editing starts
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (editingSessionId && inputRef.current) {
|
|
84
|
+
inputRef.current.focus()
|
|
85
|
+
inputRef.current.select()
|
|
86
|
+
}
|
|
87
|
+
}, [editingSessionId])
|
|
88
|
+
|
|
89
|
+
const hasNoSessions = sessions.length === 0
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div className="flex flex-col h-full">
|
|
93
|
+
{/* Tab bar */}
|
|
94
|
+
<div className="flex items-center gap-1 px-2 py-1 bg-card border-b border-border min-h-[40px]">
|
|
95
|
+
{sessions.map(session => (
|
|
96
|
+
<div
|
|
97
|
+
key={session.id}
|
|
98
|
+
onClick={() => setActiveSession(session.id)}
|
|
99
|
+
onDoubleClick={() => startEditing(session.id, session.label)}
|
|
100
|
+
role="tab"
|
|
101
|
+
tabIndex={0}
|
|
102
|
+
onKeyDown={(e) => e.key === 'Enter' && setActiveSession(session.id)}
|
|
103
|
+
className={cn(
|
|
104
|
+
'flex items-center gap-2 px-3 py-1.5 rounded-md text-sm transition-colors cursor-pointer',
|
|
105
|
+
'hover:bg-muted',
|
|
106
|
+
session.id === activeSessionId
|
|
107
|
+
? 'bg-muted text-foreground'
|
|
108
|
+
: 'text-muted-foreground'
|
|
109
|
+
)}
|
|
110
|
+
>
|
|
111
|
+
{session.isLoading ? (
|
|
112
|
+
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
113
|
+
) : session.isConnected ? (
|
|
114
|
+
<span className="relative flex h-2 w-2">
|
|
115
|
+
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
|
|
116
|
+
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500" />
|
|
117
|
+
</span>
|
|
118
|
+
) : (
|
|
119
|
+
<span className="h-2 w-2 rounded-full bg-muted-foreground/50" />
|
|
120
|
+
)}
|
|
121
|
+
{editingSessionId === session.id ? (
|
|
122
|
+
<input
|
|
123
|
+
ref={inputRef}
|
|
124
|
+
type="text"
|
|
125
|
+
value={editValue}
|
|
126
|
+
onChange={(e) => setEditValue(e.target.value)}
|
|
127
|
+
onBlur={finishEditing}
|
|
128
|
+
onKeyDown={handleKeyDown}
|
|
129
|
+
onClick={(e) => e.stopPropagation()}
|
|
130
|
+
className="w-[100px] bg-background border border-border rounded px-1 py-0.5 text-sm outline-none focus:ring-1 focus:ring-ring"
|
|
131
|
+
/>
|
|
132
|
+
) : (
|
|
133
|
+
<span className="truncate max-w-[100px]">{session.label}</span>
|
|
134
|
+
)}
|
|
135
|
+
<button
|
|
136
|
+
onClick={(e) => handleCloseTab(session.id, e)}
|
|
137
|
+
className="p-0.5 rounded hover:bg-background/50 text-muted-foreground hover:text-foreground"
|
|
138
|
+
>
|
|
139
|
+
<X className="w-3 h-3" />
|
|
140
|
+
</button>
|
|
141
|
+
</div>
|
|
142
|
+
))}
|
|
143
|
+
|
|
144
|
+
{/* New tab button */}
|
|
145
|
+
<Button
|
|
146
|
+
variant="ghost"
|
|
147
|
+
size="icon"
|
|
148
|
+
className="h-7 w-7 ml-1"
|
|
149
|
+
onClick={createSession}
|
|
150
|
+
>
|
|
151
|
+
<Plus className="w-4 h-4" />
|
|
152
|
+
</Button>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Terminal area */}
|
|
156
|
+
<div className="flex-1 relative">
|
|
157
|
+
{hasNoSessions ? (
|
|
158
|
+
<div className="absolute inset-0 flex items-center justify-center bg-background">
|
|
159
|
+
<div className="text-center">
|
|
160
|
+
<div className="w-16 h-16 rounded-full bg-muted flex items-center justify-center mx-auto mb-4">
|
|
161
|
+
<TerminalIcon className="w-8 h-8 text-muted-foreground" />
|
|
162
|
+
</div>
|
|
163
|
+
<h2 className="text-lg font-medium mb-2">No active sessions</h2>
|
|
164
|
+
<p className="text-muted-foreground text-sm mb-4">
|
|
165
|
+
Click + or press Start to create a new terminal
|
|
166
|
+
</p>
|
|
167
|
+
<Button onClick={createSession}>
|
|
168
|
+
<Plus className="w-4 h-4 mr-2" />
|
|
169
|
+
New Terminal
|
|
170
|
+
</Button>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
) : (
|
|
174
|
+
sessions.map(session => (
|
|
175
|
+
<TerminalTab
|
|
176
|
+
key={session.id}
|
|
177
|
+
session={session}
|
|
178
|
+
projectDir={projectDir}
|
|
179
|
+
isActive={session.id === activeSessionId}
|
|
180
|
+
/>
|
|
181
|
+
))
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{/* Close confirmation dialog */}
|
|
186
|
+
<AlertDialog open={!!sessionToClose} onOpenChange={(open: boolean) => !open && setSessionToClose(null)}>
|
|
187
|
+
<AlertDialogContent>
|
|
188
|
+
<AlertDialogHeader>
|
|
189
|
+
<AlertDialogTitle className="flex items-center gap-2">
|
|
190
|
+
<AlertTriangle className="w-5 h-5 text-destructive" />
|
|
191
|
+
Close Terminal?
|
|
192
|
+
</AlertDialogTitle>
|
|
193
|
+
<AlertDialogDescription>
|
|
194
|
+
This terminal has an active session. Closing it will terminate the connection.
|
|
195
|
+
</AlertDialogDescription>
|
|
196
|
+
</AlertDialogHeader>
|
|
197
|
+
<AlertDialogFooter>
|
|
198
|
+
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
199
|
+
<AlertDialogAction
|
|
200
|
+
onClick={handleConfirmClose}
|
|
201
|
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
202
|
+
>
|
|
203
|
+
Close Terminal
|
|
204
|
+
</AlertDialogAction>
|
|
205
|
+
</AlertDialogFooter>
|
|
206
|
+
</AlertDialogContent>
|
|
207
|
+
</AlertDialog>
|
|
208
|
+
</div>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { Bar, BarChart, CartesianGrid, XAxis } from 'recharts'
|
|
5
|
+
import { useQuery } from '@tanstack/react-query'
|
|
6
|
+
import {
|
|
7
|
+
Card,
|
|
8
|
+
CardContent,
|
|
9
|
+
CardDescription,
|
|
10
|
+
CardHeader,
|
|
11
|
+
CardTitle,
|
|
12
|
+
} from '@/components/ui/card'
|
|
13
|
+
import {
|
|
14
|
+
ChartConfig,
|
|
15
|
+
ChartContainer,
|
|
16
|
+
ChartTooltip,
|
|
17
|
+
ChartTooltipContent,
|
|
18
|
+
} from '@/components/ui/chart'
|
|
19
|
+
|
|
20
|
+
const chartConfig = {
|
|
21
|
+
views: {
|
|
22
|
+
label: 'Activity',
|
|
23
|
+
},
|
|
24
|
+
tasks: {
|
|
25
|
+
label: 'Tasks Completed',
|
|
26
|
+
color: 'var(--chart-1)',
|
|
27
|
+
},
|
|
28
|
+
ships: {
|
|
29
|
+
label: 'Features Shipped',
|
|
30
|
+
color: 'var(--chart-2)',
|
|
31
|
+
},
|
|
32
|
+
} satisfies ChartConfig
|
|
33
|
+
|
|
34
|
+
interface ChartDataPoint {
|
|
35
|
+
date: string
|
|
36
|
+
tasks: number
|
|
37
|
+
ships: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface SessionsHistoryResponse {
|
|
41
|
+
success: boolean
|
|
42
|
+
data: {
|
|
43
|
+
chartData: ChartDataPoint[]
|
|
44
|
+
totals: {
|
|
45
|
+
tasks: number
|
|
46
|
+
ships: number
|
|
47
|
+
}
|
|
48
|
+
dateRange: {
|
|
49
|
+
start: string
|
|
50
|
+
end: string
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function SessionsChart() {
|
|
56
|
+
const [activeChart, setActiveChart] =
|
|
57
|
+
React.useState<'tasks' | 'ships'>('tasks')
|
|
58
|
+
|
|
59
|
+
const { data: response, isLoading } = useQuery<SessionsHistoryResponse>({
|
|
60
|
+
queryKey: ['sessions-history'],
|
|
61
|
+
queryFn: async () => {
|
|
62
|
+
const res = await fetch('/api/sessions/history')
|
|
63
|
+
return res.json()
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const chartData = response?.data?.chartData || []
|
|
68
|
+
const totals = response?.data?.totals || { tasks: 0, ships: 0 }
|
|
69
|
+
|
|
70
|
+
if (isLoading) {
|
|
71
|
+
return (
|
|
72
|
+
<Card>
|
|
73
|
+
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
|
|
74
|
+
<div className="flex flex-1 flex-col justify-center gap-1 px-4 py-3 sm:px-5 sm:py-4">
|
|
75
|
+
<CardTitle>Session Activity</CardTitle>
|
|
76
|
+
<CardDescription>Loading...</CardDescription>
|
|
77
|
+
</div>
|
|
78
|
+
</CardHeader>
|
|
79
|
+
<CardContent className="px-2 sm:p-4">
|
|
80
|
+
<div className="h-[250px] flex items-center justify-center">
|
|
81
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
|
82
|
+
</div>
|
|
83
|
+
</CardContent>
|
|
84
|
+
</Card>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<Card>
|
|
90
|
+
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
|
|
91
|
+
<div className="flex flex-1 flex-col justify-center gap-1 px-4 py-3 sm:px-5 sm:py-4">
|
|
92
|
+
<CardTitle>Session Activity</CardTitle>
|
|
93
|
+
<CardDescription>
|
|
94
|
+
Last 90 days across all projects
|
|
95
|
+
</CardDescription>
|
|
96
|
+
</div>
|
|
97
|
+
<div className="flex">
|
|
98
|
+
{(['tasks', 'ships'] as const).map((key) => {
|
|
99
|
+
return (
|
|
100
|
+
<button
|
|
101
|
+
key={key}
|
|
102
|
+
data-active={activeChart === key}
|
|
103
|
+
className="relative z-30 flex flex-1 flex-col justify-center gap-1 border-t px-4 py-3 text-left even:border-l data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:px-6 sm:py-4"
|
|
104
|
+
onClick={() => setActiveChart(key)}
|
|
105
|
+
>
|
|
106
|
+
<span className="text-xs text-muted-foreground">
|
|
107
|
+
{chartConfig[key].label}
|
|
108
|
+
</span>
|
|
109
|
+
<span className="text-lg font-bold leading-none sm:text-3xl">
|
|
110
|
+
{totals[key].toLocaleString()}
|
|
111
|
+
</span>
|
|
112
|
+
</button>
|
|
113
|
+
)
|
|
114
|
+
})}
|
|
115
|
+
</div>
|
|
116
|
+
</CardHeader>
|
|
117
|
+
<CardContent className="px-2 sm:p-4">
|
|
118
|
+
{chartData.length === 0 ? (
|
|
119
|
+
<div className="h-[250px] flex items-center justify-center text-muted-foreground">
|
|
120
|
+
No session data yet
|
|
121
|
+
</div>
|
|
122
|
+
) : (
|
|
123
|
+
<ChartContainer
|
|
124
|
+
config={chartConfig}
|
|
125
|
+
className="aspect-auto h-[250px] w-full"
|
|
126
|
+
>
|
|
127
|
+
<BarChart
|
|
128
|
+
accessibilityLayer
|
|
129
|
+
data={chartData}
|
|
130
|
+
margin={{
|
|
131
|
+
left: 12,
|
|
132
|
+
right: 12,
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
<CartesianGrid vertical={false} />
|
|
136
|
+
<XAxis
|
|
137
|
+
dataKey="date"
|
|
138
|
+
tickLine={false}
|
|
139
|
+
axisLine={false}
|
|
140
|
+
tickMargin={8}
|
|
141
|
+
minTickGap={32}
|
|
142
|
+
tickFormatter={(value: string) => {
|
|
143
|
+
const date = new Date(value)
|
|
144
|
+
return date.toLocaleDateString('en-US', {
|
|
145
|
+
month: 'short',
|
|
146
|
+
day: 'numeric',
|
|
147
|
+
})
|
|
148
|
+
}}
|
|
149
|
+
/>
|
|
150
|
+
<ChartTooltip
|
|
151
|
+
content={
|
|
152
|
+
<ChartTooltipContent
|
|
153
|
+
className="w-[150px]"
|
|
154
|
+
nameKey="views"
|
|
155
|
+
labelFormatter={(value) => {
|
|
156
|
+
return new Date(value).toLocaleDateString('en-US', {
|
|
157
|
+
month: 'short',
|
|
158
|
+
day: 'numeric',
|
|
159
|
+
year: 'numeric',
|
|
160
|
+
})
|
|
161
|
+
}}
|
|
162
|
+
/>
|
|
163
|
+
}
|
|
164
|
+
/>
|
|
165
|
+
<Bar dataKey={activeChart} fill={`var(--color-${activeChart})`} />
|
|
166
|
+
</BarChart>
|
|
167
|
+
</ChartContainer>
|
|
168
|
+
)}
|
|
169
|
+
</CardContent>
|
|
170
|
+
</Card>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
4
|
+
import { useState, type ReactNode } from 'react'
|
|
5
|
+
import { ThemeProvider, type ThemeProviderProps } from 'next-themes'
|
|
6
|
+
import { TerminalProvider } from '@/context/TerminalContext'
|
|
7
|
+
|
|
8
|
+
function ThemeWrapper({ children, ...props }: ThemeProviderProps & { children: ReactNode }) {
|
|
9
|
+
return <ThemeProvider {...props}>{children}</ThemeProvider>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Providers({ children }: { children: ReactNode }) {
|
|
13
|
+
const [queryClient] = useState(() => new QueryClient({
|
|
14
|
+
defaultOptions: {
|
|
15
|
+
queries: {
|
|
16
|
+
// Data considered fresh for 2.5 seconds
|
|
17
|
+
staleTime: 2500,
|
|
18
|
+
// Garbage collect after 5 minutes
|
|
19
|
+
gcTime: 5 * 60 * 1000,
|
|
20
|
+
// Refetch on window focus for real-time feel
|
|
21
|
+
refetchOnWindowFocus: true,
|
|
22
|
+
// Refetch when reconnecting
|
|
23
|
+
refetchOnReconnect: true,
|
|
24
|
+
// Retry failed requests once
|
|
25
|
+
retry: 1,
|
|
26
|
+
retryDelay: 1000,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
}))
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<QueryClientProvider client={queryClient}>
|
|
33
|
+
<ThemeWrapper
|
|
34
|
+
attribute="class"
|
|
35
|
+
defaultTheme="dark"
|
|
36
|
+
enableSystem
|
|
37
|
+
disableTransitionOnChange
|
|
38
|
+
>
|
|
39
|
+
<TerminalProvider>
|
|
40
|
+
{children}
|
|
41
|
+
</TerminalProvider>
|
|
42
|
+
</ThemeWrapper>
|
|
43
|
+
</QueryClientProvider>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
import { buttonVariants } from "@/components/ui/button"
|
|
8
|
+
|
|
9
|
+
function AlertDialog({
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
|
12
|
+
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function AlertDialogTrigger({
|
|
16
|
+
...props
|
|
17
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
|
18
|
+
return (
|
|
19
|
+
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function AlertDialogPortal({
|
|
24
|
+
...props
|
|
25
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
|
26
|
+
return (
|
|
27
|
+
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function AlertDialogOverlay({
|
|
32
|
+
className,
|
|
33
|
+
...props
|
|
34
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
|
35
|
+
return (
|
|
36
|
+
<AlertDialogPrimitive.Overlay
|
|
37
|
+
data-slot="alert-dialog-overlay"
|
|
38
|
+
className={cn(
|
|
39
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
|
40
|
+
className
|
|
41
|
+
)}
|
|
42
|
+
{...props}
|
|
43
|
+
/>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function AlertDialogContent({
|
|
48
|
+
className,
|
|
49
|
+
...props
|
|
50
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
|
51
|
+
return (
|
|
52
|
+
<AlertDialogPortal>
|
|
53
|
+
<AlertDialogOverlay />
|
|
54
|
+
<AlertDialogPrimitive.Content
|
|
55
|
+
data-slot="alert-dialog-content"
|
|
56
|
+
className={cn(
|
|
57
|
+
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
|
58
|
+
className
|
|
59
|
+
)}
|
|
60
|
+
{...props}
|
|
61
|
+
/>
|
|
62
|
+
</AlertDialogPortal>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function AlertDialogHeader({
|
|
67
|
+
className,
|
|
68
|
+
...props
|
|
69
|
+
}: React.ComponentProps<"div">) {
|
|
70
|
+
return (
|
|
71
|
+
<div
|
|
72
|
+
data-slot="alert-dialog-header"
|
|
73
|
+
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
|
74
|
+
{...props}
|
|
75
|
+
/>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function AlertDialogFooter({
|
|
80
|
+
className,
|
|
81
|
+
...props
|
|
82
|
+
}: React.ComponentProps<"div">) {
|
|
83
|
+
return (
|
|
84
|
+
<div
|
|
85
|
+
data-slot="alert-dialog-footer"
|
|
86
|
+
className={cn(
|
|
87
|
+
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
|
88
|
+
className
|
|
89
|
+
)}
|
|
90
|
+
{...props}
|
|
91
|
+
/>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function AlertDialogTitle({
|
|
96
|
+
className,
|
|
97
|
+
...props
|
|
98
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
|
99
|
+
return (
|
|
100
|
+
<AlertDialogPrimitive.Title
|
|
101
|
+
data-slot="alert-dialog-title"
|
|
102
|
+
className={cn("text-lg font-semibold", className)}
|
|
103
|
+
{...props}
|
|
104
|
+
/>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function AlertDialogDescription({
|
|
109
|
+
className,
|
|
110
|
+
...props
|
|
111
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
|
112
|
+
return (
|
|
113
|
+
<AlertDialogPrimitive.Description
|
|
114
|
+
data-slot="alert-dialog-description"
|
|
115
|
+
className={cn("text-muted-foreground text-sm", className)}
|
|
116
|
+
{...props}
|
|
117
|
+
/>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function AlertDialogAction({
|
|
122
|
+
className,
|
|
123
|
+
...props
|
|
124
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
|
125
|
+
return (
|
|
126
|
+
<AlertDialogPrimitive.Action
|
|
127
|
+
className={cn(buttonVariants(), className)}
|
|
128
|
+
{...props}
|
|
129
|
+
/>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function AlertDialogCancel({
|
|
134
|
+
className,
|
|
135
|
+
...props
|
|
136
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
|
137
|
+
return (
|
|
138
|
+
<AlertDialogPrimitive.Cancel
|
|
139
|
+
className={cn(buttonVariants({ variant: "outline" }), className)}
|
|
140
|
+
{...props}
|
|
141
|
+
/>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export {
|
|
146
|
+
AlertDialog,
|
|
147
|
+
AlertDialogPortal,
|
|
148
|
+
AlertDialogOverlay,
|
|
149
|
+
AlertDialogTrigger,
|
|
150
|
+
AlertDialogContent,
|
|
151
|
+
AlertDialogHeader,
|
|
152
|
+
AlertDialogFooter,
|
|
153
|
+
AlertDialogTitle,
|
|
154
|
+
AlertDialogDescription,
|
|
155
|
+
AlertDialogAction,
|
|
156
|
+
AlertDialogCancel,
|
|
157
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const badgeVariants = cva(
|
|
8
|
+
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default:
|
|
13
|
+
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
|
14
|
+
secondary:
|
|
15
|
+
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
|
16
|
+
destructive:
|
|
17
|
+
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
18
|
+
outline:
|
|
19
|
+
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: {
|
|
23
|
+
variant: "default",
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
function Badge({
|
|
29
|
+
className,
|
|
30
|
+
variant,
|
|
31
|
+
asChild = false,
|
|
32
|
+
...props
|
|
33
|
+
}: React.ComponentProps<"span"> &
|
|
34
|
+
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
|
35
|
+
const Comp = asChild ? Slot : "span"
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Comp
|
|
39
|
+
data-slot="badge"
|
|
40
|
+
className={cn(badgeVariants({ variant }), className)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { Badge, badgeVariants }
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
13
|
+
destructive:
|
|
14
|
+
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
15
|
+
outline:
|
|
16
|
+
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
|
17
|
+
secondary:
|
|
18
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
19
|
+
ghost:
|
|
20
|
+
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
21
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
25
|
+
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
26
|
+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
27
|
+
icon: "size-9",
|
|
28
|
+
"icon-sm": "size-8",
|
|
29
|
+
"icon-lg": "size-10",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultVariants: {
|
|
33
|
+
variant: "default",
|
|
34
|
+
size: "default",
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
function Button({
|
|
40
|
+
className,
|
|
41
|
+
variant,
|
|
42
|
+
size,
|
|
43
|
+
asChild = false,
|
|
44
|
+
...props
|
|
45
|
+
}: React.ComponentProps<"button"> &
|
|
46
|
+
VariantProps<typeof buttonVariants> & {
|
|
47
|
+
asChild?: boolean
|
|
48
|
+
}) {
|
|
49
|
+
const Comp = asChild ? Slot : "button"
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Comp
|
|
53
|
+
data-slot="button"
|
|
54
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { Button, buttonVariants }
|