Librería de componentes moderna y elegante para Angular. Diseñada para ofrecer una experiencia premium, cada componente ha sido cuidadosamente elaborado con Material Design 3, micro-animaciones fluidas y un sistema de temas dinámico.
Instala el paquete principal y sus dependencias de diseño:
npm install nebula-ui-kit
Nota: Se recomienda el uso de Angular v21 para aprovechar al máximo el sistema de Signals y Model inputs.
import { ButtonComponent, InputComponent } from 'nebula-ui-kit';
@Component({
standalone: true,
imports: [ButtonComponent, InputComponent],
template: `
<lib-input label="Username" placeholder="Enter username..."></lib-input>
<lib-button (clicked)="onSubmit()">Submit</lib-button>
`
})
export class MyComponent {}
Incluye el tema en tu styles.scss y la fuente Open Sans en tu index.html:
@use "nebula-ui-kit/styles/theme";
Nebula permite centralizar textos y comportamientos comunes mediante el token NEBULA_UI_CONFIG.
Provee el token en tu app.config.ts:
import { ApplicationConfig } from '@angular/core';
import { NEBULA_UI_CONFIG } from 'nebula-ui-kit';
export const appConfig: ApplicationConfig = {
providers: [
{
provide: NEBULA_UI_CONFIG,
useValue: {
requiredMessage: '¡Este campo es obligatorio!',
selectPlaceholder: 'Seleccione una opción',
datepickerLabel: 'Fecha de nacimiento',
datepickerPlaceholder: 'DD/MM/AAAA',
tableEmptyDash: 'N/A',
locale: 'es-CO',
getConnectionStatus: () => !!localStorage.getItem('user_data'),
alertDurations: {
success: 3000,
warning: 4000,
error: 5000,
info: 0 // Persistente
},
navStorageKey: 'my_custom_nav_v1', // Opcional: Nombre personalizado en localStorage
encryptionKey: 'nebula-secret-key-2026' // Opcional: Habilita cifrado XOR
}
}
]
};
Nebula proporciona dos formas de registrar iconos. Se recomienda usar el set Core para aplicaciones de negocio.
Registra automáticamente el set esencial de Nébula (Layout, ERP, Finanzas):
import { provideNebulaCoreIcons } from 'nebula-ui-kit';
// app.config.ts
providers: [
provideNebulaCoreIcons(),
]
Para añadir iconos específicos de Lucide que no están en el set core:
import { provideNebulaIcons } from 'nebula-ui-kit';
import { Truck, Ship } from 'lucide-angular/icons';
// app.config.ts
providers: [
provideNebulaIcons({ Truck, Ship }),
]
Para formateo manual de datos, puede inyectar el FormatService que incluye utilidades para moneda, números y fechas estandarizadas:
import { FormatService } from 'nebula-ui-kit';
export class MyComponent {
private _format = inject(FormatService);
// Formateo de moneda (COP por defecto)
price = this._format.formatValue(1500, { key: 'p', label: 'P', pipe: 'currency' });
// Formateo de fecha estandarizado
today = this._format.formatDate(new Date(), true); // con hora
}
El catálogo de componentes vive en el Manual de Diseño 3.0, generado desde el código de la versión publicada en Nexus. Aquí no se repite para que no se desactualice.
- Inventario completo (qué existe, si tiene historia, si está deprecado y qué piezas visuales viven todavía en el ERP): http://10.120.0.2:8552/propuesto/dist/apis/inventario-componentes.html
- Componentes en vivo (Storybook, perfil 3.0): http://10.120.0.2:8552/storybook/
- Contrato de cada componente (entradas, salidas y métodos, con su captura): http://10.120.0.2:8552/propuesto/dist/apis/componentes.html
- Deprecado: qué no usar: http://10.120.0.2:8552/propuesto/dist/reglas/deprecado.html
Entrada en la wiki: Manual de Diseño 3.0.
Muchos componentes exponen el input ariaLabel para casos donde no hay texto visible (ej. botones de icono):
<lib-button icon="Search" ariaLabel="Search records" />
<lib-icon name="Info" ariaLabel="More information about the process" />
Siguiendo los estándares de la arquitectura Nébula, todas las clases de estado y estilos base deben declararse en el decorador @Component para mantener el template limpio y desacoplado:
@Component({
selector: 'lib-custom-el',
host: {
'class': 'lib-custom-el',
'[class.is-active]': 'active()',
'[attr.aria-disabled]': 'disabled()',
'[attr.aria-label]': 'ariaLabel() || null'
}
})
export class CustomComponent {
active = input(false);
disabled = input(false);
ariaLabel = input<string>('');
}
Siempre proporciona un ariaLabel para elementos interactivos que carecen de etiquetas de texto visibles.
<lib-button
variant="tonal"
icon="Trash2"
ariaLabel="Delete selected record"
></lib-button>
<!-- Los iconos decorativos están automáticamente ocultos de los lectores de pantalla (aria-hidden="true") -->
<lib-icon name="Smile"></lib-icon>
Usa skeletons como pares de tus componentes reales:
<div class="field-container">
@if (isLoading()) {
<lib-skeleton variant="rectangle" height="56px" borderRadius="12px"></lib-skeleton>
} @else {
<lib-input label="Full Name" [(value)]="fullName"></lib-input>
}
</div>
El GridCardComponent ya incluye soporte interno para skeletons mediante el input loading:
<lib-gridcard
[items]="myData()"
[loading]="isDataLoading()"
></lib-gridcard>
lib-gridcard aplica una lógica visual estricta para asegurar la legibilidad de métricas críticas cuando type === 'scorecard':
{
title: 'Ventas Útiles',
value: '84%',
type: 'scorecard',
statusColor: 'success',
iconPosition: 'right'
}
Para asegurar la paridad visual absoluta, algunos componentes como lib-card gestionan su propio estado de carga internamente:
<lib-card
title="Project Alpha"
subtitle="Active"
[loading]="isCardLoading()"
></lib-card>
Para implementar búsquedas en tiempo real, combina computed() con inputs de búsqueda. Este patrón asegura que la UI se actualice automáticamente sin necesidad de ciclos de vida manuales:
export class UserListComponent {
users = signal<User[]>([]);
query = signal('');
// El estado derivado se recalcula automáticamente cuando query() o users() cambian
filteredUsers = computed(() => {
const term = this.query().toLowerCase();
return this.users().filter(u =>
u.name.toLowerCase().includes(term) ||
u.email.toLowerCase().includes(term)
);
});
}
<lib-search [(searchTerm)]="query" placeholder="Filter users..."></lib-search>
<lib-table [data]="filteredUsers()" [config]="tableConfig"></lib-table>
El componente lib-table utiliza renderizado fixed y el truncamiento de textos largos text-overflow: ellipsis. Para gestionar que columnas "delgadas" debes configurar explícitamente la propiedad width en las columnas predecibles dentro de tu ColumnConfig. Las columnas que no declaren explícitamente un ancho se repartirán uniformemente el espacio restante.
const columns: ColumnConfig<UserData>[] = [
{ key: 'id', label: 'ID', width: '80px', align: 'center' },
{ key: 'status', label: 'Estado', width: '120px' },
{ key: 'email', label: 'Correo', width: '35%' },
{ key: 'name', label: 'Nombre Completo' },
];
El TableComponent utiliza un model para el término de búsqueda, lo que facilita el filtrado reactivo bidireccional:
<lib-input
label="Search records"
icon="Search"
[(value)]="tableSearch"
></lib-input>
<lib-table
[data]="users()"
[config]="tableConfig"
[(searchTerm)]="tableSearch"
></lib-table>
El SearchComponent utiliza el model de Angular para el enlace bidireccional y proporciona un mecanismo de debounce incorporado. También incluye una propiedad loading para mostrar un icono giratorio durante las operaciones asíncronas.
<lib-search
[(searchTerm)]="query"
[debounceMs]="300"
[loading]="isTableLoading()"
(searchChange)="onSearch($event)"
placeholder="Search users..."
></lib-search>
El SidebarComponent es totalmente reactivo y gestiona su estado de expansión a través del input collapsed y el output collapsedChange, lo que permite a los componentes padres sincronizar el estado fácilmente.
<!-- app.component.html -->
<lib-sidebar
[menuItems]="navItems()"
[(collapsed)]="sidebarCollapsed"
></lib-sidebar>
<lib-header
[user]="'Example User'"
[isDarkMode]="isDark()"
(darkModeToggle)="toggleTheme()"
(search)="onSearch($event)"
></lib-header>
<lib-breadcrumb
[items]="[
{ label: 'Finance', route: '/finance' },
{ label: 'Accounts Payable', route: '/finance/ap' },
{ label: 'Create Order' }
]"
></lib-breadcrumb>
Nebula facilita que abra "espacios de trabajo" en pestañas mediante la combinación del SidebarComponent, TabsComponent y el NebulaNavigationService.
@Component({
template: `
<lib-sidebar
[menuItems]="menu"
[activeItemIds]="navService.openedRoutes()"
(itemClick)="navService.addTabFromMenu($event)"
/>
<main>
<lib-tabs
[tabs]="navService.tabs()"
[activeId]="navService.activeTabId()"
(tabChange)="navService.selectTab($event)"
(onClose)="navService.closeTab($event)"
/>
<router-outlet />
</main>
`
})
export class AppComponent {
navService = inject(NebulaNavigationService);
}
El FooterComponent soporta diferentes variantes de diseño y un indicador de estado de conexión dinámico, centralizado a través del NEBULA_UI_CONFIG o pasado como inputs.
<lib-footer
variant="default"
[status]="'Online'"
[date]="today"
></lib-footer>
lib-alert está deprecado por completo desde nebula-ui-kit 2.4.0 (Patrón de Notificaciones §3.1): el aviso dentro del formulario está prohibido. En su lugar: NebulaToastService para el resultado de una acción, DialogService.confirm(...) para lo crítico o irreversible, estado de error en el cuerpo de la pantalla para lo que impide seguir, y el error del propio campo para lo mal diligenciado. Ver Patrón de Notificaciones y http://10.120.0.2:8552/propuesto/dist/reglas/deprecado.html.
El componente TabsComponent ahora soporta una API declarativa con Lazy Loading. El contenido envuelto en libTabContent solo se instanciará cuando la pestaña esté activa.
export class MainContainer {
activeWorkspace = signal<string | number>('orders');
onTabClose(id: string | number) {
// Lógica para cerrar pestaña
}
onSave() {
console.log('Action confirmed from inside the tab!');
}
}
<lib-tabs [(activeId)]="activeWorkspace" (onClose)="onTabClose($event)">
<!-- Pestaña declarativa con carga diferida -->
<lib-tab
id="orders"
title="Payment Orders"
[closable]="true"
(onConfirm)="onSave()">
<ng-template libTabContent>
<lib-order-form /> <!-- Solo se carga cuando está activo -->
</ng-template>
</lib-tab>
<lib-tab id="invoices" title="Invoices" route="/finance/invoices">
<ng-template libTabContent>
<lib-invoice-list />
</ng-template>
</lib-tab>
</lib-tabs>
El DialogService permite abrir componentes como modales. Para diálogos de confirmación rápidos (estilo alerta), use el ConfirmDialogComponent.
El DialogService proporciona un método directo para confirmaciones rápidas que devuelve un Observable<boolean>:
this.dialogService.confirm({
title: 'Delete invoice?',
message: 'This action cannot be undone.',
variant: 'error',
confirmLabel: 'Delete',
cancelLabel: 'Cancel'
}).subscribe(confirmed => {
if (confirmed) {
console.log('Delete confirmed');
}
});
Utiliza las directivas estructurales para mantener la consistencia:
<div libDialogHeader>
<h3>New Record</h3>
</div>
<div libDialogBody>
<lib-input label="Full Name" />
</div>
<lib-dialog-footer [multiStep]="isStep2" (backAction)="goToStep1()">
<lib-button variant="secondary" (clicked)="close()">Cancel</lib-button>
<lib-button (clicked)="save()">{{ isStep2 ? 'Finish' : 'Next' }}</lib-button>
</lib-dialog-footer>
El FileUploadComponent combina un botón de selección nativo con una zona de drag & drop. Emite filesSelected con los archivos válidos y fileSizeError cuando se supera el límite configurado.
<lib-file-upload
label="Attach payment support"
hint="Only XLSX or CSV files, maximum 5 MB."
accept=".xlsx,.csv"
[multiple]="true"
[maxSizeBytes]="5 * 1024 * 1024"
(filesSelected)="onFiles($event)"
(fileSizeError)="showError($event)"
></lib-file-upload>
export class PaymentFormComponent {
onFiles(files: UploadedFile[]) {
console.log('Valid files:', files.map(f => f.name));
}
showError(msg: string) {
console.error(msg); // "archivo.xlsx" supera el tamaño máximo de 5 MB.
}
}
Siempre utiliza una propiedad única (como id) en la expresión track de tus bucles @for para elevar el rendimiento de renderizado y mantener el estado del DOM (foco, animaciones, scroll).
<!-- RECOMENDADO: Usar propiedades únicas sustentables -->
@for (item of data(); track item.id ?? $index) {
<div class="list-item">{{ item.name }}</div>
}
Colores, tipografía, radios, medidas y temas los fija el perfil visual del kit, no cada aplicación. El vigente es el perfil 3.0 (clase
html.ui-3-0, parámetro de empresaCOMPANY_UI_PROFILE = UI_3.0), con su juego claro y oscuro. Tokens, valores y cómo se activa: http://10.120.0.2:8552/propuesto/dist/apis/perfil.html · qué cambió frente al 1.6, valor por valor: http://10.120.0.2:8552/propuesto/dist/inicio/de-1-6-a-3-0.html.La tipografía es Open Sans; no se redefine en
:root.
Nebula recomienda un modelo donde el Sidebar abre Pestañas (Tabs) y las rutas profundas están protegidas por el estado de navegación en localStorage.
Provee el NebulaNavigationService en el root para que el estado persista entre cambios de módulo.
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
NebulaNavigationService,
// ...
]
};
nebulaTabGuardAsegura que los usuarios no puedan acceder a URLs profundas si la pestaña no está abierta.
// app.routes.ts
import { nebulaTabGuard } from 'nebula-ui-kit';
import { authGuard } from './core/guards/auth.guard'; // Ejemplo de guard de autenticación
export const routes: Routes = [
{
path: 'accounting',
// IMPORTANTE: Primero autenticar, luego validar si la pestaña está abierta
canActivate: [authGuard, nebulaTabGuard],
children: [
{ path: 'ledger', component: LedgerComponent }
]
}
];
Los componentes lib-sidebar y lib-tabs detectan automáticamente el NebulaNavigationService para sincronizar su estado sin necesidad de inputs manuales.
@Component({
template: `
<!-- Sidebar: detecta rutas abiertas y gestiona clics -->
<lib-sidebar [menuItems]="activeMenu()" />
<main>
<!-- Tabs: sincronizadas con el servicio y el router -->
<lib-tabs />
<router-outlet />
</main>
`
})
export class MainLayout {
// Opcional: Sidebar y Tabs ya se sincronizan automáticamente, inyécta si necesitas control manual
private navService = inject(NebulaNavigationService);
}
Copyright © 2026 Equipo de Nébula. Todos los derechos reservados.