diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..6a842c5 --- /dev/null +++ b/TODO.md @@ -0,0 +1,217 @@ +# U-Navigator – TODO + +Stand: 2026-07-14 + +Ziel dieser Liste ist eine sichere, testbare und veröffentlichungsfähige Version des Plugins. Schreibzugriff sollte erst empfohlen werden, wenn alle Punkte unter P0 abgeschlossen und auf einer echten Unraid-Installation geprüft sind. + +## P0 – Sicherheitsblocker + +### Geschützte Root-Pfade + +- [ ] Eine zentrale Funktion `unav_assert_mutable_path()` einführen. +- [ ] Löschen eines konfigurierten Root-Pfads verbieten. +- [ ] Verschieben und Umbenennen eines konfigurierten Root-Pfads verbieten. +- [ ] Rekursive Rechte- oder Besitzeränderungen direkt am Root-Pfad verbieten. +- [ ] Quelle und Ziel auch im Worker unmittelbar vor der Operation erneut prüfen. +- [ ] API-Tests für Delete, Move und Permissions auf `/mnt/user` beziehungsweise einem Test-Root ergänzen. + +Abnahmekriterium: Kein mutierender API-Aufruf kann den erlaubten Root selbst verändern oder entfernen. + +### Sichere Symlink-Behandlung + +- [ ] Eingabepfad und kanonischen Sicherheitspfad getrennt speichern. +- [ ] Für Dateioperationen `lstat()` statt ausschließlich `realpath()` verwenden. +- [ ] Beim Löschen eines Symlinks ausschließlich den Link entfernen, niemals dessen Ziel. +- [ ] Copy und Move von Symlinks entweder kontrolliert implementieren oder ausdrücklich ablehnen. +- [ ] Symlinks auf Ziele außerhalb erlaubter Roots für Listing, Vorschau und Download weiterhin blockieren. +- [ ] Rekursive Operationen dürfen Symlinks nicht verfolgen. +- [ ] Tests für interne Symlinks, externe Symlinks, kaputte Symlinks und Symlink-Schleifen ergänzen. + +Abnahmekriterium: Eine Aktion auf einem Symlink wirkt niemals unerwartet auf dessen Ziel. + +### Upload-Pfade härten + +- [ ] Relative Pfade vollständig validieren; `..` nicht still entfernen, sondern als Fehler ablehnen. +- [ ] Absolute Pfade, Nullbytes und leere Pfadsegmente ablehnen. +- [ ] Jeden Parent-Pfad prüfen, bevor ein Verzeichnis angelegt wird. +- [ ] Beim Anlegen von Upload-Ordnern keine vorhandenen Symlinks durchlaufen. +- [ ] Zielpfad unmittelbar vor Schreiben beziehungsweise Rename erneut validieren. +- [ ] Temporäre Upload-Dateien in einem privaten Verzeichnis ablegen und zufällige IDs verwenden. +- [ ] Abgebrochene und veraltete Upload-Fragmente automatisch entfernen. +- [ ] Tests für Symlink-Parents und konkurrierende Pfadänderungen ergänzen. + +Abnahmekriterium: Ein Upload kann außerhalb der erlaubten Roots weder Dateien noch Verzeichnisse erzeugen. + +### CSRF und Request-Schutz + +- [ ] Auf einer aktuellen Unraid-7.x-Installation prüfen, ob die WebGUI-Schicht Plugin-PHP-Endpunkte automatisch gegen CSRF schützt. +- [ ] Direkte POST-Requests ohne, mit falschem und mit gültigem `csrf_token` testen. +- [ ] Falls nötig, CSRF-Prüfung zentral in `common.php` implementieren. +- [ ] Mutierende Endpunkte ausschließlich per POST erlauben. +- [ ] Für nicht erlaubte Methoden `405 Method Not Allowed` und einen `Allow`-Header liefern. +- [ ] Für JSON- und Formular-Requests dieselben Schutzregeln anwenden. +- [ ] Authentifizierungsverhalten bei direktem Aufruf aller API-Endpunkte prüfen und dokumentieren. + +Abnahmekriterium: Kein mutierender Aufruf funktioniert ohne gültige Unraid-Sitzung und gültigen CSRF-Schutz. + +### Ressourcenlimits + +- [ ] Maximale Upload-Dateigröße konfigurierbar machen. +- [ ] Maximale Gesamtgröße und Dateianzahl pro Upload begrenzen. +- [ ] Raw-Uploads streamen, statt den gesamten Request in den PHP-Speicher zu laden. +- [ ] Maximale Dateianzahl und Gesamtgröße für ZIP-Downloads festlegen. +- [ ] Vor ZIP-Erstellung freien temporären Speicher prüfen. +- [ ] ZIP-Erstellung als Hintergrundjob ausführen. +- [ ] Laufzeit- und Rekursionstiefenlimits für Size, Copy, Delete und Permissions einführen. +- [ ] Große Verzeichnisse im Listing paginieren oder begrenzen. + +Abnahmekriterium: Einzelne Requests können weder PHP-Speicher noch temporären Speicher unkontrolliert erschöpfen. + +## P1 – Datenintegrität und Jobs + +### Robuste Copy-/Move-Operationen + +- [ ] Kopien zunächst unter einem temporären Zielnamen erstellen. +- [ ] Ziel erst nach erfolgreicher vollständiger Kopie atomar sichtbar machen. +- [ ] Unvollständige Ziele bei Fehlern zuverlässig entfernen. +- [ ] Quelle bei einem Cross-Filesystem-Move erst nach verifizierter Kopie löschen. +- [ ] Dateigröße und optional Prüfsumme vor dem Löschen der Quelle vergleichen. +- [ ] Rechte, Owner, Gruppe und Zeitstempel erhalten. +- [ ] Verhalten für Sparse Files, Hardlinks und Special Files definieren. +- [ ] Zielkonflikte auch unmittelbar vor dem finalen Rename prüfen. +- [ ] Tests für Fehler mitten in einer Verzeichniskopie ergänzen. + +### Job-System + +- [ ] Job-Verzeichnis mit `0700` statt `0777` erstellen. +- [ ] Jobdateien atomar über temporäre Datei plus Rename schreiben. +- [ ] Beim Lesen passende Locks beziehungsweise atomare Snapshots verwenden. +- [ ] Kryptografisch zufällige Job-IDs verwenden. +- [ ] Prüfen, ob der Worker wirklich gestartet ist; nicht nur den Shell-Exitcode auswerten. +- [ ] Hängende Jobs nach Timeout als fehlgeschlagen markieren. +- [ ] Abbruch-Endpunkt für laufende Jobs implementieren. +- [ ] Abbruchstatus in rekursiven Operationen regelmäßig prüfen. +- [ ] Abbruchschaltfläche im Transfer-Panel ergänzen. +- [ ] Alte abgeschlossene Jobs automatisch löschen. +- [ ] Gleichzeitige, kollidierende Jobs auf denselben Pfaden erkennen und serialisieren oder ablehnen. +- [ ] Jobfehler mit verständlicher Meldung und technischem Logeintrag trennen. + +### Löschschutz + +- [ ] Optionalen Papierkorb pro Root implementieren. +- [ ] Direktes permanentes Löschen als gesonderte Aktion behandeln. +- [ ] Anzahl und geschätzte Größe vor rekursivem Löschen anzeigen. +- [ ] Mehrfachauswahl vor dem Start vollständig validieren. +- [ ] Optional eine zweite Bestätigung für sehr große Löschvorgänge verlangen. + +## P1 – Tests + +### PHP-Backend testen + +- [ ] Eigenständige Tests für die tatsächlich ausgelieferten PHP-Endpunkte aufbauen. +- [ ] Die Tests in einer Unraid-kompatiblen PHP-Version ausführen. +- [ ] Listing innerhalb und außerhalb erlaubter Roots testen. +- [ ] Upload, Download, Preview, Copy, Move, Delete, Size und Permissions testen. +- [ ] Read-only-Modus für jede mutierende Aktion testen. +- [ ] Zielkonflikte und ungültige Request-Methoden testen. +- [ ] Sonderzeichen, Unicode, sehr lange Namen und versteckte Dateien testen. +- [ ] Mehrere konfigurierte Roots testen. +- [ ] Regressionstests für alle P0-Sicherheitsfälle ergänzen. + +### CI und statische Prüfung + +- [ ] `php -l` für alle PHP-Dateien in CI ausführen. +- [ ] JavaScript-Syntaxprüfung ausführen. +- [ ] ShellCheck für `build-plugin.sh` aktivieren. +- [ ] XML beziehungsweise PLG-Manifest validieren. +- [ ] Paket bauen und mit den Quellen vergleichen. +- [ ] Manifest-Prüfsumme automatisch verifizieren. +- [ ] Tests bei jedem Push und Pull Request ausführen. + +### Test auf echter Unraid-Instanz + +- [ ] Installation auf unterstützten Unraid-Versionen testen. +- [ ] Upgrade von einer vorherigen Plugin-Version testen. +- [ ] Deinstallation und Neuinstallation testen. +- [ ] Verhalten bei gestopptem und gestartetem Array prüfen. +- [ ] User Shares, Pools und Disk Shares getrennt prüfen. +- [ ] Große Ordner und große Dateien testen. +- [ ] WebGUI-Neustart und Server-Neustart während laufender Jobs testen. +- [ ] Alle offiziellen Themes und mobile Darstellung prüfen. + +## P2 – Konfiguration und Bedienung + +### Root-Konfiguration + +- [ ] Erlaubte Roots in `settings.json` speichern. +- [ ] Einstellungsoberfläche zum Hinzufügen und Entfernen von Roots bauen. +- [ ] Nur existierende absolute Verzeichnisse akzeptieren. +- [ ] Gefährliche Systempfade wie `/`, `/boot`, `/etc`, `/usr`, `/var` und `/root` standardmäßig verbieten. +- [ ] Pro Root einen Anzeigenamen und Read-only-Status erlauben. +- [ ] Änderungen an Roots erst nach erneuter serverseitiger Validierung übernehmen. +- [ ] Root-Auswahl im Explorer anbieten. + +### Dateimanager-Funktionen + +- [ ] Neuen Ordner anlegen. +- [ ] Leere Datei anlegen. +- [ ] Umbenennen mit serverseitiger Namensvalidierung verbessern. +- [ ] Suche nach Dateiname im aktuellen Ordner implementieren. +- [ ] Optionale rekursive Suche mit Ergebnislimit ergänzen. +- [ ] Sortierung nach Name, Typ, Größe und Änderungsdatum anbieten. +- [ ] Versteckte Dateien ein- und ausblendbar machen. +- [ ] Breadcrumb-Navigation ergänzen. +- [ ] Dateibaum beziehungsweise Share-Navigation ergänzen. +- [ ] Tastaturbedienung für Auswahl, Öffnen, Kopieren, Verschieben und Löschen verbessern. + +### Transfer-Oberfläche + +- [ ] Reale Fortschrittswerte nach Bytes statt nur nach Top-Level-Einträgen berechnen. +- [ ] Quelle, Ziel, Geschwindigkeit und Restzeit anzeigen. +- [ ] Fehlgeschlagene Jobs wiederholbar machen. +- [ ] Abgeschlossene Einträge einzeln oder gesammelt entfernen. +- [ ] Uploads pausieren und abbrechen können. + +### Vorschau und Download + +- [ ] MIME-Typ anhand des Dateiinhalts prüfen, nicht nur anhand der Erweiterung. +- [ ] SVG-Vorschau sicher behandeln oder deaktivieren. +- [ ] Medien- und PDF-Vorschau mit geeigneten Sandbox-Headern testen. +- [ ] HTTP-Range-Requests für große Audio- und Videodateien unterstützen. +- [ ] RFC-konforme UTF-8-Dateinamen im `Content-Disposition`-Header verwenden. +- [ ] Einzeldateien streamen, ohne unnötige Pufferung. + +## P2 – Protokollierung und Betrieb + +- [ ] Audit-Log für Upload, Copy, Move, Delete und Permissions einführen. +- [ ] Zeitpunkt, Aktion, Quelle, Ziel, Ergebnis und Fehler protokollieren. +- [ ] Keine CSRF-Tokens oder sensiblen Inhalte ins Debug-Log schreiben. +- [ ] Logrotation und maximale Loggröße definieren. +- [ ] Diagnoseexport für Supportfälle anbieten. +- [ ] Gesundheitsstatus für Job-Worker und temporären Speicher anzeigen. + +## P3 – Paketierung und Veröffentlichung + +- [ ] Version nur an einer Stelle pflegen und beim Build überall einsetzen. +- [ ] `package.json`-Version mit der Plugin-Version synchronisieren. +- [ ] Reproduzierbare TGZ-Pakete mit stabiler Sortierung und Zeitstempeln erzeugen. +- [ ] Nach Möglichkeit SHA-256 zusätzlich zur MD5-Prüfung veröffentlichen. +- [ ] Minimale und getestete Unraid-Version im Plugin-Manifest angeben. +- [ ] Changelog pro Version vollständig pflegen. +- [ ] Release-Build in einer sauberen Working Copy erzeugen. +- [ ] Community-Applications-Metadaten validieren. +- [ ] Installations-, Update- und Rollback-Anleitung ergänzen. +- [ ] Sicherheitsmodell und bekannte Einschränkungen dokumentieren. +- [ ] Read-only als empfohlenen Standard deutlich in README und Oberfläche anzeigen. + +## Definition of Done für die erste produktive Version + +- [ ] Alle P0-Punkte sind abgeschlossen. +- [ ] PHP-Integrationstests decken alle mutierenden Operationen ab. +- [ ] Keine Root- oder Symlink-Regression ist offen. +- [ ] CSRF und Authentifizierung sind auf echter Unraid-Hardware bestätigt. +- [ ] Große Uploads, Downloads und Verzeichnisse besitzen getestete Limits. +- [ ] Fehlgeschlagene Copy-/Move-Jobs hinterlassen keine unbemerkten Teildaten. +- [ ] Installation, Upgrade, Neustart und Deinstallation wurden getestet. +- [ ] Paketinhalt und Manifest-Prüfsumme stimmen überein. +- [ ] Dokumentation und Changelog entsprechen dem ausgelieferten Funktionsumfang. diff --git a/build-plugin.sh b/build-plugin.sh index f4bfe08..ba7c4d3 100755 --- a/build-plugin.sh +++ b/build-plugin.sh @@ -2,7 +2,7 @@ set -eu PLUGIN="u-navigator" -VERSION="2026.07.04.r007" +VERSION="2026.07.14.r008" PKG_DIR="packages" WORK_DIR=".plugin-build" PKG_NAME="${PLUGIN}-${VERSION}.tgz" diff --git a/packages/u-navigator-2026.07.14.r008.tgz b/packages/u-navigator-2026.07.14.r008.tgz new file mode 100644 index 0000000..4d5e7d8 Binary files /dev/null and b/packages/u-navigator-2026.07.14.r008.tgz differ diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/U-Navigator.page b/plugin-root/usr/local/emhttp/plugins/u-navigator/U-Navigator.page index 08ac58d..24f2bba 100644 --- a/plugin-root/usr/local/emhttp/plugins/u-navigator/U-Navigator.page +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/U-Navigator.page @@ -11,26 +11,7 @@ $plugin = "u-navigator"; $version = "__VERSION__"; ?> -
-
-
-

U-Navigator

-

Initialisiere...

-
- -
-
-
+
diff --git a/server/public/styles.css b/server/public/styles.css index fa35f33..948ad27 100644 --- a/server/public/styles.css +++ b/server/public/styles.css @@ -1,1110 +1,188 @@ .u-nav { - --bg: #f4f6f8; + --accent: #ff8a3d; + --accent-deep: #e96820; + --accent-soft: #fff1e7; + --bg: #eef1f5; --surface: #ffffff; - --surface-2: #eef2f5; - --text: #18222d; - --muted: #617182; - --line: #cfd8e1; - --accent: #e86f2d; - --accent-2: #287a65; - --shadow: 0 18px 55px rgba(24, 34, 45, 0.22); + --surface-raised: #ffffff; + --surface-soft: #f6f7f9; + --surface-hover: #f0f3f7; + --text: #16202b; + --text-soft: #425466; + --muted: #7b8997; + --line: #e2e7ec; + --line-strong: #d3dae2; + --danger: #db4b4b; + --success: #218f68; + --shadow-sm: 0 1px 2px rgba(18, 32, 47, .05), 0 2px 8px rgba(18, 32, 47, .04); + --shadow-lg: 0 24px 70px rgba(21, 34, 50, .18); background: var(--bg); color: var(--text); - display: grid; - font: 12.5px/1.35 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - grid-template-rows: auto 1fr; - min-height: calc(100vh - 170px); + font: 14px/1.45 Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + min-height: calc(100vh - 150px); overflow: hidden; position: relative; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; } @media (prefers-color-scheme: dark) { .u-nav { - --bg: #101418; - --surface: #1b2229; - --surface-2: #242e37; - --text: #eef3f7; - --muted: #a3b0bd; - --line: #36434f; - --shadow: 0 18px 55px rgba(0, 0, 0, 0.44); + --accent-soft: #38271d; + --bg: #0f1419; + --surface: #171d23; + --surface-raised: #1c232b; + --surface-soft: #202830; + --surface-hover: #252f39; + --text: #f2f5f7; + --text-soft: #c2ccd5; + --muted: #8795a3; + --line: #2a343e; + --line-strong: #394552; + --shadow-sm: 0 1px 2px rgba(0, 0, 0, .22); + --shadow-lg: 0 24px 70px rgba(0, 0, 0, .5); } } -.u-nav *, -.u-nav *::before, -.u-nav *::after { - box-sizing: border-box; -} - -.u-nav button { - background: var(--surface); - border: 1px solid var(--line); - border-radius: 6px; - color: var(--text); - cursor: pointer; - font: inherit; - min-height: 28px; - padding: 0 8px; -} - -.u-nav button:hover { - border-color: var(--accent); -} - -.u-nav button.primary { - background: var(--accent); - border-color: var(--accent); - color: #ffffff; -} - -.u-nav button.danger { - color: #c4382d; -} - -.u-nav button.danger:hover { - border-color: #c4382d; -} - -.u-nav button.icon { - align-items: center; - display: inline-flex; - flex: 0 0 26px; - height: 26px; - justify-content: center; - line-height: 1; - max-width: 26px; - min-height: 26px; - min-width: 26px; - padding: 0; - width: 26px; -} - -.u-nav button.icon::before, -.u-nav button.icon::after { - content: none !important; -} - -.u-nav .ui-icon { - display: block; - fill: none; - height: 16px; - pointer-events: none; - stroke: currentColor; - stroke-linecap: round; - stroke-linejoin: round; - stroke-width: 2.1; - width: 16px; -} - -.u-nav .topbar { - align-items: center; - background: var(--surface); - border-bottom: 1px solid var(--line); - display: flex; - gap: 12px; - justify-content: space-between; - padding: 9px 12px; -} - -.u-nav .topbar h1 { - color: var(--text); - font-size: 16px; - line-height: 1; - margin: 0 0 4px; -} - -.u-nav .topbar p, -.u-nav .muted { - color: var(--muted); - margin: 0; -} - -.u-nav .toolbar { - display: flex; - flex-wrap: wrap; - gap: 6px; -} - -.u-nav .toolbar button.active { - background: var(--surface-2); - border-color: var(--accent); -} - -.u-nav .workspace { - min-height: 0; - overflow: hidden; - position: relative; -} - -.u-nav .window { - background: var(--surface); - border: 1px solid var(--line); - border-radius: 8px; - box-shadow: var(--shadow); - display: grid; - grid-template-rows: auto 1fr; - min-height: 240px; - min-width: 280px; - overflow: hidden; - position: absolute; -} - -.u-nav .window.focused { - border-color: color-mix(in srgb, var(--accent) 62%, var(--line)); -} - -.u-nav .window.maximized { - border-radius: 0; -} - -.u-nav .titlebar { - align-items: center; - background: var(--surface-2); - border-bottom: 1px solid var(--line); - cursor: move; - display: grid; - gap: 6px; - grid-template-columns: 1fr auto; - min-height: 34px; - padding: 4px 6px 4px 10px; - user-select: none; -} - -.u-nav .titlebar strong { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.u-nav .window-actions { - display: flex; - gap: 5px; -} - -.u-nav .window-body { - min-height: 0; - overflow: auto; - padding: 8px; -} - -.u-nav .resize-handle { - bottom: 0; - cursor: nwse-resize; - height: 18px; - position: absolute; - right: 0; - width: 18px; -} - -.u-nav .resize-handle::after { - border-bottom: 2px solid var(--muted); - border-right: 2px solid var(--muted); - bottom: 5px; - content: ""; - height: 8px; - position: absolute; - right: 5px; - width: 8px; -} - -.u-nav .pathbar { - align-items: center; - display: flex; - flex-wrap: wrap; - gap: 4px; - margin-bottom: 8px; -} - -.u-nav .pathbar input:not(.upload-input) { - background: var(--surface-2); - border: 1px solid var(--line); - border-radius: 6px; - color: var(--text); - font: inherit; - min-height: 28px; - min-width: 180px; - padding: 0 8px; - flex: 1 1 220px; -} - -.u-nav .pathbar .up-button { - flex: 0 0 24px; - font-size: 15px; - line-height: 1; -} - -.u-nav .pathbar > button.icon, -.u-nav .pathbar .view-toggle { - flex: 0 0 auto; -} - -.u-nav .pathbar > button.icon { - max-width: 32px; - min-width: 32px; - width: 32px; -} - -.u-nav .upload-input { - display: none; -} - -.u-nav .drop-zone { - border: 1px dashed transparent; - border-radius: 8px; - min-height: 150px; -} - -.u-nav .drop-zone.drag-over { - background: color-mix(in srgb, var(--accent) 18%, transparent); - border-color: var(--accent); - border-style: solid; - box-shadow: inset 0 0 0 2px var(--accent); -} - -.u-nav .view-toggle { - display: inline-flex; - gap: 0; - white-space: nowrap; -} - -.u-nav .view-toggle button { - border-radius: 0; - flex: 0 0 auto; - min-width: 24px; - overflow: visible; -} - -.u-nav .view-toggle button:first-child { - border-radius: 6px 0 0 6px; -} - -.u-nav .view-toggle button:last-child { - border-radius: 0 6px 6px 0; - margin-left: -1px; -} - -.u-nav .view-toggle button.active { - background: var(--surface-2); - border-color: var(--accent); - position: relative; - z-index: 1; -} - -.u-nav .file-table { - border-collapse: collapse; - background: var(--surface); - color: var(--text); - width: 100%; -} - -.u-nav .file-table th, -.u-nav .file-table td { - background: var(--surface) !important; - border-bottom: 1px solid var(--line); - color: var(--text) !important; - padding: 6px 6px; - text-align: left; - vertical-align: middle; -} - -.u-nav .file-table th { - background: var(--surface-2) !important; - color: var(--muted); - font-size: 11px; - font-weight: 600; -} - -.u-nav .file-row { - background: var(--surface); - color: var(--text); - cursor: default; - -webkit-user-drag: none; -} - -.u-nav .file-row:hover, -.u-nav .file-row.selected, -.u-nav .file-row.dragging, -.u-nav .file-row.drop-candidate, -.u-nav .file-row.drop-target { - background: var(--surface-2); -} - -.u-nav .file-row:hover td, -.u-nav .file-row.selected td, -.u-nav .file-row.dragging td, -.u-nav .file-row.drop-candidate td, -.u-nav .file-row.drop-target td { - background: var(--surface-2) !important; -} - -.u-nav .file-row.drop-candidate td:first-child { - box-shadow: inset 3px 0 0 color-mix(in srgb, var(--accent) 72%, transparent); -} - -.u-nav .file-row { - -webkit-user-drag: none; - user-select: none; -} - -.u-nav .file-row.dragging { - opacity: 0.55; -} - -.u-nav-dragging { - cursor: grabbing; -} - -.u-nav .name-cell { - align-items: center; - display: flex; - gap: 6px; - -webkit-user-drag: none; - min-width: 130px; - user-select: none; -} - -.u-nav .badge { - border: 1px solid var(--line); - border-radius: 999px; - color: var(--muted); - display: inline-flex; - font-size: 11px; - justify-content: center; - min-width: 50px; - padding: 1px 7px; - -webkit-user-drag: none; - user-select: none; -} - -.u-nav .icon-grid { - display: grid; - gap: 8px; - grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); - padding: 2px; -} - -.u-nav .icon-item { - align-items: center; - background: var(--surface); - border: 1px solid transparent; - border-radius: 8px; - color: var(--text); - display: grid; - gap: 5px; - grid-template-rows: auto minmax(28px, auto) auto; - justify-items: center; - min-height: 108px; - padding: 8px 6px; - text-align: center; - width: 100%; -} - -.u-nav .icon-item:hover, -.u-nav .icon-item.selected, -.u-nav .icon-item.dragging, -.u-nav .icon-item.drop-candidate, -.u-nav .icon-item.drop-target { - background: var(--surface-2); - border-color: var(--line); -} - -.u-nav .icon-item.selected, -.u-nav .icon-item.drop-candidate, -.u-nav .icon-item.drop-target { - border-color: var(--accent); -} - -.u-nav .icon-item.drop-candidate { - box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent); -} - -.u-nav .file-icon { - display: block; - height: 46px; - position: relative; - width: 56px; -} - -.u-nav .file-icon.directory { - background: linear-gradient(180deg, #ff9b4f 0%, var(--accent) 100%); - border: 1px solid color-mix(in srgb, var(--accent) 78%, #5b2b12); - border-radius: 7px 7px 6px 6px; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.34), 0 5px 12px rgba(24, 34, 45, 0.14); - height: 34px; - margin-top: 8px; -} - -.u-nav .file-icon.directory::before { - background: linear-gradient(180deg, #ffb16e 0%, #f58a3a 100%); - border: 1px solid color-mix(in srgb, var(--accent) 72%, #5b2b12); - border-bottom: 0; - border-radius: 6px 6px 0 0; - content: ""; - height: 10px; - left: 5px; - position: absolute; - top: -8px; - width: 24px; -} - -.u-nav .file-icon.directory::after { - background: rgba(255, 255, 255, 0.18); - border-radius: 999px; - content: ""; - height: 3px; - left: 8px; - position: absolute; - right: 8px; - top: 8px; -} - -.u-nav .file-icon.file, -.u-nav .file-icon.symlink, -.u-nav .file-icon.other { - background: linear-gradient(180deg, #ffffff 0%, #eef3f7 100%); - border: 1px solid var(--line); - border-radius: 6px; - box-shadow: 0 5px 12px rgba(24, 34, 45, 0.12); - height: 44px; - width: 36px; -} - -.u-nav .file-icon.file::before, -.u-nav .file-icon.symlink::before, -.u-nav .file-icon.other::before { - background: linear-gradient(135deg, #dce5ee 0%, #ffffff 48%, #cfd8e1 49%, #cfd8e1 100%); - border-left: 1px solid var(--line); - border-bottom: 1px solid var(--line); - border-radius: 0 5px 0 4px; - content: ""; - height: 11px; - position: absolute; - right: -1px; - top: -1px; - width: 11px; -} - -.u-nav .file-icon.file::after, -.u-nav .file-icon.symlink::after, -.u-nav .file-icon.other::after { - background: - linear-gradient(var(--muted), var(--muted)) 0 0 / 18px 2px no-repeat, - linear-gradient(var(--muted), var(--muted)) 0 8px / 22px 2px no-repeat, - linear-gradient(var(--muted), var(--muted)) 0 16px / 15px 2px no-repeat; - content: ""; - height: 20px; - left: 7px; - opacity: 0.58; - position: absolute; - top: 16px; - width: 22px; -} - -.u-nav .icon-name { - color: var(--text); - display: -webkit-box; - font-size: 12px; - line-height: 1.25; - max-width: 100%; - overflow: hidden; - overflow-wrap: anywhere; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; -} - -.u-nav .icon-meta { - color: var(--muted); - font-size: 11px; -} - -.u-nav .context-menu { - background: var(--surface); - border: 1px solid var(--line); - border-radius: 8px; - box-shadow: var(--shadow); - display: grid; - min-width: 170px; - padding: 5px; - position: absolute; - z-index: 100000; -} - -.u-nav .context-menu button { - border-color: transparent; - justify-content: flex-start; - text-align: left; - width: 100%; -} - -.u-nav .context-menu button:hover { - background: var(--surface-2); - border-color: var(--line); -} - -.u-nav .modal-overlay { - align-items: center; - background: rgba(10, 14, 18, 0.42); - display: flex; - inset: 0; - justify-content: center; - padding: 14px; - position: absolute; - z-index: 100020; -} - -.u-nav .permissions-modal { - background: var(--surface); - border: 1px solid var(--line); - border-radius: 8px; - box-shadow: var(--shadow); - color: var(--text); - display: grid; - max-height: min(580px, 100%); - max-width: 100%; - min-width: min(380px, 100%); - overflow: hidden; - width: 420px; -} - -.u-nav .permissions-modal header, -.u-nav .permissions-modal footer { - align-items: center; - background: var(--surface-2); - display: flex; - gap: 6px; - justify-content: space-between; - padding: 6px 8px 6px 10px; -} - -.u-nav .permissions-modal header { - border-bottom: 1px solid var(--line); -} - -.u-nav .permissions-modal footer { - border-top: 1px solid var(--line); - justify-content: flex-end; -} - -.u-nav .permissions-modal header .icon { - flex: 0 0 26px; - min-width: 26px; - width: 26px; -} - -.u-nav .modal-body { - display: grid; - gap: 7px; - overflow: auto; - padding: 10px; -} - -.u-nav .permissions-modal input[type="text"], -.u-nav .permissions-modal input:not([type]) { - background: var(--surface-2); - border: 1px solid var(--line); - border-radius: 6px; - color: var(--text); - font: inherit; - min-height: 28px; - padding: 0 8px; - width: 100%; -} - -.u-nav .permissions-modal input:disabled { - opacity: 0.48; -} - -.u-nav .conflict-modal { - width: min(560px, calc(100vw - 28px)); -} - -.u-nav .conflict-modal footer { - display: grid; - gap: 8px; - grid-template-columns: repeat(3, minmax(0, 1fr)); - justify-content: stretch; -} - -.u-nav .conflict-modal footer button { - line-height: 1.15; - min-width: 0; - overflow-wrap: anywhere; - padding: 0 7px; - white-space: normal; - width: 100%; -} - -.u-nav .ui-modal { - width: min(460px, calc(100vw - 28px)); -} - -.u-nav .ui-modal .modal-body p { - margin: 0; - overflow-wrap: anywhere; -} - -.u-nav .ui-modal footer { - gap: 8px; -} - -.u-nav .ui-modal .danger-action { - background: #c4382d; - border-color: #c4382d; -} - -.u-nav .conflict-details { - background: var(--surface-2); - border: 1px solid var(--line); - border-radius: 6px; - display: grid; - gap: 6px 10px; - grid-template-columns: auto minmax(0, 1fr); - margin: 0; - padding: 8px; -} - -.u-nav .conflict-details dt { - color: var(--muted); - font-weight: 700; -} - -.u-nav .conflict-details dd { - margin: 0; - overflow-wrap: anywhere; -} - -.u-nav .check-row { - align-items: center; - display: flex; - gap: 7px; -} - -.u-nav .check-row input { - accent-color: var(--accent); - height: 16px; - margin: 0; - width: 16px; -} - -.u-nav .check-row.disabled { - opacity: 0.5; -} - -.u-nav .permission-scope { - background: var(--surface-2); - border: 1px solid var(--line); - border-radius: 6px; - display: grid; - gap: 6px; - grid-template-columns: repeat(3, minmax(0, 1fr)); - margin: 0; - padding: 8px; -} - -.u-nav .permission-scope legend { - color: var(--text); - font-size: 12px; - font-weight: 600; - padding: 0 4px; -} - -.u-nav .permission-scope label { - align-items: center; - display: flex; - gap: 5px; - white-space: nowrap; -} - -.u-nav .permission-scope input { - accent-color: var(--accent); - height: 15px; - margin: 0; - width: 15px; -} - -.u-nav .field-row { - align-items: center; - display: grid; - gap: 6px; - grid-template-columns: max-content minmax(0, 1fr); -} - -.u-nav .mode-input { - font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace !important; -} - -.u-nav .transfer-panel { - background: var(--surface); - border: 1px solid var(--line); - border-radius: 8px; - box-shadow: var(--shadow); - display: grid; - grid-template-rows: auto minmax(0, 1fr); - max-height: min(480px, calc(100% - 68px)); - min-height: 190px; - min-width: 280px; - overflow: hidden; - position: absolute; - transform-origin: top right; - z-index: 10000; -} - -.u-nav .transfer-panel.open { - animation: transfer-slide-down 180ms ease-out both; -} - -.u-nav .transfer-panel.closing { - animation: transfer-slide-up 180ms ease-in both; - pointer-events: none; -} - -.u-nav .transfer-panel-header { - align-items: center; - background: var(--surface-2); - border-bottom: 1px solid var(--line); - display: flex; - justify-content: space-between; - min-height: 34px; - padding: 4px 6px 4px 10px; -} - -.u-nav .transfer-panel-body { - min-height: 0; - overflow: auto; - padding: 8px; -} - -.u-nav .settings-panel { - animation: transfer-slide-down 160ms ease-out both; - background: var(--surface); - border: 1px solid var(--line); - border-radius: 8px; - box-shadow: var(--shadow); - min-width: 280px; - overflow: hidden; - position: absolute; - transform-origin: top right; - width: min(340px, calc(100% - 32px)); - z-index: 10010; -} - -.u-nav .settings-panel-header { - align-items: center; - background: var(--surface-2); - border-bottom: 1px solid var(--line); - display: flex; - justify-content: space-between; - min-height: 34px; - padding: 4px 6px 4px 10px; -} - -.u-nav .settings-panel-body { - display: grid; - gap: 2px; - padding: 6px; -} - -.u-nav .setting-row { - align-items: center; - border-radius: 6px; - display: grid; - gap: 8px; - grid-template-columns: 1fr auto; - padding: 8px; -} - -.u-nav .setting-row:hover { - background: var(--surface-2); -} - -.u-nav .setting-row small { - color: var(--muted); - display: block; - margin-top: 2px; -} - -.u-nav .setting-row input { - accent-color: var(--accent); - height: 16px; - width: 16px; -} - -@keyframes transfer-slide-down { - from { - opacity: 0; - transform: translateY(-18px) scaleY(0.96); - } - - to { - opacity: 1; - transform: translateY(0) scaleY(1); - } -} - -@keyframes transfer-slide-up { - from { - opacity: 1; - transform: translateY(0) scaleY(1); - } - - to { - opacity: 0; - transform: translateY(-18px) scaleY(0.96); - } -} - -.u-nav .queue-list { - display: grid; - gap: 10px; -} - -.u-nav .debug-log { - border-top: 1px solid var(--line); - display: grid; - gap: 6px; - margin-top: 10px; - padding-top: 10px; -} - -.u-nav .debug-log header { - align-items: center; - display: flex; - justify-content: space-between; -} - -.u-nav .debug-entry { - background: var(--surface-2); - border: 1px solid var(--line); - border-radius: 6px; - padding: 7px; -} - -.u-nav .debug-entry time { - color: var(--muted); - display: block; - font-size: 11px; -} - -.u-nav .debug-entry pre { - color: var(--text); - font: 11px/1.3 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; - margin: 5px 0 0; - max-height: 120px; - overflow: auto; - white-space: pre-wrap; - word-break: break-word; -} - -.u-nav .job { - border: 1px solid var(--line); - border-radius: 8px; - padding: 8px; -} - -.u-nav .job .muted { - overflow-wrap: anywhere; -} - -.u-nav .upload-job { - border-color: color-mix(in srgb, var(--accent-2) 45%, var(--line)); -} - -.u-nav .progress { - background: var(--surface-2); - border-radius: 999px; - height: 6px; - margin-top: 6px; - overflow: hidden; -} - -.u-nav .progress div { - background: var(--accent-2); - height: 100%; - width: 0; -} - -.u-nav .properties dl { - display: grid; - gap: 6px 12px; - grid-template-columns: max-content 1fr; - margin: 0; -} - -.u-nav .properties dt { - color: var(--muted); -} - -.u-nav .properties dd { - margin: 0; - overflow-wrap: anywhere; -} - -.u-nav .preview-body { - display: grid; - gap: 8px; - grid-template-rows: auto minmax(0, 1fr); -} - -.u-nav .preview-meta { - align-items: center; - border-bottom: 1px solid var(--line); - color: var(--muted); - display: grid; - gap: 8px; - grid-template-columns: auto minmax(0, 1fr) auto; - padding-bottom: 6px; -} - -.u-nav .preview-nav { - display: inline-flex; - gap: 4px; -} - -.u-nav .preview-nav button:disabled { - cursor: default; - opacity: 0.42; -} - -.u-nav .preview-meta strong { - color: var(--text); - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.u-nav .preview-text { - background: var(--surface-2); - border: 1px solid var(--line); - border-radius: 6px; - color: var(--text); - font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; - margin: 0; - min-height: 0; - overflow: auto; - padding: 8px; - white-space: pre; -} - -.u-nav .preview-rendered, -.u-nav .preview-table-wrap, -.u-nav .preview-media, -.u-nav .preview-frame { - min-height: 0; -} - -.u-nav .preview-rendered { - background: var(--surface); - border: 1px solid var(--line); - border-radius: 6px; - overflow: auto; - padding: 10px; -} - -.u-nav .preview-rendered h1, -.u-nav .preview-rendered h2, -.u-nav .preview-rendered h3, -.u-nav .preview-rendered p, -.u-nav .preview-rendered ul { - margin-top: 0; -} - -.u-nav .preview-rendered code { - background: var(--surface-2); - border-radius: 4px; - font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; - padding: 1px 3px; -} - -.u-nav .preview-table-wrap { - border: 1px solid var(--line); - border-radius: 6px; - overflow: auto; -} - -.u-nav .preview-table { - border-collapse: collapse; - min-width: 100%; -} - -.u-nav .preview-table td { - border-bottom: 1px solid var(--line); - border-right: 1px solid var(--line); - padding: 5px 7px; - white-space: nowrap; -} - -.u-nav .preview-media { - align-items: center; - background: var(--surface-2); - border: 1px solid var(--line); - border-radius: 6px; - display: flex; - justify-content: center; - overflow: auto; - padding: 8px; -} - -.u-nav .preview-media img, -.u-nav .preview-media video { - max-height: 100%; - max-width: 100%; -} - -.u-nav .preview-media audio { - width: min(100%, 560px); -} - -.u-nav .preview-frame { - border: 1px solid var(--line); - border-radius: 6px; - height: 100%; - width: 100%; -} - -@media (max-width: 720px) { - .u-nav .topbar { - align-items: flex-start; - flex-direction: column; - } - - .u-nav .workspace { - overflow: auto; - } - - .u-nav .window, - .u-nav .window.maximized { - border-left: 0; - border-radius: 0; - border-right: 0; - height: calc(100vh - 120px) !important; - left: 0 !important; - min-width: 0; - position: relative; - top: 0 !important; - width: 100% !important; - } - - .u-nav .window:not(.focused) { - display: none; - } - - .u-nav .resize-handle { - display: none; - } - - .u-nav .transfer-panel { - left: 10px !important; - max-height: min(70vh, 520px); - min-width: 0; - right: 10px !important; - width: auto !important; - } - - .u-nav .settings-panel { - left: 10px !important; - min-width: 0; - right: 10px !important; - width: auto !important; - } +.u-nav *, .u-nav *::before, .u-nav *::after { box-sizing: border-box; } +.u-nav button, .u-nav input { font: inherit; } +.u-nav button { color: inherit; } +.u-nav button:focus-visible, .u-nav input:focus-visible { outline: 2px solid color-mix(in srgb, var(--accent) 70%, transparent); outline-offset: 2px; } +.u-nav .ui-icon { display: block; fill: none; height: 20px; pointer-events: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.8; width: 20px; } + +.unav-frame { background: var(--surface); display: grid; grid-template-rows: 66px minmax(0, 1fr); height: calc(100vh - 150px); min-height: 660px; overflow: hidden; } +.app-header { align-items: center; background: var(--surface); border-bottom: 1px solid var(--line); display: grid; gap: 28px; grid-template-columns: 230px minmax(260px, 620px) 1fr; padding: 0 20px; position: relative; z-index: 20; } +.brand { align-items: center; background: none; border: 0; cursor: pointer; display: flex; gap: 11px; padding: 0; text-align: left; } +.brand-mark { align-items: center; background: linear-gradient(145deg, #ff9c58, #ee6f26); border-radius: 12px; box-shadow: 0 6px 16px rgba(232, 105, 32, .25); color: #fff; display: flex; height: 38px; justify-content: center; width: 38px; } +.brand-mark .ui-icon { height: 22px; stroke-width: 2; width: 22px; } +.brand strong, .brand small { display: block; } +.brand strong { font-size: 16px; letter-spacing: -.02em; } +.brand small { color: var(--muted); font-size: 11px; letter-spacing: .07em; text-transform: uppercase; } +.header-search { align-items: center; background: var(--surface-soft); border: 1px solid transparent; border-radius: 12px; color: var(--muted); display: grid; gap: 10px; grid-template-columns: auto 1fr auto; height: 40px; padding: 0 12px; transition: .18s ease; } +.header-search:focus-within { background: var(--surface); border-color: color-mix(in srgb, var(--accent) 48%, var(--line)); box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 10%, transparent); } +.header-search .ui-icon { height: 18px; width: 18px; } +.header-search input { background: transparent; border: 0; color: var(--text); min-width: 0; outline: 0; } +.header-search input::placeholder { color: var(--muted); } +.header-search kbd { background: var(--surface); border: 1px solid var(--line); border-radius: 5px; box-shadow: 0 1px 0 var(--line-strong); color: var(--muted); font: 11px/1 system-ui; padding: 4px 6px; } +.header-actions { display: flex; gap: 7px; justify-content: flex-end; } + +.icon-button { align-items: center; background: transparent; border: 1px solid transparent; border-radius: 9px; cursor: pointer; display: inline-flex; height: 36px; justify-content: center; padding: 0; position: relative; transition: .15s ease; width: 36px; } +.icon-button:hover, .icon-button.active { background: var(--surface-hover); border-color: var(--line); } +.icon-button:disabled { cursor: default; opacity: .3; } +.action-badge { align-items: center; background: var(--accent); border: 2px solid var(--surface); border-radius: 10px; color: #fff; display: flex; font-size: 9px; height: 17px; justify-content: center; min-width: 17px; padding: 0 3px; position: absolute; right: -3px; top: -3px; } + +.browser-shell { display: grid; grid-template-columns: 236px minmax(0, 1fr); min-height: 0; } +.sidebar { background: var(--surface-soft); border-right: 1px solid var(--line); display: flex; flex-direction: column; min-height: 0; overflow-y: auto; padding: 22px 13px 14px; } +.sidebar-section + .sidebar-section { margin-top: 28px; } +.sidebar-label { color: var(--muted); font-size: 10px; font-weight: 750; letter-spacing: .1em; margin: 0 10px 8px; text-transform: uppercase; } +.place { align-items: center; background: transparent; border: 0; border-radius: 10px; cursor: pointer; display: grid; gap: 10px; grid-template-columns: auto minmax(0, 1fr) auto; padding: 8px 9px; text-align: left; transition: .15s ease; width: 100%; } +.place:hover { background: var(--surface-hover); } +.place.active { background: var(--surface); box-shadow: var(--shadow-sm); } +.place > span:nth-child(2) { min-width: 0; } +.place strong, .place small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.place strong { font-size: 13px; } +.place small { color: var(--muted); font-size: 10px; margin-top: 2px; } +.place > .ui-icon { color: var(--muted); height: 15px; width: 15px; } +.place-icon { align-items: center; border-radius: 9px; display: flex; height: 34px; justify-content: center; width: 34px; } +.place-icon .ui-icon { height: 18px; width: 18px; } +.tone-1 { background: #fff0e7; color: #ee762d; }.tone-2 { background: #e8f4ff; color: #3486cc; }.tone-3 { background: #e9f7f1; color: #248961; }.tone-4 { background: #f3ebff; color: #8453c8; } +.place.compact { grid-template-columns: auto minmax(0, 1fr); padding: 7px 11px; } +.place.compact > .ui-icon { height: 17px; width: 17px; } +.sidebar-empty { color: var(--muted); font-size: 11px; margin: 8px 11px; } +.sidebar-foot { align-items: center; border-top: 1px solid var(--line); color: var(--muted); display: flex; font-size: 11px; gap: 8px; margin-top: auto; padding: 16px 10px 2px; } +.mode-dot { background: var(--muted); border-radius: 50%; height: 7px; width: 7px; }.mode-dot.writable { background: var(--success); }.mode-dot.readonly { background: #e3a232; } + +.content-shell { display: grid; grid-template-rows: 52px 52px minmax(0, 1fr) 30px; min-width: 0; min-height: 0; } +.nav-bar { align-items: center; border-bottom: 1px solid var(--line); display: grid; gap: 8px; grid-template-columns: auto minmax(0, 1fr) auto; padding: 0 16px; } +.history-actions { display: flex; gap: 2px; } +.breadcrumbs { align-items: center; display: flex; min-width: 0; overflow: hidden; padding-left: 4px; } +.breadcrumbs button { background: none; border: 0; border-radius: 7px; cursor: pointer; font-size: 13px; font-weight: 550; max-width: 180px; overflow: hidden; padding: 6px 8px; text-overflow: ellipsis; white-space: nowrap; } +.breadcrumbs button:hover { background: var(--surface-hover); } +.breadcrumbs button[aria-current="page"] { color: var(--text); font-weight: 700; } +.crumb-separator { color: var(--muted); font-size: 18px; } +.command-bar { align-items: center; border-bottom: 1px solid var(--line); display: flex; gap: 12px; justify-content: space-between; padding: 0 16px; } +.primary-actions, .view-actions { align-items: center; display: flex; gap: 4px; } +.action { align-items: center; background: transparent; border: 1px solid transparent; border-radius: 8px; cursor: pointer; display: inline-flex; gap: 7px; height: 34px; justify-content: center; padding: 0 10px; transition: .15s ease; } +.action:hover { background: var(--surface-hover); border-color: var(--line); } +.action:disabled { cursor: default; opacity: .32; } +.action .ui-icon { height: 17px; width: 17px; } +.action.primary { background: var(--accent); border-color: var(--accent); box-shadow: 0 5px 14px rgba(232, 105, 32, .18); color: #fff; } +.action.primary:hover { background: var(--accent-deep); } +.action.danger { color: var(--danger); } +.action.full { width: 100%; } +.command-separator { background: var(--line); height: 22px; margin: 0 4px; width: 1px; } +.view-actions .icon-button { height: 32px; width: 32px; } +.item-count { color: var(--muted); font-size: 11px; margin-right: 8px; } + +.explorer-stage { display: grid; grid-template-columns: minmax(0, 1fr) 310px; min-height: 0; } +.file-pane { min-width: 0; overflow: auto; position: relative; } +.file-pane.drag-over::after { align-items: center; background: color-mix(in srgb, var(--accent) 9%, var(--surface)); border: 2px dashed var(--accent); border-radius: 14px; color: var(--accent-deep); content: "Hier ablegen zum Hochladen"; display: flex; font-size: 16px; font-weight: 700; inset: 12px; justify-content: center; pointer-events: none; position: absolute; z-index: 4; } +.details-pane { background: var(--surface-soft); border-left: 1px solid var(--line); min-width: 0; overflow: auto; padding: 18px; transition: .2s ease; } +.explorer-stage:has(.details-pane.closed) { grid-template-columns: minmax(0, 1fr) 0; } +.details-pane.closed { border: 0; padding: 0; } +.status-bar { align-items: center; background: var(--surface-soft); border-top: 1px solid var(--line); color: var(--muted); display: grid; font-size: 10px; grid-template-columns: 1fr auto auto; gap: 18px; padding: 0 16px; } +.version { font-variant-numeric: tabular-nums; } + +.file-table { min-width: 760px; } +.file-head, .file-row { align-items: center; display: grid; grid-template-columns: 42px minmax(250px, 1fr) 110px 130px 160px 40px; } +.file-head { background: color-mix(in srgb, var(--surface) 92%, var(--surface-soft)); border-bottom: 1px solid var(--line); color: var(--muted); height: 38px; position: sticky; top: 0; z-index: 3; } +.select-all, .row-check { align-items: center; background: transparent; border: 1px solid var(--line-strong); border-radius: 5px; cursor: pointer; display: flex; height: 16px; justify-content: center; margin: auto; padding: 0; width: 16px; } +.select-all .ui-icon, .row-check .ui-icon { color: #fff; height: 12px; stroke-width: 2.5; width: 12px; } +.file-row.selected .row-check, .file-head .select-all:not(:empty) { background: var(--accent); border-color: var(--accent); } +.column-title { align-items: center; align-self: stretch; background: transparent; border: 0; color: inherit; cursor: pointer; display: flex; font-size: 10px; font-weight: 700; gap: 5px; letter-spacing: .04em; padding: 0 9px; text-align: left; text-transform: uppercase; } +.column-title:hover, .column-title.active { color: var(--text-soft); } +.file-row { border-bottom: 1px solid color-mix(in srgb, var(--line) 72%, transparent); min-height: 56px; padding-right: 6px; transition: background .12s ease; } +.file-row:hover { background: var(--surface-hover); } +.file-row.selected { background: color-mix(in srgb, var(--accent) 8%, var(--surface)); } +.name-cell { align-items: center; display: flex; gap: 11px; min-width: 0; padding: 7px 9px; } +.name-cell > span:last-child { min-width: 0; } +.name-cell strong, .name-cell small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.name-cell strong { font-size: 13px; font-weight: 620; } +.name-cell small { color: var(--muted); font-size: 10px; margin-top: 2px; } +.file-type-icon { align-items: center; border-radius: 9px; display: flex; flex: 0 0 34px; height: 34px; justify-content: center; width: 34px; } +.file-type-icon .ui-icon { height: 19px; width: 19px; } +.file-folder { background: #fff0e2; color: #ef8b31; }.file-file { background: #edf1f5; color: #6f8193; }.file-image { background: #e8f5ee; color: #278a62; }.file-video { background: #f1eafd; color: #8052bc; }.file-music { background: #e8f2ff; color: #3c80c4; }.file-archive { background: #fff5d9; color: #a87918; }.file-code { background: #e8f5f6; color: #287f87; }.file-link { background: #edf0ff; color: #5d70ca; } +.meta-cell { color: var(--text-soft); font-size: 12px; padding: 0 9px; } +.row-more { align-items: center; background: transparent; border: 0; border-radius: 7px; cursor: pointer; display: flex; height: 30px; justify-content: center; opacity: 0; padding: 0; width: 30px; } +.file-row:hover .row-more, .row-more:focus { opacity: 1; }.row-more:hover { background: var(--surface); }.row-more .ui-icon { height: 17px; width: 17px; } +mark { background: color-mix(in srgb, var(--accent) 28%, transparent); border-radius: 2px; color: inherit; } + +.file-grid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); padding: 18px; } +.file-card { align-items: center; background: var(--surface); border: 1px solid var(--line); border-radius: 13px; cursor: pointer; display: flex; flex-direction: column; height: 154px; justify-content: center; min-width: 0; padding: 16px 10px 12px; position: relative; transition: .16s ease; } +.file-card:hover { border-color: var(--line-strong); box-shadow: var(--shadow-sm); transform: translateY(-1px); } +.file-card.selected { background: color-mix(in srgb, var(--accent) 7%, var(--surface)); border-color: color-mix(in srgb, var(--accent) 45%, var(--line)); } +.card-icon { align-items: center; background: transparent; display: flex; height: 62px; justify-content: center; margin-bottom: 5px; width: 68px; } +.card-icon .ui-icon { height: 46px; stroke-width: 1.35; width: 46px; } +.file-card strong { font-size: 12px; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.file-card small { color: var(--muted); font-size: 10px; margin-top: 3px; } +.card-check { align-items: center; background: var(--accent); border-radius: 50%; color: #fff; display: flex; height: 18px; justify-content: center; opacity: 0; position: absolute; right: 9px; top: 9px; width: 18px; }.file-card.selected .card-check { opacity: 1; }.card-check .ui-icon { height: 12px; stroke-width: 2.7; width: 12px; } + +.state-view { align-items: center; color: var(--muted); display: flex; flex-direction: column; height: 100%; justify-content: center; min-height: 300px; padding: 30px; text-align: center; } +.state-view > .ui-icon { color: var(--line-strong); height: 54px; margin-bottom: 15px; stroke-width: 1.2; width: 54px; }.state-view strong { color: var(--text); font-size: 15px; margin-bottom: 4px; }.state-view span { font-size: 12px; }.state-view .action { margin-top: 16px; } +.spinner { animation: spin .8s linear infinite; border: 2px solid var(--line); border-radius: 50%; border-top-color: var(--accent); display: block; height: 28px; margin-bottom: 14px; width: 28px; } @keyframes spin { to { transform: rotate(360deg); } } + +.details-header { align-items: center; display: flex; justify-content: space-between; margin-bottom: 16px; }.details-header > strong { font-size: 12px; letter-spacing: .04em; text-transform: uppercase; }.details-header .icon-button { height: 30px; width: 30px; } +.preview-box { align-items: center; background: var(--surface); border: 1px solid var(--line); border-radius: 13px; display: flex; height: 190px; justify-content: center; margin-bottom: 16px; overflow: hidden; } +.preview-box img, .preview-box video, .preview-box iframe { border: 0; height: 100%; object-fit: contain; width: 100%; }.preview-box audio { max-width: 90%; }.preview-box pre { font: 10px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace; height: 100%; margin: 0; overflow: auto; padding: 12px; white-space: pre-wrap; width: 100%; }.preview-placeholder .ui-icon { height: 58px; stroke-width: 1.2; width: 58px; }.preview-error { color: var(--danger); font-size: 11px; padding: 18px; text-align: center; } +.detail-title { align-items: center; display: flex; gap: 12px; margin-bottom: 20px; min-width: 0; }.file-type-icon.large { flex-basis: 42px; height: 42px; width: 42px; }.detail-title > div { min-width: 0; }.detail-title strong, .detail-title span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.detail-title strong { font-size: 14px; }.detail-title span { color: var(--muted); font-size: 11px; } +.detail-list { border-top: 1px solid var(--line); display: grid; font-size: 11px; gap: 12px 8px; grid-template-columns: 74px minmax(0, 1fr); margin: 0 0 18px; padding-top: 16px; }.detail-list dt { color: var(--muted); }.detail-list dd { color: var(--text-soft); margin: 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; }.detail-path { overflow-wrap: anywhere !important; white-space: normal; }.detail-list code { background: var(--surface); border: 1px solid var(--line); border-radius: 4px; padding: 2px 5px; } +.details-empty { align-items: center; color: var(--muted); display: flex; flex-direction: column; height: 100%; justify-content: center; padding: 20px; text-align: center; }.details-empty .ui-icon { height: 38px; margin-bottom: 12px; stroke-width: 1.3; width: 38px; }.details-empty strong { color: var(--text); }.details-empty span { font-size: 11px; margin-top: 3px; }.multi-selection-mark { align-items: center; background: var(--accent-soft); border-radius: 16px; color: var(--accent-deep); display: flex; font-size: 32px; font-weight: 750; height: 150px; justify-content: center; margin-bottom: 18px; } + +.drawer-scrim, .modal-scrim { background: rgba(20, 29, 39, .25); inset: 0; position: absolute; z-index: 80; }.drawer { animation: drawer-in .2s ease; background: var(--surface-raised); box-shadow: var(--shadow-lg); display: grid; grid-template-rows: 72px minmax(0, 1fr); height: 100%; max-width: calc(100% - 30px); position: absolute; right: 0; top: 0; width: 390px; z-index: 81; } @keyframes drawer-in { from { transform: translateX(30px); opacity: .5; } } +.drawer header { align-items: center; border-bottom: 1px solid var(--line); display: flex; justify-content: space-between; padding: 0 18px 0 22px; }.drawer header small, .drawer header strong { display: block; }.drawer header small { color: var(--accent); font-size: 9px; font-weight: 750; letter-spacing: .1em; text-transform: uppercase; }.drawer header strong { font-size: 17px; }.drawer-body { overflow: auto; padding: 18px; }.drawer-empty { color: var(--muted); font-size: 12px; padding: 50px 20px; text-align: center; } +.transfer-item { background: var(--surface-soft); border: 1px solid var(--line); border-radius: 11px; margin-bottom: 10px; padding: 12px; }.transfer-item > div:first-child { align-items: center; display: grid; gap: 10px; grid-template-columns: auto minmax(0, 1fr) auto; }.transfer-icon { align-items: center; background: var(--accent-soft); border-radius: 8px; color: var(--accent-deep); display: flex; height: 34px; justify-content: center; width: 34px; }.transfer-icon .ui-icon { height: 17px; width: 17px; }.transfer-item strong, .transfer-item small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.transfer-item strong { font-size: 12px; }.transfer-item small { color: var(--muted); font-size: 10px; }.transfer-item b { color: var(--muted); font-size: 10px; }.progress { background: var(--line); border-radius: 4px; height: 4px; margin-top: 10px; overflow: hidden; }.progress span { background: var(--accent); display: block; height: 100%; transition: width .3s ease; } +.settings-form label { align-items: center; border-bottom: 1px solid var(--line); display: flex; gap: 15px; justify-content: space-between; padding: 15px 2px; }.settings-form label span { min-width: 0; }.settings-form strong, .settings-form small { display: block; }.settings-form strong { font-size: 13px; }.settings-form small { color: var(--muted); font-size: 10px; margin-top: 2px; }.settings-form input[type="checkbox"] { accent-color: var(--accent); height: 18px; width: 18px; }.settings-form .action { margin-top: 22px; } + +.context-menu { animation: menu-in .12s ease; background: var(--surface-raised); border: 1px solid var(--line); border-radius: 11px; box-shadow: var(--shadow-lg); padding: 6px; position: fixed; width: 220px; z-index: 100; } @keyframes menu-in { from { opacity: 0; transform: scale(.97); } } +.context-menu button { align-items: center; background: transparent; border: 0; border-radius: 7px; cursor: pointer; display: flex; gap: 10px; padding: 8px 9px; text-align: left; width: 100%; }.context-menu button:hover { background: var(--surface-hover); }.context-menu button:disabled { opacity: .35; }.context-menu button.danger { color: var(--danger); }.context-menu .ui-icon { height: 17px; width: 17px; }.context-separator { background: var(--line); display: block; height: 1px; margin: 5px; } + +.modal-scrim { align-items: center; display: flex; justify-content: center; z-index: 120; }.modal { animation: modal-in .16s ease; background: var(--surface-raised); border: 1px solid var(--line); border-radius: 16px; box-shadow: var(--shadow-lg); max-width: calc(100% - 32px); padding: 22px; width: 420px; } @keyframes modal-in { from { opacity: 0; transform: translateY(8px) scale(.98); } } +.modal header { align-items: center; display: flex; gap: 13px; }.modal header > div { min-width: 0; }.modal header strong, .modal header small { display: block; }.modal header strong { font-size: 16px; }.modal header small { color: var(--muted); font-size: 11px; margin-top: 2px; }.modal-icon { align-items: center; background: var(--accent-soft); border-radius: 10px; color: var(--accent-deep); display: flex; flex: 0 0 40px; height: 40px; justify-content: center; }.modal-icon.danger { background: color-mix(in srgb, var(--danger) 10%, var(--surface)); color: var(--danger); }.modal input { background: var(--surface-soft); border: 1px solid var(--line); border-radius: 9px; color: var(--text); margin-top: 20px; outline: 0; padding: 10px 12px; width: 100%; }.modal input:focus { border-color: var(--accent); }.modal footer { display: flex; gap: 8px; justify-content: flex-end; margin-top: 22px; }.danger-button { background: var(--danger) !important; border-color: var(--danger) !important; } +.toast { align-items: center; background: #1d2b37; border-radius: 10px; bottom: 44px; box-shadow: var(--shadow-lg); color: #fff; display: flex; gap: 9px; left: 50%; max-width: min(460px, calc(100% - 32px)); opacity: 0; padding: 10px 14px; position: absolute; transform: translate(-50%, 8px); transition: .22s ease; z-index: 150; }.toast.visible { opacity: 1; transform: translate(-50%, 0); }.toast.error { background: #9d3535; }.toast .ui-icon { height: 17px; width: 17px; }.toast span { font-size: 12px; } +.hidden-input { display: none; } + +@media (max-width: 1120px) { + .app-header { grid-template-columns: 200px minmax(220px, 1fr) auto; }.browser-shell { grid-template-columns: 200px minmax(0, 1fr); }.explorer-stage { grid-template-columns: minmax(0, 1fr) 270px; }.primary-actions .action span { display: none; }.action { padding: 0 8px; }.type-column { display: none; }.file-head, .file-row { grid-template-columns: 42px minmax(230px, 1fr) 100px 0 150px 40px; } +} +@media (max-width: 820px) { + .unav-frame { min-height: 600px; }.app-header { gap: 10px; grid-template-columns: auto minmax(140px, 1fr) auto; padding: 0 12px; }.brand > span:last-child { display: none; }.header-search kbd { display: none; }.browser-shell { grid-template-columns: 64px minmax(0, 1fr); }.sidebar { padding: 14px 8px; }.sidebar-label, .place > span:nth-child(2), .place > .ui-icon, .sidebar-shortcuts, .sidebar-foot span:last-child { display: none; }.place { display: flex; justify-content: center; padding: 7px; }.explorer-stage { grid-template-columns: minmax(0, 1fr); }.details-pane { bottom: 30px; box-shadow: var(--shadow-lg); max-width: 88%; position: absolute; right: 0; top: 171px; width: 310px; z-index: 10; }.details-pane.closed { display: none; }.command-bar { padding: 0 8px; }.selection-action { display: none; }.item-count { display: none; }.date-column { display: none; }.file-head, .file-row { grid-template-columns: 36px minmax(190px, 1fr) 85px 0 0 36px; }.file-grid { grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); padding: 10px; }.breadcrumbs button { max-width: 110px; }.status-bar { grid-template-columns: 1fr auto; }.status-bar .version { display: none; } } diff --git a/u-navigator.plg b/u-navigator.plg index 29ce5b3..031721d 100644 --- a/u-navigator.plg +++ b/u-navigator.plg @@ -1,7 +1,7 @@ - + @@ -18,13 +18,14 @@ &description; ### &version; -- Native Dynamix-embedded U-Navigator page. -- Adds PHP API endpoints for listing, upload, move and copy without a separate web server. +- Replaces the floating window manager with a modern full-screen file browser. +- Adds responsive sidebar navigation, breadcrumbs, search, list and grid views, a detail preview and transfer drawer. +- Keeps native Dynamix integration and the existing PHP file APIs. https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package; -6b8acde65433cdcd383f647a85dab3d9 +568dd0b4ad993c41bd68d663c875805e diff --git a/u-navigator.plg.in b/u-navigator.plg.in index 901ca63..9375ed3 100644 --- a/u-navigator.plg.in +++ b/u-navigator.plg.in @@ -18,8 +18,9 @@ &description; ### &version; -- Native Dynamix-embedded U-Navigator page. -- Adds PHP API endpoints for listing, upload, move and copy without a separate web server. +- Replaces the floating window manager with a modern full-screen file browser. +- Adds responsive sidebar navigation, breadcrumbs, search, list and grid views, a detail preview and transfer drawer. +- Keeps native Dynamix integration and the existing PHP file APIs.