cis-style · hub · kit v9 · f417275
Patrones de página
Un componente resuelve una pieza; un patrón resuelve una situación que se repite en los sitios del grupo. Cada patrón trae cuándo usarlo, el ejemplo vivo armado con las clases reales del kit y la regla que lo respalda.
Las reglas se citan por identificador (H01 a H27, cis-modelo/modelo/reglas.py). La cita convierte la revisión en medición: una inconsistencia deja de ser opinión y pasa a ser una violación con evidencia.
1 · Gate SSO
Cuándo. En todo sitio privado del grupo. El sitio nunca implementa login propio: Caddy hace forward_auth contra cis-auth (Authentik) y la aplicación recibe la identidad ya resuelta por OIDC con core-auth-lib. Un formulario de usuario y clave fuera de auth.innovacionsantiago.cl viola H19.
La portada de acceso, según la spec de auth.innovacionsantiago.cl:
<div class=login-card style="max-width:360px">
<svg width="34" height="34" viewBox="0 0 200 200" fill="none" stroke="currentColor" style="display:block;margin:0 auto 14px"><circle cx="100" cy="100" r="92" stroke-width="4"/><circle cx="100" cy="100" r="56" stroke-width="18"/></svg>
<div class=login-card__title style="text-align:center">Entrar a cis-admin</div>
<button class=login-card__btn>Continuar con cis-auth</button>
<p class=login-card__meta>Una sola cuenta para todo el grupo<br>auth.innovacionsantiago.cl</p>
</div>El chip de sesión del chrome, con sus dos estados (el estado se decide server-side o en build, cero JS):
<div style="display:flex;gap:22px;align-items:center;flex-wrap:wrap">
<div class=gr-login data-state=anon>
<a class=gr-login__enter href="/oauth/login">Entrar</a>
</div>
<div class=gr-login data-state=auth>
<a class=gr-login__user href="/cuenta"><span class=gr-login__dot></span><span class=gr-login__name>M. Illanes</span></a>
<a class=gr-login__exit href="/oauth/logout">Salir</a>
</div>
</div>El gate vive en el proxy, fuera de la aplicación:
panel.innovacionsantiago.cl {
forward_auth 127.0.0.1:9000 {
uri /outpost.goauthentik.io/auth/caddy
copy_headers X-Authentik-Username X-Authentik-Email X-Authentik-Groups
}
reverse_proxy 127.0.0.1:8080
}
<pre style="margin:0;font:12px/1.7 var(--font-mono);color:var(--ink);border:1px solid var(--line);background:var(--paper-raised);padding:14px 16px;overflow-x:auto">panel.innovacionsantiago.cl {
forward_auth 127.0.0.1:9000 {
uri /outpost.goauthentik.io/auth/caddy
copy_headers X-Authentik-Username X-Authentik-Email X-Authentik-Groups
}
reverse_proxy 127.0.0.1:8080
}</pre>H19 · «La autenticación se integra a cis-auth por OIDC con core-auth-lib. No se reinventa login ni JWT por servicio.» Severidad alta. Y una trampa medida: el 302 al login prueba que el sitio está cerrado al anónimo, no que esté bien acotado. La aplicación en Authentik necesita binding a un grupo; publicada sin bindings queda abierta a cualquier cuenta. Fuente: cis-modelo/modelo/reglas.py · CANON.md §Auth.
2 · Panel de verificación de documentos
Cuándo. En todo documento que sale del grupo en papel o PDF: actas, certificados, cartas. El documento imprime un código C-XXXXXX y el panel responde si el archivo que tienes al frente es auténtico. Lo sirve cis-verify en docs.circulodesantiago.cl/v/{numero}.
Documento válido: código, hash completo y la URL del validador escrita:
- SHA-256
- 9f2a41c8d0b3e6a7551f0c98d2ab34c6e1f7a8b90d3c5e2f4a6b8c0d1e2f3a4b
- Emitido
- 2026-08-14 · Compañía de Innovación de Santiago SpA
- Verifica en
- docs.circulodesantiago.cl/v/C-000482
<div style="max-width:540px;border:1px solid var(--line);background:var(--paper-raised);padding:22px 24px">
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:14px">
<span class=eyebrow>Documento verificado</span>
<span class="badge badge--ok">auténtico</span>
</div>
<div style="font:700 30px var(--font-mono);letter-spacing:.02em;margin-bottom:16px">C-000482</div>
<dl style="margin:0;display:grid;grid-template-columns:92px 1fr;gap:8px 16px;font-size:12.5px">
<dt style="font:10.5px var(--font-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ink-muted)">SHA-256</dt>
<dd style="margin:0;font:11.5px var(--font-mono);word-break:break-all;color:var(--ink)">9f2a41c8d0b3e6a7551f0c98d2ab34c6e1f7a8b90d3c5e2f4a6b8c0d1e2f3a4b</dd>
<dt style="font:10.5px var(--font-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ink-muted)">Emitido</dt>
<dd style="margin:0">2026-08-14 · Compañía de Innovación de Santiago SpA</dd>
<dt style="font:10.5px var(--font-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ink-muted)">Verifica en</dt>
<dd style="margin:0;font:11.5px var(--font-mono)">docs.circulodesantiago.cl/v/C-000482</dd>
</dl>
</div>Código que no existe: el rechazo dice qué pasó y qué hacer:
El código no corresponde a ningún documento emitido. Revisa que lo copiaste completo, con la letra y los seis dígitos.
<div style="max-width:540px;border:1px solid var(--line);background:var(--paper-raised);padding:22px 24px">
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:12px">
<span class="eyebrow eyebrow--muted">Verificación</span>
<span class="badge badge--err">no corresponde</span>
</div>
<div style="font:700 24px var(--font-mono);letter-spacing:.02em;margin-bottom:10px;color:var(--ink-muted)">C-000999</div>
<p style="margin:0;font-size:13px;color:var(--ink-muted)">El código no corresponde a ningún documento emitido. Revisa que lo copiaste completo, con la letra y los seis dígitos.</p>
</div>H14 · H26 · El código y el hash van en JetBrains Mono, la voz secundaria de la casa (H14). El hash SHA-256 se muestra completo: truncado no verifica nada. La URL del validador va escrita, porque el papel no tiene enlaces. El copy del rechazo cumple la norma de prosa (H26). Fuente: cis-modelo/modelo/reglas.py · cis-verify.
3 · Sello de frescura
Cuándo. Bajo toda cifra medida que muestras: saldo, uptime, conteo. Una cifra sin fecha ni fuente es una opinión con dígitos. El sello declara cuándo se midió, con qué, y si el dato es fresco o cache.
medido 2026-08-20 14:32 · caddy adapt + dig · refresco activo
al 2026-08-19 09:10 · cache diario · stale sin refrescar
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:16px;max-width:580px">
<div>
<div class=stat>
<span class=stat__value>119</span>
<span class=stat__label>hosts servidos</span>
</div>
<p style="margin:6px 2px 0;font:10.5px var(--font-mono);letter-spacing:.04em;color:var(--ink-soft)">medido 2026-08-20 14:32 · caddy adapt + dig · refresco activo</p>
</div>
<div>
<div class=stat>
<span class=stat__value>412</span>
<span class=stat__label>documentos emitidos</span>
</div>
<p style="margin:6px 2px 0;font:10.5px var(--font-mono);letter-spacing:.04em;color:var(--ink-soft)">al 2026-08-19 09:10 · cache diario · <span class="badge badge--warn">stale</span> sin refrescar</p>
</div>
</div>H13 (mismo espíritu) · «La versión que un sitio carga de verdad coincide con la que el registro declara»: lo declarado se contrasta con lo medido. El sello exige tres datos: timestamp, fuente (URL, query o comando) y si hubo refresco activo o cache. Con más de 5 minutos de edad, el dato se refresca o se marca stale explícito. Fuente: CLAUDE.md del workspace, «Stale-data y freshness checks» · cis-modelo/modelo/reglas.py.
4 · Noindex en sitios técnicos
Cuándo. En todo host de clase servicio o infraestructura: paneles, hubs de documentación, APIs con vista HTML. Doble capa: la meta en el documento y la cabecera X-Robots-Tag en el proxy, porque la meta no cubre JSON ni archivos.
<meta name=robots content="noindex, nofollow">
status.innovacionsantiago.cl {
header X-Robots-Tag "noindex, nofollow"
reverse_proxy 127.0.0.1:8080
}
<div style="display:grid;gap:12px;max-width:640px">
<pre style="margin:0;font:12px/1.7 var(--font-mono);color:var(--ink);border:1px solid var(--line);background:var(--paper-raised);padding:14px 16px;overflow-x:auto"><meta name=robots content="noindex, nofollow"></pre>
<pre style="margin:0;font:12px/1.7 var(--font-mono);color:var(--ink);border:1px solid var(--line);background:var(--paper-raised);padding:14px 16px;overflow-x:auto">status.innovacionsantiago.cl {
header X-Robots-Tag "noindex, nofollow"
reverse_proxy 127.0.0.1:8080
}</pre>
</div>H11 · «Todo sitio técnico responde noindex y no se sirve al anónimo si publica algo interno.» Se comprueba midiendo X-Robots-Tag en cada host de clase servicio o infraestructura. Complementa H05: la infraestructura no lleva marca ni namebrand. Fuente: cis-modelo/modelo/reglas.py · CATALOGO.md §2 · incidente del 2026-05-29, donde un crawler encontró un endpoint técnico indexado y drenó la cuota que había detrás.
5 · Estados vacíos
Cuándo. Cuando la vista existe y los datos no: primera visita, filtro sin coincidencias, cola ya procesada. El peor estado vacío es la página en blanco que no explica nada.
Filtro sin coincidencias, con la acción que lo deshace:
Sin resultados
Ningún documento coincide con el filtro «actas 2025».
<div style="border:1px solid var(--line);background:var(--paper-raised);padding:40px 24px;text-align:center;max-width:560px">
<p style="margin:0 0 6px;font:10.5px var(--font-mono);letter-spacing:.12em;text-transform:uppercase;color:var(--ink-soft)">Sin resultados</p>
<p style="margin:0 0 16px;font-size:13.5px;color:var(--ink-muted)">Ningún documento coincide con el filtro «actas 2025».</p>
<button class="btn btn--ghost btn--sm">Limpiar filtro</button>
</div>Primer uso: declara cuándo van a aparecer los datos:
Aún sin datos
La primera medición corre esta noche a las 03:00. Vuelve mañana.
<div style="border:1px solid var(--line);background:var(--paper-raised);padding:40px 24px;text-align:center;max-width:560px">
<p style="margin:0 0 6px;font:10.5px var(--font-mono);letter-spacing:.12em;text-transform:uppercase;color:var(--ink-soft)">Aún sin datos</p>
<p style="margin:0;font-size:13.5px;color:var(--ink-muted)">La primera medición corre esta noche a las 03:00. Vuelve mañana.</p>
</div>H16 · H26 · La caja vacía lleva borde uniforme de 1 px, sin barrita de acento (H16). El copy dice qué falta y qué puedes hacer, en una frase, sin ilustraciones ni disculpas (H26). Si sabes cuándo van a aparecer los datos, dilo: es el sello de frescura aplicado al vacío. Fuente: cis-modelo/modelo/reglas.py · core/prosa/NORMA.md.
6 · Tabla de estado con badge
Cuándo. En paneles que listan servicios, trabajos o documentos con su estado actual. Es la forma canónica de responder «¿está todo bien?» de un vistazo.
| Servicio | Estado | Medido | Latencia |
|---|---|---|---|
| cis-auth | en línea | 14:32:05 | 38 ms |
| cis-verify | en línea | 14:32:05 | 51 ms |
| datos-api | degradado | 14:32:06 | 412 ms |
| panel-led | en falla | 14:31:40 | · |
<table class=table>
<thead><tr><th>Servicio</th><th>Estado</th><th>Medido</th><th style="text-align:right">Latencia</th></tr></thead>
<tbody>
<tr><td>cis-auth</td><td><span class="badge badge--ok">en línea</span></td><td style="font-family:var(--font-mono);font-size:12px">14:32:05</td><td class=num>38 ms</td></tr>
<tr><td>cis-verify</td><td><span class="badge badge--ok">en línea</span></td><td style="font-family:var(--font-mono);font-size:12px">14:32:05</td><td class=num>51 ms</td></tr>
<tr><td>datos-api</td><td><span class="badge badge--warn">degradado</span></td><td style="font-family:var(--font-mono);font-size:12px">14:32:06</td><td class=num>412 ms</td></tr>
<tr><td>panel-led</td><td><span class="badge badge--err">en falla</span></td><td style="font-family:var(--font-mono);font-size:12px">14:31:40</td><td class=num>·</td></tr>
</tbody>
</table>base/components.css · El estado va como texto dentro del badge, nunca color solo: lo exige el daltonismo y lo agradece el grep. Las cifras van en td.num, mono con cifras tabulares. La columna «Medido» es el patrón 3 aplicado a tablas: una fila sin timestamp reporta un estado que no puedes fechar. Fuente: base/components.css (.table, .badge) · CLAUDE.md del workspace.
7 · Fila de stats de dashboard
Cuándo. En la cabecera de un dashboard: las tres a cinco cifras que resumen el sistema antes de cualquier detalle.
medido 2026-08-20 14:32 · cis-modelo · una sola corrida para toda la fila · refresco activo
<div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px">
<div class=stat><span class=stat__value>119</span><span class=stat__label>hosts</span></div>
<div class=stat><span class=stat__value>27</span><span class=stat__label>reglas</span></div>
<div class=stat><span class=stat__value>22</span><span class=stat__label>automáticas</span></div>
<div class=stat><span class=stat__value>3</span><span class=stat__label>violaciones abiertas</span></div>
</div>
<p style="margin:8px 2px 0;font:10.5px var(--font-mono);letter-spacing:.04em;color:var(--ink-soft)">medido 2026-08-20 14:32 · cis-modelo · una sola corrida para toda la fila · refresco activo</p>
</div>H14 · La stat es la tarjeta de una sola cifra: valor mono pesado, etiqueta mono en mayúsculas con tracking. Si toda la fila sale de la misma corrida, comparte un sello de frescura; si mezcla fuentes, cada stat lleva el suyo. Fuente: base/components.css (.stat) · cis-modelo/modelo/reglas.py.
8 · Aviso y nota
Cuándo. Para contexto que el lector puede necesitar (nota), condiciones que exigen acción pronto (atención) y fallas activas (error). Siempre dentro del flujo del contenido, sin flotar ni tapar.
<div style="display:grid;gap:12px;max-width:560px">
<div style="border:1px solid var(--line);background:var(--accent-soft);padding:13px 16px;font-size:13px;line-height:1.55">
<b style="display:block;margin-bottom:4px;font:600 10.5px var(--font-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--accent)">Nota</b>
El validador acepta el código con o sin guion.
</div>
<div style="border:1px solid color-mix(in srgb, var(--warn) 30%, transparent);background:color-mix(in srgb, var(--warn) 8%, var(--paper));padding:13px 16px;font-size:13px;line-height:1.55">
<b style="display:block;margin-bottom:4px;font:600 10.5px var(--font-mono);letter-spacing:.1em;text-transform:uppercase;color:color-mix(in srgb, var(--warn) 60%, var(--ink))">Atención</b>
El certificado vence el 2026-09-01. Renueva antes del 25 de agosto.
</div>
<div style="border:1px solid color-mix(in srgb, var(--err) 30%, transparent);background:color-mix(in srgb, var(--err) 8%, var(--paper));padding:13px 16px;font-size:13px;line-height:1.55">
<b style="display:block;margin-bottom:4px;font:600 10.5px var(--font-mono);letter-spacing:.1em;text-transform:uppercase;color:color-mix(in srgb, var(--err) 60%, var(--ink))">Error</b>
La sonda no alcanza el servicio desde las 14:10. Revisa el runbook antes de reiniciar.
</div>
</div>H16 · «Prohibido el borde de acento a la izquierda en tarjetas, avisos y notas. La jerarquía se hace con fondo, tipografía o etiqueta.» Decisión de Martín del 2026-08-06. Si ves una barrita de color a la izquierda en un sitio del grupo, es un sitio sin migrar. Fuente: cis-modelo/modelo/reglas.py · CANON.md §Componentes UI.
Fuentes
base/tokens.css· tokens de color, tipografía y espaciado de todos los ejemplos; clase.eyebrow.base/components.css·.login-card,.stat,.table,.badge,.btn.base/chrome.css·.gr-loginy sus estados de sesión.cis-modelo/modelo/reglas.py· H05, H11, H13, H14, H16, H19, H26, y la filosofía del archivo: una regla sin comprobación es decorativa./srv/projects/CANON.md· secciones Auth, Componentes UI y Prosa clara.core/prosa/NORMA.md· el copy de estados vacíos, avisos y rechazos./srv/projects/CLAUDE.md· «Stale-data y freshness checks», la norma del sello de frescura.- cis-verify · panel de verificación y ruta
docs.circulodesantiago.cl/v/{numero}.