Initial release v1.2.0 — Hellion NewTab Browser Extension

Persoenlicher Bookmark-Dashboard als Browser-Extension.
8 Themes, Drag & Drop, Sticky Notes, JSON Export/Import.
Chrome, Edge, Brave, Opera, Vivaldi (MV3) + Firefox (MV2).

Includes GitHub Actions for security scanning, code quality
validation, and automated release packaging.
This commit is contained in:
2026-03-20 22:48:21 +01:00
commit 87c30b24d0
30 changed files with 2835 additions and 0 deletions
+276
View File
@@ -0,0 +1,276 @@
/* =============================================
HELLION NEWTAB — boards.js
Board & Bookmark Rendering, Modals
============================================= */
let pendingBookmarkBoardId = null;
let pendingRenameCallback = null;
// ---- RENDER ----
function renderBoards() {
const wrapper = document.getElementById('boardsWrapper');
wrapper.innerHTML = '';
if (boards.length === 0) {
wrapper.innerHTML = `<div class="empty-state">
No boards yet. Click <strong style="color:var(--accent)">+ Board</strong> to create one,
or use <strong style="color:var(--accent)">Import</strong> to load your browser bookmarks.
</div>`;
return;
}
boards.forEach(board => wrapper.appendChild(createBoardEl(board)));
initBoardDragDrop();
}
function createBoardEl(board) {
const div = document.createElement('div');
div.className = 'board' + (board.blurred ? ' blurred' : '');
div.dataset.boardId = board.id;
// Header
const header = document.createElement('div');
header.className = 'board-header';
header.innerHTML = `
<span class="board-drag-handle" title="Board verschieben">
<svg width="10" height="14" viewBox="0 0 10 14" fill="currentColor">
<circle cx="2" cy="2" r="1.5"/><circle cx="8" cy="2" r="1.5"/>
<circle cx="2" cy="7" r="1.5"/><circle cx="8" cy="7" r="1.5"/>
<circle cx="2" cy="12" r="1.5"/><circle cx="8" cy="12" r="1.5"/>
</svg>
</span>
<span class="board-title" title="${escHtml(board.title)}">${escHtml(board.title)}</span>
<div class="board-actions">
<button class="board-action-btn btn-blur-board" title="${board.blurred ? 'Unblur' : 'Blur (privat)'}">🔒</button>
<button class="board-action-btn btn-rename-board" title="Umbenennen">✎</button>
<button class="board-action-btn btn-delete-board" title="Löschen">✕</button>
</div>
`;
// Blur-Overlay
const blurOverlay = document.createElement('div');
blurOverlay.className = 'board-blur-overlay';
div.appendChild(blurOverlay);
header.querySelector('.btn-blur-board').addEventListener('click', async e => {
e.stopPropagation();
board.blurred = !board.blurred;
div.classList.toggle('blurred', board.blurred);
e.currentTarget.title = board.blurred ? 'Unblur' : 'Blur (privat)';
await saveBoards();
});
blurOverlay.addEventListener('click', async () => {
board.blurred = false;
div.classList.remove('blurred');
header.querySelector('.btn-blur-board').title = 'Blur (privat)';
await saveBoards();
});
header.querySelector('.btn-rename-board').addEventListener('click', e => {
e.stopPropagation();
openRenameModal(board.title, async newName => {
if (!newName.trim()) return;
board.title = newName.trim();
await saveBoards();
renderBoards();
});
});
header.querySelector('.btn-delete-board').addEventListener('click', e => {
e.stopPropagation();
if (confirm(`Board "${board.title}" löschen?`)) {
boards = boards.filter(b => b.id !== board.id);
saveBoards().then(renderBoards);
}
});
// Bookmark List
const list = document.createElement('ul');
list.className = 'board-list';
list.dataset.boardId = board.id;
const visibleCount = settings.hideExtra ? settings.visibleCount : board.bookmarks.length;
const visible = board.bookmarks.slice(0, visibleCount);
const hidden = board.bookmarks.slice(visibleCount);
visible.forEach(bm => list.appendChild(createBmEl(bm)));
div.appendChild(header);
div.appendChild(list);
// Event Delegation für Bookmark-Klicks und -Löschungen
bindBoardListEvents(list, board);
// Show More
if (hidden.length > 0) {
let expanded = false;
let hiddenEls = [];
const showMoreBtn = document.createElement('button');
showMoreBtn.className = 'show-more-btn';
showMoreBtn.textContent = `Show ${hidden.length} more…`;
showMoreBtn.addEventListener('click', () => {
if (!expanded) {
hidden.forEach(bm => { const el = createBmEl(bm); hiddenEls.push(el); list.appendChild(el); });
showMoreBtn.textContent = 'Show less';
expanded = true;
} else {
hiddenEls.forEach(el => el.remove());
hiddenEls = [];
showMoreBtn.textContent = `Show ${hidden.length} more…`;
expanded = false;
}
});
div.appendChild(showMoreBtn);
}
// Add Bookmark
const addBtn = document.createElement('button');
addBtn.className = 'add-bm-btn';
addBtn.innerHTML = `<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> Add link`;
addBtn.addEventListener('click', () => openAddBookmarkModal(board.id));
div.appendChild(addBtn);
initBookmarkDragDrop(list, board);
return div;
}
function createBmEl(bm) {
const li = document.createElement('li');
li.className = 'bm-item';
li.dataset.bmId = bm.id;
li.dataset.bmUrl = bm.url;
li.draggable = true;
const favicon = document.createElement('img');
favicon.className = 'bm-favicon';
favicon.width = 14;
favicon.height = 14;
favicon.src = getFaviconUrl(bm.url);
favicon.addEventListener('error', function() {
this.style.display = 'none';
this.nextElementSibling.style.display = 'flex';
});
const fallback = document.createElement('div');
fallback.className = 'bm-favicon-fallback';
fallback.style.display = 'none';
fallback.textContent = bm.title.charAt(0).toUpperCase();
const textDiv = document.createElement('div');
textDiv.className = 'bm-text';
const titleSpan = document.createElement('span');
titleSpan.className = 'bm-title';
titleSpan.title = bm.title;
titleSpan.textContent = bm.title;
const descSpan = document.createElement('span');
descSpan.className = 'bm-desc';
descSpan.textContent = bm.desc || '';
textDiv.appendChild(titleSpan);
textDiv.appendChild(descSpan);
const deleteBtn = document.createElement('button');
deleteBtn.className = 'bm-delete';
deleteBtn.title = 'Entfernen';
deleteBtn.textContent = '✕';
li.appendChild(favicon);
li.appendChild(fallback);
li.appendChild(textDiv);
li.appendChild(deleteBtn);
return li;
}
// Event Delegation: Ein Listener pro Board-Liste statt pro Bookmark
function bindBoardListEvents(list, board) {
list.addEventListener('click', async e => {
const bmItem = e.target.closest('.bm-item');
if (!bmItem) return;
// Delete-Button geklickt
if (e.target.closest('.bm-delete')) {
e.stopPropagation();
const bmId = bmItem.dataset.bmId;
board.bookmarks = board.bookmarks.filter(b => b.id !== bmId);
await saveBoards();
renderBoards();
return;
}
// Bookmark-Link geklickt
const url = bmItem.dataset.bmUrl;
if (url) {
window.open(url, settings.newTab ? '_blank' : '_self', 'noopener,noreferrer');
}
});
}
// ---- MODALS ----
function openModal(id) { document.getElementById(id).classList.add('active'); }
function closeModal(id) { document.getElementById(id).classList.remove('active'); }
function openAddBoardModal() {
document.getElementById('newBoardName').value = '';
openModal('addBoardOverlay');
setTimeout(() => document.getElementById('newBoardName').focus(), 50);
}
function openAddBookmarkModal(boardId) {
pendingBookmarkBoardId = boardId;
['newBmTitle','newBmUrl','newBmDesc'].forEach(id => document.getElementById(id).value = '');
openModal('addBookmarkOverlay');
setTimeout(() => document.getElementById('newBmTitle').focus(), 50);
}
function openRenameModal(currentName, callback) {
pendingRenameCallback = callback;
document.getElementById('renameInput').value = currentName;
openModal('renameOverlay');
setTimeout(() => { const i = document.getElementById('renameInput'); i.focus(); i.select(); }, 50);
}
// ---- BOOKMARK HTML IMPORT ----
function parseBookmarkHtml(html) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const result = [];
function parseFolder(dlEl, folderName) {
const bms = [];
dlEl.querySelectorAll(':scope > dt').forEach(dt => {
const a = dt.querySelector(':scope > a');
const h3 = dt.querySelector(':scope > h3');
if (a && a.href) {
bms.push({ id: uid(), title: a.textContent.trim() || a.href, url: a.href, desc: '' });
} else if (h3) {
const subDl = dt.querySelector(':scope > dl');
if (subDl) {
const sub = parseFolder(subDl, h3.textContent.trim());
if (sub.bookmarks.length > 0) result.push(sub);
}
}
});
return { id: uid(), title: folderName || 'Imported', bookmarks: bms };
}
const topDts = doc.querySelectorAll('body > dl > dt, body > p > dl > dt');
if (topDts.length === 0) {
const allLinks = [];
doc.querySelectorAll('a').forEach(a => {
if (a.href && !a.href.startsWith('place:'))
allLinks.push({ id: uid(), title: a.textContent.trim() || a.href, url: a.href, desc: '' });
});
if (allLinks.length > 0) result.push({ id: uid(), title: 'Imported Bookmarks', bookmarks: allLinks });
} else {
doc.querySelectorAll('body > dl > dt, body > p > dl > dt').forEach(dt => {
const h3 = dt.querySelector(':scope > h3');
const dl = dt.querySelector(':scope > dl');
if (h3 && dl) {
const folder = parseFolder(dl, h3.textContent.trim());
if (folder.bookmarks.length > 0) result.push(folder);
}
});
}
return result;
}