732 lines
34 KiB
HTML
732 lines
34 KiB
HTML
<!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><body></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"><body class="<span class="t">pi-shell pi-shell__<nombre-del-tema></span>"></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__<nombre></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, <valor-original>)</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>
|
||
<tr><td class="tok"><code class="token">--pi-theme-overlay-divider-color</code></td><td><small class="note">sin declarar →</small> <code>var(--pi-color-grey-100)</code></td><td><code>drawer.component.scss</code>, <code>hub.component.scss</code></td></tr>
|
||
<tr><td class="tok"><code class="token">--pi-theme-hub-grabber-color</code></td><td><small class="note">sin declarar →</small> <code>var(--pi-color-grey-100)</code></td><td><code>hub.component.scss</code></td></tr>
|
||
</tbody>
|
||
</table>
|
||
<div class="callout">
|
||
Minimal declara los seis primeros 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. Los dos
|
||
últimos (drawer/hub) son más recientes y todavía ningún tema los redefine.
|
||
</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="callout">
|
||
Los cuatro pasos de esta página los da un generador:
|
||
<code>nx g @pi/ui:theme brutalist</code>. Crea el fichero del paso 1 (en
|
||
blanco, con los grupos de tokens más habituales comentados), hace los
|
||
pasos 2 y 3, y además extiende <code>PiThemeService</code> (paso 4) para
|
||
que el tema quede seleccionable sin ningún paso manual más. Lo que sigue
|
||
es para quien prefiera hacerlo a mano o quiera entender qué toca cada
|
||
paso.
|
||
</div>
|
||
|
||
<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>Con <code>PiThemeService</code> (ver <code>apps/playground/src/app/app.ts</code>) basta con:</p>
|
||
<pre class="code">import { inject } from '@angular/core';
|
||
import { PiThemeService } from '@pi/ui';
|
||
|
||
const themeService = inject(PiThemeService);
|
||
themeService.setTheme('brutalist');</pre>
|
||
<p>Si el generador ya extendió <code>ThemeVariant</code>/<code>shellClassByVariant</code> (paso automático 4), no hace falta tocar nada más que este <code>setTheme</code>. Sin el servicio, el mecanismo de bajo nivel sigue funcionando igual:</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><mat-form-field appearance="outline"></code>
|
||
no es un <code>border</code> normal — es tres <code><div></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><html></code> (no al <code><body></code>, que es donde
|
||
vive <code>.pi-shell</code>) — hoy vía
|
||
<code>PiThemeService.setDarkMode()</code>/<code>toggleDarkMode()</code>,
|
||
que hace ese toggle por ti (persistido en <code>localStorage</code>) en
|
||
vez de que cada app repita el mismo <code>effect()</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>Se creó con <code>nx g @pi/ui:theme <nombre></code> — hace los tres puntos siguientes por ti.</li>
|
||
<li>El fichero nuevo vive en <code>themes/</code>, exporta un mixin y una clase <code>.pi-shell__<nombre></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>) y en <code>PiThemeService</code> (<code>ThemeVariant</code>/<code>shellClassByVariant</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>Si al tokenizar una superficie nueva tocaste un <code>.component.scss</code>, <code>nx run ui:lint-styles</code> no marca ese fichero (detecta <code>--pi-color-*</code> usado fuera de un fallback <code>--pi-theme-*</code>).</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>
|