Add module ordering feature with drag-and-drop
- Add modules_order setting in backend settings registry - Update ModulesContext with moduleOrder state and save functions - Create configuration tab in Features page with drag-and-drop reordering - Make feature tabs dynamic based on order (updates in real-time) - Sort sidebar modules based on saved order - Add order-cards CSS with vertical layout - Update API client to support string[] type for modules_order 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -275,6 +275,16 @@ register_setting(SettingDefinition(
|
||||
category="modules"
|
||||
))
|
||||
|
||||
register_setting(SettingDefinition(
|
||||
key="modules_order",
|
||||
type=SettingType.LIST,
|
||||
scope=SettingScope.GLOBAL,
|
||||
storage=SettingStorage.DATABASE,
|
||||
default=["feature1", "feature2", "feature3"],
|
||||
description="Order of feature modules in sidebar",
|
||||
category="modules"
|
||||
))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# AUTHENTICATION & SECURITY SETTINGS (Global, Database)
|
||||
|
||||
@@ -59,13 +59,13 @@ export const settingsAPI = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getModules: async (): Promise<Record<string, boolean | string>> => {
|
||||
const response = await api.get<Record<string, boolean | string>>('/settings/modules');
|
||||
getModules: async (): Promise<Record<string, boolean | string | string[]>> => {
|
||||
const response = await api.get<Record<string, boolean | string | string[]>>('/settings/modules');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateModules: async (data: Record<string, boolean>): Promise<Record<string, boolean>> => {
|
||||
const response = await api.put<Record<string, boolean>>('/settings/modules', data);
|
||||
updateModules: async (data: Record<string, boolean | string[]>): Promise<Record<string, boolean | string[]>> => {
|
||||
const response = await api.put<Record<string, boolean | string[]>>('/settings/modules', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
|
||||
@@ -27,14 +27,14 @@ export default function Sidebar() {
|
||||
} = useSidebar();
|
||||
const { viewMode, toggleViewMode, isUserModeEnabled } = useViewMode();
|
||||
const { user } = useAuth();
|
||||
const { isModuleEnabled, isModuleEnabledForUser, hasInitialized: modulesInitialized } = useModules();
|
||||
const { isModuleEnabled, isModuleEnabledForUser, moduleOrder, hasInitialized: modulesInitialized } = useModules();
|
||||
|
||||
// When admin is in "user mode", show only user-permitted modules
|
||||
// Otherwise, show all globally enabled modules (admin view)
|
||||
const shouldUseUserPermissions = viewMode === 'user' || !user?.is_superuser;
|
||||
|
||||
// Don't show modules until initialization is complete to prevent flash
|
||||
const mainModules = !modulesInitialized ? [] : (appModules
|
||||
const mainModulesFiltered = !modulesInitialized ? [] : (appModules
|
||||
.find((cat) => cat.id === 'main')
|
||||
?.modules.filter((m) => {
|
||||
if (!m.enabled) return false;
|
||||
@@ -44,6 +44,25 @@ export default function Sidebar() {
|
||||
return isModuleEnabled(m.id);
|
||||
}) || []);
|
||||
|
||||
// Sort modules based on moduleOrder (dashboard always first, then ordered features)
|
||||
const mainModules = [...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);
|
||||
|
||||
// If both are in the order array, sort by their position
|
||||
if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
|
||||
// If only one is in the order array, it comes first
|
||||
if (aIndex !== -1) return -1;
|
||||
if (bIndex !== -1) return 1;
|
||||
// If neither is in the order array, maintain original order
|
||||
return 0;
|
||||
});
|
||||
|
||||
const handleCollapseClick = () => {
|
||||
if (isMobileOpen) {
|
||||
closeMobileMenu();
|
||||
|
||||
@@ -18,12 +18,18 @@ export interface ModuleState {
|
||||
user: boolean;
|
||||
}
|
||||
|
||||
// Default order for modules
|
||||
const DEFAULT_MODULE_ORDER: string[] = ['feature1', 'feature2', 'feature3'];
|
||||
|
||||
interface ModulesContextType {
|
||||
moduleStates: Record<ModuleId, ModuleState>;
|
||||
moduleOrder: string[];
|
||||
isModuleEnabled: (moduleId: string) => boolean;
|
||||
isModuleEnabledForUser: (moduleId: string, userPermissions: UserPermissions | undefined, isSuperuser: boolean) => boolean;
|
||||
setModuleEnabled: (moduleId: ModuleId, type: 'admin' | 'user', enabled: boolean) => void;
|
||||
setModuleOrder: (order: string[]) => void;
|
||||
saveModulesToBackend: () => Promise<void>;
|
||||
saveModuleOrder: (order: string[]) => Promise<void>;
|
||||
isLoading: boolean;
|
||||
hasInitialized: boolean;
|
||||
}
|
||||
@@ -42,6 +48,7 @@ const getDefaultStates = (): Record<ModuleId, ModuleState> => {
|
||||
export function ModulesProvider({ children }: { children: ReactNode }) {
|
||||
const { token } = useAuth();
|
||||
const [moduleStates, setModuleStates] = useState<Record<ModuleId, ModuleState>>(getDefaultStates);
|
||||
const [moduleOrder, setModuleOrderState] = useState<string[]>(DEFAULT_MODULE_ORDER);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [hasInitialized, setHasInitialized] = useState(false);
|
||||
|
||||
@@ -88,6 +95,24 @@ export function ModulesProvider({ children }: { children: ReactNode }) {
|
||||
});
|
||||
|
||||
setModuleStates(newStates);
|
||||
|
||||
// Load module order
|
||||
if (settings.modules_order) {
|
||||
let order: string[];
|
||||
if (typeof settings.modules_order === 'string') {
|
||||
try {
|
||||
order = JSON.parse(settings.modules_order);
|
||||
} catch {
|
||||
order = DEFAULT_MODULE_ORDER;
|
||||
}
|
||||
} else if (Array.isArray(settings.modules_order)) {
|
||||
order = settings.modules_order;
|
||||
} else {
|
||||
order = DEFAULT_MODULE_ORDER;
|
||||
}
|
||||
setModuleOrderState(order);
|
||||
}
|
||||
|
||||
setHasInitialized(true);
|
||||
} catch (error) {
|
||||
console.error('Failed to load modules from backend:', error);
|
||||
@@ -112,6 +137,21 @@ export function ModulesProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
}, [moduleStates]);
|
||||
|
||||
// Save module order to backend
|
||||
const saveModuleOrder = useCallback(async (order: string[]) => {
|
||||
try {
|
||||
await settingsAPI.updateModules({ modules_order: order });
|
||||
} catch (error) {
|
||||
console.error('Failed to save module order to backend:', error);
|
||||
throw error;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Set module order (local state only, call saveModuleOrder to persist)
|
||||
const setModuleOrder = useCallback((order: string[]) => {
|
||||
setModuleOrderState(order);
|
||||
}, []);
|
||||
|
||||
// Load modules when token becomes available
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
@@ -185,10 +225,13 @@ export function ModulesProvider({ children }: { children: ReactNode }) {
|
||||
<ModulesContext.Provider
|
||||
value={{
|
||||
moduleStates,
|
||||
moduleOrder,
|
||||
isModuleEnabled,
|
||||
isModuleEnabledForUser,
|
||||
setModuleEnabled,
|
||||
setModuleOrder,
|
||||
saveModulesToBackend,
|
||||
saveModuleOrder,
|
||||
isLoading,
|
||||
hasInitialized,
|
||||
}}
|
||||
|
||||
@@ -55,7 +55,11 @@
|
||||
},
|
||||
"featuresPage": {
|
||||
"title": "Features",
|
||||
"subtitle": "Configure application features"
|
||||
"subtitle": "Configure application features",
|
||||
"configTab": "Configuration",
|
||||
"orderSection": "Sidebar Order",
|
||||
"orderDesc": "Drag to reorder features in the sidebar",
|
||||
"applyOrder": "Apply"
|
||||
},
|
||||
"sourcesPage": {
|
||||
"title": "Sources",
|
||||
|
||||
@@ -55,7 +55,11 @@
|
||||
},
|
||||
"featuresPage": {
|
||||
"title": "Funzionalità",
|
||||
"subtitle": "Configura le funzionalità dell'applicazione"
|
||||
"subtitle": "Configura le funzionalità dell'applicazione",
|
||||
"configTab": "Configurazione",
|
||||
"orderSection": "Ordine nella Sidebar",
|
||||
"orderDesc": "Trascina per riordinare le funzioni nella barra laterale",
|
||||
"applyOrder": "Applica"
|
||||
},
|
||||
"sourcesPage": {
|
||||
"title": "Sorgenti",
|
||||
|
||||
@@ -2,40 +2,32 @@ import { useState, useEffect, useRef } from 'react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useTranslation } from '../../contexts/LanguageContext';
|
||||
import { useSidebar } from '../../contexts/SidebarContext';
|
||||
import { useModules } from '../../contexts/ModulesContext';
|
||||
import { useModules, TOGGLEABLE_MODULES } from '../../contexts/ModulesContext';
|
||||
import type { ModuleId } from '../../contexts/ModulesContext';
|
||||
import Feature1Tab from '../../components/admin/Feature1Tab';
|
||||
import '../../styles/AdminPanel.css';
|
||||
|
||||
type TabId = 'feature1' | 'feature2' | 'feature3';
|
||||
type TabId = 'config' | 'feature1' | 'feature2' | 'feature3';
|
||||
|
||||
export default function Features() {
|
||||
const { user: currentUser } = useAuth();
|
||||
const { t } = useTranslation();
|
||||
const { toggleMobileMenu } = useSidebar();
|
||||
const { moduleStates, setModuleEnabled, saveModulesToBackend, hasInitialized, isLoading } = useModules();
|
||||
const [activeTab, setActiveTab] = useState<TabId>('feature1');
|
||||
const { moduleStates, moduleOrder, setModuleEnabled, setModuleOrder, saveModulesToBackend, saveModuleOrder, hasInitialized, isLoading } = useModules();
|
||||
const [activeTab, setActiveTab] = useState<TabId>('config');
|
||||
const hasUserMadeChanges = useRef(false);
|
||||
const saveRef = useRef(saveModulesToBackend);
|
||||
const [tooltip, setTooltip] = useState<{ text: string; left: number; visible: boolean }>({
|
||||
text: '',
|
||||
left: 0,
|
||||
visible: false,
|
||||
});
|
||||
|
||||
const handleTabMouseEnter = (text: string, e: React.MouseEvent) => {
|
||||
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
setTooltip({
|
||||
text,
|
||||
left: rect.left + rect.width / 2,
|
||||
visible: true,
|
||||
});
|
||||
};
|
||||
|
||||
const handleTabMouseLeave = () => {
|
||||
setTooltip((prev) => ({ ...prev, visible: false }));
|
||||
};
|
||||
const [draggedItem, setDraggedItem] = useState<string | null>(null);
|
||||
const [localOrder, setLocalOrder] = useState<string[]>([]);
|
||||
const [hasOrderChanges, setHasOrderChanges] = useState(false);
|
||||
|
||||
// Sync local order with context order
|
||||
useEffect(() => {
|
||||
if (moduleOrder.length > 0) {
|
||||
setLocalOrder(moduleOrder);
|
||||
setHasOrderChanges(false);
|
||||
}
|
||||
}, [moduleOrder]);
|
||||
// Keep saveRef updated with latest function
|
||||
useEffect(() => {
|
||||
saveRef.current = saveModulesToBackend;
|
||||
@@ -118,11 +110,118 @@ export default function Features() {
|
||||
);
|
||||
};
|
||||
|
||||
// Drag and drop handlers for ordering
|
||||
const handleDragStart = (e: React.DragEvent, moduleId: string) => {
|
||||
setDraggedItem(moduleId);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', moduleId);
|
||||
// Add dragging class after a small delay for visual feedback
|
||||
setTimeout(() => {
|
||||
(e.target as HTMLElement).classList.add('dragging');
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const handleDragEnd = (e: React.DragEvent) => {
|
||||
setDraggedItem(null);
|
||||
(e.target as HTMLElement).classList.remove('dragging');
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent, targetModuleId: string) => {
|
||||
e.preventDefault();
|
||||
if (!draggedItem || draggedItem === targetModuleId) return;
|
||||
|
||||
const newOrder = [...localOrder];
|
||||
const draggedIndex = newOrder.indexOf(draggedItem);
|
||||
const targetIndex = newOrder.indexOf(targetModuleId);
|
||||
|
||||
if (draggedIndex === -1 || targetIndex === -1) return;
|
||||
|
||||
// Remove dragged item and insert at target position
|
||||
newOrder.splice(draggedIndex, 1);
|
||||
newOrder.splice(targetIndex, 0, draggedItem);
|
||||
|
||||
setLocalOrder(newOrder);
|
||||
setHasOrderChanges(true);
|
||||
};
|
||||
|
||||
const handleApplyOrder = async () => {
|
||||
try {
|
||||
setModuleOrder(localOrder);
|
||||
await saveModuleOrder(localOrder);
|
||||
setHasOrderChanges(false);
|
||||
} catch (error) {
|
||||
console.error('Failed to save order:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const getModuleInfo = (moduleId: string) => {
|
||||
const module = TOGGLEABLE_MODULES.find(m => m.id === moduleId);
|
||||
return module || { id: moduleId, icon: 'extension', defaultEnabled: true };
|
||||
};
|
||||
|
||||
const renderConfigTab = () => {
|
||||
return (
|
||||
<div className="theme-tab-content">
|
||||
<div className="theme-section">
|
||||
<div className="section-header">
|
||||
<h3 className="section-title">{t.featuresPage?.orderSection || 'Ordine nella Sidebar'}</h3>
|
||||
</div>
|
||||
<div className="order-cards">
|
||||
{localOrder.map((moduleId, index) => {
|
||||
const moduleInfo = getModuleInfo(moduleId);
|
||||
const moduleName = t.sidebar[moduleId as keyof typeof t.sidebar] || moduleId;
|
||||
return (
|
||||
<div
|
||||
key={moduleId}
|
||||
className={`order-card ${draggedItem === moduleId ? 'dragging' : ''}`}
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, moduleId)}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={(e) => handleDrop(e, moduleId)}
|
||||
>
|
||||
<div className="order-card-preview">
|
||||
<span className="order-card-number">{index + 1}</span>
|
||||
</div>
|
||||
<div className="order-card-info">
|
||||
<span className="order-card-name">{moduleName}</span>
|
||||
<span className="order-card-desc">{t.featuresPage?.orderDesc || 'Trascina per riordinare'}</span>
|
||||
</div>
|
||||
<div className="order-card-icon">
|
||||
<span className="material-symbols-outlined">{moduleInfo.icon}</span>
|
||||
</div>
|
||||
<div className="order-card-handle">
|
||||
<span className="material-symbols-outlined">drag_indicator</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{hasOrderChanges && (
|
||||
<div className="order-actions">
|
||||
<button className="btn-primary" onClick={handleApplyOrder}>
|
||||
<span className="material-symbols-outlined">check</span>
|
||||
{t.featuresPage?.applyOrder || 'Applica'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTabContent = () => {
|
||||
if (!hasInitialized || isLoading) {
|
||||
return <div className="loading">Loading...</div>;
|
||||
}
|
||||
switch (activeTab) {
|
||||
case 'config':
|
||||
return renderConfigTab();
|
||||
case 'feature1':
|
||||
return (
|
||||
<>
|
||||
@@ -163,52 +262,37 @@ export default function Features() {
|
||||
|
||||
return (
|
||||
<main className="main-content admin-panel-root">
|
||||
{/* Tab Tooltip */}
|
||||
{tooltip.visible && (
|
||||
<div
|
||||
className="admin-tab-tooltip"
|
||||
style={{ left: tooltip.left }}
|
||||
>
|
||||
{tooltip.text}
|
||||
</div>
|
||||
)}
|
||||
<div className="admin-tabs-container">
|
||||
<div className="admin-tabs-slider">
|
||||
<div className="page-tabs-container">
|
||||
<div className="page-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
<span className="material-symbols-outlined">menu</span>
|
||||
</button>
|
||||
<div className="admin-title-section">
|
||||
<div className="page-title-section">
|
||||
<span className="material-symbols-outlined">extension</span>
|
||||
<span className="admin-title-text">{t.featuresPage.title}</span>
|
||||
<span className="page-title-text">{t.featuresPage.title}</span>
|
||||
</div>
|
||||
<div className="admin-tabs-divider"></div>
|
||||
<div className="page-tabs-divider"></div>
|
||||
<button
|
||||
className={`admin-tab-btn ${activeTab === 'feature1' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('feature1')}
|
||||
onMouseEnter={(e) => handleTabMouseEnter(t.sidebar.feature1, e)}
|
||||
onMouseLeave={handleTabMouseLeave}
|
||||
className={`page-tab-btn ${activeTab === 'config' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('config')}
|
||||
>
|
||||
<span className="material-symbols-outlined">playlist_play</span>
|
||||
<span>{t.sidebar.feature1}</span>
|
||||
<span className="material-symbols-outlined">tune</span>
|
||||
<span>{t.featuresPage?.configTab || 'Configurazione'}</span>
|
||||
</button>
|
||||
{(localOrder.length > 0 ? localOrder : ['feature1', 'feature2', 'feature3']).map((moduleId) => {
|
||||
const moduleInfo = getModuleInfo(moduleId);
|
||||
const moduleName = t.sidebar[moduleId as keyof typeof t.sidebar] || moduleId;
|
||||
return (
|
||||
<button
|
||||
className={`admin-tab-btn ${activeTab === 'feature2' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('feature2')}
|
||||
onMouseEnter={(e) => handleTabMouseEnter(t.sidebar.feature2, e)}
|
||||
onMouseLeave={handleTabMouseLeave}
|
||||
key={moduleId}
|
||||
className={`page-tab-btn ${activeTab === moduleId ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab(moduleId as TabId)}
|
||||
>
|
||||
<span className="material-symbols-outlined">download</span>
|
||||
<span>{t.sidebar.feature2}</span>
|
||||
</button>
|
||||
<button
|
||||
className={`admin-tab-btn ${activeTab === 'feature3' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('feature3')}
|
||||
onMouseEnter={(e) => handleTabMouseEnter(t.sidebar.feature3, e)}
|
||||
onMouseLeave={handleTabMouseLeave}
|
||||
>
|
||||
<span className="material-symbols-outlined">cast</span>
|
||||
<span>{t.sidebar.feature3}</span>
|
||||
<span className="material-symbols-outlined">{moduleInfo.icon}</span>
|
||||
<span>{moduleName}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2378,3 +2378,155 @@
|
||||
[data-theme='dark'][data-accent='auto'] .users-modal .toggle-inline input:checked+.toggle-slider-sm::before {
|
||||
background: #111827;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
ORDER CARDS - Feature Ordering (Theme Editor Style)
|
||||
=========================================== */
|
||||
|
||||
/* Order Cards Container - Vertical Stack */
|
||||
.order-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
/* Order Card */
|
||||
.order-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.25rem;
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: grab;
|
||||
transition: all 0.25s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.order-card:hover {
|
||||
transform: translateX(4px);
|
||||
border-color: var(--color-text-secondary);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.order-card:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.order-card.dragging {
|
||||
opacity: 0.6;
|
||||
transform: scale(1.02);
|
||||
box-shadow: var(--shadow-lg);
|
||||
border-color: var(--color-accent);
|
||||
background: rgba(var(--color-accent-rgb), 0.05);
|
||||
}
|
||||
|
||||
/* Order Card Preview - Number Badge */
|
||||
.order-card-preview {
|
||||
flex-shrink: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-bg-elevated);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.order-card-number {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* Order Card Info */
|
||||
.order-card-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.order-card-name {
|
||||
font-weight: 600;
|
||||
font-size: 1.05rem;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.order-card-desc {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* Order Card Icon */
|
||||
.order-card-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: rgba(var(--color-accent-rgb), 0.1);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.order-card-icon .material-symbols-outlined {
|
||||
font-size: 20px;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* Order Card Handle */
|
||||
.order-card-handle {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.order-card-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.order-card-handle .material-symbols-outlined {
|
||||
font-size: 24px;
|
||||
color: var(--color-text-muted);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.order-card:hover .order-card-handle .material-symbols-outlined {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* Order Actions */
|
||||
.order-actions {
|
||||
margin-top: 1.5rem;
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.order-actions .btn-primary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.order-actions .btn-primary .material-symbols-outlined {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* Mobile Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.order-cards {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user