@kodaris/krubble-components 1.0.11 → 1.0.12

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.
@@ -256,7 +256,7 @@
256
256
  {
257
257
  "kind": "variable",
258
258
  "name": "KRTable",
259
- "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._displayedColumns = []; this._widthsLocked = false; this._resizing = null; this._resizeObserver = null; this._searchPositionLocked = false; this._def = { columns: [] }; this.def = { columns: [] }; this._handleClickOutsideColumnPicker = (e) => { if (!this._columnPickerOpen) return; const path = e.composedPath(); const picker = this.shadowRoot?.querySelector('.column-picker-wrapper'); if (picker && !path.includes(picker)) { this._columnPickerOpen = false; } }; this._handleResizeMove = (e) => { if (!this._resizing) return; const col = this._def.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._handleClickOutsideColumnPicker); 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._handleClickOutsideColumnPicker); this._resizeObserver?.disconnect(); } willUpdate(changedProperties) { if (changedProperties.has('def')) { // Copy user's def and normalize action columns this._def = { ...this.def, columns: this.def.columns.map(col => { if (col.type === 'actions') { return { ...col, sticky: 'right', resizable: false }; } return { ...col }; }) }; this._displayedColumns = this._def.displayedColumns || this._def.columns.map(c => c.id); this._widthsLocked = false; this.classList.remove('kr-table--widths-locked'); this._fetch(); this._initRefresh(); } } updated(changedProperties) { this._updateScrollFlags(); } // ---------------------------------------------------------------------------- // 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 // ---------------------------------------------------------------------------- /** * 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._def.dataSource) return; this._dataState = 'loading'; // Build request based on mode let request; switch (this._def.dataSource.mode) { case 'opensearch': throw Error('Opensearch not supported yet'); case 'db': throw Error('DB not supported yet'); default: // solr request = { page: this._page - 1, size: this._pageSize, sorts: [], filterFields: [], queryFields: [], facetFields: [] }; if (this._searchQuery?.trim().length) { request.queryFields.push({ name: '_text_', operation: 'IS', value: escapeSolrQuery(this._searchQuery) }); } } this._def.dataSource.fetch(request) .then(response => { // Parse response based on mode switch (this._def.dataSource?.mode) { case 'opensearch': { throw Error('Opensearch not supported yet'); } case 'db': { throw Error('DB not supported yet'); } 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._dataState = 'success'; this._updateSearchPosition(); if (!this._widthsLocked) this._lockColumnWidths(); }) .catch(err => { this._dataState = 'error'; KRSnackbar.show({ message: err instanceof Error ? err.message : 'Failed to load data', type: 'error' }); }); } /** * 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._def.refreshInterval && this._def.refreshInterval > 0) { this._refreshTimer = window.setInterval(() => { this._fetch(); }, this._def.refreshInterval); } } _handleSearch(e) { const input = e.target; this._searchQuery = input.value; this._page = 1; this._fetch(); } _measureTextWidth(text, font) { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); ctx.font = font; return ctx.measureText(text).width; } _lockColumnWidths() { this.updateComplete.then(() => { requestAnimationFrame(() => { const headerCell = this.shadowRoot?.querySelector('.header-cell'); const dataCell = this.shadowRoot?.querySelector('.cell'); if (!headerCell) return; const headerStyle = getComputedStyle(headerCell); const headerFont = `${headerStyle.fontWeight} ${headerStyle.fontSize} ${headerStyle.fontFamily}`; const headerPadding = parseFloat(headerStyle.paddingLeft) + parseFloat(headerStyle.paddingRight); const dataStyle = dataCell ? getComputedStyle(dataCell) : headerStyle; const dataFont = `${dataStyle.fontWeight} ${dataStyle.fontSize} ${dataStyle.fontFamily}`; const dataPadding = dataCell ? parseFloat(dataStyle.paddingLeft) + parseFloat(dataStyle.paddingRight) : headerPadding; this.getDisplayedColumns().forEach(col => { if (!col.width) { let width; // For columns with custom render functions, measure the actual DOM element if (col.render) { const cell = this.shadowRoot?.querySelector(`.cell[data-column-id=\"${col.id}\"]`); width = cell ? cell.scrollWidth : 150; } else { const headerText = col.label ?? col.id; const headerWidth = this._measureTextWidth(headerText, headerFont) + headerPadding; let maxDataWidth = 0; for (const row of this._data) { const value = row[col.id]; if (value != null) { const dataWidth = this._measureTextWidth(String(value), dataFont) + dataPadding; if (dataWidth > maxDataWidth) maxDataWidth = dataWidth; } } width = Math.ceil(Math.max(headerWidth, maxDataWidth)); } col.width = `${Math.max(width, 150)}px`; } }); this._widthsLocked = true; this.classList.add('kr-table--widths-locked'); this.requestUpdate(); }); }); } /** * 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._displayedColumns.includes(columnId)) { this._displayedColumns = this._displayedColumns.filter(id => id !== columnId); } else { this._displayedColumns = [...this._displayedColumns, columnId]; } } // 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._displayedColumns .map(id => this._def.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) { this.dispatchEvent(new CustomEvent('action', { detail: { action: action.id }, bubbles: true, composed: true })); } // ---------------------------------------------------------------------------- // Rendering // ---------------------------------------------------------------------------- _renderCellContent(column, row) { const value = row[column.id]; if (column.render) { const result = column.render(row); // If render returns a string, treat it as HTML return typeof result === 'string' ? o$2(result) : result; } if (value === null || value === undefined) { return ''; } switch (column.type) { case 'number': return typeof value === 'number' ? value.toLocaleString() : String(value); case 'currency': return typeof value === 'number' ? value.toLocaleString('en-US', { style: 'currency', currency: 'USD' }) : String(value); case 'date': return value instanceof Date ? value.toLocaleDateString() : new Date(value).toLocaleDateString(); 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--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._def.title && !this._def.actions?.length && this._totalPages <= 1) { return A; } return b ` <div class=\"header\"> <div class=\"title\">${this._def.title ?? ''}</div> <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._def.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._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._def.actions?.length === 1 ? b ` <kr-button class=\"actions\" @click=${() => this._handleAction(this._def.actions[0])} > ${this._def.actions[0].label} </kr-button> ` : this._def.actions?.length ? b ` <kr-button class=\"actions\" .options=${this._def.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; } _getGridTemplateColumns() { const cols = this.getDisplayedColumns(); const lastNonStickyIndex = cols.map((c, i) => c.sticky ? -1 : i).filter(i => i >= 0).pop(); return cols.map((col, i) => { if (i === lastNonStickyIndex && this._widthsLocked) { return `minmax(${col.width || 'auto'}, 1fr)`; } return col.width || 'auto'; }).join(' '); } /** 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._data.map(row => b ` <div class=\"row\"> ${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)} </div> `)} </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._def.columns.length) { return b `<slot></slot>`; } return b ` ${this._renderHeader()} ${this._renderTable()} `; } }"
259
+ "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._displayedColumns = []; this._widthsLocked = false; this._resizing = null; this._resizeObserver = null; this._searchPositionLocked = false; this._def = { columns: [] }; this.def = { columns: [] }; this._handleClickOutsideColumnPicker = (e) => { if (!this._columnPickerOpen) return; const path = e.composedPath(); const picker = this.shadowRoot?.querySelector('.column-picker-wrapper'); if (picker && !path.includes(picker)) { this._columnPickerOpen = false; } }; this._handleResizeMove = (e) => { if (!this._resizing) return; const col = this._def.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._handleClickOutsideColumnPicker); 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._handleClickOutsideColumnPicker); this._resizeObserver?.disconnect(); } willUpdate(changedProperties) { if (changedProperties.has('def')) { // Copy user's def and normalize action columns this._def = { ...this.def, columns: this.def.columns.map(col => { if (col.type === 'actions') { return { ...col, sticky: 'right', resizable: false }; } return { ...col }; }) }; this._displayedColumns = this._def.displayedColumns || this._def.columns.map(c => c.id); this._widthsLocked = false; this.classList.remove('kr-table--widths-locked'); this._fetch(); this._initRefresh(); } } updated(changedProperties) { this._updateScrollFlags(); } // ---------------------------------------------------------------------------- // 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 // ---------------------------------------------------------------------------- /** * 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._def.dataSource) return; this._dataState = 'loading'; // Build request based on mode let request; switch (this._def.dataSource.mode) { case 'opensearch': throw Error('Opensearch not supported yet'); case 'db': throw Error('DB not supported yet'); default: // solr request = { page: this._page - 1, size: this._pageSize, sorts: [], filterFields: [], queryFields: [], facetFields: [] }; if (this._searchQuery?.trim().length) { request.queryFields.push({ name: '_text_', operation: 'IS', value: escapeSolrQuery(this._searchQuery) }); } } this._def.dataSource.fetch(request) .then(response => { // Parse response based on mode switch (this._def.dataSource?.mode) { case 'opensearch': { throw Error('Opensearch not supported yet'); } case 'db': { throw Error('DB not supported yet'); } 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._dataState = 'success'; this._updateSearchPosition(); if (!this._widthsLocked) this._lockColumnWidths(); }) .catch(err => { this._dataState = 'error'; KRSnackbar.show({ message: err instanceof Error ? err.message : 'Failed to load data', type: 'error' }); }); } /** * 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._def.refreshInterval && this._def.refreshInterval > 0) { this._refreshTimer = window.setInterval(() => { this._fetch(); }, this._def.refreshInterval); } } _handleSearch(e) { const input = e.target; this._searchQuery = input.value; this._page = 1; this._fetch(); } _lockColumnWidths() { this.getDisplayedColumns().forEach(col => { if (!col.width) { col.width = '180px'; } }); this._widthsLocked = true; this.classList.add('kr-table--widths-locked'); this.requestUpdate(); } /** * 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._displayedColumns.includes(columnId)) { this._displayedColumns = this._displayedColumns.filter(id => id !== columnId); } else { this._displayedColumns = [...this._displayedColumns, columnId]; } } // 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._displayedColumns .map(id => this._def.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) { this.dispatchEvent(new CustomEvent('action', { detail: { action: action.id }, bubbles: true, composed: true })); } // ---------------------------------------------------------------------------- // Rendering // ---------------------------------------------------------------------------- _renderCellContent(column, row) { const value = row[column.id]; if (column.render) { const result = column.render(row); // If render returns a string, treat it as HTML return typeof result === 'string' ? o$2(result) : result; } if (value === null || value === undefined) { return ''; } switch (column.type) { case 'number': return typeof value === 'number' ? value.toLocaleString() : String(value); case 'currency': return typeof value === 'number' ? value.toLocaleString('en-US', { style: 'currency', currency: 'USD' }) : String(value); case 'date': return value instanceof Date ? value.toLocaleDateString() : new Date(value).toLocaleDateString(); 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._def.title && !this._def.actions?.length && this._totalPages <= 1) { return A; } return b ` <div class=\"header\"> <div class=\"title\">${this._def.title ?? ''}</div> <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._def.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._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._def.actions?.length === 1 ? b ` <kr-button class=\"actions\" @click=${() => this._handleAction(this._def.actions[0])} > ${this._def.actions[0].label} </kr-button> ` : this._def.actions?.length ? b ` <kr-button class=\"actions\" .options=${this._def.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; } _getGridTemplateColumns() { const cols = this.getDisplayedColumns(); const lastNonStickyIndex = cols.map((c, i) => c.sticky ? -1 : i).filter(i => i >= 0).pop(); return cols.map((col, i) => { if (i === lastNonStickyIndex && this._widthsLocked) { return `minmax(${col.width || 'auto'}, 1fr)`; } return col.width || 'auto'; }).join(' '); } /** 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._data.map(row => b ` <div class=\"row\"> ${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)} </div> `)} </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._def.columns.length) { return b `<slot></slot>`; } return b ` ${this._renderHeader()} ${this._renderTable()} `; } }"
260
260
  },
261
261
  {
262
262
  "kind": "variable",
@@ -267,7 +267,7 @@
267
267
  {
268
268
  "kind": "variable",
269
269
  "name": "KRSelect",
270
- "default": "class KRSelect extends i$2 { constructor() { super(); /** * The select label text */ this.label = ''; /** * The input name for form submission */ this.name = ''; /** * The currently selected value */ this.value = ''; /** * Placeholder text when no option is selected */ this.placeholder = 'Select an option'; /** * Whether the select is disabled */ this.disabled = false; /** * Whether the field is required */ this.required = false; /** * Whether the field is readonly */ this.readonly = false; /** * Helper text shown below the select */ this.hint = ''; this._isOpen = false; this._highlightedIndex = -1; this._touched = false; this._handleInvalid = (e) => { e.preventDefault(); this._touched = true; }; this._handleOutsideClick = (e) => { if (!this.contains(e.target)) { this._close(); } }; this._handleKeyDown = (e) => { if (!this._isOpen) return; const options = Array.from(this.querySelectorAll('kr-select-option')); switch (e.key) { case 'Escape': this._close(); this._triggerElement?.focus(); break; case 'ArrowDown': e.preventDefault(); if (options.some(o => !o.disabled)) { let newIndex = this._highlightedIndex + 1; while (newIndex < options.length && options[newIndex]?.disabled) newIndex++; if (newIndex < options.length) this._highlightedIndex = newIndex; } break; case 'ArrowUp': e.preventDefault(); { let newIndex = this._highlightedIndex - 1; while (newIndex >= 0 && options[newIndex]?.disabled) newIndex--; if (newIndex >= 0) this._highlightedIndex = newIndex; } break; case 'Enter': e.preventDefault(); if (this._highlightedIndex >= 0 && this._highlightedIndex < options.length) { this._selectOption(options[this._highlightedIndex]); } break; } }; this._internals = this.attachInternals(); } // Form-associated custom element callbacks 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 = false; this._internals.setFormValue(''); this._internals.setValidity({}); } formStateRestoreCallback(state) { this.value = state; } connectedCallback() { super.connectedCallback(); document.addEventListener('click', this._handleOutsideClick); document.addEventListener('keydown', this._handleKeyDown); this.addEventListener('invalid', this._handleInvalid); } firstUpdated() { this._updateValidity(); } updated(changedProperties) { if (changedProperties.has('required') || changedProperties.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) return; if (this._isOpen) { this._close(); } else { this._isOpen = true; const options = Array.from(this.querySelectorAll('kr-select-option')); this._highlightedIndex = options.findIndex(o => o.value === this.value); } } _close() { this._isOpen = false; this._highlightedIndex = -1; } _selectOption(option) { if (option.disabled) return; this.value = option.value; this._internals.setFormValue(this.value); this._updateValidity(); this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); this._close(); this._triggerElement?.focus(); } _handleBlur() { this._touched = true; this._updateValidity(); } _updateValidity() { if (this.required && !this.value) { this._internals.setValidity({ valueMissing: true }, 'Please select an option', this._triggerElement); } else { this._internals.setValidity({}); } } render() { const options = Array.from(this.querySelectorAll('kr-select-option')); const selectedLabel = options.find(o => o.value === this.value)?.label; return b ` <div class=\"wrapper\"> ${this.label ? b ` <label> ${this.label} ${this.required ? b `<span class=\"required\" aria-hidden=\"true\">*</span>` : ''} </label> ` : A} <div class=\"select-wrapper\"> <button class=${e$1({ 'select-trigger': true, '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=${e$1({ 'select-value': true, 'select-placeholder': !selectedLabel })}> ${selectedLabel || this.placeholder} </span> <svg class=${e$1({ 'chevron-icon': true, 'select-icon': true, 'select-icon--open': this._isOpen })} viewBox=\"0 0 20 20\" fill=\"currentColor\" > <path fill-rule=\"evenodd\" d=\"M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z\" clip-rule=\"evenodd\" /> </svg> </button> <div class=${e$1({ 'select-dropdown': true, 'hidden': !this._isOpen })} role=\"listbox\"> <div class=\"select-options\"> ${options.length === 0 ? b `<div class=\"select-empty\">No options available</div>` : options.map((option, idx) => { const isSelected = option.value === this.value; return b ` <div class=${e$1({ 'select-option': true, 'select-option--selected': isSelected, 'select-option--disabled': option.disabled, 'select-option--highlighted': idx === this._highlightedIndex, })} role=\"option\" aria-selected=${isSelected} @click=${() => this._selectOption(option)} @mouseenter=${() => (this._highlightedIndex = idx)} > ${option.label} ${isSelected ? b `<svg class=\"chevron-icon select-check\" viewBox=\"0 0 20 20\" fill=\"currentColor\"> <path fill-rule=\"evenodd\" d=\"M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z\" clip-rule=\"evenodd\" /> </svg>` : A} </div> `; })} </div> </div> </div> ${this._touched && this.required && !this.value ? b `<div class=\"validation-message\">Please select an option</div>` : this.hint ? b `<div class=\"hint\">${this.hint}</div>` : ''} </div> <div class=\"options-slot\"> <slot @slotchange=${() => this.requestUpdate()}></slot> </div> `; } // Public methods for programmatic control focus() { this._triggerElement?.focus(); } blur() { this._triggerElement?.blur(); } }",
270
+ "default": "class KRSelect extends i$2 { constructor() { super(); /** * The select label text */ this.label = ''; /** * The input name for form submission */ this.name = ''; /** * The currently selected value */ this.value = ''; /** * Placeholder text when no option is selected */ this.placeholder = 'Select an option'; /** * Whether the select is disabled */ this.disabled = false; /** * Whether the field is required */ this.required = false; /** * Whether the field is readonly */ this.readonly = false; /** * Helper text shown below the select */ this.hint = ''; this._isOpen = false; this._highlightedIndex = -1; this._touched = false; this._handleInvalid = (e) => { e.preventDefault(); this._touched = true; }; this._handleOutsideClick = (e) => { if (!e.composedPath().includes(this)) { this._close(); } }; this._handleKeyDown = (e) => { if (!this._isOpen) return; const options = Array.from(this.querySelectorAll('kr-select-option')); switch (e.key) { case 'Escape': this._close(); this._triggerElement?.focus(); break; case 'ArrowDown': e.preventDefault(); if (options.some(o => !o.disabled)) { let newIndex = this._highlightedIndex + 1; while (newIndex < options.length && options[newIndex]?.disabled) newIndex++; if (newIndex < options.length) this._highlightedIndex = newIndex; } break; case 'ArrowUp': e.preventDefault(); { let newIndex = this._highlightedIndex - 1; while (newIndex >= 0 && options[newIndex]?.disabled) newIndex--; if (newIndex >= 0) this._highlightedIndex = newIndex; } break; case 'Enter': e.preventDefault(); if (this._highlightedIndex >= 0 && this._highlightedIndex < options.length) { this._selectOption(options[this._highlightedIndex]); } break; } }; this._internals = this.attachInternals(); } // Form-associated custom element callbacks 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 = false; this._internals.setFormValue(''); this._internals.setValidity({}); } formStateRestoreCallback(state) { this.value = state; } connectedCallback() { super.connectedCallback(); document.addEventListener('click', this._handleOutsideClick); document.addEventListener('keydown', this._handleKeyDown); this.addEventListener('invalid', this._handleInvalid); } firstUpdated() { this._updateValidity(); } updated(changedProperties) { if (changedProperties.has('required') || changedProperties.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) return; if (this._isOpen) { this._close(); } else { this._isOpen = true; const options = Array.from(this.querySelectorAll('kr-select-option')); this._highlightedIndex = options.findIndex(o => o.value === this.value); } } _close() { this._isOpen = false; this._highlightedIndex = -1; } _selectOption(option) { if (option.disabled) return; this.value = option.value; this._internals.setFormValue(this.value); this._updateValidity(); this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); this._close(); this._triggerElement?.focus(); } _handleBlur() { this._touched = true; this._updateValidity(); } _updateValidity() { if (this.required && !this.value) { this._internals.setValidity({ valueMissing: true }, 'Please select an option', this._triggerElement); } else { this._internals.setValidity({}); } } render() { const options = Array.from(this.querySelectorAll('kr-select-option')); const selectedLabel = options.find(o => o.value === this.value)?.label; return b ` <div class=\"wrapper\"> ${this.label ? b ` <label> ${this.label} ${this.required ? b `<span class=\"required\" aria-hidden=\"true\">*</span>` : ''} </label> ` : A} <div class=\"select-wrapper\"> <button class=${e$1({ 'select-trigger': true, '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=${e$1({ 'select-value': true, 'select-placeholder': !selectedLabel })}> ${selectedLabel || this.placeholder} </span> <svg class=${e$1({ 'chevron-icon': true, 'select-icon': true, 'select-icon--open': this._isOpen })} viewBox=\"0 0 20 20\" fill=\"currentColor\" > <path fill-rule=\"evenodd\" d=\"M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z\" clip-rule=\"evenodd\" /> </svg> </button> <div class=${e$1({ 'select-dropdown': true, 'hidden': !this._isOpen })} role=\"listbox\"> <div class=\"select-options\"> ${options.length === 0 ? b `<div class=\"select-empty\">No options available</div>` : options.map((option, idx) => { const isSelected = option.value === this.value; return b ` <div class=${e$1({ 'select-option': true, 'select-option--selected': isSelected, 'select-option--disabled': option.disabled, 'select-option--highlighted': idx === this._highlightedIndex, })} role=\"option\" aria-selected=${isSelected} @click=${() => this._selectOption(option)} @mouseenter=${() => (this._highlightedIndex = idx)} > ${option.label} ${isSelected ? b `<svg class=\"chevron-icon select-check\" viewBox=\"0 0 20 20\" fill=\"currentColor\"> <path fill-rule=\"evenodd\" d=\"M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z\" clip-rule=\"evenodd\" /> </svg>` : A} </div> `; })} </div> </div> </div> ${this._touched && this.required && !this.value ? b `<div class=\"validation-message\">Please select an option</div>` : this.hint ? b `<div class=\"hint\">${this.hint}</div>` : ''} </div> <div class=\"options-slot\"> <slot @slotchange=${() => this.requestUpdate()}></slot> </div> `; } // Public methods for programmatic control focus() { this._triggerElement?.focus(); } blur() { this._triggerElement?.blur(); } }",
271
271
  "description": "A select dropdown component that works with native browser forms.\n\nUses ElementInternals for form association, allowing the component\nto participate in form submission, validation, and reset."
272
272
  },
273
273
  {
@@ -462,7 +462,7 @@
462
462
  {
463
463
  "kind": "class",
464
464
  "description": "",
465
- "name": "Me",
465
+ "name": "De",
466
466
  "members": [
467
467
  {
468
468
  "kind": "method",
@@ -511,8 +511,8 @@
511
511
  },
512
512
  {
513
513
  "kind": "variable",
514
- "name": "De",
515
- "default": "class extends ne{constructor(){super(...arguments),this.contentElement=null,this.dialogRef=null,this.boundHandleKeyDown=this.handleKeyDown.bind(this)}static open(e,t){const i=document.querySelector(\"kr-dialog\");i&&i.remove();const o=new Me,s=document.createElement(\"kr-dialog\");o.setDialogElement(s),s.dialogRef=o;const r=new e;return r.dialogRef=o,t?.data&&(r.data=t.data),s.contentElement=r,document.body.appendChild(s),document.addEventListener(\"keydown\",s.boundHandleKeyDown),o}handleKeyDown(e){\"Escape\"===e.key&&this.dialogRef?.close(void 0)}handleBackdropClick(e){e.target.classList.contains(\"backdrop\")&&this.dialogRef?.close(void 0)}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener(\"keydown\",this.boundHandleKeyDown)}render(){return U` <div class=\"backdrop\" @click=${this.handleBackdropClick}></div> <div class=\"dialog\"> ${this.contentElement} </div> `}}"
514
+ "name": "je",
515
+ "default": "class extends ne{constructor(){super(...arguments),this.contentElement=null,this.dialogRef=null,this.boundHandleKeyDown=this.handleKeyDown.bind(this)}static open(e,t){const i=document.querySelector(\"kr-dialog\");i&&i.remove();const o=new De,s=document.createElement(\"kr-dialog\");o.setDialogElement(s),s.dialogRef=o;const r=new e;return r.dialogRef=o,t?.data&&(r.data=t.data),s.contentElement=r,document.body.appendChild(s),document.addEventListener(\"keydown\",s.boundHandleKeyDown),o}handleKeyDown(e){\"Escape\"===e.key&&this.dialogRef?.close(void 0)}handleBackdropClick(e){e.target.classList.contains(\"backdrop\")&&this.dialogRef?.close(void 0)}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener(\"keydown\",this.boundHandleKeyDown)}render(){return U` <div class=\"backdrop\" @click=${this.handleBackdropClick}></div> <div class=\"dialog\"> ${this.contentElement} </div> `}}"
516
516
  },
517
517
  {
518
518
  "kind": "variable",
@@ -531,7 +531,7 @@
531
531
  {
532
532
  "kind": "variable",
533
533
  "name": "Ye",
534
- "default": "class extends ne{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._displayedColumns=[],this._widthsLocked=!1,this._resizing=null,this._resizeObserver=null,this._searchPositionLocked=!1,this._def={columns:[]},this.def={columns:[]},this._handleClickOutsideColumnPicker=e=>{if(!this._columnPickerOpen)return;const t=e.composedPath(),i=this.shadowRoot?.querySelector(\".column-picker-wrapper\");i&&!t.includes(i)&&(this._columnPickerOpen=!1)},this._handleResizeMove=e=>{if(!this._resizing)return;const t=this._def.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._handleClickOutsideColumnPicker),this._resizeObserver=new ResizeObserver(()=>{this._searchPositionLocked=!1,this._updateSearchPosition()}),this._resizeObserver.observe(this)}disconnectedCallback(){super.disconnectedCallback(),clearInterval(this._refreshTimer),document.removeEventListener(\"click\",this._handleClickOutsideColumnPicker),this._resizeObserver?.disconnect()}willUpdate(e){e.has(\"def\")&&(this._def={...this.def,columns:this.def.columns.map(e=>\"actions\"===e.type?{...e,sticky:\"right\",resizable:!1}:{...e})},this._displayedColumns=this._def.displayedColumns||this._def.columns.map(e=>e.id),this._widthsLocked=!1,this.classList.remove(\"kr-table--widths-locked\"),this._fetch(),this._initRefresh())}updated(e){this._updateScrollFlags()}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())}_fetch(){if(!this._def.dataSource)return;let e;switch(this._dataState=\"loading\",this._def.dataSource.mode){case\"opensearch\":throw Error(\"Opensearch not supported yet\");case\"db\":throw Error(\"DB not supported yet\");default:e={page:this._page-1,size:this._pageSize,sorts:[],filterFields:[],queryFields:[],facetFields:[]},this._searchQuery?.trim().length&&e.queryFields.push({name:\"_text_\",operation:\"IS\",value:Qe(this._searchQuery)})}this._def.dataSource.fetch(e).then(e=>{switch(this._def.dataSource?.mode){case\"opensearch\":throw Error(\"Opensearch not supported yet\");case\"db\":throw Error(\"DB not supported yet\");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._dataState=\"success\",this._updateSearchPosition(),this._widthsLocked||this._lockColumnWidths()}).catch(e=>{this._dataState=\"error\",Ie.show({message:e instanceof Error?e.message:\"Failed to load data\",type:\"error\"})})}_initRefresh(){clearInterval(this._refreshTimer),this._def.refreshInterval&&this._def.refreshInterval>0&&(this._refreshTimer=window.setInterval(()=>{this._fetch()},this._def.refreshInterval))}_handleSearch(e){const t=e.target;this._searchQuery=t.value,this._page=1,this._fetch()}_measureTextWidth(e,t){const i=document.createElement(\"canvas\").getContext(\"2d\");return i.font=t,i.measureText(e).width}_lockColumnWidths(){this.updateComplete.then(()=>{requestAnimationFrame(()=>{const e=this.shadowRoot?.querySelector(\".header-cell\"),t=this.shadowRoot?.querySelector(\".cell\");if(!e)return;const i=getComputedStyle(e),o=`${i.fontWeight} ${i.fontSize} ${i.fontFamily}`,s=parseFloat(i.paddingLeft)+parseFloat(i.paddingRight),r=t?getComputedStyle(t):i,n=`${r.fontWeight} ${r.fontSize} ${r.fontFamily}`,l=t?parseFloat(r.paddingLeft)+parseFloat(r.paddingRight):s;this.getDisplayedColumns().forEach(e=>{if(!e.width){let t;if(e.render){const i=this.shadowRoot?.querySelector(`.cell[data-column-id=\"${e.id}\"]`);t=i?i.scrollWidth:150}else{const i=e.label??e.id,r=this._measureTextWidth(i,o)+s;let a=0;for(const t of this._data){const i=t[e.id];if(null!=i){const e=this._measureTextWidth(String(i),n)+l;e>a&&(a=e)}}t=Math.ceil(Math.max(r,a))}e.width=`${Math.max(t,150)}px`}}),this._widthsLocked=!0,this.classList.add(\"kr-table--widths-locked\"),this.requestUpdate()})})}_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(),o=t.getBoundingClientRect().left-i.left;e.style.justifyContent=\"flex-start\",t.style.marginLeft=`${o}px`,this._searchPositionLocked=!0}))}_toggleColumnPicker(){this._columnPickerOpen=!this._columnPickerOpen}_toggleColumn(e){this._displayedColumns.includes(e)?this._displayedColumns=this._displayedColumns.filter(t=>t!==e):this._displayedColumns=[...this._displayedColumns,e]}getDisplayedColumns(){return this._displayedColumns.map(e=>this._def.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){this.dispatchEvent(new CustomEvent(\"action\",{detail:{action:e.id},bubbles:!0,composed:!0}))}_renderCellContent(e,t){const i=t[e.id];if(e.render){const i=e.render(t);return\"string\"==typeof i?Oe(i):i}if(null==i)return\"\";switch(e.type){case\"number\":return\"number\"==typeof i?i.toLocaleString():String(i);case\"currency\":return\"number\"==typeof i?i.toLocaleString(\"en-US\",{style:\"currency\",currency:\"USD\"}):String(i);case\"date\":return i instanceof Date?i.toLocaleDateString():new Date(i).toLocaleDateString();case\"boolean\":return!0===i?\"Yes\":!1===i?\"No\":\"\";default:return String(i)}}_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--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 U` <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._def.title&&!this._def.actions?.length&&this._totalPages<=1?V:U` <div class=\"header\"> <div class=\"title\">${this._def.title??\"\"}</div> <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._def.columns].filter(e=>\"actions\"!==e.type).sort((e,t)=>(e.label??e.id).localeCompare(t.label??t.id)).map(e=>U` <div class=\"column-picker-item\" @click=${()=>this._toggleColumn(e.id)}> <div class=\"column-picker-checkbox ${this._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._def.actions?.length?U` <kr-button class=\"actions\" @click=${()=>this._handleAction(this._def.actions[0])} > ${this._def.actions[0].label} </kr-button> `:this._def.actions?.length?U` <kr-button class=\"actions\" .options=${this._def.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> `:V} </div> </div> `}_renderStatus(){return\"loading\"===this._dataState&&0===this._data.length?U`<div class=\"status\">Loading...</div>`:\"error\"===this._dataState&&0===this._data.length?U`<div class=\"status status--error\">Error loading data</div>`:0===this._data.length?U`<div class=\"status\">No data available</div>`:V}_getGridTemplateColumns(){const e=this.getDisplayedColumns(),t=e.map((e,t)=>e.sticky?-1:t).filter(e=>e>=0).pop();return e.map((e,i)=>i===t&&this._widthsLocked?`minmax(${e.width||\"auto\"}, 1fr)`:e.width||\"auto\").join(\" \")}_renderTable(){return U` <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)=>U` <div class=${we(this._getHeaderCellClasses(e,t))} style=${Ue(this._getCellStyle(e,t))} data-column-id=${e.id} >${e.label??e.id}${!1!==e.resizable?U`<div class=\"header-cell__resize\" @mousedown=${t=>this._handleResizeStart(t,e.id)} ></div>`:V}</div> `)} </div> ${this._data.map(e=>U` <div class=\"row\"> ${this.getDisplayedColumns().map((t,i)=>U` <div class=${we(this._getCellClasses(t,i))} style=${Ue(this._getCellStyle(t,i))} data-column-id=${t.id} > ${this._renderCellContent(t,e)} </div> `)} </div> `)} </div> </div> </div> `}render(){return this._def.columns.length?U` ${this._renderHeader()} ${this._renderTable()} `:U`<slot></slot>`}}"
534
+ "default": "class extends ne{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._displayedColumns=[],this._widthsLocked=!1,this._resizing=null,this._resizeObserver=null,this._searchPositionLocked=!1,this._def={columns:[]},this.def={columns:[]},this._handleClickOutsideColumnPicker=e=>{if(!this._columnPickerOpen)return;const t=e.composedPath(),i=this.shadowRoot?.querySelector(\".column-picker-wrapper\");i&&!t.includes(i)&&(this._columnPickerOpen=!1)},this._handleResizeMove=e=>{if(!this._resizing)return;const t=this._def.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._handleClickOutsideColumnPicker),this._resizeObserver=new ResizeObserver(()=>{this._searchPositionLocked=!1,this._updateSearchPosition()}),this._resizeObserver.observe(this)}disconnectedCallback(){super.disconnectedCallback(),clearInterval(this._refreshTimer),document.removeEventListener(\"click\",this._handleClickOutsideColumnPicker),this._resizeObserver?.disconnect()}willUpdate(e){e.has(\"def\")&&(this._def={...this.def,columns:this.def.columns.map(e=>\"actions\"===e.type?{...e,sticky:\"right\",resizable:!1}:{...e})},this._displayedColumns=this._def.displayedColumns||this._def.columns.map(e=>e.id),this._widthsLocked=!1,this.classList.remove(\"kr-table--widths-locked\"),this._fetch(),this._initRefresh())}updated(e){this._updateScrollFlags()}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())}_fetch(){if(!this._def.dataSource)return;let e;switch(this._dataState=\"loading\",this._def.dataSource.mode){case\"opensearch\":throw Error(\"Opensearch not supported yet\");case\"db\":throw Error(\"DB not supported yet\");default:e={page:this._page-1,size:this._pageSize,sorts:[],filterFields:[],queryFields:[],facetFields:[]},this._searchQuery?.trim().length&&e.queryFields.push({name:\"_text_\",operation:\"IS\",value:Qe(this._searchQuery)})}this._def.dataSource.fetch(e).then(e=>{switch(this._def.dataSource?.mode){case\"opensearch\":throw Error(\"Opensearch not supported yet\");case\"db\":throw Error(\"DB not supported yet\");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._dataState=\"success\",this._updateSearchPosition(),this._widthsLocked||this._lockColumnWidths()}).catch(e=>{this._dataState=\"error\",Ie.show({message:e instanceof Error?e.message:\"Failed to load data\",type:\"error\"})})}_initRefresh(){clearInterval(this._refreshTimer),this._def.refreshInterval&&this._def.refreshInterval>0&&(this._refreshTimer=window.setInterval(()=>{this._fetch()},this._def.refreshInterval))}_handleSearch(e){const t=e.target;this._searchQuery=t.value,this._page=1,this._fetch()}_lockColumnWidths(){this.getDisplayedColumns().forEach(e=>{e.width||(e.width=\"180px\")}),this._widthsLocked=!0,this.classList.add(\"kr-table--widths-locked\"),this.requestUpdate()}_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(),o=t.getBoundingClientRect().left-i.left;e.style.justifyContent=\"flex-start\",t.style.marginLeft=`${o}px`,this._searchPositionLocked=!0}))}_toggleColumnPicker(){this._columnPickerOpen=!this._columnPickerOpen}_toggleColumn(e){this._displayedColumns.includes(e)?this._displayedColumns=this._displayedColumns.filter(t=>t!==e):this._displayedColumns=[...this._displayedColumns,e]}getDisplayedColumns(){return this._displayedColumns.map(e=>this._def.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){this.dispatchEvent(new CustomEvent(\"action\",{detail:{action:e.id},bubbles:!0,composed:!0}))}_renderCellContent(e,t){const i=t[e.id];if(e.render){const i=e.render(t);return\"string\"==typeof i?Oe(i):i}if(null==i)return\"\";switch(e.type){case\"number\":return\"number\"==typeof i?i.toLocaleString():String(i);case\"currency\":return\"number\"==typeof i?i.toLocaleString(\"en-US\",{style:\"currency\",currency:\"USD\"}):String(i);case\"date\":return i instanceof Date?i.toLocaleDateString():new Date(i).toLocaleDateString();case\"boolean\":return!0===i?\"Yes\":!1===i?\"No\":\"\";default:return String(i)}}_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 U` <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._def.title&&!this._def.actions?.length&&this._totalPages<=1?V:U` <div class=\"header\"> <div class=\"title\">${this._def.title??\"\"}</div> <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._def.columns].filter(e=>\"actions\"!==e.type).sort((e,t)=>(e.label??e.id).localeCompare(t.label??t.id)).map(e=>U` <div class=\"column-picker-item\" @click=${()=>this._toggleColumn(e.id)}> <div class=\"column-picker-checkbox ${this._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._def.actions?.length?U` <kr-button class=\"actions\" @click=${()=>this._handleAction(this._def.actions[0])} > ${this._def.actions[0].label} </kr-button> `:this._def.actions?.length?U` <kr-button class=\"actions\" .options=${this._def.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> `:V} </div> </div> `}_renderStatus(){return\"loading\"===this._dataState&&0===this._data.length?U`<div class=\"status\">Loading...</div>`:\"error\"===this._dataState&&0===this._data.length?U`<div class=\"status status--error\">Error loading data</div>`:0===this._data.length?U`<div class=\"status\">No data available</div>`:V}_getGridTemplateColumns(){const e=this.getDisplayedColumns(),t=e.map((e,t)=>e.sticky?-1:t).filter(e=>e>=0).pop();return e.map((e,i)=>i===t&&this._widthsLocked?`minmax(${e.width||\"auto\"}, 1fr)`:e.width||\"auto\").join(\" \")}_renderTable(){return U` <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)=>U` <div class=${we(this._getHeaderCellClasses(e,t))} style=${Ue(this._getCellStyle(e,t))} data-column-id=${e.id} >${e.label??e.id}${!1!==e.resizable?U`<div class=\"header-cell__resize\" @mousedown=${t=>this._handleResizeStart(t,e.id)} ></div>`:V}</div> `)} </div> ${this._data.map(e=>U` <div class=\"row\"> ${this.getDisplayedColumns().map((t,i)=>U` <div class=${we(this._getCellClasses(t,i))} style=${Ue(this._getCellStyle(t,i))} data-column-id=${t.id} > ${this._renderCellContent(t,e)} </div> `)} </div> `)} </div> </div> </div> `}render(){return this._def.columns.length?U` ${this._renderHeader()} ${this._renderTable()} `:U`<slot></slot>`}}"
535
535
  },
536
536
  {
537
537
  "kind": "variable",
@@ -541,7 +541,7 @@
541
541
  {
542
542
  "kind": "variable",
543
543
  "name": "st",
544
- "default": "class extends ne{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=>{this.contains(e.target)||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)}}_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 U` <div class=\"wrapper\"> ${this.label?U` <label> ${this.label} ${this.required?U`<span class=\"required\" aria-hidden=\"true\">*</span>`:\"\"} </label> `:V} <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 20 20\" fill=\"currentColor\" > <path fill-rule=\"evenodd\" d=\"M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z\" clip-rule=\"evenodd\" /> </svg> </button> <div class=${we({\"select-dropdown\":!0,hidden:!this._isOpen})} role=\"listbox\"> <div class=\"select-options\"> ${0===e.length?U`<div class=\"select-empty\">No options available</div>`:e.map((e,t)=>{const i=e.value===this.value;return U` <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} ${i?U`<svg class=\"chevron-icon select-check\" viewBox=\"0 0 20 20\" fill=\"currentColor\"> <path fill-rule=\"evenodd\" d=\"M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z\" clip-rule=\"evenodd\" /> </svg>`:V} </div> `})} </div> </div> </div> ${this._touched&&this.required&&!this.value?U`<div class=\"validation-message\">Please select an option</div>`:this.hint?U`<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()}}"
544
+ "default": "class extends ne{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)}}_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 U` <div class=\"wrapper\"> ${this.label?U` <label> ${this.label} ${this.required?U`<span class=\"required\" aria-hidden=\"true\">*</span>`:\"\"} </label> `:V} <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 20 20\" fill=\"currentColor\" > <path fill-rule=\"evenodd\" d=\"M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z\" clip-rule=\"evenodd\" /> </svg> </button> <div class=${we({\"select-dropdown\":!0,hidden:!this._isOpen})} role=\"listbox\"> <div class=\"select-options\"> ${0===e.length?U`<div class=\"select-empty\">No options available</div>`:e.map((e,t)=>{const i=e.value===this.value;return U` <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} ${i?U`<svg class=\"chevron-icon select-check\" viewBox=\"0 0 20 20\" fill=\"currentColor\"> <path fill-rule=\"evenodd\" d=\"M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z\" clip-rule=\"evenodd\" /> </svg>`:V} </div> `})} </div> </div> </div> ${this._touched&&this.required&&!this.value?U`<div class=\"validation-message\">Please select an option</div>`:this.hint?U`<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()}}"
545
545
  },
546
546
  {
547
547
  "kind": "variable",
@@ -570,7 +570,7 @@
570
570
  "kind": "js",
571
571
  "name": "DialogRef",
572
572
  "declaration": {
573
- "name": "Me",
573
+ "name": "De",
574
574
  "module": "dist/krubble.bundled.min.js"
575
575
  }
576
576
  },
@@ -618,7 +618,7 @@
618
618
  "kind": "js",
619
619
  "name": "KRDialog",
620
620
  "declaration": {
621
- "name": "De",
621
+ "name": "je",
622
622
  "module": "dist/krubble.bundled.min.js"
623
623
  }
624
624
  },
@@ -988,44 +988,44 @@
988
988
  },
989
989
  {
990
990
  "kind": "javascript-module",
991
- "path": "dist/button/button.js",
991
+ "path": "dist/alert/alert.js",
992
992
  "declarations": [
993
993
  {
994
994
  "kind": "variable",
995
- "name": "KRButton",
996
- "default": "class KRButton extends LitElement { constructor() { super(...arguments); /** * The button variant (shape) */ this.variant = 'flat'; /** * The button color */ this.color = 'primary'; /** * The button size */ this.size = 'medium'; /** * Whether the button is disabled */ this.disabled = false; /** * Dropdown options - when provided, button becomes a dropdown */ this.options = []; this._state = 'idle'; this._stateText = ''; this._dropdownOpened = false; this._dropdownAlignRight = false; this._handleHostClick = (e) => { if (this.options.length) { e.stopPropagation(); this._toggleDropdown(); } }; this._handleKeydown = (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (this.options.length) { this._toggleDropdown(); } else { this.click(); } } if (e.key === 'Escape' && this._dropdownOpened) { this._dropdownOpened = false; } }; this._handleClickOutside = (e) => { if (this._dropdownOpened && !this.contains(e.target)) { this._dropdownOpened = false; } }; } connectedCallback() { super.connectedCallback(); this.setAttribute('role', '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; if (this._dropdownOpened) { // Check if dropdown would overflow viewport after render requestAnimationFrame(() => { const dropdown = this.shadowRoot?.querySelector('.dropdown'); if (dropdown) { const rect = dropdown.getBoundingClientRect(); this._dropdownAlignRight = rect.right > window.innerWidth; } }); } } _handleOptionClick(option, e) { e.stopPropagation(); this._dropdownOpened = false; this.dispatchEvent(new CustomEvent('option-select', { detail: { id: option.id, label: option.label }, bubbles: true, composed: true })); } /** * Shows a loading spinner and disables the button. */ showLoading() { this._clearStateTimeout(); this._state = 'loading'; this._stateText = ''; } /** * Shows a success state with optional custom text. * @param text - Text to display (default: \"Saved\") * @param duration - Duration in ms before auto-reset (default: 2000) */ showSuccess(text = 'Success', duration = 2000) { this._clearStateTimeout(); this._state = 'success'; this._stateText = text; this._stateTimeout = window.setTimeout(() => this.reset(), duration); } /** * Shows an error state with optional custom text. * @param text - Text to display (default: \"Error\") * @param duration - Duration in ms before auto-reset (default: 2000) */ showError(text = 'Error', duration = 2000) { this._clearStateTimeout(); this._state = 'error'; this._stateText = text; this._stateTimeout = window.setTimeout(() => this.reset(), duration); } /** * Resets the button to its idle state. */ reset() { this._clearStateTimeout(); this._state = 'idle'; this._stateText = ''; } _clearStateTimeout() { if (this._stateTimeout) { clearTimeout(this._stateTimeout); this._stateTimeout = undefined; } } updated(changedProperties) { // Reflect state classes to host this.classList.toggle('kr-button--loading', this._state === 'loading'); this.classList.toggle('kr-button--success', this._state === 'success'); this.classList.toggle('kr-button--error', this._state === 'error'); this.classList.toggle(`kr-button--${this.variant}`, true); this.classList.toggle(`kr-button--${this.color}`, true); this.classList.toggle('kr-button--small', this.size === 'small'); this.classList.toggle('kr-button--large', this.size === 'large'); } render() { return html ` <slot></slot> ${this.options.length ? html `<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>` : nothing} ${this._state !== 'idle' ? html `<span class=\"state-overlay\"> ${this._state === 'loading' ? html `<span class=\"spinner\"></span>` : this._stateText} </span>` : nothing} ${this.options.length ? html ` <div class=\"dropdown ${this._dropdownOpened ? 'dropdown--opened' : ''} ${this._dropdownAlignRight ? 'dropdown--align-right' : ''}\"> ${this.options.map(option => html ` <button class=\"dropdown-item\" @click=${(e) => this._handleOptionClick(option, e)} >${option.label}</button> `)} </div> ` : nothing} `; } }",
997
- "description": "A customizable button component."
995
+ "name": "KRAlert",
996
+ "default": "class KRAlert extends LitElement { constructor() { super(...arguments); /** * The alert type/severity */ this.type = 'info'; /** * Whether the alert can be dismissed */ this.dismissible = false; /** * Whether the alert is visible */ this.visible = true; } /** Handles dismiss button click */ _handleDismiss() { this.visible = false; this.dispatchEvent(new CustomEvent('dismiss', { bubbles: true, composed: true })); } render() { const icons = { info: html `<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: html `<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: html `<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: html `<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 html ` <div class=${classMap({ 'alert': true, ['alert--' + this.type]: true, 'alert--hidden': !this.visible })} role=\"alert\" > ${icons[this.type]} <div class=\"content\"> ${this.header ? html `<h4 class=\"header\">${this.header}</h4>` : nothing} <div class=\"message\"> <slot></slot> </div> </div> ${this.dismissible ? html ` <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> ` : nothing} </div> `; } }",
997
+ "description": "A customizable alert component for displaying important information to users."
998
998
  }
999
999
  ],
1000
1000
  "exports": [
1001
1001
  {
1002
1002
  "kind": "js",
1003
- "name": "KRButton",
1003
+ "name": "KRAlert",
1004
1004
  "declaration": {
1005
- "name": "KRButton",
1006
- "module": "dist/button/button.js"
1005
+ "name": "KRAlert",
1006
+ "module": "dist/alert/alert.js"
1007
1007
  }
1008
1008
  }
1009
1009
  ]
1010
1010
  },
1011
1011
  {
1012
1012
  "kind": "javascript-module",
1013
- "path": "dist/alert/alert.js",
1013
+ "path": "dist/button/button.js",
1014
1014
  "declarations": [
1015
1015
  {
1016
1016
  "kind": "variable",
1017
- "name": "KRAlert",
1018
- "default": "class KRAlert extends LitElement { constructor() { super(...arguments); /** * The alert type/severity */ this.type = 'info'; /** * Whether the alert can be dismissed */ this.dismissible = false; /** * Whether the alert is visible */ this.visible = true; } /** Handles dismiss button click */ _handleDismiss() { this.visible = false; this.dispatchEvent(new CustomEvent('dismiss', { bubbles: true, composed: true })); } render() { const icons = { info: html `<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: html `<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: html `<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: html `<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 html ` <div class=${classMap({ 'alert': true, ['alert--' + this.type]: true, 'alert--hidden': !this.visible })} role=\"alert\" > ${icons[this.type]} <div class=\"content\"> ${this.header ? html `<h4 class=\"header\">${this.header}</h4>` : nothing} <div class=\"message\"> <slot></slot> </div> </div> ${this.dismissible ? html ` <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> ` : nothing} </div> `; } }",
1019
- "description": "A customizable alert component for displaying important information to users."
1017
+ "name": "KRButton",
1018
+ "default": "class KRButton extends LitElement { constructor() { super(...arguments); /** * The button variant (shape) */ this.variant = 'flat'; /** * The button color */ this.color = 'primary'; /** * The button size */ this.size = 'medium'; /** * Whether the button is disabled */ this.disabled = false; /** * Dropdown options - when provided, button becomes a dropdown */ this.options = []; this._state = 'idle'; this._stateText = ''; this._dropdownOpened = false; this._dropdownAlignRight = false; this._handleHostClick = (e) => { if (this.options.length) { e.stopPropagation(); this._toggleDropdown(); } }; this._handleKeydown = (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (this.options.length) { this._toggleDropdown(); } else { this.click(); } } if (e.key === 'Escape' && this._dropdownOpened) { this._dropdownOpened = false; } }; this._handleClickOutside = (e) => { if (this._dropdownOpened && !this.contains(e.target)) { this._dropdownOpened = false; } }; } connectedCallback() { super.connectedCallback(); this.setAttribute('role', '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; if (this._dropdownOpened) { // Check if dropdown would overflow viewport after render requestAnimationFrame(() => { const dropdown = this.shadowRoot?.querySelector('.dropdown'); if (dropdown) { const rect = dropdown.getBoundingClientRect(); this._dropdownAlignRight = rect.right > window.innerWidth; } }); } } _handleOptionClick(option, e) { e.stopPropagation(); this._dropdownOpened = false; this.dispatchEvent(new CustomEvent('option-select', { detail: { id: option.id, label: option.label }, bubbles: true, composed: true })); } /** * Shows a loading spinner and disables the button. */ showLoading() { this._clearStateTimeout(); this._state = 'loading'; this._stateText = ''; } /** * Shows a success state with optional custom text. * @param text - Text to display (default: \"Saved\") * @param duration - Duration in ms before auto-reset (default: 2000) */ showSuccess(text = 'Success', duration = 2000) { this._clearStateTimeout(); this._state = 'success'; this._stateText = text; this._stateTimeout = window.setTimeout(() => this.reset(), duration); } /** * Shows an error state with optional custom text. * @param text - Text to display (default: \"Error\") * @param duration - Duration in ms before auto-reset (default: 2000) */ showError(text = 'Error', duration = 2000) { this._clearStateTimeout(); this._state = 'error'; this._stateText = text; this._stateTimeout = window.setTimeout(() => this.reset(), duration); } /** * Resets the button to its idle state. */ reset() { this._clearStateTimeout(); this._state = 'idle'; this._stateText = ''; } _clearStateTimeout() { if (this._stateTimeout) { clearTimeout(this._stateTimeout); this._stateTimeout = undefined; } } updated(changedProperties) { // Reflect state classes to host this.classList.toggle('kr-button--loading', this._state === 'loading'); this.classList.toggle('kr-button--success', this._state === 'success'); this.classList.toggle('kr-button--error', this._state === 'error'); this.classList.toggle(`kr-button--${this.variant}`, true); this.classList.toggle(`kr-button--${this.color}`, true); this.classList.toggle('kr-button--small', this.size === 'small'); this.classList.toggle('kr-button--large', this.size === 'large'); } render() { return html ` <slot></slot> ${this.options.length ? html `<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>` : nothing} ${this._state !== 'idle' ? html `<span class=\"state-overlay\"> ${this._state === 'loading' ? html `<span class=\"spinner\"></span>` : this._stateText} </span>` : nothing} ${this.options.length ? html ` <div class=\"dropdown ${this._dropdownOpened ? 'dropdown--opened' : ''} ${this._dropdownAlignRight ? 'dropdown--align-right' : ''}\"> ${this.options.map(option => html ` <button class=\"dropdown-item\" @click=${(e) => this._handleOptionClick(option, e)} >${option.label}</button> `)} </div> ` : nothing} `; } }",
1019
+ "description": "A customizable button component."
1020
1020
  }
1021
1021
  ],
1022
1022
  "exports": [
1023
1023
  {
1024
1024
  "kind": "js",
1025
- "name": "KRAlert",
1025
+ "name": "KRButton",
1026
1026
  "declaration": {
1027
- "name": "KRAlert",
1028
- "module": "dist/alert/alert.js"
1027
+ "name": "KRButton",
1028
+ "module": "dist/button/button.js"
1029
1029
  }
1030
1030
  }
1031
1031
  ]
@@ -1052,6 +1052,28 @@
1052
1052
  }
1053
1053
  ]
1054
1054
  },
1055
+ {
1056
+ "kind": "javascript-module",
1057
+ "path": "dist/context-menu/context-menu.js",
1058
+ "declarations": [
1059
+ {
1060
+ "kind": "variable",
1061
+ "name": "KRContextMenu",
1062
+ "default": "class KRContextMenu extends LitElement { constructor() { super(...arguments); this.items = []; this.resolvePromise = null; this.boundHandleOutsideClick = this.handleOutsideClick.bind(this); this.boundHandleKeyDown = this.handleKeyDown.bind(this); } static async open(options) { // Remove any existing context menu const existing = document.querySelector('kr-context-menu'); if (existing) { existing.remove(); } // Create and position the menu const menu = document.createElement('kr-context-menu'); document.body.appendChild(menu); return menu.show(options); } async show(options) { this.items = options.items; this.style.left = `${options.x}px`; this.style.top = `${options.y}px`; // Adjust position if menu would go off screen await this.updateComplete; const rect = this.getBoundingClientRect(); if (rect.right > window.innerWidth) { this.style.left = `${options.x - rect.width}px`; } if (rect.bottom > window.innerHeight) { this.style.top = `${options.y - rect.height}px`; } // Add event listeners after a microtask to avoid the current event triggering close requestAnimationFrame(() => { document.addEventListener('click', this.boundHandleOutsideClick); document.addEventListener('contextmenu', this.boundHandleOutsideClick); document.addEventListener('keydown', this.boundHandleKeyDown); }); return new Promise((resolve) => { this.resolvePromise = resolve; }); } handleOutsideClick(e) { if (!this.contains(e.target)) { this.close(null); } } handleKeyDown(e) { if (e.key === 'Escape') { this.close(null); } } handleItemClick(item) { if (!item.disabled && !item.divider) { this.close(item); } } close(result) { document.removeEventListener('click', this.boundHandleOutsideClick); document.removeEventListener('contextmenu', this.boundHandleOutsideClick); document.removeEventListener('keydown', this.boundHandleKeyDown); if (this.resolvePromise) { this.resolvePromise(result); this.resolvePromise = null; } this.remove(); } render() { return html ` <div class=\"menu\"> ${this.items.map(item => item.divider ? html `<div class=\"menu__divider\"></div>` : html ` <button class=\"menu__item\" ?disabled=${item.disabled} @click=${() => this.handleItemClick(item)} > ${item.icon ? html `<span class=\"menu__item-icon\">${item.icon}</span>` : null} ${item.label} </button> `)} </div> `; } }",
1063
+ "description": "Context menu component that can be opened programmatically.\n\nUsage:\n```ts\nconst result = await ContextMenu.open({\n x: event.clientX,\n y: event.clientY,\n items: [\n { id: 'edit', label: 'Edit Item' },\n { id: 'divider', label: '', divider: true },\n { id: 'add-above', label: 'Add Item Above' },\n { id: 'add-below', label: 'Add Item Below' },\n ]\n});\n\nif (result) {\n console.log('Selected:', result.id);\n}\n```"
1064
+ }
1065
+ ],
1066
+ "exports": [
1067
+ {
1068
+ "kind": "js",
1069
+ "name": "KRContextMenu",
1070
+ "declaration": {
1071
+ "name": "KRContextMenu",
1072
+ "module": "dist/context-menu/context-menu.js"
1073
+ }
1074
+ }
1075
+ ]
1076
+ },
1055
1077
  {
1056
1078
  "kind": "javascript-module",
1057
1079
  "path": "dist/dialog/dialog.js",
@@ -1123,28 +1145,6 @@
1123
1145
  }
1124
1146
  ]
1125
1147
  },
1126
- {
1127
- "kind": "javascript-module",
1128
- "path": "dist/context-menu/context-menu.js",
1129
- "declarations": [
1130
- {
1131
- "kind": "variable",
1132
- "name": "KRContextMenu",
1133
- "default": "class KRContextMenu extends LitElement { constructor() { super(...arguments); this.items = []; this.resolvePromise = null; this.boundHandleOutsideClick = this.handleOutsideClick.bind(this); this.boundHandleKeyDown = this.handleKeyDown.bind(this); } static async open(options) { // Remove any existing context menu const existing = document.querySelector('kr-context-menu'); if (existing) { existing.remove(); } // Create and position the menu const menu = document.createElement('kr-context-menu'); document.body.appendChild(menu); return menu.show(options); } async show(options) { this.items = options.items; this.style.left = `${options.x}px`; this.style.top = `${options.y}px`; // Adjust position if menu would go off screen await this.updateComplete; const rect = this.getBoundingClientRect(); if (rect.right > window.innerWidth) { this.style.left = `${options.x - rect.width}px`; } if (rect.bottom > window.innerHeight) { this.style.top = `${options.y - rect.height}px`; } // Add event listeners after a microtask to avoid the current event triggering close requestAnimationFrame(() => { document.addEventListener('click', this.boundHandleOutsideClick); document.addEventListener('contextmenu', this.boundHandleOutsideClick); document.addEventListener('keydown', this.boundHandleKeyDown); }); return new Promise((resolve) => { this.resolvePromise = resolve; }); } handleOutsideClick(e) { if (!this.contains(e.target)) { this.close(null); } } handleKeyDown(e) { if (e.key === 'Escape') { this.close(null); } } handleItemClick(item) { if (!item.disabled && !item.divider) { this.close(item); } } close(result) { document.removeEventListener('click', this.boundHandleOutsideClick); document.removeEventListener('contextmenu', this.boundHandleOutsideClick); document.removeEventListener('keydown', this.boundHandleKeyDown); if (this.resolvePromise) { this.resolvePromise(result); this.resolvePromise = null; } this.remove(); } render() { return html ` <div class=\"menu\"> ${this.items.map(item => item.divider ? html `<div class=\"menu__divider\"></div>` : html ` <button class=\"menu__item\" ?disabled=${item.disabled} @click=${() => this.handleItemClick(item)} > ${item.icon ? html `<span class=\"menu__item-icon\">${item.icon}</span>` : null} ${item.label} </button> `)} </div> `; } }",
1134
- "description": "Context menu component that can be opened programmatically.\n\nUsage:\n```ts\nconst result = await ContextMenu.open({\n x: event.clientX,\n y: event.clientY,\n items: [\n { id: 'edit', label: 'Edit Item' },\n { id: 'divider', label: '', divider: true },\n { id: 'add-above', label: 'Add Item Above' },\n { id: 'add-below', label: 'Add Item Below' },\n ]\n});\n\nif (result) {\n console.log('Selected:', result.id);\n}\n```"
1135
- }
1136
- ],
1137
- "exports": [
1138
- {
1139
- "kind": "js",
1140
- "name": "KRContextMenu",
1141
- "declaration": {
1142
- "name": "KRContextMenu",
1143
- "module": "dist/context-menu/context-menu.js"
1144
- }
1145
- }
1146
- ]
1147
- },
1148
1148
  {
1149
1149
  "kind": "javascript-module",
1150
1150
  "path": "dist/form/index.js",
@@ -1226,7 +1226,7 @@
1226
1226
  {
1227
1227
  "kind": "variable",
1228
1228
  "name": "KRTable",
1229
- "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._displayedColumns = []; this._widthsLocked = false; this._resizing = null; this._resizeObserver = null; this._searchPositionLocked = false; this._def = { columns: [] }; this.def = { columns: [] }; this._handleClickOutsideColumnPicker = (e) => { if (!this._columnPickerOpen) return; const path = e.composedPath(); const picker = this.shadowRoot?.querySelector('.column-picker-wrapper'); if (picker && !path.includes(picker)) { this._columnPickerOpen = false; } }; this._handleResizeMove = (e) => { if (!this._resizing) return; const col = this._def.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._handleClickOutsideColumnPicker); 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._handleClickOutsideColumnPicker); this._resizeObserver?.disconnect(); } willUpdate(changedProperties) { if (changedProperties.has('def')) { // Copy user's def and normalize action columns this._def = { ...this.def, columns: this.def.columns.map(col => { if (col.type === 'actions') { return { ...col, sticky: 'right', resizable: false }; } return { ...col }; }) }; this._displayedColumns = this._def.displayedColumns || this._def.columns.map(c => c.id); this._widthsLocked = false; this.classList.remove('kr-table--widths-locked'); this._fetch(); this._initRefresh(); } } updated(changedProperties) { this._updateScrollFlags(); } // ---------------------------------------------------------------------------- // 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 // ---------------------------------------------------------------------------- /** * 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._def.dataSource) return; this._dataState = 'loading'; // Build request based on mode let request; switch (this._def.dataSource.mode) { case 'opensearch': throw Error('Opensearch not supported yet'); case 'db': throw Error('DB not supported yet'); default: // solr request = { page: this._page - 1, size: this._pageSize, sorts: [], filterFields: [], queryFields: [], facetFields: [] }; if (this._searchQuery?.trim().length) { request.queryFields.push({ name: '_text_', operation: 'IS', value: escapeSolrQuery(this._searchQuery) }); } } this._def.dataSource.fetch(request) .then(response => { // Parse response based on mode switch (this._def.dataSource?.mode) { case 'opensearch': { throw Error('Opensearch not supported yet'); break; } case 'db': { throw Error('DB not supported yet'); 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._dataState = 'success'; this._updateSearchPosition(); if (!this._widthsLocked) this._lockColumnWidths(); }) .catch(err => { this._dataState = 'error'; KRSnackbar.show({ message: err instanceof Error ? err.message : 'Failed to load data', type: 'error' }); }); } /** * 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._def.refreshInterval && this._def.refreshInterval > 0) { this._refreshTimer = window.setInterval(() => { this._fetch(); }, this._def.refreshInterval); } } _handleSearch(e) { const input = e.target; this._searchQuery = input.value; this._page = 1; this._fetch(); } _measureTextWidth(text, font) { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); ctx.font = font; return ctx.measureText(text).width; } _lockColumnWidths() { this.updateComplete.then(() => { requestAnimationFrame(() => { const headerCell = this.shadowRoot?.querySelector('.header-cell'); const dataCell = this.shadowRoot?.querySelector('.cell'); if (!headerCell) return; const headerStyle = getComputedStyle(headerCell); const headerFont = `${headerStyle.fontWeight} ${headerStyle.fontSize} ${headerStyle.fontFamily}`; const headerPadding = parseFloat(headerStyle.paddingLeft) + parseFloat(headerStyle.paddingRight); const dataStyle = dataCell ? getComputedStyle(dataCell) : headerStyle; const dataFont = `${dataStyle.fontWeight} ${dataStyle.fontSize} ${dataStyle.fontFamily}`; const dataPadding = dataCell ? parseFloat(dataStyle.paddingLeft) + parseFloat(dataStyle.paddingRight) : headerPadding; this.getDisplayedColumns().forEach(col => { if (!col.width) { let width; // For columns with custom render functions, measure the actual DOM element if (col.render) { const cell = this.shadowRoot?.querySelector(`.cell[data-column-id=\"${col.id}\"]`); width = cell ? cell.scrollWidth : 150; } else { const headerText = col.label ?? col.id; const headerWidth = this._measureTextWidth(headerText, headerFont) + headerPadding; let maxDataWidth = 0; for (const row of this._data) { const value = row[col.id]; if (value != null) { const dataWidth = this._measureTextWidth(String(value), dataFont) + dataPadding; if (dataWidth > maxDataWidth) maxDataWidth = dataWidth; } } width = Math.ceil(Math.max(headerWidth, maxDataWidth)); } col.width = `${Math.max(width, 150)}px`; } }); this._widthsLocked = true; this.classList.add('kr-table--widths-locked'); this.requestUpdate(); }); }); } /** * 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._displayedColumns.includes(columnId)) { this._displayedColumns = this._displayedColumns.filter(id => id !== columnId); } else { this._displayedColumns = [...this._displayedColumns, columnId]; } } // 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._displayedColumns .map(id => this._def.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) { this.dispatchEvent(new CustomEvent('action', { detail: { action: action.id }, bubbles: true, composed: true })); } // ---------------------------------------------------------------------------- // Rendering // ---------------------------------------------------------------------------- _renderCellContent(column, row) { const value = row[column.id]; if (column.render) { const result = column.render(row); // If render returns a string, treat it as HTML return typeof result === 'string' ? unsafeHTML(result) : result; } if (value === null || value === undefined) { return ''; } switch (column.type) { case 'number': return typeof value === 'number' ? value.toLocaleString() : String(value); case 'currency': return typeof value === 'number' ? value.toLocaleString('en-US', { style: 'currency', currency: 'USD' }) : String(value); case 'date': return value instanceof Date ? value.toLocaleDateString() : new Date(value).toLocaleDateString(); 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--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._def.title && !this._def.actions?.length && this._totalPages <= 1) { return nothing; } return html ` <div class=\"header\"> <div class=\"title\">${this._def.title ?? ''}</div> <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._def.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._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._def.actions?.length === 1 ? html ` <kr-button class=\"actions\" @click=${() => this._handleAction(this._def.actions[0])} > ${this._def.actions[0].label} </kr-button> ` : this._def.actions?.length ? html ` <kr-button class=\"actions\" .options=${this._def.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; } _getGridTemplateColumns() { const cols = this.getDisplayedColumns(); const lastNonStickyIndex = cols.map((c, i) => c.sticky ? -1 : i).filter(i => i >= 0).pop(); return cols.map((col, i) => { if (i === lastNonStickyIndex && this._widthsLocked) { return `minmax(${col.width || 'auto'}, 1fr)`; } return col.width || 'auto'; }).join(' '); } /** 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._data.map(row => html ` <div class=\"row\"> ${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)} </div> `)} </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._def.columns.length) { return html `<slot></slot>`; } return html ` ${this._renderHeader()} ${this._renderTable()} `; } }"
1229
+ "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._displayedColumns = []; this._widthsLocked = false; this._resizing = null; this._resizeObserver = null; this._searchPositionLocked = false; this._def = { columns: [] }; this.def = { columns: [] }; this._handleClickOutsideColumnPicker = (e) => { if (!this._columnPickerOpen) return; const path = e.composedPath(); const picker = this.shadowRoot?.querySelector('.column-picker-wrapper'); if (picker && !path.includes(picker)) { this._columnPickerOpen = false; } }; this._handleResizeMove = (e) => { if (!this._resizing) return; const col = this._def.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._handleClickOutsideColumnPicker); 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._handleClickOutsideColumnPicker); this._resizeObserver?.disconnect(); } willUpdate(changedProperties) { if (changedProperties.has('def')) { // Copy user's def and normalize action columns this._def = { ...this.def, columns: this.def.columns.map(col => { if (col.type === 'actions') { return { ...col, sticky: 'right', resizable: false }; } return { ...col }; }) }; this._displayedColumns = this._def.displayedColumns || this._def.columns.map(c => c.id); this._widthsLocked = false; this.classList.remove('kr-table--widths-locked'); this._fetch(); this._initRefresh(); } } updated(changedProperties) { this._updateScrollFlags(); } // ---------------------------------------------------------------------------- // 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 // ---------------------------------------------------------------------------- /** * 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._def.dataSource) return; this._dataState = 'loading'; // Build request based on mode let request; switch (this._def.dataSource.mode) { case 'opensearch': throw Error('Opensearch not supported yet'); case 'db': throw Error('DB not supported yet'); default: // solr request = { page: this._page - 1, size: this._pageSize, sorts: [], filterFields: [], queryFields: [], facetFields: [] }; if (this._searchQuery?.trim().length) { request.queryFields.push({ name: '_text_', operation: 'IS', value: escapeSolrQuery(this._searchQuery) }); } } this._def.dataSource.fetch(request) .then(response => { // Parse response based on mode switch (this._def.dataSource?.mode) { case 'opensearch': { throw Error('Opensearch not supported yet'); break; } case 'db': { throw Error('DB not supported yet'); 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._dataState = 'success'; this._updateSearchPosition(); if (!this._widthsLocked) this._lockColumnWidths(); }) .catch(err => { this._dataState = 'error'; KRSnackbar.show({ message: err instanceof Error ? err.message : 'Failed to load data', type: 'error' }); }); } /** * 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._def.refreshInterval && this._def.refreshInterval > 0) { this._refreshTimer = window.setInterval(() => { this._fetch(); }, this._def.refreshInterval); } } _handleSearch(e) { const input = e.target; this._searchQuery = input.value; this._page = 1; this._fetch(); } _lockColumnWidths() { this.getDisplayedColumns().forEach(col => { if (!col.width) { col.width = '180px'; } }); this._widthsLocked = true; this.classList.add('kr-table--widths-locked'); this.requestUpdate(); } /** * 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._displayedColumns.includes(columnId)) { this._displayedColumns = this._displayedColumns.filter(id => id !== columnId); } else { this._displayedColumns = [...this._displayedColumns, columnId]; } } // 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._displayedColumns .map(id => this._def.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) { this.dispatchEvent(new CustomEvent('action', { detail: { action: action.id }, bubbles: true, composed: true })); } // ---------------------------------------------------------------------------- // Rendering // ---------------------------------------------------------------------------- _renderCellContent(column, row) { const value = row[column.id]; if (column.render) { const result = column.render(row); // If render returns a string, treat it as HTML return typeof result === 'string' ? unsafeHTML(result) : result; } if (value === null || value === undefined) { return ''; } switch (column.type) { case 'number': return typeof value === 'number' ? value.toLocaleString() : String(value); case 'currency': return typeof value === 'number' ? value.toLocaleString('en-US', { style: 'currency', currency: 'USD' }) : String(value); case 'date': return value instanceof Date ? value.toLocaleDateString() : new Date(value).toLocaleDateString(); 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._def.title && !this._def.actions?.length && this._totalPages <= 1) { return nothing; } return html ` <div class=\"header\"> <div class=\"title\">${this._def.title ?? ''}</div> <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._def.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._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._def.actions?.length === 1 ? html ` <kr-button class=\"actions\" @click=${() => this._handleAction(this._def.actions[0])} > ${this._def.actions[0].label} </kr-button> ` : this._def.actions?.length ? html ` <kr-button class=\"actions\" .options=${this._def.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; } _getGridTemplateColumns() { const cols = this.getDisplayedColumns(); const lastNonStickyIndex = cols.map((c, i) => c.sticky ? -1 : i).filter(i => i >= 0).pop(); return cols.map((col, i) => { if (i === lastNonStickyIndex && this._widthsLocked) { return `minmax(${col.width || 'auto'}, 1fr)`; } return col.width || 'auto'; }).join(' '); } /** 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._data.map(row => html ` <div class=\"row\"> ${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)} </div> `)} </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._def.columns.length) { return html `<slot></slot>`; } return html ` ${this._renderHeader()} ${this._renderTable()} `; } }"
1230
1230
  }
1231
1231
  ],
1232
1232
  "exports": [
@@ -1537,7 +1537,7 @@
1537
1537
  "kind": "field",
1538
1538
  "name": "color",
1539
1539
  "type": {
1540
- "text": "'primary' | 'secondary'"
1540
+ "text": "'primary' | 'secondary' | 'danger'"
1541
1541
  },
1542
1542
  "default": "'primary'",
1543
1543
  "description": "The button color",
@@ -1735,7 +1735,7 @@
1735
1735
  {
1736
1736
  "name": "color",
1737
1737
  "type": {
1738
- "text": "'primary' | 'secondary'"
1738
+ "text": "'primary' | 'secondary' | 'danger'"
1739
1739
  },
1740
1740
  "default": "'primary'",
1741
1741
  "description": "The button color",
@@ -2465,6 +2465,28 @@
2465
2465
  }
2466
2466
  ]
2467
2467
  },
2468
+ {
2469
+ "kind": "javascript-module",
2470
+ "path": "src/style/base.ts",
2471
+ "declarations": [
2472
+ {
2473
+ "kind": "variable",
2474
+ "name": "krBaseCSS",
2475
+ "default": "css` :host, *, *::before, *::after { box-sizing: border-box; } :host { /* Primary */ --kr-primary: rgb(22, 48, 82); --kr-primary-hover: rgb(16, 36, 62); --kr-primary-text: #ffffff; /* Accent */ --kr-accent: #BEEA4E; --kr-accent-hover: #a8d43a; --kr-accent-text: #000000; /* Text */ --kr-text: #000000; --kr-text-muted: #4b5563; --kr-text-disabled: #9ca3af; /* Borders */ --kr-border: #e5e7eb; } `",
2476
+ "description": "Base theme styles with CSS custom properties.\nImport into components: static styles = [krBaseCSS, css`...`]"
2477
+ }
2478
+ ],
2479
+ "exports": [
2480
+ {
2481
+ "kind": "js",
2482
+ "name": "krBaseCSS",
2483
+ "declaration": {
2484
+ "name": "krBaseCSS",
2485
+ "module": "src/style/base.ts"
2486
+ }
2487
+ }
2488
+ ]
2489
+ },
2468
2490
  {
2469
2491
  "kind": "javascript-module",
2470
2492
  "path": "src/table/table.ts",
@@ -2703,30 +2725,6 @@
2703
2725
  }
2704
2726
  ]
2705
2727
  },
2706
- {
2707
- "kind": "method",
2708
- "name": "_measureTextWidth",
2709
- "privacy": "private",
2710
- "return": {
2711
- "type": {
2712
- "text": "number"
2713
- }
2714
- },
2715
- "parameters": [
2716
- {
2717
- "name": "text",
2718
- "type": {
2719
- "text": "string"
2720
- }
2721
- },
2722
- {
2723
- "name": "font",
2724
- "type": {
2725
- "text": "string"
2726
- }
2727
- }
2728
- ]
2729
- },
2730
2728
  {
2731
2729
  "kind": "method",
2732
2730
  "name": "_lockColumnWidths",
@@ -3006,28 +3004,6 @@
3006
3004
  }
3007
3005
  ]
3008
3006
  },
3009
- {
3010
- "kind": "javascript-module",
3011
- "path": "src/style/base.ts",
3012
- "declarations": [
3013
- {
3014
- "kind": "variable",
3015
- "name": "krBaseCSS",
3016
- "default": "css` :host, *, *::before, *::after { box-sizing: border-box; } :host { /* Primary */ --kr-primary: rgb(22, 48, 82); --kr-primary-hover: rgb(16, 36, 62); --kr-primary-text: #ffffff; /* Accent */ --kr-accent: #BEEA4E; --kr-accent-hover: #a8d43a; --kr-accent-text: #000000; /* Text */ --kr-text: #000000; --kr-text-muted: #4b5563; --kr-text-disabled: #9ca3af; /* Borders */ --kr-border: #e5e7eb; } `",
3017
- "description": "Base theme styles with CSS custom properties.\nImport into components: static styles = [krBaseCSS, css`...`]"
3018
- }
3019
- ],
3020
- "exports": [
3021
- {
3022
- "kind": "js",
3023
- "name": "krBaseCSS",
3024
- "declaration": {
3025
- "name": "krBaseCSS",
3026
- "module": "src/style/base.ts"
3027
- }
3028
- }
3029
- ]
3030
- },
3031
3007
  {
3032
3008
  "kind": "javascript-module",
3033
3009
  "path": "src/tabs/tab.ts",
@@ -3436,6 +3412,50 @@
3436
3412
  }
3437
3413
  ]
3438
3414
  },
3415
+ {
3416
+ "kind": "javascript-module",
3417
+ "path": "dist/form/select/select-option.js",
3418
+ "declarations": [
3419
+ {
3420
+ "kind": "variable",
3421
+ "name": "KRSelectOption",
3422
+ "default": "class KRSelectOption extends LitElement { constructor() { super(...arguments); /** * The option value */ this.value = ''; /** * Whether the option is disabled */ this.disabled = false; } /** Gets the label text from the slot */ get label() { return this.textContent?.trim() || ''; } render() { return html `<slot></slot>`; } }",
3423
+ "description": "An option for the kr-select component."
3424
+ }
3425
+ ],
3426
+ "exports": [
3427
+ {
3428
+ "kind": "js",
3429
+ "name": "KRSelectOption",
3430
+ "declaration": {
3431
+ "name": "KRSelectOption",
3432
+ "module": "dist/form/select/select-option.js"
3433
+ }
3434
+ }
3435
+ ]
3436
+ },
3437
+ {
3438
+ "kind": "javascript-module",
3439
+ "path": "dist/form/select/select.js",
3440
+ "declarations": [
3441
+ {
3442
+ "kind": "variable",
3443
+ "name": "KRSelect",
3444
+ "default": "class KRSelect extends LitElement { constructor() { super(); /** * The select label text */ this.label = ''; /** * The input name for form submission */ this.name = ''; /** * The currently selected value */ this.value = ''; /** * Placeholder text when no option is selected */ this.placeholder = 'Select an option'; /** * Whether the select is disabled */ this.disabled = false; /** * Whether the field is required */ this.required = false; /** * Whether the field is readonly */ this.readonly = false; /** * Helper text shown below the select */ this.hint = ''; this._isOpen = false; this._highlightedIndex = -1; this._touched = false; this._handleInvalid = (e) => { e.preventDefault(); this._touched = true; }; this._handleOutsideClick = (e) => { if (!e.composedPath().includes(this)) { this._close(); } }; this._handleKeyDown = (e) => { if (!this._isOpen) return; const options = Array.from(this.querySelectorAll('kr-select-option')); switch (e.key) { case 'Escape': this._close(); this._triggerElement?.focus(); break; case 'ArrowDown': e.preventDefault(); if (options.some(o => !o.disabled)) { let newIndex = this._highlightedIndex + 1; while (newIndex < options.length && options[newIndex]?.disabled) newIndex++; if (newIndex < options.length) this._highlightedIndex = newIndex; } break; case 'ArrowUp': e.preventDefault(); { let newIndex = this._highlightedIndex - 1; while (newIndex >= 0 && options[newIndex]?.disabled) newIndex--; if (newIndex >= 0) this._highlightedIndex = newIndex; } break; case 'Enter': e.preventDefault(); if (this._highlightedIndex >= 0 && this._highlightedIndex < options.length) { this._selectOption(options[this._highlightedIndex]); } break; } }; this._internals = this.attachInternals(); } // Form-associated custom element callbacks 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 = false; this._internals.setFormValue(''); this._internals.setValidity({}); } formStateRestoreCallback(state) { this.value = state; } connectedCallback() { super.connectedCallback(); document.addEventListener('click', this._handleOutsideClick); document.addEventListener('keydown', this._handleKeyDown); this.addEventListener('invalid', this._handleInvalid); } firstUpdated() { this._updateValidity(); } updated(changedProperties) { if (changedProperties.has('required') || changedProperties.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) return; if (this._isOpen) { this._close(); } else { this._isOpen = true; const options = Array.from(this.querySelectorAll('kr-select-option')); this._highlightedIndex = options.findIndex(o => o.value === this.value); } } _close() { this._isOpen = false; this._highlightedIndex = -1; } _selectOption(option) { if (option.disabled) return; this.value = option.value; this._internals.setFormValue(this.value); this._updateValidity(); this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); this._close(); this._triggerElement?.focus(); } _handleBlur() { this._touched = true; this._updateValidity(); } _updateValidity() { if (this.required && !this.value) { this._internals.setValidity({ valueMissing: true }, 'Please select an option', this._triggerElement); } else { this._internals.setValidity({}); } } render() { const options = Array.from(this.querySelectorAll('kr-select-option')); const selectedLabel = options.find(o => o.value === this.value)?.label; return html ` <div class=\"wrapper\"> ${this.label ? html ` <label> ${this.label} ${this.required ? html `<span class=\"required\" aria-hidden=\"true\">*</span>` : ''} </label> ` : nothing} <div class=\"select-wrapper\"> <button class=${classMap({ 'select-trigger': true, '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=${classMap({ 'select-value': true, 'select-placeholder': !selectedLabel })}> ${selectedLabel || this.placeholder} </span> <svg class=${classMap({ 'chevron-icon': true, 'select-icon': true, 'select-icon--open': this._isOpen })} viewBox=\"0 0 20 20\" fill=\"currentColor\" > <path fill-rule=\"evenodd\" d=\"M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z\" clip-rule=\"evenodd\" /> </svg> </button> <div class=${classMap({ 'select-dropdown': true, 'hidden': !this._isOpen })} role=\"listbox\"> <div class=\"select-options\"> ${options.length === 0 ? html `<div class=\"select-empty\">No options available</div>` : options.map((option, idx) => { const isSelected = option.value === this.value; return html ` <div class=${classMap({ 'select-option': true, 'select-option--selected': isSelected, 'select-option--disabled': option.disabled, 'select-option--highlighted': idx === this._highlightedIndex, })} role=\"option\" aria-selected=${isSelected} @click=${() => this._selectOption(option)} @mouseenter=${() => (this._highlightedIndex = idx)} > ${option.label} ${isSelected ? html `<svg class=\"chevron-icon select-check\" viewBox=\"0 0 20 20\" fill=\"currentColor\"> <path fill-rule=\"evenodd\" d=\"M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z\" clip-rule=\"evenodd\" /> </svg>` : nothing} </div> `; })} </div> </div> </div> ${this._touched && this.required && !this.value ? html `<div class=\"validation-message\">Please select an option</div>` : this.hint ? html `<div class=\"hint\">${this.hint}</div>` : ''} </div> <div class=\"options-slot\"> <slot @slotchange=${() => this.requestUpdate()}></slot> </div> `; } // Public methods for programmatic control focus() { this._triggerElement?.focus(); } blur() { this._triggerElement?.blur(); } }",
3445
+ "description": "A select dropdown component that works with native browser forms.\n\nUses ElementInternals for form association, allowing the component\nto participate in form submission, validation, and reset."
3446
+ }
3447
+ ],
3448
+ "exports": [
3449
+ {
3450
+ "kind": "js",
3451
+ "name": "KRSelect",
3452
+ "declaration": {
3453
+ "name": "KRSelect",
3454
+ "module": "dist/form/select/select.js"
3455
+ }
3456
+ }
3457
+ ]
3458
+ },
3439
3459
  {
3440
3460
  "kind": "javascript-module",
3441
3461
  "path": "dist/form/text-field/text-field.js",
@@ -3460,146 +3480,197 @@
3460
3480
  },
3461
3481
  {
3462
3482
  "kind": "javascript-module",
3463
- "path": "src/form/text-field/text-field.ts",
3483
+ "path": "src/form/select/select-option.ts",
3464
3484
  "declarations": [
3465
3485
  {
3466
3486
  "kind": "class",
3467
- "description": "A text field component that works with native browser forms.\n\nUses ElementInternals for form association, allowing the component\nto participate in form submission, validation, and reset.\n\nNative input and change events bubble up from the inner input element.",
3468
- "name": "KRTextField",
3469
- "members": [
3470
- {
3471
- "kind": "field",
3472
- "name": "formAssociated",
3473
- "type": {
3474
- "text": "boolean"
3475
- },
3476
- "static": true,
3477
- "default": "true"
3478
- },
3487
+ "description": "An option for the kr-select component.",
3488
+ "name": "KRSelectOption",
3489
+ "slots": [
3479
3490
  {
3480
- "kind": "field",
3481
- "name": "_internals",
3482
- "type": {
3483
- "text": "ElementInternals"
3484
- },
3485
- "privacy": "private"
3486
- },
3491
+ "description": "The option label content",
3492
+ "name": ""
3493
+ }
3494
+ ],
3495
+ "members": [
3487
3496
  {
3488
3497
  "kind": "field",
3489
- "name": "label",
3498
+ "name": "value",
3490
3499
  "type": {
3491
3500
  "text": "string"
3492
3501
  },
3493
3502
  "default": "''",
3494
- "description": "The input label text",
3495
- "attribute": "label"
3503
+ "description": "The option value",
3504
+ "attribute": "value"
3496
3505
  },
3497
3506
  {
3498
3507
  "kind": "field",
3499
- "name": "name",
3508
+ "name": "disabled",
3500
3509
  "type": {
3501
- "text": "string"
3510
+ "text": "boolean"
3502
3511
  },
3503
- "default": "''",
3504
- "description": "The input name for form submission",
3505
- "attribute": "name"
3512
+ "default": "false",
3513
+ "description": "Whether the option is disabled",
3514
+ "attribute": "disabled"
3506
3515
  },
3507
3516
  {
3508
3517
  "kind": "field",
3509
- "name": "value",
3518
+ "name": "label",
3510
3519
  "type": {
3511
3520
  "text": "string"
3512
3521
  },
3513
- "default": "''",
3514
- "description": "The current value",
3515
- "attribute": "value"
3516
- },
3522
+ "description": "Gets the label text from the slot",
3523
+ "readonly": true
3524
+ }
3525
+ ],
3526
+ "attributes": [
3517
3527
  {
3518
- "kind": "field",
3519
- "name": "placeholder",
3528
+ "name": "value",
3520
3529
  "type": {
3521
3530
  "text": "string"
3522
3531
  },
3523
3532
  "default": "''",
3524
- "description": "Placeholder text",
3525
- "attribute": "placeholder"
3533
+ "description": "The option value",
3534
+ "fieldName": "value"
3526
3535
  },
3527
3536
  {
3528
- "kind": "field",
3529
- "name": "type",
3537
+ "name": "disabled",
3530
3538
  "type": {
3531
- "text": "'text' | 'email' | 'password' | 'tel' | 'url' | 'search'"
3539
+ "text": "boolean"
3532
3540
  },
3533
- "default": "'text'",
3534
- "description": "Input type (text, email, password, tel, url, search)",
3535
- "attribute": "type"
3536
- },
3541
+ "default": "false",
3542
+ "description": "Whether the option is disabled",
3543
+ "fieldName": "disabled"
3544
+ }
3545
+ ],
3546
+ "superclass": {
3547
+ "name": "LitElement",
3548
+ "package": "lit"
3549
+ },
3550
+ "tagName": "kr-select-option",
3551
+ "customElement": true
3552
+ }
3553
+ ],
3554
+ "exports": [
3555
+ {
3556
+ "kind": "js",
3557
+ "name": "KRSelectOption",
3558
+ "declaration": {
3559
+ "name": "KRSelectOption",
3560
+ "module": "src/form/select/select-option.ts"
3561
+ }
3562
+ },
3563
+ {
3564
+ "kind": "custom-element-definition",
3565
+ "name": "kr-select-option",
3566
+ "declaration": {
3567
+ "name": "KRSelectOption",
3568
+ "module": "src/form/select/select-option.ts"
3569
+ }
3570
+ }
3571
+ ]
3572
+ },
3573
+ {
3574
+ "kind": "javascript-module",
3575
+ "path": "src/form/select/select.ts",
3576
+ "declarations": [
3577
+ {
3578
+ "kind": "class",
3579
+ "description": "A select dropdown component that works with native browser forms.\n\nUses ElementInternals for form association, allowing the component\nto participate in form submission, validation, and reset.",
3580
+ "name": "KRSelect",
3581
+ "slots": [
3582
+ {
3583
+ "description": "The kr-select-option elements",
3584
+ "name": ""
3585
+ }
3586
+ ],
3587
+ "members": [
3537
3588
  {
3538
3589
  "kind": "field",
3539
- "name": "required",
3590
+ "name": "formAssociated",
3540
3591
  "type": {
3541
3592
  "text": "boolean"
3542
3593
  },
3543
- "default": "false",
3544
- "description": "Whether the field is required",
3545
- "attribute": "required"
3594
+ "static": true,
3595
+ "default": "true"
3546
3596
  },
3547
3597
  {
3548
3598
  "kind": "field",
3549
- "name": "disabled",
3599
+ "name": "_internals",
3550
3600
  "type": {
3551
- "text": "boolean"
3601
+ "text": "ElementInternals"
3552
3602
  },
3553
- "default": "false",
3554
- "description": "Whether the field is disabled",
3555
- "attribute": "disabled"
3603
+ "privacy": "private"
3556
3604
  },
3557
3605
  {
3558
3606
  "kind": "field",
3559
- "name": "readonly",
3607
+ "name": "label",
3560
3608
  "type": {
3561
- "text": "boolean"
3609
+ "text": "string"
3562
3610
  },
3563
- "default": "false",
3564
- "description": "Whether the field is readonly",
3565
- "attribute": "readonly"
3611
+ "default": "''",
3612
+ "description": "The select label text",
3613
+ "attribute": "label"
3566
3614
  },
3567
3615
  {
3568
3616
  "kind": "field",
3569
- "name": "minlength",
3617
+ "name": "name",
3570
3618
  "type": {
3571
- "text": "number | undefined"
3619
+ "text": "string"
3572
3620
  },
3573
- "description": "Minimum length for the value",
3574
- "attribute": "minlength"
3621
+ "default": "''",
3622
+ "description": "The input name for form submission",
3623
+ "attribute": "name"
3575
3624
  },
3576
3625
  {
3577
3626
  "kind": "field",
3578
- "name": "maxlength",
3627
+ "name": "value",
3579
3628
  "type": {
3580
- "text": "number | undefined"
3629
+ "text": "string"
3581
3630
  },
3582
- "description": "Maximum length for the value",
3583
- "attribute": "maxlength"
3631
+ "default": "''",
3632
+ "description": "The currently selected value",
3633
+ "attribute": "value"
3584
3634
  },
3585
3635
  {
3586
3636
  "kind": "field",
3587
- "name": "pattern",
3637
+ "name": "placeholder",
3588
3638
  "type": {
3589
- "text": "string | undefined"
3639
+ "text": "string"
3590
3640
  },
3591
- "description": "Pattern for validation (regex)",
3592
- "attribute": "pattern"
3641
+ "default": "'Select an option'",
3642
+ "description": "Placeholder text when no option is selected",
3643
+ "attribute": "placeholder"
3593
3644
  },
3594
3645
  {
3595
3646
  "kind": "field",
3596
- "name": "autocomplete",
3647
+ "name": "disabled",
3597
3648
  "type": {
3598
- "text": "string"
3649
+ "text": "boolean"
3599
3650
  },
3600
- "default": "''",
3601
- "description": "Autocomplete attribute value",
3602
- "attribute": "autocomplete"
3651
+ "default": "false",
3652
+ "description": "Whether the select is disabled",
3653
+ "attribute": "disabled"
3654
+ },
3655
+ {
3656
+ "kind": "field",
3657
+ "name": "required",
3658
+ "type": {
3659
+ "text": "boolean"
3660
+ },
3661
+ "default": "false",
3662
+ "description": "Whether the field is required",
3663
+ "attribute": "required"
3664
+ },
3665
+ {
3666
+ "kind": "field",
3667
+ "name": "readonly",
3668
+ "type": {
3669
+ "text": "boolean"
3670
+ },
3671
+ "default": "false",
3672
+ "description": "Whether the field is readonly",
3673
+ "attribute": "readonly"
3603
3674
  },
3604
3675
  {
3605
3676
  "kind": "field",
@@ -3608,35 +3679,44 @@
3608
3679
  "text": "string"
3609
3680
  },
3610
3681
  "default": "''",
3611
- "description": "Helper text shown below the input",
3682
+ "description": "Helper text shown below the select",
3612
3683
  "attribute": "hint"
3613
3684
  },
3614
3685
  {
3615
3686
  "kind": "field",
3616
- "name": "_input",
3687
+ "name": "_isOpen",
3617
3688
  "type": {
3618
- "text": "HTMLInputElement"
3689
+ "text": "boolean"
3619
3690
  },
3620
- "privacy": "private"
3691
+ "privacy": "private",
3692
+ "default": "false"
3621
3693
  },
3622
3694
  {
3623
3695
  "kind": "field",
3624
- "name": "_touched",
3696
+ "name": "_highlightedIndex",
3625
3697
  "type": {
3626
- "text": "boolean"
3698
+ "text": "number"
3627
3699
  },
3628
3700
  "privacy": "private",
3629
- "default": "false"
3701
+ "default": "-1"
3630
3702
  },
3631
3703
  {
3632
3704
  "kind": "field",
3633
- "name": "_dirty",
3705
+ "name": "_touched",
3634
3706
  "type": {
3635
3707
  "text": "boolean"
3636
3708
  },
3637
3709
  "privacy": "private",
3638
3710
  "default": "false"
3639
3711
  },
3712
+ {
3713
+ "kind": "field",
3714
+ "name": "_triggerElement",
3715
+ "type": {
3716
+ "text": "HTMLButtonElement"
3717
+ },
3718
+ "privacy": "private"
3719
+ },
3640
3720
  {
3641
3721
  "kind": "field",
3642
3722
  "name": "form",
@@ -3686,33 +3766,35 @@
3686
3766
  "name": "_handleInvalid",
3687
3767
  "privacy": "private"
3688
3768
  },
3769
+ {
3770
+ "kind": "field",
3771
+ "name": "_handleOutsideClick",
3772
+ "privacy": "private"
3773
+ },
3774
+ {
3775
+ "kind": "field",
3776
+ "name": "_handleKeyDown",
3777
+ "privacy": "private"
3778
+ },
3689
3779
  {
3690
3780
  "kind": "method",
3691
- "name": "_updateValidity",
3781
+ "name": "_toggle",
3692
3782
  "privacy": "private"
3693
3783
  },
3694
3784
  {
3695
3785
  "kind": "method",
3696
- "name": "_handleInput",
3697
- "privacy": "private",
3698
- "parameters": [
3699
- {
3700
- "name": "e",
3701
- "type": {
3702
- "text": "Event"
3703
- }
3704
- }
3705
- ]
3786
+ "name": "_close",
3787
+ "privacy": "private"
3706
3788
  },
3707
3789
  {
3708
3790
  "kind": "method",
3709
- "name": "_handleChange",
3791
+ "name": "_selectOption",
3710
3792
  "privacy": "private",
3711
3793
  "parameters": [
3712
3794
  {
3713
- "name": "e",
3795
+ "name": "option",
3714
3796
  "type": {
3715
- "text": "Event"
3797
+ "text": "KRSelectOption"
3716
3798
  }
3717
3799
  }
3718
3800
  ]
@@ -3724,15 +3806,24 @@
3724
3806
  },
3725
3807
  {
3726
3808
  "kind": "method",
3727
- "name": "focus"
3809
+ "name": "_updateValidity",
3810
+ "privacy": "private"
3728
3811
  },
3729
3812
  {
3730
3813
  "kind": "method",
3731
- "name": "blur"
3814
+ "name": "focus"
3732
3815
  },
3733
3816
  {
3734
3817
  "kind": "method",
3735
- "name": "select"
3818
+ "name": "blur"
3819
+ }
3820
+ ],
3821
+ "events": [
3822
+ {
3823
+ "name": "change",
3824
+ "type": {
3825
+ "text": "Event"
3826
+ }
3736
3827
  }
3737
3828
  ],
3738
3829
  "attributes": [
@@ -3742,7 +3833,7 @@
3742
3833
  "text": "string"
3743
3834
  },
3744
3835
  "default": "''",
3745
- "description": "The input label text",
3836
+ "description": "The select label text",
3746
3837
  "fieldName": "label"
3747
3838
  },
3748
3839
  {
@@ -3760,7 +3851,7 @@
3760
3851
  "text": "string"
3761
3852
  },
3762
3853
  "default": "''",
3763
- "description": "The current value",
3854
+ "description": "The currently selected value",
3764
3855
  "fieldName": "value"
3765
3856
  },
3766
3857
  {
@@ -3768,266 +3859,82 @@
3768
3859
  "type": {
3769
3860
  "text": "string"
3770
3861
  },
3771
- "default": "''",
3772
- "description": "Placeholder text",
3862
+ "default": "'Select an option'",
3863
+ "description": "Placeholder text when no option is selected",
3773
3864
  "fieldName": "placeholder"
3774
3865
  },
3775
- {
3776
- "name": "type",
3777
- "type": {
3778
- "text": "'text' | 'email' | 'password' | 'tel' | 'url' | 'search'"
3779
- },
3780
- "default": "'text'",
3781
- "description": "Input type (text, email, password, tel, url, search)",
3782
- "fieldName": "type"
3783
- },
3784
- {
3785
- "name": "required",
3786
- "type": {
3787
- "text": "boolean"
3788
- },
3789
- "default": "false",
3790
- "description": "Whether the field is required",
3791
- "fieldName": "required"
3792
- },
3793
3866
  {
3794
3867
  "name": "disabled",
3795
3868
  "type": {
3796
3869
  "text": "boolean"
3797
3870
  },
3798
3871
  "default": "false",
3799
- "description": "Whether the field is disabled",
3872
+ "description": "Whether the select is disabled",
3800
3873
  "fieldName": "disabled"
3801
3874
  },
3802
3875
  {
3803
- "name": "readonly",
3804
- "type": {
3805
- "text": "boolean"
3806
- },
3807
- "default": "false",
3808
- "description": "Whether the field is readonly",
3809
- "fieldName": "readonly"
3810
- },
3811
- {
3812
- "name": "minlength",
3813
- "type": {
3814
- "text": "number | undefined"
3815
- },
3816
- "description": "Minimum length for the value",
3817
- "fieldName": "minlength"
3818
- },
3819
- {
3820
- "name": "maxlength",
3821
- "type": {
3822
- "text": "number | undefined"
3823
- },
3824
- "description": "Maximum length for the value",
3825
- "fieldName": "maxlength"
3826
- },
3827
- {
3828
- "name": "pattern",
3829
- "type": {
3830
- "text": "string | undefined"
3831
- },
3832
- "description": "Pattern for validation (regex)",
3833
- "fieldName": "pattern"
3834
- },
3835
- {
3836
- "name": "autocomplete",
3837
- "type": {
3838
- "text": "string"
3839
- },
3840
- "default": "''",
3841
- "description": "Autocomplete attribute value",
3842
- "fieldName": "autocomplete"
3843
- },
3844
- {
3845
- "name": "hint",
3846
- "type": {
3847
- "text": "string"
3848
- },
3849
- "default": "''",
3850
- "description": "Helper text shown below the input",
3851
- "fieldName": "hint"
3852
- }
3853
- ],
3854
- "superclass": {
3855
- "name": "LitElement",
3856
- "package": "lit"
3857
- },
3858
- "tagName": "kr-text-field",
3859
- "customElement": true
3860
- }
3861
- ],
3862
- "exports": [
3863
- {
3864
- "kind": "js",
3865
- "name": "KRTextField",
3866
- "declaration": {
3867
- "name": "KRTextField",
3868
- "module": "src/form/text-field/text-field.ts"
3869
- }
3870
- },
3871
- {
3872
- "kind": "custom-element-definition",
3873
- "name": "kr-text-field",
3874
- "declaration": {
3875
- "name": "KRTextField",
3876
- "module": "src/form/text-field/text-field.ts"
3877
- }
3878
- }
3879
- ]
3880
- },
3881
- {
3882
- "kind": "javascript-module",
3883
- "path": "dist/form/select/select-option.js",
3884
- "declarations": [
3885
- {
3886
- "kind": "variable",
3887
- "name": "KRSelectOption",
3888
- "default": "class KRSelectOption extends LitElement { constructor() { super(...arguments); /** * The option value */ this.value = ''; /** * Whether the option is disabled */ this.disabled = false; } /** Gets the label text from the slot */ get label() { return this.textContent?.trim() || ''; } render() { return html `<slot></slot>`; } }",
3889
- "description": "An option for the kr-select component."
3890
- }
3891
- ],
3892
- "exports": [
3893
- {
3894
- "kind": "js",
3895
- "name": "KRSelectOption",
3896
- "declaration": {
3897
- "name": "KRSelectOption",
3898
- "module": "dist/form/select/select-option.js"
3899
- }
3900
- }
3901
- ]
3902
- },
3903
- {
3904
- "kind": "javascript-module",
3905
- "path": "dist/form/select/select.js",
3906
- "declarations": [
3907
- {
3908
- "kind": "variable",
3909
- "name": "KRSelect",
3910
- "default": "class KRSelect extends LitElement { constructor() { super(); /** * The select label text */ this.label = ''; /** * The input name for form submission */ this.name = ''; /** * The currently selected value */ this.value = ''; /** * Placeholder text when no option is selected */ this.placeholder = 'Select an option'; /** * Whether the select is disabled */ this.disabled = false; /** * Whether the field is required */ this.required = false; /** * Whether the field is readonly */ this.readonly = false; /** * Helper text shown below the select */ this.hint = ''; this._isOpen = false; this._highlightedIndex = -1; this._touched = false; this._handleInvalid = (e) => { e.preventDefault(); this._touched = true; }; this._handleOutsideClick = (e) => { if (!this.contains(e.target)) { this._close(); } }; this._handleKeyDown = (e) => { if (!this._isOpen) return; const options = Array.from(this.querySelectorAll('kr-select-option')); switch (e.key) { case 'Escape': this._close(); this._triggerElement?.focus(); break; case 'ArrowDown': e.preventDefault(); if (options.some(o => !o.disabled)) { let newIndex = this._highlightedIndex + 1; while (newIndex < options.length && options[newIndex]?.disabled) newIndex++; if (newIndex < options.length) this._highlightedIndex = newIndex; } break; case 'ArrowUp': e.preventDefault(); { let newIndex = this._highlightedIndex - 1; while (newIndex >= 0 && options[newIndex]?.disabled) newIndex--; if (newIndex >= 0) this._highlightedIndex = newIndex; } break; case 'Enter': e.preventDefault(); if (this._highlightedIndex >= 0 && this._highlightedIndex < options.length) { this._selectOption(options[this._highlightedIndex]); } break; } }; this._internals = this.attachInternals(); } // Form-associated custom element callbacks 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 = false; this._internals.setFormValue(''); this._internals.setValidity({}); } formStateRestoreCallback(state) { this.value = state; } connectedCallback() { super.connectedCallback(); document.addEventListener('click', this._handleOutsideClick); document.addEventListener('keydown', this._handleKeyDown); this.addEventListener('invalid', this._handleInvalid); } firstUpdated() { this._updateValidity(); } updated(changedProperties) { if (changedProperties.has('required') || changedProperties.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) return; if (this._isOpen) { this._close(); } else { this._isOpen = true; const options = Array.from(this.querySelectorAll('kr-select-option')); this._highlightedIndex = options.findIndex(o => o.value === this.value); } } _close() { this._isOpen = false; this._highlightedIndex = -1; } _selectOption(option) { if (option.disabled) return; this.value = option.value; this._internals.setFormValue(this.value); this._updateValidity(); this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); this._close(); this._triggerElement?.focus(); } _handleBlur() { this._touched = true; this._updateValidity(); } _updateValidity() { if (this.required && !this.value) { this._internals.setValidity({ valueMissing: true }, 'Please select an option', this._triggerElement); } else { this._internals.setValidity({}); } } render() { const options = Array.from(this.querySelectorAll('kr-select-option')); const selectedLabel = options.find(o => o.value === this.value)?.label; return html ` <div class=\"wrapper\"> ${this.label ? html ` <label> ${this.label} ${this.required ? html `<span class=\"required\" aria-hidden=\"true\">*</span>` : ''} </label> ` : nothing} <div class=\"select-wrapper\"> <button class=${classMap({ 'select-trigger': true, '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=${classMap({ 'select-value': true, 'select-placeholder': !selectedLabel })}> ${selectedLabel || this.placeholder} </span> <svg class=${classMap({ 'chevron-icon': true, 'select-icon': true, 'select-icon--open': this._isOpen })} viewBox=\"0 0 20 20\" fill=\"currentColor\" > <path fill-rule=\"evenodd\" d=\"M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z\" clip-rule=\"evenodd\" /> </svg> </button> <div class=${classMap({ 'select-dropdown': true, 'hidden': !this._isOpen })} role=\"listbox\"> <div class=\"select-options\"> ${options.length === 0 ? html `<div class=\"select-empty\">No options available</div>` : options.map((option, idx) => { const isSelected = option.value === this.value; return html ` <div class=${classMap({ 'select-option': true, 'select-option--selected': isSelected, 'select-option--disabled': option.disabled, 'select-option--highlighted': idx === this._highlightedIndex, })} role=\"option\" aria-selected=${isSelected} @click=${() => this._selectOption(option)} @mouseenter=${() => (this._highlightedIndex = idx)} > ${option.label} ${isSelected ? html `<svg class=\"chevron-icon select-check\" viewBox=\"0 0 20 20\" fill=\"currentColor\"> <path fill-rule=\"evenodd\" d=\"M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z\" clip-rule=\"evenodd\" /> </svg>` : nothing} </div> `; })} </div> </div> </div> ${this._touched && this.required && !this.value ? html `<div class=\"validation-message\">Please select an option</div>` : this.hint ? html `<div class=\"hint\">${this.hint}</div>` : ''} </div> <div class=\"options-slot\"> <slot @slotchange=${() => this.requestUpdate()}></slot> </div> `; } // Public methods for programmatic control focus() { this._triggerElement?.focus(); } blur() { this._triggerElement?.blur(); } }",
3911
- "description": "A select dropdown component that works with native browser forms.\n\nUses ElementInternals for form association, allowing the component\nto participate in form submission, validation, and reset."
3912
- }
3913
- ],
3914
- "exports": [
3915
- {
3916
- "kind": "js",
3917
- "name": "KRSelect",
3918
- "declaration": {
3919
- "name": "KRSelect",
3920
- "module": "dist/form/select/select.js"
3921
- }
3922
- }
3923
- ]
3924
- },
3925
- {
3926
- "kind": "javascript-module",
3927
- "path": "src/form/select/select-option.ts",
3928
- "declarations": [
3929
- {
3930
- "kind": "class",
3931
- "description": "An option for the kr-select component.",
3932
- "name": "KRSelectOption",
3933
- "slots": [
3934
- {
3935
- "description": "The option label content",
3936
- "name": ""
3937
- }
3938
- ],
3939
- "members": [
3940
- {
3941
- "kind": "field",
3942
- "name": "value",
3943
- "type": {
3944
- "text": "string"
3945
- },
3946
- "default": "''",
3947
- "description": "The option value",
3948
- "attribute": "value"
3949
- },
3950
- {
3951
- "kind": "field",
3952
- "name": "disabled",
3953
- "type": {
3954
- "text": "boolean"
3955
- },
3956
- "default": "false",
3957
- "description": "Whether the option is disabled",
3958
- "attribute": "disabled"
3959
- },
3960
- {
3961
- "kind": "field",
3962
- "name": "label",
3963
- "type": {
3964
- "text": "string"
3965
- },
3966
- "description": "Gets the label text from the slot",
3967
- "readonly": true
3968
- }
3969
- ],
3970
- "attributes": [
3971
- {
3972
- "name": "value",
3876
+ "name": "required",
3973
3877
  "type": {
3974
- "text": "string"
3878
+ "text": "boolean"
3975
3879
  },
3976
- "default": "''",
3977
- "description": "The option value",
3978
- "fieldName": "value"
3880
+ "default": "false",
3881
+ "description": "Whether the field is required",
3882
+ "fieldName": "required"
3979
3883
  },
3980
3884
  {
3981
- "name": "disabled",
3885
+ "name": "readonly",
3982
3886
  "type": {
3983
3887
  "text": "boolean"
3984
3888
  },
3985
3889
  "default": "false",
3986
- "description": "Whether the option is disabled",
3987
- "fieldName": "disabled"
3890
+ "description": "Whether the field is readonly",
3891
+ "fieldName": "readonly"
3892
+ },
3893
+ {
3894
+ "name": "hint",
3895
+ "type": {
3896
+ "text": "string"
3897
+ },
3898
+ "default": "''",
3899
+ "description": "Helper text shown below the select",
3900
+ "fieldName": "hint"
3988
3901
  }
3989
3902
  ],
3990
3903
  "superclass": {
3991
3904
  "name": "LitElement",
3992
3905
  "package": "lit"
3993
3906
  },
3994
- "tagName": "kr-select-option",
3907
+ "tagName": "kr-select",
3995
3908
  "customElement": true
3996
3909
  }
3997
3910
  ],
3998
3911
  "exports": [
3999
3912
  {
4000
3913
  "kind": "js",
4001
- "name": "KRSelectOption",
3914
+ "name": "KRSelect",
4002
3915
  "declaration": {
4003
- "name": "KRSelectOption",
4004
- "module": "src/form/select/select-option.ts"
3916
+ "name": "KRSelect",
3917
+ "module": "src/form/select/select.ts"
4005
3918
  }
4006
3919
  },
4007
3920
  {
4008
3921
  "kind": "custom-element-definition",
4009
- "name": "kr-select-option",
3922
+ "name": "kr-select",
4010
3923
  "declaration": {
4011
- "name": "KRSelectOption",
4012
- "module": "src/form/select/select-option.ts"
3924
+ "name": "KRSelect",
3925
+ "module": "src/form/select/select.ts"
4013
3926
  }
4014
3927
  }
4015
3928
  ]
4016
3929
  },
4017
3930
  {
4018
3931
  "kind": "javascript-module",
4019
- "path": "src/form/select/select.ts",
3932
+ "path": "src/form/text-field/text-field.ts",
4020
3933
  "declarations": [
4021
3934
  {
4022
3935
  "kind": "class",
4023
- "description": "A select dropdown component that works with native browser forms.\n\nUses ElementInternals for form association, allowing the component\nto participate in form submission, validation, and reset.",
4024
- "name": "KRSelect",
4025
- "slots": [
4026
- {
4027
- "description": "The kr-select-option elements",
4028
- "name": ""
4029
- }
4030
- ],
3936
+ "description": "A text field component that works with native browser forms.\n\nUses ElementInternals for form association, allowing the component\nto participate in form submission, validation, and reset.\n\nNative input and change events bubble up from the inner input element.",
3937
+ "name": "KRTextField",
4031
3938
  "members": [
4032
3939
  {
4033
3940
  "kind": "field",
@@ -4053,7 +3960,7 @@
4053
3960
  "text": "string"
4054
3961
  },
4055
3962
  "default": "''",
4056
- "description": "The select label text",
3963
+ "description": "The input label text",
4057
3964
  "attribute": "label"
4058
3965
  },
4059
3966
  {
@@ -4073,7 +3980,7 @@
4073
3980
  "text": "string"
4074
3981
  },
4075
3982
  "default": "''",
4076
- "description": "The currently selected value",
3983
+ "description": "The current value",
4077
3984
  "attribute": "value"
4078
3985
  },
4079
3986
  {
@@ -4082,19 +3989,19 @@
4082
3989
  "type": {
4083
3990
  "text": "string"
4084
3991
  },
4085
- "default": "'Select an option'",
4086
- "description": "Placeholder text when no option is selected",
3992
+ "default": "''",
3993
+ "description": "Placeholder text",
4087
3994
  "attribute": "placeholder"
4088
3995
  },
4089
3996
  {
4090
3997
  "kind": "field",
4091
- "name": "disabled",
3998
+ "name": "type",
4092
3999
  "type": {
4093
- "text": "boolean"
4000
+ "text": "'text' | 'email' | 'password' | 'tel' | 'url' | 'search'"
4094
4001
  },
4095
- "default": "false",
4096
- "description": "Whether the select is disabled",
4097
- "attribute": "disabled"
4002
+ "default": "'text'",
4003
+ "description": "Input type (text, email, password, tel, url, search)",
4004
+ "attribute": "type"
4098
4005
  },
4099
4006
  {
4100
4007
  "kind": "field",
@@ -4106,6 +4013,16 @@
4106
4013
  "description": "Whether the field is required",
4107
4014
  "attribute": "required"
4108
4015
  },
4016
+ {
4017
+ "kind": "field",
4018
+ "name": "disabled",
4019
+ "type": {
4020
+ "text": "boolean"
4021
+ },
4022
+ "default": "false",
4023
+ "description": "Whether the field is disabled",
4024
+ "attribute": "disabled"
4025
+ },
4109
4026
  {
4110
4027
  "kind": "field",
4111
4028
  "name": "readonly",
@@ -4118,31 +4035,58 @@
4118
4035
  },
4119
4036
  {
4120
4037
  "kind": "field",
4121
- "name": "hint",
4038
+ "name": "minlength",
4039
+ "type": {
4040
+ "text": "number | undefined"
4041
+ },
4042
+ "description": "Minimum length for the value",
4043
+ "attribute": "minlength"
4044
+ },
4045
+ {
4046
+ "kind": "field",
4047
+ "name": "maxlength",
4048
+ "type": {
4049
+ "text": "number | undefined"
4050
+ },
4051
+ "description": "Maximum length for the value",
4052
+ "attribute": "maxlength"
4053
+ },
4054
+ {
4055
+ "kind": "field",
4056
+ "name": "pattern",
4057
+ "type": {
4058
+ "text": "string | undefined"
4059
+ },
4060
+ "description": "Pattern for validation (regex)",
4061
+ "attribute": "pattern"
4062
+ },
4063
+ {
4064
+ "kind": "field",
4065
+ "name": "autocomplete",
4122
4066
  "type": {
4123
4067
  "text": "string"
4124
4068
  },
4125
4069
  "default": "''",
4126
- "description": "Helper text shown below the select",
4127
- "attribute": "hint"
4070
+ "description": "Autocomplete attribute value",
4071
+ "attribute": "autocomplete"
4128
4072
  },
4129
4073
  {
4130
4074
  "kind": "field",
4131
- "name": "_isOpen",
4075
+ "name": "hint",
4132
4076
  "type": {
4133
- "text": "boolean"
4077
+ "text": "string"
4134
4078
  },
4135
- "privacy": "private",
4136
- "default": "false"
4079
+ "default": "''",
4080
+ "description": "Helper text shown below the input",
4081
+ "attribute": "hint"
4137
4082
  },
4138
4083
  {
4139
4084
  "kind": "field",
4140
- "name": "_highlightedIndex",
4085
+ "name": "_input",
4141
4086
  "type": {
4142
- "text": "number"
4087
+ "text": "HTMLInputElement"
4143
4088
  },
4144
- "privacy": "private",
4145
- "default": "-1"
4089
+ "privacy": "private"
4146
4090
  },
4147
4091
  {
4148
4092
  "kind": "field",
@@ -4155,11 +4099,12 @@
4155
4099
  },
4156
4100
  {
4157
4101
  "kind": "field",
4158
- "name": "_triggerElement",
4102
+ "name": "_dirty",
4159
4103
  "type": {
4160
- "text": "HTMLButtonElement"
4104
+ "text": "boolean"
4161
4105
  },
4162
- "privacy": "private"
4106
+ "privacy": "private",
4107
+ "default": "false"
4163
4108
  },
4164
4109
  {
4165
4110
  "kind": "field",
@@ -4210,35 +4155,33 @@
4210
4155
  "name": "_handleInvalid",
4211
4156
  "privacy": "private"
4212
4157
  },
4213
- {
4214
- "kind": "field",
4215
- "name": "_handleOutsideClick",
4216
- "privacy": "private"
4217
- },
4218
- {
4219
- "kind": "field",
4220
- "name": "_handleKeyDown",
4221
- "privacy": "private"
4222
- },
4223
4158
  {
4224
4159
  "kind": "method",
4225
- "name": "_toggle",
4160
+ "name": "_updateValidity",
4226
4161
  "privacy": "private"
4227
4162
  },
4228
4163
  {
4229
4164
  "kind": "method",
4230
- "name": "_close",
4231
- "privacy": "private"
4165
+ "name": "_handleInput",
4166
+ "privacy": "private",
4167
+ "parameters": [
4168
+ {
4169
+ "name": "e",
4170
+ "type": {
4171
+ "text": "Event"
4172
+ }
4173
+ }
4174
+ ]
4232
4175
  },
4233
4176
  {
4234
4177
  "kind": "method",
4235
- "name": "_selectOption",
4178
+ "name": "_handleChange",
4236
4179
  "privacy": "private",
4237
4180
  "parameters": [
4238
4181
  {
4239
- "name": "option",
4182
+ "name": "e",
4240
4183
  "type": {
4241
- "text": "KRSelectOption"
4184
+ "text": "Event"
4242
4185
  }
4243
4186
  }
4244
4187
  ]
@@ -4248,11 +4191,6 @@
4248
4191
  "name": "_handleBlur",
4249
4192
  "privacy": "private"
4250
4193
  },
4251
- {
4252
- "kind": "method",
4253
- "name": "_updateValidity",
4254
- "privacy": "private"
4255
- },
4256
4194
  {
4257
4195
  "kind": "method",
4258
4196
  "name": "focus"
@@ -4260,14 +4198,10 @@
4260
4198
  {
4261
4199
  "kind": "method",
4262
4200
  "name": "blur"
4263
- }
4264
- ],
4265
- "events": [
4201
+ },
4266
4202
  {
4267
- "name": "change",
4268
- "type": {
4269
- "text": "Event"
4270
- }
4203
+ "kind": "method",
4204
+ "name": "select"
4271
4205
  }
4272
4206
  ],
4273
4207
  "attributes": [
@@ -4277,7 +4211,7 @@
4277
4211
  "text": "string"
4278
4212
  },
4279
4213
  "default": "''",
4280
- "description": "The select label text",
4214
+ "description": "The input label text",
4281
4215
  "fieldName": "label"
4282
4216
  },
4283
4217
  {
@@ -4295,7 +4229,7 @@
4295
4229
  "text": "string"
4296
4230
  },
4297
4231
  "default": "''",
4298
- "description": "The currently selected value",
4232
+ "description": "The current value",
4299
4233
  "fieldName": "value"
4300
4234
  },
4301
4235
  {
@@ -4303,18 +4237,18 @@
4303
4237
  "type": {
4304
4238
  "text": "string"
4305
4239
  },
4306
- "default": "'Select an option'",
4307
- "description": "Placeholder text when no option is selected",
4240
+ "default": "''",
4241
+ "description": "Placeholder text",
4308
4242
  "fieldName": "placeholder"
4309
4243
  },
4310
4244
  {
4311
- "name": "disabled",
4245
+ "name": "type",
4312
4246
  "type": {
4313
- "text": "boolean"
4247
+ "text": "'text' | 'email' | 'password' | 'tel' | 'url' | 'search'"
4314
4248
  },
4315
- "default": "false",
4316
- "description": "Whether the select is disabled",
4317
- "fieldName": "disabled"
4249
+ "default": "'text'",
4250
+ "description": "Input type (text, email, password, tel, url, search)",
4251
+ "fieldName": "type"
4318
4252
  },
4319
4253
  {
4320
4254
  "name": "required",
@@ -4325,6 +4259,15 @@
4325
4259
  "description": "Whether the field is required",
4326
4260
  "fieldName": "required"
4327
4261
  },
4262
+ {
4263
+ "name": "disabled",
4264
+ "type": {
4265
+ "text": "boolean"
4266
+ },
4267
+ "default": "false",
4268
+ "description": "Whether the field is disabled",
4269
+ "fieldName": "disabled"
4270
+ },
4328
4271
  {
4329
4272
  "name": "readonly",
4330
4273
  "type": {
@@ -4334,13 +4277,46 @@
4334
4277
  "description": "Whether the field is readonly",
4335
4278
  "fieldName": "readonly"
4336
4279
  },
4280
+ {
4281
+ "name": "minlength",
4282
+ "type": {
4283
+ "text": "number | undefined"
4284
+ },
4285
+ "description": "Minimum length for the value",
4286
+ "fieldName": "minlength"
4287
+ },
4288
+ {
4289
+ "name": "maxlength",
4290
+ "type": {
4291
+ "text": "number | undefined"
4292
+ },
4293
+ "description": "Maximum length for the value",
4294
+ "fieldName": "maxlength"
4295
+ },
4296
+ {
4297
+ "name": "pattern",
4298
+ "type": {
4299
+ "text": "string | undefined"
4300
+ },
4301
+ "description": "Pattern for validation (regex)",
4302
+ "fieldName": "pattern"
4303
+ },
4304
+ {
4305
+ "name": "autocomplete",
4306
+ "type": {
4307
+ "text": "string"
4308
+ },
4309
+ "default": "''",
4310
+ "description": "Autocomplete attribute value",
4311
+ "fieldName": "autocomplete"
4312
+ },
4337
4313
  {
4338
4314
  "name": "hint",
4339
4315
  "type": {
4340
4316
  "text": "string"
4341
4317
  },
4342
4318
  "default": "''",
4343
- "description": "Helper text shown below the select",
4319
+ "description": "Helper text shown below the input",
4344
4320
  "fieldName": "hint"
4345
4321
  }
4346
4322
  ],
@@ -4348,25 +4324,25 @@
4348
4324
  "name": "LitElement",
4349
4325
  "package": "lit"
4350
4326
  },
4351
- "tagName": "kr-select",
4327
+ "tagName": "kr-text-field",
4352
4328
  "customElement": true
4353
4329
  }
4354
4330
  ],
4355
4331
  "exports": [
4356
4332
  {
4357
4333
  "kind": "js",
4358
- "name": "KRSelect",
4334
+ "name": "KRTextField",
4359
4335
  "declaration": {
4360
- "name": "KRSelect",
4361
- "module": "src/form/select/select.ts"
4336
+ "name": "KRTextField",
4337
+ "module": "src/form/text-field/text-field.ts"
4362
4338
  }
4363
4339
  },
4364
4340
  {
4365
4341
  "kind": "custom-element-definition",
4366
- "name": "kr-select",
4342
+ "name": "kr-text-field",
4367
4343
  "declaration": {
4368
- "name": "KRSelect",
4369
- "module": "src/form/select/select.ts"
4344
+ "name": "KRTextField",
4345
+ "module": "src/form/text-field/text-field.ts"
4370
4346
  }
4371
4347
  }
4372
4348
  ]