From e49111f63e35f89038b8092d6ba3a454a57f03c0 Mon Sep 17 00:00:00 2001 From: 4Ventures <4ventures@MacBook-Air-de-4Ventures.local> Date: Sat, 10 Oct 2026 01:05:15 +0200 Subject: [PATCH] Add Zona de marca brandbooks and strategist calendar generation. Per-tenant brandbooks power ads, RRSS calendars, LinkedIn B2B and blog; Nov calendars now create from trends + client DNA instead of copying history. Co-authored-by: Cursor --- scripts/seed_brandbooks_regen_nov.ts | 143 +++++ src/app/api/brandbook/route.ts | 175 ++++++ src/app/api/brandbook/upload/route.ts | 159 +++++ src/app/api/calendar/preview/route.ts | 13 + src/app/dashboard/marca/page.tsx | 36 ++ src/components/BrandZone.tsx | 824 ++++++++++++++++++++++++++ src/components/CalendarCreator.tsx | 10 +- src/components/DashboardShell.tsx | 8 + src/lib/brandbook.ts | 571 ++++++++++++++++++ src/lib/calendar-generate-brand.ts | 125 ++++ src/lib/calendar-generate.ts | 580 +++--------------- src/lib/calendar-strategy.ts | 755 +++++++++++++++++++++++ src/lib/tenant-modules.ts | 8 + 13 files changed, 2907 insertions(+), 500 deletions(-) create mode 100644 scripts/seed_brandbooks_regen_nov.ts create mode 100644 src/app/api/brandbook/route.ts create mode 100644 src/app/api/brandbook/upload/route.ts create mode 100644 src/app/dashboard/marca/page.tsx create mode 100644 src/components/BrandZone.tsx create mode 100644 src/lib/brandbook.ts create mode 100644 src/lib/calendar-generate-brand.ts create mode 100644 src/lib/calendar-strategy.ts diff --git a/scripts/seed_brandbooks_regen_nov.ts b/scripts/seed_brandbooks_regen_nov.ts new file mode 100644 index 0000000..9130c22 --- /dev/null +++ b/scripts/seed_brandbooks_regen_nov.ts @@ -0,0 +1,143 @@ +/** + * Seed brandbook for every tenant + regenerate Nov 2026 editorial strategy. + * Usage: npx tsx scripts/seed_brandbooks_regen_nov.ts + * Optional: MONTH=2026-11 FORCE_BRANDBOOK=1 + */ +import { config } from 'dotenv'; +import { resolve } from 'path'; +config({ path: resolve(process.cwd(), '.env.local') }); + +import type { RrssMonthPlan } from '../src/components/RrssMonthView'; +import { generateMonthPreview } from '../src/lib/calendar-generate'; +import { + BRANDBOOK_DOC_TITLE, + BRANDBOOK_DOC_TYPE, + parseBrandbook, + seedBrandbook, +} from '../src/lib/brandbook'; +import { monthLabel } from '../src/lib/calendar-import'; + +const ym = process.env.MONTH || '2026-11'; +const forceBook = process.env.FORCE_BRANDBOOK === '1'; + +async function main() { + const url = process.env.NEXT_PUBLIC_SUPABASE_URL!; + const key = process.env.SUPABASE_SERVICE_ROLE_KEY!; + const h = { + apikey: key, + Authorization: `Bearer ${key}`, + 'Content-Type': 'application/json', + }; + + const tenants = (await ( + await fetch( + `${url}/rest/v1/tenants?select=id,slug,name&order=name.asc`, + { headers: h } + ) + ).json()) as { id: string; slug: string; name: string }[]; + + console.log(`Tenants: ${tenants.length} · month ${ym} · forceBook=${forceBook}`); + + for (const t of tenants) { + // --- brandbook --- + const bbRes = await fetch( + `${url}/rest/v1/documents?tenant_id=eq.${t.id}&type=eq.${BRANDBOOK_DOC_TYPE}&select=id,content&order=updated_at.desc&limit=1`, + { headers: h } + ); + const bbRows = (await bbRes.json()) as { id: string; content: unknown }[]; + const existing = parseBrandbook(bbRows[0]?.content); + let book = existing; + if (!existing || forceBook) { + book = seedBrandbook(t.slug, t.name); + if (bbRows[0]?.id) { + await fetch(`${url}/rest/v1/documents?id=eq.${bbRows[0].id}`, { + method: 'PATCH', + headers: { ...h, Prefer: 'return=minimal' }, + body: JSON.stringify({ + content: book, + title: BRANDBOOK_DOC_TITLE, + updated_at: new Date().toISOString(), + }), + }); + console.log(`[${t.slug}] brandbook updated`); + } else { + await fetch(`${url}/rest/v1/documents`, { + method: 'POST', + headers: { ...h, Prefer: 'return=minimal' }, + body: JSON.stringify({ + tenant_id: t.id, + type: BRANDBOOK_DOC_TYPE, + title: BRANDBOOK_DOC_TITLE, + status: 'approved', + content: book, + }), + }); + console.log(`[${t.slug}] brandbook created`); + } + } else { + console.log(`[${t.slug}] brandbook exists (skip)`); + } + + // --- calendars --- + const calRes = await fetch( + `${url}/rest/v1/documents?tenant_id=eq.${t.id}&type=eq.calendar&select=id,content&order=created_at.desc&limit=48`, + { headers: h } + ); + const docs = (await calRes.json()) as { id: string; content: unknown }[]; + const history: RrssMonthPlan[] = []; + let novId: string | null = null; + for (const d of docs) { + const c = + typeof d.content === 'string' ? JSON.parse(d.content) : d.content; + const plan = c as RrssMonthPlan; + if (plan?.month === ym) novId = d.id; + else if (plan?.month && Array.isArray(plan.pieces)) history.push(plan); + } + + const result = await generateMonthPreview({ + ym, + slug: t.slug, + label: t.name, + history, + brandbook: book, + }); + + const title = `RRSS ${monthLabel(ym)}`; + if (novId) { + const patch = await fetch(`${url}/rest/v1/documents?id=eq.${novId}`, { + method: 'PATCH', + headers: { ...h, Prefer: 'return=minimal' }, + body: JSON.stringify({ + content: result.plan, + title, + updated_at: new Date().toISOString(), + }), + }); + console.log( + `[${t.slug}] ${ym} patched ${patch.status} · ${result.plan.pieces.length} pcs · ${result.source}` + ); + } else { + const ins = await fetch(`${url}/rest/v1/documents`, { + method: 'POST', + headers: { ...h, Prefer: 'return=minimal' }, + body: JSON.stringify({ + tenant_id: t.id, + type: 'calendar', + title, + status: 'draft', + content: result.plan, + }), + }); + console.log( + `[${t.slug}] ${ym} created ${ins.status} · ${result.plan.pieces.length} pcs · ${result.source}` + ); + } + } + + console.log('Done.'); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/src/app/api/brandbook/route.ts b/src/app/api/brandbook/route.ts new file mode 100644 index 0000000..7d5fd52 --- /dev/null +++ b/src/app/api/brandbook/route.ts @@ -0,0 +1,175 @@ +/** + * GET / PUT brandbook for active tenant (documents type=brandbook). + */ +import { NextRequest, NextResponse } from 'next/server'; +import { createClient } from '@/lib/supabase/server'; +import { resolveActiveTenant, restGet } from '@/lib/tenant-context'; +import { isAgencyRole, serviceHeaders, supabaseUrl, userRole } from '@/lib/agency'; +import { + BRANDBOOK_DOC_TITLE, + BRANDBOOK_DOC_TYPE, + brandbookCompleteness, + emptyBrandbook, + parseBrandbook, + seedBrandbook, + type Brandbook, +} from '@/lib/brandbook'; + +export const dynamic = 'force-dynamic'; + +async function loadDoc(tenantId: string): Promise<{ + id: string | null; + book: Brandbook | null; +}> { + const r = await restGet<{ id: string; content: unknown }[]>( + `/rest/v1/documents?tenant_id=eq.${encodeURIComponent( + tenantId + )}&type=eq.${BRANDBOOK_DOC_TYPE}&select=id,content&order=updated_at.desc&limit=1` + ); + const row = r.data?.[0]; + if (!row) return { id: null, book: null }; + return { id: row.id, book: parseBrandbook(row.content) }; +} + +export async function GET() { + const supabase = await createClient(); + const { + data: { user }, + } = await supabase.auth.getUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + const ctx = await resolveActiveTenant(user); + if (!ctx.tenantId || ctx.portfolioWide) { + return NextResponse.json( + { error: 'Selecciona un cliente en Contexto.' }, + { status: 400 } + ); + } + + let slug = 'cliente'; + const t = await restGet<{ slug?: string }[]>( + `/rest/v1/tenants?id=eq.${encodeURIComponent(ctx.tenantId)}&select=slug` + ); + slug = t.data?.[0]?.slug || slug; + + const { id, book } = await loadDoc(ctx.tenantId); + const current = book || emptyBrandbook(ctx.label); + const completeness = brandbookCompleteness(current); + + return NextResponse.json({ + ok: true, + id, + brandbook: current, + slug, + label: ctx.label, + completeness, + exists: !!book, + }); +} + +export async function PUT(req: NextRequest) { + const supabase = await createClient(); + const { + data: { user }, + } = await supabase.auth.getUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + if (!isAgencyRole(userRole(user))) { + return NextResponse.json({ error: 'Solo agencia' }, { status: 403 }); + } + const ctx = await resolveActiveTenant(user); + if (!ctx.tenantId || ctx.portfolioWide) { + return NextResponse.json( + { error: 'Selecciona un cliente en Contexto.' }, + { status: 400 } + ); + } + + const body = (await req.json().catch(() => ({}))) as { + brandbook?: unknown; + seed?: boolean; + }; + + let slug = 'cliente'; + const t = await restGet<{ slug?: string }[]>( + `/rest/v1/tenants?id=eq.${encodeURIComponent(ctx.tenantId)}&select=slug` + ); + slug = t.data?.[0]?.slug || slug; + + let book: Brandbook | null = null; + if (body.seed) { + book = seedBrandbook(slug, ctx.label); + } else { + book = parseBrandbook(body.brandbook); + } + if (!book) { + return NextResponse.json({ error: 'brandbook inválido' }, { status: 400 }); + } + book.updated_at = new Date().toISOString(); + book.brand_name = book.brand_name || ctx.label; + + const headers = serviceHeaders(); + const base = supabaseUrl(); + if (!headers || !base) { + return NextResponse.json({ error: 'No service role' }, { status: 500 }); + } + + const { id } = await loadDoc(ctx.tenantId); + if (id) { + const patch = await fetch(`${base}/rest/v1/documents?id=eq.${id}`, { + method: 'PATCH', + headers: { + ...headers, + 'Content-Type': 'application/json', + Prefer: 'return=representation', + }, + body: JSON.stringify({ + content: book, + title: BRANDBOOK_DOC_TITLE, + updated_at: book.updated_at, + }), + }); + if (!patch.ok) { + return NextResponse.json( + { error: await patch.text() }, + { status: 500 } + ); + } + const rows = (await patch.json()) as { id: string }[]; + return NextResponse.json({ + ok: true, + id: rows[0]?.id || id, + brandbook: book, + completeness: brandbookCompleteness(book), + }); + } + + const ins = await fetch(`${base}/rest/v1/documents`, { + method: 'POST', + headers: { + ...headers, + 'Content-Type': 'application/json', + Prefer: 'return=representation', + }, + body: JSON.stringify({ + tenant_id: ctx.tenantId, + type: BRANDBOOK_DOC_TYPE, + title: BRANDBOOK_DOC_TITLE, + status: 'approved', + content: book, + }), + }); + if (!ins.ok) { + return NextResponse.json({ error: await ins.text() }, { status: 500 }); + } + const rows = (await ins.json()) as { id: string }[]; + return NextResponse.json({ + ok: true, + id: rows[0]?.id, + brandbook: book, + completeness: brandbookCompleteness(book), + created: true, + }); +} diff --git a/src/app/api/brandbook/upload/route.ts b/src/app/api/brandbook/upload/route.ts new file mode 100644 index 0000000..ac699e4 --- /dev/null +++ b/src/app/api/brandbook/upload/route.ts @@ -0,0 +1,159 @@ +/** + * POST multipart: file (.txt/.md/.csv) or JSON { text, filename } + * Appends source excerpt into the tenant brandbook. + */ +import { NextRequest, NextResponse } from 'next/server'; +import { createClient } from '@/lib/supabase/server'; +import { resolveActiveTenant, restGet } from '@/lib/tenant-context'; +import { isAgencyRole, serviceHeaders, supabaseUrl, userRole } from '@/lib/agency'; +import { + BRANDBOOK_DOC_TITLE, + BRANDBOOK_DOC_TYPE, + brandbookCompleteness, + emptyBrandbook, + ingestSourceText, + parseBrandbook, + type Brandbook, +} from '@/lib/brandbook'; + +export const dynamic = 'force-dynamic'; + +async function loadBook(tenantId: string, label: string): Promise<{ + id: string | null; + book: Brandbook; +}> { + const r = await restGet<{ id: string; content: unknown }[]>( + `/rest/v1/documents?tenant_id=eq.${encodeURIComponent( + tenantId + )}&type=eq.${BRANDBOOK_DOC_TYPE}&select=id,content&order=updated_at.desc&limit=1` + ); + const row = r.data?.[0]; + return { + id: row?.id || null, + book: parseBrandbook(row?.content) || emptyBrandbook(label), + }; +} + +export async function POST(req: NextRequest) { + const supabase = await createClient(); + const { + data: { user }, + } = await supabase.auth.getUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + if (!isAgencyRole(userRole(user))) { + return NextResponse.json({ error: 'Solo agencia' }, { status: 403 }); + } + const ctx = await resolveActiveTenant(user); + if (!ctx.tenantId || ctx.portfolioWide) { + return NextResponse.json( + { error: 'Selecciona un cliente en Contexto.' }, + { status: 400 } + ); + } + + let filename = 'pegado.txt'; + let text = ''; + const ctype = req.headers.get('content-type') || ''; + if (ctype.includes('multipart/form-data')) { + const form = await req.formData(); + const file = form.get('file'); + const pasted = form.get('text'); + if (typeof pasted === 'string' && pasted.trim()) { + text = pasted; + filename = String(form.get('filename') || 'pegado.txt'); + } else if (file && typeof file === 'object' && 'arrayBuffer' in file) { + const f = file as File; + filename = f.name || 'upload.txt'; + const lower = filename.toLowerCase(); + if ( + !lower.endsWith('.txt') && + !lower.endsWith('.md') && + !lower.endsWith('.csv') && + !lower.endsWith('.json') + ) { + return NextResponse.json( + { + error: + 'Por ahora sube .txt, .md, .csv o .json. PDF/DOCX: pega el texto en el campo.', + }, + { status: 400 } + ); + } + text = await f.text(); + } + } else { + const body = (await req.json().catch(() => ({}))) as { + text?: string; + filename?: string; + }; + text = String(body.text || ''); + filename = String(body.filename || 'pegado.txt'); + } + + if (!text.trim()) { + return NextResponse.json({ error: 'Sin texto' }, { status: 400 }); + } + if (text.length > 200_000) { + return NextResponse.json( + { error: 'Texto demasiado largo (máx ~200k)' }, + { status: 400 } + ); + } + + const { id, book } = await loadBook(ctx.tenantId, ctx.label); + const next = ingestSourceText(book, filename, text); + next.brand_name = next.brand_name || ctx.label; + + const headers = serviceHeaders(); + const base = supabaseUrl(); + if (!headers || !base) { + return NextResponse.json({ error: 'No service role' }, { status: 500 }); + } + + if (id) { + const patch = await fetch(`${base}/rest/v1/documents?id=eq.${id}`, { + method: 'PATCH', + headers: { + ...headers, + 'Content-Type': 'application/json', + Prefer: 'return=minimal', + }, + body: JSON.stringify({ + content: next, + title: BRANDBOOK_DOC_TITLE, + updated_at: next.updated_at, + }), + }); + if (!patch.ok) { + return NextResponse.json({ error: await patch.text() }, { status: 500 }); + } + } else { + const ins = await fetch(`${base}/rest/v1/documents`, { + method: 'POST', + headers: { + ...headers, + 'Content-Type': 'application/json', + Prefer: 'return=minimal', + }, + body: JSON.stringify({ + tenant_id: ctx.tenantId, + type: BRANDBOOK_DOC_TYPE, + title: BRANDBOOK_DOC_TITLE, + status: 'approved', + content: next, + }), + }); + if (!ins.ok) { + return NextResponse.json({ error: await ins.text() }, { status: 500 }); + } + } + + return NextResponse.json({ + ok: true, + brandbook: next, + completeness: brandbookCompleteness(next), + ingested: filename, + }); +} diff --git a/src/app/api/calendar/preview/route.ts b/src/app/api/calendar/preview/route.ts index f269be4..1ca34a7 100644 --- a/src/app/api/calendar/preview/route.ts +++ b/src/app/api/calendar/preview/route.ts @@ -8,6 +8,10 @@ import { isAgencyRole, userRole } from '@/lib/agency'; import { asPlanObject } from '@/lib/calendar-documents'; import { generateMonthPreview } from '@/lib/calendar-generate'; import type { RrssMonthPlan } from '@/components/RrssMonthView'; +import { + BRANDBOOK_DOC_TYPE, + parseBrandbook, +} from '@/lib/brandbook'; export const dynamic = 'force-dynamic'; export const maxDuration = 60; @@ -64,11 +68,19 @@ export async function POST(req: NextRequest) { return String(b.month).localeCompare(String(a.month)); }); + const bb = await restGet<{ content?: unknown }[]>( + `/rest/v1/documents?tenant_id=eq.${encodeURIComponent( + ctx.tenantId + )}&type=eq.${BRANDBOOK_DOC_TYPE}&select=content&order=updated_at.desc&limit=1` + ); + const brandbook = parseBrandbook(bb.data?.[0]?.content); + const result = await generateMonthPreview({ ym: month, slug, label: ctx.label, history, + brandbook, }); return NextResponse.json({ @@ -77,5 +89,6 @@ export async function POST(req: NextRequest) { rationale: result.rationale, source: result.source, brand: slug, + brandbook: !!brandbook, }); } diff --git a/src/app/dashboard/marca/page.tsx b/src/app/dashboard/marca/page.tsx new file mode 100644 index 0000000..2d33ec2 --- /dev/null +++ b/src/app/dashboard/marca/page.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { createClient } from '@/lib/supabase/server'; +import { redirect } from 'next/navigation'; +import { resolveActiveTenant } from '@/lib/tenant-context'; +import { EmptyState, PageHeader } from '@/components/ui'; +import BrandZone from '@/components/BrandZone'; + +export const dynamic = 'force-dynamic'; + +export default async function MarcaPage() { + const supabase = await createClient(); + const { + data: { user }, + } = await supabase.auth.getUser(); + if (!user) redirect('/login'); + + const ctx = await resolveActiveTenant(user); + + if (ctx.portfolioWide) { + return ( +
+ + +
+ ); + } + + return ; +} diff --git a/src/components/BrandZone.tsx b/src/components/BrandZone.tsx new file mode 100644 index 0000000..fc63948 --- /dev/null +++ b/src/components/BrandZone.tsx @@ -0,0 +1,824 @@ +'use client'; + +import React, { useCallback, useEffect, useState } from 'react'; +import { + BookOpen, + Loader2, + Save, + Sparkles, + Upload, + CheckCircle2, +} from 'lucide-react'; +import { PageHeader, Surface, SectionTitle } from '@/components/ui'; +import { + BRANDBOOK_SECTIONS, + emptyBrandbook, + linesToList, + listToLines, + type Brandbook, + type BrandbookSectionId, +} from '@/lib/brandbook'; + +function Field({ + label, + hint, + children, +}: { + label: string; + hint?: string; + children: React.ReactNode; +}) { + return ( + + ); +} + +const inputCls = + 'w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm 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`; + +export default function BrandZone({ tenantLabel }: { tenantLabel: string }) { + const [section, setSection] = useState('identidad'); + const [book, setBook] = useState(() => emptyBrandbook(tenantLabel)); + const [slug, setSlug] = useState(''); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [seeding, setSeeding] = useState(false); + const [uploading, setUploading] = useState(false); + const [err, setErr] = useState(null); + const [okMsg, setOkMsg] = useState(null); + const [score, setScore] = useState(0); + const [missing, setMissing] = useState([]); + const [paste, setPaste] = useState(''); + const [exists, setExists] = useState(false); + + const load = useCallback(async () => { + setLoading(true); + setErr(null); + try { + const r = await fetch('/api/brandbook'); + const j = await r.json(); + if (!r.ok) throw new Error(j.error || 'No se pudo cargar'); + setBook(j.brandbook || emptyBrandbook(tenantLabel)); + setSlug(j.slug || ''); + setScore(j.completeness?.score ?? 0); + setMissing(j.completeness?.missing || []); + setExists(!!j.exists); + } catch (e) { + setErr(e instanceof Error ? e.message : 'Error'); + } finally { + setLoading(false); + } + }, [tenantLabel]); + + useEffect(() => { + void load(); + }, [load]); + + function patch(partial: Partial) { + setBook((b) => ({ ...b, ...partial })); + setOkMsg(null); + } + + async function save() { + setSaving(true); + setErr(null); + setOkMsg(null); + try { + const r = await fetch('/api/brandbook', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ brandbook: book }), + }); + const j = await r.json(); + if (!r.ok) throw new Error(j.error || 'No se pudo guardar'); + setBook(j.brandbook); + setScore(j.completeness?.score ?? 0); + setMissing(j.completeness?.missing || []); + setExists(true); + setOkMsg('Brandbook guardado'); + } catch (e) { + setErr(e instanceof Error ? e.message : 'Error al guardar'); + } finally { + setSaving(false); + } + } + + async function seed() { + setSeeding(true); + setErr(null); + try { + const r = await fetch('/api/brandbook', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ seed: true }), + }); + const j = await r.json(); + if (!r.ok) throw new Error(j.error || 'No se pudo sembrar'); + setBook(j.brandbook); + setScore(j.completeness?.score ?? 0); + setMissing(j.completeness?.missing || []); + setExists(true); + setOkMsg('Base de marca creada — completa y guarda lo que falte'); + } catch (e) { + setErr(e instanceof Error ? e.message : 'Error'); + } finally { + setSeeding(false); + } + } + + async function uploadFile(file: File | null) { + if (!file && !paste.trim()) return; + setUploading(true); + setErr(null); + try { + const fd = new FormData(); + if (file) fd.append('file', file); + if (paste.trim()) { + fd.append('text', paste); + fd.append('filename', 'pegado.txt'); + } + const r = await fetch('/api/brandbook/upload', { + method: 'POST', + body: fd, + }); + const j = await r.json(); + if (!r.ok) throw new Error(j.error || 'Upload falló'); + setBook(j.brandbook); + setScore(j.completeness?.score ?? 0); + setMissing(j.completeness?.missing || []); + setExists(true); + setPaste(''); + setOkMsg(`Fuente añadida: ${j.ingested}`); + setSection('fuentes'); + } catch (e) { + setErr(e instanceof Error ? e.message : 'Error upload'); + } finally { + setUploading(false); + } + } + + if (loading) { + return ( +
+ + Cargando zona de marca… +
+ ); + } + + return ( +
+ + + +
+ } + /> + +
+ +

+ Completitud +

+

{score}%

+
+
+
+ + +

+ Usos conectados +

+

+ Calendario editorial · Pipeline creatividades / ads · Estrategia + LinkedIn B2B · Blog corporativo. Cuanto más completo el brandbook, + mejores los temas que genera la IA. +

+ {missing.length ? ( +

+ Falta: {missing.join(' · ')} +

+ ) : ( +

+ Base mínima completa +

+ )} +
+
+ + {err ? ( +

+ {err} +

+ ) : null} + {okMsg ? ( +

+ {okMsg} +

+ ) : null} + +
+ + + + s.id === section)?.label || ''} + description={ + BRANDBOOK_SECTIONS.find((s) => s.id === section)?.hint + } + /> + + {section === 'identidad' && ( +
+ + patch({ brand_name: e.target.value })} + /> + + + patch({ tagline: e.target.value })} + /> + + + patch({ vertical: e.target.value })} + /> + + +