From 48e83c3f69146fcad875672630ea7215ceecdece Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:07:35 +0200 Subject: [PATCH] Redesign U-Navigator as modern file browser --- TODO.md | 217 ++ build-plugin.sh | 2 +- packages/u-navigator-2026.07.14.r008.tgz | Bin 0 -> 28989 bytes .../plugins/u-navigator/U-Navigator.page | 21 +- server/public/app.js | 3195 ++++------------- server/public/index.html | 17 +- server/public/styles.css | 1270 +------ u-navigator.plg | 9 +- u-navigator.plg.in | 5 +- 9 files changed, 1139 insertions(+), 3597 deletions(-) create mode 100644 TODO.md create mode 100644 packages/u-navigator-2026.07.14.r008.tgz 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 0000000000000000000000000000000000000000..4d5e7d859a211d2d1c9efcf2b6f5a6ac0cafae51 GIT binary patch literal 28989 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 literal 0 HcmV?d00001 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.