@chrisdesign95/nova-iris-ui
Librería de componentes Angular 21 (standalone, Signals, OnPush y compatible con zoneless) con tokens CSS --nv-* en Light y Dark. Los componentes siguen el diseño de Figma component-ui.
Versión
0.x: la API puede cambiar entre versiones menores.
Versión local: 0.9.0 (v0.9.0). El flujo de publicación compila antes de ejecutar las pruebas para resolver los entry points desde un dist actualizado.
Instalación
npm install @chrisdesign95/nova-iris-ui
Peer dependencies: @angular/core, @angular/common y @angular/forms ^21.2.0.
Estilos y tema
Carga los tokens una sola vez en los estilos globales de la aplicación (angular.json):
"styles": ["node_modules/@chrisdesign95/nova-iris-ui/styles/tokens.css"]
Light es el tema por defecto. Para usar Dark, pon data-theme="dark" en <html>; también funcionan temas anidados en cualquier elemento. La librería no incluye ningún servicio de tema ni detecta prefers-color-scheme.
- Light: colores y medidas verificados en Figma
component-ui, con Primary Violet. - Dark: propuesta propia de NOVA IRIS UI; no existe en Figma.
Componentes
Cada componente es un secondary entry point, así que solo se incluye lo que importas.
Data Table: <nv-data-table> desde @chrisdesign95/nova-iris-ui/data-table: dataSource, columns y actions (con onClick(row)), búsqueda, orden y paginación local. Demo y API en /components/data-table del Playground. Desde 0.12.0, proyecta botones y conteos con [nvDataTableToolActions] antes de refrescar; también funciona con showSearch y showRefresh en false. La aplicación controla eventos y estados de esos controles.
| Selector | Import | Descripción |
|---|---|---|
<nv-button> | @chrisdesign95/nova-iris-ui/button | Botón nativo: variantes primary, secondary, outline, ghost y destructive; tamaños sm, md y lg; contenido label, icon-label e icon-only; estados disabled y loading. |
<nv-input> | @chrisdesign95/nova-iris-ui/input | Campo de texto con etiqueta, ayuda y error accesibles. Compatible con Reactive Forms, ngModel y [(value)]. |
<nv-tag> | @chrisdesign95/nova-iris-ui/tag | Etiqueta outline o soft, con 5 severidades. Informativa; con interactive se convierte en botón. |
<nv-badge> | @chrisdesign95/nova-iris-ui/badge | Indicador solid: contador (count) o punto (dot). |
<nv-chip> | @chrisdesign95/nova-iris-ui/chip | Botón de alternancia con aria-pressed y [(selected)]. |
<nv-avatar> | @chrisdesign95/nova-iris-ui/avatar | Iniciales o icono, con estado de presencia. |
<nv-textarea> | @chrisdesign95/nova-iris-ui/textarea | Área de texto con etiqueta, ayuda, error y contador n/max. Compatible con Reactive Forms, ngModel y [(value)]. |
<nv-checkbox> | @chrisdesign95/nova-iris-ui/checkbox | Checkbox nativo con estado indeterminado, etiqueta, descripción y Forms/CVA. |
<nv-radio> | @chrisdesign95/nova-iris-ui/radio | Radio nativo con selección por grupo, navegación por teclado y Forms/CVA. |
<nv-switch> | @chrisdesign95/nova-iris-ui/switch | Alternancia booleana con semántica switch, etiqueta, descripción y Forms/CVA. |
<nv-select> | @chrisdesign95/nova-iris-ui/select | Selección única con búsqueda opcional, teclado, Forms/CVA y [(value)]. |
<nv-multi-select> | @chrisdesign95/nova-iris-ui/multi-select | Selección múltiple con búsqueda, Forms/CVA y Tags; maxVisibleTags de 1 a 5 (predeterminado 2), con +N para excedentes. |
<nv-date-picker> | @chrisdesign95/nova-iris-ui/date-picker | Selector de fecha single, multiple o range, con Forms/CVA, límites, localización, teclado y calendarios de mes/año. |
import { Component } from '@angular/core';
import { NvButton } from '@chrisdesign95/nova-iris-ui/button';
@Component({
selector: 'app-demo',
imports: [NvButton],
template: `<nv-button variant="primary" size="md" (click)="save()">Guardar</nv-button>`,
})
export class Demo {
save() {}
}
Todos los componentes públicos usan el prefijo nv- y los tokens el prefijo --nv-.
Iconos
Los iconos son SVG integrados (Font Awesome Free 7.3.1 mediante @fortawesome/angular-fontawesome); no hay que registrar un resolver ni cargar CSS. Con npm 7+ los peers @fortawesome/angular-fontawesome y @fortawesome/fontawesome-svg-core se instalan solos (si no, instálalos):
<nv-button icon="plus" iconPosition="left">Crear</nv-button>
<nv-button content="icon-only" icon="view" ariaLabel="Ver detalle" />
Con icon, el botón muestra icono y texto; iconPosition es left (por defecto) o right. content="icon-only" exige ariaLabel o ariaLabelledby. Nombres incluidos: plus, save, search, filter, refresh, empty, view, edit, trash, email, user, calendar, error, info, success, warning y arrow-right. Para otro icono, regístralo una vez con el registro oficial de Font Awesome:
import { inject } from '@angular/core';
import { FaIconLibrary } from '@fortawesome/angular-fontawesome';
import { faMugHot } from '@fortawesome/free-solid-svg-icons';
// En el constructor de tu componente raíz:
inject(FaIconLibrary).addIcons(faMugHot);
provideNvIcons (resolver de clases CSS) sigue disponible solo por compatibilidad. Los iconos Free tienen licencia CC BY 4.0 y el código MIT; consulta los LICENSE de los paquetes instalados.
Notas y límites conocidos
- Los componentes con elemento nativo interno (
nv-button,nv-input,nv-chip) reenvían al nativo los atributos de formulario y ARIA mediante inputs; el host<nv-*>no equivale a un<button>o<input>HTML. - Un
[attr.aria-label]escrito en el host no llega al elemento nativo: usa el inputariaLabel. - La fuente Inter no se distribuye; sin ella se usa
system-ui, sans-serif. - El anillo de foco (2px, color primary) no procede de Figma: es una decisión de diseño de NOVA IRIS UI.
- Avatar, Icon y Remove del Chip quedan pendientes.
- Los días y controles de cabecera del Date Picker usan radio circular por una desviación aprobada de Figma; la actualización del diseño está pendiente.
Licencia
UNLICENSED: uso personal del autor, todos los derechos reservados. No se concede permiso de uso, copia ni redistribución. Ver LICENSE.
Componentes nuevos en 0.10.0
/tabs: NvTabs, NvTabItem. items, variant (underline/segmented), size, content, ariaLabel y [(activeId)]; changed emite el id. Configure id para identificadores estables. El consumidor conecta tabpanel con panelId(item.id) y tabId(item.id). Flechas/Home/End seleccionan saltando deshabilitados./message: NvMessage. severity neutral/info/success/warning/danger; variant soft/outline; title, description, actionLabel, dismissible, ariaLive, [(open)], action y dismissed./toast: NvToastService y NvToastViewport. show({title, description?, severity?, action?: {label, onClick}, duration?, dismissible?, showIcon?}) devuelve id; dismiss(id), clear(). Un viewport por aplicación: position top-end/top-start/bottom-end/bottom-start, maxVisible=4. FIFO, 5000 ms desde visibilidad, 0 manual; pausa independiente en hover y foco. Tokens: --nv-toast-width (360px), --nv-toast-stack-gap (8px), --nv-toast-z-index (1000)./stat-card: NvStatCard. label libre y value (texto o número), detail, comparison, showComparison, statusLabel libre, severity neutral/info/success/warning/danger, statusIcon opcional, showStatus y headingLevel (1–6). Reutiliza nv-tag y sus colores existentes. Migración: sustituir nature por statusLabel y severity explícitos.
Demos: npm run playground, http://127.0.0.1:4200/components/{select,tabs,message,toast,stat-card}. Las demos ofrecen Light/Dark (Toast usa el selector global).
Sidebar (0.13.0)
Entry point: @chrisdesign95/nova-iris-ui/sidebar (NvSidebar, NvSidebarItem,
NvSidebarUser, NvSidebarUserAction y NV_SIDEBAR_DEFAULT_USER_ACTIONS).
Demo: /components/sidebar, con controles, Light/Dark, código copiable, tablas de API
y registro de eventos.
import { NvSidebar, type NvSidebarItem } from '@chrisdesign95/nova-iris-ui/sidebar';
const items: readonly NvSidebarItem[] = [
{ code: 'home', name: 'Inicio', route: '/', icon: 'home' },
{
code: 'security',
name: 'Seguridad',
icon: 'key-round',
children: [{ code: 'users', name: 'Usuarios', route: '/users' }],
},
];
<nv-sidebar
title="Nova Iris"
subtitle="Administración"
[items]="items"
[(collapsed)]="collapsed"
[(activeCode)]="activeCode"
[searchable]="true"
[user]="user"
(itemSelect)="navigate($event)"
(userMenuAction)="handleUserAction($event)"
/>
collapsed alterna entre 260 px y un rail de 64 px. activeCode identifica la hoja activa;
itemSelect entrega el objeto original. La aplicación decide cómo navegar: el componente
no usa Router y la ruta de los grupos se ignora. logo admite una URL de imagen.
El árbol de GET /account/menu es compatible sin transformación. Los iconos fa- se buscan
sin ese prefijo únicamente en FaIconLibrary, donde la aplicación registra su pack;
los nombres desconocidos no renderizan un icono. El resolver heredado recibe el nombre intacto.
El catálogo integrado añade home, layout-grid, bell, key-round, settings, log-out,
chevron-up y chevron-down, sin necesidad de registrar el pack para esos nombres semánticos.
searchable vale false por defecto. La búsqueda ignora tildes y conserva ancestros;
se vacía al cambiar collapsed. groupsCollapsed controla todos los grupos; con null
respeta item.collapsed y, si falta, abre los ancestros del activo y cierra los demás.
ariaLabel y searchPlaceholder permiten adaptar los textos accesibles. [(query)] expone el texto de la búsqueda interna.
Los badges solo se muestran en hojas de nivel 0 expandidas; los niveles desde el segundo
usan la presentación de nivel 1, sin iconos. No se emite evento al abrir grupos.
user es opcional y contiene name, email, initials y status. userMenu sustituye las
acciones predeterminadas «Mi perfil» y «Cerrar sesión»; userMenuAction entrega la elegida.
El rail abre flyouts a la derecha y el menú de usuario se alinea por abajo con el avatar.
Los menús admiten flechas, Home/End, Enter/Espacio, Escape y Tab, con foco restaurado.
Los estados Light/Dark usan los mismos tokens --nv-*; el bloque nuevo de foundations
aporta las dimensiones del sidebar, los indicadores, la tipografía xs y el z-index.
Diferencias conocidas aceptadas respecto a Figma: nv-avatar conserva su fondo al abrir
el menú y las iniciales usan foreground en lugar de secondary-foreground. La cabecera
mide 48 px, con padding solo horizontal de 12 px y contenido centrado, porque el padding
vertical de Figma no cabe con las dos líneas. Los popovers no tienen sombra.
La revisión visual final en el navegador corresponde al Tech Lead.
Desviaciones adicionales aprobadas por el Tech Lead (Figma pendiente de actualizar):
- Corrección del ajuste de radios: el contenedor
.nv-sidebar(expandido y rail) y los contenedores.menu(flyout y usuario) usan el token existente--nv-radius-lg(18 px, Figma radius/2xl), frente a los 12/10 px originales de Figma. No se crea token. Se restauran los radios de los ítems del Anexo A: nivel 0 y grupos r8 (--nv-radius-compact), subítems r10 (--nv-radius-md), rail r18 (--nv-radius-button), flyout y acciones de usuario r8. El bloque de usuario conserva r10 (avatar en rail circular). No se aplica full a los ítems. El popover conserva padding 4 y borde 1; sus ítems r8 caben dentro de la curva r18 sin aumentar el padding ni cambiar el alto calculado. - Las filas usan
--nv-sidebar-item-height: var(--nv-button-height-lg)(40 px), también en el cálculo de posición y alto del popover. Cabecera, búsqueda, logo y avatar no crecen. - Padres y subítems tienen
padding: 0 var(--nv-space-3)(12 px desde el borde de su propia píldora);.tree-item--nested .row { padding: 0; }evita sumar padding..childrenusapadding: var(--nv-space-1) 0 0 var(--nv-space-2)(4 arriba, 8 de sangría exterior). Niveles posteriores mantienen el estilo de nivel 1 sin acumular sangría. Rail y filas de popover también usan padding horizontal de 12 px. - Se mantienen 4 px entre grupo y primer hijo, entre hijos y entre título/filas del flyout.
La barra sigue recta, 3 × 20 px: offsets de sidebar y flyout referencian
--nv-space-1(x=4), ytop: calc((var(--nv-sidebar-item-height) - var(--nv-sidebar-indicator-height)) / 2)(y=10). El badge conserva centrado porcentual. - El shell usa
[searchable]="true"ysearchPlaceholder="Buscar componentes..."dentro denv-sidebar, incluido el drawer móvil, sin input externo. La consulta interna alimenta «Enlaces directos» y «Sin resultados»; conserva normalización sin tildes y ancestros, y permite coincidencias de grupos. Al colapsar se limpia el filtro y no hay búsqueda en rail. El shell anterior no implementaba un atajo de teclado de búsqueda.