From 3734f671009968049c00b4a95a4d292af6952cac Mon Sep 17 00:00:00 2001 From: angel Date: Thu, 10 Jul 2025 17:46:59 +0000 Subject: [PATCH] feat: integrate dnd-kit for sidebar customization and reordering functionality - Added dnd-kit dependencies to package.json and pnpm-lock.yaml. - Implemented SidebarCustomization component using dnd-kit for drag-and-drop reordering of sidebar items. - Created SortableItem component for individual sidebar items with visibility toggle. - Enhanced SidebarCustomizer component with drag-and-drop support using react-beautiful-dnd. - Updated sidebar-new component to include dynamic shortcuts for recently played albums and playlists. - Improved user experience with import/export settings functionality and toast notifications. --- .env.local | 2 +- app/components/SidebarCustomization.tsx | 310 ++++++++++++ app/components/SidebarCustomizer.tsx | 321 ++++++++++++ app/components/ihateserverside.tsx | 7 +- app/components/sidebar-new.tsx | 244 ++++++++++ app/components/sidebar.tsx | 618 +++++++----------------- app/settings/page.tsx | 4 + hooks/use-sidebar-layout.ts | 38 +- package.json | 4 + pnpm-lock.yaml | 77 ++- 10 files changed, 1168 insertions(+), 457 deletions(-) create mode 100644 app/components/SidebarCustomization.tsx create mode 100644 app/components/SidebarCustomizer.tsx create mode 100644 app/components/sidebar-new.tsx diff --git a/.env.local b/.env.local index 2f18d16..a9ea562 100644 --- a/.env.local +++ b/.env.local @@ -1 +1 @@ -NEXT_PUBLIC_COMMIT_SHA=31aec81 +NEXT_PUBLIC_COMMIT_SHA=59aae6e diff --git a/app/components/SidebarCustomization.tsx b/app/components/SidebarCustomization.tsx new file mode 100644 index 0000000..fad1709 --- /dev/null +++ b/app/components/SidebarCustomization.tsx @@ -0,0 +1,310 @@ +'use client'; + +import React, { useState } from 'react'; +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragEndEvent, +} from '@dnd-kit/core'; +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + verticalListSortingStrategy, +} from '@dnd-kit/sortable'; +import { + useSortable, +} from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Label } from '@/components/ui/label'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { Switch } from '@/components/ui/switch'; +import { Input } from '@/components/ui/input'; +import { + GripVertical, + Eye, + EyeOff, + Download, + Upload, + RotateCcw, + Search, + Home, + List, + Radio, + Users, + Disc, + Music, + Heart, + Grid3X3, + Clock, + Settings +} from 'lucide-react'; +import { useSidebarLayout, SidebarItem, SidebarItemType } from '@/hooks/use-sidebar-layout'; + +// Icon mapping +const iconMap: Record = { + search: , + home: , + queue: , + radio: , + artists: , + albums: , + playlists: , + favorites: , + browse: , + songs: , + history: , + settings: , +}; + +interface SortableItemProps { + item: SidebarItem; + onToggleVisibility: (id: SidebarItemType) => void; + showIcons: boolean; +} + +function SortableItem({ item, onToggleVisibility, showIcons }: SortableItemProps) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: item.id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; + + return ( +
+
+
+ +
+ + {showIcons && ( +
+ {iconMap[item.icon] ||
} +
+ )} + + + {item.label} + +
+ + +
+ ); +} + +export function SidebarCustomization() { + const { + settings, + reorderItems, + toggleItemVisibility, + updateShortcuts, + updateShowIcons, + exportSettings, + importSettings, + resetToDefaults, + } = useSidebarLayout(); + + const [importFile, setImportFile] = useState(null); + const [importing, setImporting] = useState(false); + const [importError, setImportError] = useState(null); + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + + if (over && active.id !== over.id) { + reorderItems(active.id as string, over.id as string); + } + }; + + const handleImportFile = async () => { + if (!importFile) return; + + setImporting(true); + setImportError(null); + + try { + await importSettings(importFile); + setImportFile(null); + // Reset file input + const fileInput = document.getElementById('settings-import') as HTMLInputElement; + if (fileInput) fileInput.value = ''; + } catch (error) { + setImportError(error instanceof Error ? error.message : 'Failed to import settings'); + } finally { + setImporting(false); + } + }; + + return ( + + + Sidebar Customization + + Customize the sidebar layout, reorder items, and manage visibility settings. + + + + {/* Show Icons Toggle */} +
+
+ +
+ Display icons next to navigation items +
+
+ +
+ + {/* Shortcut Type */} +
+ + updateShortcuts(value)} + > +
+ + +
+
+ + +
+
+ + +
+
+
+ + {/* Navigation Items Order */} +
+ +
+ Drag to reorder, click the eye icon to show/hide items +
+ + + item.id)} strategy={verticalListSortingStrategy}> +
+ {settings.items.map((item) => ( + + ))} +
+
+
+
+ + {/* Settings Import/Export */} +
+ + +
+ + +
+ setImportFile(e.target.files?.[0] || null)} + className="hidden" + /> + + + {importFile && ( + + )} +
+ + +
+ + {importFile && ( +
+ Selected: {importFile.name} +
+ )} + + {importError && ( +
+ Error: {importError} +
+ )} +
+
+
+ ); +} diff --git a/app/components/SidebarCustomizer.tsx b/app/components/SidebarCustomizer.tsx new file mode 100644 index 0000000..af64ac4 --- /dev/null +++ b/app/components/SidebarCustomizer.tsx @@ -0,0 +1,321 @@ +'use client'; + +import React, { useState } from 'react'; +import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Switch } from '@/components/ui/switch'; +import { Label } from '@/components/ui/label'; +import { Badge } from '@/components/ui/badge'; +import { GripVertical, Eye, EyeOff, Download, Upload, RotateCcw } from 'lucide-react'; +import { useSidebarLayout, SidebarItem } from '@/hooks/use-sidebar-layout'; +import { Input } from '@/components/ui/input'; +import { useToast } from '@/hooks/use-toast'; + +export function SidebarCustomizer() { + const { + settings, + updateItemOrder, + toggleItemVisibility, + updateShortcuts, + updateShowIcons, + exportSettings, + importSettings, + resetToDefaults + } = useSidebarLayout(); + const { toast } = useToast(); + const [dragEnabled, setDragEnabled] = useState(false); + + const handleDragEnd = (result: DropResult) => { + if (!result.destination) return; + + const items = Array.from(settings.items); + const [reorderedItem] = items.splice(result.source.index, 1); + items.splice(result.destination.index, 0, reorderedItem); + + updateItemOrder(items); + }; + + const handleFileImport = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + try { + await importSettings(file); + toast({ + title: "Settings imported", + description: "Your sidebar settings have been imported successfully.", + }); + } catch (error) { + toast({ + title: "Import failed", + description: "Failed to import settings. Please check the file format.", + variant: "destructive", + }); + } + + // Reset the input + event.target.value = ''; + }; + + const handleExport = () => { + exportSettings(); + toast({ + title: "Settings exported", + description: "Your settings have been downloaded as a JSON file.", + }); + }; + + const handleReset = () => { + resetToDefaults(); + toast({ + title: "Settings reset", + description: "Sidebar settings have been reset to defaults.", + }); + }; + + const getSidebarIcon = (iconId: string) => { + const iconMap: Record = { + search: ( + + + + + ), + home: ( + + + + + ), + queue: ( + + + + ), + radio: ( + + + + + ), + artists: ( + + + + + ), + albums: ( + + + + + + + ), + playlists: ( + + + + + + ), + favorites: ( + + + + ), + browse: ( + + + + + ), + songs: ( + + + + + ), + history: ( + + + + + ), + settings: ( + + + + + ), + }; + + return iconMap[iconId] || iconMap.home; + }; + + return ( +
+ {/* Global Settings */} + + + Sidebar Settings + + Customize your sidebar appearance and behavior + + + +
+ + +
+ +
+ +
+ + + +
+
+
+
+ + {/* Item Management */} + + + Sidebar Items + + Drag to reorder items and toggle visibility + + + +
+ + +
+ + + + {(provided) => ( +
+ {settings.items.map((item, index) => ( + + {(provided, snapshot) => ( +
+
+
+ +
+ {settings.showIcons && getSidebarIcon(item.icon)} + {item.label} + {!item.visible && Hidden} +
+ +
+ )} +
+ ))} + {provided.placeholder} +
+ )} +
+
+
+
+ + {/* Import/Export */} + + + Settings Management + + Export, import, or reset your settings + + + +
+ +
+ + +
+ +
+
+
+
+ ); +} diff --git a/app/components/ihateserverside.tsx b/app/components/ihateserverside.tsx index 95e2e8d..04fc6c0 100644 --- a/app/components/ihateserverside.tsx +++ b/app/components/ihateserverside.tsx @@ -79,11 +79,9 @@ const Ihateserverside: React.FC = ({ children }) => {
)} @@ -123,11 +121,8 @@ const Ihateserverside: React.FC = ({ children }) => { diff --git a/app/components/sidebar-new.tsx b/app/components/sidebar-new.tsx new file mode 100644 index 0000000..8478577 --- /dev/null +++ b/app/components/sidebar-new.tsx @@ -0,0 +1,244 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { cn } from "@/lib/utils"; +import { usePathname } from 'next/navigation'; +import { Button } from "../../components/ui/button"; +import { ScrollArea } from "../../components/ui/scroll-area"; +import Link from "next/link"; +import Image from "next/image"; +import { Playlist, Album } from "@/lib/navidrome"; +import { + Search, + Home, + List, + Radio, + Users, + Disc, + Music, + Heart, + Grid3X3, + Clock, + Settings, + Circle +} from "lucide-react"; +import { useNavidrome } from "./NavidromeContext"; +import { useRecentlyPlayedAlbums } from "@/hooks/use-recently-played-albums"; +import { useSidebarShortcuts } from "@/hooks/use-sidebar-shortcuts"; +import { useSidebarLayout, SidebarItem } from "@/hooks/use-sidebar-layout"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from "@/components/ui/context-menu"; + +// Icon mapping for sidebar items +const iconMap: Record = { + search: , + home: , + queue: , + radio: , + artists: , + albums: , + playlists: , + favorites: , + browse: , + songs: , + history: , + settings: , +}; + +interface SidebarProps extends React.HTMLAttributes { + playlists: Playlist[]; + visible?: boolean; + favoriteAlbums?: Array<{id: string, name: string, artist: string, coverArt?: string}>; + onRemoveFavoriteAlbum?: (albumId: string) => void; +} + +export function Sidebar({ className, playlists, visible = true, favoriteAlbums = [], onRemoveFavoriteAlbum }: SidebarProps) { + const pathname = usePathname(); + const { api } = useNavidrome(); + const { recentAlbums } = useRecentlyPlayedAlbums(); + const { shortcutType } = useSidebarShortcuts(); + const { settings } = useSidebarLayout(); + + if (!visible) { + return null; + } + + // Check if a route is active + const isRouteActive = (href: string): boolean => { + if (href === '/') return pathname === '/'; + return pathname.startsWith(href); + }; + + // Get visible navigation items + const visibleItems = settings.items.filter(item => item.visible); + + return ( +
+
+
+
+ {/* Main Navigation Items */} + {visibleItems.map((item) => ( + + + + ))} + + {/* Dynamic Shortcuts Section */} + {(shortcutType === 'albums' || shortcutType === 'both') && favoriteAlbums.length > 0 && ( + <> +
+ {favoriteAlbums.slice(0, 5).map((album) => ( + + + + + + + + { + e.preventDefault(); + e.stopPropagation(); + onRemoveFavoriteAlbum?.(album.id); + }} + className="text-destructive focus:text-destructive" + > + Remove from favorites + + + + ))} + + )} + + {/* Recently Played Albums */} + {(shortcutType === 'albums' || shortcutType === 'both') && recentAlbums.length > 0 && ( + <> +
+ {recentAlbums.slice(0, 5).map((album) => ( + + + + ))} + + )} + + {/* Playlists Section */} + {(shortcutType === 'playlists' || shortcutType === 'both') && playlists.length > 0 && ( + <> +
+ {playlists.slice(0, 5).map((playlist) => ( + + + + ))} + + )} +
+
+
+
+ ); +} diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx index 13dadd1..8478577 100644 --- a/app/components/sidebar.tsx +++ b/app/components/sidebar.tsx @@ -8,11 +8,24 @@ import { ScrollArea } from "../../components/ui/scroll-area"; import Link from "next/link"; import Image from "next/image"; import { Playlist, Album } from "@/lib/navidrome"; -import { ChevronLeft, ChevronRight } from "lucide-react"; +import { + Search, + Home, + List, + Radio, + Users, + Disc, + Music, + Heart, + Grid3X3, + Clock, + Settings, + Circle +} from "lucide-react"; import { useNavidrome } from "./NavidromeContext"; import { useRecentlyPlayedAlbums } from "@/hooks/use-recently-played-albums"; import { useSidebarShortcuts } from "@/hooks/use-sidebar-shortcuts"; -import { useSidebarLayout, SidebarItemType } from "@/hooks/use-sidebar-layout"; +import { useSidebarLayout, SidebarItem } from "@/hooks/use-sidebar-layout"; import { ContextMenu, ContextMenuContent, @@ -20,474 +33,209 @@ import { ContextMenuTrigger, } from "@/components/ui/context-menu"; +// Icon mapping for sidebar items +const iconMap: Record = { + search: , + home: , + queue: , + radio: , + artists: , + albums: , + playlists: , + favorites: , + browse: , + songs: , + history: , + settings: , +}; + interface SidebarProps extends React.HTMLAttributes { playlists: Playlist[]; - collapsed?: boolean; - onToggle?: () => void; visible?: boolean; favoriteAlbums?: Array<{id: string, name: string, artist: string, coverArt?: string}>; onRemoveFavoriteAlbum?: (albumId: string) => void; } -export function Sidebar({ className, playlists, collapsed = false, onToggle, visible = true, favoriteAlbums = [], onRemoveFavoriteAlbum }: SidebarProps) { +export function Sidebar({ className, playlists, visible = true, favoriteAlbums = [], onRemoveFavoriteAlbum }: SidebarProps) { const pathname = usePathname(); const { api } = useNavidrome(); const { recentAlbums } = useRecentlyPlayedAlbums(); - const { showPlaylists, showAlbums } = useSidebarShortcuts(); + const { shortcutType } = useSidebarShortcuts(); + const { settings } = useSidebarLayout(); if (!visible) { return null; } - - // Define all routes and their active states - const routes = { - isRoot: pathname === "/", - isBrowse: pathname === "/browse", - isSearch: pathname === "/search", - isQueue: pathname === "/queue", - isRadio: pathname === "/radio", - isPlaylists: pathname === "/library/playlists", - isSongs: pathname === "/library/songs", - isArtists: pathname === "/library/artists", - isAlbums: pathname === "/library/albums", - isHistory: pathname === "/history", - isFavorites: pathname === "/favorites", - isSettings: pathname === "/settings", - // Handle dynamic routes - isAlbumPage: pathname.startsWith("/album/"), - isArtistPage: pathname.startsWith("/artist/"), - isPlaylistPage: pathname.startsWith("/playlist/"), - isNewPage: pathname === "/new", + + // Check if a route is active + const isRouteActive = (href: string): boolean => { + if (href === '/') return pathname === '/'; + return pathname.startsWith(href); }; - // Helper function to determine if any sidebar route is active - // This prevents highlights on pages not defined in sidebar - const isAnySidebarRouteActive = Object.values(routes).some(Boolean); + // Get visible navigation items + const visibleItems = settings.items.filter(item => item.visible); return (
-
-

- Navigation -

- {/* Search */} - - - - - {/* Home */} - - - - - {/* Queue */} - - - - - {/* Radio */} - - - - - {/* Artists */} - - - - - {/* Albums */} - - - - - {/* Playlists */} - - - - - {/* Favorites */} - - - -
-
-
-
-

- Library -

-
- {/* Browse */} - + {/* Main Navigation Items */} + {visibleItems.map((item) => ( + - - {/* Songs */} - - - - - {/* History */} - - - - - {/* Favorite Albums Section */} - {showAlbums && favoriteAlbums.length > 0 && ( - <> -
- {favoriteAlbums.slice(0, 5).map((album) => ( - - - - - - - - { - e.preventDefault(); - e.stopPropagation(); - onRemoveFavoriteAlbum?.(album.id); - }} - className="text-destructive focus:text-destructive" - > - Remove from favorites - - - - ))} - - )} - - {/* Recently Played Albums Section */} - {showAlbums && recentAlbums.length > 0 && ( - <> -
- {recentAlbums.slice(0, 5).map((album) => { - const albumImageUrl = album.coverArt && api - ? api.getCoverArtUrl(album.coverArt, 32) - : '/play.png'; - - return ( - + ))} + + {/* Dynamic Shortcuts Section */} + {(shortcutType === 'albums' || shortcutType === 'both') && favoriteAlbums.length > 0 && ( + <> +
+ {favoriteAlbums.slice(0, 5).map((album) => ( + + + - ); - })} - - )} - - {/* Playlists Section */} - {showPlaylists && playlists.length > 0 && ( - <> -
- {playlists.slice(0, 5).map((playlist) => { - const playlistImageUrl = playlist.coverArt && api - ? api.getCoverArtUrl(playlist.coverArt, 32) - : '/play.png'; // fallback to a music icon - - return ( - - - - ); - })} - - )} -
-
-
-
-
- - - + + + + + + )} + + + ))} + + )} + + {/* Playlists Section */} + {(shortcutType === 'playlists' || shortcutType === 'both') && playlists.length > 0 && ( + <> +
+ {playlists.slice(0, 5).map((playlist) => ( + + + + ))} + + )}
diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 2a45ee7..dbc4ec8 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -11,6 +11,7 @@ import { useNavidromeConfig } from '@/app/components/NavidromeConfigContext'; import { useToast } from '@/hooks/use-toast'; import { useStandaloneLastFm } from '@/hooks/use-standalone-lastfm'; import { useSidebarShortcuts, SidebarShortcutType } from '@/hooks/use-sidebar-shortcuts'; +import { SidebarCustomization } from '@/app/components/SidebarCustomization'; import { FaServer, FaUser, FaLock, FaCheck, FaTimes, FaLastfm, FaCog } from 'react-icons/fa'; import { Settings, ExternalLink } from 'lucide-react'; @@ -693,6 +694,9 @@ const SettingsPage = () => { + {/* Sidebar Customization */} + + Appearance diff --git a/hooks/use-sidebar-layout.ts b/hooks/use-sidebar-layout.ts index f30a183..af2af92 100644 --- a/hooks/use-sidebar-layout.ts +++ b/hooks/use-sidebar-layout.ts @@ -21,6 +21,7 @@ export interface SidebarItem { label: string; visible: boolean; icon: string; // We'll use this for icon identification + href: string; // Navigation path } export interface SidebarLayoutSettings { @@ -30,18 +31,18 @@ export interface SidebarLayoutSettings { } const defaultSidebarItems: SidebarItem[] = [ - { id: 'search', label: 'Search', visible: true, icon: 'search' }, - { id: 'home', label: 'Home', visible: true, icon: 'home' }, - { id: 'queue', label: 'Queue', visible: true, icon: 'queue' }, - { id: 'radio', label: 'Radio', visible: true, icon: 'radio' }, - { id: 'artists', label: 'Artists', visible: true, icon: 'artists' }, - { id: 'albums', label: 'Albums', visible: true, icon: 'albums' }, - { id: 'playlists', label: 'Playlists', visible: true, icon: 'playlists' }, - { id: 'favorites', label: 'Favorites', visible: true, icon: 'favorites' }, - { id: 'browse', label: 'Browse', visible: true, icon: 'browse' }, - { id: 'songs', label: 'Songs', visible: true, icon: 'songs' }, - { id: 'history', label: 'History', visible: true, icon: 'history' }, - { id: 'settings', label: 'Settings', visible: true, icon: 'settings' }, + { id: 'search', label: 'Search', visible: true, icon: 'search', href: '/search' }, + { id: 'home', label: 'Home', visible: true, icon: 'home', href: '/' }, + { id: 'queue', label: 'Queue', visible: true, icon: 'queue', href: '/queue' }, + { id: 'radio', label: 'Radio', visible: true, icon: 'radio', href: '/radio' }, + { id: 'artists', label: 'Artists', visible: true, icon: 'artists', href: '/library/artists' }, + { id: 'albums', label: 'Albums', visible: true, icon: 'albums', href: '/library/albums' }, + { id: 'playlists', label: 'Playlists', visible: true, icon: 'playlists', href: '/library/playlists' }, + { id: 'favorites', label: 'Favorites', visible: true, icon: 'favorites', href: '/favorites' }, + { id: 'browse', label: 'Browse', visible: true, icon: 'browse', href: '/browse' }, + { id: 'songs', label: 'Songs', visible: true, icon: 'songs', href: '/library/songs' }, + { id: 'history', label: 'History', visible: true, icon: 'history', href: '/history' }, + { id: 'settings', label: 'Settings', visible: true, icon: 'settings', href: '/settings' }, ]; const defaultSettings: SidebarLayoutSettings = { @@ -85,6 +86,18 @@ export function useSidebarLayout() { setSettings(prev => ({ ...prev, items: newItems })); }; + const reorderItems = (activeId: string, overId: string) => { + const activeIndex = settings.items.findIndex(item => item.id === activeId); + const overIndex = settings.items.findIndex(item => item.id === overId); + + if (activeIndex !== -1 && overIndex !== -1) { + const newItems = [...settings.items]; + const [removed] = newItems.splice(activeIndex, 1); + newItems.splice(overIndex, 0, removed); + setSettings(prev => ({ ...prev, items: newItems })); + } + }; + const toggleItemVisibility = (itemId: SidebarItemType) => { setSettings(prev => ({ ...prev, @@ -173,6 +186,7 @@ export function useSidebarLayout() { return { settings, updateItemOrder, + reorderItems, toggleItemVisibility, updateShortcuts, updateShowIcons, diff --git a/package.json b/package.json index 4e60554..d07c01e 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,9 @@ "lint": "next lint" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@hookform/resolvers": "^3.9.1", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", @@ -39,6 +42,7 @@ "@radix-ui/react-toggle": "^1.1.9", "@radix-ui/react-toggle-group": "^1.1.10", "@radix-ui/react-tooltip": "^1.2.7", + "@types/react-beautiful-dnd": "^13.1.8", "axios": "^1.8.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8dc30ec..a1f7c4f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,15 @@ importers: .: dependencies: + '@dnd-kit/core': + specifier: ^6.3.1 + version: 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@dnd-kit/sortable': + specifier: ^10.0.0 + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@19.1.0) '@hookform/resolvers': specifier: ^3.9.1 version: 3.10.0(react-hook-form@7.54.2(react@19.1.0)) @@ -95,6 +104,9 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.2.7 version: 1.2.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@types/react-beautiful-dnd': + specifier: ^13.1.8 + version: 13.1.8 axios: specifier: ^1.8.2 version: 1.8.2 @@ -154,7 +166,7 @@ importers: version: 3.0.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) recharts: specifier: ^3.0.2 - version: 3.0.2(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react-is@16.13.1)(react@19.1.0)(redux@5.0.1) + version: 3.0.2(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react-is@17.0.2)(react@19.1.0)(redux@5.0.1) sonner: specifier: ^2.0.5 version: 2.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -215,6 +227,28 @@ packages: '@date-fns/tz@1.2.0': resolution: {integrity: sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==} + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/sortable@10.0.0': + resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + '@emnapi/core@1.4.3': resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==} @@ -1669,6 +1703,9 @@ packages: '@types/node@24.0.10': resolution: {integrity: sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==} + '@types/react-beautiful-dnd@13.1.8': + resolution: {integrity: sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==} + '@types/react-dom@19.1.6': resolution: {integrity: sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==} peerDependencies: @@ -3005,6 +3042,9 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-redux@9.2.0: resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} peerDependencies: @@ -3448,6 +3488,31 @@ snapshots: '@date-fns/tz@1.2.0': {} + '@dnd-kit/accessibility@3.1.1(react@19.1.0)': + dependencies: + react: 19.1.0 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@19.1.0) + '@dnd-kit/utilities': 3.2.2(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + tslib: 2.8.1 + + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@dnd-kit/utilities': 3.2.2(react@19.1.0) + react: 19.1.0 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@19.1.0)': + dependencies: + react: 19.1.0 + tslib: 2.8.1 + '@emnapi/core@1.4.3': dependencies: '@emnapi/wasi-threads': 1.0.2 @@ -4816,6 +4881,10 @@ snapshots: dependencies: undici-types: 7.8.0 + '@types/react-beautiful-dnd@13.1.8': + dependencies: + '@types/react': 19.1.8 + '@types/react-dom@19.1.6(@types/react@19.1.8)': dependencies: '@types/react': 19.1.8 @@ -6286,6 +6355,8 @@ snapshots: react-is@16.13.1: {} + react-is@17.0.2: {} + react-redux@9.2.0(@types/react@19.1.8)(react@19.1.0)(redux@5.0.1): dependencies: '@types/use-sync-external-store': 0.0.6 @@ -6339,7 +6410,7 @@ snapshots: dependencies: readable-stream: 3.6.2 - recharts@3.0.2(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react-is@16.13.1)(react@19.1.0)(redux@5.0.1): + recharts@3.0.2(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react-is@17.0.2)(react@19.1.0)(redux@5.0.1): dependencies: '@reduxjs/toolkit': 2.8.2(react-redux@9.2.0(@types/react@19.1.8)(react@19.1.0)(redux@5.0.1))(react@19.1.0) clsx: 2.1.1 @@ -6349,7 +6420,7 @@ snapshots: immer: 10.1.1 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - react-is: 16.13.1 + react-is: 17.0.2 react-redux: 9.2.0(@types/react@19.1.8)(react@19.1.0)(redux@5.0.1) reselect: 5.1.1 tiny-invariant: 1.3.3