cfc-ds
Version:
Design System do Conselho Federal de Contabilidade baseado no govbr-ds
742 lines (732 loc) • 821 kB
JavaScript
import * as i0 from '@angular/core';
import { Component, Input, EventEmitter, Output, TemplateRef, ViewChild, ContentChild, Injectable, HostListener, HostBinding, forwardRef, ChangeDetectionStrategy, Optional, Inject, ContentChildren, Directive, NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core';
import * as i2$1 from '@angular/forms';
import { NG_VALUE_ACCESSOR, NG_VALIDATORS, ReactiveFormsModule, FormsModule } from '@angular/forms';
import * as i1 from '@angular/common';
import { CommonModule } from '@angular/common';
import * as i1$1 from '@angular/router';
import { NavigationEnd, RouterModule } from '@angular/router';
import { filter, interval, fromEvent, BehaviorSubject } from 'rxjs';
import * as i3 from '@angular/flex-layout/flex';
import * as i2 from '@angular/flex-layout/extended';
import { tap, map, takeWhile } from 'rxjs/operators';
import { FlexLayoutModule } from '@angular/flex-layout';
import { provideHttpClient } from '@angular/common/http';
import dayjs from 'dayjs';
import 'dayjs/locale/pt-br';
import localeData from 'dayjs/plugin/localeData';
var AvatarDensity;
(function (AvatarDensity) {
AvatarDensity["large"] = "large";
AvatarDensity["medium"] = "medium";
AvatarDensity["small"] = "small";
})(AvatarDensity || (AvatarDensity = {}));
var AvatarType;
(function (AvatarType) {
AvatarType["letter"] = "letter";
AvatarType["icon"] = "icon";
AvatarType["image"] = "image";
AvatarType["dropdown"] = "dropdown";
})(AvatarType || (AvatarType = {}));
class AvatarComponent {
type = AvatarType.icon;
name;
density = AvatarDensity.medium;
imageUrl = '';
listItems = [];
avatarTypes = AvatarType;
avatarDensities = AvatarDensity;
openDropdown = false;
toggleDropdown() {
this.openDropdown = !this.openDropdown;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AvatarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: AvatarComponent, selector: "cfc-avatar", inputs: { type: "type", name: "name", density: "density", imageUrl: "imageUrl", listItems: "listItems" }, ngImport: i0, template: "<span\r\n class=\"br-avatar mr-3\"\r\n [title]=\"name\"\r\n [class.medium]=\"density === avatarDensities.medium\"\r\n [class.large]=\"density === avatarDensities.large\"\r\n>\r\n <span *ngIf=\"type === avatarTypes.icon\" class=\"content\">\r\n <i class=\"fas fa-user bg-blue-warn-20\" aria-hidden=\"true\"></i>\r\n </span>\r\n <span\r\n *ngIf=\"type === avatarTypes.letter\"\r\n class=\"content bg-violet-50 text-pure-0\"\r\n >\r\n {{ name[0] | uppercase }}\r\n </span>\r\n <span *ngIf=\"type === avatarTypes.image\" class=\"content\">\r\n <img [src]=\"imageUrl\" alt=\"Avatar\" />\r\n </span>\r\n <span *ngIf=\"type === avatarTypes.dropdown\" class=\"dropdown\">\r\n <button class=\"br-sign-in\" type=\"button\" (click)=\"toggleDropdown()\">\r\n <span class=\"br-avatar\" title=\"Fulano da Silva\"\r\n ><span class=\"content bg-orange-vivid-30 text-pure-0\">\r\n {{ name[0] | uppercase }}</span\r\n ></span\r\n ><span class=\"ml-2 text-gray-80 text-weight-regular\">{{ name }}</span\r\n ><i class=\"fas fa-caret-down\" aria-hidden=\"true\"></i>\r\n </button>\r\n <div *ngIf=\"openDropdown\" class=\"br-list\">\r\n <div *ngIf=\"listItems?.length !== 0\">\r\n <div *ngFor=\"let items of listItems\" class=\"br-item\"><p>{{items.name}}</p></div>\r\n </div>\r\n </div>\r\n </span>\r\n</span>\r\n", styles: [".dropdown{position:relative}.br-list{position:absolute;left:0}.dropdown .br-item:not(:last-child){border-bottom:1px solid #ccc}.br-item{color:#333;height:50px;cursor:pointer}.br-item p{font-size:14px}.br-item:hover{background-color:#c6c6c6}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1.UpperCasePipe, name: "uppercase" }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AvatarComponent, decorators: [{
type: Component,
args: [{ selector: 'cfc-avatar', template: "<span\r\n class=\"br-avatar mr-3\"\r\n [title]=\"name\"\r\n [class.medium]=\"density === avatarDensities.medium\"\r\n [class.large]=\"density === avatarDensities.large\"\r\n>\r\n <span *ngIf=\"type === avatarTypes.icon\" class=\"content\">\r\n <i class=\"fas fa-user bg-blue-warn-20\" aria-hidden=\"true\"></i>\r\n </span>\r\n <span\r\n *ngIf=\"type === avatarTypes.letter\"\r\n class=\"content bg-violet-50 text-pure-0\"\r\n >\r\n {{ name[0] | uppercase }}\r\n </span>\r\n <span *ngIf=\"type === avatarTypes.image\" class=\"content\">\r\n <img [src]=\"imageUrl\" alt=\"Avatar\" />\r\n </span>\r\n <span *ngIf=\"type === avatarTypes.dropdown\" class=\"dropdown\">\r\n <button class=\"br-sign-in\" type=\"button\" (click)=\"toggleDropdown()\">\r\n <span class=\"br-avatar\" title=\"Fulano da Silva\"\r\n ><span class=\"content bg-orange-vivid-30 text-pure-0\">\r\n {{ name[0] | uppercase }}</span\r\n ></span\r\n ><span class=\"ml-2 text-gray-80 text-weight-regular\">{{ name }}</span\r\n ><i class=\"fas fa-caret-down\" aria-hidden=\"true\"></i>\r\n </button>\r\n <div *ngIf=\"openDropdown\" class=\"br-list\">\r\n <div *ngIf=\"listItems?.length !== 0\">\r\n <div *ngFor=\"let items of listItems\" class=\"br-item\"><p>{{items.name}}</p></div>\r\n </div>\r\n </div>\r\n </span>\r\n</span>\r\n", styles: [".dropdown{position:relative}.br-list{position:absolute;left:0}.dropdown .br-item:not(:last-child){border-bottom:1px solid #ccc}.br-item{color:#333;height:50px;cursor:pointer}.br-item p{font-size:14px}.br-item:hover{background-color:#c6c6c6}\n"] }]
}], propDecorators: { type: [{
type: Input
}], name: [{
type: Input
}], density: [{
type: Input
}], imageUrl: [{
type: Input
}], listItems: [{
type: Input
}] } });
class BreadcrumbComponent {
router;
activatedRoute;
/** Lista de breadcrumbs gerados */
links = [];
/** Limite de caracteres para truncamento */
maxLength = 25;
/** Inscrição para monitorar mudanças na rota */
routeSubscription;
constructor(router, activatedRoute) {
this.router = router;
this.activatedRoute = activatedRoute;
}
/** Inicializa o componente e observa mudanças de rota */
ngOnInit() {
this.updateBreadcrumbs();
this.routeSubscription = this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => {
this.updateBreadcrumbs();
});
}
/** Atualiza os breadcrumbs com base na rota ativa */
updateBreadcrumbs() {
const breadcrumbs = this.buildBreadcrumbs(this.router.routerState.root);
this.removeLastBreadcrumbUrl(breadcrumbs);
this.links = breadcrumbs;
}
/**
* Constrói os breadcrumbs recursivamente com base nas rotas ativadas.
* @param route Rota ativa.
* @param breadcrumbs Lista de breadcrumbs acumulada.
* @returns Lista de BreadcrumbLink atualizada.
*/
buildBreadcrumbs(route, breadcrumbs = []) {
route.children.forEach((child) => {
const routeSegment = this.getRouteURL(child);
const label = this.getBreadcrumbLabel(child.snapshot.data);
const fullPath = this.buildFullPath(routeSegment, breadcrumbs);
if (label && !this.isDuplicateBreadcrumb(breadcrumbs, label)) {
const truncated = this.shouldTruncate(label) ? this.truncateText(label) : label;
const breadcrumb = {
label: truncated,
url: fullPath,
target: '_self',
};
// Adicionar tooltip apenas se o texto foi truncado
if (truncated !== label) {
breadcrumb.tooltipText = label;
breadcrumb.tooltipPlace = 'top';
breadcrumb.tooltipType = 'info';
}
breadcrumbs.push(breadcrumb);
}
this.buildBreadcrumbs(child, breadcrumbs);
});
return breadcrumbs;
}
/**
* Verifica se o texto deve ser truncado
* @param text Texto para verificar
* @returns true se o texto deve ser truncado
*/
shouldTruncate(text) {
return text.length > this.maxLength;
}
/**
* Trunca o texto para o tamanho máximo definido
* @param text Texto para truncar
* @returns Texto truncado
*/
truncateText(text) {
return text.substring(0, this.maxLength - 3) + '...';
}
/**
* Obtém o texto original para exibição no tooltip
* @param link Item do breadcrumb
* @returns Texto original ou undefined se não houver tooltip
*/
getOriginalText(link) {
return link.tooltipText;
}
/**
* Obtém o segmento da URL da rota atual.
* @param route Rota atual.
* @returns Segmento da URL ou string vazia.
*/
getRouteURL(route) {
return route.snapshot.url.length > 0
? route.snapshot.url.map(segment => segment.path).join('/')
: '';
}
/**
* Constrói o caminho completo concatenando segmentos anteriores.
* @param routeSegment Segmento atual da rota.
* @param breadcrumbs Lista de breadcrumbs acumulada.
* @returns Caminho completo da URL.
*/
buildFullPath(routeSegment, breadcrumbs) {
if (!routeSegment)
return '';
const previousPath = breadcrumbs.length > 0 ? breadcrumbs[breadcrumbs.length - 1].url : '';
return previousPath ? `${previousPath}/${routeSegment}` : `/${routeSegment}`;
}
/**
* Obtém o rótulo do breadcrumb a partir dos dados da rota.
* @param data Dados da rota.
* @returns Rótulo do breadcrumb ou null.
*/
getBreadcrumbLabel(data) {
const label = data['breadcrumb'];
return label && typeof label === 'string' && label.trim() ? label.trim() : null;
}
/**
* Remove a URL do último breadcrumb para evitar que seja clicável.
* @param breadcrumbs Lista de breadcrumbs.
*/
removeLastBreadcrumbUrl(breadcrumbs) {
if (breadcrumbs.length > 0) {
breadcrumbs[breadcrumbs.length - 1].url = undefined;
breadcrumbs[breadcrumbs.length - 1].active = true;
}
}
/**
* Verifica se um breadcrumb já existe na lista para evitar duplicatas.
* @param breadcrumbs Lista de breadcrumbs acumulada.
* @param label Nome do breadcrumb a verificar.
* @returns Verdadeiro se o breadcrumb já existir, falso caso contrário.
*/
isDuplicateBreadcrumb(breadcrumbs, label) {
return breadcrumbs.some(bc => bc.tooltipText === label || bc.label === label);
}
/** Cancela a inscrição ao destruir o componente */
ngOnDestroy() {
this.routeSubscription?.unsubscribe();
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: BreadcrumbComponent, deps: [{ token: i1$1.Router }, { token: i1$1.ActivatedRoute }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: BreadcrumbComponent, selector: "cfc-breadcrumb", ngImport: i0, template: "<nav\r\n class=\"br-breadcrumb\"\r\n aria-label=\"Breadcrumbs\">\r\n <ul\r\n class=\"crumb-list\"\r\n style=\"padding-left: 0;\"\r\n role=\"list\">\r\n <li\r\n class=\"crumb\">\r\n <a\r\n class=\"br-button circle\"\r\n href=\"/\"\r\n target=\"_self\">\r\n <i class=\"fas fa-home\"></i>\r\n </a>\r\n </li>\r\n\r\n <li\r\n class=\"crumb\"\r\n *ngFor=\"let link of links; let last = last\">\r\n <i\r\n class=\"icon fas fa-chevron-right\">\r\n </i>\r\n\r\n <a\r\n *ngIf=\"!last\"\r\n [href]=\"link.url\"\r\n [target]=\"link.target\"\r\n [title]=\"link.tooltipText || ''\"\r\n [attr.data-tooltip]=\"link.tooltipText || null\"\r\n [attr.data-tooltip-place]=\"link.tooltipPlace || null\"\r\n [attr.data-tooltip-type]=\"link.tooltipType || null\"\r\n [attr.data-tooltip-timer]=\"link.tooltipTimer || null\">\r\n <span>\r\n {{ link.label | titlecase }}\r\n </span>\r\n </a>\r\n\r\n <span\r\n *ngIf=\"last\"\r\n tabindex=\"0\"\r\n aria-current=\"page\"\r\n [title]=\"link.tooltipText || ''\"\r\n [attr.data-tooltip]=\"link.tooltipText || null\"\r\n [attr.data-tooltip-place]=\"link.tooltipPlace || null\"\r\n [attr.data-tooltip-type]=\"link.tooltipType || null\"\r\n [attr.data-tooltip-timer]=\"link.tooltipTimer || null\">\r\n {{ link.label | titlecase }}\r\n </span>\r\n\r\n </li>\r\n </ul>\r\n</nav>", styles: [""], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1.TitleCasePipe, name: "titlecase" }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: BreadcrumbComponent, decorators: [{
type: Component,
args: [{ selector: 'cfc-breadcrumb', template: "<nav\r\n class=\"br-breadcrumb\"\r\n aria-label=\"Breadcrumbs\">\r\n <ul\r\n class=\"crumb-list\"\r\n style=\"padding-left: 0;\"\r\n role=\"list\">\r\n <li\r\n class=\"crumb\">\r\n <a\r\n class=\"br-button circle\"\r\n href=\"/\"\r\n target=\"_self\">\r\n <i class=\"fas fa-home\"></i>\r\n </a>\r\n </li>\r\n\r\n <li\r\n class=\"crumb\"\r\n *ngFor=\"let link of links; let last = last\">\r\n <i\r\n class=\"icon fas fa-chevron-right\">\r\n </i>\r\n\r\n <a\r\n *ngIf=\"!last\"\r\n [href]=\"link.url\"\r\n [target]=\"link.target\"\r\n [title]=\"link.tooltipText || ''\"\r\n [attr.data-tooltip]=\"link.tooltipText || null\"\r\n [attr.data-tooltip-place]=\"link.tooltipPlace || null\"\r\n [attr.data-tooltip-type]=\"link.tooltipType || null\"\r\n [attr.data-tooltip-timer]=\"link.tooltipTimer || null\">\r\n <span>\r\n {{ link.label | titlecase }}\r\n </span>\r\n </a>\r\n\r\n <span\r\n *ngIf=\"last\"\r\n tabindex=\"0\"\r\n aria-current=\"page\"\r\n [title]=\"link.tooltipText || ''\"\r\n [attr.data-tooltip]=\"link.tooltipText || null\"\r\n [attr.data-tooltip-place]=\"link.tooltipPlace || null\"\r\n [attr.data-tooltip-type]=\"link.tooltipType || null\"\r\n [attr.data-tooltip-timer]=\"link.tooltipTimer || null\">\r\n {{ link.label | titlecase }}\r\n </span>\r\n\r\n </li>\r\n </ul>\r\n</nav>" }]
}], ctorParameters: () => [{ type: i1$1.Router }, { type: i1$1.ActivatedRoute }] });
var ButtonType;
(function (ButtonType) {
ButtonType["primary"] = "primary";
ButtonType["secondary"] = "secondary";
ButtonType["tertiary"] = "tertiary";
ButtonType["danger"] = "danger";
})(ButtonType || (ButtonType = {}));
var ButtonDensity;
(function (ButtonDensity) {
ButtonDensity["large"] = "large";
ButtonDensity["middle"] = "middle";
ButtonDensity["small"] = "small";
})(ButtonDensity || (ButtonDensity = {}));
class ButtonComponent {
cdr;
label = 'button';
type = ButtonType.primary;
submit = false;
circle = false;
density = ButtonDensity.middle;
disabled = false;
block = false;
icon = '';
active = false;
inverted = false;
loading = false;
onClick = new EventEmitter();
buttonTypes = ButtonType;
buttonDensity = ButtonDensity;
constructor(cdr) {
this.cdr = cdr;
}
ngOnChanges(changes) {
if (changes['type']) {
this.cdr.detectChanges();
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ButtonComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: ButtonComponent, selector: "cfc-button", inputs: { label: "label", type: "type", submit: "submit", circle: "circle", density: "density", disabled: "disabled", block: "block", icon: "icon", active: "active", inverted: "inverted", loading: "loading" }, outputs: { onClick: "onClick" }, usesOnChanges: true, ngImport: i0, template: "<button\r\n class=\"br-button\"\r\n [ngClass]=\"{\r\n 'primary': type === buttonTypes.primary,\r\n 'secondary': type === buttonTypes.secondary,\r\n 'tertiary': type === buttonTypes.tertiary,\r\n 'danger': type === buttonTypes.danger,\r\n 'circle': circle,\r\n 'block': block,\r\n 'loading': loading,\r\n 'active': active,\r\n 'dark-mode': inverted,\r\n 'small': density === buttonDensity.small,\r\n 'lager': density === buttonDensity.large,\r\n 'middle': density === buttonDensity.middle,\r\n }\"\r\n [disabled]=\"disabled\"\r\n [attr.aria-label]=\"icon ? label : null\"\r\n [type]=\"submit ? 'submit' : 'button'\"\r\n fxLayoutGap=\"0.3rem\"\r\n (click)=\"onClick.emit()\"\r\n>\r\n <i *ngIf=\"icon\" [class]=\"'fas fa-' + icon\" aria-hidden=\"true\"></i>\r\n <span *ngIf=\"!circle\">\r\n {{ label }}\r\n </span>\r\n</button>\r\n", styles: [""], dependencies: [{ kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i3.DefaultLayoutGapDirective, selector: " [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]", inputs: ["fxLayoutGap", "fxLayoutGap.xs", "fxLayoutGap.sm", "fxLayoutGap.md", "fxLayoutGap.lg", "fxLayoutGap.xl", "fxLayoutGap.lt-sm", "fxLayoutGap.lt-md", "fxLayoutGap.lt-lg", "fxLayoutGap.lt-xl", "fxLayoutGap.gt-xs", "fxLayoutGap.gt-sm", "fxLayoutGap.gt-md", "fxLayoutGap.gt-lg"] }, { kind: "directive", type: i2.DefaultClassDirective, selector: " [ngClass], [ngClass.xs], [ngClass.sm], [ngClass.md], [ngClass.lg], [ngClass.xl], [ngClass.lt-sm], [ngClass.lt-md], [ngClass.lt-lg], [ngClass.lt-xl], [ngClass.gt-xs], [ngClass.gt-sm], [ngClass.gt-md], [ngClass.gt-lg]", inputs: ["ngClass", "ngClass.xs", "ngClass.sm", "ngClass.md", "ngClass.lg", "ngClass.xl", "ngClass.lt-sm", "ngClass.lt-md", "ngClass.lt-lg", "ngClass.lt-xl", "ngClass.gt-xs", "ngClass.gt-sm", "ngClass.gt-md", "ngClass.gt-lg"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ButtonComponent, decorators: [{
type: Component,
args: [{ selector: 'cfc-button', template: "<button\r\n class=\"br-button\"\r\n [ngClass]=\"{\r\n 'primary': type === buttonTypes.primary,\r\n 'secondary': type === buttonTypes.secondary,\r\n 'tertiary': type === buttonTypes.tertiary,\r\n 'danger': type === buttonTypes.danger,\r\n 'circle': circle,\r\n 'block': block,\r\n 'loading': loading,\r\n 'active': active,\r\n 'dark-mode': inverted,\r\n 'small': density === buttonDensity.small,\r\n 'lager': density === buttonDensity.large,\r\n 'middle': density === buttonDensity.middle,\r\n }\"\r\n [disabled]=\"disabled\"\r\n [attr.aria-label]=\"icon ? label : null\"\r\n [type]=\"submit ? 'submit' : 'button'\"\r\n fxLayoutGap=\"0.3rem\"\r\n (click)=\"onClick.emit()\"\r\n>\r\n <i *ngIf=\"icon\" [class]=\"'fas fa-' + icon\" aria-hidden=\"true\"></i>\r\n <span *ngIf=\"!circle\">\r\n {{ label }}\r\n </span>\r\n</button>\r\n" }]
}], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { label: [{
type: Input
}], type: [{
type: Input
}], submit: [{
type: Input
}], circle: [{
type: Input
}], density: [{
type: Input
}], disabled: [{
type: Input
}], block: [{
type: Input
}], icon: [{
type: Input
}], active: [{
type: Input
}], inverted: [{
type: Input
}], loading: [{
type: Input
}], onClick: [{
type: Output
}] } });
class CardContentComponent {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: CardContentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: CardContentComponent, selector: "cfc-card-content", ngImport: i0, template: "<div class=\"card-content\">\r\n <ng-content></ng-content>\r\n</div>\r\n", styles: [""] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: CardContentComponent, decorators: [{
type: Component,
args: [{ selector: 'cfc-card-content', template: "<div class=\"card-content\">\r\n <ng-content></ng-content>\r\n</div>\r\n" }]
}] });
class CardComponent {
hover = false;
disabled = false;
hFixed = false;
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: CardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: CardComponent, selector: "cfc-card", inputs: { hover: "hover", disabled: "disabled", hFixed: "hFixed" }, ngImport: i0, template: " <div class=\"br-card\"\r\n [class.hover]=\"hover\"\r\n [class.disabled]=\"disabled\"\r\n [class.h-fixed]=\"hFixed\">\r\n <ng-content select=\"[cfc-card-header]\"></ng-content>\r\n <cfc-card-content>\r\n <ng-content select=\"[cfc-card-content]\">\r\n\r\n </ng-content>\r\n </cfc-card-content>\r\n <ng-content select=\"[cfc-card-footer]\"></ng-content>\r\n </div>\r\n", styles: [""], dependencies: [{ kind: "component", type: CardContentComponent, selector: "cfc-card-content" }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: CardComponent, decorators: [{
type: Component,
args: [{ selector: 'cfc-card', template: " <div class=\"br-card\"\r\n [class.hover]=\"hover\"\r\n [class.disabled]=\"disabled\"\r\n [class.h-fixed]=\"hFixed\">\r\n <ng-content select=\"[cfc-card-header]\"></ng-content>\r\n <cfc-card-content>\r\n <ng-content select=\"[cfc-card-content]\">\r\n\r\n </ng-content>\r\n </cfc-card-content>\r\n <ng-content select=\"[cfc-card-footer]\"></ng-content>\r\n </div>\r\n" }]
}], propDecorators: { hover: [{
type: Input
}], disabled: [{
type: Input
}], hFixed: [{
type: Input
}] } });
class CarouselComponent {
items = [];
config = {
loop: false,
autoPlay: false,
autoPlayInterval: 5000,
showNavigationButtons: true,
showPlayButtons: false,
showIndicator: true,
navigationButtonsPosition: 'outside', // 'inside' ou 'outside'
indicatorPosition: 'inside', // 'inside' ou 'outside'
indicatorType: 'simple',
itemsPerPage: 1,
navigationType: 'page',
transitionDuration: 300,
transitionType: 'slide',
enableTouch: true
};
pageChange = new EventEmitter();
itemClick = new EventEmitter();
carouselStage;
carouselContent;
contentTemplate;
currentPageIndex = 0;
totalPages = 0;
isPlaying = false;
isPaused = false;
isHovering = false;
isDragging = false;
dragStartX = 0;
dragCurrentX = 0;
initialTransform = 0;
currentTransform = 0;
loadedImages = new Set();
adjacentImagesLoaded = false;
autoPlaySubscription;
touchStartX = 0;
touchEndX = 0;
touchStartY = 0;
touchStartTime = 0;
swipeSubscriptions = [];
mousedownSubscription;
mousemoveSubscription;
mouseupSubscription;
mouseleaveSubscription;
get currentPage() {
return this.currentPageIndex + 1;
}
get pages() {
return Array.from({ length: this.totalPages }, (_, i) => i);
}
get blocks() {
const blocksCount = Math.ceil(this.items.length / (this.config.itemsPerPage ?? 1));
return Array.from({ length: blocksCount }, (_, i) => i);
}
ngOnInit() {
// Verificar e ajustar config para indicador textual
if (this.config.indicatorType === 'text') {
this.config.indicatorPosition = 'outside';
}
this.calculateTotalPages();
this.initializeCarousel();
// Pré-carregar a primeira imagem e as adjacentes
if (this.items.length > 0) {
this.preloadAdjacentImages(0);
}
if (this.config.autoPlay) {
this.play();
}
}
ngAfterViewInit() {
if (this.config.enableTouch) {
this.setupTouchEvents();
this.setupMouseEvents();
}
}
ngOnDestroy() {
this.stopAutoPlay();
this.cleanupTouchEvents();
this.cleanupMouseEvents();
}
onMouseEnter() {
this.isHovering = true;
if (this.config.autoPlay && this.isPlaying) {
this.pauseAutoPlay();
}
}
onMouseLeave() {
this.isHovering = false;
if (this.config.autoPlay && this.isPaused) {
this.resumeAutoPlay();
}
}
calculateTotalPages() {
if (this.config.navigationType === 'block') {
this.totalPages = Math.ceil(this.items.length / (this.config.itemsPerPage ?? 1));
}
else {
this.totalPages = this.items.length;
}
}
initializeCarousel() {
if (this.items.length > 0) {
this.items.forEach((item, index) => {
item.active = index === 0;
});
}
}
goToPage(pageIndex) {
if (pageIndex < 0 || pageIndex >= this.totalPages) {
if (this.config.loop) {
pageIndex = pageIndex < 0 ? this.totalPages - 1 : 0;
}
else {
return;
}
}
// Pré-carregar imagens adjacentes antes da transição
this.preloadAdjacentImages(pageIndex);
this.currentPageIndex = pageIndex;
// Atualizar estado de ativo para os itens
if (this.config.navigationType === 'block') {
const startIndex = this.currentPageIndex * (this.config.itemsPerPage ?? 1);
this.items.forEach((item, index) => {
item.active = index >= startIndex && index < startIndex + (this.config.itemsPerPage ?? 1);
});
}
else {
this.items.forEach((item, index) => {
item.active = index === this.currentPageIndex;
});
}
this.updateContentTransform(this.currentPageIndex);
this.pageChange.emit(this.currentPageIndex);
}
// Método para pré-carregar imagens adjacentes
preloadAdjacentImages(currentIndex) {
// Determinar quais imagens devem ser pré-carregadas (atual, anterior e próxima)
const indicesToPreload = [
currentIndex,
Math.max(0, currentIndex - 1),
Math.min(this.totalPages - 1, currentIndex + 1)
];
// Filtrar índices únicos
const uniqueIndices = [...new Set(indicesToPreload)];
// Pré-carregar cada imagem
uniqueIndices.forEach(index => {
if (index >= 0 && index < this.items.length) {
const item = this.items[index];
if (item.imageUrl && !this.loadedImages.has(item.imageUrl)) {
this.preloadImage(item.imageUrl);
}
}
});
}
// Método para pré-carregar uma única imagem
preloadImage(imageUrl) {
if (!imageUrl || this.loadedImages.has(imageUrl))
return;
const img = new Image();
img.onload = () => {
this.loadedImages.add(imageUrl);
};
img.src = imageUrl;
}
// Método para aplicar a transformação ao conteúdo
updateContentTransform(pageIndex, animationOverride = '') {
if (!this.carouselContent || !this.carouselContent.nativeElement)
return;
const element = this.carouselContent.nativeElement;
const translateX = -pageIndex * 100;
// Aplicar a transição antes de mudar a transformação
element.style.transition = animationOverride || `transform ${this.config.transitionDuration}ms ease-in-out`;
// Pequeno delay antes de aplicar a transformação para garantir que a transição seja registrada
setTimeout(() => {
element.style.transform = `translateX(${translateX}%)`;
this.currentTransform = translateX;
}, 10);
}
next() {
this.goToPage(this.currentPageIndex + 1);
}
prev() {
this.goToPage(this.currentPageIndex - 1);
}
play() {
if (this.isPlaying) {
return;
}
this.isPlaying = true;
this.isPaused = false;
this.autoPlaySubscription = interval(this.config.autoPlayInterval)
.pipe(tap(() => {
if (!this.isHovering && !this.isDragging) {
if (this.currentPageIndex === this.totalPages - 1 && !this.config.loop) {
this.stopAutoPlay();
}
else {
this.next();
}
}
}))
.subscribe();
}
pauseAutoPlay() {
this.isPaused = true;
this.stopAutoPlay();
}
resumeAutoPlay() {
if (this.isPaused) {
this.play();
}
}
pause() {
this.pauseAutoPlay();
}
togglePlay() {
if (this.isPlaying) {
this.pause();
}
else {
this.play();
}
}
stopAutoPlay() {
if (this.autoPlaySubscription) {
this.autoPlaySubscription.unsubscribe();
this.autoPlaySubscription = undefined;
this.isPlaying = false;
}
}
setupTouchEvents() {
if (!this.carouselStage || !this.carouselStage.nativeElement) {
return;
}
const element = this.carouselStage.nativeElement;
// Usando eventos nativos para melhor desempenho
element.addEventListener('touchstart', this.handleTouchStart.bind(this), { passive: true });
element.addEventListener('touchmove', this.handleTouchMove.bind(this), { passive: false });
element.addEventListener('touchend', this.handleTouchEnd.bind(this), { passive: true });
}
handleTouchStart(event) {
if (this.isPlaying) {
this.pauseAutoPlay();
}
this.touchStartX = event.touches[0].clientX;
this.touchStartY = event.touches[0].clientY;
this.touchStartTime = Date.now();
this.isDragging = true;
if (this.carouselContent && this.carouselContent.nativeElement) {
const style = window.getComputedStyle(this.carouselContent.nativeElement);
const matrix = new WebKitCSSMatrix(style.transform);
this.initialTransform = matrix.m41 / this.carouselContent.nativeElement.offsetWidth * 100;
this.currentTransform = this.initialTransform;
// Remover transição para movimento suave durante o arrasto
this.carouselContent.nativeElement.style.transition = 'none';
}
}
handleTouchMove(event) {
if (!this.isDragging)
return;
const touch = event.touches[0];
const deltaX = touch.clientX - this.touchStartX;
const deltaY = touch.clientY - this.touchStartY;
// Verificar se o deslizamento é horizontal antes de prevenir comportamento padrão
if (Math.abs(deltaX) > Math.abs(deltaY)) {
event.preventDefault();
}
if (this.carouselContent && this.carouselContent.nativeElement) {
const containerWidth = this.carouselContent.nativeElement.offsetWidth;
const movePercentage = (deltaX / containerWidth) * 100;
// Adicionar resistência nas bordas quando não estiver em loop
let newTransform = this.initialTransform + movePercentage;
if (!this.config.loop) {
if (this.currentPageIndex === 0 && movePercentage > 0) {
newTransform = this.initialTransform + (movePercentage / 3); // Resistência no início
}
else if (this.currentPageIndex === this.totalPages - 1 && movePercentage < 0) {
newTransform = this.initialTransform + (movePercentage / 3); // Resistência no final
}
}
this.carouselContent.nativeElement.style.transform = `translateX(${newTransform}%)`;
this.currentTransform = newTransform;
}
}
handleTouchEnd(event) {
if (!this.isDragging)
return;
const touchEndTime = Date.now();
const touchDuration = touchEndTime - this.touchStartTime;
this.touchEndX = event.changedTouches[0].clientX;
const deltaX = this.touchEndX - this.touchStartX;
// Calcular velocidade do movimento para determinar swipe
const velocity = Math.abs(deltaX) / touchDuration;
// Determinar o comportamento com base no movimento e velocidade
let newPage = this.currentPageIndex;
if (Math.abs(deltaX) > 100 || velocity > 0.5) {
// Movimento significativo ou swipe rápido
newPage = deltaX > 0 ? this.currentPageIndex - 1 : this.currentPageIndex + 1;
}
else {
// Baseado na porcentagem de movimento
const movedPercentage = Math.abs(deltaX) / (this.carouselStage?.nativeElement.offsetWidth || 1);
if (movedPercentage > 0.2) {
newPage = deltaX > 0 ? this.currentPageIndex - 1 : this.currentPageIndex + 1;
}
}
this.isDragging = false;
// Voltar para o slide atual ou ir para o próximo/anterior
if (this.carouselContent && this.carouselContent.nativeElement) {
this.carouselContent.nativeElement.style.transition = `transform ${this.config.transitionDuration}ms var(--animation-ease-in-out)`;
this.goToPage(newPage);
}
// Restaurar autoplay se necessário
if (this.isPaused) {
this.resumeAutoPlay();
}
}
// Adicionar suporte a eventos de mouse para desktop
setupMouseEvents() {
if (!this.carouselStage || !this.carouselStage.nativeElement)
return;
const element = this.carouselStage.nativeElement;
this.mousedownSubscription = fromEvent(element, 'mousedown')
.subscribe(this.handleMouseDown.bind(this));
this.mousemoveSubscription = fromEvent(document, 'mousemove')
.subscribe(this.handleMouseMove.bind(this));
this.mouseupSubscription = fromEvent(document, 'mouseup')
.subscribe(this.handleMouseUp.bind(this));
this.mouseleaveSubscription = fromEvent(document, 'mouseleave')
.subscribe(this.handleMouseUp.bind(this));
}
handleMouseDown(event) {
// Só capturar cliques com botão esquerdo
if (event.button !== 0)
return;
// Prevenir comportamento padrão (seleção de texto)
event.preventDefault();
if (this.isPlaying) {
this.pauseAutoPlay();
}
this.dragStartX = event.clientX;
this.isDragging = true;
if (this.carouselContent && this.carouselContent.nativeElement) {
const style = window.getComputedStyle(this.carouselContent.nativeElement);
const matrix = new WebKitCSSMatrix(style.transform);
this.initialTransform = matrix.m41 / this.carouselContent.nativeElement.offsetWidth * 100;
this.currentTransform = this.initialTransform;
this.carouselContent.nativeElement.style.transition = 'none';
// Cursor de arrasto
document.body.style.cursor = 'grabbing';
}
}
handleMouseMove(event) {
if (!this.isDragging)
return;
this.dragCurrentX = event.clientX;
const deltaX = this.dragCurrentX - this.dragStartX;
if (this.carouselContent && this.carouselContent.nativeElement) {
const containerWidth = this.carouselContent.nativeElement.offsetWidth;
const movePercentage = (deltaX / containerWidth) * 100;
// Adicionar resistência nas bordas quando não estiver em loop
let newTransform = this.initialTransform + movePercentage;
if (!this.config.loop) {
if (this.currentPageIndex === 0 && movePercentage > 0) {
newTransform = this.initialTransform + (movePercentage / 3);
}
else if (this.currentPageIndex === this.totalPages - 1 && movePercentage < 0) {
newTransform = this.initialTransform + (movePercentage / 3);
}
}
this.carouselContent.nativeElement.style.transform = `translateX(${newTransform}%)`;
this.currentTransform = newTransform;
}
}
handleMouseUp(event) {
if (!this.isDragging)
return;
document.body.style.cursor = '';
this.isDragging = false;
if (this.dragStartX !== 0 && this.dragCurrentX !== 0) {
const deltaX = this.dragCurrentX - this.dragStartX;
let newPage = this.currentPageIndex;
// Determinar se deve mudar de slide com base na distância do movimento
if (Math.abs(deltaX) > 100) {
newPage = deltaX > 0 ? this.currentPageIndex - 1 : this.currentPageIndex + 1;
}
else {
const movedPercentage = Math.abs(deltaX) / (this.carouselStage?.nativeElement.offsetWidth || 1);
if (movedPercentage > 0.2) {
newPage = deltaX > 0 ? this.currentPageIndex - 1 : this.currentPageIndex + 1;
}
}
if (this.carouselContent && this.carouselContent.nativeElement) {
this.carouselContent.nativeElement.style.transition = `transform ${this.config.transitionDuration}ms var(--animation-ease-in-out)`;
this.goToPage(newPage);
}
}
this.dragStartX = 0;
this.dragCurrentX = 0;
// Restaurar autoplay se necessário
if (this.isPaused) {
this.resumeAutoPlay();
}
}
cleanupTouchEvents() {
if (this.carouselStage && this.carouselStage.nativeElement) {
const element = this.carouselStage.nativeElement;
element.removeEventListener('touchstart', this.handleTouchStart.bind(this));
element.removeEventListener('touchmove', this.handleTouchMove.bind(this));
element.removeEventListener('touchend', this.handleTouchEnd.bind(this));
}
this.swipeSubscriptions.forEach(sub => sub.unsubscribe());
this.swipeSubscriptions = [];
}
cleanupMouseEvents() {
this.mousedownSubscription?.unsubscribe();
this.mousemoveSubscription?.unsubscribe();
this.mouseupSubscription?.unsubscribe();
this.mouseleaveSubscription?.unsubscribe();
}
isFirstPage() {
return this.currentPageIndex === 0;
}
isLastPage() {
return this.currentPageIndex === this.totalPages - 1;
}
shouldDisableNavButton(direction) {
if (this.config.loop) {
return false;
}
return direction === 'prev' ? this.isFirstPage() : this.isLastPage();
}
trackByFn(index, item) {
return item.id || index;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: CarouselComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: CarouselComponent, selector: "cfc-carousel", inputs: { items: "items", config: "config" }, outputs: { pageChange: "pageChange", itemClick: "itemClick" }, queries: [{ propertyName: "contentTemplate", first: true, predicate: TemplateRef, descendants: true }], viewQueries: [{ propertyName: "carouselStage", first: true, predicate: ["carouselStage"], descendants: true }, { propertyName: "carouselContent", first: true, predicate: ["carouselContent"], descendants: true }], ngImport: i0, template: "<div class=\"carousel-wrapper\">\r\n <!-- Bot\u00E3o de navega\u00E7\u00E3o (anterior) outside -->\r\n <button *ngIf=\"config.showNavigationButtons && config.navigationButtonsPosition === 'outside'\"\r\n class=\"carousel-nav-button carousel-prev-button outside\"\r\n [disabled]=\"shouldDisableNavButton('prev')\"\r\n (click)=\"prev()\">\r\n <i class=\"fas fa-angle-left\"></i>\r\n </button>\r\n <div class=\"carousel-container\"\r\n (mouseenter)=\"onMouseEnter()\"\r\n (mouseleave)=\"onMouseLeave()\">\r\n <!-- Bot\u00E3o de navega\u00E7\u00E3o (anterior) inside -->\r\n <button *ngIf=\"config.showNavigationButtons && config.navigationButtonsPosition === 'inside'\"\r\n class=\"carousel-nav-button carousel-prev-button inside\"\r\n [disabled]=\"shouldDisableNavButton('prev')\"\r\n (click)=\"prev()\">\r\n <i class=\"fas fa-angle-left\"></i>\r\n </button>\r\n <!-- Palco (\u00E1rea de conte\u00FAdo) -->\r\n <div class=\"carousel-stage\" #carouselStage\r\n [class.is-dragging]=\"isDragging\">\r\n <!-- Modifica\u00E7\u00E3o no carousel-content -->\r\n <div class=\"carousel-content\" #carouselContent>\r\n <ng-container *ngFor=\"let item of items; let i = index; trackBy: trackByFn\">\r\n <div class=\"carousel-item\"\r\n [class.active]=\"item.active\"\r\n [class.dragging]=\"isDragging\"\r\n (click)=\"!isDragging && itemClick.emit(item)\">\r\n <!-- Renderiza\u00E7\u00E3o de imagem com tratamento para pr\u00E9-carregamento -->\r\n <div *ngIf=\"item.imageUrl\" class=\"carousel-image-container\">\r\n <!-- Modificado para carregar imagem diretamente, mas usar opacity para o efeito de fade -->\r\n <img \r\n [src]=\"item.imageUrl\" \r\n [alt]=\"item.imageAlt || item.title || ''\" \r\n class=\"carousel-image\"\r\n [ngClass]=\"{'lazyloaded': loadedImages.has(item.imageUrl)}\"\r\n (load)=\"loadedImages.add(item.imageUrl)\">\r\n \r\n <!-- Imagem de placeholder enquanto carrega -->\r\n <div *ngIf=\"!loadedImages.has(item.imageUrl)\" class=\"carousel-image-placeholder\"></div>\r\n </div>\r\n\r\n <!-- Resto do conte\u00FAdo permanece igual -->\r\n <ng-container *ngIf=\"!item.imageUrl || contentTemplate\">\r\n <ng-container *ngTemplateOutlet=\"itemTemplate; context: {$implicit: item}\"></ng-container>\r\n </ng-container>\r\n </div>\r\n </ng-container>\r\n </div>\r\n </div>\r\n \r\n <!-- Bot\u00E3o de navega\u00E7\u00E3o (pr\u00F3ximo) inside -->\r\n <button *ngIf=\"config.showNavigationButtons && config.navigationButtonsPosition === 'inside'\"\r\n class=\"carousel-nav-button carousel-next-button inside\"\r\n [disabled]=\"shouldDisableNavButton('next')\"\r\n (click)=\"next()\">\r\n <i class=\"fas fa-angle-right\"></i>\r\n </button>\r\n\r\n <!-- Bot\u00E3o de reprodu\u00E7\u00E3o (interno ao palco) -->\r\n <div *ngIf=\"config.showPlayButtons && config.navigationButtonsPosition === 'inside'\"\r\n class=\"carousel-play-button-container\">\r\n <button class=\"carousel-play-button\" (click)=\"togglePlay()\">\r\n <i [class]=\"isPlaying ? 'fas fa-pause' : 'fas fa-play'\"></i>\r\n </button>\r\n </div>\r\n\r\n <!-- Indicador de p\u00E1ginas (interno ao palco) -->\r\n <div *ngIf=\"config.showIndicator && config.indicatorPosition === 'inside'\"\r\n class=\"carousel-indicator-container inside\">\r\n <div *ngIf=\"config.indicatorType === 'simple'\" class=\"carousel-indicator\">\r\n <span *ngFor=\"let page of pages\"\r\n class=\"indicator-dot\"\r\n [class.active]=\"page === currentPageIndex\"\r\n (click)=\"goToPage(page)\"></span>\r\n </div>\r\n </div>\r\n </div>\r\n \r\n <!-- Bot\u00E3o de navega\u00E7\u00E3o (pr\u00F3ximo) outside -->\r\n <button *ngIf=\"config.showNavigationButtons && config.navigationButtonsPosition === 'outside'\"\r\n class=\"carousel-nav-button carousel-next-button outside\"\r\n [disabled]=\"shouldDisableNavButton('next')\"\r\n (click)=\"next()\">\r\n <i class=\"fas fa-angle-right\"></i>\r\n </button>\r\n</div>\r\n\r\n<!-- Bot\u00E3o de reprodu\u00E7\u00E3o (externo ao palco) -->\r\n<div *ngIf=\"config.showPlayButtons && config.navigationButtonsPosition === 'outside'\"\r\n class=\"carousel-play-button-container outside\">\r\n <button class=\"carousel-play-button\" (click)=\"togglePlay()\">\r\n <i [class]=\"isPlaying ? 'fas fa-pause' : 'fas fa-play'\"></i>\r\n </button>\r\n</div>\r\n\r\n<!-- Indicador de p\u00E1ginas (externo ao palco) -->\r\n<div *ngIf=\"config.showIndicator && config.indicatorPosition === 'outside'\"\r\n class=\"carousel-indicator-container outside\">\r\n <div *ngIf=\"config.indicatorType === 'simple'\" class=\"carousel-indicator\">\r\n <span *ngFor=\"let page of pages\"\r\n class=\"indicator-dot\"\r\n [class.active]=\"page === currentPageIndex\"\r\n (click)=\"goToPage(page)\"></span>\r\n </div>\r\n <div *ngIf=\"config.indicatorType === 'text'\" class=\"carousel-indicator-text\">\r\n {{ currentPage }}/{{ totalPages }}\r\n </div>\r\n</div>\r\n\r\n<!-- Template para o conte\u00FAdo -->\r\n<ng-template #itemTemplate let-item>\r\n <ng-content *ngIf=\"!contentTemplate\"></ng-content>\r\n <ng-container *ngIf=\"contentTemplate\">\r\n <ng-container *ngTemplateOutlet=\"contentTemplate; context: {$implicit: item}\"></ng-container>\r\n </ng-container>\r\n</ng-template>\r\n<ng-content select=\"[carouselContent]\"></ng-content>", styles: [":host{display:block}.carousel-wrapper{display:flex;align-items:center;position:relative;width:100%}.carousel-container{position:relative;width:100%;overflow:hidden}.carousel-container:not(:hover) .carousel-play-button-container:not(.outside){opacity:0;transition:opacity .3s ease}.carousel-container:hover .carousel-play-button-container:not(.outside){opacity:1;transition:opacity .3s ease}.carousel-stage{position:relative;overflow:hidden;width:100%;touch-action:pan-y;-webkit-user-select:none;user-select:none;cursor:grab}.carousel-stage.is-dragging{cursor:grabbing}.carousel-content{display:flex;position:relative;height:100%;width:100%;will-change:transform;touch-action:none;transition:transform .3s ease-in-out}.carousel-block{display:flex;width:100%;position:relative;transition:transform var(--transition-duration, .3s) var(--animation-ease-in-out)}.carousel-block .carousel-item{flex-shrink:0;width:100%;height:100%;position:relative;flex:0 0 100%}.carousel-block .carousel-block .carousel-item{width:calc((100% - (var(--items-per-page, 3) - 1) * var(--item-spacing, 20px)) / var(--items-per-page, 3))}.carousel-block .carousel-item.dragging{pointer-events:none}.carousel-item{flex-shrink:0;width:100%;height:100%;position:relative}.carousel-block .carousel-item{width:calc((100% - (var(--items-per-page, 3) - 1) * var(--item-spacing, 20px)) / var(--items-per-page, 3))}.carousel-item.dragging{pointer-events:none}.carousel-image-container{position:relative;width:100%;height:100%;overflow:hidden}.carousel-image{width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .3s ease-in-out}.carousel-image.lazyloaded{opacity:1}.carousel-overlay{position:absolute;bottom:0;left:0;right:0;background:#000000b3;color:#fff;padding:15px}.carousel-overlay h3{margin:0 0 8px;font-size:1.2em}.carousel-overlay p{margin:0;font-size:.9em}.carousel-nav-button{background:#ffffff80;border:none;border-radius:50%;width:40px;height:40px;display:flex;justify-content:center;align-items:center;cursor:pointer;z-index:2;transition:background-color .3s}.carousel-nav-button:hover{background:#fffc}.carousel-nav-button:disabled{opacity:.3;cursor:not-allowed}.carousel-nav-button.inside{position:absolute;top:50%;transform:translateY(-50%)}.carousel-nav-button.outside{margin:0 10px}.carousel-nav-button.carousel-prev-but