Add dark mode theme support across the UI

Introduce light/dark/system theme toggle with Bootstrap data-bs-theme and theme-aware surfaces and components.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
ryang3d
2026-06-04 00:01:03 -07:00
co-authored by Cursor
parent 47626f0c7c
commit 40bd028efe
17 changed files with 422 additions and 42 deletions
+2 -1
View File
@@ -224,7 +224,8 @@
}
/* Call log items */
.bg-light.text-dark.border {
.bg-light.text-dark.border,
.bg-body-secondary.text-body-emphasis.border {
background: white !important;
border: 1px solid #666 !important;
color: black !important;
+12 -10
View File
@@ -13,6 +13,7 @@ import Search from './components/Search'
import Summary from './components/Summary'
import ChangePasswordModal from './components/ChangePasswordModal'
import SettingsModal from './components/SettingsModal'
import ThemeToggle from './components/ThemeToggle'
import './App.css'
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8085/api'
@@ -190,7 +191,7 @@ function App() {
})
return (
<div className="vh-100 d-flex flex-column bg-light">
<div className="vh-100 d-flex flex-column bg-body-tertiary">
{/* Header */}
<header className="bg-primary bg-gradient text-white py-1 px-2 shadow" style={{zIndex: 1030}}>
<div className="d-flex justify-content-between align-items-center">
@@ -201,6 +202,7 @@ function App() {
<h1 className="h5 mb-0 fw-bold">SMS Backup Viewer</h1>
</div>
<div className="d-flex align-items-center gap-2">
<ThemeToggle />
<button
onClick={() => setShowUpload(true)}
className="btn btn-light btn-sm shadow-sm d-flex align-items-center gap-1"
@@ -256,7 +258,7 @@ function App() {
</header>
{/* View Switcher */}
<div className="bg-white border-bottom shadow-sm">
<div className="bg-body-tertiary border-bottom shadow-sm">
<div className="container-fluid">
<ul className="nav nav-tabs border-0">
<li className="nav-item">
@@ -319,7 +321,7 @@ function App() {
</div>
{/* Date Filter */}
<div className="date-filter-container bg-white border-bottom shadow-sm" style={{zIndex: 1025, position: 'relative'}}>
<div className="date-filter-container bg-body-tertiary border-bottom shadow-sm" style={{zIndex: 1025, position: 'relative'}}>
<DateFilter
startDate={startDate}
endDate={endDate}
@@ -336,9 +338,9 @@ function App() {
<>
{/* Conversation List */}
<div
className={`conversation-sidebar bg-white rounded-3 shadow overflow-hidden border ${showSidebar ? 'show' : ''}`}
className={`conversation-sidebar bg-body-tertiary rounded-3 shadow overflow-hidden border ${showSidebar ? 'show' : ''}`}
>
<div className="bg-light border-bottom p-1">
<div className="bg-body-tertiary border-bottom p-1">
<h2 className="h6 mb-1 d-flex align-items-center gap-1 px-1">
<svg style={{width: '1rem', height: '1rem'}} className="text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
@@ -369,7 +371,7 @@ function App() {
</div>
{/* Message Thread */}
<div className="flex-fill bg-white rounded-3 shadow overflow-hidden border message-thread-container" style={{minWidth: 0}}>
<div className="flex-fill bg-body-secondary rounded-3 shadow overflow-hidden border message-thread-container" style={{minWidth: 0}}>
<MessageThread
conversation={selectedConversation}
startDate={startDate}
@@ -380,7 +382,7 @@ function App() {
</>
) : activeView === 'search' ? (
/* Search View */
<div className="flex-fill bg-white rounded-3 shadow overflow-hidden border" style={{minWidth: 0}}>
<div className="flex-fill bg-body-secondary rounded-3 shadow overflow-hidden border" style={{minWidth: 0}}>
<Search
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
@@ -396,7 +398,7 @@ function App() {
</div>
) : activeView === 'calls' ? (
/* Calls View */
<div className="flex-fill bg-white rounded-3 shadow overflow-hidden border" style={{minWidth: 0}}>
<div className="flex-fill bg-body-secondary rounded-3 shadow overflow-hidden border" style={{minWidth: 0}}>
<Calls
startDate={startDate}
endDate={endDate}
@@ -404,7 +406,7 @@ function App() {
</div>
) : activeView === 'summary' ? (
/* Summary View */
<div className="flex-fill bg-white rounded-3 shadow overflow-hidden border" style={{minWidth: 0}}>
<div className="flex-fill bg-body-secondary rounded-3 shadow overflow-hidden border" style={{minWidth: 0}}>
<Summary
startDate={startDate}
endDate={endDate}
@@ -412,7 +414,7 @@ function App() {
</div>
) : (
/* Activity View */
<div className="flex-fill bg-white rounded-3 shadow overflow-hidden border" style={{minWidth: 0}}>
<div className="flex-fill bg-body-secondary rounded-3 shadow overflow-hidden border" style={{minWidth: 0}}>
<Activity
startDate={startDate}
endDate={endDate}
+1 -1
View File
@@ -266,7 +266,7 @@ function Activity({ startDate, endDate }) {
return (
<div className="h-100 d-flex flex-column">
<div className="bg-light border-bottom p-3">
<div className="bg-body-tertiary border-bottom p-3">
<h2 className="h5 mb-0 d-flex align-items-center gap-2">
<svg style={{width: '1.25rem', height: '1.25rem'}} className="text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
+1 -1
View File
@@ -200,7 +200,7 @@ function Calls({ startDate, endDate }) {
return (
<div className="h-100 d-flex flex-column">
<div className="bg-light border-bottom p-3">
<div className="bg-body-tertiary border-bottom p-3">
<h2 className="h5 mb-0 d-flex align-items-center gap-2">
<svg style={{width: '1.25rem', height: '1.25rem'}} className="text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
+1 -1
View File
@@ -133,7 +133,7 @@ function ConversationList({ conversations, selectedConversation, onSelectConvers
style={{cursor: 'pointer'}}
>
<div className="d-flex align-items-start gap-2">
<div className="flex-shrink-0 mt-1 p-2 rounded-circle bg-white shadow-sm">
<div className="flex-shrink-0 mt-1 p-2 rounded-circle bg-body-tertiary shadow-sm">
{getConversationIcon(conv.type)}
</div>
<div className="flex-fill min-w-0" style={{overflow: 'hidden'}}>
+1 -1
View File
@@ -8,7 +8,7 @@ function DateFilter({ startDate, endDate, minDate, maxDate, onStartDateChange, o
}
return (
<div className="px-2 py-1 bg-light">
<div className="px-2 py-1 bg-body-tertiary">
<div className="d-flex align-items-center gap-1 gap-sm-2 small">
<svg style={{width: '1rem', height: '1rem'}} className="text-primary flex-shrink-0 d-none d-sm-block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
+2 -2
View File
@@ -184,7 +184,7 @@ function LazyMedia({ messageId, mediaType, className, alt = "MMS attachment" })
{/* Placeholder shown before loading or while loading */}
{!src && !vcfData && !error && (
<div
className="bg-light rounded d-flex align-items-center justify-content-center position-relative overflow-hidden"
className="bg-body-tertiary rounded d-flex align-items-center justify-content-center position-relative overflow-hidden"
style={{
width: '100%',
aspectRatio: isVideo ? '16/9' : isAudio ? 'auto' : '3/4', // Common phone camera ratio
@@ -307,7 +307,7 @@ function LazyMedia({ messageId, mediaType, className, alt = "MMS attachment" })
<VCardPreview vcfText={vcfData} messageId={messageId} />
)}
{!isImage && !isVideo && !isAudio && !isVCard && (
<div className="small p-2 rounded bg-light d-flex align-items-center gap-1">
<div className="small p-2 rounded bg-body-tertiary d-flex align-items-center gap-1">
<svg style={{width: '1rem', height: '1rem'}} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
</svg>
+5 -1
View File
@@ -1,6 +1,7 @@
import { useState } from 'react'
import { useAuth } from '../contexts/AuthContext'
import { useNavigate } from 'react-router-dom'
import ThemeToggle from './ThemeToggle'
function Login() {
const [isLogin, setIsLogin] = useState(true)
@@ -64,7 +65,10 @@ function Login() {
}
return (
<div className="min-vh-100 d-flex align-items-center justify-content-center bg-light">
<div className="min-vh-100 d-flex align-items-center justify-content-center bg-body-tertiary position-relative">
<div className="position-absolute top-0 end-0 p-3" style={{ zIndex: 10 }}>
<ThemeToggle variant="surface" />
</div>
<div className="container">
<div className="row justify-content-center">
<div className="col-md-6 col-lg-4">
+5
View File
@@ -22,6 +22,11 @@
z-index: 1;
}
/* Dark mode: bring the placeholder tile in line with Bootstrap's body-tertiary value. */
html[data-bs-theme="dark"] .media-grid-item {
background-color: #2c3034;
}
.media-thumbnail {
width: 100%;
height: 100%;
+11 -6
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, useLayoutEffect, useRef } from 'react'
import { useTheme } from '../contexts/ThemeContext'
import { useLocation } from 'react-router-dom'
import axios from 'axios'
import { format } from 'date-fns'
@@ -8,6 +9,8 @@ import MediaGrid from './MediaGrid'
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8085/api'
function MessageThread({ conversation, startDate, endDate, messageLimit }) {
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'
const location = useLocation()
const [items, setItems] = useState([])
const [loading, setLoading] = useState(false)
@@ -509,12 +512,12 @@ function MessageThread({ conversation, startDate, endDate, messageLimit }) {
if (!conversation) {
return (
<div className="d-flex align-items-center justify-content-center h-100 text-muted">
<div className="d-flex align-items-center justify-content-center h-100 text-body-secondary">
<div className="text-center">
<svg style={{width: '5rem', height: '5rem'}} className="mx-auto mb-3 text-secondary opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
<p className="h5 text-dark">Select a conversation</p>
<p className="h5 text-body-emphasis mb-2">Select a conversation</p>
<p className="small mt-2">Choose a conversation from the list to view messages</p>
</div>
</div>
@@ -558,7 +561,7 @@ function MessageThread({ conversation, startDate, endDate, messageLimit }) {
)}
{/* Thread Header */}
<div className="bg-light border-bottom p-2 p-md-4 shadow-sm">
<div className="bg-body-tertiary border-bottom p-2 p-md-4 shadow-sm">
<div className="d-flex align-items-center gap-2 gap-md-3">
<div className="p-2 p-md-3 rounded-circle bg-primary bg-gradient shadow">
{isCallLog ? (
@@ -632,7 +635,7 @@ function MessageThread({ conversation, startDate, endDate, messageLimit }) {
</div>
{/* Content */}
<div ref={scrollContainerRef} className="flex-fill overflow-auto p-2 p-md-4 bg-light">
<div ref={scrollContainerRef} className="flex-fill overflow-auto p-2 p-md-4 bg-body-secondary">
{showMediaOnly && !isCallLog ? (
// Media Grid View
<MediaGrid conversation={conversation} startDate={startDate} endDate={endDate} />
@@ -710,7 +713,7 @@ function MessageThread({ conversation, startDate, endDate, messageLimit }) {
const typeInfo = getCallTypeInfo(call.type)
return (
<div key={`call-${call.id}`} className="d-flex justify-content-center my-1">
<div className="badge bg-light text-dark border px-3 py-2 d-flex align-items-center gap-2" style={{fontSize: '0.75rem'}}>
<div className="badge bg-body-secondary text-body-emphasis border px-3 py-2 d-flex align-items-center gap-2" style={{fontSize: '0.75rem'}}>
<span className={typeInfo.color} style={{fontSize: '1rem'}}>{typeInfo.icon}</span>
<span className={`fw-semibold ${typeInfo.color}`}>{typeInfo.label} call</span>
<span className="text-muted">·</span>
@@ -750,7 +753,9 @@ function MessageThread({ conversation, startDate, endDate, messageLimit }) {
className={`card shadow-sm ${
isSent
? 'bg-primary text-white'
: 'bg-white'
: isDark
? 'bg-body-tertiary text-body'
: 'bg-white'
} ${
isHighlighted
? 'border-warning border-3'
+1 -1
View File
@@ -77,7 +77,7 @@ function Search({ searchQuery, setSearchQuery, results, setResults, loading, set
return (
<div className="h-100 d-flex flex-column">
{/* Header */}
<div className="bg-light border-bottom p-3">
<div className="bg-body-tertiary border-bottom p-3">
<h2 className="h5 mb-3 d-flex align-items-center gap-2">
<svg style={{width: '1.25rem', height: '1.25rem'}} className="text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
+78
View File
@@ -0,0 +1,78 @@
/*
* Summary charts — dark-mode surface + readable axis/tooltip text.
*
* Selectors are intentionally scoped to the Summary page only, using the
* existing `.card` + `.border-*` stat cards and the plain `.card` chart
* cards that are already in the DOM from Summary.jsx. No JSX changes are
* required; this file is imported only by Summary.jsx.
*/
/* Stats cards / chart cards: re-colorize the card body in dark mode. */
html[data-bs-theme="dark"] .summary-view .card {
background-color: var(--bs-body-tertiary);
color: var(--bs-body-color);
}
html[data-bs-theme="dark"] .summary-view .card-body,
html[data-bs-theme="dark"] .summary-view .border-primary .card-body,
html[data-bs-theme="dark"] .summary-view .border-success .card-body,
html[data-bs-theme="dark"] .summary-view .border-info .card-body,
html[data-bs-theme="dark"] .summary-view .border-warning .card-body {
background-color: var(--bs-body-secondary);
}
/* Recharts SVG canvas */
html[data-bs-theme="dark"] .summary-view .recharts-wrapper {
color: var(--bs-body-color);
}
/* Axis lines and ticks */
html[data-bs-theme="dark"] .summary-view .recharts-cartesian-axis-line,
html[data-bs-theme="dark"] .summary-view .recharts-polar-axis-angle-axis line,
html[data-bs-theme="dark"] .summary-view .recharts-polar-axis-radial-axis line {
stroke: var(--bs-border-color);
}
html[data-bs-theme="dark"] .summary-view .recharts-cartesian-axis-tick-value,
html[data-bs-theme="dark"]
.summary-view
.recharts-polar-axis-angle-axis-tick-value,
html[data-bs-theme="dark"]
.summary-view
.recharts-polar-axis-radial-axis-tick-value {
fill: var(--bs-body-color);
}
/* Grid */
html[data-bs-theme="dark"]
.summary-view
.recharts-cartesian-grid-horizontal
line,
html[data-bs-theme="dark"]
.summary-view
.recharts-cartesian-grid-vertical
line {
stroke: var(--bs-border-color);
}
/* Tooltip popover */
html[data-bs-theme="dark"] .summary-view .recharts-tooltip-wrapper {
outline: none;
}
html[data-bs-theme="dark"] .summary-view .recharts-default-tooltip {
background-color: var(--bs-body-tertiary) !important;
border-color: var(--bs-border-color) !important;
color: var(--bs-body-color) !important;
}
/* Legend */
html[data-bs-theme="dark"] .summary-view .recharts-legend-item-text {
color: var(--bs-body-color);
}
/* Label text */
html[data-bs-theme="dark"] .summary-view .recharts-pie-label-text,
html[data-bs-theme="dark"] .summary-view .recharts-bar-label-text {
fill: var(--bs-body-color);
}
+3 -2
View File
@@ -4,6 +4,7 @@ import {
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
PieChart, Pie, Cell, LineChart, Line, Legend
} from 'recharts'
import './Summary.css'
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8085/api'
@@ -114,8 +115,8 @@ function Summary({ startDate, endDate }) {
const truncate = (str, max) => str.length > max ? str.slice(0, max) + '…' : str
return (
<div className="h-100 d-flex flex-column">
<div className="bg-light border-bottom p-3">
<div className="h-100 d-flex flex-column summary-view">
<div className="bg-body-tertiary border-bottom p-3">
<h2 className="h5 mb-0 d-flex align-items-center gap-2">
<svg style={{width: '1.25rem', height: '1.25rem'}} className="text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
+79
View File
@@ -0,0 +1,79 @@
import { useTheme } from '../contexts/ThemeContext'
const THEME_LABELS = {
light: 'Light mode',
dark: 'Dark mode',
system: 'System theme',
}
const NEXT_THEME_LABELS = {
light: 'dark mode',
dark: 'system theme',
system: 'light mode',
}
function ThemeToggle({ variant = 'header' }) {
const { theme, resolvedTheme, toggle } = useTheme()
const variantClass =
variant === 'surface' ? 'theme-toggle-btn-surface' : 'theme-toggle-btn'
const title = `${THEME_LABELS[theme]}${theme === 'system' ? ` (${resolvedTheme})` : ''}. Switch to ${NEXT_THEME_LABELS[theme]}.`
return (
<button
type="button"
onClick={toggle}
className={`btn btn-sm ${variantClass} p-1 d-inline-flex align-items-center justify-content-center`}
title={title}
aria-label={title}
>
{theme === 'dark' ? (
<svg
width="1.1rem"
height="1.1rem"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.25"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z" />
</svg>
) : theme === 'light' ? (
<svg
width="1.1rem"
height="1.1rem"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.25"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="5" />
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
</svg>
) : (
<svg
width="1.1rem"
height="1.1rem"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.25"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
<path d="M8 21h8M12 17v4" />
</svg>
)}
</button>
)
}
export default ThemeToggle
+89
View File
@@ -0,0 +1,89 @@
import { createContext, useContext, useState, useEffect, useCallback } from 'react'
const STORAGE_KEY = 'sbv-theme'
const THEME_ORDER = ['light', 'dark', 'system']
const ThemeContext = createContext(null)
function getSystemTheme() {
if (typeof window !== 'undefined' && window.matchMedia) {
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light'
}
return 'light'
}
function resolveTheme(preference) {
if (preference === 'system') {
return getSystemTheme()
}
return preference
}
function readStoredPreference() {
try {
const saved = localStorage.getItem(STORAGE_KEY)
if (saved === 'dark' || saved === 'light' || saved === 'system') {
return saved
}
} catch {
// localStorage unavailable (e.g. private mode on some browsers)
}
return 'system'
}
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState(readStoredPreference)
const [resolvedTheme, setResolvedTheme] = useState(() =>
resolveTheme(readStoredPreference()),
)
const applyResolvedTheme = useCallback((resolved) => {
setResolvedTheme(resolved)
document.documentElement.setAttribute('data-bs-theme', resolved)
}, [])
useEffect(() => {
applyResolvedTheme(resolveTheme(theme))
}, [theme, applyResolvedTheme])
useEffect(() => {
if (theme !== 'system' || !window.matchMedia) {
return undefined
}
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const onChange = () => applyResolvedTheme(getSystemTheme())
mediaQuery.addEventListener('change', onChange)
return () => mediaQuery.removeEventListener('change', onChange)
}, [theme, applyResolvedTheme])
const toggle = () => {
setTheme((current) => {
const index = THEME_ORDER.indexOf(current)
const next = THEME_ORDER[(index + 1) % THEME_ORDER.length]
try {
localStorage.setItem(STORAGE_KEY, next)
} catch {
// ignore storage errors
}
return next
})
}
return (
<ThemeContext.Provider value={{ theme, resolvedTheme, toggle }}>
{children}
</ThemeContext.Provider>
)
}
export function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider')
}
return context
}
+113
View File
@@ -1,8 +1,121 @@
/* Theme toggle in primary header: high contrast on blue gradient */
.theme-toggle-btn {
color: #fff;
background-color: rgba(255, 255, 255, 0.22);
border: 1px solid rgba(255, 255, 255, 0.5);
border-radius: 0.375rem;
line-height: 1;
}
.theme-toggle-btn:hover,
.theme-toggle-btn:focus-visible {
color: #fff;
background-color: rgba(255, 255, 255, 0.35);
border-color: rgba(255, 255, 255, 0.75);
}
/* Theme toggle on body/card surfaces (e.g. login page) */
.theme-toggle-btn-surface {
color: var(--bs-body-color);
background-color: var(--bs-body-bg);
border: 1px solid var(--bs-border-color);
border-radius: 0.375rem;
line-height: 1;
box-shadow: var(--bs-box-shadow-sm);
}
.theme-toggle-btn-surface:hover,
.theme-toggle-btn-surface:focus-visible {
color: var(--bs-body-color);
background-color: var(--bs-secondary-bg);
border-color: var(--bs-border-color-translucent, var(--bs-border-color));
}
body {
margin: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
}
/* Dark mode: ensure body inherits Bootstrap's dark custom properties. */
html[data-bs-theme="dark"] body {
background-color: #212529;
color: #dee2e6;
}
/* Dark mode: legacy utilities that force light-theme colors */
html[data-bs-theme="dark"] .text-dark {
color: var(--bs-body-emphasis-color) !important;
}
html[data-bs-theme="dark"] .bg-light.text-dark,
html[data-bs-theme="dark"] .badge.bg-light {
background-color: var(--bs-secondary-bg) !important;
color: var(--bs-body-color) !important;
border-color: var(--bs-border-color) !important;
}
/* Dark mode: slightly brighter secondary copy on tertiary surfaces */
html[data-bs-theme="dark"] .text-muted,
html[data-bs-theme="dark"] .text-body-secondary {
color: #adb5bd !important;
}
html[data-bs-theme="dark"] .text-body-emphasis {
color: #f8f9fa !important;
}
/* Dark mode: react-datepicker popup and inputs */
html[data-bs-theme="dark"] .react-datepicker {
background-color: var(--bs-body-bg);
border-color: var(--bs-border-color);
color: var(--bs-body-color);
}
html[data-bs-theme="dark"] .react-datepicker__header {
background-color: var(--bs-secondary-bg);
border-bottom-color: var(--bs-border-color);
}
html[data-bs-theme="dark"] .react-datepicker__current-month,
html[data-bs-theme="dark"] .react-datepicker__day-name,
html[data-bs-theme="dark"] .react-datepicker__day,
html[data-bs-theme="dark"] .react-datepicker__time-name {
color: var(--bs-body-color);
}
html[data-bs-theme="dark"] .react-datepicker__day:hover,
html[data-bs-theme="dark"] .react-datepicker__day--keyboard-selected {
background-color: var(--bs-primary);
color: #fff;
}
html[data-bs-theme="dark"] .react-datepicker__day--outside-month {
color: var(--bs-secondary-color);
}
html[data-bs-theme="dark"] .react-datepicker__navigation-icon::before {
border-color: var(--bs-body-color);
}
/* Dark mode: subdued scrollbar for WebKit/Blink */
html[data-bs-theme="dark"] ::-webkit-scrollbar {
width: 10px;
height: 10px;
}
html[data-bs-theme="dark"] ::-webkit-scrollbar-track {
background: #1a1d20;
}
html[data-bs-theme="dark"] ::-webkit-scrollbar-thumb {
background: #495057;
border-radius: 4px;
}
html[data-bs-theme="dark"] ::-webkit-scrollbar-thumb:hover {
background: #6c757d;
}
@keyframes fadeIn {
from {
opacity: 0;
+18 -15
View File
@@ -5,6 +5,7 @@ import axios from 'axios'
import 'bootstrap/dist/css/bootstrap.min.css'
import './index.css'
import { AuthProvider } from './contexts/AuthContext'
import { ThemeProvider } from './contexts/ThemeContext'
import App from './App.jsx'
import Login from './components/Login.jsx'
import PrintView from './components/PrintView.jsx'
@@ -16,21 +17,23 @@ axios.defaults.withCredentials = true
createRoot(document.getElementById('root')).render(
<StrictMode>
<BrowserRouter>
<AuthProvider>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/conversation/:address/print" element={
<ProtectedRoute>
<PrintView />
</ProtectedRoute>
} />
<Route path="/*" element={
<ProtectedRoute>
<App />
</ProtectedRoute>
} />
</Routes>
</AuthProvider>
<ThemeProvider>
<AuthProvider>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/conversation/:address/print" element={
<ProtectedRoute>
<PrintView />
</ProtectedRoute>
} />
<Route path="/*" element={
<ProtectedRoute>
<App />
</ProtectedRoute>
} />
</Routes>
</AuthProvider>
</ThemeProvider>
</BrowserRouter>
</StrictMode>,
)