@telcomdev/ui
v0.1.40
Published
Librería UI reutilizable para los sistemas TelcomDev.
Maintainers
Readme
Telcomdev UI
Theming
La librería usa tokens CSS --td-* con fallbacks internos. Si no importas
ningún tema, los componentes conservan su apariencia por defecto.
Para usar los tokens oficiales:
@import '@telcomdev/ui/lib/theme/td-theme.css';Puedes sobrescribirlos desde tu aplicación sin cambiar APIs:
:root {
--td-font-family: var(--app-font-family, Inter, system-ui, sans-serif);
--td-color-primary: #3157a4;
--td-color-primary-hover: #254680;
--td-color-primary-soft: #eaf0fb;
--td-color-surface: #ffffff;
--td-color-surface-soft: #f6f8fb;
--td-color-surface-muted: #edf1f6;
--td-color-border: #dde3ee;
--td-color-border-strong: #c8d1df;
--td-color-text: #202b3d;
--td-color-text-muted: #66758b;
--td-color-danger: #b93a3a;
--td-color-success: #16784a;
--td-focus-ring: 0 0 0 3px color-mix(in srgb, var(--td-color-primary) 18%, transparent);
--td-radius-md: 0.75rem;
--td-shadow-md: 0 18px 46px rgba(32, 43, 61, 0.14);
}Si tu aplicación ya usa Angular Material con system tokens, puedes mapearlos automáticamente importando el bridge opcional:
@import '@telcomdev/ui/lib/theme/td-material-theme.css';Ese archivo conecta --td-* con tokens como --mat-sys-primary,
--mat-sys-surface, --mat-sys-on-surface, --mat-sys-outline y
--mat-sys-error. No reemplaza TdThemeService; solo permite convivir con un
tema externo.
Input outline
<td-input
label="Nombre completo"
icon="person"
[clearable]="true"
[(ngModel)]="nombre"
/>TdInput es compatible con ngModel y formularios reactivos. La etiqueta y
el borde outline permanecen visibles en todos los estados.
Selects
<td-select
label="Estado"
[options]="estados"
[(ngModel)]="estado"
/>
<td-autocomplete-select
label="Ciudad"
[options]="ciudades"
[(ngModel)]="ciudad"
/>Ambos controles son compatibles con formularios Angular y muestran su panel mediante Angular CDK Overlay.
td-autocomplete-select soporta dos formas de trabajo:
Modo estándar
Usa TdSelectOption<T> cuando ya tienes opciones normalizadas:
readonly proveedores = [
{ value: 1, label: 'AMERICA MOVIL PERU S.A.C.', description: '20467534026' },
];
proveedorId: number | null = 1;<td-autocomplete-select
label="Proveedor"
[options]="proveedores"
[(ngModel)]="proveedorId"
/>Modo mapeado
Usa valueKey, labelKey y, opcionalmente, descriptionKey, iconKey o
disabledKey cuando quieres consumir DTOs reales sin mapearlos manualmente:
readonly proveedores = [
{
proveedorId: 1,
tipoDocumentoIdentidadId: 4,
numeroDocumento: '20467534026',
nombre: 'AMERICA MOVIL PERU S.A.C.',
},
];
proveedorId: number | null = 1;<td-autocomplete-select
label="Proveedor"
[options]="proveedores"
valueKey="proveedorId"
labelKey="nombre"
descriptionKey="numeroDocumento"
[(ngModel)]="proveedorId"
/>Si el valor llega antes que las opciones, o las opciones llegan antes que el
valor, el texto visible se sincroniza automáticamente. Esta sincronización no
emite selectionChange; ese evento se reserva para selección o limpieza
realizada por el usuario.
El comportamiento predeterminado durante el scroll es reposition, por lo que
el panel permanece unido al control. Puede cambiarse con
scrollBehavior="close".
File Upload
Selector de archivos con soporte para formularios Angular, drag and drop, validación de formatos y previsualización de imágenes/PDF mediante el visor de archivos de la librería.
Variante input
<td-file-upload
label="Logo del proveedor"
variant="input"
accept="image/*"
[maxSizeMb]="2"
[(ngModel)]="logo"
/>logo: File | null = null;Variante dropzone con preview
<td-file-upload
label="Adjuntos del expediente"
description="Arrastra contrato, evidencia o documentos."
accept="image/*,.pdf,.xml,.xlsx,.zip"
[multiple]="true"
[maxFiles]="5"
[maxSizeMb]="8"
[(ngModel)]="adjuntos"
(filesChange)="actualizarArchivos($event)"
(rejected)="mostrarRechazos($event)"
/>adjuntos: File[] = [];Cuando multiple está desactivado el valor del formulario es File | null.
Cuando multiple está activado el valor es File[].
Badge
Indicadores compactos para estados, conteos y etiquetas de producto.
<td-badge tone="success" icon="check">Activo</td-badge>
<td-badge tone="warning" icon="alert">Pendiente</td-badge>
<td-badge tone="danger" variant="solid">Vencido</td-badge>
<td-badge tone="danger" dot aria-label="Tiene alertas" />Soporta tone, variant, size, icon, dot y pill.
Tabs
<td-tabs [(selectedIndex)]="indice">
<td-tab label="General" icon="settings">
<app-general />
</td-tab>
<td-tab label="Usuarios" icon="people" [badge]="12">
<app-usuarios />
</td-tab>
</td-tabs>Incluye las variantes linea, pastilla y contenida, además de navegación
con flechas, Home y End.
Para usar componentes basados en Overlay, como TdDialog, agrega el estilo
estructural del CDK en angular.json:
"styles": [
"node_modules/@angular/cdk/overlay-prebuilt.css",
"src/styles.scss"
]Los diálogos pueden abrir cualquier componente standalone. El contenido decide libremente sus acciones:
<td-dialog-actions>
<button type="button" [tdDialogClose]="false">Cancelar</button>
<button type="button" (click)="guardarBorrador()">Borrador</button>
<button type="submit" tdDialogPrimary>Guardar</button>
</td-dialog-actions>Iconos
import { TdIcon, TdIconoRegistry } from '@telcomdev/ui';
@Component({
imports: [TdIcon],
})
export class App {
constructor(registry: TdIconoRegistry) {
registry.registrar({
estrella: 'M12 2 15 9 22 9 17 14 19 22 12 17 5 22 7 14 2 9 9 9Z',
});
}
}<td-icon nombre="security" tamano="24px" />
<td-icon nombre="estrella" titulo="Favorito" />El icono hereda el color CSS mediante currentColor. Si el nombre no existe, se muestra
folder.
Data table
TdDataTable usa @angular/cdk/scrolling para virtualizar las filas. La aplicación
consumidora mantiene la responsabilidad de consultar, paginar y modificar datos.
import {
TdDataTable,
TdDataTableAccion,
TdDataTableAccionEvento,
TdDataTableBoton,
TdDataTableColumna,
} from '@telcomdev/ui';
@Component({
imports: [TdDataTable],
})
export class Establecimientos {
columnas: TdDataTableColumna<Establecimiento>[] = [
{ clave: 'nombre', encabezado: 'Nombre', ancho: '1fr' },
{ clave: 'codigoSunat', encabezado: 'Código Sunat', ancho: '8rem' },
{ clave: 'direccion', encabezado: 'Dirección', ancho: '1.5fr' },
{ clave: 'esPrincipal', encabezado: 'Principal', tipo: 'booleano' },
{ clave: 'activo', encabezado: 'Estado', tipo: 'estado' },
];
acciones: TdDataTableAccion<Establecimiento>[] = [
{ id: 'editar', etiqueta: 'Editar', icono: 'edit' },
{ id: 'eliminar', etiqueta: 'Eliminar', icono: 'delete', tono: 'peligro' },
];
botones: TdDataTableBoton[] = [
{ id: 'agregar', etiqueta: 'Agregar establecimiento', icono: 'add', tono: 'primario' },
{ id: 'pdf', etiqueta: 'Exportar PDF', icono: 'pdf', tono: 'peligro' },
{ id: 'excel', etiqueta: 'Exportar Excel', icono: 'description', tono: 'exito' },
];
ejecutar(evento: TdDataTableAccionEvento<Establecimiento>) {
// evento.accion.id, evento.fila y evento.indice
}
ejecutarBoton(boton: TdDataTableBoton) {
if (boton.id === 'agregar') this.abrirModal(0);
if (boton.id === 'pdf') this.exportarPdf();
}
}<td-data-table
campoId="establecimientoId"
[datos]="lista()"
[columnas]="columnas"
[acciones]="acciones"
[botones]="botones"
[total]="totalEstablecimientos()"
[cargando]="cargando()"
[filasAntesDeCargar]="5"
[puedeCargarMas]="paginaActual() < totalPaginas()"
(busquedaChange)="buscar($event)"
(accion)="ejecutar($event)"
(botonClick)="ejecutarBoton($event)"
(cargarMas)="paginaSiguiente()"
>
<select tdDataTableFilters [(ngModel)]="estado">
<option value="">Todos los estados</option>
<option value="activo">Activos</option>
<option value="inactivo">Inactivos</option>
</select>
<input tdDataTableFilters type="date" [(ngModel)]="fecha" />
<button tdDataTableActions type="button">Acción personalizada</button>
</td-data-table>Incluye búsqueda con debounce, scroll virtual, carga incremental, chips de estado,
acciones condicionales, tema oscuro, estado vacío y skeleton de carga. Los elementos
marcados con tdDataTableFilters se colocan junto al buscador. Los elementos
tdDataTableActions aparecen junto al contador y al botón Agregar.
Header
import { TdHeader } from '@telcomdev/ui';
@Component({
imports: [TdHeader],
})
export class App {
sidebarAbierto = true;
}<td-header
[menuAbierto]="sidebarAbierto"
[oscuro]="oscuro"
usuario="Andrea Torres"
cargo="Administradora"
[notificaciones]="notificaciones"
(menuToggle)="sidebarAbierto = !sidebarAbierto"
(notificacionSeleccionada)="abrirNotificacion($event)"
(verTodasNotificaciones)="verNotificaciones()"
(accionPerfil)="ejecutarAccionPerfil($event)"
>
<button tdHeaderActions type="button">Acción adicional</button>
</td-header>El header incluye dropdowns funcionales de notificaciones y perfil, avatar por iniciales,
modo responsive y tema oscuro. Las acciones tdHeaderActions aparecen a la derecha.
Footer
import { TdFooter } from '@telcomdev/ui';
@Component({
imports: [TdFooter],
})
export class App {}<td-footer
empresa="TelcomDev"
texto="Todos los derechos reservados."
version="1.0.0"
[oscuro]="oscuro"
>
<a tdFooterLinks href="/privacidad">Privacidad</a>
<span tdFooterActions>Angular 21</span>
</td-footer>El footer calcula el año actual por defecto y admite el modo compacto.
Sidebar
TdSidebar es standalone y no depende de Angular Material ni Tailwind.
import { MenuSidebar, TdSidebar } from '@telcomdev/ui';
@Component({
imports: [TdSidebar],
})
export class App {
abierto = true;
oscuro = false;
menu: MenuSidebar[] = [
{
nombre: 'Inicio',
ruta: '/',
icono: 'home',
orden: 1,
subMenus: [],
},
];
}<td-sidebar
titulo="Gestión Corporativa"
subtitulo="Central Platform"
[menu]="menu"
[(abierto)]="abierto"
modoCerrado="oculto"
[oscuro]="oscuro"
[cargando]="false"
error=""
(seleccionar)="onSeleccionar($event)"
/>El botón para abrir o cerrar debe vivir en el header de la aplicación:
<button type="button" (click)="abierto = !abierto">
<td-icon [nombre]="abierto ? 'close' : 'menu'" />
</button>modoCerrado acepta compacto para conservar una barra de iconos u oculto para cerrar
completamente el sidebar.
La aplicación consumidora obtiene el menú desde su propia API. El componente ordena cada
nivel por orden, usa folder cuando no recibe icono y renderiza los submenús
recursivamente.
Iconos incluidos: home, dashboard, folder, settings, people, person,
security, inventory, shopping_cart, receipt, assessment, description y
business.
Signal Forms
TdInput, TdSelect, TdAutocompleteSelect y TdDatePicker implementan
FormValueControl y conservan soporte para ngModel y Reactive Forms.
Cuando existe [formField], el field pasa a ser la fuente principal del valor y
del estado. Los controles reciben automáticamente errors, invalid,
required, readonly, disabled, min, max, minLength, maxLength y
pattern cuando corresponda. Las propiedades tradicionales (value,
valueChange, error, disabled, etc.) continúan disponibles para no exigir
una migración completa.
TdInput acepta fields string, number y sus variantes anulables. Un valor
null se presenta como un input vacío. Con type="number" y Signal Forms, el
control entrega number | null, evitando números almacenados como texto.
La política de vaciado se resuelve automáticamente y puede fijarse cuando sea necesario:
<td-input type="number" [formField]="fields.cantidad" />
<td-input [formField]="fields.correo" emptyValue="null" />emptyValue acepta auto (predeterminado), null y empty-string. En modo
tradicional se conserva el comportamiento histórico: type="number" continúa
emitiendo texto mediante CVA para no romper formularios existentes.
Cuando el control recibe maxlength —de forma directa o mediante la metadata
maxLength de Signal Forms— muestra automáticamente un contador actual/max.
Puede ocultarse sin quitar la validación:
<td-input [maxlength]="100" [(ngModel)]="nombre" />
<td-input [formField]="fields.descripcion" [showCounter]="false" />minlength, min, max y pattern no activan el contador.
Checkbox
TdCheckbox implementa el contrato FormCheckboxControl de Angular y mantiene
compatibilidad con ControlValueAccessor. Su valor siempre es booleano:
<td-checkbox
label="Establecimiento principal"
description="Se utilizará como establecimiento predeterminado."
[formField]="establecimientoForm.esPrincipal"
/>También funciona con formularios tradicionales:
<td-checkbox label="Activo" [(ngModel)]="activo" />
<td-checkbox label="Principal" [formControl]="controlPrincipal" />Radio Button
TdRadioGroup permite selección única tipada y mantiene compatibilidad con
Signal Forms, ngModel y Reactive Forms.
<td-radio-group
label="Tipo de documento"
[options]="tiposDocumento"
[(ngModel)]="tipoDocumento"
/>readonly tiposDocumento = [
{ value: 'dni', label: 'DNI' },
{ value: 'ruc', label: 'RUC' },
{ value: 'ce', label: 'Carné de extranjería' },
];
tipoDocumento: 'dni' | 'ruc' | 'ce' | null = 'ruc';También cuenta con variante tipo tarjeta:
<td-radio-group
label="Canal de notificación"
variant="card"
direction="horizontal"
[options]="canales"
[formField]="fields.canal"
/>Con Signal Forms no se necesita una variable intermedia ni eventos manuales. El
modelo debe inicializar el campo como true o false, nunca como null.
import { signal } from '@angular/core';
import { form, FormField } from '@angular/forms/signals';
readonly model = signal({
nombre: '',
periodo: null as TdDatePickerValue,
});
readonly fields = form(this.model);Agrega FormField a los imports del componente. La librería no registra un
segundo selector [formField], evitando conflictos con la directiva oficial y
con la transformación especial del compilador de Angular.
<td-input [formField]="fields.nombre" />
<td-date-picker mode="rango" [formField]="fields.periodo" />El uso clásico sigue siendo válido:
<td-input [(ngModel)]="nombre" />
<td-select [formControl]="form.controls.estado" [options]="estados" />Date Picker
<td-date-picker
label="Fecha de emisión"
min="2026-01-01"
max="2027-12-31"
[(ngModel)]="fecha"
/>
<td-date-picker
label="Periodo"
mode="rango"
[(ngModel)]="rango"
/>Las fechas se almacenan como YYYY-MM-DD. En modo rango el valor tiene la forma
{ inicio: string, fin: string | null }.
Build
ng build telcomdev-ui