new components

This commit is contained in:
2026-08-06 18:10:55 +02:00
parent b02c885b83
commit f86db1556c
149 changed files with 7038 additions and 395 deletions

706
libs/ui/docs/theming.html Normal file
View File

@@ -0,0 +1,706 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8" />
<title>@pi/ui — Guía de theming</title>
<style>
:root {
--primary: #799ef0;
--primary-900: #5772d4;
--accent: #eda82a;
--highlighted: #0d2a6b;
--success: #1cb273;
--warn: #e45959;
--ink: #1a1a1a;
--grey-700: #44475a;
--grey-500: #6b7280;
--grey-400: #9ca3af;
--grey-200: #e5e7eb;
--grey-100: #f3f4f6;
--callout-bg: #eef1fd;
--callout-border: #5772d4;
--warncallout-bg: #fdecec;
--warncallout-border: #e45959;
--code-bg: #12162a;
--code-text: #dbe3f5;
--code-token: #9db4f5;
--code-comment: #7c869e;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
color: var(--ink);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
font-size: 13px;
line-height: 1.55;
}
code, .mono {
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-size: 0.92em;
white-space: nowrap;
}
pre.code, pre.code code {
white-space: pre-wrap;
}
p { margin: 0 0 10px; color: var(--grey-700); }
a { color: var(--primary-900); }
.token {
color: var(--primary-900);
background: #eef1fd;
padding: 1px 4px;
border-radius: 3px;
white-space: nowrap;
}
section.page {
page-break-after: always;
padding: 4px 4px 24px;
}
section.page:last-child { page-break-after: auto; }
/* ---------- Cover page ---------- */
.cover-kicker {
color: var(--primary-900);
font-weight: 700;
font-size: 11px;
letter-spacing: 0.12em;
margin-bottom: 18px;
}
.cover-title {
font-size: 34px;
font-weight: 800;
line-height: 1.15;
margin: 0 0 16px;
color: var(--ink);
}
.cover-subtitle {
font-size: 14px;
color: var(--grey-500);
max-width: 30em;
margin-bottom: 26px;
}
.dots { display: flex; gap: 10px; margin-bottom: 26px; }
.dot {
width: 26px; height: 26px; border-radius: 50%;
display: inline-block;
}
.cover-rule { border: none; border-top: 1px solid var(--grey-200); margin: 0 0 20px; }
.cover-note {
font-size: 11px;
color: var(--grey-400);
max-width: 34em;
margin-bottom: 34px;
}
.toc-item {
display: flex;
align-items: baseline;
gap: 14px;
padding: 11px 0;
border-bottom: 1px dashed var(--grey-200);
font-size: 13px;
}
.toc-num { color: var(--primary-900); font-weight: 700; width: 1.4em; }
/* ---------- Section header ---------- */
h1.section-title {
font-size: 19px;
font-weight: 800;
margin: 0 0 6px;
padding-bottom: 10px;
border-bottom: 2px solid var(--ink);
}
h1.section-title .num { color: var(--primary-900); margin-right: 8px; }
.lede { color: var(--grey-500); margin-bottom: 18px; font-size: 13px; }
h2.sub {
font-size: 13.5px;
font-weight: 700;
margin: 22px 0 8px;
}
/* ---------- Tables ---------- */
table { width: 100%; border-collapse: collapse; margin-bottom: 14px; }
thead th {
text-align: left;
font-size: 9.5px;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--grey-500);
border-bottom: 1px solid var(--grey-200);
padding: 6px 10px 6px 0;
font-weight: 700;
}
tbody td {
padding: 8px 10px 8px 0;
border-bottom: 1px solid var(--grey-100);
vertical-align: top;
font-size: 12px;
color: var(--grey-700);
}
tbody tr:last-child td { border-bottom: none; }
td.tok { white-space: nowrap; }
small.note { color: var(--grey-400); font-size: 10.5px; }
/* ---------- Code blocks ---------- */
pre.code {
background: var(--code-bg);
color: var(--code-text);
border-radius: 8px;
padding: 16px 18px;
font-size: 11.5px;
line-height: 1.6;
overflow: hidden;
margin: 10px 0 16px;
white-space: pre-wrap;
word-break: break-word;
}
pre.code .t { color: var(--code-token); }
pre.code .c { color: var(--code-comment); font-style: italic; }
/* ---------- Callouts ---------- */
.callout {
background: var(--callout-bg);
border-left: 3px solid var(--callout-border);
border-radius: 4px;
padding: 12px 16px;
font-size: 11.5px;
color: #2c3550;
margin: 14px 0;
}
.callout.warn {
background: var(--warncallout-bg);
border-left-color: var(--warncallout-border);
color: #5a2a2a;
}
.callout b { color: inherit; }
/* ---------- Numbered steps ---------- */
.step { display: flex; gap: 14px; margin-bottom: 18px; align-items: flex-start; }
.step-num {
flex: none;
width: 24px; height: 24px; border-radius: 50%;
background: var(--primary-900); color: #fff;
font-size: 12px; font-weight: 700;
display: flex; align-items: center; justify-content: center;
margin-top: 1px;
}
.step-body { flex: 1; }
.step-title { font-weight: 700; margin-bottom: 6px; font-size: 13px; }
/* ---------- Theme cards (§4) ---------- */
.theme-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin: 14px 0 18px;
}
.theme-card {
border: 1px solid var(--grey-200);
border-radius: 8px;
padding: 12px 14px;
}
.theme-card .name {
font-weight: 700;
font-size: 12.5px;
margin-bottom: 5px;
display: flex; align-items: center; gap: 7px;
}
.theme-card .name .swatch { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
.theme-card p { margin: 0; font-size: 11.5px; color: var(--grey-500); }
/* ---------- Gradient demo (§5) ---------- */
.demo-row { display: flex; gap: 26px; margin: 16px 0 4px; align-items: flex-start; }
.demo-box {
width: 150px; height: 44px;
border-radius: 12px;
display: flex; align-items: center; justify-content: center;
}
.demo-box.plain { border: 1px solid var(--grey-200); background: #fff; }
.demo-box.gradient {
border: 3px solid transparent;
background:
linear-gradient(#fff, #fff) padding-box,
linear-gradient(120deg, var(--primary), var(--accent), var(--highlighted)) border-box;
}
.demo-caption { font-size: 10.5px; color: var(--grey-500); text-align: center; margin-top: 6px; }
.demo-caption code { font-size: 10px; }
/* ---------- Checklist ---------- */
ul.check, ul.cross { list-style: none; padding: 0; margin: 0 0 20px; }
ul.check li, ul.cross li {
padding: 6px 0 6px 24px;
position: relative;
font-size: 12px;
color: var(--grey-700);
border-bottom: 1px solid var(--grey-100);
}
ul.check li:before {
content: "✓";
position: absolute; left: 0;
color: var(--success); font-weight: 700;
}
ul.cross li:before {
content: "✗";
position: absolute; left: 0;
color: var(--warn); font-weight: 700;
}
</style>
</head>
<body>
<!-- ================= PAGE 1: COVER ================= -->
<section class="page">
<div class="cover-kicker">@PI/UI — DESIGN SYSTEM</div>
<div class="cover-title">Cómo crear<br />temas</div>
<div class="cover-subtitle">
Guía práctica del sistema de theming: qué tokens existen, cómo se aplican,
y los pasos para dar de alta un tema nuevo sin tocar ni un selector de
componente.
</div>
<div class="dots">
<span class="dot" style="background:var(--primary)"></span>
<span class="dot" style="background:var(--accent)"></span>
<span class="dot" style="background:var(--highlighted)"></span>
<span class="dot" style="background:var(--success)"></span>
<span class="dot" style="background:var(--warn)"></span>
<span class="dot" style="background:var(--ink)"></span>
</div>
<hr class="cover-rule" />
<div class="cover-note">
Basada en el estado real de <code>libs/ui/src/lib/styles</code>: los cuatro
temas ya presentes en el repo (Default, Alternative, Minimal, Gradient)
sirven de ejemplo en todo el documento. Incluye los tokens de overlays
(modal, notificación, tooltip, accordion) añadidos tras la primera
versión de esta guía.
</div>
<div class="toc-item"><span class="toc-num">1</span> Arquitectura: shell + tokens</div>
<div class="toc-item"><span class="toc-num">2</span> El contrato de tokens</div>
<div class="toc-item"><span class="toc-num">3</span> Crear un tema, paso a paso</div>
<div class="toc-item"><span class="toc-num">4</span> Los cuatro temas del repo</div>
<div class="toc-item"><span class="toc-num">5</span> Caso de estudio: anillo de foco en degradado</div>
<div class="toc-item"><span class="toc-num">6</span> Modo oscuro y temas</div>
<div class="toc-item"><span class="toc-num">7</span> Checklist</div>
</section>
<!-- ================= PAGE 2: §1 ARQUITECTURA ================= -->
<section class="page">
<h1 class="section-title"><span class="num">1</span>Arquitectura: shell + tokens</h1>
<p class="lede">Dos piezas, ninguna es un selector de componente.</p>
<p>
Un tema en <code>@pi/ui</code> no es un solo fichero que reescriba
<code>.pi-button</code>, <code>.pi-card</code> o cualquier otro selector de
componente. Es, exclusivamente, un bloque que redefine
<b>variables CSS</b> (custom properties) bajo una clase contenedora. Los
componentes ya están escritos para leer esas variables — cambiarlas es
todo lo que hace falta.
</p>
<h2 class="sub">La clase contenedora: <code>.pi-shell</code></h2>
<p>
Todo tema se aplica añadiendo dos clases a un elemento raíz (normalmente
<code>&lt;body&gt;</code>, porque los overlays de Material — diálogos,
menús, snackbars — cuelgan de ahí y también deben heredar el tema):
</p>
<pre class="code">&lt;body class="<span class="t">pi-shell pi-shell__&lt;nombre-del-tema&gt;</span>"&gt;</pre>
<p>
<code>.pi-shell</code> por sí sola ya aplica el contrato completo de
tokens con sus valores por defecto (ver <code>themes/_default.scss</code>).
<code>.pi-shell__&lt;nombre&gt;</code> se aplica <i>a la vez</i>, no en su
lugar, y sobreescribe solo los tokens que ese tema quiere cambiar. Como
ambas clases tienen la misma especificidad, gana la que se declaró más
tarde en el CSS compilado — por eso el orden de <code>@forward</code> en
<code>themes/index.scss</code> importa (ver §3).
</p>
<div class="callout">
El helper <code>shellTheming()</code> (en
<code>libs/ui/src/lib/styles/configs.ts</code>) construye ese string por
ti: <code>shellTheming('minimal')</code> devuelve
<code>'pi-shell pi-shell__minimal'</code>. <code>ShellClass</code> ya
expone uno por tema — no hace falta escribir el string a mano.
</div>
<h2 class="sub">Las dos familias de variables</h2>
<table>
<thead>
<tr><th style="width:16%">Prefijo</th><th style="width:52%">Qué controla</th><th>Ejemplo</th></tr>
</thead>
<tbody>
<tr>
<td class="tok"><code class="token">--pi-color-*</code></td>
<td>Paleta de marca: <code>primary</code>, <code>accent</code>, <code>highlighted</code>, <code>warn</code>, <code>success</code>, <code>grey</code>, cada uno con escalones (<code>-10</code> a <code>-900</code>).</td>
<td><code>--pi-color-primary</code>, <code>--pi-color-primary-900</code></td>
</tr>
<tr>
<td class="tok"><code class="token">--pi-theme-*</code></td>
<td>Forma y comportamiento: radios, sombras, grosor de borde, peso/transformación de texto, rellenos de botón.</td>
<td><code>--pi-theme-button-radius</code>, <code>--pi-theme-card-shadow</code></td>
</tr>
</tbody>
</table>
<p>
Un tema puede tocar una familia, la otra, o ambas.
<code>_alternative.scss</code> cambia las dos (repaleta
primary/accent/highlighted <i>y</i> la forma). <code>_minimal.scss</code>
y <code>_gradient.scss</code> solo tocan <code>--pi-theme-*</code> — la
paleta de marca se queda igual a propósito.
</p>
<h2 class="sub">El puente con Angular Material</h2>
<p>
Los componentes nativos de Material (slider, checkbox, radio…) no leen
<code>--pi-color-*</code>: pintan con sus propios roles
<code>--mat-sys-*</code>, que genera <code>mat.theme()</code>. El puente se
declara <b>una sola vez</b>, en <code>_default.scss</code>, por
indirección:
</p>
<pre class="code"><span class="t">--mat-sys-primary</span>: var(--pi-color-primary);
<span class="t">--mat-sys-primary-container</span>: var(--pi-color-primary-100);
<span class="c">/* … */</span></pre>
<p>
Como es una indirección, cualquier tema que redefina
<code>--pi-color-primary</code> arrastra automáticamente a Material sin
tener que repetir el bloque del puente. Si tu tema no toca la paleta (como
Minimal o Gradient), no necesitas tocar esto en absoluto.
</p>
<div class="callout warn">
<b>Límite conocido:</b> el puente cubre color, no forma. El radio/sombra de
checkbox, radio, slider o mat-select vienen de tokens <code>--mdc-*</code>
que Angular Material fija en tiempo de compilación (no son variables de
runtime reasignables desde un tema). Aplanarlos también requeriría
overrides por componente — fuera del alcance de este contrato.
</div>
</section>
<!-- ================= PAGE 3-4: §2 CONTRATO DE TOKENS ================= -->
<section class="page">
<h1 class="section-title"><span class="num">2</span>El contrato de tokens</h1>
<p class="lede">Todo lo que un tema puede redefinir, con su valor en el tema por defecto y dónde se consume.</p>
<h2 class="sub">Botones</h2>
<table>
<thead><tr><th style="width:34%">Token</th><th style="width:33%">Por defecto</th><th>Se usa en</th></tr></thead>
<tbody>
<tr><td class="tok"><code class="token">--pi-theme-button-height</code></td><td><code>var(--pi-spacing-xxx-large)</code> <small class="note">(2.5rem)</small></td><td><code>pi-button/_pi-button-theme.scss</code></td></tr>
<tr><td class="tok"><code class="token">--pi-theme-button-radius</code></td><td><code>var(--pi-border-radius-medium)</code></td><td><code>pi-button/_pi-button.scss</code>, <code>dynamic-menu.component.scss</code></td></tr>
<tr><td class="tok"><code class="token">--pi-theme-button-font-weight</code></td><td><code>600</code></td><td rowspan="4"><code>pi-button-theme.scss</code></td></tr>
<tr><td class="tok"><code class="token">--pi-theme-button-letter-spacing</code></td><td><code>normal</code></td></tr>
<tr><td class="tok"><code class="token">--pi-theme-button-text-transform</code></td><td><code>none</code></td></tr>
<tr><td class="tok"><code class="token">--pi-theme-button-shadow</code></td><td><code>none</code></td></tr>
<tr><td class="tok"><code class="token">--pi-theme-button-transition</code></td><td><code>background-color .16s ease-in</code></td><td><code>pi-button-theme.scss</code>, <code>stepper.component.scss</code>, <code>file-upload.component.scss</code>, <code>dynamic-menu.component.scss</code></td></tr>
<tr><td class="tok"><code class="token">--pi-theme-button-fill-{primary,accent,warn,success,highlighted}</code></td><td><code>none</code> <small class="note">(manda el <code>background-color</code> sólido)</small></td><td><code>_pi-button-theme.scss</code></td></tr>
</tbody>
</table>
<h2 class="sub">Campos de formulario</h2>
<table>
<thead><tr><th style="width:34%">Token</th><th style="width:33%">Por defecto</th><th>Se usa en</th></tr></thead>
<tbody>
<tr><td class="tok"><code class="token">--pi-theme-field-radius</code></td><td><code>0.875rem</code></td><td rowspan="7"><code>vendors/_mat-form-field.scss</code>, <code>file-upload.component.scss</code>, <code>_mat-datepicker.scss</code>, <code>_mat-select.scss</code></td></tr>
<tr><td class="tok"><code class="token">--pi-theme-field-border-width</code></td><td><code>1px</code></td></tr>
<tr><td class="tok"><code class="token">--pi-theme-field-border-color</code></td><td><code>var(--pi-color-grey-200)</code></td></tr>
<tr><td class="tok"><code class="token">--pi-theme-field-border-color-hover</code></td><td><code>var(--pi-color-grey-300)</code></td></tr>
<tr><td class="tok"><code class="token">--pi-theme-field-border-color-focus</code></td><td><code>var(--pi-color-primary)</code></td></tr>
<tr><td class="tok"><code class="token">--pi-theme-field-surface</code></td><td><code>var(--pi-color-white)</code></td></tr>
<tr><td class="tok"><code class="token">--pi-theme-field-label-color</code></td><td><code>var(--pi-color-grey-300)</code></td></tr>
<tr><td class="tok"><code class="token">--pi-theme-field-label-transform</code> / <code>-letter-spacing</code> / <code>-weight</code></td><td><small class="note">sin declarar → hereda el valor original del componente</small></td><td><code>_mat-form-field.scss</code></td></tr>
<tr><td class="tok"><code class="token">--pi-theme-field-focus-gradient</code></td><td><small class="note">sin declarar → <code>none</code> (opt-in, ver §5)</small></td><td><code>_mat-form-field.scss</code></td></tr>
</tbody>
</table>
<h2 class="sub">Superficies</h2>
<table>
<thead><tr><th style="width:34%">Token</th><th style="width:33%">Por defecto</th><th>Se usa en</th></tr></thead>
<tbody>
<tr><td class="tok"><code class="token">--pi-theme-card-radius</code></td><td><code>var(--pi-border-radius-large)</code></td><td><code>components/_pi-card.scss</code>, <code>skeleton.component.scss</code></td></tr>
<tr><td class="tok"><code class="token">--pi-theme-card-shadow</code></td><td><code>var(--pi-box-shadow-panel)</code></td><td><code>_pi-card.scss</code></td></tr>
<tr><td class="tok"><code class="token">--pi-theme-panel-radius</code></td><td><code>var(--pi-border-radius-large)</code></td><td><code>panel.component.scss</code>, <code>_mat-select.scss</code>, <code>_mat-datepicker.scss</code>, <code>dynamic-menu.component.scss</code></td></tr>
<tr><td class="tok"><code class="token">--pi-theme-chip-radius</code></td><td><small class="note">sin declarar → radio original del chip</small></td><td><code>chip/styles/_size.scss</code> (×3)</td></tr>
</tbody>
</table>
<div class="callout">
Fíjate en el patrón <code>var(--pi-theme-x, &lt;valor-original&gt;)</code>
que usan varios consumidores (chip, panel, card, overlays). Eso significa
que <b>no es obligatorio declarar todos los tokens</b> en un tema nuevo:
los que omitas caen al valor que ya tenía el componente antes de que
existiera el sistema de theming. Solo declara lo que quieras cambiar.
</div>
</section>
<section class="page">
<h2 class="sub" style="margin-top:0">Overlays <small class="note">(modal, notificación, tooltip, accordion)</small></h2>
<p>
Grupo añadido después de la primera versión de este contrato. Ningún tema
salvo <b>Minimal</b> los redefine todavía: Default, Alternative y Gradient
usan siempre el segundo argumento de <code>var()</code>, que reproduce
exactamente la forma que tenía cada overlay antes de tokenizar.
</p>
<table>
<thead><tr><th style="width:34%">Token</th><th style="width:33%">Por defecto (fallback)</th><th>Se usa en</th></tr></thead>
<tbody>
<tr><td class="tok"><code class="token">--pi-theme-modal-radius</code></td><td><small class="note">sin declarar →</small> <code>var(--pi-border-radius-medium)</code> <small class="note">(<code>var(--pi-border-radius-large)</code> en <code>.pi-modal__panel--full</code>)</small></td><td><code>vendors/_mat-dialog.scss</code> (×2)</td></tr>
<tr><td class="tok"><code class="token">--pi-theme-notification-radius</code></td><td><small class="note">sin declarar →</small> <code>var(--pi-border-radius-large)</code></td><td><code>snack-bar.component.scss</code>, <code>_mat-snackbar.scss</code> (×2)</td></tr>
<tr><td class="tok"><code class="token">--pi-theme-notification-shadow</code></td><td><small class="note">sin declarar →</small> <code>var(--pi-box-shadow)</code></td><td><code>_mat-snackbar.scss</code></td></tr>
<tr><td class="tok"><code class="token">--pi-theme-tooltip-radius</code></td><td><small class="note">sin declarar →</small> <code>var(--pi-border-radius-medium)</code></td><td><code>_mat-tooltip.scss</code></td></tr>
<tr><td class="tok"><code class="token">--pi-theme-tooltip-shadow</code></td><td><small class="note">sin declarar →</small> <code>var(--pi-box-shadow-panel-double)</code></td><td><code>_mat-tooltip.scss</code></td></tr>
<tr><td class="tok"><code class="token">--pi-theme-accordion-radius</code></td><td><small class="note">sin declarar →</small> <code>var(--pi-border-radius-large)</code></td><td><code>accordion.component.scss</code>, <code>_horizontal-accordion.scss</code>, <code>_vertical-accordion.scss</code> (×6, incluye un <code>calc()</code>)</td></tr>
</tbody>
</table>
<div class="callout">
Minimal declara los seis en su mixin (<code>_minimal.scss</code>): aplana
todos los radios a <code>--pi-border-radius-xx-small</code> y apaga las dos
sombras (<code>none</code>) — mismo criterio que ya aplicaba a botón, campo
y card, extendido al resto de superficies flotantes.
</div>
<!-- ================= §3 CREAR UN TEMA ================= -->
<h1 class="section-title" style="margin-top:26px"><span class="num">3</span>Crear un tema, paso a paso</h1>
<p class="lede">Cuatro ficheros, ninguno toca un componente. Ejemplo con un tema ficticio <code>brutalist</code>.</p>
<div class="step">
<div class="step-num">1</div>
<div class="step-body">
<div class="step-title">Crea <code>libs/ui/src/lib/styles/themes/_brutalist.scss</code></div>
<p>Un mixin con los tokens que cambian, más una clase que lo aplica:</p>
<pre class="code">@mixin <span class="t">brutalistThemeTokens</span> {
--pi-theme-button-radius: 0;
--pi-theme-button-shadow: 4px 4px 0 var(--pi-color-black);
--pi-theme-card-radius: 0;
--pi-theme-card-shadow: none;
}
.pi-shell__brutalist {
@include brutalistThemeTokens;
}</pre>
</div>
</div>
<div class="step">
<div class="step-num">2</div>
<div class="step-body">
<div class="step-title">Regístralo en <code>themes/index.scss</code></div>
<p>Añade el <code>@forward</code>/<code>@use</code> al final de la lista — el orden es el orden de prioridad en el CSS compilado:</p>
<pre class="code">@forward 'default';
@forward 'alternative';
@forward 'minimal';
@forward 'gradient';
@forward 'brutalist'; <span class="c">// ← nuevo, al final</span>
@use 'default' as *;
@use 'alternative' as *;
@use 'minimal' as *;
@use 'gradient' as *;
@use 'brutalist' as *; <span class="c">// ← nuevo, al final</span></pre>
</div>
</div>
<div class="step">
<div class="step-num">3</div>
<div class="step-body">
<div class="step-title">Añade la entrada en <code>ShellClass</code> <small class="note">(<code>libs/ui/src/lib/styles/configs.ts</code>)</small></div>
<pre class="code">export const ShellClass = {
Default: shellTheming('default'),
Alternative: shellTheming('alternative'),
Minimal: shellTheming('minimal'),
Gradient: shellTheming('gradient'),
<span class="c">/* Bordes duros, sombra dura, sin medias tintas. */</span>
Brutalist: shellTheming('brutalist'),
};</pre>
</div>
</div>
<div class="step">
<div class="step-num">4</div>
<div class="step-body">
<div class="step-title">Aplícalo en la app consumidora</div>
<p>En el playground esto ya está resuelto con un signal + effect (ver <code>apps/playground/src/app/app.ts</code>); en cualquier otra app basta con:</p>
<pre class="code">import { ShellClass } from '@pi/ui';
document.body.className = ShellClass.Brutalist;</pre>
</div>
</div>
<div class="callout">
No hay quinto paso. No se toca ningún <code>.component.scss</code>, ningún
selector, ningún componente TypeScript. Si necesitas cambiar algo que
<b>no</b> está en el contrato de tokens (§2), esa es la señal de que hace
falta añadir un token nuevo al contrato — y entonces sí tocas el
<code>.scss</code> del componente afectado, pero solo una vez, para que
quede disponible para todos los temas futuros (así se añadió
<code>--pi-theme-field-focus-gradient</code>, ver §5).
</div>
</section>
<!-- ================= §4 LOS CUATRO TEMAS ================= -->
<section class="page">
<h1 class="section-title"><span class="num">4</span>Los cuatro temas del repo</h1>
<p class="lede">Cuatro implementaciones reales del mismo contrato, en <code>libs/ui/src/lib/styles/themes/</code>.</p>
<div class="theme-grid">
<div class="theme-card">
<div class="name"><span class="swatch" style="background:var(--primary)"></span>Default</div>
<p>Rellenos planos y sólidos, controles compactos. Declara el contrato completo — el resto de temas parten de aquí.</p>
</div>
<div class="theme-card">
<div class="name"><span class="swatch" style="background:#8a63f0"></span>Alternative</div>
<p>Aspecto de marketing: repaleta primary→violeta, accent→turquesa, botones en pastilla con degradado y sombra, campos con superficie tintada.</p>
</div>
<div class="theme-card">
<div class="name"><span class="swatch" style="background:var(--ink)"></span>Minimal</div>
<p>Esquinas casi rectas, sin sombra (separa por borde fino en vez de elevación), sin degradados ni versalitas. Ahora también aplana los overlays — modal, notificación, tooltip y accordion comparten el mismo radio recto. Misma paleta que Default.</p>
</div>
<div class="theme-card">
<div class="name"><span class="swatch" style="background:var(--accent)"></span>Gradient</div>
<p>Igual que Default en forma, pero con degradado en el relleno de los botones y en el anillo de foco de los campos.</p>
</div>
</div>
<p>
Los cuatro comparten <i>exactamente</i> los mismos selectores de
componente — la prueba de que el contrato de tokens funciona es que
ninguno necesitó tocar un <code>.component.ts</code> ni un
<code>.component.html</code>.
</p>
</section>
<!-- ================= §5 CASO DE ESTUDIO ================= -->
<section class="page">
<h1 class="section-title"><span class="num">5</span>Caso de estudio: anillo de foco en degradado</h1>
<p class="lede">Qué hacer cuando el efecto que quieres <b>no</b> lo cubre ningún token existente.</p>
<p>
El tema <i>Gradient</i> quería un borde en degradado al enfocar un campo.
El problema: el borde de un <code>&lt;mat-form-field appearance="outline"&gt;</code>
no es un <code>border</code> normal — es tres <code>&lt;div&gt;</code>
(<code>leading</code>, <code>notch</code>, <code>trailing</code>) cada uno
con su propio borde parcial, para dejar hueco a la etiqueta flotante. Un
<code>background</code> en degradado ahí se vería cortado en cada unión.
</p>
<h2 class="sub">La solución: un anillo aparte, no el borde del notch</h2>
<p>
En vez de pelear con los tres fragmentos, se añade un
<code>::after</code> sobre <code>.mat-mdc-text-field-wrapper</code> (la
caja completa, sin partir) que pinta <i>solo el borde</i> gracias a
<code>mask-composite</code>: se enmascara la copia
<code>padding-box</code> (el interior) fuera de la copia
<code>border-box</code> (el rectángulo completo), dejando visible
únicamente el anillo:
</p>
<small class="note">libs/ui/src/lib/styles/common/vendors/_mat-form-field.scss</small>
<pre class="code">.mat-mdc-text-field-wrapper { position: relative; }
&.mat-focused .mat-mdc-text-field-wrapper::after {
content: '';
position: absolute;
inset: 0;
z-index: 1;
border-radius: var(--pi-theme-field-radius);
padding: var(--pi-theme-field-border-width, 1px);
background: <span class="t">var(--pi-theme-field-focus-gradient, none)</span>;
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
}</pre>
<p>
El token nuevo, <code>--pi-theme-field-focus-gradient</code>, se añadió al
contrato compartido (consumido con fallback <code>none</code>) — así
cualquier tema puede optar por él sin que los demás se vean afectados.
<i>Gradient</i> además pone
<code>--pi-theme-field-border-color-focus: transparent</code> para apagar
el borde sólido del notch; si no, se vería el borde plano por debajo del
anillo.
</p>
<div class="demo-row">
<div>
<div class="demo-box plain"></div>
<div class="demo-caption">Sin degradado<br /><code>focus-gradient: none</code></div>
</div>
<div>
<div class="demo-box gradient"></div>
<div class="demo-caption">Con degradado<br />(anillo vía <code>mask-composite</code>)</div>
</div>
</div>
<div class="callout">
<b>Regla general:</b> cuando el efecto que buscas no cabe en una simple
reasignación de variable, la solución sigue siendo "un token opt-in con
fallback inerte" + "una técnica CSS aislada en el fichero compartido del
componente" — nunca un selector nuevo dentro del propio fichero del tema.
El fichero de tema solo declara <i>valores</i>.
</div>
</section>
<!-- ================= §6 MODO OSCURO ================= -->
<section class="page">
<h1 class="section-title"><span class="num">6</span>Modo oscuro y temas</h1>
<p class="lede">El modo oscuro del playground es ortogonal a los temas — conviven, no se solapan.</p>
<p>
<code>@pi/ui</code> no trae un tema oscuro propio. El playground activa
uno "best-effort" añadiendo la clase <code>dark-mode</code> al
<code>&lt;html&gt;</code> (no al <code>&lt;body&gt;</code>, que es donde
vive <code>.pi-shell</code>), y ahí reasigna solo los
<b>neutros</b> (<code>--pi-color-white</code>, <code>--pi-color-grey-*</code>,
<code>--pi-color-black</code>) — las paletas de marca
(<code>primary</code>, <code>accent</code>…) se dejan igual:
</p>
<small class="note">apps/playground/src/app/styles.scss</small>
<pre class="code">html.dark-mode {
--pi-color-white: #1c1f26;
--pi-color-grey-100: #2a2e38;
--pi-color-black: #f1f2f4;
<span class="c">/* … solo neutros, la marca no cambia … */</span>
}</pre>
<p>
Como es una clase distinta en un elemento distinto, cualquier combinación
(tema × modo oscuro) funciona sin código extra — son dos cascadas
independientes que solo coinciden en que ambas reasignan variables CSS. La
única excepción real es cuando un tema usa un escalón de marca
<i>como si fuera</i> superficie neutra (por ejemplo,
<code>--pi-color-primary-25</code> como fondo de campo en Alternative): ese
caso puntual sí necesita un ajuste en modo oscuro, ya presente en
<code>styles.scss</code> bajo <code>html.dark-mode .pi-shell__alternative</code>.
</p>
</section>
<!-- ================= §7 CHECKLIST ================= -->
<section class="page">
<h1 class="section-title"><span class="num">7</span>Checklist</h1>
<h2 class="sub" style="margin-top:6px">Antes de dar el tema por terminado</h2>
<ul class="check">
<li>El fichero nuevo vive en <code>themes/</code>, exporta un mixin y una clase <code>.pi-shell__&lt;nombre&gt;</code>.</li>
<li>Está en <code>themes/index.scss</code>, al final de ambos bloques (<code>@forward</code> y <code>@use</code>).</li>
<li>Tiene entrada en <code>ShellClass</code> (<code>configs.ts</code>), con un comentario de una línea que resuma el aspecto.</li>
<li>Solo declara los tokens que cambia — el resto hereda de <code>_default.scss</code> sin repetirlo.</li>
<li>Si toca <code>--pi-color-primary/accent/highlighted</code>, revisado que el puente <code>--mat-sys-*</code> (declarado una vez en default) sigue coherente.</li>
<li>Si toca overlays (modal/notificación/tooltip/accordion), revisado con las tres páginas de catálogo, no solo con un botón suelto.</li>
<li>Probado en el playground con las tres páginas de catálogo, no solo en un botón suelto.</li>
</ul>
<h2 class="sub">Evita</h2>
<ul class="cross">
<li>Añadir un selector de componente (<code>.pi-button.mi-tema</code>) dentro del fichero del tema — si hace falta, el token que falta va al contrato compartido, no aquí.</li>
<li>Redeclarar tokens que no cambian "por si acaso" — genera ruido y dificulta ver qué es realmente distinto en ese tema.</li>
<li>Tocar <code>warn</code> / <code>success</code> por estética — son colores semánticos; cambiarlos perjudica la lectura del estado (error/éxito).</li>
<li>Asumir que <code>--pi-theme-*</code> alcanza a los controles nativos de Material (checkbox, radio, slider): esos usan tokens <code>--mdc-*</code> fijados en compilación, fuera de este contrato (ver §1).</li>
</ul>
</section>
</body>
</html>

Binary file not shown.