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

shirkasoft-ui-components

v1.3.2

Published

Angular UI Components Library — themeable via CSS custom properties

Readme

Shirkasoft UI Components

Angular 21+ UI components library built with Tailwind CSS, standalone components, Angular signals, and light/dark theme support via CSS custom properties.

Installation

pnpm add shirkasoft-ui-components
# or
npm install shirkasoft-ui-components

Peer Dependencies

| Package | Minimum version | | ------------------ | --------------- | | @angular/common | ^21.2.0 | | @angular/core | ^21.2.0 | | @angular/forms | ^21.2.0 | | @angular/router | ^21.2.0 | | rxjs | ^7.8.0 | | @jsverse/transloco | >= 8.0.0 | | @lucide/angular | ^1.17.0 | | chart.js | ^4.4.0 |

Theme Setup

Import the theme in your global styles.css:

@import '@shirkasoft/ui-components/theme.css';

The theme is based on --shk-* CSS variables. To customize colors, override the variables in :root (light mode) and .dark (dark mode):

:root {
  --shk-primary-500: #3b82f6;
  --shk-primary-600: #2563eb;
  /* ... */
}

Dark mode

Add the dark class to the <html> element to enable dark mode.

document.documentElement.classList.toggle('dark');

Components

| # | Component | Selector | ControlValueAccessor | Associated service | | --- | ------------- | -------------------- | -------------------- | ---------------------- | | 1 | TextField | shk-text-field | ✅ | — | | 2 | Toggle | shk-toggle | ✅ | — | | 3 | Select | shk-select | ✅ | — | | 4 | DatePicker | shk-date-picker | ✅ | — | | 5 | PriceInput | shk-price-input | — | — | | 6 | FileUpload | shk-file-upload | — | — | | 7 | Table | shk-table | — | — | | 8 | Modal | shk-modal | — | ModalService | | 9 | Notification | shk-notification | — | NotificationService | | 10 | ConfirmDialog | shk-confirm-dialog | — | ConfirmDialogService | | 11 | Chart | shk-chart | — | — | | 12 | Tooltip | shk-tooltip | — | — | | 13 | Sidebar | shk-sidebar | — | — | | 14 | Rail Sidebar | shk-rail-sidebar | — | — | | 15 | Skeleton | shk-skeleton | — | — | | 16 | Tag | shk-tag | — | — | | 17 | Button | shk-button | — | — | | 18 | Knob | shk-knob | ✅ | — | | 19 | Loading | shk-loading-* | — | — | | 20 | Tabs | shk-tabs | — | — |


TextField (shk-text-field)

Text input with support for input, textarea, password, search, credit card formatting, and error validation. Implements ControlValueAccessor.

<shk-text-field
  [label]="'Name'"
  [control]="myControl"
  [type]="'text'"
  [placeholder]="'Enter your name'"
  [disabled]="false"
  [errorMessages]="{ required: 'This field is required' }"
  [isTextArea]="false"
  [searchMode]="false"
  (searchButtonClick)="onSearch()"
/>

| Input | Type | Default | | ------------------- | --------------------------- | ---------------- | | id | string | '' | | label | string | '' | | type | string | 'text' | | placeholder | string | '' | | disabled | boolean | false | | control | FormControl | undefined | | formGroup | FormGroup | undefined | | errorMessage | string | '' | | errorMessages | { [key: string]: string } | {} | | isTextArea | boolean | false | | textAreaHeight | string | 'h-28' | | searchMode | boolean | false | | searchButtonClick | output<void> | — | | numbersOnly | boolean | false | | allowDecimals | boolean | false | | maxLength | number | undefined | | formatCard | boolean | false | | preventNegative | boolean | false | | displayValue | string | '' | | autocomplete | string | 'new-password' |

Also exposes inputElement and textareaElement as viewChild.


Toggle (shk-toggle)

Switch or checkbox. Implements ControlValueAccessor.

<shk-toggle
  [label]="'Enable notifications'"
  [mode]="'toggle'"
  [(checked)]="isChecked"
  (checkedChange)="onChange($event)"
/>

| Input | Type | Default | | --------- | ------------------------ | ----------- | | label | string | '' | | mode | 'toggle' \| 'checkbox' | 'toggle' | | checked | boolean | undefined |

| Output | Type | | --------------- | --------- | | checkedChange | boolean |


Select (shk-select)

Searchable dropdown with pagination, multiple selection, custom options, and validation. Implements ControlValueAccessor.

<shk-select
  [options]="options"
  [label]="'Country'"
  [control]="countryControl"
  [multiple]="false"
  [isSearchable]="true"
  [usePagination]="true"
  [itemsPerPage]="10"
  [allowCustomEntries]="false"
  [placeholder]="'Select...'"
  [isLoading]="false"
  (selectionChange)="onSelect($event)"
  (search)="onSearch($event)"
/>
interface SelectOption {
  label: string;
  value: any;
  custom?: boolean;
}

| Input | Type | Default | | ---------------------- | --------------------------- | ---------------------------------------- | | options | SelectOption[] | [] | | label | string | undefined | | placeholder | string | 'Select...' | | disabled | boolean | false | | control | FormControl | undefined | | formGroup | FormGroup | undefined | | multiple | boolean | false | | isSearchable | boolean | false | | usePagination | boolean | false | | itemsPerPage | number | 10 | | allowCustomEntries | boolean | false | | isLoading | boolean | false | | isAllDataLoaded | boolean | false | | preserveSearchOnLoad | boolean | false | | dropdownUpward | boolean | false | | showEmptyOption | boolean | true | | emptyMessageKey | string | 'common.no_records' | | validCombinations | string[][] | undefined | | errorMessages | { [key: string]: string } | { required: 'This field is required' } |

| Output | Type | | ----------------- | -------- | | selectionChange | any | | search | string |


DatePicker (shk-date-picker)

Date, month, or year picker. Implements ControlValueAccessor. Supports 'date', 'month', 'year' views.

<shk-date-picker
  [label]="'Start date'"
  [control]="dateControl"
  [view]="'date'"
  [locale]="'es'"
  [minValue]="'2024-01-01'"
  [maxValue]="'2026-12-31'"
/>

| Input | Type | Default | | --------------- | ----------------------------- | ----------- | | id | string | '' | | label | string | '' | | placeholder | string | '' | | view | 'date' \| 'month' \| 'year' | 'date' | | control | FormControl | undefined | | formGroup | FormGroup | undefined | | disabled | boolean | false | | errorMessage | string | '' | | errorMessages | { [key: string]: string } | {} | | minValue | string | '' | | maxValue | string | '' | | locale | string | 'es' |

Return value depends on view:

  • 'date''YYYY-MM-DD'
  • 'month''YYYY-MM'
  • 'year''YYYY'

PriceInput (shk-price-input)

Numeric input with toggle between amount ($) and percentage (%). Uses Angular model() for two-way binding.

<shk-price-input
  [(value)]="price"
  [(isPercentage)]="isPercent"
  [label]="'Price'"
  [min]="0"
  [max]="10000"
  [required]="true"
  [showError]="hasError"
  [errorMessage]="'Invalid value'"
/>

| Input | Type | Default | | -------------- | ---------------- | ------------ | | value | model<number> | required | | isPercentage | model<boolean> | false | | label | string | '' | | inputId | string | '' | | required | boolean | false | | disabled | boolean | false | | min | number | 0 | | max | number | Infinity | | showError | boolean | false | | errorMessage | string | '' |


FileUpload (shk-file-upload)

File upload with drag & drop, image preview, file type and size validation.

<shk-file-upload
  [label]="'Upload your photo'"
  [accept]="'image/*'"
  [maxFileSize]="5 * 1024 * 1024"
  [maxFiles]="3"
  [multiple]="true"
  (fileSelected)="onFilesSelected($event)"
  (fileRemoved)="onFileRemoved()"
  (fileError)="onFileError($event)"
/>
interface FileUploadError {
  type: 'size' | 'type';
  message: string;
  file: File;
}

| Input | Type | Default | | -------------------- | --------- | ----------------------- | | label | string | 'Upload file' | | accept | string | 'image/*' | | maxFileSize | number | 2 * 1024 * 1024 (2MB) | | maxFiles | number | 0 (unlimited) | | multiple | boolean | false | | fileUploadText | string | 'Select file' | | changeFilesText | string | 'Change files' | | fileRecommendation | string | '' |

| Output | Type | | -------------- | ----------------- | | fileSelected | File[] | | fileRemoved | void | | fileError | FileUploadError | | fileWarning | FileUploadError |


Table (shk-table)

Data table with sorting, column filters, global search, pagination, row actions, and header actions. Supports client and server-side mode.

<shk-table
  [data]="users"
  [columns]="columns"
  [loading]="isLoading"
  [serverSide]="true"
  [totalRecords]="totalUsers"
  [rowActions]="rowActions"
  [headerActions]="headerActions"
  [rowsPerPage]="25"
  [showSearch]="true"
  (pageChange)="onPageChange($event)"
  (searchChange)="onSearch($event)"
  (refresh)="loadData()"
/>
interface Column {
  field: string;
  header: string;
  sortable?: boolean;
  filter?: boolean;
  filterPlaceholder?: string;
  width?: string;
  defaultSort?: true;
  filterType?: 'text' | 'exact' | 'select';
  filterOptions?: { label: string; value: any }[];
  template?: 'text' | 'tag';
  format?: (row: any) => string;
  tagValue?: (row: any) => string;
  tagSeverity?: (row: any) => 'success' | 'danger' | 'warn' | 'info' | undefined;
  tagStyle?: (row: any) => { [key: string]: string } | undefined;
}

interface TableAction {
  label: string;
  icon: string;
  onClick: () => void;
  class?: string;
  isVisible?: () => boolean;
  isDisabled?: () => boolean;
}

interface RowAction {
  label: string | ((data: any) => string);
  icon: string | ((data: any) => string);
  onClick: (rowData: any) => void;
  class?: string | ((data: any) => string);
  isVisible?: (rowData: any) => boolean;
  isDisabled?: (rowData: any) => boolean;
}

interface PageChangeEvent {
  first: number;
  rows: number;
  page: number;
  pageCount: number;
}

interface FilterChangeEvent {
  filters: { [key: string]: any };
}

| Input | Type | Default | | -------------------- | ------------------------------------ | -------------- | | data | any[] | [] | | columns | Column[] | [] | | rowsPerPage | number | 10 | | rowsPerPageOptions | number[] | [10, 25, 50] | | loading | boolean | false | | showActionRow | boolean | true | | headerActions | TableAction[] | [] | | rowActions | RowAction[] | [] | | hasShadow | boolean | true | | defaultSortField | string | '' | | defaultSortOrder | number | 1 | | showSearch | boolean | true | | searchPlaceholder | string | '' | | emptyMessage | string | '' | | serverSide | boolean | false | | totalRecords | number | 0 | | filters | { label: string; value: string }[] | [] | | activeFilter | string | '' | | customTemplates | { [key: string]: any } | {} |

| Output | Type | | -------------- | ------------------- | | pageChange | PageChangeEvent | | filterChange | FilterChangeEvent | | searchChange | string | | filterClick | string | | refresh | void |


Modal (shk-modal)

Dynamic modal that loads components via ModalService. Supports forms, fullscreen expansion, and accept/cancel buttons.

<shk-modal />
interface ModalConfig {
  title: string;
  component: Type<any>;
  data?: Record<string, any>;
  width?: string;
  showButtons?: boolean;
  showExpandButton?: boolean;
  acceptLabel?: string;
  cancelLabel?: string;
  onClose?: () => void;
}

The injected component must expose:

  • form?: FormGroup (optional, for validation)
  • onSubmit() → calls submitSuccess or submitError
  • submitSuccess?: EventEmitter<void> (optional)
  • submitError?: EventEmitter<void> (optional)
  • handleCancel?: () => void (optional)
constructor(private modalSrv: ModalService) {}

openModal() {
  this.modalSrv.open({
    title: 'Edit user',
    component: EditUserComponent,
    data: { userId: 123 },
    width: '600px',
    showButtons: true,
    showExpandButton: true,
  });
}

closeModal() {
  this.modalSrv.close();
}

Service methods:

| Method | Description | | --------------------------- | ---------------------------------- | | open(config: ModalConfig) | Opens a new modal | | close() | Closes the current modal | | accept() | Closes the modal (alias for close) | | clear() | Closes all modals |


Notification (shk-notification)

Toast notification system with configurable positions and optional progress bar.

<shk-notification position="right-top" />
interface Notification {
  id: string;
  message: string;
  type: 'success' | 'error' | 'warning' | 'info';
  progress?: number;
  showProgress?: boolean;
}
constructor(private notifSrv: NotificationService) {}

showNotif() {
  const id = this.notifSrv.addNotification(
    'Operation successful',
    'success',
    false,  // showProgress
    3000    // duration (ms)
  );
}

updateProgress(id: string, progress: number) {
  this.notifSrv.updateProgress(id, progress);
}

| Input | Type | Default | | ---------- | ------------------------------------------- | -------------- | | position | 'center-top' \| 'right-top' \| 'left-top' | 'center-top' |

Service methods:

| Method | Description | | ------------------------------------------------------- | ------------------------- | | addNotification(msg, type?, showProgress?, duration?) | Adds a notification | | updateProgress(id, progress) | Updates the progress bar | | removeNotification(notification) | Removes a notification | | removeNotificationById(id) | Removes by ID | | clearAll() | Removes all notifications |


ConfirmDialog (shk-confirm-dialog)

Programmatic confirmation dialog. Used via ConfirmDialogService which dynamically creates it.

<!-- No need to add it to the template -->
interface ConfirmConfig {
  title?: string;
  message: string;
  confirmLabel?: string;
  cancelLabel?: string;
  loadingText?: string;
  type?: 'danger' | 'info' | 'warning';
  showCancel?: boolean;
  loading?: boolean;
}
constructor(private confirmSrv: ConfirmDialogService) {}

async deleteItem() {
  const confirmed = await this.confirmSrv.confirm({
    title: 'Delete user',
    message: 'Are you sure you want to delete this user?',
    confirmLabel: 'Delete',
    type: 'danger',
  });

  if (confirmed) {
    // proceed with deletion
  }
}

| Input | Type | Default | | -------------- | --------------------------------- | ------------------------------------- | | title | string | 'Confirm action' | | message | string | 'Are you sure you want to proceed?' | | confirmLabel | string | 'Confirm' | | cancelLabel | string | 'Cancel' | | loadingText | string | 'Processing…' | | type | 'danger' \| 'info' \| 'warning' | 'danger' | | loading | boolean | false | | showCancel | boolean | true |

| Output | Type | | -------- | ------ | | closed | void |


Chart (shk-chart)

Chart.js wrapper supporting the main chart types.

<shk-chart [type]="'bar'" [data]="chartData" [options]="chartOptions" />
import { COLOR_PALETTE, readThemeColors } from 'shirkasoft-ui-components';

const colors = readThemeColors(); // current theme colors

| Input | Type | Default | | --------- | ------------------------------------------------------- | ------------------ | | type | 'line' \| 'bar' \| 'pie' \| 'doughnut' \| 'polarArea' | 'bar' | | data | any | { datasets: [] } | | options | any | {} |

Exported utils:

| Export | Description | | ------------------- | ---------------------------------------------- | | COLOR_PALETTE | Predefined color array (bg + border) | | readThemeColors() | Reads current theme colors (--shk-surface-*) |


Tooltip (shk-tooltip)

Tooltip with two modes: hover (CSS positioning) and fixed (JS-calculated positioning).

<shk-tooltip [text]="'Additional info'" [position]="'bottom'" [mode]="'hover'">
  <button>Hover me</button>
</shk-tooltip>

| Input | Type | Default | | ---------- | ------------------------------ | ---------- | | text | string | '' | | position | 'top' \| 'bottom' \| 'right' | 'bottom' | | mode | 'hover' \| 'fixed' | 'hover' | | offset | number | 8 |


Sidebar (shk-sidebar)

Brand/logo header, round collapse toggle with tooltip, expandable groups with nested children, standalone items, Lucide icons, badges, separators, disabled items, active-item highlighting (primary-600 with white text), projected header/content/footer slots, tooltips on collapsed items, and a responsive off-canvas mode below 1024px. Uses the same theme tokens (--shk-*) as the rest of the library.

<shk-sidebar
  [items]="items"
  [(collapsed)]="collapsed"
  [activeItemId]="activeId"
  title="WS Admin"
  subtitle="Panel de control"
  logo="/logo.svg"
  [defaultExpanded]="['products']"
  (itemClick)="onItemClick($event)"
>
  <div sidebarFooter>
    <button (click)="logout()">Cerrar sesión</button>
  </div>
</shk-sidebar>
interface SidebarItem {
  id: string;
  label: string;
  icon?: string; // Lucide icon name
  badge?: string | number;
  disabled?: boolean;
  separator?: boolean; // renders a divider before the item
  route?: string; // navigation target (optional)
  roles?: string[]; // when `userRole` is set, items are filtered
  children?: SidebarItem[]; // renders an expandable group with nested items
}

interface SidebarGroup {
  id?: string;
  label?: string;
  items: SidebarItem[];
}

| Input | Type | Default | | ----------------- | --------------------------- | --------------------------------------------- | | items | SidebarItem[] | [] | | groups | SidebarGroup[] | [] (when set, overrides items) | | title | string | '' | | subtitle | string | '' | | logo | string | '' (image URL shown in the brand header) | | collapsed | model<boolean> | false | | mobileOpen | model<boolean> | false (off-canvas state on mobile) | | collapsible | boolean | true | | showToggle | boolean | true | | responsive | boolean | true (off-canvas below 1024px) | | fixed | boolean | false (uses position: fixed + h-screen) | | hasShadow | boolean | true | | expandedWidth | string | '16rem' | | collapsedWidth | string | '5rem' | | activeItemId | string | '' | | userRole | string | '' (filters items by roles) | | defaultExpanded | string[] | [] (groups expanded on init) | | labelPipe | (label: string) => string | identity (e.g. pass a Transloco translate fn) | | expandTooltip | string | 'Expandir' | | collapseTooltip | string | 'Colapsar' |

| Output | Type | | ----------- | ------------- | | itemClick | SidebarItem | | toggle | boolean |

Projected slots:

| Slot | Description | | ---------------- | --------------------------------------------------- | | sidebarHeader | Brand/logo area (rendered before the title) | | sidebarContent | Custom content between the header and the nav | | sidebarFooter | Footer area (e.g. logout button); hidden when empty |

Icons are resolved by name via a Lucide map with a Menu fallback. Use [fixed]="true" for a full-screen sidebar (as in ws-admin) or leave it embedded in a container for previews.

Rail Sidebar (shk-rail-sidebar)

Sidebar estilo rail + nav, una columna vertical de iconos (una por sección) y un panel con el menú de la sección activa, con subgrupos expandibles, toggle de idioma, botón de logout y colapso.

<shk-rail-sidebar
  [sections]="sections"
  [activeUrl]="router.url"
  [(activeSectionKey)]="activeSectionKey"
  [(collapsed)]="collapsed"
  [activeLang]="transloco.getActiveLang()"
  appName="WAC Bookings"
  logo="/logo.png"
  (sectionSelected)="onSectionSelected($event)"
  (itemClick)="onItemClick($event)"
  (setLang)="onSetLanguage($event)"
  (logoutRequested)="requestLogout()"
/>
interface RailSidebarSection {
  key: string;
  label: string;
  icon?: string; // nombre del icono Lucide
  routerLink?: string; // ruta de la sección (selección automática por URL)
  matchPrefixes?: string[]; // prefijos de URL que activan la sección
  items?: RailSidebarItem[]; // menú del panel cuando la sección está activa
}

interface RailSidebarItem {
  id: string;
  label: string;
  icon?: string;
  routerLink?: string;
  children?: RailSidebarItem[]; // subgrupo expandible
  roles?: string[]; // filtro por rol (userRole)
  badge?: string | number;
  disabled?: boolean;
  separator?: boolean;
}

| Input | Type | Default | | ------------------ | ---------------------- | --------------------------------------------------------- | | sections | RailSidebarSection[] | [] | | activeUrl | string | '' — activa la sección por matchPrefixes/routerLink | | activeSectionKey | string (model) | '' | | collapsed | boolean (model) | false | | activeLang | string | 'es' | | langs | string[] | ['es', 'en'] — idiomas al togglear | | logo | string | '' — imagen del workspace | | appName | string | '' — título del panel | | userRole | string | '' — filtra items.roles | | labelPipe | (label) => string | identidad (para transloco) | | railWidth | string | '4.5rem' | | expandedWidth | string | '16.5rem' |

| Output | Type | | ----------------- | ---------------------- | | sectionSelected | { key, routerLink? } | | itemClick | RailSidebarItem | | setLang | string | | logoutRequested | void | | toggleSidebar | void | | workspaceClick | void |


Skeleton (shk-skeleton)

Placeholder block shown while content is loading. Rectangular or circular, with wave, pulse or no animation.

<shk-skeleton height="1.5rem" width="60%" />
<shk-skeleton shape="circle" size="5rem" />
<shk-skeleton width="16rem" height="5rem" borderRadius="12px" animation="pulse" />

| Input | Type | Default | | -------------- | ----------------------------- | ------------ | | shape | 'rectangle' \| 'circle' | 'rectangle'| | size | string | '' (circle)| | width | string | '100%' | | height | string | '1rem' | | borderRadius | string | '0.25rem' | | animation | 'wave' \| 'pulse' \| 'none' | 'wave' | | style | object | {} | | styleClass | string | '' |


Tag (shk-tag)

Small label used to highlight a value, status or category.

<shk-tag value="Activo" severity="success" [rounded]="true" />
<shk-tag value="Precio" severity="contrast" icon="dollar-sign" />
type TagSeverity = 'secondary' | 'success' | 'info' | 'warning' | 'danger' | 'contrast';

| Input | Type | Default | | ------------ | -------------- | ------------- | | value | string \| number | '' | | severity | TagSeverity | 'secondary' | | rounded | boolean | false | | icon | string | '' (lucide) | | style | object | {} | | styleClass | string | '' |


Button (shk-button)

Generic button with severity variants, lucide icons, sizes and a loading state. Icons are resolved by name from a built-in lucide registry (plus, pencil, trash-2, search, arrow-right, …).

<shk-button label="Añadir" icon="plus" (onClick)="create()" />
<shk-button label="Eliminar" severity="danger" [outlined]="true" />
<shk-button [label]="loading() ? 'Cargando…' : 'Guardar'" [loading]="loading()" />
<shk-button icon="search" severity="secondary" [rounded]="true" ariaLabel="Buscar" />
type ButtonSeverity = 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'danger' | 'help' | 'contrast';
type ButtonSize = 'small' | 'large';
type ButtonIconPosition = 'left' | 'right' | 'top' | 'bottom';

| Input | Type | Default | | ------------- | ------------------ | ------------ | | label | string | '' | | icon | string | '' (lucide)| | iconPos | ButtonIconPosition| 'left' | | severity | ButtonSeverity | 'primary' | | outlined | boolean | false | | text | boolean | false | | raised | boolean | false | | rounded | boolean | false | | size | ButtonSize | undefined | | disabled | boolean | false | | loading | boolean | false | | loadingIcon | string | 'refresh' | | loadingText | string | '' | | type | 'button' \| 'submit' \| 'reset' | 'button' | | ariaLabel | string | '' | | title | string | '' | | style | object | {} | | styleClass | string | '' |

| Output | Type | | -------- | ------------ | | onClick| MouseEvent |


Knob (shk-knob)

SVG radial knob with pointer drag and keyboard (arrow keys) support. Implements ControlValueAccessor (reactive forms / [(ngModel)]), and also offers [(value)].

<shk-knob [formControl]="scoreCtrl" [min]="0" [max]="10" [step]="0.5" [size]="120" />
<shk-knob [(value)]="value" valueColor="#3b82f6" rangeColor="#e2e8f0" />

| Input | Type | Default | | ------------- | --------- | -------------------------------- | | value | model<number> | 0 | | min | number | 0 | | max | number | 100 | | step | number | 1 | | size | number | 100 (px) | | strokeWidth | number | 14 | | valueColor | string | var(--shk-primary-600) | | rangeColor | string | var(--shk-surface-200) | | showValue | boolean | true | | disabled | boolean | false | | readonly | boolean | false | | ariaLabel | string | '' |

| Output | Type | | -------- | -------- | | change | number | | end | number |


Loading (shk-loading-*)

A set of reusable loading indicators: spinner, indeterminate/determinate bar, dots and overlay.

<shk-loading-spinner [size]="'2.5rem'" [thickness]="4" label="Cargando…" />
<shk-loading-bar [indeterminate]="true" height="0.5rem" />
<shk-loading-bar [indeterminate]="false" [progress]="progress()" />
<shk-loading-dots [count]="5" label="Enviando…" />
<shk-loading-overlay [visible]="loading" message="Cargando datos…" [backdrop]="'light'">
  <!-- content to cover -->
</shk-loading-overlay>

shk-loading-spinner

| Input | Type | Default | | ---------- | -------- | ------- | | size | string | '2rem'| | thickness| number | 3 | | color | string | primary token | | label | string | '' |

shk-loading-bar

| Input | Type | Default | | -------------- | --------- | ------------ | | height | string | '0.25rem' | | indeterminate| boolean | true | | progress | number | 0 (0-100) | | color | string | primary token| | rounded | boolean | true |

shk-loading-dots

| Input | Type | Default | | ------- | -------- | ------------ | | size | string | '0.5rem' | | color | string | primary token| | count | number | 3 | | label | string | '' |

shk-loading-overlay

| Input | Type | Default | | ------------ | ---------------------------------- | -------------- | | visible | boolean | false | | message | string | '' | | spinnerSize| string | '2.5rem' | | color | string | primary token | | backdrop | 'transparent' \| 'light' \| 'dark' | 'transparent' |


Tabs (shk-tabs)

Tabbed content with a PrimeNG-like nested API. Values can be numbers or strings; the active value is two-way bindable.

<shk-tabs [value]="activeTab()" (valueChange)="onTabChange($event)" [scrollable]="true">
  <shk-tablist [scrollable]="true">
    <shk-tab [value]="0">General</shk-tab>
    <shk-tab [value]="1">Detalle</shk-tab>
    <shk-tab [value]="2" [disabled]="true">Bloqueado</shk-tab>
  </shk-tablist>
  <shk-tabpanels>
    <shk-tabpanel [value]="0">Contenido General</shk-tabpanel>
    <shk-tabpanel [value]="1">Contenido Detalle</shk-tabpanel>
  </shk-tabpanels>
</shk-tabs>

| Selector | Inputs | Outputs | | ------------- | ------------------------ | --------------- | | shk-tabs | value (model), scrollable | valueChange | | shk-tablist | scrollable, styleClass | — | | shk-tab | value, disabled, styleClass | — | | shk-tabpanels| — | — | | shk-tabpanel| value | — |

Panels are rendered lazily: only the active panel exists in the DOM.


Development

# Build the library
pnpm build

# Serve the showcase (demo app)
pnpm serve

# Build the showcase
pnpm build:showcase

# Watch for library changes
pnpm watch

Versioning

pnpm version:patch   # 1.0.23 → 1.0.24 (creates commit + tag locally)
pnpm version:minor   # 1.0.23 → 1.1.0
pnpm version:major   # 1.0.23 → 2.0.0

Push code (without publishing)

git push origin main

Push code + publish to npm

git push origin main --tags

Publishing

CI/CD via GitHub Actions

| Workflow | Trigger | Description | | ------------------- | --------------------------------------------- | ------------------------------------------------------------------------- | | Publish to npm | Manual (workflow_dispatch) or push v* tag | Builds library, publishes to npm, builds showcase, deploys to public repo | | Deploy Showcase | Push to main/master or manual | Builds library + showcase, deploys to GitHub Pages public repo |

Manual publish (without GitHub Actions)

bash scripts/publish-lib.sh

License

MIT © Shirkasoft