osl-base-extended 9.2.0 → 9.4.0
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.
|
@@ -10,6 +10,8 @@ import * as i2 from '@angular/material/button';
|
|
|
10
10
|
import { MatIconButton, MatButtonModule } from '@angular/material/button';
|
|
11
11
|
import * as i3 from '@angular/material/icon';
|
|
12
12
|
import { MatIconModule } from '@angular/material/icon';
|
|
13
|
+
import * as i3$1 from '@angular/cdk/drag-drop';
|
|
14
|
+
import { DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop';
|
|
13
15
|
import { HttpHeaders, HttpParams, HttpClient } from '@angular/common/http';
|
|
14
16
|
import { firstValueFrom, timeout, BehaviorSubject, debounceTime, distinctUntilChanged, from, of } from 'rxjs';
|
|
15
17
|
import * as i1$2 from '@angular/forms';
|
|
@@ -26,8 +28,6 @@ import { NgxMatDatetimepicker, NgxMatDatepickerInput } from '@ngxmc/datetime-pic
|
|
|
26
28
|
import { MatInputModule } from '@angular/material/input';
|
|
27
29
|
import { DomSanitizer } from '@angular/platform-browser';
|
|
28
30
|
import { debounceTime as debounceTime$1, distinctUntilChanged as distinctUntilChanged$1, switchMap, tap } from 'rxjs/operators';
|
|
29
|
-
import * as i4$1 from '@angular/cdk/drag-drop';
|
|
30
|
-
import { moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop';
|
|
31
31
|
import { MatMenuModule } from '@angular/material/menu';
|
|
32
32
|
import { TemplatePortal, PortalModule } from '@angular/cdk/portal';
|
|
33
33
|
import * as i1$3 from '@angular/cdk/overlay';
|
|
@@ -42,6 +42,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
42
42
|
args: [{ selector: 'lib-osl-base-extended', imports: [], template: ` <p>osl-base-extended works!</p> ` }]
|
|
43
43
|
}] });
|
|
44
44
|
|
|
45
|
+
// Minimum sliver (px) of the dialog that must stay on-screen on every edge while dragging.
|
|
46
|
+
const DRAG_MIN_VISIBLE = 60;
|
|
45
47
|
class DialogWrapper {
|
|
46
48
|
dialogRef;
|
|
47
49
|
data;
|
|
@@ -56,13 +58,24 @@ class DialogWrapper {
|
|
|
56
58
|
this.dialogData.data = data.data;
|
|
57
59
|
this.dialogData.dialogRef = dialogRef;
|
|
58
60
|
this.dialogData.component = data.component;
|
|
61
|
+
this.dialogData.isDraggable = data.isDraggable;
|
|
59
62
|
}
|
|
63
|
+
// Lets the dialog be dragged past the viewport edges but keeps at least
|
|
64
|
+
// DRAG_MIN_VISIBLE px of it on-screen so it can never be fully lost off-screen.
|
|
65
|
+
dragConstrainPosition = (point, _dragRef, dimensions, pickupPositionInElement) => {
|
|
66
|
+
const x = point.x - pickupPositionInElement.x;
|
|
67
|
+
const y = point.y - pickupPositionInElement.y;
|
|
68
|
+
return {
|
|
69
|
+
x: Math.min(Math.max(x, DRAG_MIN_VISIBLE - dimensions.width), window.innerWidth - DRAG_MIN_VISIBLE),
|
|
70
|
+
y: Math.min(Math.max(y, DRAG_MIN_VISIBLE - dimensions.height), window.innerHeight - DRAG_MIN_VISIBLE),
|
|
71
|
+
};
|
|
72
|
+
};
|
|
60
73
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DialogWrapper, deps: [{ token: i1.MatDialogRef }, { token: MAT_DIALOG_DATA }], target: i0.ɵɵFactoryTarget.Component });
|
|
61
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: DialogWrapper, isStandalone: true, selector: "dialog-wrapper", ngImport: i0, template: "\r\n@if(dialogData.component){\r\n <ng-container [ngComponentOutlet]=\"dialogData.component\" [ngComponentOutletInputs]=\"{ data: dialogData }\"></ng-container>\r\n}\r\n@else{\r\n <div
|
|
74
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: DialogWrapper, isStandalone: true, selector: "dialog-wrapper", ngImport: i0, template: "\r\n@if(dialogData.component){\r\n <ng-container [ngComponentOutlet]=\"dialogData.component\" [ngComponentOutletInputs]=\"{ data: dialogData }\"></ng-container>\r\n}\r\n@else{\r\n <div\r\n class=\"dialog-container\"\r\n cdkDrag\r\n cdkDragRootElement=\".cdk-overlay-pane\"\r\n [cdkDragConstrainPosition]=\"dragConstrainPosition\"\r\n [cdkDragDisabled]=\"!dialogData.isDraggable\">\r\n\r\n <!-- Header -->\r\n @if(typeof dialogData.header == 'string') {\r\n <div class=\"dialog-header\" [class.draggable]=\"dialogData.isDraggable\" cdkDragHandle [cdkDragHandleDisabled]=\"!dialogData.isDraggable\" mat-dialog-title>\r\n <div class=\"dialog-header-left\">\r\n <div class=\"dialog-icon-ring\">\r\n <mat-icon class=\"dialog-icon\">\r\n {{ dialogData.header.startsWith('Add') ? 'add_circle' : dialogData.header.startsWith('Edit') ? 'edit' : 'info' }}\r\n </mat-icon>\r\n </div>\r\n <div class=\"dialog-title-group\">\r\n <span class=\"dialog-mode-label\">\r\n {{ dialogData.header.startsWith('Add') ? 'Creating New' : dialogData.header.startsWith('Edit') ? 'Edit' : '' }}\r\n </span>\r\n <span class=\"dialog-heading\"> {{ dialogData.header.startsWith('Add') ? dialogData.header.replace('Add','') : dialogData.header.startsWith('Edit') ? dialogData.header.replace('Edit','') : '' }}</span>\r\n </div>\r\n </div>\r\n @if(dialogData.isDraggable) {\r\n <div class=\"dialog-drag-handle\" title=\"Drag to move\">\r\n <mat-icon>drag_indicator</mat-icon>\r\n </div>\r\n }\r\n @if(dialogData.partialHeader) {\r\n\r\n <ng-container *ngTemplateOutlet=\"dialogData.partialHeader; context: { $implicit: dialogData }\"></ng-container>\r\n }\r\n <button mat-icon-button class=\"dialog-close-btn\" mat-dialog-close>\r\n <mat-icon>close</mat-icon>\r\n </button>\r\n\r\n </div>\r\n } @else {\r\n <ng-container *ngTemplateOutlet=\"dialogData.header; context: { $implicit: dialogData }\"></ng-container>\r\n }\r\n \r\n <!-- Body -->\r\n <mat-dialog-content class=\"dialog-content\">\r\n @if(dialogData.formBody) {\r\n <div class=\"dialog-body\">\r\n <ng-container *ngTemplateOutlet=\"dialogData.formBody; context: { $implicit: dialogData }\"></ng-container>\r\n </div>\r\n }\r\n </mat-dialog-content>\r\n \r\n <!-- Footer -->\r\n @if(dialogData.formFooter) {\r\n <mat-dialog-actions class=\"dialog-actions\" [class.draggable]=\"dialogData.isDraggable\" cdkDragHandle [cdkDragHandleDisabled]=\"!dialogData.isDraggable\">\r\n <div class=\"dialog-footer\">\r\n <ng-container *ngTemplateOutlet=\"dialogData.formFooter; context: { $implicit: dialogData }\"></ng-container>\r\n </div>\r\n </mat-dialog-actions>\r\n }\r\n \r\n </div>\r\n\r\n}\r\n", styles: [".dialog-container{display:flex;flex-direction:column;overflow:hidden}.dialog-header{position:relative;display:flex;align-items:center;justify-content:space-between;padding:20px 20px 16px;background:linear-gradient(135deg,#f0f6ff,#e8f0fe);border-bottom:1px solid rgba(37,99,235,.12);gap:12px}.dialog-header:before{display:none}.dialog-header.draggable{cursor:move}.dialog-drag-handle{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);display:flex;align-items:center;justify-content:center;color:#9ca3af;cursor:move;pointer-events:none}.dialog-drag-handle .mat-icon{font-size:20px;width:20px;height:20px}.dialog-header-left{display:flex;align-items:center;gap:14px;flex:1;min-width:0}.dialog-icon-ring{flex-shrink:0;width:48px;height:48px;border-radius:12px;background:linear-gradient(135deg,var(--osl-primary, #2563eb),#3b82f6);display:flex;align-items:center;justify-content:center;box-shadow:0 4px 12px #2563eb4d;animation:icon-pop .25s cubic-bezier(.34,1.56,.64,1)}.dialog-icon{color:#fff;font-size:24px;width:24px;height:24px}.dialog-title-group{display:flex;flex-direction:column;min-width:0}.dialog-mode-label{font-size:11px;font-weight:500;color:var(--osl-primary, #2563eb);text-transform:uppercase;letter-spacing:.6px;line-height:1.2}.dialog-heading{font-size:18px;font-weight:600;color:#111827;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dialog-close-btn{flex-shrink:0;color:#6b7280;transition:color .15s ease,background .15s ease}.dialog-close-btn:hover{color:#111827;background:#0000000f}.dialog-close-btn .mat-icon{font-size:20px;width:20px;height:20px}.dialog-content{padding:0!important;max-height:65vh;overflow-y:auto}.dialog-content::-webkit-scrollbar{width:5px}.dialog-content::-webkit-scrollbar-track{background:transparent}.dialog-content::-webkit-scrollbar-thumb{background:#2563eb33;border-radius:4px}.dialog-body{padding:20px 24px}.dialog-actions{padding:0!important;min-height:unset!important;border-top:1px solid #e5e7eb;background:#fafafa}.dialog-actions.draggable{cursor:move}.dialog-footer{width:100%;padding:14px 24px}@keyframes icon-pop{0%{transform:scale(.6);opacity:0}to{transform:scale(1);opacity:1}}.mat-mdc-dialog-title:before{display:none}\n"], dependencies: [{ kind: "ngmodule", type: MatDialogModule }, { kind: "directive", type: i1.MatDialogClose, selector: "[mat-dialog-close], [matDialogClose]", inputs: ["aria-label", "type", "mat-dialog-close", "matDialogClose"], exportAs: ["matDialogClose"] }, { kind: "directive", type: i1.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i1.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i1.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i3$1.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i3$1.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }] });
|
|
62
75
|
}
|
|
63
76
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DialogWrapper, decorators: [{
|
|
64
77
|
type: Component,
|
|
65
|
-
args: [{ selector: 'dialog-wrapper', imports: [MatDialogModule, MatIconModule, NgTemplateOutlet, MatIconButton, NgComponentOutlet], template: "\r\n@if(dialogData.component){\r\n <ng-container [ngComponentOutlet]=\"dialogData.component\" [ngComponentOutletInputs]=\"{ data: dialogData }\"></ng-container>\r\n}\r\n@else{\r\n <div
|
|
78
|
+
args: [{ selector: 'dialog-wrapper', imports: [MatDialogModule, MatIconModule, NgTemplateOutlet, MatIconButton, NgComponentOutlet, DragDropModule], template: "\r\n@if(dialogData.component){\r\n <ng-container [ngComponentOutlet]=\"dialogData.component\" [ngComponentOutletInputs]=\"{ data: dialogData }\"></ng-container>\r\n}\r\n@else{\r\n <div\r\n class=\"dialog-container\"\r\n cdkDrag\r\n cdkDragRootElement=\".cdk-overlay-pane\"\r\n [cdkDragConstrainPosition]=\"dragConstrainPosition\"\r\n [cdkDragDisabled]=\"!dialogData.isDraggable\">\r\n\r\n <!-- Header -->\r\n @if(typeof dialogData.header == 'string') {\r\n <div class=\"dialog-header\" [class.draggable]=\"dialogData.isDraggable\" cdkDragHandle [cdkDragHandleDisabled]=\"!dialogData.isDraggable\" mat-dialog-title>\r\n <div class=\"dialog-header-left\">\r\n <div class=\"dialog-icon-ring\">\r\n <mat-icon class=\"dialog-icon\">\r\n {{ dialogData.header.startsWith('Add') ? 'add_circle' : dialogData.header.startsWith('Edit') ? 'edit' : 'info' }}\r\n </mat-icon>\r\n </div>\r\n <div class=\"dialog-title-group\">\r\n <span class=\"dialog-mode-label\">\r\n {{ dialogData.header.startsWith('Add') ? 'Creating New' : dialogData.header.startsWith('Edit') ? 'Edit' : '' }}\r\n </span>\r\n <span class=\"dialog-heading\"> {{ dialogData.header.startsWith('Add') ? dialogData.header.replace('Add','') : dialogData.header.startsWith('Edit') ? dialogData.header.replace('Edit','') : '' }}</span>\r\n </div>\r\n </div>\r\n @if(dialogData.isDraggable) {\r\n <div class=\"dialog-drag-handle\" title=\"Drag to move\">\r\n <mat-icon>drag_indicator</mat-icon>\r\n </div>\r\n }\r\n @if(dialogData.partialHeader) {\r\n\r\n <ng-container *ngTemplateOutlet=\"dialogData.partialHeader; context: { $implicit: dialogData }\"></ng-container>\r\n }\r\n <button mat-icon-button class=\"dialog-close-btn\" mat-dialog-close>\r\n <mat-icon>close</mat-icon>\r\n </button>\r\n\r\n </div>\r\n } @else {\r\n <ng-container *ngTemplateOutlet=\"dialogData.header; context: { $implicit: dialogData }\"></ng-container>\r\n }\r\n \r\n <!-- Body -->\r\n <mat-dialog-content class=\"dialog-content\">\r\n @if(dialogData.formBody) {\r\n <div class=\"dialog-body\">\r\n <ng-container *ngTemplateOutlet=\"dialogData.formBody; context: { $implicit: dialogData }\"></ng-container>\r\n </div>\r\n }\r\n </mat-dialog-content>\r\n \r\n <!-- Footer -->\r\n @if(dialogData.formFooter) {\r\n <mat-dialog-actions class=\"dialog-actions\" [class.draggable]=\"dialogData.isDraggable\" cdkDragHandle [cdkDragHandleDisabled]=\"!dialogData.isDraggable\">\r\n <div class=\"dialog-footer\">\r\n <ng-container *ngTemplateOutlet=\"dialogData.formFooter; context: { $implicit: dialogData }\"></ng-container>\r\n </div>\r\n </mat-dialog-actions>\r\n }\r\n \r\n </div>\r\n\r\n}\r\n", styles: [".dialog-container{display:flex;flex-direction:column;overflow:hidden}.dialog-header{position:relative;display:flex;align-items:center;justify-content:space-between;padding:20px 20px 16px;background:linear-gradient(135deg,#f0f6ff,#e8f0fe);border-bottom:1px solid rgba(37,99,235,.12);gap:12px}.dialog-header:before{display:none}.dialog-header.draggable{cursor:move}.dialog-drag-handle{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);display:flex;align-items:center;justify-content:center;color:#9ca3af;cursor:move;pointer-events:none}.dialog-drag-handle .mat-icon{font-size:20px;width:20px;height:20px}.dialog-header-left{display:flex;align-items:center;gap:14px;flex:1;min-width:0}.dialog-icon-ring{flex-shrink:0;width:48px;height:48px;border-radius:12px;background:linear-gradient(135deg,var(--osl-primary, #2563eb),#3b82f6);display:flex;align-items:center;justify-content:center;box-shadow:0 4px 12px #2563eb4d;animation:icon-pop .25s cubic-bezier(.34,1.56,.64,1)}.dialog-icon{color:#fff;font-size:24px;width:24px;height:24px}.dialog-title-group{display:flex;flex-direction:column;min-width:0}.dialog-mode-label{font-size:11px;font-weight:500;color:var(--osl-primary, #2563eb);text-transform:uppercase;letter-spacing:.6px;line-height:1.2}.dialog-heading{font-size:18px;font-weight:600;color:#111827;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dialog-close-btn{flex-shrink:0;color:#6b7280;transition:color .15s ease,background .15s ease}.dialog-close-btn:hover{color:#111827;background:#0000000f}.dialog-close-btn .mat-icon{font-size:20px;width:20px;height:20px}.dialog-content{padding:0!important;max-height:65vh;overflow-y:auto}.dialog-content::-webkit-scrollbar{width:5px}.dialog-content::-webkit-scrollbar-track{background:transparent}.dialog-content::-webkit-scrollbar-thumb{background:#2563eb33;border-radius:4px}.dialog-body{padding:20px 24px}.dialog-actions{padding:0!important;min-height:unset!important;border-top:1px solid #e5e7eb;background:#fafafa}.dialog-actions.draggable{cursor:move}.dialog-footer{width:100%;padding:14px 24px}@keyframes icon-pop{0%{transform:scale(.6);opacity:0}to{transform:scale(1);opacity:1}}.mat-mdc-dialog-title:before{display:none}\n"] }]
|
|
66
79
|
}], ctorParameters: () => [{ type: i1.MatDialogRef }, { type: Dialog, decorators: [{
|
|
67
80
|
type: Inject,
|
|
68
81
|
args: [MAT_DIALOG_DATA]
|
|
@@ -75,6 +88,7 @@ class Dialog {
|
|
|
75
88
|
data;
|
|
76
89
|
component;
|
|
77
90
|
dialogRef;
|
|
91
|
+
isDraggable = false;
|
|
78
92
|
}
|
|
79
93
|
|
|
80
94
|
class DeleteConfirmation {
|
|
@@ -186,13 +200,14 @@ class baseComponent {
|
|
|
186
200
|
duration: 3000
|
|
187
201
|
});
|
|
188
202
|
}
|
|
189
|
-
openDialog(header, formBody, formFooter, width = '40vw', data, component) {
|
|
203
|
+
openDialog(header, formBody, formFooter, width = '40vw', data, component, isDraggable = false) {
|
|
190
204
|
const dialogData = {
|
|
191
205
|
header: header,
|
|
192
206
|
formBody: formBody,
|
|
193
207
|
formFooter: formFooter,
|
|
194
208
|
data: data,
|
|
195
|
-
component: component
|
|
209
|
+
component: component,
|
|
210
|
+
isDraggable: isDraggable
|
|
196
211
|
};
|
|
197
212
|
const dialogRef = this._injector.get(MatDialog)?.open(DialogWrapper, {
|
|
198
213
|
width: width,
|
|
@@ -3582,6 +3597,7 @@ class OslSetup {
|
|
|
3582
3597
|
totalRecords = 0;
|
|
3583
3598
|
loading = false;
|
|
3584
3599
|
dialogWidth = '50vw';
|
|
3600
|
+
isDraggable = false;
|
|
3585
3601
|
formElements = [];
|
|
3586
3602
|
beforeDisplay;
|
|
3587
3603
|
onAddEditFn;
|
|
@@ -4004,6 +4020,7 @@ class OslSetup {
|
|
|
4004
4020
|
partialHeader: this.partialCustomHeaderTemp,
|
|
4005
4021
|
formBody: this.formBodyTpl,
|
|
4006
4022
|
formFooter: this.customFormFooter ? this.customFooterWrapperTpl : this.formFooterTpl,
|
|
4023
|
+
isDraggable: this.isDraggable,
|
|
4007
4024
|
},
|
|
4008
4025
|
});
|
|
4009
4026
|
this._dialogRef.afterClosed().subscribe(() => {
|
|
@@ -4011,7 +4028,7 @@ class OslSetup {
|
|
|
4011
4028
|
});
|
|
4012
4029
|
}
|
|
4013
4030
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: OslSetup, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4014
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: OslSetup, isStandalone: false, selector: "osl-setup", inputs: { title: "title", titleIcon: "titleIcon", columns: "columns", datasource: "datasource", isPaginated: "isPaginated", pageSize: "pageSize", autoMode: "autoMode", tableHeight: "tableHeight", totalRecords: "totalRecords", loading: "loading", dialogWidth: "dialogWidth", formElements: "formElements", beforeDisplay: "beforeDisplay", onAddEditFn: "onAddEditFn", isLister: "isLister", canAdd: "canAdd", canEdit: "canEdit", canDelete: "canDelete", moreMenuActions: "moreMenuActions", customFormFooter: "customFormFooter", customHeaderTemp: "customHeaderTemp", partialCustomHeaderTemp: "partialCustomHeaderTemp", stateKey: "stateKey", primaryKey: "primaryKey", onSave: "onSave", cardPageSize: "cardPageSize", cardTemplate: "cardTemplate", cardCol: "cardCol" }, outputs: { onSearch: "onSearch", onAdd: "onAdd", onEdit: "onEdit", onDelete: "onDelete", pageChange: "pageChange", pageSizeChange: "pageSizeChange", sortChange: "sortChange", onRowClick: "onRowClick", onStateRestored: "onStateRestored" }, host: { listeners: { "document:click": "onDocumentClick()" } }, viewQueries: [{ propertyName: "formBodyTpl", first: true, predicate: ["formBodyTpl"], descendants: true }, { propertyName: "formFooterTpl", first: true, predicate: ["formFooterTpl"], descendants: true }, { propertyName: "customFooterWrapperTpl", first: true, predicate: ["customFooterWrapperTpl"], descendants: true }, { propertyName: "searchbar", first: true, predicate: ["searchbar"], descendants: true }, { propertyName: "gridRef", first: true, predicate: ["gridRef"], descendants: true }, { propertyName: "cardGridRef", first: true, predicate: ["cardGridRef"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"p-2\">\n\n <!-- \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"osl-setup-header\">\n <div class=\"d-flex align-items-center\">\n @if(titleIcon){\n\n <span class=\"title-icon mx-2 mb-1\" [innerHTML]=\"renderIcon(titleIcon)\"></span>\n }\n <h5 class=\"mb-0\">{{ title }}</h5>\n\n </div>\n <div class=\"d-flex align-items-center gap-2\">\n <osl-searchbar #searchbar class=\"mx-2\" (onSearch)=\"onSearchSetup($event)\"></osl-searchbar>\n\n <!-- View Toggle -->\n @if(!isLister){\n <div class=\"osl-view-toggle\">\n <button class=\"osl-view-toggle-btn\" [class.osl-view-toggle-btn--active]=\"viewMode === 'table'\"\n (click)=\"toggleView('table')\" title=\"Table view\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/>\n <line x1=\"3\" y1=\"9\" x2=\"21\" y2=\"9\"/>\n <line x1=\"3\" y1=\"15\" x2=\"21\" y2=\"15\"/>\n <line x1=\"9\" y1=\"9\" x2=\"9\" y2=\"21\"/>\n </svg>\n </button>\n <button class=\"osl-view-toggle-btn\" [class.osl-view-toggle-btn--active]=\"viewMode === 'card'\"\n (click)=\"toggleView('card')\" title=\"Card view\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"3\" y=\"3\" width=\"7\" height=\"7\" rx=\"1.5\"/>\n <rect x=\"14\" y=\"3\" width=\"7\" height=\"7\" rx=\"1.5\"/>\n <rect x=\"3\" y=\"14\" width=\"7\" height=\"7\" rx=\"1.5\"/>\n <rect x=\"14\" y=\"14\" width=\"7\" height=\"7\" rx=\"1.5\"/>\n </svg>\n </button>\n </div>\n }\n\n @if (!isLister && canAdd) {\n <osl-button variant=\"secondary\" size=\"sm\" [label]=\"'Add ' + title\" (clickEv)=\"openAddDialog()\"></osl-button>\n }\n </div>\n </div>\n\n <!-- \u2500\u2500 Table View \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (viewMode === 'table') {\n <div class=\"osl-setup-body my-2\">\n <osl-grid\n #gridRef\n [columns]=\"columnsWithActions\"\n [(datasource)]=\"datasource\"\n [isPaginated]=\"isPaginated\"\n [pageSize]=\"pageSize\"\n [autoMode]=\"autoMode\"\n [tableHeight]=\"tableHeight\"\n [totalRecords]=\"totalRecords\"\n [loading]=\"loading\"\n [moreMenuActions]=\"moreMenuActions\"\n [canEdit]=\"canEdit\"\n [canDelete]=\"canDelete\"\n [highlightedRow]=\"restoredRow\"\n [primaryKey]=\"primaryKey\"\n (editClick)=\"openEditDialog($event)\"\n (deleteClick)=\"onDeleteClick($event)\"\n (pageChange)=\"onPageChange(pageChange, $event)\"\n (pageSizeChange)=\"onPageChange(pageSizeChange, $event)\"\n (sortChange)=\"sortChange.emit($event)\"\n [isSelectable]=\"isLister\"\n (onRowClick)=\"onRowClick.emit($event)\"\n />\n </div>\n }\n\n <!-- \u2500\u2500 Card View \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (viewMode === 'card') {\n\n <!-- Skeleton on initial load -->\n @if (loading && cardPagedData.length === 0) {\n <div class=\"osl-card-grid my-2\">\n @for (sk of skeletonCardRows; track $index) {\n <div class=\"osl-card osl-card--skeleton\">\n <div class=\"osl-card-header osl-card-header--skeleton\">\n <div class=\"osl-card-title-wrap\">\n <div class=\"osl-sk-line osl-sk-line--title\"></div>\n </div>\n </div>\n <div class=\"osl-card-body\">\n <div class=\"row g-0\">\n <div class=\"col-6 osl-card-field\"><div class=\"osl-sk-line osl-sk-line--label\"></div><div class=\"osl-sk-line\"></div></div>\n <div class=\"col-6 osl-card-field\"><div class=\"osl-sk-line osl-sk-line--label\"></div><div class=\"osl-sk-line osl-sk-line--short\"></div></div>\n <div class=\"col-6 osl-card-field\"><div class=\"osl-sk-line osl-sk-line--label\"></div><div class=\"osl-sk-line\"></div></div>\n <div class=\"col-6 osl-card-field\"><div class=\"osl-sk-line osl-sk-line--label\"></div><div class=\"osl-sk-line osl-sk-line--short\"></div></div>\n </div>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- Empty state -->\n @else if (cardPagedData.length === 0 && !loading) {\n <div class=\"osl-card-empty my-2\">\n <svg class=\"osl-card-empty-svg\" viewBox=\"0 0 180 160\" fill=\"none\">\n <ellipse cx=\"90\" cy=\"140\" rx=\"60\" ry=\"8\" fill=\"#f3f4f6\"/>\n <rect x=\"30\" y=\"30\" width=\"120\" height=\"96\" rx=\"10\" fill=\"#f9fafb\" stroke=\"#e5e7eb\" stroke-width=\"2\"/>\n <rect x=\"44\" y=\"48\" width=\"92\" height=\"10\" rx=\"5\" fill=\"#e5e7eb\"/>\n <rect x=\"44\" y=\"66\" width=\"72\" height=\"8\" rx=\"4\" fill=\"#f3f4f6\"/>\n <rect x=\"44\" y=\"82\" width=\"84\" height=\"8\" rx=\"4\" fill=\"#f3f4f6\"/>\n <rect x=\"44\" y=\"98\" width=\"56\" height=\"8\" rx=\"4\" fill=\"#f3f4f6\"/>\n <circle cx=\"140\" cy=\"108\" r=\"26\" fill=\"#fff\" stroke=\"#e5e7eb\" stroke-width=\"2\"/>\n <line x1=\"133\" y1=\"101\" x2=\"147\" y2=\"115\" stroke=\"#d1d5db\" stroke-width=\"2.5\" stroke-linecap=\"round\"/>\n <line x1=\"147\" y1=\"101\" x2=\"133\" y2=\"115\" stroke=\"#d1d5db\" stroke-width=\"2.5\" stroke-linecap=\"round\"/>\n </svg>\n <p class=\"osl-card-empty-title\">No records found</p>\n <p class=\"osl-card-empty-desc\">Try adjusting your search or filter criteria</p>\n </div>\n }\n\n <!-- Card grid -->\n @else {\n <div #cardGridRef class=\"osl-card-grid my-2\" [style.height]=\"tableHeight\">\n @for (row of cardPagedData; track row[primaryKey] ?? $index; let i = $index) {\n\n @if (cardTemplate) {\n <ng-container *ngTemplateOutlet=\"cardTemplate; context: { $implicit: row, index: i }\"></ng-container>\n } @else {\n <div class=\"osl-card my-2\"\n [class.osl-card--selectable]=\"isLister\"\n [class.osl-card--highlighted]=\"isHighlightedCard(row)\"\n (click)=\"isLister ? onRowClick.emit(row) : null\">\n\n <!-- Card Header -->\n <div class=\"osl-card-header\">\n <!-- <svg class=\"osl-card-watermark\" viewBox=\"0 0 120 64\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M10 42 L18 52 L102 52 L110 42 Z\"/>\n <rect x=\"20\" y=\"34\" width=\"80\" height=\"9\" rx=\"1\"/>\n <rect x=\"26\" y=\"20\" width=\"16\" height=\"15\" rx=\"1\"/>\n <rect x=\"46\" y=\"20\" width=\"16\" height=\"15\" rx=\"1\"/>\n <rect x=\"66\" y=\"20\" width=\"16\" height=\"15\" rx=\"1\"/>\n <rect x=\"85\" y=\"14\" width=\"15\" height=\"21\" rx=\"2\"/>\n <rect x=\"90.5\" y=\"7\" width=\"2\" height=\"9\" rx=\"1\"/>\n <path d=\"M4 57 Q18 53 32 57 Q46 61 60 57 Q74 53 88 57 Q102 61 116 57\" stroke=\"currentColor\" stroke-width=\"2\" fill=\"none\" stroke-linecap=\"round\"/>\n </svg> -->\n <div class=\"osl-card-title-wrap\">\n <span class=\"osl-card-title\"\n >\n @if (cardTitleColumn) {\n <span\n >\n @switch (cardTitleColumn.displayType) {\n @case ('date') { {{ row[cardTitleColumn.key] | date }} }\n @case ('datetime') { {{ row[cardTitleColumn.key] | date:'medium' }} }\n @case ('time') { {{ row[cardTitleColumn.key] | date:'shortTime' }} }\n @case ('customDateFormat') { {{ row[cardTitleColumn.key] | date:cardTitleColumn.customDateFormat }} }\n @default { {{ getCellValue(row, cardTitleColumn) }} }\n }\n </span>\n }\n </span>\n </div>\n @if (!isLister && ((hasForm && (canEdit || canDelete)) || (moreMenuActions.length > 0 && hasVisibleActions(row)))) {\n <div class=\"osl-card-actions\">\n @if (hasForm && canEdit) {\n <button class=\"osl-card-action-btn osl-card-action-btn--edit\"\n (click)=\"$event.stopPropagation(); openEditDialog(row)\"\n [oslTooltip]=\"'Edit'\" oslTooltipPosition=\"bottom\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7\"/>\n <path d=\"M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z\"/>\n </svg>\n </button>\n }\n @if (hasForm && canDelete) {\n <button class=\"osl-card-action-btn osl-card-action-btn--delete\"\n (click)=\"$event.stopPropagation(); onDeleteClick(row)\"\n [oslTooltip]=\"'Delete'\" oslTooltipPosition=\"bottom\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" xmlns=\"http://www.w3.org/2000/svg\">\n <polyline points=\"3 6 5 6 21 6\"/>\n <path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\"/>\n <line x1=\"10\" y1=\"11\" x2=\"10\" y2=\"17\"/><line x1=\"14\" y1=\"11\" x2=\"14\" y2=\"17\"/>\n </svg>\n </button>\n }\n @if (moreMenuActions.length > 0 && hasVisibleActions(row)) {\n <button class=\"osl-card-action-btn osl-card-action-btn--more\"\n (click)=\"toggleCardMenu(i, $event)\"\n [oslTooltip]=\"'More actions'\" oslTooltipPosition=\"bottom\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <circle cx=\"12\" cy=\"5\" r=\"2.2\"/>\n <circle cx=\"12\" cy=\"12\" r=\"2.2\"/>\n <circle cx=\"12\" cy=\"19\" r=\"2.2\"/>\n </svg>\n </button>\n }\n </div>\n }\n </div>\n\n <!-- Card Body -->\n @if (cardBodyColumns.length > 0) {\n <div class=\"osl-card-body\">\n <div class=\"row g-0\">\n @for (col of cardBodyColumns; track col.key) {\n <div [class]=\"'col-' + cardCol + ' osl-card-field'\">\n <span class=\"osl-card-label\">{{ col.label }}</span>\n <span class=\"osl-card-value\" [class.link]=\"col.displayType === 'link'\" (click)=\"col.click ? col.click(row, col) : null\">\n\n <span>\n @switch (col.displayType) {\n @case ('date') { {{ ((row[col.key] | date) || '--') }} }\n @case ('datetime') { {{ (row[col.key] | date:'medium') || '--' }} }\n @case ('time') { {{ (row[col.key] | date:'shortTime') || '--' }} }\n @case ('customDateFormat') { {{ (row[col.key] | date:col.customDateFormat) || '--' }} }\n @default { {{ (getCellValue(row, col) || '--') }} }\n }\n </span>\n </span>\n </div>\n }\n </div>\n </div>\n }\n\n </div>\n }\n }\n </div>\n }\n\n <!-- Card Paginator -->\n @if (isPaginated && _cardTotal > 0) {\n <div class=\"osl-grid-pagination\">\n <span class=\"osl-grid-pagination__info\">\n @if (loading) {\n Loading...\n } @else {\n {{ cardStartRecord }}\u2013{{ cardEndRecord }} of {{ _cardTotal }} records\n }\n </span>\n\n <div class=\"osl-grid-pagination__controls\">\n <button class=\"osl-grid-page-btn osl-grid-page-btn--nav\"\n (click)=\"cardGoToPage(1)\"\n [disabled]=\"cardCurrentPage === 1 || loading\" title=\"First page\">\u00AB</button>\n <button class=\"osl-grid-page-btn osl-grid-page-btn--nav\"\n (click)=\"cardGoToPage(cardCurrentPage - 1)\"\n [disabled]=\"cardCurrentPage === 1 || loading\" title=\"Previous page\">\u2039 Prev</button>\n\n @for (page of cardPageNumbers; track $index) {\n @if (page === -1) {\n <span class=\"osl-grid-page-ellipsis\">\u2026</span>\n } @else {\n <button class=\"osl-grid-page-btn osl-grid-page-btn--num\"\n [class.osl-grid-page-btn--active]=\"page === cardCurrentPage\"\n [disabled]=\"loading\"\n (click)=\"cardGoToPage(page)\">{{ page }}</button>\n }\n }\n\n <button class=\"osl-grid-page-btn osl-grid-page-btn--nav\"\n (click)=\"cardGoToPage(cardCurrentPage + 1)\"\n [disabled]=\"cardCurrentPage === cardTotalPages || loading\" title=\"Next page\">Next \u203A</button>\n <button class=\"osl-grid-page-btn osl-grid-page-btn--nav\"\n (click)=\"cardGoToPage(cardTotalPages)\"\n [disabled]=\"cardCurrentPage === cardTotalPages || loading\" title=\"Last page\">\u00BB</button>\n </div>\n\n <div class=\"osl-grid-pagination__size\">\n <select class=\"osl-grid-page-size\"\n [ngModel]=\"_effectiveCardPageSize\"\n (ngModelChange)=\"cardOnPageSizeChange($event)\"\n [disabled]=\"loading\">\n @for (opt of cardPageSizeOptions; track opt) {\n <option [value]=\"opt\">{{ opt }} per page</option>\n }\n </select>\n </div>\n </div>\n }\n }\n\n</div>\n\n<!-- Dialog: Form Body -->\n<ng-template #formBodyTpl>\n <osl-dynamic-form\n [skeletonLoading]=\"formLoading\"\n [elements]=\"formElements\"\n [(model)]=\"dialogModel\"\n ></osl-dynamic-form>\n</ng-template>\n\n<!-- Dialog: Form Footer -->\n<ng-template #formFooterTpl>\n <div class=\"osl-setup-dialog-footer\">\n <osl-button [loading]=\"saveLoading\" variant=\"secondary\" label=\"Save\" (click)=\"saveDialog()\"></osl-button>\n </div>\n</ng-template>\n\n<ng-template #customFooterWrapperTpl let-data>\n <ng-container *ngTemplateOutlet=\"customFormFooter!; context: { $implicit: { dialogModel: dialogModel, dialogMode: dialogMode, dialogRef: data.dialogRef } }\"></ng-container>\n</ng-template>\n\n<!-- Floating card more-actions menu -->\n@if (viewMode === 'card' && cardOpenMenuIndex !== null && moreMenuActions.length > 0) {\n <div class=\"osl-card-menu\"\n [style.top.px]=\"cardMenuPosition.top\"\n [style.left.px]=\"cardMenuPosition.left\"\n (click)=\"$event.stopPropagation()\">\n <div class=\"osl-card-menu-header\">Actions</div>\n @for (action of moreMenuActions; track $index) {\n @if (!action.hideIf || !action.hideIf(cardPagedData[cardOpenMenuIndex])) {\n <button class=\"osl-card-menu-item\"\n (click)=\"action.click(cardPagedData[cardOpenMenuIndex]); cardOpenMenuIndex = null\">\n <span class=\"osl-card-menu-dot\"></span>\n {{ action.labelIf ? action.labelIf(cardPagedData[cardOpenMenuIndex]) : action.label }}\n </button>\n }\n }\n </div>\n}\n", styles: [".osl-setup-header{display:flex;align-items:center;justify-content:space-between}.osl-setup-dialog-footer{display:flex;align-items:center;justify-content:flex-end;gap:10px;width:100%}.dialog-cancel-btn{display:flex;align-items:center;gap:6px;height:38px;font-size:13px;font-weight:500;border-color:var(--osl-border-color, #d1d5db);color:var(--osl-secondary, #6b7280);border-radius:var(--osl-border-radius, 4px);padding:0 16px;transition:all .2s ease}.dialog-cancel-btn:hover{border-color:#9ca3af;background:#f9fafb;color:#374151}.dialog-save-btn{display:flex;align-items:center;gap:6px;height:38px;font-size:13px;font-weight:500;background:linear-gradient(135deg,var(--osl-primary, #2563eb),#3b82f6);color:#fff;border-radius:var(--osl-border-radius, 4px);padding:0 18px;transition:all .2s ease;box-shadow:0 2px 8px #2563eb40}.dialog-save-btn:hover{background:linear-gradient(135deg,var(--osl-primary-hover, #1d4ed8),#2563eb);box-shadow:0 4px 14px #2563eb66;transform:translateY(-1px)}.dialog-save-btn:active{transform:translateY(0);box-shadow:0 2px 8px #2563eb40}.osl-view-toggle{display:flex;border:1.5px solid var(--osl-border-color, #e5e7eb);border-radius:8px;overflow:hidden;flex-shrink:0;background:#fff}.osl-view-toggle-btn{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;border:none;background:transparent;cursor:pointer;color:#9ca3af;transition:background .15s,color .15s;padding:0}.osl-view-toggle-btn svg{width:16px;height:16px;transition:stroke .15s}.osl-view-toggle-btn:first-child{border-right:1.5px solid var(--osl-border-color, #e5e7eb)}.osl-view-toggle-btn--active{background:var(--osl-primary, #6366f1);color:#fff}.osl-view-toggle-btn--active svg{stroke:#fff}.osl-view-toggle-btn:not(.osl-view-toggle-btn--active):hover{background:#f3f4f6;color:#374151}.osl-card-grid{display:flexbox;width:100%;overflow:auto}@keyframes osl-card-enter{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.osl-card-grid .osl-card:nth-child(1){animation-delay:0s}.osl-card-grid .osl-card:nth-child(2){animation-delay:.03s}.osl-card-grid .osl-card:nth-child(3){animation-delay:.06s}.osl-card-grid .osl-card:nth-child(4){animation-delay:.09s}.osl-card-grid .osl-card:nth-child(5){animation-delay:.12s}.osl-card-grid .osl-card:nth-child(6){animation-delay:.15s}.osl-card-grid .osl-card:nth-child(7){animation-delay:.18s}.osl-card-grid .osl-card:nth-child(8){animation-delay:.21s}.osl-card{position:relative;background:#fff;border-radius:12px;border:1px solid #eaecf0;box-shadow:0 1px 3px #1018280f,0 1px 2px #1018280a;overflow:hidden;transition:transform .2s cubic-bezier(.34,1.56,.64,1),box-shadow .2s ease,border-color .2s ease;animation:osl-card-enter .26s ease both}.osl-card:hover{transform:translateY(-2px);box-shadow:0 8px 24px #1018281a,0 2px 6px #1018280f}.osl-card--selectable{cursor:pointer}.osl-card--highlighted{border-color:var(--card-accent, #6366f1);box-shadow:0 0 0 3px color-mix(in srgb,var(--card-accent, #6366f1) 20%,transparent),0 4px 16px #1018281a}.osl-card--skeleton{pointer-events:none;animation:osl-card-enter .26s ease both}.osl-card--skeleton:hover{transform:none;box-shadow:0 1px 3px #1018280f}@keyframes osl-card-highlight-pulse{0%{box-shadow:0 0 0 4px color-mix(in srgb,var(--card-accent, #6366f1) 30%,transparent)}to{box-shadow:none}}.osl-card-header{position:relative;overflow:hidden;display:flex;align-items:center;gap:10px;padding:9px 12px 9px 14px;border-bottom:1px solid rgba(0,0,0,.05);min-height:42px}.osl-card-header--skeleton{background:#f9fafb}.osl-card-watermark{position:absolute;right:8px;top:50%;transform:translateY(-50%);width:72px;height:40px;color:var(--card-accent, var(--osl-primary, #6366f1));opacity:.1;pointer-events:none;flex-shrink:0}.osl-card-title-wrap{flex:1;min-width:0;position:relative;z-index:1}.osl-card-title{display:block;font-weight:700;font-size:13px;color:#1e293b;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1.4}.osl-card-actions{display:flex;align-items:center;gap:4px;flex-shrink:0;position:relative;z-index:1}.osl-card-action-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:7px;border:1.5px solid rgba(255,255,255,.9);background:#fffc;cursor:pointer;transition:background .15s,border-color .15s,transform .15s,box-shadow .15s;padding:0}.osl-card-action-btn svg{width:13px;height:13px;transition:stroke .15s,fill .15s}.osl-card-action-btn--edit svg{stroke:#6b7280}.osl-card-action-btn--edit:hover{background:#fff;border-color:var(--card-accent, #6366f1);box-shadow:0 2px 8px #0000001f;transform:scale(1.1)}.osl-card-action-btn--edit:hover svg{stroke:var(--card-accent, #6366f1)}.osl-card-action-btn--delete svg{stroke:#9ca3af}.osl-card-action-btn--delete:hover{background:#fff1f2;border-color:#f43f5e;box-shadow:0 2px 8px #f43f5e33;transform:scale(1.1)}.osl-card-action-btn--delete:hover svg{stroke:#f43f5e}.osl-card-action-btn--more svg{fill:#9ca3af;stroke:none}.osl-card-action-btn--more:hover{background:#fff;border-color:var(--card-accent, #6366f1);box-shadow:0 2px 8px #0000001f;transform:scale(1.1)}.osl-card-action-btn--more:hover svg{fill:var(--card-accent, #6366f1)}.osl-card-body{padding:8px 12px 10px 14px}.osl-card-field{padding:4px 8px 2px 0;min-width:0;overflow:hidden}.osl-card-label{display:block;font-size:9.5px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.06em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:1px}.osl-card-value{display:block;font-size:12.5px;font-weight:500;color:#374151;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.osl-card-value.link{text-decoration:underline;color:#2563eb;cursor:pointer}.osl-card-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:320px;gap:12px;padding:40px 20px}.osl-card-empty-svg{width:180px;height:auto;opacity:.85}.osl-card-empty-title{font-size:16px;font-weight:600;color:#374151;margin:4px 0 0;text-align:center}.osl-card-empty-desc{font-size:13px;color:#9ca3af;margin:0;text-align:center}@keyframes osl-sk-pulse{0%,to{opacity:1}50%{opacity:.4}}.osl-sk-line{height:12px;background:#e9eaec;border-radius:6px;animation:osl-sk-pulse 1.4s ease-in-out infinite;width:80%}.osl-sk-line--title{height:13px;width:60%}.osl-sk-line--label{width:40%;height:9px;margin-bottom:4px}.osl-sk-line--short{width:50%}.osl-card-menu{position:fixed;z-index:9999;min-width:196px;background:#fff;border:1px solid #eaecf0;border-radius:12px;box-shadow:0 12px 32px #10182824,0 4px 12px #1018280f;overflow:hidden;animation:osl-menu-pop .16s cubic-bezier(.16,1,.3,1)}@keyframes osl-menu-pop{0%{opacity:0;transform:translateY(-8px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}.osl-card-menu-header{padding:10px 14px 7px;font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#9ca3af;border-bottom:1px solid #f2f4f7;-webkit-user-select:none;user-select:none}.osl-card-menu-item{display:flex;align-items:center;gap:10px;width:100%;padding:10px 14px;background:transparent;border:none;border-left:3px solid transparent;border-bottom:1px solid #f9fafb;text-align:left;font-size:13px;font-weight:500;font-family:inherit;color:#374151;cursor:pointer;white-space:nowrap;transition:background .12s,color .12s,border-left-color .12s}.osl-card-menu-item:last-child{border-bottom:none}.osl-card-menu-item:hover{background:var(--card-accent-bg, #f5f3ff);color:var(--card-accent, var(--osl-primary, #6366f1));border-left-color:var(--card-accent, var(--osl-primary, #6366f1))}.osl-card-menu-item:hover .osl-card-menu-dot{background:var(--card-accent, var(--osl-primary, #6366f1));box-shadow:0 0 0 3px #6366f129}.osl-card-menu-dot{flex-shrink:0;width:6px;height:6px;border-radius:50%;background:#d1d5db;transition:background .12s,box-shadow .12s}.osl-grid-pagination{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 16px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:10px;background:#f9fafb;flex-wrap:wrap;margin-top:4px}.osl-grid-pagination__info{font-size:12px;color:#6b7280;white-space:nowrap;min-width:120px}.osl-grid-pagination__controls{display:flex;align-items:center;gap:3px;flex-wrap:nowrap}.osl-grid-pagination__size{display:flex;align-items:center;min-width:120px;justify-content:flex-end}.osl-grid-page-btn{display:inline-flex;align-items:center;justify-content:center;height:30px;padding:0 10px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:var(--osl-border-radius, 6px);background:#fff;color:#374151;font-size:13px;font-family:inherit;cursor:pointer;transition:background .12s,border-color .12s,color .12s;white-space:nowrap}.osl-grid-page-btn:hover:not(:disabled){background:#f3f4f6;border-color:#9ca3af}.osl-grid-page-btn:disabled{opacity:.35;cursor:not-allowed}.osl-grid-page-btn--nav{font-size:12px;color:#6b7280}.osl-grid-page-btn--num{min-width:30px;padding:0;font-size:13px}.osl-grid-page-btn--active{background:var(--osl-primary, #6366f1);border-color:var(--osl-primary, #6366f1);color:#fff;font-weight:600}.osl-grid-page-btn--active:hover:not(:disabled){background:var(--osl-primary-hover, #4f46e5);border-color:var(--osl-primary-hover, #4f46e5)}.osl-grid-page-ellipsis{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;color:#9ca3af;font-size:13px;pointer-events:none}.osl-grid-page-size{height:30px;padding:0 8px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:var(--osl-border-radius, 6px);background:#fff;color:#374151;font-size:12px;font-family:inherit;cursor:pointer;outline:none}.osl-grid-page-size:focus{border-color:var(--osl-primary, #6366f1)}.title-icon{width:20px}\n"], dependencies: [{ kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: OslTooltipDirective, selector: "[oslTooltip]", inputs: ["oslTooltip", "oslTooltipPosition", "oslTooltipAnimation", "oslTooltipDisabled", "oslTooltipMaxWidth"] }, { kind: "component", type: DynamicForm, selector: "osl-dynamic-form", inputs: ["elements", "model", "skeletonLoading", "skeletonTheme"], outputs: ["modelChange"] }, { kind: "component", type: OslButton, selector: "osl-button", inputs: ["label", "icon", "variant", "size", "disabled", "loading", "type", "fullWidth"], outputs: ["clickEv"] }, { kind: "component", type: OslSearchbar, selector: "osl-searchbar", inputs: ["label"], outputs: ["onSearch"] }, { kind: "component", type: OslGrid, selector: "osl-grid", inputs: ["columns", "datasource", "isPaginated", "pageSize", "autoMode", "totalRecords", "tableHeight", "loading", "isSelectable", "moreMenuActions", "canEdit", "canDelete", "highlightedRow", "primaryKey"], outputs: ["datasourceChange", "pageChange", "pageSizeChange", "sortChange", "editClick", "deleteClick", "onRowClick"] }, { kind: "pipe", type: i1$1.DatePipe, name: "date" }] });
|
|
4031
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: OslSetup, isStandalone: false, selector: "osl-setup", inputs: { title: "title", titleIcon: "titleIcon", columns: "columns", datasource: "datasource", isPaginated: "isPaginated", pageSize: "pageSize", autoMode: "autoMode", tableHeight: "tableHeight", totalRecords: "totalRecords", loading: "loading", dialogWidth: "dialogWidth", isDraggable: "isDraggable", formElements: "formElements", beforeDisplay: "beforeDisplay", onAddEditFn: "onAddEditFn", isLister: "isLister", canAdd: "canAdd", canEdit: "canEdit", canDelete: "canDelete", moreMenuActions: "moreMenuActions", customFormFooter: "customFormFooter", customHeaderTemp: "customHeaderTemp", partialCustomHeaderTemp: "partialCustomHeaderTemp", stateKey: "stateKey", primaryKey: "primaryKey", onSave: "onSave", cardPageSize: "cardPageSize", cardTemplate: "cardTemplate", cardCol: "cardCol" }, outputs: { onSearch: "onSearch", onAdd: "onAdd", onEdit: "onEdit", onDelete: "onDelete", pageChange: "pageChange", pageSizeChange: "pageSizeChange", sortChange: "sortChange", onRowClick: "onRowClick", onStateRestored: "onStateRestored" }, host: { listeners: { "document:click": "onDocumentClick()" } }, viewQueries: [{ propertyName: "formBodyTpl", first: true, predicate: ["formBodyTpl"], descendants: true }, { propertyName: "formFooterTpl", first: true, predicate: ["formFooterTpl"], descendants: true }, { propertyName: "customFooterWrapperTpl", first: true, predicate: ["customFooterWrapperTpl"], descendants: true }, { propertyName: "searchbar", first: true, predicate: ["searchbar"], descendants: true }, { propertyName: "gridRef", first: true, predicate: ["gridRef"], descendants: true }, { propertyName: "cardGridRef", first: true, predicate: ["cardGridRef"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"p-2\">\n\n <!-- \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"osl-setup-header\">\n <div class=\"d-flex align-items-center\">\n @if(titleIcon){\n\n <span class=\"title-icon mx-2 mb-1\" [innerHTML]=\"renderIcon(titleIcon)\"></span>\n }\n <h5 class=\"mb-0\">{{ title }}</h5>\n\n </div>\n <div class=\"d-flex align-items-center gap-2\">\n <osl-searchbar #searchbar class=\"mx-2\" (onSearch)=\"onSearchSetup($event)\"></osl-searchbar>\n\n <!-- View Toggle -->\n @if(!isLister){\n <div class=\"osl-view-toggle\">\n <button class=\"osl-view-toggle-btn\" [class.osl-view-toggle-btn--active]=\"viewMode === 'table'\"\n (click)=\"toggleView('table')\" title=\"Table view\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/>\n <line x1=\"3\" y1=\"9\" x2=\"21\" y2=\"9\"/>\n <line x1=\"3\" y1=\"15\" x2=\"21\" y2=\"15\"/>\n <line x1=\"9\" y1=\"9\" x2=\"9\" y2=\"21\"/>\n </svg>\n </button>\n <button class=\"osl-view-toggle-btn\" [class.osl-view-toggle-btn--active]=\"viewMode === 'card'\"\n (click)=\"toggleView('card')\" title=\"Card view\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"3\" y=\"3\" width=\"7\" height=\"7\" rx=\"1.5\"/>\n <rect x=\"14\" y=\"3\" width=\"7\" height=\"7\" rx=\"1.5\"/>\n <rect x=\"3\" y=\"14\" width=\"7\" height=\"7\" rx=\"1.5\"/>\n <rect x=\"14\" y=\"14\" width=\"7\" height=\"7\" rx=\"1.5\"/>\n </svg>\n </button>\n </div>\n }\n\n @if (!isLister && canAdd) {\n <osl-button variant=\"secondary\" size=\"sm\" [label]=\"'Add ' + title\" (clickEv)=\"openAddDialog()\"></osl-button>\n }\n </div>\n </div>\n\n <!-- \u2500\u2500 Table View \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (viewMode === 'table') {\n <div class=\"osl-setup-body my-2\">\n <osl-grid\n #gridRef\n [columns]=\"columnsWithActions\"\n [(datasource)]=\"datasource\"\n [isPaginated]=\"isPaginated\"\n [pageSize]=\"pageSize\"\n [autoMode]=\"autoMode\"\n [tableHeight]=\"tableHeight\"\n [totalRecords]=\"totalRecords\"\n [loading]=\"loading\"\n [moreMenuActions]=\"moreMenuActions\"\n [canEdit]=\"canEdit\"\n [canDelete]=\"canDelete\"\n [highlightedRow]=\"restoredRow\"\n [primaryKey]=\"primaryKey\"\n (editClick)=\"openEditDialog($event)\"\n (deleteClick)=\"onDeleteClick($event)\"\n (pageChange)=\"onPageChange(pageChange, $event)\"\n (pageSizeChange)=\"onPageChange(pageSizeChange, $event)\"\n (sortChange)=\"sortChange.emit($event)\"\n [isSelectable]=\"isLister\"\n (onRowClick)=\"onRowClick.emit($event)\"\n />\n </div>\n }\n\n <!-- \u2500\u2500 Card View \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (viewMode === 'card') {\n\n <!-- Skeleton on initial load -->\n @if (loading && cardPagedData.length === 0) {\n <div class=\"osl-card-grid my-2\">\n @for (sk of skeletonCardRows; track $index) {\n <div class=\"osl-card osl-card--skeleton\">\n <div class=\"osl-card-header osl-card-header--skeleton\">\n <div class=\"osl-card-title-wrap\">\n <div class=\"osl-sk-line osl-sk-line--title\"></div>\n </div>\n </div>\n <div class=\"osl-card-body\">\n <div class=\"row g-0\">\n <div class=\"col-6 osl-card-field\"><div class=\"osl-sk-line osl-sk-line--label\"></div><div class=\"osl-sk-line\"></div></div>\n <div class=\"col-6 osl-card-field\"><div class=\"osl-sk-line osl-sk-line--label\"></div><div class=\"osl-sk-line osl-sk-line--short\"></div></div>\n <div class=\"col-6 osl-card-field\"><div class=\"osl-sk-line osl-sk-line--label\"></div><div class=\"osl-sk-line\"></div></div>\n <div class=\"col-6 osl-card-field\"><div class=\"osl-sk-line osl-sk-line--label\"></div><div class=\"osl-sk-line osl-sk-line--short\"></div></div>\n </div>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- Empty state -->\n @else if (cardPagedData.length === 0 && !loading) {\n <div class=\"osl-card-empty my-2\">\n <svg class=\"osl-card-empty-svg\" viewBox=\"0 0 180 160\" fill=\"none\">\n <ellipse cx=\"90\" cy=\"140\" rx=\"60\" ry=\"8\" fill=\"#f3f4f6\"/>\n <rect x=\"30\" y=\"30\" width=\"120\" height=\"96\" rx=\"10\" fill=\"#f9fafb\" stroke=\"#e5e7eb\" stroke-width=\"2\"/>\n <rect x=\"44\" y=\"48\" width=\"92\" height=\"10\" rx=\"5\" fill=\"#e5e7eb\"/>\n <rect x=\"44\" y=\"66\" width=\"72\" height=\"8\" rx=\"4\" fill=\"#f3f4f6\"/>\n <rect x=\"44\" y=\"82\" width=\"84\" height=\"8\" rx=\"4\" fill=\"#f3f4f6\"/>\n <rect x=\"44\" y=\"98\" width=\"56\" height=\"8\" rx=\"4\" fill=\"#f3f4f6\"/>\n <circle cx=\"140\" cy=\"108\" r=\"26\" fill=\"#fff\" stroke=\"#e5e7eb\" stroke-width=\"2\"/>\n <line x1=\"133\" y1=\"101\" x2=\"147\" y2=\"115\" stroke=\"#d1d5db\" stroke-width=\"2.5\" stroke-linecap=\"round\"/>\n <line x1=\"147\" y1=\"101\" x2=\"133\" y2=\"115\" stroke=\"#d1d5db\" stroke-width=\"2.5\" stroke-linecap=\"round\"/>\n </svg>\n <p class=\"osl-card-empty-title\">No records found</p>\n <p class=\"osl-card-empty-desc\">Try adjusting your search or filter criteria</p>\n </div>\n }\n\n <!-- Card grid -->\n @else {\n <div #cardGridRef class=\"osl-card-grid my-2\" [style.height]=\"tableHeight\">\n @for (row of cardPagedData; track row[primaryKey] ?? $index; let i = $index) {\n\n @if (cardTemplate) {\n <ng-container *ngTemplateOutlet=\"cardTemplate; context: { $implicit: row, index: i }\"></ng-container>\n } @else {\n <div class=\"osl-card my-2\"\n [class.osl-card--selectable]=\"isLister\"\n [class.osl-card--highlighted]=\"isHighlightedCard(row)\"\n (click)=\"isLister ? onRowClick.emit(row) : null\">\n\n <!-- Card Header -->\n <div class=\"osl-card-header\">\n <!-- <svg class=\"osl-card-watermark\" viewBox=\"0 0 120 64\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M10 42 L18 52 L102 52 L110 42 Z\"/>\n <rect x=\"20\" y=\"34\" width=\"80\" height=\"9\" rx=\"1\"/>\n <rect x=\"26\" y=\"20\" width=\"16\" height=\"15\" rx=\"1\"/>\n <rect x=\"46\" y=\"20\" width=\"16\" height=\"15\" rx=\"1\"/>\n <rect x=\"66\" y=\"20\" width=\"16\" height=\"15\" rx=\"1\"/>\n <rect x=\"85\" y=\"14\" width=\"15\" height=\"21\" rx=\"2\"/>\n <rect x=\"90.5\" y=\"7\" width=\"2\" height=\"9\" rx=\"1\"/>\n <path d=\"M4 57 Q18 53 32 57 Q46 61 60 57 Q74 53 88 57 Q102 61 116 57\" stroke=\"currentColor\" stroke-width=\"2\" fill=\"none\" stroke-linecap=\"round\"/>\n </svg> -->\n <div class=\"osl-card-title-wrap\">\n <span class=\"osl-card-title\"\n >\n @if (cardTitleColumn) {\n <span\n >\n @switch (cardTitleColumn.displayType) {\n @case ('date') { {{ row[cardTitleColumn.key] | date }} }\n @case ('datetime') { {{ row[cardTitleColumn.key] | date:'medium' }} }\n @case ('time') { {{ row[cardTitleColumn.key] | date:'shortTime' }} }\n @case ('customDateFormat') { {{ row[cardTitleColumn.key] | date:cardTitleColumn.customDateFormat }} }\n @default { {{ getCellValue(row, cardTitleColumn) }} }\n }\n </span>\n }\n </span>\n </div>\n @if (!isLister && ((hasForm && (canEdit || canDelete)) || (moreMenuActions.length > 0 && hasVisibleActions(row)))) {\n <div class=\"osl-card-actions\">\n @if (hasForm && canEdit) {\n <button class=\"osl-card-action-btn osl-card-action-btn--edit\"\n (click)=\"$event.stopPropagation(); openEditDialog(row)\"\n [oslTooltip]=\"'Edit'\" oslTooltipPosition=\"bottom\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7\"/>\n <path d=\"M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z\"/>\n </svg>\n </button>\n }\n @if (hasForm && canDelete) {\n <button class=\"osl-card-action-btn osl-card-action-btn--delete\"\n (click)=\"$event.stopPropagation(); onDeleteClick(row)\"\n [oslTooltip]=\"'Delete'\" oslTooltipPosition=\"bottom\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" xmlns=\"http://www.w3.org/2000/svg\">\n <polyline points=\"3 6 5 6 21 6\"/>\n <path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\"/>\n <line x1=\"10\" y1=\"11\" x2=\"10\" y2=\"17\"/><line x1=\"14\" y1=\"11\" x2=\"14\" y2=\"17\"/>\n </svg>\n </button>\n }\n @if (moreMenuActions.length > 0 && hasVisibleActions(row)) {\n <button class=\"osl-card-action-btn osl-card-action-btn--more\"\n (click)=\"toggleCardMenu(i, $event)\"\n [oslTooltip]=\"'More actions'\" oslTooltipPosition=\"bottom\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <circle cx=\"12\" cy=\"5\" r=\"2.2\"/>\n <circle cx=\"12\" cy=\"12\" r=\"2.2\"/>\n <circle cx=\"12\" cy=\"19\" r=\"2.2\"/>\n </svg>\n </button>\n }\n </div>\n }\n </div>\n\n <!-- Card Body -->\n @if (cardBodyColumns.length > 0) {\n <div class=\"osl-card-body\">\n <div class=\"row g-0\">\n @for (col of cardBodyColumns; track col.key) {\n <div [class]=\"'col-' + cardCol + ' osl-card-field'\">\n <span class=\"osl-card-label\">{{ col.label }}</span>\n <span class=\"osl-card-value\" [class.link]=\"col.displayType === 'link'\" (click)=\"col.click ? col.click(row, col) : null\">\n\n <span>\n @switch (col.displayType) {\n @case ('date') { {{ ((row[col.key] | date) || '--') }} }\n @case ('datetime') { {{ (row[col.key] | date:'medium') || '--' }} }\n @case ('time') { {{ (row[col.key] | date:'shortTime') || '--' }} }\n @case ('customDateFormat') { {{ (row[col.key] | date:col.customDateFormat) || '--' }} }\n @default { {{ (getCellValue(row, col) || '--') }} }\n }\n </span>\n </span>\n </div>\n }\n </div>\n </div>\n }\n\n </div>\n }\n }\n </div>\n }\n\n <!-- Card Paginator -->\n @if (isPaginated && _cardTotal > 0) {\n <div class=\"osl-grid-pagination\">\n <span class=\"osl-grid-pagination__info\">\n @if (loading) {\n Loading...\n } @else {\n {{ cardStartRecord }}\u2013{{ cardEndRecord }} of {{ _cardTotal }} records\n }\n </span>\n\n <div class=\"osl-grid-pagination__controls\">\n <button class=\"osl-grid-page-btn osl-grid-page-btn--nav\"\n (click)=\"cardGoToPage(1)\"\n [disabled]=\"cardCurrentPage === 1 || loading\" title=\"First page\">\u00AB</button>\n <button class=\"osl-grid-page-btn osl-grid-page-btn--nav\"\n (click)=\"cardGoToPage(cardCurrentPage - 1)\"\n [disabled]=\"cardCurrentPage === 1 || loading\" title=\"Previous page\">\u2039 Prev</button>\n\n @for (page of cardPageNumbers; track $index) {\n @if (page === -1) {\n <span class=\"osl-grid-page-ellipsis\">\u2026</span>\n } @else {\n <button class=\"osl-grid-page-btn osl-grid-page-btn--num\"\n [class.osl-grid-page-btn--active]=\"page === cardCurrentPage\"\n [disabled]=\"loading\"\n (click)=\"cardGoToPage(page)\">{{ page }}</button>\n }\n }\n\n <button class=\"osl-grid-page-btn osl-grid-page-btn--nav\"\n (click)=\"cardGoToPage(cardCurrentPage + 1)\"\n [disabled]=\"cardCurrentPage === cardTotalPages || loading\" title=\"Next page\">Next \u203A</button>\n <button class=\"osl-grid-page-btn osl-grid-page-btn--nav\"\n (click)=\"cardGoToPage(cardTotalPages)\"\n [disabled]=\"cardCurrentPage === cardTotalPages || loading\" title=\"Last page\">\u00BB</button>\n </div>\n\n <div class=\"osl-grid-pagination__size\">\n <select class=\"osl-grid-page-size\"\n [ngModel]=\"_effectiveCardPageSize\"\n (ngModelChange)=\"cardOnPageSizeChange($event)\"\n [disabled]=\"loading\">\n @for (opt of cardPageSizeOptions; track opt) {\n <option [value]=\"opt\">{{ opt }} per page</option>\n }\n </select>\n </div>\n </div>\n }\n }\n\n</div>\n\n<!-- Dialog: Form Body -->\n<ng-template #formBodyTpl>\n <osl-dynamic-form\n [skeletonLoading]=\"formLoading\"\n [elements]=\"formElements\"\n [(model)]=\"dialogModel\"\n ></osl-dynamic-form>\n</ng-template>\n\n<!-- Dialog: Form Footer -->\n<ng-template #formFooterTpl>\n <div class=\"osl-setup-dialog-footer\">\n <osl-button [loading]=\"saveLoading\" variant=\"secondary\" label=\"Save\" (click)=\"saveDialog()\"></osl-button>\n </div>\n</ng-template>\n\n<ng-template #customFooterWrapperTpl let-data>\n <ng-container *ngTemplateOutlet=\"customFormFooter!; context: { $implicit: { dialogModel: dialogModel, dialogMode: dialogMode, dialogRef: data.dialogRef } }\"></ng-container>\n</ng-template>\n\n<!-- Floating card more-actions menu -->\n@if (viewMode === 'card' && cardOpenMenuIndex !== null && moreMenuActions.length > 0) {\n <div class=\"osl-card-menu\"\n [style.top.px]=\"cardMenuPosition.top\"\n [style.left.px]=\"cardMenuPosition.left\"\n (click)=\"$event.stopPropagation()\">\n <div class=\"osl-card-menu-header\">Actions</div>\n @for (action of moreMenuActions; track $index) {\n @if (!action.hideIf || !action.hideIf(cardPagedData[cardOpenMenuIndex])) {\n <button class=\"osl-card-menu-item\"\n (click)=\"action.click(cardPagedData[cardOpenMenuIndex]); cardOpenMenuIndex = null\">\n <span class=\"osl-card-menu-dot\"></span>\n {{ action.labelIf ? action.labelIf(cardPagedData[cardOpenMenuIndex]) : action.label }}\n </button>\n }\n }\n </div>\n}\n", styles: [".osl-setup-header{display:flex;align-items:center;justify-content:space-between}.osl-setup-dialog-footer{display:flex;align-items:center;justify-content:flex-end;gap:10px;width:100%}.dialog-cancel-btn{display:flex;align-items:center;gap:6px;height:38px;font-size:13px;font-weight:500;border-color:var(--osl-border-color, #d1d5db);color:var(--osl-secondary, #6b7280);border-radius:var(--osl-border-radius, 4px);padding:0 16px;transition:all .2s ease}.dialog-cancel-btn:hover{border-color:#9ca3af;background:#f9fafb;color:#374151}.dialog-save-btn{display:flex;align-items:center;gap:6px;height:38px;font-size:13px;font-weight:500;background:linear-gradient(135deg,var(--osl-primary, #2563eb),#3b82f6);color:#fff;border-radius:var(--osl-border-radius, 4px);padding:0 18px;transition:all .2s ease;box-shadow:0 2px 8px #2563eb40}.dialog-save-btn:hover{background:linear-gradient(135deg,var(--osl-primary-hover, #1d4ed8),#2563eb);box-shadow:0 4px 14px #2563eb66;transform:translateY(-1px)}.dialog-save-btn:active{transform:translateY(0);box-shadow:0 2px 8px #2563eb40}.osl-view-toggle{display:flex;border:1.5px solid var(--osl-border-color, #e5e7eb);border-radius:8px;overflow:hidden;flex-shrink:0;background:#fff}.osl-view-toggle-btn{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;border:none;background:transparent;cursor:pointer;color:#9ca3af;transition:background .15s,color .15s;padding:0}.osl-view-toggle-btn svg{width:16px;height:16px;transition:stroke .15s}.osl-view-toggle-btn:first-child{border-right:1.5px solid var(--osl-border-color, #e5e7eb)}.osl-view-toggle-btn--active{background:var(--osl-primary, #6366f1);color:#fff}.osl-view-toggle-btn--active svg{stroke:#fff}.osl-view-toggle-btn:not(.osl-view-toggle-btn--active):hover{background:#f3f4f6;color:#374151}.osl-card-grid{display:flexbox;width:100%;overflow:auto}@keyframes osl-card-enter{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.osl-card-grid .osl-card:nth-child(1){animation-delay:0s}.osl-card-grid .osl-card:nth-child(2){animation-delay:.03s}.osl-card-grid .osl-card:nth-child(3){animation-delay:.06s}.osl-card-grid .osl-card:nth-child(4){animation-delay:.09s}.osl-card-grid .osl-card:nth-child(5){animation-delay:.12s}.osl-card-grid .osl-card:nth-child(6){animation-delay:.15s}.osl-card-grid .osl-card:nth-child(7){animation-delay:.18s}.osl-card-grid .osl-card:nth-child(8){animation-delay:.21s}.osl-card{position:relative;background:#fff;border-radius:12px;border:1px solid #eaecf0;box-shadow:0 1px 3px #1018280f,0 1px 2px #1018280a;overflow:hidden;transition:transform .2s cubic-bezier(.34,1.56,.64,1),box-shadow .2s ease,border-color .2s ease;animation:osl-card-enter .26s ease both}.osl-card:hover{transform:translateY(-2px);box-shadow:0 8px 24px #1018281a,0 2px 6px #1018280f}.osl-card--selectable{cursor:pointer}.osl-card--highlighted{border-color:var(--card-accent, #6366f1);box-shadow:0 0 0 3px color-mix(in srgb,var(--card-accent, #6366f1) 20%,transparent),0 4px 16px #1018281a}.osl-card--skeleton{pointer-events:none;animation:osl-card-enter .26s ease both}.osl-card--skeleton:hover{transform:none;box-shadow:0 1px 3px #1018280f}@keyframes osl-card-highlight-pulse{0%{box-shadow:0 0 0 4px color-mix(in srgb,var(--card-accent, #6366f1) 30%,transparent)}to{box-shadow:none}}.osl-card-header{position:relative;overflow:hidden;display:flex;align-items:center;gap:10px;padding:9px 12px 9px 14px;border-bottom:1px solid rgba(0,0,0,.05);min-height:42px}.osl-card-header--skeleton{background:#f9fafb}.osl-card-watermark{position:absolute;right:8px;top:50%;transform:translateY(-50%);width:72px;height:40px;color:var(--card-accent, var(--osl-primary, #6366f1));opacity:.1;pointer-events:none;flex-shrink:0}.osl-card-title-wrap{flex:1;min-width:0;position:relative;z-index:1}.osl-card-title{display:block;font-weight:700;font-size:13px;color:#1e293b;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1.4}.osl-card-actions{display:flex;align-items:center;gap:4px;flex-shrink:0;position:relative;z-index:1}.osl-card-action-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:7px;border:1.5px solid rgba(255,255,255,.9);background:#fffc;cursor:pointer;transition:background .15s,border-color .15s,transform .15s,box-shadow .15s;padding:0}.osl-card-action-btn svg{width:13px;height:13px;transition:stroke .15s,fill .15s}.osl-card-action-btn--edit svg{stroke:#6b7280}.osl-card-action-btn--edit:hover{background:#fff;border-color:var(--card-accent, #6366f1);box-shadow:0 2px 8px #0000001f;transform:scale(1.1)}.osl-card-action-btn--edit:hover svg{stroke:var(--card-accent, #6366f1)}.osl-card-action-btn--delete svg{stroke:#9ca3af}.osl-card-action-btn--delete:hover{background:#fff1f2;border-color:#f43f5e;box-shadow:0 2px 8px #f43f5e33;transform:scale(1.1)}.osl-card-action-btn--delete:hover svg{stroke:#f43f5e}.osl-card-action-btn--more svg{fill:#9ca3af;stroke:none}.osl-card-action-btn--more:hover{background:#fff;border-color:var(--card-accent, #6366f1);box-shadow:0 2px 8px #0000001f;transform:scale(1.1)}.osl-card-action-btn--more:hover svg{fill:var(--card-accent, #6366f1)}.osl-card-body{padding:8px 12px 10px 14px}.osl-card-field{padding:4px 8px 2px 0;min-width:0;overflow:hidden}.osl-card-label{display:block;font-size:9.5px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.06em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:1px}.osl-card-value{display:block;font-size:12.5px;font-weight:500;color:#374151;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.osl-card-value.link{text-decoration:underline;color:#2563eb;cursor:pointer}.osl-card-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:320px;gap:12px;padding:40px 20px}.osl-card-empty-svg{width:180px;height:auto;opacity:.85}.osl-card-empty-title{font-size:16px;font-weight:600;color:#374151;margin:4px 0 0;text-align:center}.osl-card-empty-desc{font-size:13px;color:#9ca3af;margin:0;text-align:center}@keyframes osl-sk-pulse{0%,to{opacity:1}50%{opacity:.4}}.osl-sk-line{height:12px;background:#e9eaec;border-radius:6px;animation:osl-sk-pulse 1.4s ease-in-out infinite;width:80%}.osl-sk-line--title{height:13px;width:60%}.osl-sk-line--label{width:40%;height:9px;margin-bottom:4px}.osl-sk-line--short{width:50%}.osl-card-menu{position:fixed;z-index:9999;min-width:196px;background:#fff;border:1px solid #eaecf0;border-radius:12px;box-shadow:0 12px 32px #10182824,0 4px 12px #1018280f;overflow:hidden;animation:osl-menu-pop .16s cubic-bezier(.16,1,.3,1)}@keyframes osl-menu-pop{0%{opacity:0;transform:translateY(-8px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}.osl-card-menu-header{padding:10px 14px 7px;font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#9ca3af;border-bottom:1px solid #f2f4f7;-webkit-user-select:none;user-select:none}.osl-card-menu-item{display:flex;align-items:center;gap:10px;width:100%;padding:10px 14px;background:transparent;border:none;border-left:3px solid transparent;border-bottom:1px solid #f9fafb;text-align:left;font-size:13px;font-weight:500;font-family:inherit;color:#374151;cursor:pointer;white-space:nowrap;transition:background .12s,color .12s,border-left-color .12s}.osl-card-menu-item:last-child{border-bottom:none}.osl-card-menu-item:hover{background:var(--card-accent-bg, #f5f3ff);color:var(--card-accent, var(--osl-primary, #6366f1));border-left-color:var(--card-accent, var(--osl-primary, #6366f1))}.osl-card-menu-item:hover .osl-card-menu-dot{background:var(--card-accent, var(--osl-primary, #6366f1));box-shadow:0 0 0 3px #6366f129}.osl-card-menu-dot{flex-shrink:0;width:6px;height:6px;border-radius:50%;background:#d1d5db;transition:background .12s,box-shadow .12s}.osl-grid-pagination{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 16px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:10px;background:#f9fafb;flex-wrap:wrap;margin-top:4px}.osl-grid-pagination__info{font-size:12px;color:#6b7280;white-space:nowrap;min-width:120px}.osl-grid-pagination__controls{display:flex;align-items:center;gap:3px;flex-wrap:nowrap}.osl-grid-pagination__size{display:flex;align-items:center;min-width:120px;justify-content:flex-end}.osl-grid-page-btn{display:inline-flex;align-items:center;justify-content:center;height:30px;padding:0 10px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:var(--osl-border-radius, 6px);background:#fff;color:#374151;font-size:13px;font-family:inherit;cursor:pointer;transition:background .12s,border-color .12s,color .12s;white-space:nowrap}.osl-grid-page-btn:hover:not(:disabled){background:#f3f4f6;border-color:#9ca3af}.osl-grid-page-btn:disabled{opacity:.35;cursor:not-allowed}.osl-grid-page-btn--nav{font-size:12px;color:#6b7280}.osl-grid-page-btn--num{min-width:30px;padding:0;font-size:13px}.osl-grid-page-btn--active{background:var(--osl-primary, #6366f1);border-color:var(--osl-primary, #6366f1);color:#fff;font-weight:600}.osl-grid-page-btn--active:hover:not(:disabled){background:var(--osl-primary-hover, #4f46e5);border-color:var(--osl-primary-hover, #4f46e5)}.osl-grid-page-ellipsis{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;color:#9ca3af;font-size:13px;pointer-events:none}.osl-grid-page-size{height:30px;padding:0 8px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:var(--osl-border-radius, 6px);background:#fff;color:#374151;font-size:12px;font-family:inherit;cursor:pointer;outline:none}.osl-grid-page-size:focus{border-color:var(--osl-primary, #6366f1)}.title-icon{width:20px}\n"], dependencies: [{ kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: OslTooltipDirective, selector: "[oslTooltip]", inputs: ["oslTooltip", "oslTooltipPosition", "oslTooltipAnimation", "oslTooltipDisabled", "oslTooltipMaxWidth"] }, { kind: "component", type: DynamicForm, selector: "osl-dynamic-form", inputs: ["elements", "model", "skeletonLoading", "skeletonTheme"], outputs: ["modelChange"] }, { kind: "component", type: OslButton, selector: "osl-button", inputs: ["label", "icon", "variant", "size", "disabled", "loading", "type", "fullWidth"], outputs: ["clickEv"] }, { kind: "component", type: OslSearchbar, selector: "osl-searchbar", inputs: ["label"], outputs: ["onSearch"] }, { kind: "component", type: OslGrid, selector: "osl-grid", inputs: ["columns", "datasource", "isPaginated", "pageSize", "autoMode", "totalRecords", "tableHeight", "loading", "isSelectable", "moreMenuActions", "canEdit", "canDelete", "highlightedRow", "primaryKey"], outputs: ["datasourceChange", "pageChange", "pageSizeChange", "sortChange", "editClick", "deleteClick", "onRowClick"] }, { kind: "pipe", type: i1$1.DatePipe, name: "date" }] });
|
|
4015
4032
|
}
|
|
4016
4033
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: OslSetup, decorators: [{
|
|
4017
4034
|
type: Component,
|
|
@@ -4067,6 +4084,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
4067
4084
|
}], dialogWidth: [{
|
|
4068
4085
|
type: Input,
|
|
4069
4086
|
args: ['dialogWidth']
|
|
4087
|
+
}], isDraggable: [{
|
|
4088
|
+
type: Input,
|
|
4089
|
+
args: ['isDraggable']
|
|
4070
4090
|
}], formElements: [{
|
|
4071
4091
|
type: Input,
|
|
4072
4092
|
args: ['formElements']
|
|
@@ -4462,11 +4482,11 @@ class OslFormGrid {
|
|
|
4462
4482
|
});
|
|
4463
4483
|
}
|
|
4464
4484
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: OslFormGrid, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4465
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: OslFormGrid, isStandalone: false, selector: "osl-form-grid", inputs: { columns: "columns", datasource: "datasource", isPaginated: "isPaginated", pageSize: "pageSize", canAdd: "canAdd", canDelete: "canDelete", loading: "loading", tableHeight: "tableHeight", footerColumns: "footerColumns", enableDragDrop: "enableDragDrop", draggingList: "draggingList", draggingColumns: "draggingColumns", leftPanelWidth: "leftPanelWidth", dragTitle: "dragTitle", showSearch: "showSearch", allowReorder: "allowReorder", allowRemove: "allowRemove", panelSearchType: "panelSearchType", panelLoading: "panelLoading", panelTotalRecords: "panelTotalRecords", panelPageSize: "panelPageSize" }, outputs: { datasourceChange: "datasourceChange", rowAdd: "rowAdd", rowDelete: "rowDelete", dragStarted: "dragStarted", dragEnded: "dragEnded", dragEntered: "dragEntered", dragExited: "dragExited", itemDropped: "itemDropped", itemMoved: "itemMoved", itemRemoved: "itemRemoved", itemSelected: "itemSelected", gridChanged: "gridChanged", panelSearch: "panelSearch", panelPageChange: "panelPageChange" }, ngImport: i0, template: "<div class=\"osl-fg-wrapper\" [class.osl-fg-wrapper--builder]=\"enableDragDrop\">\n\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n Available Controls panel (only in builder mode)\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n\n\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n Main grid area (always present)\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <div class=\"osl-fg-grid-area\">\n\n <!-- Panel-to-grid drop overlay: always in DOM so #gridDropZone is always resolvable.\n pointer-events: none by default; only activates visually when isDraggingFromPanel.\n CDK hit-tests by bounding rect, not pointer-events, so drops are captured regardless. -->\n <div class=\"osl-fg-table-drop-zone\"\n [class.osl-fg-table-drop-zone--active]=\"isDraggingFromPanel\"\n cdkDropList\n #gridDropZone=\"cdkDropList\"\n [cdkDropListSortingDisabled]=\"true\"\n [cdkDropListDisabled]=\"!enableDragDrop\"\n [cdkDropListData]=\"[]\"\n (cdkDropListDropped)=\"onPanelDrop($event)\">\n @if (isDraggingFromPanel) {\n <div class=\"osl-fg-drop-zone-badge\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\">\n <path d=\"M12 5v14M5 12l7 7 7-7\"/>\n </svg>\n Drop to Add\n </div>\n }\n </div>\n\n <div class=\"osl-fg-table-container\"\n [style.height]=\"tableHeight\"\n [class.osl-fg-table-container--drop-active]=\"isDragging && enableDragDrop\">\n\n <table class=\"osl-fg-table\">\n\n <!-- \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <thead class=\"osl-fg-thead\">\n <tr>\n @if (enableDragDrop && allowReorder) {\n <th class=\"osl-fg-th osl-fg-th--drag\"></th>\n }\n @if (hasActions) {\n <th class=\"osl-fg-th osl-fg-th--actions\">\n @if (canAdd) {\n <button class=\"osl-grid-icon-btn\" (click)=\"addRow()\" [disabled]=\"loading\" title=\"Add row\">\n <mat-icon>add</mat-icon>\n </button>\n }\n </th>\n }\n @for (col of columns; track col.key) {\n <th class=\"osl-fg-th\" [style.width]=\"col.width\">\n {{ col.displayName }}\n @if (colRequired(col)) {\n <span class=\"osl-fg-th-required\">*</span>\n }\n </th>\n }\n </tr>\n </thead>\n\n <!-- \u2500\u2500 Body \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <tbody\n cdkDropList\n #gridList=\"cdkDropList\"\n [cdkDropListData]=\"pagedData\"\n [cdkDropListDisabled]=\"!enableDragDrop\"\n (cdkDropListDropped)=\"onGridReorder($event)\">\n\n <!-- Loading state -->\n @if (loading) {\n @for (sk of skeletonRows; track $index) {\n <tr class=\"osl-fg-row osl-fg-row--skeleton\">\n @if (enableDragDrop && allowReorder) {\n <td class=\"osl-fg-td osl-fg-td--drag\"></td>\n }\n @if (hasActions) {\n <td class=\"osl-fg-td osl-fg-td--actions\">\n <div class=\"osl-fg-skeleton osl-fg-skeleton--sm\"></div>\n </td>\n }\n @for (col of columns; track col.key) {\n <td class=\"osl-fg-td\"><div class=\"osl-fg-skeleton\"></div></td>\n }\n </tr>\n }\n\n <!-- Empty state -->\n } @else if (pagedData.length === 0) {\n <tr>\n <td [attr.colspan]=\"columns.length + (hasActions ? 1 : 0) + (enableDragDrop && allowReorder ? 1 : 0)\"\n class=\"osl-fg-empty\">\n <div class=\"osl-fg-empty-inner\">\n @if (enableDragDrop && isDragging) {\n <svg width=\"36\" height=\"36\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\">\n <path d=\"M12 5v14M5 12l7 7 7-7\"/>\n </svg>\n <p>Drop here</p>\n } @else {\n <svg width=\"36\" height=\"36\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\">\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/>\n <line x1=\"3\" y1=\"9\" x2=\"21\" y2=\"9\"/><line x1=\"9\" y1=\"9\" x2=\"9\" y2=\"21\"/>\n </svg>\n <p>No rows yet.{{ canAdd ? ' Use + to get started.' : (enableDragDrop ? ' Drag a control here.' : '') }}</p>\n }\n </div>\n </td>\n </tr>\n\n <!-- Data rows -->\n } @else {\n @for (row of pagedData; track $index; let i = $index) {\n <tr class=\"osl-fg-row\"\n cdkDrag\n [cdkDragData]=\"row\"\n [cdkDragDisabled]=\"!enableDragDrop\"\n (cdkDragStarted)=\"onDragStartedEvent($event, row, 'grid')\"\n (cdkDragEnded)=\"onDragEndedEvent($event, row, 'grid')\"\n (click)=\"enableDragDrop ? selectGridItem(row, i) : null\">\n\n <!-- Drag handle -->\n @if (enableDragDrop && allowReorder) {\n <td class=\"osl-fg-td osl-fg-td--drag\">\n <span class=\"osl-fg-row-handle\" cdkDragHandle title=\"Drag to reorder\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <circle cx=\"9\" cy=\"5\" r=\"1.3\"/><circle cx=\"9\" cy=\"12\" r=\"1.3\"/><circle cx=\"9\" cy=\"19\" r=\"1.3\"/>\n <circle cx=\"15\" cy=\"5\" r=\"1.3\"/><circle cx=\"15\" cy=\"12\" r=\"1.3\"/><circle cx=\"15\" cy=\"19\" r=\"1.3\"/>\n </svg>\n </span>\n </td>\n }\n\n <!-- Action cell (remove / delete) -->\n @if (hasActions) {\n <td class=\"osl-fg-td osl-fg-td--actions\">\n @if (enableDragDrop && allowRemove) {\n <button class=\"osl-grid-icon-btn osl-grid-icon-btn--danger\"\n (click)=\"removeGridRow(i); $event.stopPropagation()\" title=\"Remove\">\n <mat-icon>close</mat-icon>\n </button>\n } @else if (!enableDragDrop && canDelete) {\n <button class=\"osl-grid-icon-btn osl-grid-icon-btn--danger\"\n (click)=\"deleteRow(i)\" title=\"Delete\">\n <mat-icon>delete_outline</mat-icon>\n </button>\n }\n </td>\n }\n\n <!-- Data cells -->\n @for (col of columns; track col.key) {\n <td class=\"osl-fg-td\" [style.width]=\"col.width\">\n @if (col.formElem) {\n <div class=\"osl-fg-cell-form\" [style.width]=\"col.width\">\n @switch (col.formElem.elementType) {\n\n @case ('textbox') {\n <osl-input\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [type]=\"col.formElem.inputType || 'text'\"\n [placeholder]=\"col.formElem.placeholder || col.displayName\"\n [mask]=\"col.formElem.mask || ''\"\n [min]=\"col.formElem.min ?? ''\"\n [max]=\"col.formElem.max ?? ''\"\n [minLength]=\"col.formElem.minLength ?? null\"\n [maxLength]=\"col.formElem.maxLength ?? null\"\n [prefixIcon]=\"col.formElem.prefixIcon || ''\"\n [suffixIcon]=\"col.formElem.suffixIcon || ''\"\n [decimalPortion]=\"col.formElem.decimalPortion ?? null\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-input>\n }\n\n @case ('textarea') {\n <osl-textarea\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [rows]=\"col.formElem.textareaRows || 3\"\n [placeholder]=\"col.formElem.placeholder || col.displayName\"\n [maxLength]=\"col.formElem.maxLength ?? null\"\n [minLength]=\"col.formElem.minLength ?? null\"\n [characterCounter]=\"!!col.formElem.characterCounter\"\n [resize]=\"col.formElem.resize || 'none'\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-textarea>\n }\n\n @case ('select') {\n <osl-select\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [datasource]=\"col.formElem.datasource || []\"\n [displayField]=\"col.formElem.displayField || ''\"\n [valueField]=\"col.formElem.valueField || ''\"\n [placeholder]=\"col.formElem.selectPlaceholder || col.displayName\"\n [loading]=\"isLoading(row, col.formElem)\"\n [clearable]=\"!!col.formElem.clearable\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? onSelectChange(col, row, i, $event) : null\">\n </osl-select>\n }\n\n @case ('autocomplete') {\n <osl-autocomplete\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [datasource]=\"col.formElem.datasource || []\"\n [displayField]=\"col.formElem.displayField || ''\"\n [valueField]=\"col.formElem.valueField || ''\"\n [placeholder]=\"col.formElem.autocompletePlaceholder || col.displayName\"\n [loading]=\"isLoading(row, col.formElem)\"\n [searchType]=\"col.formElem.searchType ? col.formElem.searchType : 'Local'\"\n [methodName]=\"col.formElem.apiMethod || ''\"\n [configMethodName]=\"col.formElem.apiConfigMethod || ''\"\n [service]=\"col.formElem.apiService\"\n [object]=\"col.formElem.objectName ? row[col.formElem.objectName] : null\"\n [isLister]=\"!!col.formElem.isListerAutocomplete\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? onSelectChange(col, row, i, $event) : null\">\n </osl-autocomplete>\n }\n\n @case ('radio') {\n <osl-radio\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [datasource]=\"col.formElem.datasource || []\"\n [displayField]=\"col.formElem.displayField || ''\"\n [valueField]=\"col.formElem.valueField || ''\"\n [inline]=\"!!col.formElem.inline\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-radio>\n }\n\n @case ('checkbox') {\n <osl-checkbox\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [indeterminate]=\"!!col.formElem.indeterminate\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-checkbox>\n }\n\n @case ('slide-toggle') {\n <osl-slide-toggle\n [label]=\"''\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [labelPosition]=\"col.formElem.labelPosition || 'after'\"\n [trueLabel]=\"col.formElem.trueLabel || ''\"\n [falseLabel]=\"col.formElem.falseLabel || ''\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-slide-toggle>\n }\n\n @case ('datepicker') {\n <osl-datepicker\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [dateType]=\"col.formElem.dateType || 'date'\"\n [placeholder]=\"col.formElem.placeholder || col.displayName\"\n [minDate]=\"col.formElem.minDate || ''\"\n [maxDate]=\"col.formElem.maxDate || ''\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-datepicker>\n }\n\n @case ('datetimepicker') {\n <osl-datetimepicker\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [placeholder]=\"col.formElem.placeholder || col.displayName\"\n [minDatetime]=\"col.formElem.minDatetime || ''\"\n [maxDatetime]=\"col.formElem.maxDatetime || ''\"\n [format]=\"col.formElem.datetimepickerFormat || 'YYYY-MM-DDTHH:mm'\"\n [showSeconds]=\"!!col.formElem.datetimepickerShowSeconds\"\n [enableMeridian]=\"!!col.formElem.datetimepickerEnableMeridian\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-datetimepicker>\n }\n\n @case ('file-uploader') {\n <osl-file-upload\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [accept]=\"col.formElem.accept || ''\"\n [multiple]=\"!!col.formElem.multiple\"\n [maxSize]=\"col.formElem.maxFileSize || 0\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-file-upload>\n }\n\n @case ('templateRef') {\n <ng-container *ngTemplateOutlet=\"col.formElem.templateRef; context: { $implicit: row, index: i }\"></ng-container>\n }\n\n }\n </div>\n } @else {\n <span class=\"osl-fg-cell-text\">{{ row[col.key] ?? '--' }}</span>\n }\n </td>\n }\n\n <!-- Row drag preview -->\n <ng-template cdkDragPreview>\n <div class=\"osl-fg-row-preview\">\n @for (col of columns; track col.key) {\n <span class=\"osl-fg-row-preview__cell\">{{ row[col.key] ?? '\u2014' }}</span>\n }\n </div>\n </ng-template>\n\n <!-- Drop placeholder: spans full row width with \"Drop Here\" -->\n <ng-template cdkDragPlaceholder>\n <tr class=\"osl-fg-row-placeholder\">\n <td [attr.colspan]=\"columns.length + (hasActions ? 1 : 0) + (enableDragDrop && allowReorder ? 1 : 0)\">\n <div class=\"osl-fg-drop-here\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M12 5v14M5 12l7 7 7-7\"/>\n </svg>\n Drop Here\n </div>\n </td>\n </tr>\n </ng-template>\n\n </tr>\n }\n }\n\n </tbody>\n\n <!-- \u2500\u2500 Footer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (footerColumns && footerColumns.length > 0 && datasource && datasource.length > 0) {\n <tfoot class=\"osl-fg-tfoot\">\n <tr class=\"osl-fg-footer-row\">\n @for (fcol of footerColumns; track $index) {\n <td\n [class]=\"'osl-fg-footer-td ' + (fcol.class || '')\"\n [attr.colspan]=\"fcol.colspan || 1\">\n {{ fcol.displayFn ? fcol.displayFn(datasource) : (fcol.display || '') }}\n </td>\n }\n </tr>\n </tfoot>\n }\n\n </table>\n </div>\n\n <!-- \u2500\u2500 Pagination (unchanged) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (isPaginated) {\n <div class=\"osl-fg-pagination\">\n\n <span class=\"osl-fg-pagination__info\">\n @if (loading) {\n Loading...\n } @else if (_total > 0) {\n {{ startRecord }}\u2013{{ endRecord }} of {{ _total }} rows\n } @else {\n No rows\n }\n </span>\n\n <div class=\"osl-fg-pagination__controls\">\n <button class=\"osl-fg-page-btn osl-fg-page-btn--nav\" (click)=\"goToPage(1)\" [disabled]=\"currentPage === 1 || loading\">\u00AB</button>\n <button class=\"osl-fg-page-btn osl-fg-page-btn--nav\" (click)=\"goToPage(currentPage - 1)\" [disabled]=\"currentPage === 1 || loading\">\u2039 Prev</button>\n\n @for (page of pageNumbers; track $index) {\n @if (page === -1) {\n <span class=\"osl-fg-page-ellipsis\">\u2026</span>\n } @else {\n <button\n class=\"osl-fg-page-btn osl-fg-page-btn--num\"\n [class.osl-fg-page-btn--active]=\"page === currentPage\"\n [disabled]=\"loading\"\n (click)=\"goToPage(page)\">\n {{ page }}\n </button>\n }\n }\n\n <button class=\"osl-fg-page-btn osl-fg-page-btn--nav\" (click)=\"goToPage(currentPage + 1)\" [disabled]=\"currentPage === totalPages || loading\">Next \u203A</button>\n <button class=\"osl-fg-page-btn osl-fg-page-btn--nav\" (click)=\"goToPage(totalPages)\" [disabled]=\"currentPage === totalPages || loading\">\u00BB</button>\n </div>\n\n <div class=\"osl-fg-pagination__size\">\n <select class=\"osl-fg-page-size\" [ngModel]=\"pageSize\" (ngModelChange)=\"onPageSizeChange($event)\" [disabled]=\"loading\">\n @for (opt of pageSizeOptions; track opt) {\n <option [value]=\"opt\">{{ opt }} per page</option>\n }\n </select>\n </div>\n\n </div>\n }\n\n </div><!-- /.osl-fg-grid-area -->\n @if (enableDragDrop) {\n <div class=\"osl-fg-panel\" [style.width.px]=\"leftPanelWidth\" [style.height]=\"tableHeight\">\n\n <!-- Panel header -->\n <div class=\"osl-fg-panel__header\">\n <span class=\"osl-fg-panel__title\">{{ dragTitle }}</span>\n @if (panelSearchType === 'Api' && panelTotalRecords > 0) {\n <span class=\"osl-fg-panel__count\">{{ draggingList.length }}/{{ panelTotalRecords }}</span>\n } @else if (filteredDraggingList.length > 0) {\n <span class=\"osl-fg-panel__count\">{{ filteredDraggingList.length }}</span>\n }\n </div>\n\n <!-- Search box -->\n @if (showSearch) {\n <div class=\"osl-fg-panel__search\">\n <svg class=\"osl-fg-search-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/><line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"/>\n </svg>\n <input\n class=\"osl-fg-search-input\"\n type=\"text\"\n [ngModel]=\"dragSearchQuery\"\n (ngModelChange)=\"onPanelSearchChange($event)\"\n placeholder=\"Search\u2026\"\n autocomplete=\"off\" />\n @if (dragSearchQuery) {\n <button class=\"osl-fg-search-clear\" (click)=\"clearPanelSearch()\" title=\"Clear\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/>\n </svg>\n </button>\n }\n </div>\n }\n\n <!-- Item list -->\n <div class=\"osl-fg-panel__list\"\n cdkDropList\n #availableList=\"cdkDropList\"\n [cdkDropListData]=\"filteredDraggingList\"\n [cdkDropListConnectedTo]=\"[gridDropZone]\"\n [cdkDropListSortingDisabled]=\"true\"\n (cdkDropListEntered)=\"onDragEnteredEvent($event, 'available')\"\n (cdkDropListExited)=\"onDragExitedEvent($event, 'available')\">\n\n <!-- Loading skeleton -->\n @if (panelLoading && draggingList.length === 0) {\n @for (sk of [1,2,3,4,5]; track sk) {\n <div class=\"osl-fg-item-skeleton\"></div>\n }\n\n } @else if (filteredDraggingList.length === 0) {\n <div class=\"osl-fg-panel__empty\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/><line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"/>\n </svg>\n <span>{{ dragSearchQuery ? 'No results found' : 'No results available' }}</span>\n </div>\n\n } @else {\n @for (item of filteredDraggingList; track item.id) {\n <div class=\"osl-fg-drag-item\"\n cdkDrag\n [cdkDragData]=\"item\"\n (cdkDragStarted)=\"onDragStartedEvent($event, item, 'available')\"\n (cdkDragEnded)=\"onDragEndedEvent($event, item, 'available')\">\n\n <!-- Grip handle -->\n <span class=\"osl-fg-drag-item__grip\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <circle cx=\"9\" cy=\"5\" r=\"1.4\"/><circle cx=\"9\" cy=\"12\" r=\"1.4\"/><circle cx=\"9\" cy=\"19\" r=\"1.4\"/>\n <circle cx=\"15\" cy=\"5\" r=\"1.4\"/><circle cx=\"15\" cy=\"12\" r=\"1.4\"/><circle cx=\"15\" cy=\"19\" r=\"1.4\"/>\n </svg>\n </span>\n\n <!-- Label + description -->\n <div class=\"osl-fg-drag-item__body\">\n <span class=\"osl-fg-drag-item__label\">{{ item.label }}</span>\n @if (item.description) {\n <span class=\"osl-fg-drag-item__desc\">{{ item.description }}</span>\n }\n </div>\n\n <!-- Drag-into-grid hint -->\n <span class=\"osl-fg-drag-item__arrow\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"/><polyline points=\"12 5 19 12 12 19\"/>\n </svg>\n </span>\n\n <!-- Placeholder shown in panel while card is dragged -->\n <div *cdkDragPlaceholder class=\"osl-fg-item-placeholder\"></div>\n\n <!-- Preview under the cursor while dragging -->\n <ng-template cdkDragPreview>\n <div class=\"osl-fg-drag-preview\">{{ item.label }}</div>\n </ng-template>\n\n </div>\n }\n\n <!-- Load More (API mode) -->\n @if (panelHasMore) {\n <button class=\"osl-fg-load-more\" (click)=\"loadMorePanelItems()\" [disabled]=\"panelLoading\">\n @if (panelLoading) {\n <span class=\"osl-fg-load-more__spinner\"></span> Loading\u2026\n } @else {\n Load More\n }\n </button>\n }\n\n <!-- Inline loading indicator while appending -->\n @if (panelLoading && draggingList.length > 0) {\n <div class=\"osl-fg-item-skeleton\"></div>\n }\n }\n\n </div>\n </div>\n }\n</div><!-- /.osl-fg-wrapper -->\n", styles: [".osl-fg-wrapper{display:flex;flex-direction:column;width:100%;font-size:var(--osl-text-font-size);font-family:inherit;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:var(--osl-border-radius, 8px);overflow:hidden;background:#fff}.osl-fg-wrapper--builder{flex-direction:row;overflow:visible;align-items:stretch}.osl-fg-grid-area{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;overflow:hidden;border-radius:inherit;position:relative}.osl-fg-table-drop-zone{position:absolute;inset:0;z-index:3;pointer-events:none;border-radius:inherit;transition:background .15s,border-color .15s}.osl-fg-table-drop-zone--active{border:2px dashed var(--osl-primary, #6366f1);background:#6366f108}.osl-fg-table-drop-zone--active.cdk-drop-list-receiving{background:#6366f114;border-style:solid}.osl-fg-drop-zone-badge{position:absolute;bottom:12px;left:50%;transform:translate(-50%);display:flex;align-items:center;gap:5px;padding:5px 13px;background:var(--osl-primary, #6366f1);color:#fff;border-radius:20px;font-size:11px;font-weight:600;white-space:nowrap;pointer-events:none;box-shadow:0 4px 12px #6366f159}.osl-fg-table-container{overflow-x:auto;overflow-y:auto;min-height:120px;flex:1 1 auto}.osl-fg-table-container--drop-active{outline:2px dashed var(--osl-primary, #6366f1);outline-offset:-2px}.osl-fg-table-container--drop-active .osl-fg-empty-inner{color:var(--osl-primary, #6366f1)}.osl-fg-table-container--drop-active .osl-fg-empty-inner svg{opacity:.7}.osl-fg-table{width:100%;border-collapse:collapse;table-layout:auto}.osl-fg-thead{position:sticky;top:0;z-index:2}.osl-fg-th{background:#f3f4f6;color:#4b5563;font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.04em;padding:10px 12px;text-align:left;border-bottom:2px solid var(--osl-border-color, #e5e7eb);border-right:1px solid var(--osl-border-color, #e5e7eb);white-space:nowrap;vertical-align:middle}.osl-fg-th:last-child{border-right:none}.osl-fg-th--actions{width:44px;min-width:44px;text-align:center;padding:6px}.osl-fg-th--drag{width:28px;min-width:28px;padding:0}.osl-fg-th-required{color:#ef4444;margin-left:2px;font-size:14px;line-height:1;vertical-align:middle}.osl-fg-row{border-bottom:1px solid #f0f0f0;transition:background .1s}.osl-fg-row:nth-child(odd){background:#f8fafc}.osl-fg-row:nth-child(2n){background:#fff}.osl-fg-row:hover{background:#eef2ff!important}.osl-fg-row:last-child{border-bottom:none}.osl-fg-row--skeleton{pointer-events:none}.osl-fg-td{padding:4px 10px;vertical-align:middle;border-right:1px solid #f0f0f0}.osl-fg-td:last-child{border-right:none}.osl-fg-td--actions{width:44px;min-width:44px;text-align:center;padding:4px 6px}.osl-fg-td--drag{width:28px;min-width:28px;padding:0;text-align:center}.osl-fg-cell-form{min-width:100px}.osl-fg-cell-form ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none!important}.osl-fg-cell-form ::ng-deep .mat-mdc-form-field,.osl-fg-cell-form ::ng-deep mat-form-field{width:100%}.osl-fg-cell-form ::ng-deep .mat-mdc-text-field-wrapper{padding-top:0}.osl-fg-cell-text{color:#111827;font-size:var(--osl-text-font-size)}.osl-grid-icon-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:1.5px solid var(--osl-border-color);border-radius:var(--osl-border-radius);background:transparent;color:#6b7280;cursor:pointer;padding:0;transition:border-color .15s,color .15s,background .15s;margin:0 2px}.osl-grid-icon-btn mat-icon{font-size:16px;width:16px;height:16px;line-height:16px}.osl-grid-icon-btn:hover{border-color:var(--osl-primary);color:var(--osl-primary);background:#6366f10f}.osl-grid-icon-btn--danger:hover{border-color:#ef4444;color:#ef4444;background:#ef44440f}.osl-fg-row-handle{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;cursor:grab;color:#d1d5db;border-radius:4px;transition:color .12s,background .12s}.osl-fg-row-handle svg{width:13px;height:13px}.osl-fg-row-handle:hover{color:#9ca3af;background:#f3f4f6}.osl-fg-row-handle:active{cursor:grabbing}.osl-fg-row-placeholder td{padding:4px 6px;background:transparent;border:none}.osl-fg-drop-here{display:flex;align-items:center;justify-content:center;gap:8px;height:46px;border:2px dashed var(--osl-primary, #6366f1);border-radius:6px;background:#eef2ff;color:var(--osl-primary, #6366f1);font-size:12px;font-weight:600;letter-spacing:.03em}.osl-fg-drop-here svg{width:16px;height:16px;flex-shrink:0}.osl-fg-row-preview{display:flex;align-items:center;gap:14px;padding:9px 16px;background:#fff;border:1.5px solid var(--osl-primary, #6366f1);border-radius:8px;box-shadow:0 8px 24px #6366f12e;font-size:12px;color:#374151;max-width:420px;white-space:nowrap;overflow:hidden}.osl-fg-row-preview__cell{overflow:hidden;text-overflow:ellipsis;max-width:130px}.osl-fg-row-preview__cell+.osl-fg-row-preview__cell{padding-left:14px;border-left:1px solid #e5e7eb}.osl-fg-empty{padding:40px 16px;text-align:center}.osl-fg-empty-inner{display:flex;flex-direction:column;align-items:center;gap:10px;color:#9ca3af}.osl-fg-empty-inner svg{opacity:.4}.osl-fg-empty-inner p{margin:0;font-size:13px}@keyframes osl-fg-pulse{0%,to{opacity:1}50%{opacity:.4}}.osl-fg-skeleton{height:36px;border-radius:6px;background:#e5e7eb;animation:osl-fg-pulse 1.4s ease-in-out infinite}.osl-fg-skeleton--sm{height:28px;width:28px;border-radius:4px;margin:0 auto}.osl-fg-tfoot{position:sticky;bottom:0;z-index:2}.osl-fg-footer-td{background:#f3f4f6;color:#4b5563;font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.04em;padding:10px 12px;text-align:left;border-top:2px solid var(--osl-border-color, #e5e7eb);border-right:1px solid var(--osl-border-color, #e5e7eb);white-space:nowrap;vertical-align:middle}.osl-fg-footer-td:last-child{border-right:none}.osl-fg-pagination{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 16px;border-top:1px solid var(--osl-border-color, #e5e7eb);background:#f9fafb;flex-shrink:0;flex-wrap:wrap}.osl-fg-pagination__info{font-size:12px;color:#6b7280;white-space:nowrap;min-width:120px}.osl-fg-pagination__controls{display:flex;align-items:center;gap:3px;flex-wrap:nowrap}.osl-fg-pagination__size{display:flex;align-items:center;justify-content:flex-end;min-width:120px}.osl-fg-page-btn{display:inline-flex;align-items:center;justify-content:center;height:30px;padding:0 10px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:var(--osl-border-radius, 6px);background:#fff;color:#374151;font-size:13px;font-family:inherit;cursor:pointer;transition:background .12s,border-color .12s,color .12s;white-space:nowrap}.osl-fg-page-btn:hover:not(:disabled){background:#f3f4f6;border-color:#9ca3af}.osl-fg-page-btn:disabled{opacity:.35;cursor:not-allowed}.osl-fg-page-btn--nav{font-size:12px;color:#6b7280}.osl-fg-page-btn--num{min-width:30px;padding:0;font-size:13px}.osl-fg-page-btn--active{background:var(--osl-primary, #6366f1);border-color:var(--osl-primary, #6366f1);color:#fff;font-weight:600}.osl-fg-page-btn--active:hover:not(:disabled){background:var(--osl-primary-hover, #4f46e5);border-color:var(--osl-primary-hover, #4f46e5)}.osl-fg-page-ellipsis{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;color:#9ca3af;font-size:13px;pointer-events:none}.osl-fg-page-size{height:30px;padding:0 8px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:var(--osl-border-radius, 6px);background:#fff;color:#374151;font-size:12px;font-family:inherit;cursor:pointer;outline:none}.osl-fg-page-size:focus{border-color:var(--osl-primary, #6366f1)}.osl-fg-page-size:disabled{opacity:.4;cursor:not-allowed}.osl-fg-panel{flex:0 0 auto;display:flex;flex-direction:column;border-left:1px solid var(--osl-border-color, #e5e7eb);background:#fafafa;min-height:200px;overflow:hidden}.osl-fg-panel__header{display:flex;align-items:center;justify-content:space-between;padding:11px 14px 10px;border-bottom:1px solid var(--osl-border-color, #e5e7eb);background:#f3f4f6;flex-shrink:0}.osl-fg-panel__title{font-size:11px;font-weight:700;color:#374151;text-transform:uppercase;letter-spacing:.06em}.osl-fg-panel__count{font-size:11px;font-weight:600;color:#9ca3af;background:#e5e7eb;border-radius:10px;padding:1px 7px;line-height:18px}.osl-fg-panel__search{position:relative;padding:8px 10px;border-bottom:1px solid var(--osl-border-color, #e5e7eb);flex-shrink:0;background:#fff}.osl-fg-search-icon{position:absolute;left:20px;top:50%;transform:translateY(-50%);width:13px;height:13px;color:#d1d5db;pointer-events:none}.osl-fg-search-input{width:100%;height:30px;padding:0 26px 0 30px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:6px;background:#f9fafb;font-size:12px;font-family:inherit;color:#374151;outline:none;box-sizing:border-box;transition:border-color .15s,background .15s}.osl-fg-search-input:focus{border-color:var(--osl-primary, #6366f1);background:#fff}.osl-fg-search-input::placeholder{color:#d1d5db}.osl-fg-search-clear{position:absolute;right:16px;top:50%;transform:translateY(-50%);width:18px;height:18px;padding:0;border:none;background:transparent;color:#9ca3af;cursor:pointer;display:flex;align-items:center;justify-content:center;border-radius:3px}.osl-fg-search-clear svg{width:11px;height:11px}.osl-fg-search-clear:hover{color:#374151}.osl-fg-panel__list{flex:1 1 auto;min-height:0;overflow-y:auto;padding:8px;display:flex;flex-direction:column;gap:2px}.osl-fg-panel__empty{display:flex;flex-direction:column;align-items:center;gap:8px;padding:28px 12px;color:#c4c9d4;font-size:12px;text-align:center}.osl-fg-panel__empty svg{width:26px;height:26px;opacity:.5}.osl-fg-drag-item{display:flex;align-items:center;gap:9px;padding:8px 10px 8px 8px;border-radius:6px;cursor:grab;-webkit-user-select:none;user-select:none;transition:background .12s;background:transparent;position:relative}.osl-fg-drag-item:hover{background:#f0f4ff}.osl-fg-drag-item:hover .osl-fg-drag-item__grip{opacity:1}.osl-fg-drag-item:hover .osl-fg-drag-item__arrow{opacity:.5}.osl-fg-drag-item:active{cursor:grabbing}.osl-fg-drag-item__grip{flex-shrink:0;width:16px;opacity:.2;color:#6b7280;transition:opacity .15s;display:flex;align-items:center}.osl-fg-drag-item__grip svg{width:13px;height:13px}.osl-fg-drag-item__body{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:1px}.osl-fg-drag-item__label{font-size:13px;font-weight:500;color:#1f2937;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.4}.osl-fg-drag-item__desc{font-size:11px;color:#9ca3af;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.3}.osl-fg-drag-item__arrow{flex-shrink:0;opacity:0;color:var(--osl-primary, #6366f1);transition:opacity .15s}.osl-fg-drag-item__arrow svg{width:13px;height:13px}.osl-fg-item-placeholder{height:36px;border:1.5px dashed #d1d5db;border-radius:6px;background:#f9fafb}.osl-fg-drag-preview{padding:8px 16px;background:var(--osl-primary, #6366f1);color:#fff;border-radius:8px;box-shadow:0 6px 18px #6366f159;font-size:12px;font-weight:600;white-space:nowrap;pointer-events:none}.osl-fg-item-skeleton{height:36px;border-radius:6px;background:#e5e7eb;animation:osl-fg-pulse 1.4s ease-in-out infinite;flex-shrink:0}.osl-fg-load-more{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;height:32px;margin-top:4px;border:1px dashed var(--osl-border-color, #e5e7eb);border-radius:6px;background:transparent;color:#6b7280;font-size:12px;font-family:inherit;cursor:pointer;transition:border-color .15s,color .15s,background .15s;flex-shrink:0}.osl-fg-load-more:hover:not(:disabled){border-color:var(--osl-primary, #6366f1);color:var(--osl-primary, #6366f1);background:#f5f3ff}.osl-fg-load-more:disabled{opacity:.5;cursor:not-allowed}.osl-fg-load-more__spinner{width:12px;height:12px;border:2px solid currentColor;border-top-color:transparent;border-radius:50%;animation:osl-fg-spin .7s linear infinite;display:inline-block}@keyframes osl-fg-spin{to{transform:rotate(360deg)}}:host ::ng-deep .cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}:host ::ng-deep .cdk-drop-list-dragging .osl-fg-drag-item:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}\n"], dependencies: [{ kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.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$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i4$1.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i4$1.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i4$1.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "directive", type: i4$1.CdkDragPreview, selector: "ng-template[cdkDragPreview]", inputs: ["data", "matchSize"] }, { kind: "directive", type: i4$1.CdkDragPlaceholder, selector: "ng-template[cdkDragPlaceholder]", inputs: ["data"] }, { kind: "component", type: Oslinput, selector: "osl-input", inputs: ["label", "required", "disabled", "model", "type", "placeholder", "mask", "min", "max", "minLength", "maxLength", "prefixIcon", "suffixIcon", "suffixRef", "skeletonLoading", "skeletonTheme", "onlyChars", "isCapitalize", "decimalPortion"], outputs: ["modelChange", "changeEv"] }, { kind: "component", type: Osltextarea, selector: "osl-textarea", inputs: ["label", "rows", "required", "disabled", "model", "placeholder", "maxLength", "minLength", "characterCounter", "resize", "skeletonLoading", "skeletonTheme"], outputs: ["modelChange", "changeEv"] }, { kind: "component", type: OslSelect, selector: "osl-select", inputs: ["label", "required", "disabled", "model", "datasource", "displayField", "valueField", "placeholder", "loading", "clearable", "skeletonLoading", "skeletonTheme"], outputs: ["modelChange", "changeEv"] }, { kind: "component", type: OslRadio, selector: "osl-radio", inputs: ["label", "required", "disabled", "model", "datasource", "displayField", "valueField", "inline", "skeletonLoading", "skeletonTheme"], outputs: ["modelChange", "changeEv"] }, { kind: "component", type: OslSlideToggle, selector: "osl-slide-toggle", inputs: ["label", "disabled", "model", "labelPosition", "trueLabel", "falseLabel", "skeletonLoading", "skeletonTheme"], outputs: ["modelChange", "changeEv"] }, { kind: "component", type: OslAutocomplete, selector: "osl-autocomplete", inputs: ["label", "required", "disabled", "model", "datasource", "displayField", "valueField", "placeholder", "loading", "searchType", "methodName", "configMethodName", "service", "object", "skeletonLoading", "skeletonTheme", "isLister", "apiBody", "displayFn"], outputs: ["datasourceChange", "modelChange", "changeEv"] }, { kind: "component", type: OslFileUpload, selector: "osl-file-upload", inputs: ["label", "required", "disabled", "model", "accept", "multiple", "maxSize", "skeletonLoading", "skeletonTheme", "fileMode", "downloadFn"], outputs: ["modelChange", "changeEv"] }, { kind: "component", type: OslDatepicker, selector: "osl-datepicker", inputs: ["label", "required", "disabled", "model", "dateType", "placeholder", "minDate", "maxDate", "skeletonLoading", "skeletonTheme"], outputs: ["modelChange", "changeEv"] }, { kind: "component", type: OslDatetimepicker, selector: "osl-datetimepicker", inputs: ["label", "required", "disabled", "model", "placeholder", "minDatetime", "maxDatetime", "format", "displayFormat", "showSeconds", "enableMeridian", "skeletonLoading", "skeletonTheme"], outputs: ["modelChange", "changeEv"] }, { kind: "component", type: OslCheckbox, selector: "osl-checkbox", inputs: ["label", "disabled", "required", "model", "indeterminate", "skeletonLoading", "skeletonTheme"], outputs: ["modelChange", "changeEv"] }] });
|
|
4485
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: OslFormGrid, isStandalone: false, selector: "osl-form-grid", inputs: { columns: "columns", datasource: "datasource", isPaginated: "isPaginated", pageSize: "pageSize", canAdd: "canAdd", canDelete: "canDelete", loading: "loading", tableHeight: "tableHeight", footerColumns: "footerColumns", enableDragDrop: "enableDragDrop", draggingList: "draggingList", draggingColumns: "draggingColumns", leftPanelWidth: "leftPanelWidth", dragTitle: "dragTitle", showSearch: "showSearch", allowReorder: "allowReorder", allowRemove: "allowRemove", panelSearchType: "panelSearchType", panelLoading: "panelLoading", panelTotalRecords: "panelTotalRecords", panelPageSize: "panelPageSize" }, outputs: { datasourceChange: "datasourceChange", rowAdd: "rowAdd", rowDelete: "rowDelete", dragStarted: "dragStarted", dragEnded: "dragEnded", dragEntered: "dragEntered", dragExited: "dragExited", itemDropped: "itemDropped", itemMoved: "itemMoved", itemRemoved: "itemRemoved", itemSelected: "itemSelected", gridChanged: "gridChanged", panelSearch: "panelSearch", panelPageChange: "panelPageChange" }, ngImport: i0, template: "<div class=\"osl-fg-wrapper\" [class.osl-fg-wrapper--builder]=\"enableDragDrop\">\n\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n Available Controls panel (only in builder mode)\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n\n\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n Main grid area (always present)\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <div class=\"osl-fg-grid-area\">\n\n <!-- Panel-to-grid drop overlay: always in DOM so #gridDropZone is always resolvable.\n pointer-events: none by default; only activates visually when isDraggingFromPanel.\n CDK hit-tests by bounding rect, not pointer-events, so drops are captured regardless. -->\n <div class=\"osl-fg-table-drop-zone\"\n [class.osl-fg-table-drop-zone--active]=\"isDraggingFromPanel\"\n cdkDropList\n #gridDropZone=\"cdkDropList\"\n [cdkDropListSortingDisabled]=\"true\"\n [cdkDropListDisabled]=\"!enableDragDrop\"\n [cdkDropListData]=\"[]\"\n (cdkDropListDropped)=\"onPanelDrop($event)\">\n @if (isDraggingFromPanel) {\n <div class=\"osl-fg-drop-zone-badge\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\">\n <path d=\"M12 5v14M5 12l7 7 7-7\"/>\n </svg>\n Drop to Add\n </div>\n }\n </div>\n\n <div class=\"osl-fg-table-container\"\n [style.height]=\"tableHeight\"\n [class.osl-fg-table-container--drop-active]=\"isDragging && enableDragDrop\">\n\n <table class=\"osl-fg-table\">\n\n <!-- \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <thead class=\"osl-fg-thead\">\n <tr>\n @if (enableDragDrop && allowReorder) {\n <th class=\"osl-fg-th osl-fg-th--drag\"></th>\n }\n @if (hasActions) {\n <th class=\"osl-fg-th osl-fg-th--actions\">\n @if (canAdd) {\n <button class=\"osl-grid-icon-btn\" (click)=\"addRow()\" [disabled]=\"loading\" title=\"Add row\">\n <mat-icon>add</mat-icon>\n </button>\n }\n </th>\n }\n @for (col of columns; track col.key) {\n <th class=\"osl-fg-th\" [style.width]=\"col.width\">\n {{ col.displayName }}\n @if (colRequired(col)) {\n <span class=\"osl-fg-th-required\">*</span>\n }\n </th>\n }\n </tr>\n </thead>\n\n <!-- \u2500\u2500 Body \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <tbody\n cdkDropList\n #gridList=\"cdkDropList\"\n [cdkDropListData]=\"pagedData\"\n [cdkDropListDisabled]=\"!enableDragDrop\"\n (cdkDropListDropped)=\"onGridReorder($event)\">\n\n <!-- Loading state -->\n @if (loading) {\n @for (sk of skeletonRows; track $index) {\n <tr class=\"osl-fg-row osl-fg-row--skeleton\">\n @if (enableDragDrop && allowReorder) {\n <td class=\"osl-fg-td osl-fg-td--drag\"></td>\n }\n @if (hasActions) {\n <td class=\"osl-fg-td osl-fg-td--actions\">\n <div class=\"osl-fg-skeleton osl-fg-skeleton--sm\"></div>\n </td>\n }\n @for (col of columns; track col.key) {\n <td class=\"osl-fg-td\"><div class=\"osl-fg-skeleton\"></div></td>\n }\n </tr>\n }\n\n <!-- Empty state -->\n } @else if (pagedData.length === 0) {\n <tr>\n <td [attr.colspan]=\"columns.length + (hasActions ? 1 : 0) + (enableDragDrop && allowReorder ? 1 : 0)\"\n class=\"osl-fg-empty\">\n <div class=\"osl-fg-empty-inner\">\n @if (enableDragDrop && isDragging) {\n <svg width=\"36\" height=\"36\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\">\n <path d=\"M12 5v14M5 12l7 7 7-7\"/>\n </svg>\n <p>Drop here</p>\n } @else {\n <svg width=\"36\" height=\"36\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\">\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/>\n <line x1=\"3\" y1=\"9\" x2=\"21\" y2=\"9\"/><line x1=\"9\" y1=\"9\" x2=\"9\" y2=\"21\"/>\n </svg>\n <p>No rows yet.{{ canAdd ? ' Use + to get started.' : (enableDragDrop ? ' Drag a control here.' : '') }}</p>\n }\n </div>\n </td>\n </tr>\n\n <!-- Data rows -->\n } @else {\n @for (row of pagedData; track $index; let i = $index) {\n <tr class=\"osl-fg-row\"\n cdkDrag\n [cdkDragData]=\"row\"\n [cdkDragDisabled]=\"!enableDragDrop\"\n (cdkDragStarted)=\"onDragStartedEvent($event, row, 'grid')\"\n (cdkDragEnded)=\"onDragEndedEvent($event, row, 'grid')\"\n (click)=\"enableDragDrop ? selectGridItem(row, i) : null\">\n\n <!-- Drag handle -->\n @if (enableDragDrop && allowReorder) {\n <td class=\"osl-fg-td osl-fg-td--drag\">\n <span class=\"osl-fg-row-handle\" cdkDragHandle title=\"Drag to reorder\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <circle cx=\"9\" cy=\"5\" r=\"1.3\"/><circle cx=\"9\" cy=\"12\" r=\"1.3\"/><circle cx=\"9\" cy=\"19\" r=\"1.3\"/>\n <circle cx=\"15\" cy=\"5\" r=\"1.3\"/><circle cx=\"15\" cy=\"12\" r=\"1.3\"/><circle cx=\"15\" cy=\"19\" r=\"1.3\"/>\n </svg>\n </span>\n </td>\n }\n\n <!-- Action cell (remove / delete) -->\n @if (hasActions) {\n <td class=\"osl-fg-td osl-fg-td--actions\">\n @if (enableDragDrop && allowRemove) {\n <button class=\"osl-grid-icon-btn osl-grid-icon-btn--danger\"\n (click)=\"removeGridRow(i); $event.stopPropagation()\" title=\"Remove\">\n <mat-icon>close</mat-icon>\n </button>\n } @else if (!enableDragDrop && canDelete) {\n <button class=\"osl-grid-icon-btn osl-grid-icon-btn--danger\"\n (click)=\"deleteRow(i)\" title=\"Delete\">\n <mat-icon>delete_outline</mat-icon>\n </button>\n }\n </td>\n }\n\n <!-- Data cells -->\n @for (col of columns; track col.key) {\n <td class=\"osl-fg-td\" [style.width]=\"col.width\">\n @if (col.formElem) {\n <div class=\"osl-fg-cell-form\" [style.width]=\"col.width\">\n @switch (col.formElem.elementType) {\n\n @case ('textbox') {\n <osl-input\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [type]=\"col.formElem.inputType || 'text'\"\n [placeholder]=\"col.formElem.placeholder || col.displayName\"\n [mask]=\"col.formElem.mask || ''\"\n [min]=\"col.formElem.min ?? ''\"\n [max]=\"col.formElem.max ?? ''\"\n [minLength]=\"col.formElem.minLength ?? null\"\n [maxLength]=\"col.formElem.maxLength ?? null\"\n [prefixIcon]=\"col.formElem.prefixIcon || ''\"\n [suffixIcon]=\"col.formElem.suffixIcon || ''\"\n [decimalPortion]=\"col.formElem.decimalPortion ?? null\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-input>\n }\n\n @case ('textarea') {\n <osl-textarea\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [rows]=\"col.formElem.textareaRows || 3\"\n [placeholder]=\"col.formElem.placeholder || col.displayName\"\n [maxLength]=\"col.formElem.maxLength ?? null\"\n [minLength]=\"col.formElem.minLength ?? null\"\n [characterCounter]=\"!!col.formElem.characterCounter\"\n [resize]=\"col.formElem.resize || 'none'\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-textarea>\n }\n\n @case ('select') {\n <osl-select\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [datasource]=\"col.formElem.datasource || []\"\n [displayField]=\"col.formElem.displayField || ''\"\n [valueField]=\"col.formElem.valueField || ''\"\n [placeholder]=\"col.formElem.selectPlaceholder || col.displayName\"\n [loading]=\"isLoading(row, col.formElem)\"\n [clearable]=\"!!col.formElem.clearable\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? onSelectChange(col, row, i, $event) : null\">\n </osl-select>\n }\n\n @case ('autocomplete') {\n <osl-autocomplete\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [datasource]=\"col.formElem.datasource || []\"\n [displayField]=\"col.formElem.displayField || ''\"\n [valueField]=\"col.formElem.valueField || ''\"\n [placeholder]=\"col.formElem.autocompletePlaceholder || col.displayName\"\n [loading]=\"isLoading(row, col.formElem)\"\n [searchType]=\"col.formElem.searchType ? col.formElem.searchType : 'Local'\"\n [methodName]=\"col.formElem.apiMethod || ''\"\n [configMethodName]=\"col.formElem.apiConfigMethod || ''\"\n [service]=\"col.formElem.apiService\"\n [object]=\"col.formElem.objectName ? row[col.formElem.objectName] : null\"\n [isLister]=\"!!col.formElem.isListerAutocomplete\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? onSelectChange(col, row, i, $event) : null\">\n </osl-autocomplete>\n }\n\n @case ('radio') {\n <osl-radio\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [datasource]=\"col.formElem.datasource || []\"\n [displayField]=\"col.formElem.displayField || ''\"\n [valueField]=\"col.formElem.valueField || ''\"\n [inline]=\"!!col.formElem.inline\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-radio>\n }\n\n @case ('checkbox') {\n <osl-checkbox\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [indeterminate]=\"!!col.formElem.indeterminate\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-checkbox>\n }\n\n @case ('slide-toggle') {\n <osl-slide-toggle\n [label]=\"''\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [labelPosition]=\"col.formElem.labelPosition || 'after'\"\n [trueLabel]=\"col.formElem.trueLabel || ''\"\n [falseLabel]=\"col.formElem.falseLabel || ''\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-slide-toggle>\n }\n\n @case ('datepicker') {\n <osl-datepicker\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [dateType]=\"col.formElem.dateType || 'date'\"\n [placeholder]=\"col.formElem.placeholder || col.displayName\"\n [minDate]=\"col.formElem.minDate || ''\"\n [maxDate]=\"col.formElem.maxDate || ''\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-datepicker>\n }\n\n @case ('datetimepicker') {\n <osl-datetimepicker\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [placeholder]=\"col.formElem.placeholder || col.displayName\"\n [minDatetime]=\"col.formElem.minDatetime || ''\"\n [maxDatetime]=\"col.formElem.maxDatetime || ''\"\n [format]=\"col.formElem.datetimepickerFormat || 'YYYY-MM-DDTHH:mm'\"\n [showSeconds]=\"!!col.formElem.datetimepickerShowSeconds\"\n [enableMeridian]=\"!!col.formElem.datetimepickerEnableMeridian\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-datetimepicker>\n }\n\n @case ('file-uploader') {\n <osl-file-upload\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [accept]=\"col.formElem.accept || ''\"\n [multiple]=\"!!col.formElem.multiple\"\n [maxSize]=\"col.formElem.maxFileSize || 0\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-file-upload>\n }\n\n @case ('templateRef') {\n <ng-container *ngTemplateOutlet=\"col.formElem.templateRef; context: { $implicit: row, index: i }\"></ng-container>\n }\n\n }\n </div>\n } @else {\n <span class=\"osl-fg-cell-text\">{{ row[col.key] ?? '--' }}</span>\n }\n </td>\n }\n\n <!-- Row drag preview -->\n <ng-template cdkDragPreview>\n <div class=\"osl-fg-row-preview\">\n @for (col of columns; track col.key) {\n <span class=\"osl-fg-row-preview__cell\">{{ row[col.key] ?? '\u2014' }}</span>\n }\n </div>\n </ng-template>\n\n <!-- Drop placeholder: spans full row width with \"Drop Here\" -->\n <ng-template cdkDragPlaceholder>\n <tr class=\"osl-fg-row-placeholder\">\n <td [attr.colspan]=\"columns.length + (hasActions ? 1 : 0) + (enableDragDrop && allowReorder ? 1 : 0)\">\n <div class=\"osl-fg-drop-here\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M12 5v14M5 12l7 7 7-7\"/>\n </svg>\n Drop Here\n </div>\n </td>\n </tr>\n </ng-template>\n\n </tr>\n }\n }\n\n </tbody>\n\n <!-- \u2500\u2500 Footer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (footerColumns && footerColumns.length > 0 && datasource && datasource.length > 0) {\n <tfoot class=\"osl-fg-tfoot\">\n <tr class=\"osl-fg-footer-row\">\n @for (fcol of footerColumns; track $index) {\n <td\n [class]=\"'osl-fg-footer-td ' + (fcol.class || '')\"\n [attr.colspan]=\"fcol.colspan || 1\">\n {{ fcol.displayFn ? fcol.displayFn(datasource) : (fcol.display || '') }}\n </td>\n }\n </tr>\n </tfoot>\n }\n\n </table>\n </div>\n\n <!-- \u2500\u2500 Pagination (unchanged) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (isPaginated) {\n <div class=\"osl-fg-pagination\">\n\n <span class=\"osl-fg-pagination__info\">\n @if (loading) {\n Loading...\n } @else if (_total > 0) {\n {{ startRecord }}\u2013{{ endRecord }} of {{ _total }} rows\n } @else {\n No rows\n }\n </span>\n\n <div class=\"osl-fg-pagination__controls\">\n <button class=\"osl-fg-page-btn osl-fg-page-btn--nav\" (click)=\"goToPage(1)\" [disabled]=\"currentPage === 1 || loading\">\u00AB</button>\n <button class=\"osl-fg-page-btn osl-fg-page-btn--nav\" (click)=\"goToPage(currentPage - 1)\" [disabled]=\"currentPage === 1 || loading\">\u2039 Prev</button>\n\n @for (page of pageNumbers; track $index) {\n @if (page === -1) {\n <span class=\"osl-fg-page-ellipsis\">\u2026</span>\n } @else {\n <button\n class=\"osl-fg-page-btn osl-fg-page-btn--num\"\n [class.osl-fg-page-btn--active]=\"page === currentPage\"\n [disabled]=\"loading\"\n (click)=\"goToPage(page)\">\n {{ page }}\n </button>\n }\n }\n\n <button class=\"osl-fg-page-btn osl-fg-page-btn--nav\" (click)=\"goToPage(currentPage + 1)\" [disabled]=\"currentPage === totalPages || loading\">Next \u203A</button>\n <button class=\"osl-fg-page-btn osl-fg-page-btn--nav\" (click)=\"goToPage(totalPages)\" [disabled]=\"currentPage === totalPages || loading\">\u00BB</button>\n </div>\n\n <div class=\"osl-fg-pagination__size\">\n <select class=\"osl-fg-page-size\" [ngModel]=\"pageSize\" (ngModelChange)=\"onPageSizeChange($event)\" [disabled]=\"loading\">\n @for (opt of pageSizeOptions; track opt) {\n <option [value]=\"opt\">{{ opt }} per page</option>\n }\n </select>\n </div>\n\n </div>\n }\n\n </div><!-- /.osl-fg-grid-area -->\n @if (enableDragDrop) {\n <div class=\"osl-fg-panel\" [style.width.px]=\"leftPanelWidth\" [style.height]=\"tableHeight\">\n\n <!-- Panel header -->\n <div class=\"osl-fg-panel__header\">\n <span class=\"osl-fg-panel__title\">{{ dragTitle }}</span>\n @if (panelSearchType === 'Api' && panelTotalRecords > 0) {\n <span class=\"osl-fg-panel__count\">{{ draggingList.length }}/{{ panelTotalRecords }}</span>\n } @else if (filteredDraggingList.length > 0) {\n <span class=\"osl-fg-panel__count\">{{ filteredDraggingList.length }}</span>\n }\n </div>\n\n <!-- Search box -->\n @if (showSearch) {\n <div class=\"osl-fg-panel__search\">\n <svg class=\"osl-fg-search-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/><line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"/>\n </svg>\n <input\n class=\"osl-fg-search-input\"\n type=\"text\"\n [ngModel]=\"dragSearchQuery\"\n (ngModelChange)=\"onPanelSearchChange($event)\"\n placeholder=\"Search\u2026\"\n autocomplete=\"off\" />\n @if (dragSearchQuery) {\n <button class=\"osl-fg-search-clear\" (click)=\"clearPanelSearch()\" title=\"Clear\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/>\n </svg>\n </button>\n }\n </div>\n }\n\n <!-- Item list -->\n <div class=\"osl-fg-panel__list\"\n cdkDropList\n #availableList=\"cdkDropList\"\n [cdkDropListData]=\"filteredDraggingList\"\n [cdkDropListConnectedTo]=\"[gridDropZone]\"\n [cdkDropListSortingDisabled]=\"true\"\n (cdkDropListEntered)=\"onDragEnteredEvent($event, 'available')\"\n (cdkDropListExited)=\"onDragExitedEvent($event, 'available')\">\n\n <!-- Loading skeleton -->\n @if (panelLoading && draggingList.length === 0) {\n @for (sk of [1,2,3,4,5]; track sk) {\n <div class=\"osl-fg-item-skeleton\"></div>\n }\n\n } @else if (filteredDraggingList.length === 0) {\n <div class=\"osl-fg-panel__empty\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/><line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"/>\n </svg>\n <span>{{ dragSearchQuery ? 'No results found' : 'No results available' }}</span>\n </div>\n\n } @else {\n @for (item of filteredDraggingList; track item.id) {\n <div class=\"osl-fg-drag-item\"\n cdkDrag\n [cdkDragData]=\"item\"\n (cdkDragStarted)=\"onDragStartedEvent($event, item, 'available')\"\n (cdkDragEnded)=\"onDragEndedEvent($event, item, 'available')\">\n\n <!-- Grip handle -->\n <span class=\"osl-fg-drag-item__grip\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <circle cx=\"9\" cy=\"5\" r=\"1.4\"/><circle cx=\"9\" cy=\"12\" r=\"1.4\"/><circle cx=\"9\" cy=\"19\" r=\"1.4\"/>\n <circle cx=\"15\" cy=\"5\" r=\"1.4\"/><circle cx=\"15\" cy=\"12\" r=\"1.4\"/><circle cx=\"15\" cy=\"19\" r=\"1.4\"/>\n </svg>\n </span>\n\n <!-- Label + description -->\n <div class=\"osl-fg-drag-item__body\">\n <span class=\"osl-fg-drag-item__label\">{{ item.label }}</span>\n @if (item.description) {\n <span class=\"osl-fg-drag-item__desc\">{{ item.description }}</span>\n }\n </div>\n\n <!-- Drag-into-grid hint -->\n <span class=\"osl-fg-drag-item__arrow\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"/><polyline points=\"12 5 19 12 12 19\"/>\n </svg>\n </span>\n\n <!-- Placeholder shown in panel while card is dragged -->\n <div *cdkDragPlaceholder class=\"osl-fg-item-placeholder\"></div>\n\n <!-- Preview under the cursor while dragging -->\n <ng-template cdkDragPreview>\n <div class=\"osl-fg-drag-preview\">{{ item.label }}</div>\n </ng-template>\n\n </div>\n }\n\n <!-- Load More (API mode) -->\n @if (panelHasMore) {\n <button class=\"osl-fg-load-more\" (click)=\"loadMorePanelItems()\" [disabled]=\"panelLoading\">\n @if (panelLoading) {\n <span class=\"osl-fg-load-more__spinner\"></span> Loading\u2026\n } @else {\n Load More\n }\n </button>\n }\n\n <!-- Inline loading indicator while appending -->\n @if (panelLoading && draggingList.length > 0) {\n <div class=\"osl-fg-item-skeleton\"></div>\n }\n }\n\n </div>\n </div>\n }\n</div><!-- /.osl-fg-wrapper -->\n", styles: [".osl-fg-wrapper{display:flex;flex-direction:column;width:100%;font-size:var(--osl-text-font-size);font-family:inherit;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:var(--osl-border-radius, 8px);overflow:hidden;background:#fff}.osl-fg-wrapper--builder{flex-direction:row;overflow:visible;align-items:stretch;gap:14px;border:none;background:transparent}.osl-fg-grid-area{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;overflow:hidden;border-radius:inherit;position:relative}.osl-fg-wrapper--builder .osl-fg-grid-area{border:1px solid var(--osl-border-color, #e5e7eb);border-radius:var(--osl-border-radius, 8px);background:#fff}.osl-fg-table-drop-zone{position:absolute;inset:0;z-index:3;pointer-events:none;border-radius:inherit;transition:background .15s,border-color .15s}.osl-fg-table-drop-zone--active{border:2px dashed var(--osl-primary, #6366f1);background:#6366f108}.osl-fg-table-drop-zone--active.cdk-drop-list-receiving{background:#6366f114;border-style:solid}.osl-fg-drop-zone-badge{position:absolute;bottom:12px;left:50%;transform:translate(-50%);display:flex;align-items:center;gap:5px;padding:5px 13px;background:var(--osl-primary, #6366f1);color:#fff;border-radius:20px;font-size:11px;font-weight:600;white-space:nowrap;pointer-events:none;box-shadow:0 4px 12px #6366f159}.osl-fg-table-container{overflow-x:auto;overflow-y:auto;min-height:120px;flex:1 1 auto}.osl-fg-table-container--drop-active{outline:2px dashed var(--osl-primary, #6366f1);outline-offset:-2px}.osl-fg-table-container--drop-active .osl-fg-empty-inner{color:var(--osl-primary, #6366f1)}.osl-fg-table-container--drop-active .osl-fg-empty-inner svg{opacity:.7}.osl-fg-table{width:100%;border-collapse:collapse;table-layout:auto}.osl-fg-thead{position:sticky;top:0;z-index:2}.osl-fg-th{background:#f3f4f6;color:#4b5563;font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.04em;padding:10px 12px;text-align:left;border-bottom:2px solid var(--osl-border-color, #e5e7eb);border-right:1px solid var(--osl-border-color, #e5e7eb);white-space:nowrap;vertical-align:middle}.osl-fg-th:last-child{border-right:none}.osl-fg-th--actions{width:44px;min-width:44px;text-align:center;padding:6px}.osl-fg-th--drag{width:28px;min-width:28px;padding:0}.osl-fg-th-required{color:#ef4444;margin-left:2px;font-size:14px;line-height:1;vertical-align:middle}.osl-fg-row{border-bottom:1px solid #f0f0f0;transition:background .1s}.osl-fg-row:nth-child(odd){background:#f8fafc}.osl-fg-row:nth-child(2n){background:#fff}.osl-fg-row:hover{background:#eef2ff!important}.osl-fg-row:last-child{border-bottom:none}.osl-fg-row--skeleton{pointer-events:none}.osl-fg-td{padding:4px 10px;vertical-align:middle;border-right:1px solid #f0f0f0}.osl-fg-td:last-child{border-right:none}.osl-fg-td--actions{width:44px;min-width:44px;text-align:center;padding:4px 6px}.osl-fg-td--drag{width:28px;min-width:28px;padding:0;text-align:center}.osl-fg-cell-form{min-width:100px}.osl-fg-cell-form ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none!important}.osl-fg-cell-form ::ng-deep .mat-mdc-form-field,.osl-fg-cell-form ::ng-deep mat-form-field{width:100%}.osl-fg-cell-form ::ng-deep .mat-mdc-text-field-wrapper{padding-top:0}.osl-fg-cell-text{color:#111827;font-size:var(--osl-text-font-size)}.osl-grid-icon-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:1.5px solid var(--osl-border-color);border-radius:var(--osl-border-radius);background:transparent;color:#6b7280;cursor:pointer;padding:0;transition:border-color .15s,color .15s,background .15s;margin:0 2px}.osl-grid-icon-btn mat-icon{font-size:16px;width:16px;height:16px;line-height:16px}.osl-grid-icon-btn:hover{border-color:var(--osl-primary);color:var(--osl-primary);background:#6366f10f}.osl-grid-icon-btn--danger:hover{border-color:#ef4444;color:#ef4444;background:#ef44440f}.osl-fg-row-handle{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;cursor:grab;color:#d1d5db;border-radius:4px;transition:color .12s,background .12s}.osl-fg-row-handle svg{width:13px;height:13px}.osl-fg-row-handle:hover{color:#9ca3af;background:#f3f4f6}.osl-fg-row-handle:active{cursor:grabbing}.osl-fg-row-placeholder td{padding:4px 6px;background:transparent;border:none}.osl-fg-drop-here{display:flex;align-items:center;justify-content:center;gap:8px;height:46px;border:2px dashed var(--osl-primary, #6366f1);border-radius:6px;background:#eef2ff;color:var(--osl-primary, #6366f1);font-size:12px;font-weight:600;letter-spacing:.03em}.osl-fg-drop-here svg{width:16px;height:16px;flex-shrink:0}.osl-fg-row-preview{display:flex;align-items:center;gap:14px;padding:9px 16px;background:#fff;border:1.5px solid var(--osl-primary, #6366f1);border-radius:8px;box-shadow:0 8px 24px #6366f12e;font-size:12px;color:#374151;max-width:420px;white-space:nowrap;overflow:hidden}.osl-fg-row-preview__cell{overflow:hidden;text-overflow:ellipsis;max-width:130px}.osl-fg-row-preview__cell+.osl-fg-row-preview__cell{padding-left:14px;border-left:1px solid #e5e7eb}.osl-fg-empty{padding:40px 16px;text-align:center}.osl-fg-empty-inner{display:flex;flex-direction:column;align-items:center;gap:10px;color:#9ca3af}.osl-fg-empty-inner svg{opacity:.4}.osl-fg-empty-inner p{margin:0;font-size:13px}@keyframes osl-fg-pulse{0%,to{opacity:1}50%{opacity:.4}}.osl-fg-skeleton{height:36px;border-radius:6px;background:#e5e7eb;animation:osl-fg-pulse 1.4s ease-in-out infinite}.osl-fg-skeleton--sm{height:28px;width:28px;border-radius:4px;margin:0 auto}.osl-fg-tfoot{position:sticky;bottom:0;z-index:2}.osl-fg-footer-td{background:#f3f4f6;color:#4b5563;font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.04em;padding:10px 12px;text-align:left;border-top:2px solid var(--osl-border-color, #e5e7eb);border-right:1px solid var(--osl-border-color, #e5e7eb);white-space:nowrap;vertical-align:middle}.osl-fg-footer-td:last-child{border-right:none}.osl-fg-pagination{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 16px;border-top:1px solid var(--osl-border-color, #e5e7eb);background:#f9fafb;flex-shrink:0;flex-wrap:wrap}.osl-fg-pagination__info{font-size:12px;color:#6b7280;white-space:nowrap;min-width:120px}.osl-fg-pagination__controls{display:flex;align-items:center;gap:3px;flex-wrap:nowrap}.osl-fg-pagination__size{display:flex;align-items:center;justify-content:flex-end;min-width:120px}.osl-fg-page-btn{display:inline-flex;align-items:center;justify-content:center;height:30px;padding:0 10px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:var(--osl-border-radius, 6px);background:#fff;color:#374151;font-size:13px;font-family:inherit;cursor:pointer;transition:background .12s,border-color .12s,color .12s;white-space:nowrap}.osl-fg-page-btn:hover:not(:disabled){background:#f3f4f6;border-color:#9ca3af}.osl-fg-page-btn:disabled{opacity:.35;cursor:not-allowed}.osl-fg-page-btn--nav{font-size:12px;color:#6b7280}.osl-fg-page-btn--num{min-width:30px;padding:0;font-size:13px}.osl-fg-page-btn--active{background:var(--osl-primary, #6366f1);border-color:var(--osl-primary, #6366f1);color:#fff;font-weight:600}.osl-fg-page-btn--active:hover:not(:disabled){background:var(--osl-primary-hover, #4f46e5);border-color:var(--osl-primary-hover, #4f46e5)}.osl-fg-page-ellipsis{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;color:#9ca3af;font-size:13px;pointer-events:none}.osl-fg-page-size{height:30px;padding:0 8px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:var(--osl-border-radius, 6px);background:#fff;color:#374151;font-size:12px;font-family:inherit;cursor:pointer;outline:none}.osl-fg-page-size:focus{border-color:var(--osl-primary, #6366f1)}.osl-fg-page-size:disabled{opacity:.4;cursor:not-allowed}.osl-fg-panel{flex:0 0 auto;display:flex;flex-direction:column;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:var(--osl-border-radius, 8px);background:linear-gradient(180deg,#eceafb,#e2defa);box-shadow:0 2px 6px #0f172a0f,0 8px 20px -6px #6366f129;min-height:200px;overflow:hidden}.osl-fg-panel__header{display:flex;align-items:center;justify-content:space-between;padding:11px 14px 10px;border-bottom:1px solid rgba(99,102,241,.18);background:#6366f11a;flex-shrink:0}.osl-fg-panel__title{font-size:11px;font-weight:700;color:#374151;text-transform:uppercase;letter-spacing:.06em}.osl-fg-panel__count{font-size:11px;font-weight:600;color:#9ca3af;background:#e5e7eb;border-radius:10px;padding:1px 7px;line-height:18px}.osl-fg-panel__search{position:relative;padding:8px 10px;border-bottom:1px solid rgba(99,102,241,.14);flex-shrink:0;background:#ffffff8c}.osl-fg-search-icon{position:absolute;left:20px;top:50%;transform:translateY(-50%);width:13px;height:13px;color:#d1d5db;pointer-events:none}.osl-fg-search-input{width:100%;height:30px;padding:0 26px 0 30px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:6px;background:#f9fafb;font-size:12px;font-family:inherit;color:#374151;outline:none;box-sizing:border-box;transition:border-color .15s,background .15s}.osl-fg-search-input:focus{border-color:var(--osl-primary, #6366f1);background:#fff}.osl-fg-search-input::placeholder{color:#d1d5db}.osl-fg-search-clear{position:absolute;right:16px;top:50%;transform:translateY(-50%);width:18px;height:18px;padding:0;border:none;background:transparent;color:#9ca3af;cursor:pointer;display:flex;align-items:center;justify-content:center;border-radius:3px}.osl-fg-search-clear svg{width:11px;height:11px}.osl-fg-search-clear:hover{color:#374151}.osl-fg-panel__list{flex:1 1 auto;min-height:0;overflow-y:auto;padding:8px;display:flex;flex-direction:column;gap:2px}.osl-fg-panel__empty{display:flex;flex-direction:column;align-items:center;gap:8px;padding:28px 12px;color:#c4c9d4;font-size:12px;text-align:center}.osl-fg-panel__empty svg{width:26px;height:26px;opacity:.5}.osl-fg-drag-item{display:flex;align-items:center;gap:9px;padding:8px 10px 8px 8px;border-radius:6px;cursor:grab;-webkit-user-select:none;user-select:none;transition:background .12s;background:transparent;position:relative}.osl-fg-drag-item:hover{background:#f0f4ff}.osl-fg-drag-item:hover .osl-fg-drag-item__grip{opacity:1;background:#6366f12e;color:var(--osl-primary, #6366f1)}.osl-fg-drag-item:hover .osl-fg-drag-item__arrow{opacity:.5}.osl-fg-drag-item:active{cursor:grabbing}.osl-fg-drag-item__grip{flex-shrink:0;width:20px;height:20px;opacity:.85;color:var(--osl-primary, #6366f1);background:#6366f11a;border-radius:4px;transition:opacity .15s,background .15s,color .15s;display:flex;align-items:center;justify-content:center}.osl-fg-drag-item__grip svg{width:13px;height:13px}.osl-fg-drag-item__body{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:1px}.osl-fg-drag-item__label{font-size:13px;font-weight:500;color:#1f2937;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.4}.osl-fg-drag-item__desc{font-size:11px;color:#9ca3af;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.3}.osl-fg-drag-item__arrow{flex-shrink:0;opacity:0;color:var(--osl-primary, #6366f1);transition:opacity .15s}.osl-fg-drag-item__arrow svg{width:13px;height:13px}.osl-fg-item-placeholder{height:36px;border:1.5px dashed #d1d5db;border-radius:6px;background:#f9fafb}.osl-fg-drag-preview{padding:8px 16px;background:var(--osl-primary, #6366f1);color:#fff;border-radius:8px;box-shadow:0 6px 18px #6366f159;font-size:12px;font-weight:600;white-space:nowrap;pointer-events:none}.osl-fg-item-skeleton{height:36px;border-radius:6px;background:#e5e7eb;animation:osl-fg-pulse 1.4s ease-in-out infinite;flex-shrink:0}.osl-fg-load-more{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;height:32px;margin-top:4px;border:1px dashed var(--osl-border-color, #e5e7eb);border-radius:6px;background:transparent;color:#6b7280;font-size:12px;font-family:inherit;cursor:pointer;transition:border-color .15s,color .15s,background .15s;flex-shrink:0}.osl-fg-load-more:hover:not(:disabled){border-color:var(--osl-primary, #6366f1);color:var(--osl-primary, #6366f1);background:#f5f3ff}.osl-fg-load-more:disabled{opacity:.5;cursor:not-allowed}.osl-fg-load-more__spinner{width:12px;height:12px;border:2px solid currentColor;border-top-color:transparent;border-radius:50%;animation:osl-fg-spin .7s linear infinite;display:inline-block}@keyframes osl-fg-spin{to{transform:rotate(360deg)}}:host ::ng-deep .cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}:host ::ng-deep .cdk-drop-list-dragging .osl-fg-drag-item:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}\n"], dependencies: [{ kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.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$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i3$1.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i3$1.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i3$1.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "directive", type: i3$1.CdkDragPreview, selector: "ng-template[cdkDragPreview]", inputs: ["data", "matchSize"] }, { kind: "directive", type: i3$1.CdkDragPlaceholder, selector: "ng-template[cdkDragPlaceholder]", inputs: ["data"] }, { kind: "component", type: Oslinput, selector: "osl-input", inputs: ["label", "required", "disabled", "model", "type", "placeholder", "mask", "min", "max", "minLength", "maxLength", "prefixIcon", "suffixIcon", "suffixRef", "skeletonLoading", "skeletonTheme", "onlyChars", "isCapitalize", "decimalPortion"], outputs: ["modelChange", "changeEv"] }, { kind: "component", type: Osltextarea, selector: "osl-textarea", inputs: ["label", "rows", "required", "disabled", "model", "placeholder", "maxLength", "minLength", "characterCounter", "resize", "skeletonLoading", "skeletonTheme"], outputs: ["modelChange", "changeEv"] }, { kind: "component", type: OslSelect, selector: "osl-select", inputs: ["label", "required", "disabled", "model", "datasource", "displayField", "valueField", "placeholder", "loading", "clearable", "skeletonLoading", "skeletonTheme"], outputs: ["modelChange", "changeEv"] }, { kind: "component", type: OslRadio, selector: "osl-radio", inputs: ["label", "required", "disabled", "model", "datasource", "displayField", "valueField", "inline", "skeletonLoading", "skeletonTheme"], outputs: ["modelChange", "changeEv"] }, { kind: "component", type: OslSlideToggle, selector: "osl-slide-toggle", inputs: ["label", "disabled", "model", "labelPosition", "trueLabel", "falseLabel", "skeletonLoading", "skeletonTheme"], outputs: ["modelChange", "changeEv"] }, { kind: "component", type: OslAutocomplete, selector: "osl-autocomplete", inputs: ["label", "required", "disabled", "model", "datasource", "displayField", "valueField", "placeholder", "loading", "searchType", "methodName", "configMethodName", "service", "object", "skeletonLoading", "skeletonTheme", "isLister", "apiBody", "displayFn"], outputs: ["datasourceChange", "modelChange", "changeEv"] }, { kind: "component", type: OslFileUpload, selector: "osl-file-upload", inputs: ["label", "required", "disabled", "model", "accept", "multiple", "maxSize", "skeletonLoading", "skeletonTheme", "fileMode", "downloadFn"], outputs: ["modelChange", "changeEv"] }, { kind: "component", type: OslDatepicker, selector: "osl-datepicker", inputs: ["label", "required", "disabled", "model", "dateType", "placeholder", "minDate", "maxDate", "skeletonLoading", "skeletonTheme"], outputs: ["modelChange", "changeEv"] }, { kind: "component", type: OslDatetimepicker, selector: "osl-datetimepicker", inputs: ["label", "required", "disabled", "model", "placeholder", "minDatetime", "maxDatetime", "format", "displayFormat", "showSeconds", "enableMeridian", "skeletonLoading", "skeletonTheme"], outputs: ["modelChange", "changeEv"] }, { kind: "component", type: OslCheckbox, selector: "osl-checkbox", inputs: ["label", "disabled", "required", "model", "indeterminate", "skeletonLoading", "skeletonTheme"], outputs: ["modelChange", "changeEv"] }] });
|
|
4466
4486
|
}
|
|
4467
4487
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: OslFormGrid, decorators: [{
|
|
4468
4488
|
type: Component,
|
|
4469
|
-
args: [{ selector: 'osl-form-grid', standalone: false, template: "<div class=\"osl-fg-wrapper\" [class.osl-fg-wrapper--builder]=\"enableDragDrop\">\n\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n Available Controls panel (only in builder mode)\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n\n\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n Main grid area (always present)\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <div class=\"osl-fg-grid-area\">\n\n <!-- Panel-to-grid drop overlay: always in DOM so #gridDropZone is always resolvable.\n pointer-events: none by default; only activates visually when isDraggingFromPanel.\n CDK hit-tests by bounding rect, not pointer-events, so drops are captured regardless. -->\n <div class=\"osl-fg-table-drop-zone\"\n [class.osl-fg-table-drop-zone--active]=\"isDraggingFromPanel\"\n cdkDropList\n #gridDropZone=\"cdkDropList\"\n [cdkDropListSortingDisabled]=\"true\"\n [cdkDropListDisabled]=\"!enableDragDrop\"\n [cdkDropListData]=\"[]\"\n (cdkDropListDropped)=\"onPanelDrop($event)\">\n @if (isDraggingFromPanel) {\n <div class=\"osl-fg-drop-zone-badge\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\">\n <path d=\"M12 5v14M5 12l7 7 7-7\"/>\n </svg>\n Drop to Add\n </div>\n }\n </div>\n\n <div class=\"osl-fg-table-container\"\n [style.height]=\"tableHeight\"\n [class.osl-fg-table-container--drop-active]=\"isDragging && enableDragDrop\">\n\n <table class=\"osl-fg-table\">\n\n <!-- \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <thead class=\"osl-fg-thead\">\n <tr>\n @if (enableDragDrop && allowReorder) {\n <th class=\"osl-fg-th osl-fg-th--drag\"></th>\n }\n @if (hasActions) {\n <th class=\"osl-fg-th osl-fg-th--actions\">\n @if (canAdd) {\n <button class=\"osl-grid-icon-btn\" (click)=\"addRow()\" [disabled]=\"loading\" title=\"Add row\">\n <mat-icon>add</mat-icon>\n </button>\n }\n </th>\n }\n @for (col of columns; track col.key) {\n <th class=\"osl-fg-th\" [style.width]=\"col.width\">\n {{ col.displayName }}\n @if (colRequired(col)) {\n <span class=\"osl-fg-th-required\">*</span>\n }\n </th>\n }\n </tr>\n </thead>\n\n <!-- \u2500\u2500 Body \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <tbody\n cdkDropList\n #gridList=\"cdkDropList\"\n [cdkDropListData]=\"pagedData\"\n [cdkDropListDisabled]=\"!enableDragDrop\"\n (cdkDropListDropped)=\"onGridReorder($event)\">\n\n <!-- Loading state -->\n @if (loading) {\n @for (sk of skeletonRows; track $index) {\n <tr class=\"osl-fg-row osl-fg-row--skeleton\">\n @if (enableDragDrop && allowReorder) {\n <td class=\"osl-fg-td osl-fg-td--drag\"></td>\n }\n @if (hasActions) {\n <td class=\"osl-fg-td osl-fg-td--actions\">\n <div class=\"osl-fg-skeleton osl-fg-skeleton--sm\"></div>\n </td>\n }\n @for (col of columns; track col.key) {\n <td class=\"osl-fg-td\"><div class=\"osl-fg-skeleton\"></div></td>\n }\n </tr>\n }\n\n <!-- Empty state -->\n } @else if (pagedData.length === 0) {\n <tr>\n <td [attr.colspan]=\"columns.length + (hasActions ? 1 : 0) + (enableDragDrop && allowReorder ? 1 : 0)\"\n class=\"osl-fg-empty\">\n <div class=\"osl-fg-empty-inner\">\n @if (enableDragDrop && isDragging) {\n <svg width=\"36\" height=\"36\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\">\n <path d=\"M12 5v14M5 12l7 7 7-7\"/>\n </svg>\n <p>Drop here</p>\n } @else {\n <svg width=\"36\" height=\"36\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\">\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/>\n <line x1=\"3\" y1=\"9\" x2=\"21\" y2=\"9\"/><line x1=\"9\" y1=\"9\" x2=\"9\" y2=\"21\"/>\n </svg>\n <p>No rows yet.{{ canAdd ? ' Use + to get started.' : (enableDragDrop ? ' Drag a control here.' : '') }}</p>\n }\n </div>\n </td>\n </tr>\n\n <!-- Data rows -->\n } @else {\n @for (row of pagedData; track $index; let i = $index) {\n <tr class=\"osl-fg-row\"\n cdkDrag\n [cdkDragData]=\"row\"\n [cdkDragDisabled]=\"!enableDragDrop\"\n (cdkDragStarted)=\"onDragStartedEvent($event, row, 'grid')\"\n (cdkDragEnded)=\"onDragEndedEvent($event, row, 'grid')\"\n (click)=\"enableDragDrop ? selectGridItem(row, i) : null\">\n\n <!-- Drag handle -->\n @if (enableDragDrop && allowReorder) {\n <td class=\"osl-fg-td osl-fg-td--drag\">\n <span class=\"osl-fg-row-handle\" cdkDragHandle title=\"Drag to reorder\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <circle cx=\"9\" cy=\"5\" r=\"1.3\"/><circle cx=\"9\" cy=\"12\" r=\"1.3\"/><circle cx=\"9\" cy=\"19\" r=\"1.3\"/>\n <circle cx=\"15\" cy=\"5\" r=\"1.3\"/><circle cx=\"15\" cy=\"12\" r=\"1.3\"/><circle cx=\"15\" cy=\"19\" r=\"1.3\"/>\n </svg>\n </span>\n </td>\n }\n\n <!-- Action cell (remove / delete) -->\n @if (hasActions) {\n <td class=\"osl-fg-td osl-fg-td--actions\">\n @if (enableDragDrop && allowRemove) {\n <button class=\"osl-grid-icon-btn osl-grid-icon-btn--danger\"\n (click)=\"removeGridRow(i); $event.stopPropagation()\" title=\"Remove\">\n <mat-icon>close</mat-icon>\n </button>\n } @else if (!enableDragDrop && canDelete) {\n <button class=\"osl-grid-icon-btn osl-grid-icon-btn--danger\"\n (click)=\"deleteRow(i)\" title=\"Delete\">\n <mat-icon>delete_outline</mat-icon>\n </button>\n }\n </td>\n }\n\n <!-- Data cells -->\n @for (col of columns; track col.key) {\n <td class=\"osl-fg-td\" [style.width]=\"col.width\">\n @if (col.formElem) {\n <div class=\"osl-fg-cell-form\" [style.width]=\"col.width\">\n @switch (col.formElem.elementType) {\n\n @case ('textbox') {\n <osl-input\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [type]=\"col.formElem.inputType || 'text'\"\n [placeholder]=\"col.formElem.placeholder || col.displayName\"\n [mask]=\"col.formElem.mask || ''\"\n [min]=\"col.formElem.min ?? ''\"\n [max]=\"col.formElem.max ?? ''\"\n [minLength]=\"col.formElem.minLength ?? null\"\n [maxLength]=\"col.formElem.maxLength ?? null\"\n [prefixIcon]=\"col.formElem.prefixIcon || ''\"\n [suffixIcon]=\"col.formElem.suffixIcon || ''\"\n [decimalPortion]=\"col.formElem.decimalPortion ?? null\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-input>\n }\n\n @case ('textarea') {\n <osl-textarea\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [rows]=\"col.formElem.textareaRows || 3\"\n [placeholder]=\"col.formElem.placeholder || col.displayName\"\n [maxLength]=\"col.formElem.maxLength ?? null\"\n [minLength]=\"col.formElem.minLength ?? null\"\n [characterCounter]=\"!!col.formElem.characterCounter\"\n [resize]=\"col.formElem.resize || 'none'\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-textarea>\n }\n\n @case ('select') {\n <osl-select\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [datasource]=\"col.formElem.datasource || []\"\n [displayField]=\"col.formElem.displayField || ''\"\n [valueField]=\"col.formElem.valueField || ''\"\n [placeholder]=\"col.formElem.selectPlaceholder || col.displayName\"\n [loading]=\"isLoading(row, col.formElem)\"\n [clearable]=\"!!col.formElem.clearable\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? onSelectChange(col, row, i, $event) : null\">\n </osl-select>\n }\n\n @case ('autocomplete') {\n <osl-autocomplete\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [datasource]=\"col.formElem.datasource || []\"\n [displayField]=\"col.formElem.displayField || ''\"\n [valueField]=\"col.formElem.valueField || ''\"\n [placeholder]=\"col.formElem.autocompletePlaceholder || col.displayName\"\n [loading]=\"isLoading(row, col.formElem)\"\n [searchType]=\"col.formElem.searchType ? col.formElem.searchType : 'Local'\"\n [methodName]=\"col.formElem.apiMethod || ''\"\n [configMethodName]=\"col.formElem.apiConfigMethod || ''\"\n [service]=\"col.formElem.apiService\"\n [object]=\"col.formElem.objectName ? row[col.formElem.objectName] : null\"\n [isLister]=\"!!col.formElem.isListerAutocomplete\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? onSelectChange(col, row, i, $event) : null\">\n </osl-autocomplete>\n }\n\n @case ('radio') {\n <osl-radio\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [datasource]=\"col.formElem.datasource || []\"\n [displayField]=\"col.formElem.displayField || ''\"\n [valueField]=\"col.formElem.valueField || ''\"\n [inline]=\"!!col.formElem.inline\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-radio>\n }\n\n @case ('checkbox') {\n <osl-checkbox\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [indeterminate]=\"!!col.formElem.indeterminate\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-checkbox>\n }\n\n @case ('slide-toggle') {\n <osl-slide-toggle\n [label]=\"''\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [labelPosition]=\"col.formElem.labelPosition || 'after'\"\n [trueLabel]=\"col.formElem.trueLabel || ''\"\n [falseLabel]=\"col.formElem.falseLabel || ''\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-slide-toggle>\n }\n\n @case ('datepicker') {\n <osl-datepicker\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [dateType]=\"col.formElem.dateType || 'date'\"\n [placeholder]=\"col.formElem.placeholder || col.displayName\"\n [minDate]=\"col.formElem.minDate || ''\"\n [maxDate]=\"col.formElem.maxDate || ''\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-datepicker>\n }\n\n @case ('datetimepicker') {\n <osl-datetimepicker\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [placeholder]=\"col.formElem.placeholder || col.displayName\"\n [minDatetime]=\"col.formElem.minDatetime || ''\"\n [maxDatetime]=\"col.formElem.maxDatetime || ''\"\n [format]=\"col.formElem.datetimepickerFormat || 'YYYY-MM-DDTHH:mm'\"\n [showSeconds]=\"!!col.formElem.datetimepickerShowSeconds\"\n [enableMeridian]=\"!!col.formElem.datetimepickerEnableMeridian\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-datetimepicker>\n }\n\n @case ('file-uploader') {\n <osl-file-upload\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [accept]=\"col.formElem.accept || ''\"\n [multiple]=\"!!col.formElem.multiple\"\n [maxSize]=\"col.formElem.maxFileSize || 0\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-file-upload>\n }\n\n @case ('templateRef') {\n <ng-container *ngTemplateOutlet=\"col.formElem.templateRef; context: { $implicit: row, index: i }\"></ng-container>\n }\n\n }\n </div>\n } @else {\n <span class=\"osl-fg-cell-text\">{{ row[col.key] ?? '--' }}</span>\n }\n </td>\n }\n\n <!-- Row drag preview -->\n <ng-template cdkDragPreview>\n <div class=\"osl-fg-row-preview\">\n @for (col of columns; track col.key) {\n <span class=\"osl-fg-row-preview__cell\">{{ row[col.key] ?? '\u2014' }}</span>\n }\n </div>\n </ng-template>\n\n <!-- Drop placeholder: spans full row width with \"Drop Here\" -->\n <ng-template cdkDragPlaceholder>\n <tr class=\"osl-fg-row-placeholder\">\n <td [attr.colspan]=\"columns.length + (hasActions ? 1 : 0) + (enableDragDrop && allowReorder ? 1 : 0)\">\n <div class=\"osl-fg-drop-here\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M12 5v14M5 12l7 7 7-7\"/>\n </svg>\n Drop Here\n </div>\n </td>\n </tr>\n </ng-template>\n\n </tr>\n }\n }\n\n </tbody>\n\n <!-- \u2500\u2500 Footer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (footerColumns && footerColumns.length > 0 && datasource && datasource.length > 0) {\n <tfoot class=\"osl-fg-tfoot\">\n <tr class=\"osl-fg-footer-row\">\n @for (fcol of footerColumns; track $index) {\n <td\n [class]=\"'osl-fg-footer-td ' + (fcol.class || '')\"\n [attr.colspan]=\"fcol.colspan || 1\">\n {{ fcol.displayFn ? fcol.displayFn(datasource) : (fcol.display || '') }}\n </td>\n }\n </tr>\n </tfoot>\n }\n\n </table>\n </div>\n\n <!-- \u2500\u2500 Pagination (unchanged) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (isPaginated) {\n <div class=\"osl-fg-pagination\">\n\n <span class=\"osl-fg-pagination__info\">\n @if (loading) {\n Loading...\n } @else if (_total > 0) {\n {{ startRecord }}\u2013{{ endRecord }} of {{ _total }} rows\n } @else {\n No rows\n }\n </span>\n\n <div class=\"osl-fg-pagination__controls\">\n <button class=\"osl-fg-page-btn osl-fg-page-btn--nav\" (click)=\"goToPage(1)\" [disabled]=\"currentPage === 1 || loading\">\u00AB</button>\n <button class=\"osl-fg-page-btn osl-fg-page-btn--nav\" (click)=\"goToPage(currentPage - 1)\" [disabled]=\"currentPage === 1 || loading\">\u2039 Prev</button>\n\n @for (page of pageNumbers; track $index) {\n @if (page === -1) {\n <span class=\"osl-fg-page-ellipsis\">\u2026</span>\n } @else {\n <button\n class=\"osl-fg-page-btn osl-fg-page-btn--num\"\n [class.osl-fg-page-btn--active]=\"page === currentPage\"\n [disabled]=\"loading\"\n (click)=\"goToPage(page)\">\n {{ page }}\n </button>\n }\n }\n\n <button class=\"osl-fg-page-btn osl-fg-page-btn--nav\" (click)=\"goToPage(currentPage + 1)\" [disabled]=\"currentPage === totalPages || loading\">Next \u203A</button>\n <button class=\"osl-fg-page-btn osl-fg-page-btn--nav\" (click)=\"goToPage(totalPages)\" [disabled]=\"currentPage === totalPages || loading\">\u00BB</button>\n </div>\n\n <div class=\"osl-fg-pagination__size\">\n <select class=\"osl-fg-page-size\" [ngModel]=\"pageSize\" (ngModelChange)=\"onPageSizeChange($event)\" [disabled]=\"loading\">\n @for (opt of pageSizeOptions; track opt) {\n <option [value]=\"opt\">{{ opt }} per page</option>\n }\n </select>\n </div>\n\n </div>\n }\n\n </div><!-- /.osl-fg-grid-area -->\n @if (enableDragDrop) {\n <div class=\"osl-fg-panel\" [style.width.px]=\"leftPanelWidth\" [style.height]=\"tableHeight\">\n\n <!-- Panel header -->\n <div class=\"osl-fg-panel__header\">\n <span class=\"osl-fg-panel__title\">{{ dragTitle }}</span>\n @if (panelSearchType === 'Api' && panelTotalRecords > 0) {\n <span class=\"osl-fg-panel__count\">{{ draggingList.length }}/{{ panelTotalRecords }}</span>\n } @else if (filteredDraggingList.length > 0) {\n <span class=\"osl-fg-panel__count\">{{ filteredDraggingList.length }}</span>\n }\n </div>\n\n <!-- Search box -->\n @if (showSearch) {\n <div class=\"osl-fg-panel__search\">\n <svg class=\"osl-fg-search-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/><line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"/>\n </svg>\n <input\n class=\"osl-fg-search-input\"\n type=\"text\"\n [ngModel]=\"dragSearchQuery\"\n (ngModelChange)=\"onPanelSearchChange($event)\"\n placeholder=\"Search\u2026\"\n autocomplete=\"off\" />\n @if (dragSearchQuery) {\n <button class=\"osl-fg-search-clear\" (click)=\"clearPanelSearch()\" title=\"Clear\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/>\n </svg>\n </button>\n }\n </div>\n }\n\n <!-- Item list -->\n <div class=\"osl-fg-panel__list\"\n cdkDropList\n #availableList=\"cdkDropList\"\n [cdkDropListData]=\"filteredDraggingList\"\n [cdkDropListConnectedTo]=\"[gridDropZone]\"\n [cdkDropListSortingDisabled]=\"true\"\n (cdkDropListEntered)=\"onDragEnteredEvent($event, 'available')\"\n (cdkDropListExited)=\"onDragExitedEvent($event, 'available')\">\n\n <!-- Loading skeleton -->\n @if (panelLoading && draggingList.length === 0) {\n @for (sk of [1,2,3,4,5]; track sk) {\n <div class=\"osl-fg-item-skeleton\"></div>\n }\n\n } @else if (filteredDraggingList.length === 0) {\n <div class=\"osl-fg-panel__empty\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/><line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"/>\n </svg>\n <span>{{ dragSearchQuery ? 'No results found' : 'No results available' }}</span>\n </div>\n\n } @else {\n @for (item of filteredDraggingList; track item.id) {\n <div class=\"osl-fg-drag-item\"\n cdkDrag\n [cdkDragData]=\"item\"\n (cdkDragStarted)=\"onDragStartedEvent($event, item, 'available')\"\n (cdkDragEnded)=\"onDragEndedEvent($event, item, 'available')\">\n\n <!-- Grip handle -->\n <span class=\"osl-fg-drag-item__grip\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <circle cx=\"9\" cy=\"5\" r=\"1.4\"/><circle cx=\"9\" cy=\"12\" r=\"1.4\"/><circle cx=\"9\" cy=\"19\" r=\"1.4\"/>\n <circle cx=\"15\" cy=\"5\" r=\"1.4\"/><circle cx=\"15\" cy=\"12\" r=\"1.4\"/><circle cx=\"15\" cy=\"19\" r=\"1.4\"/>\n </svg>\n </span>\n\n <!-- Label + description -->\n <div class=\"osl-fg-drag-item__body\">\n <span class=\"osl-fg-drag-item__label\">{{ item.label }}</span>\n @if (item.description) {\n <span class=\"osl-fg-drag-item__desc\">{{ item.description }}</span>\n }\n </div>\n\n <!-- Drag-into-grid hint -->\n <span class=\"osl-fg-drag-item__arrow\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"/><polyline points=\"12 5 19 12 12 19\"/>\n </svg>\n </span>\n\n <!-- Placeholder shown in panel while card is dragged -->\n <div *cdkDragPlaceholder class=\"osl-fg-item-placeholder\"></div>\n\n <!-- Preview under the cursor while dragging -->\n <ng-template cdkDragPreview>\n <div class=\"osl-fg-drag-preview\">{{ item.label }}</div>\n </ng-template>\n\n </div>\n }\n\n <!-- Load More (API mode) -->\n @if (panelHasMore) {\n <button class=\"osl-fg-load-more\" (click)=\"loadMorePanelItems()\" [disabled]=\"panelLoading\">\n @if (panelLoading) {\n <span class=\"osl-fg-load-more__spinner\"></span> Loading\u2026\n } @else {\n Load More\n }\n </button>\n }\n\n <!-- Inline loading indicator while appending -->\n @if (panelLoading && draggingList.length > 0) {\n <div class=\"osl-fg-item-skeleton\"></div>\n }\n }\n\n </div>\n </div>\n }\n</div><!-- /.osl-fg-wrapper -->\n", styles: [".osl-fg-wrapper{display:flex;flex-direction:column;width:100%;font-size:var(--osl-text-font-size);font-family:inherit;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:var(--osl-border-radius, 8px);overflow:hidden;background:#fff}.osl-fg-wrapper--builder{flex-direction:row;overflow:visible;align-items:stretch}.osl-fg-grid-area{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;overflow:hidden;border-radius:inherit;position:relative}.osl-fg-table-drop-zone{position:absolute;inset:0;z-index:3;pointer-events:none;border-radius:inherit;transition:background .15s,border-color .15s}.osl-fg-table-drop-zone--active{border:2px dashed var(--osl-primary, #6366f1);background:#6366f108}.osl-fg-table-drop-zone--active.cdk-drop-list-receiving{background:#6366f114;border-style:solid}.osl-fg-drop-zone-badge{position:absolute;bottom:12px;left:50%;transform:translate(-50%);display:flex;align-items:center;gap:5px;padding:5px 13px;background:var(--osl-primary, #6366f1);color:#fff;border-radius:20px;font-size:11px;font-weight:600;white-space:nowrap;pointer-events:none;box-shadow:0 4px 12px #6366f159}.osl-fg-table-container{overflow-x:auto;overflow-y:auto;min-height:120px;flex:1 1 auto}.osl-fg-table-container--drop-active{outline:2px dashed var(--osl-primary, #6366f1);outline-offset:-2px}.osl-fg-table-container--drop-active .osl-fg-empty-inner{color:var(--osl-primary, #6366f1)}.osl-fg-table-container--drop-active .osl-fg-empty-inner svg{opacity:.7}.osl-fg-table{width:100%;border-collapse:collapse;table-layout:auto}.osl-fg-thead{position:sticky;top:0;z-index:2}.osl-fg-th{background:#f3f4f6;color:#4b5563;font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.04em;padding:10px 12px;text-align:left;border-bottom:2px solid var(--osl-border-color, #e5e7eb);border-right:1px solid var(--osl-border-color, #e5e7eb);white-space:nowrap;vertical-align:middle}.osl-fg-th:last-child{border-right:none}.osl-fg-th--actions{width:44px;min-width:44px;text-align:center;padding:6px}.osl-fg-th--drag{width:28px;min-width:28px;padding:0}.osl-fg-th-required{color:#ef4444;margin-left:2px;font-size:14px;line-height:1;vertical-align:middle}.osl-fg-row{border-bottom:1px solid #f0f0f0;transition:background .1s}.osl-fg-row:nth-child(odd){background:#f8fafc}.osl-fg-row:nth-child(2n){background:#fff}.osl-fg-row:hover{background:#eef2ff!important}.osl-fg-row:last-child{border-bottom:none}.osl-fg-row--skeleton{pointer-events:none}.osl-fg-td{padding:4px 10px;vertical-align:middle;border-right:1px solid #f0f0f0}.osl-fg-td:last-child{border-right:none}.osl-fg-td--actions{width:44px;min-width:44px;text-align:center;padding:4px 6px}.osl-fg-td--drag{width:28px;min-width:28px;padding:0;text-align:center}.osl-fg-cell-form{min-width:100px}.osl-fg-cell-form ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none!important}.osl-fg-cell-form ::ng-deep .mat-mdc-form-field,.osl-fg-cell-form ::ng-deep mat-form-field{width:100%}.osl-fg-cell-form ::ng-deep .mat-mdc-text-field-wrapper{padding-top:0}.osl-fg-cell-text{color:#111827;font-size:var(--osl-text-font-size)}.osl-grid-icon-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:1.5px solid var(--osl-border-color);border-radius:var(--osl-border-radius);background:transparent;color:#6b7280;cursor:pointer;padding:0;transition:border-color .15s,color .15s,background .15s;margin:0 2px}.osl-grid-icon-btn mat-icon{font-size:16px;width:16px;height:16px;line-height:16px}.osl-grid-icon-btn:hover{border-color:var(--osl-primary);color:var(--osl-primary);background:#6366f10f}.osl-grid-icon-btn--danger:hover{border-color:#ef4444;color:#ef4444;background:#ef44440f}.osl-fg-row-handle{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;cursor:grab;color:#d1d5db;border-radius:4px;transition:color .12s,background .12s}.osl-fg-row-handle svg{width:13px;height:13px}.osl-fg-row-handle:hover{color:#9ca3af;background:#f3f4f6}.osl-fg-row-handle:active{cursor:grabbing}.osl-fg-row-placeholder td{padding:4px 6px;background:transparent;border:none}.osl-fg-drop-here{display:flex;align-items:center;justify-content:center;gap:8px;height:46px;border:2px dashed var(--osl-primary, #6366f1);border-radius:6px;background:#eef2ff;color:var(--osl-primary, #6366f1);font-size:12px;font-weight:600;letter-spacing:.03em}.osl-fg-drop-here svg{width:16px;height:16px;flex-shrink:0}.osl-fg-row-preview{display:flex;align-items:center;gap:14px;padding:9px 16px;background:#fff;border:1.5px solid var(--osl-primary, #6366f1);border-radius:8px;box-shadow:0 8px 24px #6366f12e;font-size:12px;color:#374151;max-width:420px;white-space:nowrap;overflow:hidden}.osl-fg-row-preview__cell{overflow:hidden;text-overflow:ellipsis;max-width:130px}.osl-fg-row-preview__cell+.osl-fg-row-preview__cell{padding-left:14px;border-left:1px solid #e5e7eb}.osl-fg-empty{padding:40px 16px;text-align:center}.osl-fg-empty-inner{display:flex;flex-direction:column;align-items:center;gap:10px;color:#9ca3af}.osl-fg-empty-inner svg{opacity:.4}.osl-fg-empty-inner p{margin:0;font-size:13px}@keyframes osl-fg-pulse{0%,to{opacity:1}50%{opacity:.4}}.osl-fg-skeleton{height:36px;border-radius:6px;background:#e5e7eb;animation:osl-fg-pulse 1.4s ease-in-out infinite}.osl-fg-skeleton--sm{height:28px;width:28px;border-radius:4px;margin:0 auto}.osl-fg-tfoot{position:sticky;bottom:0;z-index:2}.osl-fg-footer-td{background:#f3f4f6;color:#4b5563;font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.04em;padding:10px 12px;text-align:left;border-top:2px solid var(--osl-border-color, #e5e7eb);border-right:1px solid var(--osl-border-color, #e5e7eb);white-space:nowrap;vertical-align:middle}.osl-fg-footer-td:last-child{border-right:none}.osl-fg-pagination{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 16px;border-top:1px solid var(--osl-border-color, #e5e7eb);background:#f9fafb;flex-shrink:0;flex-wrap:wrap}.osl-fg-pagination__info{font-size:12px;color:#6b7280;white-space:nowrap;min-width:120px}.osl-fg-pagination__controls{display:flex;align-items:center;gap:3px;flex-wrap:nowrap}.osl-fg-pagination__size{display:flex;align-items:center;justify-content:flex-end;min-width:120px}.osl-fg-page-btn{display:inline-flex;align-items:center;justify-content:center;height:30px;padding:0 10px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:var(--osl-border-radius, 6px);background:#fff;color:#374151;font-size:13px;font-family:inherit;cursor:pointer;transition:background .12s,border-color .12s,color .12s;white-space:nowrap}.osl-fg-page-btn:hover:not(:disabled){background:#f3f4f6;border-color:#9ca3af}.osl-fg-page-btn:disabled{opacity:.35;cursor:not-allowed}.osl-fg-page-btn--nav{font-size:12px;color:#6b7280}.osl-fg-page-btn--num{min-width:30px;padding:0;font-size:13px}.osl-fg-page-btn--active{background:var(--osl-primary, #6366f1);border-color:var(--osl-primary, #6366f1);color:#fff;font-weight:600}.osl-fg-page-btn--active:hover:not(:disabled){background:var(--osl-primary-hover, #4f46e5);border-color:var(--osl-primary-hover, #4f46e5)}.osl-fg-page-ellipsis{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;color:#9ca3af;font-size:13px;pointer-events:none}.osl-fg-page-size{height:30px;padding:0 8px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:var(--osl-border-radius, 6px);background:#fff;color:#374151;font-size:12px;font-family:inherit;cursor:pointer;outline:none}.osl-fg-page-size:focus{border-color:var(--osl-primary, #6366f1)}.osl-fg-page-size:disabled{opacity:.4;cursor:not-allowed}.osl-fg-panel{flex:0 0 auto;display:flex;flex-direction:column;border-left:1px solid var(--osl-border-color, #e5e7eb);background:#fafafa;min-height:200px;overflow:hidden}.osl-fg-panel__header{display:flex;align-items:center;justify-content:space-between;padding:11px 14px 10px;border-bottom:1px solid var(--osl-border-color, #e5e7eb);background:#f3f4f6;flex-shrink:0}.osl-fg-panel__title{font-size:11px;font-weight:700;color:#374151;text-transform:uppercase;letter-spacing:.06em}.osl-fg-panel__count{font-size:11px;font-weight:600;color:#9ca3af;background:#e5e7eb;border-radius:10px;padding:1px 7px;line-height:18px}.osl-fg-panel__search{position:relative;padding:8px 10px;border-bottom:1px solid var(--osl-border-color, #e5e7eb);flex-shrink:0;background:#fff}.osl-fg-search-icon{position:absolute;left:20px;top:50%;transform:translateY(-50%);width:13px;height:13px;color:#d1d5db;pointer-events:none}.osl-fg-search-input{width:100%;height:30px;padding:0 26px 0 30px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:6px;background:#f9fafb;font-size:12px;font-family:inherit;color:#374151;outline:none;box-sizing:border-box;transition:border-color .15s,background .15s}.osl-fg-search-input:focus{border-color:var(--osl-primary, #6366f1);background:#fff}.osl-fg-search-input::placeholder{color:#d1d5db}.osl-fg-search-clear{position:absolute;right:16px;top:50%;transform:translateY(-50%);width:18px;height:18px;padding:0;border:none;background:transparent;color:#9ca3af;cursor:pointer;display:flex;align-items:center;justify-content:center;border-radius:3px}.osl-fg-search-clear svg{width:11px;height:11px}.osl-fg-search-clear:hover{color:#374151}.osl-fg-panel__list{flex:1 1 auto;min-height:0;overflow-y:auto;padding:8px;display:flex;flex-direction:column;gap:2px}.osl-fg-panel__empty{display:flex;flex-direction:column;align-items:center;gap:8px;padding:28px 12px;color:#c4c9d4;font-size:12px;text-align:center}.osl-fg-panel__empty svg{width:26px;height:26px;opacity:.5}.osl-fg-drag-item{display:flex;align-items:center;gap:9px;padding:8px 10px 8px 8px;border-radius:6px;cursor:grab;-webkit-user-select:none;user-select:none;transition:background .12s;background:transparent;position:relative}.osl-fg-drag-item:hover{background:#f0f4ff}.osl-fg-drag-item:hover .osl-fg-drag-item__grip{opacity:1}.osl-fg-drag-item:hover .osl-fg-drag-item__arrow{opacity:.5}.osl-fg-drag-item:active{cursor:grabbing}.osl-fg-drag-item__grip{flex-shrink:0;width:16px;opacity:.2;color:#6b7280;transition:opacity .15s;display:flex;align-items:center}.osl-fg-drag-item__grip svg{width:13px;height:13px}.osl-fg-drag-item__body{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:1px}.osl-fg-drag-item__label{font-size:13px;font-weight:500;color:#1f2937;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.4}.osl-fg-drag-item__desc{font-size:11px;color:#9ca3af;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.3}.osl-fg-drag-item__arrow{flex-shrink:0;opacity:0;color:var(--osl-primary, #6366f1);transition:opacity .15s}.osl-fg-drag-item__arrow svg{width:13px;height:13px}.osl-fg-item-placeholder{height:36px;border:1.5px dashed #d1d5db;border-radius:6px;background:#f9fafb}.osl-fg-drag-preview{padding:8px 16px;background:var(--osl-primary, #6366f1);color:#fff;border-radius:8px;box-shadow:0 6px 18px #6366f159;font-size:12px;font-weight:600;white-space:nowrap;pointer-events:none}.osl-fg-item-skeleton{height:36px;border-radius:6px;background:#e5e7eb;animation:osl-fg-pulse 1.4s ease-in-out infinite;flex-shrink:0}.osl-fg-load-more{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;height:32px;margin-top:4px;border:1px dashed var(--osl-border-color, #e5e7eb);border-radius:6px;background:transparent;color:#6b7280;font-size:12px;font-family:inherit;cursor:pointer;transition:border-color .15s,color .15s,background .15s;flex-shrink:0}.osl-fg-load-more:hover:not(:disabled){border-color:var(--osl-primary, #6366f1);color:var(--osl-primary, #6366f1);background:#f5f3ff}.osl-fg-load-more:disabled{opacity:.5;cursor:not-allowed}.osl-fg-load-more__spinner{width:12px;height:12px;border:2px solid currentColor;border-top-color:transparent;border-radius:50%;animation:osl-fg-spin .7s linear infinite;display:inline-block}@keyframes osl-fg-spin{to{transform:rotate(360deg)}}:host ::ng-deep .cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}:host ::ng-deep .cdk-drop-list-dragging .osl-fg-drag-item:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}\n"] }]
|
|
4489
|
+
args: [{ selector: 'osl-form-grid', standalone: false, template: "<div class=\"osl-fg-wrapper\" [class.osl-fg-wrapper--builder]=\"enableDragDrop\">\n\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n Available Controls panel (only in builder mode)\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n\n\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n Main grid area (always present)\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <div class=\"osl-fg-grid-area\">\n\n <!-- Panel-to-grid drop overlay: always in DOM so #gridDropZone is always resolvable.\n pointer-events: none by default; only activates visually when isDraggingFromPanel.\n CDK hit-tests by bounding rect, not pointer-events, so drops are captured regardless. -->\n <div class=\"osl-fg-table-drop-zone\"\n [class.osl-fg-table-drop-zone--active]=\"isDraggingFromPanel\"\n cdkDropList\n #gridDropZone=\"cdkDropList\"\n [cdkDropListSortingDisabled]=\"true\"\n [cdkDropListDisabled]=\"!enableDragDrop\"\n [cdkDropListData]=\"[]\"\n (cdkDropListDropped)=\"onPanelDrop($event)\">\n @if (isDraggingFromPanel) {\n <div class=\"osl-fg-drop-zone-badge\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\">\n <path d=\"M12 5v14M5 12l7 7 7-7\"/>\n </svg>\n Drop to Add\n </div>\n }\n </div>\n\n <div class=\"osl-fg-table-container\"\n [style.height]=\"tableHeight\"\n [class.osl-fg-table-container--drop-active]=\"isDragging && enableDragDrop\">\n\n <table class=\"osl-fg-table\">\n\n <!-- \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <thead class=\"osl-fg-thead\">\n <tr>\n @if (enableDragDrop && allowReorder) {\n <th class=\"osl-fg-th osl-fg-th--drag\"></th>\n }\n @if (hasActions) {\n <th class=\"osl-fg-th osl-fg-th--actions\">\n @if (canAdd) {\n <button class=\"osl-grid-icon-btn\" (click)=\"addRow()\" [disabled]=\"loading\" title=\"Add row\">\n <mat-icon>add</mat-icon>\n </button>\n }\n </th>\n }\n @for (col of columns; track col.key) {\n <th class=\"osl-fg-th\" [style.width]=\"col.width\">\n {{ col.displayName }}\n @if (colRequired(col)) {\n <span class=\"osl-fg-th-required\">*</span>\n }\n </th>\n }\n </tr>\n </thead>\n\n <!-- \u2500\u2500 Body \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <tbody\n cdkDropList\n #gridList=\"cdkDropList\"\n [cdkDropListData]=\"pagedData\"\n [cdkDropListDisabled]=\"!enableDragDrop\"\n (cdkDropListDropped)=\"onGridReorder($event)\">\n\n <!-- Loading state -->\n @if (loading) {\n @for (sk of skeletonRows; track $index) {\n <tr class=\"osl-fg-row osl-fg-row--skeleton\">\n @if (enableDragDrop && allowReorder) {\n <td class=\"osl-fg-td osl-fg-td--drag\"></td>\n }\n @if (hasActions) {\n <td class=\"osl-fg-td osl-fg-td--actions\">\n <div class=\"osl-fg-skeleton osl-fg-skeleton--sm\"></div>\n </td>\n }\n @for (col of columns; track col.key) {\n <td class=\"osl-fg-td\"><div class=\"osl-fg-skeleton\"></div></td>\n }\n </tr>\n }\n\n <!-- Empty state -->\n } @else if (pagedData.length === 0) {\n <tr>\n <td [attr.colspan]=\"columns.length + (hasActions ? 1 : 0) + (enableDragDrop && allowReorder ? 1 : 0)\"\n class=\"osl-fg-empty\">\n <div class=\"osl-fg-empty-inner\">\n @if (enableDragDrop && isDragging) {\n <svg width=\"36\" height=\"36\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\">\n <path d=\"M12 5v14M5 12l7 7 7-7\"/>\n </svg>\n <p>Drop here</p>\n } @else {\n <svg width=\"36\" height=\"36\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\">\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/>\n <line x1=\"3\" y1=\"9\" x2=\"21\" y2=\"9\"/><line x1=\"9\" y1=\"9\" x2=\"9\" y2=\"21\"/>\n </svg>\n <p>No rows yet.{{ canAdd ? ' Use + to get started.' : (enableDragDrop ? ' Drag a control here.' : '') }}</p>\n }\n </div>\n </td>\n </tr>\n\n <!-- Data rows -->\n } @else {\n @for (row of pagedData; track $index; let i = $index) {\n <tr class=\"osl-fg-row\"\n cdkDrag\n [cdkDragData]=\"row\"\n [cdkDragDisabled]=\"!enableDragDrop\"\n (cdkDragStarted)=\"onDragStartedEvent($event, row, 'grid')\"\n (cdkDragEnded)=\"onDragEndedEvent($event, row, 'grid')\"\n (click)=\"enableDragDrop ? selectGridItem(row, i) : null\">\n\n <!-- Drag handle -->\n @if (enableDragDrop && allowReorder) {\n <td class=\"osl-fg-td osl-fg-td--drag\">\n <span class=\"osl-fg-row-handle\" cdkDragHandle title=\"Drag to reorder\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <circle cx=\"9\" cy=\"5\" r=\"1.3\"/><circle cx=\"9\" cy=\"12\" r=\"1.3\"/><circle cx=\"9\" cy=\"19\" r=\"1.3\"/>\n <circle cx=\"15\" cy=\"5\" r=\"1.3\"/><circle cx=\"15\" cy=\"12\" r=\"1.3\"/><circle cx=\"15\" cy=\"19\" r=\"1.3\"/>\n </svg>\n </span>\n </td>\n }\n\n <!-- Action cell (remove / delete) -->\n @if (hasActions) {\n <td class=\"osl-fg-td osl-fg-td--actions\">\n @if (enableDragDrop && allowRemove) {\n <button class=\"osl-grid-icon-btn osl-grid-icon-btn--danger\"\n (click)=\"removeGridRow(i); $event.stopPropagation()\" title=\"Remove\">\n <mat-icon>close</mat-icon>\n </button>\n } @else if (!enableDragDrop && canDelete) {\n <button class=\"osl-grid-icon-btn osl-grid-icon-btn--danger\"\n (click)=\"deleteRow(i)\" title=\"Delete\">\n <mat-icon>delete_outline</mat-icon>\n </button>\n }\n </td>\n }\n\n <!-- Data cells -->\n @for (col of columns; track col.key) {\n <td class=\"osl-fg-td\" [style.width]=\"col.width\">\n @if (col.formElem) {\n <div class=\"osl-fg-cell-form\" [style.width]=\"col.width\">\n @switch (col.formElem.elementType) {\n\n @case ('textbox') {\n <osl-input\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [type]=\"col.formElem.inputType || 'text'\"\n [placeholder]=\"col.formElem.placeholder || col.displayName\"\n [mask]=\"col.formElem.mask || ''\"\n [min]=\"col.formElem.min ?? ''\"\n [max]=\"col.formElem.max ?? ''\"\n [minLength]=\"col.formElem.minLength ?? null\"\n [maxLength]=\"col.formElem.maxLength ?? null\"\n [prefixIcon]=\"col.formElem.prefixIcon || ''\"\n [suffixIcon]=\"col.formElem.suffixIcon || ''\"\n [decimalPortion]=\"col.formElem.decimalPortion ?? null\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-input>\n }\n\n @case ('textarea') {\n <osl-textarea\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [rows]=\"col.formElem.textareaRows || 3\"\n [placeholder]=\"col.formElem.placeholder || col.displayName\"\n [maxLength]=\"col.formElem.maxLength ?? null\"\n [minLength]=\"col.formElem.minLength ?? null\"\n [characterCounter]=\"!!col.formElem.characterCounter\"\n [resize]=\"col.formElem.resize || 'none'\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-textarea>\n }\n\n @case ('select') {\n <osl-select\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [datasource]=\"col.formElem.datasource || []\"\n [displayField]=\"col.formElem.displayField || ''\"\n [valueField]=\"col.formElem.valueField || ''\"\n [placeholder]=\"col.formElem.selectPlaceholder || col.displayName\"\n [loading]=\"isLoading(row, col.formElem)\"\n [clearable]=\"!!col.formElem.clearable\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? onSelectChange(col, row, i, $event) : null\">\n </osl-select>\n }\n\n @case ('autocomplete') {\n <osl-autocomplete\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [datasource]=\"col.formElem.datasource || []\"\n [displayField]=\"col.formElem.displayField || ''\"\n [valueField]=\"col.formElem.valueField || ''\"\n [placeholder]=\"col.formElem.autocompletePlaceholder || col.displayName\"\n [loading]=\"isLoading(row, col.formElem)\"\n [searchType]=\"col.formElem.searchType ? col.formElem.searchType : 'Local'\"\n [methodName]=\"col.formElem.apiMethod || ''\"\n [configMethodName]=\"col.formElem.apiConfigMethod || ''\"\n [service]=\"col.formElem.apiService\"\n [object]=\"col.formElem.objectName ? row[col.formElem.objectName] : null\"\n [isLister]=\"!!col.formElem.isListerAutocomplete\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? onSelectChange(col, row, i, $event) : null\">\n </osl-autocomplete>\n }\n\n @case ('radio') {\n <osl-radio\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [datasource]=\"col.formElem.datasource || []\"\n [displayField]=\"col.formElem.displayField || ''\"\n [valueField]=\"col.formElem.valueField || ''\"\n [inline]=\"!!col.formElem.inline\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-radio>\n }\n\n @case ('checkbox') {\n <osl-checkbox\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [indeterminate]=\"!!col.formElem.indeterminate\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-checkbox>\n }\n\n @case ('slide-toggle') {\n <osl-slide-toggle\n [label]=\"''\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [labelPosition]=\"col.formElem.labelPosition || 'after'\"\n [trueLabel]=\"col.formElem.trueLabel || ''\"\n [falseLabel]=\"col.formElem.falseLabel || ''\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-slide-toggle>\n }\n\n @case ('datepicker') {\n <osl-datepicker\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [dateType]=\"col.formElem.dateType || 'date'\"\n [placeholder]=\"col.formElem.placeholder || col.displayName\"\n [minDate]=\"col.formElem.minDate || ''\"\n [maxDate]=\"col.formElem.maxDate || ''\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-datepicker>\n }\n\n @case ('datetimepicker') {\n <osl-datetimepicker\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [placeholder]=\"col.formElem.placeholder || col.displayName\"\n [minDatetime]=\"col.formElem.minDatetime || ''\"\n [maxDatetime]=\"col.formElem.maxDatetime || ''\"\n [format]=\"col.formElem.datetimepickerFormat || 'YYYY-MM-DDTHH:mm'\"\n [showSeconds]=\"!!col.formElem.datetimepickerShowSeconds\"\n [enableMeridian]=\"!!col.formElem.datetimepickerEnableMeridian\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-datetimepicker>\n }\n\n @case ('file-uploader') {\n <osl-file-upload\n [label]=\"''\"\n [required]=\"false\"\n [disabled]=\"isDisabled(col.formElem,row,i)\"\n [accept]=\"col.formElem.accept || ''\"\n [multiple]=\"!!col.formElem.multiple\"\n [maxSize]=\"col.formElem.maxFileSize || 0\"\n [model]=\"row[col.key]\"\n (modelChange)=\"onCellChange(row, col, $event)\"\n (changeEv)=\"col.formElem.change ? col.formElem.change(row,i) : null\">\n </osl-file-upload>\n }\n\n @case ('templateRef') {\n <ng-container *ngTemplateOutlet=\"col.formElem.templateRef; context: { $implicit: row, index: i }\"></ng-container>\n }\n\n }\n </div>\n } @else {\n <span class=\"osl-fg-cell-text\">{{ row[col.key] ?? '--' }}</span>\n }\n </td>\n }\n\n <!-- Row drag preview -->\n <ng-template cdkDragPreview>\n <div class=\"osl-fg-row-preview\">\n @for (col of columns; track col.key) {\n <span class=\"osl-fg-row-preview__cell\">{{ row[col.key] ?? '\u2014' }}</span>\n }\n </div>\n </ng-template>\n\n <!-- Drop placeholder: spans full row width with \"Drop Here\" -->\n <ng-template cdkDragPlaceholder>\n <tr class=\"osl-fg-row-placeholder\">\n <td [attr.colspan]=\"columns.length + (hasActions ? 1 : 0) + (enableDragDrop && allowReorder ? 1 : 0)\">\n <div class=\"osl-fg-drop-here\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M12 5v14M5 12l7 7 7-7\"/>\n </svg>\n Drop Here\n </div>\n </td>\n </tr>\n </ng-template>\n\n </tr>\n }\n }\n\n </tbody>\n\n <!-- \u2500\u2500 Footer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (footerColumns && footerColumns.length > 0 && datasource && datasource.length > 0) {\n <tfoot class=\"osl-fg-tfoot\">\n <tr class=\"osl-fg-footer-row\">\n @for (fcol of footerColumns; track $index) {\n <td\n [class]=\"'osl-fg-footer-td ' + (fcol.class || '')\"\n [attr.colspan]=\"fcol.colspan || 1\">\n {{ fcol.displayFn ? fcol.displayFn(datasource) : (fcol.display || '') }}\n </td>\n }\n </tr>\n </tfoot>\n }\n\n </table>\n </div>\n\n <!-- \u2500\u2500 Pagination (unchanged) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (isPaginated) {\n <div class=\"osl-fg-pagination\">\n\n <span class=\"osl-fg-pagination__info\">\n @if (loading) {\n Loading...\n } @else if (_total > 0) {\n {{ startRecord }}\u2013{{ endRecord }} of {{ _total }} rows\n } @else {\n No rows\n }\n </span>\n\n <div class=\"osl-fg-pagination__controls\">\n <button class=\"osl-fg-page-btn osl-fg-page-btn--nav\" (click)=\"goToPage(1)\" [disabled]=\"currentPage === 1 || loading\">\u00AB</button>\n <button class=\"osl-fg-page-btn osl-fg-page-btn--nav\" (click)=\"goToPage(currentPage - 1)\" [disabled]=\"currentPage === 1 || loading\">\u2039 Prev</button>\n\n @for (page of pageNumbers; track $index) {\n @if (page === -1) {\n <span class=\"osl-fg-page-ellipsis\">\u2026</span>\n } @else {\n <button\n class=\"osl-fg-page-btn osl-fg-page-btn--num\"\n [class.osl-fg-page-btn--active]=\"page === currentPage\"\n [disabled]=\"loading\"\n (click)=\"goToPage(page)\">\n {{ page }}\n </button>\n }\n }\n\n <button class=\"osl-fg-page-btn osl-fg-page-btn--nav\" (click)=\"goToPage(currentPage + 1)\" [disabled]=\"currentPage === totalPages || loading\">Next \u203A</button>\n <button class=\"osl-fg-page-btn osl-fg-page-btn--nav\" (click)=\"goToPage(totalPages)\" [disabled]=\"currentPage === totalPages || loading\">\u00BB</button>\n </div>\n\n <div class=\"osl-fg-pagination__size\">\n <select class=\"osl-fg-page-size\" [ngModel]=\"pageSize\" (ngModelChange)=\"onPageSizeChange($event)\" [disabled]=\"loading\">\n @for (opt of pageSizeOptions; track opt) {\n <option [value]=\"opt\">{{ opt }} per page</option>\n }\n </select>\n </div>\n\n </div>\n }\n\n </div><!-- /.osl-fg-grid-area -->\n @if (enableDragDrop) {\n <div class=\"osl-fg-panel\" [style.width.px]=\"leftPanelWidth\" [style.height]=\"tableHeight\">\n\n <!-- Panel header -->\n <div class=\"osl-fg-panel__header\">\n <span class=\"osl-fg-panel__title\">{{ dragTitle }}</span>\n @if (panelSearchType === 'Api' && panelTotalRecords > 0) {\n <span class=\"osl-fg-panel__count\">{{ draggingList.length }}/{{ panelTotalRecords }}</span>\n } @else if (filteredDraggingList.length > 0) {\n <span class=\"osl-fg-panel__count\">{{ filteredDraggingList.length }}</span>\n }\n </div>\n\n <!-- Search box -->\n @if (showSearch) {\n <div class=\"osl-fg-panel__search\">\n <svg class=\"osl-fg-search-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/><line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"/>\n </svg>\n <input\n class=\"osl-fg-search-input\"\n type=\"text\"\n [ngModel]=\"dragSearchQuery\"\n (ngModelChange)=\"onPanelSearchChange($event)\"\n placeholder=\"Search\u2026\"\n autocomplete=\"off\" />\n @if (dragSearchQuery) {\n <button class=\"osl-fg-search-clear\" (click)=\"clearPanelSearch()\" title=\"Clear\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/>\n </svg>\n </button>\n }\n </div>\n }\n\n <!-- Item list -->\n <div class=\"osl-fg-panel__list\"\n cdkDropList\n #availableList=\"cdkDropList\"\n [cdkDropListData]=\"filteredDraggingList\"\n [cdkDropListConnectedTo]=\"[gridDropZone]\"\n [cdkDropListSortingDisabled]=\"true\"\n (cdkDropListEntered)=\"onDragEnteredEvent($event, 'available')\"\n (cdkDropListExited)=\"onDragExitedEvent($event, 'available')\">\n\n <!-- Loading skeleton -->\n @if (panelLoading && draggingList.length === 0) {\n @for (sk of [1,2,3,4,5]; track sk) {\n <div class=\"osl-fg-item-skeleton\"></div>\n }\n\n } @else if (filteredDraggingList.length === 0) {\n <div class=\"osl-fg-panel__empty\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/><line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"/>\n </svg>\n <span>{{ dragSearchQuery ? 'No results found' : 'No results available' }}</span>\n </div>\n\n } @else {\n @for (item of filteredDraggingList; track item.id) {\n <div class=\"osl-fg-drag-item\"\n cdkDrag\n [cdkDragData]=\"item\"\n (cdkDragStarted)=\"onDragStartedEvent($event, item, 'available')\"\n (cdkDragEnded)=\"onDragEndedEvent($event, item, 'available')\">\n\n <!-- Grip handle -->\n <span class=\"osl-fg-drag-item__grip\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <circle cx=\"9\" cy=\"5\" r=\"1.4\"/><circle cx=\"9\" cy=\"12\" r=\"1.4\"/><circle cx=\"9\" cy=\"19\" r=\"1.4\"/>\n <circle cx=\"15\" cy=\"5\" r=\"1.4\"/><circle cx=\"15\" cy=\"12\" r=\"1.4\"/><circle cx=\"15\" cy=\"19\" r=\"1.4\"/>\n </svg>\n </span>\n\n <!-- Label + description -->\n <div class=\"osl-fg-drag-item__body\">\n <span class=\"osl-fg-drag-item__label\">{{ item.label }}</span>\n @if (item.description) {\n <span class=\"osl-fg-drag-item__desc\">{{ item.description }}</span>\n }\n </div>\n\n <!-- Drag-into-grid hint -->\n <span class=\"osl-fg-drag-item__arrow\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"/><polyline points=\"12 5 19 12 12 19\"/>\n </svg>\n </span>\n\n <!-- Placeholder shown in panel while card is dragged -->\n <div *cdkDragPlaceholder class=\"osl-fg-item-placeholder\"></div>\n\n <!-- Preview under the cursor while dragging -->\n <ng-template cdkDragPreview>\n <div class=\"osl-fg-drag-preview\">{{ item.label }}</div>\n </ng-template>\n\n </div>\n }\n\n <!-- Load More (API mode) -->\n @if (panelHasMore) {\n <button class=\"osl-fg-load-more\" (click)=\"loadMorePanelItems()\" [disabled]=\"panelLoading\">\n @if (panelLoading) {\n <span class=\"osl-fg-load-more__spinner\"></span> Loading\u2026\n } @else {\n Load More\n }\n </button>\n }\n\n <!-- Inline loading indicator while appending -->\n @if (panelLoading && draggingList.length > 0) {\n <div class=\"osl-fg-item-skeleton\"></div>\n }\n }\n\n </div>\n </div>\n }\n</div><!-- /.osl-fg-wrapper -->\n", styles: [".osl-fg-wrapper{display:flex;flex-direction:column;width:100%;font-size:var(--osl-text-font-size);font-family:inherit;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:var(--osl-border-radius, 8px);overflow:hidden;background:#fff}.osl-fg-wrapper--builder{flex-direction:row;overflow:visible;align-items:stretch;gap:14px;border:none;background:transparent}.osl-fg-grid-area{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;overflow:hidden;border-radius:inherit;position:relative}.osl-fg-wrapper--builder .osl-fg-grid-area{border:1px solid var(--osl-border-color, #e5e7eb);border-radius:var(--osl-border-radius, 8px);background:#fff}.osl-fg-table-drop-zone{position:absolute;inset:0;z-index:3;pointer-events:none;border-radius:inherit;transition:background .15s,border-color .15s}.osl-fg-table-drop-zone--active{border:2px dashed var(--osl-primary, #6366f1);background:#6366f108}.osl-fg-table-drop-zone--active.cdk-drop-list-receiving{background:#6366f114;border-style:solid}.osl-fg-drop-zone-badge{position:absolute;bottom:12px;left:50%;transform:translate(-50%);display:flex;align-items:center;gap:5px;padding:5px 13px;background:var(--osl-primary, #6366f1);color:#fff;border-radius:20px;font-size:11px;font-weight:600;white-space:nowrap;pointer-events:none;box-shadow:0 4px 12px #6366f159}.osl-fg-table-container{overflow-x:auto;overflow-y:auto;min-height:120px;flex:1 1 auto}.osl-fg-table-container--drop-active{outline:2px dashed var(--osl-primary, #6366f1);outline-offset:-2px}.osl-fg-table-container--drop-active .osl-fg-empty-inner{color:var(--osl-primary, #6366f1)}.osl-fg-table-container--drop-active .osl-fg-empty-inner svg{opacity:.7}.osl-fg-table{width:100%;border-collapse:collapse;table-layout:auto}.osl-fg-thead{position:sticky;top:0;z-index:2}.osl-fg-th{background:#f3f4f6;color:#4b5563;font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.04em;padding:10px 12px;text-align:left;border-bottom:2px solid var(--osl-border-color, #e5e7eb);border-right:1px solid var(--osl-border-color, #e5e7eb);white-space:nowrap;vertical-align:middle}.osl-fg-th:last-child{border-right:none}.osl-fg-th--actions{width:44px;min-width:44px;text-align:center;padding:6px}.osl-fg-th--drag{width:28px;min-width:28px;padding:0}.osl-fg-th-required{color:#ef4444;margin-left:2px;font-size:14px;line-height:1;vertical-align:middle}.osl-fg-row{border-bottom:1px solid #f0f0f0;transition:background .1s}.osl-fg-row:nth-child(odd){background:#f8fafc}.osl-fg-row:nth-child(2n){background:#fff}.osl-fg-row:hover{background:#eef2ff!important}.osl-fg-row:last-child{border-bottom:none}.osl-fg-row--skeleton{pointer-events:none}.osl-fg-td{padding:4px 10px;vertical-align:middle;border-right:1px solid #f0f0f0}.osl-fg-td:last-child{border-right:none}.osl-fg-td--actions{width:44px;min-width:44px;text-align:center;padding:4px 6px}.osl-fg-td--drag{width:28px;min-width:28px;padding:0;text-align:center}.osl-fg-cell-form{min-width:100px}.osl-fg-cell-form ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none!important}.osl-fg-cell-form ::ng-deep .mat-mdc-form-field,.osl-fg-cell-form ::ng-deep mat-form-field{width:100%}.osl-fg-cell-form ::ng-deep .mat-mdc-text-field-wrapper{padding-top:0}.osl-fg-cell-text{color:#111827;font-size:var(--osl-text-font-size)}.osl-grid-icon-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:1.5px solid var(--osl-border-color);border-radius:var(--osl-border-radius);background:transparent;color:#6b7280;cursor:pointer;padding:0;transition:border-color .15s,color .15s,background .15s;margin:0 2px}.osl-grid-icon-btn mat-icon{font-size:16px;width:16px;height:16px;line-height:16px}.osl-grid-icon-btn:hover{border-color:var(--osl-primary);color:var(--osl-primary);background:#6366f10f}.osl-grid-icon-btn--danger:hover{border-color:#ef4444;color:#ef4444;background:#ef44440f}.osl-fg-row-handle{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;cursor:grab;color:#d1d5db;border-radius:4px;transition:color .12s,background .12s}.osl-fg-row-handle svg{width:13px;height:13px}.osl-fg-row-handle:hover{color:#9ca3af;background:#f3f4f6}.osl-fg-row-handle:active{cursor:grabbing}.osl-fg-row-placeholder td{padding:4px 6px;background:transparent;border:none}.osl-fg-drop-here{display:flex;align-items:center;justify-content:center;gap:8px;height:46px;border:2px dashed var(--osl-primary, #6366f1);border-radius:6px;background:#eef2ff;color:var(--osl-primary, #6366f1);font-size:12px;font-weight:600;letter-spacing:.03em}.osl-fg-drop-here svg{width:16px;height:16px;flex-shrink:0}.osl-fg-row-preview{display:flex;align-items:center;gap:14px;padding:9px 16px;background:#fff;border:1.5px solid var(--osl-primary, #6366f1);border-radius:8px;box-shadow:0 8px 24px #6366f12e;font-size:12px;color:#374151;max-width:420px;white-space:nowrap;overflow:hidden}.osl-fg-row-preview__cell{overflow:hidden;text-overflow:ellipsis;max-width:130px}.osl-fg-row-preview__cell+.osl-fg-row-preview__cell{padding-left:14px;border-left:1px solid #e5e7eb}.osl-fg-empty{padding:40px 16px;text-align:center}.osl-fg-empty-inner{display:flex;flex-direction:column;align-items:center;gap:10px;color:#9ca3af}.osl-fg-empty-inner svg{opacity:.4}.osl-fg-empty-inner p{margin:0;font-size:13px}@keyframes osl-fg-pulse{0%,to{opacity:1}50%{opacity:.4}}.osl-fg-skeleton{height:36px;border-radius:6px;background:#e5e7eb;animation:osl-fg-pulse 1.4s ease-in-out infinite}.osl-fg-skeleton--sm{height:28px;width:28px;border-radius:4px;margin:0 auto}.osl-fg-tfoot{position:sticky;bottom:0;z-index:2}.osl-fg-footer-td{background:#f3f4f6;color:#4b5563;font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.04em;padding:10px 12px;text-align:left;border-top:2px solid var(--osl-border-color, #e5e7eb);border-right:1px solid var(--osl-border-color, #e5e7eb);white-space:nowrap;vertical-align:middle}.osl-fg-footer-td:last-child{border-right:none}.osl-fg-pagination{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 16px;border-top:1px solid var(--osl-border-color, #e5e7eb);background:#f9fafb;flex-shrink:0;flex-wrap:wrap}.osl-fg-pagination__info{font-size:12px;color:#6b7280;white-space:nowrap;min-width:120px}.osl-fg-pagination__controls{display:flex;align-items:center;gap:3px;flex-wrap:nowrap}.osl-fg-pagination__size{display:flex;align-items:center;justify-content:flex-end;min-width:120px}.osl-fg-page-btn{display:inline-flex;align-items:center;justify-content:center;height:30px;padding:0 10px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:var(--osl-border-radius, 6px);background:#fff;color:#374151;font-size:13px;font-family:inherit;cursor:pointer;transition:background .12s,border-color .12s,color .12s;white-space:nowrap}.osl-fg-page-btn:hover:not(:disabled){background:#f3f4f6;border-color:#9ca3af}.osl-fg-page-btn:disabled{opacity:.35;cursor:not-allowed}.osl-fg-page-btn--nav{font-size:12px;color:#6b7280}.osl-fg-page-btn--num{min-width:30px;padding:0;font-size:13px}.osl-fg-page-btn--active{background:var(--osl-primary, #6366f1);border-color:var(--osl-primary, #6366f1);color:#fff;font-weight:600}.osl-fg-page-btn--active:hover:not(:disabled){background:var(--osl-primary-hover, #4f46e5);border-color:var(--osl-primary-hover, #4f46e5)}.osl-fg-page-ellipsis{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;color:#9ca3af;font-size:13px;pointer-events:none}.osl-fg-page-size{height:30px;padding:0 8px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:var(--osl-border-radius, 6px);background:#fff;color:#374151;font-size:12px;font-family:inherit;cursor:pointer;outline:none}.osl-fg-page-size:focus{border-color:var(--osl-primary, #6366f1)}.osl-fg-page-size:disabled{opacity:.4;cursor:not-allowed}.osl-fg-panel{flex:0 0 auto;display:flex;flex-direction:column;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:var(--osl-border-radius, 8px);background:linear-gradient(180deg,#eceafb,#e2defa);box-shadow:0 2px 6px #0f172a0f,0 8px 20px -6px #6366f129;min-height:200px;overflow:hidden}.osl-fg-panel__header{display:flex;align-items:center;justify-content:space-between;padding:11px 14px 10px;border-bottom:1px solid rgba(99,102,241,.18);background:#6366f11a;flex-shrink:0}.osl-fg-panel__title{font-size:11px;font-weight:700;color:#374151;text-transform:uppercase;letter-spacing:.06em}.osl-fg-panel__count{font-size:11px;font-weight:600;color:#9ca3af;background:#e5e7eb;border-radius:10px;padding:1px 7px;line-height:18px}.osl-fg-panel__search{position:relative;padding:8px 10px;border-bottom:1px solid rgba(99,102,241,.14);flex-shrink:0;background:#ffffff8c}.osl-fg-search-icon{position:absolute;left:20px;top:50%;transform:translateY(-50%);width:13px;height:13px;color:#d1d5db;pointer-events:none}.osl-fg-search-input{width:100%;height:30px;padding:0 26px 0 30px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:6px;background:#f9fafb;font-size:12px;font-family:inherit;color:#374151;outline:none;box-sizing:border-box;transition:border-color .15s,background .15s}.osl-fg-search-input:focus{border-color:var(--osl-primary, #6366f1);background:#fff}.osl-fg-search-input::placeholder{color:#d1d5db}.osl-fg-search-clear{position:absolute;right:16px;top:50%;transform:translateY(-50%);width:18px;height:18px;padding:0;border:none;background:transparent;color:#9ca3af;cursor:pointer;display:flex;align-items:center;justify-content:center;border-radius:3px}.osl-fg-search-clear svg{width:11px;height:11px}.osl-fg-search-clear:hover{color:#374151}.osl-fg-panel__list{flex:1 1 auto;min-height:0;overflow-y:auto;padding:8px;display:flex;flex-direction:column;gap:2px}.osl-fg-panel__empty{display:flex;flex-direction:column;align-items:center;gap:8px;padding:28px 12px;color:#c4c9d4;font-size:12px;text-align:center}.osl-fg-panel__empty svg{width:26px;height:26px;opacity:.5}.osl-fg-drag-item{display:flex;align-items:center;gap:9px;padding:8px 10px 8px 8px;border-radius:6px;cursor:grab;-webkit-user-select:none;user-select:none;transition:background .12s;background:transparent;position:relative}.osl-fg-drag-item:hover{background:#f0f4ff}.osl-fg-drag-item:hover .osl-fg-drag-item__grip{opacity:1;background:#6366f12e;color:var(--osl-primary, #6366f1)}.osl-fg-drag-item:hover .osl-fg-drag-item__arrow{opacity:.5}.osl-fg-drag-item:active{cursor:grabbing}.osl-fg-drag-item__grip{flex-shrink:0;width:20px;height:20px;opacity:.85;color:var(--osl-primary, #6366f1);background:#6366f11a;border-radius:4px;transition:opacity .15s,background .15s,color .15s;display:flex;align-items:center;justify-content:center}.osl-fg-drag-item__grip svg{width:13px;height:13px}.osl-fg-drag-item__body{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:1px}.osl-fg-drag-item__label{font-size:13px;font-weight:500;color:#1f2937;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.4}.osl-fg-drag-item__desc{font-size:11px;color:#9ca3af;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.3}.osl-fg-drag-item__arrow{flex-shrink:0;opacity:0;color:var(--osl-primary, #6366f1);transition:opacity .15s}.osl-fg-drag-item__arrow svg{width:13px;height:13px}.osl-fg-item-placeholder{height:36px;border:1.5px dashed #d1d5db;border-radius:6px;background:#f9fafb}.osl-fg-drag-preview{padding:8px 16px;background:var(--osl-primary, #6366f1);color:#fff;border-radius:8px;box-shadow:0 6px 18px #6366f159;font-size:12px;font-weight:600;white-space:nowrap;pointer-events:none}.osl-fg-item-skeleton{height:36px;border-radius:6px;background:#e5e7eb;animation:osl-fg-pulse 1.4s ease-in-out infinite;flex-shrink:0}.osl-fg-load-more{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;height:32px;margin-top:4px;border:1px dashed var(--osl-border-color, #e5e7eb);border-radius:6px;background:transparent;color:#6b7280;font-size:12px;font-family:inherit;cursor:pointer;transition:border-color .15s,color .15s,background .15s;flex-shrink:0}.osl-fg-load-more:hover:not(:disabled){border-color:var(--osl-primary, #6366f1);color:var(--osl-primary, #6366f1);background:#f5f3ff}.osl-fg-load-more:disabled{opacity:.5;cursor:not-allowed}.osl-fg-load-more__spinner{width:12px;height:12px;border:2px solid currentColor;border-top-color:transparent;border-radius:50%;animation:osl-fg-spin .7s linear infinite;display:inline-block}@keyframes osl-fg-spin{to{transform:rotate(360deg)}}:host ::ng-deep .cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}:host ::ng-deep .cdk-drop-list-dragging .osl-fg-drag-item:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}\n"] }]
|
|
4470
4490
|
}], propDecorators: { columns: [{
|
|
4471
4491
|
type: Input,
|
|
4472
4492
|
args: ['columns']
|
|
@@ -4613,7 +4633,7 @@ class OslAutocompleteLister {
|
|
|
4613
4633
|
this.dialogRef.close(event);
|
|
4614
4634
|
}
|
|
4615
4635
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: OslAutocompleteLister, deps: [{ token: i1.MatDialogRef }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
4616
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.9", type: OslAutocompleteLister, isStandalone: false, selector: "osl-autocomplete-lister", inputs: { data: "data" }, viewQueries: [{ propertyName: "setup", first: true, predicate: ["setup"], descendants: true }], ngImport: i0, template: "<div class=\"p-4\">\r\n <osl-setup #setup [loading]=\"loader\" [autoMode]=\"false\" [title]=\"autocompleteData?.title\" (sortChange)=\"onSortChange($event)\" [totalRecords]=\"recordCount\" (pageSizeChange)=\"onPageChange($event)\" (pageChange)=\"onPageChange($event)\" (onRowClick)=\"onRowClick($event)\" [columns]=\"column\" [datasource]=\"datasource\" [isLister]=\"true\"></osl-setup>\r\n \r\n</div>", styles: [""], dependencies: [{ kind: "component", type: OslSetup, selector: "osl-setup", inputs: ["title", "titleIcon", "columns", "datasource", "isPaginated", "pageSize", "autoMode", "tableHeight", "totalRecords", "loading", "dialogWidth", "formElements", "beforeDisplay", "onAddEditFn", "isLister", "canAdd", "canEdit", "canDelete", "moreMenuActions", "customFormFooter", "customHeaderTemp", "partialCustomHeaderTemp", "stateKey", "primaryKey", "onSave", "cardPageSize", "cardTemplate", "cardCol"], outputs: ["onSearch", "onAdd", "onEdit", "onDelete", "pageChange", "pageSizeChange", "sortChange", "onRowClick", "onStateRestored"] }] });
|
|
4636
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.9", type: OslAutocompleteLister, isStandalone: false, selector: "osl-autocomplete-lister", inputs: { data: "data" }, viewQueries: [{ propertyName: "setup", first: true, predicate: ["setup"], descendants: true }], ngImport: i0, template: "<div class=\"p-4\">\r\n <osl-setup #setup [loading]=\"loader\" [autoMode]=\"false\" [title]=\"autocompleteData?.title\" (sortChange)=\"onSortChange($event)\" [totalRecords]=\"recordCount\" (pageSizeChange)=\"onPageChange($event)\" (pageChange)=\"onPageChange($event)\" (onRowClick)=\"onRowClick($event)\" [columns]=\"column\" [datasource]=\"datasource\" [isLister]=\"true\"></osl-setup>\r\n \r\n</div>", styles: [""], dependencies: [{ kind: "component", type: OslSetup, selector: "osl-setup", inputs: ["title", "titleIcon", "columns", "datasource", "isPaginated", "pageSize", "autoMode", "tableHeight", "totalRecords", "loading", "dialogWidth", "isDraggable", "formElements", "beforeDisplay", "onAddEditFn", "isLister", "canAdd", "canEdit", "canDelete", "moreMenuActions", "customFormFooter", "customHeaderTemp", "partialCustomHeaderTemp", "stateKey", "primaryKey", "onSave", "cardPageSize", "cardTemplate", "cardCol"], outputs: ["onSearch", "onAdd", "onEdit", "onDelete", "pageChange", "pageSizeChange", "sortChange", "onRowClick", "onStateRestored"] }] });
|
|
4617
4637
|
}
|
|
4618
4638
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: OslAutocompleteLister, decorators: [{
|
|
4619
4639
|
type: Component,
|
|
@@ -5664,7 +5684,7 @@ class OslReportGrid {
|
|
|
5664
5684
|
asGroupRow = (row) => row;
|
|
5665
5685
|
asDataRow = (row) => row;
|
|
5666
5686
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: OslReportGrid, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5667
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: OslReportGrid, isStandalone: false, selector: "osl-report-grid", inputs: { columns: "columns", datasource: "datasource", loading: "loading", totalRecords: "totalRecords", autoMode: "autoMode", isPaginated: "isPaginated", pageSize: "pageSize", tableHeight: "tableHeight", striped: "striped", exportable: "exportable", rowHeight: "rowHeight", rowSelection: "rowSelection", showAggregates: "showAggregates", title: "title", pdfExportFromGrid: "pdfExportFromGrid", pdfConfig: "pdfConfig" }, outputs: { pageChange: "pageChange", pageSizeChange: "pageSizeChange", sortChange: "sortChange", rowClick: "rowClick", selectionChange: "selectionChange" }, host: { listeners: { "document:mousemove": "onMouseMove($event)", "document:mouseup": "onMouseUp()", "document:click": "onDocumentClick()" } }, providers: [DatePipe, DecimalPipe], viewQueries: [{ propertyName: "headerScrollRef", first: true, predicate: ["headerScrollRef"], descendants: true }, { propertyName: "bodyScrollRef", first: true, predicate: ["bodyScrollRef"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"osl-rg\" (click)=\"closeColumnMenu(); openFilterKey = null; $event.stopPropagation()\">\n\n <!-- \u2550\u2550 TOOLBAR \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <div class=\"osl-rg-toolbar\">\n <div class=\"osl-rg-toolbar__left\">\n @if (title) { <span class=\"osl-rg-title\">{{ title }}</span> }\n @if (!loading) {\n @if (_filteredTotal !== datasource.length) {\n <span class=\"osl-rg-filter-badge\">\n <span class=\"osl-rg-filter-badge__dot\"></span>\n {{ _filteredTotal | number }} of {{ datasource.length | number }} rows\n </span>\n } @else {\n <span class=\"osl-rg-record-count\">{{ datasource.length | number }} rows</span>\n }\n }\n </div>\n <div class=\"osl-rg-toolbar__right\">\n\n <!-- Global search -->\n <div class=\"osl-rg-global-search\">\n <svg class=\"osl-rg-global-search__icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.35-4.35\"/>\n </svg>\n <input class=\"osl-rg-global-search__input\" placeholder=\"Search all columns\u2026\"\n [(ngModel)]=\"globalSearch\" (ngModelChange)=\"currentPage=1; processData()\" />\n @if (globalSearch) {\n <button class=\"osl-rg-global-search__clear\" (click)=\"globalSearch=''; processData()\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n }\n </div>\n\n <!-- Sort active badge -->\n @if (sortStates.size > 0) {\n <button class=\"osl-rg-toolbar-btn osl-rg-toolbar-btn--active\" (click)=\"clearSort()\" title=\"Clear sort\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M11 5H4M11 9H4M11 13H4M18 4v16M15 7l3-3 3 3M15 17l3 3 3-3\"/>\n </svg>\n <span>{{ sortStates.size }} sort{{ sortStates.size > 1 ? 's' : '' }}</span>\n <svg class=\"rg-icon rg-icon--xs\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n }\n\n <!-- Filter active badge -->\n @if (hasAnyFilter) {\n <button class=\"osl-rg-toolbar-btn osl-rg-toolbar-btn--danger\" (click)=\"clearAllFilters()\" title=\"Clear all filters\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M22 3H2l8 9.46V19l4 2V12.46z\"/></svg>\n <span>Filtered</span>\n <svg class=\"rg-icon rg-icon--xs\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n }\n\n <!-- Group toggle -->\n <button class=\"osl-rg-toolbar-btn\" [class.osl-rg-toolbar-btn--on]=\"showGroupPanel\"\n (click)=\"showGroupPanel=!showGroupPanel\" title=\"Group panel\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"2\" y=\"7\" width=\"20\" height=\"14\" rx=\"2\"/><path d=\"M16 3H8a2 2 0 0 0-2 2v2h12V5a2 2 0 0 0-2-2z\"/>\n </svg>\n <span>Group</span>\n </button>\n\n <!-- Expand / collapse when grouped -->\n @if (activeGroups.length > 0) {\n <button class=\"osl-rg-toolbar-btn\" (click)=\"expandAll()\" title=\"Expand all\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m7 20 5-5 5 5M7 4l5 5 5-5\"/></svg>\n </button>\n <button class=\"osl-rg-toolbar-btn\" (click)=\"collapseAll()\" title=\"Collapse all\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m7 15 5-5 5 5M7 9l5 5 5 5\"/></svg>\n </button>\n }\n\n <!-- Auto-size all -->\n <button class=\"osl-rg-toolbar-btn\" (click)=\"autoSizeAll()\" title=\"Auto-size all columns\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M21 6H3M21 18H3\"/><path d=\"M8 3 3 6l5 3M16 3l5 3-5 3M8 15l-5 3 5 3M16 15l5 3-5 3\"/>\n </svg>\n <span>Auto-size</span>\n </button>\n\n <!-- Column config -->\n <button class=\"osl-rg-toolbar-btn\" [class.osl-rg-toolbar-btn--on]=\"showColumnConfig\"\n (click)=\"showColumnConfig=!showColumnConfig; $event.stopPropagation()\" title=\"Column settings\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M4 6h16M4 12h16M4 18h16\"/>\n <circle cx=\"8\" cy=\"6\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n <circle cx=\"16\" cy=\"12\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n <circle cx=\"8\" cy=\"18\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n </svg>\n <span>Columns</span>\n </button>\n\n <!-- Export Excel -->\n @if (exportable) {\n <button class=\"osl-rg-toolbar-btn osl-rg-toolbar-btn--excel\" (click)=\"exportExcel()\" title=\"Export to Excel\">\n <svg class=\"rg-icon rg-icon--excel\" viewBox=\"0 0 24 24\">\n <rect width=\"24\" height=\"24\" rx=\"3\" fill=\"#1D6F42\"/>\n <path d=\"M13.5 3H7a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V7.5L13.5 3z\" fill=\"#fff\" opacity=\".15\"/>\n <path d=\"M13.5 3v4.5H18L13.5 3z\" fill=\"#fff\" opacity=\".3\"/>\n <path d=\"M8 9.5l2.3 3.5L8 16.5h1.6l1.4-2.3 1.4 2.3H14l-2.3-3.5 2.3-3.5h-1.6l-1.4 2.3-1.4-2.3H8z\" fill=\"#fff\"/>\n </svg>\n <span>Export Excel</span>\n </button>\n }\n\n <!-- Export PDF -->\n @if (exportable && pdfExportFromGrid) {\n <button class=\"osl-rg-toolbar-btn osl-rg-toolbar-btn--pdf\" (click)=\"exportPdf()\" title=\"Export to PDF\">\n <svg class=\"rg-icon rg-icon--pdf\" viewBox=\"0 0 24 24\">\n <rect width=\"24\" height=\"24\" rx=\"3\" fill=\"#D32F2F\"/>\n <path d=\"M13.5 3H7a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V7.5L13.5 3z\" fill=\"#fff\" opacity=\".15\"/>\n <path d=\"M13.5 3v4.5H18L13.5 3z\" fill=\"#fff\" opacity=\".3\"/>\n <text x=\"4.5\" y=\"17.5\" fill=\"#fff\" font-size=\"6.5\" font-family=\"sans-serif\" font-weight=\"700\">PDF</text>\n </svg>\n <span>Export PDF</span>\n </button>\n }\n </div>\n </div>\n\n <!-- \u2550\u2550 GROUP PANEL \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @if (showGroupPanel) {\n <div class=\"osl-rg-group-panel\">\n <span class=\"osl-rg-group-panel__label\">Group by:</span>\n @if (activeGroups.length === 0) {\n <span class=\"osl-rg-group-panel__hint\">Use the \u22EE column menu \u2192 \"Group by this\"</span>\n }\n <div class=\"osl-rg-group-chips\" cdkDropList cdkDropListOrientation=\"horizontal\"\n [cdkDropListData]=\"activeGroups\" (cdkDropListDropped)=\"onGroupPanelDrop($event)\">\n @for (key of activeGroups; track key) {\n <div class=\"osl-rg-group-chip\" cdkDrag>\n <svg cdkDragHandle class=\"rg-drag-handle\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <circle cx=\"9\" cy=\"5\" r=\"1.5\"/><circle cx=\"15\" cy=\"5\" r=\"1.5\"/>\n <circle cx=\"9\" cy=\"12\" r=\"1.5\"/><circle cx=\"15\" cy=\"12\" r=\"1.5\"/>\n <circle cx=\"9\" cy=\"19\" r=\"1.5\"/><circle cx=\"15\" cy=\"19\" r=\"1.5\"/>\n </svg>\n <span>{{ getGroupColLabel(key) }}</span>\n <button class=\"osl-rg-group-chip__remove\" (click)=\"removeGroup(key)\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n </div>\n }\n </div>\n @if (activeGroups.length > 0) {\n <button class=\"osl-rg-group-panel__clear\" (click)=\"clearGroups()\">Clear groups</button>\n }\n </div>\n }\n\n <!-- \u2550\u2550 STICKY HEADER \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <div class=\"osl-rg-header-wrap\" #headerScrollRef>\n\n <!-- Double header \u2013 group row (only rendered when headerGroup is used on any column) -->\n @if (hasHeaderGroups) {\n <div class=\"osl-rg-header-row osl-rg-header-row--groups\" [style.minWidth.px]=\"totalWidth\">\n @if (rowSelection === 'multiple') {\n <div class=\"osl-rg-th osl-rg-th--checkbox osl-rg-th--group-span\"></div>\n }\n <div style=\"display:flex;flex:1;min-width:0;\">\n @for (group of computedHeaderGroups; track $index) {\n <div class=\"osl-rg-th osl-rg-th--group-span\"\n [class.osl-rg-th--group-span--labeled]=\"!!group.label\"\n [class.osl-rg-th--pinned]=\"group.isPinned\"\n [style.width.px]=\"group.width\"\n [style.minWidth.px]=\"group.width\"\n [style.left.px]=\"group.isPinned ? group.stickyLeft : null\"\n [style.position]=\"group.isPinned ? 'sticky' : 'relative'\"\n [style.zIndex]=\"group.isPinned ? 3 : 1\">\n @if (group.label) {\n <span class=\"osl-rg-th-group-label\">{{ group.label }}</span>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <div class=\"osl-rg-header-row\" [style.minWidth.px]=\"totalWidth\">\n\n <!-- Select-all checkbox -->\n @if (rowSelection === 'multiple') {\n <div class=\"osl-rg-th osl-rg-th--checkbox\">\n <input type=\"checkbox\" class=\"osl-rg-checkbox\"\n [checked]=\"allSelected\" [indeterminate]=\"someSelected\"\n (change)=\"toggleSelectAll($any($event.target).checked)\" />\n </div>\n }\n\n <!-- Column headers (drag-to-reorder) -->\n <div class=\"osl-rg-header-cols\" cdkDropList cdkDropListOrientation=\"horizontal\"\n [cdkDropListData]=\"_cols\" (cdkDropListDropped)=\"onColumnReorder($event)\"\n style=\"display:flex;flex:1;min-width:0;\">\n\n @for (col of visibleCols; track col.key) {\n <div class=\"osl-rg-th\"\n [class.osl-rg-th--pinned]=\"col._pinned\"\n [class.osl-rg-th--sorted]=\"getSortState(col.key)\"\n [class.osl-rg-th--filtered]=\"hasActiveFilter(col.key)\"\n [style.width.px]=\"col._width\"\n [style.minWidth.px]=\"col._width\"\n [style.left.px]=\"col._pinned ? col._stickyLeft : null\"\n [style.position]=\"col._pinned ? 'sticky' : 'relative'\"\n [style.zIndex]=\"col._pinned ? 3 : 1\"\n cdkDrag [cdkDragDisabled]=\"col._pinned\"\n (cdkDragStarted)=\"onColumnDragStarted()\"\n (cdkDragEnded)=\"onColumnDragEnded()\"\n (click)=\"onHeaderClick(col, $event)\">\n\n <div *cdkDragPlaceholder class=\"osl-rg-th-drag-placeholder\"></div>\n\n <!-- Pin badge -->\n @if (col._pinned) {\n <span class=\"osl-rg-th-pin-badge\" title=\"Pinned\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"9\" height=\"9\"><path d=\"M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z\"/></svg>\n </span>\n }\n\n <!-- Label row -->\n <div class=\"osl-rg-th-content\">\n <span class=\"osl-rg-th-label\" [title]=\"col.label\">{{ col.label }}</span>\n <div class=\"osl-rg-th-actions\">\n\n <!-- Sort indicator -->\n @if (col.sortable) {\n <span class=\"osl-rg-sort-icon\"\n [class.osl-rg-sort-icon--asc]=\"getSortState(col.key)?.asc === true\"\n [class.osl-rg-sort-icon--desc]=\"getSortState(col.key)?.asc === false\">\n @if (getSortState(col.key); as s) {\n @if (s.asc) {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 19V5m-7 7 7-7 7 7\"/></svg>\n } @else {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14m7-7-7 7-7-7\"/></svg>\n }\n @if (sortStates.size > 1) { <sup class=\"osl-rg-sort-badge\">{{ s.index + 1 }}</sup> }\n } @else {\n <svg class=\"osl-rg-sort-idle\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M8 9l4-4 4 4M16 15l-4 4-4-4\"/></svg>\n }\n </span>\n }\n\n <!-- Excel filter button -->\n @if (col.filterable) {\n <button class=\"osl-rg-filter-btn\" [class.osl-rg-filter-btn--active]=\"hasActiveFilter(col.key)\"\n (click)=\"openExcelFilter(col.key, $event)\" title=\"Filter\">\n @if (hasActiveFilter(col.key)) {\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M22 3H2l8 9.46V19l4 2V12.46z\"/></svg>\n } @else {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3\"/></svg>\n }\n </button>\n }\n\n <!-- Column menu -->\n <button class=\"osl-rg-col-menu-btn\" (click)=\"openColumnMenu(col.key, $event)\" title=\"Column options\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><circle cx=\"12\" cy=\"5\" r=\"2\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/><circle cx=\"12\" cy=\"19\" r=\"2\"/></svg>\n </button>\n\n </div>\n </div>\n\n <!-- Per-column search -->\n @if (col.searchable) {\n <div class=\"osl-rg-col-search\" (click)=\"$event.stopPropagation()\">\n <svg class=\"osl-rg-col-search__icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m18 18-3.5-3.5\"/>\n </svg>\n <input class=\"osl-rg-col-search__input\" [placeholder]=\"col.label + '\u2026'\"\n [ngModel]=\"columnSearch[col.key]\" (ngModelChange)=\"onColumnSearch(col.key, $event)\" />\n @if (columnSearch[col.key]) {\n <button class=\"osl-rg-col-search__clear\" (click)=\"columnSearch[col.key]=''; processData()\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n }\n </div>\n }\n\n <!-- Resize handle -->\n @if (col.resizable) {\n <div class=\"osl-rg-resize-handle\" (mousedown)=\"startResize(col, $event)\" (dblclick)=\"autoSizeColumn(col, $event)\" (click)=\"$event.stopPropagation()\"></div>\n }\n\n\n </div><!-- /osl-rg-th -->\n }\n\n </div><!-- /header-cols -->\n </div><!-- /header-row -->\n </div><!-- /header-wrap -->\n\n <!-- \u2550\u2550 SCROLLABLE BODY \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <div class=\"osl-rg-body\" [style.height]=\"tableHeight\" #bodyScrollRef (scroll)=\"onBodyScroll($event)\">\n <div class=\"osl-rg-inner\" [style.minWidth.px]=\"totalWidth\">\n\n <!-- Skeleton loading -->\n @if (loading) {\n @for (sk of skeletonRows; track $index) {\n <div class=\"osl-rg-row osl-rg-row--skeleton\" [style.height.px]=\"rowHeight\">\n @if (rowSelection === 'multiple') {\n <div class=\"osl-rg-td osl-rg-td--checkbox\"><div class=\"osl-rg-skel\" style=\"width:14px;height:14px;border-radius:3px;\"></div></div>\n }\n @for (col of visibleCols; track col.key) {\n <div class=\"osl-rg-td\" [style.width.px]=\"col._width\" [style.minWidth.px]=\"col._width\">\n <div class=\"osl-rg-skel\" [style.width]=\"$index % 3 === 0 ? '55%' : $index % 3 === 1 ? '80%' : '65%'\"></div>\n </div>\n }\n </div>\n }\n\n } @else if (isEmpty) {\n <!-- Empty state -->\n <div class=\"osl-rg-empty\">\n <div class=\"osl-rg-empty__icon\">\n <svg viewBox=\"0 0 48 48\" fill=\"none\" stroke=\"#d1d5db\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"6\" y=\"6\" width=\"36\" height=\"36\" rx=\"4\"/>\n <path d=\"M6 18h36M6 30h36M18 6v36\"/>\n </svg>\n </div>\n <p class=\"osl-rg-empty__title\">No records found</p>\n <p class=\"osl-rg-empty__sub\">\n @if (hasAnyFilter) { Try adjusting your filters or search terms }\n @else { No data to display }\n </p>\n @if (hasAnyFilter) {\n <button class=\"osl-rg-empty__action\" (click)=\"clearAllFilters()\">Clear all filters</button>\n }\n </div>\n\n } @else {\n <!-- Data rows -->\n @for (row of flatRows; track trackByRow($index, row)) {\n\n @if (row._type === 'group') {\n <div class=\"osl-rg-group-row\"\n [class.osl-rg-group-row--l0]=\"asGroupRow(row)._level === 0\"\n [class.osl-rg-group-row--l1]=\"asGroupRow(row)._level === 1\"\n [class.osl-rg-group-row--l2]=\"asGroupRow(row)._level >= 2\"\n [style.height.px]=\"rowHeight\"\n [style.paddingLeft.px]=\"asGroupRow(row)._level * 20 + 12\"\n (click)=\"toggleGroup(asGroupRow(row)._path)\">\n @if (asGroupRow(row)._expanded) {\n <svg class=\"osl-rg-group-chevron\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m6 9 6 6 6-6\"/></svg>\n } @else {\n <svg class=\"osl-rg-group-chevron\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m9 18 6-6-6-6\"/></svg>\n }\n <span class=\"osl-rg-group-row__key\">{{ asGroupRow(row)._colLabel }}:</span>\n <span class=\"osl-rg-group-row__value\">{{ asGroupRow(row)._label }}</span>\n <span class=\"osl-rg-group-row__count\">{{ asGroupRow(row)._count | number }} rows</span>\n </div>\n\n } @else {\n <div class=\"osl-rg-row\"\n [class.osl-rg-row--striped]=\"striped && asDataRow(row)._rowIndex % 2 === 1\"\n [class.osl-rg-row--selected]=\"selectedRows.has(asDataRow(row)._data)\"\n [style.height.px]=\"rowHeight\"\n (click)=\"onRowClick(asDataRow(row)._data, $event)\">\n\n @if (rowSelection === 'multiple') {\n <div class=\"osl-rg-td osl-rg-td--checkbox\">\n <input type=\"checkbox\" class=\"osl-rg-checkbox\"\n [checked]=\"selectedRows.has(asDataRow(row)._data)\"\n (change)=\"toggleRowSelect(asDataRow(row)._data, $event)\" />\n </div>\n }\n\n @for (col of visibleCols; track col.key) {\n <div class=\"osl-rg-td\"\n [class.osl-rg-td--pinned]=\"col._pinned\"\n [ngClass]=\"getCellClass(asDataRow(row)._data, col)\"\n [style.width.px]=\"col._width\"\n [style.minWidth.px]=\"col._width\"\n [style.left.px]=\"col._pinned ? col._stickyLeft : null\"\n [style.position]=\"col._pinned ? 'sticky' : 'relative'\"\n [style.zIndex]=\"col._pinned ? 2 : 0\"\n [style.textAlign]=\"col.align ?? 'left'\"\n (dblclick)=\"copyCell(getCellDisplay(asDataRow(row)._data, col), $event)\"\n [title]=\"getCellDisplay(asDataRow(row)._data, col)\">\n <span class=\"osl-rg-cell-text\">{{ getCellDisplay(asDataRow(row)._data, col) }}</span>\n </div>\n }\n\n </div>\n }\n\n }\n }\n\n </div><!-- /osl-rg-inner -->\n </div><!-- /osl-rg-body -->\n\n <!-- \u2550\u2550 AGGREGATE FOOTER \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @if (showAggregates && hasAnyAggregate() && !loading) {\n <div class=\"osl-rg-aggregate-row\" [style.minWidth.px]=\"totalWidth\">\n @if (rowSelection === 'multiple') { <div class=\"osl-rg-agg-cell osl-rg-agg-cell--checkbox\"></div> }\n @for (col of visibleCols; track col.key) {\n <div class=\"osl-rg-agg-cell\" [style.width.px]=\"col._width\" [style.minWidth.px]=\"col._width\" [style.textAlign]=\"col.align ?? 'left'\">\n @if (col.aggregate) {\n <span class=\"osl-rg-agg-label\">{{ col.aggregate | uppercase }}</span>\n <span class=\"osl-rg-agg-value\">{{ getAggregate(col) }}</span>\n }\n </div>\n }\n </div>\n }\n\n <!-- \u2550\u2550 PAGINATION \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @if (isPaginated) {\n <div class=\"osl-rg-pagination\">\n <span class=\"osl-rg-pagination__info\">\n @if (loading) { Loading\u2026 }\n @else if (_total > 0) { Showing <strong>{{ startRecord | number }}\u2013{{ endRecord | number }}</strong> of <strong>{{ _total | number }}</strong> records }\n @else { No records }\n </span>\n <div class=\"osl-rg-pagination__controls\">\n <button class=\"osl-rg-page-btn osl-rg-page-btn--nav\" (click)=\"goToPage(1)\" [disabled]=\"currentPage===1||loading\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"m11 17-5-5 5-5M18 17l-5-5 5-5\"/></svg>\n </button>\n <button class=\"osl-rg-page-btn osl-rg-page-btn--nav\" (click)=\"goToPage(currentPage-1)\" [disabled]=\"currentPage===1||loading\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"m15 18-6-6 6-6\"/></svg>\n Prev\n </button>\n @for (page of pageNumbers; track $index) {\n @if (page === -1) { <span class=\"osl-rg-page-ellipsis\">\u2026</span> }\n @else {\n <button class=\"osl-rg-page-btn osl-rg-page-btn--num\" [class.osl-rg-page-btn--active]=\"page===currentPage\"\n [disabled]=\"loading\" (click)=\"goToPage(page)\">{{ page }}</button>\n }\n }\n <button class=\"osl-rg-page-btn osl-rg-page-btn--nav\" (click)=\"goToPage(currentPage+1)\" [disabled]=\"currentPage===totalPages||loading\">\n Next\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"m9 18 6-6-6-6\"/></svg>\n </button>\n <button class=\"osl-rg-page-btn osl-rg-page-btn--nav\" (click)=\"goToPage(totalPages)\" [disabled]=\"currentPage===totalPages||loading\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"m13 17 5-5-5-5M6 17l5-5-5-5\"/></svg>\n </button>\n </div>\n <div class=\"osl-rg-pagination__size\">\n <span class=\"osl-rg-pagination__size-label\">Rows per page</span>\n <select class=\"osl-rg-page-size\" [ngModel]=\"pageSize\" (ngModelChange)=\"onPageSizeChange($event)\" [disabled]=\"loading\">\n @for (opt of pageSizeOptions; track opt) { <option [value]=\"opt\">{{ opt }}</option> }\n </select>\n </div>\n </div>\n }\n\n</div><!-- /osl-rg -->\n\n\n<!-- \u2550\u2550 EXCEL FILTER DROPDOWN (fixed, outside overflow containers) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (openFilterKey && excelFilterState[openFilterKey]) {\n <div class=\"osl-rg-excel-filter\"\n [style.top.px]=\"filterDropdownPos.top\"\n [style.left.px]=\"filterDropdownPos.left\"\n (click)=\"$event.stopPropagation()\">\n <div class=\"osl-rg-excel-filter__header\">\n <div class=\"osl-rg-excel-filter__title\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"12\" height=\"12\" style=\"flex-shrink:0;\"><path d=\"M22 3H2l8 9.46V19l4 2V12.46z\"/></svg>\n Filter: {{ getColumnByKey(openFilterKey)?.label }}\n </div>\n @if (hasActiveFilter(openFilterKey)) {\n <button class=\"osl-rg-excel-filter__clear-btn\" (click)=\"clearExcelFilter(openFilterKey!)\">Clear</button>\n }\n </div>\n <div class=\"osl-rg-excel-filter__search\">\n <input class=\"osl-rg-excel-filter__search-input\" placeholder=\"Search values\u2026\"\n [(ngModel)]=\"excelFilterState[openFilterKey].search\" />\n </div>\n <div class=\"osl-rg-excel-filter__select-all\">\n <label class=\"osl-rg-excel-filter__item osl-rg-excel-filter__item--all\">\n <input type=\"checkbox\" class=\"osl-rg-checkbox\" [checked]=\"isAllExcelChecked(openFilterKey)\"\n (change)=\"toggleAllExcelFilter(openFilterKey!, $any($event.target).checked)\" />\n <span class=\"osl-rg-excel-filter__item-label\">(Select All)</span>\n </label>\n </div>\n <div class=\"osl-rg-excel-filter__list\">\n @for (item of getFilteredExcelItems(openFilterKey); track item.value) {\n <label class=\"osl-rg-excel-filter__item\">\n <input type=\"checkbox\" class=\"osl-rg-checkbox\" [(ngModel)]=\"item.checked\" />\n <span class=\"osl-rg-excel-filter__item-label\">{{ item.label }}</span>\n </label>\n }\n </div>\n <div class=\"osl-rg-excel-filter__footer\">\n <button class=\"osl-rg-excel-filter__apply\" (click)=\"applyExcelFilter(openFilterKey!)\">Apply Filter</button>\n <button class=\"osl-rg-excel-filter__cancel\" (click)=\"openFilterKey=null\">Cancel</button>\n </div>\n </div>\n}\n\n\n<!-- \u2550\u2550 COLUMN CONTEXT MENU (fixed) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (columnMenuKey && getColumnByKey(columnMenuKey); as menuCol) {\n <div class=\"osl-rg-col-menu\"\n [style.top.px]=\"columnMenuPos.top\" [style.left.px]=\"columnMenuPos.left\"\n (click)=\"$event.stopPropagation()\">\n <div class=\"osl-rg-col-menu__header\">{{ menuCol.label }}</div>\n @if (menuCol.sortable) {\n <button class=\"osl-rg-col-menu__item\"\n (click)=\"sortStates.clear(); sortStates.set(menuCol.key,{key:menuCol.key,asc:true,index:0}); sortCounter=1; processData(); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 19V5m-7 7 7-7 7 7\"/></svg>\n Sort A \u2192 Z\n </button>\n <button class=\"osl-rg-col-menu__item\"\n (click)=\"sortStates.clear(); sortStates.set(menuCol.key,{key:menuCol.key,asc:false,index:0}); sortCounter=1; processData(); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14m7-7-7 7-7-7\"/></svg>\n Sort Z \u2192 A\n </button>\n }\n @if (menuCol.groupable) {\n <div class=\"osl-rg-col-menu__divider\"></div>\n <button class=\"osl-rg-col-menu__item\" (click)=\"addGroup(menuCol.key); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"7\" width=\"20\" height=\"14\" rx=\"2\"/><path d=\"M16 3H8a2 2 0 0 0-2 2v2h12V5a2 2 0 0 0-2-2z\"/></svg>\n Group by this\n </button>\n }\n <div class=\"osl-rg-col-menu__divider\"></div>\n <button class=\"osl-rg-col-menu__item\" (click)=\"toggleColumnPin(menuCol)\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z\"/></svg>\n {{ menuCol._pinned ? 'Unpin column' : 'Pin to left' }}\n </button>\n <button class=\"osl-rg-col-menu__item\" (click)=\"autoSizeColumn(menuCol); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 6H3M21 18H3\"/><path d=\"M8 3 3 6l5 3M16 3l5 3-5 3M8 15l-5 3 5 3M16 15l5 3-5 3\"/></svg>\n Auto-size column\n </button>\n <div class=\"osl-rg-col-menu__divider\"></div>\n <button class=\"osl-rg-col-menu__item osl-rg-col-menu__item--danger\" (click)=\"toggleColumnVisibility(menuCol); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24M1 1l22 22\"/>\n </svg>\n Hide column\n </button>\n </div>\n}\n\n\n<!-- \u2550\u2550 COLUMN CONFIG SIDE DRAWER \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (showColumnConfig) {\n <div class=\"osl-rg-config-backdrop\" (click)=\"showColumnConfig=false\"></div>\n <div class=\"osl-rg-config-panel\" (click)=\"$event.stopPropagation()\">\n <div class=\"osl-rg-config-panel__header\">\n <div class=\"osl-rg-config-panel__title\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"16\" height=\"16\">\n <path d=\"M4 6h16M4 12h16M4 18h16\"/>\n <circle cx=\"8\" cy=\"6\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n <circle cx=\"16\" cy=\"12\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n <circle cx=\"8\" cy=\"18\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n </svg>\n Column Settings\n </div>\n <button class=\"osl-rg-config-panel__close\" (click)=\"showColumnConfig=false\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n </div>\n <div class=\"osl-rg-config-panel__actions\">\n <button class=\"osl-rg-config-action\" (click)=\"autoSizeAll()\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"M21 6H3M21 18H3\"/><path d=\"M8 3 3 6l5 3M16 3l5 3-5 3\"/></svg>\n Auto-size all\n </button>\n <button class=\"osl-rg-config-action\" (click)=\"showAllColumns()\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></svg>\n Show all\n </button>\n </div>\n <div class=\"osl-rg-config-hint\">Drag to reorder \u00B7 Toggle to show/hide \u00B7 Pin to freeze</div>\n <div class=\"osl-rg-config-list\" cdkDropList (cdkDropListDropped)=\"onConfigColumnReorder($event)\">\n @for (col of _cols; track col.key) {\n <div class=\"osl-rg-config-item\" cdkDrag [class.osl-rg-config-item--hidden]=\"col._hidden\">\n <svg cdkDragHandle class=\"rg-drag-handle\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <circle cx=\"9\" cy=\"5\" r=\"1.5\"/><circle cx=\"15\" cy=\"5\" r=\"1.5\"/>\n <circle cx=\"9\" cy=\"12\" r=\"1.5\"/><circle cx=\"15\" cy=\"12\" r=\"1.5\"/>\n <circle cx=\"9\" cy=\"19\" r=\"1.5\"/><circle cx=\"15\" cy=\"19\" r=\"1.5\"/>\n </svg>\n <input type=\"checkbox\" class=\"osl-rg-checkbox\" [checked]=\"!col._hidden\" (change)=\"toggleColumnVisibility(col)\" />\n <span class=\"osl-rg-config-item__label\" [class.osl-rg-config-item__label--hidden]=\"col._hidden\">{{ col.label }}</span>\n <button class=\"osl-rg-config-item__pin-btn\" (click)=\"toggleColumnPin(col)\"\n [class.osl-rg-config-item__pin-btn--active]=\"col._pinned\"\n [title]=\"col._pinned ? 'Unpin' : 'Pin to left'\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"12\" height=\"12\"><path d=\"M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z\"/></svg>\n </button>\n </div>\n }\n </div>\n </div>\n}\n\n\n<!-- \u2550\u2550 COPY TOAST \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (copiedCell !== null) {\n <div class=\"osl-rg-copy-toast\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><polyline points=\"20 6 9 17 4 12\"/></svg>\n Copied to clipboard\n </div>\n}\n", styles: ["@keyframes rg-pulse{0%,to{opacity:1}50%{opacity:.4}}@keyframes rg-fade-up{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}@keyframes rg-slide-in{0%{opacity:0;transform:translate(20px)}to{opacity:1;transform:translate(0)}}.rg-icon{width:15px;height:15px;flex-shrink:0;display:block}.rg-icon--sm{width:14px;height:14px}.rg-icon--xs{width:12px;height:12px}.rg-icon--excel{width:18px;height:18px}.rg-drag-handle{width:14px;height:14px;flex-shrink:0;color:#d1d5db;cursor:grab;display:block}.rg-drag-handle:active{cursor:grabbing}.osl-rg{position:relative;display:flex;flex-direction:column;width:100%;font-family:inherit;font-size:13px;color:#111827;background:#fff;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:10px;overflow:hidden;box-shadow:0 2px 8px #00000012,0 1px 2px #0000000a;-webkit-user-select:none;user-select:none}.osl-rg-toolbar{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 14px;background:linear-gradient(135deg,#f8faff,#f3f4f6);border-bottom:1px solid var(--osl-border-color, #e5e7eb);flex-shrink:0;flex-wrap:wrap}.osl-rg-toolbar__left{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.osl-rg-toolbar__right{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.osl-rg-title{font-size:14px;font-weight:700;color:#111827;letter-spacing:-.01em}.osl-rg-record-count{display:inline-flex;align-items:center;gap:5px;font-size:12px;color:#9ca3af;font-weight:500}.osl-rg-record-count svg{color:#d1d5db}.osl-rg-filter-badge{display:inline-flex;align-items:center;gap:5px;padding:2px 9px 2px 6px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:12px;font-size:11px;color:#1d4ed8;font-weight:600}.osl-rg-filter-badge__dot{width:6px;height:6px;border-radius:50%;background:#2563eb;animation:rg-pulse 1.5s ease-in-out infinite;flex-shrink:0}.osl-rg-toolbar-btn{display:inline-flex;align-items:center;gap:5px;height:32px;padding:0 11px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:7px;background:#fff;color:#5a6478;font-size:12px;font-family:inherit;font-weight:500;cursor:pointer;white-space:nowrap;transition:all .15s ease}.osl-rg-toolbar-btn svg{flex-shrink:0}.osl-rg-toolbar-btn:hover{background:#f3f4f6;color:#1f2937;border-color:#c4c9d4;box-shadow:0 1px 3px #0000000f}.osl-rg-toolbar-btn--on{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8}.osl-rg-toolbar-btn--active{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8;font-weight:600}.osl-rg-toolbar-btn--danger{background:#fff5f5;border-color:#fca5a5;color:#dc2626}.osl-rg-toolbar-btn--danger:hover{background:#fee2e2;border-color:#f87171}.osl-rg-toolbar-btn--excel{background:linear-gradient(135deg,#1a7a3e,#1d6f42);border-color:#166534;color:#fff;font-weight:600;box-shadow:0 1px 3px #1665344d}.osl-rg-toolbar-btn--excel:hover{background:linear-gradient(135deg,#166534,#145a2c);border-color:#14532d;color:#fff;box-shadow:0 2px 6px #16653466}.osl-rg-toolbar-btn--pdf{background:linear-gradient(135deg,#e53935,#c62828);border-color:#b71c1c;color:#fff;font-weight:600;box-shadow:0 1px 3px #b71c1c4d}.osl-rg-toolbar-btn--pdf:hover{background:linear-gradient(135deg,#c62828,#ad1515);border-color:#ad1515;color:#fff;box-shadow:0 2px 6px #b71c1c66}.rg-icon--pdf{width:18px;height:18px}.osl-rg-global-search{position:relative;display:inline-flex;align-items:center}.osl-rg-global-search__icon{position:absolute;left:9px;width:14px;height:14px;color:#9ca3af;pointer-events:none;display:block}.osl-rg-global-search__input{height:32px;padding:0 30px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:7px;background:#fff;font-size:12.5px;font-family:inherit;color:#111827;outline:none;width:210px;transition:border-color .15s,width .2s,box-shadow .15s}.osl-rg-global-search__input::placeholder{color:#b0b7c3}.osl-rg-global-search__input:focus{border-color:var(--osl-primary, #2563eb);width:260px;box-shadow:0 0 0 3px #2563eb1a}.osl-rg-global-search__clear{position:absolute;right:6px;display:flex;align-items:center;justify-content:center;width:18px;height:18px;border:none;background:transparent;color:#9ca3af;cursor:pointer;padding:0;border-radius:4px;transition:all .12s}.osl-rg-global-search__clear svg{width:12px;height:12px}.osl-rg-global-search__clear:hover{color:#374151;background:#0000000d}.osl-rg-group-panel{display:flex;align-items:center;gap:8px;padding:6px 14px;background:linear-gradient(135deg,#fafbff,#f5f7ff);border-bottom:1px dashed #dde5ff;min-height:40px;flex-shrink:0;flex-wrap:wrap}.osl-rg-group-panel__label{display:flex;align-items:center;gap:5px;font-size:11px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.05em;white-space:nowrap;flex-shrink:0}.osl-rg-group-panel__label svg{color:#9ca3af}.osl-rg-group-panel__hint{font-size:11px;color:#b0b7c3;font-style:italic}.osl-rg-group-panel__clear{margin-left:auto;font-size:11px;color:#6b7280;background:none;border:none;cursor:pointer;text-decoration:underline;flex-shrink:0;padding:0;font-family:inherit}.osl-rg-group-panel__clear:hover{color:#dc2626}.osl-rg-group-chips{display:flex;gap:6px;flex-wrap:wrap;align-items:center;flex:1;min-height:26px}.osl-rg-group-chip{display:inline-flex;align-items:center;gap:5px;padding:3px 6px 3px 5px;background:linear-gradient(135deg,#dbeafe,#eff6ff);border:1px solid #bfdbfe;border-radius:20px;font-size:12px;font-weight:600;color:#1e40af;cursor:grab;-webkit-user-select:none;user-select:none;box-shadow:0 1px 2px #2563eb1a;transition:box-shadow .1s}.osl-rg-group-chip:active{cursor:grabbing;box-shadow:0 2px 6px #2563eb33}.osl-rg-group-chip .rg-drag-handle{color:#93c5fd}.osl-rg-group-chip__remove{display:flex;align-items:center;justify-content:center;width:16px;height:16px;border:none;background:transparent;cursor:pointer;padding:0;border-radius:50%;transition:all .1s}.osl-rg-group-chip__remove svg{width:10px;height:10px;color:#93c5fd}.osl-rg-group-chip__remove:hover{background:#dbeafe}.osl-rg-group-chip__remove:hover svg{color:#1e40af}.osl-rg-table-area{display:flex;flex-direction:column;flex:1 1 0;overflow:hidden;position:relative;min-height:0}.osl-rg-header-wrap{overflow:hidden;flex-shrink:0;border-bottom:2px solid #dde5ff;background:linear-gradient(180deg,#f0f4ff,#e8efff);z-index:10;position:relative}.osl-rg-header-row{display:flex;align-items:stretch}.osl-rg-header-row--groups{border-bottom:1px solid #c7d7f0}.osl-rg-th--group-span{display:flex;align-items:center;justify-content:center;height:28px;min-height:28px;cursor:default;padding:0;border-right:1px solid #dde5ff}.osl-rg-th--group-span:hover{background:transparent!important}.osl-rg-th--group-span:last-child{border-right:none}.osl-rg-th--group-span--labeled{background:linear-gradient(135deg,#dbeafe,#eff6ff);border-bottom:2px solid #93c5fd}.osl-rg-th-group-label{font-size:11px;font-weight:700;color:#1e40af;text-transform:uppercase;letter-spacing:.06em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:0 10px;text-align:center;width:100%}.osl-rg-header-cols{display:flex}.osl-rg-th{position:relative;display:flex;flex-direction:column;background:transparent;border-right:1px solid #dde5ff;cursor:pointer;flex-shrink:0;transition:background .12s;overflow:visible}.osl-rg-th:last-child{border-right:none}.osl-rg-th:hover{background:#2563eb0f}.osl-rg-th--pinned{background:linear-gradient(180deg,#e8efff,#dde8ff);border-right:1px solid #bfdbfe;z-index:3}.osl-rg-th--pinned:after{content:\"\";position:absolute;right:-5px;top:0;bottom:0;width:5px;background:linear-gradient(to right,rgba(37,99,235,.14),transparent);pointer-events:none;z-index:4}.osl-rg-th--sorted{background:#6366f114}.osl-rg-th--filtered{background:#f59e0b12}.osl-rg-th-drag-placeholder{background:#e0e7ff;border:2px dashed #6366f1;border-radius:4px;opacity:.6}.osl-rg-th-pin-badge{position:absolute;top:3px;right:24px;color:var(--osl-primary, #2563eb);opacity:.6;display:flex;align-items:center}.osl-rg-th-content{display:flex;align-items:center;justify-content:space-between;gap:4px;padding:0 8px 0 10px;height:36px;min-height:36px}.osl-rg-th-label{font-size:11.5px;font-weight:700;color:#1e3a6e;text-transform:uppercase;letter-spacing:.05em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;min-width:0}.osl-rg-th-actions{display:flex;align-items:center;gap:2px;flex-shrink:0}.osl-rg-sort-icon{display:inline-flex;align-items:center;position:relative;color:#9ca3af;transition:color .12s;width:14px;height:14px}.osl-rg-sort-icon svg{width:13px;height:13px;display:block}.osl-rg-sort-icon--asc,.osl-rg-sort-icon--desc{color:var(--osl-primary, #2563eb)}.osl-rg-sort-idle{opacity:.3}.osl-rg-sort-badge{position:absolute;top:-4px;right:-6px;font-size:8px;font-weight:700;background:var(--osl-primary, #2563eb);color:#fff;border-radius:50%;width:11px;height:11px;display:flex;align-items:center;justify-content:center;line-height:1}.osl-rg-filter-btn,.osl-rg-col-menu-btn{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border:none;background:transparent;color:#b0b7c3;cursor:pointer;padding:0;border-radius:5px;transition:all .12s}.osl-rg-filter-btn svg,.osl-rg-col-menu-btn svg{width:13px;height:13px;display:block}.osl-rg-filter-btn:hover,.osl-rg-col-menu-btn:hover{background:#00000012;color:#374151}.osl-rg-filter-btn--active{color:var(--osl-primary, #2563eb)!important;background:#2563eb1f!important}.osl-rg-col-search{position:relative;display:flex;align-items:center;padding:3px 6px;border-top:1px solid #e0e7ff;background:#ffffff80}.osl-rg-col-search__icon{position:absolute;left:10px;width:12px;height:12px;display:block;pointer-events:none;color:#c4cad6}.osl-rg-col-search__input{width:100%;height:23px;padding:0 18px 0 22px;border:1px solid #dde5ff;border-radius:5px;font-size:11px;font-family:inherit;background:#fff;color:#111827;outline:none;transition:border-color .12s,box-shadow .12s}.osl-rg-col-search__input::placeholder{color:#c4cad6}.osl-rg-col-search__input:focus{border-color:var(--osl-primary, #2563eb);box-shadow:0 0 0 2px #2563eb1a}.osl-rg-col-search__clear{position:absolute;right:8px;display:flex;align-items:center;width:14px;height:14px;border:none;background:transparent;color:#9ca3af;cursor:pointer;padding:0;border-radius:3px;transition:all .1s}.osl-rg-col-search__clear svg{width:10px;height:10px;display:block}.osl-rg-col-search__clear:hover{color:#374151;background:#0000000f}.osl-rg-resize-handle{position:absolute;right:0;top:0;bottom:0;width:6px;cursor:col-resize;z-index:5}.osl-rg-resize-handle:after{content:\"\";position:absolute;right:2px;top:15%;bottom:15%;width:2px;border-radius:2px;background:#a5b4fc;opacity:0;transition:opacity .15s}.osl-rg-resize-handle:hover:after{opacity:1}.osl-rg-th:hover .osl-rg-resize-handle:after{opacity:.4}.osl-rg-excel-filter{position:fixed;z-index:9999;width:250px;background:#fff;border:1px solid #e5e7eb;border-radius:10px;box-shadow:0 12px 32px #00000026,0 2px 8px #00000012;animation:rg-fade-up .14s ease;overflow:hidden}.osl-rg-excel-filter__header{display:flex;align-items:center;justify-content:space-between;padding:9px 12px;background:linear-gradient(135deg,#f0f4ff,#e8efff);border-bottom:1px solid #dde5ff}.osl-rg-excel-filter__title{display:flex;align-items:center;gap:6px;font-size:11px;font-weight:700;color:#1e3a6e;text-transform:uppercase;letter-spacing:.06em}.osl-rg-excel-filter__clear-btn{font-size:11px;color:#dc2626;background:none;border:none;cursor:pointer;font-weight:600;padding:2px 6px;border-radius:4px;font-family:inherit;transition:background .1s}.osl-rg-excel-filter__clear-btn:hover{background:#fee2e2}.osl-rg-excel-filter__search{padding:7px 8px;border-bottom:1px solid #f3f4f6;position:relative}.osl-rg-excel-filter__search-input{width:100%;height:28px;padding:0 8px 0 28px;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;font-family:inherit;outline:none;box-sizing:border-box;transition:border-color .12s;color:#111827}.osl-rg-excel-filter__search-input::placeholder{color:#b0b7c3}.osl-rg-excel-filter__search-input:focus{border-color:var(--osl-primary, #2563eb)}.osl-rg-excel-filter__select-all{padding:0 8px;border-bottom:2px solid #f3f4f6;background:#fafbff}.osl-rg-excel-filter__list{max-height:210px;overflow-y:auto;padding:3px 8px}.osl-rg-excel-filter__list::-webkit-scrollbar{width:5px}.osl-rg-excel-filter__list::-webkit-scrollbar-thumb{background:#d1d5db;border-radius:4px}.osl-rg-excel-filter__list::-webkit-scrollbar-thumb:hover{background:#9ca3af}.osl-rg-excel-filter__item{display:flex;align-items:center;gap:8px;padding:5px 4px;font-size:12.5px;color:#374151;cursor:pointer;border-radius:5px;transition:background .1s}.osl-rg-excel-filter__item:hover{background:#f3f4f6}.osl-rg-excel-filter__item--all{font-weight:600;color:#1f2937;padding:6px 4px}.osl-rg-excel-filter__item-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.osl-rg-excel-filter__footer{display:flex;gap:6px;padding:8px 10px;border-top:1px solid #f3f4f6;background:#f9fafb}.osl-rg-excel-filter__apply{flex:1;height:30px;border:none;border-radius:6px;font-size:12px;font-family:inherit;font-weight:600;cursor:pointer;background:var(--osl-primary, #2563eb);color:#fff;transition:background .12s}.osl-rg-excel-filter__apply:hover{background:#1d4ed8}.osl-rg-excel-filter__cancel{height:30px;padding:0 14px;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;font-family:inherit;font-weight:500;cursor:pointer;background:#fff;color:#6b7280;transition:all .12s}.osl-rg-excel-filter__cancel:hover{background:#f3f4f6;border-color:#9ca3af}.osl-rg-th--checkbox,.osl-rg-td--checkbox{display:flex;align-items:center;justify-content:center;width:36px;min-width:36px;flex-shrink:0;border-right:1px solid #dde5ff}.osl-rg-checkbox{width:14px;height:14px;cursor:pointer;accent-color:var(--osl-primary, #2563eb);flex-shrink:0}.osl-rg-body{overflow-x:auto;overflow-y:auto;flex-shrink:0}.osl-rg-body::-webkit-scrollbar{width:8px;height:8px}.osl-rg-body::-webkit-scrollbar-track{background:#f9fafb}.osl-rg-body::-webkit-scrollbar-thumb{background:#d1d5db;border-radius:4px}.osl-rg-body::-webkit-scrollbar-thumb:hover{background:#9ca3af}.osl-rg-body::-webkit-scrollbar-corner{background:#f9fafb}.osl-rg-inner{display:block}.osl-rg-row{display:flex;align-items:stretch;border-bottom:1px solid #f3f4f6;transition:background .08s;cursor:default}.osl-rg-row:hover{background:#f0f5ff!important}.osl-rg-row--striped{background:#fafbff}.osl-rg-row--selected{background:#eff6ff!important;border-bottom-color:#dbeafe}.osl-rg-row--skeleton{pointer-events:none}.osl-rg-td{display:flex;align-items:center;padding:0 12px;border-right:1px solid #f0f2f7;overflow:hidden;background:inherit;flex-shrink:0}.osl-rg-td:last-child{border-right:none}.osl-rg-td--pinned{background:#fff;border-right:1px solid #e0e7ff;z-index:2}.osl-rg-td--pinned:after{content:\"\";position:absolute;right:-5px;top:0;bottom:0;width:5px;background:linear-gradient(to right,rgba(37,99,235,.07),transparent);pointer-events:none}.osl-rg-cell-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;width:100%;font-size:13px;color:#1f2937;line-height:1.4}.rg-profit-pos .osl-rg-cell-text{color:#15803d;font-weight:600}.rg-profit-neg .osl-rg-cell-text{color:#dc2626;font-weight:600}.rg-status-delivered .osl-rg-cell-text{color:#15803d;font-weight:600}.rg-status-shipped .osl-rg-cell-text{color:#2563eb;font-weight:600}.rg-status-processing .osl-rg-cell-text{color:#d97706;font-weight:600}.rg-status-pending .osl-rg-cell-text{color:#6b7280;font-weight:500}.rg-status-cancelled .osl-rg-cell-text{color:#dc2626;font-weight:500;text-decoration:line-through;opacity:.75}.osl-rg-group-row{display:flex;align-items:center;gap:7px;border-bottom:1px solid #e5e7eb;font-weight:600;cursor:pointer;flex-shrink:0;transition:filter .1s}.osl-rg-group-row--l0{background:linear-gradient(135deg,#eff6ff,#dbeafe);color:#1e40af;border-left:4px solid #3b82f6}.osl-rg-group-row--l1{background:linear-gradient(135deg,#f5f3ff,#ede9fe);color:#5b21b6;border-left:4px solid #8b5cf6}.osl-rg-group-row--l2{background:linear-gradient(135deg,#fff7ed,#fef3c7);color:#92400e;border-left:4px solid #f59e0b}.osl-rg-group-row:hover{filter:brightness(.97)}.osl-rg-group-row__key{font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;opacity:.65;flex-shrink:0}.osl-rg-group-row__value{font-size:13px;font-weight:700;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.osl-rg-group-row__count{font-size:11px;font-weight:600;padding:2px 9px;border-radius:12px;background:#00000017;margin-right:12px;flex-shrink:0}.osl-rg-group-chevron{width:16px;height:16px;flex-shrink:0;opacity:.75;display:block}.osl-rg-aggregate-row{display:flex;align-items:center;border-top:2px solid #e5e7eb;background:linear-gradient(135deg,#f0fdf4,#f9fafb);flex-shrink:0;min-height:38px}.osl-rg-agg-cell{display:flex;align-items:center;gap:5px;padding:0 12px;border-right:1px solid #e5e7eb;height:38px;flex-shrink:0;overflow:hidden}.osl-rg-agg-cell--checkbox{width:36px;min-width:36px}.osl-rg-agg-label{font-size:9.5px;font-weight:800;color:#9ca3af;text-transform:uppercase;letter-spacing:.06em;flex-shrink:0;background:#e5e7eb;padding:1px 4px;border-radius:3px}.osl-rg-agg-value{font-size:12px;font-weight:700;color:#15803d;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.osl-rg-skel{height:12px;border-radius:4px;background:linear-gradient(90deg,#e5e7eb 25%,#f3f4f6,#e5e7eb 75%);background-size:200% 100%;animation:rg-pulse 1.5s ease-in-out infinite}.osl-rg-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:64px 20px;color:#9ca3af;text-align:center}.osl-rg-empty__icon{margin-bottom:18px;opacity:.7}.osl-rg-empty__title{font-size:15px;font-weight:600;color:#374151;margin:0 0 6px}.osl-rg-empty__sub{font-size:13px;color:#9ca3af;margin:0 0 18px;max-width:290px;line-height:1.5}.osl-rg-empty__action{height:34px;padding:0 18px;background:var(--osl-primary, #2563eb);color:#fff;border:none;border-radius:7px;font-size:13px;font-family:inherit;font-weight:600;cursor:pointer;transition:background .15s;box-shadow:0 2px 6px #2563eb4d}.osl-rg-empty__action:hover{background:#1d4ed8}.osl-rg-pagination{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 14px;border-top:1px solid var(--osl-border-color, #e5e7eb);background:linear-gradient(135deg,#f8faff,#f3f4f6);flex-shrink:0;flex-wrap:wrap}.osl-rg-pagination__info{font-size:12px;color:#6b7280;min-width:160px;white-space:nowrap}.osl-rg-pagination__info strong{color:#1f2937;font-weight:600}.osl-rg-pagination__controls{display:flex;align-items:center;gap:3px}.osl-rg-pagination__size{display:flex;align-items:center;gap:7px;min-width:120px;justify-content:flex-end}.osl-rg-pagination__size-label{font-size:11.5px;color:#9ca3af;white-space:nowrap}.osl-rg-page-btn{display:inline-flex;align-items:center;justify-content:center;gap:3px;height:30px;padding:0 8px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:6px;background:#fff;color:#374151;font-size:12px;font-family:inherit;font-weight:500;cursor:pointer;transition:all .12s;white-space:nowrap}.osl-rg-page-btn svg{display:block}.osl-rg-page-btn:hover:not(:disabled){background:#f3f4f6;border-color:#9ca3af;box-shadow:0 1px 2px #0000000f}.osl-rg-page-btn:disabled{opacity:.35;cursor:not-allowed}.osl-rg-page-btn--num{min-width:30px;padding:0;font-size:12.5px}.osl-rg-page-btn--active{background:var(--osl-primary, #2563eb);border-color:var(--osl-primary, #2563eb);color:#fff;font-weight:700;box-shadow:0 2px 5px #2563eb59}.osl-rg-page-btn--active:hover:not(:disabled){background:#1d4ed8;border-color:#1d4ed8}.osl-rg-page-ellipsis{display:inline-flex;align-items:center;justify-content:center;width:26px;color:#9ca3af;font-size:13px;pointer-events:none}.osl-rg-page-size{height:30px;padding:0 8px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:6px;background:#fff;font-size:12px;font-family:inherit;color:#374151;cursor:pointer;outline:none;transition:border-color .15s}.osl-rg-page-size:focus{border-color:var(--osl-primary, #2563eb)}.osl-rg-col-menu{position:fixed;z-index:9999;min-width:210px;background:#fff;border:1px solid #e5e7eb;border-radius:11px;box-shadow:0 12px 36px #00000029,0 3px 10px #00000014;overflow:hidden;animation:rg-fade-up .13s cubic-bezier(.16,1,.3,1)}.osl-rg-col-menu__header{padding:9px 14px 7px;font-size:10.5px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:#9ca3af;border-bottom:1px solid #f3f4f6;background:#fafbff}.osl-rg-col-menu__divider{height:1px;background:#f3f4f6;margin:2px 0}.osl-rg-col-menu__item{display:flex;align-items:center;gap:9px;width:100%;padding:9px 14px;background:transparent;border:none;border-left:3px solid transparent;border-bottom:none;text-align:left;font-size:13px;font-weight:500;font-family:inherit;color:#374151;cursor:pointer;transition:all .1s}.osl-rg-col-menu__item svg{color:#9ca3af;display:block}.osl-rg-col-menu__item:hover{background:#f0f4ff;color:var(--osl-primary, #2563eb);border-left-color:var(--osl-primary, #2563eb)}.osl-rg-col-menu__item:hover svg{color:var(--osl-primary, #2563eb)}.osl-rg-col-menu__item--danger:hover{background:#fff5f5;color:#dc2626;border-left-color:#dc2626}.osl-rg-col-menu__item--danger:hover svg{color:#dc2626}.osl-rg-config-backdrop{position:fixed;inset:0;z-index:9000;background:#0003;-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px)}.osl-rg-config-panel{position:fixed;top:0;right:0;bottom:0;z-index:9001;width:300px;background:#fff;border-left:1px solid #e5e7eb;box-shadow:-6px 0 24px #00000021;display:flex;flex-direction:column;animation:rg-slide-in .2s cubic-bezier(.16,1,.3,1)}.osl-rg-config-panel__header{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border-bottom:1px solid #e5e7eb;background:linear-gradient(135deg,#f0f4ff,#e8efff);flex-shrink:0}.osl-rg-config-panel__title{display:flex;align-items:center;gap:8px;font-size:14px;font-weight:700;color:#1e3a6e}.osl-rg-config-panel__title svg{color:#3b82f6}.osl-rg-config-panel__close{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border:none;background:transparent;color:#9ca3af;cursor:pointer;border-radius:7px;padding:0;transition:all .12s}.osl-rg-config-panel__close svg{width:16px;height:16px;display:block}.osl-rg-config-panel__close:hover{background:#00000012;color:#374151}.osl-rg-config-panel__actions{display:flex;gap:8px;padding:10px 14px;border-bottom:1px solid #f3f4f6;flex-shrink:0;background:#fafbff}.osl-rg-config-hint{padding:6px 16px;font-size:10.5px;color:#b0b7c3;font-style:italic;border-bottom:1px solid #f3f4f6;flex-shrink:0}.osl-rg-config-action{display:inline-flex;align-items:center;gap:5px;height:30px;padding:0 10px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;color:#374151;font-size:12px;font-family:inherit;font-weight:500;cursor:pointer;transition:all .12s}.osl-rg-config-action svg{display:block}.osl-rg-config-action:hover{background:#f3f4f6;border-color:#9ca3af}.osl-rg-config-list{flex:1;overflow-y:auto;padding:6px 0}.osl-rg-config-list::-webkit-scrollbar{width:5px}.osl-rg-config-list::-webkit-scrollbar-thumb{background:#e5e7eb;border-radius:4px}.osl-rg-config-list::-webkit-scrollbar-thumb:hover{background:#d1d5db}.osl-rg-config-item{display:flex;align-items:center;gap:9px;padding:7px 14px;transition:background .1s;cursor:grab}.osl-rg-config-item:hover{background:#f9fafb}.osl-rg-config-item:active{cursor:grabbing}.osl-rg-config-item--hidden{opacity:.5}.osl-rg-config-item__label{flex:1;font-size:13px;color:#374151;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}.osl-rg-config-item__label--hidden{color:#9ca3af;font-style:italic}.osl-rg-config-item__pin-btn{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border:1px solid #e5e7eb;background:#fff;color:#d1d5db;cursor:pointer;padding:0;border-radius:5px;transition:all .12s;flex-shrink:0}.osl-rg-config-item__pin-btn svg{display:block}.osl-rg-config-item__pin-btn:hover,.osl-rg-config-item__pin-btn--active{background:#eff6ff;border-color:#93c5fd;color:var(--osl-primary, #2563eb)}.osl-rg-copy-toast{position:fixed;bottom:24px;left:50%;transform:translate(-50%);z-index:99999;display:flex;align-items:center;gap:7px;background:#1f2937;color:#fff;font-size:12.5px;font-weight:500;padding:8px 18px;border-radius:22px;animation:rg-fade-up .2s ease;box-shadow:0 6px 16px #0000004d;pointer-events:none;white-space:nowrap;font-family:inherit}.osl-rg-copy-toast svg{flex-shrink:0;color:#4ade80}\n"], dependencies: [{ kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.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$2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i4$1.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i4$1.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i4$1.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "directive", type: i4$1.CdkDragPlaceholder, selector: "ng-template[cdkDragPlaceholder]", inputs: ["data"] }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }, { kind: "pipe", type: i1$1.UpperCasePipe, name: "uppercase" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5687
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: OslReportGrid, isStandalone: false, selector: "osl-report-grid", inputs: { columns: "columns", datasource: "datasource", loading: "loading", totalRecords: "totalRecords", autoMode: "autoMode", isPaginated: "isPaginated", pageSize: "pageSize", tableHeight: "tableHeight", striped: "striped", exportable: "exportable", rowHeight: "rowHeight", rowSelection: "rowSelection", showAggregates: "showAggregates", title: "title", pdfExportFromGrid: "pdfExportFromGrid", pdfConfig: "pdfConfig" }, outputs: { pageChange: "pageChange", pageSizeChange: "pageSizeChange", sortChange: "sortChange", rowClick: "rowClick", selectionChange: "selectionChange" }, host: { listeners: { "document:mousemove": "onMouseMove($event)", "document:mouseup": "onMouseUp()", "document:click": "onDocumentClick()" } }, providers: [DatePipe, DecimalPipe], viewQueries: [{ propertyName: "headerScrollRef", first: true, predicate: ["headerScrollRef"], descendants: true }, { propertyName: "bodyScrollRef", first: true, predicate: ["bodyScrollRef"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"osl-rg\" (click)=\"closeColumnMenu(); openFilterKey = null; $event.stopPropagation()\">\n\n <!-- \u2550\u2550 TOOLBAR \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <div class=\"osl-rg-toolbar\">\n <div class=\"osl-rg-toolbar__left\">\n @if (title) { <span class=\"osl-rg-title\">{{ title }}</span> }\n @if (!loading) {\n @if (_filteredTotal !== datasource.length) {\n <span class=\"osl-rg-filter-badge\">\n <span class=\"osl-rg-filter-badge__dot\"></span>\n {{ _filteredTotal | number }} of {{ datasource.length | number }} rows\n </span>\n } @else {\n <span class=\"osl-rg-record-count\">{{ datasource.length | number }} rows</span>\n }\n }\n </div>\n <div class=\"osl-rg-toolbar__right\">\n\n <!-- Global search -->\n <div class=\"osl-rg-global-search\">\n <svg class=\"osl-rg-global-search__icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.35-4.35\"/>\n </svg>\n <input class=\"osl-rg-global-search__input\" placeholder=\"Search all columns\u2026\"\n [(ngModel)]=\"globalSearch\" (ngModelChange)=\"currentPage=1; processData()\" />\n @if (globalSearch) {\n <button class=\"osl-rg-global-search__clear\" (click)=\"globalSearch=''; processData()\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n }\n </div>\n\n <!-- Sort active badge -->\n @if (sortStates.size > 0) {\n <button class=\"osl-rg-toolbar-btn osl-rg-toolbar-btn--active\" (click)=\"clearSort()\" title=\"Clear sort\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M11 5H4M11 9H4M11 13H4M18 4v16M15 7l3-3 3 3M15 17l3 3 3-3\"/>\n </svg>\n <span>{{ sortStates.size }} sort{{ sortStates.size > 1 ? 's' : '' }}</span>\n <svg class=\"rg-icon rg-icon--xs\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n }\n\n <!-- Filter active badge -->\n @if (hasAnyFilter) {\n <button class=\"osl-rg-toolbar-btn osl-rg-toolbar-btn--danger\" (click)=\"clearAllFilters()\" title=\"Clear all filters\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M22 3H2l8 9.46V19l4 2V12.46z\"/></svg>\n <span>Filtered</span>\n <svg class=\"rg-icon rg-icon--xs\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n }\n\n <!-- Group toggle -->\n <button class=\"osl-rg-toolbar-btn\" [class.osl-rg-toolbar-btn--on]=\"showGroupPanel\"\n (click)=\"showGroupPanel=!showGroupPanel\" title=\"Group panel\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"2\" y=\"7\" width=\"20\" height=\"14\" rx=\"2\"/><path d=\"M16 3H8a2 2 0 0 0-2 2v2h12V5a2 2 0 0 0-2-2z\"/>\n </svg>\n <span>Group</span>\n </button>\n\n <!-- Expand / collapse when grouped -->\n @if (activeGroups.length > 0) {\n <button class=\"osl-rg-toolbar-btn\" (click)=\"expandAll()\" title=\"Expand all\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m7 20 5-5 5 5M7 4l5 5 5-5\"/></svg>\n </button>\n <button class=\"osl-rg-toolbar-btn\" (click)=\"collapseAll()\" title=\"Collapse all\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m7 15 5-5 5 5M7 9l5 5 5 5\"/></svg>\n </button>\n }\n\n <!-- Auto-size all -->\n <button class=\"osl-rg-toolbar-btn\" (click)=\"autoSizeAll()\" title=\"Auto-size all columns\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M21 6H3M21 18H3\"/><path d=\"M8 3 3 6l5 3M16 3l5 3-5 3M8 15l-5 3 5 3M16 15l5 3-5 3\"/>\n </svg>\n <span>Auto-size</span>\n </button>\n\n <!-- Column config -->\n <button class=\"osl-rg-toolbar-btn\" [class.osl-rg-toolbar-btn--on]=\"showColumnConfig\"\n (click)=\"showColumnConfig=!showColumnConfig; $event.stopPropagation()\" title=\"Column settings\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M4 6h16M4 12h16M4 18h16\"/>\n <circle cx=\"8\" cy=\"6\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n <circle cx=\"16\" cy=\"12\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n <circle cx=\"8\" cy=\"18\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n </svg>\n <span>Columns</span>\n </button>\n\n <!-- Export Excel -->\n @if (exportable) {\n <button class=\"osl-rg-toolbar-btn osl-rg-toolbar-btn--excel\" (click)=\"exportExcel()\" title=\"Export to Excel\">\n <svg class=\"rg-icon rg-icon--excel\" viewBox=\"0 0 24 24\">\n <rect width=\"24\" height=\"24\" rx=\"3\" fill=\"#1D6F42\"/>\n <path d=\"M13.5 3H7a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V7.5L13.5 3z\" fill=\"#fff\" opacity=\".15\"/>\n <path d=\"M13.5 3v4.5H18L13.5 3z\" fill=\"#fff\" opacity=\".3\"/>\n <path d=\"M8 9.5l2.3 3.5L8 16.5h1.6l1.4-2.3 1.4 2.3H14l-2.3-3.5 2.3-3.5h-1.6l-1.4 2.3-1.4-2.3H8z\" fill=\"#fff\"/>\n </svg>\n <span>Export Excel</span>\n </button>\n }\n\n <!-- Export PDF -->\n @if (exportable && pdfExportFromGrid) {\n <button class=\"osl-rg-toolbar-btn osl-rg-toolbar-btn--pdf\" (click)=\"exportPdf()\" title=\"Export to PDF\">\n <svg class=\"rg-icon rg-icon--pdf\" viewBox=\"0 0 24 24\">\n <rect width=\"24\" height=\"24\" rx=\"3\" fill=\"#D32F2F\"/>\n <path d=\"M13.5 3H7a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V7.5L13.5 3z\" fill=\"#fff\" opacity=\".15\"/>\n <path d=\"M13.5 3v4.5H18L13.5 3z\" fill=\"#fff\" opacity=\".3\"/>\n <text x=\"4.5\" y=\"17.5\" fill=\"#fff\" font-size=\"6.5\" font-family=\"sans-serif\" font-weight=\"700\">PDF</text>\n </svg>\n <span>Export PDF</span>\n </button>\n }\n </div>\n </div>\n\n <!-- \u2550\u2550 GROUP PANEL \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @if (showGroupPanel) {\n <div class=\"osl-rg-group-panel\">\n <span class=\"osl-rg-group-panel__label\">Group by:</span>\n @if (activeGroups.length === 0) {\n <span class=\"osl-rg-group-panel__hint\">Use the \u22EE column menu \u2192 \"Group by this\"</span>\n }\n <div class=\"osl-rg-group-chips\" cdkDropList cdkDropListOrientation=\"horizontal\"\n [cdkDropListData]=\"activeGroups\" (cdkDropListDropped)=\"onGroupPanelDrop($event)\">\n @for (key of activeGroups; track key) {\n <div class=\"osl-rg-group-chip\" cdkDrag>\n <svg cdkDragHandle class=\"rg-drag-handle\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <circle cx=\"9\" cy=\"5\" r=\"1.5\"/><circle cx=\"15\" cy=\"5\" r=\"1.5\"/>\n <circle cx=\"9\" cy=\"12\" r=\"1.5\"/><circle cx=\"15\" cy=\"12\" r=\"1.5\"/>\n <circle cx=\"9\" cy=\"19\" r=\"1.5\"/><circle cx=\"15\" cy=\"19\" r=\"1.5\"/>\n </svg>\n <span>{{ getGroupColLabel(key) }}</span>\n <button class=\"osl-rg-group-chip__remove\" (click)=\"removeGroup(key)\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n </div>\n }\n </div>\n @if (activeGroups.length > 0) {\n <button class=\"osl-rg-group-panel__clear\" (click)=\"clearGroups()\">Clear groups</button>\n }\n </div>\n }\n\n <!-- \u2550\u2550 STICKY HEADER \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <div class=\"osl-rg-header-wrap\" #headerScrollRef>\n\n <!-- Double header \u2013 group row (only rendered when headerGroup is used on any column) -->\n @if (hasHeaderGroups) {\n <div class=\"osl-rg-header-row osl-rg-header-row--groups\" [style.minWidth.px]=\"totalWidth\">\n @if (rowSelection === 'multiple') {\n <div class=\"osl-rg-th osl-rg-th--checkbox osl-rg-th--group-span\"></div>\n }\n <div style=\"display:flex;flex:1;min-width:0;\">\n @for (group of computedHeaderGroups; track $index) {\n <div class=\"osl-rg-th osl-rg-th--group-span\"\n [class.osl-rg-th--group-span--labeled]=\"!!group.label\"\n [class.osl-rg-th--pinned]=\"group.isPinned\"\n [style.width.px]=\"group.width\"\n [style.minWidth.px]=\"group.width\"\n [style.left.px]=\"group.isPinned ? group.stickyLeft : null\"\n [style.position]=\"group.isPinned ? 'sticky' : 'relative'\"\n [style.zIndex]=\"group.isPinned ? 3 : 1\">\n @if (group.label) {\n <span class=\"osl-rg-th-group-label\">{{ group.label }}</span>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <div class=\"osl-rg-header-row\" [style.minWidth.px]=\"totalWidth\">\n\n <!-- Select-all checkbox -->\n @if (rowSelection === 'multiple') {\n <div class=\"osl-rg-th osl-rg-th--checkbox\">\n <input type=\"checkbox\" class=\"osl-rg-checkbox\"\n [checked]=\"allSelected\" [indeterminate]=\"someSelected\"\n (change)=\"toggleSelectAll($any($event.target).checked)\" />\n </div>\n }\n\n <!-- Column headers (drag-to-reorder) -->\n <div class=\"osl-rg-header-cols\" cdkDropList cdkDropListOrientation=\"horizontal\"\n [cdkDropListData]=\"_cols\" (cdkDropListDropped)=\"onColumnReorder($event)\"\n style=\"display:flex;flex:1;min-width:0;\">\n\n @for (col of visibleCols; track col.key) {\n <div class=\"osl-rg-th\"\n [class.osl-rg-th--pinned]=\"col._pinned\"\n [class.osl-rg-th--sorted]=\"getSortState(col.key)\"\n [class.osl-rg-th--filtered]=\"hasActiveFilter(col.key)\"\n [style.width.px]=\"col._width\"\n [style.minWidth.px]=\"col._width\"\n [style.left.px]=\"col._pinned ? col._stickyLeft : null\"\n [style.position]=\"col._pinned ? 'sticky' : 'relative'\"\n [style.zIndex]=\"col._pinned ? 3 : 1\"\n cdkDrag [cdkDragDisabled]=\"col._pinned\"\n (cdkDragStarted)=\"onColumnDragStarted()\"\n (cdkDragEnded)=\"onColumnDragEnded()\"\n (click)=\"onHeaderClick(col, $event)\">\n\n <div *cdkDragPlaceholder class=\"osl-rg-th-drag-placeholder\"></div>\n\n <!-- Pin badge -->\n @if (col._pinned) {\n <span class=\"osl-rg-th-pin-badge\" title=\"Pinned\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"9\" height=\"9\"><path d=\"M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z\"/></svg>\n </span>\n }\n\n <!-- Label row -->\n <div class=\"osl-rg-th-content\">\n <span class=\"osl-rg-th-label\" [title]=\"col.label\">{{ col.label }}</span>\n <div class=\"osl-rg-th-actions\">\n\n <!-- Sort indicator -->\n @if (col.sortable) {\n <span class=\"osl-rg-sort-icon\"\n [class.osl-rg-sort-icon--asc]=\"getSortState(col.key)?.asc === true\"\n [class.osl-rg-sort-icon--desc]=\"getSortState(col.key)?.asc === false\">\n @if (getSortState(col.key); as s) {\n @if (s.asc) {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 19V5m-7 7 7-7 7 7\"/></svg>\n } @else {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14m7-7-7 7-7-7\"/></svg>\n }\n @if (sortStates.size > 1) { <sup class=\"osl-rg-sort-badge\">{{ s.index + 1 }}</sup> }\n } @else {\n <svg class=\"osl-rg-sort-idle\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M8 9l4-4 4 4M16 15l-4 4-4-4\"/></svg>\n }\n </span>\n }\n\n <!-- Excel filter button -->\n @if (col.filterable) {\n <button class=\"osl-rg-filter-btn\" [class.osl-rg-filter-btn--active]=\"hasActiveFilter(col.key)\"\n (click)=\"openExcelFilter(col.key, $event)\" title=\"Filter\">\n @if (hasActiveFilter(col.key)) {\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M22 3H2l8 9.46V19l4 2V12.46z\"/></svg>\n } @else {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3\"/></svg>\n }\n </button>\n }\n\n <!-- Column menu -->\n <button class=\"osl-rg-col-menu-btn\" (click)=\"openColumnMenu(col.key, $event)\" title=\"Column options\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><circle cx=\"12\" cy=\"5\" r=\"2\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/><circle cx=\"12\" cy=\"19\" r=\"2\"/></svg>\n </button>\n\n </div>\n </div>\n\n <!-- Per-column search -->\n @if (col.searchable) {\n <div class=\"osl-rg-col-search\" (click)=\"$event.stopPropagation()\">\n <svg class=\"osl-rg-col-search__icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m18 18-3.5-3.5\"/>\n </svg>\n <input class=\"osl-rg-col-search__input\" [placeholder]=\"col.label + '\u2026'\"\n [ngModel]=\"columnSearch[col.key]\" (ngModelChange)=\"onColumnSearch(col.key, $event)\" />\n @if (columnSearch[col.key]) {\n <button class=\"osl-rg-col-search__clear\" (click)=\"columnSearch[col.key]=''; processData()\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n }\n </div>\n }\n\n <!-- Resize handle -->\n @if (col.resizable) {\n <div class=\"osl-rg-resize-handle\" (mousedown)=\"startResize(col, $event)\" (dblclick)=\"autoSizeColumn(col, $event)\" (click)=\"$event.stopPropagation()\"></div>\n }\n\n\n </div><!-- /osl-rg-th -->\n }\n\n </div><!-- /header-cols -->\n </div><!-- /header-row -->\n </div><!-- /header-wrap -->\n\n <!-- \u2550\u2550 SCROLLABLE BODY \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <div class=\"osl-rg-body\" [style.height]=\"tableHeight\" #bodyScrollRef (scroll)=\"onBodyScroll($event)\">\n <div class=\"osl-rg-inner\" [style.minWidth.px]=\"totalWidth\">\n\n <!-- Skeleton loading -->\n @if (loading) {\n @for (sk of skeletonRows; track $index) {\n <div class=\"osl-rg-row osl-rg-row--skeleton\" [style.height.px]=\"rowHeight\">\n @if (rowSelection === 'multiple') {\n <div class=\"osl-rg-td osl-rg-td--checkbox\"><div class=\"osl-rg-skel\" style=\"width:14px;height:14px;border-radius:3px;\"></div></div>\n }\n @for (col of visibleCols; track col.key) {\n <div class=\"osl-rg-td\" [style.width.px]=\"col._width\" [style.minWidth.px]=\"col._width\">\n <div class=\"osl-rg-skel\" [style.width]=\"$index % 3 === 0 ? '55%' : $index % 3 === 1 ? '80%' : '65%'\"></div>\n </div>\n }\n </div>\n }\n\n } @else if (isEmpty) {\n <!-- Empty state -->\n <div class=\"osl-rg-empty\">\n <div class=\"osl-rg-empty__icon\">\n <svg viewBox=\"0 0 48 48\" fill=\"none\" stroke=\"#d1d5db\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"6\" y=\"6\" width=\"36\" height=\"36\" rx=\"4\"/>\n <path d=\"M6 18h36M6 30h36M18 6v36\"/>\n </svg>\n </div>\n <p class=\"osl-rg-empty__title\">No records found</p>\n <p class=\"osl-rg-empty__sub\">\n @if (hasAnyFilter) { Try adjusting your filters or search terms }\n @else { No data to display }\n </p>\n @if (hasAnyFilter) {\n <button class=\"osl-rg-empty__action\" (click)=\"clearAllFilters()\">Clear all filters</button>\n }\n </div>\n\n } @else {\n <!-- Data rows -->\n @for (row of flatRows; track trackByRow($index, row)) {\n\n @if (row._type === 'group') {\n <div class=\"osl-rg-group-row\"\n [class.osl-rg-group-row--l0]=\"asGroupRow(row)._level === 0\"\n [class.osl-rg-group-row--l1]=\"asGroupRow(row)._level === 1\"\n [class.osl-rg-group-row--l2]=\"asGroupRow(row)._level >= 2\"\n [style.height.px]=\"rowHeight\"\n [style.paddingLeft.px]=\"asGroupRow(row)._level * 20 + 12\"\n (click)=\"toggleGroup(asGroupRow(row)._path)\">\n @if (asGroupRow(row)._expanded) {\n <svg class=\"osl-rg-group-chevron\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m6 9 6 6 6-6\"/></svg>\n } @else {\n <svg class=\"osl-rg-group-chevron\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m9 18 6-6-6-6\"/></svg>\n }\n <span class=\"osl-rg-group-row__key\">{{ asGroupRow(row)._colLabel }}:</span>\n <span class=\"osl-rg-group-row__value\">{{ asGroupRow(row)._label }}</span>\n <span class=\"osl-rg-group-row__count\">{{ asGroupRow(row)._count | number }} rows</span>\n </div>\n\n } @else {\n <div class=\"osl-rg-row\"\n [class.osl-rg-row--striped]=\"striped && asDataRow(row)._rowIndex % 2 === 1\"\n [class.osl-rg-row--selected]=\"selectedRows.has(asDataRow(row)._data)\"\n [style.height.px]=\"rowHeight\"\n (click)=\"onRowClick(asDataRow(row)._data, $event)\">\n\n @if (rowSelection === 'multiple') {\n <div class=\"osl-rg-td osl-rg-td--checkbox\">\n <input type=\"checkbox\" class=\"osl-rg-checkbox\"\n [checked]=\"selectedRows.has(asDataRow(row)._data)\"\n (change)=\"toggleRowSelect(asDataRow(row)._data, $event)\" />\n </div>\n }\n\n @for (col of visibleCols; track col.key) {\n <div class=\"osl-rg-td\"\n [class.osl-rg-td--pinned]=\"col._pinned\"\n [ngClass]=\"getCellClass(asDataRow(row)._data, col)\"\n [style.width.px]=\"col._width\"\n [style.minWidth.px]=\"col._width\"\n [style.left.px]=\"col._pinned ? col._stickyLeft : null\"\n [style.position]=\"col._pinned ? 'sticky' : 'relative'\"\n [style.zIndex]=\"col._pinned ? 2 : 0\"\n [style.textAlign]=\"col.align ?? 'left'\"\n (dblclick)=\"copyCell(getCellDisplay(asDataRow(row)._data, col), $event)\"\n [title]=\"getCellDisplay(asDataRow(row)._data, col)\">\n <span class=\"osl-rg-cell-text\">{{ getCellDisplay(asDataRow(row)._data, col) }}</span>\n </div>\n }\n\n </div>\n }\n\n }\n }\n\n </div><!-- /osl-rg-inner -->\n </div><!-- /osl-rg-body -->\n\n <!-- \u2550\u2550 AGGREGATE FOOTER \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @if (showAggregates && hasAnyAggregate() && !loading) {\n <div class=\"osl-rg-aggregate-row\" [style.minWidth.px]=\"totalWidth\">\n @if (rowSelection === 'multiple') { <div class=\"osl-rg-agg-cell osl-rg-agg-cell--checkbox\"></div> }\n @for (col of visibleCols; track col.key) {\n <div class=\"osl-rg-agg-cell\" [style.width.px]=\"col._width\" [style.minWidth.px]=\"col._width\" [style.textAlign]=\"col.align ?? 'left'\">\n @if (col.aggregate) {\n <span class=\"osl-rg-agg-label\">{{ col.aggregate | uppercase }}</span>\n <span class=\"osl-rg-agg-value\">{{ getAggregate(col) }}</span>\n }\n </div>\n }\n </div>\n }\n\n <!-- \u2550\u2550 PAGINATION \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @if (isPaginated) {\n <div class=\"osl-rg-pagination\">\n <span class=\"osl-rg-pagination__info\">\n @if (loading) { Loading\u2026 }\n @else if (_total > 0) { Showing <strong>{{ startRecord | number }}\u2013{{ endRecord | number }}</strong> of <strong>{{ _total | number }}</strong> records }\n @else { No records }\n </span>\n <div class=\"osl-rg-pagination__controls\">\n <button class=\"osl-rg-page-btn osl-rg-page-btn--nav\" (click)=\"goToPage(1)\" [disabled]=\"currentPage===1||loading\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"m11 17-5-5 5-5M18 17l-5-5 5-5\"/></svg>\n </button>\n <button class=\"osl-rg-page-btn osl-rg-page-btn--nav\" (click)=\"goToPage(currentPage-1)\" [disabled]=\"currentPage===1||loading\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"m15 18-6-6 6-6\"/></svg>\n Prev\n </button>\n @for (page of pageNumbers; track $index) {\n @if (page === -1) { <span class=\"osl-rg-page-ellipsis\">\u2026</span> }\n @else {\n <button class=\"osl-rg-page-btn osl-rg-page-btn--num\" [class.osl-rg-page-btn--active]=\"page===currentPage\"\n [disabled]=\"loading\" (click)=\"goToPage(page)\">{{ page }}</button>\n }\n }\n <button class=\"osl-rg-page-btn osl-rg-page-btn--nav\" (click)=\"goToPage(currentPage+1)\" [disabled]=\"currentPage===totalPages||loading\">\n Next\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"m9 18 6-6-6-6\"/></svg>\n </button>\n <button class=\"osl-rg-page-btn osl-rg-page-btn--nav\" (click)=\"goToPage(totalPages)\" [disabled]=\"currentPage===totalPages||loading\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"m13 17 5-5-5-5M6 17l5-5-5-5\"/></svg>\n </button>\n </div>\n <div class=\"osl-rg-pagination__size\">\n <span class=\"osl-rg-pagination__size-label\">Rows per page</span>\n <select class=\"osl-rg-page-size\" [ngModel]=\"pageSize\" (ngModelChange)=\"onPageSizeChange($event)\" [disabled]=\"loading\">\n @for (opt of pageSizeOptions; track opt) { <option [value]=\"opt\">{{ opt }}</option> }\n </select>\n </div>\n </div>\n }\n\n</div><!-- /osl-rg -->\n\n\n<!-- \u2550\u2550 EXCEL FILTER DROPDOWN (fixed, outside overflow containers) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (openFilterKey && excelFilterState[openFilterKey]) {\n <div class=\"osl-rg-excel-filter\"\n [style.top.px]=\"filterDropdownPos.top\"\n [style.left.px]=\"filterDropdownPos.left\"\n (click)=\"$event.stopPropagation()\">\n <div class=\"osl-rg-excel-filter__header\">\n <div class=\"osl-rg-excel-filter__title\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"12\" height=\"12\" style=\"flex-shrink:0;\"><path d=\"M22 3H2l8 9.46V19l4 2V12.46z\"/></svg>\n Filter: {{ getColumnByKey(openFilterKey)?.label }}\n </div>\n @if (hasActiveFilter(openFilterKey)) {\n <button class=\"osl-rg-excel-filter__clear-btn\" (click)=\"clearExcelFilter(openFilterKey!)\">Clear</button>\n }\n </div>\n <div class=\"osl-rg-excel-filter__search\">\n <input class=\"osl-rg-excel-filter__search-input\" placeholder=\"Search values\u2026\"\n [(ngModel)]=\"excelFilterState[openFilterKey].search\" />\n </div>\n <div class=\"osl-rg-excel-filter__select-all\">\n <label class=\"osl-rg-excel-filter__item osl-rg-excel-filter__item--all\">\n <input type=\"checkbox\" class=\"osl-rg-checkbox\" [checked]=\"isAllExcelChecked(openFilterKey)\"\n (change)=\"toggleAllExcelFilter(openFilterKey!, $any($event.target).checked)\" />\n <span class=\"osl-rg-excel-filter__item-label\">(Select All)</span>\n </label>\n </div>\n <div class=\"osl-rg-excel-filter__list\">\n @for (item of getFilteredExcelItems(openFilterKey); track item.value) {\n <label class=\"osl-rg-excel-filter__item\">\n <input type=\"checkbox\" class=\"osl-rg-checkbox\" [(ngModel)]=\"item.checked\" />\n <span class=\"osl-rg-excel-filter__item-label\">{{ item.label }}</span>\n </label>\n }\n </div>\n <div class=\"osl-rg-excel-filter__footer\">\n <button class=\"osl-rg-excel-filter__apply\" (click)=\"applyExcelFilter(openFilterKey!)\">Apply Filter</button>\n <button class=\"osl-rg-excel-filter__cancel\" (click)=\"openFilterKey=null\">Cancel</button>\n </div>\n </div>\n}\n\n\n<!-- \u2550\u2550 COLUMN CONTEXT MENU (fixed) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (columnMenuKey && getColumnByKey(columnMenuKey); as menuCol) {\n <div class=\"osl-rg-col-menu\"\n [style.top.px]=\"columnMenuPos.top\" [style.left.px]=\"columnMenuPos.left\"\n (click)=\"$event.stopPropagation()\">\n <div class=\"osl-rg-col-menu__header\">{{ menuCol.label }}</div>\n @if (menuCol.sortable) {\n <button class=\"osl-rg-col-menu__item\"\n (click)=\"sortStates.clear(); sortStates.set(menuCol.key,{key:menuCol.key,asc:true,index:0}); sortCounter=1; processData(); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 19V5m-7 7 7-7 7 7\"/></svg>\n Sort A \u2192 Z\n </button>\n <button class=\"osl-rg-col-menu__item\"\n (click)=\"sortStates.clear(); sortStates.set(menuCol.key,{key:menuCol.key,asc:false,index:0}); sortCounter=1; processData(); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14m7-7-7 7-7-7\"/></svg>\n Sort Z \u2192 A\n </button>\n }\n @if (menuCol.groupable) {\n <div class=\"osl-rg-col-menu__divider\"></div>\n <button class=\"osl-rg-col-menu__item\" (click)=\"addGroup(menuCol.key); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"7\" width=\"20\" height=\"14\" rx=\"2\"/><path d=\"M16 3H8a2 2 0 0 0-2 2v2h12V5a2 2 0 0 0-2-2z\"/></svg>\n Group by this\n </button>\n }\n <div class=\"osl-rg-col-menu__divider\"></div>\n <button class=\"osl-rg-col-menu__item\" (click)=\"toggleColumnPin(menuCol)\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z\"/></svg>\n {{ menuCol._pinned ? 'Unpin column' : 'Pin to left' }}\n </button>\n <button class=\"osl-rg-col-menu__item\" (click)=\"autoSizeColumn(menuCol); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 6H3M21 18H3\"/><path d=\"M8 3 3 6l5 3M16 3l5 3-5 3M8 15l-5 3 5 3M16 15l5 3-5 3\"/></svg>\n Auto-size column\n </button>\n <div class=\"osl-rg-col-menu__divider\"></div>\n <button class=\"osl-rg-col-menu__item osl-rg-col-menu__item--danger\" (click)=\"toggleColumnVisibility(menuCol); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24M1 1l22 22\"/>\n </svg>\n Hide column\n </button>\n </div>\n}\n\n\n<!-- \u2550\u2550 COLUMN CONFIG SIDE DRAWER \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (showColumnConfig) {\n <div class=\"osl-rg-config-backdrop\" (click)=\"showColumnConfig=false\"></div>\n <div class=\"osl-rg-config-panel\" (click)=\"$event.stopPropagation()\">\n <div class=\"osl-rg-config-panel__header\">\n <div class=\"osl-rg-config-panel__title\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"16\" height=\"16\">\n <path d=\"M4 6h16M4 12h16M4 18h16\"/>\n <circle cx=\"8\" cy=\"6\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n <circle cx=\"16\" cy=\"12\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n <circle cx=\"8\" cy=\"18\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n </svg>\n Column Settings\n </div>\n <button class=\"osl-rg-config-panel__close\" (click)=\"showColumnConfig=false\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n </div>\n <div class=\"osl-rg-config-panel__actions\">\n <button class=\"osl-rg-config-action\" (click)=\"autoSizeAll()\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"M21 6H3M21 18H3\"/><path d=\"M8 3 3 6l5 3M16 3l5 3-5 3\"/></svg>\n Auto-size all\n </button>\n <button class=\"osl-rg-config-action\" (click)=\"showAllColumns()\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></svg>\n Show all\n </button>\n </div>\n <div class=\"osl-rg-config-hint\">Drag to reorder \u00B7 Toggle to show/hide \u00B7 Pin to freeze</div>\n <div class=\"osl-rg-config-list\" cdkDropList (cdkDropListDropped)=\"onConfigColumnReorder($event)\">\n @for (col of _cols; track col.key) {\n <div class=\"osl-rg-config-item\" cdkDrag [class.osl-rg-config-item--hidden]=\"col._hidden\">\n <svg cdkDragHandle class=\"rg-drag-handle\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <circle cx=\"9\" cy=\"5\" r=\"1.5\"/><circle cx=\"15\" cy=\"5\" r=\"1.5\"/>\n <circle cx=\"9\" cy=\"12\" r=\"1.5\"/><circle cx=\"15\" cy=\"12\" r=\"1.5\"/>\n <circle cx=\"9\" cy=\"19\" r=\"1.5\"/><circle cx=\"15\" cy=\"19\" r=\"1.5\"/>\n </svg>\n <input type=\"checkbox\" class=\"osl-rg-checkbox\" [checked]=\"!col._hidden\" (change)=\"toggleColumnVisibility(col)\" />\n <span class=\"osl-rg-config-item__label\" [class.osl-rg-config-item__label--hidden]=\"col._hidden\">{{ col.label }}</span>\n <button class=\"osl-rg-config-item__pin-btn\" (click)=\"toggleColumnPin(col)\"\n [class.osl-rg-config-item__pin-btn--active]=\"col._pinned\"\n [title]=\"col._pinned ? 'Unpin' : 'Pin to left'\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"12\" height=\"12\"><path d=\"M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z\"/></svg>\n </button>\n </div>\n }\n </div>\n </div>\n}\n\n\n<!-- \u2550\u2550 COPY TOAST \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (copiedCell !== null) {\n <div class=\"osl-rg-copy-toast\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><polyline points=\"20 6 9 17 4 12\"/></svg>\n Copied to clipboard\n </div>\n}\n", styles: ["@keyframes rg-pulse{0%,to{opacity:1}50%{opacity:.4}}@keyframes rg-fade-up{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}@keyframes rg-slide-in{0%{opacity:0;transform:translate(20px)}to{opacity:1;transform:translate(0)}}.rg-icon{width:15px;height:15px;flex-shrink:0;display:block}.rg-icon--sm{width:14px;height:14px}.rg-icon--xs{width:12px;height:12px}.rg-icon--excel{width:18px;height:18px}.rg-drag-handle{width:14px;height:14px;flex-shrink:0;color:#d1d5db;cursor:grab;display:block}.rg-drag-handle:active{cursor:grabbing}.osl-rg{position:relative;display:flex;flex-direction:column;width:100%;font-family:inherit;font-size:13px;color:#111827;background:#fff;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:10px;overflow:hidden;box-shadow:0 2px 8px #00000012,0 1px 2px #0000000a;-webkit-user-select:none;user-select:none}.osl-rg-toolbar{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 14px;background:linear-gradient(135deg,#f8faff,#f3f4f6);border-bottom:1px solid var(--osl-border-color, #e5e7eb);flex-shrink:0;flex-wrap:wrap}.osl-rg-toolbar__left{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.osl-rg-toolbar__right{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.osl-rg-title{font-size:14px;font-weight:700;color:#111827;letter-spacing:-.01em}.osl-rg-record-count{display:inline-flex;align-items:center;gap:5px;font-size:12px;color:#9ca3af;font-weight:500}.osl-rg-record-count svg{color:#d1d5db}.osl-rg-filter-badge{display:inline-flex;align-items:center;gap:5px;padding:2px 9px 2px 6px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:12px;font-size:11px;color:#1d4ed8;font-weight:600}.osl-rg-filter-badge__dot{width:6px;height:6px;border-radius:50%;background:#2563eb;animation:rg-pulse 1.5s ease-in-out infinite;flex-shrink:0}.osl-rg-toolbar-btn{display:inline-flex;align-items:center;gap:5px;height:32px;padding:0 11px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:7px;background:#fff;color:#5a6478;font-size:12px;font-family:inherit;font-weight:500;cursor:pointer;white-space:nowrap;transition:all .15s ease}.osl-rg-toolbar-btn svg{flex-shrink:0}.osl-rg-toolbar-btn:hover{background:#f3f4f6;color:#1f2937;border-color:#c4c9d4;box-shadow:0 1px 3px #0000000f}.osl-rg-toolbar-btn--on{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8}.osl-rg-toolbar-btn--active{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8;font-weight:600}.osl-rg-toolbar-btn--danger{background:#fff5f5;border-color:#fca5a5;color:#dc2626}.osl-rg-toolbar-btn--danger:hover{background:#fee2e2;border-color:#f87171}.osl-rg-toolbar-btn--excel{background:linear-gradient(135deg,#1a7a3e,#1d6f42);border-color:#166534;color:#fff;font-weight:600;box-shadow:0 1px 3px #1665344d}.osl-rg-toolbar-btn--excel:hover{background:linear-gradient(135deg,#166534,#145a2c);border-color:#14532d;color:#fff;box-shadow:0 2px 6px #16653466}.osl-rg-toolbar-btn--pdf{background:linear-gradient(135deg,#e53935,#c62828);border-color:#b71c1c;color:#fff;font-weight:600;box-shadow:0 1px 3px #b71c1c4d}.osl-rg-toolbar-btn--pdf:hover{background:linear-gradient(135deg,#c62828,#ad1515);border-color:#ad1515;color:#fff;box-shadow:0 2px 6px #b71c1c66}.rg-icon--pdf{width:18px;height:18px}.osl-rg-global-search{position:relative;display:inline-flex;align-items:center}.osl-rg-global-search__icon{position:absolute;left:9px;width:14px;height:14px;color:#9ca3af;pointer-events:none;display:block}.osl-rg-global-search__input{height:32px;padding:0 30px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:7px;background:#fff;font-size:12.5px;font-family:inherit;color:#111827;outline:none;width:210px;transition:border-color .15s,width .2s,box-shadow .15s}.osl-rg-global-search__input::placeholder{color:#b0b7c3}.osl-rg-global-search__input:focus{border-color:var(--osl-primary, #2563eb);width:260px;box-shadow:0 0 0 3px #2563eb1a}.osl-rg-global-search__clear{position:absolute;right:6px;display:flex;align-items:center;justify-content:center;width:18px;height:18px;border:none;background:transparent;color:#9ca3af;cursor:pointer;padding:0;border-radius:4px;transition:all .12s}.osl-rg-global-search__clear svg{width:12px;height:12px}.osl-rg-global-search__clear:hover{color:#374151;background:#0000000d}.osl-rg-group-panel{display:flex;align-items:center;gap:8px;padding:6px 14px;background:linear-gradient(135deg,#fafbff,#f5f7ff);border-bottom:1px dashed #dde5ff;min-height:40px;flex-shrink:0;flex-wrap:wrap}.osl-rg-group-panel__label{display:flex;align-items:center;gap:5px;font-size:11px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.05em;white-space:nowrap;flex-shrink:0}.osl-rg-group-panel__label svg{color:#9ca3af}.osl-rg-group-panel__hint{font-size:11px;color:#b0b7c3;font-style:italic}.osl-rg-group-panel__clear{margin-left:auto;font-size:11px;color:#6b7280;background:none;border:none;cursor:pointer;text-decoration:underline;flex-shrink:0;padding:0;font-family:inherit}.osl-rg-group-panel__clear:hover{color:#dc2626}.osl-rg-group-chips{display:flex;gap:6px;flex-wrap:wrap;align-items:center;flex:1;min-height:26px}.osl-rg-group-chip{display:inline-flex;align-items:center;gap:5px;padding:3px 6px 3px 5px;background:linear-gradient(135deg,#dbeafe,#eff6ff);border:1px solid #bfdbfe;border-radius:20px;font-size:12px;font-weight:600;color:#1e40af;cursor:grab;-webkit-user-select:none;user-select:none;box-shadow:0 1px 2px #2563eb1a;transition:box-shadow .1s}.osl-rg-group-chip:active{cursor:grabbing;box-shadow:0 2px 6px #2563eb33}.osl-rg-group-chip .rg-drag-handle{color:#93c5fd}.osl-rg-group-chip__remove{display:flex;align-items:center;justify-content:center;width:16px;height:16px;border:none;background:transparent;cursor:pointer;padding:0;border-radius:50%;transition:all .1s}.osl-rg-group-chip__remove svg{width:10px;height:10px;color:#93c5fd}.osl-rg-group-chip__remove:hover{background:#dbeafe}.osl-rg-group-chip__remove:hover svg{color:#1e40af}.osl-rg-table-area{display:flex;flex-direction:column;flex:1 1 0;overflow:hidden;position:relative;min-height:0}.osl-rg-header-wrap{overflow:hidden;flex-shrink:0;border-bottom:2px solid #dde5ff;background:linear-gradient(180deg,#f0f4ff,#e8efff);z-index:10;position:relative}.osl-rg-header-row{display:flex;align-items:stretch}.osl-rg-header-row--groups{border-bottom:1px solid #c7d7f0}.osl-rg-th--group-span{display:flex;align-items:center;justify-content:center;height:28px;min-height:28px;cursor:default;padding:0;border-right:1px solid #dde5ff}.osl-rg-th--group-span:hover{background:transparent!important}.osl-rg-th--group-span:last-child{border-right:none}.osl-rg-th--group-span--labeled{background:linear-gradient(135deg,#dbeafe,#eff6ff);border-bottom:2px solid #93c5fd}.osl-rg-th-group-label{font-size:11px;font-weight:700;color:#1e40af;text-transform:uppercase;letter-spacing:.06em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:0 10px;text-align:center;width:100%}.osl-rg-header-cols{display:flex}.osl-rg-th{position:relative;display:flex;flex-direction:column;background:transparent;border-right:1px solid #dde5ff;cursor:pointer;flex-shrink:0;transition:background .12s;overflow:visible}.osl-rg-th:last-child{border-right:none}.osl-rg-th:hover{background:#2563eb0f}.osl-rg-th--pinned{background:linear-gradient(180deg,#e8efff,#dde8ff);border-right:1px solid #bfdbfe;z-index:3}.osl-rg-th--pinned:after{content:\"\";position:absolute;right:-5px;top:0;bottom:0;width:5px;background:linear-gradient(to right,rgba(37,99,235,.14),transparent);pointer-events:none;z-index:4}.osl-rg-th--sorted{background:#6366f114}.osl-rg-th--filtered{background:#f59e0b12}.osl-rg-th-drag-placeholder{background:#e0e7ff;border:2px dashed #6366f1;border-radius:4px;opacity:.6}.osl-rg-th-pin-badge{position:absolute;top:3px;right:24px;color:var(--osl-primary, #2563eb);opacity:.6;display:flex;align-items:center}.osl-rg-th-content{display:flex;align-items:center;justify-content:space-between;gap:4px;padding:0 8px 0 10px;height:36px;min-height:36px}.osl-rg-th-label{font-size:11.5px;font-weight:700;color:#1e3a6e;text-transform:uppercase;letter-spacing:.05em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;min-width:0}.osl-rg-th-actions{display:flex;align-items:center;gap:2px;flex-shrink:0}.osl-rg-sort-icon{display:inline-flex;align-items:center;position:relative;color:#9ca3af;transition:color .12s;width:14px;height:14px}.osl-rg-sort-icon svg{width:13px;height:13px;display:block}.osl-rg-sort-icon--asc,.osl-rg-sort-icon--desc{color:var(--osl-primary, #2563eb)}.osl-rg-sort-idle{opacity:.3}.osl-rg-sort-badge{position:absolute;top:-4px;right:-6px;font-size:8px;font-weight:700;background:var(--osl-primary, #2563eb);color:#fff;border-radius:50%;width:11px;height:11px;display:flex;align-items:center;justify-content:center;line-height:1}.osl-rg-filter-btn,.osl-rg-col-menu-btn{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border:none;background:transparent;color:#b0b7c3;cursor:pointer;padding:0;border-radius:5px;transition:all .12s}.osl-rg-filter-btn svg,.osl-rg-col-menu-btn svg{width:13px;height:13px;display:block}.osl-rg-filter-btn:hover,.osl-rg-col-menu-btn:hover{background:#00000012;color:#374151}.osl-rg-filter-btn--active{color:var(--osl-primary, #2563eb)!important;background:#2563eb1f!important}.osl-rg-col-search{position:relative;display:flex;align-items:center;padding:3px 6px;border-top:1px solid #e0e7ff;background:#ffffff80}.osl-rg-col-search__icon{position:absolute;left:10px;width:12px;height:12px;display:block;pointer-events:none;color:#c4cad6}.osl-rg-col-search__input{width:100%;height:23px;padding:0 18px 0 22px;border:1px solid #dde5ff;border-radius:5px;font-size:11px;font-family:inherit;background:#fff;color:#111827;outline:none;transition:border-color .12s,box-shadow .12s}.osl-rg-col-search__input::placeholder{color:#c4cad6}.osl-rg-col-search__input:focus{border-color:var(--osl-primary, #2563eb);box-shadow:0 0 0 2px #2563eb1a}.osl-rg-col-search__clear{position:absolute;right:8px;display:flex;align-items:center;width:14px;height:14px;border:none;background:transparent;color:#9ca3af;cursor:pointer;padding:0;border-radius:3px;transition:all .1s}.osl-rg-col-search__clear svg{width:10px;height:10px;display:block}.osl-rg-col-search__clear:hover{color:#374151;background:#0000000f}.osl-rg-resize-handle{position:absolute;right:0;top:0;bottom:0;width:6px;cursor:col-resize;z-index:5}.osl-rg-resize-handle:after{content:\"\";position:absolute;right:2px;top:15%;bottom:15%;width:2px;border-radius:2px;background:#a5b4fc;opacity:0;transition:opacity .15s}.osl-rg-resize-handle:hover:after{opacity:1}.osl-rg-th:hover .osl-rg-resize-handle:after{opacity:.4}.osl-rg-excel-filter{position:fixed;z-index:9999;width:250px;background:#fff;border:1px solid #e5e7eb;border-radius:10px;box-shadow:0 12px 32px #00000026,0 2px 8px #00000012;animation:rg-fade-up .14s ease;overflow:hidden}.osl-rg-excel-filter__header{display:flex;align-items:center;justify-content:space-between;padding:9px 12px;background:linear-gradient(135deg,#f0f4ff,#e8efff);border-bottom:1px solid #dde5ff}.osl-rg-excel-filter__title{display:flex;align-items:center;gap:6px;font-size:11px;font-weight:700;color:#1e3a6e;text-transform:uppercase;letter-spacing:.06em}.osl-rg-excel-filter__clear-btn{font-size:11px;color:#dc2626;background:none;border:none;cursor:pointer;font-weight:600;padding:2px 6px;border-radius:4px;font-family:inherit;transition:background .1s}.osl-rg-excel-filter__clear-btn:hover{background:#fee2e2}.osl-rg-excel-filter__search{padding:7px 8px;border-bottom:1px solid #f3f4f6;position:relative}.osl-rg-excel-filter__search-input{width:100%;height:28px;padding:0 8px 0 28px;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;font-family:inherit;outline:none;box-sizing:border-box;transition:border-color .12s;color:#111827}.osl-rg-excel-filter__search-input::placeholder{color:#b0b7c3}.osl-rg-excel-filter__search-input:focus{border-color:var(--osl-primary, #2563eb)}.osl-rg-excel-filter__select-all{padding:0 8px;border-bottom:2px solid #f3f4f6;background:#fafbff}.osl-rg-excel-filter__list{max-height:210px;overflow-y:auto;padding:3px 8px}.osl-rg-excel-filter__list::-webkit-scrollbar{width:5px}.osl-rg-excel-filter__list::-webkit-scrollbar-thumb{background:#d1d5db;border-radius:4px}.osl-rg-excel-filter__list::-webkit-scrollbar-thumb:hover{background:#9ca3af}.osl-rg-excel-filter__item{display:flex;align-items:center;gap:8px;padding:5px 4px;font-size:12.5px;color:#374151;cursor:pointer;border-radius:5px;transition:background .1s}.osl-rg-excel-filter__item:hover{background:#f3f4f6}.osl-rg-excel-filter__item--all{font-weight:600;color:#1f2937;padding:6px 4px}.osl-rg-excel-filter__item-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.osl-rg-excel-filter__footer{display:flex;gap:6px;padding:8px 10px;border-top:1px solid #f3f4f6;background:#f9fafb}.osl-rg-excel-filter__apply{flex:1;height:30px;border:none;border-radius:6px;font-size:12px;font-family:inherit;font-weight:600;cursor:pointer;background:var(--osl-primary, #2563eb);color:#fff;transition:background .12s}.osl-rg-excel-filter__apply:hover{background:#1d4ed8}.osl-rg-excel-filter__cancel{height:30px;padding:0 14px;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;font-family:inherit;font-weight:500;cursor:pointer;background:#fff;color:#6b7280;transition:all .12s}.osl-rg-excel-filter__cancel:hover{background:#f3f4f6;border-color:#9ca3af}.osl-rg-th--checkbox,.osl-rg-td--checkbox{display:flex;align-items:center;justify-content:center;width:36px;min-width:36px;flex-shrink:0;border-right:1px solid #dde5ff}.osl-rg-checkbox{width:14px;height:14px;cursor:pointer;accent-color:var(--osl-primary, #2563eb);flex-shrink:0}.osl-rg-body{overflow-x:auto;overflow-y:auto;flex-shrink:0}.osl-rg-body::-webkit-scrollbar{width:8px;height:8px}.osl-rg-body::-webkit-scrollbar-track{background:#f9fafb}.osl-rg-body::-webkit-scrollbar-thumb{background:#d1d5db;border-radius:4px}.osl-rg-body::-webkit-scrollbar-thumb:hover{background:#9ca3af}.osl-rg-body::-webkit-scrollbar-corner{background:#f9fafb}.osl-rg-inner{display:block}.osl-rg-row{display:flex;align-items:stretch;border-bottom:1px solid #f3f4f6;transition:background .08s;cursor:default}.osl-rg-row:hover{background:#f0f5ff!important}.osl-rg-row--striped{background:#fafbff}.osl-rg-row--selected{background:#eff6ff!important;border-bottom-color:#dbeafe}.osl-rg-row--skeleton{pointer-events:none}.osl-rg-td{display:flex;align-items:center;padding:0 12px;border-right:1px solid #f0f2f7;overflow:hidden;background:inherit;flex-shrink:0}.osl-rg-td:last-child{border-right:none}.osl-rg-td--pinned{background:#fff;border-right:1px solid #e0e7ff;z-index:2}.osl-rg-td--pinned:after{content:\"\";position:absolute;right:-5px;top:0;bottom:0;width:5px;background:linear-gradient(to right,rgba(37,99,235,.07),transparent);pointer-events:none}.osl-rg-cell-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;width:100%;font-size:13px;color:#1f2937;line-height:1.4}.rg-profit-pos .osl-rg-cell-text{color:#15803d;font-weight:600}.rg-profit-neg .osl-rg-cell-text{color:#dc2626;font-weight:600}.rg-status-delivered .osl-rg-cell-text{color:#15803d;font-weight:600}.rg-status-shipped .osl-rg-cell-text{color:#2563eb;font-weight:600}.rg-status-processing .osl-rg-cell-text{color:#d97706;font-weight:600}.rg-status-pending .osl-rg-cell-text{color:#6b7280;font-weight:500}.rg-status-cancelled .osl-rg-cell-text{color:#dc2626;font-weight:500;text-decoration:line-through;opacity:.75}.osl-rg-group-row{display:flex;align-items:center;gap:7px;border-bottom:1px solid #e5e7eb;font-weight:600;cursor:pointer;flex-shrink:0;transition:filter .1s}.osl-rg-group-row--l0{background:linear-gradient(135deg,#eff6ff,#dbeafe);color:#1e40af;border-left:4px solid #3b82f6}.osl-rg-group-row--l1{background:linear-gradient(135deg,#f5f3ff,#ede9fe);color:#5b21b6;border-left:4px solid #8b5cf6}.osl-rg-group-row--l2{background:linear-gradient(135deg,#fff7ed,#fef3c7);color:#92400e;border-left:4px solid #f59e0b}.osl-rg-group-row:hover{filter:brightness(.97)}.osl-rg-group-row__key{font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;opacity:.65;flex-shrink:0}.osl-rg-group-row__value{font-size:13px;font-weight:700;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.osl-rg-group-row__count{font-size:11px;font-weight:600;padding:2px 9px;border-radius:12px;background:#00000017;margin-right:12px;flex-shrink:0}.osl-rg-group-chevron{width:16px;height:16px;flex-shrink:0;opacity:.75;display:block}.osl-rg-aggregate-row{display:flex;align-items:center;border-top:2px solid #e5e7eb;background:linear-gradient(135deg,#f0fdf4,#f9fafb);flex-shrink:0;min-height:38px}.osl-rg-agg-cell{display:flex;align-items:center;gap:5px;padding:0 12px;border-right:1px solid #e5e7eb;height:38px;flex-shrink:0;overflow:hidden}.osl-rg-agg-cell--checkbox{width:36px;min-width:36px}.osl-rg-agg-label{font-size:9.5px;font-weight:800;color:#9ca3af;text-transform:uppercase;letter-spacing:.06em;flex-shrink:0;background:#e5e7eb;padding:1px 4px;border-radius:3px}.osl-rg-agg-value{font-size:12px;font-weight:700;color:#15803d;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.osl-rg-skel{height:12px;border-radius:4px;background:linear-gradient(90deg,#e5e7eb 25%,#f3f4f6,#e5e7eb 75%);background-size:200% 100%;animation:rg-pulse 1.5s ease-in-out infinite}.osl-rg-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:64px 20px;color:#9ca3af;text-align:center}.osl-rg-empty__icon{margin-bottom:18px;opacity:.7}.osl-rg-empty__title{font-size:15px;font-weight:600;color:#374151;margin:0 0 6px}.osl-rg-empty__sub{font-size:13px;color:#9ca3af;margin:0 0 18px;max-width:290px;line-height:1.5}.osl-rg-empty__action{height:34px;padding:0 18px;background:var(--osl-primary, #2563eb);color:#fff;border:none;border-radius:7px;font-size:13px;font-family:inherit;font-weight:600;cursor:pointer;transition:background .15s;box-shadow:0 2px 6px #2563eb4d}.osl-rg-empty__action:hover{background:#1d4ed8}.osl-rg-pagination{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 14px;border-top:1px solid var(--osl-border-color, #e5e7eb);background:linear-gradient(135deg,#f8faff,#f3f4f6);flex-shrink:0;flex-wrap:wrap}.osl-rg-pagination__info{font-size:12px;color:#6b7280;min-width:160px;white-space:nowrap}.osl-rg-pagination__info strong{color:#1f2937;font-weight:600}.osl-rg-pagination__controls{display:flex;align-items:center;gap:3px}.osl-rg-pagination__size{display:flex;align-items:center;gap:7px;min-width:120px;justify-content:flex-end}.osl-rg-pagination__size-label{font-size:11.5px;color:#9ca3af;white-space:nowrap}.osl-rg-page-btn{display:inline-flex;align-items:center;justify-content:center;gap:3px;height:30px;padding:0 8px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:6px;background:#fff;color:#374151;font-size:12px;font-family:inherit;font-weight:500;cursor:pointer;transition:all .12s;white-space:nowrap}.osl-rg-page-btn svg{display:block}.osl-rg-page-btn:hover:not(:disabled){background:#f3f4f6;border-color:#9ca3af;box-shadow:0 1px 2px #0000000f}.osl-rg-page-btn:disabled{opacity:.35;cursor:not-allowed}.osl-rg-page-btn--num{min-width:30px;padding:0;font-size:12.5px}.osl-rg-page-btn--active{background:var(--osl-primary, #2563eb);border-color:var(--osl-primary, #2563eb);color:#fff;font-weight:700;box-shadow:0 2px 5px #2563eb59}.osl-rg-page-btn--active:hover:not(:disabled){background:#1d4ed8;border-color:#1d4ed8}.osl-rg-page-ellipsis{display:inline-flex;align-items:center;justify-content:center;width:26px;color:#9ca3af;font-size:13px;pointer-events:none}.osl-rg-page-size{height:30px;padding:0 8px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:6px;background:#fff;font-size:12px;font-family:inherit;color:#374151;cursor:pointer;outline:none;transition:border-color .15s}.osl-rg-page-size:focus{border-color:var(--osl-primary, #2563eb)}.osl-rg-col-menu{position:fixed;z-index:9999;min-width:210px;background:#fff;border:1px solid #e5e7eb;border-radius:11px;box-shadow:0 12px 36px #00000029,0 3px 10px #00000014;overflow:hidden;animation:rg-fade-up .13s cubic-bezier(.16,1,.3,1)}.osl-rg-col-menu__header{padding:9px 14px 7px;font-size:10.5px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:#9ca3af;border-bottom:1px solid #f3f4f6;background:#fafbff}.osl-rg-col-menu__divider{height:1px;background:#f3f4f6;margin:2px 0}.osl-rg-col-menu__item{display:flex;align-items:center;gap:9px;width:100%;padding:9px 14px;background:transparent;border:none;border-left:3px solid transparent;border-bottom:none;text-align:left;font-size:13px;font-weight:500;font-family:inherit;color:#374151;cursor:pointer;transition:all .1s}.osl-rg-col-menu__item svg{color:#9ca3af;display:block}.osl-rg-col-menu__item:hover{background:#f0f4ff;color:var(--osl-primary, #2563eb);border-left-color:var(--osl-primary, #2563eb)}.osl-rg-col-menu__item:hover svg{color:var(--osl-primary, #2563eb)}.osl-rg-col-menu__item--danger:hover{background:#fff5f5;color:#dc2626;border-left-color:#dc2626}.osl-rg-col-menu__item--danger:hover svg{color:#dc2626}.osl-rg-config-backdrop{position:fixed;inset:0;z-index:9000;background:#0003;-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px)}.osl-rg-config-panel{position:fixed;top:0;right:0;bottom:0;z-index:9001;width:300px;background:#fff;border-left:1px solid #e5e7eb;box-shadow:-6px 0 24px #00000021;display:flex;flex-direction:column;animation:rg-slide-in .2s cubic-bezier(.16,1,.3,1)}.osl-rg-config-panel__header{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border-bottom:1px solid #e5e7eb;background:linear-gradient(135deg,#f0f4ff,#e8efff);flex-shrink:0}.osl-rg-config-panel__title{display:flex;align-items:center;gap:8px;font-size:14px;font-weight:700;color:#1e3a6e}.osl-rg-config-panel__title svg{color:#3b82f6}.osl-rg-config-panel__close{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border:none;background:transparent;color:#9ca3af;cursor:pointer;border-radius:7px;padding:0;transition:all .12s}.osl-rg-config-panel__close svg{width:16px;height:16px;display:block}.osl-rg-config-panel__close:hover{background:#00000012;color:#374151}.osl-rg-config-panel__actions{display:flex;gap:8px;padding:10px 14px;border-bottom:1px solid #f3f4f6;flex-shrink:0;background:#fafbff}.osl-rg-config-hint{padding:6px 16px;font-size:10.5px;color:#b0b7c3;font-style:italic;border-bottom:1px solid #f3f4f6;flex-shrink:0}.osl-rg-config-action{display:inline-flex;align-items:center;gap:5px;height:30px;padding:0 10px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;color:#374151;font-size:12px;font-family:inherit;font-weight:500;cursor:pointer;transition:all .12s}.osl-rg-config-action svg{display:block}.osl-rg-config-action:hover{background:#f3f4f6;border-color:#9ca3af}.osl-rg-config-list{flex:1;overflow-y:auto;padding:6px 0}.osl-rg-config-list::-webkit-scrollbar{width:5px}.osl-rg-config-list::-webkit-scrollbar-thumb{background:#e5e7eb;border-radius:4px}.osl-rg-config-list::-webkit-scrollbar-thumb:hover{background:#d1d5db}.osl-rg-config-item{display:flex;align-items:center;gap:9px;padding:7px 14px;transition:background .1s;cursor:grab}.osl-rg-config-item:hover{background:#f9fafb}.osl-rg-config-item:active{cursor:grabbing}.osl-rg-config-item--hidden{opacity:.5}.osl-rg-config-item__label{flex:1;font-size:13px;color:#374151;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}.osl-rg-config-item__label--hidden{color:#9ca3af;font-style:italic}.osl-rg-config-item__pin-btn{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border:1px solid #e5e7eb;background:#fff;color:#d1d5db;cursor:pointer;padding:0;border-radius:5px;transition:all .12s;flex-shrink:0}.osl-rg-config-item__pin-btn svg{display:block}.osl-rg-config-item__pin-btn:hover,.osl-rg-config-item__pin-btn--active{background:#eff6ff;border-color:#93c5fd;color:var(--osl-primary, #2563eb)}.osl-rg-copy-toast{position:fixed;bottom:24px;left:50%;transform:translate(-50%);z-index:99999;display:flex;align-items:center;gap:7px;background:#1f2937;color:#fff;font-size:12.5px;font-weight:500;padding:8px 18px;border-radius:22px;animation:rg-fade-up .2s ease;box-shadow:0 6px 16px #0000004d;pointer-events:none;white-space:nowrap;font-family:inherit}.osl-rg-copy-toast svg{flex-shrink:0;color:#4ade80}\n"], dependencies: [{ kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.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$2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i3$1.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i3$1.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i3$1.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "directive", type: i3$1.CdkDragPlaceholder, selector: "ng-template[cdkDragPlaceholder]", inputs: ["data"] }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }, { kind: "pipe", type: i1$1.UpperCasePipe, name: "uppercase" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5668
5688
|
}
|
|
5669
5689
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: OslReportGrid, decorators: [{
|
|
5670
5690
|
type: Component,
|