From 0e014339650d573b402fd3b292fbcff9608cc52f Mon Sep 17 00:00:00 2001 From: Docker Config Backup Date: Mon, 13 Apr 2026 15:22:15 +0200 Subject: [PATCH] feat: light/dark theme toggle (WIP) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- web/src/App.tsx | 17 ++++ web/src/ScheduleTable.tsx | 15 ++- web/src/Toolbar.tsx | 13 +++ web/src/index.css | 197 ++++++++++++++++++++++++++++++++++++++ web/vite.config.ts | 2 +- 5 files changed, 235 insertions(+), 9 deletions(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index f7ac110..e18c604 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -95,6 +95,13 @@ function getISOWeek(date: Date): number { type AppMode = 'login' | 'files' | 'editor' function App() { + // Apply theme from localStorage to 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>(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) diff --git a/web/src/ScheduleTable.tsx b/web/src/ScheduleTable.tsx index 48cacfa..532e098 100644 --- a/web/src/ScheduleTable.tsx +++ b/web/src/ScheduleTable.tsx @@ -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( -
+
) } return cells @@ -808,7 +808,7 @@ export function ScheduleTable(props: ScheduleTableProps) { ) return ( -
+
{/* === Fixed left name column === */} @@ -920,7 +920,7 @@ export function ScheduleTable(props: ScheduleTableProps) { ) } fragments.push( -
+
) fragStart = pmsPos + 1 } @@ -974,18 +974,17 @@ export function ScheduleTable(props: ScheduleTableProps) { > {/* Merged cell overlay covering DEN + DEN T. + all info rows */}
+
) } return cells diff --git a/web/src/Toolbar.tsx b/web/src/Toolbar.tsx index d6b8f95..e2cccc8 100644 --- a/web/src/Toolbar.tsx +++ b/web/src/Toolbar.tsx @@ -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 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({ )} + +
{/* Row toggles */} diff --git a/web/src/index.css b/web/src/index.css index 8bfc1de..0b5ea21 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -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 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; } diff --git a/web/vite.config.ts b/web/vite.config.ts index 8041d3e..680eb30 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -6,7 +6,7 @@ export default defineConfig({ plugins: [react(), tailwindcss()], server: { proxy: { - '/api': 'http://localhost:3080', + '/api': 'http://localhost:3090', }, }, })