Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 41cb185947 | |||
| 0236818372 |
@@ -1,123 +0,0 @@
|
|||||||
name: Release
|
|
||||||
|
|
||||||
# Wird bei einem vX.Y.Z-Tag-Push ausgeloest. Baut die drei Web-Extension-ZIPs
|
|
||||||
# (Chrome/Firefox/Opera) und haengt sie ans passende Gitea-Release.
|
|
||||||
#
|
|
||||||
# Portiert von GitHub Actions auf Gitea Actions (2026-06): der fruehere
|
|
||||||
# softprops/action-gh-release-Step ist GitHub-spezifisch und laeuft auf Gitea
|
|
||||||
# nicht. Ersetzt durch die Gitea-native release-action (volle gitea.com-URL,
|
|
||||||
# da DEFAULT_ACTIONS_URL=github nackte Namen sonst von github.com zieht).
|
|
||||||
# Muster uebernommen aus HellionChat/.gitea/workflows/release.yml.
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
# Manueller Recovery-Trigger: in Gitea "Run workflow" und den Tag (z.B. v2.2.0)
|
|
||||||
# im Ref-Dropdown waehlen, NICHT master. Der Validate-Step unten failt hart
|
|
||||||
# bei einem Nicht-Tag-Ref, weil die release-action GITHUB_REF direkt liest.
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-release:
|
|
||||||
name: Build & Release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 20
|
|
||||||
|
|
||||||
steps:
|
|
||||||
# release-action liest GITHUB_REF direkt (kein tag_name-Input). Vorab
|
|
||||||
# validieren, damit manuelle Dispatches von einem Branch-Ref hier laut
|
|
||||||
# scheitern statt nach einem vollen Build.
|
|
||||||
- name: Validate tag ref
|
|
||||||
run: |
|
|
||||||
if [[ "${GITHUB_REF}" != refs/tags/v* ]]; then
|
|
||||||
echo "::error::Release-Workflow muss auf einem v*-Tag laufen, got ${GITHUB_REF}"
|
|
||||||
echo "::error::Tag pushen, oder im workflow_dispatch-Ref-Dropdown den Tag (nicht master) waehlen."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
|
|
||||||
- name: Extract version from tag
|
|
||||||
id: version
|
|
||||||
run: echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Create Chrome/Edge ZIP (Manifest V3)
|
|
||||||
run: |
|
|
||||||
mkdir -p dist
|
|
||||||
zip -r "dist/hellion-newtab-${{ steps.version.outputs.tag }}-chrome.zip" \
|
|
||||||
manifest.json newtab.html src/js/*.js src/css/ assets/ _locales/ \
|
|
||||||
-x "*.git*" "dist/*" ".github/*" ".gitea/*" "src/js/opera/*"
|
|
||||||
|
|
||||||
- name: Create Firefox ZIP (Manifest V3)
|
|
||||||
run: |
|
|
||||||
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/js/*.js src/css/ assets/ _locales/ \
|
|
||||||
-x "*.git*" "dist/*" ".github/*" ".gitea/*" "manifest.chrome-backup.json" "manifest.firefox.json" "src/js/opera/*"
|
|
||||||
mv manifest.chrome-backup.json manifest.json
|
|
||||||
|
|
||||||
- name: Create Opera/Opera GX ZIP (Manifest V3 + workaround)
|
|
||||||
run: |
|
|
||||||
cp manifest.json manifest.chrome-backup.json
|
|
||||||
cp manifest.opera.json manifest.json
|
|
||||||
zip -r "dist/hellion-newtab-${{ steps.version.outputs.tag }}-opera.zip" \
|
|
||||||
manifest.json newtab.html src/js/*.js src/js/opera/ src/css/ assets/ _locales/ \
|
|
||||||
-x "*.git*" "dist/*" ".github/*" ".gitea/*" "manifest.chrome-backup.json" "manifest.opera.json"
|
|
||||||
mv manifest.chrome-backup.json manifest.json
|
|
||||||
|
|
||||||
- name: Generate SHA256 checksums
|
|
||||||
run: |
|
|
||||||
cd dist
|
|
||||||
sha256sum *.zip > checksums-sha256.txt
|
|
||||||
cat checksums-sha256.txt
|
|
||||||
|
|
||||||
# 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}"
|
|
||||||
|
|
||||||
# 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}))')
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# 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."
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
name: Security
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main, master]
|
|
||||||
pull_request:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 6 * * 1'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
scan:
|
|
||||||
uses: JonKazama-Hellion/security-workflows/.gitea/workflows/security-scan.yml@main
|
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Projektstruktur prüfen
|
- name: Projektstruktur prüfen
|
||||||
run: |
|
run: |
|
||||||
@@ -45,11 +45,6 @@ jobs:
|
|||||||
assert m.get('name'), 'Chrome: Name fehlt'
|
assert m.get('name'), 'Chrome: Name fehlt'
|
||||||
assert m.get('version'), 'Chrome: Version fehlt'
|
assert m.get('version'), 'Chrome: Version fehlt'
|
||||||
assert 'storage' in m.get('permissions', []), 'Chrome: Storage Permission fehlt'
|
assert 'storage' in m.get('permissions', []), 'Chrome: Storage Permission fehlt'
|
||||||
assert 'activeTab' in m.get('permissions', []), 'Chrome: activeTab Permission fehlt (Quick Save v2.3)'
|
|
||||||
assert 'background' in m, 'Chrome: background-Key fehlt (Service Worker v2.3)'
|
|
||||||
assert 'service_worker' in m.get('background', {}), 'Chrome: background.service_worker fehlt'
|
|
||||||
assert isinstance(m.get('commands'), dict) and 'quick-save' in m['commands'], 'Chrome: commands.quick-save fehlt (Quick Save v2.3)'
|
|
||||||
assert 'action' in m, 'Chrome: action-Key fehlt (Badge-Bestätigung v2.3)'
|
|
||||||
print('manifest.json (V3) OK — Version:', m['version'])
|
print('manifest.json (V3) OK — Version:', m['version'])
|
||||||
|
|
||||||
with open('manifest.firefox.json') as f:
|
with open('manifest.firefox.json') as f:
|
||||||
@@ -57,11 +52,6 @@ jobs:
|
|||||||
assert mf.get('manifest_version') == 3, 'Firefox: Manifest V3 erwartet'
|
assert mf.get('manifest_version') == 3, 'Firefox: Manifest V3 erwartet'
|
||||||
assert mf['version'] == m['version'], 'Firefox: Version stimmt nicht mit Chrome überein!'
|
assert mf['version'] == m['version'], 'Firefox: Version stimmt nicht mit Chrome überein!'
|
||||||
assert 'browser_specific_settings' in mf, 'Firefox: browser_specific_settings fehlt'
|
assert 'browser_specific_settings' in mf, 'Firefox: browser_specific_settings fehlt'
|
||||||
assert 'activeTab' in mf.get('permissions', []), 'Firefox: activeTab Permission fehlt (Quick Save v2.3)'
|
|
||||||
assert 'background' in mf, 'Firefox: background-Key fehlt (Event-Page v2.3)'
|
|
||||||
assert 'scripts' in mf.get('background', {}), 'Firefox: background.scripts fehlt (Event-Page, kein service_worker)'
|
|
||||||
assert isinstance(mf.get('commands'), dict) and 'quick-save' in mf['commands'], 'Firefox: commands.quick-save fehlt (Quick Save v2.3)'
|
|
||||||
assert 'action' in mf, 'Firefox: action-Key fehlt (Badge-Bestätigung v2.3)'
|
|
||||||
print('manifest.firefox.json (V3) OK — Version:', mf['version'])
|
print('manifest.firefox.json (V3) OK — Version:', mf['version'])
|
||||||
|
|
||||||
with open('manifest.opera.json') as f:
|
with open('manifest.opera.json') as f:
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Release — creates ZIP packages for Chrome, Firefox and Opera on new 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: Extract version from tag
|
||||||
|
id: version
|
||||||
|
run: echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Create Chrome/Edge ZIP (Manifest V3)
|
||||||
|
run: |
|
||||||
|
mkdir -p dist
|
||||||
|
zip -r "dist/hellion-newtab-${{ steps.version.outputs.tag }}-chrome.zip" \
|
||||||
|
manifest.json newtab.html src/js/*.js src/css/ assets/ _locales/ \
|
||||||
|
-x "*.git*" "dist/*" ".github/*" "src/js/opera/*"
|
||||||
|
|
||||||
|
- name: Create Firefox ZIP (Manifest V3)
|
||||||
|
run: |
|
||||||
|
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/js/*.js src/css/ assets/ _locales/ \
|
||||||
|
-x "*.git*" "dist/*" ".github/*" "manifest.chrome-backup.json" "manifest.firefox.json" "src/js/opera/*"
|
||||||
|
mv manifest.chrome-backup.json manifest.json
|
||||||
|
|
||||||
|
- name: Create Opera/Opera GX ZIP (Manifest V3 + workaround)
|
||||||
|
run: |
|
||||||
|
cp manifest.json manifest.chrome-backup.json
|
||||||
|
cp manifest.opera.json manifest.json
|
||||||
|
zip -r "dist/hellion-newtab-${{ steps.version.outputs.tag }}-opera.zip" \
|
||||||
|
manifest.json newtab.html src/js/*.js src/js/opera/ src/css/ assets/ _locales/ \
|
||||||
|
-x "*.git*" "dist/*" ".github/*" "manifest.chrome-backup.json" "manifest.opera.json"
|
||||||
|
mv manifest.chrome-backup.json manifest.json
|
||||||
|
|
||||||
|
- name: Generate SHA256 checksums
|
||||||
|
run: |
|
||||||
|
cd dist
|
||||||
|
sha256sum *.zip > checksums-sha256.txt
|
||||||
|
cat checksums-sha256.txt
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
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 / 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`
|
||||||
|
|
||||||
|
See [README](README.md) for the full installation instructions.
|
||||||
|
|
||||||
|
### Checksums
|
||||||
|
See `checksums-sha256.txt` to verify file integrity.
|
||||||
|
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
|
||||||
|
generate_release_notes: true
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Sicherheitsprüfung — läuft bei Push und PR auf main/master
|
||||||
|
name: Security Scan
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, master]
|
||||||
|
pull_request:
|
||||||
|
branches: [main, master]
|
||||||
|
schedule:
|
||||||
|
# Wöchentlich Montag 06:00 UTC
|
||||||
|
- cron: '0 6 * * 1'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
codeql:
|
||||||
|
name: CodeQL Analysis
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v3
|
||||||
|
with:
|
||||||
|
languages: javascript
|
||||||
|
|
||||||
|
- name: Run CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v3
|
||||||
|
|
||||||
|
dependency-review:
|
||||||
|
name: Dependency Review
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Dependency Review
|
||||||
|
uses: actions/dependency-review-action@v4
|
||||||
@@ -6,37 +6,6 @@ All notable changes per version. Format based on [Keep a Changelog](https://keep
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## [2.3.0] — 2026-06-14
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- **Command Palette (Ctrl+K)** — Overlay that live-filters bookmarks (title and URL) and board names from the keyboard. Arrow-key navigation, Enter opens the match, Escape closes. Read-only navigation, separate from the web search bar. Combobox/listbox ARIA pattern with focus trap and focus return. New DE/EN i18n strings.
|
|
||||||
- **Trash** — Deleted bookmarks and boards move to a 30-day trash instead of vanishing. Restore or permanently remove them from a new Settings section; entries older than 30 days are cleaned up automatically. Stored under its own storage key with a hard size cap so it cannot exhaust the storage quota.
|
|
||||||
- **Quick Save** — A global keyboard shortcut (default Alt+Shift+S, configurable in the browser shortcut settings) saves the current tab into a fixed Inbox board from any page. Backed by a background worker (service worker on Chrome/Opera, event page on Firefox) that appends to a dedicated pending queue, which the dashboard drains into the Inbox — separate write domains, so a save can never clobber the boards. A badge confirms the save, and open dashboard tabs sync the new bookmark live via a storage-change listener.
|
|
||||||
- **Free layout (bonus)** — Boards can be dragged to free positions via a drag handle, persisted per board. Positions are clamped back into view when the window shrinks, and the layout falls back to a stacked column on small screens. Each board can be pinned with a lock button: a locked board cannot be moved (its drag handle is hidden), preventing accidental repositioning. A drag only counts past a small movement threshold, so a mere click on the handle never shifts a board.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- The bookmark- and board-delete paths no longer remove entries immediately; deletions now route through the trash.
|
|
||||||
- Chrome and Firefox manifests gain a background worker, an `action` entry and the `activeTab` / `commands` permissions to support Quick Save. Opera keeps its existing `tabs` permission and redirect worker.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [2.2.0] — 2026-06-13
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- **View Transitions** — Native cross-fade on theme switch and central modals (Settings, Theme-Picker, custom dialogs, bookmark import, add-board, add-bookmark, rename). Feature-detected via `document.startViewTransition`, instant swap on older browsers. Widgets, notebook sidebar and onboarding deliberately excluded.
|
|
||||||
- **`color-scheme: dark`** — Declares the dark UA scheme so native scrollbars and form controls match the dark themes.
|
|
||||||
- **Accessibility pass** — `role="dialog"` / `aria-modal` / `aria-labelledby` on Settings and Theme-Picker with new focus trap, Escape handling and focus return; `role="toolbar"` + per-button `aria-label` on the widget toolbar; keyboard-operable theme cards (`role="button"`, `tabindex`, Enter/Space); `role="switch"` + `aria-checked` on settings toggles; focusable boards and bookmarks; visible `:focus-visible` ring tinted in the theme accent. New ARIA strings run through the i18n pipeline. Verified with Lighthouse and the axe DevTools extension, not a formal WCAG 2.2 AA audit.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- **`color-mix()` token refactor** — Accent-derived color tokens now computed via `color-mix()` from `var(--accent)`, classified per theme (formula vs. override). Theme-specific alpha values and real special colors stay overrides; no visible theme change. `--border-accent` `:root` drift (179,92,255 → 179,89,255) fixed at both the Nebula block and the `:root` default.
|
|
||||||
- **`@layer` cascade ordering** — CSS reorganized into six layers (base / theme / layout / components / theme-overrides / utilities) so theme component overrides win deterministically instead of relying on selector specificity.
|
|
||||||
- **`clamp()` fluid typography** — Clock, logo, board titles and main spacing scale fluidly via `clamp()`. Existing 768px / 480px breakpoints kept as a safety net.
|
|
||||||
|
|
||||||
### Accessibility
|
|
||||||
- **`prefers-reduced-motion`** — Unlayered `@media` block disables transitions and animations, including the `::view-transition-*` pseudo-elements. The 350ms widget teardown fallback timer is retained so widgets still close when `transitionend` no longer fires under reduced motion.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [2.1.0] — 2026-04-16
|
## [2.1.0] — 2026-04-16
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
{
|
{
|
||||||
"extName": { "message": "Hellion NewTab" },
|
"extName": { "message": "Hellion NewTab" },
|
||||||
"extDesc": { "message": "Persönliches Bookmark-Dashboard mit Boards, Widgets und 11 Themes. Komplett lokal, keine Cloud, kein Tracking." },
|
"extDesc": { "message": "Persönliches Bookmark-Dashboard mit Boards, Widgets und 11 Themes. Komplett lokal, keine Cloud, kein Tracking." }
|
||||||
"cmdQuickSave": { "message": "Aktuellen Tab in die Inbox speichern" },
|
|
||||||
"quickSaveBadge": { "message": "OK" },
|
|
||||||
"quickSaveSaved": { "message": "Gespeichert" },
|
|
||||||
"quickSaveNoTab": { "message": "Kein Tab" }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
{
|
{
|
||||||
"extName": { "message": "Hellion NewTab" },
|
"extName": { "message": "Hellion NewTab" },
|
||||||
"extDesc": { "message": "Personal bookmark dashboard with boards, widgets, and 11 themes. Local-only, no cloud, no tracking." },
|
"extDesc": { "message": "Personal bookmark dashboard with boards, widgets, and 11 themes. Local-only, no cloud, no tracking." }
|
||||||
"cmdQuickSave": { "message": "Save current tab to Inbox" },
|
|
||||||
"quickSaveBadge": { "message": "OK" },
|
|
||||||
"quickSaveSaved": { "message": "Saved" },
|
|
||||||
"quickSaveNoTab": { "message": "No tab" }
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-21
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "__MSG_extName__",
|
"name": "__MSG_extName__",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"version": "2.3.0",
|
"version": "2.1.0",
|
||||||
"description": "__MSG_extDesc__",
|
"description": "__MSG_extDesc__",
|
||||||
"author": "Hellion Online Media - Florian Wathling",
|
"author": "Hellion Online Media - Florian Wathling",
|
||||||
"homepage_url": "https://hellion-media.de",
|
"homepage_url": "https://hellion-media.de",
|
||||||
@@ -11,28 +11,9 @@
|
|||||||
"newtab": "newtab.html"
|
"newtab": "newtab.html"
|
||||||
},
|
},
|
||||||
|
|
||||||
"background": {
|
|
||||||
"scripts": ["src/js/quicksave-core.js", "src/js/background.js"]
|
|
||||||
},
|
|
||||||
|
|
||||||
"action": {
|
|
||||||
"default_title": "Hellion Dashboard"
|
|
||||||
},
|
|
||||||
|
|
||||||
"commands": {
|
|
||||||
"quick-save": {
|
|
||||||
"suggested_key": {
|
|
||||||
"default": "Alt+Shift+S",
|
|
||||||
"mac": "Alt+Shift+S"
|
|
||||||
},
|
|
||||||
"description": "__MSG_cmdQuickSave__"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"storage",
|
"storage",
|
||||||
"bookmarks",
|
"bookmarks"
|
||||||
"activeTab"
|
|
||||||
],
|
],
|
||||||
|
|
||||||
"browser_specific_settings": {
|
"browser_specific_settings": {
|
||||||
|
|||||||
+2
-18
@@ -2,32 +2,16 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "__MSG_extName__",
|
"name": "__MSG_extName__",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"version": "2.3.0",
|
"version": "2.1.0",
|
||||||
"description": "__MSG_extDesc__",
|
"description": "__MSG_extDesc__",
|
||||||
"author": "Hellion Online Media - Florian Wathling",
|
"author": "Hellion Online Media - Florian Wathling",
|
||||||
"homepage_url": "https://hellion-media.de",
|
"homepage_url": "https://hellion-media.de",
|
||||||
"chrome_url_overrides": {
|
"chrome_url_overrides": {
|
||||||
"newtab": "newtab.html"
|
"newtab": "newtab.html"
|
||||||
},
|
},
|
||||||
"background": {
|
|
||||||
"service_worker": "src/js/background.js"
|
|
||||||
},
|
|
||||||
"action": {
|
|
||||||
"default_title": "Hellion Dashboard"
|
|
||||||
},
|
|
||||||
"commands": {
|
|
||||||
"quick-save": {
|
|
||||||
"suggested_key": {
|
|
||||||
"default": "Alt+Shift+S",
|
|
||||||
"mac": "Alt+Shift+S"
|
|
||||||
},
|
|
||||||
"description": "__MSG_cmdQuickSave__"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"storage",
|
"storage",
|
||||||
"bookmarks",
|
"bookmarks"
|
||||||
"activeTab"
|
|
||||||
],
|
],
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-11
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "__MSG_extName__",
|
"name": "__MSG_extName__",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"version": "2.3.0",
|
"version": "2.1.0",
|
||||||
"description": "__MSG_extDesc__",
|
"description": "__MSG_extDesc__",
|
||||||
"author": "Hellion Online Media - Florian Wathling",
|
"author": "Hellion Online Media - Florian Wathling",
|
||||||
"homepage_url": "https://hellion-media.de",
|
"homepage_url": "https://hellion-media.de",
|
||||||
@@ -40,16 +40,6 @@
|
|||||||
"default_title": "Hellion Dashboard"
|
"default_title": "Hellion Dashboard"
|
||||||
},
|
},
|
||||||
|
|
||||||
"commands": {
|
|
||||||
"quick-save": {
|
|
||||||
"suggested_key": {
|
|
||||||
"default": "Alt+Shift+S",
|
|
||||||
"mac": "Alt+Shift+S"
|
|
||||||
},
|
|
||||||
"description": "__MSG_cmdQuickSave__"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"content_security_policy": {
|
"content_security_policy": {
|
||||||
"extension_pages": "script-src 'self'; object-src 'self'"
|
"extension_pages": "script-src 'self'; object-src 'self'"
|
||||||
},
|
},
|
||||||
|
|||||||
+24
-44
@@ -39,10 +39,6 @@
|
|||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a10 10 0 100 20 4 4 0 01-1-7.9 1 1 0 011-.1h1a2 2 0 002-2V7a5 5 0 00-3-4.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="6" r="1.5"/><circle cx="17" cy="10" r="1.5"/><circle cx="9" cy="17" r="1.5"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a10 10 0 100 20 4 4 0 01-1-7.9 1 1 0 011-.1h1a2 2 0 002-2V7a5 5 0 00-3-4.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="6" r="1.5"/><circle cx="17" cy="10" r="1.5"/><circle cx="9" cy="17" r="1.5"/></svg>
|
||||||
<span data-i18n="header.theme">Darstellung</span>
|
<span data-i18n="header.theme">Darstellung</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-icon" id="btnPalette" title="Lesezeichen durchsuchen (Strg+K)" data-i18n-title="palette.discover">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|
||||||
<span data-i18n="palette.discover_label">Suchen</span>
|
|
||||||
</button>
|
|
||||||
<button class="btn-icon" id="btnSettings" title="Einstellungen" data-i18n-title="header.settings_title">
|
<button class="btn-icon" id="btnSettings" title="Einstellungen" data-i18n-title="header.settings_title">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
|
||||||
<span data-i18n="header.settings">Settings</span>
|
<span data-i18n="header.settings">Settings</span>
|
||||||
@@ -64,7 +60,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- WIDGET TOOLBAR -->
|
<!-- WIDGET TOOLBAR -->
|
||||||
<div class="widget-toolbar" id="widgetToolbar" role="toolbar" aria-orientation="vertical" data-i18n-aria-label="toolbar.label">
|
<div class="widget-toolbar" id="widgetToolbar">
|
||||||
<button class="widget-toolbar-btn" data-action="new-note" data-i18n-title="toolbar.note" title="Note erstellen">
|
<button class="widget-toolbar-btn" data-action="new-note" data-i18n-title="toolbar.note" title="Note erstellen">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
@@ -107,9 +103,9 @@
|
|||||||
|
|
||||||
<!-- SETTINGS PANEL -->
|
<!-- SETTINGS PANEL -->
|
||||||
<div class="panel-overlay" id="settingsOverlay"></div>
|
<div class="panel-overlay" id="settingsOverlay"></div>
|
||||||
<aside class="settings-panel" id="settingsPanel" role="dialog" aria-modal="true" aria-labelledby="settingsPanelTitle" aria-hidden="true">
|
<aside class="settings-panel" id="settingsPanel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<span id="settingsPanelTitle" data-i18n="settings.title">Einstellungen</span>
|
<span data-i18n="settings.title">Einstellungen</span>
|
||||||
<button class="btn-close" id="btnCloseSettings" data-i18n-title="dialog.close">✕</button>
|
<button class="btn-close" id="btnCloseSettings" data-i18n-title="dialog.close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
@@ -158,7 +154,7 @@
|
|||||||
<span class="setting-desc" data-i18n="settings.image_ref.desc">Bilder als Referenz anzeigen (nur aktuelle Session)</span>
|
<span class="setting-desc" data-i18n="settings.image_ref.desc">Bilder als Referenz anzeigen (nur aktuelle Session)</span>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle">
|
<label class="toggle">
|
||||||
<input type="checkbox" id="settingImageRef" role="switch" aria-checked="false">
|
<input type="checkbox" id="settingImageRef">
|
||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -204,20 +200,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- PAPIERKORB / TRASH -->
|
|
||||||
<section class="settings-section" data-section="trash">
|
|
||||||
<button class="settings-section-title" type="button">
|
|
||||||
<span class="section-chevron">▸</span>
|
|
||||||
<span data-i18n="trash.section">PAPIERKORB</span>
|
|
||||||
</button>
|
|
||||||
<div class="section-content">
|
|
||||||
<div class="trash-list" id="trashList"></div>
|
|
||||||
<div class="setting-row trash-actions" id="trashActionsRow">
|
|
||||||
<button class="btn-danger" id="btnEmptyTrash" data-i18n="trash.empty_btn">Papierkorb leeren</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- DANGER ZONE -->
|
<!-- DANGER ZONE -->
|
||||||
<section class="settings-section" data-section="danger">
|
<section class="settings-section" data-section="danger">
|
||||||
<button class="settings-section-title danger" type="button">
|
<button class="settings-section-title danger" type="button">
|
||||||
@@ -241,7 +223,7 @@
|
|||||||
<div class="panel-footer">
|
<div class="panel-footer">
|
||||||
<div class="about-block">
|
<div class="about-block">
|
||||||
<div class="about-logo" data-i18n="about.title">⬡ HELLION NEWTAB</div>
|
<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.1.0 · by Hellion Online Media</div>
|
||||||
|
|
||||||
<div class="about-links">
|
<div class="about-links">
|
||||||
<a href="https://hellion-media.de/impressum" target="_blank" class="about-link">
|
<a href="https://hellion-media.de/impressum" target="_blank" class="about-link">
|
||||||
@@ -309,63 +291,63 @@
|
|||||||
|
|
||||||
<!-- THEME PICKER MODAL -->
|
<!-- THEME PICKER MODAL -->
|
||||||
<div class="modal-overlay" id="themeOverlay">
|
<div class="modal-overlay" id="themeOverlay">
|
||||||
<div class="theme-modal" id="themeModal" role="dialog" aria-modal="true" aria-labelledby="themeModalTitle" aria-hidden="true">
|
<div class="theme-modal" id="themeModal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<span id="themeModalTitle" data-i18n="modal.theme_header">Darstellung</span>
|
<span data-i18n="modal.theme_header">Darstellung</span>
|
||||||
<button class="btn-close" id="btnCloseTheme" data-i18n-title="dialog.close">✕</button>
|
<button class="btn-close" id="btnCloseTheme" data-i18n-title="dialog.close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="theme-grid">
|
<div class="theme-grid">
|
||||||
<div class="theme-card active" data-value="nebula" role="button" tabindex="0" aria-pressed="true" data-i18n-aria-label="theme.card.nebula">
|
<div class="theme-card active" data-value="nebula">
|
||||||
<img class="theme-card-img" src="assets/themes/bg-nebula.webp" alt="Nebula" />
|
<img class="theme-card-img" src="assets/themes/bg-nebula.webp" alt="Nebula" />
|
||||||
<span class="theme-card-label">Nebula</span>
|
<span class="theme-card-label">Nebula</span>
|
||||||
<span class="theme-card-check">✓</span>
|
<span class="theme-card-check">✓</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="theme-card" data-value="crescent" role="button" tabindex="0" aria-pressed="false" data-i18n-aria-label="theme.card.crescent">
|
<div class="theme-card" data-value="crescent">
|
||||||
<img class="theme-card-img" src="assets/themes/bg-crescent.webp" alt="Crescent" />
|
<img class="theme-card-img" src="assets/themes/bg-crescent.webp" alt="Crescent" />
|
||||||
<span class="theme-card-label">Crescent</span>
|
<span class="theme-card-label">Crescent</span>
|
||||||
<span class="theme-card-check">✓</span>
|
<span class="theme-card-check">✓</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="theme-card" data-value="event-horizon" role="button" tabindex="0" aria-pressed="false" data-i18n-aria-label="theme.card.event_horizon">
|
<div class="theme-card" data-value="event-horizon">
|
||||||
<img class="theme-card-img" src="assets/themes/bg-event-horizon.webp" alt="Event Horizon" />
|
<img class="theme-card-img" src="assets/themes/bg-event-horizon.webp" alt="Event Horizon" />
|
||||||
<span class="theme-card-label">Event Horizon</span>
|
<span class="theme-card-label">Event Horizon</span>
|
||||||
<span class="theme-card-check">✓</span>
|
<span class="theme-card-check">✓</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="theme-card" data-value="merchantman" role="button" tabindex="0" aria-pressed="false" data-i18n-aria-label="theme.card.merchantman">
|
<div class="theme-card" data-value="merchantman">
|
||||||
<img class="theme-card-img" src="assets/themes/bg-merchantman.webp" alt="Merchantman" />
|
<img class="theme-card-img" src="assets/themes/bg-merchantman.webp" alt="Merchantman" />
|
||||||
<span class="theme-card-label">Merchantman</span>
|
<span class="theme-card-label">Merchantman</span>
|
||||||
<span class="theme-card-check">✓</span>
|
<span class="theme-card-check">✓</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="theme-card" data-value="julia-jin" role="button" tabindex="0" aria-pressed="false" data-i18n-aria-label="theme.card.julia_jin">
|
<div class="theme-card" data-value="julia-jin">
|
||||||
<img class="theme-card-img" src="assets/themes/bg-julia-jin.webp" alt="Julia & Jin" />
|
<img class="theme-card-img" src="assets/themes/bg-julia-jin.webp" alt="Julia & Jin" />
|
||||||
<span class="theme-card-label">Julia & Jin</span>
|
<span class="theme-card-label">Julia & Jin</span>
|
||||||
<span class="theme-card-check">✓</span>
|
<span class="theme-card-check">✓</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="theme-card" data-value="sc-sunset" role="button" tabindex="0" aria-pressed="false" data-i18n-aria-label="theme.card.sc_sunset">
|
<div class="theme-card" data-value="sc-sunset">
|
||||||
<img class="theme-card-img" src="assets/themes/bg-sc-sunset.webp" alt="SC Sunset" />
|
<img class="theme-card-img" src="assets/themes/bg-sc-sunset.webp" alt="SC Sunset" />
|
||||||
<span class="theme-card-label">SC Sunset</span>
|
<span class="theme-card-label">SC Sunset</span>
|
||||||
<span class="theme-card-check">✓</span>
|
<span class="theme-card-check">✓</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="theme-card" data-value="hellion-hud" role="button" tabindex="0" aria-pressed="false" data-i18n-aria-label="theme.card.hellion_hud">
|
<div class="theme-card" data-value="hellion-hud">
|
||||||
<img class="theme-card-img" src="assets/themes/bg-hellion-hud.webp" alt="Hellion HUD" />
|
<img class="theme-card-img" src="assets/themes/bg-hellion-hud.webp" alt="Hellion HUD" />
|
||||||
<span class="theme-card-label">HUD</span>
|
<span class="theme-card-label">HUD</span>
|
||||||
<span class="theme-card-check">✓</span>
|
<span class="theme-card-check">✓</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="theme-card" data-value="hellion-energy" role="button" tabindex="0" aria-pressed="false" data-i18n-aria-label="theme.card.hellion_energy">
|
<div class="theme-card" data-value="hellion-energy">
|
||||||
<img class="theme-card-img" src="assets/themes/bg-hellion-energy.webp" alt="Hellion Energy" />
|
<img class="theme-card-img" src="assets/themes/bg-hellion-energy.webp" alt="Hellion Energy" />
|
||||||
<span class="theme-card-label">Energy</span>
|
<span class="theme-card-label">Energy</span>
|
||||||
<span class="theme-card-check">✓</span>
|
<span class="theme-card-check">✓</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="theme-card" data-value="satisfactory" role="button" tabindex="0" aria-pressed="false" data-i18n-aria-label="theme.card.satisfactory">
|
<div class="theme-card" data-value="satisfactory">
|
||||||
<img class="theme-card-img" src="assets/themes/bg-satisfactory.webp" alt="Satisfactory" />
|
<img class="theme-card-img" src="assets/themes/bg-satisfactory.webp" alt="Satisfactory" />
|
||||||
<span class="theme-card-label">Satisfactory</span>
|
<span class="theme-card-label">Satisfactory</span>
|
||||||
<span class="theme-card-check">✓</span>
|
<span class="theme-card-check">✓</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="theme-card" data-value="avorion" role="button" tabindex="0" aria-pressed="false" data-i18n-aria-label="theme.card.avorion">
|
<div class="theme-card" data-value="avorion">
|
||||||
<img class="theme-card-img" src="assets/themes/bg-avorion.webp" alt="Avorion" />
|
<img class="theme-card-img" src="assets/themes/bg-avorion.webp" alt="Avorion" />
|
||||||
<span class="theme-card-label">Avorion</span>
|
<span class="theme-card-label">Avorion</span>
|
||||||
<span class="theme-card-check">✓</span>
|
<span class="theme-card-check">✓</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="theme-card" data-value="hellion-stealth" role="button" tabindex="0" aria-pressed="false" data-i18n-aria-label="theme.card.hellion_stealth">
|
<div class="theme-card" data-value="hellion-stealth">
|
||||||
<img class="theme-card-img" src="assets/themes/bg-scPolaris.webp" alt="Hellion Stealth" />
|
<img class="theme-card-img" src="assets/themes/bg-scPolaris.webp" alt="Hellion Stealth" />
|
||||||
<span class="theme-card-label">Stealth</span>
|
<span class="theme-card-label">Stealth</span>
|
||||||
<span class="theme-card-check">✓</span>
|
<span class="theme-card-check">✓</span>
|
||||||
@@ -400,42 +382,42 @@
|
|||||||
<span class="setting-label" data-i18n="settings.compact">Kompaktmodus</span>
|
<span class="setting-label" data-i18n="settings.compact">Kompaktmodus</span>
|
||||||
<span class="setting-desc" data-i18n="settings.compact.desc">Weniger Abstand für mehr Bookmarks</span>
|
<span class="setting-desc" data-i18n="settings.compact.desc">Weniger Abstand für mehr Bookmarks</span>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle"><input type="checkbox" id="settingCompact" role="switch" aria-checked="false" /><span class="slider"></span></label>
|
<label class="toggle"><input type="checkbox" id="settingCompact" /><span class="slider"></span></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
<span class="setting-label" data-i18n="settings.shorten">Lange Titel kürzen</span>
|
<span class="setting-label" data-i18n="settings.shorten">Lange Titel kürzen</span>
|
||||||
<span class="setting-desc" data-i18n="settings.shorten.desc">Titel auf eine Zeile mit „…" kürzen</span>
|
<span class="setting-desc" data-i18n="settings.shorten.desc">Titel auf eine Zeile mit „…" kürzen</span>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle"><input type="checkbox" id="settingShorten" role="switch" aria-checked="false" /><span class="slider"></span></label>
|
<label class="toggle"><input type="checkbox" id="settingShorten" /><span class="slider"></span></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
<span class="setting-label" data-i18n="settings.search">Suchleiste anzeigen</span>
|
<span class="setting-label" data-i18n="settings.search">Suchleiste anzeigen</span>
|
||||||
<span class="setting-desc" data-i18n="settings.search.desc">Suchleiste unter dem Header ein/aus</span>
|
<span class="setting-desc" data-i18n="settings.search.desc">Suchleiste unter dem Header ein/aus</span>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle"><input type="checkbox" id="settingShowSearch" role="switch" aria-checked="true" checked /><span class="slider"></span></label>
|
<label class="toggle"><input type="checkbox" id="settingShowSearch" checked /><span class="slider"></span></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
<span class="setting-label" data-i18n="settings.newtab">Links in neuem Tab</span>
|
<span class="setting-label" data-i18n="settings.newtab">Links in neuem Tab</span>
|
||||||
<span class="setting-desc" data-i18n="settings.newtab.desc">Bookmarks in neuem Browser-Tab öffnen</span>
|
<span class="setting-desc" data-i18n="settings.newtab.desc">Bookmarks in neuem Browser-Tab öffnen</span>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle"><input type="checkbox" id="settingNewTab" role="switch" aria-checked="true" checked /><span class="slider"></span></label>
|
<label class="toggle"><input type="checkbox" id="settingNewTab" checked /><span class="slider"></span></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
<span class="setting-label" data-i18n="settings.showdesc">Beschreibungen anzeigen</span>
|
<span class="setting-label" data-i18n="settings.showdesc">Beschreibungen anzeigen</span>
|
||||||
<span class="setting-desc" data-i18n="settings.showdesc.desc">Gespeicherte Beschreibung unter Bookmarks</span>
|
<span class="setting-desc" data-i18n="settings.showdesc.desc">Gespeicherte Beschreibung unter Bookmarks</span>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle"><input type="checkbox" id="settingShowDesc" role="switch" aria-checked="false" /><span class="slider"></span></label>
|
<label class="toggle"><input type="checkbox" id="settingShowDesc" /><span class="slider"></span></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
<span class="setting-label" data-i18n="settings.hideextra">Bookmarks ausblenden</span>
|
<span class="setting-label" data-i18n="settings.hideextra">Bookmarks ausblenden</span>
|
||||||
<span class="setting-desc" data-i18n="settings.hideextra.desc">Überzählige Bookmarks in langen Boards verstecken</span>
|
<span class="setting-desc" data-i18n="settings.hideextra.desc">Überzählige Bookmarks in langen Boards verstecken</span>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle"><input type="checkbox" id="settingHideExtra" role="switch" aria-checked="false" /><span class="slider"></span></label>
|
<label class="toggle"><input type="checkbox" id="settingHideExtra" /><span class="slider"></span></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-row" id="visibleCountRow">
|
<div class="setting-row" id="visibleCountRow">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
@@ -510,7 +492,6 @@
|
|||||||
<!-- Storage muss zuerst -->
|
<!-- Storage muss zuerst -->
|
||||||
<script src="src/js/storage.js"></script>
|
<script src="src/js/storage.js"></script>
|
||||||
<!-- State & Hilfsfunktionen -->
|
<!-- State & Hilfsfunktionen -->
|
||||||
<script src="src/js/quicksave-core.js"></script>
|
|
||||||
<script src="src/js/state.js"></script>
|
<script src="src/js/state.js"></script>
|
||||||
<!-- i18n (nach state.js, vor allen Modulen die t() nutzen könnten) -->
|
<!-- i18n (nach state.js, vor allen Modulen die t() nutzen könnten) -->
|
||||||
<script src="src/js/i18n.js"></script>
|
<script src="src/js/i18n.js"></script>
|
||||||
@@ -523,7 +504,6 @@
|
|||||||
<script src="src/js/boards.js"></script>
|
<script src="src/js/boards.js"></script>
|
||||||
<script src="src/js/settings.js"></script>
|
<script src="src/js/settings.js"></script>
|
||||||
<script src="src/js/search.js"></script>
|
<script src="src/js/search.js"></script>
|
||||||
<script src="src/js/palette.js"></script>
|
|
||||||
<script src="src/js/widgets.js"></script>
|
<script src="src/js/widgets.js"></script>
|
||||||
<script src="src/js/notes.js"></script>
|
<script src="src/js/notes.js"></script>
|
||||||
<script src="src/js/calculator.js"></script>
|
<script src="src/js/calculator.js"></script>
|
||||||
|
|||||||
+4
-5
@@ -26,10 +26,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "TypeScript type definitions stay grouped with each other",
|
"description": "TypeScript type definitions stay grouped with each other",
|
||||||
"groupName": "type definitions",
|
"matchPackagePrefixes": ["@types/"],
|
||||||
"matchPackageNames": [
|
"groupName": "type definitions"
|
||||||
"@types/{/,}**"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Dev dependencies in their own group",
|
"description": "Dev dependencies in their own group",
|
||||||
@@ -44,7 +42,8 @@
|
|||||||
],
|
],
|
||||||
"vulnerabilityAlerts": {
|
"vulnerabilityAlerts": {
|
||||||
"labels": ["security", "vulnerability"],
|
"labels": ["security", "vulnerability"],
|
||||||
"schedule": ["at any time"]
|
"schedule": ["at any time"],
|
||||||
|
"prPriority": 10
|
||||||
},
|
},
|
||||||
"lockFileMaintenance": {
|
"lockFileMaintenance": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
|||||||
+137
-320
@@ -1,12 +1,3 @@
|
|||||||
/* =============================================
|
|
||||||
CASCADE LAYERS (v2.2) — Reihenfolge = Prioritaet (spaeter gewinnt)
|
|
||||||
theme-overrides MUSS nach components stehen, sonst verlieren die
|
|
||||||
theme-spezifischen Komponenten-Regeln (Board-Blur, Cinzel-Logos,
|
|
||||||
Hover-Tints) und alle 11 Themes brechen.
|
|
||||||
prefers-reduced-motion bleibt bewusst UNGESCHICHTET (Phase 4).
|
|
||||||
============================================= */
|
|
||||||
@layer base, theme, layout, components, theme-overrides, utilities;
|
|
||||||
|
|
||||||
/* =============================================
|
/* =============================================
|
||||||
HELLION Dashboard — Theme System
|
HELLION Dashboard — Theme System
|
||||||
Themes: nebula | crescent | event-horizon | merchantman | julia-jin | sc-sunset | hellion-hud | hellion-energy
|
Themes: nebula | crescent | event-horizon | merchantman | julia-jin | sc-sunset | hellion-hud | hellion-energy
|
||||||
@@ -18,7 +9,6 @@
|
|||||||
- Inter: Fließtext und allgemeine Lesbarkeit
|
- Inter: Fließtext und allgemeine Lesbarkeit
|
||||||
- Cinzel: Alternative Display-Schriftart für bestimmte Themes
|
- Cinzel: Alternative Display-Schriftart für bestimmte Themes
|
||||||
============================================= */
|
============================================= */
|
||||||
@layer base {
|
|
||||||
/* Rajdhani - Lokal */
|
/* Rajdhani - Lokal */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Rajdhani';
|
font-family: 'Rajdhani';
|
||||||
@@ -56,15 +46,9 @@
|
|||||||
/* ---- BASE VARIABLES (Nebula = Default) ---- */
|
/* ---- BASE VARIABLES (Nebula = Default) ---- */
|
||||||
:root {
|
:root {
|
||||||
--accent: #b359ff;
|
--accent: #b359ff;
|
||||||
/* Akzent-Töne als Formel aus --accent abgeleitet (Spec Block 1, color-mix Mittelweg).
|
--accent-dim: rgba(179, 89, 255, 0.12);
|
||||||
--*-pct ist die Pro-Theme-Alpha-Variable; Default = häufigster Wert über alle 11 Themes.
|
--accent-glow: rgba(179, 89, 255, 0.05);
|
||||||
Themes mit abweichendem Alpha überschreiben nur die Prozent-Variable im theme-Layer. */
|
--border-accent: rgba(179, 92, 255, 0.25);
|
||||||
--accent-dim-pct: 12%;
|
|
||||||
--accent-glow-pct: 8%;
|
|
||||||
--border-accent-pct: 25%;
|
|
||||||
--accent-dim: color-mix(in srgb, var(--accent) var(--accent-dim-pct), transparent);
|
|
||||||
--accent-glow: color-mix(in srgb, var(--accent) var(--accent-glow-pct), transparent);
|
|
||||||
--border-accent: color-mix(in srgb, var(--accent) var(--border-accent-pct), transparent);
|
|
||||||
--bg-primary: #050308;
|
--bg-primary: #050308;
|
||||||
--bg-board: rgba(10, 6, 14, 0.55);
|
--bg-board: rgba(10, 6, 14, 0.55);
|
||||||
--border: rgba(255, 255, 255, 0.06);
|
--border: rgba(255, 255, 255, 0.06);
|
||||||
@@ -77,18 +61,14 @@
|
|||||||
--radius: 8px;
|
--radius: 8px;
|
||||||
--radius-sm: 5px;
|
--radius-sm: 5px;
|
||||||
--board-width: 240px;
|
--board-width: 240px;
|
||||||
--spacing: clamp(0.5rem, 0.4583rem + 0.14vw, 0.625rem);
|
--spacing: 10px;
|
||||||
--spacing-compact: 5px;
|
--spacing-compact: 5px;
|
||||||
--overlay-bg: radial-gradient(circle at center, rgba(10,6,14,0.3) 0%, rgba(5,3,8,0.85) 100%);
|
--overlay-bg: radial-gradient(circle at center, rgba(10,6,14,0.3) 0%, rgba(5,3,8,0.85) 100%);
|
||||||
--header-bg: rgba(10,6,14,0.90);
|
--header-bg: rgba(10,6,14,0.90);
|
||||||
--board-hover-border-pct: 22%;
|
--board-hover-border: rgba(179,89,255,0.18);
|
||||||
--logo-shadow-pct: 45%;
|
|
||||||
--toggle-on-bg-pct: 20%;
|
|
||||||
--board-hover-border: color-mix(in srgb, var(--accent) var(--board-hover-border-pct), transparent);
|
|
||||||
--toggle-on-bg: rgba(214,92,255,0.22);
|
--toggle-on-bg: rgba(214,92,255,0.22);
|
||||||
--logo-shadow: color-mix(in srgb, var(--accent) var(--logo-shadow-pct), transparent);
|
--logo-shadow: rgba(179,89,255,0.35);
|
||||||
--bg-solid-fallback: #0a060e;
|
--bg-solid-fallback: #0a060e;
|
||||||
color-scheme: dark;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fallback fuer Browser die backdrop-filter blockieren (z.B. Brave Shields) */
|
/* Fallback fuer Browser die backdrop-filter blockieren (z.B. Brave Shields) */
|
||||||
@@ -102,17 +82,15 @@
|
|||||||
background-color: var(--bg-solid-fallback, var(--bg-primary));
|
background-color: var(--bg-solid-fallback, var(--bg-primary));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@layer theme {
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
THEME: NEBULA (magenta / cosmic nebula)
|
THEME: NEBULA (magenta / cosmic nebula)
|
||||||
============================================ */
|
============================================ */
|
||||||
[data-theme="nebula"] {
|
[data-theme="nebula"] {
|
||||||
--accent: #b359ff;
|
--accent: #b359ff;
|
||||||
--accent-glow-pct: 5%;
|
--accent-dim: rgba(179, 89, 255, 0.12);
|
||||||
--board-hover-border-pct: 18%;
|
--accent-glow: rgba(179, 89, 255, 0.05);
|
||||||
--logo-shadow-pct: 35%;
|
--border-accent: rgba(179, 92, 255, 0.25);
|
||||||
--bg-primary: #050308;
|
--bg-primary: #050308;
|
||||||
--bg-board: rgba(10, 6, 14, 0.55);
|
--bg-board: rgba(10, 6, 14, 0.55);
|
||||||
--border: rgba(255, 255, 255, 0.055);
|
--border: rgba(255, 255, 255, 0.055);
|
||||||
@@ -123,18 +101,22 @@
|
|||||||
--font-body: 'Inter', sans-serif;
|
--font-body: 'Inter', sans-serif;
|
||||||
--overlay-bg: radial-gradient(circle at center, rgba(10,6,14,0.3) 0%, rgba(5,3,8,0.85) 100%);
|
--overlay-bg: radial-gradient(circle at center, rgba(10,6,14,0.3) 0%, rgba(5,3,8,0.85) 100%);
|
||||||
--header-bg: rgba(10,6,14,0.92);
|
--header-bg: rgba(10,6,14,0.92);
|
||||||
|
--board-hover-border: rgba(179,89,255,0.18);
|
||||||
--toggle-on-bg: rgba(214,92,255,0.22);
|
--toggle-on-bg: rgba(214,92,255,0.22);
|
||||||
|
--logo-shadow: rgba(179,89,255,0.35);
|
||||||
--bg-solid-fallback: #0a060e;
|
--bg-solid-fallback: #0a060e;
|
||||||
}
|
}
|
||||||
|
[data-theme="nebula"] .board { border-color: rgba(214,92,255,0.10); }
|
||||||
|
[data-theme="nebula"] .bm-item:hover { background: rgba(214,92,255,0.05); }
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
THEME: CRESCENT (gold / minimalist night)
|
THEME: CRESCENT (gold / minimalist night)
|
||||||
============================================ */
|
============================================ */
|
||||||
[data-theme="crescent"] {
|
[data-theme="crescent"] {
|
||||||
--accent: #d4bd8a;
|
--accent: #d4bd8a;
|
||||||
--accent-glow-pct: 5%;
|
--accent-dim: rgba(212, 189, 138, 0.12);
|
||||||
--board-hover-border-pct: 20%;
|
--accent-glow: rgba(212, 189, 138, 0.05);
|
||||||
--logo-shadow-pct: 40%;
|
--border-accent: rgba(212, 189, 138, 0.25);
|
||||||
--bg-primary: #06080f;
|
--bg-primary: #06080f;
|
||||||
--bg-board: rgba(8, 12, 22, 0.45);
|
--bg-board: rgba(8, 12, 22, 0.45);
|
||||||
--border: rgba(212, 189, 138, 0.10);
|
--border: rgba(212, 189, 138, 0.10);
|
||||||
@@ -145,17 +127,27 @@
|
|||||||
--font-body: 'Inter', sans-serif;
|
--font-body: 'Inter', sans-serif;
|
||||||
--overlay-bg: radial-gradient(circle at center, rgba(6,8,15,0.2) 0%, rgba(6,8,15,0.9) 100%);
|
--overlay-bg: radial-gradient(circle at center, rgba(6,8,15,0.2) 0%, rgba(6,8,15,0.9) 100%);
|
||||||
--header-bg: rgba(6,8,15,0.95);
|
--header-bg: rgba(6,8,15,0.95);
|
||||||
|
--board-hover-border: rgba(212, 189, 138, 0.20);
|
||||||
--toggle-on-bg: rgba(200,168,74,0.22);
|
--toggle-on-bg: rgba(200,168,74,0.22);
|
||||||
|
--logo-shadow: rgba(212, 189, 138, 0.40);
|
||||||
--bg-solid-fallback: #080c16;
|
--bg-solid-fallback: #080c16;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme="crescent"] .logo { font-family: 'Cinzel', serif; letter-spacing: 4px; }
|
||||||
|
[data-theme="crescent"] .clock { font-family: 'Cinzel', serif; }
|
||||||
|
[data-theme="crescent"] .board-title { letter-spacing: 2px; }
|
||||||
|
[data-theme="crescent"] .bm-item:hover { background: rgba(200,168,74,0.05); }
|
||||||
|
[data-theme="crescent"] .board { border-color: rgba(200,168,74,0.10); }
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
THEME: EVENT HORIZON (Cosmic Purple / deep space)
|
THEME: EVENT HORIZON (Cosmic Purple / deep space)
|
||||||
============================================ */
|
============================================ */
|
||||||
[data-theme="event-horizon"] {
|
[data-theme="event-horizon"] {
|
||||||
--accent: #9d5cff;
|
--accent: #9d5cff;
|
||||||
--border-accent-pct: 28%;
|
--accent-dim: rgba(157, 92, 255, 0.12);
|
||||||
|
--accent-glow: rgba(157, 92, 255, 0.08);
|
||||||
|
--border-accent: rgba(157, 92, 255, 0.28);
|
||||||
--bg-primary: #040308;
|
--bg-primary: #040308;
|
||||||
--bg-board: rgba(8, 5, 15, 0.45);
|
--bg-board: rgba(8, 5, 15, 0.45);
|
||||||
--border: rgba(157, 92, 255, 0.12);
|
--border: rgba(157, 92, 255, 0.12);
|
||||||
@@ -166,17 +158,23 @@
|
|||||||
--font-body: 'Inter', sans-serif;
|
--font-body: 'Inter', sans-serif;
|
||||||
--overlay-bg: radial-gradient(circle at center, rgba(4,3,8,0.2) 0%, rgba(4,3,8,0.95) 100%);
|
--overlay-bg: radial-gradient(circle at center, rgba(4,3,8,0.2) 0%, rgba(4,3,8,0.95) 100%);
|
||||||
--header-bg: rgba(4,3,8,0.96);
|
--header-bg: rgba(4,3,8,0.96);
|
||||||
|
--board-hover-border: rgba(157, 92, 255, 0.22);
|
||||||
--toggle-on-bg: rgba(224,128,48,0.22);
|
--toggle-on-bg: rgba(224,128,48,0.22);
|
||||||
|
--logo-shadow: rgba(157, 92, 255, 0.45);
|
||||||
--bg-solid-fallback: #08050f;
|
--bg-solid-fallback: #08050f;
|
||||||
}
|
}
|
||||||
|
[data-theme="event-horizon"] .board { border-color: rgba(157, 92, 255, 0.15); }
|
||||||
|
[data-theme="event-horizon"] .bm-item:hover { background: rgba(157, 92, 255, 0.08); }
|
||||||
|
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
THEME: MERCHANTMAN (Emerald / industrial sci-fi)
|
THEME: MERCHANTMAN (Emerald / industrial sci-fi)
|
||||||
============================================ */
|
============================================ */
|
||||||
[data-theme="merchantman"] {
|
[data-theme="merchantman"] {
|
||||||
--accent: #2eb8b8;
|
--accent: #2eb8b8;
|
||||||
--accent-glow-pct: 6%;
|
--accent-dim: rgba(46, 184, 184, 0.12);
|
||||||
--board-hover-border-pct: 20%;
|
--accent-glow: rgba(46, 184, 184, 0.06);
|
||||||
|
--border-accent: rgba(46, 184, 184, 0.25);
|
||||||
--bg-primary: #040808;
|
--bg-primary: #040808;
|
||||||
--bg-board: rgba(6, 10, 10, 0.58);
|
--bg-board: rgba(6, 10, 10, 0.58);
|
||||||
--border: rgba(46, 184, 184, 0.10);
|
--border: rgba(46, 184, 184, 0.10);
|
||||||
@@ -187,17 +185,22 @@
|
|||||||
--font-body: 'Inter', sans-serif;
|
--font-body: 'Inter', sans-serif;
|
||||||
--overlay-bg: radial-gradient(circle at 30% 40%, rgba(4,8,8,0.2) 0%, rgba(4,8,8,0.92) 100%);
|
--overlay-bg: radial-gradient(circle at 30% 40%, rgba(4,8,8,0.2) 0%, rgba(4,8,8,0.92) 100%);
|
||||||
--header-bg: rgba(4,8,8,0.95);
|
--header-bg: rgba(4,8,8,0.95);
|
||||||
|
--board-hover-border: rgba(46, 184, 184, 0.20);
|
||||||
--toggle-on-bg: rgba(78,207,207,0.22);
|
--toggle-on-bg: rgba(78,207,207,0.22);
|
||||||
|
--logo-shadow: rgba(46, 184, 184, 0.45);
|
||||||
--bg-solid-fallback: #060a0a;
|
--bg-solid-fallback: #060a0a;
|
||||||
}
|
}
|
||||||
|
[data-theme="merchantman"] .board { border-color: rgba(46, 184, 184, 0.12); }
|
||||||
|
[data-theme="merchantman"] .bm-item:hover { background: rgba(46, 184, 184, 0.06); }
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
THEME: JULIA & JIN (Aetherial Night / FFXIV)
|
THEME: JULIA & JIN (Aetherial Night / FFXIV)
|
||||||
============================================ */
|
============================================ */
|
||||||
[data-theme="julia-jin"] {
|
[data-theme="julia-jin"] {
|
||||||
--accent: #7db3ff;
|
--accent: #7db3ff;
|
||||||
--border-accent-pct: 30%;
|
--accent-dim: rgba(125, 179, 255, 0.12);
|
||||||
--logo-shadow-pct: 50%;
|
--accent-glow: rgba(125, 179, 255, 0.08);
|
||||||
|
--border-accent: rgba(125, 179, 255, 0.30);
|
||||||
--bg-primary: #03050a;
|
--bg-primary: #03050a;
|
||||||
--bg-board: rgba(7, 10, 20, 0.60);
|
--bg-board: rgba(7, 10, 20, 0.60);
|
||||||
--border: rgba(125, 179, 255, 0.12);
|
--border: rgba(125, 179, 255, 0.12);
|
||||||
@@ -208,16 +211,25 @@
|
|||||||
--font-body: 'Inter', sans-serif;
|
--font-body: 'Inter', sans-serif;
|
||||||
--overlay-bg: linear-gradient(180deg, rgba(3,5,10,0.85) 0%, rgba(3,5,10,0.25) 50%, rgba(3,5,10,0.92) 100%);
|
--overlay-bg: linear-gradient(180deg, rgba(3,5,10,0.85) 0%, rgba(3,5,10,0.25) 50%, rgba(3,5,10,0.92) 100%);
|
||||||
--header-bg: rgba(3,5,10,0.94);
|
--header-bg: rgba(3,5,10,0.94);
|
||||||
|
--board-hover-border: rgba(125, 179, 255, 0.22);
|
||||||
--toggle-on-bg: rgba(91,159,255,0.22);
|
--toggle-on-bg: rgba(91,159,255,0.22);
|
||||||
|
--logo-shadow: rgba(125, 179, 255, 0.50);
|
||||||
--bg-solid-fallback: #070a14;
|
--bg-solid-fallback: #070a14;
|
||||||
}
|
}
|
||||||
|
[data-theme="julia-jin"] .logo { font-family: 'Cinzel', serif; letter-spacing: 5px; text-transform: uppercase; }
|
||||||
|
[data-theme="julia-jin"] .clock { font-family: 'Cinzel', serif; font-weight: 600; }
|
||||||
|
[data-theme="julia-jin"] .board-title { letter-spacing: 2px; font-weight: 500; }
|
||||||
|
[data-theme="julia-jin"] .board { border-color: rgba(125, 179, 255, 0.15); backdrop-filter: blur(4px); }
|
||||||
|
[data-theme="julia-jin"] .bm-item:hover { background: rgba(125, 179, 255, 0.08); }
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
THEME: SC Sunset - Horizon Glow
|
THEME: SC Sunset - Horizon Glow
|
||||||
============================================ */
|
============================================ */
|
||||||
[data-theme="sc-sunset"] {
|
[data-theme="sc-sunset"] {
|
||||||
--accent: #ff8c3d;
|
--accent: #ff8c3d;
|
||||||
--border-accent-pct: 28%;
|
--accent-dim: rgba(255, 140, 61, 0.12);
|
||||||
|
--accent-glow: rgba(255, 140, 61, 0.08);
|
||||||
|
--border-accent: rgba(255, 140, 61, 0.28);
|
||||||
--bg-primary: #080503;
|
--bg-primary: #080503;
|
||||||
--bg-board: rgba(15, 10, 8, 0.55);
|
--bg-board: rgba(15, 10, 8, 0.55);
|
||||||
--border: rgba(255, 140, 61, 0.10);
|
--border: rgba(255, 140, 61, 0.10);
|
||||||
@@ -228,19 +240,25 @@
|
|||||||
--font-body: 'Inter', sans-serif;
|
--font-body: 'Inter', sans-serif;
|
||||||
--overlay-bg: radial-gradient(circle at 50% 60%, rgba(8,5,3,0.1) 0%, rgba(8,5,3,0.92) 100%);
|
--overlay-bg: radial-gradient(circle at 50% 60%, rgba(8,5,3,0.1) 0%, rgba(8,5,3,0.92) 100%);
|
||||||
--header-bg: rgba(8,5,3,0.94);
|
--header-bg: rgba(8,5,3,0.94);
|
||||||
|
--board-hover-border: rgba(255, 140, 61, 0.22);
|
||||||
--toggle-on-bg: rgba(240,124,48,0.22);
|
--toggle-on-bg: rgba(240,124,48,0.22);
|
||||||
|
--logo-shadow: rgba(255, 140, 61, 0.45);
|
||||||
--bg-solid-fallback: #0f0a08;
|
--bg-solid-fallback: #0f0a08;
|
||||||
}
|
}
|
||||||
|
[data-theme="sc-sunset"] .board {
|
||||||
|
border-color: rgba(255, 140, 61, 0.15);
|
||||||
|
backdrop-filter: blur(6px);}
|
||||||
|
[data-theme="sc-sunset"] .bm-item:hover {
|
||||||
|
background: rgba(255, 140, 61, 0.08); }
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
THEME: HELLION HUD (circuit board / red+green)
|
THEME: HELLION HUD (circuit board / red+green)
|
||||||
============================================ */
|
============================================ */
|
||||||
[data-theme="hellion-hud"] {
|
[data-theme="hellion-hud"] {
|
||||||
--accent: #32ff6a;
|
--accent: #32ff6a;
|
||||||
--accent-dim-pct: 10%;
|
--accent-dim: rgba(50, 255, 106, 0.10);
|
||||||
--accent-glow-pct: 5%;
|
--accent-glow: rgba(50, 255, 106, 0.05);
|
||||||
--board-hover-border-pct: 20%;
|
--border-accent: rgba(50, 255, 106, 0.25);
|
||||||
--logo-shadow-pct: 40%;
|
|
||||||
--bg-primary: #030503;
|
--bg-primary: #030503;
|
||||||
--bg-board: rgba(5, 8, 5, 0.65);
|
--bg-board: rgba(5, 8, 5, 0.65);
|
||||||
--border: rgba(50, 255, 106, 0.12);
|
--border: rgba(50, 255, 106, 0.12);
|
||||||
@@ -251,19 +269,31 @@
|
|||||||
--font-body: 'Inter', sans-serif;
|
--font-body: 'Inter', sans-serif;
|
||||||
--overlay-bg: radial-gradient(circle at center, rgba(3,5,3,0.15) 0%, rgba(3,5,3,0.95) 100%);
|
--overlay-bg: radial-gradient(circle at center, rgba(3,5,3,0.15) 0%, rgba(3,5,3,0.95) 100%);
|
||||||
--header-bg: rgba(3,5,3,0.96);
|
--header-bg: rgba(3,5,3,0.96);
|
||||||
|
--board-hover-border: rgba(50, 255, 106, 0.20);
|
||||||
--toggle-on-bg: rgba(34,204,68,0.20);
|
--toggle-on-bg: rgba(34,204,68,0.20);
|
||||||
|
--logo-shadow: rgba(50, 255, 106, 0.40);
|
||||||
--bg-solid-fallback: #050805;
|
--bg-solid-fallback: #050805;
|
||||||
--danger: #ff4d4d;
|
--danger: #ff4d4d;
|
||||||
}
|
}
|
||||||
|
[data-theme="hellion-hud"] .board {
|
||||||
|
border-color: rgba(50, 255, 106, 0.15);
|
||||||
|
backdrop-filter: blur(8px) brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="hellion-hud"] .clock {
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
text-shadow: 0 0 10px var(--accent-glow);
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
THEME: HELLION ENERGY (matrix / tactical green)
|
THEME: HELLION ENERGY (matrix / tactical green)
|
||||||
============================================ */
|
============================================ */
|
||||||
[data-theme="hellion-energy"] {
|
[data-theme="hellion-energy"] {
|
||||||
--accent: #1eff8e;
|
--accent: #1eff8e;
|
||||||
--border-accent-pct: 30%;
|
--accent-dim: rgba(30, 255, 142, 0.12);
|
||||||
--board-hover-border-pct: 25%;
|
--accent-glow: rgba(30, 255, 142, 0.08);
|
||||||
--logo-shadow-pct: 60%;
|
--border-accent: rgba(30, 255, 142, 0.30);
|
||||||
--bg-primary: #020503;
|
--bg-primary: #020503;
|
||||||
--bg-board: rgba(4, 7, 5, 0.60);
|
--bg-board: rgba(4, 7, 5, 0.60);
|
||||||
--border: rgba(30, 255, 142, 0.12);
|
--border: rgba(30, 255, 142, 0.12);
|
||||||
@@ -274,18 +304,29 @@
|
|||||||
--font-body: 'Inter', sans-serif;
|
--font-body: 'Inter', sans-serif;
|
||||||
--overlay-bg: radial-gradient(circle at center, rgba(2,5,3,0.1) 0%, rgba(2,5,3,0.95) 100%);
|
--overlay-bg: radial-gradient(circle at center, rgba(2,5,3,0.1) 0%, rgba(2,5,3,0.95) 100%);
|
||||||
--header-bg: rgba(2,5,3,0.96);
|
--header-bg: rgba(2,5,3,0.96);
|
||||||
|
--board-hover-border: rgba(30, 255, 142, 0.25);
|
||||||
--toggle-on-bg: rgba(0,232,122,0.18);
|
--toggle-on-bg: rgba(0,232,122,0.18);
|
||||||
|
--logo-shadow: rgba(30, 255, 142, 0.60);
|
||||||
--bg-solid-fallback: #040705;
|
--bg-solid-fallback: #040705;
|
||||||
}
|
}
|
||||||
|
[data-theme="hellion-energy"] .board {
|
||||||
|
border-color: rgba(30, 255, 142, 0.15);
|
||||||
|
backdrop-filter: blur(10px) saturate(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="hellion-energy"] .bm-item:hover {
|
||||||
|
background: rgba(30, 255, 142, 0.10);
|
||||||
|
box-shadow: inset 0 0 10px rgba(30, 255, 142, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
THEME: SATISFACTORY (Industrial Desert)
|
THEME: SATISFACTORY (Industrial Desert)
|
||||||
============================================ */
|
============================================ */
|
||||||
[data-theme="satisfactory"] {
|
[data-theme="satisfactory"] {
|
||||||
--accent: #00b4d8;
|
--accent: #00b4d8;
|
||||||
--border-accent-pct: 35%;
|
--accent-dim: rgba(0, 180, 216, 0.12);
|
||||||
--board-hover-border-pct: 25%;
|
--accent-glow: rgba(0, 180, 216, 0.08);
|
||||||
--logo-shadow-pct: 40%;
|
--border-accent: rgba(0, 180, 216, 0.35);
|
||||||
--bg-primary: #1a0f08;
|
--bg-primary: #1a0f08;
|
||||||
--bg-board: rgba(26, 15, 8, 0.65);
|
--bg-board: rgba(26, 15, 8, 0.65);
|
||||||
--border: rgba(0, 180, 216, 0.15);
|
--border: rgba(0, 180, 216, 0.15);
|
||||||
@@ -299,17 +340,25 @@
|
|||||||
rgba(26,15,8,0.15) 50%,
|
rgba(26,15,8,0.15) 50%,
|
||||||
rgba(26,15,8,0.90) 100%);
|
rgba(26,15,8,0.90) 100%);
|
||||||
--header-bg: rgba(26,15,8,0.95);
|
--header-bg: rgba(26,15,8,0.95);
|
||||||
--toggle-on-bg: color-mix(in srgb, var(--accent) var(--toggle-on-bg-pct), transparent);
|
--board-hover-border: rgba(0, 180, 216, 0.25);
|
||||||
|
--toggle-on-bg: rgba(0, 180, 216, 0.20);
|
||||||
|
--logo-shadow: rgba(0, 180, 216, 0.40);
|
||||||
--bg-solid-fallback: #1a0f08;
|
--bg-solid-fallback: #1a0f08;
|
||||||
}
|
}
|
||||||
|
[data-theme="satisfactory"] .logo { font-family: 'Rajdhani', sans-serif; font-weight: 700; letter-spacing: 3px; text-transform: uppercase; }
|
||||||
|
[data-theme="satisfactory"] .clock { font-family: 'Rajdhani', sans-serif; font-weight: 600; color: var(--accent); }
|
||||||
|
[data-theme="satisfactory"] .board-title { font-family: 'Rajdhani', sans-serif; letter-spacing: 1.5px; text-transform: uppercase; }
|
||||||
|
[data-theme="satisfactory"] .board { border-color: rgba(0, 180, 216, 0.20); backdrop-filter: blur(12px); }
|
||||||
|
[data-theme="satisfactory"] .bm-item:hover { background: rgba(0, 180, 216, 0.10); }
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
THEME: AVORION (Deep Void)
|
THEME: AVORION (Deep Void)
|
||||||
============================================ */
|
============================================ */
|
||||||
[data-theme="avorion"] {
|
[data-theme="avorion"] {
|
||||||
--accent: #2ec4a0;
|
--accent: #2ec4a0;
|
||||||
--border-accent-pct: 30%;
|
--accent-dim: rgba(46, 196, 160, 0.12);
|
||||||
--logo-shadow-pct: 50%;
|
--accent-glow: rgba(46, 196, 160, 0.08);
|
||||||
|
--border-accent: rgba(46, 196, 160, 0.30);
|
||||||
--bg-primary: #020d0c;
|
--bg-primary: #020d0c;
|
||||||
--bg-board: rgba(2, 13, 12, 0.60);
|
--bg-board: rgba(2, 13, 12, 0.60);
|
||||||
--border: rgba(46, 196, 160, 0.12);
|
--border: rgba(46, 196, 160, 0.12);
|
||||||
@@ -322,18 +371,25 @@
|
|||||||
transparent 0%,
|
transparent 0%,
|
||||||
rgba(2, 13, 12, 0.95) 100%);
|
rgba(2, 13, 12, 0.95) 100%);
|
||||||
--header-bg: rgba(2, 13, 12, 0.94);
|
--header-bg: rgba(2, 13, 12, 0.94);
|
||||||
--toggle-on-bg-pct: 18%;
|
--board-hover-border: rgba(46, 196, 160, 0.22);
|
||||||
--toggle-on-bg: color-mix(in srgb, var(--accent) var(--toggle-on-bg-pct), transparent);
|
--toggle-on-bg: rgba(46, 196, 160, 0.18);
|
||||||
|
--logo-shadow: rgba(46, 196, 160, 0.50);
|
||||||
--bg-solid-fallback: #020d0c;
|
--bg-solid-fallback: #020d0c;
|
||||||
}
|
}
|
||||||
|
[data-theme="avorion"] .logo { font-family: 'Rajdhani', sans-serif; font-weight: 500; letter-spacing: 6px; text-transform: uppercase; }
|
||||||
|
[data-theme="avorion"] .clock { font-family: 'Rajdhani', sans-serif; font-weight: 400; color: var(--accent); }
|
||||||
|
[data-theme="avorion"] .board-title { font-family: 'Rajdhani', sans-serif; font-weight: 500; text-transform: uppercase; }
|
||||||
|
[data-theme="avorion"] .board { border-color: rgba(46, 196, 160, 0.15); backdrop-filter: blur(8px); }
|
||||||
|
[data-theme="avorion"] .bm-item:hover { background: rgba(46, 196, 160, 0.08); }
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
THEME: HELLION STEALTH (Tactical Recon)
|
THEME: HELLION STEALTH (Tactical Recon)
|
||||||
============================================ */
|
============================================ */
|
||||||
[data-theme="hellion-stealth"] {
|
[data-theme="hellion-stealth"] {
|
||||||
--accent: #5ec2ff;
|
--accent: #5ec2ff;
|
||||||
--border-accent-pct: 35%;
|
--accent-dim: rgba(94, 194, 255, 0.12);
|
||||||
--board-hover-border-pct: 25%;
|
--accent-glow: rgba(94, 194, 255, 0.08);
|
||||||
|
--border-accent: rgba(94, 194, 255, 0.35);
|
||||||
--bg-primary: #0d0f12;
|
--bg-primary: #0d0f12;
|
||||||
--bg-board: rgba(13, 15, 18, 0.70);
|
--bg-board: rgba(13, 15, 18, 0.70);
|
||||||
--border: rgba(94, 194, 255, 0.15);
|
--border: rgba(94, 194, 255, 0.15);
|
||||||
@@ -346,80 +402,21 @@
|
|||||||
transparent 0%,
|
transparent 0%,
|
||||||
rgba(13, 15, 18, 0.90) 100%);
|
rgba(13, 15, 18, 0.90) 100%);
|
||||||
--header-bg: rgba(13, 15, 18, 0.96);
|
--header-bg: rgba(13, 15, 18, 0.96);
|
||||||
--toggle-on-bg: color-mix(in srgb, var(--accent) var(--toggle-on-bg-pct), transparent);
|
--board-hover-border: rgba(94, 194, 255, 0.25);
|
||||||
|
--toggle-on-bg: rgba(94, 194, 255, 0.20);
|
||||||
|
--logo-shadow: rgba(94, 194, 255, 0.45);
|
||||||
--bg-solid-fallback: #0d0f12;
|
--bg-solid-fallback: #0d0f12;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
THEME-SCOPED KOMPONENTEN-REGELN (Sammelblock fuer @layer theme-overrides)
|
|
||||||
Aus den 11 [data-theme]-Bloecken extrahiert. Muss spaeter als components
|
|
||||||
greifen, sonst verlieren Board-Blur, Cinzel-Logos und Hover-Tints.
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
@layer theme-overrides {
|
|
||||||
[data-theme="nebula"] .board { border-color: rgba(214,92,255,0.10); }
|
|
||||||
[data-theme="nebula"] .bm-item:hover { background: rgba(214,92,255,0.05); }
|
|
||||||
[data-theme="crescent"] .logo { font-family: 'Cinzel', serif; letter-spacing: 4px; }
|
|
||||||
[data-theme="crescent"] .clock { font-family: 'Cinzel', serif; }
|
|
||||||
[data-theme="crescent"] .board-title { letter-spacing: 2px; }
|
|
||||||
[data-theme="crescent"] .bm-item:hover { background: rgba(200,168,74,0.05); }
|
|
||||||
[data-theme="crescent"] .board { border-color: rgba(200,168,74,0.10); }
|
|
||||||
[data-theme="event-horizon"] .board { border-color: rgba(157, 92, 255, 0.15); }
|
|
||||||
[data-theme="event-horizon"] .bm-item:hover { background: rgba(157, 92, 255, 0.08); }
|
|
||||||
[data-theme="merchantman"] .board { border-color: rgba(46, 184, 184, 0.12); }
|
|
||||||
[data-theme="merchantman"] .bm-item:hover { background: rgba(46, 184, 184, 0.06); }
|
|
||||||
[data-theme="julia-jin"] .logo { font-family: 'Cinzel', serif; letter-spacing: 5px; text-transform: uppercase; }
|
|
||||||
[data-theme="julia-jin"] .clock { font-family: 'Cinzel', serif; font-weight: 600; }
|
|
||||||
[data-theme="julia-jin"] .board-title { letter-spacing: 2px; font-weight: 500; }
|
|
||||||
[data-theme="julia-jin"] .board { border-color: rgba(125, 179, 255, 0.15); backdrop-filter: blur(4px); }
|
|
||||||
[data-theme="julia-jin"] .bm-item:hover { background: rgba(125, 179, 255, 0.08); }
|
|
||||||
[data-theme="sc-sunset"] .board {
|
|
||||||
border-color: rgba(255, 140, 61, 0.15);
|
|
||||||
backdrop-filter: blur(6px);}
|
|
||||||
[data-theme="sc-sunset"] .bm-item:hover {
|
|
||||||
background: rgba(255, 140, 61, 0.08); }
|
|
||||||
[data-theme="hellion-hud"] .board {
|
|
||||||
border-color: rgba(50, 255, 106, 0.15);
|
|
||||||
backdrop-filter: blur(8px) brightness(1.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="hellion-hud"] .clock {
|
|
||||||
font-family: 'Rajdhani', sans-serif;
|
|
||||||
font-weight: 700;
|
|
||||||
text-shadow: 0 0 10px var(--accent-glow);
|
|
||||||
}
|
|
||||||
[data-theme="hellion-energy"] .board {
|
|
||||||
border-color: rgba(30, 255, 142, 0.15);
|
|
||||||
backdrop-filter: blur(10px) saturate(1.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="hellion-energy"] .bm-item:hover {
|
|
||||||
background: rgba(30, 255, 142, 0.10);
|
|
||||||
box-shadow: inset 0 0 10px rgba(30, 255, 142, 0.05);
|
|
||||||
}
|
|
||||||
[data-theme="satisfactory"] .logo { font-family: 'Rajdhani', sans-serif; font-weight: 700; letter-spacing: 3px; text-transform: uppercase; }
|
|
||||||
[data-theme="satisfactory"] .clock { font-family: 'Rajdhani', sans-serif; font-weight: 600; color: var(--accent); }
|
|
||||||
[data-theme="satisfactory"] .board-title { font-family: 'Rajdhani', sans-serif; letter-spacing: 1.5px; text-transform: uppercase; }
|
|
||||||
[data-theme="satisfactory"] .board { border-color: rgba(0, 180, 216, 0.20); backdrop-filter: blur(12px); }
|
|
||||||
[data-theme="satisfactory"] .bm-item:hover { background: rgba(0, 180, 216, 0.10); }
|
|
||||||
[data-theme="avorion"] .logo { font-family: 'Rajdhani', sans-serif; font-weight: 500; letter-spacing: 6px; text-transform: uppercase; }
|
|
||||||
[data-theme="avorion"] .clock { font-family: 'Rajdhani', sans-serif; font-weight: 400; color: var(--accent); }
|
|
||||||
[data-theme="avorion"] .board-title { font-family: 'Rajdhani', sans-serif; font-weight: 500; text-transform: uppercase; }
|
|
||||||
[data-theme="avorion"] .board { border-color: rgba(46, 196, 160, 0.15); backdrop-filter: blur(8px); }
|
|
||||||
[data-theme="avorion"] .bm-item:hover { background: rgba(46, 196, 160, 0.08); }
|
|
||||||
[data-theme="hellion-stealth"] .logo { font-family: 'Rajdhani', sans-serif; font-weight: 700; letter-spacing: 4px; }
|
[data-theme="hellion-stealth"] .logo { font-family: 'Rajdhani', sans-serif; font-weight: 700; letter-spacing: 4px; }
|
||||||
[data-theme="hellion-stealth"] .clock { font-family: 'Rajdhani', sans-serif; font-weight: 600; color: var(--accent); }
|
[data-theme="hellion-stealth"] .clock { font-family: 'Rajdhani', sans-serif; font-weight: 600; color: var(--accent); }
|
||||||
[data-theme="hellion-stealth"] .board-title { text-transform: uppercase; font-size: 0.85rem; letter-spacing: 2px; }
|
[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"] .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="hellion-stealth"] .bm-item:hover { background: rgba(94, 194, 255, 0.10); border-left: 2px solid var(--accent); }
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
BASE STYLES
|
BASE STYLES
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
@layer base {
|
|
||||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
@@ -431,9 +428,7 @@ html, body {
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
transition: background 0.5s;
|
transition: background 0.5s;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@layer layout {
|
|
||||||
.bg-layer {
|
.bg-layer {
|
||||||
position: fixed; inset: 0; z-index: 0;
|
position: fixed; inset: 0; z-index: 0;
|
||||||
background-size: cover; background-position: center;
|
background-size: cover; background-position: center;
|
||||||
@@ -467,7 +462,7 @@ html, body {
|
|||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
font-size: clamp(0.875rem, 0.8125rem + 0.21vw, 1.0625rem); font-weight: 700; letter-spacing: 3px;
|
font-size: 17px; font-weight: 700; letter-spacing: 3px;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
text-shadow: 0 0 24px var(--logo-shadow);
|
text-shadow: 0 0 24px var(--logo-shadow);
|
||||||
transition: color 0.5s, text-shadow 0.5s, font-family 0.1s;
|
transition: color 0.5s, text-shadow 0.5s, font-family 0.1s;
|
||||||
@@ -480,7 +475,7 @@ html, body {
|
|||||||
|
|
||||||
.clock {
|
.clock {
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
font-size: clamp(1rem, 0.9167rem + 0.28vw, 1.25rem); font-weight: 500; letter-spacing: 2px;
|
font-size: 20px; font-weight: 500; letter-spacing: 2px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
transition: color 0.5s, font-family 0.1s;
|
transition: color 0.5s, font-family 0.1s;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@@ -513,29 +508,15 @@ html, body {
|
|||||||
/* BOARDS */
|
/* BOARDS */
|
||||||
.boards-wrapper {
|
.boards-wrapper {
|
||||||
position: relative; z-index: 10;
|
position: relative; z-index: 10;
|
||||||
/* Free-Layout: Boards liegen absolut (siehe @layer components .board).
|
|
||||||
Der Flex-Modus bleibt als Fallback fuer den Mobil-Reset erhalten
|
|
||||||
(dort wird .board wieder position:static, dann greift flex-wrap). */
|
|
||||||
display: flex; flex-wrap: wrap;
|
display: flex; flex-wrap: wrap;
|
||||||
align-content: flex-start;
|
align-content: flex-start;
|
||||||
justify-content: flex-start;
|
justify-content: center;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
padding: 110px 40px 40px;
|
padding: 110px 40px 40px;
|
||||||
/* Absolute Kinder beanspruchen keinen Platz -> Wrapper braucht eine
|
|
||||||
explizite Mindesthoehe, sonst kollabiert er auf die Padding-Hoehe. */
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@layer components {
|
|
||||||
.board {
|
.board {
|
||||||
/* Freies Layout: Position kommt aus --board-x/--board-y (von drag.js gesetzt),
|
|
||||||
NICHT inline, damit der ungeschichtete Mobil-@media-Block sie ueberschreiben
|
|
||||||
kann (Inline-Style wuerde gegen das Stylesheet gewinnen). Default 0/0 fuer
|
|
||||||
den Fall, dass die Migration noch nicht gelaufen ist. */
|
|
||||||
position: absolute;
|
|
||||||
left: var(--board-x, 0px);
|
|
||||||
top: var(--board-y, 0px);
|
|
||||||
width: var(--board-width);
|
width: var(--board-width);
|
||||||
background: var(--bg-board);
|
background: var(--bg-board);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
@@ -568,22 +549,13 @@ html, body {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
transition: color 0.15s;
|
transition: color 0.15s;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
/* Ueber dem Blur-Overlay (.board-blur-overlay, z-index:5) liegen, sonst schluckt das
|
|
||||||
Overlay den pointerdown und ein geblurrtes Board ist nicht verschiebbar. Der Rest des
|
|
||||||
Boards bleibt unter dem Overlay -> Klick dort enthuellt weiterhin (Phase-5-Review). */
|
|
||||||
position: relative;
|
|
||||||
z-index: 6;
|
|
||||||
}
|
}
|
||||||
.board-drag-handle:hover { color: var(--accent); }
|
.board-drag-handle:hover { color: var(--accent); }
|
||||||
.board-drag-handle:active { cursor: grabbing; }
|
.board-drag-handle:active { cursor: grabbing; }
|
||||||
/* Gesperrtes Board (Position fixiert): Drag-Handle ausblenden (kein Verschieben mehr),
|
|
||||||
Lock-Button aktiv einfaerben. Hoehere Spezifitaet als .board-drag-handle -> display:none gewinnt. */
|
|
||||||
.board.locked .board-drag-handle { display: none; }
|
|
||||||
.board.locked .btn-lock-board { color: var(--accent); }
|
|
||||||
|
|
||||||
.board-title {
|
.board-title {
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
font-size: clamp(0.6875rem, 0.6458rem + 0.14vw, 0.8125rem); font-weight: 600; letter-spacing: 1.5px;
|
font-size: 13px; font-weight: 600; letter-spacing: 1.5px;
|
||||||
color: var(--accent); text-transform: uppercase;
|
color: var(--accent); text-transform: uppercase;
|
||||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||||
max-width: 160px;
|
max-width: 160px;
|
||||||
@@ -714,43 +686,6 @@ body.show-desc .bm-desc { display: block; }
|
|||||||
border-radius: 4px; color: var(--text-muted); white-space: nowrap;
|
border-radius: 4px; color: var(--text-muted); white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- PAPIERKORB (Trash, v2.3) ---- */
|
|
||||||
.trash-list { padding: 4px 0; }
|
|
||||||
.trash-empty {
|
|
||||||
padding: 12px 18px; font-size: 12px; color: var(--text-muted);
|
|
||||||
}
|
|
||||||
/* Vertikale Karte pro Eintrag: Info ueber volle Breite, Aktionen rechtsbuendig darunter.
|
|
||||||
Verhindert das Zusammenquetschen im schmalen 380px-Settings-Panel. */
|
|
||||||
.trash-item {
|
|
||||||
display: flex; flex-direction: column; gap: 8px;
|
|
||||||
padding: 10px 18px;
|
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.04);
|
|
||||||
}
|
|
||||||
.trash-item:last-child { border-bottom: none; }
|
|
||||||
.trash-item-info { min-width: 0; }
|
|
||||||
.trash-item-title {
|
|
||||||
display: block; font-size: 13px; color: var(--text-primary);
|
|
||||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
||||||
}
|
|
||||||
.trash-item-meta {
|
|
||||||
display: block; font-size: 11px; color: var(--text-muted); margin-top: 3px;
|
|
||||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
||||||
}
|
|
||||||
.trash-item-badge {
|
|
||||||
display: inline-block; vertical-align: middle;
|
|
||||||
font-size: 9px; padding: 2px 6px; border-radius: 4px;
|
|
||||||
background: rgba(255,255,255,0.06); border: 1px solid var(--border);
|
|
||||||
color: var(--text-muted); white-space: nowrap; margin-right: 6px;
|
|
||||||
text-transform: uppercase; letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
.trash-item-actions { display: flex; gap: 6px; justify-content: flex-end; }
|
|
||||||
/* Per-Eintrag-Buttons kompakt: das volle Uppercase-btn-danger ist pro Eintrag zu wuchtig. */
|
|
||||||
.trash-item-actions .btn-danger {
|
|
||||||
padding: 5px 10px; font-size: 11px; letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
.trash-actions { justify-content: flex-end; }
|
|
||||||
.trash-actions.hidden { display: none; }
|
|
||||||
|
|
||||||
/* THEME PICKER */
|
/* THEME PICKER */
|
||||||
.theme-grid {
|
.theme-grid {
|
||||||
display: grid; grid-template-columns: 1fr 1fr 1fr 1fr;
|
display: grid; grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||||
@@ -902,13 +837,17 @@ body.show-desc .bm-desc { display: block; }
|
|||||||
.modal-body { padding: 14px 16px; }
|
.modal-body { padding: 14px 16px; }
|
||||||
.modal-footer { padding: 10px 16px 14px; display: flex; justify-content: flex-end; }
|
.modal-footer { padding: 10px 16px 14px; display: flex; justify-content: flex-end; }
|
||||||
|
|
||||||
/* Free-Move: das gezogene Board nach vorne heben (frueher Reorder-Opacity). */
|
.board.dragging { opacity: 0.35; }
|
||||||
.board.dragging { z-index: 50; cursor: grabbing; }
|
|
||||||
|
.board-placeholder {
|
||||||
|
border: 2px dashed var(--border-accent);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--accent-dim);
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ---- BOARD BLUR (Private Mode) ---- */
|
/* ---- BOARD BLUR (Private Mode) ---- */
|
||||||
@layer utilities {
|
|
||||||
.board.blurred .board-list,
|
.board.blurred .board-list,
|
||||||
.board.blurred .show-more-btn,
|
.board.blurred .show-more-btn,
|
||||||
.board.blurred .add-bm-btn {
|
.board.blurred .add-bm-btn {
|
||||||
@@ -919,10 +858,9 @@ body.show-desc .bm-desc { display: block; }
|
|||||||
.board.blurred .board-title {
|
.board.blurred .board-title {
|
||||||
filter: blur(5px);
|
filter: blur(5px);
|
||||||
}
|
}
|
||||||
/* HINWEIS: KEIN `.board.blurred { position: relative }` — .board ist im Free-Layout bereits
|
.board.blurred {
|
||||||
position:absolute (= positionierter Containing-Block fuers Overlay). Ein relative hier liegt
|
position: relative;
|
||||||
im @layer utilities und wuerde das absolute schlagen -> geblurrtes Board faellt aus seiner
|
}
|
||||||
freien Position + waere nicht mehr drag-bar (Phase-5-Review). */
|
|
||||||
.board-blur-overlay {
|
.board-blur-overlay {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute; inset: 0; z-index: 5;
|
position: absolute; inset: 0; z-index: 5;
|
||||||
@@ -952,10 +890,8 @@ body.show-desc .bm-desc { display: block; }
|
|||||||
}
|
}
|
||||||
.btn-blur-board:hover { background: var(--accent-dim); color: var(--accent); }
|
.btn-blur-board:hover { background: var(--accent-dim); color: var(--accent); }
|
||||||
.board.blurred .btn-blur-board { color: var(--accent); opacity: 0.7; }
|
.board.blurred .btn-blur-board { color: var(--accent); opacity: 0.7; }
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@layer components {
|
|
||||||
/* ---- ABOUT BLOCK ---- */
|
/* ---- ABOUT BLOCK ---- */
|
||||||
.about-block {
|
.about-block {
|
||||||
padding: 4px 18px 14px;
|
padding: 4px 18px 14px;
|
||||||
@@ -2407,113 +2343,23 @@ body.show-desc .bm-desc { display: block; }
|
|||||||
.settings-section.open .section-content {
|
.settings-section.open .section-content {
|
||||||
max-height: 800px;
|
max-height: 800px;
|
||||||
}
|
}
|
||||||
/* ---- COMMAND-PALETTE (Strg+K) ---- */
|
|
||||||
/* .palette-overlay erbt .dialog-overlay (Position/Backdrop/Fade/z-index:400). */
|
|
||||||
.palette-overlay {
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
.palette-box {
|
|
||||||
margin-top: 12vh;
|
|
||||||
width: 560px; max-width: 92vw;
|
|
||||||
background: rgba(10,9,20,0.98);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
backdrop-filter: blur(28px);
|
|
||||||
box-shadow: 0 20px 60px rgba(0,0,0,0.7);
|
|
||||||
transform: translateY(8px) scale(0.98);
|
|
||||||
transition: transform 0.2s;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.dialog-overlay.active .palette-box { transform: translateY(0) scale(1); }
|
|
||||||
.palette-input {
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 16px 18px;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-family: var(--font-body);
|
|
||||||
font-size: 15px;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
.palette-input::placeholder { color: var(--text-muted); }
|
|
||||||
.palette-list {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0; padding: 6px;
|
|
||||||
max-height: 46vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
.palette-option {
|
|
||||||
display: flex; flex-direction: column; gap: 2px;
|
|
||||||
padding: 9px 12px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.palette-option[aria-selected="true"] {
|
|
||||||
background: var(--accent-dim);
|
|
||||||
}
|
|
||||||
.palette-option-title {
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: 13px; font-weight: 500;
|
|
||||||
}
|
|
||||||
.palette-option-meta {
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 10px; letter-spacing: 0.3px;
|
|
||||||
}
|
|
||||||
.palette-empty {
|
|
||||||
list-style: none;
|
|
||||||
padding: 16px 12px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 13px; text-align: center;
|
|
||||||
}
|
|
||||||
.palette-live {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px; height: 1px;
|
|
||||||
overflow: hidden; clip: rect(0 0 0 0);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.palette-hint {
|
|
||||||
padding: 8px 14px;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 10px; letter-spacing: 0.3px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
UTILITY CLASSES
|
UTILITY CLASSES
|
||||||
============================================ */
|
============================================ */
|
||||||
@layer utilities {
|
|
||||||
.hidden { display: none; }
|
.hidden { display: none; }
|
||||||
.accent-text { color: var(--accent); }
|
.accent-text { color: var(--accent); }
|
||||||
.dim { opacity: 0.4; }
|
.dim { opacity: 0.4; }
|
||||||
|
.drag-ghost {
|
||||||
|
position: fixed; 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);
|
||||||
|
}
|
||||||
.bm-item.drag-over { background: rgba(255,160,50,0.07); }
|
.bm-item.drag-over { background: rgba(255,160,50,0.07); }
|
||||||
.bm-item.dragging-source { opacity: 0.4; }
|
.bm-item.dragging-source { opacity: 0.4; }
|
||||||
.about-info-label-block { display: block; margin-bottom: 6px; }
|
.about-info-label-block { display: block; margin-bottom: 6px; }
|
||||||
.about-link-subtle { color: var(--text-secondary); text-decoration: none; }
|
.about-link-subtle { color: var(--text-secondary); text-decoration: none; }
|
||||||
.modal-input-spaced { margin-top: 8px; }
|
.modal-input-spaced { margin-top: 8px; }
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
A11Y FOCUS RING (v2.2)
|
|
||||||
============================================ */
|
|
||||||
@layer utilities {
|
|
||||||
/* A11y: sichtbarer Fokus-Ring, getoent im Theme-Akzent.
|
|
||||||
Liegt bewusst im spaeten utilities-Layer, damit er die 9 outline:none-Regeln
|
|
||||||
aus dem components-Layer ueber die Kaskaden-Ordnung schlaegt (Spezifitaet
|
|
||||||
zwischen Layern irrelevant). Nur :focus-visible, damit Maus-Klicks keinen Ring zeigen. */
|
|
||||||
:focus-visible {
|
|
||||||
outline: 2px solid color-mix(in srgb, var(--accent) 70%, white 30%);
|
|
||||||
outline-offset: 2px;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
/* Bookmarks/Cards: Ring leicht enger, da sie eigene Radien haben */
|
|
||||||
.bm-item:focus-visible,
|
|
||||||
.theme-card:focus-visible {
|
|
||||||
outline-offset: 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
RESPONSIVE — Mobile & Tablet
|
RESPONSIVE — Mobile & Tablet
|
||||||
@@ -2533,13 +2379,7 @@ body.show-desc .bm-desc { display: block; }
|
|||||||
}
|
}
|
||||||
.btn-icon { padding: 6px 8px; gap: 0; }
|
.btn-icon { padding: 6px 8px; gap: 0; }
|
||||||
|
|
||||||
.boards-wrapper { padding: 100px 16px 24px; gap: 10px; justify-content: center; }
|
.boards-wrapper { padding: 100px 16px 24px; gap: 10px; }
|
||||||
/* Free-Layout-Reset: ab Tablet wieder gestapeltes Flex-Layout. Ungeschichtet ->
|
|
||||||
gewinnt ueber @layer components (.board position:absolute). pos/--board-x/y werden ignoriert. */
|
|
||||||
.board {
|
|
||||||
position: static;
|
|
||||||
left: auto; top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-panel { width: 320px; }
|
.settings-panel { width: 320px; }
|
||||||
.theme-grid { grid-template-columns: 1fr 1fr; }
|
.theme-grid { grid-template-columns: 1fr 1fr; }
|
||||||
@@ -2570,11 +2410,7 @@ body.show-desc .bm-desc { display: block; }
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.board {
|
.board { width: 100%; }
|
||||||
width: 100%;
|
|
||||||
position: static;
|
|
||||||
left: auto; top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-panel { width: 100%; }
|
.settings-panel { width: 100%; }
|
||||||
.theme-grid { grid-template-columns: 1fr 1fr; gap: 6px; }
|
.theme-grid { grid-template-columns: 1fr 1fr; gap: 6px; }
|
||||||
@@ -2594,22 +2430,3 @@ body.show-desc .bm-desc { display: block; }
|
|||||||
|
|
||||||
.modal { width: calc(100vw - 32px); }
|
.modal { width: calc(100vw - 32px); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =============================================
|
|
||||||
prefers-reduced-motion — UNGESCHICHTET (kein @layer).
|
|
||||||
Ungeschichtete Regeln gewinnen ueber alle Layer.
|
|
||||||
Kappt alle Transitions/Animationen inkl. der
|
|
||||||
View-Transition-Pseudo-Elemente (der *-Selektor
|
|
||||||
trifft sie nicht zuverlaessig, daher explizit).
|
|
||||||
============================================= */
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
*, *::before, *::after {
|
|
||||||
transition: none !important;
|
|
||||||
animation: none !important;
|
|
||||||
}
|
|
||||||
::view-transition-group(*),
|
|
||||||
::view-transition-old(*),
|
|
||||||
::view-transition-new(*) {
|
|
||||||
animation: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+1
-106
@@ -6,18 +6,8 @@
|
|||||||
async function init() {
|
async function init() {
|
||||||
const savedBoards = await Store.get('boards');
|
const savedBoards = await Store.get('boards');
|
||||||
const savedSettings = await Store.get('settings');
|
const savedSettings = await Store.get('settings');
|
||||||
const savedTrash = await Store.get('trash');
|
|
||||||
|
|
||||||
boards = savedBoards ?? getDefaultBoards();
|
boards = savedBoards ?? getDefaultBoards();
|
||||||
trash = Array.isArray(savedTrash) ? savedTrash : [];
|
|
||||||
|
|
||||||
// Auto-Cleanup: Eintraege aelter als 30 Tage verwerfen (TRASH-02). Muss VOR
|
|
||||||
// renderBoards() laufen, damit der Papierkorb-Stand konsistent ist. Schreibt nur
|
|
||||||
// zurueck, wenn wirklich etwas entfernt wurde (kein unnoetiger Storage-Write).
|
|
||||||
const cutoff = Date.now() - TRASH_RETENTION_MS;
|
|
||||||
const beforeCount = trash.length;
|
|
||||||
trash = trash.filter(entry => entry && typeof entry.deletedAt === 'number' && Number.isFinite(entry.deletedAt) && entry.deletedAt >= cutoff);
|
|
||||||
if (trash.length !== beforeCount) await saveTrash();
|
|
||||||
if (savedSettings) Object.assign(settings, savedSettings);
|
if (savedSettings) Object.assign(settings, savedSettings);
|
||||||
|
|
||||||
I18n.init();
|
I18n.init();
|
||||||
@@ -26,11 +16,7 @@ async function init() {
|
|||||||
startClock();
|
startClock();
|
||||||
bindGlobalEvents();
|
bindGlobalEvents();
|
||||||
bindSettingsEvents();
|
bindSettingsEvents();
|
||||||
bindStorageSync();
|
|
||||||
bindBoardResizeReclamp(); // Boards bei Fenster-Verkleinerung wieder in den sichtbaren Bereich holen
|
|
||||||
await drainQuickSavePending(); // beim Start angesammelte Quick-Saves (kein Tab war offen) einlesen
|
|
||||||
initSearch();
|
initSearch();
|
||||||
initPalette();
|
|
||||||
await migrateSticky();
|
await migrateSticky();
|
||||||
await Notes.init();
|
await Notes.init();
|
||||||
await Calculator.init();
|
await Calculator.init();
|
||||||
@@ -119,7 +105,7 @@ async function checkBackupReminder() {
|
|||||||
const notesData = (widgetData && Array.isArray(widgetData.notes)) ? widgetData.notes : [];
|
const notesData = (widgetData && Array.isArray(widgetData.notes)) ? widgetData.notes : [];
|
||||||
const calcHistory = (widgetData && widgetData.calculator) ? widgetData.calculator.history || [] : [];
|
const calcHistory = (widgetData && widgetData.calculator) ? widgetData.calculator.history || [] : [];
|
||||||
const timerPresets = (widgetData && widgetData.timer) ? widgetData.timer.presets || [] : [];
|
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.1.0', exported: new Date().toISOString(), boards, settings, notes: notesData, calculator: calcHistory, timerPresets };
|
||||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
@@ -234,95 +220,4 @@ function bindGlobalEvents() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- QUICK-SAVE PENDING-QUEUE ----
|
|
||||||
// Der Background-Worker haengt Quick-Saves an den eigenen Store-Key 'quicksave_pending' an (er
|
|
||||||
// schreibt NIE boards). Diese Seite ist die einzige boards-Schreiberin und drained die Queue in die
|
|
||||||
// Inbox. Getrennte Schreib-Domaenen -> Worker und Seite koennen sich nicht im boards-Array
|
|
||||||
// gegenseitig ueberschreiben (Datensicherheit, Phase-4-Review-Blocker 2b).
|
|
||||||
let _drainBusy = false;
|
|
||||||
let _drainQueued = false; // ein waehrend eines laufenden Drains angefragter Drain wird nachgeholt
|
|
||||||
let _renderDeferredByDrag = false; // Drain hat den Render wegen eines laufenden Drags ausgelassen -> nach Drag-Ende nachholen
|
|
||||||
async function drainQuickSavePending() {
|
|
||||||
if (_drainBusy) { _drainQueued = true; return; }
|
|
||||||
_drainBusy = true;
|
|
||||||
try {
|
|
||||||
const pending = await Store.get('quicksave_pending');
|
|
||||||
if (Array.isArray(pending) && pending.length > 0) {
|
|
||||||
const drained = pending.slice();
|
|
||||||
const drainedIds = new Set(drained.map(e => e && e.id).filter(Boolean));
|
|
||||||
const inbox = await ensureInboxBoard(); // legt die Inbox an, falls noetig; gibt das Board zurueck
|
|
||||||
// Idempotenz gegen den Worker/Drain-Race auf 'quicksave_pending': jede eingespielte Inbox-
|
|
||||||
// Bookmark traegt die Pending-id ihres Ursprungs als srcId. Taucht ein bereits gedrainter
|
|
||||||
// Eintrag durch einen gleichzeitigen Worker-Append erneut in der Queue auf, wird er hier
|
|
||||||
// uebersprungen statt doppelt eingefuegt — kein Duplikat, und kein Verlust (boards-Write
|
|
||||||
// bleibt vor der Queue-Bereinigung, daher keine umgekehrte Verlustgefahr).
|
|
||||||
const seenSrc = new Set(inbox.bookmarks.map(b => b && b.srcId).filter(Boolean));
|
|
||||||
for (const e of drained) {
|
|
||||||
if (!e || !e.id || seenSrc.has(e.id)) continue; // schon eingespielt
|
|
||||||
if (typeof e.url !== 'string' || !e.url || !isSafeUrl(e.url)) continue; // leeres/unsicheres Protokoll verwerfen
|
|
||||||
const bm = normalizeBookmark({ title: e.title, url: e.url });
|
|
||||||
bm.srcId = e.id; // Herkunft fuer kuenftige Dedup
|
|
||||||
inbox.bookmarks.push(bm);
|
|
||||||
seenSrc.add(e.id);
|
|
||||||
}
|
|
||||||
await saveBoards();
|
|
||||||
// NUR die verarbeiteten Eintraege entfernen — ein gleichzeitiger Worker-Append bleibt erhalten.
|
|
||||||
const still = await Store.get('quicksave_pending');
|
|
||||||
const remaining = Array.isArray(still) ? still.filter(e => e && !drainedIds.has(e.id)) : [];
|
|
||||||
await Store.set('quicksave_pending', remaining);
|
|
||||||
// Render nur, wenn gerade KEIN Drag laeuft (renderBoards->replaceChildren wuerde ihn abreissen).
|
|
||||||
// Laeuft einer, den Render-Wunsch merken und nach Drag-Ende nachholen (drag.js ruft
|
|
||||||
// flushQuickSaveRenderIfDeferred), sonst bliebe der frisch gedrainte Quick-Save bis zu einem
|
|
||||||
// unabhaengigen Fremd-Render unsichtbar (Phase-6-Review).
|
|
||||||
if (document.querySelector('.board.dragging, .bm-item.dragging-source')) {
|
|
||||||
_renderDeferredByDrag = true;
|
|
||||||
} else {
|
|
||||||
renderBoards();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Quick-Save-Drain fehlgeschlagen:', e && e.message);
|
|
||||||
} finally {
|
|
||||||
_drainBusy = false;
|
|
||||||
}
|
|
||||||
// Kam waehrend des Drains ein weiterer Quick-Save an (onChanged wurde durch _drainBusy verworfen),
|
|
||||||
// jetzt nachholen. Der Eintrag war sicher in der Queue, nur noch nicht eingelesen.
|
|
||||||
if (_drainQueued) { _drainQueued = false; drainQuickSavePending(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wird von drag.js nach jedem Drag-Ende aufgerufen: einen waehrend des Drags ausgelassenen
|
|
||||||
// Quick-Save-Render nachholen. Idempotent — tut nichts, wenn kein Render aussteht.
|
|
||||||
function flushQuickSaveRenderIfDeferred() {
|
|
||||||
if (_renderDeferredByDrag) {
|
|
||||||
_renderDeferredByDrag = false;
|
|
||||||
renderBoards();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Live-Sync (QS-03): ein offener NewTab drained die Queue, sobald der Worker etwas anhaengt.
|
|
||||||
function bindStorageSync() {
|
|
||||||
if (typeof chrome === 'undefined' || !chrome.storage || !chrome.storage.onChanged) return;
|
|
||||||
chrome.storage.onChanged.addListener((changes, area) => {
|
|
||||||
// Nur auf die Quick-Save-Queue reagieren — 'boards' schreibt ausschliesslich diese Seite.
|
|
||||||
if (area !== 'local' || !changes.quicksave_pending) return;
|
|
||||||
drainQuickSavePending();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Freies Layout (LAYOUT-04): Boards stehen absolut positioniert. Schrumpft das Fenster, koennen
|
|
||||||
// sie ganz aus dem sichtbaren Bereich rutschen. renderBoards() klemmt die --board-x/--board-y jedes
|
|
||||||
// Boards beim Aufbau gegen die aktuelle Viewport — ein simpler Re-Render holt sie also zurueck.
|
|
||||||
// Debounce (150ms), damit kontinuierliches Resizen nicht hunderte Renders ausloest. Waehrend eines
|
|
||||||
// aktiven Drags NICHT neu rendern: renderBoards->replaceChildren wuerde den laufenden Drag abreissen.
|
|
||||||
function bindBoardResizeReclamp() {
|
|
||||||
let resizeTimer = null;
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
clearTimeout(resizeTimer);
|
|
||||||
resizeTimer = setTimeout(() => {
|
|
||||||
if (document.querySelector('.board.dragging, .bm-item.dragging-source')) return;
|
|
||||||
renderBoards();
|
|
||||||
}, 150);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
/* =============================================
|
|
||||||
HELLION NEWTAB — background.js
|
|
||||||
Quick-Save Background fuer Chrome (Service-Worker)
|
|
||||||
UND Firefox (Event-Page). Kein DOM/window. Listener
|
|
||||||
synchron auf Top-Level. Geteilte Logik via importScripts.
|
|
||||||
============================================= */
|
|
||||||
|
|
||||||
// Geteiltes DOM-freies Helfer-Modul aus Phase 1: ensureInbox(boards), uid(), normalizeBookmark(...).
|
|
||||||
// Chrome-Service-Worker laedt es via importScripts. Firefox-Event-Page hat KEIN importScripts —
|
|
||||||
// dort kommt das Modul ueber background.scripts (manifest.firefox.json) in den Scope, ensureInbox
|
|
||||||
// ist dann schon definiert. Der Guard verhindert den ReferenceError in der Event-Page.
|
|
||||||
if (typeof importScripts === 'function' && typeof ensureInbox === 'undefined') {
|
|
||||||
importScripts('quicksave-core.js');
|
|
||||||
}
|
|
||||||
|
|
||||||
// chrome.storage.local-Lese-/Schreib-Helfer als Promises (kein Store-Modul im Worker,
|
|
||||||
// das ist DOM/Seiten-gebunden). Identisches Verhalten: get -> Wert oder null.
|
|
||||||
function bgGet(key) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
chrome.storage.local.get([key], r => resolve(r[key] ?? null));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function bgSet(key, value) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
chrome.storage.local.set({ [key]: value }, () => {
|
|
||||||
if (chrome.runtime.lastError) {
|
|
||||||
reject(new Error(chrome.runtime.lastError.message));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kurze Badge-Bestaetigung, dann automatisch wieder leeren. color optional (Default gruen).
|
|
||||||
function flashBadge(text, color) {
|
|
||||||
chrome.action.setBadgeText({ text });
|
|
||||||
// Hintergrundfarbe optional, ohne extra Permission moeglich.
|
|
||||||
if (chrome.action.setBadgeBackgroundColor) {
|
|
||||||
chrome.action.setBadgeBackgroundColor({ color: color || '#1f9d55' });
|
|
||||||
}
|
|
||||||
setTimeout(() => chrome.action.setBadgeText({ text: '' }), 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interne/nicht speicherbare Seiten (Browser-UI, Extension-Seiten) — kein sinnvolles Bookmark.
|
|
||||||
const UNSAVEABLE_URL = /^(chrome|chrome-extension|about|edge|opera|moz-extension|brave|vivaldi|view-source|devtools):/i;
|
|
||||||
|
|
||||||
// Quick-Save: aktiven Tab in die Pending-Queue haengen — NICHT boards schreiben.
|
|
||||||
// Datensicherheit (Phase-4-Review 2b): boards schreibt ausschliesslich die NewTab-Seite. Der Worker
|
|
||||||
// haengt nur an 'quicksave_pending' an; die Seite drained die Queue in die Inbox. So koennen Worker
|
|
||||||
// und Seite sich nicht im boards-Array gegenseitig ueberschreiben (kein Lost-Update bestehender Daten).
|
|
||||||
async function quickSaveActiveTab() {
|
|
||||||
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
||||||
const tab = tabs && tabs[0];
|
|
||||||
if (!tab || !tab.url || UNSAVEABLE_URL.test(tab.url)) {
|
|
||||||
// Kein speicherbarer Tab: kurzer roter Marker (langer Text wird im Badge abgeschnitten).
|
|
||||||
flashBadge('×', '#c0392b');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// read-modify-write nur auf der EIGENEN Queue (bgGet/bgSet sind via quickSaveChain serialisiert).
|
|
||||||
const pending = (await bgGet('quicksave_pending')) ?? [];
|
|
||||||
pending.push({ id: uid(), title: tab.title || tab.url, url: tab.url });
|
|
||||||
await bgSet('quicksave_pending', pending);
|
|
||||||
flashBadge(chrome.i18n.getMessage('quickSaveBadge'));
|
|
||||||
} catch (e) {
|
|
||||||
// Quota o.ae.: Badge zeigt nichts Gruenes, Fehler in die Worker-Konsole.
|
|
||||||
console.error('Quick-Save fehlgeschlagen:', e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quick-Saves serialisieren: zwei schnelle Tastendruecke (Key-Repeat) wuerden sonst parallel
|
|
||||||
// read-modify-write machen und sich gegenseitig ueberschreiben (lost update). Promise-Kette
|
|
||||||
// sorgt fuer sequentielle Ausfuehrung. Listener bleibt SYNCHRON auf Top-Level registriert.
|
|
||||||
let quickSaveChain = Promise.resolve();
|
|
||||||
chrome.commands.onCommand.addListener(command => {
|
|
||||||
if (command === 'quick-save') {
|
|
||||||
quickSaveChain = quickSaveChain.then(() => quickSaveActiveTab()).catch(e => console.error('Quick-Save:', e && e.message));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
+15
-134
@@ -46,34 +46,6 @@ function createPlusSvg() {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Erzeugt das Pin-/Reisszwecke-Icon SVG (Position fixieren) — bewusst KEIN Emoji (custom SVG). */
|
|
||||||
function createPinSvg() {
|
|
||||||
return svgEl('svg', { width: '11', height: '12', viewBox: '0 0 24 24', fill: 'currentColor' }, [
|
|
||||||
svgEl('path', { d: 'M16 9V4l1 0c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1 .45-1 1s.45 1 1 1l1 0v5c0 1.66-1.34 3-3 3v2h5.97v7l1 1 1-1v-7H19v-2c-1.66 0-3-1.34-3-3z' }),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- POS-MIGRATION ----
|
|
||||||
// Boards ohne pos (Altbestand vor v2.3) aus einem Auto-Raster befuellen,
|
|
||||||
// damit sie sich nicht alle auf (0,0) stapeln. Raster orientiert sich am
|
|
||||||
// Wrapper-Padding (110px oben / 40px links) und der Board-Breite.
|
|
||||||
function ensureBoardPositions() {
|
|
||||||
const COL_W = 240 + 14; // --board-width (Desktop) + gap
|
|
||||||
const ROW_H = 220; // grober Board-Hoehen-Schaetzwert fuers Auto-Raster
|
|
||||||
const startX = 40, startY = 110;
|
|
||||||
const cols = Math.max(1, Math.floor((window.innerWidth - startX * 2 + 14) / COL_W));
|
|
||||||
|
|
||||||
let migrated = false;
|
|
||||||
boards.forEach((board, i) => {
|
|
||||||
if (board.pos && typeof board.pos.x === 'number' && typeof board.pos.y === 'number') return;
|
|
||||||
const col = i % cols;
|
|
||||||
const row = Math.floor(i / cols);
|
|
||||||
board.pos = { x: startX + col * COL_W, y: startY + row * ROW_H };
|
|
||||||
migrated = true;
|
|
||||||
});
|
|
||||||
return migrated;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- RENDER ----
|
// ---- RENDER ----
|
||||||
function renderBoards() {
|
function renderBoards() {
|
||||||
const wrapper = document.getElementById('boardsWrapper');
|
const wrapper = document.getElementById('boardsWrapper');
|
||||||
@@ -98,29 +70,13 @@ function renderBoards() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Altbestand ohne pos migrieren (Auto-Raster), danach einmalig speichern.
|
boards.forEach(board => wrapper.appendChild(createBoardEl(board)));
|
||||||
const migrated = ensureBoardPositions();
|
|
||||||
|
|
||||||
boards.forEach(board => {
|
|
||||||
const el = createBoardEl(board);
|
|
||||||
wrapper.appendChild(el);
|
|
||||||
// Position als Custom-Property setzen (nicht inline left/top), damit der Mobil-@media-Reset
|
|
||||||
// sie ueberschreiben kann. Gegen den AKTUELLEN Viewport clampen, damit ein auf breiterem
|
|
||||||
// Fenster platziertes Board nie off-screen (und damit per Drag unerreichbar) rendert.
|
|
||||||
// board.pos bleibt unveraendert -> bei spaeterer Verbreiterung wird die Originalposition wieder erreicht.
|
|
||||||
const cx = Math.max(0, Math.min(window.innerWidth - el.offsetWidth, board.pos.x));
|
|
||||||
const cy = Math.max(48, Math.min(window.innerHeight - el.offsetHeight, board.pos.y));
|
|
||||||
el.style.setProperty('--board-x', cx + 'px');
|
|
||||||
el.style.setProperty('--board-y', cy + 'px');
|
|
||||||
});
|
|
||||||
initBoardDragDrop();
|
initBoardDragDrop();
|
||||||
|
|
||||||
if (migrated) saveBoards();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createBoardEl(board) {
|
function createBoardEl(board) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'board' + (board.blurred ? ' blurred' : '') + (board.locked ? ' locked' : '');
|
div.className = 'board' + (board.blurred ? ' blurred' : '');
|
||||||
div.dataset.boardId = board.id;
|
div.dataset.boardId = board.id;
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
@@ -140,11 +96,6 @@ function createBoardEl(board) {
|
|||||||
const actions = document.createElement('div');
|
const actions = document.createElement('div');
|
||||||
actions.className = 'board-actions';
|
actions.className = 'board-actions';
|
||||||
|
|
||||||
const btnLock = document.createElement('button');
|
|
||||||
btnLock.className = 'board-action-btn btn-lock-board';
|
|
||||||
btnLock.title = board.locked ? t('boards.unlock') : t('boards.lock');
|
|
||||||
btnLock.appendChild(createPinSvg());
|
|
||||||
|
|
||||||
const btnBlur = document.createElement('button');
|
const btnBlur = document.createElement('button');
|
||||||
btnBlur.className = 'board-action-btn btn-blur-board';
|
btnBlur.className = 'board-action-btn btn-blur-board';
|
||||||
btnBlur.title = board.blurred ? t('boards.unblur') : t('boards.blur');
|
btnBlur.title = board.blurred ? t('boards.unblur') : t('boards.blur');
|
||||||
@@ -155,19 +106,12 @@ function createBoardEl(board) {
|
|||||||
btnRename.title = t('boards.rename');
|
btnRename.title = t('boards.rename');
|
||||||
btnRename.textContent = '\u270E';
|
btnRename.textContent = '\u270E';
|
||||||
|
|
||||||
// Das feste Inbox-Board (Quick-Save-Ziel) darf nicht geloescht werden \u2014 kein Delete-Button.
|
const btnDelete = document.createElement('button');
|
||||||
const btnDelete = board.id === 'inbox' ? null : document.createElement('button');
|
btnDelete.className = 'board-action-btn btn-delete-board';
|
||||||
if (btnDelete) {
|
btnDelete.title = t('boards.delete');
|
||||||
btnDelete.className = 'board-action-btn btn-delete-board';
|
btnDelete.textContent = '\u2715';
|
||||||
btnDelete.title = t('boards.delete');
|
|
||||||
btnDelete.textContent = '\u2715';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (btnDelete) {
|
actions.append(btnBlur, btnRename, btnDelete);
|
||||||
actions.append(btnLock, btnBlur, btnRename, btnDelete);
|
|
||||||
} else {
|
|
||||||
actions.append(btnLock, btnBlur, btnRename);
|
|
||||||
}
|
|
||||||
header.append(dragHandle, titleSpanHeader, actions);
|
header.append(dragHandle, titleSpanHeader, actions);
|
||||||
|
|
||||||
// Blur-Overlay
|
// Blur-Overlay
|
||||||
@@ -175,16 +119,6 @@ function createBoardEl(board) {
|
|||||||
blurOverlay.className = 'board-blur-overlay';
|
blurOverlay.className = 'board-blur-overlay';
|
||||||
div.appendChild(blurOverlay);
|
div.appendChild(blurOverlay);
|
||||||
|
|
||||||
btnLock.addEventListener('click', async e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
// Position fixieren: blendet via .board.locked den Drag-Handle aus (CSS) und der onDown-Guard
|
|
||||||
// in drag.js verweigert zusaetzlich den Drag. Reiner Klassen-Toggle, kein Re-Render noetig.
|
|
||||||
board.locked = !board.locked;
|
|
||||||
div.classList.toggle('locked', board.locked);
|
|
||||||
btnLock.title = board.locked ? t('boards.unlock') : t('boards.lock');
|
|
||||||
await saveBoards();
|
|
||||||
});
|
|
||||||
|
|
||||||
btnBlur.addEventListener('click', async e => {
|
btnBlur.addEventListener('click', async e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
board.blurred = !board.blurred;
|
board.blurred = !board.blurred;
|
||||||
@@ -210,29 +144,15 @@ function createBoardEl(board) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (btnDelete) btnDelete.addEventListener('click', async e => {
|
btnDelete.addEventListener('click', async e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const ok = await HellionDialog.confirm(
|
const ok = await HellionDialog.confirm(
|
||||||
t('boards.delete_confirm', { title: board.title }),
|
t('boards.delete_confirm', { title: board.title }),
|
||||||
{ type: 'danger', title: t('boards.delete_confirm.title'), confirmText: t('boards.delete') }
|
{ type: 'danger', title: t('boards.delete_confirm.title'), confirmText: t('boards.delete') }
|
||||||
);
|
);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
// Ganzes board-Objekt (inkl. bookmarks UND blurred-Flag, CR-01) in den Papierkorb.
|
boards = boards.filter(b => b.id !== board.id);
|
||||||
// type:'board', kein originBoardId (Board hat keine Herkunft, Restore legt es direkt in boards[]).
|
await saveBoards();
|
||||||
// Datensicherheit: ZUERST Trash sichern (saveTrash), DANN Loeschung committen (saveBoards) —
|
|
||||||
// bei Quota-Reject bleibt das Board in boards[], kein Datenverlust.
|
|
||||||
const trashEntry = pushToTrash({ item: board, type: 'board', originBoardId: null });
|
|
||||||
try {
|
|
||||||
await saveTrash();
|
|
||||||
boards = boards.filter(b => b.id !== board.id);
|
|
||||||
await saveBoards();
|
|
||||||
} catch (err) {
|
|
||||||
// Save fehlgeschlagen (z.B. Quota genau zwischen den Writes): auf den Vor-Loesch-Stand
|
|
||||||
// zurueckrollen, damit In-Memory und Storage konsistent bleiben (kein Reload-Duplikat).
|
|
||||||
trash = trash.filter(t => t !== trashEntry);
|
|
||||||
if (!boards.some(b => b.id === board.id)) boards.push(board);
|
|
||||||
console.error('Board-Loeschen fehlgeschlagen, zurueckgerollt:', err && err.message);
|
|
||||||
}
|
|
||||||
renderBoards();
|
renderBoards();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -294,9 +214,6 @@ function createBmEl(bm) {
|
|||||||
li.dataset.bmId = bm.id;
|
li.dataset.bmId = bm.id;
|
||||||
li.dataset.bmUrl = bm.url;
|
li.dataset.bmUrl = bm.url;
|
||||||
li.draggable = true;
|
li.draggable = true;
|
||||||
li.setAttribute('role', 'link');
|
|
||||||
li.setAttribute('tabindex', '0');
|
|
||||||
li.setAttribute('aria-label', bm.title);
|
|
||||||
|
|
||||||
const favicon = document.createElement('div');
|
const favicon = document.createElement('div');
|
||||||
favicon.className = 'bm-favicon-local';
|
favicon.className = 'bm-favicon-local';
|
||||||
@@ -334,28 +251,12 @@ function bindBoardListEvents(list, board) {
|
|||||||
const bmItem = e.target.closest('.bm-item');
|
const bmItem = e.target.closest('.bm-item');
|
||||||
if (!bmItem) return;
|
if (!bmItem) return;
|
||||||
|
|
||||||
// Delete-Button geklickt: kein Confirm (wie bisher), aber nicht mehr hart loeschen —
|
// Delete-Button geklickt
|
||||||
// das Bookmark wandert in den Papierkorb (30 Tage, TRASH-01). Erst per find() greifen,
|
|
||||||
// dann mit Herkunft (originBoardId), type und Zeitstempel ins trash[] pushen.
|
|
||||||
if (e.target.closest('.bm-delete')) {
|
if (e.target.closest('.bm-delete')) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const bmId = bmItem.dataset.bmId;
|
const bmId = bmItem.dataset.bmId;
|
||||||
const removed = board.bookmarks.find(b => b.id === bmId);
|
board.bookmarks = board.bookmarks.filter(b => b.id !== bmId);
|
||||||
if (removed) {
|
await saveBoards();
|
||||||
// Datensicherheit: ZUERST den Trash-Klon persistieren, DANN die Loeschung committen.
|
|
||||||
// Falls saveTrash() (Quota) rejectet, ist das Original noch in boards[] -> kein Verlust.
|
|
||||||
const trashEntry = pushToTrash({ item: removed, type: 'bookmark', originBoardId: board.id });
|
|
||||||
try {
|
|
||||||
await saveTrash();
|
|
||||||
board.bookmarks = board.bookmarks.filter(b => b.id !== bmId);
|
|
||||||
await saveBoards();
|
|
||||||
} catch (err) {
|
|
||||||
// Save fehlgeschlagen: auf den Vor-Loesch-Stand zurueckrollen (kein Reload-Duplikat).
|
|
||||||
trash = trash.filter(t => t !== trashEntry);
|
|
||||||
if (!board.bookmarks.some(b => b.id === bmId)) board.bookmarks.push(removed);
|
|
||||||
console.error('Bookmark-Loeschen fehlgeschlagen, zurueckgerollt:', err && err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
renderBoards();
|
renderBoards();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -366,31 +267,11 @@ function bindBoardListEvents(list, board) {
|
|||||||
window.open(url, settings.newTab ? '_blank' : '_self', 'noopener,noreferrer');
|
window.open(url, settings.newTab ? '_blank' : '_self', 'noopener,noreferrer');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Tastatur: Enter oeffnet den Bookmark wie ein Klick. role="link" erwartet
|
|
||||||
// nur Enter (Space ist Button-Semantik). Der Delete-Button bleibt ein echter
|
|
||||||
// <button> und feuert seinen eigenen Klick ueber Space/Enter selbst.
|
|
||||||
list.addEventListener('keydown', e => {
|
|
||||||
if (e.key !== 'Enter') return;
|
|
||||||
const bmItem = e.target.closest('.bm-item');
|
|
||||||
if (!bmItem || e.target !== bmItem) return; // nur wenn der li selbst fokussiert ist
|
|
||||||
e.preventDefault();
|
|
||||||
const url = bmItem.dataset.bmUrl;
|
|
||||||
if (url) {
|
|
||||||
window.open(url, settings.newTab ? '_blank' : '_self', 'noopener,noreferrer');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- MODALS ----
|
// ---- MODALS ----
|
||||||
// reduced-motion kappt das Fade ueber den ungeschichteten @media-Block.
|
function openModal(id) { document.getElementById(id).classList.add('active'); }
|
||||||
// Feature-Detection-Fallback (Firefox < 144): instant.
|
function closeModal(id) { document.getElementById(id).classList.remove('active'); }
|
||||||
function openModal(id) {
|
|
||||||
withViewTransition(() => document.getElementById(id).classList.add('active'));
|
|
||||||
}
|
|
||||||
function closeModal(id) {
|
|
||||||
withViewTransition(() => document.getElementById(id).classList.remove('active'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function openAddBoardModal() {
|
function openAddBoardModal() {
|
||||||
document.getElementById('newBoardName').value = '';
|
document.getElementById('newBoardName').value = '';
|
||||||
|
|||||||
@@ -196,18 +196,16 @@ const BrowserBookmarkImport = {
|
|||||||
overlay.appendChild(modal);
|
overlay.appendChild(modal);
|
||||||
document.body.appendChild(overlay);
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
// View-Transition-Fade
|
// Animation
|
||||||
withViewTransition(() => overlay.classList.add('active'));
|
requestAnimationFrame(() => overlay.classList.add('active'));
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Schliesst das Modal */
|
/** Schliesst das Modal */
|
||||||
_closeModal() {
|
_closeModal() {
|
||||||
const overlay = document.getElementById('bmImportOverlay');
|
const overlay = document.getElementById('bmImportOverlay');
|
||||||
if (!overlay) return;
|
if (!overlay) return;
|
||||||
withViewTransition(() => {
|
overlay.classList.remove('active');
|
||||||
overlay.classList.remove('active');
|
setTimeout(() => overlay.remove(), 250);
|
||||||
overlay.remove();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+14
-78
@@ -24,26 +24,14 @@ function initDataButtons() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Validiert eine freie Layout-Position (LAYOUT-04). Liefert { x, y } nur bei
|
|
||||||
* endlichen Zahlen, sonst null — dann gridded ensureBoardPositions das Board neu.
|
|
||||||
* Ohne das wuerde ein Import jede vom Nutzer gesetzte Board-Position verwerfen.
|
|
||||||
* @param {*} pos
|
|
||||||
* @returns {{x:number,y:number}|null}
|
|
||||||
*/
|
|
||||||
function safePos(pos) {
|
|
||||||
return pos && Number.isFinite(pos.x) && Number.isFinite(pos.y) ? { x: pos.x, y: pos.y } : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export (inkl. Notes)
|
// Export (inkl. Notes)
|
||||||
btnExport.addEventListener('click', async () => {
|
btnExport.addEventListener('click', async () => {
|
||||||
const widgetData = await Store.get('widgetStates');
|
const widgetData = await Store.get('widgetStates');
|
||||||
const data = {
|
const data = {
|
||||||
version: '2.3.0',
|
version: '2.1.0',
|
||||||
exported: new Date().toISOString(),
|
exported: new Date().toISOString(),
|
||||||
boards,
|
boards,
|
||||||
settings,
|
settings,
|
||||||
trash,
|
|
||||||
notes: widgetData && Array.isArray(widgetData.notes) ? widgetData.notes : [],
|
notes: widgetData && Array.isArray(widgetData.notes) ? widgetData.notes : [],
|
||||||
calculator: widgetData && widgetData.calculator ? widgetData.calculator.history || [] : [],
|
calculator: widgetData && widgetData.calculator ? widgetData.calculator.history || [] : [],
|
||||||
timerPresets: widgetData && widgetData.timer ? widgetData.timer.presets || [] : []
|
timerPresets: widgetData && widgetData.timer ? widgetData.timer.presets || [] : []
|
||||||
@@ -67,26 +55,19 @@ function initDataButtons() {
|
|||||||
if (!Array.isArray(data.boards)) throw new Error(t('data.invalid_format'));
|
if (!Array.isArray(data.boards)) throw new Error(t('data.invalid_format'));
|
||||||
const validBoards = data.boards
|
const validBoards = data.boards
|
||||||
.filter(b => b && typeof b.title === 'string' && Array.isArray(b.bookmarks))
|
.filter(b => b && typeof b.title === 'string' && Array.isArray(b.bookmarks))
|
||||||
.map(b => {
|
.map(b => ({
|
||||||
const board = {
|
id: b.id || uid(),
|
||||||
id: b.id || uid(),
|
title: String(b.title).slice(0, 100),
|
||||||
title: String(b.title).slice(0, 100),
|
blurred: !!b.blurred,
|
||||||
blurred: !!b.blurred,
|
bookmarks: b.bookmarks
|
||||||
locked: !!b.locked,
|
.filter(bm => bm && typeof bm.title === 'string' && isSafeUrl(bm.url))
|
||||||
bookmarks: b.bookmarks
|
.map(bm => ({
|
||||||
.filter(bm => bm && typeof bm.title === 'string' && isSafeUrl(bm.url))
|
id: bm.id || uid(),
|
||||||
.map(bm => ({
|
title: String(bm.title).slice(0, 200),
|
||||||
id: bm.id || uid(),
|
url: bm.url,
|
||||||
title: String(bm.title).slice(0, 200),
|
desc: String(bm.desc || '').slice(0, 500)
|
||||||
url: bm.url,
|
}))
|
||||||
desc: String(bm.desc || '').slice(0, 500)
|
}));
|
||||||
}))
|
|
||||||
};
|
|
||||||
// Freies Layout (LAYOUT-04): valide Position uebernehmen, sonst gridded ensureBoardPositions neu.
|
|
||||||
const pos = safePos(b.pos);
|
|
||||||
if (pos) board.pos = pos;
|
|
||||||
return board;
|
|
||||||
});
|
|
||||||
if (validBoards.length === 0) throw new Error(t('data.no_boards'));
|
if (validBoards.length === 0) throw new Error(t('data.no_boards'));
|
||||||
const ok = await HellionDialog.confirm(
|
const ok = await HellionDialog.confirm(
|
||||||
t('data.import_confirm', { count: validBoards.length }),
|
t('data.import_confirm', { count: validBoards.length }),
|
||||||
@@ -97,51 +78,6 @@ function initDataButtons() {
|
|||||||
await saveBoards();
|
await saveBoards();
|
||||||
renderBoards();
|
renderBoards();
|
||||||
|
|
||||||
// Papierkorb importieren (falls vorhanden) — defensiv validiert.
|
|
||||||
if (Array.isArray(data.trash) && data.trash.length > 0) {
|
|
||||||
const validTrash = data.trash
|
|
||||||
.filter(e => e && e.item && ['bookmark', 'board'].includes(e.type) && typeof e.deletedAt === 'number' && Number.isFinite(e.deletedAt))
|
|
||||||
.map(e => ({
|
|
||||||
type: e.type,
|
|
||||||
originBoardId: typeof e.originBoardId === 'string' ? e.originBoardId : null,
|
|
||||||
deletedAt: e.deletedAt,
|
|
||||||
item: e.type === 'board'
|
|
||||||
? {
|
|
||||||
id: e.item.id || uid(),
|
|
||||||
title: String(e.item.title || '').slice(0, 100),
|
|
||||||
blurred: !!e.item.blurred,
|
|
||||||
locked: !!e.item.locked,
|
|
||||||
// Position erhalten, damit ein wiederhergestelltes Board an seinem Platz landet.
|
|
||||||
...(safePos(e.item.pos) ? { pos: safePos(e.item.pos) } : {}),
|
|
||||||
bookmarks: Array.isArray(e.item.bookmarks)
|
|
||||||
? e.item.bookmarks
|
|
||||||
.filter(bm => bm && typeof bm.title === 'string' && isSafeUrl(bm.url))
|
|
||||||
.map(bm => ({ id: bm.id || uid(), title: String(bm.title).slice(0, 200), url: bm.url, desc: String(bm.desc || '').slice(0, 500) }))
|
|
||||||
.slice(0, 500)
|
|
||||||
: []
|
|
||||||
}
|
|
||||||
: (isSafeUrl(e.item.url)
|
|
||||||
? { id: e.item.id || uid(), title: String(e.item.title || '').slice(0, 200), url: e.item.url, desc: String(e.item.desc || '').slice(0, 500) }
|
|
||||||
: null)
|
|
||||||
}))
|
|
||||||
.filter(e => e.item !== null);
|
|
||||||
if (validTrash.length > 0) {
|
|
||||||
// Lokale Eintraege sind die EINZIGE Kopie ihrer geloeschten Daten -> Vorrang. Importierte
|
|
||||||
// stammen aus einem Backup, das der Nutzer noch besitzt -> nachrangig. Daher: erst ALLE
|
|
||||||
// lokalen behalten (pushToTrash kappt sie bereits auf TRASH_MAX_ENTRIES), dann mit den
|
|
||||||
// NEUESTEN importierten bis zur Obergrenze auffuellen. Ein frischer Import verdraengt so
|
|
||||||
// keine aelteren lokalen Sole-Copies mehr (frueheres sort+slice(-N) konnte das, data-loss).
|
|
||||||
const room = Math.max(0, TRASH_MAX_ENTRIES - trash.length);
|
|
||||||
const keptImports = validTrash
|
|
||||||
.slice()
|
|
||||||
.sort((a, b) => b.deletedAt - a.deletedAt) // neueste Importe zuerst
|
|
||||||
.slice(0, room);
|
|
||||||
// Am Ende nach deletedAt aufsteigend fuer eine stabile Anzeige-Reihenfolge.
|
|
||||||
trash = [...trash, ...keptImports].sort((a, b) => a.deletedAt - b.deletedAt);
|
|
||||||
await saveTrash();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notes importieren (falls vorhanden)
|
// Notes importieren (falls vorhanden)
|
||||||
let notesImported = 0;
|
let notesImported = 0;
|
||||||
const existingWidgets = await Store.get('widgetStates') || {};
|
const existingWidgets = await Store.get('widgetStates') || {};
|
||||||
|
|||||||
+4
-30
@@ -40,34 +40,23 @@ const HellionDialog = {
|
|||||||
*/
|
*/
|
||||||
_show(config) {
|
_show(config) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const prevFocus = document.activeElement;
|
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
overlay.className = 'dialog-overlay';
|
overlay.className = 'dialog-overlay';
|
||||||
|
|
||||||
const box = document.createElement('div');
|
const box = document.createElement('div');
|
||||||
box.className = 'dialog-box';
|
box.className = 'dialog-box';
|
||||||
box.setAttribute('role', config.isConfirm ? 'alertdialog' : 'dialog');
|
|
||||||
box.setAttribute('aria-modal', 'true');
|
|
||||||
// Eindeutige IDs pro Dialog-Instanz: kurz gestapelte Dialoge (timer.js/
|
|
||||||
// image-ref.js feuern teils ohne await) duerfen sich keine festen IDs
|
|
||||||
// teilen, sonst liest der Screenreader ueber aria-* den falschen Titel.
|
|
||||||
const uid = 'dlg-' + Date.now().toString(36) + '-' + (HellionDialog._seq = (HellionDialog._seq || 0) + 1);
|
|
||||||
box.setAttribute('aria-labelledby', uid + '-title');
|
|
||||||
box.setAttribute('aria-describedby', uid + '-body');
|
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
const header = document.createElement('div');
|
const header = document.createElement('div');
|
||||||
header.className = 'dialog-header';
|
header.className = 'dialog-header';
|
||||||
header.appendChild(this._createIcon(config.type));
|
header.appendChild(this._createIcon(config.type));
|
||||||
const titleSpan = document.createElement('span');
|
const titleSpan = document.createElement('span');
|
||||||
titleSpan.id = uid + '-title';
|
|
||||||
titleSpan.textContent = config.title;
|
titleSpan.textContent = config.title;
|
||||||
header.appendChild(titleSpan);
|
header.appendChild(titleSpan);
|
||||||
|
|
||||||
// Body
|
// Body
|
||||||
const body = document.createElement('div');
|
const body = document.createElement('div');
|
||||||
body.className = 'dialog-body';
|
body.className = 'dialog-body';
|
||||||
body.id = uid + '-body';
|
|
||||||
body.textContent = config.message;
|
body.textContent = config.message;
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
@@ -75,12 +64,9 @@ const HellionDialog = {
|
|||||||
actions.className = 'dialog-actions';
|
actions.className = 'dialog-actions';
|
||||||
|
|
||||||
function cleanup(result) {
|
function cleanup(result) {
|
||||||
|
overlay.classList.remove('active');
|
||||||
document.removeEventListener('keydown', keyHandler);
|
document.removeEventListener('keydown', keyHandler);
|
||||||
withViewTransition(() => {
|
setTimeout(() => overlay.remove(), 200);
|
||||||
overlay.classList.remove('active');
|
|
||||||
overlay.remove();
|
|
||||||
});
|
|
||||||
if (prevFocus && typeof prevFocus.focus === 'function') prevFocus.focus();
|
|
||||||
resolve(result);
|
resolve(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,24 +104,12 @@ const HellionDialog = {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
cleanup(config.isConfirm ? false : undefined);
|
cleanup(config.isConfirm ? false : undefined);
|
||||||
}
|
}
|
||||||
if (e.key === 'Tab') {
|
|
||||||
// Fokus-Falle: nur die Buttons im actions-Container sind fokussierbar
|
|
||||||
const items = Array.from(actions.querySelectorAll('button'));
|
|
||||||
if (items.length === 0) return;
|
|
||||||
const first = items[0];
|
|
||||||
const last = items[items.length - 1];
|
|
||||||
if (e.shiftKey && document.activeElement === first) {
|
|
||||||
e.preventDefault(); last.focus();
|
|
||||||
} else if (!e.shiftKey && document.activeElement === last) {
|
|
||||||
e.preventDefault(); first.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
document.addEventListener('keydown', keyHandler);
|
document.addEventListener('keydown', keyHandler);
|
||||||
|
|
||||||
document.body.appendChild(overlay);
|
document.body.appendChild(overlay);
|
||||||
// View-Transition uebernimmt das Fade; Fokus bleibt erhalten
|
// Nächster Frame für CSS-Transition
|
||||||
withViewTransition(() => {
|
requestAnimationFrame(() => {
|
||||||
overlay.classList.add('active');
|
overlay.classList.add('active');
|
||||||
confirmBtn.focus();
|
confirmBtn.focus();
|
||||||
});
|
});
|
||||||
|
|||||||
+73
-72
@@ -5,92 +5,95 @@
|
|||||||
Bookmarks: Reihenfolge innerhalb eines Boards
|
Bookmarks: Reihenfolge innerhalb eines Boards
|
||||||
============================================= */
|
============================================= */
|
||||||
|
|
||||||
// ---- BOARD FREE-MOVE (Pointer Events) ----
|
// ---- BOARD DRAG (Pointer Events) ----
|
||||||
// Neugebaut fuer v2.3 (frueher Reorder mit Ghost/Placeholder). Vorbild:
|
|
||||||
// widgets.js _initDrag — setPointerCapture, offX/offY, onMove mit Clamping
|
|
||||||
// gegen window.innerWidth/Height, onUp schreibt board.pos + saveBoards().
|
|
||||||
// Gebunden am .board-drag-handle, NICHT am ganzen .board, damit Bookmark-Drag,
|
|
||||||
// Klick-Delegation und Action-Buttons frei bleiben.
|
|
||||||
function initBoardDragDrop() {
|
function initBoardDragDrop() {
|
||||||
const wrapper = document.getElementById('boardsWrapper');
|
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 => {
|
wrapper.querySelectorAll('.board').forEach(boardEl => {
|
||||||
const handle = boardEl.querySelector('.board-drag-handle');
|
const handle = boardEl.querySelector('.board-drag-handle');
|
||||||
if (!handle) return;
|
if (!handle) return;
|
||||||
|
|
||||||
handle.addEventListener('pointerdown', function onDown(e) {
|
handle.style.cursor = 'grab';
|
||||||
// Auf Mobil ist .board position:static (Stapel) -> kein Free-Move.
|
|
||||||
if (getComputedStyle(boardEl).position !== 'absolute') return;
|
handle.addEventListener('pointerdown', e => {
|
||||||
// Gesperrtes Board (Position fixiert, LAYOUT-LOCK) nicht verschieben. Der Drag-Handle ist
|
|
||||||
// bei .locked schon per CSS ausgeblendet; dieser Guard ist die zweite Sicherung.
|
|
||||||
if (boardEl.classList.contains('locked')) return;
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handle.setPointerCapture(e.pointerId);
|
handle.setPointerCapture(e.pointerId);
|
||||||
// .board.dragging hebt das Board per CSS nach vorne (z-index) UND ist das Signal fuer den
|
handle.style.cursor = 'grabbing';
|
||||||
// Live-Sync-Guard in app.js (bindStorageSync verwirft ein onChanged-Re-Render, das diesen
|
|
||||||
// Drag sonst abreissen wuerde). Der Guard prueft genau diese Klasse (Phase-4-Review 2a).
|
|
||||||
boardEl.classList.add('dragging');
|
|
||||||
|
|
||||||
const rect = boardEl.getBoundingClientRect();
|
const rect = boardEl.getBoundingClientRect();
|
||||||
const offX = e.clientX - rect.left;
|
|
||||||
const offY = e.clientY - rect.top;
|
|
||||||
const startCX = e.clientX, startCY = e.clientY;
|
|
||||||
// Erst eine echte Bewegung (> 3px) zaehlt als Drag. Ein reiner Klick/Tap auf den Handle darf
|
|
||||||
// board.pos NICHT ueberschreiben: renderBoards() schreibt in --board-x/y den gegen die Viewport
|
|
||||||
// GECLAMPTEN Wert, board.pos bleibt absichtlich der wahre (evtl. off-screen) Wert. onUp liest
|
|
||||||
// --board-x/y zurueck — bei einem No-Move-Klick waere das der Clamp und wuerde die wahre
|
|
||||||
// Position zerstoeren (Phase-5-Review, HIGH/data-loss).
|
|
||||||
let moved = false;
|
|
||||||
|
|
||||||
function onMove(ev) {
|
// Ghost
|
||||||
if (Math.abs(ev.clientX - startCX) > 3 || Math.abs(ev.clientY - startCY) > 3) moved = true;
|
const ghost = boardEl.cloneNode(true);
|
||||||
const maxX = window.innerWidth - boardEl.offsetWidth;
|
ghost.className += ' drag-ghost';
|
||||||
const maxY = window.innerHeight - boardEl.offsetHeight;
|
ghost.style.left = rect.left + 'px';
|
||||||
const x = Math.max(0, Math.min(maxX, ev.clientX - offX));
|
ghost.style.top = rect.top + 'px';
|
||||||
const y = Math.max(48, Math.min(maxY, ev.clientY - offY)); // 48px = Header-Hoehe
|
ghost.style.width = rect.width + 'px';
|
||||||
boardEl.style.setProperty('--board-x', x + 'px');
|
ghost.style.height = rect.height + 'px';
|
||||||
boardEl.style.setProperty('--board-y', y + 'px');
|
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);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Gemeinsames Aufraeumen: Pointer-Capture freigeben, ALLE Listener entfernen,
|
handle.addEventListener('pointerup', async () => {
|
||||||
// .board.dragging entfernen. MUSS auch im Cancel-Pfad laufen — sonst klebt die Klasse
|
if (!dragging || dragging.el !== boardEl) return;
|
||||||
// und der app.js-Sync-Guard unterdrueckt dauerhaft Quick-Save-Renders (Phase-5-Review).
|
handle.style.cursor = 'grab';
|
||||||
function cleanup() {
|
placeholder.parentNode.insertBefore(boardEl, placeholder);
|
||||||
try { handle.releasePointerCapture(e.pointerId); } catch (_) { /* schon freigegeben */ }
|
placeholder.remove(); placeholder = null;
|
||||||
handle.removeEventListener('pointermove', onMove);
|
boardEl.classList.remove('dragging');
|
||||||
handle.removeEventListener('pointerup', onUp);
|
dragging.ghost.remove();
|
||||||
handle.removeEventListener('pointercancel', onCancel);
|
dragging = null;
|
||||||
boardEl.classList.remove('dragging');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onUp() {
|
// Neue Reihenfolge aus DOM ablesen
|
||||||
cleanup();
|
const newOrder = Array.from(wrapper.querySelectorAll('.board'))
|
||||||
// Nur bei echtem Verschieben persistieren — sonst board.pos unangetastet lassen.
|
.map(el => el.dataset.boardId).filter(Boolean);
|
||||||
if (moved) {
|
boards.sort((a, b) => newOrder.indexOf(a.id) - newOrder.indexOf(b.id));
|
||||||
const id = boardEl.dataset.boardId;
|
await saveBoards();
|
||||||
const board = boards.find(b => b.id === id);
|
});
|
||||||
if (board) {
|
|
||||||
board.pos = {
|
|
||||||
x: parseFloat(boardEl.style.getPropertyValue('--board-x')),
|
|
||||||
y: parseFloat(boardEl.style.getPropertyValue('--board-y'))
|
|
||||||
};
|
|
||||||
await saveBoards();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Einen waehrend des Drags ausgelassenen Quick-Save-Render nachholen (app.js).
|
|
||||||
if (typeof flushQuickSaveRenderIfDeferred === 'function') flushQuickSaveRenderIfDeferred();
|
|
||||||
}
|
|
||||||
|
|
||||||
// pointercancel feuert STATT pointerup bei Touch-Interrupt, Browser-Geste oder wenn das
|
handle.addEventListener('pointercancel', () => {
|
||||||
// captured Element aus dem DOM faellt -> nur aufraeumen (cleanup), pos NICHT ueberschreiben.
|
if (!dragging) return;
|
||||||
function onCancel() {
|
dragging.ghost.remove();
|
||||||
cleanup();
|
if (placeholder) { placeholder.remove(); placeholder = null; }
|
||||||
if (typeof flushQuickSaveRenderIfDeferred === 'function') flushQuickSaveRenderIfDeferred();
|
boardEl.classList.remove('dragging');
|
||||||
}
|
dragging = null;
|
||||||
|
handle.style.cursor = 'grab';
|
||||||
handle.addEventListener('pointermove', onMove);
|
|
||||||
handle.addEventListener('pointerup', onUp);
|
|
||||||
handle.addEventListener('pointercancel', onCancel);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -110,8 +113,6 @@ function initBookmarkDragDrop(listEl, board) {
|
|||||||
listEl.addEventListener('dragend', e => {
|
listEl.addEventListener('dragend', e => {
|
||||||
const item = e.target.closest('.bm-item');
|
const item = e.target.closest('.bm-item');
|
||||||
if (item) item.classList.remove('dragging-source');
|
if (item) item.classList.remove('dragging-source');
|
||||||
// Blieb ein Quick-Save-Render waehrend des Bookmark-Drags aus (Drop ausgeblieben), jetzt nachholen.
|
|
||||||
if (typeof flushQuickSaveRenderIfDeferred === 'function') flushQuickSaveRenderIfDeferred();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
listEl.addEventListener('dragover', e => {
|
listEl.addEventListener('dragover', e => {
|
||||||
|
|||||||
+2
-104
@@ -21,8 +21,6 @@ const STRINGS = {
|
|||||||
'boards.drag_title': 'Board verschieben',
|
'boards.drag_title': 'Board verschieben',
|
||||||
'boards.blur': 'Blur (privat)',
|
'boards.blur': 'Blur (privat)',
|
||||||
'boards.unblur': 'Unblur',
|
'boards.unblur': 'Unblur',
|
||||||
'boards.lock': 'Position sperren',
|
|
||||||
'boards.unlock': 'Position entsperren',
|
|
||||||
'boards.rename': 'Umbenennen',
|
'boards.rename': 'Umbenennen',
|
||||||
'boards.delete': 'Löschen',
|
'boards.delete': 'Löschen',
|
||||||
'boards.delete_confirm': 'Board „{title}" wirklich löschen?',
|
'boards.delete_confirm': 'Board „{title}" wirklich löschen?',
|
||||||
@@ -32,26 +30,6 @@ const STRINGS = {
|
|||||||
'boards.add_link': ' Link hinzufügen',
|
'boards.add_link': ' Link hinzufügen',
|
||||||
'boards.remove_bookmark': 'Entfernen',
|
'boards.remove_bookmark': 'Entfernen',
|
||||||
|
|
||||||
// Papierkorb (Trash)
|
|
||||||
'trash.section': 'PAPIERKORB',
|
|
||||||
'trash.empty': 'Der Papierkorb ist leer.',
|
|
||||||
'trash.deleted_at': 'Gelöscht am {date}',
|
|
||||||
'trash.type.bookmark': 'Lesezeichen',
|
|
||||||
'trash.type.board': 'Board',
|
|
||||||
'trash.from_board': 'aus „{board}"',
|
|
||||||
'trash.from_board_unknown': 'Ursprungs-Board gelöscht',
|
|
||||||
'trash.restore': 'Wiederherstellen',
|
|
||||||
'trash.restore_title': 'Wiederherstellen',
|
|
||||||
'trash.delete_forever': 'Endgültig löschen',
|
|
||||||
'trash.delete_forever_title': 'Endgültig löschen',
|
|
||||||
'trash.empty_btn': 'Papierkorb leeren',
|
|
||||||
'trash.empty_confirm': 'Alle {count} Einträge endgültig löschen? Das kann nicht rückgängig gemacht werden.',
|
|
||||||
'trash.empty_confirm.title': 'Papierkorb leeren',
|
|
||||||
'trash.delete_forever_confirm': 'Diesen Eintrag endgültig löschen? Das kann nicht rückgängig gemacht werden.',
|
|
||||||
'trash.delete_forever_confirm.title': 'Endgültig löschen',
|
|
||||||
'trash.restored_to_inbox': 'Lesezeichen in die Inbox wiederhergestellt (Ursprungs-Board existiert nicht mehr).',
|
|
||||||
'trash.restored_to_inbox.title': 'Wiederhergestellt',
|
|
||||||
|
|
||||||
// Onboarding
|
// Onboarding
|
||||||
'onboarding.skip': 'Überspringen',
|
'onboarding.skip': 'Überspringen',
|
||||||
'onboarding.back': 'Zurück',
|
'onboarding.back': 'Zurück',
|
||||||
@@ -82,8 +60,6 @@ const STRINGS = {
|
|||||||
'onboarding.tradecenter_desc': 'Trade Center für Star Citizen',
|
'onboarding.tradecenter_desc': 'Trade Center für Star Citizen',
|
||||||
'onboarding.s7.title': 'Bereit!',
|
'onboarding.s7.title': 'Bereit!',
|
||||||
'onboarding.s7.text': 'Erstelle dein erstes Board mit „+ Board" oder importiere deine Browser-Lesezeichen über den Import-Button im Header. Viel Spaß!',
|
'onboarding.s7.text': 'Erstelle dein erstes Board mit „+ Board" oder importiere deine Browser-Lesezeichen über den Import-Button im Header. Viel Spaß!',
|
||||||
'onboarding.palette.title': 'Blitzschnell finden mit Strg+K',
|
|
||||||
'onboarding.palette.text': 'Drück jederzeit Strg+K, um die Befehls-Palette zu öffnen und alle Lesezeichen und Boards sofort zu durchsuchen. Mit ↑↓ navigieren, Enter öffnet, Esc schließt.',
|
|
||||||
|
|
||||||
// Notes
|
// Notes
|
||||||
'notes.limit_message': 'Maximale Anzahl erreicht! Du kannst maximal {max} Notes gleichzeitig haben. Lösche eine bestehende Note um eine neue zu erstellen.',
|
'notes.limit_message': 'Maximale Anzahl erreicht! Du kannst maximal {max} Notes gleichzeitig haben. Lösche eine bestehende Note um eine neue zu erstellen.',
|
||||||
@@ -411,19 +387,6 @@ const STRINGS = {
|
|||||||
'modal.rename_placeholder': 'Neuer Name...',
|
'modal.rename_placeholder': 'Neuer Name...',
|
||||||
'modal.rename_confirm': 'Umbenennen',
|
'modal.rename_confirm': 'Umbenennen',
|
||||||
'modal.theme_header': 'Darstellung',
|
'modal.theme_header': 'Darstellung',
|
||||||
// Theme-Picker-Cards (ARIA)
|
|
||||||
'theme.card.nebula': 'Theme Nebula wählen',
|
|
||||||
'theme.card.crescent': 'Theme Crescent wählen',
|
|
||||||
'theme.card.event_horizon': 'Theme Event Horizon wählen',
|
|
||||||
'theme.card.merchantman': 'Theme Merchantman wählen',
|
|
||||||
'theme.card.julia_jin': 'Theme Julia & Jin wählen',
|
|
||||||
'theme.card.sc_sunset': 'Theme SC Sunset wählen',
|
|
||||||
'theme.card.hellion_hud': 'Theme Hellion HUD wählen',
|
|
||||||
'theme.card.hellion_energy': 'Theme Hellion Energy wählen',
|
|
||||||
'theme.card.satisfactory': 'Theme Satisfactory wählen',
|
|
||||||
'theme.card.avorion': 'Theme Avorion wählen',
|
|
||||||
'theme.card.hellion_stealth': 'Theme Hellion Stealth wählen',
|
|
||||||
'toolbar.label': 'Widget-Werkzeugleiste',
|
|
||||||
|
|
||||||
// About
|
// About
|
||||||
'about.title': '⬡ HELLION NEWTAB',
|
'about.title': '⬡ HELLION NEWTAB',
|
||||||
@@ -450,19 +413,7 @@ const STRINGS = {
|
|||||||
'toolbar.calculator': 'Taschenrechner',
|
'toolbar.calculator': 'Taschenrechner',
|
||||||
'toolbar.timer': 'Timer',
|
'toolbar.timer': 'Timer',
|
||||||
'toolbar.imageref': 'Bild-Referenz',
|
'toolbar.imageref': 'Bild-Referenz',
|
||||||
'toolbar.notebook': 'Alle Notes',
|
'toolbar.notebook': 'Alle Notes'
|
||||||
|
|
||||||
// Command-Palette
|
|
||||||
'palette.placeholder': 'Bookmarks & Boards durchsuchen…',
|
|
||||||
'palette.aria_label': 'Bookmark-Suche',
|
|
||||||
'palette.list_label': 'Suchergebnisse',
|
|
||||||
'palette.no_results': 'Keine Treffer',
|
|
||||||
'palette.hint': 'Strg+K zum Öffnen · ↑↓ zum Navigieren · Enter öffnet · Esc schließt',
|
|
||||||
'palette.count': '{count} Treffer',
|
|
||||||
'palette.count_one': '1 Treffer',
|
|
||||||
'palette.board_prefix': 'Board:',
|
|
||||||
'palette.discover': 'Lesezeichen & Boards durchsuchen (Strg+K)',
|
|
||||||
'palette.discover_label': 'Suchen'
|
|
||||||
},
|
},
|
||||||
|
|
||||||
en: {
|
en: {
|
||||||
@@ -482,8 +433,6 @@ const STRINGS = {
|
|||||||
'boards.drag_title': 'Move board',
|
'boards.drag_title': 'Move board',
|
||||||
'boards.blur': 'Blur (private)',
|
'boards.blur': 'Blur (private)',
|
||||||
'boards.unblur': 'Unblur',
|
'boards.unblur': 'Unblur',
|
||||||
'boards.lock': 'Lock position',
|
|
||||||
'boards.unlock': 'Unlock position',
|
|
||||||
'boards.rename': 'Rename',
|
'boards.rename': 'Rename',
|
||||||
'boards.delete': 'Delete',
|
'boards.delete': 'Delete',
|
||||||
'boards.delete_confirm': 'Really delete board "{title}"?',
|
'boards.delete_confirm': 'Really delete board "{title}"?',
|
||||||
@@ -493,26 +442,6 @@ const STRINGS = {
|
|||||||
'boards.add_link': ' Add link',
|
'boards.add_link': ' Add link',
|
||||||
'boards.remove_bookmark': 'Remove',
|
'boards.remove_bookmark': 'Remove',
|
||||||
|
|
||||||
// Trash
|
|
||||||
'trash.section': 'TRASH',
|
|
||||||
'trash.empty': 'The trash is empty.',
|
|
||||||
'trash.deleted_at': 'Deleted on {date}',
|
|
||||||
'trash.type.bookmark': 'Bookmark',
|
|
||||||
'trash.type.board': 'Board',
|
|
||||||
'trash.from_board': 'from "{board}"',
|
|
||||||
'trash.from_board_unknown': 'Origin board deleted',
|
|
||||||
'trash.restore': 'Restore',
|
|
||||||
'trash.restore_title': 'Restore',
|
|
||||||
'trash.delete_forever': 'Delete forever',
|
|
||||||
'trash.delete_forever_title': 'Delete forever',
|
|
||||||
'trash.empty_btn': 'Empty trash',
|
|
||||||
'trash.empty_confirm': 'Permanently delete all {count} entries? This cannot be undone.',
|
|
||||||
'trash.empty_confirm.title': 'Empty trash',
|
|
||||||
'trash.delete_forever_confirm': 'Permanently delete this entry? This cannot be undone.',
|
|
||||||
'trash.delete_forever_confirm.title': 'Delete forever',
|
|
||||||
'trash.restored_to_inbox': 'Bookmark restored to the inbox (origin board no longer exists).',
|
|
||||||
'trash.restored_to_inbox.title': 'Restored',
|
|
||||||
|
|
||||||
// Onboarding
|
// Onboarding
|
||||||
'onboarding.skip': 'Skip',
|
'onboarding.skip': 'Skip',
|
||||||
'onboarding.back': 'Back',
|
'onboarding.back': 'Back',
|
||||||
@@ -543,8 +472,6 @@ const STRINGS = {
|
|||||||
'onboarding.tradecenter_desc': 'Trade Center for Star Citizen',
|
'onboarding.tradecenter_desc': 'Trade Center for Star Citizen',
|
||||||
'onboarding.s7.title': 'Ready!',
|
'onboarding.s7.title': 'Ready!',
|
||||||
'onboarding.s7.text': 'Create your first board with "+ Board" or import your browser bookmarks via the Import button in the header. Have fun!',
|
'onboarding.s7.text': 'Create your first board with "+ Board" or import your browser bookmarks via the Import button in the header. Have fun!',
|
||||||
'onboarding.palette.title': 'Find anything with Ctrl+K',
|
|
||||||
'onboarding.palette.text': 'Press Ctrl+K anytime to open the command palette and instantly search all your bookmarks and boards. Navigate with ↑↓, Enter opens, Esc closes.',
|
|
||||||
|
|
||||||
// Notes
|
// Notes
|
||||||
'notes.limit_message': 'Maximum reached! You can have at most {max} notes at the same time. Delete an existing note to create a new one.',
|
'notes.limit_message': 'Maximum reached! You can have at most {max} notes at the same time. Delete an existing note to create a new one.',
|
||||||
@@ -872,19 +799,6 @@ const STRINGS = {
|
|||||||
'modal.rename_placeholder': 'New name...',
|
'modal.rename_placeholder': 'New name...',
|
||||||
'modal.rename_confirm': 'Rename',
|
'modal.rename_confirm': 'Rename',
|
||||||
'modal.theme_header': 'Theme',
|
'modal.theme_header': 'Theme',
|
||||||
// Theme picker cards (ARIA)
|
|
||||||
'theme.card.nebula': 'Select Nebula theme',
|
|
||||||
'theme.card.crescent': 'Select Crescent theme',
|
|
||||||
'theme.card.event_horizon': 'Select Event Horizon theme',
|
|
||||||
'theme.card.merchantman': 'Select Merchantman theme',
|
|
||||||
'theme.card.julia_jin': 'Select Julia & Jin theme',
|
|
||||||
'theme.card.sc_sunset': 'Select SC Sunset theme',
|
|
||||||
'theme.card.hellion_hud': 'Select Hellion HUD theme',
|
|
||||||
'theme.card.hellion_energy': 'Select Hellion Energy theme',
|
|
||||||
'theme.card.satisfactory': 'Select Satisfactory theme',
|
|
||||||
'theme.card.avorion': 'Select Avorion theme',
|
|
||||||
'theme.card.hellion_stealth': 'Select Hellion Stealth theme',
|
|
||||||
'toolbar.label': 'Widget toolbar',
|
|
||||||
|
|
||||||
// About
|
// About
|
||||||
'about.title': '⬡ HELLION NEWTAB',
|
'about.title': '⬡ HELLION NEWTAB',
|
||||||
@@ -911,19 +825,7 @@ const STRINGS = {
|
|||||||
'toolbar.calculator': 'Calculator',
|
'toolbar.calculator': 'Calculator',
|
||||||
'toolbar.timer': 'Timer',
|
'toolbar.timer': 'Timer',
|
||||||
'toolbar.imageref': 'Image reference',
|
'toolbar.imageref': 'Image reference',
|
||||||
'toolbar.notebook': 'All notes',
|
'toolbar.notebook': 'All notes'
|
||||||
|
|
||||||
// Command palette
|
|
||||||
'palette.placeholder': 'Search bookmarks & boards…',
|
|
||||||
'palette.aria_label': 'Bookmark search',
|
|
||||||
'palette.list_label': 'Search results',
|
|
||||||
'palette.no_results': 'No results',
|
|
||||||
'palette.hint': 'Ctrl+K to open · ↑↓ to navigate · Enter opens · Esc closes',
|
|
||||||
'palette.count': '{count} results',
|
|
||||||
'palette.count_one': '1 result',
|
|
||||||
'palette.board_prefix': 'Board:',
|
|
||||||
'palette.discover': 'Search bookmarks & boards (Ctrl+K)',
|
|
||||||
'palette.discover_label': 'Search'
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -963,10 +865,6 @@ function applyLanguage() {
|
|||||||
el.title = text;
|
el.title = text;
|
||||||
el.setAttribute('aria-label', text);
|
el.setAttribute('aria-label', text);
|
||||||
});
|
});
|
||||||
// aria-label ohne sichtbaren title (z.B. Theme-Cards)
|
|
||||||
document.querySelectorAll('[data-i18n-aria-label]').forEach(el => {
|
|
||||||
el.setAttribute('aria-label', t(el.dataset.i18nAriaLabel));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -39,11 +39,6 @@ const Onboarding = {
|
|||||||
textKey: 'onboarding.s6.text',
|
textKey: 'onboarding.s6.text',
|
||||||
interactive: 'gaming-board'
|
interactive: 'gaming-board'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
hero: '\uD83D\uDD0D',
|
|
||||||
titleKey: 'onboarding.palette.title',
|
|
||||||
textKey: 'onboarding.palette.text'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
hero: '\uD83D\uDE80',
|
hero: '\uD83D\uDE80',
|
||||||
titleKey: 'onboarding.s7.title',
|
titleKey: 'onboarding.s7.title',
|
||||||
|
|||||||
@@ -32,59 +32,3 @@ chrome.tabs.onActivated.addListener((activeInfo) => {
|
|||||||
if (tab) forceRedirect(tab.id, tab.url);
|
if (tab) forceRedirect(tab.id, tab.url);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- QUICK SAVE (v2.3, additiv — Redirect oben unberuehrt) ----
|
|
||||||
// Geteiltes Helfer-Modul: ensureInbox(boards), uid(), normalizeBookmark(...).
|
|
||||||
// Pfad ist relativ zu DIESER Datei (src/js/opera/), daher ../quicksave-core.js.
|
|
||||||
importScripts('../quicksave-core.js');
|
|
||||||
|
|
||||||
// Interne/nicht speicherbare Seiten (Browser-UI, Extension-Seiten) — kein sinnvolles Bookmark.
|
|
||||||
const UNSAVEABLE_URL = /^(chrome|chrome-extension|about|edge|opera|moz-extension|brave|vivaldi|view-source|devtools):/i;
|
|
||||||
|
|
||||||
// Re-Entry-Schutz: ein zweiter Quick-Save waehrend der erste laeuft wuerde read-modify-write
|
|
||||||
// rennen (lost update). Bei aktivem Save den zweiten Druck verwerfen.
|
|
||||||
let qsBusy = false;
|
|
||||||
|
|
||||||
function quickSaveActiveTab() {
|
|
||||||
if (qsBusy) return;
|
|
||||||
qsBusy = true;
|
|
||||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
|
||||||
const tab = tabs && tabs[0];
|
|
||||||
if (!tab || !tab.url || UNSAVEABLE_URL.test(tab.url)) {
|
|
||||||
// Kein speicherbarer Tab: kurzer roter Marker (langer Text wird im Badge abgeschnitten).
|
|
||||||
chrome.action.setBadgeText({ text: '×' });
|
|
||||||
if (chrome.action.setBadgeBackgroundColor) {
|
|
||||||
chrome.action.setBadgeBackgroundColor({ color: '#c0392b' });
|
|
||||||
}
|
|
||||||
setTimeout(() => chrome.action.setBadgeText({ text: '' }), 2000);
|
|
||||||
qsBusy = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Datensicher: NICHT boards schreiben — nur an die Pending-Queue anhaengen (die Seite
|
|
||||||
// drained sie in die Inbox). So kann der Worker das boards-Array nicht clobbern (Review 2b).
|
|
||||||
chrome.storage.local.get(['quicksave_pending'], (r) => {
|
|
||||||
const pending = Array.isArray(r.quicksave_pending) ? r.quicksave_pending : [];
|
|
||||||
pending.push({ id: uid(), title: tab.title || tab.url, url: tab.url });
|
|
||||||
chrome.storage.local.set({ quicksave_pending: pending }, () => {
|
|
||||||
if (chrome.runtime.lastError) {
|
|
||||||
console.error('Quick-Save fehlgeschlagen:', chrome.runtime.lastError.message);
|
|
||||||
qsBusy = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
chrome.action.setBadgeText({ text: chrome.i18n.getMessage('quickSaveBadge') });
|
|
||||||
if (chrome.action.setBadgeBackgroundColor) {
|
|
||||||
chrome.action.setBadgeBackgroundColor({ color: '#1f9d55' });
|
|
||||||
}
|
|
||||||
setTimeout(() => chrome.action.setBadgeText({ text: '' }), 2000);
|
|
||||||
qsBusy = false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// onCommand SYNCHRON auf Top-Level (additiv neben den Redirect-Listenern).
|
|
||||||
chrome.commands.onCommand.addListener((command) => {
|
|
||||||
if (command === 'quick-save') {
|
|
||||||
quickSaveActiveTab();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,240 +0,0 @@
|
|||||||
/* =============================================
|
|
||||||
HELLION NEWTAB — palette.js
|
|
||||||
Command-Palette (Strg+K): read-only Suche ueber Boards/Bookmarks
|
|
||||||
============================================= */
|
|
||||||
|
|
||||||
// Reine init-Funktion ohne Top-Level-Seiteneffekt (Hausmuster wie initSearch).
|
|
||||||
// Wird aus app.js init() nach initSearch() aufgerufen.
|
|
||||||
function initPalette() {
|
|
||||||
let overlay = null; // aktives Overlay-Element oder null (= geschlossen)
|
|
||||||
let prevFocus = null; // Fokus-Rueckgabeziel
|
|
||||||
let keyHandler = null; // dokument-weiter Handler im offenen Zustand
|
|
||||||
let results = []; // aktuelle Trefferliste [{ title, url, boardName }]
|
|
||||||
let activeIndex = -1; // aktiver Listeneintrag fuer aria-activedescendant
|
|
||||||
|
|
||||||
// ---- Open-Guard: kein Strg+K wenn ein anderes Overlay offen ist ----
|
|
||||||
// Deckt Settings (.panel-overlay), Theme/Add-Board/Add-Bookmark/Rename
|
|
||||||
// (.modal-overlay) sowie HellionDialog UND Onboarding (.dialog-overlay) ab.
|
|
||||||
function isBlocked() {
|
|
||||||
// .palette-overlay ausklammern: das eigene (beim Schliessen noch deferred .active
|
|
||||||
// tragende) Overlay darf den Reopen-Guard nicht selbst blockieren (Self-Block-Race
|
|
||||||
// beim Toggle-Spam, da close() .active erst in withViewTransition entfernt).
|
|
||||||
return !!document.querySelector(
|
|
||||||
'.panel-overlay.active, .modal-overlay.active, .dialog-overlay.active:not(.palette-overlay)'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Trefferquelle: flach ueber alle Boards/Bookmarks ----
|
|
||||||
// Read-only auf dem globalen boards-Array. Match auf Titel, URL, Board-Name.
|
|
||||||
function search(query) {
|
|
||||||
const q = query.trim().toLowerCase();
|
|
||||||
if (!q) return [];
|
|
||||||
const out = [];
|
|
||||||
for (const board of boards) {
|
|
||||||
const boardName = board.title || '';
|
|
||||||
const boardMatch = boardName.toLowerCase().includes(q);
|
|
||||||
for (const bm of (board.bookmarks || [])) {
|
|
||||||
const title = bm.title || '';
|
|
||||||
const url = bm.url || '';
|
|
||||||
if (
|
|
||||||
title.toLowerCase().includes(q) ||
|
|
||||||
url.toLowerCase().includes(q) ||
|
|
||||||
boardMatch
|
|
||||||
) {
|
|
||||||
out.push({ title, url, boardName });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Treffer oeffnen (wie boards.js:270) ----
|
|
||||||
function openResult(item) {
|
|
||||||
if (!item || !item.url) return;
|
|
||||||
// Sicherheit: nur sichere Protokolle oeffnen. Verhindert javascript:/data:-URLs aus
|
|
||||||
// importierten Bookmarks (XSS im Extension-Origin, besonders bei _self). http/https/ftp only.
|
|
||||||
let safe = false;
|
|
||||||
try { safe = ['http:', 'https:', 'ftp:'].includes(new URL(item.url).protocol); } catch (e) { /* ungueltige URL */ }
|
|
||||||
if (!safe) { close(); return; }
|
|
||||||
window.open(item.url, settings.newTab ? '_blank' : '_self', 'noopener,noreferrer');
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Listbox neu rendern ----
|
|
||||||
function renderList(listEl, liveEl) {
|
|
||||||
listEl.textContent = '';
|
|
||||||
activeIndex = results.length ? 0 : -1;
|
|
||||||
|
|
||||||
if (results.length === 0) {
|
|
||||||
// Kein role="option": der Leerzustand ist keine auswaehlbare Option, sondern eine
|
|
||||||
// Statuszeile. Die Ansage uebernimmt die aria-live-Region (liveEl) unten.
|
|
||||||
const empty = document.createElement('li');
|
|
||||||
empty.className = 'palette-empty';
|
|
||||||
empty.setAttribute('role', 'presentation');
|
|
||||||
empty.textContent = t('palette.no_results');
|
|
||||||
listEl.appendChild(empty);
|
|
||||||
liveEl.textContent = t('palette.no_results');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
results.forEach((item, i) => {
|
|
||||||
const li = document.createElement('li');
|
|
||||||
li.className = 'palette-option';
|
|
||||||
li.id = 'palette-opt-' + i;
|
|
||||||
li.setAttribute('role', 'option');
|
|
||||||
li.setAttribute('aria-selected', i === 0 ? 'true' : 'false');
|
|
||||||
|
|
||||||
const titleSpan = document.createElement('span');
|
|
||||||
titleSpan.className = 'palette-option-title';
|
|
||||||
titleSpan.textContent = item.title;
|
|
||||||
|
|
||||||
const metaSpan = document.createElement('span');
|
|
||||||
metaSpan.className = 'palette-option-meta';
|
|
||||||
metaSpan.textContent = t('palette.board_prefix') + ' ' + item.boardName;
|
|
||||||
|
|
||||||
li.append(titleSpan, metaSpan);
|
|
||||||
// Pointer-Auswahl: Klick oeffnet, Hover markiert
|
|
||||||
li.addEventListener('click', () => openResult(item));
|
|
||||||
li.addEventListener('mousemove', () => setActive(listEl, i));
|
|
||||||
listEl.appendChild(li);
|
|
||||||
});
|
|
||||||
|
|
||||||
const count = results.length;
|
|
||||||
liveEl.textContent = count === 1 ? t('palette.count_one') : t('palette.count', { count });
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- aktiven Eintrag setzen (aria-activedescendant + aria-selected) ----
|
|
||||||
function setActive(listEl, idx) {
|
|
||||||
// Guard: close() nullt overlay synchron, das DOM-Removal laeuft aber deferred in
|
|
||||||
// withViewTransition. In diesem Frame-Fenster kann ein mousemove auf einem noch
|
|
||||||
// lebenden Treffer-<li> setActive() ausloesen -> ohne diesen Guard Null-Deref auf overlay.
|
|
||||||
if (!overlay) return;
|
|
||||||
const options = listEl.querySelectorAll('.palette-option');
|
|
||||||
if (options.length === 0) return;
|
|
||||||
activeIndex = Math.max(0, Math.min(idx, options.length - 1));
|
|
||||||
options.forEach((opt, i) => {
|
|
||||||
opt.setAttribute('aria-selected', i === activeIndex ? 'true' : 'false');
|
|
||||||
});
|
|
||||||
const input = overlay.querySelector('.palette-input');
|
|
||||||
input.setAttribute('aria-activedescendant', options[activeIndex].id);
|
|
||||||
options[activeIndex].scrollIntoView({ block: 'nearest' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- schliessen: dialog.js-Cleanup-Muster (remove Listener -> Transition -> Fokus) ----
|
|
||||||
function close() {
|
|
||||||
if (!overlay) return;
|
|
||||||
document.removeEventListener('keydown', keyHandler);
|
|
||||||
const el = overlay;
|
|
||||||
overlay = null;
|
|
||||||
keyHandler = null;
|
|
||||||
withViewTransition(() => {
|
|
||||||
el.classList.remove('active');
|
|
||||||
el.remove();
|
|
||||||
});
|
|
||||||
if (prevFocus && typeof prevFocus.focus === 'function') prevFocus.focus();
|
|
||||||
prevFocus = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- oeffnen: Overlay nach dialog.js-Muster aufbauen ----
|
|
||||||
function open() {
|
|
||||||
if (overlay) return;
|
|
||||||
prevFocus = document.activeElement;
|
|
||||||
|
|
||||||
overlay = document.createElement('div');
|
|
||||||
overlay.className = 'dialog-overlay palette-overlay';
|
|
||||||
|
|
||||||
const box = document.createElement('div');
|
|
||||||
box.className = 'palette-box';
|
|
||||||
box.setAttribute('role', 'none');
|
|
||||||
|
|
||||||
// ARIA-1.2-Combobox: role=combobox gehoert auf das fokussierbare Textfeld selbst,
|
|
||||||
// nicht auf die Huelle. aria-expanded/haspopup/controls/activedescendant ebenfalls hier.
|
|
||||||
const input = document.createElement('input');
|
|
||||||
input.className = 'palette-input';
|
|
||||||
input.type = 'text';
|
|
||||||
input.setAttribute('role', 'combobox');
|
|
||||||
input.setAttribute('aria-expanded', 'true');
|
|
||||||
input.setAttribute('aria-haspopup', 'listbox');
|
|
||||||
input.setAttribute('aria-label', t('palette.aria_label'));
|
|
||||||
input.setAttribute('aria-autocomplete', 'list');
|
|
||||||
input.setAttribute('aria-controls', 'palette-listbox');
|
|
||||||
input.placeholder = t('palette.placeholder');
|
|
||||||
input.autocomplete = 'off';
|
|
||||||
input.spellcheck = false;
|
|
||||||
|
|
||||||
const list = document.createElement('ul');
|
|
||||||
list.className = 'palette-list';
|
|
||||||
list.id = 'palette-listbox';
|
|
||||||
list.setAttribute('role', 'listbox');
|
|
||||||
list.setAttribute('aria-label', t('palette.list_label'));
|
|
||||||
|
|
||||||
const live = document.createElement('div');
|
|
||||||
live.className = 'palette-live';
|
|
||||||
live.setAttribute('aria-live', 'polite');
|
|
||||||
|
|
||||||
const hint = document.createElement('div');
|
|
||||||
hint.className = 'palette-hint';
|
|
||||||
hint.textContent = t('palette.hint');
|
|
||||||
|
|
||||||
box.append(input, list, live, hint);
|
|
||||||
overlay.appendChild(box);
|
|
||||||
|
|
||||||
// Klick auf den Overlay-Hintergrund schliesst (wie dialog.js:107)
|
|
||||||
overlay.addEventListener('click', e => {
|
|
||||||
if (e.target === overlay) close();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Live-Filter
|
|
||||||
input.addEventListener('input', () => {
|
|
||||||
results = search(input.value);
|
|
||||||
renderList(list, live);
|
|
||||||
input.setAttribute('aria-activedescendant', activeIndex >= 0 ? 'palette-opt-' + activeIndex : '');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Tastatursteuerung: Pfeile/Enter/Escape + Fokus-Falle (nur input fokussierbar)
|
|
||||||
keyHandler = function (e) {
|
|
||||||
if (e.key === 'Escape') { e.preventDefault(); close(); return; }
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
if (activeIndex >= 0 && results[activeIndex]) openResult(results[activeIndex]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (e.key === 'ArrowDown') { e.preventDefault(); setActive(list, activeIndex + 1); return; }
|
|
||||||
if (e.key === 'ArrowUp') { e.preventDefault(); setActive(list, activeIndex - 1); return; }
|
|
||||||
if (e.key === 'Tab') {
|
|
||||||
// Fokus-Falle ueber den Container: das Eingabefeld ist das einzige
|
|
||||||
// fokussierbare Element, also Tab/Shift+Tab immer dorthin zurueck.
|
|
||||||
e.preventDefault();
|
|
||||||
input.focus();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener('keydown', keyHandler);
|
|
||||||
|
|
||||||
document.body.appendChild(overlay);
|
|
||||||
// View-Transition uebernimmt das Fade; Fokus ins Eingabefeld
|
|
||||||
withViewTransition(() => {
|
|
||||||
overlay.classList.add('active');
|
|
||||||
input.focus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- globaler Ausloeser Strg+K (Meta+K auf Mac) mit Open-Guard ----
|
|
||||||
document.addEventListener('keydown', e => {
|
|
||||||
if ((e.ctrlKey || e.metaKey) && (e.key === 'k' || e.key === 'K')) {
|
|
||||||
if (overlay) { e.preventDefault(); close(); return; }
|
|
||||||
if (isBlocked()) return;
|
|
||||||
e.preventDefault();
|
|
||||||
open();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Persistenter Header-Trigger (BS-08): Klick toggelt die Palette wie Strg+K.
|
|
||||||
const paletteBtn = document.getElementById('btnPalette');
|
|
||||||
if (paletteBtn) {
|
|
||||||
paletteBtn.addEventListener('click', () => {
|
|
||||||
if (overlay) { close(); return; }
|
|
||||||
if (isBlocked()) return;
|
|
||||||
open();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
/* =============================================
|
|
||||||
HELLION NEWTAB — quicksave-core.js
|
|
||||||
DOM-freie geteilte Helfer fuer Seite UND Background-Worker.
|
|
||||||
Laeuft als <script> (newtab.html) und via importScripts (Service-Worker/Event-Page).
|
|
||||||
KEIN window/document/Store-Zugriff. Alle Exporte auf globalThis.
|
|
||||||
============================================= */
|
|
||||||
(function (root) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// Feste, nicht-zufaellige ID des Inbox-Boards (Quick-Save-Ziel).
|
|
||||||
// Bewusst KEIN uid(): die Inbox muss von Seite und Worker deterministisch
|
|
||||||
// wiedererkennbar sein, sonst entstehen Duplikate (QS-08).
|
|
||||||
const INBOX_ID = 'inbox';
|
|
||||||
|
|
||||||
// Kollisionsarme Kurz-ID. Identisch zur frueheren state.js-Variante,
|
|
||||||
// damit bestehende Aufrufer (boards.js, data.js, app.js, bookmark-import.js) unveraendert weiterlaufen.
|
|
||||||
function uid() {
|
|
||||||
return Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sichert, dass im uebergebenen boards-Array genau EIN Inbox-Board existiert,
|
|
||||||
// und gibt dieses Inbox-Board-Objekt zurueck. Idempotent: findet ein Board mit
|
|
||||||
// id === INBOX_ID, sonst legt es eins mit fester id vorne an (unshift) und mutiert
|
|
||||||
// das uebergebene Array dabei IN-PLACE. Der Worker schreibt anschliessend dasselbe
|
|
||||||
// (mutierte) boards-Array via storage zurueck; der Rueckgabewert ist das Board,
|
|
||||||
// damit Aufrufer direkt inbox.bookmarks.push(...) machen koennen (QS-08).
|
|
||||||
function ensureInbox(boardsArr) {
|
|
||||||
const list = Array.isArray(boardsArr) ? boardsArr : [];
|
|
||||||
let inbox = list.find(b => b && b.id === INBOX_ID);
|
|
||||||
if (!inbox) {
|
|
||||||
inbox = { id: INBOX_ID, title: 'Inbox', bookmarks: [], blurred: false };
|
|
||||||
list.unshift(inbox);
|
|
||||||
}
|
|
||||||
return inbox;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sicheres URL-Protokoll (http/https/ftp). Inhaltlich identisch zur data.js-Variante, aber
|
|
||||||
// DOM-frei und auf globalThis, damit der Quick-Save-Drain (app.js) dieselbe Validierung nutzt
|
|
||||||
// wie jeder andere Bookmark-Schreibpfad. URL ist in Worker UND Seite verfuegbar.
|
|
||||||
function isSafeUrl(url) {
|
|
||||||
try {
|
|
||||||
return ['http:', 'https:', 'ftp:'].includes(new URL(url).protocol);
|
|
||||||
} catch (_) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalisiert eine Bookmark in die kanonische Form { id, title, url, desc }.
|
|
||||||
// title-Fallback auf url, desc auf ''. Begrenzt Laengen wie data.js (200/500),
|
|
||||||
// damit Quick-Save-Eintraege das gleiche Schema wie Import/Manuell haben.
|
|
||||||
function normalizeBookmark(raw) {
|
|
||||||
const url = (raw && typeof raw.url === 'string') ? raw.url : '';
|
|
||||||
const title = (raw && typeof raw.title === 'string' && raw.title.trim())
|
|
||||||
? raw.title.trim()
|
|
||||||
: url;
|
|
||||||
return {
|
|
||||||
id: (raw && raw.id) ? raw.id : uid(),
|
|
||||||
title: String(title).slice(0, 200),
|
|
||||||
url: url,
|
|
||||||
desc: String((raw && raw.desc) || '').slice(0, 500)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
root.INBOX_ID = INBOX_ID;
|
|
||||||
root.uid = uid;
|
|
||||||
root.isSafeUrl = isSafeUrl;
|
|
||||||
root.ensureInbox = ensureInbox;
|
|
||||||
root.normalizeBookmark = normalizeBookmark;
|
|
||||||
})(typeof globalThis !== 'undefined' ? globalThis : self);
|
|
||||||
+15
-278
@@ -3,106 +3,24 @@
|
|||||||
Settings Panel, Theme-Modal, Accordion, Toggles
|
Settings Panel, Theme-Modal, Accordion, Toggles
|
||||||
============================================= */
|
============================================= */
|
||||||
|
|
||||||
// ---- A11Y: Fokus-Management fuer Modals ----
|
|
||||||
// Merkt sich das vor dem Oeffnen fokussierte Element, damit wir es beim
|
|
||||||
// Schliessen restaurieren koennen. Pro offenem Modal eine Closure-Variable.
|
|
||||||
const _focusReturn = { settings: null, theme: null };
|
|
||||||
|
|
||||||
/** Liefert die fokussierbaren Elemente innerhalb eines Containers. */
|
|
||||||
function _focusable(container) {
|
|
||||||
return Array.from(container.querySelectorAll(
|
|
||||||
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
||||||
)).filter(el => el.offsetParent !== null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Tab/Shift+Tab im Container einfangen + Escape schliesst. */
|
|
||||||
function _makeTrap(container, closeFn) {
|
|
||||||
return function trap(e) {
|
|
||||||
// Ein offener HellionDialog (z.B. Reset-All-Confirm oder BG-URL-Alert aus
|
|
||||||
// dem Panel) hat Vorrang: sein eigener keydown-Handler uebernimmt Escape/Tab.
|
|
||||||
// Sonst schloessen beide Listener gleichzeitig und die Dialog-Fokusfalle wird loechrig.
|
|
||||||
if (document.querySelector('.dialog-overlay')) return;
|
|
||||||
if (e.key === 'Escape') { e.preventDefault(); closeFn(); return; }
|
|
||||||
if (e.key !== 'Tab') return;
|
|
||||||
const items = _focusable(container);
|
|
||||||
if (items.length === 0) return;
|
|
||||||
const first = items[0];
|
|
||||||
const last = items[items.length - 1];
|
|
||||||
if (e.shiftKey && document.activeElement === first) {
|
|
||||||
e.preventDefault(); last.focus();
|
|
||||||
} else if (!e.shiftKey && document.activeElement === last) {
|
|
||||||
e.preventDefault(); first.focus();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- SETTINGS PANEL ----
|
// ---- SETTINGS PANEL ----
|
||||||
// Hinweis: withViewTransition (Phase 4) bleibt fuer das Fade erhalten; das
|
|
||||||
// Fokus-Management (merken, Falle, Rueckgabe) liegt bewusst ausserhalb des
|
|
||||||
// Transition-Callbacks. activeElement wird vor der Mutation gelesen.
|
|
||||||
let _settingsTrap = null;
|
|
||||||
function openSettings() {
|
function openSettings() {
|
||||||
const panel = document.getElementById('settingsPanel');
|
document.getElementById('settingsPanel').classList.add('open');
|
||||||
_focusReturn.settings = document.activeElement;
|
document.getElementById('settingsOverlay').classList.add('active');
|
||||||
withViewTransition(() => {
|
|
||||||
panel.classList.add('open');
|
|
||||||
document.getElementById('settingsOverlay').classList.add('active');
|
|
||||||
});
|
|
||||||
panel.setAttribute('aria-hidden', 'false');
|
|
||||||
renderTrash();
|
|
||||||
_settingsTrap = _makeTrap(panel, closeSettings);
|
|
||||||
document.addEventListener('keydown', _settingsTrap);
|
|
||||||
const first = _focusable(panel)[0];
|
|
||||||
if (first) first.focus();
|
|
||||||
}
|
}
|
||||||
function closeSettings() {
|
function closeSettings() {
|
||||||
const panel = document.getElementById('settingsPanel');
|
document.getElementById('settingsPanel').classList.remove('open');
|
||||||
withViewTransition(() => {
|
document.getElementById('settingsOverlay').classList.remove('active');
|
||||||
panel.classList.remove('open');
|
|
||||||
document.getElementById('settingsOverlay').classList.remove('active');
|
|
||||||
});
|
|
||||||
panel.setAttribute('aria-hidden', 'true');
|
|
||||||
if (_settingsTrap) { document.removeEventListener('keydown', _settingsTrap); _settingsTrap = null; }
|
|
||||||
if (_focusReturn.settings) { _focusReturn.settings.focus(); _focusReturn.settings = null; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- THEME MODAL ----
|
// ---- THEME MODAL ----
|
||||||
let _themeTrap = null;
|
|
||||||
function openThemeModal() {
|
function openThemeModal() {
|
||||||
const overlay = document.getElementById('themeOverlay');
|
const overlay = document.getElementById('themeOverlay');
|
||||||
const modal = document.getElementById('themeModal');
|
overlay.classList.add('active');
|
||||||
_focusReturn.theme = document.activeElement;
|
|
||||||
withViewTransition(() => {
|
|
||||||
overlay.classList.add('active');
|
|
||||||
});
|
|
||||||
modal.setAttribute('aria-hidden', 'false');
|
|
||||||
_themeTrap = _makeTrap(modal, closeThemeModal);
|
|
||||||
document.addEventListener('keydown', _themeTrap);
|
|
||||||
const first = _focusable(modal)[0];
|
|
||||||
if (first) first.focus();
|
|
||||||
}
|
}
|
||||||
function closeThemeModal() {
|
function closeThemeModal() {
|
||||||
const overlay = document.getElementById('themeOverlay');
|
const overlay = document.getElementById('themeOverlay');
|
||||||
const modal = document.getElementById('themeModal');
|
overlay.classList.remove('active');
|
||||||
withViewTransition(() => {
|
|
||||||
overlay.classList.remove('active');
|
|
||||||
});
|
|
||||||
modal.setAttribute('aria-hidden', 'true');
|
|
||||||
if (_themeTrap) { document.removeEventListener('keydown', _themeTrap); _themeTrap = null; }
|
|
||||||
if (_focusReturn.theme) { _focusReturn.theme.focus(); _focusReturn.theme = null; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wechselt das Theme mit nativem Cross-Fade (View Transitions API).
|
|
||||||
* Wrap sitzt bewusst hier am User-Ausloeser, NICHT in applyTheme(),
|
|
||||||
* sonst fadet jeder neue Tab beim Initial-Load (settings.js:101).
|
|
||||||
* Feature-Detection-Fallback: aeltere Browser (z.B. Firefox < 144)
|
|
||||||
* schalten instant um, ohne Bruch.
|
|
||||||
* @param {string} name - Theme-Name
|
|
||||||
*/
|
|
||||||
function switchTheme(name) {
|
|
||||||
const swap = () => applyTheme(name, false); // false: Theme-BG anwenden (kein User-bgUrl-Schutz hier noetig, bgUrl wurde geleert)
|
|
||||||
withViewTransition(swap);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -142,165 +60,6 @@ function initAccordion() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- PAPIERKORB ----
|
|
||||||
/**
|
|
||||||
* Formatiert einen deletedAt-Timestamp lokalisiert (folgt der aktiven UI-Sprache).
|
|
||||||
* @param {number} ts - Millisekunden-Timestamp
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function formatTrashDate(ts) {
|
|
||||||
const locale = I18n.currentLang === 'de' ? 'de-DE' : 'en-US';
|
|
||||||
return new Date(ts).toLocaleDateString(locale, { year: 'numeric', month: '2-digit', day: '2-digit' });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rendert den Papierkorb in die Settings-Section. Wird bei jedem openSettings()
|
|
||||||
* sowie nach jeder Trash-Mutation aufgerufen. Baut DOM ohne innerHTML (XSS-frei,
|
|
||||||
* Titel kommen aus User-/Importdaten).
|
|
||||||
*/
|
|
||||||
function renderTrash() {
|
|
||||||
const listEl = document.getElementById('trashList');
|
|
||||||
const actionsRow = document.getElementById('trashActionsRow');
|
|
||||||
if (!listEl) return;
|
|
||||||
listEl.replaceChildren();
|
|
||||||
|
|
||||||
if (trash.length === 0) {
|
|
||||||
const empty = document.createElement('div');
|
|
||||||
empty.className = 'trash-empty';
|
|
||||||
empty.textContent = t('trash.empty');
|
|
||||||
listEl.appendChild(empty);
|
|
||||||
if (actionsRow) actionsRow.classList.add('hidden');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (actionsRow) actionsRow.classList.remove('hidden');
|
|
||||||
|
|
||||||
// Neueste zuerst.
|
|
||||||
const sorted = [...trash].sort((a, b) => b.deletedAt - a.deletedAt);
|
|
||||||
sorted.forEach(entry => listEl.appendChild(createTrashItemEl(entry)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Baut eine einzelne Papierkorb-Zeile.
|
|
||||||
* @param {Object} entry - trash-Eintrag { item, type, originBoardId, deletedAt }
|
|
||||||
* @returns {HTMLElement}
|
|
||||||
*/
|
|
||||||
function createTrashItemEl(entry) {
|
|
||||||
const row = document.createElement('div');
|
|
||||||
row.className = 'trash-item';
|
|
||||||
|
|
||||||
const info = document.createElement('div');
|
|
||||||
info.className = 'trash-item-info';
|
|
||||||
|
|
||||||
const titleLine = document.createElement('span');
|
|
||||||
titleLine.className = 'trash-item-title';
|
|
||||||
const badge = document.createElement('span');
|
|
||||||
badge.className = 'trash-item-badge';
|
|
||||||
badge.textContent = entry.type === 'board' ? t('trash.type.board') : t('trash.type.bookmark');
|
|
||||||
const titleText = document.createTextNode(entry.item && entry.item.title ? entry.item.title : '');
|
|
||||||
titleLine.append(badge, titleText);
|
|
||||||
|
|
||||||
const meta = document.createElement('span');
|
|
||||||
meta.className = 'trash-item-meta';
|
|
||||||
let metaText = t('trash.deleted_at', { date: formatTrashDate(entry.deletedAt) });
|
|
||||||
if (entry.type === 'bookmark') {
|
|
||||||
const origin = entry.originBoardId ? boards.find(b => b.id === entry.originBoardId) : null;
|
|
||||||
metaText += origin
|
|
||||||
? ' · ' + t('trash.from_board', { board: origin.title })
|
|
||||||
: ' · ' + t('trash.from_board_unknown');
|
|
||||||
}
|
|
||||||
meta.textContent = metaText;
|
|
||||||
|
|
||||||
info.append(titleLine, meta);
|
|
||||||
|
|
||||||
const actions = document.createElement('div');
|
|
||||||
actions.className = 'trash-item-actions';
|
|
||||||
|
|
||||||
const btnRestore = document.createElement('button');
|
|
||||||
btnRestore.className = 'btn-small';
|
|
||||||
btnRestore.textContent = t('trash.restore');
|
|
||||||
btnRestore.title = t('trash.restore_title');
|
|
||||||
btnRestore.addEventListener('click', () => { btnRestore.disabled = true; restoreTrashEntry(entry); });
|
|
||||||
|
|
||||||
const btnForever = document.createElement('button');
|
|
||||||
btnForever.className = 'btn-danger';
|
|
||||||
btnForever.textContent = t('trash.delete_forever');
|
|
||||||
btnForever.title = t('trash.delete_forever_title');
|
|
||||||
btnForever.addEventListener('click', () => deleteTrashEntryForever(entry));
|
|
||||||
|
|
||||||
actions.append(btnRestore, btnForever);
|
|
||||||
row.append(info, actions);
|
|
||||||
return row;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stellt einen Papierkorb-Eintrag wieder her.
|
|
||||||
* Bookmark -> in originBoardId (falls noch vorhanden), sonst in die Inbox (ensureInboxBoard).
|
|
||||||
* Board -> zurueck in boards[].
|
|
||||||
* @param {Object} entry
|
|
||||||
*/
|
|
||||||
async function restoreTrashEntry(entry) {
|
|
||||||
// Re-Entry-Guard: ein zweiter Klick (z.B. waehrend der Inbox-Alert offen ist) wuerde sonst
|
|
||||||
// das Item ein zweites Mal einfuegen (Duplikat). Nach der ersten Ausfuehrung ist entry
|
|
||||||
// nicht mehr in trash[]; btnRestore wird zusaetzlich beim ersten Klick disabled.
|
|
||||||
if (!trash.includes(entry)) return;
|
|
||||||
if (entry.type === 'board') {
|
|
||||||
// Ganzes Board zurueck (inkl. blurred). Falls die id schon existiert (Edge-Case),
|
|
||||||
// neue uid vergeben, damit nichts ueberschrieben wird.
|
|
||||||
const restored = structuredClone(entry.item);
|
|
||||||
if (boards.some(b => b.id === restored.id)) restored.id = uid();
|
|
||||||
boards.push(restored);
|
|
||||||
await saveBoards();
|
|
||||||
} else {
|
|
||||||
const restored = structuredClone(entry.item);
|
|
||||||
let target = entry.originBoardId ? boards.find(b => b.id === entry.originBoardId) : null;
|
|
||||||
let toInbox = false;
|
|
||||||
if (!target) {
|
|
||||||
// Ursprungs-Board weg -> in die Inbox (Page-Wrapper ensureInboxBoard aus Phase 1).
|
|
||||||
target = await ensureInboxBoard();
|
|
||||||
toInbox = true;
|
|
||||||
}
|
|
||||||
target.bookmarks.push(restored);
|
|
||||||
await saveBoards();
|
|
||||||
if (toInbox) {
|
|
||||||
await HellionDialog.alert(t('trash.restored_to_inbox'), { type: 'info', title: t('trash.restored_to_inbox.title') });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
trash = trash.filter(e => e !== entry);
|
|
||||||
await saveTrash();
|
|
||||||
renderTrash();
|
|
||||||
renderBoards();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loescht einen einzelnen Papierkorb-Eintrag endgueltig (mit Confirm).
|
|
||||||
* @param {Object} entry
|
|
||||||
*/
|
|
||||||
async function deleteTrashEntryForever(entry) {
|
|
||||||
const ok = await HellionDialog.confirm(
|
|
||||||
t('trash.delete_forever_confirm'),
|
|
||||||
{ type: 'danger', title: t('trash.delete_forever_confirm.title'), confirmText: t('trash.delete_forever') }
|
|
||||||
);
|
|
||||||
if (!ok) return;
|
|
||||||
trash = trash.filter(e => e !== entry);
|
|
||||||
await saveTrash();
|
|
||||||
renderTrash();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Leert den gesamten Papierkorb (mit Confirm).
|
|
||||||
*/
|
|
||||||
async function emptyTrash() {
|
|
||||||
if (trash.length === 0) return;
|
|
||||||
const ok = await HellionDialog.confirm(
|
|
||||||
t('trash.empty_confirm', { count: trash.length }),
|
|
||||||
{ type: 'danger', title: t('trash.empty_confirm.title'), confirmText: t('trash.empty_btn') }
|
|
||||||
);
|
|
||||||
if (!ok) return;
|
|
||||||
trash = [];
|
|
||||||
await saveTrash();
|
|
||||||
renderTrash();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- APPLY SETTINGS ----
|
// ---- APPLY SETTINGS ----
|
||||||
function applySettings() {
|
function applySettings() {
|
||||||
const body = document.body;
|
const body = document.body;
|
||||||
@@ -330,11 +89,6 @@ function applySettings() {
|
|||||||
const imgRefBtn = document.querySelector('[data-action="image-ref"]');
|
const imgRefBtn = document.querySelector('[data-action="image-ref"]');
|
||||||
if (imgRefBtn) imgRefBtn.classList.toggle('hidden', !settings.imageRefEnabled);
|
if (imgRefBtn) imgRefBtn.classList.toggle('hidden', !settings.imageRefEnabled);
|
||||||
|
|
||||||
// A11y: aria-checked aller role=switch-Toggles an den realen checked-State angleichen
|
|
||||||
document.querySelectorAll('.toggle input[role="switch"]').forEach(cb => {
|
|
||||||
cb.setAttribute('aria-checked', cb.checked ? 'true' : 'false');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Toolbar-Position
|
// Toolbar-Position
|
||||||
document.body.classList.toggle('toolbar-left', settings.toolbarPos === 'left');
|
document.body.classList.toggle('toolbar-left', settings.toolbarPos === 'left');
|
||||||
const toolbarPosEl = document.getElementById('settingToolbarPos');
|
const toolbarPosEl = document.getElementById('settingToolbarPos');
|
||||||
@@ -368,25 +122,15 @@ function bindSettingsEvents() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Theme-Picker (Cards im Theme-Modal)
|
// Theme-Picker (Cards im Theme-Modal)
|
||||||
const themeCards = document.querySelectorAll('.theme-card');
|
document.querySelectorAll('.theme-card').forEach(card => {
|
||||||
function selectThemeCard(card) {
|
card.addEventListener('click', async () => {
|
||||||
const name = card.dataset.value;
|
const name = card.dataset.value;
|
||||||
if (!name || name === settings.theme) return Promise.resolve();
|
if (!name || name === settings.theme) return;
|
||||||
settings.theme = name;
|
settings.theme = name;
|
||||||
settings.bgUrl = '';
|
settings.bgUrl = '';
|
||||||
document.getElementById('bgUrlInput').value = '';
|
document.getElementById('bgUrlInput').value = '';
|
||||||
// aria-pressed synchron halten — applyTheme/switchTheme pflegt nur die .active-Klasse, nicht ARIA
|
applyTheme(name, false);
|
||||||
themeCards.forEach(c => c.setAttribute('aria-pressed', c === card ? 'true' : 'false'));
|
await saveSettings();
|
||||||
switchTheme(name); // WICHTIG: switchTheme aus Phase 4 (View-Transition-Wrapper), NICHT applyTheme direkt — sonst geht der Theme-Fade verloren
|
|
||||||
return saveSettings();
|
|
||||||
}
|
|
||||||
themeCards.forEach(card => {
|
|
||||||
card.addEventListener('click', () => selectThemeCard(card));
|
|
||||||
card.addEventListener('keydown', e => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault();
|
|
||||||
selectThemeCard(card);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -419,7 +163,6 @@ function bindSettingsEvents() {
|
|||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if (el) {
|
if (el) {
|
||||||
el.addEventListener('change', async e => {
|
el.addEventListener('change', async e => {
|
||||||
e.target.setAttribute('aria-checked', e.target.checked ? 'true' : 'false');
|
|
||||||
fn(e.target.checked);
|
fn(e.target.checked);
|
||||||
await saveSettings();
|
await saveSettings();
|
||||||
});
|
});
|
||||||
@@ -496,10 +239,6 @@ function bindSettingsEvents() {
|
|||||||
Onboarding.start();
|
Onboarding.start();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Papierkorb leeren
|
|
||||||
const btnEmptyTrash = document.getElementById('btnEmptyTrash');
|
|
||||||
if (btnEmptyTrash) btnEmptyTrash.addEventListener('click', emptyTrash);
|
|
||||||
|
|
||||||
// Reset All
|
// Reset All
|
||||||
document.getElementById('btnResetAll').addEventListener('click', async () => {
|
document.getElementById('btnResetAll').addEventListener('click', async () => {
|
||||||
const ok = await HellionDialog.confirm(
|
const ok = await HellionDialog.confirm(
|
||||||
@@ -508,13 +247,11 @@ function bindSettingsEvents() {
|
|||||||
);
|
);
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
boards = [];
|
boards = [];
|
||||||
trash = [];
|
|
||||||
settings = { compact: false, shortenTitles: false, newTab: true, showDesc: false,
|
settings = { compact: false, shortenTitles: false, newTab: true, showDesc: false,
|
||||||
hideExtra: false, visibleCount: 10, bgUrl: '', theme: 'nebula',
|
hideExtra: false, visibleCount: 10, bgUrl: '', theme: 'nebula',
|
||||||
showSearch: true, searchEngine: 'google', toolbarPos: 'right',
|
showSearch: true, searchEngine: 'google', toolbarPos: 'right',
|
||||||
imageRefEnabled: false, language: 'auto' };
|
imageRefEnabled: false, language: 'auto' };
|
||||||
await saveBoards();
|
await saveBoards();
|
||||||
await saveTrash();
|
|
||||||
await saveSettings();
|
await saveSettings();
|
||||||
setLanguage('auto');
|
setLanguage('auto');
|
||||||
applySettings();
|
applySettings();
|
||||||
|
|||||||
+4
-68
@@ -5,17 +5,6 @@
|
|||||||
|
|
||||||
let boards = [];
|
let boards = [];
|
||||||
|
|
||||||
// Papierkorb als EIGENER Store-Key (nicht im boards-Payload), isoliert das Quota-Risiko (CR-04/TRASH-02).
|
|
||||||
// Eintrag-Schema: { item, type: 'bookmark'|'board', deletedAt, originBoardId }
|
|
||||||
let trash = [];
|
|
||||||
|
|
||||||
// Papierkorb: Auto-Cleanup-Fenster und harte Obergrenze (Quota-Schutz, TRASH-04).
|
|
||||||
// 30 Tage in Millisekunden; ueber dieser Zeit wird ein Eintrag beim Laden auto-geloescht.
|
|
||||||
const TRASH_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
|
||||||
// Max. Anzahl trash-Eintraege. Bei Ueberlauf werden die aeltesten zuerst verworfen,
|
|
||||||
// damit der Papierkorb nicht das 10-MB-Storage-Limit sprengt (kein blindes Wachstum).
|
|
||||||
const TRASH_MAX_ENTRIES = 100;
|
|
||||||
|
|
||||||
let settings = {
|
let settings = {
|
||||||
compact: false,
|
compact: false,
|
||||||
shortenTitles: false,
|
shortenTitles: false,
|
||||||
@@ -32,8 +21,9 @@ let settings = {
|
|||||||
language: 'auto'
|
language: 'auto'
|
||||||
};
|
};
|
||||||
|
|
||||||
// uid() lebt jetzt in quicksave-core.js (globalThis.uid), damit Seite und
|
function uid() {
|
||||||
// Background-Worker dieselbe ID-Erzeugung teilen. Hier bewusst KEINE eigene Deklaration.
|
return Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
|
||||||
|
}
|
||||||
|
|
||||||
function escHtml(str) {
|
function escHtml(str) {
|
||||||
return String(str)
|
return String(str)
|
||||||
@@ -53,9 +43,7 @@ function getDefaultBoards() {
|
|||||||
{ id: uid(), title: 'MDN Web Docs', url: 'https://developer.mozilla.org', 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: '' },
|
{ id: uid(), title: 'Next.js Docs', url: 'https://nextjs.org/docs', desc: '' },
|
||||||
],
|
],
|
||||||
blurred: false,
|
blurred: false
|
||||||
locked: false,
|
|
||||||
pos: { x: 40, y: 110 }
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -64,58 +52,6 @@ async function saveBoards() {
|
|||||||
await Store.set('boards', boards);
|
await Store.set('boards', boards);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveTrash() {
|
|
||||||
await Store.set('trash', trash);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Legt einen Eintrag in den Papierkorb. Klont das Objekt (structuredClone),
|
|
||||||
* damit der trash-Eintrag nicht per Referenz an boards[] haengt und nach dem
|
|
||||||
* Restore-Loop konsistent bleibt. Setzt deletedAt. Erzwingt die harte Obergrenze
|
|
||||||
* TRASH_MAX_ENTRIES (Quota-Schutz, TRASH-04): bei Ueberlauf fallen die aeltesten
|
|
||||||
* Eintraege heraus. Speichern uebernimmt der Aufrufer (saveTrash()).
|
|
||||||
* @param {{ item: Object, type: 'bookmark'|'board', originBoardId: (string|null) }} entry
|
|
||||||
*/
|
|
||||||
function pushToTrash({ item, type, originBoardId }) {
|
|
||||||
const entry = {
|
|
||||||
item: structuredClone(item),
|
|
||||||
type,
|
|
||||||
originBoardId: originBoardId ?? null,
|
|
||||||
deletedAt: Date.now()
|
|
||||||
};
|
|
||||||
trash.push(entry);
|
|
||||||
// Aelteste zuerst kappen, falls die Obergrenze ueberschritten ist.
|
|
||||||
if (trash.length > TRASH_MAX_ENTRIES) {
|
|
||||||
trash.sort((a, b) => a.deletedAt - b.deletedAt);
|
|
||||||
trash = trash.slice(trash.length - TRASH_MAX_ENTRIES);
|
|
||||||
}
|
|
||||||
return entry; // fuer Rollback im Delete-Handler bei Save-Fehler (W-b/Quota)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Page-seitiger Wrapper um das DOM-freie ensureInbox() aus quicksave-core.js.
|
|
||||||
// ensureInbox() mutiert das globale boards-Array in-place; wir persistieren nur,
|
|
||||||
// wenn die Inbox neu angelegt wurde, und geben das Inbox-Board-Objekt zurueck
|
|
||||||
// (fuer Quick-Save-/Restore-Pfade).
|
|
||||||
async function ensureInboxBoard() {
|
|
||||||
const before = boards.length;
|
|
||||||
const inbox = ensureInbox(boards); // global aus quicksave-core.js; mutiert boards in-place
|
|
||||||
if (boards.length !== before) {
|
|
||||||
await saveBoards();
|
|
||||||
}
|
|
||||||
return inbox;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveSettings() {
|
async function saveSettings() {
|
||||||
await Store.set('settings', settings);
|
await Store.set('settings', settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- VIEW TRANSITIONS ----
|
|
||||||
// Fuehrt eine synchrone DOM-Mutation mit nativem View-Transition-Fade aus.
|
|
||||||
// Feature-Detection-Fallback (Firefox < 144): instant. reduced-motion kappt das Fade ueber den ungeschichteten @media-Block.
|
|
||||||
function withViewTransition(mutate) {
|
|
||||||
if (document.startViewTransition) {
|
|
||||||
document.startViewTransition(mutate);
|
|
||||||
} else {
|
|
||||||
mutate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user