|
9 | 9 | contrastRatio, |
10 | 10 | contrastLevel, |
11 | 11 | } from '$lib/utils/color'; |
12 | | - import {setEyedropperActive} from '$lib/stores/ui.svelte'; |
| 12 | + import {setEyedropperActive, setColorDrag} from '$lib/stores/ui.svelte'; |
13 | 13 | import {onActivate} from '$lib/utils/keyboard'; |
14 | 14 | import LockIcon from '$lib/components/shared/LockIcon.svelte'; |
15 | 15 | import ContextMenu from '$lib/components/shared/ContextMenu.svelte'; |
|
99 | 99 | } |
100 | 100 |
|
101 | 101 | function handleClick(event: MouseEvent) { |
| 102 | + if (didDrag) { |
| 103 | + didDrag = false; |
| 104 | + return; |
| 105 | + } |
102 | 106 | if (event.ctrlKey || event.metaKey) { |
103 | 107 | event.preventDefault(); |
104 | 108 | copyColor(color); |
|
113 | 117 | } |
114 | 118 |
|
115 | 119 | let light = $derived(isLightColor(color)); |
| 120 | +
|
| 121 | + let isDragging = $state(false); |
| 122 | + let didDrag = $state(false); |
| 123 | + let pendingDrag: {startX: number; startY: number; color: string} | null = |
| 124 | + null; |
| 125 | +
|
| 126 | + function onMouseDown(e: MouseEvent) { |
| 127 | + if (e.button !== 0) return; |
| 128 | + clearDrag(); |
| 129 | + didDrag = false; |
| 130 | + if (e.ctrlKey || e.metaKey || e.shiftKey) return; |
| 131 | + if ( |
| 132 | + (e.target as Element).closest('[role="button"]') !== e.currentTarget |
| 133 | + ) |
| 134 | + return; |
| 135 | + pendingDrag = {startX: e.clientX, startY: e.clientY, color}; |
| 136 | + window.addEventListener('mousemove', onDragMove); |
| 137 | + window.addEventListener('mouseup', onDragUp); |
| 138 | + window.addEventListener('blur', clearDrag); |
| 139 | + window.addEventListener('keydown', onDragKeydown); |
| 140 | + } |
| 141 | +
|
| 142 | + function onDragMove(e: MouseEvent) { |
| 143 | + if (!(e.buttons & 1)) { |
| 144 | + clearDrag(); |
| 145 | + return; |
| 146 | + } |
| 147 | + if (!pendingDrag) return; |
| 148 | + if (isDragging) { |
| 149 | + e.preventDefault(); |
| 150 | + setColorDrag({ |
| 151 | + color: pendingDrag.color, |
| 152 | + x: e.clientX, |
| 153 | + y: e.clientY, |
| 154 | + }); |
| 155 | + return; |
| 156 | + } |
| 157 | + const dx = e.clientX - pendingDrag.startX; |
| 158 | + const dy = e.clientY - pendingDrag.startY; |
| 159 | + if (Math.hypot(dx, dy) >= 4) { |
| 160 | + didDrag = true; |
| 161 | + isDragging = true; |
| 162 | + e.preventDefault(); |
| 163 | + setColorDrag({ |
| 164 | + color: pendingDrag.color, |
| 165 | + x: e.clientX, |
| 166 | + y: e.clientY, |
| 167 | + }); |
| 168 | + } |
| 169 | + } |
| 170 | +
|
| 171 | + function onDragKeydown(e: KeyboardEvent) { |
| 172 | + if (e.key === 'Escape') { |
| 173 | + e.preventDefault(); |
| 174 | + clearDrag(); |
| 175 | + } |
| 176 | + } |
| 177 | +
|
| 178 | + function onDragUp(e: MouseEvent) { |
| 179 | + if (e.button === 0) clearDrag(); |
| 180 | + } |
| 181 | +
|
| 182 | + function clearDrag() { |
| 183 | + window.removeEventListener('mousemove', onDragMove); |
| 184 | + window.removeEventListener('mouseup', onDragUp); |
| 185 | + window.removeEventListener('blur', clearDrag); |
| 186 | + window.removeEventListener('keydown', onDragKeydown); |
| 187 | + if (pendingDrag || isDragging) setColorDrag(null); |
| 188 | + pendingDrag = null; |
| 189 | + isDragging = false; |
| 190 | + } |
| 191 | +
|
| 192 | + $effect(() => { |
| 193 | + const _ = color; |
| 194 | + return clearDrag; |
| 195 | + }); |
116 | 196 | </script> |
117 | 197 |
|
118 | 198 | <!-- Keyboard handling lives on the parent ColorPaletteGrid (roving tabindex). --> |
|
123 | 203 | ? 'border-border cursor-default' |
124 | 204 | : selected |
125 | 205 | ? 'border-accent cursor-pointer border-2' |
126 | | - : 'hover:border-accent border-border cursor-pointer hover:z-10 hover:scale-[1.04] hover:shadow-lg'}" |
| 206 | + : 'hover:border-accent border-border cursor-pointer hover:z-10 hover:scale-[1.04] hover:shadow-lg'} |
| 207 | + {isDragging ? 'opacity-60' : ''}" |
127 | 208 | style:background-color={color} |
128 | 209 | role="button" |
129 | 210 | tabindex={focused ? 0 : -1} |
130 | 211 | data-swatch-idx={index} |
131 | 212 | onclick={handleClick} |
| 213 | + onmousedown={onMouseDown} |
132 | 214 | oncontextmenu={handleContextMenu} |
133 | 215 | title={`${label}${role ? ` · ${role}` : ''}\n${color}${ |
134 | 216 | showBadge ? `\nContrast vs BG: ${ratio.toFixed(2)}:1 (${level})` : '' |
135 | | - }\nClick edit · Ctrl+click copy · Shift+click select · Right-click for menu\n← → ↑ ↓ navigate · Enter open · L lock · C copy`} |
| 217 | + }\nClick edit · Ctrl+click copy · Shift+click select · Right-click for menu\nDrag to copy into a template override\n← → ↑ ↓ navigate · Enter open · L lock · C copy`} |
136 | 218 | > |
137 | 219 | {#if selected} |
138 | 220 | <span |
|
0 commit comments