From 00baa0231b9531331feaeb065d9e9e62c88f7ace Mon Sep 17 00:00:00 2001 From: Florian Wathling Date: Sat, 21 Mar 2026 19:08:17 +0100 Subject: [PATCH] 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 --- src/js/app.js | 60 ++++++++++++- src/js/boards.js | 136 +++++++++++++++++++++------- src/js/data.js | 15 +++- src/js/dialog.js | 154 ++++++++++++++++++++++++++++++++ src/js/drag.js | 73 ++++++++------- src/js/onboarding.js | 207 +++++++++++++++++++++++++++++++++++++++++++ src/js/storage.js | 4 +- 7 files changed, 577 insertions(+), 72 deletions(-) create mode 100644 src/js/dialog.js create mode 100644 src/js/onboarding.js 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); } }