diff --git a/dashboard/src/App.css b/dashboard/src/App.css index 64e9a774..5674a15f 100644 --- a/dashboard/src/App.css +++ b/dashboard/src/App.css @@ -3,6 +3,7 @@ :root { --primary: #25d366; --primary-hover: #1da851; + --primary-soft: rgba(37, 211, 102, 0.12); --bg-light: #f8fafc; --bg-white: #ffffff; --bg-card: #ffffff; @@ -19,6 +20,42 @@ --radius: 10px; } +[data-palette='blue'] { + --primary: #2563eb; + --primary-hover: #1d4ed8; + --primary-soft: rgba(37, 99, 235, 0.12); +} + +[data-palette='graphite'] { + --primary: #64748b; + --primary-hover: #475569; + --primary-soft: rgba(100, 116, 139, 0.14); +} + +[data-palette='indigo'] { + --primary: #4f46e5; + --primary-hover: #4338ca; + --primary-soft: rgba(79, 70, 229, 0.12); +} + +[data-palette='amber'] { + --primary: #d97706; + --primary-hover: #b45309; + --primary-soft: rgba(217, 119, 6, 0.13); +} + +[data-palette='rose'] { + --primary: #e11d48; + --primary-hover: #be123c; + --primary-soft: rgba(225, 29, 72, 0.12); +} + +[data-palette='teal'] { + --primary: #0d9488; + --primary-hover: #0f766e; + --primary-soft: rgba(13, 148, 136, 0.12); +} + /* Dark Mode */ [data-theme='dark'] { /* Make native controls (select option popups, scrollbars, date pickers) follow the EXPLICIT @@ -32,6 +69,7 @@ --text-secondary: #cbd5e1; /* Slate 300 */ --text-muted: #64748b; /* Slate 500 */ --border: #334155; /* Slate 700 */ + --primary-soft: color-mix(in srgb, var(--primary) 18%, transparent); --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3); --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.3); --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.4), 0 4px 6px -4px rgb(0 0 0 / 0.4); @@ -56,6 +94,7 @@ --text-secondary: #cbd5e1; --text-muted: #64748b; --border: #334155; + --primary-soft: color-mix(in srgb, var(--primary) 18%, transparent); --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3); --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.3); --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.4), 0 4px 6px -4px rgb(0 0 0 / 0.4); diff --git a/dashboard/src/components/Layout.css b/dashboard/src/components/Layout.css index b54d046d..e145cbae 100644 --- a/dashboard/src/components/Layout.css +++ b/dashboard/src/components/Layout.css @@ -147,7 +147,7 @@ } .nav-item.active { - background: rgba(37, 211, 102, 0.1); + background: var(--primary-soft); color: var(--primary); } @@ -184,6 +184,28 @@ overflow: hidden; } +.appearance-button-cue { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + flex-shrink: 0; + border-radius: 999px; + background: + radial-gradient(circle at 70% 25%, rgba(255, 255, 255, 0.8), transparent 24%), + var(--swatch-color); + color: white; + box-shadow: + 0 0 0 2px var(--bg-white), + 0 0 0 3px color-mix(in srgb, var(--swatch-color) 35%, transparent); +} + +.appearance-button-cue svg { + filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.25)); +} + .sidebar.collapsed .theme-toggle-btn, .sidebar.collapsed .logout-btn { justify-content: center; @@ -195,11 +217,13 @@ color: var(--text-primary); } -.language-menu { +.language-menu, +.appearance-menu { position: relative; } -.language-menu .theme-toggle-btn { +.language-menu .theme-toggle-btn, +.appearance-menu .theme-toggle-btn { width: 100%; } @@ -243,7 +267,7 @@ } .language-menu-item.active { - background: rgba(37, 211, 102, 0.1); + background: var(--primary-soft); color: var(--primary); } @@ -253,6 +277,160 @@ bottom: 0; } +.appearance-menu-list { + position: absolute; + left: 0; + right: 0; + bottom: calc(100% + 0.35rem); + display: flex; + flex-direction: column; + gap: 0.85rem; + min-width: 254px; + padding: 0.85rem; + background: var(--bg-white); + border: 1px solid var(--border); + border-radius: 12px; + box-shadow: 0 18px 45px rgba(15, 23, 42, 0.18); + z-index: 120; +} + +.appearance-menu-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + padding: 0.2rem 0.1rem 0.75rem; + border-bottom: 1px solid var(--border); +} + +.appearance-menu-header strong { + display: block; + color: var(--text-primary); + font-size: 0.9375rem; + line-height: 1.2; +} + +.appearance-menu-header span { + display: block; + margin-top: 0.15rem; + color: var(--text-muted); + font-size: 0.75rem; +} + +.appearance-current-swatch { + width: 34px; + height: 34px; + flex-shrink: 0; + border-radius: 999px; + background: + linear-gradient(135deg, rgba(255, 255, 255, 0.65), transparent 42%), + var(--swatch-color); + box-shadow: + inset 0 0 0 1px rgba(255, 255, 255, 0.35), + 0 0 0 4px color-mix(in srgb, var(--swatch-color) 13%, transparent); +} + +.appearance-section { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.appearance-section-label { + font-size: 0.6875rem; + font-weight: 800; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.appearance-mode-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.35rem; +} + +.appearance-mode { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.25rem; + min-height: 58px; + padding: 0.45rem; + background: var(--bg-light); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text-secondary); + font-size: 0.75rem; +} + +.appearance-mode:hover { + color: var(--text-primary); + border-color: var(--primary); + background: var(--bg-white); +} + +.appearance-mode.active { + background: var(--primary-soft); + border-color: var(--primary); + color: var(--primary); + box-shadow: inset 0 -2px 0 var(--primary); +} + +.palette-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 0.45rem; +} + +.palette-swatch { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + padding: 0; + border: 1px solid var(--border); + border-radius: 999px; + background: var(--bg-light); +} + +.palette-swatch span { + width: 20px; + height: 20px; + border-radius: 999px; + background: + linear-gradient(135deg, rgba(255, 255, 255, 0.6), transparent 45%), + var(--swatch-color); + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.35); +} + +.palette-swatch:hover, +.palette-swatch.active { + border-color: var(--swatch-color); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--swatch-color) 18%, transparent); +} + +.palette-swatch.active::after { + position: absolute; + right: -1px; + bottom: -1px; + width: 10px; + height: 10px; + border: 2px solid var(--bg-white); + border-radius: 999px; + background: var(--primary); + content: ''; +} + +.sidebar.collapsed .appearance-menu-list { + right: auto; + left: calc(100% + 0.5rem); + bottom: 0; +} + .logout-btn { display: flex; align-items: center; @@ -419,6 +597,11 @@ left: auto; } +[dir="rtl"] .sidebar.collapsed .appearance-menu-list { + right: calc(100% + 0.5rem); + left: auto; +} + [dir="rtl"] .main-content { margin-left: 0; margin-right: 260px; diff --git a/dashboard/src/components/Layout.tsx b/dashboard/src/components/Layout.tsx index 458ab393..96afc5ac 100644 --- a/dashboard/src/components/Layout.tsx +++ b/dashboard/src/components/Layout.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, type CSSProperties } from 'react'; import { NavLink, Outlet } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { @@ -50,9 +50,10 @@ const themeIcons = { light: Sun, dark: Moon, system: Monitor }; export function Layout({ onLogout, userRole }: LayoutProps) { const { t, i18n } = useTranslation(); - const { theme, toggleTheme } = useTheme(); + const { theme, setTheme, palette, setPalette, paletteOptions } = useTheme(); const ThemeIcon = themeIcons[theme]; const themeLabel = t(`theme.${theme}`); + const activePalette = paletteOptions.find(option => option.value === palette) ?? paletteOptions[0]; const navItems = allNavItems.filter(item => !item.adminOnly || userRole === 'admin'); @@ -60,7 +61,9 @@ export function Layout({ onLogout, userRole }: LayoutProps) { const [isMobileOpen, setIsMobileOpen] = useState(false); const [isMobile, setIsMobile] = useState(window.innerWidth < 768); const [isLanguageMenuOpen, setIsLanguageMenuOpen] = useState(false); + const [isAppearanceMenuOpen, setIsAppearanceMenuOpen] = useState(false); const languageMenuRef = useRef(null); + const appearanceMenuRef = useRef(null); useEffect(() => { const handleResize = () => { @@ -103,6 +106,26 @@ export function Layout({ onLogout, userRole }: LayoutProps) { }; }, [isLanguageMenuOpen]); + useEffect(() => { + if (!isAppearanceMenuOpen) return; + + const closeOnOutsideClick = (event: MouseEvent) => { + if (!appearanceMenuRef.current?.contains(event.target as Node)) { + setIsAppearanceMenuOpen(false); + } + }; + const closeOnEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') setIsAppearanceMenuOpen(false); + }; + + document.addEventListener('mousedown', closeOnOutsideClick); + document.addEventListener('keydown', closeOnEscape); + return () => { + document.removeEventListener('mousedown', closeOnOutsideClick); + document.removeEventListener('keydown', closeOnEscape); + }; + }, [isAppearanceMenuOpen]); + const toggleCollapse = () => setIsCollapsed(!isCollapsed); const toggleMobile = () => setIsMobileOpen(!isMobileOpen); @@ -205,14 +228,80 @@ export function Layout({ onLogout, userRole }: LayoutProps) { )} - +
+ + {isAppearanceMenuOpen && ( +
+
+
+ {t('theme.appearance')} + {activePalette.label} +
+
+
+ {t('theme.mode')} +
+ {(['light', 'dark', 'system'] as const).map(mode => { + const ModeIcon = themeIcons[mode]; + return ( + + ); + })} +
+
+
+ {t('theme.palette')} +
+ {paletteOptions.map(option => ( + + ))} +
+
+
+ )} +
+ + +
+ + setSearchTerm(event.target.value)} + placeholder={t('common.search')} + /> +
+ + {loadingTemplates ? ( +
+ +
+ ) : templates.length === 0 ? ( +
+ +

{t('templates.empty.title')}

+

{t('templates.empty.description')}

+
+ ) : filteredTemplates.length === 0 ? ( +
+ +

{t('templates.empty.title')}

+
+ ) : ( +
+ {filteredTemplates.map(template => { + const templatePlaceholders = extractPlaceholders(template); + const isSelected = editingTemplate?.id === template.id; + return ( + + ); + })} +
+ )} + +

{editingTemplate ? t('templates.editTitle') : t('templates.createTitle')}

{selectedSession ? t('templates.sessionHint', { name: selectedSession.name }) : ''}

- {editingTemplate && ( - - )} +
+ {editingTemplate && ( + + )} + {editingTemplate && canWrite && ( + + )} +
@@ -237,45 +326,48 @@ export function Templates() { />
-
- - setForm({ ...form, header: event.target.value })} - placeholder={t('templates.headerPlaceholder')} - disabled={!canWrite} - /> -
+
+
+ + setForm({ ...form, header: event.target.value })} + placeholder={t('templates.headerPlaceholder')} + disabled={!canWrite} + /> +
-
- -