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