@kodaris/krubble-components 1.0.51 → 1.0.53

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.
@@ -11,21 +11,25 @@ import { styleMap } from 'lit/directives/style-map.js';
11
11
  import { krBaseCSS } from '../style/base.js';
12
12
  import '../button/button.js';
13
13
  import { KRSnackbar } from '../snackbar/snackbar.js';
14
- // === Solr Utilities ===
15
- const SOLR_RESERVED_CHARS = [
16
- '"', '+', '-', '&&', '||', '!', '(', ')', '{',
17
- '}', '[', ']', '^', '~', '*', '?', ':'
18
- ];
19
- const SOLR_RESERVED_CHARS_REPLACEMENT = [
20
- '\\"', '\\+', '\\-', '\\&\\&', '\\|\\|', '\\!', '\\(', '\\)', '\\{',
21
- '\\}', '\\[', '\\]', '\\^', '\\~', '\\*', '\\?', '\\:'
22
- ];
23
- function escapeSolrQuery(query) {
24
- let escaped = query;
25
- for (let i = 0; i < SOLR_RESERVED_CHARS.length; i++) {
26
- escaped = escaped.split(SOLR_RESERVED_CHARS[i]).join(SOLR_RESERVED_CHARS_REPLACEMENT[i]);
27
- }
28
- return escaped;
14
+ import '../form/select-field/select-field.js';
15
+ import '../form/select-field/select-option.js';
16
+ import { KRQuery, getOperatorsForType, termify } from './query.js';
17
+ import '../tabs/tabs.js';
18
+ import '../tabs/tab.js';
19
+ /** Internal table model built from user-provided def. */
20
+ class KRTableModel {
21
+ constructor() {
22
+ this.title = '';
23
+ this.actions = [];
24
+ this.columns = [];
25
+ this.displayedColumns = [];
26
+ this.data = null;
27
+ this.dataSource = null;
28
+ this.refreshInterval = 0;
29
+ this.pageSize = 0;
30
+ this.rowClickable = false;
31
+ this.rowHref = null;
32
+ }
29
33
  }
30
34
  let KRTable = class KRTable extends LitElement {
31
35
  constructor() {
@@ -47,25 +51,33 @@ let KRTable = class KRTable extends LitElement {
47
51
  this._canScrollRight = false;
48
52
  this._canScrollHorizontal = false;
49
53
  this._columnPickerOpen = false;
50
- this._displayedColumns = [];
54
+ this._filterPanelOpened = null;
55
+ this._filterPanelTab = 'filter';
56
+ this._buckets = new Map();
57
+ this._filterPanelPos = { top: 0, left: 0 };
51
58
  this._resizing = null;
52
59
  this._resizeObserver = null;
53
60
  this._searchPositionLocked = false;
54
- this._def = { columns: [] };
61
+ this._model = new KRTableModel();
55
62
  this.def = { columns: [] };
56
- this._handleClickOutsideColumnPicker = (e) => {
57
- if (!this._columnPickerOpen)
58
- return;
63
+ this._handleClickOutside = (e) => {
59
64
  const path = e.composedPath();
60
- const picker = this.shadowRoot?.querySelector('.column-picker-wrapper');
61
- if (picker && !path.includes(picker)) {
62
- this._columnPickerOpen = false;
65
+ if (this._columnPickerOpen) {
66
+ const picker = this.shadowRoot?.querySelector('.column-picker-wrapper');
67
+ if (picker && !path.includes(picker)) {
68
+ this._columnPickerOpen = false;
69
+ }
70
+ }
71
+ if (this._filterPanelOpened) {
72
+ if (!path.some((el) => el.classList?.contains('filter-panel'))) {
73
+ this._handleFilterApply();
74
+ }
63
75
  }
64
76
  };
65
77
  this._handleResizeMove = (e) => {
66
78
  if (!this._resizing)
67
79
  return;
68
- const col = this._def.columns.find(c => c.id === this._resizing.columnId);
80
+ const col = this._model.columns.find(c => c.id === this._resizing.columnId);
69
81
  if (col) {
70
82
  const newWidth = this._resizing.startWidth + (e.clientX - this._resizing.startX);
71
83
  col.width = `${Math.min(900, Math.max(50, newWidth))}px`;
@@ -84,7 +96,7 @@ let KRTable = class KRTable extends LitElement {
84
96
  this.classList.toggle('kr-table--scroll-edge', this._scrollStyle === 'edge');
85
97
  this._fetch();
86
98
  this._initRefresh();
87
- document.addEventListener('click', this._handleClickOutsideColumnPicker);
99
+ document.addEventListener('click', this._handleClickOutside);
88
100
  this._resizeObserver = new ResizeObserver(() => {
89
101
  // Unlock and recalculate on resize since layout changes
90
102
  this._searchPositionLocked = false;
@@ -95,22 +107,71 @@ let KRTable = class KRTable extends LitElement {
95
107
  disconnectedCallback() {
96
108
  super.disconnectedCallback();
97
109
  clearInterval(this._refreshTimer);
98
- document.removeEventListener('click', this._handleClickOutsideColumnPicker);
110
+ document.removeEventListener('click', this._handleClickOutside);
99
111
  this._resizeObserver?.disconnect();
100
112
  }
101
113
  willUpdate(changedProperties) {
102
114
  if (changedProperties.has('def')) {
103
- // Copy user's def and normalize action columns
104
- this._def = {
105
- ...this.def,
106
- columns: this.def.columns.map(col => {
107
- if (col.type === 'actions') {
108
- return { ...col, label: col.label ?? '', sticky: 'right', resizable: false };
115
+ // Build internal model from user-provided def
116
+ this._model = new KRTableModel();
117
+ if (this.def.title) {
118
+ this._model.title = this.def.title;
119
+ }
120
+ if (this.def.actions) {
121
+ this._model.actions = this.def.actions;
122
+ }
123
+ if (this.def.data) {
124
+ this._model.data = this.def.data;
125
+ }
126
+ if (this.def.dataSource) {
127
+ this._model.dataSource = this.def.dataSource;
128
+ }
129
+ if (typeof this.def.refreshInterval === 'number') {
130
+ this._model.refreshInterval = this.def.refreshInterval;
131
+ }
132
+ if (typeof this.def.pageSize === 'number') {
133
+ this._model.pageSize = this.def.pageSize;
134
+ }
135
+ if (this.def.rowClickable) {
136
+ this._model.rowClickable = this.def.rowClickable;
137
+ }
138
+ if (this.def.rowHref) {
139
+ this._model.rowHref = this.def.rowHref;
140
+ }
141
+ this._model.columns = this.def.columns.map(col => {
142
+ const column = {
143
+ ...col,
144
+ filter: null
145
+ };
146
+ if (!column.type) {
147
+ column.type = 'string';
148
+ }
149
+ if (column.type === 'actions') {
150
+ column.label = col.label ?? '';
151
+ column.sticky = 'right';
152
+ column.resizable = false;
153
+ return column;
154
+ }
155
+ if (col.filterable || col.facetable) {
156
+ column.filter = new KRQuery();
157
+ column.filter.field = col.id;
158
+ column.filter.type = column.type;
159
+ if (col.facetable && !col.filterable) {
160
+ column.filter.operator = 'in';
161
+ column.filter.value = [];
162
+ }
163
+ else if (column.filter.type === 'string') {
164
+ column.filter.operator = 'contains';
109
165
  }
110
- return { ...col };
111
- })
112
- };
113
- this._displayedColumns = this._def.displayedColumns || this._def.columns.map(c => c.id);
166
+ }
167
+ return column;
168
+ });
169
+ if (this.def.displayedColumns) {
170
+ this._model.displayedColumns = this.def.displayedColumns;
171
+ }
172
+ else {
173
+ this._model.displayedColumns = this._model.columns.map(c => c.id);
174
+ }
114
175
  this._fetch();
115
176
  this._initRefresh();
116
177
  }
@@ -175,6 +236,73 @@ let KRTable = class KRTable extends LitElement {
175
236
  // ----------------------------------------------------------------------------
176
237
  // Data Fetching
177
238
  // ----------------------------------------------------------------------------
239
+ _toSolrData() {
240
+ const request = {
241
+ page: this._page - 1,
242
+ size: this._pageSize,
243
+ sorts: [],
244
+ filterFields: [],
245
+ queryFields: [],
246
+ facetFields: []
247
+ };
248
+ for (const col of this._model.columns) {
249
+ if (!col.filter || col.filter.isEmpty() || !col.filter.isValid()) {
250
+ continue;
251
+ }
252
+ const filterData = col.filter.toSolrData();
253
+ if (col.facetable && col.filter.operator === 'in') {
254
+ filterData.tagged = true;
255
+ }
256
+ request.filterFields.push(filterData);
257
+ }
258
+ for (const col of this._model.columns) {
259
+ if (!col.facetable) {
260
+ continue;
261
+ }
262
+ request.facetFields.push({
263
+ name: col.id,
264
+ type: 'FIELD',
265
+ limit: 100,
266
+ sort: 'count',
267
+ minimumCount: 1
268
+ });
269
+ }
270
+ if (this._searchQuery?.trim().length) {
271
+ request.queryFields.push({
272
+ name: '_text_',
273
+ operation: 'IS',
274
+ value: termify(this._searchQuery, false)
275
+ });
276
+ }
277
+ return request;
278
+ }
279
+ _toDbParams() {
280
+ const request = {
281
+ page: this._page - 1,
282
+ size: this._pageSize,
283
+ sorts: [],
284
+ filterFields: [],
285
+ queryFields: [],
286
+ facetFields: []
287
+ };
288
+ for (const col of this._model.columns) {
289
+ if (!col.filter || col.filter.isEmpty() || !col.filter.isValid()) {
290
+ continue;
291
+ }
292
+ request.filterFields.push(col.filter.toDbParams());
293
+ }
294
+ if (this._searchQuery?.trim().length) {
295
+ this._model.columns.filter(col => col.searchable).forEach(col => {
296
+ request.queryFields.push({
297
+ name: col.id,
298
+ operation: 'CONTAINS',
299
+ value: this._searchQuery,
300
+ and: false
301
+ });
302
+ });
303
+ }
304
+ return request;
305
+ }
178
306
  /**
179
307
  * Fetches data from the API and updates the table.
180
308
  * Shows a loading spinner while fetching, then displays rows on success
@@ -182,55 +310,27 @@ let KRTable = class KRTable extends LitElement {
182
310
  * Request/response format depends on dataSource.mode (solr, opensearch, db).
183
311
  */
184
312
  _fetch() {
185
- if (!this._def.dataSource)
313
+ if (this._model.data) {
314
+ this._data = this._model.data;
315
+ this._totalItems = this._model.data.length;
316
+ this._totalPages = Math.ceil(this._model.data.length / this._pageSize);
317
+ this._dataState = 'success';
318
+ return;
319
+ }
320
+ if (!this._model.dataSource)
186
321
  return;
187
322
  this._dataState = 'loading';
188
- // Build request based on mode
189
323
  let request;
190
- switch (this._def.dataSource.mode) {
191
- case 'opensearch':
192
- throw Error('Opensearch not supported yet');
193
- case 'db':
194
- request = {
195
- page: this._page - 1,
196
- size: this._pageSize,
197
- sorts: [],
198
- filterFields: [],
199
- queryFields: [],
200
- facetFields: []
201
- };
202
- if (this._searchQuery?.trim().length) {
203
- this._def.columns.filter(col => col.searchable).forEach(col => {
204
- request.queryFields.push({
205
- name: col.id,
206
- operation: 'CONTAINS',
207
- value: this._searchQuery,
208
- and: false
209
- });
210
- });
211
- }
212
- break;
213
- default: // solr
214
- request = {
215
- page: this._page - 1,
216
- size: this._pageSize,
217
- sorts: [],
218
- filterFields: [],
219
- queryFields: [],
220
- facetFields: []
221
- };
222
- if (this._searchQuery?.trim().length) {
223
- request.queryFields.push({
224
- name: '_text_',
225
- operation: 'IS',
226
- value: escapeSolrQuery(this._searchQuery)
227
- });
228
- }
324
+ if (this._model.dataSource.mode === 'db') {
325
+ request = this._toDbParams();
326
+ }
327
+ else {
328
+ request = this._toSolrData();
229
329
  }
230
- this._def.dataSource.fetch(request)
330
+ this._model.dataSource.fetch(request)
231
331
  .then(response => {
232
332
  // Parse response based on mode
233
- switch (this._def.dataSource?.mode) {
333
+ switch (this._model.dataSource?.mode) {
234
334
  case 'opensearch': {
235
335
  throw Error('Opensearch not supported yet');
236
336
  break;
@@ -249,6 +349,7 @@ let KRTable = class KRTable extends LitElement {
249
349
  this._totalItems = res.data.totalElements;
250
350
  this._totalPages = res.data.totalPages;
251
351
  this._pageSize = res.data.size;
352
+ this._parseFacetResults(res);
252
353
  }
253
354
  }
254
355
  this._dataState = 'success';
@@ -262,6 +363,61 @@ let KRTable = class KRTable extends LitElement {
262
363
  });
263
364
  });
264
365
  }
366
+ _parseFacetResults(response) {
367
+ if (!response.data.facetFields) {
368
+ return;
369
+ }
370
+ for (const col of this._model.columns) {
371
+ if (!col.facetable) {
372
+ continue;
373
+ }
374
+ const rawBuckets = response.data.facetFields[col.id];
375
+ if (!rawBuckets) {
376
+ this._buckets.set(col.id, []);
377
+ continue;
378
+ }
379
+ const buckets = [];
380
+ for (const raw of rawBuckets) {
381
+ // Solr returns boolean facet values as strings — coerce to actual booleans
382
+ // so they match the filter values stored by toggle().
383
+ let val = raw.name;
384
+ if (col.type === 'boolean' && typeof raw.name === 'string') {
385
+ if (raw.name === 'true') {
386
+ val = true;
387
+ }
388
+ else if (raw.name === 'false') {
389
+ val = false;
390
+ }
391
+ }
392
+ if (raw.name === null && raw.count > 0) {
393
+ buckets.unshift({
394
+ val: null,
395
+ count: raw.count
396
+ });
397
+ }
398
+ if (raw.name !== null) {
399
+ buckets.push({
400
+ val: val,
401
+ count: raw.count
402
+ });
403
+ }
404
+ }
405
+ // Bucket sync: ensure selected values appear even with 0 results
406
+ if (col.filter && col.filter.operator === 'in' && Array.isArray(col.filter.value)) {
407
+ for (const selectedVal of col.filter.value) {
408
+ if (!buckets.some(b => b.val === selectedVal)) {
409
+ buckets.push({
410
+ val: selectedVal,
411
+ count: 0
412
+ });
413
+ }
414
+ }
415
+ }
416
+ this._buckets.set(col.id, buckets);
417
+ }
418
+ // Trigger re-render since Map mutation doesn't trigger Lit updates
419
+ this._buckets = new Map(this._buckets);
420
+ }
265
421
  /**
266
422
  * Sets up auto-refresh so the table automatically fetches fresh data
267
423
  * at a regular interval (useful for dashboards, monitoring views).
@@ -269,10 +425,10 @@ let KRTable = class KRTable extends LitElement {
269
425
  */
270
426
  _initRefresh() {
271
427
  clearInterval(this._refreshTimer);
272
- if (this._def.refreshInterval && this._def.refreshInterval > 0) {
428
+ if (this._model.refreshInterval && this._model.refreshInterval > 0) {
273
429
  this._refreshTimer = window.setInterval(() => {
274
430
  this._fetch();
275
- }, this._def.refreshInterval);
431
+ }, this._model.refreshInterval);
276
432
  }
277
433
  }
278
434
  _handleSearch(e) {
@@ -331,23 +487,23 @@ let KRTable = class KRTable extends LitElement {
331
487
  this._columnPickerOpen = !this._columnPickerOpen;
332
488
  }
333
489
  _toggleColumn(columnId) {
334
- if (this._displayedColumns.includes(columnId)) {
335
- this._displayedColumns = this._displayedColumns.filter(id => id !== columnId);
490
+ if (this._model.displayedColumns.includes(columnId)) {
491
+ this._model.displayedColumns = this._model.displayedColumns.filter(id => id !== columnId);
336
492
  }
337
493
  else {
338
- this._displayedColumns = [...this._displayedColumns, columnId];
494
+ this._model.displayedColumns = [...this._model.displayedColumns, columnId];
339
495
  }
340
496
  }
341
497
  // Clear any existing text selection on mousedown so we only detect
342
498
  // selections made during this click gesture, not stale selections from elsewhere
343
499
  _handleRowMouseDown() {
344
- if (!this._def.rowClickable) {
500
+ if (!this._model.rowClickable) {
345
501
  return;
346
502
  }
347
503
  window.getSelection()?.removeAllRanges();
348
504
  }
349
505
  _handleRowClick(row, rowIndex) {
350
- if (!this._def.rowClickable) {
506
+ if (!this._model.rowClickable) {
351
507
  return;
352
508
  }
353
509
  const selection = window.getSelection();
@@ -366,8 +522,8 @@ let KRTable = class KRTable extends LitElement {
366
522
  // back to its original position in the column definition.
367
523
  // Actions columns are always moved to the end.
368
524
  getDisplayedColumns() {
369
- return this._displayedColumns
370
- .map(id => this._def.columns.find(col => col.id === id))
525
+ return this._model.displayedColumns
526
+ .map(id => this._model.columns.find(col => col.id === id))
371
527
  .sort((a, b) => {
372
528
  if (a.type === 'actions' && b.type !== 'actions')
373
529
  return 1;
@@ -436,6 +592,125 @@ let KRTable = class KRTable extends LitElement {
436
592
  }));
437
593
  }
438
594
  // ----------------------------------------------------------------------------
595
+ // Filter Handlers
596
+ // ----------------------------------------------------------------------------
597
+ _handleKqlChange(e, column) {
598
+ const kql = e.target.value.trim();
599
+ if (!kql) {
600
+ column.filter.clear();
601
+ this.requestUpdate();
602
+ }
603
+ else {
604
+ column.filter.setKql(kql);
605
+ this.requestUpdate();
606
+ if (!column.filter.isValid()) {
607
+ return;
608
+ }
609
+ }
610
+ this._page = 1;
611
+ this._fetch();
612
+ }
613
+ _handleFilterPanelToggle(e, column) {
614
+ e.stopPropagation();
615
+ if (this._filterPanelOpened === column.id) {
616
+ this._filterPanelOpened = null;
617
+ }
618
+ else {
619
+ const rect = e.currentTarget.getBoundingClientRect();
620
+ this._filterPanelPos = { top: rect.bottom + 4, left: rect.left };
621
+ this._filterPanelOpened = column.id;
622
+ if (column.facetable) {
623
+ this._filterPanelTab = 'counts';
624
+ }
625
+ else {
626
+ this._filterPanelTab = 'filter';
627
+ }
628
+ }
629
+ }
630
+ _handleKqlClear(column) {
631
+ column.filter.clear();
632
+ this._page = 1;
633
+ this._fetch();
634
+ }
635
+ _handleFilterClear() {
636
+ const column = this._model.columns.find(c => c.id === this._filterPanelOpened);
637
+ if (column) {
638
+ column.filter.clear();
639
+ if (column.facetable && !column.filterable) {
640
+ column.filter.operator = 'in';
641
+ column.filter.value = [];
642
+ }
643
+ }
644
+ this._filterPanelOpened = null;
645
+ this._page = 1;
646
+ this._fetch();
647
+ }
648
+ _handleFilterTextKeydown(e, column) {
649
+ if (e.key === 'Enter') {
650
+ e.preventDefault();
651
+ this._handleFilterApply();
652
+ }
653
+ }
654
+ _handleOperatorChange(e, column) {
655
+ column.filter.setOperator(e.target.value);
656
+ this.requestUpdate();
657
+ }
658
+ _handleFilterStringChange(e, column) {
659
+ column.filter.setValue(e.target.value);
660
+ this.requestUpdate();
661
+ }
662
+ _handleFilterNumberChange(e, column) {
663
+ column.filter.setValue(Number(e.target.value));
664
+ this.requestUpdate();
665
+ }
666
+ _handleFilterDateChange(e, column) {
667
+ column.filter.setValue(new Date(e.target.value), 'day');
668
+ this.requestUpdate();
669
+ }
670
+ _handleFilterBooleanChange(e, column) {
671
+ column.filter.setValue(e.target.value === 'true');
672
+ this.requestUpdate();
673
+ }
674
+ _handleFilterDateStartChange(e, column) {
675
+ column.filter.setStart(new Date(e.target.value), 'day');
676
+ this.requestUpdate();
677
+ }
678
+ _handleFilterDateEndChange(e, column) {
679
+ column.filter.setEnd(new Date(e.target.value), 'day');
680
+ this.requestUpdate();
681
+ }
682
+ _handleFilterNumberStartChange(e, column) {
683
+ column.filter.setStart(Number(e.target.value));
684
+ this.requestUpdate();
685
+ }
686
+ _handleFilterNumberEndChange(e, column) {
687
+ column.filter.setEnd(Number(e.target.value));
688
+ this.requestUpdate();
689
+ }
690
+ _handleFilterListChange(e, column) {
691
+ const items = e.target.value.split(',').map((v) => v.trim()).filter((v) => v !== '');
692
+ if (column.type === 'number') {
693
+ column.filter.setValue(items.map((v) => Number(v)));
694
+ }
695
+ else {
696
+ column.filter.setValue(items);
697
+ }
698
+ this.requestUpdate();
699
+ }
700
+ _handleFilterApply() {
701
+ this._filterPanelOpened = null;
702
+ this._page = 1;
703
+ this._fetch();
704
+ }
705
+ _handleFilterPanelTabChange(e) {
706
+ this._filterPanelTab = e.detail.activeTabId;
707
+ }
708
+ _handleBucketToggle(e, column, bucket) {
709
+ column.filter.toggle(bucket.val);
710
+ this._page = 1;
711
+ this._fetch();
712
+ }
713
+ // ----------------------------------------------------------------------------
439
714
  // Rendering
440
715
  // ----------------------------------------------------------------------------
441
716
  _renderCellContent(column, row, rowIndex) {
@@ -449,11 +724,10 @@ let KRTable = class KRTable extends LitElement {
449
724
  }
450
725
  switch (column.type) {
451
726
  case 'number':
452
- return typeof value === 'number' ? value.toLocaleString() : String(value);
453
- case 'currency':
454
- return typeof value === 'number'
455
- ? value.toLocaleString('en-US', { style: 'currency', currency: 'USD' })
456
- : String(value);
727
+ if (column.format === 'currency' && typeof value === 'number') {
728
+ return value.toLocaleString('en-US', { style: 'currency', currency: 'USD' });
729
+ }
730
+ return String(value);
457
731
  case 'date': {
458
732
  let date;
459
733
  if (value instanceof Date) {
@@ -468,10 +742,11 @@ let KRTable = class KRTable extends LitElement {
468
742
  else {
469
743
  date = new Date(value);
470
744
  }
471
- // Show date and time for datetime values
745
+ // Show date and time for datetime values in UTC
472
746
  return date.toLocaleString(undefined, {
473
747
  year: 'numeric', month: 'short', day: 'numeric',
474
- hour: 'numeric', minute: '2-digit'
748
+ hour: 'numeric', minute: '2-digit',
749
+ timeZone: 'UTC'
475
750
  });
476
751
  }
477
752
  case 'boolean':
@@ -588,13 +863,13 @@ let KRTable = class KRTable extends LitElement {
588
863
  * Hidden when there's no title, no actions, and data fits on one page.
589
864
  */
590
865
  _renderHeader() {
591
- if (!this._def.title && !this._def.actions?.length && this._totalPages <= 1) {
866
+ if (!this._model.title && !this._model.actions?.length && this._totalPages <= 1) {
592
867
  return nothing;
593
868
  }
594
869
  return html `
595
870
  <div class="header">
596
- <div class="title">${this._def.title ?? ''}</div>
597
- ${this._def.dataSource?.mode === 'db' && !this._def.columns.some(col => col.searchable) ? html `<div class="search"></div>` : html `
871
+ <div class="title">${this._model.title ?? ''}</div>
872
+ ${this._model.dataSource?.mode === 'db' && !this._model.columns.some(col => col.searchable) ? html `<div class="search"></div>` : html `
598
873
  <div class="search">
599
874
  <!-- TODO: Saved views dropdown
600
875
  <div class="views">
@@ -624,9 +899,9 @@ let KRTable = class KRTable extends LitElement {
624
899
  <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>
625
900
  </span>
626
901
  <div class="column-picker ${this._columnPickerOpen ? 'open' : ''}">
627
- ${[...this._def.columns].filter(col => col.type !== 'actions').sort((a, b) => (a.label ?? a.id).localeCompare(b.label ?? b.id)).map(col => html `
902
+ ${[...this._model.columns].filter(col => col.type !== 'actions').sort((a, b) => (a.label ?? a.id).localeCompare(b.label ?? b.id)).map(col => html `
628
903
  <div class="column-picker-item" @click=${() => this._toggleColumn(col.id)}>
629
- <div class="column-picker-checkbox ${this._displayedColumns.includes(col.id) ? 'checked' : ''}">
904
+ <div class="column-picker-checkbox ${this._model.displayedColumns.includes(col.id) ? 'checked' : ''}">
630
905
  <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>
631
906
  </div>
632
907
  <span class="column-picker-label">${col.label ?? col.id}</span>
@@ -634,19 +909,19 @@ let KRTable = class KRTable extends LitElement {
634
909
  `)}
635
910
  </div>
636
911
  </div>
637
- ${this._def.actions?.length === 1 ? html `
912
+ ${this._model.actions?.length === 1 ? html `
638
913
  <kr-button
639
914
  class="actions"
640
- .href=${this._def.actions[0].href}
641
- .target=${this._def.actions[0].target}
642
- @click=${() => this._handleAction(this._def.actions[0])}
915
+ .href=${this._model.actions[0].href}
916
+ .target=${this._model.actions[0].target}
917
+ @click=${() => this._handleAction(this._model.actions[0])}
643
918
  >
644
- ${this._def.actions[0].label}
919
+ ${this._model.actions[0].label}
645
920
  </kr-button>
646
- ` : this._def.actions?.length ? html `
921
+ ` : this._model.actions?.length ? html `
647
922
  <kr-button
648
923
  class="actions"
649
- .options=${this._def.actions.map(a => ({ id: a.id, label: a.label }))}
924
+ .options=${this._model.actions.map(a => ({ id: a.id, label: a.label }))}
650
925
  @option-select=${(e) => this._handleAction({ id: e.detail.id, label: e.detail.label })}
651
926
  >
652
927
  Actions
@@ -669,6 +944,299 @@ let KRTable = class KRTable extends LitElement {
669
944
  }
670
945
  return nothing;
671
946
  }
947
+ _renderFilterPanel() {
948
+ if (!this._filterPanelOpened) {
949
+ return nothing;
950
+ }
951
+ const column = this._model.columns.find(c => c.id === this._filterPanelOpened);
952
+ // Build filter content (operator + value input)
953
+ let valueInput = html ``;
954
+ if (column.filter.operator === 'empty' || column.filter.operator === 'n_empty') {
955
+ valueInput = html `
956
+ <input
957
+ type="text"
958
+ class="filter-panel__input"
959
+ disabled
960
+ .value=${column.filter.text}
961
+ />
962
+ `;
963
+ }
964
+ else if (column.filter.operator === 'between' && column.type === 'date') {
965
+ valueInput = html `
966
+ <input
967
+ type="date"
968
+ class="filter-panel__input"
969
+ .valueAsDate=${column.filter.value?.start ?? null}
970
+ @change=${(e) => this._handleFilterDateStartChange(e, column)}
971
+ />
972
+ <input
973
+ type="date"
974
+ class="filter-panel__input"
975
+ .valueAsDate=${column.filter.value?.end ?? null}
976
+ @change=${(e) => this._handleFilterDateEndChange(e, column)}
977
+ />
978
+ `;
979
+ }
980
+ else if (column.filter.operator === 'between' && column.type === 'number') {
981
+ valueInput = html `
982
+ <input
983
+ type="number"
984
+ class="filter-panel__input"
985
+ placeholder="Start"
986
+ .value=${column.filter.value?.start ?? ''}
987
+ @input=${(e) => this._handleFilterNumberStartChange(e, column)}
988
+ @keydown=${(e) => this._handleFilterTextKeydown(e, column)}
989
+ />
990
+ <input
991
+ type="number"
992
+ class="filter-panel__input"
993
+ placeholder="End"
994
+ .value=${column.filter.value?.end ?? ''}
995
+ @input=${(e) => this._handleFilterNumberEndChange(e, column)}
996
+ @keydown=${(e) => this._handleFilterTextKeydown(e, column)}
997
+ />
998
+ `;
999
+ }
1000
+ else if (column.filter.operator === 'in') {
1001
+ valueInput = html `
1002
+ <textarea
1003
+ class="filter-panel__textarea"
1004
+ rows="3"
1005
+ placeholder="Values (comma-separated)"
1006
+ .value=${column.filter.text}
1007
+ @input=${(e) => this._handleFilterListChange(e, column)}
1008
+ @keydown=${(e) => this._handleFilterTextKeydown(e, column)}
1009
+ ></textarea>
1010
+ `;
1011
+ }
1012
+ else if (column.type === 'boolean') {
1013
+ valueInput = html `
1014
+ <kr-select-field
1015
+ placeholder="Value"
1016
+ .value=${String(column.filter.value ?? '')}
1017
+ @change=${(e) => this._handleFilterBooleanChange(e, column)}
1018
+ >
1019
+ <kr-select-option value="true">Yes</kr-select-option>
1020
+ <kr-select-option value="false">No</kr-select-option>
1021
+ </kr-select-field>
1022
+ `;
1023
+ }
1024
+ else if (column.type === 'date') {
1025
+ valueInput = html `
1026
+ <input
1027
+ type="date"
1028
+ class="filter-panel__input"
1029
+ .valueAsDate=${column.filter.value}
1030
+ @change=${(e) => this._handleFilterDateChange(e, column)}
1031
+ />
1032
+ `;
1033
+ }
1034
+ else if (column.type === 'number') {
1035
+ valueInput = html `
1036
+ <input
1037
+ type="number"
1038
+ class="filter-panel__input"
1039
+ placeholder="Value"
1040
+ min="0"
1041
+ .value=${column.filter.text}
1042
+ @input=${(e) => this._handleFilterNumberChange(e, column)}
1043
+ @keydown=${(e) => this._handleFilterTextKeydown(e, column)}
1044
+ />
1045
+ `;
1046
+ }
1047
+ else {
1048
+ valueInput = html `
1049
+ <input
1050
+ type="text"
1051
+ class="filter-panel__input"
1052
+ placeholder="Value"
1053
+ .value=${column.filter.text}
1054
+ @input=${(e) => this._handleFilterStringChange(e, column)}
1055
+ @keydown=${(e) => this._handleFilterTextKeydown(e, column)}
1056
+ />
1057
+ `;
1058
+ }
1059
+ const filterContent = html `
1060
+ <div class="filter-panel__content">
1061
+ <kr-select-field
1062
+ .value=${column.filter.operator}
1063
+ @change=${(e) => this._handleOperatorChange(e, column)}
1064
+ >
1065
+ ${getOperatorsForType(column.type).map(op => html `
1066
+ <kr-select-option value=${op.key}>${op.label}</kr-select-option>
1067
+ `)}
1068
+ </kr-select-field>
1069
+ ${valueInput}
1070
+ </div>
1071
+ `;
1072
+ // Build bucket list content
1073
+ const buckets = this._buckets.get(column.id) || [];
1074
+ let bucketContent;
1075
+ if (!buckets.length) {
1076
+ bucketContent = html `<div class="bucket-empty">No data</div>`;
1077
+ }
1078
+ else {
1079
+ bucketContent = html `
1080
+ <div class="buckets">
1081
+ ${buckets.map(bucket => {
1082
+ let bucketLabel = '(Empty)';
1083
+ if (bucket.val !== null && bucket.val !== undefined) {
1084
+ if (column.type === 'boolean') {
1085
+ if (bucket.val === true || bucket.val === 'true') {
1086
+ bucketLabel = 'Yes';
1087
+ }
1088
+ else {
1089
+ bucketLabel = 'No';
1090
+ }
1091
+ }
1092
+ else {
1093
+ bucketLabel = String(bucket.val);
1094
+ }
1095
+ }
1096
+ let checkIcon = nothing;
1097
+ if (column.filter.has(bucket.val)) {
1098
+ checkIcon = html `
1099
+ <svg viewBox="0 0 24 24" fill="currentColor">
1100
+ <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
1101
+ </svg>
1102
+ `;
1103
+ }
1104
+ return html `
1105
+ <div
1106
+ class="bucket"
1107
+ @click=${(e) => this._handleBucketToggle(e, column, bucket)}
1108
+ >
1109
+ <div class=${classMap({
1110
+ 'bucket__checkbox': true,
1111
+ 'bucket__checkbox--checked': column.filter.has(bucket.val)
1112
+ })}>
1113
+ ${checkIcon}
1114
+ </div>
1115
+ <span class="bucket__label">${bucketLabel}</span>
1116
+ <span class="bucket__count">${bucket.count}</span>
1117
+ </div>
1118
+ `;
1119
+ })}
1120
+ </div>
1121
+ `;
1122
+ }
1123
+ // Build panel body — tabs if both filterable+facetable, otherwise just the relevant content
1124
+ let panelBody;
1125
+ if (column.facetable && column.filterable) {
1126
+ panelBody = html `
1127
+ <kr-tab-group
1128
+ size="small"
1129
+ active-tab-id=${this._filterPanelTab}
1130
+ @tab-change=${(e) => this._handleFilterPanelTabChange(e)}
1131
+ >
1132
+ <kr-tab id="filter" label="Filter">
1133
+ ${filterContent}
1134
+ </kr-tab>
1135
+ <kr-tab id="counts" label="Counts">
1136
+ ${bucketContent}
1137
+ </kr-tab>
1138
+ </kr-tab-group>
1139
+ `;
1140
+ }
1141
+ else if (column.facetable) {
1142
+ panelBody = bucketContent;
1143
+ }
1144
+ else {
1145
+ panelBody = filterContent;
1146
+ }
1147
+ return html `
1148
+ <div
1149
+ class="filter-panel"
1150
+ style=${styleMap({
1151
+ top: this._filterPanelPos.top + 'px',
1152
+ left: this._filterPanelPos.left + 'px'
1153
+ })}
1154
+ >
1155
+ ${panelBody}
1156
+ <div class="filter-panel__actions">
1157
+ <kr-button variant="outline" color="secondary" size="small" @click=${this._handleFilterClear}>
1158
+ Clear
1159
+ </kr-button>
1160
+ <kr-button size="small" @click=${this._handleFilterApply}>
1161
+ Apply
1162
+ </kr-button>
1163
+ </div>
1164
+ </div>
1165
+ `;
1166
+ }
1167
+ /**
1168
+ * Renders filter row below column headers.
1169
+ * Only displays for columns with filterable: true.
1170
+ */
1171
+ _renderFilterRow() {
1172
+ const columns = this.getDisplayedColumns();
1173
+ if (!columns.some(col => col.filterable || col.facetable)) {
1174
+ return nothing;
1175
+ }
1176
+ return html `
1177
+ <div class="filter-row">
1178
+ ${columns.map((col, i) => {
1179
+ if (!col.filterable && !col.facetable) {
1180
+ return html `<div
1181
+ class=${classMap({
1182
+ 'filter-cell': true,
1183
+ 'filter-cell--sticky-left': col.sticky === 'left',
1184
+ 'filter-cell--sticky-right': col.sticky === 'right',
1185
+ 'filter-cell--sticky-right-first': col.sticky === 'right' &&
1186
+ !columns.slice(0, i).some((c) => c.sticky === 'right')
1187
+ })}
1188
+ style=${styleMap(this._getCellStyle(col, i))}
1189
+ ></div>`;
1190
+ }
1191
+ return html `
1192
+ <div
1193
+ class=${classMap({
1194
+ 'filter-cell': true,
1195
+ 'filter-cell--sticky-left': col.sticky === 'left',
1196
+ 'filter-cell--sticky-right': col.sticky === 'right',
1197
+ 'filter-cell--sticky-right-first': col.sticky === 'right' &&
1198
+ !columns.slice(0, i).some((c) => c.sticky === 'right')
1199
+ })}
1200
+ style=${styleMap(this._getCellStyle(col, i))}
1201
+ >
1202
+ <div class="filter-cell__wrapper">
1203
+ <input
1204
+ type="text"
1205
+ class=${classMap({
1206
+ 'filter-cell__input': true,
1207
+ 'filter-cell__input--invalid': !col.filter.isValid()
1208
+ })}
1209
+ .value=${col.filter.kql}
1210
+ @change=${(e) => this._handleKqlChange(e, col)}
1211
+ />
1212
+ ${col.filter?.kql?.length > 0 ? html `
1213
+ <button
1214
+ class="filter-cell__clear"
1215
+ @click=${() => this._handleKqlClear(col)}
1216
+ >
1217
+ <svg viewBox="0 0 24 24" fill="currentColor">
1218
+ <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
1219
+ </svg>
1220
+ </button>
1221
+ ` : nothing}
1222
+ <button
1223
+ class=${classMap({
1224
+ 'filter-cell__advanced': true,
1225
+ 'filter-cell__advanced--opened': this._filterPanelOpened === col.id
1226
+ })}
1227
+ @click=${(e) => this._handleFilterPanelToggle(e, col)}
1228
+ >
1229
+ <svg viewBox="0 0 24 24" fill="currentColor">
1230
+ <path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>
1231
+ </svg>
1232
+ </button>
1233
+ </div>
1234
+ </div>
1235
+ `;
1236
+ })}
1237
+ </div>
1238
+ `;
1239
+ }
672
1240
  /** Renders the scrollable data grid with column headers and rows. */
673
1241
  _renderTable() {
674
1242
  return html `
@@ -690,6 +1258,7 @@ let KRTable = class KRTable extends LitElement {
690
1258
  ></div>` : nothing}</div>
691
1259
  `)}
692
1260
  </div>
1261
+ ${this._renderFilterRow()}
693
1262
  ${this._data.map((row, rowIndex) => {
694
1263
  const cells = this.getDisplayedColumns().map((col, i) => html `
695
1264
  <div
@@ -700,10 +1269,10 @@ let KRTable = class KRTable extends LitElement {
700
1269
  ${this._renderCellContent(col, row, rowIndex)}
701
1270
  </div>
702
1271
  `);
703
- if (this._def.rowHref) {
1272
+ if (this._model.rowHref) {
704
1273
  return html `
705
1274
  <a
706
- href=${this._def.rowHref(row)}
1275
+ href=${this._model.rowHref(row)}
707
1276
  class=${classMap({ 'row': true, 'row--clickable': true, 'row--link': true })}
708
1277
  @mousedown=${() => this._handleRowMouseDown()}
709
1278
  @click=${() => this._handleRowClick(row, rowIndex)}
@@ -712,7 +1281,7 @@ let KRTable = class KRTable extends LitElement {
712
1281
  }
713
1282
  return html `
714
1283
  <div
715
- class=${classMap({ 'row': true, 'row--clickable': !!this._def.rowClickable })}
1284
+ class=${classMap({ 'row': true, 'row--clickable': !!this._model.rowClickable })}
716
1285
  @mousedown=${() => this._handleRowMouseDown()}
717
1286
  @click=${() => this._handleRowClick(row, rowIndex)}
718
1287
  >${cells}</div>
@@ -730,12 +1299,13 @@ let KRTable = class KRTable extends LitElement {
730
1299
  * - Loading, error message, or empty state when no data
731
1300
  */
732
1301
  render() {
733
- if (!this._def.columns.length) {
1302
+ if (!this._model.columns.length) {
734
1303
  return html `<slot></slot>`;
735
1304
  }
736
1305
  return html `
737
1306
  ${this._renderHeader()}
738
1307
  ${this._renderTable()}
1308
+ ${this._renderFilterPanel()}
739
1309
  `;
740
1310
  }
741
1311
  };
@@ -1084,6 +1654,7 @@ KRTable.styles = [krBaseCSS, css `
1084
1654
  overflow: hidden;
1085
1655
  text-overflow: ellipsis;
1086
1656
  box-sizing: border-box;
1657
+ border-right: 1px solid #e5e7ebba;
1087
1658
  }
1088
1659
 
1089
1660
  .cell--actions {
@@ -1100,7 +1671,9 @@ KRTable.styles = [krBaseCSS, css `
1100
1671
  white-space: nowrap;
1101
1672
  box-sizing: border-box;
1102
1673
  background: #f9fafb;
1674
+ border-top: 1px solid #e5e7eb;
1103
1675
  border-bottom: 2px solid #e5e7eb;
1676
+ border-right: 1px solid #e5e7ebba;
1104
1677
  font-weight: 600;
1105
1678
  color: #374151;
1106
1679
  overflow: hidden;
@@ -1198,7 +1771,6 @@ KRTable.styles = [krBaseCSS, css `
1198
1771
  * ----------------------------------------------------------------------- */
1199
1772
  :host(.kr-table--scroll-overlay) .content {
1200
1773
  padding-left: 24px;
1201
- padding-right: 24px;
1202
1774
  }
1203
1775
 
1204
1776
  .overlay-left,
@@ -1257,6 +1829,301 @@ KRTable.styles = [krBaseCSS, css `
1257
1829
  .status--error {
1258
1830
  color: #dc2626;
1259
1831
  }
1832
+
1833
+ /* -------------------------------------------------------------------------
1834
+ * Filter Row
1835
+ * ----------------------------------------------------------------------- */
1836
+ .filter-row {
1837
+ display: contents;
1838
+ }
1839
+
1840
+ .filter-cell {
1841
+ position: sticky;
1842
+ top: 48px;
1843
+ z-index: 2;
1844
+ height: 40px;
1845
+ padding: 4px 8px;
1846
+ display: flex;
1847
+ align-items: center;
1848
+ background: #fafbfc;
1849
+ border-bottom: 1px solid #e5e7eb;
1850
+ border-right: 1px solid #e5e7ebba;
1851
+ box-sizing: border-box;
1852
+ }
1853
+
1854
+
1855
+ .filter-cell--sticky-left,
1856
+ .filter-cell--sticky-right {
1857
+ position: sticky;
1858
+ z-index: 3;
1859
+ }
1860
+
1861
+ .filter-cell--sticky-right-first {
1862
+ border-left: 1px solid #d1d5db;
1863
+ }
1864
+
1865
+ .filter-cell__wrapper {
1866
+ position: relative;
1867
+ display: flex;
1868
+ align-items: center;
1869
+ gap: 4px;
1870
+ width: 100%;
1871
+ }
1872
+
1873
+ .filter-cell__input {
1874
+ width: 100%;
1875
+ height: 32px;
1876
+ padding: 0 52px 0 8px;
1877
+ border: 1px solid #d1d5db;
1878
+ border-radius: 6px;
1879
+ font-size: 14px;
1880
+ font-family: inherit;
1881
+ color: #111827;
1882
+ background: #fff;
1883
+ outline: none;
1884
+ transition: border-color 0.2s, box-shadow 0.2s;
1885
+ }
1886
+
1887
+ .filter-cell__input:focus {
1888
+ border-color: #163052;
1889
+ box-shadow: 0 0 0 2px rgba(22, 48, 82, 0.1);
1890
+ }
1891
+
1892
+ .filter-cell__input--invalid {
1893
+ border-color: #dc2626;
1894
+ }
1895
+
1896
+ .filter-cell__input--invalid:focus {
1897
+ border-color: #dc2626;
1898
+ box-shadow: 0 0 0 2px rgba(220, 38, 38, 0.1);
1899
+ }
1900
+
1901
+ .filter-cell__input::placeholder {
1902
+ color: #9ca3af;
1903
+ font-size: 13px;
1904
+ }
1905
+
1906
+ .filter-cell__clear {
1907
+ position: absolute;
1908
+ right: 28px;
1909
+ top: 50%;
1910
+ transform: translateY(-50%);
1911
+ display: flex;
1912
+ align-items: center;
1913
+ justify-content: center;
1914
+ width: 24px;
1915
+ height: 24px;
1916
+ padding: 0;
1917
+ border: none;
1918
+ border-radius: 4px;
1919
+ background: transparent;
1920
+ color: #6b7280;
1921
+ cursor: pointer;
1922
+ transition: background 0.15s, color 0.15s;
1923
+ }
1924
+
1925
+ .filter-cell__clear:hover {
1926
+ background: #e5e7eb;
1927
+ color: #374151;
1928
+ }
1929
+
1930
+ .filter-cell__clear svg {
1931
+ width: 16px;
1932
+ height: 16px;
1933
+ }
1934
+
1935
+ .filter-cell__advanced {
1936
+ position: absolute;
1937
+ right: 4px;
1938
+ top: 50%;
1939
+ transform: translateY(-50%);
1940
+ display: flex;
1941
+ align-items: center;
1942
+ justify-content: center;
1943
+ width: 24px;
1944
+ height: 24px;
1945
+ padding: 0;
1946
+ border: none;
1947
+ border-radius: 4px;
1948
+ background: transparent;
1949
+ color: #163052;
1950
+ cursor: pointer;
1951
+ transition: background 0.15s, color 0.15s;
1952
+ }
1953
+
1954
+ .filter-cell__advanced:hover {
1955
+ background: #e5e7eb;
1956
+ }
1957
+
1958
+ .filter-cell__advanced svg {
1959
+ width: 16px;
1960
+ height: 16px;
1961
+ }
1962
+
1963
+ .filter-cell__advanced--opened {
1964
+ background: #163052;
1965
+ color: #fff;
1966
+ }
1967
+
1968
+ .filter-cell__advanced--opened:hover {
1969
+ background: #1a3a5f;
1970
+ color: #fff;
1971
+ }
1972
+
1973
+ /* -------------------------------------------------------------------------
1974
+ * Filter Panel (Advanced)
1975
+ * ----------------------------------------------------------------------- */
1976
+ .filter-panel {
1977
+ position: fixed;
1978
+ min-width: 320px;
1979
+ background: white;
1980
+ border: 1px solid #9ba7b6;
1981
+ border-radius: 8px;
1982
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
1983
+ z-index: 100;
1984
+ animation: filter-panel-fade-in 150ms ease-out;
1985
+ }
1986
+
1987
+ @keyframes filter-panel-fade-in {
1988
+ from {
1989
+ opacity: 0;
1990
+ transform: translateY(-4px);
1991
+ }
1992
+ to {
1993
+ opacity: 1;
1994
+ transform: translateY(0);
1995
+ }
1996
+ }
1997
+
1998
+ .filter-panel__content {
1999
+ padding: 16px;
2000
+ display: flex;
2001
+ flex-direction: column;
2002
+ gap: 12px;
2003
+ }
2004
+
2005
+ .filter-panel__actions {
2006
+ padding: 12px 16px;
2007
+ border-top: 1px solid #e5e7eb;
2008
+ display: flex;
2009
+ justify-content: flex-end;
2010
+ align-items: center;
2011
+ gap: 12px;
2012
+ }
2013
+
2014
+
2015
+ .filter-panel__input {
2016
+ width: 100%;
2017
+ padding: 10px 12px;
2018
+ border: 1px solid #d1d5db;
2019
+ border-radius: 8px;
2020
+ font-size: 14px;
2021
+ font-family: inherit;
2022
+ color: #111827;
2023
+ background: #fff;
2024
+ outline: none;
2025
+ transition: border-color 0.2s, box-shadow 0.2s;
2026
+ }
2027
+
2028
+ .filter-panel__input:focus {
2029
+ border-color: #163052;
2030
+ box-shadow: 0 0 0 2px rgba(22, 48, 82, 0.1);
2031
+ }
2032
+
2033
+ .filter-panel__input::placeholder {
2034
+ color: #9ca3af;
2035
+ }
2036
+
2037
+ .filter-panel__textarea {
2038
+ width: 100%;
2039
+ padding: 10px 12px;
2040
+ border: 1px solid #d1d5db;
2041
+ border-radius: 8px;
2042
+ font-size: 14px;
2043
+ font-family: inherit;
2044
+ color: #111827;
2045
+ background: #fff;
2046
+ outline: none;
2047
+ resize: vertical;
2048
+ transition: border-color 0.2s, box-shadow 0.2s;
2049
+ }
2050
+
2051
+ .filter-panel__textarea:focus {
2052
+ border-color: #163052;
2053
+ box-shadow: 0 0 0 2px rgba(22, 48, 82, 0.1);
2054
+ }
2055
+
2056
+ .filter-panel__textarea::placeholder {
2057
+ color: #9ca3af;
2058
+ }
2059
+
2060
+ /* -------------------------------------------------------------------------
2061
+ * Bucket List
2062
+ * ----------------------------------------------------------------------- */
2063
+ .buckets {
2064
+ max-height: 280px;
2065
+ overflow-y: auto;
2066
+ padding: 8px 0;
2067
+ }
2068
+
2069
+ .bucket {
2070
+ display: flex;
2071
+ align-items: center;
2072
+ gap: 16px;
2073
+ height: 32px;
2074
+ padding: 0 16px;
2075
+ cursor: pointer;
2076
+ transition: background 0.1s;
2077
+ }
2078
+
2079
+ .bucket:hover {
2080
+ background: #f3f4f6;
2081
+ }
2082
+
2083
+ .bucket__checkbox {
2084
+ width: 16px;
2085
+ height: 16px;
2086
+ border: 1.5px solid #9ca3af;
2087
+ border-radius: 3px;
2088
+ display: flex;
2089
+ align-items: center;
2090
+ justify-content: center;
2091
+ flex-shrink: 0;
2092
+ transition: all 0.15s;
2093
+ }
2094
+
2095
+ .bucket__checkbox--checked {
2096
+ background: var(--kr-primary, #163052);
2097
+ border-color: var(--kr-primary, #163052);
2098
+ }
2099
+
2100
+ .bucket__checkbox svg {
2101
+ width: 12px;
2102
+ height: 12px;
2103
+ color: white;
2104
+ }
2105
+
2106
+ .bucket__label {
2107
+ flex: 1;
2108
+ font-size: 14px;
2109
+ color: #000;
2110
+ overflow: hidden;
2111
+ text-overflow: ellipsis;
2112
+ white-space: nowrap;
2113
+ }
2114
+
2115
+ .bucket__count {
2116
+ font-size: 14px;
2117
+ color: #000;
2118
+ flex-shrink: 0;
2119
+ }
2120
+
2121
+ .bucket-empty {
2122
+ font-size: 14px;
2123
+ color: #000;
2124
+ padding: 16px;
2125
+ }
2126
+
1260
2127
  `];
1261
2128
  __decorate([
1262
2129
  state()
@@ -1293,7 +2160,13 @@ __decorate([
1293
2160
  ], KRTable.prototype, "_columnPickerOpen", void 0);
1294
2161
  __decorate([
1295
2162
  state()
1296
- ], KRTable.prototype, "_displayedColumns", void 0);
2163
+ ], KRTable.prototype, "_filterPanelOpened", void 0);
2164
+ __decorate([
2165
+ state()
2166
+ ], KRTable.prototype, "_filterPanelTab", void 0);
2167
+ __decorate([
2168
+ state()
2169
+ ], KRTable.prototype, "_buckets", void 0);
1297
2170
  __decorate([
1298
2171
  property({ type: Object })
1299
2172
  ], KRTable.prototype, "def", void 0);