From 08139ff831f5233db3a049f5646c0049df2ea33a Mon Sep 17 00:00:00 2001 From: Samuel James <143277412+SamuelSJames@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:39:50 -0400 Subject: [PATCH] Make Appearance light mode work (gray theme) + roadmap GNOME/KDE RDP (#50) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Appearance tab was local-state-only — light mode did nothing. Wire it up: - index.css: theme color tokens are now CSS variables on :root (dark default) with a [data-theme="light"] override using a soft GRAY page background (#E4E6EB), not white. The @theme tokens + html/body + select reference the vars, so the app shell and any component using bg-page/text-text-primary/etc. themes automatically. - New src/lib/theme.ts: localStorage-backed appearance prefs (theme/fontSize/ radius/animations) + applyAppearance() toggling data-theme on , mirroring the terminalPrefs pattern. - main.tsx applies saved theme before first render (no flash). - Settings AppearanceSection persists + applies on change; theme/fontSize/radius/ animations are live. Dropped the non-functional "Sidebar Expanded by default" toggle. (accent color is still cosmetic-only — full token migration of hardcoded-hex pages is a separate task, noted in ROADMAP.) Also adds the Remote Desktop GNOME & KDE support work to ROADMAP as an add-on (XFCE confirmed working; GNOME needs a FreeRDP-3 guacd image, KDE via xrdp + startplasma-x11). Full detail in docs/rdp-debug-handoff.md. Co-authored-by: Samuel James Co-authored-by: Kiro --- ROADMAP.md | 34 +++++++++++++++++++++++++++ src/index.css | 53 +++++++++++++++++++++++++++++++++++++----- src/lib/theme.ts | 48 ++++++++++++++++++++++++++++++++++++++ src/main.tsx | 4 ++++ src/pages/Settings.tsx | 35 +++++++++++++++++----------- 5 files changed, 154 insertions(+), 20 deletions(-) create mode 100644 src/lib/theme.ts diff --git a/ROADMAP.md b/ROADMAP.md index aafd104..af70f4d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -136,6 +136,40 @@ A complementary **agent** model is planned, split across tiers: --- +## Remote Desktop — GNOME & KDE support — ADD-ON + +**Status:** not built; **XFCE is confirmed working today**, the others are not. +Full investigation + research lives in `docs/rdp-debug-handoff.md`. + +Remote Desktop (Guacamole/`guacd` → RDP) works end-to-end **only with XFCE** on +the target VM, via xrdp's X11 backend. GNOME and KDE do **not** work yet: + +- **GNOME (Wayland-only on modern distros)** is blocked two ways: it ships no + Xorg session for xrdp to launch, *and* its native `gnome-remote-desktop` + mandates NLA that guacd's bundled **FreeRDP 2.x cannot complete** ("wrong + security type"). Verified on Fedora 44 / GNOME 50. +- **KDE Plasma 6** is expected to work like XFCE via xrdp + `startplasma-x11` + (X11 session shipped through ~early 2027), but is **not yet installed/tested**. + +Planned scope (add-on): +1. **Custom `guacd` image built against FreeRDP 3** (Apache's official 1.5.5 and + 1.6.0 images both still ship FreeRDP 2.11.x). This is the real unlock for + GNOME's native Wayland RDP and benefits any modern GNOME / Ubuntu-24.04+ + target — not just this VM. ~30-min from-source build to maintain in + `docker-compose.yml`. +2. **GNOME headless "system" RDP** (GDM handover, GNOME 46+) — the intended + modern path; only viable once (1) lands because it still uses NLA. +3. **KDE Plasma** via xrdp + `startplasma-x11` (quick win, no guacd change; + likely needs a KWin compositing/software-GL tweak on virtual GPUs). +4. **Per-host desktop/session selection** instead of a single global + `/etc/xrdp/startwm.sh`, so one VM can offer XFCE / KDE / GNOME. + +See `docs/rdp-debug-handoff.md` for the suggested order of work and primary-source +references (SUSE headless-GNOME series, jamesnorth GRD setup, RHEL 10 docs, KDE +discuss threads). + +--- + ## Per-integration node tabs — PAID ADD-ON **Status:** not built; planned as a paid-tier feature. diff --git a/src/index.css b/src/index.css index 5451c68..e69d469 100644 --- a/src/index.css +++ b/src/index.css @@ -8,7 +8,13 @@ font-display: swap; } -@theme { +/* Theme color tokens live as CSS variables so light/dark can swap at runtime + by toggling data-theme on . Dark is the default (:root). Light mode + uses a soft GRAY page background (not pure white) per design preference. + Components that use the Tailwind token classes (bg-page, text-text-primary, + border-border, etc.) or var(--color-*) pick this up automatically; some + pages still hardcode hex and won't fully theme yet (tracked in ROADMAP). */ +:root { --color-page: #0D0E10; --color-card: #141518; --color-sidebar: #0A0B0D; @@ -20,6 +26,40 @@ --color-text-primary: #E8E6E0; --color-text-secondary: #7A7D85; --color-teal: #1ABC9C; + --select-bg: rgba(255, 255, 255, 0.04); + --select-option-bg: #141518; + color-scheme: dark; +} + +[data-theme="light"] { + --color-page: #E4E6EB; /* soft gray, not white */ + --color-card: #F4F5F7; /* slightly lighter gray cards */ + --color-sidebar: #DADCE2; /* darker gray sidebar for separation */ + --color-border: #C4C7CF; + --color-gold: #B08D2A; /* slightly darker gold for contrast on light */ + --color-success: #1E9E57; + --color-warning: #C96A18; + --color-danger: #C0392B; + --color-text-primary: #1C1E22; + --color-text-secondary: #5A5D66; + --color-teal: #138D75; + --select-bg: rgba(0, 0, 0, 0.04); + --select-option-bg: #F4F5F7; + color-scheme: light; +} + +@theme { + --color-page: var(--color-page); + --color-card: var(--color-card); + --color-sidebar: var(--color-sidebar); + --color-border: var(--color-border); + --color-gold: var(--color-gold); + --color-success: var(--color-success); + --color-warning: var(--color-warning); + --color-danger: var(--color-danger); + --color-text-primary: var(--color-text-primary); + --color-text-secondary: var(--color-text-secondary); + --color-teal: var(--color-teal); } *, *::before, *::after { @@ -32,13 +72,14 @@ html, body { width: 100%; height: 100%; overflow: hidden; - background-color: #0D0E10; - color: #E8E6E0; + background-color: var(--color-page); + color: var(--color-text-primary); font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; font-size: 14px; line-height: 1.5; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + transition: background-color 0.2s ease, color 0.2s ease; } #root { @@ -86,7 +127,7 @@ select { border: 1px solid rgba(200, 164, 52, 0.18) !important; border-radius: 8px !important; padding: 9px 12px !important; - background-color: rgba(255, 255, 255, 0.04) !important; + background-color: var(--select-bg) !important; cursor: pointer; transition: border-color 0.15s ease, box-shadow 0.15s ease; } @@ -102,6 +143,6 @@ select:focus { } select option { - background-color: #141518; - color: #E8E6E0; + background-color: var(--select-option-bg); + color: var(--color-text-primary); } diff --git a/src/lib/theme.ts b/src/lib/theme.ts new file mode 100644 index 0000000..16127c0 --- /dev/null +++ b/src/lib/theme.ts @@ -0,0 +1,48 @@ +// App appearance prefs (theme + a few visual knobs), persisted to localStorage +// and applied by toggling attributes / CSS variables on . Kept out of a +// component module so it can be imported anywhere (and called once at boot in +// main.tsx before React renders, to avoid a dark→light flash). + +export type ThemeMode = 'dark' | 'light' + +export interface AppearancePrefs { + theme: ThemeMode + fontSize: number // base font-size in px + radius: number // card border-radius in px + animations: boolean +} + +const PREFS_KEY = 'archnest-appearance-prefs' + +export function defaultAppearance(): AppearancePrefs { + return { theme: 'dark', fontSize: 14, radius: 12, animations: true } +} + +export function loadAppearance(): AppearancePrefs { + try { + const raw = localStorage.getItem(PREFS_KEY) + if (raw) return { ...defaultAppearance(), ...JSON.parse(raw) } + } catch { + /* ignore malformed local storage */ + } + return defaultAppearance() +} + +export function saveAppearance(prefs: AppearancePrefs) { + localStorage.setItem(PREFS_KEY, JSON.stringify(prefs)) +} + +// Apply prefs to the document. Light mode flips data-theme="light" (which the +// CSS variable overrides in index.css key off of); dark removes the attribute. +// ponytail: only theme is wired to real CSS right now; fontSize/radius/animations +// are persisted and exposed as CSS vars for components to opt into later — the +// app still mostly hardcodes hex, so a full token migration is a separate task +// (tracked in ROADMAP "Appearance section"). +export function applyAppearance(prefs: AppearancePrefs) { + const root = document.documentElement + if (prefs.theme === 'light') root.setAttribute('data-theme', 'light') + else root.removeAttribute('data-theme') + root.style.setProperty('--app-font-size', `${prefs.fontSize}px`) + root.style.setProperty('--card-radius', `${prefs.radius}px`) + root.style.setProperty('--app-animations', prefs.animations ? '1' : '0') +} diff --git a/src/main.tsx b/src/main.tsx index 5abc14c..9411f37 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5,6 +5,10 @@ import './index.css' import App from './App.tsx' import { AuthProvider } from './lib/AuthContext' import { TerminalSessionProvider } from './lib/TerminalSessionContext' +import { applyAppearance, loadAppearance } from './lib/theme' + +// Apply saved theme before first render to avoid a dark→light flash. +applyAppearance(loadAppearance()) createRoot(document.getElementById('root')!).render( diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 7cfe731..92b62e9 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react' import { useSearchParams } from 'react-router-dom' import { api, ApiError, type Integration, type AuthSession, type LoginEvent, type ManagedUser, type MeshStatus } from '../lib/api' import { useAuth } from '../lib/AuthContext' +import { loadAppearance, saveAppearance, applyAppearance, type AppearancePrefs } from '../lib/theme' import { User, Palette, @@ -289,12 +290,23 @@ function ProfileSection() { } function AppearanceSection() { - const [theme, setTheme] = useState<'dark' | 'light'>('dark') + const [prefs, setPrefs] = useState(loadAppearance) const [accent, setAccent] = useState('Gold') - const [fontSize, setFontSize] = useState(13) - const [radius, setRadius] = useState(12) - const [sidebarExpanded, setSidebarExpanded] = useState(true) - const [animations, setAnimations] = useState(true) + + // Persist + apply on every change so the UI updates live. + function update(patch: Partial) { + setPrefs((prev) => { + const next = { ...prev, ...patch } + saveAppearance(next) + applyAppearance(next) + return next + }) + } + + const theme = prefs.theme + const fontSize = prefs.fontSize + const radius = prefs.radius + const animations = prefs.animations return (
@@ -306,7 +318,7 @@ function AppearanceSection() { {(['dark', 'light'] as const).map((t) => (
-
- Sidebar Expanded by Default - setSidebarExpanded((v) => !v)} /> -
-
Animations - setAnimations((v) => !v)} /> + update({ animations: !animations })} />
)