Align dark mode with Perspective's Pro Dark theme

App chrome now uses Pro Dark's neutral grays (#242526 background,
#2a2c2f panels, #4c505b borders) so the surrounding UI sits cleanly
against the viewer instead of clashing with its warmer tone. Status
accents are desaturated to match. Forecast view sets theme="Pro Dark"
or "Pro Light" on the perspective-viewer in sync with the toggle.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Paul Trowbridge 2026-04-28 20:32:48 -04:00
parent 1a3209cbc2
commit d70d813604
2 changed files with 46 additions and 24 deletions

View File

@ -13,17 +13,20 @@
--accent-text: #1d4ed8;
}
/* Dark palette tuned to Perspective's "Pro Dark" theme:
bg #242526, tooltip #2a2c2f, gridline #3b3f46, inactive #61656e,
inactive border #4c505b, active #2770a9, legend #c5c9d0. */
.dark {
--bg-primary: #111827;
--bg-secondary: #1f2937;
--bg-tertiary: #374151;
--text-primary: #f9fafb;
--text-secondary: #e5e7eb;
--text-muted: #6b7280;
--border-color: #374151;
--border-light: #1f2937;
--accent-bg: #1e3a5f;
--accent-text: #60a5fa;
--bg-primary: #242526;
--bg-secondary: #2a2c2f;
--bg-tertiary: #3b3f46;
--text-primary: #ffffff;
--text-secondary: #c5c9d0;
--text-muted: #61656e;
--border-color: #4c505b;
--border-light: #3b3f46;
--accent-bg: rgba(39, 113, 170, 0.32);
--accent-text: #4778c2;
}
body { margin: 0; background-color: var(--bg-primary); color: var(--text-primary); }
@ -47,19 +50,29 @@ body { margin: 0; background-color: var(--bg-primary); color: var(--text-primary
.dark .text-blue-700 { color: var(--accent-text); }
.dark .border-blue-300 { border-color: var(--accent-text); }
.dark .hover\:bg-blue-50:hover { background-color: var(--accent-bg); }
.dark .bg-green-50 { background-color: #064e3b; }
.dark .text-green-600 { color: #34d399; }
.dark .text-green-700 { color: #34d399; }
.dark .text-green-400 { color: #34d399; }
.dark .bg-yellow-50 { background-color: #451a03; }
.dark .text-yellow-700 { color: #fbbf24; }
.dark .bg-purple-50 { background-color: #1e1b4b; }
.dark .text-purple-700 { color: #a78bfa; }
.dark .bg-red-50 { background-color: #450a0a; }
.dark .text-red-700 { color: #f87171; }
/* Status accents — desaturated to sit on Pro Dark's neutral background */
.dark .bg-green-50 { background-color: #1a3d2c; }
.dark .text-green-600 { color: #6ee7b7; }
.dark .text-green-700 { color: #6ee7b7; }
.dark .text-green-400 { color: #6ee7b7; }
.dark .bg-yellow-50 { background-color: #3a2e14; }
.dark .text-yellow-700 { color: #f5c66f; }
.dark .bg-amber-50 { background-color: #3a2e14; }
.dark .border-amber-200 { border-color: #5a4a26; }
.dark .text-amber-700 { color: #f5c66f; }
.dark .text-amber-800 { color: #f5c66f; }
.dark .bg-purple-50 { background-color: #2a1f3d; }
.dark .text-purple-600 { color: #c4a8e8; }
.dark .text-purple-700 { color: #c4a8e8; }
.dark .bg-red-50 { background-color: #3d1f1f; }
.dark .text-red-600 { color: #ff9485; }
.dark .text-red-700 { color: #ff9485; }
.dark .border-gray-100 { border-color: var(--border-light); }
.dark .border-gray-200 { border-color: var(--border-color); }
.dark .border-gray-300 { border-color: var(--border-color); }
.dark .border-blue-100 { border-color: var(--border-color); }
.dark .border-b { border-color: var(--border-color); }
.dark .border-t { border-color: var(--border-color); }
.dark .border-r { border-color: var(--border-color); }
@ -73,8 +86,8 @@ body { margin: 0; background-color: var(--bg-primary); color: var(--text-primary
.dark .hover\:border-gray-300:hover { border-color: var(--border-color); }
.dark .hover\:border-gray-400:hover { border-color: var(--border-color); }
.dark .focus\:border-gray-300:focus { border-color: var(--border-color); }
.dark ::selection { background-color: var(--accent-bg); color: var(--accent-text); }
.dark input { background-color: var(--bg-secondary); color: var(--text-primary); }
.dark select { background-color: var(--bg-secondary); color: var(--text-primary); }
.dark textarea { background-color: var(--bg-secondary); color: var(--text-primary); }
.dark ::selection { background-color: var(--accent-bg); color: var(--text-primary); }
.dark input { background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border-color); }
.dark select { background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border-color); }
.dark textarea { background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border-color); }
.dark .bg-transparent { background-color: transparent; }

View File

@ -1,4 +1,5 @@
import { useState, useEffect, useRef } from 'react'
import useTheme from '../theme.jsx'
const LAYOUT_KEY = (vid) => `pf_layout_v${vid}` // last-used layout (auto restore)
const LAYOUTS_KEY = (vid) => `pf_layouts_v${vid}` // named layout list
@ -29,6 +30,7 @@ function cleanLayout(cfg, validCols) {
}
export default function Forecast() {
const { dark } = useTheme()
const [sources, setSources] = useState([])
const [sourceId, setSourceId] = useState('')
const [versions, setVersions] = useState([])
@ -105,6 +107,12 @@ export default function Forecast() {
initViewer(versionId, sourceId)
}, [versionId, sourceId])
useEffect(() => {
if (viewerRef.current) {
viewerRef.current.setAttribute('theme', dark ? 'Pro Dark' : 'Pro Light')
}
}, [dark, versionId])
useEffect(() => {
const blank = Object.fromEntries(Object.keys(slice).map(k => [k, '']))
setRecodeSet(blank)
@ -196,6 +204,7 @@ export default function Forecast() {
: await worker.table([], { name: tableName, index: 'pf_id' })
await viewer.load(worker)
viewer.setAttribute('theme', dark ? 'Pro Dark' : 'Pro Light')
// restore last-used layout or build default
const saved = localStorage.getItem(LAYOUT_KEY(vid))