mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
we're basically done
This commit is contained in:
@@ -29,7 +29,7 @@ export function MediaDragOverlay({
|
||||
|
||||
return (
|
||||
<button
|
||||
className="bg-foreground/5 hover:bg-foreground/10 flex size-full flex-col items-center justify-center gap-4 rounded-lg p-8 text-center transition-all duration-200"
|
||||
className="bg-foreground/5 hover:bg-foreground/10 flex size-full flex-col items-center justify-center gap-4 rounded-lg p-8 text-center"
|
||||
type="button"
|
||||
disabled={isProcessing || !onClick}
|
||||
onClick={(event) => handleClick({ event })}
|
||||
@@ -50,7 +50,7 @@ export function MediaDragOverlay({
|
||||
<div className="w-full max-w-xs">
|
||||
<div className="bg-muted/50 h-2 w-full rounded-full">
|
||||
<div
|
||||
className="bg-primary h-2 rounded-full transition-all duration-300"
|
||||
className="bg-primary h-2 rounded-full"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -106,7 +106,7 @@ function FadeOverlay({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute right-0 left-0 h-6 transition-opacity duration-200",
|
||||
"pointer-events-none absolute right-0 left-0 h-6",
|
||||
direction === "top" && show
|
||||
? "from-panel top-0 bg-gradient-to-b to-transparent"
|
||||
: "from-panel bottom-0 bg-gradient-to-t to-transparent",
|
||||
|
||||
@@ -196,144 +196,132 @@ export function MediaView() {
|
||||
<input {...fileInputProps} />
|
||||
|
||||
<div
|
||||
className={`relative flex h-full flex-col gap-1 transition-colors ${isDragOver ? "bg-accent/30" : ""}`}
|
||||
className={`relative flex h-full flex-col gap-1 ${isDragOver ? "bg-accent/30" : ""}`}
|
||||
{...dragProps}
|
||||
>
|
||||
<div className="bg-panel p-3 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="foreground"
|
||||
onClick={openFilePicker}
|
||||
disabled={isProcessing}
|
||||
className="w-full"
|
||||
>
|
||||
{isProcessing ? (
|
||||
<HugeiconsIcon icon={Loading03Icon} className="animate-spin" />
|
||||
) : (
|
||||
<HugeiconsIcon icon={CloudUploadIcon} />
|
||||
)}
|
||||
<span>Upload</span>
|
||||
</Button>
|
||||
<div className="flex items-center gap-0">
|
||||
<TooltipProvider>
|
||||
<div className="bg-panel py-2 px-4 flex items-center justify-between border-b">
|
||||
<span className="text-muted-foreground text-sm">Assets</span>
|
||||
<div className="flex items-center gap-0">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="text"
|
||||
onClick={() =>
|
||||
setMediaViewMode(
|
||||
mediaViewMode === "grid" ? "list" : "grid",
|
||||
)
|
||||
}
|
||||
disabled={isProcessing}
|
||||
className="items-center justify-center"
|
||||
>
|
||||
{mediaViewMode === "grid" ? (
|
||||
<HugeiconsIcon icon={LeftToRightListDashIcon} />
|
||||
) : (
|
||||
<HugeiconsIcon icon={GridViewIcon} />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
{mediaViewMode === "grid"
|
||||
? "Switch to list view"
|
||||
: "Switch to grid view"}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="text"
|
||||
onClick={() =>
|
||||
setMediaViewMode(
|
||||
mediaViewMode === "grid" ? "list" : "grid",
|
||||
)
|
||||
}
|
||||
disabled={isProcessing}
|
||||
className="items-center justify-center"
|
||||
>
|
||||
{mediaViewMode === "grid" ? (
|
||||
<HugeiconsIcon icon={LeftToRightListDashIcon} />
|
||||
) : (
|
||||
<HugeiconsIcon icon={GridViewIcon} />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<DropdownMenu>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="text"
|
||||
disabled={isProcessing}
|
||||
className="items-center justify-center"
|
||||
>
|
||||
<HugeiconsIcon icon={SortingOneNineIcon} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<SortMenuItem
|
||||
label="Name"
|
||||
sortKey="name"
|
||||
currentSortBy={sortBy}
|
||||
currentSortOrder={sortOrder}
|
||||
onSort={({ key }) => {
|
||||
if (sortBy === key) {
|
||||
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
||||
} else {
|
||||
setSortBy(key);
|
||||
setSortOrder("asc");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<SortMenuItem
|
||||
label="Type"
|
||||
sortKey="type"
|
||||
currentSortBy={sortBy}
|
||||
currentSortOrder={sortOrder}
|
||||
onSort={({ key }) => {
|
||||
if (sortBy === key) {
|
||||
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
||||
} else {
|
||||
setSortBy(key);
|
||||
setSortOrder("asc");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<SortMenuItem
|
||||
label="Duration"
|
||||
sortKey="duration"
|
||||
currentSortBy={sortBy}
|
||||
currentSortOrder={sortOrder}
|
||||
onSort={({ key }) => {
|
||||
if (sortBy === key) {
|
||||
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
||||
} else {
|
||||
setSortBy(key);
|
||||
setSortOrder("asc");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<SortMenuItem
|
||||
label="File size"
|
||||
sortKey="size"
|
||||
currentSortBy={sortBy}
|
||||
currentSortOrder={sortOrder}
|
||||
onSort={({ key }) => {
|
||||
if (sortBy === key) {
|
||||
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
||||
} else {
|
||||
setSortBy(key);
|
||||
setSortOrder("asc");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
{mediaViewMode === "grid"
|
||||
? "Switch to list view"
|
||||
: "Switch to grid view"}
|
||||
Sort by {sortBy} (
|
||||
{sortOrder === "asc" ? "ascending" : "descending"})
|
||||
</p>
|
||||
</TooltipContent>
|
||||
<Tooltip>
|
||||
<DropdownMenu>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="text"
|
||||
disabled={isProcessing}
|
||||
className="items-center justify-center"
|
||||
>
|
||||
<HugeiconsIcon icon={SortingOneNineIcon} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<SortMenuItem
|
||||
label="Name"
|
||||
sortKey="name"
|
||||
currentSortBy={sortBy}
|
||||
currentSortOrder={sortOrder}
|
||||
onSort={({ key }) => {
|
||||
if (sortBy === key) {
|
||||
setSortOrder(
|
||||
sortOrder === "asc" ? "desc" : "asc",
|
||||
);
|
||||
} else {
|
||||
setSortBy(key);
|
||||
setSortOrder("asc");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<SortMenuItem
|
||||
label="Type"
|
||||
sortKey="type"
|
||||
currentSortBy={sortBy}
|
||||
currentSortOrder={sortOrder}
|
||||
onSort={({ key }) => {
|
||||
if (sortBy === key) {
|
||||
setSortOrder(
|
||||
sortOrder === "asc" ? "desc" : "asc",
|
||||
);
|
||||
} else {
|
||||
setSortBy(key);
|
||||
setSortOrder("asc");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<SortMenuItem
|
||||
label="Duration"
|
||||
sortKey="duration"
|
||||
currentSortBy={sortBy}
|
||||
currentSortOrder={sortOrder}
|
||||
onSort={({ key }) => {
|
||||
if (sortBy === key) {
|
||||
setSortOrder(
|
||||
sortOrder === "asc" ? "desc" : "asc",
|
||||
);
|
||||
} else {
|
||||
setSortBy(key);
|
||||
setSortOrder("asc");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<SortMenuItem
|
||||
label="File size"
|
||||
sortKey="size"
|
||||
currentSortBy={sortBy}
|
||||
currentSortOrder={sortOrder}
|
||||
onSort={({ key }) => {
|
||||
if (sortBy === key) {
|
||||
setSortOrder(
|
||||
sortOrder === "asc" ? "desc" : "asc",
|
||||
);
|
||||
} else {
|
||||
setSortBy(key);
|
||||
setSortOrder("asc");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Sort by {sortBy} (
|
||||
{sortOrder === "asc" ? "ascending" : "descending"})
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={openFilePicker}
|
||||
disabled={isProcessing}
|
||||
size="sm"
|
||||
className="items-center justify-center gap-1.5 ml-1.5"
|
||||
>
|
||||
<HugeiconsIcon icon={CloudUploadIcon} />
|
||||
Upload
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -519,7 +519,7 @@ function AudioItem({ sound, isPlaying, onPlay }: AudioItemProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="group flex items-center gap-3 opacity-100 transition-opacity hover:opacity-75">
|
||||
<div className="group flex items-center gap-3 opacity-100 hover:opacity-75">
|
||||
<button
|
||||
type="button"
|
||||
className="flex min-w-0 flex-1 items-center gap-3 text-left"
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
|
||||
import { Label } from "../ui/label";
|
||||
import { Input } from "../ui/input";
|
||||
|
||||
export function DeleteProjectDialog({
|
||||
isOpen,
|
||||
@@ -45,25 +49,34 @@ export function DeleteProjectDialog({
|
||||
`Delete ${count} projects?`
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isSingle
|
||||
? "Are you sure you want to delete this project? This action cannot be undone."
|
||||
: `Are you sure you want to delete these ${count} projects? This action cannot be undone.`}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogBody>
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>Warning</AlertTitle>
|
||||
<AlertDescription>
|
||||
This will permanently delete{" "}
|
||||
{singleName ? `"${singleName}"` : `${count} projects`} and all
|
||||
associated files.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label className="text-xs font-semibold text-slate-500">
|
||||
Type "DELETE" to confirm
|
||||
</Label>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="DELETE"
|
||||
size="lg"
|
||||
variant="destructive"
|
||||
/>
|
||||
</div>
|
||||
</DialogBody>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onOpenChange(false);
|
||||
}}
|
||||
>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={onConfirm}>
|
||||
Delete
|
||||
Delete project
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -163,7 +163,7 @@ export function DraggableItem({
|
||||
<div className="size-6 flex-shrink-0 overflow-hidden rounded-[0.35rem]">
|
||||
{preview}
|
||||
</div>
|
||||
<span className="w-full flex-1 truncate text-sm">{name}</span>
|
||||
<span className="w-full flex-1 truncate text-sm text-left">{name}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -142,7 +142,7 @@ export function EditableTimecode({
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"text-primary cursor-pointer font-mono text-xs tabular-nums",
|
||||
"hover:bg-muted/50 -mx-1 px-1 transition-colors hover:rounded",
|
||||
"hover:bg-muted/50 -mx-1 px-1 hover:rounded",
|
||||
disabled && "cursor-default hover:bg-transparent",
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -39,9 +39,9 @@ export function ExportButton() {
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 rounded-md bg-[#38BDF8] px-[0.12rem] py-[0.12rem] text-white transition-all duration-200",
|
||||
"flex items-center gap-1.5 rounded-md bg-[#38BDF8] px-[0.12rem] py-[0.12rem] text-white",
|
||||
hasProject
|
||||
? "cursor-pointer hover:brightness-95"
|
||||
? "cursor-pointer hover:brightness-105"
|
||||
: "cursor-not-allowed opacity-50",
|
||||
)}
|
||||
onClick={hasProject ? handleExport : undefined}
|
||||
@@ -53,11 +53,11 @@ export function ExportButton() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="relative flex items-center gap-1.5 rounded-[0.8rem] bg-linear-270 from-[#2567EC] to-[#37B6F7] px-4 py-1 shadow-[0_1px_3px_0px_rgba(0,0,0,0.65)]">
|
||||
<div className="relative flex items-center gap-1.5 rounded-[0.6rem] bg-linear-270 from-[#2567EC] to-[#37B6F7] px-4 py-1 shadow-[0_1px_3px_0px_rgba(0,0,0,0.65)]">
|
||||
<HugeiconsIcon icon={TransitionTopIcon} className="z-50 size-4" />
|
||||
<span className="z-50 text-[0.875rem]">Export</span>
|
||||
<div className="absolute top-0 left-0 z-10 flex size-full items-center justify-center rounded-[0.8rem] bg-linear-to-t from-white/0 to-white/50">
|
||||
<div className="absolute top-[0.08rem] z-50 h-[calc(100%-2px)] w-[calc(100%-2px)] rounded-[0.8rem] bg-linear-270 from-[#2567EC] to-[#37B6F7]"></div>
|
||||
<div className="absolute top-0 left-0 z-10 flex size-full items-center justify-center rounded-[0.6rem] bg-linear-to-t from-white/0 to-white/50">
|
||||
<div className="absolute top-[0.08rem] z-50 h-[calc(100%-2px)] w-[calc(100%-2px)] rounded-[0.6rem] bg-linear-270 from-[#2567EC] to-[#37B6F7]"></div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import type { TProjectMetadata } from "@/types/project";
|
||||
import { formatDate } from "@/utils/date";
|
||||
import { formatTimeCode } from "@/lib/time";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
function InfoRow({
|
||||
label,
|
||||
@@ -16,7 +19,7 @@ function InfoRow({
|
||||
value: string | React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex justify-between items-center py-2 last:pb-0 border-b border-border/50 last:border-b-0">
|
||||
<div className="flex justify-between items-center py-0 last:pb-0">
|
||||
<span className="text-muted-foreground text-sm">{label}</span>
|
||||
<span className="text-sm font-medium">{value}</span>
|
||||
</div>
|
||||
@@ -42,14 +45,14 @@ export function ProjectInfoDialog({
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogContent onOpenAutoFocus={(event) => event.preventDefault()}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="truncate max-w-[350px]">
|
||||
{project.name}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<DialogBody className="flex flex-col">
|
||||
<InfoRow label="Duration" value={durationFormatted} />
|
||||
<InfoRow
|
||||
label="Created"
|
||||
@@ -67,7 +70,13 @@ export function ProjectInfoDialog({
|
||||
</code>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</DialogBody>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Close
|
||||
</Button>
|
||||
<Button onClick={() => onOpenChange(false)}>Done</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -74,10 +74,10 @@ export function PropertyGroup({
|
||||
<PropertyItem direction="column" className={cn("gap-3", className)}>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 cursor-pointer"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
<PropertyItemLabel className={cn("cursor-pointer", titleClassName)}>
|
||||
<PropertyItemLabel className={cn(titleClassName)}>
|
||||
{title}
|
||||
</PropertyItemLabel>
|
||||
<HugeiconsIcon
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
} from "./property-item";
|
||||
import { ColorPicker } from "@/components/ui/color-picker";
|
||||
import { cn } from "@/utils/ui";
|
||||
import { capitalizeFirstLetter, uppercase } from "@/utils/strings";
|
||||
import { capitalizeFirstLetter, uppercase } from "@/utils/string";
|
||||
import { clamp } from "@/utils/math";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { LayoutGridIcon } from "@hugeicons/core-free-icons";
|
||||
@@ -387,7 +387,8 @@ export function TextProperties({
|
||||
}
|
||||
className="bg-panel-accent size-9 overflow-hidden rounded-full p-0"
|
||||
>
|
||||
<HugeiconsIcon icon={LayoutGridIcon}
|
||||
<HugeiconsIcon
|
||||
icon={LayoutGridIcon}
|
||||
className={cn(
|
||||
"text-foreground",
|
||||
element.backgroundColor === "transparent" &&
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { Keyboard } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
@@ -11,8 +10,8 @@ import { useKeybindingsStore } from "@/stores/keybindings-store";
|
||||
import { Button } from "../ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
@@ -107,19 +106,12 @@ export function ShortcutsDialog({
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="flex max-h-[80vh] max-w-2xl flex-col p-0">
|
||||
<DialogHeader className="flex-shrink-0 gap-2 p-6 pb-0">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Keyboard className="size-5" />
|
||||
Keyboard shortcuts
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Speed up your video editing workflow with these keyboard shortcuts.
|
||||
Click any shortcut key to edit it.
|
||||
</DialogDescription>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Keyboard shortcuts</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="scrollbar-thin flex-grow overflow-y-auto">
|
||||
<div className="flex flex-col gap-6 p-6 pt-2">
|
||||
<DialogBody className="scrollbar-thin flex-grow overflow-y-auto">
|
||||
<div className="flex flex-col gap-6">
|
||||
{categories.map((category) => (
|
||||
<div key={category} className="flex flex-col gap-1">
|
||||
<h3 className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
|
||||
@@ -142,9 +134,9 @@ export function ShortcutsDialog({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="p-4 pt-0">
|
||||
<Button size="sm" variant="destructive" onClick={resetToDefaults}>
|
||||
</DialogBody>
|
||||
<DialogFooter>
|
||||
<Button variant="destructive" onClick={resetToDefaults}>
|
||||
Reset to default
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -166,7 +166,7 @@ export function AudioWaveform({
|
||||
)}
|
||||
<div
|
||||
ref={waveformRef}
|
||||
className={`w-full transition-opacity duration-200 ${isLoading ? "opacity-0" : "opacity-100"}`}
|
||||
className={`w-full ${isLoading ? "opacity-0" : "opacity-100"}`}
|
||||
style={{ height }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
Delete02Icon,
|
||||
TaskAdd02Icon,
|
||||
ViewIcon,
|
||||
ViewOffSlashIcon,
|
||||
VolumeHighIcon,
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
canTracktHaveAudio,
|
||||
canTrackBeHidden,
|
||||
getTimelineZoomMin,
|
||||
isMainTrack,
|
||||
} from "@/lib/timeline";
|
||||
import { TimelineToolbar } from "./timeline-toolbar";
|
||||
import { useScrollSync } from "@/hooks/timeline/use-scroll-sync";
|
||||
@@ -48,6 +50,7 @@ import { useTimelineStore } from "@/stores/timeline-store";
|
||||
import { useEditor } from "@/hooks/use-editor";
|
||||
import { useTimelinePlayhead } from "@/hooks/timeline/use-timeline-playhead";
|
||||
import { DragLine } from "./drag-line";
|
||||
import { invokeAction } from "@/lib/actions";
|
||||
|
||||
export function Timeline() {
|
||||
const tracksContainerHeight = { min: 0, max: 800 };
|
||||
@@ -181,7 +184,7 @@ export function Timeline() {
|
||||
return (
|
||||
<section
|
||||
className={
|
||||
"bg-panel relative flex h-full flex-col overflow-hidden rounded-sm transition-colors duration-200"
|
||||
"bg-panel relative flex h-full flex-col overflow-hidden rounded-sm"
|
||||
}
|
||||
{...dragProps}
|
||||
aria-label="Timeline"
|
||||
@@ -267,6 +270,10 @@ export function Timeline() {
|
||||
}}
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center justify-end gap-2">
|
||||
{process.env.NODE_ENV === "development" &&
|
||||
isMainTrack(track) && (
|
||||
<div className="bg-red-500 size-1.5 rounded-full" />
|
||||
)}
|
||||
{canTracktHaveAudio(track) && (
|
||||
<TrackToggleIcon
|
||||
isOff={track.muted}
|
||||
@@ -281,7 +288,6 @@ export function Timeline() {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{canTrackBeHidden(track) && (
|
||||
<TrackToggleIcon
|
||||
isOff={track.hidden}
|
||||
@@ -296,7 +302,6 @@ export function Timeline() {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TrackIcon track={track} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -397,6 +402,15 @@ export function Timeline() {
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent className="z-200 w-40">
|
||||
<ContextMenuItem
|
||||
icon={<HugeiconsIcon icon={TaskAdd02Icon} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
invokeAction("paste-copied");
|
||||
}}
|
||||
>
|
||||
Paste elements
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Scissors,
|
||||
Trash2,
|
||||
Copy,
|
||||
Search,
|
||||
RefreshCw,
|
||||
EyeOff,
|
||||
Eye,
|
||||
Volume2,
|
||||
VolumeX,
|
||||
ArrowUpDown,
|
||||
} from "lucide-react";
|
||||
import { useEditor } from "@/hooks/use-editor";
|
||||
import { useAssetsPanelStore } from "@/stores/assets-panel-store";
|
||||
import AudioWaveform from "./audio-waveform";
|
||||
@@ -39,9 +27,35 @@ import type {
|
||||
} from "@/types/timeline";
|
||||
import type { MediaAsset } from "@/types/assets";
|
||||
import { mediaSupportsAudio } from "@/lib/media/media-utils";
|
||||
import { type TAction, invokeAction } from "@/lib/actions";
|
||||
import { getActionDefinition, type TAction, invokeAction } from "@/lib/actions";
|
||||
import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
ScissorIcon,
|
||||
Delete02Icon,
|
||||
Copy01Icon,
|
||||
ViewIcon,
|
||||
ViewOffSlashIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeOffIcon,
|
||||
VolumeMute02Icon,
|
||||
Search01Icon,
|
||||
Exchange01Icon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { uppercase } from "@/utils/string";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
function getDisplayShortcut(action: TAction) {
|
||||
const { defaultShortcuts } = getActionDefinition(action);
|
||||
if (!defaultShortcuts?.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return uppercase({
|
||||
string: defaultShortcuts[0].replace("+", " "),
|
||||
});
|
||||
}
|
||||
|
||||
interface TimelineElementProps {
|
||||
element: TimelineElementType;
|
||||
@@ -112,17 +126,6 @@ export function TimelineElement({
|
||||
displayedDuration * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
|
||||
const elementLeft = displayedStartTime * 50 * zoomLevel;
|
||||
|
||||
const handleAction = ({
|
||||
action,
|
||||
event,
|
||||
}: {
|
||||
action: TAction;
|
||||
event: React.MouseEvent;
|
||||
}) => {
|
||||
event.stopPropagation();
|
||||
invokeAction(action);
|
||||
};
|
||||
|
||||
const handleRevealInMedia = ({ event }: { event: React.MouseEvent }) => {
|
||||
event.stopPropagation();
|
||||
if (hasMediaId(element)) {
|
||||
@@ -162,31 +165,16 @@ export function TimelineElement({
|
||||
/>
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent className="z-200">
|
||||
<ContextMenuItem
|
||||
onClick={(event) => handleAction({ action: "split-selected", event })}
|
||||
>
|
||||
<Scissors className="mr-2 size-4" />
|
||||
{selectedElements.length > 1 && isCurrentElementSelected
|
||||
? `Split ${selectedElements.length} elements at playhead`
|
||||
: "Split at playhead"}
|
||||
</ContextMenuItem>
|
||||
<CopyMenuItem
|
||||
isMultipleSelected={selectedElements.length > 1}
|
||||
isCurrentElementSelected={isCurrentElementSelected}
|
||||
selectedCount={selectedElements.length}
|
||||
onClick={(event) => handleAction({ action: "copy-selected", event })}
|
||||
/>
|
||||
<ContextMenuContent className="z-200 w-64">
|
||||
<ActionMenuItem action="split" icon={<HugeiconsIcon icon={ScissorIcon} />}>
|
||||
Split
|
||||
</ActionMenuItem>
|
||||
<CopyMenuItem />
|
||||
{canElementHaveAudio(element) && hasAudio && (
|
||||
<MuteMenuItem
|
||||
element={element}
|
||||
isMultipleSelected={selectedElements.length > 1}
|
||||
isCurrentElementSelected={isCurrentElementSelected}
|
||||
isMuted={isMuted}
|
||||
selectedCount={selectedElements.length}
|
||||
onClick={(event) =>
|
||||
handleAction({ action: "toggle-elements-muted-selected", event })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{canElementBeHidden(element) && (
|
||||
@@ -194,36 +182,26 @@ export function TimelineElement({
|
||||
element={element}
|
||||
isMultipleSelected={selectedElements.length > 1}
|
||||
isCurrentElementSelected={isCurrentElementSelected}
|
||||
selectedCount={selectedElements.length}
|
||||
onClick={(event) =>
|
||||
handleAction({
|
||||
action: "toggle-elements-visibility-selected",
|
||||
event,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{selectedElements.length === 1 && (
|
||||
<ContextMenuItem
|
||||
onClick={(event) =>
|
||||
handleAction({ action: "duplicate-selected", event })
|
||||
}
|
||||
>
|
||||
<Copy className="mr-2 size-4" />
|
||||
Duplicate {element.type === "text" ? "text" : "clip"}
|
||||
</ContextMenuItem>
|
||||
<ActionMenuItem action="duplicate-selected" icon={<HugeiconsIcon icon={Copy01Icon} />}>
|
||||
Duplicate
|
||||
</ActionMenuItem>
|
||||
)}
|
||||
{selectedElements.length === 1 && hasMediaId(element) && (
|
||||
<>
|
||||
<ContextMenuItem
|
||||
icon={<HugeiconsIcon icon={Search01Icon} />}
|
||||
onClick={(event) => handleRevealInMedia({ event })}
|
||||
>
|
||||
<Search className="mr-2 size-4" />
|
||||
Reveal in media
|
||||
Reveal media
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem disabled>
|
||||
<RefreshCw className="mr-2 size-4" />
|
||||
Replace clip (Coming soon)
|
||||
<ContextMenuItem
|
||||
icon={<HugeiconsIcon icon={Exchange01Icon} />}
|
||||
disabled
|
||||
>
|
||||
Replace media
|
||||
</ContextMenuItem>
|
||||
</>
|
||||
)}
|
||||
@@ -233,9 +211,6 @@ export function TimelineElement({
|
||||
isCurrentElementSelected={isCurrentElementSelected}
|
||||
elementType={element.type}
|
||||
selectedCount={selectedElements.length}
|
||||
onClick={(event) =>
|
||||
handleAction({ action: "delete-selected", event })
|
||||
}
|
||||
/>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
@@ -300,9 +275,15 @@ function ElementInner({
|
||||
: canElementBeHidden(element) && element.hidden) && (
|
||||
<div className="bg-opacity-50 pointer-events-none absolute inset-0 flex items-center justify-center bg-black">
|
||||
{hasAudio ? (
|
||||
<VolumeX className="size-6 text-white" />
|
||||
<HugeiconsIcon
|
||||
icon={VolumeHighIcon}
|
||||
className="size-6 text-white"
|
||||
/>
|
||||
) : (
|
||||
<EyeOff className="size-6 text-white" />
|
||||
<HugeiconsIcon
|
||||
icon={VolumeOffIcon}
|
||||
className="size-6 text-white"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -463,66 +444,38 @@ function ElementContent({
|
||||
);
|
||||
}
|
||||
|
||||
function CopyMenuItem({
|
||||
isMultipleSelected,
|
||||
isCurrentElementSelected,
|
||||
selectedCount,
|
||||
onClick,
|
||||
}: {
|
||||
isMultipleSelected: boolean;
|
||||
isCurrentElementSelected: boolean;
|
||||
selectedCount: number;
|
||||
onClick: (e: React.MouseEvent) => void;
|
||||
}) {
|
||||
function CopyMenuItem() {
|
||||
return (
|
||||
<ContextMenuItem onClick={onClick}>
|
||||
<Copy className="mr-2 size-4" />
|
||||
{isMultipleSelected && isCurrentElementSelected
|
||||
? `Copy ${selectedCount} elements`
|
||||
: "Copy element"}
|
||||
</ContextMenuItem>
|
||||
<ActionMenuItem action="copy-selected" icon={<HugeiconsIcon icon={Copy01Icon} />}>
|
||||
Copy
|
||||
</ActionMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
function MuteMenuItem({
|
||||
element,
|
||||
isMultipleSelected,
|
||||
isCurrentElementSelected,
|
||||
isMuted,
|
||||
selectedCount,
|
||||
onClick,
|
||||
}: {
|
||||
element: TimelineElementType;
|
||||
isMultipleSelected: boolean;
|
||||
isCurrentElementSelected: boolean;
|
||||
isMuted: boolean;
|
||||
selectedCount: number;
|
||||
onClick: (e: React.MouseEvent) => void;
|
||||
}) {
|
||||
const getIcon = () => {
|
||||
if (isMultipleSelected && isCurrentElementSelected) {
|
||||
return <VolumeX className="mr-2 size-4" />;
|
||||
return <HugeiconsIcon icon={VolumeMute02Icon} />;
|
||||
}
|
||||
return isMuted ? (
|
||||
<Volume2 className="mr-2 size-4" />
|
||||
<HugeiconsIcon icon={VolumeHighIcon} />
|
||||
) : (
|
||||
<VolumeX className="mr-2 size-4" />
|
||||
<HugeiconsIcon icon={VolumeOffIcon} />
|
||||
);
|
||||
};
|
||||
|
||||
const getLabel = () => {
|
||||
if (isMultipleSelected && isCurrentElementSelected) {
|
||||
return `Toggle mute ${selectedCount} elements`;
|
||||
}
|
||||
const suffix = element.type === "text" ? "text" : "clip";
|
||||
return isMuted ? `Unmute ${suffix}` : `Mute ${suffix}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<ContextMenuItem onClick={onClick}>
|
||||
{getIcon()}
|
||||
<span>{getLabel()}</span>
|
||||
</ContextMenuItem>
|
||||
<ActionMenuItem action="toggle-elements-muted-selected" icon={getIcon()}>
|
||||
{isMuted ? "Unmute" : "Mute"}
|
||||
</ActionMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -530,41 +483,28 @@ function VisibilityMenuItem({
|
||||
element,
|
||||
isMultipleSelected,
|
||||
isCurrentElementSelected,
|
||||
selectedCount,
|
||||
onClick,
|
||||
}: {
|
||||
element: TimelineElementType;
|
||||
isMultipleSelected: boolean;
|
||||
isCurrentElementSelected: boolean;
|
||||
selectedCount: number;
|
||||
onClick: (e: React.MouseEvent) => void;
|
||||
}) {
|
||||
const isHidden = canElementBeHidden(element) && element.hidden;
|
||||
|
||||
const getIcon = () => {
|
||||
if (isMultipleSelected && isCurrentElementSelected) {
|
||||
return <EyeOff className="mr-2 size-4" />;
|
||||
return <HugeiconsIcon icon={ViewOffSlashIcon} />;
|
||||
}
|
||||
return isHidden ? (
|
||||
<Eye className="mr-2 size-4" />
|
||||
<HugeiconsIcon icon={ViewIcon} />
|
||||
) : (
|
||||
<EyeOff className="mr-2 size-4" />
|
||||
<HugeiconsIcon icon={ViewOffSlashIcon} />
|
||||
);
|
||||
};
|
||||
|
||||
const getLabel = () => {
|
||||
if (isMultipleSelected && isCurrentElementSelected) {
|
||||
return `Toggle visibility ${selectedCount} elements`;
|
||||
}
|
||||
const suffix = element.type === "text" ? "text" : "clip";
|
||||
return isHidden ? `Show ${suffix}` : `Hide ${suffix}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<ContextMenuItem onClick={onClick}>
|
||||
{getIcon()}
|
||||
<span>{getLabel()}</span>
|
||||
</ContextMenuItem>
|
||||
<ActionMenuItem action="toggle-elements-visibility-selected" icon={getIcon()}>
|
||||
{isHidden ? "Show" : "Hide"}
|
||||
</ActionMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -573,23 +513,42 @@ function DeleteMenuItem({
|
||||
isCurrentElementSelected,
|
||||
elementType,
|
||||
selectedCount,
|
||||
onClick,
|
||||
}: {
|
||||
isMultipleSelected: boolean;
|
||||
isCurrentElementSelected: boolean;
|
||||
elementType: TimelineElementType["type"];
|
||||
selectedCount: number;
|
||||
onClick: (e: React.MouseEvent) => void;
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuItem
|
||||
onClick={onClick}
|
||||
className="text-destructive focus:text-destructive"
|
||||
<ActionMenuItem
|
||||
action="delete-selected"
|
||||
variant="destructive"
|
||||
icon={<HugeiconsIcon icon={Delete02Icon} />}
|
||||
>
|
||||
<Trash2 className="mr-2 size-4" />
|
||||
{isMultipleSelected && isCurrentElementSelected
|
||||
? `Delete ${selectedCount} elements`
|
||||
: `Delete ${elementType === "text" ? "text" : "clip"}`}
|
||||
</ActionMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
function ActionMenuItem({
|
||||
action,
|
||||
children,
|
||||
...props
|
||||
}: Omit<ComponentProps<typeof ContextMenuItem>, "onClick" | "textRight"> & {
|
||||
action: TAction;
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuItem
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
invokeAction(action);
|
||||
}}
|
||||
textRight={getDisplayShortcut(action)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ContextMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,11 +6,7 @@ import {
|
||||
TooltipContent,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
SkipBack,
|
||||
SplitSquareHorizontal,
|
||||
LayersIcon,
|
||||
} from "lucide-react";
|
||||
import { SkipBack, SplitSquareHorizontal } from "lucide-react";
|
||||
import {
|
||||
SplitButton,
|
||||
SplitButtonLeft,
|
||||
@@ -38,10 +34,6 @@ import {
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
Copy01Icon,
|
||||
ArrowLeft03Icon,
|
||||
ArrowRight03Icon,
|
||||
LayoutAlignLeftIcon,
|
||||
LayoutAlignRightIcon,
|
||||
AlignLeftIcon,
|
||||
AlignRightIcon,
|
||||
Layers01Icon,
|
||||
@@ -136,7 +128,7 @@ function ToolbarLeftSection() {
|
||||
icon={<HugeiconsIcon icon={ScissorIcon} />}
|
||||
tooltip="Split element"
|
||||
onClick={({ event }) =>
|
||||
handleAction({ action: "split-selected", event })
|
||||
handleAction({ action: "split", event })
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -144,7 +136,7 @@ function ToolbarLeftSection() {
|
||||
icon={<HugeiconsIcon icon={AlignLeftIcon} />}
|
||||
tooltip="Split left"
|
||||
onClick={({ event }) =>
|
||||
handleAction({ action: "split-selected-right", event })
|
||||
handleAction({ action: "split-left", event })
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -152,7 +144,7 @@ function ToolbarLeftSection() {
|
||||
icon={<HugeiconsIcon icon={AlignRightIcon} />}
|
||||
tooltip="Split right"
|
||||
onClick={({ event }) =>
|
||||
handleAction({ action: "split-selected-left", event })
|
||||
handleAction({ action: "split-right", event })
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { RiDiscordFill, RiTwitterXLine } from "react-icons/ri";
|
||||
import { FaGithub } from "react-icons/fa6";
|
||||
import Image from "next/image";
|
||||
import { DEFAULT_LOGO_URL, SOCIAL_LINKS } from "@/constants/site-constants";
|
||||
import { capitalizeFirstLetter } from "@/utils/strings";
|
||||
import { capitalizeFirstLetter } from "@/utils/string";
|
||||
|
||||
type Category = "resources" | "company";
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ export function Header() {
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/projects">
|
||||
<Button className="text-sm">
|
||||
<Button variant="foreground" className="text-sm">
|
||||
Projects
|
||||
<ArrowRight className="size-4" />
|
||||
</Button>
|
||||
|
||||
@@ -30,9 +30,10 @@ export function Hero() {
|
||||
<div className="mt-8 flex justify-center gap-8">
|
||||
<Link href="/projects">
|
||||
<Button
|
||||
variant="foreground"
|
||||
type="submit"
|
||||
size="lg"
|
||||
className="bg-foreground h-11 px-6 text-base"
|
||||
className="h-11 text-base"
|
||||
>
|
||||
Try early beta
|
||||
<ArrowRight className="ml-0.5" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useState } from "react";
|
||||
import { Label } from "./ui/label";
|
||||
|
||||
export function RenameProjectDialog({
|
||||
isOpen,
|
||||
@@ -34,24 +36,23 @@ export function RenameProjectDialog({
|
||||
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Rename Project</DialogTitle>
|
||||
<DialogDescription>
|
||||
Enter a new name for your project.
|
||||
</DialogDescription>
|
||||
<DialogTitle>Rename project</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
onConfirm(name);
|
||||
}
|
||||
}}
|
||||
placeholder="Enter a new name"
|
||||
className="bg-background border-border mt-0 border-2"
|
||||
/>
|
||||
<DialogBody className="gap-3">
|
||||
<Label>New name</Label>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
onConfirm(name);
|
||||
}
|
||||
}}
|
||||
placeholder="Enter a new name"
|
||||
/>
|
||||
</DialogBody>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
|
||||
@@ -28,13 +28,13 @@ const AccordionTrigger = React.forwardRef<
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 cursor-pointer items-center justify-between py-4 text-left text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||
"flex flex-1 cursor-pointer items-center justify-between py-4 text-left text-sm font-medium hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="text-muted-foreground size-4 shrink-0 transition-transform duration-200" />
|
||||
<ChevronDown className="text-muted-foreground size-4 shrink-0" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
));
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { cn } from "@/utils/ui";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { Alert02Icon } from "@hugeicons/core-free-icons";
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
|
||||
"relative w-full rounded-lg border p-5 py-4.5 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-destructive [&>svg~*]:pl-7",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive:
|
||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
"bg-destructive/5 border-destructive/10 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -22,13 +24,18 @@ const alertVariants = cva(
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
>(({ className, variant, ...props }, ref) => (
|
||||
>(({ className, variant, children, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
{variant === "destructive" && (
|
||||
<HugeiconsIcon icon={Alert02Icon} className="size-5 text-destructive mt-0.5" />
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
));
|
||||
Alert.displayName = "Alert";
|
||||
|
||||
@@ -38,7 +45,7 @@ const AlertTitle = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn("mb-1 leading-none font-medium tracking-tight", className)}
|
||||
className={cn("mb-2 text-base leading-none font-semibold tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/utils/ui";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
@@ -50,7 +50,7 @@ const BreadcrumbLink = React.forwardRef<
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
className={cn("hover:text-foreground transition-colors", className)}
|
||||
className={cn("hover:text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -5,34 +5,32 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/utils/ui";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center cursor-pointer justify-center gap-2 whitespace-nowrap rounded-full text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
"inline-flex items-center cursor-pointer justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-foreground text-background shadow-sm hover:bg-foreground/90",
|
||||
"bg-primary text-primary-foreground hover:bg-primary-hover",
|
||||
foreground:
|
||||
"bg-background text-foreground",
|
||||
primary:
|
||||
"bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
"bg-foreground text-background hover:bg-foreground/90",
|
||||
"primary-gradient":
|
||||
"bg-gradient-to-r from-cyan-400 to-blue-500 text-white hover:opacity-85 transition-opacity",
|
||||
"bg-gradient-to-r from-cyan-400 to-blue-500 text-white hover:opacity-85",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
"destructive-foreground":
|
||||
"border bg-background hover:bg-destructive/15 text-destructive",
|
||||
outline:
|
||||
"border border-border bg-transparent transition-colors hover:bg-accent",
|
||||
"border border-border bg-transparent hover:bg-accent/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-foreground/15 border border-input",
|
||||
text: "bg-transparent rounded-none !opacity-100",
|
||||
text: "bg-transparent rounded-none opacity-100 hover:opacity-75",
|
||||
link: "text-primary underline-offset-4 hover:underline !p-0 !h-auto",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-full px-3 text-xs",
|
||||
lg: "h-10 rounded-full p-5 px-6",
|
||||
icon: "size-7 rounded-sm",
|
||||
default: "h-9.5 px-4 py-2",
|
||||
sm: "h-8 px-3 text-xs",
|
||||
lg: "h-10 p-5 px-6",
|
||||
icon: "size-7",
|
||||
text: "p-0",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@ const Checkbox = React.forwardRef<
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"cursor-pointer bg-background peer focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary size-4 shrink-0 shadow-xs rounded-[0.35rem] border focus-visible:ring-1 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"cursor-pointer bg-background peer focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary size-4 shrink-0 shadow-xs rounded-sm border focus-visible:ring-1 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -242,7 +242,7 @@ const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(
|
||||
>
|
||||
<button
|
||||
ref={triggerRef}
|
||||
className="size-6 cursor-pointer rounded-full transition-all hover:ring-2 hover:ring-white/20"
|
||||
className="size-6 cursor-pointer rounded-full hover:ring-2 hover:ring-white/20"
|
||||
style={{ backgroundColor: `#${value}` }}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
import * as React from "react";
|
||||
import { ContextMenu as ContextMenuPrimitive } from "radix-ui";
|
||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/utils/ui";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { Tick02Icon, ArrowRightIcon, CircleIcon } from "@hugeicons/core-free-icons"
|
||||
|
||||
const ContextMenu = ContextMenuPrimitive.Root;
|
||||
|
||||
@@ -20,12 +20,12 @@ const ContextMenuSub = ContextMenuPrimitive.Sub;
|
||||
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
|
||||
|
||||
const contextMenuItemVariants = cva(
|
||||
"relative flex cursor-pointer select-none items-center gap-2 px-2 py-1.5 text-sm outline-hidden transition-opacity data-disabled:pointer-events-none data-disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"relative flex cursor-pointer select-none items-center gap-2.5 px-4 py-1.5 last:pb-1 text-base outline-hidden data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "focus:opacity-65 focus:text-accent-foreground",
|
||||
destructive: "text-destructive focus:text-destructive/80",
|
||||
default: "focus:bg-accent/35 focus:text-accent-foreground [&_svg]:text-muted-foreground",
|
||||
destructive: "text-destructive focus:bg-destructive/5 focus:text-destructive [&_svg]:text-destructive",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -39,20 +39,22 @@ const ContextMenuSubTrigger = React.forwardRef<
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
variant?: VariantProps<typeof contextMenuItemVariants>["variant"];
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
>(({ className, inset, children, variant = "default", ...props }, ref) => (
|
||||
>(({ className, inset, children, variant = "default", icon, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
contextMenuItemVariants({ variant }),
|
||||
"data-[state=open]:bg-accent data-[state=open]:opacity-65",
|
||||
"data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{icon && <span className="size-4 shrink-0 text-muted-foreground">{icon}</span>}
|
||||
{children}
|
||||
<ChevronRight className="ml-auto" />
|
||||
<HugeiconsIcon icon={ArrowRightIcon} className="ml-auto text-muted-foreground/80" />
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
));
|
||||
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
|
||||
@@ -64,7 +66,7 @@ const ContextMenuSubContent = React.forwardRef<
|
||||
<ContextMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-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 z-50 min-w-32 overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
"bg-popover text-popover-foreground z-50 min-w-48 overflow-hidden rounded-lg border shadow-xl py-2.5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -80,8 +82,7 @@ const ContextMenuContent = React.forwardRef<
|
||||
<ContextMenuPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground z-50 min-w-32 overflow-hidden rounded-md border p-1 shadow-md",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-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",
|
||||
"bg-popover text-popover-foreground z-50 min-w-48 overflow-hidden rounded-lg border shadow-xl py-2.5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -95,8 +96,10 @@ const ContextMenuItem = React.forwardRef<
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
variant?: VariantProps<typeof contextMenuItemVariants>["variant"];
|
||||
icon?: React.ReactNode;
|
||||
textRight?: string;
|
||||
}
|
||||
>(({ className, inset, variant = "default", ...props }, ref) => (
|
||||
>(({ className, inset, variant = "default", icon, children, textRight, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
@@ -105,7 +108,15 @@ const ContextMenuItem = React.forwardRef<
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
{icon && <span className="[&_svg]:size-4 [&_svg]:shrink-0">{icon}</span>}
|
||||
{children}
|
||||
{textRight && (
|
||||
<span className="ml-auto text-[0.60rem] tracking-widest text-muted-foreground/80 mb-0.5">
|
||||
{textRight}
|
||||
</span>
|
||||
)}
|
||||
</ContextMenuPrimitive.Item>
|
||||
));
|
||||
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
|
||||
|
||||
@@ -113,8 +124,9 @@ const ContextMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem> & {
|
||||
variant?: VariantProps<typeof contextMenuItemVariants>["variant"];
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
>(({ className, children, checked, variant = "default", ...props }, ref) => (
|
||||
>(({ className, children, checked, variant = "default", icon, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(contextMenuItemVariants({ variant }), "pr-2 pl-8", className)}
|
||||
@@ -123,9 +135,10 @@ const ContextMenuCheckboxItem = React.forwardRef<
|
||||
>
|
||||
<span className="absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<Check className="size-4" />
|
||||
<HugeiconsIcon icon={Tick02Icon} className="size-4" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{icon && <span className="size-4 shrink-0 text-muted-foreground">{icon}</span>}
|
||||
{children}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
@@ -136,8 +149,9 @@ const ContextMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem> & {
|
||||
variant?: VariantProps<typeof contextMenuItemVariants>["variant"];
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
>(({ className, children, variant = "default", ...props }, ref) => (
|
||||
>(({ className, children, variant = "default", icon, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(contextMenuItemVariants({ variant }), "pr-2 pl-8", className)}
|
||||
@@ -145,9 +159,10 @@ const ContextMenuRadioItem = React.forwardRef<
|
||||
>
|
||||
<span className="absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<Circle className="size-2 fill-current" />
|
||||
<HugeiconsIcon icon={CircleIcon} className="size-2 fill-current" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{icon && <span className="size-4 shrink-0 text-muted-foreground">{icon}</span>}
|
||||
{children}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
));
|
||||
@@ -157,17 +172,21 @@ const ContextMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
>(({ className, inset, icon, children, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
"flex items-center gap-2.5 px-4 py-1.5 text-sm font-semibold text-foreground",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
{icon && <span className="size-4 shrink-0 text-muted-foreground">{icon}</span>}
|
||||
{children}
|
||||
</ContextMenuPrimitive.Label>
|
||||
));
|
||||
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
|
||||
|
||||
@@ -177,7 +196,7 @@ const ContextMenuSeparator = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("bg-foreground/10 -mx-1 my-1 h-px", className)}
|
||||
className={cn("bg-border my-2 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
@@ -189,7 +208,7 @@ const ContextMenuShortcut = ({
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
className={cn("ml-auto text-xs tracking-widest text-muted-foreground opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
import * as React from "react";
|
||||
import { Dialog as DialogPrimitive } from "radix-ui";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { cn } from "@/utils/ui";
|
||||
import { ScrollArea } from "./scroll-area";
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
@@ -22,7 +20,7 @@ const DialogOverlay = React.forwardRef<
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-250 bg-black/50",
|
||||
"fixed inset-0 z-250 backdrop-blur-sm bg-black/10",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -39,7 +37,7 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"bg-popover data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-250 grid w-[calc(100%-2rem)] max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200",
|
||||
"bg-popover fixed top-[50%] left-[50%] z-250 grid w-[calc(100%-2rem)] max-w-lg translate-x-[-50%] translate-y-[-50%] rounded-lg border shadow-lg duration-200",
|
||||
className,
|
||||
)}
|
||||
onCloseAutoFocus={(e) => {
|
||||
@@ -49,8 +47,8 @@ const DialogContent = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 cursor-pointer opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||
<X className="size-4" />
|
||||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-6 right-6 cursor-pointer opacity-70 hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||
<X className="size-5 text-muted-foreground" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
@@ -63,19 +61,27 @@ const DialogHeader = ({
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("flex flex-col space-y-2 text-left", className)}
|
||||
className={cn("flex flex-col space-y-2 text-left border-b p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
const DialogBody = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("p-6 flex flex-col gap-6", className)} {...props} />
|
||||
);
|
||||
DialogBody.displayName = "DialogBody";
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-4",
|
||||
"flex gap-3 flex-col-reverse sm:flex-row sm:justify-end p-6 py-5 border-t",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -118,6 +124,7 @@ export {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogBody,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
|
||||
@@ -20,7 +20,7 @@ const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
const dropdownMenuItemVariants = cva(
|
||||
"relative flex cursor-pointer select-none items-center gap-2 rounded-xl px-2.5 py-2 text-sm text-foreground/85 outline-hidden data-[highlighted]:bg-accent/35 data-disabled:pointer-events-none data-disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"relative flex cursor-pointer select-none items-center gap-2 rounded-lg px-2.5 py-2 text-sm text-foreground/85 outline-hidden data-[highlighted]:bg-accent/35 data-disabled:pointer-events-none data-disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -88,7 +88,7 @@ const DropdownMenuContent = React.forwardRef<
|
||||
e.preventDefault();
|
||||
}}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground z-50 min-w-32 overflow-hidden rounded-2xl border p-2 shadow-lg",
|
||||
"bg-popover text-popover-foreground z-50 min-w-32 overflow-hidden rounded-lg border p-2 shadow-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -45,7 +45,7 @@ const InputOTPSlot = React.forwardRef<
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-input relative flex aspect-square min-w-[36px] flex-1 items-center justify-center border text-lg shadow-xs transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
||||
"border-input relative flex aspect-square min-w-[36px] flex-1 items-center justify-center border text-lg shadow-xs first:rounded-l-md first:border-l last:rounded-r-md",
|
||||
isActive && "ring-ring z-10 ring-1",
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -41,7 +41,7 @@ export function InputWithBack({
|
||||
return (
|
||||
<div ref={setContainerRef} className="relative w-full">
|
||||
<motion.div
|
||||
className="absolute top-1/2 left-0 z-10 -translate-y-1/2 cursor-pointer transition-opacity hover:opacity-75"
|
||||
className="absolute top-1/2 left-0 z-10 -translate-y-1/2 cursor-pointer hover:opacity-75"
|
||||
initial={{
|
||||
x: isExpanded ? 0 : buttonOffset,
|
||||
opacity: isExpanded ? 1 : 0.5,
|
||||
|
||||
@@ -8,9 +8,13 @@ import { forwardRef, type ComponentProps } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
const inputVariants = cva(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-border bg-background flex w-full min-w-0 rounded-full border shadow-xs outline-none file:inline-flex file:border-0 file:bg-transparent file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 focus-visible:border-primary focus-visible:ring-4 focus-visible:ring-primary/10 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
"file:text-foreground placeholder:text-muted-foreground border-border bg-background flex w-full min-w-0 rounded-md border shadow-xs outline-none file:inline-flex file:border-0 file:bg-transparent file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 focus-visible:ring-4 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "selection:bg-primary selection:text-primary-foreground focus-visible:border-primary focus-visible:ring-primary/10",
|
||||
destructive: "selection:bg-destructive selection:text-destructive-foreground focus-visible:border-destructive focus-visible:ring-destructive/10",
|
||||
},
|
||||
size: {
|
||||
default:
|
||||
"h-9 px-3 py-1 text-base file:h-7 file:text-sm md:text-sm",
|
||||
@@ -20,6 +24,7 @@ const inputVariants = cva(
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "default",
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -40,6 +45,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
className,
|
||||
type,
|
||||
size,
|
||||
variant,
|
||||
containerClassName,
|
||||
showPassword,
|
||||
onShowPasswordChange,
|
||||
@@ -79,6 +85,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
inputVariants({
|
||||
size,
|
||||
className: cn(paddingRight, className),
|
||||
variant,
|
||||
}),
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/utils/ui";
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
"text-sm text-muted-foreground font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
);
|
||||
|
||||
const Label = React.forwardRef<
|
||||
|
||||
@@ -41,7 +41,7 @@ NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
|
||||
const NavigationMenuItem = NavigationMenuPrimitive.Item;
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-hidden disabled:pointer-events-none disabled:opacity-50 data-active:bg-accent/50 data-[state=open]:bg-accent/50",
|
||||
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-hidden disabled:pointer-events-none disabled:opacity-50 data-active:bg-accent/50 data-[state=open]:bg-accent/50",
|
||||
);
|
||||
|
||||
const NavigationMenuTrigger = React.forwardRef<
|
||||
|
||||
@@ -18,7 +18,7 @@ const Progress = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="bg-foreground size-full flex-1 transition-all"
|
||||
className="bg-foreground size-full flex-1"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
|
||||
@@ -20,7 +20,7 @@ const Slider = React.forwardRef<
|
||||
<SliderPrimitive.Track className="bg-primary/20 relative h-1.5 w-full grow overflow-hidden rounded-full">
|
||||
<SliderPrimitive.Range className="bg-primary absolute h-full" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb className="border-primary/50 bg-background focus-visible:ring-ring block size-4 rounded-full border shadow-sm transition-colors focus-visible:ring-1 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50" />
|
||||
<SliderPrimitive.Thumb className="border-primary/50 bg-background focus-visible:ring-ring block size-4 rounded-full border shadow-sm focus-visible:ring-1 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50" />
|
||||
</SliderPrimitive.Root>
|
||||
));
|
||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||
|
||||
@@ -18,7 +18,7 @@ const SplitButton = forwardRef<HTMLDivElement, SplitButtonProps>(
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-input bg-panel-accent inline-flex h-7 overflow-hidden rounded-full border",
|
||||
"border-input bg-panel-accent inline-flex h-7 overflow-hidden rounded-lg border",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -39,7 +39,7 @@ const SplitButtonSide = forwardRef<
|
||||
ref={ref}
|
||||
variant="text"
|
||||
className={cn(
|
||||
"bg-panel-accent disabled:text-muted-foreground h-full gap-0 rounded-none border-0 font-normal !opacity-100 transition-colors",
|
||||
"bg-panel-accent disabled:text-muted-foreground h-full gap-0 rounded-none border-0 font-normal !opacity-100",
|
||||
onClick
|
||||
? "hover:bg-foreground/10 cursor-pointer hover:opacity-100"
|
||||
: "cursor-default select-text",
|
||||
|
||||
@@ -11,7 +11,7 @@ const Switch = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer focus-visible:ring-ring focus-visible:ring-offset-background data-[state=checked]:bg-primary data-[state=unchecked]:bg-input inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-xs transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"peer focus-visible:ring-ring focus-visible:ring-offset-background data-[state=checked]:bg-primary data-[state=unchecked]:bg-input inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-xs focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -19,7 +19,7 @@ const Switch = React.forwardRef<
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"bg-background pointer-events-none block size-4 rounded-full shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
|
||||
"bg-background pointer-events-none block size-4 rounded-full shadow-lg ring-0 data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
|
||||
@@ -58,7 +58,7 @@ const TableRow = React.forwardRef<
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -25,7 +25,7 @@ const ToastViewport = React.forwardRef<
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
||||
|
||||
const toastVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-(--radix-toast-swipe-end-x) data-[swipe=move]:translate-x-(--radix-toast-swipe-move-x) data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full sm:data-[state=open]:slide-in-from-bottom-full",
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-(--radix-toast-swipe-end-x) data-[swipe=move]:translate-x-(--radix-toast-swipe-move-x) data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full sm:data-[state=open]:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -62,7 +62,7 @@ const ToastAction = React.forwardRef<
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"hover:bg-secondary focus:ring-ring group-[.destructive]:border-muted/40 hover:group-[.destructive]:border-destructive/30 hover:group-[.destructive]:bg-destructive hover:group-[.destructive]:text-destructive-foreground group-[.destructive]:focus-visible::ring-destructive inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors focus-visible:ring-1 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50",
|
||||
"hover:bg-secondary focus:ring-ring group-[.destructive]:border-muted/40 hover:group-[.destructive]:border-destructive/30 hover:group-[.destructive]:bg-destructive hover:group-[.destructive]:text-destructive-foreground group-[.destructive]:focus-visible::ring-destructive inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium focus-visible:ring-1 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -77,7 +77,7 @@ const ToastClose = React.forwardRef<
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-foreground/50 hover:text-foreground focus-visible::ring-1 group-[.destructive]:text-muted-foreground absolute top-1 right-1 rounded-md p-1 opacity-0 transition-opacity group-hover:opacity-100 hover:group-[.destructive]:text-red-50 focus:opacity-100 focus:outline-hidden focus-visible:group-[.destructive]:ring-red-400 focus-visible:group-[.destructive]:ring-offset-red-600",
|
||||
"text-foreground/50 hover:text-foreground focus-visible::ring-1 group-[.destructive]:text-muted-foreground absolute top-1 right-1 rounded-md p-1 opacity-0 group-hover:opacity-100 hover:group-[.destructive]:text-red-50 focus:opacity-100 focus:outline-hidden focus-visible:group-[.destructive]:ring-red-400 focus-visible:group-[.destructive]:ring-offset-red-600",
|
||||
className,
|
||||
)}
|
||||
toast-close=""
|
||||
|
||||
@@ -7,7 +7,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/utils/ui";
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
Reference in New Issue
Block a user