23 Commits

Author SHA1 Message Date
JonKazama-Hellion c6c0d5c468 docs(release): architecture.md auf v2.0.0 aktualisieren
i18n.js und bookmark-import.js ergänzen, Load-Order und
Module-Tabelle an aktuelle newtab.html synchronisieren.
2026-03-22 18:38:18 +01:00
JonKazama-Hellion dbd209bc2b docs(release): README auf v2.0.0 mit i18n-Feature aktualisieren
Version, i18n-Sektion, Architecture-Tree mit i18n.js und _locales,
Modulanzahl und Release-Beispiel aktualisiert.
2026-03-22 18:33:27 +01:00
JonKazama-Hellion 7900962c5a fix(i18n): Review-Findings beheben
- Version in newtab.html auf 2.0.0 aktualisieren
- dialog.js OK-Fallback auf t('dialog.ok') umstellen
- Duplikat-Keys widgets.minimize/close entfernen (widget.* wird genutzt)
2026-03-22 18:28:35 +01:00
JonKazama-Hellion 1bbdbdef1c feat(release): Version auf v2.0.0 bumpen — i18n Release
Alle drei Manifests, Export-Versionen und CHANGELOG aktualisiert.
2026-03-22 18:28:35 +01:00
JonKazama-Hellion f07200cd8e feat(i18n): settings.js und widgets.js auf t() umstellen
Reset-Dialog, Dateifehler, Widget-Buttons (Minimieren/Schließen)
und Default-Titel verwenden jetzt i18n-Keys.
2026-03-22 18:28:35 +01:00
JonKazama-Hellion ab165d4f75 feat(i18n): app.js Strings auf t() umstellen
Clock-Tage/Monate, Backup-Reminder, HTML-Import und
URL-Validierung verwenden jetzt i18n-Keys.
2026-03-22 18:28:35 +01:00
JonKazama-Hellion 4a66015258 feat(i18n): data.js, bookmark-import.js, storage.js auf t() umstellen 2026-03-22 18:28:35 +01:00
JonKazama-Hellion d0f870ace1 feat(i18n): calculator.js, timer.js, image-ref.js auf t() umstellen 2026-03-22 18:28:35 +01:00
JonKazama-Hellion daea57a9df feat(i18n): notes.js Strings auf t() umstellen 2026-03-22 18:28:35 +01:00
JonKazama-Hellion f937f7c39c feat(i18n): onboarding.js Strings auf t() umstellen 2026-03-22 18:28:31 +01:00
JonKazama-Hellion 3ab8847f31 feat(i18n): boards.js Strings auf t() umstellen 2026-03-22 18:28:25 +01:00
JonKazama-Hellion 36335d3cc4 feat(i18n): dialog.js Defaults auf t() umstellen 2026-03-22 18:28:20 +01:00
JonKazama-Hellion 1b39ac863b fix(i18n): Code-Quality-Review Korrekturen
- resolveLang() Hilfsfunktion extrahieren (DRY)
- JSDoc-Kommentar in I18n.init() korrigieren
- settings.export.btn Key für Export-Button trennen
- setLanguage('auto') im Reset-Handler aufrufen
2026-03-22 14:07:17 +01:00
JonKazama-Hellion 522b177470 fix(i18n): Spec-Review-Korrekturen für i18n-Grundgerüst
- header.settings DE-String korrigiert (war englisch)
- ~17 deutsche Strings die englisch waren gefixt
- I18n.init() vor applySettings() in app.js
- html lang-Attribut dynamisch via I18n setzen
- Default html lang auf en (passend zu default_locale)
2026-03-22 14:04:04 +01:00
JonKazama-Hellion f2d4e22b86 feat(i18n): i18n-Modul, _locales und data-i18n Attribute einrichten 2026-03-22 13:56:04 +01:00
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
JonKazama-Hellion 40d4d9f37a feat(app): 3 neue Themes, WebP-Konvertierung und Browser-Bookmark-Import
- Satisfactory, Avorion und Hellion Stealth als neue Themes
- Alle 11 Theme-Bilder von JPG/PNG nach WebP konvertieren (~12 MB → 1.1 MB)
- Browser-Lesezeichen direkt importieren mit Ordner-Auswahl Modal
- Duplikat-Erkennung, URL-Validierung, Chrome/Firefox-Kompatibilität
- Version auf 1.11.1 aktualisieren (Manifeste, data.js, newtab.html, app.js)
2026-03-22 13:12:24 +01:00
JonKazama-Hellion 198171b6c2 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 13:12:24 +01:00
JonKazama-Hellion 51947b229c feat(image-ref): Bild-Referenz Widget mit Session-Storage
Opt-in Widget fuer Bild-Referenzen (max. 3 gleichzeitig).
Canvas API konvertiert zu WebP, sessionStorage fuer Bilddaten.
Positionen und Labels bleiben persistent, Bilder nur pro Session.
2026-03-22 13:12:24 +01:00
JonKazama-Hellion 2f0b76eb4e feat(timer): Timer/Countdown-Widget mit Presets und Alarm
Countdown-Timer als Single-Instance-Widget mit Preset-System
(max. 5), Web Audio API Alarm und Tab-Titel-Blink bei Ablauf.
Mute-Toggle zum Stummschalten des Alarms.
Z-Index-Hierarchie für Widgets auf 100 angehoben.
2026-03-22 13:12:24 +01:00
JonKazama-Hellion 32a6fe88dc feat(calculator): Taschenrechner-Widget mit History und Tastatureingabe
Neues Widget-Modul mit Shunting-Yard Parser, 4x5 Button-Grid,
persistenter History (max 10) und Keyboard-Support.
Storage-Handling in Notes/Data erweitert fuer parallele Persistierung.
2026-03-22 13:12:24 +01:00
JonKazama-Hellion 95e45948be Add CODEOWNER file to github 2026-03-22 12:45:04 +01:00
JonKazama-Hellion a76f63c407 Add Ko-fi funding information
Added Ko-fi username for funding support.
2026-03-21 19:56:41 +01:00
53 changed files with 2486 additions and 848 deletions
+4
View File
@@ -0,0 +1,4 @@
# Hellion NewTab — Code Owners
# Alle Änderungen müssen von @JonKazama-Hellion approved werden
* @JonKazama-Hellion
+5 -2
View File
@@ -1,4 +1,7 @@
# These are supported funding model platforms # Hellion NewTab — Support & Funding
# All tools are free and open-source. Donations are voluntary and go toward server costs.
ko_fi: hellionmedia ko_fi: hellionmedia
custom:
- "https://hellion-media.de"
+10 -10
View File
@@ -1,4 +1,4 @@
# Release — erstellt ZIP-Pakete für Chrome, Firefox und Opera bei neuem Tag # Release — creates ZIP packages for Chrome, Firefox and Opera on new tag
name: Release name: Release
on: on:
@@ -17,18 +17,18 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Version aus Tag extrahieren - name: Extract version from tag
id: version id: version
run: echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" run: echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
- name: Chrome/Edge ZIP erstellen (Manifest V3) - name: Create Chrome/Edge ZIP (Manifest V3)
run: | run: |
mkdir -p dist mkdir -p dist
zip -r "dist/hellion-newtab-${{ steps.version.outputs.tag }}-chrome.zip" \ zip -r "dist/hellion-newtab-${{ steps.version.outputs.tag }}-chrome.zip" \
manifest.json newtab.html src/js/*.js src/css/ assets/ \ manifest.json newtab.html src/js/*.js src/css/ assets/ \
-x "*.git*" "dist/*" ".github/*" "src/js/opera/*" -x "*.git*" "dist/*" ".github/*" "src/js/opera/*"
- name: Firefox ZIP erstellen (Manifest V3) - name: Create Firefox ZIP (Manifest V3)
run: | run: |
cp manifest.json manifest.chrome-backup.json cp manifest.json manifest.chrome-backup.json
cp manifest.firefox.json manifest.json cp manifest.firefox.json manifest.json
@@ -37,7 +37,7 @@ jobs:
-x "*.git*" "dist/*" ".github/*" "manifest.chrome-backup.json" "manifest.firefox.json" "src/js/opera/*" -x "*.git*" "dist/*" ".github/*" "manifest.chrome-backup.json" "manifest.firefox.json" "src/js/opera/*"
mv manifest.chrome-backup.json manifest.json mv manifest.chrome-backup.json manifest.json
- name: Opera/Opera GX ZIP erstellen (Manifest V3 + Workaround) - name: Create Opera/Opera GX ZIP (Manifest V3 + workaround)
run: | run: |
cp manifest.json manifest.chrome-backup.json cp manifest.json manifest.chrome-backup.json
cp manifest.opera.json manifest.json cp manifest.opera.json manifest.json
@@ -46,13 +46,13 @@ jobs:
-x "*.git*" "dist/*" ".github/*" "manifest.chrome-backup.json" "manifest.opera.json" -x "*.git*" "dist/*" ".github/*" "manifest.chrome-backup.json" "manifest.opera.json"
mv manifest.chrome-backup.json manifest.json mv manifest.chrome-backup.json manifest.json
- name: SHA256 Checksummen erstellen - name: Generate SHA256 checksums
run: | run: |
cd dist cd dist
sha256sum *.zip > checksums-sha256.txt sha256sum *.zip > checksums-sha256.txt
cat checksums-sha256.txt cat checksums-sha256.txt
- name: GitHub Release erstellen - name: Create GitHub Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
name: "Hellion NewTab ${{ steps.version.outputs.tag }}" name: "Hellion NewTab ${{ steps.version.outputs.tag }}"
@@ -64,10 +64,10 @@ jobs:
- **Firefox:** `hellion-newtab-${{ steps.version.outputs.tag }}-firefox.zip` - **Firefox:** `hellion-newtab-${{ steps.version.outputs.tag }}-firefox.zip`
- **Opera / Opera GX:** `hellion-newtab-${{ steps.version.outputs.tag }}-opera.zip` - **Opera / Opera GX:** `hellion-newtab-${{ steps.version.outputs.tag }}-opera.zip`
Siehe [README](README.md) für die vollständige Installationsanleitung. See [README](README.md) for the full installation instructions.
### Checksummen ### Checksums
Siehe `checksums-sha256.txt` zur Integritätsprüfung. See `checksums-sha256.txt` to verify file integrity.
files: | files: |
dist/hellion-newtab-${{ steps.version.outputs.tag }}-chrome.zip dist/hellion-newtab-${{ steps.version.outputs.tag }}-chrome.zip
dist/hellion-newtab-${{ steps.version.outputs.tag }}-firefox.zip dist/hellion-newtab-${{ steps.version.outputs.tag }}-firefox.zip
+156 -54
View File
@@ -1,91 +1,193 @@
# ⬡ Hellion Dashboard — Changelog # ⬡ Hellion Dashboard — Changelog
Alle relevanten Änderungen pro Version. Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.0.0/). All notable changes per version. Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
> Changelog entries can be written in English or German. English preferred for consistency.
---
### v2.0.0 — 22.03.2026
#### New Features
- **Internationalization (i18n)** — Full DE/EN language support with runtime switching
- Language setting in Settings panel: German, English or Auto-detect (browser language)
- `i18n.js` module with ~220+ string keys, `t(key, vars?)` helper and `data-i18n` HTML attributes
- `_locales/de/` and `_locales/en/` for manifest-level i18n (`__MSG_extName__`, `__MSG_extDesc__`)
- `<html lang>` attribute updates dynamically when language changes
- All modules migrated: dialog, boards, onboarding, notes, calculator, timer, image-ref, data, bookmark-import, storage, settings, widgets, app
#### Technical
- New script load order: `storage → state → i18n → dialog → ...`
- `applyLanguage()` scans DOM for `data-i18n`, `data-i18n-placeholder`, `data-i18n-title`
- Onboarding slides use i18n keys instead of hardcoded text (rendered at display time)
- Clock day/month names via i18n keys instead of hardcoded arrays
- `resolveLang()` helper for DRY language resolution (auto → browser detect)
---
### v1.10.0 — 22.03.2026
#### Themes
- **3 new themes** — Satisfactory (Industrial Desert), Avorion (Deep Void) and Hellion Stealth (Tactical Recon)
- Now **11 themes** total, each with its own accent colors, overlays and font styles
- Satisfactory has increased board alpha (0.65) and stronger blur (12px), a deliberate choice for better readability on a visually busy background
- Avorion uses a radial gradient overlay so the ship in the center of the image stays visible
- Hellion Stealth is the only theme with a `border-left` hover effect in tactical scanner style
---
### v1.9.0 — 22.03.2026
#### New Features
- **Onboarding reworked** — 7 slides instead of 6, new slide explains the widget toolbar with all widgets
- **Gaming Starter Board** — Opt-in during onboarding: pre-filled board with links to Satisfactory, Factorio, Avorion, Minecraft and Star Citizen
- **Settings redesign** — Settings panel slimmed down to 3 sections (Widgets, Data & Help, Danger Zone)
- **Appearance modal** — Theme picker and all display settings combined in one modal instead of spread across the panel
- **Fixed about footer** — Developer info, license and links are now permanently visible at the bottom of the settings panel
- **Project documentation** — `docs/architecture.md`, `docs/widget-schema.md` and `docs/patterns.md` for anyone who wants to fork or contribute
#### Improvements
- All labels and descriptions unified in German, no more language mix
- Dropdown options use theme colors instead of white browser default
- Firefox update URL for store publishing added to `manifest.firefox.json`
---
### v1.8.0 — 21.03.2026
#### New Features
- **Image Reference Widget** — Drop images as floating reference widgets (max. 3 at once)
- Canvas API WebP conversion for smaller file sizes, all local in the browser
- Two-layer storage: metadata persistent, image data session-only (sessionStorage)
- Load images via drag & drop or file dialog
- Labels editable with debounced save
- Feature is off by default, enable via Settings → Widgets
---
### v1.7.1 — 21.03.2026
#### Improvements
- **Timer mute toggle** — Alarm can be muted via icon button without restarting the timer
- Alarm volume reduced to 7%, 30% was a bit much
- Mute state is saved and persists on next open
---
### v1.7.0 — 21.03.2026
#### New Features
- **Calculator widget** — Shunting-yard parser (no `eval()`), history of last calculations, keyboard input
- **Timer/countdown widget** — Saveable presets, Web Audio API alarm, tab title blinks when timer completes
- **Widget z-index fix** — Widgets now correctly render above the search bar (z-index 100+)
---
### v1.6.0 — 21.03.2026
#### New Features
- **Widget system** — Draggable, resizable floating panels managed by WidgetManager
- **Notes & checklists** — Multi-instance widgets (max. 5) with text and checklist template, Markdown support, export as `.md`
- **Notebook sidebar** — All notes at a glance, quick access via toolbar
- **Widget toolbar** — Floating buttons on the side for quick access to all widgets, position (left/right) configurable in Settings
- **Sticky note migration** — Old sticky notes are automatically migrated to the new widget system on first launch
#### Improvements
- Ko-fi support link added to the about section and `FUNDING.yml`
--- ---
### v1.5.2 — 21.03.2026 ### v1.5.2 — 21.03.2026
#### Neue Features #### New Features
- **Custom Dialog-System** — Native `confirm()` und `alert()` durch Frosted-Glass-Dialoge ersetzt (`dialog.js`) - **Custom dialog system** — Native `confirm()` and `alert()` replaced with frosted glass dialogs (`dialog.js`)
- **Onboarding** — 6-stufiger Willkommens-Flow beim ersten Start mit Boards, Themes, Features und Backup-Hinweis - **Onboarding** — 6-step welcome flow on first launch with explanations for boards, themes, features and a backup reminder
- **Backup-Reminder** — Erinnert alle 7 Tage an JSON-Export, warnt vor Datenverlust bei Browser-Reset - **Backup reminder** — Reminds every 7 days to run a JSON export and warns about data loss on browser reset
- **Theme-Modal** — Theme-Picker als eigenes Modal aus Settings ausgelagert, eigener Header-Button - **Theme modal** — Theme picker moved to its own modal with its own header button
- **Accordion-Settings** — Alle Settings-Sektionen einklappbar mit Chevron (About/Danger Zone standardmäßig zu) - **Accordion settings** — All settings sections collapsible (About and Danger Zone closed by default)
#### Verbesserungen #### Improvements
- Fonts von Google Fonts API auf lokale WOFF2-Dateien umgestellt (DSGVO) - Fonts migrated from Google Fonts API to local WOFF2 files (GDPR, ~388 KB saved)
- Ungenutzte Font-Dateien entfernt (~388 KB gespart) - `innerHTML` fully replaced with `createElement` and `createElementNS` (XSS protection)
- `innerHTML` komplett durch `createElement`/`createElementNS` ersetzt (XSS-Schutz) - SVG icons now via `createElementNS` instead of inline HTML
- SVG-Icons via `createElementNS` statt Inline-HTML - Drag & drop uses CSS classes instead of inline styles (`.drag-ghost`, `.drag-over`, `.dragging-source`)
- Drag & Drop: Inline-Styles durch CSS-Klassen ersetzt (`.drag-ghost`, `.drag-over`, `.dragging-source`) - Search bar toggle moved from DATA to BEHAVIOR section
- Suchleisten-Toggle von DATA nach BEHAVIOR verschoben - Unimplemented "Quick Save" UI element removed
- Nicht implementiertes "Quick Save" UI-Element entfernt - Onboarding repeatable via Settings → Help
- Onboarding wiederholbar über Settings → Help
#### Opera / Opera GX #### Opera / Opera GX
- `manifest.opera.json` hinzugefügt (MV3 mit Workaround-Skripten) - `manifest.opera.json` added (MV3 with workaround scripts)
- `src/js/opera/background.js` — Tab-Management gegen Opera Speed Dial - `src/js/opera/background.js` monitors tabs and redirects away from Opera Speed Dial
- `src/js/opera/redirect.js` — Content Script Redirect bei `document_start` - `src/js/opera/redirect.js` fires as content script at `document_start`
#### Firefox #### Firefox
- `manifest.firefox.json` auf Manifest V3 migriert - `manifest.firefox.json` migrated to Manifest V3
- `browser_specific_settings` mit Gecko-ID und `data_collection_permissions` - `browser_specific_settings` with Gecko ID and `data_collection_permissions` added
#### Build & CI #### Build & CI
- GitHub Actions: Release erstellt jetzt 3 ZIP-Pakete (Chrome, Firefox, Opera) - GitHub Actions release now builds 3 ZIP packages (Chrome, Firefox, Opera)
- Quality-Check prüft alle 3 Manifests und Opera-Ordner - Quality check validates all 3 manifests and the Opera folder
--- ---
### v1.2.0 — 20.03.2026 ### v1.2.0 — 20.03.2026
- Projektstruktur in `src/js/`, `src/css/`, `assets/` aufgeteilt - Project structure split into `src/js/`, `src/css/` and `assets/`
- JS in 10 Module aufgeteilt (storage, state, themes, boards, drag, settings, search, sticky, data, app) - JS split into 10 modules (storage, state, themes, boards, drag, settings, search, sticky, data, app)
- Firefox-Kompatibilität (`manifest.firefox.json`, Manifest V3) - Firefox compatibility (`manifest.firefox.json`, Manifest V3)
- Vivaldi bestätigt kompatibel - Vivaldi confirmed compatible
- Theme-Bildpfade korrigiert (Settings Preview) - Theme image paths fixed (settings preview)
- URL-Validierung bei Bookmark-Erstellung - URL validation on bookmark creation
- JSON-Import mit Board- und Bookmark-Struktur-Validierung - JSON import validates board and bookmark structure
- XSS-Schutz: `createElement` statt `innerHTML` für Bookmarks - XSS protection: `createElement` instead of `innerHTML` for bookmarks
- Storage-Quota-Prüfung mit Warnung bei 8 MB+ - Storage quota check with warning at 8 MB+
- Event Delegation für Bookmark-Klicks (Performance) - Event delegation for bookmark clicks (performance)
- Responsive Design (Tablet 768px, Smartphone 480px) - Responsive design (tablet 768px, smartphone 480px)
- Sticky Note Header-Kollision behoben - Sticky note header collision fixed
- FileReader-Fehlerbehandlung für Hintergrundbild-Upload - FileReader error handling for background image upload
- GitHub Actions: Security Scan, Code Quality, Release Automation - GitHub Actions: security scan, code quality, release automation
- 3 Themes ersetzt: Astronaut → Nebula, Cosmic Clock → Crescent, Void Mage → Event Horizon - 3 themes replaced: Astronaut → Nebula, Cosmic Clock → Crescent, Void Mage → Event Horizon
- Alle Theme-Bilder lizenzrechtlich geprüft und dokumentiert - All theme images checked and documented for license compliance
- LICENSE (CC BY-NC-SA 4.0), SECURITY.md und DISCLAIMER.md hinzugefügt - LICENSE (CC BY-NC-SA 4.0), SECURITY.md and DISCLAIMER.md added
--- ---
### v1.1.0 — 20.03.2026 ### v1.1.0 — 20.03.2026
- 5 neue Themes (Merchantman, Julia & Jin, SC Sunset, Hellion HUD, Hellion Energy) - 5 new themes (Merchantman, Julia & Jin, SC Sunset, Hellion HUD, Hellion Energy)
- Suchleiste (Google / DuckDuckGo / Bing) - Search bar (Google, DuckDuckGo, Bing)
- Sticky Note Widget - Sticky note widget
- JSON Export & Import - JSON export & import
- Datum neben der Uhr - Date next to the clock
- About / Impressum in Settings - About / imprint in settings
- Board Blur-Funktion (Privat-Modus) - Board blur function (privacy mode)
- Drag & Drop auf Pointer Events umgestellt - Drag & drop migrated to Pointer Events API
- Opera / Opera GX Kompatibilität - Opera / Opera GX compatibility
--- ---
### v1.0.0 — 20.03.2026 ### v1.0.0 — 20.03.2026
- Initiales Release - Initial release
- Boards & Bookmarks mit Drag & Drop - Boards & bookmarks with drag & drop
- 3 Themes (Nebula, Crescent, Event Horizon) - 3 themes (Nebula, Crescent, Event Horizon)
- HTML-Import (Browser-Lesezeichen) - HTML import (browser bookmarks)
- Settings Panel - Settings panel
--- ---
+62 -28
View File
@@ -1,47 +1,81 @@
# Haftungsausschluss — Hellion NewTab # Disclaimer — Hellion NewTab
## Nutzung auf eigenes Risiko ## Use at Your Own Risk
Diese Browser-Extension wird "wie besehen" (as-is) zur Verfügung gestellt, ohne jegliche ausdrückliche oder stillschweigende Gewährleistung, einschließlich, aber nicht beschränkt auf die Gewährleistung der Marktgängigkeit, der Eignung für einen bestimmten Zweck und der Nichtverletzung von Rechten Dritter. This browser extension is provided "as is", without warranty of any kind, express
or implied, including but not limited to the warranties of merchantability, fitness
for a particular purpose and non-infringement.
## Keine Garantie ## No Guarantee
Der Entwickler übernimmt keine Haftung für: The developer assumes no liability for:
- Datenverlust durch fehlerhafte Speicherung, Browser-Updates oder Extension-Deinstallation - Data loss caused by storage errors, browser updates or extension uninstallation
- Inkompatibilitäten mit bestimmten Browser-Versionen oder Betriebssystemen - Incompatibilities with specific browser versions or operating systems
- Schäden, die durch die Nutzung oder Nichtnutzung dieser Extension entstehen - Damages arising from the use or inability to use this extension
- Verfügbarkeit oder Korrektheit von Drittanbieter-Diensten (Google Favicons API) - Availability or accuracy of third-party services (Google Favicons API)
## Datenspeicherung ## Data Storage
Alle Daten werden ausschließlich lokal im Browser gespeichert (`chrome.storage.local`). Es erfolgt keine Übertragung an externe Server. Der Entwickler hat keinen Zugriff auf gespeicherte Bookmarks, Einstellungen oder Notizen. All data is stored exclusively in the local browser (`chrome.storage.local`).
No data is transmitted to external servers. The developer has no access to stored
bookmarks, settings, notes or any other user data.
**Empfehlung:** Regelmäßig JSON-Backups über die Export-Funktion erstellen. **Recommendation:** Create regular JSON backups using the export function in Settings.
## Drittanbieter-Dienste ## No Guaranteed Updates
Diese Extension nutzt folgende externe Dienste: This extension is maintained by a single developer in their spare time.
Continued development and updates are not guaranteed. Features may change,
projects may pause, and support is provided on a best-effort basis, not as an obligation.
| Dienst | Zweck | Datenschutz | ## Third-Party Services
| Service | Purpose | Privacy |
|---|---|---| |---|---|---|
| Google Favicons API | Bookmark-Icons laden | Es wird nur die Domain übermittelt, keine vollständige URL | | Google Favicons API | Load bookmark icons | Only the domain is transmitted, not the full URL |
| Google Fonts | Schriftarten (Rajdhani, Inter, Cinzel) | Standardmäßige Google-Fonts-Nutzungsbedingungen |
## Änderungen ## Trademark
Der Entwickler behält sich das Recht vor, diese Extension jederzeit zu ändern, zu aktualisieren oder einzustellen, ohne vorherige Ankündigung. The name "Hellion Online Media", the associated logo and all related graphics are
the property of Florian Wathling / Hellion Online Media and may not be used without
explicit permission. The CC BY-NC-SA 4.0 license applies to the source code and
content of this project, not to trademarks or brand assets.
## Kontakt Forks and derivative works must remove or replace all Hellion Online Media branding.
| | | ## Legal
| --- | --- |
| **Entwickler** | Florian Wathling | This extension is developed and maintained by Florian Wathling / Hellion Online Media,
| **Unternehmen** | Hellion Online Media | based in Bad Harzburg, Germany. All matters are handled in accordance with German
| **Web** | [hellion-media.de](https://hellion-media.de) | and EU law, including the General Data Protection Regulation (GDPR / DSGVO).
| **E-Mail** | [kontakt@hellion-media.de](mailto:kontakt@hellion-media.de) |
| **Impressum** | [hellion-media.de/impressum](https://hellion-media.de/impressum) | For legal inquiries: [hellion-media.de/impressum](https://hellion-media.de/impressum)
## Use of AI
**Claude:** Code analysis, bug fixing, documentation and proofreading.
**Me:** Architecture, features and logic are planned, thought through and written by me.
Who looks for "AI patterns" in the code: clean indentation is the linter,
okayish variable names are the developer, and the semicolon hiding somewhere
is what Claude finds. That's how it works.
I have ADHD and mild dyslexia. Claude helps me stay focused and makes sure
others can follow the code too. That's exactly what open source is for.
Source code is open, every decision is traceable.
--- ---
**Hellion NewTab** — [Hellion Online Media - Florian Wathing](https://hellion-media.de) — JonKazama-Hellion | | |
|---|---|
| **Developer** | Florian Wathling |
| **Company** | Hellion Online Media |
| **Web** | [hellion-media.de](https://hellion-media.de) |
| **Imprint** | [hellion-media.de/impressum](https://hellion-media.de/impressum) |
| **Contact** | [kontakt@hellion-media.de](mailto:kontakt@hellion-media.de) |
---
**Hellion NewTab** — [Hellion Online Media — Florian Wathling](https://hellion-media.de) — JonKazama-Hellion
+206 -178
View File
@@ -1,34 +1,35 @@
# ⬡ Hellion Dashboard v1.9.0 # ⬡ Hellion Dashboard v2.0.0
![Version](https://img.shields.io/badge/Version-1.9.0-blue) ![Version](https://img.shields.io/badge/Version-2.0.0-blue)
![JavaScript](https://img.shields.io/badge/JavaScript-Vanilla%20ES2020-F7DF1E?logo=javascript&logoColor=black) ![JavaScript](https://img.shields.io/badge/JavaScript-Vanilla%20ES2020-F7DF1E?logo=javascript&logoColor=black)
![Manifest](https://img.shields.io/badge/Manifest-V3-green) ![Manifest](https://img.shields.io/badge/Manifest-V3-green)
![License](https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-orange) ![License](https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-orange)
![Privacy](https://img.shields.io/badge/Privacy-100%25%20Lokal-448f45) ![Privacy](https://img.shields.io/badge/Privacy-100%25%20Local-448f45)
[![Ko-fi](https://img.shields.io/badge/Support-Ko--fi-ff5e5b?logo=ko-fi)](https://ko-fi.com/hellionmedia) [![Ko-fi](https://img.shields.io/badge/Support-Ko--fi-ff5e5b?logo=ko-fi)](https://ko-fi.com/hellionmedia)
**Kein Account. Kein Abo. Keine Cloud. Alle Daten bleiben 100% lokal.** **No account. No subscription. No cloud. All data stays 100% local.**
Ein persönlicher Bookmark-Dashboard als Browser-Extension. A personal bookmark dashboard as a browser extension.
Boards, Drag & Drop, 8 Themes, Suchleiste, Sticky Notes — alles im Browser, alles offline. Boards, drag & drop, 11 themes, search bar, widget system with notes, calculator, timer and more.
Keine externe Datenübertragung, keine Tracker, keine Analytics, keine Werbung. Full DE/EN language support with runtime switching. All in the browser, all offline.
No external data transmission, no trackers, no analytics, no ads.
Entwickelt von **[Hellion Online Media — Florian Wathling](https://hellion-media.de)** — JonKazama-Hellion. Developed by **[Hellion Online Media — Florian Wathling](https://hellion-media.de)** — JonKazama-Hellion.
--- ---
## Was diese Extension NICHT ist ## What this extension is NOT
- Kein Cloud-Sync und kein Account-System - No cloud sync and no account system
- Keine Datenerfassung oder Telemetrie - No data collection or telemetry
- Keine Drittanbieter-Abhängigkeiten oder Build-Tools - No third-party dependencies or build tools
- Kein Netzwerkverkehr außer Favicon-Abruf (Google Favicons API) - No network traffic except favicon fetching (Google Favicons API)
## Was diese Extension IST ## What this extension IS
Ein lokaler, privater NewTab-Ersatz für alle gängigen Browser. A local, private NewTab replacement for all major browsers.
Bookmarks werden in `chrome.storage.local` gespeichert — nichts verlässt den Browser. Bookmarks are stored in `chrome.storage.local`, nothing leaves the browser.
Was angezeigt wird, ist was gespeichert ist. Keine Magie. What you see is what's saved. No magic.
--- ---
@@ -36,25 +37,30 @@ Was angezeigt wird, ist was gespeichert ist. Keine Magie.
### Boards & Bookmarks ### Boards & Bookmarks
- Boards als Gruppen für Links — per Drag & Drop umsortierbar - Boards as groups for links, sortable via drag & drop
- Bookmarks mit Favicon, Titel, optionaler Beschreibung - Bookmarks with favicon, title and optional description
- Boards per Blur-Button verstecken (Privat-Modus) - Hide boards with the blur button (privacy mode)
- HTML-Import von Browser-Lesezeichen (Chrome, Edge, Firefox) - HTML import from browser bookmarks (Chrome, Edge, Firefox)
- JSON Export & Import (Backup & Restore) - JSON export & import (backup & restore)
### Suchleiste ### Search Bar
- Google, DuckDuckGo oder Bing — per Klick wechselbar - Google, DuckDuckGo or Bing, switchable with a click
- Ein/ausblendbar über Settings - Toggleable via Settings
### Sticky Note ### Widget System
- Schwebendes Notiz-Widget, frei positionierbar - **Notes & Checklists** — Floating note widgets with text or checklist template (max. 5)
- Text und Position werden persistent gespeichert - **Calculator** — Shunting-yard parser (no `eval()`), history, keyboard input
- **Timer / Countdown** — Saveable presets, Web Audio API alarm, mute toggle, tab title blinks on completion
- **Image Reference** — Images as floating reference widgets, Canvas API WebP conversion (max. 3, enable in Settings)
- **Notebook Sidebar** — All notes at a glance
- **Widget Toolbar** — Floating buttons for quick access, position (left/right) configurable in Settings
- All widgets: draggable, resizable, z-index stacking on click
### 8 Themes ### 11 Themes
| Theme | Akzent | Stil | | Theme | Accent | Style |
|---|---|---| |---|---|---|
| Nebula | `#b359ff` Magenta | Cosmic Nebula | | Nebula | `#b359ff` Magenta | Cosmic Nebula |
| Crescent | `#d4bd8a` Gold | Minimalist Night | | Crescent | `#d4bd8a` Gold | Minimalist Night |
@@ -64,60 +70,64 @@ Was angezeigt wird, ist was gespeichert ist. Keine Magie.
| SC Sunset | `#ff8c3d` Amber | Planet-Side | | SC Sunset | `#ff8c3d` Amber | Planet-Side |
| Hellion HUD | `#32ff6a` Neon Green | Circuit Board | | Hellion HUD | `#32ff6a` Neon Green | Circuit Board |
| Hellion Energy | `#1eff8e` Acid Green | Tactical | | Hellion Energy | `#1eff8e` Acid Green | Tactical |
| Satisfactory | `#00b4d8` Cyan | Industrial Desert |
| Avorion | `#2ec4a0` Turquoise | Deep Void |
| Hellion Stealth | `#5ec2ff` Tech Blue | Tactical Recon |
### Bild-Credits ### Image Credits
| Theme | Quelle | Lizenz | | Theme | Source | License |
|---|---|---| |---|---|---|
| Nebula | [Temel / mrwashingt0n](https://pixabay.com/de/users/mrwashingt0n-15745216/) auf Pixabay | Pixabay License (frei) | | Nebula | [Temel / mrwashingt0n](https://pixabay.com/de/users/mrwashingt0n-15745216/) on Pixabay | Pixabay License (free) |
| Crescent | [Daniil Silantev](https://unsplash.com) auf Unsplash | Unsplash License (frei) | | Crescent | [Daniil Silantev](https://unsplash.com) on Unsplash | Unsplash License (free) |
| Event Horizon | Eigenes Werk — Stillframe von [hellion-initiative.online](https://hellion-initiative.online) | Hellion Online Media | | Event Horizon | Own work, still frame from [hellion-initiative.online](https://hellion-initiative.online) | Hellion Online Media |
| Merchantman | [Roberts Space Industries](https://robertsspaceindustries.com) — Made by the community | RSI Community Content | | Merchantman | [Roberts Space Industries](https://robertsspaceindustries.com), made by the community | RSI Community Content |
| SC Sunset | Screenshot aus Star Citizen von Cloud Imperium Games | Fan Content | | SC Sunset | Screenshot from Star Citizen by Cloud Imperium Games | Fan Content |
| Julia & Jin | Eigenes Werk — Final Fantasy XIV Screenshot, bearbeitet in Photoshop | Hellion Online Media | | Julia & Jin | Own work, Final Fantasy XIV screenshot, edited in Photoshop | Hellion Online Media |
| Hellion HUD | Eigenes Werk — AI-generiert und nachbearbeitet für hellion-media.de | Hellion Online Media | | Hellion HUD | Own work, AI-generated and post-processed for hellion-media.de | Hellion Online Media |
| Hellion Energy | Eigenes Werk — AI-generiert für hellion-media.de | Hellion Online Media | | Hellion Energy | Own work, AI-generated for hellion-media.de | Hellion Online Media |
| Satisfactory | Screenshot from Satisfactory by Coffee Stain Studios | Fan Content |
| Avorion | Own work, screenshot from Avorion, Hellion Initiative ship | Hellion Online Media |
| Hellion Stealth | Screenshot from Star Citizen by Cloud Imperium Games | Fan Content |
### Onboarding & Dialoge ### Language Support (i18n)
- 6-stufiger Willkommens-Flow beim ersten Start - German and English with runtime switching via Settings
- Custom Frosted-Glass-Dialoge statt nativer Browser-Popups - Auto-detect from browser language, manual override available
- Backup-Reminder alle 7 Tage (warnt vor Datenverlust bei Browser-Reset) - All UI elements, dialogs, onboarding and widget labels fully translated
### Settings (Accordion) ### Onboarding & Dialogs
- Einklappbare Sektionen mit Chevron — About/Danger Zone standardmäßig geschlossen - 7-step welcome flow on first launch with widget explanation and optional gaming starter board
- Compact Mode — reduziert Abstände für mehr Bookmarks - Custom frosted glass dialogs instead of native browser popups
- Shorten Titles — kürzt lange Titel auf eine Zeile - Backup reminder every 7 days (warns about data loss on browser reset)
- Open in New Tab — Bookmarks in neuem Tab öffnen
- Show Descriptions — Beschreibungen unter Bookmarks anzeigen
- Hide Extra Bookmarks — Boards ab 5/10/20 Bookmarks einklappen
- Suchleiste ein/ausblenden
- JSON Export / Import
- Onboarding wiederholbar
- Danger Zone — Reset aller Daten
### Theme-Picker (eigener Header-Button) ### Appearance & Settings
- 8 Themes als zentriertes Modal - **Appearance modal** (header button), theme picker, background image and all display options in one modal
- Hintergrundbild per URL oder lokaler Upload - **Settings panel** (header button), widgets, data & help, danger zone
- **About footer**, developer info, license and support links permanently visible
- Compact mode, shorten titles, search bar toggle, open links in new tab, descriptions, hide extra bookmarks
- JSON export & import (backup & restore)
- Onboarding repeatable
- Language setting: German, English or auto-detect
--- ---
## Browser-Kompatibilität ## Browser Compatibility
| Browser | Status | Manifest | | Browser | Status | Manifest |
|---|---|---| |---|---|---|
| Chrome | ✅ Kompatibel | V3 (`manifest.json`) | | Chrome | ✅ Compatible | V3 (`manifest.json`) |
| Edge | ✅ Kompatibel | V3 (`manifest.json`) | | Edge | ✅ Compatible | V3 (`manifest.json`) |
| Brave | ✅ Kompatibel | V3 (`manifest.json`) | | Brave | ✅ Compatible | V3 (`manifest.json`) |
| Opera | ✅ Kompatibel | V3 (`manifest.opera.json`) | | Opera | ✅ Compatible | V3 (`manifest.opera.json`) |
| Opera GX | ✅ Kompatibel | V3 (`manifest.opera.json`) | | Opera GX | ✅ Compatible | V3 (`manifest.opera.json`) |
| Vivaldi | ✅ Kompatibel | V3 (`manifest.json`) | | Vivaldi | ✅ Compatible | V3 (`manifest.json`) |
| Firefox | ✅ Kompatibel | V3 (`manifest.firefox.json`) | | Firefox | ✅ Compatible | V3 (`manifest.firefox.json`) |
> **Firefox-Hinweis:** Ab v1.2.0 läuft die Extension auf Manifest V3 identisch zu Chrome/Edge. > **Firefox note:** From v1.2.0 onwards the extension runs on Manifest V3, identical to Chrome/Edge.
> `manifest.firefox.json` bleibt als separate Datei erhalten für Firefox-spezifische Anpassungen. > `manifest.firefox.json` remains a separate file for Firefox-specific adjustments.
--- ---
@@ -126,144 +136,159 @@ Was angezeigt wird, ist was gespeichert ist. Keine Magie.
### Chrome / Edge / Brave / Vivaldi ### Chrome / Edge / Brave / Vivaldi
```text ```text
1. Repository als ZIP herunterladen oder git clone 1. Download the repository as ZIP or git clone
2. chrome://extensions öffnen (bzw. edge:// / brave://) 2. Open chrome://extensions (or edge:// / brave://)
3. Entwicklermodus aktivieren 3. Enable developer mode
4. "Entpackte Erweiterung laden" → Ordner auswählen in dem manifest.json liegt 4. Click "Load unpacked" and select the folder containing manifest.json
5. Neuen Tab öffnen 5. Open a new tab
``` ```
### Opera / Opera GX ### Opera / Opera GX
```bash ```bash
# manifest.opera.json als manifest.json verwenden: # Use manifest.opera.json as manifest.json:
copy manifest.opera.json manifest.json # Windows copy manifest.opera.json manifest.json # Windows
cp manifest.opera.json manifest.json # Linux/Mac cp manifest.opera.json manifest.json # Linux/Mac
``` ```
```text ```text
1. opera://extensions öffnen 1. Open opera://extensions
2. Entwicklermodus aktivieren 2. Enable developer mode
3. "Entpackte Erweiterung laden" → Ordner auswählen 3. Click "Load unpacked" and select the folder
4. Neuen Tab öffnen 4. Open a new tab
``` ```
> **Opera-Hinweis:** Opera GX priorisiert Speed Dial — der enthaltene Workaround > **Opera note:** Opera GX prioritizes Speed Dial, the included workaround
> übernimmt die New-Tab-Seite zuverlässig. Details: [src/js/opera/README.md](src/js/opera/README.md) > takes over the new tab page reliably. Details: [src/js/opera/README.md](src/js/opera/README.md)
### Firefox ### Firefox
```bash ```bash
# manifest.firefox.json als manifest.json verwenden: # Use manifest.firefox.json as manifest.json:
copy manifest.firefox.json manifest.json # Windows copy manifest.firefox.json manifest.json # Windows
cp manifest.firefox.json manifest.json # Linux/Mac cp manifest.firefox.json manifest.json # Linux/Mac
``` ```
```text ```text
1. about:debugging#/runtime/this-firefox öffnen 1. Open about:debugging#/runtime/this-firefox
2. "Temporäres Add-on laden" 2. Click "Load Temporary Add-on"
3. Die manifest.json aus dem Projektordner auswählen 3. Select the manifest.json from the project folder
``` ```
> **Hinweis:** Temporäre Add-ons werden beim Browser-Neustart entfernt. > **Note:** Temporary add-ons are removed on browser restart.
> Für dauerhafte Installation ist eine signierte `.xpi`-Datei nötig. > For permanent installation a signed `.xpi` file is required.
--- ---
## Browser-Bookmarks exportieren & importieren ## Importing Browser Bookmarks
| Browser | Export-Pfad | | Browser | Export path |
|---|---| |---|---|
| Chrome / Edge | Einstellungen → Lesezeichen → Exportieren | | Chrome / Edge | Settings → Bookmarks → Export bookmarks |
| Firefox | Lesezeichen → Alle Lesezeichen → Importieren und Sichern → Als HTML exportieren | | Firefox | Bookmarks → All Bookmarks → Import and Backup → Export Bookmarks to HTML |
Die exportierte `.html`-Datei über den **Import**-Button in der Extension laden. Load the exported `.html` file via the **Import** button in the extension.
--- ---
## Datenschutz ## Privacy
- Keine externe Datenübertragung (außer Google Favicons API für Icons) - No external data transmission (except Google Favicons API for icons)
- Speicherung in `chrome.storage.local` (Chromium) bzw. `browser.storage.local` (Firefox) - Storage in `chrome.storage.local` (Chromium) or `browser.storage.local` (Firefox)
- Keine Tracker, keine Analytics, keine Werbung - No trackers, no analytics, no ads
- Keine Cookies, keine Session-Daten - No cookies, no session data
- Storage-Quota-Prüfung warnt bei 8 MB+ (Limit: 10 MB) - Storage quota check warns at 8 MB+ (limit: 10 MB)
- Permissions: `storage`, `bookmarks` - Permissions: `storage`, `bookmarks` (all browsers) + `tabs` (Opera / Opera GX only)
--- ---
## Tech-Stack ## Tech Stack
| Komponente | Details | | Component | Details |
|---|---| |---|---|
| Sprache | JavaScript (Vanilla ES2020, keine Frameworks) | | Language | JavaScript (Vanilla ES2020, no frameworks) |
| Styling | CSS Custom Properties (Theme-System) | | Styling | CSS Custom Properties (theme system) |
| Fonts | Lokale Fonts (Rajdhani, Inter, Cinzel) | | Fonts | Local fonts (Rajdhani, Inter, Cinzel) |
| Storage | `chrome.storage.local` / `localStorage` Fallback | | Storage | `chrome.storage.local` / `localStorage` fallback |
| Favicons | Google Favicons API (`/s2/favicons`) | | Favicons | Google Favicons API (`/s2/favicons`) |
| Drag & Drop | Pointer Events API (nativ) | | Drag & Drop | Pointer Events API (native) |
| Build | Kein Build-Schritt — direkt lauffähig | | Build | No build step, runs directly |
| CI/CD | GitHub Actions (Security, Quality, Release) | | CI/CD | GitHub Actions (security, quality, release) |
--- ---
## Architektur ## Architecture
```text ```text
hellion-newtab/ hellion-newtab/
├── manifest.json # Chrome, Edge, Brave, Vivaldi (MV3) ├── manifest.json # Chrome, Edge, Brave, Vivaldi (MV3)
├── manifest.firefox.json # Firefox (MV3) ├── manifest.firefox.json # Firefox (MV3)
├── manifest.opera.json # Opera / Opera GX (MV3 + Workaround) ├── manifest.opera.json # Opera / Opera GX (MV3 + workaround)
├── newtab.html # Haupt-HTML (UI-Struktur, Modals, Settings Panel) ├── newtab.html # Main HTML (UI structure, modals, settings panel)
├── LICENSE # CC BY-NC-SA 4.0 ├── LICENSE # CC BY-NC-SA 4.0
├── CHANGELOG.md # Versionshistorie ├── CHANGELOG.md # Version history
├── SECURITY.md # Sicherheitsrichtlinie und Meldeprozess ├── SECURITY.md # Security policy and reporting
├── DISCLAIMER.md # Haftungsausschluss ├── DISCLAIMER.md # Disclaimer and legal
├── _locales/
│ ├── de/messages.json # Manifest-level i18n (German)
│ └── en/messages.json # Manifest-level i18n (English)
├── src/ ├── src/
│ ├── js/ │ ├── js/
│ │ ├── storage.js # Storage Abstraction + Quota-Prüfung │ │ ├── storage.js # Storage abstraction + quota check
│ │ ├── state.js # Globaler State, Defaults, Hilfsfunktionen │ │ ├── state.js # Global state, defaults, helpers
│ │ ├── dialog.js # Custom Dialog-System (HellionDialog.alert/confirm) │ │ ├── i18n.js # Internationalization (DE/EN, ~220+ keys, t() helper)
│ │ ├── themes.js # Theme-Definitionen & Anwendungslogik │ │ ├── dialog.js # Custom dialog system (HellionDialog.alert/confirm)
│ │ ├── boards.js # Board/Bookmark Rendering, Event Delegation, Modals │ │ ├── themes.js # Theme definitions & application (11 themes)
│ │ ├── drag.js # Drag & Drop (Pointer Events, Board + Bookmark) │ │ ├── boards.js # Board/bookmark rendering, event delegation, modals
│ │ ├── settings.js # Settings Panel, Theme-Modal, Accordion │ │ ├── drag.js # Drag & drop (Pointer Events, board + bookmark)
│ │ ├── search.js # Suchleiste (Google, DuckDuckGo, Bing) │ │ ├── settings.js # Settings panel, appearance modal, accordion
│ │ ├── sticky.js # Sticky Note Widget (Drag, Persist, Toggle) │ │ ├── search.js # Search bar (Google, DuckDuckGo, Bing)
│ │ ├── data.js # JSON Export / Import mit Validierung │ │ ├── widgets.js # Widget manager (registry, drag, resize, z-index)
│ │ ├── onboarding.js # Mehrstufiger Willkommens-Flow │ │ ├── notes.js # Notes & checklists (multi-instance, max. 5)
│ │ ├── app.js # Init, Clock, globale Events (Einstiegspunkt) │ │ ├── calculator.js # Calculator (shunting-yard, history)
│ │ ── opera/ # Opera GX Workaround-Skripte │ │ ── timer.js # Timer/countdown (presets, Web Audio alarm)
│ │ ├── background.js # Tab-Management gegen Speed Dial │ │ ├── image-ref.js # Image reference widget (Canvas API, sessionStorage)
│ │ └── redirect.js # Content Script Redirect │ │ ├── data.js # JSON export / import with validation
│ │ ├── onboarding.js # 7-step welcome flow + gaming board
│ │ ├── app.js # Init, clock, global events (entry point)
│ │ └── opera/ # Opera GX workaround scripts
│ │ ├── background.js # Tab management against Speed Dial
│ │ └── redirect.js # Content script redirect
│ └── css/ │ └── css/
│ └── main.css # Styles + Theme-System + Responsive Breakpoints │ └── main.css # Styles + 11 themes + responsive breakpoints
├── assets/ ├── assets/
│ ├── fonts/ # Lokale Fonts (Rajdhani, Inter, Cinzel) │ ├── fonts/ # Local fonts (Rajdhani, Inter, Cinzel)
│ ├── themes/ # 8 Theme-Hintergrundbilder │ ├── themes/ # 11 theme background images (WebP only)
│ └── icons/ │ └── icons/
│ ├── icon16.png │ ├── icon16.png
│ ├── icon48.png │ ├── icon48.png
│ └── icon128.png │ └── icon128.png
├── docs/
│ ├── architecture.md # Project architecture and init sequence
│ ├── widget-schema.md # Widget system API and schema reference
│ ├── patterns.md # Code patterns and conventions
│ └── style-guide.md # Design system and theme documentation
└── .github/ └── .github/
└── workflows/ └── workflows/
├── security.yml # CodeQL-Analyse + Dependency Review ├── security.yml # CodeQL analysis + dependency review
├── quality.yml # Struktur, Manifest, Syntax, Versions-Konsistenz ├── quality.yml # Structure, manifest, syntax, version consistency
└── release.yml # ZIP-Pakete (Chrome + Firefox + Opera) + SHA256 └── release.yml # ZIP packages (Chrome + Firefox + Opera) + SHA256
``` ```
### Design-Prinzipien ### Design Principles
- **Zero Dependencies** — Kein npm, kein Build, kein Framework. Direkt lauffähig - **Zero Dependencies** — No npm, no build, no framework. Runs directly
- **Privacy First** — Alle Daten lokal, kein Server-Kontakt - **Privacy First** — All data local, no server contact
- **Modular** — 12 JS-Dateien mit klarer Zuständigkeit - **Modular** — 16 JS files with clear responsibilities
- **Responsive** — Tablet (768px) und Smartphone (480px) Breakpoints - **Responsive** — Tablet (768px) and smartphone (480px) breakpoints
- **Secure** — `createElement` statt `innerHTML`, URL-Validierung, Storage-Fehlerbehandlung - **Secure** — `createElement` instead of `innerHTML`, URL validation, storage error handling
- **Event Delegation** — Ein Listener pro Board-Liste statt pro Bookmark (Performance) - **Event Delegation** — One listener per board list instead of per bookmark (performance)
- **Theme-System** — CSS Custom Properties, 8 Themes, Custom-Background-Support - **Theme System** — CSS Custom Properties, 11 themes, custom background support
--- ---
@@ -271,85 +296,88 @@ hellion-newtab/
### Security Scan (`security.yml`) ### Security Scan (`security.yml`)
- **CodeQL-Analyse** — Statische Sicherheitsanalyse für JavaScript - **CodeQL analysis** — Static security analysis for JavaScript
- **Dependency Review** — Prüft Pull Requests auf bekannte Schwachstellen - **Dependency review** — Checks pull requests for known vulnerabilities
- **Zeitplan** — Automatisch wöchentlich (Montag 06:00 UTC) + bei Push/PR - **Schedule** — Automatically weekly (Monday 06:00 UTC) + on push/PR
### Code Quality (`quality.yml`) ### Code Quality (`quality.yml`)
- **Projektstruktur** — Alle Pflichtdateien und -ordner vorhanden - **Project structure** — All required files and folders present
- **Manifest-Validierung** — JSON-Syntax, Version, Permissions - **Manifest validation** — JSON syntax, version, permissions
- **JavaScript Syntax-Check** — `node --check` für alle JS-Dateien - **JavaScript syntax check** — `node --check` for all JS files
- **Versions-Konsistenz** — manifest.json, manifest.firefox.json und newtab.html müssen übereinstimmen - **Version consistency** — manifest.json, manifest.firefox.json and newtab.html must match
- **Icon-Prüfung** — Alle Extension-Icons vorhanden - **Icon check** — All extension icons present
### Release (`release.yml`) ### Release (`release.yml`)
- **Trigger** — Bei Git-Tag (`v*`) - **Trigger** — On Git tag (`v*`)
- **Pakete** — Chrome-ZIP + Firefox-ZIP + Opera-ZIP (alle MV3) - **Packages** — Chrome ZIP + Firefox ZIP + Opera ZIP (all MV3)
- **Checksummen** — SHA256 für alle Artefakte - **Checksums** — SHA256 for all artifacts
- **GitHub Release** — Automatisch mit Installationsanleitung - **GitHub Release** — Automatic with installation instructions
```bash ```bash
# Release erstellen: # Create a release:
git tag v1.5.2 git tag v2.0.0
git push origin v1.5.2 git push origin v2.0.0
# → GitHub Action erstellt automatisch Release mit ZIP-Dateien # → GitHub Action automatically creates release with ZIP files
``` ```
--- ---
## Entwicklung ## Development
```bash ```bash
# Repository klonen # Clone the repository
git clone https://github.com/JonKazama-Hellion/Hellion-NewTab.git git clone https://github.com/JonKazama-Hellion/Hellion-NewTab.git
# Extension im Browser laden (siehe Installation) # Load the extension in your browser (see Installation)
# Nach Änderungen: Extension neu laden # After changes: reload the extension
chrome://extensions → Hellion NewTab → Neu laden chrome://extensions → Hellion NewTab → Reload
``` ```
Kein Build-Schritt nötig. Dateien ändern, Extension neu laden, fertig. No build step needed. Change files, reload extension, done.
--- ---
## Sicherheit ## Security
Sicherheitslücken bitte **nicht** über öffentliche Issues melden. Please do **not** report security vulnerabilities through public GitHub issues.
Details zur Meldung, Reaktionszeiten und Sicherheitsarchitektur: [SECURITY.md](SECURITY.md) Details on reporting, response times and security architecture: [SECURITY.md](SECURITY.md)
--- ---
## Lizenz & Impressum ## License & Legal
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
- Kostenlos für private Nutzung - Free for private use
- Teilen und Modifikation erlaubt mit Namensnennung - Sharing and modification allowed with attribution
- Kommerzielle Nutzung ohne Erlaubnis verboten - Commercial use without permission prohibited
Vollständige Lizenz: [LICENSE](LICENSE) | [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/) Full license: [LICENSE](LICENSE) | [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/)
| | | | | |
|---|---| |---|---|
| **Entwickler** | Florian Wathling | | **Developer** | Florian Wathling |
| **Unternehmen** | Hellion Online Media | | **Company** | Hellion Online Media |
| **Web** | [hellion-media.de](https://hellion-media.de) | | **Web** | [hellion-media.de](https://hellion-media.de) |
| **Impressum** | [hellion-media.de/impressum](https://hellion-media.de/impressum) | | **Imprint** | [hellion-media.de/impressum](https://hellion-media.de/impressum) |
| **Bug Reports** | [kontakt@hellion-media.de](mailto:kontakt@hellion-media.de?subject=Hellion%20NewTab%20%E2%80%93%20Bug%20Report) | | **Bug Reports** | [kontakt@hellion-media.de](mailto:kontakt@hellion-media.de?subject=Hellion%20NewTab%20%E2%80%93%20Bug%20Report) |
| **Security** | [SECURITY.md](SECURITY.md) | | **Security** | [SECURITY.md](SECURITY.md) |
| **Support** | [Ko-fi](https://ko-fi.com/hellionmedia) | | **Support** | [Ko-fi](https://ko-fi.com/hellionmedia) |
--- ---
### Einsatz von AI ### Use of AI
AI (Claude Code, Opus 4.6 von Anthropic) wurde als Hilfsmittel eingesetzt — für Fehleridentifikation, Code-Review und Qualitätssicherung. Architektur, Features und alle Entscheidungen sind Eigenleistung. **Claude:** Code analysis, bug fixing, documentation and proofreading.
**Me:** Architecture, features and logic are planned, thought through and written by me.
Details: [DISCLAIMER.md](DISCLAIMER.md)
--- ---
> Vollständige Versionshistorie: [CHANGELOG.md](CHANGELOG.md) > Full version history: [CHANGELOG.md](CHANGELOG.md)
**Hellion NewTab** — [Hellion Online Media](https://hellion-media.de) — JonKazama-Hellion **Hellion NewTab** — [Hellion Online Media](https://hellion-media.de) — JonKazama-Hellion
+74 -58
View File
@@ -1,76 +1,92 @@
# Sicherheitsrichtlinie — Hellion NewTab # Security Policy — Hellion NewTab
## Unterstützte Versionen ## Supported Versions
| Version | Status | | Version | Status |
|---|---| |---|---|
| 1.2.x | Aktiv unterstützt | | 1.9.x | Actively supported |
| < 1.2.0 | Nicht unterstützt | | < 1.9.0 | Not supported |
## Sicherheitslücke melden ## Reporting a Vulnerability
Wenn du eine Sicherheitslücke in Hellion NewTab findest, melde sie bitte **nicht** über ein öffentliches GitHub Issue. If you find a security vulnerability in Hellion NewTab, please **do not** open a public GitHub issue.
### Kontakt ### Contact
**E-Mail:** [kontakt@hellion-media.de](mailto:kontakt@hellion-media.de?subject=Hellion%20NewTab%20%E2%80%93%20Security%20Report) **Email:** [kontakt@hellion-media.de](mailto:kontakt@hellion-media.de?subject=Hellion%20NewTab%20%E2%80%93%20Security%20Report)
Bitte folgende Informationen angeben: Please include the following information:
- Beschreibung der Schwachstelle - Description of the vulnerability
- Schritte zur Reproduktion - Steps to reproduce
- Betroffene Version(en) - Affected version(s)
- Mögliche Auswirkungen (Datenverlust, XSS, etc.) - Potential impact (data loss, XSS, etc.)
### Reaktionszeit ### Response Times
- **Bestätigung:** Innerhalb von 48 Stunden - **Acknowledgement:** Within 48 hours
- **Ersteinschätzung:** Innerhalb von 7 Tagen - **Initial assessment:** Within 7 days
- **Fix:** Abhängig von Schweregrad, Ziel innerhalb von 14 Tagen - **Fix:** Depends on severity, target within 14 days
### Schweregrad-Einstufung ### Severity Levels
| Stufe | Beschreibung | Beispiel | | Level | Description | Example |
|---|---|---| |---|---|---|
| Kritisch | Datenverlust oder Remote Code Execution | Storage-Manipulation durch Dritte | | Critical | Data loss or remote code execution | Storage manipulation by third parties |
| Hoch | XSS oder ungewollte Datenübertragung | Script-Injection via Bookmark-Import | | High | XSS or unintended data transmission | Script injection via bookmark import |
| Mittel | Umgehung von UI-Schutzmechanismen | Blur-Bypass, Settings-Manipulation | | Medium | UI protection bypass | Blur bypass, settings manipulation |
| Niedrig | Kosmetisch oder theoretisch | Edge-Cases ohne praktische Auswirkung | | Low | Cosmetic or theoretical | Edge cases without practical impact |
## Sicherheitsarchitektur
### Datenverarbeitung
- **Keine externe Datenübertragung** — Alle Daten bleiben in `chrome.storage.local`
- **Kein Server-Kontakt** — Außer Google Favicons API für Bookmark-Icons
- **Keine Cookies, Sessions oder Tokens**
- **Kein Netzwerkzugriff** außer Favicon-Abruf
### Eingabe-Validierung
- URL-Validierung bei Bookmark-Erstellung (`new URL()`)
- JSON-Import: Board- und Bookmark-Struktur wird validiert
- HTML-Sanitierung via `escHtml()` und `createElement` (kein `innerHTML` für User-Daten)
- Storage-Quota-Prüfung mit Warnung bei 8 MB+
### Permissions
Diese Extension benötigt nur zwei Browser-Permissions:
| Permission | Grund |
| --- | --- |
| `storage` | Boards, Settings und Sticky Note lokal speichern |
| `bookmarks` | Browser-Lesezeichen für HTML-Import lesen |
Keine Permissions für: Tabs, History, Web Requests, Downloads, Clipboard oder Host-Zugriff.
### CI/CD-Sicherheit
- **CodeQL** — Automatische statische Analyse bei Push und PR
- **Dependency Review** — Prüft auf bekannte Schwachstellen in PRs
- **Wöchentlicher Scan** — Automatischer CodeQL-Lauf jeden Montag
- **SHA256-Checksummen** — Alle Release-Artefakte werden signiert
--- ---
**Hellion Dashboard** — [Hellion Online Media - Florian Wathling](https://hellion-media.de) — JonKazama-Hellion ## Security Architecture
### Data Handling
- **No external data transmission** — all data stays in `chrome.storage.local`
- **No server contact** — except Google Favicons API for bookmark icons
- **No cookies, sessions or tokens**
- **No network access** beyond favicon fetching
### Input Validation
- URL validation on bookmark creation (`new URL()`)
- JSON import validates board and bookmark structure before applying
- HTML sanitization via `escHtml()` and `createElement` — no `innerHTML` for user data
- Storage quota check with warning at 8 MB+
### Permissions
This extension requests the following browser permissions:
| Permission | Browsers | Reason |
|---|---|---|
| `storage` | All | Store boards, settings and widget states locally |
| `bookmarks` | All | Read browser bookmarks for direct import |
| `tabs` | Opera / Opera GX only | Required for the Speed Dial workaround — `background.js` monitors tab URLs and redirects via `chrome.tabs.update` |
No permissions requested for: history, web requests, downloads, clipboard or host access.
### CI/CD Security
- **CodeQL** — Automatic static analysis on every push and PR
- **Dependency Review** — Checks for known vulnerabilities in PRs
- **Weekly scan** — Automated CodeQL run every Monday at 06:00 UTC
- **SHA256 checksums** — All release artifacts are checksummed
---
## Legal
Hellion NewTab is developed and maintained by **Florian Wathling / Hellion Online Media**,
based in Bad Harzburg, Germany.
All security matters are handled in accordance with **German and EU law**, including
the General Data Protection Regulation (GDPR / DSGVO). Users in the European Union
are covered by the same legal framework.
For legal inquiries: [hellion-media.de/impressum](https://hellion-media.de/impressum)
---
**Hellion Dashboard** — [Hellion Online Media — Florian Wathling](https://hellion-media.de) — JonKazama-Hellion
+4
View File
@@ -0,0 +1,4 @@
{
"extName": { "message": "Hellion NewTab" },
"extDesc": { "message": "Persönliches Bookmark-Dashboard mit Boards, Widgets und 11 Themes. Komplett lokal, keine Cloud, kein Tracking." }
}
+4
View File
@@ -0,0 +1,4 @@
{
"extName": { "message": "Hellion NewTab" },
"extDesc": { "message": "Personal bookmark dashboard with boards, widgets, and 11 themes. Local-only, no cloud, no tracking." }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

+316
View File
@@ -0,0 +1,316 @@
# Hellion Dashboard — Design & Theme System
> 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.
---
## Design Pillars
| Pillar | Description |
|---|---|
| **Immersion** | The interface feels like a HUD floating over the scene, not a foreign object sitting on top of it |
| **Visual Clarity** | Deliberate use of `blur` separates UI from background and reduces visual noise and cognitive load |
| **Harmony** | Every theme pulls its colors from the dominant light sources in its background image |
---
## Background Images — WebP Only
**All background images must be in WebP format.** This is an intentional architectural
decision to keep storage quota usage predictable and leave room for future features
(widgets, image references, etc.) that also compete for the 10 MB `chrome.storage` limit.
JPG, PNG and other formats are not accepted, so convert before adding a theme.
### Recommended Settings
| Quality | When to use |
|---|---|
| 85 | Default, good balance of size and sharpness |
| 80 | For images over 500 KB |
| 90 | For images with fine details (stars, in-game UI text) |
### Conversion Tools
**Squoosh** (squoosh.app) — browser-based, no install, nothing gets uploaded to external servers.
Drag in the image, pick WebP, set quality to 85, download. Done.
**cwebp** (command line):
```bash
cwebp -q 85 input.jpg -o output.webp
```
### Current Theme Images
| File | Status |
|---|---|
| `bg-nebula.webp` | ✅ WebP |
| `bg-crescent.webp` | ✅ WebP |
| `bg-event-horizon.webp` | ✅ WebP |
| `bg-merchantman.webp` | ✅ WebP |
| `bg-julia-jin.webp` | ✅ WebP |
| `bg-sc-sunset.webp` | ✅ WebP |
| `bg-hellion-hud.webp` | ✅ WebP |
| `bg-hellion-energy.webp` | ✅ WebP |
| `bg-satisfactory.webp` | ✅ WebP |
| `bg-avorion.webp` | ✅ WebP |
| `bg-scPolaris.webp` | ✅ WebP |
---
## Anatomy of a Theme
Every theme lives in `main.css` as a `[data-theme="name"]` block. Copy this template
to add a new one:
```css
[data-theme="your-theme-name"] {
/* 1. ACCENTS — The light source */
--accent: #HEXCODE; /* Main color (neon/light) */
--accent-dim: rgba(R, G, B, 0.12); /* Subtle background tint */
--accent-glow: rgba(R, G, B, 0.08); /* Glow for logo & clock */
--border-accent: rgba(R, G, B, 0.25); /* Focus ring */
/* 2. BASE — The foundation */
--bg-primary: #HEXCODE; /* Darkest point in the image */
--bg-board: rgba(R, G, B, 0.55); /* Glass effect on boards */
--border: rgba(R, G, B, 0.12); /* Default border */
/* 3. TEXT — Contrast */
--text-primary: #FFFFFF; /* Readable, slightly tinted */
--text-secondary: #A0A0A0; /* Desaturated, less visual weight */
--text-muted: #606060; /* Barely visible, for hints */
/* 4. OVERLAY — Vignette */
--overlay-bg: radial-gradient(
circle at center,
transparent 0%,
var(--bg-primary) 100%
);
/* 5. COMPONENT COLORS */
--header-bg: rgba(R, G, B, 0.94);
--board-hover-border: rgba(R, G, B, 0.22);
--toggle-on-bg: rgba(R, G, B, 0.20);
--logo-shadow: rgba(R, G, B, 0.50);
/* 6. FONTS */
--font-display: 'Rajdhani', sans-serif;
--font-body: 'Inter', sans-serif;
}
/* Theme-specific overrides */
[data-theme="your-theme-name"] .logo { letter-spacing: 4px; }
[data-theme="your-theme-name"] .clock { color: var(--accent); }
[data-theme="your-theme-name"] .board-title { text-transform: uppercase; }
[data-theme="your-theme-name"] .board { backdrop-filter: blur(8px); }
[data-theme="your-theme-name"] .bm-item:hover { background: var(--accent-dim); }
```
After adding the CSS block, register the theme in `src/js/themes.js` and add a preview entry in the theme picker.
---
## UI Patterns
### Frosted Glass
Hardware-accelerated blur for readability on complex backgrounds:
```css
backdrop-filter: blur(8px);
```
Creates depth and visual calm behind text and UI elements. Standard value is `8px`. Only increase it when the background image has a lot of fine detail that competes with the UI.
### Clock Color
All themes set `color: var(--accent)` on the clock element. This is a consistent
detail across the entire theme system. Don't skip it for new themes.
```css
[data-theme="your-theme"] .clock { color: var(--accent); }
```
### Typography Hierarchy
| Font | Usage |
|---|---|
| **Rajdhani** | Display: clock, logo, titles. Anything that should feel like a system readout |
| **Inter** | Body: bookmark titles, lists, interactive elements |
| **Cinzel** | Fantasy: reserved for themes with a majestic or ancient aesthetic (Crescent, Julia & Jin) |
### Overlay Strategy
The overlay gradient determines what stays visible in the background image.
**Radial (default)** draws attention to the center and darkens edges:
```css
--overlay-bg: radial-gradient(circle at center, transparent 0%, var(--bg-primary) 100%);
```
**Linear** darkens top and bottom and leaves the middle open. Use when the subject
is horizontally centered and should stay visible (Satisfactory factory floor, SC Sunset horizon):
```css
--overlay-bg: linear-gradient(180deg, rgba(R,G,B,0.85) 0%, rgba(R,G,B,0.15) 50%, rgba(R,G,B,0.90) 100%);
```
Choose based on where the most important part of the image is, not by habit.
---
## Focus & Accessibility
For backgrounds with a lot of detail (many small elements, high contrast, busy textures),
increase board alpha and blur to reduce visual noise. This makes boards easier to scan,
especially for users with ADHD or attention sensitivities.
```css
--bg-board: rgba(R, G, B, 0.65); /* Up from default 0.55 */
backdrop-filter: blur(12px); /* Up from default 8px */
```
This was applied intentionally to the Satisfactory theme, because the factory floor screenshot
has a lot going on and needed more visual separation between background and UI.
---
## All 11 Themes
| Theme | File | Accent | Mood | Overlay |
|---|---|---|---|---|
| Nebula | `bg-nebula.webp` | `#b359ff` Magenta | Chill, Cosmic | Radial |
| Crescent | `bg-crescent.webp` | `#d4bd8a` Gold | Luxury, Night | Radial |
| Event Horizon | `bg-event-horizon.webp` | `#9d5cff` Purple | Deep Space, Void | Radial |
| Merchantman | `bg-merchantman.webp` | `#2eb8b8` Emerald | Industrial, Alien | Radial |
| Julia & Jin | `bg-julia-jin.webp` | `#7db3ff` Aetherial Blue | FFXIV Night | Linear |
| SC Sunset | `bg-sc-sunset.webp` | `#ff8c3d` Amber | Emotional, Horizon | Linear |
| Hellion HUD | `bg-hellion-hud.webp` | `#32ff6a` Neon Green | Tactical, Admin | Radial |
| Hellion Energy | `bg-hellion-energy.webp` | `#1eff8e` Acid Green | Overdrive, Power | Radial |
| Satisfactory | `bg-satisfactory.webp` | `#00b4d8` Cyan | Industrial Desert | Linear |
| Avorion | `bg-avorion.webp` | `#2ec4a0` Turquoise | Deep Void | Radial |
| Hellion Stealth | `bg-scPolaris.webp` | `#5ec2ff` Tech Blue | Tactical Recon | Radial |
### Theme Quirks Worth Knowing
**Julia & Jin** uses `Cinzel` as display font and a linear gradient. The subjects in
the screenshot are positioned left of center, so radial would soften them.
**Satisfactory** has increased board alpha (0.65) and stronger blur (12px), an intentional
ADHD optimization for a visually busy background.
**Avorion** uses `letter-spacing: 6px` on the logo for maximum HUD feel.
**Hellion Stealth** is the only theme with `border-left: 2px solid var(--accent)` on
`.bm-item:hover`. Every other theme uses background tinting only. This is intentional
and gives Stealth its tactical scanner character. Don't apply it to other themes.
---
## Registering a Theme in themes.js
The `THEMES` object in `src/js/themes.js` is the single source of truth for which
themes exist and which background image they use. CSS handles all the visual variables —
`themes.js` only needs the image path.
```javascript
const THEMES = {
'nebula': { bg: 'assets/themes/bg-nebula.webp' },
'crescent': { bg: 'assets/themes/bg-crescent.webp' },
'event-horizon': { bg: 'assets/themes/bg-event-horizon.webp' },
'merchantman': { bg: 'assets/themes/bg-merchantman.webp' },
'julia-jin': { bg: 'assets/themes/bg-julia-jin.webp' },
'sc-sunset': { bg: 'assets/themes/bg-sc-sunset.webp' },
'hellion-hud': { bg: 'assets/themes/bg-hellion-hud.webp' },
'hellion-energy': { bg: 'assets/themes/bg-hellion-energy.webp' },
'satisfactory': { bg: 'assets/themes/bg-satisfactory.webp' },
'avorion': { bg: 'assets/themes/bg-avorion.webp' },
'hellion-stealth': { bg: 'assets/themes/bg-scPolaris.webp' }
};
```
To add a new theme, add one line. The key must exactly match the `data-theme`
attribute in the CSS block. If they don't match, `applyTheme()` will silently
do nothing and no one will know why.
```javascript
// New theme: key must match [data-theme="your-theme-name"] in main.css
'your-theme-name': { bg: 'assets/themes/bg-your-theme.webp' }
```
### How applyTheme() works
```javascript
function applyTheme(themeName, skipBgOverride) {
const theme = THEMES[themeName];
if (!theme) return;
// Sets data-theme on <html> — activates the matching CSS variable block
document.documentElement.setAttribute('data-theme', themeName);
// Applies the background image unless a custom background is active
if (!skipBgOverride) {
document.getElementById('bgLayer').style.backgroundImage = `url('${theme.bg}')`;
}
// Updates the active state in the theme picker UI
document.querySelectorAll('.theme-card').forEach(card => {
card.classList.toggle('active', card.dataset.value === themeName);
});
}
```
The `skipBgOverride` flag exists for one specific case: when a user has set a custom
background image, switching themes should still update the CSS variables and the picker
UI, but not wipe their custom image. Pass `true` to skip the background update.
---
## Adding a Theme Card to newtab.html
The theme picker modal lives in `newtab.html` as `#themeOverlay`. Every theme
needs a card in the `.theme-grid` — without it the theme exists in CSS and JS
but never shows up in the UI.
Copy this block and add it inside `.theme-grid`, after the last existing card:
```html
<div class="theme-card" data-value="your-theme-name">
<img class="theme-card-img" src="assets/themes/bg-your-theme.webp" alt="Your Theme" />
<span class="theme-card-label">Your Theme</span>
<span class="theme-card-check"></span>
</div>
```
Three things that must match exactly:
- `data-value` must match the key in `THEMES` in `themes.js`
- `data-value` must match the `[data-theme="..."]` attribute in `main.css`
- `src` must point to the correct WebP file in `assets/themes/`
The label shown in the picker can be shorter than the full theme name — "HUD" and
"Energy" are good examples of that. Keep it short enough to fit the card.
The `active` class is toggled by `applyTheme()` automatically, so don't add it
manually unless you want that theme to be the default on first load (Nebula currently
has it as fallback).
---
## Adding a New Theme — Checklist
- [ ] Background image converted to WebP (quality 85)
- [ ] Image added to `assets/themes/`
- [ ] CSS block added to `src/css/main.css`
- [ ] Theme registered in `src/js/themes.js` (one line, key + bg path)
- [ ] Theme card added to `.theme-grid` in `newtab.html` (data-value, img src, label)
- [ ] Theme added to theme table in `README.md`
- [ ] Theme added to theme table in this document
- [ ] Image credit added to Bild-Credits table in `README.md`
- [ ] `CHANGELOG.md` entry added
---
Developed by **[Hellion Online Media — Florian Wathling](https://hellion-media.de)** — JonKazama-Hellion
+45 -33
View File
@@ -1,11 +1,17 @@
# Hellion Dashboard — Architecture # Hellion Dashboard — Architecture
> 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 ## 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 `<script>` tags. 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 `<script>` tags.
**Storage:** `chrome.storage.local` with `localStorage` fallback. **Storage:** `chrome.storage.local` with `localStorage` fallback.
**Manifest:** V3 for Chromium browsers, V3 for Firefox (separate manifest). **Manifest:** V3 across all supported browsers (separate files for Firefox and Opera GX).
--- ---
@@ -14,18 +20,20 @@ Hellion Dashboard is a browser extension (NewTab replacement) built with **Vanil
``` ```
HOM_NewTab_Project/ HOM_NewTab_Project/
├── newtab.html # Single HTML entry point ├── newtab.html # Single HTML entry point
├── manifest.json # Chrome/Edge/Brave/Vivaldi (MV3) ├── manifest.json # Chrome, Edge, Brave, Vivaldi (MV3)
├── manifest.firefox.json # Firefox (MV3) ├── manifest.firefox.json # Firefox (MV3)
├── manifest.opera.json # Opera/Opera GX (MV3 + workarounds) ├── manifest.opera.json # Opera, Opera GX (MV3 + workarounds)
├── src/ ├── src/
│ ├── css/ │ ├── css/
│ │ └── main.css # All styles, themes, responsive breakpoints │ │ └── main.css # All styles, 11 themes, responsive breakpoints
│ └── js/ │ └── js/
│ ├── storage.js # Storage abstraction layer │ ├── storage.js # Storage abstraction layer
│ ├── state.js # Global state, defaults, helpers │ ├── state.js # Global state, defaults, helpers
│ ├── themes.js # Theme definitions & application │ ├── i18n.js # Internationalization (DE/EN, t() helper)
│ ├── boards.js # Board/bookmark rendering & events │ ├── dialog.js # Custom dialog system (alert, confirm)
│ ├── themes.js # Theme definitions & application (11 themes)
│ ├── drag.js # Drag & drop (Pointer Events API) │ ├── drag.js # Drag & drop (Pointer Events API)
│ ├── boards.js # Board/bookmark rendering & events
│ ├── settings.js # Settings panel, toggles, theme picker │ ├── settings.js # Settings panel, toggles, theme picker
│ ├── search.js # Search bar (Google, DuckDuckGo, Bing) │ ├── search.js # Search bar (Google, DuckDuckGo, Bing)
│ ├── widgets.js # Widget manager (registry, drag, resize) │ ├── widgets.js # Widget manager (registry, drag, resize)
@@ -33,40 +41,43 @@ HOM_NewTab_Project/
│ ├── calculator.js # Calculator widget (single-instance) │ ├── calculator.js # Calculator widget (single-instance)
│ ├── timer.js # Timer/countdown widget (single-instance) │ ├── timer.js # Timer/countdown widget (single-instance)
│ ├── image-ref.js # Image reference widget (multi-instance) │ ├── image-ref.js # Image reference widget (multi-instance)
│ ├── onboarding.js # First-run onboarding flow │ ├── bookmark-import.js # Browser bookmark import (chrome.bookmarks API)
│ ├── data.js # JSON export/import (backup & restore) │ ├── data.js # JSON export/import (backup & restore)
│ ├── app.js # Init, clock, global events (entry point) │ ├── onboarding.js # First-run onboarding flow
│ └── dialog.js # Custom dialog system (alert, confirm) │ └── app.js # Init, clock, global events (entry point)
├── assets/ ├── assets/
│ ├── fonts/ # Local fonts (Rajdhani, Inter, Cinzel)
│ ├── icons/ # Extension icons (16-512px) │ ├── icons/ # Extension icons (16-512px)
│ └── themes/ # Theme background images │ └── themes/ # 11 theme background images
└── docs/ # Documentation (you are here) └── docs/ # You are here
``` ```
--- ---
## Module Responsibilities ## Module Responsibilities
Each module has exactly one responsibility. They communicate through global references (no import/export this is a browser extension without a bundler). Each module has exactly one responsibility. Communication happens through global references no import/export, because this is a browser extension without a bundler.
| Module | Responsibility | | Module | Responsibility |
|---|---| |---|---|
| `storage.js` | **Only** place that touches `chrome.storage` / `localStorage`. All other modules go through `Store.get()` / `Store.set()`. | | `storage.js` | The **only** place that touches `chrome.storage` / `localStorage`. Everything else goes through `Store.get()` / `Store.set()`. |
| `state.js` | Global `boards` and `settings` arrays, default values, `uid()`, `escHtml()`, `getFaviconUrl()`. | | `state.js` | Global `boards` and `settings` arrays, default values, `uid()`, `escHtml()`, `getFaviconUrl()`. |
| `themes.js` | Theme CSS variable application. 8 themes, each with its own `[data-theme]` block in CSS. | | `i18n.js` | Internationalization module. `STRINGS` object with ~220+ keys (DE/EN), `t(key, vars?)` helper, `applyLanguage()` DOM scanner, `setLanguage()`, `I18n.init()`. |
| `dialog.js` | `HellionDialog.alert()` and `HellionDialog.confirm()` — custom styled dialogs that replace native browser popups. |
| `themes.js` | Applies theme CSS variables. 11 themes, each with its own `[data-theme]` block in `main.css`. |
| `boards.js` | Renders boards and bookmarks. Event delegation on board containers. | | `boards.js` | Renders boards and bookmarks. Event delegation on board containers. |
| `drag.js` | Board and bookmark reordering via Pointer Events API. | | `drag.js` | Board and bookmark reordering via Pointer Events API. |
| `settings.js` | Settings panel UI, toggle handlers, theme modal, background upload. | | `settings.js` | Settings panel UI, toggle handlers, appearance modal, background upload. |
| `search.js` | Search bar with engine switching (Google, DuckDuckGo, Bing). | | `search.js` | Search bar with engine switching (Google, DuckDuckGo, Bing). |
| `widgets.js` | Widget manager — creates DOM, handles drag/resize/z-index, provides registry. See [widget-schema.md](widget-schema.md). | | `widgets.js` | Widget manager — creates DOM, handles drag/resize/z-index, provides registry. See [widget-schema.md](widget-schema.md). |
| `notes.js` | Notes and checklists as widgets. Multi-instance (max 5). Notebook sidebar. Also handles widget toolbar events. | | `notes.js` | Notes and checklists as widgets. Multi-instance (max 5). Notebook sidebar. Also handles widget toolbar events. |
| `calculator.js` | Calculator widget. Single-instance. Shunting-yard expression parser (no `eval()`). | | `calculator.js` | Calculator widget. Single-instance. Shunting-yard expression parser no `eval()`. |
| `timer.js` | Timer/countdown widget. Single-instance. Presets, Web Audio API alarm, tab-title blink. | | `timer.js` | Timer/countdown widget. Single-instance. Presets, Web Audio API alarm, tab-title blink on completion. |
| `image-ref.js` | Image reference widget. Multi-instance (max 3). Canvas API WebP conversion, sessionStorage for image data. | | `image-ref.js` | Image reference widget. Multi-instance (max 3). Canvas API WebP conversion, sessionStorage for image data — cleared on browser close. |
| `onboarding.js` | Multi-slide onboarding flow. Gaming starter board opt-in. | | `bookmark-import.js` | Direct browser bookmark import via `chrome.bookmarks.getTree()`. Folder selection modal with duplicate detection. |
| `data.js` | JSON export/import with validation. Handles boards, notes, calculator history, timer presets. | | `data.js` | JSON export/import with validation. Covers boards, notes, calculator history and timer presets. |
| `onboarding.js` | Multi-slide first-run flow including the gaming starter board opt-in. |
| `app.js` | Entry point. Calls `init()` on DOMContentLoaded. Clock, global event binding. | | `app.js` | Entry point. Calls `init()` on DOMContentLoaded. Clock, global event binding. |
| `dialog.js` | `HellionDialog.alert()` and `HellionDialog.confirm()` — custom styled dialogs replacing native browser dialogs. |
--- ---
@@ -83,7 +94,7 @@ DOMContentLoaded
→ bindGlobalEvents() # Header buttons, modals → bindGlobalEvents() # Header buttons, modals
→ bindSettingsEvents() # Settings toggles, theme picker → bindSettingsEvents() # Settings toggles, theme picker
→ initSearch() # Search bar → initSearch() # Search bar
→ migrateSticky() # Legacy sticky note migration → migrateSticky() # Legacy sticky note migration (v1.5.x → v1.6+)
→ Notes.init() # Notes + widget toolbar → Notes.init() # Notes + widget toolbar
→ Calculator.init() # Calculator widget → Calculator.init() # Calculator widget
→ Timer.init() # Timer widget → Timer.init() # Timer widget
@@ -96,29 +107,29 @@ DOMContentLoaded
## Script Load Order ## Script Load Order
Scripts are loaded in `newtab.html` in dependency order: Scripts are loaded in `newtab.html` in dependency order. A module may only reference modules loaded before it — there is no bundler to handle this automatically.
```html ```html
<script src="src/js/dialog.js"></script>
<script src="src/js/storage.js"></script> <script src="src/js/storage.js"></script>
<script src="src/js/state.js"></script> <script src="src/js/state.js"></script>
<script src="src/js/i18n.js"></script>
<script src="src/js/dialog.js"></script>
<script src="src/js/themes.js"></script> <script src="src/js/themes.js"></script>
<script src="src/js/boards.js"></script>
<script src="src/js/drag.js"></script> <script src="src/js/drag.js"></script>
<script src="src/js/boards.js"></script>
<script src="src/js/settings.js"></script> <script src="src/js/settings.js"></script>
<script src="src/js/search.js"></script> <script src="src/js/search.js"></script>
<script src="src/js/onboarding.js"></script>
<script src="src/js/widgets.js"></script> <script src="src/js/widgets.js"></script>
<script src="src/js/notes.js"></script> <script src="src/js/notes.js"></script>
<script src="src/js/calculator.js"></script> <script src="src/js/calculator.js"></script>
<script src="src/js/timer.js"></script> <script src="src/js/timer.js"></script>
<script src="src/js/image-ref.js"></script> <script src="src/js/image-ref.js"></script>
<script src="src/js/bookmark-import.js"></script>
<script src="src/js/data.js"></script> <script src="src/js/data.js"></script>
<script src="src/js/onboarding.js"></script>
<script src="src/js/app.js"></script> <script src="src/js/app.js"></script>
``` ```
**Rule:** A module may only reference modules loaded before it.
--- ---
## Z-Index Hierarchy ## Z-Index Hierarchy
@@ -133,7 +144,7 @@ Scripts are loaded in `newtab.html` in dependency order:
| Dialogs / Modals | 300 | `.hellion-dialog-overlay`, modals | | Dialogs / Modals | 300 | `.hellion-dialog-overlay`, modals |
| Onboarding | 400 | `#onboardingOverlay` | | Onboarding | 400 | `#onboardingOverlay` |
Widgets use incrementing z-index (`WidgetManager._topZ++`) to stack above each other on click. Widgets use an incrementing z-index (`WidgetManager._topZ++`) so the last clicked widget always sits on top.
--- ---
@@ -143,9 +154,9 @@ Widgets use incrementing z-index (`WidgetManager._topZ++`) to stack above each o
|---|---|---| |---|---|---|
| `boards` | Array | Board objects with bookmarks | | `boards` | Array | Board objects with bookmarks |
| `settings` | Object | User preferences (theme, toggles, etc.) | | `settings` | Object | User preferences (theme, toggles, etc.) |
| `widgetStates` | Object | All widget data (see [widget-schema.md](widget-schema.md)) | | `widgetStates` | Object | All widget data see [widget-schema.md](widget-schema.md) |
| `onboardingDone` | Boolean | Whether onboarding has been completed | | `onboardingDone` | Boolean | Whether the first-run onboarding has been completed |
| `lastBackupReminder` | Number | Timestamp of last backup reminder | | `lastBackupReminder` | Number | Timestamp of the last backup reminder |
--- ---
@@ -160,4 +171,5 @@ Widgets use incrementing z-index (`WidgetManager._topZ++`) to stack above each o
| Opera / GX | Chromium MV3 | `manifest.opera.json` | | Opera / GX | Chromium MV3 | `manifest.opera.json` |
| Firefox | Gecko MV3 | `manifest.firefox.json` | | Firefox | Gecko MV3 | `manifest.firefox.json` |
Changes affecting manifest fields must be synchronized across all three manifest files. Any change that touches manifest fields — version numbers, permissions, content scripts —
needs to be applied to all three files. The CI quality check will catch it if they drift out of sync.
+41 -43
View File
@@ -1,5 +1,11 @@
# Hellion Dashboard — Code Patterns & Conventions # Hellion Dashboard — Code Patterns & Conventions
> 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.
---
## Core Principles ## Core Principles
- **Vanilla JS ES2020** — No frameworks, no TypeScript, no build step - **Vanilla JS ES2020** — No frameworks, no TypeScript, no build step
@@ -15,7 +21,7 @@
**File:** `src/js/storage.js` **File:** `src/js/storage.js`
All persistent data goes through the `Store` object. Never access `chrome.storage` or `localStorage` directly. All persistent data goes through the `Store` object. Never access `chrome.storage` or `localStorage` directly`Store` handles the fallback between the two transparently and provides unified error handling when storage is full.
```javascript ```javascript
// Reading // Reading
@@ -30,13 +36,11 @@ await Store.set('settings', settings);
await Store.checkQuota(); 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 ## Pattern: Event Delegation
Instead of attaching listeners to each element, attach one to the container and use `closest()` to find the target. One listener on the container, `closest()` to find the target. Much cleaner than attaching a listener to every single element, and it works automatically for dynamically added content.
```javascript ```javascript
// GOOD — one listener, handles all bookmarks // GOOD — one listener, handles all bookmarks
@@ -53,13 +57,13 @@ bookmarks.forEach(bm => {
}); });
``` ```
**Used in:** `boards.js` (board/bookmark events), `notes.js` (toolbar), `calculator.js` (button grid) Used in `boards.js` (board/bookmark events), `notes.js` (toolbar) and `calculator.js` (button grid).
--- ---
## Pattern: createElement over innerHTML ## Pattern: createElement over innerHTML
Always build DOM with `document.createElement()`. This prevents XSS and is the project's #1 security rule. Always build DOM with `document.createElement()`. This is the project's #1 security rule`innerHTML` with user-provided content is an XSS risk, full stop.
```javascript ```javascript
// GOOD // GOOD
@@ -76,7 +80,7 @@ container.innerHTML = `<a href="${url}">${title}</a>`;
## Pattern: Shared Storage Key ## Pattern: Shared Storage Key
Multiple widget modules share the `widgetStates` key. Every module must read-before-write and preserve other modules' data. All widget modules share the `widgetStates` storage key. Every module that writes to it must read first and preserve what's already there — otherwise modules silently overwrite each other's data.
```javascript ```javascript
async save() { async save() {
@@ -85,7 +89,7 @@ async save() {
// Write your own data // Write your own data
data.yourKey = { /* ... */ }; data.yourKey = { /* ... */ };
// DON'T overwrite — the key already contains other modules' data // Don't replace the whole object — other modules live here too
await Store.set('widgetStates', data); await Store.set('widgetStates', data);
} }
``` ```
@@ -96,13 +100,13 @@ See [widget-schema.md](widget-schema.md) for the full `widgetStates` structure.
## Pattern: Widget Lifecycle Hooks ## 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()`: Single-instance widgets (Calculator, Timer) need to react when they're closed, minimized, or reopened. They do this by wrapping `WidgetManager` methods in their `init()`.
```javascript ```javascript
async init() { async init() {
// Wrap close
const prevClose = WidgetManager.close; const prevClose = WidgetManager.close;
const self = this; const self = this;
WidgetManager.close = function(id) { WidgetManager.close = function(id) {
prevClose.call(WidgetManager, id); prevClose.call(WidgetManager, id);
if (id === self.WIDGET_ID) { if (id === self.WIDGET_ID) {
@@ -110,7 +114,6 @@ async init() {
} }
}; };
// Wrap minimize
const prevMinimize = WidgetManager.minimize; const prevMinimize = WidgetManager.minimize;
WidgetManager.minimize = async function(id) { WidgetManager.minimize = async function(id) {
await prevMinimize.call(WidgetManager, id); await prevMinimize.call(WidgetManager, id);
@@ -122,13 +125,13 @@ async init() {
} }
``` ```
**Important:** Multiple widgets chain these wraps. Calculator wraps first, Timer wraps Calculator's already-wrapped version, and so on. The chain must not break. Multiple widgets chain these wraps Calculator wraps first, Timer wraps Calculator's already-wrapped version, and so on. Always call the previous method (`prevClose.call(...)`) or the chain breaks and other widgets stop responding.
--- ---
## Pattern: Debounced Save ## Pattern: Debounced Save
For frequent updates (typing in notes, moving widgets), use debounced saves to avoid excessive storage writes: For frequent updates like typing in notes or dragging widgets, debouncing avoids hammering storage with a write on every keystroke.
```javascript ```javascript
_saveTimer: null, _saveTimer: null,
@@ -138,20 +141,20 @@ _debouncedSave() {
this._saveTimer = setTimeout(() => this.save(), 500); this._saveTimer = setTimeout(() => this.save(), 500);
} }
// Usage: call _debouncedSave() instead of save() for frequent events // Use _debouncedSave() instead of save() for frequent events
textarea.addEventListener('input', () => { textarea.addEventListener('input', () => {
noteData.content = textarea.value; noteData.content = textarea.value;
this._debouncedSave(); this._debouncedSave();
}); });
``` ```
**Used in:** `notes.js` (text editing), `image-ref.js` (label editing) Used in `notes.js` (text editing) and `image-ref.js` (label editing).
--- ---
## Pattern: Theme System ## Pattern: Theme System
All themes use CSS Custom Properties defined in `[data-theme="name"]` blocks: All themes use CSS Custom Properties in `[data-theme="name"]` blocks in `main.css`. There are currently 11 themes.
```css ```css
[data-theme="nebula"] { [data-theme="nebula"] {
@@ -164,55 +167,51 @@ All themes use CSS Custom Properties defined in `[data-theme="name"]` blocks:
} }
``` ```
**Never hardcode colors in JS.** Use CSS classes or variables: Never hardcode colors in JS. Let CSS handle it.
```javascript ```javascript
// GOOD — let CSS handle colors // GOOD
element.classList.add('active'); element.classList.add('active');
// BAD — hardcoded color // BAD — breaks every theme that isn't Nebula
element.style.color = '#7db3ff'; element.style.color = '#7db3ff';
``` ```
8 themes are available: Nebula, Crescent, Event Horizon, Merchantman, Julia & Jin, SC Sunset, Hellion HUD, Hellion Energy.
--- ---
## Pattern: Onboarding Slides ## Pattern: Onboarding Slides
The onboarding system (`onboarding.js`) uses a data-driven slide array. Each slide is an object with rendering hints: The onboarding system in `onboarding.js` is data-driven. Each slide is a plain object — add a new slide by adding an object to the `slides` array, the `_render()` method handles the rest.
```javascript ```javascript
{ {
hero: '🎮', // Large emoji/icon hero: '🎮', // Large emoji/icon
title: 'Slide Title', // Heading title: 'Slide Title',
text: 'Description...', // Optional text paragraph text: 'Optional description',
features: ['Item 1', ...], // Optional bullet list features: ['Item 1', ...], // Optional bullet list
showThemes: true, // Optional theme grid showThemes: true, // Optional theme grid
interactive: 'gaming-board' // Optional custom buttons 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 ## Pattern: Dialog System
Custom dialogs replace native `alert()` and `confirm()`: Custom dialogs replace native `alert()` and `confirm()` everywhere in the project.
```javascript ```javascript
// Alert (informational) // Informational
await HellionDialog.alert('Message text', { await HellionDialog.alert('Message text', {
type: 'info', // 'info', 'success', 'warning', 'danger' type: 'info', // 'info', 'success', 'warning', 'danger'
title: 'Title' title: 'Title'
}); });
// Confirm (yes/no) // Yes/no
const ok = await HellionDialog.confirm('Are you sure?', { const ok = await HellionDialog.confirm('Are you sure?', {
type: 'danger', type: 'danger',
title: 'Delete', title: 'Delete',
confirmText: 'Delete', // Custom button text confirmText: 'Delete',
cancelText: 'Cancel' cancelText: 'Cancel'
}); });
if (ok) { /* user confirmed */ } if (ok) { /* user confirmed */ }
@@ -222,7 +221,7 @@ if (ok) { /* user confirmed */ }
## Pattern: Pointer Events for Drag ## Pattern: Pointer Events for Drag
Widget dragging and board reordering use the Pointer Events API (not mouse events): Widget dragging and board reordering use the Pointer Events API instead of mouse events. The reason: Pointer Events work with both mouse and touch, and `setPointerCapture` keeps the events flowing even if the cursor leaves the element mid-drag.
```javascript ```javascript
element.addEventListener('pointerdown', (e) => { element.addEventListener('pointerdown', (e) => {
@@ -243,13 +242,11 @@ element.addEventListener('pointerdown', (e) => {
}); });
``` ```
**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 ## Pattern: Canvas API Image Processing
The image reference widget converts uploaded images to WebP for smaller size: The image reference widget converts uploaded images to WebP locally in the browser — no external service, no upload, nothing leaves the device.
```javascript ```javascript
_processFile(file) { _processFile(file) {
@@ -264,7 +261,7 @@ _processFile(file) {
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0); ctx.drawImage(img, 0, 0);
const webpUrl = canvas.toDataURL('image/webp', 0.85); const webpUrl = canvas.toDataURL('image/webp', 0.85);
URL.revokeObjectURL(objectUrl); URL.revokeObjectURL(objectUrl); // Always free the object URL
resolve(webpUrl); resolve(webpUrl);
}; };
@@ -278,7 +275,7 @@ _processFile(file) {
} }
``` ```
**Important:** Always call `URL.revokeObjectURL()` to free memory. Always call `URL.revokeObjectURL()` after the image has loaded — skipping it leaks memory.
--- ---
@@ -287,24 +284,25 @@ _processFile(file) {
| Rule | Rationale | | Rule | Rationale |
|---|---| |---|---|
| `createElement` only, never `innerHTML` | XSS prevention | | `createElement` only, never `innerHTML` | XSS prevention |
| All storage through `Store` | Browser compatibility | | All storage through `Store` | Browser compatibility + unified error handling |
| CSS variables, no hardcoded colors | Theme support | | CSS variables, no hardcoded colors | Theme support across all 11 themes |
| Event delegation | Performance, dynamic content | | Event delegation | Performance, works with dynamic content |
| `const`/`let`, never `var` | Block scoping | | `const`/`let`, never `var` | Block scoping |
| No external dependencies | Extension simplicity | | No external dependencies | Extension simplicity |
| No build step | Direct development | | No build step | Direct development, no toolchain to break |
| JSDoc comments on public functions | Documentation | | JSDoc comments on public functions | Documentation for contributors |
| URL validation before `href` | Security | | URL validation before `href` | Security |
| Error handling on storage operations | Graceful failure | | Error handling on storage operations | Graceful failure |
| `URL.revokeObjectURL()` after Canvas ops | Memory management |
--- ---
## Manifest Synchronization ## Manifest Synchronization
Three manifest files must stay in sync: Three manifest files must always stay in sync:
- `manifest.json` — Chrome, Edge, Brave, Vivaldi - `manifest.json` — Chrome, Edge, Brave, Vivaldi
- `manifest.firefox.json` — Firefox - `manifest.firefox.json` — Firefox
- `manifest.opera.json` — Opera, Opera GX - `manifest.opera.json` — Opera, Opera GX
When changing version numbers, permissions, or content script entries, update all three files. Version numbers, permissions and content script entries need to be updated in all three. The CI quality check will catch drift, but it's cleaner not to let it get there in the first place.
+46 -49
View File
@@ -1,8 +1,14 @@
# Hellion Dashboard — Widget Schema # 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 ## 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. 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.
--- ---
@@ -21,7 +27,7 @@ The widget system provides draggable, resizable floating panels managed by `Widg
### `create(type, config) → string` ### `create(type, config) → string`
Creates a widget and appends it to the DOM. Creates a widget and appends it to the DOM. Returns the widget ID.
```javascript ```javascript
const id = WidgetManager.create('note', { const id = WidgetManager.create('note', {
@@ -37,7 +43,7 @@ const id = WidgetManager.create('note', {
### `getBody(id) → HTMLElement | null` ### `getBody(id) → HTMLElement | null`
Returns the `.widget-body` element for content rendering. Returns the `.widget-body` element. This is where your module renders its content.
```javascript ```javascript
const body = WidgetManager.getBody('widget_calculator'); const body = WidgetManager.getBody('widget_calculator');
@@ -46,7 +52,7 @@ if (body) Calculator.renderBody(body);
### `getState(id) → Object | null` ### `getState(id) → Object | null`
Returns the current widget state (position, size, open status). Returns the current widget state position, size, open status.
```javascript ```javascript
const state = WidgetManager.getState('widget_timer'); const state = WidgetManager.getState('widget_timer');
@@ -55,11 +61,11 @@ const state = WidgetManager.getState('widget_timer');
### `close(id)` ### `close(id)`
Permanently removes a widget from the DOM and registry. Permanently removes a widget from the DOM and registry. No undo.
### `minimize(id)` ### `minimize(id)`
Hides a widget with animation. Widget remains in registry with `open: false`. Hides a widget with animation. The widget stays in the registry with `open: false` so it can be restored.
### `openWidget(id)` ### `openWidget(id)`
@@ -67,24 +73,24 @@ Restores a minimized widget with animation.
### `bringToFront(id)` ### `bringToFront(id)`
Increments z-index to bring widget above all others. Increments z-index so the widget sits above everything else. Called automatically on `pointerdown`.
### `save() → Array` ### `save() → Array`
Returns an array of all `type: 'note'` widget states. Used by `Notes.save()` to merge with note content data. Returns an array of all `type: 'note'` widget states. Used by `Notes.save()` to merge position/size data with note content.
### `restore(renderCallback)` ### `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()`. 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` ## Shared Storage Key: `widgetStates`
All widget modules share a single storage key. Each module's `save()` method must preserve other modules' data. 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.
```javascript ```javascript
// Structure of widgetStates // Full widgetStates structure
{ {
notes: [ notes: [
{ {
@@ -95,7 +101,7 @@ All widget modules share a single storage key. Each module's `save()` method mus
x: 120, y: 80, x: 120, y: 80,
width: 280, height: 220, width: 280, height: 220,
open: true, open: true,
checklistItems: [], // For checklist template checklistItems: [], // Only used by checklist template
checkedItems: [] // Checked item IDs checkedItems: [] // Checked item IDs
} }
], ],
@@ -124,23 +130,24 @@ All widget modules share a single storage key. Each module's `save()` method mus
x: 200, y: 120, x: 200, y: 120,
width: 320, height: 280, width: 320, height: 280,
open: true open: true
// Image data is NOT stored here — sessionStorage only
} }
] ]
} }
} }
``` ```
### Save Pattern — Preserving Other Modules' Data ### The Save Pattern
Every module that saves to `widgetStates` must read existing data first and preserve keys it doesn't own: Every module that touches `widgetStates` must follow this pattern:
```javascript ```javascript
// Example from notes.js // From notes.js — same pattern applies to every widget module
async save() { async save() {
const existing = await Store.get(this.STORAGE_KEY); const existing = await Store.get(this.STORAGE_KEY);
const saveData = { notes: mergedNotes }; const saveData = { notes: mergedNotes };
// Preserve other modules // Preserve everything we don't own
if (existing && existing.calculator) saveData.calculator = existing.calculator; if (existing && existing.calculator) saveData.calculator = existing.calculator;
if (existing && existing.timer) saveData.timer = existing.timer; if (existing && existing.timer) saveData.timer = existing.timer;
if (existing && existing.imageRef) saveData.imageRef = existing.imageRef; if (existing && existing.imageRef) saveData.imageRef = existing.imageRef;
@@ -153,20 +160,21 @@ async save() {
## Creating a New Widget Type ## Creating a New Widget Type
### Step 1: Choose Single or Multi-Instance ### Step 1: Single or Multi-Instance?
- **Single-instance** (like Calculator, Timer): One widget with a fixed ID. `toggle()` opens/closes. **Single-instance** (Calculator, Timer style): one widget, fixed ID, `toggle()` opens and closes it.
- **Multi-instance** (like Notes, ImageRef): Multiple widgets with dynamic IDs. `create()` adds new ones. **Multi-instance** (Notes, ImageRef style): multiple widgets, dynamic IDs, `create()` adds new ones.
### Step 2: Create the Module (`src/js/your-widget.js`) ### 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.
```javascript ```javascript
const YourWidget = { const YourWidget = {
WIDGET_ID: 'widget_yourwidget', // Fixed ID for single-instance WIDGET_ID: 'widget_yourwidget',
STORAGE_KEY: 'widgetStates', STORAGE_KEY: 'widgetStates',
_isOpen: false, _isOpen: false,
// Load state from storage
async load() { async load() {
const data = await Store.get(this.STORAGE_KEY); const data = await Store.get(this.STORAGE_KEY);
if (data && data.yourWidget) { if (data && data.yourWidget) {
@@ -174,7 +182,6 @@ const YourWidget = {
} }
}, },
// Save state, preserving other modules
async save() { async save() {
const data = await Store.get(this.STORAGE_KEY) || {}; const data = await Store.get(this.STORAGE_KEY) || {};
if (data.notes === undefined) data.notes = []; if (data.notes === undefined) data.notes = [];
@@ -192,7 +199,6 @@ const YourWidget = {
await Store.set(this.STORAGE_KEY, data); await Store.set(this.STORAGE_KEY, data);
}, },
// Open widget
async open() { async open() {
if (this._isOpen) { if (this._isOpen) {
WidgetManager.bringToFront(this.WIDGET_ID); WidgetManager.bringToFront(this.WIDGET_ID);
@@ -219,7 +225,6 @@ const YourWidget = {
await this.save(); await this.save();
}, },
// Toggle open/close
async toggle() { async toggle() {
if (this._isOpen) { if (this._isOpen) {
const entry = WidgetManager._widgets.get(this.WIDGET_ID); const entry = WidgetManager._widgets.get(this.WIDGET_ID);
@@ -237,24 +242,23 @@ const YourWidget = {
} }
}, },
// Render widget content
renderBody(bodyEl) { renderBody(bodyEl) {
bodyEl.textContent = ''; bodyEl.textContent = '';
// Build your UI with createElement (never innerHTML!) // Build your UI with createElement never innerHTML!
}, },
// Initialize and hook into lifecycle
async init() { async init() {
await this.load(); await this.load();
// Restore if was open last time
const data = await Store.get(this.STORAGE_KEY); const data = await Store.get(this.STORAGE_KEY);
if (data && data.yourWidget && data.yourWidget.open) { if (data && data.yourWidget && data.yourWidget.open) {
await this.open(); await this.open();
} }
// Hook into close event // Lifecycle hooks — always call the previous method first
// or you'll break every widget that wrapped before yours
const self = this; const self = this;
const prevClose = WidgetManager.close; const prevClose = WidgetManager.close;
WidgetManager.close = function(id) { WidgetManager.close = function(id) {
prevClose.call(WidgetManager, id); prevClose.call(WidgetManager, id);
@@ -264,7 +268,6 @@ const YourWidget = {
} }
}; };
// Hook into minimize event
const prevMinimize = WidgetManager.minimize; const prevMinimize = WidgetManager.minimize;
WidgetManager.minimize = async function(id) { WidgetManager.minimize = async function(id) {
await prevMinimize.call(WidgetManager, id); await prevMinimize.call(WidgetManager, id);
@@ -274,7 +277,6 @@ const YourWidget = {
} }
}; };
// Hook into open event
const prevOpen = WidgetManager.openWidget; const prevOpen = WidgetManager.openWidget;
WidgetManager.openWidget = async function(id) { WidgetManager.openWidget = async function(id) {
await prevOpen.call(WidgetManager, id); await prevOpen.call(WidgetManager, id);
@@ -293,38 +295,33 @@ const YourWidget = {
### Step 3: Integration Checklist ### Step 3: Integration Checklist
1. **`newtab.html`** — Add `<script>` tag (after `widgets.js`, before `data.js`) 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">` 2. `newtab.html` — Add toolbar button: `<button class="widget-toolbar-btn" data-action="your-action">`
3. **`notes.js`** — Add toolbar handler in `initToolbar()`: `} else if (action === 'your-action') { YourWidget.toggle(); }` 3. `notes.js` — Add handler in `initToolbar()`: `else if (action === 'your-action') { YourWidget.toggle(); }`
4. **`notes.js`** — Preserve your data in `save()`: `if (existing && existing.yourWidget) saveData.yourWidget = existing.yourWidget;` 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 5. `app.js` — Add `await YourWidget.init();` to the init sequence
6. **`src/css/main.css`** — Add widget-specific CSS styles 6. `main.css` — Add widget-specific styles
7. **`data.js`** — Add export/import logic (if data should be included in backups) 7. `data.js` — Add export/import logic if your data should survive a JSON backup
--- ---
## Widget DOM Structure ## Widget DOM Structure
Every widget created by `WidgetManager.create()` has this 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.
```html ```html
<div class="widget" data-widget-id="widget_abc123" <div class="widget" data-widget-id="widget_abc123"
style="left: 120px; top: 80px; width: 280px; height: 220px;"> style="left: 120px; top: 80px; width: 280px; height: 220px;">
<div class="widget-header"> <div class="widget-header"> <!-- Drag handle -->
<span class="widget-title">Title</span> <span class="widget-title">Title</span> <!-- Double-click to edit, max 20 chars -->
<div class="widget-actions"> <div class="widget-actions">
<button class="widget-btn widget-minimize"></button> <button class="widget-btn widget-minimize"></button>
<button class="widget-btn widget-close"></button> <button class="widget-btn widget-close"></button>
</div> </div>
</div> </div>
<div class="widget-body"> <div class="widget-body">
<!-- Your content goes here (via renderBody) --> <!-- Your content goes here via renderBody() -->
</div> </div>
<div class="widget-resize-handle"></div> <div class="widget-resize-handle"></div> <!-- Bottom-right, visible on hover -->
</div> </div>
``` ```
- **Header** is the drag handle (Pointer Events)
- **Title** supports double-click to edit (contentEditable, max 20 chars)
- **Body** is where your module renders content
- **Resize handle** appears on hover (bottom-right corner)
+4 -3
View File
@@ -1,8 +1,9 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Hellion NewTab", "name": "__MSG_extName__",
"version": "1.9.0", "default_locale": "en",
"description": "Personal bookmark dashboard — local, private, no account needed. By Hellion Online Media.", "version": "2.0.0",
"description": "__MSG_extDesc__",
"author": "Hellion Online Media - Florian Wathling", "author": "Hellion Online Media - Florian Wathling",
"homepage_url": "https://hellion-media.de", "homepage_url": "https://hellion-media.de",
+4 -3
View File
@@ -1,8 +1,9 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Hellion NewTab", "name": "__MSG_extName__",
"version": "1.9.0", "default_locale": "en",
"description": "Personal bookmark dashboard — local, private, no account needed. By Hellion Online Media.", "version": "2.0.0",
"description": "__MSG_extDesc__",
"author": "Hellion Online Media - Florian Wathling", "author": "Hellion Online Media - Florian Wathling",
"homepage_url": "https://hellion-media.de", "homepage_url": "https://hellion-media.de",
"chrome_url_overrides": { "chrome_url_overrides": {
+4 -3
View File
@@ -1,8 +1,9 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Hellion Dashboard (GX Native)", "name": "__MSG_extName__",
"version": "1.9.0", "default_locale": "en",
"description": "Ersetzt die Opera GX Startseite durch dein persönliches, leistungsoptimiertes Hellion Dashboard. Schnell, sauber und werbefrei.", "version": "2.0.0",
"description": "__MSG_extDesc__",
"author": "Hellion Online Media - Florian Wathling", "author": "Hellion Online Media - Florian Wathling",
"homepage_url": "https://hellion-media.de", "homepage_url": "https://hellion-media.de",
+132 -86
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -25,23 +25,23 @@
<div class="header-right"> <div class="header-right">
<button class="btn-icon" id="btnImport" title="Bookmarks importieren (HTML)"> <button class="btn-icon" id="btnImport" title="Bookmarks importieren (HTML)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
Import <span data-i18n="header.import">Import</span>
</button> </button>
<button class="btn-icon" id="btnAddBoard" title="Neues Board hinzufügen"> <button class="btn-icon" id="btnAddBoard" title="Neues Board hinzufügen">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
Board <span data-i18n="header.board">Board</span>
</button> </button>
<button class="btn-icon" id="btnNote" title="Schnellnotiz"> <button class="btn-icon" id="btnNote" title="Schnellnotiz">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
Note <span data-i18n="header.note">Note</span>
</button> </button>
<button class="btn-icon" id="btnTheme" title="Darstellung & Theme"> <button class="btn-icon" id="btnTheme" title="Darstellung & Theme">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a10 10 0 100 20 4 4 0 01-1-7.9 1 1 0 011-.1h1a2 2 0 002-2V7a5 5 0 00-3-4.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="6" r="1.5"/><circle cx="17" cy="10" r="1.5"/><circle cx="9" cy="17" r="1.5"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a10 10 0 100 20 4 4 0 01-1-7.9 1 1 0 011-.1h1a2 2 0 002-2V7a5 5 0 00-3-4.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="6" r="1.5"/><circle cx="17" cy="10" r="1.5"/><circle cx="9" cy="17" r="1.5"/></svg>
Darstellung <span data-i18n="header.theme">Darstellung</span>
</button> </button>
<button class="btn-icon" id="btnSettings" title="Einstellungen"> <button class="btn-icon" id="btnSettings" title="Einstellungen">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
Settings <span data-i18n="header.settings">Settings</span>
</button> </button>
</div> </div>
</header> </header>
@@ -49,10 +49,10 @@
<!-- SEARCH BAR --> <!-- SEARCH BAR -->
<div class="search-bar-wrapper" id="searchBarWrapper"> <div class="search-bar-wrapper" id="searchBarWrapper">
<div class="search-bar"> <div class="search-bar">
<button class="search-engine-toggle" id="searchEngineToggle" title="Suchmaschine wechseln"> <button class="search-engine-toggle" id="searchEngineToggle" data-i18n-title="settings.search_engine_toggle" title="Suchmaschine wechseln">
<span id="searchEngineIcon">G</span> <span id="searchEngineIcon">G</span>
</button> </button>
<input type="text" class="search-input" id="searchInput" placeholder="Search the web…" autocomplete="off" /> <input type="text" class="search-input" id="searchInput" data-i18n-placeholder="search.placeholder" placeholder="Search the web…" autocomplete="off" />
<button class="search-submit" id="searchSubmit"> <button class="search-submit" id="searchSubmit">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg> <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
</button> </button>
@@ -61,22 +61,22 @@
<!-- WIDGET TOOLBAR --> <!-- WIDGET TOOLBAR -->
<div class="widget-toolbar" id="widgetToolbar"> <div class="widget-toolbar" id="widgetToolbar">
<button class="widget-toolbar-btn" data-action="new-note" title="Note erstellen"> <button class="widget-toolbar-btn" data-action="new-note" data-i18n-title="toolbar.note" title="Note erstellen">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
</button> </button>
<button class="widget-toolbar-btn" data-action="new-checklist" title="Checkliste erstellen"> <button class="widget-toolbar-btn" data-action="new-checklist" data-i18n-title="toolbar.checklist" title="Checkliste erstellen">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/></svg>
</button> </button>
<button class="widget-toolbar-btn" data-action="calculator" title="Taschenrechner"> <button class="widget-toolbar-btn" data-action="calculator" data-i18n-title="toolbar.calculator" title="Taschenrechner">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="8" y1="6" x2="16" y2="6"/><circle cx="8" cy="10" r="0.5"/><circle cx="12" cy="10" r="0.5"/><circle cx="16" cy="10" r="0.5"/><circle cx="8" cy="14" r="0.5"/><circle cx="12" cy="14" r="0.5"/><circle cx="16" cy="14" r="0.5"/><circle cx="8" cy="18" r="0.5"/><circle cx="12" cy="18" r="0.5"/><circle cx="16" cy="18" r="0.5"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="8" y1="6" x2="16" y2="6"/><circle cx="8" cy="10" r="0.5"/><circle cx="12" cy="10" r="0.5"/><circle cx="16" cy="10" r="0.5"/><circle cx="8" cy="14" r="0.5"/><circle cx="12" cy="14" r="0.5"/><circle cx="16" cy="14" r="0.5"/><circle cx="8" cy="18" r="0.5"/><circle cx="12" cy="18" r="0.5"/><circle cx="16" cy="18" r="0.5"/></svg>
</button> </button>
<button class="widget-toolbar-btn" data-action="timer" title="Timer"> <button class="widget-toolbar-btn" data-action="timer" data-i18n-title="toolbar.timer" title="Timer">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="13" r="8"/><path d="M12 9v4l2 2"/><path d="M5 3l2 2"/><path d="M19 3l-2 2"/><line x1="12" y1="1" x2="12" y2="3"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="13" r="8"/><path d="M12 9v4l2 2"/><path d="M5 3l2 2"/><path d="M19 3l-2 2"/><line x1="12" y1="1" x2="12" y2="3"/></svg>
</button> </button>
<button class="widget-toolbar-btn hidden" data-action="image-ref" title="Bild-Referenz"> <button class="widget-toolbar-btn hidden" data-action="image-ref" data-i18n-title="toolbar.imageref" title="Bild-Referenz">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>
</button> </button>
<button class="widget-toolbar-btn" data-action="notebook" title="Alle Notes"> <button class="widget-toolbar-btn" data-action="notebook" data-i18n-title="toolbar.notebook" title="Alle Notes">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 016.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 016.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z"/></svg>
</button> </button>
</div> </div>
@@ -85,7 +85,7 @@
<div class="notebook-overlay" id="notebookOverlay"></div> <div class="notebook-overlay" id="notebookOverlay"></div>
<aside class="notebook-panel" id="notebookPanel"> <aside class="notebook-panel" id="notebookPanel">
<div class="notebook-header"> <div class="notebook-header">
<span class="notebook-header-title">Notebook <span class="notebook-count" id="notebookCount">0 / 5</span></span> <span class="notebook-header-title"><span data-i18n="notebook.title">Notebook</span> <span class="notebook-count" id="notebookCount">0 / 5</span></span>
<button class="btn-close" id="btnCloseNotebook"></button> <button class="btn-close" id="btnCloseNotebook"></button>
</div> </div>
<div class="notebook-slots" id="notebookSlots"> <div class="notebook-slots" id="notebookSlots">
@@ -105,32 +105,53 @@
<div class="panel-overlay" id="settingsOverlay"></div> <div class="panel-overlay" id="settingsOverlay"></div>
<aside class="settings-panel" id="settingsPanel"> <aside class="settings-panel" id="settingsPanel">
<div class="panel-header"> <div class="panel-header">
<span>Einstellungen</span> <span data-i18n="settings.title">Einstellungen</span>
<button class="btn-close" id="btnCloseSettings"></button> <button class="btn-close" id="btnCloseSettings"></button>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<!-- SPRACHE -->
<section class="settings-section" data-section="language">
<button class="settings-section-title" type="button">
<span class="section-chevron"></span>
<span data-i18n="settings.section.display">DARSTELLUNG</span>
</button>
<div class="section-content">
<div class="setting-row">
<div class="setting-info">
<span class="setting-label" data-i18n="settings.language">Sprache</span>
<span class="setting-desc" data-i18n="settings.language.desc">Anzeigesprache wählen</span>
</div>
<select class="select-input" id="settingLanguage">
<option value="auto" data-i18n="settings.language.auto">Automatisch</option>
<option value="de">Deutsch</option>
<option value="en">English</option>
</select>
</div>
</div>
</section>
<!-- WIDGETS --> <!-- WIDGETS -->
<section class="settings-section" data-section="widgets"> <section class="settings-section" data-section="widgets">
<button class="settings-section-title" type="button"> <button class="settings-section-title" type="button">
<span class="section-chevron"></span> <span class="section-chevron"></span>
WIDGETS <span data-i18n="settings.section.widgets">WIDGETS</span>
</button> </button>
<div class="section-content"> <div class="section-content">
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Toolbar-Position</span> <span class="setting-label" data-i18n="settings.toolbar_pos">Toolbar-Position</span>
<span class="setting-desc">Widget-Toolbar links oder rechts anzeigen</span> <span class="setting-desc" data-i18n="settings.toolbar_pos.desc">Widget-Toolbar links oder rechts anzeigen</span>
</div> </div>
<select class="select-input" id="settingToolbarPos"> <select class="select-input" id="settingToolbarPos">
<option value="right" selected>Rechts</option> <option value="right" selected data-i18n="settings.toolbar_pos.right">Rechts</option>
<option value="left">Links</option> <option value="left" data-i18n="settings.toolbar_pos.left">Links</option>
</select> </select>
</div> </div>
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Bild-Referenz Widgets</span> <span class="setting-label" data-i18n="settings.image_ref">Bild-Referenz Widgets</span>
<span class="setting-desc">Bilder als Referenz anzeigen (nur aktuelle Session)</span> <span class="setting-desc" data-i18n="settings.image_ref.desc">Bilder als Referenz anzeigen (nur aktuelle Session)</span>
</div> </div>
<label class="toggle"> <label class="toggle">
<input type="checkbox" id="settingImageRef"> <input type="checkbox" id="settingImageRef">
@@ -144,28 +165,35 @@
<section class="settings-section" data-section="data"> <section class="settings-section" data-section="data">
<button class="settings-section-title" type="button"> <button class="settings-section-title" type="button">
<span class="section-chevron"></span> <span class="section-chevron"></span>
DATEN & HILFE <span data-i18n="settings.section.data">DATEN &amp; HILFE</span>
</button> </button>
<div class="section-content"> <div class="section-content">
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Backup exportieren</span> <span class="setting-label" data-i18n="settings.export">Backup exportieren</span>
<span class="setting-desc">Alle Boards, Notes und Einstellungen als JSON sichern</span> <span class="setting-desc" data-i18n="settings.export.desc">Alle Boards, Notes und Einstellungen als JSON sichern</span>
</div> </div>
<button class="btn-small" id="btnExportJSON">Export</button> <button class="btn-small" id="btnExportJSON" data-i18n="settings.export.btn">Export</button>
</div> </div>
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Backup importieren</span> <span class="setting-label" data-i18n="settings.import">Backup importieren</span>
<span class="setting-desc">JSON-Backup wiederherstellen</span> <span class="setting-desc" data-i18n="settings.import.desc">JSON-Backup wiederherstellen</span>
</div> </div>
<button class="btn-small" id="btnImportJSON">Import</button> <button class="btn-small" id="btnImportJSON" data-i18n="header.import">Import</button>
<input type="file" id="jsonImportInput" accept=".json" class="hidden" /> <input type="file" id="jsonImportInput" accept=".json" class="hidden" />
</div> </div>
<div class="setting-row" id="browserImportRow">
<div class="setting-info">
<span class="setting-label" data-i18n="settings.browser_import">Browser-Lesezeichen</span>
<span class="setting-desc" data-i18n="settings.browser_import.desc">Lesezeichen direkt aus dem Browser importieren</span>
</div>
<button class="btn-small" id="btnBrowserImport" data-i18n="header.import">Import</button>
</div>
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Onboarding wiederholen</span> <span class="setting-label" data-i18n="settings.onboarding">Onboarding wiederholen</span>
<span class="setting-desc">Willkommens-Tour erneut anzeigen</span> <span class="setting-desc" data-i18n="settings.onboarding.desc">Willkommens-Tour erneut anzeigen</span>
</div> </div>
<button class="btn-small" id="btnRestartOnboarding">Start</button> <button class="btn-small" id="btnRestartOnboarding">Start</button>
</div> </div>
@@ -176,13 +204,13 @@
<section class="settings-section" data-section="danger"> <section class="settings-section" data-section="danger">
<button class="settings-section-title danger" type="button"> <button class="settings-section-title danger" type="button">
<span class="section-chevron"></span> <span class="section-chevron"></span>
DANGER ZONE <span data-i18n="settings.section.danger">DANGER ZONE</span>
</button> </button>
<div class="section-content"> <div class="section-content">
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Alles zurücksetzen</span> <span class="setting-label" data-i18n="settings.reset">Alles zurücksetzen</span>
<span class="setting-desc">Löscht alle Boards, Notes und Einstellungen</span> <span class="setting-desc" data-i18n="settings.reset.desc">Löscht alle Boards, Notes und Einstellungen</span>
</div> </div>
<button class="btn-danger" id="btnResetAll">Reset</button> <button class="btn-danger" id="btnResetAll">Reset</button>
</div> </div>
@@ -194,13 +222,13 @@
<!-- ABOUT — fixiert am unteren Rand --> <!-- ABOUT — fixiert am unteren Rand -->
<div class="panel-footer"> <div class="panel-footer">
<div class="about-block"> <div class="about-block">
<div class="about-logo">⬡ HELLION NEWTAB</div> <div class="about-logo" data-i18n="about.title">⬡ HELLION NEWTAB</div>
<div class="about-version">Version 1.9.0 · by Hellion Online Media</div> <div class="about-version">Version 2.0.0 · by Hellion Online Media</div>
<div class="about-links"> <div class="about-links">
<a href="https://hellion-media.de/impressum" target="_blank" class="about-link"> <a href="https://hellion-media.de/impressum" target="_blank" class="about-link">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
Impressum <span data-i18n="about.impressum">Impressum</span>
</a> </a>
<a href="https://hellion-media.de" target="_blank" class="about-link"> <a href="https://hellion-media.de" target="_blank" class="about-link">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/></svg> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/></svg>
@@ -211,26 +239,26 @@
<div class="about-divider"></div> <div class="about-divider"></div>
<div class="about-info-row"> <div class="about-info-row">
<span class="about-info-label">Entwickler</span> <span class="about-info-label" data-i18n="about.developer">Entwickler</span>
<span class="about-info-value">Florian Wathling</span> <span class="about-info-value">Florian Wathling</span>
</div> </div>
<div class="about-info-row"> <div class="about-info-row">
<span class="about-info-label">Unternehmen</span> <span class="about-info-label" data-i18n="about.company">Unternehmen</span>
<span class="about-info-value">Hellion Online Media</span> <span class="about-info-value">Hellion Online Media</span>
</div> </div>
<div class="about-info-row"> <div class="about-info-row">
<span class="about-info-label">Lizenz</span> <span class="about-info-label" data-i18n="about.license">Lizenz</span>
<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank" class="about-info-value about-link-subtle">CC BY-NC-SA 4.0</a> <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank" class="about-info-value about-link-subtle">CC BY-NC-SA 4.0</a>
</div> </div>
<div class="about-info-row"> <div class="about-info-row">
<span class="about-info-label">Datenspeicherung</span> <span class="about-info-label" data-i18n="about.storage">Datenspeicherung</span>
<span class="about-info-value">100% lokal · Kein Server · Kein Account</span> <span class="about-info-value" data-i18n="about.storage.value">100% lokal · Kein Server · Kein Account</span>
</div> </div>
<div class="about-divider"></div> <div class="about-divider"></div>
<div class="about-bugreport"> <div class="about-bugreport">
<span class="about-info-label about-info-label-block">Bug Report / Feedback</span> <span class="about-info-label about-info-label-block" data-i18n="about.bugreport">Bug Report / Feedback</span>
<a href="mailto:kontakt@hellion-media.de?subject=Hellion NewTab Bug Report" class="about-link-mail"> <a href="mailto:kontakt@hellion-media.de?subject=Hellion NewTab Bug Report" class="about-link-mail">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
kontakt@hellion-media.de kontakt@hellion-media.de
@@ -238,7 +266,7 @@
</div> </div>
<div class="about-bugreport"> <div class="about-bugreport">
<span class="about-info-label about-info-label-block">Support</span> <span class="about-info-label about-info-label-block" data-i18n="about.support">Support</span>
<a href="https://ko-fi.com/hellionmedia" target="_blank" class="about-link"> <a href="https://ko-fi.com/hellionmedia" target="_blank" class="about-link">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8h1a4 4 0 010 8h-1"/><path d="M2 8h16v9a4 4 0 01-4 4H6a4 4 0 01-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/></svg> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8h1a4 4 0 010 8h-1"/><path d="M2 8h16v9a4 4 0 01-4 4H6a4 4 0 01-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/></svg>
Ko-fi — hellionmedia Ko-fi — hellionmedia
@@ -246,7 +274,7 @@
</div> </div>
<div class="about-browsers"> <div class="about-browsers">
<span class="about-info-label about-info-label-block">Kompatible Browser</span> <span class="about-info-label about-info-label-block" data-i18n="about.browsers">Kompatible Browser</span>
<div class="about-browser-tags"> <div class="about-browser-tags">
<span class="browser-tag">Chrome</span> <span class="browser-tag">Chrome</span>
<span class="browser-tag">Edge</span> <span class="browser-tag">Edge</span>
@@ -265,22 +293,22 @@
<div class="modal-overlay" id="themeOverlay"> <div class="modal-overlay" id="themeOverlay">
<div class="theme-modal" id="themeModal"> <div class="theme-modal" id="themeModal">
<div class="modal-header"> <div class="modal-header">
<span>Darstellung</span> <span data-i18n="modal.theme_header">Darstellung</span>
<button class="btn-close" id="btnCloseTheme"></button> <button class="btn-close" id="btnCloseTheme"></button>
</div> </div>
<div class="theme-grid"> <div class="theme-grid">
<div class="theme-card active" data-value="nebula"> <div class="theme-card active" data-value="nebula">
<img class="theme-card-img" src="assets/themes/bg-nebula.jpg" alt="Nebula" /> <img class="theme-card-img" src="assets/themes/bg-nebula.webp" alt="Nebula" />
<span class="theme-card-label">Nebula</span> <span class="theme-card-label">Nebula</span>
<span class="theme-card-check"></span> <span class="theme-card-check"></span>
</div> </div>
<div class="theme-card" data-value="crescent"> <div class="theme-card" data-value="crescent">
<img class="theme-card-img" src="assets/themes/bg-crescent.jpg" alt="Crescent" /> <img class="theme-card-img" src="assets/themes/bg-crescent.webp" alt="Crescent" />
<span class="theme-card-label">Crescent</span> <span class="theme-card-label">Crescent</span>
<span class="theme-card-check"></span> <span class="theme-card-check"></span>
</div> </div>
<div class="theme-card" data-value="event-horizon"> <div class="theme-card" data-value="event-horizon">
<img class="theme-card-img" src="assets/themes/bg-event-horizon.jpg" alt="Event Horizon" /> <img class="theme-card-img" src="assets/themes/bg-event-horizon.webp" alt="Event Horizon" />
<span class="theme-card-label">Event Horizon</span> <span class="theme-card-label">Event Horizon</span>
<span class="theme-card-check"></span> <span class="theme-card-check"></span>
</div> </div>
@@ -290,96 +318,111 @@
<span class="theme-card-check"></span> <span class="theme-card-check"></span>
</div> </div>
<div class="theme-card" data-value="julia-jin"> <div class="theme-card" data-value="julia-jin">
<img class="theme-card-img" src="assets/themes/bg-julia-jin.png" alt="Julia &amp; Jin" /> <img class="theme-card-img" src="assets/themes/bg-julia-jin.webp" alt="Julia &amp; Jin" />
<span class="theme-card-label">Julia &amp; Jin</span> <span class="theme-card-label">Julia &amp; Jin</span>
<span class="theme-card-check"></span> <span class="theme-card-check"></span>
</div> </div>
<div class="theme-card" data-value="sc-sunset"> <div class="theme-card" data-value="sc-sunset">
<img class="theme-card-img" src="assets/themes/bg-sc-sunset.jpg" alt="SC Sunset" /> <img class="theme-card-img" src="assets/themes/bg-sc-sunset.webp" alt="SC Sunset" />
<span class="theme-card-label">SC Sunset</span> <span class="theme-card-label">SC Sunset</span>
<span class="theme-card-check"></span> <span class="theme-card-check"></span>
</div> </div>
<div class="theme-card" data-value="hellion-hud"> <div class="theme-card" data-value="hellion-hud">
<img class="theme-card-img" src="assets/themes/bg-hellion-hud.png" alt="Hellion HUD" /> <img class="theme-card-img" src="assets/themes/bg-hellion-hud.webp" alt="Hellion HUD" />
<span class="theme-card-label">HUD</span> <span class="theme-card-label">HUD</span>
<span class="theme-card-check"></span> <span class="theme-card-check"></span>
</div> </div>
<div class="theme-card" data-value="hellion-energy"> <div class="theme-card" data-value="hellion-energy">
<img class="theme-card-img" src="assets/themes/bg-hellion-energy.jpg" alt="Hellion Energy" /> <img class="theme-card-img" src="assets/themes/bg-hellion-energy.webp" alt="Hellion Energy" />
<span class="theme-card-label">Energy</span> <span class="theme-card-label">Energy</span>
<span class="theme-card-check"></span> <span class="theme-card-check"></span>
</div> </div>
<div class="theme-card" data-value="satisfactory">
<img class="theme-card-img" src="assets/themes/bg-satisfactory.webp" alt="Satisfactory" />
<span class="theme-card-label">Satisfactory</span>
<span class="theme-card-check"></span>
</div>
<div class="theme-card" data-value="avorion">
<img class="theme-card-img" src="assets/themes/bg-avorion.webp" alt="Avorion" />
<span class="theme-card-label">Avorion</span>
<span class="theme-card-check"></span>
</div>
<div class="theme-card" data-value="hellion-stealth">
<img class="theme-card-img" src="assets/themes/bg-scPolaris.webp" alt="Hellion Stealth" />
<span class="theme-card-label">Stealth</span>
<span class="theme-card-check"></span>
</div>
</div> </div>
<div class="theme-modal-section"> <div class="theme-modal-section">
<h3 class="settings-section-title">HINTERGRUND</h3> <h3 class="settings-section-title" data-i18n="settings.section.bg">HINTERGRUND</h3>
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Bild-URL</span> <span class="setting-label" data-i18n="settings.bg_url">Bild-URL</span>
<span class="setting-desc">Eigenes Hintergrundbild per URL</span> <span class="setting-desc" data-i18n="settings.bg_url.desc">Eigenes Hintergrundbild per URL</span>
</div> </div>
<button class="btn-small" id="btnChangeBg">Ändern</button> <button class="btn-small" id="btnChangeBg" data-i18n="settings.bg_change">Ändern</button>
</div> </div>
<div class="setting-row hidden" id="bgInputRow"> <div class="setting-row hidden" id="bgInputRow">
<input type="text" class="text-input full-width" id="bgUrlInput" placeholder="https://... oder leer für Standard" /> <input type="text" class="text-input full-width" id="bgUrlInput" placeholder="https://... oder leer für Standard" />
<button class="btn-small" id="btnApplyBg">Übernehmen</button> <button class="btn-small" id="btnApplyBg" data-i18n="settings.bg_apply">Übernehmen</button>
</div> </div>
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Datei hochladen</span> <span class="setting-label" data-i18n="settings.bg_upload">Datei hochladen</span>
<span class="setting-desc">Lokales Bild als Hintergrund verwenden</span> <span class="setting-desc" data-i18n="settings.bg_upload.desc">Lokales Bild als Hintergrund verwenden</span>
</div> </div>
<button class="btn-small" id="btnBgFile">Upload</button> <button class="btn-small" id="btnBgFile">Upload</button>
<input type="file" id="bgFileInput" accept="image/*" class="hidden" /> <input type="file" id="bgFileInput" accept="image/*" class="hidden" />
</div> </div>
</div> </div>
<div class="theme-modal-section"> <div class="theme-modal-section">
<h3 class="settings-section-title">DARSTELLUNG</h3> <h3 class="settings-section-title" data-i18n="settings.section.display">DARSTELLUNG</h3>
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Kompaktmodus</span> <span class="setting-label" data-i18n="settings.compact">Kompaktmodus</span>
<span class="setting-desc">Weniger Abstand für mehr Bookmarks</span> <span class="setting-desc" data-i18n="settings.compact.desc">Weniger Abstand für mehr Bookmarks</span>
</div> </div>
<label class="toggle"><input type="checkbox" id="settingCompact" /><span class="slider"></span></label> <label class="toggle"><input type="checkbox" id="settingCompact" /><span class="slider"></span></label>
</div> </div>
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Lange Titel kürzen</span> <span class="setting-label" data-i18n="settings.shorten">Lange Titel kürzen</span>
<span class="setting-desc">Titel auf eine Zeile mit „…" kürzen</span> <span class="setting-desc" data-i18n="settings.shorten.desc">Titel auf eine Zeile mit „…" kürzen</span>
</div> </div>
<label class="toggle"><input type="checkbox" id="settingShorten" /><span class="slider"></span></label> <label class="toggle"><input type="checkbox" id="settingShorten" /><span class="slider"></span></label>
</div> </div>
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Suchleiste anzeigen</span> <span class="setting-label" data-i18n="settings.search">Suchleiste anzeigen</span>
<span class="setting-desc">Suchleiste unter dem Header ein/aus</span> <span class="setting-desc" data-i18n="settings.search.desc">Suchleiste unter dem Header ein/aus</span>
</div> </div>
<label class="toggle"><input type="checkbox" id="settingShowSearch" checked /><span class="slider"></span></label> <label class="toggle"><input type="checkbox" id="settingShowSearch" checked /><span class="slider"></span></label>
</div> </div>
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Links in neuem Tab</span> <span class="setting-label" data-i18n="settings.newtab">Links in neuem Tab</span>
<span class="setting-desc">Bookmarks in neuem Browser-Tab öffnen</span> <span class="setting-desc" data-i18n="settings.newtab.desc">Bookmarks in neuem Browser-Tab öffnen</span>
</div> </div>
<label class="toggle"><input type="checkbox" id="settingNewTab" checked /><span class="slider"></span></label> <label class="toggle"><input type="checkbox" id="settingNewTab" checked /><span class="slider"></span></label>
</div> </div>
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Beschreibungen anzeigen</span> <span class="setting-label" data-i18n="settings.showdesc">Beschreibungen anzeigen</span>
<span class="setting-desc">Gespeicherte Beschreibung unter Bookmarks</span> <span class="setting-desc" data-i18n="settings.showdesc.desc">Gespeicherte Beschreibung unter Bookmarks</span>
</div> </div>
<label class="toggle"><input type="checkbox" id="settingShowDesc" /><span class="slider"></span></label> <label class="toggle"><input type="checkbox" id="settingShowDesc" /><span class="slider"></span></label>
</div> </div>
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Bookmarks ausblenden</span> <span class="setting-label" data-i18n="settings.hideextra">Bookmarks ausblenden</span>
<span class="setting-desc">Überzählige Bookmarks in langen Boards verstecken</span> <span class="setting-desc" data-i18n="settings.hideextra.desc">Überzählige Bookmarks in langen Boards verstecken</span>
</div> </div>
<label class="toggle"><input type="checkbox" id="settingHideExtra" /><span class="slider"></span></label> <label class="toggle"><input type="checkbox" id="settingHideExtra" /><span class="slider"></span></label>
</div> </div>
<div class="setting-row" id="visibleCountRow"> <div class="setting-row" id="visibleCountRow">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Sichtbare Bookmarks</span> <span class="setting-label" data-i18n="settings.visible_count">Sichtbare Bookmarks</span>
<span class="setting-desc">Anzahl vor dem Ausblenden</span> <span class="setting-desc" data-i18n="settings.visible_count.desc">Anzahl vor dem Ausblenden</span>
</div> </div>
<select class="select-input" id="settingVisibleCount"> <select class="select-input" id="settingVisibleCount">
<option value="5">5</option> <option value="5">5</option>
@@ -395,14 +438,14 @@
<div class="modal-overlay" id="addBoardOverlay"> <div class="modal-overlay" id="addBoardOverlay">
<div class="modal"> <div class="modal">
<div class="modal-header"> <div class="modal-header">
<span>New Board</span> <span data-i18n="modal.new_board">New Board</span>
<button class="btn-close" id="btnCancelBoard"></button> <button class="btn-close" id="btnCancelBoard"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<input type="text" class="text-input full-width" id="newBoardName" placeholder="Board name..." maxlength="40" /> <input type="text" class="text-input full-width" id="newBoardName" data-i18n-placeholder="modal.board_name" placeholder="Board name..." maxlength="40" />
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn-primary" id="btnConfirmBoard">Create</button> <button class="btn-primary" id="btnConfirmBoard" data-i18n="modal.create">Create</button>
</div> </div>
</div> </div>
</div> </div>
@@ -411,16 +454,16 @@
<div class="modal-overlay" id="addBookmarkOverlay"> <div class="modal-overlay" id="addBookmarkOverlay">
<div class="modal"> <div class="modal">
<div class="modal-header"> <div class="modal-header">
<span>New Bookmark</span> <span data-i18n="modal.new_bookmark">New Bookmark</span>
<button class="btn-close" id="btnCancelBookmark"></button> <button class="btn-close" id="btnCancelBookmark"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<input type="text" class="text-input full-width" id="newBmTitle" placeholder="Title..." maxlength="60" /> <input type="text" class="text-input full-width" id="newBmTitle" data-i18n-placeholder="modal.bm_title" placeholder="Title..." maxlength="60" />
<input type="url" class="text-input full-width modal-input-spaced" id="newBmUrl" placeholder="https://..." /> <input type="url" class="text-input full-width modal-input-spaced" id="newBmUrl" placeholder="https://..." />
<input type="text" class="text-input full-width modal-input-spaced" id="newBmDesc" placeholder="Description (optional)" /> <input type="text" class="text-input full-width modal-input-spaced" id="newBmDesc" data-i18n-placeholder="modal.bm_desc" placeholder="Description (optional)" />
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn-primary" id="btnConfirmBookmark">Add</button> <button class="btn-primary" id="btnConfirmBookmark" data-i18n="modal.bm_add">Add</button>
</div> </div>
</div> </div>
</div> </div>
@@ -429,14 +472,14 @@
<div class="modal-overlay" id="renameOverlay"> <div class="modal-overlay" id="renameOverlay">
<div class="modal"> <div class="modal">
<div class="modal-header"> <div class="modal-header">
<span>Rename</span> <span data-i18n="modal.rename">Rename</span>
<button class="btn-close" id="btnCancelRename"></button> <button class="btn-close" id="btnCancelRename"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<input type="text" class="text-input full-width" id="renameInput" placeholder="New name..." maxlength="60" /> <input type="text" class="text-input full-width" id="renameInput" data-i18n-placeholder="modal.rename_placeholder" placeholder="New name..." maxlength="60" />
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn-primary" id="btnConfirmRename">Rename</button> <button class="btn-primary" id="btnConfirmRename" data-i18n="modal.rename_confirm">Rename</button>
</div> </div>
</div> </div>
</div> </div>
@@ -450,6 +493,8 @@
<script src="src/js/storage.js"></script> <script src="src/js/storage.js"></script>
<!-- State & Hilfsfunktionen --> <!-- State & Hilfsfunktionen -->
<script src="src/js/state.js"></script> <script src="src/js/state.js"></script>
<!-- i18n (nach state.js, vor allen Modulen die t() nutzen könnten) -->
<script src="src/js/i18n.js"></script>
<!-- Dialog-System (vor Features, wird überall gebraucht) --> <!-- Dialog-System (vor Features, wird überall gebraucht) -->
<script src="src/js/dialog.js"></script> <script src="src/js/dialog.js"></script>
<!-- Theme-System --> <!-- Theme-System -->
@@ -464,6 +509,7 @@
<script src="src/js/calculator.js"></script> <script src="src/js/calculator.js"></script>
<script src="src/js/timer.js"></script> <script src="src/js/timer.js"></script>
<script src="src/js/image-ref.js"></script> <script src="src/js/image-ref.js"></script>
<script src="src/js/bookmark-import.js"></script>
<script src="src/js/data.js"></script> <script src="src/js/data.js"></script>
<!-- Onboarding --> <!-- Onboarding -->
<script src="src/js/onboarding.js"></script> <script src="src/js/onboarding.js"></script>
-101
View File
@@ -1,101 +0,0 @@
# ⬡ Hellion Dashboard — Design & Theme-System
Leitfaden für das visuelle Design des Hellion Dashboards. Definiert wie Themes aufgebaut
sind und welche Patterns konsistent eingehalten werden — für eine immersive, fokussierte
Nutzererfahrung.
---
## Design-Säulen
| Säule | Beschreibung |
|---|---|
| **Immersion** | Das Interface wirkt wie ein HUD das über der Szenerie schwebt — kein Fremdkörper |
| **Visual Clarity** | Gezielter `blur`-Einsatz trennt UI und Hintergrundbild — reduziert Reizüberflutung |
| **Harmonie** | Jedes Theme zieht seine Farben aus den dominanten Lichtquellen des Hintergrundbildes |
---
## Anatomie eines Themes
Jedes Theme folgt dieser Variablen-Struktur in `main.css`.
Für ein neues Theme diesen Block kopieren und anpassen:
```css
[data-theme="dein-theme-name"] {
/* 1. AKZENTE — Die Lichtquelle */
--accent: #HEXCODE; /* Hauptfarbe (Neon/Licht) */
--accent-dim: rgba(R, G, B, 0.12); /* Subtiler Hintergrund */
--accent-glow: rgba(R, G, B, 0.08); /* Glow für Logo & Uhr */
--border-accent: rgba(R, G, B, 0.25); /* Fokus-Rahmen */
/* 2. BASIS — Das Fundament */
--bg-primary: #HEXCODE; /* Dunkelster Punkt im Bild */
--bg-board: rgba(R, G, B, 0.55); /* Glas-Effekt der Boards */
/* 3. TEXT — Kontrast */
--text-primary: #FFFFFF; /* Klar lesbar, leicht getönt */
--text-secondary: #A0A0A0; /* Entsättigt für weniger Rauschen */
/* 4. OVERLAY — Vignette */
--overlay-bg: radial-gradient(
circle at center,
transparent 0%,
var(--bg-primary) 100%
);
}
```
---
## UI-Patterns
### Frosted Glass
Hardware-beschleunigter Blur für Lesbarkeit auf komplexen Hintergründen:
```css
backdrop-filter: blur(8px);
```
Erzeugt Tiefe und visuelle Ruhe hinter Text und UI-Elementen.
### Typografie-Hierarchie
| Font | Einsatz |
|---|---|
| **Rajdhani** | Display — Uhr, Titel, Logo. Alles was nach "System" aussieht |
| **Inter** | Body — Bookmark-Titel, Listen, interaktive Elemente |
| **Cinzel** | Fantasy — Exklusiv für Themes mit majestätischem oder antikem Vibe (Crescent, Julia & Jin) |
---
## Theme-Übersicht
| Theme | Akzentfarbe | Stimmung |
|---|---|---|
| Nebula | `#b359ff` Magenta | Chill, Cosmic |
| Crescent | `#d4bd8a` Gold | Luxury, Night |
| Event Horizon | `#9d5cff` Purple | Deep Space, Void |
| Merchantman | `#2eb8b8` Emerald | Industrial, Alien |
| Julia & Jin | `#7db3ff` Aetherial Blue | FFXIV Night |
| SC Sunset | `#ff8c3d` Amber | Emotional, Horizon |
| Hellion HUD | `#32ff6a` Neon Green | Tactical, Admin |
| Hellion Energy | `#1eff8e` Acid Green | Overdrive, Power |
---
## ADHS-Optimierung
Bei Hintergrundbildern mit vielen Details (z.B. Julia & Jin) den Board-Alpha erhöhen
und den Blur verstärken — das dimmt das Hintergrundrauschen und lässt das Gehirn
schneller die relevanten Informationen erfassen:
```css
--bg-board: rgba(R, G, B, 0.65);
backdrop-filter: blur(12px);
```
---
Entwickelt von **[Hellion Online Media — Florian Wathling](https://hellion-media.de)** — JonKazama-Hellion
+178
View File
@@ -298,6 +298,97 @@
box-shadow: inset 0 0 10px rgba(30, 255, 142, 0.05); box-shadow: inset 0 0 10px rgba(30, 255, 142, 0.05);
} }
/* ============================================
THEME: SATISFACTORY (Industrial Desert)
============================================ */
[data-theme="satisfactory"] {
--accent: #00b4d8;
--accent-dim: rgba(0, 180, 216, 0.12);
--accent-glow: rgba(0, 180, 216, 0.08);
--border-accent: rgba(0, 180, 216, 0.35);
--bg-primary: #1a0f08;
--bg-board: rgba(26, 15, 8, 0.65);
--border: rgba(0, 180, 216, 0.15);
--text-primary: #f0faff;
--text-secondary: #a89f98;
--text-muted: #635a54;
--font-display: 'Rajdhani', sans-serif;
--font-body: 'Inter', sans-serif;
--overlay-bg: linear-gradient(180deg,
rgba(26,15,8,0.85) 0%,
rgba(26,15,8,0.15) 50%,
rgba(26,15,8,0.90) 100%);
--header-bg: rgba(26,15,8,0.95);
--board-hover-border: rgba(0, 180, 216, 0.25);
--toggle-on-bg: rgba(0, 180, 216, 0.20);
--logo-shadow: rgba(0, 180, 216, 0.40);
}
[data-theme="satisfactory"] .logo { font-family: 'Rajdhani', sans-serif; font-weight: 700; letter-spacing: 3px; text-transform: uppercase; }
[data-theme="satisfactory"] .clock { font-family: 'Rajdhani', sans-serif; font-weight: 600; color: var(--accent); }
[data-theme="satisfactory"] .board-title { font-family: 'Rajdhani', sans-serif; letter-spacing: 1.5px; text-transform: uppercase; }
[data-theme="satisfactory"] .board { border-color: rgba(0, 180, 216, 0.20); backdrop-filter: blur(12px); }
[data-theme="satisfactory"] .bm-item:hover { background: rgba(0, 180, 216, 0.10); }
/* ============================================
THEME: AVORION (Deep Void)
============================================ */
[data-theme="avorion"] {
--accent: #2ec4a0;
--accent-dim: rgba(46, 196, 160, 0.12);
--accent-glow: rgba(46, 196, 160, 0.08);
--border-accent: rgba(46, 196, 160, 0.30);
--bg-primary: #020d0c;
--bg-board: rgba(2, 13, 12, 0.60);
--border: rgba(46, 196, 160, 0.12);
--text-primary: #e6fffa;
--text-secondary: #8abdb3;
--text-muted: #40615a;
--font-display: 'Rajdhani', sans-serif;
--font-body: 'Inter', sans-serif;
--overlay-bg: radial-gradient(circle at center,
transparent 0%,
rgba(2, 13, 12, 0.95) 100%);
--header-bg: rgba(2, 13, 12, 0.94);
--board-hover-border: rgba(46, 196, 160, 0.22);
--toggle-on-bg: rgba(46, 196, 160, 0.18);
--logo-shadow: rgba(46, 196, 160, 0.50);
}
[data-theme="avorion"] .logo { font-family: 'Rajdhani', sans-serif; font-weight: 500; letter-spacing: 6px; text-transform: uppercase; }
[data-theme="avorion"] .clock { font-family: 'Rajdhani', sans-serif; font-weight: 400; color: var(--accent); }
[data-theme="avorion"] .board-title { font-family: 'Rajdhani', sans-serif; font-weight: 500; text-transform: uppercase; }
[data-theme="avorion"] .board { border-color: rgba(46, 196, 160, 0.15); backdrop-filter: blur(8px); }
[data-theme="avorion"] .bm-item:hover { background: rgba(46, 196, 160, 0.08); }
/* ============================================
THEME: HELLION STEALTH (Tactical Recon)
============================================ */
[data-theme="hellion-stealth"] {
--accent: #5ec2ff;
--accent-dim: rgba(94, 194, 255, 0.12);
--accent-glow: rgba(94, 194, 255, 0.08);
--border-accent: rgba(94, 194, 255, 0.35);
--bg-primary: #0d0f12;
--bg-board: rgba(13, 15, 18, 0.70);
--border: rgba(94, 194, 255, 0.15);
--text-primary: #e0f4ff;
--text-secondary: #8a9499;
--text-muted: #4a5257;
--font-display: 'Rajdhani', sans-serif;
--font-body: 'Inter', sans-serif;
--overlay-bg: radial-gradient(circle at center,
transparent 0%,
rgba(13, 15, 18, 0.90) 100%);
--header-bg: rgba(13, 15, 18, 0.96);
--board-hover-border: rgba(94, 194, 255, 0.25);
--toggle-on-bg: rgba(94, 194, 255, 0.20);
--logo-shadow: rgba(94, 194, 255, 0.45);
}
[data-theme="hellion-stealth"] .logo { font-family: 'Rajdhani', sans-serif; font-weight: 700; letter-spacing: 4px; }
[data-theme="hellion-stealth"] .clock { font-family: 'Rajdhani', sans-serif; font-weight: 600; color: var(--accent); }
[data-theme="hellion-stealth"] .board-title { text-transform: uppercase; font-size: 0.85rem; letter-spacing: 2px; }
[data-theme="hellion-stealth"] .board { border-color: rgba(94, 194, 255, 0.15); backdrop-filter: blur(10px); }
[data-theme="hellion-stealth"] .bm-item:hover { background: rgba(94, 194, 255, 0.10); border-left: 2px solid var(--accent); }
/* ============================================ /* ============================================
BASE STYLES BASE STYLES
============================================ */ ============================================ */
@@ -612,6 +703,9 @@ body.show-desc .bm-desc { display: block; }
.theme-card[data-value="sc-sunset"] .theme-card-label { color: #ff8c3d; } /* Amber Sunset */ .theme-card[data-value="sc-sunset"] .theme-card-label { color: #ff8c3d; } /* Amber Sunset */
.theme-card[data-value="hellion-hud"] .theme-card-label { color: #32ff6a; } /* Neon Green */ .theme-card[data-value="hellion-hud"] .theme-card-label { color: #32ff6a; } /* Neon Green */
.theme-card[data-value="hellion-energy"] .theme-card-label { color: #1eff8e; } /* Acid Green */ .theme-card[data-value="hellion-energy"] .theme-card-label { color: #1eff8e; } /* Acid Green */
.theme-card[data-value="satisfactory"] .theme-card-label { color: #00b4d8; } /* Cyan LED */
.theme-card[data-value="avorion"] .theme-card-label { color: #2ec4a0; } /* Turquoise */
.theme-card[data-value="hellion-stealth"] .theme-card-label { color: #5ec2ff; } /* Tech Blue */
.theme-card:hover .theme-card-label { .theme-card:hover .theme-card-label {
text-shadow: 0 0 8px currentColor; text-shadow: 0 0 8px currentColor;
transition: text-shadow 0.2s ease; transition: text-shadow 0.2s ease;
@@ -1801,6 +1895,90 @@ body.show-desc .bm-desc { display: block; }
display: flex; gap: 8px; display: flex; gap: 8px;
} }
/* ============================================
BROWSER BOOKMARK IMPORT MODAL
============================================ */
.bm-import-overlay {
position: fixed; inset: 0; z-index: 9999;
background: rgba(0,0,0,0.6); backdrop-filter: blur(6px);
display: flex; align-items: center; justify-content: center;
opacity: 0; pointer-events: none;
transition: opacity 0.25s ease;
}
.bm-import-overlay.active { opacity: 1; pointer-events: all; }
.bm-import-modal {
background: rgba(8,8,16,0.96);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
backdrop-filter: blur(28px);
max-width: 480px; width: 90%;
max-height: 75vh;
display: flex; flex-direction: column;
transform: translateY(12px) scale(0.97);
transition: transform 0.25s ease;
}
.bm-import-overlay.active .bm-import-modal {
transform: translateY(0) scale(1);
}
.bm-import-header {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 18px; border-bottom: 1px solid var(--border);
font-size: 14px; font-weight: 600;
color: var(--text-primary);
}
.bm-import-close {
background: none; border: none; color: var(--text-muted);
font-size: 20px; cursor: pointer; padding: 0 4px;
transition: color 0.15s;
}
.bm-import-close:hover { color: var(--text-primary); }
.bm-import-info {
padding: 10px 18px;
font-size: 12px; color: var(--text-secondary);
line-height: 1.5;
}
.bm-import-list {
flex: 1; overflow-y: auto; padding: 4px 0;
scrollbar-width: thin; scrollbar-color: var(--border) transparent;
max-height: 45vh;
}
.bm-import-folder {
display: flex; align-items: center; gap: 10px;
padding: 8px 12px; cursor: pointer;
transition: background 0.15s;
font-size: 13px; color: var(--text-primary);
}
.bm-import-folder:hover {
background: rgba(255,255,255,0.04);
}
.bm-import-checkbox {
accent-color: var(--accent);
width: 16px; height: 16px;
cursor: pointer; flex-shrink: 0;
}
.bm-import-folder-name {
flex: 1; overflow: hidden;
text-overflow: ellipsis; white-space: nowrap;
}
.bm-import-folder-meta {
font-size: 11px; color: var(--text-muted);
white-space: nowrap; flex-shrink: 0;
}
.bm-import-footer {
display: flex; justify-content: space-between; align-items: center;
padding: 12px 18px; border-top: 1px solid var(--border);
gap: 8px;
}
/* ============================================ /* ============================================
THEME PICKER MODAL THEME PICKER MODAL
============================================ */ ============================================ */
+12 -10
View File
@@ -10,6 +10,7 @@ async function init() {
boards = savedBoards ?? getDefaultBoards(); boards = savedBoards ?? getDefaultBoards();
if (savedSettings) Object.assign(settings, savedSettings); if (savedSettings) Object.assign(settings, savedSettings);
I18n.init();
applySettings(); applySettings();
renderBoards(); renderBoards();
startClock(); startClock();
@@ -21,6 +22,7 @@ async function init() {
await Calculator.init(); await Calculator.init();
await Timer.init(); await Timer.init();
await ImageRef.init(); await ImageRef.init();
BrowserBookmarkImport.init();
initDataButtons(); initDataButtons();
Store.checkQuota(); Store.checkQuota();
@@ -93,8 +95,8 @@ async function checkBackupReminder() {
if (boards.length === 0) return; if (boards.length === 0) return;
const doBackup = await HellionDialog.confirm( 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?', t('app.backup_reminder'),
{ type: 'warning', title: 'Backup-Erinnerung', confirmText: 'Jetzt sichern', cancelText: 'Später' } { type: 'warning', title: t('app.backup_reminder.title'), confirmText: t('app.backup_now'), cancelText: t('app.backup_later') }
); );
if (doBackup) { if (doBackup) {
@@ -103,7 +105,7 @@ async function checkBackupReminder() {
const notesData = (widgetData && Array.isArray(widgetData.notes)) ? widgetData.notes : []; const notesData = (widgetData && Array.isArray(widgetData.notes)) ? widgetData.notes : [];
const calcHistory = (widgetData && widgetData.calculator) ? widgetData.calculator.history || [] : []; const calcHistory = (widgetData && widgetData.calculator) ? widgetData.calculator.history || [] : [];
const timerPresets = (widgetData && widgetData.timer) ? widgetData.timer.presets || [] : []; const timerPresets = (widgetData && widgetData.timer) ? widgetData.timer.presets || [] : [];
const data = { version: '1.9.0', exported: new Date().toISOString(), boards, settings, notes: notesData, calculator: calcHistory, timerPresets }; const data = { version: '2.0.0', exported: new Date().toISOString(), boards, settings, notes: notesData, calculator: calcHistory, timerPresets };
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
@@ -119,15 +121,15 @@ async function checkBackupReminder() {
// ---- CLOCK & DATE ---- // ---- CLOCK & DATE ----
function startClock() { function startClock() {
const DAYS = ['So','Mo','Di','Mi','Do','Fr','Sa']; const DAY_KEYS = ['clock.days.sun','clock.days.mon','clock.days.tue','clock.days.wed','clock.days.thu','clock.days.fri','clock.days.sat'];
const MONTHS = ['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','Dez']; const MONTH_KEYS = ['clock.months.jan','clock.months.feb','clock.months.mar','clock.months.apr','clock.months.may','clock.months.jun','clock.months.jul','clock.months.aug','clock.months.sep','clock.months.oct','clock.months.nov','clock.months.dec'];
function tick() { function tick() {
const now = new Date(); const now = new Date();
document.getElementById('clock').textContent = document.getElementById('clock').textContent =
`${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}`; `${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}`;
document.getElementById('date').textContent = document.getElementById('date').textContent =
`${DAYS[now.getDay()]}, ${String(now.getDate()).padStart(2,'0')}. ${MONTHS[now.getMonth()]}`; `${t(DAY_KEYS[now.getDay()])}, ${String(now.getDate()).padStart(2,'0')}. ${t(MONTH_KEYS[now.getMonth()])}`;
} }
tick(); tick();
setInterval(tick, 1000); setInterval(tick, 1000);
@@ -147,7 +149,7 @@ function bindGlobalEvents() {
if (!file) return; if (!file) return;
const imported = parseBookmarkHtml(await file.text()); const imported = parseBookmarkHtml(await file.text());
if (imported.length === 0) { if (imported.length === 0) {
await HellionDialog.alert('Keine Bookmarks in dieser Datei gefunden.', { type: 'warning', title: 'Import' }); await HellionDialog.alert(t('app.no_bookmarks'), { type: 'warning', title: t('app.import_title') });
return; return;
} }
boards = [...boards, ...imported]; boards = [...boards, ...imported];
@@ -155,8 +157,8 @@ function bindGlobalEvents() {
renderBoards(); renderBoards();
e.target.value = ''; e.target.value = '';
await HellionDialog.alert( await HellionDialog.alert(
`${imported.length} Board(s) mit ${imported.reduce((s,b) => s + b.bookmarks.length, 0)} Bookmarks importiert.`, t('app.html_import_success', { count: imported.length, total: imported.reduce((s,b) => s + b.bookmarks.length, 0) }),
{ type: 'success', title: 'Import erfolgreich' } { type: 'success', title: t('app.import_success_title') }
); );
}); });
@@ -188,7 +190,7 @@ function bindGlobalEvents() {
const url = document.getElementById('newBmUrl').value.trim(); const url = document.getElementById('newBmUrl').value.trim();
const desc = document.getElementById('newBmDesc').value.trim(); const desc = document.getElementById('newBmDesc').value.trim();
if (!title || !url) return; if (!title || !url) return;
try { new URL(url); } catch { await HellionDialog.alert('Ungültige URL. Bitte mit https:// beginnen.', { type: 'warning', title: 'URL ungültig' }); return; } try { new URL(url); } catch { await HellionDialog.alert(t('app.invalid_url'), { type: 'warning', title: t('app.invalid_url.title') }); return; }
const board = boards.find(b => b.id === pendingBookmarkBoardId); const board = boards.find(b => b.id === pendingBookmarkBoardId);
if (!board) return; if (!board) return;
board.bookmarks.push({ id: uid(), title, url, desc }); board.bookmarks.push({ id: uid(), title, url, desc });
+16 -16
View File
@@ -57,14 +57,14 @@ function renderBoards() {
const boardStrong = document.createElement('strong'); const boardStrong = document.createElement('strong');
boardStrong.className = 'accent-text'; boardStrong.className = 'accent-text';
boardStrong.textContent = '+ Board'; boardStrong.textContent = t('boards.add_board');
const importStrong = document.createElement('strong'); const importStrong = document.createElement('strong');
importStrong.className = 'accent-text'; importStrong.className = 'accent-text';
importStrong.textContent = 'Import'; importStrong.textContent = t('boards.import');
empty.append( empty.append(
'No boards yet. Click ', boardStrong, ' to create one, or use ', importStrong, ' to load your browser bookmarks.' t('boards.empty_state_pre'), boardStrong, t('boards.empty_state_mid'), importStrong, t('boards.empty_state_post')
); );
wrapper.appendChild(empty); wrapper.appendChild(empty);
return; return;
@@ -85,7 +85,7 @@ function createBoardEl(board) {
const dragHandle = document.createElement('span'); const dragHandle = document.createElement('span');
dragHandle.className = 'board-drag-handle'; dragHandle.className = 'board-drag-handle';
dragHandle.title = 'Board verschieben'; dragHandle.title = t('boards.drag_title');
dragHandle.appendChild(createDragHandleSvg()); dragHandle.appendChild(createDragHandleSvg());
const titleSpanHeader = document.createElement('span'); const titleSpanHeader = document.createElement('span');
@@ -98,17 +98,17 @@ function createBoardEl(board) {
const btnBlur = document.createElement('button'); const btnBlur = document.createElement('button');
btnBlur.className = 'board-action-btn btn-blur-board'; btnBlur.className = 'board-action-btn btn-blur-board';
btnBlur.title = board.blurred ? 'Unblur' : 'Blur (privat)'; btnBlur.title = board.blurred ? t('boards.unblur') : t('boards.blur');
btnBlur.textContent = '\uD83D\uDD12'; btnBlur.textContent = '\uD83D\uDD12';
const btnRename = document.createElement('button'); const btnRename = document.createElement('button');
btnRename.className = 'board-action-btn btn-rename-board'; btnRename.className = 'board-action-btn btn-rename-board';
btnRename.title = 'Umbenennen'; btnRename.title = t('boards.rename');
btnRename.textContent = '\u270E'; btnRename.textContent = '\u270E';
const btnDelete = document.createElement('button'); const btnDelete = document.createElement('button');
btnDelete.className = 'board-action-btn btn-delete-board'; btnDelete.className = 'board-action-btn btn-delete-board';
btnDelete.title = 'Löschen'; btnDelete.title = t('boards.delete');
btnDelete.textContent = '\u2715'; btnDelete.textContent = '\u2715';
actions.append(btnBlur, btnRename, btnDelete); actions.append(btnBlur, btnRename, btnDelete);
@@ -123,14 +123,14 @@ function createBoardEl(board) {
e.stopPropagation(); e.stopPropagation();
board.blurred = !board.blurred; board.blurred = !board.blurred;
div.classList.toggle('blurred', board.blurred); div.classList.toggle('blurred', board.blurred);
btnBlur.title = board.blurred ? 'Unblur' : 'Blur (privat)'; btnBlur.title = board.blurred ? t('boards.unblur') : t('boards.blur');
await saveBoards(); await saveBoards();
}); });
blurOverlay.addEventListener('click', async () => { blurOverlay.addEventListener('click', async () => {
board.blurred = false; board.blurred = false;
div.classList.remove('blurred'); div.classList.remove('blurred');
btnBlur.title = 'Blur (privat)'; btnBlur.title = t('boards.blur');
await saveBoards(); await saveBoards();
}); });
@@ -147,8 +147,8 @@ function createBoardEl(board) {
btnDelete.addEventListener('click', async e => { btnDelete.addEventListener('click', async e => {
e.stopPropagation(); e.stopPropagation();
const ok = await HellionDialog.confirm( const ok = await HellionDialog.confirm(
`Board "${board.title}" wirklich löschen?`, t('boards.delete_confirm', { title: board.title }),
{ type: 'danger', title: 'Board löschen', confirmText: 'Löschen' } { type: 'danger', title: t('boards.delete_confirm.title'), confirmText: t('boards.delete') }
); );
if (ok) { if (ok) {
boards = boards.filter(b => b.id !== board.id); boards = boards.filter(b => b.id !== board.id);
@@ -180,16 +180,16 @@ function createBoardEl(board) {
let hiddenEls = []; let hiddenEls = [];
const showMoreBtn = document.createElement('button'); const showMoreBtn = document.createElement('button');
showMoreBtn.className = 'show-more-btn'; showMoreBtn.className = 'show-more-btn';
showMoreBtn.textContent = `Show ${hidden.length} more…`; showMoreBtn.textContent = t('boards.show_more', { count: hidden.length });
showMoreBtn.addEventListener('click', () => { showMoreBtn.addEventListener('click', () => {
if (!expanded) { if (!expanded) {
hidden.forEach(bm => { const el = createBmEl(bm); hiddenEls.push(el); list.appendChild(el); }); hidden.forEach(bm => { const el = createBmEl(bm); hiddenEls.push(el); list.appendChild(el); });
showMoreBtn.textContent = 'Show less'; showMoreBtn.textContent = t('boards.show_less');
expanded = true; expanded = true;
} else { } else {
hiddenEls.forEach(el => el.remove()); hiddenEls.forEach(el => el.remove());
hiddenEls = []; hiddenEls = [];
showMoreBtn.textContent = `Show ${hidden.length} more…`; showMoreBtn.textContent = t('boards.show_more', { count: hidden.length });
expanded = false; expanded = false;
} }
}); });
@@ -200,7 +200,7 @@ function createBoardEl(board) {
const addBtn = document.createElement('button'); const addBtn = document.createElement('button');
addBtn.className = 'add-bm-btn'; addBtn.className = 'add-bm-btn';
addBtn.appendChild(createPlusSvg()); addBtn.appendChild(createPlusSvg());
addBtn.append(' Add link'); addBtn.append(t('boards.add_link'));
addBtn.addEventListener('click', () => openAddBookmarkModal(board.id)); addBtn.addEventListener('click', () => openAddBookmarkModal(board.id));
div.appendChild(addBtn); div.appendChild(addBtn);
@@ -243,7 +243,7 @@ function createBmEl(bm) {
const deleteBtn = document.createElement('button'); const deleteBtn = document.createElement('button');
deleteBtn.className = 'bm-delete'; deleteBtn.className = 'bm-delete';
deleteBtn.title = 'Entfernen'; deleteBtn.title = t('boards.remove_bookmark');
deleteBtn.textContent = '✕'; deleteBtn.textContent = '✕';
li.appendChild(favicon); li.appendChild(favicon);
+303
View File
@@ -0,0 +1,303 @@
/* =============================================
HELLION NEWTAB — bookmark-import.js
Direkt-Import von Browser-Lesezeichen
via chrome.bookmarks.getTree() / browser.bookmarks.getTree()
============================================= */
const BrowserBookmarkImport = {
/** Initialisiert den Import-Button */
init() {
const btn = document.getElementById('btnBrowserImport');
const row = document.getElementById('browserImportRow');
if (!btn || !row) return;
// API-Verfuegbarkeit pruefen (nicht vorhanden im normalen Browser-Tab)
const api = this._getApi();
if (!api) {
row.style.display = 'none';
return;
}
btn.addEventListener('click', () => this._openFolderModal());
},
/**
* Gibt die Bookmarks-API zurueck (Chrome oder Firefox)
* @returns {object|null}
*/
_getApi() {
if (typeof chrome !== 'undefined' && chrome.bookmarks) return chrome.bookmarks;
if (typeof browser !== 'undefined' && browser.bookmarks) return browser.bookmarks;
return null;
},
/** Oeffnet das Ordner-Auswahl Modal */
async _openFolderModal() {
const api = this._getApi();
if (!api) return;
let tree;
try {
tree = await api.getTree();
} catch (err) {
await HellionDialog.alert(
t('bm_import.no_access'),
{ type: 'warning', title: t('bm_import.title') }
);
return;
}
const folders = this._extractFolders(tree[0]);
if (folders.length === 0) {
await HellionDialog.alert(
t('bm_import.no_folders'),
{ type: 'warning', title: t('bm_import.title') }
);
return;
}
this._renderModal(folders);
},
/**
* Extrahiert alle Ordner rekursiv aus dem Bookmark-Baum
* @param {object} node - Bookmark-Tree Node
* @param {number} depth - Einrueckungstiefe
* @returns {Array}
*/
_extractFolders(node, depth) {
if (depth === undefined) depth = 0;
const result = [];
if (!node.children) return result;
for (const child of node.children) {
if (child.children) {
const bookmarkCount = child.children.filter(function(c) { return c.url; }).length;
const subfolderCount = child.children.filter(function(c) { return c.children; }).length;
result.push({
id: child.id,
title: child.title || t('bm_import.unnamed'),
depth: depth,
bookmarkCount: bookmarkCount,
subfolderCount: subfolderCount,
node: child
});
const subFolders = this._extractFolders(child, depth + 1);
for (const sf of subFolders) {
result.push(sf);
}
}
}
return result;
},
/**
* Rendert das Ordner-Auswahl Modal
* @param {Array} folders - Liste der Ordner
*/
_renderModal(folders) {
// Overlay
const overlay = document.createElement('div');
overlay.className = 'bm-import-overlay';
overlay.id = 'bmImportOverlay';
const modal = document.createElement('div');
modal.className = 'bm-import-modal';
// Header
const header = document.createElement('div');
header.className = 'bm-import-header';
const title = document.createElement('span');
title.textContent = t('bm_import.modal_title');
header.appendChild(title);
const closeBtn = document.createElement('button');
closeBtn.className = 'bm-import-close';
closeBtn.textContent = '\u00D7';
closeBtn.addEventListener('click', () => this._closeModal());
header.appendChild(closeBtn);
modal.appendChild(header);
// Info
const info = document.createElement('div');
info.className = 'bm-import-info';
info.textContent = t('bm_import.info');
modal.appendChild(info);
// Ordner-Liste
const list = document.createElement('div');
list.className = 'bm-import-list';
for (const folder of folders) {
const row = document.createElement('label');
row.className = 'bm-import-folder';
row.style.paddingLeft = (12 + folder.depth * 20) + 'px';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'bm-import-checkbox';
checkbox.dataset.folderId = folder.id;
row.appendChild(checkbox);
const label = document.createElement('span');
label.className = 'bm-import-folder-name';
label.textContent = folder.title;
row.appendChild(label);
const meta = document.createElement('span');
meta.className = 'bm-import-folder-meta';
const parts = [];
if (folder.bookmarkCount > 0) {
parts.push(t('bm_import.link_count', { count: folder.bookmarkCount }));
}
if (folder.subfolderCount > 0) {
parts.push(t('bm_import.folder_count', { count: folder.subfolderCount }));
}
if (parts.length === 0) {
parts.push(t('bm_import.empty'));
}
meta.textContent = parts.join(', ');
row.appendChild(meta);
list.appendChild(row);
}
modal.appendChild(list);
// Footer
const footer = document.createElement('div');
footer.className = 'bm-import-footer';
const selectAll = document.createElement('button');
selectAll.className = 'btn-secondary';
selectAll.textContent = t('bm_import.select_all');
selectAll.addEventListener('click', () => {
const boxes = list.querySelectorAll('.bm-import-checkbox');
const allChecked = Array.from(boxes).every(function(cb) { return cb.checked; });
boxes.forEach(function(cb) { cb.checked = !allChecked; });
selectAll.textContent = allChecked ? t('bm_import.select_all') : t('bm_import.deselect_all');
});
footer.appendChild(selectAll);
const importBtn = document.createElement('button');
importBtn.className = 'btn-primary';
importBtn.textContent = t('bm_import.import_btn');
importBtn.addEventListener('click', () => this._importSelected(folders));
footer.appendChild(importBtn);
modal.appendChild(footer);
overlay.appendChild(modal);
document.body.appendChild(overlay);
// Animation
requestAnimationFrame(() => overlay.classList.add('active'));
},
/** Schliesst das Modal */
_closeModal() {
const overlay = document.getElementById('bmImportOverlay');
if (!overlay) return;
overlay.classList.remove('active');
setTimeout(() => overlay.remove(), 250);
},
/**
* Importiert die ausgewaehlten Ordner als Boards
* @param {Array} folders - Alle Ordner
*/
async _importSelected(folders) {
const checkboxes = document.querySelectorAll('.bm-import-checkbox:checked');
if (checkboxes.length === 0) {
await HellionDialog.alert(
t('bm_import.no_selection'),
{ type: 'warning', title: t('bm_import.title') }
);
return;
}
// Bestehende URLs sammeln fuer Duplikat-Erkennung
const existingUrls = new Set();
for (const board of boards) {
for (const bm of board.bookmarks) {
existingUrls.add(bm.url);
}
}
const selectedIds = new Set();
checkboxes.forEach(function(cb) { selectedIds.add(cb.dataset.folderId); });
let totalImported = 0;
let totalSkipped = 0;
let boardsCreated = 0;
for (const folder of folders) {
if (!selectedIds.has(folder.id)) continue;
const bookmarks = [];
for (const child of folder.node.children) {
if (!child.url) continue;
// Nur http/https URLs
try {
const parsed = new URL(child.url);
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') continue;
} catch (e) {
continue;
}
// Duplikat-Check
if (existingUrls.has(child.url)) {
totalSkipped++;
continue;
}
bookmarks.push({
id: uid(),
title: child.title || child.url,
url: child.url,
desc: ''
});
existingUrls.add(child.url);
totalImported++;
}
if (bookmarks.length === 0) continue;
boards.push({
id: uid(),
title: folder.title,
bookmarks: bookmarks,
blurred: false
});
boardsCreated++;
}
if (boardsCreated > 0) {
await saveBoards();
renderBoards();
}
this._closeModal();
// Ergebnis-Dialog
const lines = [];
lines.push(t('bm_import.boards_created', { count: boardsCreated }));
lines.push(t('bm_import.bookmarks_imported', { count: totalImported }));
if (totalSkipped > 0) {
lines.push(t('bm_import.duplicates_skipped', { count: totalSkipped }));
}
await HellionDialog.alert(
lines.join('\n'),
{ type: 'success', title: t('bm_import.success_title') }
);
}
};
+3 -3
View File
@@ -69,7 +69,7 @@ const Calculator = {
const widgetId = WidgetManager.create('calculator', { const widgetId = WidgetManager.create('calculator', {
id: this.WIDGET_ID, id: this.WIDGET_ID,
title: 'Taschenrechner', title: t('calculator.title'),
x: saved.x || 400, x: saved.x || 400,
y: saved.y || 120, y: saved.y || 120,
width: saved.width || 280, width: saved.width || 280,
@@ -214,7 +214,7 @@ const Calculator = {
const title = document.createElement('div'); const title = document.createElement('div');
title.className = 'calc-history-title'; title.className = 'calc-history-title';
title.textContent = 'History'; title.textContent = t('calculator.history');
container.appendChild(title); container.appendChild(title);
this._renderHistoryItems(container); this._renderHistoryItems(container);
@@ -345,7 +345,7 @@ const Calculator = {
const result = this._evaluate(this._currentExpr); const result = this._evaluate(this._currentExpr);
if (result === null) { if (result === null) {
this._lastResult = 'Fehler'; this._lastResult = t('calculator.error');
this._updateDisplay(); this._updateDisplay();
return; return;
} }
+11 -11
View File
@@ -13,7 +13,7 @@ function initDataButtons() {
btnExport.addEventListener('click', async () => { btnExport.addEventListener('click', async () => {
const widgetData = await Store.get('widgetStates'); const widgetData = await Store.get('widgetStates');
const data = { const data = {
version: '1.9.0', version: '2.0.0',
exported: new Date().toISOString(), exported: new Date().toISOString(),
boards, boards,
settings, settings,
@@ -37,7 +37,7 @@ function initDataButtons() {
if (!file) return; if (!file) return;
try { try {
const data = JSON.parse(await file.text()); const data = JSON.parse(await file.text());
if (!Array.isArray(data.boards)) throw new Error('Ungültiges Format'); if (!Array.isArray(data.boards)) throw new Error(t('data.invalid_format'));
const validBoards = data.boards.filter(b => { const validBoards = data.boards.filter(b => {
if (!b || typeof b.title !== 'string' || !Array.isArray(b.bookmarks)) return false; if (!b || typeof b.title !== 'string' || !Array.isArray(b.bookmarks)) return false;
b.id = b.id || uid(); b.id = b.id || uid();
@@ -50,10 +50,10 @@ function initDataButtons() {
}); });
return true; return true;
}); });
if (validBoards.length === 0) throw new Error('Keine gültigen Boards gefunden'); if (validBoards.length === 0) throw new Error(t('data.no_boards'));
const ok = await HellionDialog.confirm( const ok = await HellionDialog.confirm(
`${validBoards.length} Boards importieren? Bestehende Daten bleiben erhalten.`, t('data.import_confirm', { count: validBoards.length }),
{ type: 'info', title: 'JSON Import' } { type: 'info', title: t('data.import_confirm.title') }
); );
if (!ok) return; if (!ok) return;
boards = [...boards, ...validBoards]; boards = [...boards, ...validBoards];
@@ -112,15 +112,15 @@ function initDataButtons() {
// Gemeinsam speichern // Gemeinsam speichern
await Store.set('widgetStates', existingWidgets); await Store.set('widgetStates', existingWidgets);
const noteMsg = notesImported > 0 ? ` + ${notesImported} Note(s)` : ''; const noteMsg = notesImported > 0 ? t('data.notes_suffix', { count: notesImported }) : '';
const calcMsg = calcImported ? ' + Calculator-History' : ''; const calcMsg = calcImported ? t('data.calc_suffix') : '';
const timerMsg = timerImported ? ' + Timer-Presets' : ''; const timerMsg = timerImported ? t('data.timer_suffix') : '';
await HellionDialog.alert( await HellionDialog.alert(
`${validBoards.length} Board(s)${noteMsg}${calcMsg}${timerMsg} erfolgreich importiert.`, t('data.import_success', { boards: validBoards.length, notes: noteMsg, calc: calcMsg, timer: timerMsg }),
{ type: 'success', title: 'Import erfolgreich' } { type: 'success', title: t('data.import_success.title') }
); );
} catch (err) { } catch (err) {
await HellionDialog.alert('Fehler beim Import: ' + err.message, { type: 'danger', title: 'Import fehlgeschlagen' }); await HellionDialog.alert(t('data.import_error', { error: err.message }), { type: 'danger', title: t('data.import_error.title') });
} }
e.target.value = ''; e.target.value = '';
}); });
+5 -5
View File
@@ -126,8 +126,8 @@ const HellionDialog = {
const opts = options || {}; const opts = options || {};
return this._show({ return this._show({
message, message,
title: opts.title || 'Hinweis', title: opts.title || t('dialog.default_title'),
confirmText: opts.confirmText || 'OK', confirmText: opts.confirmText || t('dialog.ok'),
cancelText: '', cancelText: '',
type: opts.type || 'info', type: opts.type || 'info',
isConfirm: false isConfirm: false
@@ -144,9 +144,9 @@ const HellionDialog = {
const opts = options || {}; const opts = options || {};
return this._show({ return this._show({
message, message,
title: opts.title || 'Bestätigung', title: opts.title || t('dialog.confirm_title'),
confirmText: opts.confirmText || 'OK', confirmText: opts.confirmText || t('dialog.ok'),
cancelText: opts.cancelText || 'Abbrechen', cancelText: opts.cancelText || t('dialog.cancel'),
type: opts.type || 'info', type: opts.type || 'info',
isConfirm: true isConfirm: true
}); });
+666
View File
@@ -0,0 +1,666 @@
/* =============================================
HELLION NEWTAB — i18n.js
Internationalisierung: DE/EN Sprachumschaltung
============================================= */
const STRINGS = {
de: {
// Dialog-System
'dialog.default_title': 'Hinweis',
'dialog.ok': 'OK',
'dialog.confirm_title': 'Bestätigung',
'dialog.cancel': 'Abbrechen',
// Boards
'boards.empty_state_pre': 'Noch keine Boards. Klicke auf ',
'boards.add_board': '+ Board',
'boards.empty_state_mid': ' um eins zu erstellen, oder nutze ',
'boards.import': 'Import',
'boards.empty_state_post': ' um deine Browser-Lesezeichen zu laden.',
'boards.drag_title': 'Board verschieben',
'boards.blur': 'Blur (privat)',
'boards.unblur': 'Unblur',
'boards.rename': 'Umbenennen',
'boards.delete': 'Löschen',
'boards.delete_confirm': 'Board „{title}" wirklich löschen?',
'boards.delete_confirm.title': 'Board löschen',
'boards.show_more': '{count} weitere anzeigen…',
'boards.show_less': 'Weniger anzeigen',
'boards.add_link': ' Link hinzufügen',
'boards.remove_bookmark': 'Entfernen',
// Onboarding
'onboarding.skip': 'Überspringen',
'onboarding.back': 'Zurück',
'onboarding.next': 'Weiter',
'onboarding.start': 'Los geht\'s!',
'onboarding.yes': 'Ja, gerne',
'onboarding.no': 'Nein danke',
'onboarding.s1.title': 'Willkommen bei Hellion Dashboard',
'onboarding.s1.text': 'Dein neuer Browser-Startbildschirm. Minimalistisch, schnell und vollständig lokal — keine Cloud, kein Account, keine Datensammlung.',
'onboarding.s2.title': 'Boards & Bookmarks',
'onboarding.s2.f1': 'Erstelle Boards mit dem „+ Board" Button oben',
'onboarding.s2.f2': 'Importiere Browser-Lesezeichen über den „Import" Button im Header',
'onboarding.s2.f3': 'Drag & Drop zum Umsortieren von Boards und Links',
'onboarding.s2.f4': 'Blur-Modus für private Boards (🔒 Icon)',
'onboarding.s3.title': '11 handgefertigte Themes',
'onboarding.s3.text': 'Klicke auf den „Theme" Button im Header um dein Theme zu wählen. Jedes hat seinen eigenen Stil und Farbpalette.',
'onboarding.s4.title': 'Widget-Toolbar',
'onboarding.s4.f1': 'Die schwebenden Buttons rechts öffnen Widgets',
'onboarding.s4.f2': 'Notes und Checklisten für schnelle Notizen',
'onboarding.s4.f3': 'Taschenrechner mit History',
'onboarding.s4.f4': 'Timer/Countdown mit speicherbaren Presets',
'onboarding.s4.f5': 'Bild-Referenz Widgets (aktivierbar in Settings)',
'onboarding.s4.f6': 'Notebook-Sidebar zeigt alle Notes auf einen Blick',
'onboarding.s5.title': 'Backups nicht vergessen!',
'onboarding.s5.text': 'Deine Daten sind lokal im Browser gespeichert. Wenn du Browserdaten löschst, gehen sie verloren! Sichere regelmäßig über Settings → Data → Export. Wir erinnern dich alle 7 Tage daran.',
'onboarding.s6.title': 'Gaming Starter Board',
'onboarding.s6.text': 'Spielst du Games wie Satisfactory, Factorio oder Star Citizen? Ich kann ein Board mit nützlichen Community-Links anlegen.',
'onboarding.s7.title': 'Bereit!',
'onboarding.s7.text': 'Erstelle dein erstes Board mit „+ Board" oder importiere deine Browser-Lesezeichen über den Import-Button im Header. Viel Spaß!',
// Notes
'notes.limit_message': 'Maximale Anzahl erreicht! Du kannst maximal {max} Notes gleichzeitig haben. Lösche eine bestehende Note um eine neue zu erstellen.',
'notes.limit_title': 'Limit erreicht',
'notes.checklist_title': 'Checkliste',
'notes.default_title': 'Note',
'notes.placeholder': 'Notiz schreiben...',
'notes.checklist_placeholder': 'Neues Item...',
'notes.delete_confirm': 'Note endgültig löschen? Das kann nicht rückgängig gemacht werden.',
'notes.delete_title': 'Note löschen',
'notes.delete_button': 'Löschen',
'notes.checklist_progress': '{done}/{total} erledigt',
'notes.empty_preview': 'Leer',
'notes.export': 'Export',
'notes.export_footer': 'Exportiert aus Hellion Dashboard',
'notes.create': '+ Note erstellen',
'notes.text_type': '✎ Freitext',
'notes.checklist_type': '☑ Checkliste',
// Calculator
'calculator.title': 'Taschenrechner',
'calculator.history': 'History',
'calculator.error': 'Fehler',
// Timer
'timer.title': 'Timer',
'timer.start': 'Start',
'timer.pause': 'Pause',
'timer.reset': 'Reset',
'timer.restart': 'Neustart',
'timer.presets': 'Presets',
'timer.save_preset': 'Preset speichern',
'timer.preset_name_placeholder': 'Name...',
'timer.ok': 'OK',
'timer.limit_title': 'Limit erreicht',
'timer.limit_message': 'Maximale Anzahl erreicht! Du kannst maximal {max} Presets speichern.',
'timer.no_time_title': 'Keine Zeit',
'timer.no_time_message': 'Gib zuerst eine Zeit ein, bevor du ein Preset speicherst.',
'timer.mute': 'Ton ausschalten',
'timer.unmute': 'Ton einschalten',
'timer.finished_title': '[!] Timer abgelaufen',
'timer.default_page_title': 'Hellion Dashboard',
// Bild-Referenz
'imageref.title': 'Bild-Referenz',
'imageref.dropzone': 'Klicken oder Bild hierher ziehen',
'imageref.replace': 'Bild ersetzen',
'imageref.label_placeholder': 'Beschriftung (optional)',
'imageref.storage_error': 'Bild konnte nicht gespeichert werden. Der Browser-Speicher ist voll.',
'imageref.storage_error.title': 'Speicherfehler',
'imageref.limit': 'Maximal {max} Bild-Widgets gleichzeitig. Schliesse eines um ein neues zu oeffnen.',
'imageref.limit.title': 'Limit erreicht',
'imageref.load_error': 'Bild konnte nicht geladen werden: {error}',
'imageref.load_error.title': 'Bildfehler',
'imageref.invalid_file': 'Bitte eine Bilddatei verwenden (PNG, JPG, WebP, etc.).',
'imageref.invalid_file.title': 'Kein Bild',
// Widget-Manager
'widget.minimize': 'Minimieren',
'widget.close': 'Schließen',
// Daten (Export/Import)
'data.invalid_format': 'Ungültiges Format',
'data.no_boards': 'Keine gültigen Boards gefunden',
'data.import_confirm': '{count} Boards importieren? Bestehende Daten bleiben erhalten.',
'data.import_confirm.title': 'JSON Import',
'data.import_success': '{boards} Board(s){notes}{calc}{timer} erfolgreich importiert.',
'data.import_success.title': 'Import erfolgreich',
'data.import_error': 'Fehler beim Import: {error}',
'data.import_error.title': 'Import fehlgeschlagen',
'data.notes_suffix': ' + {count} Note(s)',
'data.calc_suffix': ' + Calculator-History',
'data.timer_suffix': ' + Timer-Presets',
// Browser-Lesezeichen Import
'bm_import.no_access': 'Zugriff auf Browser-Lesezeichen nicht möglich. Stelle sicher, dass die Extension die nötigen Berechtigungen hat.',
'bm_import.title': 'Lesezeichen-Import',
'bm_import.no_folders': 'Keine Lesezeichen-Ordner gefunden.',
'bm_import.modal_title': 'Browser-Lesezeichen importieren',
'bm_import.info': 'Wähle die Ordner aus, die als Boards importiert werden sollen. Jeder Ordner wird ein eigenes Board.',
'bm_import.unnamed': 'Unbenannt',
'bm_import.link_count': '{count} Link(s)',
'bm_import.folder_count': '{count} Ordner',
'bm_import.empty': 'leer',
'bm_import.select_all': 'Alle auswählen',
'bm_import.deselect_all': 'Alle abwählen',
'bm_import.import_btn': 'Importieren',
'bm_import.no_selection': 'Bitte wähle mindestens einen Ordner aus.',
'bm_import.boards_created': '{count} Board(s) erstellt',
'bm_import.bookmarks_imported': '{count} Lesezeichen importiert',
'bm_import.duplicates_skipped': '{count} Duplikat(e) übersprungen',
'bm_import.success_title': 'Import abgeschlossen',
// Storage
'storage.quota_full': 'Speicher voll! Bitte lösche alte Boards oder das Hintergrundbild, um Platz zu schaffen.',
'storage.quota_full.title': 'Speicher voll',
// App
'app.backup_reminder': 'Du hast seit über einer Woche kein Backup gemacht. Beim Löschen der Browserdaten gehen deine Boards verloren. Jetzt sichern?',
'app.backup_reminder.title': 'Backup-Erinnerung',
'app.backup_now': 'Jetzt sichern',
'app.backup_later': 'Später',
'app.no_bookmarks': 'Keine Bookmarks in dieser Datei gefunden.',
'app.import_title': 'Import',
'app.html_import_success': '{count} Board(s) mit {total} Bookmarks importiert.',
'app.import_success_title': 'Import erfolgreich',
'app.invalid_url': 'Ungültige URL. Bitte mit https:// beginnen.',
'app.invalid_url.title': 'URL ungültig',
// Uhr
'clock.days.sun': 'So',
'clock.days.mon': 'Mo',
'clock.days.tue': 'Di',
'clock.days.wed': 'Mi',
'clock.days.thu': 'Do',
'clock.days.fri': 'Fr',
'clock.days.sat': 'Sa',
'clock.months.jan': 'Jan',
'clock.months.feb': 'Feb',
'clock.months.mar': 'Mär',
'clock.months.apr': 'Apr',
'clock.months.may': 'Mai',
'clock.months.jun': 'Jun',
'clock.months.jul': 'Jul',
'clock.months.aug': 'Aug',
'clock.months.sep': 'Sep',
'clock.months.oct': 'Okt',
'clock.months.nov': 'Nov',
'clock.months.dec': 'Dez',
// Settings
'settings.file_read_error': 'Fehler beim Lesen der Datei. Bitte eine andere Datei wählen.',
'settings.file_read_error.title': 'Dateifehler',
'settings.reset_confirm': 'Wirklich alle Boards und Einstellungen löschen? Das kann nicht rückgängig gemacht werden.',
'settings.reset_confirm.title': 'Alles zurücksetzen',
'settings.reset_confirm.button': 'Alles löschen',
// Header
'header.import': 'Import',
'header.board': 'Board',
'header.note': 'Note',
'header.theme': 'Darstellung',
'header.settings': 'Einstellungen',
// Settings-Panel Überschrift
'settings.title': 'Einstellungen',
// Settings-Panel Sektionen
'settings.section.widgets': 'WIDGETS',
'settings.section.data': 'DATEN & HILFE',
'settings.section.danger': 'DANGER ZONE',
'settings.section.bg': 'HINTERGRUND',
'settings.section.display': 'DARSTELLUNG',
// Settings-Zeilen
'settings.language': 'Sprache',
'settings.language.desc': 'Anzeigesprache wählen',
'settings.language.auto': 'Automatisch',
'settings.toolbar_pos': 'Toolbar-Position',
'settings.toolbar_pos.desc': 'Widget-Toolbar links oder rechts anzeigen',
'settings.toolbar_pos.right': 'Rechts',
'settings.toolbar_pos.left': 'Links',
'settings.image_ref': 'Bild-Referenz Widgets',
'settings.image_ref.desc': 'Bilder als Referenz anzeigen (nur aktuelle Session)',
'settings.export': 'Backup exportieren',
'settings.export.desc': 'Alle Boards, Notes und Einstellungen als JSON sichern',
'settings.export.btn': 'Export',
'settings.import': 'Backup importieren',
'settings.import.desc': 'JSON-Backup wiederherstellen',
'settings.browser_import': 'Browser-Lesezeichen',
'settings.browser_import.desc': 'Lesezeichen direkt aus dem Browser importieren',
'settings.onboarding': 'Onboarding wiederholen',
'settings.onboarding.desc': 'Willkommens-Tour erneut anzeigen',
'settings.reset': 'Alles zurücksetzen',
'settings.reset.desc': 'Löscht alle Boards, Notes und Einstellungen',
'settings.compact': 'Kompaktmodus',
'settings.compact.desc': 'Weniger Abstand für mehr Bookmarks',
'settings.shorten': 'Lange Titel kürzen',
'settings.shorten.desc': 'Titel auf eine Zeile mit „…" kürzen',
'settings.search': 'Suchleiste anzeigen',
'settings.search.desc': 'Suchleiste unter dem Header ein/aus',
'settings.newtab': 'Links in neuem Tab',
'settings.newtab.desc': 'Bookmarks in neuem Browser-Tab öffnen',
'settings.showdesc': 'Beschreibungen anzeigen',
'settings.showdesc.desc': 'Gespeicherte Beschreibung unter Bookmarks',
'settings.hideextra': 'Bookmarks ausblenden',
'settings.hideextra.desc': 'Überzählige Bookmarks in langen Boards verstecken',
'settings.visible_count': 'Sichtbare Bookmarks',
'settings.visible_count.desc': 'Anzahl vor dem Ausblenden',
'settings.bg_url': 'Bild-URL',
'settings.bg_url.desc': 'Eigenes Hintergrundbild per URL',
'settings.bg_change': 'Ändern',
'settings.bg_apply': 'Übernehmen',
'settings.bg_upload': 'Datei hochladen',
'settings.bg_upload.desc': 'Lokales Bild als Hintergrund verwenden',
'settings.search_engine_toggle': 'Suchmaschine wechseln',
// Modals
'modal.new_board': 'Neues Board',
'modal.board_name': 'Board-Name...',
'modal.create': 'Erstellen',
'modal.new_bookmark': 'Neues Lesezeichen',
'modal.bm_title': 'Titel...',
'modal.bm_desc': 'Beschreibung (optional)',
'modal.bm_add': 'Hinzufügen',
'modal.rename': 'Umbenennen',
'modal.rename_placeholder': 'Neuer Name...',
'modal.rename_confirm': 'Umbenennen',
'modal.theme_header': 'Darstellung',
// About
'about.title': '⬡ HELLION NEWTAB',
'about.impressum': 'Impressum',
'about.developer': 'Entwickler',
'about.company': 'Unternehmen',
'about.license': 'Lizenz',
'about.storage': 'Datenspeicherung',
'about.storage.value': '100% lokal · Kein Server · Kein Account',
'about.bugreport': 'Bug Report / Feedback',
'about.support': 'Support',
'about.browsers': 'Kompatible Browser',
// Notebook
'notebook.title': 'Notebook',
// Suche
'search.placeholder': 'Im Web suchen…',
// Widget-Toolbar Tooltips
'toolbar.note': 'Note erstellen',
'toolbar.checklist': 'Checkliste erstellen',
'toolbar.calculator': 'Taschenrechner',
'toolbar.timer': 'Timer',
'toolbar.imageref': 'Bild-Referenz',
'toolbar.notebook': 'Alle Notes'
},
en: {
// Dialog system
'dialog.default_title': 'Notice',
'dialog.ok': 'OK',
'dialog.confirm_title': 'Confirmation',
'dialog.cancel': 'Cancel',
// Boards
'boards.empty_state_pre': 'No boards yet. Click ',
'boards.add_board': '+ Board',
'boards.empty_state_mid': ' to create one, or use ',
'boards.import': 'Import',
'boards.empty_state_post': ' to load your browser bookmarks.',
'boards.drag_title': 'Move board',
'boards.blur': 'Blur (private)',
'boards.unblur': 'Unblur',
'boards.rename': 'Rename',
'boards.delete': 'Delete',
'boards.delete_confirm': 'Really delete board "{title}"?',
'boards.delete_confirm.title': 'Delete board',
'boards.show_more': 'Show {count} more…',
'boards.show_less': 'Show less',
'boards.add_link': ' Add link',
'boards.remove_bookmark': 'Remove',
// Onboarding
'onboarding.skip': 'Skip',
'onboarding.back': 'Back',
'onboarding.next': 'Next',
'onboarding.start': 'Let\'s go!',
'onboarding.yes': 'Yes please',
'onboarding.no': 'No thanks',
'onboarding.s1.title': 'Welcome to Hellion Dashboard',
'onboarding.s1.text': 'Your new browser start screen. Minimalist, fast and fully local — no cloud, no account, no data collection.',
'onboarding.s2.title': 'Boards & Bookmarks',
'onboarding.s2.f1': 'Create boards with the "+ Board" button at the top',
'onboarding.s2.f2': 'Import browser bookmarks via the "Import" button in the header',
'onboarding.s2.f3': 'Drag & drop to reorder boards and links',
'onboarding.s2.f4': 'Blur mode for private boards (🔒 icon)',
'onboarding.s3.title': '11 handcrafted themes',
'onboarding.s3.text': 'Click the "Theme" button in the header to choose your theme. Each has its own style and color palette.',
'onboarding.s4.title': 'Widget Toolbar',
'onboarding.s4.f1': 'The floating buttons on the right open widgets',
'onboarding.s4.f2': 'Notes and checklists for quick notes',
'onboarding.s4.f3': 'Calculator with history',
'onboarding.s4.f4': 'Timer/countdown with saveable presets',
'onboarding.s4.f5': 'Image reference widgets (enable in Settings)',
'onboarding.s4.f6': 'Notebook sidebar shows all notes at a glance',
'onboarding.s5.title': 'Don\'t forget backups!',
'onboarding.s5.text': 'Your data is stored locally in the browser. If you clear browser data, it\'s gone! Back up regularly via Settings → Data → Export. We\'ll remind you every 7 days.',
'onboarding.s6.title': 'Gaming Starter Board',
'onboarding.s6.text': 'Do you play games like Satisfactory, Factorio or Star Citizen? I can create a board with useful community links.',
'onboarding.s7.title': 'Ready!',
'onboarding.s7.text': 'Create your first board with "+ Board" or import your browser bookmarks via the Import button in the header. Have fun!',
// Notes
'notes.limit_message': 'Maximum reached! You can have at most {max} notes at the same time. Delete an existing note to create a new one.',
'notes.limit_title': 'Limit reached',
'notes.checklist_title': 'Checklist',
'notes.default_title': 'Note',
'notes.placeholder': 'Write a note...',
'notes.checklist_placeholder': 'New item...',
'notes.delete_confirm': 'Permanently delete note? This cannot be undone.',
'notes.delete_title': 'Delete note',
'notes.delete_button': 'Delete',
'notes.checklist_progress': '{done}/{total} done',
'notes.empty_preview': 'Empty',
'notes.export': 'Export',
'notes.export_footer': 'Exported from Hellion Dashboard',
'notes.create': '+ Create note',
'notes.text_type': '✎ Free text',
'notes.checklist_type': '☑ Checklist',
// Calculator
'calculator.title': 'Calculator',
'calculator.history': 'History',
'calculator.error': 'Error',
// Timer
'timer.title': 'Timer',
'timer.start': 'Start',
'timer.pause': 'Pause',
'timer.reset': 'Reset',
'timer.restart': 'Restart',
'timer.presets': 'Presets',
'timer.save_preset': 'Save preset',
'timer.preset_name_placeholder': 'Name...',
'timer.ok': 'OK',
'timer.limit_title': 'Limit reached',
'timer.limit_message': 'Maximum reached! You can save at most {max} presets.',
'timer.no_time_title': 'No time',
'timer.no_time_message': 'Enter a time before saving a preset.',
'timer.mute': 'Mute sound',
'timer.unmute': 'Unmute sound',
'timer.finished_title': '[!] Timer finished',
'timer.default_page_title': 'Hellion Dashboard',
// Image reference
'imageref.title': 'Image Reference',
'imageref.dropzone': 'Click or drag image here',
'imageref.replace': 'Replace image',
'imageref.label_placeholder': 'Caption (optional)',
'imageref.storage_error': 'Image could not be saved. Browser storage is full.',
'imageref.storage_error.title': 'Storage error',
'imageref.limit': 'Maximum {max} image widgets at a time. Close one to open a new one.',
'imageref.limit.title': 'Limit reached',
'imageref.load_error': 'Image could not be loaded: {error}',
'imageref.load_error.title': 'Image error',
'imageref.invalid_file': 'Please use an image file (PNG, JPG, WebP, etc.).',
'imageref.invalid_file.title': 'Not an image',
// Widget manager
'widget.minimize': 'Minimize',
'widget.close': 'Close',
// Data (export/import)
'data.invalid_format': 'Invalid format',
'data.no_boards': 'No valid boards found',
'data.import_confirm': 'Import {count} boards? Existing data will be preserved.',
'data.import_confirm.title': 'JSON Import',
'data.import_success': '{boards} board(s){notes}{calc}{timer} successfully imported.',
'data.import_success.title': 'Import successful',
'data.import_error': 'Import error: {error}',
'data.import_error.title': 'Import failed',
'data.notes_suffix': ' + {count} note(s)',
'data.calc_suffix': ' + Calculator history',
'data.timer_suffix': ' + Timer presets',
// Browser bookmark import
'bm_import.no_access': 'Cannot access browser bookmarks. Make sure the extension has the required permissions.',
'bm_import.title': 'Bookmark import',
'bm_import.no_folders': 'No bookmark folders found.',
'bm_import.modal_title': 'Import browser bookmarks',
'bm_import.info': 'Select the folders to import as boards. Each folder becomes its own board.',
'bm_import.unnamed': 'Unnamed',
'bm_import.link_count': '{count} link(s)',
'bm_import.folder_count': '{count} folder(s)',
'bm_import.empty': 'empty',
'bm_import.select_all': 'Select all',
'bm_import.deselect_all': 'Deselect all',
'bm_import.import_btn': 'Import',
'bm_import.no_selection': 'Please select at least one folder.',
'bm_import.boards_created': '{count} board(s) created',
'bm_import.bookmarks_imported': '{count} bookmarks imported',
'bm_import.duplicates_skipped': '{count} duplicate(s) skipped',
'bm_import.success_title': 'Import complete',
// Storage
'storage.quota_full': 'Storage full! Please delete old boards or the background image to free up space.',
'storage.quota_full.title': 'Storage full',
// App
'app.backup_reminder': 'You haven\'t made a backup in over a week. If you clear browser data, your boards will be lost. Back up now?',
'app.backup_reminder.title': 'Backup reminder',
'app.backup_now': 'Back up now',
'app.backup_later': 'Later',
'app.no_bookmarks': 'No bookmarks found in this file.',
'app.import_title': 'Import',
'app.html_import_success': '{count} board(s) with {total} bookmarks imported.',
'app.import_success_title': 'Import successful',
'app.invalid_url': 'Invalid URL. Please start with https://.',
'app.invalid_url.title': 'Invalid URL',
// Clock
'clock.days.sun': 'Sun',
'clock.days.mon': 'Mon',
'clock.days.tue': 'Tue',
'clock.days.wed': 'Wed',
'clock.days.thu': 'Thu',
'clock.days.fri': 'Fri',
'clock.days.sat': 'Sat',
'clock.months.jan': 'Jan',
'clock.months.feb': 'Feb',
'clock.months.mar': 'Mar',
'clock.months.apr': 'Apr',
'clock.months.may': 'May',
'clock.months.jun': 'Jun',
'clock.months.jul': 'Jul',
'clock.months.aug': 'Aug',
'clock.months.sep': 'Sep',
'clock.months.oct': 'Oct',
'clock.months.nov': 'Nov',
'clock.months.dec': 'Dec',
// Settings
'settings.file_read_error': 'Error reading file. Please choose a different file.',
'settings.file_read_error.title': 'File error',
'settings.reset_confirm': 'Really delete all boards and settings? This cannot be undone.',
'settings.reset_confirm.title': 'Reset everything',
'settings.reset_confirm.button': 'Delete all',
// Header
'header.import': 'Import',
'header.board': 'Board',
'header.note': 'Note',
'header.theme': 'Theme',
'header.settings': 'Settings',
// Settings panel heading
'settings.title': 'Settings',
// Settings panel sections
'settings.section.widgets': 'WIDGETS',
'settings.section.data': 'DATA & HELP',
'settings.section.danger': 'DANGER ZONE',
'settings.section.bg': 'BACKGROUND',
'settings.section.display': 'DISPLAY',
// Settings rows
'settings.language': 'Language',
'settings.language.desc': 'Choose display language',
'settings.language.auto': 'Automatic',
'settings.toolbar_pos': 'Toolbar position',
'settings.toolbar_pos.desc': 'Show widget toolbar on the left or right',
'settings.toolbar_pos.right': 'Right',
'settings.toolbar_pos.left': 'Left',
'settings.image_ref': 'Image reference widgets',
'settings.image_ref.desc': 'Show images as reference (current session only)',
'settings.export': 'Export backup',
'settings.export.desc': 'Save all boards, notes and settings as JSON',
'settings.export.btn': 'Export',
'settings.import': 'Import backup',
'settings.import.desc': 'Restore a JSON backup',
'settings.browser_import': 'Browser bookmarks',
'settings.browser_import.desc': 'Import bookmarks directly from the browser',
'settings.onboarding': 'Replay onboarding',
'settings.onboarding.desc': 'Show the welcome tour again',
'settings.reset': 'Reset everything',
'settings.reset.desc': 'Deletes all boards, notes and settings',
'settings.compact': 'Compact mode',
'settings.compact.desc': 'Less spacing for more bookmarks',
'settings.shorten': 'Shorten long titles',
'settings.shorten.desc': 'Truncate titles to one line with "…"',
'settings.search': 'Show search bar',
'settings.search.desc': 'Toggle search bar below the header',
'settings.newtab': 'Links in new tab',
'settings.newtab.desc': 'Open bookmarks in a new browser tab',
'settings.showdesc': 'Show descriptions',
'settings.showdesc.desc': 'Show saved description below bookmarks',
'settings.hideextra': 'Hide bookmarks',
'settings.hideextra.desc': 'Hide excess bookmarks in long boards',
'settings.visible_count': 'Visible bookmarks',
'settings.visible_count.desc': 'Number before hiding',
'settings.bg_url': 'Image URL',
'settings.bg_url.desc': 'Custom background image via URL',
'settings.bg_change': 'Change',
'settings.bg_apply': 'Apply',
'settings.bg_upload': 'Upload file',
'settings.bg_upload.desc': 'Use a local image as background',
'settings.search_engine_toggle': 'Switch search engine',
// Modals
'modal.new_board': 'New Board',
'modal.board_name': 'Board name...',
'modal.create': 'Create',
'modal.new_bookmark': 'New Bookmark',
'modal.bm_title': 'Title...',
'modal.bm_desc': 'Description (optional)',
'modal.bm_add': 'Add',
'modal.rename': 'Rename',
'modal.rename_placeholder': 'New name...',
'modal.rename_confirm': 'Rename',
'modal.theme_header': 'Theme',
// About
'about.title': '⬡ HELLION NEWTAB',
'about.impressum': 'Legal Notice',
'about.developer': 'Developer',
'about.company': 'Company',
'about.license': 'License',
'about.storage': 'Data storage',
'about.storage.value': '100% local · No server · No account',
'about.bugreport': 'Bug Report / Feedback',
'about.support': 'Support',
'about.browsers': 'Compatible browsers',
// Notebook
'notebook.title': 'Notebook',
// Search
'search.placeholder': 'Search the web…',
// Widget toolbar tooltips
'toolbar.note': 'Create note',
'toolbar.checklist': 'Create checklist',
'toolbar.calculator': 'Calculator',
'toolbar.timer': 'Timer',
'toolbar.imageref': 'Image reference',
'toolbar.notebook': 'All notes'
}
};
/** @type {string} Aktuell aktive Sprache */
let currentLang = 'de';
/**
* Übersetzungsstring abrufen mit optionalen Platzhaltern
* @param {string} key - Schlüssel im STRINGS-Objekt
* @param {Object} [vars] - Platzhalter-Werte (z.B. { max: 5 })
* @returns {string}
*/
function t(key, vars) {
let str = (STRINGS[currentLang] && STRINGS[currentLang][key])
|| (STRINGS['en'] && STRINGS['en'][key])
|| key;
if (vars) {
for (const [k, v] of Object.entries(vars)) {
str = str.replaceAll('{' + k + '}', v);
}
}
return str;
}
/**
* Alle data-i18n Elemente im Dokument mit aktueller Sprache befüllen
*/
function applyLanguage() {
document.querySelectorAll('[data-i18n]').forEach(el => {
el.textContent = t(el.dataset.i18n);
});
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
el.placeholder = t(el.dataset.i18nPlaceholder);
});
document.querySelectorAll('[data-i18n-title]').forEach(el => {
el.title = t(el.dataset.i18nTitle);
});
}
/**
* 'auto' auflösen zu konkretem Sprachcode
* @param {string} lang - 'de', 'en' oder 'auto'
* @returns {string} 'de' oder 'en'
*/
function resolveLang(lang) {
return (lang === 'auto')
? (navigator.language.startsWith('de') ? 'de' : 'en')
: lang;
}
/**
* Sprache setzen, speichern und DOM aktualisieren
* @param {string} lang - 'de', 'en' oder 'auto'
*/
function setLanguage(lang) {
currentLang = resolveLang(lang);
document.documentElement.lang = currentLang;
applyLanguage();
}
/**
* i18n-Modul — öffentliche API
*/
const I18n = {
/** Aktuell aktive Sprache (nach Auto-Auflösung) */
get currentLang() { return currentLang; },
/**
* Initialisierung: Sprache aus Settings lesen, auflösen, DOM anwenden
* Muss NACH dem Laden des settings-Objekts aufgerufen werden
*/
init() {
const lang = (typeof settings !== 'undefined' && settings.language)
? settings.language
: 'auto';
currentLang = resolveLang(lang);
document.documentElement.lang = currentLang;
applyLanguage();
}
};
+22 -22
View File
@@ -114,8 +114,8 @@ const ImageRef = {
} catch (e) { } catch (e) {
console.warn('ImageRef: sessionStorage Write fehlgeschlagen', e); console.warn('ImageRef: sessionStorage Write fehlgeschlagen', e);
HellionDialog.alert( HellionDialog.alert(
'Bild konnte nicht gespeichert werden. Der Browser-Speicher ist voll.', t('imageref.storage_error'),
{ type: 'danger', title: 'Speicherfehler' } { type: 'danger', title: t('imageref.storage_error.title') }
); );
} }
}, },
@@ -144,8 +144,8 @@ const ImageRef = {
if (this._images.length >= this.MAX_IMAGES) { if (this._images.length >= this.MAX_IMAGES) {
await HellionDialog.alert( await HellionDialog.alert(
'Maximal ' + this.MAX_IMAGES + ' Bild-Widgets gleichzeitig. Schliesse eines um ein neues zu oeffnen.', t('imageref.limit', { max: this.MAX_IMAGES }),
{ type: 'warning', title: 'Limit erreicht' } { type: 'warning', title: t('imageref.limit.title') }
); );
return; return;
} }
@@ -172,8 +172,8 @@ const ImageRef = {
dataUrl = await this._processFile(file); dataUrl = await this._processFile(file);
} catch (err) { } catch (err) {
await HellionDialog.alert( await HellionDialog.alert(
'Bild konnte nicht geladen werden: ' + err.message, t('imageref.load_error', { error: err.message }),
{ type: 'danger', title: 'Bildfehler' } { type: 'danger', title: t('imageref.load_error.title') }
); );
return; return;
} }
@@ -206,7 +206,7 @@ const ImageRef = {
_createWidget(imageData, dataUrl) { _createWidget(imageData, dataUrl) {
WidgetManager.create('image', { WidgetManager.create('image', {
id: imageData.id, id: imageData.id,
title: imageData.label || 'Bild-Referenz', title: imageData.label || t('imageref.title'),
x: imageData.x, x: imageData.x,
y: imageData.y, y: imageData.y,
width: imageData.width, width: imageData.width,
@@ -249,14 +249,14 @@ const ImageRef = {
const img = document.createElement('img'); const img = document.createElement('img');
img.className = 'imgref-img'; img.className = 'imgref-img';
img.src = dataUrl; img.src = dataUrl;
img.alt = imageData.label || 'Bild-Referenz'; img.alt = imageData.label || t('imageref.title');
wrapper.appendChild(img); wrapper.appendChild(img);
// Bild ersetzen Button // Bild ersetzen Button
const replaceBtn = document.createElement('button'); const replaceBtn = document.createElement('button');
replaceBtn.className = 'imgref-replace-btn'; replaceBtn.className = 'imgref-replace-btn';
replaceBtn.type = 'button'; replaceBtn.type = 'button';
replaceBtn.textContent = 'Bild ersetzen'; replaceBtn.textContent = t('imageref.replace');
replaceBtn.addEventListener('click', async () => { replaceBtn.addEventListener('click', async () => {
const file = await this._pickFile(); const file = await this._pickFile();
if (!file) return; if (!file) return;
@@ -266,8 +266,8 @@ const ImageRef = {
this.renderBody(imageData, bodyEl, newDataUrl); this.renderBody(imageData, bodyEl, newDataUrl);
} catch (err) { } catch (err) {
await HellionDialog.alert( await HellionDialog.alert(
'Bild konnte nicht geladen werden: ' + err.message, t('imageref.load_error', { error: err.message }),
{ type: 'danger', title: 'Bildfehler' } { type: 'danger', title: t('imageref.load_error.title') }
); );
} }
}); });
@@ -283,7 +283,7 @@ const ImageRef = {
const label = document.createElement('input'); const label = document.createElement('input');
label.className = 'imgref-label'; label.className = 'imgref-label';
label.type = 'text'; label.type = 'text';
label.placeholder = 'Beschriftung (optional)'; label.placeholder = t('imageref.label_placeholder');
label.maxLength = 100; label.maxLength = 100;
label.value = imageData.label || ''; label.value = imageData.label || '';
@@ -295,8 +295,8 @@ const ImageRef = {
const entry = WidgetManager._widgets.get(imageData.id); const entry = WidgetManager._widgets.get(imageData.id);
if (entry) { if (entry) {
const titleEl = entry.el.querySelector('.widget-title-text'); const titleEl = entry.el.querySelector('.widget-title-text');
if (titleEl) titleEl.textContent = text || 'Bild-Referenz'; if (titleEl) titleEl.textContent = text || t('imageref.title');
entry.state.title = text || 'Bild-Referenz'; entry.state.title = text || t('imageref.title');
} }
this._debouncedSave(); this._debouncedSave();
@@ -321,7 +321,7 @@ const ImageRef = {
icon.textContent = '\uD83D\uDDBC\uFE0F'; icon.textContent = '\uD83D\uDDBC\uFE0F';
const text = document.createElement('span'); const text = document.createElement('span');
text.textContent = 'Klicken oder Bild hierher ziehen'; text.textContent = t('imageref.dropzone');
dropzone.append(icon, text); dropzone.append(icon, text);
@@ -336,8 +336,8 @@ const ImageRef = {
await this.save(); await this.save();
} catch (err) { } catch (err) {
await HellionDialog.alert( await HellionDialog.alert(
'Bild konnte nicht geladen werden: ' + err.message, t('imageref.load_error', { error: err.message }),
{ type: 'danger', title: 'Bildfehler' } { type: 'danger', title: t('imageref.load_error.title') }
); );
} }
}); });
@@ -363,8 +363,8 @@ const ImageRef = {
const file = e.dataTransfer.files[0]; const file = e.dataTransfer.files[0];
if (!file || !file.type.startsWith('image/')) { if (!file || !file.type.startsWith('image/')) {
await HellionDialog.alert( await HellionDialog.alert(
'Bitte eine Bilddatei verwenden (PNG, JPG, WebP, etc.).', t('imageref.invalid_file'),
{ type: 'warning', title: 'Kein Bild' } { type: 'warning', title: t('imageref.invalid_file.title') }
); );
return; return;
} }
@@ -376,8 +376,8 @@ const ImageRef = {
await this.save(); await this.save();
} catch (err) { } catch (err) {
await HellionDialog.alert( await HellionDialog.alert(
'Bild konnte nicht geladen werden: ' + err.message, t('imageref.load_error', { error: err.message }),
{ type: 'danger', title: 'Bildfehler' } { type: 'danger', title: t('imageref.load_error.title') }
); );
} }
}); });
@@ -433,7 +433,7 @@ const ImageRef = {
img.onerror = () => { img.onerror = () => {
URL.revokeObjectURL(objectUrl); URL.revokeObjectURL(objectUrl);
reject(new Error('Bild konnte nicht geladen werden')); reject(new Error(t('imageref.load_error', { error: 'unknown' })));
}; };
img.src = objectUrl; img.src = objectUrl;
+16 -16
View File
@@ -75,15 +75,15 @@ const Notes = {
async create(template) { async create(template) {
if (this._notes.length >= this.MAX_NOTES) { if (this._notes.length >= this.MAX_NOTES) {
await HellionDialog.alert( await HellionDialog.alert(
'Maximale Anzahl erreicht! Du kannst maximal ' + this.MAX_NOTES + ' Notes gleichzeitig haben. Lösche eine bestehende Note um eine neue zu erstellen.', t('notes.limit_message', { max: this.MAX_NOTES }),
{ type: 'warning', title: 'Limit erreicht' } { type: 'warning', title: t('notes.limit_title') }
); );
return null; return null;
} }
const noteData = { const noteData = {
id: 'note_' + uid(), id: 'note_' + uid(),
title: template === 'checklist' ? 'Checkliste' : 'Note', title: template === 'checklist' ? t('notes.checklist_title') : t('notes.default_title'),
content: '', content: '',
template: template, template: template,
x: 120 + (this._notes.length * 30), x: 120 + (this._notes.length * 30),
@@ -138,7 +138,7 @@ const Notes = {
_renderTextBody(noteData, bodyEl) { _renderTextBody(noteData, bodyEl) {
const textarea = document.createElement('textarea'); const textarea = document.createElement('textarea');
textarea.className = 'widget-textarea'; textarea.className = 'widget-textarea';
textarea.placeholder = 'Notiz schreiben...'; textarea.placeholder = t('notes.placeholder');
textarea.spellcheck = false; textarea.spellcheck = false;
textarea.value = noteData.content || ''; textarea.value = noteData.content || '';
textarea.maxLength = this.MAX_CHARS; textarea.maxLength = this.MAX_CHARS;
@@ -204,7 +204,7 @@ const Notes = {
const addInput = document.createElement('input'); const addInput = document.createElement('input');
addInput.className = 'checklist-add-input'; addInput.className = 'checklist-add-input';
addInput.type = 'text'; addInput.type = 'text';
addInput.placeholder = 'Neues Item...'; addInput.placeholder = t('notes.checklist_placeholder');
addInput.maxLength = 100; addInput.maxLength = 100;
addInput.addEventListener('keydown', async (e) => { addInput.addEventListener('keydown', async (e) => {
@@ -276,11 +276,11 @@ const Notes = {
// Auto-Titel: "X/Y erledigt" falls kein manueller Titel // Auto-Titel: "X/Y erledigt" falls kein manueller Titel
const widgetEntry = WidgetManager._widgets.get(noteData.id); const widgetEntry = WidgetManager._widgets.get(noteData.id);
if (widgetEntry) { if (widgetEntry) {
const defaultTitle = done + '/' + total + ' erledigt'; const defaultTitle = t('notes.checklist_progress', { done: done, total: total });
const titleEl = widgetEntry.el.querySelector('.widget-title'); const titleEl = widgetEntry.el.querySelector('.widget-title');
if (titleEl && titleEl.contentEditable !== 'true') { if (titleEl && titleEl.contentEditable !== 'true') {
// Nur wenn Titel noch Standard ist // Nur wenn Titel noch Standard ist
if (noteData.title === 'Checkliste' || /^\d+\/\d+ erledigt$/.test(noteData.title)) { if (noteData.title === t('notes.checklist_title') || /^\d+\/\d+\s/.test(noteData.title)) {
noteData.title = defaultTitle; noteData.title = defaultTitle;
titleEl.textContent = defaultTitle; titleEl.textContent = defaultTitle;
widgetEntry.state.title = defaultTitle; widgetEntry.state.title = defaultTitle;
@@ -307,8 +307,8 @@ const Notes = {
if (idx === -1) return; if (idx === -1) return;
const ok = await HellionDialog.confirm( const ok = await HellionDialog.confirm(
'Note endgültig löschen? Das kann nicht rückgängig gemacht werden.', t('notes.delete_confirm'),
{ type: 'danger', title: 'Note löschen', confirmText: 'Löschen' } { type: 'danger', title: t('notes.delete_title'), confirmText: t('notes.delete_button') }
); );
if (!ok) return; if (!ok) return;
@@ -330,7 +330,7 @@ const Notes = {
} else { } else {
md += noteData.content || ''; md += noteData.content || '';
} }
md += '\n\n---\n*Exportiert aus Hellion Dashboard*\n'; md += '\n\n---\n*' + t('notes.export_footer') + '*\n';
const blob = new Blob([md], { type: 'text/markdown' }); const blob = new Blob([md], { type: 'text/markdown' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
@@ -419,9 +419,9 @@ const Notes = {
if (note.template === 'checklist') { if (note.template === 'checklist') {
const total = note.checklistItems ? note.checklistItems.length : 0; const total = note.checklistItems ? note.checklistItems.length : 0;
const done = note.checklistItems ? note.checklistItems.filter(i => i.checked).length : 0; const done = note.checklistItems ? note.checklistItems.filter(i => i.checked).length : 0;
preview.textContent = done + '/' + total + ' erledigt'; preview.textContent = t('notes.checklist_progress', { done: done, total: total });
} else { } else {
preview.textContent = (note.content || '').slice(0, 50) || 'Leer'; preview.textContent = (note.content || '').slice(0, 50) || t('notes.empty_preview');
} }
// Actions // Actions
@@ -430,7 +430,7 @@ const Notes = {
const btnExport = document.createElement('button'); const btnExport = document.createElement('button');
btnExport.className = 'notebook-slot-btn'; btnExport.className = 'notebook-slot-btn';
btnExport.textContent = 'Export'; btnExport.textContent = t('notes.export');
btnExport.addEventListener('click', (e) => { btnExport.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
this.exportNote(note); this.exportNote(note);
@@ -470,7 +470,7 @@ const Notes = {
slot.className = 'notebook-slot-empty'; slot.className = 'notebook-slot-empty';
const label = document.createElement('span'); const label = document.createElement('span');
label.textContent = '+ Note erstellen'; label.textContent = t('notes.create');
slot.appendChild(label); slot.appendChild(label);
// Klick zeigt Typ-Auswahl // Klick zeigt Typ-Auswahl
@@ -485,7 +485,7 @@ const Notes = {
const btnText = document.createElement('button'); const btnText = document.createElement('button');
btnText.className = 'notebook-type-btn'; btnText.className = 'notebook-type-btn';
btnText.textContent = '\u270E Freitext'; btnText.textContent = t('notes.text_type');
btnText.addEventListener('click', async (e) => { btnText.addEventListener('click', async (e) => {
e.stopPropagation(); e.stopPropagation();
await this.create('text'); await this.create('text');
@@ -494,7 +494,7 @@ const Notes = {
const btnCheck = document.createElement('button'); const btnCheck = document.createElement('button');
btnCheck.className = 'notebook-type-btn'; btnCheck.className = 'notebook-type-btn';
btnCheck.textContent = '\u2611 Checkliste'; btnCheck.textContent = t('notes.checklist_type');
btnCheck.addEventListener('click', async (e) => { btnCheck.addEventListener('click', async (e) => {
e.stopPropagation(); e.stopPropagation();
await this.create('checklist'); await this.create('checklist');
+27 -39
View File
@@ -9,52 +9,40 @@ const Onboarding = {
slides: [ slides: [
{ {
hero: '\u2B21', hero: '\u2B21',
title: 'Willkommen bei Hellion Dashboard', titleKey: 'onboarding.s1.title',
text: 'Dein neuer Browser-Startbildschirm. Minimalistisch, schnell und vollst\u00E4ndig lokal \u2014 keine Cloud, kein Account, keine Datensammlung.' textKey: 'onboarding.s1.text'
}, },
{ {
hero: '\uD83D\uDCCB', hero: '\uD83D\uDCCB',
title: 'Boards & Bookmarks', titleKey: 'onboarding.s2.title',
features: [ featureKeys: ['onboarding.s2.f1', 'onboarding.s2.f2', 'onboarding.s2.f3', 'onboarding.s2.f4']
'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', hero: '\uD83C\uDFA8',
title: '8 handgefertigte Themes', titleKey: 'onboarding.s3.title',
text: 'Klicke auf den \u201ETheme\u201C Button im Header um dein Theme zu w\u00E4hlen. Jedes hat seinen eigenen Stil und Farbpalette.', textKey: 'onboarding.s3.text',
showThemes: true showThemes: true
}, },
{ {
hero: '\uD83E\uDDF0', hero: '\uD83E\uDDF0',
title: 'Widget-Toolbar', titleKey: 'onboarding.s4.title',
features: [ featureKeys: ['onboarding.s4.f1', 'onboarding.s4.f2', 'onboarding.s4.f3', 'onboarding.s4.f4', 'onboarding.s4.f5', 'onboarding.s4.f6']
'Die schwebenden Buttons rechts \u00F6ffnen Widgets',
'Notes und Checklisten f\u00FCr schnelle Notizen',
'Taschenrechner mit History',
'Timer/Countdown mit speicherbaren Presets',
'Bild-Referenz Widgets (aktivierbar in Settings)',
'Notebook-Sidebar zeigt alle Notes auf einen Blick'
]
}, },
{ {
hero: '\uD83D\uDEE1\uFE0F', hero: '\uD83D\uDEE1\uFE0F',
title: 'Backups nicht vergessen!', titleKey: 'onboarding.s5.title',
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.' textKey: 'onboarding.s5.text'
}, },
{ {
hero: '\uD83C\uDFAE', hero: '\uD83C\uDFAE',
title: 'Gaming Starter Board', titleKey: 'onboarding.s6.title',
text: 'Spielst du Games wie Satisfactory, Factorio oder Star Citizen? Ich kann ein Board mit n\u00FCtzlichen Community-Links anlegen.', textKey: 'onboarding.s6.text',
interactive: 'gaming-board' interactive: 'gaming-board'
}, },
{ {
hero: '\uD83D\uDE80', hero: '\uD83D\uDE80',
title: 'Bereit!', titleKey: 'onboarding.s7.title',
text: 'Erstelle dein erstes Board mit \u201E+ Board\u201C oder importiere deine Browser-Lesezeichen \u00FCber den Import-Button im Header. Viel Spa\u00DF!' textKey: 'onboarding.s7.text'
} }
], ],
@@ -87,7 +75,7 @@ const Onboarding = {
if (!isLast) { if (!isLast) {
const skip = document.createElement('button'); const skip = document.createElement('button');
skip.className = 'onboarding-skip'; skip.className = 'onboarding-skip';
skip.textContent = '\u00DCberspringen'; skip.textContent = t('onboarding.skip');
skip.addEventListener('click', () => this._finish()); skip.addEventListener('click', () => this._finish());
modal.appendChild(skip); modal.appendChild(skip);
} }
@@ -103,22 +91,22 @@ const Onboarding = {
const title = document.createElement('div'); const title = document.createElement('div');
title.className = 'onboarding-title'; title.className = 'onboarding-title';
title.textContent = slide.title; title.textContent = t(slide.titleKey);
slideEl.appendChild(title); slideEl.appendChild(title);
if (slide.text) { if (slide.textKey) {
const text = document.createElement('div'); const text = document.createElement('div');
text.className = 'onboarding-text'; text.className = 'onboarding-text';
text.textContent = slide.text; text.textContent = t(slide.textKey);
slideEl.appendChild(text); slideEl.appendChild(text);
} }
if (slide.features) { if (slide.featureKeys) {
const list = document.createElement('ul'); const list = document.createElement('ul');
list.className = 'onboarding-feature-list'; list.className = 'onboarding-feature-list';
slide.features.forEach(f => { slide.featureKeys.forEach(key => {
const li = document.createElement('li'); const li = document.createElement('li');
li.textContent = f; li.textContent = t(key);
list.appendChild(li); list.appendChild(li);
}); });
slideEl.appendChild(list); slideEl.appendChild(list);
@@ -127,7 +115,7 @@ const Onboarding = {
if (slide.showThemes) { if (slide.showThemes) {
const grid = document.createElement('div'); const grid = document.createElement('div');
grid.className = 'onboarding-theme-grid'; grid.className = 'onboarding-theme-grid';
const themeNames = ['Nebula', 'Crescent', 'Event Horizon', 'Merchantman', 'Julia & Jin', 'SC Sunset', 'Hellion HUD', 'Hellion Energy']; const themeNames = ['Nebula', 'Crescent', 'Event Horizon', 'Merchantman', 'Julia & Jin', 'SC Sunset', 'Hellion HUD', 'Hellion Energy', 'Satisfactory', 'Avorion', 'Hellion Stealth'];
themeNames.forEach(name => { themeNames.forEach(name => {
const chip = document.createElement('div'); const chip = document.createElement('div');
chip.className = 'onboarding-theme-chip'; chip.className = 'onboarding-theme-chip';
@@ -160,7 +148,7 @@ const Onboarding = {
if (this.currentSlide > 0) { if (this.currentSlide > 0) {
const backBtn = document.createElement('button'); const backBtn = document.createElement('button');
backBtn.className = 'btn-secondary'; backBtn.className = 'btn-secondary';
backBtn.textContent = 'Zur\u00FCck'; backBtn.textContent = t('onboarding.back');
backBtn.addEventListener('click', () => { backBtn.addEventListener('click', () => {
this.currentSlide--; this.currentSlide--;
this._render(); this._render();
@@ -172,7 +160,7 @@ const Onboarding = {
// Interaktive Slide: Zwei Buttons statt "Weiter" // Interaktive Slide: Zwei Buttons statt "Weiter"
const noBtn = document.createElement('button'); const noBtn = document.createElement('button');
noBtn.className = 'btn-secondary'; noBtn.className = 'btn-secondary';
noBtn.textContent = 'Nein danke'; noBtn.textContent = t('onboarding.no');
noBtn.addEventListener('click', () => { noBtn.addEventListener('click', () => {
this.currentSlide++; this.currentSlide++;
this._render(); this._render();
@@ -180,7 +168,7 @@ const Onboarding = {
const yesBtn = document.createElement('button'); const yesBtn = document.createElement('button');
yesBtn.className = 'btn-primary'; yesBtn.className = 'btn-primary';
yesBtn.textContent = 'Ja, gerne'; yesBtn.textContent = t('onboarding.yes');
yesBtn.addEventListener('click', async () => { yesBtn.addEventListener('click', async () => {
await this._createGamingBoard(); await this._createGamingBoard();
this.currentSlide++; this.currentSlide++;
@@ -191,13 +179,13 @@ const Onboarding = {
} else if (isLast) { } else if (isLast) {
const startBtn = document.createElement('button'); const startBtn = document.createElement('button');
startBtn.className = 'btn-primary'; startBtn.className = 'btn-primary';
startBtn.textContent = 'Los geht\u2019s!'; startBtn.textContent = t('onboarding.start');
startBtn.addEventListener('click', () => this._finish()); startBtn.addEventListener('click', () => this._finish());
nav.appendChild(startBtn); nav.appendChild(startBtn);
} else { } else {
const nextBtn = document.createElement('button'); const nextBtn = document.createElement('button');
nextBtn.className = 'btn-primary'; nextBtn.className = 'btn-primary';
nextBtn.textContent = 'Weiter'; nextBtn.textContent = t('onboarding.next');
nextBtn.addEventListener('click', () => { nextBtn.addEventListener('click', () => {
this.currentSlide++; this.currentSlide++;
this._render(); this._render();
+26 -11
View File
@@ -1,8 +1,12 @@
# ⬡ Opera GX — New-Tab Workaround # ⬡ Opera GX — New-Tab Workaround
Opera GX priorisiert die eigene Speed Dial Seite und ignoriert `chrome_url_overrides` Opera GX ist der einzige Browser in diesem Projekt der sich aktiv dagegen wehrt,
für entpackte Erweiterungen. Um das Hellion Dashboard trotzdem als New-Tab-Seite eine eigene New-Tab-Seite zu nutzen. Während Chrome, Edge, Firefox und selbst Vivaldi
zu etablieren, kommen zwei zusätzliche Skripte zum Einsatz. einfach `chrome_url_overrides` respektieren, priorisiert Opera GX seine eigene
Speed Dial Seite und ignoriert den Standard-Override für entpackte Erweiterungen.
Das Ergebnis: vier Stunden Debugging, zwei Workaround-Skripte und ein Reddit-Thread
der tatsächlich geholfen hat. Hier ist was dabei rausgekommen ist.
--- ---
@@ -11,8 +15,8 @@ zu etablieren, kommen zwei zusätzliche Skripte zum Einsatz.
| Browser | New-Tab Override | Zusatzaufwand | | Browser | New-Tab Override | Zusatzaufwand |
|---|---|---| |---|---|---|
| Chrome / Edge / Brave / Vivaldi | `chrome_url_overrides` | Keiner | | Chrome / Edge / Brave / Vivaldi | `chrome_url_overrides` | Keiner |
| Firefox | `chrome_url_overrides` (MV2) | Eigenes Manifest | | Firefox | `chrome_url_overrides` (MV3) | Eigenes Manifest |
| Opera / Opera GX | Blockiert durch Speed Dial | Workaround nötig | | Opera / Opera GX | Blockiert durch Speed Dial | Dieser Ordner hier |
--- ---
@@ -20,25 +24,36 @@ zu etablieren, kommen zwei zusätzliche Skripte zum Einsatz.
### `background.js` — Tab-Management ### `background.js` — Tab-Management
Überwacht Tab-Aktivitäten im Hintergrund und greift ein bevor Opera seine Startseite lädt. Überwacht Tab-Aktivitäten im Hintergrund und greift ein bevor Opera seine Startseite laden kann.
- Erkennt `opera://startpage/` und `chrome://startpage/` - Erkennt `opera://startpage/` und `chrome://startpage/`
- Leitet per `chrome.tabs.update` auf `newtab.html` um - Leitet per `chrome.tabs.update` auf `newtab.html` um
- Prüft zusätzlich bei `onActivated` — auch im Hintergrund geladene Tabs werden sofort aktualisiert - Prüft zusätzlich bei `onActivated`, weil Opera manche Tabs im Hintergrund lädt
und der erste Redirect dann nicht greift
### `redirect.js` — In-Page Redirect ### `redirect.js` — In-Page Redirect
Einige Opera-Systemprozesse sind so isoliert dass ein externer Eingriff nicht zuverlässig greift. Manche Opera-Systemprozesse sind so weit isoliert dass ein externer Eingriff
nicht zuverlässig ankommt. Also nochmal von innen.
- Wird als Content Script in Opera-Startseiten-Bereiche injiziert - Wird als Content Script direkt in Opera-Startseiten-Bereiche injiziert
- Löst den Redirect bei `document_start` aus — minimale Verzögerung, kein Flackern - Löst den Redirect bei `document_start` aus, bevor die Speed Dial Seite
überhaupt anfangen kann sich aufzubauen
Ja, es braucht wirklich beide Skripte. Opera GX hat das so entschieden.
Das Gute daran: die GitHub Actions kümmern sich darum dass jeder Browser nur bekommt
was er braucht. Das Opera-ZIP enthält die Workaround-Skripte, das Chrome-ZIP nicht.
Wer sich das manuell zusammensuchen müsste wäre vermutlich genauso genervt wie ich
beim Debuggen war.
--- ---
## Datenschutz ## Datenschutz
Kein Tracking, keine Speicherung, keine externen Requests. Kein Tracking, keine Speicherung, keine externen Requests.
Ausschließlich Standard-Browser-APIs `chrome.tabs` um die Kontrolle über den New Tab zurückzugewinnen. Nur Standard-Browser-APIs, `chrome.tabs`, um zurückzubekommen was eigentlich
standardmäßig funktionieren sollte.
**100% lokal. 0% Analytics. Wie im gesamten Hellion NewTab Projekt.** **100% lokal. 0% Analytics. Wie im gesamten Hellion NewTab Projekt.**
+20 -4
View File
@@ -83,6 +83,10 @@ function applySettings() {
const toolbarPosEl = document.getElementById('settingToolbarPos'); const toolbarPosEl = document.getElementById('settingToolbarPos');
if (toolbarPosEl) toolbarPosEl.value = settings.toolbarPos || 'right'; if (toolbarPosEl) toolbarPosEl.value = settings.toolbarPos || 'right';
// Sprache (Dropdown-Wert setzen — I18n.init() übernimmt die eigentliche Anwendung)
const langEl = document.getElementById('settingLanguage');
if (langEl) langEl.value = settings.language || 'auto';
applyTheme(settings.theme || 'nebula', !!settings.bgUrl); applyTheme(settings.theme || 'nebula', !!settings.bgUrl);
if (settings.bgUrl) { if (settings.bgUrl) {
@@ -184,11 +188,22 @@ function bindSettingsEvents() {
await saveSettings(); await saveSettings();
}; };
reader.onerror = () => { reader.onerror = () => {
HellionDialog.alert('Fehler beim Lesen der Datei. Bitte eine andere Datei wählen.', { type: 'danger', title: 'Dateifehler' }); HellionDialog.alert(t('settings.file_read_error'), { type: 'danger', title: t('settings.file_read_error.title') });
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
}); });
// Sprach-Einstellung
const languageEl = document.getElementById('settingLanguage');
if (languageEl) {
languageEl.value = settings.language || 'auto';
languageEl.addEventListener('change', async (e) => {
settings.language = e.target.value;
setLanguage(e.target.value);
await saveSettings();
});
}
// Toolbar-Position Setting // Toolbar-Position Setting
const toolbarPosEl = document.getElementById('settingToolbarPos'); const toolbarPosEl = document.getElementById('settingToolbarPos');
if (toolbarPosEl) { if (toolbarPosEl) {
@@ -209,17 +224,18 @@ function bindSettingsEvents() {
// Reset All // Reset All
document.getElementById('btnResetAll').addEventListener('click', async () => { document.getElementById('btnResetAll').addEventListener('click', async () => {
const ok = await HellionDialog.confirm( const ok = await HellionDialog.confirm(
'Wirklich alle Boards und Einstellungen löschen? Das kann nicht rückgängig gemacht werden.', t('settings.reset_confirm'),
{ type: 'danger', title: 'Alles zurücksetzen', confirmText: 'Alles löschen' } { type: 'danger', title: t('settings.reset_confirm.title'), confirmText: t('settings.reset_confirm.button') }
); );
if (!ok) return; if (!ok) return;
boards = []; boards = [];
settings = { compact: false, shortenTitles: false, newTab: true, showDesc: false, settings = { compact: false, shortenTitles: false, newTab: true, showDesc: false,
hideExtra: false, visibleCount: 10, bgUrl: '', theme: 'nebula', hideExtra: false, visibleCount: 10, bgUrl: '', theme: 'nebula',
showSearch: true, searchEngine: 'google', toolbarPos: 'right', showSearch: true, searchEngine: 'google', toolbarPos: 'right',
imageRefEnabled: false }; imageRefEnabled: false, language: 'auto' };
await saveBoards(); await saveBoards();
await saveSettings(); await saveSettings();
setLanguage('auto');
applySettings(); applySettings();
renderBoards(); renderBoards();
closeSettings(); closeSettings();
+2 -1
View File
@@ -17,7 +17,8 @@ let settings = {
showSearch: true, showSearch: true,
searchEngine: 'google', searchEngine: 'google',
toolbarPos: 'right', toolbarPos: 'right',
imageRefEnabled: false imageRefEnabled: false,
language: 'auto'
}; };
function uid() { function uid() {
+2 -2
View File
@@ -23,7 +23,7 @@ const Store = {
chrome.storage.local.set({ [key]: value }, () => { chrome.storage.local.set({ [key]: value }, () => {
if (chrome.runtime.lastError) { if (chrome.runtime.lastError) {
console.error('Storage-Fehler:', chrome.runtime.lastError.message); console.error('Storage-Fehler:', chrome.runtime.lastError.message);
HellionDialog.alert('Speicher voll! Bitte lösche alte Boards oder das Hintergrundbild, um Platz zu schaffen.', { type: 'danger', title: 'Speicher voll' }); HellionDialog.alert(t('storage.quota_full'), { type: 'danger', title: t('storage.quota_full.title') });
reject(new Error(chrome.runtime.lastError.message)); reject(new Error(chrome.runtime.lastError.message));
return; return;
} }
@@ -35,7 +35,7 @@ const Store = {
resolve(); resolve();
} catch (e) { } catch (e) {
console.error('Storage-Fehler:', e.message); console.error('Storage-Fehler:', e.message);
HellionDialog.alert('Speicher voll! Bitte lösche alte Boards oder das Hintergrundbild, um Platz zu schaffen.', { type: 'danger', title: 'Speicher voll' }); HellionDialog.alert(t('storage.quota_full'), { type: 'danger', title: t('storage.quota_full.title') });
reject(e); reject(e);
} }
} }
+10 -7
View File
@@ -4,14 +4,17 @@
============================================= */ ============================================= */
const THEMES = { const THEMES = {
'nebula': { bg: 'assets/themes/bg-nebula.jpg' }, 'nebula': { bg: 'assets/themes/bg-nebula.webp' },
'crescent': { bg: 'assets/themes/bg-crescent.jpg' }, 'crescent': { bg: 'assets/themes/bg-crescent.webp' },
'event-horizon': { bg: 'assets/themes/bg-event-horizon.jpg' }, 'event-horizon': { bg: 'assets/themes/bg-event-horizon.webp' },
'merchantman': { bg: 'assets/themes/bg-merchantman.webp' }, 'merchantman': { bg: 'assets/themes/bg-merchantman.webp' },
'julia-jin': { bg: 'assets/themes/bg-julia-jin.png' }, 'julia-jin': { bg: 'assets/themes/bg-julia-jin.webp' },
'sc-sunset': { bg: 'assets/themes/bg-sc-sunset.jpg' }, 'sc-sunset': { bg: 'assets/themes/bg-sc-sunset.webp' },
'hellion-hud': { bg: 'assets/themes/bg-hellion-hud.png' }, 'hellion-hud': { bg: 'assets/themes/bg-hellion-hud.webp' },
'hellion-energy': { bg: 'assets/themes/bg-hellion-energy.jpg' } 'hellion-energy': { bg: 'assets/themes/bg-hellion-energy.webp' },
'satisfactory': { bg: 'assets/themes/bg-satisfactory.webp' },
'avorion': { bg: 'assets/themes/bg-avorion.webp' },
'hellion-stealth': { bg: 'assets/themes/bg-scPolaris.webp' }
}; };
function applyTheme(themeName, skipBgOverride) { function applyTheme(themeName, skipBgOverride) {
+17 -17
View File
@@ -82,7 +82,7 @@ const Timer = {
WidgetManager.create('timer', { WidgetManager.create('timer', {
id: this.WIDGET_ID, id: this.WIDGET_ID,
title: 'Timer', title: t('timer.title'),
x: saved.x || 600, x: saved.x || 600,
y: saved.y || 80, y: saved.y || 80,
width: saved.width || 260, width: saved.width || 260,
@@ -190,7 +190,7 @@ const Timer = {
const btnStart = document.createElement('button'); const btnStart = document.createElement('button');
btnStart.className = 'timer-ctrl-btn primary'; btnStart.className = 'timer-ctrl-btn primary';
btnStart.type = 'button'; btnStart.type = 'button';
btnStart.textContent = 'Start'; btnStart.textContent = t('timer.start');
btnStart.addEventListener('click', () => { btnStart.addEventListener('click', () => {
if (!this._running && this._remaining === 0) { if (!this._running && this._remaining === 0) {
this._applyInput(); this._applyInput();
@@ -202,7 +202,7 @@ const Timer = {
const btnPause = document.createElement('button'); const btnPause = document.createElement('button');
btnPause.className = 'timer-ctrl-btn'; btnPause.className = 'timer-ctrl-btn';
btnPause.type = 'button'; btnPause.type = 'button';
btnPause.textContent = 'Pause'; btnPause.textContent = t('timer.pause');
btnPause.disabled = true; btnPause.disabled = true;
btnPause.addEventListener('click', () => this._pause()); btnPause.addEventListener('click', () => this._pause());
this._btnPause = btnPause; this._btnPause = btnPause;
@@ -210,7 +210,7 @@ const Timer = {
const btnReset = document.createElement('button'); const btnReset = document.createElement('button');
btnReset.className = 'timer-ctrl-btn danger'; btnReset.className = 'timer-ctrl-btn danger';
btnReset.type = 'button'; btnReset.type = 'button';
btnReset.textContent = 'Reset'; btnReset.textContent = t('timer.reset');
btnReset.addEventListener('click', () => this._reset()); btnReset.addEventListener('click', () => this._reset());
this._btnReset = btnReset; this._btnReset = btnReset;
@@ -253,13 +253,13 @@ const Timer = {
const title = document.createElement('span'); const title = document.createElement('span');
title.className = 'timer-presets-title'; title.className = 'timer-presets-title';
title.textContent = 'Presets'; title.textContent = t('timer.presets');
const addBtn = document.createElement('button'); const addBtn = document.createElement('button');
addBtn.className = 'timer-preset-add'; addBtn.className = 'timer-preset-add';
addBtn.type = 'button'; addBtn.type = 'button';
addBtn.textContent = '+'; addBtn.textContent = '+';
addBtn.title = 'Preset speichern'; addBtn.title = t('timer.save_preset');
addBtn.addEventListener('click', () => this._showAddPreset(container)); addBtn.addEventListener('click', () => this._showAddPreset(container));
header.append(title, addBtn); header.append(title, addBtn);
@@ -322,8 +322,8 @@ const Timer = {
if (this._presets.length >= this.MAX_PRESETS) { if (this._presets.length >= this.MAX_PRESETS) {
HellionDialog.alert( HellionDialog.alert(
'Maximale Anzahl erreicht! Du kannst maximal ' + this.MAX_PRESETS + ' Presets speichern.', t('timer.limit_message', { max: this.MAX_PRESETS }),
{ type: 'warning', title: 'Limit erreicht' } { type: 'warning', title: t('timer.limit_title') }
); );
return; return;
} }
@@ -334,8 +334,8 @@ const Timer = {
const parsed = this._parseTimeInput(this._inputEl.value); const parsed = this._parseTimeInput(this._inputEl.value);
if (parsed === 0) { if (parsed === 0) {
HellionDialog.alert( HellionDialog.alert(
'Gib zuerst eine Zeit ein, bevor du ein Preset speicherst.', t('timer.no_time_message'),
{ type: 'info', title: 'Keine Zeit' } { type: 'info', title: t('timer.no_time_title') }
); );
return; return;
} }
@@ -347,13 +347,13 @@ const Timer = {
const nameInput = document.createElement('input'); const nameInput = document.createElement('input');
nameInput.className = 'timer-add-input'; nameInput.className = 'timer-add-input';
nameInput.type = 'text'; nameInput.type = 'text';
nameInput.placeholder = 'Name...'; nameInput.placeholder = t('timer.preset_name_placeholder');
nameInput.maxLength = 20; nameInput.maxLength = 20;
const confirmBtn = document.createElement('button'); const confirmBtn = document.createElement('button');
confirmBtn.className = 'timer-add-confirm'; confirmBtn.className = 'timer-add-confirm';
confirmBtn.type = 'button'; confirmBtn.type = 'button';
confirmBtn.textContent = 'OK'; confirmBtn.textContent = t('timer.ok');
const doAdd = async () => { const doAdd = async () => {
const name = nameInput.value.trim(); const name = nameInput.value.trim();
@@ -508,9 +508,9 @@ const Timer = {
_startTitleBlink() { _startTitleBlink() {
this._originalTitle = document.title; this._originalTitle = document.title;
this._blinkIntervalId = setInterval(() => { this._blinkIntervalId = setInterval(() => {
document.title = document.title === '[!] Timer abgelaufen' document.title = document.title === t('timer.finished_title')
? this._originalTitle ? this._originalTitle
: '[!] Timer abgelaufen'; : t('timer.finished_title');
}, 1000); }, 1000);
}, },
@@ -521,7 +521,7 @@ const Timer = {
if (this._blinkIntervalId) { if (this._blinkIntervalId) {
clearInterval(this._blinkIntervalId); clearInterval(this._blinkIntervalId);
this._blinkIntervalId = null; this._blinkIntervalId = null;
document.title = this._originalTitle || 'Hellion Dashboard'; document.title = this._originalTitle || t('timer.default_page_title');
} }
this._finished = false; this._finished = false;
this._updateDisplay(); this._updateDisplay();
@@ -534,7 +534,7 @@ const Timer = {
_updateMuteBtn() { _updateMuteBtn() {
if (!this._muteBtn) return; if (!this._muteBtn) return;
this._muteBtn.textContent = this._muted ? '\uD83D\uDD07' : '\uD83D\uDD0A'; this._muteBtn.textContent = this._muted ? '\uD83D\uDD07' : '\uD83D\uDD0A';
this._muteBtn.title = this._muted ? 'Ton einschalten' : 'Ton ausschalten'; this._muteBtn.title = this._muted ? t('timer.unmute') : t('timer.mute');
this._muteBtn.classList.toggle('muted', this._muted); this._muteBtn.classList.toggle('muted', this._muted);
}, },
@@ -555,7 +555,7 @@ const Timer = {
_updateControls() { _updateControls() {
if (this._btnStart) { if (this._btnStart) {
this._btnStart.disabled = this._running; this._btnStart.disabled = this._running;
this._btnStart.textContent = this._finished ? 'Neustart' : 'Start'; this._btnStart.textContent = this._finished ? t('timer.restart') : t('timer.start');
} }
if (this._btnPause) { if (this._btnPause) {
this._btnPause.disabled = !this._running; this._btnPause.disabled = !this._running;
+4 -4
View File
@@ -20,7 +20,7 @@ const WidgetManager = {
const state = { const state = {
id, id,
type, type,
title: config.title || 'Note', title: config.title || t('notes.default_title'),
x: config.x || 120, x: config.x || 120,
y: config.y || 80, y: config.y || 80,
width: config.width || 280, width: config.width || 280,
@@ -75,7 +75,7 @@ const WidgetManager = {
title.addEventListener('blur', async () => { title.addEventListener('blur', async () => {
title.contentEditable = 'false'; title.contentEditable = 'false';
const newTitle = title.textContent.trim().slice(0, 20); const newTitle = title.textContent.trim().slice(0, 20);
title.textContent = newTitle || 'Note'; title.textContent = newTitle || t('notes.default_title');
const entry = this._widgets.get(state.id); const entry = this._widgets.get(state.id);
if (entry) { if (entry) {
entry.state.title = title.textContent; entry.state.title = title.textContent;
@@ -94,13 +94,13 @@ const WidgetManager = {
const btnMin = document.createElement('button'); const btnMin = document.createElement('button');
btnMin.className = 'widget-btn widget-minimize'; btnMin.className = 'widget-btn widget-minimize';
btnMin.title = 'Minimieren'; btnMin.title = t('widget.minimize');
btnMin.textContent = '\u2500'; btnMin.textContent = '\u2500';
btnMin.addEventListener('click', () => this.minimize(state.id)); btnMin.addEventListener('click', () => this.minimize(state.id));
const btnClose = document.createElement('button'); const btnClose = document.createElement('button');
btnClose.className = 'widget-btn widget-close'; btnClose.className = 'widget-btn widget-close';
btnClose.title = 'Schließen'; btnClose.title = t('widget.close');
btnClose.textContent = '\u2715'; btnClose.textContent = '\u2715';
btnClose.addEventListener('click', () => this.close(state.id)); btnClose.addEventListener('click', () => this.close(state.id));