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
JavaScript
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