your personal website on atproto - mirror blento.app
26
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #276 from flo-bit/fix/card-layout

fix card layout version 24

authored by

Florian and committed by
GitHub
335f9fa8 935d0619

+90 -94
+3 -1
.gitignore
··· 26 26 27 27 sveltekit-cloudflare-workers 28 28 29 - inlay 29 + inlay 30 + 31 + scripts/backups
+3 -2
src/lib/cards/_base/BaseCard/BaseCard.svelte
··· 45 45 draggable={false} 46 46 class={[ 47 47 fillPage 48 - ? 'card group/card selection:bg-accent-600/50 focus-within:outline-accent-500 @container/card relative isolate z-0 h-full w-full outline-offset-2 transition-[outline] duration-200 focus-within:outline-2' 49 - : '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-[outline] duration-200 focus-within:outline-2', 48 + ? 'card group/card selection:bg-accent-600/50 @container/card relative isolate z-0 h-full w-full outline-offset-2 transition-[outline] duration-200' 49 + : 'card group/card selection:bg-accent-600/50 @container/card absolute isolate z-0 rounded-3xl outline-offset-2 transition-[outline] duration-200', 50 + isEditing ? 'transition-all' : '', 50 51 !fillPage ? (color ? (colors[color] ?? colors.accent) : colors.base) : '', 51 52 color !== 'accent' && item.color !== 'base' && item.color !== 'transparent' ? color : '', 52 53 showOutline ? 'outline-2' : '',
+24 -12
src/lib/cards/_base/BaseCard/BaseEditingCard.svelte
··· 63 63 let selectedCardId = getSelectedCardId(); 64 64 let selectCard = getSelectCard(); 65 65 let isSelected = $derived(selectedCardId?.() === item.id); 66 - let isDimmed = $derived(isCoarse?.() && selectedCardId?.() != null && !isSelected); 66 + 67 + // Track pointer down position so we only select on click, not on drag 68 + let overlayDownX = 0; 69 + let overlayDownY = 0; 70 + function handleOverlayPointerDown(e: PointerEvent) { 71 + overlayDownX = e.clientX; 72 + overlayDownY = e.clientY; 73 + } 74 + function handleOverlayPointerUp(e: PointerEvent) { 75 + const dx = Math.abs(e.clientX - overlayDownX); 76 + const dy = Math.abs(e.clientY - overlayDownY); 77 + if (dx < 5 && dy < 5) { 78 + selectCard?.(item.id); 79 + } 80 + } 67 81 68 82 let colorPopoverOpen = $state(false); 69 83 ··· 184 198 {item} 185 199 isEditing={true} 186 200 bind:ref 187 - showOutline={isResizing || (isCoarse?.() && isSelected)} 201 + showOutline={isResizing || isSelected} 188 202 locked={item.cardData?.locked} 189 203 class={[ 190 204 'scale-100 starting:scale-0 starting:opacity-0', 191 - isCoarse?.() && isSelected ? 'ring-accent-500 z-10 ring-2 ring-offset-2' : '', 192 - isDimmed ? 'opacity-70' : 'opacity-100' 205 + isSelected ? 'outline-accent-500 z-10' : '' 193 206 ]} 194 207 {...rest} 195 208 > 196 - {#if isCoarse?.() && !isSelected} 197 - <!-- svelte-ignore a11y_click_events_have_key_events --> 209 + {#if !isSelected} 210 + <!-- Overlay captures pointer events so dragging cards doesn't accidentally 211 + select text / trigger inner content. Click (no drag) selects the card. --> 198 212 <div 199 213 role="button" 200 214 tabindex="-1" 201 - class="absolute inset-0 z-20 cursor-pointer" 202 - onclick={(e) => { 203 - e.stopPropagation(); 204 - selectCard?.(item.id); 205 - }} 215 + class="absolute inset-0 z-20 cursor-pointer focus:outline-none" 216 + onpointerdown={handleOverlayPointerDown} 217 + onpointerup={handleOverlayPointerUp} 206 218 ></div> 207 219 {/if} 208 220 {@render children?.()} ··· 299 311 <div 300 312 class={[ 301 313 'translate absolute -bottom-13 w-full items-center justify-center pt-2.5 text-xs lg:group-focus-within:inline-flex lg:group-hover/card:inline-flex', 302 - colorPopoverOpen || settingsPopoverOpen ? 'inline-flex' : 'hidden' 314 + colorPopoverOpen || settingsPopoverOpen || isSelected ? 'inline-flex' : 'hidden' 303 315 ]} 304 316 > 305 317 <div
+22 -30
src/lib/helper.ts
··· 198 198 export async function savePage( 199 199 data: WebsiteData, 200 200 currentItems: Item[], 201 + originalCards: Item[], 201 202 originalPublication: string 202 203 ) { 203 204 const promises = []; 204 205 205 - // Build a lookup of original cards by ID for O(1) access 206 - const originalCardsById = new Map<string, Item>(); 207 - for (const card of data.cards) { 208 - originalCardsById.set(card.id, card); 209 - } 210 - 211 - // find all cards that have been updated (where items differ from originalItems) 206 + // Save all current cards. We don't diff against originals because the 207 + // server-side load can modify cards (e.g. fixing overlaps), so the 208 + // "original" the client sees is already the post-fix version — there's 209 + // nothing reliable to diff against. 212 210 for (let item of currentItems) { 213 - const orig = originalCardsById.get(item.id); 214 - const originalItem = orig && cardsEqual(orig, item) ? orig : undefined; 211 + item.updatedAt = new Date().toISOString(); 212 + // run optional upload function for this card type 213 + const cardDef = CardDefinitionsByType[item.cardType]; 215 214 216 - if (!originalItem) { 217 - console.log('updated or new item', item); 218 - item.updatedAt = new Date().toISOString(); 219 - // run optional upload function for this card type 220 - const cardDef = CardDefinitionsByType[item.cardType]; 221 - 222 - if (cardDef?.upload) { 223 - item = await cardDef?.upload(item); 224 - } 215 + if (cardDef?.upload) { 216 + item = await cardDef?.upload(item); 217 + } 225 218 226 - const parsedItem = JSON.parse(JSON.stringify(item)); 219 + const parsedItem = JSON.parse(JSON.stringify(item)); 227 220 228 - parsedItem.page = data.page; 229 - parsedItem.version = 2; 221 + parsedItem.page = data.page; 222 + parsedItem.version = 2; 230 223 231 - promises.push( 232 - putRecord({ 233 - collection: 'app.blento.card', 234 - rkey: parsedItem.id, 235 - record: parsedItem 236 - }) 237 - ); 238 - } 224 + promises.push( 225 + putRecord({ 226 + collection: 'app.blento.card', 227 + rkey: parsedItem.id, 228 + record: parsedItem 229 + }) 230 + ); 239 231 } 240 232 241 233 // delete items that are in originalItems but not in items 242 - for (const originalItem of data.cards) { 234 + for (const originalItem of originalCards) { 243 235 const item = currentItems.find((i) => i.id === originalItem.id); 244 236 if (!item) { 245 237 console.log('deleting item', originalItem);
+14 -4
src/lib/layout/EditableGrid.svelte
··· 32 32 ref = container; 33 33 }); 34 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)); 35 + let maxHeight = $derived( 36 + items.reduce((max, item) => { 37 + const y = isMobile ? (item.mobileY ?? item.y) : item.y; 38 + const h = isMobile ? (item.mobileH ?? item.h) : item.h; 39 + return Math.max(max, y + h); 40 + }, 0) 41 + ); 42 + 38 43 39 44 // --- Drag state --- 40 45 type Phase = 'idle' | 'pending' | 'active'; ··· 387 392 > 388 393 {@render children()} 389 394 390 - <div style="height: {((maxHeight + 2) / 8) * 100}cqw;"></div> 395 + <!-- 396 + padding-top % is based on the parent's inline size (width), so this grows 397 + proportionally with the grid width. Using cqw here caused stale resolution 398 + when the grid container resized (e.g. toggling mobile view). 399 + --> 400 + <div style="padding-top: {((maxHeight + 2) / 8) * 100}%;"></div> 391 401 </div> 392 402 393 403 <style>
+17 -3
src/lib/website/EditableWebsite.svelte
··· 56 56 // Check if floating login button will be visible (to hide MadeWithBlento) 57 57 const showLoginOnEditPage = $derived(!user.isInitializing && !user.isLoggedIn); 58 58 59 + // Snapshot the original cards so savePage can detect deletions. 60 + const originalCards: Item[] = structuredClone(data.cards); 61 + 59 62 // svelte-ignore state_referenced_locally 60 63 let items: Item[] = $state(data.cards); 61 64 62 - // Flag set by checkData when overlapping cards were detected before fixing 63 65 // Flag set by checkData when overlapping cards were auto-fixed on load 64 66 let showLayoutFixModal = $state(data.hasLayoutIssue ?? false); 65 67 ··· 105 107 106 108 window.addEventListener('beforeunload', handleBeforeUnload); 107 109 return () => window.removeEventListener('beforeunload', handleBeforeUnload); 110 + }); 111 + 112 + // Press Escape to deselect the currently selected card. 113 + $effect(() => { 114 + function handleKeydown(e: KeyboardEvent) { 115 + if (e.key === 'Escape' && selectedCardId) { 116 + selectedCardId = null; 117 + } 118 + } 119 + 120 + window.addEventListener('keydown', handleKeydown); 121 + return () => window.removeEventListener('keydown', handleKeydown); 108 122 }); 109 123 110 124 let gridContainer: HTMLDivElement | undefined = $state(); ··· 238 252 data.publication.preferences ??= {}; 239 253 data.publication.preferences.editedOn = editedOn; 240 254 241 - await savePage(data, items, publication); 255 + await savePage(data, items, originalCards, publication); 242 256 243 257 publication = JSON.stringify(data.publication); 244 258 savedPronouns = JSON.stringify(data.pronounsRecord); ··· 621 635 class={[ 622 636 '@container/wrapper relative w-full', 623 637 showingMobileView 624 - ? 'bg-base-50 dark:bg-base-900 my-4 min-h-[calc(100dhv-2em)] rounded-2xl lg:mx-auto lg:w-90' 638 + ? 'bg-base-50 dark:bg-base-900 my-4 min-h-[calc(100dvh-2em)] rounded-2xl lg:mx-auto lg:w-90' 625 639 : '' 626 640 ]} 627 641 >
+7 -42
src/lib/website/load.ts
··· 378 378 const cards = data.cards.filter((v) => v.page === data.page); 379 379 380 380 if (cards.length > 0) { 381 - // Detect overlaps before fixing — flag is used by the edit UI 382 - const desktopOverlaps = hasOverlaps(cards, false); 383 - const mobileOverlaps = hasOverlaps(cards, true); 384 - data.hasLayoutIssue = desktopOverlaps || mobileOverlaps; 381 + // Detect overlaps before fixing — flag is surfaced by the edit UI 382 + // so the user knows their layout was auto-adjusted. 383 + data.hasLayoutIssue = hasOverlaps(cards, false) || hasOverlaps(cards, true); 385 384 386 - if (data.hasLayoutIssue) { 387 - console.log('[checkData] Layout issues detected:'); 388 - if (desktopOverlaps) console.log(' - Desktop has overlapping cards'); 389 - if (mobileOverlaps) console.log(' - Mobile has overlapping cards'); 390 - 391 - // Log before positions 392 - const before = cards.map((c) => ({ 393 - id: c.id, 394 - type: c.cardType, 395 - desktop: `(${c.x},${c.y},${c.w}x${c.h})`, 396 - mobile: `(${c.mobileX},${c.mobileY},${c.mobileW}x${c.mobileH})` 397 - })); 398 - 399 - fixAllCollisions(cards, false); 400 - fixAllCollisions(cards, true); 401 - compactItems(cards, false); 402 - compactItems(cards, true); 403 - 404 - // Log changes 405 - for (let i = 0; i < cards.length; i++) { 406 - const c = cards[i]; 407 - const b = before[i]; 408 - const newDesktop = `(${c.x},${c.y},${c.w}x${c.h})`; 409 - const newMobile = `(${c.mobileX},${c.mobileY},${c.mobileW}x${c.mobileH})`; 410 - if (newDesktop !== b.desktop || newMobile !== b.mobile) { 411 - console.log( 412 - ` ${b.type} ${b.id}: ` + 413 - (newDesktop !== b.desktop ? `desktop ${b.desktop} → ${newDesktop} ` : '') + 414 - (newMobile !== b.mobile ? `mobile ${b.mobile} → ${newMobile}` : '') 415 - ); 416 - } 417 - } 418 - } else { 419 - fixAllCollisions(cards, false); 420 - fixAllCollisions(cards, true); 421 - compactItems(cards, false); 422 - compactItems(cards, true); 423 - } 385 + fixAllCollisions(cards, false); 386 + fixAllCollisions(cards, true); 387 + compactItems(cards, false); 388 + compactItems(cards, true); 424 389 } 425 390 426 391 data.cards = cards;