your personal website on atproto - mirror blento.app
26
fork

Configure Feed

Select the types of activity you want to include in your feed.

commit

Florian 8e698c7d 515d15f0

+231 -234
+1
package.json
··· 81 81 "perfect-freehand": "^1.2.2", 82 82 "plyr": "^3.8.4", 83 83 "qr-code-styling": "^1.8.6", 84 + "react-grid-layout": "^2.2.2", 84 85 "simple-icons": "^16.6.0", 85 86 "svelte-sonner": "^1.0.7", 86 87 "tailwind-merge": "^3.4.0",
+110
pnpm-lock.yaml
··· 134 134 qr-code-styling: 135 135 specifier: ^1.8.6 136 136 version: 1.9.2 137 + react-grid-layout: 138 + specifier: ^2.2.2 139 + version: 2.2.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) 137 140 simple-icons: 138 141 specifier: ^16.6.0 139 142 version: 16.6.0 ··· 1967 1970 fast-deep-equal@3.1.3: 1968 1971 resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 1969 1972 1973 + fast-equals@4.0.3: 1974 + resolution: {integrity: sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==, tarball: https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz} 1975 + 1970 1976 fast-json-stable-stringify@2.1.0: 1971 1977 resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} 1972 1978 ··· 2103 2109 resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} 2104 2110 hasBin: true 2105 2111 2112 + js-tokens@4.0.0: 2113 + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==, tarball: https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz} 2114 + 2106 2115 js-yaml@4.1.1: 2107 2116 resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} 2108 2117 hasBin: true ··· 2239 2248 lodash.merge@4.6.2: 2240 2249 resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} 2241 2250 2251 + loose-envify@1.4.0: 2252 + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==, tarball: https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz} 2253 + hasBin: true 2254 + 2242 2255 lz-string@1.5.0: 2243 2256 resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} 2244 2257 hasBin: true ··· 2340 2353 number-flow@0.5.9: 2341 2354 resolution: {integrity: sha512-o3102c/4qRd6eV4n+rw6B/UP8+FosbhIxj4uA6GsjhryrGZRVtCtKIKEeBiOwUV52cUGJneeu0treELcV7U/lw==} 2342 2355 2356 + object-assign@4.1.1: 2357 + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==, tarball: https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz} 2358 + engines: {node: '>=0.10.0'} 2359 + 2343 2360 obug@2.1.1: 2344 2361 resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} 2345 2362 ··· 2526 2543 engines: {node: '>=14'} 2527 2544 hasBin: true 2528 2545 2546 + prop-types@15.8.1: 2547 + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==, tarball: https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz} 2548 + 2529 2549 prosemirror-changeset@2.3.1: 2530 2550 resolution: {integrity: sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==} 2531 2551 ··· 2608 2628 rangetouch@2.0.1: 2609 2629 resolution: {integrity: sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA==} 2610 2630 2631 + react-dom@19.2.4: 2632 + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==, tarball: https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz} 2633 + peerDependencies: 2634 + react: ^19.2.4 2635 + 2636 + react-draggable@4.5.0: 2637 + resolution: {integrity: sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==, tarball: https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz} 2638 + peerDependencies: 2639 + react: '>= 16.3.0' 2640 + react-dom: '>= 16.3.0' 2641 + 2642 + react-grid-layout@2.2.2: 2643 + resolution: {integrity: sha512-yNo9pxQWoxHWRAwHGSVT4DEGELYPyQ7+q9lFclb5jcqeFzva63/2F72CryS/jiTIr/SBIlTaDdyjqH+ODg8oBw==, tarball: https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-2.2.2.tgz} 2644 + peerDependencies: 2645 + react: '>= 16.3.0' 2646 + react-dom: '>= 16.3.0' 2647 + 2648 + react-is@16.13.1: 2649 + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==, tarball: https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz} 2650 + 2651 + react-resizable@3.1.3: 2652 + resolution: {integrity: sha512-liJBNayhX7qA4tBJiBD321FDhJxgGTJ07uzH5zSORXoE8h7PyEZ8mLqmosST7ppf6C4zUsbd2gzDMmBCfFp9Lw==, tarball: https://registry.npmjs.org/react-resizable/-/react-resizable-3.1.3.tgz} 2653 + peerDependencies: 2654 + react: '>= 16.3' 2655 + react-dom: '>= 16.3' 2656 + 2657 + react@19.2.4: 2658 + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==, tarball: https://registry.npmjs.org/react/-/react-19.2.4.tgz} 2659 + engines: {node: '>=0.10.0'} 2660 + 2611 2661 readdirp@4.1.2: 2612 2662 resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} 2613 2663 engines: {node: '>= 14.18.0'} ··· 2619 2669 require-from-string@2.0.2: 2620 2670 resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} 2621 2671 engines: {node: '>=0.10.0'} 2672 + 2673 + resize-observer-polyfill@1.5.1: 2674 + resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==, tarball: https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz} 2622 2675 2623 2676 resolve-from@4.0.0: 2624 2677 resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} ··· 2676 2729 resolution: {integrity: sha512-abovcqmwl97WKioxpkfuMeZmndB1TuDFY/R+FymrZyiGP+pMYomvgSzVPnbNMWHHESOPosVHGL352oFbdAnJcA==} 2677 2730 engines: {node: '>=16'} 2678 2731 2732 + scheduler@0.27.0: 2733 + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==, tarball: https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz} 2734 + 2679 2735 semver@7.7.3: 2680 2736 resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} 2681 2737 engines: {node: '>=10'} ··· 4731 4787 4732 4788 fast-deep-equal@3.1.3: {} 4733 4789 4790 + fast-equals@4.0.3: {} 4791 + 4734 4792 fast-json-stable-stringify@2.1.0: {} 4735 4793 4736 4794 fast-levenshtein@2.0.6: {} ··· 4836 4894 iso-datestring-validator@2.2.2: {} 4837 4895 4838 4896 jiti@2.6.1: {} 4897 + 4898 + js-tokens@4.0.0: {} 4839 4899 4840 4900 js-yaml@4.1.1: 4841 4901 dependencies: ··· 4942 5002 4943 5003 lodash.merge@4.6.2: {} 4944 5004 5005 + loose-envify@1.4.0: 5006 + dependencies: 5007 + js-tokens: 4.0.0 5008 + 4945 5009 lz-string@1.5.0: {} 4946 5010 4947 5011 maath@0.10.8(@types/three@0.176.0)(three@0.176.0): ··· 5066 5130 number-flow@0.5.9: 5067 5131 dependencies: 5068 5132 esm-env: 1.2.2 5133 + 5134 + object-assign@4.1.1: {} 5069 5135 5070 5136 obug@2.1.1: {} 5071 5137 ··· 5200 5266 5201 5267 prettier@3.8.1: {} 5202 5268 5269 + prop-types@15.8.1: 5270 + dependencies: 5271 + loose-envify: 1.4.0 5272 + object-assign: 4.1.1 5273 + react-is: 16.13.1 5274 + 5203 5275 prosemirror-changeset@2.3.1: 5204 5276 dependencies: 5205 5277 prosemirror-transform: 1.11.0 ··· 5319 5391 5320 5392 rangetouch@2.0.1: {} 5321 5393 5394 + react-dom@19.2.4(react@19.2.4): 5395 + dependencies: 5396 + react: 19.2.4 5397 + scheduler: 0.27.0 5398 + 5399 + react-draggable@4.5.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): 5400 + dependencies: 5401 + clsx: 2.1.1 5402 + prop-types: 15.8.1 5403 + react: 19.2.4 5404 + react-dom: 19.2.4(react@19.2.4) 5405 + 5406 + react-grid-layout@2.2.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4): 5407 + dependencies: 5408 + clsx: 2.1.1 5409 + fast-equals: 4.0.3 5410 + prop-types: 15.8.1 5411 + react: 19.2.4 5412 + react-dom: 19.2.4(react@19.2.4) 5413 + react-draggable: 4.5.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) 5414 + react-resizable: 3.1.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) 5415 + resize-observer-polyfill: 1.5.1 5416 + 5417 + react-is@16.13.1: {} 5418 + 5419 + react-resizable@3.1.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4): 5420 + dependencies: 5421 + prop-types: 15.8.1 5422 + react: 19.2.4 5423 + react-dom: 19.2.4(react@19.2.4) 5424 + react-draggable: 4.5.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) 5425 + 5426 + react@19.2.4: {} 5427 + 5322 5428 readdirp@4.1.2: {} 5323 5429 5324 5430 regexparam@3.0.0: {} 5325 5431 5326 5432 require-from-string@2.0.2: {} 5433 + 5434 + resize-observer-polyfill@1.5.1: {} 5327 5435 5328 5436 resolve-from@4.0.0: {} 5329 5437 ··· 5412 5520 parse-css-color: 0.2.1 5413 5521 postcss-value-parser: 4.2.0 5414 5522 yoga-wasm-web: 0.3.3 5523 + 5524 + scheduler@0.27.0: {} 5415 5525 5416 5526 semver@7.7.3: {} 5417 5527
-1
src/lib/cards/_base/BaseCard/BaseEditingCard.svelte
··· 15 15 getSelectCard 16 16 } from '$lib/website/context'; 17 17 import PlainTextEditor from '$lib/components/PlainTextEditor.svelte'; 18 - import { fixAllCollisions, fixCollisions } from '$lib/helper'; 19 18 20 19 let colorsChoices = [ 21 20 { class: 'text-base-500', label: 'base' },
+2 -223
src/lib/helper.ts
··· 3 3 import { CardDefinitionsByType } from './cards'; 4 4 import { deleteRecord, getCDNImageBlobUrl, putRecord, uploadBlob } from '$lib/atproto'; 5 5 import * as TID from '@atcute/tid'; 6 + import { overlaps } from './layout'; 6 7 7 8 export function clamp(value: number, min: number, max: number): number { 8 9 return Math.min(Math.max(value, min), max); ··· 28 29 'bg-rose-500' 29 30 ]; 30 31 31 - export const overlaps = (a: Item, b: Item, mobile: boolean = false) => { 32 - if (a === b) return false; 33 - if (mobile) { 34 - return ( 35 - a.mobileX < b.mobileX + b.mobileW && 36 - a.mobileX + a.mobileW > b.mobileX && 37 - a.mobileY < b.mobileY + b.mobileH && 38 - a.mobileY + a.mobileH > b.mobileY 39 - ); 40 - } 41 - 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; 42 - }; 43 - 44 - export function fixCollisions( 45 - items: Item[], 46 - movedItem: Item, 47 - mobile: boolean = false, 48 - skipCompact: boolean = false 49 - ) { 50 - const clampX = (item: Item) => { 51 - if (mobile) item.mobileX = clamp(item.mobileX, 0, COLUMNS - item.mobileW); 52 - else item.x = clamp(item.x, 0, COLUMNS - item.w); 53 - }; 54 - 55 - // Push `target` down until it no longer overlaps with any item (including movedItem), 56 - // while keeping target.x fixed. Any item we collide with gets pushed down first (cascade). 57 - const pushDownCascade = (target: Item, blocker: Item) => { 58 - // Keep x fixed always when pushing down 59 - const fixedX = mobile ? target.mobileX : target.x; 60 - const prevY = mobile ? target.mobileY : target.y; 61 - 62 - // We need target to move just below `blocker` 63 - const desiredY = mobile ? blocker.mobileY + blocker.mobileH : blocker.y + blocker.h; 64 - if (!mobile && target.y < desiredY) target.y = desiredY; 65 - if (mobile && target.mobileY < desiredY) target.mobileY = desiredY; 66 - 67 - const newY = mobile ? target.mobileY : target.y; 68 - const targetH = mobile ? target.mobileH : target.h; 69 - 70 - // fall trough fix 71 - if (newY > prevY) { 72 - const prevBottom = prevY + targetH; 73 - const newBottom = newY + targetH; 74 - for (const it of items) { 75 - if (it === target || it === movedItem || it === blocker) continue; 76 - const itY = mobile ? it.mobileY : it.y; 77 - const itH = mobile ? it.mobileH : it.h; 78 - const itBottom = itY + itH; 79 - if (itBottom <= prevBottom || itY >= newBottom) continue; 80 - // horizontal overlap check 81 - const hOverlap = mobile 82 - ? target.mobileX < it.mobileX + it.mobileW && target.mobileX + target.mobileW > it.mobileX 83 - : target.x < it.x + it.w && target.x + target.w > it.x; 84 - if (hOverlap) { 85 - pushDownCascade(it, target); 86 - } 87 - } 88 - } 89 - 90 - // Now resolve any collisions that creates by pushing those items down first 91 - // Repeat until target is clean. 92 - while (true) { 93 - const hit = items.find((it) => it !== target && overlaps(target, it, mobile)); 94 - if (!hit) break; 95 - 96 - // push the hit item down first (cascade), keeping its x fixed 97 - pushDownCascade(hit, target); 98 - 99 - // after moving the hit item, target.x must remain fixed 100 - if (mobile) target.mobileX = fixedX; 101 - else target.x = fixedX; 102 - } 103 - }; 104 - 105 - // Ensure moved item is in bounds 106 - clampX(movedItem); 107 - 108 - // Find all items colliding with movedItem, and push them down in a stable order: 109 - // top-to-bottom so you get the nice chain reaction (0,0 -> 0,1 -> 0,2). 110 - const colliders = items 111 - .filter((it) => it !== movedItem && overlaps(movedItem, it, mobile)) 112 - .toSorted((a, b) => 113 - mobile ? a.mobileY - b.mobileY || a.mobileX - b.mobileX : a.y - b.y || a.x - b.x 114 - ); 115 - 116 - for (const it of colliders) { 117 - // keep x clamped, but do NOT change x during push (we rely on fixed x) 118 - clampX(it); 119 - 120 - // push it down just below movedItem; cascade handles the rest 121 - pushDownCascade(it, movedItem); 122 - 123 - // enforce "x stays the same" during pushing (clamp already applied) 124 - if (mobile) it.mobileX = clamp(it.mobileX, 0, COLUMNS - it.mobileW); 125 - else it.x = clamp(it.x, 0, COLUMNS - it.w); 126 - } 127 - 128 - if (!skipCompact) { 129 - compactItems(items, mobile); 130 - } 131 - } 132 - 133 - // Fix all collisions between items (not just one moved item) 134 - // Items higher on the page have priority and stay in place 135 - export function fixAllCollisions(items: Item[], mobile: boolean = false) { 136 - // Sort by Y position (top-to-bottom, then left-to-right) 137 - // Items at the top have priority and won't be moved 138 - const sortedItems = items.toSorted((a, b) => 139 - mobile ? a.mobileY - b.mobileY || a.mobileX - b.mobileX : a.y - b.y || a.x - b.x 140 - ); 141 - 142 - // Process each item and push it down if it overlaps with any item above it 143 - for (let i = 0; i < sortedItems.length; i++) { 144 - const item = sortedItems[i]; 145 - 146 - // Clamp X to valid range 147 - if (mobile) { 148 - item.mobileX = clamp(item.mobileX, 0, COLUMNS - item.mobileW); 149 - } else { 150 - item.x = clamp(item.x, 0, COLUMNS - item.w); 151 - } 152 - 153 - // Check for collisions with all items that come before (higher priority) 154 - let hasCollision = true; 155 - while (hasCollision) { 156 - hasCollision = false; 157 - for (let j = 0; j < i; j++) { 158 - const other = sortedItems[j]; 159 - if (overlaps(item, other, mobile)) { 160 - // Push item down below the colliding item 161 - if (mobile) { 162 - item.mobileY = other.mobileY + other.mobileH; 163 - } else { 164 - item.y = other.y + other.h; 165 - } 166 - hasCollision = true; 167 - break; // Restart collision check from the beginning 168 - } 169 - } 170 - } 171 - } 172 - 173 - compactItems(items, mobile); 174 - } 175 - 176 - // Move all items up as far as possible without collisions 177 - export function compactItems(items: Item[], mobile: boolean = false) { 178 - // Sort by Y position (top-to-bottom) so upper items settle first. 179 - const sortedItems = items.toSorted((a, b) => 180 - mobile ? a.mobileY - b.mobileY || a.mobileX - b.mobileX : a.y - b.y || a.x - b.x 181 - ); 182 - 183 - // For each item, find the lowest Y it can occupy by checking the bottom edges 184 - // of all horizontally-overlapping items already placed above it. 185 - const settled: Item[] = []; 186 - 187 - for (const item of sortedItems) { 188 - const itemX = mobile ? item.mobileX : item.x; 189 - const itemW = mobile ? item.mobileW : item.w; 190 - 191 - let minY = 0; 192 - 193 - for (const other of settled) { 194 - const otherX = mobile ? other.mobileX : other.x; 195 - const otherW = mobile ? other.mobileW : other.w; 196 - 197 - // Check horizontal overlap 198 - if (itemX < otherX + otherW && itemX + itemW > otherX) { 199 - const otherBottom = mobile ? other.mobileY + other.mobileH : other.y + other.h; 200 - if (otherBottom > minY) { 201 - minY = otherBottom; 202 - } 203 - } 204 - } 205 - 206 - if (mobile) { 207 - item.mobileY = minY; 208 - } else { 209 - item.y = minY; 210 - } 211 - 212 - settled.push(item); 213 - } 214 - } 215 - 216 - // Simulate where an item would end up after fixCollisions + compaction 217 - export function simulateFinalPosition( 218 - items: Item[], 219 - movedItem: Item, 220 - newX: number, 221 - newY: number, 222 - mobile: boolean = false 223 - ): { x: number; y: number } { 224 - // Deep clone positions for simulation 225 - const clonedItems: Item[] = items.map((item) => ({ 226 - ...item, 227 - x: item.x, 228 - y: item.y, 229 - mobileX: item.mobileX, 230 - mobileY: item.mobileY 231 - })); 232 - 233 - const clonedMovedItem = clonedItems.find((item) => item.id === movedItem.id); 234 - if (!clonedMovedItem) return { x: newX, y: newY }; 235 - 236 - // Set the new position 237 - if (mobile) { 238 - clonedMovedItem.mobileX = newX; 239 - clonedMovedItem.mobileY = newY; 240 - } else { 241 - clonedMovedItem.x = newX; 242 - clonedMovedItem.y = newY; 243 - } 244 - 245 - // Run fixCollisions on the cloned data 246 - fixCollisions(clonedItems, clonedMovedItem, mobile); 247 - 248 - // Return the final position of the moved item 249 - return mobile 250 - ? { x: clonedMovedItem.mobileX, y: clonedMovedItem.mobileY } 251 - : { x: clonedMovedItem.x, y: clonedMovedItem.y }; 252 - } 253 32 254 33 export function sortItems(a: Item, b: Item) { 255 34 return a.y * COLUMNS + a.x - b.y * COLUMNS - b.x; ··· 353 132 let foundPosition = false; 354 133 while (!foundPosition) { 355 134 for (newItem.x = 0; newItem.x <= COLUMNS - newItem.w; newItem.x++) { 356 - const collision = items.find((item) => overlaps(newItem, item)); 135 + const collision = items.find((item) => overlaps(newItem, item, false)); 357 136 if (!collision) { 358 137 foundPosition = true; 359 138 break;
+110
src/lib/layout.ts
··· 1 + import { type LayoutItem, type Layout } from 'react-grid-layout/core'; 2 + import { 3 + collides, 4 + moveElement, 5 + correctBounds, 6 + getFirstCollision, 7 + verticalCompactor 8 + } from 'react-grid-layout/core'; 9 + import type { Item } from './types'; 10 + import { COLUMNS } from '$lib'; 11 + import { clamp } from './helper'; 12 + 13 + function toLayoutItem(item: Item, mobile: boolean): LayoutItem { 14 + if (mobile) { 15 + return { 16 + x: item.mobileX, 17 + y: item.mobileY, 18 + w: item.mobileW, 19 + h: item.mobileH, 20 + i: item.id 21 + }; 22 + } 23 + return { 24 + x: item.x, 25 + y: item.y, 26 + w: item.w, 27 + h: item.h, 28 + i: item.id 29 + }; 30 + } 31 + 32 + function toLayout(items: Item[], mobile: boolean): LayoutItem[] { 33 + return items.map((i) => toLayoutItem(i, mobile)); 34 + } 35 + 36 + function applyLayout(items: Item[], layout: LayoutItem[], mobile: boolean): void { 37 + const itemsMap: Map<string, Item> = new Map(); 38 + 39 + for (const item of items) { 40 + itemsMap.set(item.id, item); 41 + } 42 + for (const l of layout) { 43 + const item = itemsMap.get(l.i); 44 + 45 + if (!item) { 46 + console.error('item not found in layout!! this should never happen!'); 47 + continue; 48 + } 49 + 50 + if (mobile) { 51 + item.mobileX = l.x; 52 + item.mobileY = l.y; 53 + } else { 54 + item.x = l.x; 55 + item.y = l.y; 56 + } 57 + } 58 + } 59 + 60 + export function overlaps(a: Item, b: Item, mobile: boolean) { 61 + if (a === b) return false; 62 + return collides(toLayoutItem(a, mobile), toLayoutItem(b, mobile)); 63 + } 64 + 65 + export function fixCollisions( 66 + items: Item[], 67 + item: Item, 68 + mobile: boolean = false, 69 + skipCompact: boolean = false 70 + ) { 71 + if (mobile) item.mobileX = clamp(item.mobileX, 0, COLUMNS - item.mobileW); 72 + else item.x = clamp(item.x, 0, COLUMNS - item.w); 73 + 74 + let layout = toLayout(items, mobile); 75 + 76 + const movedLayoutItem = layout.find((i) => i.i === item.id); 77 + 78 + if (!movedLayoutItem) { 79 + console.error('item not found in layout! this should never happen!'); 80 + return; 81 + } 82 + 83 + layout = moveElement( 84 + layout, 85 + movedLayoutItem, 86 + movedLayoutItem.x, 87 + movedLayoutItem.y, 88 + true, 89 + false, 90 + 'vertical', 91 + COLUMNS 92 + ); 93 + 94 + if (!skipCompact) layout = verticalCompactor.compact(layout, COLUMNS) as LayoutItem[]; 95 + 96 + applyLayout(items, layout, mobile); 97 + } 98 + 99 + export function fixAllCollisions(items: Item[], mobile: boolean) { 100 + let layout = toLayout(items, mobile); 101 + correctBounds(layout as any, { cols: COLUMNS }); 102 + layout = verticalCompactor.compact(layout, COLUMNS) as LayoutItem[]; 103 + applyLayout(items, layout, mobile); 104 + } 105 + 106 + export function compactItems(items: Item[], mobile: boolean) { 107 + const layout = toLayout(items, mobile); 108 + const compacted = verticalCompactor.compact(layout, COLUMNS) as LayoutItem[]; 109 + applyLayout(items, compacted, mobile); 110 + }
+3 -6
src/lib/website/EditableWebsite.svelte
··· 4 4 import { 5 5 checkAndUploadImage, 6 6 clamp, 7 - compactItems, 8 7 createEmptyCard, 9 - findValidPosition, 10 - fixAllCollisions, 11 - fixCollisions, 12 8 getHideProfileSection, 13 9 getProfilePosition, 14 10 getName, ··· 43 39 import CardCommand from '$lib/components/card-command/CardCommand.svelte'; 44 40 import { shouldMirror, mirrorLayout } from './layout-mirror'; 45 41 import { SvelteMap } from 'svelte/reactivity'; 42 + import { fixCollisions, compactItems, fixAllCollisions } from '$lib/layout'; 46 43 47 44 let { 48 45 data ··· 546 543 return; 547 544 } 548 545 549 - fixAllCollisions(copiedCards); 546 + fixAllCollisions(copiedCards, false); 550 547 fixAllCollisions(copiedCards, true); 551 - compactItems(copiedCards); 548 + compactItems(copiedCards, false); 552 549 compactItems(copiedCards, true); 553 550 554 551 items = copiedCards;
+2 -1
src/lib/website/layout-mirror.ts
··· 1 1 import { COLUMNS } from '$lib'; 2 2 import { CardDefinitionsByType } from '$lib/cards'; 3 - import { clamp, findValidPosition, fixAllCollisions } from '$lib/helper'; 3 + import { clamp, findValidPosition } from '$lib/helper'; 4 + import { fixAllCollisions } from '$lib/layout'; 4 5 import type { Item } from '$lib/types'; 5 6 6 7 /**
+3 -3
src/lib/website/load.ts
··· 1 1 import { getDetailedProfile, listRecords, resolveHandle, parseUri, getRecord } from '$lib/atproto'; 2 2 import { CardDefinitionsByType } from '$lib/cards'; 3 3 import type { Item, UserCache, WebsiteData } from '$lib/types'; 4 - import { compactItems, fixAllCollisions } from '$lib/helper'; 5 4 import { error } from '@sveltejs/kit'; 6 5 import type { ActorIdentifier, Did } from '@atcute/lexicons'; 7 6 8 7 import { isDid, isHandle } from '@atcute/lexicons/syntax'; 8 + import { fixAllCollisions, compactItems } from '$lib/layout'; 9 9 10 10 const CURRENT_CACHE_VERSION = 1; 11 11 ··· 202 202 const cards = data.cards.filter((v) => v.page === data.page); 203 203 204 204 if (cards.length > 0) { 205 - fixAllCollisions(cards); 205 + fixAllCollisions(cards, false); 206 206 fixAllCollisions(cards, true); 207 207 208 - compactItems(cards); 208 + compactItems(cards, false); 209 209 compactItems(cards, true); 210 210 } 211 211