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.
This commit is contained in:
2026-06-14 20:18:00 +02:00
parent 520a062049
commit d041c66dfb
7 changed files with 40 additions and 4 deletions
+25 -3
View File
@@ -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;
+2
View File
@@ -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)
+3
View File
@@ -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
+4
View File
@@ -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}"?',
+1
View File
@@ -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 }
}
];