From 8dc7a738111d92af8925e525e9f9bba5e7157fa9 Mon Sep 17 00:00:00 2001 From: Florian Wathling Date: Sun, 22 Mar 2026 08:54:57 +0100 Subject: [PATCH] =?UTF-8?q?feat(app):=20Onboarding,=20Settings-Redesign=20?= =?UTF-8?q?und=20Docs=20f=C3=BCr=20v1.9.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Onboarding mit Widget-Toolbar Slide und Gaming Starter Board - Settings in Darstellung-Modal und schlankes Settings-Panel - About-Block als fixierten Footer im Settings-Panel - Dropdown-Optionen an Theme-Farben anpassen - Projekt-Dokumentation (Architektur, Widget-Schema, Patterns) - Firefox Update-URL für Store-Veröffentlichung - Versions-Bump auf 1.9.0 in allen Manifests --- .github/FUNDING.yml | 4 + .gitignore | 4 + README.md | 4 +- docs/architecture.md | 163 +++++++++++++++++++++ docs/patterns.md | 310 +++++++++++++++++++++++++++++++++++++++ docs/widget-schema.md | 330 ++++++++++++++++++++++++++++++++++++++++++ manifest.firefox.json | 3 +- manifest.json | 2 +- manifest.opera.json | 2 +- newtab.html | 323 +++++++++++++++++++---------------------- src/css/main.css | 11 +- src/js/app.js | 2 +- src/js/data.js | 2 +- src/js/onboarding.js | 74 ++++++++-- src/js/settings.js | 2 +- 15 files changed, 1039 insertions(+), 197 deletions(-) create mode 100644 .github/FUNDING.yml create mode 100644 docs/architecture.md create mode 100644 docs/patterns.md create mode 100644 docs/widget-schema.md diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..56a0522 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + + +ko_fi: hellionmedia diff --git a/.gitignore b/.gitignore index c89c2f7..90ae61e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,10 @@ dist/ node_modules/ /xpi/ v2-planning.md +themes-v2.md + +# Firefox Update-Manifest (wird auf hellion-media.de gehostet) +updates.json # Persönliche Backup-Dateien (nicht ins Repo) favorites_*.html diff --git a/README.md b/README.md index 3a75a8e..421839e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# ⬡ Hellion Dashboard v1.5.2 +# ⬡ Hellion Dashboard v1.9.0 -![Version](https://img.shields.io/badge/Version-1.5.2-blue) +![Version](https://img.shields.io/badge/Version-1.9.0-blue) ![JavaScript](https://img.shields.io/badge/JavaScript-Vanilla%20ES2020-F7DF1E?logo=javascript&logoColor=black) ![Manifest](https://img.shields.io/badge/Manifest-V3-green) ![License](https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-orange) diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..91b94d4 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,163 @@ +# Hellion Dashboard — Architecture + +## Overview + +Hellion Dashboard is a browser extension (NewTab replacement) built with **Vanilla JavaScript ES2020**, **CSS Custom Properties**, and **zero dependencies**. No build step, no framework, no bundler — files are loaded directly via ` + + + + + + + + + + + + + + + +``` + +**Rule:** A module may only reference modules loaded before it. + +--- + +## Z-Index Hierarchy + +| Layer | z-index | Elements | +|---|---|---| +| Background | 0-2 | `#bgLayer`, boards | +| Search bar | 90 | `.search-bar-wrapper` | +| Widgets + Toolbar | 100+ | `.widget`, `.widget-toolbar` | +| Header | 100 | `#header` | +| Settings panel | 200 | `#settingsPanel` | +| Dialogs / Modals | 300 | `.hellion-dialog-overlay`, modals | +| Onboarding | 400 | `#onboardingOverlay` | + +Widgets use incrementing z-index (`WidgetManager._topZ++`) to stack above each other on click. + +--- + +## Storage Keys + +| Key | Type | Content | +|---|---|---| +| `boards` | Array | Board objects with bookmarks | +| `settings` | Object | User preferences (theme, toggles, etc.) | +| `widgetStates` | Object | All widget data (see [widget-schema.md](widget-schema.md)) | +| `onboardingDone` | Boolean | Whether onboarding has been completed | +| `lastBackupReminder` | Number | Timestamp of last backup reminder | + +--- + +## Browser Compatibility + +| Browser | Engine | Manifest | +|---|---|---| +| Chrome | Chromium MV3 | `manifest.json` | +| Edge | Chromium MV3 | `manifest.json` | +| Brave | Chromium MV3 | `manifest.json` | +| Vivaldi | Chromium MV3 | `manifest.json` | +| Opera / GX | Chromium MV3 | `manifest.opera.json` | +| Firefox | Gecko MV3 | `manifest.firefox.json` | + +Changes affecting manifest fields must be synchronized across all three manifest files. diff --git a/docs/patterns.md b/docs/patterns.md new file mode 100644 index 0000000..aa9d1d5 --- /dev/null +++ b/docs/patterns.md @@ -0,0 +1,310 @@ +# Hellion Dashboard — Code Patterns & Conventions + +## Core Principles + +- **Vanilla JS ES2020** — No frameworks, no TypeScript, no build step +- **Zero dependencies** — Everything is built from scratch +- **`createElement` only** — Never use `innerHTML` (XSS prevention) +- **CSS Custom Properties** — No hardcoded colors, everything through `var(--name)` +- **Event delegation** — One listener per container, not per element +- **Storage abstraction** — All storage access through `Store.get()` / `Store.set()` + +--- + +## Pattern: Storage Abstraction + +**File:** `src/js/storage.js` + +All persistent data goes through the `Store` object. Never access `chrome.storage` or `localStorage` directly. + +```javascript +// Reading +const boards = await Store.get('boards'); // Returns null if not found +const settings = await Store.get('settings'); + +// Writing +await Store.set('boards', boards); +await Store.set('settings', settings); + +// Quota check (chrome.storage only, 10 MB limit) +await Store.checkQuota(); +``` + +**Why?** The `Store` handles the chrome.storage / localStorage fallback transparently. It also provides unified error handling (shows a dialog when storage is full). + +--- + +## Pattern: Event Delegation + +Instead of attaching listeners to each element, attach one to the container and use `closest()` to find the target. + +```javascript +// GOOD — one listener, handles all bookmarks +container.addEventListener('click', (e) => { + const bmItem = e.target.closest('.bm-item'); + if (!bmItem) return; + const id = bmItem.dataset.id; + // Handle click +}); + +// BAD — listener per element +bookmarks.forEach(bm => { + bm.addEventListener('click', handler); // Don't do this! +}); +``` + +**Used in:** `boards.js` (board/bookmark events), `notes.js` (toolbar), `calculator.js` (button grid) + +--- + +## Pattern: createElement over innerHTML + +Always build DOM with `document.createElement()`. This prevents XSS and is the project's #1 security rule. + +```javascript +// GOOD +const link = document.createElement('a'); +link.href = bookmark.url; +link.textContent = bookmark.title; +container.appendChild(link); + +// BAD — XSS risk! +container.innerHTML = `${title}`; +``` + +--- + +## Pattern: Shared Storage Key + +Multiple widget modules share the `widgetStates` key. Every module must read-before-write and preserve other modules' data. + +```javascript +async save() { + const data = await Store.get('widgetStates') || {}; + + // Write your own data + data.yourKey = { /* ... */ }; + + // DON'T overwrite — the key already contains other modules' data + await Store.set('widgetStates', data); +} +``` + +See [widget-schema.md](widget-schema.md) for the full `widgetStates` structure. + +--- + +## Pattern: Widget Lifecycle Hooks + +Single-instance widgets (Calculator, Timer) need to know when they're closed, minimized, or reopened. They wrap `WidgetManager` methods in their `init()`: + +```javascript +async init() { + // Wrap close + const prevClose = WidgetManager.close; + const self = this; + WidgetManager.close = function(id) { + prevClose.call(WidgetManager, id); + if (id === self.WIDGET_ID) { + self.onClose(); + } + }; + + // Wrap minimize + const prevMinimize = WidgetManager.minimize; + WidgetManager.minimize = async function(id) { + await prevMinimize.call(WidgetManager, id); + if (id === self.WIDGET_ID) { + self._isOpen = false; + await self.save(); + } + }; +} +``` + +**Important:** Multiple widgets chain these wraps. Calculator wraps first, Timer wraps Calculator's already-wrapped version, and so on. The chain must not break. + +--- + +## Pattern: Debounced Save + +For frequent updates (typing in notes, moving widgets), use debounced saves to avoid excessive storage writes: + +```javascript +_saveTimer: null, + +_debouncedSave() { + clearTimeout(this._saveTimer); + this._saveTimer = setTimeout(() => this.save(), 500); +} + +// Usage: call _debouncedSave() instead of save() for frequent events +textarea.addEventListener('input', () => { + noteData.content = textarea.value; + this._debouncedSave(); +}); +``` + +**Used in:** `notes.js` (text editing), `image-ref.js` (label editing) + +--- + +## Pattern: Theme System + +All themes use CSS Custom Properties defined in `[data-theme="name"]` blocks: + +```css +[data-theme="nebula"] { + --bg-primary: #0a0e17; + --bg-board: rgba(15, 20, 35, 0.65); + --text-primary: #e0e6f0; + --accent: #7db3ff; + --border: rgba(125, 179, 255, 0.12); + /* ... more variables */ +} +``` + +**Never hardcode colors in JS.** Use CSS classes or variables: + +```javascript +// GOOD — let CSS handle colors +element.classList.add('active'); + +// BAD — hardcoded color +element.style.color = '#7db3ff'; +``` + +8 themes are available: Nebula, Crescent, Event Horizon, Merchantman, Julia & Jin, SC Sunset, Hellion HUD, Hellion Energy. + +--- + +## Pattern: Onboarding Slides + +The onboarding system (`onboarding.js`) uses a data-driven slide array. Each slide is an object with rendering hints: + +```javascript +{ + hero: '🎮', // Large emoji/icon + title: 'Slide Title', // Heading + text: 'Description...', // Optional text paragraph + features: ['Item 1', ...], // Optional bullet list + showThemes: true, // Optional theme grid + interactive: 'gaming-board' // Optional custom buttons +} +``` + +The `_render()` method reads these properties and builds the DOM. To add a new slide, just add an object to the `slides` array. + +--- + +## Pattern: Dialog System + +Custom dialogs replace native `alert()` and `confirm()`: + +```javascript +// Alert (informational) +await HellionDialog.alert('Message text', { + type: 'info', // 'info', 'success', 'warning', 'danger' + title: 'Title' +}); + +// Confirm (yes/no) +const ok = await HellionDialog.confirm('Are you sure?', { + type: 'danger', + title: 'Delete', + confirmText: 'Delete', // Custom button text + cancelText: 'Cancel' +}); +if (ok) { /* user confirmed */ } +``` + +--- + +## Pattern: Pointer Events for Drag + +Widget dragging and board reordering use the Pointer Events API (not mouse events): + +```javascript +element.addEventListener('pointerdown', (e) => { + element.setPointerCapture(e.pointerId); + + function onMove(ev) { + // Update position + } + + function onUp() { + element.releasePointerCapture(e.pointerId); + element.removeEventListener('pointermove', onMove); + element.removeEventListener('pointerup', onUp); + } + + element.addEventListener('pointermove', onMove); + element.addEventListener('pointerup', onUp); +}); +``` + +**Why Pointer Events over Mouse Events?** They work with both mouse and touch, and `setPointerCapture` ensures events continue even if the cursor leaves the element. + +--- + +## Pattern: Canvas API Image Processing + +The image reference widget converts uploaded images to WebP for smaller size: + +```javascript +_processFile(file) { + return new Promise((resolve, reject) => { + const objectUrl = URL.createObjectURL(file); + const img = new Image(); + + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0); + const webpUrl = canvas.toDataURL('image/webp', 0.85); + URL.revokeObjectURL(objectUrl); + resolve(webpUrl); + }; + + img.onerror = () => { + URL.revokeObjectURL(objectUrl); + reject(new Error('Image could not be loaded')); + }; + + img.src = objectUrl; + }); +} +``` + +**Important:** Always call `URL.revokeObjectURL()` to free memory. + +--- + +## Coding Rules Summary + +| Rule | Rationale | +|---|---| +| `createElement` only, never `innerHTML` | XSS prevention | +| All storage through `Store` | Browser compatibility | +| CSS variables, no hardcoded colors | Theme support | +| Event delegation | Performance, dynamic content | +| `const`/`let`, never `var` | Block scoping | +| No external dependencies | Extension simplicity | +| No build step | Direct development | +| JSDoc comments on public functions | Documentation | +| URL validation before `href` | Security | +| Error handling on storage operations | Graceful failure | + +--- + +## Manifest Synchronization + +Three manifest files must stay in sync: + +- `manifest.json` — Chrome, Edge, Brave, Vivaldi +- `manifest.firefox.json` — Firefox +- `manifest.opera.json` — Opera, Opera GX + +When changing version numbers, permissions, or content script entries, update all three files. diff --git a/docs/widget-schema.md b/docs/widget-schema.md new file mode 100644 index 0000000..04415a5 --- /dev/null +++ b/docs/widget-schema.md @@ -0,0 +1,330 @@ +# Hellion Dashboard — Widget Schema + +## Overview + +The widget system provides draggable, resizable floating panels managed by `WidgetManager` (`src/js/widgets.js`). Each widget type has its own module that handles content rendering and state management. + +--- + +## Widget Types + +| Type | Module | Instance | Max | Storage | +|---|---|---|---|---| +| `note` | `notes.js` | Multi | 5 | Persistent (`widgetStates.notes`) | +| `calculator` | `calculator.js` | Single | 1 | Persistent (`widgetStates.calculator`) | +| `timer` | `timer.js` | Single | 1 | Persistent (`widgetStates.timer`) | +| `image` | `image-ref.js` | Multi | 3 | Meta: persistent, Image data: sessionStorage | + +--- + +## WidgetManager API + +### `create(type, config) → string` + +Creates a widget and appends it to the DOM. + +```javascript +const id = WidgetManager.create('note', { + id: 'note_abc123', // Optional, auto-generated if omitted + title: 'My Note', // Default: 'Note' + x: 120, // Left position in px + y: 80, // Top position in px + width: 280, // Width in px (min: 200) + height: 220, // Height in px (min: 150) + open: true // Visible state (default: true) +}); +``` + +### `getBody(id) → HTMLElement | null` + +Returns the `.widget-body` element for content rendering. + +```javascript +const body = WidgetManager.getBody('widget_calculator'); +if (body) Calculator.renderBody(body); +``` + +### `getState(id) → Object | null` + +Returns the current widget state (position, size, open status). + +```javascript +const state = WidgetManager.getState('widget_timer'); +// → { id, type, title, x, y, width, height, open } +``` + +### `close(id)` + +Permanently removes a widget from the DOM and registry. + +### `minimize(id)` + +Hides a widget with animation. Widget remains in registry with `open: false`. + +### `openWidget(id)` + +Restores a minimized widget with animation. + +### `bringToFront(id)` + +Increments z-index to bring widget above all others. + +### `save() → Array` + +Returns an array of all `type: 'note'` widget states. Used by `Notes.save()` to merge with note content data. + +### `restore(renderCallback)` + +Loads widget states from storage and recreates all note widgets. Only handles notes — single-instance widgets (calculator, timer) restore themselves in their own `init()`. + +--- + +## Shared Storage Key: `widgetStates` + +All widget modules share a single storage key. Each module's `save()` method must preserve other modules' data. + +```javascript +// Structure of widgetStates +{ + notes: [ + { + id: 'note_abc123', + title: 'My Note', + content: 'Hello world', + template: 'text', // 'text' or 'checklist' + x: 120, y: 80, + width: 280, height: 220, + open: true, + checklistItems: [], // For checklist template + checkedItems: [] // Checked item IDs + } + ], + calculator: { + x: 400, y: 120, + width: 280, height: 400, + open: false, + history: [ + { expr: '2 + 3', result: '5' } + ] + }, + timer: { + x: 600, y: 80, + width: 260, height: 360, + open: false, + muted: false, + presets: [ + { name: 'Forschung', seconds: 2700 } + ] + }, + imageRef: { + images: [ + { + id: 'image_0', + label: 'Bauplan', + x: 200, y: 120, + width: 320, height: 280, + open: true + } + ] + } +} +``` + +### Save Pattern — Preserving Other Modules' Data + +Every module that saves to `widgetStates` must read existing data first and preserve keys it doesn't own: + +```javascript +// Example from notes.js +async save() { + const existing = await Store.get(this.STORAGE_KEY); + const saveData = { notes: mergedNotes }; + + // Preserve other modules + if (existing && existing.calculator) saveData.calculator = existing.calculator; + if (existing && existing.timer) saveData.timer = existing.timer; + if (existing && existing.imageRef) saveData.imageRef = existing.imageRef; + + await Store.set(this.STORAGE_KEY, saveData); +} +``` + +--- + +## Creating a New Widget Type + +### Step 1: Choose Single or Multi-Instance + +- **Single-instance** (like Calculator, Timer): One widget with a fixed ID. `toggle()` opens/closes. +- **Multi-instance** (like Notes, ImageRef): Multiple widgets with dynamic IDs. `create()` adds new ones. + +### Step 2: Create the Module (`src/js/your-widget.js`) + +```javascript +const YourWidget = { + WIDGET_ID: 'widget_yourwidget', // Fixed ID for single-instance + STORAGE_KEY: 'widgetStates', + _isOpen: false, + + // Load state from storage + async load() { + const data = await Store.get(this.STORAGE_KEY); + if (data && data.yourWidget) { + // Restore your state + } + }, + + // Save state, preserving other modules + async save() { + const data = await Store.get(this.STORAGE_KEY) || {}; + if (data.notes === undefined) data.notes = []; + + const widgetState = WidgetManager.getState(this.WIDGET_ID); + data.yourWidget = { + x: widgetState ? widgetState.x : 400, + y: widgetState ? widgetState.y : 120, + width: widgetState ? widgetState.width : 280, + height: widgetState ? widgetState.height : 300, + open: this._isOpen, + // ... your custom data + }; + + await Store.set(this.STORAGE_KEY, data); + }, + + // Open widget + async open() { + if (this._isOpen) { + WidgetManager.bringToFront(this.WIDGET_ID); + return; + } + + const data = await Store.get(this.STORAGE_KEY); + const saved = (data && data.yourWidget) ? data.yourWidget : {}; + + WidgetManager.create('yourwidget', { + id: this.WIDGET_ID, + title: 'Your Widget', + x: saved.x || 400, + y: saved.y || 120, + width: saved.width || 280, + height: saved.height || 300, + open: true + }); + + const body = WidgetManager.getBody(this.WIDGET_ID); + if (body) this.renderBody(body); + + this._isOpen = true; + await this.save(); + }, + + // Toggle open/close + async toggle() { + if (this._isOpen) { + const entry = WidgetManager._widgets.get(this.WIDGET_ID); + if (entry && entry.state.open) { + await WidgetManager.minimize(this.WIDGET_ID); + this._isOpen = false; + await this.save(); + } else if (entry) { + await WidgetManager.openWidget(this.WIDGET_ID); + this._isOpen = true; + await this.save(); + } + } else { + await this.open(); + } + }, + + // Render widget content + renderBody(bodyEl) { + bodyEl.textContent = ''; + // Build your UI with createElement (never innerHTML!) + }, + + // Initialize and hook into lifecycle + async init() { + await this.load(); + + // Restore if was open last time + const data = await Store.get(this.STORAGE_KEY); + if (data && data.yourWidget && data.yourWidget.open) { + await this.open(); + } + + // Hook into close event + const self = this; + const prevClose = WidgetManager.close; + WidgetManager.close = function(id) { + prevClose.call(WidgetManager, id); + if (id === self.WIDGET_ID) { + self._isOpen = false; + self.save(); + } + }; + + // Hook into minimize event + const prevMinimize = WidgetManager.minimize; + WidgetManager.minimize = async function(id) { + await prevMinimize.call(WidgetManager, id); + if (id === self.WIDGET_ID) { + self._isOpen = false; + await self.save(); + } + }; + + // Hook into open event + const prevOpen = WidgetManager.openWidget; + WidgetManager.openWidget = async function(id) { + await prevOpen.call(WidgetManager, id); + if (id === self.WIDGET_ID) { + self._isOpen = true; + const body = WidgetManager.getBody(self.WIDGET_ID); + if (body && body.children.length === 0) { + self.renderBody(body); + } + await self.save(); + } + }; + } +}; +``` + +### Step 3: Integration Checklist + +1. **`newtab.html`** — Add `