From 02c14e3fbd49582cb97c10f0d1fa1cd009d94795 Mon Sep 17 00:00:00 2001 From: matteoscrugli Date: Tue, 23 Dec 2025 02:05:49 +0100 Subject: [PATCH] Add tab bar position setting with edge swipe sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add theme_tab_bar_position setting (top/bottom/responsive) - Tab bar is now fixed at top, stays visible during scroll - Bottom position uses fixed positioning with safe-area-inset - Add edge swipe gesture to open sidebar on mobile - Remove backdrop-filter for better scroll performance - Simplify TabsScroller by removing inline style manipulation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- backend/app/core/settings_registry.py | 11 ++ frontend/src/components/TabsScroller.tsx | 16 +- frontend/src/contexts/SidebarContext.tsx | 72 +++++++- frontend/src/contexts/ThemeContext.tsx | 24 ++- frontend/src/locales/en.json | 9 + frontend/src/locales/it.json | 9 + frontend/src/pages/admin/ThemeSettings.tsx | 49 +++++- frontend/src/styles/Layout.css | 186 ++++++++++++++++++--- frontend/src/styles/ThemeSettings.css | 78 +++++++++ 9 files changed, 428 insertions(+), 26 deletions(-) diff --git a/backend/app/core/settings_registry.py b/backend/app/core/settings_registry.py index 018ecfa..1964daa 100644 --- a/backend/app/core/settings_registry.py +++ b/backend/app/core/settings_registry.py @@ -210,6 +210,17 @@ register_setting(SettingDefinition( category="theme" )) +register_setting(SettingDefinition( + key="theme_tab_bar_position", + type=SettingType.STRING, + scope=SettingScope.GLOBAL, + storage=SettingStorage.DATABASE, + default="top", + description="Position of the tab bar (top, bottom, or responsive)", + category="theme", + choices=["top", "bottom", "responsive"] +)) + # ============================================================================= # MODULE/FEATURE SETTINGS (Global, Database) diff --git a/frontend/src/components/TabsScroller.tsx b/frontend/src/components/TabsScroller.tsx index e5b4091..8e6a517 100644 --- a/frontend/src/components/TabsScroller.tsx +++ b/frontend/src/components/TabsScroller.tsx @@ -40,6 +40,7 @@ const TabsScroller = forwardRef( const rafRef = useRef(0); const [showLeft, setShowLeft] = useState(false); const [showRight, setShowRight] = useState(false); + const dragRef = useRef({ pointerId: null as number | null, startX: 0, @@ -65,13 +66,23 @@ const TabsScroller = forwardRef( const updateOverflow = useCallback(() => { const node = sliderRef.current; if (!node) return; + const hasTabButtons = Boolean(node.querySelector('.page-tab-btn, .admin-tab-btn')); + const hasTabsValue = hasTabButtons ? 'true' : 'false'; + node.dataset.hasTabs = hasTabsValue; + const container = node.closest('.page-tabs-container, .admin-tabs-container'); + if (container instanceof HTMLElement) { + container.dataset.hasTabs = hasTabsValue; + } + const mainContent = node.closest('.main-content'); + if (mainContent instanceof HTMLElement) { + mainContent.dataset.hasTabs = hasTabsValue; + } if (!showArrows) { setShowLeft(false); setShowRight(false); return; } - const hasTabs = Boolean(node.querySelector('.page-tab-btn, .admin-tab-btn')); - if (!hasTabs) { + if (!hasTabButtons) { setShowLeft(false); setShowRight(false); return; @@ -93,6 +104,7 @@ const TabsScroller = forwardRef( updateOverflow(); const node = sliderRef.current; if (!node) return undefined; + const handleScroll = () => scheduleOverflowUpdate(); node.addEventListener('scroll', handleScroll, { passive: true }); let resizeObserver: ResizeObserver | null = null; diff --git a/frontend/src/contexts/SidebarContext.tsx b/frontend/src/contexts/SidebarContext.tsx index 4e6f92f..c4bc4dd 100644 --- a/frontend/src/contexts/SidebarContext.tsx +++ b/frontend/src/contexts/SidebarContext.tsx @@ -1,8 +1,11 @@ -import { createContext, useContext, useState, useEffect, useCallback } from 'react'; +import { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react'; import type { ReactNode } from 'react'; import { useAuth } from './AuthContext'; import { settingsAPI } from '../api/client'; +const EDGE_SWIPE_THRESHOLD = 30; // pixels from left edge to start swipe +const SWIPE_MIN_DISTANCE = 50; // minimum swipe distance to trigger + export type SidebarMode = 'collapsed' | 'expanded' | 'toggle' | 'dynamic'; interface SidebarContextType { @@ -85,6 +88,12 @@ export function SidebarProvider({ children }: { children: ReactNode }) { // Can only toggle if mode is 'toggle' const canToggle = sidebarMode === 'toggle'; + // Sync collapsed state to document root for CSS selectors + useEffect(() => { + const root = document.documentElement; + root.setAttribute('data-sidebar-collapsed', String(isCollapsed)); + }, [isCollapsed]); + const toggleCollapse = () => { if (canToggle) { setUserCollapsed((prev) => { @@ -103,6 +112,67 @@ export function SidebarProvider({ children }: { children: ReactNode }) { setIsMobileOpen(false); }; + const openMobileMenu = useCallback(() => { + setIsMobileOpen(true); + }, []); + + // Edge swipe detection for mobile + const swipeRef = useRef({ + startX: 0, + startY: 0, + isEdgeSwipe: false, + }); + + useEffect(() => { + const isMobile = () => window.innerWidth <= 768; + + const handleTouchStart = (e: TouchEvent) => { + if (!isMobile()) return; + const touch = e.touches[0]; + if (!touch) return; + + // Check if touch started near left edge + if (touch.clientX <= EDGE_SWIPE_THRESHOLD) { + swipeRef.current = { + startX: touch.clientX, + startY: touch.clientY, + isEdgeSwipe: true, + }; + } else { + swipeRef.current.isEdgeSwipe = false; + } + }; + + const handleTouchMove = (e: TouchEvent) => { + if (!swipeRef.current.isEdgeSwipe) return; + const touch = e.touches[0]; + if (!touch) return; + + const dx = touch.clientX - swipeRef.current.startX; + const dy = Math.abs(touch.clientY - swipeRef.current.startY); + + // If horizontal movement is greater than vertical and exceeds threshold, open sidebar + if (dx > SWIPE_MIN_DISTANCE && dx > dy * 2) { + openMobileMenu(); + swipeRef.current.isEdgeSwipe = false; // Prevent multiple triggers + } + }; + + const handleTouchEnd = () => { + swipeRef.current.isEdgeSwipe = false; + }; + + document.addEventListener('touchstart', handleTouchStart, { passive: true }); + document.addEventListener('touchmove', handleTouchMove, { passive: true }); + document.addEventListener('touchend', handleTouchEnd, { passive: true }); + + return () => { + document.removeEventListener('touchstart', handleTouchStart); + document.removeEventListener('touchmove', handleTouchMove); + document.removeEventListener('touchend', handleTouchEnd); + }; + }, [openMobileMenu]); + const setSidebarMode = useCallback(async (mode: SidebarMode) => { try { if (!token) return; diff --git a/frontend/src/contexts/ThemeContext.tsx b/frontend/src/contexts/ThemeContext.tsx index d394da8..09ceabc 100644 --- a/frontend/src/contexts/ThemeContext.tsx +++ b/frontend/src/contexts/ThemeContext.tsx @@ -12,6 +12,7 @@ export type FontFamily = 'sans' | 'inter' | 'roboto'; export type ColorPalette = 'default' | 'monochrome' | 'monochromeBlue' | 'sepia' | 'nord' | 'dracula' | 'solarized' | 'github' | 'ocean' | 'forest' | 'midnight' | 'sunset'; export type ControlLocation = 'sidebar' | 'user_menu'; +export type TabBarPosition = 'top' | 'bottom' | 'responsive'; interface ThemeContextType { theme: Theme; @@ -28,6 +29,7 @@ interface ThemeContextType { showLanguageToggle: boolean; showDarkModeLogin: boolean; showLanguageLogin: boolean; + tabBarPosition: TabBarPosition; hasInitializedSettings: boolean; isLoadingSettings: boolean; toggleTheme: () => void; @@ -44,6 +46,7 @@ interface ThemeContextType { setShowLanguageToggle: (show: boolean) => void; setShowDarkModeLogin: (show: boolean) => void; setShowLanguageLogin: (show: boolean) => void; + setTabBarPosition: (position: TabBarPosition) => void; loadThemeFromBackend: () => Promise; saveThemeToBackend: (overrides?: Partial<{ accentColor: AccentColor; @@ -59,6 +62,7 @@ interface ThemeContextType { showLanguageToggle: boolean; showDarkModeLogin: boolean; showLanguageLogin: boolean; + tabBarPosition: TabBarPosition; }>) => Promise; } @@ -480,6 +484,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) { const [showLanguageToggle, setShowLanguageToggleState] = useState(false); const [showDarkModeLogin, setShowDarkModeLoginState] = useState(true); const [showLanguageLogin, setShowLanguageLoginState] = useState(false); + const [tabBarPosition, setTabBarPositionState] = useState('top'); const [hasInitializedSettings, setHasInitializedSettings] = useState(false); const [isLoadingSettings, setIsLoadingSettings] = useState(false); @@ -546,6 +551,11 @@ export function ThemeProvider({ children }: { children: ReactNode }) { root.style.setProperty('--font-sans', FONTS[fontFamily]); }, [fontFamily]); + useEffect(() => { + const root = document.documentElement; + root.setAttribute('data-tab-position', tabBarPosition); + }, [tabBarPosition]); + // Apply color palette with overrides useEffect(() => { const root = document.documentElement; @@ -653,6 +663,9 @@ export function ThemeProvider({ children }: { children: ReactNode }) { console.error('Failed to parse custom colors:', e); } } + if (themeData.theme_tab_bar_position) { + setTabBarPositionState(themeData.theme_tab_bar_position as TabBarPosition); + } } catch (error) { console.error('Failed to load theme from backend:', error); } finally { @@ -676,6 +689,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) { showLanguageToggle: boolean; showDarkModeLogin: boolean; showLanguageLogin: boolean; + tabBarPosition: TabBarPosition; }>) => { try { const payload: Record = {}; @@ -694,6 +708,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) { if (overrides.showLanguageToggle !== undefined) payload.theme_show_language_toggle = overrides.showLanguageToggle; if (overrides.showDarkModeLogin !== undefined) payload.theme_show_dark_mode_login = overrides.showDarkModeLogin; if (overrides.showLanguageLogin !== undefined) payload.theme_show_language_login = overrides.showLanguageLogin; + if (overrides.tabBarPosition !== undefined) payload.theme_tab_bar_position = overrides.tabBarPosition; } else { payload.theme_accent_color = accentColor; payload.theme_border_radius = borderRadius; @@ -708,6 +723,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) { payload.theme_show_language_toggle = showLanguageToggle; payload.theme_show_dark_mode_login = showDarkModeLogin; payload.theme_show_language_login = showLanguageLogin; + payload.theme_tab_bar_position = tabBarPosition; } if (Object.keys(payload).length === 0) return; @@ -717,7 +733,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) { console.error('Failed to save theme to backend:', error); throw error; } - }, [accentColor, borderRadius, sidebarStyle, density, fontFamily, colorPalette, customColors, darkModeLocation, languageLocation, showDarkModeToggle, showLanguageToggle, showDarkModeLogin, showLanguageLogin]); + }, [accentColor, borderRadius, sidebarStyle, density, fontFamily, colorPalette, customColors, darkModeLocation, languageLocation, showDarkModeToggle, showLanguageToggle, showDarkModeLogin, showLanguageLogin, tabBarPosition]); // Load theme settings for both authenticated and unauthenticated users useEffect(() => { @@ -776,6 +792,10 @@ export function ThemeProvider({ children }: { children: ReactNode }) { setShowLanguageLoginState(show); }; + const setTabBarPosition = (position: TabBarPosition) => { + setTabBarPositionState(position); + }; + return ( { + setTabBarPosition(position); + saveThemeToBackend({ tabBarPosition: position }).catch(console.error); + }; + // Color picker popup state const [colorPickerState, setColorPickerState] = useState(null); const hasUserModifiedCustomColors = useRef(false); @@ -161,6 +168,12 @@ export default function ThemeSettings() { { id: 'roboto', label: t.theme.fontOptions.roboto, description: t.theme.fontOptions.robotoDesc, fontStyle: 'Roboto' }, ]; + const tabBarPositions: { id: TabBarPosition; label: string; description: string }[] = [ + { id: 'top', label: t.theme.tabBarPositions?.top || 'Sopra', description: t.theme.tabBarPositions?.topDesc || 'Barra dei tab sempre in alto' }, + { id: 'bottom', label: t.theme.tabBarPositions?.bottom || 'Sotto', description: t.theme.tabBarPositions?.bottomDesc || 'Barra dei tab sempre in basso' }, + { id: 'responsive', label: t.theme.tabBarPositions?.responsive || 'Responsivo', description: t.theme.tabBarPositions?.responsiveDesc || 'In alto su desktop, in basso su mobile' }, + ]; + const palettes: { id: ColorPalette; label: string; description: string }[] = [ { id: 'monochrome', label: t.theme.palettes.monochrome, description: t.theme.palettes.monochromeDesc }, { id: 'default', label: t.theme.palettes.default, description: t.theme.palettes.defaultDesc }, @@ -471,6 +484,40 @@ export default function ThemeSettings() { ))} + +
+
+

{t.theme.tabBarPosition || 'Posizione Tab'}

+
+
+ {tabBarPositions.map((pos) => ( +
handleTabBarPositionChange(pos.id)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleTabBarPositionChange(pos.id); + } + }} + > +
+
+
+
+
+
+
+ {pos.label} + {pos.description} +
+
+ ))} +
+
); diff --git a/frontend/src/styles/Layout.css b/frontend/src/styles/Layout.css index 813c187..34610c8 100644 --- a/frontend/src/styles/Layout.css +++ b/frontend/src/styles/Layout.css @@ -29,13 +29,31 @@ /* ========== PAGE STRUCTURE ========== */ -/* Page header container - centered with symmetric padding */ +/* Page header container - fixed at top */ .page-tabs-container, .admin-tabs-container { display: flex; justify-content: center; padding: 0.75rem; - background: transparent; + position: fixed; + top: 0; + left: var(--sidebar-width); + right: 0; + z-index: 100; +} + +/* Handle collapsed sidebar for fixed header (desktop only) */ +@media (min-width: 769px) { + [data-sidebar-collapsed='true'] .page-tabs-container, + [data-sidebar-collapsed='true'] .admin-tabs-container { + left: var(--sidebar-width-collapsed); + } +} + +/* Add top padding to content to account for fixed header */ +.page-content, +.admin-tab-swipe { + padding-top: calc(var(--page-padding-y) + 60px); } /* Ensure no extra margin from body */ @@ -76,7 +94,6 @@ label, gap: 4px; box-shadow: var(--shadow-md); max-width: 100%; - backdrop-filter: blur(14px) saturate(1.15); overflow-x: auto; overflow-y: hidden; -webkit-overflow-scrolling: touch; @@ -423,8 +440,9 @@ label, padding: var(--page-padding-y-tablet) var(--page-padding-x-tablet); } - .page-content { - padding: var(--page-padding-y-tablet) var(--page-padding-x-tablet); + .page-content, + .admin-tab-swipe { + padding-top: calc(var(--page-padding-y-tablet) + 60px); } .admin-tab-content { @@ -472,6 +490,12 @@ label, .page-tabs-container, .admin-tabs-container { padding: 0.75rem; + left: 0; + } + + .page-content, + .admin-tab-swipe { + padding-top: calc(var(--page-padding-y-mobile) + 60px); } .page-tabs-slider, @@ -517,29 +541,29 @@ label, } /* Hide title section when tabs are present on mobile */ - .page-tabs-slider:has(.page-tab-btn) .page-title-section, - .page-tabs-slider:has(.admin-tab-btn) .page-title-section, - .admin-tabs-slider:has(.admin-tab-btn) .admin-title-section, - .admin-tabs-slider:has(.page-tab-btn) .admin-title-section { + .page-tabs-slider[data-has-tabs='true'] .page-title-section, + .page-tabs-slider[data-has-tabs='true'] .admin-title-section, + .admin-tabs-slider[data-has-tabs='true'] .page-title-section, + .admin-tabs-slider[data-has-tabs='true'] .admin-title-section { display: none; } /* Add padding-left when tabs are present to avoid logo overlap */ - .page-tabs-slider:has(.page-tab-btn), - .page-tabs-slider:has(.admin-tab-btn), - .admin-tabs-slider:has(.admin-tab-btn), - .admin-tabs-slider:has(.page-tab-btn) { + .page-tabs-slider[data-has-tabs='true'], + .admin-tabs-slider[data-has-tabs='true'] { padding-left: 72px; } /* Center title section absolutely when no tabs are present on mobile */ - .page-tabs-slider:not(:has(.page-tab-btn)):not(:has(.admin-tab-btn)), - .admin-tabs-slider:not(:has(.admin-tab-btn)):not(:has(.page-tab-btn)) { + .page-tabs-slider[data-has-tabs='false'], + .admin-tabs-slider[data-has-tabs='false'] { justify-content: center; } - .page-tabs-slider:not(:has(.page-tab-btn)):not(:has(.admin-tab-btn)) .page-title-section, - .admin-tabs-slider:not(:has(.admin-tab-btn)):not(:has(.page-tab-btn)) .admin-title-section { + .page-tabs-slider[data-has-tabs='false'] .page-title-section, + .page-tabs-slider[data-has-tabs='false'] .admin-title-section, + .admin-tabs-slider[data-has-tabs='false'] .page-title-section, + .admin-tabs-slider[data-has-tabs='false'] .admin-title-section { display: flex; position: absolute; left: 50%; @@ -550,8 +574,10 @@ label, } /* Lighter icon color in dark theme when only title is shown */ - .page-tabs-slider:not(:has(.page-tab-btn)):not(:has(.admin-tab-btn)) .page-title-section .material-symbols-outlined, - .admin-tabs-slider:not(:has(.admin-tab-btn)):not(:has(.page-tab-btn)) .admin-title-section .material-symbols-outlined { + .page-tabs-slider[data-has-tabs='false'] .page-title-section .material-symbols-outlined, + .page-tabs-slider[data-has-tabs='false'] .admin-title-section .material-symbols-outlined, + .admin-tabs-slider[data-has-tabs='false'] .page-title-section .material-symbols-outlined, + .admin-tabs-slider[data-has-tabs='false'] .admin-title-section .material-symbols-outlined { color: var(--color-text-secondary); } @@ -767,8 +793,7 @@ label, @supports (color: color-mix(in srgb, black, white)) { [data-theme='dark'] .page-tabs-slider, [data-theme='dark'] .admin-tabs-slider { - background: color-mix(in srgb, var(--color-bg-sidebar) 88%, transparent); - backdrop-filter: blur(18px) saturate(1.15); + background: color-mix(in srgb, var(--color-bg-sidebar) 95%, transparent); } } @@ -815,3 +840,122 @@ label, [data-theme='dark'][data-accent='auto'] .admin-tab-btn.active:focus-visible { box-shadow: 0 0 25px 4px rgba(229, 231, 235, 0.5); } + +/* ========== TAB BAR POSITION ========== */ + +/* + * Tab bar positioning system: + * - 'top': default, no changes needed + * - 'bottom': fixed at bottom on all screens (only when tabs are present) + * - 'responsive': top on desktop (>768px), bottom on mobile (<=768px) + * + * Uses data-has-tabs set by TabsScroller to avoid :has() support issues. + */ + +/* Bottom position - fixed at bottom (global setting for all pages) */ +/* Note: Primary positioning is handled by TabsScroller component with inline styles. + These CSS rules serve as fallback/additional styling. */ +[data-tab-position='bottom'] .page-tabs-container, +[data-tab-position='bottom'] .admin-tabs-container { + position: fixed; + top: auto; + bottom: 0; + left: var(--sidebar-width); + right: 0; + z-index: 100; + padding: 0.5rem; + padding-bottom: calc(0.5rem + env(safe-area-inset-bottom, 0px)); +} + +/* Handle collapsed sidebar */ +[data-tab-position='bottom'][data-sidebar-collapsed='true'] .page-tabs-container, +[data-tab-position='bottom'][data-sidebar-collapsed='true'] .admin-tabs-container { + left: var(--sidebar-width-collapsed); +} + +/* Adjust slider styling for bottom position */ +[data-tab-position='bottom'] .page-tabs-slider, +[data-tab-position='bottom'] .admin-tabs-slider { + width: 100%; + justify-content: flex-start; +} + +/* Hide title section when bar is at bottom (only on pages with tabs) */ +[data-tab-position='bottom'] .page-tabs-slider[data-has-tabs='true'] .page-title-section, +[data-tab-position='bottom'] .page-tabs-slider[data-has-tabs='true'] .admin-title-section, +[data-tab-position='bottom'] .admin-tabs-slider[data-has-tabs='true'] .page-title-section, +[data-tab-position='bottom'] .admin-tabs-slider[data-has-tabs='true'] .admin-title-section { + display: none; +} + +/* Hide divider when bar is at bottom */ +[data-tab-position='bottom'] .page-tabs-slider .page-tabs-divider, +[data-tab-position='bottom'] .page-tabs-slider .admin-tabs-divider, +[data-tab-position='bottom'] .admin-tabs-slider .page-tabs-divider, +[data-tab-position='bottom'] .admin-tabs-slider .admin-tabs-divider { + display: none; +} + +/* Add bottom padding to main content when bar is at bottom */ +[data-tab-position='bottom'] .main-content { + padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px)); +} + +/* Mobile: full width at bottom */ +@media (max-width: 768px) { + [data-tab-position='bottom'] .page-tabs-container, + [data-tab-position='bottom'] .admin-tabs-container { + left: 0; + } + + /* Override collapsed state on mobile */ + [data-tab-position='bottom'][data-sidebar-collapsed='true'] .page-tabs-container, + [data-tab-position='bottom'][data-sidebar-collapsed='true'] .admin-tabs-container { + left: 0; + } + + /* Keep proper spacing for mobile menu button - inherits padding-left: 72px from mobile rules */ +} + +/* Responsive position - top on desktop, bottom on mobile */ +@media (max-width: 768px) { + [data-tab-position='responsive'] .page-tabs-container, + [data-tab-position='responsive'] .admin-tabs-container { + position: fixed; + top: auto; + bottom: 0; + left: 0; + right: 0; + z-index: 100; + padding: 0.5rem; + padding-bottom: calc(0.5rem + env(safe-area-inset-bottom, 0px)); + } + + [data-tab-position='responsive'] .page-tabs-slider, + [data-tab-position='responsive'] .admin-tabs-slider { + width: 100%; + justify-content: flex-start; + /* padding-left: 72px inherited from mobile rules for menu button spacing */ + } + + /* Hide title section when bar is at bottom (only on pages with tabs) */ + [data-tab-position='responsive'] .page-tabs-slider[data-has-tabs='true'] .page-title-section, + [data-tab-position='responsive'] .page-tabs-slider[data-has-tabs='true'] .admin-title-section, + [data-tab-position='responsive'] .admin-tabs-slider[data-has-tabs='true'] .page-title-section, + [data-tab-position='responsive'] .admin-tabs-slider[data-has-tabs='true'] .admin-title-section { + display: none; + } + + /* Hide divider when bar is at bottom */ + [data-tab-position='responsive'] .page-tabs-slider .page-tabs-divider, + [data-tab-position='responsive'] .page-tabs-slider .admin-tabs-divider, + [data-tab-position='responsive'] .admin-tabs-slider .page-tabs-divider, + [data-tab-position='responsive'] .admin-tabs-slider .admin-tabs-divider { + display: none; + } + + /* Add bottom padding to main content */ + [data-tab-position='responsive'] .main-content { + padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px)); + } +} diff --git a/frontend/src/styles/ThemeSettings.css b/frontend/src/styles/ThemeSettings.css index 487b650..3995c63 100644 --- a/frontend/src/styles/ThemeSettings.css +++ b/frontend/src/styles/ThemeSettings.css @@ -455,6 +455,79 @@ color: var(--color-accent); } +/* Tab Bar Position Preview */ +.tabbar-preview { + width: 36px; + height: 36px; + border-radius: var(--radius-sm); + position: relative; + background: var(--color-bg-elevated); + border: 1px solid var(--color-border); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.tabbar-preview .tabbar-line { + height: 6px; + background: var(--color-accent); + border-radius: 2px; + margin: 3px; + flex-shrink: 0; +} + +.tabbar-preview .tabbar-content { + flex: 1; + background: var(--color-bg-card); + margin: 0 3px; + border-radius: 2px; +} + +/* Top position */ +.tabbar-preview-top { + flex-direction: column; +} + +.tabbar-preview-top .tabbar-line { + margin-bottom: 0; +} + +.tabbar-preview-top .tabbar-content { + margin-bottom: 3px; +} + +/* Bottom position */ +.tabbar-preview-bottom { + flex-direction: column-reverse; +} + +.tabbar-preview-bottom .tabbar-line { + margin-top: 0; +} + +.tabbar-preview-bottom .tabbar-content { + margin-top: 3px; +} + +/* Responsive position - shows split layout */ +.tabbar-preview-responsive { + flex-direction: row; + padding: 3px; + gap: 2px; +} + +.tabbar-preview-responsive .tabbar-line { + width: 6px; + height: auto; + margin: 0; + flex-shrink: 0; +} + +.tabbar-preview-responsive .tabbar-content { + flex: 1; + margin: 0; +} + /* ========== PREVIEW TAB ========== */ /* Preview Container - Grid layout for preview cards */ @@ -1502,3 +1575,8 @@ border-color: #e5e7eb !important; box-shadow: 0 0 0 3px rgba(229, 231, 235, 0.15) !important; } + +/* Tab bar preview accent */ +[data-theme='dark'][data-accent='auto'] .tabbar-preview .tabbar-line { + background: #e5e7eb; +}