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:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user