mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
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!
This commit is contained in:
@@ -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 <BackgroundPreview variable={variable} color={currentColor} />;
|
||||
case 'text':
|
||||
return <TextPreview variable={variable} color={currentColor} />;
|
||||
case 'border':
|
||||
return <BorderPreview variable={variable} color={currentColor} />;
|
||||
case 'ring':
|
||||
return <RingPreview variable={variable} color={currentColor} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-semibold text-text-primary">
|
||||
{variable.label}
|
||||
</h3>
|
||||
<p className="text-xs text-text-secondary">
|
||||
{variable.description}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-12 h-12 rounded border-2 border-border-primary"
|
||||
style={{ backgroundColor: currentColor }}
|
||||
/>
|
||||
<div className="text-xs font-mono text-text-secondary">
|
||||
{currentColor}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-xs font-semibold text-text-secondary uppercase tracking-wide">
|
||||
Used In:
|
||||
</h4>
|
||||
{renderPreview()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Background color previews
|
||||
function BackgroundPreview({ variable, color }: { variable: ColorVariable; color: string }) {
|
||||
const varName = variable.name;
|
||||
|
||||
if (varName === 'color-background-primary') {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<PreviewCard title="Main Background">
|
||||
<div className="p-4 rounded-lg border border-border-primary" style={{ backgroundColor: color }}>
|
||||
<p className="text-text-primary text-sm">
|
||||
This is the primary background color used throughout the app.
|
||||
</p>
|
||||
<p className="text-text-secondary text-xs mt-2">
|
||||
It appears in the main chat area, settings panels, and most content areas.
|
||||
</p>
|
||||
</div>
|
||||
</PreviewCard>
|
||||
|
||||
<PreviewCard title="Chat Message">
|
||||
<div className="flex gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-background-inverse flex items-center justify-center text-text-inverse text-xs">
|
||||
AI
|
||||
</div>
|
||||
<div className="flex-1 p-3 rounded-lg border border-border-primary" style={{ backgroundColor: color }}>
|
||||
<p className="text-text-primary text-sm">
|
||||
This is how AI messages appear on the primary background.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</PreviewCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (varName === 'color-background-secondary') {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<PreviewCard title="Sidebar">
|
||||
<div className="flex gap-2 h-32">
|
||||
<div className="w-48 rounded-lg border border-border-primary p-3 space-y-2" style={{ backgroundColor: color }}>
|
||||
<div className="text-text-primary text-xs font-semibold">Navigation</div>
|
||||
<div className="space-y-1">
|
||||
<div className="text-text-secondary text-xs px-2 py-1 rounded hover:bg-background-tertiary">
|
||||
Home
|
||||
</div>
|
||||
<div className="text-text-secondary text-xs px-2 py-1 rounded hover:bg-background-tertiary">
|
||||
Chat
|
||||
</div>
|
||||
<div className="text-text-secondary text-xs px-2 py-1 rounded hover:bg-background-tertiary">
|
||||
Settings
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PreviewCard>
|
||||
|
||||
<PreviewCard title="Card Background">
|
||||
<div className="p-4 rounded-lg border border-border-primary" style={{ backgroundColor: color }}>
|
||||
<h3 className="text-text-primary text-sm font-semibold mb-2">Settings Card</h3>
|
||||
<p className="text-text-secondary text-xs">
|
||||
Cards and panels use the secondary background for visual hierarchy.
|
||||
</p>
|
||||
</div>
|
||||
</PreviewCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (varName === 'color-background-tertiary') {
|
||||
return (
|
||||
<PreviewCard title="Nested Elements">
|
||||
<div className="p-3 rounded-lg bg-background-secondary border border-border-primary">
|
||||
<div className="p-3 rounded border border-border-primary" style={{ backgroundColor: color }}>
|
||||
<p className="text-text-primary text-xs">
|
||||
Tertiary background for nested panels and hover states.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</PreviewCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (varName === 'color-background-danger') {
|
||||
return (
|
||||
<PreviewCard title="Error States">
|
||||
<div className="p-3 rounded-lg border border-border-danger" style={{ backgroundColor: color }}>
|
||||
<p className="text-text-danger text-sm font-semibold">⚠️ Error</p>
|
||||
<p className="text-text-primary text-xs mt-1">
|
||||
This background is used for error messages and dangerous actions.
|
||||
</p>
|
||||
</div>
|
||||
</PreviewCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (varName === 'color-background-info') {
|
||||
return (
|
||||
<PreviewCard title="Info States">
|
||||
<div className="p-3 rounded-lg border border-border-info" style={{ backgroundColor: color }}>
|
||||
<p className="text-text-info text-sm font-semibold">ℹ️ Information</p>
|
||||
<p className="text-text-primary text-xs mt-1">
|
||||
This background is used for informational messages and tips.
|
||||
</p>
|
||||
</div>
|
||||
</PreviewCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PreviewCard title="Background Example">
|
||||
<div className="p-4 rounded-lg border border-border-primary" style={{ backgroundColor: color }}>
|
||||
<p className="text-text-primary text-sm">Background color preview</p>
|
||||
</div>
|
||||
</PreviewCard>
|
||||
);
|
||||
}
|
||||
|
||||
// Text color previews
|
||||
function TextPreview({ variable, color }: { variable: ColorVariable; color: string }) {
|
||||
const varName = variable.name;
|
||||
|
||||
if (varName === 'color-text-primary') {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<PreviewCard title="Primary Text">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-bold" style={{ color }}>Heading Text</h1>
|
||||
<p className="text-base" style={{ color }}>
|
||||
This is the main text color used for body content, headings, and primary information.
|
||||
</p>
|
||||
<div className="text-sm" style={{ color }}>
|
||||
It appears in chat messages, settings descriptions, and all main content.
|
||||
</div>
|
||||
</div>
|
||||
</PreviewCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (varName === 'color-text-secondary') {
|
||||
return (
|
||||
<PreviewCard title="Secondary Text">
|
||||
<div className="space-y-2">
|
||||
<p className="text-text-primary text-sm">Main heading</p>
|
||||
<p className="text-xs" style={{ color }}>
|
||||
Secondary text is used for labels, captions, and less important information.
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-text-primary text-xs">Status:</span>
|
||||
<span className="text-xs" style={{ color }}>Active</span>
|
||||
</div>
|
||||
</div>
|
||||
</PreviewCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (varName === 'color-text-danger') {
|
||||
return (
|
||||
<PreviewCard title="Error Text">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-semibold" style={{ color }}>⚠️ Error Message</p>
|
||||
<p className="text-xs" style={{ color }}>
|
||||
This color is used for error messages, warnings, and destructive actions.
|
||||
</p>
|
||||
<button className="px-3 py-1 text-xs rounded border border-border-danger" style={{ color }}>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</PreviewCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (varName === 'color-text-success') {
|
||||
return (
|
||||
<PreviewCard title="Success Text">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-semibold" style={{ color }}>✓ Success</p>
|
||||
<p className="text-xs" style={{ color }}>
|
||||
Used for success messages, confirmations, and positive feedback.
|
||||
</p>
|
||||
</div>
|
||||
</PreviewCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (varName === 'color-text-warning') {
|
||||
return (
|
||||
<PreviewCard title="Warning Text">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-semibold" style={{ color }}>⚡ Warning</p>
|
||||
<p className="text-xs" style={{ color }}>
|
||||
Used for warnings and caution messages.
|
||||
</p>
|
||||
</div>
|
||||
</PreviewCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (varName === 'color-text-info') {
|
||||
return (
|
||||
<PreviewCard title="Info Text">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-semibold" style={{ color }}>ℹ️ Information</p>
|
||||
<p className="text-xs" style={{ color }}>
|
||||
Used for informational messages and tips.
|
||||
</p>
|
||||
</div>
|
||||
</PreviewCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PreviewCard title="Text Example">
|
||||
<p className="text-sm" style={{ color }}>Sample text in this color</p>
|
||||
</PreviewCard>
|
||||
);
|
||||
}
|
||||
|
||||
// Border color previews
|
||||
function BorderPreview({ variable, color }: { variable: ColorVariable; color: string }) {
|
||||
const varName = variable.name;
|
||||
|
||||
if (varName === 'color-border-primary') {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<PreviewCard title="Card Borders">
|
||||
<div className="p-4 rounded-lg bg-background-secondary" style={{ borderWidth: '1px', borderStyle: 'solid', borderColor: color }}>
|
||||
<p className="text-text-primary text-sm">Card with primary border</p>
|
||||
</div>
|
||||
</PreviewCard>
|
||||
|
||||
<PreviewCard title="Input Borders">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Type here..."
|
||||
className="w-full px-3 py-2 rounded bg-background-primary text-text-primary text-sm"
|
||||
style={{ borderWidth: '1px', borderStyle: 'solid', borderColor: color }}
|
||||
/>
|
||||
</PreviewCard>
|
||||
|
||||
<PreviewCard title="Dividers">
|
||||
<div className="space-y-2">
|
||||
<p className="text-text-primary text-xs">Section 1</p>
|
||||
<div style={{ height: '1px', backgroundColor: color }} />
|
||||
<p className="text-text-primary text-xs">Section 2</p>
|
||||
</div>
|
||||
</PreviewCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (varName === 'color-border-danger') {
|
||||
return (
|
||||
<PreviewCard title="Error Borders">
|
||||
<div className="space-y-2">
|
||||
<div className="p-3 rounded-lg bg-background-danger" style={{ borderWidth: '1px', borderStyle: 'solid', borderColor: color }}>
|
||||
<p className="text-text-danger text-sm">Error state border</p>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Invalid input..."
|
||||
className="w-full px-3 py-2 rounded bg-background-primary text-text-primary text-sm"
|
||||
style={{ borderWidth: '2px', borderStyle: 'solid', borderColor: color }}
|
||||
/>
|
||||
</div>
|
||||
</PreviewCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PreviewCard title="Border Example">
|
||||
<div className="p-4 rounded-lg bg-background-secondary" style={{ borderWidth: '1px', borderStyle: 'solid', borderColor: color }}>
|
||||
<p className="text-text-primary text-sm">Element with this border color</p>
|
||||
</div>
|
||||
</PreviewCard>
|
||||
);
|
||||
}
|
||||
|
||||
// Ring (focus) color previews
|
||||
function RingPreview({ variable, color }: { variable: ColorVariable; color: string }) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<PreviewCard title="Focus States">
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
className="px-4 py-2 rounded bg-background-secondary text-text-primary text-sm"
|
||||
style={{
|
||||
outline: `2px solid ${color}`,
|
||||
outlineOffset: '2px'
|
||||
}}
|
||||
>
|
||||
Focused Button
|
||||
</button>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Focused input..."
|
||||
className="w-full px-3 py-2 rounded border border-border-primary bg-background-primary text-text-primary text-sm"
|
||||
style={{
|
||||
outline: `2px solid ${color}`,
|
||||
outlineOffset: '2px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</PreviewCard>
|
||||
|
||||
<PreviewCard title="Interactive Elements">
|
||||
<p className="text-xs text-text-secondary mb-2">
|
||||
The ring color appears when elements receive keyboard focus for accessibility.
|
||||
</p>
|
||||
</PreviewCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Helper component for preview cards
|
||||
function PreviewCard({ title, children }: { title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs font-medium text-text-secondary">{title}</div>
|
||||
<div className="rounded-lg bg-background-primary border border-border-primary p-3">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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) {
|
||||
<TabsTrigger value="dark">Dark Mode</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value={activeMode} className="flex-1 overflow-auto mt-4">
|
||||
<div className="space-y-6 pr-2">
|
||||
{Object.entries(groupedVariables).map(([category, variables]) => (
|
||||
<div key={category} className="space-y-3">
|
||||
<h3 className="text-sm font-semibold text-text-primary capitalize">
|
||||
{category} Colors
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{variables.map((variable) => {
|
||||
const currentColor = themeColors[activeMode][variable.name] || '#000000';
|
||||
const isSelected = selectedVariable === variable.name;
|
||||
|
||||
return (
|
||||
<div key={variable.name} className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs text-text-secondary">
|
||||
{variable.label}
|
||||
</label>
|
||||
<div
|
||||
className="w-8 h-8 rounded border-2 border-border-primary cursor-pointer hover:scale-110 transition-transform"
|
||||
style={{ backgroundColor: currentColor }}
|
||||
onClick={() => setSelectedVariable(isSelected ? null : variable.name)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isSelected && (
|
||||
<div className="mt-2">
|
||||
<HexColorPicker
|
||||
color={currentColor}
|
||||
onChange={(color) => handleColorChange(variable.name, color)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={currentColor}
|
||||
onChange={(e) => 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"
|
||||
<TabsContent value={activeMode} className="flex-1 overflow-hidden mt-4">
|
||||
{/* Split Panel Layout */}
|
||||
<div className="flex gap-4 h-full">
|
||||
{/* Left Panel: Color Pickers (40%) */}
|
||||
<div className="w-[40%] overflow-auto pr-2 space-y-6">
|
||||
{Object.entries(groupedVariables).map(([category, variables]) => (
|
||||
<div key={category} className="space-y-3">
|
||||
<h3 className="text-sm font-semibold text-text-primary capitalize">
|
||||
{category} Colors
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{variables.map((variable) => {
|
||||
const currentColor = themeColors[activeMode][variable.name] || '#000000';
|
||||
const isSelected = selectedVariable === variable.name;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={variable.name}
|
||||
className={`p-3 rounded-lg border-2 transition-all cursor-pointer ${
|
||||
isSelected
|
||||
? 'border-border-secondary bg-background-secondary'
|
||||
: 'border-border-primary hover:border-border-secondary'
|
||||
}`}
|
||||
onClick={() => setSelectedVariable(variable.name)}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="text-xs font-medium text-text-primary cursor-pointer">
|
||||
{variable.label}
|
||||
</label>
|
||||
<div
|
||||
className="w-10 h-10 rounded border-2 border-border-primary shadow-sm"
|
||||
style={{ backgroundColor: currentColor }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{variable.description && (
|
||||
<p className="text-xs text-text-secondary opacity-70">
|
||||
{variable.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{isSelected && (
|
||||
<div className="mt-3 space-y-2">
|
||||
<HexColorPicker
|
||||
color={currentColor}
|
||||
onChange={(color) => handleColorChange(variable.name, color)}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={currentColor}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Right Panel: Live Preview (60%) */}
|
||||
<div className="w-[60%] overflow-auto pl-4 border-l border-border-primary">
|
||||
{selectedVariable ? (
|
||||
<ColorPreview
|
||||
variable={COLOR_VARIABLES.find(v => v.name === selectedVariable)!}
|
||||
lightColor={themeColors.light[selectedVariable] || '#000000'}
|
||||
darkColor={themeColors.dark[selectedVariable] || '#000000'}
|
||||
currentMode={activeMode}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center space-y-2">
|
||||
<div className="text-4xl">🎨</div>
|
||||
<p className="text-text-primary font-medium">Select a color to preview</p>
|
||||
<p className="text-text-secondary text-sm max-w-xs">
|
||||
Click any color on the left to see where it's used in the UI
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
Reference in New Issue
Block a user