feat(frontend): add a terminal theme and three-way theme switcher
Adds the cyber theme to the console and login page, replaces the dark/light toggle with a theme switcher, and moves the login page script to /login.js so it is served under the existing CSP.
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
var t = localStorage.getItem("floci-theme");
|
||||
document.documentElement.setAttribute(
|
||||
"data-theme",
|
||||
t === "light" ? "light" : "dark",
|
||||
t === "light" || t === "cyber" ? t : "dark",
|
||||
);
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {NavLink, Outlet, useLocation, useSearchParams} from 'react-router-dom'
|
||||
import {AlertTriangle, ChevronsLeft, ChevronsRight, LayoutDashboard, LogOut, Moon, Search, Sun} from 'lucide-react'
|
||||
import {AlertTriangle, ChevronsLeft, ChevronsRight, LayoutDashboard, LogOut, Moon, Search, Sun, Terminal} from 'lucide-react'
|
||||
import {useCallback, useEffect, useRef, useState} from 'react'
|
||||
import theCloud from '../../../../TheCloud.png'
|
||||
import {useTheme} from '@/lib/useTheme'
|
||||
@@ -157,9 +157,8 @@ function groupByGroup(services: CloudServiceDescriptor[]): Array<[string, CloudS
|
||||
export function Layout() {
|
||||
const location = useLocation()
|
||||
const activeCloud = activeCloudFromPath(location.pathname)
|
||||
const {theme, toggle} = useTheme()
|
||||
const {theme, setTheme} = useTheme()
|
||||
const {collapsed, toggle: toggleSidebar, toggleRef} = useSidebar()
|
||||
const isDark = theme === 'dark'
|
||||
const {data, isError} = useQuery({
|
||||
queryKey: ['cloud-status', activeCloud],
|
||||
queryFn: ({signal}) => getCloudStatus(activeCloud, signal),
|
||||
@@ -209,15 +208,11 @@ export function Layout() {
|
||||
<div className="shell">
|
||||
<header className="topbar">
|
||||
<TopbarSearch/>
|
||||
<button
|
||||
className="icon-btn"
|
||||
type="button"
|
||||
onClick={toggle}
|
||||
title={isDark ? 'Switch to light theme' : 'Switch to dark theme'}
|
||||
aria-label={isDark ? 'Switch to light theme' : 'Switch to dark theme'}
|
||||
>
|
||||
{isDark ? <Sun size={14} aria-hidden="true"/> : <Moon size={14} aria-hidden="true"/>}
|
||||
</button>
|
||||
<div className="theme-switcher" role="group" aria-label="Console theme">
|
||||
<button className={`icon-btn ${theme === 'dark' ? 'active' : ''}`} type="button" onClick={() => setTheme('dark')} title="Dark theme" aria-label="Dark theme" aria-pressed={theme === 'dark'}><Moon size={14} aria-hidden="true"/></button>
|
||||
<button className={`icon-btn ${theme === 'light' ? 'active' : ''}`} type="button" onClick={() => setTheme('light')} title="Light theme" aria-label="Light theme" aria-pressed={theme === 'light'}><Sun size={14} aria-hidden="true"/></button>
|
||||
<button className={`icon-btn ${theme === 'cyber' ? 'active' : ''}`} type="button" onClick={() => setTheme('cyber')} title="Terminal theme" aria-label="Terminal theme" aria-pressed={theme === 'cyber'}><Terminal size={14} aria-hidden="true"/></button>
|
||||
</div>
|
||||
<div id="topbar-status" className="topbar-status"/>
|
||||
<AccountSwitcher/>
|
||||
<button className="icon-btn" type="button" title="Sign out" aria-label="Sign out" onClick={() => void signOut()}>
|
||||
|
||||
@@ -218,6 +218,58 @@
|
||||
--sidebar-overlay-shadow: 6px 0 28px rgba(38, 48, 82, 0.18);
|
||||
}
|
||||
|
||||
[data-theme="cyber"] {
|
||||
color-scheme: dark;
|
||||
|
||||
--bg: #02060a;
|
||||
--surface: #050d10;
|
||||
--elevated: #071216;
|
||||
--raised: #0a191d;
|
||||
--raised-hover: #10252a;
|
||||
--border: rgba(78, 255, 150, 0.28);
|
||||
--border-faint: rgba(78, 255, 150, 0.16);
|
||||
--border-lighter: rgba(78, 255, 150, 0.08);
|
||||
--border-hover: #ffea00;
|
||||
--active-nav-bg: rgba(78, 255, 150, 0.14);
|
||||
|
||||
--text: #8affb8;
|
||||
--text-bright: #d6ffe6;
|
||||
--text-2: #62d992;
|
||||
--text-3: #2f9a68;
|
||||
--text-muted: var(--text-3);
|
||||
--text-nav: #8affb8;
|
||||
|
||||
--status-ok: #4eff96;
|
||||
--status-warn: #ffea00;
|
||||
--status-error: #ff8da1;
|
||||
|
||||
--accent: #4eff96;
|
||||
--accent-tint: rgba(78, 255, 150, 0.14);
|
||||
--accent-tint-faint: rgba(78, 255, 150, 0.07);
|
||||
--accent-tint-border: rgba(78, 255, 150, 0.34);
|
||||
|
||||
--link: #ffea00;
|
||||
--link-tint: rgba(255, 234, 0, 0.08);
|
||||
--link-tint-hover: rgba(255, 234, 0, 0.14);
|
||||
--link-tint-border: rgba(255, 234, 0, 0.3);
|
||||
|
||||
--hover: rgba(78, 255, 150, 0.06);
|
||||
--hover-md: rgba(78, 255, 150, 0.1);
|
||||
--hover-lg: rgba(78, 255, 150, 0.14);
|
||||
--input-border: rgba(78, 255, 150, 0.42);
|
||||
--input-bg: #030b0d;
|
||||
--connection-bg: #02090c;
|
||||
--kbd-border: rgba(78, 255, 150, 0.35);
|
||||
--scrollbar-thumb: #26784e;
|
||||
--service-card-bg: #050d10;
|
||||
--service-card-hover: #071216;
|
||||
--fn-card-hover: #0a191d;
|
||||
--bulk-bar-bg: #071b13;
|
||||
--bulk-bar-border: #4eff96;
|
||||
--bulk-bar-text: #d6ffe6;
|
||||
--code-bg: #000;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -626,6 +678,26 @@ pre,
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.theme-switcher {
|
||||
display: inline-flex;
|
||||
gap: 2px;
|
||||
padding: 2px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 5px;
|
||||
background: var(--connection-bg);
|
||||
}
|
||||
|
||||
.theme-switcher .icon-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.theme-switcher .icon-btn.active {
|
||||
border-color: var(--accent);
|
||||
background: var(--accent);
|
||||
color: var(--bg);
|
||||
}
|
||||
|
||||
.search {
|
||||
position: relative;
|
||||
width: min(480px, 100%);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
|
||||
type Theme = 'dark' | 'light'
|
||||
export type Theme = 'dark' | 'light' | 'cyber'
|
||||
|
||||
function getStored(): Theme {
|
||||
const v = localStorage.getItem('floci-theme')
|
||||
return v === 'light' ? 'light' : 'dark'
|
||||
return v === 'light' || v === 'cyber' ? v : 'dark'
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
@@ -15,7 +15,5 @@ export function useTheme() {
|
||||
localStorage.setItem('floci-theme', theme)
|
||||
}, [theme])
|
||||
|
||||
const toggle = () => setTheme(t => t === 'dark' ? 'light' : 'dark')
|
||||
|
||||
return {theme, toggle}
|
||||
return {theme, setTheme}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user