From d041c66dfb687fd908ce7779c9802830a74684a1 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Sun, 14 Jun 2026 20:18:00 +0200 Subject: [PATCH] feat(layout): Board-Position per Lock-Button fixieren Neuer Pin-Button (custom SVG, kein Emoji) im Board-Header sperrt die Position eines Boards. Bei gesperrtem Board (.board.locked): - der Drag-Handle wird per CSS ausgeblendet (Flos Wunsch: Handle weg statt nur inaktiv), - ein zweiter Guard in drag.js onDown verweigert zusaetzlich jeden Drag. Schuetzt vor versehentlichem Verschieben (ergaenzt den 3px-Bewegungs-Schwellwert). locked wird wie blurred persistiert, im Export/Import durchgereicht und mit ins Trash-Board geklont. i18n DE/EN ergaenzt. --- CHANGELOG.md | 2 +- src/css/main.css | 4 ++++ src/js/boards.js | 28 +++++++++++++++++++++++++--- src/js/data.js | 2 ++ src/js/drag.js | 3 +++ src/js/i18n.js | 4 ++++ src/js/state.js | 1 + 7 files changed, 40 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df61030..c21355d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ All notable changes per version. Format based on [Keep a Changelog](https://keep - **Command Palette (Ctrl+K)** — Overlay that live-filters bookmarks (title and URL) and board names from the keyboard. Arrow-key navigation, Enter opens the match, Escape closes. Read-only navigation, separate from the web search bar. Combobox/listbox ARIA pattern with focus trap and focus return. New DE/EN i18n strings. - **Trash** — Deleted bookmarks and boards move to a 30-day trash instead of vanishing. Restore or permanently remove them from a new Settings section; entries older than 30 days are cleaned up automatically. Stored under its own storage key with a hard size cap so it cannot exhaust the storage quota. - **Quick Save** — A global keyboard shortcut (default Alt+Shift+S, configurable in the browser shortcut settings) saves the current tab into a fixed Inbox board from any page. Backed by a background worker (service worker on Chrome/Opera, event page on Firefox) that appends to a dedicated pending queue, which the dashboard drains into the Inbox — separate write domains, so a save can never clobber the boards. A badge confirms the save, and open dashboard tabs sync the new bookmark live via a storage-change listener. -- **Free layout (bonus)** — Boards can be dragged to free positions via a drag handle, persisted per board. Positions are clamped back into view when the window shrinks, and the layout falls back to a stacked column on small screens. +- **Free layout (bonus)** — Boards can be dragged to free positions via a drag handle, persisted per board. Positions are clamped back into view when the window shrinks, and the layout falls back to a stacked column on small screens. Each board can be pinned with a lock button: a locked board cannot be moved (its drag handle is hidden), preventing accidental repositioning. A drag only counts past a small movement threshold, so a mere click on the handle never shifts a board. ### Changed - The bookmark- and board-delete paths no longer remove entries immediately; deletions now route through the trash. diff --git a/src/css/main.css b/src/css/main.css index 3c508e2..ccb0bab 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -576,6 +576,10 @@ html, body { } .board-drag-handle:hover { color: var(--accent); } .board-drag-handle:active { cursor: grabbing; } +/* Gesperrtes Board (Position fixiert): Drag-Handle ausblenden (kein Verschieben mehr), + Lock-Button aktiv einfaerben. Hoehere Spezifitaet als .board-drag-handle -> display:none gewinnt. */ +.board.locked .board-drag-handle { display: none; } +.board.locked .btn-lock-board { color: var(--accent); } .board-title { font-family: var(--font-display); diff --git a/src/js/boards.js b/src/js/boards.js index 2f2219f..e771f9e 100644 --- a/src/js/boards.js +++ b/src/js/boards.js @@ -46,6 +46,13 @@ function createPlusSvg() { ]); } +/** Erzeugt das Pin-/Reisszwecke-Icon SVG (Position fixieren) — bewusst KEIN Emoji (custom SVG). */ +function createPinSvg() { + return svgEl('svg', { width: '11', height: '12', viewBox: '0 0 24 24', fill: 'currentColor' }, [ + svgEl('path', { d: 'M16 9V4l1 0c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1 .45-1 1s.45 1 1 1l1 0v5c0 1.66-1.34 3-3 3v2h5.97v7l1 1 1-1v-7H19v-2c-1.66 0-3-1.34-3-3z' }), + ]); +} + // ---- POS-MIGRATION ---- // Boards ohne pos (Altbestand vor v2.3) aus einem Auto-Raster befuellen, // damit sie sich nicht alle auf (0,0) stapeln. Raster orientiert sich am @@ -113,7 +120,7 @@ function renderBoards() { function createBoardEl(board) { const div = document.createElement('div'); - div.className = 'board' + (board.blurred ? ' blurred' : ''); + div.className = 'board' + (board.blurred ? ' blurred' : '') + (board.locked ? ' locked' : ''); div.dataset.boardId = board.id; // Header @@ -133,6 +140,11 @@ function createBoardEl(board) { const actions = document.createElement('div'); actions.className = 'board-actions'; + const btnLock = document.createElement('button'); + btnLock.className = 'board-action-btn btn-lock-board'; + btnLock.title = board.locked ? t('boards.unlock') : t('boards.lock'); + btnLock.appendChild(createPinSvg()); + const btnBlur = document.createElement('button'); btnBlur.className = 'board-action-btn btn-blur-board'; btnBlur.title = board.blurred ? t('boards.unblur') : t('boards.blur'); @@ -152,9 +164,9 @@ function createBoardEl(board) { } if (btnDelete) { - actions.append(btnBlur, btnRename, btnDelete); + actions.append(btnLock, btnBlur, btnRename, btnDelete); } else { - actions.append(btnBlur, btnRename); + actions.append(btnLock, btnBlur, btnRename); } header.append(dragHandle, titleSpanHeader, actions); @@ -163,6 +175,16 @@ function createBoardEl(board) { blurOverlay.className = 'board-blur-overlay'; div.appendChild(blurOverlay); + btnLock.addEventListener('click', async e => { + e.stopPropagation(); + // Position fixieren: blendet via .board.locked den Drag-Handle aus (CSS) und der onDown-Guard + // in drag.js verweigert zusaetzlich den Drag. Reiner Klassen-Toggle, kein Re-Render noetig. + board.locked = !board.locked; + div.classList.toggle('locked', board.locked); + btnLock.title = board.locked ? t('boards.unlock') : t('boards.lock'); + await saveBoards(); + }); + btnBlur.addEventListener('click', async e => { e.stopPropagation(); board.blurred = !board.blurred; diff --git a/src/js/data.js b/src/js/data.js index 9e5d82a..d9ebe14 100644 --- a/src/js/data.js +++ b/src/js/data.js @@ -72,6 +72,7 @@ function initDataButtons() { id: b.id || uid(), title: String(b.title).slice(0, 100), blurred: !!b.blurred, + locked: !!b.locked, bookmarks: b.bookmarks .filter(bm => bm && typeof bm.title === 'string' && isSafeUrl(bm.url)) .map(bm => ({ @@ -109,6 +110,7 @@ function initDataButtons() { id: e.item.id || uid(), title: String(e.item.title || '').slice(0, 100), blurred: !!e.item.blurred, + locked: !!e.item.locked, // 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) diff --git a/src/js/drag.js b/src/js/drag.js index b826406..14695b1 100644 --- a/src/js/drag.js +++ b/src/js/drag.js @@ -21,6 +21,9 @@ function initBoardDragDrop() { handle.addEventListener('pointerdown', function onDown(e) { // Auf Mobil ist .board position:static (Stapel) -> kein Free-Move. if (getComputedStyle(boardEl).position !== 'absolute') return; + // Gesperrtes Board (Position fixiert, LAYOUT-LOCK) nicht verschieben. Der Drag-Handle ist + // bei .locked schon per CSS ausgeblendet; dieser Guard ist die zweite Sicherung. + if (boardEl.classList.contains('locked')) return; e.preventDefault(); handle.setPointerCapture(e.pointerId); // .board.dragging hebt das Board per CSS nach vorne (z-index) UND ist das Signal fuer den diff --git a/src/js/i18n.js b/src/js/i18n.js index cd79bda..f1c3c74 100644 --- a/src/js/i18n.js +++ b/src/js/i18n.js @@ -21,6 +21,8 @@ const STRINGS = { 'boards.drag_title': 'Board verschieben', 'boards.blur': 'Blur (privat)', 'boards.unblur': 'Unblur', + 'boards.lock': 'Position sperren', + 'boards.unlock': 'Position entsperren', 'boards.rename': 'Umbenennen', 'boards.delete': 'Löschen', 'boards.delete_confirm': 'Board „{title}" wirklich löschen?', @@ -480,6 +482,8 @@ const STRINGS = { 'boards.drag_title': 'Move board', 'boards.blur': 'Blur (private)', 'boards.unblur': 'Unblur', + 'boards.lock': 'Lock position', + 'boards.unlock': 'Unlock position', 'boards.rename': 'Rename', 'boards.delete': 'Delete', 'boards.delete_confirm': 'Really delete board "{title}"?', diff --git a/src/js/state.js b/src/js/state.js index 3fcaf0f..6148fd2 100644 --- a/src/js/state.js +++ b/src/js/state.js @@ -54,6 +54,7 @@ function getDefaultBoards() { { id: uid(), title: 'Next.js Docs', url: 'https://nextjs.org/docs', desc: '' }, ], blurred: false, + locked: false, pos: { x: 40, y: 110 } } ];