feat: Initialize core frontend application, editor layout, and integrate UI components including alert dialogs and toasts.

This commit is contained in:
SaelixCode
2026-02-25 10:18:59 -05:00
parent cdecbba59a
commit ac6b8e2849
8 changed files with 361 additions and 48 deletions
+69
View File
@@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"@monaco-editor/react": "^4.7.0",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-scroll-area": "^1.2.10",
@@ -19,8 +20,10 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.575.0",
"next-themes": "^0.4.6",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^3.4.19",
"tailwindcss-animate": "^1.0.7"
@@ -1104,6 +1107,52 @@
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-alert-dialog": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz",
"integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dialog": "1.1.15",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "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-alert-dialog/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collection": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
@@ -3810,6 +3859,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/next-themes": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
"integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
@@ -4449,6 +4508,16 @@
"node": ">=8"
}
},
"node_modules/sonner": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
"integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+3
View File
@@ -11,6 +11,7 @@
},
"dependencies": {
"@monaco-editor/react": "^4.7.0",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-scroll-area": "^1.2.10",
@@ -21,8 +22,10 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.575.0",
"next-themes": "^0.4.6",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^3.4.19",
"tailwindcss-animate": "^1.0.7"
+3
View File
@@ -33,10 +33,13 @@ function AppContent() {
return <EditorLayout />;
}
import { Toaster } from 'sonner';
function App() {
return (
<AuthProvider>
<AppContent />
<Toaster position="top-right" richColors />
</AuthProvider>
);
}
+24 -19
View File
@@ -7,13 +7,16 @@ import BashExecModal from './BashExecModal';
import MaintenanceModal from './MaintenanceModal';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription, DialogTrigger } from './ui/dialog';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogTrigger } from './ui/dialog';
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from './ui/alert-dialog';
import { Tabs, TabsList, TabsTrigger } from './ui/tabs';
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
import { Badge } from './ui/badge';
import { Plus, Trash2, Play, Square, Save, Terminal, Sun, Moon, RotateCw, CloudDownload, Pencil, X, Search, Home, LogOut, Brush } from 'lucide-react';
import { useAuth } from '@/context/AuthContext';
import { apiFetch } from '@/lib/api';
import { toast } from 'sonner';
import { Label } from './ui/label';
interface ContainerInfo {
Id: string;
@@ -209,10 +212,10 @@ export default function EditorLayout() {
setOriginalEnvContent(envContent);
}
setIsEditing(false);
alert('File saved successfully!');
toast.success('File saved successfully!');
} catch (error) {
console.error('Failed to save file:', error);
alert(`Failed to save file: ${(error as Error).message}`);
toast.error(`Failed to save file: ${(error as Error).message}`);
}
};
@@ -362,7 +365,7 @@ export default function EditorLayout() {
await refreshStacks();
} catch (error) {
console.error('Failed to delete stack:', error);
alert('Failed to delete stack');
toast.error('Failed to delete stack');
} finally {
setIsActionLoading(false);
}
@@ -392,7 +395,7 @@ export default function EditorLayout() {
await loadFile(stackName);
} catch (error: any) {
console.error('Failed to create stack:', error);
alert(error.message || 'Failed to create stack');
toast.error(error.message || 'Failed to create stack');
}
};
@@ -460,8 +463,10 @@ export default function EditorLayout() {
<DialogHeader>
<DialogTitle>Create New Stack</DialogTitle>
</DialogHeader>
<div className="py-4">
<div className="py-4 space-y-2">
<Label htmlFor="create-stack-name">Stack Name</Label>
<Input
id="create-stack-name"
placeholder="Stack name (e.g., myapp)"
value={newStackName}
onChange={(e) => setNewStackName(e.target.value)}
@@ -764,20 +769,20 @@ export default function EditorLayout() {
</div>
{/* Delete Confirmation Dialog */}
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Stack</DialogTitle>
<DialogDescription>
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Stack</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete {stackToDelete}? This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>Cancel</Button>
<Button variant="destructive" onClick={deleteStack}>Delete</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setDeleteDialogOpen(false)}>Cancel</AlertDialogCancel>
<AlertDialogAction className="bg-destructive text-destructive-foreground hover:bg-destructive/90" onClick={deleteStack}>Delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Bash Exec Modal */}
{selectedContainer && (
+23 -12
View File
@@ -5,6 +5,8 @@ import { Input } from './ui/input';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from './ui/dialog';
import { Activity, Square, PauseCircle, ArrowRight, Plus, Cpu, HardDrive, MemoryStick } from 'lucide-react';
import { apiFetch } from '@/lib/api';
import { toast } from 'sonner';
import { Label } from './ui/label';
interface Stats {
active: number;
@@ -96,7 +98,7 @@ export default function HomeDashboard() {
setConvertedYaml(data.yaml);
} catch (error) {
console.error('Conversion error:', error);
alert('Failed to convert docker run command');
toast.error('Failed to convert docker run command');
} finally {
setIsConverting(false);
}
@@ -128,7 +130,7 @@ export default function HomeDashboard() {
window.location.reload(); // Refresh to show new stack
} catch (error) {
console.error('Failed to create stack:', error);
alert('Failed to create stack');
toast.error('Failed to create stack');
}
};
@@ -201,7 +203,7 @@ export default function HomeDashboard() {
{systemStats ? `${systemStats.memory.usagePercent}%` : '...'}
</div>
<p className="text-xs text-muted-foreground mt-1">
{systemStats
{systemStats
? `${formatBytes(systemStats.memory.used)} / ${formatBytes(systemStats.memory.total)}`
: 'Loading...'}
</p>
@@ -218,7 +220,7 @@ export default function HomeDashboard() {
{systemStats?.disk ? `${systemStats.disk.usagePercent}%` : '...'}
</div>
<p className="text-xs text-muted-foreground mt-1">
{systemStats?.disk
{systemStats?.disk
? `${formatBytes(systemStats.disk.used)} / ${formatBytes(systemStats.disk.total)}`
: 'Loading...'}
</p>
@@ -241,7 +243,7 @@ export default function HomeDashboard() {
value={dockerRunInput}
onChange={(e) => setDockerRunInput(e.target.value)}
/>
<div className="flex gap-2">
<Button onClick={handleConvert} disabled={isConverting || !dockerRunInput.trim()}>
{isConverting ? 'Converting...' : 'Convert'}
@@ -270,14 +272,23 @@ export default function HomeDashboard() {
<DialogTitle>Create New Stack</DialogTitle>
</DialogHeader>
<div className="py-4 space-y-4">
<Input
placeholder="Stack name (e.g., myapp)"
value={newStackName}
onChange={(e) => setNewStackName(e.target.value)}
/>
<div className="p-3 rounded-lg bg-muted/50 font-mono text-sm whitespace-pre-wrap overflow-auto max-h-48">
{convertedYaml}
<div className="space-y-2">
<Label htmlFor="stack-name">Stack Name</Label>
<Input
id="stack-name"
placeholder="e.g., myapp"
value={newStackName}
onChange={(e) => setNewStackName(e.target.value)}
/>
</div>
{convertedYaml && (
<div className="space-y-2">
<Label>Converted Compose File (Preview)</Label>
<div className="p-3 rounded-lg bg-muted/50 font-mono text-sm whitespace-pre-wrap overflow-auto max-h-48">
{convertedYaml}
</div>
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setCreateDialogOpen(false)}>Cancel</Button>
+71 -17
View File
@@ -6,10 +6,21 @@ import {
DialogTitle,
DialogDescription,
} from './ui/dialog';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "./ui/alert-dialog"
import { Tabs, TabsList, TabsTrigger, TabsContent } from './ui/tabs';
import { Button } from './ui/button';
import { Badge } from './ui/badge';
import { apiFetch } from '@/lib/api';
import { toast } from 'sonner';
import { Trash2, AlertTriangle, MonitorX, PackageMinus, Network } from 'lucide-react';
interface MaintenanceModalProps {
@@ -38,6 +49,11 @@ export default function MaintenanceModal({ isOpen, onClose }: MaintenanceModalPr
const [isPruning, setIsPruning] = useState(false);
const [pruneResult, setPruneResult] = useState<{ message: string; stdout: string; stderr: string } | null>(null);
// Confirm Modals state
const [purgeConfirmOpen, setPurgeConfirmOpen] = useState(false);
const [pruneConfirmOpen, setPruneConfirmOpen] = useState(false);
const [pruneTarget, setPruneTarget] = useState<'containers' | 'images' | 'networks' | null>(null);
useEffect(() => {
if (isOpen && activeTab === 'ghosts') {
fetchOrphans();
@@ -77,13 +93,13 @@ export default function MaintenanceModal({ isOpen, onClose }: MaintenanceModalPr
}
};
const purgeSelectedOrphans = async () => {
const requestPurgeOrphans = () => {
if (selectedOrphans.length === 0) return;
setPurgeConfirmOpen(true);
};
if (!confirm(`Are you sure you want to forcefully remove ${selectedOrphans.length} ghost container(s) ? `)) {
return;
}
const confirmPurgeOrphans = async () => {
setPurgeConfirmOpen(false);
setIsPurging(true);
try {
const res = await apiFetch('/system/prune/orphans', {
@@ -93,33 +109,39 @@ export default function MaintenanceModal({ isOpen, onClose }: MaintenanceModalPr
if (!res.ok) throw new Error('Purge failed');
await fetchOrphans(); // Refresh the list
toast.success(`Purged ${selectedOrphans.length} ghost container(s)`);
} catch (error) {
console.error('Failed to purge orphans:', error);
alert('Failed to purge selected containers.');
toast.error('Failed to purge selected containers.');
} finally {
setIsPurging(false);
}
};
const pruneSystem = async (target: 'containers' | 'images' | 'networks') => {
if (!confirm(`Are you sure you want to prune all unused ${target}? This cannot be undone.`)) {
return;
}
const requestPruneSystem = (target: 'containers' | 'images' | 'networks') => {
setPruneTarget(target);
setPruneConfirmOpen(true);
};
const confirmPruneSystem = async () => {
if (!pruneTarget) return;
setPruneConfirmOpen(false);
setIsPruning(true);
setPruneResult(null);
try {
const res = await apiFetch('/system/prune/system', {
method: 'POST',
body: JSON.stringify({ target })
body: JSON.stringify({ target: pruneTarget })
});
const data = await res.json();
setPruneResult(data);
toast.success(`Successfully pruned ${pruneTarget}`);
} catch (error) {
console.error(`Failed to prune ${target}: `, error);
alert(`Failed to prune ${target}.`);
console.error(`Failed to prune ${pruneTarget}: `, error);
toast.error(`Failed to prune ${pruneTarget}.`);
} finally {
setIsPruning(false);
setPruneTarget(null);
}
};
@@ -165,7 +187,7 @@ export default function MaintenanceModal({ isOpen, onClose }: MaintenanceModalPr
<Button
variant="destructive"
size="sm"
onClick={purgeSelectedOrphans}
onClick={requestPurgeOrphans}
disabled={selectedOrphans.length === 0 || isPurging}
>
<Trash2 className="w-4 h-4 mr-2" />
@@ -240,7 +262,7 @@ export default function MaintenanceModal({ isOpen, onClose }: MaintenanceModalPr
<Button
variant="outline"
className="h-24 flex flex-col items-center justify-center gap-2"
onClick={() => pruneSystem('containers')}
onClick={() => requestPruneSystem('containers')}
disabled={isPruning}
>
<MonitorX className="w-6 h-6 text-orange-500" />
@@ -253,7 +275,7 @@ export default function MaintenanceModal({ isOpen, onClose }: MaintenanceModalPr
<Button
variant="outline"
className="h-24 flex flex-col items-center justify-center gap-2"
onClick={() => pruneSystem('images')}
onClick={() => requestPruneSystem('images')}
disabled={isPruning}
>
<PackageMinus className="w-6 h-6 text-blue-500" />
@@ -266,7 +288,7 @@ export default function MaintenanceModal({ isOpen, onClose }: MaintenanceModalPr
<Button
variant="outline"
className="h-24 flex flex-col items-center justify-center gap-2"
onClick={() => pruneSystem('networks')}
onClick={() => requestPruneSystem('networks')}
disabled={isPruning}
>
<Network className="w-6 h-6 text-green-500" />
@@ -288,6 +310,38 @@ export default function MaintenanceModal({ isOpen, onClose }: MaintenanceModalPr
</TabsContent>
</Tabs>
</DialogContent>
{/* Purge Ghost Containers Confirmation */}
<AlertDialog open={purgeConfirmOpen} onOpenChange={setPurgeConfirmOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Forcefully Remove Containers</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to forcefully remove {selectedOrphans.length} ghost container(s)?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setPurgeConfirmOpen(false)}>Cancel</AlertDialogCancel>
<AlertDialogAction className="bg-destructive text-destructive-foreground hover:bg-destructive/90" onClick={confirmPurgeOrphans}>Remove</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Prune System Confirmation */}
<AlertDialog open={pruneConfirmOpen} onOpenChange={setPruneConfirmOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Prune {pruneTarget}</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to prune all unused {pruneTarget}? This cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setPruneConfirmOpen(false)}>Cancel</AlertDialogCancel>
<AlertDialogAction className="bg-destructive text-destructive-foreground hover:bg-destructive/90" onClick={confirmPruneSystem}>Prune</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Dialog>
);
}
+139
View File
@@ -0,0 +1,139 @@
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}
+29
View File
@@ -0,0 +1,29 @@
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
)
}
export { Toaster }