Files
Hellion-NewTab/docs/patterns.md
T
JonKazama-Hellion 8dc7a73811 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
2026-03-22 08:54:57 +01:00

8.2 KiB

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.

// 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.

// 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.

// 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.

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 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():

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:

_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:

[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:

// 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:

{
  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():

// 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):

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:

_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.