@devjuliovilla/jv-ui 1.5.4 → 1.5.6

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.
@@ -18908,6 +18908,7 @@ const ES = {
18908
18908
  'grid.exportCsv': 'CSV',
18909
18909
  'grid.exportExcel': 'Excel',
18910
18910
  'grid.filterPlaceholder': 'Filtrar...',
18911
+ 'grid.filterForColumn': 'Filtrar por ${column}',
18911
18912
  'grid.actionsLabel': 'Acciones',
18912
18913
  'pagination.pageLabel': 'Página {page}',
18913
18914
  'pagination.itemsShowing': '{start}-{end} de {total}',
@@ -18942,6 +18943,7 @@ const EN = {
18942
18943
  'grid.exportCsv': 'CSV',
18943
18944
  'grid.exportExcel': 'Excel',
18944
18945
  'grid.filterPlaceholder': 'Filter...',
18946
+ 'grid.filterForColumn': 'Filter by ${column}',
18945
18947
  'grid.actionsLabel': 'Actions',
18946
18948
  'pagination.pageLabel': 'Page {page}',
18947
18949
  'pagination.itemsShowing': '{start}-{end} of {total}',
@@ -19381,6 +19383,97 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
19381
19383
  `, styles: [".jv-switch{display:inline-flex;align-items:center;gap:var(--jv-spacing-sm);min-height:var(--jv-density-control-height);color:var(--jv-color-foreground)}.jv-switch.is-disabled{opacity:.7}.jv-switch-input{position:absolute;opacity:0;pointer-events:none}.jv-switch-track{position:relative;display:inline-flex;align-items:center;width:2.75rem;height:1.6rem;padding:.15rem;border-radius:999px;background:var(--jv-color-border);transition:background-color .16s ease}.jv-switch-thumb{width:1.25rem;height:1.25rem;border-radius:999px;background:#fff;box-shadow:var(--jv-shadow-sm);transition:transform .16s ease}.jv-switch-input:checked+.jv-switch-track{background:var(--jv-color-primary)}.jv-switch-input:checked+.jv-switch-track .jv-switch-thumb{transform:translate(1.1rem)}\n"] }]
19382
19384
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], describedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "describedBy", required: false }] }], inputId: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputId", required: false }] }] } });
19383
19385
 
19386
+ let textareaIdSequence = 0;
19387
+ class JvTextareaComponent {
19388
+ placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
19389
+ required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
19390
+ invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
19391
+ describedBy = input('', ...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
19392
+ readonly = input(false, ...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
19393
+ rows = input(3, ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
19394
+ cols = input(undefined, ...(ngDevMode ? [{ debugName: "cols" }] : /* istanbul ignore next */ []));
19395
+ maxlength = input(undefined, ...(ngDevMode ? [{ debugName: "maxlength" }] : /* istanbul ignore next */ []));
19396
+ inputId = input(`jv-textarea-${++textareaIdSequence}`, ...(ngDevMode ? [{ debugName: "inputId" }] : /* istanbul ignore next */ []));
19397
+ value = '';
19398
+ disabled = false;
19399
+ onChange = () => undefined;
19400
+ onTouched = () => undefined;
19401
+ writeValue(value) {
19402
+ this.value = value ?? '';
19403
+ }
19404
+ registerOnChange(fn) {
19405
+ this.onChange = fn;
19406
+ }
19407
+ registerOnTouched(fn) {
19408
+ this.onTouched = fn;
19409
+ }
19410
+ setDisabledState(isDisabled) {
19411
+ this.disabled = isDisabled;
19412
+ }
19413
+ handleInput(event) {
19414
+ const nextValue = event.target.value;
19415
+ this.value = nextValue;
19416
+ this.onChange(nextValue);
19417
+ }
19418
+ handleBlur() {
19419
+ this.onTouched();
19420
+ }
19421
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JvTextareaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
19422
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.17", type: JvTextareaComponent, isStandalone: true, selector: "jv-textarea", inputs: { placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, describedBy: { classPropertyName: "describedBy", publicName: "describedBy", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, cols: { classPropertyName: "cols", publicName: "cols", isSignal: true, isRequired: false, transformFunction: null }, maxlength: { classPropertyName: "maxlength", publicName: "maxlength", isSignal: true, isRequired: false, transformFunction: null }, inputId: { classPropertyName: "inputId", publicName: "inputId", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
19423
+ {
19424
+ provide: NG_VALUE_ACCESSOR,
19425
+ useExisting: forwardRef(() => JvTextareaComponent),
19426
+ multi: true,
19427
+ },
19428
+ ], ngImport: i0, template: `
19429
+ <textarea
19430
+ class="jv-textarea"
19431
+ [class.is-invalid]="invalid()"
19432
+ [id]="inputId()"
19433
+ [value]="value"
19434
+ [placeholder]="placeholder()"
19435
+ [required]="required()"
19436
+ [disabled]="disabled"
19437
+ [readonly]="readonly()"
19438
+ [rows]="rows()"
19439
+ [cols]="cols()"
19440
+ [attr.maxlength]="maxlength() ?? null"
19441
+ [attr.aria-invalid]="invalid() ? 'true' : null"
19442
+ [attr.aria-describedby]="describedBy() || null"
19443
+ (input)="handleInput($event)"
19444
+ (blur)="handleBlur()"
19445
+ ></textarea>
19446
+ `, isInline: true, 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"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
19447
+ }
19448
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JvTextareaComponent, decorators: [{
19449
+ type: Component,
19450
+ args: [{ selector: 'jv-textarea', standalone: true, imports: [CommonModule], providers: [
19451
+ {
19452
+ provide: NG_VALUE_ACCESSOR,
19453
+ useExisting: forwardRef(() => JvTextareaComponent),
19454
+ multi: true,
19455
+ },
19456
+ ], template: `
19457
+ <textarea
19458
+ class="jv-textarea"
19459
+ [class.is-invalid]="invalid()"
19460
+ [id]="inputId()"
19461
+ [value]="value"
19462
+ [placeholder]="placeholder()"
19463
+ [required]="required()"
19464
+ [disabled]="disabled"
19465
+ [readonly]="readonly()"
19466
+ [rows]="rows()"
19467
+ [cols]="cols()"
19468
+ [attr.maxlength]="maxlength() ?? null"
19469
+ [attr.aria-invalid]="invalid() ? 'true' : null"
19470
+ [attr.aria-describedby]="describedBy() || null"
19471
+ (input)="handleInput($event)"
19472
+ (blur)="handleBlur()"
19473
+ ></textarea>
19474
+ `, 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"] }]
19475
+ }], 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 }] }] } });
19476
+
19384
19477
  class JvDialogService {
19385
19478
  announcementService = inject(JvAnnouncementService);
19386
19479
  document = inject(DOCUMENT);
@@ -20718,685 +20811,705 @@ class JvGridComponent {
20718
20811
  URL.revokeObjectURL(url);
20719
20812
  }
20720
20813
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JvGridComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
20721
- 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: `
20722
- <div class="jv-grid" role="region" [attr.aria-label]="resolvedOptions().ariaLabel || gridLabel()">
20723
- @if (resolvedOptions().searchable || resolvedOptions().exportable) {
20724
- <div class="jv-grid-toolbar">
20725
- @if (resolvedOptions().searchable) {
20726
- <div class="jv-grid-search">
20727
- <jv-icon name="search" [size]="16" />
20728
- <jv-input
20729
- inputId="grid-search"
20730
- [placeholder]="t('grid.searchPlaceholder')"
20731
- [ngModel]="searchTerm()"
20732
- (ngModelChange)="onSearchInput($event)"
20733
- />
20734
- </div>
20735
- }
20736
- @if (resolvedOptions().exportable) {
20737
- <div class="jv-grid-export-actions">
20738
- <jv-button variant="outline" icon="download" (click)="exportCsv()">{{ t('grid.exportCsv') }}</jv-button>
20739
- <jv-button variant="outline" icon="file-spreadsheet" (click)="exportExcel()">{{ t('grid.exportExcel') }}</jv-button>
20740
- </div>
20741
- }
20742
- </div>
20743
- }
20744
-
20745
- @if (resolvedOptions().loading) {
20746
- <div class="jv-grid-loading" role="status" aria-live="polite">
20747
- <span class="jv-grid-spinner" aria-hidden="true"></span>
20748
- <span>{{ t('grid.loading') }}</span>
20749
- </div>
20750
- } @else {
20751
- <div
20752
- class="jv-grid-table-wrap"
20753
- [class.jv-grid-virtual]="resolvedOptions().virtualScroll"
20754
- #tableWrap
20755
- (scroll)="onVirtualScroll($any($event.target))"
20756
- >
20757
- @if (resolvedOptions().virtualScroll) {
20758
- <div class="jv-grid-virtual-spacer" [style.height.px]="virtualTotalHeight()"></div>
20759
- }
20760
- <table
20761
- class="jv-grid-table"
20762
- role="grid"
20763
- [attr.aria-label]="resolvedOptions().ariaLabel || gridLabel()"
20764
- [class]="densityClass()"
20765
- [style.position]="resolvedOptions().virtualScroll ? 'relative' : null"
20766
- [style.top.px]="virtualOffsetY()"
20767
- >
20768
- <caption class="sr-only">{{ resolvedOptions().ariaLabel || gridLabel() }}</caption>
20769
-
20770
- @let hasGroupedHeaders = groupedHeaders().length > 0;
20771
-
20772
- <thead class="jv-grid-thead">
20773
- @if (hasGroupedHeaders) {
20774
- <tr>
20775
- @if (resolvedOptions().selectable) {
20776
- <th class="jv-grid-th jv-grid-th-select" scope="col" [attr.rowspan]="2">
20777
- <input
20778
- type="checkbox"
20779
- class="jv-grid-checkbox"
20780
- [checked]="allSelected()"
20781
- (change)="toggleSelectAll($any($event.target).checked)"
20782
- [attr.aria-label]="allSelected() ? t('grid.deselectAll') : t('grid.selectAll')"
20783
- [indeterminate]="someSelected()"
20784
- />
20785
- </th>
20786
- }
20787
- @for (group of groupedHeaders(); track colKey(group)) {
20788
- <th
20789
- class="jv-grid-th"
20790
- [class]="group.headerClass || ''"
20791
- [class.jv-grid-th-sticky-left]="isStickyLeft(group)"
20792
- [class.jv-grid-th-sticky-right]="isStickyRight(group)"
20793
- [attr.colspan]="groupColspan(group)"
20794
- scope="colgroup"
20795
- [style.text-align]="group.align ?? 'start'"
20796
- >
20797
- <div class="jv-grid-th-content">{{ group.header }}</div>
20798
- </th>
20799
- }
20800
- @if (actions().length > 0) {
20801
- <th class="jv-grid-th jv-grid-th-actions" scope="col" [attr.rowspan]="2">
20802
- <span class="sr-only">{{ t('grid.actionsLabel', { label: '' }) }}</span>
20803
- </th>
20804
- }
20805
- </tr>
20806
- }
20807
-
20808
- <tr>
20809
- @if (!hasGroupedHeaders && resolvedOptions().selectable) {
20810
- <th class="jv-grid-th jv-grid-th-select" scope="col">
20811
- <input
20812
- type="checkbox"
20813
- class="jv-grid-checkbox"
20814
- [checked]="allSelected()"
20815
- (change)="toggleSelectAll($any($event.target).checked)"
20816
- [attr.aria-label]="allSelected() ? t('grid.deselectAll') : t('grid.selectAll')"
20817
- [indeterminate]="someSelected()"
20818
- />
20819
- </th>
20820
- }
20821
- @for (col of visibleColumns(); track colKey(col); let colIdx = $index) {
20822
- <th
20823
- class="jv-grid-th"
20824
- [class]="col.headerClass || ''"
20825
- [class.jv-grid-th-sortable]="col.sortable !== false && resolvedOptions().sortable"
20826
- [class.jv-grid-th-sticky-left]="isStickyLeft(col)"
20827
- [class.jv-grid-th-sticky-right]="isStickyRight(col)"
20828
- [class.jv-grid-th-reorderable]="resolvedOptions().reorderableColumns"
20829
- [style.text-align]="col.align ?? 'start'"
20830
- [style.width]="getColumnWidth(col)"
20831
- [style.min-width]="col.minWidth || undefined"
20832
- [style.max-width]="col.maxWidth || undefined"
20833
- scope="col"
20834
- [attr.aria-sort]="getAriaSort(col)"
20835
- [attr.aria-label]="getSortLabel(col)"
20836
- [tabindex]="col.sortable !== false && resolvedOptions().sortable ? 0 : -1"
20837
- (click)="toggleSort(col)"
20838
- (keydown.enter)="toggleSort(col)"
20839
- (keydown.space)="toggleSort(col, $event)"
20840
- draggable="{{ resolvedOptions().reorderableColumns ? 'true' : 'false' }}"
20841
- (dragstart)="onDragStart($event, colIdx)"
20842
- (dragover)="onDragOver($event, colIdx)"
20843
- (drop)="onDrop($event, colIdx)"
20844
- (dragend)="onDragEnd($event)"
20845
- >
20846
- <div class="jv-grid-th-content">
20847
- <span>{{ col.header }}</span>
20848
- @if (col.sortable !== false && resolvedOptions().sortable) {
20849
- <span class="jv-grid-sort-icon" aria-hidden="true">
20850
- @if (sortState().columnKey === colKey(col)) {
20851
- @if (sortState().direction === 'asc') { ▲ }
20852
- @if (sortState().direction === 'desc') { ▼ }
20853
- }
20854
- </span>
20855
- }
20856
- </div>
20857
- @if (isColumnResizable(col)) {
20858
- <div
20859
- class="jv-grid-resize-handle"
20860
- (mousedown)="onResizeStart($event, col, colIdx)"
20861
- ></div>
20862
- }
20863
- </th>
20864
- }
20865
- @if (!hasGroupedHeaders && actions().length > 0) {
20866
- <th class="jv-grid-th jv-grid-th-actions" scope="col">
20867
- <span class="sr-only">{{ t('grid.actionsLabel', { label: '' }) }}</span>
20868
- </th>
20869
- }
20870
- </tr>
20871
-
20872
- @if (resolvedOptions().columnFilters) {
20873
- <tr class="jv-grid-filter-row">
20874
- @if (resolvedOptions().selectable) {
20875
- <th class="jv-grid-th jv-grid-th-select"></th>
20876
- }
20877
- @for (col of visibleColumns(); track colKey(col)) {
20878
- <th class="jv-grid-th jv-grid-th-filter">
20879
- @if (col.filterable !== false) {
20880
- <input
20881
- class="jv-grid-filter-input"
20882
- [placeholder]="t('grid.filterPlaceholder')"
20883
- [ngModel]="colFilters()[colKey(col)]?.value ?? ''"
20884
- (ngModelChange)="onColumnFilterChange(col, $event)"
20885
- />
20886
- }
20887
- </th>
20888
- }
20889
- @if (actions().length > 0) {
20890
- <th class="jv-grid-th jv-grid-th-actions"></th>
20891
- }
20892
- </tr>
20893
- }
20894
- </thead>
20895
-
20896
- <tbody class="jv-grid-tbody">
20897
- @if (displayData().length === 0 && !searchTerm().trim() && !hasActiveFilters()) {
20898
- <tr>
20899
- <td class="jv-grid-empty" [attr.colspan]="colspan()">
20900
- {{ resolvedOptions().emptyMessage || t('grid.emptyMessage') }}
20901
- </td>
20902
- </tr>
20903
- } @else if (displayData().length === 0 && (searchTerm().trim() || hasActiveFilters())) {
20904
- <tr>
20905
- <td class="jv-grid-empty" [attr.colspan]="colspan()">
20906
- {{ resolvedOptions().noResultsMessage || t('grid.noResults') }}
20907
- </td>
20908
- </tr>
20909
- } @else {
20910
- @for (row of displayData(); track getRowId(row, $index); let rowIdx = $index) {
20911
- <tr
20912
- class="jv-grid-tr"
20913
- [class.jv-grid-tr-editing]="isRowEditing(row)"
20914
- (click)="onRowClick(row)"
20915
- (dblclick)="onRowDblClick(row)"
20916
- (keydown.enter)="onRowClick(row)"
20917
- (keydown.space)="onRowClick($event, row)"
20918
- [tabindex]="0"
20919
- >
20920
- @if (resolvedOptions().selectable) {
20921
- <td
20922
- class="jv-grid-td jv-grid-td-select"
20923
- [class.jv-grid-td-sticky-left]="resolvedOptions().stickyColumns"
20924
- >
20925
- <input
20926
- type="checkbox"
20927
- class="jv-grid-checkbox"
20928
- [checked]="isSelected(row)"
20929
- (change)="toggleRow(row); $event.stopPropagation()"
20930
- (keydown.space)="$event.stopPropagation()"
20931
- [attr.aria-label]="getRowLabel(row, $index)"
20932
- />
20933
- </td>
20934
- }
20935
- @for (col of visibleColumns(); track colKey(col)) {
20936
- <td
20937
- class="jv-grid-td"
20938
- [class]="col.cellClass || ''"
20939
- [class.jv-grid-td-sticky-left]="isStickyLeft(col)"
20940
- [class.jv-grid-td-sticky-right]="isStickyRight(col)"
20941
- [class.jv-grid-td-editable]="isCellEditable(col)"
20942
- [style.text-align]="col.align ?? 'start'"
20943
- (dblclick)="startEdit(row, col, $event)"
20944
- >
20945
- @if (isEditingCell(row, col)) {
20946
- @if (col.editType === 'select' && col.editOptions) {
20947
- <jv-select
20948
- [options]="getEditOptions(col)"
20949
- [modelValue]="getEditValue(row, col)"
20950
- (selectionChange)="onEditValueChange(row, col, $event)"
20951
- />
20952
- } @else if (col.editType === 'boolean') {
20953
- <input
20954
- type="checkbox"
20955
- class="jv-grid-checkbox"
20956
- [checked]="getCellValue(row, col) === true"
20957
- (change)="commitEdit(row, col, $any($event.target).checked)"
20958
- />
20959
- } @else {
20960
- <input
20961
- class="jv-grid-edit-input"
20962
- [type]="col.editType === 'number' ? 'number' : 'text'"
20963
- [value]="getEditValue(row, col)"
20964
- (blur)="commitEdit(row, col, $any($event.target).value)"
20965
- (keydown.enter)="commitEdit(row, col, $any($event.target).value)"
20966
- (keydown.escape)="cancelEdit()"
20967
- #editInput
20968
- />
20969
- }
20970
- } @else {
20971
- @let value = getCellValue(row, col);
20972
- @if (col.type === 'boolean') {
20973
- @if (value) {
20974
- <span class="jv-grid-boolean jv-grid-boolean-true" aria-label="true">✓</span>
20975
- } @else {
20976
- <span class="jv-grid-boolean jv-grid-boolean-false" aria-label="false">✗</span>
20977
- }
20978
- } @else if (col.format) {
20979
- {{ col.format(value, row) }}
20980
- } @else {
20981
- {{ formatValue(value, col) }}
20982
- }
20983
- }
20984
- </td>
20985
- }
20986
- @if (actions().length > 0) {
20987
- <td
20988
- class="jv-grid-td jv-grid-td-actions"
20989
- [class.jv-grid-td-sticky-right]="resolvedOptions().stickyColumns"
20990
- >
20991
- <div class="jv-grid-actions-group">
20992
- @for (action of actions(); track action.id) {
20993
- <jv-button
20994
- [variant]="actionVariant(action.variant)"
20995
- [icon]="action.icon ?? null"
20996
- [disabled]="action.disabled?.(row) ?? false"
20997
- [attr.aria-label]="action.label"
20998
- (click)="onActionClick(action, row); $event.stopPropagation()"
20999
- >{{ action.label }}</jv-button>
21000
- }
21001
- </div>
21002
- </td>
21003
- }
21004
- </tr>
21005
- }
21006
- }
21007
- </tbody>
21008
- </table>
21009
- </div>
21010
- }
21011
-
21012
- @if (resolvedOptions().pageable && effectiveTotalPages() > 1 && !resolvedOptions().loading) {
21013
- <nav class="jv-grid-pagination" [attr.aria-label]="t('grid.paginationLabel', { label: '' })">
21014
- <div class="jv-grid-pagination-info">
21015
- {{ t('grid.itemsShowing', { start: pageStart(), end: pageEnd(), total: effectiveTotalItems() }) }}
21016
- </div>
21017
- <div class="jv-grid-pagination-controls">
21018
- <jv-button
21019
- variant="outline"
21020
- icon="chevrons-left"
21021
- [disabled]="pageIndex() === 0"
21022
- (click)="goToPage(0)"
21023
- [attr.aria-label]="t('grid.pageFirst')"
21024
- ></jv-button>
21025
- <jv-button
21026
- variant="outline"
21027
- icon="chevron-left"
21028
- [disabled]="pageIndex() === 0"
21029
- (click)="goToPage(pageIndex() - 1)"
21030
- [attr.aria-label]="t('grid.pagePrev')"
21031
- ></jv-button>
21032
- @for (p of pageRange(); track p) {
21033
- <jv-button
21034
- [variant]="p === pageIndex() ? 'primary' : 'outline'"
21035
- [attr.aria-current]="p === pageIndex() ? 'page' : null"
21036
- [attr.aria-label]="t('grid.pageOfTotal', { page: p + 1, total: effectiveTotalPages() })"
21037
- (click)="goToPage(p)"
21038
- >{{ p + 1 }}</jv-button>
21039
- }
21040
- <jv-button
21041
- variant="outline"
21042
- icon="chevron-right"
21043
- [disabled]="pageIndex() >= effectiveTotalPages() - 1"
21044
- (click)="goToPage(pageIndex() + 1)"
21045
- [attr.aria-label]="t('grid.pageNext')"
21046
- ></jv-button>
21047
- <jv-button
21048
- variant="outline"
21049
- icon="chevrons-right"
21050
- [disabled]="pageIndex() >= effectiveTotalPages() - 1"
21051
- (click)="goToPage(effectiveTotalPages() - 1)"
21052
- [attr.aria-label]="t('grid.pageLast')"
21053
- ></jv-button>
21054
- </div>
21055
- </nav>
21056
- }
21057
- </div>
21058
- `, isInline: true, styles: [".jv-grid{display:flex;flex-direction:column;gap:var(--jv-spacing-md)}.jv-grid-toolbar{display:flex;align-items:center;justify-content:space-between;gap:var(--jv-spacing-md);flex-wrap:wrap}.jv-grid-search{display:flex;align-items:center;gap:var(--jv-spacing-sm)}.jv-grid-export-actions{display:flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-grid-table-wrap{overflow-x:auto;overflow-y:auto;border:1px solid var(--jv-color-border);border-radius:var(--jv-radius-md);position:relative}.jv-grid-virtual{max-height:600px}.jv-grid-virtual-spacer{pointer-events:none}.jv-grid-table{width:100%;border-collapse:collapse;background:var(--jv-color-surface)}.jv-grid-thead{background:var(--jv-color-surface-muted)}.jv-grid-th{padding:var(--jv-spacing-sm) var(--jv-spacing-md);font-weight:600;font-size:.875rem;color:var(--jv-color-foreground-muted);text-align:start;white-space:nowrap;border-bottom:1px solid var(--jv-color-border);-webkit-user-select:none;user-select:none;position:relative}.jv-grid-th-sortable{cursor:pointer}.jv-grid-th-sortable:hover{background:var(--jv-color-surface)}.jv-grid-th-reorderable{cursor:grab}.jv-grid-th-reorderable:active{cursor:grabbing}.jv-grid-th-sticky-left{position:sticky;left:0;z-index:2;background:var(--jv-color-surface-muted);border-right:1px solid var(--jv-color-border)}.jv-grid-th-sticky-right{position:sticky;right:0;z-index:2;background:var(--jv-color-surface-muted);border-left:1px solid var(--jv-color-border)}.jv-grid-th-content{display:flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-grid-sort-icon{font-size:.75rem;line-height:1}.jv-grid-th-select,.jv-grid-td-select{width:3rem;text-align:center}.jv-grid-th-actions,.jv-grid-td-actions{width:1%;white-space:nowrap}.jv-grid-td{padding:var(--jv-spacing-sm) var(--jv-spacing-md);color:var(--jv-color-foreground);font-size:var(--jv-density-font-size);border-bottom:1px solid var(--jv-color-border)}.jv-grid-td-sticky-left{position:sticky;left:0;z-index:1;background:var(--jv-color-surface);border-right:1px solid var(--jv-color-border)}.jv-grid-td-sticky-right{position:sticky;right:0;z-index:1;background:var(--jv-color-surface);border-left:1px solid var(--jv-color-border)}.jv-grid-t:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:-2px}.jv-grid-tr{transition:background-color .12s ease}.jv-grid-tr:hover{background:var(--jv-color-surface-muted)}.jv-grid-tr:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:-2px}.jv-grid-tr-editing{background:var(--jv-color-surface-highlight, rgba(59, 130, 246, .05))}.jv-grid-checkbox{width:1rem;height:1rem;accent-color:var(--jv-color-primary);cursor:pointer}.jv-grid-boolean{display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;border-radius:999px;font-size:.75rem;font-weight:700}.jv-grid-boolean-true{background:var(--jv-color-success);color:#fff}.jv-grid-boolean-false{background:var(--jv-color-danger);color:#fff}.jv-grid-empty{padding:var(--jv-spacing-xl) var(--jv-spacing-md);text-align:center;color:var(--jv-color-foreground-muted);font-style:italic}.jv-grid-actions-group{display:flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-grid-loading{display:flex;align-items:center;justify-content:center;gap:var(--jv-spacing-md);padding:var(--jv-spacing-xl);color:var(--jv-color-foreground-muted)}.jv-grid-spinner{width:1.25rem;height:1.25rem;border:2px solid var(--jv-color-border);border-right-color:var(--jv-color-primary);border-radius:999px;animation:jv-grid-spin .8s linear infinite}@keyframes jv-grid-spin{to{transform:rotate(360deg)}}.jv-grid-pagination{display:flex;align-items:center;justify-content:space-between;gap:var(--jv-spacing-md);flex-wrap:wrap}.jv-grid-pagination-info{font-size:.875rem;color:var(--jv-color-foreground-muted)}.jv-grid-pagination-controls{display:flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-grid-resize-handle{position:absolute;top:0;right:0;width:4px;height:100%;cursor:col-resize;background:transparent;z-index:3}.jv-grid-resize-handle:hover,.jv-grid-resize-handle:active{background:var(--jv-color-primary)}.jv-grid-filter-row .jv-grid-th{padding:var(--jv-spacing-xs) var(--jv-spacing-sm)}.jv-grid-filter-input{width:100%;min-height:var(--jv-density-control-height);padding:0 var(--jv-spacing-sm);border:1px solid var(--jv-color-border);border-radius:var(--jv-radius-sm);background:var(--jv-color-surface);color:var(--jv-color-foreground);font-size:.8rem;box-sizing:border-box}.jv-grid-td-editable{cursor:pointer}.jv-grid-td-editable:hover{box-shadow:inset 0 0 0 1px var(--jv-color-primary)}.jv-grid-edit-input{width:100%;min-height:var(--jv-density-control-height);padding:0 var(--jv-spacing-sm);border:1px solid var(--jv-color-primary);border-radius:var(--jv-radius-sm);background:var(--jv-color-surface);color:var(--jv-color-foreground);font-size:var(--jv-density-font-size);box-sizing:border-box}.jv-grid-edit-input:focus{outline:2px solid var(--jv-color-primary);outline-offset:-1px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: JvButtonComponent, selector: "jv-button", inputs: ["type", "variant", "icon", "iconPosition", "loading", "disabled"] }, { kind: "component", type: JvIconComponent, selector: "jv-icon", inputs: ["name", "size", "strokeWidth", "decorative", "ariaLabel"] }, { kind: "component", type: JvInputComponent, selector: "jv-input", inputs: ["type", "placeholder", "required", "invalid", "describedBy", "inputId"] }, { kind: "component", type: JvSelectComponent, selector: "jv-select", inputs: ["options", "placeholder", "required", "invalid", "describedBy", "inputId", "modelValue"], outputs: ["selectionChange"] }] });
20814
+ 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: `
20815
+ <div class="jv-grid" role="region" [attr.aria-label]="resolvedOptions().ariaLabel || gridLabel()">
20816
+ @if (resolvedOptions().searchable || resolvedOptions().exportable) {
20817
+ <div class="jv-grid-toolbar">
20818
+ @if (resolvedOptions().searchable) {
20819
+ <div class="jv-grid-search">
20820
+ <jv-icon name="search" [size]="16" />
20821
+ <jv-input
20822
+ inputId="grid-search"
20823
+ [placeholder]="t('grid.searchPlaceholder')"
20824
+ [ngModel]="searchTerm()"
20825
+ (ngModelChange)="onSearchInput($event)"
20826
+ />
20827
+ </div>
20828
+ }
20829
+ @if (resolvedOptions().exportable) {
20830
+ <div class="jv-grid-export-actions">
20831
+ <jv-button variant="outline" icon="download" (click)="exportCsv()">{{ t('grid.exportCsv') }}</jv-button>
20832
+ <jv-button variant="outline" icon="file-spreadsheet" (click)="exportExcel()">{{ t('grid.exportExcel') }}</jv-button>
20833
+ </div>
20834
+ }
20835
+ </div>
20836
+ }
20837
+
20838
+ @if (resolvedOptions().loading) {
20839
+ <div class="jv-grid-loading" role="status" aria-live="polite">
20840
+ <span class="jv-grid-spinner" aria-hidden="true"></span>
20841
+ <span>{{ t('grid.loading') }}</span>
20842
+ </div>
20843
+ } @else {
20844
+ <div
20845
+ class="jv-grid-table-wrap"
20846
+ [class.jv-grid-virtual]="resolvedOptions().virtualScroll"
20847
+ #tableWrap
20848
+ (scroll)="onVirtualScroll($any($event.target))"
20849
+ >
20850
+ @if (resolvedOptions().virtualScroll) {
20851
+ <div class="jv-grid-virtual-spacer" [style.height.px]="virtualTotalHeight()"></div>
20852
+ }
20853
+ <table
20854
+ class="jv-grid-table"
20855
+ role="grid"
20856
+ [attr.aria-label]="resolvedOptions().ariaLabel || gridLabel()"
20857
+ [attr.aria-rowcount]="effectiveTotalItems()"
20858
+ [attr.aria-colcount]="visibleColumns().length"
20859
+ [class]="densityClass()"
20860
+ [style.position]="resolvedOptions().virtualScroll ? 'relative' : null"
20861
+ [style.top.px]="virtualOffsetY()"
20862
+ >
20863
+ <caption class="sr-only">{{ resolvedOptions().ariaLabel || gridLabel() }}</caption>
20864
+
20865
+ @let hasGroupedHeaders = groupedHeaders().length > 0;
20866
+
20867
+ <thead class="jv-grid-thead">
20868
+ @if (hasGroupedHeaders) {
20869
+ <tr>
20870
+ @if (resolvedOptions().selectable) {
20871
+ <th class="jv-grid-th jv-grid-th-select" scope="col" [attr.rowspan]="2">
20872
+ <input
20873
+ type="checkbox"
20874
+ class="jv-grid-checkbox"
20875
+ [checked]="allSelected()"
20876
+ (change)="toggleSelectAll($any($event.target).checked)"
20877
+ [attr.aria-label]="allSelected() ? t('grid.deselectAll') : t('grid.selectAll')"
20878
+ [indeterminate]="someSelected()"
20879
+ />
20880
+ </th>
20881
+ }
20882
+ @for (group of groupedHeaders(); track colKey(group)) {
20883
+ <th
20884
+ class="jv-grid-th"
20885
+ [class]="group.headerClass || ''"
20886
+ [class.jv-grid-th-sticky-left]="isStickyLeft(group)"
20887
+ [class.jv-grid-th-sticky-right]="isStickyRight(group)"
20888
+ [attr.colspan]="groupColspan(group)"
20889
+ scope="colgroup"
20890
+ [style.text-align]="group.align ?? 'start'"
20891
+ >
20892
+ <div class="jv-grid-th-content">{{ group.header }}</div>
20893
+ </th>
20894
+ }
20895
+ @if (actions().length > 0) {
20896
+ <th class="jv-grid-th jv-grid-th-actions" scope="col" [attr.rowspan]="2">
20897
+ <span class="sr-only">{{ t('grid.actionsLabel', { label: '' }) }}</span>
20898
+ </th>
20899
+ }
20900
+ </tr>
20901
+ }
20902
+
20903
+ <tr>
20904
+ @if (!hasGroupedHeaders && resolvedOptions().selectable) {
20905
+ <th class="jv-grid-th jv-grid-th-select" scope="col">
20906
+ <input
20907
+ type="checkbox"
20908
+ class="jv-grid-checkbox"
20909
+ [checked]="allSelected()"
20910
+ (change)="toggleSelectAll($any($event.target).checked)"
20911
+ [attr.aria-label]="allSelected() ? t('grid.deselectAll') : t('grid.selectAll')"
20912
+ [indeterminate]="someSelected()"
20913
+ />
20914
+ </th>
20915
+ }
20916
+ @for (col of visibleColumns(); track colKey(col); let colIdx = $index) {
20917
+ <th
20918
+ class="jv-grid-th"
20919
+ [class]="col.headerClass || ''"
20920
+ [class.jv-grid-th-sortable]="col.sortable !== false && resolvedOptions().sortable"
20921
+ [class.jv-grid-th-sticky-left]="isStickyLeft(col)"
20922
+ [class.jv-grid-th-sticky-right]="isStickyRight(col)"
20923
+ [class.jv-grid-th-reorderable]="resolvedOptions().reorderableColumns"
20924
+ [style.text-align]="col.align ?? 'start'"
20925
+ [style.width]="getColumnWidth(col)"
20926
+ [style.min-width]="col.minWidth || undefined"
20927
+ [style.max-width]="col.maxWidth || undefined"
20928
+ scope="col"
20929
+ [attr.aria-sort]="getAriaSort(col)"
20930
+ [attr.aria-label]="getSortLabel(col)"
20931
+ [tabindex]="col.sortable !== false && resolvedOptions().sortable ? 0 : -1"
20932
+ (click)="toggleSort(col)"
20933
+ (keydown.enter)="toggleSort(col)"
20934
+ (keydown.space)="toggleSort(col, $event)"
20935
+ draggable="{{ resolvedOptions().reorderableColumns ? 'true' : 'false' }}"
20936
+ (dragstart)="onDragStart($event, colIdx)"
20937
+ (dragover)="onDragOver($event, colIdx)"
20938
+ (drop)="onDrop($event, colIdx)"
20939
+ (dragend)="onDragEnd($event)"
20940
+ >
20941
+ <div class="jv-grid-th-content">
20942
+ <span>{{ col.header }}</span>
20943
+ @if (col.sortable !== false && resolvedOptions().sortable) {
20944
+ <span class="jv-grid-sort-icon" aria-hidden="true">
20945
+ @if (sortState().columnKey === colKey(col)) {
20946
+ @if (sortState().direction === 'asc') { ▲ }
20947
+ @if (sortState().direction === 'desc') { ▼ }
20948
+ }
20949
+ </span>
20950
+ }
20951
+ </div>
20952
+ @if (isColumnResizable(col)) {
20953
+ <div
20954
+ class="jv-grid-resize-handle"
20955
+ (mousedown)="onResizeStart($event, col, colIdx)"
20956
+ ></div>
20957
+ }
20958
+ </th>
20959
+ }
20960
+ @if (!hasGroupedHeaders && actions().length > 0) {
20961
+ <th class="jv-grid-th jv-grid-th-actions" scope="col">
20962
+ <span class="sr-only">{{ t('grid.actionsLabel', { label: '' }) }}</span>
20963
+ </th>
20964
+ }
20965
+ </tr>
20966
+
20967
+ @if (resolvedOptions().columnFilters) {
20968
+ <tr class="jv-grid-filter-row">
20969
+ @if (resolvedOptions().selectable) {
20970
+ <th class="jv-grid-th jv-grid-th-select"></th>
20971
+ }
20972
+ @for (col of visibleColumns(); track colKey(col)) {
20973
+ <th class="jv-grid-th jv-grid-th-filter">
20974
+ @if (col.filterable !== false) {
20975
+ <input
20976
+ class="jv-grid-filter-input"
20977
+ [placeholder]="t('grid.filterPlaceholder')"
20978
+ [attr.aria-label]="t('grid.filterForColumn', { column: col.header })"
20979
+ [ngModel]="colFilters()[colKey(col)]?.value ?? ''"
20980
+ (ngModelChange)="onColumnFilterChange(col, $event)"
20981
+ />
20982
+ }
20983
+ </th>
20984
+ }
20985
+ @if (actions().length > 0) {
20986
+ <th class="jv-grid-th jv-grid-th-actions"></th>
20987
+ }
20988
+ </tr>
20989
+ }
20990
+ </thead>
20991
+
20992
+ <tbody class="jv-grid-tbody">
20993
+ @if (displayData().length === 0 && !searchTerm().trim() && !hasActiveFilters()) {
20994
+ <tr>
20995
+ <td class="jv-grid-empty" [attr.colspan]="colspan()">
20996
+ {{ resolvedOptions().emptyMessage || t('grid.emptyMessage') }}
20997
+ </td>
20998
+ </tr>
20999
+ } @else if (displayData().length === 0 && (searchTerm().trim() || hasActiveFilters())) {
21000
+ <tr>
21001
+ <td class="jv-grid-empty" [attr.colspan]="colspan()">
21002
+ {{ resolvedOptions().noResultsMessage || t('grid.noResults') }}
21003
+ </td>
21004
+ </tr>
21005
+ } @else {
21006
+ @for (row of displayData(); track getRowId(row, $index); let rowIdx = $index) {
21007
+ <tr
21008
+ class="jv-grid-tr"
21009
+ role="row"
21010
+ [class.jv-grid-tr-editing]="isRowEditing(row)"
21011
+ [class.jv-grid-tr-selected]="isSelected(row)"
21012
+ [attr.aria-selected]="resolvedOptions().selectable ? isSelected(row) : null"
21013
+ (click)="onRowClick(row)"
21014
+ (dblclick)="onRowDblClick(row)"
21015
+ (keydown.enter)="onRowClick(row)"
21016
+ (keydown.space)="onRowClick($event, row)"
21017
+ [tabindex]="-1"
21018
+ >
21019
+ @if (resolvedOptions().selectable) {
21020
+ <td
21021
+ class="jv-grid-td jv-grid-td-select"
21022
+ [class.jv-grid-td-sticky-left]="resolvedOptions().stickyColumns"
21023
+ >
21024
+ <input
21025
+ type="checkbox"
21026
+ class="jv-grid-checkbox"
21027
+ [checked]="isSelected(row)"
21028
+ (change)="toggleRow(row); $event.stopPropagation()"
21029
+ (keydown.space)="$event.stopPropagation()"
21030
+ [attr.aria-label]="getRowLabel(row, $index)"
21031
+ />
21032
+ </td>
21033
+ }
21034
+ @for (col of visibleColumns(); track colKey(col)) {
21035
+ <td
21036
+ class="jv-grid-td"
21037
+ [class]="col.cellClass || ''"
21038
+ [class.jv-grid-td-sticky-left]="isStickyLeft(col)"
21039
+ [class.jv-grid-td-sticky-right]="isStickyRight(col)"
21040
+ [class.jv-grid-td-editable]="isCellEditable(col)"
21041
+ [style.text-align]="col.align ?? 'start'"
21042
+ (dblclick)="startEdit(row, col, $event)"
21043
+ (keydown.enter)="startEdit(row, col, $event)"
21044
+ [tabindex]="isCellEditable(col) ? -1 : null"
21045
+ >
21046
+ @if (isEditingCell(row, col)) {
21047
+ @if (col.editType === 'select' && col.editOptions) {
21048
+ <jv-select
21049
+ [options]="getEditOptions(col)"
21050
+ [modelValue]="getEditValue(row, col)"
21051
+ (selectionChange)="onEditValueChange(row, col, $event)"
21052
+ />
21053
+ } @else if (col.editType === 'boolean') {
21054
+ <input
21055
+ type="checkbox"
21056
+ class="jv-grid-checkbox"
21057
+ [checked]="getCellValue(row, col) === true"
21058
+ (change)="commitEdit(row, col, $any($event.target).checked)"
21059
+ [attr.aria-label]="col.header"
21060
+ />
21061
+ } @else {
21062
+ <input
21063
+ class="jv-grid-edit-input"
21064
+ [type]="col.editType === 'number' ? 'number' : 'text'"
21065
+ [value]="getEditValue(row, col)"
21066
+ (blur)="commitEdit(row, col, $any($event.target).value)"
21067
+ (keydown.enter)="commitEdit(row, col, $any($event.target).value)"
21068
+ (keydown.escape)="cancelEdit()"
21069
+ autofocus
21070
+ />
21071
+ }
21072
+ } @else {
21073
+ @let value = getCellValue(row, col);
21074
+ @if (col.type === 'boolean') {
21075
+ @if (value) {
21076
+ <span class="jv-grid-boolean jv-grid-boolean-true" aria-label="true">✓</span>
21077
+ } @else {
21078
+ <span class="jv-grid-boolean jv-grid-boolean-false" aria-label="false">✗</span>
21079
+ }
21080
+ } @else if (col.format) {
21081
+ {{ col.format(value, row) }}
21082
+ } @else {
21083
+ {{ formatValue(value, col) }}
21084
+ }
21085
+ }
21086
+ </td>
21087
+ }
21088
+ @if (actions().length > 0) {
21089
+ <td
21090
+ class="jv-grid-td jv-grid-td-actions"
21091
+ [class.jv-grid-td-sticky-right]="resolvedOptions().stickyColumns"
21092
+ >
21093
+ <div class="jv-grid-actions-group">
21094
+ @for (action of actions(); track action.id) {
21095
+ <jv-button
21096
+ [variant]="actionVariant(action.variant)"
21097
+ [icon]="action.icon ?? null"
21098
+ [disabled]="action.disabled?.(row) ?? false"
21099
+ [attr.aria-label]="action.label"
21100
+ [title]="action.label"
21101
+ (click)="onActionClick(action, row); $event.stopPropagation()"
21102
+ ></jv-button>
21103
+ }
21104
+ </div>
21105
+ </td>
21106
+ }
21107
+ </tr>
21108
+ }
21109
+ }
21110
+ </tbody>
21111
+ </table>
21112
+ </div>
21113
+ }
21114
+
21115
+ @if (resolvedOptions().pageable && effectiveTotalPages() > 1 && !resolvedOptions().loading) {
21116
+ <nav class="jv-grid-pagination" role="navigation" [attr.aria-label]="t('grid.paginationLabel', { label: '' })">
21117
+ <div class="jv-grid-pagination-info" aria-live="polite" aria-atomic="true">
21118
+ {{ t('grid.itemsShowing', { start: pageStart(), end: pageEnd(), total: effectiveTotalItems() }) }}
21119
+ </div>
21120
+ <div class="jv-grid-pagination-controls">
21121
+ <jv-button
21122
+ variant="outline"
21123
+ icon="chevrons-left"
21124
+ [disabled]="pageIndex() === 0"
21125
+ (click)="goToPage(0)"
21126
+ [attr.aria-label]="t('grid.pageFirst')"
21127
+ ></jv-button>
21128
+ <jv-button
21129
+ variant="outline"
21130
+ icon="chevron-left"
21131
+ [disabled]="pageIndex() === 0"
21132
+ (click)="goToPage(pageIndex() - 1)"
21133
+ [attr.aria-label]="t('grid.pagePrev')"
21134
+ ></jv-button>
21135
+ @for (p of pageRange(); track p) {
21136
+ <jv-button
21137
+ [variant]="p === pageIndex() ? 'primary' : 'outline'"
21138
+ [attr.aria-current]="p === pageIndex() ? 'page' : null"
21139
+ [attr.aria-label]="t('grid.pageOfTotal', { page: p + 1, total: effectiveTotalPages() })"
21140
+ (click)="goToPage(p)"
21141
+ >{{ p + 1 }}</jv-button>
21142
+ }
21143
+ <jv-button
21144
+ variant="outline"
21145
+ icon="chevron-right"
21146
+ [disabled]="pageIndex() >= effectiveTotalPages() - 1"
21147
+ (click)="goToPage(pageIndex() + 1)"
21148
+ [attr.aria-label]="t('grid.pageNext')"
21149
+ ></jv-button>
21150
+ <jv-button
21151
+ variant="outline"
21152
+ icon="chevrons-right"
21153
+ [disabled]="pageIndex() >= effectiveTotalPages() - 1"
21154
+ (click)="goToPage(effectiveTotalPages() - 1)"
21155
+ [attr.aria-label]="t('grid.pageLast')"
21156
+ ></jv-button>
21157
+ </div>
21158
+ </nav>
21159
+ }
21160
+ </div>
21161
+ `, isInline: true, styles: [".jv-grid{display:flex;flex-direction:column;gap:var(--jv-spacing-md)}.jv-grid-toolbar{display:flex;align-items:center;justify-content:space-between;gap:var(--jv-spacing-md);flex-wrap:wrap}.jv-grid-search{display:flex;align-items:center;gap:var(--jv-spacing-sm)}.jv-grid-export-actions{display:flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-grid-table-wrap{overflow-x:auto;overflow-y:auto;-webkit-overflow-scrolling:touch;border:1px solid var(--jv-color-border);border-radius:var(--jv-radius-md);position:relative}.jv-grid-virtual{max-height:600px}.jv-grid-virtual-spacer{pointer-events:none}.jv-grid-table{width:100%;border-collapse:collapse;background:var(--jv-color-surface)}.jv-grid-thead{background:var(--jv-color-surface-muted)}.jv-grid-th{padding:var(--jv-spacing-sm) var(--jv-spacing-md);font-weight:600;font-size:.875rem;color:var(--jv-color-foreground-muted);text-align:start;white-space:nowrap;border-bottom:1px solid var(--jv-color-border);-webkit-user-select:none;user-select:none;position:relative}.jv-grid-th-sortable{cursor:pointer}.jv-grid-th-sortable:hover{background:var(--jv-color-surface)}.jv-grid-th-reorderable{cursor:grab}.jv-grid-th-reorderable:active{cursor:grabbing}.jv-grid-th-sticky-left{position:sticky;left:0;z-index:2;background:var(--jv-color-surface-muted);border-right:1px solid var(--jv-color-border)}.jv-grid-th-sticky-right{position:sticky;right:0;z-index:2;background:var(--jv-color-surface-muted);border-left:1px solid var(--jv-color-border)}.jv-grid-th-content{display:flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-grid-sort-icon{font-size:.75rem;line-height:1}.jv-grid-th-select,.jv-grid-td-select{width:3rem;text-align:center}.jv-grid-th-actions,.jv-grid-td-actions{width:1%;white-space:nowrap}.jv-grid-td{padding:var(--jv-spacing-sm) var(--jv-spacing-md);color:var(--jv-color-foreground);font-size:var(--jv-density-font-size);border-bottom:1px solid var(--jv-color-border)}.jv-grid-td-sticky-left{position:sticky;left:0;z-index:1;background:var(--jv-color-surface);border-right:1px solid var(--jv-color-border)}.jv-grid-td-sticky-right{position:sticky;right:0;z-index:1;background:var(--jv-color-surface);border-left:1px solid var(--jv-color-border)}.jv-grid-t:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:-2px}.jv-grid-tr{transition:background-color .12s ease}.jv-grid-tr:hover{background:var(--jv-color-surface-muted)}.jv-grid-tr:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:-2px}.jv-grid-tr-editing{background:var(--jv-color-surface-highlight, rgba(59, 130, 246, .05))}.jv-grid-tr-selected{background:var(--jv-color-primary-light, rgba(59, 130, 246, .08))}.jv-grid-checkbox{width:1rem;height:1rem;accent-color:var(--jv-color-primary);cursor:pointer}.jv-grid-boolean{display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;border-radius:999px;font-size:.75rem;font-weight:700}.jv-grid-boolean-true{background:var(--jv-color-success);color:#fff}.jv-grid-boolean-false{background:var(--jv-color-danger);color:#fff}.jv-grid-empty{padding:var(--jv-spacing-xl) var(--jv-spacing-md);text-align:center;color:var(--jv-color-foreground-muted);font-style:italic}.jv-grid-actions-group{display:flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-grid-loading{display:flex;align-items:center;justify-content:center;gap:var(--jv-spacing-md);padding:var(--jv-spacing-xl);color:var(--jv-color-foreground-muted)}.jv-grid-spinner{width:1.25rem;height:1.25rem;border:2px solid var(--jv-color-border);border-right-color:var(--jv-color-primary);border-radius:999px;animation:jv-grid-spin .8s linear infinite}@keyframes jv-grid-spin{to{transform:rotate(360deg)}}.jv-grid-pagination{display:flex;align-items:center;justify-content:space-between;gap:var(--jv-spacing-md);flex-wrap:wrap}.jv-grid-pagination-info{font-size:.875rem;color:var(--jv-color-foreground-muted)}.jv-grid-pagination-controls{display:flex;align-items:center;gap:var(--jv-spacing-xs);flex-wrap:wrap}@media(max-width:640px){.jv-grid-pagination{flex-direction:column;align-items:stretch;gap:var(--jv-spacing-sm)}.jv-grid-pagination-info{text-align:center}.jv-grid-pagination-controls{justify-content:center}.jv-grid-toolbar{flex-direction:column;align-items:stretch}.jv-grid-search{width:100%}.jv-grid-export-actions{justify-content:stretch}.jv-grid-export-actions jv-button{flex:1}}.jv-grid-resize-handle{position:absolute;top:0;right:0;width:4px;height:100%;cursor:col-resize;background:transparent;z-index:3}.jv-grid-resize-handle:hover,.jv-grid-resize-handle:active{background:var(--jv-color-primary)}.jv-grid-filter-row .jv-grid-th{padding:var(--jv-spacing-xs) var(--jv-spacing-sm)}.jv-grid-filter-input{width:100%;min-height:var(--jv-density-control-height);padding:0 var(--jv-spacing-sm);border:1px solid var(--jv-color-border);border-radius:var(--jv-radius-sm);background:var(--jv-color-surface);color:var(--jv-color-foreground);font-size:.8rem;box-sizing:border-box}.jv-grid-td-editable{cursor:pointer}.jv-grid-td-editable:hover{box-shadow:inset 0 0 0 1px var(--jv-color-primary)}.jv-grid-edit-input{width:100%;min-height:var(--jv-density-control-height);padding:0 var(--jv-spacing-sm);border:1px solid var(--jv-color-primary);border-radius:var(--jv-radius-sm);background:var(--jv-color-surface);color:var(--jv-color-foreground);font-size:var(--jv-density-font-size);box-sizing:border-box}.jv-grid-edit-input:focus{outline:2px solid var(--jv-color-primary);outline-offset:-1px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: JvButtonComponent, selector: "jv-button", inputs: ["type", "variant", "icon", "iconPosition", "loading", "disabled"] }, { kind: "component", type: JvIconComponent, selector: "jv-icon", inputs: ["name", "size", "strokeWidth", "decorative", "ariaLabel"] }, { kind: "component", type: JvInputComponent, selector: "jv-input", inputs: ["type", "placeholder", "required", "invalid", "describedBy", "inputId"] }, { kind: "component", type: JvSelectComponent, selector: "jv-select", inputs: ["options", "placeholder", "required", "invalid", "describedBy", "inputId", "modelValue"], outputs: ["selectionChange"] }] });
21059
21162
  }
21060
21163
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JvGridComponent, decorators: [{
21061
21164
  type: Component,
21062
- args: [{ selector: 'jv-grid', standalone: true, imports: [FormsModule, JvButtonComponent, JvIconComponent, JvInputComponent, JvSelectComponent], template: `
21063
- <div class="jv-grid" role="region" [attr.aria-label]="resolvedOptions().ariaLabel || gridLabel()">
21064
- @if (resolvedOptions().searchable || resolvedOptions().exportable) {
21065
- <div class="jv-grid-toolbar">
21066
- @if (resolvedOptions().searchable) {
21067
- <div class="jv-grid-search">
21068
- <jv-icon name="search" [size]="16" />
21069
- <jv-input
21070
- inputId="grid-search"
21071
- [placeholder]="t('grid.searchPlaceholder')"
21072
- [ngModel]="searchTerm()"
21073
- (ngModelChange)="onSearchInput($event)"
21074
- />
21075
- </div>
21076
- }
21077
- @if (resolvedOptions().exportable) {
21078
- <div class="jv-grid-export-actions">
21079
- <jv-button variant="outline" icon="download" (click)="exportCsv()">{{ t('grid.exportCsv') }}</jv-button>
21080
- <jv-button variant="outline" icon="file-spreadsheet" (click)="exportExcel()">{{ t('grid.exportExcel') }}</jv-button>
21081
- </div>
21082
- }
21083
- </div>
21084
- }
21085
-
21086
- @if (resolvedOptions().loading) {
21087
- <div class="jv-grid-loading" role="status" aria-live="polite">
21088
- <span class="jv-grid-spinner" aria-hidden="true"></span>
21089
- <span>{{ t('grid.loading') }}</span>
21090
- </div>
21091
- } @else {
21092
- <div
21093
- class="jv-grid-table-wrap"
21094
- [class.jv-grid-virtual]="resolvedOptions().virtualScroll"
21095
- #tableWrap
21096
- (scroll)="onVirtualScroll($any($event.target))"
21097
- >
21098
- @if (resolvedOptions().virtualScroll) {
21099
- <div class="jv-grid-virtual-spacer" [style.height.px]="virtualTotalHeight()"></div>
21100
- }
21101
- <table
21102
- class="jv-grid-table"
21103
- role="grid"
21104
- [attr.aria-label]="resolvedOptions().ariaLabel || gridLabel()"
21105
- [class]="densityClass()"
21106
- [style.position]="resolvedOptions().virtualScroll ? 'relative' : null"
21107
- [style.top.px]="virtualOffsetY()"
21108
- >
21109
- <caption class="sr-only">{{ resolvedOptions().ariaLabel || gridLabel() }}</caption>
21110
-
21111
- @let hasGroupedHeaders = groupedHeaders().length > 0;
21112
-
21113
- <thead class="jv-grid-thead">
21114
- @if (hasGroupedHeaders) {
21115
- <tr>
21116
- @if (resolvedOptions().selectable) {
21117
- <th class="jv-grid-th jv-grid-th-select" scope="col" [attr.rowspan]="2">
21118
- <input
21119
- type="checkbox"
21120
- class="jv-grid-checkbox"
21121
- [checked]="allSelected()"
21122
- (change)="toggleSelectAll($any($event.target).checked)"
21123
- [attr.aria-label]="allSelected() ? t('grid.deselectAll') : t('grid.selectAll')"
21124
- [indeterminate]="someSelected()"
21125
- />
21126
- </th>
21127
- }
21128
- @for (group of groupedHeaders(); track colKey(group)) {
21129
- <th
21130
- class="jv-grid-th"
21131
- [class]="group.headerClass || ''"
21132
- [class.jv-grid-th-sticky-left]="isStickyLeft(group)"
21133
- [class.jv-grid-th-sticky-right]="isStickyRight(group)"
21134
- [attr.colspan]="groupColspan(group)"
21135
- scope="colgroup"
21136
- [style.text-align]="group.align ?? 'start'"
21137
- >
21138
- <div class="jv-grid-th-content">{{ group.header }}</div>
21139
- </th>
21140
- }
21141
- @if (actions().length > 0) {
21142
- <th class="jv-grid-th jv-grid-th-actions" scope="col" [attr.rowspan]="2">
21143
- <span class="sr-only">{{ t('grid.actionsLabel', { label: '' }) }}</span>
21144
- </th>
21145
- }
21146
- </tr>
21147
- }
21148
-
21149
- <tr>
21150
- @if (!hasGroupedHeaders && resolvedOptions().selectable) {
21151
- <th class="jv-grid-th jv-grid-th-select" scope="col">
21152
- <input
21153
- type="checkbox"
21154
- class="jv-grid-checkbox"
21155
- [checked]="allSelected()"
21156
- (change)="toggleSelectAll($any($event.target).checked)"
21157
- [attr.aria-label]="allSelected() ? t('grid.deselectAll') : t('grid.selectAll')"
21158
- [indeterminate]="someSelected()"
21159
- />
21160
- </th>
21161
- }
21162
- @for (col of visibleColumns(); track colKey(col); let colIdx = $index) {
21163
- <th
21164
- class="jv-grid-th"
21165
- [class]="col.headerClass || ''"
21166
- [class.jv-grid-th-sortable]="col.sortable !== false && resolvedOptions().sortable"
21167
- [class.jv-grid-th-sticky-left]="isStickyLeft(col)"
21168
- [class.jv-grid-th-sticky-right]="isStickyRight(col)"
21169
- [class.jv-grid-th-reorderable]="resolvedOptions().reorderableColumns"
21170
- [style.text-align]="col.align ?? 'start'"
21171
- [style.width]="getColumnWidth(col)"
21172
- [style.min-width]="col.minWidth || undefined"
21173
- [style.max-width]="col.maxWidth || undefined"
21174
- scope="col"
21175
- [attr.aria-sort]="getAriaSort(col)"
21176
- [attr.aria-label]="getSortLabel(col)"
21177
- [tabindex]="col.sortable !== false && resolvedOptions().sortable ? 0 : -1"
21178
- (click)="toggleSort(col)"
21179
- (keydown.enter)="toggleSort(col)"
21180
- (keydown.space)="toggleSort(col, $event)"
21181
- draggable="{{ resolvedOptions().reorderableColumns ? 'true' : 'false' }}"
21182
- (dragstart)="onDragStart($event, colIdx)"
21183
- (dragover)="onDragOver($event, colIdx)"
21184
- (drop)="onDrop($event, colIdx)"
21185
- (dragend)="onDragEnd($event)"
21186
- >
21187
- <div class="jv-grid-th-content">
21188
- <span>{{ col.header }}</span>
21189
- @if (col.sortable !== false && resolvedOptions().sortable) {
21190
- <span class="jv-grid-sort-icon" aria-hidden="true">
21191
- @if (sortState().columnKey === colKey(col)) {
21192
- @if (sortState().direction === 'asc') { ▲ }
21193
- @if (sortState().direction === 'desc') { ▼ }
21194
- }
21195
- </span>
21196
- }
21197
- </div>
21198
- @if (isColumnResizable(col)) {
21199
- <div
21200
- class="jv-grid-resize-handle"
21201
- (mousedown)="onResizeStart($event, col, colIdx)"
21202
- ></div>
21203
- }
21204
- </th>
21205
- }
21206
- @if (!hasGroupedHeaders && actions().length > 0) {
21207
- <th class="jv-grid-th jv-grid-th-actions" scope="col">
21208
- <span class="sr-only">{{ t('grid.actionsLabel', { label: '' }) }}</span>
21209
- </th>
21210
- }
21211
- </tr>
21212
-
21213
- @if (resolvedOptions().columnFilters) {
21214
- <tr class="jv-grid-filter-row">
21215
- @if (resolvedOptions().selectable) {
21216
- <th class="jv-grid-th jv-grid-th-select"></th>
21217
- }
21218
- @for (col of visibleColumns(); track colKey(col)) {
21219
- <th class="jv-grid-th jv-grid-th-filter">
21220
- @if (col.filterable !== false) {
21221
- <input
21222
- class="jv-grid-filter-input"
21223
- [placeholder]="t('grid.filterPlaceholder')"
21224
- [ngModel]="colFilters()[colKey(col)]?.value ?? ''"
21225
- (ngModelChange)="onColumnFilterChange(col, $event)"
21226
- />
21227
- }
21228
- </th>
21229
- }
21230
- @if (actions().length > 0) {
21231
- <th class="jv-grid-th jv-grid-th-actions"></th>
21232
- }
21233
- </tr>
21234
- }
21235
- </thead>
21236
-
21237
- <tbody class="jv-grid-tbody">
21238
- @if (displayData().length === 0 && !searchTerm().trim() && !hasActiveFilters()) {
21239
- <tr>
21240
- <td class="jv-grid-empty" [attr.colspan]="colspan()">
21241
- {{ resolvedOptions().emptyMessage || t('grid.emptyMessage') }}
21242
- </td>
21243
- </tr>
21244
- } @else if (displayData().length === 0 && (searchTerm().trim() || hasActiveFilters())) {
21245
- <tr>
21246
- <td class="jv-grid-empty" [attr.colspan]="colspan()">
21247
- {{ resolvedOptions().noResultsMessage || t('grid.noResults') }}
21248
- </td>
21249
- </tr>
21250
- } @else {
21251
- @for (row of displayData(); track getRowId(row, $index); let rowIdx = $index) {
21252
- <tr
21253
- class="jv-grid-tr"
21254
- [class.jv-grid-tr-editing]="isRowEditing(row)"
21255
- (click)="onRowClick(row)"
21256
- (dblclick)="onRowDblClick(row)"
21257
- (keydown.enter)="onRowClick(row)"
21258
- (keydown.space)="onRowClick($event, row)"
21259
- [tabindex]="0"
21260
- >
21261
- @if (resolvedOptions().selectable) {
21262
- <td
21263
- class="jv-grid-td jv-grid-td-select"
21264
- [class.jv-grid-td-sticky-left]="resolvedOptions().stickyColumns"
21265
- >
21266
- <input
21267
- type="checkbox"
21268
- class="jv-grid-checkbox"
21269
- [checked]="isSelected(row)"
21270
- (change)="toggleRow(row); $event.stopPropagation()"
21271
- (keydown.space)="$event.stopPropagation()"
21272
- [attr.aria-label]="getRowLabel(row, $index)"
21273
- />
21274
- </td>
21275
- }
21276
- @for (col of visibleColumns(); track colKey(col)) {
21277
- <td
21278
- class="jv-grid-td"
21279
- [class]="col.cellClass || ''"
21280
- [class.jv-grid-td-sticky-left]="isStickyLeft(col)"
21281
- [class.jv-grid-td-sticky-right]="isStickyRight(col)"
21282
- [class.jv-grid-td-editable]="isCellEditable(col)"
21283
- [style.text-align]="col.align ?? 'start'"
21284
- (dblclick)="startEdit(row, col, $event)"
21285
- >
21286
- @if (isEditingCell(row, col)) {
21287
- @if (col.editType === 'select' && col.editOptions) {
21288
- <jv-select
21289
- [options]="getEditOptions(col)"
21290
- [modelValue]="getEditValue(row, col)"
21291
- (selectionChange)="onEditValueChange(row, col, $event)"
21292
- />
21293
- } @else if (col.editType === 'boolean') {
21294
- <input
21295
- type="checkbox"
21296
- class="jv-grid-checkbox"
21297
- [checked]="getCellValue(row, col) === true"
21298
- (change)="commitEdit(row, col, $any($event.target).checked)"
21299
- />
21300
- } @else {
21301
- <input
21302
- class="jv-grid-edit-input"
21303
- [type]="col.editType === 'number' ? 'number' : 'text'"
21304
- [value]="getEditValue(row, col)"
21305
- (blur)="commitEdit(row, col, $any($event.target).value)"
21306
- (keydown.enter)="commitEdit(row, col, $any($event.target).value)"
21307
- (keydown.escape)="cancelEdit()"
21308
- #editInput
21309
- />
21310
- }
21311
- } @else {
21312
- @let value = getCellValue(row, col);
21313
- @if (col.type === 'boolean') {
21314
- @if (value) {
21315
- <span class="jv-grid-boolean jv-grid-boolean-true" aria-label="true">✓</span>
21316
- } @else {
21317
- <span class="jv-grid-boolean jv-grid-boolean-false" aria-label="false">✗</span>
21318
- }
21319
- } @else if (col.format) {
21320
- {{ col.format(value, row) }}
21321
- } @else {
21322
- {{ formatValue(value, col) }}
21323
- }
21324
- }
21325
- </td>
21326
- }
21327
- @if (actions().length > 0) {
21328
- <td
21329
- class="jv-grid-td jv-grid-td-actions"
21330
- [class.jv-grid-td-sticky-right]="resolvedOptions().stickyColumns"
21331
- >
21332
- <div class="jv-grid-actions-group">
21333
- @for (action of actions(); track action.id) {
21334
- <jv-button
21335
- [variant]="actionVariant(action.variant)"
21336
- [icon]="action.icon ?? null"
21337
- [disabled]="action.disabled?.(row) ?? false"
21338
- [attr.aria-label]="action.label"
21339
- (click)="onActionClick(action, row); $event.stopPropagation()"
21340
- >{{ action.label }}</jv-button>
21341
- }
21342
- </div>
21343
- </td>
21344
- }
21345
- </tr>
21346
- }
21347
- }
21348
- </tbody>
21349
- </table>
21350
- </div>
21351
- }
21352
-
21353
- @if (resolvedOptions().pageable && effectiveTotalPages() > 1 && !resolvedOptions().loading) {
21354
- <nav class="jv-grid-pagination" [attr.aria-label]="t('grid.paginationLabel', { label: '' })">
21355
- <div class="jv-grid-pagination-info">
21356
- {{ t('grid.itemsShowing', { start: pageStart(), end: pageEnd(), total: effectiveTotalItems() }) }}
21357
- </div>
21358
- <div class="jv-grid-pagination-controls">
21359
- <jv-button
21360
- variant="outline"
21361
- icon="chevrons-left"
21362
- [disabled]="pageIndex() === 0"
21363
- (click)="goToPage(0)"
21364
- [attr.aria-label]="t('grid.pageFirst')"
21365
- ></jv-button>
21366
- <jv-button
21367
- variant="outline"
21368
- icon="chevron-left"
21369
- [disabled]="pageIndex() === 0"
21370
- (click)="goToPage(pageIndex() - 1)"
21371
- [attr.aria-label]="t('grid.pagePrev')"
21372
- ></jv-button>
21373
- @for (p of pageRange(); track p) {
21374
- <jv-button
21375
- [variant]="p === pageIndex() ? 'primary' : 'outline'"
21376
- [attr.aria-current]="p === pageIndex() ? 'page' : null"
21377
- [attr.aria-label]="t('grid.pageOfTotal', { page: p + 1, total: effectiveTotalPages() })"
21378
- (click)="goToPage(p)"
21379
- >{{ p + 1 }}</jv-button>
21380
- }
21381
- <jv-button
21382
- variant="outline"
21383
- icon="chevron-right"
21384
- [disabled]="pageIndex() >= effectiveTotalPages() - 1"
21385
- (click)="goToPage(pageIndex() + 1)"
21386
- [attr.aria-label]="t('grid.pageNext')"
21387
- ></jv-button>
21388
- <jv-button
21389
- variant="outline"
21390
- icon="chevrons-right"
21391
- [disabled]="pageIndex() >= effectiveTotalPages() - 1"
21392
- (click)="goToPage(effectiveTotalPages() - 1)"
21393
- [attr.aria-label]="t('grid.pageLast')"
21394
- ></jv-button>
21395
- </div>
21396
- </nav>
21397
- }
21398
- </div>
21399
- `, styles: [".jv-grid{display:flex;flex-direction:column;gap:var(--jv-spacing-md)}.jv-grid-toolbar{display:flex;align-items:center;justify-content:space-between;gap:var(--jv-spacing-md);flex-wrap:wrap}.jv-grid-search{display:flex;align-items:center;gap:var(--jv-spacing-sm)}.jv-grid-export-actions{display:flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-grid-table-wrap{overflow-x:auto;overflow-y:auto;border:1px solid var(--jv-color-border);border-radius:var(--jv-radius-md);position:relative}.jv-grid-virtual{max-height:600px}.jv-grid-virtual-spacer{pointer-events:none}.jv-grid-table{width:100%;border-collapse:collapse;background:var(--jv-color-surface)}.jv-grid-thead{background:var(--jv-color-surface-muted)}.jv-grid-th{padding:var(--jv-spacing-sm) var(--jv-spacing-md);font-weight:600;font-size:.875rem;color:var(--jv-color-foreground-muted);text-align:start;white-space:nowrap;border-bottom:1px solid var(--jv-color-border);-webkit-user-select:none;user-select:none;position:relative}.jv-grid-th-sortable{cursor:pointer}.jv-grid-th-sortable:hover{background:var(--jv-color-surface)}.jv-grid-th-reorderable{cursor:grab}.jv-grid-th-reorderable:active{cursor:grabbing}.jv-grid-th-sticky-left{position:sticky;left:0;z-index:2;background:var(--jv-color-surface-muted);border-right:1px solid var(--jv-color-border)}.jv-grid-th-sticky-right{position:sticky;right:0;z-index:2;background:var(--jv-color-surface-muted);border-left:1px solid var(--jv-color-border)}.jv-grid-th-content{display:flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-grid-sort-icon{font-size:.75rem;line-height:1}.jv-grid-th-select,.jv-grid-td-select{width:3rem;text-align:center}.jv-grid-th-actions,.jv-grid-td-actions{width:1%;white-space:nowrap}.jv-grid-td{padding:var(--jv-spacing-sm) var(--jv-spacing-md);color:var(--jv-color-foreground);font-size:var(--jv-density-font-size);border-bottom:1px solid var(--jv-color-border)}.jv-grid-td-sticky-left{position:sticky;left:0;z-index:1;background:var(--jv-color-surface);border-right:1px solid var(--jv-color-border)}.jv-grid-td-sticky-right{position:sticky;right:0;z-index:1;background:var(--jv-color-surface);border-left:1px solid var(--jv-color-border)}.jv-grid-t:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:-2px}.jv-grid-tr{transition:background-color .12s ease}.jv-grid-tr:hover{background:var(--jv-color-surface-muted)}.jv-grid-tr:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:-2px}.jv-grid-tr-editing{background:var(--jv-color-surface-highlight, rgba(59, 130, 246, .05))}.jv-grid-checkbox{width:1rem;height:1rem;accent-color:var(--jv-color-primary);cursor:pointer}.jv-grid-boolean{display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;border-radius:999px;font-size:.75rem;font-weight:700}.jv-grid-boolean-true{background:var(--jv-color-success);color:#fff}.jv-grid-boolean-false{background:var(--jv-color-danger);color:#fff}.jv-grid-empty{padding:var(--jv-spacing-xl) var(--jv-spacing-md);text-align:center;color:var(--jv-color-foreground-muted);font-style:italic}.jv-grid-actions-group{display:flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-grid-loading{display:flex;align-items:center;justify-content:center;gap:var(--jv-spacing-md);padding:var(--jv-spacing-xl);color:var(--jv-color-foreground-muted)}.jv-grid-spinner{width:1.25rem;height:1.25rem;border:2px solid var(--jv-color-border);border-right-color:var(--jv-color-primary);border-radius:999px;animation:jv-grid-spin .8s linear infinite}@keyframes jv-grid-spin{to{transform:rotate(360deg)}}.jv-grid-pagination{display:flex;align-items:center;justify-content:space-between;gap:var(--jv-spacing-md);flex-wrap:wrap}.jv-grid-pagination-info{font-size:.875rem;color:var(--jv-color-foreground-muted)}.jv-grid-pagination-controls{display:flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-grid-resize-handle{position:absolute;top:0;right:0;width:4px;height:100%;cursor:col-resize;background:transparent;z-index:3}.jv-grid-resize-handle:hover,.jv-grid-resize-handle:active{background:var(--jv-color-primary)}.jv-grid-filter-row .jv-grid-th{padding:var(--jv-spacing-xs) var(--jv-spacing-sm)}.jv-grid-filter-input{width:100%;min-height:var(--jv-density-control-height);padding:0 var(--jv-spacing-sm);border:1px solid var(--jv-color-border);border-radius:var(--jv-radius-sm);background:var(--jv-color-surface);color:var(--jv-color-foreground);font-size:.8rem;box-sizing:border-box}.jv-grid-td-editable{cursor:pointer}.jv-grid-td-editable:hover{box-shadow:inset 0 0 0 1px var(--jv-color-primary)}.jv-grid-edit-input{width:100%;min-height:var(--jv-density-control-height);padding:0 var(--jv-spacing-sm);border:1px solid var(--jv-color-primary);border-radius:var(--jv-radius-sm);background:var(--jv-color-surface);color:var(--jv-color-foreground);font-size:var(--jv-density-font-size);box-sizing:border-box}.jv-grid-edit-input:focus{outline:2px solid var(--jv-color-primary);outline-offset:-1px}\n"] }]
21165
+ args: [{ selector: 'jv-grid', standalone: true, imports: [FormsModule, JvButtonComponent, JvIconComponent, JvInputComponent, JvSelectComponent], template: `
21166
+ <div class="jv-grid" role="region" [attr.aria-label]="resolvedOptions().ariaLabel || gridLabel()">
21167
+ @if (resolvedOptions().searchable || resolvedOptions().exportable) {
21168
+ <div class="jv-grid-toolbar">
21169
+ @if (resolvedOptions().searchable) {
21170
+ <div class="jv-grid-search">
21171
+ <jv-icon name="search" [size]="16" />
21172
+ <jv-input
21173
+ inputId="grid-search"
21174
+ [placeholder]="t('grid.searchPlaceholder')"
21175
+ [ngModel]="searchTerm()"
21176
+ (ngModelChange)="onSearchInput($event)"
21177
+ />
21178
+ </div>
21179
+ }
21180
+ @if (resolvedOptions().exportable) {
21181
+ <div class="jv-grid-export-actions">
21182
+ <jv-button variant="outline" icon="download" (click)="exportCsv()">{{ t('grid.exportCsv') }}</jv-button>
21183
+ <jv-button variant="outline" icon="file-spreadsheet" (click)="exportExcel()">{{ t('grid.exportExcel') }}</jv-button>
21184
+ </div>
21185
+ }
21186
+ </div>
21187
+ }
21188
+
21189
+ @if (resolvedOptions().loading) {
21190
+ <div class="jv-grid-loading" role="status" aria-live="polite">
21191
+ <span class="jv-grid-spinner" aria-hidden="true"></span>
21192
+ <span>{{ t('grid.loading') }}</span>
21193
+ </div>
21194
+ } @else {
21195
+ <div
21196
+ class="jv-grid-table-wrap"
21197
+ [class.jv-grid-virtual]="resolvedOptions().virtualScroll"
21198
+ #tableWrap
21199
+ (scroll)="onVirtualScroll($any($event.target))"
21200
+ >
21201
+ @if (resolvedOptions().virtualScroll) {
21202
+ <div class="jv-grid-virtual-spacer" [style.height.px]="virtualTotalHeight()"></div>
21203
+ }
21204
+ <table
21205
+ class="jv-grid-table"
21206
+ role="grid"
21207
+ [attr.aria-label]="resolvedOptions().ariaLabel || gridLabel()"
21208
+ [attr.aria-rowcount]="effectiveTotalItems()"
21209
+ [attr.aria-colcount]="visibleColumns().length"
21210
+ [class]="densityClass()"
21211
+ [style.position]="resolvedOptions().virtualScroll ? 'relative' : null"
21212
+ [style.top.px]="virtualOffsetY()"
21213
+ >
21214
+ <caption class="sr-only">{{ resolvedOptions().ariaLabel || gridLabel() }}</caption>
21215
+
21216
+ @let hasGroupedHeaders = groupedHeaders().length > 0;
21217
+
21218
+ <thead class="jv-grid-thead">
21219
+ @if (hasGroupedHeaders) {
21220
+ <tr>
21221
+ @if (resolvedOptions().selectable) {
21222
+ <th class="jv-grid-th jv-grid-th-select" scope="col" [attr.rowspan]="2">
21223
+ <input
21224
+ type="checkbox"
21225
+ class="jv-grid-checkbox"
21226
+ [checked]="allSelected()"
21227
+ (change)="toggleSelectAll($any($event.target).checked)"
21228
+ [attr.aria-label]="allSelected() ? t('grid.deselectAll') : t('grid.selectAll')"
21229
+ [indeterminate]="someSelected()"
21230
+ />
21231
+ </th>
21232
+ }
21233
+ @for (group of groupedHeaders(); track colKey(group)) {
21234
+ <th
21235
+ class="jv-grid-th"
21236
+ [class]="group.headerClass || ''"
21237
+ [class.jv-grid-th-sticky-left]="isStickyLeft(group)"
21238
+ [class.jv-grid-th-sticky-right]="isStickyRight(group)"
21239
+ [attr.colspan]="groupColspan(group)"
21240
+ scope="colgroup"
21241
+ [style.text-align]="group.align ?? 'start'"
21242
+ >
21243
+ <div class="jv-grid-th-content">{{ group.header }}</div>
21244
+ </th>
21245
+ }
21246
+ @if (actions().length > 0) {
21247
+ <th class="jv-grid-th jv-grid-th-actions" scope="col" [attr.rowspan]="2">
21248
+ <span class="sr-only">{{ t('grid.actionsLabel', { label: '' }) }}</span>
21249
+ </th>
21250
+ }
21251
+ </tr>
21252
+ }
21253
+
21254
+ <tr>
21255
+ @if (!hasGroupedHeaders && resolvedOptions().selectable) {
21256
+ <th class="jv-grid-th jv-grid-th-select" scope="col">
21257
+ <input
21258
+ type="checkbox"
21259
+ class="jv-grid-checkbox"
21260
+ [checked]="allSelected()"
21261
+ (change)="toggleSelectAll($any($event.target).checked)"
21262
+ [attr.aria-label]="allSelected() ? t('grid.deselectAll') : t('grid.selectAll')"
21263
+ [indeterminate]="someSelected()"
21264
+ />
21265
+ </th>
21266
+ }
21267
+ @for (col of visibleColumns(); track colKey(col); let colIdx = $index) {
21268
+ <th
21269
+ class="jv-grid-th"
21270
+ [class]="col.headerClass || ''"
21271
+ [class.jv-grid-th-sortable]="col.sortable !== false && resolvedOptions().sortable"
21272
+ [class.jv-grid-th-sticky-left]="isStickyLeft(col)"
21273
+ [class.jv-grid-th-sticky-right]="isStickyRight(col)"
21274
+ [class.jv-grid-th-reorderable]="resolvedOptions().reorderableColumns"
21275
+ [style.text-align]="col.align ?? 'start'"
21276
+ [style.width]="getColumnWidth(col)"
21277
+ [style.min-width]="col.minWidth || undefined"
21278
+ [style.max-width]="col.maxWidth || undefined"
21279
+ scope="col"
21280
+ [attr.aria-sort]="getAriaSort(col)"
21281
+ [attr.aria-label]="getSortLabel(col)"
21282
+ [tabindex]="col.sortable !== false && resolvedOptions().sortable ? 0 : -1"
21283
+ (click)="toggleSort(col)"
21284
+ (keydown.enter)="toggleSort(col)"
21285
+ (keydown.space)="toggleSort(col, $event)"
21286
+ draggable="{{ resolvedOptions().reorderableColumns ? 'true' : 'false' }}"
21287
+ (dragstart)="onDragStart($event, colIdx)"
21288
+ (dragover)="onDragOver($event, colIdx)"
21289
+ (drop)="onDrop($event, colIdx)"
21290
+ (dragend)="onDragEnd($event)"
21291
+ >
21292
+ <div class="jv-grid-th-content">
21293
+ <span>{{ col.header }}</span>
21294
+ @if (col.sortable !== false && resolvedOptions().sortable) {
21295
+ <span class="jv-grid-sort-icon" aria-hidden="true">
21296
+ @if (sortState().columnKey === colKey(col)) {
21297
+ @if (sortState().direction === 'asc') { ▲ }
21298
+ @if (sortState().direction === 'desc') { ▼ }
21299
+ }
21300
+ </span>
21301
+ }
21302
+ </div>
21303
+ @if (isColumnResizable(col)) {
21304
+ <div
21305
+ class="jv-grid-resize-handle"
21306
+ (mousedown)="onResizeStart($event, col, colIdx)"
21307
+ ></div>
21308
+ }
21309
+ </th>
21310
+ }
21311
+ @if (!hasGroupedHeaders && actions().length > 0) {
21312
+ <th class="jv-grid-th jv-grid-th-actions" scope="col">
21313
+ <span class="sr-only">{{ t('grid.actionsLabel', { label: '' }) }}</span>
21314
+ </th>
21315
+ }
21316
+ </tr>
21317
+
21318
+ @if (resolvedOptions().columnFilters) {
21319
+ <tr class="jv-grid-filter-row">
21320
+ @if (resolvedOptions().selectable) {
21321
+ <th class="jv-grid-th jv-grid-th-select"></th>
21322
+ }
21323
+ @for (col of visibleColumns(); track colKey(col)) {
21324
+ <th class="jv-grid-th jv-grid-th-filter">
21325
+ @if (col.filterable !== false) {
21326
+ <input
21327
+ class="jv-grid-filter-input"
21328
+ [placeholder]="t('grid.filterPlaceholder')"
21329
+ [attr.aria-label]="t('grid.filterForColumn', { column: col.header })"
21330
+ [ngModel]="colFilters()[colKey(col)]?.value ?? ''"
21331
+ (ngModelChange)="onColumnFilterChange(col, $event)"
21332
+ />
21333
+ }
21334
+ </th>
21335
+ }
21336
+ @if (actions().length > 0) {
21337
+ <th class="jv-grid-th jv-grid-th-actions"></th>
21338
+ }
21339
+ </tr>
21340
+ }
21341
+ </thead>
21342
+
21343
+ <tbody class="jv-grid-tbody">
21344
+ @if (displayData().length === 0 && !searchTerm().trim() && !hasActiveFilters()) {
21345
+ <tr>
21346
+ <td class="jv-grid-empty" [attr.colspan]="colspan()">
21347
+ {{ resolvedOptions().emptyMessage || t('grid.emptyMessage') }}
21348
+ </td>
21349
+ </tr>
21350
+ } @else if (displayData().length === 0 && (searchTerm().trim() || hasActiveFilters())) {
21351
+ <tr>
21352
+ <td class="jv-grid-empty" [attr.colspan]="colspan()">
21353
+ {{ resolvedOptions().noResultsMessage || t('grid.noResults') }}
21354
+ </td>
21355
+ </tr>
21356
+ } @else {
21357
+ @for (row of displayData(); track getRowId(row, $index); let rowIdx = $index) {
21358
+ <tr
21359
+ class="jv-grid-tr"
21360
+ role="row"
21361
+ [class.jv-grid-tr-editing]="isRowEditing(row)"
21362
+ [class.jv-grid-tr-selected]="isSelected(row)"
21363
+ [attr.aria-selected]="resolvedOptions().selectable ? isSelected(row) : null"
21364
+ (click)="onRowClick(row)"
21365
+ (dblclick)="onRowDblClick(row)"
21366
+ (keydown.enter)="onRowClick(row)"
21367
+ (keydown.space)="onRowClick($event, row)"
21368
+ [tabindex]="-1"
21369
+ >
21370
+ @if (resolvedOptions().selectable) {
21371
+ <td
21372
+ class="jv-grid-td jv-grid-td-select"
21373
+ [class.jv-grid-td-sticky-left]="resolvedOptions().stickyColumns"
21374
+ >
21375
+ <input
21376
+ type="checkbox"
21377
+ class="jv-grid-checkbox"
21378
+ [checked]="isSelected(row)"
21379
+ (change)="toggleRow(row); $event.stopPropagation()"
21380
+ (keydown.space)="$event.stopPropagation()"
21381
+ [attr.aria-label]="getRowLabel(row, $index)"
21382
+ />
21383
+ </td>
21384
+ }
21385
+ @for (col of visibleColumns(); track colKey(col)) {
21386
+ <td
21387
+ class="jv-grid-td"
21388
+ [class]="col.cellClass || ''"
21389
+ [class.jv-grid-td-sticky-left]="isStickyLeft(col)"
21390
+ [class.jv-grid-td-sticky-right]="isStickyRight(col)"
21391
+ [class.jv-grid-td-editable]="isCellEditable(col)"
21392
+ [style.text-align]="col.align ?? 'start'"
21393
+ (dblclick)="startEdit(row, col, $event)"
21394
+ (keydown.enter)="startEdit(row, col, $event)"
21395
+ [tabindex]="isCellEditable(col) ? -1 : null"
21396
+ >
21397
+ @if (isEditingCell(row, col)) {
21398
+ @if (col.editType === 'select' && col.editOptions) {
21399
+ <jv-select
21400
+ [options]="getEditOptions(col)"
21401
+ [modelValue]="getEditValue(row, col)"
21402
+ (selectionChange)="onEditValueChange(row, col, $event)"
21403
+ />
21404
+ } @else if (col.editType === 'boolean') {
21405
+ <input
21406
+ type="checkbox"
21407
+ class="jv-grid-checkbox"
21408
+ [checked]="getCellValue(row, col) === true"
21409
+ (change)="commitEdit(row, col, $any($event.target).checked)"
21410
+ [attr.aria-label]="col.header"
21411
+ />
21412
+ } @else {
21413
+ <input
21414
+ class="jv-grid-edit-input"
21415
+ [type]="col.editType === 'number' ? 'number' : 'text'"
21416
+ [value]="getEditValue(row, col)"
21417
+ (blur)="commitEdit(row, col, $any($event.target).value)"
21418
+ (keydown.enter)="commitEdit(row, col, $any($event.target).value)"
21419
+ (keydown.escape)="cancelEdit()"
21420
+ autofocus
21421
+ />
21422
+ }
21423
+ } @else {
21424
+ @let value = getCellValue(row, col);
21425
+ @if (col.type === 'boolean') {
21426
+ @if (value) {
21427
+ <span class="jv-grid-boolean jv-grid-boolean-true" aria-label="true">✓</span>
21428
+ } @else {
21429
+ <span class="jv-grid-boolean jv-grid-boolean-false" aria-label="false">✗</span>
21430
+ }
21431
+ } @else if (col.format) {
21432
+ {{ col.format(value, row) }}
21433
+ } @else {
21434
+ {{ formatValue(value, col) }}
21435
+ }
21436
+ }
21437
+ </td>
21438
+ }
21439
+ @if (actions().length > 0) {
21440
+ <td
21441
+ class="jv-grid-td jv-grid-td-actions"
21442
+ [class.jv-grid-td-sticky-right]="resolvedOptions().stickyColumns"
21443
+ >
21444
+ <div class="jv-grid-actions-group">
21445
+ @for (action of actions(); track action.id) {
21446
+ <jv-button
21447
+ [variant]="actionVariant(action.variant)"
21448
+ [icon]="action.icon ?? null"
21449
+ [disabled]="action.disabled?.(row) ?? false"
21450
+ [attr.aria-label]="action.label"
21451
+ [title]="action.label"
21452
+ (click)="onActionClick(action, row); $event.stopPropagation()"
21453
+ ></jv-button>
21454
+ }
21455
+ </div>
21456
+ </td>
21457
+ }
21458
+ </tr>
21459
+ }
21460
+ }
21461
+ </tbody>
21462
+ </table>
21463
+ </div>
21464
+ }
21465
+
21466
+ @if (resolvedOptions().pageable && effectiveTotalPages() > 1 && !resolvedOptions().loading) {
21467
+ <nav class="jv-grid-pagination" role="navigation" [attr.aria-label]="t('grid.paginationLabel', { label: '' })">
21468
+ <div class="jv-grid-pagination-info" aria-live="polite" aria-atomic="true">
21469
+ {{ t('grid.itemsShowing', { start: pageStart(), end: pageEnd(), total: effectiveTotalItems() }) }}
21470
+ </div>
21471
+ <div class="jv-grid-pagination-controls">
21472
+ <jv-button
21473
+ variant="outline"
21474
+ icon="chevrons-left"
21475
+ [disabled]="pageIndex() === 0"
21476
+ (click)="goToPage(0)"
21477
+ [attr.aria-label]="t('grid.pageFirst')"
21478
+ ></jv-button>
21479
+ <jv-button
21480
+ variant="outline"
21481
+ icon="chevron-left"
21482
+ [disabled]="pageIndex() === 0"
21483
+ (click)="goToPage(pageIndex() - 1)"
21484
+ [attr.aria-label]="t('grid.pagePrev')"
21485
+ ></jv-button>
21486
+ @for (p of pageRange(); track p) {
21487
+ <jv-button
21488
+ [variant]="p === pageIndex() ? 'primary' : 'outline'"
21489
+ [attr.aria-current]="p === pageIndex() ? 'page' : null"
21490
+ [attr.aria-label]="t('grid.pageOfTotal', { page: p + 1, total: effectiveTotalPages() })"
21491
+ (click)="goToPage(p)"
21492
+ >{{ p + 1 }}</jv-button>
21493
+ }
21494
+ <jv-button
21495
+ variant="outline"
21496
+ icon="chevron-right"
21497
+ [disabled]="pageIndex() >= effectiveTotalPages() - 1"
21498
+ (click)="goToPage(pageIndex() + 1)"
21499
+ [attr.aria-label]="t('grid.pageNext')"
21500
+ ></jv-button>
21501
+ <jv-button
21502
+ variant="outline"
21503
+ icon="chevrons-right"
21504
+ [disabled]="pageIndex() >= effectiveTotalPages() - 1"
21505
+ (click)="goToPage(effectiveTotalPages() - 1)"
21506
+ [attr.aria-label]="t('grid.pageLast')"
21507
+ ></jv-button>
21508
+ </div>
21509
+ </nav>
21510
+ }
21511
+ </div>
21512
+ `, styles: [".jv-grid{display:flex;flex-direction:column;gap:var(--jv-spacing-md)}.jv-grid-toolbar{display:flex;align-items:center;justify-content:space-between;gap:var(--jv-spacing-md);flex-wrap:wrap}.jv-grid-search{display:flex;align-items:center;gap:var(--jv-spacing-sm)}.jv-grid-export-actions{display:flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-grid-table-wrap{overflow-x:auto;overflow-y:auto;-webkit-overflow-scrolling:touch;border:1px solid var(--jv-color-border);border-radius:var(--jv-radius-md);position:relative}.jv-grid-virtual{max-height:600px}.jv-grid-virtual-spacer{pointer-events:none}.jv-grid-table{width:100%;border-collapse:collapse;background:var(--jv-color-surface)}.jv-grid-thead{background:var(--jv-color-surface-muted)}.jv-grid-th{padding:var(--jv-spacing-sm) var(--jv-spacing-md);font-weight:600;font-size:.875rem;color:var(--jv-color-foreground-muted);text-align:start;white-space:nowrap;border-bottom:1px solid var(--jv-color-border);-webkit-user-select:none;user-select:none;position:relative}.jv-grid-th-sortable{cursor:pointer}.jv-grid-th-sortable:hover{background:var(--jv-color-surface)}.jv-grid-th-reorderable{cursor:grab}.jv-grid-th-reorderable:active{cursor:grabbing}.jv-grid-th-sticky-left{position:sticky;left:0;z-index:2;background:var(--jv-color-surface-muted);border-right:1px solid var(--jv-color-border)}.jv-grid-th-sticky-right{position:sticky;right:0;z-index:2;background:var(--jv-color-surface-muted);border-left:1px solid var(--jv-color-border)}.jv-grid-th-content{display:flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-grid-sort-icon{font-size:.75rem;line-height:1}.jv-grid-th-select,.jv-grid-td-select{width:3rem;text-align:center}.jv-grid-th-actions,.jv-grid-td-actions{width:1%;white-space:nowrap}.jv-grid-td{padding:var(--jv-spacing-sm) var(--jv-spacing-md);color:var(--jv-color-foreground);font-size:var(--jv-density-font-size);border-bottom:1px solid var(--jv-color-border)}.jv-grid-td-sticky-left{position:sticky;left:0;z-index:1;background:var(--jv-color-surface);border-right:1px solid var(--jv-color-border)}.jv-grid-td-sticky-right{position:sticky;right:0;z-index:1;background:var(--jv-color-surface);border-left:1px solid var(--jv-color-border)}.jv-grid-t:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:-2px}.jv-grid-tr{transition:background-color .12s ease}.jv-grid-tr:hover{background:var(--jv-color-surface-muted)}.jv-grid-tr:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:-2px}.jv-grid-tr-editing{background:var(--jv-color-surface-highlight, rgba(59, 130, 246, .05))}.jv-grid-tr-selected{background:var(--jv-color-primary-light, rgba(59, 130, 246, .08))}.jv-grid-checkbox{width:1rem;height:1rem;accent-color:var(--jv-color-primary);cursor:pointer}.jv-grid-boolean{display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;border-radius:999px;font-size:.75rem;font-weight:700}.jv-grid-boolean-true{background:var(--jv-color-success);color:#fff}.jv-grid-boolean-false{background:var(--jv-color-danger);color:#fff}.jv-grid-empty{padding:var(--jv-spacing-xl) var(--jv-spacing-md);text-align:center;color:var(--jv-color-foreground-muted);font-style:italic}.jv-grid-actions-group{display:flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-grid-loading{display:flex;align-items:center;justify-content:center;gap:var(--jv-spacing-md);padding:var(--jv-spacing-xl);color:var(--jv-color-foreground-muted)}.jv-grid-spinner{width:1.25rem;height:1.25rem;border:2px solid var(--jv-color-border);border-right-color:var(--jv-color-primary);border-radius:999px;animation:jv-grid-spin .8s linear infinite}@keyframes jv-grid-spin{to{transform:rotate(360deg)}}.jv-grid-pagination{display:flex;align-items:center;justify-content:space-between;gap:var(--jv-spacing-md);flex-wrap:wrap}.jv-grid-pagination-info{font-size:.875rem;color:var(--jv-color-foreground-muted)}.jv-grid-pagination-controls{display:flex;align-items:center;gap:var(--jv-spacing-xs);flex-wrap:wrap}@media(max-width:640px){.jv-grid-pagination{flex-direction:column;align-items:stretch;gap:var(--jv-spacing-sm)}.jv-grid-pagination-info{text-align:center}.jv-grid-pagination-controls{justify-content:center}.jv-grid-toolbar{flex-direction:column;align-items:stretch}.jv-grid-search{width:100%}.jv-grid-export-actions{justify-content:stretch}.jv-grid-export-actions jv-button{flex:1}}.jv-grid-resize-handle{position:absolute;top:0;right:0;width:4px;height:100%;cursor:col-resize;background:transparent;z-index:3}.jv-grid-resize-handle:hover,.jv-grid-resize-handle:active{background:var(--jv-color-primary)}.jv-grid-filter-row .jv-grid-th{padding:var(--jv-spacing-xs) var(--jv-spacing-sm)}.jv-grid-filter-input{width:100%;min-height:var(--jv-density-control-height);padding:0 var(--jv-spacing-sm);border:1px solid var(--jv-color-border);border-radius:var(--jv-radius-sm);background:var(--jv-color-surface);color:var(--jv-color-foreground);font-size:.8rem;box-sizing:border-box}.jv-grid-td-editable{cursor:pointer}.jv-grid-td-editable:hover{box-shadow:inset 0 0 0 1px var(--jv-color-primary)}.jv-grid-edit-input{width:100%;min-height:var(--jv-density-control-height);padding:0 var(--jv-spacing-sm);border:1px solid var(--jv-color-primary);border-radius:var(--jv-radius-sm);background:var(--jv-color-surface);color:var(--jv-color-foreground);font-size:var(--jv-density-font-size);box-sizing:border-box}.jv-grid-edit-input:focus{outline:2px solid var(--jv-color-primary);outline-offset:-1px}\n"] }]
21400
21513
  }], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], actions: [{ type: i0.Input, args: [{ isSignal: true, alias: "actions", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], trackBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackBy", required: false }] }], selectedIds: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedIds", required: false }] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }], rowDoubleClick: [{ type: i0.Output, args: ["rowDoubleClick"] }], actionClick: [{ type: i0.Output, args: ["actionClick"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], pageChange: [{ type: i0.Output, args: ["pageChange"] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }], sortChange: [{ type: i0.Output, args: ["sortChange"] }], columnFilter: [{ type: i0.Output, args: ["columnFilter"] }], columnResize: [{ type: i0.Output, args: ["columnResize"] }], columnReorder: [{ type: i0.Output, args: ["columnReorder"] }], rowEdit: [{ type: i0.Output, args: ["rowEdit"] }] } });
21401
21514
 
21402
21515
  class JvPaginationComponent {
@@ -21567,5 +21680,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
21567
21680
  * Generated bundle index. Do not edit.
21568
21681
  */
21569
21682
 
21570
- 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, JvAlertComponent, JvAnnouncementService, JvBadgeComponent, JvBreadcrumbComponent, JvButtonComponent, JvButtonGroupComponent, JvCardComponent, JvChangePasswordPageComponent, JvCheckboxComponent, JvConfirmDialogComponent, JvDashboardShellComponent, JvDialogComponent, JvDialogService, JvDividerComponent, JvForgotPasswordPageComponent, JvFormContainerComponent, JvGridComponent, JvIconButtonComponent, JvIconComponent, JvInputComponent, JvLoaderComponent, JvLoaderService, JvLoginPageComponent, JvPageComponent, JvPaginationComponent, JvRadioComponent, JvSectionComponent, JvSelectComponent, JvSidebarComponent, JvSwitchComponent, JvThemeService, JvToastComponent, JvToastService, JvTopbarComponent, JvTranslationService, provideJvUi };
21683
+ 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, JvAlertComponent, JvAnnouncementService, JvBadgeComponent, JvBreadcrumbComponent, JvButtonComponent, JvButtonGroupComponent, JvCardComponent, JvChangePasswordPageComponent, JvCheckboxComponent, JvConfirmDialogComponent, JvDashboardShellComponent, JvDialogComponent, JvDialogService, JvDividerComponent, JvForgotPasswordPageComponent, JvFormContainerComponent, JvGridComponent, JvIconButtonComponent, JvIconComponent, JvInputComponent, JvLoaderComponent, JvLoaderService, JvLoginPageComponent, JvPageComponent, JvPaginationComponent, JvRadioComponent, JvSectionComponent, JvSelectComponent, JvSidebarComponent, JvSwitchComponent, JvTextareaComponent, JvThemeService, JvToastComponent, JvToastService, JvTopbarComponent, JvTranslationService, provideJvUi };
21571
21684
  //# sourceMappingURL=devjuliovilla-jv-ui.mjs.map