feat: integrate Radix UI toggle components and replace custom toast implementation with Sonner

- Added @radix-ui/react-toggle and @radix-ui/react-toggle-group to package dependencies.
- Implemented ToggleGroup and Toggle components for improved UI interaction.
- Replaced custom toast notifications with Sonner for better user feedback.
- Updated CanvasPage and ProjectsPage to utilize new toast notifications.
- Removed obsolete toast context and provider.
- Enhanced ProjectsPage with a toggle view for project display (cards/table).
This commit is contained in:
2026-03-09 23:43:05 +01:00
parent 57dfcb6458
commit 3ad02cfb76
8 changed files with 557 additions and 215 deletions

View File

@@ -1,111 +0,0 @@
/**
* Simple toast: message + optional Undo action. Auto-dismisses after a delay.
*/
import React, { createContext, useCallback, useContext, useState } from 'react'
import { cn } from '@/lib/utils'
export type ToastItem = {
id: string
message: string
undo?: () => void
}
type ToastContextValue = {
toasts: ToastItem[]
addToast: (message: string, options?: { undo?: () => void; duration?: number }) => void
removeToast: (id: string) => void
}
const ToastContext = createContext<ToastContextValue | null>(null)
const TOAST_DURATION = 5000
export function ToastProvider({ children }: { children: React.ReactNode }) {
const [toasts, setToasts] = useState<ToastItem[]>([])
const timersRef = React.useRef<Record<string, ReturnType<typeof setTimeout>>>({})
const removeToast = useCallback((id: string) => {
setToasts((prev) => prev.filter((t) => t.id !== id))
const t = timersRef.current[id]
if (t) {
clearTimeout(t)
delete timersRef.current[id]
}
}, [])
const addToast = useCallback(
(message: string, options?: { undo?: () => void; duration?: number }) => {
const id = `toast-${Date.now()}-${Math.random().toString(36).slice(2)}`
const duration = options?.duration ?? TOAST_DURATION
setToasts((prev) => [...prev, { id, message, undo: options?.undo }])
const timer = setTimeout(() => removeToast(id), duration)
timersRef.current[id] = timer
},
[removeToast]
)
return (
<ToastContext.Provider value={{ toasts, addToast, removeToast }}>
{children}
<ToastViewport toasts={toasts} removeToast={removeToast} />
</ToastContext.Provider>
)
}
function ToastViewport({
toasts,
removeToast,
}: {
toasts: ToastItem[]
removeToast: (id: string) => void
}) {
if (toasts.length === 0) return null
return (
<div
className="fixed bottom-4 right-4 z-[100] flex max-w-[420px] flex-col gap-2"
role="region"
aria-label="Notifications"
>
{toasts.map((t) => (
<div
key={t.id}
className={cn(
'flex items-center justify-between gap-3 rounded-lg border bg-background px-4 py-3 text-sm shadow-lg',
'animate-in slide-in-from-bottom-2 fade-in-0'
)}
>
<span className="flex-1">{t.message}</span>
<div className="flex shrink-0 gap-2">
{t.undo && (
<button
type="button"
className="font-medium text-primary underline-offset-4 hover:underline"
onClick={() => {
t.undo?.()
removeToast(t.id)
}}
>
Undo
</button>
)}
<button
type="button"
className="text-muted-foreground hover:text-foreground"
onClick={() => removeToast(t.id)}
aria-label="Dismiss"
>
×
</button>
</div>
</div>
))}
</div>
)
}
export function useToast() {
const ctx = useContext(ToastContext)
if (!ctx) throw new Error('useToast must be used within ToastProvider')
return ctx
}

View File

@@ -0,0 +1,61 @@
"use client"
import * as React from "react"
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
import { type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { toggleVariants } from "@/components/ui/toggle"
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: "default",
variant: "default",
})
const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, children, ...props }, ref) => (
<ToggleGroupPrimitive.Root
ref={ref}
className={cn("flex items-center justify-center gap-1", className)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
))
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext)
return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
className
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
)
})
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
export { ToggleGroup, ToggleGroupItem }

View File

@@ -0,0 +1,43 @@
import * as React from "react"
import * as TogglePrimitive from "@radix-ui/react-toggle"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-9 px-2 min-w-9",
sm: "h-8 px-1.5 min-w-8",
lg: "h-10 px-2.5 min-w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => (
<TogglePrimitive.Root
ref={ref}
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
))
Toggle.displayName = TogglePrimitive.Root.displayName
export { Toggle, toggleVariants }