diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 47e7b7d..1cd5583 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -40,21 +40,14 @@ export default function Sidebar() { .find((cat) => cat.id === 'main') ?.modules.filter((m) => { if (!m.enabled) return false; - // Dashboard is always shown - if (m.id === 'dashboard') return true; if (shouldUseUserPermissions) { return isModuleEnabledForUser(m.id, user?.permissions, user?.is_superuser || false); } return isModuleEnabled(m.id); }) || []); - // Sort modules based on moduleOrder (dashboard always first, then ordered features) + // Sort modules based on moduleOrder const sortedModules = [...mainModulesFiltered].sort((a, b) => { - // Dashboard always comes first - if (a.id === 'dashboard') return -1; - if (b.id === 'dashboard') return 1; - - // Sort other modules by moduleOrder const aIndex = moduleOrder.indexOf(a.id); const bIndex = moduleOrder.indexOf(b.id); @@ -69,13 +62,11 @@ export default function Sidebar() { // Split modules by position (top = main nav, bottom = above footer) const topModules = sortedModules.filter(m => { - if (m.id === 'dashboard') return true; // Dashboard always at top const state = moduleStates[m.id as keyof typeof moduleStates]; return !state || state.position === 'top'; }); const bottomModules = sortedModules.filter(m => { - if (m.id === 'dashboard') return false; // Dashboard never at bottom const state = moduleStates[m.id as keyof typeof moduleStates]; return state && state.position === 'bottom'; }); diff --git a/frontend/src/contexts/ModulesContext.tsx b/frontend/src/contexts/ModulesContext.tsx index 94788ab..096a62d 100644 --- a/frontend/src/contexts/ModulesContext.tsx +++ b/frontend/src/contexts/ModulesContext.tsx @@ -6,6 +6,7 @@ import type { UserPermissions } from '../types'; // User-facing modules that can be toggled export const TOGGLEABLE_MODULES = [ + { id: 'dashboard', icon: 'dashboard', defaultEnabled: true, defaultPosition: 'top' as const }, { id: 'feature1', icon: 'playlist_play', defaultEnabled: true, defaultPosition: 'top' as const }, { id: 'feature2', icon: 'download', defaultEnabled: true, defaultPosition: 'top' as const }, { id: 'feature3', icon: 'cast', defaultEnabled: true, defaultPosition: 'top' as const }, @@ -23,7 +24,7 @@ export interface ModuleState { } // Default order for modules (top modules, then bottom modules) -const DEFAULT_MODULE_ORDER: string[] = ['feature1', 'feature2', 'feature3', 'search', 'notifications']; +const DEFAULT_MODULE_ORDER: string[] = ['dashboard', 'feature1', 'feature2', 'feature3', 'search', 'notifications']; interface ModulesContextType { moduleStates: Record; @@ -123,12 +124,14 @@ export function ModulesProvider({ children }: { children: ReactNode }) { } else { order = DEFAULT_MODULE_ORDER; } - // Ensure all toggleable modules are included (for newly added modules) - const allModuleIds = TOGGLEABLE_MODULES.map(m => m.id); - const missingModules = allModuleIds.filter(id => !order.includes(id)); - if (missingModules.length > 0) { - order = [...order, ...missingModules]; - } + // Ensure all toggleable modules are included at correct positions (for newly added modules) + TOGGLEABLE_MODULES.forEach((module, defaultIndex) => { + if (!order.includes(module.id)) { + // Insert at the default index position, or at end if index is beyond current length + const insertAt = Math.min(defaultIndex, order.length); + order.splice(insertAt, 0, module.id); + } + }); setModuleOrderState(order); } diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index f7a2cd3..8553c98 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -66,6 +66,7 @@ "orderSection": "Sidebar Order", "orderDesc": "Drag to reorder features in the sidebar", "applyOrder": "Apply", + "cancelOrder": "Cancel", "visibility": "Visibility", "topSection": "Main Section", "bottomSection": "Bottom Section", diff --git a/frontend/src/locales/it.json b/frontend/src/locales/it.json index 3aa97d1..b8c6644 100644 --- a/frontend/src/locales/it.json +++ b/frontend/src/locales/it.json @@ -66,6 +66,7 @@ "orderSection": "Ordine nella Sidebar", "orderDesc": "Trascina per riordinare le funzioni nella barra laterale", "applyOrder": "Applica", + "cancelOrder": "Annulla", "visibility": "Visibilità", "topSection": "Sezione Principale", "bottomSection": "Sezione Inferiore", diff --git a/frontend/src/pages/admin/Features.tsx b/frontend/src/pages/admin/Features.tsx index 6e193c4..9dd2aec 100644 --- a/frontend/src/pages/admin/Features.tsx +++ b/frontend/src/pages/admin/Features.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useCallback } from 'react'; import { useAuth } from '../../contexts/AuthContext'; import { useTranslation } from '../../contexts/LanguageContext'; import { useSidebar } from '../../contexts/SidebarContext'; @@ -7,7 +7,7 @@ import type { ModuleId } from '../../contexts/ModulesContext'; import Feature1Tab from '../../components/admin/Feature1Tab'; import '../../styles/AdminPanel.css'; -type TabId = 'config' | 'feature1' | 'feature2' | 'feature3' | 'search' | 'notifications'; +type TabId = 'config' | 'dashboard' | 'feature1' | 'feature2' | 'feature3' | 'search' | 'notifications'; export default function Features() { const { user: currentUser } = useAuth(); @@ -19,15 +19,64 @@ export default function Features() { const saveRef = useRef(saveModulesToBackend); const [draggedItem, setDraggedItem] = useState(null); const [localOrder, setLocalOrder] = useState([]); + const [localPositions, setLocalPositions] = useState>({}); const [hasOrderChanges, setHasOrderChanges] = useState(false); + const tabsContainerRef = useRef(null); + const isUserEditing = useRef(false); // Track if user is actively editing - // Sync local order with context order + // Sync local order with context order (only when moduleOrder changes, not moduleStates) useEffect(() => { - if (moduleOrder.length > 0) { - setLocalOrder(moduleOrder); + if (moduleOrder.length > 0 && !isUserEditing.current) { + // Start with current order + const fullOrder = [...moduleOrder]; + + // Insert missing modules at their default positions from TOGGLEABLE_MODULES + TOGGLEABLE_MODULES.forEach((module, defaultIndex) => { + if (!fullOrder.includes(module.id)) { + // Insert at the default index position, or at end if index is beyond current length + const insertAt = Math.min(defaultIndex, fullOrder.length); + fullOrder.splice(insertAt, 0, module.id); + } + }); + + setLocalOrder(fullOrder); setHasOrderChanges(false); } }, [moduleOrder]); + + // Sync positions from moduleStates only on initial load or when not editing + useEffect(() => { + if (!isUserEditing.current) { + const positions: Record = {}; + TOGGLEABLE_MODULES.forEach(module => { + const state = moduleStates[module.id]; + positions[module.id] = state?.position || module.defaultPosition; + }); + setLocalPositions(positions); + } + }, [moduleStates]); + + // Scroll active tab to center of container + const scrollActiveTabIntoView = useCallback((tabId: string) => { + if (tabsContainerRef.current) { + const container = tabsContainerRef.current; + const activeButton = container.querySelector(`[data-tab-id="${tabId}"]`) as HTMLElement; + if (activeButton) { + const containerWidth = container.clientWidth; + const buttonLeft = activeButton.offsetLeft; + const buttonWidth = activeButton.offsetWidth; + // Calculate scroll position to center the button + const scrollLeft = buttonLeft - (containerWidth / 2) + (buttonWidth / 2); + container.scrollTo({ left: Math.max(0, scrollLeft), behavior: 'smooth' }); + } + } + }, []); + + // Handle tab change with scroll + const handleTabChange = useCallback((tabId: TabId) => { + setActiveTab(tabId); + setTimeout(() => scrollActiveTabIntoView(tabId), 50); + }, [scrollActiveTabIntoView]); // Keep saveRef updated with latest function useEffect(() => { saveRef.current = saveModulesToBackend; @@ -133,24 +182,32 @@ export default function Features() { e.stopPropagation(); if (!draggedItem) return; - const draggedPosition = moduleStates[draggedItem as ModuleId]?.position || 'top'; + const draggedPosition = localPositions[draggedItem] || 'top'; // If dropping on same item, just change section if different if (draggedItem === targetModuleId) { if (draggedPosition !== targetSection) { + isUserEditing.current = true; // Mark as user editing hasUserMadeChanges.current = true; setModulePosition(draggedItem as ModuleId, targetSection); + // Update local positions immediately for UI responsiveness + setLocalPositions(prev => ({ ...prev, [draggedItem]: targetSection })); + setHasOrderChanges(true); } return; } // Change position if moving to different section if (draggedPosition !== targetSection) { + isUserEditing.current = true; // Mark as user editing hasUserMadeChanges.current = true; setModulePosition(draggedItem as ModuleId, targetSection); + // Update local positions immediately for UI responsiveness + setLocalPositions(prev => ({ ...prev, [draggedItem]: targetSection })); } // Reorder within the list + isUserEditing.current = true; // Mark as user editing const newOrder = [...localOrder]; const draggedIndex = newOrder.indexOf(draggedItem); const targetIndex = newOrder.indexOf(targetModuleId); @@ -168,12 +225,16 @@ export default function Features() { e.preventDefault(); if (!draggedItem) return; - const draggedPosition = moduleStates[draggedItem as ModuleId]?.position || 'top'; + const draggedPosition = localPositions[draggedItem] || 'top'; // Change position if moving to different section if (draggedPosition !== section) { + isUserEditing.current = true; // Mark as user editing hasUserMadeChanges.current = true; setModulePosition(draggedItem as ModuleId, section); + // Update local positions immediately for UI responsiveness + setLocalPositions(prev => ({ ...prev, [draggedItem]: section })); + setHasOrderChanges(true); } }; @@ -181,26 +242,41 @@ export default function Features() { try { setModuleOrder(localOrder); await saveModuleOrder(localOrder); - setHasOrderChanges(false); } catch (error) { console.error('Failed to save order:', error); + } finally { + isUserEditing.current = false; + setHasOrderChanges(false); } }; + const handleCancelOrder = () => { + isUserEditing.current = false; // Done editing + setLocalOrder(moduleOrder); + // Reset positions from moduleStates + const positions: Record = {}; + TOGGLEABLE_MODULES.forEach(module => { + const state = moduleStates[module.id]; + positions[module.id] = state?.position || module.defaultPosition; + }); + setLocalPositions(positions); + setHasOrderChanges(false); + }; + const getModuleInfo = (moduleId: string) => { const module = TOGGLEABLE_MODULES.find(m => m.id === moduleId); return module || { id: moduleId, icon: 'extension', defaultEnabled: true }; }; - // Split modules by position for the config tab + // Split modules by position for the config tab (using localPositions for immediate UI updates) const topOrderModules = localOrder.filter(id => { - const state = moduleStates[id as ModuleId]; - return !state || state.position === 'top'; + const position = localPositions[id]; + return !position || position === 'top'; }); const bottomOrderModules = localOrder.filter(id => { - const state = moduleStates[id as ModuleId]; - return state && state.position === 'bottom'; + const position = localPositions[id]; + return position === 'bottom'; }); const renderConfigTab = () => { @@ -233,7 +309,6 @@ export default function Features() {
{moduleName} - {t.featuresPage?.orderDesc || 'Trascina per riordinare'}
drag_indicator @@ -274,7 +349,6 @@ export default function Features() {
{moduleName} - {t.featuresPage?.orderDesc || 'Trascina per riordinare'}
drag_indicator @@ -290,6 +364,10 @@ export default function Features() { {hasOrderChanges && (
+ @@ -384,21 +475,24 @@ export default function Features() {
- {TOGGLEABLE_MODULES.map((module) => { - const moduleName = t.sidebar[module.id as keyof typeof t.sidebar] || module.id; + {[...topOrderModules, ...bottomOrderModules].map((moduleId) => { + const moduleInfo = getModuleInfo(moduleId); + const moduleName = t.sidebar[moduleId as keyof typeof t.sidebar] || moduleId; return ( ); diff --git a/frontend/src/styles/AdminPanel.css b/frontend/src/styles/AdminPanel.css index 18bd1cf..50065a7 100644 --- a/frontend/src/styles/AdminPanel.css +++ b/frontend/src/styles/AdminPanel.css @@ -2542,7 +2542,7 @@ display: flex; flex-direction: column; gap: 0.25rem; - min-width: 0; + min-width: 280px; } .order-card-name { @@ -2642,6 +2642,30 @@ font-size: 20px; } +.order-actions .btn-secondary { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1.5rem; + font-size: 0.95rem; + font-weight: 500; + background: var(--color-bg-elevated); + color: var(--color-text-primary); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + cursor: pointer; + transition: all 0.2s ease; +} + +.order-actions .btn-secondary:hover { + background: var(--color-bg-card); + border-color: var(--color-text-secondary); +} + +.order-actions .btn-secondary .material-symbols-outlined { + font-size: 20px; +} + /* Mobile Responsive */ @media (max-width: 768px) { .order-cards { diff --git a/frontend/src/styles/Layout.css b/frontend/src/styles/Layout.css index 523912a..457e5ae 100644 --- a/frontend/src/styles/Layout.css +++ b/frontend/src/styles/Layout.css @@ -34,10 +34,16 @@ .admin-tabs-container { display: flex; justify-content: center; - padding: 0.75rem var(--page-padding-x); + padding: 0.75rem; background: transparent; } +/* Ensure no extra margin from body */ +body { + margin: 0; + padding: 0; +} + /* Page header slider - rounded pill style (like admin panel) */ .page-tabs-slider, .admin-tabs-slider { @@ -51,6 +57,16 @@ box-shadow: var(--shadow-md); max-width: 100%; backdrop-filter: blur(14px) saturate(1.15); + overflow-x: auto; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + -ms-overflow-style: none; +} + +.page-tabs-slider::-webkit-scrollbar, +.admin-tabs-slider::-webkit-scrollbar { + display: none; } @supports (color: color-mix(in srgb, black, white)) { @@ -149,7 +165,6 @@ background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-accent-hover) 100%); color: white; box-shadow: 0 2px 8px rgba(var(--color-accent-rgb), 0.3); - transform: translateY(-1px); } .page-subtitle { @@ -291,7 +306,7 @@ .mobile-menu-btn { display: flex; position: absolute; - left: 4px; + left: 16px; top: 50%; transform: translateY(-50%); z-index: 1; @@ -315,30 +330,39 @@ .page-tabs-container, .admin-tabs-container { - padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile); + padding: 0.75rem; } .page-tabs-slider, .admin-tabs-slider { width: 100%; - flex-wrap: wrap; + flex-wrap: nowrap; justify-content: flex-start; - gap: 8px; + gap: 4px; position: relative; + min-height: 48px; } .page-title-section, .admin-title-section { + display: flex; flex: 1; justify-content: flex-start; padding: 0.5rem 0.75rem; - padding-left: 48px; + padding-left: 72px; font-size: 1rem; + height: 100%; + align-items: center; } .page-title-section .material-symbols-outlined, .admin-title-section .material-symbols-outlined { - font-size: 24px; + font-size: 22px; + } + + .page-title-text, + .admin-title-text { + font-size: 0.95rem; } /* Hide divider on mobile */ @@ -349,19 +373,29 @@ /* Hide title section when tabs are present on mobile */ .page-tabs-slider:has(.page-tab-btn) .page-title-section, - .admin-tabs-slider:has(.admin-tab-btn) .admin-title-section { + .page-tabs-slider:has(.admin-tab-btn) .page-title-section, + .admin-tabs-slider:has(.admin-tab-btn) .admin-title-section, + .admin-tabs-slider:has(.page-tab-btn) .admin-title-section { display: none; } - /* Center title section absolutely when no tabs are present on mobile */ - .page-tabs-slider:not(:has(.page-tab-btn)), - .admin-tabs-slider:not(:has(.admin-tab-btn)) { - justify-content: center; - min-height: 48px; + /* Add padding-left when tabs are present to avoid logo overlap */ + .page-tabs-slider:has(.page-tab-btn), + .page-tabs-slider:has(.admin-tab-btn), + .admin-tabs-slider:has(.admin-tab-btn), + .admin-tabs-slider:has(.page-tab-btn) { + padding-left: 72px; } - .page-tabs-slider:not(:has(.page-tab-btn)) .page-title-section, - .admin-tabs-slider:not(:has(.admin-tab-btn)) .admin-title-section { + /* Center title section absolutely when no tabs are present on mobile */ + .page-tabs-slider:not(:has(.page-tab-btn)):not(:has(.admin-tab-btn)), + .admin-tabs-slider:not(:has(.admin-tab-btn)):not(:has(.page-tab-btn)) { + justify-content: center; + } + + .page-tabs-slider:not(:has(.page-tab-btn)):not(:has(.admin-tab-btn)) .page-title-section, + .admin-tabs-slider:not(:has(.admin-tab-btn)):not(:has(.page-tab-btn)) .admin-title-section { + display: flex; position: absolute; left: 50%; top: 50%; @@ -371,19 +405,19 @@ } /* Lighter icon color in dark theme when only title is shown */ - .page-tabs-slider:not(:has(.page-tab-btn)) .page-title-section .material-symbols-outlined, - .admin-tabs-slider:not(:has(.admin-tab-btn)) .admin-title-section .material-symbols-outlined { + .page-tabs-slider:not(:has(.page-tab-btn)):not(:has(.admin-tab-btn)) .page-title-section .material-symbols-outlined, + .admin-tabs-slider:not(:has(.admin-tab-btn)):not(:has(.page-tab-btn)) .admin-title-section .material-symbols-outlined { color: var(--color-text-secondary); } - /* Tabs on second row - full width */ + /* Tabs - expand to fill, but scrollable when overflow */ .page-tab-btn, .admin-tab-btn { - flex: 1; + flex: 1 0 auto; justify-content: center; - padding: 0.75rem 1rem; + padding: 0.5rem 0.75rem; font-size: 0.9rem; - min-width: 0; + min-width: 44px; } /* Hide text on mobile, show only icons */ @@ -492,6 +526,52 @@ font-size: 1rem; } +/* ========== DARK THEME OVERRIDES ========== */ + +/* Top bar in dark mode: match sidebar style exactly */ +[data-theme='dark'] .page-tabs-slider, +[data-theme='dark'] .admin-tabs-slider { + background: var(--color-bg-sidebar); + border: 1px solid var(--color-sidebar-border); + border-radius: var(--radius-lg); + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.1); +} + +@supports (color: color-mix(in srgb, black, white)) { + [data-theme='dark'] .page-tabs-slider, + [data-theme='dark'] .admin-tabs-slider { + background: color-mix(in srgb, var(--color-bg-sidebar) 88%, transparent); + backdrop-filter: blur(18px) saturate(1.15); + } +} + +/* Text colors in dark top bar */ +[data-theme='dark'] .page-title-section, +[data-theme='dark'] .admin-title-section { + color: #f1f5f9; +} + +[data-theme='dark'] .page-title-text, +[data-theme='dark'] .admin-title-text { + color: #f1f5f9; +} + +[data-theme='dark'] .page-tab-btn, +[data-theme='dark'] .admin-tab-btn { + color: rgba(241, 245, 249, 0.7); +} + +[data-theme='dark'] .page-tab-btn:hover:not(.active), +[data-theme='dark'] .admin-tab-btn:hover:not(.active) { + color: #f1f5f9; + background: rgba(255, 255, 255, 0.08); +} + +[data-theme='dark'] .page-tabs-divider, +[data-theme='dark'] .admin-tabs-divider { + background: rgba(255, 255, 255, 0.12); +} + /* ========== DARK THEME + AUTO ACCENT OVERRIDES ========== */ /* Tab buttons with auto accent in dark mode: use off-white background with dark text */ diff --git a/frontend/src/styles/Sidebar.css b/frontend/src/styles/Sidebar.css index 9fedca2..fc2e3a5 100644 --- a/frontend/src/styles/Sidebar.css +++ b/frontend/src/styles/Sidebar.css @@ -1,17 +1,17 @@ .sidebar { width: 260px; - height: calc(100vh - 2rem); + height: calc(100vh - 1.5rem); /* Floating height */ - max-height: calc(100vh - 2rem); + max-height: calc(100vh - 1.5rem); background: var(--color-bg-sidebar); color: var(--color-text-sidebar); display: flex; flex-direction: column; position: fixed; - left: 1rem; + left: 0.75rem; /* Floating position */ - top: 1rem; - bottom: 1rem; + top: 0.75rem; + bottom: 0.75rem; box-shadow: var(--shadow-xl); /* Enhanced shadow for floating effect */ z-index: 1000; @@ -657,12 +657,12 @@ button.nav-item { /* Ensure fully hidden */ width: 280px; z-index: 1001; - height: calc(100dvh - 2rem); + height: calc(100dvh - 1.5rem); /* Floating height on mobile too */ - max-height: calc(100dvh - 2rem); - left: 1rem; - top: 1rem; - bottom: 1rem; + max-height: calc(100dvh - 1.5rem); + left: 0.75rem; + top: 0.75rem; + bottom: 0.75rem; border-radius: var(--radius-lg); /* Rounded on mobile */ margin: 0; diff --git a/frontend/src/styles/theme/dimensions.css b/frontend/src/styles/theme/dimensions.css index 0d9ae83..bc448db 100644 --- a/frontend/src/styles/theme/dimensions.css +++ b/frontend/src/styles/theme/dimensions.css @@ -42,12 +42,12 @@ --height-header: 70px; /* Sidebar Dimensions */ - /* Sidebar is positioned at left: 1rem (16px) with these widths */ - /* margin-left = left offset (16px) + sidebar width */ - --sidebar-width: 276px; - /* 16px offset + 260px width */ - --sidebar-width-collapsed: 96px; - /* 16px offset + 80px width */ + /* Sidebar is positioned at left: 0.75rem (12px) with these widths */ + /* margin-left = left offset (12px) + sidebar width */ + --sidebar-width: 272px; + /* 12px offset + 260px width */ + --sidebar-width-collapsed: 92px; + /* 12px offset + 80px width */ --sidebar-mobile-width: 280px; /* Page Layout Spacing */