[READ-ONLY] Mirror of https://github.com/flo-bit/ui-kit. 🦊 fox ui, svelte 5 and tailwind 4 flo-bit.dev/ui-kit/
svelte tailwindcss ui-components
0

Configure Feed

Select the types of activity you want to include in your feed.

resizable drag n drop test

Florian (Mar 3, 2025, 1:21 AM +0100) c015677f c74b0fa1

+606
+68
package-lock.json
··· 11 11 "@atproto/api": "^0.14.7", 12 12 "@number-flow/svelte": "^0.3.3", 13 13 "@sveltejs/adapter-static": "^3.0.8", 14 + "@thisux/sveltednd": "^0.0.20", 14 15 "@threlte/core": "^8.0.1", 15 16 "@threlte/extras": "^9.0.1", 16 17 "@types/three": "^0.173.0", 17 18 "bits-ui": "^1.3.4", 18 19 "hls.js": "^1.5.20", 19 20 "layerchart": "^1.0.0", 21 + "paneforge": "^0.0.6", 20 22 "plyr": "^3.7.8", 21 23 "svelte-sonner": "^0.3.28", 22 24 "tailwind-variants": "^0.3.1", ··· 2042 2044 "peerDependencies": { 2043 2045 "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" 2044 2046 } 2047 + }, 2048 + "node_modules/@thisux/sveltednd": { 2049 + "version": "0.0.20", 2050 + "resolved": "https://registry.npmjs.org/@thisux/sveltednd/-/sveltednd-0.0.20.tgz", 2051 + "integrity": "sha512-VE0HopIlHIvNOfSZ1SsiIzl1AmTU0VSd2Nz2Q2nIdqIUsT/UP4hiLw2ed2jFqTKTbqC+kbCYL9vuBpkYnEw+Kw==", 2052 + "dependencies": { 2053 + "@thisux/sveltednd": "^0.0.18" 2054 + }, 2055 + "peerDependencies": { 2056 + "svelte": "^5.0.0" 2057 + } 2058 + }, 2059 + "node_modules/@thisux/sveltednd/node_modules/@thisux/sveltednd": { 2060 + "version": "0.0.18", 2061 + "resolved": "https://registry.npmjs.org/@thisux/sveltednd/-/sveltednd-0.0.18.tgz", 2062 + "integrity": "sha512-MO+iR9ZRHApvtwgWujmqwUbhv9mKs0jLOmeG4tn98GmU1/9wVOi02jrNqeggKg3c8dXz6pMmUHfOfmcq7WwYZg==", 2063 + "dependencies": { 2064 + "@thisux/sveltednd": "^0.0.17" 2065 + }, 2066 + "peerDependencies": { 2067 + "svelte": "^5.0.0" 2068 + } 2069 + }, 2070 + "node_modules/@thisux/sveltednd/node_modules/@thisux/sveltednd/node_modules/@thisux/sveltednd": { 2071 + "version": "0.0.17", 2072 + "resolved": "https://registry.npmjs.org/@thisux/sveltednd/-/sveltednd-0.0.17.tgz", 2073 + "integrity": "sha512-lRninjw439phhA8xAHqCpMAX0hnwFMdbXW4M0XJgAdnGxeum+QsLiIC4P3HnkNXAygsVKUqxRcbS84CxDZ9hPw==", 2074 + "dependencies": { 2075 + "@thisux/sveltednd": "^0.0.14" 2076 + }, 2077 + "peerDependencies": { 2078 + "svelte": "^5.0.0" 2079 + } 2080 + }, 2081 + "node_modules/@thisux/sveltednd/node_modules/@thisux/sveltednd/node_modules/@thisux/sveltednd/node_modules/@thisux/sveltednd": { 2082 + "version": "0.0.14", 2083 + "resolved": "https://registry.npmjs.org/@thisux/sveltednd/-/sveltednd-0.0.14.tgz", 2084 + "integrity": "sha512-Vbq69SU3HUomPg6oCXtb89OG89hka0YIkdaErYibn3waK7tYE66IcQxD/Fzg8YNW3EVsXoA9kc7kW5EUBCSQGg==" 2045 2085 }, 2046 2086 "node_modules/@threejs-kit/instanced-sprite-mesh": { 2047 2087 "version": "2.5.0", ··· 4494 4534 "version": "1.0.1", 4495 4535 "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", 4496 4536 "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" 4537 + }, 4538 + "node_modules/paneforge": { 4539 + "version": "0.0.6", 4540 + "resolved": "https://registry.npmjs.org/paneforge/-/paneforge-0.0.6.tgz", 4541 + "integrity": "sha512-jYeN/wdREihja5c6nK3S5jritDQ+EbCqC5NrDo97qCZzZ9GkmEcN5C0ZCjF4nmhBwkDKr6tLIgz4QUKWxLXjAw==", 4542 + "dependencies": { 4543 + "nanoid": "^5.0.4" 4544 + }, 4545 + "peerDependencies": { 4546 + "svelte": "^4.0.0 || ^5.0.0-next.1" 4547 + } 4548 + }, 4549 + "node_modules/paneforge/node_modules/nanoid": { 4550 + "version": "5.1.2", 4551 + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.2.tgz", 4552 + "integrity": "sha512-b+CiXQCNMUGe0Ri64S9SXFcP9hogjAJ2Rd6GdVxhPLRm7mhGaM7VgOvCAJ1ZshfHbqVDI3uqTI5C8/GaKuLI7g==", 4553 + "funding": [ 4554 + { 4555 + "type": "github", 4556 + "url": "https://github.com/sponsors/ai" 4557 + } 4558 + ], 4559 + "bin": { 4560 + "nanoid": "bin/nanoid.js" 4561 + }, 4562 + "engines": { 4563 + "node": "^18 || >=20" 4564 + } 4497 4565 }, 4498 4566 "node_modules/parent-module": { 4499 4567 "version": "1.0.1",
+2
package.json
··· 41 41 "@atproto/api": "^0.14.7", 42 42 "@number-flow/svelte": "^0.3.3", 43 43 "@sveltejs/adapter-static": "^3.0.8", 44 + "@thisux/sveltednd": "^0.0.20", 44 45 "@threlte/core": "^8.0.1", 45 46 "@threlte/extras": "^9.0.1", 46 47 "@types/three": "^0.173.0", 47 48 "bits-ui": "^1.3.4", 48 49 "hls.js": "^1.5.20", 49 50 "layerchart": "^1.0.0", 51 + "paneforge": "^0.0.6", 50 52 "plyr": "^3.7.8", 51 53 "svelte-sonner": "^0.3.28", 52 54 "tailwind-variants": "^0.3.1",
+142
src/lib/sections/resizable/Resizable.svelte
··· 1 + <script lang="ts"> 2 + import { Portal } from 'bits-ui'; 3 + import { PaneGroup, Pane, PaneResizer } from 'paneforge'; 4 + import { draggable, droppable, type DragDropState } from './dragndrop'; 5 + 6 + import TimerPreview from '$lib/preview/TimerPreview.svelte'; 7 + import WaveformAudioPlayerPreview from '$lib/preview/WaveformAudioPlayerPreview.svelte'; 8 + import NumberInputPreview from '$lib/preview/NumberInputPreview.svelte'; 9 + import ThemeToggle from '$lib/components/theme-toggle/ThemeToggle.svelte'; 10 + import SelectTheme from '$lib/preview/SelectTheme.svelte'; 11 + 12 + let contentToPanes = $state({ 13 + first: '#pane1', 14 + second: '#pane2', 15 + third: '#pane3' 16 + }); 17 + 18 + let paneToContent = $state({ 19 + '#pane1': 'first', 20 + '#pane2': 'second', 21 + '#pane3': 'third' 22 + }); 23 + 24 + function handleDrop(event: DragDropState) { 25 + const { sourceContainer, targetContainer } = event; 26 + 27 + if (!sourceContainer || !targetContainer) return; 28 + 29 + const targetPane = targetContainer; 30 + const sourcePane = contentToPanes[sourceContainer as keyof typeof contentToPanes]; 31 + 32 + // get current content of target container 33 + const targetContent = paneToContent[targetContainer as keyof typeof paneToContent]; 34 + const sourceContent = sourceContainer; 35 + 36 + contentToPanes[targetContent as keyof typeof contentToPanes] = sourcePane; 37 + contentToPanes[sourceContent as keyof typeof contentToPanes] = targetPane; 38 + 39 + paneToContent[sourcePane as keyof typeof paneToContent] = targetContent; 40 + paneToContent[targetPane as keyof typeof paneToContent] = sourceContent; 41 + } 42 + 43 + let background = 'bg-base-100 dark:bg-base-900 border border-base-200 dark:border-base-800 relative overflow-hidden rounded-2xl p-6 pt-10 h-full w-full flex flex-col gap-4'; 44 + let accentBackground = 45 + 'bg-accent-500/5 dark:bg-accent-500/5 border border-accent-500/20 dark:border-accent-500/20'; 46 + </script> 47 + 48 + <div class="h-screen w-screen"> 49 + <PaneGroup direction="horizontal" class="p-6"> 50 + <Pane defaultSize={50} id="pane1" class="h-full p-2"> 51 + <div 52 + id="pane1" 53 + use:droppable={{ container: '#pane1', callbacks: { onDrop: handleDrop } }} 54 + class="h-full w-full rounded-2xl" 55 + ></div> 56 + </Pane> 57 + <PaneResizer 58 + class="bg-accent-200 dark:bg-accent-900 h-full w-1 rounded-full opacity-0 hover:opacity-100" 59 + /> 60 + <Pane defaultSize={50} class=""> 61 + <PaneGroup direction="vertical"> 62 + <Pane defaultSize={50} class="p-2"> 63 + <div 64 + use:droppable={{ container: '#pane2', callbacks: { onDrop: handleDrop } }} 65 + id="pane2" 66 + class="h-full w-full rounded-2xl" 67 + ></div> 68 + </Pane> 69 + <PaneResizer 70 + class="bg-accent-200 dark:bg-accent-900 h-1 w-full rounded-full opacity-0 hover:opacity-100" 71 + /> 72 + <Pane defaultSize={50} class="p-2"> 73 + <div 74 + id="pane3" 75 + use:droppable={{ container: '#pane3', callbacks: { onDrop: handleDrop } }} 76 + class="h-full w-full rounded-2xl" 77 + ></div> 78 + </Pane> 79 + </PaneGroup> 80 + </Pane> 81 + </PaneGroup> 82 + </div> 83 + 84 + <Portal to={contentToPanes.first}> 85 + <div 86 + class="h-full w-full" 87 + use:draggable={{ 88 + container: 'first', 89 + dragData: { id: 'first' }, 90 + handle: '.handle' 91 + }} 92 + > 93 + <div 94 + class={background} 95 + > 96 + <button class="handle bg-base-500/5 absolute top-0 right-0 left-0 h-5 w-full cursor-grab"> 97 + <span class="sr-only">drag</span> 98 + </button> 99 + 100 + <ThemeToggle class="absolute top-6 right-2" /> 101 + 102 + <SelectTheme /> 103 + 104 + </div> 105 + </div> 106 + </Portal> 107 + 108 + <Portal to={contentToPanes.second}> 109 + <div 110 + class="h-full w-full" 111 + use:draggable={{ container: 'second', dragData: { id: 'second' }, handle: '.handle' }} 112 + > 113 + <div 114 + class={background} 115 + > 116 + <button class="handle bg-base-500/5 absolute top-0 right-0 left-0 h-5 w-full cursor-grab"> 117 + <span class="sr-only">drag</span> 118 + </button> 119 + 120 + <NumberInputPreview /> 121 + 122 + <WaveformAudioPlayerPreview /> 123 + </div> 124 + </div> 125 + </Portal> 126 + 127 + <Portal to={contentToPanes.third}> 128 + <div 129 + class="h-full w-full" 130 + use:draggable={{ container: 'third', dragData: { id: 'third' }, handle: '.handle' }} 131 + > 132 + <div 133 + class={background} 134 + > 135 + <button class="handle bg-base-500/5 absolute top-0 right-0 left-0 h-5 w-full cursor-grab"> 136 + <span class="sr-only">drag</span> 137 + </button> 138 + 139 + <TimerPreview /> 140 + </div> 141 + </div> 142 + </Portal>
+6
src/routes/sections/resizable/+page.svelte
··· 1 + <script> 2 + import Resizable from "$lib/sections/resizable/Resizable.svelte"; 3 + 4 + </script> 5 + 6 + <Resizable />
+13
src/lib/sections/resizable/dragndrop/index.ts
··· 1 + // adapted from https://github.com/thisuxhq/sveltednd 2 + 3 + // Actions 4 + export { draggable, droppable } from './actions/index.js'; 5 + 6 + // Store 7 + export { dndState } from './stores/dnd.svelte.js'; 8 + 9 + // Types 10 + export type * from './types/index.js'; 11 + 12 + // Styles 13 + import './styles/dnd.css';
+148
src/lib/sections/resizable/dragndrop/actions/draggable.ts
··· 1 + import { dndState } from '../stores/dnd.svelte.js'; 2 + import type { DragDropOptions, DragDropState } from '../types'; 3 + 4 + const DEFAULT_DRAGGING_CLASS = 'dragging'; 5 + 6 + interface DraggableOptions<T> extends DragDropOptions<T> { 7 + // Add new option for interactive selectors 8 + interactive?: string[]; 9 + 10 + handle?: string; 11 + } 12 + 13 + export function draggable<T>(node: HTMLElement, options: DraggableOptions<T>) { 14 + const draggingClass = (options.attributes?.draggingClass || DEFAULT_DRAGGING_CLASS).split(' '); 15 + let initialX: number; 16 + let initialY: number; 17 + 18 + function isInteractiveElement(target: HTMLElement): boolean { 19 + if (!options.interactive) return false; 20 + 21 + // Check if the target or its parents match any of the interactive selectors 22 + return options.interactive.some( 23 + (selector) => target.matches(selector) || target.closest(selector) 24 + ); 25 + } 26 + 27 + function handleDragStart(event: DragEvent) { 28 + if (!node.draggable) return; 29 + 30 + console.log('drag start', event); 31 + 32 + dndState.isDragging = true; 33 + dndState.draggedItem = options.dragData; 34 + dndState.sourceContainer = options.container; 35 + dndState.targetContainer = null; 36 + 37 + if (event.dataTransfer) { 38 + event.dataTransfer.effectAllowed = 'move'; 39 + event.dataTransfer.setData('text/plain', JSON.stringify(options.dragData)); 40 + } 41 + 42 + node.classList.add(...draggingClass); 43 + options.callbacks?.onDragStart?.(dndState as DragDropState<T>); 44 + 45 + // **Dispatch the custom event that bubbles up to the container** 46 + const customEvent = new CustomEvent('dragstart-on-container', { bubbles: true }); 47 + node.dispatchEvent(customEvent); 48 + } 49 + 50 + function handleDragEnd() { 51 + node.classList.remove(...draggingClass); 52 + options.callbacks?.onDragEnd?.(dndState as DragDropState<T>); 53 + 54 + // Reset state 55 + dndState.isDragging = false; 56 + dndState.draggedItem = null; 57 + dndState.sourceContainer = ''; 58 + dndState.targetContainer = null; 59 + } 60 + 61 + function handlePointerDown(event: PointerEvent) { 62 + if (!node.draggable) return; 63 + 64 + // If the target is an interactive element, don't start dragging 65 + if (isInteractiveElement(event.target as HTMLElement)) { 66 + return; 67 + } 68 + 69 + // Store initial pointer position 70 + initialX = event.clientX; 71 + initialY = event.clientY; 72 + 73 + dndState.isDragging = true; 74 + dndState.draggedItem = options.dragData; 75 + dndState.sourceContainer = options.container; 76 + dndState.targetContainer = null; 77 + 78 + node.setPointerCapture(event.pointerId); 79 + node.classList.add(...draggingClass); 80 + options.callbacks?.onDragStart?.(dndState as DragDropState<T>); 81 + } 82 + 83 + function handlePointerMove(event: PointerEvent) { 84 + if (!dndState.isDragging) return; 85 + 86 + // Optional: Update visual feedback or position 87 + } 88 + 89 + function handlePointerUp(event: PointerEvent) { 90 + if (!dndState.isDragging) return; 91 + 92 + node.releasePointerCapture(event.pointerId); 93 + node.classList.remove(...draggingClass); 94 + options.callbacks?.onDragEnd?.(dndState as DragDropState<T>); 95 + 96 + // Reset state 97 + dndState.isDragging = false; 98 + dndState.draggedItem = null; 99 + dndState.sourceContainer = ''; 100 + dndState.targetContainer = null; 101 + } 102 + 103 + function handlePointerDownOnHandle(event: PointerEvent) { 104 + node.draggable = !options.disabled; 105 + } 106 + 107 + function handlePointerUpOnHandle(event: PointerEvent) { 108 + node.draggable = false; 109 + } 110 + 111 + // if handles are provided, add event listeners for each handle 112 + if (options.handle) { 113 + // handles are selectors, so we need to get the elements 114 + const handle = node.querySelector(options.handle) as HTMLElement; 115 + 116 + handle.addEventListener('pointerdown', handlePointerDownOnHandle); 117 + handle.addEventListener('pointerup', handlePointerUpOnHandle); 118 + } 119 + 120 + node.draggable = !options.disabled && !options.handle; 121 + 122 + node.addEventListener('dragstart', handleDragStart); 123 + node.addEventListener('dragend', handleDragEnd); 124 + node.addEventListener('pointerdown', handlePointerDown); 125 + node.addEventListener('pointermove', handlePointerMove); 126 + node.addEventListener('pointerup', handlePointerUp); 127 + 128 + return { 129 + update(newOptions: DraggableOptions<T>) { 130 + options = newOptions; 131 + 132 + node.draggable = !options.disabled && !options.handle; 133 + }, 134 + 135 + destroy() { 136 + if (options.handle) { 137 + const handle = node.querySelector(options.handle) as HTMLElement; 138 + handle.removeEventListener('pointerdown', handlePointerDownOnHandle); 139 + handle.removeEventListener('pointerup', handlePointerUpOnHandle); 140 + } 141 + 142 + node.removeEventListener('dragstart', handleDragStart); 143 + node.removeEventListener('dragend', handleDragEnd); 144 + node.removeEventListener('pointerdown', handlePointerDown); 145 + node.removeEventListener('pointermove', handlePointerMove); 146 + } 147 + }; 148 + }
+136
src/lib/sections/resizable/dragndrop/actions/droppable.ts
··· 1 + import { dndState } from '../stores/dnd.svelte.js'; 2 + import type { DragDropOptions, DragDropState } from '../types/index.js'; 3 + 4 + const DEFAULT_DRAG_OVER_CLASS = 5 + 'outline-2 outline-offset-4 outline-accent-500 rounded-2xl outline-dashed'; 6 + 7 + export function droppable<T>(node: HTMLElement, options: DragDropOptions<T>) { 8 + const dragOverClass = (options.attributes?.draggingClass || DEFAULT_DRAG_OVER_CLASS).split(' '); 9 + let dragEnterCounter = 0; // Initialize the counter 10 + 11 + function handleDragEnter(event: DragEvent) { 12 + if (options.disabled) return; 13 + event.preventDefault(); 14 + 15 + dragEnterCounter++; 16 + 17 + dndState.targetContainer = options.container; 18 + dndState.targetElement = event.target as HTMLElement; 19 + 20 + if (dragEnterCounter === 0) return; 21 + 22 + node.classList.add(...dragOverClass); 23 + options.callbacks?.onDragEnter?.(dndState as DragDropState<T>); 24 + } 25 + 26 + function handleDragLeave(event: DragEvent) { 27 + if (options.disabled) return; 28 + 29 + dragEnterCounter--; 30 + 31 + // check if element is still being dragged over 32 + if (dragEnterCounter > 0) return; 33 + 34 + node.classList.remove(...dragOverClass); 35 + 36 + options.callbacks?.onDragLeave?.(dndState as DragDropState<T>); 37 + 38 + if (dndState.targetContainer === options.container && dndState.targetElement === event.target) { 39 + dndState.targetContainer = null; 40 + dndState.targetElement = null; 41 + } 42 + } 43 + 44 + function handleDragOver(event: DragEvent) { 45 + if (options.disabled) return; 46 + event.preventDefault(); 47 + 48 + if (event.dataTransfer) { 49 + event.dataTransfer.dropEffect = 'move'; 50 + } 51 + 52 + options.callbacks?.onDragOver?.(dndState as DragDropState<T>); 53 + } 54 + 55 + async function handleDrop(event: DragEvent) { 56 + if (options.disabled) return; 57 + event.preventDefault(); 58 + 59 + dragEnterCounter = 0; // Reset the counter 60 + node.classList.remove(...dragOverClass); 61 + 62 + try { 63 + if (event.dataTransfer) { 64 + const dragData = JSON.parse(event.dataTransfer.getData('text/plain')) as T; 65 + dndState.draggedItem = dragData; 66 + } 67 + 68 + await options.callbacks?.onDrop?.(dndState as DragDropState<T>); 69 + } catch (error) { 70 + console.error('Drop handling failed:', error); 71 + } 72 + } 73 + 74 + function handleDragStartOnContainer(event: Event) { 75 + if (options.disabled) return; 76 + 77 + // Reset the counter and remove the class 78 + dragEnterCounter = 0; 79 + node.classList.remove(...dragOverClass); 80 + } 81 + 82 + function handlePointerOver(event: PointerEvent) { 83 + if (options.disabled || !dndState.isDragging) return; 84 + 85 + // console.log('handlePointerOver', dndState, dndState.isDragging); 86 + 87 + dndState.targetContainer = options.container; 88 + // node.classList.add(...dragOverClass); 89 + options.callbacks?.onDragEnter?.(dndState as DragDropState<T>); 90 + } 91 + 92 + function handlePointerOut(event: PointerEvent) { 93 + if (options.disabled || !dndState.isDragging) return; 94 + 95 + if (dndState.targetContainer === options.container) { 96 + dndState.targetContainer = null; 97 + } 98 + node.classList.remove(...dragOverClass); 99 + options.callbacks?.onDragLeave?.(dndState as DragDropState<T>); 100 + } 101 + 102 + function handlePointerUp(event: PointerEvent) { 103 + if (options.disabled || !dndState.isDragging) return; 104 + 105 + node.classList.remove(...dragOverClass); 106 + options.callbacks?.onDrop?.(dndState as DragDropState<T>); 107 + } 108 + 109 + node.addEventListener('dragenter', handleDragEnter); 110 + node.addEventListener('dragleave', handleDragLeave); 111 + node.addEventListener('dragover', handleDragOver); 112 + node.addEventListener('drop', handleDrop); 113 + node.addEventListener('dragstart-on-container', handleDragStartOnContainer); 114 + 115 + node.addEventListener('pointerover', handlePointerOver); 116 + node.addEventListener('pointerout', handlePointerOut); 117 + node.addEventListener('pointerup', handlePointerUp); 118 + 119 + return { 120 + update(newOptions: DragDropOptions<T>) { 121 + options = newOptions; 122 + }, 123 + 124 + destroy() { 125 + node.removeEventListener('dragenter', handleDragEnter); 126 + node.removeEventListener('dragleave', handleDragLeave); 127 + node.removeEventListener('dragover', handleDragOver); 128 + node.removeEventListener('drop', handleDrop); 129 + node.removeEventListener('dragstart-on-container', handleDragStartOnContainer); 130 + 131 + node.removeEventListener('pointerover', handlePointerOver); 132 + node.removeEventListener('pointerout', handlePointerOut); 133 + node.removeEventListener('pointerup', handlePointerUp); 134 + } 135 + }; 136 + }
+2
src/lib/sections/resizable/dragndrop/actions/index.ts
··· 1 + export { draggable } from './draggable.js'; 2 + export { droppable } from './droppable.js';
+11
src/lib/sections/resizable/dragndrop/stores/dnd.svelte.ts
··· 1 + import type { DragDropState } from '../types/index.js'; 2 + 3 + // Global DnD state using Svelte 5's state rune 4 + export const dndState = $state<DragDropState>({ 5 + isDragging: false, 6 + draggedItem: null, 7 + sourceContainer: '', 8 + targetContainer: null, 9 + targetElement: null, 10 + invalidDrop: false 11 + });
+48
src/lib/sections/resizable/dragndrop/styles/dnd.css
··· 1 + /* Base draggable styles */ 2 + .svelte-dnd-draggable { 3 + touch-action: none; /* Prevents touch scrolling while dragging */ 4 + user-select: none; /* Prevents text selection during drag */ 5 + } 6 + 7 + /* Active dragging state */ 8 + .svelte-dnd-dragging { 9 + opacity: 0.5; 10 + cursor: grabbing; 11 + } 12 + 13 + /* Draggable hover state */ 14 + .svelte-dnd-draggable:hover { 15 + cursor: grab; 16 + } 17 + 18 + /* Droppable area styles */ 19 + .svelte-dnd-droppable { 20 + position: relative; 21 + } 22 + 23 + /* Active drop target */ 24 + .svelte-dnd-drop-target { 25 + outline: 2px dashed #4caf50; 26 + } 27 + 28 + /* Invalid drop target */ 29 + .svelte-dnd-invalid-target { 30 + outline: 2px dashed #f44336; 31 + } 32 + 33 + /* Drop preview/placeholder */ 34 + .svelte-dnd-placeholder { 35 + border: 2px dashed #9e9e9e; 36 + } 37 + 38 + /* Media queries for responsive design */ 39 + @media (max-width: 600px) { 40 + .svelte-dnd-draggable { 41 + width: 100%; 42 + touch-action: none; /* Prevents scrolling during drag */ 43 + } 44 + 45 + .svelte-dnd-droppable { 46 + padding: 10px; 47 + } 48 + }
+30
src/lib/sections/resizable/dragndrop/types/index.ts
··· 1 + export interface DragDropState<T = unknown> { 2 + isDragging: boolean; 3 + draggedItem: T; 4 + sourceContainer: string; 5 + targetContainer: string | null; 6 + targetElement: HTMLElement | null; 7 + invalidDrop?: boolean; 8 + } 9 + 10 + export interface DragDropCallbacks<T = unknown> { 11 + onDragStart?: (state: DragDropState<T>) => void; 12 + onDragEnter?: (state: DragDropState<T>) => void; 13 + onDragLeave?: (state: DragDropState<T>) => void; 14 + onDragOver?: (state: DragDropState<T>) => void; 15 + onDrop?: (state: DragDropState<T>) => Promise<void> | void; 16 + onDragEnd?: (state: DragDropState<T>) => void; 17 + } 18 + 19 + export interface DragDropAttributes { 20 + draggingClass?: string; 21 + dragOverClass?: string; 22 + } 23 + 24 + export interface DragDropOptions<T = unknown> { 25 + dragData?: T; 26 + container: string; 27 + disabled?: boolean; 28 + callbacks?: DragDropCallbacks<T>; 29 + attributes?: DragDropAttributes; 30 + }