From 2559bca02a0f2ed21e25f04fc8bda83bdfbaf85e Mon Sep 17 00:00:00 2001 From: spencrmartin Date: Fri, 13 Feb 2026 11:11:30 -0500 Subject: [PATCH] feat: Add split-panel live preview to Custom Colors tab - Implement 40/60 split layout: Color pickers (left) + Live preview (right) - Create ColorPreview component showing in-situ usage examples - Each color category shows relevant UI components: - Backgrounds: Cards, chat messages, sidebar, panels - Text: Typography samples, headings, body text - Borders: Inputs, dividers, card borders - Ring: Focus states on buttons and inputs - Selected color highlights with border and background - Empty state with instructions when no color selected - Real-time preview updates as colors change - Improved UX: Users can see exactly where colors are used Component showcases: - Primary background: Chat messages, main areas - Secondary background: Sidebar, cards - Tertiary background: Nested panels, hover states - Text colors: Headings, body, labels, semantic states - Border colors: Cards, inputs, dividers - Ring colors: Focus indicators This makes theme customization much more intuitive and powerful! --- .../ThemeColorEditor/Preview/ColorPreview.tsx | 391 ++++++++++++++++++ .../settings/app/ThemeColorEditor/index.tsx | 129 +++--- 2 files changed, 470 insertions(+), 50 deletions(-) create mode 100644 ui/desktop/src/components/settings/app/ThemeColorEditor/Preview/ColorPreview.tsx diff --git a/ui/desktop/src/components/settings/app/ThemeColorEditor/Preview/ColorPreview.tsx b/ui/desktop/src/components/settings/app/ThemeColorEditor/Preview/ColorPreview.tsx new file mode 100644 index 0000000000..f5f2020865 --- /dev/null +++ b/ui/desktop/src/components/settings/app/ThemeColorEditor/Preview/ColorPreview.tsx @@ -0,0 +1,391 @@ +/** + * ColorPreview Component + * + * Shows live preview of how a selected color variable is used in the UI. + * Displays real component examples with the color applied. + */ + +import { ColorVariable } from '../types'; + +interface ColorPreviewProps { + variable: ColorVariable; + lightColor: string; + darkColor: string; + currentMode: 'light' | 'dark'; +} + +export function ColorPreview({ variable, lightColor, darkColor, currentMode }: ColorPreviewProps) { + const currentColor = currentMode === 'light' ? lightColor : darkColor; + + // Render different previews based on color category + const renderPreview = () => { + switch (variable.category) { + case 'background': + return ; + case 'text': + return ; + case 'border': + return ; + case 'ring': + return ; + default: + return null; + } + }; + + return ( +
+
+

+ {variable.label} +

+

+ {variable.description} +

+
+
+
+ {currentColor} +
+
+
+ +
+

+ Used In: +

+ {renderPreview()} +
+
+ ); +} + +// Background color previews +function BackgroundPreview({ variable, color }: { variable: ColorVariable; color: string }) { + const varName = variable.name; + + if (varName === 'color-background-primary') { + return ( +
+ +
+

+ This is the primary background color used throughout the app. +

+

+ It appears in the main chat area, settings panels, and most content areas. +

+
+
+ + +
+
+ AI +
+
+

+ This is how AI messages appear on the primary background. +

+
+
+
+
+ ); + } + + if (varName === 'color-background-secondary') { + return ( +
+ +
+
+
Navigation
+
+
+ Home +
+
+ Chat +
+
+ Settings +
+
+
+
+
+ + +
+

Settings Card

+

+ Cards and panels use the secondary background for visual hierarchy. +

+
+
+
+ ); + } + + if (varName === 'color-background-tertiary') { + return ( + +
+
+

+ Tertiary background for nested panels and hover states. +

+
+
+
+ ); + } + + if (varName === 'color-background-danger') { + return ( + +
+

⚠️ Error

+

+ This background is used for error messages and dangerous actions. +

+
+
+ ); + } + + if (varName === 'color-background-info') { + return ( + +
+

ℹ️ Information

+

+ This background is used for informational messages and tips. +

+
+
+ ); + } + + return ( + +
+

Background color preview

+
+
+ ); +} + +// Text color previews +function TextPreview({ variable, color }: { variable: ColorVariable; color: string }) { + const varName = variable.name; + + if (varName === 'color-text-primary') { + return ( +
+ +
+

Heading Text

+

+ This is the main text color used for body content, headings, and primary information. +

+
+ It appears in chat messages, settings descriptions, and all main content. +
+
+
+
+ ); + } + + if (varName === 'color-text-secondary') { + return ( + +
+

Main heading

+

+ Secondary text is used for labels, captions, and less important information. +

+
+ Status: + Active +
+
+
+ ); + } + + if (varName === 'color-text-danger') { + return ( + +
+

⚠️ Error Message

+

+ This color is used for error messages, warnings, and destructive actions. +

+ +
+
+ ); + } + + if (varName === 'color-text-success') { + return ( + +
+

✓ Success

+

+ Used for success messages, confirmations, and positive feedback. +

+
+
+ ); + } + + if (varName === 'color-text-warning') { + return ( + +
+

⚡ Warning

+

+ Used for warnings and caution messages. +

+
+
+ ); + } + + if (varName === 'color-text-info') { + return ( + +
+

ℹ️ Information

+

+ Used for informational messages and tips. +

+
+
+ ); + } + + return ( + +

Sample text in this color

+
+ ); +} + +// Border color previews +function BorderPreview({ variable, color }: { variable: ColorVariable; color: string }) { + const varName = variable.name; + + if (varName === 'color-border-primary') { + return ( +
+ +
+

Card with primary border

+
+
+ + + + + + +
+

Section 1

+
+

Section 2

+
+ +
+ ); + } + + if (varName === 'color-border-danger') { + return ( + +
+
+

Error state border

+
+ +
+
+ ); + } + + return ( + +
+

Element with this border color

+
+
+ ); +} + +// Ring (focus) color previews +function RingPreview({ variable, color }: { variable: ColorVariable; color: string }) { + return ( +
+ +
+ + + +
+
+ + +

+ The ring color appears when elements receive keyboard focus for accessibility. +

+
+
+ ); +} + +// Helper component for preview cards +function PreviewCard({ title, children }: { title: string; children: React.ReactNode }) { + return ( +
+
{title}
+
+ {children} +
+
+ ); +} diff --git a/ui/desktop/src/components/settings/app/ThemeColorEditor/index.tsx b/ui/desktop/src/components/settings/app/ThemeColorEditor/index.tsx index 183dedf346..99536e5b0e 100644 --- a/ui/desktop/src/components/settings/app/ThemeColorEditor/index.tsx +++ b/ui/desktop/src/components/settings/app/ThemeColorEditor/index.tsx @@ -10,10 +10,11 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } f import { Tabs, TabsContent, TabsList, TabsTrigger } from '../../../ui/tabs'; import { Button } from '../../../ui/button'; import { getThemeVariables, saveTheme } from '../../../../api'; -import { ThemeColorEditorProps, ThemeColors, ColorMode, COLOR_VARIABLES } from './types'; +import { ThemeColorEditorProps, ThemeColors, ColorMode, COLOR_VARIABLES, ColorVariable } from './types'; import { HexColorPicker } from 'react-colorful'; import { toast } from 'react-toastify'; import { PresetGallery } from './ThemeSelector/PresetGallery'; +import { ColorPreview } from './Preview/ColorPreview'; export function ThemeColorEditor({ onClose }: ThemeColorEditorProps) { const [loading, setLoading] = useState(true); @@ -171,58 +172,86 @@ export function ThemeColorEditor({ onClose }: ThemeColorEditorProps) { Dark Mode - -
- {Object.entries(groupedVariables).map(([category, variables]) => ( -
-

- {category} Colors -

-
- {variables.map((variable) => { - const currentColor = themeColors[activeMode][variable.name] || '#000000'; - const isSelected = selectedVariable === variable.name; - - return ( -
-
- -
setSelectedVariable(isSelected ? null : variable.name)} - /> -
- - {isSelected && ( -
- handleColorChange(variable.name, color)} - /> - handleColorChange(variable.name, e.target.value)} - className="mt-2 w-full px-2 py-1 text-xs border border-border-primary rounded bg-background-primary text-text-primary" - placeholder="#000000" + + {/* Split Panel Layout */} +
+ {/* Left Panel: Color Pickers (40%) */} +
+ {Object.entries(groupedVariables).map(([category, variables]) => ( +
+

+ {category} Colors +

+
+ {variables.map((variable) => { + const currentColor = themeColors[activeMode][variable.name] || '#000000'; + const isSelected = selectedVariable === variable.name; + + return ( +
setSelectedVariable(variable.name)} + > +
+ +
- )} - - {variable.description && ( -

- {variable.description} -

- )} -
- ); - })} + + {isSelected && ( +
+ handleColorChange(variable.name, color)} + style={{ width: '100%' }} + /> + handleColorChange(variable.name, e.target.value)} + className="w-full px-3 py-2 text-sm border border-border-primary rounded bg-background-primary text-text-primary font-mono" + placeholder="#000000" + /> +
+ )} +
+ ); + })} +
-
- ))} + ))} +
+ + {/* Right Panel: Live Preview (60%) */} +
+ {selectedVariable ? ( + v.name === selectedVariable)!} + lightColor={themeColors.light[selectedVariable] || '#000000'} + darkColor={themeColors.dark[selectedVariable] || '#000000'} + currentMode={activeMode} + /> + ) : ( +
+
+
🎨
+

Select a color to preview

+

+ Click any color on the left to see where it's used in the UI +

+
+
+ )} +