feat(ui): apply brand identity colors, typography, and contextual toolbar
This commit is contained in:
+4
-1
@@ -3,7 +3,10 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Remix — Editor de Branding Automatizado</title>
|
<title>Bradly — Especialista en Contenido de Marca</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
<meta name="description" content="Plataforma SaaS para automatizar el branding visual de tu empresa. Crea videos e imágenes de marca con un sistema de diseño estricto usando Remotion." />
|
<meta name="description" content="Plataforma SaaS para automatizar el branding visual de tu empresa. Crea videos e imágenes de marca con un sistema de diseño estricto usando Remotion." />
|
||||||
<meta property="og:title" content="Remix — Editor de Branding Automatizado" />
|
<meta property="og:title" content="Remix — Editor de Branding Automatizado" />
|
||||||
<meta property="og:description" content="Automatiza tu branding visual con Design MD y Remotion." />
|
<meta property="og:description" content="Automatiza tu branding visual con Design MD y Remotion." />
|
||||||
|
|||||||
@@ -85,9 +85,9 @@ export const StudioProperties: React.FC<StudioPropertiesProps> = ({
|
|||||||
const isImageMode = outputFormat === 'image';
|
const isImageMode = outputFormat === 'image';
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* 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' ? (
|
{activeTool === 'transitions' ? (
|
||||||
<TransitionsPanel designMD={designMD} />
|
<TransitionsPanel designMD={designMD} />
|
||||||
) : (selectedElementIds && selectedElementIds.size >= 2) ? (
|
) : (selectedElementIds && selectedElementIds.size >= 2) ? (
|
||||||
|
|||||||
@@ -241,6 +241,15 @@ export const ImageLayersPanel: React.FC<ImageLayersPanelProps> = ({
|
|||||||
{getLabel(el)}
|
{getLabel(el)}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-2 mt-0.5">
|
<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">
|
<span className="text-[8px] text-neutral-600 uppercase tracking-wider font-semibold">
|
||||||
{TYPE_LABELS[el.type] || el.type}
|
{TYPE_LABELS[el.type] || el.type}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { TimelineMarkerList, TimelineMarker } from '../timeline/TimelineMarkerLi
|
|||||||
import { ResponsivePreviewToggle } from '../ui/ResponsivePreviewToggle';
|
import { ResponsivePreviewToggle } from '../ui/ResponsivePreviewToggle';
|
||||||
import { AutoSaveIndicator } from '../ui/AutoSaveIndicator';
|
import { AutoSaveIndicator } from '../ui/AutoSaveIndicator';
|
||||||
import { CanvasGridOverlay } from '../ui/CanvasGridOverlay';
|
import { CanvasGridOverlay } from '../ui/CanvasGridOverlay';
|
||||||
|
import { FloatingContextToolbar } from '../ui/FloatingContextToolbar';
|
||||||
|
|
||||||
import { useEditor } from '../../context/EditorContext';
|
import { useEditor } from '../../context/EditorContext';
|
||||||
import { useKeyboardShortcuts } from '../../hooks/useKeyboardShortcuts';
|
import { useKeyboardShortcuts } from '../../hooks/useKeyboardShortcuts';
|
||||||
@@ -337,6 +338,16 @@ export const StudioEditor: React.FC<{ onAssetSaved?: (url: string) => void }> =
|
|||||||
</div>
|
</div>
|
||||||
{/* Canvas Grid + Safe Zone Overlay */}
|
{/* Canvas Grid + Safe Zone Overlay */}
|
||||||
<CanvasGridOverlay showGrid={showGrid} showSafeZone={showSafeZone} width={1080} height={1080} />
|
<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 */}
|
{/* Auto-save indicator */}
|
||||||
<AutoSaveIndicator lastSaved={lastSaved} />
|
<AutoSaveIndicator lastSaved={lastSaved} />
|
||||||
</div>
|
</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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,14 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-brand-blue: #0D2C54;
|
||||||
|
--color-brand-teal: #00A69C;
|
||||||
|
--color-brand-orange: #F9A826;
|
||||||
|
--color-brand-gray: #E0E0E0;
|
||||||
|
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Brand Content Editor input utility */
|
/* Brand Content Editor input utility */
|
||||||
.input-sm {
|
.input-sm {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
Reference in New Issue
Block a user