Padlock-on-a-shield mark, dark and light themes, history note

A new mark: a padlock on a shield with the W as its keyhole, in the
console's teal on a dark shield. It replaces the placeholder W tile in the
sidebar, on the sign-in and setup pages, and as the favicon, with app icons
and a web manifest. The SVG sources, a single-colour variant, a wordmark
and a social preview image live in docs/brand.

The console now has a theme choice: dark by default, light, or follow the
system, switched from the sidebar or the Account page and remembered per
browser. The light palette is a first-class theme rather than a media
query, so both are laid out the same way.

The README opens with the mark and gains a History section about the 2025
WGX installer this replaces.
This commit is contained in:
jcoffey
2026-09-12 20:48:53 -07:00
parent 6385a2a466
commit 6c1a13d1fc
26 changed files with 244 additions and 47 deletions
+5 -4
View File
@@ -2,6 +2,8 @@ import type { ReactNode } from "react";
import { Link, useLocation } from "wouter";
import { Activity, ClipboardList, LayoutDashboard, LogOut, Settings, Shield, Users, Wifi, WifiOff } from "lucide-react";
import { useAuth, useLive } from "../state";
import { Mark } from "./Mark";
import { ThemeSwitch } from "./ThemeSwitch";
const items = [
{ href: "/", label: "Dashboard", icon: LayoutDashboard },
@@ -20,10 +22,8 @@ export function Layout({ children }: { children: ReactNode }) {
<div className="shell">
<aside className="sidebar">
<div className="brand">
<div className="brand-mark" aria-hidden="true">
<svg width="18" height="18" viewBox="0 0 32 32">
<path d="M7 10l4 12 5-9 5 9 4-12" fill="none" stroke="currentColor" strokeWidth="3.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
<div className="brand-mark">
<Mark size={34} />
</div>
<div>
<div className="brand-name">WGX</div>
@@ -43,6 +43,7 @@ export function Layout({ children }: { children: ReactNode }) {
})}
</nav>
<div className="sidebar-foot">
<ThemeSwitch compact />
<div title={connected ? "Live updates connected" : "Live updates reconnecting"} className="nowrap">
{connected ? <Wifi size={13} style={{ verticalAlign: -2, color: "var(--ok)" }} /> : <WifiOff size={13} style={{ verticalAlign: -2, color: "var(--warn)" }} />} {connected ? "live" : "reconnecting"}
</div>
+24
View File
@@ -0,0 +1,24 @@
// The WGX mark: a padlock on a shield, with the W as its keyhole. The same
// drawing as docs/brand/wgx-mark.svg, inlined so it needs no request and
// takes its size from wherever it is placed.
export function Mark({ size = 32 }: { size?: number }) {
return (
<svg width={size} height={size} viewBox="0 0 128 128" role="img" aria-label="WGX">
<defs>
<linearGradient id="wgx-shield" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stopColor="#26332e" />
<stop offset="1" stopColor="#121a17" />
</linearGradient>
<linearGradient id="wgx-lock" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stopColor="#3fae90" />
<stop offset="1" stopColor="#2b8f75" />
</linearGradient>
</defs>
<path d="M64 6 L114 22 V60 C114 90 93 112 64 122 C35 112 14 90 14 60 V22 Z" fill="url(#wgx-shield)" />
<path d="M64 6 L114 22 V60 C114 90 93 112 64 122 C35 112 14 90 14 60 V22 Z" fill="none" stroke="#3fae90" strokeWidth="3" strokeLinejoin="round" />
<path d="M46 62 V50 a18 18 0 0 1 36 0 V62" fill="none" stroke="#3fae90" strokeWidth="8.5" strokeLinecap="round" />
<rect x="34" y="58" width="60" height="42" rx="10" fill="url(#wgx-lock)" />
<path d="M46 70 L53 89 L64 76 L75 89 L82 70" fill="none" stroke="#ffffff" strokeWidth="6" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
+31
View File
@@ -0,0 +1,31 @@
import { useState } from "react";
import { Monitor, Moon, Sun } from "lucide-react";
import { getThemeChoice, setThemeChoice, type ThemeChoice } from "../theme";
const options: { value: ThemeChoice; label: string; icon: typeof Moon }[] = [
{ value: "dark", label: "Dark", icon: Moon },
{ value: "light", label: "Light", icon: Sun },
{ value: "system", label: "System", icon: Monitor },
];
// Dark / Light / System. `compact` shows icons only, for the sidebar.
export function ThemeSwitch({ compact = false }: { compact?: boolean }) {
const [choice, setChoice] = useState<ThemeChoice>(getThemeChoice);
const pick = (v: ThemeChoice) => {
setThemeChoice(v);
setChoice(v);
};
return (
<div className="segmented" role="radiogroup" aria-label="Theme">
{options.map((o) => {
const Icon = o.icon;
return (
<button key={o.value} role="radio" aria-checked={choice === o.value} aria-label={o.label} title={o.label} className={choice === o.value ? "active" : ""} onClick={() => pick(o.value)}>
<Icon size={14} style={{ verticalAlign: -2 }} />
{!compact && <span style={{ marginLeft: 6 }}>{o.label}</span>}
</button>
);
})}
</div>
);
}