diff --git a/web/src/App.vue b/web/src/App.vue
index a3a8ba9..dbb1ae6 100644
--- a/web/src/App.vue
+++ b/web/src/App.vue
@@ -16,7 +16,13 @@
{{ link.label }}
-
+
+
@@ -25,6 +31,7 @@
diff --git a/web/src/main.ts b/web/src/main.ts
index 37aa5aa..3cb1e95 100644
--- a/web/src/main.ts
+++ b/web/src/main.ts
@@ -3,9 +3,17 @@ import { createPinia } from 'pinia'
import 'virtual:uno.css'
import '@fontsource/jetbrains-mono/400.css'
import '@fontsource/jetbrains-mono/600.css'
+import './style/theme.css'
import App from './App.vue'
import router from './router'
+// Apply saved theme or OS preference before first paint to avoid flash
+const saved = localStorage.getItem('ts-theme')
+const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
+if (saved === 'dark' || (!saved && prefersDark)) {
+ document.documentElement.classList.add('dark')
+}
+
const app = createApp(App)
app.use(createPinia())
app.use(router)
diff --git a/web/src/style/theme.css b/web/src/style/theme.css
new file mode 100644
index 0000000..318b1dd
--- /dev/null
+++ b/web/src/style/theme.css
@@ -0,0 +1,52 @@
+/* Turnstone theme — light (default) and dark override via .dark on */
+
+:root {
+ --color-surface: #f8fafc;
+ --color-surface-raised: #f1f5f9;
+ --color-surface-border: #e2e8f0;
+ --color-accent: #2563eb;
+ --color-accent-muted: #dbeafe;
+ --color-text-primary: #0f172a;
+ --color-text-muted: #475569;
+ --color-text-dim: #94a3b8;
+
+ /* severity text colors */
+ --color-sev-debug: #6b7280;
+ --color-sev-info: #2563eb;
+ --color-sev-warn: #d97706;
+ --color-sev-error: #dc2626;
+ --color-sev-critical: #b91c1c;
+
+ /* severity badge — explicit bg + text pairs (no opacity hacks) */
+ --badge-low-bg: #f1f5f9; --badge-low-text: #475569;
+ --badge-medium-bg: #fef3c7; --badge-medium-text: #b45309;
+ --badge-high-bg: #ffedd5; --badge-high-text: #c2410c;
+ --badge-critical-bg: #fee2e2; --badge-critical-text: #b91c1c;
+}
+
+.dark {
+ --color-surface: #0f1117;
+ --color-surface-raised: #161b25;
+ --color-surface-border: #1e2636;
+ --color-accent: #4e9af1;
+ --color-accent-muted: #2a4a72;
+ --color-text-primary: #e2e8f0;
+ --color-text-muted: #94a3b8;
+ --color-text-dim: #475569;
+
+ --color-sev-debug: #6b7280;
+ --color-sev-info: #60a5fa;
+ --color-sev-warn: #fbbf24;
+ --color-sev-error: #f87171;
+ --color-sev-critical: #ef4444;
+
+ --badge-low-bg: #1e293b; --badge-low-text: #94a3b8;
+ --badge-medium-bg: #451a03; --badge-medium-text: #fbbf24;
+ --badge-high-bg: #431407; --badge-high-text: #fb923c;
+ --badge-critical-bg: #450a0a; --badge-critical-text: #f87171;
+}
+
+/* Smooth theme transitions */
+*, *::before, *::after {
+ transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
+}
diff --git a/web/src/views/IncidentsView.vue b/web/src/views/IncidentsView.vue
index adddc66..39b29e8 100644
--- a/web/src/views/IncidentsView.vue
+++ b/web/src/views/IncidentsView.vue
@@ -149,7 +149,7 @@
>
{{ inc.label }} |
-
+
{{ inc.severity }}
|
@@ -366,13 +366,15 @@ async function selectIncident(inc: Incident) {
}
// ── helpers ───────────────────────────────────────────────────
-function severityClass(sev: string): string {
+function severityStyle(sev: string): Record
{
+ const k = sev?.toLowerCase() ?? 'low'
+ const known = ['low', 'medium', 'high', 'critical']
+ const key = known.includes(k) ? k : 'low'
return {
- low: 'bg-surface-border text-text-muted',
- medium: 'bg-yellow-900 text-yellow-300',
- high: 'bg-orange-900 text-orange-300',
- critical: 'bg-red-900 text-red-300',
- }[sev] ?? 'bg-surface-border text-text-muted'
+ backgroundColor: `var(--badge-${key}-bg)`,
+ color: `var(--badge-${key}-text)`,
+ borderColor: `var(--badge-${key}-text)`,
+ }
}
function severityTextClass(sev: string | null): string {
diff --git a/web/uno.config.ts b/web/uno.config.ts
index f67af61..2a24e79 100644
--- a/web/uno.config.ts
+++ b/web/uno.config.ts
@@ -2,28 +2,29 @@ import { defineConfig, presetAttributify, presetWind } from 'unocss'
export default defineConfig({
presets: [presetWind(), presetAttributify()],
+ dark: 'class',
theme: {
colors: {
surface: {
- DEFAULT: '#0f1117',
- raised: '#161b25',
- border: '#1e2636',
+ DEFAULT: 'var(--color-surface)',
+ raised: 'var(--color-surface-raised)',
+ border: 'var(--color-surface-border)',
},
accent: {
- DEFAULT: '#4e9af1',
- muted: '#2a4a72',
+ DEFAULT: 'var(--color-accent)',
+ muted: 'var(--color-accent-muted)',
},
sev: {
- debug: '#6b7280',
- info: '#60a5fa',
- warn: '#fbbf24',
- error: '#f87171',
- critical: '#ef4444',
+ debug: 'var(--color-sev-debug)',
+ info: 'var(--color-sev-info)',
+ warn: 'var(--color-sev-warn)',
+ error: 'var(--color-sev-error)',
+ critical: 'var(--color-sev-critical)',
},
text: {
- primary: '#e2e8f0',
- muted: '#94a3b8',
- dim: '#475569',
+ primary: 'var(--color-text-primary)',
+ muted: 'var(--color-text-muted)',
+ dim: 'var(--color-text-dim)',
},
},
fontFamily: {