commit 87c30b24d0f85a74eb057d400421cfbad9f91308 Author: Florian Wathling Date: Fri Mar 20 22:48:21 2026 +0100 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. diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..03bb7af --- /dev/null +++ b/.github/workflows/quality.yml @@ -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" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3835843 --- /dev/null +++ b/.github/workflows/release.yml @@ -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 diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..5106a2c --- /dev/null +++ b/.github/workflows/security.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7d5abd0 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..643b7fc --- /dev/null +++ b/README.md @@ -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. diff --git a/assets/icons/icon128.png b/assets/icons/icon128.png new file mode 100644 index 0000000..0c9d76c Binary files /dev/null and b/assets/icons/icon128.png differ diff --git a/assets/icons/icon16.png b/assets/icons/icon16.png new file mode 100644 index 0000000..427f476 Binary files /dev/null and b/assets/icons/icon16.png differ diff --git a/assets/icons/icon48.png b/assets/icons/icon48.png new file mode 100644 index 0000000..2081eca Binary files /dev/null and b/assets/icons/icon48.png differ diff --git a/assets/themes/bg-astronaut.jpg b/assets/themes/bg-astronaut.jpg new file mode 100644 index 0000000..5a80247 Binary files /dev/null and b/assets/themes/bg-astronaut.jpg differ diff --git a/assets/themes/bg-cosmic-clock.jpg b/assets/themes/bg-cosmic-clock.jpg new file mode 100644 index 0000000..b8b0f30 Binary files /dev/null and b/assets/themes/bg-cosmic-clock.jpg differ diff --git a/assets/themes/bg-hellion-energy.jpg b/assets/themes/bg-hellion-energy.jpg new file mode 100644 index 0000000..6a40dcd Binary files /dev/null and b/assets/themes/bg-hellion-energy.jpg differ diff --git a/assets/themes/bg-hellion-hud.png b/assets/themes/bg-hellion-hud.png new file mode 100644 index 0000000..d7cb8d6 Binary files /dev/null and b/assets/themes/bg-hellion-hud.png differ diff --git a/assets/themes/bg-julia-jin.png b/assets/themes/bg-julia-jin.png new file mode 100644 index 0000000..e0d3886 Binary files /dev/null and b/assets/themes/bg-julia-jin.png differ diff --git a/assets/themes/bg-merchantman.webp b/assets/themes/bg-merchantman.webp new file mode 100644 index 0000000..7e838f9 Binary files /dev/null and b/assets/themes/bg-merchantman.webp differ diff --git a/assets/themes/bg-sc-sunset.jpg b/assets/themes/bg-sc-sunset.jpg new file mode 100644 index 0000000..6101fe1 Binary files /dev/null and b/assets/themes/bg-sc-sunset.jpg differ diff --git a/assets/themes/bg-void-mage.jpg b/assets/themes/bg-void-mage.jpg new file mode 100644 index 0000000..d480645 Binary files /dev/null and b/assets/themes/bg-void-mage.jpg differ diff --git a/manifest.firefox.json b/manifest.firefox.json new file mode 100644 index 0000000..15cfcd9 --- /dev/null +++ b/manifest.firefox.json @@ -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" + } +} diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..49426f5 --- /dev/null +++ b/manifest.json @@ -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" + } +} diff --git a/newtab.html b/newtab.html new file mode 100644 index 0000000..2f95089 --- /dev/null +++ b/newtab.html @@ -0,0 +1,398 @@ + + + + + + Hellion NewTab + + + + + + + + +
+
+
+ + +
+
+ +
+ 00:00 + Fr, 01. Jan +
+
+
+ + + + +
+
+ + +
+ +
+ + +
+
+ + + Note + + +
+ +
+ + +
+ +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/css/main.css b/src/css/main.css new file mode 100644 index 0000000..bdba434 --- /dev/null +++ b/src/css/main.css @@ -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); } +} diff --git a/src/js/app.js b/src/js/app.js new file mode 100644 index 0000000..2a8a059 --- /dev/null +++ b/src/js/app.js @@ -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); diff --git a/src/js/boards.js b/src/js/boards.js new file mode 100644 index 0000000..0489f62 --- /dev/null +++ b/src/js/boards.js @@ -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 = `
+ No boards yet. Click + Board to create one, + or use Import to load your browser bookmarks. +
`; + 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 = ` + + + + + + + + ${escHtml(board.title)} +
+ + + +
+ `; + + // 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 = ` 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; +} diff --git a/src/js/data.js b/src/js/data.js new file mode 100644 index 0000000..7d05eb7 --- /dev/null +++ b/src/js/data.js @@ -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 = ''; + }); +} diff --git a/src/js/drag.js b/src/js/drag.js new file mode 100644 index 0000000..fa8414c --- /dev/null +++ b/src/js/drag.js @@ -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(); + }); + }); +} diff --git a/src/js/search.js b/src/js/search.js new file mode 100644 index 0000000..a76454a --- /dev/null +++ b/src/js/search.js @@ -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(); }); +} diff --git a/src/js/settings.js b/src/js/settings.js new file mode 100644 index 0000000..11229ca --- /dev/null +++ b/src/js/settings.js @@ -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(); + }); +} diff --git a/src/js/state.js b/src/js/state.js new file mode 100644 index 0000000..6989a33 --- /dev/null +++ b/src/js/state.js @@ -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, '"'); +} + +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); +} diff --git a/src/js/sticky.js b/src/js/sticky.js new file mode 100644 index 0000000..108fc51 --- /dev/null +++ b/src/js/sticky.js @@ -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); + }); +} diff --git a/src/js/storage.js b/src/js/storage.js new file mode 100644 index 0000000..8a5ec05 --- /dev/null +++ b/src/js/storage.js @@ -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; + } +}; diff --git a/src/js/themes.js b/src/js/themes.js new file mode 100644 index 0000000..822e478 --- /dev/null +++ b/src/js/themes.js @@ -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); + }); +}