UNPKG

@sixbell-telco/sdk

Version:

A collection of reusable components designed for use in Sixbell Telco Angular projects

1,087 lines (1,079 loc) 50.5 kB
import { injectFlexRenderContext } from '@tanstack/angular-table'; export * from '@tanstack/angular-table'; import { CommonModule } from '@angular/common'; import * as i0 from '@angular/core'; import { signal, Injectable, inject, computed, ChangeDetectionStrategy, Component, input, output, effect } from '@angular/core'; import { ButtonComponent } from '@sixbell-telco/sdk/components/button'; import { IconComponent } from '@sixbell-telco/sdk/components/icon'; import { matKeyboardArrowDown, matSwapVert, matArrowUpward, matInfo } from '@sixbell-telco/sdk/components/icon/material/baseline'; import { CheckboxComponent } from '@sixbell-telco/sdk/components/forms/checkbox'; import { heroDocumentText, heroXCircle } from '@ng-icons/heroicons/outline'; import { SelectComponent } from '@sixbell-telco/sdk/components/forms/select'; import { PaginatorComponent } from '@sixbell-telco/sdk/components/paginator'; import { TypographyDirective } from '@sixbell-telco/sdk/directives/typography'; import { cn } from '@sixbell-telco/sdk/utils/cn'; import { TranslateService, TranslatePipe } from '@sixbell-telco/sdk/utils/translation'; import { cva } from 'class-variance-authority'; /** * Context service to share data table configuration with sub-components * This allows sub-components to automatically sync their sizes with the parent data table */ class DataTableContextService { /** * Current table size that sub-components can subscribe to */ size = signal('sm'); /** * Update the table size - called by the main data table component */ setSize(size) { this.size.set(size); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DataTableContextService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DataTableContextService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DataTableContextService, decorators: [{ type: Injectable }] }); /** * Expandable cell component that shows row data and expand/collapse toggle */ class DataTableExpandableCellComponent { /** * TanStack Table context injected by flexRender */ context = injectFlexRenderContext(); /** * Data table context for size synchronization */ dataTableContext = inject(DataTableContextService); /** * Get the row instance from context */ get row() { return this.context.row; } /** @internal */ iconChevronDown = matKeyboardArrowDown; /** * @internal * Computes button size based on data table size */ buttonSize = computed(() => { switch (this.dataTableContext.size()) { case 'xs': return 'xs'; case 'sm': return 'xs'; case 'md': return 'sm'; case 'lg': return 'sm'; case 'xl': return 'md'; default: return 'xs'; } }); /** * @internal * Computes icon size based on data table size */ iconSize = computed(() => { switch (this.dataTableContext.size()) { case 'xs': return 'xs'; case 'sm': return 'xs'; case 'md': return 'sm'; case 'lg': return 'sm'; case 'xl': return 'md'; default: return 'xs'; } }); static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DataTableExpandableCellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.0", type: DataTableExpandableCellComponent, isStandalone: true, selector: "st-data-table-expandable-cell", ngImport: i0, template: ` <div [style.--depth]="row.depth"> <div> @if (row.getCanExpand()) { <st-button variant="base" [size]="buttonSize()" [square]="true" [link]="true" class="group" (click)="row.getToggleExpandedHandler()()" [attr.aria-expanded]="row.getIsExpanded()" > <st-icon class="transition-transform duration-300 ease-in-out group-aria-[expanded=false]:rotate-0 group-aria-[expanded=true]:rotate-180" [icon]="iconChevronDown" [size]="iconSize()" ></st-icon> </st-button> } @else { <span>&nbsp;&nbsp;</span> } {{ context.getValue() }} </div> </div> `, isInline: true, styles: [":host{>div{padding-left:calc(2rem * var(--depth, 1))}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IconComponent, selector: "st-icon", inputs: ["color", "size", "icon"] }, { kind: "component", type: ButtonComponent, selector: "st-button", inputs: ["variant", "ghost", "outline", "link", "soft", "dash", "wide", "circle", "square", "glass", "block", "loader", "size", "shadow", "focusable", "icon", "iconPosition", "type", "disabled"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DataTableExpandableCellComponent, decorators: [{ type: Component, args: [{ standalone: true, selector: 'st-data-table-expandable-cell', imports: [CommonModule, IconComponent, ButtonComponent], template: ` <div [style.--depth]="row.depth"> <div> @if (row.getCanExpand()) { <st-button variant="base" [size]="buttonSize()" [square]="true" [link]="true" class="group" (click)="row.getToggleExpandedHandler()()" [attr.aria-expanded]="row.getIsExpanded()" > <st-icon class="transition-transform duration-300 ease-in-out group-aria-[expanded=false]:rotate-0 group-aria-[expanded=true]:rotate-180" [icon]="iconChevronDown" [size]="iconSize()" ></st-icon> </st-button> } @else { <span>&nbsp;&nbsp;</span> } {{ context.getValue() }} </div> </div> `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{>div{padding-left:calc(2rem * var(--depth, 1))}}\n"] }] }] }); /** * Expandable header cell component with sorting capability and expand all functionality */ class DataTableExpandableHeaderCellComponent { /** * TanStack Table context injected by flexRender */ context = injectFlexRenderContext(); /** * Data table context for size synchronization */ dataTableContext = inject(DataTableContextService); /** * Display label for the column */ label = input.required(); /** * Whether this column can be sorted */ canSort = input(false); /** * Current sort state of the column */ sortState = input(false); /** * Event emitted when sort toggle is requested */ sortToggle = output(); /** * Get the table instance from context */ get table() { return this.context.table; } /** @internal */ iconChevronDown = matKeyboardArrowDown; /** @internal */ iconDefaultSort = matSwapVert; /** @internal */ iconAscending = matArrowUpward; /** * @internal * Computes button size based on data table size */ buttonSize = computed(() => { switch (this.dataTableContext.size()) { case 'xs': return 'xs'; case 'sm': return 'xs'; case 'md': return 'sm'; case 'lg': return 'sm'; case 'xl': return 'md'; default: return 'xs'; } }); /** * @internal * Computes icon size based on data table size */ iconSize = computed(() => { switch (this.dataTableContext.size()) { case 'xs': return 'xs'; case 'sm': return 'xs'; case 'md': return 'sm'; case 'lg': return 'sm'; case 'xl': return 'md'; default: return 'xs'; } }); /** * @internal * Computes the appropriate icon based on current sort state */ currentSortIcon = computed(() => { switch (this.sortState()) { case 'asc': case 'desc': return this.iconAscending; default: return this.iconDefaultSort; } }); /** * @internal * Computes aria-sort attribute for accessibility */ computedAriaSortAttribute = computed(() => { switch (this.sortState()) { case 'asc': return 'ascending'; case 'desc': return 'descending'; default: return 'none'; } }); static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DataTableExpandableHeaderCellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.0", type: DataTableExpandableHeaderCellComponent, isStandalone: true, selector: "st-data-table-expandable-header-cell", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, canSort: { classPropertyName: "canSort", publicName: "canSort", isSignal: true, isRequired: false, transformFunction: null }, sortState: { classPropertyName: "sortState", publicName: "sortState", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sortToggle: "sortToggle" }, ngImport: i0, template: ` <div class="inline-flex items-center gap-1"> <st-button variant="base" [size]="buttonSize()" [square]="true" [link]="true" (click)="context.table.getToggleAllRowsExpandedHandler()($event)" class="group" [attr.aria-expanded]="context.table.getIsAllRowsExpanded()" > <st-icon class="transition-transform duration-300 ease-in-out group-aria-[expanded=false]:rotate-0 group-aria-[expanded=true]:rotate-180" [icon]="iconChevronDown" [size]="iconSize()" ></st-icon> </st-button> <!-- Column Label --> <span class="font-bold">{{ label() }}</span> <!-- Button for sorting --> @if (canSort()) { <st-button variant="secondary" [size]="buttonSize()" [square]="true" [ghost]="true" (click)="sortToggle.emit(); $event.stopPropagation()" class="group" [attr.aria-label]="'Sort by ' + label()" [attr.aria-sort]="computedAriaSortAttribute()" > <st-icon class="transition-transform duration-300 ease-in-out group-aria-[sort=ascending]:rotate-0 group-aria-[sort=descending]:rotate-180" [icon]="currentSortIcon()" [size]="iconSize()" ></st-icon> </st-button> } </div> `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IconComponent, selector: "st-icon", inputs: ["color", "size", "icon"] }, { kind: "component", type: ButtonComponent, selector: "st-button", inputs: ["variant", "ghost", "outline", "link", "soft", "dash", "wide", "circle", "square", "glass", "block", "loader", "size", "shadow", "focusable", "icon", "iconPosition", "type", "disabled"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DataTableExpandableHeaderCellComponent, decorators: [{ type: Component, args: [{ standalone: true, selector: 'st-data-table-expandable-header-cell', imports: [CommonModule, IconComponent, ButtonComponent], template: ` <div class="inline-flex items-center gap-1"> <st-button variant="base" [size]="buttonSize()" [square]="true" [link]="true" (click)="context.table.getToggleAllRowsExpandedHandler()($event)" class="group" [attr.aria-expanded]="context.table.getIsAllRowsExpanded()" > <st-icon class="transition-transform duration-300 ease-in-out group-aria-[expanded=false]:rotate-0 group-aria-[expanded=true]:rotate-180" [icon]="iconChevronDown" [size]="iconSize()" ></st-icon> </st-button> <!-- Column Label --> <span class="font-bold">{{ label() }}</span> <!-- Button for sorting --> @if (canSort()) { <st-button variant="secondary" [size]="buttonSize()" [square]="true" [ghost]="true" (click)="sortToggle.emit(); $event.stopPropagation()" class="group" [attr.aria-label]="'Sort by ' + label()" [attr.aria-sort]="computedAriaSortAttribute()" > <st-icon class="transition-transform duration-300 ease-in-out group-aria-[sort=ascending]:rotate-0 group-aria-[sort=descending]:rotate-180" [icon]="currentSortIcon()" [size]="iconSize()" ></st-icon> </st-button> } </div> `, changeDetection: ChangeDetectionStrategy.OnPush, }] }] }); /** * Sub-component toggle cell for controlling custom sub-component visibility */ class DataTableSubToggleCellComponent { /** * The table row instance */ row = input.required(); /** * Whether the sub-component is currently expanded */ isExpanded = input(false); /** * Whether this row can toggle a sub-component */ canToggleSubComponent = input(false); /** * Event emitted when toggle is clicked */ toggled = output(); /** @internal */ expandIcon = matKeyboardArrowDown; /** @internal */ infoIcon = matInfo; /** * @internal * Handles the toggle click */ handleToggle() { this.toggled.emit(this.row().id); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DataTableSubToggleCellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.0", type: DataTableSubToggleCellComponent, isStandalone: true, selector: "st-data-table-sub-toggle-cell", inputs: { row: { classPropertyName: "row", publicName: "row", isSignal: true, isRequired: true, transformFunction: null }, isExpanded: { classPropertyName: "isExpanded", publicName: "isExpanded", isSignal: true, isRequired: false, transformFunction: null }, canToggleSubComponent: { classPropertyName: "canToggleSubComponent", publicName: "canToggleSubComponent", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { toggled: "toggled" }, ngImport: i0, template: ` @if (canToggleSubComponent()) { <st-button variant="secondary" size="xs" [circle]="true" [ghost]="true" class="group" (click)="handleToggle(); $event.stopPropagation()" [attr.aria-expanded]="isExpanded()" [attr.title]="isExpanded() ? 'Hide Details' : 'Show Details'" > <st-icon class="transition-transform duration-300 ease-in-out group-aria-[expanded=false]:rotate-0 group-aria-[expanded=true]:rotate-180" [icon]="expandIcon" size="sm" ></st-icon> <span class="sr-only">{{ isExpanded() ? 'Hide Details' : 'Show Details' }}</span> </st-button> } @else { <!-- Display an informational icon if a sub-component cannot be shown for this row --> <st-button variant="secondary" size="xs" [circle]="true" [ghost]="true" [attr.aria-expanded]="isExpanded()" [attr.title]="isExpanded() ? 'Hide Details' : 'Show Details'" [disabled]="true" > <st-icon [icon]="infoIcon" size="sm"></st-icon> <span class="sr-only">{{ isExpanded() ? 'Hide Details' : 'Show Details' }}</span> </st-button> } `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IconComponent, selector: "st-icon", inputs: ["color", "size", "icon"] }, { kind: "component", type: ButtonComponent, selector: "st-button", inputs: ["variant", "ghost", "outline", "link", "soft", "dash", "wide", "circle", "square", "glass", "block", "loader", "size", "shadow", "focusable", "icon", "iconPosition", "type", "disabled"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DataTableSubToggleCellComponent, decorators: [{ type: Component, args: [{ standalone: true, selector: 'st-data-table-sub-toggle-cell', imports: [CommonModule, IconComponent, ButtonComponent], template: ` @if (canToggleSubComponent()) { <st-button variant="secondary" size="xs" [circle]="true" [ghost]="true" class="group" (click)="handleToggle(); $event.stopPropagation()" [attr.aria-expanded]="isExpanded()" [attr.title]="isExpanded() ? 'Hide Details' : 'Show Details'" > <st-icon class="transition-transform duration-300 ease-in-out group-aria-[expanded=false]:rotate-0 group-aria-[expanded=true]:rotate-180" [icon]="expandIcon" size="sm" ></st-icon> <span class="sr-only">{{ isExpanded() ? 'Hide Details' : 'Show Details' }}</span> </st-button> } @else { <!-- Display an informational icon if a sub-component cannot be shown for this row --> <st-button variant="secondary" size="xs" [circle]="true" [ghost]="true" [attr.aria-expanded]="isExpanded()" [attr.title]="isExpanded() ? 'Hide Details' : 'Show Details'" [disabled]="true" > <st-icon [icon]="infoIcon" size="sm"></st-icon> <span class="sr-only">{{ isExpanded() ? 'Hide Details' : 'Show Details' }}</span> </st-button> } `, changeDetection: ChangeDetectionStrategy.OnPush, }] }] }); /** * Header checkbox component for selecting/deselecting all rows on current page */ class DataTableHeadSelectionComponent { /** * TanStack Table context injected by flexRender */ context = injectFlexRenderContext(); /** * Data table context for size synchronization */ dataTableContext = inject(DataTableContextService); /** * Computed checkbox size based on table size */ checkboxSize = computed(() => { const tableSize = this.dataTableContext.size(); switch (tableSize) { case 'xs': return 'xs'; case 'sm': return 'sm'; case 'md': return 'md'; case 'lg': return 'lg'; case 'xl': return 'lg'; default: return 'sm'; } }); static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DataTableHeadSelectionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.0", type: DataTableHeadSelectionComponent, isStandalone: true, selector: "st-data-table-head-selection", ngImport: i0, template: ` <st-checkbox [value]="context.table.getIsAllPageRowsSelected()" (valueUpdated)="context.table.toggleAllPageRowsSelected()" [size]="checkboxSize()" ></st-checkbox> `, isInline: true, dependencies: [{ kind: "component", type: CheckboxComponent, selector: "st-checkbox", inputs: ["variant", "size", "label", "name", "value", "disabled"], outputs: ["valueChange", "valueUpdated", "disabledChange"] }, { kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DataTableHeadSelectionComponent, decorators: [{ type: Component, args: [{ standalone: true, selector: 'st-data-table-head-selection', imports: [CheckboxComponent, CommonModule], template: ` <st-checkbox [value]="context.table.getIsAllPageRowsSelected()" (valueUpdated)="context.table.toggleAllPageRowsSelected()" [size]="checkboxSize()" ></st-checkbox> `, changeDetection: ChangeDetectionStrategy.OnPush, }] }] }); /** * Row checkbox component for individual row selection */ class DataTableRowSelectionComponent { /** * TanStack Table context injected by flexRender */ context = injectFlexRenderContext(); /** * Data table context for size synchronization */ dataTableContext = inject(DataTableContextService); /** * Computed checkbox size based on table size */ checkboxSize = computed(() => { const tableSize = this.dataTableContext.size(); switch (tableSize) { case 'xs': return 'xs'; case 'sm': return 'sm'; case 'md': return 'md'; case 'lg': return 'lg'; case 'xl': return 'lg'; default: return 'sm'; } }); static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DataTableRowSelectionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.0", type: DataTableRowSelectionComponent, isStandalone: true, selector: "st-data-table-row-selection", ngImport: i0, template: ` <st-checkbox [value]="context.row.getIsSelected()" [disabled]="!context.row.getCanSelect()" (valueUpdated)="context.row.toggleSelected()" [size]="checkboxSize()" ></st-checkbox> `, isInline: true, dependencies: [{ kind: "component", type: CheckboxComponent, selector: "st-checkbox", inputs: ["variant", "size", "label", "name", "value", "disabled"], outputs: ["valueChange", "valueUpdated", "disabledChange"] }, { kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DataTableRowSelectionComponent, decorators: [{ type: Component, args: [{ standalone: true, selector: 'st-data-table-row-selection', imports: [CheckboxComponent, CommonModule], template: ` <st-checkbox [value]="context.row.getIsSelected()" [disabled]="!context.row.getCanSelect()" (valueUpdated)="context.row.toggleSelected()" [size]="checkboxSize()" ></st-checkbox> `, changeDetection: ChangeDetectionStrategy.OnPush, }] }] }); /** * Sortable header component for data table columns * * @example * ```typescript * flexRenderComponent(DataTableSortableHeaderComponent, { * inputs: { * label: 'Column Name', * sortState: column.getIsSorted(), * }, * outputs: { * sortToggle: () => column.toggleSorting(), * }, * }) * ``` */ class DataTableSortableHeaderComponent { dataTableContext = inject(DataTableContextService); /** * Display label for the column */ label = input.required(); /** * Current sort state of the column */ sortState = input(false); /** * Event emitted when sort toggle is requested */ sortToggle = output(); /** @internal */ iconDefaultSort = matSwapVert; /** @internal */ iconAscending = matArrowUpward; /** * @internal * Computes button size based on data table size */ buttonSize = computed(() => { switch (this.dataTableContext.size()) { case 'xs': return 'xs'; case 'sm': return 'xs'; case 'md': return 'sm'; case 'lg': return 'sm'; case 'xl': return 'md'; default: return 'xs'; } }); /** * @internal * Computes icon size based on data table size */ iconSize = computed(() => { switch (this.dataTableContext.size()) { case 'xs': return 'xs'; case 'sm': return 'xs'; case 'md': return 'sm'; case 'lg': return 'sm'; case 'xl': return 'md'; default: return 'xs'; } }); /** * @internal * Computes the appropriate icon based on current sort state */ currentSortIcon = computed(() => { switch (this.sortState()) { case 'asc': case 'desc': return this.iconAscending; default: return this.iconDefaultSort; } }); /** * @internal * Computes aria-sort attribute for accessibility */ computedAriaSortAttribute = computed(() => { switch (this.sortState()) { case 'asc': return 'ascending'; case 'desc': return 'descending'; default: return 'none'; } }); static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DataTableSortableHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "19.2.0", type: DataTableSortableHeaderComponent, isStandalone: true, selector: "st-data-table-sortable-header", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, sortState: { classPropertyName: "sortState", publicName: "sortState", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sortToggle: "sortToggle" }, ngImport: i0, template: ` <div class="inline-flex items-center gap-1"> <span class="font-bold">{{ label() }}</span> <st-button variant="secondary" [size]="buttonSize()" [square]="true" [ghost]="true" (click)="sortToggle.emit(); $event.stopPropagation()" class="group" [attr.aria-label]="'Sort by ' + label()" [attr.aria-sort]="computedAriaSortAttribute()" > <st-icon class="transition-transform duration-300 ease-in-out group-aria-[sort=ascending]:rotate-0 group-aria-[sort=descending]:rotate-180" [icon]="currentSortIcon()" [size]="iconSize()" ></st-icon> </st-button> </div> `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IconComponent, selector: "st-icon", inputs: ["color", "size", "icon"] }, { kind: "component", type: ButtonComponent, selector: "st-button", inputs: ["variant", "ghost", "outline", "link", "soft", "dash", "wide", "circle", "square", "glass", "block", "loader", "size", "shadow", "focusable", "icon", "iconPosition", "type", "disabled"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DataTableSortableHeaderComponent, decorators: [{ type: Component, args: [{ standalone: true, selector: 'st-data-table-sortable-header', imports: [CommonModule, IconComponent, ButtonComponent], template: ` <div class="inline-flex items-center gap-1"> <span class="font-bold">{{ label() }}</span> <st-button variant="secondary" [size]="buttonSize()" [square]="true" [ghost]="true" (click)="sortToggle.emit(); $event.stopPropagation()" class="group" [attr.aria-label]="'Sort by ' + label()" [attr.aria-sort]="computedAriaSortAttribute()" > <st-icon class="transition-transform duration-300 ease-in-out group-aria-[sort=ascending]:rotate-0 group-aria-[sort=descending]:rotate-180" [icon]="currentSortIcon()" [size]="iconSize()" ></st-icon> </st-button> </div> `, changeDetection: ChangeDetectionStrategy.OnPush, }] }] }); /** * @internal * Generates data table classes with size and height variants */ const dataTableContent = cva([ 'table', 'relative', 'text-pretty', '[&_tbody]:font-body', '[&_thead]:font-heading', '[&_thead]:leading-normal', '[&_tbody]:leading-tight', '[&_th]:bg-neutral [&_th]:text-neutral-content', '[&_th]:px-4', '[&_td]:px-4', '[&_tbody_tr]:border-transparent', 'rounded-t-box', '[&_tbody_tr]:border-transparent', ], { variants: { size: { xs: ['table-xs'], sm: ['table-sm'], md: ['table-md'], lg: ['table-lg'], xl: ['table-xl'], }, }, defaultVariants: { size: 'sm', }, }); /** * A styled container component for data tables - provides styling shell only * * This component is similar to st-table but with additional data table specific styling. * Users should compose their own table structure using TanStack Table and SDK components. * * @example * ```html * <st-data-table-container size="md" height="lg"> * <table> * <thead> * <tr> * <th><st-data-table-sortable-header>Name</st-data-table-sortable-header></th> * </tr> * </thead> * <tbody> * <tr> * <td>John Doe</td> * </tr> * </tbody> * </table> * </st-data-table-container> * ``` */ class DataTableContainerComponent { translationService = inject(TranslateService); dataTableContext = inject(DataTableContextService); // Icon references for template noDataIcon = heroDocumentText; noResultsIcon = heroXCircle; /** * Table size variant * @defaultValue 'sm' */ size = input('sm'); constructor() { // Update context when size changes effect(() => { this.dataTableContext.setSize(this.size()); }); } /** * Enable sticky header row * @defaultValue false */ pinRows = input(false); /** * Enable sticky first column * @defaultValue false */ pinColumns = input(false); /** * Apply zebra-striping to rows * @defaultValue false */ zebra = input(false); /** * Pagination control display variant * @defaultValue 'default' */ paginatorType = input('default'); /** * Total number of items across all pages * @defaultValue 0 */ totalItems = input(0); /** * Pagination configuration * @defaultValue { id: 'paginator', currentPage: 1, itemsPerPage: 10, search: '', sortColumn: null, sortDirection: null, filters: {} } */ pagination = input({ id: 'paginator', currentPage: 1, itemsPerPage: 10, search: '', sortColumn: null, sortDirection: null, filters: {}, }); /** * Maximum number of pagination buttons to display * @defaultValue 4 */ maxSize = input(4); /** Event emitted when page changes */ pageChanged = output(); /** Event emitted when page size changes */ pageSizeChanged = output(); ENTRIES = [5, 10, 20, 50, 100]; entriesPerPageOptions = computed(() => { return this.ENTRIES.map((n) => { const translatedText = this.translationService.instant('sdk.dataTable.perPage', { count: n }); // Fallback if translation fails const fallbackText = `${n} / page`; return { value: n, text: translatedText && translatedText !== 'sdk.dataTable.perPage' ? translatedText : fallbackText, }; }); }); /** * Computed total number of pages for the internal paginator. * Calculated from totalItems and itemsPerPage. */ totalPages = computed(() => { const itemsPerPage = this.pagination().itemsPerPage || 1; const totalItems = this.totalItems(); return Math.ceil(totalItems / itemsPerPage); }); /** * Computed display range for current page entries * @example "1-10" of 100 entries */ paginationRange = computed(() => { const start = this.pagination().currentPage * this.pagination().itemsPerPage - this.pagination().itemsPerPage + 1; const end = Math.min(this.pagination().currentPage * this.pagination().itemsPerPage, this.totalItems()); return `${start}-${end}`; }); /** * Function to determine if any filters are currently active * This allows the parent component to define what constitutes an "active filter" */ hasActiveFiltersCallback = input(); /** * Computed boolean indicating if any filters are currently active */ hasActiveFilters = computed(() => { const callback = this.hasActiveFiltersCallback(); if (callback) { return callback(); } // Fallback: check for basic search term only const pagination = this.pagination(); return !!pagination.search?.trim(); }); /** * Computed boolean indicating if we should show "no results" vs "no data" */ hasNoResults = computed(() => { return this.totalItems() === 0 && this.hasActiveFilters(); }); /** * Computed boolean indicating if we should show "no data" state */ hasNoData = computed(() => { return this.totalItems() === 0 && !this.hasActiveFilters(); }); /** * Handles page navigation events * @param page - New page number */ handlePageChange(page) { this.pageChanged.emit(page); } /** * Handles page size changes * @param size - New items per page value */ handlePageSizeChange(size) { let resolved; if (typeof size === 'number') { resolved = size; } else if (size && typeof size === 'object' && 'value' in size) { const v = size['value']; const safeStringify = (x) => (['string', 'number', 'boolean'].includes(typeof x) ? String(x) : '0'); resolved = typeof v === 'number' ? v : Number(safeStringify(v)); } else { const safeStringify = (x) => (['string', 'number', 'boolean'].includes(typeof x) ? String(x) : '0'); resolved = Number(safeStringify(size)) || undefined; } const s = Number(resolved || 1); this.pageSizeChanged.emit(s); } /** * @internal * Computes typography variant for paginator text based on table size */ paginatorTextSize = computed(() => { switch (this.size()) { case 'xs': return 'body-xxs'; case 'sm': return 'body-xs'; case 'md': return 'body-sm'; case 'lg': return 'body'; case 'xl': return 'body'; default: return 'body-xs'; } }); /** * @internal * Computes table element classes based on current configuration */ tableClass = computed(() => cn(dataTableContent({ size: this.size(), }), { 'table-pin-rows': this.pinRows(), 'table-pin-cols': this.pinColumns(), 'table-zebra': this.zebra(), })); /** * @internal * Computes table container classes based on height input */ tableContainerClass = computed(() => cn('w-full flex-grow')); /** * @internal * Computes outer container classes based on size */ containerClass = computed(() => cn('rounded-box border-neutral flex flex-col overflow-x-auto border border-solid gap-3 @container')); static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DataTableContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.0", type: DataTableContainerComponent, isStandalone: true, selector: "st-data-table", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, pinRows: { classPropertyName: "pinRows", publicName: "pinRows", isSignal: true, isRequired: false, transformFunction: null }, pinColumns: { classPropertyName: "pinColumns", publicName: "pinColumns", isSignal: true, isRequired: false, transformFunction: null }, zebra: { classPropertyName: "zebra", publicName: "zebra", isSignal: true, isRequired: false, transformFunction: null }, paginatorType: { classPropertyName: "paginatorType", publicName: "paginatorType", isSignal: true, isRequired: false, transformFunction: null }, totalItems: { classPropertyName: "totalItems", publicName: "totalItems", isSignal: true, isRequired: false, transformFunction: null }, pagination: { classPropertyName: "pagination", publicName: "pagination", isSignal: true, isRequired: false, transformFunction: null }, maxSize: { classPropertyName: "maxSize", publicName: "maxSize", isSignal: true, isRequired: false, transformFunction: null }, hasActiveFiltersCallback: { classPropertyName: "hasActiveFiltersCallback", publicName: "hasActiveFiltersCallback", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pageChanged: "pageChanged", pageSizeChanged: "pageSizeChanged" }, providers: [DataTableContextService], ngImport: i0, template: "<div [class]=\"containerClass()\">\n\t<!-- Table container -->\n\t<div [class]=\"tableContainerClass()\">\n\t\t<div class=\"w-full flex-grow overflow-x-auto\">\n\t\t\t@if (hasNoData()) {\n\t\t\t\t<!-- No data state -->\n\t\t\t\t<div class=\"flex flex-col items-center justify-center py-12 text-center\">\n\t\t\t\t\t<div class=\"text-base-content/40 mb-3\">\n\t\t\t\t\t\t<st-icon [icon]=\"noDataIcon\" size=\"lg\" color=\"base-content\" class=\"mx-auto opacity-40\"></st-icon>\n\t\t\t\t\t</div>\n\t\t\t\t\t<h3 typography tyVariant=\"h6\" class=\"text-base-content/60 mb-2\">{{ 'sdk.dataTable.noData.title' | translate }}</h3>\n\t\t\t\t\t<p typography tyVariant=\"body-sm\" class=\"text-base-content/40\">{{ 'sdk.dataTable.noData.message' | translate }}</p>\n\t\t\t\t</div>\n\t\t\t} @else if (hasNoResults()) {\n\t\t\t\t<!-- No results from filters state -->\n\t\t\t\t<div class=\"flex flex-col items-center justify-center py-12 text-center\">\n\t\t\t\t\t<div class=\"text-base-content/40 mb-3\">\n\t\t\t\t\t\t<st-icon [icon]=\"noResultsIcon\" size=\"xl\" color=\"base-content\" class=\"mx-auto opacity-40\"></st-icon>\n\t\t\t\t\t</div>\n\t\t\t\t\t<h3 typography tyVariant=\"h6\" class=\"text-base-content/60 mb-2\">{{ 'sdk.dataTable.noResults.title' | translate }}</h3>\n\t\t\t\t\t<p typography tyVariant=\"body-sm\" class=\"text-base-content/40\">{{ 'sdk.dataTable.noResults.message' | translate }}</p>\n\t\t\t\t</div>\n\t\t\t} @else {\n\t\t\t\t<table [class]=\"tableClass()\">\n\t\t\t\t\t<ng-content></ng-content>\n\t\t\t\t</table>\n\t\t\t}\n\t\t</div>\n\t</div>\n\n\t<!-- Pagination from showcase -->\n\t<div class=\"@container flex w-full flex-col gap-4 overflow-x-auto pb-3\">\n\t\t<!-- Divider -->\n\t\t<div class=\"divider divider-neutral my-0 h-[0.0625rem] px-4 before:h-[0.0625rem] after:h-[0.0625rem]\"></div>\n\t\t<!-- Paginator Container -->\n\t\t<div\n\t\t\tclass=\"flex flex-col items-center gap-4 px-4 @2xl:flex-row @2xl:justify-between @2xl:gap-2 @4xl:grid @4xl:grid-cols-[auto_1fr_auto] @4xl:items-center\"\n\t\t>\n\t\t\t<!-- Entries range text -->\n\t\t\t<div class=\"flex items-center justify-end\">\n\t\t\t\t<span typography [tyVariant]=\"paginatorTextSize()\" [tyColor]=\"'inherit'\">{{\n\t\t\t\t\t'sdk.dataTable.entriesRange' | translate: { range: paginationRange(), total: totalItems() }\n\t\t\t\t}}</span>\n\t\t\t</div>\n\n\t\t\t<!-- Paginator -->\n\t\t\t<div class=\"flex justify-center\">\n\t\t\t\t<st-paginator\n\t\t\t\t\t[pages]=\"totalPages()\"\n\t\t\t\t\t[currentPage]=\"pagination().currentPage\"\n\t\t\t\t\t[maxSize]=\"maxSize()\"\n\t\t\t\t\ttype=\"default\"\n\t\t\t\t\t(pageChanged)=\"handlePageChange($event)\"\n\t\t\t\t></st-paginator>\n\t\t\t</div>\n\n\t\t\t<!-- Entries per page dropdown -->\n\t\t\t<st-select\n\t\t\t\t[options]=\"entriesPerPageOptions()\"\n\t\t\t\t[value]=\"pagination().itemsPerPage\"\n\t\t\t\tvalueKey=\"value\"\n\t\t\t\tdisplayKey=\"text\"\n\t\t\t\tsize=\"sm\"\n\t\t\t\t(valueUpdated)=\"handlePageSizeChange($event)\"\n\t\t\t\tvariant=\"secondary\"\n\t\t\t\t[allowClear]=\"false\"\n\t\t\t></st-select>\n\t\t</div>\n\t</div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: PaginatorComponent, selector: "st-paginator", inputs: ["paginatorId", "maxSize", "type", "pages", "currentPage"], outputs: ["pageChanged", "currentPageChange"] }, { kind: "directive", type: TypographyDirective, selector: "[typography]", inputs: ["tyVariant", "tyColor", "tyFontWeight", "tyTextOverflow"] }, { kind: "component", type: SelectComponent, selector: "st-select", inputs: ["variant", "size", "ghost", "name", "placeholder", "label", "value", "options", "displayKey", "valueKey", "allowClear", "parentForm", "formControlName", "disabled"], outputs: ["valueChange", "disabledChange", "blurred", "valueUpdated"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }, { kind: "component", type: IconComponent, selector: "st-icon", inputs: ["color", "size", "icon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DataTableContainerComponent, decorators: [{ type: Component, args: [{ selector: 'st-data-table', imports: [CommonModule, PaginatorComponent, TypographyDirective, SelectComponent, TranslatePipe, IconComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [DataTableContextService], template: "<div [class]=\"containerClass()\">\n\t<!-- Table container -->\n\t<div [class]=\"tableContainerClass()\">\n\t\t<div class=\"w-full flex-grow overflow-x-auto\">\n\t\t\t@if (hasNoData()) {\n\t\t\t\t<!-- No data state -->\n\t\t\t\t<div class=\"flex flex-col items-center justify-center py-12 text-center\">\n\t\t\t\t\t<div class=\"text-base-content/40 mb-3\">\n\t\t\t\t\t\t<st-icon [icon]=\"noDataIcon\" size=\"lg\" color=\"base-content\" class=\"mx-auto opacity-40\"></st-icon>\n\t\t\t\t\t</div>\n\t\t\t\t\t<h3 typography tyVariant=\"h6\" class=\"text-base-content/60 mb-2\">{{ 'sdk.dataTable.noData.title' | translate }}</h3>\n\t\t\t\t\t<p typography tyVariant=\"body-sm\" class=\"text-base-content/40\">{{ 'sdk.dataTable.noData.message' | translate }}</p>\n\t\t\t\t</div>\n\t\t\t} @else if (hasNoResults()) {\n\t\t\t\t<!-- No results from filters state -->\n\t\t\t\t<div class=\"flex flex-col items-center justify-center py-12 text-center\">\n\t\t\t\t\t<div class=\"text-base-content/40 mb-3\">\n\t\t\t\t\t\t<st-icon [icon]=\"noResultsIcon\" size=\"xl\" color=\"base-content\" class=\"mx-auto opacity-40\"></st-icon>\n\t\t\t\t\t</div>\n\t\t\t\t\t<h3 typography tyVariant=\"h6\" class=\"text-base-content/60 mb-2\">{{ 'sdk.dataTable.noResults.title' | translate }}</h3>\n\t\t\t\t\t<p typography tyVariant=\"body-sm\" class=\"text-base-content/40\">{{ 'sdk.dataTable.noResults.message' | translate }}</p>\n\t\t\t\t</div>\n\t\t\t} @else {\n\t\t\t\t<table [class]=\"tableClass()\">\n\t\t\t\t\t<ng-content></ng-content>\n\t\t\t\t</table>\n\t\t\t}\n\t\t</div>\n\t</div>\n\n\t<!-- Pagination from showcase -->\n\t<div class=\"@container flex w-full flex-col gap-4 overflow-x-auto pb-3\">\n\t\t<!-- Divider -->\n\t\t<div class=\"divider divider-neutral my-0 h-[0.0625rem] px-4 before:h-[0.0625rem] after:h-[0.0625rem]\"></div>\n\t\t<!-- Paginator Container -->\n\t\t<div\n\t\t\tclass=\"flex flex-col items-center gap-4 px-4 @2xl:flex-row @2xl:justify-between @2xl:gap-2 @4xl:grid @4xl:grid-cols-[auto_1fr_auto] @4xl:items-center\"\n\t\t>\n\t\t\t<!-- Entries range text -->\n\t\t\t<div class=\"flex items-center justify-end\">\n\t\t\t\t<span typography [tyVariant]=\"paginatorTextSize()\" [tyColor]=\"'inherit'\">{{\n\t\t\t\t\t'sdk.dataTable.entriesRange' | translate: { range: paginationRange(), total: totalItems() }\n\t\t\t\t}}</span>\n\t\t\t</div>\n\n\t\t\t<!-- Paginator -->\n\t\t\t<div class=\"flex justify-center\">\n\t\t\t\t<st-paginator\n\t\t\t\t\t[pages]=\"totalPages()\"\n\t\t\t\t\t[currentPage]=\"pagination().currentPage\"\n\t\t\t\t\t[maxSize]=\"maxSize()\"\n\t\t\t\t\ttype=\"default\"\n\t\t\t\t\t(pageChanged)=\"handlePageChange($event)\"\n\t\t\t\t></st-paginator>\n\t\t\t</div>\n\n\t\t\t<!-- Entries per page dropdown -->\n\t\t\t<st-select\n\t\t\t\t[options]=\"entriesPerPageOptions()\"\n\t\t\t\t[value]=\"pagination().itemsPerPage\"\n\t\t\t\tvalueKey=\"value\"\n\t\t\t\tdisplayKey=\"text\"\n\t\t\t\tsize=\"sm\"\n\t\t\t\t(valueUpdated)=\"handlePageSizeChange($event)\"\n\t\t\t\tvariant=\"secondary\"\n\t\t\t\t[allowClear]=\"false\"\n\t\t\t></st-select>\n\t\t</div>\n\t</div>\n</div>\n" }] }], ctorParameters: () => [] }); class DataTableSubComponentToggleCellComponent { row = input.required(); isExpanded = input.required(); // Current state of the sub-component for this row toggled = output(); // Emits row.id when clicked to trigger parent component update // Input to determine if this specific row is eligible to display a sub-component canToggleSubComponent = input(false); iconChevronDown = matKeyboardArrowDown; iconInfo = matInfo; static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DataTableSubComponentToggleCellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.0", type: DataTableSubComponentToggleCellComponent, isStandalone: true, selector: "st-data-table-sub-component-toggle-cell", inputs: { row: { classPropertyName: "row", publicName: "row", isSignal: true, isRequired: true, transformFunction: null }, isExpanded: { classPropertyName: "isExpanded", publicName: "isExpanded", isSignal: true, isRequired: true, transformFunction: null }, canToggleSubComponent: { classPropertyName: "canToggleSubComponent", publicName: "canToggleSubComponent", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { toggled: "toggled" }, ngImport: i0, template: ` @if (canToggleSubComponent()) { <st-button variant="secondary" size="xs" [circle]="true" [ghost]="true" class="group" (click)="toggled.emit(row().id)" [attr.aria-expanded]="isExpanded()" title="{{ isExpanded() ? 'Hide Details' : 'Show Details' }}" > <st-icon class="transition-transform duration-300 ease-in-out group-aria-[expanded=false]:rotate-0 group-aria-[expanded=true]:rotate-180" [icon]="iconChevronDown" size="sm" ></st-icon> <span class="sr-only">{{ isExpanded() ? 'Hide Details' : 'Show Details' }}</span> </st-button> } @else { <!-- Display an informational icon if a sub-component cannot be shown for this row --> <st-button variant="secondary" size="xs" [circle]="true" [ghost]="true" [attr.aria-expanded]="isExpanded()" title="{{ isExpanded() ? 'Hide Details' : 'Show Details' }}" [disabled]="true" > <st-icon [icon]="iconInfo" size="sm"></st-icon> <span class="sr-only">{{ isExpanded() ? 'Hide Details' : 'Show Details' }}</span> </st-button> } `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IconComponent, selector: "st-icon", inputs: ["color", "size", "icon"] }, { kind: "component", type: ButtonComponent, selector: "st-button", inputs: ["variant", "ghost", "outline", "link", "soft", "dash", "wide", "circle", "square", "glass", "block", "loader", "size", "shadow", "focusable", "icon", "iconPosition", "type", "disabled"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DataTableSubComponentToggleCellComponent, decorators: [{ type: Component, args: [{ standalone: true, selector: 'st-data-table-sub-component-toggle-cell', imports: [CommonModule, IconComponent, ButtonComponent], template: ` @if (canToggleSubComponent()) { <st-button variant="secondary" size="xs" [circle]="true" [ghost]="true" class="group" (click)="toggled.emit(row().id)" [attr.aria-expanded]="isExpanded()" title="{{ isExpanded() ? 'Hide Details' : 'Show Details' }}" > <st-icon class="transition-transform duration-300 ease-in-out group-aria-[expanded=false]:rotate-0 group-aria-[expanded=true]:rotate-180" [icon]="iconChevronDown" size="sm" ></st-icon> <span class="sr-only">{{ isExpanded() ? 'Hide Details' : 'Show Details' }}</span> </st-button> } @else { <!-- Display an informational icon if a sub-component cannot be shown for this row --> <st-button variant="secondary" size="xs" [circle]="true" [ghost]="true" [attr.aria-expanded]="isExpanded()" title="{{ isExpanded() ? 'Hide Details' : 'Show Details' }}" [disabled]="true" > <st-icon [icon]="iconInfo" size="sm"></st-icon> <span class="sr-only">{{ isExpanded() ? 'Hide Details' : 'Show Details' }}</span> </st-button> } `, changeDetection: ChangeDetectionStrategy.OnPush, }] }] }); /**