Refactor service drag-and-drop to use SortableJS
Replaces custom drag-and-drop logic in ServicesManager with SortableJS for improved reliability and cross-browser support. Updates UI to use a dedicated drag handle, removes legacy drag styles, and adds new CSS for SortableJS integration. Also removes redundant version comments from source files.
This commit is contained in:
1 parent
56298322ba
commit
157c1d40d5
8 files changed
+177
-361
No files matched your search
@@ -56,6 +56,9 @@
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- SortableJS for drag-and-drop functionality -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Theme Toggle -->
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
*
|
||||
* @author xixu-me
|
||||
* @license GPL-3.0
|
||||
* @version 2.0.0
|
||||
*/
|
||||
|
||||
import { OutputManager } from "./components/OutputManager.js";
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
*
|
||||
* @author xixu-me
|
||||
* @license GPL-3.0
|
||||
* @version 2.0.0
|
||||
*/
|
||||
|
||||
export class OutputManager {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
*
|
||||
* @author xixu-me
|
||||
* @license GPL-3.0
|
||||
* @version 2.0.0
|
||||
*/
|
||||
|
||||
export class ServiceConfigGenerator {
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
*
|
||||
* @author xixu-me
|
||||
* @license GPL-3.0
|
||||
* @version 2.0.0
|
||||
*/
|
||||
|
||||
export class ServicesManager {
|
||||
@@ -21,19 +20,9 @@ export class ServicesManager {
|
||||
|
||||
this.allServices = [];
|
||||
this.selectedServices = new Set();
|
||||
this.selectedServicesOrder = []; // Track order for drag functionality
|
||||
this.selectedServicesOrder = [];
|
||||
this.searchTimeout = null;
|
||||
|
||||
// Drag state
|
||||
this.dragState = {
|
||||
isDragging: false,
|
||||
draggedElement: null,
|
||||
draggedIndex: -1,
|
||||
placeholder: null,
|
||||
initialMouseY: 0,
|
||||
initialElementY: 0,
|
||||
mouseOffset: 0,
|
||||
};
|
||||
this.sortable = null; // SortableJS instance
|
||||
|
||||
this.elements = {
|
||||
searchInput: document.getElementById("search-input"),
|
||||
@@ -127,26 +116,6 @@ export class ServicesManager {
|
||||
this.elements.selectedServices.addEventListener("click", (e) => {
|
||||
this.handleSelectedServiceClick(e);
|
||||
});
|
||||
|
||||
// Handle drag events
|
||||
this.setupDragListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up drag and drop event listeners
|
||||
*/
|
||||
setupDragListeners() {
|
||||
this.elements.selectedServices.addEventListener("mousedown", (e) => {
|
||||
this.handleDragStart(e);
|
||||
});
|
||||
|
||||
document.addEventListener("mousemove", (e) => {
|
||||
this.handleDragMove(e);
|
||||
});
|
||||
|
||||
document.addEventListener("mouseup", (e) => {
|
||||
this.handleDragEnd(e);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -299,6 +268,7 @@ export class ServicesManager {
|
||||
<p>Search for services above to get started</p>
|
||||
</div>
|
||||
`;
|
||||
this.destroySortable();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -306,9 +276,11 @@ export class ServicesManager {
|
||||
this.elements.selectedServices.innerHTML = this.selectedServicesOrder
|
||||
.map(
|
||||
(serviceName, index) => `
|
||||
<div class="selected-service-item" data-index="${index}">
|
||||
<div class="selected-service-item" data-index="${index}" data-service-name="${serviceName}">
|
||||
<div class="drag-handle" title="Drag to reorder">
|
||||
<i class="fas fa-grip-vertical"></i>
|
||||
</div>
|
||||
<div class="service-info">
|
||||
<i class="fas fa-grip-lines service-drag-handle" title="Drag to reorder"></i>
|
||||
<span class="service-name">${serviceName}</span>
|
||||
</div>
|
||||
<button class="service-remove-btn"
|
||||
@@ -321,6 +293,74 @@ export class ServicesManager {
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
// Initialize or reinitialize sortable
|
||||
this.initializeSortable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize SortableJS for drag-and-drop reordering
|
||||
*/
|
||||
initializeSortable() {
|
||||
// Destroy existing sortable instance if it exists
|
||||
this.destroySortable();
|
||||
|
||||
// Only initialize if we have selected services
|
||||
if (this.selectedServices.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize SortableJS
|
||||
this.sortable = new Sortable(this.elements.selectedServices, {
|
||||
animation: 200, // Animation speed in ms
|
||||
ghostClass: "sortable-ghost", // Class for the drop placeholder
|
||||
dragClass: "sortable-drag", // Class for the item being dragged
|
||||
chosenClass: "sortable-chosen", // Class for the chosen item
|
||||
handle: ".drag-handle", // Only allow dragging by the handle
|
||||
forceFallback: true, // Force HTML5 DnD fallback for better cross-browser support
|
||||
fallbackClass: "sortable-fallback", // Class for fallback
|
||||
onStart: (evt) => {
|
||||
// Add dragging state to container
|
||||
this.elements.selectedServices.classList.add("is-dragging");
|
||||
},
|
||||
onEnd: (evt) => {
|
||||
// Remove dragging state from container
|
||||
this.elements.selectedServices.classList.remove("is-dragging");
|
||||
|
||||
// Update the order if the position changed
|
||||
if (evt.oldIndex !== evt.newIndex) {
|
||||
this.updateServiceOrder(evt.oldIndex, evt.newIndex);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the current SortableJS instance
|
||||
*/
|
||||
destroySortable() {
|
||||
if (this.sortable) {
|
||||
this.sortable.destroy();
|
||||
this.sortable = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update service order after drag and drop
|
||||
* @param {number} oldIndex - Previous index
|
||||
* @param {number} newIndex - New index
|
||||
*/
|
||||
updateServiceOrder(oldIndex, newIndex) {
|
||||
// Move the item in the order array
|
||||
const movedService = this.selectedServicesOrder.splice(oldIndex, 1)[0];
|
||||
this.selectedServicesOrder.splice(newIndex, 0, movedService);
|
||||
|
||||
// Trigger selection change event for other components
|
||||
this.triggerSelectionChange();
|
||||
|
||||
console.log(
|
||||
`Moved service "${movedService}" from position ${oldIndex} to ${newIndex}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -350,218 +390,21 @@ export class ServicesManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle drag start
|
||||
* @param {MouseEvent} event - Mouse event
|
||||
* Destroy the services manager and cleanup
|
||||
*/
|
||||
handleDragStart(event) {
|
||||
const dragHandle = event.target.closest(".service-drag-handle");
|
||||
if (!dragHandle) return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const serviceItem = dragHandle.closest(".selected-service-item");
|
||||
if (!serviceItem) return;
|
||||
|
||||
this.dragState.isDragging = true;
|
||||
this.dragState.draggedElement = serviceItem;
|
||||
this.dragState.draggedIndex = parseInt(serviceItem.dataset.index);
|
||||
|
||||
// Store initial positions for mouse following
|
||||
const rect = serviceItem.getBoundingClientRect();
|
||||
this.dragState.initialMouseY = event.clientY;
|
||||
this.dragState.initialElementY = rect.top;
|
||||
this.dragState.mouseOffset = event.clientY - rect.top;
|
||||
|
||||
// Create placeholder with smooth appearance
|
||||
this.dragState.placeholder = document.createElement("div");
|
||||
this.dragState.placeholder.className =
|
||||
"selected-service-item drag-placeholder";
|
||||
this.dragState.placeholder.innerHTML = `
|
||||
<div class="service-info">
|
||||
<i class="fas fa-grip-lines service-drag-handle"></i>
|
||||
<span class="service-name"></span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add dragging classes with smooth animation
|
||||
serviceItem.classList.add("dragging");
|
||||
document.body.classList.add("dragging");
|
||||
|
||||
// Insert placeholder with animation
|
||||
this.dragState.placeholder.style.height = "0px";
|
||||
this.dragState.placeholder.style.opacity = "0";
|
||||
this.dragState.placeholder.style.marginBottom = "0px";
|
||||
serviceItem.parentNode.insertBefore(
|
||||
this.dragState.placeholder,
|
||||
serviceItem.nextSibling
|
||||
);
|
||||
|
||||
// Animate placeholder appearance
|
||||
requestAnimationFrame(() => {
|
||||
this.dragState.placeholder.style.height = serviceItem.offsetHeight + "px";
|
||||
this.dragState.placeholder.style.opacity = "1";
|
||||
this.dragState.placeholder.style.marginBottom = "0.75rem";
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle drag move
|
||||
* @param {MouseEvent} event - Mouse event
|
||||
*/
|
||||
handleDragMove(event) {
|
||||
if (!this.dragState.isDragging) return;
|
||||
|
||||
// Make dragged element follow mouse vertically
|
||||
const draggedElement = this.dragState.draggedElement;
|
||||
if (draggedElement) {
|
||||
const deltaY = event.clientY - this.dragState.initialMouseY;
|
||||
draggedElement.style.transform = `translateY(${deltaY}px) scale(1.05)`;
|
||||
draggedElement.style.zIndex = "1000";
|
||||
draggedElement.style.position = "relative";
|
||||
destroy() {
|
||||
// Clear search timeout
|
||||
if (this.searchTimeout) {
|
||||
clearTimeout(this.searchTimeout);
|
||||
this.searchTimeout = null;
|
||||
}
|
||||
|
||||
const serviceItems = Array.from(
|
||||
this.elements.selectedServices.querySelectorAll(
|
||||
".selected-service-item:not(.dragging):not(.drag-placeholder)"
|
||||
)
|
||||
);
|
||||
// Destroy sortable instance
|
||||
this.destroySortable();
|
||||
|
||||
let targetIndex = serviceItems.length; // Default to end
|
||||
let closestItem = null;
|
||||
let closestDistance = Infinity;
|
||||
|
||||
// Find the closest item for smoother animation
|
||||
for (let i = 0; i < serviceItems.length; i++) {
|
||||
const item = serviceItems[i];
|
||||
const rect = item.getBoundingClientRect();
|
||||
const itemCenter = rect.top + rect.height / 2;
|
||||
const distance = Math.abs(event.clientY - itemCenter);
|
||||
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance;
|
||||
closestItem = item;
|
||||
}
|
||||
|
||||
if (event.clientY < itemCenter) {
|
||||
targetIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add smooth animation classes to other items
|
||||
serviceItems.forEach((item, index) => {
|
||||
const rect = item.getBoundingClientRect();
|
||||
const itemCenter = rect.top + rect.height / 2;
|
||||
|
||||
if (event.clientY < itemCenter && index >= targetIndex) {
|
||||
item.style.transform = "translateY(8px)";
|
||||
} else if (event.clientY > itemCenter && index < targetIndex) {
|
||||
item.style.transform = "translateY(-8px)";
|
||||
} else {
|
||||
item.style.transform = "translateY(0)";
|
||||
}
|
||||
});
|
||||
|
||||
// Move placeholder to new position with smooth transition
|
||||
const targetItem = serviceItems[targetIndex];
|
||||
if (targetItem) {
|
||||
targetItem.parentNode.insertBefore(
|
||||
this.dragState.placeholder,
|
||||
targetItem
|
||||
);
|
||||
} else {
|
||||
// Insert at end
|
||||
this.elements.selectedServices.appendChild(this.dragState.placeholder);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle drag end
|
||||
* @param {MouseEvent} event - Mouse event
|
||||
*/
|
||||
handleDragEnd(event) {
|
||||
if (!this.dragState.isDragging) return;
|
||||
|
||||
// Reset transforms on all items with smooth animation
|
||||
const allServiceItems = Array.from(
|
||||
this.elements.selectedServices.querySelectorAll(
|
||||
".selected-service-item:not(.dragging):not(.drag-placeholder)"
|
||||
)
|
||||
);
|
||||
|
||||
allServiceItems.forEach((item) => {
|
||||
item.style.transform = "";
|
||||
item.style.transition = "transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)";
|
||||
});
|
||||
|
||||
// Calculate new index based on placeholder position
|
||||
const allItems = Array.from(
|
||||
this.elements.selectedServices.querySelectorAll(
|
||||
".selected-service-item, .drag-placeholder"
|
||||
)
|
||||
);
|
||||
const placeholderIndex = allItems.indexOf(this.dragState.placeholder);
|
||||
const newIndex =
|
||||
placeholderIndex > this.dragState.draggedIndex
|
||||
? placeholderIndex - 1
|
||||
: placeholderIndex;
|
||||
|
||||
// Update services order
|
||||
this.reorderServices(this.dragState.draggedIndex, newIndex);
|
||||
|
||||
// Animate placeholder disappearance
|
||||
if (this.dragState.placeholder && this.dragState.placeholder.parentNode) {
|
||||
this.dragState.placeholder.style.height = "0px";
|
||||
this.dragState.placeholder.style.opacity = "0";
|
||||
this.dragState.placeholder.style.marginBottom = "0px";
|
||||
|
||||
setTimeout(() => {
|
||||
if (
|
||||
this.dragState.placeholder &&
|
||||
this.dragState.placeholder.parentNode
|
||||
) {
|
||||
this.dragState.placeholder.parentNode.removeChild(
|
||||
this.dragState.placeholder
|
||||
);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Clean up with smooth animation
|
||||
this.dragState.draggedElement.classList.remove("dragging");
|
||||
this.dragState.draggedElement.style.transform = "";
|
||||
this.dragState.draggedElement.style.zIndex = "";
|
||||
this.dragState.draggedElement.style.position = "";
|
||||
document.body.classList.remove("dragging");
|
||||
|
||||
// Reset drag state
|
||||
this.dragState = {
|
||||
isDragging: false,
|
||||
draggedElement: null,
|
||||
draggedIndex: -1,
|
||||
placeholder: null,
|
||||
initialMouseY: 0,
|
||||
initialElementY: 0,
|
||||
mouseOffset: 0,
|
||||
};
|
||||
|
||||
// Update UI and trigger change event with smooth animation
|
||||
setTimeout(() => {
|
||||
this.renderSelectedServices();
|
||||
this.triggerSelectionChange();
|
||||
}, 50);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder services array
|
||||
* @param {number} fromIndex - Original index
|
||||
* @param {number} toIndex - Target index
|
||||
*/
|
||||
reorderServices(fromIndex, toIndex) {
|
||||
if (fromIndex === toIndex) return;
|
||||
|
||||
const item = this.selectedServicesOrder[fromIndex];
|
||||
this.selectedServicesOrder.splice(fromIndex, 1);
|
||||
this.selectedServicesOrder.splice(toIndex, 0, item);
|
||||
// Clear data
|
||||
this.allServices = [];
|
||||
this.selectedServices.clear();
|
||||
this.selectedServicesOrder = [];
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@
|
||||
*
|
||||
* @author xixu-me
|
||||
* @license GPL-3.0
|
||||
* @version 2.0.0
|
||||
*/
|
||||
|
||||
export class ThemeManager {
|
||||
|
||||
+87
-112
@@ -11,7 +11,6 @@
|
||||
*
|
||||
* @author xixu-me
|
||||
* @license GPL-3.0
|
||||
* @version 2.0.0
|
||||
*/
|
||||
|
||||
/* ==========================================================================
|
||||
@@ -1033,6 +1032,64 @@ body::before {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Drag Handle Styles */
|
||||
.drag-handle {
|
||||
color: var(--text-secondary);
|
||||
cursor: grab;
|
||||
padding: 0.25rem;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 24px;
|
||||
}
|
||||
|
||||
.drag-handle:hover {
|
||||
color: var(--text-primary);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* SortableJS Styles */
|
||||
.sortable-ghost {
|
||||
opacity: 0.4;
|
||||
background: rgba(99, 102, 241, 0.2) !important;
|
||||
border: 2px dashed rgba(99, 102, 241, 0.5) !important;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.sortable-drag {
|
||||
opacity: 0.9;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3) !important;
|
||||
transform: rotate(3deg) scale(1.02);
|
||||
z-index: 1000 !important;
|
||||
}
|
||||
|
||||
.sortable-chosen {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.sortable-fallback {
|
||||
opacity: 0.8;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
transform: rotate(3deg);
|
||||
}
|
||||
|
||||
/* Container state during dragging */
|
||||
#selected-services.is-dragging {
|
||||
background: rgba(99, 102, 241, 0.05);
|
||||
}
|
||||
|
||||
#selected-services.is-dragging
|
||||
.selected-service-item:not(.sortable-drag):not(.sortable-ghost) {
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
@@ -1418,21 +1475,6 @@ body::before {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.service-drag-handle {
|
||||
color: var(--text-muted);
|
||||
cursor: grab;
|
||||
font-size: 0.875rem;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.service-drag-handle:hover {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.service-drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.service-name {
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
@@ -1493,6 +1535,26 @@ body::before {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
/* Dark theme drag handle styles */
|
||||
[data-theme="dark"] .drag-handle {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .drag-handle:hover {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
/* Dark theme sortable styles */
|
||||
[data-theme="dark"] .sortable-ghost {
|
||||
background: rgba(99, 102, 241, 0.15) !important;
|
||||
border: 2px dashed rgba(99, 102, 241, 0.4) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] #selected-services.is-dragging {
|
||||
background: rgba(99, 102, 241, 0.03);
|
||||
}
|
||||
|
||||
/* Focus Management for Accessibility */
|
||||
.focus-visible {
|
||||
outline: 2px solid var(--primary-color);
|
||||
@@ -1508,6 +1570,15 @@ body::before {
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
|
||||
/* Disable drag animations for reduced motion */
|
||||
.sortable-ghost,
|
||||
.sortable-drag,
|
||||
.sortable-fallback {
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* High Contrast Mode Support */
|
||||
@@ -1572,99 +1643,3 @@ body::before {
|
||||
.skip-link:focus {
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Drag and drop styles
|
||||
========================================================================== */
|
||||
.dragging .selected-service-item {
|
||||
z-index: 1000;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||||
transform: scale(1.02);
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.drag-placeholder {
|
||||
border: 2px solid var(--primary-color) !important;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(37, 99, 235, 0.08),
|
||||
rgba(37, 99, 235, 0.15)
|
||||
) !important;
|
||||
opacity: 1;
|
||||
animation: pulse-glow 1.5s ease-in-out infinite alternate;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0% {
|
||||
box-shadow: 0 0 5px rgba(37, 99, 235, 0.3);
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 20px rgba(37, 99, 235, 0.6);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
.drag-placeholder .service-name {
|
||||
color: var(--primary-color) !important;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
body.dragging {
|
||||
cursor: grabbing !important;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
body.dragging * {
|
||||
cursor: grabbing !important;
|
||||
}
|
||||
|
||||
.service-drag-handle {
|
||||
cursor: grab;
|
||||
opacity: 0.6;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.service-drag-handle:hover {
|
||||
opacity: 1;
|
||||
color: var(--primary-color);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.service-drag-handle:active {
|
||||
cursor: grabbing;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.selected-service-item.dragging {
|
||||
transition: none !important;
|
||||
z-index: 1000;
|
||||
position: relative;
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.4);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Smooth animations for reordering */
|
||||
.selected-service-item:not(.dragging):not(.drag-placeholder) {
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Enhanced animations for drag interactions */
|
||||
.selected-service-item.drag-over-top {
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
|
||||
.selected-service-item.drag-over-bottom {
|
||||
transform: translateY(8px);
|
||||
}
|
||||
|
||||
/* Dark theme drag styles */
|
||||
[data-theme="dark"] .drag-placeholder {
|
||||
border-color: var(--primary-color) !important;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(37, 99, 235, 0.12),
|
||||
rgba(37, 99, 235, 0.2)
|
||||
) !important;
|
||||
}
|
||||
@@ -8,7 +8,6 @@
|
||||
* @source https://github.com/xixu-me/META
|
||||
* @author xixu-me
|
||||
* @license GPL-3.0
|
||||
* @version 2.0.0
|
||||
*/
|
||||
|
||||
const metaTemplate = `// Generated by META.js Customizer (https://metajs.xi-xu.me)
|
||||
|
||||
Reference in new issue
Block a user