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

885
package-lock.json generated
View File

@@ -74,6 +74,84 @@
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"license": "MIT"
},
"node_modules/@emnapi/runtime": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
"integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
"integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
"integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
"integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
@@ -91,6 +169,363 @@
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
"integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
"integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
"integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
"integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
"integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
"integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
"integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
"integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
"integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
"integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
"integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
"integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
"integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
"integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
"integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
"integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
"integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
"integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
"integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
"integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
"integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
@@ -113,6 +548,28 @@
"@img/sharp-libvips-darwin-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.0.4"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
@@ -129,6 +586,307 @@
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
"cpu": [
"arm"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
"cpu": [
"s390x"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
"cpu": [
"arm"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.0.5"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
"cpu": [
"s390x"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.0.4"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.0.4"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
"cpu": [
"wasm32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.2.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
"cpu": [
"ia32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -190,6 +948,118 @@
"node": ">= 10"
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "15.1.7",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.7.tgz",
"integrity": "sha512-2qoas+fO3OQKkU0PBUfwTiw/EYpN+kdAx62cePRyY1LqKtP09Vp5UcUntfZYajop5fDFTjSxCHfZVRxzi+9FYQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "15.1.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.7.tgz",
"integrity": "sha512-sKLLwDX709mPdzxMnRIXLIT9zaX2w0GUlkLYQnKGoXeWUhcvpCrK+yevcwCJPdTdxZEUA0mOXGLdPsGkudGdnA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "15.1.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.7.tgz",
"integrity": "sha512-zblK1OQbQWdC8fxdX4fpsHDw+VSpBPGEUX4PhSE9hkaWPrWoeIJn+baX53vbsbDRaDKd7bBNcXRovY1hEhFd7w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "15.1.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.7.tgz",
"integrity": "sha512-GOzXutxuLvLHFDAPsMP2zDBMl1vfUHHpdNpFGhxu90jEzH6nNIgmtw/s1MDwpTOiM+MT5V8+I1hmVFeAUhkbgQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "15.1.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.7.tgz",
"integrity": "sha512-WrZ7jBhR7ATW1z5iEQ0ZJfE2twCNSXbpCSaAunF3BKcVeHFADSI/AW1y5Xt3DzTqPF1FzQlwQTewqetAABhZRQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "15.1.7",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.7.tgz",
"integrity": "sha512-LDnj1f3OVbou1BqvvXVqouJZKcwq++mV2F+oFHptToZtScIEnhNRJAhJzqAtTE2dB31qDYL45xJwrc+bLeKM2Q==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "15.1.7",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.7.tgz",
"integrity": "sha512-dC01f1quuf97viOfW05/K8XYv2iuBgAxJZl7mbCKEjMgdQl5JjAKJ0D2qMKZCgPWDeFbFT0Q0nYWwytEW0DWTQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1094,6 +1964,21 @@
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",

View File

@@ -8,6 +8,12 @@ import {
compileClientSkillMarkdown,
normalizeCreativeStyle,
} from '@/lib/creative-style';
import {
compileClientSkillFromBrandbook,
parseBrandbook,
seedBrandbook,
type Brandbook,
} from '@/lib/brandbook';
export const dynamic = 'force-dynamic';
@@ -62,8 +68,39 @@ export async function GET(req: NextRequest) {
: (tenant.settings as any)?.creative_style;
const style = normalizeCreativeStyle(rawStyle, tenant.name || 'Cliente', tenant.slug || '');
const compiledMarkdown = compileClientSkillMarkdown(
style,
// Cargar brandbook de documents (fuente central de verdad)
const { data: brandDoc } = await admin
.from('documents')
.select('content')
.eq('tenant_id', tenantId)
.eq('type', 'brandbook')
.order('updated_at', { ascending: false })
.limit(1)
.maybeSingle();
let brandbook: Brandbook | null = null;
if (brandDoc?.content) {
try {
const parsedContent =
typeof brandDoc.content === 'string'
? JSON.parse(brandDoc.content)
: brandDoc.content;
brandbook = parseBrandbook(parsedContent);
} catch {
brandbook = null;
}
}
if (!brandbook) {
brandbook = seedBrandbook(tenant.slug || 'cliente', tenant.name || 'Cliente');
}
const compiledMarkdown = compileClientSkillFromBrandbook(
brandbook,
{
image_ai: style.image_ai,
campaign_rules: style.campaign_rules,
},
tenant.name || 'Cliente',
tenant.slug || 'cliente'
);
@@ -204,8 +241,38 @@ export async function PUT(req: NextRequest) {
return NextResponse.json({ error: updErr.message }, { status: 500 });
}
const compiledMarkdown = compileClientSkillMarkdown(
normalizedStyle,
// Cargar brandbook de documents para compilar skill actualizado
const { data: brandDoc } = await admin
.from('documents')
.select('content')
.eq('tenant_id', tenantId)
.eq('type', 'brandbook')
.order('updated_at', { ascending: false })
.limit(1)
.maybeSingle();
let brandbook: Brandbook | null = null;
if (brandDoc?.content) {
try {
const parsedContent =
typeof brandDoc.content === 'string'
? JSON.parse(brandDoc.content)
: brandDoc.content;
brandbook = parseBrandbook(parsedContent);
} catch {
brandbook = null;
}
}
if (!brandbook) {
brandbook = seedBrandbook(existing.slug || 'cliente', existing.name || 'Cliente');
}
const compiledMarkdown = compileClientSkillFromBrandbook(
brandbook,
{
image_ai: normalizedStyle.image_ai,
campaign_rules: normalizedStyle.campaign_rules,
},
existing.name || 'Cliente',
existing.slug || 'cliente'
);

View File

@@ -1,7 +1,13 @@
import { NextRequest, NextResponse } from 'next/server';
import { authenticateIntegrationRequest } from '@/lib/integrations-auth';
import { createClient } from '@supabase/supabase-js';
import { compileClientSkillMarkdown, normalizeCreativeStyle } from '@/lib/creative-style';
import { normalizeCreativeStyle } from '@/lib/creative-style';
import {
compileClientSkillFromBrandbook,
parseBrandbook,
seedBrandbook,
type Brandbook,
} from '@/lib/brandbook';
import fs from 'fs';
import path from 'path';
@@ -32,36 +38,54 @@ export async function GET(req: NextRequest) {
const slug = (tenant.slug || 'bps').toLowerCase().trim();
const formatParam = (req.nextUrl.searchParams.get('format') || 'estatico').toLowerCase().trim();
// 2. Resolver skill del cliente: primero desde BD (creative_style editable en la app), fallback a archivo en disco
// 2. Obtener brandbook de documents (fuente central de verdad: Zona de Marca)
const { data: brandDoc } = await adminClient
.from('documents')
.select('content')
.eq('tenant_id', tenantId)
.eq('type', 'brandbook')
.order('updated_at', { ascending: false })
.limit(1)
.maybeSingle();
let brandbook: Brandbook | null = null;
if (brandDoc?.content) {
try {
const parsedContent =
typeof brandDoc.content === 'string'
? JSON.parse(brandDoc.content)
: brandDoc.content;
brandbook = parseBrandbook(parsedContent);
} catch {
brandbook = null;
}
}
if (!brandbook) {
brandbook = seedBrandbook(slug, tenant.name || 'Cliente');
}
// 3. Obtener configuración operativa de Ads (Freepik Mystic, Canva y Reglas de campaña)
const rawDbStyle =
tenant.creative_style && Object.keys(tenant.creative_style).length > 0
? tenant.creative_style
: (tenant.settings as any)?.creative_style;
let clientSkill = '';
const operativeStyle = normalizeCreativeStyle(rawDbStyle, tenant.name || 'Cliente', slug);
// 4. Compilar skill de cliente a partir de Zona de Marca + Operativa Ads
let clientSkill = compileClientSkillFromBrandbook(
brandbook,
{
image_ai: operativeStyle.image_ai,
campaign_rules: operativeStyle.campaign_rules,
},
tenant.name || 'Cliente',
slug
);
// 5. Resolver skill del formato (skills/formats/<format>-4-5.md o skills/formats/<format>.md, fallback a estatico)
const skillsDir = path.join(process.cwd(), 'skills');
if (rawDbStyle && Object.keys(rawDbStyle).length > 0) {
const normalized = normalizeCreativeStyle(rawDbStyle, tenant.name || 'Cliente', slug);
clientSkill = compileClientSkillMarkdown(normalized, tenant.name || 'Cliente', slug);
} else {
const clientCandidates = [
path.join(skillsDir, 'clients', `${slug}.md`),
path.join(skillsDir, 'clients', slug === 'bps' ? 'blue-parrot-school.md' : `${slug}.md`),
];
for (const p of clientCandidates) {
if (fs.existsSync(p)) {
clientSkill = fs.readFileSync(p, 'utf-8');
break;
}
}
if (!clientSkill) {
const normalized = normalizeCreativeStyle(null, tenant.name || 'Cliente', slug);
clientSkill = compileClientSkillMarkdown(normalized, tenant.name || 'Cliente', slug);
}
}
// 3. Resolver skill del formato (skills/formats/<format>-4-5.md o skills/formats/<format>.md, fallback a estatico)
const formatCandidates = [
path.join(skillsDir, 'formats', `${formatParam}-4-5.md`),
path.join(skillsDir, 'formats', `${formatParam}.md`),

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>
)}

File diff suppressed because it is too large Load Diff

View File

@@ -70,6 +70,27 @@ export type BrandPersona = {
channels?: string;
};
export type CreativeB2BPersona = {
name: string;
role: string;
goal: string;
pain_point: string;
buying_trigger: string;
};
export type CreativeB2CPersona = {
name: string;
age_range: string;
situation: string;
pain_point: string;
goal: string;
};
export type CreativeObjection = {
objection: string;
rebuttal: string;
};
export type Brandbook = {
version: 2;
updated_at?: string;
@@ -85,6 +106,11 @@ export type Brandbook = {
vision: string;
values: string[];
vertical: string;
b2b_offering?: string;
b2c_offering?: string;
usp_differentiators?: string;
current_priorities?: string;
website?: string;
/** Voz */
tone: string;
personality: string[];
@@ -96,6 +122,10 @@ export type Brandbook = {
example_good: string[];
example_bad: string[];
slogan_variants: string[];
copy_structure_rules?: string;
english_terms_policy?: string;
power_words?: string[];
allowed_ctas?: string[];
/** Visual kit */
colors: ColorToken[];
fonts_kit: FontToken[];
@@ -112,10 +142,18 @@ export type Brandbook = {
icon_style: string;
imagery_do: string[];
imagery_dont: string[];
surface_rules?: string;
image_text_rules?: string;
typography_headline?: string;
typography_body?: string;
kit_assets: BrandAsset[];
/** Audiencia */
icp: string;
personas: BrandPersona[];
b2b_personas?: CreativeB2BPersona[];
b2c_personas?: CreativeB2CPersona[];
customer_frustrations?: string;
desired_transformation?: string;
geo: string;
languages: string[];
jobs_to_be_done: string[];
@@ -131,6 +169,7 @@ export type Brandbook = {
competitive_alternatives: string[];
guarantee: string;
pricing_notes: string;
price_and_offer_framing?: string;
/** Pilares */
pillars_rrss: string[];
series_recurrentes: string[];
@@ -169,6 +208,9 @@ export type Brandbook = {
ads_formats: string[];
landing_notes: string;
retargeting_angles: string[];
objections?: CreativeObjection[];
winning_hooks?: string[];
preferred_angles?: string[];
/** B2B */
linkedin_themes: string[];
linkedin_formats: string[];
@@ -189,6 +231,9 @@ export type Brandbook = {
legal_notes: string;
sector_rules: string;
image_rights: string;
emoji_policy?: string;
testimonials_policy?: string;
competitors_never_mention?: string[];
/** Fuentes */
source_excerpts: Array<{
id: string;
@@ -404,6 +449,30 @@ export function emptyBrandbook(label = ''): Brandbook {
image_rights: '',
source_excerpts: [],
freeform_notes: '',
b2b_offering: '',
b2c_offering: '',
usp_differentiators: '',
current_priorities: '',
website: '',
copy_structure_rules: '',
english_terms_policy: '',
power_words: [],
allowed_ctas: [],
surface_rules: '',
image_text_rules: '',
typography_headline: '',
typography_body: '',
b2b_personas: [],
b2c_personas: [],
customer_frustrations: '',
desired_transformation: '',
price_and_offer_framing: '',
objections: [],
winning_hooks: [],
preferred_angles: [],
emoji_policy: '',
testimonials_policy: '',
competitors_never_mention: [],
};
}
@@ -582,6 +651,80 @@ export function seedBrandbook(slug: string, label: string): Brandbook {
],
value_proposition:
'Aprendes inglés usable desde la primera semana, con camino claro a certificado.',
b2b_offering:
'Programas de inglés profesional para equipos. Diferencia frente a apps conversacionales tipo Twenix: profesor fijo, sesiones de 60 minutos, programas por sector (Inglés Legal, IT, Financiero, Comercial Internacional), modalidad híbrida. Es Entidad Organizadora de FUNDAE (gestiona la bonificación directamente, no a través de un partner).',
b2c_offering:
'Cursos de inglés para adultos, preparación de exámenes oficiales (Cambridge, Aptis, IELTS), intensivos, clases 1to1 y plataforma online.',
usp_differentiators:
'Profesor fijo asignado + sesiones completas de 60 min (progreso estructural frente a conversación suelta de 15-20 min en apps). Gestión integral FUNDAE como Entidad Organizadora oficial. Especialización real por vertical profesional.',
current_priorities:
'B2C: captación de matrículas de adultos y preparación de certificados oficiales (Cambridge / Aptis). B2B: captación de empresas que quieren aprovechar su crédito anual FUNDAE con programas in-company u online estructurados.',
price_and_offer_framing:
'En B2B destacar el coste bonificado vía FUNDAE (coste cero o casi cero para la empresa, expresado en coste neto o gestión bonificada). En B2C destacar el valor del programa estructurado con profesor fijo, sin recurrir nunca a descuentos agresivos ni cuentas atrás.',
website: 'blueparrotschool.com',
copy_structure_rules:
'Frases cortas. Un dato concreto vale más que un adjetivo. En primary_text: 2-4 frases, máx. 300 caracteres; la frase 1 amplía el hook, frases 2-3 dan un beneficio o dato concreto, frase final cierra con el CTA sin enlaces ni hashtags.',
english_terms_policy:
'Inglés en el copy: solo nombres propios de exámenes (Cambridge, Aptis, IELTS) o términos sin traducción natural (in-company). Nada de "boost", "unlock", "level up".',
power_words: [
'Profesor fijo',
'60 minutos',
'Entidad Organizadora FUNDAE',
'Inglés de tu sector',
'Programa estructurado',
'Madrid y Málaga + Online',
],
allowed_ctas: [
'Más información',
'Solicitar presupuesto',
'Reservar',
'Contactar',
'Registrarte',
],
b2b_personas: [
{
name: 'Marta (RRHH / Formación)',
role: 'Responsable de RRHH o Desarrollo de Talento',
goal: 'Quiere un proveedor llave en mano que no le dé trabajo administrativo.',
pain_point:
'Proveedores que obligan a RRHH a gestionar papeleo FUNDAE, perseguir asistencias o lidiar con quejas por cambios constantes de profesor.',
buying_trigger:
'Gestión FUNDAE 100% resuelta por Blue Parrot, informes periódicos de asistencia y progreso, y un único interlocutor.',
},
{
name: 'Carlos (Socio / Director de Despacho o Consultora)',
role: 'Director General o Socio en firma legal, financiera o consultoría',
goal: 'Que su equipo hable con clientes internacionales con solvencia y sin hacer el ridículo.',
pain_point:
'Su equipo sabe gramática pero se bloquea al negociar, redactar contratos o defender propuestas en inglés técnico.',
buying_trigger:
'Compra rigor y especialización sectorial (Inglés Legal, Financiero, IT), no "clases de inglés general".',
},
{
name: 'Lucía (Directora Comercial / Internacional)',
role: 'Directora Comercial o de Expansión Internacional',
goal: 'Resultados medibles en poco tiempo para presentaciones, negociación y llamadas internacionales.',
pain_point:
'Las apps conversacionales entretienen pero el equipo sigue sin cerrar reuniones complejas en inglés.',
buying_trigger:
'Sesiones de 60 minutos enfocadas en casos reales de su puesto de trabajo.',
},
],
b2c_personas: [
{
name: 'Adulto Profesional / Opositor (25-45 años)',
age_range: '25-45 años',
situation:
'Trabaja o busca salto laboral / necesita certificar nivel oficial (Cambridge B2/C1, Aptis, IELTS).',
pain_point:
'Ha fracasado o se ha estancado con apps y cursos grabados; siente que pierde el tiempo sin un profesor que le corrija y le marque ritmo.',
goal: 'Avanzar de nivel de forma real o aprobar su examen oficial con horario compatible con su jornada.',
},
],
customer_frustrations:
'Estancamiento en el "nivel intermedio eterno" por usar apps de 15 minutos con profesores distintos cada día que solo dan charla superficial sin estructura ni seguimiento.',
desired_transformation:
'Pasar de evitar hablar en inglés o depender de traductores a desenvolverse con seguridad en reuniones de trabajo o aprobar su certificación oficial a la primera.',
ads_usps: [
'Metodología práctica',
'Profes preparados',
@@ -593,6 +736,58 @@ export function seedBrandbook(slug: string, label: string): Brandbook {
'Ya probé y no aprendí',
'Es caro',
],
objections: [
{
objection: 'Ya usamos una app',
rebuttal:
'La app mantiene, no avanza. Sin profesor fijo ni sesiones de 60 min no se sale del nivel intermedio.',
},
{
objection: 'Es caro',
rebuttal:
'Bonificación FUNDAE gestionada directamente por nosotros; coste mínimo por empleado y mes.',
},
{
objection: 'No tenemos tiempo',
rebuttal:
'Sesiones in-company u online integradas dentro del horario laboral.',
},
],
winning_hooks: [
'Tu equipo no necesita más inglés. Necesita el inglés de su trabajo.',
'FUNDAE lo paga. Nosotros lo gestionamos.',
'Un profesor. Sesenta minutos. Cada semana. Así se avanza.',
'El inglés legal no se aprende en una app.',
],
preferred_angles: [
'objeción tiempo',
'coste FUNDAE',
'contraste app vs programa real',
'especialización por sector (Legal / IT / Finanzas)',
'cero carga para RRHH',
'certificación oficial (Cambridge / Aptis)',
],
claims_forbidden: [
'Prometer niveles o plazos cerrados ("B2 en 3 meses").',
'Comparar nombrando explícitamente a la competencia (ni Twenix ni ninguna).',
'Descuentos agresivos, cuentas atrás, "última oportunidad".',
'Fotografía de stock genérica de "gente feliz con portátil" con aspecto de banco de imágenes.',
'Deformar el logo, recolorearlo o recrearlo (se coloca siempre el archivo oficial en plantilla).',
'Empezar un hook por "¿Sabías que", "Descubre" o "Imagina".',
],
competitors_never_mention: ['Twenix', 'Preply', 'Italki', 'Open English', "What's Up!"],
emoji_policy:
'Prohibidos los emojis en creatividades de imagen. En copy (primary_text), como mucho uno al inicio y solo en B2C (nunca en B2B).',
testimonials_policy:
'Prohibidos testimonios inventados o con nombre y cargo de personas que no existen. Si no hay un caso real aprobado, no se usa testimonio.',
surface_rules:
'Azul (#2A4D9C) y blanco (#FFFFFF) como base. Amarillo (#FBC02D) solo para resaltar una palabra, un dato o el CTA. Negro (#000000) reservado a mensajes de formalidad o urgencia (FUNDAE que caduca, manifiesto). Ningún otro color.',
typography_headline: 'Poppins Extrabold (siempre en titulares / hook). Nunca otra fuente.',
typography_body: 'Poppins Regular (en cuerpo y línea de apoyo / support).',
image_text_rules:
'Máximo 2 niveles en imagen (titular hook + línea de apoyo support). El titular no supera 8 palabras. Margen seguro mínimo del 8% en todos los lados; nunca texto en el 15% superior ni inferior.',
photography_style:
'Fotografía real de aula, oficina o reunión con luz natural; personas adultas de 25-50 años en contexto de trabajo; sin poses de catálogo. Si es ilustración o composición gráfica, geometría limpia, mucho aire y una sola idea.',
ads_funnel: ['Awareness Reel', 'Consideración carrusel', 'Conversión test'],
linkedin_themes: [
'Inglés profesional',
@@ -878,6 +1073,51 @@ export function parseBrandbook(raw: unknown): Brandbook | null {
source_excerpts: Array.isArray(partial.source_excerpts)
? partial.source_excerpts
: [],
b2b_offering: String(partial.b2b_offering || ''),
b2c_offering: String(partial.b2c_offering || ''),
usp_differentiators: String(partial.usp_differentiators || ''),
current_priorities: String(partial.current_priorities || ''),
website: String(partial.website || ''),
copy_structure_rules: String(partial.copy_structure_rules || ''),
english_terms_policy: String(partial.english_terms_policy || ''),
power_words: arr(partial.power_words),
allowed_ctas: arr(partial.allowed_ctas),
surface_rules: String(partial.surface_rules || ''),
image_text_rules: String(partial.image_text_rules || ''),
typography_headline: String(partial.typography_headline || ''),
typography_body: String(partial.typography_body || ''),
b2b_personas: Array.isArray(partial.b2b_personas)
? partial.b2b_personas.map((p: any) => ({
name: String(p.name || ''),
role: String(p.role || ''),
goal: String(p.goal || ''),
pain_point: String(p.pain_point || ''),
buying_trigger: String(p.buying_trigger || ''),
}))
: [],
b2c_personas: Array.isArray(partial.b2c_personas)
? partial.b2c_personas.map((p: any) => ({
name: String(p.name || ''),
age_range: String(p.age_range || ''),
situation: String(p.situation || ''),
pain_point: String(p.pain_point || ''),
goal: String(p.goal || ''),
}))
: [],
customer_frustrations: String(partial.customer_frustrations || ''),
desired_transformation: String(partial.desired_transformation || ''),
price_and_offer_framing: String(partial.price_and_offer_framing || ''),
objections: Array.isArray(partial.objections)
? partial.objections.map((o: any) => ({
objection: String(o.objection || ''),
rebuttal: String(o.rebuttal || ''),
}))
: [],
winning_hooks: arr(partial.winning_hooks),
preferred_angles: arr(partial.preferred_angles),
emoji_policy: String(partial.emoji_policy || ''),
testimonials_policy: String(partial.testimonials_policy || ''),
competitors_never_mention: arr(partial.competitors_never_mention),
};
}
@@ -1046,3 +1286,356 @@ export function primaryLogo(book: Brandbook): BrandAsset | undefined {
) || book.kit_assets.find((a) => a.kind === 'logo')
);
}
export type OperativeAdsConfig = {
image_ai?: {
photo_style?: string;
rule_1_5_seconds?: string;
forbidden_scenes?: string;
people_count_rules?: string;
negative_space_prompt?: string;
};
campaign_rules?: {
b2c_detection_rules?: string;
b2b_detection_rules?: string;
fallback_audience_rule?: string;
extra_claude_instructions?: string;
};
};
export function compileClientSkillFromBrandbook(
book: Brandbook,
technicalOps?: OperativeAdsConfig,
clientName?: string,
slug?: string
): string {
const name = clientName || book.brand_name || 'Cliente';
const cSlug = slug || name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
const lines: string[] = [];
lines.push(`# Skill de cliente · ${name} (slug: ${cSlug})`);
lines.push('');
lines.push(
`> Esta skill define cómo habla y cómo se ve ${name} en cualquier creatividad. Se inyecta como contexto fijo a Claude en cada petición de relevo. Si algo aquí contradice un brief puntual, manda esta skill.`
);
lines.push('');
// 1. Quién es
lines.push('## Quién es');
lines.push('');
const summary =
book.elevator_pitch ||
book.mission ||
book.positioning ||
`${name} — actividad y propuesta de valor.`;
lines.push(summary);
lines.push('');
if (book.b2b_offering) {
lines.push(`- **B2B (empresas):** ${book.b2b_offering}`);
}
if (book.b2c_offering) {
lines.push(`- **B2C (particulares):** ${book.b2c_offering}`);
}
const usps =
book.usp_differentiators || book.differentiators?.join(' · ') || '';
if (usps) {
lines.push(`- **Diferenciación clave:** ${usps}`);
}
if (book.current_priorities) {
lines.push(
`- **Prioridades actuales y foco comercial:** ${book.current_priorities}`
);
}
const priceFraming =
book.price_and_offer_framing || book.pricing_notes || '';
if (priceFraming) {
lines.push(`- **Enfoque de precio / oferta:** ${priceFraming}`);
}
if (book.website) {
lines.push('');
lines.push(`Web: ${book.website}`);
}
lines.push('');
// 2. A quién le hablamos
lines.push('## A quién le hablamos');
lines.push('');
if (book.b2b_personas && book.b2b_personas.length > 0) {
lines.push('**B2B**');
for (const p of book.b2b_personas) {
const details = [p.role, p.goal, p.pain_point, p.buying_trigger]
.filter(Boolean)
.join('. ');
lines.push(`- *${p.name}* — ${details}`);
}
lines.push('');
}
if (book.b2c_personas && book.b2c_personas.length > 0) {
lines.push('**B2C**');
for (const p of book.b2c_personas) {
const details = [p.age_range, p.situation, p.pain_point, p.goal]
.filter(Boolean)
.join('. ');
lines.push(`- *${p.name}* — ${details}`);
}
lines.push('');
}
if (
(!book.b2b_personas || book.b2b_personas.length === 0) &&
(!book.b2c_personas || book.b2c_personas.length === 0) &&
book.personas &&
book.personas.length > 0
) {
for (const p of book.personas) {
const details = [p.description, p.pains, p.goals, p.channels]
.filter(Boolean)
.join('. ');
lines.push(`- *${p.name}* — ${details}`);
}
lines.push('');
}
const frust =
book.customer_frustrations ||
(book.barriers?.length ? book.barriers.join('; ') : '');
if (frust) {
lines.push(`- **Frustración principal previa:** ${frust}`);
}
const transf = book.desired_transformation || book.brand_promise || '';
if (transf) {
lines.push(`- **Transformación que buscan:** ${transf}`);
}
lines.push('');
// 3. Cómo habla
lines.push('## Cómo habla');
lines.push('');
if (book.tone) lines.push(`- **Tono:** ${book.tone}`);
const posture =
book.positioning ||
(book.personality?.length ? book.personality.join(', ') : '');
if (posture) lines.push(`- **Postura:** ${posture}`);
const langPolicy =
book.languages && book.languages.length > 0
? book.languages.includes('es')
? 'Español de España, tratamiento de tú.'
: book.languages.join(', ')
: 'Español de España, tratamiento de tú.';
lines.push(`- **Idioma y tratamiento:** ${langPolicy}`);
const copyRules =
book.copy_structure_rules ||
(book.voice_do?.length
? book.voice_do.join('; ')
: 'Frases cortas. Un dato concreto vale más que un adjetivo.');
lines.push(`- **Estructura y estilo:** ${copyRules}`);
const englishPolicy =
book.english_terms_policy ||
(book.vocabulary_avoid?.length
? `Evitar términos y jerga en inglés sin traducción: ${book.vocabulary_avoid.join(', ')}`
: '');
if (englishPolicy) {
lines.push(`- **Idiomas / anglicismos en el copy:** ${englishPolicy}`);
}
const powers =
book.power_words && book.power_words.length > 0
? book.power_words
: book.vocabulary_prefer;
if (powers && powers.length > 0) {
lines.push(`- **Conceptos fuerza:** ${powers.join(' · ')}`);
}
const ctas =
book.allowed_ctas && book.allowed_ctas.length > 0
? book.allowed_ctas
: book.ads_ctas;
if (ctas && ctas.length > 0) {
lines.push(`- **CTAs permitidos:** ${ctas.join(', ')}`);
}
lines.push('');
// 4. Argumentos y objeciones
const winArgs = book.ads_usps?.length
? book.ads_usps
: book.proof_points?.length
? book.proof_points
: book.differentiators;
if (winArgs && winArgs.length > 0) {
lines.push('### Argumentos que funcionan');
winArgs.forEach((arg, idx) => {
lines.push(`${idx + 1}. ${arg}`);
});
lines.push('');
}
if (book.objections && book.objections.length > 0) {
lines.push('### Objeciones a desactivar');
for (const obj of book.objections) {
lines.push(`- "${obj.objection}" → ${obj.rebuttal}`);
}
lines.push('');
} else if (book.ads_objections && book.ads_objections.length > 0) {
lines.push('### Objeciones a desactivar');
for (const obj of book.ads_objections) {
lines.push(`- "${obj}"`);
}
lines.push('');
}
if (book.proof_points && book.proof_points.length > 0) {
lines.push('### Pruebas y datos reales verificados');
for (const pt of book.proof_points) {
lines.push(`- ${pt}`);
}
lines.push('');
}
const angles =
book.preferred_angles && book.preferred_angles.length > 0
? book.preferred_angles
: book.ads_angles;
if (angles && angles.length > 0) {
lines.push(
`### Ángulos recomendados para rotar\n- ${angles.join(' · ')}\n`
);
}
// 5. Prohibido
lines.push('## Prohibido');
const forbidden =
book.claims_forbidden && book.claims_forbidden.length > 0
? book.claims_forbidden
: [
'Promesas irreales o plazos garantizados sin base.',
'Fotografía genérica con aspecto artificial.',
'Deformar o recrear el logo mediante IA.',
];
for (const item of forbidden) {
lines.push(`- ${item}`);
}
if (
book.competitors_never_mention &&
book.competitors_never_mention.length > 0
) {
lines.push(
`- Prohibido mencionar a competidores (${book.competitors_never_mention.join(', ')}).`
);
}
if (book.emoji_policy) {
lines.push(`- **Emojis:** ${book.emoji_policy}`);
}
if (book.testimonials_policy) {
lines.push(`- **Testimonios:** ${book.testimonials_policy}`);
}
lines.push('');
// 6. Identidad visual
lines.push('## Identidad visual');
lines.push('');
if (book.colors && book.colors.length > 0) {
const paletteStr = book.colors
.map((c) => `${c.name} \`${c.hex}\` (${c.usage || c.role})`)
.join(' · ');
lines.push(`- **Paleta exacta y única:** ${paletteStr}`);
}
if (book.surface_rules) {
lines.push(`- **Uso de superficies:** ${book.surface_rules}`);
}
const headFont =
book.typography_headline ||
book.fonts_kit?.find(
(f) => f.role === 'headline' || f.role === 'display'
)?.family ||
'N/A';
const bodyFont =
book.typography_body ||
book.fonts_kit?.find((f) => f.role === 'body')?.family ||
'N/A';
if (headFont !== 'N/A' || bodyFont !== 'N/A') {
lines.push(
`- **Tipografía:** Titulares: ${headFont}. Cuerpo/Apoyo: ${bodyFont}.`
);
}
const logoText =
book.logo_usage ||
'Logo oficial de marca con área de protección respetada en esquina; nunca centrado ni dominando.';
lines.push(`- **Logo:** ${logoText}`);
const photoStyle =
technicalOps?.image_ai?.photo_style ||
book.photography_style ||
'Fotografía realista con luz natural, personas reales en contexto del servicio, sin poses forzadas.';
lines.push(`- **Estilo de imagen:** ${photoStyle}`);
const rule15 =
technicalOps?.image_ai?.rule_1_5_seconds ||
'La escena debe mostrar claramente el producto o servicio en menos de 1,5 segundos sin necesidad de leer el texto.';
lines.push(`- **Regla de lectura en 1,5 s:** ${rule15}`);
const forbiddenScenes =
technicalOps?.image_ai?.forbidden_scenes ||
(book.imagery_dont?.length
? book.imagery_dont.join('; ')
: 'Escenas ambiguas que puedan confundirse con otro sector o stock genérico.');
lines.push(`- **Escenas a evitar en imagen:** ${forbiddenScenes}`);
const peopleCount =
technicalOps?.image_ai?.people_count_rules ||
'1 persona si el mensaje es individual; 2 personas si muestra interacción profesional.';
lines.push(`- **Personas en escena:** ${peopleCount}`);
const negSpace =
technicalOps?.image_ai?.negative_space_prompt ||
'the upper third of the frame is empty wall or sky. no text, no logos, no watermarks, photorealistic, vertical 4:5 composition with clear negative space for a headline.';
lines.push(`- **Espacio negativo (image_prompt):** ${negSpace}`);
const imgText =
book.image_text_rules ||
'Máximo 2 niveles (titular ≤ 8 palabras + apoyo ≤ 12 palabras). Margen mínimo del 8% en todos los lados.';
lines.push(`- **Texto en imagen:** ${imgText}`);
lines.push('');
// 7. Audiencia por campaña
lines.push('## Audiencia por campaña');
lines.push('');
lines.push(
'El brief trae `audience`. Si no viene, se deduce del nombre de campaña:'
);
const b2cDet =
technicalOps?.campaign_rules?.b2c_detection_rules ||
'Campañas B2C: dolor personal, cursos individuales, examen oficial. Habla siempre de "tu".';
lines.push(`- **B2C:** ${b2cDet}`);
const b2bDet =
technicalOps?.campaign_rules?.b2b_detection_rules ||
'Campañas B2B: foco en equipos, formación para empresas, bonificación. Habla siempre de "tu equipo".';
lines.push(`- **B2B:** ${b2bDet}`);
const fallbackAud =
technicalOps?.campaign_rules?.fallback_audience_rule ||
'Si el brief no trae audience y no se puede deducir del nombre de campaña, se pregunta en notes y se genera en B2C.';
lines.push(fallbackAud);
lines.push('');
// 8. Ejemplos de hooks
const hooks =
book.winning_hooks && book.winning_hooks.length > 0
? book.winning_hooks
: book.slogan_variants;
if (hooks && hooks.length > 0) {
lines.push(
'## Ejemplos de hooks que han funcionado (referencia de registro, no para copiar)'
);
for (const h of hooks) {
lines.push(`- "${h}"`);
}
lines.push('');
}
const extra =
technicalOps?.campaign_rules?.extra_claude_instructions?.trim() ||
book.freeform_notes?.trim();
if (extra) {
lines.push('## Instrucciones adicionales específicas del cliente');
lines.push(extra);
lines.push('');
}
return lines.join('\n');
}