Improve layout
This commit is contained in:
35
frontend/package-lock.json
generated
35
frontend/package-lock.json
generated
@@ -16,6 +16,7 @@
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"click-to-react-component": "^1.1.2",
|
||||
"clsx": "^2.0.0",
|
||||
@@ -1687,6 +1688,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz",
|
||||
"integrity": "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.2",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.10",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-popper": "1.2.7",
|
||||
"@radix-ui/react-portal": "1.1.9",
|
||||
"@radix-ui/react-presence": "1.1.4",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"@radix-ui/react-visually-hidden": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"click-to-react-component": "^1.1.2",
|
||||
"clsx": "^2.0.0",
|
||||
|
||||
@@ -6,29 +6,39 @@ import {
|
||||
Clock,
|
||||
FileText,
|
||||
Code,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Plus,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
import { makeRequest } from "@/lib/api";
|
||||
import { getTaskPanelClasses, getBackdropClasses } from "@/lib/responsive-config";
|
||||
import {
|
||||
getTaskPanelClasses,
|
||||
getBackdropClasses,
|
||||
} from "@/lib/responsive-config";
|
||||
import type {
|
||||
TaskStatus,
|
||||
TaskAttempt,
|
||||
TaskAttemptActivity,
|
||||
TaskAttemptStatus,
|
||||
ExecutionProcess,
|
||||
ExecutionProcessStatus,
|
||||
ExecutionProcessType,
|
||||
ApiResponse,
|
||||
TaskWithAttemptStatus,
|
||||
} from "shared/types";
|
||||
@@ -100,51 +110,6 @@ const getAttemptStatusDisplay = (
|
||||
}
|
||||
};
|
||||
|
||||
const getProcessStatusDisplay = (
|
||||
status: ExecutionProcessStatus
|
||||
): { label: string; className: string } => {
|
||||
switch (status) {
|
||||
case "running":
|
||||
return {
|
||||
label: "Running",
|
||||
className: "bg-status-running text-status-running-foreground",
|
||||
};
|
||||
case "completed":
|
||||
return {
|
||||
label: "Completed",
|
||||
className: "bg-status-complete text-status-complete-foreground",
|
||||
};
|
||||
case "failed":
|
||||
return {
|
||||
label: "Failed",
|
||||
className: "bg-status-failed text-status-failed-foreground",
|
||||
};
|
||||
case "killed":
|
||||
return {
|
||||
label: "Killed",
|
||||
className: "bg-status-failed text-status-failed-foreground",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
label: "Unknown",
|
||||
className: "bg-status-init text-status-init-foreground",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getProcessTypeDisplay = (type: ExecutionProcessType): string => {
|
||||
switch (type) {
|
||||
case "setupscript":
|
||||
return "Setup Script";
|
||||
case "codingagent":
|
||||
return "Coding Agent";
|
||||
case "devserver":
|
||||
return "Dev Server";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
};
|
||||
|
||||
export function TaskDetailsPanel({
|
||||
task,
|
||||
projectId,
|
||||
@@ -158,12 +123,9 @@ export function TaskDetailsPanel({
|
||||
const [attemptActivities, setAttemptActivities] = useState<
|
||||
TaskAttemptActivity[]
|
||||
>([]);
|
||||
const [executionProcesses, setExecutionProcesses] = useState<
|
||||
ExecutionProcess[]
|
||||
>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [followUpMessage, setFollowUpMessage] = useState("");
|
||||
const [showAttemptHistory, setShowAttemptHistory] = useState(false);
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
|
||||
|
||||
// Check if the selected attempt is active (not in a final state)
|
||||
const isAttemptRunning =
|
||||
@@ -181,7 +143,6 @@ export function TaskDetailsPanel({
|
||||
const interval = setInterval(() => {
|
||||
if (selectedAttempt) {
|
||||
fetchAttemptActivities(selectedAttempt.id, true);
|
||||
fetchExecutionProcesses(selectedAttempt.id, true);
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
@@ -217,7 +178,6 @@ export function TaskDetailsPanel({
|
||||
);
|
||||
setSelectedAttempt(latestAttempt);
|
||||
fetchAttemptActivities(latestAttempt.id);
|
||||
fetchExecutionProcesses(latestAttempt.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -251,35 +211,11 @@ export function TaskDetailsPanel({
|
||||
}
|
||||
};
|
||||
|
||||
const fetchExecutionProcesses = async (
|
||||
attemptId: string,
|
||||
_isBackgroundUpdate = false
|
||||
) => {
|
||||
if (!task) return;
|
||||
|
||||
try {
|
||||
const response = await makeRequest(
|
||||
`/api/projects/${projectId}/tasks/${task.id}/attempts/${attemptId}/execution-processes`
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
const result: ApiResponse<ExecutionProcess[]> = await response.json();
|
||||
if (result.success && result.data) {
|
||||
setExecutionProcesses(result.data);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch execution processes:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAttemptChange = (attemptId: string) => {
|
||||
const attempt = taskAttempts.find((a) => a.id === attemptId);
|
||||
if (attempt) {
|
||||
setSelectedAttempt(attempt);
|
||||
fetchAttemptActivities(attempt.id);
|
||||
fetchExecutionProcesses(attempt.id);
|
||||
setShowAttemptHistory(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -289,30 +225,6 @@ export function TaskDetailsPanel({
|
||||
setFollowUpMessage("");
|
||||
};
|
||||
|
||||
const stopExecutionProcess = async (processId: string) => {
|
||||
if (!task || !selectedAttempt) return;
|
||||
|
||||
try {
|
||||
const response = await makeRequest(
|
||||
`/api/projects/${projectId}/tasks/${task.id}/attempts/${selectedAttempt.id}/execution-processes/${processId}/stop`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
// Refresh the execution processes
|
||||
fetchExecutionProcesses(selectedAttempt.id);
|
||||
fetchAttemptActivities(selectedAttempt.id);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to stop execution process:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const openInEditor = async () => {
|
||||
if (!task || !selectedAttempt) return;
|
||||
|
||||
@@ -331,6 +243,29 @@ export function TaskDetailsPanel({
|
||||
}
|
||||
};
|
||||
|
||||
const createNewAttempt = async () => {
|
||||
if (!task) return;
|
||||
|
||||
try {
|
||||
const response = await makeRequest(
|
||||
`/api/projects/${projectId}/tasks/${task.id}/attempts`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
// Refresh the attempts list
|
||||
fetchTaskAttempts();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to create new attempt:", err);
|
||||
}
|
||||
};
|
||||
|
||||
if (!task) return null;
|
||||
|
||||
return (
|
||||
@@ -338,15 +273,10 @@ export function TaskDetailsPanel({
|
||||
{isOpen && (
|
||||
<>
|
||||
{/* Backdrop - only on smaller screens (overlay mode) */}
|
||||
<div
|
||||
className={getBackdropClasses()}
|
||||
onClick={onClose}
|
||||
/>
|
||||
<div className={getBackdropClasses()} onClick={onClose} />
|
||||
|
||||
{/* Panel */}
|
||||
<div
|
||||
className={getTaskPanelClasses()}
|
||||
>
|
||||
<div className={getTaskPanelClasses()}>
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b space-y-4">
|
||||
@@ -380,13 +310,59 @@ export function TaskDetailsPanel({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<div className="p-3 bg-muted rounded-md">
|
||||
{task.description ? (
|
||||
<div>
|
||||
<p
|
||||
className={`text-sm whitespace-pre-wrap ${
|
||||
!isDescriptionExpanded &&
|
||||
task.description.length > 200
|
||||
? "line-clamp-3"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{task.description}
|
||||
</p>
|
||||
{task.description.length > 200 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setIsDescriptionExpanded(!isDescriptionExpanded)
|
||||
}
|
||||
className="mt-2 p-0 h-auto text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{isDescriptionExpanded ? (
|
||||
<>
|
||||
<ChevronUp className="h-3 w-3 mr-1" />
|
||||
Show less
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronDown className="h-3 w-3 mr-1" />
|
||||
Show more
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground italic">
|
||||
No description provided
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Attempt Selection */}
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedAttempt && !showAttemptHistory ? (
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Current attempt:
|
||||
</span>
|
||||
<div className="flex items-center gap-2 p-3 bg-muted/30 rounded-md">
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Current attempt:
|
||||
</span>
|
||||
{selectedAttempt && (
|
||||
<span className="text-sm font-medium">
|
||||
{new Date(
|
||||
selectedAttempt.created_at
|
||||
@@ -395,55 +371,64 @@ export function TaskDetailsPanel({
|
||||
selectedAttempt.created_at
|
||||
).toLocaleTimeString()}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex gap-1">
|
||||
{taskAttempts.length > 1 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowAttemptHistory(true)}
|
||||
>
|
||||
<History className="h-4 w-4 mr-1" />
|
||||
History
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<History className="h-4 w-4 mr-1" />
|
||||
History
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-64">
|
||||
{taskAttempts.map((attempt) => (
|
||||
<DropdownMenuItem
|
||||
key={attempt.id}
|
||||
onClick={() => handleAttemptChange(attempt.id)}
|
||||
className={
|
||||
selectedAttempt?.id === attempt.id
|
||||
? "bg-accent"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col w-full">
|
||||
<span className="font-medium text-sm">
|
||||
{new Date(
|
||||
attempt.created_at
|
||||
).toLocaleDateString()}{" "}
|
||||
{new Date(
|
||||
attempt.created_at
|
||||
).toLocaleTimeString()}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{attempt.executor || "executor"}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="default"
|
||||
onClick={createNewAttempt}
|
||||
className="h-9 w-9 p-0"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Create new attempt</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<Select
|
||||
value={selectedAttempt?.id || ""}
|
||||
onValueChange={handleAttemptChange}
|
||||
>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue placeholder="Select an attempt..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{taskAttempts.map((attempt) => (
|
||||
<SelectItem key={attempt.id} value={attempt.id}>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">
|
||||
{new Date(
|
||||
attempt.created_at
|
||||
).toLocaleDateString()}{" "}
|
||||
{new Date(
|
||||
attempt.created_at
|
||||
).toLocaleTimeString()}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground text-left">
|
||||
{attempt.executor || "executor"}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowAttemptHistory(false)}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedAttempt && (
|
||||
<div className="flex gap-1">
|
||||
@@ -482,116 +467,6 @@ export function TaskDetailsPanel({
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Description */}
|
||||
<div>
|
||||
<Label className="text-sm font-medium mb-2 block">
|
||||
Description
|
||||
</Label>
|
||||
<div className="p-3 bg-muted rounded-md min-h-[60px]">
|
||||
{task.description ? (
|
||||
<p className="text-sm whitespace-pre-wrap">
|
||||
{task.description}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground italic">
|
||||
No description provided
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Execution Processes */}
|
||||
{selectedAttempt && executionProcesses.length > 0 && (
|
||||
<div>
|
||||
<Label className="text-sm font-medium mb-3 block">
|
||||
Execution Processes
|
||||
</Label>
|
||||
<div className="space-y-3">
|
||||
{executionProcesses.map((process) => (
|
||||
<Card key={process.id} className="border">
|
||||
<CardContent className="p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span
|
||||
className={`px-2 py-1 rounded-full text-xs font-medium ${
|
||||
getProcessStatusDisplay(process.status)
|
||||
.className
|
||||
}`}
|
||||
>
|
||||
{
|
||||
getProcessStatusDisplay(process.status)
|
||||
.label
|
||||
}
|
||||
</span>
|
||||
<span className="font-medium text-sm">
|
||||
{getProcessTypeDisplay(
|
||||
process.process_type
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{new Date(
|
||||
process.started_at
|
||||
).toLocaleTimeString()}
|
||||
</span>
|
||||
{process.status === "running" && (
|
||||
<Button
|
||||
onClick={() =>
|
||||
stopExecutionProcess(process.id)
|
||||
}
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
>
|
||||
Stop
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(process.stdout || process.stderr) && (
|
||||
<div className="space-y-2">
|
||||
{process.stdout && (
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground mb-1 block">
|
||||
STDOUT
|
||||
</Label>
|
||||
<div
|
||||
className="bg-black text-green-400 border border-green-400 rounded-md p-2 font-mono text-xs max-h-32 overflow-y-auto whitespace-pre-wrap"
|
||||
style={{
|
||||
fontFamily:
|
||||
'ui-monospace, SFMono-Regular, "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||
}}
|
||||
>
|
||||
{process.stdout}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{process.stderr && (
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground mb-1 block">
|
||||
STDERR
|
||||
</Label>
|
||||
<div
|
||||
className="bg-black text-red-400 border border-red-400 rounded-md p-2 font-mono text-xs max-h-32 overflow-y-auto whitespace-pre-wrap"
|
||||
style={{
|
||||
fontFamily:
|
||||
'ui-monospace, SFMono-Regular, "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||
}}
|
||||
>
|
||||
{process.stderr}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Activity History */}
|
||||
{selectedAttempt && (
|
||||
<div>
|
||||
@@ -604,7 +479,7 @@ export function TaskDetailsPanel({
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{attemptActivities.map((activity) => (
|
||||
{attemptActivities.slice().reverse().map((activity) => (
|
||||
<Card key={activity.id} className="border">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
|
||||
28
frontend/src/components/ui/tooltip.tsx
Normal file
28
frontend/src/components/ui/tooltip.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 origin-[--radix-tooltip-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
Reference in New Issue
Block a user