your personal website on atproto - mirror blento.app
at fix-build 833 lines 23 kB view raw
1<script lang="ts"> 2 import { Button, toast, Toaster, Sidebar } from '@foxui/core'; 3 import { COLUMNS, margin, mobileMargin } from '$lib'; 4 import { 5 checkAndUploadImage, 6 clamp, 7 compactItems, 8 createEmptyCard, 9 findValidPosition, 10 fixCollisions, 11 getHideProfileSection, 12 getProfilePosition, 13 getName, 14 isTyping, 15 savePage, 16 scrollToItem, 17 setPositionOfNewItem, 18 validateLink, 19 getImage 20 } from '../helper'; 21 import EditableProfile from './EditableProfile.svelte'; 22 import type { Item, WebsiteData } from '../types'; 23 import { innerWidth } from 'svelte/reactivity/window'; 24 import EditingCard from '../cards/Card/EditingCard.svelte'; 25 import { AllCardDefinitions, CardDefinitionsByType } from '../cards'; 26 import { tick, type Component } from 'svelte'; 27 import type { CreationModalComponentProps } from '../cards/types'; 28 import { dev } from '$app/environment'; 29 import { setIsMobile } from './context'; 30 import BaseEditingCard from '../cards/BaseCard/BaseEditingCard.svelte'; 31 import Context from './Context.svelte'; 32 import Head from './Head.svelte'; 33 import Account from './Account.svelte'; 34 import { SelectThemePopover } from '$lib/components/select-theme'; 35 import EditBar from './EditBar.svelte'; 36 import SaveModal from './SaveModal.svelte'; 37 import FloatingEditButton from './FloatingEditButton.svelte'; 38 import { user } from '$lib/atproto'; 39 import { launchConfetti } from '@foxui/visual'; 40 import Controls from './Controls.svelte'; 41 42 let { 43 data 44 }: { 45 data: WebsiteData; 46 } = $props(); 47 48 // Check if floating login button will be visible (to hide MadeWithBlento) 49 const showLoginOnEditPage = $derived(!user.isInitializing && !user.isLoggedIn); 50 51 let accentColor = $derived(data.publication?.preferences?.accentColor ?? 'pink'); 52 let baseColor = $derived(data.publication?.preferences?.baseColor ?? 'stone'); 53 54 function updateTheme(newAccent: string, newBase: string) { 55 data.publication.preferences ??= {}; 56 data.publication.preferences.accentColor = newAccent; 57 data.publication.preferences.baseColor = newBase; 58 data = { ...data }; 59 } 60 61 let imageDragOver = $state(false); 62 63 // svelte-ignore state_referenced_locally 64 let items: Item[] = $state(data.cards); 65 66 // svelte-ignore state_referenced_locally 67 let publication = $state(JSON.stringify(data.publication)); 68 69 // Track saved state for comparison 70 // svelte-ignore state_referenced_locally 71 let savedItems = $state(JSON.stringify(data.cards)); 72 // svelte-ignore state_referenced_locally 73 let savedPublication = $state(JSON.stringify(data.publication)); 74 75 let hasUnsavedChanges = $derived( 76 JSON.stringify(items) !== savedItems || JSON.stringify(data.publication) !== savedPublication 77 ); 78 79 // Warn user before closing tab if there are unsaved changes 80 $effect(() => { 81 function handleBeforeUnload(e: BeforeUnloadEvent) { 82 if (hasUnsavedChanges) { 83 e.preventDefault(); 84 return ''; 85 } 86 } 87 88 window.addEventListener('beforeunload', handleBeforeUnload); 89 return () => window.removeEventListener('beforeunload', handleBeforeUnload); 90 }); 91 92 let container: HTMLDivElement | undefined = $state(); 93 94 let activeDragElement: { 95 element: HTMLDivElement | null; 96 item: Item | null; 97 w: number; 98 h: number; 99 x: number; 100 y: number; 101 mouseDeltaX: number; 102 mouseDeltaY: number; 103 // For hysteresis - track last decision to prevent flickering 104 lastTargetId: string | null; 105 lastPlacement: 'above' | 'below' | null; 106 // Store original positions to reset from during drag 107 originalPositions: Map<string, { x: number; y: number; mobileX: number; mobileY: number }>; 108 } = $state({ 109 element: null, 110 item: null, 111 w: 0, 112 h: 0, 113 x: -1, 114 y: -1, 115 mouseDeltaX: 0, 116 mouseDeltaY: 0, 117 lastTargetId: null, 118 lastPlacement: null, 119 originalPositions: new Map() 120 }); 121 122 let showingMobileView = $state(false); 123 let isMobile = $derived(showingMobileView || (innerWidth.current ?? 1000) < 1024); 124 125 setIsMobile(() => isMobile); 126 127 const getY = (item: Item) => (isMobile ? (item.mobileY ?? item.y) : item.y); 128 const getH = (item: Item) => (isMobile ? (item.mobileH ?? item.h) : item.h); 129 130 let maxHeight = $derived(items.reduce((max, item) => Math.max(max, getY(item) + getH(item)), 0)); 131 132 function newCard(type: string = 'link', cardData?: any) { 133 // close sidebar if open 134 const popover = document.getElementById('mobile-menu'); 135 if (popover) { 136 popover.hidePopover(); 137 } 138 139 let item = createEmptyCard(data.page); 140 item.cardType = type; 141 142 item.cardData = cardData ?? {}; 143 144 const cardDef = CardDefinitionsByType[type]; 145 cardDef?.createNew?.(item); 146 147 newItem.item = item; 148 149 if (cardDef?.creationModalComponent) { 150 newItem.modal = cardDef.creationModalComponent; 151 } else { 152 saveNewItem(); 153 } 154 } 155 156 async function saveNewItem() { 157 if (!newItem.item) return; 158 const item = newItem.item; 159 160 setPositionOfNewItem(item, items); 161 162 items = [...items, item]; 163 164 newItem = {}; 165 166 await tick(); 167 168 scrollToItem(item, isMobile, container); 169 } 170 171 let isSaving = $state(false); 172 let showSaveModal = $state(false); 173 let saveSuccess = $state(false); 174 175 let newItem: { modal?: Component<CreationModalComponentProps>; item?: Item } = $state({}); 176 177 async function save() { 178 isSaving = true; 179 saveSuccess = false; 180 showSaveModal = true; 181 182 try { 183 // Upload profile icon if changed 184 if (data.publication?.icon) { 185 await checkAndUploadImage(data.publication, 'icon'); 186 } 187 188 await savePage(data, items, publication); 189 190 publication = JSON.stringify(data.publication); 191 192 // Update saved state 193 savedItems = JSON.stringify(items); 194 savedPublication = JSON.stringify(data.publication); 195 196 saveSuccess = true; 197 198 launchConfetti(); 199 200 // Refresh cached data 201 await fetch('/' + data.handle + '/api/refresh'); 202 } catch (error) { 203 console.log(error); 204 showSaveModal = false; 205 toast.error('Error saving page!'); 206 } finally { 207 isSaving = false; 208 } 209 } 210 211 const sidebarItems = AllCardDefinitions.filter((cardDef) => cardDef.sidebarButtonText); 212 213 let debugPoint = $state({ x: 0, y: 0 }); 214 215 function getDragXY( 216 e: DragEvent & { 217 currentTarget: EventTarget & HTMLDivElement; 218 } 219 ): 220 | { x: number; y: number; swapWithId: string | null; placement: 'above' | 'below' | null } 221 | undefined { 222 if (!container || !activeDragElement.item) return; 223 224 // x, y represent the top-left corner of the dragged card 225 const x = e.clientX + activeDragElement.mouseDeltaX; 226 const y = e.clientY + activeDragElement.mouseDeltaY; 227 228 const rect = container.getBoundingClientRect(); 229 const currentMargin = isMobile ? mobileMargin : margin; 230 const cellSize = (rect.width - currentMargin * 2) / COLUMNS; 231 232 // Get card dimensions based on current view mode 233 const cardW = isMobile 234 ? (activeDragElement.item?.mobileW ?? activeDragElement.w) 235 : activeDragElement.w; 236 const cardH = isMobile 237 ? (activeDragElement.item?.mobileH ?? activeDragElement.h) 238 : activeDragElement.h; 239 240 // Get dragged card's original position 241 const draggedOrigPos = activeDragElement.originalPositions.get(activeDragElement.item.id); 242 243 const draggedOrigY = draggedOrigPos 244 ? isMobile 245 ? draggedOrigPos.mobileY 246 : draggedOrigPos.y 247 : 0; 248 249 // Calculate raw grid position based on top-left of dragged card 250 let gridX = clamp(Math.round((x - rect.left - currentMargin) / cellSize), 0, COLUMNS - cardW); 251 gridX = Math.floor(gridX / 2) * 2; 252 253 let gridY = Math.max(Math.round((y - rect.top - currentMargin) / cellSize), 0); 254 255 if (isMobile) { 256 gridX = Math.floor(gridX / 2) * 2; 257 gridY = Math.floor(gridY / 2) * 2; 258 } 259 260 // Find if we're hovering over another card (using ORIGINAL positions) 261 const centerGridY = gridY + cardH / 2; 262 const centerGridX = gridX + cardW / 2; 263 264 let swapWithId: string | null = null; 265 let placement: 'above' | 'below' | null = null; 266 267 for (const other of items) { 268 if (other === activeDragElement.item) continue; 269 270 // Use original positions for hit testing 271 const origPos = activeDragElement.originalPositions.get(other.id); 272 if (!origPos) continue; 273 274 const otherX = isMobile ? origPos.mobileX : origPos.x; 275 const otherY = isMobile ? origPos.mobileY : origPos.y; 276 const otherW = isMobile ? other.mobileW : other.w; 277 const otherH = isMobile ? other.mobileH : other.h; 278 279 // Check if dragged card's center point is within this card's original bounds 280 if ( 281 centerGridX >= otherX && 282 centerGridX < otherX + otherW && 283 centerGridY >= otherY && 284 centerGridY < otherY + otherH 285 ) { 286 // Check if this is a swap situation: 287 // Cards have the same dimensions and are on the same row 288 const canSwap = cardW === otherW && cardH === otherH && draggedOrigY === otherY; 289 290 if (canSwap) { 291 // Swap positions 292 swapWithId = other.id; 293 gridX = otherX; 294 gridY = otherY; 295 placement = null; 296 297 activeDragElement.lastTargetId = other.id; 298 activeDragElement.lastPlacement = null; 299 } else { 300 // Vertical placement (above/below) 301 // Detect drag direction: if dragging up, always place above 302 const isDraggingUp = gridY < draggedOrigY; 303 304 if (isDraggingUp) { 305 // When dragging up, always place above 306 placement = 'above'; 307 } else { 308 // When dragging down, use top/bottom half logic 309 const midpointY = otherY + otherH / 2; 310 const hysteresis = 0.3; 311 312 if (activeDragElement.lastTargetId === other.id && activeDragElement.lastPlacement) { 313 if (activeDragElement.lastPlacement === 'above') { 314 placement = centerGridY > midpointY + hysteresis ? 'below' : 'above'; 315 } else { 316 placement = centerGridY < midpointY - hysteresis ? 'above' : 'below'; 317 } 318 } else { 319 placement = centerGridY < midpointY ? 'above' : 'below'; 320 } 321 } 322 323 activeDragElement.lastTargetId = other.id; 324 activeDragElement.lastPlacement = placement; 325 326 if (placement === 'above') { 327 gridY = otherY; 328 } else { 329 gridY = otherY + otherH; 330 } 331 } 332 break; 333 } 334 } 335 336 // If we're not over any card, clear the tracking 337 if (!swapWithId && !placement) { 338 activeDragElement.lastTargetId = null; 339 activeDragElement.lastPlacement = null; 340 } 341 342 debugPoint.x = x - rect.left; 343 debugPoint.y = y - rect.top + currentMargin; 344 345 return { x: gridX, y: gridY, swapWithId, placement }; 346 } 347 348 let linkValue = $state(''); 349 350 function addLink(url: string) { 351 let link = validateLink(url); 352 if (!link) { 353 toast.error('invalid link'); 354 return; 355 } 356 let item = createEmptyCard(data.page); 357 358 for (const cardDef of AllCardDefinitions.toSorted( 359 (a, b) => (b.urlHandlerPriority ?? 0) - (a.urlHandlerPriority ?? 0) 360 )) { 361 if (cardDef.onUrlHandler?.(link, item)) { 362 item.cardType = cardDef.type; 363 364 newItem.item = item; 365 saveNewItem(); 366 toast(cardDef.name + ' added!'); 367 break; 368 } 369 } 370 371 if (linkValue === url) { 372 linkValue = ''; 373 } 374 } 375 376 async function processImageFile(file: File, gridX?: number, gridY?: number) { 377 const isGif = file.type === 'image/gif'; 378 379 // Don't compress GIFs to preserve animation 380 const objectUrl = URL.createObjectURL(file); 381 382 let item = createEmptyCard(data.page); 383 384 item.cardType = isGif ? 'gif' : 'image'; 385 item.cardData = { 386 image: { blob: file, objectUrl } 387 }; 388 389 // If grid position is provided 390 if (gridX !== undefined && gridY !== undefined) { 391 if (isMobile) { 392 item.mobileX = gridX; 393 item.mobileY = gridY; 394 // Find valid desktop position 395 findValidPosition(item, items, false); 396 } else { 397 item.x = gridX; 398 item.y = gridY; 399 // Find valid mobile position 400 findValidPosition(item, items, true); 401 } 402 403 items = [...items, item]; 404 fixCollisions(items, item, isMobile); 405 } else { 406 setPositionOfNewItem(item, items); 407 items = [...items, item]; 408 } 409 410 await tick(); 411 412 scrollToItem(item, isMobile, container); 413 } 414 415 function handleImageDragOver(event: DragEvent) { 416 const dt = event.dataTransfer; 417 if (!dt) return; 418 419 let hasImage = false; 420 if (dt.items) { 421 for (let i = 0; i < dt.items.length; i++) { 422 const item = dt.items[i]; 423 if (item && item.kind === 'file' && item.type.startsWith('image/')) { 424 hasImage = true; 425 break; 426 } 427 } 428 } else if (dt.files) { 429 for (let i = 0; i < dt.files.length; i++) { 430 const file = dt.files[i]; 431 if (file?.type.startsWith('image/')) { 432 hasImage = true; 433 break; 434 } 435 } 436 } 437 438 if (hasImage) { 439 event.preventDefault(); 440 event.stopPropagation(); 441 442 imageDragOver = true; 443 } 444 } 445 446 function handleImageDragLeave(event: DragEvent) { 447 event.preventDefault(); 448 event.stopPropagation(); 449 imageDragOver = false; 450 } 451 452 async function handleImageDrop(event: DragEvent) { 453 event.preventDefault(); 454 event.stopPropagation(); 455 const dropX = event.clientX; 456 const dropY = event.clientY; 457 imageDragOver = false; 458 459 if (!event.dataTransfer?.files?.length) return; 460 461 const imageFiles = Array.from(event.dataTransfer.files).filter((f) => 462 f?.type.startsWith('image/') 463 ); 464 if (imageFiles.length === 0) return; 465 466 // Calculate starting grid position from drop coordinates 467 let gridX = 0; 468 let gridY = 0; 469 if (container) { 470 const rect = container.getBoundingClientRect(); 471 const currentMargin = isMobile ? mobileMargin : margin; 472 const cellSize = (rect.width - currentMargin * 2) / COLUMNS; 473 const cardW = isMobile ? 4 : 2; 474 475 gridX = clamp(Math.round((dropX - rect.left - currentMargin) / cellSize), 0, COLUMNS - cardW); 476 gridX = Math.floor(gridX / 2) * 2; 477 478 gridY = Math.max(Math.round((dropY - rect.top - currentMargin) / cellSize), 0); 479 if (isMobile) { 480 gridY = Math.floor(gridY / 2) * 2; 481 } 482 } 483 484 for (const file of imageFiles) { 485 await processImageFile(file, gridX, gridY); 486 487 // Move to next cell position 488 const cardW = isMobile ? 4 : 2; 489 gridX += cardW; 490 if (gridX + cardW > COLUMNS) { 491 gridX = 0; 492 gridY += isMobile ? 4 : 2; 493 } 494 } 495 } 496 497 async function handleImageInputChange(event: Event) { 498 const target = event.target as HTMLInputElement; 499 if (!target.files || target.files.length < 1) return; 500 501 const files = Array.from(target.files); 502 503 if (files.length === 1) { 504 // Single file: use default positioning 505 await processImageFile(files[0]); 506 } else { 507 // Multiple files: place in grid pattern starting from first available position 508 let gridX = 0; 509 let gridY = maxHeight; 510 const cardW = isMobile ? 4 : 2; 511 const cardH = isMobile ? 4 : 2; 512 513 for (const file of files) { 514 await processImageFile(file, gridX, gridY); 515 516 // Move to next cell position 517 gridX += cardW; 518 if (gridX + cardW > COLUMNS) { 519 gridX = 0; 520 gridY += cardH; 521 } 522 } 523 } 524 525 // Reset the input so the same file can be selected again 526 target.value = ''; 527 } 528 529 async function processVideoFile(file: File) { 530 const objectUrl = URL.createObjectURL(file); 531 532 let item = createEmptyCard(data.page); 533 534 item.cardType = 'video'; 535 item.cardData = { 536 blob: file, 537 objectUrl 538 }; 539 540 setPositionOfNewItem(item, items); 541 items = [...items, item]; 542 543 await tick(); 544 545 scrollToItem(item, isMobile, container); 546 } 547 548 async function handleVideoInputChange(event: Event) { 549 const target = event.target as HTMLInputElement; 550 if (!target.files || target.files.length < 1) return; 551 552 const files = Array.from(target.files); 553 554 for (const file of files) { 555 await processVideoFile(file); 556 } 557 558 // Reset the input so the same file can be selected again 559 target.value = ''; 560 } 561 562 // $inspect(items); 563</script> 564 565<svelte:body 566 onpaste={(event) => { 567 if (isTyping()) return; 568 569 const text = event.clipboardData?.getData('text/plain'); 570 const link = validateLink(text, false); 571 if (!link) return; 572 573 addLink(link); 574 }} 575/> 576 577<svelte:window 578 ondragover={handleImageDragOver} 579 ondragleave={handleImageDragLeave} 580 ondrop={handleImageDrop} 581/> 582 583<Head 584 favicon={getImage(data.publication, data.did, 'icon') || data.profile.avatar} 585 title={getName(data)} 586 image={'/' + data.handle + '/og.png'} 587 accentColor={data.publication?.preferences?.accentColor} 588 baseColor={data.publication?.preferences?.baseColor} 589/> 590 591<Account {data} /> 592 593<Context {data}> 594 {#if !dev} 595 <div 596 class="bg-base-200 dark:bg-base-800 fixed inset-0 z-50 inline-flex h-full w-full items-center justify-center p-4 text-center lg:hidden" 597 > 598 Editing on mobile is not supported yet. Please use a desktop browser. 599 </div> 600 {/if} 601 602 <Controls bind:data /> 603 604 {#if showingMobileView} 605 <div 606 class="bg-base-200 dark:bg-base-950 pointer-events-none fixed inset-0 -z-10 h-full w-full" 607 ></div> 608 {/if} 609 610 {#if newItem.modal && newItem.item} 611 <newItem.modal 612 oncreate={() => { 613 saveNewItem(); 614 }} 615 bind:item={newItem.item} 616 oncancel={() => { 617 newItem = {}; 618 }} 619 /> 620 {/if} 621 622 <SaveModal 623 bind:open={showSaveModal} 624 success={saveSuccess} 625 handle={data.handle} 626 page={data.page} 627 /> 628 629 <div 630 class={[ 631 '@container/wrapper relative w-full', 632 showingMobileView 633 ? 'bg-base-50 dark:bg-base-900 my-4 min-h-[calc(100dhv-2em)] rounded-2xl lg:mx-auto lg:w-90' 634 : '' 635 ]} 636 > 637 {#if !getHideProfileSection(data)} 638 <EditableProfile bind:data hideBlento={showLoginOnEditPage} /> 639 {/if} 640 641 <div 642 class={[ 643 'pointer-events-none relative mx-auto max-w-lg', 644 !getHideProfileSection(data) && getProfilePosition(data) === 'side' 645 ? '@5xl/wrapper:grid @5xl/wrapper:max-w-7xl @5xl/wrapper:grid-cols-4' 646 : '@5xl/wrapper:max-w-4xl' 647 ]} 648 > 649 <div class="pointer-events-none"></div> 650 <!-- svelte-ignore a11y_no_static_element_interactions --> 651 <div 652 bind:this={container} 653 ondragover={(e) => { 654 e.preventDefault(); 655 656 const result = getDragXY(e); 657 if (!result) return; 658 659 activeDragElement.x = result.x; 660 activeDragElement.y = result.y; 661 662 if (activeDragElement.item) { 663 // Get dragged card's original position for swapping 664 const draggedOrigPos = activeDragElement.originalPositions.get( 665 activeDragElement.item.id 666 ); 667 668 // Reset all items to original positions first 669 for (const it of items) { 670 const origPos = activeDragElement.originalPositions.get(it.id); 671 if (origPos && it !== activeDragElement.item) { 672 if (isMobile) { 673 it.mobileX = origPos.mobileX; 674 it.mobileY = origPos.mobileY; 675 } else { 676 it.x = origPos.x; 677 it.y = origPos.y; 678 } 679 } 680 } 681 682 // Update dragged item position 683 if (isMobile) { 684 activeDragElement.item.mobileX = result.x; 685 activeDragElement.item.mobileY = result.y; 686 } else { 687 activeDragElement.item.x = result.x; 688 activeDragElement.item.y = result.y; 689 } 690 691 // Handle horizontal swap 692 if (result.swapWithId && draggedOrigPos) { 693 const swapTarget = items.find((it) => it.id === result.swapWithId); 694 if (swapTarget) { 695 // Move swap target to dragged card's original position 696 if (isMobile) { 697 swapTarget.mobileX = draggedOrigPos.mobileX; 698 swapTarget.mobileY = draggedOrigPos.mobileY; 699 } else { 700 swapTarget.x = draggedOrigPos.x; 701 swapTarget.y = draggedOrigPos.y; 702 } 703 } 704 } 705 706 // Now fix collisions (with compacting) 707 fixCollisions(items, activeDragElement.item, isMobile); 708 } 709 710 // Auto-scroll when dragging near top or bottom of viewport 711 const scrollZone = 100; 712 const scrollSpeed = 10; 713 const viewportHeight = window.innerHeight; 714 715 if (e.clientY < scrollZone) { 716 // Near top - scroll up 717 const intensity = 1 - e.clientY / scrollZone; 718 window.scrollBy(0, -scrollSpeed * intensity); 719 } else if (e.clientY > viewportHeight - scrollZone) { 720 // Near bottom - scroll down 721 const intensity = 1 - (viewportHeight - e.clientY) / scrollZone; 722 window.scrollBy(0, scrollSpeed * intensity); 723 } 724 }} 725 ondragend={async (e) => { 726 e.preventDefault(); 727 const cell = getDragXY(e); 728 if (!cell) return; 729 730 if (activeDragElement.item) { 731 if (isMobile) { 732 activeDragElement.item.mobileX = cell.x; 733 activeDragElement.item.mobileY = cell.y; 734 } else { 735 activeDragElement.item.x = cell.x; 736 activeDragElement.item.y = cell.y; 737 } 738 739 // Fix collisions and compact items after drag ends 740 fixCollisions(items, activeDragElement.item, isMobile); 741 } 742 activeDragElement.x = -1; 743 activeDragElement.y = -1; 744 activeDragElement.element = null; 745 activeDragElement.item = null; 746 activeDragElement.lastTargetId = null; 747 activeDragElement.lastPlacement = null; 748 return true; 749 }} 750 class={[ 751 '@container/grid pointer-events-auto relative col-span-3 rounded-4xl px-2 py-8 @5xl/wrapper:px-8', 752 imageDragOver && 'outline-accent-500 outline-3 -outline-offset-10 outline-dashed' 753 ]} 754 > 755 {#each items as item, i (item.id)} 756 <!-- {#if item !== activeDragElement.item} --> 757 <BaseEditingCard 758 bind:item={items[i]} 759 ondelete={() => { 760 items = items.filter((it) => it !== item); 761 compactItems(items, isMobile); 762 }} 763 onsetsize={(newW: number, newH: number) => { 764 if (isMobile) { 765 item.mobileW = newW; 766 item.mobileH = newH; 767 } else { 768 item.w = newW; 769 item.h = newH; 770 } 771 772 fixCollisions(items, item, isMobile); 773 }} 774 ondragstart={(e: DragEvent) => { 775 const target = e.currentTarget as HTMLDivElement; 776 activeDragElement.element = target; 777 activeDragElement.w = item.w; 778 activeDragElement.h = item.h; 779 activeDragElement.item = item; 780 781 // Store original positions of all items 782 activeDragElement.originalPositions = new Map(); 783 for (const it of items) { 784 activeDragElement.originalPositions.set(it.id, { 785 x: it.x, 786 y: it.y, 787 mobileX: it.mobileX, 788 mobileY: it.mobileY 789 }); 790 } 791 792 const rect = target.getBoundingClientRect(); 793 activeDragElement.mouseDeltaX = rect.left - e.clientX; 794 activeDragElement.mouseDeltaY = rect.top - e.clientY; 795 }} 796 > 797 <EditingCard bind:item={items[i]} /> 798 </BaseEditingCard> 799 <!-- {/if} --> 800 {/each} 801 802 <div style="height: {((maxHeight + 2) / 8) * 100}cqw;"></div> 803 </div> 804 </div> 805 </div> 806 807 <Sidebar mobileOnly mobileClasses="lg:block p-4 gap-4"> 808 <div class="flex flex-col gap-2"> 809 {#each sidebarItems as cardDef (cardDef.type)} 810 <Button onclick={() => newCard(cardDef.type)} variant="ghost" class="w-full justify-start" 811 >{cardDef.sidebarButtonText}</Button 812 > 813 {/each} 814 </div> 815 </Sidebar> 816 817 <EditBar 818 {data} 819 bind:linkValue 820 bind:isSaving 821 bind:showingMobileView 822 {hasUnsavedChanges} 823 {newCard} 824 {addLink} 825 {save} 826 {handleImageInputChange} 827 {handleVideoInputChange} 828 /> 829 830 <Toaster /> 831 832 <FloatingEditButton {data} /> 833</Context>