feat(ui): Custom Dialog-System, Onboarding und Backup-Reminder
- HellionDialog.alert/confirm ersetzt alle nativen confirm() und alert() Aufrufe - 6-stufiger Onboarding-Flow beim ersten Start (Boards, Themes, Features, Backup) - Backup-Reminder erinnert alle 7 Tage an JSON-Export - innerHTML komplett durch createElement/createElementNS ersetzt (XSS-Schutz) - Drag & Drop Inline-Styles durch CSS-Klassen ersetzt
This commit is contained in:
+57
-3
@@ -19,6 +19,54 @@ async function init() {
|
||||
initStickyNote();
|
||||
initDataButtons();
|
||||
Store.checkQuota();
|
||||
|
||||
// Onboarding beim ersten Start
|
||||
const onboardingDone = await Store.get('onboardingDone');
|
||||
if (!onboardingDone) {
|
||||
Onboarding.start();
|
||||
} else {
|
||||
// Backup-Reminder (nur wenn Onboarding schon durch ist)
|
||||
await checkBackupReminder();
|
||||
}
|
||||
}
|
||||
|
||||
// ---- BACKUP REMINDER ----
|
||||
const BACKUP_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000; // 7 Tage
|
||||
|
||||
async function checkBackupReminder() {
|
||||
const lastReminder = await Store.get('lastBackupReminder');
|
||||
const now = Date.now();
|
||||
|
||||
// Beim allerersten Mal: Timestamp setzen, aber noch nicht nerven
|
||||
if (!lastReminder) {
|
||||
await Store.set('lastBackupReminder', now);
|
||||
return;
|
||||
}
|
||||
|
||||
if (now - lastReminder < BACKUP_INTERVAL_MS) return;
|
||||
|
||||
// Nur erinnern wenn es Boards gibt die sich lohnen zu sichern
|
||||
if (boards.length === 0) return;
|
||||
|
||||
const doBackup = await HellionDialog.confirm(
|
||||
'Du hast seit über einer Woche kein Backup gemacht. Beim Löschen der Browserdaten gehen deine Boards verloren. Jetzt sichern?',
|
||||
{ type: 'warning', title: 'Backup-Erinnerung', confirmText: 'Jetzt sichern', cancelText: 'Später' }
|
||||
);
|
||||
|
||||
if (doBackup) {
|
||||
// JSON-Export auslösen (gleiche Logik wie btnExportJSON)
|
||||
const data = { version: '1.5.2', exported: new Date().toISOString(), boards, settings };
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'hellion-newtab-backup-' + new Date().toISOString().slice(0, 10) + '.json';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// Timestamp immer aktualisieren (egal ob gesichert oder "Später")
|
||||
await Store.set('lastBackupReminder', now);
|
||||
}
|
||||
|
||||
// ---- CLOCK & DATE ----
|
||||
@@ -50,12 +98,18 @@ function bindGlobalEvents() {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
const imported = parseBookmarkHtml(await file.text());
|
||||
if (imported.length === 0) { alert('Keine Bookmarks gefunden.'); return; }
|
||||
if (imported.length === 0) {
|
||||
await HellionDialog.alert('Keine Bookmarks in dieser Datei gefunden.', { type: 'warning', title: 'Import' });
|
||||
return;
|
||||
}
|
||||
boards = [...boards, ...imported];
|
||||
await saveBoards();
|
||||
renderBoards();
|
||||
e.target.value = '';
|
||||
alert(`✓ ${imported.length} Board(s) mit ${imported.reduce((s,b) => s + b.bookmarks.length, 0)} Bookmarks importiert.`);
|
||||
await HellionDialog.alert(
|
||||
`${imported.length} Board(s) mit ${imported.reduce((s,b) => s + b.bookmarks.length, 0)} Bookmarks importiert.`,
|
||||
{ type: 'success', title: 'Import erfolgreich' }
|
||||
);
|
||||
});
|
||||
|
||||
// Add Board Modal
|
||||
@@ -86,7 +140,7 @@ function bindGlobalEvents() {
|
||||
const url = document.getElementById('newBmUrl').value.trim();
|
||||
const desc = document.getElementById('newBmDesc').value.trim();
|
||||
if (!title || !url) return;
|
||||
try { new URL(url); } catch { alert('Ungültige URL. Bitte mit https:// beginnen.'); return; }
|
||||
try { new URL(url); } catch { await HellionDialog.alert('Ungültige URL. Bitte mit https:// beginnen.', { type: 'warning', title: 'URL ungültig' }); return; }
|
||||
const board = boards.find(b => b.id === pendingBookmarkBoardId);
|
||||
if (!board) return;
|
||||
board.bookmarks.push({ id: uid(), title, url, desc });
|
||||
|
||||
+104
-32
@@ -6,16 +6,67 @@
|
||||
let pendingBookmarkBoardId = null;
|
||||
let pendingRenameCallback = null;
|
||||
|
||||
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||
|
||||
/**
|
||||
* Erzeugt ein SVG-Element mit Attributen und Kinder-Elementen.
|
||||
* @param {string} tag - SVG-Tag (z.B. 'svg', 'circle', 'line')
|
||||
* @param {Object} attrs - Attribute als Key-Value
|
||||
* @param {Array} children - Kind-Elemente
|
||||
* @returns {SVGElement}
|
||||
*/
|
||||
function svgEl(tag, attrs, children) {
|
||||
const el = document.createElementNS(SVG_NS, tag);
|
||||
if (attrs) {
|
||||
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
|
||||
}
|
||||
if (children) {
|
||||
for (const child of children) el.appendChild(child);
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
/** Erzeugt das 6-Punkt Drag-Handle SVG */
|
||||
function createDragHandleSvg() {
|
||||
return svgEl('svg', { width: '10', height: '14', viewBox: '0 0 10 14', fill: 'currentColor' }, [
|
||||
svgEl('circle', { cx: '2', cy: '2', r: '1.5' }),
|
||||
svgEl('circle', { cx: '8', cy: '2', r: '1.5' }),
|
||||
svgEl('circle', { cx: '2', cy: '7', r: '1.5' }),
|
||||
svgEl('circle', { cx: '8', cy: '7', r: '1.5' }),
|
||||
svgEl('circle', { cx: '2', cy: '12', r: '1.5' }),
|
||||
svgEl('circle', { cx: '8', cy: '12', r: '1.5' }),
|
||||
]);
|
||||
}
|
||||
|
||||
/** Erzeugt das Plus-Icon SVG */
|
||||
function createPlusSvg() {
|
||||
return svgEl('svg', { width: '11', height: '11', viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', 'stroke-width': '2' }, [
|
||||
svgEl('line', { x1: '12', y1: '5', x2: '12', y2: '19' }),
|
||||
svgEl('line', { x1: '5', y1: '12', x2: '19', y2: '12' }),
|
||||
]);
|
||||
}
|
||||
|
||||
// ---- RENDER ----
|
||||
function renderBoards() {
|
||||
const wrapper = document.getElementById('boardsWrapper');
|
||||
wrapper.innerHTML = '';
|
||||
wrapper.replaceChildren();
|
||||
|
||||
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>`;
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'empty-state';
|
||||
|
||||
const boardStrong = document.createElement('strong');
|
||||
boardStrong.className = 'accent-text';
|
||||
boardStrong.textContent = '+ Board';
|
||||
|
||||
const importStrong = document.createElement('strong');
|
||||
importStrong.className = 'accent-text';
|
||||
importStrong.textContent = 'Import';
|
||||
|
||||
empty.append(
|
||||
'No boards yet. Click ', boardStrong, ' to create one, or use ', importStrong, ' to load your browser bookmarks.'
|
||||
);
|
||||
wrapper.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -31,43 +82,59 @@ function createBoardEl(board) {
|
||||
// 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>
|
||||
`;
|
||||
|
||||
const dragHandle = document.createElement('span');
|
||||
dragHandle.className = 'board-drag-handle';
|
||||
dragHandle.title = 'Board verschieben';
|
||||
dragHandle.appendChild(createDragHandleSvg());
|
||||
|
||||
const titleSpanHeader = document.createElement('span');
|
||||
titleSpanHeader.className = 'board-title';
|
||||
titleSpanHeader.title = board.title;
|
||||
titleSpanHeader.textContent = board.title;
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'board-actions';
|
||||
|
||||
const btnBlur = document.createElement('button');
|
||||
btnBlur.className = 'board-action-btn btn-blur-board';
|
||||
btnBlur.title = board.blurred ? 'Unblur' : 'Blur (privat)';
|
||||
btnBlur.textContent = '\uD83D\uDD12';
|
||||
|
||||
const btnRename = document.createElement('button');
|
||||
btnRename.className = 'board-action-btn btn-rename-board';
|
||||
btnRename.title = 'Umbenennen';
|
||||
btnRename.textContent = '\u270E';
|
||||
|
||||
const btnDelete = document.createElement('button');
|
||||
btnDelete.className = 'board-action-btn btn-delete-board';
|
||||
btnDelete.title = 'Löschen';
|
||||
btnDelete.textContent = '\u2715';
|
||||
|
||||
actions.append(btnBlur, btnRename, btnDelete);
|
||||
header.append(dragHandle, titleSpanHeader, actions);
|
||||
|
||||
// Blur-Overlay
|
||||
const blurOverlay = document.createElement('div');
|
||||
blurOverlay.className = 'board-blur-overlay';
|
||||
div.appendChild(blurOverlay);
|
||||
|
||||
header.querySelector('.btn-blur-board').addEventListener('click', async e => {
|
||||
btnBlur.addEventListener('click', async e => {
|
||||
e.stopPropagation();
|
||||
board.blurred = !board.blurred;
|
||||
div.classList.toggle('blurred', board.blurred);
|
||||
e.currentTarget.title = board.blurred ? 'Unblur' : 'Blur (privat)';
|
||||
btnBlur.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)';
|
||||
btnBlur.title = 'Blur (privat)';
|
||||
await saveBoards();
|
||||
});
|
||||
|
||||
header.querySelector('.btn-rename-board').addEventListener('click', e => {
|
||||
btnRename.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
openRenameModal(board.title, async newName => {
|
||||
if (!newName.trim()) return;
|
||||
@@ -77,11 +144,16 @@ function createBoardEl(board) {
|
||||
});
|
||||
});
|
||||
|
||||
header.querySelector('.btn-delete-board').addEventListener('click', e => {
|
||||
btnDelete.addEventListener('click', async e => {
|
||||
e.stopPropagation();
|
||||
if (confirm(`Board "${board.title}" löschen?`)) {
|
||||
const ok = await HellionDialog.confirm(
|
||||
`Board "${board.title}" wirklich löschen?`,
|
||||
{ type: 'danger', title: 'Board löschen', confirmText: 'Löschen' }
|
||||
);
|
||||
if (ok) {
|
||||
boards = boards.filter(b => b.id !== board.id);
|
||||
saveBoards().then(renderBoards);
|
||||
await saveBoards();
|
||||
renderBoards();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -127,7 +199,8 @@ function createBoardEl(board) {
|
||||
// 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.appendChild(createPlusSvg());
|
||||
addBtn.append(' Add link');
|
||||
addBtn.addEventListener('click', () => openAddBookmarkModal(board.id));
|
||||
div.appendChild(addBtn);
|
||||
|
||||
@@ -148,13 +221,12 @@ function createBmEl(bm) {
|
||||
favicon.height = 14;
|
||||
favicon.src = getFaviconUrl(bm.url);
|
||||
favicon.addEventListener('error', function() {
|
||||
this.style.display = 'none';
|
||||
this.nextElementSibling.style.display = 'flex';
|
||||
this.classList.add('hidden');
|
||||
this.nextElementSibling.classList.remove('hidden');
|
||||
});
|
||||
|
||||
const fallback = document.createElement('div');
|
||||
fallback.className = 'bm-favicon-fallback';
|
||||
fallback.style.display = 'none';
|
||||
fallback.className = 'bm-favicon-fallback hidden';
|
||||
fallback.textContent = bm.title.charAt(0).toUpperCase();
|
||||
|
||||
const textDiv = document.createElement('div');
|
||||
|
||||
+11
-4
@@ -11,7 +11,7 @@ function initDataButtons() {
|
||||
|
||||
// Export
|
||||
btnExport.addEventListener('click', () => {
|
||||
const data = { version: '1.2.0', exported: new Date().toISOString(), boards, settings };
|
||||
const data = { version: '1.5.2', exported: new Date().toISOString(), boards, settings };
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
@@ -42,13 +42,20 @@ function initDataButtons() {
|
||||
return true;
|
||||
});
|
||||
if (validBoards.length === 0) throw new Error('Keine gültigen Boards gefunden');
|
||||
if (!confirm(`${validBoards.length} Boards importieren? Bestehende Daten bleiben erhalten.`)) return;
|
||||
const ok = await HellionDialog.confirm(
|
||||
`${validBoards.length} Boards importieren? Bestehende Daten bleiben erhalten.`,
|
||||
{ type: 'info', title: 'JSON Import' }
|
||||
);
|
||||
if (!ok) return;
|
||||
boards = [...boards, ...validBoards];
|
||||
await saveBoards();
|
||||
renderBoards();
|
||||
alert(`✓ ${validBoards.length} Board(s) importiert.`);
|
||||
await HellionDialog.alert(
|
||||
`${validBoards.length} Board(s) erfolgreich importiert.`,
|
||||
{ type: 'success', title: 'Import erfolgreich' }
|
||||
);
|
||||
} catch (err) {
|
||||
alert('Fehler beim Import: ' + err.message);
|
||||
await HellionDialog.alert('Fehler beim Import: ' + err.message, { type: 'danger', title: 'Import fehlgeschlagen' });
|
||||
}
|
||||
e.target.value = '';
|
||||
});
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
/* =============================================
|
||||
HELLION NEWTAB — dialog.js
|
||||
Custom Dialog System (ersetzt native alert/confirm)
|
||||
============================================= */
|
||||
|
||||
const HellionDialog = {
|
||||
/** SVG-Icons je nach Dialog-Typ */
|
||||
_icons: {
|
||||
info: '<circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/>',
|
||||
success: '<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>',
|
||||
warning: '<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>',
|
||||
danger: '<circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/>'
|
||||
},
|
||||
|
||||
/**
|
||||
* Erzeugt das SVG-Icon-Element
|
||||
* @param {string} type - info | success | warning | danger
|
||||
* @returns {SVGElement}
|
||||
*/
|
||||
_createIcon(type) {
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.setAttribute('width', '20');
|
||||
svg.setAttribute('height', '20');
|
||||
svg.setAttribute('viewBox', '0 0 24 24');
|
||||
svg.setAttribute('fill', 'none');
|
||||
svg.setAttribute('stroke', 'currentColor');
|
||||
svg.setAttribute('stroke-width', '2');
|
||||
svg.setAttribute('stroke-linecap', 'round');
|
||||
svg.setAttribute('stroke-linejoin', 'round');
|
||||
svg.className.baseVal = 'dialog-icon type-' + type;
|
||||
// SVG-Pfade müssen per innerHTML gesetzt werden (kein User-Input, nur statische Pfade)
|
||||
svg.innerHTML = this._icons[type] || this._icons.info;
|
||||
return svg;
|
||||
},
|
||||
|
||||
/**
|
||||
* Erstellt und zeigt einen Dialog
|
||||
* @param {Object} config
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
_show(config) {
|
||||
return new Promise(resolve => {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'dialog-overlay';
|
||||
|
||||
const box = document.createElement('div');
|
||||
box.className = 'dialog-box';
|
||||
|
||||
// Header
|
||||
const header = document.createElement('div');
|
||||
header.className = 'dialog-header';
|
||||
header.appendChild(this._createIcon(config.type));
|
||||
const titleSpan = document.createElement('span');
|
||||
titleSpan.textContent = config.title;
|
||||
header.appendChild(titleSpan);
|
||||
|
||||
// Body
|
||||
const body = document.createElement('div');
|
||||
body.className = 'dialog-body';
|
||||
body.textContent = config.message;
|
||||
|
||||
// Actions
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'dialog-actions';
|
||||
|
||||
function cleanup(result) {
|
||||
overlay.classList.remove('active');
|
||||
document.removeEventListener('keydown', keyHandler);
|
||||
setTimeout(() => overlay.remove(), 200);
|
||||
resolve(result);
|
||||
}
|
||||
|
||||
// Cancel-Button (nur bei confirm)
|
||||
if (config.isConfirm) {
|
||||
const cancelBtn = document.createElement('button');
|
||||
cancelBtn.className = 'btn-secondary';
|
||||
cancelBtn.textContent = config.cancelText;
|
||||
cancelBtn.addEventListener('click', () => cleanup(false));
|
||||
actions.appendChild(cancelBtn);
|
||||
}
|
||||
|
||||
// Confirm/OK-Button
|
||||
const confirmBtn = document.createElement('button');
|
||||
confirmBtn.className = config.type === 'danger' && config.isConfirm ? 'btn-danger' : 'btn-primary';
|
||||
confirmBtn.textContent = config.confirmText;
|
||||
confirmBtn.addEventListener('click', () => cleanup(config.isConfirm ? true : undefined));
|
||||
actions.appendChild(confirmBtn);
|
||||
|
||||
box.append(header, body, actions);
|
||||
overlay.appendChild(box);
|
||||
|
||||
// Overlay-Klick schließt
|
||||
overlay.addEventListener('click', e => {
|
||||
if (e.target === overlay) cleanup(config.isConfirm ? false : undefined);
|
||||
});
|
||||
|
||||
// Keyboard
|
||||
function keyHandler(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
cleanup(config.isConfirm ? true : undefined);
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
cleanup(config.isConfirm ? false : undefined);
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', keyHandler);
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
// Nächster Frame für CSS-Transition
|
||||
requestAnimationFrame(() => {
|
||||
overlay.classList.add('active');
|
||||
confirmBtn.focus();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Zeigt einen Alert-Dialog (ersetzt window.alert)
|
||||
* @param {string} message - Nachricht
|
||||
* @param {Object} [options] - { title, confirmText, type }
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
alert(message, options) {
|
||||
const opts = options || {};
|
||||
return this._show({
|
||||
message,
|
||||
title: opts.title || 'Hinweis',
|
||||
confirmText: opts.confirmText || 'OK',
|
||||
cancelText: '',
|
||||
type: opts.type || 'info',
|
||||
isConfirm: false
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Zeigt einen Confirm-Dialog (ersetzt window.confirm)
|
||||
* @param {string} message - Nachricht
|
||||
* @param {Object} [options] - { title, confirmText, cancelText, type }
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
confirm(message, options) {
|
||||
const opts = options || {};
|
||||
return this._show({
|
||||
message,
|
||||
title: opts.title || 'Bestätigung',
|
||||
confirmText: opts.confirmText || 'OK',
|
||||
cancelText: opts.cancelText || 'Abbrechen',
|
||||
type: opts.type || 'info',
|
||||
isConfirm: true
|
||||
});
|
||||
}
|
||||
};
|
||||
+42
-31
@@ -37,13 +37,11 @@ function initBoardDragDrop() {
|
||||
|
||||
// Ghost
|
||||
const ghost = boardEl.cloneNode(true);
|
||||
ghost.style.cssText = `
|
||||
position:fixed; left:${rect.left}px; top:${rect.top}px;
|
||||
width:${rect.width}px; height:${rect.height}px;
|
||||
opacity:0.75; pointer-events:none; z-index:9999;
|
||||
transform:rotate(1.5deg) scale(1.02);
|
||||
box-shadow:0 12px 40px rgba(0,0,0,0.6);
|
||||
`;
|
||||
ghost.className += ' drag-ghost';
|
||||
ghost.style.left = rect.left + 'px';
|
||||
ghost.style.top = rect.top + 'px';
|
||||
ghost.style.width = rect.width + 'px';
|
||||
ghost.style.height = rect.height + 'px';
|
||||
document.body.appendChild(ghost);
|
||||
|
||||
// Placeholder
|
||||
@@ -104,29 +102,42 @@ function initBoardDragDrop() {
|
||||
function initBookmarkDragDrop(listEl, board) {
|
||||
let dragSrcBmId = null;
|
||||
|
||||
listEl.querySelectorAll('.bm-item').forEach(item => {
|
||||
item.addEventListener('dragstart', e => {
|
||||
dragSrcBmId = item.dataset.bmId;
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
setTimeout(() => item.style.opacity = '0.4', 0);
|
||||
});
|
||||
item.addEventListener('dragend', () => { item.style.opacity = ''; });
|
||||
item.addEventListener('dragover', e => {
|
||||
e.preventDefault();
|
||||
item.style.background = 'rgba(255,160,50,0.07)';
|
||||
});
|
||||
item.addEventListener('dragleave', () => { item.style.background = ''; });
|
||||
item.addEventListener('drop', async e => {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
item.style.background = '';
|
||||
const targetBmId = item.dataset.bmId;
|
||||
if (!dragSrcBmId || dragSrcBmId === targetBmId) return;
|
||||
const srcIdx = board.bookmarks.findIndex(b => b.id === dragSrcBmId);
|
||||
const tgtIdx = board.bookmarks.findIndex(b => b.id === targetBmId);
|
||||
const [moved] = board.bookmarks.splice(srcIdx, 1);
|
||||
board.bookmarks.splice(tgtIdx, 0, moved);
|
||||
await saveBoards();
|
||||
renderBoards();
|
||||
});
|
||||
listEl.addEventListener('dragstart', e => {
|
||||
const item = e.target.closest('.bm-item');
|
||||
if (!item) return;
|
||||
dragSrcBmId = item.dataset.bmId;
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
setTimeout(() => item.classList.add('dragging-source'), 0);
|
||||
});
|
||||
|
||||
listEl.addEventListener('dragend', e => {
|
||||
const item = e.target.closest('.bm-item');
|
||||
if (item) item.classList.remove('dragging-source');
|
||||
});
|
||||
|
||||
listEl.addEventListener('dragover', e => {
|
||||
e.preventDefault();
|
||||
const item = e.target.closest('.bm-item');
|
||||
if (item) item.classList.add('drag-over');
|
||||
});
|
||||
|
||||
listEl.addEventListener('dragleave', e => {
|
||||
const item = e.target.closest('.bm-item');
|
||||
if (item) item.classList.remove('drag-over');
|
||||
});
|
||||
|
||||
listEl.addEventListener('drop', async e => {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
const item = e.target.closest('.bm-item');
|
||||
if (!item) return;
|
||||
item.classList.remove('drag-over');
|
||||
const targetBmId = item.dataset.bmId;
|
||||
if (!dragSrcBmId || dragSrcBmId === targetBmId) return;
|
||||
const srcIdx = board.bookmarks.findIndex(b => b.id === dragSrcBmId);
|
||||
const tgtIdx = board.bookmarks.findIndex(b => b.id === targetBmId);
|
||||
const [moved] = board.bookmarks.splice(srcIdx, 1);
|
||||
board.bookmarks.splice(tgtIdx, 0, moved);
|
||||
await saveBoards();
|
||||
renderBoards();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
/* =============================================
|
||||
HELLION NEWTAB — onboarding.js
|
||||
Mehrstufiger Willkommens-Flow beim ersten Start
|
||||
============================================= */
|
||||
|
||||
const Onboarding = {
|
||||
currentSlide: 0,
|
||||
|
||||
slides: [
|
||||
{
|
||||
hero: '\u2B21',
|
||||
title: 'Willkommen bei Hellion Dashboard',
|
||||
text: 'Dein neuer Browser-Startbildschirm. Minimalistisch, schnell und vollst\u00E4ndig lokal \u2014 keine Cloud, kein Account, keine Datensammlung.'
|
||||
},
|
||||
{
|
||||
hero: '\uD83D\uDCCB',
|
||||
title: 'Boards & Bookmarks',
|
||||
features: [
|
||||
'Erstelle Boards mit dem \u201E+ Board\u201C Button oben',
|
||||
'Importiere Browser-Lesezeichen \u00FCber den \u201EImport\u201C Button im Header',
|
||||
'Drag & Drop zum Umsortieren von Boards und Links',
|
||||
'Blur-Modus f\u00FCr private Boards (\uD83D\uDD12 Icon)'
|
||||
]
|
||||
},
|
||||
{
|
||||
hero: '\uD83C\uDFA8',
|
||||
title: '8 handgefertigte Themes',
|
||||
text: 'Klicke auf den „Theme" Button im Header um dein Theme zu wählen. Jedes hat seinen eigenen Stil und Farbpalette.',
|
||||
showThemes: true
|
||||
},
|
||||
{
|
||||
hero: '\u26A1',
|
||||
title: 'Weitere Features',
|
||||
features: [
|
||||
'Suchleiste mit Google, DuckDuckGo oder Bing',
|
||||
'Sticky Notes f\u00FCr schnelle Notizen',
|
||||
'Funktioniert komplett offline \u2014 alles lokal gespeichert'
|
||||
]
|
||||
},
|
||||
{
|
||||
hero: '\uD83D\uDEE1\uFE0F',
|
||||
title: 'Backups nicht vergessen!',
|
||||
text: 'Deine Daten sind lokal im Browser gespeichert. Wenn du Browserdaten l\u00F6schst, gehen sie verloren! Sichere regelm\u00E4\u00DFig \u00FCber Settings \u2192 Data \u2192 Export. Wir erinnern dich alle 7 Tage daran.'
|
||||
},
|
||||
{
|
||||
hero: '\uD83D\uDE80',
|
||||
title: 'Bereit!',
|
||||
text: 'Klicke auf \u201E+ Board\u201C um dein erstes Board zu erstellen, oder nutze den \u201EImport\u201C Button im Header um deine Browser-Lesezeichen zu importieren.'
|
||||
}
|
||||
],
|
||||
|
||||
/** Startet das Onboarding */
|
||||
start() {
|
||||
this.currentSlide = 0;
|
||||
this._render();
|
||||
this._bindKeyboard();
|
||||
const overlay = document.getElementById('onboardingOverlay');
|
||||
requestAnimationFrame(() => overlay.classList.add('active'));
|
||||
},
|
||||
|
||||
/** Schlie\u00DFt das Onboarding und speichert den Status */
|
||||
async _finish() {
|
||||
const overlay = document.getElementById('onboardingOverlay');
|
||||
overlay.classList.remove('active');
|
||||
document.removeEventListener('keydown', this._keyHandler);
|
||||
await Store.set('onboardingDone', true);
|
||||
},
|
||||
|
||||
/** Rendert den aktuellen Slide */
|
||||
_render() {
|
||||
const modal = document.getElementById('onboardingModal');
|
||||
modal.replaceChildren();
|
||||
|
||||
const slide = this.slides[this.currentSlide];
|
||||
const isLast = this.currentSlide === this.slides.length - 1;
|
||||
|
||||
// Skip-Button (nicht auf letztem Slide)
|
||||
if (!isLast) {
|
||||
const skip = document.createElement('button');
|
||||
skip.className = 'onboarding-skip';
|
||||
skip.textContent = '\u00DCberspringen';
|
||||
skip.addEventListener('click', () => this._finish());
|
||||
modal.appendChild(skip);
|
||||
}
|
||||
|
||||
// Slide-Content
|
||||
const slideEl = document.createElement('div');
|
||||
slideEl.className = 'onboarding-slide';
|
||||
|
||||
const hero = document.createElement('div');
|
||||
hero.className = 'onboarding-hero';
|
||||
hero.textContent = slide.hero;
|
||||
slideEl.appendChild(hero);
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'onboarding-title';
|
||||
title.textContent = slide.title;
|
||||
slideEl.appendChild(title);
|
||||
|
||||
if (slide.text) {
|
||||
const text = document.createElement('div');
|
||||
text.className = 'onboarding-text';
|
||||
text.textContent = slide.text;
|
||||
slideEl.appendChild(text);
|
||||
}
|
||||
|
||||
if (slide.features) {
|
||||
const list = document.createElement('ul');
|
||||
list.className = 'onboarding-feature-list';
|
||||
slide.features.forEach(f => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = f;
|
||||
list.appendChild(li);
|
||||
});
|
||||
slideEl.appendChild(list);
|
||||
}
|
||||
|
||||
if (slide.showThemes) {
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'onboarding-theme-grid';
|
||||
const themeNames = ['Nebula', 'Crescent', 'Event Horizon', 'Merchantman', 'Julia & Jin', 'SC Sunset', 'Hellion HUD', 'Hellion Energy'];
|
||||
themeNames.forEach(name => {
|
||||
const chip = document.createElement('div');
|
||||
chip.className = 'onboarding-theme-chip';
|
||||
chip.textContent = name;
|
||||
grid.appendChild(chip);
|
||||
});
|
||||
slideEl.appendChild(grid);
|
||||
}
|
||||
|
||||
modal.appendChild(slideEl);
|
||||
|
||||
// Footer
|
||||
const footer = document.createElement('div');
|
||||
footer.className = 'onboarding-footer';
|
||||
|
||||
// Dots
|
||||
const dots = document.createElement('div');
|
||||
dots.className = 'onboarding-dots';
|
||||
for (let i = 0; i < this.slides.length; i++) {
|
||||
const dot = document.createElement('div');
|
||||
dot.className = 'onboarding-dot' + (i === this.currentSlide ? ' active' : '');
|
||||
dots.appendChild(dot);
|
||||
}
|
||||
footer.appendChild(dots);
|
||||
|
||||
// Navigation
|
||||
const nav = document.createElement('div');
|
||||
nav.className = 'onboarding-nav';
|
||||
|
||||
if (this.currentSlide > 0) {
|
||||
const backBtn = document.createElement('button');
|
||||
backBtn.className = 'btn-secondary';
|
||||
backBtn.textContent = 'Zur\u00FCck';
|
||||
backBtn.addEventListener('click', () => {
|
||||
this.currentSlide--;
|
||||
this._render();
|
||||
});
|
||||
nav.appendChild(backBtn);
|
||||
}
|
||||
|
||||
if (isLast) {
|
||||
const startBtn = document.createElement('button');
|
||||
startBtn.className = 'btn-primary';
|
||||
startBtn.textContent = 'Los geht\u2019s!';
|
||||
startBtn.addEventListener('click', () => this._finish());
|
||||
nav.appendChild(startBtn);
|
||||
} else {
|
||||
const nextBtn = document.createElement('button');
|
||||
nextBtn.className = 'btn-primary';
|
||||
nextBtn.textContent = 'Weiter';
|
||||
nextBtn.addEventListener('click', () => {
|
||||
this.currentSlide++;
|
||||
this._render();
|
||||
});
|
||||
nav.appendChild(nextBtn);
|
||||
}
|
||||
|
||||
footer.appendChild(nav);
|
||||
modal.appendChild(footer);
|
||||
},
|
||||
|
||||
/** Keyboard-Navigation */
|
||||
_bindKeyboard() {
|
||||
this._keyHandler = (e) => {
|
||||
if (e.key === 'ArrowRight' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (this.currentSlide < this.slides.length - 1) {
|
||||
this.currentSlide++;
|
||||
this._render();
|
||||
} else {
|
||||
this._finish();
|
||||
}
|
||||
}
|
||||
if (e.key === 'ArrowLeft' && this.currentSlide > 0) {
|
||||
e.preventDefault();
|
||||
this.currentSlide--;
|
||||
this._render();
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
this._finish();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', this._keyHandler);
|
||||
}
|
||||
};
|
||||
+2
-2
@@ -23,7 +23,7 @@ const Store = {
|
||||
chrome.storage.local.set({ [key]: value }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error('Storage-Fehler:', chrome.runtime.lastError.message);
|
||||
alert('Speicher voll! Bitte lösche alte Boards oder das Hintergrundbild, um Platz zu schaffen.');
|
||||
HellionDialog.alert('Speicher voll! Bitte lösche alte Boards oder das Hintergrundbild, um Platz zu schaffen.', { type: 'danger', title: 'Speicher voll' });
|
||||
reject(new Error(chrome.runtime.lastError.message));
|
||||
return;
|
||||
}
|
||||
@@ -35,7 +35,7 @@ const Store = {
|
||||
resolve();
|
||||
} catch (e) {
|
||||
console.error('Storage-Fehler:', e.message);
|
||||
alert('Speicher voll! Bitte lösche alte Boards oder das Hintergrundbild, um Platz zu schaffen.');
|
||||
HellionDialog.alert('Speicher voll! Bitte lösche alte Boards oder das Hintergrundbild, um Platz zu schaffen.', { type: 'danger', title: 'Speicher voll' });
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user