From 45f75d42b45d1b2a1619d27c2bd66bd77ee0dd27 Mon Sep 17 00:00:00 2001 From: Keagan Stokoe Date: Tue, 17 Mar 2026 12:21:57 +0200 Subject: [PATCH 1/3] Add light theme with sidebar toggle Adds a complete light theme toggled via a sun/moon button in the sidebar header. Theme choice persists in localStorage and applies before render to prevent flash. This is a practical demonstration of the semantic token system: the entire light theme is CSS variable overrides with zero component changes. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/public/css/shell.css | 48 +++++ lib/public/css/theme.css | 213 +++++++++++++++++++++++ lib/public/js/components/sidebar.js | 4 + lib/public/js/components/theme-toggle.js | 66 +++++++ lib/public/login.html | 3 + lib/public/setup.html | 4 + 6 files changed, 338 insertions(+) create mode 100644 lib/public/js/components/theme-toggle.js diff --git a/lib/public/css/shell.css b/lib/public/css/shell.css index de8315d..2d9b08e 100644 --- a/lib/public/css/shell.css +++ b/lib/public/css/shell.css @@ -491,3 +491,51 @@ pointer-events: auto; } } + +/* ── Light theme overrides ─────────────────────── */ + +[data-theme="light"] .app-sidebar { + background: + linear-gradient(180deg, rgba(0, 0, 0, 0.01) 0%, rgba(0, 0, 0, 0.03) 100%), + var(--bg-sidebar); +} + +[data-theme="light"] .brand-dropdown { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); +} + +[data-theme="light"] .global-restart-banner__content { + background: rgba(254, 243, 199, 0.97); + border-color: rgba(253, 224, 71, 0.6); + box-shadow: 0 18px 46px rgba(0, 0, 0, 0.12); +} + +[data-theme="light"] .global-restart-banner__text { + color: #92400e; +} + +[data-theme="light"] .global-restart-banner__dismiss { + color: #92400e; +} + +[data-theme="light"] .sidebar-update-btn { + border-color: rgba(202, 138, 4, 0.3); + color: #a16207; + background: rgba(202, 138, 4, 0.06); +} + +[data-theme="light"] .sidebar-update-btn:hover { + background: rgba(202, 138, 4, 0.1); + border-color: rgba(202, 138, 4, 0.4); +} + +[data-theme="light"] .sidebar-resizer:hover::after, +[data-theme="light"] .sidebar-resizer.is-resizing::after { + background: rgba(8, 145, 178, 0.55); +} + +@media (max-width: 768px) { + [data-theme="light"] .app-sidebar { + box-shadow: 0 8px 28px rgba(0, 0, 0, 0.15); + } +} diff --git a/lib/public/css/theme.css b/lib/public/css/theme.css index 7cc526b..b6d5fbf 100644 --- a/lib/public/css/theme.css +++ b/lib/public/css/theme.css @@ -52,6 +52,52 @@ --status-info-border: rgba(14, 116, 144, 0.8); } +/* ── Light theme ─────────────────────────────────── */ +[data-theme="light"] { + --bg: #f8f9fb; + --bg-sidebar: #f0f2f5; + --bg-content: #ffffff; + --bg-hover: rgba(0, 0, 0, 0.04); + --bg-active: rgba(8, 145, 178, 0.08); + --border: rgba(0, 0, 0, 0.08); + --border-strong: rgba(0, 0, 0, 0.15); + --text: #1f2937; + --text-muted: #6b7280; + --text-dim: #9ca3af; + --text-bright: #111827; + --card-label-bright: #1f2937; + --accent: #0891b2; + --accent-dim: rgba(8, 145, 178, 0.3); + --accent-link: rgba(8, 145, 178, 0.7); + --orange: #c2410c; + --comment: #9ca3af; + --keyword: #dc2626; + --string: #2563eb; + --number: #0284c7; + --panel-bg-contrast: rgba(0, 0, 0, 0.02); + --panel-border-contrast: rgba(0, 0, 0, 0.1); + --field-bg-contrast: rgba(0, 0, 0, 0.04); + --field-border-contrast: rgba(0, 0, 0, 0.15); + --overlay: rgba(0, 0, 0, 0.5); + + --status-error: #dc2626; + --status-error-muted: #ef4444; + --status-error-bg: rgba(254, 226, 226, 0.95); + --status-error-border: rgba(252, 165, 165, 0.8); + --status-warning: #ca8a04; + --status-warning-muted: #eab308; + --status-warning-bg: rgba(254, 249, 195, 0.95); + --status-warning-border: rgba(253, 224, 71, 0.8); + --status-success: #16a34a; + --status-success-muted: #22c55e; + --status-success-bg: rgba(220, 252, 231, 0.95); + --status-success-border: rgba(134, 239, 172, 0.8); + --status-info: #0891b2; + --status-info-muted: #06b6d4; + --status-info-bg: rgba(207, 250, 254, 0.95); + --status-info-border: rgba(103, 232, 249, 0.8); +} + html, body { height: 100%; } body { @@ -738,3 +784,170 @@ textarea:focus { overflow-y: auto !important; } +/* ── Light theme overrides for hardcoded dark patterns ── */ + +[data-theme="light"] body::before { + background-image: + linear-gradient(rgba(0, 0, 0, 0.03) 1px, transparent 1px), + linear-gradient(90deg, rgba(0, 0, 0, 0.03) 1px, transparent 1px); +} + +[data-theme="light"] .ac-history-item { + background: rgba(0, 0, 0, 0.02); +} + +[data-theme="light"] .ac-history-summary { + color: var(--text); +} + +[data-theme="light"] .ac-history-item[open] > .ac-history-summary .ac-history-toggle { + color: var(--text); +} + +[data-theme="light"] .ac-surface-inset { + background: rgba(0, 0, 0, 0.02); +} + +[data-theme="light"] .snippet-collapse-fade { + background: linear-gradient(to bottom, transparent 0%, rgba(255, 255, 255, 0.85) 70%); +} + +[data-theme="light"] input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):focus, +[data-theme="light"] select:focus, +[data-theme="light"] textarea:focus { + border-color: rgba(0, 0, 0, 0.35); +} + +[data-theme="light"] ::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.12); +} + +[data-theme="light"] .scope-btn { background: rgba(0, 0, 0, 0.03); } +[data-theme="light"] .scope-btn-read.active, +[data-theme="light"] .scope-btn-write.active { + background: rgba(0, 0, 0, 0.03); + color: var(--text-bright); + border-color: rgba(0, 0, 0, 0.35); +} + +[data-theme="light"] .ac-btn-cyan { + border: 1px solid var(--accent-dim); + background: linear-gradient(180deg, rgba(8, 145, 178, 0.1) 0%, rgba(8, 145, 178, 0.05) 100%); + color: var(--accent); + box-shadow: inset 0 0 0 1px rgba(8, 145, 178, 0.08); +} + +[data-theme="light"] .ac-btn-cyan:hover:not(:disabled) { + border-color: rgba(8, 145, 178, 0.6); + background: linear-gradient(180deg, rgba(8, 145, 178, 0.16) 0%, rgba(8, 145, 178, 0.08) 100%); + color: #065666; + box-shadow: inset 0 0 0 1px rgba(8, 145, 178, 0.15), 0 0 12px rgba(8, 145, 178, 0.1); +} + +[data-theme="light"] .ac-btn-cyan-ghost { + border: 1px solid var(--accent-dim); + color: var(--accent); + background: rgba(8, 145, 178, 0.04); +} + +[data-theme="light"] .ac-btn-cyan-ghost:hover { + border-color: rgba(8, 145, 178, 0.5); + color: #065666; + background: rgba(8, 145, 178, 0.08); +} + +[data-theme="light"] .ac-btn-secondary { + color: var(--text); + background: rgba(0, 0, 0, 0.02); +} + +[data-theme="light"] .ac-btn-secondary:hover:not(:disabled) { + border-color: rgba(0, 0, 0, 0.25); + color: var(--text-bright); + background: rgba(0, 0, 0, 0.04); +} + +[data-theme="light"] .ac-btn-ghost:hover:not(:disabled) { + color: var(--text-bright); +} + +[data-theme="light"] .ac-btn-danger { + border: 1px solid rgba(220, 38, 38, 0.3); + background: rgba(220, 38, 38, 0.06); + color: #dc2626; +} + +[data-theme="light"] .ac-btn-danger:hover:not(:disabled) { + border-color: rgba(220, 38, 38, 0.5); + background: rgba(220, 38, 38, 0.1); + color: #b91c1c; +} + +[data-theme="light"] .ac-btn-green { + border: 1px solid rgba(22, 163, 74, 0.3); + background: linear-gradient(180deg, rgba(22, 163, 74, 0.1) 0%, rgba(22, 163, 74, 0.05) 100%); + color: #16a34a; + box-shadow: inset 0 0 0 1px rgba(22, 163, 74, 0.08); +} + +[data-theme="light"] .ac-btn-green:hover:not(:disabled) { + border-color: rgba(22, 163, 74, 0.5); + background: linear-gradient(180deg, rgba(22, 163, 74, 0.16) 0%, rgba(22, 163, 74, 0.08) 100%); + color: #15803d; + box-shadow: inset 0 0 0 1px rgba(22, 163, 74, 0.15), 0 0 12px rgba(22, 163, 74, 0.08); +} + +[data-theme="light"] .ac-toggle-track { + background: rgba(0, 0, 0, 0.1); +} + +[data-theme="light"] .ac-toggle-thumb { + background: #9ca3af; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1); +} + +[data-theme="light"] .ac-toggle-input:checked + .ac-toggle-track { + border-color: rgba(8, 145, 178, 0.6); + background: rgba(8, 145, 178, 0.12); + box-shadow: inset 0 0 0 1px rgba(8, 145, 178, 0.15); +} + +[data-theme="light"] .ac-toggle-input:checked + .ac-toggle-track .ac-toggle-thumb { + background: #0891b2; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08); +} + +[data-theme="light"] .ac-toggle-label { + color: var(--text); +} + +[data-theme="light"] .ac-path-card { + background: rgba(0, 0, 0, 0.02); +} + +[data-theme="light"] .ac-path-card:hover { + border-color: rgba(8, 145, 178, 0.4); + background: rgba(8, 145, 178, 0.04); + box-shadow: inset 0 0 0 1px rgba(8, 145, 178, 0.08), 0 0 12px rgba(8, 145, 178, 0.06); +} + +[data-theme="light"] .ac-path-card:hover .ac-path-title { + color: #065666; +} + +[data-theme="light"] .ac-path-card:hover .ac-path-desc { + color: var(--text-muted); +} + +[data-theme="light"] .ac-segmented-control { + background: rgba(0, 0, 0, 0.02); +} + +[data-theme="light"] .ac-segmented-control-button:hover { + background: rgba(0, 0, 0, 0.04); +} + +[data-theme="light"] .ac-segmented-control-dark { + background: rgba(0, 0, 0, 0.04); +} + diff --git a/lib/public/js/components/sidebar.js b/lib/public/js/components/sidebar.js index 6580024..eaa87c3 100644 --- a/lib/public/js/components/sidebar.js +++ b/lib/public/js/components/sidebar.js @@ -21,6 +21,7 @@ import { UpdateActionButton } from "./update-action-button.js"; import { SidebarGitPanel } from "./sidebar-git-panel.js"; import { UpdateModal } from "./update-modal.js"; import { readUiSettings, writeUiSettings } from "../lib/ui-settings.js"; +import { ThemeToggle } from "./theme-toggle.js"; const html = htm.bind(h); const kBrowseBottomPanelUiSettingKey = "browseBottomPanelHeightPx"; @@ -176,6 +177,8 @@ export const AppSidebar = ({