cors-demo/public/index.html
2026-10-01 13:02:44 -03:00

292 lines
15 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laboratorio de CORS</title>
<style>
:root {
--bg: #f6f7f9;
--panel: #ffffff;
--panel-2: #f1f3f6;
--ink: #14181f;
--ink-2: #4a5462;
--ink-3: #78828f;
--line: #dde1e7;
--accent: #2563eb;
--ok: #157f4a;
--ok-bg: #e6f5ec;
--bad: #c0281f;
--bad-bg: #fdecea;
--warn: #8a5a00;
--warn-bg: #fdf3e0;
--code-bg: #f4f6f9;
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--radius: 10px;
--scale: 1;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #0e1116;
--panel: #161b22;
--panel-2: #1c222b;
--ink: #e8ecf1;
--ink-2: #a8b3c0;
--ink-3: #7d8896;
--line: #2a313b;
--accent: #6a9bff;
--ok: #62d09a;
--ok-bg: #12291e;
--bad: #ff8c80;
--bad-bg: #2d1715;
--warn: #ffc76b;
--warn-bg: #2c2210;
--code-bg: #10151b;
}
}
:root[data-theme="dark"] {
--bg: #0e1116; --panel: #161b22; --panel-2: #1c222b; --ink: #e8ecf1;
--ink-2: #a8b3c0; --ink-3: #7d8896; --line: #2a313b; --accent: #6a9bff;
--ok: #62d09a; --ok-bg: #12291e; --bad: #ff8c80; --bad-bg: #2d1715;
--warn: #ffc76b; --warn-bg: #2c2210; --code-bg: #10151b;
}
* { box-sizing: border-box; }
html { font-size: calc(16px * var(--scale)); }
body {
margin: 0; background: var(--bg); color: var(--ink);
font-family: var(--sans); line-height: 1.55;
-webkit-font-smoothing: antialiased;
}
code, pre, .mono { font-family: var(--mono); }
a { color: var(--accent); }
/* ---------- header ---------- */
header.top {
position: sticky; top: 0; z-index: 30;
background: var(--panel); border-bottom: 1px solid var(--line);
padding: 0.7rem 1rem;
}
.top-row { display: flex; flex-wrap: wrap; gap: 0.6rem 1rem; align-items: center; }
.brand { font-weight: 650; font-size: 1.05rem; letter-spacing: -0.01em; margin-right: auto; }
.brand span { color: var(--ink-3); font-weight: 400; font-size: 0.85rem; margin-left: 0.5rem; }
.origins { display: flex; flex-wrap: wrap; gap: 0.4rem; font-size: 0.75rem; }
.chip {
font-family: var(--mono); background: var(--panel-2); border: 1px solid var(--line);
padding: 0.15rem 0.45rem; border-radius: 6px; white-space: nowrap;
}
.chip b { color: var(--ink-3); font-weight: 500; }
.chip.live { border-color: var(--ok); color: var(--ok); }
.chip.dead { border-color: var(--bad); color: var(--bad); }
button, select, input, textarea {
font: inherit; font-size: 0.85rem; color: var(--ink);
background: var(--panel); border: 1px solid var(--line);
border-radius: 7px; padding: 0.3rem 0.6rem;
}
button { cursor: pointer; }
button:hover { border-color: var(--accent); }
button.primary { background: var(--accent); color: #fff; border-color: transparent; font-weight: 550; }
button.primary:hover { filter: brightness(1.08); }
button.run { font-weight: 550; }
.toolbar { display: flex; gap: 0.4rem; align-items: center; flex-wrap: wrap; }
nav.groups { display: flex; gap: 0.35rem; flex-wrap: wrap; margin-top: 0.6rem; font-size: 0.8rem; }
nav.groups a { text-decoration: none; padding: 0.2rem 0.55rem; border-radius: 6px; background: var(--panel-2); color: var(--ink-2); }
nav.groups a:hover { color: var(--accent); }
/* ---------- layout ---------- */
.wrap { display: grid; grid-template-columns: minmax(0,1fr) 25rem; gap: 1.2rem; padding: 1.2rem 1rem 6rem; max-width: 96rem; margin: 0 auto; }
@media (max-width: 1100px) { .wrap { grid-template-columns: minmax(0,1fr); } aside.log { position: static; max-height: 22rem; } }
/* ---------- intro ---------- */
.intro { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 1rem 1.1rem; margin-bottom: 1.2rem; }
.intro h1 { margin: 0 0 0.4rem; font-size: 1.25rem; letter-spacing: -0.02em; }
.intro p { margin: 0.4rem 0; color: var(--ink-2); font-size: 0.92rem; max-width: 62ch; }
.intro ol { margin: 0.5rem 0 0; padding-left: 1.3rem; color: var(--ink-2); font-size: 0.92rem; }
/* ---------- groups & cards ---------- */
section.group { margin-bottom: 2rem; scroll-margin-top: 7rem; }
section.group > h2 { font-size: 1.1rem; margin: 0 0 0.15rem; letter-spacing: -0.015em; }
section.group > p.blurb { margin: 0 0 0.9rem; color: var(--ink-3); font-size: 0.88rem; }
article.card {
background: var(--panel); border: 1px solid var(--line); border-left: 3px solid var(--line);
border-radius: var(--radius); padding: 0.9rem 1rem; margin-bottom: 0.8rem;
}
article.card[data-expect="pass"] { border-left-color: var(--ok); }
article.card[data-expect="fail"] { border-left-color: var(--bad); }
article.card[data-expect="partial"] { border-left-color: var(--warn); }
article.card[data-expect="special"] { border-left-color: var(--accent); }
.card-head { display: flex; gap: 0.6rem; align-items: baseline; flex-wrap: wrap; }
.card-head h3 { margin: 0; font-size: 1rem; font-weight: 600; letter-spacing: -0.01em; }
.num { font-family: var(--mono); font-size: 0.78rem; color: var(--ink-3); }
.badge {
font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em; font-weight: 650;
padding: 0.1rem 0.45rem; border-radius: 5px; white-space: nowrap;
}
.badge.pass { background: var(--ok-bg); color: var(--ok); }
.badge.fail { background: var(--bad-bg); color: var(--bad); }
.badge.partial { background: var(--warn-bg); color: var(--warn); }
.badge.special { background: var(--panel-2); color: var(--accent); }
.badge.neutral { background: var(--panel-2); color: var(--ink-2); }
.why { margin: 0.5rem 0 0.7rem; font-size: 0.9rem; color: var(--ink-2); max-width: 78ch; }
.why code { background: var(--code-bg); padding: 0.05em 0.3em; border-radius: 4px; font-size: 0.86em; }
.why strong { color: var(--ink); }
.cols { display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem; }
@media (max-width: 720px) { .cols { grid-template-columns: 1fr; } }
.panel { background: var(--code-bg); border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
.panel h4 {
margin: 0; padding: 0.35rem 0.6rem; font-size: 0.68rem; font-weight: 650;
text-transform: uppercase; letter-spacing: 0.07em; color: var(--ink-3);
border-bottom: 1px solid var(--line); background: var(--panel-2);
}
.panel pre { margin: 0; padding: 0.55rem 0.6rem; font-size: 0.775rem; line-height: 1.5; overflow-x: auto; white-space: pre; }
.panel pre .k { color: var(--accent); }
.panel pre .c { color: var(--ink-3); }
.panel pre .s { color: var(--ok); }
.panel pre .hl { background: var(--warn-bg); border-radius: 3px; }
.actions { display: flex; gap: 0.6rem; align-items: center; margin-top: 0.7rem; flex-wrap: wrap; }
.watch { font-size: 0.8rem; color: var(--ink-3); }
.watch b { color: var(--ink-2); font-weight: 600; }
/* ---------- results ---------- */
.result { margin-top: 0.7rem; display: none; }
.result.shown { display: block; }
.verdict { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; font-size: 0.85rem; margin-bottom: 0.5rem; }
.verdict .big { font-weight: 650; }
.verdict .big.ok { color: var(--ok); }
.verdict .big.blocked { color: var(--bad); }
.meta { color: var(--ink-3); font-size: 0.78rem; font-family: var(--mono); }
.surprise { background: var(--warn-bg); color: var(--warn); border-radius: 5px; padding: 0.1rem 0.4rem; font-size: 0.72rem; font-weight: 650; }
.errbox { background: var(--bad-bg); color: var(--bad); border-radius: 7px; padding: 0.5rem 0.65rem; font-family: var(--mono); font-size: 0.78rem; white-space: pre-wrap; }
.kv { font-family: var(--mono); font-size: 0.78rem; }
.kv div { padding: 0.1rem 0; }
.kv .null { color: var(--bad); }
.kv .val { color: var(--ok); }
details.more { margin-top: 0.5rem; }
details.more > summary { cursor: pointer; font-size: 0.8rem; color: var(--ink-3); }
details.more > summary:hover { color: var(--accent); }
details.more pre { background: var(--code-bg); border: 1px solid var(--line); border-radius: 7px; padding: 0.5rem 0.6rem; font-size: 0.75rem; overflow-x: auto; margin: 0.4rem 0 0; }
.trace { margin-top: 0.5rem; border: 1px solid var(--line); border-radius: 7px; overflow: hidden; }
.trace-row { display: flex; gap: 0.5rem; padding: 0.3rem 0.55rem; font-family: var(--mono); font-size: 0.76rem; border-top: 1px solid var(--line); align-items: baseline; }
.trace-row:first-child { border-top: 0; }
.trace-row.pre { background: var(--warn-bg); }
.trace-row .m { font-weight: 650; min-width: 4.8rem; }
.trace-row .st { margin-left: auto; color: var(--ink-3); }
.trace-empty { padding: 0.4rem 0.55rem; font-size: 0.78rem; color: var(--ink-3); }
/* ---------- server log ---------- */
aside.log { position: sticky; top: 6.5rem; align-self: start; max-height: calc(100vh - 8rem); display: flex; flex-direction: column;
background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
aside.log header { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.7rem; border-bottom: 1px solid var(--line); background: var(--panel-2); }
aside.log header h2 { margin: 0; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.07em; color: var(--ink-3); margin-right: auto; }
.logbody { overflow-y: auto; padding: 0.3rem; font-family: var(--mono); font-size: 0.74rem; }
.entry { padding: 0.35rem 0.45rem; border-radius: 6px; margin-bottom: 0.25rem; background: var(--panel-2); border-left: 2px solid var(--line); }
.entry.preflight { border-left-color: var(--warn); }
.entry.actual { border-left-color: var(--accent); }
.entry .line1 { display: flex; gap: 0.4rem; align-items: baseline; }
.entry .line1 .m { font-weight: 650; }
.entry .line1 .s { margin-left: auto; }
.entry .s.s2 { color: var(--ok); } .entry .s.s3 { color: var(--warn); }
.entry .s.s4, .entry .s.s5 { color: var(--bad); }
.entry .sub { color: var(--ink-3); word-break: break-all; }
.entry .sent { color: var(--ok); }
.entry .none { color: var(--bad); }
.hint { padding: 0.6rem; color: var(--ink-3); font-size: 0.78rem; font-family: var(--sans); }
/* ---------- special demos ---------- */
.demo-row { display: flex; gap: 1rem; flex-wrap: wrap; align-items: flex-start; margin-top: 0.6rem; }
.demo-box { border: 1px dashed var(--line); border-radius: 8px; padding: 0.6rem; font-size: 0.78rem; color: var(--ink-3); }
.demo-box img { display: block; max-width: 220px; height: 60px; }
iframe.sink { width: 100%; height: 6.5rem; border: 1px solid var(--line); border-radius: 7px; background: var(--panel-2); margin-top: 0.5rem; }
/* ---------- playground ---------- */
.pg { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 1rem; }
.pg h2 { margin: 0 0 0.2rem; font-size: 1.1rem; }
.pg > p { margin: 0 0 0.9rem; color: var(--ink-3); font-size: 0.88rem; }
.pg-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 0.9rem; }
fieldset { border: 1px solid var(--line); border-radius: 8px; padding: 0.6rem 0.7rem; margin: 0; }
legend { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.07em; color: var(--ink-3); font-weight: 650; padding: 0 0.3rem; }
.field { display: grid; grid-template-columns: 8.5rem 1fr; gap: 0.4rem; align-items: center; margin-bottom: 0.35rem; font-size: 0.8rem; }
.field label { color: var(--ink-2); font-family: var(--mono); font-size: 0.75rem; }
.field input[type="text"], .field input[type="number"], .field select, .field textarea { width: 100%; }
.field textarea { min-height: 3.4rem; font-family: var(--mono); font-size: 0.75rem; resize: vertical; }
.field.check { grid-template-columns: 8.5rem 1fr; }
.field.check input { justify-self: start; }
.pg-actions { margin-top: 0.9rem; display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
.footnote { color: var(--ink-3); font-size: 0.78rem; margin-top: 1.2rem; max-width: 70ch; }
</style>
</head>
<body>
<header class="top">
<div class="top-row">
<div class="brand">Laboratorio de CORS <span>todas las configuraciones, una sola página</span></div>
<div class="origins" id="origins"></div>
<div class="toolbar">
<button id="cookieSet" title="Abre :3001 en una pestaña nueva y deja las cookies ahí">Poner cookies en la API</button>
<button id="runAll" class="primary">Probar todo</button>
<button id="clearAll">Limpiar</button>
<button id="fontDown" title="Más chico">A−</button>
<button id="fontUp" title="Más grande">A+</button>
<button id="theme" title="Cambiar tema">◐</button>
</div>
</div>
<nav class="groups" id="nav"></nav>
</header>
<div class="wrap">
<main>
<div class="intro">
<h1>Qué es esta página</h1>
<p>
Esta página se sirve desde <code id="pageOrigin"></code>. Hace requests a una API en
<code id="apiOrigin"></code> — un <strong>origen distinto</strong>, así que todos ellos pasan por CORS.
La API reconstruye sus headers CORS a partir de los query params en cada request, de modo que cada escenario
de abajo reconfigura el servidor al vuelo. No hace falta reiniciar nada.
</p>
<p>
El panel de la derecha es el <strong>log del propio servidor</strong>, en vivo. Es lo más útil de todo esto:
muestra qué recibió y qué respondió el servidor realmente, al lado de lo que el navegador estuvo dispuesto a
contarle a JavaScript. Esas dos cosas difieren mucho más seguido de lo que uno esperaría.
</p>
<ol>
<li>Abrí DevTools → Network y dejalo a la vista. Todas las respuestas bloqueadas están ahí.</li>
<li>Hacé clic una vez en <b>Poner cookies en la API</b>, así los escenarios de credenciales tienen algo para mandar.</li>
<li>Recorré los grupos de arriba abajo, o tocá <b>Probar todo</b> y comentá el resumen.</li>
</ol>
</div>
<div id="scenarios"></div>
<section class="group" id="g-playground">
<h2>6 · Laboratorio libre</h2>
<p class="blurb">Todas las perillas, para improvisar cuando alguien pregunta "¿y si…?".</p>
<div class="pg" id="pg"></div>
</section>
<p class="footnote">
El código del servidor está en <code>server.js</code>: unas 330 líneas de Node puro, sin dependencias, escritas para
leerse en voz alta. La lista de escenarios son datos en <code>public/scenarios.js</code>, así que podés agregar los tuyos.
</p>
</main>
<aside class="log">
<header>
<h2>Log del servidor</h2>
<span class="chip" id="logStatus">conectando…</span>
<button id="logClear">Limpiar</button>
</header>
<div class="logbody" id="logBody">
<div class="hint">Acá aparece cada request que recibe la API — incluidos los preflights que el navegador mandó por su cuenta, y los requests cuyas respuestas el navegador se negó a darle a JavaScript.</div>
</div>
</aside>
</div>
<script src="scenarios.js"></script>
<script src="app.js"></script>
</body>
</html>