Compare commits
12 Commits
v2.3.0
..
86f5644cd5
| Author | SHA1 | Date | |
|---|---|---|---|
| 86f5644cd5 | |||
| 4d1ca1bc7e | |||
| 083e78e693 | |||
| 0001de7dd7 | |||
| c985a531ef | |||
| 2af52fc46d | |||
| 1bd2cbb9ad | |||
| d305d37da5 | |||
| 96d4eaa8a1 | |||
| 22e74d41bc | |||
| d0feddbda0 | |||
| 9beeec3182 |
@@ -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 `<input type="color">` 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# ⬡ Hellion Dashboard v2.0.0
|
||||
# ⬡ Hellion Dashboard v2.4.0
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
@@ -10,7 +10,7 @@
|
||||
**No account. No subscription. No cloud. All data stays 100% local.**
|
||||
|
||||
A personal bookmark dashboard as a browser extension.
|
||||
Boards, drag & drop, 11 themes, search bar, widget system with notes, calculator, timer and more.
|
||||
Boards, drag & drop, free layout, command palette, trash, quick save, 11 themes plus a custom theme builder, search bar, widget system with notes, calculator, timer and more.
|
||||
Full DE/EN language support with runtime switching. All in the browser, all offline.
|
||||
No external data transmission, no trackers, no analytics, no ads.
|
||||
|
||||
@@ -38,8 +38,10 @@ What you see is what's saved. No magic.
|
||||
### Boards & Bookmarks
|
||||
|
||||
- Boards as groups for links, sortable via drag & drop
|
||||
- Free layout: drag boards to any position via a handle, each position is saved; a lock button pins a board in place
|
||||
- Bookmarks with favicon, title and optional description
|
||||
- Hide boards with the blur button (privacy mode)
|
||||
- Trash: deleted bookmarks and boards are kept for 30 days before removal, with restore from Settings
|
||||
- HTML import from browser bookmarks (Chrome, Edge, Firefox)
|
||||
- JSON export & import (backup & restore)
|
||||
|
||||
@@ -48,6 +50,16 @@ What you see is what's saved. No magic.
|
||||
- Google, DuckDuckGo or Bing, switchable with a click
|
||||
- Toggleable via Settings
|
||||
|
||||
### Command Palette
|
||||
|
||||
- Open with **Ctrl+K**, live-filters all bookmarks (title and URL) and board names from the keyboard
|
||||
- Arrow keys to navigate, Enter opens the match, Escape closes (read-only, separate from the web search bar)
|
||||
|
||||
### Quick Save
|
||||
|
||||
- Global shortcut (default **Alt+Shift+S**) saves the current tab into a fixed Inbox board from any page, without opening the dashboard
|
||||
- A badge confirms the save; an open dashboard tab shows the new bookmark live
|
||||
|
||||
### Widget System
|
||||
|
||||
- **Notes & Checklists** — Floating note widgets with text or checklist template (max. 5)
|
||||
@@ -58,7 +70,7 @@ What you see is what's saved. No magic.
|
||||
- **Widget Toolbar** — Floating buttons for quick access, position (left/right) configurable in Settings
|
||||
- All widgets: draggable, resizable, z-index stacking on click
|
||||
|
||||
### 11 Themes
|
||||
### Themes
|
||||
|
||||
| Theme | Accent | Style |
|
||||
|---|---|---|
|
||||
@@ -74,6 +86,8 @@ What you see is what's saved. No magic.
|
||||
| Avorion | `#2ec4a0` Turquoise | Deep Void |
|
||||
| Hellion Stealth | `#5ec2ff` Tech Blue | Tactical Recon |
|
||||
|
||||
Plus a **custom theme**: build your own via the theme picker with six colour pickers (accent, background, board surface and three text levels). Colours apply live, the accent drives the derived glow, border and toggle tints, and a non-blocking WCAG contrast hint flags hard-to-read combinations without blocking the choice. Combinable with your own background image (local upload or https URL).
|
||||
|
||||
### Image Credits
|
||||
|
||||
| Theme | Source | License |
|
||||
@@ -104,7 +118,7 @@ What you see is what's saved. No magic.
|
||||
|
||||
### Appearance & Settings
|
||||
|
||||
- **Appearance modal** (header button), theme picker, background image and all display options in one modal
|
||||
- **Appearance modal** (header button), theme picker with custom theme builder, background image (local upload or https URL) and all display options in one modal
|
||||
- **Settings panel** (header button), widgets, data & help, danger zone
|
||||
- **About footer**, developer info, license and support links permanently visible
|
||||
- Compact mode, shorten titles, search bar toggle, open links in new tab, descriptions, hide extra bookmarks
|
||||
@@ -288,7 +302,7 @@ hellion-newtab/
|
||||
- **Responsive** — Tablet (768px) and smartphone (480px) breakpoints
|
||||
- **Secure** — `createElement` instead of `innerHTML`, URL validation, storage error handling
|
||||
- **Event Delegation** — One listener per board list instead of per bookmark (performance)
|
||||
- **Theme System** — CSS Custom Properties, 11 themes, custom background support
|
||||
- **Theme System** — CSS Custom Properties, 11 themes plus a custom theme builder, custom background support (local upload or https URL)
|
||||
|
||||
---
|
||||
|
||||
@@ -317,8 +331,8 @@ hellion-newtab/
|
||||
|
||||
```bash
|
||||
# Create a release:
|
||||
git tag v2.0.0
|
||||
git push origin v2.0.0
|
||||
git tag v2.4.0
|
||||
git push origin v2.4.0
|
||||
# → GitHub Action automatically creates release with ZIP files
|
||||
```
|
||||
|
||||
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
|
||||
+2
-2
@@ -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",
|
||||
@@ -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",
|
||||
|
||||
+2
-2
@@ -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",
|
||||
@@ -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",
|
||||
|
||||
+20
-2
@@ -241,7 +241,7 @@
|
||||
<div class="panel-footer">
|
||||
<div class="about-block">
|
||||
<div class="about-logo" data-i18n="about.title">⬡ HELLION NEWTAB</div>
|
||||
<div class="about-version">Version 2.3.0 · by Hellion Online Media</div>
|
||||
<div class="about-version">Version 2.4.0 · by Hellion Online Media</div>
|
||||
|
||||
<div class="about-links">
|
||||
<a href="https://hellion-media.de/impressum" target="_blank" class="about-link">
|
||||
@@ -370,6 +370,23 @@
|
||||
<span class="theme-card-label">Stealth</span>
|
||||
<span class="theme-card-check">✓</span>
|
||||
</div>
|
||||
<div class="theme-card theme-card-custom" data-value="custom" role="button" tabindex="0" aria-pressed="false" data-i18n-aria-label="theme.card.custom">
|
||||
<span class="theme-card-custom-swatch"></span>
|
||||
<span class="theme-card-label" data-i18n="theme.builder.title">Eigenes</span>
|
||||
<span class="theme-card-check">✓</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="theme-builder-panel hidden" id="themeBuilderPanel">
|
||||
<div class="tb-grid">
|
||||
<div class="tb-picker"><input type="color" id="tbAccent" value="#6c8cff"><label for="tbAccent" data-i18n="theme.builder.accent">Akzent</label></div>
|
||||
<div class="tb-picker"><input type="color" id="tbBg" value="#0b0d12"><label for="tbBg" data-i18n="theme.builder.bg">Hintergrund</label></div>
|
||||
<div class="tb-picker"><input type="color" id="tbBoard" value="#141821"><label for="tbBoard" data-i18n="theme.builder.board">Board-Fläche</label></div>
|
||||
<div class="tb-picker"><input type="color" id="tbText" value="#e6e8ef"><label for="tbText" data-i18n="theme.builder.text">Text primär</label></div>
|
||||
<div class="tb-picker"><input type="color" id="tbTextSec" value="#9aa3b8"><label for="tbTextSec" data-i18n="theme.builder.text_secondary">Text sekundär</label></div>
|
||||
<div class="tb-picker"><input type="color" id="tbTextMuted" value="#5b6478"><label for="tbTextMuted" data-i18n="theme.builder.text_muted">Text gedämpft</label></div>
|
||||
</div>
|
||||
<div class="tb-contrast good" id="tbContrast"><span class="tb-dot"></span><span id="tbContrastText" data-i18n="theme.builder.contrast_good">Gut lesbar</span></div>
|
||||
<div class="tb-foot"><button class="tb-reset" id="tbReset" data-i18n="theme.builder.reset">Zurücksetzen</button></div>
|
||||
</div>
|
||||
<div class="theme-modal-section">
|
||||
<h3 class="settings-section-title" data-i18n="settings.section.bg">HINTERGRUND</h3>
|
||||
@@ -381,9 +398,10 @@
|
||||
<button class="btn-small" id="btnChangeBg" data-i18n="settings.bg_change">Ändern</button>
|
||||
</div>
|
||||
<div class="setting-row hidden" id="bgInputRow">
|
||||
<input type="text" class="text-input full-width" id="bgUrlInput" placeholder="https://... oder leer für Standard" />
|
||||
<input type="text" class="text-input full-width" id="bgUrlInput" data-i18n-placeholder="settings.bg_url.placeholder" placeholder="https://… oder leer für Standard" />
|
||||
<button class="btn-small" id="btnApplyBg" data-i18n="settings.bg_apply">Übernehmen</button>
|
||||
</div>
|
||||
<p class="setting-desc bg-url-hint hidden" id="bgUrlHint" data-i18n="settings.bg_url.privacy_hint">Hinweis: Ein per URL eingebundenes Bild wird bei jedem Öffnen vom fremden Server geladen.</p>
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label" data-i18n="settings.bg_upload">Datei hochladen</span>
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
@@ -2387,6 +2413,35 @@ 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;
|
||||
}
|
||||
.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
|
||||
|
||||
+1
-1
@@ -119,7 +119,7 @@ async function checkBackupReminder() {
|
||||
const notesData = (widgetData && Array.isArray(widgetData.notes)) ? widgetData.notes : [];
|
||||
const calcHistory = (widgetData && widgetData.calculator) ? widgetData.calculator.history || [] : [];
|
||||
const timerPresets = (widgetData && widgetData.timer) ? widgetData.timer.presets || [] : [];
|
||||
const data = { version: '2.3.0', exported: new Date().toISOString(), boards, settings, trash, notes: notesData, calculator: calcHistory, timerPresets };
|
||||
const data = { version: '2.4.0', exported: new Date().toISOString(), boards, settings, trash, notes: notesData, calculator: calcHistory, timerPresets };
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ function initDataButtons() {
|
||||
btnExport.addEventListener('click', async () => {
|
||||
const widgetData = await Store.get('widgetStates');
|
||||
const data = {
|
||||
version: '2.3.0',
|
||||
version: '2.4.0',
|
||||
exported: new Date().toISOString(),
|
||||
boards,
|
||||
settings,
|
||||
|
||||
+36
-8
@@ -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',
|
||||
@@ -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
|
||||
@@ -423,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
|
||||
@@ -527,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',
|
||||
@@ -846,7 +860,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 +873,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
|
||||
@@ -884,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
|
||||
|
||||
+186
-7
@@ -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');
|
||||
@@ -113,7 +115,131 @@ 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://'));
|
||||
}
|
||||
|
||||
// ---- 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);
|
||||
|
||||
// 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).
|
||||
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.
|
||||
// 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 ----
|
||||
@@ -344,7 +470,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}')`;
|
||||
@@ -371,10 +501,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
|
||||
@@ -390,6 +535,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();
|
||||
|
||||
@@ -434,7 +604,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 +630,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 = () => {
|
||||
@@ -512,7 +690,8 @@ 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 };
|
||||
clearCustomTheme();
|
||||
await saveBoards();
|
||||
await saveTrash();
|
||||
await saveSettings();
|
||||
|
||||
@@ -25,6 +25,7 @@ let settings = {
|
||||
visibleCount: 10,
|
||||
bgUrl: '',
|
||||
theme: 'nebula',
|
||||
customTheme: null,
|
||||
showSearch: true,
|
||||
searchEngine: 'google',
|
||||
toolbarPos: 'right',
|
||||
|
||||
Reference in New Issue
Block a user