mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 02:06:36 +00:00
- Add 'Frosted Glass' to design features list in design-scheme.ts - Implement visual styling for frosted glass feature in ColorSchemeDialog - Adjust sidebar button margin in Workbench for better spacing
379 lines
17 KiB
TypeScript
379 lines
17 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Dialog, DialogTitle, DialogDescription, DialogRoot } from './Dialog';
|
|
import { Button } from './Button';
|
|
import { IconButton } from './IconButton';
|
|
import type { DesignScheme } from '~/types/design-scheme';
|
|
import { defaultDesignScheme, designFeatures, designFonts, paletteRoles } from '~/types/design-scheme';
|
|
|
|
export interface ColorSchemeDialogProps {
|
|
designScheme?: DesignScheme;
|
|
setDesignScheme?: (scheme: DesignScheme) => void;
|
|
}
|
|
|
|
export const ColorSchemeDialog: React.FC<ColorSchemeDialogProps> = ({ setDesignScheme, designScheme }) => {
|
|
const [palette, setPalette] = useState<{ [key: string]: string }>(() => {
|
|
if (designScheme?.palette) {
|
|
return { ...defaultDesignScheme.palette, ...designScheme.palette };
|
|
}
|
|
|
|
return defaultDesignScheme.palette;
|
|
});
|
|
|
|
const [features, setFeatures] = useState<string[]>(designScheme?.features || defaultDesignScheme.features);
|
|
const [font, setFont] = useState<string[]>(designScheme?.font || defaultDesignScheme.font);
|
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
const [activeSection, setActiveSection] = useState<'colors' | 'typography' | 'features'>('colors');
|
|
|
|
useEffect(() => {
|
|
if (designScheme) {
|
|
setPalette(() => ({ ...defaultDesignScheme.palette, ...designScheme.palette }));
|
|
setFeatures(designScheme.features || defaultDesignScheme.features);
|
|
setFont(designScheme.font || defaultDesignScheme.font);
|
|
} else {
|
|
setPalette(defaultDesignScheme.palette);
|
|
setFeatures(defaultDesignScheme.features);
|
|
setFont(defaultDesignScheme.font);
|
|
}
|
|
}, [designScheme]);
|
|
|
|
const handleColorChange = (role: string, value: string) => {
|
|
setPalette((prev) => ({ ...prev, [role]: value }));
|
|
};
|
|
|
|
const handleFeatureToggle = (key: string) => {
|
|
setFeatures((prev) => (prev.includes(key) ? prev.filter((f) => f !== key) : [...prev, key]));
|
|
};
|
|
|
|
const handleFontToggle = (key: string) => {
|
|
setFont((prev) => (prev.includes(key) ? prev.filter((f) => f !== key) : [...prev, key]));
|
|
};
|
|
|
|
const handleSave = () => {
|
|
setDesignScheme?.({ palette, features, font });
|
|
setIsDialogOpen(false);
|
|
};
|
|
|
|
const handleReset = () => {
|
|
setPalette(defaultDesignScheme.palette);
|
|
setFeatures(defaultDesignScheme.features);
|
|
setFont(defaultDesignScheme.font);
|
|
};
|
|
|
|
const renderColorSection = () => (
|
|
<div className="space-y-4">
|
|
<div className="flex justify-between items-center">
|
|
<h3 className="text-lg font-semibold text-bolt-elements-textPrimary flex items-center gap-2">
|
|
<div className="w-2 h-2 rounded-full bg-bolt-elements-item-contentAccent"></div>
|
|
Color Palette
|
|
</h3>
|
|
<button
|
|
onClick={handleReset}
|
|
className="text-sm bg-transparent hover:bg-bolt-elements-bg-depth-2 text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary rounded-lg flex items-center gap-2 transition-all duration-200"
|
|
>
|
|
<span className="i-ph:arrow-clockwise text-sm" />
|
|
Reset
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 max-h-80 overflow-y-auto pr-2 custom-scrollbar">
|
|
{paletteRoles.map((role) => (
|
|
<div
|
|
key={role.key}
|
|
className="group flex items-center gap-4 p-4 rounded-xl bg-bolt-elements-bg-depth-3 hover:bg-bolt-elements-bg-depth-2 border border-transparent hover:border-bolt-elements-borderColor transition-all duration-200"
|
|
>
|
|
<div className="relative flex-shrink-0">
|
|
<div
|
|
className="w-12 h-12 rounded-xl shadow-md cursor-pointer transition-all duration-200 hover:scale-110 ring-2 ring-transparent hover:ring-bolt-elements-borderColorActive"
|
|
style={{ backgroundColor: palette[role.key] }}
|
|
onClick={() => document.getElementById(`color-input-${role.key}`)?.click()}
|
|
role="button"
|
|
tabIndex={0}
|
|
aria-label={`Change ${role.label} color`}
|
|
/>
|
|
<input
|
|
id={`color-input-${role.key}`}
|
|
type="color"
|
|
value={palette[role.key]}
|
|
onChange={(e) => handleColorChange(role.key, e.target.value)}
|
|
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
|
tabIndex={-1}
|
|
/>
|
|
<div className="absolute -bottom-1 -right-1 w-4 h-4 bg-bolt-elements-bg-depth-1 rounded-full flex items-center justify-center shadow-sm">
|
|
<span className="i-ph:pencil-simple text-xs text-bolt-elements-textSecondary" />
|
|
</div>
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="font-semibold text-bolt-elements-textPrimary transition-colors">{role.label}</div>
|
|
<div className="text-sm text-bolt-elements-textSecondary line-clamp-2 leading-relaxed">
|
|
{role.description}
|
|
</div>
|
|
<div className="text-xs text-bolt-elements-textTertiary font-mono mt-1 px-2 py-1 bg-bolt-elements-bg-depth-1 rounded-md inline-block">
|
|
{palette[role.key]}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const renderTypographySection = () => (
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-semibold text-bolt-elements-textPrimary flex items-center gap-2">
|
|
<div className="w-2 h-2 rounded-full bg-bolt-elements-item-contentAccent"></div>
|
|
Typography
|
|
</h3>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 max-h-80 overflow-y-auto pr-2 custom-scrollbar">
|
|
{designFonts.map((f) => (
|
|
<button
|
|
key={f.key}
|
|
type="button"
|
|
onClick={() => handleFontToggle(f.key)}
|
|
className={`group p-4 rounded-xl border-2 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-bolt-elements-borderColorActive ${
|
|
font.includes(f.key)
|
|
? 'bg-bolt-elements-item-backgroundAccent border-bolt-elements-borderColorActive shadow-lg'
|
|
: 'bg-bolt-elements-background-depth-3 border-bolt-elements-borderColor hover:border-bolt-elements-borderColorActive hover:bg-bolt-elements-bg-depth-2'
|
|
}`}
|
|
>
|
|
<div className="text-center space-y-2">
|
|
<div
|
|
className={`text-2xl font-medium transition-colors ${
|
|
font.includes(f.key) ? 'text-bolt-elements-item-contentAccent' : 'text-bolt-elements-textPrimary'
|
|
}`}
|
|
style={{ fontFamily: f.key }}
|
|
>
|
|
{f.preview}
|
|
</div>
|
|
<div
|
|
className={`text-sm font-medium transition-colors ${
|
|
font.includes(f.key) ? 'text-bolt-elements-item-contentAccent' : 'text-bolt-elements-textSecondary'
|
|
}`}
|
|
>
|
|
{f.label}
|
|
</div>
|
|
{font.includes(f.key) && (
|
|
<div className="w-6 h-6 mx-auto bg-bolt-elements-item-contentAccent rounded-full flex items-center justify-center">
|
|
<span className="i-ph:check text-white text-sm" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const renderFeaturesSection = () => (
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-semibold text-bolt-elements-textPrimary flex items-center gap-2">
|
|
<div className="w-2 h-2 rounded-full bg-bolt-elements-item-contentAccent"></div>
|
|
Design Features
|
|
</h3>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 max-h-80 overflow-y-auto pr-2 custom-scrollbar">
|
|
{designFeatures.map((f) => {
|
|
const isSelected = features.includes(f.key);
|
|
|
|
return (
|
|
<div key={f.key} className="feature-card-container p-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => handleFeatureToggle(f.key)}
|
|
className={`group relative w-full p-6 text-sm font-medium transition-all duration-200 bg-bolt-elements-background-depth-3 text-bolt-elements-item-textSecondary ${
|
|
f.key === 'rounded'
|
|
? isSelected
|
|
? 'rounded-3xl'
|
|
: 'rounded-xl'
|
|
: f.key === 'border'
|
|
? 'rounded-lg'
|
|
: 'rounded-xl'
|
|
} ${
|
|
f.key === 'border'
|
|
? isSelected
|
|
? 'border-3 border-bolt-elements-borderColorActive bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent'
|
|
: 'border-2 border-bolt-elements-borderColor hover:border-bolt-elements-borderColorActive text-bolt-elements-textSecondary'
|
|
: f.key === 'gradient'
|
|
? ''
|
|
: isSelected
|
|
? 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent shadow-lg'
|
|
: 'bg-bolt-elements-bg-depth-3 hover:bg-bolt-elements-bg-depth-2 text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary'
|
|
} ${f.key === 'shadow' ? (isSelected ? 'shadow-xl' : 'shadow-lg') : 'shadow-md'}`}
|
|
style={{
|
|
...(f.key === 'gradient' && {
|
|
background: isSelected
|
|
? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
|
|
: 'var(--bolt-elements-bg-depth-3)',
|
|
color: isSelected ? 'white' : 'var(--bolt-elements-textSecondary)',
|
|
}),
|
|
}}
|
|
>
|
|
<div className="flex flex-col items-center gap-4">
|
|
<div className="flex items-center justify-center w-12 h-12 rounded-xl bg-bolt-elements-bg-depth-1 bg-opacity-20">
|
|
{f.key === 'rounded' && (
|
|
<div
|
|
className={`w-6 h-6 bg-current transition-all duration-200 ${
|
|
isSelected ? 'rounded-full' : 'rounded'
|
|
} opacity-80`}
|
|
/>
|
|
)}
|
|
{f.key === 'border' && (
|
|
<div
|
|
className={`w-6 h-6 rounded-lg transition-all duration-200 ${
|
|
isSelected ? 'border-3 border-current opacity-90' : 'border-2 border-current opacity-70'
|
|
}`}
|
|
/>
|
|
)}
|
|
{f.key === 'gradient' && (
|
|
<div className="w-6 h-6 rounded-lg bg-gradient-to-br from-purple-400 via-pink-400 to-indigo-400 opacity-90" />
|
|
)}
|
|
{f.key === 'shadow' && (
|
|
<div className="relative">
|
|
<div
|
|
className={`w-6 h-6 bg-current rounded-lg transition-all duration-200 ${
|
|
isSelected ? 'opacity-90' : 'opacity-70'
|
|
}`}
|
|
/>
|
|
<div
|
|
className={`absolute top-1 left-1 w-6 h-6 bg-current rounded-lg transition-all duration-200 ${
|
|
isSelected ? 'opacity-40' : 'opacity-30'
|
|
}`}
|
|
/>
|
|
</div>
|
|
)}
|
|
{f.key === 'frosted-glass' && (
|
|
<div className="relative">
|
|
<div
|
|
className={`w-6 h-6 rounded-lg transition-all duration-200 backdrop-blur-sm bg-white/20 border border-white/30 ${
|
|
isSelected ? 'opacity-90' : 'opacity-70'
|
|
}`}
|
|
/>
|
|
<div
|
|
className={`absolute inset-0 w-6 h-6 rounded-lg transition-all duration-200 backdrop-blur-md bg-gradient-to-br from-white/10 to-transparent ${
|
|
isSelected ? 'opacity-60' : 'opacity-40'
|
|
}`}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="text-center">
|
|
<div className="font-semibold">{f.label}</div>
|
|
{isSelected && <div className="mt-2 w-8 h-1 bg-current rounded-full mx-auto opacity-60" />}
|
|
</div>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div>
|
|
<IconButton title="Design Palette" className="transition-all" onClick={() => setIsDialogOpen(!isDialogOpen)}>
|
|
<div className="i-ph:palette text-xl"></div>
|
|
</IconButton>
|
|
|
|
<DialogRoot open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
|
<Dialog>
|
|
<div className="py-4 px-4 min-w-[480px] max-w-[90vw] max-h-[85vh] flex flex-col gap-6 overflow-hidden">
|
|
<div className="">
|
|
<DialogTitle className="text-2xl font-bold text-bolt-elements-textPrimary">
|
|
Design Palette & Features
|
|
</DialogTitle>
|
|
<DialogDescription className="text-bolt-elements-textSecondary leading-relaxed">
|
|
Customize your color palette, typography, and design features. These preferences will guide the AI in
|
|
creating designs that match your style.
|
|
</DialogDescription>
|
|
</div>
|
|
|
|
{/* Navigation Tabs */}
|
|
<div className="flex gap-1 p-1 bg-bolt-elements-bg-depth-3 rounded-xl">
|
|
{[
|
|
{ key: 'colors', label: 'Colors', icon: 'i-ph:palette' },
|
|
{ key: 'typography', label: 'Typography', icon: 'i-ph:text-aa' },
|
|
{ key: 'features', label: 'Features', icon: 'i-ph:magic-wand' },
|
|
].map((tab) => (
|
|
<button
|
|
key={tab.key}
|
|
onClick={() => setActiveSection(tab.key as any)}
|
|
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-lg font-medium transition-all duration-200 ${
|
|
activeSection === tab.key
|
|
? 'bg-bolt-elements-background-depth-3 text-bolt-elements-textPrimary shadow-md'
|
|
: 'bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary hover:bg-bolt-elements-bg-depth-2'
|
|
}`}
|
|
>
|
|
<span className={`${tab.icon} text-lg`} />
|
|
<span>{tab.label}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Content Area */}
|
|
<div className=" min-h-92 overflow-y-auto">
|
|
{activeSection === 'colors' && renderColorSection()}
|
|
{activeSection === 'typography' && renderTypographySection()}
|
|
{activeSection === 'features' && renderFeaturesSection()}
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex justify-between items-center">
|
|
<div className="text-sm text-bolt-elements-textSecondary">
|
|
{Object.keys(palette).length} colors • {font.length} fonts • {features.length} features
|
|
</div>
|
|
<div className="flex gap-3">
|
|
<Button variant="secondary" onClick={() => setIsDialogOpen(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
onClick={handleSave}
|
|
className="bg-bolt-elements-button-primary-background hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-button-primary-text"
|
|
>
|
|
Save Changes
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Dialog>
|
|
</DialogRoot>
|
|
|
|
<style>{`
|
|
.custom-scrollbar {
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--bolt-elements-textTertiary) transparent;
|
|
}
|
|
.custom-scrollbar::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
.custom-scrollbar::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
background-color: var(--bolt-elements-textTertiary);
|
|
border-radius: 3px;
|
|
}
|
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
background-color: var(--bolt-elements-textSecondary);
|
|
}
|
|
.line-clamp-2 {
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
.feature-card-container {
|
|
min-height: 140px;
|
|
display: flex;
|
|
align-items: stretch;
|
|
}
|
|
.feature-card-container button {
|
|
flex: 1;
|
|
}
|
|
`}</style>
|
|
</div>
|
|
);
|
|
};
|