diff --git a/src/js/app.js b/src/js/app.js
index 2a8a059..5c16187 100644
--- a/src/js/app.js
+++ b/src/js/app.js
@@ -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 });
diff --git a/src/js/boards.js b/src/js/boards.js
index b0f2afc..276463c 100644
--- a/src/js/boards.js
+++ b/src/js/boards.js
@@ -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 = `
- No boards yet. Click + Board to create one,
- or use Import to load your browser bookmarks.
-
`;
+ 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 = `
-
-
-
- ${escHtml(board.title)}
-
-
-
-
-
- `;
+
+ 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 = ` 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');
diff --git a/src/js/data.js b/src/js/data.js
index 7d05eb7..6f1a2c6 100644
--- a/src/js/data.js
+++ b/src/js/data.js
@@ -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 = '';
});
diff --git a/src/js/dialog.js b/src/js/dialog.js
new file mode 100644
index 0000000..26fd8a4
--- /dev/null
+++ b/src/js/dialog.js
@@ -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: '',
+ success: '',
+ warning: '',
+ danger: ''
+ },
+
+ /**
+ * 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}
+ */
+ _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}
+ */
+ 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}
+ */
+ 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
+ });
+ }
+};
diff --git a/src/js/drag.js b/src/js/drag.js
index fa8414c..c7864a4 100644
--- a/src/js/drag.js
+++ b/src/js/drag.js
@@ -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();
});
}
diff --git a/src/js/onboarding.js b/src/js/onboarding.js
new file mode 100644
index 0000000..dfb35d1
--- /dev/null
+++ b/src/js/onboarding.js
@@ -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);
+ }
+};
diff --git a/src/js/storage.js b/src/js/storage.js
index 8a5ec05..858b5d9 100644
--- a/src/js/storage.js
+++ b/src/js/storage.js
@@ -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);
}
}