feat: add logo image and RTL support styles
- Added logo image to assets. - Implemented RTL support styles in rtlSupport.scss for various components including breadcrumb, datatable, and tree node toggle button. chore: configure environment files for production, staging, and development - Created environment.prod.ts for production settings. - Created environment.staging.ts for staging settings. - Created environment.ts for local development settings. feat: define custom theme preset - Added presets.ts to define a custom theme preset using PrimeUIX with specific component styles and semantic colors. chore: set up proxy configuration for local development
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
<div
|
||||
class="relative w-full flex items-center justify-center gap-2 cursor-pointer"
|
||||
(click)="copy($event)"
|
||||
aria-label="کپی"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="pi pi-copy text-lg"></i>
|
||||
</div>
|
||||
|
||||
{{ text }}
|
||||
<p-popover #op>کپی شد</p-popover>
|
||||
</div>
|
||||
@@ -0,0 +1,55 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, Input, ViewChild } from '@angular/core';
|
||||
import { PopoverModule } from 'primeng/popover';
|
||||
|
||||
@Component({
|
||||
selector: 'uikit-copy',
|
||||
standalone: true,
|
||||
imports: [CommonModule, PopoverModule],
|
||||
templateUrl: './copy.component.html',
|
||||
})
|
||||
export class UikitCopyComponent {
|
||||
@Input() text: string | null = null;
|
||||
@ViewChild('op') op: any;
|
||||
|
||||
copied = false;
|
||||
|
||||
async copy(event?: Event) {
|
||||
try {
|
||||
const payload = this.text ?? '';
|
||||
if (navigator && navigator.clipboard && payload !== '') {
|
||||
await navigator.clipboard.writeText(payload);
|
||||
this.showCopiedTooltip(event);
|
||||
}
|
||||
} catch (err) {
|
||||
// fallback: try execCommand (older browsers) or ignore
|
||||
try {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = this.text ?? '';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
this.showCopiedTooltip(event);
|
||||
} catch (e) {
|
||||
console.error('Copy failed', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private showCopiedTooltip(event?: Event) {
|
||||
this.copied = true;
|
||||
try {
|
||||
this.op?.show(event);
|
||||
} catch (e) {
|
||||
// ignore if popover not available
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
try {
|
||||
this.op?.hide();
|
||||
} catch (_) {}
|
||||
this.copied = false;
|
||||
}, 1400);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<uikit-field [label]="label" [control]="control" class="w-full datepicker">
|
||||
<div #wrapper [attr.data-disable]="disabled" [attr.data-name]="name" class="w-full">
|
||||
<input pInputText data-input [attr.placeholder]="placeholder" [name]="name" class="w-full" />
|
||||
</div>
|
||||
<div class="flat"></div>
|
||||
</uikit-field>
|
||||
|
||||
<!--
|
||||
|
||||
<div #wrapper class="flex items-center gap-2" data-enable-time="false" data-click-opens="true">
|
||||
<input pInputText class="w-full bg-white text-sm rounded border p-2" data-input [attr.placeholder]="placeholder" />
|
||||
|
||||
<button pButton type="button" class="p-button-text text-gray-500" data-toggle aria-label="toggle">
|
||||
<i class="pi pi-calendar"></i>
|
||||
</button>
|
||||
|
||||
<button pButton type="button" class="p-button-text text-gray-500" data-clear aria-label="clear">
|
||||
<i class="pi pi-times"></i>
|
||||
</button>
|
||||
</div> -->
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Maybe } from '@/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import flatpickr from 'flatpickr-wrap';
|
||||
import { BaseOptions } from 'flatpickr-wrap/dist/types/options';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { UikitFieldComponent } from '../uikit-field.component';
|
||||
|
||||
@Component({
|
||||
selector: 'uikit-datepicker',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule, InputTextModule, UikitFieldComponent],
|
||||
templateUrl: './datepicker.component.html',
|
||||
})
|
||||
export class UikitFlatpickrJalaliComponent implements OnInit {
|
||||
@Input() control?: FormControl<Maybe<string>>;
|
||||
@Input() placeholder = 'انتخاب تاریخ';
|
||||
@Input() label = 'تاریخ';
|
||||
@Input() disabled = false;
|
||||
@Input() name: string = 'datepicker';
|
||||
@Input() options: Partial<BaseOptions> = {};
|
||||
|
||||
@Output() valueChange = new EventEmitter<string>();
|
||||
|
||||
@ViewChild('wrapper', { static: true }) wrapperRef!: ElementRef<HTMLElement>;
|
||||
|
||||
private fpInstance: any | null = null;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.fpInstance = flatpickr(this.wrapperRef.nativeElement, {
|
||||
...this.options,
|
||||
altInputClass: 'p-inputtext w-full',
|
||||
onChange: (selectedDates: Date[], dateStr: string) => {
|
||||
this.valueChange.emit(dateStr);
|
||||
if (this.control) this.control.setValue(dateStr);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<uikit-field [label]="label" [name]="name" [control]="control">
|
||||
<div class="grid grid-cols-2 items-center gap-4">
|
||||
<p-autoComplete
|
||||
[formControl]="hoursCtrl"
|
||||
[suggestions]="filteredHours"
|
||||
(completeMethod)="filterHours($event)"
|
||||
[dropdown]="true"
|
||||
[forceSelection]="false"
|
||||
placeholder="ساعت"
|
||||
inputClass="w-full"
|
||||
type="number"
|
||||
></p-autoComplete>
|
||||
|
||||
<p-autoComplete
|
||||
[formControl]="minutesCtrl"
|
||||
[suggestions]="filteredMinutes"
|
||||
(completeMethod)="filterMinutes($event)"
|
||||
[dropdown]="true"
|
||||
[forceSelection]="false"
|
||||
placeholder="دقیقه"
|
||||
type="number"
|
||||
inputClass="w-full"
|
||||
></p-autoComplete>
|
||||
</div>
|
||||
@if (hoursCtrl.value || minutesCtrl.value) {
|
||||
<span class="text-xs mt-1 text-muted-color">
|
||||
مدت زمان آزمون را {{ hoursCtrl.value || 0 }} ساعت و {{ minutesCtrl.value || 0 }} دقیقه انتخاب کردید.
|
||||
</span>
|
||||
}
|
||||
</uikit-field>
|
||||
@@ -0,0 +1,93 @@
|
||||
import { UikitFieldComponent } from '@/uikit/uikit-field.component';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { AutoCompleteModule } from 'primeng/autocomplete';
|
||||
|
||||
@Component({
|
||||
selector: 'uikit-duration',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule, AutoCompleteModule, UikitFieldComponent],
|
||||
templateUrl: './duration-picker.component.html',
|
||||
})
|
||||
export class UikitDurationPickerComponent implements OnInit {
|
||||
@Input() label = 'مدت زمان';
|
||||
@Input() name = 'duration';
|
||||
@Input() control!: FormControl<string | null>;
|
||||
|
||||
hoursCtrl = new FormControl<string | null>('0');
|
||||
minutesCtrl = new FormControl<string | null>('0');
|
||||
|
||||
hoursOptions: string[] = Array.from({ length: 24 }, (_, i) => String(i));
|
||||
minutesOptions: string[] = ['0', '5', '10', '15', '20', '30', '45'];
|
||||
|
||||
filteredHours: string[] = [];
|
||||
filteredMinutes: string[] = [];
|
||||
|
||||
ngOnInit(): void {
|
||||
// initialize from parent control if present
|
||||
if (this.control) {
|
||||
const total = Number(this.control.value) || 0;
|
||||
const h = Math.floor(total / 60);
|
||||
const m = total % 60;
|
||||
this.hoursCtrl.setValue(String(h));
|
||||
this.minutesCtrl.setValue(String(m));
|
||||
|
||||
this.hoursCtrl.valueChanges.subscribe(() => {
|
||||
if (this.toNumberSafe(this.hoursCtrl.value) > 23) {
|
||||
this.hoursCtrl.setValue('23', { emitEvent: false });
|
||||
}
|
||||
this.syncToParent();
|
||||
});
|
||||
this.minutesCtrl.valueChanges.subscribe(() => {
|
||||
if (this.toNumberSafe(this.minutesCtrl.value) > 59) {
|
||||
this.minutesCtrl.setValue('59', { emitEvent: false });
|
||||
}
|
||||
this.syncToParent();
|
||||
});
|
||||
|
||||
this.control.valueChanges.subscribe((v) => this.syncFromParent(v));
|
||||
} else {
|
||||
this.hoursCtrl.valueChanges.subscribe(() => this.syncToParent());
|
||||
this.minutesCtrl.valueChanges.subscribe(() => this.syncToParent());
|
||||
}
|
||||
}
|
||||
|
||||
private toNumberSafe(s: string | null | undefined) {
|
||||
if (s == null || s === '') return 0;
|
||||
const n = Number(s);
|
||||
return Number.isFinite(n) ? Math.max(0, Math.trunc(n)) : 0;
|
||||
}
|
||||
|
||||
syncToParent() {
|
||||
if (!this.control) return;
|
||||
const h = this.toNumberSafe(this.hoursCtrl.value);
|
||||
const m = this.toNumberSafe(this.minutesCtrl.value);
|
||||
const total = h * 60 + m;
|
||||
// write numeric minutes into parent control only when changed
|
||||
const current = this.control.value;
|
||||
if (String(current) !== String(total)) {
|
||||
this.control.setValue(String(total), { emitEvent: false });
|
||||
}
|
||||
}
|
||||
|
||||
syncFromParent(v: any) {
|
||||
const total = Number(v) || 0;
|
||||
const h = Math.floor(total / 60);
|
||||
const m = total % 60;
|
||||
if (String(h) !== this.hoursCtrl.value)
|
||||
this.hoursCtrl.setValue(String(h), { emitEvent: false });
|
||||
if (String(m) !== this.minutesCtrl.value)
|
||||
this.minutesCtrl.setValue(String(m), { emitEvent: false });
|
||||
}
|
||||
|
||||
filterHours(event: { query: string }) {
|
||||
const q = (event.query || '').toString();
|
||||
this.filteredHours = this.hoursOptions.filter((x) => x.startsWith(q));
|
||||
}
|
||||
|
||||
filterMinutes(event: { query: string }) {
|
||||
const q = (event.query || '').toString();
|
||||
this.filteredMinutes = this.minutesOptions.filter((x) => x.startsWith(q));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export * from './copy/copy.component';
|
||||
export * from './datepicker/datepicker.component';
|
||||
export * from './duration-picker/duration-picker.component';
|
||||
export * from './paginator.component';
|
||||
export * from './uikit-emptystate.component';
|
||||
export * from './uikit-field.component';
|
||||
export * from './uikit-label.component';
|
||||
@@ -0,0 +1,19 @@
|
||||
<div class="flex items-center justify-center gap-3 p-4">
|
||||
<button
|
||||
pButton
|
||||
icon="pi pi-angle-right"
|
||||
size="small"
|
||||
severity="secondary"
|
||||
[disabled]="loading || currentPage === 1"
|
||||
(click)="prevPage()"
|
||||
></button>
|
||||
<span class="text-pretty text-sm text-muted-color">صفحه {{ currentPage }} از {{ totalPages() }}</span>
|
||||
<button
|
||||
pButton
|
||||
icon="pi pi-angle-left"
|
||||
size="small"
|
||||
severity="secondary"
|
||||
[disabled]="loading || currentPage >= totalPages()"
|
||||
(click)="nextPage()"
|
||||
></button>
|
||||
</div>
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Component, EventEmitter, Input, Output, signal } from '@angular/core';
|
||||
import { ButtonModule } from 'primeng/button';
|
||||
|
||||
@Component({
|
||||
selector: 'app-paginator',
|
||||
templateUrl: './paginator.component.html',
|
||||
imports: [ButtonModule],
|
||||
})
|
||||
export class PaginatorComponent {
|
||||
@Input() totalRecords!: number;
|
||||
@Input() currentPage: number = 1;
|
||||
@Input() perPage: number = 10;
|
||||
@Input() loading: boolean = false;
|
||||
@Output() onChange = new EventEmitter<number>();
|
||||
|
||||
totalPages = signal<number>(0);
|
||||
|
||||
ngOnChanges() {
|
||||
this.totalPages.set(Math.ceil(this.totalRecords / this.perPage));
|
||||
}
|
||||
|
||||
onPageChange(newPage: number) {
|
||||
this.onChange.emit(newPage);
|
||||
}
|
||||
|
||||
prevPage() {
|
||||
if (this.currentPage > 1) {
|
||||
this.onPageChange(this.currentPage - 1);
|
||||
}
|
||||
}
|
||||
nextPage() {
|
||||
if (this.currentPage < this.totalPages()) {
|
||||
this.onPageChange(this.currentPage + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<div class="flex flex-col gap-3">
|
||||
<uikit-field [label]="label" [control]="control" class="w-full">
|
||||
<p-inputGroup>
|
||||
<input
|
||||
pInputText
|
||||
[formControl]="control"
|
||||
[placeholder]="placeholder"
|
||||
[readonly]="searchedLoading()"
|
||||
[minLength]="6"
|
||||
[maxLength]="6"
|
||||
[invalid]="control.touched && (control.invalid || !searchedItem())"
|
||||
/>
|
||||
@if (searchedLoading()) {
|
||||
<p-inputGroupAddon>
|
||||
<p-progressSpinner class="!w-4 !h-4"></p-progressSpinner>
|
||||
</p-inputGroupAddon>
|
||||
}
|
||||
</p-inputGroup>
|
||||
</uikit-field>
|
||||
@if (searchedLoading()) {
|
||||
<div class="flex items-center justify-center">
|
||||
<p-skeleton />
|
||||
</div>
|
||||
} @else if (searchedItem()) {
|
||||
<p-message severity="success" [closable]="false" variant="text" icon="pi pi-check-circle">
|
||||
{{ foundedMessage(searchedItem()!) }}
|
||||
</p-message>
|
||||
} @else if (control.value!.length === 6 && itemNotFound()) {
|
||||
<p-message severity="error" [closable]="false" variant="text" icon="pi pi-times-circle">
|
||||
{{ notFoundMessage }}
|
||||
</p-message>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Maybe } from '@/core';
|
||||
import { Component, EventEmitter, Input, Output, signal } from '@angular/core';
|
||||
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { InputGroup } from 'primeng/inputgroup';
|
||||
import { InputGroupAddon } from 'primeng/inputgroupaddon';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { Message } from 'primeng/message';
|
||||
import { ProgressSpinner } from 'primeng/progressspinner';
|
||||
import { Skeleton } from 'primeng/skeleton';
|
||||
import { debounceTime, filter, Observable } from 'rxjs';
|
||||
import { UikitFieldComponent } from '../uikit-field.component';
|
||||
|
||||
@Component({
|
||||
selector: 'uikit-search-field',
|
||||
templateUrl: './search-field.component.html',
|
||||
imports: [
|
||||
UikitFieldComponent,
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
ProgressSpinner,
|
||||
Skeleton,
|
||||
Message,
|
||||
InputTextModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
})
|
||||
export class UikitSearchFieldComponent<T> {
|
||||
@Input() label!: string;
|
||||
@Input() placeholder: string = 'جستجو...';
|
||||
@Input() debounceTime: number = 300;
|
||||
@Input() notFoundMessage: string = 'موردی یافت نشد.';
|
||||
@Input() foundedMessage: (item: T) => string = () => 'مورد یافت شد.';
|
||||
@Input() search!: (query: string) => Observable<Maybe<T>>;
|
||||
|
||||
@Output() searchResult = new EventEmitter<Maybe<T>>();
|
||||
|
||||
control = new FormControl<string>('');
|
||||
|
||||
constructor() {
|
||||
this.control.valueChanges
|
||||
.pipe(
|
||||
debounceTime(this.debounceTime),
|
||||
filter((val) => !!val && val.length === 6),
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.searchedLoading.set(true);
|
||||
this.search(this.control.value!).subscribe({
|
||||
next: (item) => {
|
||||
this.searchedItem.set(item);
|
||||
this.searchedLoading.set(false);
|
||||
this.itemNotFound.set(!item);
|
||||
this.searchResult.emit(item);
|
||||
},
|
||||
error: () => {
|
||||
this.searchedLoading.set(false);
|
||||
this.itemNotFound.set(true);
|
||||
this.searchResult.emit(null);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
submitLoading = signal(false);
|
||||
searchedLoading = signal(false);
|
||||
searchedItem = signal<Maybe<T>>(null);
|
||||
itemNotFound = signal(false);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<!-- Table Header Cell -->
|
||||
@if (type === "th") {
|
||||
<th [class]="widthClass" class="text-center">عملیات</th>
|
||||
}
|
||||
|
||||
<!-- Table Body Cell -->
|
||||
@if (type === "td") {
|
||||
<td [class]="widthClass" class="text-center flex items-center gap-1">
|
||||
@if (showEdit) {
|
||||
<p-button (click)="edit.emit()" title="ویرایش">
|
||||
<i class="pi pi-pencil"></i>
|
||||
</p-button>
|
||||
}
|
||||
@if (showDelete) {
|
||||
<p-button (click)="delete.emit()" title="حذف">
|
||||
<i class="pi pi-trash"></i>
|
||||
</p-button>
|
||||
}
|
||||
@if (showDetails) {
|
||||
<p-button (click)="details.emit()" title="جزئیات">
|
||||
<i class="pi pi-info-circle"></i>
|
||||
</p-button>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<div class="flex flex-col items-center justify-center py-12 text-center bg-surface-overlay">
|
||||
<ng-container>
|
||||
<i [class]="icon + ' !text-6xl mb-4 text-surface-500'" aria-hidden="true"></i>
|
||||
</ng-container>
|
||||
<span class="text-xl font-semibold mb-2 text-surface-500">{{ title }}</span>
|
||||
@if (description) {
|
||||
<p class="text-surface-500 inline-block">{{ description }}</p>
|
||||
}
|
||||
<ng-content select="[cta]" #cta class="mt-4"></ng-content>
|
||||
@if (!hasCtaContent && ctaLabel && showCTA) {
|
||||
<p-button class="mt-2" size="small" (click)="ctaClick.emit()">
|
||||
{{ ctaLabel }}
|
||||
<i class="pi pi-plus" pButtonIcon></i>
|
||||
</p-button>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
AfterContentInit,
|
||||
Component,
|
||||
ContentChild,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Button } from 'primeng/button';
|
||||
|
||||
@Component({
|
||||
selector: 'uikit-empty-state',
|
||||
standalone: true,
|
||||
imports: [CommonModule, Button],
|
||||
templateUrl: './uikit-emptystate.component.html',
|
||||
})
|
||||
export class UikitEmptyStateComponent implements AfterContentInit {
|
||||
@ContentChild('cta', { static: false, read: ElementRef })
|
||||
ctaContent?: ElementRef;
|
||||
hasCtaContent = false;
|
||||
|
||||
@Input() title: string = 'متاسفانه موردی یافت نشد.';
|
||||
@Input() description?: string;
|
||||
@Input() icon: string = 'pi pi-database';
|
||||
@Input() ctaLabel?: string;
|
||||
@Input() showCTA?: boolean = true;
|
||||
|
||||
@Output() ctaClick = new EventEmitter<void>();
|
||||
|
||||
ngAfterContentInit() {
|
||||
this.hasCtaContent = !!this.ctaContent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<div class="flex flex-col gap-1">
|
||||
<uikit-label [name]="name" [invalid]="!!invalid || showErrors">
|
||||
{{ label }}
|
||||
</uikit-label>
|
||||
<ng-content></ng-content>
|
||||
@if (showErrors) {
|
||||
<ng-container>
|
||||
<p-message severity="error" size="small" variant="simple" class="formHint">
|
||||
{{ errors[0] }}
|
||||
</p-message>
|
||||
</ng-container>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,36 @@
|
||||
import { FormErrorsService } from '@/core/services/form-errors.service';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, Input, inject } from '@angular/core';
|
||||
import { AbstractControl } from '@angular/forms';
|
||||
import { MessageModule } from 'primeng/message';
|
||||
import { UikitLabelComponent } from './uikit-label.component';
|
||||
|
||||
type PSize = 'small' | 'normal' | 'large';
|
||||
|
||||
@Component({
|
||||
selector: 'uikit-field',
|
||||
standalone: true,
|
||||
imports: [CommonModule, UikitLabelComponent, MessageModule],
|
||||
templateUrl: './uikit-field.component.html',
|
||||
})
|
||||
export class UikitFieldComponent {
|
||||
@Input() label!: string;
|
||||
@Input() name!: string;
|
||||
@Input() pSize: PSize = 'normal';
|
||||
@Input() className?: string;
|
||||
@Input() invalid?: boolean = false;
|
||||
// accept a FormControl or AbstractControl to display errors for
|
||||
@Input() control?: AbstractControl | null;
|
||||
|
||||
private errorsService = inject(FormErrorsService);
|
||||
|
||||
get showErrors(): boolean {
|
||||
if (!this.control) return !!this.invalid;
|
||||
return !!(this.control.invalid && (this.control.touched || this.control.dirty));
|
||||
}
|
||||
|
||||
get errors(): string[] {
|
||||
if (!this.control) return this.invalid ? ['خطا'] : [];
|
||||
return this.errorsService.getErrors(this.control, this.label).map((m) => m.message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, Input } from '@angular/core';
|
||||
|
||||
type PSize = 'small' | 'normal' | 'large';
|
||||
|
||||
@Component({
|
||||
selector: 'uikit-label',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
template: `
|
||||
<label [attr.for]="name" [ngClass]="computedClass" [attr.class]="className" class="select-none">
|
||||
<ng-content></ng-content>
|
||||
</label>
|
||||
`,
|
||||
})
|
||||
export class UikitLabelComponent {
|
||||
@Input() name?: string;
|
||||
@Input() pSize: PSize = 'normal';
|
||||
@Input() className?: string;
|
||||
@Input() invalid?: boolean;
|
||||
|
||||
private sizeMap: Record<PSize, string> = {
|
||||
small: 'text-sm',
|
||||
normal: 'text-md',
|
||||
large: 'text-lg',
|
||||
};
|
||||
|
||||
get computedClass(): string {
|
||||
// default styling used elsewhere in the app for labels
|
||||
const base = 'block text-muted-color font-medium';
|
||||
const size = this.sizeMap[this.pSize] ?? this.sizeMap.normal;
|
||||
const errorClass = this.invalid ? 'text-red-600' : '';
|
||||
return `${base} ${size} ${errorClass}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user