From f5a65c5918300b1993502d2409b305cf2ebe377a Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Sat, 31 Jan 2026 20:00:08 +0000 Subject: [PATCH] this is fine --- src/lib/types.ts | 3 +++ src/lib/website/EditableWebsite.svelte | 35 ++++++++++++++++++++++++++++++++++- src/lib/website/layout-mirror.ts | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 file(s) changed, 110 insertion(s)(+), 1 deletion(s)(-) diff --git a/src/lib/types.ts b/src/lib/types.ts --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -55,6 +55,9 @@ // theme colors accentColor?: string; baseColor?: string; + + // layout mirroring: 0/undefined=never edited, 1=desktop only, 2=mobile only, 3=both + editedOn?: number; }; }; profile: AppBskyActorDefs.ProfileViewDetailed; diff --git a/src/lib/website/EditableWebsite.svelte b/src/lib/website/EditableWebsite.svelte --- a/src/lib/website/EditableWebsite.svelte +++ b/src/lib/website/EditableWebsite.svelte @@ -39,6 +39,7 @@ import { launchConfetti } from '@foxui/visual'; import Controls from './Controls.svelte'; import CardCommand from '$lib/components/card-command/CardCommand.svelte'; + import { shouldMirror, mirrorLayout } from './layout-mirror'; let { data @@ -122,6 +123,17 @@ setIsMobile(() => isMobile); + // svelte-ignore state_referenced_locally + let editedOn = $state(data.publication.preferences?.editedOn ?? 0); + + function onLayoutChanged() { + // Set the bit for the current layout: desktop=1, mobile=2 + editedOn = editedOn | (isMobile ? 2 : 1); + if (shouldMirror(editedOn)) { + mirrorLayout(items, isMobile); + } + } + const isCoarse = typeof window !== 'undefined' && window.matchMedia('(pointer: coarse)').matches; setIsCoarse(() => isCoarse); @@ -191,6 +203,8 @@ compactItems(items, false); compactItems(items, true); + onLayoutChanged(); + newItem = {}; await tick(); @@ -214,6 +228,10 @@ if (data.publication?.icon) { await checkAndUploadImage(data.publication, 'icon'); } + + // Persist layout editing state + data.publication.preferences ??= {}; + data.publication.preferences.editedOn = editedOn; await savePage(data, items, publication); @@ -494,6 +512,7 @@ if (touchDragActive && activeDragElement.item) { // Finalize position fixCollisions(items, activeDragElement.item, isMobile); + onLayoutChanged(); activeDragElement.x = -1; activeDragElement.y = -1; @@ -645,6 +664,8 @@ compactItems(items, true); } + onLayoutChanged(); + await tick(); scrollToItem(item, isMobile, container); @@ -779,6 +800,8 @@ fixCollisions(items, item, true, true); compactItems(items, false); compactItems(items, true); + + onLayoutChanged(); await tick(); @@ -1002,6 +1025,7 @@ // Fix collisions and compact items after drag ends fixCollisions(items, activeDragElement.item, isMobile); + onLayoutChanged(); } activeDragElement.x = -1; activeDragElement.y = -1; @@ -1024,6 +1048,7 @@ items = items.filter((it) => it !== item); compactItems(items, false); compactItems(items, true); + onLayoutChanged(); }} onsetsize={(newW: number, newH: number) => { if (isMobile) { @@ -1035,6 +1060,7 @@ } fixCollisions(items, item, isMobile); + onLayoutChanged(); }} ondragstart={(e: DragEvent) => { const target = e.currentTarget as HTMLDivElement; @@ -1069,7 +1095,6 @@ - it.id !== selectedCardId); compactItems(items, false); compactItems(items, true); + onLayoutChanged(); selectedCardId = null; } }} @@ -1108,6 +1134,7 @@ selectedCard.h = h; } fixCollisions(items, selectedCard, isMobile); + onLayoutChanged(); } }} /> @@ -1115,4 +1142,10 @@ + + {#if dev} +
+ editedOn: {editedOn} +
+ {/if} diff --git a/src/lib/website/layout-mirror.ts b/src/lib/website/layout-mirror.ts new file mode 100644 --- /dev/null +++ b/src/lib/website/layout-mirror.ts @@ -0,0 +1,73 @@ +import { COLUMNS } from '$lib'; +import { CardDefinitionsByType } from '$lib/cards'; +import { clamp, fixAllCollisions } from '$lib/helper'; +import type { Item } from '$lib/types'; + +/** + * Returns true when mirroring should still happen (i.e. user hasn't edited both layouts). + * editedOn: 0/undefined = never, 1 = desktop only, 2 = mobile only, 3 = both + */ +export function shouldMirror(editedOn: number | undefined): boolean { + return (editedOn ?? 0) !== 3; +} + +/** Snap a value to the nearest even integer (min 2). */ +function snapEven(v: number): number { + return Math.max(2, Math.round(v / 2) * 2); +} + +/** + * Compute the other layout's size for a single item, preserving aspect ratio. + * Clamps to the card definition's minW/maxW/minH/maxH if defined. + * Mutates the item in-place. + */ +export function mirrorItemSize(item: Item, fromMobile: boolean): void { + const def = CardDefinitionsByType[item.cardType]; + const minW = def?.minW ?? 2; + const maxW = def?.maxW ?? COLUMNS; + const minH = def?.minH ?? 2; + const maxH = def?.maxH ?? Infinity; + + if (fromMobile) { + const srcW = item.mobileW; + const srcH = item.mobileH; + // Full-width cards stay full-width + item.w = srcW >= COLUMNS ? COLUMNS : clamp(snapEven(srcW / 2), minW, maxW); + item.h = clamp(snapEven((srcH * item.w) / srcW), minH, maxH); + } else { + const srcW = item.w; + const srcH = item.h; + // Full-width cards stay full-width + if (srcW >= COLUMNS) { + item.mobileW = clamp(COLUMNS, minW, Math.min(maxW, COLUMNS)); + } else { + const scaleFactor = Math.min(2, COLUMNS / srcW); + item.mobileW = clamp(snapEven(srcW * scaleFactor), minW, Math.min(maxW, COLUMNS)); + } + item.mobileH = clamp(snapEven((srcH * item.mobileW) / srcW), minH, maxH); + } +} + +/** + * Mirror the full layout from one view to the other. + * Copies sizes proportionally and maps positions, then resolves collisions. + * Mutates items in-place. + */ +export function mirrorLayout(items: Item[], fromMobile: boolean): void { + for (const item of items) { + mirrorItemSize(item, fromMobile); + + if (fromMobile) { + // Mobile → Desktop positions + item.x = clamp(Math.floor(item.mobileX / 2 / 2) * 2, 0, COLUMNS - item.w); + item.y = Math.max(0, Math.round(item.mobileY / 2)); + } else { + // Desktop → Mobile positions + item.mobileX = clamp(Math.floor((item.x * 2) / 2) * 2, 0, COLUMNS - item.mobileW); + item.mobileY = Math.max(0, Math.round(item.y * 2)); + } + } + + // Resolve collisions on the target layout + fixAllCollisions(items, !fromMobile); +} -- tangled.sh