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