diff --git a/package.json b/package.json index b7e00bf..b38512c 100644 --- 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 index fefeeb3..8d2acdb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -134,6 +134,9 @@ importers: 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 @@ packages: 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==} @@ -2103,6 +2109,9 @@ packages: 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==} hasBin: true @@ -2239,6 +2248,10 @@ packages: 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 @@ -2340,6 +2353,10 @@ packages: 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 @@ packages: 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 @@ packages: 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'} @@ -2620,6 +2670,9 @@ packages: 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==} engines: {node: '>=4'} @@ -2676,6 +2729,9 @@ packages: 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==} engines: {node: '>=10'} @@ -4731,6 +4787,8 @@ snapshots: 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 @@ snapshots: jiti@2.6.1: {} + js-tokens@4.0.0: {} + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -4942,6 +5002,10 @@ snapshots: lodash.merge@4.6.2: {} + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + lz-string@1.5.0: {} maath@0.10.8(@types/three@0.176.0)(three@0.176.0): @@ -5067,6 +5131,8 @@ snapshots: dependencies: esm-env: 1.2.2 + object-assign@4.1.1: {} + obug@2.1.1: {} optionator@0.9.4: @@ -5200,6 +5266,12 @@ snapshots: 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,12 +5391,48 @@ snapshots: 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: {} resolve-protobuf-schema@2.1.0: @@ -5413,6 +5521,8 @@ snapshots: postcss-value-parser: 4.2.0 yoga-wasm-web: 0.3.3 + scheduler@0.27.0: {} + semver@7.7.3: {} set-cookie-parser@2.7.2: {} diff --git a/src/lib/cards/_base/BaseCard/BaseEditingCard.svelte b/src/lib/cards/_base/BaseCard/BaseEditingCard.svelte index 243aae4..fac957f 100644 --- a/src/lib/cards/_base/BaseCard/BaseEditingCard.svelte +++ b/src/lib/cards/_base/BaseCard/BaseEditingCard.svelte @@ -15,7 +15,6 @@ getSelectCard } from '$lib/website/context'; import PlainTextEditor from '$lib/components/PlainTextEditor.svelte'; - import { fixAllCollisions, fixCollisions } from '$lib/helper'; let colorsChoices = [ { class: 'text-base-500', label: 'base' }, diff --git a/src/lib/helper.ts b/src/lib/helper.ts index 0253e6e..e1f00e4 100644 --- a/src/lib/helper.ts +++ b/src/lib/helper.ts @@ -3,6 +3,7 @@ import { COLUMNS, margin, mobileMargin } from '$lib'; import { CardDefinitionsByType } from './cards'; import { deleteRecord, getCDNImageBlobUrl, putRecord, uploadBlob } from '$lib/atproto'; import * as TID from '@atcute/tid'; +import { overlaps } from './layout'; export function clamp(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); @@ -28,228 +29,6 @@ export const colors = [ '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; @@ -353,7 +132,7 @@ export function setPositionOfNewItem( let foundPosition = false; while (!foundPosition) { for (newItem.x = 0; newItem.x <= COLUMNS - newItem.w; newItem.x++) { - const collision = items.find((item) => overlaps(newItem, item)); + const collision = items.find((item) => overlaps(newItem, item, false)); if (!collision) { foundPosition = true; break; diff --git a/src/lib/layout.ts b/src/lib/layout.ts new file mode 100644 index 0000000..697458b --- /dev/null +++ b/src/lib/layout.ts @@ -0,0 +1,110 @@ +import { type LayoutItem, type Layout } from 'react-grid-layout/core'; +import { + collides, + moveElement, + correctBounds, + getFirstCollision, + verticalCompactor +} from 'react-grid-layout/core'; +import type { Item } from './types'; +import { COLUMNS } from '$lib'; +import { clamp } from './helper'; + +function toLayoutItem(item: Item, mobile: boolean): LayoutItem { + if (mobile) { + return { + x: item.mobileX, + y: item.mobileY, + w: item.mobileW, + h: item.mobileH, + i: item.id + }; + } + return { + x: item.x, + y: item.y, + w: item.w, + h: item.h, + i: item.id + }; +} + +function toLayout(items: Item[], mobile: boolean): LayoutItem[] { + return items.map((i) => toLayoutItem(i, mobile)); +} + +function applyLayout(items: Item[], layout: LayoutItem[], mobile: boolean): void { + const itemsMap: Map = new Map(); + + for (const item of items) { + itemsMap.set(item.id, item); + } + for (const l of layout) { + const item = itemsMap.get(l.i); + + if (!item) { + console.error('item not found in layout!! this should never happen!'); + continue; + } + + if (mobile) { + item.mobileX = l.x; + item.mobileY = l.y; + } else { + item.x = l.x; + item.y = l.y; + } + } +} + +export function overlaps(a: Item, b: Item, mobile: boolean) { + if (a === b) return false; + return collides(toLayoutItem(a, mobile), toLayoutItem(b, mobile)); +} + +export function fixCollisions( + items: Item[], + item: Item, + mobile: boolean = false, + skipCompact: boolean = false +) { + if (mobile) item.mobileX = clamp(item.mobileX, 0, COLUMNS - item.mobileW); + else item.x = clamp(item.x, 0, COLUMNS - item.w); + + let layout = toLayout(items, mobile); + + const movedLayoutItem = layout.find((i) => i.i === item.id); + + if (!movedLayoutItem) { + console.error('item not found in layout! this should never happen!'); + return; + } + + layout = moveElement( + layout, + movedLayoutItem, + movedLayoutItem.x, + movedLayoutItem.y, + true, + false, + 'vertical', + COLUMNS + ); + + if (!skipCompact) layout = verticalCompactor.compact(layout, COLUMNS) as LayoutItem[]; + + applyLayout(items, layout, mobile); +} + +export function fixAllCollisions(items: Item[], mobile: boolean) { + let layout = toLayout(items, mobile); + correctBounds(layout as any, { cols: COLUMNS }); + layout = verticalCompactor.compact(layout, COLUMNS) as LayoutItem[]; + applyLayout(items, layout, mobile); +} + +export function compactItems(items: Item[], mobile: boolean) { + const layout = toLayout(items, mobile); + const compacted = verticalCompactor.compact(layout, COLUMNS) as LayoutItem[]; + applyLayout(items, compacted, mobile); +} diff --git a/src/lib/website/EditableWebsite.svelte b/src/lib/website/EditableWebsite.svelte index b0fc5b1..ed64123 100644 --- a/src/lib/website/EditableWebsite.svelte +++ b/src/lib/website/EditableWebsite.svelte @@ -4,11 +4,7 @@ import { checkAndUploadImage, clamp, - compactItems, createEmptyCard, - findValidPosition, - fixAllCollisions, - fixCollisions, getHideProfileSection, getProfilePosition, getName, @@ -43,6 +39,7 @@ import CardCommand from '$lib/components/card-command/CardCommand.svelte'; import { shouldMirror, mirrorLayout } from './layout-mirror'; import { SvelteMap } from 'svelte/reactivity'; + import { fixCollisions, compactItems, fixAllCollisions } from '$lib/layout'; let { data @@ -546,9 +543,9 @@ return; } - fixAllCollisions(copiedCards); + fixAllCollisions(copiedCards, false); fixAllCollisions(copiedCards, true); - compactItems(copiedCards); + compactItems(copiedCards, false); compactItems(copiedCards, true); items = copiedCards; diff --git a/src/lib/website/layout-mirror.ts b/src/lib/website/layout-mirror.ts index 9de9a8c..fa4dacd 100644 --- a/src/lib/website/layout-mirror.ts +++ b/src/lib/website/layout-mirror.ts @@ -1,6 +1,7 @@ import { COLUMNS } from '$lib'; import { CardDefinitionsByType } from '$lib/cards'; -import { clamp, findValidPosition, fixAllCollisions } from '$lib/helper'; +import { clamp, findValidPosition } from '$lib/helper'; +import { fixAllCollisions } from '$lib/layout'; import type { Item } from '$lib/types'; /** diff --git a/src/lib/website/load.ts b/src/lib/website/load.ts index e6ef259..f52407a 100644 --- a/src/lib/website/load.ts +++ b/src/lib/website/load.ts @@ -1,11 +1,11 @@ import { getDetailedProfile, listRecords, resolveHandle, parseUri, getRecord } from '$lib/atproto'; import { CardDefinitionsByType } from '$lib/cards'; import type { Item, UserCache, WebsiteData } from '$lib/types'; -import { compactItems, fixAllCollisions } from '$lib/helper'; import { error } from '@sveltejs/kit'; import type { ActorIdentifier, Did } from '@atcute/lexicons'; import { isDid, isHandle } from '@atcute/lexicons/syntax'; +import { fixAllCollisions, compactItems } from '$lib/layout'; const CURRENT_CACHE_VERSION = 1; @@ -202,10 +202,10 @@ function checkData(data: WebsiteData): WebsiteData { const cards = data.cards.filter((v) => v.page === data.page); if (cards.length > 0) { - fixAllCollisions(cards); + fixAllCollisions(cards, false); fixAllCollisions(cards, true); - compactItems(cards); + compactItems(cards, false); compactItems(cards, true); }