feat: rediseño «El documento» en curso, contacto real y enlaces externos
Primitivas y contenido del rediseño (escala tipográfica, Revelar, MarcaSeccion, Seccion, Antetitulo, SimboloMarca, Resplandor, ClaridadHero, sección Proceso de ocho etapas, lib/content.ts reestructurado en seis secciones con alias para las viejas). Mecanismo y FormaTrabajar se retiran. Encima, lo de esta sesión: - recorrido: creeer vuelve a responder cada situación en vez de repetirla (el alias apuntaba respuesta a situacion); respuestas en plural. Créditos de las seis fotos de México. - contacto: WhatsApp, email (con mailto:) y LinkedIn reales; iconos de las redes en el cierre y el footer (IconoRed, trazos de Simple Icons, CC0). - enlaces: lo externo abre en otra pestaña con noopener y aviso para lectores de pantalla (lib/enlaces.ts); mailto y anclas se quedan. - Proceso: las ocho tarjetas no aparecían nunca en móvil (amount 0.3 sobre una lista de ~3000px); ahora cada una se revela sola. El ≡ de la 08 ya no tapa un entregable, título equilibrado, etiqueta a 12px, sin elevar al hover lo que no es clicable. - Disciplinas: la frase «No son la promesa — son los medios» gana peso y se agrupa con título e ilustración; sin cortes huérfanos en móvil. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3242a8bf71
commit
89e54a6e84
73 files changed
+5261
-592
No files matched your search
@@ -0,0 +1,101 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import * as content from '../content';
|
||||
import * as contenido from '../content';
|
||||
import {
|
||||
apertura, dondeEstas, queCambia, elTrabajo, paraQuienQuien, empezar,
|
||||
puntosInflexion, dolores, fraseAncla,
|
||||
} from '../content';
|
||||
|
||||
// The landing speaks in the first person plural (Erik's decision, 6/ago/2026,
|
||||
// recorded in docs/superpowers/specs/2026-08-06-proceso-ocho-etapas-design.md).
|
||||
// Before that it was singular, and the change touched copy in six different
|
||||
// objects — easy to reintroduce by accident when editing one of them.
|
||||
//
|
||||
// The reader stays "tú": that half is the dueña's locked decision and is not
|
||||
// what this file guards.
|
||||
|
||||
// Unambiguous first-person-singular markers: conjugated verbs and imperatives
|
||||
// with the enclitic "-me". Deliberately excludes anything that doubles as a
|
||||
// noun — "trabajo" is one ("el trabajo completo"), which is why the whole
|
||||
// word list is verbs and never bare stems.
|
||||
const SINGULAR = /\b(me sumerjo|sumerjo|cuestiono|separo|reviso|ubico|acompaño|escríbeme|cuéntame|conmigo)\b/i;
|
||||
|
||||
describe('voz de la marca en lib/content.ts', () => {
|
||||
it('speaks in the plural everywhere creeer is the one speaking', () => {
|
||||
// `dolores` is the one exception and it is not an oversight: those four
|
||||
// quotes are in the first person singular because the CLIENT is talking,
|
||||
// not creeer ("Trabajo más y gano casi lo mismo que hace dos años").
|
||||
// They are verbatim from slide 3 of the dueña's deck and must not move.
|
||||
const { dolores, ...resto } = content;
|
||||
|
||||
for (const [nombre, valor] of Object.entries(resto)) {
|
||||
expect(JSON.stringify(valor), `«${nombre}» quedó en singular`).not.toMatch(SINGULAR);
|
||||
}
|
||||
|
||||
// The eyebrow is where the old singular voice survived the plural
|
||||
// migration — locked now that the dueña confirmed it (19/ago/2026).
|
||||
expect(apertura.eyebrow).not.toMatch(/\btu estratega\b/i);
|
||||
|
||||
// Guard the guard: if the quotes are ever rewritten, this fails and forces
|
||||
// a decision instead of silently weakening the check above.
|
||||
expect(JSON.stringify(dolores)).toMatch(/Trabajo más y gano casi lo mismo/);
|
||||
});
|
||||
|
||||
it('keeps the reader in the second person', () => {
|
||||
expect(JSON.stringify(content.paraQuien)).toMatch(/\btienes\b/i);
|
||||
expect(JSON.stringify(content.proceso)).toMatch(/\btu\b/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe('la estructura de seis secciones', () => {
|
||||
it('numera el documento del 01 al 06 sin saltos', () => {
|
||||
expect(
|
||||
[apertura, dondeEstas, queCambia, elTrabajo, paraQuienQuien, empezar].map((s) => s.numero),
|
||||
).toEqual(['01', '02', '03', '04', '05', '06']);
|
||||
});
|
||||
|
||||
it('conserva verbatim las palabras de la dueña', () => {
|
||||
// Her doc is explicit: "they are already written in her voice — use them,
|
||||
// don't rewrite them." These tests fail if someone rewrites them.
|
||||
expect(dondeEstas.dolores).toContain('Trabajo más y gano casi lo mismo que hace dos años.');
|
||||
expect(dondeEstas.remate.accent).toBe('Se resuelve con una estrategia clara.');
|
||||
expect(queCambia.ancla.accent).toBe('dejan de crecer por falta de claridad.');
|
||||
expect(dondeEstas.puntos.map((p) => p.name)).toEqual([
|
||||
'Crecer', 'Consolidarse', 'Recuperar rentabilidad',
|
||||
'Simplificar', 'Expandirse', 'Replantear el portafolio',
|
||||
]);
|
||||
});
|
||||
|
||||
it('los alias viejos siguen leyendo de la misma fuente, no de una copia', () => {
|
||||
// While the two structures coexist, a silent divergence between them
|
||||
// would mean different copy on the same page.
|
||||
expect(puntosInflexion.items[0].situacion).toBe(dondeEstas.puntos[0].situacion);
|
||||
expect(dolores.quotes).toEqual(dondeEstas.dolores);
|
||||
expect(fraseAncla.quote).toBe(queCambia.ancla);
|
||||
});
|
||||
|
||||
it('mantiene en pie todo lo que las secciones viejas importan', () => {
|
||||
// The ten old sections stay mounted until Task 13, and a single missing
|
||||
// name here breaks the whole build. This is the real list of names the
|
||||
// code imports today from `@/lib/content`.
|
||||
const modulo = contenido as Record<string, unknown>;
|
||||
[
|
||||
'brandPillars', 'cierre', 'comoEmpezar', 'creditosFotos', 'disciplinas',
|
||||
'dolores', 'estratega', 'footer', 'fraseAncla', 'header', 'hero',
|
||||
'paraQuien', 'proceso', 'puntosInflexion',
|
||||
].forEach((nombre) => expect(modulo[nombre], nombre).toBeDefined());
|
||||
});
|
||||
|
||||
it('no promete escasez en la apertura', () => {
|
||||
expect(JSON.stringify(apertura)).not.toMatch(/cupos|cohorte|generación fundadora/i);
|
||||
});
|
||||
|
||||
it('arma los tres contactos como enlaces que abren algo', () => {
|
||||
// An email without `mailto:` is a relative URL: the browser navigates to
|
||||
// /nombre@dominio and lands on a 404 instead of opening the mail client.
|
||||
const { whatsapp, email, linkedin } = empezar.contacto;
|
||||
expect(whatsapp.href).toMatch(/^https:\/\/wa\.me\/\d{10,15}$/);
|
||||
expect(email.href).toMatch(/^mailto:[^\s@]+@[^\s@]+\.[^\s@]+$/);
|
||||
expect(linkedin.href).toMatch(/^https:\/\/(www\.)?linkedin\.com\/in\/[\w-]+\/?$/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
// Guardas de contraste, leídas del código fuente porque son una regla de
|
||||
// sistema, no el comportamiento de un componente suelto: lo que hay que impedir
|
||||
// es que el token vuelva a aparecer donde no se lee.
|
||||
//
|
||||
// `gris-medio` (#AAAAAA) da 7,2:1 sobre azul profundo — ahí es el token
|
||||
// correcto, y en Disciplinas y el footer se queda. Sobre blanco da 2,3:1 y
|
||||
// sobre gris-claro 2,1:1: en fondo claro es texto que no se lee.
|
||||
//
|
||||
// `dorado` (#C4942E) da 6,07:1 sobre azul profundo y 2,76:1 / 2,46:1 sobre los
|
||||
// dos fondos claros. Sigue siendo el color de rellenos, reglas, iconos y el ≡;
|
||||
// lo que no puede ser en claro es texto pequeño, y para eso está `dorado-texto`.
|
||||
|
||||
const SOBRE_CLARO = [
|
||||
'components/sections/Hero.tsx',
|
||||
'components/sections/Dolores.tsx',
|
||||
'components/sections/Proceso.tsx',
|
||||
'components/sections/ParaQuien.tsx',
|
||||
'components/sections/Estratega.tsx',
|
||||
'components/sections/ComoEmpezar.tsx',
|
||||
// La rama de movimiento reducido del recorrido va sobre gris-claro, y es
|
||||
// justo la que sirve a quien más necesita que el texto se lea.
|
||||
'components/primitives/ParDialogo.tsx',
|
||||
];
|
||||
|
||||
describe('tokens de texto sobre fondo claro', () => {
|
||||
it.each(SOBRE_CLARO)('%s no pinta texto en gris-medio', (archivo) => {
|
||||
expect(readFileSync(archivo, 'utf8')).not.toMatch(/text-gris-medio/);
|
||||
});
|
||||
|
||||
it('ParDialogo etiqueta al hablante en el dorado legible', () => {
|
||||
// La regla vertical sigue siendo `bg-dorado` — el dorado del manual como
|
||||
// relleno no tiene problema. Lo que no puede quedarse en #C4942E es la
|
||||
// etiqueta, que es texto de 12px sobre gris-claro.
|
||||
const fuente = readFileSync('components/primitives/ParDialogo.tsx', 'utf8');
|
||||
expect(fuente).toMatch(/text-dorado-texto/);
|
||||
expect(fuente).not.toMatch(/text-dorado(?!-)/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { esExterno, propsEnlace } from '../enlaces';
|
||||
|
||||
describe('propsEnlace', () => {
|
||||
it('opens outside links in a new tab without handing over window.opener', () => {
|
||||
expect(propsEnlace('https://wa.me/525558185802')).toEqual({ target: '_blank', rel: 'noopener noreferrer' });
|
||||
expect(propsEnlace('https://www.linkedin.com/in/x')).toEqual({ target: '_blank', rel: 'noopener noreferrer' });
|
||||
});
|
||||
|
||||
it('leaves in-page anchors and mailto: in the same tab', () => {
|
||||
expect(propsEnlace('#contacto')).toEqual({});
|
||||
expect(propsEnlace('mailto:hola@creeer.com')).toEqual({});
|
||||
expect(esExterno('mailto:hola@creeer.com')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import config from '../../tailwind.config';
|
||||
|
||||
// The scale is a contract, not a preference: the problem this redesign fixes is
|
||||
// that five elements were tied for largest on the page, and because of that,
|
||||
// none of them read as largest. Five roles: only one reaches above 4rem.
|
||||
const escala = () => (config.theme?.extend?.fontSize ?? {}) as Record<string, [string, object]>;
|
||||
|
||||
describe('escala tipográfica', () => {
|
||||
it('define los cinco roles del documento', () => {
|
||||
expect(Object.keys(escala())).toEqual(
|
||||
expect.arrayContaining(['display', 'titulo', 'sumario', 'cuerpo', 'rotulo']),
|
||||
);
|
||||
});
|
||||
|
||||
it('display es el único que llega a 7rem', () => {
|
||||
const conSieteRem = Object.entries(escala()).filter(([, v]) => v[0].includes('7rem'));
|
||||
expect(conSieteRem.map(([k]) => k)).toEqual(['display']);
|
||||
});
|
||||
|
||||
it('el cuerpo se compone holgado y el display apretado', () => {
|
||||
const lh = (rol: string) => (escala()[rol][1] as { lineHeight: string }).lineHeight;
|
||||
expect(Number(lh('display'))).toBeLessThan(1);
|
||||
expect(Number(lh('cuerpo'))).toBeGreaterThan(1.6);
|
||||
});
|
||||
|
||||
it('reserva una medida de 68 caracteres para el texto corrido', () => {
|
||||
const anchos = (config.theme?.extend?.maxWidth ?? {}) as Record<string, string>;
|
||||
expect(anchos.medida).toBe('68ch');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user