feat: centralizar ADN y estilo de marca en Zona de Marca y simplificar panel de creatividades a operativa ads

This commit is contained in:
Marcos Fargas
2026-10-14 14:51:41 +02:00
parent feef339c2b
commit aed73e0171
6 changed files with 2483 additions and 1425 deletions

View File

@@ -29,6 +29,9 @@ import {
type BrandbookSectionId,
type ColorToken,
type FontToken,
type CreativeB2BPersona,
type CreativeB2CPersona,
type CreativeObjection,
} from '@/lib/brandbook';
import GoogleFontsGallery from '@/components/GoogleFontsGallery';
import BrandOverview from '@/components/BrandOverview';
@@ -632,6 +635,58 @@ export default function BrandZone({ tenantLabel }: { tenantLabel: string }) {
onChange={(e) => patch({ origin_story: e.target.value })}
/>
</Field>
<Field label="Sitio Web">
<input
className={inputCls}
value={book.website || ''}
placeholder="ej. blueparrotschool.com"
onChange={(e) => patch({ website: e.target.value })}
/>
</Field>
<Field
label="Oferta y programas B2B (empresas)"
hint="Programas in-company, formación equipos, acreditaciones oficiales"
className="sm:col-span-2"
>
<textarea
className={areaCls}
value={book.b2b_offering || ''}
onChange={(e) => patch({ b2b_offering: e.target.value })}
/>
</Field>
<Field
label="Oferta y cursos B2C (particulares)"
hint="Cursos adultos, intensivos, certificaciones Cambridge/Aptis, 1to1"
className="sm:col-span-2"
>
<textarea
className={areaCls}
value={book.b2c_offering || ''}
onChange={(e) => patch({ b2c_offering: e.target.value })}
/>
</Field>
<Field
label="Diferenciación clave (USPs frente a competencia)"
hint="Profesor fijo, sesiones completas, acreditación directa FUNDAE..."
className="sm:col-span-2"
>
<textarea
className={areaCls}
value={book.usp_differentiators || ''}
onChange={(e) => patch({ usp_differentiators: e.target.value })}
/>
</Field>
<Field
label="Prioridades actuales y foco comercial"
hint="Foco de captación prioritario para este periodo"
className="sm:col-span-2"
>
<textarea
className={areaCls}
value={book.current_priorities || ''}
onChange={(e) => patch({ current_priorities: e.target.value })}
/>
</Field>
</div>
)}
@@ -725,6 +780,48 @@ export default function BrandZone({ tenantLabel }: { tenantLabel: string }) {
}
/>
</Field>
<Field
label="Estructura y reglas de redacción (Copy Ads & Web)"
hint="Longitud, número de frases, estructura hook -> beneficio -> CTA"
className="sm:col-span-2"
>
<textarea
className={areaCls}
value={book.copy_structure_rules || ''}
placeholder="ej. Frases cortas. Un dato concreto vale más que un adjetivo. En primary_text: 2-4 frases, máx. 300 caracteres..."
onChange={(e) => patch({ copy_structure_rules: e.target.value })}
/>
</Field>
<Field
label="Política de términos en inglés y anglicismos"
hint="Términos admitidos vs anglicismos prohibidos tipo 'boost' o 'level up'"
className="sm:col-span-2"
>
<textarea
className={areaCls}
value={book.english_terms_policy || ''}
placeholder="ej. Inglés en el copy: solo nombres propios de exámenes o términos sin traducción natural..."
onChange={(e) => patch({ english_terms_policy: e.target.value })}
/>
</Field>
<Field label="Conceptos fuerza (Power Words)" hint="Uno por línea">
<textarea
className={areaCls}
value={listToLines(book.power_words || [])}
onChange={(e) =>
patch({ power_words: linesToList(e.target.value) })
}
/>
</Field>
<Field label="CTAs permitidos para Ads / Web" hint="Uno por línea">
<textarea
className={areaCls}
value={listToLines(book.allowed_ctas || [])}
onChange={(e) =>
patch({ allowed_ctas: linesToList(e.target.value) })
}
/>
</Field>
</div>
)}
@@ -1031,6 +1128,38 @@ export default function BrandZone({ tenantLabel }: { tenantLabel: string }) {
}
/>
</Field>
<Field label="Uso de superficies y combinaciones" className="sm:col-span-2">
<textarea
className={areaCls}
value={book.surface_rules || ''}
placeholder="ej. Azul (#2A4D9C) y blanco (#FFFFFF) como base. Amarillo (#FBC02D) solo para resaltar una palabra o CTA..."
onChange={(e) => patch({ surface_rules: e.target.value })}
/>
</Field>
<Field label="Tipografía titulares (Hook)">
<input
className={inputCls}
value={book.typography_headline || ''}
placeholder="ej. Poppins Extrabold"
onChange={(e) => patch({ typography_headline: e.target.value })}
/>
</Field>
<Field label="Tipografía cuerpo / apoyo (Support)">
<input
className={inputCls}
value={book.typography_body || ''}
placeholder="ej. Poppins Regular"
onChange={(e) => patch({ typography_body: e.target.value })}
/>
</Field>
<Field label="Reglas de texto en imagen" className="sm:col-span-2">
<textarea
className={areaCls}
value={book.image_text_rules || ''}
placeholder="ej. Máximo 2 niveles (titular ≤ 8 palabras + apoyo ≤ 12 palabras). Margen mínimo del 8% en todos los lados..."
onChange={(e) => patch({ image_text_rules: e.target.value })}
/>
</Field>
</div>
<div>
@@ -1181,6 +1310,274 @@ export default function BrandZone({ tenantLabel }: { tenantLabel: string }) {
onChange={(e) => patch({ customer_journey: e.target.value })}
/>
</Field>
<Field
label="Frustración principal previa de la audiencia"
hint="Por qué han fracasado con otras alternativas o en qué punto están atascados"
className="sm:col-span-2"
>
<textarea
className={areaCls}
value={book.customer_frustrations || ''}
placeholder="ej. Estancamiento en el nivel intermedio eterno por apps de 15 minutos sin profesor fijo..."
onChange={(e) => patch({ customer_frustrations: e.target.value })}
/>
</Field>
<Field
label="Transformación deseada (Antes vs Después)"
hint="De qué estado evitado pasan a qué resultado concreto"
className="sm:col-span-2"
>
<textarea
className={areaCls}
value={book.desired_transformation || ''}
placeholder="ej. Pasar de evitar hablar en inglés o depender de traductores a negociar con seguridad..."
onChange={(e) => patch({ desired_transformation: e.target.value })}
/>
</Field>
{/* B2B Personas Card Editor */}
<div className="sm:col-span-2 space-y-3 pt-3 border-t border-gray-200">
<div className="flex items-center justify-between">
<div>
<h4 className="text-xs font-bold uppercase tracking-wider text-gray-800">
Buyer Personas B2B (Empresas / Directivos)
</h4>
<p className="text-[11px] text-gray-500">
Cargos, dolores de gestión y disparadores de compra corporativos.
</p>
</div>
<button
type="button"
onClick={() => {
const list = [...(book.b2b_personas || [])];
list.push({
name: `Persona B2B #${list.length + 1}`,
role: '',
goal: '',
pain_point: '',
buying_trigger: '',
});
patch({ b2b_personas: list });
}}
className="inline-flex items-center gap-1 rounded-lg border border-gray-300 bg-white px-2.5 py-1 text-xs font-semibold text-gray-700 hover:bg-gray-50 shadow-sm"
>
<Plus className="w-3.5 h-3.5 text-gray-500" />
Añadir B2B
</button>
</div>
{(!book.b2b_personas || book.b2b_personas.length === 0) ? (
<p className="text-xs text-gray-400 italic">No hay buyer personas B2B definidas.</p>
) : (
<div className="grid gap-3 sm:grid-cols-2">
{book.b2b_personas.map((p, idx) => (
<div key={idx} className="rounded-xl border border-gray-200 bg-gray-50/50 p-3 space-y-2">
<div className="flex items-center justify-between">
<input
type="text"
value={p.name}
onChange={(e) => {
const list = [...book.b2b_personas!];
list[idx] = { ...list[idx], name: e.target.value };
patch({ b2b_personas: list });
}}
placeholder="Nombre / Identificador"
className="font-bold text-xs text-gray-900 bg-transparent border-b border-dashed border-gray-300 focus:outline-none focus:border-brand-500 px-1 py-0.5"
/>
<button
type="button"
onClick={() => {
const list = book.b2b_personas!.filter((_, i) => i !== idx);
patch({ b2b_personas: list });
}}
className="text-gray-400 hover:text-red-500 p-1"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
<div>
<label className="text-[10px] font-semibold text-gray-500 block">Cargo / Rol</label>
<input
type="text"
value={p.role}
onChange={(e) => {
const list = [...book.b2b_personas!];
list[idx] = { ...list[idx], role: e.target.value };
patch({ b2b_personas: list });
}}
placeholder="ej. Responsable de RRHH o Talento"
className="w-full text-xs rounded border border-gray-200 bg-white px-2 py-1"
/>
</div>
<div>
<label className="text-[10px] font-semibold text-gray-500 block">Objetivo</label>
<input
type="text"
value={p.goal}
onChange={(e) => {
const list = [...book.b2b_personas!];
list[idx] = { ...list[idx], goal: e.target.value };
patch({ b2b_personas: list });
}}
placeholder="ej. Proveedor llave en mano sin carga administrativa"
className="w-full text-xs rounded border border-gray-200 bg-white px-2 py-1"
/>
</div>
<div>
<label className="text-[10px] font-semibold text-gray-500 block">Dolor / Frustración</label>
<textarea
value={p.pain_point}
onChange={(e) => {
const list = [...book.b2b_personas!];
list[idx] = { ...list[idx], pain_point: e.target.value };
patch({ b2b_personas: list });
}}
placeholder="ej. Papeleo FUNDAE, quejas por rotación de profes..."
className="w-full text-xs rounded border border-gray-200 bg-white px-2 py-1 min-h-[50px]"
/>
</div>
<div>
<label className="text-[10px] font-semibold text-gray-500 block">Disparador de Compra</label>
<input
type="text"
value={p.buying_trigger}
onChange={(e) => {
const list = [...book.b2b_personas!];
list[idx] = { ...list[idx], buying_trigger: e.target.value };
patch({ b2b_personas: list });
}}
placeholder="ej. Gestión FUNDAE directa 100% resuelta"
className="w-full text-xs rounded border border-gray-200 bg-white px-2 py-1"
/>
</div>
</div>
))}
</div>
)}
</div>
{/* B2C Personas Card Editor */}
<div className="sm:col-span-2 space-y-3 pt-3 border-t border-gray-200">
<div className="flex items-center justify-between">
<div>
<h4 className="text-xs font-bold uppercase tracking-wider text-gray-800">
Buyer Personas B2C (Particulares / Alumnos)
</h4>
<p className="text-[11px] text-gray-500">
Segmentos de particulares, edad, situación vital, dolor y meta.
</p>
</div>
<button
type="button"
onClick={() => {
const list = [...(book.b2c_personas || [])];
list.push({
name: `Persona B2C #${list.length + 1}`,
age_range: '',
situation: '',
pain_point: '',
goal: '',
});
patch({ b2c_personas: list });
}}
className="inline-flex items-center gap-1 rounded-lg border border-gray-300 bg-white px-2.5 py-1 text-xs font-semibold text-gray-700 hover:bg-gray-50 shadow-sm"
>
<Plus className="w-3.5 h-3.5 text-gray-500" />
Añadir B2C
</button>
</div>
{(!book.b2c_personas || book.b2c_personas.length === 0) ? (
<p className="text-xs text-gray-400 italic">No hay buyer personas B2C definidas.</p>
) : (
<div className="grid gap-3 sm:grid-cols-2">
{book.b2c_personas.map((p, idx) => (
<div key={idx} className="rounded-xl border border-gray-200 bg-gray-50/50 p-3 space-y-2">
<div className="flex items-center justify-between">
<input
type="text"
value={p.name}
onChange={(e) => {
const list = [...book.b2c_personas!];
list[idx] = { ...list[idx], name: e.target.value };
patch({ b2c_personas: list });
}}
placeholder="Perfil / Segmento"
className="font-bold text-xs text-gray-900 bg-transparent border-b border-dashed border-gray-300 focus:outline-none focus:border-brand-500 px-1 py-0.5"
/>
<button
type="button"
onClick={() => {
const list = book.b2c_personas!.filter((_, i) => i !== idx);
patch({ b2c_personas: list });
}}
className="text-gray-400 hover:text-red-500 p-1"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="text-[10px] font-semibold text-gray-500 block">Rango de Edad</label>
<input
type="text"
value={p.age_range}
onChange={(e) => {
const list = [...book.b2c_personas!];
list[idx] = { ...list[idx], age_range: e.target.value };
patch({ b2c_personas: list });
}}
placeholder="ej. 25-45 años"
className="w-full text-xs rounded border border-gray-200 bg-white px-2 py-1"
/>
</div>
<div>
<label className="text-[10px] font-semibold text-gray-500 block">Situación</label>
<input
type="text"
value={p.situation}
onChange={(e) => {
const list = [...book.b2c_personas!];
list[idx] = { ...list[idx], situation: e.target.value };
patch({ b2c_personas: list });
}}
placeholder="ej. Opositor / Empleado"
className="w-full text-xs rounded border border-gray-200 bg-white px-2 py-1"
/>
</div>
</div>
<div>
<label className="text-[10px] font-semibold text-gray-500 block">Dolor / Frustración</label>
<textarea
value={p.pain_point}
onChange={(e) => {
const list = [...book.b2c_personas!];
list[idx] = { ...list[idx], pain_point: e.target.value };
patch({ b2c_personas: list });
}}
placeholder="ej. Ha fracasado con apps sin feedback ni estructura..."
className="w-full text-xs rounded border border-gray-200 bg-white px-2 py-1 min-h-[50px]"
/>
</div>
<div>
<label className="text-[10px] font-semibold text-gray-500 block">Meta / Objetivo</label>
<input
type="text"
value={p.goal}
onChange={(e) => {
const list = [...book.b2c_personas!];
list[idx] = { ...list[idx], goal: e.target.value };
patch({ b2c_personas: list });
}}
placeholder="ej. Certificar B2/C1 Cambridge en 6 meses"
className="w-full text-xs rounded border border-gray-200 bg-white px-2 py-1"
/>
</div>
</div>
))}
</div>
)}
</div>
</div>
)}
@@ -1256,6 +1653,18 @@ export default function BrandZone({ tenantLabel }: { tenantLabel: string }) {
onChange={(e) => patch({ pricing_notes: e.target.value })}
/>
</Field>
<Field
label="Enfoque de precio / oferta para Ads y Funnel"
hint="Coste neto, bonificación FUNDAE, evitar descuentos agresivos..."
className="sm:col-span-2"
>
<textarea
className={areaCls}
value={book.price_and_offer_framing || ''}
placeholder="ej. En B2B destacar el coste bonificado vía FUNDAE... En B2C destacar el valor del programa sin descuentos agresivos..."
onChange={(e) => patch({ price_and_offer_framing: e.target.value })}
/>
</Field>
</div>
)}
@@ -1473,6 +1882,116 @@ export default function BrandZone({ tenantLabel }: { tenantLabel: string }) {
onChange={(e) => patch({ landing_notes: e.target.value })}
/>
</Field>
<Field
label="Hooks ganadores de referencia"
hint="Ejemplos reales que han tenido buen CTR / conversión (uno por línea, para referencia de estilo)"
className="sm:col-span-2"
>
<textarea
className={areaCls}
value={listToLines(book.winning_hooks || [])}
placeholder="ej. Tu equipo no necesita más inglés. Necesita el inglés de su trabajo..."
onChange={(e) =>
patch({ winning_hooks: linesToList(e.target.value) })
}
/>
</Field>
<Field
label="Ángulos recomendados para rotar en campañas"
hint="Ángulos prioritarios para Claude (uno por línea)"
className="sm:col-span-2"
>
<textarea
className={areaCls}
value={listToLines(book.preferred_angles || [])}
placeholder="ej. objeción tiempo, coste FUNDAE, contraste app vs programa real, especialización sectorial..."
onChange={(e) =>
patch({ preferred_angles: linesToList(e.target.value) })
}
/>
</Field>
{/* Objeciones a desactivar con Rebuttal */}
<div className="sm:col-span-2 space-y-3 pt-3 border-t border-gray-200">
<div className="flex items-center justify-between">
<div>
<h4 className="text-xs font-bold uppercase tracking-wider text-gray-800">
Objeciones a Desactivar (Objeción → Argumento de Refutación)
</h4>
<p className="text-[11px] text-gray-500">
Utilizadas por Claude para redactar copys de consideración y remarketing que resuelvan dudas del lead.
</p>
</div>
<button
type="button"
onClick={() => {
const list = [...(book.objections || [])];
list.push({ objection: '', rebuttal: '' });
patch({ objections: list });
}}
className="inline-flex items-center gap-1 rounded-lg border border-gray-300 bg-white px-2.5 py-1 text-xs font-semibold text-gray-700 hover:bg-gray-50 shadow-sm"
>
<Plus className="w-3.5 h-3.5 text-gray-500" />
Añadir Objeción
</button>
</div>
{(!book.objections || book.objections.length === 0) ? (
<p className="text-xs text-gray-400 italic">No hay objeciones estructuradas definidas.</p>
) : (
<div className="space-y-2">
{book.objections.map((o, idx) => (
<div
key={idx}
className="flex flex-col sm:flex-row items-start sm:items-center gap-2 rounded-xl border border-gray-200 bg-gray-50/60 p-2.5"
>
<div className="flex-1 w-full sm:w-auto">
<label className="text-[10px] font-semibold text-gray-500 block mb-0.5">
Objeción del cliente
</label>
<input
type="text"
value={o.objection}
onChange={(e) => {
const list = [...book.objections!];
list[idx] = { ...list[idx], objection: e.target.value };
patch({ objections: list });
}}
placeholder='ej. "Ya usamos una app"'
className="w-full text-xs rounded border border-gray-200 bg-white px-2 py-1.5"
/>
</div>
<div className="flex-[2] w-full sm:w-auto">
<label className="text-[10px] font-semibold text-gray-500 block mb-0.5">
Respuesta / Refutación
</label>
<input
type="text"
value={o.rebuttal}
onChange={(e) => {
const list = [...book.objections!];
list[idx] = { ...list[idx], rebuttal: e.target.value };
patch({ objections: list });
}}
placeholder='ej. "La app mantiene, no avanza. Sin profesor fijo ni sesiones de 60 min no se sale del nivel intermedio."'
className="w-full text-xs rounded border border-gray-200 bg-white px-2 py-1.5"
/>
</div>
<button
type="button"
onClick={() => {
const list = book.objections!.filter((_, i) => i !== idx);
patch({ objections: list });
}}
className="self-end sm:self-center mt-2 sm:mt-4 text-gray-400 hover:text-red-500 p-1"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
))}
</div>
)}
</div>
</div>
)}
@@ -1634,6 +2153,42 @@ export default function BrandZone({ tenantLabel }: { tenantLabel: string }) {
onChange={(e) => patch({ image_rights: e.target.value })}
/>
</Field>
<Field
label="Competidores que NUNCA se deben nombrar"
hint="Marcas de la competencia totalmente vetadas en copy y claims (uno por línea)"
className="sm:col-span-2"
>
<textarea
className={areaCls}
value={listToLines(book.competitors_never_mention || [])}
placeholder="ej. Twenix, Preply, Italki, Open English, What's Up!..."
onChange={(e) =>
patch({ competitors_never_mention: linesToList(e.target.value) })
}
/>
</Field>
<Field
label="Política de Emojis"
hint="Normas sobre el uso de emojis en creatividades y copies"
>
<textarea
className={areaCls}
value={book.emoji_policy || ''}
placeholder="ej. Prohibidos los emojis en creatividades de imagen. En copy, como mucho uno al inicio solo en B2C..."
onChange={(e) => patch({ emoji_policy: e.target.value })}
/>
</Field>
<Field
label="Política de Testimonios y Casos de Estudio"
hint="Normas para evitar testimonios falsos o no autorizados"
>
<textarea
className={areaCls}
value={book.testimonials_policy || ''}
placeholder="ej. Prohibidos testimonios inventados. Si no hay un caso real verificado, no se usa..."
onChange={(e) => patch({ testimonials_policy: e.target.value })}
/>
</Field>
</div>
)}