@tolle_/tolle-ui 18.2.26 → 18.2.27

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tolle_/tolle-ui",
3
- "version": "18.2.26",
3
+ "version": "18.2.27",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "package": "@tolle_/tolle-ui",
3
- "version": "18.2.26",
3
+ "version": "18.2.27",
4
4
  "registry": "r/{name}.json",
5
5
  "components": [
6
6
  {
@@ -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\">\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 rounded-md border border-border overflow-hidden shadow-sm 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 py-2\">\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=\"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"
26
26
  },
27
27
  {
28
28
  "path": "select.service.ts",
@@ -16,7 +16,7 @@
16
16
  {
17
17
  "path": "pagination.component.ts",
18
18
  "target": "ui/pagination.component.ts",
19
- "content": "import {\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n EventEmitter,\n inject,\n Input,\n OnChanges,\n OnInit,\n Output,\n SimpleChanges\n} from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { cn } from './utils/cn';\nimport {FormsModule} from '@angular/forms';\nimport {SelectComponent} from './select.component';\nimport {SelectItemComponent} from './select-item.component';\n\n@Component({\n selector: 'tolle-pagination',\n standalone: true,\n imports: [CommonModule, FormsModule, SelectComponent, SelectItemComponent],\n template: `\n <nav role=\"navigation\" aria-label=\"pagination\">\n <div [class]=\"cn('flex items-center justify-between px-2 py-4', class)\">\n\n <div *ngIf=\"showCurrentPageInfo\" class=\"text-sm text-muted-foreground\">\n <ng-container *ngIf=\"currentPageInfoTemplate; else defaultReport\">\n {{ pageReport }}\n </ng-container>\n <ng-template #defaultReport>\n Showing {{ first }} to {{ last }} of {{ totalRecords }} entries\n </ng-template>\n </div>\n\n <div class=\"flex items-center space-x-6 lg:space-x-8\">\n\n <div *ngIf=\"showPageOptions\" class=\"flex items-center space-x-2\">\n <p class=\"text-sm font-medium\">Rows per page</p>\n <tolle-select\n class=\"w-[70px]\"\n size=\"sm\"\n [ngModel]=\"currentPageSize\"\n (ngModelChange)=\"sizeChange($event)\"\n >\n <tolle-select-item *ngFor=\"let opt of pageSizeOptions\" [value]=\"opt\">\n {{ opt }}\n </tolle-select-item>\n </tolle-select>\n </div>\n\n <div class=\"flex items-center space-x-2\">\n <div *ngIf=\"!showPageLinks\" class=\"flex w-[100px] items-center justify-center text-sm font-medium\">\n Page {{ currentPage }} of {{ totalPages }}\n </div>\n\n <div *ngIf=\"showPageLinks\" class=\"flex items-center space-x-1\">\n <button\n (click)=\"previousPage()\"\n [disabled]=\"currentPage === 1\"\n aria-label=\"Go to previous page\"\n [class]=\"navBtnClass\"\n >\n <i aria-hidden=\"true\" class=\"ri-arrow-left-s-line\"></i>\n </button>\n\n <button\n *ngFor=\"let page of displayPageIndex\"\n (click)=\"selectPage(page)\"\n [attr.aria-current]=\"currentPage === page ? 'page' : null\"\n [class]=\"cn(\n 'h-8 w-8 text-sm rounded-md flex items-center justify-center transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 cursor-pointer',\n currentPage === page\n ? 'bg-primary text-primary-foreground font-medium'\n : 'hover:bg-accent hover:text-accent-foreground'\n )\"\n >\n {{ page }}\n </button>\n\n <button\n (click)=\"nextPage()\"\n [disabled]=\"currentPage === totalPages || totalPages === 0\"\n aria-label=\"Go to next page\"\n [class]=\"navBtnClass\"\n >\n <i aria-hidden=\"true\" class=\"ri-arrow-right-s-line\"></i>\n </button>\n </div>\n </div>\n </div>\n </div>\n </nav>\n `,\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class PaginationComponent implements OnInit, OnChanges {\n @Input() class = '';\n @Input() showPageLinks = true;\n @Input() showPageOptions = true;\n @Input() showCurrentPageInfo = true;\n @Input() currentPageInfoTemplate?: string;\n\n @Input() totalRecords = 0;\n @Input() currentPageSize = 10;\n @Input() currentPage = 1;\n @Input() pageSizeOptions: number[] = [10, 20, 30, 50];\n\n @Output() onPageNumberChange = new EventEmitter<number>();\n @Output() onPageSizeChange = new EventEmitter<number>();\n\n totalPages = 0;\n first = 0;\n last = 0;\n displayPageIndex: number[] = [];\n pageReport = '';\n\n private initialized = false;\n private cd = inject(ChangeDetectorRef);\n protected cn = cn;\n\n navBtnClass = 'h-8 w-8 p-0 flex items-center justify-center rounded-md border border-input bg-background hover:bg-accent hover:text-accent-foreground disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 cursor-pointer';\n\n ngOnInit(): void {\n if (this.pageSizeOptions.length === 0) {\n this.pageSizeOptions = [10, 20, 30, 50];\n }\n this.initializePagination();\n }\n\n ngOnChanges(changes: SimpleChanges): void {\n // Only re-init if meaningful data changes\n if (changes['totalRecords'] || changes['currentPage'] || changes['currentPageSize']) {\n this.initializePagination();\n }\n }\n\n private initializePagination(): void {\n this.calcPagination();\n if (!this.initialized) {\n this.initialized = true;\n }\n this.cd.detectChanges();\n }\n\n nextPage() {\n if (this.totalPages > this.currentPage) {\n this.currentPage++;\n this.emitChange();\n }\n }\n\n previousPage() {\n if (this.currentPage > 1) {\n this.currentPage--;\n this.emitChange();\n }\n }\n\n selectPage(page: number) {\n if (this.currentPage === page) return;\n this.currentPage = page;\n this.emitChange();\n }\n\n sizeChange(size: number) {\n this.currentPageSize = size;\n this.currentPage = 1; // Reset to page 1 on size change\n this.emitChange();\n }\n\n private emitChange(): void {\n this.calcPagination();\n this.onPageNumberChange.emit(this.currentPage);\n this.onPageSizeChange.emit(this.currentPageSize);\n this.cd.detectChanges();\n }\n\n private calcPagination(): void {\n this.totalPages = Math.ceil(this.totalRecords / this.currentPageSize) || 0;\n\n // Bounds check\n if (this.currentPage > this.totalPages && this.totalPages > 0) {\n this.currentPage = this.totalPages;\n }\n\n this.first = this.totalRecords === 0 ? 0 : (this.currentPage - 1) * this.currentPageSize + 1;\n this.last = Math.min(this.totalRecords, this.currentPage * this.currentPageSize);\n\n // Calculate Sliding Window for Page Numbers (Max 5 visible)\n const pages: number[] = Array.from({ length: this.totalPages }, (_, i) => i + 1);\n\n if (this.totalPages <= 5) {\n this.displayPageIndex = pages;\n } else {\n let start = Math.max(0, this.currentPage - 3);\n let end = start + 5;\n\n if (end > this.totalPages) {\n end = this.totalPages;\n start = end - 5;\n }\n this.displayPageIndex = pages.slice(start, end);\n }\n\n // Template Parsing\n if (this.currentPageInfoTemplate) {\n this.pageReport = this.currentPageInfoTemplate.replace(\n /{first}|{last}|{totalRecords}|{currentPage}|{currentPageSize}|{totalPages}/g,\n (match) => {\n switch (match) {\n case '{first}': return `${this.first}`;\n case '{last}': return `${this.last}`;\n case '{totalRecords}': return `${this.totalRecords}`;\n case '{totalPages}': return `${this.totalPages}`;\n case '{currentPage}': return `${this.currentPage}`;\n case '{currentPageSize}': return `${this.currentPageSize}`;\n default: return match;\n }\n }\n );\n }\n }\n}\n"
19
+ "content": "import {\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n EventEmitter,\n inject,\n Input,\n OnChanges,\n OnInit,\n Output,\n SimpleChanges\n} from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { cn } from './utils/cn';\nimport {FormsModule} from '@angular/forms';\nimport {SelectComponent} from './select.component';\nimport {SelectItemComponent} from './select-item.component';\n\n@Component({\n selector: 'tolle-pagination',\n standalone: true,\n imports: [CommonModule, FormsModule, SelectComponent, SelectItemComponent],\n template: `\n <nav role=\"navigation\" aria-label=\"pagination\">\n <div [class]=\"cn('flex items-center justify-between px-2 py-1', class)\">\n\n <div *ngIf=\"showCurrentPageInfo\" class=\"text-sm text-muted-foreground\">\n <ng-container *ngIf=\"currentPageInfoTemplate; else defaultReport\">\n {{ pageReport }}\n </ng-container>\n <ng-template #defaultReport>\n Showing {{ first }} to {{ last }} of {{ totalRecords }} entries\n </ng-template>\n </div>\n\n <div class=\"flex items-center space-x-6 lg:space-x-8\">\n\n <div *ngIf=\"showPageOptions\" class=\"flex items-center space-x-2\">\n <p class=\"text-sm font-medium\">Rows per page</p>\n <tolle-select\n class=\"w-[70px]\"\n size=\"sm\"\n [ngModel]=\"currentPageSize\"\n (ngModelChange)=\"sizeChange($event)\"\n >\n <tolle-select-item *ngFor=\"let opt of pageSizeOptions\" [value]=\"opt\">\n {{ opt }}\n </tolle-select-item>\n </tolle-select>\n </div>\n\n <div class=\"flex items-center space-x-2\">\n <div *ngIf=\"!showPageLinks\" class=\"flex w-[100px] items-center justify-center text-sm font-medium\">\n Page {{ currentPage }} of {{ totalPages }}\n </div>\n\n <div *ngIf=\"showPageLinks\" class=\"flex items-center space-x-1\">\n <button\n (click)=\"previousPage()\"\n [disabled]=\"currentPage === 1\"\n aria-label=\"Go to previous page\"\n [class]=\"navBtnClass\"\n >\n <i aria-hidden=\"true\" class=\"ri-arrow-left-s-line\"></i>\n </button>\n\n <button\n *ngFor=\"let page of displayPageIndex\"\n (click)=\"selectPage(page)\"\n [attr.aria-current]=\"currentPage === page ? 'page' : null\"\n [class]=\"cn(\n 'h-8 w-8 text-sm rounded-md flex items-center justify-center transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 cursor-pointer',\n currentPage === page\n ? 'bg-primary text-primary-foreground font-medium'\n : 'hover:bg-accent hover:text-accent-foreground'\n )\"\n >\n {{ page }}\n </button>\n\n <button\n (click)=\"nextPage()\"\n [disabled]=\"currentPage === totalPages || totalPages === 0\"\n aria-label=\"Go to next page\"\n [class]=\"navBtnClass\"\n >\n <i aria-hidden=\"true\" class=\"ri-arrow-right-s-line\"></i>\n </button>\n </div>\n </div>\n </div>\n </div>\n </nav>\n `,\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class PaginationComponent implements OnInit, OnChanges {\n @Input() class = '';\n @Input() showPageLinks = true;\n @Input() showPageOptions = true;\n @Input() showCurrentPageInfo = true;\n @Input() currentPageInfoTemplate?: string;\n\n @Input() totalRecords = 0;\n @Input() currentPageSize = 10;\n @Input() currentPage = 1;\n @Input() pageSizeOptions: number[] = [10, 20, 30, 50];\n\n @Output() onPageNumberChange = new EventEmitter<number>();\n @Output() onPageSizeChange = new EventEmitter<number>();\n\n totalPages = 0;\n first = 0;\n last = 0;\n displayPageIndex: number[] = [];\n pageReport = '';\n\n private initialized = false;\n private cd = inject(ChangeDetectorRef);\n protected cn = cn;\n\n navBtnClass = 'h-8 w-8 p-0 flex items-center justify-center rounded-md border border-input bg-background hover:bg-accent hover:text-accent-foreground disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 cursor-pointer';\n\n ngOnInit(): void {\n if (this.pageSizeOptions.length === 0) {\n this.pageSizeOptions = [10, 20, 30, 50];\n }\n this.initializePagination();\n }\n\n ngOnChanges(changes: SimpleChanges): void {\n // Only re-init if meaningful data changes\n if (changes['totalRecords'] || changes['currentPage'] || changes['currentPageSize']) {\n this.initializePagination();\n }\n }\n\n private initializePagination(): void {\n this.calcPagination();\n if (!this.initialized) {\n this.initialized = true;\n }\n this.cd.detectChanges();\n }\n\n nextPage() {\n if (this.totalPages > this.currentPage) {\n this.currentPage++;\n this.emitChange();\n }\n }\n\n previousPage() {\n if (this.currentPage > 1) {\n this.currentPage--;\n this.emitChange();\n }\n }\n\n selectPage(page: number) {\n if (this.currentPage === page) return;\n this.currentPage = page;\n this.emitChange();\n }\n\n sizeChange(size: number) {\n this.currentPageSize = size;\n this.currentPage = 1; // Reset to page 1 on size change\n this.emitChange();\n }\n\n private emitChange(): void {\n this.calcPagination();\n this.onPageNumberChange.emit(this.currentPage);\n this.onPageSizeChange.emit(this.currentPageSize);\n this.cd.detectChanges();\n }\n\n private calcPagination(): void {\n this.totalPages = Math.ceil(this.totalRecords / this.currentPageSize) || 0;\n\n // Bounds check\n if (this.currentPage > this.totalPages && this.totalPages > 0) {\n this.currentPage = this.totalPages;\n }\n\n this.first = this.totalRecords === 0 ? 0 : (this.currentPage - 1) * this.currentPageSize + 1;\n this.last = Math.min(this.totalRecords, this.currentPage * this.currentPageSize);\n\n // Calculate Sliding Window for Page Numbers (Max 5 visible)\n const pages: number[] = Array.from({ length: this.totalPages }, (_, i) => i + 1);\n\n if (this.totalPages <= 5) {\n this.displayPageIndex = pages;\n } else {\n let start = Math.max(0, this.currentPage - 3);\n let end = start + 5;\n\n if (end > this.totalPages) {\n end = this.totalPages;\n start = end - 5;\n }\n this.displayPageIndex = pages.slice(start, end);\n }\n\n // Template Parsing\n if (this.currentPageInfoTemplate) {\n this.pageReport = this.currentPageInfoTemplate.replace(\n /{first}|{last}|{totalRecords}|{currentPage}|{currentPageSize}|{totalPages}/g,\n (match) => {\n switch (match) {\n case '{first}': return `${this.first}`;\n case '{last}': return `${this.last}`;\n case '{totalRecords}': return `${this.totalRecords}`;\n case '{totalPages}': return `${this.totalPages}`;\n case '{currentPage}': return `${this.currentPage}`;\n case '{currentPageSize}': return `${this.currentPageSize}`;\n default: return match;\n }\n }\n );\n }\n }\n}\n"
20
20
  },
21
21
  {
22
22
  "path": "select.service.ts",