npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aseansc-admin/ui

v0.6.0

Published

ASC Internal UI Component Library — Angular 20 + PrimeNG 20 + TailwindCSS 4

Readme

@aseansc-admin/ui

ASC Internal UI Component Library — Angular 20 · PrimeNG 20 · TailwindCSS 4


Migration — v0.5.x → v0.6.0

Breaking: Tailwind spacing scale — base unit đổi từ 0.25rem sang 1px. Tất cả numeric spacing class cần nhân ×4:

p-4  → p-16    gap-2 → gap-8    px-6 → px-24
w-12 → w-48    h-6   → h-24     mt-1 → mt-4

Chạy lệnh migrate tự động trong project:

# Ví dụ sed (Linux/macOS) — backup trước khi chạy
find src -name "*.ts" -o -name "*.html" | xargs sed -i -E \
  's/\b(p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr|gap|w|h)-([1-9][0-9]*)\b/\1-$((\2*4))/g'

Hoặc xem CHANGELOG để biết chi tiết.


Installation

npm install @aseansc-admin/ui

Peer dependencies

npm install @angular/core@^20 @angular/cdk@^20

Setup

1. app.config.ts

import { ApplicationConfig, provideZoneChangeDetection,
         provideBrowserGlobalErrorListeners }  from '@angular/core';
import { provideRouter }                        from '@angular/router';
import { provideHttpClient }                    from '@angular/common/http';
import { provideAnimations }                    from '@angular/platform-browser/animations';
import { provideAscUI }                         from '@aseansc-admin/ui';
import { appRoutes }                            from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideAscUI({
      theme:  { darkMode: false },
      locale: 'vi-VN',           // 'vi-VN' | 'en-US'
      table:  { rows: 20 },
    }),
    provideAnimations(),
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(appRoutes),
    provideHttpClient(),
  ],
};

2. styles.scss

@use "@aseansc-admin/ui/tokens/preset" as *;
@use 'prismjs/themes/prism-okaidia.css';
@use "tailwindcss";

3. postcss.config.json (workspace root)

{
  "plugins": {
    "@tailwindcss/postcss": {}
  }
}

4. Root component template

<asc-toast />
<asc-confirm-dialog />
<router-outlet />

Components

Layout — AscLayout

import { AscLayout } from '@aseansc-admin/ui';
<asc-layout [menuItems]="menuItems" [topBarLogo]="logoTpl" [breadCrumb]="breadcrumbTpl">
  <router-outlet />
</asc-layout>

<ng-template #breadcrumbTpl>
  <asc-breadcrumb />
</ng-template>

Toggle dark mode via the sun/moon button in the topbar — adds/removes .app-dark on <html>.


Breadcrumb — AscBreadcrumb

import { AscBreadcrumb } from '@aseansc-admin/ui';

Auto-router mode — khai báo data.breadcrumb trong route config, không cần truyền gì thêm:

// app.routes.ts
{
  path: 'form',
  data: { breadcrumb: 'Form' },
  children: [
    { path: 'input', data: { breadcrumb: 'Input' }, loadComponent: ... },
  ],
}
<asc-breadcrumb />

Manual mode — truyền items thủ công:

<asc-breadcrumb
  [items]="[{ label: 'Admin', routerLink: '/admin' }, { label: 'Users' }]"
  [home]="{ icon: 'pi pi-home', routerLink: '/' }"
/>

<!-- Ẩn home icon -->
<asc-breadcrumb [items]="items" [home]="null" />

Form

Button — AscButtonComponent

<asc-button label="Lưu"   icon="pi pi-save"  (clicked)="onSave()" />
<asc-button label="Huỷ"   variant="outlined" severity="secondary" />
<asc-button label="Xoá"   severity="danger"  [loading]="loading" />

Input — AscInputComponent

<asc-form-field label="Email">
  <asc-input formControlName="email" placeholder="Nhập email..." />
</asc-form-field>

Textarea — AscTextareaComponent

<asc-textarea formControlName="note" [rows]="4" [maxLength]="500" [showCounter]="true" />

Input Number — AscInputNumberComponent

<asc-input-number formControlName="amount" mode="currency" currency="VND" />

Password — AscPasswordComponent

<asc-password formControlName="password" [feedback]="true" />

Select — AscSelectComponent

<asc-select formControlName="status" [options]="statusOptions" optionLabel="label" optionValue="value" />

Datepicker — AscDatepickerComponent

<asc-datepicker formControlName="date" dateFormat="dd/MM/yyyy" />

Checkbox — AscCheckboxComponent

<asc-checkbox formControlName="agree" label="Tôi đồng ý với điều khoản" />

Radio — AscRadioComponent

<asc-radio formControlName="gender" [options]="genderOptions" optionLabel="label" optionValue="value" />

Upload — AscUpload

<!-- Image (default) -->
<asc-upload formControlName="avatar" uploadMode="img" [maxSizePerFile]="2000000">
  <div empty>Kéo thả ảnh vào đây</div>
</asc-upload>

<!-- Excel / CSV -->
<asc-upload formControlName="sheet" uploadMode="excelFile" />

<!-- Word / Text -->
<asc-upload formControlName="doc" uploadMode="docFile" />

<!-- PDF only -->
<asc-upload formControlName="report" uploadMode="pdfFile" />

<!-- All document types (.pdf .doc .docx .xls .xlsx .csv .txt) -->
<asc-upload formControlName="attachment" uploadMode="file" />

<!-- Any file -->
<asc-upload formControlName="misc" uploadMode="all" />

| uploadMode | Accept | |---------------|-----------------------------------------| | img | image/* | | pdfFile | .pdf | | docFile | .doc, .docx, .txt | | excelFile | .xls, .xlsx, .csv | | file | .pdf, .doc, .docx, .xls, .xlsx, .csv, .txt | | all | */* |


Data

Table — AscTableComponent

<asc-table [data]="rows" [columns]="cols" [loading]="loading" (lazyLoad)="onLoad($event)">
  <!-- type='custom': dùng [ascTableCell]="key" để render tự do -->
  <ng-template [ascTableCell]="'status'" let-value let-row="row">
    <p-tag [value]="value" />
  </ng-template>
</asc-table>

Column types:

columns: AscTableColumn[] = [
  { field: 'name',      header: 'Tên',       type: 'avatar' },
  { field: 'email',     header: 'Email',     type: 'text' },
  { field: 'active',    header: 'Hoạt động', type: 'boolean' },
  { field: 'joinDate',  header: 'Ngày vào',  type: 'date' },
  { field: 'lastSeen',  header: 'Lần cuối',  type: 'datetime' },
  // Number — thousandSep + locale kiểm soát ký tự phân cách
  { field: 'score',  header: 'Điểm',  type: 'number',
    format: { thousandSep: true } },               // 8.000 (vi-VN mặc định)
  { field: 'score',  header: 'Điểm',  type: 'number',
    format: { thousandSep: true, locale: 'en-US' } }, // 8,000
  { field: 'score',  header: 'Điểm',  type: 'number',
    format: { thousandSep: false } },              // 8000
  // Currency
  { field: 'salary', header: 'Lương', type: 'currency',
    format: { locale: 'vi-VN' } },
  // Badge — map value → label + severity
  { field: 'status', header: 'Trạng thái', type: 'badge',
    badgeMap: {
      active:   { label: 'Hoạt động', severity: 'success' },
      inactive: { label: 'Tạm dừng',  severity: 'warn' },
    },
  },
  // Custom template
  { field: 'progress', header: 'Tiến độ', type: 'custom', templateKey: 'progress' },
];

Picklist — AscPicklistComponent

<asc-picklist [(source)]="available" [(target)]="selected"
              optionLabel="name" sourceHeader="Nguồn" targetHeader="Đã chọn" />

Overlay

Toast & Confirm (programmatic)

import { AscToastService, AscConfirmService } from '@aseansc-admin/ui';

// Toast
this.toast.success('Lưu thành công');
this.toast.error('Có lỗi xảy ra');

// Confirm dialog — preset
this.confirm.delete('Xoá bản ghi này?', () => this.delete(id));
this.confirm.save('Lưu thay đổi?', () => this.save());
this.confirm.leave(() => this.router.navigate(['/']));

// Confirm dialog — generic
this.confirm.show({ message: 'Bạn có chắc?', accept: () => this.doIt() });

// Confirm popup (anchor to click event)
onDelete(event: MouseEvent) {
  this.confirm.popup(event, {
    message: 'Xoá bản ghi này?',
    accept: () => this.delete(),
  });
}

Đặt <asc-confirm-popup /> trong layout để dùng confirm.popup():

<asc-confirm-popup />

Dialog — AscDialogComponent

<asc-dialog [(visible)]="showDialog" header="Tiêu đề">
  <p>Nội dung dialog</p>
  <ng-template ascDialogFooter>
    <asc-button label="Đóng" severity="secondary" (clicked)="showDialog = false" />
  </ng-template>
</asc-dialog>

Sidebar — AscSidebarComponent

<asc-sidebar [(visible)]="showSidebar" position="right">
  <p>Nội dung sidebar</p>
</asc-sidebar>

Popover — AscPopoverComponent

<asc-button label="Mở popover" (clicked)="pop.toggle($event)" />
<asc-popover #pop>
  <p>Nội dung popover</p>
</asc-popover>

Tooltip — AscTooltipDirective

<asc-button label="Lưu" ascTooltip="Lưu thay đổi" tooltipPosition="top" />
<span ascTooltip="Thông tin thêm">Hover vào đây</span>

Panel

Tabs — AscTabsComponent / AscTabComponent

<asc-tabs [(value)]="activeTab">
  <asc-tab value="info"     label="Thông tin" icon="pi pi-user">
    <p>Nội dung thông tin...</p>
  </asc-tab>
  <asc-tab value="settings" label="Cài đặt"   icon="pi pi-cog">
    <p>Nội dung cài đặt...</p>
  </asc-tab>
</asc-tabs>

Stepper — AscStepperComponent / AscStepComponent

<asc-stepper [(value)]="activeStep">
  <asc-step [value]="1" label="Thông tin">
    <p>Bước 1...</p>
    <asc-button label="Tiếp theo" icon="pi pi-arrow-right" iconPos="right"
                (clicked)="activeStep.set(2)" />
  </asc-step>
  <asc-step [value]="2" label="Xác nhận">
    <p>Bước 2...</p>
    <asc-button label="Quay lại" severity="secondary" (clicked)="activeStep.set(1)" />
    <asc-button label="Hoàn tất" severity="success"   (clicked)="activeStep.set(3)" />
  </asc-step>
  <asc-step [value]="3" label="Hoàn thành">
    <p>Hoàn tất!</p>
  </asc-step>
</asc-stepper>

Validation Error Messages — AscMessageErrorPipe

AscMessageErrorPipe chuyển Angular validator error key thành human-readable message theo locale. Pipe này được AscFormFieldComponent dùng tự động — bạn không cần gọi trực tiếp khi dùng <asc-form-field>. Tuy nhiên bạn có thể dùng độc lập trong bất kỳ template nào.

Cú pháp

{{ errorKey | ascMessageError : label : control.errors }}

| Tham số | Type | Mô tả | |----------------|--------------------|----------------------------------------------------| | errorKey | string | Angular validator error key: 'required', 'email', ... | | label | string | Tên field hiển thị trong message: 'Email', 'Họ tên' | | control.errors | ValidationErrors \| null | AbstractControl.errors — để pipe đọc metadata (vd: minlength.requiredLength) |

Dùng trực tiếp trong template

import { AscMessageErrorPipe } from '@aseansc-admin/ui';

@Component({
  imports: [AscMessageErrorPipe, ReactiveFormsModule, KeyValuePipe],
  template: `
    <input [formControl]="emailCtrl" />
    @if (emailCtrl.errors && emailCtrl.touched) {
      @for (err of emailCtrl.errors | keyvalue; track err.key) {
        <span class="error">
          {{ err.key | ascMessageError : 'Email' : emailCtrl.errors }}
        </span>
      }
    }
  `,
})
export class MyForm {
  emailCtrl = new FormControl('', [Validators.required, Validators.email]);
}

Kết quả:

| Validator | Output | |-------------------------|-------------------------------------------| | Validators.required | Email là bắt buộc | | Validators.email | Email không đúng định dạng email | | Validators.minLength(6) | Email phải có ít nhất 6 ký tự | | Validators.maxLength(100) | Email không được vượt quá 100 ký tự | | Validators.min(0) | Email phải lớn hơn hoặc bằng 0 | | Validators.max(100) | Email phải nhỏ hơn hoặc bằng 100 | | Validators.pattern(...) | Email không đúng định dạng |

Built-in error keys

| Key | Validator tương ứng | |-----------------|----------------------------------------------| | required | Validators.required | | requiredTrue | Validators.requiredTrue | | email | Validators.email | | minlength | Validators.minLength(n) | | maxlength | Validators.maxLength(n) | | min | Validators.min(n) | | max | Validators.max(n) | | pattern | Validators.pattern(...) | | phone | custom validator | | url | custom validator | | dateRange | custom validator | | passwordMatch | custom validator | | unique | custom validator | | _fallback | mọi key không có trong map → "{label} không hợp lệ" |

Override message của built-in key

Truyền validation.messages vào provideAscUI() — chỉ cần khai báo key muốn đổi, các key còn lại giữ nguyên mặc định:

// app.config.ts
provideAscUI({
  locale: 'vi-VN',
  validation: {
    messages: {
      required:  (label) => `${label} không được bỏ trống`,
      minlength: (label, errors) => {
        const min = (errors?.['minlength'] as any)?.requiredLength;
        return `${label} tối thiểu ${min} ký tự`;
      },
    },
  },
})

Thêm error key cho custom validator

Tạo validator trả về object với key tùy chọn, sau đó đăng ký message tương ứng:

// validators/phone.validator.ts
export function phoneValidator(): ValidatorFn {
  return (control) => {
    const valid = /^(0[3|5|7|8|9])\d{8}$/.test(control.value);
    return valid ? null : { phone: true };   // key = 'phone'
  };
}
// validator với metadata (để pipe đọc thêm thông tin)
export function passwordStrengthValidator(minScore: number): ValidatorFn {
  return (control) => {
    const score = calcScore(control.value);
    return score >= minScore ? null : { passwordStrength: { minScore, actualScore: score } };
  };
}
// app.config.ts
provideAscUI({
  validation: {
    messages: {
      // Key khớp với key trả về từ validator
      phone: (label) =>
        `${label} không đúng định dạng (vd: 0912345678)`,

      passwordStrength: (label, errors) => {
        const { minScore, actualScore } = (errors?.['passwordStrength'] ?? {}) as any;
        return `${label} chưa đủ mạnh (điểm: ${actualScore}/${minScore})`;
      },
    },
  },
})
// dùng trong form
password = new FormControl('', [
  Validators.required,
  passwordStrengthValidator(3),
]);

Template hiển thị tự động qua <asc-form-field>:

<asc-form-field label="Mật khẩu">
  <asc-password formControlName="password" />
</asc-form-field>

Dark Mode

The library uses .app-dark on <html> to toggle dark mode. PrimeNG components and Tailwind utilities both respond to this class automatically.

/* styles.scss */
@use "tailwindcss";
@custom-variant dark (&:where(.app-dark, .app-dark *));

i18n

provideAscUI({ locale: 'vi-VN' })  // mặc định — Tiếng Việt
provideAscUI({ locale: 'en-US' })  // English

License

UNLICENSED — Internal use only.