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:
+1
-1
@@ -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.
|
- **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.
|
- **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.
|
- **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
|
### Changed
|
||||||
- The bookmark- and board-delete paths no longer remove entries immediately; deletions now route through the trash.
|
- The bookmark- and board-delete paths no longer remove entries immediately; deletions now route through the trash.
|
||||||
|
|||||||
@@ -576,6 +576,10 @@ html, body {
|
|||||||
}
|
}
|
||||||
.board-drag-handle:hover { color: var(--accent); }
|
.board-drag-handle:hover { color: var(--accent); }
|
||||||
.board-drag-handle:active { cursor: grabbing; }
|
.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 {
|
.board-title {
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
|
|||||||
+25
-3
@@ -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 ----
|
// ---- POS-MIGRATION ----
|
||||||
// Boards ohne pos (Altbestand vor v2.3) aus einem Auto-Raster befuellen,
|
// 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
|
// damit sie sich nicht alle auf (0,0) stapeln. Raster orientiert sich am
|
||||||
@@ -113,7 +120,7 @@ function renderBoards() {
|
|||||||
|
|
||||||
function createBoardEl(board) {
|
function createBoardEl(board) {
|
||||||
const div = document.createElement('div');
|
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;
|
div.dataset.boardId = board.id;
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
@@ -133,6 +140,11 @@ function createBoardEl(board) {
|
|||||||
const actions = document.createElement('div');
|
const actions = document.createElement('div');
|
||||||
actions.className = 'board-actions';
|
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');
|
const btnBlur = document.createElement('button');
|
||||||
btnBlur.className = 'board-action-btn btn-blur-board';
|
btnBlur.className = 'board-action-btn btn-blur-board';
|
||||||
btnBlur.title = board.blurred ? t('boards.unblur') : t('boards.blur');
|
btnBlur.title = board.blurred ? t('boards.unblur') : t('boards.blur');
|
||||||
@@ -152,9 +164,9 @@ function createBoardEl(board) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (btnDelete) {
|
if (btnDelete) {
|
||||||
actions.append(btnBlur, btnRename, btnDelete);
|
actions.append(btnLock, btnBlur, btnRename, btnDelete);
|
||||||
} else {
|
} else {
|
||||||
actions.append(btnBlur, btnRename);
|
actions.append(btnLock, btnBlur, btnRename);
|
||||||
}
|
}
|
||||||
header.append(dragHandle, titleSpanHeader, actions);
|
header.append(dragHandle, titleSpanHeader, actions);
|
||||||
|
|
||||||
@@ -163,6 +175,16 @@ function createBoardEl(board) {
|
|||||||
blurOverlay.className = 'board-blur-overlay';
|
blurOverlay.className = 'board-blur-overlay';
|
||||||
div.appendChild(blurOverlay);
|
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 => {
|
btnBlur.addEventListener('click', async e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
board.blurred = !board.blurred;
|
board.blurred = !board.blurred;
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ function initDataButtons() {
|
|||||||
id: b.id || uid(),
|
id: b.id || uid(),
|
||||||
title: String(b.title).slice(0, 100),
|
title: String(b.title).slice(0, 100),
|
||||||
blurred: !!b.blurred,
|
blurred: !!b.blurred,
|
||||||
|
locked: !!b.locked,
|
||||||
bookmarks: b.bookmarks
|
bookmarks: b.bookmarks
|
||||||
.filter(bm => bm && typeof bm.title === 'string' && isSafeUrl(bm.url))
|
.filter(bm => bm && typeof bm.title === 'string' && isSafeUrl(bm.url))
|
||||||
.map(bm => ({
|
.map(bm => ({
|
||||||
@@ -109,6 +110,7 @@ function initDataButtons() {
|
|||||||
id: e.item.id || uid(),
|
id: e.item.id || uid(),
|
||||||
title: String(e.item.title || '').slice(0, 100),
|
title: String(e.item.title || '').slice(0, 100),
|
||||||
blurred: !!e.item.blurred,
|
blurred: !!e.item.blurred,
|
||||||
|
locked: !!e.item.locked,
|
||||||
// Position erhalten, damit ein wiederhergestelltes Board an seinem Platz landet.
|
// Position erhalten, damit ein wiederhergestelltes Board an seinem Platz landet.
|
||||||
...(safePos(e.item.pos) ? { pos: safePos(e.item.pos) } : {}),
|
...(safePos(e.item.pos) ? { pos: safePos(e.item.pos) } : {}),
|
||||||
bookmarks: Array.isArray(e.item.bookmarks)
|
bookmarks: Array.isArray(e.item.bookmarks)
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ function initBoardDragDrop() {
|
|||||||
handle.addEventListener('pointerdown', function onDown(e) {
|
handle.addEventListener('pointerdown', function onDown(e) {
|
||||||
// Auf Mobil ist .board position:static (Stapel) -> kein Free-Move.
|
// Auf Mobil ist .board position:static (Stapel) -> kein Free-Move.
|
||||||
if (getComputedStyle(boardEl).position !== 'absolute') return;
|
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();
|
e.preventDefault();
|
||||||
handle.setPointerCapture(e.pointerId);
|
handle.setPointerCapture(e.pointerId);
|
||||||
// .board.dragging hebt das Board per CSS nach vorne (z-index) UND ist das Signal fuer den
|
// .board.dragging hebt das Board per CSS nach vorne (z-index) UND ist das Signal fuer den
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ const STRINGS = {
|
|||||||
'boards.drag_title': 'Board verschieben',
|
'boards.drag_title': 'Board verschieben',
|
||||||
'boards.blur': 'Blur (privat)',
|
'boards.blur': 'Blur (privat)',
|
||||||
'boards.unblur': 'Unblur',
|
'boards.unblur': 'Unblur',
|
||||||
|
'boards.lock': 'Position sperren',
|
||||||
|
'boards.unlock': 'Position entsperren',
|
||||||
'boards.rename': 'Umbenennen',
|
'boards.rename': 'Umbenennen',
|
||||||
'boards.delete': 'Löschen',
|
'boards.delete': 'Löschen',
|
||||||
'boards.delete_confirm': 'Board „{title}" wirklich löschen?',
|
'boards.delete_confirm': 'Board „{title}" wirklich löschen?',
|
||||||
@@ -480,6 +482,8 @@ const STRINGS = {
|
|||||||
'boards.drag_title': 'Move board',
|
'boards.drag_title': 'Move board',
|
||||||
'boards.blur': 'Blur (private)',
|
'boards.blur': 'Blur (private)',
|
||||||
'boards.unblur': 'Unblur',
|
'boards.unblur': 'Unblur',
|
||||||
|
'boards.lock': 'Lock position',
|
||||||
|
'boards.unlock': 'Unlock position',
|
||||||
'boards.rename': 'Rename',
|
'boards.rename': 'Rename',
|
||||||
'boards.delete': 'Delete',
|
'boards.delete': 'Delete',
|
||||||
'boards.delete_confirm': 'Really delete board "{title}"?',
|
'boards.delete_confirm': 'Really delete board "{title}"?',
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ function getDefaultBoards() {
|
|||||||
{ id: uid(), title: 'Next.js Docs', url: 'https://nextjs.org/docs', desc: '' },
|
{ id: uid(), title: 'Next.js Docs', url: 'https://nextjs.org/docs', desc: '' },
|
||||||
],
|
],
|
||||||
blurred: false,
|
blurred: false,
|
||||||
|
locked: false,
|
||||||
pos: { x: 40, y: 110 }
|
pos: { x: 40, y: 110 }
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user