Guía técnica y de uso
Un paquete Angular privado con design tokens, tema, iconos y componentes de marca. Se desarrolla una vez, se publica en el registro privado de GitLab y se consume desde cada proyecto del cliente — con una identidad visual idéntica y garantizada.
En una frase: es la fuente única de verdad de la interfaz del cliente. Cada botón, campo o color vive en un solo sitio versionado; los proyectos lo instalan como dependencia. Un cambio se hace una vez y se propaga a todos con una nueva versión.
Qué es y qué problema resuelve
El objetivo de negocio: que todos los proyectos del cliente se vean iguales, sin reinventar ni mantener la interfaz por separado en cada repositorio.
Arquitectura
La librería no es solo componentes: es un sistema en capas donde cada nivel se apoya en el anterior. Esta es la clave de que todo se vea coherente.
La base: la paleta, tipografía, espaciados, radios y sombras como design tokens. Un módulo TypeScript es la fuente de verdad de los valores; de ahí se derivan el preset de PrimeNG (que tinta todos sus componentes de azul de marca) y las utilidades de Tailwind. Ningún componente hardcodea un color.
La familia de iconos de marca (SVG propios, optimizados) se sirve inline con <bui-icon>. El trazo usa currentColor, así que cada icono hereda el color del texto que lo rodea. Un registro central los carga al arrancar; un proyecto puede añadir iconos propios sin tocar la librería.
Componentes de presentación (standalone, OnPush, signals) que envuelven PrimeNG tras una API de marca estable — o migran componentes propios. El equipo consume <bui-button> en vez de repetir la configuración cruda de PrimeNG por todos lados.
El motor del tema
Este es el corazón técnico: cómo un solo azul de marca (#024496) llega de forma consistente a PrimeNG, a Tailwind y a los componentes.
| Pieza | Rol |
|---|---|
tokens.ts | Fuente de verdad en TypeScript de los valores crudos (rampa de azul, neutros, estado). Con tests que verifican contraste AA. |
BrasiliaPreset | definePreset(Aura, …): sobrescribe el primitivo blue con la rampa de marca, de modo que la variable --p-primary-* de PrimeNG resuelve al azul Expreso. Ajusta botón y select. |
_tokens.scss | Replica los tokens como variables CSS (--color-*, --space-*, --radius-*) que consumen los componentes. |
@theme (Tailwind) | Puente que genera las utilidades oficiales (bg-primary, rounded-sm, text-body) desde esos mismos valores. |
| Orden de capas CSS | @layer theme, base, primeng, components, utilities: garantiza que los estilos del kit ganen a los de PrimeNG sin usar !important. |
Registrar el tema en una app es una sola llamada:
import { providePrimeNG } from 'primeng/config';
import { BrasiliaPreset, PRIMENG_ES } from '@aspsols/brasilia-ui';
providePrimeNG({
translation: PRIMENG_ES,
theme: { preset: BrasiliaPreset },
});
Catálogo
Todos son standalone: se importan y se usan. Cada uno trae su documentación viva en Storybook (con código copiable) y sus pruebas. Despliega cada uno para ver su API.
Envuelve p-button + BrasiliaPreset tras una API de variant, en vez de combinar severity/outlined/text a mano.
El icono crece con el botón. El lienzo va a 12, 14 y 16 según size, y iconPos decide el lado. El trazo se dibuja a 1,5 en los tres tamaños: el catálogo vive en una rejilla de 24, así que sin corregirlo el trazo se encogería con el lienzo —0,75 en el botón pequeño, la mitad de lo que pide el diseño— y el icono se vería desvaído al lado del texto.
<!-- variantes, icono del catálogo, estados -->
<bui-button label="Guardar" variant="primary" />
<bui-button label="Cancelar" variant="secondary" />
<bui-button label="Eliminar" variant="danger" [loading]="saving()" />
<bui-button label="Nueva solicitud" icon="plus" />
variant | primary · secondary · tertiary · danger · success |
size | small · normal · large |
icon / iconPos | Icono del catálogo a la izquierda o a la derecha; el lienzo (12/14/16) lo pone el size |
loading · disabled · fullWidth · type | Estados y tipo (submit) |
Campo completo: label + input + error/ayuda, con los id de accesibilidad enlazados. Implementa ControlValueAccessor, así que funciona con formControlName.
<bui-input
label="Email"
type="email"
formControlName="requesterEmail"
[error]="errorFor('requesterEmail')"
required />
label · required | Etiqueta y asterisco de requerido |
type | text · email · password · number · tel · url · search |
error · hint | Mensaje de error (marca inválido) o ayuda |
readonly · disabled | Estados |
uiDisabled | Bloqueo visual que no saca el control de la validación. Es la vía limpia con Reactive Forms: [disabled] junto a formControlName hace que Angular avise por consola |
Sobre p-select / p-multiSelect. Las variantes se controlan por input(): básico, filtrable, panel de tabla y multiselección. ControlValueAccessor + proyección de plantillas para filas a medida.
En móvil el panel se convierte solo en hoja inferior. Por debajo de 768px (BREAKPOINT.md) deja de colgar del disparador y sube desde el borde de abajo como modal a pantalla casi completa, con velo, encabezado y botón de cerrar; en multiselección añade un pie con «Aplicar». No hay nada que activar: es el comportamiento por defecto desde la v0.8.0.
La fila de persona la dibuja el kit. Con optionCaption el nombre gana una segunda línea pequeña debajo, showOptionAvatar le antepone el avatar de iniciales y optionStatus/optionStatusTone le cuelgan la píldora de estado a la derecha. Las tres piezas son independientes: la píldora es opcional y sin ella la fila se dibuja igual. Es el selector de conductor del diseño sin proyectar ninguna plantilla — #item queda para lo que el kit no cubre.
En multiselección, «Seleccionar todos» es una fila más. Va justo debajo del buscador, con su casilla a 16 y su rótulo a 48 como las opciones, y una línea que la separa de la lista; el toque lo atiende la fila entera, no solo la casilla. Abajo, el pie fijo con «Aplicar» —el botón grande del kit, con la flecha delante—, porque marcar casillas no cierra la hoja.
Con columnas, la hoja no cambia de ritmo. Cuando el panel declara headerLabel/metaLabel, la hoja mantiene la manija, la cabecera de 48 y el buscador sangrado a 16; lo que cambia es de ahí para abajo: una fila de rótulos de columna abre la lista y las filas suben de 44 a 52, que es el aire que el diseño le da a la tabla.
Con buscador, el panel abre con el cursor puesto en él. Desde la v0.10.1 no hace falta un clic más para escribir, ni en el panel de escritorio ni en la hoja de móvil, y el teclado sigue mandando en la lista: flechas para recorrerla, Enter para elegir y Escape para cerrar sin salir del buscador.
Buscar y paginar contra el servicio, desde la v0.14.0. Para los catálogos que no caben en el navegador —miles de filas detrás de un servicio paginado—, serverFilter apaga el acotado del panel: el buscador pasa a ser un aviso (searchChange) y la lista la manda el servicio. Filtrar además en local escondería resultados buenos, porque el servicio busca por unos campos y filterBy por otros. serverPaging avisa por loadMore cuando la lista se acaba —al llegar al final con el scroll, y también cuando la página que llegó no llena el panel— y el consumidor añade la siguiente a options; mientras la lista no crezca no se vuelve a pedir, así que no hay que avisar de que el catálogo se agotó. El rebote y el número de página se quedan en el consumidor, que es quien conoce la red: la historia «Búsqueda en servidor» de Storybook trae el patrón entero. Funciona igual en selección simple y en múltiple, y lo elegido no se pierde de vista aunque el servicio deje de traerlo en la página siguiente.
El calendario, en cambio, se abre centrado. Desde la v0.10.0 bui-datepicker ya no usa la hoja inferior: en móvil se abre como tarjeta centrada con velo, y se cierra al elegir el día o tocando fuera. Una hoja es el patrón de una lista larga que se desplaza; un calendario mide lo que mide, y a pantalla completa se le deformaban las celdas —el día elegido salía ovalado— y se le descolocaban las vistas de Mes y Año. Desde la v0.10.1 la tarjeta es la del marco de Figma pieza a pieza, y mide siempre lo mismo: no salta al cambiar de mes ni al pasar a las vistas de Mes y Año, y un mes de cinco semanas reparte el hueco entre las filas en vez de dejarlo debajo de la última.
<!-- panel de tabla: basta con optionMeta + rótulos -->
<bui-select
[options]="vehiculos()"
optionLabel="placa" optionMeta="numeroBus"
headerLabel="Placa" metaLabel="Bus"
[filter]="true" formControlName="placa" />
multiple | Simple o multiselección |
optionMeta + headerLabel/metaLabel | Panel de tabla de dos columnas, sin plantillas |
filter · filterBy | Buscador interno |
serverFilter + searchChange | La búsqueda la resuelve un servicio: el panel deja de acotar y solo avisa de lo tecleado |
serverPaging + loadMore | La lista llega por páginas: avisa al acabársele a quien la recorre |
label · required · hint | Etiquetado, igual que bui-input y bui-datepicker |
invalid · error · loading | Estados |
disabled · uiDisabled | Deshabilitar de verdad · bloqueo visual que no invalida el control |
optionCaption · showOptionAvatar | Fila de persona: avatar de iniciales y segunda línea bajo el nombre |
optionStatus · optionStatusTone | Píldora de estado a la derecha de la fila; opcional — sin optionStatus no se pinta |
showOptionRadio | Marca de radio al principio de cada fila (solo selección simple) |
plantillas #item/#selectedItem | Filas personalizadas proyectadas, para lo que el kit no cubre |
sheetTitle | Título de la hoja móvil; si se omite cae en label y luego en placeholder |
applyLabel · closeSheetLabel | Rótulos del pie y de la X de la hoja móvil |
Campo completo: label + calendario + error/ayuda, sobre p-datepicker con el español ya puesto (meses y días, «Hoy», semana desde el lunes). Implementa ControlValueAccessor. En móvil el calendario se abre como tarjeta centrada — ver <bui-select>.
Rango: dos campos que se dibujan igual. No hay modo rango ni valor doble; siguen siendo dos campos de fecha suelta, cada uno con el suyo. Lo que los une es que a los dos se les pasa el mismo par rangeStart/rangeEnd: con eso, el calendario marca los dos extremos y pinta el carril entre ellos. Con un solo extremo puesto —el caso de abrir «Fin» cuando solo hay inicio— ese día ya sale marcado y el calendario abre en su mes.
rangeStart/rangeEnd solo dibujan: no eligen ni limitan nada. Impedir un fin anterior al inicio se sigue haciendo con minDate, que es quien decide sobre la validez del dato.
<!-- rango: el mismo par a los dos campos -->
<bui-datepicker
label="Inicio" formControlName="inicio"
[rangeStart]="inicio()" [rangeEnd]="fin()" />
<bui-datepicker
label="Fin" formControlName="fin" [minDate]="inicio()"
[rangeStart]="inicio()" [rangeEnd]="fin()" />
label · required · hint | Etiquetado, igual que bui-input y bui-select |
minDate · maxDate | Límites reales de lo que se puede elegir |
rangeStart · rangeEnd | Extremos del rango a dibujar; el mismo par en los dos campos |
rangeStartLabel · rangeEndLabel | Texto de los extremos para lectores de pantalla |
error · hint | Mensaje de error (marca inválido) o ayuda |
readonly · disabled · uiDisabled | Estados |
Píldora de estado con 5 tonos semánticos (neutro, info, éxito, aviso, peligro). Cada proyecto la envuelve para mapear sus propios estados de dominio al tono correcto, sin duplicar el estilo.
<bui-status-pill label="Aprobado" tone="success" />
<bui-status-pill label="Pendiente" tone="warning" />
Renderiza inline un SVG del catálogo por su name. Hereda el color del texto (currentColor) y admite tamaños sm/md/lg o un valor CSS.
<bui-icon name="bus" size="lg" />
// icono propio de un proyecto, sin tocar la librería
iconRegistry.register('mi-icono', '<svg>…</svg>');
Fija por defecto lo que el design system da por supuesto y que en cada proyecto se repetía a mano: modal, no arrastrable, no redimensionable, cierre por Escape y por clic en el velo, y montado en el body. Un diálogo del kit no debería poder salir arrastrable por olvido.
El contenido va por proyección, no por plantillas de PrimeNG. Si no proyectas nada en [dialog-footer], la banda del pie no se dibuja.
Cabecera propia, desde la v0.11.0. header cubre el caso corriente —un título y la ×—, pero hay diálogos cuya cabecera es más que una línea de texto: una insignia, un subtítulo, un estado. Para esos, [dialog-header] recibe el bloque entero y lo coloca en la banda de cabecera, donde la × sigue siendo la del catálogo. Es una alternativa a header, no un añadido. Existe porque los proyectos consumidores estaban apagando showHeader y volviendo a p-dialog a pelo, y con ello renunciaban al resto: ancho, velo, Escape y foco atrapado.
Sin banda de cabecera, desde la v0.12.0. [showHeader]="false" apaga la banda entera, para el diálogo que sencillamente no tiene cabecera: un aviso donde la insignia, el titular y el texto van compuestos como un bloque. No es la vía por defecto —con cabecera, header o [dialog-header] son mejores porque conservan la × del kit—, pero sin ella la única salida era volver a p-dialog a pelo y perder el velo, el Escape, el ancho del sistema y el foco atrapado.
<bui-dialog [(visible)]="abierto" header="Confirmar asignación">
<p>¿Seguro que quieres asignar este conductor?</p>
<div dialog-footer>
<bui-button variant="secondary" label="Cancelar" (onClick)="abierto.set(false)" />
<bui-button label="Confirmar" (onClick)="confirmar()" />
</div>
</bui-dialog>
<!-- bloqueante: sin ×, sin Escape y sin cierre por velo -->
<bui-dialog [(visible)]="abierto" header="Eliminar registro" blocking />
<!-- cabecera propia: la × la sigue poniendo el kit -->
<bui-dialog [(visible)]="abierto" width="520px">
<header dialog-header>
<bui-icon name="building" />
<h2>Nuevo centro de operaciones</h2>
<p>Agrupa agencias bajo un mismo centro</p>
</header>
</bui-dialog>
Se monta una sola vez en la raíz de la aplicación; los avisos se lanzan desde BuiToastService, que habla de tonos y no de las severidades de PrimeNG. El servicio sube al kit porque cada proyecto acababa escribiendo el mismo envoltorio.
El tono no tiñe el fondo: la superficie es siempre blanca y el color lo aporta solo el icono. Es lo correcto para algo que cae sobre contenido cualquiera — un fondo teñido a pantalla completa compite con lo que hay debajo.
// una vez, en el arranque
bootstrapApplication(App, { providers: [provideBuiToast()] });
<!-- una vez, en la raíz -->
<bui-toast position="top-right" />
// desde cualquier componente
private readonly toast = inject(BuiToastService);
this.toast.success('Asignación guardada');
this.toast.danger('No se pudo guardar', 'Revisa la conexión');
Son cuatro tonos, no los cinco del sistema: neutral significa «sin estado» y no tiene lectura posible en un aviso.
Es la directiva pTooltip de PrimeNG compuesta como host directive: el comportamiento (posicionamiento, retardos, cierre con Escape, accesibilidad) es el de PrimeNG, ya probado, y el kit solo aporta el nombre y la piel. Se expone con nombre propio por lo mismo que el resto: un proyecto consumidor no debería tener que importar de primeng/* para usar el kit.
<bui-icon name="pencil" buiTooltip="Editar" tooltipPosition="top" />
<!-- dentro de una tabla o un panel con overflow -->
<button buiTooltip="Ver historial" appendTo="body">…</button>
Los tres cumplen el mismo contrato que bui-input (label, required, hint, error, disabled, uiDisabled…), fijado en un spec compartido desde el primer día — que es justo lo que no se hizo con input, select y datepicker y por lo que acabaron derivando.
Cuándo cada uno: el interruptor aplica el cambio al instante («Notificaciones activas»); la casilla marca una intención que se confirma al enviar el formulario. Si hay un botón Guardar de por medio, es casilla. El radio no tiene sentido suelto: todas las opciones del grupo comparten name y el mismo formControlName.
<bui-switch formControlName="notificaciones" label="Notificaciones por correo" />
<!-- el intermedio es «seleccionar todo» con solo algunos hijos marcados -->
<bui-checkbox label="Todas las agencias" [indeterminate]="algunas()" />
<bui-radio formControlName="turno" name="turno" value="dia" label="Diurno" />
<bui-radio formControlName="turno" name="turno" value="noche" label="Nocturno" />
<!-- `contained`: el control en una superficie propia, para listas de ajustes -->
<bui-switch contained label="Alertas de relevo" />
bui-textarea es bui-input con altura: mismo label, mismo asterisco de requerido, mismo bloque de error/ayuda y el mismo contrato, así que un formulario mezcla los dos sin cambiar de vocabulario.
bui-search-input no es un componente de raíz: es la composición de bui-input + prefijo de lupa + botón de limpiar. Existe como pieza propia porque esa composición se repetía en cada pantalla con buscador, cada una con un icono y un aria-label distintos. Queda fuera del contrato de campos: filtra una lista, no aporta un valor a un formulario.
<bui-textarea formControlName="observaciones" label="Observaciones"
[rows]="3" hint="Máximo 240 caracteres." [maxlength]="240" />
<bui-search-input placeholder="Buscar agencia" (searchChange)="filtrar($event)" />
Control segmentado: un carril redondeado con las opciones dentro y la activa rellena en el azul de marca. No envuelve p-tabs: las pestañas de PrimeNG son de subrayado y traen la gestión de paneles; reproducir el diseño encima habría sido anular su chrome entero para volver a dibujarlo, heredando además un modelo de paneles que aquí no se quiere — la barra solo cambia un valor y el contenido lo pone quien la usa.
La accesibilidad sí es la del patrón de pestañas: role="tablist", aria-selected, una única parada de tabulación y navegación con flechas, Inicio y Fin.
Ancho. El carril se ciñe a las pestañas que hay: con dos opciones no sobra fondo a la derecha, ni siquiera dentro de una columna flex. fullWidth invierte eso cuando se quiere a propósito —la barra ocupa el contenedor y las pestañas se reparten el ancho a partes iguales, que es el control segmentado de cabecera de formulario—, y scrollable saca dos flechados para recorrer una lista que no cabe. Los flechados solo aparecen si hacen falta y se apagan en cada extremo; no entran en la tabulación, porque el teclado ya recorre la barra con las flechas y el foco arrastra la pestaña a la vista.
<bui-tabs [tabs]="pestanas" [(active)]="seleccionada" ariaLabel="Tipo de cambio" />
<!-- control segmentado que encabeza un formulario: dos opciones a mitad y mitad -->
<bui-tabs fullWidth [tabs]="acceso" [(active)]="modo" ariaLabel="Acceso" />
<!-- lista larga: flechados de anterior y siguiente -->
<bui-tabs scrollable [tabs]="muchas" [(active)]="seleccionada" ariaLabel="Sección" />
// cada pestaña admite contador o icono
pestanas: BuiTab[] = [
{ value: 'viaje', label: 'Cambio de viaje', badge: 3 },
{ value: 'pasajero', label: 'Cambio de pasajero' },
{ value: 'abierto', label: 'Viaje abierto', disabled: true },
];
Cinco piezas pequeñas sin PrimeNG detrás, solo CSS sobre tokens. Existen porque estaban definidas a mano decenas de veces por proyecto, cada una con su propio gris.
bui-empty-state con tone, desde la v0.11.0. Sin tono, el disco se queda en el azul de marca: es lo correcto para un vacío normal, donde no ha pasado nada malo. Con tone="danger" cambia de familia, para que la lista que no cargó no se lea igual que la que todavía no tiene nada — si se ven iguales, el usuario no sabe si volver a intentarlo. Admite los cinco tonos del sistema y el glifo usa el rol dot, el mismo que el punto de la píldora.
bui-avatar con size, desde la v0.13.0. Antes medía 32px fijos y no había forma de cambiarlo, lo que lo dejaba inservible para casi cualquier pantalla real: en un proyecto consumidor había ocho medidas distintas entre 22 y 46px y solo dos coincidían. Acepta sm/md/lg (24/32/40) o un valor CSS suelto, igual que bui-icon; md es lo que medía antes, así que quien no lo pase no ve ningún cambio. La letra sale de la caja —la mitad de su lado—, porque a 24px las dos iniciales no caben con los 16 fijos de antes. fontSize permite fijarla, pero es un andamio para adoptar el componente sin salto visual, no una opción de diseño: la proporción letra/caja es del sistema. Si hace falta en una pantalla nueva, lo que está mal es esa pantalla.
bui-chip frente a bui-status-pill: la diferencia no es el aspecto, es si se puede pulsar. El chip es un filtro — se renderiza como <button> con aria-pressed, así que trae teclado y anillo de foco. La píldora es presentación pura, para un estado que solo se lee.
<bui-chip label="Disponibles" [active]="filtro() === 'disponibles'" (toggle)="filtrar('disponibles')" />
<!-- la caja del contador crece con la cifra: 3, 12, 99+ -->
<bui-badge [value]="sinLeer()" [max]="99" />
<!-- deriva las iniciales del nombre, ignorando partículas (de, del, la) -->
<bui-avatar name="María del Carmen Rodríguez" />
<bui-avatar name="María del Carmen Rodríguez" size="lg" />
<bui-divider />
<bui-empty-state icon="inbox" title="Sin resultados"
description="No se encontraron elementos con los filtros actuales.">
<bui-button empty-action label="Limpiar filtros" (onClick)="limpiar()" />
</bui-empty-state>
<!-- el hueco que NO se esperaba: la carga que falló -->
<bui-empty-state tone="danger" icon="alert" title="No se pudo cargar la lista"
description="Ocurrió un error al consultar el servidor.">
<bui-button empty-action label="Reintentar" (onClick)="reintentar()" />
</bui-empty-state>
El catálogo incluye además bui-card (superficie, ahora con estados selectable/selected para listas elegibles), bui-expandable-card (plegable con línea de acento), bui-datepicker (fecha en español, que en móvil se abre como modal centrado) y bui-page-subheader (cabecera de página). 22 componentes y 3 directivas, 258 pruebas en verde.
Para el equipo
Tras autenticar el scope @aspsols contra el registro de GitLab (una vez por máquina — ver Primera vez: configurar el acceso más abajo):
pnpm add @aspsols/brasilia-ui
En el styles.scss de la app. Incluye Tailwind, los tokens, el puente @theme y el chrome de los overlays:
@use '@aspsols/brasilia-ui/styles';
En app.config.ts (ver el bloque de «El motor del tema» arriba).
Se importan como cualquier standalone y se usan en la plantilla:
import { ButtonComponent } from '@aspsols/brasilia-ui';
// …
<bui-button label="Enviar" type="submit" [loading]="saving()" />
Para el equipo · primera vez
El paquete no vive en el npm público, sino en el registro privado de GitLab (proyecto 1666). Si al instalar te sale ERR_PNPM_FETCH_404 · registry.npmjs.org/@aspsols…: Not Found o No authorization header was set, es porque todavía no configuraste el acceso. Sigue estos 5 pasos en orden — se hacen una sola vez por máquina.
La idea, en dos archivos. El acceso se reparte en dos archivos .npmrc distintos, y esto es la clave para que funcione:
① El del proyecto (en la carpeta del frontend) → solo dice dónde buscar el paquete. Se puede subir al repo.
② El de tu usuario (en tu carpeta personal) → guarda tu token. Es personal y nunca se sube.
⚠️ Importante (pnpm v11): el token no puede ir en el .npmrc del proyecto — las versiones nuevas de pnpm lo ignoran por seguridad (para que no se filtre al subirlo al repo). Por eso va en el archivo de tu usuario. Si lo pones en el del proyecto verás el aviso Ignored project-level auth setting… environment variables are not expanded y no autenticará.
Cada persona genera su propio token — es personal y no se comparte. Abre este enlace (te lleva directo a la página de tokens):
https://git.aspsols.com/-/user_settings/personal_access_tokens
npm-brasilia-ui (o el nombre que quieras).read_api. Con eso basta para instalar. (Si aparece read_package_registry aparte, márcalo también.)⚠️ GitLab te muestra el token una sola vez. Cópialo apenas aparezca (empieza por glpat-…). Si cierras la página sin copiarlo, no lo vuelves a ver y toca crear otro. Guárdalo un momento en el bloc de notas: lo usarás en el paso 3.
.npmrc del proyecto (solo el registro)Este archivo dice «el scope @aspsols se descarga de GitLab». Abre la terminal en la raíz de tu frontend (donde está el package.json) y corre una línea según tu sistema. Crea un archivo .npmrc con una sola línea:
Windows · PowerShell
'@aspsols:registry=https://git.aspsols.com/api/v4/projects/1666/packages/npm/' | Out-File -FilePath .npmrc -Encoding utf8
WSL / Linux / Mac
echo '@aspsols:registry=https://git.aspsols.com/api/v4/projects/1666/packages/npm/' > .npmrc
Este archivo es seguro de subir al repositorio (no contiene tu token). De hecho, lo ideal es que quede versionado para que todo el equipo lo tenga sin repetir este paso.
.npmrc de tu usuarioAquí va tu token, en el .npmrc de tu carpeta personal (no la del proyecto). Reemplaza glpat-TU_TOKEN_AQUI por el token real del paso 1 y corre la línea de tu sistema:
Windows · PowerShell
Add-Content -Path "$HOME\.npmrc" -Value "//git.aspsols.com/api/v4/projects/1666/packages/npm/:_authToken=glpat-TU_TOKEN_AQUI"
WSL / Linux / Mac
echo '//git.aspsols.com/api/v4/projects/1666/packages/npm/:_authToken=glpat-TU_TOKEN_AQUI' >> ~/.npmrc
WSL tiene su propia carpeta personal, separada de Windows. Si vas a correr pnpm dentro de WSL, el token debe estar en el ~/.npmrc de WSL (con el comando de Linux de arriba), no en el de Windows. Si trabajas en los dos, ponlo en ambos.
No uses pnpm config get con el token: da el error option is protected a propósito (los tokens no se pueden leer así). En su lugar, abre el archivo y comprueba que aparece la línea con tu glpat-…:
Windows · PowerShell
Get-Content "$HOME\.npmrc"
WSL / Linux / Mac
cat ~/.npmrc
Si ves la línea //git.aspsols.com/...:_authToken=glpat-…, el token está bien guardado. Si no aparece o el archivo está vacío, repite el paso 3.
Parado en la raíz del proyecto:
pnpm install
Ahora pnpm encuentra @aspsols/brasilia-ui en GitLab (paso 2) y se autentica con tu token (paso 3). Debe terminar en Done, sin 404 ni 401. ✅
Usa pnpm install, no npm install — este proyecto usa pnpm. La versión actual es pnpm add @aspsols/brasilia-ui@0.14.0.
Ojo con el caret mientras el kit siga en 0.x. pnpm lo escribe en el package.json como "^0.10.0", y en 0.x el caret no cruza de minor: recoge los parches (0.10.1, 0.10.2…) con un pnpm install, pero ^0.10.0 nunca subirá solo a 0.11.0. Cada minor nuevo obliga a repetir el pnpm add @aspsols/brasilia-ui@<versión> a mano en cada app.
| Mensaje que ves | Qué pasa | Solución |
|---|---|---|
404 Not Found · registry.npmjs.org/@aspsols… | Falta el .npmrc del proyecto, o pnpm no lo está leyendo. | Repite el paso 2 parado en la raíz del proyecto (donde está el package.json). |
| Ignored project-level auth setting · environment variables are not expanded | Pusiste el token en el .npmrc del proyecto; pnpm v11 lo ignora por seguridad. | Borra la línea del _authToken del .npmrc del proyecto (deja solo la del registro) y ponlo en el de tu usuario: paso 3. |
| 401 · No authorization header was set | Falta el token en el .npmrc de tu usuario, o está mal escrito. | Revisa con el paso 4 que la línea del token esté; si no, repite el paso 3. |
| 403 Forbidden | El token no tiene permiso de lectura del registro. | Crea uno nuevo con el scope read_api marcado (paso 1). |
| option is protected, can not be retrieved | Es normal: los tokens no se leen con config get. | No es un error. Verifica con Get-Content / cat (paso 4). |
| No aparece «Access Tokens» en GitLab | El admin deshabilitó los tokens personales. | Pide un Deploy Token del proyecto 1666 como alternativa. |
Remove-Item: no se puede quitar… el directorio no está vacío / ruta muy larga | El proyecto está en OneDrive: rutas de node_modules que superan el límite de 260 caracteres de Windows. | Mueve el proyecto fuera de OneDrive (ej. C:\Projects\…). Para borrar node_modules: mkdir vacia; robocopy vacia node_modules /MIR; rmdir vacia,node_modules. |
No pongas el proyecto dentro de OneDrive. Node genera miles de archivos en node_modules; OneDrive los sincroniza (lento, con conflictos) y alarga las rutas hasta romper el límite de Windows. Trabaja desde una ruta corta como C:\Projects\mi-proyecto.
Decisiones técnicas
Las preguntas que un revisor técnico hará, respondidas.
Componentes envoltura (wrappers), no forks de PrimeNG. Exponemos variant="primary" en vez de la mezcla cruda de PrimeNG. Ventaja: una API de marca estable que aísla a las apps de los cambios internos de PrimeNG, y un punto único donde encapsular variantes, estados y accesibilidad.
Estilos encapsulados salvo los overlays. Los estilos de cada componente van encapsulados (no se escapan). La excepción son los paneles que PrimeNG monta en <body> (appendTo="body", para que no los recorte un contenedor): esos deben ser globales y viajan en la hoja de estilos del kit. Es un híbrido deliberado, no una fuga.
Angular y PrimeNG como peerDependencies. La librería no empaqueta Angular ni PrimeNG: los declara como «pares». La app consumidora manda la versión; así no hay duplicados ni conflictos de versión.
Ingeniería y calidad
| Herramienta | Rol |
|---|---|
| Angular 21 | Framework de las apps del cliente; componentes standalone, OnPush y signals. |
| PrimeNG 21 + @primeuix/themes | Componentes ricos ya resueltos y el motor de tema (preset). |
| Tailwind v4 | Utilidades de layout y espaciado, atadas a los tokens. |
| Storybook 10 | Documentación viva: cada componente con sus estados y código copiable. |
| Vitest | Pruebas unitarias; un componente no se da por terminado sin ellas en verde. |
| ng-packagr | Empaqueta la librería en el formato estándar (FESM + tipos) que se publica. |
| pnpm · Node 22 | Gestor y runtime, fijados para builds reproducibles. |
Definición de hecho por componente: API con input()/output(), standalone + OnPush, cero valores hardcodeados (todo por token), story con estados y spec en verde.
Distribución
El componente se construye y aprueba en Storybook, con pruebas.
ng-packagr genera el paquete en dist/.
Se sube el número (SemVer) y se publica al GitLab Registry.
Cada proyecto actualiza a la nueva versión cuando le conviene.
El versionado semántico (mayor.menor.parche) le dice a cada proyecto si una actualización trae cambios que rompen (mayor), funciones nuevas (menor) o solo correcciones (parche). Nadie se actualiza a la fuerza.
Adopción en producción
La versión 0.1.0 está publicada en el GitLab Registry y es el primer proyecto que la instala como dependencia real — no un enlace local, sino el paquete descargado del registro.
1664)Relevobui-button y bui-status-pillbui-button en la app, en una veintena de pantallasbui-button reemplaza a p-button en botones de acción y diálogos de toda la app.bui-status-pill: el chip de estado propio ahora envuelve el del kit, con los 5 tonos semánticos unificados..npmrc con token de GitLab.PRIMENG_ES local).p-button crudos que aún quedan en algunas plantillas.input, select, card, datepicker y subheader.Estado del proyecto
Workspace, librería, azul de marca y tokens, Storybook funcionando.
36 iconos de marca como bui-icon, con galería y pruebas.
7 componentes migrados (botón, input, select, chip, tarjeta, datepicker, subheader), 55 pruebas en verde.
v0.4.0 publicada en el Registry. Relevo Tarjetón consume el paquete con el tema aplicado: 106 usos de bui-icon, 53 de bui-button, 23 de bui-select.
El preset de PrimeNG anclado a los tokens, los 5 tonos resueltos a su familia, contrato único para los campos de formulario y una regla de lint que impide volver a meter color a mano.
18 piezas nuevas: diálogo, toast, tooltip, pestañas, casilla, radio, interruptor, área de texto, buscador, estado vacío, chip de filtro, badge, avatar y divisor. 121 pruebas en verde.
La librería de Figma se genera ahora desde el código, con los 130 tokens como variables enlazadas — antes cada capa llevaba el color escrito a mano y el sistema no podía propagarse. Pendiente: incorporar las correcciones al archivo del equipo de diseño.
Par etiqueta/valor, indicador de carga, diálogo de confirmación, tabla con paginación y popover.