mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-09 17:16:46 +00:00
feat(ux): introduce UX 3.5 with new console shell and theme options
- Added UX 3.5 as the default console shell, featuring a full-list sidebar, topbar, and customizable themes (Dark/Light/Custom). - Updated documentation to reflect the new UX 3.5 shell. - Removed legacy Desktop Mode and Beta 3.1 styles, ensuring a streamlined user experience. - Enhanced language support for new theme options across multiple languages. - Fixed OIDC client login issues by proxying callback URLs to the Go API. This update improves the overall user interface and experience, aligning with modern design standards.
This commit is contained in:
+4
-1
@@ -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://<panel>/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`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
unknown
|
||||
+1
-1
@@ -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]] |
|
||||
|
||||
@@ -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 `<a href>` 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 `<html>`, 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
|
||||
@@ -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]].
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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": "الفاتح والداكن مدمجان. يُفعَّل المخصص عند تعديل الألوان أدناه."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "लाइट और डार्क बिल्ट-इन हैं। नीचे रंग बदलने पर कस्टम सक्रिय होता है।"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "ライトとダークは標準です。下で色を編集するとカスタムになります。"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "밝게와 어둡게는 기본입니다. 아래에서 색상을 편집하면 사용자 지정이 활성화됩니다."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "สว่างและมืดมีให้ในตัว กำหนดเองจะเปิดเมื่อคุณแก้ไขสีด้านล่าง"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "Світла і Темна вбудовані. Власний активується під час редагування кольорів нижче."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "淺色與深色為內建。編輯下方顏色時會啟用自訂。"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "浅色和深色为内置。编辑下方颜色时将启用自定义。"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
@@ -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 = `
|
||||
<div class="b31-topbar-logo">
|
||||
<span class="material-icons">dns</span>
|
||||
<span>${escHtml(appName)}</span>
|
||||
<span class="b31-topbar-badge">BETA 3.1</span>
|
||||
</div>
|
||||
<div class="b31-topbar-spacer"></div>
|
||||
<div class="b31-topbar-actions">
|
||||
<button class="b31-topbar-btn" id="b31-refresh-btn" title="Refresh">
|
||||
<span class="material-icons">refresh</span>
|
||||
</button>
|
||||
<button class="b31-topbar-btn" id="b31-theme-btn" title="Toggle theme">
|
||||
<span class="material-icons">${isDark ? 'light_mode' : 'dark_mode'}</span>
|
||||
</button>
|
||||
<button class="b31-topbar-btn" id="b31-exit-btn" title="Exit Beta 3.1">
|
||||
<span class="material-icons">close</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 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 = `<div class="b31-sidebar-heading">${escHtml(sec.heading)}</div>`;
|
||||
sec.items.forEach(item => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'b31-sidebar-item';
|
||||
btn.dataset.panel = item.id;
|
||||
btn.innerHTML = `<span class="material-icons">${item.icon}</span><span>${escHtml(item.label)}</span>`;
|
||||
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 = '<div class="b31-loading"><span class="material-icons">sync</span> Loading…</div>';
|
||||
|
||||
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();
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
})();
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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`;
|
||||
}
|
||||
|
||||
@@ -1,30 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="<%= lang || 'en' %>">
|
||||
<html lang="<%= lang || 'en' %>" data-theme="<%= (branding && branding.themeMode === 'light') ? 'light' : 'dark' %>" data-theme-mode="<%= (branding && branding.themeMode) || 'dark' %>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<meta name="description" content="<%= appName %> - <%= appDescription %>">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<title><%= typeof title !== 'undefined' ? title + ' - ' : '' %><%= appName %></title>
|
||||
|
||||
<!-- Favicon -->
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="/branding/favicon.svg">
|
||||
|
||||
<!-- Stylesheets -->
|
||||
|
||||
<link rel="stylesheet" href="/css/main.css?v=<%= cacheVersion %>">
|
||||
<link rel="stylesheet" href="/css/theme.css?v=<%= cacheVersion %>">
|
||||
<link rel="stylesheet" href="/css/branding.css?v=<%= typeof brandingRevision !== 'undefined' ? brandingRevision : cacheVersion %>">
|
||||
<link rel="stylesheet" href="/css/ux35-tokens.css?v=<%= cacheVersion %>">
|
||||
<link rel="stylesheet" href="/css/ux35.css?v=<%= cacheVersion %>">
|
||||
<link rel="stylesheet" href="/css/transitions.css?v=<%= cacheVersion %>">
|
||||
<link rel="stylesheet" href="/css/notif-center.css?v=<%= cacheVersion %>">
|
||||
<link rel="stylesheet" href="/css/accessibility.css?v=<%= cacheVersion %>">
|
||||
<% if (!embed) { %>
|
||||
<link rel="stylesheet" href="/css/mobile-shell.css?v=<%= cacheVersion %>">
|
||||
<link rel="stylesheet" href="/css/desktop-mode.css?v=<%= cacheVersion %>">
|
||||
<link rel="stylesheet" href="/css/desktop-widgets.css?v=<%= cacheVersion %>">
|
||||
<link rel="stylesheet" href="/css/tutorial.css?v=<%= cacheVersion %>">
|
||||
<link rel="stylesheet" href="/css/beta31.css?v=<%= cacheVersion %>">
|
||||
<% } else { %>
|
||||
<link rel="stylesheet" href="/css/desktop-widget-overrides.css?v=<%= cacheVersion %>">
|
||||
<% } %>
|
||||
<% if (typeof pageStyles !== 'undefined' && pageStyles.length) { %>
|
||||
<% pageStyles.forEach(style => { %>
|
||||
@@ -32,110 +27,33 @@
|
||||
<% }); %>
|
||||
<% } %>
|
||||
</head>
|
||||
<body class="app-page<%= embed ? ' embed-mode' : '' %>">
|
||||
<div class="app-layout" id="app">
|
||||
<body class="app-page ux35-page<%= embed ? ' embed-mode' : '' %>">
|
||||
<div class="ux35-shell<%= embed ? ' embed-mode' : '' %>" id="app">
|
||||
<% if (!embed) { %>
|
||||
<!-- Sidebar -->
|
||||
<%- include('../partials/sidebar') %>
|
||||
|
||||
<!-- Sidebar overlay for mobile -->
|
||||
<div class="sidebar-overlay" id="sidebar-overlay"></div>
|
||||
<%- include('../partials/ux35-topbar') %>
|
||||
<% } %>
|
||||
|
||||
<!-- Main wrapper -->
|
||||
<div class="main-wrapper">
|
||||
|
||||
<div class="ux35-body">
|
||||
<% if (!embed) { %>
|
||||
<!-- Navbar -->
|
||||
<%- include('../partials/navbar') %>
|
||||
<%- include('../partials/ux35-sidebar') %>
|
||||
<% } %>
|
||||
|
||||
<main class="main-content">
|
||||
<%- body %>
|
||||
</main>
|
||||
|
||||
<div class="ux35-main">
|
||||
<main class="ux35-content main-content" id="ux35-content">
|
||||
<%- body %>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if (!embed) { %>
|
||||
<%- include('../partials/mobile-bottom-nav') %>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<% if (!embed) { %>
|
||||
<!-- Desktop Mode Shell (hidden by default, activated via toggle) -->
|
||||
<div id="desktop-shell" class="desktop-shell">
|
||||
<!-- Top bar with logo + app drawer + clock -->
|
||||
<div class="desktop-topbar" id="desktop-topbar">
|
||||
<div class="topbar-left">
|
||||
<button class="topbar-btn topbar-app-drawer-btn" id="topbar-app-drawer-btn" title="<%= _('desktop.app_drawer') || 'Applications' %>">
|
||||
<span class="material-icons">apps</span>
|
||||
</button>
|
||||
<span class="topbar-brand"><%= appName %></span>
|
||||
<div class="topbar-shortcuts" id="topbar-shortcuts"></div>
|
||||
<button class="topbar-btn topbar-edit-btn" id="topbar-edit-btn" title="<%= _('desktop.edit_shortcuts') || 'Edit shortcuts' %>">
|
||||
<span class="material-icons">edit</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="topbar-center" id="topbar-clock"></div>
|
||||
<div class="topbar-right">
|
||||
<button class="topbar-btn" id="topbar-add-widget-btn" title="<%= _('desktop.add_widget') || 'Add Widget' %>">
|
||||
<span class="material-icons">add_circle_outline</span>
|
||||
</button>
|
||||
<button class="topbar-btn" id="topbar-snap-layout-btn" title="<%= _('desktop.label_snap_layout') || 'Window Layout' %>">
|
||||
<span class="material-icons">grid_view</span>
|
||||
</button>
|
||||
<button class="topbar-btn" id="topbar-wallpaper-btn" title="<%= _('desktop.wallpaper') || 'Wallpaper' %>">
|
||||
<span class="material-icons">wallpaper</span>
|
||||
</button>
|
||||
<button class="topbar-btn" id="topbar-fullscreen-btn" title="<%= _('desktop.fullscreen') || 'Fullscreen' %>">
|
||||
<span class="material-icons">fullscreen</span>
|
||||
</button>
|
||||
<button class="topbar-btn" id="topbar-widgets-btn" title="<%= _('desktop.widgets') || 'Widgets' %>">
|
||||
<span class="material-icons">widgets</span>
|
||||
</button>
|
||||
|
||||
<button class="topbar-btn" id="topbar-help-btn" title="<%= _('desktop.help') || 'Help' %>">
|
||||
<span class="material-icons">help_outline</span>
|
||||
</button>
|
||||
<button class="topbar-btn" id="topbar-exit-btn" title="<%= _('desktop.exit_desktop') %>">
|
||||
<span class="material-icons">logout</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- App drawer overlay -->
|
||||
<div class="app-drawer-overlay" id="app-drawer-overlay" style="display:none">
|
||||
<div class="app-drawer" id="app-drawer">
|
||||
<div class="app-drawer-header">
|
||||
<input type="text" class="app-drawer-search" id="app-drawer-search" placeholder="<%= _('desktop.search_apps') || 'Search apps...' %>">
|
||||
</div>
|
||||
<div class="app-drawer-grid" id="app-drawer-grid"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="desktop-wallpaper"></div>
|
||||
<div class="widget-canvas" id="widget-canvas"></div>
|
||||
<div class="desktop-icons" id="desktop-icons"></div>
|
||||
<div class="desktop-windows" id="desktop-windows"></div>
|
||||
<div class="desktop-taskbar" id="desktop-taskbar">
|
||||
<div class="taskbar-apps" id="taskbar-apps"></div>
|
||||
</div>
|
||||
<!-- Desktop right-click context menu -->
|
||||
<div class="desktop-context-menu" id="desktop-context-menu" style="display:none">
|
||||
<button class="ctx-item" data-action="wallpaper"><span class="material-icons">wallpaper</span><%= _('desktop.wallpaper') %></button>
|
||||
<button class="ctx-item" data-action="layout"><span class="material-icons">grid_view</span><%= _('desktop.label_snap_layout') || 'Window Layout' %></button>
|
||||
<button class="ctx-item" data-action="refresh"><span class="material-icons">refresh</span><%= _('desktop.refresh') || 'Refresh' %></button>
|
||||
<div class="ctx-divider"></div>
|
||||
<button class="ctx-item" data-action="exit"><span class="material-icons">view_sidebar</span><%= _('desktop.exit_desktop') %></button>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<!-- Modal container -->
|
||||
<div id="modal-container"></div>
|
||||
|
||||
<!-- Toast container -->
|
||||
<div id="toast-container" class="toast-container"></div>
|
||||
|
||||
<!-- Scripts -->
|
||||
|
||||
<script nonce="<%= cspNonce %>">
|
||||
// Global config
|
||||
window.BetterDesk = {
|
||||
lang: '<%= lang || "en" %>',
|
||||
availableLanguages: <%- JSON.stringify(availableLanguageList || []) %>,
|
||||
@@ -144,7 +62,8 @@
|
||||
csrfToken: '<%= typeof csrfToken !== 'undefined' ? csrfToken : '' %>',
|
||||
user: <%- JSON.stringify(user || null) %>,
|
||||
branding: <%- JSON.stringify(branding || {}) %>,
|
||||
embed: <%= embed ? 'true' : 'false' %>
|
||||
embed: <%= embed ? 'true' : 'false' %>,
|
||||
uiShell: 'ux35'
|
||||
};
|
||||
</script>
|
||||
<script src="/js/utils.js?v=<%= cacheVersion %>"></script>
|
||||
@@ -159,12 +78,9 @@
|
||||
<script src="/js/accessibility.js?v=<%= cacheVersion %>"></script>
|
||||
<script src="/js/app.js?v=<%= cacheVersion %>"></script>
|
||||
<% if (!embed) { %>
|
||||
<script src="/js/ux35-shell.js?v=<%= cacheVersion %>"></script>
|
||||
<script src="/js/mobile-nav.js?v=<%= cacheVersion %>"></script>
|
||||
<script src="/js/widget-plugins.js?v=<%= cacheVersion %>"></script>
|
||||
<script src="/js/desktop-widgets.js?v=<%= cacheVersion %>"></script>
|
||||
<script src="/js/desktop-mode.js?v=<%= cacheVersion %>"></script>
|
||||
<script src="/js/tutorial.js?v=<%= cacheVersion %>"></script>
|
||||
<script src="/js/beta31.js?v=<%= cacheVersion %>"></script>
|
||||
<script nonce="<%= cspNonce %>">if (typeof Tutorial !== 'undefined') Tutorial.autoStart('console');</script>
|
||||
<% } %>
|
||||
<% if (typeof pageScripts !== 'undefined' && pageScripts.length) { %>
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
<% const ux35ServerTab = typeof currentTab !== 'undefined' && currentTab ? currentTab : 'overview'; %>
|
||||
<% const ux35CommercialTab = typeof currentTab !== 'undefined' && currentTab ? currentTab : 'overview'; %>
|
||||
<aside class="ux35-sidebar" id="ux35-sidebar" aria-label="<%= _('nav.main') %>">
|
||||
<div class="ux35-sidebar-resize" id="ux35-sidebar-resize" title="<%= _('ux35.resize_sidebar') || 'Resize sidebar' %>"></div>
|
||||
<nav class="ux35-sidebar-nav" id="ux35-sidebar-nav">
|
||||
<!-- Main -->
|
||||
<div class="ux35-sidebar-section" data-panel="main">
|
||||
<div class="ux35-sidebar-heading"><%= _('nav.main') %></div>
|
||||
<a href="/" class="ux35-sidebar-item <%= currentPage === 'dashboard' ? 'active' : '' %>">
|
||||
<span class="material-icons">dashboard</span>
|
||||
<span><%= _('nav.dashboard') %></span>
|
||||
</a>
|
||||
<% if (hasPermission('device.view')) { %>
|
||||
<a href="/devices" class="ux35-sidebar-item <%= currentPage === 'devices' ? 'active' : '' %>">
|
||||
<span class="material-icons">devices</span>
|
||||
<span><%= _('nav.devices') %></span>
|
||||
</a>
|
||||
<% } %>
|
||||
<% if (hasPermission('enrollment.approve')) { %>
|
||||
<a href="/registrations" class="ux35-sidebar-item <%= currentPage === 'registrations' ? 'active' : '' %>">
|
||||
<span class="material-icons">how_to_reg</span>
|
||||
<span><%= _('nav.registrations') %></span>
|
||||
<span class="ux35-sidebar-item-badge" id="reg-sidebar-badge" style="display: none;">0</span>
|
||||
</a>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<!-- Management -->
|
||||
<div class="ux35-sidebar-section" data-panel="management">
|
||||
<div class="ux35-sidebar-heading"><%= _('nav.management') || 'Management' %></div>
|
||||
<% if (hasPermission('device.view')) { %>
|
||||
<a href="/inventory" class="ux35-sidebar-item <%= currentPage === 'inventory' ? 'active' : '' %>">
|
||||
<span class="material-icons">inventory_2</span>
|
||||
<span><%= _('nav.inventory') %></span>
|
||||
</a>
|
||||
<a href="/tickets" class="ux35-sidebar-item <%= currentPage === 'tickets' ? 'active' : '' %>">
|
||||
<span class="material-icons">confirmation_number</span>
|
||||
<span><%= _('nav.tickets') %></span>
|
||||
</a>
|
||||
<a href="/help-requests" class="ux35-sidebar-item <%= currentPage === 'help-requests' ? 'active' : '' %>">
|
||||
<span class="material-icons">support_agent</span>
|
||||
<span><%= _('nav.help_requests') %></span>
|
||||
</a>
|
||||
<% } %>
|
||||
<% if (hasPermission('server.config')) { %>
|
||||
<a href="/automation" class="ux35-sidebar-item <%= currentPage === 'automation' ? 'active' : '' %>">
|
||||
<span class="material-icons">smart_toy</span>
|
||||
<span><%= _('nav.automation') %></span>
|
||||
</a>
|
||||
<% } %>
|
||||
<% if (hasPermission('device.view')) { %>
|
||||
<a href="/network" class="ux35-sidebar-item <%= currentPage === 'network' ? 'active' : '' %>">
|
||||
<span class="material-icons">lan</span>
|
||||
<span><%= _('nav.network') %></span>
|
||||
</a>
|
||||
<% } %>
|
||||
<% if (hasPermission('audit.view')) { %>
|
||||
<a href="/activity" class="ux35-sidebar-item <%= currentPage === 'activity' ? 'active' : '' %>">
|
||||
<span class="material-icons">timeline</span>
|
||||
<span><%= _('nav.activity') %></span>
|
||||
</a>
|
||||
<% } %>
|
||||
<% if (hasPermission('cdap.view')) { %>
|
||||
<a href="/cdap" class="ux35-sidebar-item <%= currentPage === 'cdap' ? 'active' : '' %>">
|
||||
<span class="material-icons">developer_board</span>
|
||||
<span><%= _('nav.cdap') %></span>
|
||||
</a>
|
||||
<% } %>
|
||||
<% if (hasPermission('org.manage_devices')) { %>
|
||||
<a href="/fleet" class="ux35-sidebar-item <%= currentPage === 'fleet' ? 'active' : '' %>">
|
||||
<span class="material-icons">hub</span>
|
||||
<span><%= _('nav.fleet') || 'Fleet' %></span>
|
||||
</a>
|
||||
<% } %>
|
||||
<% if (hasPermission('server.config')) { %>
|
||||
<a href="/scaling" class="ux35-sidebar-item <%= currentPage === 'scaling' ? 'active' : '' %>">
|
||||
<span class="material-icons">cell_tower</span>
|
||||
<span><%= _('nav.scaling') || 'Scaling' %></span>
|
||||
</a>
|
||||
<% } %>
|
||||
<% if (hasPermission('device.view')) { %>
|
||||
<a href="/cross-platform" class="ux35-sidebar-item <%= currentPage === 'cross-platform' ? 'active' : '' %>">
|
||||
<span class="material-icons">devices</span>
|
||||
<span><%= _('nav.cross_platform') || 'Cross-Platform' %></span>
|
||||
</a>
|
||||
<% } %>
|
||||
<a href="/chat" class="ux35-sidebar-item <%= currentPage === 'chat' ? 'active' : '' %>">
|
||||
<span class="material-icons">chat</span>
|
||||
<span><%= _('nav.chat') %></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Tools -->
|
||||
<div class="ux35-sidebar-section" data-panel="tools">
|
||||
<div class="ux35-sidebar-heading"><%= _('nav.tools') %></div>
|
||||
<% if (hasPermission('audit.view')) { %>
|
||||
<a href="/reports" class="ux35-sidebar-item <%= currentPage === 'reports' ? 'active' : '' %>">
|
||||
<span class="material-icons">assessment</span>
|
||||
<span><%= _('nav.reports') %></span>
|
||||
</a>
|
||||
<% } %>
|
||||
<% if (hasPermission('server.config')) { %>
|
||||
<a href="/keys" class="ux35-sidebar-item <%= currentPage === 'keys' ? 'active' : '' %>">
|
||||
<span class="material-icons">vpn_key</span>
|
||||
<span><%= _('nav.keys') %></span>
|
||||
</a>
|
||||
<a href="/generator" class="ux35-sidebar-item <%= currentPage === 'generator' ? 'active' : '' %>" title="<%= _('generator.alpha_tooltip') || _('nav.generator') %>">
|
||||
<span class="material-icons">build</span>
|
||||
<span><%= _('nav.generator') %></span>
|
||||
<span class="badge-sidebar badge-sidebar--alpha"><%= _('generator.alpha_badge') || 'ALPHA' %></span>
|
||||
</a>
|
||||
<% } %>
|
||||
<% if (hasPermission('device.connect')) { %>
|
||||
<a href="/remote" class="ux35-sidebar-item <%= currentPage === 'remote' ? 'active' : '' %>" target="_blank" rel="noopener noreferrer" title="<%= _('nav.remote_open_tab') || _('nav.remote') %>">
|
||||
<span class="material-icons">connected_tv</span>
|
||||
<span><%= _('nav.remote') || 'Remote Desktop' %></span>
|
||||
<span class="material-icons sidebar-link-external" aria-hidden="true">open_in_new</span>
|
||||
</a>
|
||||
<a href="/toolkit" class="ux35-sidebar-item <%= currentPage === 'toolkit' ? 'active' : '' %>">
|
||||
<span class="material-icons">handyman</span>
|
||||
<span><%= _('nav.toolkit') || 'Toolkit' %></span>
|
||||
</a>
|
||||
<% } %>
|
||||
<% if (hasPermission('cdap.view')) { %>
|
||||
<a href="/cdap-studio" class="ux35-sidebar-item <%= currentPage === 'cdap-studio' ? 'active' : '' %>">
|
||||
<span class="material-icons">account_tree</span>
|
||||
<span><%= _('nav.sdk_studio') || 'SDK Studio' %></span>
|
||||
</a>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<!-- System -->
|
||||
<% if (isSuperAdmin || hasPermission('server.config') || hasPermission('user.view') || hasPermission('enrollment.manage') || hasPermission('enrollment.approve') || hasPermission('audit.view') || hasPermission('org.create') || hasPermission('org.edit') || hasPermission('org.manage_users')) { %>
|
||||
<div class="ux35-sidebar-section" data-panel="system">
|
||||
<div class="ux35-sidebar-heading"><%= _('nav.system') %></div>
|
||||
<% if (hasPermission('enrollment.manage')) { %>
|
||||
<a href="/tokens" class="ux35-sidebar-item <%= currentPage === 'tokens' ? 'active' : '' %>">
|
||||
<span class="material-icons">token</span>
|
||||
<span><%= _('nav.tokens') %></span>
|
||||
</a>
|
||||
<% } %>
|
||||
<% if (hasPermission('org.create') || hasPermission('org.edit') || hasPermission('org.manage_users')) { %>
|
||||
<a href="/organizations" class="ux35-sidebar-item <%= currentPage === 'organizations' ? 'active' : '' %>">
|
||||
<span class="material-icons">corporate_fare</span>
|
||||
<span><%= _('nav.organizations') %></span>
|
||||
</a>
|
||||
<% } %>
|
||||
<% if (hasPermission('server.config')) { %>
|
||||
<a href="/policies" class="ux35-sidebar-item <%= currentPage === 'policies' ? 'active' : '' %>">
|
||||
<span class="material-icons">policy</span>
|
||||
<span><%= _('nav.policies') || 'Policies' %></span>
|
||||
</a>
|
||||
<a href="/attestation" class="ux35-sidebar-item <%= currentPage === 'attestation' ? 'active' : '' %>">
|
||||
<span class="material-icons">fingerprint</span>
|
||||
<span><%= _('nav.attestation') || 'Attestation' %></span>
|
||||
</a>
|
||||
<a href="/dataguard" class="ux35-sidebar-item <%= currentPage === 'dataguard' ? 'active' : '' %>">
|
||||
<span class="material-icons">shield</span>
|
||||
<span><%= _('nav.dataguard') %></span>
|
||||
</a>
|
||||
<% } %>
|
||||
<% if (hasPermission('user.view')) { %>
|
||||
<a href="/users" class="ux35-sidebar-item <%= currentPage === 'users' ? 'active' : '' %>">
|
||||
<span class="material-icons">group</span>
|
||||
<span><%= _('nav.users') %></span>
|
||||
</a>
|
||||
<% } %>
|
||||
<% if (hasPermission('audit.view')) { %>
|
||||
<a href="/security-audit" class="ux35-sidebar-item <%= currentPage === 'security-audit' ? 'active' : '' %>">
|
||||
<span class="material-icons">security</span>
|
||||
<span><%= _('nav.security_audit') %></span>
|
||||
</a>
|
||||
<% } %>
|
||||
<% if (isSuperAdmin) { %>
|
||||
<a href="/permissions" class="ux35-sidebar-item <%= currentPage === 'permissions' ? 'active' : '' %>">
|
||||
<span class="material-icons">admin_panel_settings</span>
|
||||
<span><%= _('nav.permissions') %></span>
|
||||
</a>
|
||||
<% } %>
|
||||
<% if (hasPermission('server.config')) { %>
|
||||
<a href="/languages" class="ux35-sidebar-item <%= currentPage === 'languages' ? 'active' : '' %>">
|
||||
<span class="material-icons">translate</span>
|
||||
<span><%= _('nav.languages') %></span>
|
||||
</a>
|
||||
<a href="/resource-control" class="ux35-sidebar-item <%= currentPage === 'resource-control' ? 'active' : '' %>">
|
||||
<span class="material-icons">tune</span>
|
||||
<span><%= _('nav.resource_control') %></span>
|
||||
</a>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<!-- Server Management -->
|
||||
<% if (hasPermission('server.config')) { %>
|
||||
<div class="ux35-sidebar-section" data-panel="server-mgmt">
|
||||
<div class="ux35-sidebar-heading"><%= _('nav.server_management') || 'Server Management' %></div>
|
||||
<a href="/server-management?tab=overview" class="ux35-sidebar-item <%= currentPage === 'server-management' && ux35ServerTab === 'overview' ? 'active' : '' %>">
|
||||
<span class="material-icons">monitoring</span>
|
||||
<span><%= _('server_mgmt.tab_overview') || 'Overview' %></span>
|
||||
</a>
|
||||
<a href="/server-management?tab=terminal" class="ux35-sidebar-item <%= currentPage === 'server-management' && ux35ServerTab === 'terminal' ? 'active' : '' %>">
|
||||
<span class="material-icons">terminal</span>
|
||||
<span><%= _('server_mgmt.tab_terminal') || 'Terminal' %></span>
|
||||
</a>
|
||||
<a href="/server-management?tab=files" class="ux35-sidebar-item <%= currentPage === 'server-management' && ux35ServerTab === 'files' ? 'active' : '' %>">
|
||||
<span class="material-icons">folder_open</span>
|
||||
<span><%= _('server_mgmt.tab_files') || 'Files' %></span>
|
||||
</a>
|
||||
<a href="/server-management?tab=services" class="ux35-sidebar-item <%= currentPage === 'server-management' && ux35ServerTab === 'services' ? 'active' : '' %>">
|
||||
<span class="material-icons">miscellaneous_services</span>
|
||||
<span><%= _('server_mgmt.tab_services') || 'Services' %></span>
|
||||
<span class="badge-sidebar" style="background:linear-gradient(135deg,#ff7b72,#d29922);color:#fff;">BETA</span>
|
||||
</a>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<!-- Commercialization -->
|
||||
<% if (hasPermission('billing.view')) { %>
|
||||
<div class="ux35-sidebar-section" data-panel="commercialization">
|
||||
<div class="ux35-sidebar-heading"><%= _('nav.commercialization') || 'Commercialization' %></div>
|
||||
<a href="/commercialization?tab=overview" class="ux35-sidebar-item <%= currentPage === 'commercialization' && ux35CommercialTab === 'overview' ? 'active' : '' %>">
|
||||
<span class="material-icons">dashboard</span>
|
||||
<span><%= _('commercialization.tabs.overview') %></span>
|
||||
</a>
|
||||
<a href="/commercialization?tab=packages" class="ux35-sidebar-item <%= currentPage === 'commercialization' && ux35CommercialTab === 'packages' ? 'active' : '' %>">
|
||||
<span class="material-icons">inventory_2</span>
|
||||
<span><%= _('commercialization.tabs.packages') %></span>
|
||||
</a>
|
||||
<a href="/commercialization?tab=sessions" class="ux35-sidebar-item <%= currentPage === 'commercialization' && ux35CommercialTab === 'sessions' ? 'active' : '' %>">
|
||||
<span class="material-icons">schedule</span>
|
||||
<span><%= _('commercialization.tabs.sessions') %></span>
|
||||
</a>
|
||||
<a href="/commercialization?tab=reports" class="ux35-sidebar-item <%= currentPage === 'commercialization' && ux35CommercialTab === 'reports' ? 'active' : '' %>">
|
||||
<span class="material-icons">description</span>
|
||||
<span><%= _('commercialization.tabs.reports') %></span>
|
||||
</a>
|
||||
<a href="/commercialization?tab=settings" class="ux35-sidebar-item <%= currentPage === 'commercialization' && ux35CommercialTab === 'settings' ? 'active' : '' %>">
|
||||
<span class="material-icons">tune</span>
|
||||
<span><%= _('commercialization.tabs.settings') %></span>
|
||||
</a>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<!-- Settings & attestation -->
|
||||
<div class="ux35-sidebar-section" data-panel="settings">
|
||||
<div class="ux35-sidebar-heading"><%= _('nav.settings') %></div>
|
||||
<% if (hasPermission('server.attestation')) { %>
|
||||
<a href="/server-attestation" class="ux35-sidebar-item <%= currentPage === 'server-attestation' ? 'active' : '' %>">
|
||||
<span class="material-icons">verified</span>
|
||||
<span><%= _('nav.server_attestation') || 'Server Attestation' %></span>
|
||||
</a>
|
||||
<% } %>
|
||||
<a href="/settings" class="ux35-sidebar-item <%= currentPage === 'settings' ? 'active' : '' %>">
|
||||
<span class="material-icons">settings</span>
|
||||
<span><%= _('nav.settings') %></span>
|
||||
</a>
|
||||
<button type="button" class="ux35-sidebar-item" id="ux35-help-btn" title="<%= _('nav.help') || 'Help' %>">
|
||||
<span class="material-icons">help_outline</span>
|
||||
<span><%= _('nav.help') || 'Help' %></span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="ux35-sidebar-footer">
|
||||
<div class="ux35-sidebar-user" id="user-menu-trigger">
|
||||
<div class="ux35-sidebar-user-avatar">
|
||||
<%= user && user.username ? user.username.charAt(0).toUpperCase() : 'A' %>
|
||||
</div>
|
||||
<div class="ux35-sidebar-user-info">
|
||||
<div class="ux35-sidebar-user-name"><%= user ? user.username : 'Admin' %></div>
|
||||
<%
|
||||
var _roleKey = user && user.role ? ('users.role_' + user.role) : 'users.role_admin';
|
||||
var _roleLabel = _(_roleKey);
|
||||
if (!_roleLabel || _roleLabel === _roleKey) {
|
||||
_roleLabel = user && user.role
|
||||
? user.role.replace(/_/g, ' ').replace(/\b\w/g, function(c){ return c.toUpperCase(); })
|
||||
: 'Administrator';
|
||||
}
|
||||
%>
|
||||
<div class="ux35-sidebar-user-role"><%= _roleLabel %></div>
|
||||
</div>
|
||||
</div>
|
||||
<% if ((branding && branding.footerText) || (branding && branding.showPoweredBy !== 'false')) { %>
|
||||
<div class="ux35-sidebar-attribution">
|
||||
<% if (branding.footerText) { %><span><%= branding.footerText %></span><% } %>
|
||||
<% if (branding.showPoweredBy !== 'false') { %><span><%= _('branding.powered_by') %> BetterDesk</span><% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="ux35-sidebar-overlay" id="ux35-sidebar-overlay" hidden></div>
|
||||
@@ -0,0 +1,111 @@
|
||||
<% if (typeof emergencyMode !== 'undefined' && emergencyMode) { %>
|
||||
<div class="emergency-banner" role="alert">
|
||||
<span class="material-icons">warning</span>
|
||||
<span><%= _('auth.emergency_mode_banner') %></span>
|
||||
</div>
|
||||
<% } %>
|
||||
<header class="ux35-topbar" id="ux35-topbar">
|
||||
<button type="button" class="ux35-topbar-menu-btn" id="ux35-menu-btn" aria-label="<%= _('ux35.open_menu') || 'Open menu' %>" aria-controls="ux35-sidebar" aria-expanded="false">
|
||||
<span class="material-icons">menu</span>
|
||||
</button>
|
||||
|
||||
<a href="/" class="ux35-topbar-logo" title="<%= appName %>">
|
||||
<% if (branding.logoType === 'text' && branding.logoText) { %>
|
||||
<span class="brand-text-logo"><%= branding.logoText %><% if (branding.logoTextAccent) { %><span class="brand-text-accent"><%= branding.logoTextAccent %></span><% } %></span>
|
||||
<% } else if (branding.logoType === 'svg' && branding.logoSvg) { %>
|
||||
<span class="sidebar-logo-svg"><%- branding.logoSvg %></span>
|
||||
<% } else if (branding.logoType === 'image' && branding.logoUrl) { %>
|
||||
<img src="<%= branding.logoUrl %>" alt="<%= appName %>">
|
||||
<% } else { %>
|
||||
<span class="material-icons"><%= branding.logoIcon || 'dns' %></span>
|
||||
<span><%= appName %></span>
|
||||
<% } %>
|
||||
</a>
|
||||
|
||||
<nav class="ux35-topbar-breadcrumb" aria-label="Breadcrumb">
|
||||
<a href="/"><%= _('nav.dashboard') %></a>
|
||||
<% if (typeof breadcrumb !== 'undefined' && breadcrumb.length) { %>
|
||||
<% breadcrumb.forEach((item, index) => { %>
|
||||
<span class="separator">/</span>
|
||||
<% if (index === breadcrumb.length - 1) { %>
|
||||
<span class="current"><%= item.label %></span>
|
||||
<% } else if (item.href) { %>
|
||||
<a href="<%= item.href %>"><%= item.label %></a>
|
||||
<% } else { %>
|
||||
<span><%= item.label %></span>
|
||||
<% } %>
|
||||
<% }); %>
|
||||
<% } else if (typeof title !== 'undefined' && title) { %>
|
||||
<span class="separator">/</span>
|
||||
<span class="current"><%= title %></span>
|
||||
<% } %>
|
||||
</nav>
|
||||
|
||||
<div class="ux35-topbar-spacer"></div>
|
||||
|
||||
<div class="ux35-topbar-actions">
|
||||
<button type="button" class="ux35-topbar-btn" id="refresh-btn" title="<%= _('actions.refresh') %>">
|
||||
<span class="material-icons">refresh</span>
|
||||
</button>
|
||||
|
||||
<div class="notif-wrapper">
|
||||
<button type="button" class="ux35-topbar-btn notif-btn" id="notif-btn" title="<%= _('notifications.title') %>" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="material-icons">notifications</span>
|
||||
<span class="notif-badge" id="notif-badge" hidden>0</span>
|
||||
</button>
|
||||
<div class="notif-dropdown" id="notif-dropdown" role="menu" hidden>
|
||||
<div class="notif-header">
|
||||
<span><%= _('notifications.title') %></span>
|
||||
<button type="button" class="notif-mark-all" id="notif-mark-all"><%= _('notifications.mark_all_read') %></button>
|
||||
</div>
|
||||
<div class="notif-list" id="notif-list" aria-live="polite">
|
||||
<div class="notif-empty"><%= _('notifications.empty') %></div>
|
||||
</div>
|
||||
<div class="notif-footer">
|
||||
<a href="/help-requests" class="notif-see-all"><%= _('notifications.see_all') %></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="ux35-topbar-btn" id="ux35-theme-btn" title="<%= _('theme.title') || 'Theme' %>" aria-label="<%= _('theme.title') || 'Theme' %>">
|
||||
<span class="material-icons" id="ux35-theme-icon">dark_mode</span>
|
||||
</button>
|
||||
|
||||
<button type="button" class="ux35-topbar-btn accessibility-btn" id="accessibility-btn" title="<%= _('accessibility.title') %>" aria-label="<%= _('accessibility.title') %>" aria-haspopup="dialog">
|
||||
<span class="material-icons">accessibility_new</span>
|
||||
</button>
|
||||
|
||||
<div class="lang-selector">
|
||||
<button type="button" class="ux35-topbar-btn" id="lang-btn" title="<%= _('settings.language') %>">
|
||||
<span class="material-icons">language</span>
|
||||
</button>
|
||||
<div class="lang-dropdown" id="lang-dropdown">
|
||||
<% (availableLanguageList || Object.values(availableLanguages || {})).forEach(function(language) { %>
|
||||
<div class="lang-option <%= lang === language.code ? 'active' : '' %>" data-lang="<%= language.code %>" role="button" tabindex="0">
|
||||
<span class="lang-flag"><%= language.flag || '🌐' %></span>
|
||||
<span class="lang-label"><%= language.native || language.name || language.code %></span>
|
||||
<% if (lang === language.code) { %>
|
||||
<span class="material-icons lang-check">check</span>
|
||||
<% } %>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lang-selector">
|
||||
<button type="button" class="ux35-topbar-btn" id="user-btn" title="<%= user ? user.username : 'User' %>">
|
||||
<span class="material-icons">account_circle</span>
|
||||
</button>
|
||||
<div class="lang-dropdown" id="user-dropdown">
|
||||
<div class="lang-option" id="change-password-btn">
|
||||
<span class="material-icons" style="font-size: 18px;">lock</span>
|
||||
<span><%= _('auth.change_password') %></span>
|
||||
</div>
|
||||
<div class="lang-option" id="logout-btn" style="color: var(--accent-red);">
|
||||
<span class="material-icons" style="font-size: 18px;">logout</span>
|
||||
<span><%= _('auth.logout') %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -676,6 +676,33 @@
|
||||
<p class="settings-panel-desc">${_('branding.colors_desc')}</p>
|
||||
</div>
|
||||
<div class="settings-panel-body">
|
||||
<div class="form-group" style="max-width: 520px; margin-bottom: 1.25rem;">
|
||||
<label class="form-label" for="theme-mode-select">${_('theme.title') || 'Theme'}</label>
|
||||
<p class="settings-row-hint">${_('ux35.theme_mode_hint') || 'Light and Dark are built-in. Custom activates when you edit colors below.'}</p>
|
||||
<div class="logo-type-selector" id="theme-mode-selector" role="radiogroup" aria-label="${_('theme.title') || 'Theme'}">
|
||||
<label class="logo-type-option">
|
||||
<input type="radio" name="theme-mode" value="dark" checked>
|
||||
<span class="logo-type-card">
|
||||
<span class="material-icons">dark_mode</span>
|
||||
<span>${_('theme.dark') || 'Dark'}</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="logo-type-option">
|
||||
<input type="radio" name="theme-mode" value="light">
|
||||
<span class="logo-type-card">
|
||||
<span class="material-icons">light_mode</span>
|
||||
<span>${_('theme.light') || 'Light'}</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="logo-type-option">
|
||||
<input type="radio" name="theme-mode" value="custom">
|
||||
<span class="logo-type-card">
|
||||
<span class="material-icons">palette</span>
|
||||
<span>${_('theme.custom') || 'Custom'}</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="color-grid">
|
||||
<div class="color-group">
|
||||
<h3 class="color-group-title">${_('branding.colors_backgrounds')}</h3>
|
||||
|
||||
Reference in New Issue
Block a user