@tolle_/tolle-ui 18.2.27 → 18.2.29

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.
@@ -19,6 +19,7 @@ export declare class DataTableComponent implements OnInit, OnChanges {
19
19
  size: 'xs' | 'sm' | 'default' | 'lg';
20
20
  stickyHeader: boolean;
21
21
  maxHeight?: string;
22
+ bordered: boolean;
22
23
  allowColumnHiding: boolean;
23
24
  showSettings: boolean;
24
25
  columnVisibility: Record<string, boolean>;
@@ -52,5 +53,5 @@ export declare class DataTableComponent implements OnInit, OnChanges {
52
53
  toggleRow(index: number): void;
53
54
  getTemplate(key: string): TolleCellDirective | undefined;
54
55
  static ɵfac: i0.ɵɵFactoryDeclaration<DataTableComponent, never>;
55
- static ɵcmp: i0.ɵɵComponentDeclaration<DataTableComponent, "tolle-data-table", never, { "data": { "alias": "data"; "required": false; }; "columns": { "alias": "columns"; "required": false; }; "searchable": { "alias": "searchable"; "required": false; }; "paginate": { "alias": "paginate"; "required": false; }; "pageSizeOptions": { "alias": "pageSizeOptions"; "required": false; }; "pageSize": { "alias": "pageSize"; "required": false; }; "expandable": { "alias": "expandable"; "required": false; }; "size": { "alias": "size"; "required": false; }; "stickyHeader": { "alias": "stickyHeader"; "required": false; }; "maxHeight": { "alias": "maxHeight"; "required": false; }; "allowColumnHiding": { "alias": "allowColumnHiding"; "required": false; }; "showSettings": { "alias": "showSettings"; "required": false; }; "expandedTemplate": { "alias": "expandedTemplate"; "required": false; }; }, {}, ["cellTemplates"], never, true, never>;
56
+ static ɵcmp: i0.ɵɵComponentDeclaration<DataTableComponent, "tolle-data-table", never, { "data": { "alias": "data"; "required": false; }; "columns": { "alias": "columns"; "required": false; }; "searchable": { "alias": "searchable"; "required": false; }; "paginate": { "alias": "paginate"; "required": false; }; "pageSizeOptions": { "alias": "pageSizeOptions"; "required": false; }; "pageSize": { "alias": "pageSize"; "required": false; }; "expandable": { "alias": "expandable"; "required": false; }; "size": { "alias": "size"; "required": false; }; "stickyHeader": { "alias": "stickyHeader"; "required": false; }; "maxHeight": { "alias": "maxHeight"; "required": false; }; "bordered": { "alias": "bordered"; "required": false; }; "allowColumnHiding": { "alias": "allowColumnHiding"; "required": false; }; "showSettings": { "alias": "showSettings"; "required": false; }; "expandedTemplate": { "alias": "expandedTemplate"; "required": false; }; }, {}, ["cellTemplates"], never, true, never>;
56
57
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tolle_/tolle-ui",
3
- "version": "18.2.27",
3
+ "version": "18.2.29",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1476,6 +1476,12 @@
1476
1476
  "type": "string",
1477
1477
  "required": false
1478
1478
  },
1479
+ {
1480
+ "name": "bordered",
1481
+ "type": "boolean",
1482
+ "default": "true",
1483
+ "required": false
1484
+ },
1479
1485
  {
1480
1486
  "name": "allowColumnHiding",
1481
1487
  "type": "boolean",
@@ -592,6 +592,7 @@ Inputs:
592
592
  | size | `'xs' | 'sm' | 'default' | 'lg'` | `'default'` | |
593
593
  | stickyHeader | `boolean` | `false` | |
594
594
  | maxHeight | `string` | — | |
595
+ | bordered | `boolean` | `true` | |
595
596
  | allowColumnHiding | `boolean` | `true` | |
596
597
  | showSettings | `boolean` | `true` | |
597
598
  | expandedTemplate | `TemplateRef<any>` | — | |
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "package": "@tolle_/tolle-ui",
3
- "version": "18.2.27",
3
+ "version": "18.2.29",
4
4
  "registry": "r/{name}.json",
5
5
  "components": [
6
6
  {
@@ -1714,6 +1714,12 @@
1714
1714
  "type": "string",
1715
1715
  "required": false
1716
1716
  },
1717
+ {
1718
+ "name": "bordered",
1719
+ "type": "boolean",
1720
+ "default": "true",
1721
+ "required": false
1722
+ },
1717
1723
  {
1718
1724
  "name": "allowColumnHiding",
1719
1725
  "type": "boolean",
@@ -22,7 +22,7 @@
22
22
  {
23
23
  "path": "data-table.component.ts",
24
24
  "target": "ui/data-table.component.ts",
25
- "content": "import {\n Component, Input, OnInit, OnChanges, SimpleChanges,\n TemplateRef, ContentChildren, QueryList\n} from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\nimport { cn } from './utils/cn';\nimport { PaginationComponent } from './pagination.component';\nimport { TolleCellDirective } from './tolle-cell.directive';\nimport { InputComponent } from './input.component';\nimport { PopoverComponent } from './popover.component';\nimport { PopoverContentComponent } from './popover-content.component';\nimport { CheckboxComponent } from './checkbox.component';\n\nexport interface TableColumn {\n key: string;\n label: string;\n sortable?: boolean;\n class?: string;\n}\n\n@Component({\n selector: 'tolle-data-table',\n standalone: true,\n styles: [`\n :host { display: block; width: 100%; height: 100%; }\n table { width: 100%; border-collapse: collapse; display: table; }\n thead { display: table-header-group; }\n tbody { display: table-row-group; }\n tr { display: table-row; }\n th, td { display: table-cell; }\n `],\n imports: [\n CommonModule,\n FormsModule,\n PaginationComponent,\n InputComponent,\n PopoverComponent,\n PopoverContentComponent,\n CheckboxComponent,\n ],\n template: `\n <div class=\"h-full flex flex-col w-full\">\n <div class=\"flex items-center justify-between py-2 flex-none\">\n <div *ngIf=\"searchable\" class=\"w-full max-w-sm\">\n <tolle-input\n [size]=\"size === 'lg' ? 'default' : 'sm'\"\n class=\"w-full\"\n aria-label=\"Filter records\"\n placeholder=\"Filter records...\"\n [(ngModel)]=\"searchTerm\"\n (ngModelChange)=\"onSearch()\">\n <i prefix class=\"ri-search-line\"></i>\n </tolle-input>\n </div>\n\n <tolle-popover *ngIf=\"allowColumnHiding && showSettings\" class=\"ml-auto\">\n <button trigger\n [class]=\"cn(\n 'ml-auto flex items-center gap-2 rounded-md border border-input bg-background px-3 hover:bg-accent hover:text-accent-foreground transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring',\n size === 'xs' ? 'h-7 text-xs' : 'h-9 text-sm'\n )\">\n <i class=\"ri-settings-3-line\"></i>\n View\n <i class=\"ri-arrow-down-s-line ml-1 opacity-50\"></i>\n </button>\n <tolle-popover-content class=\"w-56 p-0 bg-background border-border\">\n <div class=\"p-2 border-b border-border\">\n <p class=\"text-xs font-medium text-muted-foreground px-2\">Toggle Columns</p>\n </div>\n <div class=\"p-2 max-h-[300px] overflow-y-auto space-y-1\">\n <label *ngFor=\"let col of columns\" class=\"flex items-center gap-2 px-2 py-1.5 rounded-sm hover:bg-accent cursor-pointer text-sm transition-colors\">\n <tolle-checkbox [ngModel]=\"columnVisibility[col.key]\"\n (ngModelChange)=\"toggleColumn(col.key)\"/>\n <span>{{ col.label }}</span>\n </label>\n </div>\n </tolle-popover-content>\n </tolle-popover>\n </div>\n\n <div class=\"flex-1 min-h-0 overflow-hidden relative w-full\">\n <div class=\"overflow-auto w-full h-full\" [style.max-height]=\"maxHeight\">\n\n <table class=\"w-full table-auto border-collapse\">\n <thead class=\"border-b border-border bg-background\">\n <tr>\n <th *ngIf=\"expandable\" scope=\"col\" [class]=\"cn('px-4', size === 'xs' ? 'w-[32px]' : 'w-[48px]', stickyHeader ? 'sticky top-0 z-10 bg-background' : '')\"></th>\n\n <th *ngFor=\"let col of activeColumns\"\n scope=\"col\"\n [attr.aria-sort]=\"sortKey === col.key ? (sortDir === 'asc' ? 'ascending' : (sortDir === 'desc' ? 'descending' : 'none')) : 'none'\"\n [class]=\"cn('text-left font-medium text-foreground',headerPaddingClass,fontSizeClass, col.class, stickyHeader ? 'sticky top-0 z-10 bg-background shadow-sm' : '')\">\n\n <button *ngIf=\"col.sortable; else plainHeader\"\n type=\"button\"\n (click)=\"toggleSort(col.key)\"\n class=\"flex items-center gap-1 cursor-pointer select-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm\">\n {{ col.label }}\n <i [class]=\"getSortIcon(col.key)\" aria-hidden=\"true\"></i>\n </button>\n\n <ng-template #plainHeader>\n {{ col.label }}\n </ng-template>\n </th>\n </tr>\n </thead>\n\n <tbody class=\"divide-y divide-border\">\n <ng-container *ngFor=\"let row of pagedData; let i = index\">\n <tr class=\"hover:bg-muted/50 transition-colors\">\n <td *ngIf=\"expandable\" class=\"px-4\">\n <button (click)=\"toggleRow(i)\"\n [attr.aria-label]=\"expandedRows.has(i) ? 'Collapse row' : 'Expand row'\"\n [attr.aria-expanded]=\"expandedRows.has(i)\"\n [class]=\"cn(\n 'rounded-md hover:bg-accent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n size === 'xs' ? 'h-6 w-6' : 'h-8 w-8'\n )\">\n <i [class]=\"expandedRows.has(i)\n ? 'ri-arrow-down-s-line'\n : 'ri-arrow-right-s-line'\" aria-hidden=\"true\">\n </i>\n </button>\n </td>\n\n <td *ngFor=\"let col of activeColumns\"\n [class]=\"cn('align-middle',cellPaddingClass,fontSizeClass,col.class)\">\n\n <ng-container *ngIf=\"getTemplate(col.key) as cell; else defaultCell\">\n <ng-container\n *ngTemplateOutlet=\"cell.template; context: { $implicit: row[col.key], row: row }\">\n </ng-container>\n </ng-container>\n\n <ng-template #defaultCell>\n {{ row[col.key] }}\n </ng-template>\n </td>\n </tr>\n\n <tr *ngIf=\"expandedRows.has(i)\" class=\"bg-muted/10\">\n <td [attr.colspan]=\"activeColumns.length + (expandable ? 1 : 0)\"\n class=\"p-0\">\n <div class=\"p-6 border-t border-dashed border-border\">\n <ng-container *ngIf=\"expandedTemplate; else noExpanded\">\n <ng-container\n *ngTemplateOutlet=\"expandedTemplate; context: { row: row }\">\n </ng-container>\n </ng-container>\n\n <ng-template #noExpanded>\n <p class=\"text-xs text-muted-foreground italic\">\n No details available.\n </p>\n </ng-template>\n </div>\n </td>\n </tr>\n </ng-container>\n\n <tr *ngIf=\"pagedData.length === 0\">\n <td [attr.colspan]=\"activeColumns.length + (expandable ? 1 : 0)\"\n class=\"h-24 text-center text-muted-foreground\">\n No results found.\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n\n <div class=\"flex-none\">\n <tolle-pagination\n *ngIf=\"paginate && filteredData.length > 0\"\n [totalRecords]=\"filteredData.length\"\n [pageSizeOptions]=\"pageSizeOptions\"\n [currentPage]=\"currentPage\"\n [currentPageSize]=\"pageSize\"\n (onPageNumberChange)=\"handlePageChange($event)\"\n (onPageSizeChange)=\"handlePageSizeChange($event)\"\n ></tolle-pagination>\n </div>\n </div>\n `\n})\nexport class DataTableComponent implements OnInit, OnChanges {\n @Input() data: any[] = [];\n @Input() columns: TableColumn[] = [];\n @Input() searchable = true;\n @Input() paginate = true;\n @Input() pageSizeOptions: number[] = []\n @Input() pageSize = 10;\n @Input() expandable = false;\n\n @Input() size: 'xs' | 'sm' | 'default' | 'lg' = 'default';\n @Input() stickyHeader = false;\n @Input() maxHeight?: string;\n\n // --- Column Hiding Settings ---\n @Input() allowColumnHiding = true;\n @Input() showSettings = true;\n\n // Track visibility state\n columnVisibility: Record<string, boolean> = {};\n\n // Filters columns based on visibility state\n get activeColumns() {\n return this.columns.filter(col => this.columnVisibility[col.key]);\n }\n\n expandedRows = new Set<number>();\n @ContentChildren(TolleCellDirective) cellTemplates!: QueryList<TolleCellDirective>;\n @Input() expandedTemplate?: TemplateRef<any>;\n\n filteredData: any[] = [];\n pagedData: any[] = [];\n searchTerm = '';\n currentPage = 1;\n sortKey = '';\n sortDir: 'asc' | 'desc' | null = null;\n\n // Style Getters\n get cellPaddingClass(): string {\n switch (this.size) {\n case 'xs': return 'p-1 px-4';\n case 'sm': return 'p-2 px-4';\n case 'lg': return 'p-6 px-4';\n default: return 'p-4';\n }\n }\n\n get headerPaddingClass(): string {\n switch (this.size) {\n case 'xs': return 'h-7 px-4';\n case 'sm': return 'h-9 px-4';\n case 'lg': return 'h-14 px-4';\n default: return 'h-12 px-4';\n }\n }\n\n get fontSizeClass(): string {\n switch (this.size) {\n case 'xs': return 'text-[11px]';\n case 'sm': return 'text-xs';\n case 'lg': return 'text-base';\n default: return 'text-sm';\n }\n }\n\n protected cn = cn;\n\n ngOnInit() {\n this.initializeVisibility();\n this.refreshTable();\n }\n\n ngOnChanges(changes: SimpleChanges) {\n if (changes['data'] && !changes['data'].firstChange) {\n this.currentPage = 1; // Reset to page 1 when data changes\n this.refreshTable();\n }\n if (changes['columns']) {\n this.initializeVisibility();\n }\n }\n\n private initializeVisibility() {\n this.columns.forEach(col => {\n // Only set true if undefined, preserves user selection on other data refreshes\n if (this.columnVisibility[col.key] === undefined) {\n this.columnVisibility[col.key] = true;\n }\n });\n }\n\n refreshTable() {\n this.applySearch();\n this.applySort();\n this.updatePage();\n }\n onSearch() {\n this.currentPage = 1;\n this.refreshTable();\n }\n\n // Toggle function used by the checkbox in popover\n toggleColumn(key: string) {\n this.columnVisibility[key] = !this.columnVisibility[key];\n // No explicit refresh needed as the getter 'activeColumns' will automatically re-evaluate\n // when change detection runs after the click event.\n }\n\n private applySearch() {\n if (!this.searchTerm) {\n this.filteredData =\n [...this.data];\n return;\n }\n const q = this.searchTerm.toLowerCase();\n this.filteredData = this.data.filter(row =>\n Object.values(row).some(val => String(val).toLowerCase().includes(q))\n );\n }\n\n private applySort() {\n if (!this.sortKey || !this.sortDir) return;\n this.filteredData.sort((a, b) => {\n const valA = a[this.sortKey]; const valB = b[this.sortKey];\n if (valA < valB) return this.sortDir === 'asc' ? -1 : 1;\n if (valA > valB) return this.sortDir === 'asc' ? 1 : -1;\n return 0;\n });\n }\n\n handlePageChange(newPage: number) {\n this.currentPage = newPage;\n this.updatePage();\n }\n\n handlePageSizeChange(newSize: number) {\n if (newSize !== this.pageSize) {\n this.pageSize = newSize;\n this.currentPage = 1; // Reset to page 1 when size changes\n this.updatePage();\n }\n }\n\n updatePage() {\n if (!this.paginate) {\n this.pagedData = this.filteredData;\n return;\n }\n\n const start = (this.currentPage - 1) * this.pageSize;\n const end = start + this.pageSize;\n\n // Safety check if current page is out of bounds after filtering\n if (start >= this.filteredData.length && this.currentPage > 1) {\n this.currentPage = 1;\n this.updatePage();\n return;\n }\n\n this.pagedData = this.filteredData.slice(start, end);\n }\n\n toggleSort(key: string) {\n if (this.sortKey === key) {\n this.sortDir = this.sortDir === 'asc' ? 'desc' : this.sortDir === 'desc' ? null : 'asc';\n } else {\n this.sortKey = key;\n this.sortDir = 'asc';\n }\n this.currentPage = 1; // Reset to page 1 when sorting\n this.refreshTable();\n }\n\n getSortIcon(key: string) {\n if (this.sortKey !== key || !this.sortDir) return 'ri-expand-up-down-line opacity-20'; // Changed icon for inactive state\n return this.sortDir === 'asc' ? 'ri-arrow-up-line text-foreground' : 'ri-arrow-down-line text-foreground';\n }\n\n toggleRow(index: number) {\n if (this.expandedRows.has(index)) {\n this.expandedRows.delete(index);\n }\n else {\n this.expandedRows.add(index);\n }\n }\n\n getTemplate(key: string): TolleCellDirective | undefined {\n return this.cellTemplates?.find(t => t.name === key);\n }\n}\n"
25
+ "content": "import {\n Component, Input, OnInit, OnChanges, SimpleChanges,\n TemplateRef, ContentChildren, QueryList\n} from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\nimport { cn } from './utils/cn';\nimport { PaginationComponent } from './pagination.component';\nimport { TolleCellDirective } from './tolle-cell.directive';\nimport { InputComponent } from './input.component';\nimport { PopoverComponent } from './popover.component';\nimport { PopoverContentComponent } from './popover-content.component';\nimport { CheckboxComponent } from './checkbox.component';\n\nexport interface TableColumn {\n key: string;\n label: string;\n sortable?: boolean;\n class?: string;\n}\n\n@Component({\n selector: 'tolle-data-table',\n standalone: true,\n styles: [`\n :host { display: block; width: 100%; height: 100%; }\n table { width: 100%; border-collapse: collapse; display: table; }\n thead { display: table-header-group; }\n tbody { display: table-row-group; }\n tr { display: table-row; }\n th, td { display: table-cell; }\n `],\n imports: [\n CommonModule,\n FormsModule,\n PaginationComponent,\n InputComponent,\n PopoverComponent,\n PopoverContentComponent,\n CheckboxComponent,\n ],\n template: `\n <div class=\"h-full flex flex-col w-full\">\n <div class=\"flex items-center justify-between py-2 flex-none\">\n <div *ngIf=\"searchable\" class=\"w-full max-w-sm\">\n <tolle-input\n [size]=\"size === 'lg' ? 'default' : 'sm'\"\n class=\"w-full\"\n aria-label=\"Filter records\"\n placeholder=\"Filter records...\"\n [(ngModel)]=\"searchTerm\"\n (ngModelChange)=\"onSearch()\">\n <i prefix class=\"ri-search-line\"></i>\n </tolle-input>\n </div>\n\n <tolle-popover *ngIf=\"allowColumnHiding && showSettings\" class=\"ml-auto\">\n <button trigger\n [class]=\"cn(\n 'ml-auto flex items-center gap-2 rounded-md border border-input bg-background px-3 hover:bg-accent hover:text-accent-foreground transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring',\n size === 'xs' ? 'h-7 text-xs' : 'h-9 text-sm'\n )\">\n <i class=\"ri-settings-3-line\"></i>\n View\n <i class=\"ri-arrow-down-s-line ml-1 opacity-50\"></i>\n </button>\n <tolle-popover-content class=\"w-56 p-0 bg-background border-border\">\n <div class=\"p-2 border-b border-border\">\n <p class=\"text-xs font-medium text-muted-foreground px-2\">Toggle Columns</p>\n </div>\n <div class=\"p-2 max-h-[300px] overflow-y-auto space-y-1\">\n <label *ngFor=\"let col of columns\" class=\"flex items-center gap-2 px-2 py-1.5 rounded-sm hover:bg-accent cursor-pointer text-sm transition-colors\">\n <tolle-checkbox [ngModel]=\"columnVisibility[col.key]\"\n (ngModelChange)=\"toggleColumn(col.key)\"/>\n <span>{{ col.label }}</span>\n </label>\n </div>\n </tolle-popover-content>\n </tolle-popover>\n </div>\n\n <div [class]=\"cn('flex-1 min-h-0 overflow-hidden relative w-full', bordered ? 'rounded-md border border-border' : '')\">\n <div class=\"overflow-auto w-full h-full\" [style.max-height]=\"maxHeight\">\n\n <table class=\"w-full table-auto border-collapse\">\n <thead class=\"border-b border-border bg-background\">\n <tr>\n <th *ngIf=\"expandable\" scope=\"col\" [class]=\"cn('px-4', size === 'xs' ? 'w-[32px]' : 'w-[48px]', stickyHeader ? 'sticky top-0 z-10 bg-background' : '')\"></th>\n\n <th *ngFor=\"let col of activeColumns\"\n scope=\"col\"\n [attr.aria-sort]=\"sortKey === col.key ? (sortDir === 'asc' ? 'ascending' : (sortDir === 'desc' ? 'descending' : 'none')) : 'none'\"\n [class]=\"cn('text-left font-medium text-foreground',headerPaddingClass,fontSizeClass, col.class, stickyHeader ? 'sticky top-0 z-10 bg-background shadow-sm' : '')\">\n\n <button *ngIf=\"col.sortable; else plainHeader\"\n type=\"button\"\n (click)=\"toggleSort(col.key)\"\n class=\"flex items-center gap-1 cursor-pointer select-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm\">\n {{ col.label }}\n <i [class]=\"getSortIcon(col.key)\" aria-hidden=\"true\"></i>\n </button>\n\n <ng-template #plainHeader>\n {{ col.label }}\n </ng-template>\n </th>\n </tr>\n </thead>\n\n <tbody class=\"divide-y divide-border\">\n <ng-container *ngFor=\"let row of pagedData; let i = index\">\n <tr class=\"hover:bg-muted/50 transition-colors\">\n <td *ngIf=\"expandable\" class=\"px-4\">\n <button (click)=\"toggleRow(i)\"\n [attr.aria-label]=\"expandedRows.has(i) ? 'Collapse row' : 'Expand row'\"\n [attr.aria-expanded]=\"expandedRows.has(i)\"\n [class]=\"cn(\n 'rounded-md hover:bg-accent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n size === 'xs' ? 'h-6 w-6' : 'h-8 w-8'\n )\">\n <i [class]=\"expandedRows.has(i)\n ? 'ri-arrow-down-s-line'\n : 'ri-arrow-right-s-line'\" aria-hidden=\"true\">\n </i>\n </button>\n </td>\n\n <td *ngFor=\"let col of activeColumns\"\n [class]=\"cn('align-middle',cellPaddingClass,fontSizeClass,col.class)\">\n\n <ng-container *ngIf=\"getTemplate(col.key) as cell; else defaultCell\">\n <ng-container\n *ngTemplateOutlet=\"cell.template; context: { $implicit: row[col.key], row: row }\">\n </ng-container>\n </ng-container>\n\n <ng-template #defaultCell>\n {{ row[col.key] }}\n </ng-template>\n </td>\n </tr>\n\n <tr *ngIf=\"expandedRows.has(i)\" class=\"bg-muted/10\">\n <td [attr.colspan]=\"activeColumns.length + (expandable ? 1 : 0)\"\n class=\"p-0\">\n <div class=\"p-6 border-t border-dashed border-border\">\n <ng-container *ngIf=\"expandedTemplate; else noExpanded\">\n <ng-container\n *ngTemplateOutlet=\"expandedTemplate; context: { row: row }\">\n </ng-container>\n </ng-container>\n\n <ng-template #noExpanded>\n <p class=\"text-xs text-muted-foreground italic\">\n No details available.\n </p>\n </ng-template>\n </div>\n </td>\n </tr>\n </ng-container>\n\n <tr *ngIf=\"pagedData.length === 0\">\n <td [attr.colspan]=\"activeColumns.length + (expandable ? 1 : 0)\"\n class=\"h-24 text-center text-muted-foreground\">\n No results found.\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n\n <div class=\"flex-none\">\n <tolle-pagination\n *ngIf=\"paginate && filteredData.length > 0\"\n [totalRecords]=\"filteredData.length\"\n [pageSizeOptions]=\"pageSizeOptions\"\n [currentPage]=\"currentPage\"\n [currentPageSize]=\"pageSize\"\n (onPageNumberChange)=\"handlePageChange($event)\"\n (onPageSizeChange)=\"handlePageSizeChange($event)\"\n ></tolle-pagination>\n </div>\n </div>\n `\n})\nexport class DataTableComponent implements OnInit, OnChanges {\n @Input() data: any[] = [];\n @Input() columns: TableColumn[] = [];\n @Input() searchable = true;\n @Input() paginate = true;\n @Input() pageSizeOptions: number[] = []\n @Input() pageSize = 10;\n @Input() expandable = false;\n\n @Input() size: 'xs' | 'sm' | 'default' | 'lg' = 'default';\n @Input() stickyHeader = false;\n @Input() maxHeight?: string;\n\n // Draws a physical border around the table itself (not the whole component).\n @Input() bordered = true;\n\n // --- Column Hiding Settings ---\n @Input() allowColumnHiding = true;\n @Input() showSettings = true;\n\n // Track visibility state\n columnVisibility: Record<string, boolean> = {};\n\n // Filters columns based on visibility state\n get activeColumns() {\n return this.columns.filter(col => this.columnVisibility[col.key]);\n }\n\n expandedRows = new Set<number>();\n @ContentChildren(TolleCellDirective) cellTemplates!: QueryList<TolleCellDirective>;\n @Input() expandedTemplate?: TemplateRef<any>;\n\n filteredData: any[] = [];\n pagedData: any[] = [];\n searchTerm = '';\n currentPage = 1;\n sortKey = '';\n sortDir: 'asc' | 'desc' | null = null;\n\n // Style Getters\n get cellPaddingClass(): string {\n switch (this.size) {\n case 'xs': return 'p-1 px-4';\n case 'sm': return 'p-2 px-4';\n case 'lg': return 'p-6 px-4';\n default: return 'p-4';\n }\n }\n\n get headerPaddingClass(): string {\n switch (this.size) {\n case 'xs': return 'h-7 px-4';\n case 'sm': return 'h-9 px-4';\n case 'lg': return 'h-14 px-4';\n default: return 'h-12 px-4';\n }\n }\n\n get fontSizeClass(): string {\n switch (this.size) {\n case 'xs': return 'text-[11px]';\n case 'sm': return 'text-xs';\n case 'lg': return 'text-base';\n default: return 'text-sm';\n }\n }\n\n protected cn = cn;\n\n ngOnInit() {\n this.initializeVisibility();\n this.refreshTable();\n }\n\n ngOnChanges(changes: SimpleChanges) {\n if (changes['data'] && !changes['data'].firstChange) {\n this.currentPage = 1; // Reset to page 1 when data changes\n this.refreshTable();\n }\n if (changes['columns']) {\n this.initializeVisibility();\n }\n }\n\n private initializeVisibility() {\n this.columns.forEach(col => {\n // Only set true if undefined, preserves user selection on other data refreshes\n if (this.columnVisibility[col.key] === undefined) {\n this.columnVisibility[col.key] = true;\n }\n });\n }\n\n refreshTable() {\n this.applySearch();\n this.applySort();\n this.updatePage();\n }\n onSearch() {\n this.currentPage = 1;\n this.refreshTable();\n }\n\n // Toggle function used by the checkbox in popover\n toggleColumn(key: string) {\n this.columnVisibility[key] = !this.columnVisibility[key];\n // No explicit refresh needed as the getter 'activeColumns' will automatically re-evaluate\n // when change detection runs after the click event.\n }\n\n private applySearch() {\n if (!this.searchTerm) {\n this.filteredData =\n [...this.data];\n return;\n }\n const q = this.searchTerm.toLowerCase();\n this.filteredData = this.data.filter(row =>\n Object.values(row).some(val => String(val).toLowerCase().includes(q))\n );\n }\n\n private applySort() {\n if (!this.sortKey || !this.sortDir) return;\n this.filteredData.sort((a, b) => {\n const valA = a[this.sortKey]; const valB = b[this.sortKey];\n if (valA < valB) return this.sortDir === 'asc' ? -1 : 1;\n if (valA > valB) return this.sortDir === 'asc' ? 1 : -1;\n return 0;\n });\n }\n\n handlePageChange(newPage: number) {\n this.currentPage = newPage;\n this.updatePage();\n }\n\n handlePageSizeChange(newSize: number) {\n if (newSize !== this.pageSize) {\n this.pageSize = newSize;\n this.currentPage = 1; // Reset to page 1 when size changes\n this.updatePage();\n }\n }\n\n updatePage() {\n if (!this.paginate) {\n this.pagedData = this.filteredData;\n return;\n }\n\n const start = (this.currentPage - 1) * this.pageSize;\n const end = start + this.pageSize;\n\n // Safety check if current page is out of bounds after filtering\n if (start >= this.filteredData.length && this.currentPage > 1) {\n this.currentPage = 1;\n this.updatePage();\n return;\n }\n\n this.pagedData = this.filteredData.slice(start, end);\n }\n\n toggleSort(key: string) {\n if (this.sortKey === key) {\n this.sortDir = this.sortDir === 'asc' ? 'desc' : this.sortDir === 'desc' ? null : 'asc';\n } else {\n this.sortKey = key;\n this.sortDir = 'asc';\n }\n this.currentPage = 1; // Reset to page 1 when sorting\n this.refreshTable();\n }\n\n getSortIcon(key: string) {\n if (this.sortKey !== key || !this.sortDir) return 'ri-expand-up-down-line opacity-20'; // Changed icon for inactive state\n return this.sortDir === 'asc' ? 'ri-arrow-up-line text-foreground' : 'ri-arrow-down-line text-foreground';\n }\n\n toggleRow(index: number) {\n if (this.expandedRows.has(index)) {\n this.expandedRows.delete(index);\n }\n else {\n this.expandedRows.add(index);\n }\n }\n\n getTemplate(key: string): TolleCellDirective | undefined {\n return this.cellTemplates?.find(t => t.name === key);\n }\n}\n"
26
26
  },
27
27
  {
28
28
  "path": "select.service.ts",