Files
Hellion-NewTab/docs/widget-schema.md
JonKazama-Hellion 677344f24d docs(release): Dokumentation ins Englische übersetzen und v1.11.1 Docs
- README, CHANGELOG, DISCLAIMER, SECURITY auf Englisch übersetzen
- Projekt-Docs (architecture, patterns, widget-schema, style-guide) übersetzen
- CODEOWNERS für Master-Branch-Schutz hinzufügen
- release.yml auf Englisch übersetzen
- STYLE_GUIDE von src/css/ nach docs/ verschieben
2026-03-22 13:12:24 +01:00

9.5 KiB

Hellion Dashboard — Widget Schema

This document is intentionally written in English. Full German/English i18n support is planned for v2.0 — until then, English keeps the docs accessible to anyone who wants to contribute or fork the project.


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 — WidgetManager only knows about DOM and position, never about content.


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. Returns the widget ID.

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. This is where your module renders its content.

const body = WidgetManager.getBody('widget_calculator');
if (body) Calculator.renderBody(body);

getState(id) → Object | null

Returns the current widget state — position, size, open status.

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. No undo.

minimize(id)

Hides a widget with animation. The widget stays in the registry with open: false so it can be restored.

openWidget(id)

Restores a minimized widget with animation.

bringToFront(id)

Increments z-index so the widget sits above everything else. Called automatically on pointerdown.

save() → Array

Returns an array of all type: 'note' widget states. Used by Notes.save() to merge position/size data with note content.

restore(renderCallback)

Loads widget states from storage and recreates all note widgets. Single-instance widgets (Calculator, Timer) restore themselves in their own init()restore() only handles notes.


Shared Storage Key: widgetStates

All widget modules share a single storage key. Every module's save() must read first and preserve whatever it doesn't own — otherwise modules silently wipe each other's data on every save.

// Full widgetStates structure
{
  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: [],    // Only used by 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
        // Image data is NOT stored here — sessionStorage only
      }
    ]
  }
}

The Save Pattern

Every module that touches widgetStates must follow this pattern:

// From notes.js — same pattern applies to every widget module
async save() {
  const existing = await Store.get(this.STORAGE_KEY);
  const saveData = { notes: mergedNotes };

  // Preserve everything we don't own
  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: Single or Multi-Instance?

Single-instance (Calculator, Timer style): one widget, fixed ID, toggle() opens and closes it. Multi-instance (Notes, ImageRef style): multiple widgets, dynamic IDs, create() adds new ones.

Step 2: Create the Module

Here's a minimal single-instance widget template. Follow the same structure — the lifecycle hooks especially are easy to get wrong.

const YourWidget = {
  WIDGET_ID: 'widget_yourwidget',
  STORAGE_KEY: 'widgetStates',
  _isOpen: false,

  async load() {
    const data = await Store.get(this.STORAGE_KEY);
    if (data && data.yourWidget) {
      // Restore your state
    }
  },

  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);
  },

  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();
  },

  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();
    }
  },

  renderBody(bodyEl) {
    bodyEl.textContent = '';
    // Build your UI with createElement — never innerHTML!
  },

  async init() {
    await this.load();

    const data = await Store.get(this.STORAGE_KEY);
    if (data && data.yourWidget && data.yourWidget.open) {
      await this.open();
    }

    // Lifecycle hooks — always call the previous method first
    // or you'll break every widget that wrapped before yours
    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();
      }
    };

    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();
      }
    };

    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 <script> tag after widgets.js and before data.js
  2. newtab.html — Add toolbar button: <button class="widget-toolbar-btn" data-action="your-action">
  3. notes.js — Add handler in initToolbar(): else if (action === 'your-action') { YourWidget.toggle(); }
  4. notes.js — Preserve your key in save(): if (existing && existing.yourWidget) saveData.yourWidget = existing.yourWidget;
  5. app.js — Add await YourWidget.init(); to the init sequence
  6. main.css — Add widget-specific styles
  7. data.js — Add export/import logic if your data should survive a JSON backup

Widget DOM Structure

Every widget created by WidgetManager.create() has this structure. Your module renders into .widget-body via renderBody() — never touch the header or resize handle.

<div class="widget" data-widget-id="widget_abc123"
     style="left: 120px; top: 80px; width: 280px; height: 220px;">
  <div class="widget-header">         <!-- Drag handle -->
    <span class="widget-title">Title</span>   <!-- Double-click to edit, max 20 chars -->
    <div class="widget-actions">
      <button class="widget-btn widget-minimize"></button>
      <button class="widget-btn widget-close"></button>
    </div>
  </div>
  <div class="widget-body">
    <!-- Your content goes here via renderBody() -->
  </div>
  <div class="widget-resize-handle"></div>   <!-- Bottom-right, visible on hover -->
</div>