Initial release v1.2.0 — Hellion NewTab Browser Extension

Persoenlicher Bookmark-Dashboard als Browser-Extension.
8 Themes, Drag & Drop, Sticky Notes, JSON Export/Import.
Chrome, Edge, Brave, Opera, Vivaldi (MV3) + Firefox (MV2).

Includes GitHub Actions for security scanning, code quality
validation, and automated release packaging.
This commit is contained in:
2026-03-20 22:48:21 +01:00
commit 87c30b24d0
30 changed files with 2835 additions and 0 deletions
+92
View File
@@ -0,0 +1,92 @@
# Code-Qualität — Validierung bei Push und PR
name: Code Quality
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
permissions:
contents: read
jobs:
validate:
name: Validate Extension
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: HTML-Validierung (newtab.html existiert)
run: |
echo "Prüfe Projektstruktur..."
test -f manifest.json || (echo "FEHLER: manifest.json fehlt!" && exit 1)
test -f manifest.firefox.json || (echo "FEHLER: manifest.firefox.json fehlt!" && exit 1)
test -f newtab.html || (echo "FEHLER: newtab.html fehlt!" && exit 1)
test -d src/js || (echo "FEHLER: src/js/ fehlt!" && exit 1)
test -d src/css || (echo "FEHLER: src/css/ fehlt!" && exit 1)
test -d assets/icons || (echo "FEHLER: assets/icons/ fehlt!" && exit 1)
test -d assets/themes || (echo "FEHLER: assets/themes/ fehlt!" && exit 1)
echo "Projektstruktur OK"
- name: Manifest-Validierung
run: |
echo "Prüfe manifest.json..."
python3 -c "
import json, sys
with open('manifest.json') as f:
m = json.load(f)
assert m.get('manifest_version') == 3, 'Manifest V3 erwartet'
assert m.get('name'), 'Name fehlt'
assert m.get('version'), 'Version fehlt'
assert 'storage' in m.get('permissions', []), 'Storage Permission fehlt'
print('manifest.json (V3) OK — Version:', m['version'])
with open('manifest.firefox.json') as f:
mf = json.load(f)
assert mf.get('manifest_version') == 2, 'Firefox Manifest V2 erwartet'
assert mf['version'] == m['version'], 'Versionen stimmen nicht überein!'
print('manifest.firefox.json (V2) OK — Version:', mf['version'])
"
- name: JavaScript Syntax-Check
run: |
echo "Prüfe JavaScript-Syntax..."
ERRORS=0
for f in src/js/*.js; do
if ! node --check "$f" 2>&1; then
echo "SYNTAX-FEHLER in $f"
ERRORS=$((ERRORS + 1))
fi
done
if [ "$ERRORS" -gt 0 ]; then
echo "$ERRORS Datei(en) mit Syntax-Fehlern!"
exit 1
fi
echo "Alle JS-Dateien syntaktisch korrekt"
- name: Icon-Dateien prüfen
run: |
for icon in assets/icons/icon16.png assets/icons/icon48.png assets/icons/icon128.png; do
test -f "$icon" || (echo "FEHLER: $icon fehlt!" && exit 1)
done
echo "Alle Icons vorhanden"
- name: Versions-Konsistenz prüfen
run: |
MANIFEST_VER=$(python3 -c "import json; print(json.load(open('manifest.json'))['version'])")
FIREFOX_VER=$(python3 -c "import json; print(json.load(open('manifest.firefox.json'))['version'])")
HTML_VER=$(grep -oP 'Version \K[0-9]+\.[0-9]+\.[0-9]+' newtab.html || echo 'NICHT GEFUNDEN')
echo "manifest.json: $MANIFEST_VER"
echo "manifest.firefox.json: $FIREFOX_VER"
echo "newtab.html: $HTML_VER"
if [ "$MANIFEST_VER" != "$FIREFOX_VER" ]; then
echo "FEHLER: Versionen in Manifests stimmen nicht überein!"
exit 1
fi
if [ "$MANIFEST_VER" != "$HTML_VER" ]; then
echo "WARNUNG: Version in newtab.html ($HTML_VER) weicht ab von Manifest ($MANIFEST_VER)"
exit 1
fi
echo "Alle Versionen konsistent: $MANIFEST_VER"
+67
View File
@@ -0,0 +1,67 @@
# Release — erstellt ZIP-Pakete für Chrome und Firefox bei neuem Tag
name: Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
build-release:
name: Build & Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Version aus Tag extrahieren
id: version
run: echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
- name: Chrome/Edge ZIP erstellen (Manifest V3)
run: |
mkdir -p dist
zip -r "dist/hellion-newtab-${{ steps.version.outputs.tag }}-chrome.zip" \
manifest.json newtab.html src/ assets/ \
-x "*.git*" "dist/*" ".github/*"
- name: Firefox ZIP erstellen (Manifest V2)
run: |
# manifest.firefox.json wird zu manifest.json für Firefox
cp manifest.json manifest.chrome-backup.json
cp manifest.firefox.json manifest.json
zip -r "dist/hellion-newtab-${{ steps.version.outputs.tag }}-firefox.zip" \
manifest.json newtab.html src/ assets/ \
-x "*.git*" "dist/*" ".github/*" "manifest.chrome-backup.json" "manifest.firefox.json"
# Wiederherstellen
mv manifest.chrome-backup.json manifest.json
- name: SHA256 Checksummen erstellen
run: |
cd dist
sha256sum *.zip > checksums-sha256.txt
cat checksums-sha256.txt
- name: GitHub Release erstellen
uses: softprops/action-gh-release@v2
with:
name: "Hellion NewTab ${{ steps.version.outputs.tag }}"
body: |
## Hellion NewTab ${{ steps.version.outputs.tag }}
### Installation
- **Chrome / Edge / Brave / Opera / Vivaldi:** `hellion-newtab-${{ steps.version.outputs.tag }}-chrome.zip` herunterladen und entpacken
- **Firefox:** `hellion-newtab-${{ steps.version.outputs.tag }}-firefox.zip` herunterladen und entpacken
Siehe [README](README.md) für die vollständige Installationsanleitung.
### Checksummen
Siehe `checksums-sha256.txt` zur Integritätsprüfung.
files: |
dist/hellion-newtab-${{ steps.version.outputs.tag }}-chrome.zip
dist/hellion-newtab-${{ steps.version.outputs.tag }}-firefox.zip
dist/checksums-sha256.txt
generate_release_notes: true
+42
View File
@@ -0,0 +1,42 @@
# Sicherheitsprüfung — läuft bei Push und PR auf main/master
name: Security Scan
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
schedule:
# Wöchentlich Montag 06:00 UTC
- cron: '0 6 * * 1'
permissions:
contents: read
security-events: write
jobs:
codeql:
name: CodeQL Analysis
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: javascript
- name: Run CodeQL Analysis
uses: github/codeql-action/analyze@v3
dependency-review:
name: Dependency Review
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Dependency Review
uses: actions/dependency-review-action@v4
+20
View File
@@ -0,0 +1,20 @@
# System
.DS_Store
Thumbs.db
desktop.ini
# Editor
.vscode/
.idea/
*.swp
*.swo
# Build / Temp
dist/
*.zip
*.tar.gz
node_modules/
# Persönliche Backup-Dateien (nicht ins Repo)
favorites_*.html
*_backup*.json
+209
View File
@@ -0,0 +1,209 @@
# ⬡ Hellion NewTab
> **Privates Projekt — Proprietär · Nicht open source**
> Entwickelt von Florian Wathling · [Hellion Online Media](https://hellion-media.de)
Persönlicher Bookmark-Dashboard als Browser-Extension.
**Kein Account. Kein Abo. Keine Cloud. Alle Daten bleiben 100% lokal.**
---
## Features
### 📋 Boards & Bookmarks
- Boards als Gruppen für Links — per Drag & Drop umsortierbar
- Bookmarks mit Favicon, Titel, optionaler Beschreibung
- Boards per 🔒 blurren (Privat-Modus)
### 🔍 Suchleiste
- Google, DuckDuckGo oder Bing — per Klick wechselbar
### 📝 Sticky Note
- Schwebendes Notiz-Widget, frei positionierbar, persistent
### 🎨 8 Themes
| Theme | Akzent | Stil |
|---|---|---|
| Astronaut | Orange | Dark / Space |
| Cosmic Clock | Gold | Warm / Mystisch |
| Void Mage | Lila | Arkan |
| Merchantman | Teal | Industrial Sci-Fi |
| Julia & Jin | Blau | FFXIV Night |
| SC Sunset | Amber | Planet-Side |
| Hellion HUD | Grün | Circuit Board |
| Hellion Energy | Matrix-Grün | Tactical |
### ⚙️ Settings
- Compact mode · Shorten titles · Open in new tab
- Show descriptions · Hide extra bookmarks (5/10/20)
- Hintergrundbild (URL oder lokaler Upload)
- Suchleiste ein/ausblenden
- JSON Export / Import
---
## Browser-Kompatibilität
| Browser | Status | Manifest |
|---|---|---|
| Chrome | ✅ | V3 (`manifest.json`) |
| Edge | ✅ | V3 (`manifest.json`) |
| Brave | ✅ | V3 (`manifest.json`) |
| Opera | ✅ | V3 (`manifest.json`) |
| Opera GX | ✅ | V3 (`manifest.json`) |
| Vivaldi | ✅ | V3 (`manifest.json`) |
| Firefox | ✅ | V2 (`manifest.firefox.json`) |
---
## Installation
### Chrome / Edge / Brave / Opera / Opera GX / Vivaldi
```
1. Repo klonen oder ZIP entpacken
2. chrome://extensions (oder edge:// / brave:// / opera://)
3. Entwicklermodus aktivieren
4. "Entpackte Erweiterung laden" → Ordner "hellion-newtab" auswählen
5. Neuen Tab öffnen ✓
```
### Firefox
Firefox benötigt `manifest.json` im Format V2.
```bash
# manifest.json durch Firefox-Version ersetzen:
copy manifest.firefox.json manifest.json # Windows
cp manifest.firefox.json manifest.json # Linux/Mac
```
```
1. about:debugging#/runtime/this-firefox öffnen
2. "Temporäres Add-on laden"
3. Die manifest.json aus dem hellion-newtab Ordner auswählen
```
> **Hinweis Firefox:** Temporäre Add-ons werden beim Browser-Neustart entfernt.
> Für dauerhafte Installation ist eine signierte `.xpi`-Datei nötig.
> **Wichtig allgemein:** Den Ordner auswählen, in dem `manifest.json` direkt liegt.
---
## Browser-Bookmarks exportieren & importieren
**Chrome / Edge:**
```
Einstellungen → Lesezeichen → Exportieren
```
**Firefox:**
```
Lesezeichen → Alle Lesezeichen → Importieren und Sichern → Als HTML exportieren
```
Die exportierte `.html` Datei über den `Import`-Button in der Extension laden.
---
## Projektstruktur
```
hellion-newtab/
├── manifest.json ← Chrome, Edge, Brave, Opera, Vivaldi (MV3)
├── manifest.firefox.json ← Firefox (MV2)
├── newtab.html ← Haupt-HTML
├── src/
│ ├── js/
│ │ ├── storage.js ← Storage Abstraction (chrome.storage / localStorage)
│ │ ├── state.js ← Globaler State, Defaults, Hilfsfunktionen
│ │ ├── themes.js ← Theme-Definitionen & Anwendungslogik
│ │ ├── boards.js ← Board/Bookmark Rendering & Modals
│ │ ├── drag.js ← Drag & Drop (Pointer Events)
│ │ ├── settings.js ← Settings Panel Logik
│ │ ├── search.js ← Suchleiste
│ │ ├── sticky.js ← Sticky Note Widget
│ │ ├── data.js ← JSON Export / Import
│ │ └── app.js ← Init, Clock, globale Events (Einstiegspunkt)
│ └── css/
│ └── main.css ← Styles + Theme-System (CSS Custom Properties)
└── assets/
├── themes/
│ ├── bg-astronaut.jpg
│ ├── bg-cosmic-clock.jpg
│ ├── bg-void-mage.jpg
│ ├── bg-merchantman.webp
│ ├── bg-julia-jin.png
│ ├── bg-sc-sunset.jpg
│ ├── bg-hellion-hud.png
│ └── bg-hellion-energy.jpg
└── icons/
├── icon16.png
├── icon48.png
└── icon128.png
```
---
## Entwicklung
```bash
# Nach Änderungen: Extension im Browser neu laden
chrome://extensions → Hellion NewTab → ↻ Neu laden
```
---
## Datenschutz
- Keine externe Datenübertragung
- Speicherung in `chrome.storage.local` (Chromium) bzw. `browser.storage.local` (Firefox)
- Keine Tracker, keine Analytics, keine Werbung
- Permissions: `storage`, `bookmarks`
---
## Lizenz & Impressum
**Proprietäres Projekt — alle Rechte vorbehalten.**
| | |
|---|---|
| **Entwickler** | Florian Wathling |
| **Unternehmen** | Hellion Online Media |
| **Web** | [hellion-media.de](https://hellion-media.de) |
| **Impressum** | [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) |
---
## Changelog
### v1.2.0 — 20.03.2026
- Projektstruktur in `src/js/`, `src/css/`, `assets/` aufgeteilt
- JS in Module aufgeteilt (storage, state, themes, boards, drag, settings, search, sticky, data, app)
- Firefox-Kompatibilität (`manifest.firefox.json`, Manifest V2)
- Vivaldi bestätigt kompatibel
### v1.1.0 — 20.03.2026
- 5 neue Themes
- Suchleiste (Google / DDG / Bing)
- Sticky Note Widget
- JSON Export & Import
- Datum neben der Uhr
- About / Impressum in Settings
- Board Blur-Funktion
- Drag & Drop auf Pointer Events umgestellt
- Opera / Opera GX Kompatibilität
### v1.0.0 — 20.03.2026
- Initiales Release
- Boards & Bookmarks mit Drag & Drop
- 3 Themes
- HTML-Import
- Settings Panel
---
> **Hinweis:** Bei der Entwicklung dieses Projekts wurde [Claude Code (Opus 4.6)](https://claude.ai) von Anthropic als Assistent zur Fehleridentifikation und Code-Qualitätssicherung eingesetzt.
Binary file not shown.

After

Width:  |  Height:  |  Size: 672 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

+26
View File
@@ -0,0 +1,26 @@
{
"manifest_version": 2,
"name": "Hellion NewTab",
"version": "1.2.0",
"description": "Personal bookmark dashboard — local, private, no account needed. By Hellion Online Media.",
"author": "Florian Wathling hellion-media.de",
"homepage_url": "https://hellion-media.de",
"chrome_url_overrides": {
"newtab": "newtab.html"
},
"permissions": [
"storage",
"bookmarks"
],
"browser_specific_settings": {
"gecko": {
"id": "hellion-newtab@hellion-media.de",
"strict_min_version": "109.0"
}
},
"icons": {
"16": "assets/icons/icon16.png",
"48": "assets/icons/icon48.png",
"128": "assets/icons/icon128.png"
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"manifest_version": 3,
"name": "Hellion NewTab",
"version": "1.2.0",
"description": "Personal bookmark dashboard — local, private, no account needed. By Hellion Online Media.",
"author": "Florian Wathling hellion-media.de",
"homepage_url": "https://hellion-media.de",
"chrome_url_overrides": {
"newtab": "newtab.html"
},
"permissions": [
"storage",
"bookmarks"
],
"icons": {
"16": "assets/icons/icon16.png",
"48": "assets/icons/icon48.png",
"128": "assets/icons/icon128.png"
}
}
+398
View File
@@ -0,0 +1,398 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hellion NewTab</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;500;600;700&family=Inter:wght@300;400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="src/css/main.css" />
</head>
<body>
<!-- BACKGROUND -->
<div class="bg-layer" id="bgLayer"></div>
<div class="bg-overlay"></div>
<div class="bg-noise"></div>
<!-- HEADER -->
<header class="header">
<div class="header-left">
<span class="logo">⬡ HELLION</span>
<div class="clock-block">
<span class="clock" id="clock">00:00</span>
<span class="date" id="date">Fr, 01. Jan</span>
</div>
</div>
<div class="header-right">
<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>
Import
</button>
<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>
Board
</button>
<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>
Note
</button>
<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>
Settings
</button>
</div>
</header>
<!-- SEARCH BAR -->
<div class="search-bar-wrapper" id="searchBarWrapper">
<div class="search-bar">
<button class="search-engine-toggle" id="searchEngineToggle" title="Suchmaschine wechseln">
<span id="searchEngineIcon">G</span>
</button>
<input type="text" class="search-input" id="searchInput" placeholder="Search the web…" autocomplete="off" />
<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>
</button>
</div>
</div>
<!-- STICKY NOTE -->
<div class="sticky-note" id="stickyNote">
<div class="sticky-note-header" id="stickyNoteHeader">
<span class="sticky-note-title">
<svg width="11" height="11" 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>
<button class="sticky-note-close" id="stickyNoteClose"></button>
</div>
<textarea class="sticky-note-body" id="stickyNoteBody" placeholder="Quick note…" spellcheck="false"></textarea>
</div>
<!-- BOARDS CONTAINER -->
<main class="boards-wrapper" id="boardsWrapper">
<!-- dynamisch via JS -->
</main>
<!-- HIDDEN FILE INPUT FOR IMPORT -->
<input type="file" id="importInput" accept=".html,.htm" style="display:none" />
<!-- SETTINGS PANEL -->
<div class="panel-overlay" id="settingsOverlay"></div>
<aside class="settings-panel" id="settingsPanel">
<div class="panel-header">
<span>Settings</span>
<button class="btn-close" id="btnCloseSettings"></button>
</div>
<div class="panel-body">
<!-- APPEARANCE -->
<section class="settings-section">
<!-- THEME PICKER -->
<h3 class="settings-section-title">THEME</h3>
<div class="theme-grid">
<div class="theme-card active" data-value="astronaut">
<img class="theme-card-img" src="assets/themes/bg-astronaut.jpg" alt="Astronaut" />
<span class="theme-card-label">Astronaut</span>
<span class="theme-card-check"></span>
</div>
<div class="theme-card" data-value="cosmic-clock">
<img class="theme-card-img" src="assets/themes/bg-cosmic-clock.jpg" alt="Cosmic Clock" />
<span class="theme-card-label">Cosmic</span>
<span class="theme-card-check"></span>
</div>
<div class="theme-card" data-value="void-mage">
<img class="theme-card-img" src="assets/themes/bg-void-mage.jpg" alt="Void Mage" />
<span class="theme-card-label">Void Mage</span>
<span class="theme-card-check"></span>
</div>
<div class="theme-card" data-value="merchantman">
<img class="theme-card-img" src="assets/themes/bg-merchantman.webp" alt="Merchantman" />
<span class="theme-card-label">Merchantman</span>
<span class="theme-card-check"></span>
</div>
<div class="theme-card" data-value="julia-jin">
<img class="theme-card-img" src="assets/themes/bg-julia-jin.png" alt="Julia & Jin" />
<span class="theme-card-label">Julia &amp; Jin</span>
<span class="theme-card-check"></span>
</div>
<div class="theme-card" data-value="sc-sunset">
<img class="theme-card-img" src="assets/themes/bg-sc-sunset.jpg" alt="SC Sunset" />
<span class="theme-card-label">SC Sunset</span>
<span class="theme-card-check"></span>
</div>
<div class="theme-card" data-value="hellion-hud">
<img class="theme-card-img" src="assets/themes/bg-hellion-hud.png" alt="Hellion HUD" />
<span class="theme-card-label">HUD</span>
<span class="theme-card-check"></span>
</div>
<div class="theme-card" data-value="hellion-energy">
<img class="theme-card-img" src="assets/themes/bg-hellion-energy.jpg" alt="Hellion Energy" />
<span class="theme-card-label">Energy</span>
<span class="theme-card-check"></span>
</div>
</div>
<h3 class="settings-section-title">APPEARANCE</h3>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Compact mode</span>
<span class="setting-desc">Reduce spacing to show more bookmarks</span>
</div>
<label class="toggle"><input type="checkbox" id="settingCompact" /><span class="slider"></span></label>
</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Shorten long titles</span>
<span class="setting-desc">Shorten title to one line with "…"</span>
</div>
<label class="toggle"><input type="checkbox" id="settingShorten" /><span class="slider"></span></label>
</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Background image</span>
<span class="setting-desc">Custom wallpaper URL or upload</span>
</div>
<button class="btn-small" id="btnChangeBg">Change</button>
</div>
<div class="setting-row" id="bgInputRow" style="display:none">
<input type="text" class="text-input full-width" id="bgUrlInput" placeholder="https://... or leave empty for default" />
<button class="btn-small" id="btnApplyBg">Apply</button>
</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Background file upload</span>
<span class="setting-desc">Use a local image as background</span>
</div>
<button class="btn-small" id="btnBgFile">Upload</button>
<input type="file" id="bgFileInput" accept="image/*" style="display:none" />
</div>
</section>
<!-- BEHAVIOR -->
<section class="settings-section">
<h3 class="settings-section-title">BEHAVIOR</h3>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Open links in new tab</span>
<span class="setting-desc">Open bookmarks in a new browser tab</span>
</div>
<label class="toggle"><input type="checkbox" id="settingNewTab" checked /><span class="slider"></span></label>
</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Show bookmark descriptions</span>
<span class="setting-desc">Display saved descriptions below bookmark titles</span>
</div>
<label class="toggle"><input type="checkbox" id="settingShowDesc" /><span class="slider"></span></label>
</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Hide extra bookmarks in long boards</span>
<span class="setting-desc">Automatically hides extra bookmarks in long boards</span>
</div>
<label class="toggle"><input type="checkbox" id="settingHideExtra" /><span class="slider"></span></label>
</div>
<div class="setting-row" id="visibleCountRow">
<div class="setting-info">
<span class="setting-label">Visible bookmarks before hide</span>
<span class="setting-desc">Choose how many bookmarks are shown</span>
</div>
<select class="select-input" id="settingVisibleCount">
<option value="5">5</option>
<option value="10" selected>10</option>
<option value="20">20</option>
</select>
</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Quick Save shortcut</span>
<span class="setting-desc">Save current page to a board quickly</span>
</div>
<span class="setting-badge" id="quickSaveBadge">Not set</span>
</div>
</section>
<!-- ABOUT / IMPRESSUM -->
<section class="settings-section">
<h3 class="settings-section-title">ABOUT</h3>
<div class="about-block">
<div class="about-logo">⬡ HELLION NEWTAB</div>
<div class="about-version">Version 1.2.0 · by Hellion Online Media</div>
<div class="about-links">
<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>
Impressum
</a>
<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>
hellion-media.de
</a>
</div>
<div class="about-divider"></div>
<div class="about-info-row">
<span class="about-info-label">Entwickler</span>
<span class="about-info-value">Florian Wathling</span>
</div>
<div class="about-info-row">
<span class="about-info-label">Unternehmen</span>
<span class="about-info-value">Hellion Online Media</span>
</div>
<div class="about-info-row">
<span class="about-info-label">Lizenz</span>
<span class="about-info-value">Proprietär · Nicht open source</span>
</div>
<div class="about-info-row">
<span class="about-info-label">Datenspeicherung</span>
<span class="about-info-value">100% lokal · Kein Server · Kein Account</span>
</div>
<div class="about-divider"></div>
<div class="about-bugreport">
<span class="about-info-label" style="display:block;margin-bottom:6px">Bug Report / Feedback</span>
<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>
kontakt@hellion-media.de
</a>
</div>
<div class="about-browsers">
<span class="about-info-label" style="display:block;margin-bottom:6px">Kompatible Browser</span>
<div class="about-browser-tags">
<span class="browser-tag">Chrome</span>
<span class="browser-tag">Edge</span>
<span class="browser-tag">Opera</span>
<span class="browser-tag">Opera GX</span>
<span class="browser-tag">Brave</span>
</div>
</div>
</div>
</section>
<!-- DATA -->
<section class="settings-section">
<h3 class="settings-section-title">DATA</h3>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Export Boards</span>
<span class="setting-desc">Alle Boards als JSON sichern</span>
</div>
<button class="btn-small" id="btnExportJSON">Export</button>
</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Import Boards</span>
<span class="setting-desc">JSON-Backup wiederherstellen</span>
</div>
<button class="btn-small" id="btnImportJSON">Import</button>
<input type="file" id="jsonImportInput" accept=".json" style="display:none" />
</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Suchleiste anzeigen</span>
<span class="setting-desc">Suchleiste unter dem Header ein/aus</span>
</div>
<label class="toggle"><input type="checkbox" id="settingShowSearch" checked /><span class="slider"></span></label>
</div>
</section>
<!-- DANGER ZONE -->
<section class="settings-section">
<h3 class="settings-section-title danger">DANGER ZONE</h3>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Reset all data</span>
<span class="setting-desc">Deletes all boards and bookmarks</span>
</div>
<button class="btn-danger" id="btnResetAll">Reset</button>
</div>
</section>
</div>
</aside>
<!-- ADD BOARD MODAL -->
<div class="modal-overlay" id="addBoardOverlay">
<div class="modal">
<div class="modal-header">
<span>New Board</span>
<button class="btn-close" id="btnCancelBoard"></button>
</div>
<div class="modal-body">
<input type="text" class="text-input full-width" id="newBoardName" placeholder="Board name..." maxlength="40" />
</div>
<div class="modal-footer">
<button class="btn-primary" id="btnConfirmBoard">Create</button>
</div>
</div>
</div>
<!-- ADD BOOKMARK MODAL -->
<div class="modal-overlay" id="addBookmarkOverlay">
<div class="modal">
<div class="modal-header">
<span>New Bookmark</span>
<button class="btn-close" id="btnCancelBookmark"></button>
</div>
<div class="modal-body">
<input type="text" class="text-input full-width" id="newBmTitle" placeholder="Title..." maxlength="60" />
<input type="url" class="text-input full-width" id="newBmUrl" placeholder="https://..." style="margin-top:8px" />
<input type="text" class="text-input full-width" id="newBmDesc" placeholder="Description (optional)" style="margin-top:8px" />
</div>
<div class="modal-footer">
<button class="btn-primary" id="btnConfirmBookmark">Add</button>
</div>
</div>
</div>
<!-- RENAME MODAL -->
<div class="modal-overlay" id="renameOverlay">
<div class="modal">
<div class="modal-header">
<span>Rename</span>
<button class="btn-close" id="btnCancelRename"></button>
</div>
<div class="modal-body">
<input type="text" class="text-input full-width" id="renameInput" placeholder="New name..." maxlength="60" />
</div>
<div class="modal-footer">
<button class="btn-primary" id="btnConfirmRename">Rename</button>
</div>
</div>
</div>
<!-- Storage muss zuerst -->
<script src="src/js/storage.js"></script>
<!-- State & Hilfsfunktionen -->
<script src="src/js/state.js"></script>
<!-- Theme-System -->
<script src="src/js/themes.js"></script>
<!-- Features -->
<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/search.js"></script>
<script src="src/js/sticky.js"></script>
<script src="src/js/data.js"></script>
<!-- Einstiegspunkt zuletzt -->
<script src="src/js/app.js"></script>
</body>
</html>
+968
View File
@@ -0,0 +1,968 @@
/* =============================================
HELLION NEWTAB — Theme System
Themes: astronaut | cosmic-clock | void-mage
============================================= */
@import url('https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;500;600;700&family=Inter:wght@300;400;500&family=Cinzel:wght@400;600&display=swap');
/* ---- BASE VARIABLES (Astronaut = Default) ---- */
:root {
--accent: #ffa032;
--accent-dim: rgba(255, 160, 50, 0.15);
--accent-glow: rgba(255, 160, 50, 0.08);
--border-accent: rgba(255, 160, 50, 0.35);
--bg-primary: #08090d;
--bg-board: rgba(10, 12, 18, 0.48);
--border: rgba(255, 255, 255, 0.06);
--text-primary: #dde0ee;
--text-secondary: #7a7d8e;
--text-muted: #44475a;
--danger: #e05555;
--font-display: 'Rajdhani', sans-serif;
--font-body: 'Inter', sans-serif;
--radius: 8px;
--radius-sm: 5px;
--board-width: 240px;
--spacing: 10px;
--spacing-compact: 5px;
--overlay-bg: linear-gradient(180deg, rgba(8,9,13,0.85) 0%, rgba(8,9,13,0.42) 45%, rgba(8,9,13,0.90) 100%);
--header-bg: rgba(8,9,13,0.90);
--board-hover-border: rgba(255,160,50,0.22);
--toggle-on-bg: rgba(255,160,50,0.22);
--logo-shadow: rgba(255,160,50,0.45);
}
/* ============================================
THEME: ASTRONAUT
============================================ */
[data-theme="astronaut"] {
--accent: #ffa032;
--accent-dim: rgba(255, 160, 50, 0.14);
--accent-glow: rgba(255, 160, 50, 0.06);
--border-accent: rgba(255, 160, 50, 0.32);
--bg-primary: #07080c;
--bg-board: rgba(9, 10, 16, 0.46);
--border: rgba(255, 255, 255, 0.055);
--text-primary: #d8dbe8;
--text-secondary: #72758a;
--text-muted: #404358;
--font-display: 'Rajdhani', sans-serif;
--font-body: 'Inter', sans-serif;
--overlay-bg: linear-gradient(180deg, rgba(7,8,12,0.88) 0%, rgba(7,8,12,0.38) 45%, rgba(7,8,12,0.94) 100%);
--header-bg: rgba(7,8,12,0.92);
--board-hover-border: rgba(255,160,50,0.22);
--toggle-on-bg: rgba(255,160,50,0.22);
--logo-shadow: rgba(255,160,50,0.50);
}
/* ============================================
THEME: COSMIC CLOCK
============================================ */
[data-theme="cosmic-clock"] {
--accent: #d4a843;
--accent-dim: rgba(212, 168, 67, 0.16);
--accent-glow: rgba(212, 168, 67, 0.07);
--border-accent: rgba(212, 168, 67, 0.38);
--bg-primary: #0d090c;
--bg-board: rgba(18, 10, 14, 0.50);
--border: rgba(212, 168, 67, 0.09);
--text-primary: #ecdccc;
--text-secondary: #8a6e5e;
--text-muted: #523930;
--font-display: 'Cinzel', serif;
--font-body: 'Inter', sans-serif;
--overlay-bg: linear-gradient(160deg, rgba(13,9,12,0.90) 0%, rgba(13,9,12,0.42) 50%, rgba(13,9,12,0.94) 100%);
--header-bg: rgba(13,9,12,0.93);
--board-hover-border: rgba(212,168,67,0.28);
--toggle-on-bg: rgba(212,168,67,0.22);
--logo-shadow: rgba(212,168,67,0.55);
}
[data-theme="cosmic-clock"] .logo { font-family: 'Cinzel', serif; letter-spacing: 4px; }
[data-theme="cosmic-clock"] .clock { font-family: 'Cinzel', serif; }
[data-theme="cosmic-clock"] .board-title { letter-spacing: 2px; }
[data-theme="cosmic-clock"] .bm-item:hover { background: rgba(212,168,67,0.05); }
[data-theme="cosmic-clock"] .board { border-color: rgba(212,168,67,0.10); }
/* ============================================
THEME: VOID MAGE
============================================ */
[data-theme="void-mage"] {
--accent: #9b6fff;
--accent-dim: rgba(155, 111, 255, 0.15);
--accent-glow: rgba(155, 111, 255, 0.07);
--border-accent: rgba(155, 111, 255, 0.35);
--bg-primary: #07060f;
--bg-board: rgba(10, 8, 20, 0.50);
--border: rgba(155, 111, 255, 0.09);
--text-primary: #d4cef5;
--text-secondary: #7068a0;
--text-muted: #3e3860;
--font-display: 'Cinzel', serif;
--font-body: 'Inter', sans-serif;
--overlay-bg: linear-gradient(160deg, rgba(7,6,15,0.92) 0%, rgba(7,6,15,0.40) 50%, rgba(7,6,15,0.95) 100%);
--header-bg: rgba(7,6,15,0.93);
--board-hover-border: rgba(155,111,255,0.28);
--toggle-on-bg: rgba(155,111,255,0.22);
--logo-shadow: rgba(155,111,255,0.60);
}
[data-theme="void-mage"] .logo { font-family: 'Cinzel', serif; letter-spacing: 5px; }
[data-theme="void-mage"] .clock { font-family: 'Cinzel', serif; }
[data-theme="void-mage"] .board-title { letter-spacing: 2px; }
[data-theme="void-mage"] .bm-item:hover { background: rgba(155,111,255,0.06); }
[data-theme="void-mage"] .board { border-color: rgba(155,111,255,0.11); }
/* ============================================
THEME: MERCHANTMAN (teal / industrial sci-fi)
============================================ */
[data-theme="merchantman"] {
--accent: #4ecfcf;
--accent-dim: rgba(78, 207, 207, 0.14);
--accent-glow: rgba(78, 207, 207, 0.07);
--border-accent: rgba(78, 207, 207, 0.32);
--bg-primary: #060d0d;
--bg-board: rgba(6, 14, 16, 0.50);
--border: rgba(78, 207, 207, 0.09);
--text-primary: #c8e8e8;
--text-secondary: #5a8888;
--text-muted: #2e5050;
--font-display: 'Rajdhani', sans-serif;
--font-body: 'Inter', sans-serif;
--overlay-bg: linear-gradient(160deg, rgba(6,13,13,0.88) 0%, rgba(6,13,13,0.40) 50%, rgba(6,13,13,0.92) 100%);
--header-bg: rgba(6,13,13,0.93);
--board-hover-border: rgba(78,207,207,0.26);
--toggle-on-bg: rgba(78,207,207,0.22);
--logo-shadow: rgba(78,207,207,0.55);
}
[data-theme="merchantman"] .board { border-color: rgba(78,207,207,0.10); }
[data-theme="merchantman"] .bm-item:hover { background: rgba(78,207,207,0.05); }
/* ============================================
THEME: JULIA & JIN (blue night / FFXIV)
============================================ */
[data-theme="julia-jin"] {
--accent: #5b9fff;
--accent-dim: rgba(91, 159, 255, 0.15);
--accent-glow: rgba(91, 159, 255, 0.07);
--border-accent: rgba(91, 159, 255, 0.35);
--bg-primary: #06080f;
--bg-board: rgba(8, 12, 26, 0.52);
--border: rgba(91, 159, 255, 0.09);
--text-primary: #ccd8f8;
--text-secondary: #5c72a8;
--text-muted: #2e3a60;
--font-display: 'Cinzel', serif;
--font-body: 'Inter', sans-serif;
--overlay-bg: linear-gradient(180deg, rgba(6,8,15,0.85) 0%, rgba(6,8,15,0.38) 50%, rgba(6,8,15,0.90) 100%);
--header-bg: rgba(6,8,15,0.92);
--board-hover-border: rgba(91,159,255,0.26);
--toggle-on-bg: rgba(91,159,255,0.22);
--logo-shadow: rgba(91,159,255,0.55);
}
[data-theme="julia-jin"] .logo { font-family: 'Cinzel', serif; letter-spacing: 4px; }
[data-theme="julia-jin"] .clock { font-family: 'Cinzel', serif; }
[data-theme="julia-jin"] .board-title { letter-spacing: 2px; }
[data-theme="julia-jin"] .board { border-color: rgba(91,159,255,0.10); }
[data-theme="julia-jin"] .bm-item:hover { background: rgba(91,159,255,0.06); }
/* ============================================
THEME: SC SUNSET (amber / planet-side)
============================================ */
[data-theme="sc-sunset"] {
--accent: #f07c30;
--accent-dim: rgba(240, 124, 48, 0.15);
--accent-glow: rgba(240, 124, 48, 0.07);
--border-accent: rgba(240, 124, 48, 0.35);
--bg-primary: #0c0805;
--bg-board: rgba(18, 10, 5, 0.50);
--border: rgba(240, 124, 48, 0.09);
--text-primary: #f0dcc8;
--text-secondary: #906050;
--text-muted: #503828;
--font-display: 'Rajdhani', sans-serif;
--font-body: 'Inter', sans-serif;
--overlay-bg: linear-gradient(180deg, rgba(12,8,5,0.90) 0%, rgba(12,8,5,0.35) 50%, rgba(12,8,5,0.92) 100%);
--header-bg: rgba(12,8,5,0.93);
--board-hover-border: rgba(240,124,48,0.26);
--toggle-on-bg: rgba(240,124,48,0.22);
--logo-shadow: rgba(240,124,48,0.55);
}
[data-theme="sc-sunset"] .board { border-color: rgba(240,124,48,0.10); }
[data-theme="sc-sunset"] .bm-item:hover { background: rgba(240,124,48,0.05); }
/* ============================================
THEME: HELLION HUD (circuit board / red+green)
============================================ */
[data-theme="hellion-hud"] {
--accent: #22cc44;
--accent-dim: rgba(34, 204, 68, 0.14);
--accent-glow: rgba(34, 204, 68, 0.07);
--border-accent: rgba(34, 204, 68, 0.30);
--bg-primary: #080a08;
--bg-board: rgba(8, 12, 8, 0.50);
--border: rgba(34, 204, 68, 0.09);
--text-primary: #c8e8cc;
--text-secondary: #507050;
--text-muted: #2c402c;
--font-display: 'Rajdhani', sans-serif;
--font-body: 'Inter', sans-serif;
--overlay-bg: linear-gradient(160deg, rgba(8,10,8,0.88) 0%, rgba(8,10,8,0.42) 50%, rgba(8,10,8,0.92) 100%);
--header-bg: rgba(8,10,8,0.93);
--board-hover-border: rgba(34,204,68,0.24);
--toggle-on-bg: rgba(34,204,68,0.20);
--logo-shadow: rgba(34,204,68,0.50);
}
[data-theme="hellion-hud"] .board { border-color: rgba(34,204,68,0.10); }
[data-theme="hellion-hud"] .bm-item:hover { background: rgba(34,204,68,0.05); }
/* ============================================
THEME: HELLION ENERGY (matrix / tactical green)
============================================ */
[data-theme="hellion-energy"] {
--accent: #00e87a;
--accent-dim: rgba(0, 232, 122, 0.13);
--accent-glow: rgba(0, 232, 122, 0.06);
--border-accent: rgba(0, 232, 122, 0.28);
--bg-primary: #040a06;
--bg-board: rgba(4, 12, 6, 0.52);
--border: rgba(0, 232, 122, 0.08);
--text-primary: #b8f0d0;
--text-secondary: #3a7050;
--text-muted: #1e3a28;
--font-display: 'Rajdhani', sans-serif;
--font-body: 'Inter', sans-serif;
--overlay-bg: linear-gradient(180deg, rgba(4,10,6,0.90) 0%, rgba(4,10,6,0.40) 50%, rgba(4,10,6,0.94) 100%);
--header-bg: rgba(4,10,6,0.94);
--board-hover-border: rgba(0,232,122,0.22);
--toggle-on-bg: rgba(0,232,122,0.18);
--logo-shadow: rgba(0,232,122,0.50);
}
[data-theme="hellion-energy"] .board { border-color: rgba(0,232,122,0.09); }
[data-theme="hellion-energy"] .bm-item:hover { background: rgba(0,232,122,0.05); }
/* ============================================
BASE STYLES
============================================ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
width: 100%; height: 100%;
background: var(--bg-primary);
color: var(--text-primary);
font-family: var(--font-body);
font-size: 13px;
overflow-x: hidden;
transition: background 0.5s;
}
.bg-layer {
position: fixed; inset: 0; z-index: 0;
background-size: cover; background-position: center;
transition: opacity 0.6s ease;
}
.bg-overlay {
position: fixed; inset: 0; z-index: 1;
background: var(--overlay-bg);
transition: background 0.6s;
}
.bg-noise {
position: fixed; inset: 0; z-index: 2;
opacity: 0.025;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
pointer-events: none;
}
/* HEADER */
.header {
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
height: 48px;
display: flex; align-items: center; justify-content: space-between;
padding: 0 20px;
background: var(--header-bg);
border-bottom: 1px solid var(--border);
backdrop-filter: blur(18px);
transition: background 0.5s, border-color 0.5s;
}
.header-left { display: flex; align-items: center; gap: 20px; }
.header-right { display: flex; align-items: center; gap: 8px; }
.logo {
font-family: var(--font-display);
font-size: 17px; font-weight: 700; letter-spacing: 3px;
color: var(--accent);
text-shadow: 0 0 24px var(--logo-shadow);
transition: color 0.5s, text-shadow 0.5s, font-family 0.1s;
}
.clock-block {
display: flex; flex-direction: column; justify-content: center;
line-height: 1;
}
.clock {
font-family: var(--font-display);
font-size: 20px; font-weight: 500; letter-spacing: 2px;
color: var(--text-secondary);
transition: color 0.5s, font-family 0.1s;
line-height: 1;
}
.date {
font-family: var(--font-body);
font-size: 10px; font-weight: 400; letter-spacing: 0.5px;
color: var(--text-muted);
margin-top: 2px;
transition: color 0.5s;
}
.btn-icon {
display: flex; align-items: center; gap: 5px;
padding: 5px 10px;
background: rgba(255,255,255,0.04);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-secondary);
font-family: var(--font-body); font-size: 12px;
cursor: pointer; transition: all 0.15s;
}
.btn-icon:hover {
background: var(--accent-dim);
border-color: var(--border-accent);
color: var(--accent);
}
/* BOARDS */
.boards-wrapper {
position: relative; z-index: 10;
display: flex; flex-wrap: wrap;
align-content: flex-start;
justify-content: center;
gap: 14px;
padding: 110px 40px 40px;
min-height: 100vh;
}
.board {
width: var(--board-width);
background: var(--bg-board);
border: 1px solid var(--border);
border-radius: var(--radius);
backdrop-filter: blur(12px);
transition: border-color 0.2s, box-shadow 0.2s, background 0.3s;
overflow: hidden;
}
.board:hover {
border-color: var(--board-hover-border);
box-shadow: 0 4px 32px rgba(0,0,0,0.45);
}
.board.drag-over {
border-color: var(--accent);
box-shadow: 0 0 0 2px var(--accent-dim);
}
.board-header {
display: flex; align-items: center; justify-content: space-between;
padding: 8px 10px 7px;
border-bottom: 1px solid var(--border);
user-select: none;
}
.board-drag-handle {
display: flex; align-items: center;
padding: 2px 4px 2px 0;
color: var(--text-muted);
cursor: grab;
flex-shrink: 0;
transition: color 0.15s;
touch-action: none;
}
.board-drag-handle:hover { color: var(--accent); }
.board-drag-handle:active { cursor: grabbing; }
.board-title {
font-family: var(--font-display);
font-size: 13px; font-weight: 600; letter-spacing: 1.5px;
color: var(--accent); text-transform: uppercase;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
max-width: 160px;
transition: color 0.5s;
}
.board-actions { display: flex; gap: 4px; }
.board-action-btn {
width: 22px; height: 22px;
display: flex; align-items: center; justify-content: center;
background: none; border: none;
color: var(--text-muted); border-radius: 4px; cursor: pointer; font-size: 11px;
transition: all 0.15s;
}
.board-action-btn:hover { background: var(--accent-dim); color: var(--accent); }
.board-list { list-style: none; padding: 4px 0; }
.bm-item {
display: flex; align-items: center; gap: 7px;
padding: var(--spacing) 10px;
border-bottom: 1px solid rgba(255,255,255,0.03);
cursor: pointer; transition: background 0.12s; position: relative;
}
.bm-item:last-child { border-bottom: none; }
.bm-item:hover { background: rgba(255,255,255,0.04); }
.bm-item:hover .bm-delete { opacity: 1; }
body.compact .bm-item { padding: var(--spacing-compact) 10px; }
.bm-favicon { width: 14px; height: 14px; flex-shrink: 0; border-radius: 2px; opacity: 0.85; }
.bm-favicon-fallback {
width: 14px; height: 14px; flex-shrink: 0;
background: var(--accent-dim); border-radius: 2px;
display: flex; align-items: center; justify-content: center;
font-size: 8px; color: var(--accent);
}
.bm-text { flex: 1; min-width: 0; }
.bm-title { font-size: 12px; font-weight: 400; color: var(--text-primary); line-height: 1.3; }
body.shorten-titles .bm-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bm-desc { font-size: 10px; color: var(--text-muted); margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: none; }
body.show-desc .bm-desc { display: block; }
.bm-delete {
opacity: 0; width: 18px; height: 18px;
display: flex; align-items: center; justify-content: center;
background: none; border: none; color: var(--text-muted);
border-radius: 3px; cursor: pointer; font-size: 10px;
transition: all 0.12s; flex-shrink: 0;
}
.bm-delete:hover { background: rgba(224,85,85,0.18); color: var(--danger); }
.show-more-btn {
width: 100%; padding: 6px 10px; background: none; border: none;
border-top: 1px solid var(--border); color: var(--text-muted);
font-size: 11px; font-family: var(--font-body); cursor: pointer; text-align: center;
transition: all 0.15s;
}
.show-more-btn:hover { background: var(--accent-dim); color: var(--accent); }
.add-bm-btn {
width: 100%; padding: 6px 10px; background: none; border: none;
border-top: 1px solid var(--border); color: var(--text-muted);
font-size: 11px; font-family: var(--font-body); cursor: pointer;
display: flex; align-items: center; gap: 5px; transition: all 0.15s;
}
.add-bm-btn:hover { background: var(--accent-dim); color: var(--accent); }
.empty-state { padding: 40px; text-align: center; color: var(--text-muted); font-size: 11px; line-height: 1.6; }
/* SETTINGS PANEL */
.panel-overlay {
position: fixed; inset: 0; z-index: 200;
background: rgba(0,0,0,0.55); backdrop-filter: blur(5px);
opacity: 0; pointer-events: none; transition: opacity 0.25s;
}
.panel-overlay.active { opacity: 1; pointer-events: all; }
.settings-panel {
position: fixed; top: 0; right: 0; bottom: 0; z-index: 201;
width: 380px;
background: rgba(8,8,16,0.97);
border-left: 1px solid var(--border);
backdrop-filter: blur(28px);
transform: translateX(100%);
transition: transform 0.3s cubic-bezier(0.4,0,0.2,1);
display: flex; flex-direction: column;
}
.settings-panel.open { transform: translateX(0); }
.panel-header {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 18px; border-bottom: 1px solid var(--border);
font-family: var(--font-display); font-size: 15px; font-weight: 600;
letter-spacing: 2px; color: var(--accent); text-transform: uppercase;
}
.panel-body { flex: 1; overflow-y: auto; padding: 12px 0; scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
.settings-section { margin-bottom: 4px; }
.settings-section-title {
font-family: var(--font-display); font-size: 10px; font-weight: 700;
letter-spacing: 2px; color: var(--text-muted); padding: 10px 18px 6px; text-transform: uppercase;
}
.settings-section-title.danger { color: var(--danger); }
.setting-row {
display: flex; align-items: center; justify-content: space-between;
gap: 12px; padding: 10px 18px;
border-bottom: 1px solid rgba(255,255,255,0.03);
}
.setting-row:last-child { border-bottom: none; }
.setting-info { flex: 1; min-width: 0; }
.setting-label { display: block; font-size: 13px; color: var(--text-primary); }
.setting-desc { display: block; font-size: 11px; color: var(--text-muted); margin-top: 2px; }
.setting-badge {
font-size: 11px; padding: 3px 8px;
background: rgba(255,255,255,0.06); border: 1px solid var(--border);
border-radius: 4px; color: var(--text-muted); white-space: nowrap;
}
/* THEME PICKER */
.theme-grid {
display: grid; grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 8px; padding: 4px 18px 16px;
}
.theme-card {
position: relative; border-radius: 8px; overflow: hidden;
cursor: pointer; border: 2px solid transparent;
transition: border-color 0.2s, transform 0.15s, box-shadow 0.2s;
aspect-ratio: 16/10; background: #111;
}
.theme-card:hover { transform: translateY(-2px); }
.theme-card.active {
border-color: var(--accent);
box-shadow: 0 0 0 1px var(--accent), 0 4px 20px var(--accent-dim);
}
.theme-card-img {
width: 100%; height: 100%; object-fit: cover;
display: block; opacity: 0.65;
transition: opacity 0.2s;
}
.theme-card:hover .theme-card-img { opacity: 0.88; }
.theme-card.active .theme-card-img { opacity: 1; }
.theme-card-label {
position: absolute; bottom: 0; left: 0; right: 0;
padding: 14px 6px 5px;
background: linear-gradient(transparent, rgba(0,0,0,0.90));
font-size: 9px; font-weight: 700; letter-spacing: 1.2px;
text-align: center; text-transform: uppercase;
}
.theme-card[data-value="astronaut"] .theme-card-label { color: #ffa032; }
.theme-card[data-value="cosmic-clock"] .theme-card-label { color: #d4a843; }
.theme-card[data-value="void-mage"] .theme-card-label { color: #9b6fff; }
.theme-card[data-value="merchantman"] .theme-card-label { color: #4ecfcf; }
.theme-card[data-value="julia-jin"] .theme-card-label { color: #5b9fff; }
.theme-card[data-value="sc-sunset"] .theme-card-label { color: #f07c30; }
.theme-card[data-value="hellion-hud"] .theme-card-label { color: #22cc44; }
.theme-card[data-value="hellion-energy"] .theme-card-label { color: #00e87a; }
.theme-card-check {
position: absolute; top: 5px; right: 5px;
width: 16px; height: 16px; border-radius: 50%;
background: var(--accent);
display: flex; align-items: center; justify-content: center;
font-size: 9px; color: #000;
opacity: 0; transition: opacity 0.2s;
}
.theme-card.active .theme-card-check { opacity: 1; }
/* TOGGLE */
.toggle { position: relative; display: inline-block; width: 38px; height: 21px; flex-shrink: 0; }
.toggle input { opacity: 0; width: 0; height: 0; }
.slider {
position: absolute; inset: 0;
background: rgba(255,255,255,0.1); border-radius: 21px; cursor: pointer;
transition: background 0.2s;
}
.slider::before {
content: ''; position: absolute; left: 3px; bottom: 3px;
width: 15px; height: 15px; background: var(--text-muted); border-radius: 50%;
transition: transform 0.2s, background 0.2s;
}
.toggle input:checked + .slider { background: var(--toggle-on-bg); }
.toggle input:checked + .slider::before { transform: translateX(17px); background: var(--accent); }
/* INPUTS */
.select-input {
padding: 5px 8px; background: rgba(255,255,255,0.06);
border: 1px solid var(--border); border-radius: var(--radius-sm);
color: var(--text-primary); font-family: var(--font-body); font-size: 12px;
cursor: pointer; min-width: 70px;
}
.select-input:focus { outline: none; border-color: var(--border-accent); }
.text-input {
padding: 7px 10px; background: rgba(255,255,255,0.05);
border: 1px solid var(--border); border-radius: var(--radius-sm);
color: var(--text-primary); font-family: var(--font-body); font-size: 13px;
}
.text-input:focus { outline: none; border-color: var(--border-accent); background: rgba(255,255,255,0.07); }
.text-input.full-width { width: 100%; }
.btn-small {
padding: 5px 10px; background: rgba(255,255,255,0.06);
border: 1px solid var(--border); border-radius: var(--radius-sm);
color: var(--text-secondary); font-family: var(--font-body); font-size: 12px;
cursor: pointer; white-space: nowrap; transition: all 0.15s;
}
.btn-small:hover { background: var(--accent-dim); border-color: var(--border-accent); color: var(--accent); }
.btn-primary {
padding: 8px 20px; background: var(--accent); border: none;
border-radius: var(--radius-sm); color: #07060f;
font-family: var(--font-display); font-weight: 700; font-size: 14px; letter-spacing: 1px;
cursor: pointer; transition: all 0.15s;
}
.btn-primary:hover { filter: brightness(1.15); box-shadow: 0 0 18px var(--accent-glow); }
.btn-danger {
padding: 5px 12px; background: rgba(224,85,85,0.1);
border: 1px solid rgba(224,85,85,0.3); border-radius: var(--radius-sm);
color: var(--danger); font-family: var(--font-body); font-size: 12px;
cursor: pointer; transition: all 0.15s;
}
.btn-danger:hover { background: rgba(224,85,85,0.2); }
.btn-close {
background: none; border: none; color: var(--text-muted); font-size: 14px;
cursor: pointer; padding: 2px 6px; border-radius: 4px; transition: all 0.15s;
}
.btn-close:hover { background: rgba(255,255,255,0.07); color: var(--text-primary); }
/* MODAL */
.modal-overlay {
position: fixed; inset: 0; z-index: 300;
background: rgba(0,0,0,0.65); backdrop-filter: blur(7px);
display: flex; align-items: center; justify-content: center;
opacity: 0; pointer-events: none; transition: opacity 0.2s;
}
.modal-overlay.active { opacity: 1; pointer-events: all; }
.modal {
background: rgba(10,9,20,0.98); border: 1px solid var(--border);
border-radius: var(--radius); width: 320px; backdrop-filter: blur(28px);
transform: translateY(8px); transition: transform 0.2s;
box-shadow: 0 20px 60px rgba(0,0,0,0.7);
}
.modal-overlay.active .modal { transform: translateY(0); }
.modal-header {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 16px; border-bottom: 1px solid var(--border);
font-family: var(--font-display); font-size: 14px; font-weight: 600;
letter-spacing: 1.5px; color: var(--text-primary); text-transform: uppercase;
}
.modal-body { padding: 14px 16px; }
.modal-footer { padding: 10px 16px 14px; display: flex; justify-content: flex-end; }
.board.dragging { opacity: 0.35; }
.board-placeholder {
border: 2px dashed var(--border-accent);
border-radius: var(--radius);
background: var(--accent-dim);
flex-shrink: 0;
}
/* ---- BOARD BLUR (Private Mode) ---- */
.board.blurred .board-list,
.board.blurred .show-more-btn,
.board.blurred .add-bm-btn {
filter: blur(6px);
user-select: none;
pointer-events: none;
}
.board.blurred .board-title {
filter: blur(5px);
}
.board.blurred {
position: relative;
}
.board-blur-overlay {
display: none;
position: absolute; inset: 0; z-index: 5;
border-radius: var(--radius);
cursor: pointer;
}
.board.blurred .board-blur-overlay {
display: block;
}
.board-blur-overlay:hover::after {
content: '🔒 Click to reveal';
position: absolute; top: 50%; left: 50%;
transform: translate(-50%, -50%);
font-size: 11px; color: var(--text-secondary);
background: rgba(0,0,0,0.6);
padding: 5px 10px; border-radius: 20px;
white-space: nowrap; letter-spacing: 0.5px;
backdrop-filter: blur(4px);
}
.btn-blur-board {
width: 22px; height: 22px;
display: flex; align-items: center; justify-content: center;
background: none; border: none;
color: var(--text-muted); border-radius: 4px; cursor: pointer; font-size: 11px;
transition: all 0.15s;
}
.btn-blur-board:hover { background: var(--accent-dim); color: var(--accent); }
.board.blurred .btn-blur-board { color: var(--accent); opacity: 0.7; }
/* ---- ABOUT BLOCK ---- */
.about-block {
padding: 4px 18px 14px;
}
.about-logo {
font-family: var(--font-display);
font-size: 15px; font-weight: 700; letter-spacing: 3px;
color: var(--accent);
text-shadow: 0 0 18px var(--logo-shadow);
margin-bottom: 3px;
}
.about-version {
font-size: 10px; color: var(--text-muted); letter-spacing: 0.3px;
margin-bottom: 12px;
}
.about-links {
display: flex; gap: 8px; flex-wrap: wrap;
margin-bottom: 12px;
}
.about-link {
display: inline-flex; align-items: center; gap: 5px;
padding: 4px 10px;
background: var(--accent-dim);
border: 1px solid var(--border-accent);
border-radius: 20px;
color: var(--accent); font-size: 11px; font-weight: 500;
text-decoration: none; letter-spacing: 0.3px;
transition: all 0.15s;
}
.about-link:hover {
background: rgba(255,255,255,0.1);
filter: brightness(1.15);
}
.about-divider {
height: 1px; background: var(--border);
margin: 10px 0;
}
.about-info-row {
display: flex; justify-content: space-between; align-items: baseline;
gap: 8px; padding: 4px 0;
border-bottom: 1px solid rgba(255,255,255,0.02);
}
.about-info-row:last-of-type { border-bottom: none; }
.about-info-label {
font-size: 10px; color: var(--text-muted);
letter-spacing: 0.5px; text-transform: uppercase; flex-shrink: 0;
}
.about-info-value {
font-size: 11px; color: var(--text-secondary); text-align: right;
}
.about-bugreport { margin-top: 10px; }
.about-link-mail {
display: inline-flex; align-items: center; gap: 5px;
color: var(--text-secondary); font-size: 11px;
text-decoration: none;
transition: color 0.15s;
}
.about-link-mail:hover { color: var(--accent); }
.about-browsers { margin-top: 10px; }
.about-browser-tags {
display: flex; flex-wrap: wrap; gap: 5px;
}
.browser-tag {
padding: 3px 9px;
background: rgba(255,255,255,0.05);
border: 1px solid var(--border);
border-radius: 20px;
font-size: 10px; color: var(--text-secondary);
letter-spacing: 0.3px;
}
/* ============================================
SEARCH BAR
============================================ */
.search-bar-wrapper {
position: fixed;
top: 48px; left: 0; right: 0;
z-index: 90;
display: flex; justify-content: center;
padding: 8px 20px;
transition: opacity 0.3s, transform 0.3s;
}
.search-bar-wrapper.hidden {
opacity: 0; transform: translateY(-8px); pointer-events: none;
}
.search-bar {
display: flex; align-items: center;
width: 100%; max-width: 560px;
background: var(--bg-board);
border: 1px solid var(--border);
border-radius: 30px;
backdrop-filter: blur(18px);
overflow: hidden;
transition: border-color 0.2s, box-shadow 0.2s;
}
.search-bar:focus-within {
border-color: var(--border-accent);
box-shadow: 0 0 0 3px var(--accent-dim), 0 4px 24px rgba(0,0,0,0.4);
}
.search-engine-toggle {
width: 36px; height: 36px; flex-shrink: 0;
margin: 2px 0 2px 4px;
display: flex; align-items: center; justify-content: center;
background: var(--accent-dim);
border: 1px solid var(--border-accent);
border-radius: 50%;
color: var(--accent);
font-family: var(--font-display); font-size: 14px; font-weight: 700;
cursor: pointer; letter-spacing: 0;
transition: all 0.15s;
flex-shrink: 0;
}
.search-engine-toggle:hover { filter: brightness(1.2); }
.search-input {
flex: 1;
padding: 10px 12px;
background: none; border: none;
color: var(--text-primary);
font-family: var(--font-body); font-size: 14px;
outline: none;
}
.search-input::placeholder { color: var(--text-muted); }
.search-submit {
width: 38px; height: 38px; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
background: none; border: none;
color: var(--text-muted); cursor: pointer;
transition: color 0.15s;
margin-right: 3px;
}
.search-submit:hover { color: var(--accent); }
/* ============================================
STICKY NOTE
============================================ */
.sticky-note {
position: fixed;
bottom: 24px; right: 24px;
z-index: 50;
width: 240px;
background: var(--bg-board);
border: 1px solid var(--border-accent);
border-radius: var(--radius);
backdrop-filter: blur(20px);
box-shadow: 0 8px 40px rgba(0,0,0,0.5), 0 0 0 1px var(--accent-dim);
display: flex; flex-direction: column;
transition: opacity 0.25s, transform 0.25s, border-color 0.5s;
opacity: 0; transform: translateY(8px) scale(0.97);
pointer-events: none;
}
.sticky-note.visible {
opacity: 1; transform: translateY(0) scale(1);
pointer-events: all;
}
.sticky-note-header {
display: flex; align-items: center; justify-content: space-between;
padding: 7px 10px 6px;
border-bottom: 1px solid var(--border);
cursor: move;
user-select: none;
}
.sticky-note-title {
display: flex; align-items: center; gap: 5px;
font-family: var(--font-display); font-size: 11px; font-weight: 600;
letter-spacing: 1.5px; text-transform: uppercase;
color: var(--accent);
}
.sticky-note-close {
background: none; border: none;
color: var(--text-muted); font-size: 11px;
cursor: pointer; padding: 1px 4px; border-radius: 3px;
transition: all 0.15s;
}
.sticky-note-close:hover { background: rgba(255,255,255,0.07); color: var(--text-primary); }
.sticky-note-body {
flex: 1;
padding: 10px;
background: none; border: none; outline: none; resize: none;
color: var(--text-primary);
font-family: var(--font-body); font-size: 12px; line-height: 1.6;
min-height: 120px; max-height: 300px;
scrollbar-width: thin; scrollbar-color: var(--border) transparent;
}
.sticky-note-body::placeholder { color: var(--text-muted); }
/* Resize-handle unten rechts */
.sticky-note::after {
content: '';
position: absolute; bottom: 4px; right: 4px;
width: 8px; height: 8px;
border-right: 2px solid var(--border-accent);
border-bottom: 2px solid var(--border-accent);
border-radius: 0 0 2px 0;
opacity: 0.5;
}
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
/* ============================================
RESPONSIVE — Mobile & Tablet
============================================ */
/* Tablet (max 768px) */
@media (max-width: 768px) {
:root { --board-width: 220px; }
.header { padding: 0 12px; }
.header-left { gap: 12px; }
.header-right { gap: 4px; }
.btn-icon span,
.btn-icon:not(:has(svg)) {
font-size: 0;
}
.btn-icon { padding: 6px 8px; gap: 0; }
.boards-wrapper { padding: 100px 16px 24px; gap: 10px; }
.settings-panel { width: 320px; }
.theme-grid { grid-template-columns: 1fr 1fr; }
.search-bar { max-width: 400px; }
}
/* Smartphone (max 480px) */
@media (max-width: 480px) {
:root { --board-width: 100%; }
.header { height: 42px; padding: 0 10px; }
.logo { font-size: 14px; letter-spacing: 2px; }
.clock { font-size: 16px; }
.date { font-size: 9px; }
.btn-icon { padding: 5px 6px; }
.btn-icon svg { width: 14px; height: 14px; }
.boards-wrapper {
padding: 90px 10px 20px;
gap: 8px;
flex-direction: column;
align-items: stretch;
}
.board { width: 100%; }
.settings-panel { width: 100%; }
.theme-grid { grid-template-columns: 1fr 1fr; gap: 6px; }
.search-bar-wrapper { padding: 6px 10px; }
.search-bar { max-width: 100%; }
.search-input { font-size: 13px; padding: 8px 10px; }
.sticky-note { width: 200px; bottom: 12px; right: 12px; }
.modal { width: calc(100vw - 32px); }
}
+119
View File
@@ -0,0 +1,119 @@
/* =============================================
HELLION NEWTAB — app.js
Einstiegspunkt: Init, Clock, globale Events
============================================= */
async function init() {
const savedBoards = await Store.get('boards');
const savedSettings = await Store.get('settings');
boards = savedBoards ?? getDefaultBoards();
if (savedSettings) Object.assign(settings, savedSettings);
applySettings();
renderBoards();
startClock();
bindGlobalEvents();
bindSettingsEvents();
initSearch();
initStickyNote();
initDataButtons();
Store.checkQuota();
}
// ---- CLOCK & DATE ----
function startClock() {
const DAYS = ['So','Mo','Di','Mi','Do','Fr','Sa'];
const MONTHS = ['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','Dez'];
function tick() {
const now = new Date();
document.getElementById('clock').textContent =
`${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}`;
document.getElementById('date').textContent =
`${DAYS[now.getDay()]}, ${String(now.getDate()).padStart(2,'0')}. ${MONTHS[now.getMonth()]}`;
}
tick();
setInterval(tick, 1000);
}
// ---- GLOBALE EVENTS (Header-Buttons, Modals, Import) ----
function bindGlobalEvents() {
// Header
document.getElementById('btnAddBoard').addEventListener('click', openAddBoardModal);
document.getElementById('btnImport').addEventListener('click', () => {
document.getElementById('importInput').click();
});
// HTML Bookmark Import
document.getElementById('importInput').addEventListener('change', async e => {
const file = e.target.files[0];
if (!file) return;
const imported = parseBookmarkHtml(await file.text());
if (imported.length === 0) { alert('Keine Bookmarks gefunden.'); return; }
boards = [...boards, ...imported];
await saveBoards();
renderBoards();
e.target.value = '';
alert(`${imported.length} Board(s) mit ${imported.reduce((s,b) => s + b.bookmarks.length, 0)} Bookmarks importiert.`);
});
// Add Board Modal
document.getElementById('btnCancelBoard').addEventListener('click', () => closeModal('addBoardOverlay'));
document.getElementById('addBoardOverlay').addEventListener('click', e => {
if (e.target === document.getElementById('addBoardOverlay')) closeModal('addBoardOverlay');
});
document.getElementById('btnConfirmBoard').addEventListener('click', async () => {
const name = document.getElementById('newBoardName').value.trim();
if (!name) return;
boards.push({ id: uid(), title: name, bookmarks: [] });
await saveBoards();
renderBoards();
closeModal('addBoardOverlay');
});
document.getElementById('newBoardName').addEventListener('keydown', e => {
if (e.key === 'Enter') document.getElementById('btnConfirmBoard').click();
if (e.key === 'Escape') closeModal('addBoardOverlay');
});
// Add Bookmark Modal
document.getElementById('btnCancelBookmark').addEventListener('click', () => closeModal('addBookmarkOverlay'));
document.getElementById('addBookmarkOverlay').addEventListener('click', e => {
if (e.target === document.getElementById('addBookmarkOverlay')) closeModal('addBookmarkOverlay');
});
document.getElementById('btnConfirmBookmark').addEventListener('click', async () => {
const title = document.getElementById('newBmTitle').value.trim();
const url = document.getElementById('newBmUrl').value.trim();
const desc = document.getElementById('newBmDesc').value.trim();
if (!title || !url) return;
try { new URL(url); } catch { alert('Ungültige URL. Bitte mit https:// beginnen.'); return; }
const board = boards.find(b => b.id === pendingBookmarkBoardId);
if (!board) return;
board.bookmarks.push({ id: uid(), title, url, desc });
await saveBoards();
renderBoards();
closeModal('addBookmarkOverlay');
});
document.getElementById('newBmUrl').addEventListener('keydown', e => {
if (e.key === 'Enter') document.getElementById('btnConfirmBookmark').click();
if (e.key === 'Escape') closeModal('addBookmarkOverlay');
});
// Rename Modal
document.getElementById('btnCancelRename').addEventListener('click', () => closeModal('renameOverlay'));
document.getElementById('renameOverlay').addEventListener('click', e => {
if (e.target === document.getElementById('renameOverlay')) closeModal('renameOverlay');
});
document.getElementById('btnConfirmRename').addEventListener('click', () => {
const val = document.getElementById('renameInput').value.trim();
if (pendingRenameCallback) pendingRenameCallback(val);
pendingRenameCallback = null;
closeModal('renameOverlay');
});
document.getElementById('renameInput').addEventListener('keydown', e => {
if (e.key === 'Enter') document.getElementById('btnConfirmRename').click();
if (e.key === 'Escape') closeModal('renameOverlay');
});
}
document.addEventListener('DOMContentLoaded', init);
+276
View File
@@ -0,0 +1,276 @@
/* =============================================
HELLION NEWTAB — boards.js
Board & Bookmark Rendering, Modals
============================================= */
let pendingBookmarkBoardId = null;
let pendingRenameCallback = null;
// ---- RENDER ----
function renderBoards() {
const wrapper = document.getElementById('boardsWrapper');
wrapper.innerHTML = '';
if (boards.length === 0) {
wrapper.innerHTML = `<div class="empty-state">
No boards yet. Click <strong style="color:var(--accent)">+ Board</strong> to create one,
or use <strong style="color:var(--accent)">Import</strong> to load your browser bookmarks.
</div>`;
return;
}
boards.forEach(board => wrapper.appendChild(createBoardEl(board)));
initBoardDragDrop();
}
function createBoardEl(board) {
const div = document.createElement('div');
div.className = 'board' + (board.blurred ? ' blurred' : '');
div.dataset.boardId = board.id;
// Header
const header = document.createElement('div');
header.className = 'board-header';
header.innerHTML = `
<span class="board-drag-handle" title="Board verschieben">
<svg width="10" height="14" viewBox="0 0 10 14" fill="currentColor">
<circle cx="2" cy="2" r="1.5"/><circle cx="8" cy="2" r="1.5"/>
<circle cx="2" cy="7" r="1.5"/><circle cx="8" cy="7" r="1.5"/>
<circle cx="2" cy="12" r="1.5"/><circle cx="8" cy="12" r="1.5"/>
</svg>
</span>
<span class="board-title" title="${escHtml(board.title)}">${escHtml(board.title)}</span>
<div class="board-actions">
<button class="board-action-btn btn-blur-board" title="${board.blurred ? 'Unblur' : 'Blur (privat)'}">🔒</button>
<button class="board-action-btn btn-rename-board" title="Umbenennen">✎</button>
<button class="board-action-btn btn-delete-board" title="Löschen">✕</button>
</div>
`;
// Blur-Overlay
const blurOverlay = document.createElement('div');
blurOverlay.className = 'board-blur-overlay';
div.appendChild(blurOverlay);
header.querySelector('.btn-blur-board').addEventListener('click', async e => {
e.stopPropagation();
board.blurred = !board.blurred;
div.classList.toggle('blurred', board.blurred);
e.currentTarget.title = board.blurred ? 'Unblur' : 'Blur (privat)';
await saveBoards();
});
blurOverlay.addEventListener('click', async () => {
board.blurred = false;
div.classList.remove('blurred');
header.querySelector('.btn-blur-board').title = 'Blur (privat)';
await saveBoards();
});
header.querySelector('.btn-rename-board').addEventListener('click', e => {
e.stopPropagation();
openRenameModal(board.title, async newName => {
if (!newName.trim()) return;
board.title = newName.trim();
await saveBoards();
renderBoards();
});
});
header.querySelector('.btn-delete-board').addEventListener('click', e => {
e.stopPropagation();
if (confirm(`Board "${board.title}" löschen?`)) {
boards = boards.filter(b => b.id !== board.id);
saveBoards().then(renderBoards);
}
});
// Bookmark List
const list = document.createElement('ul');
list.className = 'board-list';
list.dataset.boardId = board.id;
const visibleCount = settings.hideExtra ? settings.visibleCount : board.bookmarks.length;
const visible = board.bookmarks.slice(0, visibleCount);
const hidden = board.bookmarks.slice(visibleCount);
visible.forEach(bm => list.appendChild(createBmEl(bm)));
div.appendChild(header);
div.appendChild(list);
// Event Delegation für Bookmark-Klicks und -Löschungen
bindBoardListEvents(list, board);
// Show More
if (hidden.length > 0) {
let expanded = false;
let hiddenEls = [];
const showMoreBtn = document.createElement('button');
showMoreBtn.className = 'show-more-btn';
showMoreBtn.textContent = `Show ${hidden.length} more…`;
showMoreBtn.addEventListener('click', () => {
if (!expanded) {
hidden.forEach(bm => { const el = createBmEl(bm); hiddenEls.push(el); list.appendChild(el); });
showMoreBtn.textContent = 'Show less';
expanded = true;
} else {
hiddenEls.forEach(el => el.remove());
hiddenEls = [];
showMoreBtn.textContent = `Show ${hidden.length} more…`;
expanded = false;
}
});
div.appendChild(showMoreBtn);
}
// Add Bookmark
const addBtn = document.createElement('button');
addBtn.className = 'add-bm-btn';
addBtn.innerHTML = `<svg width="11" height="11" 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> Add link`;
addBtn.addEventListener('click', () => openAddBookmarkModal(board.id));
div.appendChild(addBtn);
initBookmarkDragDrop(list, board);
return div;
}
function createBmEl(bm) {
const li = document.createElement('li');
li.className = 'bm-item';
li.dataset.bmId = bm.id;
li.dataset.bmUrl = bm.url;
li.draggable = true;
const favicon = document.createElement('img');
favicon.className = 'bm-favicon';
favicon.width = 14;
favicon.height = 14;
favicon.src = getFaviconUrl(bm.url);
favicon.addEventListener('error', function() {
this.style.display = 'none';
this.nextElementSibling.style.display = 'flex';
});
const fallback = document.createElement('div');
fallback.className = 'bm-favicon-fallback';
fallback.style.display = 'none';
fallback.textContent = bm.title.charAt(0).toUpperCase();
const textDiv = document.createElement('div');
textDiv.className = 'bm-text';
const titleSpan = document.createElement('span');
titleSpan.className = 'bm-title';
titleSpan.title = bm.title;
titleSpan.textContent = bm.title;
const descSpan = document.createElement('span');
descSpan.className = 'bm-desc';
descSpan.textContent = bm.desc || '';
textDiv.appendChild(titleSpan);
textDiv.appendChild(descSpan);
const deleteBtn = document.createElement('button');
deleteBtn.className = 'bm-delete';
deleteBtn.title = 'Entfernen';
deleteBtn.textContent = '✕';
li.appendChild(favicon);
li.appendChild(fallback);
li.appendChild(textDiv);
li.appendChild(deleteBtn);
return li;
}
// Event Delegation: Ein Listener pro Board-Liste statt pro Bookmark
function bindBoardListEvents(list, board) {
list.addEventListener('click', async e => {
const bmItem = e.target.closest('.bm-item');
if (!bmItem) return;
// Delete-Button geklickt
if (e.target.closest('.bm-delete')) {
e.stopPropagation();
const bmId = bmItem.dataset.bmId;
board.bookmarks = board.bookmarks.filter(b => b.id !== bmId);
await saveBoards();
renderBoards();
return;
}
// Bookmark-Link geklickt
const url = bmItem.dataset.bmUrl;
if (url) {
window.open(url, settings.newTab ? '_blank' : '_self', 'noopener,noreferrer');
}
});
}
// ---- MODALS ----
function openModal(id) { document.getElementById(id).classList.add('active'); }
function closeModal(id) { document.getElementById(id).classList.remove('active'); }
function openAddBoardModal() {
document.getElementById('newBoardName').value = '';
openModal('addBoardOverlay');
setTimeout(() => document.getElementById('newBoardName').focus(), 50);
}
function openAddBookmarkModal(boardId) {
pendingBookmarkBoardId = boardId;
['newBmTitle','newBmUrl','newBmDesc'].forEach(id => document.getElementById(id).value = '');
openModal('addBookmarkOverlay');
setTimeout(() => document.getElementById('newBmTitle').focus(), 50);
}
function openRenameModal(currentName, callback) {
pendingRenameCallback = callback;
document.getElementById('renameInput').value = currentName;
openModal('renameOverlay');
setTimeout(() => { const i = document.getElementById('renameInput'); i.focus(); i.select(); }, 50);
}
// ---- BOOKMARK HTML IMPORT ----
function parseBookmarkHtml(html) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const result = [];
function parseFolder(dlEl, folderName) {
const bms = [];
dlEl.querySelectorAll(':scope > dt').forEach(dt => {
const a = dt.querySelector(':scope > a');
const h3 = dt.querySelector(':scope > h3');
if (a && a.href) {
bms.push({ id: uid(), title: a.textContent.trim() || a.href, url: a.href, desc: '' });
} else if (h3) {
const subDl = dt.querySelector(':scope > dl');
if (subDl) {
const sub = parseFolder(subDl, h3.textContent.trim());
if (sub.bookmarks.length > 0) result.push(sub);
}
}
});
return { id: uid(), title: folderName || 'Imported', bookmarks: bms };
}
const topDts = doc.querySelectorAll('body > dl > dt, body > p > dl > dt');
if (topDts.length === 0) {
const allLinks = [];
doc.querySelectorAll('a').forEach(a => {
if (a.href && !a.href.startsWith('place:'))
allLinks.push({ id: uid(), title: a.textContent.trim() || a.href, url: a.href, desc: '' });
});
if (allLinks.length > 0) result.push({ id: uid(), title: 'Imported Bookmarks', bookmarks: allLinks });
} else {
doc.querySelectorAll('body > dl > dt, body > p > dl > dt').forEach(dt => {
const h3 = dt.querySelector(':scope > h3');
const dl = dt.querySelector(':scope > dl');
if (h3 && dl) {
const folder = parseFolder(dl, h3.textContent.trim());
if (folder.bookmarks.length > 0) result.push(folder);
}
});
}
return result;
}
+55
View File
@@ -0,0 +1,55 @@
/* =============================================
HELLION NEWTAB — data.js
JSON Export / Import (Backup & Restore)
============================================= */
function initDataButtons() {
const btnExport = document.getElementById('btnExportJSON');
const btnImport = document.getElementById('btnImportJSON');
const jsonInput = document.getElementById('jsonImportInput');
if (!btnExport || !btnImport) return;
// Export
btnExport.addEventListener('click', () => {
const data = { version: '1.2.0', exported: new Date().toISOString(), boards, settings };
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `hellion-newtab-backup-${new Date().toISOString().slice(0, 10)}.json`;
a.click();
URL.revokeObjectURL(url);
});
// Import
btnImport.addEventListener('click', () => jsonInput.click());
jsonInput.addEventListener('change', async e => {
const file = e.target.files[0];
if (!file) return;
try {
const data = JSON.parse(await file.text());
if (!Array.isArray(data.boards)) throw new Error('Ungültiges Format');
const validBoards = data.boards.filter(b => {
if (!b || typeof b.title !== 'string' || !Array.isArray(b.bookmarks)) return false;
b.id = b.id || uid();
b.blurred = !!b.blurred;
b.bookmarks = b.bookmarks.filter(bm => {
if (!bm || typeof bm.title !== 'string' || typeof bm.url !== 'string') return false;
bm.id = bm.id || uid();
bm.desc = bm.desc || '';
return true;
});
return true;
});
if (validBoards.length === 0) throw new Error('Keine gültigen Boards gefunden');
if (!confirm(`${validBoards.length} Boards importieren? Bestehende Daten bleiben erhalten.`)) return;
boards = [...boards, ...validBoards];
await saveBoards();
renderBoards();
alert(`${validBoards.length} Board(s) importiert.`);
} catch (err) {
alert('Fehler beim Import: ' + err.message);
}
e.target.value = '';
});
}
+132
View File
@@ -0,0 +1,132 @@
/* =============================================
HELLION NEWTAB — drag.js
Drag & Drop via Pointer Events
Boards: Reihenfolge per Handle
Bookmarks: Reihenfolge innerhalb eines Boards
============================================= */
// ---- BOARD DRAG (Pointer Events) ----
function initBoardDragDrop() {
const wrapper = document.getElementById('boardsWrapper');
let dragging = null;
let placeholder = null;
function getInsertTarget(clientX, clientY) {
const boardEls = Array.from(wrapper.querySelectorAll('.board:not(.dragging)'));
for (const b of boardEls) {
const r = b.getBoundingClientRect();
if (clientX >= r.left && clientX <= r.right && clientY >= r.top && clientY <= r.bottom) {
return { el: b, before: clientX < r.left + r.width / 2 };
}
}
return null;
}
wrapper.querySelectorAll('.board').forEach(boardEl => {
const handle = boardEl.querySelector('.board-drag-handle');
if (!handle) return;
handle.style.cursor = 'grab';
handle.addEventListener('pointerdown', e => {
e.preventDefault();
handle.setPointerCapture(e.pointerId);
handle.style.cursor = 'grabbing';
const rect = boardEl.getBoundingClientRect();
// Ghost
const ghost = boardEl.cloneNode(true);
ghost.style.cssText = `
position:fixed; left:${rect.left}px; top:${rect.top}px;
width:${rect.width}px; height:${rect.height}px;
opacity:0.75; pointer-events:none; z-index:9999;
transform:rotate(1.5deg) scale(1.02);
box-shadow:0 12px 40px rgba(0,0,0,0.6);
`;
document.body.appendChild(ghost);
// Placeholder
placeholder = document.createElement('div');
placeholder.className = 'board-placeholder';
placeholder.style.cssText = `width:${rect.width}px; height:${rect.height}px;`;
boardEl.parentNode.insertBefore(placeholder, boardEl);
boardEl.classList.add('dragging');
dragging = { el: boardEl, ghost,
offsetX: e.clientX - rect.left,
offsetY: e.clientY - rect.top
};
});
handle.addEventListener('pointermove', e => {
if (!dragging || dragging.el !== boardEl) return;
e.preventDefault();
dragging.ghost.style.left = (e.clientX - dragging.offsetX) + 'px';
dragging.ghost.style.top = (e.clientY - dragging.offsetY) + 'px';
const target = getInsertTarget(e.clientX, e.clientY);
if (target && target.el !== boardEl) {
target.before
? target.el.parentNode.insertBefore(placeholder, target.el)
: target.el.parentNode.insertBefore(placeholder, target.el.nextSibling);
}
});
handle.addEventListener('pointerup', async () => {
if (!dragging || dragging.el !== boardEl) return;
handle.style.cursor = 'grab';
placeholder.parentNode.insertBefore(boardEl, placeholder);
placeholder.remove(); placeholder = null;
boardEl.classList.remove('dragging');
dragging.ghost.remove();
dragging = null;
// Neue Reihenfolge aus DOM ablesen
const newOrder = Array.from(wrapper.querySelectorAll('.board'))
.map(el => el.dataset.boardId).filter(Boolean);
boards.sort((a, b) => newOrder.indexOf(a.id) - newOrder.indexOf(b.id));
await saveBoards();
});
handle.addEventListener('pointercancel', () => {
if (!dragging) return;
dragging.ghost.remove();
if (placeholder) { placeholder.remove(); placeholder = null; }
boardEl.classList.remove('dragging');
dragging = null;
handle.style.cursor = 'grab';
});
});
}
// ---- BOOKMARK DRAG (innerhalb eines Boards) ----
function initBookmarkDragDrop(listEl, board) {
let dragSrcBmId = null;
listEl.querySelectorAll('.bm-item').forEach(item => {
item.addEventListener('dragstart', e => {
dragSrcBmId = item.dataset.bmId;
e.dataTransfer.effectAllowed = 'move';
setTimeout(() => item.style.opacity = '0.4', 0);
});
item.addEventListener('dragend', () => { item.style.opacity = ''; });
item.addEventListener('dragover', e => {
e.preventDefault();
item.style.background = 'rgba(255,160,50,0.07)';
});
item.addEventListener('dragleave', () => { item.style.background = ''; });
item.addEventListener('drop', async e => {
e.preventDefault(); e.stopPropagation();
item.style.background = '';
const targetBmId = item.dataset.bmId;
if (!dragSrcBmId || dragSrcBmId === targetBmId) return;
const srcIdx = board.bookmarks.findIndex(b => b.id === dragSrcBmId);
const tgtIdx = board.bookmarks.findIndex(b => b.id === targetBmId);
const [moved] = board.bookmarks.splice(srcIdx, 1);
board.bookmarks.splice(tgtIdx, 0, moved);
await saveBoards();
renderBoards();
});
});
}
+41
View File
@@ -0,0 +1,41 @@
/* =============================================
HELLION NEWTAB — search.js
Suchleiste: Google / DuckDuckGo / Bing
============================================= */
function initSearch() {
const input = document.getElementById('searchInput');
const submit = document.getElementById('searchSubmit');
const toggle = document.getElementById('searchEngineToggle');
const icon = document.getElementById('searchEngineIcon');
if (!input) return;
const engines = {
google: { label: 'G', url: 'https://www.google.com/search?q=' },
ddg: { label: '⊙', url: 'https://duckduckgo.com/?q=' },
bing: { label: 'B', url: 'https://www.bing.com/search?q=' },
};
function updateIcon() {
icon.textContent = engines[settings.searchEngine]?.label ?? 'G';
}
updateIcon();
function doSearch() {
const q = input.value.trim();
if (!q) return;
const engine = engines[settings.searchEngine] ?? engines.google;
window.open(engine.url + encodeURIComponent(q), settings.newTab ? '_blank' : '_self');
input.value = '';
}
toggle.addEventListener('click', async () => {
const keys = Object.keys(engines);
settings.searchEngine = keys[(keys.indexOf(settings.searchEngine) + 1) % keys.length];
updateIcon();
await saveSettings();
});
submit.addEventListener('click', doSearch);
input.addEventListener('keydown', e => { if (e.key === 'Enter') doSearch(); });
}
+140
View File
@@ -0,0 +1,140 @@
/* =============================================
HELLION NEWTAB — settings.js
Settings Panel: Toggles, Hintergrund, Theme-Picker
============================================= */
function openSettings() {
document.getElementById('settingsPanel').classList.add('open');
document.getElementById('settingsOverlay').classList.add('active');
}
function closeSettings() {
document.getElementById('settingsPanel').classList.remove('open');
document.getElementById('settingsOverlay').classList.remove('active');
}
function applySettings() {
const body = document.body;
body.classList.toggle('compact', settings.compact);
body.classList.toggle('shorten-titles', settings.shortenTitles);
body.classList.toggle('show-desc', settings.showDesc);
document.getElementById('settingCompact').checked = settings.compact;
document.getElementById('settingShorten').checked = settings.shortenTitles;
document.getElementById('settingNewTab').checked = settings.newTab;
document.getElementById('settingShowDesc').checked = settings.showDesc;
document.getElementById('settingHideExtra').checked = settings.hideExtra;
document.getElementById('settingVisibleCount').value = String(settings.visibleCount);
document.getElementById('visibleCountRow').style.opacity = settings.hideExtra ? '1' : '0.4';
// showSearch: undefined (alter Save) → true
if (settings.showSearch === undefined) settings.showSearch = true;
const searchWrapper = document.getElementById('searchBarWrapper');
if (searchWrapper) searchWrapper.classList.toggle('hidden', !settings.showSearch);
const showSearchEl = document.getElementById('settingShowSearch');
if (showSearchEl) showSearchEl.checked = settings.showSearch;
applyTheme(settings.theme || 'astronaut', !!settings.bgUrl);
if (settings.bgUrl) {
document.getElementById('bgLayer').style.backgroundImage = `url('${settings.bgUrl}')`;
}
}
function bindSettingsEvents() {
// Panel
document.getElementById('settingsOverlay').addEventListener('click', closeSettings);
document.getElementById('btnCloseSettings').addEventListener('click', closeSettings);
document.getElementById('btnSettings').addEventListener('click', openSettings);
// Theme-Picker
document.querySelectorAll('.theme-card').forEach(card => {
card.addEventListener('click', async () => {
const name = card.dataset.value;
if (!name || name === settings.theme) return;
settings.theme = name;
settings.bgUrl = '';
document.getElementById('bgUrlInput').value = '';
applyTheme(name, false);
await saveSettings();
});
});
// Toggles
const toggleMap = {
settingCompact: v => { settings.compact = v; document.body.classList.toggle('compact', v); },
settingShorten: v => { settings.shortenTitles = v; document.body.classList.toggle('shorten-titles', v); },
settingNewTab: v => { settings.newTab = v; },
settingShowDesc: v => { settings.showDesc = v; document.body.classList.toggle('show-desc', v); },
settingHideExtra: v => {
settings.hideExtra = v;
document.getElementById('visibleCountRow').style.opacity = v ? '1' : '0.4';
renderBoards();
},
settingShowSearch: v => {
settings.showSearch = v;
document.getElementById('searchBarWrapper').classList.toggle('hidden', !v);
}
};
Object.entries(toggleMap).forEach(([id, fn]) => {
const el = document.getElementById(id);
if (el) {
el.addEventListener('change', async e => {
fn(e.target.checked);
await saveSettings();
});
}
});
document.getElementById('settingVisibleCount').addEventListener('change', async e => {
settings.visibleCount = parseInt(e.target.value, 10);
await saveSettings();
renderBoards();
});
// Background URL
document.getElementById('btnChangeBg').addEventListener('click', () => {
const row = document.getElementById('bgInputRow');
row.style.display = row.style.display === 'none' ? 'flex' : 'none';
});
document.getElementById('btnApplyBg').addEventListener('click', async () => {
const url = document.getElementById('bgUrlInput').value.trim();
settings.bgUrl = url;
document.getElementById('bgLayer').style.backgroundImage = url ? `url('${url}')` : '';
await saveSettings();
document.getElementById('bgInputRow').style.display = 'none';
});
// Background File Upload
document.getElementById('btnBgFile').addEventListener('click', () => {
document.getElementById('bgFileInput').click();
});
document.getElementById('bgFileInput').addEventListener('change', e => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async ev => {
settings.bgUrl = ev.target.result;
document.getElementById('bgLayer').style.backgroundImage = `url('${ev.target.result}')`;
await saveSettings();
};
reader.onerror = () => {
alert('Fehler beim Lesen der Datei. Bitte eine andere Datei wählen.');
};
reader.readAsDataURL(file);
});
// Reset All
document.getElementById('btnResetAll').addEventListener('click', async () => {
if (!confirm('Wirklich alle Boards und Einstellungen löschen? Nicht rückgängig machbar.')) return;
boards = [];
settings = { compact: false, shortenTitles: false, newTab: true, showDesc: false,
hideExtra: false, visibleCount: 10, bgUrl: '', theme: 'astronaut',
showSearch: true, searchEngine: 'google' };
await saveBoards();
await saveSettings();
applySettings();
renderBoards();
closeSettings();
});
}
+63
View File
@@ -0,0 +1,63 @@
/* =============================================
HELLION NEWTAB — state.js
Globaler State, Default-Werte, Hilfsfunktionen
============================================= */
let boards = [];
let settings = {
compact: false,
shortenTitles: false,
newTab: true,
showDesc: false,
hideExtra: false,
visibleCount: 10,
bgUrl: '',
theme: 'astronaut',
showSearch: true,
searchEngine: 'google'
};
function uid() {
return Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
}
function escHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function getFaviconUrl(url) {
try {
const u = new URL(url);
return `https://www.google.com/s2/favicons?domain=${u.hostname}&sz=16`;
} catch {
return '';
}
}
function getDefaultBoards() {
return [
{
id: uid(),
title: 'Getting Started',
bookmarks: [
{ id: uid(), title: 'GitHub', url: 'https://github.com', desc: '' },
{ id: uid(), title: 'MDN Web Docs', url: 'https://developer.mozilla.org', desc: '' },
{ id: uid(), title: 'Next.js Docs', url: 'https://nextjs.org/docs', desc: '' },
],
blurred: false
}
];
}
async function saveBoards() {
await Store.set('boards', boards);
}
async function saveSettings() {
await Store.set('settings', settings);
}
+78
View File
@@ -0,0 +1,78 @@
/* =============================================
HELLION NEWTAB — sticky.js
Sticky Note: draggable, persistent
============================================= */
function initStickyNote() {
const note = document.getElementById('stickyNote');
const body = document.getElementById('stickyNoteBody');
const header = document.getElementById('stickyNoteHeader');
const btnClose = document.getElementById('stickyNoteClose');
const btnNote = document.getElementById('btnNote');
if (!note || !body) return;
// Gespeicherten Text & Position laden
Store.get('stickyNote').then(val => { if (val) body.value = val; });
Store.get('stickyPos').then(pos => {
if (pos) {
note.style.right = 'auto'; note.style.bottom = 'auto';
note.style.left = pos.x + 'px'; note.style.top = pos.y + 'px';
}
});
Store.get('stickyVisible').then(vis => { if (vis) note.classList.add('visible'); });
// Text speichern (debounced)
let saveTimer;
body.addEventListener('input', () => {
clearTimeout(saveTimer);
saveTimer = setTimeout(() => Store.set('stickyNote', body.value), 600);
});
// Toggle
btnNote.addEventListener('click', async () => {
const visible = note.classList.toggle('visible');
await Store.set('stickyVisible', visible);
if (visible) body.focus();
});
btnClose.addEventListener('click', async () => {
note.classList.remove('visible');
await Store.set('stickyVisible', false);
});
// Drag via Pointer Events
header.style.cursor = 'grab';
header.addEventListener('pointerdown', e => {
if (e.target === btnClose || e.target.closest('.sticky-note-close')) return;
e.preventDefault();
header.setPointerCapture(e.pointerId);
header.style.cursor = 'grabbing';
const rect = note.getBoundingClientRect();
note.style.right = 'auto'; note.style.bottom = 'auto';
note.style.left = rect.left + 'px'; note.style.top = rect.top + 'px';
const offX = e.clientX - rect.left;
const offY = e.clientY - rect.top;
function onMove(ev) {
const maxX = window.innerWidth - note.offsetWidth;
const maxY = window.innerHeight - note.offsetHeight;
note.style.left = Math.max(0, Math.min(maxX, ev.clientX - offX)) + 'px';
note.style.top = Math.max(48, Math.min(maxY, ev.clientY - offY)) + 'px';
}
async function onUp() {
header.style.cursor = 'grab';
header.releasePointerCapture(e.pointerId);
header.removeEventListener('pointermove', onMove);
header.removeEventListener('pointerup', onUp);
await Store.set('stickyPos', {
x: parseFloat(note.style.left),
y: parseFloat(note.style.top)
});
}
header.addEventListener('pointermove', onMove);
header.addEventListener('pointerup', onUp);
});
}
+59
View File
@@ -0,0 +1,59 @@
/* =============================================
HELLION NEWTAB — storage.js
Abstraction Layer: chrome.storage.local / localStorage
============================================= */
const Store = {
QUOTA_WARNING_BYTES: 8 * 1024 * 1024, // 8 MB Warnung (Limit ist 10 MB)
get(key) {
return new Promise(resolve => {
if (typeof chrome !== 'undefined' && chrome.storage) {
chrome.storage.local.get([key], r => resolve(r[key] ?? null));
} else {
try { resolve(JSON.parse(localStorage.getItem(key))); }
catch { resolve(null); }
}
});
},
set(key, value) {
return new Promise((resolve, reject) => {
if (typeof chrome !== 'undefined' && chrome.storage) {
chrome.storage.local.set({ [key]: value }, () => {
if (chrome.runtime.lastError) {
console.error('Storage-Fehler:', chrome.runtime.lastError.message);
alert('Speicher voll! Bitte lösche alte Boards oder das Hintergrundbild, um Platz zu schaffen.');
reject(new Error(chrome.runtime.lastError.message));
return;
}
resolve();
});
} else {
try {
localStorage.setItem(key, JSON.stringify(value));
resolve();
} catch (e) {
console.error('Storage-Fehler:', e.message);
alert('Speicher voll! Bitte lösche alte Boards oder das Hintergrundbild, um Platz zu schaffen.');
reject(e);
}
}
});
},
async checkQuota() {
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local.getBytesInUse) {
return new Promise(resolve => {
chrome.storage.local.getBytesInUse(null, bytes => {
if (bytes > Store.QUOTA_WARNING_BYTES) {
const usedMB = (bytes / 1024 / 1024).toFixed(1);
console.warn('Storage-Warnung: ' + usedMB + ' MB von 10 MB belegt.');
}
resolve(bytes);
});
});
}
return 0;
}
};
+30
View File
@@ -0,0 +1,30 @@
/* =============================================
HELLION NEWTAB — themes.js
Theme-Definitionen & Anwendungslogik
============================================= */
const THEMES = {
'astronaut': { bg: 'assets/themes/bg-astronaut.jpg' },
'cosmic-clock': { bg: 'assets/themes/bg-cosmic-clock.jpg' },
'void-mage': { bg: 'assets/themes/bg-void-mage.jpg' },
'merchantman': { bg: 'assets/themes/bg-merchantman.webp' },
'julia-jin': { bg: 'assets/themes/bg-julia-jin.png' },
'sc-sunset': { bg: 'assets/themes/bg-sc-sunset.jpg' },
'hellion-hud': { bg: 'assets/themes/bg-hellion-hud.png' },
'hellion-energy': { bg: 'assets/themes/bg-hellion-energy.jpg' }
};
function applyTheme(themeName, skipBgOverride) {
const theme = THEMES[themeName];
if (!theme) return;
document.documentElement.setAttribute('data-theme', themeName);
if (!skipBgOverride) {
document.getElementById('bgLayer').style.backgroundImage = `url('${theme.bg}')`;
}
document.querySelectorAll('.theme-card').forEach(card => {
card.classList.toggle('active', card.dataset.value === themeName);
});
}