feat: light/dark theme toggle (WIP)
CSS-override approach: light-mode rules in index.css target Tailwind utility classes when body.light is present — no component rewrites. Theme persists in localStorage. Toggle button next to FPD in toolbar. Custom classes for elements that needed mode-specific styling: - pms-cell (Práce mimo směnu cells) - pms-label (rotated PMS column header) - pms-week (week-row PMS cell, merges visually in light mode) - schedule-wrapper (decouples table container from weekend cell tint) Also: vite.config.ts proxy 3080 → 3090 to point at the TKB server. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -95,6 +95,13 @@ function getISOWeek(date: Date): number {
|
||||
type AppMode = 'login' | 'files' | 'editor'
|
||||
|
||||
function App() {
|
||||
// Apply theme from localStorage to <body> on every render of the app.
|
||||
// Theme state lives inside ScheduleApp; this just applies the persisted value.
|
||||
useEffect(() => {
|
||||
const t = (localStorage.getItem('tkb-theme') as 'dark' | 'light') || 'dark'
|
||||
document.body.classList.toggle('light', t === 'light')
|
||||
}, [])
|
||||
|
||||
// Auto-reload when server restarts with a new version (new deploy)
|
||||
useEffect(() => {
|
||||
let serverVersion: string | null = null
|
||||
@@ -477,6 +484,14 @@ function ScheduleApp({ fileId, fileName, data, compareFileId, onBack, onFileName
|
||||
const [showD8, setShowD8] = useState(true)
|
||||
const [showSazlt, setShowSazlt] = useState(true)
|
||||
const [hide162, setHide162] = useState(false)
|
||||
const [theme, setTheme] = useState<'dark' | 'light'>(() =>
|
||||
(localStorage.getItem('tkb-theme') as 'dark' | 'light') || 'dark'
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
document.body.classList.toggle('light', theme === 'light')
|
||||
localStorage.setItem('tkb-theme', theme)
|
||||
}, [theme])
|
||||
const [hiddenValues, setHiddenValues] = useState<Set<string>>(new Set())
|
||||
|
||||
const handleExportPdf = useCallback(async (month: number) => {
|
||||
@@ -581,6 +596,8 @@ function ScheduleApp({ fileId, fileName, data, compareFileId, onBack, onFileName
|
||||
onToggleSazlt={() => setShowSazlt(v => !v)}
|
||||
hide162={hide162}
|
||||
onToggle162={() => setHide162(v => !v)}
|
||||
theme={theme}
|
||||
onToggleTheme={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}
|
||||
hiddenValues={hiddenValues}
|
||||
onToggleValue={(code: string) => setHiddenValues(prev => {
|
||||
const next = new Set(prev)
|
||||
|
||||
@@ -260,7 +260,7 @@ const PersonRow = memo(function PersonRow({
|
||||
key={`pms-${pmsMonth}`}
|
||||
className={`flex items-center justify-center text-sm font-mono font-bold relative
|
||||
border-r border-slate-600 cursor-text hover:brightness-125
|
||||
${!pmsColor ? 'bg-slate-700/30' : ''}`}
|
||||
${!pmsColor ? 'pms-cell' : ''}`}
|
||||
style={pmsStyle}
|
||||
onDoubleClick={() => onStartEdit(person.id, pmsDayIdx, pmsValue)}
|
||||
onClick={() => {
|
||||
@@ -787,7 +787,7 @@ export function ScheduleTable(props: ScheduleTableProps) {
|
||||
)
|
||||
if (pmsAfterSet.has(i)) {
|
||||
cells.push(
|
||||
<div key={`pms-info-${rowId}-${i}`} className="bg-slate-700/30" style={{ width: CELL_W, height: 28 }} />
|
||||
<div key={`pms-info-${rowId}-${i}`} className="pms-cell" style={{ width: CELL_W, height: 28 }} />
|
||||
)
|
||||
}
|
||||
return cells
|
||||
@@ -808,7 +808,7 @@ export function ScheduleTable(props: ScheduleTableProps) {
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-700 bg-slate-800/50 overflow-hidden">
|
||||
<div className="rounded-lg border border-slate-700 bg-slate-800/50 schedule-wrapper overflow-hidden">
|
||||
<div className="flex">
|
||||
|
||||
{/* === Fixed left name column === */}
|
||||
@@ -920,7 +920,7 @@ export function ScheduleTable(props: ScheduleTableProps) {
|
||||
)
|
||||
}
|
||||
fragments.push(
|
||||
<div key={`w-${i}-pms-${pmsPos}`} className="bg-slate-700/30" style={{ width: CELL_W, height: 24 }} />
|
||||
<div key={`w-${i}-pms-${pmsPos}`} className="pms-week" style={{ width: CELL_W, height: 24 }} />
|
||||
)
|
||||
fragStart = pmsPos + 1
|
||||
}
|
||||
@@ -974,18 +974,17 @@ export function ScheduleTable(props: ScheduleTableProps) {
|
||||
>
|
||||
{/* Merged cell overlay covering DEN + DEN T. + all info rows */}
|
||||
<div
|
||||
className="absolute flex items-center justify-center border-l border-r border-slate-600 overflow-hidden"
|
||||
className="absolute flex items-center justify-center border-l border-r border-slate-600 overflow-hidden pms-label"
|
||||
style={{
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: CELL_W,
|
||||
height: 28 + 24 + 28 + (showSazlt ? 28 : 0) + (showMetro ? 28 : 0) + (showD8 ? 28 : 0),
|
||||
backgroundColor: '#283040',
|
||||
zIndex: 15,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="text-[10px] font-bold text-slate-300 pointer-events-none select-none"
|
||||
className="text-[10px] font-bold pms-label-text pointer-events-none select-none"
|
||||
style={{
|
||||
writingMode: 'vertical-rl',
|
||||
transform: 'rotate(180deg)',
|
||||
@@ -1021,7 +1020,7 @@ export function ScheduleTable(props: ScheduleTableProps) {
|
||||
)
|
||||
if (pmsAfterSet.has(i)) {
|
||||
cells.push(
|
||||
<div key={`pms-dayname-${i}`} className="bg-slate-700/30" style={{ width: CELL_W, height: 24 }} />
|
||||
<div key={`pms-dayname-${i}`} className="pms-cell" style={{ width: CELL_W, height: 24 }} />
|
||||
)
|
||||
}
|
||||
return cells
|
||||
|
||||
@@ -27,10 +27,12 @@ interface ToolbarProps {
|
||||
showD8: boolean
|
||||
showSazlt: boolean
|
||||
hide162: boolean
|
||||
theme: 'dark' | 'light'
|
||||
onToggleMetro: () => void
|
||||
onToggleD8: () => void
|
||||
onToggleSazlt: () => void
|
||||
onToggle162: () => void
|
||||
onToggleTheme: () => void
|
||||
hiddenValues: Set<string>
|
||||
onToggleValue: (code: string) => void
|
||||
diffFileName?: string | null
|
||||
@@ -53,10 +55,12 @@ export function Toolbar({
|
||||
showD8,
|
||||
showSazlt,
|
||||
hide162,
|
||||
theme,
|
||||
onToggleMetro,
|
||||
onToggleD8,
|
||||
onToggleSazlt,
|
||||
onToggle162,
|
||||
onToggleTheme,
|
||||
hiddenValues,
|
||||
onToggleValue,
|
||||
diffFileName,
|
||||
@@ -179,6 +183,15 @@ export function Toolbar({
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={onToggleTheme}
|
||||
title={theme === 'dark' ? 'Přepnout na světlý režim' : 'Přepnout na tmavý režim'}
|
||||
className="px-3 py-1 rounded text-xs bg-slate-800 text-slate-300 border border-slate-700
|
||||
hover:bg-slate-700 cursor-pointer transition-colors"
|
||||
>
|
||||
{theme === 'dark' ? '☀ Světlý' : '🌙 Tmavý'}
|
||||
</button>
|
||||
|
||||
<div className="h-5 w-px bg-slate-700" />
|
||||
|
||||
{/* Row toggles */}
|
||||
|
||||
@@ -7,6 +7,11 @@ body {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
body.light {
|
||||
background: #f8fafc;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
#root {
|
||||
min-height: 100svh;
|
||||
}
|
||||
@@ -25,3 +30,195 @@ body {
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #64748b;
|
||||
}
|
||||
|
||||
body.light ::-webkit-scrollbar-track {
|
||||
background: #e2e8f0;
|
||||
}
|
||||
body.light ::-webkit-scrollbar-thumb {
|
||||
background: #94a3b8;
|
||||
}
|
||||
body.light ::-webkit-scrollbar-thumb:hover {
|
||||
background: #64748b;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
LIGHT THEME OVERRIDES
|
||||
Targets Tailwind utility classes used throughout the app.
|
||||
The .light class is toggled on <body> by App.tsx.
|
||||
============================================================ */
|
||||
|
||||
/* Backgrounds: solid slate */
|
||||
.light .bg-slate-900 { background-color: #f8fafc !important; }
|
||||
.light .bg-slate-900\/80 { background-color: rgba(248, 250, 252, 0.85) !important; }
|
||||
.light .bg-slate-900\/95 { background-color: rgba(248, 250, 252, 0.95) !important; }
|
||||
.light .bg-slate-800 { background-color: #ffffff !important; }
|
||||
.light .bg-slate-800\/80 { background-color: rgba(255, 255, 255, 0.85) !important; }
|
||||
.light .bg-slate-800\/60 { background-color: rgba(241, 245, 249, 0.6) !important; }
|
||||
/* Weekday empty cells in info rows — plain white */
|
||||
.light .bg-slate-800\/30 { background-color: #ffffff !important; }
|
||||
/* Weekend/holiday empty cells in person rows — distinct tint */
|
||||
.light .bg-slate-800\/50 { background-color: #dbeafe !important; }
|
||||
.light .bg-slate-750 { background-color: #f1f5f9 !important; }
|
||||
.light .bg-slate-700 { background-color: #e2e8f0 !important; }
|
||||
.light .bg-slate-700\/60 { background-color: rgba(226, 232, 240, 0.6) !important; }
|
||||
.light .bg-slate-700\/50 { background-color: rgba(226, 232, 240, 0.5) !important; }
|
||||
.light .bg-slate-700\/40 { background-color: rgba(226, 232, 240, 0.4) !important; }
|
||||
/* Weekend/holiday cells in info rows — same blue tint as person-row weekends */
|
||||
.light .bg-slate-700\/30 { background-color: #dbeafe !important; }
|
||||
|
||||
/* PMS column (Práce mimo směnu) — month-boundary marker.
|
||||
Dark mode: matches original bg-slate-700/30. Light mode: pale yellow. */
|
||||
.pms-cell { background-color: rgba(51, 65, 85, 0.3); }
|
||||
.light .pms-cell { background-color: #fde68a !important; }
|
||||
|
||||
/* PMS vertical label (rotated "Práce mimo směnu" text overlay) */
|
||||
.pms-label { background-color: #283040; }
|
||||
.light .pms-label { background-color: #fde68a !important; }
|
||||
.pms-label-text { color: #cbd5e1; }
|
||||
.light .pms-label-text { color: #78350f !important; }
|
||||
|
||||
/* PMS cell in the WEEK header row.
|
||||
Dark mode: subtle (matches original bg-slate-700/30). Light mode: pale yellow
|
||||
merged with the label below. */
|
||||
.pms-week { background-color: rgba(51, 65, 85, 0.3); }
|
||||
.light .pms-week {
|
||||
background-color: #fde68a !important;
|
||||
box-shadow: inset 1px 0 0 #94a3b8, inset -1px 0 0 #94a3b8;
|
||||
}
|
||||
|
||||
/* Hover effect: brightness(1.25) washes out pale colors in light mode.
|
||||
Use a subtle dim instead. */
|
||||
.light .hover\:brightness-125:hover { filter: brightness(0.94) !important; }
|
||||
|
||||
/* Schedule outer wrapper — keep dark mode identical, force white in light mode
|
||||
(separate from bg-slate-800/50 used as the weekend cell tint). */
|
||||
.light .schedule-wrapper { background-color: #ffffff !important; }
|
||||
.light .bg-slate-600 { background-color: #cbd5e1 !important; }
|
||||
|
||||
/* Hover backgrounds */
|
||||
.light .hover\:bg-slate-700:hover { background-color: #e2e8f0 !important; }
|
||||
.light .hover\:bg-slate-700\/70:hover { background-color: rgba(226, 232, 240, 0.7) !important; }
|
||||
.light .hover\:bg-slate-750:hover { background-color: #f1f5f9 !important; }
|
||||
.light .hover\:bg-slate-800:hover { background-color: #f1f5f9 !important; }
|
||||
|
||||
/* Text colors */
|
||||
.light .text-slate-100 { color: #0f172a !important; }
|
||||
.light .text-slate-200 { color: #1e293b !important; }
|
||||
.light .text-slate-300 { color: #334155 !important; }
|
||||
.light .text-slate-400 { color: #475569 !important; }
|
||||
.light .text-slate-500 { color: #64748b !important; }
|
||||
.light .text-slate-600 { color: #94a3b8 !important; }
|
||||
|
||||
/* Hover text */
|
||||
.light .hover\:text-slate-100:hover { color: #0f172a !important; }
|
||||
.light .hover\:text-slate-200:hover { color: #1e293b !important; }
|
||||
.light .hover\:text-slate-300:hover { color: #334155 !important; }
|
||||
|
||||
/* Borders */
|
||||
.light .border-slate-900 { border-color: #e2e8f0 !important; }
|
||||
.light .border-slate-800 { border-color: #e2e8f0 !important; }
|
||||
.light .border-slate-700 { border-color: #cbd5e1 !important; }
|
||||
.light .border-slate-700\/50 { border-color: rgba(203, 213, 225, 0.7) !important; }
|
||||
.light .border-slate-600 { border-color: #94a3b8 !important; }
|
||||
.light .border-slate-600\/50 { border-color: rgba(148, 163, 184, 0.7) !important; }
|
||||
.light .border-slate-500 { border-color: #64748b !important; }
|
||||
.light .border-b-slate-700 { border-bottom-color: #cbd5e1 !important; }
|
||||
.light .divide-slate-700 > * + * { border-color: #cbd5e1 !important; }
|
||||
|
||||
/* Tinted accent backgrounds — make them paper-like */
|
||||
.light .bg-blue-900\/50 { background-color: #dbeafe !important; }
|
||||
.light .bg-blue-900\/60 { background-color: #dbeafe !important; }
|
||||
.light .bg-blue-800\/60 { background-color: #bfdbfe !important; }
|
||||
.light .bg-blue-700\/70 { background-color: #93c5fd !important; }
|
||||
.light .text-blue-100 { color: #1e3a8a !important; }
|
||||
.light .text-blue-200 { color: #1e40af !important; }
|
||||
.light .text-blue-300 { color: #1e40af !important; }
|
||||
.light .text-blue-400 { color: #2563eb !important; }
|
||||
.light .border-blue-800 { border-color: #93c5fd !important; }
|
||||
.light .border-blue-700 { border-color: #93c5fd !important; }
|
||||
.light .border-blue-600 { border-color: #60a5fa !important; }
|
||||
.light .hover\:bg-blue-600\/70:hover { background-color: #60a5fa !important; }
|
||||
.light .hover\:bg-blue-700:hover { background-color: #2563eb !important; color: #fff !important; }
|
||||
|
||||
.light .bg-green-900\/30 { background-color: #dcfce7 !important; }
|
||||
.light .bg-green-900\/20 { background-color: #dcfce7 !important; }
|
||||
.light .bg-green-900\/50 { background-color: #bbf7d0 !important; }
|
||||
.light .bg-green-800\/60 { background-color: #bbf7d0 !important; }
|
||||
.light .bg-green-800\/70 { background-color: #86efac !important; }
|
||||
.light .bg-green-700\/70 { background-color: #4ade80 !important; }
|
||||
.light .text-green-100 { color: #14532d !important; }
|
||||
.light .text-green-200 { color: #166534 !important; }
|
||||
.light .text-green-300 { color: #15803d !important; }
|
||||
.light .text-green-400 { color: #16a34a !important; }
|
||||
.light .border-green-700 { border-color: #4ade80 !important; }
|
||||
.light .border-green-600 { border-color: #22c55e !important; }
|
||||
.light .border-green-700\/30 { border-color: rgba(74, 222, 128, 0.5) !important; }
|
||||
.light .border-green-700\/50 { border-color: rgba(74, 222, 128, 0.7) !important; }
|
||||
.light .hover\:bg-green-600\/70:hover { background-color: #22c55e !important; color: #fff !important; }
|
||||
.light .hover\:bg-green-700\/70:hover { background-color: #16a34a !important; color: #fff !important; }
|
||||
|
||||
.light .bg-orange-800\/60 { background-color: #fed7aa !important; }
|
||||
.light .text-orange-200 { color: #9a3412 !important; }
|
||||
.light .border-orange-600 { border-color: #fb923c !important; }
|
||||
|
||||
.light .bg-purple-800\/60 { background-color: #e9d5ff !important; }
|
||||
.light .bg-purple-700\/70 { background-color: #c4b5fd !important; }
|
||||
.light .text-purple-100 { color: #581c87 !important; }
|
||||
.light .text-purple-200 { color: #6b21a8 !important; }
|
||||
.light .border-purple-600 { border-color: #a855f7 !important; }
|
||||
.light .hover\:bg-purple-600\/70:hover { background-color: #a855f7 !important; color: #fff !important; }
|
||||
|
||||
.light .bg-amber-900\/10 { background-color: #fef3c7 !important; }
|
||||
.light .bg-amber-700\/40 { background-color: #fde68a !important; }
|
||||
.light .bg-amber-700\/70 { background-color: #fbbf24 !important; }
|
||||
.light .text-amber-100 { color: #78350f !important; }
|
||||
.light .text-amber-300 { color: #b45309 !important; }
|
||||
.light .text-amber-400 { color: #d97706 !important; }
|
||||
.light .border-amber-600 { border-color: #f59e0b !important; }
|
||||
.light .border-amber-700\/50 { border-color: rgba(217, 119, 6, 0.5) !important; }
|
||||
.light .hover\:bg-amber-600\/70:hover { background-color: #f59e0b !important; color: #fff !important; }
|
||||
.light .hover\:text-amber-400:hover { color: #d97706 !important; }
|
||||
|
||||
.light .bg-cyan-700\/70 { background-color: #67e8f9 !important; }
|
||||
.light .text-cyan-100 { color: #164e63 !important; }
|
||||
.light .border-cyan-600 { border-color: #06b6d4 !important; }
|
||||
.light .hover\:bg-cyan-600\/70:hover { background-color: #06b6d4 !important; color: #fff !important; }
|
||||
|
||||
.light .bg-red-900\/30 { background-color: #fee2e2 !important; }
|
||||
.light .bg-red-900\/50 { background-color: #fecaca !important; }
|
||||
.light .bg-red-800\/50 { background-color: #fca5a5 !important; }
|
||||
.light .text-red-100 { color: #7f1d1d !important; }
|
||||
.light .text-red-300 { color: #b91c1c !important; }
|
||||
.light .text-red-400 { color: #dc2626 !important; }
|
||||
.light .border-red-700 { border-color: #f87171 !important; }
|
||||
.light .border-red-700\/50 { border-color: rgba(248, 113, 113, 0.7) !important; }
|
||||
.light .border-red-800 { border-color: #ef4444 !important; }
|
||||
.light .hover\:bg-red-800\/50:hover { background-color: #fca5a5 !important; }
|
||||
|
||||
.light .bg-indigo-700\/60 { background-color: #c7d2fe !important; }
|
||||
.light .text-indigo-100 { color: #312e81 !important; }
|
||||
.light .text-indigo-200 { color: #3730a3 !important; }
|
||||
|
||||
.light .bg-rose-800\/60 { background-color: #fecdd3 !important; }
|
||||
.light .text-rose-200 { color: #9f1239 !important; }
|
||||
.light .border-rose-600 { border-color: #f43f5e !important; }
|
||||
|
||||
/* Modal black overlays — soften */
|
||||
.light .bg-black\/50 { background-color: rgba(15, 23, 42, 0.35) !important; }
|
||||
|
||||
/* ------- Info row default backgrounds (TKB/SAZLT/Metro/D8 with closure value) ------- */
|
||||
.light .bg-orange-700\/40 { background-color: #fed7aa !important; }
|
||||
.light .bg-amber-700\/40 { background-color: #fde68a !important; }
|
||||
.light .bg-blue-700\/40 { background-color: #bfdbfe !important; }
|
||||
.light .bg-green-700\/40 { background-color: #bbf7d0 !important; }
|
||||
.light .text-amber-200 { color: #92400e !important; }
|
||||
|
||||
/* ------- Section header bars (Pohotovost TKB / IT) ------- */
|
||||
.light .bg-indigo-700\/60 { background-color: #c7d2fe !important; }
|
||||
.light .bg-teal-700\/60 { background-color: #99f6e4 !important; }
|
||||
.light .text-teal-100 { color: #134e4a !important; }
|
||||
.light .text-teal-200 { color: #115e59 !important; }
|
||||
|
||||
/* ------- Borders inside the schedule grid (slate-700/20 etc.) ------- */
|
||||
.light .border-slate-700\/20 { border-color: rgba(148, 163, 184, 0.4) !important; }
|
||||
.light .border-l-slate-500 { border-left-color: #64748b !important; }
|
||||
|
||||
@@ -6,7 +6,7 @@ export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': 'http://localhost:3080',
|
||||
'/api': 'http://localhost:3090',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user