diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 6a842c5..0000000 --- a/TODO.md +++ /dev/null @@ -1,217 +0,0 @@ -# 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 ba7c4d3..f4bfe08 100755 --- a/build-plugin.sh +++ b/build-plugin.sh @@ -2,7 +2,7 @@ set -eu PLUGIN="u-navigator" -VERSION="2026.07.14.r008" +VERSION="2026.07.04.r007" 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 deleted file mode 100644 index 4d5e7d8..0000000 Binary files a/packages/u-navigator-2026.07.14.r008.tgz and /dev/null 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 24f2bba..08ac58d 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,7 +11,26 @@ $plugin = "u-navigator"; $version = "__VERSION__"; ?> -
+
+
+
+

U-Navigator

+

Initialisiere...

+
+ +
+
+
diff --git a/server/public/styles.css b/server/public/styles.css index 948ad27..fa35f33 100644 --- a/server/public/styles.css +++ b/server/public/styles.css @@ -1,188 +1,1110 @@ .u-nav { - --accent: #ff8a3d; - --accent-deep: #e96820; - --accent-soft: #fff1e7; - --bg: #eef1f5; + --bg: #f4f6f8; --surface: #ffffff; - --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); + --surface-2: #eef2f5; + --text: #18222d; + --muted: #617182; + --line: #cfd8e1; + --accent: #e86f2d; + --accent-2: #287a65; + --shadow: 0 18px 55px rgba(24, 34, 45, 0.22); background: var(--bg); color: var(--text); - font: 14px/1.45 Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - min-height: calc(100vh - 150px); + 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); overflow: hidden; position: relative; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; } @media (prefers-color-scheme: dark) { .u-nav { - --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); + --bg: #101418; + --surface: #1b2229; + --surface-2: #242e37; + --text: #eef3f7; + --muted: #a3b0bd; + --line: #36434f; + --shadow: 0 18px 55px rgba(0, 0, 0, 0.44); } } -.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; } +.u-nav *, +.u-nav *::before, +.u-nav *::after { + box-sizing: border-box; } -@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; } + +.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; + } } diff --git a/u-navigator.plg b/u-navigator.plg index 031721d..29ce5b3 100644 --- a/u-navigator.plg +++ b/u-navigator.plg @@ -1,7 +1,7 @@ - + @@ -18,14 +18,13 @@ &description; ### &version; -- 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. +- Native Dynamix-embedded U-Navigator page. +- Adds PHP API endpoints for listing, upload, move and copy without a separate web server. https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package; -568dd0b4ad993c41bd68d663c875805e +6b8acde65433cdcd383f647a85dab3d9 diff --git a/u-navigator.plg.in b/u-navigator.plg.in index 9375ed3..901ca63 100644 --- a/u-navigator.plg.in +++ b/u-navigator.plg.in @@ -18,9 +18,8 @@ &description; ### &version; -- 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. +- Native Dynamix-embedded U-Navigator page. +- Adds PHP API endpoints for listing, upload, move and copy without a separate web server.