ryzen-ui
v0.2.18
Published
Ryzen UI — standalone Angular component library built with signals, OnPush, and pure CSS with oklch theming
Downloads
2,888
Maintainers
Readme
Ryzen UI
A lightweight, standalone Angular component library built with signals, OnPush change detection, pure component-scoped CSS with oklch color theming.
- 27 components across Form, Data Display, Feedback, Navigation, and Overlay categories
- Zero runtime dependencies beyond
@angular/coreandprimeicons - No
@angular/animations— CSS keyframes for all animations - No CDK — manual document listeners and
position: fixed / absolutefor overlays - Tree-shakeable, side-effect-free
Installation
pnpm add ryzen-ui primeicons
# or
npm install ryzen-ui primeiconsOptional peer dependencies (for Table export)
pnpm add exceljs jspdf html2canvas
# or
npm install exceljs jspdf html2canvasQuick Start
import { Component } from '@angular/core';
import { ToastService } from 'ryzen-ui';
import { RzButtonComponent } from 'ryzen-ui';
// etc. — all components are standalone and tree-shakeableGlobal styles
/* styles.css */
@import 'primeicons/primeicons.css';
:root {
--color-primary: oklch(0.32 0.09 258);
--color-surface: oklch(0.99 0 0);
--color-border: oklch(0.83 0.015 260);
--color-text: oklch(0.14 0.01 260);
--color-text-muted: oklch(0.48 0.01 260);
--color-surface-alt: oklch(0.975 0.005 260);
--color-secondary: oklch(0.55 0.12 40);
--color-accent: oklch(0.64 0.2 50);
--color-success: oklch(0.55 0.18 145);
--color-warning: oklch(0.68 0.18 75);
--color-danger: oklch(0.55 0.22 25);
--color-info: oklch(0.55 0.15 235);
--radius-md: 0.5rem;
--font-sans: ui-sans-serif, system-ui, sans-serif;
}
.dark {
--color-primary: oklch(0.65 0.15 258);
--color-surface: oklch(0.18 0.01 260);
--color-border: oklch(0.3 0.01 260);
--color-text: oklch(0.95 0.005 260);
--color-text-muted: oklch(0.65 0.01 260);
--color-surface-alt: oklch(0.22 0.01 260);
}Theming
All components inherit colors through CSS custom properties. Each component's :host maps theme variables into internal --rz-* variables with oklch fallbacks, so components work without any theme setup:
| Variable | Default | Description |
|----------|---------|-------------|
| --rz-accent | oklch(0.32 0.09 258) | Accent/highlight color |
| --rz-bg | oklch(0.99 0 0) | Input/component background |
| --rz-border | oklch(0.83 0.015 260) | Border color |
| --rz-text | oklch(0.14 0.01 260) | Text color |
| --rz-muted | oklch(0.48 0.01 260) | Muted/secondary text |
| --rz-radius | 0.5rem | Border radius |
Dark mode
Add .dark class to <html>:
<html class="dark">...</html>The library uses @custom-variant dark (&:where(.dark, .dark *)) — no media query, fully manual toggle.
Components
Common Conventions
- Size:
'sm' | 'md' | 'lg'— componentsizeinput, defaults to'md' - Accent: applies via
accentColorinput (namedThemeColoror any raw CSS color string) orconfig.accentColorwhere available - Error/Hint: every form component supports
error,errorMessage, andhintinputs - Animations: CSS
@keyframesprefixed withrz-*, triggered by Angular animation triggers ([animate.enter],[animate.leave]) - Panel positioning: panels (dropdown, datepicker) use
position: fixedwith manual(document:click)listener — no CDK
Form
<app-text-input>
Standard text input field.
| Input | Type | Default |
|-------|------|---------|
| value | model<string> | '' |
| placeholder | string | '' |
| size | FieldSize | 'md' |
| disabled | boolean | false |
| accentColor | string \| null | null |
| hint | string | '' |
| errorMessage | string | '' |
| error | boolean | false |
| width | string | '' |
<app-text-input [(value)]="name" placeholder="Enter name" [accentColor]="'primary'" /><app-password-input>
Password field with visibility toggle.
| Input | Type | Default |
|-------|------|---------|
| value | model<string> | '' |
| placeholder | string | '' |
| size | FieldSize | 'md' |
| disabled | boolean | false |
| accentColor | string \| null | null |
| hint | string | '' |
| errorMessage | string | '' |
| error | boolean | false |
| hideToggle | boolean | false |
| width | string | '' |
Methods: toggleVisibility()
<app-password-input [(value)]="password" placeholder="Password" /><app-search-input>
Search field with search icon and clear button.
| Input | Type | Default |
|-------|------|---------|
| value | model<string> | '' |
| placeholder | string | 'Search...' |
| size | FieldSize | 'md' |
| disabled | boolean | false |
| accentColor | string \| null | null |
| hint | string | '' |
| errorMessage | string | '' |
| error | boolean | false |
| width | string | '' |
<app-search-input [(value)]="query" placeholder="Search users..." /><app-checkbox>
Checkbox with optional label, indeterminate state.
| Input | Type | Default |
|-------|------|---------|
| checked | model<boolean> | false |
| label | string | '' |
| size | FieldSize | 'md' |
| disabled | boolean | false |
| accentColor | string \| null | null |
| indeterminate | boolean | false |
| hint | string | '' |
| errorMessage | string | '' |
| error | boolean | false |
| width | string | '' |
Methods: toggle()
<app-checkbox [(checked)]="agreed" label="I agree to the terms" />
<app-checkbox [indeterminate]="true" label="Select all" /><app-toggle>
Switch/toggle control.
| Input | Type | Default |
|-------|------|---------|
| checked | model<boolean> | false |
| label | string | '' |
| size | FieldSize | 'md' |
| disabled | boolean | false |
| accentColor | string \| null | null |
| hint | string | '' |
| errorMessage | string | '' |
| error | boolean | false |
| width | string | '' |
Methods: toggle()
<app-toggle [(checked)]="notifications" label="Enable notifications" /><app-date-picker>
Inline calendar with month/year navigation, today button.
| Input | Type | Default |
|-------|------|---------|
| value | model<Date \| null> | null |
| placeholder | string | 'Select date' |
| size | FieldSize | 'md' |
| disabled | boolean | false |
| accentColor | string \| null | null |
| hint | string | '' |
| errorMessage | string | '' |
| error | boolean | false |
| format | 'dd/MM/yyyy' \| 'MM/dd/yyyy' \| 'yyyy-MM-dd' | 'dd/MM/yyyy' |
| width | string | '' |
| config | DatePickerConfig | {} |
DatePickerConfig:
interface DatePickerConfig {
minDate?: Date;
maxDate?: Date;
disabledDates?: Date[];
highlightedDates?: Date[];
firstDayOfWeek?: 0 | 1; // 0=Sunday, 1=Monday
showTodayButton?: boolean; // default true
showYearNavigation?: boolean; // default true
appendToParent?: boolean; // default false
}<app-date-picker [(value)]="dob" placeholder="Date of birth" />
<app-date-picker [(value)]="range" [config]="{ minDate: today, firstDayOfWeek: 1 }" /><app-drop-down>
Searchable dropdown with smart panel positioning.
| Input | Type | Default |
|-------|------|---------|
| value | model<string \| null> | null |
| options | (DropdownOption \| string)[] | [] |
| placeholder | string | 'Select...' |
| size | FieldSize | 'md' |
| disabled | boolean | false |
| accentColor | string \| null | null |
| searchable | boolean | false |
| hint | string | '' |
| errorMessage | string | '' |
| error | boolean | false |
| width | string | '' |
| config | DropdownConfig | {} |
DropdownOption<T>:
interface DropdownOption<T = unknown> {
label: string;
value: T;
}DropdownConfig:
interface DropdownConfig {
appendToParent?: boolean;
maxHeight?: string;
direction?: 'up' | 'down' | 'auto';
}<app-drop-down [(value)]="country" :options="['Egypt', 'UAE', 'KSA']" />
<app-drop-down
[(value)]="user"
[options]="[{ label: 'Alice', value: 1 }, { label: 'Bob', value: 2 }]"
searchable
/><app-multi-select>
Multi-select with chips, search, select all.
| Input | Type | Default |
|-------|------|---------|
| value | model<string[]> | [] |
| options | (DropdownOption \| string)[] | [] |
| placeholder | string | 'Select...' |
| size | FieldSize | 'md' |
| disabled | boolean | false |
| selectAll | boolean | true |
| selectAllLabel | string | 'Select All' |
| accentColor | string \| null | null |
| searchable | boolean | false |
| hint | string | '' |
| errorMessage | string | '' |
| error | boolean | false |
| width | string | '' |
| config | MultiSelectConfig | {} |
MultiSelectConfig:
interface MultiSelectConfig {
appendToParent?: boolean;
maxHeight?: string;
direction?: 'up' | 'down' | 'auto';
}<app-multi-select
[(value)]="roles"
[options]="['Admin', 'Editor', 'Viewer']"
searchable
/><app-file-upload>
Drag-and-drop file upload with validation.
| Input | Type | Default |
|-------|------|---------|
| files | model<File[]> | [] |
| accept | string | '' |
| multiple | boolean | true |
| maxSize | number (bytes) | 0 (unlimited) |
| maxFiles | number | 0 (unlimited) |
| disabled | boolean | false |
| placeholder | string | 'Drop files here or click to browse' |
| size | FieldSize | 'md' |
| hint | string | '' |
| errorMessage | string | '' |
| error | boolean | false |
<app-file-upload [(files)]="docs" accept=".pdf,.docx" [maxSize]="5 * 1024 * 1024" />Data Display
<app-badge>
Small label/badge with variants.
| Input | Type | Default |
|-------|------|---------|
| accentColor | string | 'primary' |
| variant | 'filled' \| 'outlined' \| 'subtle' | 'filled' |
| size | FieldSize | 'sm' |
| dot | boolean | false |
<app-badge variant="subtle" accentColor="success">Active</app-badge>
<app-badge [dot]="true" accentColor="danger" /><app-skeleton>
Placeholder loader.
| Input | Type | Default |
|-------|------|---------|
| variant | 'text' \| 'circle' \| 'rect' | 'text' |
| width | string | '' |
| height | string | '' |
| count | number | 1 |
| borderRadius | string | '' |
| accentColor | string | '' |
<app-skeleton variant="circle" width="40px" height="40px" />
<app-skeleton variant="text" count="3" /><app-table-skeleton>
Table skeleton loader.
| Input | Type | Default |
|-------|------|---------|
| rows | number | 5 |
| columns | number \| TableSkeletonColumn[] | 4 |
<app-table-skeleton [rows]="8" [columns]="6" /><app-tooltip>
Overflow-detection tooltip.
| Input | Type | Default |
|-------|------|---------|
| text | string | '' |
| position | 'top' \| 'bottom' \| 'left' \| 'right' | 'top' |
| disabled | boolean | false |
| alwaysShow | boolean | false |
<app-tooltip text="This is a tooltip" position="top">
<span>Hover me</span>
</app-tooltip><app-table>
Full-featured data table with sorting, column filtering, text search, pagination, row selection, column resize, column toggle, Excel/PDF export, row expansion, sticky header, and skeleton loading.
| Input | Type | Default | Description |
|-------|------|---------|-------------|
| columns | ColumnDef[] | [] | Column definitions |
| rows | Record<string, unknown>[] | [] | Data rows |
| striped | boolean | false | Alternating row colors |
| hoverable | boolean | true | Highlight row on hover |
| sortable | boolean | false | Enable column sorting |
| emptyMessage | string | 'No data available' | Text when no rows match |
| width | string | '' | CSS width |
| loading | boolean | false | Show skeleton loader |
| searchable | boolean | false | Global text search bar |
| filterable | boolean | false | Per-column filter dropdowns |
| skeletonRows | number | 5 | Skeleton row count during loading |
| resizable | boolean | false | Drag column borders to resize |
| pageable | boolean | false | Pagination controls |
| pageSize | number | 10 | Rows per page |
| pageSizeOptions | number[] | [5, 10, 20, 50] | Page size choices |
| selectable | boolean | false | Checkbox row selection |
| selectionMode | 'single' \| 'multiple' | 'multiple' | Single or multi select |
| selectionKey | string | '' | Row property for selection identity |
| stickyHeader | boolean | false | Fixed header on scroll |
| exportable | boolean | false | Excel (xlsx) + PDF export buttons |
| columnToggle | boolean | false | Show/hide columns menu |
| expandable | boolean | false | Expandable row detail |
| Output | Emit Type | Description |
|--------|-----------|-------------|
| sortChange | SortChange | Emitted when sort column/direction changes |
| pageChange | number | Emitted on page navigation |
| selectedRows | Record<string, unknown>[] | Emitted when selection changes |
interface SortChange {
key: string;
direction: 'asc' | 'desc' | '';
}ColumnDef:
interface ColumnDef {
key: string; // Property name in row data
header: string; // Display header text
sortable?: boolean; // Allow sorting on this column
width?: string; // CSS width (e.g. '120px', '2fr')
align?: 'left' | 'center' | 'right'; // Text alignment
cell?: (row: Record<string, unknown>) => string; // Custom cell renderer
rowSpan?: (row, index, rows) => number; // Dynamic rowspan (0=hidden, >1=span)
actions?: ActionDef[]; // Action buttons per row
filterable?: boolean; // Show filter dropdown
filterOptions?: { label: string; value: string }[]; // Filter choices
footer?: (rows: Record<string, unknown>[]) => string; // Footer text aggregator
truncate?: boolean; // Truncate long text
maxChars?: number; // Max chars before truncation
}ActionDef:
interface ActionDef {
icon?: string; // PrimeIcon class (e.g. 'pi pi-pencil')
label?: string; // Button text (falls back to icon-only)
accentColor?: string; // Button color
visible?: (row: Record<string, unknown>) => boolean; // Conditional visibility
disabled?: (row: Record<string, unknown>) => boolean; // Conditional disable
click: (row: Record<string, unknown>) => void; // Click handler
}Complete usage example:
import { Component } from '@angular/core';
import { ColumnDef, SortChange } from 'ryzen-ui';
@Component({ ... })
export class MyComponent {
cols: ColumnDef[] = [
{ key: 'name', header: 'Name', sortable: true, width: '150px' },
{ key: 'email', header: 'Email', sortable: true, filterable: true, filterOptions: [] },
{ key: 'role', header: 'Role', sortable: true },
{
key: 'actions',
header: '',
width: '100px',
align: 'center',
actions: [
{
icon: 'pi pi-pencil',
accentColor: 'primary',
click: row => this.edit(row),
},
{
icon: 'pi pi-trash',
accentColor: 'danger',
visible: row => row['status'] !== 'archived',
click: row => this.delete(row),
},
],
},
];
data = [
{ name: 'Alice', email: '[email protected]', role: 'Admin', status: 'active' },
{ name: 'Bob', email: '[email protected]', role: 'User', status: 'active' },
];
onSort(ev: SortChange) { console.log('sort', ev); }
onPage(page: number) { console.log('page', page); }
onSelect(rows: Record<string, unknown>[]) { console.log('selected', rows); }
edit(row: any) { /* ... */ }
delete(row: any) { /* ... */ }
}<app-table
[columns]="cols"
[rows]="data"
sortable
pageable
searchable
filterable
selectable
selectionKey="email"
exportable
columnToggle
resizable
striped
stickyHeader
(sortChange)="onSort($event)"
(pageChange)="onPage($event)"
(selectedRows)="onSelect($event)"
/>
<!-- With expandable rows -->
<app-table [columns]="cols" [rows]="data" expandable>
<ng-template #rowDetail let-row>
<div style="padding: 1rem">
<p><strong>Email:</strong> {{ row['email'] }}</p>
<p><strong>Role:</strong> {{ row['role'] }}</p>
</div>
</ng-template>
</app-table><app-image-upload>
Image uploader with thumbnail previews.
| Input | Type | Default |
|-------|------|---------|
| images | model<ImageFile[]> | [] |
| multiple | boolean | true |
| maxFiles | number | 4 |
| maxSize | number (bytes) | 2097152 (2 MB) |
| disabled | boolean | false |
| size | FieldSize | 'md' |
| placeholder | string | 'Drop images or click' |
| errorMessage | string | '' |
| error | boolean | false |
interface ImageFile {
file: File;
url: string;
}<app-image-upload [(images)]="photos" [maxFiles]="6" /><app-carousel>
Image carousel with autoplay, dots, arrows.
| Input | Type | Default |
|-------|------|---------|
| items | CarouselItem[] | required |
| height | string | '320px' |
| autoPlay | boolean | true |
| interval | number (ms) | 4000 |
| showArrows | boolean | true |
| showDots | boolean | true |
interface CarouselItem {
src: string;
alt?: string;
title?: string;
description?: string;
}<app-carousel [items]="slides" [autoPlay]="false" /><app-banner-slider>
Hero banner with CTA button and text overlays.
| Input | Type | Default |
|-------|------|---------|
| items | BannerItem[] | required |
| height | string | '400px' |
| autoPlay | boolean | true |
| interval | number (ms) | 5000 |
| showArrows | boolean | true |
| showDots | boolean | true |
| overlayMode | 'left' \| 'center' \| 'right' | 'left' |
| Output | Emit Type |
|--------|-----------|
| ctaClick | BannerItem |
interface BannerItem {
src: string;
alt?: string;
title?: string;
subtitle?: string;
description?: string;
ctaLabel?: string;
ctaLink?: string;
}<app-banner-slider [items]="banners" (ctaClick)="onCtaClick($event)" /><app-empty-state>
Centered placeholder with icon and action.
| Input | Type | Default |
|-------|------|---------|
| icon | string | '' |
| title | string | '' |
| message | string | '' |
| width | string | '' |
<app-empty-state icon="pi pi-inbox" title="No messages" message="You have no unread messages" />Feedback
<app-alert>
Colored alert banner with icon and dismiss.
| Input | Type | Default |
|-------|------|---------|
| type | 'success' \| 'warning' \| 'danger' \| 'info' | 'info' |
| title | string | '' |
| dismissible | boolean | true |
| showIcon | boolean | true |
| width | string | '' |
| Output | Emit Type |
|--------|-----------|
| dismiss | void |
<app-alert type="success" title="Operation completed successfully" (dismiss)="onDismiss()" />
<app-alert type="danger" [dismissible]="false" title="Critical error" /><app-spinner>
Loading spinner with 5 variants, image/text support, overlay mode.
| Input | Type | Default |
|-------|------|---------|
| variant | 'circle' \| 'dual-ring' \| 'dots' \| 'pulse' \| 'bars' | 'dual-ring' |
| size | FieldSize | 'md' |
| accentColor | string | 'primary' |
| overlay | boolean | false |
| text | string | '' |
| imageUrl | string | '' |
<app-spinner variant="dots" text="Loading..." />
<app-spinner [overlay]="true" variant="bars" />
<app-spinner variant="circle" [imageUrl]="'/assets/logo.png'" /><app-toast>
Toast notification container (used with ToastService).
| Input | Type | Default |
|-------|------|---------|
| position | 'top-right' \| 'top-left' \| 'bottom-right' \| 'bottom-left' | 'top-right' |
| maxVisible | number | 5 |
<app-toast position="top-right" />ToastService (providedIn: 'root'):
class ToastService {
readonly toasts: Signal<ToastMessage[]>;
show(type: ToastType, message: string, config?: { title?: string; duration?: number }): void;
success(message: string, config?: { title?: string; duration?: number }): void;
error(message: string, config?: { title?: string; duration?: number }): void;
warning(message: string, config?: { title?: string; duration?: number }): void;
info(message: string, config?: { title?: string; duration?: number }): void;
remove(id: number): void;
clear(): void;
}@Component({ ... })
export class MyComponent {
private toast = inject(ToastService);
save() {
this.toast.success('Data saved', { title: 'Success', duration: 3000 });
}
}Navigation
<app-accordion> + <app-accordion-panel>
Collapsible accordion with single/multi mode.
<app-accordion>:
| Input | Type | Default |
|-------|------|---------|
| multi | boolean | false |
<app-accordion-panel>:
| Input | Type | Default |
|-------|------|---------|
| expanded | model<boolean> | false |
| title | string | '' |
| disabled | boolean | false |
<app-accordion [multi]="true">
<app-accordion-panel title="Section 1" [(expanded)]="sec1">
Content for section 1
</app-accordion-panel>
<app-accordion-panel title="Section 2" [disabled]="true">
Disabled content
</app-accordion-panel>
</app-accordion><app-sidebar>
Responsive sidebar with collapse, submenus, logout.
| Input | Type | Default |
|-------|------|---------|
| isOpen | boolean | false |
| isCollapsed | boolean | false |
| items | SidebarItem[] | [] |
| title | string | 'Menu' |
| width | string | '280px' |
| activeId | string \| null | null |
| Output | Emit Type |
|--------|-----------|
| toggleOpen | void |
| toggleCollapse | void |
| itemClick | SidebarItem |
| logout | void |
interface SidebarItem {
id: string;
label: string;
icon?: string;
route?: string;
children?: SidebarItem[];
}<app-sidebar
[isOpen]="sidebarOpen"
[isCollapsed]="sidebarCollapsed"
[items]="menuItems"
[activeId]="currentRoute"
(itemClick)="onNav($event)"
(toggleCollapse)="sidebarCollapsed = !sidebarCollapsed"
(logout)="onLogout()"
/><app-nav>
Top navbar with desktop dropdowns, mobile hamburger menu.
| Input | Type | Default |
|-------|------|---------|
| items | NavItem[] | [] |
| brandText | string | 'Brand' |
| brandIcon | string | 'pi pi-box' |
| fixed | boolean | true |
| Output | Emit Type |
|--------|-----------|
| logout | void |
interface NavItem {
id: string;
label: string;
icon?: string;
route?: string;
children?: NavItem[];
}<app-nav
[items]="navItems"
brandText="MyApp"
brandIcon="pi pi-cog"
(logout)="onLogout()"
/>Overlay
<app-modal>
Modal dialog with title slot, footer slot, close on backdrop/escape.
| Input | Type | Default |
|-------|------|---------|
| open | model<boolean> | false |
| title | string | '' |
| width | string | '' |
| config | ModalConfig | {} |
| Output | Emit Type |
|--------|-----------|
| closed | void |
Methods: close()
interface ModalConfig {
closeOnBackdrop?: boolean; // default true
closeOnEscape?: boolean; // default true
showCloseButton?: boolean; // default true
maxWidth?: string; // default '500px'
}<app-modal [(open)]="modalOpen" title="Edit User" [config]="{ maxWidth: '600px' }">
<p>Modal body content</p>
<ng-template #modalFooter>
<button (click)="save()">Save</button>
</ng-template>
</app-modal><app-confirm-dialog>
Pre-built confirmation dialog with customizable icon, accent, and button text.
| Input | Type | Default |
|-------|------|---------|
| open | model<boolean> | false |
| message | string | '' |
| config | ConfirmDialogConfig | {} |
| Output | Emit Type |
|--------|-----------|
| confirm | void |
| cancel | void |
interface ConfirmDialogConfig {
title?: string;
confirmText?: string;
cancelText?: string;
icon?: string;
confirmAccent?: ThemeColor | string;
closeOnBackdrop?: boolean;
closeOnEscape?: boolean;
}<app-confirm-dialog
[(open)]="confirmOpen"
message="Are you sure you want to delete this item?"
[config]="{ title: 'Confirm Delete', confirmAccent: 'danger', confirmText: 'Delete' }"
(confirm)="deleteItem()"
(cancel)="confirmOpen = false"
/>Shared Types
type FieldSize = 'sm' | 'md' | 'lg';
type ThemeColor = 'primary' | 'secondary' | 'accent' | 'success' | 'warning' | 'danger' | 'info';Development
# Build the library
npm run build
# Watch mode
npm run watch
# Publish
npm run publishBuild artifacts go to dist/rz-lib/.
Conventions
| Convention | Value |
|------------|-------|
| Change detection | ChangeDetectionStrategy.OnPush |
| State management | Angular signal() / model() / computed() / input() |
| Styling | Pure CSS custom properties — no @angular/animations |
| Theming | CSS variables with oklch fallbacks (light + dark) |
| Positioning | CSS position: fixed / absolute — no CDK overlay |
| Animation | @keyframes + setTimeout for close sequence |
| Panel pattern | Open: setTimeout(0) → enter animation. Close: _isClosing signal → wait 150ms → remove |
| document:click | Manually bound/unbound on open/close |
| Timer cleanup | _closeTimer field + ngOnDestroy |
| Field sizes | sm / md / lg |
| Accent colors | Named theme colors or any raw CSS color string |
Dependencies
| Dependency | Type | Required |
|------------|------|----------|
| @angular/core | peer | Yes |
| @angular/common | peer | Yes |
| primeicons | peer | Yes |
| exceljs | optional peer | Table export (XLSX) |
| jspdf | optional peer | Table export (PDF) |
| html2canvas | optional peer | Table export (PDF) |
License
MIT
