Make Appearance light mode work (gray theme) + roadmap GNOME/KDE RDP (#50)
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 <html>, 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 <ssamjame@amazon.com> Co-authored-by: Kiro <noreply@kiro.dev>
This commit is contained in:
parent
d8223b01cc
commit
08139ff831
5 changed files with 154 additions and 20 deletions
34
ROADMAP.md
34
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.
|
||||
|
|
|
|||
|
|
@ -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 <html>. 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);
|
||||
}
|
||||
|
|
|
|||
48
src/lib/theme.ts
Normal file
48
src/lib/theme.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// App appearance prefs (theme + a few visual knobs), persisted to localStorage
|
||||
// and applied by toggling attributes / CSS variables on <html>. 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')
|
||||
}
|
||||
|
|
@ -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(
|
||||
<StrictMode>
|
||||
|
|
|
|||
|
|
@ -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<AppearancePrefs>(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<AppearancePrefs>) {
|
||||
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 (
|
||||
<div style={cardBase}>
|
||||
|
|
@ -306,7 +318,7 @@ function AppearanceSection() {
|
|||
{(['dark', 'light'] as const).map((t) => (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => setTheme(t)}
|
||||
onClick={() => update({ theme: t })}
|
||||
className="cursor-pointer border-none capitalize"
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
|
|
@ -356,7 +368,7 @@ function AppearanceSection() {
|
|||
min={12}
|
||||
max={16}
|
||||
value={fontSize}
|
||||
onChange={(e) => setFontSize(Number(e.target.value))}
|
||||
onChange={(e) => update({ fontSize: Number(e.target.value) })}
|
||||
className="w-full"
|
||||
style={{ accentColor: '#C8A434' }}
|
||||
/>
|
||||
|
|
@ -372,20 +384,15 @@ function AppearanceSection() {
|
|||
min={4}
|
||||
max={16}
|
||||
value={radius}
|
||||
onChange={(e) => setRadius(Number(e.target.value))}
|
||||
onChange={(e) => update({ radius: Number(e.target.value) })}
|
||||
className="w-full"
|
||||
style={{ accentColor: '#C8A434' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between" style={{ marginBottom: '16px' }}>
|
||||
<span style={{ fontSize: '13px', color: '#E8E6E0' }}>Sidebar Expanded by Default</span>
|
||||
<Toggle on={sidebarExpanded} onClick={() => setSidebarExpanded((v) => !v)} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span style={{ fontSize: '13px', color: '#E8E6E0' }}>Animations</span>
|
||||
<Toggle on={animations} onClick={() => setAnimations((v) => !v)} />
|
||||
<Toggle on={animations} onClick={() => update({ animations: !animations })} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue