mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
polish chat input and sidebar interactions (#99)
* Polish chat input and sidebar interactions. Standardize keyboard focus behavior and tighten sidebar and composer controls so the compact UI feels more consistent while preserving drag-and-drop image workflows. * Fix lint follow-ups for UI polish. Extract the chat image drop-target logic and tighten the shared focus selector so CI passes without changing the polished behavior. * Fix keyboard focus detection for non-Tab navigation. Preserve hidden pointer focus rings while restoring visible focus for keyboard shortcuts and other keyboard-driven flows that move focus without Tab.
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { AppShell } from "@/app/AppShell";
|
||||
import { useKeyboardNavDetection } from "@/shared/hooks/useKeyboardNavDetection";
|
||||
|
||||
export function App() {
|
||||
useKeyboardNavDetection();
|
||||
|
||||
useEffect(() => {
|
||||
const preventWindowFileNavigation = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { useCallback, useRef, useState, type DragEvent } from "react";
|
||||
|
||||
interface UseImageDropTargetOptions {
|
||||
disabled: boolean;
|
||||
isStreaming: boolean;
|
||||
onDropFile: (file: File) => void;
|
||||
}
|
||||
|
||||
function hasDraggedFiles(dataTransfer: DataTransfer) {
|
||||
return (
|
||||
Array.from(dataTransfer.items).some(
|
||||
(item) => item.kind === "file" || item.type.startsWith("image/"),
|
||||
) || Array.from(dataTransfer.types).includes("Files")
|
||||
);
|
||||
}
|
||||
|
||||
export function useImageDropTarget({
|
||||
disabled,
|
||||
isStreaming,
|
||||
onDropFile,
|
||||
}: UseImageDropTargetOptions) {
|
||||
const [isImageDragOver, setIsImageDragOver] = useState(false);
|
||||
const dragDepthRef = useRef(0);
|
||||
|
||||
const handleDragEnter = useCallback(
|
||||
(e: DragEvent<HTMLDivElement>) => {
|
||||
if (disabled || isStreaming || !hasDraggedFiles(e.dataTransfer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
dragDepthRef.current += 1;
|
||||
setIsImageDragOver(true);
|
||||
},
|
||||
[disabled, isStreaming],
|
||||
);
|
||||
|
||||
const handleDragOver = useCallback(
|
||||
(e: DragEvent<HTMLDivElement>) => {
|
||||
if (disabled || isStreaming || !hasDraggedFiles(e.dataTransfer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
setIsImageDragOver(true);
|
||||
},
|
||||
[disabled, isStreaming],
|
||||
);
|
||||
|
||||
const handleDragLeave = useCallback(
|
||||
(e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!isImageDragOver) {
|
||||
return;
|
||||
}
|
||||
|
||||
dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);
|
||||
if (dragDepthRef.current === 0) {
|
||||
setIsImageDragOver(false);
|
||||
}
|
||||
},
|
||||
[isImageDragOver],
|
||||
);
|
||||
|
||||
const handleDrop = useCallback(
|
||||
(e: DragEvent<HTMLDivElement>) => {
|
||||
dragDepthRef.current = 0;
|
||||
setIsImageDragOver(false);
|
||||
|
||||
if (disabled || isStreaming) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = Array.from(e.dataTransfer.files).filter((file) =>
|
||||
file.type.startsWith("image/"),
|
||||
);
|
||||
if (files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
for (const file of files) {
|
||||
onDropFile(file);
|
||||
}
|
||||
},
|
||||
[disabled, isStreaming, onDropFile],
|
||||
);
|
||||
|
||||
return {
|
||||
isImageDragOver,
|
||||
handleDragEnter,
|
||||
handleDragOver,
|
||||
handleDragLeave,
|
||||
handleDrop,
|
||||
};
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { X } from "lucide-react";
|
||||
import type { AcpProvider } from "@/shared/api/acp";
|
||||
import type { Persona } from "@/shared/types/agents";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
MentionAutocomplete,
|
||||
@@ -15,6 +16,7 @@ import { PersonaAvatar } from "./PersonaPicker";
|
||||
import { ImageLightbox } from "@/shared/ui/ImageLightbox";
|
||||
import type { PastedImage } from "@/shared/types/messages";
|
||||
import { resizeImage } from "../lib/resizeImage";
|
||||
import { useImageDropTarget } from "../hooks/useImageDropTarget";
|
||||
|
||||
export interface ModelOption {
|
||||
id: string;
|
||||
@@ -83,7 +85,7 @@ function PastedImageThumb({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLightboxOpen(true)}
|
||||
className="block cursor-pointer rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
className="block cursor-pointer rounded-lg"
|
||||
aria-label={`View attachment ${index + 1}`}
|
||||
>
|
||||
<img
|
||||
@@ -308,26 +310,17 @@ export function ChatInput({
|
||||
[addImageFile],
|
||||
);
|
||||
|
||||
const handleDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
||||
const hasImages = Array.from(e.dataTransfer.items).some((item) =>
|
||||
item.type.startsWith("image/"),
|
||||
);
|
||||
if (hasImages) e.preventDefault();
|
||||
}, []);
|
||||
|
||||
const handleDrop = useCallback(
|
||||
(e: React.DragEvent<HTMLDivElement>) => {
|
||||
const files = Array.from(e.dataTransfer.files).filter((f) =>
|
||||
f.type.startsWith("image/"),
|
||||
);
|
||||
if (files.length === 0) return;
|
||||
e.preventDefault();
|
||||
for (const file of files) {
|
||||
addImageFile(file);
|
||||
}
|
||||
},
|
||||
[addImageFile],
|
||||
);
|
||||
const {
|
||||
isImageDragOver,
|
||||
handleDragEnter,
|
||||
handleDragOver,
|
||||
handleDragLeave,
|
||||
handleDrop,
|
||||
} = useImageDropTarget({
|
||||
disabled,
|
||||
isStreaming,
|
||||
onDropFile: addImageFile,
|
||||
});
|
||||
|
||||
const removeImage = useCallback((index: number) => {
|
||||
setImages((prev) => {
|
||||
@@ -352,10 +345,25 @@ export function ChatInput({
|
||||
<div className="mx-auto max-w-3xl">
|
||||
{/* biome-ignore lint/a11y/noStaticElementInteractions: drop zone for image files */}
|
||||
<div
|
||||
className="relative rounded-2xl border border-border bg-background px-4 pb-3 pt-4"
|
||||
className={cn(
|
||||
"relative rounded-2xl border border-border bg-background px-4 pb-3 pt-4 transition-colors",
|
||||
isImageDragOver && "bg-muted/20",
|
||||
)}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
{isImageDragOver && (
|
||||
<div className="pointer-events-none absolute inset-0 z-10 flex items-center justify-center rounded-2xl border border-dashed border-border bg-background/70">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="px-3 py-1 text-sm shadow-sm"
|
||||
>
|
||||
Drop images to attach
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
<MentionAutocomplete
|
||||
personas={personas}
|
||||
query={mentionQuery}
|
||||
|
||||
@@ -103,30 +103,30 @@ export function ChatInputSelector({
|
||||
key={item.value}
|
||||
disabled={item.disabled}
|
||||
onSelect={() => onValueChange(item.value)}
|
||||
className={cn(
|
||||
"gap-2 justify-between",
|
||||
item.description ? "items-start" : "items-center",
|
||||
)}
|
||||
className="justify-between gap-2"
|
||||
>
|
||||
{item.icon ? (
|
||||
<span
|
||||
className={cn("shrink-0", item.description && "mt-1.5")}
|
||||
>
|
||||
{item.icon}
|
||||
</span>
|
||||
) : null}
|
||||
<div className="min-w-0 flex-1">
|
||||
<span className="block truncate font-normal">
|
||||
{item.label}
|
||||
</span>
|
||||
{item.description ? (
|
||||
<span className="block truncate text-xs text-muted-foreground">
|
||||
{item.description}
|
||||
</span>
|
||||
<div
|
||||
className={cn(
|
||||
"min-w-0 flex flex-1 gap-2",
|
||||
item.description ? "items-start" : "items-center",
|
||||
)}
|
||||
>
|
||||
{item.icon ? (
|
||||
<span className="shrink-0">{item.icon}</span>
|
||||
) : null}
|
||||
<div className="min-w-0 flex-1">
|
||||
<span className="block truncate font-normal">
|
||||
{item.label}
|
||||
</span>
|
||||
{item.description ? (
|
||||
<span className="block truncate text-xs text-muted-foreground">
|
||||
{item.description}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{item.value === value ? (
|
||||
<Check className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
|
||||
<Check className="size-4 shrink-0 self-center text-muted-foreground" />
|
||||
) : null}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import {
|
||||
Paperclip,
|
||||
Mic,
|
||||
ChevronDown,
|
||||
Check,
|
||||
ArrowUp,
|
||||
Square,
|
||||
} from "lucide-react";
|
||||
import { Mic, ChevronDown, Check, ArrowUp, Square } from "lucide-react";
|
||||
import {
|
||||
formatProviderLabel,
|
||||
getProviderIcon,
|
||||
@@ -277,16 +270,6 @@ export function ChatInputToolbar({
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="Attach file"
|
||||
title="Attach file"
|
||||
>
|
||||
<Paperclip />
|
||||
</Button>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
|
||||
@@ -8,7 +8,7 @@ export function ClickableImage({ src, alt }: { src: string; alt: string }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(true)}
|
||||
className="cursor-pointer rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
className="cursor-pointer rounded-lg"
|
||||
aria-label={`View ${alt}`}
|
||||
>
|
||||
<img
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { BookOpen, Bot, ChevronLeft, ChevronRight, Home } from "lucide-react";
|
||||
import {
|
||||
IconLayoutSidebar,
|
||||
IconLayoutSidebarFilled,
|
||||
} from "@tabler/icons-react";
|
||||
import { BookOpen, Bot, Home } from "lucide-react";
|
||||
import { GooseIcon } from "@/shared/ui/icons/GooseIcon";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import type { AppView } from "@/app/AppShell";
|
||||
@@ -7,6 +11,7 @@ import type { ProjectInfo } from "@/features/projects/api/projects";
|
||||
import { useChatStore } from "@/features/chat/stores/chatStore";
|
||||
import { useChatSessionStore } from "@/features/chat/stores/chatSessionStore";
|
||||
import { isSessionRunning } from "@/features/chat/lib/sessionActivity";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { SidebarProjectsSection } from "./SidebarProjectsSection";
|
||||
import { useSidebarHighlight } from "./useSidebarHighlight";
|
||||
|
||||
@@ -196,6 +201,10 @@ export function Sidebar({
|
||||
|
||||
// Update active rect when activeView/activeSessionId changes
|
||||
useEffect(() => {
|
||||
if (activeSessionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeView === "home") {
|
||||
updateActiveRect(homeRef.current);
|
||||
} else if (activeView && navItemRefs.current[activeView]) {
|
||||
@@ -203,7 +212,7 @@ export function Sidebar({
|
||||
} else {
|
||||
updateActiveRect(null);
|
||||
}
|
||||
}, [activeView, updateActiveRect]);
|
||||
}, [activeSessionId, activeView, updateActiveRect]);
|
||||
|
||||
// Callback for SidebarProjectsSection to register active session refs
|
||||
const activeSessionRefCallback = useCallback(
|
||||
@@ -224,30 +233,35 @@ export function Sidebar({
|
||||
)}
|
||||
style={{ width: collapsed ? 54 : width }}
|
||||
>
|
||||
{/* Collapse toggle — vertically centered, half outside the right edge */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCollapse}
|
||||
className={cn(
|
||||
"absolute top-1/2 -translate-y-1/2 -right-3 z-50",
|
||||
"flex items-center justify-center size-6 rounded-full",
|
||||
"bg-background border border-border",
|
||||
"text-muted-foreground hover:text-foreground hover:scale-110",
|
||||
"transition-transform duration-200",
|
||||
)}
|
||||
aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
|
||||
>
|
||||
{collapsed ? (
|
||||
<ChevronRight className="size-3" />
|
||||
) : (
|
||||
<ChevronLeft className="size-3" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="flex flex-col h-full overflow-hidden bg-background border border-border rounded-xl">
|
||||
{/* Goose logo — pinned top */}
|
||||
<div className="flex-shrink-0 px-3 pt-3 pb-1">
|
||||
<GooseIcon className="text-muted-foreground" />
|
||||
<div
|
||||
className={cn(
|
||||
"flex-shrink-0 pt-3",
|
||||
collapsed ? "px-1.5 pb-1.5" : "px-3 pb-1",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center",
|
||||
collapsed ? "justify-center" : "justify-between",
|
||||
)}
|
||||
>
|
||||
<GooseIcon className="text-muted-foreground" />
|
||||
{!collapsed && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={onCollapse}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
aria-label="Collapse sidebar"
|
||||
title="Collapse sidebar"
|
||||
>
|
||||
<IconLayoutSidebarFilled className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation (scrollable) */}
|
||||
@@ -271,6 +285,19 @@ export function Sidebar({
|
||||
)}
|
||||
|
||||
<div className="relative z-10 space-y-0.5">
|
||||
{collapsed && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCollapse}
|
||||
title="Expand sidebar"
|
||||
className="flex w-full items-center gap-2.5 rounded-md p-3 text-[13px] text-muted-foreground transition-colors duration-200 hover:text-foreground"
|
||||
aria-label="Expand sidebar"
|
||||
>
|
||||
<IconLayoutSidebar className="size-4 flex-shrink-0" />
|
||||
<span className="sr-only">Expand sidebar</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* TODO: Search bar — uncomment when onSearchClick is wired up */}
|
||||
{/* <button
|
||||
type="button"
|
||||
|
||||
@@ -7,7 +7,8 @@ import { SessionActivityIndicator } from "@/shared/ui/SessionActivityIndicator";
|
||||
|
||||
const INACTIVE_CHAT_ROW_CLASS =
|
||||
"text-muted-foreground hover:bg-transparent hover:text-foreground group-hover:text-foreground";
|
||||
const ACTIVE_CHAT_ROW_CLASS = "text-foreground";
|
||||
const ACTIVE_CHAT_ROW_CLASS =
|
||||
"text-foreground hover:bg-transparent hover:text-foreground";
|
||||
|
||||
interface SidebarChatRowProps {
|
||||
id: string;
|
||||
@@ -41,6 +42,7 @@ export function SidebarChatRow({
|
||||
const [draftTitle, setDraftTitle] = useState(title);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const rowRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!menuOpen) return;
|
||||
@@ -64,6 +66,20 @@ export function SidebarChatRow({
|
||||
inputRef.current?.select();
|
||||
}, [editing]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeRef || !isActive || !rowRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const frame = requestAnimationFrame(() => {
|
||||
if (rowRef.current) {
|
||||
activeRef(rowRef.current);
|
||||
}
|
||||
});
|
||||
|
||||
return () => cancelAnimationFrame(frame);
|
||||
}, [activeRef, isActive]);
|
||||
|
||||
const startRename = () => {
|
||||
setDraftTitle(title);
|
||||
setMenuOpen(false);
|
||||
@@ -116,7 +132,7 @@ export function SidebarChatRow({
|
||||
return (
|
||||
// biome-ignore lint/a11y/noStaticElementInteractions: wrapper div for hover detection, interactive content is the inner Button
|
||||
<div
|
||||
ref={activeRef}
|
||||
ref={rowRef}
|
||||
className={cn(
|
||||
"flex items-center group rounded-md pr-0.5 transition-colors duration-200",
|
||||
className,
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
const ATTR = "data-keyboard-nav";
|
||||
const NAVIGATION_KEYS = new Set([
|
||||
"Tab",
|
||||
"ArrowUp",
|
||||
"ArrowDown",
|
||||
"ArrowLeft",
|
||||
"ArrowRight",
|
||||
"Enter",
|
||||
" ",
|
||||
"Escape",
|
||||
"Home",
|
||||
"End",
|
||||
"PageUp",
|
||||
"PageDown",
|
||||
]);
|
||||
|
||||
function isKeyboardNavigationEvent(event: KeyboardEvent) {
|
||||
if (event.key === "Shift" || event.key === "Meta") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return NAVIGATION_KEYS.has(event.key);
|
||||
}
|
||||
|
||||
export function useKeyboardNavDetection() {
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (isKeyboardNavigationEvent(e)) {
|
||||
root.setAttribute(ATTR, "");
|
||||
}
|
||||
}
|
||||
|
||||
function onPointerDown() {
|
||||
root.removeAttribute(ATTR);
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", onKeyDown, true);
|
||||
window.addEventListener("pointerdown", onPointerDown, true);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", onKeyDown, true);
|
||||
window.removeEventListener("pointerdown", onPointerDown, true);
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
@@ -755,13 +755,22 @@ body,
|
||||
@layer components {
|
||||
*:not(body):not(.focus-override) {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
html[data-keyboard-nav] *:not(body):not(.focus-override) {
|
||||
&:focus-visible {
|
||||
@apply focus-visible:ring-ring focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:outline-hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
@apply hover:text-foreground/80 focus-visible:ring-ring focus-visible:ring-offset-background inline-flex items-center gap-1 rounded-xs font-medium underline underline-offset-4 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden;
|
||||
@apply hover:text-foreground/80 inline-flex items-center gap-1 rounded-xs font-medium underline underline-offset-4;
|
||||
}
|
||||
|
||||
html[data-keyboard-nav] *:not(body):not(.focus-override).link {
|
||||
&:focus-visible {
|
||||
@apply ring-ring ring-offset-background ring-2 ring-offset-2 outline-hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ function AccordionTrigger({
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm transition-all outline-none hover:underline focus-visible:ring-[1px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||
"flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm transition-all outline-none hover:underline disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-pill border px-2 py-0.5 text-xs w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[1px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
"inline-flex items-center justify-center rounded-pill border px-2 py-0.5 text-xs w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -14,7 +14,7 @@ const badgeVariants = cva(
|
||||
secondary:
|
||||
"border-transparent bg-muted text-foreground [a&]:hover:bg-muted/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-muted [a&]:hover:text-muted-foreground",
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-full text-left text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-full text-left text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -19,7 +19,7 @@ const buttonVariants = cva(
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
toolbar:
|
||||
"justify-start bg-transparent font-normal text-foreground shadow-none hover:bg-accent hover:text-accent-foreground active:bg-accent active:text-accent-foreground focus-visible:ring-0 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground aria-expanded:bg-accent aria-expanded:text-accent-foreground",
|
||||
"justify-start bg-transparent font-normal text-foreground shadow-none hover:bg-accent hover:text-accent-foreground active:bg-accent active:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground aria-expanded:bg-accent aria-expanded:text-accent-foreground",
|
||||
link: "text-brand underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
|
||||
@@ -12,7 +12,7 @@ function Checkbox({
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-border-inverse focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border transition-shadow outline-none focus-visible:ring-[1px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"peer border-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-border-inverse aria-invalid:ring-destructive/20 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border transition-shadow outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -72,7 +72,7 @@ function DropdownMenuItem({
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-muted focus:text-muted-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"focus:bg-muted data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -90,7 +90,7 @@ function DropdownMenuCheckboxItem({
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-muted focus:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"focus:bg-muted relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
@@ -126,7 +126,7 @@ function DropdownMenuRadioItem({
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-muted focus:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"focus:bg-muted relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -206,7 +206,7 @@ function DropdownMenuSubTrigger({
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-muted focus:text-muted-foreground data-[state=open]:bg-muted data-[state=open]:text-muted-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
||||
"focus:bg-muted data-[state=open]:bg-muted flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -9,7 +9,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input hover:border-border-input-hover flex h-9 w-full min-w-0 rounded-input border bg-transparent px-3 py-1 text-base transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file: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-[1px]",
|
||||
"",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -59,7 +59,7 @@ function NavigationMenuItem({
|
||||
}
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-9 w-max items-center justify-center rounded-full bg-background px-4 py-2 text-sm hover:bg-muted focus:bg-muted disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-muted data-[state=open]:focus:bg-muted data-[state=open]:bg-muted/50 focus-visible:ring-ring/50 outline-none transition-[color] focus-visible:ring-[1px] focus-visible:outline-1",
|
||||
"group inline-flex h-9 w-max items-center justify-center rounded-full bg-background px-4 py-2 text-sm hover:bg-muted focus:bg-muted disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-muted data-[state=open]:focus:bg-muted data-[state=open]:bg-muted/50 outline-none transition-[color]",
|
||||
);
|
||||
|
||||
function NavigationMenuTrigger({
|
||||
@@ -129,7 +129,7 @@ function NavigationMenuLink({
|
||||
<NavigationMenuPrimitive.Link
|
||||
data-slot="navigation-menu-link"
|
||||
className={cn(
|
||||
"data-[active=true]:focus:bg-muted data-[active=true]:hover:bg-muted data-[active=true]:bg-muted/50 hover:bg-muted focus:bg-muted focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[1px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
"data-[active=true]:focus:bg-muted data-[active=true]:hover:bg-muted data-[active=true]:bg-muted/50 hover:bg-muted focus:bg-muted [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -25,7 +25,7 @@ function RadioGroupItem({
|
||||
<RadioGroupPrimitive.Item
|
||||
data-slot="radio-group-item"
|
||||
className={cn(
|
||||
"border-input text-primary-foreground data-[state=checked]:bg-primary data-[state=checked]:border-none focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive aspect-square size-4 shrink-0 rounded-full border transition-[color,box-shadow] outline-none focus-visible:ring-[1px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"border-input text-primary-foreground data-[state=checked]:bg-primary data-[state=checked]:border-none aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive aspect-square size-4 shrink-0 rounded-full border transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -37,7 +37,7 @@ function ResizableHandle({
|
||||
<ResizablePrimitive.Separator
|
||||
data-slot="resizable-handle"
|
||||
className={cn(
|
||||
"bg-border-default focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
||||
"bg-border-default relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -16,7 +16,7 @@ function ScrollArea({
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[1px] focus-visible:outline-1"
|
||||
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
|
||||
@@ -35,7 +35,7 @@ function SelectTrigger({
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex w-fit items-center justify-between gap-2 rounded-input border bg-transparent px-3 py-2 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none focus-visible:ring-[1px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex w-fit items-center justify-between gap-2 rounded-input border bg-transparent px-3 py-2 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -404,7 +404,7 @@ function SidebarGroupLabel({
|
||||
data-slot="sidebar-group-label"
|
||||
data-sidebar="group-label"
|
||||
className={cn(
|
||||
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs outline-hidden transition-[margin,opacity] duration-200 ease-linear [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||
className,
|
||||
)}
|
||||
@@ -425,7 +425,7 @@ function SidebarGroupAction({
|
||||
data-slot="sidebar-group-action"
|
||||
data-sidebar="group-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
@@ -473,7 +473,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
}
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -560,7 +560,7 @@ function SidebarMenuAction({
|
||||
data-slot="sidebar-menu-action"
|
||||
data-sidebar="menu-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
@@ -685,7 +685,7 @@ function SidebarMenuSubButton({
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
||||
size === "sm" && "text-xs",
|
||||
size === "md" && "text-sm",
|
||||
|
||||
@@ -51,7 +51,7 @@ function Slider({
|
||||
<SliderPrimitive.Thumb
|
||||
data-slot="slider-thumb"
|
||||
key={index}
|
||||
className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
||||
className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border transition-[color,box-shadow] hover:ring-4 disabled:pointer-events-none disabled:opacity-50"
|
||||
/>
|
||||
))}
|
||||
</SliderPrimitive.Root>
|
||||
|
||||
@@ -11,7 +11,7 @@ function Switch({
|
||||
<SwitchPrimitive.Root
|
||||
data-slot="switch"
|
||||
className={cn(
|
||||
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-background-medium focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-muted/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent transition-all outline-none focus-visible:ring-[1px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-background-medium dark:data-[state=unchecked]:bg-muted/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent transition-all outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -20,7 +20,7 @@ const tabsListVariants = cva(
|
||||
);
|
||||
|
||||
const tabsTriggerVariants = cva(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring inline-flex flex-1 items-center justify-center gap-1.5 whitespace-nowrap transition-[color,box-shadow,background-color,border-color] focus-visible:ring-[1px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"inline-flex flex-1 items-center justify-center gap-1.5 whitespace-nowrap transition-[color,box-shadow,background-color,border-color] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base transition-[color,box-shadow] outline-none focus-visible:ring-[1px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"border-input placeholder:text-muted-foreground aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 rounded-full text-sm hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-muted data-[state=on]:text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[1px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
||||
"inline-flex items-center justify-center gap-2 rounded-full text-sm hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-muted data-[state=on]:text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
Reference in New Issue
Block a user