your personal website on atproto - mirror blento.app

this is fine

Florian f5a65c59 fc5edb07

+110 -1
+3
src/lib/types.ts
··· 55 55 // theme colors 56 56 accentColor?: string; 57 57 baseColor?: string; 58 + 59 + // layout mirroring: 0/undefined=never edited, 1=desktop only, 2=mobile only, 3=both 60 + editedOn?: number; 58 61 }; 59 62 }; 60 63 profile: AppBskyActorDefs.ProfileViewDetailed;
+34 -1
src/lib/website/EditableWebsite.svelte
··· 39 39 import { launchConfetti } from '@foxui/visual'; 40 40 import Controls from './Controls.svelte'; 41 41 import CardCommand from '$lib/components/card-command/CardCommand.svelte'; 42 + import { shouldMirror, mirrorLayout } from './layout-mirror'; 42 43 43 44 let { 44 45 data ··· 122 123 123 124 setIsMobile(() => isMobile); 124 125 126 + // svelte-ignore state_referenced_locally 127 + let editedOn = $state(data.publication.preferences?.editedOn ?? 0); 128 + 129 + function onLayoutChanged() { 130 + // Set the bit for the current layout: desktop=1, mobile=2 131 + editedOn = editedOn | (isMobile ? 2 : 1); 132 + if (shouldMirror(editedOn)) { 133 + mirrorLayout(items, isMobile); 134 + } 135 + } 136 + 125 137 const isCoarse = typeof window !== 'undefined' && window.matchMedia('(pointer: coarse)').matches; 126 138 setIsCoarse(() => isCoarse); 127 139 ··· 191 203 compactItems(items, false); 192 204 compactItems(items, true); 193 205 206 + onLayoutChanged(); 207 + 194 208 newItem = {}; 195 209 196 210 await tick(); ··· 214 228 if (data.publication?.icon) { 215 229 await checkAndUploadImage(data.publication, 'icon'); 216 230 } 231 + 232 + // Persist layout editing state 233 + data.publication.preferences ??= {}; 234 + data.publication.preferences.editedOn = editedOn; 217 235 218 236 await savePage(data, items, publication); 219 237 ··· 494 512 if (touchDragActive && activeDragElement.item) { 495 513 // Finalize position 496 514 fixCollisions(items, activeDragElement.item, isMobile); 515 + onLayoutChanged(); 497 516 498 517 activeDragElement.x = -1; 499 518 activeDragElement.y = -1; ··· 645 664 compactItems(items, true); 646 665 } 647 666 667 + onLayoutChanged(); 668 + 648 669 await tick(); 649 670 650 671 scrollToItem(item, isMobile, container); ··· 779 800 fixCollisions(items, item, true, true); 780 801 compactItems(items, false); 781 802 compactItems(items, true); 803 + 804 + onLayoutChanged(); 782 805 783 806 await tick(); 784 807 ··· 1002 1025 1003 1026 // Fix collisions and compact items after drag ends 1004 1027 fixCollisions(items, activeDragElement.item, isMobile); 1028 + onLayoutChanged(); 1005 1029 } 1006 1030 activeDragElement.x = -1; 1007 1031 activeDragElement.y = -1; ··· 1024 1048 items = items.filter((it) => it !== item); 1025 1049 compactItems(items, false); 1026 1050 compactItems(items, true); 1051 + onLayoutChanged(); 1027 1052 }} 1028 1053 onsetsize={(newW: number, newH: number) => { 1029 1054 if (isMobile) { ··· 1035 1060 } 1036 1061 1037 1062 fixCollisions(items, item, isMobile); 1063 + onLayoutChanged(); 1038 1064 }} 1039 1065 ondragstart={(e: DragEvent) => { 1040 1066 const target = e.currentTarget as HTMLDivElement; ··· 1068 1094 </div> 1069 1095 </div> 1070 1096 </div> 1071 - 1072 1097 1073 1098 <EditBar 1074 1099 {data} ··· 1095 1120 items = items.filter((it) => it.id !== selectedCardId); 1096 1121 compactItems(items, false); 1097 1122 compactItems(items, true); 1123 + onLayoutChanged(); 1098 1124 selectedCardId = null; 1099 1125 } 1100 1126 }} ··· 1108 1134 selectedCard.h = h; 1109 1135 } 1110 1136 fixCollisions(items, selectedCard, isMobile); 1137 + onLayoutChanged(); 1111 1138 } 1112 1139 }} 1113 1140 /> ··· 1115 1142 <Toaster /> 1116 1143 1117 1144 <FloatingEditButton {data} /> 1145 + 1146 + {#if dev} 1147 + <div class="bg-base-900/70 text-base-100 fixed top-2 right-2 z-50 rounded px-2 py-1 font-mono text-xs"> 1148 + editedOn: {editedOn} 1149 + </div> 1150 + {/if} 1118 1151 </Context>
+73
src/lib/website/layout-mirror.ts
··· 1 + import { COLUMNS } from '$lib'; 2 + import { CardDefinitionsByType } from '$lib/cards'; 3 + import { clamp, fixAllCollisions } from '$lib/helper'; 4 + import type { Item } from '$lib/types'; 5 + 6 + /** 7 + * Returns true when mirroring should still happen (i.e. user hasn't edited both layouts). 8 + * editedOn: 0/undefined = never, 1 = desktop only, 2 = mobile only, 3 = both 9 + */ 10 + export function shouldMirror(editedOn: number | undefined): boolean { 11 + return (editedOn ?? 0) !== 3; 12 + } 13 + 14 + /** Snap a value to the nearest even integer (min 2). */ 15 + function snapEven(v: number): number { 16 + return Math.max(2, Math.round(v / 2) * 2); 17 + } 18 + 19 + /** 20 + * Compute the other layout's size for a single item, preserving aspect ratio. 21 + * Clamps to the card definition's minW/maxW/minH/maxH if defined. 22 + * Mutates the item in-place. 23 + */ 24 + export function mirrorItemSize(item: Item, fromMobile: boolean): void { 25 + const def = CardDefinitionsByType[item.cardType]; 26 + const minW = def?.minW ?? 2; 27 + const maxW = def?.maxW ?? COLUMNS; 28 + const minH = def?.minH ?? 2; 29 + const maxH = def?.maxH ?? Infinity; 30 + 31 + if (fromMobile) { 32 + const srcW = item.mobileW; 33 + const srcH = item.mobileH; 34 + // Full-width cards stay full-width 35 + item.w = srcW >= COLUMNS ? COLUMNS : clamp(snapEven(srcW / 2), minW, maxW); 36 + item.h = clamp(snapEven((srcH * item.w) / srcW), minH, maxH); 37 + } else { 38 + const srcW = item.w; 39 + const srcH = item.h; 40 + // Full-width cards stay full-width 41 + if (srcW >= COLUMNS) { 42 + item.mobileW = clamp(COLUMNS, minW, Math.min(maxW, COLUMNS)); 43 + } else { 44 + const scaleFactor = Math.min(2, COLUMNS / srcW); 45 + item.mobileW = clamp(snapEven(srcW * scaleFactor), minW, Math.min(maxW, COLUMNS)); 46 + } 47 + item.mobileH = clamp(snapEven((srcH * item.mobileW) / srcW), minH, maxH); 48 + } 49 + } 50 + 51 + /** 52 + * Mirror the full layout from one view to the other. 53 + * Copies sizes proportionally and maps positions, then resolves collisions. 54 + * Mutates items in-place. 55 + */ 56 + export function mirrorLayout(items: Item[], fromMobile: boolean): void { 57 + for (const item of items) { 58 + mirrorItemSize(item, fromMobile); 59 + 60 + if (fromMobile) { 61 + // Mobile → Desktop positions 62 + item.x = clamp(Math.floor(item.mobileX / 2 / 2) * 2, 0, COLUMNS - item.w); 63 + item.y = Math.max(0, Math.round(item.mobileY / 2)); 64 + } else { 65 + // Desktop → Mobile positions 66 + item.mobileX = clamp(Math.floor((item.x * 2) / 2) * 2, 0, COLUMNS - item.mobileW); 67 + item.mobileY = Math.max(0, Math.round(item.y * 2)); 68 + } 69 + } 70 + 71 + // Resolve collisions on the target layout 72 + fixAllCollisions(items, !fromMobile); 73 + }