@kodaris/krubble-components 1.0.55 → 1.0.56

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.
@@ -573,7 +573,7 @@
573
573
  {
574
574
  "kind": "variable",
575
575
  "name": "KRTable",
576
- "default": "class KRTable extends i$2 { constructor() { super(...arguments); /** * Internal flag to switch between scroll edge modes: * - 'overlay': Fixed padding with overlay elements that hide content at edges (scrollbar at viewport edge) * - 'edge': Padding scrolls with content, allowing table to reach edges when scrolling */ this._scrollStyle = 'overlay'; this._data = []; this._dataState = 'idle'; this._page = 1; this._pageSize = 50; this._totalItems = 0; this._totalPages = 0; this._searchQuery = ''; this._canScrollLeft = false; this._canScrollRight = false; this._canScrollHorizontal = false; this._columnPickerOpen = false; this._filterPanelOpened = null; this._filterPanelTab = 'filter'; this._buckets = new Map(); this._filterPanelPos = { top: 0, left: 0 }; this._resizing = null; this._resizeObserver = null; this._searchPositionLocked = false; this._model = new KRTableModel(); this.def = { columns: [] }; this._handleClickOutside = (e) => { const path = e.composedPath(); if (this._columnPickerOpen) { const picker = this.shadowRoot?.querySelector('.column-picker-wrapper'); if (picker && !path.includes(picker)) { this._columnPickerOpen = false; } } if (this._filterPanelOpened) { if (!path.some((el) => el.classList?.contains('filter-panel'))) { this._handleFilterApply(); } } }; this._handleResizeMove = (e) => { if (!this._resizing) return; const col = this._model.columns.find(c => c.id === this._resizing.columnId); if (col) { const newWidth = this._resizing.startWidth + (e.clientX - this._resizing.startX); col.width = `${Math.min(900, Math.max(50, newWidth))}px`; this.requestUpdate(); } }; this._handleResizeEnd = () => { this._resizing = null; document.removeEventListener('mousemove', this._handleResizeMove); document.removeEventListener('mouseup', this._handleResizeEnd); }; } connectedCallback() { super.connectedCallback(); this.classList.toggle('kr-table--scroll-overlay', this._scrollStyle === 'overlay'); this.classList.toggle('kr-table--scroll-edge', this._scrollStyle === 'edge'); this._fetch(); this._initRefresh(); document.addEventListener('click', this._handleClickOutside); this._resizeObserver = new ResizeObserver(() => { // Unlock and recalculate on resize since layout changes this._searchPositionLocked = false; this._updateSearchPosition(); }); this._resizeObserver.observe(this); } disconnectedCallback() { super.disconnectedCallback(); clearInterval(this._refreshTimer); document.removeEventListener('click', this._handleClickOutside); this._resizeObserver?.disconnect(); } willUpdate(changedProperties) { if (changedProperties.has('def')) { // Build internal model from user-provided def this._model = new KRTableModel(); if (this.def.title) { this._model.title = this.def.title; } if (this.def.actions) { this._model.actions = this.def.actions; } if (this.def.data) { this._model.data = this.def.data; } if (this.def.dataSource) { this._model.dataSource = this.def.dataSource; } if (typeof this.def.refreshInterval === 'number') { this._model.refreshInterval = this.def.refreshInterval; } if (typeof this.def.pageSize === 'number') { this._model.pageSize = this.def.pageSize; } if (this.def.rowClickable) { this._model.rowClickable = this.def.rowClickable; } if (this.def.rowHref) { this._model.rowHref = this.def.rowHref; } this._model.columns = this.def.columns.map(col => { const column = { ...col, filter: null }; if (!column.type) { column.type = 'string'; } if (column.type === 'actions') { column.label = col.label ?? ''; column.sticky = 'right'; column.resizable = false; return column; } if (col.filterable || col.facetable) { column.filter = new KRQuery(); column.filter.field = col.id; column.filter.type = column.type; if (col.filter) { column.filter.setOperator(col.filter.operator); column.filter.setValue(col.filter.value); } else if (col.facetable && !col.filterable) { column.filter.operator = 'in'; column.filter.value = []; } else if (column.filter.type === 'string') { column.filter.operator = 'contains'; } } return column; }); if (this.def.displayedColumns) { this._model.displayedColumns = this.def.displayedColumns; } else { this._model.displayedColumns = this._model.columns.map(c => c.id); } this._fetch(); this._initRefresh(); } } updated(changedProperties) { this._updateScrollFlags(); this._syncSlottedContent(); } /** Syncs light DOM content for cells with custom render functions */ _syncSlottedContent() { const columns = this.getDisplayedColumns().filter(col => col.render); if (!columns.length) return; // Clear old slotted content this.querySelectorAll('[slot^=\"cell-\"]').forEach(el => el.remove()); // Create new slotted content this._data.forEach((row, rowIndex) => { columns.forEach(col => { const result = col.render(row); if (!result) return; const el = document.createElement('span'); el.slot = `cell-${rowIndex}-${col.id}`; if (col.type === 'actions') { el.style.display = 'flex'; el.style.gap = '8px'; } if (typeof result === 'string') { el.innerHTML = result; } else { D(result, el); } this.appendChild(el); }); }); } // ---------------------------------------------------------------------------- // Public Interface // ---------------------------------------------------------------------------- refresh() { this._fetch(); } goToPrevPage() { if (this._page > 1) { this._page--; this._fetch(); } } goToNextPage() { if (this._page < this._totalPages) { this._page++; this._fetch(); } } goToPage(page) { if (page >= 1 && page <= this._totalPages) { this._page = page; this._fetch(); } } // ---------------------------------------------------------------------------- // Data Fetching // ---------------------------------------------------------------------------- _toSolrData() { const request = { page: this._page - 1, size: this._pageSize, sorts: [], filterFields: [], queryFields: [], facetFields: [] }; for (const col of this._model.columns) { if (!col.filter || col.filter.isEmpty() || !col.filter.isValid()) { continue; } const filterData = col.filter.toSolrData(); if (col.facetable && col.filter.operator === 'in') { filterData.tagged = true; } request.filterFields.push(filterData); } for (const col of this._model.columns) { if (!col.facetable) { continue; } request.facetFields.push({ name: col.id, type: 'FIELD', limit: 100, sort: 'count', minimumCount: 1 }); } if (this._searchQuery?.trim().length) { request.queryFields.push({ name: '_text_', operation: 'IS', value: termify(this._searchQuery, false) }); } return request; } _toDbParams() { const request = { page: this._page - 1, size: this._pageSize, sorts: [], filterFields: [], queryFields: [], facetFields: [] }; for (const col of this._model.columns) { if (!col.filter || col.filter.isEmpty() || !col.filter.isValid()) { continue; } request.filterFields.push(col.filter.toDbParams()); } if (this._searchQuery?.trim().length) { this._model.columns.filter(col => col.searchable).forEach(col => { request.queryFields.push({ name: col.id, operation: 'CONTAINS', value: this._searchQuery, and: false }); }); } return request; } /** * Fetches data from the API and updates the table. * Shows a loading spinner while fetching, then displays rows on success * or an error snackbar on failure. * Request/response format depends on dataSource.mode (solr, opensearch, db). */ _fetch() { if (this._model.data) { this._data = this._model.data; this._totalItems = this._model.data.length; this._totalPages = Math.ceil(this._model.data.length / this._pageSize); this._dataState = 'success'; return; } if (!this._model.dataSource) return; this._dataState = 'loading'; let request; if (this._model.dataSource.mode === 'db') { request = this._toDbParams(); } else { request = this._toSolrData(); } this._model.dataSource.fetch(request) .then(response => { // Parse response based on mode switch (this._model.dataSource?.mode) { case 'opensearch': { throw Error('Opensearch not supported yet'); } case 'db': { const res = response; this._data = res.data.content; this._totalItems = res.data.totalElements; this._totalPages = res.data.totalPages; this._pageSize = res.data.size; break; } default: { // solr const res = response; this._data = res.data.content; this._totalItems = res.data.totalElements; this._totalPages = res.data.totalPages; this._pageSize = res.data.size; this._parseFacetResults(res); } } this._dataState = 'success'; this._updateSearchPosition(); }) .catch(err => { this._dataState = 'error'; KRSnackbar.show({ message: err instanceof Error ? err.message : 'Failed to load data', type: 'error' }); }); } _parseFacetResults(response) { if (!response.data.facetFields) { return; } for (const col of this._model.columns) { if (!col.facetable) { continue; } const rawBuckets = response.data.facetFields[col.id]; if (!rawBuckets) { this._buckets.set(col.id, []); continue; } const buckets = []; for (const raw of rawBuckets) { // Solr returns boolean facet values as strings — coerce to actual booleans // so they match the filter values stored by toggle(). let val = raw.name; if (col.type === 'boolean' && typeof raw.name === 'string') { if (raw.name === 'true') { val = true; } else if (raw.name === 'false') { val = false; } } if (raw.name === null && raw.count > 0) { buckets.unshift({ val: null, count: raw.count }); } if (raw.name !== null) { buckets.push({ val: val, count: raw.count }); } } // Bucket sync: ensure selected values appear even with 0 results if (col.filter && col.filter.operator === 'in' && Array.isArray(col.filter.value)) { for (const selectedVal of col.filter.value) { if (!buckets.some(b => b.val === selectedVal)) { buckets.push({ val: selectedVal, count: 0 }); } } } this._buckets.set(col.id, buckets); } // Trigger re-render since Map mutation doesn't trigger Lit updates this._buckets = new Map(this._buckets); } /** * Sets up auto-refresh so the table automatically fetches fresh data * at a regular interval (useful for dashboards, monitoring views). * Configured via def.refreshInterval in milliseconds. */ _initRefresh() { clearInterval(this._refreshTimer); if (this._model.refreshInterval && this._model.refreshInterval > 0) { this._refreshTimer = window.setInterval(() => { this._fetch(); }, this._model.refreshInterval); } } _handleSearch(e) { const input = e.target; this._searchQuery = input.value; this._page = 1; this._fetch(); } _getGridTemplateColumns() { const cols = this.getDisplayedColumns(); return cols.map((col) => { // If column has explicit width, use it if (col.width) { return col.width; } // Actions columns: fit content without minimum if (col.type === 'actions') { return 'max-content'; } // No width specified - use content-based sizing with minimum return 'minmax(80px, auto)'; }).join(' '); } /** * Updates search position to be centered with equal gaps from title and tools. * On first call: resets to flex centering, measures position, then locks with fixed margin. * Subsequent calls are ignored unless _searchPositionLocked is reset (e.g., on resize). */ _updateSearchPosition() { // Skip if already locked (prevents shifts on pagination changes) if (this._searchPositionLocked) return; const search = this.shadowRoot?.querySelector('.search'); const searchField = search?.querySelector('.search-field'); if (!search || !searchField) return; // Reset to flex centering search.style.justifyContent = 'center'; searchField.style.marginLeft = ''; requestAnimationFrame(() => { const searchRect = search.getBoundingClientRect(); const fieldRect = searchField.getBoundingClientRect(); // Calculate how far from the left of search container the field currently is const currentOffset = fieldRect.left - searchRect.left; // Lock position: switch to flex-start and use fixed margin search.style.justifyContent = 'flex-start'; searchField.style.marginLeft = `${currentOffset}px`; // Mark as locked so pagination changes don't shift the search this._searchPositionLocked = true; }); } // ---------------------------------------------------------------------------- // Columns // ---------------------------------------------------------------------------- _toggleColumnPicker() { this._columnPickerOpen = !this._columnPickerOpen; } _toggleColumn(columnId) { if (this._model.displayedColumns.includes(columnId)) { this._model.displayedColumns = this._model.displayedColumns.filter(id => id !== columnId); } else { this._model.displayedColumns = [...this._model.displayedColumns, columnId]; } this.requestUpdate(); } // Clear any existing text selection on mousedown so we only detect // selections made during this click gesture, not stale selections from elsewhere _handleRowMouseDown() { if (!this._model.rowClickable) { return; } window.getSelection()?.removeAllRanges(); } _handleRowClick(row, rowIndex) { if (!this._model.rowClickable) { return; } const selection = window.getSelection(); if (selection && selection.toString().length > 0) { return; } this.dispatchEvent(new CustomEvent('row-click', { detail: { row, rowIndex }, bubbles: true, composed: true })); } // When a user toggles a column on via the column picker, it gets appended // to _displayedColumns. By mapping over _displayedColumns (not def.columns), // the new column appears at the right edge of the table instead of jumping // back to its original position in the column definition. // Actions columns are always moved to the end. getDisplayedColumns() { return this._model.displayedColumns .map(id => this._model.columns.find(col => col.id === id)) .sort((a, b) => { if (a.type === 'actions' && b.type !== 'actions') return 1; if (a.type !== 'actions' && b.type === 'actions') return -1; return 0; }); } // ---------------------------------------------------------------------------- // Scrolling // ---------------------------------------------------------------------------- /** * Scroll event handler that updates scroll flags in real-time as user scrolls. * Updates shadow indicators to show if more content exists left/right. */ _handleScroll(e) { const container = e.target; this._canScrollLeft = container.scrollLeft > 0; this._canScrollRight = container.scrollLeft < container.scrollWidth - container.clientWidth - 1; } /** * Updates scroll state flags for the table content container. * - _canScrollLeft: true if scrolled right (can scroll back left) * - _canScrollRight: true if more content exists to the right * - _canScrollHorizontal: true if content is wider than container * These flags control scroll shadow indicators and CSS classes. */ _updateScrollFlags() { const container = this.shadowRoot?.querySelector('.content'); if (container) { this._canScrollLeft = container.scrollLeft > 0; this._canScrollRight = container.scrollWidth > container.clientWidth && container.scrollLeft < container.scrollWidth - container.clientWidth - 1; this._canScrollHorizontal = container.scrollWidth > container.clientWidth; } this.classList.toggle('kr-table--scroll-left-available', this._canScrollLeft); this.classList.toggle('kr-table--scroll-right-available', this._canScrollRight); this.classList.toggle('kr-table--scroll-horizontal-available', this._canScrollHorizontal); this.classList.toggle('kr-table--sticky-left', this.getDisplayedColumns().some(c => c.sticky === 'left')); this.classList.toggle('kr-table--sticky-right', this.getDisplayedColumns().some(c => c.sticky === 'right')); } // ---------------------------------------------------------------------------- // Column Resizing // ---------------------------------------------------------------------------- _handleResizeStart(e, columnId) { e.preventDefault(); const headerCell = this.shadowRoot?.querySelector(`.header-cell[data-column-id=\"${columnId}\"]`); this._resizing = { columnId, startX: e.clientX, startWidth: headerCell?.offsetWidth || 200 }; document.addEventListener('mousemove', this._handleResizeMove); document.addEventListener('mouseup', this._handleResizeEnd); } // ---------------------------------------------------------------------------- // Header // ---------------------------------------------------------------------------- _handleAction(action) { if (action.href) { return; } this.dispatchEvent(new CustomEvent('action', { detail: { action: action.id }, bubbles: true, composed: true })); } // ---------------------------------------------------------------------------- // Filter Handlers // ---------------------------------------------------------------------------- _handleKqlChange(e, column) { const kql = e.target.value.trim(); if (!kql) { column.filter.clear(); this.requestUpdate(); } else { column.filter.setKql(kql); this.requestUpdate(); if (!column.filter.isValid()) { return; } } this._page = 1; this._fetch(); } _handleFilterPanelToggle(e, column) { e.stopPropagation(); if (this._filterPanelOpened === column.id) { this._filterPanelOpened = null; } else { const rect = e.currentTarget.getBoundingClientRect(); let left = rect.left; if (left + 328 > window.innerWidth) { left = window.innerWidth - 328; } this._filterPanelPos = { top: rect.bottom + 4, left }; this._filterPanelOpened = column.id; if (column.facetable) { this._filterPanelTab = 'counts'; } else { this._filterPanelTab = 'filter'; } } } _handleKqlClear(column) { column.filter.clear(); this._page = 1; this._fetch(); } _handleFilterClear() { const column = this._model.columns.find(c => c.id === this._filterPanelOpened); if (column) { column.filter.clear(); if (column.facetable && !column.filterable) { column.filter.operator = 'in'; column.filter.value = []; } } this._filterPanelOpened = null; this._page = 1; this._fetch(); } _handleFilterTextKeydown(e, column) { if (e.key === 'Enter') { e.preventDefault(); this._handleFilterApply(); } } _handleOperatorChange(e, column) { column.filter.setOperator(e.target.value); this.requestUpdate(); } _handleFilterStringChange(e, column) { column.filter.setValue(e.target.value); this.requestUpdate(); } _handleFilterNumberChange(e, column) { column.filter.setValue(Number(e.target.value)); this.requestUpdate(); } _handleFilterDateChange(e, column) { column.filter.setValue(new Date(e.target.value), 'day'); this.requestUpdate(); } _handleFilterBooleanChange(e, column) { column.filter.setValue(e.target.value === 'true'); this.requestUpdate(); } _handleFilterDateStartChange(e, column) { column.filter.setStart(new Date(e.target.value), 'day'); this.requestUpdate(); } _handleFilterDateEndChange(e, column) { column.filter.setEnd(new Date(e.target.value), 'day'); this.requestUpdate(); } _handleFilterNumberStartChange(e, column) { column.filter.setStart(Number(e.target.value)); this.requestUpdate(); } _handleFilterNumberEndChange(e, column) { column.filter.setEnd(Number(e.target.value)); this.requestUpdate(); } _handleFilterListChange(e, column) { const items = e.target.value.split(',').map((v) => v.trim()).filter((v) => v !== ''); if (column.type === 'number') { column.filter.setValue(items.map((v) => Number(v))); } else { column.filter.setValue(items); } this.requestUpdate(); } _handleFilterApply() { this._filterPanelOpened = null; this._page = 1; this._fetch(); } _handleFilterPanelTabChange(e) { this._filterPanelTab = e.detail.activeTabId; } _handleBucketToggle(e, column, bucket) { column.filter.toggle(bucket.val); this._page = 1; this._fetch(); } // ---------------------------------------------------------------------------- // Rendering // ---------------------------------------------------------------------------- _renderCellContent(column, row, rowIndex) { const value = row[column.id]; if (column.render) { // Use slot to project content from light DOM so external styles apply return b `<slot name=\"cell-${rowIndex}-${column.id}\"></slot>`; } if (value === null || value === undefined) { return ''; } switch (column.type) { case 'number': if (column.format === 'currency' && typeof value === 'number') { return value.toLocaleString('en-US', { style: 'currency', currency: 'USD' }); } return String(value); case 'date': { let date; if (value instanceof Date) { date = value; } else if (typeof value === 'string' && /^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}/.test(value)) { // MySQL datetime format (UTC): \"2026-01-28 01:33:44:517\" // Replace last colon before ms with dot, append Z for UTC const isoString = value.replace(/(\\d{2}:\\d{2}:\\d{2}):(\\d+)$/, '$1.$2').replace(' ', 'T') + 'Z'; date = new Date(isoString); } else { date = new Date(value); } // Show date and time for datetime values in UTC return date.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', timeZone: 'UTC' }); } case 'boolean': if (value === true) return 'Yes'; if (value === false) return 'No'; return ''; default: return String(value); } } /** * Returns CSS classes for a header cell based on column config. */ _getHeaderCellClasses(column, index) { return { 'header-cell': true, 'header-cell--align-center': column.align === 'center', 'header-cell--align-right': column.align === 'right', 'header-cell--sticky-left': column.sticky === 'left', 'header-cell--sticky-left-last': column.sticky === 'left' && !this.getDisplayedColumns().slice(index + 1).some(c => c.sticky === 'left'), 'header-cell--sticky-right': column.sticky === 'right', 'header-cell--sticky-right-first': column.sticky === 'right' && !this.getDisplayedColumns().slice(0, index).some(c => c.sticky === 'right') }; } /** * Returns CSS classes for a table cell based on column config: * - Alignment (center, right) * - Sticky positioning (left, right) * - Border classes for the last left-sticky or first right-sticky column */ _getCellClasses(column, index) { return { 'cell': true, 'cell--actions': column.type === 'actions', 'cell--align-center': column.align === 'center', 'cell--align-right': column.align === 'right', 'cell--sticky-left': column.sticky === 'left', 'cell--sticky-left-last': column.sticky === 'left' && !this.getDisplayedColumns().slice(index + 1).some(c => c.sticky === 'left'), 'cell--sticky-right': column.sticky === 'right', 'cell--sticky-right-first': column.sticky === 'right' && !this.getDisplayedColumns().slice(0, index).some(c => c.sticky === 'right') }; } /** * Returns inline styles for a table cell: * - Width (from column config or default 150px) * - Min-width (if specified) * - Left/right offset for sticky columns (calculated from widths of preceding sticky columns) */ _getCellStyle(column, index) { const styles = {}; if (column.sticky === 'left') { let leftOffset = 0; for (let i = 0; i < index; i++) { const col = this.getDisplayedColumns()[i]; if (col.sticky === 'left') { leftOffset += parseInt(col.width || '0', 10); } } styles.left = `${leftOffset}px`; } if (column.sticky === 'right') { let rightOffset = 0; for (let i = index + 1; i < this.getDisplayedColumns().length; i++) { const col = this.getDisplayedColumns()[i]; if (col.sticky === 'right') { rightOffset += parseInt(col.width || '0', 10); } } styles.right = `${rightOffset}px`; } return styles; } /** * Renders the pagination controls: * - Previous page arrow (disabled on first page) * - Range text showing \"1-50 of 150\" format * - Next page arrow (disabled on last page) * * Hidden when there's no data or all data fits on one page. */ _renderPagination() { const start = (this._page - 1) * this._pageSize + 1; const end = Math.min(this._page * this._pageSize, this._totalItems); return b ` <div class=\"pagination\"> <span class=\"pagination-icon ${this._page === 1 ? 'pagination-icon--disabled' : ''}\" @click=${this.goToPrevPage} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z\"/></svg> </span> <span class=\"pagination-info\">${start}-${end} of ${this._totalItems}</span> <span class=\"pagination-icon ${this._page === this._totalPages ? 'pagination-icon--disabled' : ''}\" @click=${this.goToNextPage} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\"/></svg> </span> </div> `; } /** * Renders the header toolbar containing: * - Title (left) * - Search bar with view selector dropdown (center) * - Tools (right): page navigation, refresh button, column visibility picker, actions dropdown * * Hidden when there's no title, no actions, and data fits on one page. */ _renderHeader() { if (!this._model.title && !this._model.actions?.length && this._totalPages <= 1) { return A; } return b ` <div class=\"header\"> <div class=\"title\">${this._model.title ?? ''}</div> ${this._model.dataSource?.mode === 'db' && !this._model.columns.some(col => col.searchable) ? b `<div class=\"search\"></div>` : b ` <div class=\"search\"> <!-- TODO: Saved views dropdown <div class=\"views\"> <span>Default View</span> <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z\"/></svg> </div> --> <div class=\"search-field\"> <svg class=\"search-icon\" viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z\"/></svg> <input type=\"text\" class=\"search-input\" placeholder=\"Search...\" .value=${this._searchQuery} @input=${this._handleSearch} /> </div> </div> `} <div class=\"tools\"> ${this._renderPagination()} <span class=\"refresh\" title=\"Refresh\" @click=${() => this.refresh()}> <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M480-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100 70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z\"/></svg> </span> <div class=\"column-picker-wrapper\"> <span class=\"header-icon\" title=\"Columns\" @click=${this._toggleColumnPicker}> <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M121-280v-400q0-33 23.5-56.5T201-760h559q33 0 56.5 23.5T840-680v400q0 33-23.5 56.5T760-200H201q-33 0-56.5-23.5T121-280Zm79 0h133v-400H200v400Zm213 0h133v-400H413v400Zm213 0h133v-400H626v400Z\"/></svg> </span> <div class=\"column-picker ${this._columnPickerOpen ? 'open' : ''}\"> ${[...this._model.columns].filter(col => col.type !== 'actions').sort((a, b) => (a.label ?? a.id).localeCompare(b.label ?? b.id)).map(col => b ` <div class=\"column-picker-item\" @click=${() => this._toggleColumn(col.id)}> <div class=\"column-picker-checkbox ${this._model.displayedColumns.includes(col.id) ? 'checked' : ''}\"> <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z\"/></svg> </div> <span class=\"column-picker-label\">${col.label ?? col.id}</span> </div> `)} </div> </div> ${this._model.actions?.length === 1 ? b ` <kr-button class=\"actions\" .href=${this._model.actions[0].href} .target=${this._model.actions[0].target} @click=${() => this._handleAction(this._model.actions[0])} > ${this._model.actions[0].label} </kr-button> ` : this._model.actions?.length ? b ` <kr-button class=\"actions\" .options=${this._model.actions.map(a => ({ id: a.id, label: a.label }))} @option-select=${(e) => this._handleAction({ id: e.detail.id, label: e.detail.label })} > Actions </kr-button> ` : A} </div> </div> `; } /** Renders status message (loading, error, empty) */ _renderStatus() { if (this._dataState === 'loading' && this._data.length === 0) { return b `<div class=\"status\">Loading...</div>`; } if (this._dataState === 'error' && this._data.length === 0) { return b `<div class=\"status status--error\">Error loading data</div>`; } if (this._data.length === 0) { return b `<div class=\"status\">No data available</div>`; } return A; } _renderFilterPanel() { if (!this._filterPanelOpened) { return A; } const column = this._model.columns.find(c => c.id === this._filterPanelOpened); // Build filter content (operator + value input) let valueInput = b ``; if (column.filter.operator === 'empty' || column.filter.operator === 'n_empty') { valueInput = b ` <input type=\"text\" class=\"filter-panel__input\" disabled .value=${column.filter.text} /> `; } else if (column.filter.operator === 'between' && column.type === 'date') { valueInput = b ` <input type=\"date\" class=\"filter-panel__input\" .valueAsDate=${column.filter.value?.start ?? null} @change=${(e) => this._handleFilterDateStartChange(e, column)} /> <input type=\"date\" class=\"filter-panel__input\" .valueAsDate=${column.filter.value?.end ?? null} @change=${(e) => this._handleFilterDateEndChange(e, column)} /> `; } else if (column.filter.operator === 'between' && column.type === 'number') { valueInput = b ` <input type=\"number\" class=\"filter-panel__input\" placeholder=\"Start\" .value=${column.filter.value?.start ?? ''} @input=${(e) => this._handleFilterNumberStartChange(e, column)} @keydown=${(e) => this._handleFilterTextKeydown(e, column)} /> <input type=\"number\" class=\"filter-panel__input\" placeholder=\"End\" .value=${column.filter.value?.end ?? ''} @input=${(e) => this._handleFilterNumberEndChange(e, column)} @keydown=${(e) => this._handleFilterTextKeydown(e, column)} /> `; } else if (column.filter.operator === 'in') { valueInput = b ` <textarea class=\"filter-panel__textarea\" rows=\"3\" placeholder=\"Values (comma-separated)\" .value=${column.filter.text} @input=${(e) => this._handleFilterListChange(e, column)} @keydown=${(e) => this._handleFilterTextKeydown(e, column)} ></textarea> `; } else if (column.type === 'boolean') { valueInput = b ` <kr-select-field placeholder=\"Value\" .value=${String(column.filter.value ?? '')} @change=${(e) => this._handleFilterBooleanChange(e, column)} > <kr-select-option value=\"true\">Yes</kr-select-option> <kr-select-option value=\"false\">No</kr-select-option> </kr-select-field> `; } else if (column.type === 'date') { valueInput = b ` <input type=\"date\" class=\"filter-panel__input\" .valueAsDate=${column.filter.value} @change=${(e) => this._handleFilterDateChange(e, column)} /> `; } else if (column.type === 'number') { valueInput = b ` <input type=\"number\" class=\"filter-panel__input\" placeholder=\"Value\" min=\"0\" .value=${column.filter.text} @input=${(e) => this._handleFilterNumberChange(e, column)} @keydown=${(e) => this._handleFilterTextKeydown(e, column)} /> `; } else { valueInput = b ` <input type=\"text\" class=\"filter-panel__input\" placeholder=\"Value\" .value=${column.filter.text} @input=${(e) => this._handleFilterStringChange(e, column)} @keydown=${(e) => this._handleFilterTextKeydown(e, column)} /> `; } const filterContent = b ` <div class=\"filter-panel__content\"> <kr-select-field .value=${column.filter.operator} @change=${(e) => this._handleOperatorChange(e, column)} > ${getOperatorsForType(column.type).map(op => b ` <kr-select-option value=${op.key}>${op.label}</kr-select-option> `)} </kr-select-field> ${valueInput} </div> `; // Build bucket list content const buckets = this._buckets.get(column.id) || []; let bucketContent; if (!buckets.length) { bucketContent = b `<div class=\"bucket-empty\">No data</div>`; } else { bucketContent = b ` <div class=\"buckets\"> ${buckets.map(bucket => { let bucketLabel = '(Empty)'; if (bucket.val !== null && bucket.val !== undefined) { if (column.type === 'boolean') { if (bucket.val === true || bucket.val === 'true') { bucketLabel = 'Yes'; } else { bucketLabel = 'No'; } } else { bucketLabel = String(bucket.val); } } let checkIcon = A; if (column.filter.has(bucket.val)) { checkIcon = b ` <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"> <path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z\"/> </svg> `; } return b ` <div class=\"bucket\" @click=${(e) => this._handleBucketToggle(e, column, bucket)} > <div class=${e$1({ 'bucket__checkbox': true, 'bucket__checkbox--checked': column.filter.has(bucket.val) })}> ${checkIcon} </div> <span class=\"bucket__label\">${bucketLabel}</span> <span class=\"bucket__count\">${bucket.count}</span> </div> `; })} </div> `; } // Build panel body — tabs if both filterable+facetable, otherwise just the relevant content let panelBody; if (column.facetable && column.filterable) { panelBody = b ` <kr-tab-group size=\"small\" active-tab-id=${this._filterPanelTab} @tab-change=${(e) => this._handleFilterPanelTabChange(e)} > <kr-tab id=\"filter\" label=\"Filter\"> ${filterContent} </kr-tab> <kr-tab id=\"counts\" label=\"Counts\"> ${bucketContent} </kr-tab> </kr-tab-group> `; } else if (column.facetable) { panelBody = bucketContent; } else { panelBody = filterContent; } return b ` <div class=\"filter-panel\" style=${o$1({ top: this._filterPanelPos.top + 'px', left: this._filterPanelPos.left + 'px' })} > ${panelBody} <div class=\"filter-panel__actions\"> <kr-button variant=\"outline\" color=\"secondary\" size=\"small\" @click=${this._handleFilterClear}> Clear </kr-button> <kr-button size=\"small\" @click=${this._handleFilterApply}> Apply </kr-button> </div> </div> `; } /** * Renders filter row below column headers. * Only displays for columns with filterable: true. */ _renderFilterRow() { const columns = this.getDisplayedColumns(); if (!columns.some(col => col.filterable || col.facetable)) { return A; } return b ` <div class=\"filter-row\"> ${columns.map((col, i) => { if (!col.filterable && !col.facetable) { return b `<div class=${e$1({ 'filter-cell': true, 'filter-cell--sticky-left': col.sticky === 'left', 'filter-cell--sticky-right': col.sticky === 'right', 'filter-cell--sticky-right-first': col.sticky === 'right' && !columns.slice(0, i).some((c) => c.sticky === 'right') })} style=${o$1(this._getCellStyle(col, i))} ></div>`; } return b ` <div class=${e$1({ 'filter-cell': true, 'filter-cell--sticky-left': col.sticky === 'left', 'filter-cell--sticky-right': col.sticky === 'right', 'filter-cell--sticky-right-first': col.sticky === 'right' && !columns.slice(0, i).some((c) => c.sticky === 'right') })} style=${o$1(this._getCellStyle(col, i))} > <div class=\"filter-cell__wrapper\"> <input type=\"text\" class=${e$1({ 'filter-cell__input': true, 'filter-cell__input--invalid': !col.filter.isValid() })} .value=${col.filter.kql} @change=${(e) => this._handleKqlChange(e, col)} /> ${col.filter?.kql?.length > 0 ? b ` <button class=\"filter-cell__clear\" @click=${() => this._handleKqlClear(col)} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"> <path d=\"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z\"/> </svg> </button> ` : A} <button class=${e$1({ 'filter-cell__advanced': true, 'filter-cell__advanced--opened': this._filterPanelOpened === col.id })} @click=${(e) => this._handleFilterPanelToggle(e, col)} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"> <path d=\"M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z\"/> </svg> </button> </div> </div> `; })} </div> `; } /** Renders the scrollable data grid with column headers and rows. */ _renderTable() { return b ` <div class=\"wrapper\"> <div class=\"overlay-left\"></div> <div class=\"overlay-right\"></div> ${this._renderStatus()} <div class=\"content\" @scroll=${this._handleScroll}> <div class=\"table\" style=\"grid-template-columns: ${this._getGridTemplateColumns()}\"> <div class=\"header-row\"> ${this.getDisplayedColumns().map((col, i) => b ` <div class=${e$1(this._getHeaderCellClasses(col, i))} style=${o$1(this._getCellStyle(col, i))} data-column-id=${col.id} >${col.label ?? col.id}${col.resizable !== false ? b `<div class=\"header-cell__resize\" @mousedown=${(e) => this._handleResizeStart(e, col.id)} ></div>` : A}</div> `)} </div> ${this._renderFilterRow()} ${this._data.map((row, rowIndex) => { const cells = this.getDisplayedColumns().map((col, i) => b ` <div class=${e$1(this._getCellClasses(col, i))} style=${o$1(this._getCellStyle(col, i))} data-column-id=${col.id} > ${this._renderCellContent(col, row, rowIndex)} </div> `); if (this._model.rowHref) { return b ` <a href=${this._model.rowHref(row)} class=${e$1({ 'row': true, 'row--clickable': true, 'row--link': true })} @mousedown=${() => this._handleRowMouseDown()} @click=${() => this._handleRowClick(row, rowIndex)} >${cells}</a> `; } return b ` <div class=${e$1({ 'row': true, 'row--clickable': !!this._model.rowClickable })} @mousedown=${() => this._handleRowMouseDown()} @click=${() => this._handleRowClick(row, rowIndex)} >${cells}</div> `; })} </div> </div> </div> `; } /** * Renders a data table with: * - Header bar with title, search input with view selector, and tools (pagination, refresh, column visibility, actions dropdown) * - Scrollable grid with sticky header row and optional sticky left/right columns * - Loading, error message, or empty state when no data */ render() { if (!this._model.columns.length) { return b `<slot></slot>`; } return b ` ${this._renderHeader()} ${this._renderTable()} ${this._renderFilterPanel()} `; } }"
576
+ "default": "class KRTable extends i$2 { constructor() { super(...arguments); /** * Internal flag to switch between scroll edge modes: * - 'overlay': Fixed padding with overlay elements that hide content at edges (scrollbar at viewport edge) * - 'edge': Padding scrolls with content, allowing table to reach edges when scrolling */ this._scrollStyle = 'overlay'; this._data = []; this._dataState = 'idle'; this._page = 1; this._pageSize = 50; this._totalItems = 0; this._totalPages = 0; this._searchQuery = ''; this._canScrollLeft = false; this._canScrollRight = false; this._canScrollHorizontal = false; this._columnPickerOpen = false; this._filterPanelOpened = null; this._filterPanelTab = 'filter'; this._buckets = new Map(); this._filterPanelPos = { top: 0, left: 0 }; this._sorts = []; this._resizing = null; this._resizeObserver = null; this._searchPositionLocked = false; this._model = new KRTableModel(); this.def = { columns: [] }; this._handleClickOutside = (e) => { const path = e.composedPath(); if (this._columnPickerOpen) { const picker = this.shadowRoot?.querySelector('.column-picker-wrapper'); if (picker && !path.includes(picker)) { this._columnPickerOpen = false; } } if (this._filterPanelOpened) { if (!path.some((el) => el.classList?.contains('filter-panel'))) { this._handleFilterApply(); } } }; this._handleResizeMove = (e) => { if (!this._resizing) return; const col = this._model.columns.find(c => c.id === this._resizing.columnId); if (col) { const newWidth = this._resizing.startWidth + (e.clientX - this._resizing.startX); col.width = `${Math.min(900, Math.max(50, newWidth))}px`; this.requestUpdate(); } }; this._handleResizeEnd = () => { this._resizing = null; document.removeEventListener('mousemove', this._handleResizeMove); document.removeEventListener('mouseup', this._handleResizeEnd); }; } connectedCallback() { super.connectedCallback(); this.classList.toggle('kr-table--scroll-overlay', this._scrollStyle === 'overlay'); this.classList.toggle('kr-table--scroll-edge', this._scrollStyle === 'edge'); this._fetch(); this._initRefresh(); document.addEventListener('click', this._handleClickOutside); this._resizeObserver = new ResizeObserver(() => { // Unlock and recalculate on resize since layout changes this._searchPositionLocked = false; this._updateSearchPosition(); }); this._resizeObserver.observe(this); } disconnectedCallback() { super.disconnectedCallback(); clearInterval(this._refreshTimer); document.removeEventListener('click', this._handleClickOutside); this._resizeObserver?.disconnect(); } willUpdate(changedProperties) { if (changedProperties.has('def')) { // Build internal model from user-provided def this._model = new KRTableModel(); if (this.def.title) { this._model.title = this.def.title; } if (this.def.actions) { this._model.actions = this.def.actions; } if (this.def.data) { this._model.data = this.def.data; } if (this.def.dataSource) { this._model.dataSource = this.def.dataSource; } if (typeof this.def.refreshInterval === 'number') { this._model.refreshInterval = this.def.refreshInterval; } if (typeof this.def.pageSize === 'number') { this._model.pageSize = this.def.pageSize; } if (this.def.rowClickable) { this._model.rowClickable = this.def.rowClickable; } if (this.def.rowHref) { this._model.rowHref = this.def.rowHref; } this._model.columns = this.def.columns.map(col => { const column = { ...col, filter: null }; if (!column.type) { column.type = 'string'; } if (column.type === 'actions') { column.label = col.label ?? ''; column.sticky = 'right'; column.resizable = false; return column; } if (col.filterable || col.facetable) { column.filter = new KRQuery(); column.filter.field = col.id; column.filter.type = column.type; if (col.filter) { column.filter.setOperator(col.filter.operator); column.filter.setValue(col.filter.value); } else if (col.facetable && !col.filterable) { column.filter.operator = 'in'; column.filter.value = []; } else if (column.filter.type === 'string') { column.filter.operator = 'contains'; } } return column; }); if (this.def.displayedColumns) { this._model.displayedColumns = this.def.displayedColumns; } else { this._model.displayedColumns = this._model.columns.map(c => c.id); } this._fetch(); this._initRefresh(); } } updated(changedProperties) { this._updateScrollFlags(); this._syncSlottedContent(); } /** Syncs light DOM content for cells with custom render functions */ _syncSlottedContent() { const columns = this.getDisplayedColumns().filter(col => col.render); if (!columns.length) return; // Clear old slotted content this.querySelectorAll('[slot^=\"cell-\"]').forEach(el => el.remove()); // Create new slotted content this._data.forEach((row, rowIndex) => { columns.forEach(col => { const result = col.render(row); if (!result) return; const el = document.createElement('span'); el.slot = `cell-${rowIndex}-${col.id}`; if (col.type === 'actions') { el.style.display = 'flex'; el.style.gap = '8px'; } if (typeof result === 'string') { el.innerHTML = result; } else { D(result, el); } this.appendChild(el); }); }); } // ---------------------------------------------------------------------------- // Public Interface // ---------------------------------------------------------------------------- refresh() { this._fetch(); } goToPrevPage() { if (this._page > 1) { this._page--; this._fetch(); } } goToNextPage() { if (this._page < this._totalPages) { this._page++; this._fetch(); } } goToPage(page) { if (page >= 1 && page <= this._totalPages) { this._page = page; this._fetch(); } } // ---------------------------------------------------------------------------- // Data Fetching // ---------------------------------------------------------------------------- _toSolrData() { const request = { page: this._page - 1, size: this._pageSize, sorts: this._sorts, filterFields: [], queryFields: [], facetFields: [] }; for (const col of this._model.columns) { if (!col.filter || col.filter.isEmpty() || !col.filter.isValid()) { continue; } const filterData = col.filter.toSolrData(); if (col.facetable && col.filter.operator === 'in') { filterData.tagged = true; } request.filterFields.push(filterData); } for (const col of this._model.columns) { if (!col.facetable) { continue; } request.facetFields.push({ name: col.id, type: 'FIELD', limit: 100, sort: 'count', minimumCount: 1 }); } if (this._searchQuery?.trim().length) { request.queryFields.push({ name: '_text_', operation: 'IS', value: termify(this._searchQuery, false) }); } return request; } _toDbParams() { const request = { page: this._page - 1, size: this._pageSize, sorts: this._sorts, filterFields: [], queryFields: [], facetFields: [] }; for (const col of this._model.columns) { if (!col.filter || col.filter.isEmpty() || !col.filter.isValid()) { continue; } request.filterFields.push(col.filter.toDbParams()); } if (this._searchQuery?.trim().length) { this._model.columns.filter(col => col.searchable).forEach(col => { request.queryFields.push({ name: col.id, operation: 'CONTAINS', value: this._searchQuery, and: false }); }); } return request; } /** * Fetches data from the API and updates the table. * Shows a loading spinner while fetching, then displays rows on success * or an error snackbar on failure. * Request/response format depends on dataSource.mode (solr, opensearch, db). */ _fetch() { if (this._model.data) { this._data = this._model.data; this._totalItems = this._model.data.length; this._totalPages = Math.ceil(this._model.data.length / this._pageSize); this._dataState = 'success'; return; } if (!this._model.dataSource) return; this._dataState = 'loading'; let request; if (this._model.dataSource.mode === 'db') { request = this._toDbParams(); } else { request = this._toSolrData(); } this._model.dataSource.fetch(request) .then(response => { // Parse response based on mode switch (this._model.dataSource?.mode) { case 'opensearch': { throw Error('Opensearch not supported yet'); } case 'db': { const res = response; this._data = res.data.content; this._totalItems = res.data.totalElements; this._totalPages = res.data.totalPages; this._pageSize = res.data.size; break; } default: { // solr const res = response; this._data = res.data.content; this._totalItems = res.data.totalElements; this._totalPages = res.data.totalPages; this._pageSize = res.data.size; this._parseFacetResults(res); } } this._dataState = 'success'; this._updateSearchPosition(); }) .catch(err => { this._dataState = 'error'; KRSnackbar.show({ message: err instanceof Error ? err.message : 'Failed to load data', type: 'error' }); }); } _parseFacetResults(response) { if (!response.data.facetFields) { return; } for (const col of this._model.columns) { if (!col.facetable) { continue; } const rawBuckets = response.data.facetFields[col.id]; if (!rawBuckets) { this._buckets.set(col.id, []); continue; } const buckets = []; for (const raw of rawBuckets) { // Solr returns boolean facet values as strings — coerce to actual booleans // so they match the filter values stored by toggle(). let val = raw.name; if (col.type === 'boolean' && typeof raw.name === 'string') { if (raw.name === 'true') { val = true; } else if (raw.name === 'false') { val = false; } } if (raw.name === null && raw.count > 0) { buckets.unshift({ val: null, count: raw.count }); } if (raw.name !== null) { buckets.push({ val: val, count: raw.count }); } } // Bucket sync: ensure selected values appear even with 0 results if (col.filter && col.filter.operator === 'in' && Array.isArray(col.filter.value)) { for (const selectedVal of col.filter.value) { if (!buckets.some(b => b.val === selectedVal)) { buckets.push({ val: selectedVal, count: 0 }); } } } this._buckets.set(col.id, buckets); } // Trigger re-render since Map mutation doesn't trigger Lit updates this._buckets = new Map(this._buckets); } /** * Sets up auto-refresh so the table automatically fetches fresh data * at a regular interval (useful for dashboards, monitoring views). * Configured via def.refreshInterval in milliseconds. */ _initRefresh() { clearInterval(this._refreshTimer); if (this._model.refreshInterval && this._model.refreshInterval > 0) { this._refreshTimer = window.setInterval(() => { this._fetch(); }, this._model.refreshInterval); } } _handleSearch(e) { const input = e.target; this._searchQuery = input.value; this._page = 1; this._fetch(); } _getGridTemplateColumns() { const cols = this.getDisplayedColumns(); return cols.map((col) => { // If column has explicit width, use it if (col.width) { return col.width; } // Actions columns: fit content without minimum if (col.type === 'actions') { return 'max-content'; } // No width specified - use content-based sizing with minimum return 'minmax(80px, auto)'; }).join(' '); } /** * Updates search position to be centered with equal gaps from title and tools. * On first call: resets to flex centering, measures position, then locks with fixed margin. * Subsequent calls are ignored unless _searchPositionLocked is reset (e.g., on resize). */ _updateSearchPosition() { // Skip if already locked (prevents shifts on pagination changes) if (this._searchPositionLocked) return; const search = this.shadowRoot?.querySelector('.search'); const searchField = search?.querySelector('.search-field'); if (!search || !searchField) return; // Reset to flex centering search.style.justifyContent = 'center'; searchField.style.marginLeft = ''; requestAnimationFrame(() => { const searchRect = search.getBoundingClientRect(); const fieldRect = searchField.getBoundingClientRect(); // Calculate how far from the left of search container the field currently is const currentOffset = fieldRect.left - searchRect.left; // Lock position: switch to flex-start and use fixed margin search.style.justifyContent = 'flex-start'; searchField.style.marginLeft = `${currentOffset}px`; // Mark as locked so pagination changes don't shift the search this._searchPositionLocked = true; }); } // ---------------------------------------------------------------------------- // Columns // ---------------------------------------------------------------------------- _toggleColumnPicker() { this._columnPickerOpen = !this._columnPickerOpen; } _toggleColumn(columnId) { if (this._model.displayedColumns.includes(columnId)) { this._model.displayedColumns = this._model.displayedColumns.filter(id => id !== columnId); } else { this._model.displayedColumns = [...this._model.displayedColumns, columnId]; } this.requestUpdate(); } // Clear any existing text selection on mousedown so we only detect // selections made during this click gesture, not stale selections from elsewhere _handleRowMouseDown() { if (!this._model.rowClickable) { return; } window.getSelection()?.removeAllRanges(); } _handleRowClick(row, rowIndex) { if (!this._model.rowClickable) { return; } const selection = window.getSelection(); if (selection && selection.toString().length > 0) { return; } this.dispatchEvent(new CustomEvent('row-click', { detail: { row, rowIndex }, bubbles: true, composed: true })); } // When a user toggles a column on via the column picker, it gets appended // to _displayedColumns. By mapping over _displayedColumns (not def.columns), // the new column appears at the right edge of the table instead of jumping // back to its original position in the column definition. // Actions columns are always moved to the end. getDisplayedColumns() { return this._model.displayedColumns .map(id => this._model.columns.find(col => col.id === id)) .sort((a, b) => { if (a.type === 'actions' && b.type !== 'actions') return 1; if (a.type !== 'actions' && b.type === 'actions') return -1; return 0; }); } // ---------------------------------------------------------------------------- // Scrolling // ---------------------------------------------------------------------------- /** * Scroll event handler that updates scroll flags in real-time as user scrolls. * Updates shadow indicators to show if more content exists left/right. */ _handleScroll(e) { const container = e.target; this._canScrollLeft = container.scrollLeft > 0; this._canScrollRight = container.scrollLeft < container.scrollWidth - container.clientWidth - 1; } /** * Updates scroll state flags for the table content container. * - _canScrollLeft: true if scrolled right (can scroll back left) * - _canScrollRight: true if more content exists to the right * - _canScrollHorizontal: true if content is wider than container * These flags control scroll shadow indicators and CSS classes. */ _updateScrollFlags() { const container = this.shadowRoot?.querySelector('.content'); if (container) { this._canScrollLeft = container.scrollLeft > 0; this._canScrollRight = container.scrollWidth > container.clientWidth && container.scrollLeft < container.scrollWidth - container.clientWidth - 1; this._canScrollHorizontal = container.scrollWidth > container.clientWidth; } this.classList.toggle('kr-table--scroll-left-available', this._canScrollLeft); this.classList.toggle('kr-table--scroll-right-available', this._canScrollRight); this.classList.toggle('kr-table--scroll-horizontal-available', this._canScrollHorizontal); this.classList.toggle('kr-table--sticky-left', this.getDisplayedColumns().some(c => c.sticky === 'left')); this.classList.toggle('kr-table--sticky-right', this.getDisplayedColumns().some(c => c.sticky === 'right')); } // ---------------------------------------------------------------------------- // Column Resizing // ---------------------------------------------------------------------------- _handleResizeStart(e, columnId) { e.preventDefault(); const headerCell = this.shadowRoot?.querySelector(`.header-cell[data-column-id=\"${columnId}\"]`); this._resizing = { columnId, startX: e.clientX, startWidth: headerCell?.offsetWidth || 200 }; document.addEventListener('mousemove', this._handleResizeMove); document.addEventListener('mouseup', this._handleResizeEnd); } // ---------------------------------------------------------------------------- // Sorting // ---------------------------------------------------------------------------- _handleSortClick(e, column) { if (e.shiftKey) { // Multi-sort: add or cycle existing const existingIndex = this._sorts.findIndex(s => s.sortBy === column.id); if (existingIndex === -1) { this._sorts.push({ sortBy: column.id, sortDirection: 'asc' }); } else { const existing = this._sorts[existingIndex]; if (existing.sortDirection === 'asc') { existing.sortDirection = 'desc'; } else { // on third click, remove sorting for the column this._sorts.splice(existingIndex, 1); } } this.requestUpdate(); } else { // Single sort: replace all let existing = null; if (this._sorts.length === 1) { existing = this._sorts.find(s => s.sortBy === column.id); } if (!existing) { this._sorts = [{ sortBy: column.id, sortDirection: 'asc' }]; } else if (existing.sortDirection === 'asc') { this._sorts = [{ sortBy: column.id, sortDirection: 'desc' }]; } else { this._sorts = []; } } this._page = 1; this._fetch(); } _renderSortIndicator(column) { if (!column.sortable) { return A; } const sortIndex = this._sorts.findIndex(s => s.sortBy === column.id); if (sortIndex === -1) { // Ghost arrow: visible only on hover via CSS return b ` <span class=\"header-cell__sort\" @click=${(e) => this._handleSortClick(e, column)}> <svg class=\"header-cell__sort-arrow header-cell__sort-arrow--ghost\" viewBox=\"0 0 24 24\" fill=\"currentColor\"> <path d=\"M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z\"/> </svg> </span> `; } let arrowStyle = {}; if (this._sorts[sortIndex].sortDirection === 'desc') { arrowStyle = { transform: 'rotate(180deg)' }; } return b ` <span class=\"header-cell__sort\" @click=${(e) => this._handleSortClick(e, column)}> <svg class=\"header-cell__sort-arrow\" viewBox=\"0 0 24 24\" fill=\"currentColor\" style=${o$1(arrowStyle)}> <path d=\"M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z\"/> </svg> ${this._sorts.length > 1 ? b ` <span class=\"header-cell__sort-priority\">${sortIndex + 1}</span> ` : A} </span> `; } // ---------------------------------------------------------------------------- // Header // ---------------------------------------------------------------------------- _handleAction(action) { if (action.href) { return; } this.dispatchEvent(new CustomEvent('action', { detail: { action: action.id }, bubbles: true, composed: true })); } // ---------------------------------------------------------------------------- // Filter Handlers // ---------------------------------------------------------------------------- _handleKqlChange(e, column) { const kql = e.target.value.trim(); if (!kql) { column.filter.clear(); this.requestUpdate(); } else { column.filter.setKql(kql); this.requestUpdate(); if (!column.filter.isValid()) { return; } } this._page = 1; this._fetch(); } _handleFilterPanelToggle(e, column) { e.stopPropagation(); if (this._filterPanelOpened === column.id) { this._filterPanelOpened = null; } else { const rect = e.currentTarget.getBoundingClientRect(); let left = rect.left; if (left + 328 > window.innerWidth) { left = window.innerWidth - 328; } this._filterPanelPos = { top: rect.bottom + 4, left }; this._filterPanelOpened = column.id; if (column.facetable) { this._filterPanelTab = 'counts'; } else { this._filterPanelTab = 'filter'; } } } _handleKqlClear(column) { column.filter.clear(); this._page = 1; this._fetch(); } _handleFilterClear() { const column = this._model.columns.find(c => c.id === this._filterPanelOpened); if (column) { column.filter.clear(); if (column.facetable && !column.filterable) { column.filter.operator = 'in'; column.filter.value = []; } } this._filterPanelOpened = null; this._page = 1; this._fetch(); } _handleFilterTextKeydown(e, column) { if (e.key === 'Enter') { e.preventDefault(); this._handleFilterApply(); } } _handleOperatorChange(e, column) { column.filter.setOperator(e.target.value); this.requestUpdate(); } _handleFilterStringChange(e, column) { column.filter.setValue(e.target.value); this.requestUpdate(); } _handleFilterNumberChange(e, column) { column.filter.setValue(Number(e.target.value)); this.requestUpdate(); } _handleFilterDateChange(e, column) { column.filter.setValue(new Date(e.target.value), 'day'); this.requestUpdate(); } _handleFilterBooleanChange(e, column) { column.filter.setValue(e.target.value === 'true'); this.requestUpdate(); } _handleFilterDateStartChange(e, column) { column.filter.setStart(new Date(e.target.value), 'day'); this.requestUpdate(); } _handleFilterDateEndChange(e, column) { column.filter.setEnd(new Date(e.target.value), 'day'); this.requestUpdate(); } _handleFilterNumberStartChange(e, column) { column.filter.setStart(Number(e.target.value)); this.requestUpdate(); } _handleFilterNumberEndChange(e, column) { column.filter.setEnd(Number(e.target.value)); this.requestUpdate(); } _handleFilterListChange(e, column) { const items = e.target.value.split(',').map((v) => v.trim()).filter((v) => v !== ''); if (column.type === 'number') { column.filter.setValue(items.map((v) => Number(v))); } else { column.filter.setValue(items); } this.requestUpdate(); } _handleFilterApply() { this._filterPanelOpened = null; this._page = 1; this._fetch(); } _handleFilterPanelTabChange(e) { this._filterPanelTab = e.detail.activeTabId; } _handleBucketToggle(e, column, bucket) { column.filter.toggle(bucket.val); this._page = 1; this._fetch(); } // ---------------------------------------------------------------------------- // Rendering // ---------------------------------------------------------------------------- _renderCellContent(column, row, rowIndex) { const value = row[column.id]; if (column.render) { // Use slot to project content from light DOM so external styles apply return b `<slot name=\"cell-${rowIndex}-${column.id}\"></slot>`; } if (value === null || value === undefined) { return ''; } switch (column.type) { case 'number': if (column.format === 'currency' && typeof value === 'number') { return value.toLocaleString('en-US', { style: 'currency', currency: 'USD' }); } return String(value); case 'date': { let date; if (value instanceof Date) { date = value; } else if (typeof value === 'string' && /^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}/.test(value)) { // MySQL datetime format (UTC): \"2026-01-28 01:33:44:517\" // Replace last colon before ms with dot, append Z for UTC const isoString = value.replace(/(\\d{2}:\\d{2}:\\d{2}):(\\d+)$/, '$1.$2').replace(' ', 'T') + 'Z'; date = new Date(isoString); } else { date = new Date(value); } // Show date and time for datetime values in UTC return date.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', timeZone: 'UTC' }); } case 'boolean': if (value === true) return 'Yes'; if (value === false) return 'No'; return ''; default: return String(value); } } /** * Returns CSS classes for a header cell based on column config. */ _getHeaderCellClasses(column, index) { return { 'header-cell': true, 'header-cell--sortable': !!column.sortable, 'header-cell--align-center': column.align === 'center', 'header-cell--align-right': column.align === 'right', 'header-cell--sticky-left': column.sticky === 'left', 'header-cell--sticky-left-last': column.sticky === 'left' && !this.getDisplayedColumns().slice(index + 1).some(c => c.sticky === 'left'), 'header-cell--sticky-right': column.sticky === 'right', 'header-cell--sticky-right-first': column.sticky === 'right' && !this.getDisplayedColumns().slice(0, index).some(c => c.sticky === 'right') }; } /** * Returns CSS classes for a table cell based on column config: * - Alignment (center, right) * - Sticky positioning (left, right) * - Border classes for the last left-sticky or first right-sticky column */ _getCellClasses(column, index) { return { 'cell': true, 'cell--actions': column.type === 'actions', 'cell--align-center': column.align === 'center', 'cell--align-right': column.align === 'right', 'cell--sticky-left': column.sticky === 'left', 'cell--sticky-left-last': column.sticky === 'left' && !this.getDisplayedColumns().slice(index + 1).some(c => c.sticky === 'left'), 'cell--sticky-right': column.sticky === 'right', 'cell--sticky-right-first': column.sticky === 'right' && !this.getDisplayedColumns().slice(0, index).some(c => c.sticky === 'right') }; } /** * Returns inline styles for a table cell: * - Width (from column config or default 150px) * - Min-width (if specified) * - Left/right offset for sticky columns (calculated from widths of preceding sticky columns) */ _getCellStyle(column, index) { const styles = {}; if (column.sticky === 'left') { let leftOffset = 0; for (let i = 0; i < index; i++) { const col = this.getDisplayedColumns()[i]; if (col.sticky === 'left') { leftOffset += parseInt(col.width || '0', 10); } } styles.left = `${leftOffset}px`; } if (column.sticky === 'right') { let rightOffset = 0; for (let i = index + 1; i < this.getDisplayedColumns().length; i++) { const col = this.getDisplayedColumns()[i]; if (col.sticky === 'right') { rightOffset += parseInt(col.width || '0', 10); } } styles.right = `${rightOffset}px`; } return styles; } /** * Renders the pagination controls: * - Previous page arrow (disabled on first page) * - Range text showing \"1-50 of 150\" format * - Next page arrow (disabled on last page) * * Hidden when there's no data or all data fits on one page. */ _renderPagination() { const start = (this._page - 1) * this._pageSize + 1; const end = Math.min(this._page * this._pageSize, this._totalItems); return b ` <div class=\"pagination\"> <span class=\"pagination-icon ${this._page === 1 ? 'pagination-icon--disabled' : ''}\" @click=${this.goToPrevPage} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z\"/></svg> </span> <span class=\"pagination-info\">${start}-${end} of ${this._totalItems}</span> <span class=\"pagination-icon ${this._page === this._totalPages ? 'pagination-icon--disabled' : ''}\" @click=${this.goToNextPage} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\"/></svg> </span> </div> `; } /** * Renders the header toolbar containing: * - Title (left) * - Search bar with view selector dropdown (center) * - Tools (right): page navigation, refresh button, column visibility picker, actions dropdown * * Hidden when there's no title, no actions, and data fits on one page. */ _renderHeader() { if (!this._model.title && !this._model.actions?.length && this._totalPages <= 1) { return A; } return b ` <div class=\"header\"> <div class=\"title\">${this._model.title ?? ''}</div> ${this._model.dataSource?.mode === 'db' && !this._model.columns.some(col => col.searchable) ? b `<div class=\"search\"></div>` : b ` <div class=\"search\"> <!-- TODO: Saved views dropdown <div class=\"views\"> <span>Default View</span> <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z\"/></svg> </div> --> <div class=\"search-field\"> <svg class=\"search-icon\" viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z\"/></svg> <input type=\"text\" class=\"search-input\" placeholder=\"Search...\" .value=${this._searchQuery} @input=${this._handleSearch} /> </div> </div> `} <div class=\"tools\"> ${this._renderPagination()} <span class=\"refresh\" title=\"Refresh\" @click=${() => this.refresh()}> <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M480-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100 70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z\"/></svg> </span> <div class=\"column-picker-wrapper\"> <span class=\"header-icon\" title=\"Columns\" @click=${this._toggleColumnPicker}> <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M121-280v-400q0-33 23.5-56.5T201-760h559q33 0 56.5 23.5T840-680v400q0 33-23.5 56.5T760-200H201q-33 0-56.5-23.5T121-280Zm79 0h133v-400H200v400Zm213 0h133v-400H413v400Zm213 0h133v-400H626v400Z\"/></svg> </span> <div class=\"column-picker ${this._columnPickerOpen ? 'open' : ''}\"> ${[...this._model.columns].filter(col => col.type !== 'actions').sort((a, b) => (a.label ?? a.id).localeCompare(b.label ?? b.id)).map(col => b ` <div class=\"column-picker-item\" @click=${() => this._toggleColumn(col.id)}> <div class=\"column-picker-checkbox ${this._model.displayedColumns.includes(col.id) ? 'checked' : ''}\"> <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z\"/></svg> </div> <span class=\"column-picker-label\">${col.label ?? col.id}</span> </div> `)} </div> </div> ${this._model.actions?.length === 1 ? b ` <kr-button class=\"actions\" .href=${this._model.actions[0].href} .target=${this._model.actions[0].target} @click=${() => this._handleAction(this._model.actions[0])} > ${this._model.actions[0].label} </kr-button> ` : this._model.actions?.length ? b ` <kr-button class=\"actions\" .options=${this._model.actions.map(a => ({ id: a.id, label: a.label }))} @option-select=${(e) => this._handleAction({ id: e.detail.id, label: e.detail.label })} > Actions </kr-button> ` : A} </div> </div> `; } /** Renders status message (loading, error, empty) */ _renderStatus() { if (this._dataState === 'loading' && this._data.length === 0) { return b `<div class=\"status\">Loading...</div>`; } if (this._dataState === 'error' && this._data.length === 0) { return b `<div class=\"status status--error\">Error loading data</div>`; } if (this._data.length === 0) { return b `<div class=\"status\">No data available</div>`; } return A; } _renderFilterPanel() { if (!this._filterPanelOpened) { return A; } const column = this._model.columns.find(c => c.id === this._filterPanelOpened); // Build filter content (operator + value input) let valueInput = b ``; if (column.filter.operator === 'empty' || column.filter.operator === 'n_empty') { valueInput = b ` <input type=\"text\" class=\"filter-panel__input\" disabled .value=${column.filter.text} /> `; } else if (column.filter.operator === 'between' && column.type === 'date') { valueInput = b ` <input type=\"date\" class=\"filter-panel__input\" .valueAsDate=${column.filter.value?.start ?? null} @change=${(e) => this._handleFilterDateStartChange(e, column)} /> <input type=\"date\" class=\"filter-panel__input\" .valueAsDate=${column.filter.value?.end ?? null} @change=${(e) => this._handleFilterDateEndChange(e, column)} /> `; } else if (column.filter.operator === 'between' && column.type === 'number') { valueInput = b ` <input type=\"number\" class=\"filter-panel__input\" placeholder=\"Start\" .value=${column.filter.value?.start ?? ''} @input=${(e) => this._handleFilterNumberStartChange(e, column)} @keydown=${(e) => this._handleFilterTextKeydown(e, column)} /> <input type=\"number\" class=\"filter-panel__input\" placeholder=\"End\" .value=${column.filter.value?.end ?? ''} @input=${(e) => this._handleFilterNumberEndChange(e, column)} @keydown=${(e) => this._handleFilterTextKeydown(e, column)} /> `; } else if (column.filter.operator === 'in') { valueInput = b ` <textarea class=\"filter-panel__textarea\" rows=\"3\" placeholder=\"Values (comma-separated)\" .value=${column.filter.text} @input=${(e) => this._handleFilterListChange(e, column)} @keydown=${(e) => this._handleFilterTextKeydown(e, column)} ></textarea> `; } else if (column.type === 'boolean') { valueInput = b ` <kr-select-field placeholder=\"Value\" .value=${String(column.filter.value ?? '')} @change=${(e) => this._handleFilterBooleanChange(e, column)} > <kr-select-option value=\"true\">Yes</kr-select-option> <kr-select-option value=\"false\">No</kr-select-option> </kr-select-field> `; } else if (column.type === 'date') { valueInput = b ` <input type=\"date\" class=\"filter-panel__input\" .valueAsDate=${column.filter.value} @change=${(e) => this._handleFilterDateChange(e, column)} /> `; } else if (column.type === 'number') { valueInput = b ` <input type=\"number\" class=\"filter-panel__input\" placeholder=\"Value\" min=\"0\" .value=${column.filter.text} @input=${(e) => this._handleFilterNumberChange(e, column)} @keydown=${(e) => this._handleFilterTextKeydown(e, column)} /> `; } else { valueInput = b ` <input type=\"text\" class=\"filter-panel__input\" placeholder=\"Value\" .value=${column.filter.text} @input=${(e) => this._handleFilterStringChange(e, column)} @keydown=${(e) => this._handleFilterTextKeydown(e, column)} /> `; } const filterContent = b ` <div class=\"filter-panel__content\"> <kr-select-field .value=${column.filter.operator} @change=${(e) => this._handleOperatorChange(e, column)} > ${getOperatorsForType(column.type).map(op => b ` <kr-select-option value=${op.key}>${op.label}</kr-select-option> `)} </kr-select-field> ${valueInput} </div> `; // Build bucket list content const buckets = this._buckets.get(column.id) || []; let bucketContent; if (!buckets.length) { bucketContent = b `<div class=\"bucket-empty\">No data</div>`; } else { bucketContent = b ` <div class=\"buckets\"> ${buckets.map(bucket => { let bucketLabel = '(Empty)'; if (bucket.val !== null && bucket.val !== undefined) { if (column.type === 'boolean') { if (bucket.val === true || bucket.val === 'true') { bucketLabel = 'Yes'; } else { bucketLabel = 'No'; } } else { bucketLabel = String(bucket.val); } } let checkIcon = A; if (column.filter.has(bucket.val)) { checkIcon = b ` <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"> <path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z\"/> </svg> `; } return b ` <div class=\"bucket\" @click=${(e) => this._handleBucketToggle(e, column, bucket)} > <div class=${e$1({ 'bucket__checkbox': true, 'bucket__checkbox--checked': column.filter.has(bucket.val) })}> ${checkIcon} </div> <span class=\"bucket__label\">${bucketLabel}</span> <span class=\"bucket__count\">${bucket.count}</span> </div> `; })} </div> `; } // Build panel body — tabs if both filterable+facetable, otherwise just the relevant content let panelBody; if (column.facetable && column.filterable) { panelBody = b ` <kr-tab-group size=\"small\" active-tab-id=${this._filterPanelTab} @tab-change=${(e) => this._handleFilterPanelTabChange(e)} > <kr-tab id=\"filter\" label=\"Filter\"> ${filterContent} </kr-tab> <kr-tab id=\"counts\" label=\"Counts\"> ${bucketContent} </kr-tab> </kr-tab-group> `; } else if (column.facetable) { panelBody = bucketContent; } else { panelBody = filterContent; } return b ` <div class=\"filter-panel\" style=${o$1({ top: this._filterPanelPos.top + 'px', left: this._filterPanelPos.left + 'px' })} > ${panelBody} <div class=\"filter-panel__actions\"> <kr-button variant=\"outline\" color=\"secondary\" size=\"small\" @click=${this._handleFilterClear}> Clear </kr-button> <kr-button size=\"small\" @click=${this._handleFilterApply}> Apply </kr-button> </div> </div> `; } /** * Renders filter row below column headers. * Only displays for columns with filterable: true. */ _renderFilterRow() { const columns = this.getDisplayedColumns(); if (!columns.some(col => col.filterable || col.facetable)) { return A; } return b ` <div class=\"filter-row\"> ${columns.map((col, i) => { if (!col.filterable && !col.facetable) { return b `<div class=${e$1({ 'filter-cell': true, 'filter-cell--sticky-left': col.sticky === 'left', 'filter-cell--sticky-right': col.sticky === 'right', 'filter-cell--sticky-right-first': col.sticky === 'right' && !columns.slice(0, i).some((c) => c.sticky === 'right') })} style=${o$1(this._getCellStyle(col, i))} ></div>`; } return b ` <div class=${e$1({ 'filter-cell': true, 'filter-cell--sticky-left': col.sticky === 'left', 'filter-cell--sticky-right': col.sticky === 'right', 'filter-cell--sticky-right-first': col.sticky === 'right' && !columns.slice(0, i).some((c) => c.sticky === 'right') })} style=${o$1(this._getCellStyle(col, i))} > <div class=\"filter-cell__wrapper\"> <input type=\"text\" class=${e$1({ 'filter-cell__input': true, 'filter-cell__input--invalid': !col.filter.isValid() })} .value=${col.filter.kql} @change=${(e) => this._handleKqlChange(e, col)} /> ${col.filter?.kql?.length > 0 ? b ` <button class=\"filter-cell__clear\" @click=${() => this._handleKqlClear(col)} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"> <path d=\"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z\"/> </svg> </button> ` : A} <button class=${e$1({ 'filter-cell__advanced': true, 'filter-cell__advanced--opened': this._filterPanelOpened === col.id })} @click=${(e) => this._handleFilterPanelToggle(e, col)} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"> <path d=\"M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z\"/> </svg> </button> </div> </div> `; })} </div> `; } /** Renders the scrollable data grid with column headers and rows. */ _renderTable() { return b ` <div class=\"wrapper\"> <div class=\"overlay-left\"></div> <div class=\"overlay-right\"></div> ${this._renderStatus()} <div class=\"content\" @scroll=${this._handleScroll}> <div class=\"table\" style=\"grid-template-columns: ${this._getGridTemplateColumns()}\"> <div class=\"header-row\"> ${this.getDisplayedColumns().map((col, i) => b ` <div class=${e$1(this._getHeaderCellClasses(col, i))} style=${o$1(this._getCellStyle(col, i))} data-column-id=${col.id} > <span class=\"header-cell__label\">${col.label ?? col.id}</span> ${this._renderSortIndicator(col)} ${col.resizable !== false ? b `<div class=\"header-cell__resize\" @mousedown=${(e) => this._handleResizeStart(e, col.id)} ></div>` : A} </div> `)} </div> ${this._renderFilterRow()} ${this._data.map((row, rowIndex) => { const cells = this.getDisplayedColumns().map((col, i) => b ` <div class=${e$1(this._getCellClasses(col, i))} style=${o$1(this._getCellStyle(col, i))} data-column-id=${col.id} > ${this._renderCellContent(col, row, rowIndex)} </div> `); if (this._model.rowHref) { return b ` <a href=${this._model.rowHref(row)} class=${e$1({ 'row': true, 'row--clickable': true, 'row--link': true })} @mousedown=${() => this._handleRowMouseDown()} @click=${() => this._handleRowClick(row, rowIndex)} >${cells}</a> `; } return b ` <div class=${e$1({ 'row': true, 'row--clickable': !!this._model.rowClickable })} @mousedown=${() => this._handleRowMouseDown()} @click=${() => this._handleRowClick(row, rowIndex)} >${cells}</div> `; })} </div> </div> </div> `; } /** * Renders a data table with: * - Header bar with title, search input with view selector, and tools (pagination, refresh, column visibility, actions dropdown) * - Scrollable grid with sticky header row and optional sticky left/right columns * - Loading, error message, or empty state when no data */ render() { if (!this._model.columns.length) { return b `<slot></slot>`; } return b ` ${this._renderHeader()} ${this._renderTable()} ${this._renderFilterPanel()} `; } }"
577
577
  },
578
578
  {
579
579
  "kind": "variable",
@@ -866,12 +866,12 @@
866
866
  {
867
867
  "kind": "variable",
868
868
  "name": "Ce",
869
- "default": "class extends le{constructor(){super(...arguments),this.type=\"info\",this.title=\"\",this.dismissible=!1,this.visible=!0}_handleDismiss(){this.visible=!1,this.dispatchEvent(new CustomEvent(\"dismiss\",{bubbles:!0,composed:!0}))}render(){const e={info:V`<svg class=\"icon\" viewBox=\"0 0 20 20\" fill=\"currentColor\"><path fill-rule=\"evenodd\" d=\"M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z\" clip-rule=\"evenodd\"/></svg>`,success:V`<svg class=\"icon\" viewBox=\"0 0 20 20\" fill=\"currentColor\"><path fill-rule=\"evenodd\" d=\"M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z\" clip-rule=\"evenodd\"/></svg>`,warning:V`<svg class=\"icon\" viewBox=\"0 0 20 20\" fill=\"currentColor\"><path fill-rule=\"evenodd\" d=\"M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z\" clip-rule=\"evenodd\"/></svg>`,error:V`<svg class=\"icon\" viewBox=\"0 0 20 20\" fill=\"currentColor\"><path fill-rule=\"evenodd\" d=\"M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z\" clip-rule=\"evenodd\"/></svg>`};return V` <div class=${we({alert:!0,[\"alert--\"+this.type]:!0,\"alert--has-header\":!!this.title,\"alert--hidden\":!this.visible})} role=\"alert\" > ${e[this.type]} <div class=\"content\"> ${this.title?V`<h4 class=\"header\">${this.title}</h4>`:B} <div class=\"message\"> <slot></slot> </div> </div> ${this.dismissible?V` <button class=\"dismiss\" type=\"button\" aria-label=\"Dismiss alert\" @click=${this._handleDismiss} > <svg viewBox=\"0 0 20 20\" fill=\"currentColor\" width=\"16\" height=\"16\"> <path fill-rule=\"evenodd\" d=\"M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z\" clip-rule=\"evenodd\"/> </svg> </button> `:B} </div> `}}"
869
+ "default": "class extends le{constructor(){super(...arguments),this.type=\"info\",this.title=\"\",this.dismissible=!1,this.visible=!0}_handleDismiss(){this.visible=!1,this.dispatchEvent(new CustomEvent(\"dismiss\",{bubbles:!0,composed:!0}))}render(){const e={info:V`<svg class=\"icon\" viewBox=\"0 0 20 20\" fill=\"currentColor\"><path fill-rule=\"evenodd\" d=\"M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z\" clip-rule=\"evenodd\"/></svg>`,success:V`<svg class=\"icon\" viewBox=\"0 0 20 20\" fill=\"currentColor\"><path fill-rule=\"evenodd\" d=\"M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z\" clip-rule=\"evenodd\"/></svg>`,warning:V`<svg class=\"icon\" viewBox=\"0 0 20 20\" fill=\"currentColor\"><path fill-rule=\"evenodd\" d=\"M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z\" clip-rule=\"evenodd\"/></svg>`,error:V`<svg class=\"icon\" viewBox=\"0 0 20 20\" fill=\"currentColor\"><path fill-rule=\"evenodd\" d=\"M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z\" clip-rule=\"evenodd\"/></svg>`};return V` <div class=${we({alert:!0,[\"alert--\"+this.type]:!0,\"alert--has-header\":!!this.title,\"alert--hidden\":!this.visible})} role=\"alert\" > ${e[this.type]} <div class=\"content\"> ${this.title?V`<h4 class=\"header\">${this.title}</h4>`:U} <div class=\"message\"> <slot></slot> </div> </div> ${this.dismissible?V` <button class=\"dismiss\" type=\"button\" aria-label=\"Dismiss alert\" @click=${this._handleDismiss} > <svg viewBox=\"0 0 20 20\" fill=\"currentColor\" width=\"16\" height=\"16\"> <path fill-rule=\"evenodd\" d=\"M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z\" clip-rule=\"evenodd\"/> </svg> </button> `:U} </div> `}}"
870
870
  },
871
871
  {
872
872
  "kind": "variable",
873
873
  "name": "Ee",
874
- "default": "class extends le{constructor(){super(...arguments),this.variant=\"flat\",this.color=\"primary\",this.size=\"medium\",this.disabled=!1,this.options=[],this.iconPosition=\"left\",this._state=\"idle\",this._stateText=\"\",this._dropdownOpened=!1,this._handleHostClick=e=>{this.options.length&&(e.stopPropagation(),this._toggleDropdown())},this._handleKeydown=e=>{\"Enter\"!==e.key&&\" \"!==e.key||(e.preventDefault(),this.options.length?this._toggleDropdown():this.click()),\"Escape\"===e.key&&this._dropdownOpened&&(this._dropdownOpened=!1)},this._handleClickOutside=e=>{this._dropdownOpened&&!this.contains(e.target)&&(this._dropdownOpened=!1)}}connectedCallback(){super.connectedCallback(),this.setAttribute(\"role\",this.href?\"link\":\"button\"),this.setAttribute(\"tabindex\",\"0\"),this.addEventListener(\"keydown\",this._handleKeydown),this.addEventListener(\"click\",this._handleHostClick),document.addEventListener(\"click\",this._handleClickOutside)}disconnectedCallback(){super.disconnectedCallback(),this.removeEventListener(\"keydown\",this._handleKeydown),this.removeEventListener(\"click\",this._handleHostClick),document.removeEventListener(\"click\",this._handleClickOutside)}_toggleDropdown(){this._dropdownOpened=!this._dropdownOpened,this._dropdownOpened&&requestAnimationFrame((()=>{const e=this.shadowRoot?.querySelector(\".dropdown\");if(e){const t=this.getBoundingClientRect();e.style.top=t.bottom+4+\"px\",e.style.bottom=\"\",e.style.left=t.left+\"px\",e.style.right=\"\",e.style.minWidth=t.width+\"px\",e.style.transformOrigin=\"top left\";const i=e.getBoundingClientRect();i.bottom>window.innerHeight?(e.style.top=\"\",e.style.bottom=window.innerHeight-t.top+4+\"px\",e.style.transformOrigin=\"bottom left\",e.classList.add(\"dropdown--above\")):e.classList.remove(\"dropdown--above\"),i.right>window.innerWidth&&(e.style.left=\"\",e.style.right=window.innerWidth-t.right+\"px\",i.bottom>window.innerHeight?e.style.transformOrigin=\"bottom right\":e.style.transformOrigin=\"top right\")}}))}_handleOptionClick(e,t){t.stopPropagation(),this._dropdownOpened=!1,this.dispatchEvent(new CustomEvent(\"option-select\",{detail:{id:e.id,label:e.label},bubbles:!0,composed:!0}))}showLoading(){this._clearStateTimeout(),this._state=\"loading\",this._stateText=\"\"}showSuccess(e=\"Success\",t=2e3){this._clearStateTimeout(),this._state=\"success\",this._stateText=e,t>0&&(this._stateTimeout=window.setTimeout((()=>this.reset()),t))}showError(e=\"Error\",t=2e3){this._clearStateTimeout(),this._state=\"error\",this._stateText=e,t>0&&(this._stateTimeout=window.setTimeout((()=>this.reset()),t))}isLoading(){return\"loading\"===this._state}reset(){this._clearStateTimeout(),this._state=\"idle\",this._stateText=\"\"}_clearStateTimeout(){this._stateTimeout&&(clearTimeout(this._stateTimeout),this._stateTimeout=void 0)}updated(e){this.classList.toggle(\"kr-button--loading\",\"loading\"===this._state),this.classList.toggle(\"kr-button--success\",\"success\"===this._state),this.classList.toggle(\"kr-button--error\",\"error\"===this._state),this.classList.toggle(`kr-button--${this.variant}`,!0),this.classList.toggle(`kr-button--${this.color}`,!0),this.classList.toggle(\"kr-button--small\",\"small\"===this.size),this.classList.toggle(\"kr-button--large\",\"large\"===this.size)}render(){const e=V` <span class=\"content\"> <slot name=\"icon\"></slot> <slot></slot> </span> ${this.options.length?V`<svg class=\"caret\" xmlns=\"http://www.w3.org/2000/svg\" height=\"20\" width=\"20\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z\"/></svg>`:B} ${\"idle\"!==this._state?V`<span class=\"state-overlay\"> ${\"loading\"===this._state?V`<span class=\"spinner\"></span>`:this._stateText} </span>`:B} ${this.options.length?V` <div class=\"dropdown ${this._dropdownOpened?\"dropdown--opened\":\"\"}\"> ${this.options.map((e=>V` <button class=\"dropdown-item\" @click=${t=>this._handleOptionClick(e,t)} >${e.label}</button> `))} </div> `:B} `;return this.href?V`<a class=\"link\" href=${this.href} target=${this.target||B}>${e}</a>`:e}}"
874
+ "default": "class extends le{constructor(){super(...arguments),this.variant=\"flat\",this.color=\"primary\",this.size=\"medium\",this.disabled=!1,this.options=[],this.iconPosition=\"left\",this._state=\"idle\",this._stateText=\"\",this._dropdownOpened=!1,this._handleHostClick=e=>{this.options.length&&(e.stopPropagation(),this._toggleDropdown())},this._handleKeydown=e=>{\"Enter\"!==e.key&&\" \"!==e.key||(e.preventDefault(),this.options.length?this._toggleDropdown():this.click()),\"Escape\"===e.key&&this._dropdownOpened&&(this._dropdownOpened=!1)},this._handleClickOutside=e=>{this._dropdownOpened&&!this.contains(e.target)&&(this._dropdownOpened=!1)}}connectedCallback(){super.connectedCallback(),this.setAttribute(\"role\",this.href?\"link\":\"button\"),this.setAttribute(\"tabindex\",\"0\"),this.addEventListener(\"keydown\",this._handleKeydown),this.addEventListener(\"click\",this._handleHostClick),document.addEventListener(\"click\",this._handleClickOutside)}disconnectedCallback(){super.disconnectedCallback(),this.removeEventListener(\"keydown\",this._handleKeydown),this.removeEventListener(\"click\",this._handleHostClick),document.removeEventListener(\"click\",this._handleClickOutside)}_toggleDropdown(){this._dropdownOpened=!this._dropdownOpened,this._dropdownOpened&&requestAnimationFrame((()=>{const e=this.shadowRoot?.querySelector(\".dropdown\");if(e){const t=this.getBoundingClientRect();e.style.top=t.bottom+4+\"px\",e.style.bottom=\"\",e.style.left=t.left+\"px\",e.style.right=\"\",e.style.minWidth=t.width+\"px\",e.style.transformOrigin=\"top left\";const i=e.getBoundingClientRect();i.bottom>window.innerHeight?(e.style.top=\"\",e.style.bottom=window.innerHeight-t.top+4+\"px\",e.style.transformOrigin=\"bottom left\",e.classList.add(\"dropdown--above\")):e.classList.remove(\"dropdown--above\"),i.right>window.innerWidth&&(e.style.left=\"\",e.style.right=window.innerWidth-t.right+\"px\",i.bottom>window.innerHeight?e.style.transformOrigin=\"bottom right\":e.style.transformOrigin=\"top right\")}}))}_handleOptionClick(e,t){t.stopPropagation(),this._dropdownOpened=!1,this.dispatchEvent(new CustomEvent(\"option-select\",{detail:{id:e.id,label:e.label},bubbles:!0,composed:!0}))}showLoading(){this._clearStateTimeout(),this._state=\"loading\",this._stateText=\"\"}showSuccess(e=\"Success\",t=2e3){this._clearStateTimeout(),this._state=\"success\",this._stateText=e,t>0&&(this._stateTimeout=window.setTimeout((()=>this.reset()),t))}showError(e=\"Error\",t=2e3){this._clearStateTimeout(),this._state=\"error\",this._stateText=e,t>0&&(this._stateTimeout=window.setTimeout((()=>this.reset()),t))}isLoading(){return\"loading\"===this._state}reset(){this._clearStateTimeout(),this._state=\"idle\",this._stateText=\"\"}_clearStateTimeout(){this._stateTimeout&&(clearTimeout(this._stateTimeout),this._stateTimeout=void 0)}updated(e){this.classList.toggle(\"kr-button--loading\",\"loading\"===this._state),this.classList.toggle(\"kr-button--success\",\"success\"===this._state),this.classList.toggle(\"kr-button--error\",\"error\"===this._state),this.classList.toggle(`kr-button--${this.variant}`,!0),this.classList.toggle(`kr-button--${this.color}`,!0),this.classList.toggle(\"kr-button--small\",\"small\"===this.size),this.classList.toggle(\"kr-button--large\",\"large\"===this.size)}render(){const e=V` <span class=\"content\"> <slot name=\"icon\"></slot> <slot></slot> </span> ${this.options.length?V`<svg class=\"caret\" xmlns=\"http://www.w3.org/2000/svg\" height=\"20\" width=\"20\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z\"/></svg>`:U} ${\"idle\"!==this._state?V`<span class=\"state-overlay\"> ${\"loading\"===this._state?V`<span class=\"spinner\"></span>`:this._stateText} </span>`:U} ${this.options.length?V` <div class=\"dropdown ${this._dropdownOpened?\"dropdown--opened\":\"\"}\"> ${this.options.map((e=>V` <button class=\"dropdown-item\" @click=${t=>this._handleOptionClick(e,t)} >${e.label}</button> `))} </div> `:U} `;return this.href?V`<a class=\"link\" href=${this.href} target=${this.target||U}>${e}</a>`:e}}"
875
875
  },
876
876
  {
877
877
  "kind": "variable",
@@ -935,8 +935,8 @@
935
935
  },
936
936
  {
937
937
  "kind": "variable",
938
- "name": "Ue",
939
- "default": "class extends le{constructor(){super(...arguments),this._dialogRef=null,this._contentElement=null,this.opened=!1,this.label=\"\",this.width=\"560px\",this._handleDocumentKeyDown=e=>{\"Escape\"===e.key&&this.close()}}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener(\"keydown\",this._handleDocumentKeyDown)}updated(e){super.updated(e),e.has(\"opened\")&&(this.opened?document.addEventListener(\"keydown\",this._handleDocumentKeyDown):document.removeEventListener(\"keydown\",this._handleDocumentKeyDown))}open(){this.opened=!0}close(){this._dialogRef?this._dialogRef.close(void 0):(this.opened=!1,this.dispatchEvent(new CustomEvent(\"close\",{bubbles:!0,composed:!0})))}static open(e,t){document.querySelectorAll(\"kr-dialog\").forEach((e=>{e._dialogRef&&e.remove()}));const i=new Ve,s=document.createElement(\"kr-dialog\");i.setDialogElement(s),s._dialogRef=i;const o=new e;return o.dialogRef=i,t?.data&&(o.data=t.data),t?.label&&(s.label=t.label),t?.width&&(s.width=t.width),s._contentElement=o,s.opened=!0,document.body.appendChild(s),i}_handleBackdropClick(e){e.target.classList.contains(\"backdrop\")&&this.close()}render(){return V` <div class=\"backdrop\" @click=${this._handleBackdropClick}></div> <div class=\"dialog\" style=${Le({width:this.width})}> ${this.label?V`<div class=\"dialog__header\"><div class=\"dialog__header-label\">${this.label}</div></div>`:\"\"} ${this._contentElement?this._contentElement:V`<slot></slot>`} </div> `}}"
938
+ "name": "Be",
939
+ "default": "class extends le{constructor(){super(...arguments),this._dialogRef=null,this._contentElement=null,this.opened=!1,this.label=\"\",this.width=\"560px\",this._handleDocumentKeyDown=e=>{\"Escape\"===e.key&&this.close()}}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener(\"keydown\",this._handleDocumentKeyDown)}updated(e){super.updated(e),e.has(\"opened\")&&(this.opened?document.addEventListener(\"keydown\",this._handleDocumentKeyDown):document.removeEventListener(\"keydown\",this._handleDocumentKeyDown))}open(){this.opened=!0}close(){this._dialogRef?this._dialogRef.close(void 0):(this.opened=!1,this.dispatchEvent(new CustomEvent(\"close\",{bubbles:!0,composed:!0})))}static open(e,t){document.querySelectorAll(\"kr-dialog\").forEach((e=>{e._dialogRef&&e.remove()}));const i=new Ve,s=document.createElement(\"kr-dialog\");i.setDialogElement(s),s._dialogRef=i;const o=new e;return o.dialogRef=i,t?.data&&(o.data=t.data),t?.label&&(s.label=t.label),t?.width&&(s.width=t.width),s._contentElement=o,s.opened=!0,document.body.appendChild(s),i}_handleBackdropClick(e){e.target.classList.contains(\"backdrop\")&&this.close()}render(){return V` <div class=\"backdrop\" @click=${this._handleBackdropClick}></div> <div class=\"dialog\" style=${qe({width:this.width})}> ${this.label?V`<div class=\"dialog__header\"><div class=\"dialog__header-label\">${this.label}</div></div>`:\"\"} ${this._contentElement?this._contentElement:V`<slot></slot>`} </div> `}}"
940
940
  },
941
941
  {
942
942
  "kind": "variable",
@@ -945,7 +945,7 @@
945
945
  {
946
946
  "kind": "variable",
947
947
  "name": "We",
948
- "default": "class extends le{constructor(){super(...arguments),this.justified=!1,this.size=\"medium\"}updated(e){e.has(\"activeTabId\")&&this._updateActiveTab()}firstUpdated(){this._updateActiveTab()}_getTabs(){return Array.from(this.querySelectorAll(\"kr-tab\"))}_updateActiveTab(){const e=this._getTabs();0!==e.length&&(this.activeTabId||(this.activeTabId=e[0]?.id),e.forEach((e=>{e.active=e.id===this.activeTabId})),this.requestUpdate())}_handleTabClick(e){e.disabled||(this.activeTabId=e.id,this.dispatchEvent(new CustomEvent(\"tab-change\",{detail:{activeTabId:e.id},bubbles:!0,composed:!0})))}_handleTabDismiss(e,t){t.stopPropagation(),this.dispatchEvent(new CustomEvent(\"tab-dismiss\",{detail:{tabId:e.id},bubbles:!0,composed:!0}))}_handleKeyDown(e){const t=this._getTabs().filter((e=>!e.disabled)),i=t.findIndex((e=>e.id===this.activeTabId));let s=-1;switch(e.key){case\"ArrowLeft\":s=i>0?i-1:t.length-1;break;case\"ArrowRight\":s=i<t.length-1?i+1:0;break;case\"Home\":s=0;break;case\"End\":s=t.length-1}if(s>=0){e.preventDefault();const i=t[s];this.activeTabId=i.id,this.dispatchEvent(new CustomEvent(\"tab-change\",{detail:{activeTabId:i.id},bubbles:!0,composed:!0}));const o=this.shadowRoot?.querySelectorAll(\".label\"),r=Array.from(o||[]).find((e=>e.getAttribute(\"data-tab-id\")===i.id));r?.focus()}}_renderTabIcon(e){const t=e.getIconElement();if(!t)return B;const i=t.cloneNode(!0);return i.removeAttribute(\"slot\"),V`<span class=\"label-icon\">${i}</span>`}render(){return V` <div class=\"header\" role=\"tablist\" @keydown=${this._handleKeyDown}> ${this._getTabs().map((e=>V` <button class=${we({label:!0,\"label--active\":e.id===this.activeTabId,\"label--justified\":this.justified})} role=\"tab\" data-tab-id=${e.id} aria-selected=${e.id===this.activeTabId} aria-controls=${`panel-${e.id}`} tabindex=${e.id===this.activeTabId?0:-1} ?disabled=${e.disabled} @click=${()=>this._handleTabClick(e)} > ${this._renderTabIcon(e)} <span>${e.label}</span> ${e.badge?V`<span class=\"label-badge\" style=${Le({backgroundColor:e.badgeBackground,color:e.badgeColor})}>${e.badge}</span>`:B} ${e.dismissible?V` <button class=\"label-dismiss\" type=\"button\" aria-label=\"Close tab\" @click=${t=>this._handleTabDismiss(e,t)} > <svg viewBox=\"0 0 20 20\" fill=\"currentColor\" width=\"12\" height=\"12\"><path fill-rule=\"evenodd\" d=\"M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z\" clip-rule=\"evenodd\"/></svg> </button> `:B} </button> `))} </div> <div class=\"content\" role=\"tabpanel\" aria-labelledby=${this.activeTabId||\"\"}> <slot @slotchange=${this._updateActiveTab}></slot> </div> `}}"
948
+ "default": "class extends le{constructor(){super(...arguments),this.justified=!1,this.size=\"medium\"}updated(e){e.has(\"activeTabId\")&&this._updateActiveTab()}firstUpdated(){this._updateActiveTab()}_getTabs(){return Array.from(this.querySelectorAll(\"kr-tab\"))}_updateActiveTab(){const e=this._getTabs();0!==e.length&&(this.activeTabId||(this.activeTabId=e[0]?.id),e.forEach((e=>{e.active=e.id===this.activeTabId})),this.requestUpdate())}_handleTabClick(e){e.disabled||(this.activeTabId=e.id,this.dispatchEvent(new CustomEvent(\"tab-change\",{detail:{activeTabId:e.id},bubbles:!0,composed:!0})))}_handleTabDismiss(e,t){t.stopPropagation(),this.dispatchEvent(new CustomEvent(\"tab-dismiss\",{detail:{tabId:e.id},bubbles:!0,composed:!0}))}_handleKeyDown(e){const t=this._getTabs().filter((e=>!e.disabled)),i=t.findIndex((e=>e.id===this.activeTabId));let s=-1;switch(e.key){case\"ArrowLeft\":s=i>0?i-1:t.length-1;break;case\"ArrowRight\":s=i<t.length-1?i+1:0;break;case\"Home\":s=0;break;case\"End\":s=t.length-1}if(s>=0){e.preventDefault();const i=t[s];this.activeTabId=i.id,this.dispatchEvent(new CustomEvent(\"tab-change\",{detail:{activeTabId:i.id},bubbles:!0,composed:!0}));const o=this.shadowRoot?.querySelectorAll(\".label\"),r=Array.from(o||[]).find((e=>e.getAttribute(\"data-tab-id\")===i.id));r?.focus()}}_renderTabIcon(e){const t=e.getIconElement();if(!t)return U;const i=t.cloneNode(!0);return i.removeAttribute(\"slot\"),V`<span class=\"label-icon\">${i}</span>`}render(){return V` <div class=\"header\" role=\"tablist\" @keydown=${this._handleKeyDown}> ${this._getTabs().map((e=>V` <button class=${we({label:!0,\"label--active\":e.id===this.activeTabId,\"label--justified\":this.justified})} role=\"tab\" data-tab-id=${e.id} aria-selected=${e.id===this.activeTabId} aria-controls=${`panel-${e.id}`} tabindex=${e.id===this.activeTabId?0:-1} ?disabled=${e.disabled} @click=${()=>this._handleTabClick(e)} > ${this._renderTabIcon(e)} <span>${e.label}</span> ${e.badge?V`<span class=\"label-badge\" style=${qe({backgroundColor:e.badgeBackground,color:e.badgeColor})}>${e.badge}</span>`:U} ${e.dismissible?V` <button class=\"label-dismiss\" type=\"button\" aria-label=\"Close tab\" @click=${t=>this._handleTabDismiss(e,t)} > <svg viewBox=\"0 0 20 20\" fill=\"currentColor\" width=\"12\" height=\"12\"><path fill-rule=\"evenodd\" d=\"M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z\" clip-rule=\"evenodd\"/></svg> </button> `:U} </button> `))} </div> <div class=\"content\" role=\"tabpanel\" aria-labelledby=${this.activeTabId||\"\"}> <slot @slotchange=${this._updateActiveTab}></slot> </div> `}}"
949
949
  },
950
950
  {
951
951
  "kind": "variable",
@@ -955,7 +955,7 @@
955
955
  {
956
956
  "kind": "variable",
957
957
  "name": "Qe",
958
- "default": "class extends le{constructor(){super(),this.label=\"\",this.name=\"\",this.value=\"\",this.placeholder=\"Select an option\",this.disabled=!1,this.required=!1,this.readonly=!1,this.hint=\"\",this._isOpen=!1,this._highlightedIndex=-1,this._touched=!1,this._handleInvalid=e=>{e.preventDefault(),this._touched=!0},this._handleOutsideClick=e=>{e.composedPath().includes(this)||this._close()},this._handleKeyDown=e=>{if(!this._isOpen)return;const t=Array.from(this.querySelectorAll(\"kr-select-option\"));switch(e.key){case\"Escape\":this._close(),this._triggerElement?.focus();break;case\"ArrowDown\":if(e.preventDefault(),t.some((e=>!e.disabled))){let e=this._highlightedIndex+1;for(;e<t.length&&t[e]?.disabled;)e++;e<t.length&&(this._highlightedIndex=e)}break;case\"ArrowUp\":e.preventDefault();{let e=this._highlightedIndex-1;for(;e>=0&&t[e]?.disabled;)e--;e>=0&&(this._highlightedIndex=e)}break;case\"Enter\":e.preventDefault(),this._highlightedIndex>=0&&this._highlightedIndex<t.length&&this._selectOption(t[this._highlightedIndex])}},this._internals=this.attachInternals()}get form(){return this._internals.form}get validity(){return this._internals.validity}get validationMessage(){return this._internals.validationMessage}get willValidate(){return this._internals.willValidate}checkValidity(){return this._internals.checkValidity()}reportValidity(){return this._internals.reportValidity()}formResetCallback(){this.value=\"\",this._touched=!1,this._internals.setFormValue(\"\"),this._internals.setValidity({})}formStateRestoreCallback(e){this.value=e}connectedCallback(){super.connectedCallback(),document.addEventListener(\"click\",this._handleOutsideClick),document.addEventListener(\"keydown\",this._handleKeyDown),this.addEventListener(\"invalid\",this._handleInvalid)}firstUpdated(){this._updateValidity()}updated(e){(e.has(\"required\")||e.has(\"value\"))&&this._updateValidity()}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener(\"click\",this._handleOutsideClick),document.removeEventListener(\"keydown\",this._handleKeyDown),this.removeEventListener(\"invalid\",this._handleInvalid)}_toggle(){if(!this.disabled&&!this.readonly)if(this._isOpen)this._close();else{this._isOpen=!0;const e=Array.from(this.querySelectorAll(\"kr-select-option\"));this._highlightedIndex=e.findIndex((e=>e.value===this.value)),requestAnimationFrame((()=>{const e=this.shadowRoot?.querySelector(\".select-dropdown\");if(e){const t=this._triggerElement.getBoundingClientRect(),i=window.innerHeight-t.bottom-4-8;e.style.top=t.bottom+4+\"px\",e.style.left=t.left+\"px\",e.style.width=t.width+\"px\",e.style.maxHeight=i+\"px\"}}))}}_close(){this._isOpen=!1,this._highlightedIndex=-1}_selectOption(e){e.disabled||(this.value=e.value,this._internals.setFormValue(this.value),this._updateValidity(),this.dispatchEvent(new Event(\"change\",{bubbles:!0,composed:!0})),this._close(),this._triggerElement?.focus())}_handleBlur(){this._touched=!0,this._updateValidity()}_updateValidity(){this.required&&!this.value?this._internals.setValidity({valueMissing:!0},\"Please select an option\",this._triggerElement):this._internals.setValidity({})}render(){const e=Array.from(this.querySelectorAll(\"kr-select-option\")),t=e.find((e=>e.value===this.value))?.label;return V` <div class=\"wrapper\"> ${this.label?V` <label> ${this.label} ${this.required?V`<span class=\"required\" aria-hidden=\"true\">*</span>`:\"\"} </label> `:B} <div class=\"select-wrapper\"> <button class=${we({\"select-trigger\":!0,\"select-trigger--open\":this._isOpen,\"select-trigger--invalid\":this._touched&&this.required&&!this.value})} type=\"button\" ?disabled=${this.disabled} aria-haspopup=\"listbox\" aria-expanded=${this._isOpen} @click=${this._toggle} @blur=${this._handleBlur} > <span class=${we({\"select-value\":!0,\"select-placeholder\":!t})}> ${t||this.placeholder} </span> <svg class=${we({\"chevron-icon\":!0,\"select-icon\":!0,\"select-icon--open\":this._isOpen})} viewBox=\"0 0 24 24\" fill=\"currentColor\" > <path d=\"M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z\"/> </svg> </button> <div class=${we({\"select-dropdown\":!0,hidden:!this._isOpen})} role=\"listbox\"> <div class=\"select-options\"> ${0===e.length?V`<div class=\"select-empty\">No options available</div>`:e.map(((e,t)=>{const i=e.value===this.value;return V` <div class=${we({\"select-option\":!0,\"select-option--selected\":i,\"select-option--disabled\":e.disabled,\"select-option--highlighted\":t===this._highlightedIndex})} role=\"option\" aria-selected=${i} @click=${()=>this._selectOption(e)} @mouseenter=${()=>this._highlightedIndex=t} > ${e.label} </div> `}))} </div> </div> </div> ${this._touched&&this.required&&!this.value?V`<div class=\"validation-message\">Please select an option</div>`:this.hint?V`<div class=\"hint\">${this.hint}</div>`:\"\"} </div> <div class=\"options-slot\"> <slot @slotchange=${()=>this.requestUpdate()}></slot> </div> `}focus(){this._triggerElement?.focus()}blur(){this._triggerElement?.blur()}}"
958
+ "default": "class extends le{constructor(){super(),this.label=\"\",this.name=\"\",this.value=\"\",this.placeholder=\"Select an option\",this.disabled=!1,this.required=!1,this.readonly=!1,this.hint=\"\",this._isOpen=!1,this._highlightedIndex=-1,this._touched=!1,this._handleInvalid=e=>{e.preventDefault(),this._touched=!0},this._handleOutsideClick=e=>{e.composedPath().includes(this)||this._close()},this._handleKeyDown=e=>{if(!this._isOpen)return;const t=Array.from(this.querySelectorAll(\"kr-select-option\"));switch(e.key){case\"Escape\":this._close(),this._triggerElement?.focus();break;case\"ArrowDown\":if(e.preventDefault(),t.some((e=>!e.disabled))){let e=this._highlightedIndex+1;for(;e<t.length&&t[e]?.disabled;)e++;e<t.length&&(this._highlightedIndex=e)}break;case\"ArrowUp\":e.preventDefault();{let e=this._highlightedIndex-1;for(;e>=0&&t[e]?.disabled;)e--;e>=0&&(this._highlightedIndex=e)}break;case\"Enter\":e.preventDefault(),this._highlightedIndex>=0&&this._highlightedIndex<t.length&&this._selectOption(t[this._highlightedIndex])}},this._internals=this.attachInternals()}get form(){return this._internals.form}get validity(){return this._internals.validity}get validationMessage(){return this._internals.validationMessage}get willValidate(){return this._internals.willValidate}checkValidity(){return this._internals.checkValidity()}reportValidity(){return this._internals.reportValidity()}formResetCallback(){this.value=\"\",this._touched=!1,this._internals.setFormValue(\"\"),this._internals.setValidity({})}formStateRestoreCallback(e){this.value=e}connectedCallback(){super.connectedCallback(),document.addEventListener(\"click\",this._handleOutsideClick),document.addEventListener(\"keydown\",this._handleKeyDown),this.addEventListener(\"invalid\",this._handleInvalid)}firstUpdated(){this._updateValidity()}updated(e){(e.has(\"required\")||e.has(\"value\"))&&this._updateValidity()}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener(\"click\",this._handleOutsideClick),document.removeEventListener(\"keydown\",this._handleKeyDown),this.removeEventListener(\"invalid\",this._handleInvalid)}_toggle(){if(!this.disabled&&!this.readonly)if(this._isOpen)this._close();else{this._isOpen=!0;const e=Array.from(this.querySelectorAll(\"kr-select-option\"));this._highlightedIndex=e.findIndex((e=>e.value===this.value)),requestAnimationFrame((()=>{const e=this.shadowRoot?.querySelector(\".select-dropdown\");if(e){const t=this._triggerElement.getBoundingClientRect(),i=window.innerHeight-t.bottom-4-8;e.style.top=t.bottom+4+\"px\",e.style.left=t.left+\"px\",e.style.width=t.width+\"px\",e.style.maxHeight=i+\"px\"}}))}}_close(){this._isOpen=!1,this._highlightedIndex=-1}_selectOption(e){e.disabled||(this.value=e.value,this._internals.setFormValue(this.value),this._updateValidity(),this.dispatchEvent(new Event(\"change\",{bubbles:!0,composed:!0})),this._close(),this._triggerElement?.focus())}_handleBlur(){this._touched=!0,this._updateValidity()}_updateValidity(){this.required&&!this.value?this._internals.setValidity({valueMissing:!0},\"Please select an option\",this._triggerElement):this._internals.setValidity({})}render(){const e=Array.from(this.querySelectorAll(\"kr-select-option\")),t=e.find((e=>e.value===this.value))?.label;return V` <div class=\"wrapper\"> ${this.label?V` <label> ${this.label} ${this.required?V`<span class=\"required\" aria-hidden=\"true\">*</span>`:\"\"} </label> `:U} <div class=\"select-wrapper\"> <button class=${we({\"select-trigger\":!0,\"select-trigger--open\":this._isOpen,\"select-trigger--invalid\":this._touched&&this.required&&!this.value})} type=\"button\" ?disabled=${this.disabled} aria-haspopup=\"listbox\" aria-expanded=${this._isOpen} @click=${this._toggle} @blur=${this._handleBlur} > <span class=${we({\"select-value\":!0,\"select-placeholder\":!t})}> ${t||this.placeholder} </span> <svg class=${we({\"chevron-icon\":!0,\"select-icon\":!0,\"select-icon--open\":this._isOpen})} viewBox=\"0 0 24 24\" fill=\"currentColor\" > <path d=\"M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z\"/> </svg> </button> <div class=${we({\"select-dropdown\":!0,hidden:!this._isOpen})} role=\"listbox\"> <div class=\"select-options\"> ${0===e.length?V`<div class=\"select-empty\">No options available</div>`:e.map(((e,t)=>{const i=e.value===this.value;return V` <div class=${we({\"select-option\":!0,\"select-option--selected\":i,\"select-option--disabled\":e.disabled,\"select-option--highlighted\":t===this._highlightedIndex})} role=\"option\" aria-selected=${i} @click=${()=>this._selectOption(e)} @mouseenter=${()=>this._highlightedIndex=t} > ${e.label} </div> `}))} </div> </div> </div> ${this._touched&&this.required&&!this.value?V`<div class=\"validation-message\">Please select an option</div>`:this.hint?V`<div class=\"hint\">${this.hint}</div>`:\"\"} </div> <div class=\"options-slot\"> <slot @slotchange=${()=>this.requestUpdate()}></slot> </div> `}focus(){this._triggerElement?.focus()}blur(){this._triggerElement?.blur()}}"
959
959
  },
960
960
  {
961
961
  "kind": "variable",
@@ -1172,7 +1172,7 @@
1172
1172
  {
1173
1173
  "kind": "variable",
1174
1174
  "name": "pt",
1175
- "default": "class extends le{constructor(){super(...arguments),this._scrollStyle=\"overlay\",this._data=[],this._dataState=\"idle\",this._page=1,this._pageSize=50,this._totalItems=0,this._totalPages=0,this._searchQuery=\"\",this._canScrollLeft=!1,this._canScrollRight=!1,this._canScrollHorizontal=!1,this._columnPickerOpen=!1,this._filterPanelOpened=null,this._filterPanelTab=\"filter\",this._buckets=new Map,this._filterPanelPos={top:0,left:0},this._resizing=null,this._resizeObserver=null,this._searchPositionLocked=!1,this._model=new ct,this.def={columns:[]},this._handleClickOutside=e=>{const t=e.composedPath();if(this._columnPickerOpen){const e=this.shadowRoot?.querySelector(\".column-picker-wrapper\");e&&!t.includes(e)&&(this._columnPickerOpen=!1)}this._filterPanelOpened&&(t.some((e=>e.classList?.contains(\"filter-panel\")))||this._handleFilterApply())},this._handleResizeMove=e=>{if(!this._resizing)return;const t=this._model.columns.find((e=>e.id===this._resizing.columnId));if(t){const i=this._resizing.startWidth+(e.clientX-this._resizing.startX);t.width=`${Math.min(900,Math.max(50,i))}px`,this.requestUpdate()}},this._handleResizeEnd=()=>{this._resizing=null,document.removeEventListener(\"mousemove\",this._handleResizeMove),document.removeEventListener(\"mouseup\",this._handleResizeEnd)}}connectedCallback(){super.connectedCallback(),this.classList.toggle(\"kr-table--scroll-overlay\",\"overlay\"===this._scrollStyle),this.classList.toggle(\"kr-table--scroll-edge\",\"edge\"===this._scrollStyle),this._fetch(),this._initRefresh(),document.addEventListener(\"click\",this._handleClickOutside),this._resizeObserver=new ResizeObserver((()=>{this._searchPositionLocked=!1,this._updateSearchPosition()})),this._resizeObserver.observe(this)}disconnectedCallback(){super.disconnectedCallback(),clearInterval(this._refreshTimer),document.removeEventListener(\"click\",this._handleClickOutside),this._resizeObserver?.disconnect()}willUpdate(e){e.has(\"def\")&&(this._model=new ct,this.def.title&&(this._model.title=this.def.title),this.def.actions&&(this._model.actions=this.def.actions),this.def.data&&(this._model.data=this.def.data),this.def.dataSource&&(this._model.dataSource=this.def.dataSource),\"number\"==typeof this.def.refreshInterval&&(this._model.refreshInterval=this.def.refreshInterval),\"number\"==typeof this.def.pageSize&&(this._model.pageSize=this.def.pageSize),this.def.rowClickable&&(this._model.rowClickable=this.def.rowClickable),this.def.rowHref&&(this._model.rowHref=this.def.rowHref),this._model.columns=this.def.columns.map((e=>{const t={...e,filter:null};return t.type||(t.type=\"string\"),\"actions\"===t.type?(t.label=e.label??\"\",t.sticky=\"right\",t.resizable=!1,t):((e.filterable||e.facetable)&&(t.filter=new dt,t.filter.field=e.id,t.filter.type=t.type,e.filter?(t.filter.setOperator(e.filter.operator),t.filter.setValue(e.filter.value)):e.facetable&&!e.filterable?(t.filter.operator=\"in\",t.filter.value=[]):\"string\"===t.filter.type&&(t.filter.operator=\"contains\")),t)})),this.def.displayedColumns?this._model.displayedColumns=this.def.displayedColumns:this._model.displayedColumns=this._model.columns.map((e=>e.id)),this._fetch(),this._initRefresh())}updated(e){this._updateScrollFlags(),this._syncSlottedContent()}_syncSlottedContent(){const e=this.getDisplayedColumns().filter((e=>e.render));e.length&&(this.querySelectorAll('[slot^=\"cell-\"]').forEach((e=>e.remove())),this._data.forEach(((t,i)=>{e.forEach((e=>{const s=e.render(t);if(!s)return;const o=document.createElement(\"span\");o.slot=`cell-${i}-${e.id}`,\"actions\"===e.type&&(o.style.display=\"flex\",o.style.gap=\"8px\"),\"string\"==typeof s?o.innerHTML=s:oe(s,o),this.appendChild(o)}))})))}refresh(){this._fetch()}goToPrevPage(){this._page>1&&(this._page--,this._fetch())}goToNextPage(){this._page<this._totalPages&&(this._page++,this._fetch())}goToPage(e){e>=1&&e<=this._totalPages&&(this._page=e,this._fetch())}_toSolrData(){const e={page:this._page-1,size:this._pageSize,sorts:[],filterFields:[],queryFields:[],facetFields:[]};for(const t of this._model.columns){if(!t.filter||t.filter.isEmpty()||!t.filter.isValid())continue;const i=t.filter.toSolrData();t.facetable&&\"in\"===t.filter.operator&&(i.tagged=!0),e.filterFields.push(i)}for(const t of this._model.columns)t.facetable&&e.facetFields.push({name:t.id,type:\"FIELD\",limit:100,sort:\"count\",minimumCount:1});return this._searchQuery?.trim().length&&e.queryFields.push({name:\"_text_\",operation:\"IS\",value:ot(this._searchQuery,!1)}),e}_toDbParams(){const e={page:this._page-1,size:this._pageSize,sorts:[],filterFields:[],queryFields:[],facetFields:[]};for(const t of this._model.columns)t.filter&&!t.filter.isEmpty()&&t.filter.isValid()&&e.filterFields.push(t.filter.toDbParams());return this._searchQuery?.trim().length&&this._model.columns.filter((e=>e.searchable)).forEach((t=>{e.queryFields.push({name:t.id,operation:\"CONTAINS\",value:this._searchQuery,and:!1})})),e}_fetch(){if(this._model.data)return this._data=this._model.data,this._totalItems=this._model.data.length,this._totalPages=Math.ceil(this._model.data.length/this._pageSize),void(this._dataState=\"success\");if(!this._model.dataSource)return;let e;this._dataState=\"loading\",e=\"db\"===this._model.dataSource.mode?this._toDbParams():this._toSolrData(),this._model.dataSource.fetch(e).then((e=>{switch(this._model.dataSource?.mode){case\"opensearch\":throw Error(\"Opensearch not supported yet\");case\"db\":{const t=e;this._data=t.data.content,this._totalItems=t.data.totalElements,this._totalPages=t.data.totalPages,this._pageSize=t.data.size;break}default:{const t=e;this._data=t.data.content,this._totalItems=t.data.totalElements,this._totalPages=t.data.totalPages,this._pageSize=t.data.size,this._parseFacetResults(t)}}this._dataState=\"success\",this._updateSearchPosition()})).catch((e=>{this._dataState=\"error\",Fe.show({message:e instanceof Error?e.message:\"Failed to load data\",type:\"error\"})}))}_parseFacetResults(e){if(e.data.facetFields){for(const t of this._model.columns){if(!t.facetable)continue;const i=e.data.facetFields[t.id];if(!i){this._buckets.set(t.id,[]);continue}const s=[];for(const e of i){let i=e.name;\"boolean\"===t.type&&\"string\"==typeof e.name&&(\"true\"===e.name?i=!0:\"false\"===e.name&&(i=!1)),null===e.name&&e.count>0&&s.unshift({val:null,count:e.count}),null!==e.name&&s.push({val:i,count:e.count})}if(t.filter&&\"in\"===t.filter.operator&&Array.isArray(t.filter.value))for(const e of t.filter.value)s.some((t=>t.val===e))||s.push({val:e,count:0});this._buckets.set(t.id,s)}this._buckets=new Map(this._buckets)}}_initRefresh(){clearInterval(this._refreshTimer),this._model.refreshInterval&&this._model.refreshInterval>0&&(this._refreshTimer=window.setInterval((()=>{this._fetch()}),this._model.refreshInterval))}_handleSearch(e){const t=e.target;this._searchQuery=t.value,this._page=1,this._fetch()}_getGridTemplateColumns(){return this.getDisplayedColumns().map((e=>e.width?e.width:\"actions\"===e.type?\"max-content\":\"minmax(80px, auto)\")).join(\" \")}_updateSearchPosition(){if(this._searchPositionLocked)return;const e=this.shadowRoot?.querySelector(\".search\"),t=e?.querySelector(\".search-field\");e&&t&&(e.style.justifyContent=\"center\",t.style.marginLeft=\"\",requestAnimationFrame((()=>{const i=e.getBoundingClientRect(),s=t.getBoundingClientRect().left-i.left;e.style.justifyContent=\"flex-start\",t.style.marginLeft=`${s}px`,this._searchPositionLocked=!0})))}_toggleColumnPicker(){this._columnPickerOpen=!this._columnPickerOpen}_toggleColumn(e){this._model.displayedColumns.includes(e)?this._model.displayedColumns=this._model.displayedColumns.filter((t=>t!==e)):this._model.displayedColumns=[...this._model.displayedColumns,e],this.requestUpdate()}_handleRowMouseDown(){this._model.rowClickable&&window.getSelection()?.removeAllRanges()}_handleRowClick(e,t){if(!this._model.rowClickable)return;const i=window.getSelection();i&&i.toString().length>0||this.dispatchEvent(new CustomEvent(\"row-click\",{detail:{row:e,rowIndex:t},bubbles:!0,composed:!0}))}getDisplayedColumns(){return this._model.displayedColumns.map((e=>this._model.columns.find((t=>t.id===e)))).sort(((e,t)=>\"actions\"===e.type&&\"actions\"!==t.type?1:\"actions\"!==e.type&&\"actions\"===t.type?-1:0))}_handleScroll(e){const t=e.target;this._canScrollLeft=t.scrollLeft>0,this._canScrollRight=t.scrollLeft<t.scrollWidth-t.clientWidth-1}_updateScrollFlags(){const e=this.shadowRoot?.querySelector(\".content\");e&&(this._canScrollLeft=e.scrollLeft>0,this._canScrollRight=e.scrollWidth>e.clientWidth&&e.scrollLeft<e.scrollWidth-e.clientWidth-1,this._canScrollHorizontal=e.scrollWidth>e.clientWidth),this.classList.toggle(\"kr-table--scroll-left-available\",this._canScrollLeft),this.classList.toggle(\"kr-table--scroll-right-available\",this._canScrollRight),this.classList.toggle(\"kr-table--scroll-horizontal-available\",this._canScrollHorizontal),this.classList.toggle(\"kr-table--sticky-left\",this.getDisplayedColumns().some((e=>\"left\"===e.sticky))),this.classList.toggle(\"kr-table--sticky-right\",this.getDisplayedColumns().some((e=>\"right\"===e.sticky)))}_handleResizeStart(e,t){e.preventDefault();const i=this.shadowRoot?.querySelector(`.header-cell[data-column-id=\"${t}\"]`);this._resizing={columnId:t,startX:e.clientX,startWidth:i?.offsetWidth||200},document.addEventListener(\"mousemove\",this._handleResizeMove),document.addEventListener(\"mouseup\",this._handleResizeEnd)}_handleAction(e){e.href||this.dispatchEvent(new CustomEvent(\"action\",{detail:{action:e.id},bubbles:!0,composed:!0}))}_handleKqlChange(e,t){const i=e.target.value.trim();if(i){if(t.filter.setKql(i),this.requestUpdate(),!t.filter.isValid())return}else t.filter.clear(),this.requestUpdate();this._page=1,this._fetch()}_handleFilterPanelToggle(e,t){if(e.stopPropagation(),this._filterPanelOpened===t.id)this._filterPanelOpened=null;else{const i=e.currentTarget.getBoundingClientRect();let s=i.left;s+328>window.innerWidth&&(s=window.innerWidth-328),this._filterPanelPos={top:i.bottom+4,left:s},this._filterPanelOpened=t.id,t.facetable?this._filterPanelTab=\"counts\":this._filterPanelTab=\"filter\"}}_handleKqlClear(e){e.filter.clear(),this._page=1,this._fetch()}_handleFilterClear(){const e=this._model.columns.find((e=>e.id===this._filterPanelOpened));e&&(e.filter.clear(),e.facetable&&!e.filterable&&(e.filter.operator=\"in\",e.filter.value=[])),this._filterPanelOpened=null,this._page=1,this._fetch()}_handleFilterTextKeydown(e,t){\"Enter\"===e.key&&(e.preventDefault(),this._handleFilterApply())}_handleOperatorChange(e,t){t.filter.setOperator(e.target.value),this.requestUpdate()}_handleFilterStringChange(e,t){t.filter.setValue(e.target.value),this.requestUpdate()}_handleFilterNumberChange(e,t){t.filter.setValue(Number(e.target.value)),this.requestUpdate()}_handleFilterDateChange(e,t){t.filter.setValue(new Date(e.target.value),\"day\"),this.requestUpdate()}_handleFilterBooleanChange(e,t){t.filter.setValue(\"true\"===e.target.value),this.requestUpdate()}_handleFilterDateStartChange(e,t){t.filter.setStart(new Date(e.target.value),\"day\"),this.requestUpdate()}_handleFilterDateEndChange(e,t){t.filter.setEnd(new Date(e.target.value),\"day\"),this.requestUpdate()}_handleFilterNumberStartChange(e,t){t.filter.setStart(Number(e.target.value)),this.requestUpdate()}_handleFilterNumberEndChange(e,t){t.filter.setEnd(Number(e.target.value)),this.requestUpdate()}_handleFilterListChange(e,t){const i=e.target.value.split(\",\").map((e=>e.trim())).filter((e=>\"\"!==e));\"number\"===t.type?t.filter.setValue(i.map((e=>Number(e)))):t.filter.setValue(i),this.requestUpdate()}_handleFilterApply(){this._filterPanelOpened=null,this._page=1,this._fetch()}_handleFilterPanelTabChange(e){this._filterPanelTab=e.detail.activeTabId}_handleBucketToggle(e,t,i){t.filter.toggle(i.val),this._page=1,this._fetch()}_renderCellContent(e,t,i){const s=t[e.id];if(e.render)return V`<slot name=\"cell-${i}-${e.id}\"></slot>`;if(null==s)return\"\";switch(e.type){case\"number\":return\"currency\"===e.format&&\"number\"==typeof s?s.toLocaleString(\"en-US\",{style:\"currency\",currency:\"USD\"}):String(s);case\"date\":{let e;if(s instanceof Date)e=s;else if(\"string\"==typeof s&&/^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}/.test(s)){const t=s.replace(/(\\d{2}:\\d{2}:\\d{2}):(\\d+)$/,\"$1.$2\").replace(\" \",\"T\")+\"Z\";e=new Date(t)}else e=new Date(s);return e.toLocaleString(void 0,{year:\"numeric\",month:\"short\",day:\"numeric\",hour:\"numeric\",minute:\"2-digit\",timeZone:\"UTC\"})}case\"boolean\":return!0===s?\"Yes\":!1===s?\"No\":\"\";default:return String(s)}}_getHeaderCellClasses(e,t){return{\"header-cell\":!0,\"header-cell--align-center\":\"center\"===e.align,\"header-cell--align-right\":\"right\"===e.align,\"header-cell--sticky-left\":\"left\"===e.sticky,\"header-cell--sticky-left-last\":\"left\"===e.sticky&&!this.getDisplayedColumns().slice(t+1).some((e=>\"left\"===e.sticky)),\"header-cell--sticky-right\":\"right\"===e.sticky,\"header-cell--sticky-right-first\":\"right\"===e.sticky&&!this.getDisplayedColumns().slice(0,t).some((e=>\"right\"===e.sticky))}}_getCellClasses(e,t){return{cell:!0,\"cell--actions\":\"actions\"===e.type,\"cell--align-center\":\"center\"===e.align,\"cell--align-right\":\"right\"===e.align,\"cell--sticky-left\":\"left\"===e.sticky,\"cell--sticky-left-last\":\"left\"===e.sticky&&!this.getDisplayedColumns().slice(t+1).some((e=>\"left\"===e.sticky)),\"cell--sticky-right\":\"right\"===e.sticky,\"cell--sticky-right-first\":\"right\"===e.sticky&&!this.getDisplayedColumns().slice(0,t).some((e=>\"right\"===e.sticky))}}_getCellStyle(e,t){const i={};if(\"left\"===e.sticky){let e=0;for(let i=0;i<t;i++){const t=this.getDisplayedColumns()[i];\"left\"===t.sticky&&(e+=parseInt(t.width||\"0\",10))}i.left=`${e}px`}if(\"right\"===e.sticky){let e=0;for(let i=t+1;i<this.getDisplayedColumns().length;i++){const t=this.getDisplayedColumns()[i];\"right\"===t.sticky&&(e+=parseInt(t.width||\"0\",10))}i.right=`${e}px`}return i}_renderPagination(){const e=(this._page-1)*this._pageSize+1,t=Math.min(this._page*this._pageSize,this._totalItems);return V` <div class=\"pagination\"> <span class=\"pagination-icon ${1===this._page?\"pagination-icon--disabled\":\"\"}\" @click=${this.goToPrevPage} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z\"/></svg> </span> <span class=\"pagination-info\">${e}-${t} of ${this._totalItems}</span> <span class=\"pagination-icon ${this._page===this._totalPages?\"pagination-icon--disabled\":\"\"}\" @click=${this.goToNextPage} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\"/></svg> </span> </div> `}_renderHeader(){return!this._model.title&&!this._model.actions?.length&&this._totalPages<=1?B:V` <div class=\"header\"> <div class=\"title\">${this._model.title??\"\"}</div> ${\"db\"!==this._model.dataSource?.mode||this._model.columns.some((e=>e.searchable))?V` <div class=\"search\"> <!-- TODO: Saved views dropdown <div class=\"views\"> <span>Default View</span> <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z\"/></svg> </div> --> <div class=\"search-field\"> <svg class=\"search-icon\" viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z\"/></svg> <input type=\"text\" class=\"search-input\" placeholder=\"Search...\" .value=${this._searchQuery} @input=${this._handleSearch} /> </div> </div> `:V`<div class=\"search\"></div>`} <div class=\"tools\"> ${this._renderPagination()} <span class=\"refresh\" title=\"Refresh\" @click=${()=>this.refresh()}> <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M480-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100 70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z\"/></svg> </span> <div class=\"column-picker-wrapper\"> <span class=\"header-icon\" title=\"Columns\" @click=${this._toggleColumnPicker}> <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M121-280v-400q0-33 23.5-56.5T201-760h559q33 0 56.5 23.5T840-680v400q0 33-23.5 56.5T760-200H201q-33 0-56.5-23.5T121-280Zm79 0h133v-400H200v400Zm213 0h133v-400H413v400Zm213 0h133v-400H626v400Z\"/></svg> </span> <div class=\"column-picker ${this._columnPickerOpen?\"open\":\"\"}\"> ${[...this._model.columns].filter((e=>\"actions\"!==e.type)).sort(((e,t)=>(e.label??e.id).localeCompare(t.label??t.id))).map((e=>V` <div class=\"column-picker-item\" @click=${()=>this._toggleColumn(e.id)}> <div class=\"column-picker-checkbox ${this._model.displayedColumns.includes(e.id)?\"checked\":\"\"}\"> <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z\"/></svg> </div> <span class=\"column-picker-label\">${e.label??e.id}</span> </div> `))} </div> </div> ${1===this._model.actions?.length?V` <kr-button class=\"actions\" .href=${this._model.actions[0].href} .target=${this._model.actions[0].target} @click=${()=>this._handleAction(this._model.actions[0])} > ${this._model.actions[0].label} </kr-button> `:this._model.actions?.length?V` <kr-button class=\"actions\" .options=${this._model.actions.map((e=>({id:e.id,label:e.label})))} @option-select=${e=>this._handleAction({id:e.detail.id,label:e.detail.label})} > Actions </kr-button> `:B} </div> </div> `}_renderStatus(){return\"loading\"===this._dataState&&0===this._data.length?V`<div class=\"status\">Loading...</div>`:\"error\"===this._dataState&&0===this._data.length?V`<div class=\"status status--error\">Error loading data</div>`:0===this._data.length?V`<div class=\"status\">No data available</div>`:B}_renderFilterPanel(){if(!this._filterPanelOpened)return B;const e=this._model.columns.find((e=>e.id===this._filterPanelOpened));let t=V``;t=\"empty\"===e.filter.operator||\"n_empty\"===e.filter.operator?V` <input type=\"text\" class=\"filter-panel__input\" disabled .value=${e.filter.text} /> `:\"between\"===e.filter.operator&&\"date\"===e.type?V` <input type=\"date\" class=\"filter-panel__input\" .valueAsDate=${e.filter.value?.start??null} @change=${t=>this._handleFilterDateStartChange(t,e)} /> <input type=\"date\" class=\"filter-panel__input\" .valueAsDate=${e.filter.value?.end??null} @change=${t=>this._handleFilterDateEndChange(t,e)} /> `:\"between\"===e.filter.operator&&\"number\"===e.type?V` <input type=\"number\" class=\"filter-panel__input\" placeholder=\"Start\" .value=${e.filter.value?.start??\"\"} @input=${t=>this._handleFilterNumberStartChange(t,e)} @keydown=${t=>this._handleFilterTextKeydown(t,e)} /> <input type=\"number\" class=\"filter-panel__input\" placeholder=\"End\" .value=${e.filter.value?.end??\"\"} @input=${t=>this._handleFilterNumberEndChange(t,e)} @keydown=${t=>this._handleFilterTextKeydown(t,e)} /> `:\"in\"===e.filter.operator?V` <textarea class=\"filter-panel__textarea\" rows=\"3\" placeholder=\"Values (comma-separated)\" .value=${e.filter.text} @input=${t=>this._handleFilterListChange(t,e)} @keydown=${t=>this._handleFilterTextKeydown(t,e)} ></textarea> `:\"boolean\"===e.type?V` <kr-select-field placeholder=\"Value\" .value=${String(e.filter.value??\"\")} @change=${t=>this._handleFilterBooleanChange(t,e)} > <kr-select-option value=\"true\">Yes</kr-select-option> <kr-select-option value=\"false\">No</kr-select-option> </kr-select-field> `:\"date\"===e.type?V` <input type=\"date\" class=\"filter-panel__input\" .valueAsDate=${e.filter.value} @change=${t=>this._handleFilterDateChange(t,e)} /> `:\"number\"===e.type?V` <input type=\"number\" class=\"filter-panel__input\" placeholder=\"Value\" min=\"0\" .value=${e.filter.text} @input=${t=>this._handleFilterNumberChange(t,e)} @keydown=${t=>this._handleFilterTextKeydown(t,e)} /> `:V` <input type=\"text\" class=\"filter-panel__input\" placeholder=\"Value\" .value=${e.filter.text} @input=${t=>this._handleFilterStringChange(t,e)} @keydown=${t=>this._handleFilterTextKeydown(t,e)} /> `;const i=V` <div class=\"filter-panel__content\"> <kr-select-field .value=${e.filter.operator} @change=${t=>this._handleOperatorChange(t,e)} > ${tt(e.type).map((e=>V` <kr-select-option value=${e.key}>${e.label}</kr-select-option> `))} </kr-select-field> ${t} </div> `,s=this._buckets.get(e.id)||[];let o,r;return o=s.length?V` <div class=\"buckets\"> ${s.map((t=>{let i=\"(Empty)\";null!==t.val&&void 0!==t.val&&(i=\"boolean\"===e.type?!0===t.val||\"true\"===t.val?\"Yes\":\"No\":String(t.val));let s=B;return e.filter.has(t.val)&&(s=V` <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"> <path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z\"/> </svg> `),V` <div class=\"bucket\" @click=${i=>this._handleBucketToggle(i,e,t)} > <div class=${we({bucket__checkbox:!0,\"bucket__checkbox--checked\":e.filter.has(t.val)})}> ${s} </div> <span class=\"bucket__label\">${i}</span> <span class=\"bucket__count\">${t.count}</span> </div> `}))} </div> `:V`<div class=\"bucket-empty\">No data</div>`,r=e.facetable&&e.filterable?V` <kr-tab-group size=\"small\" active-tab-id=${this._filterPanelTab} @tab-change=${e=>this._handleFilterPanelTabChange(e)} > <kr-tab id=\"filter\" label=\"Filter\"> ${i} </kr-tab> <kr-tab id=\"counts\" label=\"Counts\"> ${o} </kr-tab> </kr-tab-group> `:e.facetable?o:i,V` <div class=\"filter-panel\" style=${Le({top:this._filterPanelPos.top+\"px\",left:this._filterPanelPos.left+\"px\"})} > ${r} <div class=\"filter-panel__actions\"> <kr-button variant=\"outline\" color=\"secondary\" size=\"small\" @click=${this._handleFilterClear}> Clear </kr-button> <kr-button size=\"small\" @click=${this._handleFilterApply}> Apply </kr-button> </div> </div> `}_renderFilterRow(){const e=this.getDisplayedColumns();return e.some((e=>e.filterable||e.facetable))?V` <div class=\"filter-row\"> ${e.map(((t,i)=>t.filterable||t.facetable?V` <div class=${we({\"filter-cell\":!0,\"filter-cell--sticky-left\":\"left\"===t.sticky,\"filter-cell--sticky-right\":\"right\"===t.sticky,\"filter-cell--sticky-right-first\":\"right\"===t.sticky&&!e.slice(0,i).some((e=>\"right\"===e.sticky))})} style=${Le(this._getCellStyle(t,i))} > <div class=\"filter-cell__wrapper\"> <input type=\"text\" class=${we({\"filter-cell__input\":!0,\"filter-cell__input--invalid\":!t.filter.isValid()})} .value=${t.filter.kql} @change=${e=>this._handleKqlChange(e,t)} /> ${t.filter?.kql?.length>0?V` <button class=\"filter-cell__clear\" @click=${()=>this._handleKqlClear(t)} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"> <path d=\"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z\"/> </svg> </button> `:B} <button class=${we({\"filter-cell__advanced\":!0,\"filter-cell__advanced--opened\":this._filterPanelOpened===t.id})} @click=${e=>this._handleFilterPanelToggle(e,t)} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"> <path d=\"M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z\"/> </svg> </button> </div> </div> `:V`<div class=${we({\"filter-cell\":!0,\"filter-cell--sticky-left\":\"left\"===t.sticky,\"filter-cell--sticky-right\":\"right\"===t.sticky,\"filter-cell--sticky-right-first\":\"right\"===t.sticky&&!e.slice(0,i).some((e=>\"right\"===e.sticky))})} style=${Le(this._getCellStyle(t,i))} ></div>`))} </div> `:B}_renderTable(){return V` <div class=\"wrapper\"> <div class=\"overlay-left\"></div> <div class=\"overlay-right\"></div> ${this._renderStatus()} <div class=\"content\" @scroll=${this._handleScroll}> <div class=\"table\" style=\"grid-template-columns: ${this._getGridTemplateColumns()}\"> <div class=\"header-row\"> ${this.getDisplayedColumns().map(((e,t)=>V` <div class=${we(this._getHeaderCellClasses(e,t))} style=${Le(this._getCellStyle(e,t))} data-column-id=${e.id} >${e.label??e.id}${!1!==e.resizable?V`<div class=\"header-cell__resize\" @mousedown=${t=>this._handleResizeStart(t,e.id)} ></div>`:B}</div> `))} </div> ${this._renderFilterRow()} ${this._data.map(((e,t)=>{const i=this.getDisplayedColumns().map(((i,s)=>V` <div class=${we(this._getCellClasses(i,s))} style=${Le(this._getCellStyle(i,s))} data-column-id=${i.id} > ${this._renderCellContent(i,e,t)} </div> `));return this._model.rowHref?V` <a href=${this._model.rowHref(e)} class=${we({row:!0,\"row--clickable\":!0,\"row--link\":!0})} @mousedown=${()=>this._handleRowMouseDown()} @click=${()=>this._handleRowClick(e,t)} >${i}</a> `:V` <div class=${we({row:!0,\"row--clickable\":!!this._model.rowClickable})} @mousedown=${()=>this._handleRowMouseDown()} @click=${()=>this._handleRowClick(e,t)} >${i}</div> `}))} </div> </div> </div> `}render(){return this._model.columns.length?V` ${this._renderHeader()} ${this._renderTable()} ${this._renderFilterPanel()} `:V`<slot></slot>`}}"
1175
+ "default": "class extends le{constructor(){super(...arguments),this._scrollStyle=\"overlay\",this._data=[],this._dataState=\"idle\",this._page=1,this._pageSize=50,this._totalItems=0,this._totalPages=0,this._searchQuery=\"\",this._canScrollLeft=!1,this._canScrollRight=!1,this._canScrollHorizontal=!1,this._columnPickerOpen=!1,this._filterPanelOpened=null,this._filterPanelTab=\"filter\",this._buckets=new Map,this._filterPanelPos={top:0,left:0},this._sorts=[],this._resizing=null,this._resizeObserver=null,this._searchPositionLocked=!1,this._model=new ct,this.def={columns:[]},this._handleClickOutside=e=>{const t=e.composedPath();if(this._columnPickerOpen){const e=this.shadowRoot?.querySelector(\".column-picker-wrapper\");e&&!t.includes(e)&&(this._columnPickerOpen=!1)}this._filterPanelOpened&&(t.some((e=>e.classList?.contains(\"filter-panel\")))||this._handleFilterApply())},this._handleResizeMove=e=>{if(!this._resizing)return;const t=this._model.columns.find((e=>e.id===this._resizing.columnId));if(t){const i=this._resizing.startWidth+(e.clientX-this._resizing.startX);t.width=`${Math.min(900,Math.max(50,i))}px`,this.requestUpdate()}},this._handleResizeEnd=()=>{this._resizing=null,document.removeEventListener(\"mousemove\",this._handleResizeMove),document.removeEventListener(\"mouseup\",this._handleResizeEnd)}}connectedCallback(){super.connectedCallback(),this.classList.toggle(\"kr-table--scroll-overlay\",\"overlay\"===this._scrollStyle),this.classList.toggle(\"kr-table--scroll-edge\",\"edge\"===this._scrollStyle),this._fetch(),this._initRefresh(),document.addEventListener(\"click\",this._handleClickOutside),this._resizeObserver=new ResizeObserver((()=>{this._searchPositionLocked=!1,this._updateSearchPosition()})),this._resizeObserver.observe(this)}disconnectedCallback(){super.disconnectedCallback(),clearInterval(this._refreshTimer),document.removeEventListener(\"click\",this._handleClickOutside),this._resizeObserver?.disconnect()}willUpdate(e){e.has(\"def\")&&(this._model=new ct,this.def.title&&(this._model.title=this.def.title),this.def.actions&&(this._model.actions=this.def.actions),this.def.data&&(this._model.data=this.def.data),this.def.dataSource&&(this._model.dataSource=this.def.dataSource),\"number\"==typeof this.def.refreshInterval&&(this._model.refreshInterval=this.def.refreshInterval),\"number\"==typeof this.def.pageSize&&(this._model.pageSize=this.def.pageSize),this.def.rowClickable&&(this._model.rowClickable=this.def.rowClickable),this.def.rowHref&&(this._model.rowHref=this.def.rowHref),this._model.columns=this.def.columns.map((e=>{const t={...e,filter:null};return t.type||(t.type=\"string\"),\"actions\"===t.type?(t.label=e.label??\"\",t.sticky=\"right\",t.resizable=!1,t):((e.filterable||e.facetable)&&(t.filter=new dt,t.filter.field=e.id,t.filter.type=t.type,e.filter?(t.filter.setOperator(e.filter.operator),t.filter.setValue(e.filter.value)):e.facetable&&!e.filterable?(t.filter.operator=\"in\",t.filter.value=[]):\"string\"===t.filter.type&&(t.filter.operator=\"contains\")),t)})),this.def.displayedColumns?this._model.displayedColumns=this.def.displayedColumns:this._model.displayedColumns=this._model.columns.map((e=>e.id)),this._fetch(),this._initRefresh())}updated(e){this._updateScrollFlags(),this._syncSlottedContent()}_syncSlottedContent(){const e=this.getDisplayedColumns().filter((e=>e.render));e.length&&(this.querySelectorAll('[slot^=\"cell-\"]').forEach((e=>e.remove())),this._data.forEach(((t,i)=>{e.forEach((e=>{const s=e.render(t);if(!s)return;const o=document.createElement(\"span\");o.slot=`cell-${i}-${e.id}`,\"actions\"===e.type&&(o.style.display=\"flex\",o.style.gap=\"8px\"),\"string\"==typeof s?o.innerHTML=s:oe(s,o),this.appendChild(o)}))})))}refresh(){this._fetch()}goToPrevPage(){this._page>1&&(this._page--,this._fetch())}goToNextPage(){this._page<this._totalPages&&(this._page++,this._fetch())}goToPage(e){e>=1&&e<=this._totalPages&&(this._page=e,this._fetch())}_toSolrData(){const e={page:this._page-1,size:this._pageSize,sorts:this._sorts,filterFields:[],queryFields:[],facetFields:[]};for(const t of this._model.columns){if(!t.filter||t.filter.isEmpty()||!t.filter.isValid())continue;const i=t.filter.toSolrData();t.facetable&&\"in\"===t.filter.operator&&(i.tagged=!0),e.filterFields.push(i)}for(const t of this._model.columns)t.facetable&&e.facetFields.push({name:t.id,type:\"FIELD\",limit:100,sort:\"count\",minimumCount:1});return this._searchQuery?.trim().length&&e.queryFields.push({name:\"_text_\",operation:\"IS\",value:ot(this._searchQuery,!1)}),e}_toDbParams(){const e={page:this._page-1,size:this._pageSize,sorts:this._sorts,filterFields:[],queryFields:[],facetFields:[]};for(const t of this._model.columns)t.filter&&!t.filter.isEmpty()&&t.filter.isValid()&&e.filterFields.push(t.filter.toDbParams());return this._searchQuery?.trim().length&&this._model.columns.filter((e=>e.searchable)).forEach((t=>{e.queryFields.push({name:t.id,operation:\"CONTAINS\",value:this._searchQuery,and:!1})})),e}_fetch(){if(this._model.data)return this._data=this._model.data,this._totalItems=this._model.data.length,this._totalPages=Math.ceil(this._model.data.length/this._pageSize),void(this._dataState=\"success\");if(!this._model.dataSource)return;let e;this._dataState=\"loading\",e=\"db\"===this._model.dataSource.mode?this._toDbParams():this._toSolrData(),this._model.dataSource.fetch(e).then((e=>{switch(this._model.dataSource?.mode){case\"opensearch\":throw Error(\"Opensearch not supported yet\");case\"db\":{const t=e;this._data=t.data.content,this._totalItems=t.data.totalElements,this._totalPages=t.data.totalPages,this._pageSize=t.data.size;break}default:{const t=e;this._data=t.data.content,this._totalItems=t.data.totalElements,this._totalPages=t.data.totalPages,this._pageSize=t.data.size,this._parseFacetResults(t)}}this._dataState=\"success\",this._updateSearchPosition()})).catch((e=>{this._dataState=\"error\",Fe.show({message:e instanceof Error?e.message:\"Failed to load data\",type:\"error\"})}))}_parseFacetResults(e){if(e.data.facetFields){for(const t of this._model.columns){if(!t.facetable)continue;const i=e.data.facetFields[t.id];if(!i){this._buckets.set(t.id,[]);continue}const s=[];for(const e of i){let i=e.name;\"boolean\"===t.type&&\"string\"==typeof e.name&&(\"true\"===e.name?i=!0:\"false\"===e.name&&(i=!1)),null===e.name&&e.count>0&&s.unshift({val:null,count:e.count}),null!==e.name&&s.push({val:i,count:e.count})}if(t.filter&&\"in\"===t.filter.operator&&Array.isArray(t.filter.value))for(const e of t.filter.value)s.some((t=>t.val===e))||s.push({val:e,count:0});this._buckets.set(t.id,s)}this._buckets=new Map(this._buckets)}}_initRefresh(){clearInterval(this._refreshTimer),this._model.refreshInterval&&this._model.refreshInterval>0&&(this._refreshTimer=window.setInterval((()=>{this._fetch()}),this._model.refreshInterval))}_handleSearch(e){const t=e.target;this._searchQuery=t.value,this._page=1,this._fetch()}_getGridTemplateColumns(){return this.getDisplayedColumns().map((e=>e.width?e.width:\"actions\"===e.type?\"max-content\":\"minmax(80px, auto)\")).join(\" \")}_updateSearchPosition(){if(this._searchPositionLocked)return;const e=this.shadowRoot?.querySelector(\".search\"),t=e?.querySelector(\".search-field\");e&&t&&(e.style.justifyContent=\"center\",t.style.marginLeft=\"\",requestAnimationFrame((()=>{const i=e.getBoundingClientRect(),s=t.getBoundingClientRect().left-i.left;e.style.justifyContent=\"flex-start\",t.style.marginLeft=`${s}px`,this._searchPositionLocked=!0})))}_toggleColumnPicker(){this._columnPickerOpen=!this._columnPickerOpen}_toggleColumn(e){this._model.displayedColumns.includes(e)?this._model.displayedColumns=this._model.displayedColumns.filter((t=>t!==e)):this._model.displayedColumns=[...this._model.displayedColumns,e],this.requestUpdate()}_handleRowMouseDown(){this._model.rowClickable&&window.getSelection()?.removeAllRanges()}_handleRowClick(e,t){if(!this._model.rowClickable)return;const i=window.getSelection();i&&i.toString().length>0||this.dispatchEvent(new CustomEvent(\"row-click\",{detail:{row:e,rowIndex:t},bubbles:!0,composed:!0}))}getDisplayedColumns(){return this._model.displayedColumns.map((e=>this._model.columns.find((t=>t.id===e)))).sort(((e,t)=>\"actions\"===e.type&&\"actions\"!==t.type?1:\"actions\"!==e.type&&\"actions\"===t.type?-1:0))}_handleScroll(e){const t=e.target;this._canScrollLeft=t.scrollLeft>0,this._canScrollRight=t.scrollLeft<t.scrollWidth-t.clientWidth-1}_updateScrollFlags(){const e=this.shadowRoot?.querySelector(\".content\");e&&(this._canScrollLeft=e.scrollLeft>0,this._canScrollRight=e.scrollWidth>e.clientWidth&&e.scrollLeft<e.scrollWidth-e.clientWidth-1,this._canScrollHorizontal=e.scrollWidth>e.clientWidth),this.classList.toggle(\"kr-table--scroll-left-available\",this._canScrollLeft),this.classList.toggle(\"kr-table--scroll-right-available\",this._canScrollRight),this.classList.toggle(\"kr-table--scroll-horizontal-available\",this._canScrollHorizontal),this.classList.toggle(\"kr-table--sticky-left\",this.getDisplayedColumns().some((e=>\"left\"===e.sticky))),this.classList.toggle(\"kr-table--sticky-right\",this.getDisplayedColumns().some((e=>\"right\"===e.sticky)))}_handleResizeStart(e,t){e.preventDefault();const i=this.shadowRoot?.querySelector(`.header-cell[data-column-id=\"${t}\"]`);this._resizing={columnId:t,startX:e.clientX,startWidth:i?.offsetWidth||200},document.addEventListener(\"mousemove\",this._handleResizeMove),document.addEventListener(\"mouseup\",this._handleResizeEnd)}_handleSortClick(e,t){if(e.shiftKey){const e=this._sorts.findIndex((e=>e.sortBy===t.id));if(-1===e)this._sorts.push({sortBy:t.id,sortDirection:\"asc\"});else{const t=this._sorts[e];\"asc\"===t.sortDirection?t.sortDirection=\"desc\":this._sorts.splice(e,1)}this.requestUpdate()}else{let e=null;1===this._sorts.length&&(e=this._sorts.find((e=>e.sortBy===t.id))),e?\"asc\"===e.sortDirection?this._sorts=[{sortBy:t.id,sortDirection:\"desc\"}]:this._sorts=[]:this._sorts=[{sortBy:t.id,sortDirection:\"asc\"}]}this._page=1,this._fetch()}_renderSortIndicator(e){if(!e.sortable)return U;const t=this._sorts.findIndex((t=>t.sortBy===e.id));if(-1===t)return V` <span class=\"header-cell__sort\" @click=${t=>this._handleSortClick(t,e)}> <svg class=\"header-cell__sort-arrow header-cell__sort-arrow--ghost\" viewBox=\"0 0 24 24\" fill=\"currentColor\"> <path d=\"M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z\"/> </svg> </span> `;let i={};return\"desc\"===this._sorts[t].sortDirection&&(i={transform:\"rotate(180deg)\"}),V` <span class=\"header-cell__sort\" @click=${t=>this._handleSortClick(t,e)}> <svg class=\"header-cell__sort-arrow\" viewBox=\"0 0 24 24\" fill=\"currentColor\" style=${qe(i)}> <path d=\"M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z\"/> </svg> ${this._sorts.length>1?V` <span class=\"header-cell__sort-priority\">${t+1}</span> `:U} </span> `}_handleAction(e){e.href||this.dispatchEvent(new CustomEvent(\"action\",{detail:{action:e.id},bubbles:!0,composed:!0}))}_handleKqlChange(e,t){const i=e.target.value.trim();if(i){if(t.filter.setKql(i),this.requestUpdate(),!t.filter.isValid())return}else t.filter.clear(),this.requestUpdate();this._page=1,this._fetch()}_handleFilterPanelToggle(e,t){if(e.stopPropagation(),this._filterPanelOpened===t.id)this._filterPanelOpened=null;else{const i=e.currentTarget.getBoundingClientRect();let s=i.left;s+328>window.innerWidth&&(s=window.innerWidth-328),this._filterPanelPos={top:i.bottom+4,left:s},this._filterPanelOpened=t.id,t.facetable?this._filterPanelTab=\"counts\":this._filterPanelTab=\"filter\"}}_handleKqlClear(e){e.filter.clear(),this._page=1,this._fetch()}_handleFilterClear(){const e=this._model.columns.find((e=>e.id===this._filterPanelOpened));e&&(e.filter.clear(),e.facetable&&!e.filterable&&(e.filter.operator=\"in\",e.filter.value=[])),this._filterPanelOpened=null,this._page=1,this._fetch()}_handleFilterTextKeydown(e,t){\"Enter\"===e.key&&(e.preventDefault(),this._handleFilterApply())}_handleOperatorChange(e,t){t.filter.setOperator(e.target.value),this.requestUpdate()}_handleFilterStringChange(e,t){t.filter.setValue(e.target.value),this.requestUpdate()}_handleFilterNumberChange(e,t){t.filter.setValue(Number(e.target.value)),this.requestUpdate()}_handleFilterDateChange(e,t){t.filter.setValue(new Date(e.target.value),\"day\"),this.requestUpdate()}_handleFilterBooleanChange(e,t){t.filter.setValue(\"true\"===e.target.value),this.requestUpdate()}_handleFilterDateStartChange(e,t){t.filter.setStart(new Date(e.target.value),\"day\"),this.requestUpdate()}_handleFilterDateEndChange(e,t){t.filter.setEnd(new Date(e.target.value),\"day\"),this.requestUpdate()}_handleFilterNumberStartChange(e,t){t.filter.setStart(Number(e.target.value)),this.requestUpdate()}_handleFilterNumberEndChange(e,t){t.filter.setEnd(Number(e.target.value)),this.requestUpdate()}_handleFilterListChange(e,t){const i=e.target.value.split(\",\").map((e=>e.trim())).filter((e=>\"\"!==e));\"number\"===t.type?t.filter.setValue(i.map((e=>Number(e)))):t.filter.setValue(i),this.requestUpdate()}_handleFilterApply(){this._filterPanelOpened=null,this._page=1,this._fetch()}_handleFilterPanelTabChange(e){this._filterPanelTab=e.detail.activeTabId}_handleBucketToggle(e,t,i){t.filter.toggle(i.val),this._page=1,this._fetch()}_renderCellContent(e,t,i){const s=t[e.id];if(e.render)return V`<slot name=\"cell-${i}-${e.id}\"></slot>`;if(null==s)return\"\";switch(e.type){case\"number\":return\"currency\"===e.format&&\"number\"==typeof s?s.toLocaleString(\"en-US\",{style:\"currency\",currency:\"USD\"}):String(s);case\"date\":{let e;if(s instanceof Date)e=s;else if(\"string\"==typeof s&&/^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}/.test(s)){const t=s.replace(/(\\d{2}:\\d{2}:\\d{2}):(\\d+)$/,\"$1.$2\").replace(\" \",\"T\")+\"Z\";e=new Date(t)}else e=new Date(s);return e.toLocaleString(void 0,{year:\"numeric\",month:\"short\",day:\"numeric\",hour:\"numeric\",minute:\"2-digit\",timeZone:\"UTC\"})}case\"boolean\":return!0===s?\"Yes\":!1===s?\"No\":\"\";default:return String(s)}}_getHeaderCellClasses(e,t){return{\"header-cell\":!0,\"header-cell--sortable\":!!e.sortable,\"header-cell--align-center\":\"center\"===e.align,\"header-cell--align-right\":\"right\"===e.align,\"header-cell--sticky-left\":\"left\"===e.sticky,\"header-cell--sticky-left-last\":\"left\"===e.sticky&&!this.getDisplayedColumns().slice(t+1).some((e=>\"left\"===e.sticky)),\"header-cell--sticky-right\":\"right\"===e.sticky,\"header-cell--sticky-right-first\":\"right\"===e.sticky&&!this.getDisplayedColumns().slice(0,t).some((e=>\"right\"===e.sticky))}}_getCellClasses(e,t){return{cell:!0,\"cell--actions\":\"actions\"===e.type,\"cell--align-center\":\"center\"===e.align,\"cell--align-right\":\"right\"===e.align,\"cell--sticky-left\":\"left\"===e.sticky,\"cell--sticky-left-last\":\"left\"===e.sticky&&!this.getDisplayedColumns().slice(t+1).some((e=>\"left\"===e.sticky)),\"cell--sticky-right\":\"right\"===e.sticky,\"cell--sticky-right-first\":\"right\"===e.sticky&&!this.getDisplayedColumns().slice(0,t).some((e=>\"right\"===e.sticky))}}_getCellStyle(e,t){const i={};if(\"left\"===e.sticky){let e=0;for(let i=0;i<t;i++){const t=this.getDisplayedColumns()[i];\"left\"===t.sticky&&(e+=parseInt(t.width||\"0\",10))}i.left=`${e}px`}if(\"right\"===e.sticky){let e=0;for(let i=t+1;i<this.getDisplayedColumns().length;i++){const t=this.getDisplayedColumns()[i];\"right\"===t.sticky&&(e+=parseInt(t.width||\"0\",10))}i.right=`${e}px`}return i}_renderPagination(){const e=(this._page-1)*this._pageSize+1,t=Math.min(this._page*this._pageSize,this._totalItems);return V` <div class=\"pagination\"> <span class=\"pagination-icon ${1===this._page?\"pagination-icon--disabled\":\"\"}\" @click=${this.goToPrevPage} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z\"/></svg> </span> <span class=\"pagination-info\">${e}-${t} of ${this._totalItems}</span> <span class=\"pagination-icon ${this._page===this._totalPages?\"pagination-icon--disabled\":\"\"}\" @click=${this.goToNextPage} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\"/></svg> </span> </div> `}_renderHeader(){return!this._model.title&&!this._model.actions?.length&&this._totalPages<=1?U:V` <div class=\"header\"> <div class=\"title\">${this._model.title??\"\"}</div> ${\"db\"!==this._model.dataSource?.mode||this._model.columns.some((e=>e.searchable))?V` <div class=\"search\"> <!-- TODO: Saved views dropdown <div class=\"views\"> <span>Default View</span> <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z\"/></svg> </div> --> <div class=\"search-field\"> <svg class=\"search-icon\" viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z\"/></svg> <input type=\"text\" class=\"search-input\" placeholder=\"Search...\" .value=${this._searchQuery} @input=${this._handleSearch} /> </div> </div> `:V`<div class=\"search\"></div>`} <div class=\"tools\"> ${this._renderPagination()} <span class=\"refresh\" title=\"Refresh\" @click=${()=>this.refresh()}> <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M480-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100 70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z\"/></svg> </span> <div class=\"column-picker-wrapper\"> <span class=\"header-icon\" title=\"Columns\" @click=${this._toggleColumnPicker}> <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M121-280v-400q0-33 23.5-56.5T201-760h559q33 0 56.5 23.5T840-680v400q0 33-23.5 56.5T760-200H201q-33 0-56.5-23.5T121-280Zm79 0h133v-400H200v400Zm213 0h133v-400H413v400Zm213 0h133v-400H626v400Z\"/></svg> </span> <div class=\"column-picker ${this._columnPickerOpen?\"open\":\"\"}\"> ${[...this._model.columns].filter((e=>\"actions\"!==e.type)).sort(((e,t)=>(e.label??e.id).localeCompare(t.label??t.id))).map((e=>V` <div class=\"column-picker-item\" @click=${()=>this._toggleColumn(e.id)}> <div class=\"column-picker-checkbox ${this._model.displayedColumns.includes(e.id)?\"checked\":\"\"}\"> <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z\"/></svg> </div> <span class=\"column-picker-label\">${e.label??e.id}</span> </div> `))} </div> </div> ${1===this._model.actions?.length?V` <kr-button class=\"actions\" .href=${this._model.actions[0].href} .target=${this._model.actions[0].target} @click=${()=>this._handleAction(this._model.actions[0])} > ${this._model.actions[0].label} </kr-button> `:this._model.actions?.length?V` <kr-button class=\"actions\" .options=${this._model.actions.map((e=>({id:e.id,label:e.label})))} @option-select=${e=>this._handleAction({id:e.detail.id,label:e.detail.label})} > Actions </kr-button> `:U} </div> </div> `}_renderStatus(){return\"loading\"===this._dataState&&0===this._data.length?V`<div class=\"status\">Loading...</div>`:\"error\"===this._dataState&&0===this._data.length?V`<div class=\"status status--error\">Error loading data</div>`:0===this._data.length?V`<div class=\"status\">No data available</div>`:U}_renderFilterPanel(){if(!this._filterPanelOpened)return U;const e=this._model.columns.find((e=>e.id===this._filterPanelOpened));let t=V``;t=\"empty\"===e.filter.operator||\"n_empty\"===e.filter.operator?V` <input type=\"text\" class=\"filter-panel__input\" disabled .value=${e.filter.text} /> `:\"between\"===e.filter.operator&&\"date\"===e.type?V` <input type=\"date\" class=\"filter-panel__input\" .valueAsDate=${e.filter.value?.start??null} @change=${t=>this._handleFilterDateStartChange(t,e)} /> <input type=\"date\" class=\"filter-panel__input\" .valueAsDate=${e.filter.value?.end??null} @change=${t=>this._handleFilterDateEndChange(t,e)} /> `:\"between\"===e.filter.operator&&\"number\"===e.type?V` <input type=\"number\" class=\"filter-panel__input\" placeholder=\"Start\" .value=${e.filter.value?.start??\"\"} @input=${t=>this._handleFilterNumberStartChange(t,e)} @keydown=${t=>this._handleFilterTextKeydown(t,e)} /> <input type=\"number\" class=\"filter-panel__input\" placeholder=\"End\" .value=${e.filter.value?.end??\"\"} @input=${t=>this._handleFilterNumberEndChange(t,e)} @keydown=${t=>this._handleFilterTextKeydown(t,e)} /> `:\"in\"===e.filter.operator?V` <textarea class=\"filter-panel__textarea\" rows=\"3\" placeholder=\"Values (comma-separated)\" .value=${e.filter.text} @input=${t=>this._handleFilterListChange(t,e)} @keydown=${t=>this._handleFilterTextKeydown(t,e)} ></textarea> `:\"boolean\"===e.type?V` <kr-select-field placeholder=\"Value\" .value=${String(e.filter.value??\"\")} @change=${t=>this._handleFilterBooleanChange(t,e)} > <kr-select-option value=\"true\">Yes</kr-select-option> <kr-select-option value=\"false\">No</kr-select-option> </kr-select-field> `:\"date\"===e.type?V` <input type=\"date\" class=\"filter-panel__input\" .valueAsDate=${e.filter.value} @change=${t=>this._handleFilterDateChange(t,e)} /> `:\"number\"===e.type?V` <input type=\"number\" class=\"filter-panel__input\" placeholder=\"Value\" min=\"0\" .value=${e.filter.text} @input=${t=>this._handleFilterNumberChange(t,e)} @keydown=${t=>this._handleFilterTextKeydown(t,e)} /> `:V` <input type=\"text\" class=\"filter-panel__input\" placeholder=\"Value\" .value=${e.filter.text} @input=${t=>this._handleFilterStringChange(t,e)} @keydown=${t=>this._handleFilterTextKeydown(t,e)} /> `;const i=V` <div class=\"filter-panel__content\"> <kr-select-field .value=${e.filter.operator} @change=${t=>this._handleOperatorChange(t,e)} > ${tt(e.type).map((e=>V` <kr-select-option value=${e.key}>${e.label}</kr-select-option> `))} </kr-select-field> ${t} </div> `,s=this._buckets.get(e.id)||[];let o,r;return o=s.length?V` <div class=\"buckets\"> ${s.map((t=>{let i=\"(Empty)\";null!==t.val&&void 0!==t.val&&(i=\"boolean\"===e.type?!0===t.val||\"true\"===t.val?\"Yes\":\"No\":String(t.val));let s=U;return e.filter.has(t.val)&&(s=V` <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"> <path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z\"/> </svg> `),V` <div class=\"bucket\" @click=${i=>this._handleBucketToggle(i,e,t)} > <div class=${we({bucket__checkbox:!0,\"bucket__checkbox--checked\":e.filter.has(t.val)})}> ${s} </div> <span class=\"bucket__label\">${i}</span> <span class=\"bucket__count\">${t.count}</span> </div> `}))} </div> `:V`<div class=\"bucket-empty\">No data</div>`,r=e.facetable&&e.filterable?V` <kr-tab-group size=\"small\" active-tab-id=${this._filterPanelTab} @tab-change=${e=>this._handleFilterPanelTabChange(e)} > <kr-tab id=\"filter\" label=\"Filter\"> ${i} </kr-tab> <kr-tab id=\"counts\" label=\"Counts\"> ${o} </kr-tab> </kr-tab-group> `:e.facetable?o:i,V` <div class=\"filter-panel\" style=${qe({top:this._filterPanelPos.top+\"px\",left:this._filterPanelPos.left+\"px\"})} > ${r} <div class=\"filter-panel__actions\"> <kr-button variant=\"outline\" color=\"secondary\" size=\"small\" @click=${this._handleFilterClear}> Clear </kr-button> <kr-button size=\"small\" @click=${this._handleFilterApply}> Apply </kr-button> </div> </div> `}_renderFilterRow(){const e=this.getDisplayedColumns();return e.some((e=>e.filterable||e.facetable))?V` <div class=\"filter-row\"> ${e.map(((t,i)=>t.filterable||t.facetable?V` <div class=${we({\"filter-cell\":!0,\"filter-cell--sticky-left\":\"left\"===t.sticky,\"filter-cell--sticky-right\":\"right\"===t.sticky,\"filter-cell--sticky-right-first\":\"right\"===t.sticky&&!e.slice(0,i).some((e=>\"right\"===e.sticky))})} style=${qe(this._getCellStyle(t,i))} > <div class=\"filter-cell__wrapper\"> <input type=\"text\" class=${we({\"filter-cell__input\":!0,\"filter-cell__input--invalid\":!t.filter.isValid()})} .value=${t.filter.kql} @change=${e=>this._handleKqlChange(e,t)} /> ${t.filter?.kql?.length>0?V` <button class=\"filter-cell__clear\" @click=${()=>this._handleKqlClear(t)} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"> <path d=\"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z\"/> </svg> </button> `:U} <button class=${we({\"filter-cell__advanced\":!0,\"filter-cell__advanced--opened\":this._filterPanelOpened===t.id})} @click=${e=>this._handleFilterPanelToggle(e,t)} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"> <path d=\"M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z\"/> </svg> </button> </div> </div> `:V`<div class=${we({\"filter-cell\":!0,\"filter-cell--sticky-left\":\"left\"===t.sticky,\"filter-cell--sticky-right\":\"right\"===t.sticky,\"filter-cell--sticky-right-first\":\"right\"===t.sticky&&!e.slice(0,i).some((e=>\"right\"===e.sticky))})} style=${qe(this._getCellStyle(t,i))} ></div>`))} </div> `:U}_renderTable(){return V` <div class=\"wrapper\"> <div class=\"overlay-left\"></div> <div class=\"overlay-right\"></div> ${this._renderStatus()} <div class=\"content\" @scroll=${this._handleScroll}> <div class=\"table\" style=\"grid-template-columns: ${this._getGridTemplateColumns()}\"> <div class=\"header-row\"> ${this.getDisplayedColumns().map(((e,t)=>V` <div class=${we(this._getHeaderCellClasses(e,t))} style=${qe(this._getCellStyle(e,t))} data-column-id=${e.id} > <span class=\"header-cell__label\">${e.label??e.id}</span> ${this._renderSortIndicator(e)} ${!1!==e.resizable?V`<div class=\"header-cell__resize\" @mousedown=${t=>this._handleResizeStart(t,e.id)} ></div>`:U} </div> `))} </div> ${this._renderFilterRow()} ${this._data.map(((e,t)=>{const i=this.getDisplayedColumns().map(((i,s)=>V` <div class=${we(this._getCellClasses(i,s))} style=${qe(this._getCellStyle(i,s))} data-column-id=${i.id} > ${this._renderCellContent(i,e,t)} </div> `));return this._model.rowHref?V` <a href=${this._model.rowHref(e)} class=${we({row:!0,\"row--clickable\":!0,\"row--link\":!0})} @mousedown=${()=>this._handleRowMouseDown()} @click=${()=>this._handleRowClick(e,t)} >${i}</a> `:V` <div class=${we({row:!0,\"row--clickable\":!!this._model.rowClickable})} @mousedown=${()=>this._handleRowMouseDown()} @click=${()=>this._handleRowClick(e,t)} >${i}</div> `}))} </div> </div> </div> `}render(){return this._model.columns.length?V` ${this._renderHeader()} ${this._renderTable()} ${this._renderFilterPanel()} `:V`<slot></slot>`}}"
1176
1176
  },
1177
1177
  {
1178
1178
  "kind": "variable",
@@ -1201,17 +1201,17 @@
1201
1201
  {
1202
1202
  "kind": "variable",
1203
1203
  "name": "zt",
1204
- "default": "class extends le{constructor(){super(),this.label=\"\",this.name=\"\",this.value=\"\",this.placeholder=\"\",this.required=!1,this.disabled=!1,this.readonly=!1,this.rows=3,this.autocomplete=\"\",this.hint=\"\",this._touched=!1,this._dirty=!1,this._handleInvalid=e=>{e.preventDefault(),this._touched=!0},this._internals=this.attachInternals()}get form(){return this._internals.form}get validity(){return this._internals.validity}get validationMessage(){return this._internals.validationMessage}get willValidate(){return this._internals.willValidate}checkValidity(){return this._internals.checkValidity()}reportValidity(){return this._internals.reportValidity()}formResetCallback(){this.value=\"\",this._touched=!1,this._dirty=!1,this._internals.setFormValue(\"\"),this._internals.setValidity({})}formStateRestoreCallback(e){this.value=e}connectedCallback(){super.connectedCallback(),this.addEventListener(\"invalid\",this._handleInvalid)}disconnectedCallback(){super.disconnectedCallback(),this.removeEventListener(\"invalid\",this._handleInvalid)}firstUpdated(){this._updateValidity()}updated(e){(e.has(\"required\")||e.has(\"value\"))&&this._updateValidity()}_updateValidity(){this._textarea&&this._internals.setValidity(this._textarea.validity,this._textarea.validationMessage)}_handleInput(e){this.value=e.target.value,this._dirty=!0,this._internals.setFormValue(this.value),this._internals.setValidity(this._textarea.validity,this._textarea.validationMessage)}_handleChange(e){this.value=e.target.value,this._internals.setFormValue(this.value)}_handleBlur(){this._touched=!0,this._internals.setValidity(this._textarea.validity,this._textarea.validationMessage)}render(){let e=\"\";return this._touched&&this._textarea&&!this._textarea.validity.valid&&(e=this._textarea.validationMessage),V` <div class=\"wrapper\"> ${this.label?V` <label for=\"textarea\"> ${this.label} ${this.required?V`<span class=\"required\" aria-hidden=\"true\">*</span>`:B} </label> `:B} <textarea id=\"textarea\" class=${we({\"textarea--invalid\":this._touched&&this._textarea&&!this._textarea.validity.valid})} name=${this.name} .value=${$t(this.value)} placeholder=${this.placeholder} ?required=${this.required} ?disabled=${this.disabled} ?readonly=${this.readonly} rows=${this.rows} cols=${kt(this.cols)} minlength=${kt(this.minlength)} maxlength=${kt(this.maxlength)} autocomplete=${kt(this.autocomplete||void 0)} @input=${this._handleInput} @change=${this._handleChange} @blur=${this._handleBlur} ></textarea> ${e?V`<div class=\"validation-message\">${e}</div>`:this.hint?V`<div class=\"hint\">${this.hint}</div>`:B} </div> `}focus(){this._textarea?.focus()}blur(){this._textarea?.blur()}select(){this._textarea?.select()}}"
1204
+ "default": "class extends le{constructor(){super(),this.label=\"\",this.name=\"\",this.value=\"\",this.placeholder=\"\",this.required=!1,this.disabled=!1,this.readonly=!1,this.rows=3,this.autocomplete=\"\",this.hint=\"\",this._touched=!1,this._dirty=!1,this._handleInvalid=e=>{e.preventDefault(),this._touched=!0},this._internals=this.attachInternals()}get form(){return this._internals.form}get validity(){return this._internals.validity}get validationMessage(){return this._internals.validationMessage}get willValidate(){return this._internals.willValidate}checkValidity(){return this._internals.checkValidity()}reportValidity(){return this._internals.reportValidity()}formResetCallback(){this.value=\"\",this._touched=!1,this._dirty=!1,this._internals.setFormValue(\"\"),this._internals.setValidity({})}formStateRestoreCallback(e){this.value=e}connectedCallback(){super.connectedCallback(),this.addEventListener(\"invalid\",this._handleInvalid)}disconnectedCallback(){super.disconnectedCallback(),this.removeEventListener(\"invalid\",this._handleInvalid)}firstUpdated(){this._updateValidity()}updated(e){(e.has(\"required\")||e.has(\"value\"))&&this._updateValidity()}_updateValidity(){this._textarea&&this._internals.setValidity(this._textarea.validity,this._textarea.validationMessage)}_handleInput(e){this.value=e.target.value,this._dirty=!0,this._internals.setFormValue(this.value),this._internals.setValidity(this._textarea.validity,this._textarea.validationMessage)}_handleChange(e){this.value=e.target.value,this._internals.setFormValue(this.value)}_handleBlur(){this._touched=!0,this._internals.setValidity(this._textarea.validity,this._textarea.validationMessage)}render(){let e=\"\";return this._touched&&this._textarea&&!this._textarea.validity.valid&&(e=this._textarea.validationMessage),V` <div class=\"wrapper\"> ${this.label?V` <label for=\"textarea\"> ${this.label} ${this.required?V`<span class=\"required\" aria-hidden=\"true\">*</span>`:U} </label> `:U} <textarea id=\"textarea\" class=${we({\"textarea--invalid\":this._touched&&this._textarea&&!this._textarea.validity.valid})} name=${this.name} .value=${$t(this.value)} placeholder=${this.placeholder} ?required=${this.required} ?disabled=${this.disabled} ?readonly=${this.readonly} rows=${this.rows} cols=${kt(this.cols)} minlength=${kt(this.minlength)} maxlength=${kt(this.maxlength)} autocomplete=${kt(this.autocomplete||void 0)} @input=${this._handleInput} @change=${this._handleChange} @blur=${this._handleBlur} ></textarea> ${e?V`<div class=\"validation-message\">${e}</div>`:this.hint?V`<div class=\"hint\">${this.hint}</div>`:U} </div> `}focus(){this._textarea?.focus()}blur(){this._textarea?.blur()}select(){this._textarea?.select()}}"
1205
1205
  },
1206
1206
  {
1207
1207
  "kind": "variable",
1208
1208
  "name": "At",
1209
- "default": "class extends le{constructor(){super(),this._requestId=0,this._handleDocumentClick=e=>{e.composedPath().includes(this)||(this._isOpen=!1)},this.label=\"\",this.name=\"\",this.value=\"\",this.placeholder=\"\",this.disabled=!1,this.readonly=!1,this.required=!1,this.hint=\"\",this.options=[],this.fetch=null,this._isOpen=!1,this._highlightedIndex=-1,this._touched=!1,this._handleInvalid=e=>{e.preventDefault(),this._touched=!0},this._internals=this.attachInternals()}connectedCallback(){super.connectedCallback(),document.addEventListener(\"click\",this._handleDocumentClick),this.addEventListener(\"invalid\",this._handleInvalid)}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener(\"click\",this._handleDocumentClick),this.removeEventListener(\"invalid\",this._handleInvalid)}firstUpdated(){this._updateValidity()}updated(e){(e.has(\"required\")||e.has(\"value\"))&&this._updateValidity(),e.has(\"options\")&&this._isOpen&&this._positionDropdown()}get form(){return this._internals.form}get validity(){return this._internals.validity}get validationMessage(){return this._internals.validationMessage}checkValidity(){return this._internals.checkValidity()}reportValidity(){return this._internals.reportValidity()}formResetCallback(){this.value=\"\",this._touched=!1,this._isOpen=!1,this._highlightedIndex=-1,this._internals.setFormValue(\"\"),this._internals.setValidity({})}formStateRestoreCallback(e){this.value=e}_updateValidity(){this._input&&this._internals.setValidity(this._input.validity,this._input.validationMessage)}_fetch(){if(!this.fetch)return;this._requestId++;const e=this._requestId;this.fetch(this.value).then((t=>{e===this._requestId&&(this.options=t)})).catch((e=>{console.error(\"kr-auto-suggest: fetch failed\",e)}))}_handleInput(e){this.value=e.target.value,this._isOpen=!0,this._highlightedIndex=-1,this._positionDropdown(),this._internals.setFormValue(this.value),this._internals.setValidity(this._input.validity,this._input.validationMessage),this.dispatchEvent(new Event(\"change\",{bubbles:!0,composed:!0})),this._fetch()}_positionDropdown(){requestAnimationFrame((()=>{const e=this.shadowRoot?.querySelector(\".dropdown\");if(!e)return;const t=this._input.getBoundingClientRect(),i=window.innerHeight-t.bottom-4-8,s=t.top-4-8;e.style.left=t.left+\"px\",e.style.width=t.width+\"px\",s>i?(e.style.top=\"\",e.style.bottom=window.innerHeight-t.top+4+\"px\",e.style.maxHeight=s+\"px\"):(e.style.bottom=\"\",e.style.top=t.bottom+4+\"px\",e.style.maxHeight=i+\"px\")}))}_handleFocus(){this._isOpen=!0,this._positionDropdown(),this._fetch()}_handleBlur(){this._touched=!0,this._internals.setValidity(this._input.validity,this._input.validationMessage),setTimeout((()=>{this._isOpen=!1,this._highlightedIndex=-1}),200)}_handleKeyDown(e){switch(e.key){case\"ArrowDown\":e.preventDefault(),this._isOpen=!0,this._highlightedIndex=Math.min(this._highlightedIndex+1,this.options.length-1),this._scrollToHighlighted();break;case\"ArrowUp\":e.preventDefault(),-1===this._highlightedIndex?(this._isOpen=!0,this._highlightedIndex=this.options.length-1):this._highlightedIndex=Math.max(this._highlightedIndex-1,0),this._scrollToHighlighted();break;case\"Enter\":this._highlightedIndex>=0&&this.options[this._highlightedIndex]&&(e.preventDefault(),this._selectOption(this.options[this._highlightedIndex]));break;case\"Escape\":e.preventDefault(),this._isOpen=!1,this._highlightedIndex=-1;break;case\"Tab\":this._isOpen=!1,this._highlightedIndex=-1}}_scrollToHighlighted(){this.updateComplete.then((()=>{const e=this.shadowRoot?.querySelector(\".dropdown\"),t=this.shadowRoot?.querySelector(\".option--highlighted\");if(e&&t){const i=e.getBoundingClientRect(),s=t.getBoundingClientRect();(s.bottom>i.bottom||s.top<i.top)&&t.scrollIntoView({block:\"nearest\"})}}))}_selectOption(e){e.disabled||(this.value=e.value,this._isOpen=!1,this._highlightedIndex=-1,this._internals.setFormValue(this.value),this.dispatchEvent(new CustomEvent(\"select\",{detail:{value:e.value,option:e},bubbles:!0,composed:!0})),this.dispatchEvent(new Event(\"change\",{bubbles:!0,composed:!0})))}_handleClear(){this.value=\"\",this._internals.setFormValue(this.value),this.dispatchEvent(new Event(\"change\",{bubbles:!0,composed:!0})),this._input?.focus()}_handleOptionMouseEnter(e,t){this._highlightedIndex=t}_renderOption(e,t){return V` <button class=${we({option:!0,\"option--highlighted\":t===this._highlightedIndex})} type=\"button\" role=\"option\" aria-selected=${t===this._highlightedIndex} ?disabled=${e.disabled} @click=${t=>this._selectOption(e)} @mouseenter=${e=>this._handleOptionMouseEnter(e,t)} > ${e.label||e.value} </button> `}render(){let e=B;return this._touched&&this._input&&!this._input.validity.valid?e=V`<div class=\"validation-message\">${this._input.validationMessage}</div>`:this.hint&&(e=V`<div class=\"hint\">${this.hint}</div>`),V` <div class=\"wrapper\"> ${this.label?V` <label> ${this.label} ${this.required?V`<span class=\"required\">*</span>`:B} </label> `:B} <div class=\"input-wrapper\"> <input type=\"text\" .value=${$t(this.value)} placeholder=${this.placeholder} ?disabled=${this.disabled} ?readonly=${this.readonly} ?required=${this.required} name=${this.name} autocomplete=\"off\" role=\"combobox\" aria-autocomplete=\"list\" aria-expanded=${this._isOpen} aria-controls=\"dropdown\" class=${we({\"input--invalid\":this._touched&&this._input&&!this._input.validity.valid})} @input=${this._handleInput} @blur=${this._handleBlur} @focus=${this._handleFocus} @keydown=${this._handleKeyDown} /> <div class=\"icon-wrapper\"> ${!this.value||this.disabled||this.readonly?B:V` <svg class=\"clear\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-label=\"Clear\" @click=${this._handleClear}> <path d=\"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z\"/> </svg> `} ${this.value||this.disabled||this.readonly?B:V` <svg class=\"search-icon\" viewBox=\"0 0 24 24\" fill=\"currentColor\"> <path d=\"M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z\"/> </svg> `} </div> </div> <div id=\"dropdown\" role=\"listbox\" class=${we({dropdown:!0,\"dropdown--open\":this._isOpen&&this.options.length>0})} > ${this.options.map(((e,t)=>this._renderOption(e,t)))} </div> ${e} </div> `}}"
1209
+ "default": "class extends le{constructor(){super(),this._requestId=0,this._handleDocumentClick=e=>{e.composedPath().includes(this)||(this._isOpen=!1)},this.label=\"\",this.name=\"\",this.value=\"\",this.placeholder=\"\",this.disabled=!1,this.readonly=!1,this.required=!1,this.hint=\"\",this.options=[],this.fetch=null,this._isOpen=!1,this._highlightedIndex=-1,this._touched=!1,this._handleInvalid=e=>{e.preventDefault(),this._touched=!0},this._internals=this.attachInternals()}connectedCallback(){super.connectedCallback(),document.addEventListener(\"click\",this._handleDocumentClick),this.addEventListener(\"invalid\",this._handleInvalid)}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener(\"click\",this._handleDocumentClick),this.removeEventListener(\"invalid\",this._handleInvalid)}firstUpdated(){this._updateValidity()}updated(e){(e.has(\"required\")||e.has(\"value\"))&&this._updateValidity(),e.has(\"options\")&&this._isOpen&&this._positionDropdown()}get form(){return this._internals.form}get validity(){return this._internals.validity}get validationMessage(){return this._internals.validationMessage}checkValidity(){return this._internals.checkValidity()}reportValidity(){return this._internals.reportValidity()}formResetCallback(){this.value=\"\",this._touched=!1,this._isOpen=!1,this._highlightedIndex=-1,this._internals.setFormValue(\"\"),this._internals.setValidity({})}formStateRestoreCallback(e){this.value=e}_updateValidity(){this._input&&this._internals.setValidity(this._input.validity,this._input.validationMessage)}_fetch(){if(!this.fetch)return;this._requestId++;const e=this._requestId;this.fetch(this.value).then((t=>{e===this._requestId&&(this.options=t)})).catch((e=>{console.error(\"kr-auto-suggest: fetch failed\",e)}))}_handleInput(e){this.value=e.target.value,this._isOpen=!0,this._highlightedIndex=-1,this._positionDropdown(),this._internals.setFormValue(this.value),this._internals.setValidity(this._input.validity,this._input.validationMessage),this.dispatchEvent(new Event(\"change\",{bubbles:!0,composed:!0})),this._fetch()}_positionDropdown(){requestAnimationFrame((()=>{const e=this.shadowRoot?.querySelector(\".dropdown\");if(!e)return;const t=this._input.getBoundingClientRect(),i=window.innerHeight-t.bottom-4-8,s=t.top-4-8;e.style.left=t.left+\"px\",e.style.width=t.width+\"px\",s>i?(e.style.top=\"\",e.style.bottom=window.innerHeight-t.top+4+\"px\",e.style.maxHeight=s+\"px\"):(e.style.bottom=\"\",e.style.top=t.bottom+4+\"px\",e.style.maxHeight=i+\"px\")}))}_handleFocus(){this._isOpen=!0,this._positionDropdown(),this._fetch()}_handleBlur(){this._touched=!0,this._internals.setValidity(this._input.validity,this._input.validationMessage),setTimeout((()=>{this._isOpen=!1,this._highlightedIndex=-1}),200)}_handleKeyDown(e){switch(e.key){case\"ArrowDown\":e.preventDefault(),this._isOpen=!0,this._highlightedIndex=Math.min(this._highlightedIndex+1,this.options.length-1),this._scrollToHighlighted();break;case\"ArrowUp\":e.preventDefault(),-1===this._highlightedIndex?(this._isOpen=!0,this._highlightedIndex=this.options.length-1):this._highlightedIndex=Math.max(this._highlightedIndex-1,0),this._scrollToHighlighted();break;case\"Enter\":this._highlightedIndex>=0&&this.options[this._highlightedIndex]&&(e.preventDefault(),this._selectOption(this.options[this._highlightedIndex]));break;case\"Escape\":e.preventDefault(),this._isOpen=!1,this._highlightedIndex=-1;break;case\"Tab\":this._isOpen=!1,this._highlightedIndex=-1}}_scrollToHighlighted(){this.updateComplete.then((()=>{const e=this.shadowRoot?.querySelector(\".dropdown\"),t=this.shadowRoot?.querySelector(\".option--highlighted\");if(e&&t){const i=e.getBoundingClientRect(),s=t.getBoundingClientRect();(s.bottom>i.bottom||s.top<i.top)&&t.scrollIntoView({block:\"nearest\"})}}))}_selectOption(e){e.disabled||(this.value=e.value,this._isOpen=!1,this._highlightedIndex=-1,this._internals.setFormValue(this.value),this.dispatchEvent(new CustomEvent(\"select\",{detail:{value:e.value,option:e},bubbles:!0,composed:!0})),this.dispatchEvent(new Event(\"change\",{bubbles:!0,composed:!0})))}_handleClear(){this.value=\"\",this._internals.setFormValue(this.value),this.dispatchEvent(new Event(\"change\",{bubbles:!0,composed:!0})),this._input?.focus()}_handleOptionMouseEnter(e,t){this._highlightedIndex=t}_renderOption(e,t){return V` <button class=${we({option:!0,\"option--highlighted\":t===this._highlightedIndex})} type=\"button\" role=\"option\" aria-selected=${t===this._highlightedIndex} ?disabled=${e.disabled} @click=${t=>this._selectOption(e)} @mouseenter=${e=>this._handleOptionMouseEnter(e,t)} > ${e.label||e.value} </button> `}render(){let e=U;return this._touched&&this._input&&!this._input.validity.valid?e=V`<div class=\"validation-message\">${this._input.validationMessage}</div>`:this.hint&&(e=V`<div class=\"hint\">${this.hint}</div>`),V` <div class=\"wrapper\"> ${this.label?V` <label> ${this.label} ${this.required?V`<span class=\"required\">*</span>`:U} </label> `:U} <div class=\"input-wrapper\"> <input type=\"text\" .value=${$t(this.value)} placeholder=${this.placeholder} ?disabled=${this.disabled} ?readonly=${this.readonly} ?required=${this.required} name=${this.name} autocomplete=\"off\" role=\"combobox\" aria-autocomplete=\"list\" aria-expanded=${this._isOpen} aria-controls=\"dropdown\" class=${we({\"input--invalid\":this._touched&&this._input&&!this._input.validity.valid})} @input=${this._handleInput} @blur=${this._handleBlur} @focus=${this._handleFocus} @keydown=${this._handleKeyDown} /> <div class=\"icon-wrapper\"> ${!this.value||this.disabled||this.readonly?U:V` <svg class=\"clear\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-label=\"Clear\" @click=${this._handleClear}> <path d=\"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z\"/> </svg> `} ${this.value||this.disabled||this.readonly?U:V` <svg class=\"search-icon\" viewBox=\"0 0 24 24\" fill=\"currentColor\"> <path d=\"M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z\"/> </svg> `} </div> </div> <div id=\"dropdown\" role=\"listbox\" class=${we({dropdown:!0,\"dropdown--open\":this._isOpen&&this.options.length>0})} > ${this.options.map(((e,t)=>this._renderOption(e,t)))} </div> ${e} </div> `}}"
1210
1210
  },
1211
1211
  {
1212
1212
  "kind": "variable",
1213
1213
  "name": "It",
1214
- "default": "class extends le{constructor(){super(),this._requestId=0,this._selectedOption=null,this._handleDocumentClick=e=>{e.composedPath().includes(this)||this._close()},this.label=\"\",this.name=\"\",this.value=\"\",this.placeholder=\"Select an option\",this.disabled=!1,this.readonly=!1,this.required=!1,this.hint=\"\",this.optionValue=\"value\",this.optionLabel=\"label\",this.options=[],this.fetch=null,this.fetchSelection=null,this._isOpen=!1,this._highlightedIndex=-1,this._touched=!1,this._searchQuery=\"\",this._loading=!1,this._handleInvalid=e=>{e.preventDefault(),this._touched=!0},this._internals=this.attachInternals()}connectedCallback(){super.connectedCallback(),document.addEventListener(\"click\",this._handleDocumentClick),this.addEventListener(\"invalid\",this._handleInvalid)}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener(\"click\",this._handleDocumentClick),this.removeEventListener(\"invalid\",this._handleInvalid)}firstUpdated(){this._updateValidity(),this.value&&this.fetchSelection&&this._resolveSelection()}updated(e){(e.has(\"required\")||e.has(\"value\"))&&this._updateValidity(),e.has(\"value\")&&(this.value?this._selectedOption&&this._getOptionValue(this._selectedOption)===this.value||this._resolveSelection():this._selectedOption=null),e.has(\"options\")&&this._isOpen&&this._positionDropdown()}get form(){return this._internals.form}get validity(){return this._internals.validity}get validationMessage(){return this._internals.validationMessage}checkValidity(){return this._internals.checkValidity()}reportValidity(){return this._internals.reportValidity()}formResetCallback(){this.value=\"\",this._selectedOption=null,this._touched=!1,this._isOpen=!1,this._highlightedIndex=-1,this._searchQuery=\"\",this._internals.setFormValue(\"\"),this._internals.setValidity({})}formStateRestoreCallback(e){this.value=e}focus(){this._isOpen||this._open()}blur(){this._triggerElement?.blur()}_updateValidity(){this.required&&!this.value?this._internals.setValidity({valueMissing:!0},\"Please select an option\",this._triggerElement):this._internals.setValidity({})}_handleTriggerClick(){this.disabled||this.readonly||(this._isOpen?this._close():this._open())}_open(){this._isOpen=!0,this._searchQuery=\"\",this._highlightedIndex=-1,this._fetch(),this.updateComplete.then((()=>{this._positionDropdown(),this._searchInput&&this._searchInput.focus()}))}_close(){this._isOpen=!1,this._highlightedIndex=-1,this._searchQuery=\"\"}_positionDropdown(){requestAnimationFrame((()=>{const e=this.shadowRoot?.querySelector(\".dropdown\");if(!e)return;const t=this._triggerElement.getBoundingClientRect(),i=window.innerHeight-t.bottom-4-8,s=t.top-4-8;e.style.left=t.left+\"px\",e.style.width=t.width+\"px\",i<200&&s>i?(e.style.top=\"\",e.style.bottom=window.innerHeight-t.top+4+\"px\",e.style.maxHeight=s+\"px\"):(e.style.bottom=\"\",e.style.top=t.bottom+4+\"px\",e.style.maxHeight=i+\"px\")}))}_fetch(){if(!this.fetch)return;this._loading=!0,this._requestId++;const e=this._requestId;this.fetch(this._searchQuery).then((t=>{e===this._requestId&&(this.options=t,this._loading=!1)})).catch((e=>{console.error(\"kr-combo-box: fetch failed\",e),this._loading=!1}))}_handleSearchInput(e){this._searchQuery=e.target.value,this._highlightedIndex=-1,this._fetch()}_handleSearchKeyDown(e){switch(e.key){case\"ArrowDown\":e.preventDefault(),this._highlightedIndex=Math.min(this._highlightedIndex+1,this.options.length-1),this._scrollToHighlighted();break;case\"ArrowUp\":e.preventDefault(),-1===this._highlightedIndex?this._highlightedIndex=this.options.length-1:this._highlightedIndex=Math.max(this._highlightedIndex-1,0),this._scrollToHighlighted();break;case\"Enter\":e.preventDefault(),this._highlightedIndex>=0&&this.options[this._highlightedIndex]&&this._selectOption(this.options[this._highlightedIndex]);break;case\"Escape\":e.preventDefault(),this._close(),this._triggerElement?.focus();break;case\"Tab\":this._close()}}_handleTriggerKeyDown(e){\"ArrowDown\"!==e.key&&\"ArrowUp\"!==e.key&&\"Enter\"!==e.key&&\" \"!==e.key||(e.preventDefault(),this._isOpen||this._open())}_handleTriggerBlur(){this._touched=!0,this._updateValidity()}_scrollToHighlighted(){this.updateComplete.then((()=>{const e=this.shadowRoot?.querySelector(\".options\"),t=this.shadowRoot?.querySelector(\".option--highlighted\");if(e&&t){const i=e.getBoundingClientRect(),s=t.getBoundingClientRect();(s.bottom>i.bottom||s.top<i.top)&&t.scrollIntoView({block:\"nearest\"})}}))}_getOptionValue(e){return\"function\"==typeof this.optionValue?this.optionValue(e):e[this.optionValue]}_getOptionLabel(e){let t;return t=\"function\"==typeof this.optionLabel?this.optionLabel(e):e[this.optionLabel],t||this._getOptionValue(e)}_resolveSelection(){if(!this.fetchSelection)return;const e=this.value;this.fetchSelection(this.value).then((t=>{this.value===e&&t&&(this._selectedOption=t,this.requestUpdate())})).catch((e=>{console.error(\"kr-combo-box: fetchSelection failed\",e)}))}_selectOption(e){e.disabled||(this.value=this._getOptionValue(e),this._selectedOption=e,this._close(),this._internals.setFormValue(this.value),this._updateValidity(),this.dispatchEvent(new CustomEvent(\"select\",{detail:{option:e},bubbles:!0,composed:!0})),this.dispatchEvent(new Event(\"change\",{bubbles:!0,composed:!0})),this._triggerElement?.focus())}_handleOptionMouseEnter(e,t){this._highlightedIndex=t}_renderOption(e,t){const i=this._getOptionValue(e);return V` <button class=${we({option:!0,\"option--highlighted\":t===this._highlightedIndex,\"option--selected\":i===this.value})} type=\"button\" role=\"option\" aria-selected=${i===this.value} ?disabled=${e.disabled} @click=${t=>this._selectOption(e)} @mouseenter=${e=>this._handleOptionMouseEnter(e,t)} > ${this._getOptionLabel(e)} </button> `}render(){let e=B;return this._touched&&this.required&&!this.value?e=V`<div class=\"validation-message\">Please select an option</div>`:this.hint&&(e=V`<div class=\"hint\">${this.hint}</div>`),V` <div class=\"wrapper\"> ${this.label?V` <label> ${this.label} ${this.required?V`<span class=\"required\">*</span>`:B} </label> `:B} <button class=${we({trigger:!0,\"trigger--open\":this._isOpen,\"trigger--invalid\":this._touched&&this.required&&!this.value})} type=\"button\" ?disabled=${this.disabled} aria-haspopup=\"listbox\" aria-expanded=${this._isOpen} @click=${this._handleTriggerClick} @keydown=${this._handleTriggerKeyDown} @blur=${this._handleTriggerBlur} > <span class=${we({trigger__value:!0,trigger__placeholder:!this._selectedOption})}> ${this._selectedOption?this._getOptionLabel(this._selectedOption):this.placeholder} </span> <svg class=${we({trigger__icon:!0,\"trigger__icon--open\":this._isOpen})} viewBox=\"0 0 24 24\" fill=\"currentColor\" > <path d=\"M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z\"/> </svg> </button> <div role=\"listbox\" class=${we({dropdown:!0,\"dropdown--open\":this._isOpen})} > <div class=\"search\"> <div class=\"search__field\"> <input class=\"search__input\" type=\"text\" placeholder=\"Search...\" .value=${this._searchQuery} @input=${this._handleSearchInput} @keydown=${this._handleSearchKeyDown} /> <svg class=\"search__icon\" viewBox=\"0 0 24 24\" fill=\"currentColor\"> <path d=\"M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z\"/> </svg> </div> </div> <div class=\"options\"> ${this._loading?V`<div class=\"empty\">Loading...</div>`:0===this.options.length?V`<div class=\"empty\">No options found</div>`:this.options.map(((e,t)=>this._renderOption(e,t)))} </div> </div> ${e} </div> `}}"
1214
+ "default": "class extends le{constructor(){super(),this._requestId=0,this._selectedOption=null,this._handleDocumentClick=e=>{e.composedPath().includes(this)||this._close()},this.label=\"\",this.name=\"\",this.value=\"\",this.placeholder=\"Select an option\",this.disabled=!1,this.readonly=!1,this.required=!1,this.hint=\"\",this.optionValue=\"value\",this.optionLabel=\"label\",this.options=[],this.fetch=null,this.fetchSelection=null,this._isOpen=!1,this._highlightedIndex=-1,this._touched=!1,this._searchQuery=\"\",this._loading=!1,this._handleInvalid=e=>{e.preventDefault(),this._touched=!0},this._internals=this.attachInternals()}connectedCallback(){super.connectedCallback(),document.addEventListener(\"click\",this._handleDocumentClick),this.addEventListener(\"invalid\",this._handleInvalid)}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener(\"click\",this._handleDocumentClick),this.removeEventListener(\"invalid\",this._handleInvalid)}firstUpdated(){this._updateValidity(),this.value&&this.fetchSelection&&this._resolveSelection()}updated(e){(e.has(\"required\")||e.has(\"value\"))&&this._updateValidity(),e.has(\"value\")&&(this.value?this._selectedOption&&this._getOptionValue(this._selectedOption)===this.value||this._resolveSelection():this._selectedOption=null),e.has(\"options\")&&this._isOpen&&this._positionDropdown()}get form(){return this._internals.form}get validity(){return this._internals.validity}get validationMessage(){return this._internals.validationMessage}checkValidity(){return this._internals.checkValidity()}reportValidity(){return this._internals.reportValidity()}formResetCallback(){this.value=\"\",this._selectedOption=null,this._touched=!1,this._isOpen=!1,this._highlightedIndex=-1,this._searchQuery=\"\",this._internals.setFormValue(\"\"),this._internals.setValidity({})}formStateRestoreCallback(e){this.value=e}focus(){this._isOpen||this._open()}blur(){this._triggerElement?.blur()}_updateValidity(){this.required&&!this.value?this._internals.setValidity({valueMissing:!0},\"Please select an option\",this._triggerElement):this._internals.setValidity({})}_handleTriggerClick(){this.disabled||this.readonly||(this._isOpen?this._close():this._open())}_open(){this._isOpen=!0,this._searchQuery=\"\",this._highlightedIndex=-1,this._fetch(),this.updateComplete.then((()=>{this._positionDropdown(),this._searchInput&&this._searchInput.focus()}))}_close(){this._isOpen=!1,this._highlightedIndex=-1,this._searchQuery=\"\"}_positionDropdown(){requestAnimationFrame((()=>{const e=this.shadowRoot?.querySelector(\".dropdown\");if(!e)return;const t=this._triggerElement.getBoundingClientRect(),i=window.innerHeight-t.bottom-4-8,s=t.top-4-8;e.style.left=t.left+\"px\",e.style.width=t.width+\"px\",i<200&&s>i?(e.style.top=\"\",e.style.bottom=window.innerHeight-t.top+4+\"px\",e.style.maxHeight=s+\"px\"):(e.style.bottom=\"\",e.style.top=t.bottom+4+\"px\",e.style.maxHeight=i+\"px\")}))}_fetch(){if(!this.fetch)return;this._loading=!0,this._requestId++;const e=this._requestId;this.fetch(this._searchQuery).then((t=>{e===this._requestId&&(this.options=t,this._loading=!1)})).catch((e=>{console.error(\"kr-combo-box: fetch failed\",e),this._loading=!1}))}_handleSearchInput(e){this._searchQuery=e.target.value,this._highlightedIndex=-1,this._fetch()}_handleSearchKeyDown(e){switch(e.key){case\"ArrowDown\":e.preventDefault(),this._highlightedIndex=Math.min(this._highlightedIndex+1,this.options.length-1),this._scrollToHighlighted();break;case\"ArrowUp\":e.preventDefault(),-1===this._highlightedIndex?this._highlightedIndex=this.options.length-1:this._highlightedIndex=Math.max(this._highlightedIndex-1,0),this._scrollToHighlighted();break;case\"Enter\":e.preventDefault(),this._highlightedIndex>=0&&this.options[this._highlightedIndex]&&this._selectOption(this.options[this._highlightedIndex]);break;case\"Escape\":e.preventDefault(),this._close(),this._triggerElement?.focus();break;case\"Tab\":this._close()}}_handleTriggerKeyDown(e){\"ArrowDown\"!==e.key&&\"ArrowUp\"!==e.key&&\"Enter\"!==e.key&&\" \"!==e.key||(e.preventDefault(),this._isOpen||this._open())}_handleTriggerBlur(){this._touched=!0,this._updateValidity()}_scrollToHighlighted(){this.updateComplete.then((()=>{const e=this.shadowRoot?.querySelector(\".options\"),t=this.shadowRoot?.querySelector(\".option--highlighted\");if(e&&t){const i=e.getBoundingClientRect(),s=t.getBoundingClientRect();(s.bottom>i.bottom||s.top<i.top)&&t.scrollIntoView({block:\"nearest\"})}}))}_getOptionValue(e){return\"function\"==typeof this.optionValue?this.optionValue(e):e[this.optionValue]}_getOptionLabel(e){let t;return t=\"function\"==typeof this.optionLabel?this.optionLabel(e):e[this.optionLabel],t||this._getOptionValue(e)}_resolveSelection(){if(!this.fetchSelection)return;const e=this.value;this.fetchSelection(this.value).then((t=>{this.value===e&&t&&(this._selectedOption=t,this.requestUpdate())})).catch((e=>{console.error(\"kr-combo-box: fetchSelection failed\",e)}))}_selectOption(e){e.disabled||(this.value=this._getOptionValue(e),this._selectedOption=e,this._close(),this._internals.setFormValue(this.value),this._updateValidity(),this.dispatchEvent(new CustomEvent(\"select\",{detail:{option:e},bubbles:!0,composed:!0})),this.dispatchEvent(new Event(\"change\",{bubbles:!0,composed:!0})),this._triggerElement?.focus())}_handleOptionMouseEnter(e,t){this._highlightedIndex=t}_renderOption(e,t){const i=this._getOptionValue(e);return V` <button class=${we({option:!0,\"option--highlighted\":t===this._highlightedIndex,\"option--selected\":i===this.value})} type=\"button\" role=\"option\" aria-selected=${i===this.value} ?disabled=${e.disabled} @click=${t=>this._selectOption(e)} @mouseenter=${e=>this._handleOptionMouseEnter(e,t)} > ${this._getOptionLabel(e)} </button> `}render(){let e=U;return this._touched&&this.required&&!this.value?e=V`<div class=\"validation-message\">Please select an option</div>`:this.hint&&(e=V`<div class=\"hint\">${this.hint}</div>`),V` <div class=\"wrapper\"> ${this.label?V` <label> ${this.label} ${this.required?V`<span class=\"required\">*</span>`:U} </label> `:U} <button class=${we({trigger:!0,\"trigger--open\":this._isOpen,\"trigger--invalid\":this._touched&&this.required&&!this.value})} type=\"button\" ?disabled=${this.disabled} aria-haspopup=\"listbox\" aria-expanded=${this._isOpen} @click=${this._handleTriggerClick} @keydown=${this._handleTriggerKeyDown} @blur=${this._handleTriggerBlur} > <span class=${we({trigger__value:!0,trigger__placeholder:!this._selectedOption})}> ${this._selectedOption?this._getOptionLabel(this._selectedOption):this.placeholder} </span> <svg class=${we({trigger__icon:!0,\"trigger__icon--open\":this._isOpen})} viewBox=\"0 0 24 24\" fill=\"currentColor\" > <path d=\"M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z\"/> </svg> </button> <div role=\"listbox\" class=${we({dropdown:!0,\"dropdown--open\":this._isOpen})} > <div class=\"search\"> <div class=\"search__field\"> <input class=\"search__input\" type=\"text\" placeholder=\"Search...\" .value=${this._searchQuery} @input=${this._handleSearchInput} @keydown=${this._handleSearchKeyDown} /> <svg class=\"search__icon\" viewBox=\"0 0 24 24\" fill=\"currentColor\"> <path d=\"M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z\"/> </svg> </div> </div> <div class=\"options\"> ${this._loading?V`<div class=\"empty\">Loading...</div>`:0===this.options.length?V`<div class=\"empty\">No options found</div>`:this.options.map(((e,t)=>this._renderOption(e,t)))} </div> </div> ${e} </div> `}}"
1215
1215
  }
1216
1216
  ],
1217
1217
  "exports": [
@@ -1291,7 +1291,7 @@
1291
1291
  "kind": "js",
1292
1292
  "name": "KRDialog",
1293
1293
  "declaration": {
1294
- "name": "Ue",
1294
+ "name": "Be",
1295
1295
  "module": "dist/krubble-components.bundled.min.js"
1296
1296
  }
1297
1297
  },
@@ -2582,7 +2582,7 @@
2582
2582
  {
2583
2583
  "kind": "variable",
2584
2584
  "name": "KRTable",
2585
- "default": "class KRTable extends LitElement { constructor() { super(...arguments); /** * Internal flag to switch between scroll edge modes: * - 'overlay': Fixed padding with overlay elements that hide content at edges (scrollbar at viewport edge) * - 'edge': Padding scrolls with content, allowing table to reach edges when scrolling */ this._scrollStyle = 'overlay'; this._data = []; this._dataState = 'idle'; this._page = 1; this._pageSize = 50; this._totalItems = 0; this._totalPages = 0; this._searchQuery = ''; this._canScrollLeft = false; this._canScrollRight = false; this._canScrollHorizontal = false; this._columnPickerOpen = false; this._filterPanelOpened = null; this._filterPanelTab = 'filter'; this._buckets = new Map(); this._filterPanelPos = { top: 0, left: 0 }; this._resizing = null; this._resizeObserver = null; this._searchPositionLocked = false; this._model = new KRTableModel(); this.def = { columns: [] }; this._handleClickOutside = (e) => { const path = e.composedPath(); if (this._columnPickerOpen) { const picker = this.shadowRoot?.querySelector('.column-picker-wrapper'); if (picker && !path.includes(picker)) { this._columnPickerOpen = false; } } if (this._filterPanelOpened) { if (!path.some((el) => el.classList?.contains('filter-panel'))) { this._handleFilterApply(); } } }; this._handleResizeMove = (e) => { if (!this._resizing) return; const col = this._model.columns.find(c => c.id === this._resizing.columnId); if (col) { const newWidth = this._resizing.startWidth + (e.clientX - this._resizing.startX); col.width = `${Math.min(900, Math.max(50, newWidth))}px`; this.requestUpdate(); } }; this._handleResizeEnd = () => { this._resizing = null; document.removeEventListener('mousemove', this._handleResizeMove); document.removeEventListener('mouseup', this._handleResizeEnd); }; } connectedCallback() { super.connectedCallback(); this.classList.toggle('kr-table--scroll-overlay', this._scrollStyle === 'overlay'); this.classList.toggle('kr-table--scroll-edge', this._scrollStyle === 'edge'); this._fetch(); this._initRefresh(); document.addEventListener('click', this._handleClickOutside); this._resizeObserver = new ResizeObserver(() => { // Unlock and recalculate on resize since layout changes this._searchPositionLocked = false; this._updateSearchPosition(); }); this._resizeObserver.observe(this); } disconnectedCallback() { super.disconnectedCallback(); clearInterval(this._refreshTimer); document.removeEventListener('click', this._handleClickOutside); this._resizeObserver?.disconnect(); } willUpdate(changedProperties) { if (changedProperties.has('def')) { // Build internal model from user-provided def this._model = new KRTableModel(); if (this.def.title) { this._model.title = this.def.title; } if (this.def.actions) { this._model.actions = this.def.actions; } if (this.def.data) { this._model.data = this.def.data; } if (this.def.dataSource) { this._model.dataSource = this.def.dataSource; } if (typeof this.def.refreshInterval === 'number') { this._model.refreshInterval = this.def.refreshInterval; } if (typeof this.def.pageSize === 'number') { this._model.pageSize = this.def.pageSize; } if (this.def.rowClickable) { this._model.rowClickable = this.def.rowClickable; } if (this.def.rowHref) { this._model.rowHref = this.def.rowHref; } this._model.columns = this.def.columns.map(col => { const column = { ...col, filter: null }; if (!column.type) { column.type = 'string'; } if (column.type === 'actions') { column.label = col.label ?? ''; column.sticky = 'right'; column.resizable = false; return column; } if (col.filterable || col.facetable) { column.filter = new KRQuery(); column.filter.field = col.id; column.filter.type = column.type; if (col.filter) { column.filter.setOperator(col.filter.operator); column.filter.setValue(col.filter.value); } else if (col.facetable && !col.filterable) { column.filter.operator = 'in'; column.filter.value = []; } else if (column.filter.type === 'string') { column.filter.operator = 'contains'; } } return column; }); if (this.def.displayedColumns) { this._model.displayedColumns = this.def.displayedColumns; } else { this._model.displayedColumns = this._model.columns.map(c => c.id); } this._fetch(); this._initRefresh(); } } updated(changedProperties) { this._updateScrollFlags(); this._syncSlottedContent(); } /** Syncs light DOM content for cells with custom render functions */ _syncSlottedContent() { const columns = this.getDisplayedColumns().filter(col => col.render); if (!columns.length) return; // Clear old slotted content this.querySelectorAll('[slot^=\"cell-\"]').forEach(el => el.remove()); // Create new slotted content this._data.forEach((row, rowIndex) => { columns.forEach(col => { const result = col.render(row); if (!result) return; const el = document.createElement('span'); el.slot = `cell-${rowIndex}-${col.id}`; if (col.type === 'actions') { el.style.display = 'flex'; el.style.gap = '8px'; } if (typeof result === 'string') { el.innerHTML = result; } else { render(result, el); } this.appendChild(el); }); }); } // ---------------------------------------------------------------------------- // Public Interface // ---------------------------------------------------------------------------- refresh() { this._fetch(); } goToPrevPage() { if (this._page > 1) { this._page--; this._fetch(); } } goToNextPage() { if (this._page < this._totalPages) { this._page++; this._fetch(); } } goToPage(page) { if (page >= 1 && page <= this._totalPages) { this._page = page; this._fetch(); } } // ---------------------------------------------------------------------------- // Data Fetching // ---------------------------------------------------------------------------- _toSolrData() { const request = { page: this._page - 1, size: this._pageSize, sorts: [], filterFields: [], queryFields: [], facetFields: [] }; for (const col of this._model.columns) { if (!col.filter || col.filter.isEmpty() || !col.filter.isValid()) { continue; } const filterData = col.filter.toSolrData(); if (col.facetable && col.filter.operator === 'in') { filterData.tagged = true; } request.filterFields.push(filterData); } for (const col of this._model.columns) { if (!col.facetable) { continue; } request.facetFields.push({ name: col.id, type: 'FIELD', limit: 100, sort: 'count', minimumCount: 1 }); } if (this._searchQuery?.trim().length) { request.queryFields.push({ name: '_text_', operation: 'IS', value: termify(this._searchQuery, false) }); } return request; } _toDbParams() { const request = { page: this._page - 1, size: this._pageSize, sorts: [], filterFields: [], queryFields: [], facetFields: [] }; for (const col of this._model.columns) { if (!col.filter || col.filter.isEmpty() || !col.filter.isValid()) { continue; } request.filterFields.push(col.filter.toDbParams()); } if (this._searchQuery?.trim().length) { this._model.columns.filter(col => col.searchable).forEach(col => { request.queryFields.push({ name: col.id, operation: 'CONTAINS', value: this._searchQuery, and: false }); }); } return request; } /** * Fetches data from the API and updates the table. * Shows a loading spinner while fetching, then displays rows on success * or an error snackbar on failure. * Request/response format depends on dataSource.mode (solr, opensearch, db). */ _fetch() { if (this._model.data) { this._data = this._model.data; this._totalItems = this._model.data.length; this._totalPages = Math.ceil(this._model.data.length / this._pageSize); this._dataState = 'success'; return; } if (!this._model.dataSource) return; this._dataState = 'loading'; let request; if (this._model.dataSource.mode === 'db') { request = this._toDbParams(); } else { request = this._toSolrData(); } this._model.dataSource.fetch(request) .then(response => { // Parse response based on mode switch (this._model.dataSource?.mode) { case 'opensearch': { throw Error('Opensearch not supported yet'); break; } case 'db': { const res = response; this._data = res.data.content; this._totalItems = res.data.totalElements; this._totalPages = res.data.totalPages; this._pageSize = res.data.size; break; } default: { // solr const res = response; this._data = res.data.content; this._totalItems = res.data.totalElements; this._totalPages = res.data.totalPages; this._pageSize = res.data.size; this._parseFacetResults(res); } } this._dataState = 'success'; this._updateSearchPosition(); }) .catch(err => { this._dataState = 'error'; KRSnackbar.show({ message: err instanceof Error ? err.message : 'Failed to load data', type: 'error' }); }); } _parseFacetResults(response) { if (!response.data.facetFields) { return; } for (const col of this._model.columns) { if (!col.facetable) { continue; } const rawBuckets = response.data.facetFields[col.id]; if (!rawBuckets) { this._buckets.set(col.id, []); continue; } const buckets = []; for (const raw of rawBuckets) { // Solr returns boolean facet values as strings — coerce to actual booleans // so they match the filter values stored by toggle(). let val = raw.name; if (col.type === 'boolean' && typeof raw.name === 'string') { if (raw.name === 'true') { val = true; } else if (raw.name === 'false') { val = false; } } if (raw.name === null && raw.count > 0) { buckets.unshift({ val: null, count: raw.count }); } if (raw.name !== null) { buckets.push({ val: val, count: raw.count }); } } // Bucket sync: ensure selected values appear even with 0 results if (col.filter && col.filter.operator === 'in' && Array.isArray(col.filter.value)) { for (const selectedVal of col.filter.value) { if (!buckets.some(b => b.val === selectedVal)) { buckets.push({ val: selectedVal, count: 0 }); } } } this._buckets.set(col.id, buckets); } // Trigger re-render since Map mutation doesn't trigger Lit updates this._buckets = new Map(this._buckets); } /** * Sets up auto-refresh so the table automatically fetches fresh data * at a regular interval (useful for dashboards, monitoring views). * Configured via def.refreshInterval in milliseconds. */ _initRefresh() { clearInterval(this._refreshTimer); if (this._model.refreshInterval && this._model.refreshInterval > 0) { this._refreshTimer = window.setInterval(() => { this._fetch(); }, this._model.refreshInterval); } } _handleSearch(e) { const input = e.target; this._searchQuery = input.value; this._page = 1; this._fetch(); } _getGridTemplateColumns() { const cols = this.getDisplayedColumns(); return cols.map((col) => { // If column has explicit width, use it if (col.width) { return col.width; } // Actions columns: fit content without minimum if (col.type === 'actions') { return 'max-content'; } // No width specified - use content-based sizing with minimum return 'minmax(80px, auto)'; }).join(' '); } /** * Updates search position to be centered with equal gaps from title and tools. * On first call: resets to flex centering, measures position, then locks with fixed margin. * Subsequent calls are ignored unless _searchPositionLocked is reset (e.g., on resize). */ _updateSearchPosition() { // Skip if already locked (prevents shifts on pagination changes) if (this._searchPositionLocked) return; const search = this.shadowRoot?.querySelector('.search'); const searchField = search?.querySelector('.search-field'); if (!search || !searchField) return; // Reset to flex centering search.style.justifyContent = 'center'; searchField.style.marginLeft = ''; requestAnimationFrame(() => { const searchRect = search.getBoundingClientRect(); const fieldRect = searchField.getBoundingClientRect(); // Calculate how far from the left of search container the field currently is const currentOffset = fieldRect.left - searchRect.left; // Lock position: switch to flex-start and use fixed margin search.style.justifyContent = 'flex-start'; searchField.style.marginLeft = `${currentOffset}px`; // Mark as locked so pagination changes don't shift the search this._searchPositionLocked = true; }); } // ---------------------------------------------------------------------------- // Columns // ---------------------------------------------------------------------------- _toggleColumnPicker() { this._columnPickerOpen = !this._columnPickerOpen; } _toggleColumn(columnId) { if (this._model.displayedColumns.includes(columnId)) { this._model.displayedColumns = this._model.displayedColumns.filter(id => id !== columnId); } else { this._model.displayedColumns = [...this._model.displayedColumns, columnId]; } this.requestUpdate(); } // Clear any existing text selection on mousedown so we only detect // selections made during this click gesture, not stale selections from elsewhere _handleRowMouseDown() { if (!this._model.rowClickable) { return; } window.getSelection()?.removeAllRanges(); } _handleRowClick(row, rowIndex) { if (!this._model.rowClickable) { return; } const selection = window.getSelection(); if (selection && selection.toString().length > 0) { return; } this.dispatchEvent(new CustomEvent('row-click', { detail: { row, rowIndex }, bubbles: true, composed: true })); } // When a user toggles a column on via the column picker, it gets appended // to _displayedColumns. By mapping over _displayedColumns (not def.columns), // the new column appears at the right edge of the table instead of jumping // back to its original position in the column definition. // Actions columns are always moved to the end. getDisplayedColumns() { return this._model.displayedColumns .map(id => this._model.columns.find(col => col.id === id)) .sort((a, b) => { if (a.type === 'actions' && b.type !== 'actions') return 1; if (a.type !== 'actions' && b.type === 'actions') return -1; return 0; }); } // ---------------------------------------------------------------------------- // Scrolling // ---------------------------------------------------------------------------- /** * Scroll event handler that updates scroll flags in real-time as user scrolls. * Updates shadow indicators to show if more content exists left/right. */ _handleScroll(e) { const container = e.target; this._canScrollLeft = container.scrollLeft > 0; this._canScrollRight = container.scrollLeft < container.scrollWidth - container.clientWidth - 1; } /** * Updates scroll state flags for the table content container. * - _canScrollLeft: true if scrolled right (can scroll back left) * - _canScrollRight: true if more content exists to the right * - _canScrollHorizontal: true if content is wider than container * These flags control scroll shadow indicators and CSS classes. */ _updateScrollFlags() { const container = this.shadowRoot?.querySelector('.content'); if (container) { this._canScrollLeft = container.scrollLeft > 0; this._canScrollRight = container.scrollWidth > container.clientWidth && container.scrollLeft < container.scrollWidth - container.clientWidth - 1; this._canScrollHorizontal = container.scrollWidth > container.clientWidth; } this.classList.toggle('kr-table--scroll-left-available', this._canScrollLeft); this.classList.toggle('kr-table--scroll-right-available', this._canScrollRight); this.classList.toggle('kr-table--scroll-horizontal-available', this._canScrollHorizontal); this.classList.toggle('kr-table--sticky-left', this.getDisplayedColumns().some(c => c.sticky === 'left')); this.classList.toggle('kr-table--sticky-right', this.getDisplayedColumns().some(c => c.sticky === 'right')); } // ---------------------------------------------------------------------------- // Column Resizing // ---------------------------------------------------------------------------- _handleResizeStart(e, columnId) { e.preventDefault(); const headerCell = this.shadowRoot?.querySelector(`.header-cell[data-column-id=\"${columnId}\"]`); this._resizing = { columnId, startX: e.clientX, startWidth: headerCell?.offsetWidth || 200 }; document.addEventListener('mousemove', this._handleResizeMove); document.addEventListener('mouseup', this._handleResizeEnd); } // ---------------------------------------------------------------------------- // Header // ---------------------------------------------------------------------------- _handleAction(action) { if (action.href) { return; } this.dispatchEvent(new CustomEvent('action', { detail: { action: action.id }, bubbles: true, composed: true })); } // ---------------------------------------------------------------------------- // Filter Handlers // ---------------------------------------------------------------------------- _handleKqlChange(e, column) { const kql = e.target.value.trim(); if (!kql) { column.filter.clear(); this.requestUpdate(); } else { column.filter.setKql(kql); this.requestUpdate(); if (!column.filter.isValid()) { return; } } this._page = 1; this._fetch(); } _handleFilterPanelToggle(e, column) { e.stopPropagation(); if (this._filterPanelOpened === column.id) { this._filterPanelOpened = null; } else { const rect = e.currentTarget.getBoundingClientRect(); let left = rect.left; if (left + 328 > window.innerWidth) { left = window.innerWidth - 328; } this._filterPanelPos = { top: rect.bottom + 4, left }; this._filterPanelOpened = column.id; if (column.facetable) { this._filterPanelTab = 'counts'; } else { this._filterPanelTab = 'filter'; } } } _handleKqlClear(column) { column.filter.clear(); this._page = 1; this._fetch(); } _handleFilterClear() { const column = this._model.columns.find(c => c.id === this._filterPanelOpened); if (column) { column.filter.clear(); if (column.facetable && !column.filterable) { column.filter.operator = 'in'; column.filter.value = []; } } this._filterPanelOpened = null; this._page = 1; this._fetch(); } _handleFilterTextKeydown(e, column) { if (e.key === 'Enter') { e.preventDefault(); this._handleFilterApply(); } } _handleOperatorChange(e, column) { column.filter.setOperator(e.target.value); this.requestUpdate(); } _handleFilterStringChange(e, column) { column.filter.setValue(e.target.value); this.requestUpdate(); } _handleFilterNumberChange(e, column) { column.filter.setValue(Number(e.target.value)); this.requestUpdate(); } _handleFilterDateChange(e, column) { column.filter.setValue(new Date(e.target.value), 'day'); this.requestUpdate(); } _handleFilterBooleanChange(e, column) { column.filter.setValue(e.target.value === 'true'); this.requestUpdate(); } _handleFilterDateStartChange(e, column) { column.filter.setStart(new Date(e.target.value), 'day'); this.requestUpdate(); } _handleFilterDateEndChange(e, column) { column.filter.setEnd(new Date(e.target.value), 'day'); this.requestUpdate(); } _handleFilterNumberStartChange(e, column) { column.filter.setStart(Number(e.target.value)); this.requestUpdate(); } _handleFilterNumberEndChange(e, column) { column.filter.setEnd(Number(e.target.value)); this.requestUpdate(); } _handleFilterListChange(e, column) { const items = e.target.value.split(',').map((v) => v.trim()).filter((v) => v !== ''); if (column.type === 'number') { column.filter.setValue(items.map((v) => Number(v))); } else { column.filter.setValue(items); } this.requestUpdate(); } _handleFilterApply() { this._filterPanelOpened = null; this._page = 1; this._fetch(); } _handleFilterPanelTabChange(e) { this._filterPanelTab = e.detail.activeTabId; } _handleBucketToggle(e, column, bucket) { column.filter.toggle(bucket.val); this._page = 1; this._fetch(); } // ---------------------------------------------------------------------------- // Rendering // ---------------------------------------------------------------------------- _renderCellContent(column, row, rowIndex) { const value = row[column.id]; if (column.render) { // Use slot to project content from light DOM so external styles apply return html `<slot name=\"cell-${rowIndex}-${column.id}\"></slot>`; } if (value === null || value === undefined) { return ''; } switch (column.type) { case 'number': if (column.format === 'currency' && typeof value === 'number') { return value.toLocaleString('en-US', { style: 'currency', currency: 'USD' }); } return String(value); case 'date': { let date; if (value instanceof Date) { date = value; } else if (typeof value === 'string' && /^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}/.test(value)) { // MySQL datetime format (UTC): \"2026-01-28 01:33:44:517\" // Replace last colon before ms with dot, append Z for UTC const isoString = value.replace(/(\\d{2}:\\d{2}:\\d{2}):(\\d+)$/, '$1.$2').replace(' ', 'T') + 'Z'; date = new Date(isoString); } else { date = new Date(value); } // Show date and time for datetime values in UTC return date.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', timeZone: 'UTC' }); } case 'boolean': if (value === true) return 'Yes'; if (value === false) return 'No'; return ''; default: return String(value); } } /** * Returns CSS classes for a header cell based on column config. */ _getHeaderCellClasses(column, index) { return { 'header-cell': true, 'header-cell--align-center': column.align === 'center', 'header-cell--align-right': column.align === 'right', 'header-cell--sticky-left': column.sticky === 'left', 'header-cell--sticky-left-last': column.sticky === 'left' && !this.getDisplayedColumns().slice(index + 1).some(c => c.sticky === 'left'), 'header-cell--sticky-right': column.sticky === 'right', 'header-cell--sticky-right-first': column.sticky === 'right' && !this.getDisplayedColumns().slice(0, index).some(c => c.sticky === 'right') }; } /** * Returns CSS classes for a table cell based on column config: * - Alignment (center, right) * - Sticky positioning (left, right) * - Border classes for the last left-sticky or first right-sticky column */ _getCellClasses(column, index) { return { 'cell': true, 'cell--actions': column.type === 'actions', 'cell--align-center': column.align === 'center', 'cell--align-right': column.align === 'right', 'cell--sticky-left': column.sticky === 'left', 'cell--sticky-left-last': column.sticky === 'left' && !this.getDisplayedColumns().slice(index + 1).some(c => c.sticky === 'left'), 'cell--sticky-right': column.sticky === 'right', 'cell--sticky-right-first': column.sticky === 'right' && !this.getDisplayedColumns().slice(0, index).some(c => c.sticky === 'right') }; } /** * Returns inline styles for a table cell: * - Width (from column config or default 150px) * - Min-width (if specified) * - Left/right offset for sticky columns (calculated from widths of preceding sticky columns) */ _getCellStyle(column, index) { const styles = {}; if (column.sticky === 'left') { let leftOffset = 0; for (let i = 0; i < index; i++) { const col = this.getDisplayedColumns()[i]; if (col.sticky === 'left') { leftOffset += parseInt(col.width || '0', 10); } } styles.left = `${leftOffset}px`; } if (column.sticky === 'right') { let rightOffset = 0; for (let i = index + 1; i < this.getDisplayedColumns().length; i++) { const col = this.getDisplayedColumns()[i]; if (col.sticky === 'right') { rightOffset += parseInt(col.width || '0', 10); } } styles.right = `${rightOffset}px`; } return styles; } /** * Renders the pagination controls: * - Previous page arrow (disabled on first page) * - Range text showing \"1-50 of 150\" format * - Next page arrow (disabled on last page) * * Hidden when there's no data or all data fits on one page. */ _renderPagination() { const start = (this._page - 1) * this._pageSize + 1; const end = Math.min(this._page * this._pageSize, this._totalItems); return html ` <div class=\"pagination\"> <span class=\"pagination-icon ${this._page === 1 ? 'pagination-icon--disabled' : ''}\" @click=${this.goToPrevPage} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z\"/></svg> </span> <span class=\"pagination-info\">${start}-${end} of ${this._totalItems}</span> <span class=\"pagination-icon ${this._page === this._totalPages ? 'pagination-icon--disabled' : ''}\" @click=${this.goToNextPage} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\"/></svg> </span> </div> `; } /** * Renders the header toolbar containing: * - Title (left) * - Search bar with view selector dropdown (center) * - Tools (right): page navigation, refresh button, column visibility picker, actions dropdown * * Hidden when there's no title, no actions, and data fits on one page. */ _renderHeader() { if (!this._model.title && !this._model.actions?.length && this._totalPages <= 1) { return nothing; } return html ` <div class=\"header\"> <div class=\"title\">${this._model.title ?? ''}</div> ${this._model.dataSource?.mode === 'db' && !this._model.columns.some(col => col.searchable) ? html `<div class=\"search\"></div>` : html ` <div class=\"search\"> <!-- TODO: Saved views dropdown <div class=\"views\"> <span>Default View</span> <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z\"/></svg> </div> --> <div class=\"search-field\"> <svg class=\"search-icon\" viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z\"/></svg> <input type=\"text\" class=\"search-input\" placeholder=\"Search...\" .value=${this._searchQuery} @input=${this._handleSearch} /> </div> </div> `} <div class=\"tools\"> ${this._renderPagination()} <span class=\"refresh\" title=\"Refresh\" @click=${() => this.refresh()}> <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M480-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100 70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z\"/></svg> </span> <div class=\"column-picker-wrapper\"> <span class=\"header-icon\" title=\"Columns\" @click=${this._toggleColumnPicker}> <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M121-280v-400q0-33 23.5-56.5T201-760h559q33 0 56.5 23.5T840-680v400q0 33-23.5 56.5T760-200H201q-33 0-56.5-23.5T121-280Zm79 0h133v-400H200v400Zm213 0h133v-400H413v400Zm213 0h133v-400H626v400Z\"/></svg> </span> <div class=\"column-picker ${this._columnPickerOpen ? 'open' : ''}\"> ${[...this._model.columns].filter(col => col.type !== 'actions').sort((a, b) => (a.label ?? a.id).localeCompare(b.label ?? b.id)).map(col => html ` <div class=\"column-picker-item\" @click=${() => this._toggleColumn(col.id)}> <div class=\"column-picker-checkbox ${this._model.displayedColumns.includes(col.id) ? 'checked' : ''}\"> <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z\"/></svg> </div> <span class=\"column-picker-label\">${col.label ?? col.id}</span> </div> `)} </div> </div> ${this._model.actions?.length === 1 ? html ` <kr-button class=\"actions\" .href=${this._model.actions[0].href} .target=${this._model.actions[0].target} @click=${() => this._handleAction(this._model.actions[0])} > ${this._model.actions[0].label} </kr-button> ` : this._model.actions?.length ? html ` <kr-button class=\"actions\" .options=${this._model.actions.map(a => ({ id: a.id, label: a.label }))} @option-select=${(e) => this._handleAction({ id: e.detail.id, label: e.detail.label })} > Actions </kr-button> ` : nothing} </div> </div> `; } /** Renders status message (loading, error, empty) */ _renderStatus() { if (this._dataState === 'loading' && this._data.length === 0) { return html `<div class=\"status\">Loading...</div>`; } if (this._dataState === 'error' && this._data.length === 0) { return html `<div class=\"status status--error\">Error loading data</div>`; } if (this._data.length === 0) { return html `<div class=\"status\">No data available</div>`; } return nothing; } _renderFilterPanel() { if (!this._filterPanelOpened) { return nothing; } const column = this._model.columns.find(c => c.id === this._filterPanelOpened); // Build filter content (operator + value input) let valueInput = html ``; if (column.filter.operator === 'empty' || column.filter.operator === 'n_empty') { valueInput = html ` <input type=\"text\" class=\"filter-panel__input\" disabled .value=${column.filter.text} /> `; } else if (column.filter.operator === 'between' && column.type === 'date') { valueInput = html ` <input type=\"date\" class=\"filter-panel__input\" .valueAsDate=${column.filter.value?.start ?? null} @change=${(e) => this._handleFilterDateStartChange(e, column)} /> <input type=\"date\" class=\"filter-panel__input\" .valueAsDate=${column.filter.value?.end ?? null} @change=${(e) => this._handleFilterDateEndChange(e, column)} /> `; } else if (column.filter.operator === 'between' && column.type === 'number') { valueInput = html ` <input type=\"number\" class=\"filter-panel__input\" placeholder=\"Start\" .value=${column.filter.value?.start ?? ''} @input=${(e) => this._handleFilterNumberStartChange(e, column)} @keydown=${(e) => this._handleFilterTextKeydown(e, column)} /> <input type=\"number\" class=\"filter-panel__input\" placeholder=\"End\" .value=${column.filter.value?.end ?? ''} @input=${(e) => this._handleFilterNumberEndChange(e, column)} @keydown=${(e) => this._handleFilterTextKeydown(e, column)} /> `; } else if (column.filter.operator === 'in') { valueInput = html ` <textarea class=\"filter-panel__textarea\" rows=\"3\" placeholder=\"Values (comma-separated)\" .value=${column.filter.text} @input=${(e) => this._handleFilterListChange(e, column)} @keydown=${(e) => this._handleFilterTextKeydown(e, column)} ></textarea> `; } else if (column.type === 'boolean') { valueInput = html ` <kr-select-field placeholder=\"Value\" .value=${String(column.filter.value ?? '')} @change=${(e) => this._handleFilterBooleanChange(e, column)} > <kr-select-option value=\"true\">Yes</kr-select-option> <kr-select-option value=\"false\">No</kr-select-option> </kr-select-field> `; } else if (column.type === 'date') { valueInput = html ` <input type=\"date\" class=\"filter-panel__input\" .valueAsDate=${column.filter.value} @change=${(e) => this._handleFilterDateChange(e, column)} /> `; } else if (column.type === 'number') { valueInput = html ` <input type=\"number\" class=\"filter-panel__input\" placeholder=\"Value\" min=\"0\" .value=${column.filter.text} @input=${(e) => this._handleFilterNumberChange(e, column)} @keydown=${(e) => this._handleFilterTextKeydown(e, column)} /> `; } else { valueInput = html ` <input type=\"text\" class=\"filter-panel__input\" placeholder=\"Value\" .value=${column.filter.text} @input=${(e) => this._handleFilterStringChange(e, column)} @keydown=${(e) => this._handleFilterTextKeydown(e, column)} /> `; } const filterContent = html ` <div class=\"filter-panel__content\"> <kr-select-field .value=${column.filter.operator} @change=${(e) => this._handleOperatorChange(e, column)} > ${getOperatorsForType(column.type).map(op => html ` <kr-select-option value=${op.key}>${op.label}</kr-select-option> `)} </kr-select-field> ${valueInput} </div> `; // Build bucket list content const buckets = this._buckets.get(column.id) || []; let bucketContent; if (!buckets.length) { bucketContent = html `<div class=\"bucket-empty\">No data</div>`; } else { bucketContent = html ` <div class=\"buckets\"> ${buckets.map(bucket => { let bucketLabel = '(Empty)'; if (bucket.val !== null && bucket.val !== undefined) { if (column.type === 'boolean') { if (bucket.val === true || bucket.val === 'true') { bucketLabel = 'Yes'; } else { bucketLabel = 'No'; } } else { bucketLabel = String(bucket.val); } } let checkIcon = nothing; if (column.filter.has(bucket.val)) { checkIcon = html ` <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"> <path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z\"/> </svg> `; } return html ` <div class=\"bucket\" @click=${(e) => this._handleBucketToggle(e, column, bucket)} > <div class=${classMap({ 'bucket__checkbox': true, 'bucket__checkbox--checked': column.filter.has(bucket.val) })}> ${checkIcon} </div> <span class=\"bucket__label\">${bucketLabel}</span> <span class=\"bucket__count\">${bucket.count}</span> </div> `; })} </div> `; } // Build panel body — tabs if both filterable+facetable, otherwise just the relevant content let panelBody; if (column.facetable && column.filterable) { panelBody = html ` <kr-tab-group size=\"small\" active-tab-id=${this._filterPanelTab} @tab-change=${(e) => this._handleFilterPanelTabChange(e)} > <kr-tab id=\"filter\" label=\"Filter\"> ${filterContent} </kr-tab> <kr-tab id=\"counts\" label=\"Counts\"> ${bucketContent} </kr-tab> </kr-tab-group> `; } else if (column.facetable) { panelBody = bucketContent; } else { panelBody = filterContent; } return html ` <div class=\"filter-panel\" style=${styleMap({ top: this._filterPanelPos.top + 'px', left: this._filterPanelPos.left + 'px' })} > ${panelBody} <div class=\"filter-panel__actions\"> <kr-button variant=\"outline\" color=\"secondary\" size=\"small\" @click=${this._handleFilterClear}> Clear </kr-button> <kr-button size=\"small\" @click=${this._handleFilterApply}> Apply </kr-button> </div> </div> `; } /** * Renders filter row below column headers. * Only displays for columns with filterable: true. */ _renderFilterRow() { const columns = this.getDisplayedColumns(); if (!columns.some(col => col.filterable || col.facetable)) { return nothing; } return html ` <div class=\"filter-row\"> ${columns.map((col, i) => { if (!col.filterable && !col.facetable) { return html `<div class=${classMap({ 'filter-cell': true, 'filter-cell--sticky-left': col.sticky === 'left', 'filter-cell--sticky-right': col.sticky === 'right', 'filter-cell--sticky-right-first': col.sticky === 'right' && !columns.slice(0, i).some((c) => c.sticky === 'right') })} style=${styleMap(this._getCellStyle(col, i))} ></div>`; } return html ` <div class=${classMap({ 'filter-cell': true, 'filter-cell--sticky-left': col.sticky === 'left', 'filter-cell--sticky-right': col.sticky === 'right', 'filter-cell--sticky-right-first': col.sticky === 'right' && !columns.slice(0, i).some((c) => c.sticky === 'right') })} style=${styleMap(this._getCellStyle(col, i))} > <div class=\"filter-cell__wrapper\"> <input type=\"text\" class=${classMap({ 'filter-cell__input': true, 'filter-cell__input--invalid': !col.filter.isValid() })} .value=${col.filter.kql} @change=${(e) => this._handleKqlChange(e, col)} /> ${col.filter?.kql?.length > 0 ? html ` <button class=\"filter-cell__clear\" @click=${() => this._handleKqlClear(col)} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"> <path d=\"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z\"/> </svg> </button> ` : nothing} <button class=${classMap({ 'filter-cell__advanced': true, 'filter-cell__advanced--opened': this._filterPanelOpened === col.id })} @click=${(e) => this._handleFilterPanelToggle(e, col)} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"> <path d=\"M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z\"/> </svg> </button> </div> </div> `; })} </div> `; } /** Renders the scrollable data grid with column headers and rows. */ _renderTable() { return html ` <div class=\"wrapper\"> <div class=\"overlay-left\"></div> <div class=\"overlay-right\"></div> ${this._renderStatus()} <div class=\"content\" @scroll=${this._handleScroll}> <div class=\"table\" style=\"grid-template-columns: ${this._getGridTemplateColumns()}\"> <div class=\"header-row\"> ${this.getDisplayedColumns().map((col, i) => html ` <div class=${classMap(this._getHeaderCellClasses(col, i))} style=${styleMap(this._getCellStyle(col, i))} data-column-id=${col.id} >${col.label ?? col.id}${col.resizable !== false ? html `<div class=\"header-cell__resize\" @mousedown=${(e) => this._handleResizeStart(e, col.id)} ></div>` : nothing}</div> `)} </div> ${this._renderFilterRow()} ${this._data.map((row, rowIndex) => { const cells = this.getDisplayedColumns().map((col, i) => html ` <div class=${classMap(this._getCellClasses(col, i))} style=${styleMap(this._getCellStyle(col, i))} data-column-id=${col.id} > ${this._renderCellContent(col, row, rowIndex)} </div> `); if (this._model.rowHref) { return html ` <a href=${this._model.rowHref(row)} class=${classMap({ 'row': true, 'row--clickable': true, 'row--link': true })} @mousedown=${() => this._handleRowMouseDown()} @click=${() => this._handleRowClick(row, rowIndex)} >${cells}</a> `; } return html ` <div class=${classMap({ 'row': true, 'row--clickable': !!this._model.rowClickable })} @mousedown=${() => this._handleRowMouseDown()} @click=${() => this._handleRowClick(row, rowIndex)} >${cells}</div> `; })} </div> </div> </div> `; } /** * Renders a data table with: * - Header bar with title, search input with view selector, and tools (pagination, refresh, column visibility, actions dropdown) * - Scrollable grid with sticky header row and optional sticky left/right columns * - Loading, error message, or empty state when no data */ render() { if (!this._model.columns.length) { return html `<slot></slot>`; } return html ` ${this._renderHeader()} ${this._renderTable()} ${this._renderFilterPanel()} `; } }"
2585
+ "default": "class KRTable extends LitElement { constructor() { super(...arguments); /** * Internal flag to switch between scroll edge modes: * - 'overlay': Fixed padding with overlay elements that hide content at edges (scrollbar at viewport edge) * - 'edge': Padding scrolls with content, allowing table to reach edges when scrolling */ this._scrollStyle = 'overlay'; this._data = []; this._dataState = 'idle'; this._page = 1; this._pageSize = 50; this._totalItems = 0; this._totalPages = 0; this._searchQuery = ''; this._canScrollLeft = false; this._canScrollRight = false; this._canScrollHorizontal = false; this._columnPickerOpen = false; this._filterPanelOpened = null; this._filterPanelTab = 'filter'; this._buckets = new Map(); this._filterPanelPos = { top: 0, left: 0 }; this._sorts = []; this._resizing = null; this._resizeObserver = null; this._searchPositionLocked = false; this._model = new KRTableModel(); this.def = { columns: [] }; this._handleClickOutside = (e) => { const path = e.composedPath(); if (this._columnPickerOpen) { const picker = this.shadowRoot?.querySelector('.column-picker-wrapper'); if (picker && !path.includes(picker)) { this._columnPickerOpen = false; } } if (this._filterPanelOpened) { if (!path.some((el) => el.classList?.contains('filter-panel'))) { this._handleFilterApply(); } } }; this._handleResizeMove = (e) => { if (!this._resizing) return; const col = this._model.columns.find(c => c.id === this._resizing.columnId); if (col) { const newWidth = this._resizing.startWidth + (e.clientX - this._resizing.startX); col.width = `${Math.min(900, Math.max(50, newWidth))}px`; this.requestUpdate(); } }; this._handleResizeEnd = () => { this._resizing = null; document.removeEventListener('mousemove', this._handleResizeMove); document.removeEventListener('mouseup', this._handleResizeEnd); }; } connectedCallback() { super.connectedCallback(); this.classList.toggle('kr-table--scroll-overlay', this._scrollStyle === 'overlay'); this.classList.toggle('kr-table--scroll-edge', this._scrollStyle === 'edge'); this._fetch(); this._initRefresh(); document.addEventListener('click', this._handleClickOutside); this._resizeObserver = new ResizeObserver(() => { // Unlock and recalculate on resize since layout changes this._searchPositionLocked = false; this._updateSearchPosition(); }); this._resizeObserver.observe(this); } disconnectedCallback() { super.disconnectedCallback(); clearInterval(this._refreshTimer); document.removeEventListener('click', this._handleClickOutside); this._resizeObserver?.disconnect(); } willUpdate(changedProperties) { if (changedProperties.has('def')) { // Build internal model from user-provided def this._model = new KRTableModel(); if (this.def.title) { this._model.title = this.def.title; } if (this.def.actions) { this._model.actions = this.def.actions; } if (this.def.data) { this._model.data = this.def.data; } if (this.def.dataSource) { this._model.dataSource = this.def.dataSource; } if (typeof this.def.refreshInterval === 'number') { this._model.refreshInterval = this.def.refreshInterval; } if (typeof this.def.pageSize === 'number') { this._model.pageSize = this.def.pageSize; } if (this.def.rowClickable) { this._model.rowClickable = this.def.rowClickable; } if (this.def.rowHref) { this._model.rowHref = this.def.rowHref; } this._model.columns = this.def.columns.map(col => { const column = { ...col, filter: null }; if (!column.type) { column.type = 'string'; } if (column.type === 'actions') { column.label = col.label ?? ''; column.sticky = 'right'; column.resizable = false; return column; } if (col.filterable || col.facetable) { column.filter = new KRQuery(); column.filter.field = col.id; column.filter.type = column.type; if (col.filter) { column.filter.setOperator(col.filter.operator); column.filter.setValue(col.filter.value); } else if (col.facetable && !col.filterable) { column.filter.operator = 'in'; column.filter.value = []; } else if (column.filter.type === 'string') { column.filter.operator = 'contains'; } } return column; }); if (this.def.displayedColumns) { this._model.displayedColumns = this.def.displayedColumns; } else { this._model.displayedColumns = this._model.columns.map(c => c.id); } this._fetch(); this._initRefresh(); } } updated(changedProperties) { this._updateScrollFlags(); this._syncSlottedContent(); } /** Syncs light DOM content for cells with custom render functions */ _syncSlottedContent() { const columns = this.getDisplayedColumns().filter(col => col.render); if (!columns.length) return; // Clear old slotted content this.querySelectorAll('[slot^=\"cell-\"]').forEach(el => el.remove()); // Create new slotted content this._data.forEach((row, rowIndex) => { columns.forEach(col => { const result = col.render(row); if (!result) return; const el = document.createElement('span'); el.slot = `cell-${rowIndex}-${col.id}`; if (col.type === 'actions') { el.style.display = 'flex'; el.style.gap = '8px'; } if (typeof result === 'string') { el.innerHTML = result; } else { render(result, el); } this.appendChild(el); }); }); } // ---------------------------------------------------------------------------- // Public Interface // ---------------------------------------------------------------------------- refresh() { this._fetch(); } goToPrevPage() { if (this._page > 1) { this._page--; this._fetch(); } } goToNextPage() { if (this._page < this._totalPages) { this._page++; this._fetch(); } } goToPage(page) { if (page >= 1 && page <= this._totalPages) { this._page = page; this._fetch(); } } // ---------------------------------------------------------------------------- // Data Fetching // ---------------------------------------------------------------------------- _toSolrData() { const request = { page: this._page - 1, size: this._pageSize, sorts: this._sorts, filterFields: [], queryFields: [], facetFields: [] }; for (const col of this._model.columns) { if (!col.filter || col.filter.isEmpty() || !col.filter.isValid()) { continue; } const filterData = col.filter.toSolrData(); if (col.facetable && col.filter.operator === 'in') { filterData.tagged = true; } request.filterFields.push(filterData); } for (const col of this._model.columns) { if (!col.facetable) { continue; } request.facetFields.push({ name: col.id, type: 'FIELD', limit: 100, sort: 'count', minimumCount: 1 }); } if (this._searchQuery?.trim().length) { request.queryFields.push({ name: '_text_', operation: 'IS', value: termify(this._searchQuery, false) }); } return request; } _toDbParams() { const request = { page: this._page - 1, size: this._pageSize, sorts: this._sorts, filterFields: [], queryFields: [], facetFields: [] }; for (const col of this._model.columns) { if (!col.filter || col.filter.isEmpty() || !col.filter.isValid()) { continue; } request.filterFields.push(col.filter.toDbParams()); } if (this._searchQuery?.trim().length) { this._model.columns.filter(col => col.searchable).forEach(col => { request.queryFields.push({ name: col.id, operation: 'CONTAINS', value: this._searchQuery, and: false }); }); } return request; } /** * Fetches data from the API and updates the table. * Shows a loading spinner while fetching, then displays rows on success * or an error snackbar on failure. * Request/response format depends on dataSource.mode (solr, opensearch, db). */ _fetch() { if (this._model.data) { this._data = this._model.data; this._totalItems = this._model.data.length; this._totalPages = Math.ceil(this._model.data.length / this._pageSize); this._dataState = 'success'; return; } if (!this._model.dataSource) return; this._dataState = 'loading'; let request; if (this._model.dataSource.mode === 'db') { request = this._toDbParams(); } else { request = this._toSolrData(); } this._model.dataSource.fetch(request) .then(response => { // Parse response based on mode switch (this._model.dataSource?.mode) { case 'opensearch': { throw Error('Opensearch not supported yet'); break; } case 'db': { const res = response; this._data = res.data.content; this._totalItems = res.data.totalElements; this._totalPages = res.data.totalPages; this._pageSize = res.data.size; break; } default: { // solr const res = response; this._data = res.data.content; this._totalItems = res.data.totalElements; this._totalPages = res.data.totalPages; this._pageSize = res.data.size; this._parseFacetResults(res); } } this._dataState = 'success'; this._updateSearchPosition(); }) .catch(err => { this._dataState = 'error'; KRSnackbar.show({ message: err instanceof Error ? err.message : 'Failed to load data', type: 'error' }); }); } _parseFacetResults(response) { if (!response.data.facetFields) { return; } for (const col of this._model.columns) { if (!col.facetable) { continue; } const rawBuckets = response.data.facetFields[col.id]; if (!rawBuckets) { this._buckets.set(col.id, []); continue; } const buckets = []; for (const raw of rawBuckets) { // Solr returns boolean facet values as strings — coerce to actual booleans // so they match the filter values stored by toggle(). let val = raw.name; if (col.type === 'boolean' && typeof raw.name === 'string') { if (raw.name === 'true') { val = true; } else if (raw.name === 'false') { val = false; } } if (raw.name === null && raw.count > 0) { buckets.unshift({ val: null, count: raw.count }); } if (raw.name !== null) { buckets.push({ val: val, count: raw.count }); } } // Bucket sync: ensure selected values appear even with 0 results if (col.filter && col.filter.operator === 'in' && Array.isArray(col.filter.value)) { for (const selectedVal of col.filter.value) { if (!buckets.some(b => b.val === selectedVal)) { buckets.push({ val: selectedVal, count: 0 }); } } } this._buckets.set(col.id, buckets); } // Trigger re-render since Map mutation doesn't trigger Lit updates this._buckets = new Map(this._buckets); } /** * Sets up auto-refresh so the table automatically fetches fresh data * at a regular interval (useful for dashboards, monitoring views). * Configured via def.refreshInterval in milliseconds. */ _initRefresh() { clearInterval(this._refreshTimer); if (this._model.refreshInterval && this._model.refreshInterval > 0) { this._refreshTimer = window.setInterval(() => { this._fetch(); }, this._model.refreshInterval); } } _handleSearch(e) { const input = e.target; this._searchQuery = input.value; this._page = 1; this._fetch(); } _getGridTemplateColumns() { const cols = this.getDisplayedColumns(); return cols.map((col) => { // If column has explicit width, use it if (col.width) { return col.width; } // Actions columns: fit content without minimum if (col.type === 'actions') { return 'max-content'; } // No width specified - use content-based sizing with minimum return 'minmax(80px, auto)'; }).join(' '); } /** * Updates search position to be centered with equal gaps from title and tools. * On first call: resets to flex centering, measures position, then locks with fixed margin. * Subsequent calls are ignored unless _searchPositionLocked is reset (e.g., on resize). */ _updateSearchPosition() { // Skip if already locked (prevents shifts on pagination changes) if (this._searchPositionLocked) return; const search = this.shadowRoot?.querySelector('.search'); const searchField = search?.querySelector('.search-field'); if (!search || !searchField) return; // Reset to flex centering search.style.justifyContent = 'center'; searchField.style.marginLeft = ''; requestAnimationFrame(() => { const searchRect = search.getBoundingClientRect(); const fieldRect = searchField.getBoundingClientRect(); // Calculate how far from the left of search container the field currently is const currentOffset = fieldRect.left - searchRect.left; // Lock position: switch to flex-start and use fixed margin search.style.justifyContent = 'flex-start'; searchField.style.marginLeft = `${currentOffset}px`; // Mark as locked so pagination changes don't shift the search this._searchPositionLocked = true; }); } // ---------------------------------------------------------------------------- // Columns // ---------------------------------------------------------------------------- _toggleColumnPicker() { this._columnPickerOpen = !this._columnPickerOpen; } _toggleColumn(columnId) { if (this._model.displayedColumns.includes(columnId)) { this._model.displayedColumns = this._model.displayedColumns.filter(id => id !== columnId); } else { this._model.displayedColumns = [...this._model.displayedColumns, columnId]; } this.requestUpdate(); } // Clear any existing text selection on mousedown so we only detect // selections made during this click gesture, not stale selections from elsewhere _handleRowMouseDown() { if (!this._model.rowClickable) { return; } window.getSelection()?.removeAllRanges(); } _handleRowClick(row, rowIndex) { if (!this._model.rowClickable) { return; } const selection = window.getSelection(); if (selection && selection.toString().length > 0) { return; } this.dispatchEvent(new CustomEvent('row-click', { detail: { row, rowIndex }, bubbles: true, composed: true })); } // When a user toggles a column on via the column picker, it gets appended // to _displayedColumns. By mapping over _displayedColumns (not def.columns), // the new column appears at the right edge of the table instead of jumping // back to its original position in the column definition. // Actions columns are always moved to the end. getDisplayedColumns() { return this._model.displayedColumns .map(id => this._model.columns.find(col => col.id === id)) .sort((a, b) => { if (a.type === 'actions' && b.type !== 'actions') return 1; if (a.type !== 'actions' && b.type === 'actions') return -1; return 0; }); } // ---------------------------------------------------------------------------- // Scrolling // ---------------------------------------------------------------------------- /** * Scroll event handler that updates scroll flags in real-time as user scrolls. * Updates shadow indicators to show if more content exists left/right. */ _handleScroll(e) { const container = e.target; this._canScrollLeft = container.scrollLeft > 0; this._canScrollRight = container.scrollLeft < container.scrollWidth - container.clientWidth - 1; } /** * Updates scroll state flags for the table content container. * - _canScrollLeft: true if scrolled right (can scroll back left) * - _canScrollRight: true if more content exists to the right * - _canScrollHorizontal: true if content is wider than container * These flags control scroll shadow indicators and CSS classes. */ _updateScrollFlags() { const container = this.shadowRoot?.querySelector('.content'); if (container) { this._canScrollLeft = container.scrollLeft > 0; this._canScrollRight = container.scrollWidth > container.clientWidth && container.scrollLeft < container.scrollWidth - container.clientWidth - 1; this._canScrollHorizontal = container.scrollWidth > container.clientWidth; } this.classList.toggle('kr-table--scroll-left-available', this._canScrollLeft); this.classList.toggle('kr-table--scroll-right-available', this._canScrollRight); this.classList.toggle('kr-table--scroll-horizontal-available', this._canScrollHorizontal); this.classList.toggle('kr-table--sticky-left', this.getDisplayedColumns().some(c => c.sticky === 'left')); this.classList.toggle('kr-table--sticky-right', this.getDisplayedColumns().some(c => c.sticky === 'right')); } // ---------------------------------------------------------------------------- // Column Resizing // ---------------------------------------------------------------------------- _handleResizeStart(e, columnId) { e.preventDefault(); const headerCell = this.shadowRoot?.querySelector(`.header-cell[data-column-id=\"${columnId}\"]`); this._resizing = { columnId, startX: e.clientX, startWidth: headerCell?.offsetWidth || 200 }; document.addEventListener('mousemove', this._handleResizeMove); document.addEventListener('mouseup', this._handleResizeEnd); } // ---------------------------------------------------------------------------- // Sorting // ---------------------------------------------------------------------------- _handleSortClick(e, column) { if (e.shiftKey) { // Multi-sort: add or cycle existing const existingIndex = this._sorts.findIndex(s => s.sortBy === column.id); if (existingIndex === -1) { this._sorts.push({ sortBy: column.id, sortDirection: 'asc' }); } else { const existing = this._sorts[existingIndex]; if (existing.sortDirection === 'asc') { existing.sortDirection = 'desc'; } else { // on third click, remove sorting for the column this._sorts.splice(existingIndex, 1); } } this.requestUpdate(); } else { // Single sort: replace all let existing = null; if (this._sorts.length === 1) { existing = this._sorts.find(s => s.sortBy === column.id); } if (!existing) { this._sorts = [{ sortBy: column.id, sortDirection: 'asc' }]; } else if (existing.sortDirection === 'asc') { this._sorts = [{ sortBy: column.id, sortDirection: 'desc' }]; } else { this._sorts = []; } } this._page = 1; this._fetch(); } _renderSortIndicator(column) { if (!column.sortable) { return nothing; } const sortIndex = this._sorts.findIndex(s => s.sortBy === column.id); if (sortIndex === -1) { // Ghost arrow: visible only on hover via CSS return html ` <span class=\"header-cell__sort\" @click=${(e) => this._handleSortClick(e, column)}> <svg class=\"header-cell__sort-arrow header-cell__sort-arrow--ghost\" viewBox=\"0 0 24 24\" fill=\"currentColor\"> <path d=\"M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z\"/> </svg> </span> `; } let arrowStyle = {}; if (this._sorts[sortIndex].sortDirection === 'desc') { arrowStyle = { transform: 'rotate(180deg)' }; } return html ` <span class=\"header-cell__sort\" @click=${(e) => this._handleSortClick(e, column)}> <svg class=\"header-cell__sort-arrow\" viewBox=\"0 0 24 24\" fill=\"currentColor\" style=${styleMap(arrowStyle)}> <path d=\"M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z\"/> </svg> ${this._sorts.length > 1 ? html ` <span class=\"header-cell__sort-priority\">${sortIndex + 1}</span> ` : nothing} </span> `; } // ---------------------------------------------------------------------------- // Header // ---------------------------------------------------------------------------- _handleAction(action) { if (action.href) { return; } this.dispatchEvent(new CustomEvent('action', { detail: { action: action.id }, bubbles: true, composed: true })); } // ---------------------------------------------------------------------------- // Filter Handlers // ---------------------------------------------------------------------------- _handleKqlChange(e, column) { const kql = e.target.value.trim(); if (!kql) { column.filter.clear(); this.requestUpdate(); } else { column.filter.setKql(kql); this.requestUpdate(); if (!column.filter.isValid()) { return; } } this._page = 1; this._fetch(); } _handleFilterPanelToggle(e, column) { e.stopPropagation(); if (this._filterPanelOpened === column.id) { this._filterPanelOpened = null; } else { const rect = e.currentTarget.getBoundingClientRect(); let left = rect.left; if (left + 328 > window.innerWidth) { left = window.innerWidth - 328; } this._filterPanelPos = { top: rect.bottom + 4, left }; this._filterPanelOpened = column.id; if (column.facetable) { this._filterPanelTab = 'counts'; } else { this._filterPanelTab = 'filter'; } } } _handleKqlClear(column) { column.filter.clear(); this._page = 1; this._fetch(); } _handleFilterClear() { const column = this._model.columns.find(c => c.id === this._filterPanelOpened); if (column) { column.filter.clear(); if (column.facetable && !column.filterable) { column.filter.operator = 'in'; column.filter.value = []; } } this._filterPanelOpened = null; this._page = 1; this._fetch(); } _handleFilterTextKeydown(e, column) { if (e.key === 'Enter') { e.preventDefault(); this._handleFilterApply(); } } _handleOperatorChange(e, column) { column.filter.setOperator(e.target.value); this.requestUpdate(); } _handleFilterStringChange(e, column) { column.filter.setValue(e.target.value); this.requestUpdate(); } _handleFilterNumberChange(e, column) { column.filter.setValue(Number(e.target.value)); this.requestUpdate(); } _handleFilterDateChange(e, column) { column.filter.setValue(new Date(e.target.value), 'day'); this.requestUpdate(); } _handleFilterBooleanChange(e, column) { column.filter.setValue(e.target.value === 'true'); this.requestUpdate(); } _handleFilterDateStartChange(e, column) { column.filter.setStart(new Date(e.target.value), 'day'); this.requestUpdate(); } _handleFilterDateEndChange(e, column) { column.filter.setEnd(new Date(e.target.value), 'day'); this.requestUpdate(); } _handleFilterNumberStartChange(e, column) { column.filter.setStart(Number(e.target.value)); this.requestUpdate(); } _handleFilterNumberEndChange(e, column) { column.filter.setEnd(Number(e.target.value)); this.requestUpdate(); } _handleFilterListChange(e, column) { const items = e.target.value.split(',').map((v) => v.trim()).filter((v) => v !== ''); if (column.type === 'number') { column.filter.setValue(items.map((v) => Number(v))); } else { column.filter.setValue(items); } this.requestUpdate(); } _handleFilterApply() { this._filterPanelOpened = null; this._page = 1; this._fetch(); } _handleFilterPanelTabChange(e) { this._filterPanelTab = e.detail.activeTabId; } _handleBucketToggle(e, column, bucket) { column.filter.toggle(bucket.val); this._page = 1; this._fetch(); } // ---------------------------------------------------------------------------- // Rendering // ---------------------------------------------------------------------------- _renderCellContent(column, row, rowIndex) { const value = row[column.id]; if (column.render) { // Use slot to project content from light DOM so external styles apply return html `<slot name=\"cell-${rowIndex}-${column.id}\"></slot>`; } if (value === null || value === undefined) { return ''; } switch (column.type) { case 'number': if (column.format === 'currency' && typeof value === 'number') { return value.toLocaleString('en-US', { style: 'currency', currency: 'USD' }); } return String(value); case 'date': { let date; if (value instanceof Date) { date = value; } else if (typeof value === 'string' && /^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}/.test(value)) { // MySQL datetime format (UTC): \"2026-01-28 01:33:44:517\" // Replace last colon before ms with dot, append Z for UTC const isoString = value.replace(/(\\d{2}:\\d{2}:\\d{2}):(\\d+)$/, '$1.$2').replace(' ', 'T') + 'Z'; date = new Date(isoString); } else { date = new Date(value); } // Show date and time for datetime values in UTC return date.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', timeZone: 'UTC' }); } case 'boolean': if (value === true) return 'Yes'; if (value === false) return 'No'; return ''; default: return String(value); } } /** * Returns CSS classes for a header cell based on column config. */ _getHeaderCellClasses(column, index) { return { 'header-cell': true, 'header-cell--sortable': !!column.sortable, 'header-cell--align-center': column.align === 'center', 'header-cell--align-right': column.align === 'right', 'header-cell--sticky-left': column.sticky === 'left', 'header-cell--sticky-left-last': column.sticky === 'left' && !this.getDisplayedColumns().slice(index + 1).some(c => c.sticky === 'left'), 'header-cell--sticky-right': column.sticky === 'right', 'header-cell--sticky-right-first': column.sticky === 'right' && !this.getDisplayedColumns().slice(0, index).some(c => c.sticky === 'right') }; } /** * Returns CSS classes for a table cell based on column config: * - Alignment (center, right) * - Sticky positioning (left, right) * - Border classes for the last left-sticky or first right-sticky column */ _getCellClasses(column, index) { return { 'cell': true, 'cell--actions': column.type === 'actions', 'cell--align-center': column.align === 'center', 'cell--align-right': column.align === 'right', 'cell--sticky-left': column.sticky === 'left', 'cell--sticky-left-last': column.sticky === 'left' && !this.getDisplayedColumns().slice(index + 1).some(c => c.sticky === 'left'), 'cell--sticky-right': column.sticky === 'right', 'cell--sticky-right-first': column.sticky === 'right' && !this.getDisplayedColumns().slice(0, index).some(c => c.sticky === 'right') }; } /** * Returns inline styles for a table cell: * - Width (from column config or default 150px) * - Min-width (if specified) * - Left/right offset for sticky columns (calculated from widths of preceding sticky columns) */ _getCellStyle(column, index) { const styles = {}; if (column.sticky === 'left') { let leftOffset = 0; for (let i = 0; i < index; i++) { const col = this.getDisplayedColumns()[i]; if (col.sticky === 'left') { leftOffset += parseInt(col.width || '0', 10); } } styles.left = `${leftOffset}px`; } if (column.sticky === 'right') { let rightOffset = 0; for (let i = index + 1; i < this.getDisplayedColumns().length; i++) { const col = this.getDisplayedColumns()[i]; if (col.sticky === 'right') { rightOffset += parseInt(col.width || '0', 10); } } styles.right = `${rightOffset}px`; } return styles; } /** * Renders the pagination controls: * - Previous page arrow (disabled on first page) * - Range text showing \"1-50 of 150\" format * - Next page arrow (disabled on last page) * * Hidden when there's no data or all data fits on one page. */ _renderPagination() { const start = (this._page - 1) * this._pageSize + 1; const end = Math.min(this._page * this._pageSize, this._totalItems); return html ` <div class=\"pagination\"> <span class=\"pagination-icon ${this._page === 1 ? 'pagination-icon--disabled' : ''}\" @click=${this.goToPrevPage} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z\"/></svg> </span> <span class=\"pagination-info\">${start}-${end} of ${this._totalItems}</span> <span class=\"pagination-icon ${this._page === this._totalPages ? 'pagination-icon--disabled' : ''}\" @click=${this.goToNextPage} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\"/></svg> </span> </div> `; } /** * Renders the header toolbar containing: * - Title (left) * - Search bar with view selector dropdown (center) * - Tools (right): page navigation, refresh button, column visibility picker, actions dropdown * * Hidden when there's no title, no actions, and data fits on one page. */ _renderHeader() { if (!this._model.title && !this._model.actions?.length && this._totalPages <= 1) { return nothing; } return html ` <div class=\"header\"> <div class=\"title\">${this._model.title ?? ''}</div> ${this._model.dataSource?.mode === 'db' && !this._model.columns.some(col => col.searchable) ? html `<div class=\"search\"></div>` : html ` <div class=\"search\"> <!-- TODO: Saved views dropdown <div class=\"views\"> <span>Default View</span> <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z\"/></svg> </div> --> <div class=\"search-field\"> <svg class=\"search-icon\" viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z\"/></svg> <input type=\"text\" class=\"search-input\" placeholder=\"Search...\" .value=${this._searchQuery} @input=${this._handleSearch} /> </div> </div> `} <div class=\"tools\"> ${this._renderPagination()} <span class=\"refresh\" title=\"Refresh\" @click=${() => this.refresh()}> <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M480-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100 70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z\"/></svg> </span> <div class=\"column-picker-wrapper\"> <span class=\"header-icon\" title=\"Columns\" @click=${this._toggleColumnPicker}> <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M121-280v-400q0-33 23.5-56.5T201-760h559q33 0 56.5 23.5T840-680v400q0 33-23.5 56.5T760-200H201q-33 0-56.5-23.5T121-280Zm79 0h133v-400H200v400Zm213 0h133v-400H413v400Zm213 0h133v-400H626v400Z\"/></svg> </span> <div class=\"column-picker ${this._columnPickerOpen ? 'open' : ''}\"> ${[...this._model.columns].filter(col => col.type !== 'actions').sort((a, b) => (a.label ?? a.id).localeCompare(b.label ?? b.id)).map(col => html ` <div class=\"column-picker-item\" @click=${() => this._toggleColumn(col.id)}> <div class=\"column-picker-checkbox ${this._model.displayedColumns.includes(col.id) ? 'checked' : ''}\"> <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z\"/></svg> </div> <span class=\"column-picker-label\">${col.label ?? col.id}</span> </div> `)} </div> </div> ${this._model.actions?.length === 1 ? html ` <kr-button class=\"actions\" .href=${this._model.actions[0].href} .target=${this._model.actions[0].target} @click=${() => this._handleAction(this._model.actions[0])} > ${this._model.actions[0].label} </kr-button> ` : this._model.actions?.length ? html ` <kr-button class=\"actions\" .options=${this._model.actions.map(a => ({ id: a.id, label: a.label }))} @option-select=${(e) => this._handleAction({ id: e.detail.id, label: e.detail.label })} > Actions </kr-button> ` : nothing} </div> </div> `; } /** Renders status message (loading, error, empty) */ _renderStatus() { if (this._dataState === 'loading' && this._data.length === 0) { return html `<div class=\"status\">Loading...</div>`; } if (this._dataState === 'error' && this._data.length === 0) { return html `<div class=\"status status--error\">Error loading data</div>`; } if (this._data.length === 0) { return html `<div class=\"status\">No data available</div>`; } return nothing; } _renderFilterPanel() { if (!this._filterPanelOpened) { return nothing; } const column = this._model.columns.find(c => c.id === this._filterPanelOpened); // Build filter content (operator + value input) let valueInput = html ``; if (column.filter.operator === 'empty' || column.filter.operator === 'n_empty') { valueInput = html ` <input type=\"text\" class=\"filter-panel__input\" disabled .value=${column.filter.text} /> `; } else if (column.filter.operator === 'between' && column.type === 'date') { valueInput = html ` <input type=\"date\" class=\"filter-panel__input\" .valueAsDate=${column.filter.value?.start ?? null} @change=${(e) => this._handleFilterDateStartChange(e, column)} /> <input type=\"date\" class=\"filter-panel__input\" .valueAsDate=${column.filter.value?.end ?? null} @change=${(e) => this._handleFilterDateEndChange(e, column)} /> `; } else if (column.filter.operator === 'between' && column.type === 'number') { valueInput = html ` <input type=\"number\" class=\"filter-panel__input\" placeholder=\"Start\" .value=${column.filter.value?.start ?? ''} @input=${(e) => this._handleFilterNumberStartChange(e, column)} @keydown=${(e) => this._handleFilterTextKeydown(e, column)} /> <input type=\"number\" class=\"filter-panel__input\" placeholder=\"End\" .value=${column.filter.value?.end ?? ''} @input=${(e) => this._handleFilterNumberEndChange(e, column)} @keydown=${(e) => this._handleFilterTextKeydown(e, column)} /> `; } else if (column.filter.operator === 'in') { valueInput = html ` <textarea class=\"filter-panel__textarea\" rows=\"3\" placeholder=\"Values (comma-separated)\" .value=${column.filter.text} @input=${(e) => this._handleFilterListChange(e, column)} @keydown=${(e) => this._handleFilterTextKeydown(e, column)} ></textarea> `; } else if (column.type === 'boolean') { valueInput = html ` <kr-select-field placeholder=\"Value\" .value=${String(column.filter.value ?? '')} @change=${(e) => this._handleFilterBooleanChange(e, column)} > <kr-select-option value=\"true\">Yes</kr-select-option> <kr-select-option value=\"false\">No</kr-select-option> </kr-select-field> `; } else if (column.type === 'date') { valueInput = html ` <input type=\"date\" class=\"filter-panel__input\" .valueAsDate=${column.filter.value} @change=${(e) => this._handleFilterDateChange(e, column)} /> `; } else if (column.type === 'number') { valueInput = html ` <input type=\"number\" class=\"filter-panel__input\" placeholder=\"Value\" min=\"0\" .value=${column.filter.text} @input=${(e) => this._handleFilterNumberChange(e, column)} @keydown=${(e) => this._handleFilterTextKeydown(e, column)} /> `; } else { valueInput = html ` <input type=\"text\" class=\"filter-panel__input\" placeholder=\"Value\" .value=${column.filter.text} @input=${(e) => this._handleFilterStringChange(e, column)} @keydown=${(e) => this._handleFilterTextKeydown(e, column)} /> `; } const filterContent = html ` <div class=\"filter-panel__content\"> <kr-select-field .value=${column.filter.operator} @change=${(e) => this._handleOperatorChange(e, column)} > ${getOperatorsForType(column.type).map(op => html ` <kr-select-option value=${op.key}>${op.label}</kr-select-option> `)} </kr-select-field> ${valueInput} </div> `; // Build bucket list content const buckets = this._buckets.get(column.id) || []; let bucketContent; if (!buckets.length) { bucketContent = html `<div class=\"bucket-empty\">No data</div>`; } else { bucketContent = html ` <div class=\"buckets\"> ${buckets.map(bucket => { let bucketLabel = '(Empty)'; if (bucket.val !== null && bucket.val !== undefined) { if (column.type === 'boolean') { if (bucket.val === true || bucket.val === 'true') { bucketLabel = 'Yes'; } else { bucketLabel = 'No'; } } else { bucketLabel = String(bucket.val); } } let checkIcon = nothing; if (column.filter.has(bucket.val)) { checkIcon = html ` <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"> <path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z\"/> </svg> `; } return html ` <div class=\"bucket\" @click=${(e) => this._handleBucketToggle(e, column, bucket)} > <div class=${classMap({ 'bucket__checkbox': true, 'bucket__checkbox--checked': column.filter.has(bucket.val) })}> ${checkIcon} </div> <span class=\"bucket__label\">${bucketLabel}</span> <span class=\"bucket__count\">${bucket.count}</span> </div> `; })} </div> `; } // Build panel body — tabs if both filterable+facetable, otherwise just the relevant content let panelBody; if (column.facetable && column.filterable) { panelBody = html ` <kr-tab-group size=\"small\" active-tab-id=${this._filterPanelTab} @tab-change=${(e) => this._handleFilterPanelTabChange(e)} > <kr-tab id=\"filter\" label=\"Filter\"> ${filterContent} </kr-tab> <kr-tab id=\"counts\" label=\"Counts\"> ${bucketContent} </kr-tab> </kr-tab-group> `; } else if (column.facetable) { panelBody = bucketContent; } else { panelBody = filterContent; } return html ` <div class=\"filter-panel\" style=${styleMap({ top: this._filterPanelPos.top + 'px', left: this._filterPanelPos.left + 'px' })} > ${panelBody} <div class=\"filter-panel__actions\"> <kr-button variant=\"outline\" color=\"secondary\" size=\"small\" @click=${this._handleFilterClear}> Clear </kr-button> <kr-button size=\"small\" @click=${this._handleFilterApply}> Apply </kr-button> </div> </div> `; } /** * Renders filter row below column headers. * Only displays for columns with filterable: true. */ _renderFilterRow() { const columns = this.getDisplayedColumns(); if (!columns.some(col => col.filterable || col.facetable)) { return nothing; } return html ` <div class=\"filter-row\"> ${columns.map((col, i) => { if (!col.filterable && !col.facetable) { return html `<div class=${classMap({ 'filter-cell': true, 'filter-cell--sticky-left': col.sticky === 'left', 'filter-cell--sticky-right': col.sticky === 'right', 'filter-cell--sticky-right-first': col.sticky === 'right' && !columns.slice(0, i).some((c) => c.sticky === 'right') })} style=${styleMap(this._getCellStyle(col, i))} ></div>`; } return html ` <div class=${classMap({ 'filter-cell': true, 'filter-cell--sticky-left': col.sticky === 'left', 'filter-cell--sticky-right': col.sticky === 'right', 'filter-cell--sticky-right-first': col.sticky === 'right' && !columns.slice(0, i).some((c) => c.sticky === 'right') })} style=${styleMap(this._getCellStyle(col, i))} > <div class=\"filter-cell__wrapper\"> <input type=\"text\" class=${classMap({ 'filter-cell__input': true, 'filter-cell__input--invalid': !col.filter.isValid() })} .value=${col.filter.kql} @change=${(e) => this._handleKqlChange(e, col)} /> ${col.filter?.kql?.length > 0 ? html ` <button class=\"filter-cell__clear\" @click=${() => this._handleKqlClear(col)} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"> <path d=\"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z\"/> </svg> </button> ` : nothing} <button class=${classMap({ 'filter-cell__advanced': true, 'filter-cell__advanced--opened': this._filterPanelOpened === col.id })} @click=${(e) => this._handleFilterPanelToggle(e, col)} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"> <path d=\"M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z\"/> </svg> </button> </div> </div> `; })} </div> `; } /** Renders the scrollable data grid with column headers and rows. */ _renderTable() { return html ` <div class=\"wrapper\"> <div class=\"overlay-left\"></div> <div class=\"overlay-right\"></div> ${this._renderStatus()} <div class=\"content\" @scroll=${this._handleScroll}> <div class=\"table\" style=\"grid-template-columns: ${this._getGridTemplateColumns()}\"> <div class=\"header-row\"> ${this.getDisplayedColumns().map((col, i) => html ` <div class=${classMap(this._getHeaderCellClasses(col, i))} style=${styleMap(this._getCellStyle(col, i))} data-column-id=${col.id} > <span class=\"header-cell__label\">${col.label ?? col.id}</span> ${this._renderSortIndicator(col)} ${col.resizable !== false ? html `<div class=\"header-cell__resize\" @mousedown=${(e) => this._handleResizeStart(e, col.id)} ></div>` : nothing} </div> `)} </div> ${this._renderFilterRow()} ${this._data.map((row, rowIndex) => { const cells = this.getDisplayedColumns().map((col, i) => html ` <div class=${classMap(this._getCellClasses(col, i))} style=${styleMap(this._getCellStyle(col, i))} data-column-id=${col.id} > ${this._renderCellContent(col, row, rowIndex)} </div> `); if (this._model.rowHref) { return html ` <a href=${this._model.rowHref(row)} class=${classMap({ 'row': true, 'row--clickable': true, 'row--link': true })} @mousedown=${() => this._handleRowMouseDown()} @click=${() => this._handleRowClick(row, rowIndex)} >${cells}</a> `; } return html ` <div class=${classMap({ 'row': true, 'row--clickable': !!this._model.rowClickable })} @mousedown=${() => this._handleRowMouseDown()} @click=${() => this._handleRowClick(row, rowIndex)} >${cells}</div> `; })} </div> </div> </div> `; } /** * Renders a data table with: * - Header bar with title, search input with view selector, and tools (pagination, refresh, column visibility, actions dropdown) * - Scrollable grid with sticky header row and optional sticky left/right columns * - Loading, error message, or empty state when no data */ render() { if (!this._model.columns.length) { return html `<slot></slot>`; } return html ` ${this._renderHeader()} ${this._renderTable()} ${this._renderFilterPanel()} `; } }"
2586
2586
  }
2587
2587
  ],
2588
2588
  "exports": [
@@ -5483,6 +5483,15 @@
5483
5483
  "privacy": "private",
5484
5484
  "default": "{ top: 0, left: 0 }"
5485
5485
  },
5486
+ {
5487
+ "kind": "field",
5488
+ "name": "_sorts",
5489
+ "type": {
5490
+ "text": "KRSolrSort[]"
5491
+ },
5492
+ "privacy": "private",
5493
+ "default": "[]"
5494
+ },
5486
5495
  {
5487
5496
  "kind": "field",
5488
5497
  "name": "_resizing",
@@ -5724,6 +5733,38 @@
5724
5733
  "name": "_handleResizeEnd",
5725
5734
  "privacy": "private"
5726
5735
  },
5736
+ {
5737
+ "kind": "method",
5738
+ "name": "_handleSortClick",
5739
+ "privacy": "private",
5740
+ "parameters": [
5741
+ {
5742
+ "name": "e",
5743
+ "type": {
5744
+ "text": "MouseEvent"
5745
+ }
5746
+ },
5747
+ {
5748
+ "name": "column",
5749
+ "type": {
5750
+ "text": "KRTableColumn"
5751
+ }
5752
+ }
5753
+ ]
5754
+ },
5755
+ {
5756
+ "kind": "method",
5757
+ "name": "_renderSortIndicator",
5758
+ "privacy": "private",
5759
+ "parameters": [
5760
+ {
5761
+ "name": "column",
5762
+ "type": {
5763
+ "text": "KRTableColumn"
5764
+ }
5765
+ }
5766
+ ]
5767
+ },
5727
5768
  {
5728
5769
  "kind": "method",
5729
5770
  "name": "_handleAction",