feat(ui): apply brand identity colors, typography, and contextual toolbar
This commit is contained in:
@@ -85,9 +85,9 @@ export const StudioProperties: React.FC<StudioPropertiesProps> = ({
|
||||
const isImageMode = outputFormat === 'image';
|
||||
|
||||
return (
|
||||
<aside className="w-72 bg-neutral-900 border-l border-neutral-800/60 flex flex-col z-10 shrink-0 h-full" onClick={(e) => e.stopPropagation()}>
|
||||
<aside className="w-72 bg-brand-blue/90 backdrop-blur-xl border-l border-brand-teal/20 flex flex-col z-10 shrink-0 h-full shadow-2xl shadow-brand-blue/50" onClick={(e) => e.stopPropagation()}>
|
||||
{/* Properties section */}
|
||||
<div className={isImageMode ? 'flex-1 min-h-0 flex flex-col border-b border-neutral-800' : 'flex-1 min-h-0 flex flex-col'}>
|
||||
<div className={isImageMode ? 'flex-1 min-h-0 flex flex-col border-b border-brand-teal/20' : 'flex-1 min-h-0 flex flex-col'}>
|
||||
{activeTool === 'transitions' ? (
|
||||
<TransitionsPanel designMD={designMD} />
|
||||
) : (selectedElementIds && selectedElementIds.size >= 2) ? (
|
||||
|
||||
@@ -241,6 +241,15 @@ export const ImageLayersPanel: React.FC<ImageLayersPanelProps> = ({
|
||||
{getLabel(el)}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
{el.isBrandElement ? (
|
||||
<span className="text-[8px] text-brand-orange bg-brand-orange/10 px-1 rounded uppercase tracking-wider font-semibold">
|
||||
🏷️ Marca
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-[8px] text-brand-teal border border-brand-teal/30 border-dashed px-1 rounded uppercase tracking-wider font-semibold">
|
||||
Editable
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[8px] text-neutral-600 uppercase tracking-wider font-semibold">
|
||||
{TYPE_LABELS[el.type] || el.type}
|
||||
</span>
|
||||
|
||||
@@ -19,6 +19,7 @@ import { TimelineMarkerList, TimelineMarker } from '../timeline/TimelineMarkerLi
|
||||
import { ResponsivePreviewToggle } from '../ui/ResponsivePreviewToggle';
|
||||
import { AutoSaveIndicator } from '../ui/AutoSaveIndicator';
|
||||
import { CanvasGridOverlay } from '../ui/CanvasGridOverlay';
|
||||
import { FloatingContextToolbar } from '../ui/FloatingContextToolbar';
|
||||
|
||||
import { useEditor } from '../../context/EditorContext';
|
||||
import { useKeyboardShortcuts } from '../../hooks/useKeyboardShortcuts';
|
||||
@@ -337,6 +338,16 @@ export const StudioEditor: React.FC<{ onAssetSaved?: (url: string) => void }> =
|
||||
</div>
|
||||
{/* Canvas Grid + Safe Zone Overlay */}
|
||||
<CanvasGridOverlay showGrid={showGrid} showSafeZone={showSafeZone} width={1080} height={1080} />
|
||||
|
||||
{/* Contextual Floating Toolbar */}
|
||||
<FloatingContextToolbar
|
||||
element={timelineElements.find(el => el.id === selectedElementId) || null}
|
||||
onDuplicate={handleDuplicate}
|
||||
onDelete={handleDelete}
|
||||
onLock={handleLock}
|
||||
setTimelineElements={setTimelineElements}
|
||||
/>
|
||||
|
||||
{/* Auto-save indicator */}
|
||||
<AutoSaveIndicator lastSaved={lastSaved} />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import { Wand2, Copy, Trash2, Lock, Unlock, AlignCenter, AlignVerticalSpaceAround } from 'lucide-react';
|
||||
import { TimelineElement } from '../../types';
|
||||
|
||||
import { removeImageBackground } from '../../utils/backgroundRemoval';
|
||||
|
||||
interface FloatingContextToolbarProps {
|
||||
element: TimelineElement | null;
|
||||
onDuplicate: (id: string) => void;
|
||||
onDelete: (id: string) => void;
|
||||
onLock: (id: string) => void;
|
||||
setTimelineElements: React.Dispatch<React.SetStateAction<TimelineElement[]>>;
|
||||
}
|
||||
|
||||
export const FloatingContextToolbar: React.FC<FloatingContextToolbarProps> = ({
|
||||
element,
|
||||
onDuplicate,
|
||||
onDelete,
|
||||
onLock,
|
||||
setTimelineElements,
|
||||
}) => {
|
||||
const [isRemovingBg, setIsRemovingBg] = React.useState(false);
|
||||
|
||||
if (!element) return null;
|
||||
|
||||
const handleRemoveBg = async () => {
|
||||
if (!element || element.type !== 'image') return;
|
||||
setIsRemovingBg(true);
|
||||
try {
|
||||
const newUrl = await removeImageBackground(element.content);
|
||||
setTimelineElements(prev => prev.map(el =>
|
||||
el.id === element.id ? { ...el, content: newUrl } : el
|
||||
));
|
||||
} catch (error) {
|
||||
alert('Error al extraer el sujeto. Inténtalo de nuevo.');
|
||||
} finally {
|
||||
setIsRemovingBg(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||
className="absolute bottom-4 left-1/2 -translate-x-1/2 z-50 flex items-center gap-1 p-1 bg-brand-blue/80 backdrop-blur-xl border border-brand-teal/20 rounded-xl shadow-2xl"
|
||||
>
|
||||
{element.type === 'image' && (
|
||||
<button
|
||||
onClick={handleRemoveBg}
|
||||
disabled={isRemovingBg}
|
||||
title="Extracción IA (Smart Mask)"
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
|
||||
isRemovingBg
|
||||
? 'text-neutral-400 bg-neutral-800 cursor-wait'
|
||||
: 'text-white bg-brand-teal hover:bg-brand-teal/80 shadow-lg shadow-brand-teal/20'
|
||||
}`}
|
||||
>
|
||||
<Wand2 className={`w-3.5 h-3.5 ${isRemovingBg ? 'animate-pulse' : ''}`} />
|
||||
{isRemovingBg ? 'Extrayendo...' : 'Extracción IA'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="w-px h-4 bg-brand-gray/20 mx-1" />
|
||||
|
||||
<button
|
||||
onClick={() => onDuplicate(element.id)}
|
||||
title="Duplicar elemento"
|
||||
className="p-1.5 text-brand-gray hover:text-white hover:bg-white/10 rounded-lg transition-colors"
|
||||
>
|
||||
<Copy className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onLock(element.id)}
|
||||
title={element.isLocked ? "Desbloquear" : "Bloquear"}
|
||||
className="p-1.5 text-brand-gray hover:text-white hover:bg-white/10 rounded-lg transition-colors"
|
||||
>
|
||||
{element.isLocked ? <Lock className="w-4 h-4 text-brand-orange" /> : <Unlock className="w-4 h-4" />}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onDelete(element.id)}
|
||||
title="Eliminar elemento"
|
||||
className="p-1.5 text-brand-gray hover:text-red-400 hover:bg-red-400/10 rounded-lg transition-colors"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user