tangled
alpha
login
or
join now
flo-bit.dev
/
blento
21
fork
atom
your personal website on atproto - mirror
blento.app
21
fork
atom
overview
issues
pulls
pipelines
this is fine
Florian
1 month ago
f5a65c59
fc5edb07
+110
-1
3 changed files
expand all
collapse all
unified
split
src
lib
types.ts
website
EditableWebsite.svelte
layout-mirror.ts
+3
src/lib/types.ts
···
55
55
// theme colors
56
56
accentColor?: string;
57
57
baseColor?: string;
58
58
+
59
59
+
// layout mirroring: 0/undefined=never edited, 1=desktop only, 2=mobile only, 3=both
60
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
42
+
import { shouldMirror, mirrorLayout } from './layout-mirror';
42
43
43
44
let {
44
45
data
···
122
123
123
124
setIsMobile(() => isMobile);
124
125
126
126
+
// svelte-ignore state_referenced_locally
127
127
+
let editedOn = $state(data.publication.preferences?.editedOn ?? 0);
128
128
+
129
129
+
function onLayoutChanged() {
130
130
+
// Set the bit for the current layout: desktop=1, mobile=2
131
131
+
editedOn = editedOn | (isMobile ? 2 : 1);
132
132
+
if (shouldMirror(editedOn)) {
133
133
+
mirrorLayout(items, isMobile);
134
134
+
}
135
135
+
}
136
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
206
+
onLayoutChanged();
207
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
231
+
232
232
+
// Persist layout editing state
233
233
+
data.publication.preferences ??= {};
234
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
515
+
onLayoutChanged();
497
516
498
517
activeDragElement.x = -1;
499
518
activeDragElement.y = -1;
···
645
664
compactItems(items, true);
646
665
}
647
666
667
667
+
onLayoutChanged();
668
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
803
+
804
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
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
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
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
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
1123
+
onLayoutChanged();
1098
1124
selectedCardId = null;
1099
1125
}
1100
1126
}}
···
1108
1134
selectedCard.h = h;
1109
1135
}
1110
1136
fixCollisions(items, selectedCard, isMobile);
1137
1137
+
onLayoutChanged();
1111
1138
}
1112
1139
}}
1113
1140
/>
···
1115
1142
<Toaster />
1116
1143
1117
1144
<FloatingEditButton {data} />
1145
1145
+
1146
1146
+
{#if dev}
1147
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
1148
+
editedOn: {editedOn}
1149
1149
+
</div>
1150
1150
+
{/if}
1118
1151
</Context>
+73
src/lib/website/layout-mirror.ts
···
1
1
+
import { COLUMNS } from '$lib';
2
2
+
import { CardDefinitionsByType } from '$lib/cards';
3
3
+
import { clamp, fixAllCollisions } from '$lib/helper';
4
4
+
import type { Item } from '$lib/types';
5
5
+
6
6
+
/**
7
7
+
* Returns true when mirroring should still happen (i.e. user hasn't edited both layouts).
8
8
+
* editedOn: 0/undefined = never, 1 = desktop only, 2 = mobile only, 3 = both
9
9
+
*/
10
10
+
export function shouldMirror(editedOn: number | undefined): boolean {
11
11
+
return (editedOn ?? 0) !== 3;
12
12
+
}
13
13
+
14
14
+
/** Snap a value to the nearest even integer (min 2). */
15
15
+
function snapEven(v: number): number {
16
16
+
return Math.max(2, Math.round(v / 2) * 2);
17
17
+
}
18
18
+
19
19
+
/**
20
20
+
* Compute the other layout's size for a single item, preserving aspect ratio.
21
21
+
* Clamps to the card definition's minW/maxW/minH/maxH if defined.
22
22
+
* Mutates the item in-place.
23
23
+
*/
24
24
+
export function mirrorItemSize(item: Item, fromMobile: boolean): void {
25
25
+
const def = CardDefinitionsByType[item.cardType];
26
26
+
const minW = def?.minW ?? 2;
27
27
+
const maxW = def?.maxW ?? COLUMNS;
28
28
+
const minH = def?.minH ?? 2;
29
29
+
const maxH = def?.maxH ?? Infinity;
30
30
+
31
31
+
if (fromMobile) {
32
32
+
const srcW = item.mobileW;
33
33
+
const srcH = item.mobileH;
34
34
+
// Full-width cards stay full-width
35
35
+
item.w = srcW >= COLUMNS ? COLUMNS : clamp(snapEven(srcW / 2), minW, maxW);
36
36
+
item.h = clamp(snapEven((srcH * item.w) / srcW), minH, maxH);
37
37
+
} else {
38
38
+
const srcW = item.w;
39
39
+
const srcH = item.h;
40
40
+
// Full-width cards stay full-width
41
41
+
if (srcW >= COLUMNS) {
42
42
+
item.mobileW = clamp(COLUMNS, minW, Math.min(maxW, COLUMNS));
43
43
+
} else {
44
44
+
const scaleFactor = Math.min(2, COLUMNS / srcW);
45
45
+
item.mobileW = clamp(snapEven(srcW * scaleFactor), minW, Math.min(maxW, COLUMNS));
46
46
+
}
47
47
+
item.mobileH = clamp(snapEven((srcH * item.mobileW) / srcW), minH, maxH);
48
48
+
}
49
49
+
}
50
50
+
51
51
+
/**
52
52
+
* Mirror the full layout from one view to the other.
53
53
+
* Copies sizes proportionally and maps positions, then resolves collisions.
54
54
+
* Mutates items in-place.
55
55
+
*/
56
56
+
export function mirrorLayout(items: Item[], fromMobile: boolean): void {
57
57
+
for (const item of items) {
58
58
+
mirrorItemSize(item, fromMobile);
59
59
+
60
60
+
if (fromMobile) {
61
61
+
// Mobile → Desktop positions
62
62
+
item.x = clamp(Math.floor(item.mobileX / 2 / 2) * 2, 0, COLUMNS - item.w);
63
63
+
item.y = Math.max(0, Math.round(item.mobileY / 2));
64
64
+
} else {
65
65
+
// Desktop → Mobile positions
66
66
+
item.mobileX = clamp(Math.floor((item.x * 2) / 2) * 2, 0, COLUMNS - item.mobileW);
67
67
+
item.mobileY = Math.max(0, Math.round(item.y * 2));
68
68
+
}
69
69
+
}
70
70
+
71
71
+
// Resolve collisions on the target layout
72
72
+
fixAllCollisions(items, !fromMobile);
73
73
+
}