refactor: remove unused UI components and replace GlassCard with Card in voice components

- Deleted Item, Kbd, Marker, Message, NativeSelect, Questionnaire, Spinner components.
- Replaced GlassCard with Card in VoiceActivityTimeline, VoiceConnectionCard, ListenControl, MicControl, and SpeakerWaveform components.
- Introduced AppSidebar and ThemeToggle components for improved navigation and theme management.
This commit is contained in:
asepharyana
2026-08-07 17:33:12 +07:00
parent 2c995b41d7
commit 4f9d4a5c7d
50 changed files with 382 additions and 2710 deletions
@@ -7,10 +7,15 @@ import {
ChatbotProvider,
useChatbot,
} from "@/components/chatbot/chatbot-context";
import { HiddenSidebar } from "@/components/layout/hidden-sidebar";
import { MobileNav } from "@/components/layout/mobile-nav";
import { TopNav } from "@/components/layout/top-nav";
import { AppSidebar } from "@/components/layout/app-sidebar";
import { MiniPlayer } from "@/components/media/mini-player";
import { GuildSelector } from "@/components/shared/guild-selector";
import { Separator } from "@/components/ui/separator";
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
import { MediaPlayerProvider } from "@/lib/hooks/use-media-player";
import { useWebSocket, WsProvider } from "@/lib/ws/context";
@@ -69,29 +74,41 @@ export default function DashboardLayout({
<ChatbotProvider>
<ChatbotGuildSync guildId={guildId} />
<ChatbotExpressionSync />
<div className="min-h-screen bg-canvas">
<TopNav />
<HiddenSidebar
guildId={guildId}
onGuildChange={(g) => setGuildId(g ?? "")}
/>
<div className="min-h-svh bg-canvas">
<SidebarProvider>
<AppSidebar />
<SidebarInset className="gap-0">
<header className="flex h-14 shrink-0 items-center gap-2 border-b px-4 bg-canvas">
<SidebarTrigger className="-ml-1" />
<Separator
orientation="vertical"
className="mr-2 h-6 max-md:hidden"
/>
<div className="font-semibold max-md:hidden">Overview</div>
<div className="ms-auto">
<GuildSelector
value={guildId}
onChange={(g) => setGuildId(g ?? "")}
/>
</div>
</header>
{/* Sub-nav space — filled per-page */}
<div className="pt-11">
<main className="p-4 md:p-6 pb-24 md:pb-6 max-w-[1600px] mx-auto">
<Suspense
fallback={
<div className="flex h-[60vh] items-center justify-center">
<div className="size-8 rounded-full border-2 border-primary border-t-transparent animate-spin" />
</div>
}
>
{children}
</Suspense>
</main>
</div>
<main className="flex flex-1 flex-col gap-4 p-4 pb-28 md:p-6 lg:pb-8">
<div className="mx-auto w-full max-w-[1440px]">
<Suspense
fallback={
<div className="flex h-[60vh] items-center justify-center">
<div className="size-8 rounded-full border-2 border-primary border-t-transparent animate-spin" />
</div>
}
>
{children}
</Suspense>
</div>
</main>
</SidebarInset>
</SidebarProvider>
<MobileNav />
<MiniPlayer />
<ChatbotContainer />
</div>
@@ -3,8 +3,6 @@
import { Flag, Image, Loader2, Search } from "lucide-react";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
import { GlassCard } from "@/components/glass/card";
import { GlassPanel } from "@/components/glass/panel";
import { SubNav } from "@/components/layout/sub-nav";
import { Lightbox } from "@/components/messages/lightbox";
import { extractFirstImage } from "@/components/messages/message-card";
@@ -13,6 +11,7 @@ import { MessageList } from "@/components/messages/message-list";
import { SearchOverlay } from "@/components/messages/search-overlay";
import { EmptyState, ErrorState, LoadingSkeleton } from "@/components/shared";
import { GuildSelector } from "@/components/shared/guild-selector";
import { Card } from "@/components/ui/card";
import {
Select,
SelectContent,
@@ -218,12 +217,14 @@ export default function MessagesView({
{detailId && (
<div className="sticky top-16 hidden w-1/2 self-start md:block lg:w-3/5">
{detailLoading ? (
<GlassPanel
dense
className="flex items-center justify-center py-12"
<Card
className={cn(
"flex items-center justify-center py-12",
"[--card-spacing:0px]",
)}
>
<Loader2 className="size-5 animate-spin text-text-secondary/60" />
</GlassPanel>
</Card>
) : detailMessage ? (
<div className="space-y-3">
<button
@@ -337,10 +338,14 @@ function ReviewList({
return (
<div className="space-y-2">
{items.map((item) => (
<GlassCard
<Card
key={item.id}
variant="danger"
className="cursor-pointer p-3"
className={cn(
"cursor-pointer p-3",
"border border-red-500/30 ring-red-500/20",
"[--card-spacing:0px]",
"rounded-2xl",
)}
onClick={() => onSelect(item.id)}
>
<div className="flex items-start gap-2">
@@ -351,7 +356,7 @@ function ReviewList({
</p>
</div>
</div>
</GlassCard>
</Card>
))}
{items.length === 0 && (
<EmptyState
+20
View File
@@ -56,6 +56,16 @@
/* Ring / focus outline for shadcn outline-ring utility */
--color-ring: var(--color-primary);
/* Sidebar (shadcn) */
--color-sidebar: oklch(1 0 0 / 0.6);
--color-sidebar-foreground: var(--color-text-primary);
--color-sidebar-primary: var(--color-primary);
--color-sidebar-primary-foreground: var(--color-primary-foreground);
--color-sidebar-accent: oklch(0.93 0.01 250 / 0.7);
--color-sidebar-accent-foreground: var(--color-text-primary);
--color-sidebar-border: var(--color-border);
--color-sidebar-ring: var(--color-ring);
/* Radius */
--radius-card: 16px;
--radius-panel: 12px;
@@ -102,6 +112,16 @@
--color-accent: var(--color-primary);
--color-accent-foreground: var(--color-primary-foreground);
--color-ring: var(--color-primary);
/* Sidebar (shadcn) */
--color-sidebar: oklch(0.11 0.02 245 / 0.55);
--color-sidebar-foreground: var(--color-text-primary);
--color-sidebar-primary: var(--color-primary);
--color-sidebar-primary-foreground: var(--color-primary-foreground);
--color-sidebar-accent: oklch(0.17 0.02 245 / 0.7);
--color-sidebar-accent-foreground: var(--color-text-primary);
--color-sidebar-border: var(--color-border);
--color-sidebar-ring: var(--color-ring);
}
@layer utilities {
+10 -1
View File
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import { Inter, JetBrains_Mono } from "next/font/google";
import { ThemeProvider } from "next-themes";
import { Toaster } from "@/components/ui/sonner";
import "./globals.css";
@@ -30,7 +31,15 @@ export default function RootLayout({
suppressHydrationWarning
>
<body className="min-h-full flex flex-col">
{children}
<ThemeProvider
attribute="class"
defaultTheme="dark"
enableSystem={false}
enableColorScheme={false}
disableTransitionOnChange
>
{children}
</ThemeProvider>
<Toaster position="bottom-right" richColors closeButton />
</body>
</html>
@@ -9,8 +9,9 @@ import {
XAxis,
YAxis,
} from "recharts";
import { GlassCard } from "@/components/glass/card";
import { Card } from "@/components/ui/card";
import { useMounted } from "@/lib/hooks/use-mounted";
import { cn } from "@/lib/utils";
interface ActivityChartProps {
data?: { day: string; messages: number; flagged: number }[];
@@ -28,7 +29,7 @@ export function ActivityChart({ data = [] }: ActivityChartProps) {
const mounted = useMounted();
return (
<GlassCard variant="base">
<Card className={cn("[--card-spacing:0px]", "rounded-2xl", "p-5")}>
<div className="flex items-center justify-between mb-3">
<span className="text-xs font-semibold tracking-wide uppercase text-text-secondary">
Message Activity
@@ -123,6 +124,6 @@ export function ActivityChart({ data = [] }: ActivityChartProps) {
<div className="h-full w-full animate-pulse rounded-md bg-card/40" />
)}
</div>
</GlassCard>
</Card>
);
}
@@ -2,7 +2,6 @@
import { Hash, Search } from "lucide-react";
import { useCallback, useState } from "react";
import { GlassCard } from "@/components/glass/card";
import { EmptyState, LoadingSkeleton } from "@/components/shared";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -11,6 +10,7 @@ import { Input } from "@/components/ui/input";
import { useChannelDetail, useChannels } from "@/hooks";
import { renderMessageContent } from "@/lib/format";
import type { DashboardChannel } from "@/lib/types";
import { cn } from "@/lib/utils";
export function ChannelsSection({ guildId }: { guildId?: string }) {
const [search, setSearch] = useState("");
@@ -31,7 +31,14 @@ export function ChannelsSection({ guildId }: { guildId?: string }) {
if (error) {
return (
<GlassCard variant="danger" className="p-6 text-sm">
<Card
className={cn(
"p-6 text-sm",
"border border-red-500/30 ring-red-500/20",
"[--card-spacing:0px]",
"rounded-2xl",
)}
>
Failed to load channels: {error.message}
<Button
variant="outline"
@@ -41,7 +48,7 @@ export function ChannelsSection({ guildId }: { guildId?: string }) {
>
Retry
</Button>
</GlassCard>
</Card>
);
}
@@ -76,7 +83,9 @@ export function ChannelsSection({ guildId }: { guildId?: string }) {
)}
</div>
<GlassCard variant="base" className="h-fit">
<Card
className={cn("h-fit", "[--card-spacing:0px]", "rounded-2xl", "p-5")}
>
{detail ? (
<div className="space-y-3">
<div className="flex items-center gap-2">
@@ -144,7 +153,7 @@ export function ChannelsSection({ guildId }: { guildId?: string }) {
</p>
</div>
)}
</GlassCard>
</Card>
</div>
);
}
@@ -9,8 +9,9 @@ import {
XAxis,
YAxis,
} from "recharts";
import { GlassCard } from "@/components/glass/card";
import { Card } from "@/components/ui/card";
import { useMounted } from "@/lib/hooks/use-mounted";
import { cn } from "@/lib/utils";
interface HourlyActivityChartProps {
data?: { hour: number; messages: number; flagged: number }[];
@@ -46,7 +47,7 @@ export function HourlyActivityChart({ data = [] }: HourlyActivityChartProps) {
const maxMessages = Math.max(...full.map((d) => d.messages));
return (
<GlassCard variant="base">
<Card className={cn("[--card-spacing:0px]", "rounded-2xl", "p-5")}>
<div className="flex items-center justify-between mb-3">
<span className="text-xs font-semibold tracking-wide uppercase text-text-secondary">
Hourly Activity
@@ -103,6 +104,6 @@ export function HourlyActivityChart({ data = [] }: HourlyActivityChartProps) {
<div className="h-full w-full animate-pulse rounded-md bg-card/40" />
)}
</div>
</GlassCard>
</Card>
);
}
@@ -1,8 +1,9 @@
"use client";
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts";
import { GlassCard } from "@/components/glass/card";
import { Card } from "@/components/ui/card";
import { useMounted } from "@/lib/hooks/use-mounted";
import { cn } from "@/lib/utils";
interface ModerationDonutProps {
data?: { name: string; value: number; color: string }[];
@@ -27,7 +28,7 @@ export function ModerationDonut({ data = [] }: ModerationDonutProps) {
: 0;
return (
<GlassCard variant="base">
<Card className={cn("[--card-spacing:0px]", "rounded-2xl", "p-5")}>
<div className="flex items-center justify-between mb-1">
<span className="text-xs font-semibold tracking-wide uppercase text-text-secondary">
Moderation Breakdown
@@ -96,6 +97,6 @@ export function ModerationDonut({ data = [] }: ModerationDonutProps) {
)}
</div>
</div>
</GlassCard>
</Card>
);
}
@@ -1,11 +1,12 @@
"use client";
import { Flame, Heart, SmilePlus } from "lucide-react";
import { GlassCard } from "@/components/glass/card";
import { EmptyState, LoadingSkeleton } from "@/components/shared";
import { Badge } from "@/components/ui/badge";
import { Card } from "@/components/ui/card";
import { useTopReactions, useTopReactors } from "@/hooks";
import { renderMessageContent } from "@/lib/format";
import { cn } from "@/lib/utils";
function formatReactionTime(ts: number | null): string {
if (!ts) return "";
@@ -31,19 +32,23 @@ export function ReactionsSection() {
{reactionsLoading ? (
<LoadingSkeleton count={5} height="h-16" />
) : !reactions || reactions.length === 0 ? (
<GlassCard className="p-6">
<Card className={cn("p-6", "[--card-spacing:0px]", "rounded-2xl")}>
<EmptyState
icon={Heart}
title="Belum ada reaksi"
description="Pesan dengan reaksi emoji akan muncul di sini."
/>
</GlassCard>
</Card>
) : (
<div className="space-y-2">
{reactions.map((r, i) => (
<GlassCard
<Card
key={r.message_id}
className="flex items-center gap-3 p-3"
className={cn(
"flex items-center gap-3 p-3",
"[--card-spacing:0px]",
"rounded-2xl",
)}
>
<span className="w-6 shrink-0 text-center font-mono text-xs text-text-secondary/50">
{i + 1}
@@ -76,7 +81,7 @@ export function ReactionsSection() {
<Heart className="size-3" />
{r.reaction_count}
</Badge>
</GlassCard>
</Card>
))}
</div>
)}
@@ -90,19 +95,23 @@ export function ReactionsSection() {
{reactorsLoading ? (
<LoadingSkeleton count={5} height="h-14" />
) : !reactors || reactors.length === 0 ? (
<GlassCard className="p-6">
<Card className={cn("p-6", "[--card-spacing:0px]", "rounded-2xl")}>
<EmptyState
icon={SmilePlus}
title="Belum ada reaktor"
description="User yang ngasih reaksi emoji akan muncul di sini."
/>
</GlassCard>
</Card>
) : (
<div className="space-y-2">
{reactors.map((r, i) => (
<GlassCard
<Card
key={r.user_id}
className="flex items-center gap-3 p-3"
className={cn(
"flex items-center gap-3 p-3",
"[--card-spacing:0px]",
"rounded-2xl",
)}
>
<span className="w-6 shrink-0 text-center font-mono text-xs text-text-secondary/50">
{i + 1}
@@ -120,7 +129,7 @@ export function ReactionsSection() {
<Flame className="size-3" />
{r.net_count}
</Badge>
</GlassCard>
</Card>
))}
</div>
)}
@@ -2,7 +2,7 @@
import type { LucideIcon } from "lucide-react";
import { Area, AreaChart, ResponsiveContainer } from "recharts";
import { GlassCard } from "@/components/glass/card";
import { Card } from "@/components/ui/card";
import { useMounted } from "@/lib/hooks/use-mounted";
import { cn } from "@/lib/utils";
@@ -39,7 +39,13 @@ export function StatCard({
const numValue = typeof value === "number" ? value : Number(value);
return (
<GlassCard variant="base" className="relative overflow-hidden p-4">
<Card
className={cn(
"relative overflow-hidden p-4",
"[--card-spacing:0px]",
"rounded-2xl",
)}
>
<div className="flex items-start justify-between mb-2">
<div className={cn("p-1.5 rounded-md", bgAccent)}>
<Icon className="size-4" />
@@ -90,6 +96,6 @@ export function StatCard({
</ResponsiveContainer>
</div>
)}
</GlassCard>
</Card>
);
}
@@ -8,8 +8,9 @@ import {
XAxis,
YAxis,
} from "recharts";
import { GlassCard } from "@/components/glass/card";
import { Card } from "@/components/ui/card";
import { useMounted } from "@/lib/hooks/use-mounted";
import { cn } from "@/lib/utils";
interface TopChannelsChartProps {
data?: { name: string; count: number }[];
@@ -19,7 +20,7 @@ export function TopChannelsChart({ data = [] }: TopChannelsChartProps) {
const mounted = useMounted();
return (
<GlassCard variant="base">
<Card className={cn("[--card-spacing:0px]", "rounded-2xl", "p-5")}>
<div className="flex items-center gap-2 mb-3">
<span className="text-xs font-semibold tracking-wide uppercase text-text-secondary">
Top Channels
@@ -68,6 +69,6 @@ export function TopChannelsChart({ data = [] }: TopChannelsChartProps) {
<div className="h-full w-full animate-pulse rounded-md bg-card/40" />
)}
</div>
</GlassCard>
</Card>
);
}
@@ -2,7 +2,6 @@
import { Search, Users, UserX } from "lucide-react";
import { useCallback, useState } from "react";
import { GlassCard } from "@/components/glass/card";
import { EmptyState, LoadingSkeleton } from "@/components/shared";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
@@ -12,6 +11,7 @@ import { Input } from "@/components/ui/input";
import { useUserDetail, useUsers } from "@/hooks";
import { renderMessageContent } from "@/lib/format";
import type { DashboardUser } from "@/lib/types";
import { cn } from "@/lib/utils";
const TRUST_TIERS = [
{
@@ -67,7 +67,14 @@ export function UsersSection() {
if (error) {
return (
<GlassCard variant="danger" className="p-6 text-sm">
<Card
className={cn(
"p-6 text-sm",
"border border-red-500/30 ring-red-500/20",
"[--card-spacing:0px]",
"rounded-2xl",
)}
>
Failed to load users: {error.message}
<Button
variant="outline"
@@ -77,7 +84,7 @@ export function UsersSection() {
>
Retry
</Button>
</GlassCard>
</Card>
);
}
@@ -112,7 +119,9 @@ export function UsersSection() {
)}
</div>
<GlassCard variant="base" className="h-fit">
<Card
className={cn("h-fit", "[--card-spacing:0px]", "rounded-2xl", "p-5")}
>
{detail ? (
<div className="space-y-3">
<div className="flex items-center gap-3">
@@ -196,7 +205,7 @@ export function UsersSection() {
</p>
</div>
)}
</GlassCard>
</Card>
</div>
);
}
@@ -1,31 +0,0 @@
"use client";
import type { ComponentPropsWithoutRef } from "react";
import { cn } from "@/lib/utils";
type GlassVariant = "base" | "elevated" | "interactive" | "danger";
interface GlassCardProps extends ComponentPropsWithoutRef<"div"> {
variant?: GlassVariant;
}
const variantStyles: Record<GlassVariant, string> = {
base: "glass rounded-[var(--radius-card)]",
elevated: "glass-elevated rounded-[var(--radius-card)]",
interactive:
"glass rounded-[var(--radius-card)] transition-all duration-150 hover:scale-[1.01] hover:border-[var(--color-border-glow)] cursor-pointer",
danger: "glass rounded-[var(--radius-card)] border-red-500/30",
};
export function GlassCard({
variant = "base",
className,
children,
...props
}: GlassCardProps) {
return (
<div className={cn(variantStyles[variant], "p-5", className)} {...props}>
{children}
</div>
);
}
@@ -1,2 +0,0 @@
export { GlassCard } from "./card";
export { GlassPanel } from "./panel";
@@ -1,28 +0,0 @@
"use client";
import type { ComponentPropsWithoutRef } from "react";
import { cn } from "@/lib/utils";
interface GlassPanelProps extends ComponentPropsWithoutRef<"div"> {
dense?: boolean;
}
export function GlassPanel({
dense = false,
className,
children,
...props
}: GlassPanelProps) {
return (
<div
className={cn(
"glass rounded-[var(--radius-panel)]",
dense ? "p-3" : "p-4",
className,
)}
{...props}
>
{children}
</div>
);
}
@@ -0,0 +1,84 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { ThemeToggle } from "@/components/layout/theme-toggle";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarRail,
} from "@/components/ui/sidebar";
import { isActivePath, navItems } from "@/lib/navigation";
/**
* Primary application navigation — pure shadcn Sidebar primitives.
* Renders as a fixed desktop rail (collapsible to icons) and a Sheet
* on mobile via the Sidebar component itself.
*/
export function AppSidebar() {
const pathname = usePathname();
return (
<Sidebar collapsible="icon">
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" render={<Link href="/dashboard" />}>
<div className="flex size-6 shrink-0 items-center justify-center rounded-md bg-gradient-to-br from-cyan-500 to-teal-400 text-white text-[10px] font-bold">
D
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">Discord Automod</span>
<span className="truncate text-xs text-muted-foreground">
Moderation dashboard
</span>
</div>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
<SidebarMenu>
{navItems.map(({ href, label, icon: Icon, matchPrefix }) => {
const active = isActivePath(pathname, matchPrefix);
return (
<SidebarMenuItem key={href}>
<SidebarMenuButton
render={<Link href={href} />}
isActive={active}
tooltip={label}
className="group-data-[collapsible=icon]:size-8 group-data-[collapsible=icon]:justify-center"
>
<Icon />
<span>{label}</span>
</SidebarMenuButton>
</SidebarMenuItem>
);
})}
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<ThemeToggle />
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
<SidebarRail />
</Sidebar>
);
}
@@ -1,54 +0,0 @@
"use client";
import { useState } from "react";
import { GuildSelector } from "@/components/shared/guild-selector";
interface HiddenSidebarProps {
guildId: string;
onGuildChange: (guildId: string) => void;
}
export function HiddenSidebar({ guildId, onGuildChange }: HiddenSidebarProps) {
const [visible, setVisible] = useState(false);
let hideTimer: ReturnType<typeof setTimeout> | null = null;
const handleMouseEnter = () => {
if (hideTimer) clearTimeout(hideTimer);
setVisible(true);
};
const handleMouseLeave = () => {
hideTimer = setTimeout(() => setVisible(false), 300);
};
return (
<>
{/* Hotspot trigger */}
{/* biome-ignore lint/a11y/noStaticElementInteractions: transparent mouse detection zone, not interactive content */}
<div
className="fixed left-0 top-0 bottom-0 w-1 z-50"
onMouseEnter={handleMouseEnter}
/>
{/* Sidebar */}
<div
role="region"
aria-label="Guild selector sidebar"
className={`fixed left-0 top-0 bottom-0 z-40 w-56 glass-intense border-r border-glass-border transition-transform duration-150 ease-out ${
visible ? "translate-x-0" : "-translate-x-full"
}`}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div className="flex h-11 items-center gap-2 px-4 border-b border-glass-border">
<span className="text-xs font-semibold tracking-wider uppercase text-text-secondary">
Guilds
</span>
</div>
<div className="p-3 space-y-4">
<GuildSelector value={guildId} onChange={onGuildChange} />
</div>
</div>
</>
);
}
@@ -1,41 +0,0 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { isActivePath, mobileNavItems } from "@/lib/navigation";
import { cn } from "@/lib/utils";
export function MobileNav() {
const pathname = usePathname();
return (
<nav className="md:hidden fixed bottom-0 inset-x-0 z-30 glass-intense border-t border-glass-border">
<div className="flex items-center justify-around h-14 px-2">
{mobileNavItems.map(({ href, label, icon: Icon, matchPrefix }) => {
const active = isActivePath(pathname, matchPrefix);
return (
<Link
key={href}
href={href}
className={cn(
"flex flex-col items-center gap-0.5 py-1 px-3 rounded-lg transition-all relative min-w-0",
active
? "text-primary"
: "text-text-secondary/50 hover:text-text-secondary/80",
)}
>
<Icon className="size-5" />
<span className="text-[10px] font-medium leading-tight">
{label}
</span>
{active && (
<span className="absolute -top-0.5 left-1/2 -translate-x-1/2 size-1 rounded-full bg-primary shadow-[0_0_6px] shadow-primary/80" />
)}
</Link>
);
})}
</div>
</nav>
);
}
@@ -0,0 +1,40 @@
"use client";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export function ThemeToggle() {
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button variant="ghost" size="sm" className="w-full justify-start" />
}
>
<Sun className="scale-100 dark:scale-0" />
<Moon className="absolute scale-0 dark:scale-100" />
<span className="truncate pl-1.5">Toggle theme</span>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-32">
<DropdownMenuItem onClick={() => setTheme("light")}>
<Sun className="mr-2 size-4" />
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
<Moon className="mr-2 size-4" />
Dark
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
@@ -1,87 +0,0 @@
"use client";
import { Moon, Sun } from "lucide-react";
import { usePathname, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { isActivePath, navItems } from "@/lib/navigation";
export function TopNav() {
const pathname = usePathname();
const router = useRouter();
const [theme, setTheme] = useState<"light" | "dark">("dark");
// Apply persisted theme on mount (client-side only — avoids SSR hydration
// mismatch from touching <html> during server render).
useEffect(() => {
const stored = localStorage.getItem("theme") as "light" | "dark" | null;
const initial =
stored === "light" ? "light" : stored === "dark" ? "dark" : "light";
setTheme(initial);
document.documentElement.classList.remove("light", "dark");
document.documentElement.classList.add(initial);
}, []);
const toggleTheme = () => {
const next = theme === "dark" ? "light" : "dark";
setTheme(next);
localStorage.setItem("theme", next);
document.documentElement.classList.remove("light", "dark");
document.documentElement.classList.add(next);
};
return (
<header className="fixed top-0 left-0 right-0 z-40 h-11 flex items-center gap-1 px-3 glass-intense border-b border-[var(--color-border-glow)]">
{/* Brand */}
<div className="flex items-center gap-2 mr-4 shrink-0">
<div className="relative flex size-6 items-center justify-center rounded-md bg-gradient-to-br from-cyan-500 to-teal-400 text-white text-[10px] font-bold">
D
<span className="absolute -top-0.5 -right-0.5 size-1.5 rounded-full bg-emerald-500 shadow-[0_0_6px] shadow-emerald-500/80 animate-pulse" />
</div>
<span className="text-xs font-semibold text-text-primary tracking-tight hidden sm:inline">
Discord Automod
</span>
</div>
{/* Nav links */}
<nav className="flex items-center gap-0.5 flex-1 justify-center">
{navItems.map(({ href, label, icon: Icon, matchPrefix }) => {
const active = isActivePath(pathname, matchPrefix);
return (
<button
key={href}
type="button"
onClick={() => router.push(href)}
className={`relative flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all duration-150 ${
active
? "text-text-primary"
: "text-text-secondary/60 hover:text-text-primary/80"
}`}
>
<Icon className="size-3.5" />
<span className="hidden sm:inline">{label}</span>
{active && (
<span className="absolute bottom-0 left-1/2 -translate-x-1/2 w-6 h-0.5 rounded-full bg-primary shadow-[0_0_8px] shadow-primary/60" />
)}
</button>
);
})}
</nav>
{/* Right side */}
<div className="flex items-center gap-2 shrink-0">
<button
type="button"
onClick={toggleTheme}
className="size-7 flex items-center justify-center rounded-md text-text-secondary/60 hover:text-text-primary hover:bg-glass-bg transition-all"
aria-label="Toggle theme"
>
{theme === "dark" ? (
<Moon className="size-3.5" />
) : (
<Sun className="size-3.5" />
)}
</button>
</div>
</header>
);
}
@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import { GlassPanel } from "@/components/glass/panel";
import { Card } from "@/components/ui/card";
import { cn } from "@/lib/utils";
interface AiAnalysisPanelProps {
@@ -37,11 +37,11 @@ export function AiAnalysisPanel({
if (!status || status === "pending") {
return (
<GlassPanel dense>
<Card className={cn("[--card-spacing:0px]", "p-3")}>
<span className="text-xs text-text-secondary/50">
AI analysis pending
</span>
</GlassPanel>
</Card>
);
}
@@ -55,7 +55,7 @@ export function AiAnalysisPanel({
: categories || [];
return (
<GlassPanel dense className="space-y-2">
<Card className={cn("space-y-2", "[--card-spacing:0px]", "p-3")}>
<div className="flex items-center gap-2">
<span className="text-xs font-semibold tracking-wide uppercase text-text-secondary">
AI Analysis
@@ -155,6 +155,6 @@ export function AiAnalysisPanel({
<span className="font-mono text-accent-amber">{action}</span>
</div>
)}
</GlassPanel>
</Card>
);
}
@@ -1,9 +1,10 @@
"use client";
import { ArrowLeft, MessageSquare, MessagesSquare, Pencil } from "lucide-react";
import { GlassCard } from "@/components/glass/card";
import { Card } from "@/components/ui/card";
import { getMessageChannelLabel, renderMessageContent } from "@/lib/format";
import type { AttachmentRecord, MessageRecord } from "@/lib/types";
import { cn } from "@/lib/utils";
import { AiAnalysisPanel } from "./ai-analysis-panel";
import { AttachmentsGrid } from "./attachments-grid";
@@ -21,7 +22,9 @@ export function MessageDetailView({
onImageClick,
}: MessageDetailViewProps) {
return (
<GlassCard variant="base" className="h-full">
<Card
className={cn("h-full", "[--card-spacing:0px]", "rounded-2xl", "p-5")}
>
{onBack && (
<button
type="button"
@@ -94,6 +97,6 @@ export function MessageDetailView({
score={message.ai_moderation_score}
analysis={message.ai_analysis}
/>
</GlassCard>
</Card>
);
}
@@ -1,9 +1,10 @@
"use client";
import { ArrowLeft, MessageSquare, MessagesSquare } from "lucide-react";
import { GlassCard } from "@/components/glass/card";
import { Card } from "@/components/ui/card";
import { getMessageChannelLabel, renderMessageContent } from "@/lib/format";
import type { AttachmentRecord, MessageRecord } from "@/lib/types";
import { cn } from "@/lib/utils";
import { AiAnalysisPanel } from "./ai-analysis-panel";
import { AttachmentsGrid } from "./attachments-grid";
@@ -19,7 +20,9 @@ export function MessageDetail({
onBack,
}: MessageDetailProps) {
return (
<GlassCard variant="base" className="h-full">
<Card
className={cn("h-full", "[--card-spacing:0px]", "rounded-2xl", "p-5")}
>
{onBack && (
<button
type="button"
@@ -68,6 +71,6 @@ export function MessageDetail({
score={message.ai_moderation_score}
analysis={message.ai_analysis}
/>
</GlassCard>
</Card>
);
}
@@ -12,9 +12,9 @@ import {
XCircle,
} from "lucide-react";
import { useState } from "react";
import { GlassCard } from "@/components/glass/card";
import { EmptyState, LoadingSkeleton } from "@/components/shared";
import { Badge } from "@/components/ui/badge";
import { Card } from "@/components/ui/card";
import { useModerationActions, useModerationStats } from "@/hooks";
import { renderMessageContent } from "@/lib/format";
import type {
@@ -173,13 +173,13 @@ export function ModerationSection({
{actionsLoading && !actions ? (
<LoadingSkeleton count={6} height="h-16" />
) : !actions || actions.length === 0 ? (
<GlassCard className="p-6">
<Card className={cn("p-6", "[--card-spacing:0px]", "rounded-2xl")}>
<EmptyState
icon={ShieldAlert}
title="Belum ada aksi moderasi"
description="Aksi auto- moderasi (delete, warn, kick, ban) akan muncul di sini."
/>
</GlassCard>
</Card>
) : (
<div className="space-y-2">
{actions.map((a) => (
@@ -207,7 +207,7 @@ function SummaryCard({
hint?: string;
}) {
return (
<GlassCard className="p-4">
<Card className={cn("p-4", "[--card-spacing:0px]", "rounded-2xl")}>
<p className="text-[10px] uppercase tracking-wide text-text-secondary/50">
{label}
</p>
@@ -217,7 +217,7 @@ function SummaryCard({
<span className="ml-1 text-xs font-medium opacity-80">({hint})</span>
)}
</p>
</GlassCard>
</Card>
);
}
@@ -251,7 +251,13 @@ function ActionRow({ action }: { action: ModerationAction }) {
const st = STATUS_META[action.status];
const Icon = meta.Icon;
return (
<GlassCard className="flex items-start gap-3 p-3">
<Card
className={cn(
"flex items-start gap-3 p-3",
"[--card-spacing:0px]",
"rounded-2xl",
)}
>
<span className={cn("mt-0.5 shrink-0", meta.className)}>
<Icon className="size-4" />
</span>
@@ -301,7 +307,7 @@ function ActionRow({ action }: { action: ModerationAction }) {
) : (
<Loader2 className="mt-0.5 size-3.5 shrink-0 animate-spin text-amber-500" />
)}
</GlassCard>
</Card>
);
}
@@ -2,9 +2,10 @@
import { Download, Loader2, Pause, Play } from "lucide-react";
import { useState } from "react";
import { GlassCard } from "@/components/glass/card";
import { Card } from "@/components/ui/card";
import { formatBytes } from "@/lib/format";
import type { VoiceRecording } from "@/lib/types";
import { cn } from "@/lib/utils";
interface RecordingCardProps {
recording: VoiceRecording;
@@ -55,13 +56,17 @@ export function RecordingCard({
};
return (
<GlassCard
variant="interactive"
className={`p-4 transition-all ${
active
? "ring-1 ring-primary/40 border-primary/30 animate-card-glow"
: "hover:ring-1 hover:ring-border/60"
}`}
<Card
className={cn(
`p-4 transition-all ${
active
? "ring-1 ring-primary/40 border-primary/30 animate-card-glow"
: "hover:ring-1 hover:ring-border/60"
}`,
"cursor-pointer transition-colors hover:ring-primary/40",
"[--card-spacing:0px]",
"rounded-2xl",
)}
onClick={() => onTogglePlay(recording.id)}
>
<div className="flex items-start gap-3">
@@ -151,6 +156,6 @@ export function RecordingCard({
)}
</div>
</div>
</GlassCard>
</Card>
);
}
@@ -2,7 +2,8 @@
import { Loader2, Pause, Play, X } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { GlassPanel } from "@/components/glass/panel";
import { Card } from "@/components/ui/card";
import { cn } from "@/lib/utils";
interface RecordingPlayerProps {
url?: string;
@@ -70,9 +71,12 @@ export function RecordingPlayer({
const pct = duration > 0 ? Math.min(100, (progress / duration) * 100) : 0;
return (
<GlassPanel
dense
className="fixed bottom-20 left-4 z-30 w-80 flex flex-col gap-1.5"
<Card
className={cn(
"fixed bottom-20 left-4 z-30 w-80 flex flex-col gap-1.5",
"[--card-spacing:0px]",
"p-3",
)}
>
<div className="flex items-center gap-2.5">
<button
@@ -140,6 +144,6 @@ export function RecordingPlayer({
}}
className="hidden"
/>
</GlassPanel>
</Card>
);
}
@@ -2,7 +2,7 @@
import type { LucideIcon } from "lucide-react";
import { Inbox } from "lucide-react";
import { GlassPanel } from "@/components/glass/panel";
import { Card } from "@/components/ui/card";
import { cn } from "@/lib/utils";
interface EmptyStateProps {
@@ -19,13 +19,16 @@ export function EmptyState({
className,
}: EmptyStateProps) {
return (
<GlassPanel
dense
className={cn("flex flex-col items-center gap-2 py-12", className)}
<Card
className={cn(
"flex flex-col items-center gap-2 py-12",
className,
"[--card-spacing:0px]",
)}
>
<Icon className="size-8 text-text-secondary/20" />
<p className="text-sm text-text-secondary/60">{title}</p>
<p className="text-xs text-text-secondary/40">{description}</p>
</GlassPanel>
</Card>
);
}
@@ -2,7 +2,8 @@
import { AlertCircle, RefreshCw } from "lucide-react";
import { Component, type ReactNode } from "react";
import { GlassCard } from "@/components/glass/card";
import { Card } from "@/components/ui/card";
import { cn } from "@/lib/utils";
interface Props {
children: ReactNode;
@@ -24,9 +25,13 @@ export class ErrorBoundary extends Component<Props, State> {
if (this.state.hasError) {
return (
this.props.fallback || (
<GlassCard
variant="danger"
className="flex flex-col items-center gap-2 py-8"
<Card
className={cn(
"flex flex-col items-center gap-2 py-8",
"border border-red-500/30 ring-red-500/20",
"[--card-spacing:0px]",
"rounded-2xl",
)}
>
<AlertCircle className="size-6 text-destructive" />
<p className="text-sm text-text-secondary">
@@ -39,7 +44,7 @@ export class ErrorBoundary extends Component<Props, State> {
>
<RefreshCw className="size-3" /> Try again
</button>
</GlassCard>
</Card>
)
);
}
@@ -1,207 +0,0 @@
import * as React from "react"
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
const attachmentVariants = cva(
"group/attachment relative flex w-fit max-w-full min-w-0 shrink-0 flex-wrap rounded-xl border bg-card text-card-foreground transition-colors focus-within:ring-1 focus-within:ring-ring/50 has-[>a,>button]:hover:bg-muted/50 data-[state=error]:border-destructive/30 data-[state=idle]:border-dashed",
{
variants: {
size: {
default:
"gap-2 text-sm has-data-[slot=attachment-content]:px-2.5 has-data-[slot=attachment-content]:py-2 has-data-[slot=attachment-media]:p-2",
sm: "gap-2.5 text-xs has-data-[slot=attachment-content]:px-2 has-data-[slot=attachment-content]:py-1.5 has-data-[slot=attachment-media]:p-1.5",
xs: "gap-1.5 rounded-lg text-xs has-data-[slot=attachment-content]:px-1.5 has-data-[slot=attachment-content]:py-1 has-data-[slot=attachment-media]:p-1",
},
orientation: {
horizontal: "min-w-40 items-center",
vertical: "w-24 flex-col has-data-[slot=attachment-content]:w-30",
},
},
}
)
function Attachment({
className,
state = "done",
size = "default",
orientation = "horizontal",
...props
}: React.ComponentProps<"div"> &
VariantProps<typeof attachmentVariants> & {
state?: "idle" | "uploading" | "processing" | "error" | "done"
}) {
return (
<div
data-slot="attachment"
data-state={state}
data-size={size}
data-orientation={orientation}
className={cn(attachmentVariants({ size, orientation }), className)}
{...props}
/>
)
}
const attachmentMediaVariants = cva(
"relative flex aspect-square w-10 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted text-foreground group-data-[orientation=vertical]/attachment:w-full group-data-[size=sm]/attachment:w-8 group-data-[size=xs]/attachment:w-7 group-data-[size=xs]/attachment:rounded-md group-data-[state=error]/attachment:bg-destructive/10 group-data-[state=error]/attachment:text-destructive group-data-[orientation=vertical]/attachment:*:data-[slot=spinner]:size-6! [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 group-data-[orientation=vertical]/attachment:[&_svg:not([class*='size-'])]:size-6 group-data-[size=xs]/attachment:[&_svg:not([class*='size-'])]:size-3.5",
{
variants: {
variant: {
icon: "",
image:
"opacity-60 group-data-[state=done]/attachment:opacity-100 group-data-[state=idle]/attachment:opacity-100 *:[img]:aspect-square *:[img]:w-full *:[img]:object-cover",
},
},
defaultVariants: {
variant: "icon",
},
}
)
function AttachmentMedia({
className,
variant = "icon",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof attachmentMediaVariants>) {
return (
<div
data-slot="attachment-media"
data-variant={variant}
className={cn(attachmentMediaVariants({ variant }), className)}
{...props}
/>
)
}
function AttachmentContent({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="attachment-content"
className={cn(
"max-w-full min-w-0 flex-1 leading-tight group-data-[orientation=vertical]/attachment:px-1",
className
)}
{...props}
/>
)
}
function AttachmentTitle({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="attachment-title"
className={cn(
"block max-w-full min-w-0 truncate font-medium group-data-[state=processing]/attachment:shimmer group-data-[state=uploading]/attachment:shimmer",
className
)}
{...props}
/>
)
}
function AttachmentDescription({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="attachment-description"
className={cn(
"mt-0.5 block min-w-0 truncate text-xs text-muted-foreground group-data-[state=error]/attachment:text-destructive/80",
"max-w-full",
className
)}
{...props}
/>
)
}
function AttachmentActions({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="attachment-actions"
className={cn(
"relative z-20 flex shrink-0 items-center group-data-[orientation=vertical]/attachment:absolute group-data-[orientation=vertical]/attachment:top-3 group-data-[orientation=vertical]/attachment:right-3 group-data-[orientation=vertical]/attachment:gap-1",
className
)}
{...props}
/>
)
}
function AttachmentAction({
className,
variant,
size = "icon-xs",
...props
}: React.ComponentProps<typeof Button>) {
return (
<Button
data-slot="attachment-action"
variant={variant ?? "ghost"}
size={size}
className={cn(className)}
{...props}
/>
)
}
function AttachmentTrigger({
className,
render,
type,
...props
}: useRender.ComponentProps<"button">) {
return useRender({
defaultTagName: "button",
props: mergeProps<"button">(
{
type: render ? type : (type ?? "button"),
className: cn("absolute inset-0 z-10 outline-none", className),
},
props
),
render,
state: {
slot: "attachment-trigger",
},
})
}
function AttachmentGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="attachment-group"
className={cn(
"flex min-w-0 scroll-fade-x snap-x snap-mandatory scroll-px-1 scrollbar-none gap-3 overflow-x-auto overscroll-x-contain py-1 *:data-[slot=attachment]:flex-none *:data-[slot=attachment]:snap-start",
className
)}
{...props}
/>
)
}
export {
Attachment,
AttachmentGroup,
AttachmentMedia,
AttachmentContent,
AttachmentTitle,
AttachmentDescription,
AttachmentActions,
AttachmentAction,
AttachmentTrigger,
}
@@ -1,128 +0,0 @@
import * as React from "react"
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
function BubbleGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="bubble-group"
className={cn("flex min-w-0 flex-col gap-2", className)}
{...props}
/>
)
}
const bubbleVariants = cva(
"group/bubble relative flex w-fit max-w-[80%] min-w-0 flex-col gap-1 group-data-[align=end]/message:self-end data-[align=end]:self-end data-[variant=ghost]:max-w-full",
{
variants: {
variant: {
default:
"*:data-[slot=bubble-content]:bg-primary *:data-[slot=bubble-content]:text-primary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-primary/80",
secondary:
"*:data-[slot=bubble-content]:bg-secondary *:data-[slot=bubble-content]:text-secondary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]",
muted:
"*:data-[slot=bubble-content]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--muted),var(--foreground)_5%)]",
tinted:
"*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.93_calc(c*0.4)_h)] *:data-[slot=bubble-content]:text-foreground dark:*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.3_calc(c*0.4)_h)] [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.88_calc(c*0.5)_h)] dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.35_calc(c*0.5)_h)]",
outline:
"*:data-[slot=bubble-content]:border-border *:data-[slot=bubble-content]:bg-background [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-input/30",
ghost:
"border-none *:data-[slot=bubble-content]:rounded-none *:data-[slot=bubble-content]:bg-transparent *:data-[slot=bubble-content]:p-0 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted/50",
destructive:
"*:data-[slot=bubble-content]:bg-destructive/10 *:data-[slot=bubble-content]:text-destructive dark:*:data-[slot=bubble-content]:bg-destructive/20 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/20 dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/30",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Bubble({
variant = "default",
align = "start",
className,
...props
}: React.ComponentProps<"div"> &
VariantProps<typeof bubbleVariants> & {
align?: "start" | "end"
}) {
return (
<div
data-slot="bubble"
data-variant={variant}
data-align={align}
className={cn(bubbleVariants({ variant }), className)}
{...props}
/>
)
}
function BubbleContent({
className,
render,
...props
}: useRender.ComponentProps<"div">) {
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(
{
className: cn(
"w-fit max-w-full min-w-0 overflow-hidden rounded-xl border border-transparent px-3 py-2 text-sm leading-relaxed wrap-break-word group-data-[align=end]/bubble:self-end [button]:text-left [button,a]:transition-colors [button,a]:outline-none [button,a]:focus-visible:border-ring [button,a]:focus-visible:ring-3 [button,a]:focus-visible:ring-ring/50",
className
),
},
props
),
render,
state: {
slot: "bubble-content",
},
})
}
const bubbleReactionsVariants = cva(
"absolute z-10 flex w-fit shrink-0 items-center justify-center gap-1 rounded-full bg-muted px-1.5 py-0.5 text-sm ring-3 ring-card has-[button]:p-0",
{
variants: {
side: {
top: "top-0 -translate-y-3/4",
bottom: "bottom-0 translate-y-3/4",
},
align: {
start: "left-3",
end: "right-3",
},
},
defaultVariants: {
side: "bottom",
align: "end",
},
}
)
function BubbleReactions({
side = "bottom",
align = "end",
className,
...props
}: React.ComponentProps<"div"> & {
align?: "start" | "end"
side?: "top" | "bottom"
}) {
return (
<div
data-slot="bubble-reactions"
data-align={align}
data-side={side}
className={cn(bubbleReactionsVariants({ side, align }), className)}
{...props}
/>
)
}
export { BubbleGroup, Bubble, BubbleContent, BubbleReactions }
@@ -1,87 +0,0 @@
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Separator } from "@/components/ui/separator"
const buttonGroupVariants = cva(
"flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-lg [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
{
variants: {
orientation: {
horizontal:
"*:data-slot:rounded-r-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-lg! [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0",
vertical:
"flex-col *:data-slot:rounded-b-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-lg! [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0",
},
},
defaultVariants: {
orientation: "horizontal",
},
}
)
function ButtonGroup({
className,
orientation,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
return (
<div
role="group"
data-slot="button-group"
data-orientation={orientation}
className={cn(buttonGroupVariants({ orientation }), className)}
{...props}
/>
)
}
function ButtonGroupText({
className,
render,
...props
}: useRender.ComponentProps<"div">) {
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(
{
className: cn(
"flex items-center gap-2 rounded-lg border bg-muted px-2.5 text-sm font-medium [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
),
},
props
),
render,
state: {
slot: "button-group-text",
},
})
}
function ButtonGroupSeparator({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="button-group-separator"
orientation={orientation}
className={cn(
"relative self-stretch bg-input data-horizontal:mx-px data-horizontal:w-auto data-vertical:my-px data-vertical:h-auto",
className
)}
{...props}
/>
)
}
export {
ButtonGroup,
ButtonGroupSeparator,
ButtonGroupText,
buttonGroupVariants,
}
@@ -1,297 +0,0 @@
"use client"
import * as React from "react"
import { Combobox as ComboboxPrimitive } from "@base-ui/react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/components/ui/input-group"
import { ChevronDownIcon, XIcon, CheckIcon } from "lucide-react"
const Combobox = ComboboxPrimitive.Root
function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />
}
function ComboboxTrigger({
className,
children,
...props
}: ComboboxPrimitive.Trigger.Props) {
return (
<ComboboxPrimitive.Trigger
data-slot="combobox-trigger"
className={cn("[&_svg:not([class*='size-'])]:size-4", className)}
{...props}
>
{children}
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
</ComboboxPrimitive.Trigger>
)
}
function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
return (
<ComboboxPrimitive.Clear
data-slot="combobox-clear"
render={<InputGroupButton variant="ghost" size="icon-xs" />}
className={cn(className)}
{...props}
>
<XIcon className="pointer-events-none" />
</ComboboxPrimitive.Clear>
)
}
function ComboboxInput({
className,
children,
disabled = false,
showTrigger = true,
showClear = false,
...props
}: ComboboxPrimitive.Input.Props & {
showTrigger?: boolean
showClear?: boolean
}) {
return (
<InputGroup className={cn("w-auto", className)}>
<ComboboxPrimitive.Input
render={<InputGroupInput disabled={disabled} />}
{...props}
/>
<InputGroupAddon align="inline-end">
{showTrigger && (
<InputGroupButton
size="icon-xs"
variant="ghost"
render={<ComboboxTrigger />}
data-slot="input-group-button"
className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent"
disabled={disabled}
/>
)}
{showClear && <ComboboxClear disabled={disabled} />}
</InputGroupAddon>
{children}
</InputGroup>
)
}
function ComboboxContent({
className,
side = "bottom",
sideOffset = 6,
align = "start",
alignOffset = 0,
anchor,
...props
}: ComboboxPrimitive.Popup.Props &
Pick<
ComboboxPrimitive.Positioner.Props,
"side" | "align" | "sideOffset" | "alignOffset" | "anchor"
>) {
return (
<ComboboxPrimitive.Portal>
<ComboboxPrimitive.Positioner
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
anchor={anchor}
className="isolate z-50"
>
<ComboboxPrimitive.Popup
data-slot="combobox-content"
data-chips={!!anchor}
className={cn("group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[chips=true]:min-w-(--anchor-width) data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
{...props}
/>
</ComboboxPrimitive.Positioner>
</ComboboxPrimitive.Portal>
)
}
function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
return (
<ComboboxPrimitive.List
data-slot="combobox-list"
className={cn(
"no-scrollbar max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 overflow-y-auto overscroll-contain p-1 data-empty:p-0",
className
)}
{...props}
/>
)
}
function ComboboxItem({
className,
children,
...props
}: ComboboxPrimitive.Item.Props) {
return (
<ComboboxPrimitive.Item
data-slot="combobox-item"
className={cn(
"relative flex w-full cursor-default items-center gap-2 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ComboboxPrimitive.ItemIndicator
render={
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
}
>
<CheckIcon className="pointer-events-none" />
</ComboboxPrimitive.ItemIndicator>
</ComboboxPrimitive.Item>
)
}
function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
return (
<ComboboxPrimitive.Group
data-slot="combobox-group"
className={cn(className)}
{...props}
/>
)
}
function ComboboxLabel({
className,
...props
}: ComboboxPrimitive.GroupLabel.Props) {
return (
<ComboboxPrimitive.GroupLabel
data-slot="combobox-label"
className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)}
{...props}
/>
)
}
function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
return (
<ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />
)
}
function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
return (
<ComboboxPrimitive.Empty
data-slot="combobox-empty"
className={cn(
"hidden w-full justify-center py-2 text-center text-sm text-muted-foreground group-data-empty/combobox-content:flex",
className
)}
{...props}
/>
)
}
function ComboboxSeparator({
className,
...props
}: ComboboxPrimitive.Separator.Props) {
return (
<ComboboxPrimitive.Separator
data-slot="combobox-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function ComboboxChips({
className,
...props
}: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &
ComboboxPrimitive.Chips.Props) {
return (
<ComboboxPrimitive.Chips
data-slot="combobox-chips"
className={cn(
"flex min-h-8 flex-wrap items-center gap-1 rounded-lg border border-input bg-transparent bg-clip-padding px-2.5 py-1 text-sm transition-colors focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 has-aria-invalid:border-destructive has-aria-invalid:ring-3 has-aria-invalid:ring-destructive/20 has-data-[slot=combobox-chip]:px-1 dark:bg-input/30 dark:has-aria-invalid:border-destructive/50 dark:has-aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
function ComboboxChip({
className,
children,
showRemove = true,
...props
}: ComboboxPrimitive.Chip.Props & {
showRemove?: boolean
}) {
return (
<ComboboxPrimitive.Chip
data-slot="combobox-chip"
className={cn(
"flex h-[calc(--spacing(5.25))] w-fit items-center justify-center gap-1 rounded-sm bg-muted px-1.5 text-xs font-medium whitespace-nowrap text-foreground has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0",
className
)}
{...props}
>
{children}
{showRemove && (
<ComboboxPrimitive.ChipRemove
render={<Button variant="ghost" size="icon-xs" />}
className="-ml-1 opacity-50 hover:opacity-100"
data-slot="combobox-chip-remove"
>
<XIcon className="pointer-events-none" />
</ComboboxPrimitive.ChipRemove>
)}
</ComboboxPrimitive.Chip>
)
}
function ComboboxChipsInput({
className,
...props
}: ComboboxPrimitive.Input.Props) {
return (
<ComboboxPrimitive.Input
data-slot="combobox-chip-input"
className={cn("min-w-16 flex-1 outline-none", className)}
{...props}
/>
)
}
function useComboboxAnchor() {
return React.useRef<HTMLDivElement | null>(null)
}
export {
Combobox,
ComboboxInput,
ComboboxContent,
ComboboxList,
ComboboxItem,
ComboboxGroup,
ComboboxLabel,
ComboboxCollection,
ComboboxEmpty,
ComboboxSeparator,
ComboboxChips,
ComboboxChip,
ComboboxChipsInput,
ComboboxTrigger,
ComboboxValue,
useComboboxAnchor,
}
@@ -1,196 +0,0 @@
"use client"
import * as React from "react"
import { Command as CommandPrimitive } from "cmdk"
import { cn } from "@/lib/utils"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
InputGroup,
InputGroupAddon,
} from "@/components/ui/input-group"
import { SearchIcon, CheckIcon } from "lucide-react"
function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"flex size-full flex-col overflow-hidden rounded-xl! bg-popover p-1 text-popover-foreground",
className
)}
{...props}
/>
)
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
className,
showCloseButton = false,
...props
}: Omit<React.ComponentProps<typeof Dialog>, "children"> & {
title?: string
description?: string
className?: string
showCloseButton?: boolean
children: React.ReactNode
}) {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent
className={cn(
"top-1/3 translate-y-0 overflow-hidden rounded-xl! p-0",
className
)}
showCloseButton={showCloseButton}
>
{children}
</DialogContent>
</Dialog>
)
}
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div data-slot="command-input-wrapper" className="p-1 pb-0">
<InputGroup className="h-8! rounded-lg! border-input/30 bg-input/30 shadow-none! *:data-[slot=input-group-addon]:pl-2!">
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
<InputGroupAddon>
<SearchIcon className="size-4 shrink-0 opacity-50" />
</InputGroupAddon>
</InputGroup>
</div>
)
}
function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"no-scrollbar max-h-72 scroll-py-1 overflow-x-hidden overflow-y-auto outline-none",
className
)}
{...props}
/>
)
}
function CommandEmpty({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className={cn("py-6 text-center text-sm", className)}
{...props}
/>
)
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"overflow-hidden p-1 text-foreground **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
)
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
)
}
function CommandItem({
className,
children,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"group/command-item relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-lg! data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-selected:bg-muted data-selected:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-selected:*:[svg]:text-foreground",
className
)}
{...props}
>
{children}
<CheckIcon className="ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100" />
</CommandPrimitive.Item>
)
}
function CommandShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground group-data-selected/command-item:text-foreground",
className
)}
{...props}
/>
)
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}
@@ -1,6 +0,0 @@
"use client"
export {
DirectionProvider,
useDirection,
} from "@base-ui/react/direction-provider"
@@ -1,104 +0,0 @@
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
function Empty({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty"
className={cn(
"flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-xl border-dashed p-6 text-center text-balance",
className
)}
{...props}
/>
)
}
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-header"
className={cn("flex max-w-sm flex-col items-center gap-2", className)}
{...props}
/>
)
}
const emptyMediaVariants = cva(
"mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-transparent",
icon: "flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-4",
},
},
defaultVariants: {
variant: "default",
},
}
)
function EmptyMedia({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
return (
<div
data-slot="empty-icon"
data-variant={variant}
className={cn(emptyMediaVariants({ variant, className }))}
{...props}
/>
)
}
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-title"
className={cn(
"text-sm font-medium tracking-tight",
className
)}
{...props}
/>
)
}
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<div
data-slot="empty-description"
className={cn(
"text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
className
)}
{...props}
/>
)
}
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-content"
className={cn(
"flex w-full max-w-sm min-w-0 flex-col items-center gap-2.5 text-sm text-balance",
className
)}
{...props}
/>
)
}
export {
Empty,
EmptyHeader,
EmptyTitle,
EmptyDescription,
EmptyContent,
EmptyMedia,
}
@@ -1,238 +0,0 @@
"use client"
import { useMemo } from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
return (
<fieldset
data-slot="field-set"
className={cn(
"flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
className
)}
{...props}
/>
)
}
function FieldLegend({
className,
variant = "legend",
...props
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
return (
<legend
data-slot="field-legend"
data-variant={variant}
className={cn(
"mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base",
className
)}
{...props}
/>
)
}
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-group"
className={cn(
"group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4",
className
)}
{...props}
/>
)
}
const fieldVariants = cva(
"group/field flex w-full gap-2 data-[invalid=true]:text-destructive",
{
variants: {
orientation: {
vertical: "flex-col *:w-full [&>.sr-only]:w-auto",
horizontal:
"flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
responsive:
"flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
},
},
defaultVariants: {
orientation: "vertical",
},
}
)
function Field({
className,
orientation = "vertical",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
return (
<div
role="group"
data-slot="field"
data-orientation={orientation}
className={cn(fieldVariants({ orientation }), className)}
{...props}
/>
)
}
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-content"
className={cn(
"group/field-content flex flex-1 flex-col gap-0.5 leading-snug",
className
)}
{...props}
/>
)
}
function FieldLabel({
className,
...props
}: React.ComponentProps<typeof Label>) {
return (
<Label
data-slot="field-label"
className={cn(
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-data-checked:border-primary/30 has-data-checked:bg-primary/5 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border *:data-[slot=field]:p-2.5 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10",
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col",
className
)}
{...props}
/>
)
}
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-label"
className={cn(
"flex w-fit items-center gap-2 text-sm font-medium group-data-[disabled=true]/field:opacity-50",
className
)}
{...props}
/>
)
}
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="field-description"
className={cn(
"text-left text-sm leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5",
"last:mt-0 nth-last-2:-mt-1",
"[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
className
)}
{...props}
/>
)
}
function FieldSeparator({
children,
className,
...props
}: React.ComponentProps<"div"> & {
children?: React.ReactNode
}) {
return (
<div
data-slot="field-separator"
data-content={!!children}
className={cn(
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
className
)}
{...props}
>
<Separator className="absolute inset-0 top-1/2" />
{children && (
<span
className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
data-slot="field-separator-content"
>
{children}
</span>
)}
</div>
)
}
function FieldError({
className,
children,
errors,
...props
}: React.ComponentProps<"div"> & {
errors?: Array<{ message?: string } | undefined>
}) {
const content = useMemo(() => {
if (children) {
return children
}
if (!errors?.length) {
return null
}
const uniqueErrors = [
...new Map(errors.map((error) => [error?.message, error])).values(),
]
if (uniqueErrors?.length == 1) {
return uniqueErrors[0]?.message
}
return (
<ul className="ml-4 flex list-disc flex-col gap-1">
{uniqueErrors.map(
(error, index) =>
error?.message && <li key={index}>{error.message}</li>
)}
</ul>
)
}, [children, errors])
if (!content) {
return null
}
return (
<div
role="alert"
data-slot="field-error"
className={cn("text-sm font-normal text-destructive", className)}
{...props}
>
{content}
</div>
)
}
export {
Field,
FieldLabel,
FieldDescription,
FieldError,
FieldGroup,
FieldLegend,
FieldSeparator,
FieldSet,
FieldContent,
FieldTitle,
}
@@ -1,158 +0,0 @@
"use client"
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-group"
role="group"
className={cn(
"group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
className
)}
{...props}
/>
)
}
const inputGroupAddonVariants = cva(
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
{
variants: {
align: {
"inline-start":
"order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
"inline-end":
"order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
"block-start":
"order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
"block-end":
"order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2",
},
},
defaultVariants: {
align: "inline-start",
},
}
)
function InputGroupAddon({
className,
align = "inline-start",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
return (
<div
role="group"
data-slot="input-group-addon"
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => {
if ((e.target as HTMLElement).closest("button")) {
return
}
e.currentTarget.parentElement?.querySelector("input")?.focus()
}}
{...props}
/>
)
}
const inputGroupButtonVariants = cva(
"flex items-center gap-2 text-sm shadow-none",
{
variants: {
size: {
xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
sm: "",
"icon-xs":
"size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0",
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
},
},
defaultVariants: {
size: "xs",
},
}
)
function InputGroupButton({
className,
type = "button",
variant = "ghost",
size = "xs",
...props
}: Omit<React.ComponentProps<typeof Button>, "size" | "type"> &
VariantProps<typeof inputGroupButtonVariants> & {
type?: "button" | "submit" | "reset"
}) {
return (
<Button
type={type}
data-size={size}
variant={variant}
className={cn(inputGroupButtonVariants({ size }), className)}
{...props}
/>
)
}
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
className={cn(
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function InputGroupInput({
className,
...props
}: React.ComponentProps<"input">) {
return (
<Input
data-slot="input-group-control"
className={cn(
"flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
className
)}
{...props}
/>
)
}
function InputGroupTextarea({
className,
...props
}: React.ComponentProps<"textarea">) {
return (
<Textarea
data-slot="input-group-control"
className={cn(
"flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
className
)}
{...props}
/>
)
}
export {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupInput,
InputGroupTextarea,
}
@@ -1,201 +0,0 @@
import * as React from "react"
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Separator } from "@/components/ui/separator"
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
role="list"
data-slot="item-group"
className={cn(
"group/item-group flex w-full flex-col gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2",
className
)}
{...props}
/>
)
}
function ItemSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="item-separator"
orientation="horizontal"
className={cn("my-2", className)}
{...props}
/>
)
}
const itemVariants = cva(
"group/item flex w-full flex-wrap items-center rounded-lg border text-sm transition-colors duration-100 outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [a]:transition-colors [a]:hover:bg-muted",
{
variants: {
variant: {
default: "border-transparent",
outline: "border-border",
muted: "border-transparent bg-muted/50",
},
size: {
default: "gap-2.5 px-3 py-2.5",
sm: "gap-2.5 px-3 py-2.5",
xs: "gap-2 px-2.5 py-2 in-data-[slot=dropdown-menu-content]:p-0",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Item({
className,
variant = "default",
size = "default",
render,
...props
}: useRender.ComponentProps<"div"> & VariantProps<typeof itemVariants>) {
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(
{
className: cn(itemVariants({ variant, size, className })),
},
props
),
render,
state: {
slot: "item",
variant,
size,
},
})
}
const itemMediaVariants = cva(
"flex shrink-0 items-center justify-center gap-2 group-has-data-[slot=item-description]/item:translate-y-0.5 group-has-data-[slot=item-description]/item:self-start [&_svg]:pointer-events-none",
{
variants: {
variant: {
default: "bg-transparent",
icon: "[&_svg:not([class*='size-'])]:size-4",
image:
"size-10 overflow-hidden rounded-sm group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 [&_img]:size-full [&_img]:object-cover",
},
},
defaultVariants: {
variant: "default",
},
}
)
function ItemMedia({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) {
return (
<div
data-slot="item-media"
data-variant={variant}
className={cn(itemMediaVariants({ variant, className }))}
{...props}
/>
)
}
function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-content"
className={cn(
"flex flex-1 flex-col gap-1 group-data-[size=xs]/item:gap-0 [&+[data-slot=item-content]]:flex-none",
className
)}
{...props}
/>
)
}
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-title"
className={cn(
"line-clamp-1 flex w-fit items-center gap-2 text-sm leading-snug font-medium underline-offset-4",
className
)}
{...props}
/>
)
}
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="item-description"
className={cn(
"line-clamp-2 text-left text-sm leading-normal font-normal text-muted-foreground group-data-[size=xs]/item:text-xs [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
className
)}
{...props}
/>
)
}
function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-actions"
className={cn("flex items-center gap-2", className)}
{...props}
/>
)
}
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-header"
className={cn(
"flex basis-full items-center justify-between gap-2",
className
)}
{...props}
/>
)
}
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-footer"
className={cn(
"flex basis-full items-center justify-between gap-2",
className
)}
{...props}
/>
)
}
export {
Item,
ItemMedia,
ItemContent,
ItemActions,
ItemGroup,
ItemSeparator,
ItemTitle,
ItemDescription,
ItemHeader,
ItemFooter,
}
@@ -1,26 +0,0 @@
import { cn } from "@/lib/utils"
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
return (
<kbd
data-slot="kbd"
className={cn(
"pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm bg-muted px-1 font-sans text-xs font-medium text-muted-foreground select-none in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 [&_svg:not([class*='size-'])]:size-3",
className
)}
{...props}
/>
)
}
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<kbd
data-slot="kbd-group"
className={cn("inline-flex items-center gap-1", className)}
{...props}
/>
)
}
export { Kbd, KbdGroup }
@@ -1,71 +0,0 @@
import * as React from "react"
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const markerVariants = cva(
"group/marker relative flex min-h-4 w-full items-center gap-2 text-left text-sm text-muted-foreground [&_svg:not([class*='size-'])]:size-4 [a]:underline [a]:underline-offset-3 [a]:hover:text-foreground",
{
variants: {
variant: {
default: "",
separator:
"before:mr-1 before:h-px before:min-w-0 before:flex-1 before:bg-border after:ml-1 after:h-px after:min-w-0 after:flex-1 after:bg-border",
border: "border-b border-border pb-2",
},
},
}
)
function Marker({
className,
variant = "default",
render,
...props
}: useRender.ComponentProps<"div"> & VariantProps<typeof markerVariants>) {
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(
{
className: cn(markerVariants({ variant, className })),
},
props
),
render,
state: {
slot: "marker",
variant,
},
})
}
function MarkerIcon({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="marker-icon"
aria-hidden="true"
className={cn(
"size-4 shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function MarkerContent({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="marker-content"
className={cn(
"min-w-0 wrap-break-word group-data-[variant=separator]/marker:flex-none group-data-[variant=separator]/marker:text-center *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
className
)}
{...props}
/>
)
}
export { Marker, MarkerIcon, MarkerContent, markerVariants }
@@ -1,131 +0,0 @@
"use client"
import * as React from "react"
import {
MessageScroller as MessageScrollerPrimitive,
useMessageScroller,
useMessageScrollerScrollable,
useMessageScrollerVisibility,
} from "@shadcn/react/message-scroller"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { ArrowDownIcon } from "lucide-react"
function MessageScrollerProvider(
props: React.ComponentProps<typeof MessageScrollerPrimitive.Provider>
) {
return <MessageScrollerPrimitive.Provider {...props} />
}
function MessageScroller({
className,
...props
}: React.ComponentProps<typeof MessageScrollerPrimitive.Root>) {
return (
<MessageScrollerPrimitive.Root
data-slot="message-scroller"
className={cn(
"group/message-scroller relative flex size-full min-h-0 flex-col overflow-hidden",
className
)}
{...props}
/>
)
}
function MessageScrollerViewport({
className,
...props
}: React.ComponentProps<typeof MessageScrollerPrimitive.Viewport>) {
return (
<MessageScrollerPrimitive.Viewport
data-slot="message-scroller-viewport"
className={cn(
"size-full min-h-0 min-w-0 scroll-fade-b scrollbar-thin scrollbar-gutter-stable overflow-y-auto overscroll-contain contain-content data-autoscrolling:scrollbar-thumb-transparent data-autoscrolling:scrollbar-track-transparent",
className
)}
{...props}
/>
)
}
function MessageScrollerContent({
className,
...props
}: React.ComponentProps<typeof MessageScrollerPrimitive.Content>) {
return (
<MessageScrollerPrimitive.Content
data-slot="message-scroller-content"
className={cn("flex h-max min-h-full flex-col gap-6", className)}
{...props}
/>
)
}
function MessageScrollerItem({
className,
scrollAnchor = false,
...props
}: React.ComponentProps<typeof MessageScrollerPrimitive.Item>) {
return (
<MessageScrollerPrimitive.Item
data-slot="message-scroller-item"
scrollAnchor={scrollAnchor}
className={cn(
"min-w-0 shrink-0 [contain-intrinsic-size:auto_10rem] [content-visibility:auto]",
className
)}
{...props}
/>
)
}
function MessageScrollerButton({
direction = "end",
className,
children,
render,
variant = "secondary",
size = "icon-sm",
...props
}: React.ComponentProps<typeof MessageScrollerPrimitive.Button> &
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
return (
<MessageScrollerPrimitive.Button
data-slot="message-scroller-button"
data-direction={direction}
data-variant={variant}
data-size={size}
direction={direction}
className={cn(
"absolute inset-s-1/2 -translate-x-1/2 border-border bg-background text-foreground transition-[translate,scale,opacity] duration-200 hover:bg-muted hover:text-foreground data-[active=false]:pointer-events-none data-[active=false]:scale-95 data-[active=false]:opacity-0 data-[active=false]:duration-400 data-[active=false]:ease-[cubic-bezier(0.7,0,0.84,0)] data-[active=true]:translate-y-0 data-[active=true]:scale-100 data-[active=true]:opacity-100 data-[active=true]:ease-[cubic-bezier(0.23,1,0.32,1)] data-[direction=end]:bottom-4 data-[direction=end]:data-[active=false]:translate-y-full data-[direction=start]:top-4 data-[direction=start]:data-[active=false]:-translate-y-full rtl:translate-x-1/2 data-[direction=start]:[&_svg]:rotate-180",
className
)}
render={render ?? <Button variant={variant} size={size} />}
{...props}
>
{children ?? (
<>
<ArrowDownIcon
/>
<span className="sr-only">
{direction === "end" ? "Scroll to end" : "Scroll to start"}
</span>
</>
)}
</MessageScrollerPrimitive.Button>
)
}
export {
MessageScrollerProvider,
MessageScroller,
MessageScrollerViewport,
MessageScrollerContent,
MessageScrollerItem,
MessageScrollerButton,
useMessageScroller,
useMessageScrollerScrollable,
useMessageScrollerVisibility,
}
@@ -1,92 +0,0 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function MessageGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="message-group"
className={cn("flex min-w-0 flex-col gap-2", className)}
{...props}
/>
)
}
function Message({
className,
align = "start",
...props
}: React.ComponentProps<"div"> & { align?: "start" | "end" }) {
return (
<div
data-slot="message"
data-align={align}
className={cn(
"group/message relative flex w-full min-w-0 gap-2 text-sm data-[align=end]:flex-row-reverse",
className
)}
{...props}
/>
)
}
function MessageAvatar({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="message-avatar"
className={cn(
"flex w-fit min-w-8 shrink-0 items-center justify-center self-end overflow-hidden rounded-full bg-muted group-has-data-[slot=message-footer]/message:-translate-y-8",
className
)}
{...props}
/>
)
}
function MessageContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="message-content"
className={cn(
"flex w-full min-w-0 flex-col gap-2.5 wrap-break-word group-data-[align=end]/message:*:data-slot:self-end",
className
)}
{...props}
/>
)
}
function MessageHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="message-header"
className={cn(
"flex max-w-full min-w-0 items-center px-3 text-xs font-medium text-muted-foreground group-has-data-[variant=ghost]/message:px-0",
className
)}
{...props}
/>
)
}
function MessageFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="message-footer"
className={cn(
"flex max-w-full min-w-0 items-center px-3 text-xs font-medium text-muted-foreground group-has-data-[variant=ghost]/message:px-0 group-data-[align=end]/message:justify-end",
className
)}
{...props}
/>
)
}
export {
MessageGroup,
Message,
MessageAvatar,
MessageContent,
MessageFooter,
MessageHeader,
}
@@ -1,61 +0,0 @@
import * as React from "react"
import { cn } from "@/lib/utils"
import { ChevronDownIcon } from "lucide-react"
type NativeSelectProps = Omit<React.ComponentProps<"select">, "size"> & {
size?: "sm" | "default"
}
function NativeSelect({
className,
size = "default",
...props
}: NativeSelectProps) {
return (
<div
className={cn(
"group/native-select relative w-fit has-[select:disabled]:opacity-50",
className
)}
data-slot="native-select-wrapper"
data-size={size}
>
<select
data-slot="native-select"
data-size={size}
className="h-8 w-full min-w-0 appearance-none rounded-lg border border-input bg-transparent py-1 pr-8 pl-2.5 text-sm transition-colors outline-none select-none selection:bg-primary selection:text-primary-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 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-[size=sm]:py-0.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40"
{...props}
/>
<ChevronDownIcon className="pointer-events-none absolute top-1/2 right-2.5 size-4 -translate-y-1/2 text-muted-foreground select-none" aria-hidden="true" data-slot="native-select-icon" />
</div>
)
}
function NativeSelectOption({
className,
...props
}: React.ComponentProps<"option">) {
return (
<option
data-slot="native-select-option"
className={cn("bg-[Canvas] text-[CanvasText]", className)}
{...props}
/>
)
}
function NativeSelectOptGroup({
className,
...props
}: React.ComponentProps<"optgroup">) {
return (
<optgroup
data-slot="native-select-optgroup"
className={cn("bg-[Canvas] text-[CanvasText]", className)}
{...props}
/>
)
}
export { NativeSelect, NativeSelectOptGroup, NativeSelectOption }
@@ -1,324 +0,0 @@
"use client"
import * as React from "react"
import { Questionnaire as QuestionnairePrimitive } from "@shadcn/react/questionnaire"
import { cn } from "@/lib/utils"
import { buttonVariants, type Button } from "@/components/ui/button"
import { CheckIcon } from "lucide-react"
function Questionnaire({
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Root>) {
return (
<QuestionnairePrimitive.Root
data-slot="questionnaire"
className={cn("flex w-full min-w-0 flex-col gap-4", className)}
{...props}
/>
)
}
function QuestionnaireProgress({
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Progress>) {
return (
<QuestionnairePrimitive.Progress
data-slot="questionnaire-progress"
className={cn(
"min-h-[1lh] w-fit min-w-[14ch] text-xs font-medium text-muted-foreground tabular-nums",
className
)}
{...props}
/>
)
}
function QuestionnaireItem({
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Item>) {
return (
<QuestionnairePrimitive.Item
data-slot="questionnaire-item"
className={cn(
"flex min-w-0 flex-col gap-4 border-0 p-0 outline-none",
className
)}
{...props}
/>
)
}
function QuestionnaireTitle({
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Title>) {
return (
<QuestionnairePrimitive.Title
data-slot="questionnaire-title"
className={cn(
"text-base leading-snug font-medium text-pretty [&:not(:has(~[data-slot=questionnaire-description]))]:mb-4",
className
)}
{...props}
/>
)
}
function QuestionnaireDescription({
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Description>) {
return (
<QuestionnairePrimitive.Description
data-slot="questionnaire-description"
className={cn("text-sm text-pretty text-muted-foreground", className)}
{...props}
/>
)
}
function QuestionnaireChoices({
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Choices>) {
return (
<QuestionnairePrimitive.Choices
data-slot="questionnaire-choices"
className={cn(
"group/questionnaire-choices grid min-w-0 gap-2",
className
)}
{...props}
/>
)
}
function QuestionnaireChoice({
children,
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Choice>) {
return (
<QuestionnairePrimitive.Choice
data-slot="questionnaire-choice"
className={cn(
"group/questionnaire-choice relative flex min-h-11 cursor-pointer items-start gap-2.5 rounded-lg border border-input bg-transparent px-3 py-2.5 text-start text-sm transition-colors outline-none select-none hover:bg-muted/50 has-[>input:focus-visible]:border-ring has-[>input:focus-visible]:ring-3 has-[>input:focus-visible]:ring-ring/50 data-invalid:border-destructive dark:bg-input/20 data-checked:border-primary/40 data-checked:bg-muted dark:data-checked:bg-muted",
"data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50",
className
)}
{...props}
>
<QuestionnairePrimitive.ChoiceInput
data-slot="questionnaire-choice-input"
className="absolute inset-0 z-10 size-full cursor-pointer opacity-0"
/>
<span
aria-hidden="true"
data-slot="questionnaire-choice-indicator"
className="pointer-events-none relative flex size-4 shrink-0 translate-y-[--spacing(0.45)] items-center justify-center rounded-[4px] border border-input group-has-data-[slot=questionnaire-choice-description]/questionnaire-choice:translate-y-0.5 group-data-[type=radio]/questionnaire-choice:rounded-full group-data-checked/questionnaire-choice:border-primary group-data-checked/questionnaire-choice:bg-primary group-data-checked/questionnaire-choice:text-primary-foreground dark:bg-input/30 dark:group-data-checked/questionnaire-choice:bg-primary"
>
<span
data-slot="questionnaire-choice-indicator-dot"
className="hidden size-2 rounded-full bg-primary-foreground group-data-[type=checkbox]/questionnaire-choice:hidden group-data-checked/questionnaire-choice:block"
/>
<CheckIcon data-slot="questionnaire-choice-indicator-check" className="hidden size-3.5 group-data-[type=radio]/questionnaire-choice:hidden group-data-checked/questionnaire-choice:block" />
</span>
<QuestionnairePrimitive.ChoiceLabel
data-slot="questionnaire-choice-label"
className="flex min-w-0 flex-1 flex-col gap-0.5 leading-snug"
>
{children}
</QuestionnairePrimitive.ChoiceLabel>
<QuestionnairePrimitive.ChoiceShortcut
data-slot="questionnaire-choice-shortcut"
className="pointer-events-none ms-auto hidden size-5 shrink-0 translate-y-[--spacing(0.45)] items-center justify-center rounded-md border border-input bg-background font-mono text-[0.625rem] leading-none font-medium text-muted-foreground group-has-data-[slot=questionnaire-choice-description]/questionnaire-choice:translate-y-0.5 group-data-[shortcut]/questionnaire-choice:inline-flex"
/>
</QuestionnairePrimitive.Choice>
)
}
function QuestionnaireChoiceDescription({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="questionnaire-choice-description"
className={cn("text-muted-foreground", className)}
{...props}
/>
)
}
function QuestionnaireInput({
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Input>) {
return (
<div
data-slot="questionnaire-input-wrapper"
className="group/questionnaire-input relative w-full min-w-0"
>
<QuestionnairePrimitive.Input
data-slot="questionnaire-input"
className={cn(
"h-8 min-h-11 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-[color,box-shadow,background-color] outline-none 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 sm:min-h-0 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
"selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground",
className
)}
{...props}
/>
</div>
)
}
function QuestionnaireError({
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Error>) {
return (
<QuestionnairePrimitive.Error
data-slot="questionnaire-error"
className={cn("mt-2 text-sm text-destructive", className)}
{...props}
/>
)
}
function QuestionnaireActions({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="questionnaire-actions"
className={cn(
"grid min-h-11 w-full grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-2 sm:min-h-8",
className
)}
{...props}
/>
)
}
function QuestionnairePrevious({
children,
className,
size = "default",
variant = "outline",
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Previous> &
Pick<React.ComponentProps<typeof Button>, "size" | "variant">) {
return (
<QuestionnairePrimitive.Previous
data-slot="questionnaire-previous"
data-size={size}
data-variant={variant}
className={cn(
buttonVariants({ size, variant }),
"col-start-1 row-start-1 min-h-11 justify-self-start sm:min-h-0",
className
)}
{...props}
>
{children ?? "Previous"}
</QuestionnairePrimitive.Previous>
)
}
function QuestionnaireSkip({
children,
className,
size = "default",
variant = "outline",
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Skip> &
Pick<React.ComponentProps<typeof Button>, "size" | "variant">) {
return (
<QuestionnairePrimitive.Skip
data-slot="questionnaire-skip"
data-size={size}
data-variant={variant}
className={cn(
buttonVariants({ size, variant }),
"col-start-2 row-start-1 min-h-11 justify-self-end sm:min-h-0",
className
)}
{...props}
>
{children ?? "Skip"}
</QuestionnairePrimitive.Skip>
)
}
function QuestionnaireNext({
children,
className,
size = "default",
variant = "default",
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Next> &
Pick<React.ComponentProps<typeof Button>, "size" | "variant">) {
return (
<QuestionnairePrimitive.Next
data-slot="questionnaire-next"
data-size={size}
data-variant={variant}
className={cn(
buttonVariants({ size, variant }),
"col-start-3 row-start-1 min-h-11 justify-self-end sm:min-h-0",
className
)}
{...props}
>
{children ?? "Next"}
</QuestionnairePrimitive.Next>
)
}
function QuestionnaireSubmit({
children,
className,
size = "default",
variant = "default",
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Submit> &
Pick<React.ComponentProps<typeof Button>, "size" | "variant">) {
return (
<QuestionnairePrimitive.Submit
data-slot="questionnaire-submit"
data-size={size}
data-variant={variant}
className={cn(
buttonVariants({ size, variant }),
"col-start-3 row-start-1 min-h-11 justify-self-end sm:min-h-0",
className
)}
{...props}
>
{children ?? "Submit"}
</QuestionnairePrimitive.Submit>
)
}
export {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoiceDescription,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireInput,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSkip,
QuestionnaireSubmit,
QuestionnaireTitle,
}
@@ -1,10 +0,0 @@
import { cn } from "@/lib/utils"
import { Loader2Icon } from "lucide-react"
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
return (
<Loader2Icon data-slot="spinner" role="status" aria-label="Loading" className={cn("size-4 animate-spin", className)} {...props} />
)
}
export { Spinner }
@@ -1,8 +1,9 @@
"use client";
import { Mic, MicOff } from "lucide-react";
import { GlassCard } from "@/components/glass/card";
import { Card } from "@/components/ui/card";
import type { ActiveSpeaker } from "@/lib/types";
import { cn } from "@/lib/utils";
interface ActivityTimelineProps {
data?: ActiveSpeaker[];
@@ -27,7 +28,7 @@ export function VoiceActivityTimeline({ data = [] }: ActivityTimelineProps) {
);
return (
<GlassCard variant="base">
<Card className={cn("[--card-spacing:0px]", "rounded-2xl", "p-5")}>
<div className="flex items-center gap-2 mb-3">
<span className="text-xs font-semibold tracking-wide uppercase text-text-secondary">
Voice Activity
@@ -79,6 +80,6 @@ export function VoiceActivityTimeline({ data = [] }: ActivityTimelineProps) {
))}
</div>
)}
</GlassCard>
</Card>
);
}
@@ -1,8 +1,8 @@
"use client";
import { Headphones, Server, Volume2 } from "lucide-react";
import { GlassCard } from "@/components/glass/card";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import {
Select,
SelectContent,
@@ -41,7 +41,14 @@ export function VoiceConnectionCard({
connecting,
}: ConnectionCardProps) {
return (
<GlassCard variant={connected ? "elevated" : "base"}>
<Card
className={cn(
connected && "ring-2 ring-primary/30",
"[--card-spacing:0px]",
"rounded-2xl",
"p-5",
)}
>
<div className="flex items-center gap-3 mb-4">
<span
className={cn(
@@ -188,6 +195,6 @@ export function VoiceConnectionCard({
</Select>
</div>
</div>
</GlassCard>
</Card>
);
}
@@ -2,10 +2,11 @@
import { HeadphoneOff, Headphones } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { GlassCard } from "@/components/glass/card";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { hashUserId } from "@/hooks";
import type { ActiveSpeaker } from "@/lib/types";
import { cn } from "@/lib/utils";
interface ListenControlProps {
connected: boolean;
@@ -86,7 +87,7 @@ export function ListenControl({
);
return (
<GlassCard variant="base">
<Card className={cn("[--card-spacing:0px]", "rounded-2xl", "p-5")}>
<div className="flex items-center gap-3">
<Button
variant={active ? "default" : "secondary"}
@@ -148,6 +149,6 @@ export function ListenControl({
</span>
)}
</div>
</GlassCard>
</Card>
);
}
@@ -1,8 +1,9 @@
"use client";
import { Mic, MicOff } from "lucide-react";
import { GlassCard } from "@/components/glass/card";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { cn } from "@/lib/utils";
interface MicControlProps {
connected: boolean;
@@ -20,7 +21,7 @@ export function MicControl({
onVolumeChange,
}: MicControlProps) {
return (
<GlassCard variant="base">
<Card className={cn("[--card-spacing:0px]", "rounded-2xl", "p-5")}>
<div className="flex items-center gap-3">
<Button
variant={active ? "default" : "secondary"}
@@ -53,6 +54,6 @@ export function MicControl({
</span>
</div>
</div>
</GlassCard>
</Card>
);
}
@@ -1,8 +1,9 @@
"use client";
import { useEffect, useRef } from "react";
import { GlassPanel } from "@/components/glass/panel";
import { Card } from "@/components/ui/card";
import type { ActiveSpeaker } from "@/lib/types";
import { cn } from "@/lib/utils";
interface SpeakerWaveformProps {
speakers: ActiveSpeaker[];
@@ -45,16 +46,16 @@ export function SpeakerWaveform({ speakers }: SpeakerWaveformProps) {
if (speakers.length === 0) {
return (
<GlassPanel dense>
<Card className={cn("[--card-spacing:0px]", "p-3")}>
<span className="text-xs text-text-secondary/40">
No speakers detected
</span>
</GlassPanel>
</Card>
);
}
return (
<GlassPanel dense>
<Card className={cn("[--card-spacing:0px]", "p-3")}>
<div className="space-y-1">
{speakers.map((s) => (
<div key={s.userId} className="flex items-center gap-2 text-xs">
@@ -76,6 +77,6 @@ export function SpeakerWaveform({ speakers }: SpeakerWaveformProps) {
height={speakers.length * 30}
className="w-full h-auto mt-2 rounded"
/>
</GlassPanel>
</Card>
);
}