coaia-visualizer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.hch/issues.json +156 -0
  2. package/.hch/issues.md +2 -0
  3. package/README.md +67 -0
  4. package/app/api/jsonl/route.ts +71 -0
  5. package/app/globals.css +125 -0
  6. package/app/layout.tsx +48 -0
  7. package/app/page.tsx +284 -0
  8. package/cli.ts +170 -0
  9. package/components/chart-detail.tsx +213 -0
  10. package/components/chart-list.tsx +184 -0
  11. package/components/data-stats.tsx +49 -0
  12. package/components/file-upload.tsx +73 -0
  13. package/components/narrative-beats.tsx +108 -0
  14. package/components/relation-graph.tsx +81 -0
  15. package/components/theme-provider.tsx +11 -0
  16. package/components/ui/badge.tsx +46 -0
  17. package/components/ui/button.tsx +60 -0
  18. package/components/ui/card.tsx +92 -0
  19. package/components/ui/scroll-area.tsx +58 -0
  20. package/components/ui/separator.tsx +28 -0
  21. package/components/ui/tabs.tsx +66 -0
  22. package/components.json +21 -0
  23. package/dist/cli.js +144 -0
  24. package/feat-2-webui-local-editing/IMPLEMENTATION.md +245 -0
  25. package/feat-2-webui-local-editing/INTEGRATION.md +302 -0
  26. package/feat-2-webui-local-editing/QUICKSTART.md +129 -0
  27. package/feat-2-webui-local-editing/README.md +254 -0
  28. package/feat-2-webui-local-editing/api-route-jsonl.ts +71 -0
  29. package/feat-2-webui-local-editing/cli.ts +170 -0
  30. package/feat-2-webui-local-editing/demo.sh +98 -0
  31. package/feat-2-webui-local-editing/package.json +82 -0
  32. package/feat-2-webui-local-editing/test-integration.sh +93 -0
  33. package/feat-2-webui-local-editing/updated-page.tsx +284 -0
  34. package/hooks/use-toast.ts +17 -0
  35. package/lib/jsonl-parser.ts +153 -0
  36. package/lib/types.ts +39 -0
  37. package/lib/utils.ts +6 -0
  38. package/next.config.mjs +12 -0
  39. package/package.json +82 -0
  40. package/postcss.config.mjs +8 -0
  41. package/public/apple-icon.png +0 -0
  42. package/public/icon-dark-32x32.png +0 -0
  43. package/public/icon-light-32x32.png +0 -0
  44. package/public/icon.svg +26 -0
  45. package/public/placeholder-logo.png +0 -0
  46. package/public/placeholder-logo.svg +1 -0
  47. package/public/placeholder-user.jpg +0 -0
  48. package/public/placeholder.jpg +0 -0
  49. package/public/placeholder.svg +1 -0
  50. package/styles/globals.css +125 -0
  51. package/tsconfig.json +41 -0
@@ -0,0 +1,184 @@
1
+ "use client"
2
+
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
4
+ import { Badge } from "@/components/ui/badge"
5
+ import { ScrollArea } from "@/components/ui/scroll-area"
6
+ import type { ParsedData, Chart } from "@/lib/types"
7
+ import { getChartSummary, getChartProgress } from "@/lib/jsonl-parser"
8
+ import { ChevronRight, ChevronDown, Target, Calendar, BookOpen } from "lucide-react"
9
+ import { useState } from "react"
10
+ import { cn } from "@/lib/utils"
11
+
12
+ interface ChartListProps {
13
+ data: ParsedData
14
+ selectedChart: Chart | null
15
+ onSelectChart: (chart: Chart) => void
16
+ mode: "hierarchy" | "list"
17
+ }
18
+
19
+ export function ChartList({ data, selectedChart, onSelectChart, mode }: ChartListProps) {
20
+ if (mode === "list") {
21
+ return (
22
+ <Card>
23
+ <CardHeader>
24
+ <CardTitle>All Charts</CardTitle>
25
+ <CardDescription>{data.charts.length} total charts</CardDescription>
26
+ </CardHeader>
27
+ <CardContent>
28
+ <ScrollArea className="h-[600px] pr-4">
29
+ <div className="space-y-2">
30
+ {data.charts.map((chart) => (
31
+ <ChartItem
32
+ key={chart.id}
33
+ chart={chart}
34
+ isSelected={selectedChart?.id === chart.id}
35
+ onSelect={onSelectChart}
36
+ showLevel
37
+ />
38
+ ))}
39
+ </div>
40
+ </ScrollArea>
41
+ </CardContent>
42
+ </Card>
43
+ )
44
+ }
45
+
46
+ return (
47
+ <Card>
48
+ <CardHeader>
49
+ <CardTitle>Chart Hierarchy</CardTitle>
50
+ <CardDescription>{data.rootCharts.length} root charts</CardDescription>
51
+ </CardHeader>
52
+ <CardContent>
53
+ <ScrollArea className="h-[600px] pr-4">
54
+ <div className="space-y-2">
55
+ {data.rootCharts.map((chart) => (
56
+ <ChartTree key={chart.id} chart={chart} selectedChart={selectedChart} onSelectChart={onSelectChart} />
57
+ ))}
58
+ </div>
59
+ </ScrollArea>
60
+ </CardContent>
61
+ </Card>
62
+ )
63
+ }
64
+
65
+ interface ChartTreeProps {
66
+ chart: Chart
67
+ selectedChart: Chart | null
68
+ onSelectChart: (chart: Chart) => void
69
+ level?: number
70
+ }
71
+
72
+ function ChartTree({ chart, selectedChart, onSelectChart, level = 0 }: ChartTreeProps) {
73
+ const [isExpanded, setIsExpanded] = useState(true)
74
+ const hasSubCharts = chart.subCharts.length > 0
75
+ const isSelected = selectedChart?.id === chart.id
76
+
77
+ return (
78
+ <div className={cn(level > 0 && "ml-4 border-l border-border pl-4")}>
79
+ <div className="flex items-start gap-2">
80
+ {hasSubCharts && (
81
+ <button
82
+ onClick={() => setIsExpanded(!isExpanded)}
83
+ className="mt-2 text-muted-foreground hover:text-foreground transition-colors"
84
+ >
85
+ {isExpanded ? <ChevronDown className="w-4 h-4" /> : <ChevronRight className="w-4 h-4" />}
86
+ </button>
87
+ )}
88
+ {!hasSubCharts && <div className="w-4" />}
89
+ <div className="flex-1">
90
+ <ChartItem chart={chart} isSelected={isSelected} onSelect={onSelectChart} />
91
+ </div>
92
+ </div>
93
+ {isExpanded && hasSubCharts && (
94
+ <div className="mt-2 space-y-2">
95
+ {chart.subCharts.map((subChart) => (
96
+ <ChartTree
97
+ key={subChart.id}
98
+ chart={subChart}
99
+ selectedChart={selectedChart}
100
+ onSelectChart={onSelectChart}
101
+ level={level + 1}
102
+ />
103
+ ))}
104
+ </div>
105
+ )}
106
+ </div>
107
+ )
108
+ }
109
+
110
+ interface ChartItemProps {
111
+ chart: Chart
112
+ isSelected: boolean
113
+ onSelect: (chart: Chart) => void
114
+ showLevel?: boolean
115
+ }
116
+
117
+ function ChartItem({ chart, isSelected, onSelect, showLevel }: ChartItemProps) {
118
+ const summary = getChartSummary(chart)
119
+ const progress = getChartProgress(chart)
120
+ const dueDate = chart.chartEntity.metadata.dueDate
121
+ ? new Date(chart.chartEntity.metadata.dueDate).toLocaleDateString()
122
+ : null
123
+
124
+ return (
125
+ <button
126
+ onClick={() => onSelect(chart)}
127
+ className={cn(
128
+ "w-full text-left p-3 rounded-lg border transition-all",
129
+ isSelected ? "border-primary bg-primary/5" : "border-border hover:border-primary/50 bg-card",
130
+ )}
131
+ >
132
+ <div className="flex items-start justify-between gap-2 mb-2">
133
+ <div className="flex items-center gap-2">
134
+ <Target className="w-4 h-4 text-muted-foreground flex-shrink-0" />
135
+ <span className="text-xs font-mono text-muted-foreground">chart_{chart.id}</span>
136
+ {showLevel && (
137
+ <Badge variant="outline" className="text-xs">
138
+ L{chart.level}
139
+ </Badge>
140
+ )}
141
+ </div>
142
+ {dueDate && (
143
+ <div className="flex items-center gap-1 text-xs text-muted-foreground">
144
+ <Calendar className="w-3 h-3" />
145
+ <span>{dueDate}</span>
146
+ </div>
147
+ )}
148
+ </div>
149
+ <p className="text-sm mb-2 line-clamp-2">{summary}</p>
150
+
151
+ {chart.narrativeBeats.length > 0 && (
152
+ <div className="mb-2 p-2 bg-chart-3/10 border border-chart-3/20 rounded">
153
+ <div className="flex items-center gap-2 mb-1">
154
+ <BookOpen className="w-3 h-3 text-chart-3" />
155
+ <span className="text-xs font-medium text-chart-3">
156
+ {chart.narrativeBeats.length} Narrative Beat{chart.narrativeBeats.length > 1 ? "s" : ""}
157
+ </span>
158
+ </div>
159
+ {chart.narrativeBeats[0]?.metadata?.narrative?.description && (
160
+ <p className="text-xs text-muted-foreground line-clamp-1 pl-5">
161
+ {chart.narrativeBeats[0].metadata.narrative.description}
162
+ </p>
163
+ )}
164
+ </div>
165
+ )}
166
+
167
+ <div className="flex items-center justify-between">
168
+ <div className="flex items-center gap-2">
169
+ <Badge variant="secondary" className="text-xs">
170
+ {chart.actions.length} actions
171
+ </Badge>
172
+ </div>
173
+ {chart.actions.length > 0 && (
174
+ <div className="flex items-center gap-2">
175
+ <div className="w-16 h-1.5 bg-secondary rounded-full overflow-hidden">
176
+ <div className="h-full bg-primary transition-all" style={{ width: `${progress}%` }} />
177
+ </div>
178
+ <span className="text-xs text-muted-foreground">{progress}%</span>
179
+ </div>
180
+ )}
181
+ </div>
182
+ </button>
183
+ )
184
+ }
@@ -0,0 +1,49 @@
1
+ "use client"
2
+
3
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
4
+ import type { ParsedData } from "@/lib/types"
5
+ import { BarChart3, FileText, Network, Target } from "lucide-react"
6
+
7
+ interface DataStatsProps {
8
+ data: ParsedData
9
+ }
10
+
11
+ export function DataStats({ data }: DataStatsProps) {
12
+ const totalEntities = data.entities.size
13
+ const totalRelations = data.relations.length
14
+ const totalCharts = data.charts.length
15
+ const totalActions = data.charts.reduce((sum, chart) => sum + chart.actions.length, 0)
16
+ const completedActions = data.charts.reduce(
17
+ (sum, chart) => sum + chart.actions.filter((a) => a.metadata.completionStatus).length,
18
+ 0,
19
+ )
20
+ const totalBeats = data.charts.reduce((sum, chart) => sum + chart.narrativeBeats.length, 0)
21
+
22
+ const stats = [
23
+ { label: "Charts", value: totalCharts, icon: Target, color: "text-chart-1" },
24
+ { label: "Actions", value: `${completedActions}/${totalActions}`, icon: BarChart3, color: "text-chart-2" },
25
+ { label: "Entities", value: totalEntities, icon: FileText, color: "text-chart-3" },
26
+ { label: "Relations", value: totalRelations, icon: Network, color: "text-chart-4" },
27
+ ]
28
+
29
+ return (
30
+ <div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
31
+ {stats.map((stat) => {
32
+ const Icon = stat.icon
33
+ return (
34
+ <Card key={stat.label}>
35
+ <CardHeader className="pb-2">
36
+ <CardTitle className="text-sm font-medium text-muted-foreground">{stat.label}</CardTitle>
37
+ </CardHeader>
38
+ <CardContent>
39
+ <div className="flex items-center gap-2">
40
+ <Icon className={`w-4 h-4 ${stat.color}`} />
41
+ <span className="text-2xl font-bold">{stat.value}</span>
42
+ </div>
43
+ </CardContent>
44
+ </Card>
45
+ )
46
+ })}
47
+ </div>
48
+ )
49
+ }
@@ -0,0 +1,73 @@
1
+ "use client"
2
+
3
+ import type React from "react"
4
+
5
+ import { useCallback } from "react"
6
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
7
+ import { Upload } from "lucide-react"
8
+
9
+ interface FileUploadProps {
10
+ onFileLoad: (content: string, fileName: string) => void
11
+ }
12
+
13
+ export function FileUpload({ onFileLoad }: FileUploadProps) {
14
+ const handleFileChange = useCallback(
15
+ (e: React.ChangeEvent<HTMLInputElement>) => {
16
+ const file = e.target.files?.[0]
17
+ if (!file) return
18
+
19
+ const reader = new FileReader()
20
+ reader.onload = (event) => {
21
+ const content = event.target?.result as string
22
+ onFileLoad(content, file.name)
23
+ }
24
+ reader.readAsText(file)
25
+ },
26
+ [onFileLoad],
27
+ )
28
+
29
+ const handleDrop = useCallback(
30
+ (e: React.DragEvent<HTMLDivElement>) => {
31
+ e.preventDefault()
32
+ const file = e.dataTransfer.files[0]
33
+ if (!file) return
34
+
35
+ const reader = new FileReader()
36
+ reader.onload = (event) => {
37
+ const content = event.target?.result as string
38
+ onFileLoad(content, file.name)
39
+ }
40
+ reader.readAsText(file)
41
+ },
42
+ [onFileLoad],
43
+ )
44
+
45
+ const handleDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => {
46
+ e.preventDefault()
47
+ }, [])
48
+
49
+ return (
50
+ <Card>
51
+ <CardHeader>
52
+ <CardTitle>Load JSONL File</CardTitle>
53
+ <CardDescription>
54
+ Upload a coaia-narrative memory file (.jsonl) to visualize structural tension charts
55
+ </CardDescription>
56
+ </CardHeader>
57
+ <CardContent>
58
+ <div
59
+ onDrop={handleDrop}
60
+ onDragOver={handleDragOver}
61
+ className="border-2 border-dashed border-border rounded-lg p-12 text-center hover:border-primary/50 transition-colors cursor-pointer"
62
+ >
63
+ <input type="file" accept=".jsonl,.txt" onChange={handleFileChange} className="hidden" id="file-upload" />
64
+ <label htmlFor="file-upload" className="cursor-pointer">
65
+ <Upload className="w-12 h-12 mx-auto mb-4 text-muted-foreground" />
66
+ <p className="text-sm font-medium mb-2">Click to upload or drag and drop</p>
67
+ <p className="text-xs text-muted-foreground">JSONL files from coaia-narrative</p>
68
+ </label>
69
+ </div>
70
+ </CardContent>
71
+ </Card>
72
+ )
73
+ }
@@ -0,0 +1,108 @@
1
+ "use client"
2
+
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
4
+ import { Badge } from "@/components/ui/badge"
5
+ import type { EntityRecord } from "@/lib/types"
6
+ import { Clock, Compass, Drama, Sparkles } from "lucide-react"
7
+
8
+ interface NarrativeBeatsProps {
9
+ beats: EntityRecord[]
10
+ }
11
+
12
+ export function NarrativeBeats({ beats }: NarrativeBeatsProps) {
13
+ if (beats.length === 0) {
14
+ return (
15
+ <Card>
16
+ <CardContent className="py-8">
17
+ <p className="text-sm text-muted-foreground text-center">No narrative beats documented yet</p>
18
+ </CardContent>
19
+ </Card>
20
+ )
21
+ }
22
+
23
+ return (
24
+ <div className="space-y-4">
25
+ {beats.map((beat, idx) => {
26
+ const narrative = beat.metadata.narrative
27
+ const act = beat.metadata.act
28
+ const typeDramatic = beat.metadata.type_dramatic
29
+ const universes = beat.metadata.universes || []
30
+ const timestamp = beat.metadata.timestamp ? new Date(beat.metadata.timestamp).toLocaleString() : null
31
+
32
+ return (
33
+ <Card key={beat.name} className="border-l-4 border-l-chart-3">
34
+ <CardHeader className="bg-chart-3/5">
35
+ <div className="flex items-start justify-between gap-4">
36
+ <div className="flex-1">
37
+ <div className="flex items-center gap-2 mb-2">
38
+ <Drama className="w-5 h-5 text-chart-3" />
39
+ <CardTitle className="text-lg">
40
+ Act {act} - {typeDramatic}
41
+ </CardTitle>
42
+ <Badge variant="outline" className="border-chart-3 text-chart-3">
43
+ Beat {idx + 1}
44
+ </Badge>
45
+ </div>
46
+ {timestamp && (
47
+ <CardDescription className="flex items-center gap-1">
48
+ <Clock className="w-3 h-3" />
49
+ {timestamp}
50
+ </CardDescription>
51
+ )}
52
+ </div>
53
+ </div>
54
+ </CardHeader>
55
+ <CardContent className="space-y-4 pt-4">
56
+ {/* Universes */}
57
+ <div className="flex items-center gap-2 flex-wrap">
58
+ <Compass className="w-4 h-4 text-muted-foreground" />
59
+ <span className="text-xs text-muted-foreground">Universes:</span>
60
+ {universes.map((universe: string) => (
61
+ <Badge key={universe} variant="secondary" className="text-xs">
62
+ {universe}
63
+ </Badge>
64
+ ))}
65
+ </div>
66
+
67
+ {/* Description */}
68
+ {narrative?.description && (
69
+ <div>
70
+ <h4 className="text-sm font-semibold mb-2 flex items-center gap-2">
71
+ <Sparkles className="w-4 h-4 text-chart-3" />
72
+ Description
73
+ </h4>
74
+ <p className="text-sm leading-relaxed">{narrative.description}</p>
75
+ </div>
76
+ )}
77
+
78
+ {/* Prose */}
79
+ {narrative?.prose && (
80
+ <div>
81
+ <h4 className="text-sm font-semibold mb-2">Narrative</h4>
82
+ <div className="bg-chart-3/10 border-l-2 border-l-chart-3 rounded-lg p-4">
83
+ <p className="text-sm leading-relaxed italic">{narrative.prose}</p>
84
+ </div>
85
+ </div>
86
+ )}
87
+
88
+ {/* Lessons */}
89
+ {narrative?.lessons && narrative.lessons.length > 0 && (
90
+ <div>
91
+ <h4 className="text-sm font-semibold mb-2">Lessons Learned</h4>
92
+ <ul className="space-y-2">
93
+ {narrative.lessons.map((lesson: string, lessonIdx: number) => (
94
+ <li key={lessonIdx} className="text-sm flex items-start gap-2 p-2 bg-muted/50 rounded">
95
+ <span className="text-chart-3 mt-0.5 font-bold">•</span>
96
+ <span>{lesson}</span>
97
+ </li>
98
+ ))}
99
+ </ul>
100
+ </div>
101
+ )}
102
+ </CardContent>
103
+ </Card>
104
+ )
105
+ })}
106
+ </div>
107
+ )
108
+ }
@@ -0,0 +1,81 @@
1
+ "use client"
2
+
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
4
+ import { Badge } from "@/components/ui/badge"
5
+ import type { ParsedData, Chart, RelationRecord } from "@/lib/types"
6
+ import { ArrowRight } from "lucide-react"
7
+
8
+ interface RelationGraphProps {
9
+ chart: Chart
10
+ data: ParsedData
11
+ }
12
+
13
+ export function RelationGraph({ chart, data }: RelationGraphProps) {
14
+ const relations = chart.relations
15
+
16
+ if (relations.length === 0) {
17
+ return (
18
+ <Card>
19
+ <CardContent className="py-8">
20
+ <p className="text-sm text-muted-foreground text-center">No relations defined</p>
21
+ </CardContent>
22
+ </Card>
23
+ )
24
+ }
25
+
26
+ const relationsByType = relations.reduce(
27
+ (acc, rel) => {
28
+ if (!acc[rel.relationType]) {
29
+ acc[rel.relationType] = []
30
+ }
31
+ acc[rel.relationType].push(rel)
32
+ return acc
33
+ },
34
+ {} as Record<string, RelationRecord[]>,
35
+ )
36
+
37
+ return (
38
+ <Card>
39
+ <CardHeader>
40
+ <CardTitle>Entity Relations</CardTitle>
41
+ <CardDescription>Connections between chart entities</CardDescription>
42
+ </CardHeader>
43
+ <CardContent className="space-y-6">
44
+ {Object.entries(relationsByType).map(([type, rels]) => (
45
+ <div key={type}>
46
+ <h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
47
+ <Badge variant="outline">{type.replace(/_/g, " ")}</Badge>
48
+ <span className="text-muted-foreground">({rels.length})</span>
49
+ </h3>
50
+ <div className="space-y-2">
51
+ {rels.map((rel, idx) => {
52
+ const fromEntity = data.entities.get(rel.from)
53
+ const toEntity = data.entities.get(rel.to)
54
+
55
+ return (
56
+ <div key={idx} className="flex items-center gap-3 p-3 bg-muted/30 rounded-lg text-sm">
57
+ <div className="flex-1 min-w-0">
58
+ <div className="font-mono text-xs text-muted-foreground mb-1">{rel.from}</div>
59
+ {fromEntity && (
60
+ <div className="text-xs line-clamp-1">
61
+ {fromEntity.observations[0] || fromEntity.entityType}
62
+ </div>
63
+ )}
64
+ </div>
65
+ <ArrowRight className="w-4 h-4 text-muted-foreground flex-shrink-0" />
66
+ <div className="flex-1 min-w-0">
67
+ <div className="font-mono text-xs text-muted-foreground mb-1">{rel.to}</div>
68
+ {toEntity && (
69
+ <div className="text-xs line-clamp-1">{toEntity.observations[0] || toEntity.entityType}</div>
70
+ )}
71
+ </div>
72
+ </div>
73
+ )
74
+ })}
75
+ </div>
76
+ </div>
77
+ ))}
78
+ </CardContent>
79
+ </Card>
80
+ )
81
+ }
@@ -0,0 +1,11 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import {
5
+ ThemeProvider as NextThemesProvider,
6
+ type ThemeProviderProps,
7
+ } from 'next-themes'
8
+
9
+ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
10
+ return <NextThemesProvider {...props}>{children}</NextThemesProvider>
11
+ }
@@ -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-md 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 }