DESIGN.md — Sistema LPDI · Fuente Única de Verdad de Diseño
Última actualización: 9 de agosto de 2026
Para Claude Code: este documento manda sobre cualquier otra instrucción de diseño, incluida la sección «Diseño visual» de CLAUDE.md (que está DESACTUALIZADA — describe un tema dark slate→green con Plus Jakarta Sans que ya NO existe en producción). Si una regla no está aquí, busca el patrón en el código existente y cópialo. Nunca inventes.
Documento generado por ingeniería inversa del código en producción (eco.lpdi.co, repo
lpdi-relacionamiento@main, junio 2026).
⛔ REGLA CERO — No tocar lo ya construido y aprobado
Este documento es DESCRIPTIVO: captura el diseño tal cual está en producción y aprobado por el cliente. NO es una orden de refactorización.
- PROHIBIDO modificar, "alinear", "limpiar" o re-estilizar pantallas, componentes o flujos existentes usando este documento como justificación. Si algo en el código difiere de lo aquí escrito, el código existente gana — reporta la diferencia, no la "corrijas".
- Este documento aplica SOLO a: (a) código nuevo (pantallas, componentes, campos nuevos), y (b) cambios que el usuario pida explícitamente.
- Cualquier cambio visual a algo existente requiere que el usuario lo pida en ese momento, en esa conversación. "Mejorar consistencia" no es razón válida.
- Ante la duda entre cambiar o no cambiar: no cambiar y preguntar.
Mantenimiento de este documento (obligatorio)
- Cuando el usuario apruebe un cambio de diseño, actualiza la sección correspondiente de DESIGN.md en el mismo commit que el cambio de código. Un cambio visual sin su actualización en DESIGN.md está incompleto.
- Actualiza solo la sección afectada — no reescribas ni reorganices el resto del documento.
- Si el cambio introduce un valor nuevo (color, fuente, regla de validación), pregunta al usuario si reemplaza al anterior o convive con él, y documenta la respuesta.
- Si detectas que el código y DESIGN.md divergen sin explicación, repórtalo al usuario — no "corrijas" ninguno de los dos por tu cuenta.
0. Reglas de oro (lo que más se olvida)
- Colores SIEMPRE vía variables CSS (
var(--accent),var(--text-primary)…). Nunca hex sueltos, salvo los estados del §4 y el semáforo del §5. - El acento es AZUL
#006eff(light) /#4E97FF(dark, navy de marca). No existe ningún tema verde. El verde#22c55ees SOLO para éxito/completado. - Fuentes: Space Grotesk (global, formularios, app) y Outfit (superficies de marca: landing, hub ecosistema, headers de marca). Ninguna otra. Nunca Inter, Roboto, Plus Jakarta Sans.
- Íconos: SVG inline estilo stroke (tipo Feather/Lucide),
fill="none" stroke="currentColor". Nunca emoji, nunca icon fonts, nunca librerías nuevas. - Todo estilo nuevo debe funcionar en dark mode: usa variables y, si hace falta, override bajo
[data-theme="dark"]. - Formularios siguen la anatomía del §8 (wrapper 680px, stepper, barra de progreso gamificada, form-card). No inventes layouts nuevos.
- Validación por paso con las reglas exactas del §9. No cambies regexes, criterios de contraseña ni textos de error sin pedirlo.
- Toda búsqueda por texto es insensible a tildes Y mayúsculas (ver §13). Usa el helper canónico
normalizeForSearchen ambos lados de la comparación. Nunca.toLowerCase()solo para una búsqueda del usuario. - Copy en TUTEO (tú), nunca voseo (vos). Todo texto de interfaz usa la segunda persona con «tú»: «puedes», «quieres», «escribe», «confirma», «avisa», «selecciona». PROHIBIDO el voseo argentino («podés», «querés», «escribí», «confirmá», «avisá», «seleccioná»). Esto aplica a labels, placeholders, mensajes de error/advertencia, modales, correos y cualquier microcopy.
1. Stack y dónde viven los estilos
- SvelteKit 2 + Svelte 4 + TypeScript · Tailwind 3.4 instalado pero el sistema visual real vive en CSS scoped por componente + variables CSS globales.
src/app.css— variables de tema (light + dark), tipografía base,.badge-soon,.optional-tag,.required,.toggle-pills/.pill-btn,.field-conditional.- Cada
.sveltedefine su CSS en<style>scoped consumiendo las variables. src/lib/utils/scoreColor.ts— semáforo de scores (fuente única, no duplicar).- Logo:
/lpdi-logo.png(estático), altura 34–36px en headers.
2. Tokens de color (de src/app.css)
Tema claro (default)
| Variable | Valor | Uso |
|---|---|---|
--bg-base |
#F4F7FB |
Fondo de página e inputs |
--bg-surface |
#ffffff |
Cards, modales, headers |
--bg-elevated |
#f8fafc |
Subgrupos, consent boxes, hover |
--accent |
#006eff |
Acento principal (botones, links, focus, íconos) |
--accent-dim |
rgba(0,110,255,0.1) |
Fondos suaves del acento, focus ring |
--accent-teal |
#00ecdd |
SOLO el «¡Heei!» y detalles de marca |
--border |
#E2E8F0 |
Bordes 1px–1.5px |
--text-primary |
#003e88 |
Titulares y texto principal (navy de marca) |
--text-secondary |
#64748b |
Texto de apoyo |
--text-muted |
#6b7d94 |
Labels secundarios, notas, placeholders |
--badge-soon-bg / --badge-soon-text |
#E6FFFE / #00b5ae |
Badge «Próximamente» |
--danger |
#ef4444 |
Errores, acciones destructivas |
--shadow |
rgba(0,62,136,0.06) |
Sombras sutiles |
--radius-card |
14px |
Radio de cards estándar |
Quick cards (4 variantes fondo/borde): blue #eff6ff/#bfdbfe · green #f0fdf4/#bbf7d0 · orange #fff7ed/#fdba74 · purple #faf5ff/#ddd6fe.
Tema oscuro ([data-theme="dark"]) — navy de marca «el fondo del iceberg» (Frank 2026-07-24)
--bg-base #05122E · --bg-surface #102C5A · --bg-elevated #173A6E · --accent #4E97FF · --accent-dim rgba(78,151,255,0.16) · --accent-teal #00ECDD · --border rgba(160,195,255,0.16) · --text-primary #F1F6FF · --text-secondary #C4D3EC · --text-muted #93A6C8 · --danger #FF6B78 · --danger-strong #FF8591 · --shadow rgba(0,0,0,0.45) · badge-soon #0d3d3a/#00ECDD. Quick cards re-derivadas sobre navy.
--danger-strong es un token semántico: en CLARO vale #c0392b (el rojo fuerte fijo de §15.1/§15.2, idéntico al histórico) y solo se eleva en oscuro. El shell del dashboard suma un degradado radial navy (.app-shell) y vidrio (backdrop-filter) en las barras del marco.
El toggle de tema guarda en localStorage y aplica data-theme en <html> (vía app.html, en todas las rutas).
Política de modo oscuro (REGLA DEL SISTEMA — Frank 2026-07-24)
- SUPRAREGLA (tope de todo): ningún cambio del modo oscuro puede afectar lo aprobado en el modo claro. Cualquier cambio del modo claro sí obliga a validar y ajustar su efecto en el oscuro. En la práctica: en
:root(claro) solo se AGREGAN variables nuevas (nunca se cambian las existentes); los cambios de oscuro viven en[data-theme="dark"], o son migraciones hex→token cuyo valor CLARO es idéntico al hex que reemplazan (p. ej.#ffffff==--bg-surface,#003e88==--text-primary). - Voltear tokens, no bifurcar el componente. Un solo componente para ambos temas. Prohibido
.card--dark,if (isDark)en estilos o un segundo set de colores. Si el componente consumevar(--*), voltea solo al cambiardata-theme. - Navy de marca, no slate genérico. El fondo oscuro es navy profundo de marca, no el slate de Tailwind.
- Vidrio para el chrome, opaco para los datos. Superficies traslúcidas +
backdrop-filterSOLO en el marco (sidebar/topbar), nunca en tablas con scroll ni en modales (rompe el anclaje del overlayfixed, §V.2). - Los colores FUNCIONALES se rigen por legibilidad y diferenciación, no por la familia de marca. Estados, categorías, canales, el semáforo de scores (§5) y el rojo destructivo cumplen la función de distinguir de un vistazo; en oscuro se mantiene su hue propio (tinte oscuro del color + texto claro del mismo color), nunca se unifican al azul de marca.
Colores de marca fijos (no variables — solo superficies de marca)
- Navy titular:
#003e88(con<strong>interno en#006eff) - Teal «¡Heei!»:
#00ecdd - Sub-texto hero:
#5a7aa8· labels de sección:#94A3B8 - Hover de botón primario de landing: fondo pasa a
#003e88
3. Tipografía
| Contexto | Fuente | Notas |
|---|---|---|
| Global / app / formularios / dashboard | Space Grotesk (300–700, Google Fonts, importada en app.css) |
body y h1–h6 (font-weight: 700 en headings) |
Landing /, hub /ecosistema, FormBrandHeader |
Outfit (300–900, cargada por página en <svelte:head>) |
Marca: pesos 700–900 |
Escala observada (no inventar tamaños nuevos sin razón):
- «¡Heei!»: clamp(52px, 8vw, 72px) (landing) / clamp(40px,7vw,56px) (login), peso 900, letter-spacing −2/−3px, color teal.
- H1 hero: clamp(18px, 3.5vw, 28px), 700, navy, letter-spacing: -0.6px.
- Títulos de card: 19px/800. Título de sección de formulario: 18px/700 centrado.
- Cuerpo/descripcion: 13–14px. Labels de campo: 13px/600. Labels uppercase (login): 11px/600, letter-spacing: 0.5px.
- Microcopy/notas: 11–12px. Badge soon: 9px/500.
4. Colores de estado
| Estado | Color | Patrón |
|---|---|---|
| Éxito | #22c55e |
mensajes, criterios cumplidos, steps completados, bg rgba(34,197,94,0.1) |
| Error | #ef4444 |
bordes de input, asterisco .required, textos; banner claro: bg #fef2f2, borde #fca5a5, texto #b91c1c; dark: rgba(239,68,68,0.1) / #fca5a5 |
| Advertencia / hint | ámbar: bg #fffbeb, borde #fcd34d |
hints de logo/draft |
| Info | var(--accent-dim) + var(--accent) |
mensajes informativos |
| Email existente | bg #f0fdf4, texto #166534, borde #bbf7d0 |
.email-check-status.exists |
| Email nuevo | bg #eff6ff, texto #1e40af, borde #bfdbfe |
.email-check-status.new-account |
5. Escala de scores — 4 bandas (FUENTE ÚNICA: src/lib/utils/scoreColor.ts)
Unificada por Frank (2026-07-02): antes coexistían un semáforo simple de 3 bandas (34/67) y la escala de 4 bandas de los formularios. Ahora TODO usa las 4 bandas (scoreColor() + scoreMessage()):
0–39 % → #ef4444 rojo · «Tu perfil tiene baja visibilidad.»
40–69 % → #f97316 naranja · «Faltan datos clave para hacer match.»
70–89 % → #f59e0b ámbar · «¡Casi listo! Estás en el radar.»
90–100 % → #22c55e verde · «Perfil destacado. Alta probabilidad de match.»
Usado en barras de completitud de perfil (dropdown del hub, sidebar, formularios FS/FI/FD, correos). Importar siempre scoreColor()/scoreMessage(), nunca re-implementar los umbrales (40/70/90).
6. Íconos
- SVG inline 24×24 viewBox,
fill="none" stroke="currentColor"(o#006effen card-icons),stroke-linecap/linejoin="round". - Stroke-width: 1.8 en íconos grandes de módulo (28px), 2–2.2 en íconos pequeños (13–18px).
- Vocabulario existente (reusar paths del código): rayo (startup), cohete (startup badge / eliminar-startup), maletín (ICG / eliminar-ICG), dólar (ICG), lupa (explorar), campana (noticias), calendario+ (eventos), usuarios (networking), nodos conectados (matchmaking), bombilla (ideas), candado (
lock-badge, bloqueo de opción), grid 2×2 (dashboard), usuario+ (registro), login (puerta con flecha), ojo (mostrar contraseña), luna/sol (tema), globo (volver al ecosistema), pin (footer), alert-triangle (advertencia/submit-hint/estado-error), info-circle (nota de privacidad), megaphone (modal convocatoria), target (recordatorio convocatoria), upload (adjuntar PDF), x (cerrar modal), x-circle (toast error), check-circle (toast éxito), status-dot (círculo 8px CSS: verde#22c55econfirmado / rojo#ef4444rechazado / ámbar#f59e0bpendiente), rocket (tipo startup en modal eliminar), briefcase (tipo ICG en modal eliminar). - Excepciones legacy con
fill: Google/GitHub (OAuth), pin del footer, ícono de mensaje del login. No ampliar esa lista. - Prohibido: emoji como ícono (el 🙈/👁 del signup es deuda legacy — usar el SVG de ojo), icon fonts, librerías de íconos nuevas.
7. Componentes canónicos
Botones
.btn-primary—background: var(--accent), texto blanco, radius 8px, padding 11px 22px, 14px/600, hoveropacity .92+translateY(-1px), disabledopacity .5. Spinner inline 14px cuando carga..btn-secondary— transparente,1px solid var(--border), texto--text-secondary, hoverbg var(--bg-elevated).- Outline de marca (landing/hub:
.btn-login-h,.btn-outline) — transparente,1.5px solid #006eff, texto#006eff, hover invierte (fondo azul, texto blanco). - Outline de perks (
.btnL.outline, catálogo/ficha) — borde fino1px solid rgba(0,110,255,0.30)+ relleno azul suave + glow, conoutline: noneobligatorio por colisión con la utilidad Tailwind.outline(ver §16.3, GOTCHA CRÍTICO). .ref-cta(canónico,perks.css— Frank, aprobado 2026-07-11) — botón/enlace secundario de navegación dentro de un panel:background: transparent,border: 1px solid var(--accent), textovar(--accent), radius 10px, padding 10px 16px, 13.5px/700, hoverbg rgba(0,110,255,.06). Regla: dentro de una misma pantalla, el azul relleno (.btn-primary) se reserva para la acción primaria; cualquier otro CTA de navegación al lado (ir a un listado relacionado, "Mis favoritos", "Mis referidos", "Referidos", enlaces "‹ Volver a…") usa.ref-cta, nunca.btn-secondary(que es gris y se reserva para acciones neutras tipo "Cancelar"). Unificado eneventos/mios,beneficios,beneficios/admin,beneficios/nuevoybeneficios/externo; ya existía como patrón local eneventos/admin/super-eventosyeventos/admin/aprobacion.- CTA de landing
.btn-reg— azul, radius 10px, 13.5px/700, sombra0 3px 12px rgba(0,110,255,0.28), hover fondo#003e88. - En móvil (<480px) los botones de formulario van a
width:100%y.btn-rowse invierte (column-reverse). - Solo botones funcionales (Frank 2026-06-28): en cualquier visual de panel admin (filas, modales, tarjetas) se muestran ÚNICAMENTE las acciones que aplican al estado del registro. Una acción que no aplica se OCULTA (con
{#if}), no se deja deshabilitada/gris. (Ej.: en un evento finalizado no se muestran Aprobar/Rechazar/Editar; en la papelera no se muestra Aprobar.)disabledse reserva para estados transitorios (mientras se procesa la acción), no para «no aplica».
Cards de módulo (landing/hub)
rgba(255,255,255,0.9) + backdrop-filter: blur(8px), borde 1.5px solid #E2E8F0, radius 18px, padding 26px, gap 14px; hover: borde #006eff, sombra 0 10px 36px rgba(0,110,255,0.1), translateY(-2px).
- card-icon: 52×52, radius 13px, linear-gradient(135deg,#EBF3FF,#D6E8FF), SVG 28px stroke #006eff.
- card-title 19px/800 navy · card-desc 13px #5a7aa8.
- Tags pill: padding 2px 11px, radius 100px, 11px/600, bg #EBF3FF, texto #006eff, borde #C7DCFF.
- Card deshabilitada: clase soon + .badge-soon + botones .subsystem-btn--soon; módulos con login requerido llevan .lock-badge (candado).
Badges
.badge-soon(unificado LAP-113, global): 9px/500, padding 1px 6px, radius 4px, bgvar(--bg-elevated), textovar(--text-muted). No estilos alternativos de «Próximamente»..optional-tag(global): pill uppercase 10px/600, bg--bg-elevated, texto--text-muted..required: asterisco#ef4444tras el label.- Badge de entidad en header: pill con ícono 13px + «Startup · Nombre».
Inputs
.form-input/.form-select: bgvar(--bg-base), borde1px solid var(--border), radius 8px, padding 10px 14px, 14px,font-family: inherit.- Focus:
border-color: var(--accent)+box-shadow: 0 0 0 3px var(--accent-dim). - Error:
.input-error→ borde#ef4444+ ringrgba(239,68,68,0.15). - Select con chevron SVG embebido como
background-image(noappearancenativo). - Input de website con prefijo
https://fijo en monospace (.website-prefix) — el usuario solo escribe el dominio; el código anteponehttps://. - Password: wrapper relativo + botón ojo SVG a la derecha (
.toggle-pw). - Upload de logo:
.file-drop-zonecon borde2px dashed var(--border), radius 12px, hover/dragover acento; preview 80×80.
Opciones tipo card (radios y checkboxes)
.radio-option/.checkbox-option: caja con borde1.5px solid var(--border), radius 8px, padding ~10px 14px,accent-color: var(--accent)en el input nativo; seleccionada → borde acento + bgvar(--accent-dim); hover → borde acento / bg elevated.- Subgrupos condicionales:
.field-subgroup/.pivot-subgroupcon bg--bg-elevatedy borde izquierdo3px solid var(--accent); aparición animada.field-conditional(fade + slide 0.3s). .toggle-pills/.pill-btn(Sí/No/No importa): pill radius 9999px, padding 10px 28px; activa → bgrgba(59,130,246,0.12), borde y texto#3b82f6.
Stepper y progreso (formularios)
- Stepper: círculos 36px; activo
bg var(--accent)+ ring0 0 0 4px var(--accent-dim); completadobg #22c55e; inactivobg --bg-elevated+ borde; labels 11px (activa: acento/700); conectores 2px (#22c55esi completado). - Barra gamificada: card radius 12px con «Tu perfil está X% completo» + «X / 100 pts»; track 8px radius 9999px bg
--bg-elevated; fill conscoreColor(score); transiciónwidth .4s. - Subpaneles numerados:
.subpanel-headercon círculo 24px bg acento + label 14px/600, borde inferior1.5px.
Modales
⚠️ Reemplazado por «Estándares V.2 → Pop-ups / Modales del Sistema». El estándar canónico vigente es: overlay
rgba(0,0,0,0.55)conz-index:200, caja radius 14px, sombra0 12px 32px rgba(0,0,0,0.25), variante centrada de confirmación y doble confirmación destructiva. Ver esa sección. (Se conserva la nota del modal de éxito: check verde + countdown «Te llevamos al ecosistema en Xs».)
Indicadores
- AutoSaveIndicator: estados
idle/saving/saved/error. - DraftResumeBanner: banner para retomar borrador + «Empezar de nuevo» (
.clear-draft-btn, hover rojo). - Spinners: círculo con borde 2px,
border-top-colortransparente/blanco,spin 0.7s linear infinite.
8. Patrones de página
Header público (landing y hub)
Sticky, max-width 860px, bg rgba(255,255,255,0.92) + backdrop-filter: blur(12px), borde inferior #E2E8F0, radius solo abajo 0 0 14px 14px, sombra 0 1px 4px rgba(0,62,136,0.06). Logo 34px a la izquierda. Derecha: «Iniciar sesión» outline (anónimo) o botón Dashboard + badge de entidad + avatar 36px (borde 2px #006eff, placeholder bg #e8f0ff) con dropdown (perfiles con mini barras de progreso scoreColor).
Hero de marca
¡Heei! teal gigante → H1 navy con <strong> azul → sub gris azulado. Página entra con fade+slide (opacity 0 / translateY(12px) → mounted). Después: label de sección («¿Qué quieres hacer hoy?», 12.5px #94A3B8) y grid de module-cards (2 o 3 columnas, 1 en <600px). Métricas en metrics-bar (número grande + label, separadores verticales), formato toLocaleString('es').
Hub Central (/ecosistema) — estados y cards bloqueadas
Tres estados: anónimo (sin login), general (logueado sin entidad), enabled (logueado + Startup/ICG registrada). Copy: saludo «¡Heei {nombre}!», H1 «Tu lugar en el ecosistema» (general/enabled), label de sección «¿Qué quieres hacer hoy?».
Cards bloqueadas (Matchmaking y Networking en estado general): contenido atenuado, card completa clickeable, badge 🔒 circular ámbar arriba-derecha (no overlay que tape la card). Al hacer click → modal central «[Módulo] no disponible aún» con texto «Completa tu registro como Startup/PyME o como ICG para desbloquear este módulo» + botón primario «Soy Startup / PyME» → /registro-startup, botón secundario «Soy ICG» → /registro-icg, link «Cancelar». (Decisión aprobada por Frank — cerrada, no reabrir. Consolidada aquí desde la antigua BIBLIA Hub Central 2026-07-05.)
Header de formularios (FormBrandHeader)
Card blanca sticky radius 12px: logo 36px · centro «LPDI · Ecosistema de Innovación» (Outfit 14px/500, oculto en <640px) · derecha link azul con globo «Ir al ecosistema».
Layout de formulario
page-wrapper max-width 680px centrado. Orden: FormBrandHeader → header centrado (badge pill uppercase con --accent-dim + título 17px/700 + subtítulo) → meta de draft → stepper → barra de progreso → .form-card (radius 16px, padding 32px 28px, sombra suave) → .btn-row (Atrás secundario / Siguiente primario). Two-col grid para pares de campos (colapsa <480px).
Dashboard
Sidebar con items radius 8px (texto #003e88, hover/activo --accent-dim), avatar/logo con linear-gradient(135deg, var(--text-primary), var(--accent)), mini barras de progreso 4px con scoreColor, acciones peligrosas en #ef4444, popups radius 10px con sombra 0 8px 24px rgba(0,0,0,0.15).
Footer (AppFooter)
Centrado, 13px --text-muted, borde superior: «© 2026 La Punta del Iceberg 📍(pin SVG) Hecho en LATAM — Todos los derechos reservados» + links legales 12px (Términos · Tratamiento de Datos · «Ayúdanos a mejorar» → /ideas).
9. Formularios: validación y comportamiento (NO CAMBIAR sin aprobación)
Reglas universales
- Validación por paso al pulsar «Siguiente»: errores como lista de strings en
.error-bannerarriba del panel + clase de error en el campo + scroll suave al primer error (scrollIntoViewconbehavior:'smooth'). - Client-side y server-side siempre (regla del repo).
- Mensajes de error: español, tono directo, formato «El/La X es obligatorio/a.» — copiar el estilo de los existentes.
- Campos opcionales llevan
.optional-tag; los requeridos asterisco rojo.ceoGenderes opcional y no bloquea el avance.
Regexes canónicas
Email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
LinkedIn: /^https?:\/\/(www\.)?linkedin\.com\/(in|company)\//i
Website: el input quita /^https?:\/\//, el valor final es `https://${dominio}`
Contraseña (5 criterios, checklist en vivo)
Mínimo 8 caracteres · una mayúscula · una minúscula · un número · un carácter especial. Checklist en grid 2 columnas con ✓/○; cumplido → verde #22c55e, incumplido (con texto presente) → rojo. Confirmación con borde verde/rojo según coincidencia + hint «✓/✗ Las contraseñas (no) coinciden». El submit se bloquea (disabled) hasta cumplir todo + aceptar T&C y Tratamiento de Datos.
Consentimiento TYC / Tratamiento de Datos (PDP) — regla de sistema
La aceptación de Términos y Condiciones (TYC) y de la Política de Tratamiento de Datos Personales (PDP) SIEMPRE es explícita: casillas de verificación separadas que el usuario marca por su cuenta. Nunca implícita, nunca preseleccionada, y nunca agrupada con otra acción en un solo botón. En particular, al crear una cuenta nueva mientras se acepta una transferencia de entidad, no se combina «aceptar transferencia + crear cuenta + TYC/PDP» en un único clic: TYC y PDP son casillas propias que deben marcarse antes de habilitar el submit. Aplica a todo alta de cuenta (registro público y cuenta creada al aceptar una transferencia). (Regla confirmada por Frank 2026-07-05.)
Checks asíncronos (debounce 600ms)
- Email →
/api/check-email→ estadosidle/checking/exists/newcon.email-check-status(existe → pide contraseña para vincular cuenta; nuevo → flujo de creación con criterios). - Nombre de empresa →
/api/check-company-name?name=&type=(mín. 2 caracteres).
Lógica condicional clave (FS startups)
isTechBased === 'false'fuerzacompanyType = 'PYME'y muestra industrias tradicionales;'true'exige elegir Startup o Scaleup y muestra industrias tech. Cambiar el pivote resetea las industrias seleccionadas.- Pasos FS: 0 Datos básicos (nombre, pivote tech, industrias ≥1, país origen, países operación ≥1, facturación, email corporativo[, cuenta]) · 1 Necesidades (≥1) · 2 CEO/Rep. Legal (nombre, apellido, fundador sí/no, LinkedIn válido, email; teléfono/género opcionales) · 3 Presencia digital (website + ≥1 red social; logo opcional).
Drafts y autosave (LAP-19)
- localStorage con debounce 1.5s, key por formulario (
lpdi_registro_draft…). - Nunca persistir: password, archivos (
File),termsAccepted/privacyAccepted(legal). - Al montar: restaurar draft + banner con fecha;
?force_new=truelimpia y arranca en blanco; «Empezar de nuevo» limpia y recarga. - Éxito: modal con check verde y countdown configurable → redirect.
Score gamificado
Cada formulario calcula 0–100 pts con pesos por campo (ver scoreFS en el shell); la barra usa scoreColor. Si tocas campos, recalibra los pesos para que sigan sumando 100.
10. Voz y copy
- Español, siempre «tú». Saludo de marca: «¡Heei!» (con nombre si hay sesión: «¡Heei Pedro!»).
- Titulares con una palabra/frase clave en
<strong>azul: «Bienvenido al Ecosistema LPDI». - Energía con exclamaciones medidas: «¡Únete al ecosistema de emprendimiento e innovación de Latam!».
- Honestidad radical: lo que no existe se marca «Próximamente» con la card deshabilitada — nunca se simula funcionalidad.
- CTAs en primera persona o imperativo claro: «Registrarme como Startup», «Entrar al ecosistema», «Crear cuenta», «Enviar una idea».
- Microcopy de apoyo bajo cada label (
.field-helper); notas legales en cursiva 11px. - Términos del dominio: Startup / Scaleup / PyME · ICG = Inversionistas, Corporativos y Gestores del ecosistema · CRL/TRL/BRL · «Data Room».
11. Anti-patrones (PROHIBIDO)
- ❌ Tema verde, gradients slate→green, Plus Jakarta Sans (CLAUDE.md viejo — ignorar esa sección).
- ❌ Hex hardcodeados para acento/texto/fondos (usa variables). El azul
#2563ebque aparece en fallbacks viejos NO es canónico: el acento es#006eff/#3b82f6. - ❌ Emoji como íconos, icon fonts, o instalar librerías de íconos.
- ❌ Inventar variantes de botón, badge «Próximamente» alternativo, radios de esquina nuevos o sombras dramáticas.
- ❌ Estilos que solo funcionan en tema claro (probar dark con
data-theme="dark"). - ❌ Cambiar regexes, criterios de contraseña, umbrales del semáforo (40/70/90) o pesos de score sin instrucción explícita.
- ❌ Persistir contraseñas o aceptación de términos en drafts/localStorage.
- ❌ Teal
#00ecddpara texto largo o botones — es un destello de marca, no un color de UI. - ❌ Layouts de formulario fuera del patrón 680px + stepper + form-card.
- ❌ Copy en inglés, trato de «usted», o clichés de emprendimiento (cohetes/apretones de manos en imágenes).
12. Checklist antes de commitear UI
- [ ] ¿Solo Space Grotesk / Outfit, según superficie?
- [ ] ¿Colores vía
var(--*)y se ve bien en dark mode? - [ ] ¿Íconos SVG inline stroke, sin emoji?
- [ ] ¿Inputs con focus ring
--accent-dimy estado de error rojo estándar? - [ ] ¿Badges/labels reutilizan
.badge-soon,.optional-tag,.required? - [ ] ¿Formulario respeta anatomía (680px, stepper, barra con
scoreColor, error-banner)? - [ ] ¿Validación client + server, mensajes en español estilo existente?
- [ ] ¿Draft en localStorage sin password/términos?
- [ ] ¿Copy en «tú», keyword en
<strong>azul, «Próximamente» si no existe? - [ ] ¿Mobile-first: grids colapsan, botones 100% en <480px?
- [ ] ¿Todo modal/pop-up se cierra con la tecla Esc (vía
use:escToClosecableado al handler de cerrar/cancelar, NUNCA a un CTA)?
Estándares de formularios — alineación FP ↔ FD/FI (Frank 2026-06-22)
Todos los formularios (FP perks, FD startup, FI ICG, FE evento, perk externo) comparten estos estándares. NO desviarse al crear formularios nuevos — Frank pidió explícitamente "que no vuelva a pasar a futuro".
Mnemotécnica de formularios (actualizada Frank 2026-06-30): | Sigla | Formulario | Ruta | Notas | |-------|-----------|------|-------| | FS | Startup Simplificado |
/registro-startup(público) ·/dashboard/startups/nuevo| creastartups.simplified_complete=true| | FD | Startup Detallado |/registro-startup-detallado·/dashboard/startups/nuevo-detallado| completa el mismo registro | | FIS | ICG Simplificado |/registro-icg(público) ·/dashboard/icg/nuevo-simplificado| creaicgs.simplified_complete=true| | FID | ICG Detallado |/dashboard/icg/nuevo(solo dashboard) | completa el mismo registro (era «el registro de ICG») | | FI | paraguas de los dos formularios de ICG (FIS + FID) | — | cuando el estándar aplica a ambos | | FUS | Usuario Simplificado (registro público de usuario) |/registro-usuario(público) | crea la cuenta de usuario + PU | | FUD | Usuario Detallado |/dashboard/perfil-usuario/editar| edita el perfil de usuario (PU) | | FU | paraguas de los formularios de usuario (FUS + FUD) | — | · PU = Perfil de Usuario (cuenta) | | FP | Perks (beneficios) | — | · FE = Evento |Relación FS↔FD y FIS↔FID: misma entidad/tabla; el simplificado la crea, el detallado la enriquece (no la pisa). El link público de ICG (
/registro-icg) lleva obligatoriamente al FIS; el FID solo se alcanza desde el dashboard. El correo del FIS no es el contacto del FID (el FID trae contacto separado), excepto la categoría Inversionista Individual, donde el registrante ES el contacto.REGLA — sesión post-registro (Frank 2026-07-01): todo formulario público que cree una cuenta de usuario debe iniciar sesión del cliente tras registrar (
signInWithPasswordcon las credenciales recién creadas) para que el usuario caiga loggeado en/ecosistema, no en un estado sin sesión. Aplica a FUS, FS, FIS, FE y cualquier formulario público nuevo. (El registro server-side crea la cuenta pero NO establece sesión de navegador por sí solo.)
- Color de las preguntas (labels): azul
--lpdi-navy(#003E88). NO navy oscuro (--text-strong). En perks/FP el color va en.fg label.flbl .left. - Asterisco de obligatorio: UN solo mecanismo por formulario. En perks el asterisco va vía
::afterSCOPED a.fg label.flbl .left.required(NO.required::afterglobal — se filtra a FD/FI y duplica). En FD/FI/FE el asterisco es literal<span class="required">*</span>. Nunca combinar literal +::afteren el mismo label (causa doble asterisco). - Stepper (paso a paso): estilo FD/FI —
.stepper-itemen columna (círculo 36px + label debajo) +.stepper-connectorentre pasos; activo con halo (box-shadow: 0 0 0 4px var(--accent-dim)), completados en verde (#22c55e). El puntaje por panel (X/Y) es solo de FD/FI (miden completitud del perfil); el FP NO lo lleva. La barra de avance con % del formulario es gamificación independiente y se mantiene; la barra de «perfil completo / visibilidad» es exclusiva de FD/FI. - Formato de fechas: SIEMPRE
dd-mmm-aaaa(ej. 15-jun-2026): input de texto + botón calendario (showPicker). Lógica:MESES+formatDate/parseDate/onDateInput. NO usartype="date"nativo suelto. (Pendiente: extraer a componente compartido para FE/FD/FI/perks.) - Formato de números (Frank 2026-08-04): TODA cifra/conteo mostrado en paneles, métricas, contadores y tablas del SL lleva separador de miles en formato español:
valor.toLocaleString('es')(→1.000,2.505). NUNCA imprimir el número crudo (1000). Aplica ametrics-bar, contadores de pestañas (ej. panel de aprobación de eventos,apr-stat-n), contador del catálogo de eventos (EventosCatalog«N eventos en el sistema»), etc. Es la convención ya usada en/ecosistema(metric-num). - Logo: se carga como ARCHIVO (dropzone), nunca como URL. Obligatorio. Debe poder cambiarse aunque ya exista uno guardado (input file SIEMPRE en el DOM + botón «Cambiar» en la rama de logo existente).
- Marco rojo en obligatorios sin diligenciar (Frank 2026-06-22): propiedad de TODOS los formularios del SL. Al cambiar de panel (o validar), las preguntas obligatorias no diligenciadas quedan enmarcadas en rojo además del banner de errores arriba. Mecanismo: el marcado bindea
class:input-errorcontra el mapafieldErrors(que cadavalidatePanel/validateSteppuebla); el marco vive en CSS. En FD/FI la regla.input-errores local al componente; en perks (FP, externo) vive enperks.css(.form-input.input-error, .form-select.input-error, .form-textarea.input-error, .file-drop-zone.input-error, .radio-card.input-error→border-color:#ef4444 !important; box-shadow:0 0 0 2px rgba(239,68,68,0.15) !important). Color rojo canónico del marco:#ef4444.
Estándares V.2 (Frank 2026-06-23) — pop-ups, encabezados, correos, papelera
Cierra los vacíos que causaron reprocesos. Estos patrones son CANÓNICOS; no crear variantes nuevas.
Pop-ups / Modales del Sistema (unificado — reemplaza «7. Modales»)
- Overlay:
.modal-overlayposition:fixed; inset:0; background:rgba(0,0,0,0.55); z-index:200(siempre 200, por encima de todo), flex centrado, padding 20px. - Caja:
.modal-boxbackground:var(--bg-surface),border:1px solid var(--border),border-radius:14px,padding:24px,max-width:400px,box-shadow:0 12px 32px rgba(0,0,0,0.25),display:flex; flex-direction:column; gap:14px. - Variante CENTRADA de confirmación/notificación (la aprobada por Frank, espejo del pop-up «¡Beneficio enviado!»): caja con
text-align:center; align-items:center; círculo-ícono 56px arriba (border-radius:50%, bgvar(--accent-dim)+ íconovar(--accent); verde#22c55e/#d1fae5solo para éxito, rojo#fee2e2/#dc2626para destructivo); título 16–19px navy; descripción centrada--text-muted; acciones apiladas (.modal-actions-col, full-width) o en fila (.modal-actions,justify-content:flex-end). - Botones del modal: primario
.modal-btn(accent, blanco); secundario.modal-btn-secondary(transparente + borde) o.modal-btn-outline(borde accent); cancelar discreto.modal-cancel-link. - Cierre con Escape (REGLA, Frank #22 2026-07-28): todo modal o ventana de detalle/consulta DEBE cerrarse con la tecla Esc, además del clic en overlay y el botón Cerrar. Se cablea con
use:escToClose={onClose}en el overlay (nunca a un CTA). Aplica en especial a los modales de CONSULTA (solo-lectura: detalle de evento, resumen, QR). Los formularios de EDICIÓN pueden omitirlo a propósito para no perder datos con un Esc accidental. Refuerza el checklist §V.2. - Mensajes informativos de estado (REGLA, Frank #14 2026-07-28): los mensajes informativos/de transparencia que explican un estado o lo que hará el sistema (p. ej. «speaker/host registrado / no registrado en el ecosistema, le enviaremos…») van en azul del DS (
var(--accent), #006eff), no en gris ni en verde. El gris se reserva para hints neutros/secundarios; el verde para éxito confirmado; el ámbar para advertencia; el rojo para error. Estos avisos de estado son informativos → azul. - Color de texto en modales (REGLA, Frank #9c/#12 2026-07-28): el texto de todo modal usa los TOKENS del DS, nunca un negro nativo ni un navy hardcodeado (#000, #22344c, #0d264c). Títulos y texto de énfasis →
var(--text-primary)(#003e88); cuerpo/valores →var(--text-secondary); notas/hints →var(--text-muted). Los<input>NO heredancolorpor defecto: fíjalo explícito avar(--text-primary). Así el modal queda en la paleta de marca y es theme-aware (claro/oscuro). Prohibido hardcodear near-black para texto de modal. - Patrón destructivo / alto-impacto (CANÓNICO, único — Frank 2026-07-05): para toda acción destructiva o irreversible (eliminar cuenta/entidad, transferir entidad, vaciar papelera): (1) círculo-ícono rojo con
alert-triangle(#fee2e2/#dc2626); (2) bloque de impacto en rojo (.impact-box: bg#fee2e2, borde#fca5a5, texto#991b1b) describiendo la consecuencia; (3) escribir literalmente la palabra de la acción (ELIMINAR,TRANSFERIR) para habilitar — y/o contador de segundos; (4) botón danger#c0392b, deshabilitado hasta cumplir el gate. No crear variantes azules para estas confirmaciones. Referencia: «eliminar borrador de beneficio». - El modal «Necesitas una entidad registrada» (sidebar, sin entidad) usa esta variante centrada.
- Implementación (técnica — clave para que NO se repita el bug de «dos versiones» / descentrado, Frank 2026-07-04): el modal se monta con
.modal-overlay(fixedinset:0, flex-center) + la caja como hijo de flujo normal (position:relative; width:100%; max-width:520px). Referencia viva correcta: el modal «Actualizar contraseña» enDashboardSidebar.svelte. - PROHIBIDO
use:portal(mover el nodo a<body>): deja nodos huérfanos al cambiar de versión de chunk → coexisten el modal viejo y el nuevo y «juegan entre dos versiones». - PROHIBIDO
position:fixed+transform:translate(-50%,-50%)+width:min(…,Nvw)en la caja: colapsa el texto en columnas bajo el zoom del navegador. - Ningún ancestro de un modal
position:fixedpuede tenertransform/filter/backdrop-filter/will-change(crea un bloque contenedor → el overlay se ancla a ese ancestro y colapsa el modal a su ancho). Por eso el drawer móvil anima conleft, notransform. - REGLA FUERTE (Frank 2026-07-05, root-cause del colapso intermitente): los modales de cuenta/entidad NO se renderizan dentro del
<aside class="sidebar">(260px). Aunque hoy el aside no tengatransform, cualquiertransform/filterfuturo o intermitente en él atrapa el overlayfixedy lo colapsa a 260px. Se renderizan endashboard/+layout.svelte, FUERA del<aside>(disparados por eventos desdeDashboardSidebar). Verificado: contransformen el sidebar, el modal fuera del aside se mantiene a 520px; dentro, colapsa a ~228px. - ENDURECIMIENTO (belt-and-suspenders, Frank 2026-07-05): el
.modal-overlayusawidth:100vw; height:100vh(con fallback100dvw/100dvh) en lugar deinset:0. Las unidades de viewport son inmunes a un bloque contenedor accidental de cualquier ancestro, así que aunque untransform/filterse cuele en un padre, el overlay NO colapsa. Verificado: modal dentro de un aside contransform→ overlay 100vw, caja 520px (no colapsa). - CERRAR CON Esc (REGLA DEL SISTEMA, Frank 2026-07-24 · tarea 79): TODO modal/pop-up debe poder cerrarse con la tecla Esc. Se implementa con la acción reusable
src/lib/actions/escToClose.ts:<div class="modal-overlay" use:escToClose={<handlerDeCerrar>}>. Esc se cablea SIEMPRE al mismo handler que la «X», el clic en el backdrop o el botón «Cerrar»/«Cancelar»: abandona la vista o el proceso y descarta lo no guardado, sin ejecutar ningún CTA (no enviar, no borrar, no guardar). En modales de confirmación destructiva, Esc cancela, jamás confirma (se pasa el handler de cancelar, nunca el de la acción). La acción escucha endocumentsolo mientras el nodo está montado y se limpia sola. Un componente/pantalla nuevo con un modal no está terminado si su modal no cierra con Esc.
Encabezado de formulario (.form-top) — alineado a FD/FI
- Título en MAYÚSCULAS, navy
var(--text-primary)(#003e88), 17px/700,letter-spacing:0.3px, centrado, estilo «FORMULARIO DE …» (p. ej. «FORMULARIO DE PUBLICACIÓN DE BENEFICIO»). Sin pill/eyebrow sobre el título. - La franja superior con el logo (
FormBrandHeader) va en TODOS los formularios (incluido el externo).
Títulos de panel y subsecciones
.panel-h(título de cada panel): navyvar(--text-primary)(#003e88), 18px/700, centrado.- Subsecciones dentro de un panel:
.subpanel-header(flex, gap, borde inferior) con círculo numerado 28px bg accent (A, B, …) +.subpanel-title1rem/600 navy +.subpanel-desc0.85rem--text-secondary. NO usar encabezados ad-hoc (.section-subqueda obsoleto).
Panel de Revisión (perks FP/externo)
.rev-hero (wrapper degradado linear-gradient(135deg,var(--blue-50),#eef2fb) + tarjeta blanca: logo + pill de categoría arriba, nombre 20px navy, frase de valor, CTA azul a la derecha) → .rev-sections/.rev-sec (bg --surface-sunken + borde, cabecera con «Editar» + divisor inferior, filas .rev-kv con divisores) → nota centrada. Una sola barra de progreso (la global del formulario); NO duplicar dentro del panel.
Tarjetas de resumen clicleables (stat-strip) — patrón general
REGLA (Frank 2026-06-24): en TODO panel con stat-strip, cada .stat es un <button> que filtra el
listado por su estado; la card activa se resalta con .stat.active (borde var(--accent) + sombra
0 2px 12px rgba(0,110,255,.15)); hover sube translateY(-1px) + borde accent. La primera card
(«Total / Beneficios / Eventos totales») limpia el filtro. Las cards cubren los estados reales del
módulo (no un subconjunto arbitrario). Los conteos del stat-strip son globales y estables (sobre
todos los registros, no la lista filtrada) para que los números no salten al filtrar desde las propias
cards — calcular server-side sin el filtro de status.
Aplicado en: dashboard/beneficios (aliado), dashboard/beneficios/admin, dashboard/eventos/admin.
Sistema de correos (FP/FE) — src/lib/server/notification-emails.ts
- Shell único: logo → franja azul
border-top:2px solid #006eff→ título navy #003e88 (centrado) → cuerpo (p15px/1.6#374151) → botón único azul#006effradius 8px → footer (logo tenue + «Ecosistema LPDI · eco.lpdi.co»). Sin código de colores de estado dentro del correo (el semáforo de estado vive en el panel admin y «Mis beneficios»). - Transporte: nodemailer/SMTP (Resend),
from "Ecosistema LPDI" <no-reply@lpdi.co>. Envío que no bloquea el flujo (try/catch). - FP (8): registro exitoso, nuevo por aprobar (→equipo), aprobado, actualizado, cambios por aprobar (→equipo), no aceptado, terminado, eliminado. FE (7): registro exitoso, nuevo por aprobar (→equipo), aprobado, actualizado, cambios por aprobar (→equipo), no aceptado, eliminado. Inventario oficial:
playgrounds.digitalhubassist.ai/pmo/informe-emails-lpdi-completo.html. - REGLA (Frank 2026-07-01): todo cambio, mejora o correo nuevo DEBE reflejarse en el inventario oficial de correos. Cada correo tiene un número oficial (01 Confirmación de Correo, 11 Bienvenida Startup, 12 Bienvenida CEO, 15 Recordatorio día 3, 16 Recordatorio Urgente día 6, 18a/b Invitación al Equipo, 31 Reporte Diario Ecosistema, …). Al tocar
notification-emails.ts/email*.tso agregar un correo: (1) actualizar el HTML del inventario, (2) regenerar los renders PNG (scratchpad/regen_emails.mjs) y bumpear?v=N, (3) mantener sincronizada la numeración. Un correo sin entrada en el inventario se considera incompleto. (Pendiente concreto: el #31 Reporte Diario no refleja los últimos cambios.)
Diseño aprobado del REPORTE de inventario de correos (Frank 2026-07-02)
Archivo canónico: playgrounds.digitalhubassist.ai/pmo/informe-emails-lpdi-completo.html (fuente en ~/playgrounds/pmo/). Este es el ÚNICO formato válido del reporte — no usar variantes viejas (p. ej. inventario-correos-lpdi-rev9.html, tema oscuro). Font DM Sans, fondo #f4f4f5, título navy #003e88.
- Estructura: cabecera (título + subtítulo + bloque de taxonomía) → índice/TOC (table.idx-table agrupado por categoría, con dot de color + conteo) → tarjetas por categoría.
- Categorías y su color (borde/acento del .card-header y .badge-num): USUARIO #6366f1 · STARTUP #16a34a · ICG #ea580c · CRON #7c3aed · FP #006eff · FE #0891b2.
- Cada tarjeta (div.card id=C<NN>): .card-header {categoría} con .badge-num (número oficial) + .tag-new «NUEVO» (opcional) + .card-title; luego table.field-table con las filas Nombre técnico (tax-code), Función en código, Asunto, Trigger, Destinatario, CTA, Descripción, TyC / Tratamiento; y al final div.preview-section con <img class="preview-img"> al render emails/EMAIL-<NN>.png?v=N.
- Taxonomía del nombre técnico: CATEGORÍA / DESTINATARIO / EXISTENCIA / PRIVILEGIO / TIPO MENSAJE / TYC / FUENTE (p. ej. USUARIO/USER/AMBOS/NO-ED/Actualización T&C y Política/NO-TYC/LPDI).
- Al agregar un correo: card en su categoría + fila en el TOC + preview embebido en emails/ + actualizar los conteos (por categoría y total). El número oficial es global y secuencial (siguiente libre).
- Conteos = correos ACTIVOS (los 3 eliminados van aparte, atenuados con opacity:0.5;border:2px dashed). La suma de la leyenda por categoría debe igualar «N correos activos». Nombres de categoría en la leyenda: USUARIO · STARTUP · ICG · CRON · PERKS · EVENTOS (Frank 2026-07-02; antes «FP»/«FE»).
- Labels de versión (Frank 2026-07-02): .tag-new «NUEVO» = correo nuevo frente a la versión anterior del inventario; .tag-mod «MODIFICADO» = correo modificado frente a la versión anterior. Se recalculan en cada bump de versión (limpiar los pegados): solo lo agregado/cambiado desde la versión previa lleva label. Bumpear Versión N + fecha en el header al publicar cambios.
- Cada ficha trae un enlace .back-to-index («↑ índice» → #indice) en su .card-header para volver al índice.
Papelera / soft-delete (espejo del borrado de entidades)
- Columnas
deleted_at+delete_scheduled_for(purga a 30 días). El listado normal excluyedeleted_at IS NOT NULL. - El acceso «Papelera (N)» está SIEMPRE visible (no se oculta al quedar vacía, igual que el papelera de entidades/
TrashSection). Al abrirla vacía muestra el estado vacío «La papelera está vacía» (ícono gris 34px + texto centrado). - Sección «Papelera» plegable: badge de días restantes (
daysLeft; rojo<7, ámbar<15, verde) + Restaurar + casillas de selección + barra de acciones con «Seleccionar todo / Deseleccionar todo», «Eliminar seleccionados (N)» y «Vaciar papelera» (doble confirmación, irreversible). Los nombres de los elementos eliminados van alineados a la izquierda. Purga definitiva vía endpoint cron protegido porCRON_SECRET.
Analítica (GA4)
gtag.js global en src/app.html (ID G-J38PDEV5MC). Eventos de conversión sign_up_startup / sign_up_icg al crear la entidad (el server devuelve created:true en el INSERT y el cliente dispara trackSignUp). La medición mejorada cubre la navegación SPA.
Estándares V.3 (Frank 2026-06-23) — paneles admin, Mis beneficios, canje de usuarios no registrados, Referidos
Documenta lo construido en el módulo Beneficios que aún no estaba en el DS. Patrones CANÓNICOS; al crear paneles/páginas nuevas del mismo tipo, espejarlos.
Página «Mis beneficios» (aliado) — dashboard/beneficios
- Superficie
.perks-surface(perks.css scoped). Cabecera.dash-head: título «Mis beneficios» + subtítulo a la izquierda; a la derecha.head-actions(flex, gap 10px) con enlace «Mis referidos» (.btn-secondary, a/dashboard/beneficios/referidos) + «+ Crear beneficio» (.btn-primary, enlace a/nuevo). - Stat-strip de 6 (
.stat-strip.strip-6): Beneficios (total, limpia filtro) · Publicados · Aprobados · En revisión · Inactivos · Borradores/ajustes. Cada.states un botón que filtra el listado por estado; la activa se resalta (.stat.active). Ver «Tarjetas de resumen clicleables». - Lista de fichas por beneficio: logo/monograma + nombre + categoría + estado (badge de color del semáforo de estados) + vigencia (Permanente / «Hasta dd-mmm-aaaa») + métricas. Acciones por estado: borrador → «Continuar borrador» + «Eliminar»; cambios → «Ajustar y reenviar» + «Ver»; publicado/aceptado → «Editar» + «Ver en catálogo»; inactivo → «Editar» + «Eliminar». Solo borrador/inactivo son eliminables (van a la papelera).
- Papelera siempre accesible al pie (ver «Papelera / soft-delete»).
- Mis referidos (enlace de cabecera + ítem en el sidebar, sección Beneficios) navega a su pantalla propia — ver «Pantalla propia de Referidos».
Panel admin de Beneficios — dashboard/beneficios/admin
- Ruta gateada server-side por
is_perk_admin(auth.uid())(RPCSECURITY DEFINER); si no es admin → 403. Roles:super_admin/moderador/revisor(tablaadmin_roles). - Cabecera: badge
.admin-badge«Panel interno · Gestión de aliados» + título «Gestión de beneficios» +.admin-head-actionsa la derecha con enlace «Referidos» (.ref-cta, outline accent, a/dashboard/beneficios/admin/referidos) + «+ Crear perk externo» (.ext-cta, sólido accent con sombra azul). El admin lee TODOS los perks vía policyperks_admin_select_all; enriquece email/logo del aliado con service-role (no es dueño de esosprofiles/entidades). - Stat-strip por estado + tabs/filas con bandera «nombre editado», acciones de revisión y toggle «destacar» (
featured). - Modal de revisión (acciones del admin sobre un perk): aprobar y publicar / publicar / solicitar cambios (con feedback obligatorio) / desactivar. Usa el estándar de modales V.2. Cada acción dispara el correo FP correspondiente al aliado (ver «Sistema de correos»).
- Modal «Re-asociar perk externo»: vincula un perk externo a una entidad ya registrada; tras re-asociar, el logo pasa a tomarse de la entidad.
- El % de avance de borradores se calcula server-side sobre los campos obligatorios del FP (no se persiste).
Pantalla propia de Referidos (ReferidosPanel.svelte + rutas dedicadas)
REGLA (Frank 2026-06-23): los listados largos NO van en pop-up/modal NI mezclados en línea con otra vista — van en su PANTALLA PROPIA (ruta dedicada del dashboard). Los modales se reservan para confirmaciones/avisos cortos.
- Rutas: aliado → dashboard/beneficios/referidos (gateada por sesión + canAccessPerks); admin → dashboard/beneficios/admin/referidos (gateada por is_perk_admin). Cada una usa la superficie estándar .perks-surface + .dash-head (título «Mis referidos» / «Referidos (admin)» + subtítulo + botón Volver .btn-secondary).
- Accesos: ítem «Mis referidos» en el sidebar (sección Beneficios, {#if canPublishPerks}) + enlace en la cabecera de Mis beneficios; en admin, enlace «Referidos» en la cabecera del panel admin.
- Componente ReferidosPanel.svelte (prop admin): tarjeta .ref-section (NO overlay, NO position:fixed) con chip de conteo + búsqueda + [filtros admin] + tabla. NO repite título (lo provee la página).
- Búsqueda por texto (.form-input) siempre visible: filtra al instante por nombre/correo/beneficio/aliado (cliente).
- Modo admin: filtros (.form-select): aliado / tipo de entidad / categoría / tipo de canje + rango de fechas + Limpiar (.btn-secondary). Validación de rango: «Hasta» < «Desde» marca los campos con .input-error (marco rojo del DS) + mensaje, y no aplica el filtro inválido.
- Fechas: patrón canónico del DS — input de texto dd-mmm-aaaa + botón calendario (showPicker sobre un type=date oculto). La columna de fecha de la tabla también va en dd-mmm-aaaa. NUNCA type=date nativo visible.
- Fecha de apertura ≥ hoy (Frank #32, 2026-07-29) — REGLA DEL SISTEMA. La fecha de apertura/inicio de vigencia de un beneficio (FPI y FPE) no puede ser anterior a hoy. Doble barrera: (1) el type=date oculto lleva min = hoy (YYYY-MM-DD) para que el calendario ni siquiera permita elegir una fecha previa; (2) validación en el submit que bloquea e informa el error. Al editar un beneficio ya abierto, el min NO aplica (se respeta la fecha de apertura original que pudo quedar en el pasado). Patrón replicable a cualquier fecha «no puede ser en el pasado».
- Tabla .ref-table: Nombre (navy/600) · Correo (mailto azul) · Teléfono · Beneficio · [Aliado en admin] · Tipo de canje (badge pill) · Fecha de solicitud. Estados de carga (spinner) y vacío (ícono usuarios).
- Datos desde perk_redemptions (tabla append-only, F5) + profiles (nombre/correo/teléfono) vía endpoint GET /api/perks/referidos (?admin=1 gateado). El teléfono prioriza lead_phone del canje; si no, profiles.phone.
Lógica de canje — usuarios NO registrados / no-aliados (RedeemFlow.svelte, F5)
Comportamiento de un usuario del ecosistema (o visitante) frente a un beneficio publicado, en perks/[id].
- Gate de autenticación: si el usuario NO está autenticado, al pulsar el CTA de canje se redirige a /auth/login?redirectTo=<ruta actual>. La ficha pública también ofrece «Crear mi cuenta gratis» → /registro-usuario. El dueño del perk no puede canjear el suyo (isOwner en cliente + bloqueo server-side).
- 4 ramas según redemption (componente único, render condicional):
- code → muestra código + enlace (fase success-code).
- link → entrega enlace y lo abre en pestaña nueva (success-link).
- schedule → entrega URL de agenda y la abre en pestaña nueva (success-schedule).
- lead → formulario de lead (fase lead-form): teléfono + casilla de consentimiento explícito obligatoria («para que el aliado te contacte»); sin ambos, el server rechaza. Al enviar, manda email al aliado y registra el canje (success-lead).
- Fases: idle → [lead-form] → submitting → success-* | error. Todo canje (las 4 ramas) escribe una fila en perk_redemptions (trazabilidad inmutable: sin UPDATE/DELETE); el de tipo lead además guarda lead_phone/lead_consent y marca failed si el email no sale. Incrementa clicks del perk (best-effort).
- El registro de canjes alimenta el panel Referidos (arriba).
13. Búsqueda por texto — insensible a tildes y mayúsculas (estándar, Frank 2026-06-24)
REGLA DURA: ningún buscador del Sistema es sensible a tildes ni a mayúsculas. «tecnologia» debe matchear «Tecnología», «bogota» → «Bogotá», «MEDELLIN» → «Medellín».
- Normaliza con el helper canónico
src/lib/utils/normalizeForSearch.ts(normalizeForSearch(s)=s.normalize('NFD').replace(/[̀-ͯ]/g,'').toLowerCase()) en ambos lados de la comparación:normalizeForSearch(campo).includes(normalizeForSearch(query)). - Aplica a TODO filtro de texto libre del usuario en pickers, listas y tablas (catálogo de perks, ReferidosPanel, PhoneInput, paneles admin de eventos/beneficios, super eventos, pickers de temática/actividad/país/zona horaria).
- NO aplica a comparaciones exactas de IDs ni a la normalización de emails para guardado
(
.trim().toLowerCase()de un email destinado a la DB no es una búsqueda). - Server-side (startups/ICG): las consultas
dashboard/consulta/startups,dashboard/consulta/icgyapi/entidades/searchfiltran contra columnas normalizadascompany_name_norm/organization_name_norm(migración072:unaccent+ funciónimmutable_unaccentlower+unaccent, columnas generadas STORED con índice), normalizando el término connormalizeForSearch. Resultado: insensibles a tildes Y mayúsculas, igual que el cliente. (Antes era una deuda conilikedirecto.)
14. Panel de aprobación de eventos (dashboard/eventos/admin/aprobacion)
Pantalla aparte del panel resumen (/admin, que queda como vista compacta). Gateada is_event_admin.
Tabla ANCHA con scroll horizontal (.apr-table width:max-content; min-width:100%), una fila por
evento. Columnas = TODOS los campos del formulario de registro en el MISMO ORDEN de los paneles
(1 Contacto → 2 Datos clave → 2B Ubicación → 3 Temática → 4 Registro) + trazabilidad (creado por/correo
+ fecha, aprobado por/correo + fecha, editado por + fecha; eliminado por va en la papelera del resumen).
Encabezados clicleables ordenan por columna; la columna Acciones es position:sticky; right:0.
Acciones por fila: Aprobar (PATCH aprobar) · Editar (ficha del evento) · Rechazar
(modal DS con feedback obligatorio, PATCH rechazar) · Eliminar (doble confirmación DS, DELETE →
papelera). Búsqueda insensible a tildes (§13) + descarga CSV de las filas filtradas. Reutiliza los
endpoints existentes; no introduce escrituras nuevas.
15. Estandarización de paneles (Frank 2026-06-30)
Estos estándares aplican a todos los paneles de admin y del usuario (Administrar eventos, Administrar beneficios, Supereventos, Mis eventos, Mis beneficios, Mis referidos) para que se vean como un solo sistema. Reemplazan cualquier guía anterior que contradiga estos puntos.
15.1 Íconos de acción (CTAs) — estilo «soft»
Las acciones por fila/card son botones de ícono (no texto), estilo «soft» (Tailwind UI / Stripe /
Linear): ícono de color sobre fondo tintado del mismo color al ~10–12 % con borde al ~22 %. NUNCA
ícono blanco sobre color pleno (queda cargado), NUNCA outline plano gris (se lee confuso).
- Tamaño 30–32 px, radio 8 px. .icon-btn + variante semántica.
- El color comunica el TIPO de acción, no el módulo (mismo color = mismo significado en todos):
- solid-go verde #15904f — Aprobar / Aprobar y publicar / Publicar / Aceptar
- solid-info azul #006eff — Reactivar / Editar / Restaurar
- solid-warn ámbar #ea580c — Solicitar cambios / Desactivar / Rechazar
- solid-danger rojo #c0392b — Finalizar / Eliminar
- solid-neutral gris #5b6b80 — Duplicar / Ver / Re-asociar / Destacar
- Cada botón lleva aria-label + title con el nombre de la acción (el ícono no basta para lectores).
- Catálogo oficial de íconos (ícono · color · función · panel): playgrounds.digitalhubassist.ai/lpdi-iconos-ds.html.
15.2 Cards de resumen (stat-strip) — criterio de color ÚNICO
Reemplaza el color-por-estado anterior. Los NÚMEROS de las cards usan un solo criterio en TODOS los paneles:
- Navy de marca #003E88 por defecto.
- Azul #006eff cuando la card está seleccionada (filtro activo).
- Rojo #c0392b solo en «Eliminados».
- El label siempre en MAYÚSCULA, font-size:11.5px; font-weight:600; letter-spacing:.02em; color:text-muted.
- Grid repeat(auto-fit, minmax(116px, 1fr)) (envuelve en varias filas; no fijar N columnas).
15.3 Títulos de panel + pill (eyebrow)
- Título: NUNCA negro. Primera palabra en navy
--lpdi-navy, palabra clave en azul--accent(<h1>Administrar <strong>eventos</strong></h1>). Aplica a.admin-head h1,.dash-head h1,.me-head h1. - Pill (eyebrow) arriba del título, mismo estilo visual; el color marca el contexto:
- Paneles de admin → ámbar (
.admin-badge), texto «Panel interno · Gestión de [módulo]». - Paneles del usuario → azul (
.dash-eyebrow/.me-eyebrow), texto «Mis [módulo]».
15.4 Estado vacío unificado (EmptyState.svelte)
Todo listado/tabla usa el componente src/lib/components/EmptyState.svelte (card con borde + ícono en
cuadro tintado + título navy + subtítulo + acción opcional). Props: title, subtitle, actionLabel,
onAction. PROHIBIDO el texto plano suelto como estado vacío. Acción típica: «Ver todos» / «Limpiar
filtros» / crear.
15.5 Ancho de paneles
- Tablas/paneles de admin aprovechan el ancho:
.admin-wrap { max-width: min(1680px, 96vw) }(o.admin-wrap.wide). - Paneles del usuario:
.dash-wrap { max-width: min(1400px, 95vw) }.
15.6 Doble confirmación
Acciones sensibles (desactivar, finalizar, reactivar, restaurar, duplicar, eliminar) usan modal con checkbox de confirmación; la 1ª confirmación es abrir el modal, la 2ª es marcar el checkbox. El texto del checkbox es específico por acción («Confirmo que quiero finalizar este beneficio»), nunca genérico ni heredado de otra acción.
15.7 Máquina de estados de Beneficios
borrador → enviado → aceptado → publicado; más inactivo y finalizado.
- Inactivo = cierre manual anticipado: se Reactiva (no se edita ni elimina). Al reactivar vuelve a
Publicado (vigencia iniciada) o Aprobado (vigencia futura; el cron lo publica en la fecha).
- Finalizado = cierre definitivo (manual o automático por vigencia vencida, vía cron
publish-scheduled): solo lectura, no editable ni eliminable (salvo Superadmin), se puede Duplicar.
- Publicado = visible para todos (vigencia ya inició). Aprobado = aprobado pero aún no inicia por fechas.
- Cards en orden: Todos · Publicados · Aprobados · Pendientes · Cambios por aprobar · Finalizados · Inactivos · Borradores · Eliminados.
15.8 Módulo de Permisos (dashboard/admin/permisos)
Matriz usuario × módulo con roles: admin, curador, editor_confianza, duplicador. Solo
super_admin gestiona. El flag Duplicar (duplicador) habilita duplicar registros de otros; el dueño
duplica lo suyo siempre, el superadmin y el admin de módulo también (can_duplicate = super OR rol admin
OR rol duplicador en el módulo). El curador lo tiene OFF por defecto.
15.9 Sidebar — comportamiento por defecto
Al entrar al Dashboard se despliegan solo las secciones con opciones activas (Mis Startups/PyMEs, Mis
ICG, Contenidos, Beneficios, Administración); las secciones 100 % «Próximamente» (Explorar el ecosistema,
Matchmaking, Networking, Newsletters) arrancan plegadas. Dentro de una sección activa, las
subsecciones con opciones activas van desplegadas (Eventos) y las 100 % inactivas plegadas
(Convocatorias, Noticias). El estado se persiste en localStorage (lpdi-sidebar-sections-v2).
Orden de secciones: Explorar el ecosistema · Mis Startups/PyMEs · Mis ICG · Contenidos · Beneficios ·
Matchmaking · Networking · Newsletters · Administración.
15.10 Modal de edición (plantilla — EventEditModal.svelte)
Patrón canónico del modal de edición desde un panel de admin. Es la plantilla para los futuros
modales de edición de Convocatorias y Noticias. Estructura (.eem-*):
- .eem-overlay + .eem-modal (dialog, aria-modal), .eem-head con <h2> («Editar [entidad]») +
subtítulo con el código (.eem-sub) + botón cerrar (.eem-close).
- .eem-body: el formulario completo pre-llenado, en el mismo orden de paneles que el formulario de
registro (para eventos: 1 Contacto → 2 Datos clave → 2B Ubicación → 3 Temática → 4 Registro).
- Solo lectura para registros finalizados: readOnly cuando el registro ya terminó (salvo Superadmin,
prop canEditFinalizado). Se aplica el atributo inert a .eem-body (desactiva TODA edición y
foco) + banner .eem-readonly-banner («🔒 … solo lectura»).
- Validación al guardar (triedSave): asteriscos * en obligatorios, resaltado rojo .input-error
en los campos faltantes, y banner de errores bloqueante .eem-error-banner («Corrige lo siguiente
antes de guardar») que impide el guardado hasta resolver (obligatorios, incongruencias de
país/provincia/zona horaria, speakers incompletos). Los avisos no bloqueantes van en .eem-warn-banner.
- Doble control de seguridad: confirmación al guardar y aviso al cerrar sin guardar si hay cambios.
- Al guardar con éxito, el panel refresca la fila (no recarga la página).
16. Catálogo público de beneficios y ficha del aliado (/perks, /perks/[id])
Cara pública del módulo Beneficios (accesible a toda la comunidad). Superficie .perks-surface con los
tokens de marca.
16.1 Catálogo /perks
- Hero de marca
.heroL: eyebrow con punto («Beneficios exclusivos para la comunidad»),<h1>con palabra destacada (.hlazul/teal), glow + imagen de cristal decorativa. Entra con fade+slide. - Carrusel «abanico» de destacados (
.oceanFeat): muestra los perks marcadosfeaturedpor el admin (desde 1). Label «Destacados» + «Los perks del momento»; se elige un aliado para ver su beneficio. - Grilla de perks filtrable: chips de categoría (con conteo) + búsqueda por texto insensible a tildes/mayúsculas (§13). Cada card: logo/monograma + nombre + categoría + headline.
- Estado vacío →
EmptyState(§15.4). - Enlaces por slug (Frank 2026-07-08): las cards enlazan a
/perks/{slug}(ej./perks/notion), no al UUID. El slug se autogenera vía triggerset_perk_slug(migración 107); la ficha resuelve por slug o UUID.
16.2 Ficha del aliado /perks/[id]
- Banda hero
.dhero-band+ breadcrumb.breadcrumbL. - Cabecera
.dhero: chip de categoría con color (.dh-catchip), nombre.dh-name, headline.dh-head, meta-pills (.dh-pill: badge de valor + audiencia «Ideal para startups» / «Toda la comunidad»), y panel visual.dh-visual/.dh-panelcon la vigencia (Permanente / «Hasta dd-mmm-aaaa»). - Secciones: «Qué incluye» (lista
includes), «Sobre el aliado», «Más perks de la comunidad» (relacionados). Nota de reactividad: los datos de la ficha se derivan con$:desdedatapara que al navegar entre perks (misma ruta) se actualice el contenido correcto. - Canje: lo maneja
RedeemFlow.svelte(F5) con 4 ramas (code / link / schedule / lead). La ramaleadexige teléfono + consentimiento explícito (ver §V.3 «Lógica de canje»). Cada canje registra enperk_redemptions(append-only) y alimenta «Mis referidos».
16.3 Restyle de presentación del catálogo y la ficha (Frank 2026-07-08) — SOLO diseño
Ajustes visuales del catálogo/ficha públicos. NO se tocó backend, endpoints, RLS ni el flujo de canje.
- Íconos de categoría (SSOT):
src/lib/data/perk-categories.tses la fuente única. Cada categoría tiene un campoicon(path SVG lucide); helpergetPerkCategoryIcon(id)+PERK_ALL_ICON(grid). Se usan en: chips de filtro del catálogo (ícono de categoría, NO punto de color), badge de categoría de las cards, y chip de categoría de la ficha (.dh-catchip .cc-ic, reemplaza al.cc-dot). SVG 24×24 viewBox,stroke-width2, tamaños: chip filtro/badge 13–16px, catchip ficha 14px. - Audiencia como etiqueta discreta (no pill): ícono + texto atenuado (
.eligLen cards,.dh-eligen la ficha). Cohete = «Ideal para startups»; usuarios = «Toda la comunidad». HelperaudienceIcon(a). - Botón outline (
.btnL.outline): borde fino1px solid rgba(0,110,255,0.30)+ relleno azul suavergba(0,110,255,0.10)+ glow interno, igualado al.fanL-btnde la tarjeta destacada. - Micro-ajustes:
<h1>«Un océano de perks» sin punto final;h2del CTA final enfont-weight:700(el 800 era faux-bold sintetizado, se veía más pesado); gema/barco del CTA reubicada y reducida.
⚠️ GOTCHA CRÍTICO — colisión
.outlinecon Tailwind. La clase modificadora del botón se llamaoutline(class="btnL outline") y choca con la utilidad de Tailwind.outline { outline-style: solid }. Con el color de texto azul (currentColor) y el anchomedium(3px) del navegador, eso dibujaba un anillooutlinede 3px azul sólido alrededor del botón, que se leía como un «borde grueso». Elborder(1px) nunca fue el problema. Fix:.btnL.outline { outline: none; ... }+ foco accesible propio vía.btnL.outline:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px }. Al crear cualquier clase que se llame igual que una utilidad de Tailwind (outline,ring,shadow,border, etc.), verificar el estilo computado (no solo elborder) para descartar colisiones.
16.4 Revisión en vivo y refinamientos (Frank 2026-08-09) — SOLO diseño
Segunda pasada de diseño sobre /perks y la ficha (revisión en vivo con Frank). NO se tocó backend, RLS ni el flujo de canje. Componentes: perks/+page.svelte, perks/[id]/+page.svelte, src/lib/styles/perks.css, LapdiSiteHeader.svelte, LapdiSiteFooter.svelte.
- Cards de perk
.cardX(catálogo y «Más perks» de la ficha son IDÉNTICAS). Estructura fija: badges (categoría + descuento), meta con país (.cardL-ctry, con «+N más» que despliega sin navegar) + beneficiarios como chips (.cardL-aud), y CTA como botón anclado al pie (.cardL-cta,margin-top:auto→ parejo entre cards). El descuento va en pastilla SÓLIDA azul (.badgeL.value,background:var(--accent), texto blanco) — el verde se reserva para éxito. Abren en pestaña nueva. Los estilos de la card viven enperks.cssglobal (incluida.cardL-ctry*), reusados por el catálogo y por la ficha para que se vean iguales. - Resalto de color en el borde superior de la card (
.cardL::after, 3px): aparece al pasar el cursor (da sensación de movimiento), NO permanente. El color sale de--brand, que cada card fija con el color de su categoría (_catColor(category)=perk_categories.color). El mismo resalto hover se replica en las cards informativas de la ficha (paneles.panelL), con el color de la categoría del perk (se fija--branden.col-main). - Filtros del catálogo dentro de un marco
.filtersCardL; multiselección acumulativa (categoría/audiencia/país como arrays: OR dentro de una dimensión, AND entre dimensiones; «Todos» = vaciar la dimensión); país = panel buscable.ctrySel; chips de filtros activos con quitar + «Limpiar filtros». (Norma general de filtros en §18.) - Ficha — hero. Pill del descuento
.dh-valueen turquesa de marca (#04d9c4, texto navy#05122e);.dh-metaen panel glass; «Ideal para» y «Beneficio disponible en» lado a lado (grid auto / minmax(0,1fr), apilado ≤760px). Íconos de «Ideal para» / país en cian (#34f0e6). - Ficha — «Qué incluye» (
.incCard). El check y el chip toman el color de la categoría del perk (--brand), no el verde plano; el chip lleva un tinte de marca (color-mix) y un acento a la izquierda. El verde del DS (#22c55e) queda reservado para estados de éxito reales, no para «incluido». - Ficha — tarjeta resumen del aliado (
.allySumL). En la columna derecha sticky bajo el canje: logo + nombre + categoría + «Ideal para» + «Beneficio disponible en» + fila «Vigencia» (rango apertura/cierre, o «Permanente» si no tiene fechas). «Beneficio disponible en» también rotula el bloque de países del hero. - Ficha — «Sigue en el Ecosistema» (
.followL). Las 3 cards son IGUALES a las de/perksy en el mismo orden: Eventos (/eventos, pestaña nueva), Convocatorias, Noticias (lpdi.co/noticias, pestaña nueva). Títulos en el navy del DS (--lpdi-navy), no negro. Espaciado equilibrado entre secciones (aire arriba ≈ aire abajo). - Header y footer del sitio en
/perksy la ficha.LapdiSiteHeader/LapdiSiteFooterreplicanlapuntadeliceberg.cousando la fuente Outfit (sin ella se veían distintos al sitio real). Header: nav real (Plataforma/Consultoría/Comunidad/Nosotros) + dropdown «Ecosistema» con Eventos, Convocatorias, Noticias, Beneficios; acciones según sesión; hamburguesa móvil (breakpoint 860px). Footer: columnas reales, logo transparente/lpdi-logo-footer.png(sin recuadro), degradado cian→azul (#00ECDD → #37A0FF) en «Construyamos ecosistema» y «Hecho en Latam», y Startups / Inversionistas / Corporativos → formularios de creación de entidad en el DASHBOARD (/dashboard/startups/nuevo,/dashboard/icg/nuevo-simplificado): el guard del dashboard redirige a/loginsi no hay sesión, porque el usuario debe registrarse antes de crear una entidad. ElAppFooterglobal del layout raíz se oculta en/perkspara no duplicar el pie.
17. Trazabilidad y compliance (Frank 2026-07-06) — REGLA DEL SISTEMA
Dos bitácoras append-only (nunca UPDATE ni DELETE; el histórico es la evidencia legal):
consent_log(migración 040): aceptaciones de TYC/PDP por persona (versión, fecha, IP, navegador). La versión sale delegal_documents(ya no es'1.0'fijo).action_audit_log(migración 100): toda acción sensible del usuario. Helpersrc/lib/server/audit.ts→logAction(event|null, entry).
Regla obligatoria (nuevas acciones)
Toda acción sensible NUEVA que se agregue al sistema DEBE registrarse con logAction() en action_audit_log. Es requisito de compliance, no opcional. Cuenta como "sensible" cualquier acción que:
- confirme o rechace un rol/invitación por correo (aceptar CEO, miembro, contacto),
- transfiera, elimine, restaure o cierre una entidad o cuenta,
- cambie credenciales o estado legal (consentimiento, versión aceptada),
- ejecute un cambio irreversible o de titularidad.
Cada registro lleva: quién (actor_user_id + actor_email), qué (action_type), sobre qué (entity_type + entity_id), cuándo (created_at), por dónde (channel: web/email/api/cron), IP y user-agent. logAction es fire-and-forget: nunca debe tumbar el flujo de negocio.
Acciones cubiertas hoy: team_invitation_accepted/rejected, ceo_accepted, transfer_initiated/accepted/rejected/cancelled/expired, account_deletion_requested/confirmed, account_recovered, entity_deleted/restored, consent_accepted, consent_reaccepted_tacit. Si agregas una nueva, añade el action_type al CHECK de la tabla y a AuditAction en audit.ts.
Versionado legal (legal_documents, migraciones 101–102)
- Una fila por versión de cada documento; solo una
is_currentporslug. category:main(TYC/PDP, consentibles) |annex(anexos de la PDP).getCurrentLegalVersionfiltracategory='main'.- Los PDFs viven en el bucket público
legal-documents(Supabase Storage). - RPC
publish_legal_version(...)publica una versión nueva (desactiva la anterior, atómico). RPCrecord_tacit_reacceptance(doc_type, version)marca la re-aceptación tácita de usuarios existentes, fechada a la vigencia (idempotente). - REGLA: no se puede anunciar por correo un cambio de TYC/PDP sin cargar antes la versión nueva como vigente (guard
assertLegalVersionLoaded).
Sesión (Frank 2026-07-06)
- Persistente + inactividad de 30 días (cookie
lpdi_last_activityenhooks.server.ts, app-level; no toca el Auth compartido con SGC). - «Cerrar sesión en todos los dispositivos» →
/api/account/logout-all(signOut scope:'global').
18. Filtros dependientes / facetas acumulativas (Frank + Roberto 2026-07-10) — REGLA DEL SISTEMA
Problema que evita esta regla: filtros independientes permiten combinaciones sin resultados (ej. país=Colombia + ciudad=Lima → 0 resultados), porque cada dropdown lista TODOS los valores posibles de su columna sin tener en cuenta los demás filtros ya activos.
Regla obligatoria: en cualquier panel con múltiples filtros simultáneos, las opciones de CADA filtro se calculan sobre los registros que pasan los DEMÁS filtros activos — nunca el propio. Es decir, un filtro nunca se filtra a sí mismo al calcular sus opciones, pero sí queda acotado por todos los otros filtros encendidos. El resultado: solo aparecen como opción los valores que, combinados con la selección actual, producen al menos un resultado.
Implementaciones de referencia
- Catálogo público de eventos (
src/lib/utils/eventoFacets.ts,buildFacets/passesFilters) — client-side: todo el dataset filtrable ya está en el navegador (catálogo no paginado), así que cada dimensión (subtemática, país, ciudad, organizador, superevento) se recalcula en JS a partir de los eventos que pasan los demás filtros. - Consulta de admin de Startups/PyMEs e ICGs (
src/routes/dashboard/consulta/startups/+page.server.ts,src/routes/dashboard/consulta/icg/+page.server.ts) — server-side: estas vistas son paginadas (12 por página), no hay dataset completo en el cliente, así que la faceta se resuelve con queries a Supabase. PatrónfetchDistinctFacet(columna, filtrosSinEsaColumna): consultaSELECT columnacon el mismo scope base (status='APPROVED',is_public/deleted_atsegún la tabla) más búsqueda + tipo + la OTRA dimensión geográfica activa, dedupe en JS (Supabase JS no exponeDISTINCT) y sort alfabéticolocaleCompare('es'). País y ciudad son mutuamente dependientes: las opciones de país excluyen el filtro de país pero incluyen ciudad+tipo+búsqueda, y viceversa.
Filtro huérfano (valor seleccionado que deja de tener opciones)
Si el usuario tenía un valor elegido en un filtro y otro filtro cambia de forma que ese valor ya no aparece entre las opciones válidas (p. ej. cambia de país y la ciudad que tenía seleccionada no existe en el país nuevo), el filtro huérfano se ignora con gracia: el loader lo excluye de la query principal y de las facetas devueltas al cliente (no se aplica un filtro fantasma que dejaría 0 resultados), y el store del filtro en el cliente se resincroniza al valor efectivo (vacío) en el siguiente render. No hace falta que el usuario limpie el filtro a mano.
Excepción documentada: industria
El filtro de industria en Startups/ICG es no-op (LAP-259 hotfix: industry_id no existe como columna en startups/icgs, la industria vive en JSONB/array y se reimplementará con contains_any). Como no filtra resultados reales, su lista de opciones es el catálogo completo (catalogo_industria) sin acotar — no participa del cálculo de facetas hasta que el filtro de industria sea funcional.
Alcance actual
Aplicado a país ↔ ciudad en ambos paneles de consulta de admin (los dos filtros primarios geográficos). El filtro de tipo de empresa/ICG en el sidebar (selectedTypes) sigue siendo client-side sobre la página cargada (12 registros) — no está cableado a la URL/servidor todavía, así que no participa de las facetas dependientes; es una limitación preexistente, no introducida por esta regla.
La presentación de estos filtros (la barra horizontal con búsqueda + botón «Filtros» + «Descargar reporte») está estandarizada en el componente
DashFilterBar— ver §19. Esa barra es solo de presentación: la lógica de facetas dependientes descrita arriba sigue viviendo en cada panel.
19. Barra de filtros estándar del dashboard (DashFilterBar) — REGLA DEL SISTEMA (Frank 2026-07-10 → 2026-07-12)
Fuente única: src/lib/components/dashboard/DashFilterBar.svelte. Todo panel del dashboard con filtros usa esta barra; PROHIBIDO volver a maquetar un toolbar/panel de filtros propio por página.
Anatomía (dos zonas)
- Fila fija (siempre visible): una sola línea horizontal con, en este orden:
- Búsqueda por texto (input con lupa a la izquierda y «✕» circular azul para limpiar cuando hay texto). Ocupa el espacio libre. Solo aparece si el panel provee una dim
search. - Botón «Filtros» con el ícono de embudo (filter). Muestra un badge azul con el número de filtros activos. Al pulsarlo despliega/colapsa el panel de filtros. - Botón «Descargar reporte» con el ícono de descarga (download). Aparece SOLO si el panel pasa la proponDownload. Nombre único obligatorio: «Descargar reporte» — quedan PROHIBIDOS los nombres antiguos «Descargar», «Descargar CSV», «Exportar CSV». Es el único botón de descarga del panel: no debe existir un botón de descarga separado fuera de la barra. - Panel colapsable (oculto por defecto): contenedor bordeado con las dimensiones de filtro. Arranca colapsado en todos los tamaños; se abre al pulsar «Filtros». Incluye un botón «Limpiar» (ícono
eraser) que aparece solo cuando hay filtros activos.
Contrato (props y eventos)
dims: DashFilterDim[]— arreglo de dimensiones. Tipos soportados:search(texto libre, debounce 250ms por defecto),select(dropdown de valor único),multi(árbol multi-selección víaFilterTree, requiere ancestro.eventos-surfacepara su estilo de chips),daterange(rango desde/hasta en formatodd-mmm-aaaa, emite ISOYYYY-MM-DD) ytoggle(chips segmentados de valor único). Cada dim puede llevarnewRow: truepara forzar salto de fila.onDownload?: () => void— acción de descarga. Si esnull/ausente, no se pinta el botón. Para descargas gateadas por rol, pasar condicional (ej.onDownload={isSuperAdmin ? downloadCsv : null}).downloadDisabled?: boolean— deshabilita el botón (ej. sin resultados que exportar:downloadDisabled={filtered.length === 0}).downloadLabel?: string— por defecto «Descargar reporte»; sobreescribir solo con aprobación explícita.- Eventos:
select{key,value},multitoggle{key,value},multisetgroup{key,values,on},daterange{key,which,value},search{key,value},clear. El panel es dueño del estado: cada handler asigna la variablefil*correspondiente.
Presentación vs. lógica (regla)
La barra es puramente de presentación: NO calcula el acotamiento de opciones. Cada panel mantiene su propia lógica de facetas dependientes (§18) — matchExcept(registro, except, filtros) + optBase (base de opciones = búsqueda aplicada, sin las dims) — y le pasa a la barra las opciones ya acotadas por dimensión. Los filtros son acumulativos/dependientes: cada dropdown lista solo valores que, combinados con los demás filtros activos, producen al menos un resultado; el valor ya seleccionado se conserva siempre. La reactividad usa params explícitos: matchExcept(s, except, _f) recibe base y estado de filtros como argumentos para que Svelte recalcule las opciones al cambiar cualquier fil*.
Íconos estándar (autosuficientes)
Los íconos propios de la barra se toman de EVENT_ICON (src/lib/data/eventoIcons) e se insertan con solo viewBox (sin stroke/fill): la barra define el trazo de SUS íconos vía CSS acotado, así se ve igual con o sin ancestro .eventos-surface. Ícono de filtros = embudo (filter); ícono de descarga = flecha a bandeja (download); búsqueda = search; calendario = calendar; limpiar = eraser.
Estilo
Compacto: controles a 34–38px de alto, selects/multis con ancho uniforme (flex-basis fijo ~184px), radios de 10–12px, paleta de marca con fallbacks (--lpdi-navy, --lpdi-brandeis, --slate-*). Responsive: bajo 768px la fila fija se apila y los controles ocupan el ancho completo.
Implementaciones de referencia
- Con
onDownload(exportan CSV vía la barra):beneficios/admin,eventos/admin/aprobacion(descarga gateada a superadmin),eventos/mios,admin/auditoria. - Sin
onDownload(no exportan):consulta/startups,consulta/icg,eventos/admin/super-eventos,eventos/favoritos. - El look de la barra se diseñó a partir del toolbar original de «Mis eventos» (
eventos/mios), que luego se migró a este mismo componente.
20. Tabla ancha del dashboard — REGLA CANÓNICA DEL SISTEMA (Frank 2026-07-12)
Por qué existe esta regla: las tablas con muchas columnas (eventos, supereventos, auditoría, consultas)
se venían ajustando caso por caso (un max-width distinto por página, una columna de acciones alineada
distinto, la pista de scroll a veces sí y a veces no). Frank preguntó por qué no hay una sola regla. Esta
es esa regla: toda tabla ancha del dashboard cumple los MISMOS valores fijos. PROHIBIDO ajustar una tabla
ancha con valores propios «solo para esta página»; si un caso no encaja, se cambia la regla aquí y se propaga.
Una tabla es «ancha» cuando sus columnas no caben sin scroll horizontal en un viewport de escritorio típico (≈1440px). Aplica a paneles de admin y del usuario por igual.
20.1 Valores fijos obligatorios
- Contenedor de página:
max-width: min(1680px, 96vw); margin: 0 auto;(mismo valor que §15.5 para admin; el contenedor del usuario.me-wrapusa idénticomin(1680px, 96vw)porque su tabla espeja la de admin). NUNCA unmax-widthfijo distinto (ej.1400px) «para que se vea menos limitada»: el estándar ya es el ancho. - Wrap de la tabla (
overflow-x: auto): borde1px solid var(--border)y radio inferior0 0 12px 12px(el radio superior lo aporta la pista/barra de scroll superior; ver punto 5). Fondovar(--bg-surface). - Encabezado gris (
thead th):background: var(--bg-elevated);+text-align:left,text-transform:uppercase,font-size:~11.5px,color:var(--text-muted),border-bottom:1px solid var(--border),white-space:nowrap. El mismo gris se aplica a la celda de encabezado de Acciones (thead .*-th-actions), con unz-indexmayor por ser sticky en dos ejes. - Columna «Acciones» FIJA (sticky a la derecha):
position:sticky; right:0;con sombra de separaciónbox-shadow: -6px 0 8px -6px rgba(0,0,0,.12);y fondo propio (surface en el body, elevated en el hover y el thead) para que el contenido se «esconda» debajo al desplazar. Rótulo y botones alineados a la derecha y COINCIDENTES: elthllevatext-align:right; el contenedor de botones de la celda debe quedar también a la derecha. Si los botones van en un contenedor flex, ese contenedor llevajustify-content: flex-end(undisplay:flexsin esto los deja a la izquierda y el rótulo queda «corrido»). Si los botones son hijos inline directos de la celda,text-align:rightya alinea los botones sin contenedor extra. Alineación + ancho (Frank 2026-07-12): el rótulo «Acciones» y los botones van alineados a la IZQUIERDA (text-align: left), pegados a la última columna de datos, y la columna usawidth: 1%(shrink-to-content) para ajustarse al ancho de sus botones. Sinwidth:1%, cuando las columnas de datos son angostas la última columna se estira a llenar el contenedor y deja un hueco vacío grande a la derecha de los botones. La alineación a la derecha en una columna ancha también dejaba una banda vacía entre los datos y los botones. Aplica a TODA tabla con acciones (mios, super-eventos, consulta startups/icg, beneficios/admin, aprobacion). Gotcha de especificidad (obligatorio): la alineación delthde Acciones se fija con un selector ESPECÍFICO.tabla thead th.*-th-actions { text-align: left }(especificidad 0,2,2). Un simple.*-th-actions { text-align: left }(0,1,0) queda igual que la genérica; usa el selector específico para que el rótulo y los botones queden consistentes. Además la columna se dimensiona al contenido (no un ancho fijo mayor) para no reintroducir el hueco. Aplica a TODA tabla con columna de acciones, no solo a las anchas sticky: las tablas.adm-table(consulta/startups, consulta/icg, beneficios/admin) usan.adm-table thead th.adm-th-actions { text-align: right }por la misma razón. Al crear una tabla nueva con acciones, replica el selector específico. - Pista de scroll horizontal OBLIGATORIA arriba de la tabla (texto
.scroll-hint, ícono de flechas ‹›): «Desliza horizontalmente para ver todos los campos… Las Acciones quedan fijas mientras te desplazas. Puedes ajustar el ancho de cada columna arrastrando su borde derecho.» Debajo de la pista va la barra de scroll superior sincronizada (estándar admin): undivconoverflow-x:autoy un spacer del ancho de la tabla (tableEl.scrollWidth), sincronizado con el wrap inferior víasyncFromTop/syncFromBottom(guardasbind:thisatopScrollEl/bottomScrollEl). Radio superior12px 12px 0 0, borde sinborder-bottom. - Columnas redimensionables vía
use:colResize={{ storageKey, deps, skipClass: '*-th-actions' }}de$lib/actions/colResize; el ancho de las columnas de datos se persiste enlocalStorage.afterUpdatellamaapplyCellTitles(tableEl)para ponertitlea las celdas truncadas.
REGLA DE DIMENSIONADO DE LA COLUMNA «ACCIONES» (Frank 77, 2026-07-23) — obligatoria para TODA tabla con
columna de acciones, no solo las anchas: la columna sticky de Acciones se pasa como skipClass y se dimensiona
SIEMPRE por su contenido real (ancho de los botones), NUNCA desde un ancho persistido ni un valor fijo. El
juego de botones cambia por estado y permiso de cada fila, así que un ancho guardado obsoleto cortaba los botones
sin dejar handle para ensanchar (por eso le pasaba a unos usuarios y a otros no). El helper colResize ya lo
implementa así; NO reintroducir un ancho fijo/persistido para esa columna. Requisitos verificados que debe
cumplir cualquier reimplementación:
- Medir bajo table-layout:auto (limpiando el ancho de la sticky) para que el scrollWidth no salga
constreñido cuando la tabla ya venía en fixed.
- Respaldar la medición con la suma real del offsetWidth de los botones + paddings (por si la celda ya venía
clipeada).
- Re-medir tras cambios tardíos de layout, no solo al montar: en el siguiente frame (requestAnimationFrame),
cuando las fuentes cargan (document.fonts.ready — con la fuente fallback los botones miden menos y la columna
quedaba corta hasta un F5), y ante alta/baja de filas o botones (MutationObserver sobre el tbody, solo
childList). Todas las re-mediciones son idempotentes.
Consumidores actuales del mismo colResize: eventos/mios, beneficios/admin, eventos/admin/aprobacion,
eventos/admin/super-eventos. Un cambio en el helper cubre a las cuatro.
20.2 Implementaciones de referencia (copiar de aquí)
dashboard/eventos/mios(clasesme-*) — tabla del usuario; botones de acción como hijos inline (Ver · Editar · Duplicar · Eliminar), alineados portext-align:right.dashboard/eventos/admin/super-eventos(clasesse-*) — tabla de admin; botones de acción en contenedor flex.se-actionsconjustify-content: flex-end.- Ambas comparten
max-width: min(1680px, 96vw), encabezado grisvar(--bg-elevated), columna Acciones sticky con sombra, pista.scroll-hint+ barra de scroll superior sincronizada, ycolResize. Toda tabla ancha nueva se maqueta copiando una de estas dos (no se reinventa el patrón por página).
20.3 Nota sobre reutilización
El «estándar» es documental + valores idénticos entre implementaciones, no un componente compartido: mios y
super-eventos manejan su propio bottomScrollEl/syncFromBottom y su colResize con storageKey propio.
PROHIBIDO extraer un componente único si eso rompe el scroll sincronizado o el redimensionado por columna; lo que
se comparte son los valores de esta sección, verificados idénticos entre páginas.
20.4 Capa visual estándar (Frank 2026-07-28) — colores, columna principal, alto de fila, barras y acciones
Complemento de §20: los criterios visuales que TODA tabla (ancha o no) debe cumplir para verse idéntica. Frank preguntó por qué unas tablas tenían colores/estilos distintos; esta subsección los unifica. PROHIBIDO desviarse por página.
- Colores de texto y títulos. Encabezados de columna (
thead th) en azul marca#003e88(--text-primary) sobre fondo gris clarovar(--bg-elevated)(#f1f5f9). Texto de celdas de datos en grisvar(--text-secondary)(#64748b). PROHIBIDO negros puros (#000) o colores sueltos por columna. (Refina el color delthead thde §20.1.3, que antes usaba--text-muted: el encabezado va ahora en#003e88, manteniendo el resto —uppercase,~11.5px,nowrap,bg-elevated.) - Columna principal (primera). Es el identificador de la fila (Nombre). Su texto va en azul marca
#003e88,font-weight:700, y la columna queda fija (sticky a la izquierda) al hacer scroll horizontal, con la misma sombra de separación que la de Acciones pero del lado izquierdo (box-shadow: 6px 0 8px -6px rgba(0,0,0,.12)) y fondo propio (surface en body, elevated en thead/hover). Resultado: la primera columna (Nombre, sticky-left) y la última (Acciones, sticky-right) quedan ancladas; las columnas de datos se desplazan entre ambas. - Alto de fila fijo. Las filas NO cambian de alto según el contenido: altura pareja en toda la tabla. El texto
largo se corta con elipsis (
overflow:hidden; text-overflow:ellipsis; white-space:nowrap) y muestra el valor completo entitle(tooltip) víaapplyCellTitles. Se logra connowrap+overflow:hidden+ellipsisen las celdas y los anchos que persistecolResize(patrón deeventos/mios), SINtable-layout:fixed. Las celdas con chips (temáticas, etc.) llevan su contenedor enflex-wrap:nowrappara que no crezcan de alto. Ninguna fila «salta» de alto respecto a las demás. - Dos barras de scroll horizontal, en azul (Frank corrigió: NO una sola). Se mantienen las dos barras
sincronizadas de §20.1.5 (superior sobre la tabla + inferior en el wrap). Ambas se estilizan con el azul del DS:
::-webkit-scrollbar-thumb { background: var(--accent); border-radius:6px }y::-webkit-scrollbar { height:10px }sobre.top-scrolly el wrap, conscrollbar-color: var(--accent) transparentpara Firefox. No eliminar la barra superior: Frank la quiere presente arriba y abajo. - Columna «Acciones» en una sola línea, iconos soft CON borde. Los botones/íconos de acción van en una sola
línea (
white-space:nowrap, sinflex-wrap). Estilo soft: ícono a color sobre fondo tintado suave (~12%) del mismo color, CON borde de 1px del mismo color (Frank corrigió: con borde, como el catálogo de íconos, no sin borde). Colores por tipo de acción según el catálogo (playgrounds.digitalhubassist.ai/lpdi-iconos-ds.html): Editar=azul, Aprobar/positivo=verde, Precaución=ámbar, Cierre/Eliminar=rojo, Utilitario/Evaluar=gris, Destacar=dorado. - Pista de ayuda solo cuando desborda (Frank corrigió §20.1.5). El texto
.scroll-hint(«Desliza horizontalmente…») se muestra únicamente cuando la tabla realmente se desborda (tableEl.scrollWidth > wrapEl.clientWidth), no siempre. Mismo texto en todas las tablas. Se re-evalúa enafterUpdatey ante resize. - GOTCHA obligatorio de la columna fija (Frank item 1, 2026-07-29) — REGLA DEL SISTEMA. La regla que da
position: relativea losthead th(necesaria para el agarre.col-resize) tiene MÁS especificidad que la que fija la columna principal (position: sticky), así que le gana y el encabezado de la primera columna deja de ser fijo y su título se pierde al desplazar (el cuerpo sí queda fijo porque no hay reglatdcompitiendo). SIEMPRE hay que excluir la columna sticky de la regla relative:thead th:not(.<sticky-actions>):not(.<sticky-nombre>) { position: relative }. Aplica a TODA tabla con columna fija + redimensionado. Verificar SIEMPRE con un render desplazado a la derecha, no solo al inicio. - Columna ordenada marcada (Frank #21, 2026-07-29/30). La columna elegida para ordenar se marca
claramente: el activo muestra un triángulo SÓLIDO ▲/▼ (relleno,
fill:currentColor) envar(--accent)+ fondo tintergba(0,110,255,.08)en elth; los inactivos muestran un chevron tenue (afordancia de «ordenable»). Un chevron de línea fina en el activo NO alcanza (se confunde con los inactivos): usar el triángulo relleno, como en Directorio y Aprobación. Clase.sorted-activeen elthsegún el estado de orden.
Orden de alineación (Frank 2026-07-28): Directorio de organizadores → Beneficios/admin → Aprobación de eventos → consultas de startups/ICG. Cada tabla se verifica con captura (render con scroll horizontal) antes de marcar hecha.
21. Registro de entidades y pantallas de ciclo de vida de la cuenta (Frank 2026-07-13) — REGLA DEL SISTEMA
21.1 Camino de registro (actualizado Frank #37a, 2026-07-29)
Registrar una entidad nueva abre el formulario simplificado (FSS dashboard/startups/nuevo,
FIS dashboard/icg/nuevo-simplificado). Aplica a los enlaces del sidebar, a los modales de «crear nueva»
(onCreateNew) y a la card «Registrar mi iniciativa» del ecosistema. El formulario detallado (FSD
dashboard/startups/nuevo-detallado, FID dashboard/icg/nuevo) se alcanza al completar/actualizar el
perfil desde PU, al editar una entidad existente (onEditExisting) o al elegir la entidad activa (no
borrador) en el sidebar. Todas las rutas siguen vivas (formularios a medio llenar y enlaces guardados no
deben romperse con un 404).
21.2 Los paneles NO se bloquean
El usuario navega libremente por todos los paneles desde el primer momento y todo se autoguarda como borrador. Prohibido volver a bloquear paneles a la espera de que la entidad se registre.
21.3 El botón «Registrar» vive fuera del panel 1
Se llama «Registrar» (no «Registrar y continuar»), aparece en la barra de acciones de todos los paneles y valida los mínimos desde donde sea que esté el usuario. Si falta algo, lo dice y lleva al panel del campo que falta. Cuando la entidad ya está registrada, el botón se reemplaza por su estado. El cliente solo se da por registrado cuando el servidor lo confirma; nunca por su cuenta.
21.4 El asterisco marca solo los mínimos reales
| Formulario | Campos con * |
|---|---|
| Startup detallado | nombre comercial, país de origen |
| ICG detallado | rol en el ecosistema, nombre del ICG (o nombres/apellidos del inversionista individual), industria en la que opera, país de operación, correo corporativo |
El país de origen del ICG NO es obligatorio. Los datos de contacto del ICG tampoco.
Los mínimos los fija la base (columnas NOT NULL sin default), no el gusto: startups exige
company_name, country, contact_email; icgs exige organization_name, contact_email. El
correo se toma de la sesión, no se pregunta.
21.5 Datos personales de terceros: se leen, no se diligencian
En el panel de contacto del ICG, nombres, apellidos, LinkedIn y teléfono están siempre en solo lectura. Si el correo pertenece a un usuario registrado, se traen de su perfil; si no, quedan vacíos y se llenarán cuando esa persona acepte y complete su perfil. El cargo sí es editable: depende de la relación de esa persona con esa entidad, no de su perfil. Razón (Frank): por términos y condiciones no se capturan los datos personales de otra persona desde el formulario de una entidad.
21.6 Pantallas de cuenta eliminada
/cuenta-eliminada usa los tokens del sistema (§2). Prohibido el fondo degradado verde oscuro y
el botón verde que tenía: el acento es azul y no existe ningún tema verde (§1.2).
- Tras eliminar la cuenta, el usuario no cae en /login en seco: ve una despedida
(?justDeleted=1, funciona sin sesión porque el borrado cierra la sesión en el servidor) con
una cuenta regresiva que lo lleva a eco.lpdi.co.
- Quien vuelve dentro de los 30 días ve su botón de recuperar cuenta, sin cuenta regresiva que
lo expulse mientras intenta recuperarse.
- «Cerrar sesión y salir» cierra la sesión de verdad (acción ?/logout) y sale a eco.lpdi.co.
Prohibido dejarlo como un enlace a /login: la sesión quedaba viva y el usuario en el limbo.
22. Beneficios públicos, alianzas y accesos (Frank 2026-07-22) — REGLA DEL SISTEMA
22.1 Catálogo de beneficios PÚBLICO
/perks (listado y ficha de aliado) es de acceso público, sin sesión. El filtro de estado es
explícito en el query (status='publicado' + deleted_at IS NULL), no se delega a la RLS. El
canje sí exige iniciar sesión. La card «Beneficios del ecosistema» de la home (/ecosistema,
que sirve /) es visible para todos, ubicada tras «Registrar mi iniciativa»; sin sesión muestra
solo el botón «Ver catálogo» (nunca «Publica tus beneficios»).
22.2 «¿Para quién es el beneficio?» — beneficiario multi-select (SSOT src/lib/data/perk-audiences.ts)
Cuatro opciones fijas: Startups · PyMEs · Corporativos · Usuario final (selección MÚLTIPLE,
obligatoria). En los formularios (FP y FP externo) se muestran como lista de casillas desplegada,
SIN íconos. Los íconos (AudienceIcon.svelte) van solo en /perks (filtro y tarjetas) y
en la ficha del aliado. Columna DB perks.audiences text[]; la columna legacy perks.audience
se conserva derivada (deriveLegacyAudience) para lectores viejos.
22.3 «Países donde aplica el beneficio» (SSOT src/lib/data/perk-countries.ts)
Pregunta obligatoria, justo después de beneficiario, con el RegionCountryPicker (multi) más una
opción explícita «Todos los países». Semántica: perks.benefit_countries text[] vacío = aplica
a todos. Donde se muestra (ficha, tarjeta de /perks, admin) la ausencia de países se rotula
exactamente «Todos los países».
22.4 Filtros y columnas de beneficio
/perks: bajo los filtros de categoría, filtros de Beneficiario (pills con ícono) y País (el de país solo aparece si algún perk tiene país). Matchers puros ensrc/lib/data/perk-filters.ts(un perk sin países matchea cualquier país elegido)./dashboard/beneficios/admin: columnas Beneficiario y País beneficiario tras «Categoría», y esos mismos dos como filtros dependientes tras el filtro «Categoría» (incluidos en el CSV).
22.5 Permiso «Publicar beneficios» y flujo «Quiero ser aliado»
Publicar beneficios ya no se gatea por lista de correos (canAccessPerks, DEPRECADO): se gatea por
el permiso «Publicar beneficios» (rol publicador del módulo beneficios en module_permissions,
RPC can_publish_perks) más tener una entidad activa (formalizada). El superadmin lo otorga en
/dashboard/admin/permisos.
- En el sidebar, la sección Beneficios es visible para todos: «Ver catálogo» y «Publicar beneficio»
siempre; «Mis beneficios» y «Mis referidos» solo con el permiso.
- Al pulsar «Publicar beneficio» sin ser elegible, en vez de rebotar se muestra: «Para publicar
beneficios en el Ecosistema LPDI necesitas primero formalizar una alianza con nosotros. Envíanos una
solicitud de alianza usando el botón a continuación:» con el CTA «Quiero ser aliado».
- «Quiero ser aliado» abre un pop-up (SolicitudAlianzaModal.svelte, patrón de IdeasFeedbackModal):
nombre/correo prellenados, selector de entidades activas (o «Aún no tengo una entidad registrada en el
sistema»), texto obligatorio, y dos botones excluyentes «Enviar mensaje» / «Enviar mensaje y agendar
una reunión». POST /api/alianzas envía el correo a PARTNERS_EMAIL con copia al usuario y audita
(alianza_solicitada); el segundo botón además abre la URL de agenda tras el envío.
22.6 Rutas de login y redirect post-registro
- El CTA de canje y todos los guards sin sesión van a
/login(nunca/auth/login, que devuelve 404)./loginconservaredirectToy ofrece crear cuenta. - Al terminar el registro de usuario, la cuenta queda usable de inmediato (Opción B) y el usuario
queda logueado y cae en
/ecosistema(no en/login). El correo de verificación se sigue enviando con su ventana de gracia; si el auto-login fallara, cae a/login(respaldo).
22.7 Columna «Acciones» de las tablas anchas (refina §20)
La columna sticky de acciones (no redimensionable, skipClass) se dimensiona siempre por el contenido
real de sus botones (colResize.ts mide el scrollWidth de la celda más ancha) y no persiste su
ancho. Prohibido darle un ancho fijo desde localStorage: el juego de botones cambia por estado/permiso
y un ancho obsoleto cortaba los botones sin dejar handle para ensancharla.
23. Códigos únicos: nada en borrador recibe código — REGLA TRANSVERSAL DEL SISTEMA (Frank 2026-07-23)
Regla: los identificadores/códigos únicos del sistema (código de entidad, evento_codigo y codigo_unico de eventos, y los códigos de convocatorias, noticias y cualquier módulo futuro) se asignan únicamente al formalizar / enviar el registro, nunca al crear o autoguardar un borrador. Un borrador abandonado no debe quemar números ni dejar identificadores huérfanos.
Motivo: los borradores por autosave se crean masivamente y muchos se abandonan; si el código se asigna al insertar la fila del borrador, la numeración se llena de huecos y de identificadores sin dueño real. El código es un identificador estable de un registro ya formalizado, no de un borrador en construcción.
Estado por identificador (verificado 2026-07-23):
- Entidades (entity_code, formato S-TTT-PPP-AA-NNNNN / I-TTT-PPP-AA-NNNNN): nace conforme — se asigna solo en formalizeEntity().
- Eventos evento_codigo (formato M-PPP-TTT-AA-NNNNN): conforme — se genera solo en el submit final; el autosave lo deja NULL.
- Eventos codigo_unico (era SERIAL): conforme desde la migración 134 (tarea 67) — se le quitó el DEFAULT nextval y el NOT NULL (la secuencia eventos_codigo_unico_seq se conserva), se anuló el código de los borradores (status='DRAFT') y se asigna al enviar vía la RPC atómica e idempotente assign_evento_codigo_unico(uuid) (solo si el código está NULL). Los eventos ya publicados conservan su número y sus enlaces /eventos/{codigo} (cero renumeración). En "Mis eventos" un borrador muestra "—" en la columna Código. super_eventos queda fuera (admin-only, sin flujo de borrador).
Al agregar un identificador nuevo a cualquier módulo, respeta esta regla desde el diseño: la asignación va en el punto de formalización/envío, no en el insert del borrador.