your personal website on atproto - mirror blento.app

commit

+633 -634
+2
.gitignore
··· 21 21 # Vite 22 22 vite.config.js.timestamp-* 23 23 vite.config.ts.timestamp-* 24 + 25 + react-grid-layout
+1 -11
src/lib/cards/_base/BaseCard/BaseCard.svelte
··· 5 5 import type { Snippet } from 'svelte'; 6 6 import type { HTMLAttributes } from 'svelte/elements'; 7 7 import { getColor } from '../..'; 8 - import { getIsCoarse } from '$lib/website/context'; 9 - 10 - function tryGetIsCoarse(): (() => boolean) | undefined { 11 - try { 12 - return getIsCoarse(); 13 - } catch { 14 - return undefined; 15 - } 16 - } 17 - const isCoarse = tryGetIsCoarse(); 18 8 19 9 const colors = { 20 10 base: 'bg-base-200/50 dark:bg-base-950/50', ··· 49 39 id={item.id} 50 40 data-flip-id={item.id} 51 41 bind:this={ref} 52 - draggable={isEditing && !locked && !isCoarse?.()} 42 + draggable={false} 53 43 class={[ 54 44 'card group/card selection:bg-accent-600/50 focus-within:outline-accent-500 @container/card absolute isolate z-0 rounded-3xl outline-offset-2 transition-all duration-200 focus-within:outline-2', 55 45 color ? (colors[color] ?? colors.accent) : colors.base,
+4 -6
src/lib/cards/_base/BaseCard/BaseEditingCard.svelte
··· 193 193 ]} 194 194 {...rest} 195 195 > 196 - {#if isCoarse?.() ? !isSelected : !item.cardData?.locked} 196 + {#if isCoarse?.() && !isSelected} 197 197 <!-- svelte-ignore a11y_click_events_have_key_events --> 198 198 <div 199 199 role="button" 200 200 tabindex="-1" 201 - class={['absolute inset-0', isCoarse?.() ? 'z-20 cursor-pointer' : 'cursor-grab']} 201 + class="absolute inset-0 z-20 cursor-pointer" 202 202 onclick={(e) => { 203 - if (isCoarse?.()) { 204 - e.stopPropagation(); 205 - selectCard?.(item.id); 206 - } 203 + e.stopPropagation(); 204 + selectCard?.(item.id); 207 205 }} 208 206 ></div> 209 207 {/if}
+2 -2
src/lib/layout.ts src/lib/layout/algorithms.ts
··· 6 6 getFirstCollision, 7 7 verticalCompactor 8 8 } from 'react-grid-layout/core'; 9 - import type { Item } from './types'; 9 + import type { Item } from '../types'; 10 10 import { COLUMNS } from '$lib'; 11 - import { clamp } from './helper'; 11 + import { clamp } from '../helper'; 12 12 13 13 function toLayoutItem(item: Item, mobile: boolean): LayoutItem { 14 14 if (mobile) {
+378
src/lib/layout/EditableGrid.svelte
··· 1 + <script lang="ts"> 2 + import type { Snippet } from 'svelte'; 3 + import type { Item } from '$lib/types'; 4 + import { getGridPosition, pixelToGrid, type DragState, type GridPosition } from './grid'; 5 + import { fixCollisions } from './algorithms'; 6 + 7 + let { 8 + items = $bindable(), 9 + isMobile, 10 + selectedCardId, 11 + isCoarse, 12 + children, 13 + ref = $bindable<HTMLDivElement | undefined>(undefined), 14 + onlayoutchange, 15 + ondeselect, 16 + onfiledrop 17 + }: { 18 + items: Item[]; 19 + isMobile: boolean; 20 + selectedCardId: string | null; 21 + isCoarse: boolean; 22 + children: Snippet; 23 + ref?: HTMLDivElement | undefined; 24 + onlayoutchange: () => void; 25 + ondeselect: () => void; 26 + onfiledrop?: (files: File[], gridX: number, gridY: number) => void; 27 + } = $props(); 28 + 29 + // Internal container ref (synced with bindable ref) 30 + let container: HTMLDivElement | undefined = $state(); 31 + $effect(() => { 32 + ref = container; 33 + }); 34 + 35 + const getY = (item: Item) => (isMobile ? (item.mobileY ?? item.y) : item.y); 36 + const getH = (item: Item) => (isMobile ? (item.mobileH ?? item.h) : item.h); 37 + let maxHeight = $derived(items.reduce((max, item) => Math.max(max, getY(item) + getH(item)), 0)); 38 + 39 + // --- Drag state --- 40 + type Phase = 'idle' | 'pending' | 'active'; 41 + 42 + let phase: Phase = $state('idle'); 43 + let pointerId: number = $state(0); 44 + let startClientX = $state(0); 45 + let startClientY = $state(0); 46 + 47 + let dragState: DragState = $state({ 48 + item: null as unknown as Item, 49 + mouseDeltaX: 0, 50 + mouseDeltaY: 0, 51 + originalPositions: new Map(), 52 + lastTargetId: null, 53 + lastPlacement: null 54 + }); 55 + 56 + let lastGridPos: GridPosition | null = $state(null); 57 + 58 + // Ref to the dragged card DOM element (for visual feedback) 59 + let draggedCardEl: HTMLElement | null = null; 60 + 61 + // --- File drag state --- 62 + let fileDragOver = $state(false); 63 + 64 + // --- Pointer event handlers --- 65 + 66 + function handlePointerDown(e: PointerEvent) { 67 + if (phase !== 'idle') return; 68 + 69 + const cardEl = (e.target as HTMLElement)?.closest?.('.card') as HTMLDivElement | null; 70 + if (!cardEl) return; 71 + 72 + // On touch devices, only drag the selected card 73 + if (e.pointerType === 'touch' && cardEl.id !== selectedCardId) return; 74 + 75 + // On mouse, don't intercept interactive elements 76 + if (e.pointerType === 'mouse') { 77 + const tag = (e.target as HTMLElement)?.tagName; 78 + if ( 79 + tag === 'BUTTON' || 80 + tag === 'INPUT' || 81 + tag === 'TEXTAREA' || 82 + (e.target as HTMLElement)?.isContentEditable 83 + ) { 84 + return; 85 + } 86 + } 87 + 88 + const item = items.find((i) => i.id === cardEl.id); 89 + if (!item || item.cardData?.locked) return; 90 + 91 + phase = 'pending'; 92 + pointerId = e.pointerId; 93 + startClientX = e.clientX; 94 + startClientY = e.clientY; 95 + draggedCardEl = cardEl; 96 + 97 + // Pre-compute mouse delta from card rect 98 + const rect = cardEl.getBoundingClientRect(); 99 + dragState.item = item; 100 + dragState.mouseDeltaX = rect.left - e.clientX; 101 + dragState.mouseDeltaY = rect.top - e.clientY; 102 + 103 + // Do NOT preventDefault — allow scroll on touch 104 + document.addEventListener('pointermove', handlePointerMove); 105 + document.addEventListener('pointerup', handlePointerUp); 106 + document.addEventListener('pointercancel', handlePointerCancel); 107 + } 108 + 109 + function activateDrag(e: PointerEvent) { 110 + phase = 'active'; 111 + 112 + try { 113 + (e.target as HTMLElement)?.setPointerCapture?.(pointerId); 114 + } catch { 115 + // setPointerCapture can throw if pointer is already released 116 + } 117 + 118 + // Visual feedback: lift the dragged card 119 + draggedCardEl?.classList.add('dragging'); 120 + 121 + // Store original positions of all items 122 + dragState.originalPositions = new Map(); 123 + for (const it of items) { 124 + dragState.originalPositions.set(it.id, { 125 + x: it.x, 126 + y: it.y, 127 + mobileX: it.mobileX, 128 + mobileY: it.mobileY 129 + }); 130 + } 131 + dragState.lastTargetId = null; 132 + dragState.lastPlacement = null; 133 + 134 + document.body.style.userSelect = 'none'; 135 + } 136 + 137 + function handlePointerMove(e: PointerEvent) { 138 + if (!container) return; 139 + 140 + if (phase === 'pending') { 141 + // Check 3px threshold 142 + const dx = e.clientX - startClientX; 143 + const dy = e.clientY - startClientY; 144 + if (dx * dx + dy * dy < 9) return; 145 + activateDrag(e); 146 + } 147 + 148 + if (phase !== 'active') return; 149 + 150 + // Auto-scroll near edges 151 + const scrollZone = 100; 152 + const scrollSpeed = 10; 153 + const viewportHeight = window.innerHeight; 154 + 155 + if (e.clientY < scrollZone) { 156 + const intensity = 1 - e.clientY / scrollZone; 157 + window.scrollBy(0, -scrollSpeed * intensity); 158 + } else if (e.clientY > viewportHeight - scrollZone) { 159 + const intensity = 1 - (viewportHeight - e.clientY) / scrollZone; 160 + window.scrollBy(0, scrollSpeed * intensity); 161 + } 162 + 163 + const result = getGridPosition(e.clientX, e.clientY, container, dragState, items, isMobile); 164 + if (!result || !dragState.item) return; 165 + 166 + // Skip redundant work if grid position hasn't changed 167 + if ( 168 + lastGridPos && 169 + lastGridPos.x === result.x && 170 + lastGridPos.y === result.y && 171 + lastGridPos.swapWithId === result.swapWithId && 172 + lastGridPos.placement === result.placement 173 + ) { 174 + return; 175 + } 176 + lastGridPos = result; 177 + 178 + const draggedOrigPos = dragState.originalPositions.get(dragState.item.id); 179 + 180 + // Reset all items to original positions first 181 + for (const it of items) { 182 + const origPos = dragState.originalPositions.get(it.id); 183 + if (origPos && it !== dragState.item) { 184 + if (isMobile) { 185 + it.mobileX = origPos.mobileX; 186 + it.mobileY = origPos.mobileY; 187 + } else { 188 + it.x = origPos.x; 189 + it.y = origPos.y; 190 + } 191 + } 192 + } 193 + 194 + // Update dragged item position 195 + if (isMobile) { 196 + dragState.item.mobileX = result.x; 197 + dragState.item.mobileY = result.y; 198 + } else { 199 + dragState.item.x = result.x; 200 + dragState.item.y = result.y; 201 + } 202 + 203 + // Handle horizontal swap 204 + if (result.swapWithId && draggedOrigPos) { 205 + const swapTarget = items.find((it) => it.id === result.swapWithId); 206 + if (swapTarget) { 207 + if (isMobile) { 208 + swapTarget.mobileX = draggedOrigPos.mobileX; 209 + swapTarget.mobileY = draggedOrigPos.mobileY; 210 + } else { 211 + swapTarget.x = draggedOrigPos.x; 212 + swapTarget.y = draggedOrigPos.y; 213 + } 214 + } 215 + } 216 + 217 + fixCollisions( 218 + items, 219 + dragState.item, 220 + isMobile, 221 + false, 222 + draggedOrigPos 223 + ? { 224 + x: isMobile ? draggedOrigPos.mobileX : draggedOrigPos.x, 225 + y: isMobile ? draggedOrigPos.mobileY : draggedOrigPos.y 226 + } 227 + : undefined 228 + ); 229 + } 230 + 231 + function handlePointerUp() { 232 + if (phase === 'active' && dragState.item) { 233 + fixCollisions(items, dragState.item, isMobile); 234 + onlayoutchange(); 235 + } 236 + cleanup(); 237 + } 238 + 239 + function handlePointerCancel() { 240 + if (phase === 'active') { 241 + // Restore all items to original positions 242 + for (const it of items) { 243 + const origPos = dragState.originalPositions.get(it.id); 244 + if (origPos) { 245 + it.x = origPos.x; 246 + it.y = origPos.y; 247 + it.mobileX = origPos.mobileX; 248 + it.mobileY = origPos.mobileY; 249 + } 250 + } 251 + } 252 + cleanup(); 253 + } 254 + 255 + function cleanup() { 256 + draggedCardEl?.classList.remove('dragging'); 257 + draggedCardEl = null; 258 + phase = 'idle'; 259 + lastGridPos = null; 260 + document.body.style.userSelect = ''; 261 + 262 + document.removeEventListener('pointermove', handlePointerMove); 263 + document.removeEventListener('pointerup', handlePointerUp); 264 + document.removeEventListener('pointercancel', handlePointerCancel); 265 + } 266 + 267 + // Ensure cleanup on unmount 268 + $effect(() => { 269 + return () => { 270 + if (phase !== 'idle') cleanup(); 271 + }; 272 + }); 273 + 274 + // For touch: register non-passive touchmove to prevent scroll during active drag 275 + $effect(() => { 276 + if (phase !== 'active' || !container) return; 277 + function preventTouch(e: TouchEvent) { 278 + e.preventDefault(); 279 + } 280 + container.addEventListener('touchmove', preventTouch, { passive: false }); 281 + return () => { 282 + container?.removeEventListener('touchmove', preventTouch); 283 + }; 284 + }); 285 + 286 + function handleClick(e: MouseEvent) { 287 + // Deselect when tapping empty grid space 288 + if (e.target === e.currentTarget || !(e.target as HTMLElement)?.closest?.('.card')) { 289 + ondeselect(); 290 + } 291 + } 292 + 293 + // --- File drop handlers --- 294 + 295 + function hasImageFile(dt: DataTransfer): boolean { 296 + if (dt.items) { 297 + for (let i = 0; i < dt.items.length; i++) { 298 + const item = dt.items[i]; 299 + if (item && item.kind === 'file' && item.type.startsWith('image/')) { 300 + return true; 301 + } 302 + } 303 + } else if (dt.files) { 304 + for (let i = 0; i < dt.files.length; i++) { 305 + const file = dt.files[i]; 306 + if (file?.type.startsWith('image/')) { 307 + return true; 308 + } 309 + } 310 + } 311 + return false; 312 + } 313 + 314 + function handleFileDragOver(event: DragEvent) { 315 + const dt = event.dataTransfer; 316 + if (!dt) return; 317 + 318 + if (hasImageFile(dt)) { 319 + event.preventDefault(); 320 + event.stopPropagation(); 321 + fileDragOver = true; 322 + } 323 + } 324 + 325 + function handleFileDragLeave(event: DragEvent) { 326 + event.preventDefault(); 327 + event.stopPropagation(); 328 + fileDragOver = false; 329 + } 330 + 331 + function handleFileDrop(event: DragEvent) { 332 + event.preventDefault(); 333 + event.stopPropagation(); 334 + fileDragOver = false; 335 + 336 + if (!event.dataTransfer?.files?.length || !onfiledrop || !container) return; 337 + 338 + const imageFiles = Array.from(event.dataTransfer.files).filter((f) => 339 + f?.type.startsWith('image/') 340 + ); 341 + if (imageFiles.length === 0) return; 342 + 343 + const cardW = isMobile ? 4 : 2; 344 + const { gridX, gridY } = pixelToGrid(event.clientX, event.clientY, container, isMobile, cardW); 345 + 346 + onfiledrop(imageFiles, gridX, gridY); 347 + } 348 + </script> 349 + 350 + <svelte:window 351 + ondragover={handleFileDragOver} 352 + ondragleave={handleFileDragLeave} 353 + ondrop={handleFileDrop} 354 + /> 355 + 356 + <!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events --> 357 + <div 358 + bind:this={container} 359 + onpointerdown={handlePointerDown} 360 + onclick={handleClick} 361 + ondragstart={(e) => e.preventDefault()} 362 + class={[ 363 + '@container/grid pointer-events-auto relative col-span-3 rounded-4xl px-2 py-8 @5xl/wrapper:px-8', 364 + fileDragOver && 'outline-accent-500 outline-3 -outline-offset-10 outline-dashed' 365 + ]} 366 + > 367 + {@render children()} 368 + 369 + <div style="height: {((maxHeight + 2) / 8) * 100}cqw;"></div> 370 + </div> 371 + 372 + <style> 373 + :global(.card.dragging) { 374 + z-index: 50 !important; 375 + scale: 1.03; 376 + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); 377 + } 378 + </style>
+190
src/lib/layout/grid.ts
··· 1 + import { COLUMNS, margin, mobileMargin } from '$lib'; 2 + import { clamp } from '$lib/helper'; 3 + import type { Item } from '$lib/types'; 4 + 5 + export type GridPosition = { 6 + x: number; 7 + y: number; 8 + swapWithId: string | null; 9 + placement: 'above' | 'below' | null; 10 + }; 11 + 12 + export type DragState = { 13 + item: Item; 14 + mouseDeltaX: number; 15 + mouseDeltaY: number; 16 + originalPositions: Map<string, { x: number; y: number; mobileX: number; mobileY: number }>; 17 + lastTargetId: string | null; 18 + lastPlacement: 'above' | 'below' | null; 19 + }; 20 + 21 + /** 22 + * Convert client coordinates to a grid position with swap detection and hysteresis. 23 + * Returns undefined if container or dragState.item is missing. 24 + * Mutates dragState.lastTargetId and dragState.lastPlacement for hysteresis tracking. 25 + */ 26 + export function getGridPosition( 27 + clientX: number, 28 + clientY: number, 29 + container: HTMLElement, 30 + dragState: DragState, 31 + items: Item[], 32 + isMobile: boolean 33 + ): GridPosition | undefined { 34 + if (!dragState.item) return; 35 + 36 + // x, y represent the top-left corner of the dragged card 37 + const x = clientX + dragState.mouseDeltaX; 38 + const y = clientY + dragState.mouseDeltaY; 39 + 40 + const rect = container.getBoundingClientRect(); 41 + const currentMargin = isMobile ? mobileMargin : margin; 42 + const cellSize = (rect.width - currentMargin * 2) / COLUMNS; 43 + 44 + // Get card dimensions based on current view mode 45 + const cardW = isMobile ? dragState.item.mobileW : dragState.item.w; 46 + const cardH = isMobile ? dragState.item.mobileH : dragState.item.h; 47 + 48 + // Get dragged card's original position 49 + const draggedOrigPos = dragState.originalPositions.get(dragState.item.id); 50 + const draggedOrigY = draggedOrigPos ? (isMobile ? draggedOrigPos.mobileY : draggedOrigPos.y) : 0; 51 + 52 + // Calculate raw grid position based on top-left of dragged card 53 + let gridX = clamp(Math.round((x - rect.left - currentMargin) / cellSize), 0, COLUMNS - cardW); 54 + gridX = Math.floor(gridX / 2) * 2; 55 + 56 + let gridY = Math.max(Math.round((y - rect.top - currentMargin) / cellSize), 0); 57 + 58 + if (isMobile) { 59 + gridX = Math.floor(gridX / 2) * 2; 60 + gridY = Math.floor(gridY / 2) * 2; 61 + } 62 + 63 + // Find if we're hovering over another card (using ORIGINAL positions) 64 + const centerGridY = gridY + cardH / 2; 65 + const centerGridX = gridX + cardW / 2; 66 + 67 + let swapWithId: string | null = null; 68 + let placement: 'above' | 'below' | null = null; 69 + 70 + for (const other of items) { 71 + if (other === dragState.item) continue; 72 + 73 + // Use original positions for hit testing 74 + const origPos = dragState.originalPositions.get(other.id); 75 + if (!origPos) continue; 76 + 77 + const otherX = isMobile ? origPos.mobileX : origPos.x; 78 + const otherY = isMobile ? origPos.mobileY : origPos.y; 79 + const otherW = isMobile ? other.mobileW : other.w; 80 + const otherH = isMobile ? other.mobileH : other.h; 81 + 82 + // Check if dragged card's center point is within this card's original bounds 83 + if ( 84 + centerGridX >= otherX && 85 + centerGridX < otherX + otherW && 86 + centerGridY >= otherY && 87 + centerGridY < otherY + otherH 88 + ) { 89 + // Check if this is a swap situation: 90 + // Cards have the same dimensions and are on the same row 91 + const canSwap = cardW === otherW && cardH === otherH && draggedOrigY === otherY; 92 + 93 + if (canSwap) { 94 + // Swap positions 95 + swapWithId = other.id; 96 + gridX = otherX; 97 + gridY = otherY; 98 + placement = null; 99 + 100 + dragState.lastTargetId = other.id; 101 + dragState.lastPlacement = null; 102 + } else { 103 + // Vertical placement (above/below) 104 + // Detect drag direction: if dragging up, always place above 105 + const isDraggingUp = gridY < draggedOrigY; 106 + 107 + if (isDraggingUp) { 108 + // When dragging up, always place above 109 + placement = 'above'; 110 + } else { 111 + // When dragging down, use top/bottom half logic 112 + const midpointY = otherY + otherH / 2; 113 + const hysteresis = 0.3; 114 + 115 + if (dragState.lastTargetId === other.id && dragState.lastPlacement) { 116 + if (dragState.lastPlacement === 'above') { 117 + placement = centerGridY > midpointY + hysteresis ? 'below' : 'above'; 118 + } else { 119 + placement = centerGridY < midpointY - hysteresis ? 'above' : 'below'; 120 + } 121 + } else { 122 + placement = centerGridY < midpointY ? 'above' : 'below'; 123 + } 124 + } 125 + 126 + dragState.lastTargetId = other.id; 127 + dragState.lastPlacement = placement; 128 + 129 + if (placement === 'above') { 130 + gridY = otherY; 131 + } else { 132 + gridY = otherY + otherH; 133 + } 134 + } 135 + break; 136 + } 137 + } 138 + 139 + // If we're not over any card, clear the tracking 140 + if (!swapWithId && !placement) { 141 + dragState.lastTargetId = null; 142 + dragState.lastPlacement = null; 143 + } 144 + 145 + return { x: gridX, y: gridY, swapWithId, placement }; 146 + } 147 + 148 + /** 149 + * Get the grid Y coordinate at the viewport center. 150 + */ 151 + export function getViewportCenterGridY( 152 + container: HTMLElement, 153 + isMobile: boolean 154 + ): { gridY: number; isMobile: boolean } { 155 + const rect = container.getBoundingClientRect(); 156 + const currentMargin = isMobile ? mobileMargin : margin; 157 + const cellSize = (rect.width - currentMargin * 2) / COLUMNS; 158 + const viewportCenterY = window.innerHeight / 2; 159 + const gridY = (viewportCenterY - rect.top - currentMargin) / cellSize; 160 + return { gridY, isMobile }; 161 + } 162 + 163 + /** 164 + * Convert pixel drop coordinates to grid position. Used for file drops. 165 + */ 166 + export function pixelToGrid( 167 + clientX: number, 168 + clientY: number, 169 + container: HTMLElement, 170 + isMobile: boolean, 171 + cardW: number 172 + ): { gridX: number; gridY: number } { 173 + const rect = container.getBoundingClientRect(); 174 + const currentMargin = isMobile ? mobileMargin : margin; 175 + const cellSize = (rect.width - currentMargin * 2) / COLUMNS; 176 + 177 + let gridX = clamp( 178 + Math.round((clientX - rect.left - currentMargin) / cellSize), 179 + 0, 180 + COLUMNS - cardW 181 + ); 182 + gridX = Math.floor(gridX / 2) * 2; 183 + 184 + let gridY = Math.max(Math.round((clientY - rect.top - currentMargin) / cellSize), 0); 185 + if (isMobile) { 186 + gridY = Math.floor(gridY / 2) * 2; 187 + } 188 + 189 + return { gridX, gridY }; 190 + }
+15
src/lib/layout/index.ts
··· 1 + export { 2 + overlaps, 3 + fixCollisions, 4 + fixAllCollisions, 5 + compactItems, 6 + setPositionOfNewItem, 7 + findValidPosition 8 + } from './algorithms'; 9 + 10 + export { shouldMirror, mirrorItemSize, mirrorLayout } from './mirror'; 11 + 12 + export { getGridPosition, getViewportCenterGridY, pixelToGrid } from './grid'; 13 + export type { GridPosition, DragState } from './grid'; 14 + 15 + export { default as EditableGrid } from './EditableGrid.svelte';
+40 -614
src/lib/website/EditableWebsite.svelte
··· 1 1 <script lang="ts"> 2 - import { Button, Modal, toast, Toaster, Sidebar } from '@foxui/core'; 3 - import { COLUMNS, margin, mobileMargin } from '$lib'; 2 + import { Button, Modal, toast, Toaster } from '@foxui/core'; 3 + import { COLUMNS } from '$lib'; 4 4 import { 5 5 checkAndUploadImage, 6 - clamp, 7 6 createEmptyCard, 8 7 getHideProfileSection, 9 8 getProfilePosition, ··· 27 26 import Context from './Context.svelte'; 28 27 import Head from './Head.svelte'; 29 28 import Account from './Account.svelte'; 30 - import { SelectThemePopover } from '$lib/components/select-theme'; 31 29 import EditBar from './EditBar.svelte'; 32 30 import SaveModal from './SaveModal.svelte'; 33 31 import FloatingEditButton from './FloatingEditButton.svelte'; ··· 36 34 import { launchConfetti } from '@foxui/visual'; 37 35 import Controls from './Controls.svelte'; 38 36 import CardCommand from '$lib/components/card-command/CardCommand.svelte'; 39 - import { shouldMirror, mirrorLayout } from './layout-mirror'; 40 37 import { SvelteMap } from 'svelte/reactivity'; 41 - import { fixCollisions, compactItems, fixAllCollisions, setPositionOfNewItem } from '$lib/layout'; 38 + import { 39 + fixCollisions, 40 + compactItems, 41 + fixAllCollisions, 42 + setPositionOfNewItem, 43 + shouldMirror, 44 + mirrorLayout, 45 + getViewportCenterGridY, 46 + EditableGrid 47 + } from '$lib/layout'; 42 48 43 49 let { 44 50 data ··· 49 55 // Check if floating login button will be visible (to hide MadeWithBlento) 50 56 const showLoginOnEditPage = $derived(!user.isInitializing && !user.isLoggedIn); 51 57 52 - function updateTheme(newAccent: string, newBase: string) { 53 - data.publication.preferences ??= {}; 54 - data.publication.preferences.accentColor = newAccent; 55 - data.publication.preferences.baseColor = newBase; 56 - hasUnsavedChanges = true; 57 - data = { ...data }; 58 - } 59 - 60 - let imageDragOver = $state(false); 61 - 62 58 // svelte-ignore state_referenced_locally 63 59 let items: Item[] = $state(data.cards); 64 60 ··· 97 93 return () => window.removeEventListener('beforeunload', handleBeforeUnload); 98 94 }); 99 95 100 - let container: HTMLDivElement | undefined = $state(); 101 - 102 - let activeDragElement: { 103 - element: HTMLDivElement | null; 104 - item: Item | null; 105 - w: number; 106 - h: number; 107 - x: number; 108 - y: number; 109 - mouseDeltaX: number; 110 - mouseDeltaY: number; 111 - // For hysteresis - track last decision to prevent flickering 112 - lastTargetId: string | null; 113 - lastPlacement: 'above' | 'below' | null; 114 - // Store original positions to reset from during drag 115 - originalPositions: Map<string, { x: number; y: number; mobileX: number; mobileY: number }>; 116 - } = $state({ 117 - element: null, 118 - item: null, 119 - w: 0, 120 - h: 0, 121 - x: -1, 122 - y: -1, 123 - mouseDeltaX: 0, 124 - mouseDeltaY: 0, 125 - lastTargetId: null, 126 - lastPlacement: null, 127 - originalPositions: new Map() 128 - }); 96 + let gridContainer: HTMLDivElement | undefined = $state(); 129 97 130 98 let showingMobileView = $state(false); 131 99 let isMobile = $derived(showingMobileView || (innerWidth.current ?? 1000) < 1024); ··· 160 128 161 129 const getY = (item: Item) => (isMobile ? (item.mobileY ?? item.y) : item.y); 162 130 const getH = (item: Item) => (isMobile ? (item.mobileH ?? item.h) : item.h); 163 - 164 131 let maxHeight = $derived(items.reduce((max, item) => Math.max(max, getY(item) + getH(item)), 0)); 165 132 166 - function getViewportCenterGridY(): { gridY: number; isMobile: boolean } | undefined { 167 - if (!container) return undefined; 168 - const rect = container.getBoundingClientRect(); 169 - const currentMargin = isMobile ? mobileMargin : margin; 170 - const cellSize = (rect.width - currentMargin * 2) / COLUMNS; 171 - const viewportCenterY = window.innerHeight / 2; 172 - const gridY = (viewportCenterY - rect.top - currentMargin) / cellSize; 173 - return { gridY, isMobile }; 174 - } 175 - 176 133 function newCard(type: string = 'link', cardData?: any) { 177 134 selectedCardId = null; 178 135 ··· 221 178 if (!newItem.item) return; 222 179 const item = newItem.item; 223 180 224 - const viewportCenter = getViewportCenterGridY(); 181 + const viewportCenter = gridContainer 182 + ? getViewportCenterGridY(gridContainer, isMobile) 183 + : undefined; 225 184 setPositionOfNewItem(item, items, viewportCenter); 226 185 227 186 items = [...items, item]; ··· 239 198 await tick(); 240 199 cleanupDialogArtifacts(); 241 200 242 - scrollToItem(item, isMobile, container); 201 + scrollToItem(item, isMobile, gridContainer); 243 202 } 244 203 245 204 let isSaving = $state(false); ··· 558 517 } 559 518 } 560 519 561 - let lastGridPos: { 562 - x: number; 563 - y: number; 564 - swapWithId: string | null; 565 - placement: string | null; 566 - } | null = $state(null); 567 - 568 - let debugPoint = $state({ x: 0, y: 0 }); 569 - 570 - function getGridPosition( 571 - clientX: number, 572 - clientY: number 573 - ): 574 - | { x: number; y: number; swapWithId: string | null; placement: 'above' | 'below' | null } 575 - | undefined { 576 - if (!container || !activeDragElement.item) return; 577 - 578 - // x, y represent the top-left corner of the dragged card 579 - const x = clientX + activeDragElement.mouseDeltaX; 580 - const y = clientY + activeDragElement.mouseDeltaY; 581 - 582 - const rect = container.getBoundingClientRect(); 583 - const currentMargin = isMobile ? mobileMargin : margin; 584 - const cellSize = (rect.width - currentMargin * 2) / COLUMNS; 585 - 586 - // Get card dimensions based on current view mode 587 - const cardW = isMobile 588 - ? (activeDragElement.item?.mobileW ?? activeDragElement.w) 589 - : activeDragElement.w; 590 - const cardH = isMobile 591 - ? (activeDragElement.item?.mobileH ?? activeDragElement.h) 592 - : activeDragElement.h; 593 - 594 - // Get dragged card's original position 595 - const draggedOrigPos = activeDragElement.originalPositions.get(activeDragElement.item.id); 596 - 597 - const draggedOrigY = draggedOrigPos 598 - ? isMobile 599 - ? draggedOrigPos.mobileY 600 - : draggedOrigPos.y 601 - : 0; 602 - 603 - // Calculate raw grid position based on top-left of dragged card 604 - let gridX = clamp(Math.round((x - rect.left - currentMargin) / cellSize), 0, COLUMNS - cardW); 605 - gridX = Math.floor(gridX / 2) * 2; 606 - 607 - let gridY = Math.max(Math.round((y - rect.top - currentMargin) / cellSize), 0); 608 - 609 - if (isMobile) { 610 - gridX = Math.floor(gridX / 2) * 2; 611 - gridY = Math.floor(gridY / 2) * 2; 612 - } 613 - 614 - // Find if we're hovering over another card (using ORIGINAL positions) 615 - const centerGridY = gridY + cardH / 2; 616 - const centerGridX = gridX + cardW / 2; 617 - 618 - let swapWithId: string | null = null; 619 - let placement: 'above' | 'below' | null = null; 620 - 621 - for (const other of items) { 622 - if (other === activeDragElement.item) continue; 623 - 624 - // Use original positions for hit testing 625 - const origPos = activeDragElement.originalPositions.get(other.id); 626 - if (!origPos) continue; 627 - 628 - const otherX = isMobile ? origPos.mobileX : origPos.x; 629 - const otherY = isMobile ? origPos.mobileY : origPos.y; 630 - const otherW = isMobile ? other.mobileW : other.w; 631 - const otherH = isMobile ? other.mobileH : other.h; 632 - 633 - // Check if dragged card's center point is within this card's original bounds 634 - if ( 635 - centerGridX >= otherX && 636 - centerGridX < otherX + otherW && 637 - centerGridY >= otherY && 638 - centerGridY < otherY + otherH 639 - ) { 640 - // Check if this is a swap situation: 641 - // Cards have the same dimensions and are on the same row 642 - const canSwap = cardW === otherW && cardH === otherH && draggedOrigY === otherY; 643 - 644 - if (canSwap) { 645 - // Swap positions 646 - swapWithId = other.id; 647 - gridX = otherX; 648 - gridY = otherY; 649 - placement = null; 650 - 651 - activeDragElement.lastTargetId = other.id; 652 - activeDragElement.lastPlacement = null; 653 - } else { 654 - // Vertical placement (above/below) 655 - // Detect drag direction: if dragging up, always place above 656 - const isDraggingUp = gridY < draggedOrigY; 657 - 658 - if (isDraggingUp) { 659 - // When dragging up, always place above 660 - placement = 'above'; 661 - } else { 662 - // When dragging down, use top/bottom half logic 663 - const midpointY = otherY + otherH / 2; 664 - const hysteresis = 0.3; 665 - 666 - if (activeDragElement.lastTargetId === other.id && activeDragElement.lastPlacement) { 667 - if (activeDragElement.lastPlacement === 'above') { 668 - placement = centerGridY > midpointY + hysteresis ? 'below' : 'above'; 669 - } else { 670 - placement = centerGridY < midpointY - hysteresis ? 'above' : 'below'; 671 - } 672 - } else { 673 - placement = centerGridY < midpointY ? 'above' : 'below'; 674 - } 675 - } 676 - 677 - activeDragElement.lastTargetId = other.id; 678 - activeDragElement.lastPlacement = placement; 679 - 680 - if (placement === 'above') { 681 - gridY = otherY; 682 - } else { 683 - gridY = otherY + otherH; 684 - } 685 - } 686 - break; 687 - } 688 - } 689 - 690 - // If we're not over any card, clear the tracking 691 - if (!swapWithId && !placement) { 692 - activeDragElement.lastTargetId = null; 693 - activeDragElement.lastPlacement = null; 694 - } 695 - 696 - debugPoint.x = x - rect.left; 697 - debugPoint.y = y - rect.top + currentMargin; 698 - 699 - return { x: gridX, y: gridY, swapWithId, placement }; 700 - } 701 - 702 - function getDragXY( 703 - e: DragEvent & { 704 - currentTarget: EventTarget & HTMLDivElement; 705 - } 706 - ) { 707 - return getGridPosition(e.clientX, e.clientY); 708 - } 709 - 710 - // Touch drag system (instant drag on selected card) 711 - let touchDragActive = $state(false); 712 - 713 - function touchStart(e: TouchEvent) { 714 - if (!selectedCardId || !container) return; 715 - const touch = e.touches[0]; 716 - if (!touch) return; 717 - 718 - // Check if the touch is on the selected card element 719 - const target = (e.target as HTMLElement)?.closest?.('.card'); 720 - if (!target || target.id !== selectedCardId) return; 721 - 722 - const item = items.find((i) => i.id === selectedCardId); 723 - if (!item || item.cardData?.locked) return; 724 - 725 - // Start dragging immediately 726 - touchDragActive = true; 727 - 728 - const cardEl = container.querySelector(`#${CSS.escape(selectedCardId)}`) as HTMLDivElement; 729 - if (!cardEl) return; 730 - 731 - activeDragElement.element = cardEl; 732 - activeDragElement.w = item.w; 733 - activeDragElement.h = item.h; 734 - activeDragElement.item = item; 735 - 736 - // Store original positions of all items 737 - activeDragElement.originalPositions = new Map(); 738 - for (const it of items) { 739 - activeDragElement.originalPositions.set(it.id, { 740 - x: it.x, 741 - y: it.y, 742 - mobileX: it.mobileX, 743 - mobileY: it.mobileY 744 - }); 745 - } 746 - 747 - const rect = cardEl.getBoundingClientRect(); 748 - activeDragElement.mouseDeltaX = rect.left - touch.clientX; 749 - activeDragElement.mouseDeltaY = rect.top - touch.clientY; 750 - } 751 - 752 - function touchMove(e: TouchEvent) { 753 - if (!touchDragActive) return; 754 - 755 - const touch = e.touches[0]; 756 - if (!touch) return; 757 - 758 - e.preventDefault(); 759 - 760 - // Auto-scroll near edges (always process, even if grid pos unchanged) 761 - const scrollZone = 100; 762 - const scrollSpeed = 10; 763 - const viewportHeight = window.innerHeight; 764 - 765 - if (touch.clientY < scrollZone) { 766 - const intensity = 1 - touch.clientY / scrollZone; 767 - window.scrollBy(0, -scrollSpeed * intensity); 768 - } else if (touch.clientY > viewportHeight - scrollZone) { 769 - const intensity = 1 - (viewportHeight - touch.clientY) / scrollZone; 770 - window.scrollBy(0, scrollSpeed * intensity); 771 - } 772 - 773 - const result = getGridPosition(touch.clientX, touch.clientY); 774 - if (!result || !activeDragElement.item) return; 775 - 776 - // Skip redundant work if grid position hasn't changed 777 - if ( 778 - lastGridPos && 779 - lastGridPos.x === result.x && 780 - lastGridPos.y === result.y && 781 - lastGridPos.swapWithId === result.swapWithId && 782 - lastGridPos.placement === result.placement 783 - ) { 784 - return; 785 - } 786 - lastGridPos = { 787 - x: result.x, 788 - y: result.y, 789 - swapWithId: result.swapWithId, 790 - placement: result.placement 791 - }; 792 - 793 - const draggedOrigPos = activeDragElement.originalPositions.get(activeDragElement.item.id); 794 - 795 - // Reset all items to original positions first 796 - for (const it of items) { 797 - const origPos = activeDragElement.originalPositions.get(it.id); 798 - if (origPos && it !== activeDragElement.item) { 799 - if (isMobile) { 800 - it.mobileX = origPos.mobileX; 801 - it.mobileY = origPos.mobileY; 802 - } else { 803 - it.x = origPos.x; 804 - it.y = origPos.y; 805 - } 806 - } 807 - } 808 - 809 - // Update dragged item position 810 - if (isMobile) { 811 - activeDragElement.item.mobileX = result.x; 812 - activeDragElement.item.mobileY = result.y; 813 - } else { 814 - activeDragElement.item.x = result.x; 815 - activeDragElement.item.y = result.y; 816 - } 817 - 818 - // Handle horizontal swap 819 - if (result.swapWithId && draggedOrigPos) { 820 - const swapTarget = items.find((it) => it.id === result.swapWithId); 821 - if (swapTarget) { 822 - if (isMobile) { 823 - swapTarget.mobileX = draggedOrigPos.mobileX; 824 - swapTarget.mobileY = draggedOrigPos.mobileY; 825 - } else { 826 - swapTarget.x = draggedOrigPos.x; 827 - swapTarget.y = draggedOrigPos.y; 828 - } 829 - } 830 - } 831 - 832 - fixCollisions( 833 - items, 834 - activeDragElement.item, 835 - isMobile, 836 - false, 837 - draggedOrigPos 838 - ? { 839 - x: isMobile ? draggedOrigPos.mobileX : draggedOrigPos.x, 840 - y: isMobile ? draggedOrigPos.mobileY : draggedOrigPos.y 841 - } 842 - : undefined 843 - ); 844 - } 845 - 846 - function touchEnd() { 847 - if (touchDragActive && activeDragElement.item) { 848 - // Finalize position 849 - fixCollisions(items, activeDragElement.item, isMobile); 850 - onLayoutChanged(); 851 - 852 - activeDragElement.x = -1; 853 - activeDragElement.y = -1; 854 - activeDragElement.element = null; 855 - activeDragElement.item = null; 856 - activeDragElement.lastTargetId = null; 857 - activeDragElement.lastPlacement = null; 858 - } 859 - 860 - lastGridPos = null; 861 - touchDragActive = false; 862 - } 863 - 864 - // Only register non-passive touchmove when actively dragging 865 - $effect(() => { 866 - const el = container; 867 - if (!touchDragActive || !el) return; 868 - 869 - el.addEventListener('touchmove', touchMove, { passive: false }); 870 - return () => { 871 - el.removeEventListener('touchmove', touchMove); 872 - }; 873 - }); 874 - 875 520 let linkValue = $state(''); 876 521 877 522 function addLink(url: string, specificCardDef?: CardDefinition) { ··· 995 640 fixCollisions(items, item, isMobile); 996 641 fixCollisions(items, item, !isMobile); 997 642 } else { 998 - const viewportCenter = getViewportCenterGridY(); 643 + const viewportCenter = gridContainer 644 + ? getViewportCenterGridY(gridContainer, isMobile) 645 + : undefined; 999 646 setPositionOfNewItem(item, items, viewportCenter); 1000 647 items = [...items, item]; 1001 648 fixCollisions(items, item, false, true); ··· 1008 655 1009 656 await tick(); 1010 657 1011 - scrollToItem(item, isMobile, container); 1012 - } 1013 - 1014 - function handleImageDragOver(event: DragEvent) { 1015 - const dt = event.dataTransfer; 1016 - if (!dt) return; 1017 - 1018 - let hasImage = false; 1019 - if (dt.items) { 1020 - for (let i = 0; i < dt.items.length; i++) { 1021 - const item = dt.items[i]; 1022 - if (item && item.kind === 'file' && item.type.startsWith('image/')) { 1023 - hasImage = true; 1024 - break; 1025 - } 1026 - } 1027 - } else if (dt.files) { 1028 - for (let i = 0; i < dt.files.length; i++) { 1029 - const file = dt.files[i]; 1030 - if (file?.type.startsWith('image/')) { 1031 - hasImage = true; 1032 - break; 1033 - } 1034 - } 1035 - } 1036 - 1037 - if (hasImage) { 1038 - event.preventDefault(); 1039 - event.stopPropagation(); 1040 - 1041 - imageDragOver = true; 1042 - } 658 + scrollToItem(item, isMobile, gridContainer); 1043 659 } 1044 660 1045 - function handleImageDragLeave(event: DragEvent) { 1046 - event.preventDefault(); 1047 - event.stopPropagation(); 1048 - imageDragOver = false; 1049 - } 1050 - 1051 - async function handleImageDrop(event: DragEvent) { 1052 - event.preventDefault(); 1053 - event.stopPropagation(); 1054 - const dropX = event.clientX; 1055 - const dropY = event.clientY; 1056 - imageDragOver = false; 1057 - 1058 - if (!event.dataTransfer?.files?.length) return; 1059 - 1060 - const imageFiles = Array.from(event.dataTransfer.files).filter((f) => 1061 - f?.type.startsWith('image/') 1062 - ); 1063 - if (imageFiles.length === 0) return; 1064 - 1065 - // Calculate starting grid position from drop coordinates 1066 - let gridX = 0; 1067 - let gridY = 0; 1068 - if (container) { 1069 - const rect = container.getBoundingClientRect(); 1070 - const currentMargin = isMobile ? mobileMargin : margin; 1071 - const cellSize = (rect.width - currentMargin * 2) / COLUMNS; 1072 - const cardW = isMobile ? 4 : 2; 1073 - 1074 - gridX = clamp(Math.round((dropX - rect.left - currentMargin) / cellSize), 0, COLUMNS - cardW); 1075 - gridX = Math.floor(gridX / 2) * 2; 1076 - 1077 - gridY = Math.max(Math.round((dropY - rect.top - currentMargin) / cellSize), 0); 1078 - if (isMobile) { 1079 - gridY = Math.floor(gridY / 2) * 2; 1080 - } 1081 - } 1082 - 1083 - for (let i = 0; i < imageFiles.length; i++) { 661 + async function handleFileDrop(files: File[], gridX: number, gridY: number) { 662 + for (let i = 0; i < files.length; i++) { 1084 663 // First image gets the drop position, rest use normal placement 1085 664 if (i === 0) { 1086 - await processImageFile(imageFiles[i], gridX, gridY); 665 + await processImageFile(files[i], gridX, gridY); 1087 666 } else { 1088 - await processImageFile(imageFiles[i]); 667 + await processImageFile(files[i]); 1089 668 } 1090 669 } 1091 670 } ··· 1133 712 objectUrl 1134 713 }; 1135 714 1136 - const viewportCenter = getViewportCenterGridY(); 715 + const viewportCenter = gridContainer 716 + ? getViewportCenterGridY(gridContainer, isMobile) 717 + : undefined; 1137 718 setPositionOfNewItem(item, items, viewportCenter); 1138 719 items = [...items, item]; 1139 720 fixCollisions(items, item, false, true); ··· 1145 726 1146 727 await tick(); 1147 728 1148 - scrollToItem(item, isMobile, container); 729 + scrollToItem(item, isMobile, gridContainer); 1149 730 } 1150 731 1151 732 async function handleVideoInputChange(event: Event) { ··· 1175 756 1176 757 addLink(link); 1177 758 }} 1178 - /> 1179 - 1180 - <svelte:window 1181 - ondragover={handleImageDragOver} 1182 - ondragleave={handleImageDragLeave} 1183 - ondrop={handleImageDrop} 1184 759 /> 1185 760 1186 761 <Head ··· 1292 867 ]} 1293 868 > 1294 869 <div class="pointer-events-none"></div> 1295 - <!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events --> 1296 - <div 1297 - bind:this={container} 1298 - onclick={(e) => { 1299 - // Deselect when tapping empty grid space 1300 - if (e.target === e.currentTarget || !(e.target as HTMLElement)?.closest?.('.card')) { 1301 - selectedCardId = null; 1302 - } 870 + <EditableGrid 871 + bind:items 872 + bind:ref={gridContainer} 873 + {isMobile} 874 + {selectedCardId} 875 + {isCoarse} 876 + onlayoutchange={onLayoutChanged} 877 + ondeselect={() => { 878 + selectedCardId = null; 1303 879 }} 1304 - ontouchstart={touchStart} 1305 - ontouchend={touchEnd} 1306 - ondragover={(e) => { 1307 - e.preventDefault(); 1308 - 1309 - // Auto-scroll when dragging near top or bottom of viewport (always process) 1310 - const scrollZone = 100; 1311 - const scrollSpeed = 10; 1312 - const viewportHeight = window.innerHeight; 1313 - 1314 - if (e.clientY < scrollZone) { 1315 - const intensity = 1 - e.clientY / scrollZone; 1316 - window.scrollBy(0, -scrollSpeed * intensity); 1317 - } else if (e.clientY > viewportHeight - scrollZone) { 1318 - const intensity = 1 - (viewportHeight - e.clientY) / scrollZone; 1319 - window.scrollBy(0, scrollSpeed * intensity); 1320 - } 1321 - 1322 - const result = getDragXY(e); 1323 - if (!result) return; 1324 - 1325 - // Skip redundant work if grid position hasn't changed 1326 - if ( 1327 - lastGridPos && 1328 - lastGridPos.x === result.x && 1329 - lastGridPos.y === result.y && 1330 - lastGridPos.swapWithId === result.swapWithId && 1331 - lastGridPos.placement === result.placement 1332 - ) { 1333 - return; 1334 - } 1335 - lastGridPos = { 1336 - x: result.x, 1337 - y: result.y, 1338 - swapWithId: result.swapWithId, 1339 - placement: result.placement 1340 - }; 1341 - 1342 - activeDragElement.x = result.x; 1343 - activeDragElement.y = result.y; 1344 - 1345 - if (activeDragElement.item) { 1346 - // Get dragged card's original position for swapping 1347 - const draggedOrigPos = activeDragElement.originalPositions.get( 1348 - activeDragElement.item.id 1349 - ); 1350 - 1351 - // Reset all items to original positions first 1352 - for (const it of items) { 1353 - const origPos = activeDragElement.originalPositions.get(it.id); 1354 - if (origPos && it !== activeDragElement.item) { 1355 - if (isMobile) { 1356 - it.mobileX = origPos.mobileX; 1357 - it.mobileY = origPos.mobileY; 1358 - } else { 1359 - it.x = origPos.x; 1360 - it.y = origPos.y; 1361 - } 1362 - } 1363 - } 1364 - 1365 - // Update dragged item position 1366 - if (isMobile) { 1367 - activeDragElement.item.mobileX = result.x; 1368 - activeDragElement.item.mobileY = result.y; 1369 - } else { 1370 - activeDragElement.item.x = result.x; 1371 - activeDragElement.item.y = result.y; 1372 - } 1373 - 1374 - // Handle horizontal swap 1375 - if (result.swapWithId && draggedOrigPos) { 1376 - const swapTarget = items.find((it) => it.id === result.swapWithId); 1377 - if (swapTarget) { 1378 - // Move swap target to dragged card's original position 1379 - if (isMobile) { 1380 - swapTarget.mobileX = draggedOrigPos.mobileX; 1381 - swapTarget.mobileY = draggedOrigPos.mobileY; 1382 - } else { 1383 - swapTarget.x = draggedOrigPos.x; 1384 - swapTarget.y = draggedOrigPos.y; 1385 - } 1386 - } 1387 - } 1388 - 1389 - // Now fix collisions (with compacting) 1390 - fixCollisions( 1391 - items, 1392 - activeDragElement.item, 1393 - isMobile, 1394 - false, 1395 - draggedOrigPos 1396 - ? { 1397 - x: isMobile ? draggedOrigPos.mobileX : draggedOrigPos.x, 1398 - y: isMobile ? draggedOrigPos.mobileY : draggedOrigPos.y 1399 - } 1400 - : undefined 1401 - ); 1402 - } 1403 - }} 1404 - ondragend={async (e) => { 1405 - e.preventDefault(); 1406 - // safari fix 1407 - activeDragElement.x = -1; 1408 - activeDragElement.y = -1; 1409 - activeDragElement.element = null; 1410 - activeDragElement.item = null; 1411 - activeDragElement.lastTargetId = null; 1412 - activeDragElement.lastPlacement = null; 1413 - lastGridPos = null; 1414 - return true; 1415 - }} 1416 - class={[ 1417 - '@container/grid pointer-events-auto relative col-span-3 rounded-4xl px-2 py-8 @5xl/wrapper:px-8', 1418 - imageDragOver && 'outline-accent-500 outline-3 -outline-offset-10 outline-dashed' 1419 - ]} 880 + onfiledrop={handleFileDrop} 1420 881 > 1421 882 {#each items as item, i (item.id)} 1422 - <!-- {#if item !== activeDragElement.item} --> 1423 883 <BaseEditingCard 1424 884 bind:item={items[i]} 1425 885 ondelete={() => { ··· 1440 900 fixCollisions(items, item, isMobile); 1441 901 onLayoutChanged(); 1442 902 }} 1443 - ondragstart={(e: DragEvent) => { 1444 - const target = e.currentTarget as HTMLDivElement; 1445 - activeDragElement.element = target; 1446 - activeDragElement.w = item.w; 1447 - activeDragElement.h = item.h; 1448 - activeDragElement.item = item; 1449 - // fix for div shadow during drag and drop 1450 - const transparent = document.createElement('div'); 1451 - transparent.style.position = 'fixed'; 1452 - transparent.style.top = '-1000px'; 1453 - transparent.style.width = '1px'; 1454 - transparent.style.height = '1px'; 1455 - document.body.appendChild(transparent); 1456 - e.dataTransfer?.setDragImage(transparent, 0, 0); 1457 - requestAnimationFrame(() => transparent.remove()); 1458 - 1459 - // Store original positions of all items 1460 - activeDragElement.originalPositions = new Map(); 1461 - for (const it of items) { 1462 - activeDragElement.originalPositions.set(it.id, { 1463 - x: it.x, 1464 - y: it.y, 1465 - mobileX: it.mobileX, 1466 - mobileY: it.mobileY 1467 - }); 1468 - } 1469 - 1470 - const rect = target.getBoundingClientRect(); 1471 - activeDragElement.mouseDeltaX = rect.left - e.clientX; 1472 - activeDragElement.mouseDeltaY = rect.top - e.clientY; 1473 - }} 1474 903 > 1475 904 <EditingCard bind:item={items[i]} /> 1476 905 </BaseEditingCard> 1477 - <!-- {/if} --> 1478 906 {/each} 1479 - 1480 - <div style="height: {((maxHeight + 2) / 8) * 100}cqw;"></div> 1481 - </div> 907 + </EditableGrid> 1482 908 </div> 1483 909 </div> 1484 910
+1 -1
src/lib/website/layout-mirror.ts src/lib/layout/mirror.ts
··· 1 1 import { COLUMNS } from '$lib'; 2 2 import { CardDefinitionsByType } from '$lib/cards'; 3 3 import { clamp } from '$lib/helper'; 4 - import { fixAllCollisions, findValidPosition } from '$lib/layout'; 4 + import { fixAllCollisions, findValidPosition } from './algorithms'; 5 5 import type { Item } from '$lib/types'; 6 6 7 7 /**