mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
projects page re-design and a ton of other stuff
This commit is contained in:
@@ -44,6 +44,7 @@ import {
|
||||
Video01Icon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon, type IconSvgElement } from "@hugeicons/react";
|
||||
import { OcVideoIcon } from "@opencut/ui/icons";
|
||||
|
||||
export function MediaView() {
|
||||
const editor = useEditor();
|
||||
@@ -558,19 +559,18 @@ function MediaPreview({
|
||||
item: MediaAsset;
|
||||
variant?: "grid" | "compact";
|
||||
}) {
|
||||
const shouldShowVideoOverlay = variant === "grid";
|
||||
const shouldShowDurationBadge = variant === "grid";
|
||||
|
||||
if (item.type === "image") {
|
||||
return (
|
||||
<div className="flex size-full items-center justify-center">
|
||||
<div className="relative flex size-full items-center justify-center">
|
||||
<Image
|
||||
src={item.url ?? ""}
|
||||
alt={item.name}
|
||||
className="max-h-full w-full object-cover"
|
||||
fill
|
||||
sizes="100vw"
|
||||
className="object-cover"
|
||||
loading="lazy"
|
||||
width={item.width}
|
||||
height={item.height}
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
@@ -584,18 +584,12 @@ function MediaPreview({
|
||||
<Image
|
||||
src={item.thumbnailUrl}
|
||||
alt={item.name}
|
||||
className="size-full rounded object-cover"
|
||||
fill
|
||||
sizes="100vw"
|
||||
className="rounded object-cover"
|
||||
loading="lazy"
|
||||
unoptimized
|
||||
/>
|
||||
{shouldShowVideoOverlay ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center rounded bg-black/20">
|
||||
<HugeiconsIcon
|
||||
icon={Video01Icon}
|
||||
className="size-6 text-white drop-shadow-md"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{shouldShowDurationBadge ? (
|
||||
<MediaDurationBadge duration={item.duration} />
|
||||
) : null}
|
||||
|
||||
@@ -12,38 +12,43 @@ export function DeleteProjectDialog({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
projectName,
|
||||
projectNames,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onConfirm: () => void;
|
||||
projectName?: string;
|
||||
projectNames: string[];
|
||||
}) {
|
||||
const count = projectNames.length;
|
||||
const isSingle = count === 1;
|
||||
const singleName = isSingle ? projectNames[0] : null;
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
onOpenAutoFocus={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onOpenAutoFocus={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{projectName ? (
|
||||
{singleName ? (
|
||||
<>
|
||||
{"Delete '"}
|
||||
<span className="inline-block max-w-[300px] truncate align-bottom">
|
||||
{projectName}
|
||||
{singleName}
|
||||
</span>
|
||||
{"'?"}
|
||||
</>
|
||||
) : (
|
||||
"Delete Project?"
|
||||
`Delete ${count} projects?`
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete this project? This action cannot be
|
||||
undone.
|
||||
{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>
|
||||
<DialogFooter>
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
ArrowLeft02Icon,
|
||||
Edit03Icon,
|
||||
Delete02Icon,
|
||||
Keyboard,
|
||||
CommandIcon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { ShortcutsDialog } from "./shortcuts-dialog";
|
||||
@@ -92,7 +92,9 @@ function ProjectDropdown() {
|
||||
const handleDeleteProject = async () => {
|
||||
if (activeProject) {
|
||||
try {
|
||||
await editor.project.deleteProject({ id: activeProject.metadata.id });
|
||||
await editor.project.deleteProjects({
|
||||
ids: [activeProject.metadata.id],
|
||||
});
|
||||
router.push("/projects");
|
||||
} catch (error) {
|
||||
toast.error("Failed to delete project", {
|
||||
@@ -148,7 +150,7 @@ function ProjectDropdown() {
|
||||
className="flex items-center gap-1.5"
|
||||
onClick={() => setOpenDialog("shortcuts")}
|
||||
>
|
||||
<HugeiconsIcon icon={Keyboard} className="size-4" />
|
||||
<HugeiconsIcon icon={CommandIcon} className="size-4" />
|
||||
Keyboard shortcuts
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
@@ -174,7 +176,7 @@ function ProjectDropdown() {
|
||||
isOpen={openDialog === "delete"}
|
||||
onOpenChange={(isOpen) => setOpenDialog(isOpen ? "delete" : null)}
|
||||
onConfirm={handleDeleteProject}
|
||||
projectName={activeProject?.metadata.name || ""}
|
||||
projectNames={[activeProject?.metadata.name || ""]}
|
||||
/>
|
||||
<ShortcutsDialog
|
||||
isOpen={openDialog === "shortcuts"}
|
||||
|
||||
@@ -1,30 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowRightIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { SOCIAL_LINKS } from "@/constants/site-constants";
|
||||
import { useLocalStorage } from "@/hooks/storage/use-local-storage";
|
||||
import { Button } from "../ui/button";
|
||||
import { Dialog, DialogContent, DialogTitle } from "../ui/dialog";
|
||||
|
||||
export function Onboarding() {
|
||||
const [step, setStep] = useState(0);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [hasSeenOnboarding, setHasSeenOnboarding] = useLocalStorage({
|
||||
key: "hasSeenOnboarding",
|
||||
defaultValue: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const hasSeenOnboarding = localStorage.getItem("hasSeenOnboarding");
|
||||
if (!hasSeenOnboarding) {
|
||||
setIsOpen(true);
|
||||
}
|
||||
}, []);
|
||||
const isOpen = !hasSeenOnboarding;
|
||||
|
||||
const handleNext = () => {
|
||||
setStep(step + 1);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
localStorage.setItem("hasSeenOnboarding", "true");
|
||||
setHasSeenOnboarding({ value: true });
|
||||
};
|
||||
|
||||
const getStepTitle = () => {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import type { TProjectMetadata } from "@/types/project";
|
||||
import { formatDate } from "@/utils/date";
|
||||
import { formatTimeCode } from "@/lib/time";
|
||||
|
||||
function InfoRow({
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
label: string;
|
||||
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">
|
||||
<span className="text-muted-foreground text-sm">{label}</span>
|
||||
<span className="text-sm font-medium">{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProjectInfoDialog({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
project,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
project: TProjectMetadata;
|
||||
}) {
|
||||
const durationFormatted =
|
||||
project.duration > 0
|
||||
? formatTimeCode({
|
||||
timeInSeconds: project.duration,
|
||||
format: project.duration >= 3600 ? "HH:MM:SS" : "MM:SS",
|
||||
})
|
||||
: "0:00";
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="truncate max-w-[350px]">
|
||||
{project.name}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<InfoRow label="Duration" value={durationFormatted} />
|
||||
<InfoRow
|
||||
label="Created"
|
||||
value={formatDate({ date: project.createdAt })}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Modified"
|
||||
value={formatDate({ date: project.updatedAt })}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Project ID"
|
||||
value={
|
||||
<code className="text-xs bg-muted px-1.5 py-0.5 rounded">
|
||||
{project.id.slice(0, 8)}
|
||||
</code>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -151,7 +151,7 @@ export function TextProperties({
|
||||
<Textarea
|
||||
placeholder="Name"
|
||||
defaultValue={element.content}
|
||||
className="bg-panel-accent min-h-18 resize-none"
|
||||
className="bg-panel-accent min-h-20"
|
||||
onChange={(e) =>
|
||||
editor.timeline.updateTextElement({
|
||||
trackId,
|
||||
|
||||
@@ -65,7 +65,7 @@ const AlertDialogFooter = ({
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -12,25 +12,28 @@ const buttonVariants = cva(
|
||||
default:
|
||||
"bg-foreground text-background shadow-sm hover:bg-foreground/90",
|
||||
foreground:
|
||||
"bg-background text-foreground shadow-sm hover:bg-background/80",
|
||||
"bg-background text-foreground",
|
||||
primary:
|
||||
"bg-primary text-primary-foreground shadow-sm hover:bg-primary/90",
|
||||
"bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
"primary-gradient":
|
||||
"bg-gradient-to-r from-cyan-400 to-blue-500 text-white hover:opacity-85 transition-opacity",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/80",
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
"destructive-foreground":
|
||||
"border bg-background hover:bg-destructive/15 text-destructive",
|
||||
outline:
|
||||
"border border-input bg-transparent shadow-xs transition-colors hover:bg-accent",
|
||||
"border border-border bg-transparent transition-colors hover:bg-accent",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-foreground/15 border border-input",
|
||||
text: "bg-transparent p-0 rounded-none opacity-100 hover:opacity-50 transition-opacity", // Instead of ghost (matches app better)
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
"bg-secondary text-secondary-foreground hover:bg-foreground/15 border border-input",
|
||||
text: "bg-transparent rounded-none !opacity-100",
|
||||
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",
|
||||
icon: "size-7",
|
||||
lg: "h-10 rounded-full p-5 px-6",
|
||||
icon: "size-7 rounded-sm",
|
||||
text: "p-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -49,9 +52,10 @@ export interface ButtonProps
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? SlotPrimitive.Slot : "button";
|
||||
const effectiveSize = size ?? (variant === "text" ? "text" : "default");
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
className={cn(buttonVariants({ variant, size: effectiveSize, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -13,7 +13,7 @@ const Checkbox = React.forwardRef<
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer border-primary focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground size-4 shrink-0 rounded-sm border shadow-sm 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-[0.35rem] border focus-visible:ring-1 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -75,7 +75,7 @@ const DialogFooter = ({
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -21,16 +21,16 @@ export function FontPicker({
|
||||
return (
|
||||
<Select defaultValue={defaultValue} onValueChange={onValueChange}>
|
||||
<SelectTrigger
|
||||
className={`bg-panel-accent h-8 w-full text-xs ${className || ""}`}
|
||||
className={`bg-panel-accent h-9 w-full text-base ${className || ""}`}
|
||||
>
|
||||
<SelectValue placeholder="Select a font" />
|
||||
<SelectValue placeholder="Select a font" className="text-sm" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{FONT_OPTIONS.map((font) => (
|
||||
<SelectItem
|
||||
key={font.value}
|
||||
value={font.value}
|
||||
className="text-xs"
|
||||
className="text-sn"
|
||||
style={{ fontFamily: font.value }}
|
||||
>
|
||||
{font.label}
|
||||
|
||||
@@ -1,12 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import { Eye, EyeOff, X } from "lucide-react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/utils/ui";
|
||||
import { Button } from "./button";
|
||||
import { forwardRef, type ComponentProps } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
interface InputProps extends ComponentProps<"input"> {
|
||||
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",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
default:
|
||||
"h-9 px-3 py-1 text-base file:h-7 file:text-sm md:text-sm",
|
||||
sm: "h-8 px-3 text-xs file:h-6 file:text-xs",
|
||||
lg: "h-10 px-4 text-base file:h-8 file:text-sm md:text-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
interface InputProps
|
||||
extends Omit<ComponentProps<"input">, "size">,
|
||||
VariantProps<typeof inputVariants> {
|
||||
showPassword?: boolean;
|
||||
onShowPasswordChange?: (show: boolean) => void;
|
||||
showClearIcon?: boolean;
|
||||
@@ -19,6 +39,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
{
|
||||
className,
|
||||
type,
|
||||
size,
|
||||
containerClassName,
|
||||
showPassword,
|
||||
onShowPasswordChange,
|
||||
@@ -55,11 +76,10 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
<input
|
||||
type={inputType}
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input bg-background flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[2px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
paddingRight,
|
||||
className,
|
||||
inputVariants({
|
||||
size,
|
||||
className: cn(paddingRight, className),
|
||||
}),
|
||||
)}
|
||||
ref={ref}
|
||||
value={value}
|
||||
|
||||
@@ -14,12 +14,12 @@ const SelectGroup = SelectPrimitive.Group;
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
const selectItemVariants = cva(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-hidden transition-opacity data-disabled:pointer-events-none data-disabled:opacity-50",
|
||||
"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",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "focus:opacity-65 focus:text-accent-foreground",
|
||||
destructive: "text-destructive focus:text-destructive/80",
|
||||
default: "",
|
||||
destructive: "text-destructive data-[highlighted]:bg-destructive/5 data-[highlighted]:text-destructive",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -36,6 +36,7 @@ const SelectTrigger = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"bg-accent ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-7 w-auto cursor-pointer items-center justify-between gap-1 rounded-md px-3 py-2 text-sm whitespace-nowrap focus:ring-1 focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
"focus:border-primary focus:ring-4 focus:ring-primary/10 border-transparent transition-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -91,9 +92,7 @@ const SelectContent = React.forwardRef<
|
||||
<SelectPrimitive.Content
|
||||
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 relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
"bg-popover text-popover-foreground z-50 max-h-96 min-w-32 overflow-hidden rounded-2xl border p-2 shadow-lg",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
@@ -106,7 +105,6 @@ const SelectContent = React.forwardRef<
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)",
|
||||
)}
|
||||
@@ -125,7 +123,10 @@ const SelectLabel = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
|
||||
className={cn(
|
||||
"px-3 pb-2 pt-1 text-[11px] font-bold uppercase tracking-wider text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
@@ -139,7 +140,7 @@ const SelectItem = React.forwardRef<
|
||||
>(({ className, children, variant = "default", ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(selectItemVariants({ variant }), className)}
|
||||
className={cn(selectItemVariants({ variant }), "pr-8 pl-2", className)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
@@ -158,7 +159,7 @@ const SelectSeparator = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("bg-foreground/10 -mx-1 my-1 h-px", className)}
|
||||
className={cn("bg-border/60 mx-1 my-2 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -98,7 +98,7 @@ const SheetFooter = ({
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -9,8 +9,8 @@ const Textarea = React.forwardRef<
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input bg-accent/50 flex min-h-[60px] w-full rounded-md border px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[2px]",
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input bg-accent/50 flex min-h-[60px] w-full rounded-md border px-3 py-2 text-base shadow-xs outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"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",
|
||||
className,
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user