@devjuliovilla/jv-ui 1.10.0 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, computed, Component, forwardRef, inject, output, Directive, Injectable, signal, InjectionToken, makeEnvironmentProviders, ENVIRONMENT_INITIALIZER, effect, contentChildren, HostListener, viewChild, viewChildren, afterNextRender, Renderer2, ElementRef } from '@angular/core';
2
+ import { input, computed, Component, forwardRef, inject, output, Directive, Injectable, signal, InjectionToken, makeEnvironmentProviders, ENVIRONMENT_INITIALIZER, effect, contentChildren, HostListener, viewChild, viewChildren, numberAttribute, afterNextRender, Renderer2, ElementRef } from '@angular/core';
3
3
  import * as i1 from '@angular/forms';
4
4
  import { NG_VALUE_ACCESSOR, FormBuilder, Validators, ReactiveFormsModule, FormsModule } from '@angular/forms';
5
5
  import { CommonModule, DOCUMENT, NgTemplateOutlet } from '@angular/common';
@@ -18729,6 +18729,7 @@ const JV_UI_DEFAULT_CONFIG = {
18729
18729
  percent: { style: 'percent', minimumFractionDigits: 0, maximumFractionDigits: 2 },
18730
18730
  },
18731
18731
  toastPosition: 'bottom-right',
18732
+ tabsTransition: 'none',
18732
18733
  };
18733
18734
 
18734
18735
  class JvFormatService {
@@ -19026,6 +19027,12 @@ const ES = {
19026
19027
  'dialog.cancel': 'Cancelar',
19027
19028
  'common.expanded': 'Expandido',
19028
19029
  'common.collapsed': 'Colapsado',
19030
+ 'slider.prevImage': 'Imagen anterior',
19031
+ 'slider.nextImage': 'Imagen siguiente',
19032
+ 'slider.thumbnailList': 'Miniaturas de imagenes',
19033
+ 'gallery.close': 'Cerrar',
19034
+ 'gallery.prevImage': 'Imagen anterior',
19035
+ 'gallery.nextImage': 'Imagen siguiente',
19029
19036
  };
19030
19037
 
19031
19038
  const EN = {
@@ -19088,6 +19095,12 @@ const EN = {
19088
19095
  'dialog.cancel': 'Cancel',
19089
19096
  'common.expanded': 'Expanded',
19090
19097
  'common.collapsed': 'Collapsed',
19098
+ 'slider.prevImage': 'Previous image',
19099
+ 'slider.nextImage': 'Next image',
19100
+ 'slider.thumbnailList': 'Image thumbnails',
19101
+ 'gallery.close': 'Close',
19102
+ 'gallery.prevImage': 'Previous image',
19103
+ 'gallery.nextImage': 'Next image',
19091
19104
  };
19092
19105
 
19093
19106
  const JV_LOCALE_DICTIONARIES = {
@@ -19923,10 +19936,10 @@ class JvDatePickerComponent {
19923
19936
  visibleMonth = signal(firstOfMonth(new Date()), ...(ngDevMode ? [{ debugName: "visibleMonth" }] : /* istanbul ignore next */ []));
19924
19937
  focusedDate = signal(startOfDay(new Date()), ...(ngDevMode ? [{ debugName: "focusedDate" }] : /* istanbul ignore next */ []));
19925
19938
  dialogId = `jv-date-picker-dialog-${this.instanceId}`;
19926
- value = null;
19939
+ value = signal(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
19927
19940
  disabled = false;
19928
19941
  resolvedDialogTitle = computed(() => this.dialogTitle() || this.translation.translate('datePicker.dialogTitle'), ...(ngDevMode ? [{ debugName: "resolvedDialogTitle" }] : /* istanbul ignore next */ []));
19929
- currentValue = computed(() => this.valueInput() !== undefined ? this.valueInput() : this.value, ...(ngDevMode ? [{ debugName: "currentValue" }] : /* istanbul ignore next */ []));
19942
+ currentValue = computed(() => this.valueInput() !== undefined ? this.valueInput() : this.value(), ...(ngDevMode ? [{ debugName: "currentValue" }] : /* istanbul ignore next */ []));
19930
19943
  triggerAriaLabel = computed(() => {
19931
19944
  const name = this.label() || this.resolvedDialogTitle();
19932
19945
  return `${name}. ${this.displayValue()}`;
@@ -19974,7 +19987,7 @@ class JvDatePickerComponent {
19974
19987
  onChange = () => undefined;
19975
19988
  onTouched = () => undefined;
19976
19989
  writeValue(value) {
19977
- this.value = value ? startOfDay(value) : null;
19990
+ this.value.set(value ? startOfDay(value) : null);
19978
19991
  }
19979
19992
  registerOnChange(fn) {
19980
19993
  this.onChange = fn;
@@ -20080,7 +20093,7 @@ class JvDatePickerComponent {
20080
20093
  this.onTouched();
20081
20094
  }
20082
20095
  updateValue(value) {
20083
- this.value = value;
20096
+ this.value.set(value);
20084
20097
  this.onChange(value);
20085
20098
  this.selectionChange.emit(value);
20086
20099
  }
@@ -21242,6 +21255,535 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
21242
21255
  `, styles: [".jv-textarea{width:100%;min-height:var(--jv-density-control-height);padding:var(--jv-spacing-sm) var(--jv-spacing-md);border:1px solid var(--jv-color-border);border-radius:var(--jv-radius-md);background:var(--jv-color-surface);color:var(--jv-color-foreground);resize:vertical;font-family:inherit;font-size:inherit;line-height:inherit;box-sizing:border-box}.jv-textarea::placeholder{color:var(--jv-color-foreground-muted)}.jv-textarea.is-invalid{border-color:var(--jv-color-danger)}.jv-textarea:disabled{opacity:.7;cursor:not-allowed}.jv-textarea:read-only{background:var(--jv-color-surface-muted);cursor:default}\n"] }]
21243
21256
  }], propDecorators: { placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], describedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "describedBy", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], cols: [{ type: i0.Input, args: [{ isSignal: true, alias: "cols", required: false }] }], maxlength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxlength", required: false }] }], inputId: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputId", required: false }] }] } });
21244
21257
 
21258
+ class JvImageGalleryComponent {
21259
+ translation = inject(JvTranslationService);
21260
+ images = input([], ...(ngDevMode ? [{ debugName: "images" }] : /* istanbul ignore next */ []));
21261
+ stripHeightVh = input(25, { ...(ngDevMode ? { debugName: "stripHeightVh" } : /* istanbul ignore next */ {}), transform: numberAttribute });
21262
+ actions = input([], ...(ngDevMode ? [{ debugName: "actions" }] : /* istanbul ignore next */ []));
21263
+ imageClick = output();
21264
+ actionClick = output();
21265
+ strip = viewChild('strip', ...(ngDevMode ? [{ debugName: "strip" }] : /* istanbul ignore next */ []));
21266
+ dialogOpen = signal(false, ...(ngDevMode ? [{ debugName: "dialogOpen" }] : /* istanbul ignore next */ []));
21267
+ currentIndex = signal(0, ...(ngDevMode ? [{ debugName: "currentIndex" }] : /* istanbul ignore next */ []));
21268
+ slideDirection = signal(null, ...(ngDevMode ? [{ debugName: "slideDirection" }] : /* istanbul ignore next */ []));
21269
+ hasMultiple = computed(() => this.images().length > 1, ...(ngDevMode ? [{ debugName: "hasMultiple" }] : /* istanbul ignore next */ []));
21270
+ stripHeightStyle = computed(() => `${this.clampStripHeight(this.stripHeightVh())}vh`, ...(ngDevMode ? [{ debugName: "stripHeightStyle" }] : /* istanbul ignore next */ []));
21271
+ currentImage = computed(() => this.images()[this.currentIndex()] ?? null, ...(ngDevMode ? [{ debugName: "currentImage" }] : /* istanbul ignore next */ []));
21272
+ dialogTitle = computed(() => {
21273
+ const image = this.currentImage();
21274
+ if (!image)
21275
+ return '';
21276
+ const counter = this.hasMultiple() ? ` (${this.currentIndex() + 1}/${this.images().length})` : '';
21277
+ return `${image.alt}${counter}`;
21278
+ }, ...(ngDevMode ? [{ debugName: "dialogTitle" }] : /* istanbul ignore next */ []));
21279
+ translatedLabels = computed(() => ({
21280
+ close: this.translation.translate('gallery.close'),
21281
+ prev: this.translation.translate('gallery.prevImage'),
21282
+ next: this.translation.translate('gallery.nextImage'),
21283
+ }), ...(ngDevMode ? [{ debugName: "translatedLabels" }] : /* istanbul ignore next */ []));
21284
+ handleKeydown(event) {
21285
+ if (!this.dialogOpen())
21286
+ return;
21287
+ switch (event.key) {
21288
+ case 'ArrowLeft':
21289
+ event.preventDefault();
21290
+ this.previous();
21291
+ break;
21292
+ case 'ArrowRight':
21293
+ event.preventDefault();
21294
+ this.next();
21295
+ break;
21296
+ }
21297
+ }
21298
+ onWheel(event) {
21299
+ const el = this.strip()?.nativeElement;
21300
+ if (!el)
21301
+ return;
21302
+ if (Math.abs(event.deltaY) > Math.abs(event.deltaX)) {
21303
+ const delta = event.deltaMode === WheelEvent.DOM_DELTA_LINE
21304
+ ? event.deltaY * 32
21305
+ : event.deltaMode === WheelEvent.DOM_DELTA_PAGE
21306
+ ? event.deltaY * el.clientWidth
21307
+ : event.deltaY;
21308
+ const firstThumb = el.querySelector('.jv-gallery-thumb');
21309
+ const gap = Number.parseFloat(getComputedStyle(el).columnGap || getComputedStyle(el).gap || '0') || 0;
21310
+ const step = firstThumb ? firstThumb.getBoundingClientRect().width + gap : 0;
21311
+ const offset = Math.sign(delta) * Math.max(Math.abs(delta), step);
21312
+ event.preventDefault();
21313
+ el.scrollLeft += offset;
21314
+ }
21315
+ }
21316
+ openDialog(index) {
21317
+ this.currentIndex.set(index);
21318
+ this.slideDirection.set(null);
21319
+ this.dialogOpen.set(true);
21320
+ this.imageClick.emit(this.images()[index]);
21321
+ }
21322
+ closeDialog() {
21323
+ this.dialogOpen.set(false);
21324
+ this.slideDirection.set(null);
21325
+ }
21326
+ previous() {
21327
+ if (!this.hasMultiple())
21328
+ return;
21329
+ const newIndex = this.currentIndex() === 0
21330
+ ? this.images().length - 1
21331
+ : this.currentIndex() - 1;
21332
+ this.showImage(newIndex, 'right');
21333
+ }
21334
+ next() {
21335
+ if (!this.hasMultiple())
21336
+ return;
21337
+ const newIndex = this.currentIndex() === this.images().length - 1
21338
+ ? 0
21339
+ : this.currentIndex() + 1;
21340
+ this.showImage(newIndex, 'left');
21341
+ }
21342
+ onAction(action) {
21343
+ const image = this.currentImage();
21344
+ if (image) {
21345
+ this.actionClick.emit({ actionId: action.id, image });
21346
+ }
21347
+ }
21348
+ clampStripHeight(value) {
21349
+ if (!Number.isFinite(value)) {
21350
+ return 25;
21351
+ }
21352
+ return Math.min(100, Math.max(0, value));
21353
+ }
21354
+ showImage(index, direction) {
21355
+ this.slideDirection.set(null);
21356
+ this.currentIndex.set(index);
21357
+ if (typeof requestAnimationFrame === 'function') {
21358
+ requestAnimationFrame(() => this.slideDirection.set(direction));
21359
+ return;
21360
+ }
21361
+ this.slideDirection.set(direction);
21362
+ }
21363
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JvImageGalleryComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
21364
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: JvImageGalleryComponent, isStandalone: true, selector: "jv-image-gallery", inputs: { images: { classPropertyName: "images", publicName: "images", isSignal: true, isRequired: false, transformFunction: null }, stripHeightVh: { classPropertyName: "stripHeightVh", publicName: "stripHeightVh", isSignal: true, isRequired: false, transformFunction: null }, actions: { classPropertyName: "actions", publicName: "actions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { imageClick: "imageClick", actionClick: "actionClick" }, host: { listeners: { "document:keydown": "handleKeydown($event)" } }, viewQueries: [{ propertyName: "strip", first: true, predicate: ["strip"], descendants: true, isSignal: true }], ngImport: i0, template: `
21365
+ <div class="jv-gallery">
21366
+ <div class="jv-gallery-strip" #strip [style.height]="stripHeightStyle()" (wheel)="onWheel($event)">
21367
+ @for (image of images(); track image.src; let i = $index) {
21368
+ <button
21369
+ type="button"
21370
+ class="jv-gallery-thumb"
21371
+ [class.jv-gallery-thumb--active]="dialogOpen() && currentIndex() === i"
21372
+ [attr.aria-label]="image.alt"
21373
+ (wheel)="onWheel($event)"
21374
+ (click)="openDialog(i)"
21375
+ >
21376
+ <img
21377
+ class="jv-gallery-thumb-image"
21378
+ [src]="image.thumbnail || image.src"
21379
+ [alt]="image.alt"
21380
+ loading="lazy"
21381
+ />
21382
+ </button>
21383
+ }
21384
+ </div>
21385
+
21386
+ <jv-dialog
21387
+ [open]="dialogOpen()"
21388
+ [title]="dialogTitle()"
21389
+ (closed)="closeDialog()"
21390
+ >
21391
+ <div class="jv-gallery-viewer" tabindex="0">
21392
+ @if (currentImage(); as image) {
21393
+ <img
21394
+ class="jv-gallery-viewer-image"
21395
+ [class.jv-gallery-viewer-image--slide-left]="slideDirection() === 'left'"
21396
+ [class.jv-gallery-viewer-image--slide-right]="slideDirection() === 'right'"
21397
+ [src]="image.src"
21398
+ [alt]="image.alt"
21399
+ />
21400
+ }
21401
+
21402
+ @if (hasMultiple()) {
21403
+ <button
21404
+ type="button"
21405
+ class="jv-gallery-viewer-nav jv-gallery-viewer-nav--prev"
21406
+ [attr.aria-label]="translatedLabels().prev"
21407
+ (click)="previous(); $event.stopPropagation()"
21408
+ >
21409
+ <jv-icon name="chevron-left" [size]="24" />
21410
+ </button>
21411
+
21412
+ <button
21413
+ type="button"
21414
+ class="jv-gallery-viewer-nav jv-gallery-viewer-nav--next"
21415
+ [attr.aria-label]="translatedLabels().next"
21416
+ (click)="next(); $event.stopPropagation()"
21417
+ >
21418
+ <jv-icon name="chevron-right" [size]="24" />
21419
+ </button>
21420
+
21421
+ <span class="jv-gallery-viewer-counter">
21422
+ {{ currentIndex() + 1 }} / {{ images().length }}
21423
+ </span>
21424
+ }
21425
+ </div>
21426
+
21427
+ <div dialog-actions>
21428
+ @for (action of actions(); track action.id) {
21429
+ <jv-button
21430
+ [variant]="action.tone ?? 'outline'"
21431
+ (click)="onAction(action)"
21432
+ >
21433
+ @if (action.icon) {
21434
+ <jv-icon [name]="action.icon" [size]="16" />
21435
+ }
21436
+ {{ action.label }}
21437
+ </jv-button>
21438
+ }
21439
+ <jv-button variant="ghost" (click)="closeDialog()">
21440
+ {{ translatedLabels().close }}
21441
+ </jv-button>
21442
+ </div>
21443
+ </jv-dialog>
21444
+ </div>
21445
+ `, isInline: true, styles: [".jv-gallery{display:block;width:100%;max-width:100vw}.jv-gallery-strip{display:flex;gap:5px;width:100%;overflow-x:auto;overflow-y:hidden;overscroll-behavior-x:contain;scrollbar-width:none;-ms-overflow-style:none;scroll-snap-type:x proximity;scroll-behavior:smooth;-webkit-overflow-scrolling:touch}.jv-gallery-strip::-webkit-scrollbar{display:none}.jv-gallery-thumb{flex:0 0 auto;width:calc((100vw - 20px)/4);min-width:120px;height:100%;padding:0;border:2px solid transparent;border-radius:var(--jv-radius-sm);background:transparent;cursor:pointer;overflow:hidden;scroll-snap-align:start;transition:border-color .16s ease,opacity .16s ease}.jv-gallery-thumb:hover{opacity:.85}.jv-gallery-thumb--active{border-color:var(--jv-color-primary)}.jv-gallery-thumb:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:1px}.jv-gallery-thumb-image{display:block;width:100%;height:100%;object-fit:cover;pointer-events:none}.jv-gallery-viewer{position:relative;display:flex;align-items:center;justify-content:center;width:100%;height:70vh;min-height:300px;background:#000;border-radius:var(--jv-radius-md);overflow:hidden}.jv-gallery-viewer:focus{outline:none}.jv-gallery-viewer-image{display:block;max-width:100%;max-height:100%;object-fit:contain;-webkit-user-select:none;user-select:none}@keyframes gallery-slide-left{0%{transform:translate(100%);opacity:0}to{transform:translate(0);opacity:1}}@keyframes gallery-slide-right{0%{transform:translate(-100%);opacity:0}to{transform:translate(0);opacity:1}}.jv-gallery-viewer-image--slide-left{animation:gallery-slide-left .28s ease-out}.jv-gallery-viewer-image--slide-right{animation:gallery-slide-right .28s ease-out}.jv-gallery-viewer-nav{position:absolute;top:50%;transform:translateY(-50%);display:inline-flex;align-items:center;justify-content:center;width:3rem;height:3rem;border:none;border-radius:var(--jv-radius-md);background:#00000080;color:#fff;cursor:pointer;transition:background-color .16s ease;z-index:1}.jv-gallery-viewer-nav:hover{background:#000000bf}.jv-gallery-viewer-nav:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:1px}.jv-gallery-viewer-nav--prev{left:var(--jv-spacing-md)}.jv-gallery-viewer-nav--next{right:var(--jv-spacing-md)}.jv-gallery-viewer-counter{position:absolute;bottom:var(--jv-spacing-md);left:50%;transform:translate(-50%);padding:var(--jv-spacing-xs) var(--jv-spacing-sm);border-radius:var(--jv-radius-sm);background:#0009;color:#fff;font-size:.8125rem;font-variant-numeric:tabular-nums;pointer-events:none}@media(max-width:768px){.jv-gallery-thumb{width:calc(100vw - 20px);min-width:200px}.jv-gallery-viewer-nav{width:2.5rem;height:2.5rem}.jv-gallery-viewer-nav--prev{left:var(--jv-spacing-sm)}.jv-gallery-viewer-nav--next{right:var(--jv-spacing-sm)}}@media(prefers-reduced-motion:reduce){.jv-gallery-strip{scroll-behavior:auto}.jv-gallery-viewer-image--slide-left,.jv-gallery-viewer-image--slide-right{animation:none}}\n"], dependencies: [{ kind: "component", type: JvButtonComponent, selector: "jv-button", inputs: ["type", "variant", "icon", "iconPosition", "loading", "disabled"] }, { kind: "component", type: JvDialogComponent, selector: "jv-dialog", inputs: ["open", "title"], outputs: ["closed"] }, { kind: "component", type: JvIconComponent, selector: "jv-icon", inputs: ["name", "size", "strokeWidth", "decorative", "ariaLabel"] }] });
21446
+ }
21447
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JvImageGalleryComponent, decorators: [{
21448
+ type: Component,
21449
+ args: [{ selector: 'jv-image-gallery', standalone: true, imports: [JvButtonComponent, JvDialogComponent, JvIconComponent], template: `
21450
+ <div class="jv-gallery">
21451
+ <div class="jv-gallery-strip" #strip [style.height]="stripHeightStyle()" (wheel)="onWheel($event)">
21452
+ @for (image of images(); track image.src; let i = $index) {
21453
+ <button
21454
+ type="button"
21455
+ class="jv-gallery-thumb"
21456
+ [class.jv-gallery-thumb--active]="dialogOpen() && currentIndex() === i"
21457
+ [attr.aria-label]="image.alt"
21458
+ (wheel)="onWheel($event)"
21459
+ (click)="openDialog(i)"
21460
+ >
21461
+ <img
21462
+ class="jv-gallery-thumb-image"
21463
+ [src]="image.thumbnail || image.src"
21464
+ [alt]="image.alt"
21465
+ loading="lazy"
21466
+ />
21467
+ </button>
21468
+ }
21469
+ </div>
21470
+
21471
+ <jv-dialog
21472
+ [open]="dialogOpen()"
21473
+ [title]="dialogTitle()"
21474
+ (closed)="closeDialog()"
21475
+ >
21476
+ <div class="jv-gallery-viewer" tabindex="0">
21477
+ @if (currentImage(); as image) {
21478
+ <img
21479
+ class="jv-gallery-viewer-image"
21480
+ [class.jv-gallery-viewer-image--slide-left]="slideDirection() === 'left'"
21481
+ [class.jv-gallery-viewer-image--slide-right]="slideDirection() === 'right'"
21482
+ [src]="image.src"
21483
+ [alt]="image.alt"
21484
+ />
21485
+ }
21486
+
21487
+ @if (hasMultiple()) {
21488
+ <button
21489
+ type="button"
21490
+ class="jv-gallery-viewer-nav jv-gallery-viewer-nav--prev"
21491
+ [attr.aria-label]="translatedLabels().prev"
21492
+ (click)="previous(); $event.stopPropagation()"
21493
+ >
21494
+ <jv-icon name="chevron-left" [size]="24" />
21495
+ </button>
21496
+
21497
+ <button
21498
+ type="button"
21499
+ class="jv-gallery-viewer-nav jv-gallery-viewer-nav--next"
21500
+ [attr.aria-label]="translatedLabels().next"
21501
+ (click)="next(); $event.stopPropagation()"
21502
+ >
21503
+ <jv-icon name="chevron-right" [size]="24" />
21504
+ </button>
21505
+
21506
+ <span class="jv-gallery-viewer-counter">
21507
+ {{ currentIndex() + 1 }} / {{ images().length }}
21508
+ </span>
21509
+ }
21510
+ </div>
21511
+
21512
+ <div dialog-actions>
21513
+ @for (action of actions(); track action.id) {
21514
+ <jv-button
21515
+ [variant]="action.tone ?? 'outline'"
21516
+ (click)="onAction(action)"
21517
+ >
21518
+ @if (action.icon) {
21519
+ <jv-icon [name]="action.icon" [size]="16" />
21520
+ }
21521
+ {{ action.label }}
21522
+ </jv-button>
21523
+ }
21524
+ <jv-button variant="ghost" (click)="closeDialog()">
21525
+ {{ translatedLabels().close }}
21526
+ </jv-button>
21527
+ </div>
21528
+ </jv-dialog>
21529
+ </div>
21530
+ `, styles: [".jv-gallery{display:block;width:100%;max-width:100vw}.jv-gallery-strip{display:flex;gap:5px;width:100%;overflow-x:auto;overflow-y:hidden;overscroll-behavior-x:contain;scrollbar-width:none;-ms-overflow-style:none;scroll-snap-type:x proximity;scroll-behavior:smooth;-webkit-overflow-scrolling:touch}.jv-gallery-strip::-webkit-scrollbar{display:none}.jv-gallery-thumb{flex:0 0 auto;width:calc((100vw - 20px)/4);min-width:120px;height:100%;padding:0;border:2px solid transparent;border-radius:var(--jv-radius-sm);background:transparent;cursor:pointer;overflow:hidden;scroll-snap-align:start;transition:border-color .16s ease,opacity .16s ease}.jv-gallery-thumb:hover{opacity:.85}.jv-gallery-thumb--active{border-color:var(--jv-color-primary)}.jv-gallery-thumb:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:1px}.jv-gallery-thumb-image{display:block;width:100%;height:100%;object-fit:cover;pointer-events:none}.jv-gallery-viewer{position:relative;display:flex;align-items:center;justify-content:center;width:100%;height:70vh;min-height:300px;background:#000;border-radius:var(--jv-radius-md);overflow:hidden}.jv-gallery-viewer:focus{outline:none}.jv-gallery-viewer-image{display:block;max-width:100%;max-height:100%;object-fit:contain;-webkit-user-select:none;user-select:none}@keyframes gallery-slide-left{0%{transform:translate(100%);opacity:0}to{transform:translate(0);opacity:1}}@keyframes gallery-slide-right{0%{transform:translate(-100%);opacity:0}to{transform:translate(0);opacity:1}}.jv-gallery-viewer-image--slide-left{animation:gallery-slide-left .28s ease-out}.jv-gallery-viewer-image--slide-right{animation:gallery-slide-right .28s ease-out}.jv-gallery-viewer-nav{position:absolute;top:50%;transform:translateY(-50%);display:inline-flex;align-items:center;justify-content:center;width:3rem;height:3rem;border:none;border-radius:var(--jv-radius-md);background:#00000080;color:#fff;cursor:pointer;transition:background-color .16s ease;z-index:1}.jv-gallery-viewer-nav:hover{background:#000000bf}.jv-gallery-viewer-nav:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:1px}.jv-gallery-viewer-nav--prev{left:var(--jv-spacing-md)}.jv-gallery-viewer-nav--next{right:var(--jv-spacing-md)}.jv-gallery-viewer-counter{position:absolute;bottom:var(--jv-spacing-md);left:50%;transform:translate(-50%);padding:var(--jv-spacing-xs) var(--jv-spacing-sm);border-radius:var(--jv-radius-sm);background:#0009;color:#fff;font-size:.8125rem;font-variant-numeric:tabular-nums;pointer-events:none}@media(max-width:768px){.jv-gallery-thumb{width:calc(100vw - 20px);min-width:200px}.jv-gallery-viewer-nav{width:2.5rem;height:2.5rem}.jv-gallery-viewer-nav--prev{left:var(--jv-spacing-sm)}.jv-gallery-viewer-nav--next{right:var(--jv-spacing-sm)}}@media(prefers-reduced-motion:reduce){.jv-gallery-strip{scroll-behavior:auto}.jv-gallery-viewer-image--slide-left,.jv-gallery-viewer-image--slide-right{animation:none}}\n"] }]
21531
+ }], propDecorators: { images: [{ type: i0.Input, args: [{ isSignal: true, alias: "images", required: false }] }], stripHeightVh: [{ type: i0.Input, args: [{ isSignal: true, alias: "stripHeightVh", required: false }] }], actions: [{ type: i0.Input, args: [{ isSignal: true, alias: "actions", required: false }] }], imageClick: [{ type: i0.Output, args: ["imageClick"] }], actionClick: [{ type: i0.Output, args: ["actionClick"] }], strip: [{ type: i0.ViewChild, args: ['strip', { isSignal: true }] }], handleKeydown: [{
21532
+ type: HostListener,
21533
+ args: ['document:keydown', ['$event']]
21534
+ }] } });
21535
+
21536
+ class JvImageSliderComponent {
21537
+ thumbnailStripRef = viewChild('thumbnailStrip', ...(ngDevMode ? [{ debugName: "thumbnailStripRef" }] : /* istanbul ignore next */ []));
21538
+ translation = inject(JvTranslationService);
21539
+ images = input([], ...(ngDevMode ? [{ debugName: "images" }] : /* istanbul ignore next */ []));
21540
+ showThumbnails = input(true, ...(ngDevMode ? [{ debugName: "showThumbnails" }] : /* istanbul ignore next */ []));
21541
+ showDescription = input(true, ...(ngDevMode ? [{ debugName: "showDescription" }] : /* istanbul ignore next */ []));
21542
+ imageChange = output();
21543
+ currentIndex = signal(0, ...(ngDevMode ? [{ debugName: "currentIndex" }] : /* istanbul ignore next */ []));
21544
+ hasMultiple = computed(() => this.images().length > 1, ...(ngDevMode ? [{ debugName: "hasMultiple" }] : /* istanbul ignore next */ []));
21545
+ currentImage = computed(() => this.images()[this.currentIndex()] ?? null, ...(ngDevMode ? [{ debugName: "currentImage" }] : /* istanbul ignore next */ []));
21546
+ translatedAriaLabels = computed(() => ({
21547
+ prev: this.translation.translate('slider.prevImage'),
21548
+ next: this.translation.translate('slider.nextImage'),
21549
+ thumbnailList: this.translation.translate('slider.thumbnailList'),
21550
+ }), ...(ngDevMode ? [{ debugName: "translatedAriaLabels" }] : /* istanbul ignore next */ []));
21551
+ constructor() {
21552
+ effect(() => {
21553
+ const images = this.images();
21554
+ if (images.length > 0 && this.currentIndex() >= images.length) {
21555
+ this.currentIndex.set(0);
21556
+ }
21557
+ });
21558
+ }
21559
+ handleKeydown(event) {
21560
+ if (!this.hasMultiple()) {
21561
+ return;
21562
+ }
21563
+ switch (event.key) {
21564
+ case 'ArrowLeft':
21565
+ event.preventDefault();
21566
+ this.previous();
21567
+ break;
21568
+ case 'ArrowRight':
21569
+ event.preventDefault();
21570
+ this.next();
21571
+ break;
21572
+ case 'Home':
21573
+ event.preventDefault();
21574
+ this.goTo(0);
21575
+ break;
21576
+ case 'End':
21577
+ event.preventDefault();
21578
+ this.goTo(this.images().length - 1);
21579
+ break;
21580
+ }
21581
+ }
21582
+ previous() {
21583
+ if (!this.hasMultiple()) {
21584
+ return;
21585
+ }
21586
+ const newIndex = this.currentIndex() === 0
21587
+ ? this.images().length - 1
21588
+ : this.currentIndex() - 1;
21589
+ this.goTo(newIndex);
21590
+ }
21591
+ next() {
21592
+ if (!this.hasMultiple()) {
21593
+ return;
21594
+ }
21595
+ const newIndex = this.currentIndex() === this.images().length - 1
21596
+ ? 0
21597
+ : this.currentIndex() + 1;
21598
+ this.goTo(newIndex);
21599
+ }
21600
+ goTo(index) {
21601
+ if (index === this.currentIndex() || index < 0 || index >= this.images().length) {
21602
+ return;
21603
+ }
21604
+ this.currentIndex.set(index);
21605
+ this.imageChange.emit(index);
21606
+ this.scrollThumbnailIntoView(index);
21607
+ }
21608
+ scrollThumbnailIntoView(index) {
21609
+ const strip = this.thumbnailStripRef()?.nativeElement;
21610
+ if (!strip) {
21611
+ return;
21612
+ }
21613
+ const thumbnails = strip.children;
21614
+ if (thumbnails[index]) {
21615
+ thumbnails[index].scrollIntoView({
21616
+ behavior: 'smooth',
21617
+ block: 'nearest',
21618
+ inline: 'center',
21619
+ });
21620
+ }
21621
+ }
21622
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JvImageSliderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
21623
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: JvImageSliderComponent, isStandalone: true, selector: "jv-image-slider", inputs: { images: { classPropertyName: "images", publicName: "images", isSignal: true, isRequired: false, transformFunction: null }, showThumbnails: { classPropertyName: "showThumbnails", publicName: "showThumbnails", isSignal: true, isRequired: false, transformFunction: null }, showDescription: { classPropertyName: "showDescription", publicName: "showDescription", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { imageChange: "imageChange" }, host: { listeners: { "keydown": "handleKeydown($event)" } }, viewQueries: [{ propertyName: "thumbnailStripRef", first: true, predicate: ["thumbnailStrip"], descendants: true, isSignal: true }], ngImport: i0, template: `
21624
+ <div class="jv-slider">
21625
+ <div class="jv-slider-main">
21626
+ @if (hasMultiple()) {
21627
+ <button
21628
+ type="button"
21629
+ class="jv-slider-nav jv-slider-nav--prev"
21630
+ [attr.aria-label]="translatedAriaLabels().prev"
21631
+ (click)="previous()"
21632
+ >
21633
+ <jv-icon name="chevron-left" [size]="20" />
21634
+ </button>
21635
+ }
21636
+
21637
+ <div class="jv-slider-stage">
21638
+ @for (image of images(); track image.src; let i = $index) {
21639
+ <div
21640
+ class="jv-slider-slide"
21641
+ [class.jv-slider-slide--active]="i === currentIndex()"
21642
+ >
21643
+ <img
21644
+ class="jv-slider-image"
21645
+ [src]="image.src"
21646
+ [alt]="image.alt"
21647
+ loading="lazy"
21648
+ />
21649
+ </div>
21650
+ }
21651
+ </div>
21652
+
21653
+ @if (hasMultiple()) {
21654
+ <button
21655
+ type="button"
21656
+ class="jv-slider-nav jv-slider-nav--next"
21657
+ [attr.aria-label]="translatedAriaLabels().next"
21658
+ (click)="next()"
21659
+ >
21660
+ <jv-icon name="chevron-right" [size]="20" />
21661
+ </button>
21662
+ }
21663
+ </div>
21664
+
21665
+ @if (currentImage(); as image) {
21666
+ @if (showDescription() && image.description) {
21667
+ <div class="jv-slider-description">
21668
+ <p>{{ image.description }}</p>
21669
+ </div>
21670
+ }
21671
+ }
21672
+
21673
+ @if (hasMultiple() && showThumbnails()) {
21674
+ <div class="jv-slider-thumbnails" #thumbnailStrip role="tablist" [attr.aria-label]="translatedAriaLabels().thumbnailList">
21675
+ @for (image of images(); track image.src; let i = $index) {
21676
+ <button
21677
+ type="button"
21678
+ class="jv-slider-thumbnail"
21679
+ [class.jv-slider-thumbnail--active]="i === currentIndex()"
21680
+ role="tab"
21681
+ [attr.aria-selected]="i === currentIndex() ? 'true' : 'false'"
21682
+ [attr.aria-label]="image.alt"
21683
+ (click)="goTo(i)"
21684
+ >
21685
+ <img
21686
+ class="jv-slider-thumbnail-image"
21687
+ [src]="image.thumbnail || image.src"
21688
+ [alt]="image.alt"
21689
+ loading="lazy"
21690
+ />
21691
+ </button>
21692
+ }
21693
+ </div>
21694
+
21695
+ <div class="jv-slider-counter">
21696
+ {{ currentIndex() + 1 }} / {{ images().length }}
21697
+ </div>
21698
+ }
21699
+ </div>
21700
+ `, isInline: true, styles: [".jv-slider{display:grid;gap:var(--jv-spacing-sm);width:100%;max-width:100%}.jv-slider-main{position:relative;display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:var(--jv-spacing-xs)}.jv-slider-stage{position:relative;aspect-ratio:16 / 9;overflow:hidden;border-radius:var(--jv-radius-md);background:var(--jv-color-surface-muted)}.jv-slider-slide{position:absolute;inset:0;opacity:0;transition:opacity .2s ease;pointer-events:none}.jv-slider-slide--active{opacity:1;pointer-events:auto}.jv-slider-image{display:block;width:100%;height:100%;object-fit:contain;-webkit-user-select:none;user-select:none}.jv-slider-nav{display:inline-flex;align-items:center;justify-content:center;width:2.5rem;height:2.5rem;border:1px solid var(--jv-color-border);border-radius:var(--jv-radius-md);background:var(--jv-color-surface);color:var(--jv-color-foreground);cursor:pointer;transition:background-color .16s ease,border-color .16s ease,color .16s ease;flex-shrink:0;z-index:1}.jv-slider-nav:hover{background:var(--jv-color-surface-muted);border-color:var(--jv-color-foreground-muted)}.jv-slider-nav:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:1px}.jv-slider-description{padding:var(--jv-spacing-sm) 0}.jv-slider-description p{margin:0;font-size:var(--jv-density-font-size);color:var(--jv-color-foreground-muted);line-height:1.5}.jv-slider-thumbnails{display:flex;gap:var(--jv-spacing-xs);overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none;padding:var(--jv-spacing-xs) 0}.jv-slider-thumbnails::-webkit-scrollbar{display:none}.jv-slider-thumbnail{flex:0 0 auto;width:3.5rem;height:3.5rem;padding:0;border:2px solid transparent;border-radius:var(--jv-radius-sm);background:transparent;cursor:pointer;overflow:hidden;transition:border-color .16s ease,opacity .16s ease;opacity:.6}.jv-slider-thumbnail:hover{opacity:.85}.jv-slider-thumbnail--active{border-color:var(--jv-color-primary);opacity:1}.jv-slider-thumbnail:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:1px}.jv-slider-thumbnail-image{display:block;width:100%;height:100%;object-fit:cover}.jv-slider-counter{text-align:center;font-size:.8125rem;color:var(--jv-color-foreground-muted);font-variant-numeric:tabular-nums}@media(max-width:640px){.jv-slider-nav{width:2rem;height:2rem}.jv-slider-thumbnail{width:2.75rem;height:2.75rem}}@media(prefers-reduced-motion:reduce){.jv-slider-slide{transition:none}}\n"], dependencies: [{ kind: "component", type: JvIconComponent, selector: "jv-icon", inputs: ["name", "size", "strokeWidth", "decorative", "ariaLabel"] }] });
21701
+ }
21702
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JvImageSliderComponent, decorators: [{
21703
+ type: Component,
21704
+ args: [{ selector: 'jv-image-slider', standalone: true, imports: [JvIconComponent], template: `
21705
+ <div class="jv-slider">
21706
+ <div class="jv-slider-main">
21707
+ @if (hasMultiple()) {
21708
+ <button
21709
+ type="button"
21710
+ class="jv-slider-nav jv-slider-nav--prev"
21711
+ [attr.aria-label]="translatedAriaLabels().prev"
21712
+ (click)="previous()"
21713
+ >
21714
+ <jv-icon name="chevron-left" [size]="20" />
21715
+ </button>
21716
+ }
21717
+
21718
+ <div class="jv-slider-stage">
21719
+ @for (image of images(); track image.src; let i = $index) {
21720
+ <div
21721
+ class="jv-slider-slide"
21722
+ [class.jv-slider-slide--active]="i === currentIndex()"
21723
+ >
21724
+ <img
21725
+ class="jv-slider-image"
21726
+ [src]="image.src"
21727
+ [alt]="image.alt"
21728
+ loading="lazy"
21729
+ />
21730
+ </div>
21731
+ }
21732
+ </div>
21733
+
21734
+ @if (hasMultiple()) {
21735
+ <button
21736
+ type="button"
21737
+ class="jv-slider-nav jv-slider-nav--next"
21738
+ [attr.aria-label]="translatedAriaLabels().next"
21739
+ (click)="next()"
21740
+ >
21741
+ <jv-icon name="chevron-right" [size]="20" />
21742
+ </button>
21743
+ }
21744
+ </div>
21745
+
21746
+ @if (currentImage(); as image) {
21747
+ @if (showDescription() && image.description) {
21748
+ <div class="jv-slider-description">
21749
+ <p>{{ image.description }}</p>
21750
+ </div>
21751
+ }
21752
+ }
21753
+
21754
+ @if (hasMultiple() && showThumbnails()) {
21755
+ <div class="jv-slider-thumbnails" #thumbnailStrip role="tablist" [attr.aria-label]="translatedAriaLabels().thumbnailList">
21756
+ @for (image of images(); track image.src; let i = $index) {
21757
+ <button
21758
+ type="button"
21759
+ class="jv-slider-thumbnail"
21760
+ [class.jv-slider-thumbnail--active]="i === currentIndex()"
21761
+ role="tab"
21762
+ [attr.aria-selected]="i === currentIndex() ? 'true' : 'false'"
21763
+ [attr.aria-label]="image.alt"
21764
+ (click)="goTo(i)"
21765
+ >
21766
+ <img
21767
+ class="jv-slider-thumbnail-image"
21768
+ [src]="image.thumbnail || image.src"
21769
+ [alt]="image.alt"
21770
+ loading="lazy"
21771
+ />
21772
+ </button>
21773
+ }
21774
+ </div>
21775
+
21776
+ <div class="jv-slider-counter">
21777
+ {{ currentIndex() + 1 }} / {{ images().length }}
21778
+ </div>
21779
+ }
21780
+ </div>
21781
+ `, styles: [".jv-slider{display:grid;gap:var(--jv-spacing-sm);width:100%;max-width:100%}.jv-slider-main{position:relative;display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:var(--jv-spacing-xs)}.jv-slider-stage{position:relative;aspect-ratio:16 / 9;overflow:hidden;border-radius:var(--jv-radius-md);background:var(--jv-color-surface-muted)}.jv-slider-slide{position:absolute;inset:0;opacity:0;transition:opacity .2s ease;pointer-events:none}.jv-slider-slide--active{opacity:1;pointer-events:auto}.jv-slider-image{display:block;width:100%;height:100%;object-fit:contain;-webkit-user-select:none;user-select:none}.jv-slider-nav{display:inline-flex;align-items:center;justify-content:center;width:2.5rem;height:2.5rem;border:1px solid var(--jv-color-border);border-radius:var(--jv-radius-md);background:var(--jv-color-surface);color:var(--jv-color-foreground);cursor:pointer;transition:background-color .16s ease,border-color .16s ease,color .16s ease;flex-shrink:0;z-index:1}.jv-slider-nav:hover{background:var(--jv-color-surface-muted);border-color:var(--jv-color-foreground-muted)}.jv-slider-nav:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:1px}.jv-slider-description{padding:var(--jv-spacing-sm) 0}.jv-slider-description p{margin:0;font-size:var(--jv-density-font-size);color:var(--jv-color-foreground-muted);line-height:1.5}.jv-slider-thumbnails{display:flex;gap:var(--jv-spacing-xs);overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none;padding:var(--jv-spacing-xs) 0}.jv-slider-thumbnails::-webkit-scrollbar{display:none}.jv-slider-thumbnail{flex:0 0 auto;width:3.5rem;height:3.5rem;padding:0;border:2px solid transparent;border-radius:var(--jv-radius-sm);background:transparent;cursor:pointer;overflow:hidden;transition:border-color .16s ease,opacity .16s ease;opacity:.6}.jv-slider-thumbnail:hover{opacity:.85}.jv-slider-thumbnail--active{border-color:var(--jv-color-primary);opacity:1}.jv-slider-thumbnail:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:1px}.jv-slider-thumbnail-image{display:block;width:100%;height:100%;object-fit:cover}.jv-slider-counter{text-align:center;font-size:.8125rem;color:var(--jv-color-foreground-muted);font-variant-numeric:tabular-nums}@media(max-width:640px){.jv-slider-nav{width:2rem;height:2rem}.jv-slider-thumbnail{width:2.75rem;height:2.75rem}}@media(prefers-reduced-motion:reduce){.jv-slider-slide{transition:none}}\n"] }]
21782
+ }], ctorParameters: () => [], propDecorators: { thumbnailStripRef: [{ type: i0.ViewChild, args: ['thumbnailStrip', { isSignal: true }] }], images: [{ type: i0.Input, args: [{ isSignal: true, alias: "images", required: false }] }], showThumbnails: [{ type: i0.Input, args: [{ isSignal: true, alias: "showThumbnails", required: false }] }], showDescription: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDescription", required: false }] }], imageChange: [{ type: i0.Output, args: ["imageChange"] }], handleKeydown: [{
21783
+ type: HostListener,
21784
+ args: ['keydown', ['$event']]
21785
+ }] } });
21786
+
21245
21787
  class JvDialogService {
21246
21788
  announcementService = inject(JvAnnouncementService);
21247
21789
  translation = inject(JvTranslationService);
@@ -22125,12 +22667,25 @@ class JvTabsComponent {
22125
22667
  instanceId = ++tabsSequence;
22126
22668
  panelTemplates = contentChildren(JvTabPanelDirective, ...(ngDevMode ? [{ debugName: "panelTemplates" }] : /* istanbul ignore next */ []));
22127
22669
  tabListRef = viewChild('tabList', ...(ngDevMode ? [{ debugName: "tabListRef" }] : /* istanbul ignore next */ []));
22670
+ config = inject(JvUiConfigService);
22128
22671
  tabs = input([], ...(ngDevMode ? [{ debugName: "tabs" }] : /* istanbul ignore next */ []));
22129
22672
  activeTabId = input(null, ...(ngDevMode ? [{ debugName: "activeTabId" }] : /* istanbul ignore next */ []));
22130
22673
  keepMounted = input(true, ...(ngDevMode ? [{ debugName: "keepMounted" }] : /* istanbul ignore next */ []));
22674
+ transition = input(null, ...(ngDevMode ? [{ debugName: "transition" }] : /* istanbul ignore next */ []));
22131
22675
  activeTabChange = output();
22132
22676
  showScrollLeft = signal(false, ...(ngDevMode ? [{ debugName: "showScrollLeft" }] : /* istanbul ignore next */ []));
22133
22677
  showScrollRight = signal(false, ...(ngDevMode ? [{ debugName: "showScrollRight" }] : /* istanbul ignore next */ []));
22678
+ slideDirection = signal('right', ...(ngDevMode ? [{ debugName: "slideDirection" }] : /* istanbul ignore next */ []));
22679
+ outgoingPanelId = signal(null, ...(ngDevMode ? [{ debugName: "outgoingPanelId" }] : /* istanbul ignore next */ []));
22680
+ previousActiveTabId = signal(null, ...(ngDevMode ? [{ debugName: "previousActiveTabId" }] : /* istanbul ignore next */ []));
22681
+ effectiveTransition = computed(() => {
22682
+ const local = this.transition();
22683
+ if (local !== null) {
22684
+ return local === 'slide' && this.keepMounted() ? 'fade' : local;
22685
+ }
22686
+ const global = this.config.snapshot.tabsTransition;
22687
+ return global === 'slide' && this.keepMounted() ? 'fade' : global;
22688
+ }, ...(ngDevMode ? [{ debugName: "effectiveTransition" }] : /* istanbul ignore next */ []));
22134
22689
  resolvedActiveTabId = computed(() => {
22135
22690
  const enabledTabs = this.tabs().filter((tab) => !tab.disabled);
22136
22691
  const requestedId = this.activeTabId();
@@ -22150,6 +22705,20 @@ class JvTabsComponent {
22150
22705
  this.updateScrollControls();
22151
22706
  });
22152
22707
  });
22708
+ effect(() => {
22709
+ const activeId = this.resolvedActiveTabId();
22710
+ const prevId = this.previousActiveTabId();
22711
+ if (activeId !== prevId && prevId !== null && !this.keepMounted()) {
22712
+ const enabledTabs = this.tabs().filter((t) => !t.disabled);
22713
+ const newIndex = enabledTabs.findIndex((t) => t.id === activeId);
22714
+ const oldIndex = enabledTabs.findIndex((t) => t.id === prevId);
22715
+ if (newIndex !== oldIndex) {
22716
+ this.slideDirection.set(newIndex > oldIndex ? 'left' : 'right');
22717
+ }
22718
+ this.outgoingPanelId.set(prevId);
22719
+ }
22720
+ this.previousActiveTabId.set(activeId);
22721
+ });
22153
22722
  }
22154
22723
  ngAfterViewInit() {
22155
22724
  this.updateScrollControls();
@@ -22174,6 +22743,14 @@ class JvTabsComponent {
22174
22743
  this.activeTabChange.emit(tabId);
22175
22744
  queueMicrotask(() => this.focusTabButton(tabId));
22176
22745
  }
22746
+ handleTransitionEnd(event) {
22747
+ if (event.propertyName !== 'opacity' && event.propertyName !== 'transform') {
22748
+ return;
22749
+ }
22750
+ if (!this.keepMounted() && this.outgoingPanelId()) {
22751
+ this.outgoingPanelId.set(null);
22752
+ }
22753
+ }
22177
22754
  handleKeydown(event, currentTabId) {
22178
22755
  const enabledTabs = this.tabs().filter((tab) => !tab.disabled);
22179
22756
  if (enabledTabs.length === 0) {
@@ -22254,148 +22831,194 @@ class JvTabsComponent {
22254
22831
  return (Array.from(tabList.querySelectorAll('[data-jv-tab-id]')).find((button) => button.dataset['jvTabId'] === tabId) ?? null);
22255
22832
  }
22256
22833
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JvTabsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
22257
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: JvTabsComponent, isStandalone: true, selector: "jv-tabs", inputs: { tabs: { classPropertyName: "tabs", publicName: "tabs", isSignal: true, isRequired: false, transformFunction: null }, activeTabId: { classPropertyName: "activeTabId", publicName: "activeTabId", isSignal: true, isRequired: false, transformFunction: null }, keepMounted: { classPropertyName: "keepMounted", publicName: "keepMounted", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activeTabChange: "activeTabChange" }, host: { listeners: { "window:resize": "handleWindowResize()" } }, queries: [{ propertyName: "panelTemplates", predicate: JvTabPanelDirective, isSignal: true }], viewQueries: [{ propertyName: "tabListRef", first: true, predicate: ["tabList"], descendants: true, isSignal: true }], ngImport: i0, template: `
22258
- <div class="jv-tabs">
22259
- <div class="jv-tabs-header">
22260
- <div class="jv-tabs-scroll" #tabList role="tablist" aria-orientation="horizontal" (scroll)="updateScrollControls()">
22261
- @for (tab of tabs(); track tab.id) {
22262
- <button
22263
- type="button"
22264
- class="jv-tab"
22265
- [class.jv-tab--active]="isActive(tab.id)"
22266
- [class.jv-tab--disabled]="tab.disabled ?? false"
22267
- [id]="tabButtonId(tab.id)"
22268
- [attr.data-jv-tab-id]="tab.id"
22269
- role="tab"
22270
- [attr.aria-selected]="isActive(tab.id) ? 'true' : 'false'"
22271
- [attr.aria-controls]="panelId(tab.id)"
22272
- [attr.tabindex]="getTabIndex(tab.id, tab.disabled ?? false)"
22273
- [disabled]="tab.disabled ?? false"
22274
- (click)="activateTab(tab.id)"
22275
- (keydown)="handleKeydown($event, tab.id)"
22276
- >
22277
- @if (tab.icon) {
22278
- <jv-icon [name]="tab.icon" [size]="16" />
22279
- }
22280
- <span>{{ tab.label }}</span>
22281
- </button>
22282
- }
22283
- </div>
22284
-
22285
- @if (showScrollLeft() || showScrollRight()) {
22286
- <div class="jv-tabs-controls" aria-hidden="true">
22287
- <button type="button" class="jv-tabs-scroll-button" [disabled]="!showScrollLeft()" (click)="scrollTabs(-1)">
22288
- <jv-icon name="chevron-left" [size]="16" />
22289
- </button>
22290
- <button type="button" class="jv-tabs-scroll-button" [disabled]="!showScrollRight()" (click)="scrollTabs(1)">
22291
- <jv-icon name="chevron-right" [size]="16" />
22292
- </button>
22293
- </div>
22294
- }
22295
- </div>
22296
-
22297
- <div class="jv-tabs-panels">
22298
- @if (keepMounted()) {
22299
- @for (tab of tabs(); track tab.id) {
22300
- <div
22301
- class="jv-tab-panel"
22302
- [class.jv-tab-panel--hidden]="!isActive(tab.id)"
22303
- [id]="panelId(tab.id)"
22304
- role="tabpanel"
22305
- [attr.aria-labelledby]="tabButtonId(tab.id)"
22306
- [attr.tabindex]="isActive(tab.id) ? 0 : null"
22307
- [hidden]="!isActive(tab.id)"
22308
- >
22309
- <ng-container [ngTemplateOutlet]="panelTemplate(tab.id) ?? null" />
22310
- </div>
22311
- }
22312
- } @else if (resolvedActiveTabId(); as activeId) {
22313
- <div
22314
- class="jv-tab-panel"
22315
- [id]="panelId(activeId)"
22316
- role="tabpanel"
22317
- [attr.aria-labelledby]="tabButtonId(activeId)"
22318
- tabindex="0"
22319
- >
22320
- <ng-container [ngTemplateOutlet]="panelTemplate(activeId) ?? null" />
22321
- </div>
22322
- }
22323
- </div>
22324
- </div>
22325
- `, isInline: true, styles: [".jv-tabs{display:grid;gap:var(--jv-spacing-md)}.jv-tabs-header{display:flex;align-items:center;gap:var(--jv-spacing-sm);min-width:0}.jv-tabs-scroll{flex:1;min-width:0;display:flex;align-items:stretch;gap:var(--jv-spacing-xs);overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none;border-bottom:1px solid var(--jv-color-border);scroll-behavior:smooth}.jv-tabs-scroll::-webkit-scrollbar{display:none}.jv-tab{position:relative;display:inline-flex;align-items:center;gap:var(--jv-spacing-xs);flex:0 0 auto;min-height:var(--jv-density-control-height);padding:0 var(--jv-spacing-md);border:0;border-bottom:2px solid transparent;background:transparent;color:var(--jv-color-foreground-muted);font:inherit;font-weight:600;white-space:nowrap;cursor:pointer;transition:color .16s ease,border-color .16s ease,background-color .16s ease}.jv-tab:hover:not(:disabled){color:var(--jv-color-foreground);background:color-mix(in srgb,var(--jv-color-primary) 6%,transparent)}.jv-tab:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:-2px;border-radius:var(--jv-radius-sm) var(--jv-radius-sm) 0 0}.jv-tab--active{color:var(--jv-color-primary);border-bottom-color:var(--jv-color-primary)}.jv-tab--disabled{opacity:.5;cursor:not-allowed}.jv-tabs-controls{display:inline-flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-tabs-scroll-button{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;border:1px solid var(--jv-color-border);border-radius:var(--jv-radius-md);background:var(--jv-color-surface);color:var(--jv-color-foreground);cursor:pointer}.jv-tabs-scroll-button:disabled{opacity:.4;cursor:default}.jv-tab-panel{outline:none}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: JvIconComponent, selector: "jv-icon", inputs: ["name", "size", "strokeWidth", "decorative", "ariaLabel"] }] });
22834
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: JvTabsComponent, isStandalone: true, selector: "jv-tabs", inputs: { tabs: { classPropertyName: "tabs", publicName: "tabs", isSignal: true, isRequired: false, transformFunction: null }, activeTabId: { classPropertyName: "activeTabId", publicName: "activeTabId", isSignal: true, isRequired: false, transformFunction: null }, keepMounted: { classPropertyName: "keepMounted", publicName: "keepMounted", isSignal: true, isRequired: false, transformFunction: null }, transition: { classPropertyName: "transition", publicName: "transition", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activeTabChange: "activeTabChange" }, host: { listeners: { "window:resize": "handleWindowResize()" } }, queries: [{ propertyName: "panelTemplates", predicate: JvTabPanelDirective, isSignal: true }], viewQueries: [{ propertyName: "tabListRef", first: true, predicate: ["tabList"], descendants: true, isSignal: true }], ngImport: i0, template: `
22835
+ <div class="jv-tabs">
22836
+ <div class="jv-tabs-header">
22837
+ <div class="jv-tabs-scroll" #tabList role="tablist" aria-orientation="horizontal" (scroll)="updateScrollControls()">
22838
+ @for (tab of tabs(); track tab.id) {
22839
+ <button
22840
+ type="button"
22841
+ class="jv-tab"
22842
+ [class.jv-tab--active]="isActive(tab.id)"
22843
+ [class.jv-tab--disabled]="tab.disabled ?? false"
22844
+ [id]="tabButtonId(tab.id)"
22845
+ [attr.data-jv-tab-id]="tab.id"
22846
+ role="tab"
22847
+ [attr.aria-selected]="isActive(tab.id) ? 'true' : 'false'"
22848
+ [attr.aria-controls]="panelId(tab.id)"
22849
+ [attr.tabindex]="getTabIndex(tab.id, tab.disabled ?? false)"
22850
+ [disabled]="tab.disabled ?? false"
22851
+ (click)="activateTab(tab.id)"
22852
+ (keydown)="handleKeydown($event, tab.id)"
22853
+ >
22854
+ @if (tab.icon) {
22855
+ <jv-icon [name]="tab.icon" [size]="16" />
22856
+ }
22857
+ <span>{{ tab.label }}</span>
22858
+ </button>
22859
+ }
22860
+ </div>
22861
+
22862
+ @if (showScrollLeft() || showScrollRight()) {
22863
+ <div class="jv-tabs-controls" aria-hidden="true">
22864
+ <button type="button" class="jv-tabs-scroll-button" [disabled]="!showScrollLeft()" (click)="scrollTabs(-1)">
22865
+ <jv-icon name="chevron-left" [size]="16" />
22866
+ </button>
22867
+ <button type="button" class="jv-tabs-scroll-button" [disabled]="!showScrollRight()" (click)="scrollTabs(1)">
22868
+ <jv-icon name="chevron-right" [size]="16" />
22869
+ </button>
22870
+ </div>
22871
+ }
22872
+ </div>
22873
+
22874
+ <div class="jv-tabs-panels">
22875
+ @if (keepMounted()) {
22876
+ @for (tab of tabs(); track tab.id) {
22877
+ <div
22878
+ class="jv-tab-panel"
22879
+ [class.jv-tab-panel--hidden]="!isActive(tab.id)"
22880
+ [class.jv-tab-panel--fade]="effectiveTransition() === 'fade'"
22881
+ [class.jv-tab-panel--slide-left]="effectiveTransition() === 'slide' && slideDirection() === 'left'"
22882
+ [class.jv-tab-panel--slide-right]="effectiveTransition() === 'slide' && slideDirection() === 'right'"
22883
+ [id]="panelId(tab.id)"
22884
+ role="tabpanel"
22885
+ [attr.aria-labelledby]="tabButtonId(tab.id)"
22886
+ [attr.tabindex]="isActive(tab.id) ? 0 : null"
22887
+ >
22888
+ <ng-container [ngTemplateOutlet]="panelTemplate(tab.id) ?? null" />
22889
+ </div>
22890
+ }
22891
+ } @else {
22892
+ @if (outgoingPanelId(); as outgoingId) {
22893
+ <div
22894
+ class="jv-tab-panel jv-tab-panel--exit"
22895
+ [class.jv-tab-panel--fade]="effectiveTransition() === 'fade'"
22896
+ [class.jv-tab-panel--slide-left]="effectiveTransition() === 'slide' && slideDirection() === 'left'"
22897
+ [class.jv-tab-panel--slide-right]="effectiveTransition() === 'slide' && slideDirection() === 'right'"
22898
+ [id]="panelId(outgoingId)"
22899
+ role="tabpanel"
22900
+ [attr.aria-labelledby]="tabButtonId(outgoingId)"
22901
+ tabindex="-1"
22902
+ (transitionend)="handleTransitionEnd($event)"
22903
+ >
22904
+ <ng-container [ngTemplateOutlet]="panelTemplate(outgoingId) ?? null" />
22905
+ </div>
22906
+ }
22907
+
22908
+ @if (resolvedActiveTabId(); as activeId) {
22909
+ <div
22910
+ class="jv-tab-panel jv-tab-panel--entering"
22911
+ [class.jv-tab-panel--fade]="effectiveTransition() === 'fade'"
22912
+ [class.jv-tab-panel--slide-left]="effectiveTransition() === 'slide' && slideDirection() === 'left'"
22913
+ [class.jv-tab-panel--slide-right]="effectiveTransition() === 'slide' && slideDirection() === 'right'"
22914
+ [id]="panelId(activeId)"
22915
+ role="tabpanel"
22916
+ [attr.aria-labelledby]="tabButtonId(activeId)"
22917
+ tabindex="0"
22918
+ >
22919
+ <ng-container [ngTemplateOutlet]="panelTemplate(activeId) ?? null" />
22920
+ </div>
22921
+ }
22922
+ }
22923
+ </div>
22924
+ </div>
22925
+ `, isInline: true, styles: [".jv-tabs{display:grid;gap:var(--jv-spacing-md)}.jv-tabs-header{display:flex;align-items:center;gap:var(--jv-spacing-sm);min-width:0}.jv-tabs-scroll{flex:1;min-width:0;display:flex;align-items:stretch;gap:var(--jv-spacing-xs);overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none;border-bottom:1px solid var(--jv-color-border);scroll-behavior:smooth}.jv-tabs-scroll::-webkit-scrollbar{display:none}.jv-tab{position:relative;display:inline-flex;align-items:center;gap:var(--jv-spacing-xs);flex:0 0 auto;min-height:var(--jv-density-control-height);padding:0 var(--jv-spacing-md);border:0;border-bottom:2px solid transparent;background:transparent;color:var(--jv-color-foreground-muted);font:inherit;font-weight:600;white-space:nowrap;cursor:pointer;transition:color .16s ease,border-color .16s ease,background-color .16s ease}.jv-tab:hover:not(:disabled){color:var(--jv-color-foreground);background:color-mix(in srgb,var(--jv-color-primary) 6%,transparent)}.jv-tab:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:-2px;border-radius:var(--jv-radius-sm) var(--jv-radius-sm) 0 0}.jv-tab--active{color:var(--jv-color-primary);border-bottom-color:var(--jv-color-primary)}.jv-tab--disabled{opacity:.5;cursor:not-allowed}.jv-tabs-controls{display:inline-flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-tabs-scroll-button{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;border:1px solid var(--jv-color-border);border-radius:var(--jv-radius-md);background:var(--jv-color-surface);color:var(--jv-color-foreground);cursor:pointer}.jv-tabs-scroll-button:disabled{opacity:.4;cursor:default}.jv-tab-panel{outline:none}.jv-tab-panel--hidden{display:none}.jv-tab-panel--fade{transition:opacity .18s ease}.jv-tab-panel--hidden.jv-tab-panel--fade{display:block;opacity:0;pointer-events:none}.jv-tab-panel--slide-left,.jv-tab-panel--slide-right{transition:opacity .18s ease,transform .18s ease}.jv-tab-panel--hidden.jv-tab-panel--slide-left{display:block;opacity:0;transform:translate(20px);pointer-events:none}.jv-tab-panel--hidden.jv-tab-panel--slide-right{display:block;opacity:0;transform:translate(-20px);pointer-events:none}.jv-tab-panel--exit.jv-tab-panel--fade{position:absolute;opacity:0;pointer-events:none}.jv-tab-panel--exit.jv-tab-panel--slide-left{position:absolute;opacity:0;transform:translate(-20px);pointer-events:none}.jv-tab-panel--exit.jv-tab-panel--slide-right{position:absolute;opacity:0;transform:translate(20px);pointer-events:none}.jv-tab-panel--entering{animation:jv-tab-enter .18s ease forwards}.jv-tab-panel--entering.jv-tab-panel--slide-left{animation-name:jv-tab-slide-from-right}.jv-tab-panel--entering.jv-tab-panel--slide-right{animation-name:jv-tab-slide-from-left}@keyframes jv-tab-enter{0%{opacity:0}to{opacity:1}}@keyframes jv-tab-slide-from-right{0%{opacity:0;transform:translate(20px)}to{opacity:1;transform:translate(0)}}@keyframes jv-tab-slide-from-left{0%{opacity:0;transform:translate(-20px)}to{opacity:1;transform:translate(0)}}@media(prefers-reduced-motion:reduce){.jv-tab-panel--fade,.jv-tab-panel--slide-left,.jv-tab-panel--slide-right,.jv-tab-panel--entering{transition:none;animation:none}.jv-tab-panel--hidden.jv-tab-panel--fade,.jv-tab-panel--hidden.jv-tab-panel--slide-left,.jv-tab-panel--hidden.jv-tab-panel--slide-right{opacity:0}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: JvIconComponent, selector: "jv-icon", inputs: ["name", "size", "strokeWidth", "decorative", "ariaLabel"] }] });
22326
22926
  }
22327
22927
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JvTabsComponent, decorators: [{
22328
22928
  type: Component,
22329
- args: [{ selector: 'jv-tabs', standalone: true, imports: [NgTemplateOutlet, JvIconComponent], template: `
22330
- <div class="jv-tabs">
22331
- <div class="jv-tabs-header">
22332
- <div class="jv-tabs-scroll" #tabList role="tablist" aria-orientation="horizontal" (scroll)="updateScrollControls()">
22333
- @for (tab of tabs(); track tab.id) {
22334
- <button
22335
- type="button"
22336
- class="jv-tab"
22337
- [class.jv-tab--active]="isActive(tab.id)"
22338
- [class.jv-tab--disabled]="tab.disabled ?? false"
22339
- [id]="tabButtonId(tab.id)"
22340
- [attr.data-jv-tab-id]="tab.id"
22341
- role="tab"
22342
- [attr.aria-selected]="isActive(tab.id) ? 'true' : 'false'"
22343
- [attr.aria-controls]="panelId(tab.id)"
22344
- [attr.tabindex]="getTabIndex(tab.id, tab.disabled ?? false)"
22345
- [disabled]="tab.disabled ?? false"
22346
- (click)="activateTab(tab.id)"
22347
- (keydown)="handleKeydown($event, tab.id)"
22348
- >
22349
- @if (tab.icon) {
22350
- <jv-icon [name]="tab.icon" [size]="16" />
22351
- }
22352
- <span>{{ tab.label }}</span>
22353
- </button>
22354
- }
22355
- </div>
22356
-
22357
- @if (showScrollLeft() || showScrollRight()) {
22358
- <div class="jv-tabs-controls" aria-hidden="true">
22359
- <button type="button" class="jv-tabs-scroll-button" [disabled]="!showScrollLeft()" (click)="scrollTabs(-1)">
22360
- <jv-icon name="chevron-left" [size]="16" />
22361
- </button>
22362
- <button type="button" class="jv-tabs-scroll-button" [disabled]="!showScrollRight()" (click)="scrollTabs(1)">
22363
- <jv-icon name="chevron-right" [size]="16" />
22364
- </button>
22365
- </div>
22366
- }
22367
- </div>
22368
-
22369
- <div class="jv-tabs-panels">
22370
- @if (keepMounted()) {
22371
- @for (tab of tabs(); track tab.id) {
22372
- <div
22373
- class="jv-tab-panel"
22374
- [class.jv-tab-panel--hidden]="!isActive(tab.id)"
22375
- [id]="panelId(tab.id)"
22376
- role="tabpanel"
22377
- [attr.aria-labelledby]="tabButtonId(tab.id)"
22378
- [attr.tabindex]="isActive(tab.id) ? 0 : null"
22379
- [hidden]="!isActive(tab.id)"
22380
- >
22381
- <ng-container [ngTemplateOutlet]="panelTemplate(tab.id) ?? null" />
22382
- </div>
22383
- }
22384
- } @else if (resolvedActiveTabId(); as activeId) {
22385
- <div
22386
- class="jv-tab-panel"
22387
- [id]="panelId(activeId)"
22388
- role="tabpanel"
22389
- [attr.aria-labelledby]="tabButtonId(activeId)"
22390
- tabindex="0"
22391
- >
22392
- <ng-container [ngTemplateOutlet]="panelTemplate(activeId) ?? null" />
22393
- </div>
22394
- }
22395
- </div>
22396
- </div>
22397
- `, styles: [".jv-tabs{display:grid;gap:var(--jv-spacing-md)}.jv-tabs-header{display:flex;align-items:center;gap:var(--jv-spacing-sm);min-width:0}.jv-tabs-scroll{flex:1;min-width:0;display:flex;align-items:stretch;gap:var(--jv-spacing-xs);overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none;border-bottom:1px solid var(--jv-color-border);scroll-behavior:smooth}.jv-tabs-scroll::-webkit-scrollbar{display:none}.jv-tab{position:relative;display:inline-flex;align-items:center;gap:var(--jv-spacing-xs);flex:0 0 auto;min-height:var(--jv-density-control-height);padding:0 var(--jv-spacing-md);border:0;border-bottom:2px solid transparent;background:transparent;color:var(--jv-color-foreground-muted);font:inherit;font-weight:600;white-space:nowrap;cursor:pointer;transition:color .16s ease,border-color .16s ease,background-color .16s ease}.jv-tab:hover:not(:disabled){color:var(--jv-color-foreground);background:color-mix(in srgb,var(--jv-color-primary) 6%,transparent)}.jv-tab:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:-2px;border-radius:var(--jv-radius-sm) var(--jv-radius-sm) 0 0}.jv-tab--active{color:var(--jv-color-primary);border-bottom-color:var(--jv-color-primary)}.jv-tab--disabled{opacity:.5;cursor:not-allowed}.jv-tabs-controls{display:inline-flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-tabs-scroll-button{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;border:1px solid var(--jv-color-border);border-radius:var(--jv-radius-md);background:var(--jv-color-surface);color:var(--jv-color-foreground);cursor:pointer}.jv-tabs-scroll-button:disabled{opacity:.4;cursor:default}.jv-tab-panel{outline:none}\n"] }]
22398
- }], ctorParameters: () => [], propDecorators: { panelTemplates: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => JvTabPanelDirective), { isSignal: true }] }], tabListRef: [{ type: i0.ViewChild, args: ['tabList', { isSignal: true }] }], tabs: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabs", required: false }] }], activeTabId: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeTabId", required: false }] }], keepMounted: [{ type: i0.Input, args: [{ isSignal: true, alias: "keepMounted", required: false }] }], activeTabChange: [{ type: i0.Output, args: ["activeTabChange"] }], handleWindowResize: [{
22929
+ args: [{ selector: 'jv-tabs', standalone: true, imports: [NgTemplateOutlet, JvIconComponent], template: `
22930
+ <div class="jv-tabs">
22931
+ <div class="jv-tabs-header">
22932
+ <div class="jv-tabs-scroll" #tabList role="tablist" aria-orientation="horizontal" (scroll)="updateScrollControls()">
22933
+ @for (tab of tabs(); track tab.id) {
22934
+ <button
22935
+ type="button"
22936
+ class="jv-tab"
22937
+ [class.jv-tab--active]="isActive(tab.id)"
22938
+ [class.jv-tab--disabled]="tab.disabled ?? false"
22939
+ [id]="tabButtonId(tab.id)"
22940
+ [attr.data-jv-tab-id]="tab.id"
22941
+ role="tab"
22942
+ [attr.aria-selected]="isActive(tab.id) ? 'true' : 'false'"
22943
+ [attr.aria-controls]="panelId(tab.id)"
22944
+ [attr.tabindex]="getTabIndex(tab.id, tab.disabled ?? false)"
22945
+ [disabled]="tab.disabled ?? false"
22946
+ (click)="activateTab(tab.id)"
22947
+ (keydown)="handleKeydown($event, tab.id)"
22948
+ >
22949
+ @if (tab.icon) {
22950
+ <jv-icon [name]="tab.icon" [size]="16" />
22951
+ }
22952
+ <span>{{ tab.label }}</span>
22953
+ </button>
22954
+ }
22955
+ </div>
22956
+
22957
+ @if (showScrollLeft() || showScrollRight()) {
22958
+ <div class="jv-tabs-controls" aria-hidden="true">
22959
+ <button type="button" class="jv-tabs-scroll-button" [disabled]="!showScrollLeft()" (click)="scrollTabs(-1)">
22960
+ <jv-icon name="chevron-left" [size]="16" />
22961
+ </button>
22962
+ <button type="button" class="jv-tabs-scroll-button" [disabled]="!showScrollRight()" (click)="scrollTabs(1)">
22963
+ <jv-icon name="chevron-right" [size]="16" />
22964
+ </button>
22965
+ </div>
22966
+ }
22967
+ </div>
22968
+
22969
+ <div class="jv-tabs-panels">
22970
+ @if (keepMounted()) {
22971
+ @for (tab of tabs(); track tab.id) {
22972
+ <div
22973
+ class="jv-tab-panel"
22974
+ [class.jv-tab-panel--hidden]="!isActive(tab.id)"
22975
+ [class.jv-tab-panel--fade]="effectiveTransition() === 'fade'"
22976
+ [class.jv-tab-panel--slide-left]="effectiveTransition() === 'slide' && slideDirection() === 'left'"
22977
+ [class.jv-tab-panel--slide-right]="effectiveTransition() === 'slide' && slideDirection() === 'right'"
22978
+ [id]="panelId(tab.id)"
22979
+ role="tabpanel"
22980
+ [attr.aria-labelledby]="tabButtonId(tab.id)"
22981
+ [attr.tabindex]="isActive(tab.id) ? 0 : null"
22982
+ >
22983
+ <ng-container [ngTemplateOutlet]="panelTemplate(tab.id) ?? null" />
22984
+ </div>
22985
+ }
22986
+ } @else {
22987
+ @if (outgoingPanelId(); as outgoingId) {
22988
+ <div
22989
+ class="jv-tab-panel jv-tab-panel--exit"
22990
+ [class.jv-tab-panel--fade]="effectiveTransition() === 'fade'"
22991
+ [class.jv-tab-panel--slide-left]="effectiveTransition() === 'slide' && slideDirection() === 'left'"
22992
+ [class.jv-tab-panel--slide-right]="effectiveTransition() === 'slide' && slideDirection() === 'right'"
22993
+ [id]="panelId(outgoingId)"
22994
+ role="tabpanel"
22995
+ [attr.aria-labelledby]="tabButtonId(outgoingId)"
22996
+ tabindex="-1"
22997
+ (transitionend)="handleTransitionEnd($event)"
22998
+ >
22999
+ <ng-container [ngTemplateOutlet]="panelTemplate(outgoingId) ?? null" />
23000
+ </div>
23001
+ }
23002
+
23003
+ @if (resolvedActiveTabId(); as activeId) {
23004
+ <div
23005
+ class="jv-tab-panel jv-tab-panel--entering"
23006
+ [class.jv-tab-panel--fade]="effectiveTransition() === 'fade'"
23007
+ [class.jv-tab-panel--slide-left]="effectiveTransition() === 'slide' && slideDirection() === 'left'"
23008
+ [class.jv-tab-panel--slide-right]="effectiveTransition() === 'slide' && slideDirection() === 'right'"
23009
+ [id]="panelId(activeId)"
23010
+ role="tabpanel"
23011
+ [attr.aria-labelledby]="tabButtonId(activeId)"
23012
+ tabindex="0"
23013
+ >
23014
+ <ng-container [ngTemplateOutlet]="panelTemplate(activeId) ?? null" />
23015
+ </div>
23016
+ }
23017
+ }
23018
+ </div>
23019
+ </div>
23020
+ `, styles: [".jv-tabs{display:grid;gap:var(--jv-spacing-md)}.jv-tabs-header{display:flex;align-items:center;gap:var(--jv-spacing-sm);min-width:0}.jv-tabs-scroll{flex:1;min-width:0;display:flex;align-items:stretch;gap:var(--jv-spacing-xs);overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none;border-bottom:1px solid var(--jv-color-border);scroll-behavior:smooth}.jv-tabs-scroll::-webkit-scrollbar{display:none}.jv-tab{position:relative;display:inline-flex;align-items:center;gap:var(--jv-spacing-xs);flex:0 0 auto;min-height:var(--jv-density-control-height);padding:0 var(--jv-spacing-md);border:0;border-bottom:2px solid transparent;background:transparent;color:var(--jv-color-foreground-muted);font:inherit;font-weight:600;white-space:nowrap;cursor:pointer;transition:color .16s ease,border-color .16s ease,background-color .16s ease}.jv-tab:hover:not(:disabled){color:var(--jv-color-foreground);background:color-mix(in srgb,var(--jv-color-primary) 6%,transparent)}.jv-tab:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:-2px;border-radius:var(--jv-radius-sm) var(--jv-radius-sm) 0 0}.jv-tab--active{color:var(--jv-color-primary);border-bottom-color:var(--jv-color-primary)}.jv-tab--disabled{opacity:.5;cursor:not-allowed}.jv-tabs-controls{display:inline-flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-tabs-scroll-button{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;border:1px solid var(--jv-color-border);border-radius:var(--jv-radius-md);background:var(--jv-color-surface);color:var(--jv-color-foreground);cursor:pointer}.jv-tabs-scroll-button:disabled{opacity:.4;cursor:default}.jv-tab-panel{outline:none}.jv-tab-panel--hidden{display:none}.jv-tab-panel--fade{transition:opacity .18s ease}.jv-tab-panel--hidden.jv-tab-panel--fade{display:block;opacity:0;pointer-events:none}.jv-tab-panel--slide-left,.jv-tab-panel--slide-right{transition:opacity .18s ease,transform .18s ease}.jv-tab-panel--hidden.jv-tab-panel--slide-left{display:block;opacity:0;transform:translate(20px);pointer-events:none}.jv-tab-panel--hidden.jv-tab-panel--slide-right{display:block;opacity:0;transform:translate(-20px);pointer-events:none}.jv-tab-panel--exit.jv-tab-panel--fade{position:absolute;opacity:0;pointer-events:none}.jv-tab-panel--exit.jv-tab-panel--slide-left{position:absolute;opacity:0;transform:translate(-20px);pointer-events:none}.jv-tab-panel--exit.jv-tab-panel--slide-right{position:absolute;opacity:0;transform:translate(20px);pointer-events:none}.jv-tab-panel--entering{animation:jv-tab-enter .18s ease forwards}.jv-tab-panel--entering.jv-tab-panel--slide-left{animation-name:jv-tab-slide-from-right}.jv-tab-panel--entering.jv-tab-panel--slide-right{animation-name:jv-tab-slide-from-left}@keyframes jv-tab-enter{0%{opacity:0}to{opacity:1}}@keyframes jv-tab-slide-from-right{0%{opacity:0;transform:translate(20px)}to{opacity:1;transform:translate(0)}}@keyframes jv-tab-slide-from-left{0%{opacity:0;transform:translate(-20px)}to{opacity:1;transform:translate(0)}}@media(prefers-reduced-motion:reduce){.jv-tab-panel--fade,.jv-tab-panel--slide-left,.jv-tab-panel--slide-right,.jv-tab-panel--entering{transition:none;animation:none}.jv-tab-panel--hidden.jv-tab-panel--fade,.jv-tab-panel--hidden.jv-tab-panel--slide-left,.jv-tab-panel--hidden.jv-tab-panel--slide-right{opacity:0}}\n"] }]
23021
+ }], ctorParameters: () => [], propDecorators: { panelTemplates: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => JvTabPanelDirective), { isSignal: true }] }], tabListRef: [{ type: i0.ViewChild, args: ['tabList', { isSignal: true }] }], tabs: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabs", required: false }] }], activeTabId: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeTabId", required: false }] }], keepMounted: [{ type: i0.Input, args: [{ isSignal: true, alias: "keepMounted", required: false }] }], transition: [{ type: i0.Input, args: [{ isSignal: true, alias: "transition", required: false }] }], activeTabChange: [{ type: i0.Output, args: ["activeTabChange"] }], handleWindowResize: [{
22399
23022
  type: HostListener,
22400
23023
  args: ['window:resize']
22401
23024
  }] } });
@@ -22593,6 +23216,280 @@ const JV_GRID_DEFAULT_OPTIONS = {
22593
23216
  rowDoubleClick: false,
22594
23217
  };
22595
23218
 
23219
+ const MAIN_NS = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
23220
+ const REL_NS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
23221
+ const PKG_REL_NS = 'http://schemas.openxmlformats.org/package/2006/relationships';
23222
+ const XML_DECL = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
23223
+ /**
23224
+ * Builds a real Office Open XML (.xlsx) spreadsheet as a Blob.
23225
+ *
23226
+ * The package is assembled in memory with the ZIP "store" method (no
23227
+ * compression) so no third-party dependency is required. All cell text is
23228
+ * written as `inlineStr`, which keeps every character (including UTF-8
23229
+ * accented glyphs) intact and is understood by Microsoft Excel and Apple
23230
+ * Numbers alike.
23231
+ */
23232
+ function buildXlsxBlob(sheets) {
23233
+ return new Blob([buildXlsxBuffer(sheets)], {
23234
+ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
23235
+ });
23236
+ }
23237
+ /**
23238
+ * Returns the raw bytes of a minimal valid .xlsx package. Exposed separately
23239
+ * from the Blob wrapper to make the package easy to assert on in tests.
23240
+ */
23241
+ function buildXlsxBuffer(sheets) {
23242
+ const parts = [];
23243
+ const sheetXml = sheets.map((sheet, index) => buildSheetXml(sheet, index));
23244
+ const sheetNames = sheets.length > 0 ? sheets.map((s) => s.name) : ['Sheet1'];
23245
+ parts.push({ name: '[Content_Types].xml', data: utf8(buildContentTypes(sheets.length)) });
23246
+ parts.push({ name: '_rels/.rels', data: utf8(buildRootRels()) });
23247
+ parts.push({ name: 'xl/workbook.xml', data: utf8(buildWorkbook(sheetNames)) });
23248
+ parts.push({ name: 'xl/_rels/workbook.xml.rels', data: utf8(buildWorkbookRels(sheets.length)) });
23249
+ parts.push({ name: 'xl/styles.xml', data: utf8(buildStyles()) });
23250
+ for (let i = 0; i < sheetXml.length; i++) {
23251
+ parts.push({ name: `xl/worksheets/sheet${i + 1}.xml`, data: utf8(sheetXml[i]) });
23252
+ }
23253
+ return toZip(parts);
23254
+ }
23255
+ function buildContentTypes(sheetCount) {
23256
+ let xml = XML_DECL;
23257
+ xml += `<Types xmlns="${PKG_REL_NS}">`;
23258
+ xml += `<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>`;
23259
+ xml += `<Default Extension="xml" ContentType="application/xml"/>`;
23260
+ xml += `<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>`;
23261
+ for (let i = 0; i < sheetCount; i++) {
23262
+ xml += `<Override PartName="/xl/worksheets/sheet${i + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`;
23263
+ }
23264
+ xml += `<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>`;
23265
+ xml += `</Types>`;
23266
+ return xml;
23267
+ }
23268
+ function buildRootRels() {
23269
+ return XML_DECL +
23270
+ `<Relationships xmlns="${PKG_REL_NS}">` +
23271
+ `<Relationship Id="rId1" Type="${REL_NS}/officeDocument" Target="xl/workbook.xml"/>` +
23272
+ `</Relationships>`;
23273
+ }
23274
+ function buildWorkbook(sheetNames) {
23275
+ let xml = XML_DECL;
23276
+ xml += `<workbook xmlns="${MAIN_NS}" xmlns:r="${REL_NS}">`;
23277
+ xml += `<sheets>`;
23278
+ for (let i = 0; i < sheetNames.length; i++) {
23279
+ const name = sanitizeSheetName(sheetNames[i]) || `Sheet${i + 1}`;
23280
+ xml += `<sheet name="${escapeXml(name)}" sheetId="${i + 1}" r:id="rId${i + 1}"/>`;
23281
+ }
23282
+ xml += `</sheets></workbook>`;
23283
+ return xml;
23284
+ }
23285
+ function buildWorkbookRels(sheetCount) {
23286
+ let xml = XML_DECL;
23287
+ xml += `<Relationships xmlns="${PKG_REL_NS}">`;
23288
+ for (let i = 0; i < sheetCount; i++) {
23289
+ xml += `<Relationship Id="rId${i + 1}" Type="${REL_NS}/worksheet" Target="worksheets/sheet${i + 1}.xml"/>`;
23290
+ }
23291
+ xml += `<Relationship Id="rId${sheetCount + 1}" Type="${REL_NS}/styles" Target="styles.xml"/>`;
23292
+ xml += `</Relationships>`;
23293
+ return xml;
23294
+ }
23295
+ function buildSheetXml(sheet, index) {
23296
+ const rows = sheet.rows;
23297
+ const rowCount = rows.length;
23298
+ const colCount = rows.reduce((max, row) => Math.max(max, row.length), 0);
23299
+ const dimension = rowCount === 0 || colCount === 0 ? 'A1' : `A1:${columnName(colCount - 1)}${rowCount}`;
23300
+ let xml = XML_DECL;
23301
+ xml += `<worksheet xmlns="${MAIN_NS}">`;
23302
+ xml += `<dimension ref="${dimension}"/>`;
23303
+ xml += `<sheetViews><sheetView workbookViewId="0"/></sheetViews>`;
23304
+ xml += `<sheetFormatPr defaultRowHeight="15"/>`;
23305
+ xml += `<sheetData>`;
23306
+ for (let r = 0; r < rowCount; r++) {
23307
+ const row = rows[r];
23308
+ xml += `<row r="${r + 1}">`;
23309
+ for (let c = 0; c < row.length; c++) {
23310
+ xml += `<c r="${columnName(c)}${r + 1}" t="inlineStr"><is><t xml:space="preserve">${escapeXml(String(row[c]))}</t></is></c>`;
23311
+ }
23312
+ xml += `</row>`;
23313
+ }
23314
+ xml += `</sheetData></worksheet>`;
23315
+ void index;
23316
+ return xml;
23317
+ }
23318
+ function buildStyles() {
23319
+ return XML_DECL +
23320
+ `<styleSheet xmlns="${MAIN_NS}">` +
23321
+ `<fonts count="1"><font><sz val="11"/><name val="Calibri"/></font></fonts>` +
23322
+ `<fills count="1"><fill><patternFill patternType="none"/></fill></fills>` +
23323
+ `<borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>` +
23324
+ `<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>` +
23325
+ `<cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/></cellXfs>` +
23326
+ `<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>` +
23327
+ `</styleSheet>`;
23328
+ }
23329
+ function columnName(index) {
23330
+ let value = '';
23331
+ let n = index;
23332
+ while (n >= 0) {
23333
+ value = String.fromCharCode((n % 26) + 65) + value;
23334
+ n = Math.floor(n / 26) - 1;
23335
+ }
23336
+ return value;
23337
+ }
23338
+ function sanitizeSheetName(name) {
23339
+ return name.replace(/[\[\]:*?/\\]/g, '').slice(0, 31);
23340
+ }
23341
+ function escapeXml(value) {
23342
+ return value
23343
+ .replace(/&/g, '&amp;')
23344
+ .replace(/</g, '&lt;')
23345
+ .replace(/>/g, '&gt;')
23346
+ .replace(/"/g, '&quot;')
23347
+ .replace(/'/g, '&apos;');
23348
+ }
23349
+ function utf8(value) {
23350
+ return new TextEncoder().encode(value);
23351
+ }
23352
+ // ---------------------------------------------------------------------------
23353
+ // Minimal ZIP writer ("store" method, no compression)
23354
+ // ---------------------------------------------------------------------------
23355
+ const CRC_TABLE = (() => {
23356
+ const table = new Array(256);
23357
+ for (let n = 0; n < 256; n++) {
23358
+ let c = n;
23359
+ for (let k = 0; k < 8; k++) {
23360
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
23361
+ }
23362
+ table[n] = c >>> 0;
23363
+ }
23364
+ return table;
23365
+ })();
23366
+ function crc32(bytes) {
23367
+ let crc = 0xffffffff;
23368
+ for (let i = 0; i < bytes.length; i++) {
23369
+ crc = CRC_TABLE[(crc ^ bytes[i]) & 0xff] ^ (crc >>> 8);
23370
+ }
23371
+ return (crc ^ 0xffffffff) >>> 0;
23372
+ }
23373
+ function toZip(files) {
23374
+ const encoder = new TextEncoder();
23375
+ const chunks = [];
23376
+ const centralDirectory = new Uint8Array(0);
23377
+ const centralRecords = [];
23378
+ let offset = 0;
23379
+ for (const file of files) {
23380
+ const nameBytes = encoder.encode(file.name);
23381
+ const crc = crc32(file.data);
23382
+ // Local file header
23383
+ const local = new DataView(new ArrayBuffer(30));
23384
+ local.setUint32(0, 0x04034b50, true);
23385
+ local.setUint16(4, 20, true);
23386
+ local.setUint16(6, 0x0800, true);
23387
+ local.setUint16(8, 0, true); // store (no compression)
23388
+ local.setUint16(10, 0, true);
23389
+ local.setUint16(12, 0, true);
23390
+ local.setUint32(14, crc, true);
23391
+ local.setUint32(18, file.data.length, true);
23392
+ local.setUint32(22, file.data.length, true);
23393
+ local.setUint16(26, nameBytes.length, true);
23394
+ local.setUint16(28, 0, true);
23395
+ chunks.push(new Uint8Array(local.buffer), nameBytes, file.data);
23396
+ // Central directory header
23397
+ const central = new DataView(new ArrayBuffer(46));
23398
+ central.setUint32(0, 0x02014b50, true);
23399
+ central.setUint16(4, 20, true); // version made by
23400
+ central.setUint16(6, 20, true); // version needed
23401
+ central.setUint16(8, 0x0800, true); // UTF-8 filename flag
23402
+ central.setUint16(10, 0, true); // store (no compression)
23403
+ central.setUint16(12, 0, true); // mod time
23404
+ central.setUint16(14, 0, true); // mod date
23405
+ central.setUint32(16, crc, true);
23406
+ central.setUint32(20, file.data.length, true); // compressed size
23407
+ central.setUint32(24, file.data.length, true); // uncompressed size
23408
+ central.setUint16(28, nameBytes.length, true);
23409
+ central.setUint16(30, 0, true); // extra field length
23410
+ central.setUint16(32, 0, true); // comment length
23411
+ central.setUint16(34, 0, true); // disk number start
23412
+ central.setUint16(36, 0, true); // internal file attributes
23413
+ central.setUint32(38, 0, true); // external file attributes
23414
+ central.setUint32(42, offset, true); // local header offset
23415
+ centralRecords.push(new Uint8Array(central.buffer), nameBytes);
23416
+ offset += 30 + nameBytes.length + file.data.length;
23417
+ }
23418
+ const centralSize = centralRecords.reduce((sum, chunk) => sum + chunk.length, 0);
23419
+ const eocd = new DataView(new ArrayBuffer(22));
23420
+ eocd.setUint32(0, 0x06054b50, true);
23421
+ eocd.setUint16(4, 0, true);
23422
+ eocd.setUint16(6, 0, true);
23423
+ eocd.setUint16(8, files.length, true);
23424
+ eocd.setUint16(10, files.length, true);
23425
+ eocd.setUint32(12, centralSize, true);
23426
+ eocd.setUint32(16, offset, true);
23427
+ eocd.setUint16(20, 0, true);
23428
+ const result = new Uint8Array(offset + centralSize + 22);
23429
+ let pointer = 0;
23430
+ for (const chunk of chunks) {
23431
+ result.set(chunk, pointer);
23432
+ pointer += chunk.length;
23433
+ }
23434
+ for (const chunk of centralRecords) {
23435
+ result.set(chunk, pointer);
23436
+ pointer += chunk.length;
23437
+ }
23438
+ result.set(new Uint8Array(eocd.buffer), pointer);
23439
+ void centralDirectory;
23440
+ return result;
23441
+ }
23442
+
23443
+ /**
23444
+ * Triggers a browser download for the given Blob.
23445
+ */
23446
+ function downloadBlob(blob, filename) {
23447
+ const url = URL.createObjectURL(blob);
23448
+ const a = document.createElement('a');
23449
+ a.href = url;
23450
+ a.download = filename;
23451
+ a.click();
23452
+ URL.revokeObjectURL(url);
23453
+ }
23454
+ const CSV_BOM = '\uFEFF';
23455
+ /**
23456
+ * Builds the raw CSV text (including the UTF-8 BOM) for the given header and
23457
+ * rows. Exposed separately from the downloader to keep it easy to test.
23458
+ */
23459
+ function buildCsv(header, rows) {
23460
+ const escapeCell = (value) => {
23461
+ const text = value == null ? '' : String(value);
23462
+ return `"${text.replace(/"/g, '""')}"`;
23463
+ };
23464
+ const lines = [];
23465
+ lines.push(header.map(escapeCell).join(','));
23466
+ for (const row of rows) {
23467
+ lines.push(row.map(escapeCell).join(','));
23468
+ }
23469
+ return CSV_BOM + lines.join('\r\n');
23470
+ }
23471
+ /**
23472
+ * Serializes a grid/table to CSV and downloads it.
23473
+ *
23474
+ * A UTF-8 BOM is prepended so Microsoft Excel recognises the file as UTF-8
23475
+ * and renders accented characters correctly.
23476
+ */
23477
+ function exportGridToCsv(header, rows, filename = 'export.csv') {
23478
+ const csv = buildCsv(header, rows);
23479
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
23480
+ downloadBlob(blob, filename);
23481
+ }
23482
+ /**
23483
+ * Serializes a grid/table to a real Open XML (.xlsx) spreadsheet and downloads it.
23484
+ *
23485
+ * Generates an actual xlsx package (not an HTML file with an .xls extension),
23486
+ * preserving UTF-8 characters. Comma-separated columns are exported as-is.
23487
+ */
23488
+ function exportGridToExcel(header, rows, filename = 'export.xlsx') {
23489
+ const blob = buildXlsxBlob([{ name: 'Sheet1', rows: [header, ...rows] }]);
23490
+ downloadBlob(blob, filename);
23491
+ }
23492
+
22596
23493
  let gridIdSequence = 0;
22597
23494
  class JvGridComponent {
22598
23495
  gridId = `jv-grid-${++gridIdSequence}`;
@@ -23104,41 +24001,20 @@ class JvGridComponent {
23104
24001
  }
23105
24002
  // Export
23106
24003
  exportCsv() {
23107
- const cols = this.visibleColumns().map(c => ({ key: this.colKey(c), header: c.header, column: c }));
23108
- const allData = this.effectiveOptions().serverSide ? this.data() : this.sortedData();
23109
- const headerRow = cols.map(c => `"${c.header}"`).join(',');
23110
- const dataRows = allData.map(row => cols.map(col => {
23111
- const val = row[col.key];
23112
- if (val == null)
23113
- return '';
23114
- return `"${this.getExportValue(val, col.column).replace(/"/g, '""')}"`;
23115
- }).join(','));
23116
- const csv = [headerRow, ...dataRows].join('\r\n');
23117
- const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
23118
- const url = URL.createObjectURL(blob);
23119
- const a = document.createElement('a');
23120
- a.href = url;
23121
- a.download = 'grid-export.csv';
23122
- a.click();
23123
- URL.revokeObjectURL(url);
24004
+ const { header, rows } = this.buildExportMatrix();
24005
+ exportGridToCsv(header, rows, 'grid-export.csv');
23124
24006
  }
23125
24007
  exportExcel() {
24008
+ const { header, rows } = this.buildExportMatrix();
24009
+ exportGridToExcel(header, rows, 'grid-export.xlsx');
24010
+ }
24011
+ buildExportMatrix() {
23126
24012
  const cols = this.visibleColumns().map(c => ({ key: this.colKey(c), header: c.header, column: c }));
23127
24013
  const allData = this.effectiveOptions().serverSide ? this.data() : this.sortedData();
23128
- let html = '<table>';
23129
- html += '<thead><tr>' + cols.map(c => `<th>${c.header}</th>`).join('') + '</tr></thead>';
23130
- html += '<tbody>';
23131
- for (const row of allData) {
23132
- html += '<tr>' + cols.map(col => `<td>${this.getExportValue(row[col.key], col.column)}</td>`).join('') + '</tr>';
23133
- }
23134
- html += '</tbody></table>';
23135
- const blob = new Blob([html], { type: 'application/vnd.ms-excel' });
23136
- const url = URL.createObjectURL(blob);
23137
- const a = document.createElement('a');
23138
- a.href = url;
23139
- a.download = 'grid-export.xls';
23140
- a.click();
23141
- URL.revokeObjectURL(url);
24014
+ return {
24015
+ header: cols.map(c => c.header),
24016
+ rows: allData.map(row => cols.map(col => this.getExportValue(row[col.key], col.column))),
24017
+ };
23142
24018
  }
23143
24019
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JvGridComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
23144
24020
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: JvGridComponent, isStandalone: true, selector: "jv-grid", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, actions: { classPropertyName: "actions", publicName: "actions", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, trackBy: { classPropertyName: "trackBy", publicName: "trackBy", isSignal: true, isRequired: false, transformFunction: null }, selectedIds: { classPropertyName: "selectedIds", publicName: "selectedIds", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { rowClick: "rowClick", rowDoubleClick: "rowDoubleClick", actionClick: "actionClick", selectionChange: "selectionChange", pageChange: "pageChange", searchChange: "searchChange", sortChange: "sortChange", columnFilter: "columnFilter", columnResize: "columnResize", columnReorder: "columnReorder", rowEdit: "rowEdit" }, ngImport: i0, template: `
@@ -24010,5 +24886,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
24010
24886
  * Generated bundle index. Do not edit.
24011
24887
  */
24012
24888
 
24013
- export { EN, ES, JV_DEFAULT_LOCALE, JV_FALLBACK_ICON_NAME, JV_GRID_DEFAULT_OPTIONS, JV_LOCALE_DICTIONARIES, JV_LUCIDE_ICON_REGISTRY, JV_UI_CONFIG, JV_UI_CONFIG_DEFAULTS, JV_UI_DEFAULT_CONFIG, JvAccordionComponent, JvAccordionPanelDirective, JvAlertComponent, JvAnnouncementService, JvBadgeComponent, JvBreadcrumbComponent, JvButtonComponent, JvButtonGroupComponent, JvCardComponent, JvChangePasswordPageComponent, JvCheckboxComponent, JvCodeComponent, JvCollapsiblePanelComponent, JvConfirmDialogComponent, JvDashboardShellComponent, JvDatePickerComponent, JvDialogComponent, JvDialogService, JvDividerComponent, JvFileInputComponent, JvForgotPasswordPageComponent, JvFormContainerComponent, JvFormatService, JvGridComponent, JvIconButtonComponent, JvIconComponent, JvInputComponent, JvListComponent, JvListItemComponent, JvLoaderComponent, JvLoaderService, JvLoginPageComponent, JvModalSelectorComponent, JvPageComponent, JvPaginationComponent, JvRadioComponent, JvSectionComponent, JvSelectComponent, JvSidebarComponent, JvSwitchComponent, JvTabPanelDirective, JvTabsComponent, JvTextareaComponent, JvThemeService, JvToastComponent, JvToastService, JvTooltipDirective, JvTopbarComponent, JvTranslationService, provideJvUi };
24889
+ export { EN, ES, JV_DEFAULT_LOCALE, JV_FALLBACK_ICON_NAME, JV_GRID_DEFAULT_OPTIONS, JV_LOCALE_DICTIONARIES, JV_LUCIDE_ICON_REGISTRY, JV_UI_CONFIG, JV_UI_CONFIG_DEFAULTS, JV_UI_DEFAULT_CONFIG, JvAccordionComponent, JvAccordionPanelDirective, JvAlertComponent, JvAnnouncementService, JvBadgeComponent, JvBreadcrumbComponent, JvButtonComponent, JvButtonGroupComponent, JvCardComponent, JvChangePasswordPageComponent, JvCheckboxComponent, JvCodeComponent, JvCollapsiblePanelComponent, JvConfirmDialogComponent, JvDashboardShellComponent, JvDatePickerComponent, JvDialogComponent, JvDialogService, JvDividerComponent, JvFileInputComponent, JvForgotPasswordPageComponent, JvFormContainerComponent, JvFormatService, JvGridComponent, JvIconButtonComponent, JvIconComponent, JvImageGalleryComponent, JvImageSliderComponent, JvInputComponent, JvListComponent, JvListItemComponent, JvLoaderComponent, JvLoaderService, JvLoginPageComponent, JvModalSelectorComponent, JvPageComponent, JvPaginationComponent, JvRadioComponent, JvSectionComponent, JvSelectComponent, JvSidebarComponent, JvSwitchComponent, JvTabPanelDirective, JvTabsComponent, JvTextareaComponent, JvThemeService, JvToastComponent, JvToastService, JvTooltipDirective, JvTopbarComponent, JvTranslationService, provideJvUi };
24014
24890
  //# sourceMappingURL=devjuliovilla-jv-ui.mjs.map