your personal website on atproto - mirror blento.app

fixes

+137 -76
+33 -16
src/lib/helper.ts
··· 180 180 mobile ? a.mobileY - b.mobileY || a.mobileX - b.mobileX : a.y - b.y || a.x - b.x 181 181 ); 182 182 183 + // For each item, find the lowest Y it can occupy by checking the bottom edges 184 + // of all horizontally-overlapping items already placed above it. 185 + const settled: Item[] = []; 186 + 183 187 for (const item of sortedItems) { 184 - // Try moving item up row by row until we hit y=0 or a collision 185 - while (true) { 186 - const currentY = mobile ? item.mobileY : item.y; 187 - if (currentY <= 0) break; 188 + const itemX = mobile ? item.mobileX : item.x; 189 + const itemW = mobile ? item.mobileW : item.w; 188 190 189 - // Temporarily move up by 1 190 - if (mobile) item.mobileY -= 1; 191 - else item.y -= 1; 191 + let minY = 0; 192 192 193 - // Check for collision with any other item 194 - const hasCollision = items.some((other) => other !== item && overlaps(item, other, mobile)); 193 + for (const other of settled) { 194 + const otherX = mobile ? other.mobileX : other.x; 195 + const otherW = mobile ? other.mobileW : other.w; 195 196 196 - if (hasCollision) { 197 - // Revert the move 198 - if (mobile) item.mobileY += 1; 199 - else item.y += 1; 200 - break; 197 + // Check horizontal overlap 198 + if (itemX < otherX + otherW && itemX + itemW > otherX) { 199 + const otherBottom = mobile ? other.mobileY + other.mobileH : other.y + other.h; 200 + if (otherBottom > minY) { 201 + minY = otherBottom; 202 + } 201 203 } 202 - // No collision, keep the new position and try moving up again 204 + } 205 + 206 + if (mobile) { 207 + item.mobileY = minY; 208 + } else { 209 + item.y = minY; 203 210 } 211 + 212 + settled.push(item); 204 213 } 205 214 } 206 215 ··· 553 562 originalPublication: string 554 563 ) { 555 564 const promises = []; 565 + 566 + // Build a lookup of original cards by ID for O(1) access 567 + const originalCardsById = new Map<string, Item>(); 568 + for (const card of data.cards) { 569 + originalCardsById.set(card.id, card); 570 + } 571 + 556 572 // find all cards that have been updated (where items differ from originalItems) 557 573 for (let item of currentItems) { 558 - const originalItem = data.cards.find((i) => cardsEqual(i, item)); 574 + const orig = originalCardsById.get(item.id); 575 + const originalItem = orig && cardsEqual(orig, item) ? orig : undefined; 559 576 560 577 if (!originalItem) { 561 578 console.log('updated or new item', item);
+84 -39
src/lib/website/EditableWebsite.svelte
··· 57 57 data.publication.preferences ??= {}; 58 58 data.publication.preferences.accentColor = newAccent; 59 59 data.publication.preferences.baseColor = newBase; 60 + hasUnsavedChanges = true; 60 61 data = { ...data }; 61 62 } 62 63 ··· 68 69 // svelte-ignore state_referenced_locally 69 70 let publication = $state(JSON.stringify(data.publication)); 70 71 71 - // Track saved state for comparison 72 72 // svelte-ignore state_referenced_locally 73 - let savedItems = $state(JSON.stringify(data.cards)); 74 - // svelte-ignore state_referenced_locally 75 - let savedPublication = $state(JSON.stringify(data.publication)); 73 + let savedItemsSnapshot = JSON.stringify(data.cards); 76 74 77 75 let hasUnsavedChanges = $state(false); 78 76 77 + // Detect card content and publication changes (e.g. sidebar edits) 78 + // The guard ensures JSON.stringify only runs while no changes are detected yet. 79 + // Once hasUnsavedChanges is true, Svelte still fires this effect on item mutations 80 + // but the early return makes it effectively free. 79 81 $effect(() => { 80 - if (!hasUnsavedChanges) { 81 - hasUnsavedChanges = 82 - JSON.stringify(items) !== savedItems || 83 - JSON.stringify(data.publication) !== savedPublication; 82 + if (hasUnsavedChanges) return; 83 + if ( 84 + JSON.stringify(items) !== savedItemsSnapshot || 85 + JSON.stringify(data.publication) !== publication 86 + ) { 87 + hasUnsavedChanges = true; 84 88 } 85 89 }); 86 90 ··· 137 141 let editedOn = $state(data.publication.preferences?.editedOn ?? 0); 138 142 139 143 function onLayoutChanged() { 144 + hasUnsavedChanges = true; 140 145 // Set the bit for the current layout: desktop=1, mobile=2 141 146 editedOn = editedOn | (isMobile ? 2 : 1); 142 147 if (shouldMirror(editedOn)) { ··· 266 271 267 272 publication = JSON.stringify(data.publication); 268 273 269 - // Update saved state 270 - savedItems = JSON.stringify(items); 271 - savedPublication = JSON.stringify(data.publication); 274 + savedItemsSnapshot = JSON.stringify(items); 275 + hasUnsavedChanges = false; 272 276 273 277 saveSuccess = true; 274 278 ··· 558 562 } 559 563 } 560 564 565 + let lastGridPos: { 566 + x: number; 567 + y: number; 568 + swapWithId: string | null; 569 + placement: string | null; 570 + } | null = $state(null); 571 + 561 572 let debugPoint = $state({ x: 0, y: 0 }); 562 573 563 574 function getGridPosition( ··· 750 761 751 762 e.preventDefault(); 752 763 764 + // Auto-scroll near edges (always process, even if grid pos unchanged) 765 + const scrollZone = 100; 766 + const scrollSpeed = 10; 767 + const viewportHeight = window.innerHeight; 768 + 769 + if (touch.clientY < scrollZone) { 770 + const intensity = 1 - touch.clientY / scrollZone; 771 + window.scrollBy(0, -scrollSpeed * intensity); 772 + } else if (touch.clientY > viewportHeight - scrollZone) { 773 + const intensity = 1 - (viewportHeight - touch.clientY) / scrollZone; 774 + window.scrollBy(0, scrollSpeed * intensity); 775 + } 776 + 753 777 const result = getGridPosition(touch.clientX, touch.clientY); 754 778 if (!result || !activeDragElement.item) return; 755 779 780 + // Skip redundant work if grid position hasn't changed 781 + if ( 782 + lastGridPos && 783 + lastGridPos.x === result.x && 784 + lastGridPos.y === result.y && 785 + lastGridPos.swapWithId === result.swapWithId && 786 + lastGridPos.placement === result.placement 787 + ) { 788 + return; 789 + } 790 + lastGridPos = { 791 + x: result.x, 792 + y: result.y, 793 + swapWithId: result.swapWithId, 794 + placement: result.placement 795 + }; 796 + 756 797 const draggedOrigPos = activeDragElement.originalPositions.get(activeDragElement.item.id); 757 798 758 799 // Reset all items to original positions first ··· 793 834 } 794 835 795 836 fixCollisions(items, activeDragElement.item, isMobile); 796 - 797 - // Auto-scroll near edges 798 - const scrollZone = 100; 799 - const scrollSpeed = 10; 800 - const viewportHeight = window.innerHeight; 801 - 802 - if (touch.clientY < scrollZone) { 803 - const intensity = 1 - touch.clientY / scrollZone; 804 - window.scrollBy(0, -scrollSpeed * intensity); 805 - } else if (touch.clientY > viewportHeight - scrollZone) { 806 - const intensity = 1 - (viewportHeight - touch.clientY) / scrollZone; 807 - window.scrollBy(0, scrollSpeed * intensity); 808 - } 809 837 } 810 838 811 839 function touchEnd() { ··· 822 850 activeDragElement.lastPlacement = null; 823 851 } 824 852 853 + lastGridPos = null; 825 854 touchDragActive = false; 826 855 } 827 856 ··· 1270 1299 ondragover={(e) => { 1271 1300 e.preventDefault(); 1272 1301 1302 + // Auto-scroll when dragging near top or bottom of viewport (always process) 1303 + const scrollZone = 100; 1304 + const scrollSpeed = 10; 1305 + const viewportHeight = window.innerHeight; 1306 + 1307 + if (e.clientY < scrollZone) { 1308 + const intensity = 1 - e.clientY / scrollZone; 1309 + window.scrollBy(0, -scrollSpeed * intensity); 1310 + } else if (e.clientY > viewportHeight - scrollZone) { 1311 + const intensity = 1 - (viewportHeight - e.clientY) / scrollZone; 1312 + window.scrollBy(0, scrollSpeed * intensity); 1313 + } 1314 + 1273 1315 const result = getDragXY(e); 1274 1316 if (!result) return; 1317 + 1318 + // Skip redundant work if grid position hasn't changed 1319 + if ( 1320 + lastGridPos && 1321 + lastGridPos.x === result.x && 1322 + lastGridPos.y === result.y && 1323 + lastGridPos.swapWithId === result.swapWithId && 1324 + lastGridPos.placement === result.placement 1325 + ) { 1326 + return; 1327 + } 1328 + lastGridPos = { 1329 + x: result.x, 1330 + y: result.y, 1331 + swapWithId: result.swapWithId, 1332 + placement: result.placement 1333 + }; 1275 1334 1276 1335 activeDragElement.x = result.x; 1277 1336 activeDragElement.y = result.y; ··· 1323 1382 // Now fix collisions (with compacting) 1324 1383 fixCollisions(items, activeDragElement.item, isMobile); 1325 1384 } 1326 - 1327 - // Auto-scroll when dragging near top or bottom of viewport 1328 - const scrollZone = 100; 1329 - const scrollSpeed = 10; 1330 - const viewportHeight = window.innerHeight; 1331 - 1332 - if (e.clientY < scrollZone) { 1333 - // Near top - scroll up 1334 - const intensity = 1 - e.clientY / scrollZone; 1335 - window.scrollBy(0, -scrollSpeed * intensity); 1336 - } else if (e.clientY > viewportHeight - scrollZone) { 1337 - // Near bottom - scroll down 1338 - const intensity = 1 - (viewportHeight - e.clientY) / scrollZone; 1339 - window.scrollBy(0, scrollSpeed * intensity); 1340 - } 1341 1385 }} 1342 1386 ondragend={async (e) => { 1343 1387 e.preventDefault(); ··· 1348 1392 activeDragElement.item = null; 1349 1393 activeDragElement.lastTargetId = null; 1350 1394 activeDragElement.lastPlacement = null; 1395 + lastGridPos = null; 1351 1396 return true; 1352 1397 }} 1353 1398 class={[
+20 -21
src/lib/website/load.ts
··· 73 73 throw error(404); 74 74 } 75 75 76 - const cards = await listRecords({ did, collection: 'app.blento.card' }).catch(() => { 77 - console.error('error getting records for collection app.blento.card'); 78 - return [] as Awaited<ReturnType<typeof listRecords>>; 79 - }); 80 - 81 - const mainPublication = await getRecord({ 82 - did, 83 - collection: 'site.standard.publication', 84 - rkey: 'blento.self' 85 - }).catch(() => { 86 - console.error('error getting record for collection site.standard.publication'); 87 - return undefined; 88 - }); 89 - 90 - const pages = await listRecords({ did, collection: 'app.blento.page' }).catch(() => { 91 - console.error('error getting records for collection app.blento.page'); 92 - return [] as Awaited<ReturnType<typeof listRecords>>; 93 - }); 94 - 95 - const profile = await getDetailedProfile({ did }); 76 + const [cards, mainPublication, pages, profile] = await Promise.all([ 77 + listRecords({ did, collection: 'app.blento.card' }).catch(() => { 78 + console.error('error getting records for collection app.blento.card'); 79 + return [] as Awaited<ReturnType<typeof listRecords>>; 80 + }), 81 + getRecord({ 82 + did, 83 + collection: 'site.standard.publication', 84 + rkey: 'blento.self' 85 + }).catch(() => { 86 + console.error('error getting record for collection site.standard.publication'); 87 + return undefined; 88 + }), 89 + listRecords({ did, collection: 'app.blento.page' }).catch(() => { 90 + console.error('error getting records for collection app.blento.page'); 91 + return [] as Awaited<ReturnType<typeof listRecords>>; 92 + }), 93 + getDetailedProfile({ did }) 94 + ]); 96 95 97 96 const cardTypes = new Set(cards.map((v) => v.value.cardType ?? '') as string[]); 98 97 const cardTypesArray = Array.from(cardTypes); ··· 144 143 const stringifiedResult = JSON.stringify(result); 145 144 await cache?.put?.(handle, stringifiedResult); 146 145 147 - const parsedResult = JSON.parse(stringifiedResult); 146 + const parsedResult = structuredClone(result) as any; 148 147 149 148 parsedResult.publication = ( 150 149 parsedResult.publications as Awaited<ReturnType<typeof listRecords>>