diff --git a/package.json b/package.json
index 974f36d3..aeb7c0a0 100644
--- a/package.json
+++ b/package.json
@@ -47,19 +47,12 @@
"lint": "eslint . --max-warnings=100"
},
"dependencies": {
+ "@base-ui/react": "^1.3.0",
"@earthyscience/netcdf4-wasm": "0.2.3",
"@ffmpeg/ffmpeg": "^0.12.15",
"@ffmpeg/util": "^0.12.2",
+ "@hookform/resolvers": "^5.2.2",
"@monaco-editor/react": "^4.7.0",
- "@radix-ui/react-accordion": "^1.2.12",
- "@radix-ui/react-collapsible": "^1.1.12",
- "@radix-ui/react-dialog": "^1.1.15",
- "@radix-ui/react-scroll-area": "^1.2.9",
- "@radix-ui/react-separator": "^1.1.7",
- "@radix-ui/react-slider": "^1.3.6",
- "@radix-ui/react-slot": "^1.2.3",
- "@radix-ui/react-switch": "^1.2.6",
- "@radix-ui/react-tooltip": "^1.2.8",
"@react-spring/three": "^10.0.0",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
@@ -69,16 +62,23 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
+ "date-fns": "^4.1.0",
+ "embla-carousel-react": "^8.6.0",
"fflate": "^0.8.2",
"gsap": "^3.13.0",
+ "input-otp": "^1.4.2",
"js-colormaps-es": "^0.0.5",
"lucide-react": "^0.562.0",
"next": "16.1.6",
"next-themes": "^0.4.6",
- "radix-ui": "latest",
+ "radix-ui": "1.4.3",
"react": "^19.0.0",
+ "react-day-picker": "^9.14.0",
"react-dom": "^19.0.0",
+ "react-hook-form": "^7.71.2",
"react-icons": "^5.5.0",
+ "react-resizable-panels": "^4.7.3",
+ "recharts": "2.15.4",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.0",
"three": "^0.182.0",
@@ -86,6 +86,7 @@
"vaul": "^1.1.2",
"webgpu-utils": "^1.11.0",
"zarrita": "^0.5.4",
+ "zod": "^4.3.6",
"zustand": "^5.0.8"
},
"devDependencies": {
diff --git a/src/app/BrowZarrPopover.tsx b/src/app/BrowZarrPopover.tsx
index de1ffb02..2d6d2ba8 100644
--- a/src/app/BrowZarrPopover.tsx
+++ b/src/app/BrowZarrPopover.tsx
@@ -1,5 +1,5 @@
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
-import { Button } from "@/components/ui/button"
+import { Button } from "@/components/ui/button-enhanced"
import { Separator } from "@/components/ui/separator"
import Link from "next/link"
import Image from "next/image"
diff --git a/src/app/globals.css b/src/app/globals.css
index 30daddfe..d91a8203 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1,6 +1,8 @@
@import "tailwindcss";
@import "tw-animate-css";
+@custom-variant dark (&:is(.dark *));
+
:root {
--background-plot: white;
--background-modal: hsla(0, 0%, 95%, 0.7);
@@ -422,4 +424,15 @@ a[href]:hover::after {
body {
@apply bg-background text-foreground;
}
+}
+
+.dark {
+ --sidebar: hsl(240 5.9% 10%);
+ --sidebar-foreground: hsl(240 4.8% 95.9%);
+ --sidebar-primary: hsl(224.3 76.3% 48%);
+ --sidebar-primary-foreground: hsl(0 0% 100%);
+ --sidebar-accent: hsl(240 3.7% 15.9%);
+ --sidebar-accent-foreground: hsl(240 4.8% 95.9%);
+ --sidebar-border: hsl(240 3.7% 15.9%);
+ --sidebar-ring: hsl(217.2 91.2% 59.8%);
}
\ No newline at end of file
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 32451907..fe10eddd 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,6 +1,7 @@
import type { Metadata, Viewport } from "next";
import ClientRoot from "./ClientRoot";
import { Toaster } from "@/components/ui/sonner"
+import { TooltipProvider } from "@/components/ui/tooltip"
import "./globals.css";
export const viewport: Viewport = {
@@ -20,10 +21,12 @@ export default function RootLayout({
return (
-
- {children}
-
-
+
+
+ {children}
+
+
+
);
diff --git a/src/components/ui/Elements/AboutDrawer.tsx b/src/components/ui/Elements/AboutDrawer.tsx
index c6f4d4ae..717995dd 100644
--- a/src/components/ui/Elements/AboutDrawer.tsx
+++ b/src/components/ui/Elements/AboutDrawer.tsx
@@ -2,7 +2,7 @@ import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerTrigger } from
import AboutInfo from "@/components/ui/Elements/AboutInfo";
import logo from "@/app/logo.png";
import Image from "next/image";
-import { Button } from "@/components/ui/button";
+import { Button } from "@/components/ui/button-enhanced";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
export default function LogoDrawer() {
diff --git a/src/components/ui/Elements/GithubButton.tsx b/src/components/ui/Elements/GithubButton.tsx
index 4937c3d9..4bfbcbae 100644
--- a/src/components/ui/Elements/GithubButton.tsx
+++ b/src/components/ui/Elements/GithubButton.tsx
@@ -2,7 +2,7 @@
import { FaGithub } from "react-icons/fa";
import Link from "next/link";
-import { Button } from "@/components/ui/button";
+import { Button } from "@/components/ui/button-enhanced";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
export default function GithubButton() {
diff --git a/src/components/ui/Elements/HomeButton.tsx b/src/components/ui/Elements/HomeButton.tsx
index 3d6c7c9e..2cb5cf1d 100644
--- a/src/components/ui/Elements/HomeButton.tsx
+++ b/src/components/ui/Elements/HomeButton.tsx
@@ -3,7 +3,7 @@
import Image from "next/image";
import Link from "next/link";
import logoHome from "public/logo-light.svg";
-import { Button } from "@/components/ui/button";
+import { Button } from "@/components/ui/button-enhanced";
import {
Tooltip,
TooltipContent,
diff --git a/src/components/ui/Elements/ThemeSwitch.tsx b/src/components/ui/Elements/ThemeSwitch.tsx
index 980d5d92..6e24f151 100644
--- a/src/components/ui/Elements/ThemeSwitch.tsx
+++ b/src/components/ui/Elements/ThemeSwitch.tsx
@@ -3,7 +3,7 @@
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import { BsMoonStarsFill, BsSunFill } from "react-icons/bs";
-import { Button } from "@/components/ui/button";
+import { Button } from "@/components/ui/button-enhanced";
import {
Tooltip,
TooltipContent,
diff --git a/src/components/ui/Elements/VersionSelector.tsx b/src/components/ui/Elements/VersionSelector.tsx
index e869a64b..b7df7eab 100644
--- a/src/components/ui/Elements/VersionSelector.tsx
+++ b/src/components/ui/Elements/VersionSelector.tsx
@@ -2,7 +2,7 @@
import React, { useState, useEffect } from "react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
-import { Button } from "@/components/ui/button";
+import { Button } from "@/components/ui/button-enhanced";
import { FaCheck } from "react-icons/fa6";
import { LuChevronDown } from "react-icons/lu";
import { BsTags } from "react-icons/bs";
diff --git a/src/components/ui/LinePlotArea/PlotLineOptions.tsx b/src/components/ui/LinePlotArea/PlotLineOptions.tsx
index 375c0e93..a62fb13e 100644
--- a/src/components/ui/LinePlotArea/PlotLineOptions.tsx
+++ b/src/components/ui/LinePlotArea/PlotLineOptions.tsx
@@ -1,5 +1,5 @@
import React from 'react'
-import { Button } from "@/components/ui/button"
+import { Button } from "@/components/ui/button-enhanced"
import '../css/PlotLineOptions.css'
import {
DropdownMenu,
diff --git a/src/components/ui/MainPanel/AnalysisOptions.tsx b/src/components/ui/MainPanel/AnalysisOptions.tsx
index 7e40afbc..b5ec11b9 100644
--- a/src/components/ui/MainPanel/AnalysisOptions.tsx
+++ b/src/components/ui/MainPanel/AnalysisOptions.tsx
@@ -9,7 +9,7 @@ import '../css/MainPanel.css';
import { PiMathOperationsBold } from "react-icons/pi";
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover';
import { Input } from '../input';
-import { Button } from '../button';
+import { Button } from '../button-enhanced';
import { CiUndo } from "react-icons/ci";
import {KernelVisualizer} from "@/components/ui";
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
diff --git a/src/components/ui/MainPanel/Colormaps.tsx b/src/components/ui/MainPanel/Colormaps.tsx
index e3660f99..1e02d72a 100644
--- a/src/components/ui/MainPanel/Colormaps.tsx
+++ b/src/components/ui/MainPanel/Colormaps.tsx
@@ -7,7 +7,7 @@ import { colormaps } from '@/components/textures';
import { useShallow } from 'zustand/shallow';
import { MdOutlineSwapVert } from "react-icons/md";
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"
-import { Button } from "@/components/ui/button";
+import { Button } from "@/components/ui/button-enhanced";
import Image from 'next/image';
import {
Tooltip,
diff --git a/src/components/ui/MainPanel/PanelItem.tsx b/src/components/ui/MainPanel/PanelItem.tsx
index 7f354520..b0805793 100644
--- a/src/components/ui/MainPanel/PanelItem.tsx
+++ b/src/components/ui/MainPanel/PanelItem.tsx
@@ -5,7 +5,7 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
-import { Button } from "@/components/ui/button"
+import { Button } from "@/components/ui/button-enhanced"
import { ReactNode } from "react"
export function PanelItem({ children, options }: { children: ReactNode; options: ReactNode }) {
diff --git a/src/components/ui/MainPanel/PlotType.tsx b/src/components/ui/MainPanel/PlotType.tsx
index e9fe5a7e..54bb94c6 100644
--- a/src/components/ui/MainPanel/PlotType.tsx
+++ b/src/components/ui/MainPanel/PlotType.tsx
@@ -10,7 +10,7 @@ import { CgMenuGridO } from "react-icons/cg";
import { PiCubeLight } from "react-icons/pi";
import { MdOutlineSquare } from "react-icons/md";
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"
-import { Button } from "@/components/ui/button"
+import { Button } from "@/components/ui/button-enhanced"
import {
Tooltip,
TooltipContent,
diff --git a/src/components/ui/MainPanel/Variables.tsx b/src/components/ui/MainPanel/Variables.tsx
index bf29d17b..373ef705 100644
--- a/src/components/ui/MainPanel/Variables.tsx
+++ b/src/components/ui/MainPanel/Variables.tsx
@@ -9,7 +9,7 @@ import MetaDataInfo from "./MetaDataInfo";
import { GetDimInfo } from "@/utils/HelperFuncs";
import { GetAttributes } from "@/components/zarr/ZarrLoaderLRU";
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
-import { Button } from "@/components/ui/button";
+import { Button } from "@/components/ui/button-enhanced";
import { Input } from "../input";
import {
Tooltip,
diff --git a/src/components/ui/MetaData.tsx b/src/components/ui/MetaData.tsx
index 1997996c..0771add0 100644
--- a/src/components/ui/MetaData.tsx
+++ b/src/components/ui/MetaData.tsx
@@ -18,7 +18,7 @@ import {
} from "@/components/ui/tooltip"
-import { Button } from "@/components/ui/button"
+import { Button } from "@/components/ui/button-enhanced"
export const defaultAttributes = [
"long_name",
diff --git a/src/components/ui/NavBar/PerformanceMode.tsx b/src/components/ui/NavBar/PerformanceMode.tsx
index cbf625bc..efdedaa4 100644
--- a/src/components/ui/NavBar/PerformanceMode.tsx
+++ b/src/components/ui/NavBar/PerformanceMode.tsx
@@ -5,7 +5,7 @@ import { Potato } from '../Elements/Icons';
import { FaCarSide } from "react-icons/fa6";
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"
import { useGlobalStore } from '@/GlobalStates/GlobalStore';
-import { Button } from "@/components/ui/button"
+import { Button } from "@/components/ui/button-enhanced"
const icons = {
"fast": ,
diff --git a/src/components/ui/NavBar/PlotLineButton.tsx b/src/components/ui/NavBar/PlotLineButton.tsx
index 54e875a6..adc1fef2 100644
--- a/src/components/ui/NavBar/PlotLineButton.tsx
+++ b/src/components/ui/NavBar/PlotLineButton.tsx
@@ -7,7 +7,7 @@ import { useErrorStore } from '@/GlobalStates/ErrorStore';
import { useShallow } from 'zustand/shallow';
import '../css/PlotLineButton.css'
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
-import { Button } from "@/components/ui/button"
+import { Button } from "@/components/ui/button-enhanced"
const PlotLineButton = () => {
const {selectTS, resetAnim, animate, plotType, displaceSurface, setSelectTS, setResetAnim} = usePlotStore(useShallow(state => ({
diff --git a/src/components/ui/ShaderEditor.tsx b/src/components/ui/ShaderEditor.tsx
index 7f6165f4..bc6c1240 100644
--- a/src/components/ui/ShaderEditor.tsx
+++ b/src/components/ui/ShaderEditor.tsx
@@ -9,7 +9,7 @@ import {
SelectValue,
} from '@/components/ui/select';
import './css/ShaderEditor.css'
-import { Button } from './button';
+import { Button } from './button-enhanced';
import { useGlobalStore } from '@/GlobalStates/GlobalStore';
import { useAnalysisStore } from '@/GlobalStates/AnalysisStore';
import { IoCloseCircleSharp } from "react-icons/io5";
diff --git a/src/components/ui/VariablesTable.tsx b/src/components/ui/VariablesTable.tsx
index d99a2450..e873394b 100644
--- a/src/components/ui/VariablesTable.tsx
+++ b/src/components/ui/VariablesTable.tsx
@@ -3,7 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Input } from "@/components/ui/input";
-import { Button } from "@/components/ui/button";
+import { Button } from "@/components/ui/button-enhanced";
import { useGlobalStore } from "@/GlobalStates/GlobalStore";
import { useShallow } from "zustand/shallow";
import {
diff --git a/src/components/ui/Widgets/SliderThumbs.tsx b/src/components/ui/Widgets/SliderThumbs.tsx
index 51728cbc..b113ed79 100644
--- a/src/components/ui/Widgets/SliderThumbs.tsx
+++ b/src/components/ui/Widgets/SliderThumbs.tsx
@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
-import * as SliderPrimitive from "@radix-ui/react-slider"
+import { Slider as SliderPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx
index abb96119..91e3a250 100644
--- a/src/components/ui/accordion.tsx
+++ b/src/components/ui/accordion.tsx
@@ -1,8 +1,8 @@
"use client"
import * as React from "react"
-import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDownIcon } from "lucide-react"
+import { Accordion as AccordionPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
@@ -19,7 +19,7 @@ function AccordionItem({
return (
)
@@ -35,13 +35,13 @@ function AccordionTrigger({
svg]:rotate-180",
+ "flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
-
+
)
@@ -55,13 +55,12 @@ function AccordionContent({
return (
- {children}
+ {children}
)
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
-
diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx
new file mode 100644
index 00000000..dad32734
--- /dev/null
+++ b/src/components/ui/alert-dialog.tsx
@@ -0,0 +1,196 @@
+"use client"
+
+import * as React from "react"
+import { AlertDialog as AlertDialogPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+
+function AlertDialog({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function AlertDialogTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogPortal({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogContent({
+ className,
+ size = "default",
+ ...props
+}: React.ComponentProps & {
+ size?: "default" | "sm"
+}) {
+ return (
+
+
+
+
+ )
+}
+
+function AlertDialogHeader({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function AlertDialogFooter({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function AlertDialogTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogMedia({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function AlertDialogAction({
+ className,
+ variant = "default",
+ size = "default",
+ ...props
+}: React.ComponentProps &
+ Pick, "variant" | "size">) {
+ return (
+
+ )
+}
+
+function AlertDialogCancel({
+ className,
+ variant = "outline",
+ size = "default",
+ ...props
+}: React.ComponentProps &
+ Pick, "variant" | "size">) {
+ return (
+
+ )
+}
+
+export {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogMedia,
+ AlertDialogOverlay,
+ AlertDialogPortal,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+}
diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx
index 14213546..f99164ed 100644
--- a/src/components/ui/alert.tsx
+++ b/src/components/ui/alert.tsx
@@ -4,13 +4,13 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
- "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
+ "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
- "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
+ "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current",
},
},
defaultVariants: {
@@ -55,7 +55,7 @@ function AlertDescription({
) {
+ return
+}
+
+export { AspectRatio }
diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx
new file mode 100644
index 00000000..ea658505
--- /dev/null
+++ b/src/components/ui/avatar.tsx
@@ -0,0 +1,109 @@
+"use client"
+
+import * as React from "react"
+import { Avatar as AvatarPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function Avatar({
+ className,
+ size = "default",
+ ...props
+}: React.ComponentProps
& {
+ size?: "default" | "sm" | "lg"
+}) {
+ return (
+
+ )
+}
+
+function AvatarImage({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AvatarFallback({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
+ return (
+ svg]:hidden",
+ "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
+ "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function AvatarGroupCount({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+ svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+export {
+ Avatar,
+ AvatarImage,
+ AvatarFallback,
+ AvatarBadge,
+ AvatarGroup,
+ AvatarGroupCount,
+}
diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx
index fd3a406b..6eb2a057 100644
--- a/src/components/ui/badge.tsx
+++ b/src/components/ui/badge.tsx
@@ -1,22 +1,23 @@
import * as React from "react"
-import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
+import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
- "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
+ "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
{
variants: {
variant: {
- default:
- "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
+ default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
- "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
+ "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
- "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ "bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
outline:
- "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
+ "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
+ ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 [a&]:hover:underline",
},
},
defaultVariants: {
@@ -27,16 +28,17 @@ const badgeVariants = cva(
function Badge({
className,
- variant,
+ variant = "default",
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps
& { asChild?: boolean }) {
- const Comp = asChild ? Slot : "span"
+ const Comp = asChild ? Slot.Root : "span"
return (
diff --git a/src/components/ui/breadcrumb.tsx b/src/components/ui/breadcrumb.tsx
new file mode 100644
index 00000000..004bb63a
--- /dev/null
+++ b/src/components/ui/breadcrumb.tsx
@@ -0,0 +1,109 @@
+import * as React from "react"
+import { ChevronRight, MoreHorizontal } from "lucide-react"
+import { Slot } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
+ return
+}
+
+function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
+ return (
+
+ )
+}
+
+function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
+ return (
+
+ )
+}
+
+function BreadcrumbLink({
+ asChild,
+ className,
+ ...props
+}: React.ComponentProps<"a"> & {
+ asChild?: boolean
+}) {
+ const Comp = asChild ? Slot.Root : "a"
+
+ return (
+
+ )
+}
+
+function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
+ return (
+
+ )
+}
+
+function BreadcrumbSeparator({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) {
+ return (
+ svg]:size-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+ )
+}
+
+function BreadcrumbEllipsis({
+ className,
+ ...props
+}: React.ComponentProps<"span">) {
+ return (
+
+
+ More
+
+ )
+}
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/src/components/ui/button-enhanced.tsx b/src/components/ui/button-enhanced.tsx
new file mode 100644
index 00000000..730c9725
--- /dev/null
+++ b/src/components/ui/button-enhanced.tsx
@@ -0,0 +1,66 @@
+"use client"
+
+import * as React from "react"
+import { Button as BaseButton, buttonVariants } from "@/components/ui/button"
+import { cva, type VariantProps } from "class-variance-authority"
+import { cn } from "@/lib/utils"
+
+// Extend only the NEW variants — shadcn owns the rest
+const enhancedVariants = cva("", {
+ variants: {
+ variant: {
+ pink: "bg-linear-to-tr from-pink-500 to-yellow-500 text-white shadow-lg",
+ },
+ },
+})
+
+type RippleInfo = { key: number; x: number; y: number; size: number }
+
+function RippleCircle({ info, onDone }: { info: RippleInfo; onDone: (key: number) => void }) {
+ const [active, setActive] = React.useState(false)
+ React.useEffect(() => { const id = requestAnimationFrame(() => setActive(true)); return () => cancelAnimationFrame(id) }, [])
+ return (
+ onDone(info.key)} />
+ )
+}
+
+const rippleKey = { current: 0 } // stable counter, no collision risk
+
+type ExtendedVariant = React.ComponentProps["variant"] | "pink"
+
+function Button({ className, variant, disableRipple, onPointerDown, children, ...props }:
+ Omit, "variant"> & {
+ variant?: ExtendedVariant
+ disableRipple?: boolean
+ }) {
+ const [ripples, setRipples] = React.useState([])
+ const isExtended = variant === "pink"
+
+ const handlePointerDown = (e: React.PointerEvent) => {
+ onPointerDown?.(e)
+ if (disableRipple || props.disabled || e.button === 2) return
+ const rect = e.currentTarget.getBoundingClientRect()
+ const size = Math.max(rect.width, rect.height) * 2
+ setRipples(prev => [...prev, { key: rippleKey.current++, x: e.clientX - rect.left - size / 2, y: e.clientY - rect.top - size / 2, size }])
+ }
+
+ return (
+
+
+ {ripples.map(r => setRipples(p => p.filter(r => r.key !== k))} />)}
+
+ {children}
+
+ )
+}
+
+export { Button, buttonVariants }
\ No newline at end of file
diff --git a/src/components/ui/button-group.tsx b/src/components/ui/button-group.tsx
index 8600af03..cd550d7a 100644
--- a/src/components/ui/button-group.tsx
+++ b/src/components/ui/button-group.tsx
@@ -1,11 +1,11 @@
-import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
+import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
import { Separator } from "@/components/ui/separator"
const buttonGroupVariants = cva(
- "flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
+ "flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
{
variants: {
orientation: {
@@ -44,12 +44,12 @@ function ButtonGroupText({
}: React.ComponentProps<"div"> & {
asChild?: boolean
}) {
- const Comp = asChild ? Slot : "div"
+ const Comp = asChild ? Slot.Root : "div"
return (
svg]:px-3",
- sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+ xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
+ sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
+ "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
+ "icon-sm": "size-8",
+ "icon-lg": "size-10",
},
},
defaultVariants: {
@@ -38,99 +38,25 @@ const buttonVariants = cva(
}
)
-type RippleInfo = {
- key: number
- x: number
- y: number
- size: number
-}
-
function Button({
className,
- variant,
- size,
+ variant = "default",
+ size = "default",
asChild = false,
- disabled,
- onPointerDown,
- children,
- // Optional ripple toggle
- disableRipple,
...props
}: React.ComponentProps<"button"> &
VariantProps & {
asChild?: boolean
- disableRipple?: boolean
}) {
- const Comp = asChild ? SlotPrimitive.Slot : "button"
- const [ripples, setRipples] = React.useState([])
-
- const handlePointerDown = (e: React.PointerEvent) => {
- onPointerDown?.(e)
- if (disableRipple || disabled || e.button === 2) return
-
- const target = e.currentTarget
- const rect = target.getBoundingClientRect()
- const maxDim = Math.max(rect.width, rect.height)
- const size = maxDim * 2
- const x = e.clientX - rect.left - size / 2
- const y = e.clientY - rect.top - size / 2
-
- const key = Date.now() + Math.random()
- setRipples(prev => [...prev, { key, x, y, size }])
- }
-
- const removeRipple = (key: number) => {
- setRipples(prev => prev.filter(r => r.key !== key))
- }
+ const Comp = asChild ? Slot.Root : "button"
return (
- {/* Ripple layer */}
- {!disableRipple && (
-
- {ripples.map(r => (
-
- ))}
-
- )}
- {/* Content layer */}
-
- {children}
-
-
- )
-}
-
-function RippleCircle({ info, onDone }: { info: RippleInfo; onDone: (key: number) => void }) {
- const { x, y, size, key } = info
- const [active, setActive] = React.useState(false)
- React.useEffect(() => {
- const id = requestAnimationFrame(() => setActive(true))
- return () => cancelAnimationFrame(id)
- }, [])
-
- const handleTransitionEnd = () => onDone(key)
-
- return (
-
)
}
diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx
new file mode 100644
index 00000000..47551459
--- /dev/null
+++ b/src/components/ui/calendar.tsx
@@ -0,0 +1,220 @@
+"use client"
+
+import * as React from "react"
+import {
+ ChevronDownIcon,
+ ChevronLeftIcon,
+ ChevronRightIcon,
+} from "lucide-react"
+import {
+ DayPicker,
+ getDefaultClassNames,
+ type DayButton,
+} from "react-day-picker"
+
+import { cn } from "@/lib/utils"
+import { Button, buttonVariants } from "@/components/ui/button"
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ captionLayout = "label",
+ buttonVariant = "ghost",
+ formatters,
+ components,
+ ...props
+}: React.ComponentProps & {
+ buttonVariant?: React.ComponentProps["variant"]
+}) {
+ const defaultClassNames = getDefaultClassNames()
+
+ return (
+ svg]:rotate-180`,
+ String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
+ className
+ )}
+ captionLayout={captionLayout}
+ formatters={{
+ formatMonthDropdown: (date) =>
+ date.toLocaleString("default", { month: "short" }),
+ ...formatters,
+ }}
+ classNames={{
+ root: cn("w-fit", defaultClassNames.root),
+ months: cn(
+ "relative flex flex-col gap-4 md:flex-row",
+ defaultClassNames.months
+ ),
+ month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
+ nav: cn(
+ "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
+ defaultClassNames.nav
+ ),
+ button_previous: cn(
+ buttonVariants({ variant: buttonVariant }),
+ "size-(--cell-size) p-0 select-none aria-disabled:opacity-50",
+ defaultClassNames.button_previous
+ ),
+ button_next: cn(
+ buttonVariants({ variant: buttonVariant }),
+ "size-(--cell-size) p-0 select-none aria-disabled:opacity-50",
+ defaultClassNames.button_next
+ ),
+ month_caption: cn(
+ "flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
+ defaultClassNames.month_caption
+ ),
+ dropdowns: cn(
+ "flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
+ defaultClassNames.dropdowns
+ ),
+ dropdown_root: cn(
+ "relative rounded-md border border-input shadow-xs has-focus:border-ring has-focus:ring-[3px] has-focus:ring-ring/50",
+ defaultClassNames.dropdown_root
+ ),
+ dropdown: cn(
+ "absolute inset-0 bg-popover opacity-0",
+ defaultClassNames.dropdown
+ ),
+ caption_label: cn(
+ "font-medium select-none",
+ captionLayout === "label"
+ ? "text-sm"
+ : "flex h-8 items-center gap-1 rounded-md pr-1 pl-2 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground",
+ defaultClassNames.caption_label
+ ),
+ table: "w-full border-collapse",
+ weekdays: cn("flex", defaultClassNames.weekdays),
+ weekday: cn(
+ "flex-1 rounded-md text-[0.8rem] font-normal text-muted-foreground select-none",
+ defaultClassNames.weekday
+ ),
+ week: cn("mt-2 flex w-full", defaultClassNames.week),
+ week_number_header: cn(
+ "w-(--cell-size) select-none",
+ defaultClassNames.week_number_header
+ ),
+ week_number: cn(
+ "text-[0.8rem] text-muted-foreground select-none",
+ defaultClassNames.week_number
+ ),
+ day: cn(
+ "group/day relative aspect-square h-full w-full p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-md",
+ props.showWeekNumber
+ ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
+ : "[&:first-child[data-selected=true]_button]:rounded-l-md",
+ defaultClassNames.day
+ ),
+ range_start: cn(
+ "rounded-l-md bg-accent",
+ defaultClassNames.range_start
+ ),
+ range_middle: cn("rounded-none", defaultClassNames.range_middle),
+ range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
+ today: cn(
+ "rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none",
+ defaultClassNames.today
+ ),
+ outside: cn(
+ "text-muted-foreground aria-selected:text-muted-foreground",
+ defaultClassNames.outside
+ ),
+ disabled: cn(
+ "text-muted-foreground opacity-50",
+ defaultClassNames.disabled
+ ),
+ hidden: cn("invisible", defaultClassNames.hidden),
+ ...classNames,
+ }}
+ components={{
+ Root: ({ className, rootRef, ...props }) => {
+ return (
+
+ )
+ },
+ Chevron: ({ className, orientation, ...props }) => {
+ if (orientation === "left") {
+ return (
+
+ )
+ }
+
+ if (orientation === "right") {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+ },
+ DayButton: CalendarDayButton,
+ WeekNumber: ({ children, ...props }) => {
+ return (
+
+
+ {children}
+
+ |
+ )
+ },
+ ...components,
+ }}
+ {...props}
+ />
+ )
+}
+
+function CalendarDayButton({
+ className,
+ day,
+ modifiers,
+ ...props
+}: React.ComponentProps) {
+ const defaultClassNames = getDefaultClassNames()
+
+ const ref = React.useRef(null)
+ React.useEffect(() => {
+ if (modifiers.focused) ref.current?.focus()
+ }, [modifiers.focused])
+
+ return (
+