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,
|
ChatbotProvider,
|
||||||
useChatbot,
|
useChatbot,
|
||||||
} from "@/components/chatbot/chatbot-context";
|
} from "@/components/chatbot/chatbot-context";
|
||||||
import { HiddenSidebar } from "@/components/layout/hidden-sidebar";
|
import { AppSidebar } from "@/components/layout/app-sidebar";
|
||||||
import { MobileNav } from "@/components/layout/mobile-nav";
|
|
||||||
import { TopNav } from "@/components/layout/top-nav";
|
|
||||||
import { MiniPlayer } from "@/components/media/mini-player";
|
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 { MediaPlayerProvider } from "@/lib/hooks/use-media-player";
|
||||||
import { useWebSocket, WsProvider } from "@/lib/ws/context";
|
import { useWebSocket, WsProvider } from "@/lib/ws/context";
|
||||||
|
|
||||||
@@ -69,29 +74,41 @@ export default function DashboardLayout({
|
|||||||
<ChatbotProvider>
|
<ChatbotProvider>
|
||||||
<ChatbotGuildSync guildId={guildId} />
|
<ChatbotGuildSync guildId={guildId} />
|
||||||
<ChatbotExpressionSync />
|
<ChatbotExpressionSync />
|
||||||
<div className="min-h-screen bg-canvas">
|
<div className="min-h-svh bg-canvas">
|
||||||
<TopNav />
|
<SidebarProvider>
|
||||||
<HiddenSidebar
|
<AppSidebar />
|
||||||
guildId={guildId}
|
<SidebarInset className="gap-0">
|
||||||
onGuildChange={(g) => setGuildId(g ?? "")}
|
<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 */}
|
<main className="flex flex-1 flex-col gap-4 p-4 pb-28 md:p-6 lg:pb-8">
|
||||||
<div className="pt-11">
|
<div className="mx-auto w-full max-w-[1440px]">
|
||||||
<main className="p-4 md:p-6 pb-24 md:pb-6 max-w-[1600px] mx-auto">
|
<Suspense
|
||||||
<Suspense
|
fallback={
|
||||||
fallback={
|
<div className="flex h-[60vh] items-center justify-center">
|
||||||
<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 className="size-8 rounded-full border-2 border-primary border-t-transparent animate-spin" />
|
</div>
|
||||||
</div>
|
}
|
||||||
}
|
>
|
||||||
>
|
{children}
|
||||||
{children}
|
</Suspense>
|
||||||
</Suspense>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</SidebarInset>
|
||||||
|
</SidebarProvider>
|
||||||
|
|
||||||
<MobileNav />
|
|
||||||
<MiniPlayer />
|
<MiniPlayer />
|
||||||
<ChatbotContainer />
|
<ChatbotContainer />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
import { Flag, Image, Loader2, Search } from "lucide-react";
|
import { Flag, Image, Loader2, Search } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
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 { SubNav } from "@/components/layout/sub-nav";
|
||||||
import { Lightbox } from "@/components/messages/lightbox";
|
import { Lightbox } from "@/components/messages/lightbox";
|
||||||
import { extractFirstImage } from "@/components/messages/message-card";
|
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 { SearchOverlay } from "@/components/messages/search-overlay";
|
||||||
import { EmptyState, ErrorState, LoadingSkeleton } from "@/components/shared";
|
import { EmptyState, ErrorState, LoadingSkeleton } from "@/components/shared";
|
||||||
import { GuildSelector } from "@/components/shared/guild-selector";
|
import { GuildSelector } from "@/components/shared/guild-selector";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -218,12 +217,14 @@ export default function MessagesView({
|
|||||||
{detailId && (
|
{detailId && (
|
||||||
<div className="sticky top-16 hidden w-1/2 self-start md:block lg:w-3/5">
|
<div className="sticky top-16 hidden w-1/2 self-start md:block lg:w-3/5">
|
||||||
{detailLoading ? (
|
{detailLoading ? (
|
||||||
<GlassPanel
|
<Card
|
||||||
dense
|
className={cn(
|
||||||
className="flex items-center justify-center py-12"
|
"flex items-center justify-center py-12",
|
||||||
|
"[--card-spacing:0px]",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Loader2 className="size-5 animate-spin text-text-secondary/60" />
|
<Loader2 className="size-5 animate-spin text-text-secondary/60" />
|
||||||
</GlassPanel>
|
</Card>
|
||||||
) : detailMessage ? (
|
) : detailMessage ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<button
|
<button
|
||||||
@@ -337,10 +338,14 @@ function ReviewList({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<GlassCard
|
<Card
|
||||||
key={item.id}
|
key={item.id}
|
||||||
variant="danger"
|
className={cn(
|
||||||
className="cursor-pointer p-3"
|
"cursor-pointer p-3",
|
||||||
|
"border border-red-500/30 ring-red-500/20",
|
||||||
|
"[--card-spacing:0px]",
|
||||||
|
"rounded-2xl",
|
||||||
|
)}
|
||||||
onClick={() => onSelect(item.id)}
|
onClick={() => onSelect(item.id)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
@@ -351,7 +356,7 @@ function ReviewList({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</Card>
|
||||||
))}
|
))}
|
||||||
{items.length === 0 && (
|
{items.length === 0 && (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
|
|||||||
@@ -56,6 +56,16 @@
|
|||||||
/* Ring / focus outline for shadcn outline-ring utility */
|
/* Ring / focus outline for shadcn outline-ring utility */
|
||||||
--color-ring: var(--color-primary);
|
--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 */
|
||||||
--radius-card: 16px;
|
--radius-card: 16px;
|
||||||
--radius-panel: 12px;
|
--radius-panel: 12px;
|
||||||
@@ -102,6 +112,16 @@
|
|||||||
--color-accent: var(--color-primary);
|
--color-accent: var(--color-primary);
|
||||||
--color-accent-foreground: var(--color-primary-foreground);
|
--color-accent-foreground: var(--color-primary-foreground);
|
||||||
--color-ring: var(--color-primary);
|
--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 {
|
@layer utilities {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Inter, JetBrains_Mono } from "next/font/google";
|
import { Inter, JetBrains_Mono } from "next/font/google";
|
||||||
|
import { ThemeProvider } from "next-themes";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
@@ -30,7 +31,15 @@ export default function RootLayout({
|
|||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
>
|
>
|
||||||
<body className="min-h-full flex flex-col">
|
<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 />
|
<Toaster position="bottom-right" richColors closeButton />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ import {
|
|||||||
XAxis,
|
XAxis,
|
||||||
YAxis,
|
YAxis,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import { GlassCard } from "@/components/glass/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { useMounted } from "@/lib/hooks/use-mounted";
|
import { useMounted } from "@/lib/hooks/use-mounted";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface ActivityChartProps {
|
interface ActivityChartProps {
|
||||||
data?: { day: string; messages: number; flagged: number }[];
|
data?: { day: string; messages: number; flagged: number }[];
|
||||||
@@ -28,7 +29,7 @@ export function ActivityChart({ data = [] }: ActivityChartProps) {
|
|||||||
const mounted = useMounted();
|
const mounted = useMounted();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GlassCard variant="base">
|
<Card className={cn("[--card-spacing:0px]", "rounded-2xl", "p-5")}>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<span className="text-xs font-semibold tracking-wide uppercase text-text-secondary">
|
<span className="text-xs font-semibold tracking-wide uppercase text-text-secondary">
|
||||||
Message Activity
|
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 className="h-full w-full animate-pulse rounded-md bg-card/40" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { Hash, Search } from "lucide-react";
|
import { Hash, Search } from "lucide-react";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { GlassCard } from "@/components/glass/card";
|
|
||||||
import { EmptyState, LoadingSkeleton } from "@/components/shared";
|
import { EmptyState, LoadingSkeleton } from "@/components/shared";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -11,6 +10,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { useChannelDetail, useChannels } from "@/hooks";
|
import { useChannelDetail, useChannels } from "@/hooks";
|
||||||
import { renderMessageContent } from "@/lib/format";
|
import { renderMessageContent } from "@/lib/format";
|
||||||
import type { DashboardChannel } from "@/lib/types";
|
import type { DashboardChannel } from "@/lib/types";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export function ChannelsSection({ guildId }: { guildId?: string }) {
|
export function ChannelsSection({ guildId }: { guildId?: string }) {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
@@ -31,7 +31,14 @@ export function ChannelsSection({ guildId }: { guildId?: string }) {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
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}
|
Failed to load channels: {error.message}
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -41,7 +48,7 @@ export function ChannelsSection({ guildId }: { guildId?: string }) {
|
|||||||
>
|
>
|
||||||
Retry
|
Retry
|
||||||
</Button>
|
</Button>
|
||||||
</GlassCard>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +83,9 @@ export function ChannelsSection({ guildId }: { guildId?: string }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GlassCard variant="base" className="h-fit">
|
<Card
|
||||||
|
className={cn("h-fit", "[--card-spacing:0px]", "rounded-2xl", "p-5")}
|
||||||
|
>
|
||||||
{detail ? (
|
{detail ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -144,7 +153,7 @@ export function ChannelsSection({ guildId }: { guildId?: string }) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</GlassCard>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ import {
|
|||||||
XAxis,
|
XAxis,
|
||||||
YAxis,
|
YAxis,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import { GlassCard } from "@/components/glass/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { useMounted } from "@/lib/hooks/use-mounted";
|
import { useMounted } from "@/lib/hooks/use-mounted";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface HourlyActivityChartProps {
|
interface HourlyActivityChartProps {
|
||||||
data?: { hour: number; messages: number; flagged: number }[];
|
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));
|
const maxMessages = Math.max(...full.map((d) => d.messages));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GlassCard variant="base">
|
<Card className={cn("[--card-spacing:0px]", "rounded-2xl", "p-5")}>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<span className="text-xs font-semibold tracking-wide uppercase text-text-secondary">
|
<span className="text-xs font-semibold tracking-wide uppercase text-text-secondary">
|
||||||
Hourly Activity
|
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 className="h-full w-full animate-pulse rounded-md bg-card/40" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts";
|
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 { useMounted } from "@/lib/hooks/use-mounted";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface ModerationDonutProps {
|
interface ModerationDonutProps {
|
||||||
data?: { name: string; value: number; color: string }[];
|
data?: { name: string; value: number; color: string }[];
|
||||||
@@ -27,7 +28,7 @@ export function ModerationDonut({ data = [] }: ModerationDonutProps) {
|
|||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GlassCard variant="base">
|
<Card className={cn("[--card-spacing:0px]", "rounded-2xl", "p-5")}>
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<span className="text-xs font-semibold tracking-wide uppercase text-text-secondary">
|
<span className="text-xs font-semibold tracking-wide uppercase text-text-secondary">
|
||||||
Moderation Breakdown
|
Moderation Breakdown
|
||||||
@@ -96,6 +97,6 @@ export function ModerationDonut({ data = [] }: ModerationDonutProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Flame, Heart, SmilePlus } from "lucide-react";
|
import { Flame, Heart, SmilePlus } from "lucide-react";
|
||||||
import { GlassCard } from "@/components/glass/card";
|
|
||||||
import { EmptyState, LoadingSkeleton } from "@/components/shared";
|
import { EmptyState, LoadingSkeleton } from "@/components/shared";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
import { useTopReactions, useTopReactors } from "@/hooks";
|
import { useTopReactions, useTopReactors } from "@/hooks";
|
||||||
import { renderMessageContent } from "@/lib/format";
|
import { renderMessageContent } from "@/lib/format";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function formatReactionTime(ts: number | null): string {
|
function formatReactionTime(ts: number | null): string {
|
||||||
if (!ts) return "";
|
if (!ts) return "";
|
||||||
@@ -31,19 +32,23 @@ export function ReactionsSection() {
|
|||||||
{reactionsLoading ? (
|
{reactionsLoading ? (
|
||||||
<LoadingSkeleton count={5} height="h-16" />
|
<LoadingSkeleton count={5} height="h-16" />
|
||||||
) : !reactions || reactions.length === 0 ? (
|
) : !reactions || reactions.length === 0 ? (
|
||||||
<GlassCard className="p-6">
|
<Card className={cn("p-6", "[--card-spacing:0px]", "rounded-2xl")}>
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={Heart}
|
icon={Heart}
|
||||||
title="Belum ada reaksi"
|
title="Belum ada reaksi"
|
||||||
description="Pesan dengan reaksi emoji akan muncul di sini."
|
description="Pesan dengan reaksi emoji akan muncul di sini."
|
||||||
/>
|
/>
|
||||||
</GlassCard>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{reactions.map((r, i) => (
|
{reactions.map((r, i) => (
|
||||||
<GlassCard
|
<Card
|
||||||
key={r.message_id}
|
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">
|
<span className="w-6 shrink-0 text-center font-mono text-xs text-text-secondary/50">
|
||||||
{i + 1}
|
{i + 1}
|
||||||
@@ -76,7 +81,7 @@ export function ReactionsSection() {
|
|||||||
<Heart className="size-3" />
|
<Heart className="size-3" />
|
||||||
{r.reaction_count}
|
{r.reaction_count}
|
||||||
</Badge>
|
</Badge>
|
||||||
</GlassCard>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -90,19 +95,23 @@ export function ReactionsSection() {
|
|||||||
{reactorsLoading ? (
|
{reactorsLoading ? (
|
||||||
<LoadingSkeleton count={5} height="h-14" />
|
<LoadingSkeleton count={5} height="h-14" />
|
||||||
) : !reactors || reactors.length === 0 ? (
|
) : !reactors || reactors.length === 0 ? (
|
||||||
<GlassCard className="p-6">
|
<Card className={cn("p-6", "[--card-spacing:0px]", "rounded-2xl")}>
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={SmilePlus}
|
icon={SmilePlus}
|
||||||
title="Belum ada reaktor"
|
title="Belum ada reaktor"
|
||||||
description="User yang ngasih reaksi emoji akan muncul di sini."
|
description="User yang ngasih reaksi emoji akan muncul di sini."
|
||||||
/>
|
/>
|
||||||
</GlassCard>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{reactors.map((r, i) => (
|
{reactors.map((r, i) => (
|
||||||
<GlassCard
|
<Card
|
||||||
key={r.user_id}
|
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">
|
<span className="w-6 shrink-0 text-center font-mono text-xs text-text-secondary/50">
|
||||||
{i + 1}
|
{i + 1}
|
||||||
@@ -120,7 +129,7 @@ export function ReactionsSection() {
|
|||||||
<Flame className="size-3" />
|
<Flame className="size-3" />
|
||||||
{r.net_count}
|
{r.net_count}
|
||||||
</Badge>
|
</Badge>
|
||||||
</GlassCard>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
import { Area, AreaChart, ResponsiveContainer } from "recharts";
|
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 { useMounted } from "@/lib/hooks/use-mounted";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
@@ -39,7 +39,13 @@ export function StatCard({
|
|||||||
const numValue = typeof value === "number" ? value : Number(value);
|
const numValue = typeof value === "number" ? value : Number(value);
|
||||||
|
|
||||||
return (
|
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="flex items-start justify-between mb-2">
|
||||||
<div className={cn("p-1.5 rounded-md", bgAccent)}>
|
<div className={cn("p-1.5 rounded-md", bgAccent)}>
|
||||||
<Icon className="size-4" />
|
<Icon className="size-4" />
|
||||||
@@ -90,6 +96,6 @@ export function StatCard({
|
|||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</GlassCard>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import {
|
|||||||
XAxis,
|
XAxis,
|
||||||
YAxis,
|
YAxis,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import { GlassCard } from "@/components/glass/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { useMounted } from "@/lib/hooks/use-mounted";
|
import { useMounted } from "@/lib/hooks/use-mounted";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface TopChannelsChartProps {
|
interface TopChannelsChartProps {
|
||||||
data?: { name: string; count: number }[];
|
data?: { name: string; count: number }[];
|
||||||
@@ -19,7 +20,7 @@ export function TopChannelsChart({ data = [] }: TopChannelsChartProps) {
|
|||||||
const mounted = useMounted();
|
const mounted = useMounted();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GlassCard variant="base">
|
<Card className={cn("[--card-spacing:0px]", "rounded-2xl", "p-5")}>
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<span className="text-xs font-semibold tracking-wide uppercase text-text-secondary">
|
<span className="text-xs font-semibold tracking-wide uppercase text-text-secondary">
|
||||||
Top Channels
|
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 className="h-full w-full animate-pulse rounded-md bg-card/40" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { Search, Users, UserX } from "lucide-react";
|
import { Search, Users, UserX } from "lucide-react";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { GlassCard } from "@/components/glass/card";
|
|
||||||
import { EmptyState, LoadingSkeleton } from "@/components/shared";
|
import { EmptyState, LoadingSkeleton } from "@/components/shared";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -12,6 +11,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { useUserDetail, useUsers } from "@/hooks";
|
import { useUserDetail, useUsers } from "@/hooks";
|
||||||
import { renderMessageContent } from "@/lib/format";
|
import { renderMessageContent } from "@/lib/format";
|
||||||
import type { DashboardUser } from "@/lib/types";
|
import type { DashboardUser } from "@/lib/types";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const TRUST_TIERS = [
|
const TRUST_TIERS = [
|
||||||
{
|
{
|
||||||
@@ -67,7 +67,14 @@ export function UsersSection() {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
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}
|
Failed to load users: {error.message}
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -77,7 +84,7 @@ export function UsersSection() {
|
|||||||
>
|
>
|
||||||
Retry
|
Retry
|
||||||
</Button>
|
</Button>
|
||||||
</GlassCard>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +119,9 @@ export function UsersSection() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GlassCard variant="base" className="h-fit">
|
<Card
|
||||||
|
className={cn("h-fit", "[--card-spacing:0px]", "rounded-2xl", "p-5")}
|
||||||
|
>
|
||||||
{detail ? (
|
{detail ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -196,7 +205,7 @@ export function UsersSection() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</GlassCard>
|
</Card>
|
||||||
</div>
|
</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";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { GlassPanel } from "@/components/glass/panel";
|
import { Card } from "@/components/ui/card";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface AiAnalysisPanelProps {
|
interface AiAnalysisPanelProps {
|
||||||
@@ -37,11 +37,11 @@ export function AiAnalysisPanel({
|
|||||||
|
|
||||||
if (!status || status === "pending") {
|
if (!status || status === "pending") {
|
||||||
return (
|
return (
|
||||||
<GlassPanel dense>
|
<Card className={cn("[--card-spacing:0px]", "p-3")}>
|
||||||
<span className="text-xs text-text-secondary/50">
|
<span className="text-xs text-text-secondary/50">
|
||||||
AI analysis pending
|
AI analysis pending
|
||||||
</span>
|
</span>
|
||||||
</GlassPanel>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ export function AiAnalysisPanel({
|
|||||||
: categories || [];
|
: categories || [];
|
||||||
|
|
||||||
return (
|
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">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs font-semibold tracking-wide uppercase text-text-secondary">
|
<span className="text-xs font-semibold tracking-wide uppercase text-text-secondary">
|
||||||
AI Analysis
|
AI Analysis
|
||||||
@@ -155,6 +155,6 @@ export function AiAnalysisPanel({
|
|||||||
<span className="font-mono text-accent-amber">{action}</span>
|
<span className="font-mono text-accent-amber">{action}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</GlassPanel>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ArrowLeft, MessageSquare, MessagesSquare, Pencil } from "lucide-react";
|
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 { getMessageChannelLabel, renderMessageContent } from "@/lib/format";
|
||||||
import type { AttachmentRecord, MessageRecord } from "@/lib/types";
|
import type { AttachmentRecord, MessageRecord } from "@/lib/types";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { AiAnalysisPanel } from "./ai-analysis-panel";
|
import { AiAnalysisPanel } from "./ai-analysis-panel";
|
||||||
import { AttachmentsGrid } from "./attachments-grid";
|
import { AttachmentsGrid } from "./attachments-grid";
|
||||||
|
|
||||||
@@ -21,7 +22,9 @@ export function MessageDetailView({
|
|||||||
onImageClick,
|
onImageClick,
|
||||||
}: MessageDetailViewProps) {
|
}: MessageDetailViewProps) {
|
||||||
return (
|
return (
|
||||||
<GlassCard variant="base" className="h-full">
|
<Card
|
||||||
|
className={cn("h-full", "[--card-spacing:0px]", "rounded-2xl", "p-5")}
|
||||||
|
>
|
||||||
{onBack && (
|
{onBack && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -94,6 +97,6 @@ export function MessageDetailView({
|
|||||||
score={message.ai_moderation_score}
|
score={message.ai_moderation_score}
|
||||||
analysis={message.ai_analysis}
|
analysis={message.ai_analysis}
|
||||||
/>
|
/>
|
||||||
</GlassCard>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ArrowLeft, MessageSquare, MessagesSquare } from "lucide-react";
|
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 { getMessageChannelLabel, renderMessageContent } from "@/lib/format";
|
||||||
import type { AttachmentRecord, MessageRecord } from "@/lib/types";
|
import type { AttachmentRecord, MessageRecord } from "@/lib/types";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { AiAnalysisPanel } from "./ai-analysis-panel";
|
import { AiAnalysisPanel } from "./ai-analysis-panel";
|
||||||
import { AttachmentsGrid } from "./attachments-grid";
|
import { AttachmentsGrid } from "./attachments-grid";
|
||||||
|
|
||||||
@@ -19,7 +20,9 @@ export function MessageDetail({
|
|||||||
onBack,
|
onBack,
|
||||||
}: MessageDetailProps) {
|
}: MessageDetailProps) {
|
||||||
return (
|
return (
|
||||||
<GlassCard variant="base" className="h-full">
|
<Card
|
||||||
|
className={cn("h-full", "[--card-spacing:0px]", "rounded-2xl", "p-5")}
|
||||||
|
>
|
||||||
{onBack && (
|
{onBack && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -68,6 +71,6 @@ export function MessageDetail({
|
|||||||
score={message.ai_moderation_score}
|
score={message.ai_moderation_score}
|
||||||
analysis={message.ai_analysis}
|
analysis={message.ai_analysis}
|
||||||
/>
|
/>
|
||||||
</GlassCard>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import {
|
|||||||
XCircle,
|
XCircle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { GlassCard } from "@/components/glass/card";
|
|
||||||
import { EmptyState, LoadingSkeleton } from "@/components/shared";
|
import { EmptyState, LoadingSkeleton } from "@/components/shared";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
import { useModerationActions, useModerationStats } from "@/hooks";
|
import { useModerationActions, useModerationStats } from "@/hooks";
|
||||||
import { renderMessageContent } from "@/lib/format";
|
import { renderMessageContent } from "@/lib/format";
|
||||||
import type {
|
import type {
|
||||||
@@ -173,13 +173,13 @@ export function ModerationSection({
|
|||||||
{actionsLoading && !actions ? (
|
{actionsLoading && !actions ? (
|
||||||
<LoadingSkeleton count={6} height="h-16" />
|
<LoadingSkeleton count={6} height="h-16" />
|
||||||
) : !actions || actions.length === 0 ? (
|
) : !actions || actions.length === 0 ? (
|
||||||
<GlassCard className="p-6">
|
<Card className={cn("p-6", "[--card-spacing:0px]", "rounded-2xl")}>
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={ShieldAlert}
|
icon={ShieldAlert}
|
||||||
title="Belum ada aksi moderasi"
|
title="Belum ada aksi moderasi"
|
||||||
description="Aksi auto- moderasi (delete, warn, kick, ban) akan muncul di sini."
|
description="Aksi auto- moderasi (delete, warn, kick, ban) akan muncul di sini."
|
||||||
/>
|
/>
|
||||||
</GlassCard>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{actions.map((a) => (
|
{actions.map((a) => (
|
||||||
@@ -207,7 +207,7 @@ function SummaryCard({
|
|||||||
hint?: string;
|
hint?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
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">
|
<p className="text-[10px] uppercase tracking-wide text-text-secondary/50">
|
||||||
{label}
|
{label}
|
||||||
</p>
|
</p>
|
||||||
@@ -217,7 +217,7 @@ function SummaryCard({
|
|||||||
<span className="ml-1 text-xs font-medium opacity-80">({hint})</span>
|
<span className="ml-1 text-xs font-medium opacity-80">({hint})</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</GlassCard>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,7 +251,13 @@ function ActionRow({ action }: { action: ModerationAction }) {
|
|||||||
const st = STATUS_META[action.status];
|
const st = STATUS_META[action.status];
|
||||||
const Icon = meta.Icon;
|
const Icon = meta.Icon;
|
||||||
return (
|
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)}>
|
<span className={cn("mt-0.5 shrink-0", meta.className)}>
|
||||||
<Icon className="size-4" />
|
<Icon className="size-4" />
|
||||||
</span>
|
</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" />
|
<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 { Download, Loader2, Pause, Play } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { GlassCard } from "@/components/glass/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { formatBytes } from "@/lib/format";
|
import { formatBytes } from "@/lib/format";
|
||||||
import type { VoiceRecording } from "@/lib/types";
|
import type { VoiceRecording } from "@/lib/types";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface RecordingCardProps {
|
interface RecordingCardProps {
|
||||||
recording: VoiceRecording;
|
recording: VoiceRecording;
|
||||||
@@ -55,13 +56,17 @@ export function RecordingCard({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GlassCard
|
<Card
|
||||||
variant="interactive"
|
className={cn(
|
||||||
className={`p-4 transition-all ${
|
`p-4 transition-all ${
|
||||||
active
|
active
|
||||||
? "ring-1 ring-primary/40 border-primary/30 animate-card-glow"
|
? "ring-1 ring-primary/40 border-primary/30 animate-card-glow"
|
||||||
: "hover:ring-1 hover:ring-border/60"
|
: "hover:ring-1 hover:ring-border/60"
|
||||||
}`}
|
}`,
|
||||||
|
"cursor-pointer transition-colors hover:ring-primary/40",
|
||||||
|
"[--card-spacing:0px]",
|
||||||
|
"rounded-2xl",
|
||||||
|
)}
|
||||||
onClick={() => onTogglePlay(recording.id)}
|
onClick={() => onTogglePlay(recording.id)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
@@ -151,6 +156,6 @@ export function RecordingCard({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
import { Loader2, Pause, Play, X } from "lucide-react";
|
import { Loader2, Pause, Play, X } from "lucide-react";
|
||||||
import { useEffect, useRef, useState } from "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 {
|
interface RecordingPlayerProps {
|
||||||
url?: string;
|
url?: string;
|
||||||
@@ -70,9 +71,12 @@ export function RecordingPlayer({
|
|||||||
const pct = duration > 0 ? Math.min(100, (progress / duration) * 100) : 0;
|
const pct = duration > 0 ? Math.min(100, (progress / duration) * 100) : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GlassPanel
|
<Card
|
||||||
dense
|
className={cn(
|
||||||
className="fixed bottom-20 left-4 z-30 w-80 flex flex-col gap-1.5"
|
"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">
|
<div className="flex items-center gap-2.5">
|
||||||
<button
|
<button
|
||||||
@@ -140,6 +144,6 @@ export function RecordingPlayer({
|
|||||||
}}
|
}}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
/>
|
/>
|
||||||
</GlassPanel>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
import { Inbox } 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";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface EmptyStateProps {
|
interface EmptyStateProps {
|
||||||
@@ -19,13 +19,16 @@ export function EmptyState({
|
|||||||
className,
|
className,
|
||||||
}: EmptyStateProps) {
|
}: EmptyStateProps) {
|
||||||
return (
|
return (
|
||||||
<GlassPanel
|
<Card
|
||||||
dense
|
className={cn(
|
||||||
className={cn("flex flex-col items-center gap-2 py-12", className)}
|
"flex flex-col items-center gap-2 py-12",
|
||||||
|
className,
|
||||||
|
"[--card-spacing:0px]",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Icon className="size-8 text-text-secondary/20" />
|
<Icon className="size-8 text-text-secondary/20" />
|
||||||
<p className="text-sm text-text-secondary/60">{title}</p>
|
<p className="text-sm text-text-secondary/60">{title}</p>
|
||||||
<p className="text-xs text-text-secondary/40">{description}</p>
|
<p className="text-xs text-text-secondary/40">{description}</p>
|
||||||
</GlassPanel>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
import { AlertCircle, RefreshCw } from "lucide-react";
|
import { AlertCircle, RefreshCw } from "lucide-react";
|
||||||
import { Component, type ReactNode } from "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 {
|
interface Props {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -24,9 +25,13 @@ export class ErrorBoundary extends Component<Props, State> {
|
|||||||
if (this.state.hasError) {
|
if (this.state.hasError) {
|
||||||
return (
|
return (
|
||||||
this.props.fallback || (
|
this.props.fallback || (
|
||||||
<GlassCard
|
<Card
|
||||||
variant="danger"
|
className={cn(
|
||||||
className="flex flex-col items-center gap-2 py-8"
|
"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" />
|
<AlertCircle className="size-6 text-destructive" />
|
||||||
<p className="text-sm text-text-secondary">
|
<p className="text-sm text-text-secondary">
|
||||||
@@ -39,7 +44,7 @@ export class ErrorBoundary extends Component<Props, State> {
|
|||||||
>
|
>
|
||||||
<RefreshCw className="size-3" /> Try again
|
<RefreshCw className="size-3" /> Try again
|
||||||
</button>
|
</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";
|
"use client";
|
||||||
|
|
||||||
import { Mic, MicOff } from "lucide-react";
|
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 type { ActiveSpeaker } from "@/lib/types";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface ActivityTimelineProps {
|
interface ActivityTimelineProps {
|
||||||
data?: ActiveSpeaker[];
|
data?: ActiveSpeaker[];
|
||||||
@@ -27,7 +28,7 @@ export function VoiceActivityTimeline({ data = [] }: ActivityTimelineProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GlassCard variant="base">
|
<Card className={cn("[--card-spacing:0px]", "rounded-2xl", "p-5")}>
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<span className="text-xs font-semibold tracking-wide uppercase text-text-secondary">
|
<span className="text-xs font-semibold tracking-wide uppercase text-text-secondary">
|
||||||
Voice Activity
|
Voice Activity
|
||||||
@@ -79,6 +80,6 @@ export function VoiceActivityTimeline({ data = [] }: ActivityTimelineProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</GlassCard>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Headphones, Server, Volume2 } from "lucide-react";
|
import { Headphones, Server, Volume2 } from "lucide-react";
|
||||||
import { GlassCard } from "@/components/glass/card";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -41,7 +41,14 @@ export function VoiceConnectionCard({
|
|||||||
connecting,
|
connecting,
|
||||||
}: ConnectionCardProps) {
|
}: ConnectionCardProps) {
|
||||||
return (
|
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">
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -188,6 +195,6 @@ export function VoiceConnectionCard({
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
import { HeadphoneOff, Headphones } from "lucide-react";
|
import { HeadphoneOff, Headphones } from "lucide-react";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { GlassCard } from "@/components/glass/card";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
import { hashUserId } from "@/hooks";
|
import { hashUserId } from "@/hooks";
|
||||||
import type { ActiveSpeaker } from "@/lib/types";
|
import type { ActiveSpeaker } from "@/lib/types";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface ListenControlProps {
|
interface ListenControlProps {
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
@@ -86,7 +87,7 @@ export function ListenControl({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GlassCard variant="base">
|
<Card className={cn("[--card-spacing:0px]", "rounded-2xl", "p-5")}>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Button
|
<Button
|
||||||
variant={active ? "default" : "secondary"}
|
variant={active ? "default" : "secondary"}
|
||||||
@@ -148,6 +149,6 @@ export function ListenControl({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Mic, MicOff } from "lucide-react";
|
import { Mic, MicOff } from "lucide-react";
|
||||||
import { GlassCard } from "@/components/glass/card";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface MicControlProps {
|
interface MicControlProps {
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
@@ -20,7 +21,7 @@ export function MicControl({
|
|||||||
onVolumeChange,
|
onVolumeChange,
|
||||||
}: MicControlProps) {
|
}: MicControlProps) {
|
||||||
return (
|
return (
|
||||||
<GlassCard variant="base">
|
<Card className={cn("[--card-spacing:0px]", "rounded-2xl", "p-5")}>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Button
|
<Button
|
||||||
variant={active ? "default" : "secondary"}
|
variant={active ? "default" : "secondary"}
|
||||||
@@ -53,6 +54,6 @@ export function MicControl({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { GlassPanel } from "@/components/glass/panel";
|
import { Card } from "@/components/ui/card";
|
||||||
import type { ActiveSpeaker } from "@/lib/types";
|
import type { ActiveSpeaker } from "@/lib/types";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface SpeakerWaveformProps {
|
interface SpeakerWaveformProps {
|
||||||
speakers: ActiveSpeaker[];
|
speakers: ActiveSpeaker[];
|
||||||
@@ -45,16 +46,16 @@ export function SpeakerWaveform({ speakers }: SpeakerWaveformProps) {
|
|||||||
|
|
||||||
if (speakers.length === 0) {
|
if (speakers.length === 0) {
|
||||||
return (
|
return (
|
||||||
<GlassPanel dense>
|
<Card className={cn("[--card-spacing:0px]", "p-3")}>
|
||||||
<span className="text-xs text-text-secondary/40">
|
<span className="text-xs text-text-secondary/40">
|
||||||
No speakers detected
|
No speakers detected
|
||||||
</span>
|
</span>
|
||||||
</GlassPanel>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GlassPanel dense>
|
<Card className={cn("[--card-spacing:0px]", "p-3")}>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{speakers.map((s) => (
|
{speakers.map((s) => (
|
||||||
<div key={s.userId} className="flex items-center gap-2 text-xs">
|
<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}
|
height={speakers.length * 30}
|
||||||
className="w-full h-auto mt-2 rounded"
|
className="w-full h-auto mt-2 rounded"
|
||||||
/>
|
/>
|
||||||
</GlassPanel>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user