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.
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
|
After Width: | Height: | Size: 672 B |
|
After Width: | Height: | Size: 109 B |
|
After Width: | Height: | Size: 227 B |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 344 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 247 KiB |
|
After Width: | Height: | Size: 211 KiB |
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 & 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>
|
||||||
@@ -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); }
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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 = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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(); });
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||