claude-smart 0.2.22 → 0.2.24

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 (113) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/README.md +69 -27
  3. package/bin/claude-smart.js +296 -11
  4. package/package.json +11 -1
  5. package/plugin/.claude-plugin/plugin.json +17 -0
  6. package/plugin/.codex-plugin/plugin.json +35 -0
  7. package/plugin/LICENSE +202 -0
  8. package/plugin/README.md +37 -0
  9. package/plugin/bin/cs-cite +77 -0
  10. package/plugin/commands/clear-all.md +8 -0
  11. package/plugin/commands/dashboard.md +8 -0
  12. package/plugin/commands/learn.md +12 -0
  13. package/plugin/commands/restart.md +8 -0
  14. package/plugin/commands/show.md +8 -0
  15. package/plugin/dashboard/AGENTS.md +6 -0
  16. package/plugin/dashboard/app/api/claude-settings/route.ts +19 -0
  17. package/plugin/dashboard/app/api/config/route.ts +16 -0
  18. package/plugin/dashboard/app/api/health/route.ts +10 -0
  19. package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +63 -0
  20. package/plugin/dashboard/app/api/sessions/[id]/route.ts +28 -0
  21. package/plugin/dashboard/app/api/sessions/route.ts +14 -0
  22. package/plugin/dashboard/app/configure/env/page.tsx +318 -0
  23. package/plugin/dashboard/app/configure/layout.tsx +47 -0
  24. package/plugin/dashboard/app/configure/page.tsx +5 -0
  25. package/plugin/dashboard/app/configure/server/page.tsx +258 -0
  26. package/plugin/dashboard/app/dashboard/page.tsx +227 -0
  27. package/plugin/dashboard/app/globals.css +129 -0
  28. package/plugin/dashboard/app/icon.png +0 -0
  29. package/plugin/dashboard/app/layout.tsx +40 -0
  30. package/plugin/dashboard/app/page.tsx +5 -0
  31. package/plugin/dashboard/app/preferences/[id]/page.tsx +531 -0
  32. package/plugin/dashboard/app/preferences/page.tsx +126 -0
  33. package/plugin/dashboard/app/providers.tsx +12 -0
  34. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +321 -0
  35. package/plugin/dashboard/app/sessions/page.tsx +186 -0
  36. package/plugin/dashboard/app/skills/page.tsx +362 -0
  37. package/plugin/dashboard/app/skills/project/[id]/page.tsx +597 -0
  38. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +830 -0
  39. package/plugin/dashboard/components/common/delete-all-button.tsx +45 -0
  40. package/plugin/dashboard/components/common/empty-state.tsx +34 -0
  41. package/plugin/dashboard/components/common/learnings-badge.tsx +34 -0
  42. package/plugin/dashboard/components/common/page-header.tsx +34 -0
  43. package/plugin/dashboard/components/common/page-tabs.tsx +115 -0
  44. package/plugin/dashboard/components/common/stat-card.tsx +38 -0
  45. package/plugin/dashboard/components/layout/nav-items.ts +22 -0
  46. package/plugin/dashboard/components/layout/sidebar.tsx +45 -0
  47. package/plugin/dashboard/components/layout/top-bar.tsx +64 -0
  48. package/plugin/dashboard/components/stall-banner.tsx +53 -0
  49. package/plugin/dashboard/components/ui/badge.tsx +52 -0
  50. package/plugin/dashboard/components/ui/button.tsx +60 -0
  51. package/plugin/dashboard/components/ui/collapsible.tsx +21 -0
  52. package/plugin/dashboard/components/ui/input.tsx +20 -0
  53. package/plugin/dashboard/components/ui/label.tsx +20 -0
  54. package/plugin/dashboard/components/ui/scroll-area.tsx +55 -0
  55. package/plugin/dashboard/components/ui/select.tsx +201 -0
  56. package/plugin/dashboard/components/ui/separator.tsx +25 -0
  57. package/plugin/dashboard/components/ui/sheet.tsx +135 -0
  58. package/plugin/dashboard/components/ui/switch.tsx +32 -0
  59. package/plugin/dashboard/components.json +25 -0
  60. package/plugin/dashboard/eslint.config.mjs +16 -0
  61. package/plugin/dashboard/hooks/use-settings.tsx +88 -0
  62. package/plugin/dashboard/hooks/use-stall-state.ts +59 -0
  63. package/plugin/dashboard/lib/claude-settings-file.ts +114 -0
  64. package/plugin/dashboard/lib/config-file.ts +131 -0
  65. package/plugin/dashboard/lib/format.ts +58 -0
  66. package/plugin/dashboard/lib/reflexio-client.ts +238 -0
  67. package/plugin/dashboard/lib/reflexio-url.ts +17 -0
  68. package/plugin/dashboard/lib/session-reader.ts +245 -0
  69. package/plugin/dashboard/lib/status.ts +24 -0
  70. package/plugin/dashboard/lib/types.ts +145 -0
  71. package/plugin/dashboard/lib/utils.ts +6 -0
  72. package/plugin/dashboard/next.config.ts +7 -0
  73. package/plugin/dashboard/package-lock.json +10275 -0
  74. package/plugin/dashboard/package.json +37 -0
  75. package/plugin/dashboard/postcss.config.mjs +7 -0
  76. package/plugin/dashboard/public/claude-smart-icon.png +0 -0
  77. package/plugin/dashboard/tsconfig.json +34 -0
  78. package/plugin/hooks/codex-hooks.json +67 -0
  79. package/plugin/hooks/hooks.json +111 -0
  80. package/plugin/pyproject.toml +49 -0
  81. package/plugin/scripts/_codex_env.sh +27 -0
  82. package/plugin/scripts/_lib.sh +325 -0
  83. package/plugin/scripts/backend-service.sh +208 -0
  84. package/plugin/scripts/cli.sh +40 -0
  85. package/plugin/scripts/dashboard-build.sh +139 -0
  86. package/plugin/scripts/dashboard-open.sh +107 -0
  87. package/plugin/scripts/dashboard-service.sh +195 -0
  88. package/plugin/scripts/ensure-plugin-root.sh +84 -0
  89. package/plugin/scripts/hook_entry.sh +70 -0
  90. package/plugin/scripts/smart-install.sh +411 -0
  91. package/plugin/src/claude_smart/__init__.py +3 -0
  92. package/plugin/src/claude_smart/cli.py +1273 -0
  93. package/plugin/src/claude_smart/context_format.py +277 -0
  94. package/plugin/src/claude_smart/context_inject.py +92 -0
  95. package/plugin/src/claude_smart/cs_cite.py +236 -0
  96. package/plugin/src/claude_smart/events/__init__.py +1 -0
  97. package/plugin/src/claude_smart/events/post_tool.py +148 -0
  98. package/plugin/src/claude_smart/events/pre_tool.py +52 -0
  99. package/plugin/src/claude_smart/events/session_end.py +20 -0
  100. package/plugin/src/claude_smart/events/session_start.py +119 -0
  101. package/plugin/src/claude_smart/events/stop.py +393 -0
  102. package/plugin/src/claude_smart/events/user_prompt.py +73 -0
  103. package/plugin/src/claude_smart/hook.py +114 -0
  104. package/plugin/src/claude_smart/ids.py +56 -0
  105. package/plugin/src/claude_smart/internal_call.py +89 -0
  106. package/plugin/src/claude_smart/optimizer_assistant.py +203 -0
  107. package/plugin/src/claude_smart/publish.py +71 -0
  108. package/plugin/src/claude_smart/query_compose.py +51 -0
  109. package/plugin/src/claude_smart/reflexio_adapter.py +403 -0
  110. package/plugin/src/claude_smart/runtime.py +52 -0
  111. package/plugin/src/claude_smart/stall_banner.py +61 -0
  112. package/plugin/src/claude_smart/state.py +276 -0
  113. package/plugin/uv.lock +3720 -0
@@ -0,0 +1,45 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Trash2 } from "lucide-react";
5
+ import { Button } from "@/components/ui/button";
6
+
7
+ interface Props {
8
+ label: string;
9
+ confirmMessage: string;
10
+ disabled?: boolean;
11
+ onConfirm: () => Promise<void>;
12
+ }
13
+
14
+ export function DeleteAllButton({
15
+ label,
16
+ confirmMessage,
17
+ disabled,
18
+ onConfirm,
19
+ }: Props) {
20
+ const [busy, setBusy] = useState(false);
21
+
22
+ const click = async () => {
23
+ if (busy) return;
24
+ if (!confirm(confirmMessage)) return;
25
+ setBusy(true);
26
+ try {
27
+ await onConfirm();
28
+ } finally {
29
+ setBusy(false);
30
+ }
31
+ };
32
+
33
+ return (
34
+ <Button
35
+ variant="outline"
36
+ size="sm"
37
+ onClick={click}
38
+ disabled={busy || disabled}
39
+ className="text-destructive hover:bg-destructive/10 hover:text-destructive border-destructive/30"
40
+ >
41
+ <Trash2 className="h-3.5 w-3.5" />
42
+ {busy ? "Deleting…" : label}
43
+ </Button>
44
+ );
45
+ }
@@ -0,0 +1,34 @@
1
+ import { cn } from "@/lib/utils";
2
+ import type { LucideIcon } from "lucide-react";
3
+
4
+ export function EmptyState({
5
+ icon: Icon,
6
+ title,
7
+ description,
8
+ action,
9
+ className,
10
+ }: {
11
+ icon?: LucideIcon;
12
+ title: string;
13
+ description?: string;
14
+ action?: React.ReactNode;
15
+ className?: string;
16
+ }) {
17
+ return (
18
+ <div
19
+ className={cn(
20
+ "flex flex-col items-center justify-center text-center py-12 px-6 border border-dashed border-border rounded-xl bg-muted/20",
21
+ className,
22
+ )}
23
+ >
24
+ {Icon && (
25
+ <Icon className="h-8 w-8 text-muted-foreground/60 mb-3" strokeWidth={1.5} />
26
+ )}
27
+ <div className="text-sm font-medium text-foreground">{title}</div>
28
+ {description && (
29
+ <p className="text-sm text-muted-foreground mt-1 max-w-md">{description}</p>
30
+ )}
31
+ {action && <div className="mt-4">{action}</div>}
32
+ </div>
33
+ );
34
+ }
@@ -0,0 +1,34 @@
1
+ import { Sparkles } from "lucide-react";
2
+ import { Badge } from "@/components/ui/badge";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ export function LearningsBadge({
6
+ count,
7
+ size = "md",
8
+ className,
9
+ }: {
10
+ count: number;
11
+ size?: "md" | "sm";
12
+ className?: string;
13
+ }) {
14
+ if (count <= 0) return null;
15
+ const isSmall = size === "sm";
16
+ return (
17
+ <Badge
18
+ variant="outline"
19
+ className={cn(
20
+ "border-amber-500/40 gap-1",
21
+ isSmall ? "h-4 px-1.5 text-[10px] shrink-0" : "h-5",
22
+ className,
23
+ )}
24
+ >
25
+ <Sparkles
26
+ className={cn(
27
+ "text-amber-500",
28
+ isSmall ? "h-2.5 w-2.5" : "h-3 w-3",
29
+ )}
30
+ />
31
+ {count} learning applied
32
+ </Badge>
33
+ );
34
+ }
@@ -0,0 +1,34 @@
1
+ import { cn } from "@/lib/utils";
2
+
3
+ export function PageHeader({
4
+ title,
5
+ description,
6
+ actions,
7
+ className,
8
+ }: {
9
+ title: string;
10
+ description?: string;
11
+ actions?: React.ReactNode;
12
+ className?: string;
13
+ }) {
14
+ return (
15
+ <div
16
+ className={cn(
17
+ "border-b border-border px-6 py-5 flex flex-wrap items-start justify-between gap-4",
18
+ className,
19
+ )}
20
+ >
21
+ <div className="min-w-[min(18rem,100%)] flex-1">
22
+ <h1 className="text-xl font-semibold tracking-tight">{title}</h1>
23
+ {description && (
24
+ <p className="text-sm text-muted-foreground mt-0.5">{description}</p>
25
+ )}
26
+ </div>
27
+ {actions && (
28
+ <div className="flex max-w-full flex-wrap items-center justify-end gap-2">
29
+ {actions}
30
+ </div>
31
+ )}
32
+ </div>
33
+ );
34
+ }
@@ -0,0 +1,115 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import type { LucideIcon } from "lucide-react";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ export interface PageTabItem {
8
+ id: string;
9
+ label: string;
10
+ description?: string;
11
+ count?: number;
12
+ href?: string;
13
+ icon?: LucideIcon;
14
+ }
15
+
16
+ interface PageTabsProps {
17
+ items: PageTabItem[];
18
+ activeId: string;
19
+ onSelect?: (id: string) => void;
20
+ className?: string;
21
+ }
22
+
23
+ export function PageTabs({
24
+ items,
25
+ activeId,
26
+ onSelect,
27
+ className,
28
+ }: PageTabsProps) {
29
+ return (
30
+ <div
31
+ className={cn(
32
+ "grid gap-2 sm:grid-cols-[repeat(auto-fit,minmax(220px,1fr))]",
33
+ className,
34
+ )}
35
+ role="tablist"
36
+ >
37
+ {items.map((item) => {
38
+ const active = item.id === activeId;
39
+ const Icon = item.icon;
40
+ const content = (
41
+ <>
42
+ <span className="flex items-start justify-between gap-3">
43
+ <span className="flex min-w-0 items-center gap-2">
44
+ {Icon && (
45
+ <Icon
46
+ className={cn(
47
+ "mt-0.5 h-4 w-4 shrink-0",
48
+ active ? "text-foreground" : "text-muted-foreground",
49
+ )}
50
+ />
51
+ )}
52
+ <span className="truncate text-sm font-semibold">
53
+ {item.label}
54
+ </span>
55
+ </span>
56
+ {item.count !== undefined && (
57
+ <span
58
+ className={cn(
59
+ "rounded-md border px-1.5 py-0.5 font-mono text-[10px]",
60
+ active
61
+ ? "border-foreground/20 bg-background text-foreground"
62
+ : "border-border bg-muted/40 text-muted-foreground",
63
+ )}
64
+ >
65
+ {item.count}
66
+ </span>
67
+ )}
68
+ </span>
69
+ {item.description && (
70
+ <span className="mt-1 block text-xs leading-relaxed text-muted-foreground">
71
+ {item.description}
72
+ </span>
73
+ )}
74
+ </>
75
+ );
76
+
77
+ const className = cn(
78
+ "relative rounded-lg border px-3 py-2.5 text-left transition-colors",
79
+ "hover:border-foreground/25 hover:bg-accent/40 focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/50",
80
+ active
81
+ ? "border-foreground/25 bg-card text-foreground shadow-sm before:absolute before:inset-x-3 before:top-0 before:h-0.5 before:rounded-full before:bg-foreground"
82
+ : "border-border bg-background/60 text-muted-foreground",
83
+ );
84
+
85
+ if (item.href) {
86
+ return (
87
+ <Link
88
+ key={item.id}
89
+ href={item.href}
90
+ className={className}
91
+ role="tab"
92
+ aria-selected={active}
93
+ aria-current={active ? "page" : undefined}
94
+ >
95
+ {content}
96
+ </Link>
97
+ );
98
+ }
99
+
100
+ return (
101
+ <button
102
+ key={item.id}
103
+ type="button"
104
+ className={className}
105
+ role="tab"
106
+ aria-selected={active}
107
+ onClick={() => onSelect?.(item.id)}
108
+ >
109
+ {content}
110
+ </button>
111
+ );
112
+ })}
113
+ </div>
114
+ );
115
+ }
@@ -0,0 +1,38 @@
1
+ import { cn } from "@/lib/utils";
2
+ import type { LucideIcon } from "lucide-react";
3
+
4
+ export function StatCard({
5
+ label,
6
+ value,
7
+ hint,
8
+ icon: Icon,
9
+ className,
10
+ }: {
11
+ label: string;
12
+ value: string | number;
13
+ hint?: string;
14
+ icon?: LucideIcon;
15
+ className?: string;
16
+ }) {
17
+ return (
18
+ <div
19
+ className={cn(
20
+ "rounded-xl border border-border bg-card px-5 py-4 flex items-start justify-between gap-4",
21
+ className,
22
+ )}
23
+ >
24
+ <div className="min-w-0">
25
+ <div className="text-xs uppercase tracking-wide text-muted-foreground font-medium">
26
+ {label}
27
+ </div>
28
+ <div className="mt-1.5 text-2xl font-semibold tracking-tight tabular-nums">
29
+ {value}
30
+ </div>
31
+ {hint && (
32
+ <div className="text-xs text-muted-foreground mt-1">{hint}</div>
33
+ )}
34
+ </div>
35
+ {Icon && <Icon className="h-4 w-4 text-muted-foreground mt-0.5" />}
36
+ </div>
37
+ );
38
+ }
@@ -0,0 +1,22 @@
1
+ import {
2
+ LayoutDashboard,
3
+ MessageSquare,
4
+ Users,
5
+ BookOpen,
6
+ Settings,
7
+ type LucideIcon,
8
+ } from "lucide-react";
9
+
10
+ export interface NavItem {
11
+ href: string;
12
+ label: string;
13
+ icon: LucideIcon;
14
+ }
15
+
16
+ export const navItems: NavItem[] = [
17
+ { href: "/dashboard", label: "Dashboard", icon: LayoutDashboard },
18
+ { href: "/sessions", label: "Sessions", icon: MessageSquare },
19
+ { href: "/preferences", label: "Preferences", icon: Users },
20
+ { href: "/skills", label: "Skills", icon: BookOpen },
21
+ { href: "/configure", label: "Configure", icon: Settings },
22
+ ];
@@ -0,0 +1,45 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { usePathname } from "next/navigation";
5
+ import { cn } from "@/lib/utils";
6
+ import { ScrollArea } from "@/components/ui/scroll-area";
7
+ import { navItems } from "./nav-items";
8
+
9
+ export function Sidebar() {
10
+ const pathname = usePathname();
11
+
12
+ return (
13
+ <ScrollArea className="h-full">
14
+ <div className="px-3 py-4">
15
+ <div className="px-2 mb-6">
16
+ <div className="font-semibold text-sm">Claude-Smart</div>
17
+ <div className="text-[11px] text-muted-foreground font-mono">dashboard</div>
18
+ </div>
19
+
20
+ <nav className="space-y-0.5">
21
+ {navItems.map((item) => {
22
+ const active =
23
+ pathname === item.href || pathname.startsWith(`${item.href}/`);
24
+ const Icon = item.icon;
25
+ return (
26
+ <Link
27
+ key={item.href}
28
+ href={item.href}
29
+ className={cn(
30
+ "flex items-center gap-2 px-2 py-1.5 text-sm rounded-md transition-colors",
31
+ active
32
+ ? "bg-accent text-accent-foreground font-medium"
33
+ : "text-muted-foreground hover:text-foreground hover:bg-accent/50",
34
+ )}
35
+ >
36
+ <Icon className="h-4 w-4 shrink-0" />
37
+ <span className="truncate">{item.label}</span>
38
+ </Link>
39
+ );
40
+ })}
41
+ </nav>
42
+ </div>
43
+ </ScrollArea>
44
+ );
45
+ }
@@ -0,0 +1,64 @@
1
+ "use client";
2
+
3
+ import { useTheme } from "next-themes";
4
+ import Image from "next/image";
5
+ import { Moon, Sun, Menu } from "lucide-react";
6
+ import { Input } from "@/components/ui/input";
7
+ import { Button } from "@/components/ui/button";
8
+ import { useSettings } from "@/hooks/use-settings";
9
+ import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
10
+ import { Sidebar } from "./sidebar";
11
+
12
+ export function TopBar() {
13
+ const { reflexioUrl, setReflexioUrl } = useSettings();
14
+ const { theme, setTheme } = useTheme();
15
+
16
+ return (
17
+ <header className="h-14 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 flex items-center px-4 gap-3 shrink-0">
18
+ <Sheet>
19
+ <SheetTrigger
20
+ render={<Button variant="ghost" size="icon" className="lg:hidden" />}
21
+ >
22
+ <Menu className="h-4 w-4" />
23
+ </SheetTrigger>
24
+ <SheetContent side="left" className="w-72 p-0">
25
+ <Sidebar />
26
+ </SheetContent>
27
+ </Sheet>
28
+
29
+ <div className="flex items-center gap-2 flex-1 min-w-0">
30
+ <Image
31
+ src="/claude-smart-icon.png"
32
+ alt="claude-smart"
33
+ width={24}
34
+ height={24}
35
+ className="h-6 w-6 shrink-0"
36
+ priority
37
+ />
38
+ <span className="text-sm font-semibold whitespace-nowrap hidden sm:block">
39
+ Claude-Smart
40
+ </span>
41
+ <div className="mx-2 h-5 w-px bg-border hidden sm:block" />
42
+ <label className="text-xs text-muted-foreground whitespace-nowrap hidden sm:block">
43
+ Reflexio
44
+ </label>
45
+ <Input
46
+ value={reflexioUrl}
47
+ onChange={(e) => setReflexioUrl(e.target.value)}
48
+ placeholder="http://localhost:8071"
49
+ className="h-8 text-xs max-w-xs font-mono"
50
+ />
51
+ </div>
52
+
53
+ <Button
54
+ variant="ghost"
55
+ size="icon"
56
+ className="h-8 w-8"
57
+ onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
58
+ >
59
+ <Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
60
+ <Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
61
+ </Button>
62
+ </header>
63
+ );
64
+ }
@@ -0,0 +1,53 @@
1
+ "use client";
2
+
3
+ import { useStallState } from "@/hooks/use-stall-state";
4
+
5
+ /**
6
+ * Persistent top-of-page banner shown while reflexio learning is stalled.
7
+ * Renders nothing when state is null or clean.
8
+ */
9
+ export function StallBanner() {
10
+ const state = useStallState();
11
+ if (!state || !state.stalled) return null;
12
+
13
+ const text = renderText(state.reason, state.reset_estimate);
14
+ if (!text) return null;
15
+
16
+ return (
17
+ <div
18
+ role="status"
19
+ aria-live="polite"
20
+ className="w-full bg-amber-100 text-amber-900 border-b border-amber-300 px-4 py-2 text-sm dark:bg-amber-950 dark:text-amber-100 dark:border-amber-800"
21
+ >
22
+ {text}
23
+ </div>
24
+ );
25
+ }
26
+
27
+ function renderText(
28
+ reason: string | null,
29
+ resetEstimate: string | null,
30
+ ): string {
31
+ if (reason === "billing_error") {
32
+ if (resetEstimate) {
33
+ const formatted = formatReset(resetEstimate);
34
+ return `claude-smart: learning paused — Agent SDK credit exhausted (resets ~${formatted}).`;
35
+ }
36
+ return "claude-smart: learning paused — Agent SDK credit exhausted.";
37
+ }
38
+ if (reason === "auth_error") {
39
+ return "claude-smart: learning paused — please run /login in Claude Code.";
40
+ }
41
+ return "";
42
+ }
43
+
44
+ function formatReset(iso: string): string {
45
+ const d = new Date(iso);
46
+ if (Number.isNaN(d.getTime())) return iso;
47
+ return d.toLocaleString(undefined, {
48
+ month: "short",
49
+ day: "numeric",
50
+ hour: "2-digit",
51
+ minute: "2-digit",
52
+ });
53
+ }
@@ -0,0 +1,52 @@
1
+ import { mergeProps } from "@base-ui/react/merge-props"
2
+ import { useRender } from "@base-ui/react/use-render"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const badgeVariants = cva(
8
+ "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
13
+ secondary:
14
+ "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
15
+ destructive:
16
+ "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
17
+ outline:
18
+ "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
19
+ ghost:
20
+ "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ },
24
+ defaultVariants: {
25
+ variant: "default",
26
+ },
27
+ }
28
+ )
29
+
30
+ function Badge({
31
+ className,
32
+ variant = "default",
33
+ render,
34
+ ...props
35
+ }: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
36
+ return useRender({
37
+ defaultTagName: "span",
38
+ props: mergeProps<"span">(
39
+ {
40
+ className: cn(badgeVariants({ variant }), className),
41
+ },
42
+ props
43
+ ),
44
+ render,
45
+ state: {
46
+ slot: "badge",
47
+ variant,
48
+ },
49
+ })
50
+ }
51
+
52
+ export { Badge, badgeVariants }
@@ -0,0 +1,60 @@
1
+ "use client"
2
+
3
+ import { Button as ButtonPrimitive } from "@base-ui/react/button"
4
+ import { cva, type VariantProps } from "class-variance-authority"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const buttonVariants = cva(
9
+ "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
10
+ {
11
+ variants: {
12
+ variant: {
13
+ default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
14
+ outline:
15
+ "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
16
+ secondary:
17
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
18
+ ghost:
19
+ "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
20
+ destructive:
21
+ "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
22
+ link: "text-primary underline-offset-4 hover:underline",
23
+ },
24
+ size: {
25
+ default:
26
+ "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
27
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
28
+ sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
29
+ lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
30
+ icon: "size-8",
31
+ "icon-xs":
32
+ "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
33
+ "icon-sm":
34
+ "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
35
+ "icon-lg": "size-9",
36
+ },
37
+ },
38
+ defaultVariants: {
39
+ variant: "default",
40
+ size: "default",
41
+ },
42
+ }
43
+ )
44
+
45
+ function Button({
46
+ className,
47
+ variant = "default",
48
+ size = "default",
49
+ ...props
50
+ }: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
51
+ return (
52
+ <ButtonPrimitive
53
+ data-slot="button"
54
+ className={cn(buttonVariants({ variant, size, className }))}
55
+ {...props}
56
+ />
57
+ )
58
+ }
59
+
60
+ export { Button, buttonVariants }
@@ -0,0 +1,21 @@
1
+ "use client"
2
+
3
+ import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible"
4
+
5
+ function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) {
6
+ return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
7
+ }
8
+
9
+ function CollapsibleTrigger({ ...props }: CollapsiblePrimitive.Trigger.Props) {
10
+ return (
11
+ <CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} />
12
+ )
13
+ }
14
+
15
+ function CollapsibleContent({ ...props }: CollapsiblePrimitive.Panel.Props) {
16
+ return (
17
+ <CollapsiblePrimitive.Panel data-slot="collapsible-content" {...props} />
18
+ )
19
+ }
20
+
21
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent }
@@ -0,0 +1,20 @@
1
+ import * as React from "react"
2
+ import { Input as InputPrimitive } from "@base-ui/react/input"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
7
+ return (
8
+ <InputPrimitive
9
+ type={type}
10
+ data-slot="input"
11
+ className={cn(
12
+ "h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
13
+ className
14
+ )}
15
+ {...props}
16
+ />
17
+ )
18
+ }
19
+
20
+ export { Input }
@@ -0,0 +1,20 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ function Label({ className, ...props }: React.ComponentProps<"label">) {
8
+ return (
9
+ <label
10
+ data-slot="label"
11
+ className={cn(
12
+ "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
13
+ className
14
+ )}
15
+ {...props}
16
+ />
17
+ )
18
+ }
19
+
20
+ export { Label }