fix(ui): paneles Zona de marca tipo Overview/Prohibido

Cards con eyebrow, tipografía sm legible, más aire y agrupación por
bloques en Identidad, Voz, Visual, Ads, Canales y Compliance.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-10-15 03:54:48 +02:00
parent de546e00e0
commit 49d3529525

View File

@@ -79,27 +79,93 @@ function Field({
hint,
children,
className = '',
wide = false,
}: {
label: string;
hint?: string;
children: React.ReactNode;
className?: string;
/** Ocupa las 2 columnas del grid del PanelCard */
wide?: boolean;
}) {
return (
<div className={`block ${className}`}>
<label className="block text-xs font-bold text-gray-700 mb-1">{label}</label>
<div className={`block min-w-0 ${wide ? 'sm:col-span-2' : ''} ${className}`}>
<label className="block text-[13px] font-semibold text-gray-900 mb-1.5 leading-snug">
{label}
</label>
{hint ? (
<p className="text-[11px] text-gray-500 leading-snug mb-1.5">{hint}</p>
<p className="text-xs text-gray-500 leading-relaxed mb-2">{hint}</p>
) : null}
{children}
</div>
);
}
/** Bloque tipo Overview / Prohibido: aire, eyebrow, título y grid legible */
function PanelCard({
eyebrow,
title,
description,
children,
accent = 'neutral',
cols = 2,
}: {
eyebrow?: string;
title: string;
description?: string;
children: React.ReactNode;
accent?: 'neutral' | 'brand' | 'amber' | 'sky' | 'rose';
cols?: 1 | 2;
}) {
const tone =
accent === 'brand'
? 'border-brand-200/80 bg-gradient-to-br from-brand-50/70 via-white to-white'
: accent === 'amber'
? 'border-amber-200/80 bg-gradient-to-br from-amber-50/60 via-white to-white'
: accent === 'sky'
? 'border-sky-200/80 bg-gradient-to-br from-sky-50/60 via-white to-white'
: accent === 'rose'
? 'border-rose-200/80 bg-gradient-to-br from-rose-50/50 via-white to-white'
: 'border-gray-200 bg-gradient-to-br from-gray-50/80 via-white to-white';
return (
<section
className={`rounded-2xl border ${tone} px-5 py-5 sm:px-6 sm:py-6 shadow-xs`}
>
<div className="mb-4 pb-3 border-b border-black/[0.04]">
{eyebrow ? (
<p className="text-[10px] font-bold uppercase tracking-[0.16em] text-gray-500">
{eyebrow}
</p>
) : null}
<h4
className={`text-[15px] font-bold text-gray-900 tracking-tight ${
eyebrow ? 'mt-1' : ''
}`}
>
{title}
</h4>
{description ? (
<p className="text-xs text-gray-500 mt-1.5 leading-relaxed max-w-2xl">
{description}
</p>
) : null}
</div>
<div
className={
cols === 1 ? 'space-y-4' : 'grid gap-4 sm:grid-cols-2 sm:gap-x-5 sm:gap-y-5'
}
>
{children}
</div>
</section>
);
}
const inputCls =
'w-full rounded-xl border border-gray-300 bg-white px-3 py-2 text-xs text-gray-900 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-brand-200';
const areaCls = `${inputCls} min-h-[88px] resize-y`;
'w-full rounded-xl border border-gray-200 bg-white px-3.5 py-2.5 text-sm text-gray-900 placeholder:text-gray-400 shadow-xs focus:outline-none focus:border-brand-400 focus:ring-2 focus:ring-brand-100 transition-colors';
const areaCls = `${inputCls} min-h-[104px] resize-y leading-relaxed`;
const selectCls = inputCls;
const sectionStack = 'space-y-5';
function KitThumb({ assetFileId, alt }: { assetFileId: string; alt: string }) {
const [url, setUrl] = useState<string | null>(null);
@@ -555,7 +621,7 @@ export default function BrandZone({
}
return (
<div className="space-y-6">
<div className={sectionStack}>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between rounded-2xl border border-brand-200 bg-brand-50/50 p-4">
<div className="flex items-start gap-3 min-w-0">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-brand-600 text-white shadow-xs">
@@ -662,11 +728,18 @@ export default function BrandZone({
})}
</div>
<div className="lg:col-span-9 rounded-2xl border border-gray-200 bg-white p-6 shadow-xs space-y-6">
<div className="lg:col-span-9 rounded-2xl border border-gray-200 bg-white p-5 sm:p-8 shadow-xs space-y-6 min-h-[520px]">
{section !== 'overview' ? (
<div>
<h3 className="text-base font-bold text-gray-900">{secMeta?.label}</h3>
<p className="text-xs text-gray-500 mt-0.5">{secMeta?.subtitle}</p>
<div className="pb-1">
<p className="text-[10px] font-bold uppercase tracking-[0.16em] text-brand-600">
Zona de marca
</p>
<h3 className="text-lg font-bold text-gray-900 tracking-tight mt-1">
{secMeta?.label}
</h3>
<p className="text-sm text-gray-500 mt-1.5 leading-relaxed max-w-2xl">
{secMeta?.subtitle}
</p>
</div>
) : null}
@@ -683,7 +756,13 @@ export default function BrandZone({
)}
{section === 'identidad' && (
<div className="grid gap-3 sm:grid-cols-2">
<div className={sectionStack}>
<PanelCard
eyebrow="Esencia"
title="Quién es la marca"
description="Nombre, tagline y pitch: lo primero que debe leerse con claridad."
accent="brand"
>
<Field label="Nombre de marca">
<input
className={inputCls}
@@ -701,7 +780,7 @@ export default function BrandZone({
<Field
label="Elevator pitch (30s)"
hint="Una frase que un desconocido entiende"
className="sm:col-span-2"
wide
>
<textarea
className={areaCls}
@@ -723,6 +802,13 @@ export default function BrandZone({
onChange={(e) => patch({ positioning: e.target.value })}
/>
</Field>
</PanelCard>
<PanelCard
eyebrow="Marco"
title="Misión, visión y valores"
description="Marco estratégico que sostiene el tono y las campañas."
>
<Field label="Arquetipo" hint="Jung / marca (Sabio, Héroe…)">
<input
className={inputCls}
@@ -767,7 +853,7 @@ export default function BrandZone({
onChange={(e) => patch({ origin_story: e.target.value })}
/>
</Field>
<Field label="Sitio Web">
<Field label="Sitio web" wide>
<input
className={inputCls}
value={book.website || ''}
@@ -775,10 +861,18 @@ export default function BrandZone({
onChange={(e) => patch({ website: e.target.value })}
/>
</Field>
</PanelCard>
<PanelCard
eyebrow="Negocio"
title="Oferta B2B / B2C y foco comercial"
description="Qué vende cada línea y qué priorizamos ahora."
accent="sky"
>
<Field
label="Oferta y programas B2B (empresas)"
hint="Programas in-company, formación equipos, acreditaciones oficiales"
className="sm:col-span-2"
hint="In-company, equipos, acreditaciones"
wide
>
<textarea
className={areaCls}
@@ -788,8 +882,8 @@ export default function BrandZone({
</Field>
<Field
label="Oferta y cursos B2C (particulares)"
hint="Cursos adultos, intensivos, certificaciones Cambridge/Aptis, 1to1"
className="sm:col-span-2"
hint="Adultos, intensivos, certificaciones"
wide
>
<textarea
className={areaCls}
@@ -798,32 +892,43 @@ export default function BrandZone({
/>
</Field>
<Field
label="Diferenciación clave (USPs frente a competencia)"
hint="Profesor fijo, sesiones completas, acreditación directa FUNDAE..."
className="sm:col-span-2"
label="Diferenciación clave (USPs)"
hint="Por qué ganar frente a alternativas"
wide
>
<textarea
className={areaCls}
value={book.usp_differentiators || ''}
onChange={(e) => patch({ usp_differentiators: e.target.value })}
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"
hint="Captación prioritaria este periodo"
wide
>
<textarea
className={areaCls}
value={book.current_priorities || ''}
onChange={(e) => patch({ current_priorities: e.target.value })}
onChange={(e) =>
patch({ current_priorities: e.target.value })
}
/>
</Field>
</PanelCard>
</div>
)}
{section === 'voz' && (
<div className="grid gap-3 sm:grid-cols-2">
<div className={sectionStack}>
<PanelCard
eyebrow="Voz"
title="Cómo suena la marca"
description="Tono, do/don’t y ejemplos reales"
accent="brand"
>
<Field label="Tono" className="sm:col-span-2">
<textarea
className={areaCls}
@@ -954,28 +1059,107 @@ export default function BrandZone({
}
/>
</Field>
</PanelCard>
</div>
)}
{section === 'visual' && (
<div className="space-y-6">
<div>
<div className="flex items-center justify-between mb-2">
<p className="text-sm font-semibold text-gray-900 inline-flex items-center gap-1.5">
<Palette className="w-4 h-4 text-rose-500" />
Colores
<div className={sectionStack}>
<PanelCard
eyebrow="Upload"
title="Añadir al kit"
description="Logos, tipografías, patrones e imágenes oficiales."
accent="neutral"
cols={1}
>
<div className="flex flex-col md:flex-row md:items-end gap-3">
<Field label="Tipo" className="md:w-40">
<select
className={selectCls}
value={kitKind}
onChange={(e) => {
const k = e.target.value as BrandAssetKind;
setKitKind(k);
setKitRole(
k === 'logo'
? 'primary'
: k === 'font'
? 'body'
: k === 'image'
? 'moodboard'
: 'asset'
);
}}
>
{Object.entries(ASSET_KIND_LABELS).map(([k, v]) => (
<option key={k} value={k}>
{v}
</option>
))}
</select>
</Field>
<Field label="Rol" className="md:w-44" hint="ej. primary, dark">
<input
className={inputCls}
value={kitRole}
onChange={(e) => setKitRole(e.target.value)}
/>
</Field>
<label className="inline-flex items-center justify-center gap-1.5 rounded-xl bg-gray-900 px-4 py-2.5 text-xs font-bold text-white cursor-pointer hover:bg-gray-800">
{kitUploading ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
) : (
<Plus className="w-3.5 h-3.5" />
)}
Subir archivo
<input
type="file"
className="hidden"
accept={
kitKind === 'logo' ||
kitKind === 'image' ||
kitKind === 'icon'
? 'image/*,.svg,.pdf'
: kitKind === 'font'
? '.ttf,.otf,.woff,.woff2,.zip'
: kitKind === 'video'
? 'video/*'
: '*/*'
}
disabled={kitUploading}
onChange={(e) => {
const f = e.target.files?.[0];
if (f) void uploadKit(f);
e.target.value = '';
}}
/>
</label>
</div>
</PanelCard>
<PanelCard
eyebrow="Paleta"
title="Colores de marca"
description="Tokens HEX con rol y uso. Misma claridad que el overview."
accent="brand"
cols={1}
>
<div className="flex items-center justify-between">
<p className="text-sm font-semibold text-gray-800 inline-flex items-center gap-1.5">
<Palette className="w-4 h-4 text-brand-600" />
Tokens de color
</p>
<button
type="button"
onClick={addColor}
className="text-xs font-semibold text-brand-700 inline-flex items-center gap-1"
className="inline-flex items-center gap-1 rounded-lg bg-brand-50 px-2.5 py-1.5 text-xs font-semibold text-brand-800 hover:bg-brand-100"
>
<Plus className="w-3.5 h-3.5" /> Añadir color
</button>
</div>
<div className="space-y-2">
<div className="space-y-2.5">
{book.colors.length === 0 ? (
<p className="text-xs text-gray-500">
<p className="text-sm text-gray-500 italic">
Añade tokens de color con picker o pega un #hex.
</p>
) : null}
@@ -1061,9 +1245,14 @@ export default function BrandZone({
</div>
))}
</div>
</div>
</PanelCard>
<div className="space-y-3">
<PanelCard
eyebrow="Tipografía"
title="Fuentes del kit"
description="Headline y body con la misma jerarquía visual que el overview."
cols={1}
>
<GoogleFontsGallery
selectedFamilies={book.fonts_kit.map((f) => f.family)}
onPick={(font, pickRole) => {
@@ -1093,15 +1282,15 @@ export default function BrandZone({
setOkMsg(`${font.family} añadida al kit (${pickRole})`);
}}
/>
<div className="flex items-center justify-between mb-2">
<p className="text-sm font-semibold text-gray-900 inline-flex items-center gap-1.5">
<Type className="w-4 h-4 text-rose-500" />
<div className="flex items-center justify-between">
<p className="text-sm font-semibold text-gray-800 inline-flex items-center gap-1.5">
<Type className="w-4 h-4 text-brand-600" />
Tipografías del kit
</p>
<button
type="button"
onClick={addFont}
className="text-xs font-semibold text-brand-700 inline-flex items-center gap-1"
className="inline-flex items-center gap-1 rounded-lg bg-brand-50 px-2.5 py-1.5 text-xs font-semibold text-brand-800 hover:bg-brand-100"
>
<Plus className="w-3.5 h-3.5" /> Manual
</button>
@@ -1199,13 +1388,17 @@ export default function BrandZone({
</div>
))}
</div>
<p className="text-[11px] text-gray-500 mt-2">
<p className="text-xs text-gray-500">
Para archivos .ttf/.otf/.woff usa la barra «Añadir al kit» con
tipo Tipografía.
</p>
</div>
</PanelCard>
<div className="grid gap-3 sm:grid-cols-2">
<PanelCard
eyebrow="Estilo"
title="Logo, foto y reglas de composición"
description="Cómo se ve la marca en creatividades y layouts."
>
<Field label="Uso de logo">
<textarea
className={areaCls}
@@ -1284,7 +1477,7 @@ export default function BrandZone({
onChange={(e) => patch({ typography_body: e.target.value })}
/>
</Field>
<Field label="Reglas de texto en imagen" className="sm:col-span-2">
<Field label="Reglas de texto en imagen" wide>
<textarea
className={areaCls}
value={book.image_text_rules || ''}
@@ -1292,12 +1485,14 @@ export default function BrandZone({
onChange={(e) => patch({ image_text_rules: e.target.value })}
/>
</Field>
</div>
</PanelCard>
<div>
<p className="text-sm font-semibold text-gray-900 mb-2">
Archivos del kit ({book.kit_assets.length})
</p>
<PanelCard
eyebrow="Kit"
title={`Archivos del kit (${book.kit_assets.length})`}
description="Logos, tipografías e imágenes subidos al brand kit."
cols={1}
>
{book.kit_assets.length === 0 ? (
<p className="text-xs text-gray-500">
Todavía no hay logos ni archivos. Usa «Añadir al kit»
@@ -1344,13 +1539,19 @@ export default function BrandZone({
))}
</ul>
)}
</div>
</PanelCard>
</div>
)}
{section === 'audiencia' && (
<div className="grid gap-3 sm:grid-cols-2">
<Field label="ICP" className="sm:col-span-2">
<div className={sectionStack}>
<PanelCard
eyebrow="ICP"
title="A quién hablamos"
description="Perfil ideal, geografía e idiomas — misma claridad que Overview."
accent="sky"
>
<Field label="ICP" wide>
<textarea
className={areaCls}
value={book.icp}
@@ -1501,7 +1702,7 @@ export default function BrandZone({
{(!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">
<div className="grid gap-4 sm:grid-cols-2 sm:gap-x-5 sm:gap-y-5">
{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">
@@ -1622,7 +1823,7 @@ export default function BrandZone({
{(!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">
<div className="grid gap-4 sm:grid-cols-2 sm:gap-x-5 sm:gap-y-5">
{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">
@@ -1710,11 +1911,18 @@ export default function BrandZone({
</div>
)}
</div>
</PanelCard>
</div>
)}
{section === 'oferta' && (
<div className="grid gap-3 sm:grid-cols-2">
<div className={sectionStack}>
<PanelCard
eyebrow="Oferta"
title="Propuesta de valor"
description="Qué vendes, prueba y alternativas"
accent="sky"
>
<Field label="Propuesta de valor" className="sm:col-span-2">
<textarea
className={areaCls}
@@ -1797,11 +2005,18 @@ export default function BrandZone({
onChange={(e) => patch({ price_and_offer_framing: e.target.value })}
/>
</Field>
</PanelCard>
</div>
)}
{section === 'pilares' && (
<div className="grid gap-3 sm:grid-cols-2">
<div className={sectionStack}>
<PanelCard
eyebrow="Contenido"
title="Pilares y series"
description="Temas recurrentes y matriz editorial"
accent="neutral"
>
<Field label="Pilares RRSS">
<textarea
className={areaCls}
@@ -1855,12 +2070,18 @@ export default function BrandZone({
placeholder="Pilar × formato × KPI…"
/>
</Field>
</PanelCard>
</div>
)}
{section === 'canales' && (
<div className="space-y-4">
<div className="grid gap-3 sm:grid-cols-2">
<div className={sectionStack}>
<PanelCard
eyebrow="Presencia"
title="Rol por canal"
description="Para qué usamos cada red y qué no debe mezclarse."
accent="sky"
>
{(
[
['instagram', 'Instagram'],
@@ -1885,8 +2106,12 @@ export default function BrandZone({
/>
</Field>
))}
</div>
<div className="grid gap-3 sm:grid-cols-2">
</PanelCard>
<PanelCard
eyebrow="Handles"
title="URLs y cadencia"
description="Enlaces oficiales y ritmo de publicación."
>
{(
[
['instagram', 'Handle IG'],
@@ -1929,12 +2154,18 @@ export default function BrandZone({
}
/>
</Field>
</div>
</PanelCard>
</div>
)}
{section === 'ads' && (
<div className="grid gap-3 sm:grid-cols-2">
<div className={sectionStack}>
<PanelCard
eyebrow="Paid"
title="Anuncios que convierten"
description="USPs, hooks, CTAs y ángulos observados"
accent="amber"
>
<Field label="USPs">
<textarea
className={areaCls}
@@ -2124,6 +2355,7 @@ export default function BrandZone({
</div>
)}
</div>
</PanelCard>
</div>
)}
@@ -2381,7 +2613,7 @@ export default function BrandZone({
</div>
)}
<div className="grid gap-3 sm:grid-cols-2">
<div className="grid gap-4 sm:grid-cols-2 sm:gap-x-5 sm:gap-y-5">
<Field label="Temas LinkedIn">
<textarea
className={areaCls}
@@ -2742,7 +2974,13 @@ export default function BrandZone({
)}
{section === 'compliance' && (
<div className="grid gap-3 sm:grid-cols-2">
<div className={sectionStack}>
<PanelCard
eyebrow="Líneas rojas"
title="Prohibido y compliance"
description="Claims, competencia, emojis y testimonios"
accent="rose"
>
<Field label="Claims prohibidos">
<textarea
className={areaCls}
@@ -2820,6 +3058,7 @@ export default function BrandZone({
onChange={(e) => patch({ testimonials_policy: e.target.value })}
/>
</Field>
</PanelCard>
</div>
)}