diff --git a/README.md b/README.md index a896703..aa2e342 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,11 @@ become a dropdown and the page scrolls, because pinning six panels into a phone viewport produces unreadable eight-pixel type. The same release applies below 700 px tall: a short screen gets a scrollbar rather than a one-pixel chart. -Verified across ten viewports from 1920x1080 to 375x667, six tabs each. +Light and dark themes, defaulting to your system setting, switchable from the +header or the Settings tab and remembered per browser. + +Verified across ten viewports from 1920x1080 to 375x667, six tabs each, in +both themes. | Tab | Answers | | --- | --- | diff --git a/ashvale/dashboard.py b/ashvale/dashboard.py index 817a9bc..7f08852 100644 --- a/ashvale/dashboard.py +++ b/ashvale/dashboard.py @@ -23,6 +23,13 @@ the height goes to the chart rather than the chrome. Below 1024 px wide the tab row becomes a native select, because six labels do not fit a phone header and a clipped tab strip is worse than a dropdown. +Themes. The markup is dark-first Tailwind utilities, so rather than adding a +dark: variant to several hundred class attributes, light is an overlay that +remaps the slate scale and the accent hues under [data-theme="light"]. The +dark path is untouched: nothing is re-specified unless the attribute is set. +Charts cannot follow CSS, so they read the same tokens from the computed style +and are re-rendered on a change. + Layout contract. The page is a fixed three-row grid pinned to the viewport height: header, tab bar, then a content region that takes the remaining space and never overflows the fold. Each tab lays its panels @@ -76,6 +83,128 @@ DASHBOARD_HTML = r""" box-shadow: 0 10px 30px -10px rgba(0,0,0,.5); } .tick { transition: width .5s cubic-bezier(.4,0,.2,1); } + + /* ------------------------------------------------------------------ theme + The markup is written dark-first in Tailwind utilities, so rather than + adding a dark: variant to several hundred class attributes, the light theme + is an overlay that remaps the twenty-eight slate shades actually in use. + One place to reason about, and the dark path stays byte-identical to what + it was: nothing is re-specified unless [data-theme="light"] is set. + + Charts cannot follow CSS, so they read these tokens from the computed style + and are re-rendered on a change. Same source of truth either way. */ + :root { + --ink: #f1f5f9; --grid: rgba(255,255,255,.04); + --axis: #64748b; --axis-strong: #cbd5e1; + --tooltip: rgba(15,23,42,.95); + } + html { color-scheme: dark; } + + html[data-theme="light"] { color-scheme: light; } + html[data-theme="light"] { + --ink: #0f172a; --grid: rgba(15,23,42,.07); + --axis: #64748b; --axis-strong: #334155; + --tooltip: rgba(255,255,255,.97); + } + html[data-theme="light"] body { background:#eef2f7; color:#0f172a; } + html[data-theme="light"] .glass { + background: radial-gradient(130% 130% at 50% 0%, rgba(255,255,255,.95) 0%, rgba(248,250,252,.92) 100%); + border: 1px solid rgba(15,23,42,.10); + box-shadow: 0 8px 24px -12px rgba(15,23,42,.20); + } + /* The decorative glows are tuned for a dark ground and turn to mud on white. */ + html[data-theme="light"] .bg-blobs { opacity:.35; } + + html[data-theme="light"] .text-slate-100 { color:#0f172a; } + html[data-theme="light"] .text-slate-200 { color:#1e293b; } + html[data-theme="light"] .text-slate-300 { color:#334155; } + html[data-theme="light"] .text-slate-400 { color:#475569; } + html[data-theme="light"] .text-slate-500 { color:#64748b; } + html[data-theme="light"] .text-slate-600 { color:#7c8aa0; } + html[data-theme="light"] .text-slate-700 { color:#94a3b8; } + + html[data-theme="light"] .bg-slate-950, + html[data-theme="light"] .bg-slate-950\/90 { background-color:#f8fafc; } + html[data-theme="light"] .bg-slate-950\/70, + html[data-theme="light"] .bg-slate-950\/60 { background-color:rgba(255,255,255,.85); } + html[data-theme="light"] .bg-slate-900, + html[data-theme="light"] .bg-slate-900\/90, + html[data-theme="light"] .bg-slate-900\/80 { background-color:#f1f5f9; } + html[data-theme="light"] .bg-slate-900\/70, + html[data-theme="light"] .bg-slate-900\/60, + html[data-theme="light"] .bg-slate-900\/50, + html[data-theme="light"] .bg-slate-900\/40 { background-color:rgba(15,23,42,.045); } + html[data-theme="light"] .bg-slate-800, + html[data-theme="light"] .bg-slate-800\/60 { background-color:rgba(15,23,42,.08); } + html[data-theme="light"] .bg-slate-500\/15 { background-color:rgba(100,116,139,.14); } + + html[data-theme="light"] .border-slate-700 { border-color:rgba(15,23,42,.20); } + html[data-theme="light"] .border-slate-800, + html[data-theme="light"] .border-slate-800\/80, + html[data-theme="light"] .border-slate-800\/70, + html[data-theme="light"] .border-slate-800\/60, + html[data-theme="light"] .border-slate-800\/40 { border-color:rgba(15,23,42,.11); } + html[data-theme="light"] .border-slate-500\/20 { border-color:rgba(100,116,139,.28); } + + /* The station name is a gradient to transparent, which is invisible on white. */ + html[data-theme="light"] h1.bg-clip-text { + background-image:linear-gradient(to right,#0f172a,#475569) !important; + } + html[data-theme="light"] input, html[data-theme="light"] select { + background-color:#fff; color:#0f172a; border-color:rgba(15,23,42,.18); + } + html[data-theme="light"] .scroller::-webkit-scrollbar-thumb { background:rgba(15,23,42,.22); } + + /* Accents are the 200-400 shades, chosen to glow against black. On white they + wash out: measured against WCAG AA for small text, amber-300 on #f8fafc is + about 1.6:1. Each is remapped to the 600-700 shade of the same hue, which + keeps the colour coding intact and clears 4.5:1. */ + /* Attribute matches, not exact class names. Tailwind's opacity variants are + separate classes (text-amber-200/60), and an exact-name list silently + missed every one of them: measured at 1.02:1 against white. Matching the + hue prefix catches all shades and all opacity suffixes, and dropping the + alpha is a gain here rather than a loss. */ + html[data-theme="light"] [class*="text-amber-"] { color:#b45309; } + html[data-theme="light"] [class*="text-cyan-"] { color:#0e7490; } + html[data-theme="light"] [class*="text-violet-"] { color:#6d28d9; } + html[data-theme="light"] [class*="text-indigo-"] { color:#4338ca; } + html[data-theme="light"] [class*="text-emerald-"] { color:#047857; } + html[data-theme="light"] [class*="text-rose-"] { color:#be123c; } + html[data-theme="light"] [class*="text-blue-"] { color:#1d4ed8; } + html[data-theme="light"] [class*="text-sky-"] { color:#0369a1; } + html[data-theme="light"] .text-white { color:#0f172a; } + /* Tertiary labels are deliberately quiet, but slate-600/700 on white is + illegible rather than quiet. Lifted just enough to read. */ + html[data-theme="light"] .text-slate-600 { color:#64748b; } + html[data-theme="light"] .text-slate-700 { color:#7c8aa0; } + /* The value-changed flash is a pale indigo picked to glow on black. On white + it drops a headline reading to 2.06:1 for half a second, which is exactly + when you are looking at it. */ + html[data-theme="light"] .flash { animation:flash-light .5s ease-out; } + @keyframes flash-light { from { color:#4338ca; } to { color:inherit; } } + + /* Tint backgrounds at 10-30% alpha vanish on white, taking their badges with + them. Lift the alpha and darken the border so a badge still reads as one. */ + html[data-theme="light"] [class*="bg-amber-5"], html[data-theme="light"] [class*="bg-amber-6"] + { background-color:rgba(180,83,9,.13); } + html[data-theme="light"] [class*="bg-emerald-5"], html[data-theme="light"] [class*="bg-emerald-6"] + { background-color:rgba(4,120,87,.13); } + html[data-theme="light"] [class*="bg-indigo-5"], html[data-theme="light"] [class*="bg-indigo-6"] + { background-color:rgba(67,56,202,.13); } + html[data-theme="light"] [class*="bg-cyan-6"] { background-color:rgba(14,116,144,.13); } + html[data-theme="light"] [class*="bg-blue-6"] { background-color:rgba(29,78,216,.13); } + html[data-theme="light"] [class*="bg-rose-5"], html[data-theme="light"] [class*="bg-rose-6"] + { background-color:rgba(190,18,60,.13); } + html[data-theme="light"] [class*="border-amber-5"] { border-color:rgba(180,83,9,.35); } + html[data-theme="light"] [class*="border-emerald-5"] { border-color:rgba(4,120,87,.35); } + html[data-theme="light"] [class*="border-indigo-5"] { border-color:rgba(67,56,202,.35); } + html[data-theme="light"] [class*="border-cyan-5"] { border-color:rgba(14,116,144,.35); } + html[data-theme="light"] [class*="border-blue-5"] { border-color:rgba(29,78,216,.35); } + html[data-theme="light"] [class*="border-rose-5"] { border-color:rgba(190,18,60,.35); } + + /* A little separation between the ground and the panels, or the cards float + on an identical white and the layout loses all structure. */ + html[data-theme="light"] body.bg-slate-950 { background-color:#e8edf4; } .tabbtn { transition: all .18s ease; } .tabbtn[aria-selected="true"] { background: rgba(99,102,241,.16); color:#c7d2fe; border-color: rgba(99,102,241,.4); @@ -157,7 +286,7 @@ DASHBOARD_HTML = r"""
-Stored in this browser, not on the station: a theme belongs to the screen you read on. Auto tracks the system setting live.
+