From da5fd6fba2f1516fa1b78be6113db1778c3d23c1 Mon Sep 17 00:00:00 2001
From: Florian <45694132+flo-bit@users.noreply.github.com>
Date: Thu, 05 Feb 2026 11:02:53 +0000
Subject: [PATCH] Merge pull request #160 from flo-bit/switch-grid-layout
Switch grid layout editing to react-grid-layout
---
.gitignore | 2 ++
package.json | 1 +
pnpm-lock.yaml | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/lib/helper.ts | 361 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
src/lib/layout/EditableGrid.svelte | 398 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/lib/layout/algorithms.ts | 254 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/lib/layout/grid.ts | 190 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/lib/layout/index.ts | 15 +++++++++++++++
src/lib/layout/mirror.ts | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/lib/website/EditableWebsite.svelte | 640 ++++++++++++++++++++++++++++++++++++++++++----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
src/lib/website/layout-mirror.ts | 72 ------------------------------------------------------------------------
src/lib/website/load.ts | 6 +++---
src/lib/cards/_base/BaseCard/BaseCard.svelte | 12 +-----------
src/lib/cards/_base/BaseCard/BaseEditingCard.svelte | 11 ++++-------
14 file(s) changed, 1093 insertion(s)(+), 1052 deletion(s)(-)
diff --git a/.gitignore b/.gitignore
--- a/.gitignore
+++ b/.gitignore
@@ -21,3 +21,5 @@
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
+
+react-grid-layout
\ No newline at end of file
diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -81,6 +81,7 @@
"perfect-freehand": "^1.2.2",
"plyr": "^3.8.4",
"qr-code-styling": "^1.8.6",
+ "react-grid-layout": "^2.2.2",
"simple-icons": "^16.6.0",
"svelte-sonner": "^1.0.7",
"tailwind-merge": "^3.4.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -134,6 +134,9 @@
qr-code-styling:
specifier: ^1.8.6
version: 1.9.2
+ react-grid-layout:
+ specifier: ^2.2.2
+ version: 2.2.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
simple-icons:
specifier: ^16.6.0
version: 16.6.0
@@ -1967,6 +1970,9 @@
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+ fast-equals@4.0.3:
+ resolution: {integrity: sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==, tarball: https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz}
+
fast-json-stable-stringify@2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
@@ -2102,6 +2108,9 @@
jiti@2.6.1:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==, tarball: https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz}
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
@@ -2239,6 +2248,10 @@
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+ loose-envify@1.4.0:
+ resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==, tarball: https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz}
+ hasBin: true
+
lz-string@1.5.0:
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
hasBin: true
@@ -2339,6 +2352,10 @@
number-flow@0.5.9:
resolution: {integrity: sha512-o3102c/4qRd6eV4n+rw6B/UP8+FosbhIxj4uA6GsjhryrGZRVtCtKIKEeBiOwUV52cUGJneeu0treELcV7U/lw==}
+
+ object-assign@4.1.1:
+ resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==, tarball: https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz}
+ engines: {node: '>=0.10.0'}
obug@2.1.1:
resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
@@ -2526,6 +2543,9 @@
engines: {node: '>=14'}
hasBin: true
+ prop-types@15.8.1:
+ resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==, tarball: https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz}
+
prosemirror-changeset@2.3.1:
resolution: {integrity: sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==}
@@ -2608,6 +2628,36 @@
rangetouch@2.0.1:
resolution: {integrity: sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA==}
+ react-dom@19.2.4:
+ resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==, tarball: https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz}
+ peerDependencies:
+ react: ^19.2.4
+
+ react-draggable@4.5.0:
+ resolution: {integrity: sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==, tarball: https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz}
+ peerDependencies:
+ react: '>= 16.3.0'
+ react-dom: '>= 16.3.0'
+
+ react-grid-layout@2.2.2:
+ resolution: {integrity: sha512-yNo9pxQWoxHWRAwHGSVT4DEGELYPyQ7+q9lFclb5jcqeFzva63/2F72CryS/jiTIr/SBIlTaDdyjqH+ODg8oBw==, tarball: https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-2.2.2.tgz}
+ peerDependencies:
+ react: '>= 16.3.0'
+ react-dom: '>= 16.3.0'
+
+ react-is@16.13.1:
+ resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==, tarball: https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz}
+
+ react-resizable@3.1.3:
+ resolution: {integrity: sha512-liJBNayhX7qA4tBJiBD321FDhJxgGTJ07uzH5zSORXoE8h7PyEZ8mLqmosST7ppf6C4zUsbd2gzDMmBCfFp9Lw==, tarball: https://registry.npmjs.org/react-resizable/-/react-resizable-3.1.3.tgz}
+ peerDependencies:
+ react: '>= 16.3'
+ react-dom: '>= 16.3'
+
+ react@19.2.4:
+ resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==, tarball: https://registry.npmjs.org/react/-/react-19.2.4.tgz}
+ engines: {node: '>=0.10.0'}
+
readdirp@4.1.2:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
@@ -2619,6 +2669,9 @@
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
+
+ resize-observer-polyfill@1.5.1:
+ resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==, tarball: https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz}
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
@@ -2675,6 +2728,9 @@
satori@0.10.14:
resolution: {integrity: sha512-abovcqmwl97WKioxpkfuMeZmndB1TuDFY/R+FymrZyiGP+pMYomvgSzVPnbNMWHHESOPosVHGL352oFbdAnJcA==}
engines: {node: '>=16'}
+
+ scheduler@0.27.0:
+ resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==, tarball: https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz}
semver@7.7.3:
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
@@ -4731,6 +4787,8 @@
fast-deep-equal@3.1.3: {}
+ fast-equals@4.0.3: {}
+
fast-json-stable-stringify@2.1.0: {}
fast-levenshtein@2.0.6: {}
@@ -4837,6 +4895,8 @@
jiti@2.6.1: {}
+ js-tokens@4.0.0: {}
+
js-yaml@4.1.1:
dependencies:
argparse: 2.0.1
@@ -4941,6 +5001,10 @@
p-locate: 5.0.0
lodash.merge@4.6.2: {}
+
+ loose-envify@1.4.0:
+ dependencies:
+ js-tokens: 4.0.0
lz-string@1.5.0: {}
@@ -5066,6 +5130,8 @@
number-flow@0.5.9:
dependencies:
esm-env: 1.2.2
+
+ object-assign@4.1.1: {}
obug@2.1.1: {}
@@ -5200,6 +5266,12 @@
prettier@3.8.1: {}
+ prop-types@15.8.1:
+ dependencies:
+ loose-envify: 1.4.0
+ object-assign: 4.1.1
+ react-is: 16.13.1
+
prosemirror-changeset@2.3.1:
dependencies:
prosemirror-transform: 1.11.0
@@ -5319,11 +5391,47 @@
rangetouch@2.0.1: {}
+ react-dom@19.2.4(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+ scheduler: 0.27.0
+
+ react-draggable@4.5.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ dependencies:
+ clsx: 2.1.1
+ prop-types: 15.8.1
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+
+ react-grid-layout@2.2.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ dependencies:
+ clsx: 2.1.1
+ fast-equals: 4.0.3
+ prop-types: 15.8.1
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ react-draggable: 4.5.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react-resizable: 3.1.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ resize-observer-polyfill: 1.5.1
+
+ react-is@16.13.1: {}
+
+ react-resizable@3.1.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ dependencies:
+ prop-types: 15.8.1
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ react-draggable: 4.5.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+
+ react@19.2.4: {}
+
readdirp@4.1.2: {}
regexparam@3.0.0: {}
require-from-string@2.0.2: {}
+
+ resize-observer-polyfill@1.5.1: {}
resolve-from@4.0.0: {}
@@ -5412,6 +5520,8 @@
parse-css-color: 0.2.1
postcss-value-parser: 4.2.0
yoga-wasm-web: 0.3.3
+
+ scheduler@0.27.0: {}
semver@7.7.3: {}
diff --git a/src/lib/helper.ts b/src/lib/helper.ts
--- a/src/lib/helper.ts
+++ b/src/lib/helper.ts
@@ -3,7 +3,6 @@
import { CardDefinitionsByType } from './cards';
import { deleteRecord, getCDNImageBlobUrl, putRecord, uploadBlob } from '$lib/atproto';
import * as TID from '@atcute/tid';
-
export function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
@@ -28,229 +27,6 @@
'bg-rose-500'
];
-export const overlaps = (a: Item, b: Item, mobile: boolean = false) => {
- if (a === b) return false;
- if (mobile) {
- return (
- a.mobileX < b.mobileX + b.mobileW &&
- a.mobileX + a.mobileW > b.mobileX &&
- a.mobileY < b.mobileY + b.mobileH &&
- a.mobileY + a.mobileH > b.mobileY
- );
- }
- return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
-};
-
-export function fixCollisions(
- items: Item[],
- movedItem: Item,
- mobile: boolean = false,
- skipCompact: boolean = false
-) {
- const clampX = (item: Item) => {
- if (mobile) item.mobileX = clamp(item.mobileX, 0, COLUMNS - item.mobileW);
- else item.x = clamp(item.x, 0, COLUMNS - item.w);
- };
-
- // Push `target` down until it no longer overlaps with any item (including movedItem),
- // while keeping target.x fixed. Any item we collide with gets pushed down first (cascade).
- const pushDownCascade = (target: Item, blocker: Item) => {
- // Keep x fixed always when pushing down
- const fixedX = mobile ? target.mobileX : target.x;
- const prevY = mobile ? target.mobileY : target.y;
-
- // We need target to move just below `blocker`
- const desiredY = mobile ? blocker.mobileY + blocker.mobileH : blocker.y + blocker.h;
- if (!mobile && target.y < desiredY) target.y = desiredY;
- if (mobile && target.mobileY < desiredY) target.mobileY = desiredY;
-
- const newY = mobile ? target.mobileY : target.y;
- const targetH = mobile ? target.mobileH : target.h;
-
- // fall trough fix
- if (newY > prevY) {
- const prevBottom = prevY + targetH;
- const newBottom = newY + targetH;
- for (const it of items) {
- if (it === target || it === movedItem || it === blocker) continue;
- const itY = mobile ? it.mobileY : it.y;
- const itH = mobile ? it.mobileH : it.h;
- const itBottom = itY + itH;
- if (itBottom <= prevBottom || itY >= newBottom) continue;
- // horizontal overlap check
- const hOverlap = mobile
- ? target.mobileX < it.mobileX + it.mobileW && target.mobileX + target.mobileW > it.mobileX
- : target.x < it.x + it.w && target.x + target.w > it.x;
- if (hOverlap) {
- pushDownCascade(it, target);
- }
- }
- }
-
- // Now resolve any collisions that creates by pushing those items down first
- // Repeat until target is clean.
- while (true) {
- const hit = items.find((it) => it !== target && overlaps(target, it, mobile));
- if (!hit) break;
-
- // push the hit item down first (cascade), keeping its x fixed
- pushDownCascade(hit, target);
-
- // after moving the hit item, target.x must remain fixed
- if (mobile) target.mobileX = fixedX;
- else target.x = fixedX;
- }
- };
-
- // Ensure moved item is in bounds
- clampX(movedItem);
-
- // Find all items colliding with movedItem, and push them down in a stable order:
- // top-to-bottom so you get the nice chain reaction (0,0 -> 0,1 -> 0,2).
- const colliders = items
- .filter((it) => it !== movedItem && overlaps(movedItem, it, mobile))
- .toSorted((a, b) =>
- mobile ? a.mobileY - b.mobileY || a.mobileX - b.mobileX : a.y - b.y || a.x - b.x
- );
-
- for (const it of colliders) {
- // keep x clamped, but do NOT change x during push (we rely on fixed x)
- clampX(it);
-
- // push it down just below movedItem; cascade handles the rest
- pushDownCascade(it, movedItem);
-
- // enforce "x stays the same" during pushing (clamp already applied)
- if (mobile) it.mobileX = clamp(it.mobileX, 0, COLUMNS - it.mobileW);
- else it.x = clamp(it.x, 0, COLUMNS - it.w);
- }
-
- if (!skipCompact) {
- compactItems(items, mobile);
- }
-}
-
-// Fix all collisions between items (not just one moved item)
-// Items higher on the page have priority and stay in place
-export function fixAllCollisions(items: Item[], mobile: boolean = false) {
- // Sort by Y position (top-to-bottom, then left-to-right)
- // Items at the top have priority and won't be moved
- const sortedItems = items.toSorted((a, b) =>
- mobile ? a.mobileY - b.mobileY || a.mobileX - b.mobileX : a.y - b.y || a.x - b.x
- );
-
- // Process each item and push it down if it overlaps with any item above it
- for (let i = 0; i < sortedItems.length; i++) {
- const item = sortedItems[i];
-
- // Clamp X to valid range
- if (mobile) {
- item.mobileX = clamp(item.mobileX, 0, COLUMNS - item.mobileW);
- } else {
- item.x = clamp(item.x, 0, COLUMNS - item.w);
- }
-
- // Check for collisions with all items that come before (higher priority)
- let hasCollision = true;
- while (hasCollision) {
- hasCollision = false;
- for (let j = 0; j < i; j++) {
- const other = sortedItems[j];
- if (overlaps(item, other, mobile)) {
- // Push item down below the colliding item
- if (mobile) {
- item.mobileY = other.mobileY + other.mobileH;
- } else {
- item.y = other.y + other.h;
- }
- hasCollision = true;
- break; // Restart collision check from the beginning
- }
- }
- }
- }
-
- compactItems(items, mobile);
-}
-
-// Move all items up as far as possible without collisions
-export function compactItems(items: Item[], mobile: boolean = false) {
- // Sort by Y position (top-to-bottom) so upper items settle first.
- const sortedItems = items.toSorted((a, b) =>
- mobile ? a.mobileY - b.mobileY || a.mobileX - b.mobileX : a.y - b.y || a.x - b.x
- );
-
- // For each item, find the lowest Y it can occupy by checking the bottom edges
- // of all horizontally-overlapping items already placed above it.
- const settled: Item[] = [];
-
- for (const item of sortedItems) {
- const itemX = mobile ? item.mobileX : item.x;
- const itemW = mobile ? item.mobileW : item.w;
-
- let minY = 0;
-
- for (const other of settled) {
- const otherX = mobile ? other.mobileX : other.x;
- const otherW = mobile ? other.mobileW : other.w;
-
- // Check horizontal overlap
- if (itemX < otherX + otherW && itemX + itemW > otherX) {
- const otherBottom = mobile ? other.mobileY + other.mobileH : other.y + other.h;
- if (otherBottom > minY) {
- minY = otherBottom;
- }
- }
- }
-
- if (mobile) {
- item.mobileY = minY;
- } else {
- item.y = minY;
- }
-
- settled.push(item);
- }
-}
-
-// Simulate where an item would end up after fixCollisions + compaction
-export function simulateFinalPosition(
- items: Item[],
- movedItem: Item,
- newX: number,
- newY: number,
- mobile: boolean = false
-): { x: number; y: number } {
- // Deep clone positions for simulation
- const clonedItems: Item[] = items.map((item) => ({
- ...item,
- x: item.x,
- y: item.y,
- mobileX: item.mobileX,
- mobileY: item.mobileY
- }));
-
- const clonedMovedItem = clonedItems.find((item) => item.id === movedItem.id);
- if (!clonedMovedItem) return { x: newX, y: newY };
-
- // Set the new position
- if (mobile) {
- clonedMovedItem.mobileX = newX;
- clonedMovedItem.mobileY = newY;
- } else {
- clonedMovedItem.x = newX;
- clonedMovedItem.y = newY;
- }
-
- // Run fixCollisions on the cloned data
- fixCollisions(clonedItems, clonedMovedItem, mobile);
-
- // Return the final position of the moved item
- return mobile
- ? { x: clonedMovedItem.mobileX, y: clonedMovedItem.mobileY }
- : { x: clonedMovedItem.x, y: clonedMovedItem.y };
-}
-
export function sortItems(a: Item, b: Item) {
return a.y * COLUMNS + a.x - b.y * COLUMNS - b.x;
}
@@ -271,143 +47,6 @@
a.color === b.color &&
a.page === b.page
);
-}
-
-export function setPositionOfNewItem(
- newItem: Item,
- items: Item[],
- viewportCenter?: { gridY: number; isMobile: boolean }
-) {
- if (viewportCenter) {
- const { gridY, isMobile } = viewportCenter;
-
- if (isMobile) {
- // Place at viewport center Y
- newItem.mobileY = Math.max(0, Math.round(gridY - newItem.mobileH / 2));
- newItem.mobileY = Math.floor(newItem.mobileY / 2) * 2;
-
- // Try to find a free X at this Y
- let found = false;
- for (
- newItem.mobileX = 0;
- newItem.mobileX <= COLUMNS - newItem.mobileW;
- newItem.mobileX += 2
- ) {
- if (!items.some((item) => overlaps(newItem, item, true))) {
- found = true;
- break;
- }
- }
- if (!found) {
- newItem.mobileX = 0;
- }
-
- // Desktop: derive from mobile
- newItem.y = Math.max(0, Math.round(newItem.mobileY / 2));
- found = false;
- for (newItem.x = 0; newItem.x <= COLUMNS - newItem.w; newItem.x += 2) {
- if (!items.some((item) => overlaps(newItem, item, false))) {
- found = true;
- break;
- }
- }
- if (!found) {
- newItem.x = 0;
- }
- } else {
- // Place at viewport center Y
- newItem.y = Math.max(0, Math.round(gridY - newItem.h / 2));
-
- // Try to find a free X at this Y
- let found = false;
- for (newItem.x = 0; newItem.x <= COLUMNS - newItem.w; newItem.x += 2) {
- if (!items.some((item) => overlaps(newItem, item, false))) {
- found = true;
- break;
- }
- }
- if (!found) {
- newItem.x = 0;
- }
-
- // Mobile: derive from desktop
- newItem.mobileY = Math.max(0, Math.round(newItem.y * 2));
- found = false;
- for (
- newItem.mobileX = 0;
- newItem.mobileX <= COLUMNS - newItem.mobileW;
- newItem.mobileX += 2
- ) {
- if (!items.some((item) => overlaps(newItem, item, true))) {
- found = true;
- break;
- }
- }
- if (!found) {
- newItem.mobileX = 0;
- }
- }
- return;
- }
-
- let foundPosition = false;
- while (!foundPosition) {
- for (newItem.x = 0; newItem.x <= COLUMNS - newItem.w; newItem.x++) {
- const collision = items.find((item) => overlaps(newItem, item));
- if (!collision) {
- foundPosition = true;
- break;
- }
- }
- if (!foundPosition) newItem.y += 1;
- }
-
- let foundMobilePosition = false;
- while (!foundMobilePosition) {
- for (newItem.mobileX = 0; newItem.mobileX <= COLUMNS - newItem.mobileW; newItem.mobileX += 1) {
- const collision = items.find((item) => overlaps(newItem, item, true));
-
- if (!collision) {
- foundMobilePosition = true;
- break;
- }
- }
- if (!foundMobilePosition) newItem.mobileY! += 1;
- }
-}
-
-/**
- * Find a valid position for a new item in a single mode (desktop or mobile).
- * This modifies the item's position properties in-place.
- */
-export function findValidPosition(newItem: Item, items: Item[], mobile: boolean) {
- if (mobile) {
- let foundPosition = false;
- newItem.mobileY = 0;
- while (!foundPosition) {
- for (newItem.mobileX = 0; newItem.mobileX <= COLUMNS - newItem.mobileW; newItem.mobileX++) {
- const collision = items.find((item) => overlaps(newItem, item, true));
- if (!collision) {
- foundPosition = true;
- break;
- }
- }
- if (!foundPosition) newItem.mobileY! += 1;
- }
- } else {
- let foundPosition = false;
- newItem.y = 0;
- while (!foundPosition) {
- for (newItem.x = 0; newItem.x <= COLUMNS - newItem.w; newItem.x++) {
- const collision = items.find((item) => overlaps(newItem, item, false));
- if (!collision) {
- foundPosition = true;
- break;
- }
- }
- if (!foundPosition) newItem.y += 1;
- }
- }
}
export async function refreshData(data: { updatedAt?: number; handle: string }) {
diff --git a/src/lib/layout/EditableGrid.svelte b/src/lib/layout/EditableGrid.svelte
new file mode 100644
--- /dev/null
+++ b/src/lib/layout/EditableGrid.svelte
@@ -0,0 +1,398 @@
+
+
+