From 42e3cf0dec345c43a2d1f84a0aad8fa29ffd4b7f Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Sun, 14 Jun 2026 21:05:36 +0200 Subject: [PATCH 01/10] ci(release): Release via Gitea-API (curl) statt go-basierter release-action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die gitea.com/actions/release-action ist 'using: go' und scheitert auf dem Forge-Runner mit exit 127 — act_runner v0.6.1 bekommt die go-Action weder im Job-Image noch im Runner kompiliert ('go: executable file not found'). Der Schritt legt das Release jetzt per curl gegen die Gitea-API an und laedt die Assets hoch, idempotent (vorhandenes Release/Assets werden wiederverwendet bzw. ersetzt). Laeuft als normaler run-Step im Job-Image und ist damit unabhaengig von go-Toolchain, Action-Cache und @main-Drift. --- .gitea/workflows/release.yml | 63 +++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index dbd6576..54454f9 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -77,28 +77,47 @@ jobs: sha256sum *.zip > checksums-sha256.txt cat checksums-sha256.txt - # Gitea-native Release-Action. Legt das Release an, falls der Tag noch - # keins hat, oder aktualisiert das bestehende und haengt die Assets an. - # Der auto-injizierte GITHUB_TOKEN auf Gitea Actions hat Gitea-API-Scope - # und reicht fuer Release-Write. - - name: Attach to Gitea release - uses: https://gitea.com/actions/release-action@main - with: - files: |- - dist/hellion-newtab-${{ steps.version.outputs.tag }}-chrome.zip - dist/hellion-newtab-${{ steps.version.outputs.tag }}-firefox.zip - dist/hellion-newtab-${{ steps.version.outputs.tag }}-opera.zip - dist/checksums-sha256.txt - api_key: ${{ secrets.GITHUB_TOKEN }} - body: | - ## Hellion NewTab ${{ steps.version.outputs.tag }} + # Release per Gitea-API (curl), NICHT via gitea.com/actions/release-action: die ist `using: go` + # und stirbt auf diesem Runner mit exit 127 ("go not found"), weil act_runner v0.6.1 die go-Action + # weder im Job-Image noch im Runner kompiliert bekommt. curl + python3 sind im Job-Image vorhanden + # und laufen als normaler Step -> unabhaengig von go-Toolchain, Action-Cache und @main-Drift. + # GITHUB_API_URL/GITHUB_REPOSITORY/GITHUB_TOKEN injiziert Gitea Actions automatisch. + - name: Create release & upload assets (Gitea API) + env: + GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.version.outputs.tag }} + run: | + set -euo pipefail + API="${GITHUB_API_URL:-https://gitea.hellion-forge.cloud/api/v1}" + REPO="${GITHUB_REPOSITORY}" + AUTH="Authorization: token ${GITEA_TOKEN}" - ### Installation - - **Chrome / Edge / Brave / Vivaldi:** `hellion-newtab-${{ steps.version.outputs.tag }}-chrome.zip` - - **Firefox:** `hellion-newtab-${{ steps.version.outputs.tag }}-firefox.zip` - - **Opera / Opera GX:** `hellion-newtab-${{ steps.version.outputs.tag }}-opera.zip` + # Release-Request-JSON (Body inkl. Installationshinweise) als python-Einzeiler bauen + # (mehrzeilig wuerde den YAML-run-Block brechen: Zeilen auf Spalte 0). + REQ=$(python3 -c 'import json,os; t=os.environ["TAG"]; body="## Hellion NewTab "+t+"\n\n### Installation\n- **Chrome / Edge / Brave / Vivaldi:** `hellion-newtab-"+t+"-chrome.zip`\n- **Firefox:** `hellion-newtab-"+t+"-firefox.zip`\n- **Opera / Opera GX:** `hellion-newtab-"+t+"-opera.zip`\n\nVollstaendige Installationsanleitung siehe README.\n\n### Checksums\n`checksums-sha256.txt` zum Verifizieren der Dateiintegritaet."; print(json.dumps({"tag_name": t, "name": "Hellion NewTab "+t, "body": body}))') - Vollstaendige Installationsanleitung siehe README. + # Idempotent: existierendes Release zum Tag wiederverwenden, sonst anlegen. + REL_ID=$(curl -sf -H "$AUTH" "$API/repos/$REPO/releases/tags/$TAG" \ + | python3 -c 'import sys,json;print(json.load(sys.stdin).get("id",""))' 2>/dev/null || true) + if [ -z "$REL_ID" ]; then + REL_ID=$(printf '%s' "$REQ" \ + | curl -sf -X POST -H "$AUTH" -H "Content-Type: application/json" -d @- "$API/repos/$REPO/releases" \ + | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])') + echo "Release angelegt: $REL_ID" + else + echo "Release existiert bereits, wiederverwenden: $REL_ID" + fi - ### Checksums - `checksums-sha256.txt` zum Verifizieren der Dateiintegritaet. + # Vorhandene gleichnamige Assets entfernen (idempotent bei Re-Runs), dann hochladen. + EXIST=$(curl -sf -H "$AUTH" "$API/repos/$REPO/releases/$REL_ID/assets" 2>/dev/null || echo '[]') + for f in dist/hellion-newtab-$TAG-chrome.zip dist/hellion-newtab-$TAG-firefox.zip dist/hellion-newtab-$TAG-opera.zip dist/checksums-sha256.txt; do + name=$(basename "$f") + aid=$(printf '%s' "$EXIST" | NAME="$name" python3 -c 'import sys,json,os;n=os.environ["NAME"];print(next((a["id"] for a in json.load(sys.stdin) if a.get("name")==n), ""))' 2>/dev/null || true) + if [ -n "$aid" ]; then + echo "ersetze vorhandenes Asset $name (id $aid)" + curl -sf -X DELETE -H "$AUTH" "$API/repos/$REPO/releases/$REL_ID/assets/$aid" >/dev/null || true + fi + echo "Upload $name ..." + curl -sf -X POST -H "$AUTH" -F "attachment=@$f" "$API/repos/$REPO/releases/$REL_ID/assets?name=$name" >/dev/null + done + echo "Release $TAG fertig: alle Assets hochgeladen." From 9beeec31826e15259328624741ef33c5895d91c5 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Sun, 14 Jun 2026 21:34:34 +0200 Subject: [PATCH 02/10] feat(theme): eigenes Hintergrundbild um https-URLs und Quota-Schutz erweitern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - isValidBgUrl akzeptiert jetzt https:// zusätzlich zu data:/blob: (http bleibt ausgeschlossen wegen Mixed-Content) - CSP img-src 'self' https: data: blob: in allen 3 Manifesten, damit remote Hintergründe deterministisch laden statt still am CSP-Default zu haengen - Upload-Bilder werden vor dem Speichern per Canvas auf die Bildschirmkante (max 2560px) verkleinert und als WebP re-kodiert -> schont chrome.storage.local - URL-Feld: Platzhalter lokalisierbar (data-i18n-placeholder) + Tracking-Hinweis, dass ein per URL geladenes Bild bei jedem Oeffnen vom fremden Server kommt - i18n DE/EN: bg_url.desc + bg_invalid_url an https angepasst, 2 neue Keys --- manifest.firefox.json | 2 +- manifest.json | 2 +- manifest.opera.json | 2 +- newtab.html | 3 ++- src/css/main.css | 4 ++++ src/js/i18n.js | 12 +++++++---- src/js/settings.js | 46 +++++++++++++++++++++++++++++++++++++++---- 7 files changed, 59 insertions(+), 12 deletions(-) diff --git a/manifest.firefox.json b/manifest.firefox.json index fab6932..064a3a4 100644 --- a/manifest.firefox.json +++ b/manifest.firefox.json @@ -54,7 +54,7 @@ } ], "content_security_policy": { - "extension_pages": "script-src 'self'; object-src 'self'" + "extension_pages": "script-src 'self'; object-src 'self'; img-src 'self' https: data: blob:" }, "icons": { "16": "assets/icons/icon16.png", diff --git a/manifest.json b/manifest.json index cc5ed6f..917322f 100644 --- a/manifest.json +++ b/manifest.json @@ -36,7 +36,7 @@ } ], "content_security_policy": { - "extension_pages": "script-src 'self'; object-src 'self'" + "extension_pages": "script-src 'self'; object-src 'self'; img-src 'self' https: data: blob:" }, "icons": { "16": "assets/icons/icon16.png", diff --git a/manifest.opera.json b/manifest.opera.json index 942930d..8e2f2e8 100644 --- a/manifest.opera.json +++ b/manifest.opera.json @@ -51,7 +51,7 @@ }, "content_security_policy": { - "extension_pages": "script-src 'self'; object-src 'self'" + "extension_pages": "script-src 'self'; object-src 'self'; img-src 'self' https: data: blob:" }, "icons": { "16": "assets/icons/icon16.png", diff --git a/newtab.html b/newtab.html index 0826aab..04bea63 100644 --- a/newtab.html +++ b/newtab.html @@ -381,9 +381,10 @@ +
Datei hochladen diff --git a/src/css/main.css b/src/css/main.css index ccb0bab..72a9f65 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -2387,6 +2387,10 @@ body.show-desc .bm-desc { display: block; } .theme-modal-section .setting-row { padding: 8px 0; } +.bg-url-hint { + padding: 2px 0 6px; + line-height: 1.4; +} /* ============================================ ACCORDION SETTINGS diff --git a/src/js/i18n.js b/src/js/i18n.js index f1c3c74..8aeaa95 100644 --- a/src/js/i18n.js +++ b/src/js/i18n.js @@ -385,7 +385,9 @@ const STRINGS = { 'settings.visible_count': 'Sichtbare Bookmarks', 'settings.visible_count.desc': 'Anzahl vor dem Ausblenden', 'settings.bg_url': 'Bild-URL', - 'settings.bg_url.desc': 'Eigenes Hintergrundbild per URL', + 'settings.bg_url.desc': 'Eigenes Bild per https-URL oder lokalem Upload', + 'settings.bg_url.placeholder': 'https://… oder leer für Standard', + 'settings.bg_url.privacy_hint': 'Hinweis: Ein per URL eingebundenes Bild wird bei jedem Öffnen vom fremden Server geladen.', 'settings.bg_change': 'Ändern', 'settings.bg_apply': 'Übernehmen', 'settings.bg_upload': 'Datei hochladen', @@ -396,7 +398,7 @@ const STRINGS = { 'settings.onboarding_btn': 'Start', 'settings.reset_btn': 'Reset', 'settings.bg_upload_btn': 'Upload', - 'settings.bg_invalid_url': 'Nur lokale Bilder (Upload) sind als Hintergrund erlaubt.', + 'settings.bg_invalid_url': 'Nur https-URLs oder lokale Bilder (Upload) sind als Hintergrund erlaubt.', 'settings.bg_invalid_url.title': 'Ungültige URL', // Modals @@ -846,7 +848,9 @@ const STRINGS = { 'settings.visible_count': 'Visible bookmarks', 'settings.visible_count.desc': 'Number before hiding', 'settings.bg_url': 'Image URL', - 'settings.bg_url.desc': 'Custom background image via URL', + 'settings.bg_url.desc': 'Custom image via https URL or local upload', + 'settings.bg_url.placeholder': 'https://… or empty for default', + 'settings.bg_url.privacy_hint': 'Note: an image loaded via URL is fetched from the remote server every time you open a tab.', 'settings.bg_change': 'Change', 'settings.bg_apply': 'Apply', 'settings.bg_upload': 'Upload file', @@ -857,7 +861,7 @@ const STRINGS = { 'settings.onboarding_btn': 'Start', 'settings.reset_btn': 'Reset', 'settings.bg_upload_btn': 'Upload', - 'settings.bg_invalid_url': 'Only local images (upload) are allowed as background.', + 'settings.bg_invalid_url': 'Only https URLs or local images (upload) are allowed as background.', 'settings.bg_invalid_url.title': 'Invalid URL', // Modals diff --git a/src/js/settings.js b/src/js/settings.js index ec94088..358dbfb 100644 --- a/src/js/settings.js +++ b/src/js/settings.js @@ -113,7 +113,37 @@ function switchTheme(name) { */ function isValidBgUrl(url) { return typeof url === 'string' && url.length > 0 && - (url.startsWith('blob:') || url.startsWith('data:image/')); + (url.startsWith('blob:') || url.startsWith('data:image/') || url.startsWith('https://')); +} + +// Eigenes Upload-Bild Quota-schonend verkleinern: auf die laengste Bildschirmkante +// (× devicePixelRatio, gedeckelt) herunterrechnen und als WebP neu kodieren. Das spart +// gegenueber dem rohen Base64-Upload locker den Grossteil der chrome.storage.local-Quota. +// Greift nur beim lokalen Upload (data:-URL ist same-origin, Canvas wird nicht getainted); +// https-Hintergruende liegen remote und kosten keine Quota. +function downscaleBgImage(dataUrl) { + const MAX_DIM = Math.min(2560, Math.round(Math.max(screen.width, screen.height) * (window.devicePixelRatio || 1))); + const QUALITY = 0.82; + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => { + const scale = Math.min(1, MAX_DIM / Math.max(img.naturalWidth, img.naturalHeight)); + const w = Math.max(1, Math.round(img.naturalWidth * scale)); + const h = Math.max(1, Math.round(img.naturalHeight * scale)); + const canvas = document.createElement('canvas'); + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext('2d'); + if (!ctx) { resolve(dataUrl); return; } // kein 2D-Context -> Original behalten + ctx.drawImage(img, 0, 0, w, h); + // WebP wo verfuegbar (Chrome/Opera/FF142+); sonst faellt toDataURL auf PNG zurueck -> dann JPEG + let out = canvas.toDataURL('image/webp', QUALITY); + if (!out.startsWith('data:image/webp')) out = canvas.toDataURL('image/jpeg', QUALITY); + resolve(out); + }; + img.onerror = () => reject(new Error('image decode failed')); + img.src = dataUrl; + }); } // ---- ACCORDION ---- @@ -434,7 +464,9 @@ function bindSettingsEvents() { // Background URL (im Theme-Modal) document.getElementById('btnChangeBg').addEventListener('click', () => { - document.getElementById('bgInputRow').classList.toggle('hidden'); + // toggle() liefert true, wenn 'hidden' jetzt gesetzt ist -> Hinweis exakt parallel schalten + const isNowHidden = document.getElementById('bgInputRow').classList.toggle('hidden'); + document.getElementById('bgUrlHint').classList.toggle('hidden', isNowHidden); }); document.getElementById('btnApplyBg').addEventListener('click', async () => { const url = document.getElementById('bgUrlInput').value.trim(); @@ -458,8 +490,14 @@ function bindSettingsEvents() { const reader = new FileReader(); reader.onload = async ev => { if (!isValidBgUrl(ev.target.result)) return; - settings.bgUrl = ev.target.result; - document.getElementById('bgLayer').style.backgroundImage = `url('${ev.target.result}')`; + let bg = ev.target.result; + try { + bg = await downscaleBgImage(bg); // Quota-Schutz: verkleinern + WebP + } catch { + // Downscale fehlgeschlagen -> Original-Upload nutzen (besser als gar kein Bild) + } + settings.bgUrl = bg; + document.getElementById('bgLayer').style.backgroundImage = `url('${bg}')`; await saveSettings(); }; reader.onerror = () => { From d0feddbda0debd60a21b6b1c80fc70f4c0cfff9e Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Mon, 15 Jun 2026 01:38:55 +0200 Subject: [PATCH 03/10] feat(theme): customTheme-Default im State + Reset-Literal --- src/js/settings.js | 2 +- src/js/state.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/js/settings.js b/src/js/settings.js index 358dbfb..a15e969 100644 --- a/src/js/settings.js +++ b/src/js/settings.js @@ -550,7 +550,7 @@ function bindSettingsEvents() { settings = { compact: false, shortenTitles: false, newTab: true, showDesc: false, hideExtra: false, visibleCount: 10, bgUrl: '', theme: 'nebula', showSearch: true, searchEngine: 'google', toolbarPos: 'right', - imageRefEnabled: false, language: 'auto' }; + imageRefEnabled: false, language: 'auto', customTheme: null }; await saveBoards(); await saveTrash(); await saveSettings(); diff --git a/src/js/state.js b/src/js/state.js index 6148fd2..1839dd3 100644 --- a/src/js/state.js +++ b/src/js/state.js @@ -25,6 +25,7 @@ let settings = { visibleCount: 10, bgUrl: '', theme: 'nebula', + customTheme: null, showSearch: true, searchEngine: 'google', toolbarPos: 'right', From 22e74d41bcdf600fde51fe046d6af3a46ffca780 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Mon, 15 Jun 2026 02:06:38 +0200 Subject: [PATCH 04/10] =?UTF-8?q?feat(theme):=20i18n-Keys=20f=C3=BCr=20The?= =?UTF-8?q?me-Builder=20+=20Onboarding-Wortlaut=20entschaerft?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/js/i18n.js | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/js/i18n.js b/src/js/i18n.js index 8aeaa95..0ebc189 100644 --- a/src/js/i18n.js +++ b/src/js/i18n.js @@ -66,8 +66,8 @@ const STRINGS = { 'onboarding.s2.f2': 'Importiere Browser-Lesezeichen über den „Import" Button im Header', 'onboarding.s2.f3': 'Drag & Drop zum Umsortieren von Boards und Links', 'onboarding.s2.f4': 'Blur-Modus für private Boards (🔒 Icon)', - 'onboarding.s3.title': '11 handgefertigte Themes', - 'onboarding.s3.text': 'Klicke auf den „Theme" Button im Header um dein Theme zu wählen. Jedes hat seinen eigenen Stil und Farbpalette.', + 'onboarding.s3.title': 'Handgefertigte Themes + dein eigenes', + 'onboarding.s3.text': 'Klicke auf den „Theme" Button im Header, um ein Theme zu wählen oder dir ein eigenes zu bauen. Jedes hat seinen eigenen Stil.', 'onboarding.s4.title': 'Widget-Toolbar', 'onboarding.s4.f1': 'Die schwebenden Buttons rechts öffnen Widgets', 'onboarding.s4.f2': 'Notes und Checklisten für schnelle Notizen', @@ -425,6 +425,18 @@ const STRINGS = { 'theme.card.satisfactory': 'Theme Satisfactory wählen', 'theme.card.avorion': 'Theme Avorion wählen', 'theme.card.hellion_stealth': 'Theme Hellion Stealth wählen', + 'theme.card.custom': 'Eigenes Theme wählen', + 'theme.builder.title': 'Eigenes', + 'theme.builder.accent': 'Akzent', + 'theme.builder.bg': 'Hintergrund', + 'theme.builder.board': 'Board-Fläche', + 'theme.builder.text': 'Text primär', + 'theme.builder.text_secondary': 'Text sekundär', + 'theme.builder.text_muted': 'Text gedämpft', + 'theme.builder.reset': 'Zurücksetzen', + 'theme.builder.contrast_good': 'Gut lesbar', + 'theme.builder.contrast_ok': 'Grenzwertig', + 'theme.builder.contrast_bad': 'Schwer lesbar', 'toolbar.label': 'Widget-Werkzeugleiste', // About @@ -529,8 +541,8 @@ const STRINGS = { 'onboarding.s2.f2': 'Import browser bookmarks via the "Import" button in the header', 'onboarding.s2.f3': 'Drag & drop to reorder boards and links', 'onboarding.s2.f4': 'Blur mode for private boards (🔒 icon)', - 'onboarding.s3.title': '11 handcrafted themes', - 'onboarding.s3.text': 'Click the "Theme" button in the header to choose your theme. Each has its own style and color palette.', + 'onboarding.s3.title': 'Hand-crafted themes + your own', + 'onboarding.s3.text': 'Click the "Theme" button in the header to choose a theme or build your own. Each has its own style.', 'onboarding.s4.title': 'Widget Toolbar', 'onboarding.s4.f1': 'The floating buttons on the right open widgets', 'onboarding.s4.f2': 'Notes and checklists for quick notes', @@ -888,6 +900,18 @@ const STRINGS = { 'theme.card.satisfactory': 'Select Satisfactory theme', 'theme.card.avorion': 'Select Avorion theme', 'theme.card.hellion_stealth': 'Select Hellion Stealth theme', + 'theme.card.custom': 'Select custom theme', + 'theme.builder.title': 'Custom', + 'theme.builder.accent': 'Accent', + 'theme.builder.bg': 'Background', + 'theme.builder.board': 'Board surface', + 'theme.builder.text': 'Text primary', + 'theme.builder.text_secondary': 'Text secondary', + 'theme.builder.text_muted': 'Text muted', + 'theme.builder.reset': 'Reset', + 'theme.builder.contrast_good': 'Good contrast', + 'theme.builder.contrast_ok': 'Borderline', + 'theme.builder.contrast_bad': 'Hard to read', 'toolbar.label': 'Widget toolbar', // About From 96d4eaa8a1b34fafb853363041267d9be9ab46ed Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Mon, 15 Jun 2026 02:34:42 +0200 Subject: [PATCH 05/10] feat(theme): [data-theme=custom]-Block + Theme-Builder-Panel-Styling --- src/css/main.css | 51 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/css/main.css b/src/css/main.css index 72a9f65..6d2c9b6 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -349,6 +349,30 @@ --toggle-on-bg: color-mix(in srgb, var(--accent) var(--toggle-on-bg-pct), transparent); --bg-solid-fallback: #0d0f12; } + +/* ============================================ + THEME: CUSTOM (User-Theme-Builder, neutrale Defaults) + Inline-Vars aus applyCustomTheme() ueberschreiben die 6 Picker-Werte. + ============================================ */ +[data-theme="custom"] { + --accent: #6c8cff; + --accent-glow-pct: 5%; + --board-hover-border-pct: 20%; + --logo-shadow-pct: 38%; + --toggle-on-bg-pct: 20%; + --bg-primary: #0b0d12; + --bg-board: rgba(20, 24, 33, 0.55); + --border: rgba(255, 255, 255, 0.06); + --text-primary: #e6e8ef; + --text-secondary: #9aa3b8; + --text-muted: #5b6478; + --font-display: 'Rajdhani', sans-serif; + --font-body: 'Inter', sans-serif; + --overlay-bg: radial-gradient(circle at center, color-mix(in srgb, var(--bg-primary) 35%, transparent) 0%, color-mix(in srgb, var(--bg-primary) 88%, transparent) 100%); + --header-bg: color-mix(in srgb, var(--bg-primary) 92%, transparent); + --toggle-on-bg: color-mix(in srgb, var(--accent) var(--toggle-on-bg-pct), transparent); + --bg-solid-fallback: var(--bg-primary); +} } /* ============================================ @@ -413,6 +437,8 @@ [data-theme="hellion-stealth"] .board-title { text-transform: uppercase; font-size: 0.85rem; letter-spacing: 2px; } [data-theme="hellion-stealth"] .board { border-color: rgba(94, 194, 255, 0.15); backdrop-filter: blur(10px); } [data-theme="hellion-stealth"] .bm-item:hover { background: rgba(94, 194, 255, 0.10); border-left: 2px solid var(--accent); } +[data-theme="custom"] .board { border-color: color-mix(in srgb, var(--accent) 15%, transparent); } +[data-theme="custom"] .bm-item:hover { background: color-mix(in srgb, var(--accent) 7%, transparent); } } /* ============================================ @@ -2391,6 +2417,31 @@ body.show-desc .bm-desc { display: block; } padding: 2px 0 6px; line-height: 1.4; } +.theme-card-custom-swatch { + width: 100%; height: 56px; border-radius: 8px; + border: 1.5px dashed var(--accent); + background: repeating-linear-gradient(45deg, rgba(255,255,255,0.03) 0 6px, rgba(255,255,255,0.06) 6px 12px); +} +.theme-builder-panel { padding: 10px 0 4px; } +.theme-builder-panel.hidden { display: none; } +.tb-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 18px; } +.tb-picker { display: flex; align-items: center; gap: 9px; } +.tb-picker input[type="color"] { + width: 30px; height: 30px; padding: 0; border: 1px solid var(--border); + border-radius: 6px; background: none; cursor: pointer; +} +.tb-picker label { font-size: 12px; color: var(--text-secondary); } +.tb-contrast { margin-top: 12px; font-size: 12px; display: flex; align-items: center; gap: 8px; } +.tb-contrast .tb-dot { width: 10px; height: 10px; border-radius: 50%; flex: 0 0 auto; } +.tb-contrast.good { color: #7bd88f; } .tb-contrast.good .tb-dot { background: #3fbf6f; } +.tb-contrast.ok { color: #e3c97a; } .tb-contrast.ok .tb-dot { background: #d8b24a; } +.tb-contrast.bad { color: #e58f8f; } .tb-contrast.bad .tb-dot { background: #d65c5c; } +.tb-foot { display: flex; justify-content: flex-end; margin-top: 10px; } +.tb-reset { + font-size: 11px; color: var(--text-secondary); background: none; + border: 1px solid var(--border); border-radius: 6px; padding: 5px 12px; cursor: pointer; +} +@media (max-width: 480px) { .tb-grid { grid-template-columns: 1fr; } } /* ============================================ ACCORDION SETTINGS From d305d37da5804014dfe0c95b74226296553754fa Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Mon, 15 Jun 2026 03:03:29 +0200 Subject: [PATCH 06/10] feat(theme): Eigenes-Kachel + 6-Picker-Panel im Theme-Modal --- newtab.html | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/newtab.html b/newtab.html index 04bea63..6018c1d 100644 --- a/newtab.html +++ b/newtab.html @@ -370,6 +370,23 @@ Stealth
+
+ + Eigenes + +
+
+

HINTERGRUND

From 1bd2cbb9ad6e9050bbbd7dc9e65330c7e3bae436 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Mon, 15 Jun 2026 03:31:25 +0200 Subject: [PATCH 07/10] feat(theme): applyCustomTheme/clearCustomTheme/syncCustomPickers + Hex-Validierung + WCAG-Kontrast --- src/js/settings.js | 88 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/src/js/settings.js b/src/js/settings.js index a15e969..5bfdefc 100644 --- a/src/js/settings.js +++ b/src/js/settings.js @@ -116,6 +116,94 @@ function isValidBgUrl(url) { (url.startsWith('blob:') || url.startsWith('data:image/') || url.startsWith('https://')); } +// ---- THEME-BUILDER: Konstanten + reine Helfer ---- +const CUSTOM_DEFAULTS = { + accent: '#6c8cff', bgPrimary: '#0b0d12', bgBoard: '#141821', + textPrimary: '#e6e8ef', textSecondary: '#9aa3b8', textMuted: '#5b6478', +}; +const HEX_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/; + +function isValidHexColor(v) { return typeof v === 'string' && HEX_RE.test(v); } +function safeHex(v, fallback) { return isValidHexColor(v) ? v : fallback; } + +function hexToRgba(hex, alpha) { + let h = hex.replace('#', ''); + if (h.length === 3) h = h.split('').map(c => c + c).join(''); + const r = parseInt(h.slice(0, 2), 16), g = parseInt(h.slice(2, 4), 16), b = parseInt(h.slice(4, 6), 16); + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +} + +// WCAG 2.x Relativluminanz + Kontrastverhaeltnis +function relLuminance(hex) { + let h = hex.replace('#', ''); + if (h.length === 3) h = h.split('').map(c => c + c).join(''); + const lin = [0, 2, 4].map(i => { + const c = parseInt(h.slice(i, i + 2), 16) / 255; + return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); + }); + return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2]; +} +function contrastRatio(hexA, hexB) { + const a = relLuminance(hexA), b = relLuminance(hexB); + return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05); +} + +function updateContrastIndicator(textHex, bgHex) { + const el = document.getElementById('tbContrast'); + if (!el) return; + const ratio = contrastRatio(textHex, bgHex); + let cls, key; + if (ratio >= 4.5) { cls = 'good'; key = 'theme.builder.contrast_good'; } + else if (ratio >= 3) { cls = 'ok'; key = 'theme.builder.contrast_ok'; } + else { cls = 'bad'; key = 'theme.builder.contrast_bad'; } + el.classList.remove('good', 'ok', 'bad'); + el.classList.add(cls); + const txt = document.getElementById('tbContrastText'); + if (txt) txt.textContent = `${t(key)} (${ratio.toFixed(1)}:1)`; +} + +// Setzt data-theme='custom' + 6 validierte Inline-Vars (Gate vor jedem setProperty). +function applyCustomTheme(ct) { + const root = document.documentElement; + const c = ct || {}; + const accent = safeHex(c.accent, CUSTOM_DEFAULTS.accent); + const bgPrimary = safeHex(c.bgPrimary, CUSTOM_DEFAULTS.bgPrimary); + const bgBoard = safeHex(c.bgBoard, CUSTOM_DEFAULTS.bgBoard); + const textPrimary = safeHex(c.textPrimary, CUSTOM_DEFAULTS.textPrimary); + const textSecondary = safeHex(c.textSecondary, CUSTOM_DEFAULTS.textSecondary); + const textMuted = safeHex(c.textMuted, CUSTOM_DEFAULTS.textMuted); + + root.setAttribute('data-theme', 'custom'); + root.style.setProperty('--accent', accent); + root.style.setProperty('--bg-primary', bgPrimary); + root.style.setProperty('--bg-board', hexToRgba(bgBoard, 0.55)); + root.style.setProperty('--text-primary', textPrimary); + root.style.setProperty('--text-secondary', textSecondary); + root.style.setProperty('--text-muted', textMuted); + + document.querySelectorAll('.theme-card').forEach(card => { + const on = card.dataset.value === 'custom'; + card.classList.toggle('active', on); + card.setAttribute('aria-pressed', on ? 'true' : 'false'); + }); + updateContrastIndicator(textPrimary, bgPrimary); +} + +// Entfernt die 6 Inline-Vars (Rueckwechsel auf Preset / Reset). +function clearCustomTheme() { + const root = document.documentElement; + ['--accent', '--bg-primary', '--bg-board', '--text-primary', '--text-secondary', '--text-muted'] + .forEach(v => root.style.removeProperty(v)); +} + +// Schreibt die gespeicherten (oder Default-) Farben in die 6 Picker-Inputs. +function syncCustomPickers() { + const ct = settings.customTheme || {}; + const set = (id, key) => { const el = document.getElementById(id); if (el) el.value = safeHex(ct[key], CUSTOM_DEFAULTS[key]); }; + set('tbAccent', 'accent'); set('tbBg', 'bgPrimary'); set('tbBoard', 'bgBoard'); + set('tbText', 'textPrimary'); set('tbTextSec', 'textSecondary'); set('tbTextMuted', 'textMuted'); +} + // Eigenes Upload-Bild Quota-schonend verkleinern: auf die laengste Bildschirmkante // (× devicePixelRatio, gedeckelt) herunterrechnen und als WebP neu kodieren. Das spart // gegenueber dem rohen Base64-Upload locker den Grossteil der chrome.storage.local-Quota. From 2af52fc46d02f6ebdeea6334a3f4d9692e7aa779 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Mon, 15 Jun 2026 04:00:59 +0200 Subject: [PATCH 08/10] feat(theme): Verdrahtung Custom-Theme (applySettings, selectThemeCard, Picker, Reset, Modal-Sync) --- src/js/settings.js | 51 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/src/js/settings.js b/src/js/settings.js index 5bfdefc..8827ec8 100644 --- a/src/js/settings.js +++ b/src/js/settings.js @@ -80,6 +80,8 @@ function openThemeModal() { document.addEventListener('keydown', _themeTrap); const first = _focusable(modal)[0]; if (first) first.focus(); + syncCustomPickers(); + document.getElementById('themeBuilderPanel').classList.toggle('hidden', settings.theme !== 'custom'); } function closeThemeModal() { const overlay = document.getElementById('themeOverlay'); @@ -462,7 +464,11 @@ function applySettings() { const langEl = document.getElementById('settingLanguage'); if (langEl) langEl.value = settings.language || 'auto'; - applyTheme(settings.theme || 'nebula', !!settings.bgUrl); + if (settings.theme === 'custom') { + applyCustomTheme(settings.customTheme); + } else { + applyTheme(settings.theme || 'nebula', !!settings.bgUrl); + } if (settings.bgUrl && isValidBgUrl(settings.bgUrl)) { document.getElementById('bgLayer').style.backgroundImage = `url('${settings.bgUrl}')`; @@ -489,10 +495,25 @@ function bindSettingsEvents() { const themeCards = document.querySelectorAll('.theme-card'); function selectThemeCard(card) { const name = card.dataset.value; - if (!name || name === settings.theme) return Promise.resolve(); + if (!name) return Promise.resolve(); + + // Custom: VOR dem name===settings.theme-Guard, damit ein Re-Klick das Panel wieder oeffnet. + if (name === 'custom') { + settings.theme = 'custom'; + if (!settings.customTheme) settings.customTheme = { ...CUSTOM_DEFAULTS }; + themeCards.forEach(c => c.setAttribute('aria-pressed', c === card ? 'true' : 'false')); + applyCustomTheme(settings.customTheme); // setzt data-theme + Inline-Vars; bgUrl UNANGETASTET (Koexistenz) + syncCustomPickers(); + document.getElementById('themeBuilderPanel').classList.remove('hidden'); + return saveSettings(); + } + + if (name === settings.theme) return Promise.resolve(); settings.theme = name; settings.bgUrl = ''; document.getElementById('bgUrlInput').value = ''; + clearCustomTheme(); // Inline-Vars weg beim Rueckwechsel auf ein Preset + document.getElementById('themeBuilderPanel').classList.add('hidden'); // aria-pressed synchron halten — applyTheme/switchTheme pflegt nur die .active-Klasse, nicht ARIA themeCards.forEach(c => c.setAttribute('aria-pressed', c === card ? 'true' : 'false')); switchTheme(name); // WICHTIG: switchTheme aus Phase 4 (View-Transition-Wrapper), NICHT applyTheme direkt — sonst geht der Theme-Fade verloren @@ -508,6 +529,31 @@ function bindSettingsEvents() { }); }); + // Theme-Builder Picker + const TB_PICKERS = [['tbAccent', 'accent'], ['tbBg', 'bgPrimary'], ['tbBoard', 'bgBoard'], + ['tbText', 'textPrimary'], ['tbTextSec', 'textSecondary'], ['tbTextMuted', 'textMuted']]; + TB_PICKERS.forEach(([id, key]) => { + const el = document.getElementById(id); + if (!el) return; + el.addEventListener('input', () => { // live waehrend des Ziehens + if (!settings.customTheme) settings.customTheme = { ...CUSTOM_DEFAULTS }; + settings.customTheme[key] = el.value; + settings.theme = 'custom'; + applyCustomTheme(settings.customTheme); + }); + el.addEventListener('change', () => saveSettings()); // persistiert beim Loslassen/Schliessen + }); + + const tbReset = document.getElementById('tbReset'); + if (tbReset) { + tbReset.addEventListener('click', async () => { + settings.customTheme = { ...CUSTOM_DEFAULTS }; + applyCustomTheme(settings.customTheme); + syncCustomPickers(); + await saveSettings(); + }); + } + // Accordion initialisieren initAccordion(); @@ -639,6 +685,7 @@ function bindSettingsEvents() { hideExtra: false, visibleCount: 10, bgUrl: '', theme: 'nebula', showSearch: true, searchEngine: 'google', toolbarPos: 'right', imageRefEnabled: false, language: 'auto', customTheme: null }; + clearCustomTheme(); await saveBoards(); await saveTrash(); await saveSettings(); From c985a531efc538ec09cd25ee62bb7c60fa26dcc8 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Mon, 15 Jun 2026 04:30:51 +0200 Subject: [PATCH 09/10] fix(theme): bgLayer beim Custom-Wechsel ohne eigenes Bild leeren Preset->Custom liess das alte Preset-Hintergrundbild im bgLayer haengen, weil applyCustomTheme den bgLayer nie anfasste. Jetzt wird er geleert, wenn keine gueltige bgUrl gesetzt ist, sodass --bg-primary (Solid) durchscheint. --- src/js/settings.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/js/settings.js b/src/js/settings.js index 8827ec8..87a2400 100644 --- a/src/js/settings.js +++ b/src/js/settings.js @@ -189,6 +189,12 @@ function applyCustomTheme(ct) { card.setAttribute('aria-pressed', on ? 'true' : 'false'); }); updateContrastIndicator(textPrimary, bgPrimary); + + // Kein eigenes Bild gesetzt -> bgLayer leeren, damit --bg-primary (Solid) durchscheint + // statt des Hintergrundbilds eines zuvor gewaehlten Presets (das sonst haengen bliebe). + if (!(settings.bgUrl && isValidBgUrl(settings.bgUrl))) { + document.getElementById('bgLayer').style.backgroundImage = ''; + } } // Entfernt die 6 Inline-Vars (Rueckwechsel auf Preset / Reset). From 0001de7dd765c08f4dba38b72d781d978ddfe470 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Mon, 15 Jun 2026 08:10:43 +0200 Subject: [PATCH 10/10] chore(release): Version-Bump 2.4.0 (6 Stellen) + CHANGELOG --- CHANGELOG.md | 13 +++++++++++++ manifest.firefox.json | 2 +- manifest.json | 2 +- manifest.opera.json | 2 +- newtab.html | 2 +- src/js/app.js | 2 +- src/js/data.js | 2 +- 7 files changed, 19 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c21355d..31214b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ All notable changes per version. Format based on [Keep a Changelog](https://keep --- +## [2.4.0] — 2026-06-15 + +### Added +- **Custom theme builder** — A new "Custom" tile in the theme picker opens an inline panel with six colour pickers (accent, background, board surface, and three text levels). Colours apply live to the dashboard; the accent drives the derived glow, border and toggle tints via `color-mix`. A non-blocking WCAG contrast indicator flags hard-to-read text/background combinations without preventing the choice. The custom theme persists across reloads and can be combined with a custom background image. A reset button returns the pickers to neutral defaults. New DE/EN i18n strings; the `` pickers are labelled for accessibility. +- **Custom background via https URL** — The background URL field now accepts `https://` images in addition to local uploads (http stays out to avoid mixed content). A privacy note explains that a URL-loaded image is fetched from the remote server on every new tab. + +### Changed +- Uploaded background images are downscaled (to the longest screen edge, capped at 2560px) and re-encoded as WebP before storage, to protect the `chrome.storage.local` quota. +- The extension-page CSP gains `img-src 'self' https: data: blob:` so https and data-URL backgrounds load deterministically instead of relying on the browser default. +- Onboarding slide 3 wording no longer hard-codes a fixed theme count. + +--- + ## [2.3.0] — 2026-06-14 ### Added diff --git a/manifest.firefox.json b/manifest.firefox.json index 064a3a4..c3665c7 100644 --- a/manifest.firefox.json +++ b/manifest.firefox.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "__MSG_extName__", "default_locale": "en", - "version": "2.3.0", + "version": "2.4.0", "description": "__MSG_extDesc__", "author": "Hellion Online Media - Florian Wathling", "homepage_url": "https://hellion-media.de", diff --git a/manifest.json b/manifest.json index 917322f..a24275d 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "__MSG_extName__", "default_locale": "en", - "version": "2.3.0", + "version": "2.4.0", "description": "__MSG_extDesc__", "author": "Hellion Online Media - Florian Wathling", "homepage_url": "https://hellion-media.de", diff --git a/manifest.opera.json b/manifest.opera.json index 8e2f2e8..aea38e1 100644 --- a/manifest.opera.json +++ b/manifest.opera.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "__MSG_extName__", "default_locale": "en", - "version": "2.3.0", + "version": "2.4.0", "description": "__MSG_extDesc__", "author": "Hellion Online Media - Florian Wathling", "homepage_url": "https://hellion-media.de", diff --git a/newtab.html b/newtab.html index 6018c1d..f471ce0 100644 --- a/newtab.html +++ b/newtab.html @@ -241,7 +241,7 @@