diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b89175c..7cf067da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,13 @@ ## [Unreleased] +### Added +- **UX 3.5 default console shell:** native full-list sidebar + topbar (promoted from retired Beta 3.1), glass branding, Dark/Light/Custom themes in Settings → Branding, resizable sidebar, tablet/phone drawer. Login / RdClient / remote viewer unchanged. Docs: `docs/wiki/UX-3.5.md`. Ships via panel update. + ### Fixed - **OIDC client login stuck on “Waiting…” (#304):** panel now proxies `GET /api/auth/oidc/callback` (and `/api/oidc/callback`) to the Go API, so Redirect URLs that use the console origin (`:5000` / `:5443`) complete RustDesk SSO instead of returning 404. Settings also validate the callback path and clarify the hint. Ships via panel update. Verify: IdP Redirect URL = `https:///api/auth/oidc/callback` → SSO in RustDesk finishes with access token (browser shows success page). ### Changed -- _(none yet)_ +- **Console chrome:** TeamViewer-style rail/flyout and Desktop Mode are no longer loaded as the management UI; UX 3.5 is the single console interface on `dev`. --- diff --git a/data/.image-commit b/data/.image-commit new file mode 100644 index 00000000..35466456 --- /dev/null +++ b/data/.image-commit @@ -0,0 +1 @@ +unknown diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md index 96ccdbd4..201595ae 100644 --- a/docs/wiki/Home.md +++ b/docs/wiki/Home.md @@ -50,7 +50,7 @@ Open **http://your-server:5000** for the web console. Default install paths: Go | **Installation** | [[Installation]] — Linux, Windows, Docker | | **Configuration** | [[Configuration]] — CLI flags, `.env`, systemd/NSSM | | **Client setup** | [[Client Setup\|Client-Setup]] — RustDesk desktop/mobile | -| **Web console** | [[Web Console\|Web-Console]] — Dashboard, devices, settings | +| **Web console** | [[Web Console\|Web-Console]] — Dashboard, devices, settings ([[UX 3.5\|UX-3.5]] shell) | | **Users & RBAC** | [[User Management\|User-Management]], [[Organizations and RBAC\|Organizations-and-RBAC]] | | **OIDC / SSO** | [[OIDC SSO\|OIDC-SSO]] | | **Client generator** | [[Client Generator\|Client-Generator]] | diff --git a/docs/wiki/UX-3.5.md b/docs/wiki/UX-3.5.md new file mode 100644 index 00000000..40208b6d --- /dev/null +++ b/docs/wiki/UX-3.5.md @@ -0,0 +1,69 @@ +# UX 3.5 — Console shell + +UX 3.5 is the **default management console chrome** in BetterDesk `web-nodejs`. It replaces the TeamViewer-style icon rail / flyout and the retired Beta 3.1 iframe overlay with a **native** server-rendered layout. + +## Architecture + +| Piece | Path | +|-------|------| +| Layout | `web-nodejs/views/layouts/main.ejs` | +| Topbar | `web-nodejs/views/partials/ux35-topbar.ejs` | +| Sidebar (full nav + permissions) | `web-nodejs/views/partials/ux35-sidebar.ejs` | +| Tokens | `web-nodejs/public/css/ux35-tokens.css` | +| Shell CSS | `web-nodejs/public/css/ux35.css` | +| Shell JS (drawer, resize, theme icon) | `web-nodejs/public/js/ux35-shell.js` | + +Navigation uses ordinary `` links (full page load). There is **no** iframe shell and **no** Modern/Classic toggle. + +Out of scope for UX 3.5 (unchanged): login page, RdClient, remote viewer, CDAP Studio. + +## Chrome + +- **Topbar:** 48px — brand, breadcrumb (desktop), refresh, notifications, theme, accessibility, language, user menu +- **Sidebar:** default **220px** (resizable 200–320px, stored in `localStorage` key `bd_ux35_sidebar_width`) +- **Content:** padding via `--ux35-content-padding` / `--ux35-content-padding-lg` (16–24px) + +## Spacing scale + +Use only: **4 / 8 / 12 / 16 / 24 / 32** px (`--ux35-space-1` … `--ux35-space-6`, or `--space-xs` / `sm` / `3` / `md` / `lg` / `xl`). + +Avoid ad-hoc page paddings that diverge from this scale. + +## Themes + +Branding `themeMode` (Settings → Branding → Colors): + +| Mode | Meaning | +|------|---------| +| `dark` | Built-in dark palette | +| `light` | Built-in light palette | +| `custom` | Operator-edited colors (auto-selected when a color picker changes) | + +CSS: `data-theme` / `data-theme-mode` on ``, colors and glass from `/css/branding.css` (`brandingService.generateThemeCss()`). + +Glass/blur tokens (`--surface-glass-*`) map onto topbar, sidebar, and cards. + +## Breakpoints + +| Width | Behavior | +|-------|----------| +| ≥1100px | Persistent sidebar + resize handle | +| ≤1099px | Hamburger opens sidebar drawer + overlay | +| ≤767px | Narrower drawer; content padding reduced; bottom nav retained | + +## Migration from Beta 3.1 + +Beta 3.1 (`beta31.js` / `beta31.css`) was an optional overlay that loaded pages in `?embed=1` iframes. UX 3.5 reuses the **visual pattern** (full-list sidebar + topbar) as the native default. Desktop Mode is no longer loaded by the console layout. + +## i18n + +Keys under `ux35.*` and `theme.custom` exist in all 26 `web-nodejs/lang/*.json` locales. Helper script: `web-nodejs/scripts/patch-ux35-i18n.js`. + +## Acceptance checklist + +- [ ] Desktop 1280: sidebar + topbar + dashboard +- [ ] Tablet 1024 / 768: drawer opens/closes +- [ ] Phone 390: drawer + bottom nav +- [ ] Settings → Branding: Dark / Light / Custom + glass +- [ ] Permission-gated nav items match former sidebar +- [ ] `prefers-reduced-motion` disables content enter animation diff --git a/docs/wiki/Web-Console.md b/docs/wiki/Web-Console.md index 2c1bf7ef..efebd6da 100644 --- a/docs/wiki/Web-Console.md +++ b/docs/wiki/Web-Console.md @@ -2,6 +2,8 @@ The BetterDesk Web Console is a Node.js (Express.js) management panel accessible at **http://your-server:5000**. +The default chrome is **UX 3.5** (full-list sidebar + topbar, glass branding). See [[UX 3.5|UX-3.5]] for tokens, breakpoints, and theming (Dark / Light / Custom). + --- ## Dashboard @@ -13,18 +15,7 @@ The dashboard provides an overview of your BetterDesk deployment: - **Server Uptime** — Go server uptime - **System Info** — Hostname, OS, Node.js/Go versions -### Desktop Widget Dashboard - -The dashboard supports an **OS-style desktop mode** with: -- 20+ draggable, resizable widgets -- Windows 11-style snap layouts (6 predefined zone layouts) -- Aero Shake, edge-snap, maximize hover picker -- Dark/light/auto themes with glassmorphism -- Widget groups (tabbed containers) -- 4 built-in presets (Monitoring, Helpdesk, Minimal, Developer) -- Full-screen login screen with TOTP 2FA support - -See [[Desktop Dashboard|Desktop-Dashboard]] for the complete widget reference. +> **Note:** OS-style Desktop Mode is no longer part of the default console shell (removed in favor of UX 3.5). Historical widget docs may remain under [[Desktop Dashboard|Desktop-Dashboard]]. --- diff --git a/web-nodejs/lang/ar.json b/web-nodejs/lang/ar.json index 501b9821..a69d6881 100644 --- a/web-nodejs/lang/ar.json +++ b/web-nodejs/lang/ar.json @@ -2785,7 +2785,8 @@ "dark": "Dark", "light": "Light", "auto": "System", - "title": "Theme" + "title": "Theme", + "custom": "مخصص" }, "days": { "mon": "Mon", @@ -4334,5 +4335,10 @@ "empty": "لا توجد أجهزة على رابط الضيف هذا.", "expires": "ينتهي", "back_to_list": "العودة إلى قائمة الضيف" + }, + "ux35": { + "resize_sidebar": "تغيير عرض الشريط الجانبي", + "open_menu": "فتح القائمة", + "theme_mode_hint": "الفاتح والداكن مدمجان. يُفعَّل المخصص عند تعديل الألوان أدناه." } } diff --git a/web-nodejs/lang/cs.json b/web-nodejs/lang/cs.json index e54feb61..382bcb9f 100644 --- a/web-nodejs/lang/cs.json +++ b/web-nodejs/lang/cs.json @@ -2778,7 +2778,8 @@ "dark": "Tmavý", "light": "Světlý", "auto": "Systémový", - "title": "Motiv" + "title": "Motiv", + "custom": "Vlastní" }, "days": { "mon": "Po", @@ -4327,5 +4328,10 @@ "empty": "Na tomto odkazu nejsou žádná zařízení.", "expires": "Vyprší", "back_to_list": "Zpět na seznam hosta" + }, + "ux35": { + "resize_sidebar": "Změnit šířku postranního panelu", + "open_menu": "Otevřít nabídku", + "theme_mode_hint": "Světlý a tmavý jsou vestavěné. Vlastní se aktivuje při úpravě barev níže." } } diff --git a/web-nodejs/lang/da.json b/web-nodejs/lang/da.json index e5b995fd..3d5b8da9 100644 --- a/web-nodejs/lang/da.json +++ b/web-nodejs/lang/da.json @@ -2784,7 +2784,8 @@ "dark": "Dark", "light": "Light", "auto": "System", - "title": "Theme" + "title": "Theme", + "custom": "Tilpasset" }, "days": { "mon": "Mon", @@ -4333,5 +4334,10 @@ "empty": "Ingen enheder på dette gæstelink.", "expires": "Udløber", "back_to_list": "Tilbage til gæstelisten" + }, + "ux35": { + "resize_sidebar": "Tilpas sidepanelbredde", + "open_menu": "Åbn menu", + "theme_mode_hint": "Lys og mørk er indbygget. Tilpasset aktiveres, når du redigerer farverne nedenfor." } } diff --git a/web-nodejs/lang/de.json b/web-nodejs/lang/de.json index abf2e51c..9f826782 100644 --- a/web-nodejs/lang/de.json +++ b/web-nodejs/lang/de.json @@ -2778,7 +2778,8 @@ "dark": "Dark", "light": "Light", "auto": "System", - "title": "Dashboard" + "title": "Dashboard", + "custom": "Benutzerdefiniert" }, "days": { "mon": "Mon", @@ -4327,5 +4328,10 @@ "empty": "Keine Geräte auf diesem Gastlink.", "expires": "Läuft ab", "back_to_list": "Zurück zur Gästeliste" + }, + "ux35": { + "resize_sidebar": "Seitenleiste skalieren", + "open_menu": "Menü öffnen", + "theme_mode_hint": "Hell und Dunkel sind integriert. Benutzerdefiniert wird aktiviert, wenn Sie unten Farben ändern." } } diff --git a/web-nodejs/lang/en.json b/web-nodejs/lang/en.json index a747fd97..fc431142 100644 --- a/web-nodejs/lang/en.json +++ b/web-nodejs/lang/en.json @@ -2908,8 +2908,14 @@ "dark": "Dark", "light": "Light", "auto": "System", + "custom": "Custom", "title": "Theme" }, + "ux35": { + "resize_sidebar": "Resize sidebar", + "open_menu": "Open menu", + "theme_mode_hint": "Light and Dark are built-in. Custom activates when you edit colors below." + }, "days": { "mon": "Mon", "tue": "Tue", diff --git a/web-nodejs/lang/es.json b/web-nodejs/lang/es.json index 353763cb..896206a8 100644 --- a/web-nodejs/lang/es.json +++ b/web-nodejs/lang/es.json @@ -2778,7 +2778,8 @@ "dark": "Dark", "light": "Light", "auto": "System", - "title": "Panel" + "title": "Panel", + "custom": "Personalizado" }, "days": { "mon": "Mon", @@ -4327,5 +4328,10 @@ "empty": "No hay dispositivos en este enlace.", "expires": "Caduca", "back_to_list": "Volver a la lista de invitado" + }, + "ux35": { + "resize_sidebar": "Cambiar ancho de la barra lateral", + "open_menu": "Abrir menú", + "theme_mode_hint": "Claro y Oscuro son predefinidos. Personalizado se activa al editar los colores abajo." } } diff --git a/web-nodejs/lang/fi.json b/web-nodejs/lang/fi.json index 6832f73e..1e6a4bb5 100644 --- a/web-nodejs/lang/fi.json +++ b/web-nodejs/lang/fi.json @@ -2784,7 +2784,8 @@ "dark": "Dark", "light": "Light", "auto": "System", - "title": "Theme" + "title": "Theme", + "custom": "Mukautettu" }, "days": { "mon": "Mon", @@ -4333,5 +4334,10 @@ "empty": "Tällä vieraslinkillä ei ole laitteita.", "expires": "Vanhenee", "back_to_list": "Takaisin vieraslistaan" + }, + "ux35": { + "resize_sidebar": "Muuta sivupalkin leveyttä", + "open_menu": "Avaa valikko", + "theme_mode_hint": "Vaalea ja Tumma ovat sisäänrakennettuja. Mukautettu otetaan käyttöön, kun muokkaat värejä alla." } } diff --git a/web-nodejs/lang/fr.json b/web-nodejs/lang/fr.json index e927b7c4..2ebc2eea 100644 --- a/web-nodejs/lang/fr.json +++ b/web-nodejs/lang/fr.json @@ -2866,7 +2866,8 @@ "dark": "Dark", "light": "Light", "auto": "System", - "title": "Tableau de bord" + "title": "Tableau de bord", + "custom": "Personnalisé" }, "days": { "mon": "Mon", @@ -4331,5 +4332,10 @@ "empty": "Aucun appareil sur ce lien invité.", "expires": "Expire", "back_to_list": "Retour à la liste invitée" + }, + "ux35": { + "resize_sidebar": "Redimensionner la barre latérale", + "open_menu": "Ouvrir le menu", + "theme_mode_hint": "Clair et Sombre sont intégrés. Personnalisé s’active lorsque vous modifiez les couleurs ci-dessous." } } diff --git a/web-nodejs/lang/hi.json b/web-nodejs/lang/hi.json index 2989158c..ecc6d945 100644 --- a/web-nodejs/lang/hi.json +++ b/web-nodejs/lang/hi.json @@ -2784,7 +2784,8 @@ "dark": "Dark", "light": "Light", "auto": "System", - "title": "Theme" + "title": "Theme", + "custom": "कस्टम" }, "days": { "mon": "Mon", @@ -4333,5 +4334,10 @@ "empty": "इस गेस्ट लिंक पर कोई डिवाइस नहीं।", "expires": "समाप्ति", "back_to_list": "गेस्ट सूची पर वापस" + }, + "ux35": { + "resize_sidebar": "साइडबार की चौड़ाई बदलें", + "open_menu": "मेनू खोलें", + "theme_mode_hint": "लाइट और डार्क बिल्ट-इन हैं। नीचे रंग बदलने पर कस्टम सक्रिय होता है।" } } diff --git a/web-nodejs/lang/hu.json b/web-nodejs/lang/hu.json index e4af3e1a..81edcd4a 100644 --- a/web-nodejs/lang/hu.json +++ b/web-nodejs/lang/hu.json @@ -2784,7 +2784,8 @@ "dark": "Dark", "light": "Light", "auto": "System", - "title": "Theme" + "title": "Theme", + "custom": "Egyéni" }, "days": { "mon": "Mon", @@ -4333,5 +4334,10 @@ "empty": "Nincs eszköz ezen a vendéglinken.", "expires": "Lejár", "back_to_list": "Vissza a vendéglistához" + }, + "ux35": { + "resize_sidebar": "Oldalsáv átméretezése", + "open_menu": "Menü megnyitása", + "theme_mode_hint": "A Világos és a Sötét beépített. Az Egyéni a színek szerkesztésekor aktiválódik." } } diff --git a/web-nodejs/lang/id.json b/web-nodejs/lang/id.json index b82bc911..786ecc71 100644 --- a/web-nodejs/lang/id.json +++ b/web-nodejs/lang/id.json @@ -2784,7 +2784,8 @@ "dark": "Dark", "light": "Light", "auto": "System", - "title": "Theme" + "title": "Theme", + "custom": "Kustom" }, "days": { "mon": "Mon", @@ -4333,5 +4334,10 @@ "empty": "Tidak ada perangkat pada tautan ini.", "expires": "Kedaluwarsa", "back_to_list": "Kembali ke daftar tamu" + }, + "ux35": { + "resize_sidebar": "Ubah lebar bilah samping", + "open_menu": "Buka menu", + "theme_mode_hint": "Terang dan Gelap bawaan. Kustom aktif saat Anda mengubah warna di bawah." } } diff --git a/web-nodejs/lang/it.json b/web-nodejs/lang/it.json index 2e09a3b5..672bf0c5 100644 --- a/web-nodejs/lang/it.json +++ b/web-nodejs/lang/it.json @@ -2778,7 +2778,8 @@ "dark": "Dark", "light": "Light", "auto": "System", - "title": "Theme" + "title": "Theme", + "custom": "Personalizzato" }, "days": { "mon": "Mon", @@ -4327,5 +4328,10 @@ "empty": "Nessun dispositivo su questo link.", "expires": "Scade", "back_to_list": "Torna all'elenco ospite" + }, + "ux35": { + "resize_sidebar": "Ridimensiona barra laterale", + "open_menu": "Apri menu", + "theme_mode_hint": "Chiaro e Scuro sono integrati. Personalizzato si attiva modificando i colori sotto." } } diff --git a/web-nodejs/lang/ja.json b/web-nodejs/lang/ja.json index 477d26c5..092aa54f 100644 --- a/web-nodejs/lang/ja.json +++ b/web-nodejs/lang/ja.json @@ -2778,7 +2778,8 @@ "dark": "Dark", "light": "Light", "auto": "System", - "title": "Theme" + "title": "Theme", + "custom": "カスタム" }, "days": { "mon": "Mon", @@ -4327,5 +4328,10 @@ "empty": "このゲストリンクに端末がありません。", "expires": "期限", "back_to_list": "ゲスト一覧に戻る" + }, + "ux35": { + "resize_sidebar": "サイドバーの幅を変更", + "open_menu": "メニューを開く", + "theme_mode_hint": "ライトとダークは標準です。下で色を編集するとカスタムになります。" } } diff --git a/web-nodejs/lang/ko.json b/web-nodejs/lang/ko.json index 435a0aa1..be294d90 100644 --- a/web-nodejs/lang/ko.json +++ b/web-nodejs/lang/ko.json @@ -2778,7 +2778,8 @@ "dark": "Dark", "light": "Light", "auto": "System", - "title": "Theme" + "title": "Theme", + "custom": "사용자 지정" }, "days": { "mon": "Mon", @@ -4327,5 +4328,10 @@ "empty": "이 게스트 링크에 장치가 없습니다.", "expires": "만료", "back_to_list": "게스트 목록으로" + }, + "ux35": { + "resize_sidebar": "사이드바 너비 조절", + "open_menu": "메뉴 열기", + "theme_mode_hint": "밝게와 어둡게는 기본입니다. 아래에서 색상을 편집하면 사용자 지정이 활성화됩니다." } } diff --git a/web-nodejs/lang/nb.json b/web-nodejs/lang/nb.json index 937f00e9..3731aaaa 100644 --- a/web-nodejs/lang/nb.json +++ b/web-nodejs/lang/nb.json @@ -2784,7 +2784,8 @@ "dark": "Dark", "light": "Light", "auto": "System", - "title": "Theme" + "title": "Theme", + "custom": "Tilpasset" }, "days": { "mon": "Mon", @@ -4333,5 +4334,10 @@ "empty": "Ingen enheter på denne gjestelenken.", "expires": "Utløper", "back_to_list": "Tilbake til gjestelisten" + }, + "ux35": { + "resize_sidebar": "Endre sidepanelbredde", + "open_menu": "Åpne meny", + "theme_mode_hint": "Lys og Mørk er innebygd. Tilpasset aktiveres når du redigerer fargene nedenfor." } } diff --git a/web-nodejs/lang/nl.json b/web-nodejs/lang/nl.json index 88c8623a..b17f70db 100644 --- a/web-nodejs/lang/nl.json +++ b/web-nodejs/lang/nl.json @@ -2778,7 +2778,8 @@ "dark": "Dark", "light": "Light", "auto": "System", - "title": "Theme" + "title": "Theme", + "custom": "Aangepast" }, "days": { "mon": "Mon", @@ -4327,5 +4328,10 @@ "empty": "Geen apparaten op deze gastlink.", "expires": "Verloopt", "back_to_list": "Terug naar gastenlijst" + }, + "ux35": { + "resize_sidebar": "Zijbalkbreedte aanpassen", + "open_menu": "Menu openen", + "theme_mode_hint": "Licht en Donker zijn ingebouwd. Aangepast wordt actief wanneer u hieronder kleuren bewerkt." } } diff --git a/web-nodejs/lang/pl.json b/web-nodejs/lang/pl.json index 1d5c1dc3..567340ae 100644 --- a/web-nodejs/lang/pl.json +++ b/web-nodejs/lang/pl.json @@ -2862,7 +2862,8 @@ "dark": "Ciemny", "light": "Jasny", "auto": "Systemowy", - "title": "Motyw" + "title": "Motyw", + "custom": "Niestandardowy" }, "days": { "mon": "Pon", @@ -4327,5 +4328,10 @@ "empty": "Brak urządzeń na tym linku.", "expires": "Wygasa", "back_to_list": "Wróć do listy gościa" + }, + "ux35": { + "resize_sidebar": "Zmień szerokość paska bocznego", + "open_menu": "Otwórz menu", + "theme_mode_hint": "Jasny i Ciemny są wbudowane. Niestandardowy włącza się po edycji kolorów poniżej." } } diff --git a/web-nodejs/lang/pt.json b/web-nodejs/lang/pt.json index bb8a3116..35dd853f 100644 --- a/web-nodejs/lang/pt.json +++ b/web-nodejs/lang/pt.json @@ -2778,7 +2778,8 @@ "dark": "Dark", "light": "Light", "auto": "System", - "title": "Theme" + "title": "Theme", + "custom": "Personalizado" }, "days": { "mon": "Mon", @@ -4327,5 +4328,10 @@ "empty": "Nenhum dispositivo nesta ligação.", "expires": "Expira", "back_to_list": "Voltar à lista de convidado" + }, + "ux35": { + "resize_sidebar": "Redimensionar barra lateral", + "open_menu": "Abrir menu", + "theme_mode_hint": "Claro e Escuro são predefinidos. Personalizado ativa-se ao editar as cores abaixo." } } diff --git a/web-nodejs/lang/ro.json b/web-nodejs/lang/ro.json index 0f48d039..146dc161 100644 --- a/web-nodejs/lang/ro.json +++ b/web-nodejs/lang/ro.json @@ -2784,7 +2784,8 @@ "dark": "Dark", "light": "Light", "auto": "System", - "title": "Theme" + "title": "Theme", + "custom": "Personalizat" }, "days": { "mon": "Mon", @@ -4333,5 +4334,10 @@ "empty": "Niciun dispozitiv pe acest link.", "expires": "Expiră", "back_to_list": "Înapoi la lista oaspete" + }, + "ux35": { + "resize_sidebar": "Redimensionați bara laterală", + "open_menu": "Deschide meniul", + "theme_mode_hint": "Deschis și Întunecat sunt integrate. Personalizat se activează când editați culorile de mai jos." } } diff --git a/web-nodejs/lang/sv.json b/web-nodejs/lang/sv.json index 6c00dab9..4f15ad5b 100644 --- a/web-nodejs/lang/sv.json +++ b/web-nodejs/lang/sv.json @@ -2784,7 +2784,8 @@ "dark": "Dark", "light": "Light", "auto": "System", - "title": "Theme" + "title": "Theme", + "custom": "Anpassad" }, "days": { "mon": "Mon", @@ -4333,5 +4334,10 @@ "empty": "Inga enheter på denna gästlänk.", "expires": "Går ut", "back_to_list": "Tillbaka till gästlistan" + }, + "ux35": { + "resize_sidebar": "Ändra sidopanelens bredd", + "open_menu": "Öppna meny", + "theme_mode_hint": "Ljust och Mörkt är inbyggda. Anpassad aktiveras när du redigerar färgerna nedan." } } diff --git a/web-nodejs/lang/th.json b/web-nodejs/lang/th.json index d243bd39..4ee28164 100644 --- a/web-nodejs/lang/th.json +++ b/web-nodejs/lang/th.json @@ -2784,7 +2784,8 @@ "dark": "Dark", "light": "Light", "auto": "System", - "title": "Theme" + "title": "Theme", + "custom": "กำหนดเอง" }, "days": { "mon": "Mon", @@ -4333,5 +4334,10 @@ "empty": "ไม่มีอุปกรณ์ในลิงก์นี้", "expires": "หมดอายุ", "back_to_list": "กลับไปรายการผู้เยี่ยมชม" + }, + "ux35": { + "resize_sidebar": "ปรับความกว้างแถบด้านข้าง", + "open_menu": "เปิดเมนู", + "theme_mode_hint": "สว่างและมืดมีให้ในตัว กำหนดเองจะเปิดเมื่อคุณแก้ไขสีด้านล่าง" } } diff --git a/web-nodejs/lang/tr.json b/web-nodejs/lang/tr.json index 8086e5d7..b85f46cd 100644 --- a/web-nodejs/lang/tr.json +++ b/web-nodejs/lang/tr.json @@ -2784,7 +2784,8 @@ "dark": "Dark", "light": "Light", "auto": "System", - "title": "Theme" + "title": "Theme", + "custom": "Özel" }, "days": { "mon": "Mon", @@ -4333,5 +4334,10 @@ "empty": "Bu misafir bağlantısında cihaz yok.", "expires": "Bitiş", "back_to_list": "Misafir listesine dön" + }, + "ux35": { + "resize_sidebar": "Kenar çubuğu genişliğini ayarla", + "open_menu": "Menüyü aç", + "theme_mode_hint": "Açık ve Koyu yerleşiktir. Aşağıdaki renkleri düzenlediğinizde Özel etkinleşir." } } diff --git a/web-nodejs/lang/uk.json b/web-nodejs/lang/uk.json index 76d2bde0..23afec82 100644 --- a/web-nodejs/lang/uk.json +++ b/web-nodejs/lang/uk.json @@ -2784,7 +2784,8 @@ "dark": "Dark", "light": "Light", "auto": "System", - "title": "Theme" + "title": "Theme", + "custom": "Власний" }, "days": { "mon": "Mon", @@ -4333,5 +4334,10 @@ "empty": "На цьому посиланні немає пристроїв.", "expires": "Закінчується", "back_to_list": "Назад до списку гостя" + }, + "ux35": { + "resize_sidebar": "Змінити ширину бічної панелі", + "open_menu": "Відкрити меню", + "theme_mode_hint": "Світла і Темна вбудовані. Власний активується під час редагування кольорів нижче." } } diff --git a/web-nodejs/lang/vi.json b/web-nodejs/lang/vi.json index 53c00760..7cfb3bf1 100644 --- a/web-nodejs/lang/vi.json +++ b/web-nodejs/lang/vi.json @@ -2784,7 +2784,8 @@ "dark": "Dark", "light": "Light", "auto": "System", - "title": "Theme" + "title": "Theme", + "custom": "Tùy chỉnh" }, "days": { "mon": "Mon", @@ -4333,5 +4334,10 @@ "empty": "Không có thiết bị trên liên kết này.", "expires": "Hết hạn", "back_to_list": "Quay lại danh sách khách" + }, + "ux35": { + "resize_sidebar": "Đổi độ rộng thanh bên", + "open_menu": "Mở menu", + "theme_mode_hint": "Sáng và Tối là mặc định. Tùy chỉnh bật khi bạn chỉnh màu bên dưới." } } diff --git a/web-nodejs/lang/zh-TW.json b/web-nodejs/lang/zh-TW.json index 43d572b4..5d7f4833 100644 --- a/web-nodejs/lang/zh-TW.json +++ b/web-nodejs/lang/zh-TW.json @@ -2866,7 +2866,8 @@ "dark": "深色", "light": "淺色", "auto": "跟隨系統", - "title": "主題" + "title": "主題", + "custom": "自訂" }, "days": { "mon": "週一", @@ -4331,5 +4332,10 @@ "empty": "此訪客連結上沒有裝置。", "expires": "到期", "back_to_list": "返回訪客清單" + }, + "ux35": { + "resize_sidebar": "調整側邊欄寬度", + "open_menu": "開啟選單", + "theme_mode_hint": "淺色與深色為內建。編輯下方顏色時會啟用自訂。" } } diff --git a/web-nodejs/lang/zh.json b/web-nodejs/lang/zh.json index 4e0c7fbe..3aea0fcb 100644 --- a/web-nodejs/lang/zh.json +++ b/web-nodejs/lang/zh.json @@ -2849,7 +2849,8 @@ "dark": "深色", "light": "浅色", "auto": "跟随系统", - "title": "主题" + "title": "主题", + "custom": "自定义" }, "days": { "mon": "周一", @@ -4327,5 +4328,10 @@ "empty": "此访客链接上没有设备。", "expires": "过期时间", "back_to_list": "返回访客列表" + }, + "ux35": { + "resize_sidebar": "调整侧边栏宽度", + "open_menu": "打开菜单", + "theme_mode_hint": "浅色和深色为内置。编辑下方颜色时将启用自定义。" } } diff --git a/web-nodejs/public/css/beta31.css b/web-nodejs/public/css/beta31.css index 6dcf2c7d..7531ba95 100644 --- a/web-nodejs/public/css/beta31.css +++ b/web-nodejs/public/css/beta31.css @@ -1,5 +1,10 @@ /** - * BetterDesk Console — Beta 3.1 Panel + * @deprecated Retired Beta 3.1 overlay styles — replaced by ux35.css / ux35-tokens.css. + * Not loaded by layouts/main.ejs. + */ + +/** + * BetterDesk Console — Beta 3.1 Panel (legacy reference) * ISPmanager-inspired layout with persistent sidebar + content area */ diff --git a/web-nodejs/public/css/ux35-tokens.css b/web-nodejs/public/css/ux35-tokens.css new file mode 100644 index 00000000..87dd7929 --- /dev/null +++ b/web-nodejs/public/css/ux35-tokens.css @@ -0,0 +1,68 @@ +/** + * BetterDesk Console — UX 3.5 Design Tokens + * Spacing scale, chrome dimensions, glass surfaces for the default shell. + */ + +:root { + /* Spacing scale (4 / 8 / 12 / 16 / 24 / 32) */ + --ux35-space-1: 4px; + --ux35-space-2: 8px; + --ux35-space-3: 12px; + --ux35-space-4: 16px; + --ux35-space-5: 24px; + --ux35-space-6: 32px; + + /* Content padding — pages must use these, not ad-hoc values */ + --ux35-content-padding: var(--ux35-space-4); + --ux35-content-padding-lg: var(--ux35-space-5); + --ux35-card-gap: var(--ux35-space-4); + --ux35-section-gap: var(--ux35-space-5); + + /* Chrome */ + --ux35-topbar-height: 48px; + --ux35-sidebar-width: 220px; + --ux35-sidebar-width-min: 200px; + --ux35-sidebar-width-max: 320px; + --ux35-radius: 8px; + --ux35-radius-sm: 6px; + --ux35-motion: 150ms; + --ux35-motion-fast: 120ms; + + /* Semantic aliases → existing branding / glass tokens */ + --ux35-bg: var(--bg-primary); + --ux35-topbar-bg: var(--surface-glass-bg-elevated, var(--bg-secondary)); + --ux35-sidebar-bg: var(--surface-glass-bg-secondary, var(--bg-secondary)); + --ux35-card-bg: var(--card-bg, var(--surface-glass-bg-secondary, var(--bg-secondary))); + --ux35-border: var(--surface-glass-border, var(--border-primary)); + --ux35-border-light: var(--border-secondary); + --ux35-text: var(--text-primary); + --ux35-muted: var(--text-secondary); + --ux35-hover: var(--bg-hover); + --ux35-active-bg: var(--accent-blue-muted); + --ux35-primary: var(--accent-blue); + --ux35-topbar-fg: #fff; + --ux35-topbar-fg-muted: rgba(255, 255, 255, 0.75); + --ux35-glass-blur: var(--surface-glass-blur, 16px); + --ux35-glass-saturate: var(--surface-glass-saturate, 1.2); +} + +[data-theme="light"] { + --ux35-topbar-bg: var(--accent-blue, #2b579a); + --ux35-sidebar-bg: var(--surface-glass-bg-secondary, rgba(255, 255, 255, 0.82)); + --ux35-card-bg: var(--card-bg, rgba(255, 255, 255, 0.9)); + --ux35-bg: var(--bg-primary, #f0f2f5); + --ux35-border: var(--border-primary, #dde1e6); + --ux35-border-light: var(--border-secondary, #eef0f3); + --ux35-text: var(--text-primary, #1f2328); + --ux35-muted: var(--text-secondary, #656d76); + --ux35-hover: rgba(0, 0, 0, 0.04); + --ux35-active-bg: var(--accent-blue-muted, #e8eef7); + --ux35-primary: var(--accent-blue, #0969da); +} + +@media (prefers-reduced-motion: reduce) { + :root { + --ux35-motion: 0ms; + --ux35-motion-fast: 0ms; + } +} diff --git a/web-nodejs/public/css/ux35.css b/web-nodejs/public/css/ux35.css new file mode 100644 index 00000000..9b24b3f6 --- /dev/null +++ b/web-nodejs/public/css/ux35.css @@ -0,0 +1,447 @@ +/** + * BetterDesk Console — UX 3.5 Shell + * Native topbar + full-list sidebar (promoted from Beta 3.1), with glass surfaces. + */ + +/* ── App shell ── */ +body.app-page.ux35-page { + margin: 0; + background: var(--ux35-bg); + color: var(--ux35-text); + font-family: var(--font-family); +} + +.ux35-shell { + display: flex; + flex-direction: column; + height: 100vh; + height: 100dvh; + overflow: hidden; +} + +.ux35-shell.embed-mode { + height: auto; + min-height: 100vh; +} + +/* ── Topbar ── */ +.ux35-topbar { + height: var(--ux35-topbar-height); + min-height: var(--ux35-topbar-height); + background: var(--ux35-topbar-bg); + color: var(--ux35-topbar-fg); + display: flex; + align-items: center; + padding: 0 var(--ux35-space-4); + gap: var(--ux35-space-3); + z-index: 40; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18); + backdrop-filter: blur(var(--ux35-glass-blur)) saturate(var(--ux35-glass-saturate)); + -webkit-backdrop-filter: blur(var(--ux35-glass-blur)) saturate(var(--ux35-glass-saturate)); + flex-shrink: 0; +} + +.ux35-topbar-logo { + display: flex; + align-items: center; + gap: var(--ux35-space-2); + font-weight: 700; + font-size: 15px; + letter-spacing: 0.02em; + white-space: nowrap; + color: inherit; + text-decoration: none; +} + +.ux35-topbar-logo .material-icons { font-size: 22px; } +.ux35-topbar-logo img { height: 24px; width: auto; } +.ux35-topbar-logo .brand-text-accent { opacity: 0.85; } + +.ux35-topbar-menu-btn { + display: none; + background: none; + border: none; + color: var(--ux35-topbar-fg-muted); + padding: 6px; + border-radius: var(--ux35-radius-sm); + cursor: pointer; +} +.ux35-topbar-menu-btn:hover { background: rgba(255, 255, 255, 0.12); color: #fff; } +.ux35-topbar-menu-btn .material-icons { font-size: 22px; } + +.ux35-topbar-breadcrumb { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + color: var(--ux35-topbar-fg-muted); + min-width: 0; + overflow: hidden; +} +.ux35-topbar-breadcrumb a { + color: var(--ux35-topbar-fg-muted); + text-decoration: none; +} +.ux35-topbar-breadcrumb a:hover { color: #fff; } +.ux35-topbar-breadcrumb .current { color: #fff; font-weight: 600; } +.ux35-topbar-breadcrumb .separator { opacity: 0.5; } + +.ux35-topbar-spacer { flex: 1; } + +.ux35-topbar-actions { + display: flex; + align-items: center; + gap: 4px; +} + +.ux35-topbar-btn { + background: none; + border: none; + color: var(--ux35-topbar-fg-muted); + padding: 6px; + border-radius: var(--ux35-radius-sm); + cursor: pointer; + display: flex; + align-items: center; + position: relative; + transition: background var(--ux35-motion-fast), color var(--ux35-motion-fast); +} +.ux35-topbar-btn:hover { background: rgba(255, 255, 255, 0.12); color: #fff; } +.ux35-topbar-btn .material-icons { font-size: 20px; } + +.ux35-topbar .notif-badge { + position: absolute; + top: 2px; + right: 2px; + min-width: 16px; + height: 16px; + padding: 0 4px; + border-radius: 8px; + background: var(--accent-red, #f85149); + color: #fff; + font-size: 10px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; +} + +.ux35-topbar .lang-selector, +.ux35-topbar .notif-wrapper { + position: relative; +} + +.ux35-topbar .lang-dropdown, +.ux35-topbar .notif-dropdown { + right: 0; + top: calc(100% + 6px); +} + +/* ── Body: sidebar + content ── */ +.ux35-body { + flex: 1; + display: flex; + overflow: hidden; + min-height: 0; +} + +/* ── Sidebar ── */ +.ux35-sidebar { + width: var(--ux35-sidebar-width); + min-width: var(--ux35-sidebar-width); + max-width: var(--ux35-sidebar-width-max); + background: var(--ux35-sidebar-bg); + border-right: 1px solid var(--ux35-border); + display: flex; + flex-direction: column; + overflow: hidden; + flex-shrink: 0; + backdrop-filter: blur(var(--ux35-glass-blur)) saturate(var(--ux35-glass-saturate)); + -webkit-backdrop-filter: blur(var(--ux35-glass-blur)) saturate(var(--ux35-glass-saturate)); + z-index: 30; + transition: transform var(--ux35-motion) ease, box-shadow var(--ux35-motion) ease; +} + +.ux35-sidebar-nav { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding-bottom: var(--ux35-space-2); +} + +.ux35-sidebar-section { + padding: 6px 0; + border-bottom: 1px solid var(--ux35-border); +} +.ux35-sidebar-section:last-of-type { border-bottom: none; } + +.ux35-sidebar-heading { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--ux35-muted); + padding: 10px 16px 4px; +} + +.ux35-sidebar-item { + display: flex; + align-items: center; + gap: 10px; + padding: 7px 16px; + font-size: 13px; + color: var(--ux35-text); + cursor: pointer; + transition: background var(--ux35-motion-fast), color var(--ux35-motion-fast); + border: none; + background: none; + width: 100%; + text-align: left; + border-left: 3px solid transparent; + text-decoration: none; + box-sizing: border-box; +} +.ux35-sidebar-item:hover { + background: var(--ux35-hover); + color: var(--ux35-primary); +} +.ux35-sidebar-item.active { + background: var(--ux35-active-bg); + color: var(--ux35-primary); + font-weight: 600; + border-left-color: var(--ux35-primary); +} +.ux35-sidebar-item .material-icons { + font-size: 20px; + opacity: 0.7; + flex-shrink: 0; +} +.ux35-sidebar-item.active .material-icons { opacity: 1; } +.ux35-sidebar-item .sidebar-link-external { + font-size: 14px; + margin-left: auto; + opacity: 0.5; +} +.ux35-sidebar-item-badge, +.ux35-sidebar .badge-sidebar { + margin-left: auto; + background: var(--ux35-primary); + color: #fff; + font-size: 10px; + padding: 1px 6px; + border-radius: 10px; + font-weight: 600; + flex-shrink: 0; +} +.ux35-sidebar .badge-sidebar--alpha { + background: linear-gradient(135deg, #a371f7, #58a6ff); +} + +.ux35-sidebar-footer { + border-top: 1px solid var(--ux35-border); + padding: var(--ux35-space-3) var(--ux35-space-4); + flex-shrink: 0; +} +.ux35-sidebar-user { + display: flex; + align-items: center; + gap: var(--ux35-space-3); + min-width: 0; +} +.ux35-sidebar-user-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + background: var(--ux35-active-bg); + color: var(--ux35-primary); + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 14px; + flex-shrink: 0; +} +.ux35-sidebar-user-info { min-width: 0; } +.ux35-sidebar-user-name { + font-size: 13px; + font-weight: 600; + color: var(--ux35-text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.ux35-sidebar-user-role { + font-size: 11px; + color: var(--ux35-muted); +} +.ux35-sidebar-attribution { + margin-top: var(--ux35-space-2); + font-size: 10px; + color: var(--ux35-muted); + line-height: 1.4; +} + +/* Resize handle */ +.ux35-sidebar-resize { + position: absolute; + top: 0; + right: 0; + width: 4px; + height: 100%; + cursor: col-resize; + z-index: 5; +} +.ux35-sidebar-resize:hover, +.ux35-sidebar-resize.active { + background: var(--ux35-primary); + opacity: 0.35; +} +.ux35-sidebar { position: relative; } + +/* Overlay for mobile drawer */ +.ux35-sidebar-overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.45); + z-index: 25; + opacity: 0; + transition: opacity var(--ux35-motion) ease; +} +.ux35-sidebar-overlay.visible { + display: block; + opacity: 1; +} + +/* ── Main content ── */ +.ux35-main { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + overflow: hidden; + background: var(--ux35-bg); +} + +.ux35-content.main-content { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: var(--ux35-content-padding-lg); + margin-top: 0; + height: auto; + width: 100%; + animation: ux35PageEnter var(--ux35-motion) ease-out; + overscroll-behavior: contain; + scrollbar-gutter: stable; + -webkit-overflow-scrolling: touch; +} + +/* Tighten page chrome spacing inside the shell */ +.ux35-content > .dashboard-header, +.ux35-content > .devices-header, +.ux35-content > .page-header, +.ux35-content > .settings-header, +.ux35-content .b31-page-header { + margin-bottom: var(--ux35-section-gap); +} + +.ux35-content .dashboard-stats, +.ux35-content .devices-page, +.ux35-content .settings-layout, +.ux35-content .card, +.ux35-content .settings-panel { + margin-bottom: var(--ux35-card-gap); +} + +.ux35-content .settings-tabs { + gap: var(--ux35-space-2); +} + +/* Settings page density under UX 3.5 */ +.ux35-page .settings-layout { + gap: var(--ux35-space-4); +} +.ux35-page .settings-panel { + border-radius: var(--ux35-radius); + background: var(--ux35-card-bg); + border: 1px solid var(--ux35-border); + backdrop-filter: blur(var(--ux35-glass-blur)); +} +.ux35-page .settings-panel-header { + padding: var(--ux35-space-4); + border-bottom: 1px solid var(--ux35-border); +} +.ux35-page .dashboard-stats .stat-card, +.ux35-page .card { + border-radius: var(--ux35-radius); + background: var(--ux35-card-bg); + border: 1px solid var(--ux35-border); + backdrop-filter: blur(var(--ux35-glass-blur)); +} +.ux35-page .devices-page { + gap: var(--ux35-space-3); +} + +@keyframes ux35PageEnter { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } +} + +@media (prefers-reduced-motion: reduce) { + .ux35-content { animation: none; } +} + +/* Embed: no chrome */ +body.embed-mode .ux35-topbar, +body.embed-mode .ux35-sidebar, +body.embed-mode .ux35-sidebar-overlay, +body.embed-mode .mobile-bottom-nav { + display: none !important; +} +body.embed-mode .ux35-body { display: block; } +body.embed-mode .ux35-content { padding: var(--ux35-content-padding); } + +/* Emergency banner sits above topbar */ +.ux35-shell > .emergency-banner { + flex-shrink: 0; +} + +/* ── Responsive ── */ +@media (max-width: 1099px) { + .ux35-topbar-menu-btn { display: flex; } + .ux35-topbar-breadcrumb { display: none; } + + .ux35-sidebar { + position: fixed; + top: var(--ux35-topbar-height); + left: 0; + bottom: 0; + height: auto; + transform: translateX(-105%); + box-shadow: none; + } + .ux35-shell.ux35-drawer-open .ux35-sidebar { + transform: translateX(0); + box-shadow: 4px 0 24px rgba(0, 0, 0, 0.25); + } + .ux35-sidebar-resize { display: none; } +} + +@media (max-width: 767px) { + .ux35-content { + padding: var(--ux35-content-padding); + padding-bottom: calc(var(--ux35-content-padding) + 72px); + } + .ux35-sidebar { + width: min(86vw, 280px); + min-width: min(86vw, 280px); + max-width: 280px; + } +} + +/* Hide legacy rail layout leftovers if any */ +.app-page.ux35-page .app-layout > .sidebar, +.app-page.ux35-page .main-wrapper > .navbar { + display: none; +} diff --git a/web-nodejs/public/css/variables.css b/web-nodejs/public/css/variables.css index d0b788cd..98cf06f5 100644 --- a/web-nodejs/public/css/variables.css +++ b/web-nodejs/public/css/variables.css @@ -123,6 +123,7 @@ --space-xs: 0.25rem; /* 4px */ --space-sm: 0.5rem; /* 8px */ + --space-3: 0.75rem; /* 12px — UX 3.5 scale */ --space-md: 1rem; /* 16px */ --space-lg: 1.5rem; /* 24px */ --space-xl: 2rem; /* 32px */ diff --git a/web-nodejs/public/js/app.js b/web-nodejs/public/js/app.js index 7e285a8e..5601702e 100644 --- a/web-nodejs/public/js/app.js +++ b/web-nodejs/public/js/app.js @@ -9,57 +9,11 @@ document.addEventListener('DOMContentLoaded', init); function init() { - initSidebar(); initNavbar(); initLanguageSelector(); initUserMenu(); initRefreshButton(); initRegistrationBadge(); - initDesktopMode(); - } - - /** - * Sidebar functionality (mobile support) - */ - function initSidebar() { - const sidebar = document.getElementById('sidebar'); - const overlay = document.getElementById('sidebar-overlay'); - - if (!sidebar) return; - - // Close sidebar on overlay click (legacy desktop sidebar) - overlay?.addEventListener('click', () => { - sidebar.classList.remove('open'); - }); - - // Close sidebar when navigating (legacy) - sidebar.querySelectorAll('.sidebar-link, .sidebar-rail-btn[href]').forEach(link => { - link.addEventListener('click', () => { - if (window.innerWidth <= 1024) { - sidebar.classList.remove('open'); - } - }); - }); - - // Mobile menu button (hidden when mobile-shell uses bottom nav More) - const mobileMenuBtn = document.getElementById('mobile-menu-btn'); - if (mobileMenuBtn) { - mobileMenuBtn.addEventListener('click', () => { - if (window.MobileNav && window.DeviceCapabilities?.isMobileShell()) { - window.MobileNav.openDrawer(); - return; - } - sidebar.classList.toggle('open'); - }); - - function checkMobile() { - const useLegacy = window.innerWidth <= 1024 - && !(window.DeviceCapabilities && window.DeviceCapabilities.isMobileShell()); - mobileMenuBtn.style.display = useLegacy ? 'flex' : 'none'; - } - checkMobile(); - window.addEventListener('resize', Utils.debounce(checkMobile, 200)); - } } /** @@ -208,13 +162,4 @@ badgeInterval = setInterval(updateBadge, 30000); // every 30s } - /** - * Desktop mode initialization - */ - function initDesktopMode() { - if (window.DesktopMode && typeof window.DesktopMode.init === 'function') { - window.DesktopMode.init(); - } - } - })(); diff --git a/web-nodejs/public/js/beta31.js b/web-nodejs/public/js/beta31.js index 2bf080f2..e5904818 100644 --- a/web-nodejs/public/js/beta31.js +++ b/web-nodejs/public/js/beta31.js @@ -1,265 +1,14 @@ /** - * BetterDesk Console — Beta 3.1 Panel Controller - * Manages the ISPmanager-inspired shell: sidebar navigation, content loading + * @deprecated Retired — Beta 3.1 overlay replaced by native UX 3.5 shell. + * Kept for reference only; not loaded by layouts/main.ejs. */ - (function () { 'use strict'; - - const STORAGE_KEY = 'bd_beta31_active'; - const STORAGE_PANEL = 'bd_beta31_panel'; - let shell = null; - let contentArea = null; - let currentPanel = null; - - const PANEL_ROUTES = { - dashboard: '/', - cdap: '/cdap' - }; - - /* ────────────────────────────────────────────── - Menu definition — mirrors sidebar.ejs entries - ────────────────────────────────────────────── */ - function getMenuSections() { - const t = (k, fb) => (typeof _ === 'function' ? _(k) : fb) || fb; - return [ - { - heading: t('nav.main', 'Main'), - items: [ - { id: 'dashboard', icon: 'dashboard', label: t('nav.dashboard', 'Dashboard') }, - { id: 'devices', icon: 'devices', label: t('nav.devices', 'Devices') }, - { id: 'registrations', icon: 'how_to_reg', label: t('nav.registrations', 'Registrations') }, - ] - }, - { - heading: t('nav.management', 'Management'), - items: [ - { id: 'inventory', icon: 'inventory_2', label: t('nav.inventory', 'Inventory') }, - { id: 'tickets', icon: 'confirmation_number', label: t('nav.tickets', 'Tickets') }, - { id: 'help-requests', icon: 'support_agent', label: t('nav.help_requests', 'Help Requests') }, - { id: 'automation', icon: 'smart_toy', label: t('nav.automation', 'Automation') }, - { id: 'network', icon: 'lan', label: t('nav.network', 'Network') }, - { id: 'activity', icon: 'timeline', label: t('nav.activity', 'Activity') }, - { id: 'cdap', icon: 'developer_board', label: t('nav.cdap', 'CDAP') }, - { id: 'fleet', icon: 'hub', label: t('nav.fleet', 'Fleet') }, - { id: 'scaling', icon: 'cell_tower', label: t('nav.scaling', 'Scaling') }, - { id: 'cross-platform', icon: 'devices', label: t('nav.cross_platform', 'Cross-Platform') }, - { id: 'chat', icon: 'chat', label: t('nav.chat', 'Chat') }, - ] - }, - { - heading: t('nav.tools', 'Tools'), - items: [ - { id: 'reports', icon: 'assessment', label: t('nav.reports', 'Reports') }, - { id: 'keys', icon: 'vpn_key', label: t('nav.keys', 'Keys') }, - { id: 'generator', icon: 'build', label: t('nav.generator', 'Generator') }, - { id: 'remote', icon: 'connected_tv', label: t('nav.remote', 'Remote Desktop') }, - { id: 'toolkit', icon: 'handyman', label: t('nav.toolkit', 'Toolkit') }, - { id: 'cdap-studio', icon: 'account_tree', label: t('nav.sdk_studio', 'SDK Studio') }, - ] - }, - { - heading: t('nav.system', 'System'), - items: [ - { id: 'tokens', icon: 'token', label: t('nav.tokens', 'Tokens') }, - { id: 'organizations', icon: 'corporate_fare', label: t('nav.organizations', 'Organizations') }, - { id: 'policies', icon: 'policy', label: t('nav.policies', 'Policies') }, - { id: 'attestation', icon: 'fingerprint', label: t('nav.attestation', 'Attestation') }, - { id: 'dataguard', icon: 'shield', label: t('nav.dataguard', 'DataGuard') }, - { id: 'users', icon: 'group', label: t('nav.users', 'Users') }, - { id: 'security-audit', icon: 'security', label: t('nav.security_audit', 'Security Audit') }, - { id: 'permissions', icon: 'admin_panel_settings', label: t('nav.permissions', 'Permissions') }, - { id: 'languages', icon: 'translate', label: t('nav.languages', 'Languages') }, - { id: 'resource-control', icon: 'tune', label: t('nav.resource_control', 'Resource Control') }, - ] - }, - { - heading: t('nav.settings', 'Settings'), - items: [ - { id: 'settings', icon: 'settings', label: t('nav.settings', 'Settings') }, - ] - } - ]; - } - - /* ────────────────────────────────────────────── - Build the shell DOM - ────────────────────────────────────────────── */ - function buildShell() { - if (document.getElementById('beta31-shell')) return; - - const appName = (window.BetterDesk && window.BetterDesk.branding && window.BetterDesk.branding.appName) || 'BetterDesk'; - const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; - - shell = document.createElement('div'); - shell.id = 'beta31-shell'; - shell.className = 'beta31-shell' + (isDark ? ' b31-dark' : ''); - - // Build topbar - const topbar = document.createElement('div'); - topbar.className = 'b31-topbar'; - topbar.innerHTML = ` - -
-
- - - -
- `; - - // Build body (sidebar + content) - const body = document.createElement('div'); - body.className = 'b31-body'; - - // Sidebar - const sidebar = document.createElement('nav'); - sidebar.className = 'b31-sidebar'; - sidebar.id = 'b31-sidebar'; - - const sections = getMenuSections(); - sections.forEach(sec => { - const div = document.createElement('div'); - div.className = 'b31-sidebar-section'; - div.innerHTML = `
${escHtml(sec.heading)}
`; - sec.items.forEach(item => { - const btn = document.createElement('button'); - btn.className = 'b31-sidebar-item'; - btn.dataset.panel = item.id; - btn.innerHTML = `${item.icon}${escHtml(item.label)}`; - btn.addEventListener('click', () => navigateTo(item.id)); - div.appendChild(btn); - }); - sidebar.appendChild(div); - }); - - // Content - contentArea = document.createElement('div'); - contentArea.className = 'b31-content'; - contentArea.id = 'b31-content'; - - body.appendChild(sidebar); - body.appendChild(contentArea); - - shell.appendChild(topbar); - shell.appendChild(body); - document.body.appendChild(shell); - - // Event listeners - document.getElementById('b31-exit-btn').addEventListener('click', deactivate); - document.getElementById('b31-refresh-btn').addEventListener('click', () => { - if (currentPanel) navigateTo(currentPanel); - }); - document.getElementById('b31-theme-btn').addEventListener('click', toggleTheme); - } - - /* ────────────────────────────────────────────── - Navigation — load panel content via fetch - ────────────────────────────────────────────── */ - function navigateTo(panelId) { - if (!contentArea) return; - currentPanel = panelId; - localStorage.setItem(STORAGE_PANEL, panelId); - - // Update sidebar active state - shell.querySelectorAll('.b31-sidebar-item').forEach(el => { - el.classList.toggle('active', el.dataset.panel === panelId); - }); - - // Show loading - contentArea.classList.remove('has-frame'); - contentArea.innerHTML = '
sync Loading…
'; - - renderEmbeddedPanel(panelId); - } - - function getPanelUrl(panelId) { - const route = PANEL_ROUTES[panelId] || '/' + panelId; - const url = new URL(route, window.location.origin); - url.searchParams.set('embed', '1'); - url.searchParams.set('beta31', '1'); - return url.pathname + url.search; - } - - function renderEmbeddedPanel(panelId) { - const frame = document.createElement('iframe'); - frame.className = 'b31-panel-frame'; - frame.title = panelId; - frame.src = getPanelUrl(panelId); - frame.loading = 'eager'; - frame.addEventListener('load', () => { - frame.classList.add('loaded'); - }); - contentArea.classList.add('has-frame'); - contentArea.innerHTML = ''; - contentArea.appendChild(frame); - } - - /* ────────────────────────────────────────────── - Activate / Deactivate - ────────────────────────────────────────────── */ - function activate() { - buildShell(); - shell.classList.add('active'); - document.body.style.overflow = 'hidden'; - localStorage.setItem(STORAGE_KEY, '1'); - - // Navigate to saved or default panel - const saved = localStorage.getItem(STORAGE_PANEL) || 'dashboard'; - navigateTo(saved); - } - - function deactivate() { - if (shell) shell.classList.remove('active'); - document.body.style.overflow = ''; - localStorage.setItem(STORAGE_KEY, '0'); - } - - function toggleTheme() { - if (!shell) return; - shell.classList.toggle('b31-dark'); - const icon = document.querySelector('#b31-theme-btn .material-icons'); - if (icon) icon.textContent = shell.classList.contains('b31-dark') ? 'light_mode' : 'dark_mode'; - } - - /* ────────────────────────────────────────────── - Helpers - ────────────────────────────────────────────── */ - function escHtml(s) { - const d = document.createElement('div'); - d.textContent = s || ''; - return d.innerHTML; - } - - /* ────────────────────────────────────────────── - Public API - ────────────────────────────────────────────── */ window.Beta31 = { - activate: activate, - deactivate: deactivate, - navigateTo: navigateTo, - isActive: () => shell && shell.classList.contains('active'), - init: function () { - // Beta 3.1 console retired — clear persisted state, do not auto-restore - localStorage.removeItem(STORAGE_KEY); - localStorage.removeItem(STORAGE_PANEL); - } + activate: function () {}, + deactivate: function () {}, + navigateTo: function () {}, + isActive: function () { return false; }, + init: function () {} }; - - // Auto-init on DOM ready - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => window.Beta31.init()); - } else { - window.Beta31.init(); - } })(); diff --git a/web-nodejs/public/js/mobile-nav.js b/web-nodejs/public/js/mobile-nav.js index e3bb72f4..bac54290 100644 --- a/web-nodejs/public/js/mobile-nav.js +++ b/web-nodejs/public/js/mobile-nav.js @@ -11,7 +11,91 @@ return window.DeviceCapabilities && window.DeviceCapabilities.isMobileShell(); } + function buildDrawerFromSidebar() { + var body = document.getElementById('mobile-more-drawer-body'); + if (!body || body.dataset.built === '1') return; + + /* UX 3.5: full-list sidebar sections */ + var sections = document.querySelectorAll('.ux35-sidebar-section'); + if (sections.length) { + sections.forEach(function(panel) { + var links = panel.querySelectorAll('a.ux35-sidebar-item'); + if (!links.length) return; + + var section = document.createElement('div'); + section.className = 'mobile-drawer-section'; + + var heading = panel.querySelector('.ux35-sidebar-heading'); + var title = document.createElement('div'); + title.className = 'mobile-drawer-section-title'; + title.textContent = heading ? heading.textContent.trim() : (panel.getAttribute('data-panel') || ''); + section.appendChild(title); + + links.forEach(function(link) { + var a = document.createElement('a'); + a.href = link.getAttribute('href') || '#'; + a.className = 'mobile-drawer-link'; + if (link.classList.contains('active')) a.classList.add('active'); + if (link.getAttribute('target')) a.target = link.getAttribute('target'); + + var icon = link.querySelector('.material-icons'); + if (icon) { + var ic = document.createElement('span'); + ic.className = 'material-icons'; + ic.textContent = icon.textContent; + a.appendChild(ic); + } + + var labels = link.querySelectorAll('span:not(.material-icons):not(.badge-sidebar):not(.ux35-sidebar-item-badge)'); + var span = document.createElement('span'); + span.textContent = labels.length ? labels[0].textContent.trim() : link.textContent.trim(); + a.appendChild(span); + + a.addEventListener('click', closeDrawer); + section.appendChild(a); + }); + + body.appendChild(section); + }); + body.dataset.built = '1'; + return; + } + + /* Legacy flyout fallback (should not run on UX 3.5) */ + var panels = document.querySelectorAll('.sidebar-flyout-panel'); + if (!panels.length) return; + + panels.forEach(function(panel) { + var links = panel.querySelectorAll('.sidebar-link'); + if (!links.length) return; + var section = document.createElement('div'); + section.className = 'mobile-drawer-section'; + var title = document.createElement('div'); + title.className = 'mobile-drawer-section-title'; + title.textContent = panel.getAttribute('data-panel') || ''; + section.appendChild(title); + links.forEach(function(link) { + var a = document.createElement('a'); + a.href = link.getAttribute('href') || '#'; + a.className = 'mobile-drawer-link'; + if (link.classList.contains('active')) a.classList.add('active'); + a.textContent = link.textContent.trim(); + a.addEventListener('click', closeDrawer); + section.appendChild(a); + }); + body.appendChild(section); + }); + + body.dataset.built = '1'; + } + function openDrawer() { + /* Prefer UX 3.5 sidebar drawer on narrow viewports */ + if (window.Ux35Shell && typeof window.Ux35Shell.openDrawer === 'function' + && window.matchMedia('(max-width: 1099px)').matches) { + window.Ux35Shell.openDrawer(); + return; + } var drawer = document.getElementById('mobile-more-drawer'); var btn = document.getElementById('mobile-more-btn'); if (!drawer) return; @@ -23,6 +107,9 @@ } function closeDrawer() { + if (window.Ux35Shell && typeof window.Ux35Shell.closeDrawer === 'function') { + window.Ux35Shell.closeDrawer(); + } var drawer = document.getElementById('mobile-more-drawer'); var btn = document.getElementById('mobile-more-btn'); if (!drawer) return; @@ -35,97 +122,6 @@ } } - function buildDrawerFromSidebar() { - var body = document.getElementById('mobile-more-drawer-body'); - if (!body || body.dataset.built === '1') return; - - var panels = document.querySelectorAll('.sidebar-flyout-panel'); - if (!panels.length) return; - - var categoryTitles = { - main: _('nav.main') || 'Main', - management: _('nav.management') || 'Management', - tools: _('nav.tools') || 'Tools', - system: _('nav.system') || 'System', - 'server-mgmt': _('nav.server_management') || 'Server', - commercialization: _('nav.commercialization') || 'Commercialization' - }; - - panels.forEach(function(panel) { - var key = panel.getAttribute('data-panel'); - var links = panel.querySelectorAll('.sidebar-link'); - if (!links.length) return; - - var section = document.createElement('div'); - section.className = 'mobile-drawer-section'; - - var title = document.createElement('div'); - title.className = 'mobile-drawer-section-title'; - title.textContent = categoryTitles[key] || key; - section.appendChild(title); - - links.forEach(function(link) { - var a = document.createElement('a'); - a.href = link.getAttribute('href') || '#'; - a.className = 'mobile-drawer-link'; - if (link.classList.contains('active')) a.classList.add('active'); - - var icon = link.querySelector('.material-icons'); - if (icon) { - var ic = document.createElement('span'); - ic.className = 'material-icons'; - ic.textContent = icon.textContent; - a.appendChild(ic); - } - - var text = link.querySelector('.sidebar-link-text'); - var span = document.createElement('span'); - span.textContent = text ? text.textContent.trim() : link.textContent.trim(); - a.appendChild(span); - - a.addEventListener('click', function() { - closeDrawer(); - }); - section.appendChild(a); - }); - - body.appendChild(section); - }); - - /* Settings + attestation from rail */ - var railLinks = document.querySelectorAll('.sidebar-rail-nav a.sidebar-rail-btn[href]'); - if (railLinks.length) { - var railSection = document.createElement('div'); - railSection.className = 'mobile-drawer-section'; - var railTitle = document.createElement('div'); - railTitle.className = 'mobile-drawer-section-title'; - railTitle.textContent = _('nav.settings') || 'Settings'; - railSection.appendChild(railTitle); - - railLinks.forEach(function(link) { - var a = document.createElement('a'); - a.href = link.getAttribute('href'); - a.className = 'mobile-drawer-link'; - if (link.classList.contains('active')) a.classList.add('active'); - var icon = link.querySelector('.material-icons'); - if (icon) { - var ic = document.createElement('span'); - ic.className = 'material-icons'; - ic.textContent = icon.textContent; - a.appendChild(ic); - } - var span = document.createElement('span'); - span.textContent = link.getAttribute('title') || link.textContent.trim(); - a.appendChild(span); - a.addEventListener('click', closeDrawer); - railSection.appendChild(a); - }); - body.appendChild(railSection); - } - - body.dataset.built = '1'; - } - function initSwipeGestures() { document.addEventListener('touchstart', function(e) { if (!isMobileShell()) return; diff --git a/web-nodejs/public/js/settings.js b/web-nodejs/public/js/settings.js index 6e9949f2..5b1b3e52 100644 --- a/web-nodejs/public/js/settings.js +++ b/web-nodejs/public/js/settings.js @@ -1488,6 +1488,12 @@ if (hex) hex.value = value; } } + + // Theme mode (dark / light / custom) + const themeMode = data.themeMode === 'auto' ? 'dark' : (data.themeMode || 'dark'); + const themeRadio = document.querySelector(`input[name="theme-mode"][value="${themeMode}"]`) + || document.querySelector('input[name="theme-mode"][value="dark"]'); + if (themeRadio) themeRadio.checked = true; // Background & appearance (console) const setVal = (id, val) => { const el = document.getElementById(id); if (el != null && el) el.value = val; }; @@ -2139,6 +2145,8 @@ const key = picker.dataset.color; const hex = document.querySelector(`.color-hex[data-color="${key}"]`); if (hex) hex.value = picker.value; + const customRadio = document.querySelector('input[name="theme-mode"][value="custom"]'); + if (customRadio) customRadio.checked = true; onBrandingFieldChange(); }); }); @@ -2150,9 +2158,44 @@ if (picker && /^#[0-9a-fA-F]{6}$/.test(hex.value)) { picker.value = hex.value; } + const customRadio = document.querySelector('input[name="theme-mode"][value="custom"]'); + if (customRadio) customRadio.checked = true; onBrandingFieldChange(); }); }); + + document.querySelectorAll('input[name="theme-mode"]').forEach(radio => { + radio.addEventListener('change', () => { + if (!radio.checked) return; + if (radio.value === 'light') { + applyBuiltInPalette({ + bgPrimary: '#ffffff', bgSecondary: '#f6f8fa', bgTertiary: '#eaeef2', bgElevated: '#ffffff', + textPrimary: '#1f2328', textSecondary: '#656d76', + accentBlue: '#0969da', accentBlueHover: '#0550ae', + accentGreen: '#1a7f37', accentRed: '#cf222e', accentYellow: '#9a6700', accentPurple: '#8250df', + borderPrimary: '#d0d7de', borderSecondary: '#eaeef2' + }); + } else if (radio.value === 'dark') { + applyBuiltInPalette({ + bgPrimary: '#0d1117', bgSecondary: '#161b22', bgTertiary: '#21262d', bgElevated: '#30363d', + textPrimary: '#e6edf3', textSecondary: '#8b949e', + accentBlue: '#58a6ff', accentBlueHover: '#79c0ff', + accentGreen: '#2ea44f', accentRed: '#f85149', accentYellow: '#d29922', accentPurple: '#a371f7', + borderPrimary: '#30363d', borderSecondary: '#21262d' + }); + } + onBrandingFieldChange(); + }); + }); + } + + function applyBuiltInPalette(colors) { + Object.entries(colors).forEach(([key, value]) => { + const picker = document.querySelector(`.color-picker[data-color="${key}"]`); + const hex = document.querySelector(`.color-hex[data-color="${key}"]`); + if (picker) picker.value = value; + if (hex) hex.value = value; + }); } /** @@ -2181,6 +2224,8 @@ data.colors[key] = value; } }); + + data.themeMode = document.querySelector('input[name="theme-mode"]:checked')?.value || 'dark'; // Background & appearance (console) data.bgType = document.querySelector('input[name="bg-type"]:checked')?.value || 'none'; diff --git a/web-nodejs/public/js/ux35-shell.js b/web-nodejs/public/js/ux35-shell.js new file mode 100644 index 00000000..a81fd677 --- /dev/null +++ b/web-nodejs/public/js/ux35-shell.js @@ -0,0 +1,224 @@ +/** + * BetterDesk Console — UX 3.5 Shell Controller + * Drawer (tablet/mobile), sidebar resize, theme icon sync. + */ +(function () { + 'use strict'; + + var STORAGE_WIDTH = 'bd_ux35_sidebar_width'; + var WIDTH_MIN = 200; + var WIDTH_MAX = 320; + var WIDTH_DEFAULT = 220; + var MQ_DRAWER = window.matchMedia('(max-width: 1099px)'); + + function t(key, fb) { + return (typeof _ === 'function' ? _(key) : fb) || fb; + } + + function clamp(n, min, max) { + return Math.min(max, Math.max(min, n)); + } + + function applySidebarWidth(px) { + var w = clamp(px, WIDTH_MIN, WIDTH_MAX); + document.documentElement.style.setProperty('--ux35-sidebar-width', w + 'px'); + return w; + } + + function restoreSidebarWidth() { + if (MQ_DRAWER.matches) return; + try { + var saved = parseInt(localStorage.getItem(STORAGE_WIDTH), 10); + if (saved && !isNaN(saved)) applySidebarWidth(saved); + else applySidebarWidth(WIDTH_DEFAULT); + } catch (e) { + applySidebarWidth(WIDTH_DEFAULT); + } + } + + function setDrawerOpen(open) { + var shell = document.getElementById('app'); + var overlay = document.getElementById('ux35-sidebar-overlay'); + var btn = document.getElementById('ux35-menu-btn'); + if (!shell) return; + shell.classList.toggle('ux35-drawer-open', open); + if (overlay) { + overlay.hidden = !open; + overlay.classList.toggle('visible', open); + } + if (btn) btn.setAttribute('aria-expanded', open ? 'true' : 'false'); + document.body.style.overflow = open && MQ_DRAWER.matches ? 'hidden' : ''; + } + + function syncThemeIcon() { + var icon = document.getElementById('ux35-theme-icon'); + if (!icon) return; + var mode = document.documentElement.getAttribute('data-theme-mode') || 'dark'; + var theme = document.documentElement.getAttribute('data-theme') || 'dark'; + if (mode === 'custom') { + icon.textContent = 'palette'; + } else if (theme === 'light') { + icon.textContent = 'light_mode'; + } else { + icon.textContent = 'dark_mode'; + } + } + + /** + * Cycle visual preview: dark ↔ light. + * Server-side themeMode (Settings → Branding) is the source of truth; + * this only toggles local data-theme for immediate feedback when mode is dark/light. + * Custom mode opens Settings branding. + */ + function cycleThemePreview() { + var mode = document.documentElement.getAttribute('data-theme-mode') || 'dark'; + if (mode === 'custom') { + window.location.href = '/settings#branding'; + return; + } + var next = document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light'; + document.documentElement.setAttribute('data-theme', next); + syncThemeIcon(); + // Persist via branding API when operator has permission (best-effort) + if (window.BetterDesk && window.BetterDesk.csrfToken) { + fetch('/api/settings/branding', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': window.BetterDesk.csrfToken + }, + credentials: 'same-origin', + body: JSON.stringify({ themeMode: next }) + }).then(function (res) { + if (res.ok) { + document.documentElement.setAttribute('data-theme-mode', next); + var link = document.querySelector('link[href*="branding.css"]'); + if (link) { + var url = new URL(link.href, window.location.origin); + url.searchParams.set('v', String(Date.now())); + link.href = url.pathname + url.search; + } + } + }).catch(function () { /* ignore */ }); + } + } + + function initResize() { + var handle = document.getElementById('ux35-sidebar-resize'); + if (!handle) return; + var dragging = false; + + handle.addEventListener('mousedown', function (e) { + if (MQ_DRAWER.matches) return; + dragging = true; + handle.classList.add('active'); + e.preventDefault(); + }); + + window.addEventListener('mousemove', function (e) { + if (!dragging) return; + var sidebar = document.getElementById('ux35-sidebar'); + if (!sidebar) return; + var rect = sidebar.getBoundingClientRect(); + var w = applySidebarWidth(e.clientX - rect.left); + try { localStorage.setItem(STORAGE_WIDTH, String(w)); } catch (err) { /* ignore */ } + }); + + window.addEventListener('mouseup', function () { + if (!dragging) return; + dragging = false; + handle.classList.remove('active'); + }); + } + + function initDrawer() { + var menuBtn = document.getElementById('ux35-menu-btn'); + var overlay = document.getElementById('ux35-sidebar-overlay'); + var sidebar = document.getElementById('ux35-sidebar'); + + if (menuBtn) { + menuBtn.addEventListener('click', function () { + var shell = document.getElementById('app'); + var open = !(shell && shell.classList.contains('ux35-drawer-open')); + setDrawerOpen(open); + }); + } + if (overlay) { + overlay.addEventListener('click', function () { setDrawerOpen(false); }); + } + if (sidebar) { + sidebar.querySelectorAll('a.ux35-sidebar-item').forEach(function (link) { + link.addEventListener('click', function () { + if (MQ_DRAWER.matches) setDrawerOpen(false); + }); + }); + } + + function onMqChange() { + if (!MQ_DRAWER.matches) { + setDrawerOpen(false); + restoreSidebarWidth(); + } + } + if (MQ_DRAWER.addEventListener) MQ_DRAWER.addEventListener('change', onMqChange); + else if (MQ_DRAWER.addListener) MQ_DRAWER.addListener(onMqChange); + } + + function initHelp() { + var helpBtn = document.getElementById('ux35-help-btn'); + if (helpBtn) { + helpBtn.addEventListener('click', function () { + if (typeof Tutorial !== 'undefined') Tutorial.start('console'); + }); + } + } + + function initThemeBtn() { + var btn = document.getElementById('ux35-theme-btn'); + if (btn) btn.addEventListener('click', cycleThemePreview); + syncThemeIcon(); + } + + function initScrollPreserve() { + var nav = document.getElementById('ux35-sidebar-nav'); + if (!nav) return; + var KEY = 'bd_ux35_sidebar_scroll'; + try { + var saved = sessionStorage.getItem(KEY); + if (saved) nav.scrollTop = parseInt(saved, 10) || 0; + } catch (e) { /* ignore */ } + var active = nav.querySelector('.ux35-sidebar-item.active'); + if (active) { + var rect = active.getBoundingClientRect(); + var navRect = nav.getBoundingClientRect(); + if (rect.top < navRect.top || rect.bottom > navRect.bottom) { + active.scrollIntoView({ block: 'center' }); + } + } + window.addEventListener('beforeunload', function () { + try { sessionStorage.setItem(KEY, String(nav.scrollTop)); } catch (e) { /* ignore */ } + }); + } + + function init() { + if (window.BetterDesk && window.BetterDesk.embed) return; + restoreSidebarWidth(); + initDrawer(); + initResize(); + initHelp(); + initThemeBtn(); + initScrollPreserve(); + } + + window.Ux35Shell = { + openDrawer: function () { setDrawerOpen(true); }, + closeDrawer: function () { setDrawerOpen(false); }, + syncThemeIcon: syncThemeIcon + }; + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); diff --git a/web-nodejs/scripts/patch-ux35-i18n.js b/web-nodejs/scripts/patch-ux35-i18n.js new file mode 100644 index 00000000..568e0cf0 --- /dev/null +++ b/web-nodejs/scripts/patch-ux35-i18n.js @@ -0,0 +1,51 @@ +const fs = require('fs'); +const path = require('path'); +const dir = path.join(__dirname, '..', 'lang'); +const translations = { + ar: { custom: 'مخصص', resize_sidebar: 'تغيير عرض الشريط الجانبي', open_menu: 'فتح القائمة', theme_mode_hint: 'الفاتح والداكن مدمجان. يُفعَّل المخصص عند تعديل الألوان أدناه.' }, + cs: { custom: 'Vlastní', resize_sidebar: 'Změnit šířku postranního panelu', open_menu: 'Otevřít nabídku', theme_mode_hint: 'Světlý a tmavý jsou vestavěné. Vlastní se aktivuje při úpravě barev níže.' }, + da: { custom: 'Tilpasset', resize_sidebar: 'Tilpas sidepanelbredde', open_menu: 'Åbn menu', theme_mode_hint: 'Lys og mørk er indbygget. Tilpasset aktiveres, når du redigerer farverne nedenfor.' }, + de: { custom: 'Benutzerdefiniert', resize_sidebar: 'Seitenleiste skalieren', open_menu: 'Menü öffnen', theme_mode_hint: 'Hell und Dunkel sind integriert. Benutzerdefiniert wird aktiviert, wenn Sie unten Farben ändern.' }, + es: { custom: 'Personalizado', resize_sidebar: 'Cambiar ancho de la barra lateral', open_menu: 'Abrir menú', theme_mode_hint: 'Claro y Oscuro son predefinidos. Personalizado se activa al editar los colores abajo.' }, + fi: { custom: 'Mukautettu', resize_sidebar: 'Muuta sivupalkin leveyttä', open_menu: 'Avaa valikko', theme_mode_hint: 'Vaalea ja Tumma ovat sisäänrakennettuja. Mukautettu otetaan käyttöön, kun muokkaat värejä alla.' }, + fr: { custom: 'Personnalisé', resize_sidebar: 'Redimensionner la barre latérale', open_menu: 'Ouvrir le menu', theme_mode_hint: 'Clair et Sombre sont intégrés. Personnalisé s’active lorsque vous modifiez les couleurs ci-dessous.' }, + hi: { custom: 'कस्टम', resize_sidebar: 'साइडबार की चौड़ाई बदलें', open_menu: 'मेनू खोलें', theme_mode_hint: 'लाइट और डार्क बिल्ट-इन हैं। नीचे रंग बदलने पर कस्टम सक्रिय होता है।' }, + hu: { custom: 'Egyéni', resize_sidebar: 'Oldalsáv átméretezése', open_menu: 'Menü megnyitása', theme_mode_hint: 'A Világos és a Sötét beépített. Az Egyéni a színek szerkesztésekor aktiválódik.' }, + id: { custom: 'Kustom', resize_sidebar: 'Ubah lebar bilah samping', open_menu: 'Buka menu', theme_mode_hint: 'Terang dan Gelap bawaan. Kustom aktif saat Anda mengubah warna di bawah.' }, + it: { custom: 'Personalizzato', resize_sidebar: 'Ridimensiona barra laterale', open_menu: 'Apri menu', theme_mode_hint: 'Chiaro e Scuro sono integrati. Personalizzato si attiva modificando i colori sotto.' }, + ja: { custom: 'カスタム', resize_sidebar: 'サイドバーの幅を変更', open_menu: 'メニューを開く', theme_mode_hint: 'ライトとダークは標準です。下で色を編集するとカスタムになります。' }, + ko: { custom: '사용자 지정', resize_sidebar: '사이드바 너비 조절', open_menu: '메뉴 열기', theme_mode_hint: '밝게와 어둡게는 기본입니다. 아래에서 색상을 편집하면 사용자 지정이 활성화됩니다.' }, + nb: { custom: 'Tilpasset', resize_sidebar: 'Endre sidepanelbredde', open_menu: 'Åpne meny', theme_mode_hint: 'Lys og Mørk er innebygd. Tilpasset aktiveres når du redigerer fargene nedenfor.' }, + nl: { custom: 'Aangepast', resize_sidebar: 'Zijbalkbreedte aanpassen', open_menu: 'Menu openen', theme_mode_hint: 'Licht en Donker zijn ingebouwd. Aangepast wordt actief wanneer u hieronder kleuren bewerkt.' }, + pl: { custom: 'Niestandardowy', resize_sidebar: 'Zmień szerokość paska bocznego', open_menu: 'Otwórz menu', theme_mode_hint: 'Jasny i Ciemny są wbudowane. Niestandardowy włącza się po edycji kolorów poniżej.' }, + pt: { custom: 'Personalizado', resize_sidebar: 'Redimensionar barra lateral', open_menu: 'Abrir menu', theme_mode_hint: 'Claro e Escuro são predefinidos. Personalizado ativa-se ao editar as cores abaixo.' }, + ro: { custom: 'Personalizat', resize_sidebar: 'Redimensionați bara laterală', open_menu: 'Deschide meniul', theme_mode_hint: 'Deschis și Întunecat sunt integrate. Personalizat se activează când editați culorile de mai jos.' }, + sv: { custom: 'Anpassad', resize_sidebar: 'Ändra sidopanelens bredd', open_menu: 'Öppna meny', theme_mode_hint: 'Ljust och Mörkt är inbyggda. Anpassad aktiveras när du redigerar färgerna nedan.' }, + th: { custom: 'กำหนดเอง', resize_sidebar: 'ปรับความกว้างแถบด้านข้าง', open_menu: 'เปิดเมนู', theme_mode_hint: 'สว่างและมืดมีให้ในตัว กำหนดเองจะเปิดเมื่อคุณแก้ไขสีด้านล่าง' }, + tr: { custom: 'Özel', resize_sidebar: 'Kenar çubuğu genişliğini ayarla', open_menu: 'Menüyü aç', theme_mode_hint: 'Açık ve Koyu yerleşiktir. Aşağıdaki renkleri düzenlediğinizde Özel etkinleşir.' }, + uk: { custom: 'Власний', resize_sidebar: 'Змінити ширину бічної панелі', open_menu: 'Відкрити меню', theme_mode_hint: 'Світла і Темна вбудовані. Власний активується під час редагування кольорів нижче.' }, + vi: { custom: 'Tùy chỉnh', resize_sidebar: 'Đổi độ rộng thanh bên', open_menu: 'Mở menu', theme_mode_hint: 'Sáng và Tối là mặc định. Tùy chỉnh bật khi bạn chỉnh màu bên dưới.' }, + zh: { custom: '自定义', resize_sidebar: '调整侧边栏宽度', open_menu: '打开菜单', theme_mode_hint: '浅色和深色为内置。编辑下方颜色时将启用自定义。' }, + 'zh-TW': { custom: '自訂', resize_sidebar: '調整側邊欄寬度', open_menu: '開啟選單', theme_mode_hint: '淺色與深色為內建。編輯下方顏色時會啟用自訂。' } +}; +const fallback = { + custom: 'Custom', + resize_sidebar: 'Resize sidebar', + open_menu: 'Open menu', + theme_mode_hint: 'Light and Dark are built-in. Custom activates when you edit colors below.' +}; +for (const file of fs.readdirSync(dir).filter(f => f.endsWith('.json') && f !== 'en.json')) { + const code = file.replace('.json', ''); + const p = path.join(dir, file); + const j = JSON.parse(fs.readFileSync(p, 'utf8')); + const t = translations[code] || fallback; + if (!j.theme) j.theme = {}; + j.theme.custom = t.custom; + j.ux35 = { + resize_sidebar: t.resize_sidebar, + open_menu: t.open_menu, + theme_mode_hint: t.theme_mode_hint + }; + fs.writeFileSync(p, JSON.stringify(j, null, 4) + '\n'); + console.log('updated', code); +} diff --git a/web-nodejs/services/brandingService.js b/web-nodejs/services/brandingService.js index 2c434d55..85d46420 100644 --- a/web-nodejs/services/brandingService.js +++ b/web-nodejs/services/brandingService.js @@ -370,7 +370,7 @@ const DEFAULT_BRANDING = { // Appearance model metadata (v2 keeps flat keys for backward compatibility) appearanceSchemaVersion: '2', - themeMode: 'dark', // 'dark' | 'light' | 'auto' (CSS coverage remains token-driven) + themeMode: 'dark', // 'dark' | 'light' | 'custom' (auto kept for backward compat → dark) // Color scheme overrides (empty = use defaults from variables.css) colors: { @@ -580,7 +580,9 @@ async function saveBranding(updates) { } else if (key === 'bgSize' || key === 'rdclientBgSize') { entries.push({ key, value: normalizeBackgroundSize(value) }); } else if (key === 'themeMode') { - const mode = ['dark', 'light', 'auto'].includes(String(value)) ? String(value) : 'dark'; + let mode = String(value); + if (mode === 'auto') mode = 'dark'; + if (!['dark', 'light', 'custom'].includes(mode)) mode = 'dark'; entries.push({ key, value: mode }); } else if (key === 'customCss') { // Security: Neutralize CSS-based XSS / external resource loading. @@ -733,7 +735,16 @@ function generateSemanticAliasCss(branding) { ' --color-text-muted: var(--text-secondary);', ' --color-border: var(--border-primary);', ' --focus-ring-color: var(--accent-blue-muted);', - ` color-scheme: ${(branding.themeMode || 'dark') === 'light' ? 'light' : 'dark'};` + ` color-scheme: ${(branding.themeMode || 'dark') === 'light' ? 'light' : 'dark'};`, + ' /* UX 3.5 chrome aliases (branding-aware) */', + ' --ux35-bg: var(--bg-primary);', + ' --ux35-card-bg: var(--card-bg, var(--surface-glass-bg-secondary, var(--bg-secondary)));', + ' --ux35-border: var(--surface-glass-border, var(--border-primary));', + ' --ux35-text: var(--text-primary);', + ' --ux35-muted: var(--text-secondary);', + ' --ux35-primary: var(--accent-blue);', + ' --ux35-active-bg: var(--accent-blue-muted);', + ' --ux35-glass-blur: var(--surface-glass-blur, 16px);' ]; return `:root {\n${lines.join('\n')}\n}\n`; } diff --git a/web-nodejs/views/layouts/main.ejs b/web-nodejs/views/layouts/main.ejs index c17b83b5..07fc1fbe 100644 --- a/web-nodejs/views/layouts/main.ejs +++ b/web-nodejs/views/layouts/main.ejs @@ -1,30 +1,25 @@ - + <%= typeof title !== 'undefined' ? title + ' - ' : '' %><%= appName %> - - + - - + + + <% if (!embed) { %> - - - - <% } else { %> - <% } %> <% if (typeof pageStyles !== 'undefined' && pageStyles.length) { %> <% pageStyles.forEach(style => { %> @@ -32,110 +27,33 @@ <% }); %> <% } %> - -
+ +
<% if (!embed) { %> - - <%- include('../partials/sidebar') %> - - - + <%- include('../partials/ux35-topbar') %> <% } %> - - -
+ +
<% if (!embed) { %> - - <%- include('../partials/navbar') %> + <%- include('../partials/ux35-sidebar') %> <% } %> - -
- <%- body %> -
+ +
+
+ <%- body %> +
+
<% if (!embed) { %> <%- include('../partials/mobile-bottom-nav') %> <% } %>
- - <% if (!embed) { %> - -
- -
-
- - <%= appName %> -
- -
-
-
- - - - - - - -
-
- - -
-
-
-
-
-
-
- - -
- <% } %> - - - -
- - + @@ -159,12 +78,9 @@ <% if (!embed) { %> + - - - - <% } %> <% if (typeof pageScripts !== 'undefined' && pageScripts.length) { %> diff --git a/web-nodejs/views/partials/ux35-sidebar.ejs b/web-nodejs/views/partials/ux35-sidebar.ejs new file mode 100644 index 00000000..70d06cab --- /dev/null +++ b/web-nodejs/views/partials/ux35-sidebar.ejs @@ -0,0 +1,291 @@ +<% const ux35ServerTab = typeof currentTab !== 'undefined' && currentTab ? currentTab : 'overview'; %> +<% const ux35CommercialTab = typeof currentTab !== 'undefined' && currentTab ? currentTab : 'overview'; %> +
+ diff --git a/web-nodejs/views/partials/ux35-topbar.ejs b/web-nodejs/views/partials/ux35-topbar.ejs new file mode 100644 index 00000000..4af85c8e --- /dev/null +++ b/web-nodejs/views/partials/ux35-topbar.ejs @@ -0,0 +1,111 @@ +<% if (typeof emergencyMode !== 'undefined' && emergencyMode) { %> + +<% } %> +
+ + + + + + +
+ +
+ + +
+ + +
+ + + + + +
+ +
+ <% (availableLanguageList || Object.values(availableLanguages || {})).forEach(function(language) { %> +
+ <%= language.flag || '🌐' %> + <%= language.native || language.name || language.code %> + <% if (lang === language.code) { %> + check + <% } %> +
+ <% }); %> +
+
+ +
+ +
+
+ lock + <%= _('auth.change_password') %> +
+
+ logout + <%= _('auth.logout') %> +
+
+
+
+
diff --git a/web-nodejs/views/settings.ejs b/web-nodejs/views/settings.ejs index e7b5b382..501fecbf 100644 --- a/web-nodejs/views/settings.ejs +++ b/web-nodejs/views/settings.ejs @@ -676,6 +676,33 @@

${_('branding.colors_desc')}

+
+ +

${_('ux35.theme_mode_hint') || 'Light and Dark are built-in. Custom activates when you edit colors below.'}

+
+ + + +
+

${_('branding.colors_backgrounds')}