UNPKG

juvo-rafa-library

Version:

A comprehensive Angular component library featuring real-world components and validators extracted from the Juvo Rafa backoffice application. Now with improved select components and bug fixes.

716 lines (711 loc) 138 kB
import * as i0 from '@angular/core'; import { EventEmitter, Output, Input, Component, forwardRef } from '@angular/core'; import * as i1 from '@angular/common'; import { CommonModule } from '@angular/common'; import { Observable, combineLatest, map, of } from 'rxjs'; import * as i2 from '@angular/forms'; import { ReactiveFormsModule, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; /** * Action Button Group Component * * @description * A reusable button group component commonly used in forms and data entry screens. * Provides standard actions like Save, Create, Create Copy, Save Copy, and Cancel. * Originally designed for backoffice applications where consistent form actions are needed. * * @example * ```html * <!-- Basic usage --> * <juvo-button-action * [buttonSave]="true" * [buttonCreate]="true" * (actionSaveTriggered)="onSave()" * (actionCreateTriggered)="onCreate()" * (actionCancelTriggered)="onCancel()"> * </juvo-button-action> * * <!-- Advanced usage with loading state --> * <juvo-button-action * [loading$]="loadingObservable$" * [buttonSave]="true" * [buttonCreateCopy]="true" * [buttonType]="true" * [disable]="!form.valid" * saveText="Update Record" * (actionSaveTriggered)="onUpdate()" * (actionCreateCopyTriggered)="onDuplicate()"> * </juvo-button-action> * ``` * * @selector juvo-button-action * @since 2.0.0 * @author Juvo Rafa Team */ class JuvoButtonActionComponent { constructor() { /** Configuration object for button visibility and behavior */ this.config = {}; /** Disable all action buttons */ this.disable = false; /** Custom text for Save button @default "Save" */ this.saveText = "Save"; /** Button style: true for solid, false for outlined @default false */ this.buttonType = false; /** Emitted when Save button is clicked */ this.actionSaveTriggered = new EventEmitter(); /** Emitted when Create button is clicked */ this.actionCreateTriggered = new EventEmitter(); /** Emitted when Create and Copy button is clicked */ this.actionCreateCopyTriggered = new EventEmitter(); /** Emitted when Save and Copy button is clicked */ this.actionSaveCopyTriggered = new EventEmitter(); /** Emitted when Cancel button is clicked */ this.actionCancelTriggered = new EventEmitter(); } /** * Gets the current loading state * @returns true if loading, false otherwise */ get isLoading() { return this.loading$ ? false : false; // Simplified for now } /** * Handles Save button click * @emits actionSaveTriggered */ onSave() { this.actionSaveTriggered.emit(); } /** * Handles Create button click * @emits actionCreateTriggered */ onCreate() { this.actionCreateTriggered.emit(); } /** * Handles Create and Copy button click * @emits actionCreateCopyTriggered */ onCreateCopy() { this.actionCreateCopyTriggered.emit(); } /** * Handles Save and Copy button click * @emits actionSaveCopyTriggered */ onSaveCopy() { this.actionSaveCopyTriggered.emit(); } /** * Handles Cancel button click * @emits actionCancelTriggered */ onCancel() { this.actionCancelTriggered.emit(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: JuvoButtonActionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: JuvoButtonActionComponent, isStandalone: true, selector: "juvo-button-action", inputs: { loading$: "loading$", config: "config", buttonCreate: "buttonCreate", buttonCreateCopy: "buttonCreateCopy", buttonSaveCopy: "buttonSaveCopy", buttonSave: "buttonSave", disable: "disable", saveText: "saveText", buttonType: "buttonType" }, outputs: { actionSaveTriggered: "actionSaveTriggered", actionCreateTriggered: "actionCreateTriggered", actionCreateCopyTriggered: "actionCreateCopyTriggered", actionSaveCopyTriggered: "actionSaveCopyTriggered", actionCancelTriggered: "actionCancelTriggered" }, ngImport: i0, template: "<div class=\"juvo-button-action\">\n <button\n type=\"button\"\n class=\"btn btn-outlined\"\n (click)=\"onCancel()\">\n Cancel\n </button>\n\n <button\n *ngIf=\"buttonCreateCopy\"\n type=\"button\"\n class=\"btn btn-outlined\"\n [disabled]=\"isLoading\"\n (click)=\"onCreateCopy()\">\n <span *ngIf=\"isLoading\" class=\"spinner\"></span>\n Create and copy\n </button>\n\n <button\n *ngIf=\"buttonSaveCopy\"\n type=\"button\"\n class=\"btn btn-outlined\"\n [disabled]=\"isLoading\"\n (click)=\"onSaveCopy()\">\n <span *ngIf=\"isLoading\" class=\"spinner\"></span>\n Save and copy\n </button>\n\n <button\n *ngIf=\"buttonCreate\"\n type=\"button\"\n class=\"btn btn-outlined\"\n [disabled]=\"disable || isLoading\"\n (click)=\"onCreate()\">\n <span *ngIf=\"isLoading\" class=\"spinner\"></span>\n Create\n </button>\n \n <button\n *ngIf=\"buttonSave\"\n type=\"button\"\n [disabled]=\"disable || isLoading\"\n [class]=\"buttonType ? 'btn btn-primary' : 'btn btn-outlined'\"\n (click)=\"onSave()\">\n <span *ngIf=\"isLoading\" class=\"spinner\"></span>\n {{ saveText }}\n </button>\n</div> ", styles: [".juvo-button-action{display:flex;justify-content:flex-end;flex-direction:column;gap:.5rem}@media (min-width: 768px){.juvo-button-action{flex-direction:row}}.btn{padding:.75rem 1.5rem;border-radius:.375rem;font-weight:500;cursor:pointer;transition:all .15s;display:inline-flex;align-items:center;justify-content:center;gap:.5rem;width:100%;border:1px solid}@media (min-width: 768px){.btn{width:auto}}.btn-primary{background:#3b82f6;color:#fff;border-color:#3b82f6}.btn-primary:hover:not(:disabled){background:#2563eb;border-color:#2563eb}.btn-outlined{background:transparent;color:#6b7280;border-color:#d1d5db}.btn-outlined:hover:not(:disabled){background:#f9fafb;border-color:#9ca3af}.btn:disabled{opacity:.5;cursor:not-allowed}.spinner{width:1rem;height:1rem;border:2px solid transparent;border-top:2px solid currentColor;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: JuvoButtonActionComponent, decorators: [{ type: Component, args: [{ selector: 'juvo-button-action', standalone: true, imports: [CommonModule], template: "<div class=\"juvo-button-action\">\n <button\n type=\"button\"\n class=\"btn btn-outlined\"\n (click)=\"onCancel()\">\n Cancel\n </button>\n\n <button\n *ngIf=\"buttonCreateCopy\"\n type=\"button\"\n class=\"btn btn-outlined\"\n [disabled]=\"isLoading\"\n (click)=\"onCreateCopy()\">\n <span *ngIf=\"isLoading\" class=\"spinner\"></span>\n Create and copy\n </button>\n\n <button\n *ngIf=\"buttonSaveCopy\"\n type=\"button\"\n class=\"btn btn-outlined\"\n [disabled]=\"isLoading\"\n (click)=\"onSaveCopy()\">\n <span *ngIf=\"isLoading\" class=\"spinner\"></span>\n Save and copy\n </button>\n\n <button\n *ngIf=\"buttonCreate\"\n type=\"button\"\n class=\"btn btn-outlined\"\n [disabled]=\"disable || isLoading\"\n (click)=\"onCreate()\">\n <span *ngIf=\"isLoading\" class=\"spinner\"></span>\n Create\n </button>\n \n <button\n *ngIf=\"buttonSave\"\n type=\"button\"\n [disabled]=\"disable || isLoading\"\n [class]=\"buttonType ? 'btn btn-primary' : 'btn btn-outlined'\"\n (click)=\"onSave()\">\n <span *ngIf=\"isLoading\" class=\"spinner\"></span>\n {{ saveText }}\n </button>\n</div> ", styles: [".juvo-button-action{display:flex;justify-content:flex-end;flex-direction:column;gap:.5rem}@media (min-width: 768px){.juvo-button-action{flex-direction:row}}.btn{padding:.75rem 1.5rem;border-radius:.375rem;font-weight:500;cursor:pointer;transition:all .15s;display:inline-flex;align-items:center;justify-content:center;gap:.5rem;width:100%;border:1px solid}@media (min-width: 768px){.btn{width:auto}}.btn-primary{background:#3b82f6;color:#fff;border-color:#3b82f6}.btn-primary:hover:not(:disabled){background:#2563eb;border-color:#2563eb}.btn-outlined{background:transparent;color:#6b7280;border-color:#d1d5db}.btn-outlined:hover:not(:disabled){background:#f9fafb;border-color:#9ca3af}.btn:disabled{opacity:.5;cursor:not-allowed}.spinner{width:1rem;height:1rem;border:2px solid transparent;border-top:2px solid currentColor;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}\n"] }] }], propDecorators: { loading$: [{ type: Input }], config: [{ type: Input }], buttonCreate: [{ type: Input }], buttonCreateCopy: [{ type: Input }], buttonSaveCopy: [{ type: Input }], buttonSave: [{ type: Input }], disable: [{ type: Input }], saveText: [{ type: Input }], buttonType: [{ type: Input }], actionSaveTriggered: [{ type: Output }], actionCreateTriggered: [{ type: Output }], actionCreateCopyTriggered: [{ type: Output }], actionSaveCopyTriggered: [{ type: Output }], actionCancelTriggered: [{ type: Output }] } }); /** * Loading Component * * @description * A flexible loading indicator component that can be used inline or as an overlay. * Originally designed for backoffice applications to provide consistent loading states. * Supports different sizes, custom colors, and overlay modes. * * @example * ```html * <!-- Inline loading --> * <juvo-loading * message="Loading data..." * size="medium" * [overlay]="false"> * </juvo-loading> * * <!-- Overlay loading --> * <juvo-loading * *ngIf="isLoading" * message="Please wait..." * size="large" * [overlay]="true"> * </juvo-loading> * * <!-- Custom styled loading --> * <juvo-loading * message="Processing..." * size="small" * color="#10b981" * [transparent]="true"> * </juvo-loading> * ``` * * @selector juvo-loading * @since 2.0.0 * @author Juvo Rafa Team */ class JuvoLoadingComponent { constructor() { /** Loading message to display @default "Loading..." */ this.message = 'Loading...'; /** Size of the loading spinner @default "medium" */ this.size = 'medium'; /** Color of the spinner @default "#3b82f6" */ this.color = '#3b82f6'; /** Whether to display as overlay covering the entire screen @default true */ this.overlay = true; /** Whether the overlay background should be transparent @default false */ this.transparent = false; } /** * Gets the CSS class for the spinner size * @returns CSS class name for the current size */ get sizeClass() { return `loading-${this.size}`; } /** * Gets the CSS classes for the container * @returns Combined CSS classes for the container */ get containerClass() { let classes = 'juvo-loading'; if (this.overlay) classes += ' overlay'; if (this.transparent) classes += ' transparent'; return classes; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: JuvoLoadingComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: JuvoLoadingComponent, isStandalone: true, selector: "juvo-loading", inputs: { message: "message", size: "size", color: "color", overlay: "overlay", transparent: "transparent" }, ngImport: i0, template: "<div [class]=\"containerClass\">\n <div class=\"loading-content\">\n <div [class]=\"sizeClass\" class=\"loading-spinner\">\n <div class=\"spinner-circle\" [style.border-top-color]=\"color\"></div>\n </div>\n <p class=\"loading-message\" *ngIf=\"message\">{{ message }}</p>\n </div>\n</div> ", styles: [".juvo-loading{display:flex;align-items:center;justify-content:center;padding:2rem}.juvo-loading.overlay{position:fixed;inset:0;background:#ffffffe6;z-index:9999}.juvo-loading.transparent{background:transparent}.loading-content{display:flex;flex-direction:column;align-items:center;gap:1rem}.loading-spinner{display:flex;align-items:center;justify-content:center}.spinner-circle{border:3px solid #f3f4f6;border-top:3px solid #3b82f6;border-radius:50%;animation:spin 1s linear infinite}.loading-small .spinner-circle{width:1.5rem;height:1.5rem;border-width:2px}.loading-medium .spinner-circle{width:2.5rem;height:2.5rem;border-width:3px}.loading-large .spinner-circle{width:4rem;height:4rem;border-width:4px}.loading-message{margin:0;color:#6b7280;font-size:.875rem;text-align:center}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: JuvoLoadingComponent, decorators: [{ type: Component, args: [{ selector: 'juvo-loading', standalone: true, imports: [CommonModule], template: "<div [class]=\"containerClass\">\n <div class=\"loading-content\">\n <div [class]=\"sizeClass\" class=\"loading-spinner\">\n <div class=\"spinner-circle\" [style.border-top-color]=\"color\"></div>\n </div>\n <p class=\"loading-message\" *ngIf=\"message\">{{ message }}</p>\n </div>\n</div> ", styles: [".juvo-loading{display:flex;align-items:center;justify-content:center;padding:2rem}.juvo-loading.overlay{position:fixed;inset:0;background:#ffffffe6;z-index:9999}.juvo-loading.transparent{background:transparent}.loading-content{display:flex;flex-direction:column;align-items:center;gap:1rem}.loading-spinner{display:flex;align-items:center;justify-content:center}.spinner-circle{border:3px solid #f3f4f6;border-top:3px solid #3b82f6;border-radius:50%;animation:spin 1s linear infinite}.loading-small .spinner-circle{width:1.5rem;height:1.5rem;border-width:2px}.loading-medium .spinner-circle{width:2.5rem;height:2.5rem;border-width:3px}.loading-large .spinner-circle{width:4rem;height:4rem;border-width:4px}.loading-message{margin:0;color:#6b7280;font-size:.875rem;text-align:center}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}\n"] }] }], propDecorators: { message: [{ type: Input }], size: [{ type: Input }], color: [{ type: Input }], overlay: [{ type: Input }], transparent: [{ type: Input }] } }); /** * Screen Loading Component * * @description * A screen-wide loading overlay component for blocking UI interactions during operations. * Originally designed for backoffice applications to handle multiple loading states. * Can combine multiple observables to show loading when any operation is in progress. * * @example * ```html * <!-- Single loading state --> * <juvo-screen-loading * [loading$]="dataLoading$" * message="Loading data..."> * </juvo-screen-loading> * * <!-- Multiple loading states --> * <juvo-screen-loading * [loadingList$]="[saveLoading$, deleteLoading$, updateLoading$]" * message="Processing operations..." * [backdrop]="true"> * </juvo-screen-loading> * * <!-- Custom styled loading --> * <juvo-screen-loading * [loading$]="criticalOperation$" * message="Critical operation in progress. Please do not refresh the page." * color="#dc2626" * size="large"> * </juvo-screen-loading> * ``` * * @selector juvo-screen-loading * @since 2.1.0 * @author Juvo Rafa Team */ class JuvoScreenLoadingComponent { constructor() { /** Array of loading observables to combine */ this.loadingList$ = []; /** Loading message to display @default "Loading..." */ this.message = 'Loading...'; /** Size of the loading spinner @default "large" */ this.size = 'large'; /** Color of the spinner @default "#3b82f6" */ this.color = '#3b82f6'; /** Whether to show backdrop @default true */ this.backdrop = true; /** Backdrop opacity @default 0.8 */ this.backdropOpacity = 0.8; /** Whether to show the loading message @default true */ this.showMessage = true; } /** * Gets the combined loading state from all sources * @returns Observable<boolean> indicating if any loading is in progress */ get combinedLoading$() { const observables = []; if (this.loading$) { observables.push(this.loading$); } if (this.loadingList$ && this.loadingList$.length > 0) { observables.push(...this.loadingList$); } if (observables.length === 0) { return new Observable(subscriber => subscriber.next(false)); } if (observables.length === 1) { return observables[0]; } return combineLatest(observables).pipe(map(loadingStates => loadingStates.some(loading => loading))); } /** * Gets the CSS class for the spinner size * @returns CSS class name for the current size */ get sizeClass() { return `spinner-${this.size}`; } /** * Gets the backdrop style * @returns CSS style object for backdrop */ get backdropStyle() { return { 'background-color': `rgba(255, 255, 255, ${this.backdropOpacity})` }; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: JuvoScreenLoadingComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: JuvoScreenLoadingComponent, isStandalone: true, selector: "juvo-screen-loading", inputs: { loading$: "loading$", loadingList$: "loadingList$", message: "message", size: "size", color: "color", backdrop: "backdrop", backdropOpacity: "backdropOpacity", showMessage: "showMessage" }, ngImport: i0, template: "<div class=\"screen-loading-overlay\" \n *ngIf=\"combinedLoading$ | async\"\n [class.with-backdrop]=\"backdrop\"\n [ngStyle]=\"backdrop ? backdropStyle : null\">\n \n <div class=\"screen-loading-content\">\n <div class=\"spinner-container\">\n <div [class]=\"sizeClass\" \n class=\"loading-spinner\">\n <div class=\"spinner-circle\" \n [style.border-top-color]=\"color\"\n [style.border-right-color]=\"color\">\n </div>\n </div>\n </div>\n \n <div class=\"loading-message\" \n *ngIf=\"showMessage && message\">\n {{ message }}\n </div>\n </div>\n</div> ", styles: [".screen-loading-overlay{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;z-index:9999}.screen-loading-overlay.with-backdrop{background-color:#fffc;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.screen-loading-content{display:flex;flex-direction:column;align-items:center;gap:1.5rem;text-align:center;padding:2rem;border-radius:.5rem;background:#fffffff2;box-shadow:0 4px 12px #0000001a;min-width:200px}.spinner-container,.loading-spinner{display:flex;align-items:center;justify-content:center}.spinner-circle{border:4px solid #f3f4f6;border-top:4px solid #3b82f6;border-right:4px solid #3b82f6;border-radius:50%;animation:spin 1s linear infinite}.spinner-small .spinner-circle{width:2rem;height:2rem;border-width:3px}.spinner-medium .spinner-circle{width:3rem;height:3rem;border-width:4px}.spinner-large .spinner-circle{width:4rem;height:4rem;border-width:5px}.loading-message{color:#374151;font-size:1rem;font-weight:500;max-width:300px;line-height:1.5}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@media (max-width: 640px){.screen-loading-content{margin:1rem;padding:1.5rem;min-width:auto;max-width:calc(100vw - 2rem)}.loading-message{font-size:.875rem;max-width:250px}.spinner-large .spinner-circle{width:3rem;height:3rem;border-width:4px}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: JuvoScreenLoadingComponent, decorators: [{ type: Component, args: [{ selector: 'juvo-screen-loading', standalone: true, imports: [CommonModule], template: "<div class=\"screen-loading-overlay\" \n *ngIf=\"combinedLoading$ | async\"\n [class.with-backdrop]=\"backdrop\"\n [ngStyle]=\"backdrop ? backdropStyle : null\">\n \n <div class=\"screen-loading-content\">\n <div class=\"spinner-container\">\n <div [class]=\"sizeClass\" \n class=\"loading-spinner\">\n <div class=\"spinner-circle\" \n [style.border-top-color]=\"color\"\n [style.border-right-color]=\"color\">\n </div>\n </div>\n </div>\n \n <div class=\"loading-message\" \n *ngIf=\"showMessage && message\">\n {{ message }}\n </div>\n </div>\n</div> ", styles: [".screen-loading-overlay{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;z-index:9999}.screen-loading-overlay.with-backdrop{background-color:#fffc;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.screen-loading-content{display:flex;flex-direction:column;align-items:center;gap:1.5rem;text-align:center;padding:2rem;border-radius:.5rem;background:#fffffff2;box-shadow:0 4px 12px #0000001a;min-width:200px}.spinner-container,.loading-spinner{display:flex;align-items:center;justify-content:center}.spinner-circle{border:4px solid #f3f4f6;border-top:4px solid #3b82f6;border-right:4px solid #3b82f6;border-radius:50%;animation:spin 1s linear infinite}.spinner-small .spinner-circle{width:2rem;height:2rem;border-width:3px}.spinner-medium .spinner-circle{width:3rem;height:3rem;border-width:4px}.spinner-large .spinner-circle{width:4rem;height:4rem;border-width:5px}.loading-message{color:#374151;font-size:1rem;font-weight:500;max-width:300px;line-height:1.5}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@media (max-width: 640px){.screen-loading-content{margin:1rem;padding:1.5rem;min-width:auto;max-width:calc(100vw - 2rem)}.loading-message{font-size:.875rem;max-width:250px}.spinner-large .spinner-circle{width:3rem;height:3rem;border-width:4px}}\n"] }] }], propDecorators: { loading$: [{ type: Input }], loadingList$: [{ type: Input }], message: [{ type: Input }], size: [{ type: Input }], color: [{ type: Input }], backdrop: [{ type: Input }], backdropOpacity: [{ type: Input }], showMessage: [{ type: Input }] } }); /** * Input Component * * @description * A comprehensive input component supporting various input types and form integration. * Includes support for text, email, password, number, textarea, and select inputs. * Originally designed for backoffice applications with extensive form requirements. * * @example * ```html * <!-- Basic text input --> * <juvo-input * label="Full Name" * placeholder="Enter your full name" * [(ngModel)]="name"> * </juvo-input> * * <!-- Email input with validation --> * <juvo-input * type="email" * label="Email Address" * placeholder="user@example.com" * [required]="true" * [error]="emailError" * [(ngModel)]="email"> * </juvo-input> * * <!-- Select dropdown --> * <juvo-input * type="select" * label="Country" * [options]="countryOptions" * [(ngModel)]="selectedCountry"> * </juvo-input> * * <!-- Textarea --> * <juvo-input * type="textarea" * label="Description" * [rows]="4" * placeholder="Enter description..." * [(ngModel)]="description"> * </juvo-input> * ``` * * @selector juvo-input * @since 2.1.0 * @author Juvo Rafa Team */ class JuvoInputComponent { constructor() { /** Input placeholder text */ this.placeholder = ''; /** Input type @default "text" */ this.type = 'text'; /** Input size @default "medium" */ this.size = 'medium'; /** Whether input is disabled @default false */ this.disabled = false; /** Whether input is readonly @default false */ this.readonly = false; /** Whether input is required @default false */ this.required = false; /** Number of rows for textarea @default 3 */ this.rows = 3; /** Options for select input */ this.options = []; /** Whether to show character counter @default false */ this.showCounter = false; /** Emitted when value changes */ this.valueChange = new EventEmitter(); /** Emitted when input receives focus */ this.onFocus = new EventEmitter(); /** Emitted when input loses focus */ this.onBlur = new EventEmitter(); this._value = ''; this._onChange = (value) => { }; this._onTouched = () => { }; } get value() { return this._value; } set value(val) { if (val !== this._value) { this._value = val; this._onChange(val); this.valueChange.emit(val); } } /** * Gets CSS classes for the input container * @returns Combined CSS classes */ get containerClasses() { let classes = `input-container input-${this.size}`; if (this.disabled) classes += ' disabled'; if (this.error) classes += ' error'; if (this.readonly) classes += ' readonly'; return classes; } /** * Gets CSS classes for the input element * @returns Combined CSS classes */ get inputClasses() { let classes = 'input-element'; if (this.prefix) classes += ' has-prefix'; if (this.suffix) classes += ' has-suffix'; return classes; } /** * Handles input value changes */ onInput(event) { const target = event.target; this.value = target.value; } /** * Handles input focus */ handleFocus() { this.onFocus.emit(); } /** * Handles input blur */ handleBlur() { this._onTouched(); this.onBlur.emit(); } /** * Gets the character count for display * @returns Current character count */ get characterCount() { return this.value ? this.value.toString().length : 0; } // ControlValueAccessor implementation writeValue(value) { this._value = value || ''; } registerOnChange(fn) { this._onChange = fn; } registerOnTouched(fn) { this._onTouched = fn; } setDisabledState(isDisabled) { this.disabled = isDisabled; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: JuvoInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: JuvoInputComponent, isStandalone: true, selector: "juvo-input", inputs: { label: "label", placeholder: "placeholder", type: "type", size: "size", disabled: "disabled", readonly: "readonly", required: "required", error: "error", hint: "hint", maxLength: "maxLength", min: "min", max: "max", step: "step", rows: "rows", options: "options", prefix: "prefix", suffix: "suffix", showCounter: "showCounter" }, outputs: { valueChange: "valueChange", onFocus: "onFocus", onBlur: "onBlur" }, providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => JuvoInputComponent), multi: true } ], ngImport: i0, template: "<div [class]=\"containerClasses\">\n <!-- Label -->\n <label class=\"input-label\" *ngIf=\"label\">\n {{ label }}\n <span class=\"required-indicator\" *ngIf=\"required\">*</span>\n </label>\n\n <!-- Input wrapper -->\n <div class=\"input-wrapper\">\n <!-- Prefix -->\n <span class=\"input-prefix\" *ngIf=\"prefix\">{{ prefix }}</span>\n\n <!-- Text Input -->\n <input\n *ngIf=\"type !== 'textarea' && type !== 'select'\"\n [class]=\"inputClasses\"\n [type]=\"type\"\n [placeholder]=\"placeholder\"\n [disabled]=\"disabled\"\n [readonly]=\"readonly\"\n [required]=\"required\"\n [attr.maxlength]=\"maxLength\"\n [min]=\"min\"\n [max]=\"max\"\n [step]=\"step\"\n [value]=\"value\"\n (input)=\"onInput($event)\"\n (focus)=\"handleFocus()\"\n (blur)=\"handleBlur()\"\n />\n\n <!-- Textarea -->\n <textarea\n *ngIf=\"type === 'textarea'\"\n [class]=\"inputClasses\"\n [placeholder]=\"placeholder\"\n [disabled]=\"disabled\"\n [readonly]=\"readonly\"\n [required]=\"required\"\n [attr.maxlength]=\"maxLength\"\n [rows]=\"rows\"\n [value]=\"value\"\n (input)=\"onInput($event)\"\n (focus)=\"handleFocus()\"\n (blur)=\"handleBlur()\">\n </textarea>\n\n <!-- Select -->\n <select\n *ngIf=\"type === 'select'\"\n [class]=\"inputClasses\"\n [disabled]=\"disabled\"\n [required]=\"required\"\n [value]=\"value\"\n (change)=\"onInput($event)\"\n (focus)=\"handleFocus()\"\n (blur)=\"handleBlur()\">\n <option value=\"\" disabled *ngIf=\"placeholder\">{{ placeholder }}</option>\n <option \n *ngFor=\"let option of options\"\n [value]=\"option.value\"\n [disabled]=\"option.disabled\">\n {{ option.label }}\n </option>\n </select>\n\n <!-- Suffix -->\n <span class=\"input-suffix\" *ngIf=\"suffix\">{{ suffix }}</span>\n </div>\n\n <!-- Footer -->\n <div class=\"input-footer\" *ngIf=\"error || hint || (showCounter && maxLength)\">\n <!-- Error message -->\n <div class=\"input-error\" *ngIf=\"error\">\n {{ error }}\n </div>\n \n <!-- Hint text -->\n <div class=\"input-hint\" *ngIf=\"hint && !error\">\n {{ hint }}\n </div>\n \n <!-- Character counter -->\n <div class=\"input-counter\" *ngIf=\"showCounter && maxLength && !error\">\n {{ characterCount }}/{{ maxLength }}\n </div>\n </div>\n</div> ", styles: [".input-container{display:flex;flex-direction:column;gap:.5rem;width:100%}.input-label{display:block;font-weight:500;color:#374151;font-size:.875rem}.required-indicator{color:#ef4444;margin-left:.25rem}.input-wrapper{position:relative;display:flex;align-items:center}.input-element{width:100%;border:1px solid #d1d5db;border-radius:.375rem;transition:all .15s;font-size:.875rem;background:#fff}.input-element:focus{outline:none;border-color:#3b82f6;box-shadow:0 0 0 3px #3b82f61a}.input-element:disabled{background:#f9fafb;color:#9ca3af;cursor:not-allowed}.input-element:readonly{background:#f9fafb;color:#6b7280}.input-small .input-element{padding:.5rem .75rem;font-size:.8125rem}.input-medium .input-element{padding:.625rem .875rem;font-size:.875rem}.input-large .input-element{padding:.75rem 1rem;font-size:1rem}.input-element.has-prefix{padding-left:2.5rem}.input-element.has-suffix{padding-right:2.5rem}.input-prefix,.input-suffix{position:absolute;color:#6b7280;font-size:.875rem;pointer-events:none}.input-prefix{left:.75rem}.input-suffix{right:.75rem}select.input-element{cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e\");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem}select.input-element::-ms-expand{display:none}textarea.input-element{resize:vertical;min-height:80px}.input-container.error .input-element{border-color:#ef4444}.input-container.error .input-element:focus{border-color:#ef4444;box-shadow:0 0 0 3px #ef44441a}.input-container.disabled{opacity:.6}.input-container.readonly .input-element{border-color:#e5e7eb}.input-footer{display:flex;justify-content:space-between;align-items:flex-start;gap:.5rem;min-height:1.25rem}.input-error{color:#ef4444;font-size:.75rem;flex:1}.input-hint{color:#6b7280;font-size:.75rem;flex:1}.input-counter{color:#9ca3af;font-size:.75rem;flex-shrink:0}@media (max-width: 640px){.input-label{font-size:.8125rem}.input-large .input-element{padding:.625rem .875rem;font-size:.875rem}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "ngmodule", type: FormsModule }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: JuvoInputComponent, decorators: [{ type: Component, args: [{ selector: 'juvo-input', standalone: true, imports: [CommonModule, ReactiveFormsModule, FormsModule], providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => JuvoInputComponent), multi: true } ], template: "<div [class]=\"containerClasses\">\n <!-- Label -->\n <label class=\"input-label\" *ngIf=\"label\">\n {{ label }}\n <span class=\"required-indicator\" *ngIf=\"required\">*</span>\n </label>\n\n <!-- Input wrapper -->\n <div class=\"input-wrapper\">\n <!-- Prefix -->\n <span class=\"input-prefix\" *ngIf=\"prefix\">{{ prefix }}</span>\n\n <!-- Text Input -->\n <input\n *ngIf=\"type !== 'textarea' && type !== 'select'\"\n [class]=\"inputClasses\"\n [type]=\"type\"\n [placeholder]=\"placeholder\"\n [disabled]=\"disabled\"\n [readonly]=\"readonly\"\n [required]=\"required\"\n [attr.maxlength]=\"maxLength\"\n [min]=\"min\"\n [max]=\"max\"\n [step]=\"step\"\n [value]=\"value\"\n (input)=\"onInput($event)\"\n (focus)=\"handleFocus()\"\n (blur)=\"handleBlur()\"\n />\n\n <!-- Textarea -->\n <textarea\n *ngIf=\"type === 'textarea'\"\n [class]=\"inputClasses\"\n [placeholder]=\"placeholder\"\n [disabled]=\"disabled\"\n [readonly]=\"readonly\"\n [required]=\"required\"\n [attr.maxlength]=\"maxLength\"\n [rows]=\"rows\"\n [value]=\"value\"\n (input)=\"onInput($event)\"\n (focus)=\"handleFocus()\"\n (blur)=\"handleBlur()\">\n </textarea>\n\n <!-- Select -->\n <select\n *ngIf=\"type === 'select'\"\n [class]=\"inputClasses\"\n [disabled]=\"disabled\"\n [required]=\"required\"\n [value]=\"value\"\n (change)=\"onInput($event)\"\n (focus)=\"handleFocus()\"\n (blur)=\"handleBlur()\">\n <option value=\"\" disabled *ngIf=\"placeholder\">{{ placeholder }}</option>\n <option \n *ngFor=\"let option of options\"\n [value]=\"option.value\"\n [disabled]=\"option.disabled\">\n {{ option.label }}\n </option>\n </select>\n\n <!-- Suffix -->\n <span class=\"input-suffix\" *ngIf=\"suffix\">{{ suffix }}</span>\n </div>\n\n <!-- Footer -->\n <div class=\"input-footer\" *ngIf=\"error || hint || (showCounter && maxLength)\">\n <!-- Error message -->\n <div class=\"input-error\" *ngIf=\"error\">\n {{ error }}\n </div>\n \n <!-- Hint text -->\n <div class=\"input-hint\" *ngIf=\"hint && !error\">\n {{ hint }}\n </div>\n \n <!-- Character counter -->\n <div class=\"input-counter\" *ngIf=\"showCounter && maxLength && !error\">\n {{ characterCount }}/{{ maxLength }}\n </div>\n </div>\n</div> ", styles: [".input-container{display:flex;flex-direction:column;gap:.5rem;width:100%}.input-label{display:block;font-weight:500;color:#374151;font-size:.875rem}.required-indicator{color:#ef4444;margin-left:.25rem}.input-wrapper{position:relative;display:flex;align-items:center}.input-element{width:100%;border:1px solid #d1d5db;border-radius:.375rem;transition:all .15s;font-size:.875rem;background:#fff}.input-element:focus{outline:none;border-color:#3b82f6;box-shadow:0 0 0 3px #3b82f61a}.input-element:disabled{background:#f9fafb;color:#9ca3af;cursor:not-allowed}.input-element:readonly{background:#f9fafb;color:#6b7280}.input-small .input-element{padding:.5rem .75rem;font-size:.8125rem}.input-medium .input-element{padding:.625rem .875rem;font-size:.875rem}.input-large .input-element{padding:.75rem 1rem;font-size:1rem}.input-element.has-prefix{padding-left:2.5rem}.input-element.has-suffix{padding-right:2.5rem}.input-prefix,.input-suffix{position:absolute;color:#6b7280;font-size:.875rem;pointer-events:none}.input-prefix{left:.75rem}.input-suffix{right:.75rem}select.input-element{cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e\");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem}select.input-element::-ms-expand{display:none}textarea.input-element{resize:vertical;min-height:80px}.input-container.error .input-element{border-color:#ef4444}.input-container.error .input-element:focus{border-color:#ef4444;box-shadow:0 0 0 3px #ef44441a}.input-container.disabled{opacity:.6}.input-container.readonly .input-element{border-color:#e5e7eb}.input-footer{display:flex;justify-content:space-between;align-items:flex-start;gap:.5rem;min-height:1.25rem}.input-error{color:#ef4444;font-size:.75rem;flex:1}.input-hint{color:#6b7280;font-size:.75rem;flex:1}.input-counter{color:#9ca3af;font-size:.75rem;flex-shrink:0}@media (max-width: 640px){.input-label{font-size:.8125rem}.input-large .input-element{padding:.625rem .875rem;font-size:.875rem}}\n"] }] }], propDecorators: { label: [{ type: Input }], placeholder: [{ type: Input }], type: [{ type: Input }], size: [{ type: Input }], disabled: [{ type: Input }], readonly: [{ type: Input }], required: [{ type: Input }], error: [{ type: Input }], hint: [{ type: Input }], maxLength: [{ type: Input }], min: [{ type: Input }], max: [{ type: Input }], step: [{ type: Input }], rows: [{ type: Input }], options: [{ type: Input }], prefix: [{ type: Input }], suffix: [{ type: Input }], showCounter: [{ type: Input }], valueChange: [{ type: Output }], onFocus: [{ type: Output }], onBlur: [{ type: Output }] } }); /** * List Component * * @description * A simple list component for displaying tabular data with basic functionality. * Originally designed for backoffice applications requiring data presentation. * Supports sorting, custom columns, and item selection. * * @example * ```html * <!-- Basic list --> * <juvo-list * [data$]="listData$" * [columns]="[ * { key: 'name', label: 'Name', sortable: true }, * { key: 'email', label: 'Email' }, * { key: 'status', label: 'Status', type: 'boolean' } * ]" * (itemSelected)="onItemSelected($event)"> * </juvo-list> * * <!-- List with loading state --> * <juvo-list * [data$]="userData$" * [columns]="userColumns" * [loading$]="userLoading$" * [selectable]="true" * (itemSelected)="handleSelection($event)"> * </juvo-list> * ``` * * @selector juvo-list * @since 2.1.0 * @author Juvo Rafa Team */ class JuvoListComponent { constructor() { /** Observable data source */ this.data$ = of([]); /** Column configuration */ this.columns = []; /** Loading state observable */ this.loading$ = of(false); /** Whether items are selectable @default false */ this.selectable = false; /** Whether to show header @default true */ this.showHeader = true; /** Empty state message */ this.emptyMessage = 'No data available'; /** Loading message */ this.loadingMessage = 'Loading...'; /** List size variant @default "medium" */ this.size = 'medium'; /** Whether to show borders @default true */ this.bordered = true; /** Whether to show striped rows @default true */ this.striped = true; /** Emitted when an item is selected */ this.itemSelected = new EventEmitter(); /** Emitted when an item is clicked */ this.itemClicked = new EventEmitter(); /** Emitted when column header is clicked for sorting */ this.columnSort = new EventEmitter(); this.selectedItems = []; this.sortDirection = 'asc'; } /** * Gets CSS classes for the list container * @returns Combined CSS classes */ get containerClasses() { let classes = `juvo-list juvo-list-${this.size}`; if (this.bordered) classes += ' bordered'; if (this.striped) classes += ' striped'; return classes; } /** * Handles item selection */ onItemSelect(item) { if (!this.selectable) return; const index = this.selectedItems.findIndex(selected => selected === item); if (index > -1) { this.selectedItems.splice(index, 1); } else { this.selectedItems.push(item); } this.itemSelected.emit(item); } /** * Handles item click */ onItemClick(item) { this.itemClicked.emit(item); } /** * Handles column header click for sorting */ onColumnHeaderClick(column) { if (!column.sortable) return; if (this.sortColumn === column.key) { this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; } else { this.sortColumn = column.key; this.sortDirection = 'asc'; } this.columnSort.emit({ column, direction: this.sortDirection }); } /** * Checks if an item is selected */ isSelected(item) { return this.selectedItems.includes(item); } /** * Gets the display value for a cell */ getCellValue(item, column) { const value = item[column.key]; switch (column.type) { case 'boolean': return value ? '✅' : '❌'; case 'date': return value ? new Date(value).toLocaleDateString() : ''; case 'number': return typeof value === 'number' ? value.toLocaleString() : ''; default: return value?.toString() || ''; } } /** * Gets CSS classes for column header */ getHeaderClasses(column) { let classes = 'list-header-cell'; if (column.sortable) classes += ' sortable'; if (this.sortColumn === column.key) classes += ` sorted-${this.sortDirection}`; return classes; } /** * Track by function for ngFor performance */ trackByFn(index, item) { return item.id || index; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: JuvoListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: JuvoListComponent, isStandalone: true, selector: "juvo-list", inputs: { data$: "data$", columns: "columns", loading$: "loading$", selectable: "selectable", showHeader: "showHeader", emptyMessage: "emptyMessage", loadingMessage: "loadingMessage", size: "size", bordered: "bordered", striped: "striped" }, outputs: { itemSelected: "itemSelected", itemClicked: "itemClicked", columnSort: "columnSort" }, ngImport: i0, template: "<div [class]=\"containerClasses\">\n <!-- Loading State -->\n <div class=\"list-loading\" *ngIf=\"loading$ | async\">\n <div class=\"loading-spinner\"></div>\n <div class=\"loading-text\">{{ loadingMessage }}</div>\n </div>\n\n <!-- List Content -->\n <div class=\"list-content\" *ngIf=\"!(loading$ | async)\">\n <!-- Header -->\n <div class=\"list-header\" *ngIf=\"showHeader && columns.length > 0\">\n <div class=\"list-header-row\">\n <div \n *ngIf=\"selectable\" \n class=\"list-header-cell checkbox-cell\">\n <!-- Checkbox for select all could go here -->\n </div>\n <div\n *ngFor=\"let column of columns\"\n [class]=\"getHeaderClasses(column)\"\n [style.width]=\"column.width\"\n (click)=\"onColumnHeaderClick(column)\">\n {{ column.label }}\n <span class=\"sort-indicator\" *ngIf=\"column.sortable && sortColumn === column.key\">\n {{ sortDirection === 'asc' ? '\u2191' : '\u2193' }}\n </span>\n </div>\n </div>\n </div>\n\n <!-- Body -->\n <div class=\"list-body\">\n <!-- Data Rows -->\n <div \n class=\"list-row\"\n *ngFor=\"let item of data$ | async; trackBy: trackByFn\"\n [class.selected]=\"isSelected(item)\"\n (click)=\"onItemClick(item)\">\n \n <!-- Selection checkbox -->\n <div \n *ngIf=\"selectable\" \n class=\"list-cell checkbox-cell\"\n (click)=\"onItemSelect(item); $event.stopPropagation()\">\n <input \n type=\"checkbox\" \n [checked]=\"isSelected(item)\"\n (change)=\"onItemSelect(item)\">\n </div>\n\n <!-- Data cells -->\n <div\n *ngFor=\"let column of columns\"\n class=\"list-cell\"\n [style.width]=\"column.width\">\n {{ getCellValue(item, column) }}\n </div>\n </div>\n\n <!-- Empty State -->\n <div class=\"list-empty\" *ngIf=\"(data$ | async)?.length === 0\">\n <div class=\"empty-icon\">\uD83D\uDCCB</div>\n <div class=\"empty-text\">{{ emptyMessage }}</div>\n </div>\n </div>\n </div>\n</div> ", styles: [".juvo-list{width:100%;background:#fff;border-radius:.5rem;overflow:hidden}.juvo-list.bordered{border:1px solid #e5e7eb}.list-loading{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:3rem;gap:1rem}.loading-spinner{width:2rem;height:2rem;border:2px solid #f3f4f6;border-top:2px solid #3b82f6;border-radius:50%;animation:spin 1s linear infinite}.loading-text{color:#6b7280;font-size:.875rem}@keyframes spin{to{transform:rotate(360deg)}}.list-content{width:100%}.list-header{background:#f9fafb;border-bottom:1px solid #e5e7eb}.list-header-row{display:flex;align-items:center}.list-header-cell{display:flex;align-items:center;gap:.5rem;font-weight:600;color:#374151;font-size:.875rem;border-right:1px solid #e5e7eb}.list-header-cell:last-child{border-right:none}.list-header-cell.sortable{cursor:pointer;transition:background-color .15s}.list-header-cell.sortable:hover{background:#f3f4f6}.sort-indicator{font-size:.75rem;color:#3b82f6}.list-body{max-height:400px;overflow-y:auto}.list-row{display:flex;align-items:center;border-bottom:1px solid #f3f4f6;cursor:pointer;transition:all .15s}.list-row:hover{background:#f9fafb}.list-row.selected{background:#eff6ff;border-color:#bfdbfe}.list-row:last-child{border-bottom:none}.juvo-list.striped .list-row:nth-child(2n){background:#fafafa}.juvo-list.striped .list-row:nth-child(2n):hover{background:#f3f4f6}.list-cell{flex:1;color:#374151;font-size:.875rem;border-right:1px solid #f3f4f6;overflow:hidden;te