feat: add element inspector with chat integration

- Implement element inspector tool for preview iframe with hover/click detection
- Add inspector panel UI to display element details and styles
- Integrate selected elements into chat messages for reference
- Style improvements for chat messages and scroll behavior
- Add inspector script injection to preview iframe
- Support element selection and context in chat prompts
-Redesign Messgaes, Workbench and Header for a more refined look allowing more workspace in view
This commit is contained in:
KevIsDev
2025-05-30 13:16:53 +01:00
parent 6c4b4204e3
commit 5838d7121a
20 changed files with 1114 additions and 333 deletions

View File

@@ -20,8 +20,9 @@ export const ColorSchemeDialog: React.FC<ColorSchemeDialogProps> = ({ setDesignS
});
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) {
@@ -29,7 +30,6 @@ export const ColorSchemeDialog: React.FC<ColorSchemeDialogProps> = ({ setDesignS
setFeatures(designScheme.features || defaultDesignScheme.features);
setFont(designScheme.font || defaultDesignScheme.font);
} else {
// Reset to defaults if no designScheme provided
setPalette(defaultDesignScheme.palette);
setFeatures(defaultDesignScheme.features);
setFont(defaultDesignScheme.font);
@@ -37,10 +37,7 @@ export const ColorSchemeDialog: React.FC<ColorSchemeDialogProps> = ({ setDesignS
}, [designScheme]);
const handleColorChange = (role: string, value: string) => {
setPalette((prev) => ({
...prev,
[role]: value,
}));
setPalette((prev) => ({ ...prev, [role]: value }));
};
const handleFeatureToggle = (key: string) => {
@@ -51,8 +48,6 @@ export const ColorSchemeDialog: React.FC<ColorSchemeDialogProps> = ({ setDesignS
setFont((prev) => (prev.includes(key) ? prev.filter((f) => f !== key) : [...prev, key]));
};
const [isDialogOpen, setIsDialogOpen] = useState(false);
const handleSave = () => {
setDesignScheme?.({ palette, features, font });
setIsDialogOpen(false);
@@ -64,210 +59,306 @@ export const ColorSchemeDialog: React.FC<ColorSchemeDialogProps> = ({ setDesignS
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>
)}
</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="Upload file" className="transition-all" onClick={() => setIsDialogOpen(!isDialogOpen)}>
<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="p-8 min-w-[380px] max-w-[95vw]">
<DialogTitle className="mb-2 text-lg font-bold">Design Palette & Features</DialogTitle>
<DialogDescription className="mb-6 text-sm text-bolt-elements-textPrimary">
Choose your color palette, typography, and key design features. These will be used as design instructions
for the LLM.
</DialogDescription>
<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>
<div className="mb-5">
<div className="w-full flex justify-between items-center mb-3">
<span className="font-semibold text-sm text-bolt-elements-textPrimary">Color Palette</span>
{/* 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
onClick={handleReset}
className="text-xs bg-transparent text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary flex items-center gap-1 transition-colors"
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="i-ph:arrow-clockwise" />
Reset to defaults
<span className={`${tab.icon} text-lg`} />
<span>{tab.label}</span>
</button>
</div>
<div className="grid grid-cols-2 gap-4 max-h-48 overflow-y-auto">
{paletteRoles.map((role) => (
<div
key={role.key}
className="flex items-center gap-3 p-2 rounded-lg bg-bolt-elements-background-depth-3 hover:bg-bolt-elements-background-depth-2"
>
<div className="relative flex-shrink-0">
<div
className="w-10 h-10 rounded-lg shadow-sm cursor-pointer hover:scale-105 transition-transform"
style={{ backgroundColor: palette[role.key] }}
onClick={() => document.getElementById(`color-input-${role.key}`)?.click()}
/>
<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>
<div className="flex-1 min-w-0">
<div className="font-medium text-sm text-bolt-elements-textPrimary">{role.label}</div>
<div className="text-xs text-bolt-elements-textSecondary truncate">{role.description}</div>
<div className="text-xs text-bolt-elements-textSecondary opacity-50 font-mono">
{palette[role.key]}
</div>
</div>
</div>
))}
</div>
))}
</div>
<div className="mb-5">
<div className="w-full flex justify-between items-center mb-3">
<span className="font-semibold text-sm text-bolt-elements-textPrimary">Typography</span>
<span className="text-xs text-bolt-elements-textSecondary flex items-center gap-1">
<span className="i-ph:arrow-right" />
Scroll for more
</span>
</div>
<div className="flex gap-3 overflow-x-auto pb-2 px-0.5">
{designFonts.map((f) => (
<button
key={f.key}
type="button"
onClick={() => handleFontToggle(f.key)}
className={`flex-shrink-0 px-4 py-2 rounded-lg border text-xs font-medium transition-colors shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-300 flex items-center gap-2 min-w-[120px] ${font.includes(f.key) ? 'bg-blue-100 border-blue-400 text-blue-700' : 'bg-gray-50 border-gray-200 text-gray-500 hover:bg-blue-50 hover:border-blue-300'}`}
style={{ fontFamily: f.key }}
>
<span className="text-lg" style={{ fontFamily: f.key }}>
{f.preview}
</span>
<span>{f.label}</span>
</button>
))}
</div>
{/* Content Area */}
<div className=" min-h-92 overflow-y-auto">
{activeSection === 'colors' && renderColorSection()}
{activeSection === 'typography' && renderTypographySection()}
{activeSection === 'features' && renderFeaturesSection()}
</div>
<div className="mb-6">
<div className="w-full flex justify-between items-center mb-3">
<span className="font-semibold text-sm text-bolt-elements-textPrimary">Design Features</span>
<span className="text-xs text-bolt-elements-textSecondary flex items-center gap-1">
<span className="i-ph:arrow-right" />
Scroll for more
</span>
{/* 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-4 overflow-x-auto pb-2 px-0.5">
{designFeatures.map((f) => {
const isSelected = features.includes(f.key);
return (
<button
key={f.key}
type="button"
onClick={() => handleFeatureToggle(f.key)}
className={`
group relative px-4 py-2 text-xs font-medium transition-all duration-300
focus:outline-none focus:ring-2 focus:ring-purple-300 cursor-pointer
transform hover:scale-105 active:scale-95 flex-shrink-0 min-w-[140px]
${
f.key === 'rounded'
? isSelected
? 'rounded-2xl'
: 'rounded-lg hover:rounded-xl'
: f.key === 'border'
? 'rounded-md'
: 'rounded-lg'
}
${
f.key === 'border'
? isSelected
? 'border-2 border-purple-400 bg-purple-50'
: 'border-2 border-gray-200 hover:border-purple-300 bg-white'
: f.key === 'gradient'
? ''
: isSelected
? 'bg-purple-100 text-purple-700'
: 'bg-gray-50 hover:bg-purple-50 text-gray-600 hover:text-purple-600'
}
${
f.key === 'shadow'
? isSelected
? 'shadow-md shadow-purple-200'
: 'shadow-md hover:shadow-lg'
: 'shadow-sm hover:shadow-md'
}
`}
style={{
...(f.key === 'gradient' && {
background: isSelected
? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
: 'linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%)',
color: isSelected ? 'white' : '#6b7280',
}),
}}
>
{/* Feature preview area */}
<div className="flex items-center gap-3">
{/* Visual preview */}
<div className="flex items-center justify-center w-6 h-6">
{f.key === 'rounded' && (
<div
className={`w-4 h-4 bg-current transition-all duration-300 ${
isSelected ? 'rounded-full' : 'rounded-sm group-hover:rounded-lg'
} opacity-70`}
></div>
)}
{f.key === 'border' && (
<div
className={`w-4 h-4 rounded transition-all duration-300 ${
isSelected
? 'border-2 border-current opacity-80'
: 'border border-current opacity-60 group-hover:border-2'
}`}
></div>
)}
{f.key === 'gradient' && (
<div className="w-4 h-4 rounded-sm bg-gradient-to-br from-purple-400 via-pink-400 to-indigo-400 opacity-90 transition-all duration-300 group-hover:scale-110"></div>
)}
{f.key === 'shadow' && (
<div className="relative">
<div
className={`w-4 h-4 bg-current rounded transition-all duration-300 ${
isSelected ? 'opacity-80' : 'opacity-60'
}`}
></div>
<div
className={`absolute top-0.5 left-0.5 w-4 h-4 bg-current rounded transition-all duration-300 ${
isSelected ? 'opacity-30' : 'opacity-20'
}`}
></div>
</div>
)}
</div>
{/* Label */}
<span className="transition-all duration-300">{f.label}</span>
</div>
{/* Hover effect overlay (might replace this) */}
<div className="absolute inset-0 bg-purple-400 opacity-0 group-hover:opacity-5 transition-opacity duration-300 rounded-inherit"></div>
</button>
);
})}
<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 className="flex justify-end gap-2">
<Button variant="secondary" onClick={() => setIsDialogOpen(false)}>
Cancel
</Button>
<Button variant="ghost" onClick={handleSave}>
Save
</Button>
</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>
);
};