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.
Files changed (105) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/bin/dev.js +217 -0
  3. package/bin/prjct +10 -0
  4. package/bin/serve.js +78 -0
  5. package/core/bus/index.js +322 -0
  6. package/core/command-registry.js +65 -0
  7. package/core/domain/snapshot-manager.js +375 -0
  8. package/core/plugin/hooks.js +313 -0
  9. package/core/plugin/index.js +52 -0
  10. package/core/plugin/loader.js +331 -0
  11. package/core/plugin/registry.js +325 -0
  12. package/core/plugins/webhook.js +143 -0
  13. package/core/session/index.js +449 -0
  14. package/core/session/metrics.js +293 -0
  15. package/package.json +28 -4
  16. package/packages/shared/dist/index.d.ts +615 -0
  17. package/packages/shared/dist/index.js +204 -0
  18. package/packages/shared/package.json +29 -0
  19. package/packages/shared/src/index.ts +9 -0
  20. package/packages/shared/src/schemas.ts +124 -0
  21. package/packages/shared/src/types.ts +187 -0
  22. package/packages/shared/src/utils.ts +148 -0
  23. package/packages/shared/tsconfig.json +18 -0
  24. package/packages/web/README.md +36 -0
  25. package/packages/web/app/api/claude/sessions/route.ts +44 -0
  26. package/packages/web/app/api/claude/status/route.ts +34 -0
  27. package/packages/web/app/api/projects/[id]/delete/route.ts +21 -0
  28. package/packages/web/app/api/projects/[id]/icon/route.ts +33 -0
  29. package/packages/web/app/api/projects/[id]/route.ts +29 -0
  30. package/packages/web/app/api/projects/[id]/stats/route.ts +36 -0
  31. package/packages/web/app/api/projects/[id]/status/route.ts +21 -0
  32. package/packages/web/app/api/projects/route.ts +16 -0
  33. package/packages/web/app/api/sessions/history/route.ts +122 -0
  34. package/packages/web/app/api/stats/route.ts +38 -0
  35. package/packages/web/app/error.tsx +34 -0
  36. package/packages/web/app/favicon.ico +0 -0
  37. package/packages/web/app/globals.css +155 -0
  38. package/packages/web/app/layout.tsx +43 -0
  39. package/packages/web/app/loading.tsx +7 -0
  40. package/packages/web/app/not-found.tsx +25 -0
  41. package/packages/web/app/page.tsx +227 -0
  42. package/packages/web/app/project/[id]/error.tsx +41 -0
  43. package/packages/web/app/project/[id]/loading.tsx +9 -0
  44. package/packages/web/app/project/[id]/not-found.tsx +27 -0
  45. package/packages/web/app/project/[id]/page.tsx +253 -0
  46. package/packages/web/app/project/[id]/stats/page.tsx +447 -0
  47. package/packages/web/app/sessions/page.tsx +165 -0
  48. package/packages/web/app/settings/page.tsx +150 -0
  49. package/packages/web/components/AppSidebar.tsx +113 -0
  50. package/packages/web/components/CommandButton.tsx +39 -0
  51. package/packages/web/components/ConnectionStatus.tsx +29 -0
  52. package/packages/web/components/Logo.tsx +65 -0
  53. package/packages/web/components/MarkdownContent.tsx +123 -0
  54. package/packages/web/components/ProjectAvatar.tsx +54 -0
  55. package/packages/web/components/TechStackBadges.tsx +20 -0
  56. package/packages/web/components/TerminalTab.tsx +84 -0
  57. package/packages/web/components/TerminalTabs.tsx +210 -0
  58. package/packages/web/components/charts/SessionsChart.tsx +172 -0
  59. package/packages/web/components/providers.tsx +45 -0
  60. package/packages/web/components/ui/alert-dialog.tsx +157 -0
  61. package/packages/web/components/ui/badge.tsx +46 -0
  62. package/packages/web/components/ui/button.tsx +60 -0
  63. package/packages/web/components/ui/card.tsx +92 -0
  64. package/packages/web/components/ui/chart.tsx +385 -0
  65. package/packages/web/components/ui/dropdown-menu.tsx +257 -0
  66. package/packages/web/components/ui/scroll-area.tsx +58 -0
  67. package/packages/web/components/ui/sheet.tsx +139 -0
  68. package/packages/web/components/ui/tabs.tsx +66 -0
  69. package/packages/web/components/ui/tooltip.tsx +61 -0
  70. package/packages/web/components.json +22 -0
  71. package/packages/web/context/TerminalContext.tsx +45 -0
  72. package/packages/web/context/TerminalTabsContext.tsx +136 -0
  73. package/packages/web/eslint.config.mjs +18 -0
  74. package/packages/web/hooks/useClaudeTerminal.ts +375 -0
  75. package/packages/web/hooks/useProjectStats.ts +38 -0
  76. package/packages/web/hooks/useProjects.ts +73 -0
  77. package/packages/web/hooks/useStats.ts +28 -0
  78. package/packages/web/lib/format.ts +23 -0
  79. package/packages/web/lib/parse-prjct-files.ts +1122 -0
  80. package/packages/web/lib/projects.ts +452 -0
  81. package/packages/web/lib/pty.ts +101 -0
  82. package/packages/web/lib/query-config.ts +44 -0
  83. package/packages/web/lib/utils.ts +6 -0
  84. package/packages/web/next-env.d.ts +6 -0
  85. package/packages/web/next.config.ts +7 -0
  86. package/packages/web/package.json +53 -0
  87. package/packages/web/postcss.config.mjs +7 -0
  88. package/packages/web/public/file.svg +1 -0
  89. package/packages/web/public/globe.svg +1 -0
  90. package/packages/web/public/next.svg +1 -0
  91. package/packages/web/public/vercel.svg +1 -0
  92. package/packages/web/public/window.svg +1 -0
  93. package/packages/web/server.ts +262 -0
  94. package/packages/web/tsconfig.json +34 -0
  95. package/templates/commands/done.md +176 -54
  96. package/templates/commands/history.md +176 -0
  97. package/templates/commands/init.md +28 -1
  98. package/templates/commands/now.md +191 -9
  99. package/templates/commands/pause.md +176 -12
  100. package/templates/commands/redo.md +142 -0
  101. package/templates/commands/resume.md +166 -62
  102. package/templates/commands/serve.md +121 -0
  103. package/templates/commands/ship.md +45 -1
  104. package/templates/commands/sync.md +34 -1
  105. 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 }