diff --git a/src/components/drawer-event-task.tsx b/src/components/drawer-event-task.tsx index c5200c4..7b55f86 100644 --- a/src/components/drawer-event-task.tsx +++ b/src/components/drawer-event-task.tsx @@ -11,54 +11,177 @@ import { import { Label } from "./ui/label"; import { Input } from "./ui/input"; import { Popover, PopoverTrigger, PopoverContent } from "./ui/popover"; -import { useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { TimePicker } from "./ui/time-picker/time-picker"; -import { saveTaskToLocalStorage } from "@/lib/utils/agenda"; +import { + saveTaskToLocalStorage, + updateTaskInLocalStorage, +} from "@/lib/utils/agenda"; import { Lesson } from "@/types/aurion"; import { saveUserEventToLocalStorage } from "@/lib/utils/planning"; import { toast } from "sonner"; import { format } from "date-fns"; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from "react-i18next"; +import { TaskData } from "@/types/data"; + +const combineDateAndTime = ( + targetDate: Date | undefined, + time: Date | undefined +): Date | undefined => { + if (!targetDate || !time) return undefined; + + return new Date( + targetDate.getFullYear(), + targetDate.getMonth(), + targetDate.getDate(), + time.getHours(), + time.getMinutes(), + time.getSeconds(), + time.getMilliseconds() + ); +}; + +const createDefaultDateTimes = () => { + const start = new Date(); + start.setHours(start.getHours() + 1); // Add an hour to the default time + + const date = new Date( + start.getFullYear(), + start.getMonth(), + start.getDate() + ); + + const end = new Date(start); + end.setHours(end.getHours() + 1); // Add an hour to the default time + + return { + date, + startTime: start, + endTime: end, + }; +}; + +interface DrawerEventTaskProps { + type: "event" | "task"; + onClose: () => void; + mode?: "create" | "edit"; + initialTask?: TaskData; + onSave?: () => void; + open?: boolean; + onOpenChange?: (open: boolean) => void; + hideTrigger?: boolean; +} export function DrawerEventTask({ type, onClose, -}: { - type: "event" | "task"; - onClose: () => void; -}) { - const [open, setOpen] = useState(false); + mode = "create", + initialTask, + onSave, + open: controlledOpen, + onOpenChange, + hideTrigger = false, +}: DrawerEventTaskProps) { const { t } = useTranslation(); + const isEditMode = mode === "edit"; + const isTaskDrawer = type === "task"; const [title, setTitle] = useState(""); - const [date, setDate] = useState(new Date()); - const [startTime, setStartTime] = useState(new Date()); - const [endTime, setEndTime] = useState(new Date()); + const [date, setDate] = useState(); + const [startTime, setStartTime] = useState(); + const [endTime, setEndTime] = useState(); + + const [internalOpen, setInternalOpen] = useState(false); + const isControlled = typeof controlledOpen === "boolean"; + const open = isControlled ? Boolean(controlledOpen) : internalOpen; + + const setDrawerOpen = useCallback( + (next: boolean) => { + if (!isControlled) { + setInternalOpen(next); + } + onOpenChange?.(next); + }, + [isControlled, onOpenChange] + ); + + const initializeForm = useCallback(() => { + if (isEditMode && isTaskDrawer && initialTask) { + const taskDate = new Date(initialTask.date); + setTitle(initialTask.task); + setDate(new Date(taskDate)); + setStartTime(new Date(taskDate)); + setEndTime(new Date(taskDate)); + return; + } + + const defaults = createDefaultDateTimes(); + setTitle(""); + setDate(defaults.date); + setStartTime(defaults.startTime); + setEndTime(defaults.endTime); + }, [initialTask, isEditMode, isTaskDrawer]); + + const handleOpen = useCallback(() => { + initializeForm(); + setDrawerOpen(true); + }, [initializeForm, setDrawerOpen]); + + const closeDrawer = useCallback(() => { + setDrawerOpen(false); + onClose(); + }, [onClose, setDrawerOpen]); + + const handleDrawerOpenChange = useCallback( + (next: boolean) => { + if (next) { + handleOpen(); + } else { + closeDrawer(); + } + }, + [closeDrawer, handleOpen] + ); + + const now = new Date(); + const startDateTimeForValidation = combineDateAndTime(date, startTime); + const endDateTimeForValidation = combineDateAndTime(date, endTime); + const isEventDisabled = + type === "event" && + (!startDateTimeForValidation || + !endDateTimeForValidation || + endDateTimeForValidation.getTime() <= + startDateTimeForValidation.getTime() || + startDateTimeForValidation.getTime() < now.getTime()); + const isTaskDisabled = + type === "task" && + (!startDateTimeForValidation || + startDateTimeForValidation.getTime() < now.getTime()); const handleValidate = () => { - if (!title || !date || !startTime || (type === "event" && !endTime)) + const validationNow = new Date(); + const startDateTime = combineDateAndTime(date, startTime); + const endDateTime = combineDateAndTime(date, endTime); + + if (!title || !startDateTime || (type === "event" && !endDateTime)) + return; + + if (startDateTime.getTime() < validationNow.getTime()) return; + if ( + type === "event" && + endDateTime && + endDateTime.getTime() <= startDateTime.getTime() + ) return; switch (type) { case "event": { - if (!endTime) return; + if (!endDateTime) return; const newUserEvent: Lesson = { id: crypto.randomUUID(), title: title, - start: new Date( - date.getFullYear(), - date.getMonth(), - date.getDate(), - startTime.getHours(), - startTime.getMinutes() - ).toISOString(), - end: new Date( - date.getFullYear(), - date.getMonth(), - date.getDate(), - endTime.getHours(), - endTime.getMinutes() - ).toISOString(), + start: startDateTime.toISOString(), + end: endDateTime.toISOString(), allDay: false, editable: true, className: "est-perso", @@ -72,60 +195,84 @@ export function DrawerEventTask({ } case "task": { - saveTaskToLocalStorage({ - task: { - id: crypto.randomUUID(), + if (isEditMode && initialTask) { + const updatedTask: TaskData = { + ...initialTask, task: title, - date: new Date(startTime), - }, - }); - toast.success("Tâche ajoutée", { - description: "La tâche a été ajoutée à l’agenda.", - duration: 3000, - }); + date: startDateTime, + }; + updateTaskInLocalStorage({ task: updatedTask }); + toast.success(t("agendaPage.taskUpdated"), { + description: t( + "agendaPage.taskUpdatedDescription" + ), + duration: 3000, + }); + } else { + saveTaskToLocalStorage({ + task: { + id: crypto.randomUUID(), + task: title, + date: startDateTime, + }, + }); + toast.success("Tâche ajoutée", { + description: "La tâche a été ajoutée à l’agenda.", + duration: 3000, + }); + } break; } default: break; } - handleClose(); - }; - const handleClose = () => { - onClose(); - setOpen(false); - setTitle(""); - setDate(new Date()); - setStartTime(new Date()); - setEndTime(new Date()); + onSave?.(); + closeDrawer(); }; + const drawerTitle = + type === "event" + ? isEditMode + ? "Modifier l'événement" + : "Ajouter un événement" + : isEditMode + ? t("agendaPage.editTaskTitle") + : t("agendaPage.addTask"); + + const submitLabel = + type === "event" + ? isEditMode + ? "Mettre à jour l'événement" + : "Créer l'événement" + : isEditMode + ? t("agendaPage.updateTaskBtn") + : t("agendaPage.createTaskBtn"); + return ( - - - - + + {!hideTrigger && ( + + + + )} - - {type === "event" - ? "Ajouter un événement" - : "Ajouter une tâche"} - + {drawerTitle}
setTitle(e.target.value)} /> @@ -134,24 +281,28 @@ export function DrawerEventTask({
setDate(date)} + value={date} + onChange={setDate} />
{type === "task" ? ( setStartTime(time)} + label={t("agendaPage.hour")} + value={startTime} + onChange={setStartTime} /> ) : ( <> setStartTime(time)} + value={startTime} + onChange={setStartTime} /> setEndTime(time)} + value={endTime} + onChange={setEndTime} /> )} @@ -165,15 +316,11 @@ export function DrawerEventTask({ !title || !date || !startTime || - (endTime && - (startTime.getTime() === endTime.getTime() || - startTime.getTime() > endTime.getTime())) || - (type === "event" && !endTime) + isEventDisabled || + isTaskDisabled } > - {type === "event" - ? "Créer l'événement" - : "Créer la tâche"} + {submitLabel}
@@ -182,14 +329,22 @@ export function DrawerEventTask({ } const DatePickerComponent = ({ + value, onChange, }: { + value: Date | undefined; onChange: (date: Date | undefined) => void; }) => { - const [date, setDate] = useState(new Date()); + const [date, setDate] = useState(() => + value ? new Date(value) : undefined + ); const [open, setOpen] = useState(false); const { t } = useTranslation(); + useEffect(() => { + setDate(value ? new Date(value) : undefined); + }, [value]); + const handleDateChange = (selectedDate: Date | undefined) => { setDate(selectedDate); onChange(selectedDate); @@ -233,16 +388,17 @@ const DatePickerComponent = ({ const TimePickerComponent = ({ label, + value, onChange, }: { label: string; - onChange: (time: Date) => void; + value: Date | undefined; + onChange: (time: Date | undefined) => void; }) => { - const [time, setTime] = useState(new Date()); + const time = value ? new Date(value) : new Date(); const handleTimeChange = (newTime: Date | undefined) => { if (!newTime) return; - setTime(newTime); onChange(newTime); }; diff --git a/src/components/sidebar.tsx b/src/components/sidebar.tsx index 5c98d1e..842fbcb 100644 --- a/src/components/sidebar.tsx +++ b/src/components/sidebar.tsx @@ -1,7 +1,7 @@ /* eslint-disable i18next/no-literal-string */ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useState, type CSSProperties } from "react"; import { Moon, Sun, @@ -219,7 +219,14 @@ export default function Sidebar() { {t("sidebar.title")} diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx index 52128ab..9f45f03 100644 --- a/src/components/ui/sheet.tsx +++ b/src/components/ui/sheet.tsx @@ -80,8 +80,13 @@ const SheetContent = React.forwardRef< {...props} > {children} - - + + diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx index 5f2c25d..0c27d79 100644 --- a/src/components/ui/sonner.tsx +++ b/src/components/ui/sonner.tsx @@ -1,67 +1,74 @@ -"use client" +"use client"; -import { useTheme } from "@/components/theme-provider" -import { Toaster as Sonner } from "sonner" +import { useTheme } from "@/components/theme-provider"; +import { Toaster as Sonner } from "sonner"; -type ToasterProps = React.ComponentProps +type ToasterProps = React.ComponentProps; -type OffsetObject = Exclude +type OffsetObject = Exclude; const SAFE_AREA_OFFSET: OffsetObject = { - top: "calc(var(--safe-area-top, 50px) + 1rem)", - right: "calc(var(--safe-area-right, 0px) + 1rem)", - bottom: "calc(var(--safe-area-bottom, 30px) + 1rem)", - left: "calc(var(--safe-area-left, 0px) + 1rem)", -} + top: "calc(var(--safe-area-top, 50px) + 1rem)", + right: "calc(var(--safe-area-right, 0px) + 1rem)", + bottom: "calc(var(--safe-area-bottom, 30px) + 1rem)", + left: "calc(var(--safe-area-left, 0px) + 1rem)", +}; const mergeClassName = (base: string, extra?: string) => - [base, extra].filter(Boolean).join(" ") + [base, extra].filter(Boolean).join(" "); -const resolveOffset = (value?: ToasterProps["offset"]): ToasterProps["offset"] => { - if (!value) return { ...SAFE_AREA_OFFSET } - if (typeof value === "number" || typeof value === "string") return value - return { - ...SAFE_AREA_OFFSET, - ...value, - } -} +const resolveOffset = ( + value?: ToasterProps["offset"] +): ToasterProps["offset"] => { + if (!value) return { ...SAFE_AREA_OFFSET }; + if (typeof value === "number" || typeof value === "string") return value; + return { + ...SAFE_AREA_OFFSET, + ...value, + }; +}; -const Toaster = ({ toastOptions, offset, mobileOffset, ...props }: ToasterProps) => { - const { theme = "system" } = useTheme() +const Toaster = ({ + toastOptions, + offset, + mobileOffset, + ...props +}: ToasterProps) => { + const { theme = "system" } = useTheme(); - const mergedToastOptions: ToasterProps["toastOptions"] = { - ...toastOptions, - classNames: { - ...toastOptions?.classNames, - toast: mergeClassName( - "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg", - toastOptions?.classNames?.toast - ), - description: mergeClassName( - "group-[.toast]:text-muted-foreground", - toastOptions?.classNames?.description - ), - actionButton: mergeClassName( - "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground", - toastOptions?.classNames?.actionButton - ), - cancelButton: mergeClassName( - "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground", - toastOptions?.classNames?.cancelButton - ), - }, - } + const mergedToastOptions: ToasterProps["toastOptions"] = { + ...toastOptions, + classNames: { + ...toastOptions?.classNames, + toast: mergeClassName( + "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg", + toastOptions?.classNames?.toast + ), + description: mergeClassName( + "group-[.toast]:text-muted-foreground", + toastOptions?.classNames?.description + ), + actionButton: mergeClassName( + "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground", + toastOptions?.classNames?.actionButton + ), + cancelButton: mergeClassName( + "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground", + toastOptions?.classNames?.cancelButton + ), + }, + }; - return ( - - ) -} + return ( + + ); +}; -export { Toaster } +export { Toaster }; diff --git a/src/lib/utils/agenda.ts b/src/lib/utils/agenda.ts index fdbb635..08435c4 100644 --- a/src/lib/utils/agenda.ts +++ b/src/lib/utils/agenda.ts @@ -1,16 +1,21 @@ import { TaskData } from "@/types/data"; import { getFromStorage, saveToStorage } from "./storage"; +type StoredTask = Omit & { date: string }; + export function saveTaskToLocalStorage({ task }: { task: TaskData }) { const existingTasks = JSON.parse( getFromStorage("tasks") ?? "[]" - ) as TaskData[]; - existingTasks.push(task); + ) as StoredTask[]; + existingTasks.push({ + ...task, + date: new Date(task.date).toISOString(), + }); saveToStorage("tasks", JSON.stringify(existingTasks)); } export function getTasksFromLocalStorage(): TaskData[] { - const tasks = JSON.parse(getFromStorage("tasks") ?? "[]") as TaskData[]; + const tasks = JSON.parse(getFromStorage("tasks") ?? "[]") as StoredTask[]; return tasks.map((task) => ({ ...task, date: new Date(task.date), @@ -20,7 +25,25 @@ export function getTasksFromLocalStorage(): TaskData[] { export function removeTaskFromLocalStorage({ taskId }: { taskId: string }) { const existingTasks = JSON.parse( getFromStorage("tasks") ?? "[]" - ) as TaskData[]; + ) as StoredTask[]; const updatedTasks = existingTasks.filter((task) => task.id !== taskId); saveToStorage("tasks", JSON.stringify(updatedTasks)); } + +export function updateTaskInLocalStorage({ task }: { task: TaskData }) { + const existingTasks = JSON.parse( + getFromStorage("tasks") ?? "[]" + ) as StoredTask[]; + + const updatedTasks = existingTasks.map((storedTask) => + storedTask.id === task.id + ? { + ...storedTask, + task: task.task, + date: new Date(task.date).toISOString(), + } + : storedTask + ); + + saveToStorage("tasks", JSON.stringify(updatedTasks)); +} diff --git a/src/locales/en-US.json b/src/locales/en-US.json index 594edab..1f729a6 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -135,7 +135,13 @@ "taskDescription": "Enter the title", "date": "Date", "hour": "Hour", - "createTaskBtn": "Create task" + "createTaskBtn": "Create task", + "markTaskDone": "Mark as done", + "undoTaskCompletion": "Undo completion", + "editTaskTitle": "Edit task", + "updateTaskBtn": "Update task", + "taskUpdated": "Task updated", + "taskUpdatedDescription": "Your changes have been saved." }, "associationsPage": { "search": "Find an association", diff --git a/src/locales/es-ES.json b/src/locales/es-ES.json index fb034af..1488c91 100644 --- a/src/locales/es-ES.json +++ b/src/locales/es-ES.json @@ -135,7 +135,13 @@ "taskDescription": "Introduce el título", "date": "Fecha", "hour": "Hora", - "createTaskBtn": "Crear tarea" + "createTaskBtn": "Crear tarea", + "markTaskDone": "Marcar como completada", + "undoTaskCompletion": "Deshacer la finalización", + "editTaskTitle": "Editar tarea", + "updateTaskBtn": "Actualizar tarea", + "taskUpdated": "Tarea actualizada", + "taskUpdatedDescription": "Los cambios se han guardado." }, "associationsPage": { "search": "Buscar una asociación", diff --git a/src/locales/fr-FR.json b/src/locales/fr-FR.json index ddf7a38..71d9b82 100644 --- a/src/locales/fr-FR.json +++ b/src/locales/fr-FR.json @@ -135,7 +135,13 @@ "taskDescription": "Entrez le titre", "date": "Date", "hour": "Heure", - "createTaskBtn": "Créer la tâche" + "createTaskBtn": "Créer la tâche", + "markTaskDone": "Marquer comme effectuée", + "undoTaskCompletion": "Annuler la complétion", + "editTaskTitle": "Modifier la tâche", + "updateTaskBtn": "Mettre à jour la tâche", + "taskUpdated": "Tâche mise à jour", + "taskUpdatedDescription": "Les modifications ont été enregistrées." }, "associationsPage": { "search": "Rechercher une association", diff --git a/src/pages/secondary/agenda.tsx b/src/pages/secondary/agenda.tsx index bb9d5db..542eb9d 100644 --- a/src/pages/secondary/agenda.tsx +++ b/src/pages/secondary/agenda.tsx @@ -3,29 +3,132 @@ import { Button } from "@/components/ui/button"; import { getTasksFromLocalStorage, removeTaskFromLocalStorage, + saveTaskToLocalStorage, } from "@/lib/utils/agenda"; import { TaskData } from "@/types/data"; import { format } from "date-fns"; import { fr } from "date-fns/locale"; -import { Calendar, Check, ClipboardListIcon } from "lucide-react"; -import { useState } from "react"; -import { useTranslation } from 'react-i18next'; +import { Calendar, Check, ClipboardListIcon, Undo2 } from "lucide-react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { cn } from "@/lib/utils/cn"; export function AgendaPage() { - const [tasks, setTasks] = useState(getTasksFromLocalStorage()); + const [tasks, setTasks] = useState(() => + getTasksFromLocalStorage() + ); + const [pendingTaskIds, setPendingTaskIds] = useState< + Record + >({}); + const pendingTimeoutsRef = useRef< + Map> + >(new Map()); + const pendingTasksRef = useRef>(new Map()); + const [editingTask, setEditingTask] = useState(null); + const [isEditDrawerOpen, setIsEditDrawerOpen] = useState(false); const { t } = useTranslation(); - const handleTaskComplete = (taskId: string) => { + const refreshTasks = useCallback(() => { + const storedTasks = getTasksFromLocalStorage(); + const pendingTasks = Array.from(pendingTasksRef.current.values()); + const combined = [ + ...storedTasks, + ...pendingTasks.filter( + (pendingTask) => + !storedTasks.some((task) => task.id === pendingTask.id) + ), + ]; + combined.sort( + (a, b) => a.date.getTime() - b.date.getTime() + ); + setTasks(combined); + }, []); + + const finalizeTaskCompletion = useCallback( + (taskId: string) => { + pendingTimeoutsRef.current.delete(taskId); + pendingTasksRef.current.delete(taskId); + setPendingTaskIds((prev) => { + const { [taskId]: _removed, ...rest } = prev; + return rest; + }); + setTasks((prev) => prev.filter((task) => task.id !== taskId)); + if (editingTask?.id === taskId) { + setIsEditDrawerOpen(false); + setEditingTask(null); + } + }, + [editingTask] + ); + + const handleTaskCompletionToggle = (taskId: string) => { + const isPending = Boolean(pendingTaskIds[taskId]); + + if (isPending) { + const existingTimeout = pendingTimeoutsRef.current.get(taskId); + if (existingTimeout) { + clearTimeout(existingTimeout); + } + pendingTimeoutsRef.current.delete(taskId); + const pendingTask = pendingTasksRef.current.get(taskId); + if (pendingTask) { + saveTaskToLocalStorage({ task: pendingTask }); + } + pendingTasksRef.current.delete(taskId); + setPendingTaskIds((prev) => { + const { [taskId]: _removed, ...rest } = prev; + return rest; + }); + refreshTasks(); + return; + } + + const taskToComplete = tasks.find((task) => task.id === taskId); + if (!taskToComplete) return; + pendingTasksRef.current.set(taskId, taskToComplete); removeTaskFromLocalStorage({ taskId }); - setTasks(getTasksFromLocalStorage()); + if (editingTask?.id === taskId) { + setIsEditDrawerOpen(false); + setEditingTask(null); + } + setPendingTaskIds((prev) => ({ ...prev, [taskId]: true })); + const timeoutId = setTimeout( + () => finalizeTaskCompletion(taskId), + 5000 + ); + pendingTimeoutsRef.current.set(taskId, timeoutId); }; + const handleTaskCardClick = useCallback( + (task: TaskData) => { + if (pendingTaskIds[task.id]) return; + setEditingTask(task); + setIsEditDrawerOpen(true); + }, + [pendingTaskIds] + ); + + useEffect(() => { + return () => { + pendingTimeoutsRef.current.forEach((timeoutId) => + clearTimeout(timeoutId) + ); + pendingTimeoutsRef.current.clear(); + }; + }, []); + + useEffect(() => { + refreshTasks(); + }, [refreshTasks]); + return (
{/* Header Section */}
-

{t("agendaPage.title")}

+

+ {t("agendaPage.title")} +

@@ -39,46 +142,91 @@ export function AgendaPage() { {/* Tasks List */} {tasks.length > 0 ? (
- {tasks.map((task: TaskData, index: number) => ( -
-
-
-
-

- {task.task} -

-
-
- - - {format( - task.date, - "EEEE d MMM p", - { locale: fr } - )} - + {tasks.map((task: TaskData) => { + const isPending = Boolean(pendingTaskIds[task.id]); + return ( +
handleTaskCardClick(task)} + > +
+
+
+

+ {task.task} +

+
+
+ + + {format( + task.date, + "EEEE d MMM p", + { locale: fr } + )} + +
-
-
- +
+ +
-
- ))} + ); + })}
) : (
@@ -96,11 +244,21 @@ export function AgendaPage() {
)}
+ { - setTasks(getTasksFromLocalStorage()); + mode="edit" + hideTrigger + initialTask={editingTask ?? undefined} + open={isEditDrawerOpen && Boolean(editingTask)} + onOpenChange={(open) => { + setIsEditDrawerOpen(open); + if (!open) { + setEditingTask(null); + } }} + onClose={refreshTasks} + onSave={refreshTasks} />
); diff --git a/src/styles/globals.css b/src/styles/globals.css index 7338ecb..4bce13c 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -22,7 +22,7 @@ overscroll-behavior: none; } :root { - --safe-area-top: 40px; + --safe-area-top: 50px; --safe-area-bottom: 30px; --safe-area-left: env(safe-area-inset-left, 0px); --safe-area-right: env(safe-area-inset-right, 0px); @@ -463,6 +463,32 @@ } } +@keyframes task-complete-fade { + 0% { + opacity: 1; + transform: translateY(0); + } + 70% { + opacity: 0.9; + transform: translateY(-2px); + } + 90% { + opacity: 0.4; + transform: translateY(-8px); + } + 100% { + opacity: 0; + transform: translateY(-12px); + } +} + +@layer utilities { + .animate-task-complete { + animation: task-complete-fade 5s ease-in-out forwards; + will-change: opacity, transform; + } +} + /* The default border color has changed to `currentcolor` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still