fix(layout): Phase-5-Review — off-screen-Clamp, Drag-Cleanup, Blur-Position, Import-pos

- Render + neuer debounced Resize-Handler clampen --board-x/y gegen den
  aktuellen Viewport: ein auf breiterem Fenster platziertes Board rendert
  nie mehr off-screen (und damit per Drag unerreichbar). board.pos bleibt
  unveraendert, bei spaeterer Verbreiterung wird die Originalposition erreicht.
- drag.js: cleanup() + pointercancel-Listener. Die Klasse .board.dragging
  klebte bei Touch-Interrupt/Browser-Geste sonst dauerhaft und legte den
  app.js-Sync-Guard (Quick-Save-Render) still.
- main.css: '.board.blurred { position: relative }' entfernt — lag im
  utilities-Layer und schlug das absolute Free-Layout (geblurrtes Board fiel
  aus seiner Position + war nicht mehr drag-bar).
- data.js: board.pos wird beim JSON-Import durchgereicht (safePos-Validierung
  via Number.isFinite), sonst Verlust des frei gesetzten Layouts beim Restore.
This commit is contained in:
2026-06-14 15:16:51 +02:00
parent 1d9e9dab81
commit 70f3f705b4
5 changed files with 76 additions and 23 deletions
+32 -13
View File
@@ -24,6 +24,17 @@ function initDataButtons() {
}
}
/**
* Validiert eine freie Layout-Position (LAYOUT-04). Liefert { x, y } nur bei
* endlichen Zahlen, sonst null — dann gridded ensureBoardPositions das Board neu.
* Ohne das wuerde ein Import jede vom Nutzer gesetzte Board-Position verwerfen.
* @param {*} pos
* @returns {{x:number,y:number}|null}
*/
function safePos(pos) {
return pos && Number.isFinite(pos.x) && Number.isFinite(pos.y) ? { x: pos.x, y: pos.y } : null;
}
// Export (inkl. Notes)
btnExport.addEventListener('click', async () => {
const widgetData = await Store.get('widgetStates');
@@ -56,19 +67,25 @@ function initDataButtons() {
if (!Array.isArray(data.boards)) throw new Error(t('data.invalid_format'));
const validBoards = data.boards
.filter(b => b && typeof b.title === 'string' && Array.isArray(b.bookmarks))
.map(b => ({
id: b.id || uid(),
title: String(b.title).slice(0, 100),
blurred: !!b.blurred,
bookmarks: b.bookmarks
.filter(bm => bm && typeof bm.title === 'string' && isSafeUrl(bm.url))
.map(bm => ({
id: bm.id || uid(),
title: String(bm.title).slice(0, 200),
url: bm.url,
desc: String(bm.desc || '').slice(0, 500)
}))
}));
.map(b => {
const board = {
id: b.id || uid(),
title: String(b.title).slice(0, 100),
blurred: !!b.blurred,
bookmarks: b.bookmarks
.filter(bm => bm && typeof bm.title === 'string' && isSafeUrl(bm.url))
.map(bm => ({
id: bm.id || uid(),
title: String(bm.title).slice(0, 200),
url: bm.url,
desc: String(bm.desc || '').slice(0, 500)
}))
};
// Freies Layout (LAYOUT-04): valide Position uebernehmen, sonst gridded ensureBoardPositions neu.
const pos = safePos(b.pos);
if (pos) board.pos = pos;
return board;
});
if (validBoards.length === 0) throw new Error(t('data.no_boards'));
const ok = await HellionDialog.confirm(
t('data.import_confirm', { count: validBoards.length }),
@@ -92,6 +109,8 @@ function initDataButtons() {
id: e.item.id || uid(),
title: String(e.item.title || '').slice(0, 100),
blurred: !!e.item.blurred,
// Position erhalten, damit ein wiederhergestelltes Board an seinem Platz landet.
...(safePos(e.item.pos) ? { pos: safePos(e.item.pos) } : {}),
bookmarks: Array.isArray(e.item.bookmarks)
? e.item.bookmarks
.filter(bm => bm && typeof bm.title === 'string' && isSafeUrl(bm.url))