feat(app): Onboarding, Settings-Redesign und Docs für v1.9.0
- 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
This commit is contained in:
@@ -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 = `<a href="${url}">${title}</a>`;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
Reference in New Issue
Block a user